From 44d4cc87c16e1e5991d1d4a67f7690195b5ea631 Mon Sep 17 00:00:00 2001 From: Christina Tong Date: Tue, 17 Feb 2026 12:01:03 -0800 Subject: [PATCH] feat: pass Edit code diff start line in tool return (#994) --- src/cli/components/ToolCallMessageRich.tsx | 21 +++++++++++++++++- src/tools/impl/Edit.ts | 12 +++++++++++ src/tools/impl/MultiEdit.ts | 25 ++++++++++++++++------ src/tools/manager.ts | 6 ++++++ 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/cli/components/ToolCallMessageRich.tsx b/src/cli/components/ToolCallMessageRich.tsx index 6475a2c..28e052c 100644 --- a/src/cli/components/ToolCallMessageRich.tsx +++ b/src/cli/components/ToolCallMessageRich.tsx @@ -233,10 +233,29 @@ export const ToolCallMessage = memo( const dotShouldAnimate = line.phase === "running" || (line.phase === "ready" && !isStreaming); + // Extract display text from tool result (handles JSON responses) + const extractMessageFromResult = (text: string): string => { + try { + const parsed = JSON.parse(text); + // If it's a JSON object with a message field, extract that + if ( + parsed && + typeof parsed === "object" && + typeof parsed.message === "string" + ) { + return parsed.message; + } + } catch { + // Not JSON or parsing failed, use as-is + } + return text; + }; + // Format result for display const getResultElement = () => { if (!line.resultText) return null; + const extractedText = extractMessageFromResult(line.resultText); const prefix = ` ⎿ `; // Match old format: 2 spaces, glyph, 2 spaces const prefixWidth = 5; // Total width of prefix const contentWidth = Math.max(0, columns - prefixWidth); @@ -270,7 +289,7 @@ export const ToolCallMessage = memo( // Truncate the result text for display (UI only, API gets full response) // Strip trailing newlines to avoid extra visual spacing (e.g., from bash echo) - const displayResultText = clipToolReturn(line.resultText).replace( + const displayResultText = clipToolReturn(extractedText).replace( /\n+$/, "", ); diff --git a/src/tools/impl/Edit.ts b/src/tools/impl/Edit.ts index 4a5d1b2..b28ec77 100644 --- a/src/tools/impl/Edit.ts +++ b/src/tools/impl/Edit.ts @@ -12,6 +12,7 @@ interface EditArgs { interface EditResult { message: string; replacements: number; + startLine?: number; } function countOccurrences(content: string, needle: string): number { @@ -184,13 +185,22 @@ export async function edit(args: EditArgs): Promise { (expected_replacements !== undefined && expected_replacements > 1); let newContent: string; let replacements: number; + let startLine: number | undefined; + if (effectiveReplaceAll) { newContent = content.split(finalOldString).join(finalNewString); replacements = occurrences; + // For replace_all, calculate line number of first occurrence + const firstIndex = content.indexOf(finalOldString); + if (firstIndex !== -1) { + startLine = content.substring(0, firstIndex).split("\n").length; + } } else { const index = content.indexOf(finalOldString); if (index === -1) throw new Error(`String not found in file: ${finalOldString}`); + // Calculate the line number where old_string starts (1-indexed) + startLine = content.substring(0, index).split("\n").length; newContent = content.substring(0, index) + finalNewString + @@ -198,9 +208,11 @@ export async function edit(args: EditArgs): Promise { replacements = 1; } await fs.writeFile(resolvedPath, newContent, "utf-8"); + return { message: `Successfully replaced ${replacements} occurrence${replacements !== 1 ? "s" : ""} in ${resolvedPath}`, replacements, + startLine, }; } catch (error) { const err = error as NodeJS.ErrnoException; diff --git a/src/tools/impl/MultiEdit.ts b/src/tools/impl/MultiEdit.ts index a8a0e27..312d797 100644 --- a/src/tools/impl/MultiEdit.ts +++ b/src/tools/impl/MultiEdit.ts @@ -11,9 +11,15 @@ export interface MultiEditArgs { file_path: string; edits: Edit[]; } +interface EditWithLine { + description: string; + startLine: number; +} + interface MultiEditResult { message: string; edits_applied: number; + edits: EditWithLine[]; } export async function multi_edit( @@ -45,7 +51,7 @@ export async function multi_edit( const rawContent = await fs.readFile(resolvedPath, "utf-8"); // Normalize line endings to LF for consistent matching (Windows uses CRLF) let content = rawContent.replace(/\r\n/g, "\n"); - const appliedEdits: string[] = []; + const appliedEdits: EditWithLine[] = []; for (let i = 0; i < edits.length; i++) { const edit = edits[i]; if (!edit) continue; @@ -61,26 +67,33 @@ export async function multi_edit( `Found ${occurrences} matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, please provide more context to uniquely identify the instance.\nString: ${old_string}`, ); } + + // Calculate start line before applying the edit + const index = content.indexOf(old_string); + const startLine = content.substring(0, index).split("\n").length; + if (replace_all) { content = content.split(old_string).join(new_string); } else { - const index = content.indexOf(old_string); content = content.substring(0, index) + new_string + content.substring(index + old_string.length); } - appliedEdits.push( - `Replaced "${old_string.substring(0, 50)}${old_string.length > 50 ? "..." : ""}" with "${new_string.substring(0, 50)}${new_string.length > 50 ? "..." : ""}"`, - ); + appliedEdits.push({ + description: `Replaced "${old_string.substring(0, 50)}${old_string.length > 50 ? "..." : ""}" with "${new_string.substring(0, 50)}${new_string.length > 50 ? "..." : ""}"`, + startLine, + }); } await fs.writeFile(resolvedPath, content, "utf-8"); const editList = appliedEdits - .map((edit, i) => `${i + 1}. ${edit}`) + .map((edit, i) => `${i + 1}. ${edit.description}`) .join("\n"); + return { message: `Applied ${edits.length} edit${edits.length !== 1 ? "s" : ""} to ${resolvedPath}:\n${editList}`, edits_applied: edits.length, + edits: appliedEdits, }; } catch (error) { const err = error as NodeJS.ErrnoException; diff --git a/src/tools/manager.ts b/src/tools/manager.ts index 7ea9172..81fb955 100644 --- a/src/tools/manager.ts +++ b/src/tools/manager.ts @@ -949,6 +949,12 @@ function flattenToolResponse(result: unknown): ToolReturnContent { } if (typeof result.message === "string") { + // If there are other fields besides 'message', return the full object as JSON + + const keys = Object.keys(result); + if (keys.length > 1) { + return JSON.stringify(result); + } return result.message; }