feat: add Apply Patch tool with patch input and diff view (#196)
* feat: add Apply Patch tool with patch input and diff view Introduce Apply Patch tool with a patch input field and diff preview Display target file path or file count for patch operations in tool UI Update status label to show applying patch during execution * fix: include apply_patch in editing tools detection
This commit is contained in:
committed by
GitHub
parent
ef4d37df09
commit
87175566b9
@@ -283,6 +283,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
errorMessage,
|
||||
}) => {
|
||||
|
||||
void _streamPhase;
|
||||
void _allowAnimation;
|
||||
const [copyHintVisible, setCopyHintVisible] = React.useState(false);
|
||||
const copyHintTimeoutRef = React.useRef<number | null>(null);
|
||||
|
||||
@@ -39,7 +39,7 @@ const getToolIcon = (toolName: string) => {
|
||||
if (tool === 'image-preview') {
|
||||
return <RiFileImageLine className={iconClass} />;
|
||||
}
|
||||
if (tool === 'edit' || tool === 'multiedit' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
|
||||
if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
|
||||
return <RiPencilAiLine className={iconClass} />;
|
||||
}
|
||||
if (tool === 'write' || tool === 'create' || tool === 'file_write') {
|
||||
@@ -109,7 +109,8 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
{popup.metadata?.input && typeof popup.metadata.input === 'object' &&
|
||||
Object.keys(popup.metadata.input).length > 0 &&
|
||||
popup.metadata?.tool !== 'todowrite' &&
|
||||
popup.metadata?.tool !== 'todoread' ? (() => {
|
||||
popup.metadata?.tool !== 'todoread' &&
|
||||
popup.metadata?.tool !== 'apply_patch' ? (() => {
|
||||
const meta = popup.metadata!;
|
||||
const input = meta.input as Record<string, unknown>;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export const getToolIcon = (toolName: string) => {
|
||||
const iconClass = 'h-3.5 w-3.5 flex-shrink-0';
|
||||
const tool = toolName.toLowerCase();
|
||||
|
||||
if (tool === 'edit' || tool === 'multiedit' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
|
||||
if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
|
||||
return <RiPencilLine className={iconClass} />;
|
||||
}
|
||||
if (tool === 'write' || tool === 'create' || tool === 'file_write') {
|
||||
@@ -175,6 +175,19 @@ const getToolDescription = (part: ToolPartType, state: ToolStateUnion, isMobile:
|
||||
const metadata = stateWithData.metadata;
|
||||
const input = stateWithData.input;
|
||||
|
||||
if (part.tool === 'apply_patch') {
|
||||
const files = Array.isArray(metadata?.files) ? metadata?.files : [];
|
||||
const firstFile = files[0] as { relativePath?: string; filePath?: string } | undefined;
|
||||
const filePath = firstFile?.relativePath || firstFile?.filePath;
|
||||
if (files.length > 1) {
|
||||
return `${files.length} files`;
|
||||
}
|
||||
if (typeof filePath === 'string') {
|
||||
return getRelativePath(filePath, currentDirectory, isMobile);
|
||||
}
|
||||
return 'Patch';
|
||||
}
|
||||
|
||||
// Question tool: show "Asked N question(s)"
|
||||
if (part.tool === 'question' && input?.questions && Array.isArray(input.questions)) {
|
||||
const count = input.questions.length;
|
||||
@@ -605,7 +618,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
|
||||
return formatInputForDisplay(input, part.tool);
|
||||
}, [input, part.tool]);
|
||||
const hasInputText = inputTextContent.trim().length > 0;
|
||||
const hasInputText = part.tool !== 'apply_patch' && inputTextContent.trim().length > 0;
|
||||
|
||||
const renderScrollableBlock = (
|
||||
content: React.ReactNode,
|
||||
@@ -755,7 +768,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if ((part.tool === 'edit' || part.tool === 'multiedit') && diffContent) {
|
||||
if ((part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && diffContent) {
|
||||
return renderScrollableBlock(
|
||||
<DiffPreview diff={diffContent} syntaxTheme={syntaxTheme} input={input} />,
|
||||
{ className: 'p-1' }
|
||||
@@ -1013,7 +1026,7 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
||||
onContentChange?.('structural');
|
||||
}, [isTaskTool, onContentChange, taskSummaryEntries.length]);
|
||||
|
||||
const diffStats = (part.tool === 'edit' || part.tool === 'multiedit') ? parseDiffStats(metadata) : null;
|
||||
const diffStats = (part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') ? parseDiffStats(metadata) : null;
|
||||
const description = getToolDescription(part, state, isMobile, currentDirectory);
|
||||
const displayName = getToolMetadata(part.tool).displayName;
|
||||
|
||||
|
||||
@@ -410,19 +410,22 @@ export const parseDiffToUnified = (diffText: string): UnifiedDiffHunk[] => {
|
||||
const newStart = match ? parseInt(match[2]) : 0;
|
||||
|
||||
const unifiedLines: UnifiedDiffLine[] = [];
|
||||
let lineNum = newStart;
|
||||
let oldLineNum = oldStart;
|
||||
let newLineNum = newStart;
|
||||
let j = i + 1;
|
||||
|
||||
while (j < lines.length && !lines[j].startsWith('@@') && !lines[j].startsWith('Index:')) {
|
||||
const contentLine = lines[j];
|
||||
if (contentLine.startsWith('+')) {
|
||||
unifiedLines.push({ type: 'added', lineNumber: lineNum, content: contentLine.substring(1) });
|
||||
lineNum++;
|
||||
unifiedLines.push({ type: 'added', lineNumber: newLineNum, content: contentLine.substring(1) });
|
||||
newLineNum++;
|
||||
} else if (contentLine.startsWith('-')) {
|
||||
unifiedLines.push({ type: 'removed', lineNumber: null, content: contentLine.substring(1) });
|
||||
unifiedLines.push({ type: 'removed', lineNumber: oldLineNum, content: contentLine.substring(1) });
|
||||
oldLineNum++;
|
||||
} else if (contentLine.startsWith(' ')) {
|
||||
unifiedLines.push({ type: 'context', lineNumber: lineNum, content: contentLine.substring(1) });
|
||||
lineNum++;
|
||||
unifiedLines.push({ type: 'context', lineNumber: newLineNum, content: contentLine.substring(1) });
|
||||
oldLineNum++;
|
||||
newLineNum++;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
|
||||
let activePartType: 'text' | 'tool' | 'reasoning' | 'editing' | undefined = undefined;
|
||||
let activeToolName: string | undefined = undefined;
|
||||
|
||||
const editingTools = new Set(['edit', 'write']);
|
||||
const editingTools = new Set(['edit', 'write', 'apply_patch']);
|
||||
|
||||
for (let i = (lastAssistant.parts ?? []).length - 1; i >= 0; i -= 1) {
|
||||
const part = lastAssistant.parts?.[i];
|
||||
@@ -218,6 +218,7 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
|
||||
write: 'writing file',
|
||||
edit: 'editing file',
|
||||
multiedit: 'editing files',
|
||||
apply_patch: 'applying patch',
|
||||
bash: 'running command',
|
||||
grep: 'searching content',
|
||||
glob: 'finding files',
|
||||
|
||||
@@ -52,6 +52,14 @@ export const TOOL_METADATA: Record<string, ToolMetadata> = {
|
||||
{ key: 'edits', label: 'Edits', type: 'code', language: 'json' }
|
||||
]
|
||||
},
|
||||
apply_patch: {
|
||||
displayName: 'Apply Patch',
|
||||
category: 'file',
|
||||
outputLanguage: 'diff',
|
||||
inputFields: [
|
||||
{ key: 'patchText', label: 'Patch', type: 'code', language: 'diff' }
|
||||
]
|
||||
},
|
||||
|
||||
bash: {
|
||||
displayName: 'Shell Command',
|
||||
@@ -665,6 +673,13 @@ export function formatToolInput(input: Record<string, unknown>, toolName: string
|
||||
if (desc) return desc;
|
||||
}
|
||||
|
||||
if (toolName === 'apply_patch' && typeof input === 'object') {
|
||||
const patchText = getString('patchText') || getString('patch_text') || getString('patch');
|
||||
if (patchText) {
|
||||
return patchText;
|
||||
}
|
||||
}
|
||||
|
||||
if ((toolName === 'edit' || toolName === 'multiedit') && typeof input === 'object') {
|
||||
const filePath = getString('filePath') || getString('file_path') || getString('path');
|
||||
if (filePath) {
|
||||
|
||||
Reference in New Issue
Block a user