Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
# Conflicts: # .agents/notes/archived/feature/2026-07-28-dsh-guided-skill-session-commands.i18n.yaml # .agents/notes/archived/manifest.json # .agents/notes/implemented/bug-fix/2026-07-29-human-transcript-append-origin.i18n.yaml # .agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml # apps/cli/tests/snapshots/multi-turn-conversation/terminal.expected.txt # apps/cli/tests/snapshots/queued-manual-compact/terminal.expected.txt # apps/cli/tests/tui-keyless-smoke.e2e.ts # apps/cli/tests/tui.snapshot.ts # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/cookbook/extension-cookbook.i18n.yaml # docs/event-producer-consumer.md # packages/ui/tui/README.i18n.yaml # packages/ui/tui/README.md # packages/ui/tui/README.zh.md # packages/ui/tui/src/index.ts # packages/ui/tui/tests/harness.ts # packages/ui/tui/tests/session-reference.snapshot.ts # packages/ui/tui/tests/snapshots/conversation-streaming.expected.txt # packages/ui/tui/tests/snapshots/details-command.expected.txt # packages/ui/tui/tests/snapshots/details-selector.expected.txt # packages/ui/tui/tests/snapshots/disposed-terminal.expected.txt # packages/ui/tui/tests/snapshots/errors-and-help.expected.txt # packages/ui/tui/tests/snapshots/status-diagnostics-narrow.expected.txt # packages/ui/tui/tests/snapshots/status-diagnostics.expected.txt # packages/ui/tui/tests/snapshots/surface-after-compaction-narrow.expected.txt # packages/ui/tui/tests/snapshots/surface-after-compaction-wide.expected.txt # packages/ui/tui/tests/snapshots/surface-before-compaction.expected.txt # packages/ui/tui/tests/snapshots/surface-replayed-compaction.expected.txt # packages/ui/tui/tests/snapshots/todo-plan-cleared.expected.txt # packages/ui/tui/tests/snapshots/tool-cards-hidden-folded.expected.txt # packages/ui/tui/tests/snapshots/untrusted-controls.expected.txt # packages/ui/tui/tests/tui.snapshot.ts # packages/ui/tui/tests/tui.spec.ts
This commit is contained in:
-6
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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
|
||||
2026-07-22-tui-interactive-extension-service.md: 86cb39748358882d26766467d08f4f43510c1cc2
|
||||
2026-07-22-tui-interactive-extension-service.zh.md: d53f526a07b20fcff7086a1f501558d23e7eea8a
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
# Agent Note: Effect-owned TUI interactive extensions
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-22-tui-interactive-extension-service.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Cordis plugins can register human commands through `ctx.commands`, but a command that needs terminal interaction has no supported presentation boundary. It must either remain non-interactive or capture the TUI's private pi-tui tree, focus state, renderer, and shutdown lifecycle. That coupling makes the extension depend on one front door's internals, lets independently developed overlays compete for focus, and leaves plugin unload with no reliable way to remove queued or visible UI.
|
||||
|
||||
## Decision
|
||||
|
||||
A mounted `@deepseek-ai/dsh-tui` provides `ctx.tui` after terminal startup succeeds. The service belongs to that exact terminal and agent, disappears before terminal teardown, and causes plugins that inject it to unload and reload with provider availability. Other front doors do not emulate it.
|
||||
|
||||
`ctx.tui.openOverlay()` is the first and only interactive extension primitive. It accepts a component factory, constrained layout options, and an optional abort signal. The factory receives a frozen host with the current viewport, semantic theme functions, display-text escaping, redraw, close, and a lifetime signal. It does not receive the pi-tui `TUI`, overlay handle, editor, transcript tree, focus controller, or terminal object.
|
||||
|
||||
One private overlay manager serializes built-in and plugin requests in FIFO order. The model selector and `ctx.userInteraction` question panel use the same manager, so all modal interaction has one focus owner. Closing the active overlay restores pi-tui's previous focus before the next request activates. Overlay state is process-local presentation: it is neither appended to the session log nor rebuilt during resume.
|
||||
|
||||
The service method runs through Cordis's traceable service proxy. It installs an effect on the calling plugin fiber before admitting the request; caller disposal therefore removes a queued request or closes an active overlay and awaits the same settled outcome. TUI shutdown first rejects admission, then disposes the service fiber so dependent plugins and their effects quiesce, settles remaining built-in work, and only then drains and stops the terminal.
|
||||
|
||||
Component construction, rendering, input, and invalidation run behind an exception boundary. A failure closes that request with an `error` outcome, reports a visible terminal error, and lets the queue continue. Components are trusted package code: their rendered lines may contain ANSI styling, and they must call `host.display()` before including untrusted text.
|
||||
|
||||
## Verification
|
||||
|
||||
Manager tests pin FIFO admission, cancellation, repeated close, shutdown outcomes, guarded callbacks, host capabilities, and per-file coverage. Cordis lifecycle tests pin caller ownership, provider loss and return, unloading-time rejection, and cleanup quiescence. Fake-terminal integration tests exercise plugin overlays alongside built-in questions, restored editor input, terminal remount, startup rollback, and service disappearance. Existing TUI interaction tests continue to exercise the model selector and question panel through the shared path.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Expose pi-tui objects directly.** This gives plugins maximum freedom but makes private focus, rendering, and teardown state a public compatibility contract. It also cannot arbitrate independently loaded overlays.
|
||||
|
||||
**Put interactive callbacks on command definitions.** Commands remain transport-neutral domain entries even though TUI is their only shipped consumer. Adding terminal state to `ctx.commands` would couple discovery and dispatch to one presentation implementation.
|
||||
|
||||
**Create a complete TUI slot and action framework at once.** Actions, editor replacement, transcript renderers, status regions, and completion providers have different composition and conflict rules. Shipping them behind one broad API would freeze those rules before a concrete consumer proves them.
|
||||
|
||||
**Persist open overlays in session events.** Modal presentation is not model-visible session state, and arbitrary component state is not replayable. The plugin that owns durable data records that data through its domain service and recreates presentation when appropriate.
|
||||
|
||||
## Consequences
|
||||
|
||||
Interactive plugins gain a small stable front door with deterministic focus and Cordis-owned cleanup, while the TUI keeps authority over terminal lifecycle and pi-tui internals. Built-in dialogs and extensions cannot overlap or strand focus.
|
||||
|
||||
The API deliberately covers modal overlays only. Human command registration remains on `ctx.commands`; actions, slots, editor replacement, event renderers, and completion providers require separate contracts when real consumers establish their ordering and ownership semantics. FIFO serialization also means one stalled overlay blocks later modal work until its owner closes, aborts, or unloads it.
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
# Agent Note: 由 effect 持有的 TUI 交互扩展
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-22-tui-interactive-extension-service.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Cordis 插件可以通过 `ctx.commands` 注册用户命令,但需要终端交互的命令没有受支持的呈现边界。它只能保持非交互,或者捕获 TUI 私有的 pi-tui 树、焦点状态、渲染器和关闭生命周期。此类耦合会使扩展依赖某个入口的内部实现,让各自独立开发的浮层争抢焦点,并导致插件卸载时无法可靠移除排队中或已显示的 UI。
|
||||
|
||||
## 决策
|
||||
|
||||
挂载的 `@deepseek-ai/dsh-tui` 在终端成功启动后提供 `ctx.tui`。该服务只属于挂载时绑定的终端与 agent(智能体),在终端拆卸前消失,并使注入它的插件随着提供方的可用与否卸载和重新加载。其他入口不会模拟该服务。
|
||||
|
||||
`ctx.tui.openOverlay()` 是第一个也是唯一一个交互扩展原语。它接受组件工厂、受限的布局选项,以及可选的中止信号。工厂收到一个冻结的 host,其中包含当前视口、语义化主题函数、显示文本转义、重绘、关闭和生命周期信号。它不会收到 pi-tui `TUI`、浮层句柄、编辑器、transcript(文本记录)树、焦点控制器或终端对象。
|
||||
|
||||
一个私有浮层管理器按 FIFO 顺序串行处理内置请求和插件请求。模型选择器与 `ctx.userInteraction` 问题面板使用同一个管理器,因此所有模态交互只有一个焦点所有者。关闭活动浮层时,系统会先恢复 pi-tui 之前的焦点,再激活下一项请求。浮层状态是进程本地的呈现状态:它既不会追加到会话日志,也不会在恢复期间重建。
|
||||
|
||||
服务方法通过 Cordis 的可追踪服务代理运行。它在接纳请求前,向调用方插件的 fiber 注册一个 effect;因此,调用方执行 dispose(资源释放)时会移除排队中的请求或关闭活动浮层,并等待同一个结果完成结算。TUI 关闭时会先拒绝新请求,再 dispose 服务 fiber,让依赖插件及其 effect 完全静止,然后结算其余内置工作,最后才排空并停止终端。
|
||||
|
||||
组件构造、渲染、输入与失效处理均在异常边界内运行。任何失败都会以 `error` 结果关闭对应请求、在终端中报告一条可见错误,并让队列继续处理。组件属于受信任的包代码:其渲染行可以包含 ANSI 样式,但加入不受信任的文本前必须调用 `host.display()`。
|
||||
|
||||
## 验证
|
||||
|
||||
管理器测试固定了 FIFO 准入、取消、重复关闭、关闭结果、受保护回调、host 能力和逐文件覆盖率。Cordis 生命周期测试固定了调用方所有权、提供方消失与恢复、卸载期间的拒绝,以及清理达到完全静止。模拟终端集成测试覆盖插件浮层与内置问题的协作、编辑器输入焦点恢复、终端重新挂载、启动回滚和服务消失。既有 TUI 交互测试继续通过共享路径覆盖模型选择器与问题面板。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**直接暴露 pi-tui 对象。** 这会赋予插件最大的自由度,却会把私有的焦点、渲染与拆卸状态变成公开兼容性契约,也无法在独立加载的浮层之间进行仲裁。
|
||||
|
||||
**在命令定义中加入交互回调。** 命令仍是传输无关的领域条目,尽管 TUI 是唯一已交付的消费方。向 `ctx.commands` 添加终端状态,会让发现与分派流程耦合到某一种呈现实现。
|
||||
|
||||
**一次性建立完整的 TUI slot 与 action 框架。** action、编辑器替换、transcript 渲染器、状态区域和补全提供方具有不同的组合规则与冲突规则。在具体消费方验证这些规则之前就将其纳入一个宽泛 API,会过早固化这些规则。
|
||||
|
||||
**将打开的浮层持久化为会话事件。** 模态呈现并非模型可见的会话状态,任意组件状态也无法回放。拥有持久数据的插件应通过自身的领域服务记录这些数据,并在适当时重新创建呈现。
|
||||
|
||||
## 后果
|
||||
|
||||
交互式插件获得一个小而稳定的入口,具备确定性的焦点管理和由 Cordis 持有的清理机制;TUI 则继续掌控终端生命周期和 pi-tui 内部实现。内置对话框与扩展无法重叠,也不会遗留失去归属的焦点。
|
||||
|
||||
该 API 有意只覆盖模态浮层。用户命令仍然在 `ctx.commands` 上注册;action、slot、编辑器替换、事件渲染器和补全提供方需要另行设计契约,等待实际消费方确定其顺序与所有权语义。FIFO 串行处理也意味着,一个停滞的浮层会阻塞后续模态工作,直至其所有者关闭、中止或卸载该浮层。
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/architecture/2026-07-27-tui-chat-channel-module-split.md
|
||||
2026-07-27-tui-chat-channel-module-split.md: 56b345b670cd8426780bfdd8c2b2f5719461554c
|
||||
2026-07-27-tui-chat-channel-module-split.zh.md: d74844a762bc519d0f499696fe343567eebfe920
|
||||
@@ -1,36 +0,0 @@
|
||||
# Agent Note: dsh-tui chat channel module split
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-27-tui-chat-channel-module-split.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`packages/ui/tui/src/index.ts` had grown past 2000 lines. Most of it was one `createTuiChat` factory: a ~1600-line closure holding roughly forty mutable variables and as many nested closures. Model selection, the ask-user-question queue, and session resume were tangled into that single scope, so a reader could not follow any one concern without holding the whole file in their head, and unrelated edits collided. A prior pass had grouped `src/` into `components/`, `session/`, `extension/`, but the entry file itself and the loose top-level input files (`autocomplete.ts`, `file-autocomplete.ts`, `skill-invocation.ts`, `xml-tool-output.ts`) were untouched.
|
||||
|
||||
## Decision
|
||||
|
||||
The chat channel's cohesive sub-machines are extracted from `createTuiChat` into `src/chat/`, each a factory that takes an explicit dependency bundle instead of closing over the entry scope:
|
||||
|
||||
- `chat/model-command.ts` — `createModelController`: the queued `/model` command, the model+reasoning-effort selector overlay, and the selected model's context-window resolution. Owns the context-window cache that the prompt and status views read.
|
||||
- `chat/questions.ts` — `createQuestionQueue`: the user-interaction provider and the one-at-a-time FIFO ask-user-question overlays.
|
||||
- `chat/resume.ts` — `createResumeController`: the `/resume` selector, per-candidate summary reads, the pre-handoff preflight, the terminal handoff, and the durable resume-hint command.
|
||||
- `chat/helpers.ts` — zero-state helpers (`formatCwd`, `gitBranch`, surface/tool-call derivations, session-reference cards), the `HintEditor`, and banner-reveal constants.
|
||||
- `chat/channel.ts` — `ChatChannelDeps` (the collaborator surface every sub-controller shares) and `ChannelNotice` (mixed in by the controllers that report outcomes). Each `*Deps` extends these, so the shared surface has one definition.
|
||||
|
||||
`src/` is reorganized so `chat/` holds every chat-channel concern: the sub-controllers above plus the former input files and the former `session/` files (`timing.ts`, `tokens.ts`) all move under `chat/`. `xml-tool-output.ts` moves under `components/`. The host/process boundary interfaces (`TuiRuntime`, `TuiResumeHost`) move to `src/runtime.ts`. After the split `src/` is `chat/`, `components/`, `extension/`, and the top-level `index.ts` / `config.ts` / `prompt.ts` / `runtime.ts` / `invariant.ts`; `index.ts` drops from 2067 to ~1530 lines and now constructs and wires the three controllers.
|
||||
|
||||
The convention for a controller's dependency bundle: stable value collaborators (`ctx`, `resolved`, `palette`, `overlayManager`, and each controller's own services) are destructured once; the channel callbacks (`appendNotice`, `requestRender`, `isDisposed`, `agentStatus`) stay on `deps` so a controller always calls the channel's current implementation. `channel.ts`'s JSDoc states this rule.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Free functions taking a shared mutable context object.** Rejected: it would re-expose the same forty-field grab-bag the split set out to remove, just under a parameter name.
|
||||
- **Extracting the status/timing animation controller too.** Deferred: `runningStatus` is read directly by the prompt caret animation in `updatePromptValues`, so a controller boundary there would leak its internal state back through getters — a leaky seam for little gain. It stays inline in `index.ts`.
|
||||
|
||||
## Consequences
|
||||
|
||||
Each concern is now readable and testable in isolation, and the shared dependency surface is defined once instead of copied into three interfaces. The cost: `index.ts` constructs the controllers and threads the callback bundle, and the model controller is a `let` forward-reference (`updatePromptValues` closes over it, but it is built later once `appendNotice`/`overlayManager` exist), carrying one justified `prefer-const` disable and a deferred first paint.
|
||||
|
||||
## Testing
|
||||
|
||||
Behavior is unchanged: all existing package tests and TUI snapshots pass without re-recording, which is the contract for this refactor.
|
||||
@@ -1,36 +0,0 @@
|
||||
# Agent Note: dsh-tui 聊天通道模块拆分
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-27-tui-chat-channel-module-split.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`packages/ui/tui/src/index.ts` 已超过 2000 行,其中绝大部分是单个 `createTuiChat` 工厂:一个约 1600 行的闭包,持有约四十个可变变量以及同等数量的嵌套闭包。模型选择、ask-user-question 队列、会话恢复都缠绕在这一个作用域里,读者无法在不把整份文件装进脑子的前提下理清任何单一关注点,互不相关的改动也会彼此冲突。此前一轮已把 `src/` 归组为 `components/`、`session/`、`extension/`,但入口文件本身以及散落在顶层的输入相关文件(`autocomplete.ts`、`file-autocomplete.ts`、`skill-invocation.ts`、`xml-tool-output.ts`)未动。
|
||||
|
||||
## Decision
|
||||
|
||||
聊天通道内聚的子机制从 `createTuiChat` 中抽出,迁入 `src/chat/`,每个都是接收显式依赖包的工厂,而非闭包捕获入口作用域:
|
||||
|
||||
- `chat/model-command.ts` — `createModelController`:排队执行的 `/model` 命令、模型加推理力度(reasoning-effort)的选择浮层,以及所选模型上下文窗口的解析。持有供提示行与状态视图读取的上下文窗口缓存。
|
||||
- `chat/questions.ts` — `createQuestionQueue`:user-interaction provider 以及一次仅一个的 FIFO ask-user-question 浮层。
|
||||
- `chat/resume.ts` — `createResumeController`:`/resume` 选择器、逐候选摘要读取、交接前预检、终端交接,以及持久化的恢复提示命令。
|
||||
- `chat/helpers.ts` — 无状态辅助函数(`formatCwd`、`gitBranch`、surface/工具调用派生、会话引用卡片)、`HintEditor`,以及横幅揭示常量。
|
||||
- `chat/channel.ts` — `ChatChannelDeps`(每个子控制器共享的协作者面)与 `ChannelNotice`(由需要上报结果的控制器混入)。各 `*Deps` 继承它们,使共享面只有一处定义。
|
||||
|
||||
`src/` 随之重组,使 `chat/` 汇集所有聊天通道关注点:上述子控制器,加上原来的输入文件与原 `session/` 文件(`timing.ts`、`tokens.ts`)都迁到 `chat/` 之下。`xml-tool-output.ts` 迁到 `components/` 之下。宿主/进程边界接口(`TuiRuntime`、`TuiResumeHost`)迁到 `src/runtime.ts`。拆分后 `src/` 为 `chat/`、`components/`、`extension/`,以及顶层的 `index.ts` / `config.ts` / `prompt.ts` / `runtime.ts` / `invariant.ts`;`index.ts` 从 2067 行降至约 1530 行,现负责构造并接线这三个控制器。
|
||||
|
||||
控制器依赖包的约定:稳定的取值型协作者(`ctx`、`resolved`、`palette`、`overlayManager`,以及各控制器自有的服务)一次性解构;通道回调(`appendNotice`、`requestRender`、`isDisposed`、`agentStatus`)保留在 `deps` 上,使控制器始终调用通道当前的实现。`channel.ts` 的 JSDoc 陈述了此规则。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **接收共享可变上下文对象的自由函数。** 否决:那会把拆分本要消除的四十字段大杂烩,仅换个参数名重新暴露出来。
|
||||
- **同时抽出状态/计时动画控制器。** 推迟:`runningStatus` 被 `updatePromptValues` 中的提示光标动画直接读取,在此设控制器边界会让其内部状态经 getter 反向泄漏——收益甚微的漏隙缝。它继续内联在 `index.ts` 中。
|
||||
|
||||
## Consequences
|
||||
|
||||
每个关注点现可独立阅读与测试,共享依赖面只定义一次,而非复制进三个接口。代价:`index.ts` 负责构造这些控制器并穿针引线地传入回调包;模型控制器是 `let` 前向引用(`updatePromptValues` 闭包捕获它,但它要待 `appendNotice`/`overlayManager` 就绪后才构造),因而带一处有正当理由的 `prefer-const` 禁用与一次延后的首帧绘制。
|
||||
|
||||
## Testing
|
||||
|
||||
行为不变:现有的包测试与 TUI 快照全部无需重录即通过,这正是本次重构的契约。
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/architecture/2026-07-28-consolidated-tui-presentation.md
|
||||
2026-07-28-consolidated-tui-presentation.md: 8200c8e96cdea7cf5ae54c1328623bb03f841d00
|
||||
2026-07-28-consolidated-tui-presentation.zh.md: 852bd413b25f93f3b9096b78d69b21ab420df37b
|
||||
@@ -1,63 +0,0 @@
|
||||
# Agent Note: Consolidated TUI presentation and navigation
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-consolidated-tui-presentation.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The terminal UI accumulated independent presentation rules that interacted poorly: palette roles aliased one another or inverted emphasis on light terminals; tool-card framing, output, and exit markers repeated or competed; injected context was parsed as XML and could not fold reliably; and `/resume` excluded sessions outside the current workspace even when the launcher could reach them. Each symptom appeared local, but the durable decision is one terminal-reading model: a small inspectable palette, status-led cards with recessed bodies, content-independent transcript folding, and workspace-aware navigation.
|
||||
|
||||
## Decision
|
||||
|
||||
### Palette
|
||||
|
||||
`paletteSpec(scheme)` is the single table of SGR codes, close codes, and purposes. `createPalette` derives every wrapper from it and `/palette` prints the same table in the running terminal. Components do not emit their own SGR sequences except for the fixed startup brand gradient. Every close resets every SGR group its open sets.
|
||||
|
||||
Duplicate roles are merged: `muted` into `dim`, `added` into `success`, `removed` into `error`, and the unused second accent is removed. `dim` uses `2;39` and closes with `22;39` on both schemes so recessed text stays relative to the terminal foreground rather than becoming a fixed heavy gray on light backgrounds. Colors and attributes are branded separately in TypeScript, allowing attribute/color composition while rejecting nested colors whose reset would discard the outer color.
|
||||
|
||||
### Tool cards
|
||||
|
||||
A tool card has one colored `Tool / <name>` status header over one dim body. Presenter titles, terminal commands and cwd rows, output, XML text, and fold markers use that body tone. Diff colors remain because red and green carry meaning, and signal markers remain errors.
|
||||
|
||||
`renderUnknownXml` receives an explicit body styler for unknown tool results. Terminal presenters parse and remove the model-facing final exit or signal marker before returning `TerminalResultView.output`; the TUI renders the structured status once as its own pill. Truncation, timeout, and sandbox lines remain in the body because the pill does not represent them.
|
||||
|
||||
### Injected context and folding
|
||||
|
||||
Injected context renders as prose in `ContextCardComponent`, not through the XML tree renderer. Exact matched outer `<system-reminder>` lines are stripped, but mismatched, unpaired, or inline tag-like text remains verbatim. Model-facing content is unchanged. Folding uses the shared `preview` helper after body assembly, so it depends only on row count, never parser success or payload characters.
|
||||
|
||||
`Ctrl+O` cycles collapsed, expanded, and hidden. Tool cards disappear in the hidden state together with their card-owned leading gap. Context cards participate in collapsed and expanded states but fall back to collapsed while tools are hidden, because injected instructions are not disposable tool traffic. The hidden phase additionally folds each turn's assistant steps into one message; the [hidden-mode assistant fold Agent Note](../feature/2026-07-29-tui-hidden-mode-assistant-fold.md) owns that rule.
|
||||
|
||||
### Cross-workspace resume
|
||||
|
||||
The resume picker summarizes all records and owns a current-workspace/all-workspaces scope toggled with Tab. It defaults to the current workspace, adds workspace labels only in the broader scope, and refuses records without a cwd because there is no directory to enter.
|
||||
|
||||
`TuiResumeHost.handoff` receives the selected `SessionId` and the cwd re-read during preflight. The CLI changes directory before disposing the current app, so an unreachable directory fails while the terminal can still recover; `execve` then inherits the selected workspace. The launcher also supplies the exit message rather than asking the TUI to reconstruct launcher syntax.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep separate notes and local fixes for each visual symptom.** Rejected: the decisions share one reading hierarchy and repeatedly superseded each other. One owner makes the final palette, card, context, and navigation rules clear without requiring readers to reconstruct chronology.
|
||||
|
||||
**Keep aliases and enforce presentation by convention.** Rejected: aliases imply distinctions that do not exist, and nested color resets or incomplete SGR closes fail silently. A single table plus types makes the contract inspectable and mechanically checked.
|
||||
|
||||
**Retain framing/output color splits inside tool cards.** Rejected: real cards mixed default foreground, cyan commands, dim cwd, unstyled XML, and dim output. The status header already provides the scan anchor; one recessed body removes noise. Diff colors are the narrow semantic exception.
|
||||
|
||||
**Parse or repair injected context as XML.** Rejected: reminder frames are prompting conventions around arbitrary prose containing raw ampersands, comparisons, and placeholder angle brackets. Repairing or escaping it would either guess structure or alter model-visible text.
|
||||
|
||||
**Hide context cards with tool cards.** Rejected: context carries injected instructions, not recoverable execution detail. The hidden phase therefore removes only tool traffic.
|
||||
|
||||
**Keep resume restricted to one workspace or infer cwd after boot.** Rejected: the restriction forces manual relaunch, while restored header cwd does not control filesystem and shell resolution. The target directory must cross the host seam before process replacement.
|
||||
|
||||
**Drop the TUI exit pill or remove model-facing exit markers.** Rejected: the pill is the scannable UI status, while the text marker is the model's status signal. The presenter consumes the marker when constructing the structured view so both audiences receive one representation.
|
||||
|
||||
## Consequences
|
||||
|
||||
The transcript reads as colored status headers over recessed detail, context presentation is stable for arbitrary prose, and one shortcut controls transcript density. The public `TuiTheme.muted` role is removed; extensions use `dim`. The palette and `renderUnknownXml` contracts are stricter, adding small compile-time friction in exchange for preventing silent style loss.
|
||||
|
||||
Cross-workspace resume can move every path-resolving tool to another directory. A missing or inaccessible cwd prevents handoff. The broader picker also makes concurrent access to a shared session store easier to reach; cross-process session locking remains separate work.
|
||||
|
||||
The terminal presenter still treats a final output line exactly matching its exit-marker grammar as structured status, so a command that intentionally prints such a line can lose it from the card body. This residual is documented by `dsh-tool-bash`.
|
||||
|
||||
## Testing
|
||||
|
||||
TUI unit and keyless terminal snapshots cover palette enumeration, light/dark roles, legal and illegal style composition, uniformly dim card bodies, semantic diff colors, marker-free terminal output with one exit pill, prose-preserving context frames, content-independent folding, the three-state Ctrl+O cycle, model filtering, and both resume scopes. CLI handoff tests cover passing the re-read cwd and rejecting directory-entry failure before teardown. Tool-bash tests pin result-marker emission, parse, and stripping as one round trip.
|
||||
@@ -1,63 +0,0 @@
|
||||
# Agent Note: 统一的 TUI 呈现与导航
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-28-consolidated-tui-presentation.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
终端 UI 逐步积累了多套彼此干扰的呈现规则:调色板角色互为别名,或在浅色终端中颠倒强调层级;工具卡片的框架、输出和退出标记重复或争夺注意力;注入上下文被当作 XML 解析,无法可靠折叠;`/resume` 即使能通过启动器访问其他工作区,也会排除不属于当前工作区的会话。每个症状看似局部,但持久决策只有一个终端阅读模型:精简且可检查的调色板、以状态为首且正文内收的卡片、与内容无关的记录折叠,以及感知工作区的导航。
|
||||
|
||||
## Decision
|
||||
|
||||
### 调色板
|
||||
|
||||
`paletteSpec(scheme)` 是 SGR 开始码、结束码和用途的唯一表。`createPalette` 从该表派生所有包装器,`/palette` 在运行中的终端打印同一张表。除固定的启动品牌渐变外,组件不自行发出 SGR 序列。每个结束码都重置对应开始码设置的所有 SGR 组。
|
||||
|
||||
重复角色被合并:`muted` 并入 `dim`,`added` 并入 `success`,`removed` 并入 `error`,未使用的第二强调色被移除。`dim` 在两种配色方案中都使用 `2;39`,并以 `22;39` 结束,使内收文本相对于终端前景色变暗,而不会在浅色背景上变成固定的深灰色。TypeScript 分别标记颜色和属性,允许属性与颜色组合,同时拒绝会因重置而丢失外层颜色的嵌套颜色。
|
||||
|
||||
### 工具卡片
|
||||
|
||||
工具卡片由一行带颜色的 `Tool / <name>` 状态标题和一块统一的 dim 正文组成。呈现器标题、终端命令及 cwd 行、输出、XML 文本和折叠标记都使用正文色调。差异颜色继续保留,因为红绿承载语义;信号标记也继续作为错误显示。
|
||||
|
||||
`renderUnknownXml` 对未知工具结果显式接收正文样式器。终端呈现器在返回 `TerminalResultView.output` 前解析并移除面向模型的末尾退出或信号标记;TUI 只把结构化状态呈现一次。截断、超时和沙箱信息继续留在正文中,因为状态标记不表达这些事实。
|
||||
|
||||
### 注入上下文与折叠
|
||||
|
||||
注入上下文由 `ContextCardComponent` 按普通文本呈现,不经过 XML 树渲染器。仅移除精确配对的外层 `<system-reminder>` 行;不匹配、单边或正文内类似标签的文本都原样保留。面向模型的内容不变。折叠在正文组装完成后使用共享 `preview` 辅助函数,因此只取决于行数,不依赖解析是否成功或载荷包含哪些字符。
|
||||
|
||||
`Ctrl+O` 在折叠、展开和隐藏之间循环。隐藏状态会连同卡片自有的前导间距一起移除工具卡片。上下文卡片参与折叠和展开状态,但工具隐藏时回到折叠状态,因为注入指令不是可丢弃的工具流量。隐藏阶段还会把每个轮次的 assistant 步骤折叠为一条消息;该规则由[隐藏模式 assistant 折叠 Agent Note](../feature/2026-07-29-tui-hidden-mode-assistant-fold.md)负责。
|
||||
|
||||
### 跨工作区恢复
|
||||
|
||||
恢复选择器汇总所有记录,并维护可用 Tab 切换的当前工作区/所有工作区范围。默认范围是当前工作区;只有更宽范围才显示工作区标签。没有 cwd 的记录会被拒绝,因为没有可进入的目录。
|
||||
|
||||
`TuiResumeHost.handoff` 接收选中的 `SessionId` 和预检时重新读取的 cwd。CLI 在释放当前应用前切换目录,因此无法访问的目录会在终端仍可恢复时失败;随后 `execve` 继承所选工作区。退出提示也由启动器提供,而不是让 TUI 反推启动器命令语法。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**为每个视觉症状保留独立 Agent Note 和局部修复。** 否决:这些决策共享同一阅读层级,而且彼此多次取代。由一份记录统一拥有最终的调色板、卡片、上下文和导航规则,读者无需重建变更顺序。
|
||||
|
||||
**保留别名,并依靠约定执行呈现规则。** 否决:别名暗示并不存在的差异;嵌套颜色重置或不完整的 SGR 结束会静默失败。单一表格加类型约束使契约可检查且可机械验证。
|
||||
|
||||
**保留工具卡片内部的框架/输出颜色分层。** 否决:真实卡片会混用默认前景、青色命令、dim cwd、无样式 XML 和 dim 输出。状态标题已经提供扫描锚点;统一内收正文能消除噪声。差异颜色是狭窄的语义例外。
|
||||
|
||||
**把注入上下文继续解析或修复成 XML。** 否决:提醒框架只是包裹任意普通文本的提示约定,其中会包含原始 `&`、比较表达式和尖括号占位符。修复或转义要么猜测结构,要么改变模型可见文本。
|
||||
|
||||
**随工具卡片一起隐藏上下文卡片。** 否决:上下文承载注入指令,不是可恢复的执行细节。因此隐藏阶段只移除工具流量。
|
||||
|
||||
**把恢复限制在一个工作区,或在启动后推断 cwd。** 否决:前者迫使用户手动重启;后者恢复的会话头 cwd 并不控制文件系统和 shell 的路径解析。目标目录必须在进程替换前跨过主机接口。
|
||||
|
||||
**移除 TUI 退出状态标记,或移除面向模型的退出标记。** 否决:前者是便于扫描的 UI 状态,后者是模型的状态信号。呈现器在构造结构化视图时消费文本标记,使两类受众各看到一种表示。
|
||||
|
||||
## Consequences
|
||||
|
||||
记录现在表现为带颜色的状态标题和内收细节;上下文对任意普通文本都稳定呈现;一个快捷键控制记录密度。公共 `TuiTheme.muted` 角色被移除,扩展改用 `dim`。调色板和 `renderUnknownXml` 契约更严格,以少量编译期摩擦换取对静默样式丢失的防护。
|
||||
|
||||
跨工作区恢复会把所有依赖路径解析的工具移动到另一个目录。cwd 缺失或不可访问时不能交接。更宽的选择范围也使共享会话存储的并发访问更容易触达;跨进程会话锁仍是独立后续工作。
|
||||
|
||||
终端呈现器仍会把与退出标记语法完全一致的最后一行输出视为结构化状态,因此命令有意打印这种行时,卡片正文可能丢失该行。`dsh-tool-bash` 已记录这一残余限制。
|
||||
|
||||
## Testing
|
||||
|
||||
TUI 单元测试和无密钥终端快照覆盖调色板枚举、浅色/深色角色、合法与非法样式组合、统一 dim 卡片正文、保留语义的差异颜色、仅有一个退出状态且正文无标记、普通文本上下文框架、与内容无关的折叠、Ctrl+O 三态循环、模型过滤和两种恢复范围。CLI 交接测试覆盖传递重新读取的 cwd,并在释放前拒绝目录切换失败。tool-bash 测试把结果标记的生成、解析和移除固定为同一轮往返契约。
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/architecture/2026-07-28-launcher-owned-resume-identity.md
|
||||
2026-07-28-launcher-owned-resume-identity.md: da9b4571d154137d34ef3690e7b4aa9bc7bb9082
|
||||
2026-07-28-launcher-owned-resume-identity.zh.md: 51ccffd7bb9c8eeda03afe2528d3cb4250e2d906
|
||||
@@ -1,64 +0,0 @@
|
||||
# Agent Note: Launcher-owned session identity and exit line
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-launcher-owned-resume-identity.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Two facts a launcher owns were shipped as deployment config keys on the TUI app bundle: `resumeSessionId` (which session `main` binds to) and `resumeCommand` (the exit hint template, with `{session}` interpolated). Neither varies by deployment — both are properties of how the process was invoked, which only the launcher knows.
|
||||
|
||||
Routing them through YAML made them silently droppable. `@cordisjs/plugin-include` applies a targeted patch by replacing whole top-level keys (`target[key] = value`), so a personal `~/.dsh/config.yaml` patching the `tui-agent` entry's `config` replaces the shipped block entirely. A user overlay written to change provider and model therefore deleted every resume key it did not restate, and nothing reported it: absent `resumeCommand` legitimately means "no fallback configured".
|
||||
|
||||
Both failures were live in one real overlay. The exit hint stopped printing, because the overlay omitted `resumeCommand`. Worse, the overlay carried `resumeSessionId: !!js process.env.RESUME_SESSION_ID` — a stale line from before [the env-var bridge was removed](../../archived/architecture/2026-07-24-dsh-commander-argument-adapter.md) — which overwrote the shipped `!!js "typeof resumeSessionId === 'string' ? …"` intake with a read of a variable nothing sets. `dsh --resume <valid-id>` then started a *fresh* session and said nothing, reproduced directly: the banner showed a newly minted id, not the requested one. The [`dsh meta`](../feature/2026-07-28-dsh-meta-source-workspace.md) note had recorded this silent resume as an unexplained pre-existing defect; the overlay's shallow replacement is the cause.
|
||||
|
||||
A config key cannot express these facts safely, because the deployment is not the authority on them.
|
||||
|
||||
## Decision
|
||||
|
||||
Session identity and the exit line are launcher-owned context slots, provided before any Loader entry mounts. Neither appears in any `cordis.yml` nor in any plugin's `Config`.
|
||||
|
||||
Both sit beside the existing `tuiResumeHost` host capability, which set the precedent — a resume host has always been a provided capability rather than config. Each slot is declared by the package that consumes it:
|
||||
|
||||
- `CONFIGURED_AGENT_IDENTITIES_KEY` (`dsh-agent-loop`) carries launcher identities keyed by configured-agent `id`, each a `LauncherAgentIdentity` (`{ id: SessionId, resume: boolean }`). `agent-loop` applies the matching identity over its configured agent, replacing both identity keys, and takes the history-loading `resumeSessionId` path only when `resume` is set, because that path requires an existing log and fails loud without one. An absent slot leaves the configured identity untouched. The `tui` row resolves the same id through its own `sessionId` key, so the front door renders exactly the agent that was bound.
|
||||
- `TUI_GOODBYE_MESSAGE_KEY` (`dsh-tui`) carries the complete line printed once the terminal is released on exit. Absent prints nothing.
|
||||
|
||||
Identity belongs to `agent-loop` because that is the plugin which creates configured agents, and because a patch replaces a row's whole `config`: an overlay repointing the agent row's model route would erase a launcher-set identity key. See [the shared-base overlay note](../simplification/2026-07-29-shared-base-config-overlays.md).
|
||||
|
||||
`apps/cli` mints or selects the id and builds the line from the invocation it is reproducing, sharing one `resumeArgs` helper with the `/resume` execve handoff so the printed command and the in-place handoff cannot diverge. The line names `--config` when one was passed. Resume always re-enters the default surface through `dsh --resume <id>`; `dsh meta` accepts no default-surface options and always starts fresh.
|
||||
|
||||
**`ctx.provide` is the only channel from launcher argv into a Loader-mounted plugin.** Config `!!js` expressions evaluate as `with (entry.ctx) { eval(expr) }` (`vendor/loader/src/config/utils.ts`), so a bare identifier resolves against the entry's context and nothing else reaches it. The slot therefore cannot be removed while the app bundle is mounted from YAML; what changes is that it is now internal launcher↔app plumbing instead of a documented key a config author must wire correctly.
|
||||
|
||||
The message is a plain string, not a callback. That forces the launcher to know the id before boot, which is why minting moved out of the app bundle — and it keeps exit free of awaited work after the terminal is released.
|
||||
|
||||
The TUI owns rendering, not wording: it applies `displayText` before its own `palette.muted`, so a hostile `--config` path cannot inject terminal escapes into the exit line. Sanitizing means the launcher cannot embed its own ANSI.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the keys and add built-in defaults in the app bundle.** Rejected: a default in code survives an overlay, but two ways to state one fact remain, and a config author can still set the key wrong — which is exactly how the stale `process.env.RESUME_SESSION_ID` line disabled resume.
|
||||
|
||||
**Merge the app bundle into `apps/cli` and delete the slot entirely.** Rejected here, then [adopted later](../simplification/2026-07-29-shared-base-config-overlays.md) in a form this note did not consider: the composition moved into flat config files (`apps/cli/config/base.cordis.yml` plus a per-surface overlay) rather than into CLI code, so it never entered the `v8 ignore` process-wiring block, and the overlay extension points survive as ordinary row patches. The slot itself was not deleted — it moved to `dsh-agent-loop`, because a launcher fact still cannot travel through a replaceable config key.
|
||||
|
||||
**Put the goodbye message on `TuiResumeHost`.** Rejected: an exit line is not a handoff capability, and a host that cannot replace its process may still want to print one. They are independent slots.
|
||||
|
||||
**Have the host supply only the command text and let the TUI keep the `To resume this session:` prefix.** Rejected: the TUI would retain resume vocabulary for a string it no longer understands, and meta mode proves the launcher is the only component that knows what the command should say.
|
||||
|
||||
**Let the TUI keep suppressing the line until the session is durably persisted.** Rejected: that check is why the exit path queried persistence and swallowed listing failures. A plain string cannot consult persistence, and misuse now fails loud through `agent-loop/config-start-failed` rather than silently resuming nothing.
|
||||
|
||||
**A callback (`goodbyeMessage(agent)`) so the host could decide at exit time.** Rejected: it restores async work after `ui.stop()`, reintroducing a hang risk during teardown for a string that is already knowable at boot.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Removing two published `Config` keys is a breaking config change: a stale config naming either now fails schema validation at boot instead of degrading silently. Intended, and acceptable pre-release.
|
||||
- `TuiResumeHost` is unchanged, but `TuiRuntime` gains `goodbyeMessage`; `apps/cli` is the only provider.
|
||||
- The exit line prints even for a session with no log (launch, quit immediately). Using it then fails loud rather than starting a surprise session. This is the deliberate cost of dropping the persistence check.
|
||||
- `dsh-tui` no longer reads `sessionPersistence` at all: `currentResumeCommand`, `listWorkspaceSessions`, and its swallowed-error path are deleted, and the `/resume` selector's `sessionQuery` reads are now the only session discovery in the TUI.
|
||||
- The launcher mints session ids for its own app, so a non-CLI host that provides no slot keeps the bundle's own minting.
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/ui/tui/tests/tui.spec.ts` pins the printed line, the absent-slot silence, and escape sanitization of a hostile message; the former two exit-suppression tests are replaced, since suppression is the behavior this change removes. `packages/core/agent-loop/tests/` drives the identity slot for the resume, launcher-minted, and no-slot cases.
|
||||
|
||||
The load-bearing coverage is `apps/cli/tests/tui-keyless-smoke.e2e.ts`, which launches the real `apps/cli/src/bin.ts` in a PTY: one test asserts the exit line carries `--config`, and a regression test seeds a personal `config.yaml` that replaces the entire `agent-loop` config block and asserts the line still prints — encoding "an overlay cannot drop resume" as an executed contract rather than a comment.
|
||||
|
||||
Verified live in tmux against the real personal overlay: the defect reproduced on unmodified staging (requested id ignored, fresh id in the banner), and on this branch the same overlay yields a printed exit line, a `--resume` that restores the prior turn, and a `/resume` selector marking the session `current · live · persisted`. A wrong id now fails loud.
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
# Agent Note:由启动器持有的会话身份与退出行
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-28-launcher-owned-resume-identity.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
有两项本应由启动器持有的事实,却被作为 TUI 应用组合包上的部署配置键交付:`resumeSessionId`(`main` 绑定到哪个会话)与 `resumeCommand`(退出提示的模板,其中 `{session}` 会被插值)。二者都不随部署而变——它们都是进程被如何调用的属性,而这一点只有启动器知道。
|
||||
|
||||
把它们经由 YAML 传递,使其可被静默丢弃。`@cordisjs/plugin-include` 施加定向补丁的方式是替换整个顶层键(`target[key] = value`),因此一份对 `tui-agent` 条目的 `config` 打补丁的个人 `~/.dsh/config.yaml`,会把交付时的整块内容整体替换掉。于是,一份为改动 provider 和 model 而写的用户 overlay,会删掉它未重述的每一个 resume 键,且没有任何东西报告这一点:缺失 `resumeCommand` 合法地意味着「未配置回退」。
|
||||
|
||||
两处失效在同一份真实的 overlay 中同时存在。退出提示不再打印,因为该 overlay 省略了 `resumeCommand`。更糟的是,该 overlay 带着 `resumeSessionId: !!js process.env.RESUME_SESSION_ID`——一行来自 [env 变量桥被移除](../../archived/architecture/2026-07-24-dsh-commander-argument-adapter.md)之前的陈旧代码——它用一次对某个无人设置的变量的读取,覆盖掉了交付时的 `!!js "typeof resumeSessionId === 'string' ? …"` 入口。此后 `dsh --resume <valid-id>` 会开启一个*全新*会话且什么都不说,并被直接复现:banner 显示的是一个新铸造的 id,而非所请求的那个。[`dsh meta`](../feature/2026-07-28-dsh-meta-source-workspace.md) note 曾把这次静默的 resume 记为一处无法解释的既有缺陷;而 overlay 的浅层替换正是其成因。
|
||||
|
||||
一个配置键无法安全地表达这些事实,因为部署方并非它们的权威。
|
||||
|
||||
## Decision
|
||||
|
||||
会话身份与退出行是由启动器持有的上下文槽位,在任何 Loader 条目挂载之前提供。二者都不出现在任何 `cordis.yml` 中,也不出现在任何插件的 `Config` 中。
|
||||
|
||||
这两个槽位与既有的 `tuiResumeHost` 宿主能力并列,后者确立了先例——resume 宿主一直是一项被提供的能力,而非配置。每个槽位都由消费它的包声明:
|
||||
|
||||
- `CONFIGURED_AGENT_IDENTITIES_KEY`(`dsh-agent-loop`)按所配置 agent 的 `id` 承载启动器身份,每项为一个 `LauncherAgentIdentity`(`{ id: SessionId, resume: boolean }`)。`agent-loop` 将匹配的身份覆盖到其所配置的 agent 上,替换两个身份键;并且仅当 `resume` 被置位时才走加载历史的 `resumeSessionId` 路径,因为该路径要求存在一份日志、否则会明确报错。槽位缺失则保留配置中的身份不变。`tui` 配置项通过自身的 `sessionId` 键解析同一个 id,因此前端入口渲染的正是被绑定的那个 agent。
|
||||
- `TUI_GOODBYE_MESSAGE_KEY`(`dsh-tui`)承载退出时终端释放后打印一次的完整行。缺失则什么都不打印。
|
||||
|
||||
身份归属于 `agent-loop`,因为它才是创建所配置 agent 的插件;也因为 patch 会整体替换配置项的 `config`:重新指向 agent 配置项模型路由的 overlay 会抹掉启动器设置的身份键。参见[共享 base overlay note](../simplification/2026-07-29-shared-base-config-overlays.md)。
|
||||
|
||||
`apps/cli` 铸造或选定 id,并依据它所复现的那次调用构建该行,与 `/resume` 的 execve 移交共用同一个 `resumeArgs` 助手,从而使打印出的命令与原地移交不会分歧。该行会在传入了 `--config` 时将其写入命令。恢复始终通过 `dsh --resume <id>` 重新进入默认界面;`dsh meta` 不接受任何默认界面选项,并且总是启动新会话。
|
||||
|
||||
**`ctx.provide` 是从启动器 argv 进入被 Loader 挂载的插件的唯一通道。** 配置的 `!!js` 表达式会以 `with (entry.ctx) { eval(expr) }`(`vendor/loader/src/config/utils.ts`)求值,因此一个裸标识符会针对该条目的上下文解析,别无它物可达。于是只要应用 bundle 仍从 YAML 挂载,这个槽位就无法被移除;变化之处在于它现在是启动器↔应用之间的内部管线,而不再是一个配置作者必须正确接线的、有文档记载的键。
|
||||
|
||||
该消息是一个纯字符串,而非回调。这迫使启动器在启动前就知道 id,也正是铸造从应用 bundle 中移出的原因——并且它让退出在终端释放之后免于任何被 await 的工作。
|
||||
|
||||
TUI 持有渲染,而非措辞:它在自己的 `palette.muted` 之前先应用 `displayText`,因此一个恶意的 `--config` 路径无法把终端转义序列注入退出行。做净化意味着启动器无法嵌入自己的 ANSI。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留这些键,并在应用组合包中加入内建默认值。** 拒绝:代码中的默认值能在 overlay 下存活,但表达同一事实的两种途径依然并存,而配置作者仍可把键设错——这正是那行陈旧的 `process.env.RESUME_SESSION_ID` 使 resume 失效的方式。
|
||||
|
||||
**把应用组合包合并进 `apps/cli` 并彻底删除该槽位。** 此处拒绝,但后来以本 note 未曾设想的形式[被采纳](../simplification/2026-07-29-shared-base-config-overlays.md):组合被搬进平铺的配置文件(`apps/cli/config/base.cordis.yml` 加各 surface 一份 overlay),而非搬进 CLI 代码,因此从未进入 `v8 ignore` 进程接线块,overlay 的扩展点也作为普通配置项 patch 保留了下来。槽位本身并未被删除——它迁移到了 `dsh-agent-loop`,因为启动器的事实依然不能经由一个可被整体替换的配置键传递。
|
||||
|
||||
**把 goodbye 消息放到 `TuiResumeHost` 上。** 拒绝:退出行不是一项移交能力,而一个无法替换自身进程的宿主仍可能想要打印一行。它们是相互独立的槽位。
|
||||
|
||||
**让宿主只提供命令文本,而由 TUI 保留 `To resume this session:` 前缀。** 拒绝:TUI 将为一个它已不再理解的字符串保留 resume 词汇,而 meta 模式证明启动器才是唯一知道该命令应当说什么的组件。
|
||||
|
||||
**让 TUI 继续在会话被持久化之前抑制该行。** 拒绝:这项检查正是退出路径要查询持久化并吞掉列举失败的原因。一个纯字符串无法查询持久化,而误用现在会经由 `agent-loop/config-start-failed` 明确报错,而不是静默地恢复了个空。
|
||||
|
||||
**用一个回调(`goodbyeMessage(agent)`)让宿主能在退出时决定。** 拒绝:它会在 `ui.stop()` 之后恢复异步工作,为一个在启动时就已可知的字符串,重新引入拆解期间的挂起风险。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 移除两个已发布的 `Config` 键是一次破坏性配置变更:一份命名了任一键的陈旧配置,现在会在启动时的 schema 校验中明确报错,而不再静默降级。这是有意为之,且在预发布阶段可以接受。
|
||||
- `TuiResumeHost` 保持不变,但 `TuiRuntime` 新增 `goodbyeMessage`;`apps/cli` 是唯一的提供方。
|
||||
- 即便某会话没有日志(启动后立即退出),退出行也会打印。此时使用它会明确报错,而不是开启一个意外的会话。这是丢弃持久化检查的有意代价。
|
||||
- `dsh-tui` 完全不再读取 `sessionPersistence`:`currentResumeCommand`、`listWorkspaceSessions` 及其吞错路径都被删除,`/resume` 选择器的 `sessionQuery` 读取如今是 TUI 中唯一的会话发现途径。
|
||||
- 启动器为其自身的应用铸造会话 id,因此一个不提供任何槽位的非 CLI 宿主,仍保留 bundle 自带的铸造逻辑。
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/ui/tui/tests/tui.spec.ts` 钉住打印出的行、槽位缺失时的静默,以及对恶意消息的转义净化;此前那两个退出抑制测试被替换,因为抑制正是本次改动移除的行为。`packages/core/agent-loop/tests/` 为 resume、启动器铸造与无槽位三种情形驱动身份槽位。
|
||||
|
||||
承重的覆盖是 `apps/cli/tests/tui-keyless-smoke.e2e.ts`,它在一个 PTY 中拉起真实的 `apps/cli/src/bin.ts`:一个测试断言退出行携带 `--config`,一个回归测试植入一份个人 `config.yaml` 来替换整块 `agent-loop` 配置块并断言该行仍会打印——把「overlay 不能丢掉 resume」编码为一条被执行的契约,而非一句注释。
|
||||
|
||||
在 tmux 中针对真实的个人 overlay 做过实测:该缺陷在未修改的 staging 上复现(所请求的 id 被忽略,banner 里是新的 id),而在本分支上同一份 overlay 会产出一行打印的退出行、一个能恢复上一轮次的 `--resume`,以及一个把该会话标记为 `current · live · persisted` 的 `/resume` 选择器。错误的 id 现在会明确报错。
|
||||
Reference in New Issue
Block a user