fix(tui): attach inline diffs to tool timeline
This commit is contained in:
parent
05dc2eec36
commit
d91e24547c
@ -185,19 +185,17 @@ describe('createGatewayEventHandler', () => {
|
||||
expect(appended).toHaveLength(0)
|
||||
expect(turnController.segmentMessages).toEqual([
|
||||
{ role: 'assistant', text: 'Editing the file' },
|
||||
{ kind: 'trail', role: 'system', text: '', tools: ['Patch("foo.ts") ✓'] },
|
||||
{ kind: 'diff', role: 'assistant', text: block }
|
||||
{ kind: 'diff', role: 'assistant', text: block, tools: ['Patch("foo.ts") ✓'] }
|
||||
])
|
||||
|
||||
onEvent({ payload: { text: 'patch applied' }, type: 'message.complete' } as any)
|
||||
|
||||
expect(appended).toHaveLength(5)
|
||||
expect(appended).toHaveLength(4)
|
||||
expect(appended[0]?.text).toBe('Editing the file')
|
||||
expect(appended[1]).toMatchObject({ kind: 'trail' })
|
||||
expect(appended[1]).toMatchObject({ kind: 'diff', text: block })
|
||||
expect(appended[1]?.tools?.[0]).toContain('Patch')
|
||||
expect(appended[2]).toMatchObject({ kind: 'diff', text: block })
|
||||
expect(appended[4]?.text).toBe('patch applied')
|
||||
expect(appended[4]?.text).not.toContain('```diff')
|
||||
expect(appended[3]?.text).toBe('patch applied')
|
||||
expect(appended[3]?.text).not.toContain('```diff')
|
||||
})
|
||||
|
||||
it('drops the diff segment when the final assistant text narrates the same diff', () => {
|
||||
@ -211,10 +209,9 @@ describe('createGatewayEventHandler', () => {
|
||||
|
||||
// Only the final message — diff-only segment dropped so we don't
|
||||
// render two stacked copies of the same patch.
|
||||
expect(appended).toHaveLength(2)
|
||||
expect(appended[0]).toMatchObject({ kind: 'trail' })
|
||||
expect(appended[1]?.text).toBe(assistantText)
|
||||
expect((appended[1]?.text.match(/```diff/g) ?? []).length).toBe(1)
|
||||
expect(appended).toHaveLength(1)
|
||||
expect(appended[0]?.text).toBe(assistantText)
|
||||
expect((appended[0]?.text.match(/```diff/g) ?? []).length).toBe(1)
|
||||
})
|
||||
|
||||
it('strips the CLI "┊ review diff" header from inline diff segments', () => {
|
||||
@ -226,12 +223,12 @@ describe('createGatewayEventHandler', () => {
|
||||
onEvent({ payload: { text: 'done' }, type: 'message.complete' } as any)
|
||||
|
||||
// Tool trail first, then diff segment (kind='diff'), then final narration.
|
||||
expect(appended).toHaveLength(3)
|
||||
expect(appended[0]?.kind).toBe('trail')
|
||||
expect(appended[1]?.kind).toBe('diff')
|
||||
expect(appended[1]?.text).not.toContain('┊ review diff')
|
||||
expect(appended[1]?.text).toContain('--- a/foo.ts')
|
||||
expect(appended[2]?.text).toBe('done')
|
||||
expect(appended).toHaveLength(2)
|
||||
expect(appended[0]?.kind).toBe('diff')
|
||||
expect(appended[0]?.text).not.toContain('┊ review diff')
|
||||
expect(appended[0]?.text).toContain('--- a/foo.ts')
|
||||
expect(appended[0]?.tools?.[0]).toContain('Tool')
|
||||
expect(appended[1]?.text).toBe('done')
|
||||
})
|
||||
|
||||
it('drops the diff segment when assistant writes its own ```diff fence', () => {
|
||||
@ -246,10 +243,9 @@ describe('createGatewayEventHandler', () => {
|
||||
} as any)
|
||||
onEvent({ payload: { text: assistantText }, type: 'message.complete' } as any)
|
||||
|
||||
expect(appended).toHaveLength(2)
|
||||
expect(appended[0]).toMatchObject({ kind: 'trail' })
|
||||
expect(appended[1]?.text).toBe(assistantText)
|
||||
expect((appended[1]?.text.match(/```diff/g) ?? []).length).toBe(1)
|
||||
expect(appended).toHaveLength(1)
|
||||
expect(appended[0]?.text).toBe(assistantText)
|
||||
expect((appended[0]?.text.match(/```diff/g) ?? []).length).toBe(1)
|
||||
})
|
||||
|
||||
it('keeps tool trail terse when inline_diff is present', () => {
|
||||
@ -265,15 +261,13 @@ describe('createGatewayEventHandler', () => {
|
||||
|
||||
// Tool row is now placed before the diff, so telemetry does not render
|
||||
// below the patch that came from that tool.
|
||||
expect(appended).toHaveLength(3)
|
||||
expect(appended[0]?.kind).toBe('trail')
|
||||
expect(appended).toHaveLength(2)
|
||||
expect(appended[0]?.kind).toBe('diff')
|
||||
expect(appended[0]?.text).toContain('```diff')
|
||||
expect(appended[0]?.tools?.[0]).toContain('Review Diff')
|
||||
expect(appended[0]?.tools?.[0]).not.toContain('--- a/foo.ts')
|
||||
expect(appended[1]?.kind).toBe('diff')
|
||||
expect(appended[1]?.text).toContain('```diff')
|
||||
expect(appended[1]?.text).toBe('done')
|
||||
expect(appended[1]?.tools ?? []).toEqual([])
|
||||
expect(appended[2]?.text).toBe('done')
|
||||
expect(appended[2]?.tools ?? []).toEqual([])
|
||||
})
|
||||
|
||||
it('shows setup panel for missing provider startup error', () => {
|
||||
|
||||
@ -380,18 +380,14 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||
ev.payload.inline_diff && getUiState().inlineDiffs ? stripAnsi(String(ev.payload.inline_diff)).trim() : ''
|
||||
|
||||
if (inlineDiffText) {
|
||||
turnController.flushStreamingSegment()
|
||||
}
|
||||
|
||||
turnController.recordToolComplete(
|
||||
ev.payload.tool_id,
|
||||
ev.payload.name,
|
||||
ev.payload.error,
|
||||
inlineDiffText ? '' : ev.payload.summary
|
||||
)
|
||||
|
||||
if (inlineDiffText) {
|
||||
turnController.pushInlineDiffSegment(inlineDiffText)
|
||||
turnController.recordInlineDiffToolComplete(
|
||||
inlineDiffText,
|
||||
ev.payload.tool_id,
|
||||
ev.payload.name,
|
||||
ev.payload.error
|
||||
)
|
||||
} else {
|
||||
turnController.recordToolComplete(ev.payload.tool_id, ev.payload.name, ev.payload.error, ev.payload.summary)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@ -220,7 +220,7 @@ class TurnController {
|
||||
}, REASONING_PULSE_MS)
|
||||
}
|
||||
|
||||
pushInlineDiffSegment(diffText: string) {
|
||||
pushInlineDiffSegment(diffText: string, tools: string[] = []) {
|
||||
// Strip CLI chrome the gateway emits before the unified diff (e.g. a
|
||||
// leading "┊ review diff" header written by `_emit_inline_diff` for the
|
||||
// terminal printer). That header only makes sense as stdout dressing,
|
||||
@ -247,7 +247,7 @@ class TurnController {
|
||||
return
|
||||
}
|
||||
|
||||
this.segmentMessages = [...this.segmentMessages, { kind: 'diff', role: 'assistant', text: block }]
|
||||
this.segmentMessages = [...this.segmentMessages, { kind: 'diff', role: 'assistant', text: block, ...(tools.length && { tools }) }]
|
||||
patchTurnState({ streamSegments: this.segmentMessages })
|
||||
}
|
||||
|
||||
@ -397,13 +397,25 @@ class TurnController {
|
||||
}
|
||||
|
||||
recordToolComplete(toolId: string, fallbackName?: string, error?: string, summary?: string) {
|
||||
const line = this.completeTool(toolId, fallbackName, error, summary)
|
||||
|
||||
this.pendingSegmentTools = [...this.pendingSegmentTools, line]
|
||||
this.publishToolState()
|
||||
}
|
||||
|
||||
recordInlineDiffToolComplete(diffText: string, toolId: string, fallbackName?: string, error?: string) {
|
||||
this.flushStreamingSegment()
|
||||
this.pushInlineDiffSegment(diffText, [this.completeTool(toolId, fallbackName, error, '')])
|
||||
this.publishToolState()
|
||||
}
|
||||
|
||||
private completeTool(toolId: string, fallbackName?: string, error?: string, summary?: string) {
|
||||
const done = this.activeTools.find(tool => tool.id === toolId)
|
||||
const name = done?.name ?? fallbackName ?? 'tool'
|
||||
const label = toolTrailLabel(name)
|
||||
const line = buildToolTrailLine(name, done?.context || '', Boolean(error), error || summary || '')
|
||||
|
||||
this.activeTools = this.activeTools.filter(tool => tool.id !== toolId)
|
||||
this.pendingSegmentTools = [...this.pendingSegmentTools, line]
|
||||
|
||||
const next = this.turnTools.filter(item => !sameToolTrailGroup(label, item))
|
||||
|
||||
@ -412,6 +424,11 @@ class TurnController {
|
||||
}
|
||||
|
||||
this.turnTools = next.slice(-TRAIL_LIMIT)
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
private publishToolState() {
|
||||
patchTurnState({
|
||||
streamPendingTools: this.pendingSegmentTools,
|
||||
tools: this.activeTools,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user