fix: keep the program on the COMPLETED run_code card (agent review follow-up)

The previous commit put the fenced program only on the pending card —
but an ACP tool_call_update's content REPLACES the card content (Zed
truncates to the new list, crates/acp_thread update_fields), so the
code vanished the moment the run completed and was effectively never
visible. presentResult now re-carries the fenced program before the
captured output via a shared fencedProgram helper; the completed card
body is program + output, rendered by Zed as syntax-highlighted
markdown behind the card disclosure. Goldens re-recorded (filtered
this time: DSH_SNAPSHOT=record vitest -u -t mode-turn); unit test pins
the two-block result content.
This commit is contained in:
Tianyi Cui
2026-07-09 22:04:52 +08:00
parent 387f19c7f6
commit f505776eee
6 changed files with 422 additions and 383 deletions

View File

@@ -135,6 +135,15 @@ function asRunCodeMeta(meta: unknown): RunCodeMeta | undefined {
return m as unknown as RunCodeMeta
}
/**
* Render a program as the markdown block the tool-call cards carry.
* @param code - the program text.
* @returns the ts-fenced markdown block.
*/
function fencedProgram(code: string): string {
return `\`\`\`ts\n${code}\n\`\`\``
}
/**
* Build the `run_code` {@link ToolDefinition}: one required `code` parameter,
* executed through the dispatch bridge described in the module doc. The
@@ -289,25 +298,32 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
exec.signal?.removeEventListener('abort', onOuterAbort)
}
},
// The program IS the call: surface it as an always-visible fenced block in
// the card body (rawInput alone lands in detail/expanded views many
// clients never open). Fence collisions are impossible to break rendering
// — a backtick run inside the program at worst ends the block early.
// The program IS the call: surface it as a fenced block in the card body
// (rawInput alone lands in detail/expanded views many clients never
// open). Fence collisions are impossible to break rendering — a backtick
// run inside the program at worst ends the block early.
presentCall: args => ({
card: 'generic',
title: 'Run code',
kind: 'execute',
rawInput: args.code,
content: [{ type: 'text', text: `\`\`\`ts\n${args.code}\n\`\`\`` }],
content: [{ type: 'text', text: fencedProgram(args.code) }],
}),
presentResult: (_args, result) => {
// The result re-carries the program BEFORE the captured output: an ACP
// tool_call_update's `content` REPLACES the pending card's (clients
// truncate to the new list), so a result without the program would wipe
// it the moment the run completes.
presentResult: (args, result) => {
const meta = asRunCodeMeta(result.meta)
if (!meta) return undefined
const output = meta.logs.map(entry => entry.text).join('\n')
return {
card: 'generic',
title: `Run code (${meta.dispatches} tool call${meta.dispatches === 1 ? '' : 's'})`,
...output.length > 0 ? { content: [{ type: 'text', text: output }] } : {},
content: [
{ type: 'text', text: fencedProgram(args.code) },
...output.length > 0 ? [{ type: 'text' as const, text: output }] : [],
],
}
},
})

View File

@@ -461,10 +461,17 @@ describe('the run_code dispatch bridge', () => {
isError: false,
meta: { logs: [{ source: 'console', level: 'log', text: 'printed' }], dispatches: 1 },
})
expect(view).toEqual({ card: 'generic', title: 'Run code (1 tool call)', content: [{ type: 'text', text: 'printed' }] })
// Plural title, and no content when the program printed nothing.
// The result re-carries the fenced program before the output: the ACP
// update's content REPLACES the pending card's, so omitting it would
// wipe the code from the card the moment the run completes.
expect(view).toEqual({
card: 'generic',
title: 'Run code (1 tool call)',
content: [{ type: 'text', text: '```ts\nreturn 1\n```' }, { type: 'text', text: 'printed' }],
})
// Plural title, and the program alone when it printed nothing.
expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [], dispatches: 2 } }))
.toEqual({ card: 'generic', title: 'Run code (2 tool calls)' })
.toEqual({ card: 'generic', title: 'Run code (2 tool calls)', content: [{ type: 'text', text: '```ts\nx\n```' }] })
// Replay with an unrecognizable meta falls back to the generic rendering.
expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { other: true } })).toBeUndefined()
expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false })).toBeUndefined()