fix(tui): bound diff rendering work

This commit is contained in:
kingwl
2026-07-31 12:58:30 +08:00
parent 51beb34f97
commit 81ff2894ca
14 changed files with 246 additions and 45 deletions

View File

@@ -1,7 +1,7 @@
terminal 100x40 buffer=normal length=40 base=0 viewport=0
terminal 100x40 buffer=normal length=41 base=1 viewport=1
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=7 viewportRow=34 bufferRow=34
cursor hidden column=7 viewportRow=39 bufferRow=40
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold
@@ -58,17 +58,27 @@ buffer
style 0-99 dim
30| "Loaded review instructions. "
style 0-99 dim
31| "Model wait 0.0s "
31| <blank>
32| "● Tool / large_edit"
style 0-18 fg=green
33| "src/large.ts "
style 0-11 bold
34| "[exact line diff omitted: >2 changed lines] "
style 0-42 dim
35| "… +6 lines (Ctrl+O to expand) "
style 0-28 dim
36| "└ +3 -3 · 1 file · approximate "
style 0-29 dim
37| "Model wait 0.0s "
style 0-14 dim
32| <blank>
33| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
38| <blank>
39| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
style 0-17 fg=bright-magenta bold
style 18-31 dim
style 34-50 dim
style 53-57 dim
style 60-69 dim
34| " dsh > "
40| " dsh > "
style 1-3 fg=bright-magenta bold
style 5-6 dim
style 7-7 inverse
35-39| <blank>

View File

@@ -1,7 +1,7 @@
terminal 100x40 buffer=normal length=42 base=2 viewport=2
terminal 100x40 buffer=normal length=53 base=13 viewport=13
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=7 viewportRow=39 bufferRow=41
cursor hidden column=7 viewportRow=39 bufferRow=52
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold
@@ -70,19 +70,40 @@ buffer
style 0-99 dim
35| "Loaded review instructions. "
style 0-99 dim
36| "Model wait 0.0s "
36| <blank>
37| "● Tool / large_edit"
style 0-18 fg=green
38| "src/large.ts "
style 0-11 bold
39| "[exact line diff omitted: >2 changed lines] "
style 0-42 dim
40| "- old one "
style 0-8 fg=red
41| "- old two "
style 0-8 fg=red
42| "- old three "
style 0-10 fg=red
43| "+ new one "
style 0-8 fg=green
44| "+ new two "
style 0-8 fg=green
45| "+ new three "
style 0-10 fg=green
46| "└ +3 -3 · 1 file · approximate "
style 0-29 dim
47| "Model wait 0.0s "
style 0-14 dim
37| <blank>
38| "Tool and context cards expanded. "
48| <blank>
49| "Tool and context cards expanded. "
style 0-31 dim
39| <blank>
40| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
50| <blank>
51| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
style 0-17 fg=bright-magenta bold
style 18-31 dim
style 34-50 dim
style 53-57 dim
style 60-69 dim
41| " dsh > "
52| " dsh > "
style 1-3 fg=bright-magenta bold
style 5-6 dim
style 7-7 inverse

View File

