Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cli/src/components/tools/str-replace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
extractDiff,
extractFilePath,
isCreateFile,
shouldShowEditDiff,
} from '../../utils/implementor-helpers'

import type { ToolRenderConfig } from './types'
Expand Down Expand Up @@ -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: (
<EditBody
name={isCreate ? 'Create' : 'Edit'}
filePath={filePath}
diffText={diff ?? ''}
diffText={showDiff ? (diff ?? '') : ''}
isCreate={isCreate}
/>
),
Expand Down
77 changes: 77 additions & 0 deletions cli/src/utils/__tests__/implementor-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
groupConsecutiveToolBlocks,
getMultiPromptProgress,
getMultiPromptPreview,
shouldShowEditDiff,
} from '../implementor-helpers'

import type {
Expand Down Expand Up @@ -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[] = [
Expand Down
27 changes: 27 additions & 0 deletions cli/src/utils/implementor-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading