From 11ab673ed0fd8db74501ea4cb84932a595171d97 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:02:45 -0700 Subject: [PATCH] fix(native-chat): render structured patches as diffs --- .../native-chat/NativeChatToolRun.test.tsx | 24 ++++++++++++++ .../native-chat/native-chat-diff.test.ts | 33 +++++++++++++++++++ src/shared/native-chat-diff.ts | 33 +++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx index fa5358a3fad..ac77ca5f3e4 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx @@ -24,4 +24,28 @@ describe('NativeChatToolRun', () => { expect(screen.getByTitle('src/index.ts')).toHaveTextContent('src/index.ts') expect(screen.queryByTitle('{"file_path":"src/index.ts","offset":10}')).toBeNull() }) + + it('renders structured apply_patch changes as a reviewable diff instead of JSON', () => { + const blocks: NativeChatBlock[] = [ + { + type: 'tool-call', + name: 'apply_patch', + input: { + changes: [ + { + path: '/repo/src/app.ts', + kind: { type: 'update', move_path: null }, + diff: '@@ -1 +1 @@\n-before\n+after' + } + ] + } + } + ] + + const { container } = render() + + expect(screen.getByText('+after')).toBeInTheDocument() + expect(screen.getByText('-before')).toBeInTheDocument() + expect(container.querySelector('pre')).toBeNull() + }) }) diff --git a/src/renderer/src/components/native-chat/native-chat-diff.test.ts b/src/renderer/src/components/native-chat/native-chat-diff.test.ts index 229edc1e4bd..7f8fa7af575 100644 --- a/src/renderer/src/components/native-chat/native-chat-diff.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-diff.test.ts @@ -30,6 +30,39 @@ describe('diffFromToolCall', () => { ]) }) + it('reads structured apply_patch file changes as a diff', () => { + const diff = diffFromToolCall('apply_patch', { + changes: [ + { + path: '/repo/src/app.ts', + kind: { type: 'update', move_path: null }, + diff: '@@ -1,2 +1,2 @@\n const value = 1\n-old()\n+newValue()' + } + ] + }) + + expect(diff).toEqual([ + { kind: 'meta', text: '--- /repo/src/app.ts' }, + { kind: 'meta', text: '+++ /repo/src/app.ts' }, + { kind: 'meta', text: '@@ -1,2 +1,2 @@' }, + { kind: 'context', text: ' const value = 1' }, + { kind: 'del', text: 'old()' }, + { kind: 'add', text: 'newValue()' } + ]) + }) + + it('reads direct apply_patch text as a diff', () => { + expect( + diffFromToolCall('apply_patch', { + patch: '@@ -1 +1 @@\n-before\n+after' + }) + ).toEqual([ + { kind: 'meta', text: '@@ -1 +1 @@' }, + { kind: 'del', text: 'before' }, + { kind: 'add', text: 'after' } + ]) + }) + it('returns null when there is no old/new payload', () => { expect(diffFromToolCall('Edit', { file_path: '/x' })).toBeNull() }) diff --git a/src/shared/native-chat-diff.ts b/src/shared/native-chat-diff.ts index 0e9e242af6e..1df82db4800 100644 --- a/src/shared/native-chat-diff.ts +++ b/src/shared/native-chat-diff.ts @@ -80,6 +80,35 @@ function toLines(value: unknown, maxLines: number): { lines: string[]; truncated return { lines: bounded, truncated } } +function patchTextFromToolInput(value: Record): string | null { + if (typeof value.patch === 'string') { + return value.patch + } + if (typeof value.diff === 'string') { + return value.diff + } + if (!Array.isArray(value.changes)) { + return null + } + const sections = value.changes.flatMap((entry) => { + if (typeof entry !== 'object' || entry === null) { + return [] + } + const change = entry as Record + if (typeof change.diff !== 'string') { + return [] + } + const path = typeof change.path === 'string' ? change.path : 'file' + const kind = + typeof change.kind === 'object' && change.kind !== null + ? (change.kind as Record) + : null + const nextPath = kind && typeof kind.move_path === 'string' ? kind.move_path : path + return [`--- ${path}\n+++ ${nextPath}\n${change.diff}`] + }) + return sections.length > 0 ? sections.join('\n') : null +} + export function diffFromToolCall( name: string, input: unknown, @@ -89,6 +118,10 @@ export function diffFromToolCall( return null } const value = input as Record + const patchText = patchTextFromToolInput(value) + if (patchText !== null) { + return diffFromText(patchText, maxLines) + } const oldLines = toLines(value.old_string ?? value.oldString ?? value.old, maxLines) const newLines = toLines( value.new_string ?? value.newString ?? value.new ?? value.content ?? value.file_text,