Dedicated full-screen TUI front door
独立的全屏 TUI 入口
The reusable TUI package remains implemented, but [`dsh` no longer ships it as an application entrypoint](../simplification/2026-08-03-explicit-config-dsh-entrypoint.md). This note continues to own the package boundary and terminal behavior; the later note owns product composition. At the time this front door was introduced, the line-oriented agent handled pipes and ordinary terminals, but a full-screen coding interf
English
Problem
The reusable TUI package remains implemented, but dsh no longer ships it as an application entrypoint. This note continues to own the package boundary and terminal behavior; the later note owns product composition.
At the time this front door was introduced, the line-oriented agent handled pipes and ordinary terminals, but a full-screen coding interface had to own raw input, differential screen drawing, cursor state, overlays, and terminal restoration. Combining those contracts in one UI plugin would have coupled a stream-oriented path to a TTY-only lifecycle. The later redundant-agent removal removes that line agent; this Note continues to own the TUI design.
The interactive channel must remain a Cordis plugin over the same agent, session, tool, and user-interaction services as every other front door. It needs to resume durable history, follow compaction replacements, display tool-owned presentation, and restore the terminal on startup failure and disposal. A standalone chat application or a second agent composition would duplicate behavior outside the plugin graph.
Decision
DeepSeek Harness ships @deepseek-ai/dsh-tui as a dedicated Cordis plugin. It owns terminal input and presentation only; agent lifecycle, session persistence, tool execution, and the model-facing question tool remain separate composition entries. The plugin requires both stdin and stdout to be TTYs and fails instead of silently changing to line-oriented behavior.
The package is a terminal front door, not a complete application. A host mounts @deepseek-ai/dsh-tui before its configured agent and composes the backends, tools, and policies around it. The product CLI currently ships no terminal composition; non-interactive tasks use headless mode, Web owns the installed human surface, and ACP remains a separate automation protocol.
The host supplies the exact generated or resumed SessionId used by its pre-created agent. The TUI waits for the matching root agent and enters full-screen mode only after that agent exists. A matching agent-loop/config-start-failed event is therefore reported before screen takeover.
Session projection and interaction
The TUI rebuilds the transcript from the append-origin session events, so resumed history keeps every message the reader already saw; a compacted range stays readable behind one marker instead of matching the model-visible conversation (append-origin transcript). It renders Markdown text and reasoning, including fenced code with hidden Markdown markers, a dim optional language label, and a code-colored body, token totals, the latest todo/write plan, and tool cards produced through each tool definition's presentCall and presentResult methods. Long card bodies retain a configurable head/tail preview with the hidden-line count; one terminal control expands or collapses every card. Pending chunks and tool calls update the same components that completed events settle.
Editor input calls agent.send() while idle and agent.steer() while a turn is running. Cancellation, reasoning visibility, tool-card expansion, redraw, transcript clearing, and exit are terminal-only controls. /exit and /quit share the same exit path: they cancel an active turn, wait for idle, and then restore and close the terminal. The idle footer derives context occupancy from tokenMeter and shows the selected model and explicit reasoning effort; during a run, elapsed activity and the Escape interrupt hint replace that summary. /status remains available in either state and appends a detailed terminal-only snapshot: session identity and timestamps, selected model, reasoning effort/default state and reasoning visibility, lifecycle counts folded from the event log, the same deduplicated usage buckets and KV-cache rate as the footer, and context use from tokenMeter plus the selected model's advertised capacity. The plugin registers the shared userInteraction provider and presents queued questions in a wide bottom-left keyboard panel with batch progress, numbered options, and aligned descriptions; the panel's controls hint lists only actions meaningful for the current option count, omitting navigation when exactly one option is shown; agent behavior and answer logging remain owned by their existing services.
The /model command presents the advisory ctx.llm catalog as a keyboard selector and changes only this TUI session's target; argument forms remain available for direct selection. The selector carries a filter box above the list: typing narrows the rows to a case-insensitive substring over each row's provider/model label, model name, and description, keeping the selection on the previously highlighted row when it survives the filter; Escape clears a non-empty filter before a second Escape cancels the selector. Each model row owns the adapter-advertised reasoning-effort order and default: Shift+Tab cycles that row's efforts, includes provider-default behavior when the adapter advertises no default, and leaves models without selectable metadata unchanged. Agent-scoped prompt-assembly and request waterfalls snapshot one provider/model/reasoning-effort target per step, so {{provider}} / {{model}} interpolation and request routing cannot split when a command arrives during assembly. The latest logged request header restores a used target; a selection that never reaches a request remains process-local.
Terminal ownership
Before model output, session data, tool presentation, questions, configuration, or diagnostics reach pi-tui or the terminal title, displayText() renders C0 and C1 controls other than line feeds as visible hexadecimal escapes. Only the TUI and pi-tui create ANSI control sequences.
The built-in palette uses standard 16-color ANSI foregrounds and SGR attributes, keeps body text and backgrounds at terminal defaults, and uses reverse video for selection. Host terminals therefore remap the interface for light and dark themes without a TUI-specific theme setting; color: false removes styling.
Verification
The implemented TUI terminal-state snapshot Agent Note owns the package verification contract: direct behavior tests and semantic terminal snapshots. A deployment shipping this front door owns its assembled transcript and process/PTY acceptance. The package README owns configuration, commands, model-visible effects, and current limitations.
Alternatives considered
- Keep readline and full-screen modes inside
@deepseek-ai/dsh-stdio— rejected because line-oriented output and differential TTY rendering have different dependencies, input rules, logging ownership, and teardown obligations. Separate packages keep the pipe-safe contract small and explicit. - Let the TUI plugin silently downgrade when either stream is not a TTY — rejected because a fallback hides deployment mistakes and changes interaction semantics. A host may select a different front door; an explicitly mounted TUI fails loud.
- Keep TUI wiring and tests under the readline
repl-agentleaf — rejected at the time because one leaf would represent two distinct front doors. The later product-entrypoint removal deleted that application wiring while retaining the package boundary. - Mutate
agent.optionswhen/modelruns — rejected because creation options do not provide an atomic boundary between asynchronous prompt assembly and request routing. Agent-scoped waterfalls preserve immutable creation input and snapshot the selected pair for each step.
Consequences
- Deployments that mount the TUI gain a stateful Markdown, card, plan, and question interface with no second terminal protocol to keep aligned.
- The TUI carries a pi-tui dependency and a strict TTY requirement; non-TTY deployments use the Headless app or a structured protocol.
- Session projection makes resume consistent with the durable conversation, but one configured session owns the transcript and editor.
- Tool packages extend terminal cards through their existing presentation methods without adding tool-specific branches to the TUI.
- Model and reasoning-effort selection use adapter-advertised metadata without turning catalog membership into request validation; unused selections are not durable state.
中文
问题
可复用的 TUI 包(package)仍然保留实现,但 dsh 不再将其作为应用入口交付。本记录继续负责包边界和终端行为;后续记录负责产品组合。
在本入口引入时,面向行的 agent 负责 pipe 与普通终端,但全屏 coding 界面必须负责原始输入、差分绘制、光标状态、浮层和终端恢复。把这两类契约合并到一个 UI 插件中,会迫使面向 stream 的路径依赖仅适用于 TTY 的生命周期。后续的移除重复 agent 决策移除了这个面向行 agent;本 Note 继续负责 TUI 设计。
交互通道必须继续作为 Cordis 插件,使用与其他入口相同的 agent(智能体)、会话、工具和用户交互服务。它需要恢复持久历史、跟随压缩替换、显示工具自有的呈现内容,并在启动失败和资源释放时恢复终端。独立聊天应用或第二套 agent 组合会在插件图之外重复实现这些行为。
决策
DeepSeek Harness 将 @deepseek-ai/dsh-tui 作为独立的 Cordis 插件交付。该插件只负责终端输入与呈现;agent 生命周期、会话持久化、工具执行以及模型可见的提问工具仍由不同组合项负责。插件要求 stdin 和 stdout 均为 TTY;条件不满足时会失败,不会静默切换为逐行输出。
该包是终端入口,而不是完整应用。宿主在已配置 agent 之前挂载 @deepseek-ai/dsh-tui,并围绕它组合后端、工具和策略。产品 CLI 目前不交付终端组合;非交互任务使用 headless 模式,Web 是已安装产品中面向人的界面,而 ACP 仍是独立的自动化协议。
宿主提供其预创建 agent 使用的同一个新建或恢复 SessionId。TUI 等待相符的根 agent 出现,然后才进入全屏模式。因此,相符的 agent-loop/config-start-failed 事件会在接管屏幕前报告。
会话投影与交互
TUI 从追加来源的会话事件重建 transcript(文本记录),因此恢复后的历史会保留读者已经看到的每条消息;被压缩的范围不再与模型可见会话保持一致,而是留在一行标记之后仍可阅读(追加来源的 transcript)。TUI 渲染 Markdown 文本与推理(其中围栏代码块隐藏 Markdown 标记、保留一个暗色的可选语言标签,并使用代码配色的正文)、token 用量、最新 todo/write 计划,以及各工具定义通过 presentCall 和 presentResult 方法生成的工具卡片。较长的工具卡片正文会保留可配置的头尾预览,并显示隐藏行数;一个终端控制可以展开或收起全部卡片。进行中的分片与工具调用会更新同一组组件,随后由完成事件收束状态。
agent 空闲时,编辑器输入调用 agent.send();轮次运行中则调用 agent.steer()。取消、推理显隐、工具卡片展开、重绘、清空 transcript 和退出都只是终端控制。/exit 和 /quit 共用同一条退出路径:先取消进行中的轮次,等待 agent 空闲,然后恢复并关闭终端。空闲态页脚根据 tokenMeter 得出上下文占用率,并显示所选模型和显式选定的推理强度;agent 运行期间,该摘要会替换为带已用时长的活动指示和 Escape 中断提示。/status 在这两种状态下均可用,并会追加一份仅在终端显示的详细快照,其中包括会话标识与时间戳、所选模型、推理强度(或默认状态)及推理显隐状态、从事件日志归并得出的生命周期计数、与页脚一致的去重用量分项和 KV 缓存命中率,以及 tokenMeter 给出的上下文用量和所选模型公布的容量。插件注册共享的 userInteraction 提供方,在左下角宽幅键盘操作面板中呈现排队的问题,面板显示批次进度、带编号的选项和对齐的描述;面板的操作提示只列出在当前选项数量下有意义的操作,仅有一个选项时不显示导航项;agent 行为和答案日志仍由既有服务负责。
/model 命令将建议性的 ctx.llm 目录呈现为键盘选择器,并且只更改当前 TUI 会话的目标;带参数的形式仍可直接选择目标。选择器在列表上方设有一个过滤框:输入内容会按对每行 provider/model 标签、模型名称和描述的大小写不敏感子串匹配来缩小行集,并在原先高亮行仍通过过滤时保持其选中状态;Escape 会先清空非空的过滤内容,再次按 Escape 才取消选择器。每个模型行都持有适配器公布的推理强度顺序和默认值:按 Shift+Tab 可循环切换该行的推理强度;如果适配器没有公布默认值,循环中还会包含提供方默认行为;没有可选元数据的模型则保持不变。agent 作用域内的 prompt 组装和请求两条 waterfall(瀑布式事件)会为每个步骤快照一次同一个提供方/模型/推理强度目标,因此即使命令在组装期间到达,{{provider}} / {{model}} 插值与请求路由也不会分裂。系统通过日志中最新的请求头恢复已经使用过的目标;未被请求使用的选择只保留在当前进程中。
终端所有权
在模型输出、会话数据、工具呈现、问题、配置或诊断信息进入 pi-tui 或终端标题前,displayText() 会把换行之外的 C0 和 C1 控制字符显示为十六进制转义文本。只有 TUI 和 pi-tui 可以生成 ANSI 控制序列。
内置配色仅使用标准 16 色 ANSI 前景色和 SGR 属性,正文文字和背景沿用终端默认值,选中项使用反显。因此,宿主终端可以直接按浅色或深色主题重映射界面,无需 TUI 专用主题设置;color: false 会移除样式。
验证
已实现的 TUI 终端状态快照 Agent Note 规定包验证契约:直接行为测试和语义终端快照。交付该入口的部署负责其组装后 transcript 和进程/PTY 验收。包 README 负责记录配置、命令、模型可见效果和当前限制。
曾考虑的替代方案
- 把 readline 与全屏模式都保留在
@deepseek-ai/dsh-stdio中:不予采纳,因为逐行输出和差分 TTY 渲染具有不同的依赖、输入规则、日志所有权和资源清理义务。拆分为独立包可以让管道安全契约保持精简、明确。 - 当任一进程流不是 TTY 时,让 TUI 插件静默降级:不予采纳,因为回退会掩盖部署错误并改变交互语义。宿主可以选择其他入口;明确挂载的 TUI 会快速失败。
- 把 TUI 接线与测试保留在 readline
repl-agent叶节点下:当时不予采纳,因为一个叶节点会代表两个不同入口。后续移除产品入口时删除了该应用接线,但保留了包边界。 - 在
/model运行时修改agent.options:不予采纳,因为创建选项无法在异步 prompt 组装与请求路由之间提供原子边界。agent 作用域内的 waterfall 会在保持创建输入不可变的同时,为每个 step 快照一次选中的字段组合。
后果
- 挂载 TUI 的部署会获得带状态的 Markdown、卡片、计划和提问界面,无需再对齐第二套终端协议。
- TUI 会引入 pi-tui 依赖并严格要求 TTY;非 TTY 部署使用 Headless app 或结构化协议。
- 会话投影使恢复与持久会话保持一致,但只有一个已配置会话拥有 transcript 和编辑器。
- 工具包通过既有呈现方法扩展终端卡片,无需在 TUI 中增加工具专用分支。
- 模型和推理强度选择使用适配器公布的元数据,但不会把目录成员关系变成请求校验;未使用的选择不属于持久化状态。