@@ -269,13 +269,32 @@ const ADVANCED_CARD_TOOLS: Record<string, ToolDefinition> = {
edit: visualTool(
'edit',
() => ({ card: 'diff', title: 'Edit src/view.ts', diffs: [{ path: 'src/view.ts', oldText: 'old line', newText: 'new line' }] }),
// The real edit/write tools produce exactly one diff whose path the title
// already names, so the card omits the redundant per-file header.
// The fixed tool header never names a path, so the hunk retains its path.
(): ToolResultView => ({
card: 'diff',
diffs: [{ path: 'src/view.ts', oldText: 'old line\nkeep', newText: 'new line\nkeep' }],
}),
),
large_edit: visualTool(
'large_edit',
() => ({
card: 'diff',
title: 'Edit src/large.ts',
diffs: [{
path: 'src/large.ts',
oldText: 'old one\nold two\nold three',
newText: 'new one\nnew two\nnew three',
}],
}),
(): ToolResultView => ({
card: 'diff',
diffs: [{
path: 'src/large.ts',
oldText: 'old one\nold two\nold three',
newText: 'new one\nnew two\nnew three',
}],
}),
),
subagent: visualTool('subagent', args => ({
card: 'generic',
title: 'Delegate renderer audit',
@@ -585,7 +604,7 @@ describe('TUI terminal-state snapshots', () => {
it('pins terminal, diff, subagent, task, skill, collapsed, and expanded cards', async () => {
const harness = await setupSnapshot({
tools: ADVANCED_CARD_TOOLS,
config: { maxToolOutputLines: 3 },
config: { maxToolOutputLines: 3, maxDiffEditLength: 2 },
}, { columns: 100, rows: 40 })
const calls = [
{ id: 'advanced-1', name: 'bash', arguments: { command: 'pnpm run test:coverage' } },
@@ -593,6 +612,7 @@ describe('TUI terminal-state snapshots', () => {
{ id: 'advanced-3', name: 'subagent', arguments: { prompt: 'Review renderer ownership and report only gaps.' } },
{ id: 'advanced-4', name: 'task_output', arguments: { task_id: 'subagent-7', wait: true } },
{ id: 'advanced-5', name: 'skill', arguments: { name: 'dsh-code-review' } },
{ id: 'advanced-6', name: 'large_edit', arguments: { file_path: 'src/large.ts' } },
]
await renderAfter(harness, () => {
appendToolCalls(harness.session, calls)
@@ -601,6 +621,7 @@ describe('TUI terminal-state snapshots', () => {
appendToolResult(harness.session, 'advanced-3', [{ type: 'text', text: 'The renderer has explicit lifecycle ownership.' }])
appendToolResult(harness.session, 'advanced-4', [{ type: 'text', text: 'audit complete\n[status: completed]' }])
appendToolResult(harness.session, 'advanced-5', [{ type: 'text', text: 'Loaded review instructions.' }])
appendToolResult(harness.session, 'advanced-6', [{ type: 'text', text: 'large edit complete' }])
})
await checkpoint('advanced-cards-collapsed', harness.terminal, { includeScrollback: true })

View File

@@ -178,6 +178,7 @@ describe('TUI config', () => {
expect(resolveTuiConfig(undefined)).toEqual({
showReasoning: true,
maxToolOutputLines: 6,
maxDiffEditLength: 1000,
maxQuestionOptions: 8,
maxModelOptions: 8,
maxResumeOptions: 8,
@@ -202,6 +203,7 @@ describe('TUI config', () => {
expect(resolveTuiConfig({
showReasoning: false,
maxToolOutputLines: 2,
maxDiffEditLength: 12,
maxQuestionOptions: 3,
maxModelOptions: 4,
maxResumeOptions: 5,
@@ -218,6 +220,7 @@ describe('TUI config', () => {
})).toEqual({
showReasoning: false,
maxToolOutputLines: 2,
maxDiffEditLength: 12,
maxQuestionOptions: 3,
maxModelOptions: 4,
maxResumeOptions: 5,
@@ -4651,6 +4654,98 @@ describe('tool cards and surface replay', () => {
await dispose(result)
})
it('renders an empty create without a synthetic added row', async () => {
const emptyCreate: Record<string, ToolDefinition> = {
emptyCreate: {
name: 'emptyCreate',
description: '',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: () => ({
card: 'diff',
title: 'Write empty.txt',
diffs: [{ path: 'empty.txt', oldText: null, newText: '' }],
}),
},
}
const result = await setup({
tools: emptyCreate,
config: { maxToolOutputLines: 20, theme: { color: false } },
})
appendAssistant(result.session, [
{ type: 'tool-call', id: 'empty-create' as never, name: 'emptyCreate', arguments: '{}' },
])
result.session.append('tool/call', {
turn: 1,
step: 1,
callId: 'empty-create' as never,
name: 'emptyCreate',
arguments: '{}',
})
await tick()
const rows = result.terminal.output.split('\n').map(row => row.trim())
expect(result.terminal.output).toContain('empty.txt')
expect(result.terminal.output).toContain('└ +0 -0 · 1 file')
expect(rows).not.toContain('+')
await dispose(result)
})
it('bounds and caches exact diff comparison before whole-side fallback', async () => {
let oldTextReads = 0
const boundedDiff = {
path: 'bounded.txt',
get oldText() {
oldTextReads += 1
return 'old one\nold two'
},
newText: 'new one\nnew two',
}
const bounded: Record<string, ToolDefinition> = {
bounded: {
name: 'bounded',
description: '',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: () => ({
card: 'diff',
title: 'Edit bounded.txt',
diffs: [boundedDiff],
}),
},
}
const result = await setup({
tools: bounded,
config: {
maxToolOutputLines: 20,
maxDiffEditLength: 1,
theme: { color: false },
},
})
appendAssistant(result.session, [
{ type: 'tool-call', id: 'bounded-diff' as never, name: 'bounded', arguments: '{}' },
])
result.session.append('tool/call', {
turn: 1,
step: 1,
callId: 'bounded-diff' as never,
name: 'bounded',
arguments: '{}',
})
await tick()
expect(result.terminal.output).toContain('[exact line diff omitted: >1 changed lines]')
expect(result.terminal.output).toContain('- old one')
expect(result.terminal.output).toContain('+ new one')
expect(result.terminal.output).toContain('└ +2 -2 · 1 file · approximate')
const readsAfterFirstRender = oldTextReads
expect(readsAfterFirstRender).toBeGreaterThan(0)
result.terminal.resize(87)
await tick()
expect(oldTextReads).toBe(readsAfterFirstRender)
await dispose(result)
})
it('drops blank rows from a terminal card result that the dim styling wraps', async () => {
const blankRowTools: Record<string, ToolDefinition> = {
trailing: {