From 17dfd7011f223a49ffbda08d92574358cd710804 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 5 May 2026 18:38:48 -0700 Subject: [PATCH] Fix edit tool diff rendering --- cli/src/components/tools/str-replace.tsx | 4 +- .../__tests__/implementor-helpers.test.ts | 77 +++++++++++++++++++ cli/src/utils/implementor-helpers.ts | 27 +++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/cli/src/components/tools/str-replace.tsx b/cli/src/components/tools/str-replace.tsx index 10e00672c..ab1cc3823 100644 --- a/cli/src/components/tools/str-replace.tsx +++ b/cli/src/components/tools/str-replace.tsx @@ -7,6 +7,7 @@ import { extractDiff, extractFilePath, isCreateFile, + shouldShowEditDiff, } from '../../utils/implementor-helpers' import type { ToolRenderConfig } from './types' @@ -60,13 +61,14 @@ export const StrReplaceComponent = defineToolComponent({ const diff = extractDiff(toolBlock) const filePath = extractFilePath(toolBlock) const isCreate = isCreateFile(toolBlock) + const showDiff = shouldShowEditDiff(toolBlock) return { content: ( ), diff --git a/cli/src/utils/__tests__/implementor-helpers.test.ts b/cli/src/utils/__tests__/implementor-helpers.test.ts index 03699fc41..44793c408 100644 --- a/cli/src/utils/__tests__/implementor-helpers.test.ts +++ b/cli/src/utils/__tests__/implementor-helpers.test.ts @@ -17,6 +17,7 @@ import { groupConsecutiveToolBlocks, getMultiPromptProgress, getMultiPromptPreview, + shouldShowEditDiff, } from '../implementor-helpers' import type { @@ -368,6 +369,82 @@ describe('getFileChangeType', () => { }) }) +describe('shouldShowEditDiff', () => { + test('does not show pending str_replace diffs before the result arrives', () => { + const block: ToolContentBlock = { + type: 'tool', + toolCallId: 'test-1', + toolName: 'str_replace', + input: { + replacements: [{ oldString: 'const x = 1', newString: 'const x = 2' }], + }, + } + + expect(shouldShowEditDiff(block)).toBe(false) + }) + + test('shows str_replace diffs after a successful result', () => { + const block: ToolContentBlock = { + type: 'tool', + toolCallId: 'test-1', + toolName: 'str_replace', + input: { + replacements: [{ oldString: 'const x = 1', newString: 'const x = 2' }], + }, + output: 'file: src/existing.ts\nmessage: String replace applied successfully.', + } + + expect(shouldShowEditDiff(block)).toBe(true) + }) + + test('does not show pending write_file diffs before the result arrives', () => { + const block: ToolContentBlock = { + type: 'tool', + toolCallId: 'test-1', + toolName: 'write_file', + input: { path: 'src/new.ts', content: 'const x = 1\n' }, + } + + expect(extractDiff(block)).toBe('+ const x = 1\n+ ') + expect(shouldShowEditDiff(block)).toBe(false) + }) + + test('shows write_file diffs after an overwrite result', () => { + const block: ToolContentBlock = { + type: 'tool', + toolCallId: 'test-1', + toolName: 'write_file', + input: { path: 'src/existing.ts', content: 'const x = 2\n' }, + output: 'file: src/existing.ts\nmessage: Overwrote file successfully.', + } + + expect(shouldShowEditDiff(block)).toBe(true) + }) + + test('does not show write_file diffs after a create result', () => { + const block: ToolContentBlock = { + type: 'tool', + toolCallId: 'test-1', + toolName: 'write_file', + input: { path: 'src/new.ts', content: 'const x = 1\n' }, + output: 'file: src/new.ts\nmessage: Created file successfully.', + } + + expect(shouldShowEditDiff(block)).toBe(false) + }) + + test('continues to show pending proposed write_file diffs', () => { + const block: ToolContentBlock = { + type: 'tool', + toolCallId: 'test-1', + toolName: 'propose_write_file', + input: { path: 'src/new.ts', content: 'const x = 1\n' }, + } + + expect(shouldShowEditDiff(block)).toBe(true) + }) +}) + describe('getFileStatsFromBlocks', () => { test('aggregates stats for same file', () => { const blocks: ContentBlock[] = [ diff --git a/cli/src/utils/implementor-helpers.ts b/cli/src/utils/implementor-helpers.ts index 3fb5027a3..ccb92c5c1 100644 --- a/cli/src/utils/implementor-helpers.ts +++ b/cli/src/utils/implementor-helpers.ts @@ -430,6 +430,33 @@ export function isCreateFile(toolBlock: ToolContentBlock): boolean { ) } +function hasToolResultOutput(toolBlock: ToolContentBlock): boolean { + const outputStr = typeof toolBlock.output === 'string' ? toolBlock.output : '' + return outputStr.length > 0 || toolBlock.outputRaw !== undefined +} + +/** + * Decide whether the direct edit tool renderer should show a diff preview. + * + * Real edit tool calls render immediately with input only, then receive output + * once the edit completes. Wait for that result before showing diffs so create + * operations never briefly flash an input-derived full-file diff. + */ +export function shouldShowEditDiff(toolBlock: ToolContentBlock): boolean { + if (!extractDiff(toolBlock) || isCreateFile(toolBlock)) { + return false + } + + if ( + !isProposedToolName(toolBlock.toolName) && + !hasToolResultOutput(toolBlock) + ) { + return false + } + + return true +} + export interface TimelineItem { type: 'commentary' | 'edit' content: string // For commentary: the text. For edits: file path