Skip to content

Commit 75f7fa9

Browse files
committed
Skip failed edit previews
1 parent 07402e8 commit 75f7fa9

2 files changed

Lines changed: 203 additions & 1 deletion

File tree

cli/src/utils/__tests__/implementor-helpers.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,24 @@ describe('extractDiff', () => {
132132
expect(diff).toContain('+ const x = 2')
133133
})
134134

135+
test('constructs diff from successful str_replace input with warning output', () => {
136+
const block: ToolContentBlock = {
137+
type: 'tool',
138+
toolCallId: 'test-1',
139+
toolName: 'str_replace',
140+
input: {
141+
replacements: [{ oldString: 'const x = 1', newString: 'const x = 2' }],
142+
},
143+
output: `message: |
144+
Matched with indentation modification
145+
146+
String replace applied successfully.`,
147+
}
148+
const diff = extractDiff(block)
149+
expect(diff).toContain('- const x = 1')
150+
expect(diff).toContain('+ const x = 2')
151+
})
152+
135153
test('uses patch content from successful str_replace input when output omits diff', () => {
136154
const block: ToolContentBlock = {
137155
type: 'tool',
@@ -143,6 +161,38 @@ describe('extractDiff', () => {
143161
expect(extractDiff(block)).toBe('- const x = 1\n+ const x = 2')
144162
})
145163

164+
test('returns null for failed str_replace output without a diff', () => {
165+
const block: ToolContentBlock = {
166+
type: 'tool',
167+
toolCallId: 'test-1',
168+
toolName: 'str_replace',
169+
input: {
170+
replacements: [{ oldString: 'const x = 1', newString: 'const x = 2' }],
171+
},
172+
output: 'No change to the file',
173+
}
174+
expect(extractDiff(block)).toBeNull()
175+
})
176+
177+
test('returns null for failed str_replace output even when it includes patch input', () => {
178+
const block: ToolContentBlock = {
179+
type: 'tool',
180+
toolCallId: 'test-1',
181+
toolName: 'str_replace',
182+
input: { type: 'patch', content: '- const x = 1\n+ const x = 2' },
183+
outputRaw: [
184+
{
185+
type: 'json',
186+
value: {
187+
errorMessage: 'Failed to apply patch.',
188+
patch: '- const x = 1\n+ const x = 2',
189+
},
190+
},
191+
],
192+
}
193+
expect(extractDiff(block)).toBeNull()
194+
})
195+
146196
test('constructs diff from write_file input', () => {
147197
const block: ToolContentBlock = {
148198
type: 'tool',
@@ -166,6 +216,17 @@ describe('extractDiff', () => {
166216
expect(diff).toBe('+ line1\n+ line2')
167217
})
168218

219+
test('returns null for failed write_file output without a diff', () => {
220+
const block: ToolContentBlock = {
221+
type: 'tool',
222+
toolCallId: 'test-1',
223+
toolName: 'write_file',
224+
input: { content: 'line1\nline2' },
225+
output: 'Failed to write to file',
226+
}
227+
expect(extractDiff(block)).toBeNull()
228+
})
229+
169230
test('constructs diff from propose_str_replace input', () => {
170231
const block: ToolContentBlock = {
171232
type: 'tool',
@@ -367,6 +428,25 @@ describe('getFileStatsFromBlocks', () => {
367428
const stats = getFileStatsFromBlocks(blocks)
368429
expect(stats).toHaveLength(0)
369430
})
431+
432+
test('ignores failed edit tools', () => {
433+
const blocks: ContentBlock[] = [
434+
{
435+
type: 'tool',
436+
toolCallId: 'test-1',
437+
toolName: 'str_replace',
438+
input: {
439+
path: 'file.ts',
440+
replacements: [
441+
{ oldString: 'const x = 1', newString: 'const x = 2' },
442+
],
443+
},
444+
output: 'No change to the file',
445+
},
446+
]
447+
const stats = getFileStatsFromBlocks(blocks)
448+
expect(stats).toHaveLength(0)
449+
})
370450
})
371451

372452
describe('buildActivityTimeline', () => {
@@ -414,6 +494,25 @@ describe('buildActivityTimeline', () => {
414494
expect(timeline).toHaveLength(1)
415495
expect(timeline[0].content).toBe('Normal text')
416496
})
497+
498+
test('skips failed edit tools', () => {
499+
const blocks: ContentBlock[] = [
500+
{
501+
type: 'text',
502+
content: 'Trying an edit',
503+
} as TextContentBlock,
504+
{
505+
type: 'tool',
506+
toolCallId: 'test-1',
507+
toolName: 'write_file',
508+
input: { path: 'file.ts', content: 'new content' },
509+
output: 'Failed to write to file',
510+
},
511+
]
512+
const timeline = buildActivityTimeline(blocks)
513+
expect(timeline).toHaveLength(1)
514+
expect(timeline[0].type).toBe('commentary')
515+
})
417516
})
418517

419518
describe('isImplementorAgent', () => {

cli/src/utils/implementor-helpers.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ const isProposedToolName = (toolName: ToolContentBlock['toolName']): boolean =>
2525
const getBaseToolName = (toolName: ToolContentBlock['toolName']): string =>
2626
isProposedToolName(toolName) ? toolName.slice('propose_'.length) : toolName
2727

28+
const SUCCESSFUL_EDIT_MESSAGES = [
29+
'String replace applied successfully',
30+
'Created file successfully',
31+
'Created new file',
32+
'Overwrote file successfully',
33+
'Wrote file successfully',
34+
'Updated file',
35+
'Proposed new file',
36+
'Proposed changes',
37+
'Proposed string replacement',
38+
] as const
39+
2840
const hasProposedTools = (blocks?: ContentBlock[]): boolean => {
2941
if (!blocks || blocks.length === 0) return false
3042

@@ -221,32 +233,55 @@ export function extractFilePath(toolBlock: ToolContentBlock): string | null {
221233
* For proposed tools (implementors): construct diff from input replacements.
222234
*/
223235
export function extractDiff(toolBlock: ToolContentBlock): string | null {
236+
let hasSuccessfulOutput = false
237+
224238
// First try to get from outputRaw (for executed tool results)
225239
// outputRaw is typically an array like [{type: "json", value: {unifiedDiff: "..."}}]
226240
const outputRaw = toolBlock.outputRaw as unknown
227241
if (Array.isArray(outputRaw) && outputRaw[0]?.value) {
228242
const value = outputRaw[0].value as Record<string, unknown>
243+
if (hasErrorMessage(value)) return null
244+
if (isSuccessfulEditMessage(value.message)) hasSuccessfulOutput = true
229245
if (value.unifiedDiff) return value.unifiedDiff as string
230246
if (value.patch) return value.patch as string
231247
}
232248
// Also check direct properties (in case format differs)
233249
if (typeof outputRaw === 'object' && outputRaw !== null) {
234250
const rawObj = outputRaw as Record<string, unknown>
251+
if (hasErrorMessage(rawObj)) return null
252+
if (isSuccessfulEditMessage(rawObj.message)) hasSuccessfulOutput = true
235253
if (rawObj.unifiedDiff) return rawObj.unifiedDiff as string
236254
if (rawObj.patch) return rawObj.patch as string
237255
}
238256

239257
// Try to get from output string (key: value format)
240258
const outputStr = typeof toolBlock.output === 'string' ? toolBlock.output : ''
259+
const message = extractValueForKey(outputStr, 'message')
241260
const diffFromOutput =
242261
extractValueForKey(outputStr, 'unifiedDiff') ||
243262
extractValueForKey(outputStr, 'patch')
244263

264+
if (hasFailedEditOutput({ outputStr, message, diffFromOutput })) {
265+
return null
266+
}
267+
if (isSuccessfulEditMessage(message)) {
268+
hasSuccessfulOutput = true
269+
}
270+
245271
if (diffFromOutput) {
246272
return diffFromOutput
247273
}
248274

249-
// For proposed edits (no output yet): construct diff from input
275+
// For proposed/pending edits, or confirmed successful executions, construct
276+
// the preview from input when the result omits a diff.
277+
const canUseInputFallback =
278+
isProposedToolName(toolBlock.toolName) ||
279+
outputStr === '' ||
280+
hasSuccessfulOutput
281+
if (!canUseInputFallback) {
282+
return null
283+
}
284+
250285
const input = toolBlock.input as Record<string, unknown>
251286
const baseToolName = getBaseToolName(toolBlock.toolName)
252287

@@ -271,6 +306,70 @@ export function extractDiff(toolBlock: ToolContentBlock): string | null {
271306
return null
272307
}
273308

309+
function hasErrorMessage(value: Record<string, unknown>): boolean {
310+
return Boolean(value.errorMessage || (value.value as any)?.errorMessage)
311+
}
312+
313+
function hasFailedEditOutput(params: {
314+
outputStr: string
315+
message: string | null
316+
diffFromOutput: string | null
317+
}): boolean {
318+
const { outputStr, message, diffFromOutput } = params
319+
const trimmedOutput = outputStr.trim()
320+
if (!trimmedOutput) {
321+
return false
322+
}
323+
if (
324+
extractValueForKey(outputStr, 'errorMessage') ||
325+
isErrorOutput(outputStr)
326+
) {
327+
return true
328+
}
329+
if (diffFromOutput || isSuccessfulEditMessage(message)) {
330+
return false
331+
}
332+
return !isSuccessfulEditMessage(trimmedOutput)
333+
}
334+
335+
function isFailedEditToolBlock(toolBlock: ToolContentBlock): boolean {
336+
const outputRaw = toolBlock.outputRaw as unknown
337+
if (Array.isArray(outputRaw) && outputRaw[0]?.value) {
338+
const value = outputRaw[0].value as Record<string, unknown>
339+
if (hasErrorMessage(value)) return true
340+
}
341+
if (typeof outputRaw === 'object' && outputRaw !== null) {
342+
const rawObj = outputRaw as Record<string, unknown>
343+
if (hasErrorMessage(rawObj)) return true
344+
}
345+
346+
const outputStr = typeof toolBlock.output === 'string' ? toolBlock.output : ''
347+
const message = extractValueForKey(outputStr, 'message')
348+
const diffFromOutput =
349+
extractValueForKey(outputStr, 'unifiedDiff') ||
350+
extractValueForKey(outputStr, 'patch')
351+
return hasFailedEditOutput({ outputStr, message, diffFromOutput })
352+
}
353+
354+
function isSuccessfulEditMessage(message: unknown): boolean {
355+
if (typeof message !== 'string') {
356+
return false
357+
}
358+
359+
return message
360+
.split('\n')
361+
.some((line) =>
362+
SUCCESSFUL_EDIT_MESSAGES.some((successMessage) =>
363+
line.trim().startsWith(successMessage),
364+
),
365+
)
366+
}
367+
368+
function isErrorOutput(output: string): boolean {
369+
const trimmedOutput = output.trim()
370+
return trimmedOutput.startsWith('Error:') || trimmedOutput.startsWith('Failed ')
371+
}
372+
274373
/**
275374
* Construct a simple diff view from str_replace replacements.
276375
*/
@@ -425,6 +524,8 @@ export function getFileStatsFromBlocks(
425524
block.toolName as (typeof ALL_EDIT_TOOL_NAMES)[number],
426525
)
427526
) {
527+
if (isFailedEditToolBlock(block)) continue
528+
428529
const filePath = extractFilePath(block)
429530
if (!filePath) continue
430531

@@ -475,6 +576,8 @@ export function buildActivityTimeline(
475576
block.toolName as (typeof ALL_EDIT_TOOL_NAMES)[number],
476577
)
477578
) {
579+
if (isFailedEditToolBlock(block)) continue
580+
478581
const filePath = extractFilePath(block)
479582
const diff = extractDiff(block)
480583
const isCreate = isCreateFile(block)

0 commit comments

Comments
 (0)