feat(acp): render bash as a terminal card via the _meta convention
When the client advertises clientCapabilities._meta.terminal_output (Zed), a
bash tool call now renders as a real TERMINAL card — a cwd header + the command
+ its output — instead of the plain ```console text block. Keeps agent-side
dsh-bash execution; rejects the spec's client-side terminal/create (which would
bypass sandbox/env-scrub/ownership/cwd). Matches what claude-agent-acp and
codex-acp do; wire contract verified against Zed's source.
- dsh-tools: a provider-neutral ToolTerminal shape ({ cwd?, output? }) on
ToolCallPresentation/ToolResultPresentation — a tool asks "render me as a
terminal"; no ACP types leak in.
- dsh-tool-bash: bash presentCall marks terminal (cwd from an explicit absolute
workdir, else left for the bridge to fill from the session cwd); presentResult
carries the output alongside the ```console fallback.
- dsh-acp: initialize reads/remembers the _meta.terminal_output capability;
streamSessionEventUpdate maps a terminal presentation to
content:[{type:'terminal',terminalId}] + _meta.terminal_info on the call and
_meta.terminal_output on the update WHEN capable — else the unchanged text
path. terminalId is the callId; cwd defaults to the session header. The pure
translator gained a TerminalRendering {enabled,cwd} param (off by default).
Tests via the REAL tool-bash + bash-local: capability ON -> terminal content +
_meta; OFF -> no _meta (text path). The with-key e2e adds a real-model terminal
card case (echo over ACP with the capability on). 773 tests, 100% coverage.
The exit-status pill (_meta.terminal_exit), live streaming
(_meta.terminal_output_delta), and command classification are RFC follow-ups.
This commit is contained in:
@@ -190,5 +190,36 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent e2e: real prompt over
|
||||
expect(bashCall.title.length).toBeGreaterThan(0)
|
||||
expect(bashCall.title).not.toBe('bash') // the old, unhelpful title
|
||||
expect(typeof bashCall.rawInput).toBe('string') // the exact command
|
||||
// Capability OFF: no terminal _meta — the ```console text path renders.
|
||||
expect((bashCall as { _meta?: unknown })._meta).toBeUndefined()
|
||||
}, 180_000)
|
||||
|
||||
it('with the terminal_output capability, a real bash call renders as a terminal card (content + _meta)', async () => {
|
||||
workdir = await mkdtemp(join(tmpdir(), 'acp-e2e-'))
|
||||
spawned = spawnAcpAgent(workdir)
|
||||
const { client, updates } = spawned
|
||||
|
||||
// Advertise the Zed `_meta.terminal_output` capability so the bridge emits
|
||||
// the terminal card for the real bash tool.
|
||||
await client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: { _meta: { terminal_output: true } } })
|
||||
const { sessionId } = await client.newSession({ cwd: workdir, mcpServers: [] })
|
||||
const res = await client.prompt({
|
||||
sessionId,
|
||||
prompt: [{ type: 'text', text: 'Use the bash tool to run: echo ACP_TERMINAL_OK. Then stop.' }],
|
||||
})
|
||||
expect(['end_turn', 'max_tokens']).toContain(res.stopReason)
|
||||
|
||||
// A bash tool_call now carries a terminal content block + _meta.terminal_info
|
||||
// with the session cwd as the header; the matching update streams the output
|
||||
// on _meta.terminal_output.
|
||||
const bashCall = updates.find(u => u.sessionUpdate === 'tool_call' && u.kind === 'execute')
|
||||
if (bashCall?.sessionUpdate !== 'tool_call') throw new Error('expected an execute tool_call')
|
||||
const block = bashCall.content?.[0] as { type: string; terminalId?: string } | undefined
|
||||
expect(block?.type).toBe('terminal')
|
||||
expect(typeof block?.terminalId).toBe('string')
|
||||
const info = (bashCall._meta as { terminal_info?: { terminal_id: string; cwd?: string } }).terminal_info
|
||||
expect(info?.cwd).toBe(workdir)
|
||||
const updatesForTerminal = updates.filter(u => u.sessionUpdate === 'tool_call_update' && (u._meta as { terminal_output?: unknown } | undefined)?.terminal_output !== undefined)
|
||||
expect(updatesForTerminal.length).toBeGreaterThan(0)
|
||||
}, 180_000)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user