DSH / Atlas
2026-07-26implementedfeature

Code Mode UI foundation — run_code description and native-parity dispatch logging

Code Mode 的 UI 基础——run_code 的 description 参数,以及与原生同等保真的分发日志

A `run_code` turn was opaque in every product surface. The call card's title was the raw program text — unreadable at row width, and unlike `bash` (whose required `description` labels the card while the command rides the expanded input) there was no model-authored label at all. The `tool/code-dispatch` event carried only a 200-char, cwd-normalized `resultSummary` of each sub-call, so no UI could ever show what a sub-

English

Problem

A run_code turn was opaque in every product surface. The call card's title was the raw program text — unreadable at row width, and unlike bash (whose required description labels the card while the command rides the expanded input) there was no model-authored label at all. The tool/code-dispatch event carried only a 200-char, cwd-normalized resultSummary of each sub-call, so no UI could ever show what a sub-call actually returned: the web conversation view (chat sub-call rows) renders sub-calls through the exact components that render native tool/result cards, and a bounded summary cannot feed a native-parity card. And the dsh web composition had no way to enable Code Mode at all — the tools row pinned the schema default and the runtime was absent from the tree.

Decision

Three changes, one per obstacle:

  1. run_code gains a required description parameter (bash's exact contract: active voice, 5-10 words, shown in the UI; whitespace-only rejected at execute). presentCall now titles the card with the description and moves the program to rawInput. The prompt-side cost is a few tokens per call; the return is that every surface — TUI card, ACP title, web row — gets a human-readable label without parsing TypeScript.
  2. tool/code-dispatch logs the sub-call's complete model-facing outcomecontent: ContentBlock[] + isError, the tool/result vocabulary — replacing resultSummary and deleting the summarize/cwd-normalization machinery outright. A UI renders a sub-call through the identical code path as a native result, including error text and non-text blocks. The event stays log-only (deriveMessages() ignores it): nothing about model context changes.
  3. DSH_TOOLS_MODE env var on the dsh config tree (native|code|both; unset keeps the schema default): the tools row reads it via !!js, and the worker code runtime is mounted unconditionally (Loader metadata was static when this shipped — no conditional row existed; the later disabled interpolation decision makes one possible but changes nothing here — a native boot only registers the service, workers spawn per run). This is an explicitly temporary configuration hook: per-session tool-presentation selection owned by the web UI is the design goal, and the env var dies when that lands.

Alternatives considered

Keep a bounded summary (raised cap, or a cap + truncated flag). Rejected: the stack's settled requirement is that sub-call rows and details render identically to native calls; any cap forces a second, degraded render path plus truncation UI. The cost accepted instead: a program that reads a large file logs the rendered content verbatim on the dispatch event — uncapped, outside spill policy, growing the session log by the same bytes. Spill integration for the logged copy shipped as code-dispatch log spill.

A --tools-mode CLI flag or profile key. Deferred, not rejected: the flag grammar suggests permanence, and the profile json is user config — both would harden a seam the per-session design intends to remove. An env var reads as the workaround it is.

Log the canonical value instead of rendered content. Rejected: tool/result persists content, not values (the canonical output contract), and native parity means matching that exactly; values remain execution-local everywhere.

Consequences

Session format keeps SESSION_FORMAT_VERSION 0 (pre-release churn does not bump; old logs with resultSummary simply carry an extra unread field and lack content — v0 makes no compatibility promise). Existing code-mode snapshot fixtures were re-recorded. Model-visible surface grew: the run_code schema (one required parameter) and every code-mode system prompt/tool-schema snapshot changed. The web UI work builds directly on the new event payload; live per-sub-call running state reshaped this event into a dispatch start/end pair (live parallel dispatch).

中文

问题

run_code 轮次过去在每个产品界面上都不透明。调用卡片的标题就是原始程序文本,在行宽内无法阅读;而且不同于 bash(其必填的 description 用作卡片标签,命令本身放在展开后的输入里),run_code 完全没有模型撰写的标签。tool/code-dispatch 事件过去只携带每个子调用的 resultSummary(上限 200 字符、经 cwd 归一化),因此任何 UI 都无从展示子调用实际返回的内容:Web 对话视图(chat 子调用行)会用渲染原生 tool/result 卡片的同一批组件来渲染子调用,而有界摘要无法支撑一张与原生同等保真的卡片。同时,dsh web 组合此前根本无法启用 Code Mode:tools 行钉死在 schema 默认值上,配置树里也完全没有该运行时。

决策

三项变更,每项对应一个障碍:

  1. run_code 新增必填的 description 参数(与 bash 完全相同的约定:主动语态、5-10 个词、展示在 UI 中;仅含空白的取值在执行时被拒绝)。presentCall 现在以该 description 作为卡片标题,并把程序文本移入 rawInput。提示词侧的成本是每次调用多出几个 token;换来的是每个界面——TUI 卡片、ACP(Agent Client Protocol)标题、Web 行——都无需解析 TypeScript 就能获得可供人阅读的标签。
  2. tool/code-dispatch 记录子调用面向模型的完整结果content: ContentBlock[]isError,即 tool/result 的词汇),取代 resultSummary,并把摘要与 cwd 归一化机制彻底删除。UI 渲染子调用走的代码路径与渲染原生结果完全相同,包括错误文本和非文本块。该事件仍仅用于日志(deriveMessages() 忽略它):模型上下文没有任何变化。
  3. dsh 配置树上的 DSH_TOOLS_MODE 环境变量native|code|both;未设置时保持 schema 默认值):tools 行通过 !!js 读取它,worker 代码运行时则无条件挂载(本项交付时 loader 元数据仍是静态的,因此不存在条件行;后来的 disabled 插值决策 让条件行成为可能,但此处不变——native 启动只是注册该服务,worker 要到每次运行时才 spawn)。这是一个明确标注为临时的配置钩子:设计目标是让 Web UI 拥有按会话的工具模式选择,该目标落地后,这个环境变量随即退役。

曾考虑的替代方案

保留有界摘要(提高上限,或上限加 truncated 标志)。 否决:本堆叠 PR(Pull Request)链已敲定的要求是,子调用的行与详情必须与原生调用渲染得完全一致;任何上限都会强制引入第二条降级的渲染路径,外加截断 UI。转而接受的代价是:读取大文件的程序会把渲染后的内容原样记录在分发事件上,不设上限、位于 spill 策略之外,并以同样的字节数增大会话日志。持久化副本的 spill 集成已作为 code-dispatch 日志 spill 交付。

一个 --tools-mode CLI(命令行界面)标志或 profile 配置键。 推迟,而非否决:标志语法暗示永久性,profile JSON 又是用户配置;两者都会固化这个 seam,而按会话选择的设计本就打算移除它。环境变量则如实呈现了它权宜之计的本质。

记录规范 value,而非渲染后的 content 否决:tool/result 持久化的是内容而非值(见规范输出约定),与原生同等保真意味着与之精确对齐;值始终仅存在于执行期本地。

后果

会话格式保持 SESSION_FORMAT_VERSION 为 0(预发布阶段的变动不递增版本号;携带 resultSummary 的旧日志只是多出一个不被读取的字段并缺少 content;v0 不作任何兼容性承诺)。既有的 Code Mode 快照 fixture(测试前置数据)已重新录制。模型可见范围扩大了:run_code 的 schema(新增一个必填参数)以及每一份 Code Mode 系统提示词/工具 schema 快照都发生了变化。Web UI 工作直接构建在新的事件载荷之上;每个子调用的实时运行状态已把本事件重塑为一对分发 start/end 事件(实时并行分发)。