From f13003df757125176692740cac16898251457e84 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 30 Jul 2026 23:45:50 +0800 Subject: [PATCH] feat(tui): two-entry /details selector with Tab cycling DetailsDialog now shows one entry per dimension (Tool cards, Reasoning) seeded with the current values; Tab cycles the highlighted entry's pending value (rendered as current -> pending), Enter applies every changed dimension in one confirm, Esc/Ctrl+C cancels. --- .../2026-07-30-tui-details-command.i18n.yaml | 4 +- .../feature/2026-07-30-tui-details-command.md | 6 +- .../2026-07-30-tui-details-command.zh.md | 6 +- packages/ui/tui/README.i18n.yaml | 4 +- packages/ui/tui/README.md | 6 +- packages/ui/tui/README.zh.md | 6 +- packages/ui/tui/src/components/dialogs.ts | 79 +++++++++++++------ packages/ui/tui/src/index.ts | 5 +- .../snapshots/details-selector.expected.txt | 58 ++++++-------- packages/ui/tui/tests/tui.snapshot.ts | 6 +- packages/ui/tui/tests/tui.spec.ts | 36 ++++++--- 11 files changed, 131 insertions(+), 85 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-30-tui-details-command.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-tui-details-command.i18n.yaml index 33278b66d2..8e6c202a5e 100644 --- a/.agents/notes/implemented/feature/2026-07-30-tui-details-command.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-tui-details-command.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-tui-details-command.md -2026-07-30-tui-details-command.md: c14461aa08790033026eead38f53d4b8e686bbbb -2026-07-30-tui-details-command.zh.md: e9c9787a4911f29f49e7709d1ba673dc1a25e8a1 +2026-07-30-tui-details-command.md: 5de80ceb3ad78a949268c31e9c1d1a50c956b90f +2026-07-30-tui-details-command.zh.md: f74d0658cb776752bf3da1682291de882638310a diff --git a/.agents/notes/implemented/feature/2026-07-30-tui-details-command.md b/.agents/notes/implemented/feature/2026-07-30-tui-details-command.md index c14461aa08..5de80ceb3a 100644 --- a/.agents/notes/implemented/feature/2026-07-30-tui-details-command.md +++ b/.agents/notes/implemented/feature/2026-07-30-tui-details-command.md @@ -10,7 +10,7 @@ The TUI's transcript detail state — tool-card visibility (`collapsed`/`expande ## Decision -`dsh-tui` registers `/details` beside its other agent-scoped commands. Bare `/details` opens `DetailsDialog`, a centered keyboard selector over the five detail states — the three tool-card phases and reasoning shown/hidden — that preselects the current phase, marks both current values, applies the highlighted state on Enter, and cancels on Esc or Ctrl+C; its width is the `detailsDialogWidth` config key and a second `/details` replaces an open selector, mirroring the `/model` overlay. Arguments name target states directly: `collapsed|expanded|hidden` jumps tool cards to that phase, `reasoning on|off` sets reasoning display, bare `reasoning` toggles it, and directives combine in one invocation. An unknown token returns a command error carrying the usage line. Every entry mutates the same closure state as the shortcuts, refactored so the cycle and toggle are thin wrappers over `setToolsVisibility`/`setReasoning`; the shortcuts and their notices are unchanged. +`dsh-tui` registers `/details` beside its other agent-scoped commands. Bare `/details` opens `DetailsDialog`, a centered keyboard selector with one entry per dimension — `Tool cards` and `Reasoning` — seeded with the current values: Tab cycles the highlighted entry's pending value (rendered as `current → pending`), Enter applies every changed dimension in one confirm and closes, and Esc or Ctrl+C cancels; its width is the `detailsDialogWidth` config key and a second `/details` replaces an open selector, mirroring the `/model` overlay. Arguments name target states directly: `collapsed|expanded|hidden` jumps tool cards to that phase, `reasoning on|off` sets reasoning display, bare `reasoning` toggles it, and directives combine in one invocation. An unknown token returns a command error carrying the usage line. Every entry mutates the same closure state as the shortcuts, refactored so the cycle and toggle are thin wrappers over `setToolsVisibility`/`setReasoning`; the shortcuts and their notices are unchanged. A combined invocation applies reasoning before visibility because `setReasoning` rebuilds the transcript from session events, which drops non-durable notice components; applying it last would erase the just-appended visibility notice. @@ -30,5 +30,5 @@ The reasoning rebuild exposed a replay defect that this change fixes in `renderE - A user can jump to any detail mode, set both dimensions at once, and see the current state in the selector — including on terminals that intercept Ctrl+O/Ctrl+R. - The parser accepts order-free tokens, so `/details reasoning expanded` toggles reasoning and expands cards; last directive wins per dimension. This leniency is deliberate and documented in the README. -- The selector applies one dimension per confirm; a combined change still needs the argument form. Enter on the already-current row re-applies it idempotently and repeats its notice. -- Transcript rebuilds no longer lose assistant messages when a step carries more than one `assistant/message` event; the `details-command` snapshot pins the argument surface and the fixed replay, and `details-selector` pins the open selector with its current-state markers. +- The selector applies only changed dimensions on confirm, so Enter without a Tab closes silently; a two-dimension change is one open-Tab-Enter interaction. +- Transcript rebuilds no longer lose assistant messages when a step carries more than one `assistant/message` event; the `details-command` snapshot pins the argument surface and the fixed replay, and `details-selector` pins the open selector with a Tab-cycled `hidden → collapsed` pending value. diff --git a/.agents/notes/implemented/feature/2026-07-30-tui-details-command.zh.md b/.agents/notes/implemented/feature/2026-07-30-tui-details-command.zh.md index e9c9787a49..f74d0658cb 100644 --- a/.agents/notes/implemented/feature/2026-07-30-tui-details-command.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-tui-details-command.zh.md @@ -10,7 +10,7 @@ TUI 的 transcript(文本记录)细节状态——工具卡片可见性(`c ## Decision -`dsh-tui` 在其他 agent 作用域命令旁注册 `/details`。裸 `/details` 打开 `DetailsDialog`:一个居中的键盘选择器,列出五个细节状态——三个工具卡片阶段与 reasoning 显示/隐藏——预选当前阶段并标记两个当前值,Enter 应用高亮状态并关闭,Esc 或 Ctrl+C 取消;其宽度由配置键 `detailsDialogWidth` 决定,选择器打开时再次执行 `/details` 会替换它,与 `/model` 浮层一致。参数直接命名目标状态:`collapsed|expanded|hidden` 让工具卡片跳到该阶段,`reasoning on|off` 设置 reasoning 显示,裸 `reasoning` 切换它,且指令可在一次调用中组合。未知 token 返回携带用法行的命令错误。每个入口改动的都是与快捷键相同的闭包状态,重构后循环与切换成为 `setToolsVisibility`/`setReasoning` 之上的薄封装;快捷键及其通知保持不变。 +`dsh-tui` 在其他 agent 作用域命令旁注册 `/details`。裸 `/details` 打开 `DetailsDialog`:一个居中的键盘选择器,每个维度一个条目——`Tool cards` 与 `Reasoning`——以当前值为初始:Tab 循环高亮条目的待定值(渲染为 `current → pending`),Enter 一次确认应用所有已改变的维度并关闭,Esc 或 Ctrl+C 取消;其宽度由配置键 `detailsDialogWidth` 决定,选择器打开时再次执行 `/details` 会替换它,与 `/model` 浮层一致。参数直接命名目标状态:`collapsed|expanded|hidden` 让工具卡片跳到该阶段,`reasoning on|off` 设置 reasoning 显示,裸 `reasoning` 切换它,且指令可在一次调用中组合。未知 token 返回携带用法行的命令错误。每个入口改动的都是与快捷键相同的闭包状态,重构后循环与切换成为 `setToolsVisibility`/`setReasoning` 之上的薄封装;快捷键及其通知保持不变。 组合调用先应用 reasoning 再应用可见性,因为 `setReasoning` 会从会话事件重建 transcript,而重建会丢弃非持久的通知组件;若最后才应用它,会抹掉刚追加的可见性通知。 @@ -30,5 +30,5 @@ reasoning 重建暴露了一个重放缺陷,本变更在 `renderEvent` 中修 - 用户可以跳到任意细节模式、一次设置两个维度,并在选择器中看到当前状态——包括在拦截 Ctrl+O/Ctrl+R 的终端上。 - 解析器接受无序 token,因此 `/details reasoning expanded` 会切换 reasoning 并展开卡片;每个维度以最后一个指令为准。这一宽松是刻意的,并记录在 README 中。 -- 选择器每次确认只应用一个维度;组合变更仍需参数形式。在已是当前值的行上按 Enter 会幂等地重新应用并重复其通知。 -- 当一个步骤携带多条 `assistant/message` 事件时,transcript 重建不再丢失 assistant 消息;`details-command` 快照固定参数表面与修复后的重放,`details-selector` 固定带当前值标记的打开选择器。 +- 选择器确认时只应用已改变的维度,因此未按 Tab 直接 Enter 会静默关闭;两个维度的变更是一次打开-Tab-Enter 交互。 +- 当一个步骤携带多条 `assistant/message` 事件时,transcript 重建不再丢失 assistant 消息;`details-command` 快照固定参数表面与修复后的重放,`details-selector` 固定经 Tab 循环出 `hidden → collapsed` 待定值的打开选择器。 diff --git a/packages/ui/tui/README.i18n.yaml b/packages/ui/tui/README.i18n.yaml index 6587bc7624..af9ba4a67a 100644 --- a/packages/ui/tui/README.i18n.yaml +++ b/packages/ui/tui/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/ui/tui/README.md -README.md: a65d72b3b80b992fabcb33d4b4345942b58e9147 -README.zh.md: fd056da59335af5cb9fc63fd2fb681266fadeb62 +README.md: 7230e75535ce5710e12040e026bc77a1782ed6dd +README.zh.md: 3079b797b670de64cfdc1faa9d002ee04a6e28dd diff --git a/packages/ui/tui/README.md b/packages/ui/tui/README.md index 23ca8343ec..8ef5f20ca0 100644 --- a/packages/ui/tui/README.md +++ b/packages/ui/tui/README.md @@ -12,7 +12,7 @@ This package owns interactive terminal presentation and input only. It injects ` After terminal startup succeeds, the package provides the terminal-local `ctx.tui` extension service. A plugin that injects it can call `openOverlay()` with a component factory and constrained layout options; the host exposes the viewport, semantic theme, display-text escaping, redraw, close, and a lifetime signal, but not the pi-tui tree, terminal, focus controller, or overlay handle. Plugin overlays, the model selector, and user questions share one FIFO modal queue. Each request is an effect of the calling plugin fiber, so unload removes queued work or closes visible work before cleanup settles; terminal shutdown unloads dependents before stopping pi-tui. Overlay state is not logged or replayed. Component code is trusted and may render ANSI styling, but must pass untrusted text through `host.display()`. The [interactive-extension Agent Note](../../../.agents/notes/implemented/architecture/2026-07-22-tui-interactive-extension-service.md) owns the boundary and rejected alternatives. -The TUI rebuilds resumed history from the append-origin session events, renders Markdown responses and reasoning, applies each tool's `presentCall` / `presentResult` intent to terminal, diff, or generic cards, keeps the standing `todo/write` plan above the editor (cleared on the next `turn/start`), and presents `ctx.userInteraction` questions in a wide bottom-left keyboard panel with progress, numbered options, and aligned descriptions. The latest logged session title becomes the header subtitle, with `welcome` before a title exists, and the terminal window title becomes ``. A durable `llm/retry` event retracts the failed step's live chunks and renders the scheduled retry count, delay, and failure in the transcript; success, exhaustion, and cancellation then settle through ordinary session events. The footer totals each logged model step's usage once, including failed attempts, while treating committed-message usage as a fallback for logs without a usage chunk. Its idle view compares token-meter pressure with `ctx.llm.resolveModelInfo()` context for the current route, displays `context unknown` when the adapter has no capacity metadata, and also shows tool-card mode plus the current model and any explicitly selected reasoning effort; while the agent runs, an elapsed working indicator and `esc interrupt` replace that summary. A surface replacement never rewrites the rendered transcript: the conversation it shadows stays readable, and a landed compaction checkpoint adds one dim `… earlier context was compacted …` marker at its log position, so the terminal reports where the model stopped seeing that history instead of erasing it. Model-only replacement copies — a pruned tool result, a regenerated assistant message — render nothing. +The TUI rebuilds resumed history from the active session surface, renders Markdown responses and reasoning, applies each tool's `presentCall` / `presentResult` intent to terminal, diff, or generic cards, keeps the standing `todo/write` plan above the editor (cleared on the next `turn/start`), and presents `ctx.userInteraction` questions in a wide bottom-left keyboard panel with progress, numbered options, and aligned descriptions. The latest logged session title becomes the header subtitle, with `welcome` before a title exists, and the terminal window title becomes ``. A durable `llm/retry` event retracts the failed step's live chunks and renders the scheduled retry count, delay, and failure in the transcript; success, exhaustion, and cancellation then settle through ordinary session events. The footer totals each logged model step's usage once, including failed attempts, while treating committed-message usage as a fallback for logs without a usage chunk. Its idle view compares token-meter pressure with `ctx.llm.resolveModelInfo()` context for the current route, displays `context unknown` when the adapter has no capacity metadata, and also shows tool-card mode plus the current model and any explicitly selected reasoning effort; while the agent runs, an elapsed working indicator and `esc interrupt` replace that summary. Surface replacement events rebuild the transcript so compacted history does not reappear. An embedding may provide `TuiRuntime.formatCwd` when its logical workspace label differs from the session's host directory. The override changes only the footer label; tools continue to use the session `cwd`. @@ -22,7 +22,7 @@ Typing `@` at a token boundary searches files and directories under the session When optional `ctx.sessionReferences` is mounted, the same `@` menu also offers metadata-only session candidates, inserts `@[label](dsh-session:)`, and prepares the selected snapshots before dispatch. Session references remain structured because the model has no filesystem-like tool for retrieving session snapshots later. Preparation disables duplicate submission and restores the editor input on failure. The TUI chooses `agent.steer()` or `agent.followup()` from the status after that asynchronous preparation, so idle follow-ups still dispatch `agent/prompt-submit` while in-turn steering joins at a checkpoint without that hook. -While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.followup()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly, unknown commands produce a warning, and neither path automatically reaches the model. A command producer may explicitly schedule agent work; [`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-surfaces) uses that contract for `/plan [message]`. The TUI registers `/help`, `/model`, `/clear`, `/details`, `/palette`, `/reload`, `/resume`, `/status`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically, as do `/skill:` completions. A status line above the editor reports the turn phase the TUI derives from session events — waiting for the first token, thinking, responding, or executing tools — with the elapsed time in that phase and the running step total, refreshed each second, and ends with the `Enter sends steering, Esc cancels` hint; while steering messages wait to reach the model it inserts a `N queued ·` badge before the hint that clears as each drains. Ctrl+C or Escape cancels a running turn. Tool and injected-context cards collapse long bodies into a configurable head/tail preview; Ctrl+O cycles tool cards through collapsed preview, full output, and hidden — the hidden phase drops tool cards from the transcript entirely while context cards stay at their preview, since injected instructions are not tool traffic. The hidden phase also folds each turn's assistant steps into one message: the first step with visible text or reasoning keeps the turn's single `Assistant` header, later steps render as headerless continuations, and a step without a visible body renders nothing; leaving the hidden phase restores the per-step headers. An injected-context card renders its message as prose with the producer's outer reminder frame stripped, so neither the fold nor the frame stripping depends on the payload's syntax. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle. `/details` names the same state those two shortcuts cycle: bare it opens a centered keyboard toggle with one entry per dimension — `Tool cards` and `Reasoning` — showing the live values, where Tab cycles the highlighted entry and applies the change immediately (the transcript behind the dialog is the preview), and Enter, Esc, or Ctrl+C closes; `/details collapsed|expanded|hidden` jumps tool cards to that phase directly, and `/details reasoning [on|off]` sets — or bare `reasoning` toggles — reasoning-block display; arguments combine in one invocation, an unknown argument fails with the usage line, and a combined invocation applies reasoning first so its transcript rebuild never drops the card notice. +While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.followup()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly, unknown commands produce a warning, and neither path automatically reaches the model. A command producer may explicitly schedule agent work; [`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-surfaces) uses that contract for `/plan [message]`. The TUI registers `/help`, `/model`, `/clear`, `/details`, `/palette`, `/reload`, `/resume`, `/status`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically, as do `/skill:` completions. A status line above the editor reports the turn phase the TUI derives from session events — waiting for the first token, thinking, responding, or executing tools — with the elapsed time in that phase and the running step total, refreshed each second, and ends with the `Enter sends steering, Esc cancels` hint; while steering messages wait to reach the model it inserts a `N queued ·` badge before the hint that clears as each drains. Ctrl+C or Escape cancels a running turn. Tool and injected-context cards collapse long bodies into a configurable head/tail preview; Ctrl+O cycles tool cards through collapsed preview, full output, and hidden — the hidden phase drops tool cards from the transcript entirely while context cards stay at their preview, since injected instructions are not tool traffic. The hidden phase also folds each turn's assistant steps into one message: the first step with visible text or reasoning keeps the turn's single `Assistant` header, later steps render as headerless continuations, and a step without a visible body renders nothing; leaving the hidden phase restores the per-step headers. An injected-context card renders its message as prose with the producer's outer reminder frame stripped, so neither the fold nor the frame stripping depends on the payload's syntax. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle. `/details` names the same state those two shortcuts cycle: bare it opens a centered keyboard selector with one entry per dimension — `Tool cards` and `Reasoning` — seeded with the current values, where Tab cycles the highlighted entry's pending value (rendered as `current → pending`), Enter applies every changed dimension and closes, and Esc or Ctrl+C cancels without changing anything; `/details collapsed|expanded|hidden` jumps tool cards to that phase directly, and `/details reasoning [on|off]` sets — or bare `reasoning` toggles — reasoning-block display; arguments combine in one invocation, an unknown argument fails with the usage line, and a combined invocation applies reasoning first so its transcript rebuild never drops the card notice. `/model` opens the advisory `ctx.llm` catalog as a keyboard selector: a filter box above the list narrows rows by a case-insensitive substring over each row's `provider/model` label, model name, and description, keeping the highlighted row selected when it survives the filter; Up/Down moves, Shift+Tab cycles the focused model's adapter-advertised reasoning efforts in display order, Enter selects the model and effort, and Escape clears a non-empty filter before a second Escape closes it. When an adapter does not advertise a default effort, the cycle also includes `Default`, which clears an explicit selection and preserves the provider default; models without selectable effort metadata ignore Shift+Tab. The selector renders the exact advertised effort list—including `off` when present—and does not synthesize, clamp, or transfer an effort between models. `/model ` still selects an unambiguous model id directly, while `/model /` selects an exact target and uses its adapter default when one exists. The configured target or latest logged request header initializes the selector, and an unlisted current model remains visible because catalogs are advisory. Selection is local to this TUI session. Prompt assembly snapshots the target for one step, replaces `{{provider}}` and `{{model}}`, and applies the same provider/model/reasoning-effort target through `agent/request`; a switch during assembly therefore starts with a later step. The request header durably records targets that reach the model, while an unused selection remains process-local. @@ -32,6 +32,7 @@ The footer sums the session's reported usage as `↑ `/status` adds a point-in-time diagnostics card to the transcript and remains available while the agent runs. It reports the session id, title, working directory, selected provider/model, selected reasoning effort or default behavior, reasoning-block visibility, agent state, event/turn/step/tool-call counts, exact input/output/cache token buckets, KV-cache hit rate, token-meter context use and capacity, creation time, and latest event time. Missing titles, models, cache input, or context capacity are labeled instead of inferred. The card is terminal-only and does not duplicate the compact footer. + `/resume` opens a full-viewport keyboard selector instead of a centered dialog. Two scopes cover the same candidate set: the current workspace, which it opens on, and all workspaces, which Tab toggles to. The scope line under the search field names the active scope and the count the other holds, and each row in the all-workspaces scope also reports its own workspace. Toggling clears the search and selection so the highlighted row always belongs to the visible list. Its focused search field starts immediately after the search glyph and emits pi-tui's cursor marker, so terminal IME composition remains anchored inside the field. Candidates are sorted by last logged activity and searchable by log-backed title or session id, and by workspace label in the all-workspaces scope; each row reports current/live/persisted state, last turn outcome, recent provider/model, and durable goal phase when present. Up/Down and Page Up/Page Down navigate, Enter resumes, Escape clears a non-empty search before a second Escape cancels, and Ctrl+C cancels directly. The current session, a session already live in this runtime, an unreadable log, a session with no recorded workspace to run in, or a session whose logged provider has no current adapter remains visible but disabled; a workspace other than the current one is a scope rather than a disabled reason, because resume enters that directory. @@ -57,6 +58,7 @@ A launcher can seed a fresh session's first turn by providing `INITIAL_SKILL_KEY | `questionDialogMaxHeight` | `20` | Question-panel maximum rows | | `modelDialogWidth` | `76` | Model-selector width in columns | | `modelDialogMaxHeight` | `20` | Model-selector maximum rows | +| `detailsDialogWidth` | `72` | Transcript-details selector width in columns | | `fileSearchMaxResults` | `20` | Maximum file and directory candidates shown for one `@` query | | `fileSearchMaxEntries` | `10000` | Maximum paths retained in the bounded workspace index used by bare fuzzy queries | | `fileSearchExcludedDirectories` | `['.git', 'node_modules']` | Directory basenames omitted from traversal and direct completion | diff --git a/packages/ui/tui/README.zh.md b/packages/ui/tui/README.zh.md index 40aa849838..48cb25fb94 100644 --- a/packages/ui/tui/README.zh.md +++ b/packages/ui/tui/README.zh.md @@ -12,7 +12,7 @@ DeepSeek Harness agent(智能体)的交互式终端入口,基于 [`@earend 终端成功启动后,本包会提供终端本地的 `ctx.tui` 扩展服务。注入该服务的插件可以使用组件工厂和受限布局选项调用 `openOverlay()`;宿主会公开 viewport、语义化主题、显示文本转义、重绘、关闭和生命周期信号,但不公开 pi-tui 树、终端、焦点控制器或 overlay 句柄。插件 overlay、模型选择器和用户问题共用一个 FIFO 模态队列。每个请求都是调用方插件 fiber 的 effect,因此卸载会移除排队工作,或在清理结算前关闭可见工作;终端关闭会先卸载依赖项,再停止 pi-tui。Overlay 状态不会记录或回放。组件代码受信任,可以渲染 ANSI 样式,但必须通过 `host.display()` 处理不受信任文本。[交互式扩展 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-22-tui-interactive-extension-service.md)持有该边界和未采用的替代方案。 -TUI 从追加来源的会话事件重建已恢复历史,渲染 Markdown 响应与 reasoning,将每个工具的 `presentCall` / `presentResult` 意图应用到终端、diff 或通用卡片,把站立的 `todo/write` 计划保留在编辑器上方(下一个 `turn/start` 时清空),并在左下方宽键盘面板中展示 `ctx.userInteraction` 问题,包含进度、编号选项和对齐说明。最新记录的会话标题成为 header 副标题;标题不存在时使用 `welcome`,终端窗口标题则变为 ``。持久 `llm/retry` 事件会撤回失败步骤的实时 chunk,并在 transcript(文本记录)中渲染计划重试次数、延迟和失败;成功、耗尽与取消随后通过普通会话事件结算。Footer 会对每个已记录模型步骤的用量只计一次,包括失败尝试;对于没有用量 chunk 的日志,以已提交消息的用量回退。其空闲视图会将 token-meter 压力与 `ctx.llm.resolveModelInfo()` 为当前路由返回的上下文容量进行比较;适配器没有容量元数据时显示 `context unknown`,并显示工具卡片模式、当前模型,以及任何显式选择的推理强度。Agent 运行时,这些摘要会替换为已经过工作时间指示器和 `esc interrupt`。表层替换从不重写已渲染的 transcript:被它遮蔽的对话仍可阅读,而已落地的压缩(compaction)检查点会在其日志位置添加一行暗色 `… earlier context was compacted …` 标记,因此终端报告的是模型从何处起不再看到那段历史,而不是把它抹掉。仅供模型使用的替换副本——被裁剪的工具结果、重新生成的 assistant 消息——不渲染任何内容。 +TUI 从活跃会话表层重建已恢复历史,渲染 Markdown 响应与 reasoning,将每个工具的 `presentCall` / `presentResult` 意图应用到终端、diff 或通用卡片,把站立的 `todo/write` 计划保留在编辑器上方(下一个 `turn/start` 时清空),并在左下方宽键盘面板中展示 `ctx.userInteraction` 问题,包含进度、编号选项和对齐说明。最新记录的会话标题成为 header 副标题;标题不存在时使用 `welcome`,终端窗口标题则变为 ``。持久 `llm/retry` 事件会撤回失败步骤的实时 chunk,并在 transcript(文本记录)中渲染计划重试次数、延迟和失败;成功、耗尽与取消随后通过普通会话事件结算。Footer 会对每个已记录模型步骤的用量只计一次,包括失败尝试;对于没有用量 chunk 的日志,以已提交消息的用量回退。其空闲视图会将 token-meter 压力与 `ctx.llm.resolveModelInfo()` 为当前路由返回的上下文容量进行比较;适配器没有容量元数据时显示 `context unknown`,并显示工具卡片模式、当前模型,以及任何显式选择的推理强度。Agent 运行时,这些摘要会替换为已经过工作时间指示器和 `esc interrupt`。表层替换事件会重建 transcript,使经过压缩(compaction)的历史不会再次出现。 如果逻辑工作区标签与会话宿主目录不同,嵌入方可以提供 `TuiRuntime.formatCwd`。该覆盖只改变 footer 标签;工具仍使用会话 `cwd`。 @@ -22,7 +22,7 @@ TUI 从追加来源的会话事件重建已恢复历史,渲染 Markdown 响应 挂载可选的 `ctx.sessionReferences` 后,同一个 `@` 菜单还会提供仅含元数据的会话候选项,插入 `@[label](dsh-session:)`,并在分派前准备所选快照。会话引用保持结构化,因为模型没有类似文件系统的工具可在稍后检索会话快照。准备期间会禁止重复提交,并在失败时恢复编辑器输入。TUI 会在异步准备后根据状态选择 `agent.steer()` 或 `agent.followup()`,因此空闲 followup 仍会分派 `agent/prompt-submit`,而轮次中的 steering 会在检查点加入且不触发该 hook。 -Agent 运行时,普通编辑器提交会调用 `agent.steer()`;其他时候调用 `agent.followup()`。提交行以斜杠开头时会改为进入 `ctx.commands`:已知命令直接执行,未知命令产生警告,两条路径都不会自动到达模型。命令生产方可以显式调度 agent 工作;[`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-surfaces) 使用该契约实现 `/plan [message]`。TUI 将 `/help`、`/model`、`/clear`、`/details`、`/palette`、`/reload`、`/resume`、`/status` 和 `/exit` 注册为 agent 作用域定义;其他所有有效命令都会动态加入自动补全与 `/help`,`/skill:` 补全也相同。编辑器上方的状态行会报告 TUI 从会话事件派生的轮次阶段,包括等待首个 token、思考、响应或执行工具;它显示该阶段已经过时间和运行中的步骤总数,每秒刷新,并以 `Enter sends steering, Esc cancels` 提示结尾。Steering 消息等待到达模型期间,会在提示前插入 `N queued ·` 徽标,每条消息排空后随即清除。Ctrl+C 或 Escape 会取消运行中的轮次。工具卡片与注入上下文卡片都把长主体折叠为可配置的头尾预览;Ctrl+O 让工具卡片在折叠预览、完整输出、隐藏三种状态间循环——隐藏阶段把工具卡片从 transcript 中完全去掉,而上下文卡片保持预览,因为注入的指令不属于工具流量。隐藏阶段还会把每个轮次的 assistant 步骤折叠为一条消息:第一个有可见文本或 reasoning 的步骤保留该轮次唯一的 `Assistant` 标题,之后的步骤渲染为无标题的续段,没有可见正文的步骤则不渲染任何内容;离开隐藏阶段会恢复每步各自的标题。注入上下文卡片把消息渲染为文本,并去掉生产方的外层提醒外框,因此折叠与去外框都不依赖载荷的语法。Ctrl+R 切换 reasoning,Ctrl+L 重绘,Ctrl+D 在空闲时退出。`/details` 命名的正是这两个快捷键循环的同一份状态:不带参数时打开一个居中的键盘开关,每个维度一个条目——`Tool cards` 与 `Reasoning`——显示实时值,Tab 循环高亮条目并立即应用变更(对话框背后的 transcript 即是预览),Enter、Esc 或 Ctrl+C 关闭;`/details collapsed|expanded|hidden` 让工具卡片直接跳到该阶段,`/details reasoning [on|off]` 设置——或裸 `reasoning` 切换——reasoning 块显示;参数可在一次调用中组合,未知参数会以用法行报错,组合调用先应用 reasoning,使其 transcript 重建不会丢掉卡片通知。 +Agent 运行时,普通编辑器提交会调用 `agent.steer()`;其他时候调用 `agent.followup()`。提交行以斜杠开头时会改为进入 `ctx.commands`:已知命令直接执行,未知命令产生警告,两条路径都不会自动到达模型。命令生产方可以显式调度 agent 工作;[`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-surfaces) 使用该契约实现 `/plan [message]`。TUI 将 `/help`、`/model`、`/clear`、`/details`、`/palette`、`/reload`、`/resume`、`/status` 和 `/exit` 注册为 agent 作用域定义;其他所有有效命令都会动态加入自动补全与 `/help`,`/skill:` 补全也相同。编辑器上方的状态行会报告 TUI 从会话事件派生的轮次阶段,包括等待首个 token、思考、响应或执行工具;它显示该阶段已经过时间和运行中的步骤总数,每秒刷新,并以 `Enter sends steering, Esc cancels` 提示结尾。Steering 消息等待到达模型期间,会在提示前插入 `N queued ·` 徽标,每条消息排空后随即清除。Ctrl+C 或 Escape 会取消运行中的轮次。工具卡片与注入上下文卡片都把长主体折叠为可配置的头尾预览;Ctrl+O 让工具卡片在折叠预览、完整输出、隐藏三种状态间循环——隐藏阶段把工具卡片从 transcript 中完全去掉,而上下文卡片保持预览,因为注入的指令不属于工具流量。隐藏阶段还会把每个轮次的 assistant 步骤折叠为一条消息:第一个有可见文本或 reasoning 的步骤保留该轮次唯一的 `Assistant` 标题,之后的步骤渲染为无标题的续段,没有可见正文的步骤则不渲染任何内容;离开隐藏阶段会恢复每步各自的标题。注入上下文卡片把消息渲染为文本,并去掉生产方的外层提醒外框,因此折叠与去外框都不依赖载荷的语法。Ctrl+R 切换 reasoning,Ctrl+L 重绘,Ctrl+D 在空闲时退出。`/details` 命名的正是这两个快捷键循环的同一份状态:不带参数时打开一个居中的键盘选择器,每个维度一个条目——`Tool cards` 与 `Reasoning`——以当前值为初始,Tab 循环高亮条目的待定值(渲染为 `current → pending`),Enter 应用所有已改变的维度并关闭,Esc 或 Ctrl+C 取消且不改变任何东西;`/details collapsed|expanded|hidden` 让工具卡片直接跳到该阶段,`/details reasoning [on|off]` 设置——或裸 `reasoning` 切换——reasoning 块显示;参数可在一次调用中组合,未知参数会以用法行报错,组合调用先应用 reasoning,使其 transcript 重建不会丢掉卡片通知。 `/model` 将建议性的 `ctx.llm` catalog 打开为键盘选择器:列表上方设有一个过滤框,按对每行 `provider/model` 标签、模型名称和描述的大小写不敏感子串匹配来缩小行集,并在高亮行仍通过过滤时保持其选中状态;Up/Down 移动,Shift+Tab 按显示顺序循环切换适配器为焦点模型公布的推理强度,Enter 选择模型和推理强度,Escape 会先清除非空过滤内容,再次按下才关闭选择器。适配器未公布默认推理强度时,循环还会包含 `Default`,该项会清除显式选择并保留提供方默认行为;没有可选推理强度元数据的模型会忽略 Shift+Tab。选择器会原样呈现公布的推理强度列表(包括存在时的 `off`),不会合成、自动调整或在模型之间转移推理强度。`/model ` 仍可直接选择无歧义的模型 id,`/model /` 则选择精确目标,并在存在时使用其适配器默认值。已配置目标或最新记录的请求 header 会初始化选择器;由于 catalog 仅提供建议,未列出的当前模型仍会显示。选择仅对本 TUI 会话有效。提示词组装会为一个步骤建立目标快照,替换 `{{provider}}` 和 `{{model}}`,并通过 `agent/request` 应用同一个提供方/模型/推理强度目标;因此组装期间的切换会从后续步骤开始生效。请求 header 会持久记录真正到达模型的目标,未使用的选择则只存在于进程本地。 @@ -32,6 +32,7 @@ Footer 将会话报告的用量汇总为 `↑`;任 `/status` 会向 transcript 添加一张时间点诊断卡片,并在 agent 运行时保持可用。它报告会话 id、标题、工作目录、所选提供方/模型、所选推理强度或默认行为、reasoning 块可见性、agent 状态、事件/轮次/步骤/工具调用计数、精确输入/输出/缓存 token bucket、KV-cache 命中率、token-meter 上下文用量与容量、创建时间和最新事件时间。缺失标题、模型、缓存输入或上下文容量时会明确标记,而非推断。该卡片只存在于终端,不会重复紧凑 footer。 + `/resume` 会打开全 viewport 键盘选择器,而非居中对话框。两个作用域覆盖同一候选项集合:打开时所处的当前工作区,以及按 Tab 切换到的所有工作区。搜索字段下方的作用域行会给出当前作用域的名称以及另一个作用域包含的数量,且在所有工作区作用域中每行还会报告自身所属的工作区。切换会清除搜索与选择,使高亮行始终属于可见列表。 获得焦点的搜索字段紧跟搜索 glyph 开始,并发出 pi-tui 的 cursor marker,使终端 IME 组合保持锚定在字段内。候选项按最近记录的活动排序,可按日志支持的标题或会话 id 搜索,在所有工作区作用域中还可按工作区标签搜索;每行报告 current/live/persisted 状态、上一轮次结果、近期提供方/模型,以及存在时的持久目标阶段。Up/Down 与 Page Up/Page Down 导航,Enter 恢复,Escape 会先清除非空搜索,再次按下才取消,Ctrl+C 则直接取消。当前会话、已在本运行时中活跃的会话、不可读日志、没有可运行的已记录工作区的会话,或日志所记提供方没有当前适配器的会话仍会显示,但不可选择;不同于当前工作区的工作区属于作用域而非禁用原因,因为恢复会进入该目录。 @@ -57,6 +58,7 @@ Footer 将会话报告的用量汇总为 `↑`;任 | `questionDialogMaxHeight` | `20` | 问题面板最大行数 | | `modelDialogWidth` | `76` | 模型选择器宽度(列数) | | `modelDialogMaxHeight` | `20` | 模型选择器最大行数 | +| `detailsDialogWidth` | `72` | transcript 细节选择器宽度(列数) | | `fileSearchMaxResults` | `20` | 一次 `@` 查询显示的最大文件和目录候选数 | | `fileSearchMaxEntries` | `10000` | 无路径模糊查询使用的有界工作区索引最多保留的路径数 | | `fileSearchExcludedDirectories` | `['.git', 'node_modules']` | 遍历和直接补全时忽略的目录 basename | diff --git a/packages/ui/tui/src/components/dialogs.ts b/packages/ui/tui/src/components/dialogs.ts index f59ff747be..93c416b230 100644 --- a/packages/ui/tui/src/components/dialogs.ts +++ b/packages/ui/tui/src/components/dialogs.ts @@ -433,40 +433,70 @@ export class ModelDialog implements Component { } } -/** One transcript-detail state the details selector applies on confirm. */ -export type DetailsSelection = - | { readonly kind: 'tools'; readonly visibility: ToolCardVisibility } - | { readonly kind: 'reasoning'; readonly show: boolean } +/** Both transcript-detail dimensions the details selector applies on confirm. */ +export interface DetailsSelection { + readonly visibility: ToolCardVisibility + readonly showReasoning: boolean +} + +const TOOL_CARD_PHASES: readonly ToolCardVisibility[] = ['collapsed', 'expanded', 'hidden'] /** - * Keyboard selector over the transcript detail states: the three tool-card - * visibility phases and reasoning-block display. Enter applies the highlighted - * state and closes; Esc or Ctrl+C closes without changing anything. + * Keyboard selector over the two transcript-detail entries — tool-card + * visibility and reasoning display. Tab cycles the highlighted entry's pending + * value, Enter applies both pending values and closes, Esc or Ctrl+C closes + * without changing anything. A pending value renders as `current → pending`. */ export class DetailsDialog implements Component { private readonly list: SelectList + private readonly toolsItem: SelectItem + private readonly reasoningItem: SelectItem + private pendingVisibility: ToolCardVisibility + private pendingReasoning: boolean constructor( - visibility: ToolCardVisibility, - showReasoning: boolean, + private readonly visibility: ToolCardVisibility, + private readonly showReasoning: boolean, private readonly palette: Palette, done: (selection: DetailsSelection) => void, private readonly cancel: () => void, ) { - const current = (isCurrent: boolean): string => isCurrent ? ' — current' : '' - const items: SelectItem[] = [ - { value: 'collapsed', label: 'Tool cards · collapsed', description: `head/tail preview${current(visibility === 'collapsed')}` }, - { value: 'expanded', label: 'Tool cards · expanded', description: `full bodies${current(visibility === 'expanded')}` }, - { value: 'hidden', label: 'Tool cards · hidden', description: `conversation only${current(visibility === 'hidden')}` }, - { value: 'reasoning-shown', label: 'Reasoning · shown', description: `show reasoning blocks${current(showReasoning)}` }, - { value: 'reasoning-hidden', label: 'Reasoning · hidden', description: `omit reasoning blocks${current(!showReasoning)}` }, - ] - this.list = new SelectList(items, items.length, dialogSelectTheme(palette)) - this.list.setSelectedIndex(items.findIndex(item => item.value === visibility)) - this.list.onSelect = (item) => { - done(item.value === 'reasoning-shown' || item.value === 'reasoning-hidden' - ? { kind: 'reasoning', show: item.value === 'reasoning-shown' } - : { kind: 'tools', visibility: item.value as ToolCardVisibility }) + this.pendingVisibility = visibility + this.pendingReasoning = showReasoning + this.toolsItem = { value: 'tools', label: 'Tool cards', description: this.describeTools() } + this.reasoningItem = { value: 'reasoning', label: 'Reasoning', description: this.describeReasoning() } + this.list = new SelectList([this.toolsItem, this.reasoningItem], 2, dialogSelectTheme(palette)) + this.list.onSelect = () => { + done({ visibility: this.pendingVisibility, showReasoning: this.pendingReasoning }) + } + } + + /** `current → pending` when Tab moved the value, otherwise the current value. */ + private static pendingLabel(current: string, pending: string): string { + return pending === current ? current : `${current} → ${pending}` + } + + private describeTools(): string { + return DetailsDialog.pendingLabel(this.visibility, this.pendingVisibility) + } + + private describeReasoning(): string { + const label = (show: boolean): string => show ? 'shown' : 'hidden' + return DetailsDialog.pendingLabel(label(this.showReasoning), label(this.pendingReasoning)) + } + + /** Cycle the highlighted entry's pending value one step. */ + private cyclePending(): void { + const selected = this.list.getSelectedItem() + /* v8 ignore next -- the two-entry list always has a selection. */ + if (selected === null) return + if (selected.value === 'tools') { + const index = TOOL_CARD_PHASES.indexOf(this.pendingVisibility) + this.pendingVisibility = TOOL_CARD_PHASES[(index + 1) % TOOL_CARD_PHASES.length] as ToolCardVisibility + this.toolsItem.description = this.describeTools() + } else { + this.pendingReasoning = !this.pendingReasoning + this.reasoningItem.description = this.describeReasoning() } } @@ -476,6 +506,7 @@ export class DetailsDialog implements Component { handleInput(data: string): void { if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) this.cancel() + else if (matchesKey(data, Key.tab)) this.cyclePending() else this.list.handleInput(data) this.invalidate() } @@ -485,7 +516,7 @@ export class DetailsDialog implements Component { return renderDialog('Transcript details', [ ...this.list.render(innerWidth), '', - this.palette.dim('↑/↓ move • Enter apply • Esc cancel'), + this.palette.dim('↑/↓ move • Tab cycle • Enter apply • Esc cancel'), ], width, this.palette) } } diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts index 7fd76a1a6f..9a07b1f7c4 100644 --- a/packages/ui/tui/src/index.ts +++ b/packages/ui/tui/src/index.ts @@ -1051,8 +1051,9 @@ export function createTuiChat( palette, (selection: DetailsSelection) => { void session.close() - if (selection.kind === 'reasoning') setReasoning(selection.show) - else setToolsVisibility(selection.visibility) + // Reasoning first: its transcript rebuild would drop the card notice. + if (selection.showReasoning !== showReasoning) setReasoning(selection.showReasoning) + if (selection.visibility !== toolsVisibility) setToolsVisibility(selection.visibility) }, () => { void session.close() }, ), diff --git a/packages/ui/tui/tests/snapshots/details-selector.expected.txt b/packages/ui/tui/tests/snapshots/details-selector.expected.txt index ad6514d4e9..d6cacf8cef 100644 --- a/packages/ui/tui/tests/snapshots/details-selector.expected.txt +++ b/packages/ui/tui/tests/snapshots/details-selector.expected.txt @@ -28,39 +28,33 @@ buffer 13| 14| "Tool cards hidden. " style 0-17 dim -15| " ╭ Transcript details ──────────────────────────────────────────────────╮ " - style 14-85 fg=bright-magenta -16| "/workspace/pro│ Tool cards · collapsed head/tail preview │ " - style 0-13 fg=bright-magenta bold - style 14-14 fg=bright-magenta - style 40-66 dim - style 85-85 fg=bright-magenta -17| " dsh > │ Tool cards · expanded full bodies │ " +15| +16| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context" + style 0-17 fg=bright-magenta bold + style 18-31 dim + style 34-50 dim + style 53-57 dim + style 60-69 dim +17| " dsh > ╭ Transcript details ──────────────────────────────────────────────────╮ " style 1-3 fg=bright-magenta bold style 5-6 dim style 7-7 inverse - style 14-14 fg=bright-magenta - style 39-60 dim - style 85-85 fg=bright-magenta -18| " │ → Tool cards · hidden conversation only — current │ " - style 14-14 fg=bright-magenta - style 16-76 fg=bright-magenta inverse - style 85-85 fg=bright-magenta -19| " │ Reasoning · shown show reasoning blocks │ " - style 14-14 fg=bright-magenta - style 35-70 dim - style 85-85 fg=bright-magenta -20| " │ Reasoning · hidden omit reasoning blocks — current │ " - style 14-14 fg=bright-magenta - style 36-80 dim - style 85-85 fg=bright-magenta -21| " │ │ " - style 14-14 fg=bright-magenta - style 85-85 fg=bright-magenta -22| " │ ↑/↓ move • Enter apply • Esc cancel │ " - style 14-14 fg=bright-magenta - style 16-50 dim - style 85-85 fg=bright-magenta -23| " ╰──────────────────────────────────────────────────────────────────────╯ " style 14-85 fg=bright-magenta -24-39| +18| " │ → Tool cards hidden → collapsed │ " + style 14-14 fg=bright-magenta + style 16-67 fg=bright-magenta inverse + style 85-85 fg=bright-magenta +19| " │ Reasoning hidden │ " + style 14-14 fg=bright-magenta + style 27-55 dim + style 85-85 fg=bright-magenta +20| " │ │ " + style 14-14 fg=bright-magenta + style 85-85 fg=bright-magenta +21| " │ ↑/↓ move • Tab cycle • Enter apply • Esc cancel │ " + style 14-14 fg=bright-magenta + style 16-62 dim + style 85-85 fg=bright-magenta +22| " ╰──────────────────────────────────────────────────────────────────────╯ " + style 14-85 fg=bright-magenta +23-39| diff --git a/packages/ui/tui/tests/tui.snapshot.ts b/packages/ui/tui/tests/tui.snapshot.ts index c0b6d44f88..6931df723f 100644 --- a/packages/ui/tui/tests/tui.snapshot.ts +++ b/packages/ui/tui/tests/tui.snapshot.ts @@ -663,11 +663,13 @@ describe('TUI terminal-state snapshots', () => { harness.terminal.send('\r') }) await checkpoint('details-command', harness.terminal, { includeScrollback: true }) - // Bare /details opens the selector, preselecting and marking the current - // hidden/reasoning-off state. + // Bare /details opens the two-entry selector seeded with the current + // hidden/reasoning-off state; one Tab renders the tool-card entry's + // pending cycle as `hidden → collapsed`. await renderAfter(harness, () => { harness.terminal.send('/details') harness.terminal.send('\r') + harness.terminal.send('\t') }) await checkpoint('details-selector', harness.terminal, { includeScrollback: true }) nowSpy.mockRestore() diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index 093d8ad285..1f4272bb6b 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -2554,10 +2554,9 @@ describe('pi-tui chat lifecycle and transcript', () => { return from } - await open() - expect(result.terminal.output).toContain('Tool cards · collapsed') - expect(result.terminal.output).toContain('head/tail preview — current') - expect(result.terminal.output).toContain('show reasoning blocks — current') + const opened = await open() + expect(result.terminal.output.slice(opened)).toContain('Tool cards') + expect(result.terminal.output.slice(opened)).toContain('Reasoning') // A second /details while the selector is open replaces the overlay // instead of stacking a second one behind it. @@ -2570,22 +2569,37 @@ describe('pi-tui chat lifecycle and transcript', () => { await tick() expect(result.terminal.output.slice(cancelOutput)).not.toContain('Tool and context cards') - // Enter on the next visibility row applies it and closes. - await open() - result.terminal.send('\x1b[B') + // Tab cycles the highlighted entry's pending value; Enter applies it. + const cycled = await open() + result.terminal.send('\t') + await tick() + expect(result.terminal.output.slice(cycled)).toContain('collapsed → expanded') result.terminal.send('\r') await tick() expect(result.terminal.output).toContain('Tool and context cards expanded.') - // The reopened selector preselects the current phase and marks it. + // Both entries apply in one confirm: cycle tool cards through the + // wraparound back to collapsed and toggle reasoning off. const reopened = await open() - expect(result.terminal.output.slice(reopened)).toContain('full bodies — current') - result.terminal.send('\x1b[B') - result.terminal.send('\x1b[B') + result.terminal.send('\t') + result.terminal.send('\t') + await tick() + expect(result.terminal.output.slice(reopened)).toContain('expanded → collapsed') result.terminal.send('\x1b[B') + result.terminal.send('\t') + await tick() + expect(result.terminal.output.slice(reopened)).toContain('shown → hidden') result.terminal.send('\r') await tick() expect(result.terminal.output).toContain('Reasoning blocks hidden.') + expect(result.terminal.output).toContain('Tool and context cards collapsed.') + + // Enter with no pending change closes without a notice. + const unchanged = await open() + result.terminal.send('\r') + await tick() + expect(result.terminal.output.slice(unchanged)).not.toContain('Tool and context cards') + expect(result.terminal.output.slice(unchanged)).not.toContain('Reasoning blocks') // Ctrl+C also cancels. const ctrlCOutput = result.terminal.output.length