Browse Source

refactor(tools): move render-intent vocabulary to presentation.ts

The tool render-intent vocabulary (ToolCallView/ToolResultView + members,
FileLocation, FileDiff, ToolCallKind) is the UI-facing surface of
dsh-tools; it lived inline in index.ts alongside the registry and
execution core. Move it to its own presentation.ts module so index.ts is
the registry + execute waterfall and the presentation vocabulary is a
separate, one-directional dependency.

presentation.ts owns ONLY render-intent types and references none of the
execution types; index.ts imports the view types for ToolDefinition's
presentCall/presentResult signatures (clean acyclic index -> presentation).
The opaque `meta` presentation channel (ToolExecuteReturn, ToolResult,
ToolExecutionResult) is execution plumbing and stays in index.ts.

Public surface unchanged: index.ts re-exports the vocabulary, so consumers
(tool-fs/tool-bash/tool-web/tool-todo, the ACP bridge) keep importing from
@deepseek-ai/dsh-tools with zero churn. No producer/bridge/test edits; a
pure internal relocation with no observable-output change (snapshot goldens
untouched).
Tianyi Cui 2 months ago
parent
commit
539051b2c9

+ 3 - 3
docs/cordis-catalog/events-and-services.md

@@ -337,7 +337,7 @@ A tool was registered or unregistered (the available tool set changed).
 'tools/change'(): void
 ```
 
-Source: [`packages/core/tools/src/index.ts:48`](../../packages/core/tools/src/index.ts)
+Source: [`packages/core/tools/src/index.ts:66`](../../packages/core/tools/src/index.ts)
 
 #### `tools/execute` — waterfall
 
@@ -349,7 +349,7 @@ Waterfall around every tool execution — the single seam where sandbox, permiss
 
 Types: [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
 
-Source: [`packages/core/tools/src/index.ts:43`](../../packages/core/tools/src/index.ts)
+Source: [`packages/core/tools/src/index.ts:61`](../../packages/core/tools/src/index.ts)
 
 ### `web/*`
 
@@ -559,7 +559,7 @@ async execute(exec: ToolExecution): Promise<ToolExecutionResult>
 
 Types: [ToolDefinition](../core-data-structures/tools.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
 
-Source: [`packages/core/tools/src/index.ts:366`](../../packages/core/tools/src/index.ts)
+Source: [`packages/core/tools/src/index.ts:199`](../../packages/core/tools/src/index.ts)
 
 ### `ctx.web` — `WebService`
 

+ 1 - 1
docs/core-data-structures/tools.md

@@ -121,4 +121,4 @@ How a tool wants its call shown in a UI (an editor tool-call card, a CLI log lin
 
 `ToolCallKind` (`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`) picks an icon on a generic card. `FileLocation` (`{ path, line? }`) and `FileDiff` (`{ path, oldText, newText }`) are the shared file-card vocabulary. The design is pinned in [the render-intent-union RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md); the ACP bridge maps a `diff` card to a `{ type: 'diff' }` content block, a `terminal` card to the `_meta` terminal convention, and relativizes a file card's title against the session cwd.
 
-The full presentation field docs live in [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts). The bash tool's own schemas (`bash`/`bash_output`/`bash_kill`) and the executor they drive are on [bash.md](bash.md).
+The full presentation field docs live in [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts). The bash tool's own schemas (`bash`/`bash_output`/`bash_kill`) and the executor they drive are on [bash.md](bash.md).

+ 18 - 185
packages/core/tools/src/index.ts

@@ -12,6 +12,7 @@ import type { CallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
 import { HarnessError } from '@deepseek-ai/dsh-llm'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import type {} from '@deepseek-ai/dsh-system-prompt'
+import type { ToolCallView, ToolResultView } from './presentation.ts'
 
 export {
   defineTool,
@@ -26,6 +27,23 @@ export {
   type JsonSchemaObject,
 } from './schema.ts'
 
+// The render-intent vocabulary a tool declares via `presentCall`/`presentResult`
+// lives in its own UI-facing module; re-export it so `@deepseek-ai/dsh-tools`
+// stays the single public surface for consumers (producers + the ACP bridge).
+export type {
+  ToolCallKind,
+  FileLocation,
+  FileDiff,
+  ToolCallView,
+  GenericCallView,
+  TerminalCallView,
+  DiffCallView,
+  ToolResultView,
+  GenericResultView,
+  TerminalResultView,
+  DiffResultView,
+} from './presentation.ts'
+
 declare module 'cordis' {
   interface Context {
     tools: ToolRegistry
@@ -54,191 +72,6 @@ declare module 'cordis' {
 // parallel execution — Claude Code partitions read-only tools; phase 1
 // executes sequentially).
 
-/**
- * Category of a tool call, used by a UI to pick an icon / treatment. A neutral
- * vocabulary owned here (NOT an ACP type) so tools describe themselves without
- * depending on any client protocol; a UI bridge maps it to its own enum. The
- * member set mirrors the common ACP `ToolKind` values; `other` is the default.
- */
-export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'
-
-/**
- * A file location a tool reads or modifies, so a capable UI can "follow along" —
- * highlight or jump to the file (and line) as the tool runs. Provider-neutral;
- * a UI bridge maps it to its own affordance (the ACP bridge forwards it as
- * `tool_call.locations`). `path` is what the tool operated on (the model-facing
- * path); `line` is an optional 1-based line to focus (e.g. a read's offset).
- */
-export interface FileLocation {
-  path: string
-  line?: number
-}
-
-/**
- * A single-file change a tool is about to make, for a UI that renders inline
- * diffs (an editor's diff card). Provider-neutral; the ACP bridge forwards it as
- * a `{ type: 'diff' }` tool-call content block. `oldText` is `null` for a
- * new-file create (nothing to diff against); an overwrite also uses `null`,
- * because a call-time presenter has no access to the file's prior content.
- */
-export interface FileDiff {
-  path: string
-  /** Prior content, or `null` for a new file / an overwrite (no prior content available at call time). */
-  oldText: string | null
-  /** Content after the change. */
-  newText: string
-}
-
-/**
- * How a tool wants ONE of its calls shown in a UI (an editor's tool-call card, a
- * CLI log line) BEFORE the result is known — the *pending* state. A `card`-tagged
- * discriminated union: a tool declares its render INTENT once and a UI bridge
- * switches on `card` to map it to the bridge's own wire shape. Provider-neutral —
- * the tool owns its presentation, so a UI never special-cases tool names.
- *
- * Returned by {@link ToolDefinition.presentCall}. See the render-intent-union
- * RFC (docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md).
- */
-export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView
-
-/**
- * The default card: a titled tool-call row with an optional category icon, a
- * salient raw input, extra content blocks, and follow-along file locations. Any
- * tool whose call is not a terminal or a diff uses this.
- */
-export interface GenericCallView {
-  card: 'generic'
-  /**
-   * Human-readable, always-visible label describing what THIS call does. Keep it
-   * short — a UI shows it as a card header / log line.
-   */
-  title: string
-  /** Category for icon/treatment; defaults to `other` when omitted. */
-  kind?: ToolCallKind
-  /**
-   * The salient input to surface in a detail/expanded view (e.g. a background
-   * task id). Omit to show nothing; a string renders as-is, an object as pretty
-   * JSON. NOT the full raw args object unless that is genuinely what a reader wants.
-   */
-  rawInput?: unknown
-  /**
-   * UI-facing content blocks to show on the pending call alongside the title.
-   * Omit to show none. A UI maps these to its own content blocks.
-   */
-  content?: ContentBlock[]
-  /** Files this call reads/modifies, for editor follow-along. Omit for a call that touches no file. */
-  locations?: FileLocation[]
-}
-
-/**
- * A call that IS a shell command running in a working directory: a capable UI
- * renders it as a terminal card (cwd-headed, with the command as the title and
- * live/afterward output from the {@link TerminalResultView}); an incapable UI
- * falls back to a generic card whose body is the fenced command output. Set by a
- * tool whose call is a foreground command (e.g. `bash`).
- */
-export interface TerminalCallView {
-  card: 'terminal'
-  /** The command, shown as the terminal card's title / header line. */
-  title: string
-  /**
-   * A human-readable one-line summary of what the command does, rendered ABOVE
-   * the terminal card (the card itself has no description slot). Omit for none.
-   */
-  description?: string
-  /**
-   * Working directory the command runs in, shown as the terminal header. An
-   * ABSOLUTE path is used as-is; a RELATIVE path is resolved by the UI bridge
-   * against the session workspace (the pure presenter can't see the session cwd).
-   * Omit entirely to let the bridge use the session workspace.
-   */
-  cwd?: string
-}
-
-/**
- * A call that creates or modifies files, rendered as an inline diff card by a
- * capable UI. Set by a tool whose call writes/edits a file (e.g. `write`,
- * `edit`). The diffs are derived from the call ARGUMENTS (a create's `oldText` is
- * `null`); the tool emits a separate {@link DiffResultView} after `execute` — the
- * applied change (an edit/overwrite hunk with context, or a whole-file diff for a
- * create).
- */
-export interface DiffCallView {
-  card: 'diff'
-  /** Card header (e.g. `Write foo.txt`). */
-  title: string
-  /** One entry per file the call changes. */
-  diffs: FileDiff[]
-  /** Files this call modifies, for editor follow-along (usually the diffs' paths). */
-  locations?: FileLocation[]
-}
-
-/**
- * How a tool wants the COMPLETED call shown — the *result* state, after `execute`
- * returns. A `card`-tagged union mirroring {@link ToolCallView}: a UI switches on
- * `card`. Lets the tool reformat its result for a UI distinctly from the
- * model-facing text it returned from `execute`. Returned by
- * {@link ToolDefinition.presentResult}; omitting the method keeps the pending
- * title and renders the raw result content.
- */
-export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView
-
-/**
- * The default completed card: an optional replacement title and reformatted
- * content. Omit a field to keep the pending title / render the raw result content.
- */
-export interface GenericResultView {
-  card: 'generic'
-  /** Replacement title for the completed call. Omit to keep the pending-state title. */
-  title?: string
-  /**
-   * UI-facing result content (harness {@link ContentBlock}s), reformatted from
-   * the model-facing result. Omit to let the UI render the raw result content.
-   */
-  content?: ContentBlock[]
-}
-
-/**
- * The completed state of a {@link TerminalCallView}: the captured output and exit
- * status. A capable UI renders `output` in the terminal card and shows an
- * exit-status pill; an incapable UI gets a fenced ```console fallback the BRIDGE
- * derives from `output` (the tool does not double-encode it).
- */
-export interface TerminalResultView {
-  card: 'terminal'
-  /** Replacement title for the completed call. Omit to keep the pending-state title. */
-  title?: string
-  /** Captured command output (stdout+stderr as the tool chooses to combine them). */
-  output?: string
-  /**
-   * Process exit code, when the run ended by exiting (not a signal). Lets a
-   * capable UI show an exit-status pill. Omit when killed by a signal or unknown.
-   */
-  exitCode?: number
-  /** Signal name that killed the process (e.g. `SIGTERM`). Mutually exclusive with `exitCode`. */
-  signal?: string
-}
-
-/**
- * A completed file mutation rendered as an inline diff card, the *result-time*
- * analogue of {@link DiffCallView}. Set by a tool whose `execute` applied a file
- * change (e.g. `write`, `edit`): `diffs` are the change to show — typically the
- * APPLIED hunks computed from the before/after content (one entry per hunk, each
- * with surrounding context lines), so the editor shows the real change in place;
- * a tool with no before-image (e.g. a file create) may instead give a whole-file
- * diff (`oldText: null`). A `tool_call_update`'s content REPLACES the call's
- * content in an editor, so a mutation tool returns this even when it duplicates
- * the call-time snippet — otherwise the model-facing result text would replace
- * (clobber) the pending diff card.
- */
-export interface DiffResultView {
-  card: 'diff'
-  /** Replacement title for the completed call. Omit to keep the pending-state title. */
-  title?: string
-  /** The change to show, in file order — applied contextual hunks, or a whole-file diff when there is no before-image. */
-  diffs: FileDiff[]
-}
-
 /**
  * What a tool's `execute` returns. The bare {@link ContentBlock}`[]` form is the
  * common case (model-facing content only); the object form additionally attaches

+ 206 - 0
packages/core/tools/src/presentation.ts

@@ -0,0 +1,206 @@
+/**
+ * Tool render-intent vocabulary: the provider-neutral types a tool declares via
+ * {@link ToolDefinition.presentCall}/{@link ToolDefinition.presentResult} to say
+ * how ONE of its calls renders in a UI (an editor's tool-call card, a CLI log
+ * line). A UI bridge switches on the `card` tag to map each intent to its own
+ * wire shape, so a UI never special-cases tool names.
+ *
+ * This is the UI-facing surface of `dsh-tools`, kept separate from the registry
+ * and execution core in `index.ts`: this module owns ONLY presentation
+ * vocabulary and references none of the execution types, so the dependency runs
+ * one way (`index.ts` imports these views for the `ToolDefinition` method
+ * signatures). The opaque `meta` presentation channel is execution plumbing and
+ * lives with the registry in `index.ts`, not here.
+ *
+ * See the render-intent-union RFC
+ * (docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md).
+ *
+ * @module @deepseek-ai/dsh-tools/src/presentation
+ */
+
+import type { ContentBlock } from '@deepseek-ai/dsh-llm'
+
+/**
+ * Category of a tool call, used by a UI to pick an icon / treatment. A neutral
+ * vocabulary owned here (NOT an ACP type) so tools describe themselves without
+ * depending on any client protocol; a UI bridge maps it to its own enum. The
+ * member set mirrors the common ACP `ToolKind` values; `other` is the default.
+ */
+export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'
+
+/**
+ * A file location a tool reads or modifies, so a capable UI can "follow along" —
+ * highlight or jump to the file (and line) as the tool runs. Provider-neutral;
+ * a UI bridge maps it to its own affordance (the ACP bridge forwards it as
+ * `tool_call.locations`). `path` is what the tool operated on (the model-facing
+ * path); `line` is an optional 1-based line to focus (e.g. a read's offset).
+ */
+export interface FileLocation {
+  path: string
+  line?: number
+}
+
+/**
+ * A single-file change a tool is about to make, for a UI that renders inline
+ * diffs (an editor's diff card). Provider-neutral; the ACP bridge forwards it as
+ * a `{ type: 'diff' }` tool-call content block. `oldText` is `null` for a
+ * new-file create (nothing to diff against); an overwrite also uses `null`,
+ * because a call-time presenter has no access to the file's prior content.
+ */
+export interface FileDiff {
+  path: string
+  /** Prior content, or `null` for a new file / an overwrite (no prior content available at call time). */
+  oldText: string | null
+  /** Content after the change. */
+  newText: string
+}
+
+/**
+ * How a tool wants ONE of its calls shown in a UI (an editor's tool-call card, a
+ * CLI log line) BEFORE the result is known — the *pending* state. A `card`-tagged
+ * discriminated union: a tool declares its render INTENT once and a UI bridge
+ * switches on `card` to map it to the bridge's own wire shape. Provider-neutral —
+ * the tool owns its presentation, so a UI never special-cases tool names.
+ *
+ * Returned by {@link ToolDefinition.presentCall}. See the render-intent-union
+ * RFC (docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md).
+ */
+export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView
+
+/**
+ * The default card: a titled tool-call row with an optional category icon, a
+ * salient raw input, extra content blocks, and follow-along file locations. Any
+ * tool whose call is not a terminal or a diff uses this.
+ */
+export interface GenericCallView {
+  card: 'generic'
+  /**
+   * Human-readable, always-visible label describing what THIS call does. Keep it
+   * short — a UI shows it as a card header / log line.
+   */
+  title: string
+  /** Category for icon/treatment; defaults to `other` when omitted. */
+  kind?: ToolCallKind
+  /**
+   * The salient input to surface in a detail/expanded view (e.g. a background
+   * task id). Omit to show nothing; a string renders as-is, an object as pretty
+   * JSON. NOT the full raw args object unless that is genuinely what a reader wants.
+   */
+  rawInput?: unknown
+  /**
+   * UI-facing content blocks to show on the pending call alongside the title.
+   * Omit to show none. A UI maps these to its own content blocks.
+   */
+  content?: ContentBlock[]
+  /** Files this call reads/modifies, for editor follow-along. Omit for a call that touches no file. */
+  locations?: FileLocation[]
+}
+
+/**
+ * A call that IS a shell command running in a working directory: a capable UI
+ * renders it as a terminal card (cwd-headed, with the command as the title and
+ * live/afterward output from the {@link TerminalResultView}); an incapable UI
+ * falls back to a generic card whose body is the fenced command output. Set by a
+ * tool whose call is a foreground command (e.g. `bash`).
+ */
+export interface TerminalCallView {
+  card: 'terminal'
+  /** The command, shown as the terminal card's title / header line. */
+  title: string
+  /**
+   * A human-readable one-line summary of what the command does, rendered ABOVE
+   * the terminal card (the card itself has no description slot). Omit for none.
+   */
+  description?: string
+  /**
+   * Working directory the command runs in, shown as the terminal header. An
+   * ABSOLUTE path is used as-is; a RELATIVE path is resolved by the UI bridge
+   * against the session workspace (the pure presenter can't see the session cwd).
+   * Omit entirely to let the bridge use the session workspace.
+   */
+  cwd?: string
+}
+
+/**
+ * A call that creates or modifies files, rendered as an inline diff card by a
+ * capable UI. Set by a tool whose call writes/edits a file (e.g. `write`,
+ * `edit`). The diffs are derived from the call ARGUMENTS (a create's `oldText` is
+ * `null`); the tool emits a separate {@link DiffResultView} after `execute` — the
+ * applied change (an edit/overwrite hunk with context, or a whole-file diff for a
+ * create).
+ */
+export interface DiffCallView {
+  card: 'diff'
+  /** Card header (e.g. `Write foo.txt`). */
+  title: string
+  /** One entry per file the call changes. */
+  diffs: FileDiff[]
+  /** Files this call modifies, for editor follow-along (usually the diffs' paths). */
+  locations?: FileLocation[]
+}
+
+/**
+ * How a tool wants the COMPLETED call shown — the *result* state, after `execute`
+ * returns. A `card`-tagged union mirroring {@link ToolCallView}: a UI switches on
+ * `card`. Lets the tool reformat its result for a UI distinctly from the
+ * model-facing text it returned from `execute`. Returned by
+ * {@link ToolDefinition.presentResult}; omitting the method keeps the pending
+ * title and renders the raw result content.
+ */
+export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView
+
+/**
+ * The default completed card: an optional replacement title and reformatted
+ * content. Omit a field to keep the pending title / render the raw result content.
+ */
+export interface GenericResultView {
+  card: 'generic'
+  /** Replacement title for the completed call. Omit to keep the pending-state title. */
+  title?: string
+  /**
+   * UI-facing result content (harness {@link ContentBlock}s), reformatted from
+   * the model-facing result. Omit to let the UI render the raw result content.
+   */
+  content?: ContentBlock[]
+}
+
+/**
+ * The completed state of a {@link TerminalCallView}: the captured output and exit
+ * status. A capable UI renders `output` in the terminal card and shows an
+ * exit-status pill; an incapable UI gets a fenced ```console fallback the BRIDGE
+ * derives from `output` (the tool does not double-encode it).
+ */
+export interface TerminalResultView {
+  card: 'terminal'
+  /** Replacement title for the completed call. Omit to keep the pending-state title. */
+  title?: string
+  /** Captured command output (stdout+stderr as the tool chooses to combine them). */
+  output?: string
+  /**
+   * Process exit code, when the run ended by exiting (not a signal). Lets a
+   * capable UI show an exit-status pill. Omit when killed by a signal or unknown.
+   */
+  exitCode?: number
+  /** Signal name that killed the process (e.g. `SIGTERM`). Mutually exclusive with `exitCode`. */
+  signal?: string
+}
+
+/**
+ * A completed file mutation rendered as an inline diff card, the *result-time*
+ * analogue of {@link DiffCallView}. Set by a tool whose `execute` applied a file
+ * change (e.g. `write`, `edit`): `diffs` are the change to show — typically the
+ * APPLIED hunks computed from the before/after content (one entry per hunk, each
+ * with surrounding context lines), so the editor shows the real change in place;
+ * a tool with no before-image (e.g. a file create) may instead give a whole-file
+ * diff (`oldText: null`). A `tool_call_update`'s content REPLACES the call's
+ * content in an editor, so a mutation tool returns this even when it duplicates
+ * the call-time snippet — otherwise the model-facing result text would replace
+ * (clobber) the pending diff card.
+ */
+export interface DiffResultView {
+  card: 'diff'
+  /** Replacement title for the completed call. Omit to keep the pending-state title. */
+  title?: string
+  /** The change to show, in file order — applied contextual hunks, or a whole-file diff when there is no before-image. */
+  diffs: FileDiff[]
+}

+ 2 - 1
packages/core/tools/src/schema.ts

@@ -20,7 +20,8 @@
  */
 
 import { assertNever, HarnessError } from '@deepseek-ai/dsh-llm'
-import type { ToolCallView, ToolDefinition, ToolExecuteReturn, ToolExecution, ToolResult, ToolResultView } from './index.ts'
+import type { ToolDefinition, ToolExecuteReturn, ToolExecution, ToolResult } from './index.ts'
+import type { ToolCallView, ToolResultView } from './presentation.ts'
 
 // ---------------------------------------------------------------------------
 // SchemaSpec — the author-facing per-property type