feat(acp): show the command in execute titles; test via the real bash tool; RFC for terminal rendering
- bash presentCall title is now "description — command" (e.g. "List files in
src — ls -la src"). An execute-kind ACP card HIDES rawInput (Zed renders it
only for non-terminal tools), so the command must ride in the always-visible
title to be seen — matching how claude-agent-acp/codex-acp title execute
tools. The command stays in rawInput too for non-execute UIs that show it.
- Rework the acp tool-call presentation tests (turns + load replay) to drive the
REAL dsh-tool-bash + dsh-bash-local via a new makeBridgeHarness({ withBash })
option, running an actual `echo` — instead of an inline fake bash tool. The
mock MODEL still scripts the call (deterministic, no key), but the tool and
executor are real, so the test verifies the shipping presentCall/presentResult.
- AGENTS.md: add the principle "prefer the REAL implementation over a mock/
stand-in in tests" (mock only the expensive/non-deterministic boundary).
- RFC (proposed): the ACP terminal sub-protocol + command classification — the
capability-gated rich rendering (live cwd-header terminal card, classify a
`cat` as a read / `grep` as a search) that the reference adapters do; the
fenced ```console text block stays the no-capability baseline. Studied
codex-acp, claude-agent-acp, and Zed's renderer to ground it.
This commit is contained in:
@@ -34,7 +34,7 @@ The owning agent is recorded per task id at spawn and kept for the lifetime of t
|
||||
|
||||
## UI presentation
|
||||
|
||||
These tools own how their calls render in a UI (an editor's tool-call card) via the `dsh-tools` `presentCall`/`presentResult` seam — a UI never special-cases tool names. For `bash`: the model-written `description` is the always-visible **title** (e.g. "List files in the current directory"), the exact `command` is the **rawInput** (the verbatim command stays visible in a detail view without crowding the title), `kind` is `execute` (terminal/run treatment), and the completed output is wrapped in a fenced ` ```console ` block — a UI-only affordance, so the model-facing result text stays unfenced. `bash_output`/`bash_kill` present a task-scoped title ("Read output from background task bash-3" / "Kill background task bash-3") with the task id as rawInput. These methods are pure/display-only (they also run on `session/load` replay), and a malformed/older logged arg shape falls back to a generic presentation rather than throwing. See `packages/tools` ("Tool-owned UI presentation") and `packages/acp` ("Tool-call presentation").
|
||||
These tools own how their calls render in a UI (an editor's tool-call card) via the `dsh-tools` `presentCall`/`presentResult` seam — a UI never special-cases tool names. For `bash`: the **title** is the model-written `description` followed by the exact `command` ("List files in src — ls -la src"), `kind` is `execute` (terminal/run treatment), and the `command` is ALSO the **rawInput**. Why both in the title: an execute-kind card hides `rawInput` (Zed renders it only for non-terminal tools), so the command must ride in the always-visible title to be seen — the reference ACP adapters (claude-agent-acp, codex-acp) likewise put the command in an execute tool's title. The completed output is wrapped in a fenced ` ```console ` block — a UI-only affordance, so the model-facing result text stays unfenced. `bash_output`/`bash_kill` present a task-scoped title ("Read output from background task bash-3" / "Kill background task bash-3") with the task id as rawInput. These methods are pure/display-only (they also run on `session/load` replay), and a malformed/older logged arg shape falls back to a generic presentation rather than throwing. See `packages/tools` ("Tool-owned UI presentation") and `packages/acp` ("Tool-call presentation").
|
||||
|
||||
## Background completion notices
|
||||
|
||||
|
||||
@@ -133,15 +133,18 @@ export function renderResult(result: BashRunResult): string {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Pending-state presentation for a `bash` call: the model-written `description`
|
||||
* is the always-visible title (the schema requires it precisely so a UI has a
|
||||
* readable summary — "List files in the current directory"), `kind: 'execute'`
|
||||
* (a terminal/run treatment), and the exact `command` is the `rawInput` so the
|
||||
* verbatim command stays visible in a UI's detail view without crowding the
|
||||
* title. Mirrors how Zed / the reference ACP adapters render execute tools.
|
||||
* Pending-state presentation for a `bash` call. The title is the model-written
|
||||
* `description` followed by the exact `command` ("List files — ls -la src"):
|
||||
* `kind: 'execute'` gets a terminal/run treatment in a UI, but an execute-kind
|
||||
* card HIDES `rawInput` (Zed: `should_show_raw_input = !is_terminal_tool`), so
|
||||
* the command MUST ride in the always-visible title to be seen — the reference
|
||||
* ACP adapters (claude-agent-acp, codex-acp) likewise put the command in the
|
||||
* title for execute tools. The description leads (a readable summary the schema
|
||||
* requires); the command follows so the verbatim text is still there. `rawInput`
|
||||
* still carries the bare command for non-execute UIs that DO render it.
|
||||
*/
|
||||
function presentBashCall(args: { command: string; description: string }): ToolCallPresentation {
|
||||
return { title: args.description, kind: 'execute', rawInput: args.command }
|
||||
return { title: `${args.description} — ${args.command}`, kind: 'execute', rawInput: args.command }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -564,10 +564,10 @@ describe('status lines', () => {
|
||||
})
|
||||
|
||||
describe('tool-owned UI presentation (presentCall / presentResult)', () => {
|
||||
it('bash presentCall: the model description is the title, the command is the rawInput, kind execute', async () => {
|
||||
it('bash presentCall: title is "description — command" (execute cards hide rawInput), command also in rawInput', async () => {
|
||||
const ctx = await setup()
|
||||
const present = ctx.tools.get('bash')!.presentCall!({ command: 'ls -la src', description: 'List files in src' })
|
||||
expect(present).toEqual({ title: 'List files in src', kind: 'execute', rawInput: 'ls -la src' })
|
||||
const present = ctx.tools.get('bash')?.presentCall?.({ command: 'ls -la src', description: 'List files in src' })
|
||||
expect(present).toEqual({ title: 'List files in src — ls -la src', kind: 'execute', rawInput: 'ls -la src' })
|
||||
})
|
||||
|
||||
it('bash presentResult: wraps the model-facing text in a fenced console block', async () => {
|
||||
|
||||
Reference in New Issue
Block a user