Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.zh.md
#	THIRD_PARTY_NOTICES.md
#	apps/cli/composition.md
#	apps/cli/config/base.cordis.yml
#	apps/cli/package.json
#	apps/cli/src/app-cli-entry.ts
#	apps/cli/src/bin.ts
#	apps/cli/tests/args.spec.ts
#	apps/web/tests/built-boot.snapshot.ts
#	apps/web/tests/navigation-panes.e2e.ts
#	docs/architecture.i18n.yaml
#	docs/architecture.md
#	docs/architecture.zh.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/README.i18n.yaml
#	packages/bundle/README.i18n.yaml
#	packages/client/connection/README.i18n.yaml
#	packages/client/connection/README.md
#	packages/client/connection/README.zh.md
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/src/http-bridge.ts
#	packages/client/connection/src/index.ts
#	packages/client/connection/tests/fixture.spec.ts
#	packages/client/connection/tests/node-half.spec.ts
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/README.md
#	packages/client/runtime/README.zh.md
#	packages/client/runtime/src/client/contract/session.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
#	packages/client/ui-conversation/src/client/chat/ChatView.tsx
#	packages/client/ui-conversation/src/client/chat/MessageItem.module.css
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-conversation/src/client/index.ts
#	packages/client/ui-conversation/src/client/input/contract.ts
#	packages/client/ui-conversation/src/client/input/facade.ts
#	packages/client/ui-conversation/src/client/input/hub.ts
#	packages/client/ui-conversation/src/client/locales.ts
#	packages/client/ui-conversation/src/client/service.ts
#	packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx
#	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
#	packages/client/ui-conversation/tests/apply-inject.spec.tsx
#	packages/client/ui-conversation/tests/input-bar.spec.tsx
#	packages/client/ui-conversation/tests/input-matrix.spec.tsx
#	packages/client/ui-conversation/tests/input-scenarios.spec.tsx
#	packages/client/ui-conversation/tests/service-orchestration.spec.ts
#	packages/client/ui-conversation/tests/skeleton.spec.tsx
#	packages/client/ui-trajectory/tests/views.spec.tsx
#	packages/compact/compact-basic/README.i18n.yaml
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.md
#	packages/host/apiproxy/README.zh.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/rpc.ts
#	packages/host/apiproxy/src/api/sessions.ts
#	packages/host/apiproxy/src/index.ts
#	packages/host/apiproxy/tests/api-proxy-models.spec.ts
#	packages/host/apiproxy/tests/rpc-schemas.spec.ts
#	packages/llm/llm-pi-ai/README.i18n.yaml
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/README.zh.md
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm/README.i18n.yaml
#	packages/ui/tui/README.md
#	packages/ui/tui/README.zh.md
#	packages/ui/tui/src/components/content.ts
#	packages/ui/tui/src/components/transcript.ts
#	packages/ui/tui/tests/tui.spec.ts
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-08-08 01:56:29 +08:00
3973 changed files with 144864 additions and 60887 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/README.md
README.md: 3cfbb5154713046846a3bfcb2ccea62c0e4cb6c0
README.zh.md: a3369a94c6761e27567b1408d98a81665443f2ef
README.zh.md: 61887320e9e54b35154c79e33e9a9525aceeef50
+2 -2
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
这里存放一类设计文档。**Agent Noteagent 决策记录)** 记录塑造本代码库的决策或提案:代码和文档无法承载的*为什么*以及*放弃了什么*。本文件是入口和契约:Agent Note 存放在哪里、何时需要写一份,以及[文件内格式](#the-file-format)。
这里存放一类设计文档。**Agent Note** 记录塑造本代码库的决策或提案:代码和文档无法承载的*为什么*以及*放弃了什么*。本文件是入口和契约:Agent Note 存放在哪里、何时需要写一份,以及[文件内格式](#the-file-format)。
## 布局与命名
@@ -10,7 +10,7 @@
- **生命周期**(顶层文件夹)是 Agent Note 的状态,Agent Note 随状态变化在文件夹之间移动:
- **`proposed/`**:实施前评审的提案;尚未构建(或仅部分构建)。
- **`implemented/`**:决策已交付。文件记录做了什么决定、否决了什么,并**与实际交付的内容保持同步**:当代码后续移动文件、重命名包package或更改键名/默认值时,Agent Note 在同一个变更中同步更新(仅限事实——路径、名称、结构——而非决策本身)。见 [implemented/AGENTS.md](implemented/AGENTS.md)。
- **`implemented/`**:决策已交付。文件记录做了什么决定、否决了什么,并**与实际交付的内容保持同步**:当代码后续移动文件、重命名包或更改键名/默认值时,Agent Note 在同一个变更中同步更新(仅限事实——路径、名称、结构——而非决策本身)。见 [implemented/AGENTS.md](implemented/AGENTS.md)。
- **`rejected/`**:提案经过讨论后被否决。仅当其决策依据仍能避免一种诱人且影响重大的错误时保留;否则删除完整的英文、中文和伴随记录三文件组。
- **类别**(嵌套文件夹)是决策的*种类*——见下方[分类](#classification)。
@@ -0,0 +1,6 @@
# 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/archived/architecture/2026-07-22-tui-interactive-extension-service.md
2026-07-22-tui-interactive-extension-service.md: e523361ceb920d2848369d7d3cf1093765d296d4
2026-07-22-tui-interactive-extension-service.zh.md: 23c46f6bc18d576912aaa648b26877873ea60eda
@@ -1,6 +1,7 @@
# Agent Note: Effect-owned TUI interactive extensions
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-22-tui-interactive-extension-service.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: 由 effect 持有的 TUI 交互扩展
Status: implemented
Archived: 2026-08-04
[English](2026-07-22-tui-interactive-extension-service.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/architecture/2026-07-27-tui-chat-channel-module-split.md
2026-07-27-tui-chat-channel-module-split.md: 3cf9548d4d9d1b845bee5f7137d79a490827b082
2026-07-27-tui-chat-channel-module-split.zh.md: d5de02bd0f7090c24ccfc5dabbbadff9ff42174e
@@ -1,6 +1,7 @@
# Agent Note: dsh-tui chat channel module split
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-27-tui-chat-channel-module-split.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: dsh-tui 聊天通道模块拆分
Status: implemented
Archived: 2026-08-04
[English](2026-07-27-tui-chat-channel-module-split.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/architecture/2026-07-28-consolidated-tui-presentation.md
2026-07-28-consolidated-tui-presentation.md: c772985a11bc61c2bfcbb4422cae219966246163
2026-07-28-consolidated-tui-presentation.zh.md: c96fd105bc4ff6632017b16a8b7a0e8f448449bd
@@ -1,6 +1,7 @@
# Agent Note: Consolidated TUI presentation and navigation
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-28-consolidated-tui-presentation.zh.md)
@@ -26,7 +27,7 @@ A tool card has one colored `Tool / <name>` status header over one dim body. Pre
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.
`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
@@ -1,6 +1,7 @@
# Agent Note: 统一的 TUI 呈现与导航
Status: implemented
Archived: 2026-08-04
[English](2026-07-28-consolidated-tui-presentation.md) | 中文
@@ -26,7 +27,7 @@ Status: implemented
注入上下文由 `ContextCardComponent` 按普通文本呈现,不经过 XML 树渲染器。仅移除精确配对的外层 `<system-reminder>` 行;不匹配、单边或正文内类似标签的文本都原样保留。面向模型的内容不变。折叠在正文组装完成后使用共享 `preview` 辅助函数,因此只取决于行数,不依赖解析是否成功或载荷包含哪些字符。
`Ctrl+O` 在折叠、展开和隐藏之间循环。隐藏状态会连同卡片自有的前导间距一起移除工具卡片。上下文卡片参与折叠和展开状态,但工具隐藏时回到折叠状态,因为注入指令不是可丢弃的工具流量。
`Ctrl+O` 在折叠、展开和隐藏之间循环。隐藏状态会连同卡片自有的前导间距一起移除工具卡片。上下文卡片参与折叠和展开状态,但工具隐藏时回到折叠状态,因为注入指令不是可丢弃的工具流量。隐藏阶段还会把每个轮次的 assistant 步骤折叠为一条消息;该规则由[隐藏模式 assistant 折叠 Agent Note](../feature/2026-07-29-tui-hidden-mode-assistant-fold.md)负责。
### 跨工作区恢复
@@ -0,0 +1,6 @@
# 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/archived/architecture/2026-07-28-dsh-native-typescript-source-launch.md
2026-07-28-dsh-native-typescript-source-launch.md: b5e8ed2a18bb0cbb3ab54cf5ed4a427efaa9dfeb
2026-07-28-dsh-native-typescript-source-launch.zh.md: 05a4f92d2d3be553bb9ca363bf08e3ebf8e3f5b8
@@ -1,6 +1,7 @@
# Agent Note: Native TypeScript source launch for dsh
Status: implemented
Archived: 2026-08-07
English | [中文](2026-07-28-dsh-native-typescript-source-launch.zh.md)
@@ -1,30 +1,31 @@
# Agent Note: dsh 原生 TypeScript 源码启动
Status: implemented
Archived: 2026-08-07
[English](2026-07-28-dsh-native-typescript-source-launch.md) | 中文
> Node 原生启动向量已被 [dsh 通过 tsx ESM hook 源码启动](2026-07-29-dsh-source-launch-tsx-esm.md) 取代:Node 26.0.0 移除了 `--experimental-transform-types`,本文描述的 paths loader 已删除。Cordis 配置声明门禁(`verify-cordis-config`)、app-boot 的 fail-loud 插件诊断以及 vendor 中的 `import type` 标注仍然有效。
> Node 原生启动方案已被 [dsh 通过 tsx ESM hook 源码启动](2026-07-29-dsh-source-launch-tsx-esm.md) 取代:Node 26.0.0 移除了 `--experimental-transform-types`,本文描述的 paths loader 已删除。Cordis 配置声明门禁(`verify-cordis-config`)、app-boot 的显式失败插件诊断以及 vendor 中的 `import type` 标注仍然有效。
## 问题
`dsh` 源码入口原本使用 `tsx` 运行 `apps/cli/src/bin.ts`TypeScript 转换和根 tsconfig 的 `paths` 解析都由同一个第三方 loader 隐式处理。改由 Node 原生处理 TypeScript 后,Node 不会应用 tsconfig 路径映射;如果改为通过包导出解析,源码启动会混入可能陈旧或不存在的 `lib/` 产物。
Node 的转换也不执行类型分析。通过普通值 import 导入的类型会保留为运行时 ESM 请求,而 TypeScript 的 `export =`成 CommonJS 赋值,而不是 ESM default export。因此,源码图必须显式使用仅类型导入和原生 ESM 导出;resolve hook 无法修复不兼容的源码语法。
Node 的转换也不执行类型分析。通过普通值 import 导入的类型会保留为运行时 ESM 请求,而 TypeScript 的 `export =`转换成 CommonJS 赋值,而不是 ESM default export。因此,源码图必须显式使用仅类型导入和原生 ESM 导出;resolve hook 无法修复不兼容的源码语法。
Cordis 配置还引入了另一条解析边界。`cordis.yml` 中的 bare plugin 不经过 TypeScript import 分析,其解析方 manifest 可能漏掉所需依赖。Cordis Loader 会记录插件 import 错误,并留下没有 fiber 的 entry,但不会让启动本身失败;配置中的拼写错误因此可能得到退出码为 0 的残缺应用。
Cordis 配置还引入了另一条解析边界。`cordis.yml` 中的 bare plugin 不经过 TypeScript import 分析,其解析方 manifest(元数据清单)可能漏掉所需依赖。Cordis Loader 会记录插件 import 错误,并留下没有 fiber 的 entry,但不会让启动本身失败;配置中的拼写错误因此可能得到退出码为 0 的残缺应用。
## 决策
`dsh` 的 TUI、Web 和无头源码启动使用 `node --experimental-transform-types`,由 Node 完成 TypeScript 转换,不加载 `tsx` 或 esbuild。`bin/dsh`、根级 `dsh`/TUI/Web demo 以及 Code Mode TUI 都进入同一条 `apps/cli/src/bin.ts` 启动链路。测试与 e2e 启动器保留各自现有策略,构建后的 `lib/bin.js` 继续由普通 Node 运行。
`scripts/tspath-loader.ts` 只注册一个模块 resolve hook。设置 `TSX_TSCONFIG_PATH` 时,它会使用该路径(相对路径从调用方的 cwd 解析),否则读取根 `tsconfig.json``TsconfigPathsResolver` 使用仓库已有的 TypeScript 开发工具沿该配置的 `extends` 链解析,按 tsconfig 规则选择精确或 wildcard `paths` 条目,并将命中的 workspace bare specifier 映射到 `.ts`/`.mts`/`.cts` 源文件或目录 index 文件。代码转换始终只由 Node 负责。该源码专用 loader 不属于构建后的 CLI,`apps/cli` 也不会把 `typescript` 声明为运行时依赖。
`scripts/tspath-loader.ts` 只注册一个模块解析钩子。设置 `TSX_TSCONFIG_PATH` 时,它会使用该路径(相对路径从调用方的 cwd 解析),否则读取根 `tsconfig.json``TsconfigPathsResolver` 使用仓库已有的 TypeScript 开发工具沿该配置的 `extends` 链解析,按 tsconfig 规则选择精确或 wildcard `paths` 条目,并将命中的 workspace bare specifier 映射到 `.ts`/`.mts`/`.cts` 源文件或目录 index 文件。代码转换始终只由 Node 负责。该源码专用 loader 不属于构建后的 CLI,`apps/cli` 也不会把 `typescript` 声明为运行时依赖。
只有当目标包是最近 package manifest 的自身名称或已声明的运行时依赖时,源码 import 才会重定向。Cordis Loader 使用配置目录 URL 作为 import parent;此时 resolver 会向上查找声明该插件的 workspace manifest。因此,已交付的 `apps/cli/config/base.cordis.yml` 及其界面覆盖层所需依赖由 `apps/cli/package.json` 持有。未命中 tsconfig paths、引用未声明依赖或不是 bare specifier 的说明符全部交回 Node 默认解析。
只有当目标包是最近一层包 manifest 的自身名称或该 manifest 已声明的运行时依赖时,源码 import 才会重定向。Cordis Loader 使用配置目录 URL 作为 import parent;此时 resolver 会向上查找声明该插件的 workspace manifest。因此,已交付的 `apps/cli/config/base.cordis.yml` 及其界面覆盖层所需依赖由 `apps/cli/package.json` 持有。未命中 tsconfig paths、引用未声明依赖或不是 bare specifier 的说明符全部交回 Node 默认解析。
`verify-cordis-config` 对该解析方 manifest 执行单向完整性检查:配置中的每个 bare plugin package 都必须出现在对应 manifest 的 `dependencies` 中,manifest 可以包含该配置未引用的额外依赖。根 `AGENTS.md` 将同步更新配置和依赖定为常驻规则。
Loader 完成结算后,共享的 `dsh-app-boot` 会检查每个已启用但没有 fiber 的 entry,并 `plugin(s) failed to load: ...; Cordis startup failed because these plugin(s) could not be resolved` 拒绝启动,同时列出全部加载失败的插件。该诊断位于应用层,不改变 vendor 中 Loader 的启动行为。
Loader 完全停稳后,共享的 `dsh-app-boot` 会检查每个已启用但没有 fiber 的 entry,并拒绝启动,报错为 `plugin(s) failed to load: ...; Cordis startup failed because these plugin(s) could not be resolved`,同时列出全部加载失败的插件。该诊断位于应用层,不改变 vendor 中 Loader 的启动行为。
Node-compatible TypeScript 是这项源码启动契约的一部分。vendor 中的 Cordis、Loader、Include、HMR(热模块替换)和 Schemastery 使用 `import type` 标记会被擦除的导入。Schemastery 使用原生 ESM default export 并声明 `type: module`;其 `.mjs``.cjs` 构建产物分别保留现有的 ESM default export 行为和 `require()` 返回可调用值的行为。这些差异记录在 `vendor/README.md` 中;没有为 vendor 中的框架新增运行时行为。
@@ -32,7 +33,7 @@ Node-compatible TypeScript 是这项源码启动契约的一部分。vendor 中
**继续使用 `tsx`。** 不采用,因为 `tsx`/esbuild 会继续负责 TypeScript 转换,本启动链路无法因此证明 Node 原生转换可用。
**让源码入口通过包导出加载构建后的 `lib/`。** 不采用,因为这会混合 source plane 与 artifact plane构建开发启动可能读取陈旧产物或直接失败。
**让源码入口通过包导出加载构建后的 `lib/`。** 不采用,因为这会混合 source plane 与 artifact plane无需预先构建开发启动可能读取陈旧产物或直接失败。
**无条件应用根 tsconfig `paths`。** 不采用,因为这会让未声明的跨包 import 和 Cordis 插件继续成功解析,从而掩盖 manifest 与实际运行图之间的不一致。
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-23-tui-generic-card-markdown.md
2026-07-23-tui-generic-card-markdown.md: 5c092c4d5ffbc566074f9b280d3fbc207d645ec7
2026-07-23-tui-generic-card-markdown.zh.md: 93e6388c64e69d9028822ce65f9eda368ed26e7d
@@ -1,6 +1,7 @@
# Agent Note: TUI generic-card Markdown rendering
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-23-tui-generic-card-markdown.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: TUI 通用卡片的 Markdown 渲染
Status: implemented
Archived: 2026-08-04
[English](2026-07-23-tui-generic-card-markdown.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-24-tui-turn-end-stop-reason-notices.md
2026-07-24-tui-turn-end-stop-reason-notices.md: 62784ed7cd085ada49b2e0e3d9d809c33a2af917
2026-07-24-tui-turn-end-stop-reason-notices.zh.md: 162ba4ed44c98bfe00153b7acf69424784f92f5e
@@ -1,6 +1,7 @@
# Agent Note: TUI presents a reason for every turn-end kind
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-24-tui-turn-end-stop-reason-notices.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: TUI 为每种轮次结束 kind 呈现原因
Status: implemented
Archived: 2026-08-04
[English](2026-07-24-tui-turn-end-stop-reason-notices.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-27-question-composer-rows-do-not-shrink.md
2026-07-27-question-composer-rows-do-not-shrink.md: 47e581a23caaeb9368b075fa84a01d2bc945ab46
2026-07-27-question-composer-rows-do-not-shrink.zh.md: a1b72cab41c51c268540dcec60e85a0edf2fe9bc
@@ -1,6 +1,7 @@
# Agent Note: Question-composer option rows are scroll content, not the slack absorber
Status: implemented
Archived: 2026-08-07
English | [中文](2026-07-27-question-composer-rows-do-not-shrink.zh.md)
@@ -1,12 +1,13 @@
# Agent Note: 提问 composer 的选项行是滚动内容,而非空间不足时的吸收方
Status: implemented
Archived: 2026-08-07
[English](2026-07-27-question-composer-rows-do-not-shrink.md) | 中文
## 问题
提问 composer 的卡片会按视口设上限(`max-height: min(60vh, 520px)`),并让选项列表自行滚动,这样在成批提问时标题和底部操作按钮始终可达。但当 composer 的容器变矮时(窗口较小,或视口偏矮且详情面板处于展开状态),选项行会互相叠在一起,也会叠到问题标题上。
提问 composer 的卡片会按视口设上限(`max-height: min(60vh, 520px)`),并让选项列表自行滚动,这样在成批提问时头部和底部操作按钮始终可达。但当 composer 的可用区域变矮时(窗口较小,或视口偏矮且详情面板处于展开状态),选项行会互相叠在一起,也会叠到问题标题上。
缺陷不在这个高度上限,而在高度不足时由谁来吸收。`.options` 是一个 `flex-direction: column` 的盒子,其子元素默认取 `flex-shrink: 1`,因此空间不足时首先被压缩的是各个选项行,而不是让滚动容器产生溢出。一行会被压到它的 `min-height: 42px`,而 `.optionCopy` 仍保持文案折行后所需的更大固有高度(带描述的选项会占两行)。由于 `align-items: center`,文案于是以一个比自身更矮的盒子为基准居中,并向上下两个方向画到该行边框盒之外——向上盖住标题,向下盖住下一行。在实际发布的客户端上于 900x440 处实测:文案有 6.5px 落在行盒之外,视口高度降到 380px 时增至 10px,而 `.options` 报告的 `scrollHeight` 等于 `clientHeight`,因此始终不会给出滚动条。
@@ -26,25 +27,25 @@ Status: implemented
**把 `align-items: center` 改为 `align-items: flex-start`。** 文案就只会向下生长,因此不再向上盖住标题。但这什么也没修好:被压缩的行依然会溢出到下一行上,而且这一改动会在所有尺寸下(包括常见尺寸)无声改变每个选项行的垂直对齐。
**移除卡片的 `max-height` 上限,使其永远不会被压缩。** 没有高度不足,就没有分配问题。之所以否决:正是这个上限保证成批提问时标题和底部操作按钮留在屏幕内;移除它会重新引入该上限本就为之存在的失败(composer 所处的容器是一个固定高度、`overflow: hidden` 的会话列,因此不设上限的卡片会连自己的提交按钮一起丢掉)。
**移除卡片的 `max-height` 上限,使其永远不会被压缩。** 没有高度不足,就没有分配问题。之所以否决:正是这个上限保证成批提问时头部和底部操作按钮留在屏幕内;移除它会重新引入该上限本就为之存在的失败(composer 所处的容器是一个固定高度、`overflow: hidden` 的会话列,因此不设上限的卡片会连自己的提交按钮一起丢掉)。
**把折行文案限制为单行(对 `.description``white-space: nowrap` 加省略号)。** 行永远不会折行,因此被压缩时也永远不会溢出。否决理由与裁剪相同,此外它还为了修一个窄视口缺陷,而牺牲了空间充裕的宽视口渲染效果。
## 后果
- 被压缩的 composer 会滚动其选项列表,而不是让互相重叠:在 900x380 处,该列表报告 `scrollHeight` 为 200、`clientHeight` 为 114,并给出滚动条;此前两者相等,不给滚动条。
- 被压缩的 composer 会滚动其选项列表,而不是让选项行互相重叠:在 900x380 处,该列表报告 `scrollHeight` 为 200、`clientHeight` 为 114,并给出滚动条;此前两者相等,不给滚动条。
- 选项行在任何视口尺寸下都保留完整的折行文案。不裁剪、不加省略号,宽视口下的渲染保持不变(该规则仅在 flex 盒子空间不足时才生效)。
- 由于高度不足不再被行部分吸收,卡片现在更早进入滚动状态。这正是该高度上限想要的行为,也意味着在此前只会无声画错列表的情形下,矮容器现在会显示滚动条。
- 由于高度不足不再被行部分吸收,卡片现在更早进入滚动状态。这正是该高度上限想要的行为,也意味着在此前只会无声画错列表的情形下,较矮的可用区域现在会显示滚动条。
- 该场景录制的问题,比它主要测试的那次往返所需的长度更长。这个代价是有意付出的:没有折行文案,该布局不变式无法被证伪,而为一条 CSS 规则再加一份 fixture 会更糟。
## 验证
Web e2e 的 composer 场景会在三个被压缩的容器高度(900x520440380上,对活动的 composer 断言该运行时不变式:每个选项行的子元素都留在该行的边框盒之内。两道守卫防止该断言空洞地成立——必须至少有一行处于折行状态(这是唯一会溢出的形态),且 `.options` 必须确实处在滚动状态(证明容器确实触及了高度上限)。该场景录制的问题现在带有较长的选项描述,正是为此;没有折行文案,这条断言不可能失败。
Web e2e 的 composer 场景会在三个受挤压的可用区域高度900x520440380,在实际运行的 composer 断言该不变式:每个选项行的子元素都留在该行的边框盒之内。两道守卫防止该断言空洞地成立——必须至少有一行处于折行状态(这是唯一会溢出的形态),且 `.options` 必须确实处在滚动状态(证明可用区域确实受到了高度上限约束)。该场景录制的问题现在带有较长的选项描述,正是为此;没有折行文案,这条断言不可能失败。
在构建产物客户端上双向确认过:撤销 `flex-shrink: 0` 后该场景失败(`scrolls: false`,6.5px 溢出),恢复后通过。一次覆盖 340 种视口尺寸(420-1600 x 320-960)的独立几何遍历,从 86 种尺寸存在文案落在行盒之外,降到 0 种。
该断言仅在回放模式下执行:录制模式必须走到写入 fixture 那一步,而不是在布局检查处中断。另需注意,composer 以客户端模包的形式发布,因此单跑 `pnpm run build:web` 不会带上对 `QuestionComposer.module.css` 的改动——必须执行包构建,浏览器测试通道才能看到它。
该断言仅在回放模式下执行:录制模式必须走到写入 fixture 那一步,而不是在布局检查处中断。另需注意,composer 以客户端模包的形式发布,因此单跑 `pnpm run build:web` 不会带上对 `QuestionComposer.module.css` 的改动——必须执行包构建,浏览器测试通道才能看到它。
要复现这种空间不足,需要的是矮视口,而不是矮容器。高度上限为 `min(60vh, 520px)`,因此把会话列压到比卡片自身高度更矮,只会裁剪卡片,而不会让它空间不足——各行仍保持完整高度,也不会有任何溢出。凡是在 e2e 场景之外演示或测量该缺陷的手段,都必须改变视口。
`lib/` 陈旧会让浏览器测试通道对着一个比工作树更旧的客户端做断言,而中途失败的 `pnpm run build` 留下的正是这种状态:失败之前构建的那些包是新的,其余不是。在这种状态下刷新 golden,记录下来的是旧客户端的界面。抓取之前先确认构建以 0 退出;另需注意 `packages/` 下的未跟踪目录同样会被编译——来自另一个分支的遗留物可能以 diff 无法解释的原因让构建失败。
`lib/` 陈旧会让浏览器测试通道对着一个比工作树更旧的客户端做断言,而中途失败的 `pnpm run build` 留下的正是这种状态:失败之前构建的那些包是新的,其余不是。在这种状态下刷新预期输出,记录下来的是旧客户端的界面。抓取之前先确认构建以 0 退出;另需注意 `packages/` 下的未跟踪目录同样会被编译——来自另一个分支的遗留物可能以 diff 无法解释的原因让构建失败。
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-27-tool-card-single-row-fields-inline.md
2026-07-27-tool-card-single-row-fields-inline.md: 23989e08ac7e32097212bf951461ca1d4213f729
2026-07-27-tool-card-single-row-fields-inline.zh.md: 8358f7b8dff67c0d3f459d08bf37e3a47f276107
@@ -1,6 +1,7 @@
# Agent Note: Tool-card single-row fields render inline
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-27-tool-card-single-row-fields-inline.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: 工具卡片的单行字段以内联方式渲染
Status: implemented
Archived: 2026-08-04
[English](2026-07-27-tool-card-single-row-fields-inline.md) | 中文
@@ -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/bug-fix/2026-07-27-tui-diff-card-redundant-path-header.md
2026-07-27-tui-diff-card-redundant-path-header.md: 708e543ff079828b4929d2a50ac697a9c846608a
2026-07-27-tui-diff-card-redundant-path-header.zh.md: 863868ae707f37689bbc202267c5470d8c3163e9
2026-07-27-tui-diff-card-redundant-path-header.md: 608a11892a20d020087180175eff847021dc0554
2026-07-27-tui-diff-card-redundant-path-header.zh.md: bf7f1c1eeb994f9940b5f7dfb7db72d422293bd4
@@ -1,6 +1,7 @@
# Agent Note: TUI diff card dropped the duplicated file path
Status: implemented
Archived: 2026-07-31
English | [中文](2026-07-27-tui-diff-card-redundant-path-header.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: TUI diff 卡片重复打印文件路径
Status: implemented
Archived: 2026-07-31
[English](2026-07-27-tui-diff-card-redundant-path-header.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-27-tui-step-timing-trails-tool-cards.md
2026-07-27-tui-step-timing-trails-tool-cards.md: 00256fc0f1c0e9469d0dd4e8567fdbc4a48dcaff
2026-07-27-tui-step-timing-trails-tool-cards.zh.md: a566b521923fecb47c721617ac1b4a5fffce7a0f
@@ -1,6 +1,7 @@
# Agent Note: TUI step timing trails the step's last message
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-27-tui-step-timing-trails-tool-cards.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: TUI 步骤计时跟在该步骤最后一条消息之后
Status: implemented
Archived: 2026-08-04
[English](2026-07-27-tui-step-timing-trails-tool-cards.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-28-web-conversation-polish-sweep.md
2026-07-28-web-conversation-polish-sweep.md: 338b687c041c585c1d490fdad9b8bbcf88fc3912
2026-07-28-web-conversation-polish-sweep.zh.md: 166662070b67837b9f4755e6457578f74af771ed
@@ -1,6 +1,7 @@
# Agent Note: Web conversation UI polish sweep
Status: implemented
Archived: 2026-08-07
English | [中文](2026-07-28-web-conversation-polish-sweep.zh.md)
@@ -1,37 +1,38 @@
# Agent Note: Web conversation UI polish sweep
# Agent Note: Web 对话 UI 视觉优化
Status: implemented
Archived: 2026-08-07
[English](2026-07-28-web-conversation-polish-sweep.md) | 中文
## Problem
## 问题
一次针对 web GUI 对话界面的设计评审发现了一批视觉呈现缺陷:portal 菜单在重新定位前会先在错误位置绘制一帧(打开时可见跳动);只要某条步骤消息只携带工具调用头,聊天列就会把一次工具运行拆成好几组;工具行摘要打印以工作区为根的绝对路径,占掉行内大部分空间;运行中行的扫光效果用 alpha 遮罩实现,把整行都压暗;hero 区的工作区 chip 会从会话 cwd 里复现已删除工作区的文件夹名;标题栏还在 13px 的标题旁显示一个没人需要的轮次计数。
## Decision
## 决策
本次修复全部为纯展示侧改动落地;不会有任何内容进入会话日志。
本次修复仅改动展示层;不会有任何内容进入会话日志。
- **Portal 菜单先隐藏预渲染,绘制前完成测量。**菜单列表以 `visibility: hidden` 挂载在 (0,0),在 `useLayoutEffect` 中测量,显示时已处于最终位置。菜单与视口保持 12px 间距并支持内部滚动;工作区创建操作固定在不滚动的页脚区。
- **聊天流跳过不渲染任何内容的助手节点。**已定稿的助手节点若其块仅含工具调用头和空白的文本/推理(reasoning)内容,就会从流推导中剔除,于是连续的工具结果合并为一组。被中断的节点始终渲染(它们携带「已停止」标记)。
- **工具行摘要把以工作区为根的路径转为相对路径。**会话 cwd 经由 toolview 插槽契约(`ToolRowOwnerProps.cwd`)逐层传递,`toolRowModel` 从以其开头的摘要中剥去该前缀;工作区之外的路径保持原样。这只影响显示:工具参数与日志均不受影响。
- **运行中的扫光效果改为高光带叠加层。**一条固定宽度的 `::after` 渐变光带横向扫过整行(即 deepsuite 的 ShimmerText 模式),取代先前的 `mask-image` 方案,ToolRow 与 Bash toolview 两处均已替换。
- **hero 区的工作区 chip 是选择器,而非回显。**没有有效选中项时(冷启动,或列表稳定后工作区被删除),它显示「Choose workspace」占位文案;由 cwd 推导的名称只用于衔接列表的首次加载,待定选择对应的工作区从已就绪的列表中消失时,该陈旧选择会被清除。
- **hero 区的工作区 chip 是选择器,而非回显。**没有有效选中项时(冷启动,或列表稳定后工作区被删除),它显示「Choose workspace」占位文案;由 cwd 推导的名称只用于衔接列表的首次加载,尚待确认的选择对应的工作区从已就绪的列表中消失时,该陈旧选择会被清除。
- **统一为 16px 的纵向节奏。**聊天列间距与分组内工具行间距统一为 16px,取代原先「分组内 10px 间距加跨分组负外边距」的做法。
- **标题栏标题改为 14/20,去掉轮次计数**;StateDot 的进行中状态与轮次尾部采用逐格推进的像素追逐式加载视觉语言;`body` 启用灰度抗锯齿(`-webkit-font-smoothing` 及 Firefox 在 macOS 上的等价设置)。
- **标题文字改为 14/20标题栏去掉轮次计数**;StateDot 的进行中状态与轮次尾部采用逐格推进的像素追逐式加载视觉语言;`body` 启用灰度抗锯齿(`-webkit-font-smoothing` 及 Firefox 在 macOS 上的等价设置)。
## Alternatives considered
## 考虑过的替代方案
- **挂载前根据锚点矩形同步定位菜单。**不予采纳:列表自身尺寸在布局完成前无从得知,向视口内收拢仍然需要布局后测量;对已挂载的隐藏节点做测量正是 React 与 Floating UI 文档记载的模式。
- **在宿主侧过滤空的助手消息。**不予采纳:该节点是真实的模型输出,Trajectory 与回放都必须保留它;只有聊天展示应当跳过它,且按契约 web 层只负责呈现。
- **在每个工具各自的展示器中做路径相对化。**不予采纳:这种冗余是所有输出路径摘要的工具共有的;在 `toolRowModel` 里做一次仅影响显示的处理即可覆盖全部工具,非聊天消费方仍拿到绝对路径。
- **在每个工具各自的 presenter 中做路径相对化。**不予采纳:这种冗余是所有输出路径摘要的工具共有的;在 `toolRowModel` 里做一次仅影响显示的处理即可覆盖全部工具,非聊天消费方仍拿到绝对路径。
- **保留基于遮罩的扫光。**不予采纳:遮罩会把包括状态圆点在内的整行内容压暗,其退出过渡还与悬停图标的交叉淡入淡出相互冲突;叠加光带在内容之上合成,完全不触碰内容的 alpha。
- **让 chip 继续显示已删除工作区的名称。**不予采纳:chip 是为*下一个*会话服务的选择器;用户刚删掉某个工作区,还回显它的 cwd,就是在错误呈现当前的选择。
## Consequences
## 后果
聊天渲染出的流条目数少于快照中的节点数:凡是拿渲染出的块与节点数对账的人,都必须把被跳过的「不渲染任何内容」的助手节点计算在内(chat-view 规格测试固定了这一点)。路径相对化只是针对会话 cwd 的前缀检查,因此会话中途重命名工作区后,摘要在重新推导前会显示绝对路径,这被接受为仅影响显示的陈旧状态。统一的 16px 节奏淘汰了原先更紧凑的 10px 工具运行外观;将来若要更紧凑的布局,应当有意识地重新引入第二个常量。菜单预渲染让每次打开多一次隐藏布局计算,在菜单的尺寸量级下开销可忽略。
## Testing
## 测试
`chat-view.spec.tsx` 定了「不渲染任何内容」节点的分组行为(含被中断节点这一例外);`chat-tool-row.spec.tsx` 定了工作区内、工作区外以及 cwd 为空时的 cwd 相对化行为;`atoms.spec.tsx``workspace-picker.spec.tsx` 覆盖菜单与 chip 的各种状态;ui-conversation、ui-primitives 与 ui-workspace 的全量测试套件通过。
`chat-view.spec.tsx` 定了「不渲染任何内容」节点的分组行为(含被中断节点这一例外);`chat-tool-row.spec.tsx` 定了工作区内、工作区外以及 cwd 为空时的 cwd 相对化行为;`atoms.spec.tsx``workspace-picker.spec.tsx` 覆盖菜单与 chip 的各种状态;ui-conversation、ui-primitives 与 ui-workspace 的全量测试套件通过。
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-30-tui-adapter-registration-race.md
2026-07-30-tui-adapter-registration-race.md: 051f5e6af5d7fd6c0096c84af064d065dd6f8983
2026-07-30-tui-adapter-registration-race.zh.md: 1706acb1d23bab987333a7ac0b5e2695226c1af5
@@ -1,6 +1,7 @@
# Agent Note: TUI model-context resolution defers on the adapter-registration race
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-30-tui-adapter-registration-race.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: TUI 模型上下文解析在适配器注册竞争时延后重试
Status: implemented
Archived: 2026-08-04
[English](2026-07-30-tui-adapter-registration-race.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-30-web-details-default-closed.md
2026-07-30-web-details-default-closed.md: e4271917b998c9a916d8c67b30627022587547b8
2026-07-30-web-details-default-closed.zh.md: 3b9432067c944ea604e039ee1b305a927a1c206e
@@ -1,6 +1,7 @@
# Agent Note: Web details default closed
Status: implemented
Archived: 2026-08-07
English | [中文](2026-07-30-web-details-default-closed.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: Web 详情栏默认关闭
Status: implemented
Archived: 2026-08-07
[English](2026-07-30-web-details-default-closed.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-31-hero-visible-while-blank-session-opens.md
2026-07-31-hero-visible-while-blank-session-opens.md: 1b1f35d82731675978585d718e4ef837f0c78aa5
2026-07-31-hero-visible-while-blank-session-opens.zh.md: 91c1a372ebc6341632820450d9e54d58c9d36916
@@ -1,6 +1,7 @@
# Agent Note: Hero stays visible while a blank session opens
Status: implemented
Archived: 2026-08-07
English | [中文](2026-07-31-hero-visible-while-blank-session-opens.zh.md)
@@ -24,12 +25,10 @@ The summary flag and the snapshot's own `blank` are distinct sources: the snapsh
## Deferred
The no-session→session tree relocation in `ConversationRoot` (the hero/composer subtree moves into the `conversation.session` outlet) still rebuilds the composer DOM on the same transition; removing it means moving `conversation.session` to `session-maybe` scope, a slot-contract change that needs its own proposal.
Object-layer reference churn found while diagnosing this — no-op projections minting fresh snapshots, the create path projecting twice, `select()` using `notifyNow` from async continuations — is real but independent of the visible flash.
## Consequences
Startup auto-selection renders the hero immediately and keeps the composer seat and header visible through the history round-trip, so launching into a recent workspace no longer looks like a page reload. Sessions whose summary does not prove them blank keep the previous settling behavior, so the guard still covers the case it was written for. Skeleton tests pin all three summary shapes: a row reporting `blank: false` settles, an absent row settles, and a summary-proven blank session opening under `loading` renders hero chrome with a live textarea.
The assembled coverage is `apps/web/tests/startup-auto-selection.e2e.ts` (keyless web browser lane): it registers a workspace, holds the `session.history` response open at the browser's network boundary, and asserts the visible frame while the auto-selected open is in flight — hero phase, hero title, painted composer — plus a recorded phase timeline of exactly `['hero']` for the whole load. Holding the round-trip is what makes it a regression test rather than a race: against a loopback host the open settles too fast to sample, and with the exemption reverted the held window is precisely when the root reports `settling`.
The assembled coverage is `apps/web/tests/startup-auto-selection.e2e.ts` (keyless web browser lane). Its first Workspace connection asserts that the Hero root, Workspace chip, scroll body, composer seat, and textarea remain the same DOM nodes when the blank Session appears. It then holds the `session.history` response open at the browser's network boundary and asserts the visible frame while the auto-selected open is in flight — hero phase, hero title, painted composer — plus a recorded phase timeline of exactly `['hero']` for the whole load. Holding the round-trip is what makes the second case a regression test rather than a race: against a loopback host the open settles too fast to sample, and with the exemption reverted the held window is precisely when the root reports `settling`.
@@ -1,6 +1,7 @@
# Agent Note: 空白会话打开期间保持 hero 可见
Status: implemented
Archived: 2026-08-07
[English](2026-07-31-hero-visible-while-blank-session-opens.md) | 中文
@@ -24,12 +25,10 @@ Status: implemented
## 推迟事项
`ConversationRoot` 中"无会话→有会话"的树位置迁移(hero/composer 子树移入 `conversation.session` 出口)仍会在同一次转换中重建 composer 的 DOM;消除它意味着把 `conversation.session` 移到 `session-maybe` 作用域,这是一次插槽契约变更,需要单独立项。
诊断期间发现的对象层引用抖动——空操作投影铸造出新的快照、创建路径重复投影一次、`select()` 在异步续体中使用 `notifyNow`——确实存在,但与这次可见闪烁相互独立。
## 影响
启动自动选择会立即渲染 hero,并在整个历史往返期间保持 composer 座位与 header 可见,因此启动进入最近工作区不再像页面重载。摘要未证明为空白的会话保持原有的 settling 行为,这道防护仍覆盖它当初针对的场景。骨架测试固定了摘要的三种形态:报告 `blank: false` 的行进入 settling;根本没有该行同样进入 settling;摘要已证明为空白的会话在 `loading` 期间渲染 hero 外壳与可用的文本框。
组装级覆盖是 `apps/web/tests/startup-auto-selection.e2e.ts`(无密钥的 Web 浏览器泳道):它注册一个工作区,在浏览器网络边界上扣住 `session.history` 的响应,并在自动选择的打开仍在飞行途中断言可见画面——hero 阶段、hero 标题、已绘制的 composer——外加整次加载记录到的阶段时间线恰好为 `['hero']`。扣住这次往返正是成为回归测试而非竞态的原因:对着回环主机,打开会快到无从采样;而一旦回退这条豁免,被扣住的这段窗口恰恰就是根节点报告 `settling` 的时刻。
组装级覆盖是 `apps/web/tests/startup-auto-selection.e2e.ts`(无密钥的 Web 浏览器泳道)。首次连接 Workspace 时,它断言 blank Session 出现前后 Hero root、Workspace chip、滚动主体、composer seat 与 textarea 都是同一 DOM 节点。随后它在浏览器网络边界上扣住 `session.history` 的响应,并在自动选择的打开仍在飞行途中断言可见画面——hero 阶段、hero 标题、已绘制的 composer——外加整次加载记录到的阶段时间线恰好为 `['hero']`。扣住这次往返正是第二个用例成为回归测试而非竞态的原因:对着回环主机,打开会快到无从采样;而一旦回退这条豁免,被扣住的这段窗口恰恰就是根节点报告 `settling` 的时刻。
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-07-31-tui-diff-context-line-accounting.md
2026-07-31-tui-diff-context-line-accounting.md: 5172069c7def39808b018030b630ea4313bd7038
2026-07-31-tui-diff-context-line-accounting.zh.md: b9a9863f05d1c5b829facdc850eccc7a321a76ab
@@ -0,0 +1,34 @@
# Agent Note: TUI diff context lines stay neutral
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-31-tui-diff-context-line-accounting.zh.md)
## Problem
Result-time filesystem diffs carry the applied change with three surrounding context lines in each `FileDiff.oldText` and `FileDiff.newText`. The TUI rendered every old-side row as removed and every new-side row as added, including the identical context present on both sides. A one-line edit therefore appeared as seven removals plus seven additions, and the footer repeated those inflated totals.
## Decision
The TUI compares each `FileDiff` whose old and new text are both available. Added and removed rows retain their green `+` and red `-` markers; equal context rows use the recessed body tone with a neutral two-space prefix. The footer sums only the rows classified as added or removed. `maxDiffEditLength` bounds the exact comparison by its combined added and removed line count; the default is 1000. Exceeding the bound renders the complete old side as removed and the complete new side as added, marks the footer approximate, and caches that result so redraws do not repeat the comparison. A tool result clears the pending-view cache before deriving the settled view, including when a presenter mutates and reuses the same view object.
When `oldText` is `null`, the renderer cannot distinguish a create from a pending overwrite or an argument fallback whose prior text is unavailable. It therefore shows every non-empty new-side row as added, without claiming those rows were absent from an existing file. Empty new content renders no synthetic added row.
This remains a consumer-side interpretation of the existing `FileDiff` contract. Filesystem tools continue to persist contextual before/after snippets, so other consumers keep their placement context and existing session logs replay with corrected TUI presentation. The TUI uses the same maintained `diff` package as `dsh-tool-fs` instead of introducing a second line-diff implementation.
## Alternatives considered
**Remove context from filesystem result metadata.** Rejected: contextual applied hunks are intentional producer output used by capable editors, and changing them would weaken every consumer while leaving old session logs misleading in the TUI.
**Extend `FileDiff` with persisted per-line tags.** Rejected: the tags can be derived deterministically from the existing before/after pair; persisting them would widen the cross-package and session-log contract solely for one renderer.
**Match equal lines by position without a diff algorithm.** Rejected: insertions and deletions shift subsequent context, so positional pairing would misclassify valid hunks.
**Run every comparison to completion.** Rejected: pending tool views can contain unrestricted model-authored old and new strings, and an unbounded Myers comparison can block the synchronous terminal renderer.
## Consequences
TUI diff cards distinguish evidence-bearing context from the mutation itself, and an exact `+A -R` footer reports the actual line delta. Replaying an existing contextual diff gains the corrected rendering without a migration. Result-time filesystem hunks are context-bounded; unrestricted pending views either complete within the configured edit-length budget or degrade to an explicitly approximate linear rendering.
The focused TUI tests cover neutral context, exact totals, an empty create, bounded fallback, result-time cache invalidation, and redraw cache reuse. The assembled `advanced-cards` terminal snapshots pin the neutral context style, semantic change colors, exact footer, and approximate fallback through collapsed and expanded card states.
@@ -0,0 +1,34 @@
# Agent Note: TUI diff 上下文行保持中性
Status: implemented
Archived: 2026-08-04
[English](2026-07-31-tui-diff-context-line-accounting.md) | 中文
## 问题
文件系统 diff 返回结果时,每个 `FileDiff.oldText``FileDiff.newText` 都会包含已应用的变更及其前后各 3 行上下文。TUI 将旧侧的每一行都渲染为删除行,将新侧的每一行都渲染为新增行,其中包括两侧相同的上下文。因此,一行编辑会显示为删除 7 行并新增 7 行,页脚还会重复这些虚高的合计值。
## 决策
TUI 会比较每个变更前后文本均可用的 `FileDiff`。新增行和删除行仍分别使用绿色 `+` 和红色 `-` 标记;相同的上下文行则使用弱化的正文色调,并带有由两个空格构成的中性前缀。页脚只汇总归类为新增或删除的行。`maxDiffEditLength` 以新增行与删除行的合计数为精确比较设置上限,默认值为 1000。超过上限时,TUI 会把完整旧侧渲染为删除内容、把完整新侧渲染为新增内容,将页脚标记为近似结果,并缓存该结果,避免后续重绘重复比较。工具结果会在派生已结算视图前清除待处理视图缓存,即使 presenter 修改并复用同一个视图对象也一样。
`oldText``null` 时,渲染器无法区分文件创建、待处理覆写,以及旧文本不可用的参数回退。因此,它会把新侧的每个非空行显示并计作新增行,但不会声称这些行原先不存在于已有文件中。新内容为空时,不会渲染虚构的新增行。
该行为仍然只是消费方对现有 `FileDiff` 契约的解释。文件系统工具仍会持久化带上下文的变更前后片段,因此其他消费方仍能获得定位上下文,已有会话日志在回放时也会采用修正后的 TUI 呈现。TUI 与 `dsh-tool-fs` 共用同一个受维护的 `diff` 包(package),无需引入第二套逐行 diff 实现。
## 考虑过的替代方案
**从文件系统结果元数据中移除上下文。** 不予采纳:带上下文的已应用 hunk 是有意保留的生产方输出,供具备相应能力的编辑器使用;更改这些内容会让所有消费方丢失信息,同时旧会话日志在 TUI 中仍会产生误导。
**为 `FileDiff` 扩展持久化的逐行标签。** 不予采纳:这些标签可以根据现有的变更前后文本对确定性派生;仅为一个渲染器持久化标签,会扩大跨包契约和会话日志契约。
**不使用 diff 算法,按位置匹配相同行。** 不予采纳:插入和删除会使后续上下文发生位移,因此按位置配对会把有效 hunk 错误分类。
**让所有比较都运行至完成。** 不予采纳:待处理工具视图可能包含由模型生成且长度不受限制的新旧字符串,无界的 Myers 比较可能阻塞同步终端渲染器。
## 后果
TUI diff 卡片会区分用于佐证的上下文与变更本身,精确的 `+A -R` 页脚会报告实际的行变更量。回放已有的上下文 diff 无需迁移即可获得修正后的渲染。结果时刻的文件系统 hunk 受上下文范围限制;不受限制的待处理视图要么在配置的编辑长度预算内完成比较,要么降级为明确标注为近似结果的线性渲染。
聚焦的 TUI 测试覆盖中性上下文、精确合计值、空文件创建、有界回退、结果到达时的缓存失效和重绘缓存复用。组装后的 `advanced-cards` 终端快照在卡片折叠和展开状态下固定了中性上下文样式、变更行的语义色彩、精确结果页脚和近似回退。
@@ -0,0 +1,6 @@
# 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/archived/bug-fix/2026-08-03-tui-long-session-render-costs.md
2026-08-03-tui-long-session-render-costs.md: 2f6e909aa280cf72cd15a635a64c80aee6cd0cf1
2026-08-03-tui-long-session-render-costs.zh.md: eeff7bc33df1f6e69d749c590953a42675a4d688
@@ -0,0 +1,34 @@
# Agent Note: TUI long-session render costs — shared step-timing scan and card line caches
Status: implemented
Archived: 2026-08-04
English | [中文](2026-08-03-tui-long-session-render-costs.zh.md)
## Problem
On a long resumed session (196k events, 2.2k steps, 1.8k tool cards) the TUI took ~12 s to render the transcript and ~800 ms to echo one keystroke. Profiling attributed both to the render path, not to session load (zstd + parse + surface seed is ~1.7 s):
- Every step's timing footer called `stepTimingAt`, which replayed the whole event log from index 0 per footer — O(steps × events) on the initial render, ~6 s of CPU.
- pi-tui re-renders every component each frame and relies on per-component line caches (its own `Text`/`Markdown` cache by `(text, width)`). `ToolCardComponent.render()` and `ContextCardComponent.render()` built throwaway `new Text(...)`/`new Markdown(...)` instances inside `render(width)`, so every frame — every keystroke — re-wrapped every settled card's output.
## Decision
`packages/ui/tui/src/chat/timing.ts` replaces `stepTimingAt` with `StepTimingTracker`: one accumulator per chat mount, created in `createTuiChat` and threaded through `StreamingAssistantComponent` into each `StepTimingComponent`. A query advances a cursor over events appended since the previous query and keeps per-step bucket state in a map, so all footers together cost O(events). The open bucket is accumulated to the query clock at lookup, and a step is pinned at its `step/end`. The tracker requires the append-only session log (the `seq = log length` contract).
`ToolCardComponent` and `ContextCardComponent` cache their rendered rows keyed by width. The cache drops on every state mutator (`updateResult`, `setVisibility`, `setExpanded`) and on `invalidate()` (pi-tui's tree-wide cascade), so a state change always re-renders; everything else — including every keystroke frame — returns the cached rows. This restores upstream pi's own component convention (persistent child components plus explicit `cachedWidth`/`cachedLines` where rendering is custom, e.g. pi `coding-agent` `bash.ts`), which the imperative `render(width)` bodies here had silently defeated.
Measured on the 196k-event session (tmux, 200×50): resume prompt-ready 12.2 s → 7.2 s; per-keystroke echo 796 ms median → 17 ms (fresh-session parity).
## Alternatives considered
- **Index `step/start` offsets, keep per-footer replay** — removes the `findIndex` but each footer still scans its step's span from a shared array; the tracker's single shared pass is the same complexity win with less bookkeeping.
- **Restructure the cards into persistent pi-tui child components** (upstream pi's primary style) — equivalent steady-state cost, but a larger diff across card state handling for no additional win over the width-keyed cache.
- **Cache inside pi-tui's `Container.render`** — wrong layer: the vendored patch surface would grow, and the contract (components own their caches) already exists upstream.
## Consequences
- Typing latency no longer scales with total tool output; the residual per-frame cost is pi-tui's tree traversal and row concatenation, linear in rendered rows. Resume render cost is now dominated by pi-tui's one-time initial layout (~4 s at 196k events) plus load (~1.7 s), both linear.
- The tracker consumes event times as logged and drops the removed implementation's mid-scan `time > at` cutoff, which per-footer `at` values make impossible in a shared scan; under a backward wall-clock step each bucket clamps at zero, which can differ from the old cutoff's totals.
- Card `render()` is no longer a pure function of `(state, width)` per call — mutators must drop `linesCache`. A new mutator that forgets to do so shows stale rows; the cache tests in `packages/ui/tui/tests/transcript-card-cache.spec.ts` pin the contract for the existing mutators.
- `StepTimingTracker` assumes step coordinates are not reused after `step/end`; a duplicate `step/start` for a closed step is ignored rather than restarting the step.
@@ -0,0 +1,34 @@
# Agent Note: TUI 长会话渲染开销:共享步骤耗时扫描与卡片行缓存
Status: implemented
Archived: 2026-08-04
[English](2026-08-03-tui-long-session-render-costs.md) | 中文
## 问题
在一个恢复后的长会话(196k 条事件、2.2k 个步骤、1.8k 张工具卡片)中,TUI 渲染 transcript(文本记录)耗时约 12 秒,回显一次按键耗时约 800 毫秒。性能剖析表明,两项耗时都来自渲染路径,而非会话加载(zstd + 解析 + 表层播种约为 1.7 秒):
- 每个步骤的耗时页脚都会调用 `stepTimingAt`,而它会针对每个页脚从索引 0 起回放整个事件日志,因此初次渲染的复杂度为 O(步骤数 × 事件数),占用约 6 秒 CPU 时间。
- pi-tui 每一帧都会重新渲染所有组件,并依赖各组件自己的行缓存(它的 `Text`/`Markdown` 会按 `(text, width)` 缓存)。`ToolCardComponent.render()``ContextCardComponent.render()` 构造用后即弃的 `new Text(...)`/`new Markdown(...)` 实例,且构造发生在 `render(width)` 内,因此每一帧,也就是每次按键,都会重新对每张已结算卡片的输出进行折行。
## 决策
`packages/ui/tui/src/chat/timing.ts` 不再使用 `stepTimingAt`,改用 `StepTimingTracker`:每次挂载聊天界面时在 `createTuiChat` 中创建一个累加器,再经 `StreamingAssistantComponent` 传入每个 `StepTimingComponent`。每次查询都会推进游标,扫描上次查询后追加的事件,并在一个映射表中保存各步骤的 bucket 状态,因此所有页脚合计只需 O(事件数)。查询时,系统把未闭合 bucket 累加到查询时刻;步骤在其 `step/end` 处固定。该跟踪器要求会话日志仅追加,即遵守 `seq = log length` 契约。
`ToolCardComponent``ContextCardComponent` 按宽度键控缓存渲染行。调用任一状态修改方法(`updateResult``setVisibility``setExpanded`)或 `invalidate()`(pi-tui 的全树级联)时会清空缓存,因此状态变化一定会重新渲染;其他情况,包括每一次按键帧,都会返回缓存行。这恢复了上游 pi 自身的组件惯例:使用常驻子组件;自定义渲染时显式使用 `cachedWidth`/`cachedLines`,例如 pi `coding-agent``bash.ts`。而这里命令式的 `render(width)` 函数体此前让这套惯例失效。
在该 196k 条事件的会话上测得(tmux,200×50):恢复后提示符就绪耗时从 12.2 秒降至 7.2 秒;每次按键的回显耗时中位数从 796 毫秒降至 17 毫秒(与新会话持平)。
## 曾考虑的替代方案
- **索引 `step/start` 偏移量,保留逐页脚回放**:这会消除 `findIndex`,但每个页脚仍要从共享数组扫描所属步骤的区间;跟踪器的一次共享遍历以更少的额外状态记录取得相同的复杂度改进。
- **把卡片重构为常驻 pi-tui 子组件**(上游 pi 的主要风格):稳定状态下成本相同,但卡片状态处理所需改动更大,相较按宽度键控的缓存并无额外收益。
- **在 pi-tui 的 `Container.render` 内缓存**:层级不对:对第三方内嵌代码的补丁范围会扩大,而上游已经约定由组件拥有各自的缓存。
## 后果
- 输入延迟不再随工具输出总量增长;剩余的每帧成本是 pi-tui 的树遍历与行拼接,与渲染行数呈线性关系。恢复时的渲染成本现由 pi-tui 的一次性初始布局(196k 条事件时约 4 秒)与加载(约 1.7 秒)主导,两者均为线性。
- 该跟踪器直接采用日志记录的事件时间,不再像已移除的实现那样,在扫描中途遇到 `time > at` 时截断;由于每个页脚的 `at` 值不同,共享扫描无法采用这种截断;挂钟时间倒退时,每个 bucket 都以零为下限,所得总计值可能与旧截断下的总计值不同。
- 卡片的 `render()` 不再是每次调用时 `(state, width)` 的纯函数,状态修改方法必须清空 `linesCache`。若新增状态修改方法时忘记清空,界面会显示陈旧行;`packages/ui/tui/tests/transcript-card-cache.spec.ts` 中的缓存测试固定了现有状态修改方法的契约。
- `StepTimingTracker` 假定步骤坐标在 `step/end` 后不会复用;对已关闭步骤重复出现的 `step/start` 会被忽略,不会重新启动该步骤。
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-17-dedicated-full-screen-tui-front-door.md
2026-07-17-dedicated-full-screen-tui-front-door.md: 1a04e738a2810abdb4bac81aebe9b23db404879e
2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 75fc3d47aa258fa6db767b1049194663148d1afa
@@ -1,11 +1,14 @@
# Agent Note: Dedicated full-screen TUI front door
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-17-dedicated-full-screen-tui-front-door.zh.md)
## Problem
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 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](../simplification/2026-07-20-remove-stdio-and-echo-agents.md) 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.
@@ -14,9 +17,9 @@ The interactive channel must remain a Cordis plugin over the same agent, session
DeepSeek Harness ships [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 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.
There is one terminal front door. `@deepseek-ai/dsh-tui` mounts before the configured agent, and `apps/cli/config/tui.cordis.yml` — an overlay over the shared `base.cordis.yml` — owns the interactive coding composition. Non-interactive tasks use the official headless surface; ACP remains a separate automation protocol and owns the supported Code Mode demo.
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 selected front door receives the exact generated or resumed `SessionId` used by the pre-created agent. It mounts before the agent composition, 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 and exits with status 1.
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
@@ -34,18 +37,18 @@ The built-in palette uses standard 16-color ANSI foregrounds and SGR attributes,
## Verification
The implemented [TUI terminal-state snapshot Agent Note](../testing/2026-07-18-tui-terminal-state-snapshots.md) owns the four-layer verification contract: direct behavior tests, transient semantic terminal snapshots, recorded JSONL journeys through production tools, and Loader/PTY smoke tests. The package README owns configuration, commands, model-visible effects, and current limitations.
The implemented [TUI terminal-state snapshot Agent Note](../testing/2026-07-18-tui-terminal-state-snapshots.md) 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. The app bundle may select a front door with `auto`; an explicitly mounted TUI fails loud.
- **Keep TUI wiring and tests under the readline `repl-agent` leaf** — rejected because one leaf would represent two distinct front doors and break symmetry with `acp-agent`. A dedicated `tui-agent` leaf owns TUI overlays and tests while reusing the repl-agent backend composition.
- **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-agent` leaf** — 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.options` when `/model` runs** — 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
- Interactive terminal work has a stateful Markdown, card, plan, and question interface with no second terminal protocol to keep aligned.
- 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.
@@ -1,11 +1,14 @@
# Agent Note: 独立的全屏 TUI 入口
Status: implemented
Archived: 2026-08-04
[English](2026-07-17-dedicated-full-screen-tui-front-door.md) | 中文
## 问题
可复用的 TUI 包(package)仍然保留实现,但 [`dsh` 不再将其作为应用入口交付](../simplification/2026-08-03-explicit-config-dsh-entrypoint.md)。本记录继续负责包边界和终端行为;后续记录负责产品组合。
在本入口引入时,面向行的 agent 负责 pipe 与普通终端,但全屏 coding 界面必须负责原始输入、差分绘制、光标状态、浮层和终端恢复。把这两类契约合并到一个 UI 插件中,会迫使面向 stream 的路径依赖仅适用于 TTY 的生命周期。后续的[移除重复 agent 决策](../simplification/2026-07-20-remove-stdio-and-echo-agents.md)移除了这个面向行 agent;本 Note 继续负责 TUI 设计。
交互通道必须继续作为 Cordis 插件,使用与其他入口相同的 agent(智能体)、会话、工具和用户交互服务。它需要恢复持久历史、跟随压缩替换、显示工具自有的呈现内容,并在启动失败和资源释放时恢复终端。独立聊天应用或第二套 agent 组合会在插件图之外重复实现这些行为。
@@ -14,9 +17,9 @@ Status: implemented
DeepSeek Harness 将 [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 作为独立的 Cordis 插件交付。该插件只负责终端输入与呈现;agent 生命周期、会话持久化、工具执行以及模型可见的提问工具仍由不同组合项负责。插件要求 stdin 和 stdout 均为 TTY;条件不满足时会失败,不会静默切换为逐行输出。
只有一个终端入口。`@deepseek-ai/dsh-tui` 在已配置 agent 之前挂载,而 `apps/cli/config/tui.cordis.yml`——叠加在共享 `base.cordis.yml` 之上的 overlay——拥有交互式 coding 组装。非交互任务使用官方 headless 界面;ACP 仍是独立的自动化协议,并拥有受支持的 Code Mode demo
该包是终端入口,而不是完整应用。宿主在已配置 agent 之前挂载 `@deepseek-ai/dsh-tui`,并围绕它组合后端、工具和策略。产品 CLI 目前不交付终端组合;非交互任务使用 headless 模式,Web 是已安装产品中面向人的界面,而 ACP 仍是独立的自动化协议
所选入口接收预创建 agent 使用的同一个新建或恢复 `SessionId`入口先于 agent 组合挂载,等待相符的根 agent 出现,然后才进入全屏模式。因此,相符的 `agent-loop/config-start-failed` 事件会在接管屏幕前报告,并以状态码 1 退出
宿主提供其预创建 agent 使用的同一个新建或恢复 `SessionId`TUI 等待相符的根 agent 出现,然后才进入全屏模式。因此,相符的 `agent-loop/config-start-failed` 事件会在接管屏幕前报告。
### 会话投影与交互
@@ -34,18 +37,18 @@ agent 空闲时,编辑器输入调用 `agent.send()`;轮次运行中则调
## 验证
已实现的 [TUI 终端状态快照 Agent Note](../testing/2026-07-18-tui-terminal-state-snapshots.md) 规定四层验证契约:直接行为测试、瞬态语义终端快照、通过生产工具执行的已录制 JSONL 流程,以及 Loader/PTY 冒烟测试。包(packageREADME 负责记录配置、命令、模型可见效果和当前限制。
已实现的 [TUI 终端状态快照 Agent Note](../testing/2026-07-18-tui-terminal-state-snapshots.md) 规定验证契约:直接行为测试语义终端快照。交付该入口的部署负责其组装后 transcript 和进程/PTY 验收。包 README 负责记录配置、命令、模型可见效果和当前限制。
## 曾考虑的替代方案
- **把 readline 与全屏模式都保留在 `@deepseek-ai/dsh-stdio` 中**:不予采纳,因为逐行输出和差分 TTY 渲染具有不同的依赖、输入规则、日志所有权和资源清理义务。拆分为独立包可以让管道安全契约保持精简、明确。
- **当任一进程流不是 TTY 时,让 TUI 插件静默降级**:不予采纳,因为回退会掩盖部署错误并改变交互语义。应用包可以通过 `auto` 选择入口;明确挂载的 TUI 会快速失败。
- **把 TUI 接线与测试保留在 readline `repl-agent` 叶节点下**:不予采纳,因为一个叶节点会代表两个不同入口,也会破坏它与 `acp-agent` 的对称性。独立的 `tui-agent` 叶节点负责 TUI 浮层和测试,同时复用 repl-agent 的后端组合
- **当任一进程流不是 TTY 时,让 TUI 插件静默降级**:不予采纳,因为回退会掩盖部署错误并改变交互语义。宿主可以选择其他入口;明确挂载的 TUI 会快速失败。
- **把 TUI 接线与测试保留在 readline `repl-agent` 叶节点下**当时不予采纳,因为一个叶节点会代表两个不同入口。后续移除产品入口时删除了该应用接线,但保留了包边界
- **在 `/model` 运行时修改 `agent.options`**:不予采纳,因为创建选项无法在异步 prompt 组装与请求路由之间提供原子边界。agent 作用域内的 waterfall 会在保持创建输入不可变的同时,为每个 step 快照一次选中的字段组合。
## 后果
- 交互式终端拥有带状态的 Markdown、卡片、计划和提问界面,无需再对齐第二套终端协议。
- 挂载 TUI 的部署会获得带状态的 Markdown、卡片、计划和提问界面,无需再对齐第二套终端协议。
- TUI 会引入 pi-tui 依赖并严格要求 TTY;非 TTY 部署使用 Headless app 或结构化协议。
- 会话投影使恢复与持久会话保持一致,但只有一个已配置会话拥有 transcript 和编辑器。
- 工具包通过既有呈现方法扩展终端卡片,无需在 TUI 中增加工具专用分支。
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-20-windows-tui-support.md
2026-07-20-windows-tui-support.md: 9308df7fb43378610cb2d480324c55bff07ea108
2026-07-20-windows-tui-support.zh.md: 8b4a1eb4cef3056b47b911a07ed62202b52d1f73
@@ -1,6 +1,7 @@
# Agent Note: Support the TUI on Windows
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-20-windows-tui-support.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: 在 Windows 上支持 TUI
Status: implemented
Archived: 2026-08-04
[English](2026-07-20-windows-tui-support.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-21-tui-resume-command.md
2026-07-21-tui-resume-command.md: de6c488b51b300705b33db13b8959b8114fdd025
2026-07-21-tui-resume-command.zh.md: d7639c7a51fc5c35f10c514356ceb79d9c1ca76a
@@ -1,6 +1,7 @@
# Agent Note: Product-level TUI session resume
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-21-tui-resume-command.zh.md)
@@ -36,4 +37,4 @@ The exit line is a launcher-owned context slot rather than a config template, an
## Testing
TUI tests cover keyboard navigation, title/id search, search-clear/cancel behavior, running-agent refusal, refusal of the current session and sessions already live in this runtime, route absence, corrupt rows, preflight revalidation, the no-host warning, and stop-before-handoff ordering. Session-query tests pin detached full-log validation. Agent-loop resume tests pin exact identity and history; title, todo, and goal replay suites pin restored projections and disarmed goal activation. The keyless TUI snapshot owns the full-viewport selector and its IME cursor anchor, and a real PTY smoke covers search plus handoff.
TUI package tests cover keyboard navigation, title/id search, search-clear/cancel behavior, running-agent refusal, refusal of the current session and sessions already live in this runtime, route absence, corrupt rows, preflight revalidation, the no-host warning, and stop-before-handoff ordering. Session-query tests pin detached full-log validation. Agent-loop resume tests pin exact identity and history; title, todo, and goal replay suites pin restored projections and disarmed goal activation. The package semantic snapshot owns the full-viewport selector and its IME cursor anchor; a deployment shipping the TUI owns its process and PTY handoff acceptance.
@@ -1,6 +1,7 @@
# Agent Note: 产品级 TUI 会话恢复
Status: implemented
Archived: 2026-08-04
[English](2026-07-21-tui-resume-command.md) | 中文
@@ -36,4 +37,4 @@ Status: implemented
## Testing
TUI 测试覆盖键盘导航、标题/id 搜索、清空搜索后再取消、agent 运行期间拒绝恢复、拒绝恢复当前会话和已在本运行时中处于活跃状态的会话、路由缺失、损坏的候选行、预检复查、无宿主时的告警,以及停止终端先于宿主交接的顺序。session-query 测试固定脱离运行时的完整日志验证。agent-loop 恢复测试固定会话身份和历史完全一致;标题、待办事项和目标回放测试套件固定这些投影均可恢复,且目标激活状态已经解除。无密钥 TUI 快照固定全屏选择页和输入法光标锚点,真实 PTY smoke 则覆盖搜索与交接
TUI 包(package测试覆盖键盘导航、标题/id 搜索、清空搜索后再取消、agent 运行期间拒绝恢复、拒绝恢复当前会话和已在本运行时中处于活跃状态的会话、路由缺失、损坏的候选行、预检复查、无宿主时的告警,以及停止终端先于宿主交接的顺序。session-query 测试固定脱离运行时的完整日志验证。agent-loop 恢复测试固定会话身份和历史完全一致;标题、待办事项和目标回放测试套件固定这些投影均可恢复,且目标激活状态已经解除。包级语义快照固定全屏选择页和输入法光标锚点;交付 TUI 的部署负责其进程与 PTY 交接验收
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-21-tui-skill-slash-command.md
2026-07-21-tui-skill-slash-command.md: e151f92d0d9eb1486bcf21498fb73ba156edd861
2026-07-21-tui-skill-slash-command.zh.md: ac1c4da948dcbd6b01bdb6e62d66d5f704c0b132
@@ -1,6 +1,7 @@
# Agent Note: TUI skill slash command
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-21-tui-skill-slash-command.zh.md)
@@ -30,4 +31,4 @@ Autocomplete filters the invocation-neutral `list()` result with `isUserInvocabl
## Consequences
Manual invocation always reloads the full skill body: the TUI does not detect a skill already present in the conversation, so a repeated `/skill:` appends its instructions again — acceptable because re-injection is sometimes the intent, and documented under the package README's Known Limitations. The two-renderer duplication is a standing maintenance cost accepted above. The `<skill name="…">` wrapper is stable model-visible text and is pinned verbatim in unit tests against a real `SkillService`; the help-panel line is pinned by the `errors-and-help` terminal snapshot. Autocomplete population and the disposed-lookup and failed-lookup branches are covered by unit tests that mount the real registry or a controllable service. End-to-end delivery is proven by a dedicated real-composition test: the `examples/tui-agent` keyless PTY smoke (`tui-keyless-smoke.e2e.ts`) boots the production TUI/agent/skill stack through the Loader under a genuine pseudo-terminal with only the model scripted, drops a fixture skill under the agents-home `skills/` root, types `/skill:<name>` as live keystrokes, and asserts the scripted adapter echoes the fixture's body marker only when the rendered `<skill>` block arrives — exercising `ctx.get('skills')` resolution in the shipped tree, the client-side parse, the local provider load, and the user turn reaching the model together. That fixture's frontmatter description avoids a `: ` colon-space so its YAML stays a plain scalar; an invalid-frontmatter skill is silently dropped during discovery.
Manual invocation always reloads the full skill body: the TUI does not detect a skill already present in the conversation, so a repeated `/skill:` appends its instructions again — acceptable because re-injection is sometimes the intent, and documented under the package README's Known Limitations. The two-renderer duplication is a standing maintenance cost accepted above. The `<skill name="…">` wrapper is stable model-visible text and is pinned verbatim in package tests against a real `SkillService`; the package semantic matrix pins the help-panel line. Autocomplete population, user-only discovery, delivery to idle and running agents, and the disposed-lookup and failed-lookup branches are covered by package tests that mount the real registry or a controllable service. The removed product TUI's keyless PTY smoke formerly covered the assembled Loader path; a future terminal deployment owns that application-level scenario.
@@ -1,6 +1,7 @@
# Agent Note: TUI skill slash command
Status: implemented
Archived: 2026-08-04
[English](2026-07-21-tui-skill-slash-command.md) | 中文
@@ -30,4 +31,4 @@ TUI 通过 `ctx.get('skills')` 读取 skill 服务,而非声明式注入,因
## Consequences
手动调用总是重新加载完整的 skill 正文:TUI 不会检测某个 skill 是否已在对话中出现,因此重复的 `/skill:` 会再次追加其指令——这可以接受,因为重新注入有时正是意图所在,且已在本包 README 的已知限制中说明。上文接受的双渲染器重复是一项长期维护成本。`<skill name="…">`是稳定的、模型可见的文本,并在单元测试中针对一个真实的 `SkillService` 逐字固定;帮助面板那一行由 `errors-and-help` 终端快照固定。自动补全填充、dispose 后查找分支、以及查找失败分支,都由挂载真实注册表或可控服务的单元测试覆盖。端到端的投递由一项专门的真实组合测试证明:`examples/tui-agent` 的无密钥 PTY 冒烟测试(`tui-keyless-smoke.e2e.ts`)在真实伪终端下经由 loader 引导生产环境的 TUI/agent/skill 栈,仅对模型进行脚本化,把一个夹具 skill 放入 agents home 的 `skills/` 根下,以真实按键输入 `/skill:<name>`,并断言:只有当渲染出的 `<skill>` 文本块抵达时,脚本化适配器才会回显该夹具的正文标记——从而一并演练了 `ctx.get('skills')` 在发布树中的解析、客户端解析、本地 provider 的加载,以及用户回合抵达模型。该夹具的 frontmatter 描述避免出现 `: ` 冒号加空格,使其 YAML 保持为纯标量;frontmatter 无效的 skill 会在发现阶段被静默丢弃
手动调用总是重新加载完整的 skill 正文:TUI 不会检测某个 skill 是否已在对话中出现,因此重复的 `/skill:` 会再次追加其指令——这可以接受,因为重新注入有时正是意图所在,且已在本包 README 的已知限制中说明。上文接受的双渲染器重复是一项长期维护成本。`<skill name="…">`装层是稳定的、模型可见的文本,并在测试中针对一个真实的 `SkillService` 逐字固定;包语义矩阵固定帮助面板中的这一行。自动补全填充、仅限用户的发现、向空闲及运行中 agent 投递,以及 dispose 后查找查找失败分支,都由挂载真实注册表或可控服务的测试覆盖。已移除的产品 TUI 的无密钥 PTY 冒烟测试过去覆盖组装后的 Loader 路径;未来的终端部署负责该应用级场景
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-22-docked-web-goal-bar.md
2026-07-22-docked-web-goal-bar.md: decf40996b51a0f2358a943bbb928f00d4db2026
2026-07-22-docked-web-goal-bar.zh.md: 8ebb106b260a6107084c07b51b1763adb1df2da8
@@ -1,6 +1,7 @@
# Agent Note: Docked web goal bar
Status: implemented
Archived: 2026-08-07
English | [中文](2026-07-22-docked-web-goal-bar.zh.md)
@@ -10,19 +11,19 @@ The web UI had no goal surface at all: the goal stack shipped with model tools,
## Decision
`GoalBar` (`packages/client/ui-goal/src/client/GoalBar.tsx`) is a props-driven, self-contained component registered first in the composer's input-dock list. Its standalone 752px card follows the composer's horizontal geometry, and every visible state shares one fixed 36px height so switching phases never resizes it. Loading (`goal === undefined`), absent (`goal === null`), and `phase === 'complete'` render nothing — a completed goal is history, not chrome.
`GoalBar` (`packages/client/ui-goal/src/client/GoalBar.tsx`) is a props-driven, self-contained component registered second in the composer's input-dock list, after Todo and before Queue. Its standalone 752px card follows the composer's horizontal geometry, and every visible state shares one fixed 36px height so switching phases never resizes it. Loading (`goal === undefined`), absent (`goal === null`), and `phase === 'complete'` render nothing — a completed goal is history, not chrome.
Visibility drives the label and actions: active shows "Ongoing Goal" with pause/edit/clear; paused shows "Paused Goal" and swaps pause for a resume icon button; blocked shows "Blocked Goal" and carries `blockedReason.message` as the strip's `title` tooltip. Goal creation lives on the `/goal` command, not in the bar. The pencil swaps the strip for an inline edit form prefilled with the current objective: Enter or the check button saves through `GoalBarActions.onEdit(objective)`, Esc cancels, and an all-whitespace objective keeps save disabled. The form closes only when the edit succeeds; a failure preserves the draft and displays the error in the bar. Resume and clear failures are displayed there as well. Clear otherwise calls `onClear` directly with no confirmation — a clear keeps a durable tombstone, so nothing is unrecoverable. An effect keyed on the goal's id drops the edit form when the goal's identity changes, so a surviving draft can never be written over the goal that replaced it.
Visibility drives the label and actions: active shows "Ongoing Goal" with pause/edit/clear; paused shows "Paused Goal" and swaps pause for a resume icon button; blocked shows "Blocked Goal" and carries `blockedReason.message` as the strip's `title` tooltip. Goal creation lives on the `/goal` command, not in the bar. The pencil swaps the strip for an inline edit form prefilled with the current objective: Enter or the check button saves through `GoalBarActions.onEdit(objective)`, Esc cancels, and an all-whitespace objective keeps save disabled. The form closes only when the edit succeeds; a failure preserves the draft and displays the error in the bar. Resume and clear failures are displayed there as well. Clear otherwise calls `onClear` directly with no confirmation — a clear keeps a durable tombstone, so nothing is unrecoverable. Every mutation first acquires a synchronous component-local single-flight latch because React's pending-state render cannot close the same-frame click window. A successful clear also suppresses that exact goal id immediately while the authoritative null projection catches up, so an acknowledged tombstone cannot leave a stale clear control that submits `GOAL_NOT_FOUND`; a failure releases the latch and remains retryable. An effect keyed on the goal's id resets this transient state and drops the edit form when the goal's identity changes, so neither a cleared marker nor a surviving draft can affect the replacement goal.
`GoalBarActions` lives in ui-goal's slot contract (`packages/client/ui-goal/src/client/slots.ts`) and carries exactly the rendered verbs: `onEdit`/`onPause`/`onResume`/`onClear`. Each callback asynchronously returns an explicit success/failure result so `GoalBar` owns its transitions and error display. `apply.ts` wires them to the runtime session methods; the runtime session resolves the current goal's compare-and-set ref internally, so the UI passes no ref.
The runtime session gains the goal surface the strip (and future UI) needs: `fetchGoal` populates the snapshot on open, and a live `context/message` carrying `goal/change` meta triggers a coalesced refetch — concurrent triggers share the in-flight `goal.get`, while a trigger received during that read schedules one coalesced trailing read so independently ordered notifications and GET responses cannot leave stale state. Window replays never refetch, and matching the meta kind (rather than a goal key) also catches clear tombstones written by other clients. The six mutation verbs fold transport failures into `{ ok: false }` results like every sibling session method, and a get result older than a mutation response that landed mid-flight is dropped.
The runtime session gains the goal surface the strip (and future UI) needs through the host-computed `goal` projection. The history tail seeds its whole current value, and `session/projection` frames update it when durable `agent/inbox/spliced` insertions commit goal snapshots or clear tombstones; later context admission is irrelevant to UI freshness. The four rendered mutation verbs fold transport failures into `{ ok: false }` results like every sibling session method.
The strip's background is `--dsw-alias-interactive-bg-hover` rather than the mock's literal `#F5F6F7`: the translucent hover gray resolves to that value over the white light-theme base and lifts the strip off the composer card in dark mode, where a static light token would sink. All colors are `--dsw-*` tokens.
## Testing
`packages/client/ui-goal/tests/goalbar.spec.tsx` pins the behavior through props alone: loading/absent/complete render nothing, the active strip renders label/objective and fires clear, the edit form prefills, rejects empty, saves on Enter, cancels on Esc, and resets when the goal's identity changes, the active strip fires pause, the paused strip fires resume, and the blocked strip exposes the reason tooltip. Component failure-path cases prove that a failed edit preserves its draft and that edit/resume/clear errors remain visible in the bar. The skeleton specs mount `ConversationRoot` with and without `goalActions`; the undefined case is seeded with an active goal, so the missing gate — not the missing goal — is what hides the strip. Runtime session specs pin the folded-error results, the live-only in-flight-plus-trailing refetch, and the stale-read guard. A keyless real-browser smoke boots the assembled application through `boot → RPC → runtime → GoalBar` and records an inline snapshot of the rendered label, objective, and actions.
`packages/client/ui-goal/tests/goalbar.spec.tsx` pins the behavior through props alone: loading/absent/complete render nothing, the active strip renders label/objective and fires clear, rapid same-frame clear clicks dispatch once and a successful clear hides before projection convergence, the edit form prefills, rejects empty, saves on Enter, cancels on Esc, and resets when the goal's identity changes, the active strip fires pause, the paused strip fires resume, and the blocked strip exposes the reason tooltip. Component failure-path cases prove that a failed edit preserves its draft and that edit/resume/clear errors remain visible and retryable in the bar. The skeleton specs mount `ConversationRoot` with and without `goalActions`; the undefined case is seeded with an active goal, so the missing gate — not the missing goal — is what hides the strip. Runtime session specs pin folded-error results and projection updates. A keyless real-browser smoke boots the assembled application through `boot → RPC → runtime → GoalBar` and records an inline snapshot of the rendered label, objective, and actions.
## Alternatives considered
@@ -34,6 +35,7 @@ The strip's background is `--dsw-alias-interactive-bg-hover` rather than the moc
## Consequences
- Goal presence in the web UI is a standalone composer-context strip: sparkle, phase label, truncated objective, and pause/edit/clear (resume replacing pause when paused) — the browser client's first goal surface.
- The runtime session exposes the goal verbs over RPC with folded transport errors, and refreshes the snapshot's goal on open and on live goal-change meta (coalesced, guarded against stale reads).
- Goal mutations are single-flight within the component; a successful clear hides its exact goal immediately while projection delivery converges, preventing duplicate CAS errors without making transient UI state authoritative.
- The runtime session exposes the goal verbs over RPC with folded transport errors and consumes the host's durable whole-goal projection on open and live updates.
- Objective editing is reachable from the UI for the first time, through `goal.edit` with the runtime-owned ref; complete remains available to other surfaces (`/goal`, model tools).
- `goal === null` renders nothing; the composer carries no persistent create affordance — creation is the `/goal` command's job.
@@ -1,6 +1,7 @@
# Agent Note: 停靠式 Web 目标条
Status: implemented
Archived: 2026-08-07
[English](2026-07-22-docked-web-goal-bar.md) | 中文
@@ -10,19 +11,19 @@ Web UI 此前没有任何目标相关的界面:目标栈已随模型工具、T
## 决策
`GoalBar``packages/client/ui-goal/src/client/GoalBar.tsx`)是一个由 props 驱动的自包含组件,在 composer 的 input-dock 列表中注册为第个条目。它采用独立的 752px 卡片,遵循 composer 的水平几何;所有可见状态均使用固定的 36px 高度,切换阶段不会改变尺寸。加载中(`goal === undefined`)、无目标(`goal === null`)和 `phase === 'complete'` 时不渲染任何内容:已完成的目标是历史记录,不是常驻界面元素。
`GoalBar``packages/client/ui-goal/src/client/GoalBar.tsx`)是一个由 props 驱动的自包含组件,在 composer 的 input-dock 列表中注册为第个条目,位于 Todo 之后、Queue 之前。它采用独立的 752px 卡片,遵循 composer 的水平几何;所有可见状态均使用固定的 36px 高度,切换阶段不会改变尺寸。加载中(`goal === undefined`)、无目标(`goal === null`)和 `phase === 'complete'` 时不渲染任何内容:已完成的目标是历史记录,不是常驻界面元素。
可见性决定标签和操作:active 状态显示 "Ongoing Goal" 并提供暂停/编辑/清除;paused 状态显示 "Paused Goal",把暂停换成一个恢复图标按钮;blocked 状态显示 "Blocked Goal",并把 `blockedReason.message` 作为横条的 `title` 悬浮提示。创建目标的入口在 `/goal` 命令上,不在横条里。铅笔图标把横条切换为内联编辑表单,预填当前目标内容:Enter 或勾选按钮通过 `GoalBarActions.onEdit(objective)` 保存,Esc 取消,目标内容全为空白字符时保存按钮保持禁用。编辑成功后表单才会关闭;编辑失败时保留草稿,并在横条中显示错误。恢复和清除失败也显示在横条中。除此之外,清除直接调用 `onClear`,不做确认——清除会保留 durable 墓碑,没有不可恢复的损失。一个以目标 id 为键的 effect 会在目标身份变化时丢弃编辑表单,因此存留草稿绝不可能覆盖掉替换它的新目标。
可见性决定标签和操作:active 状态显示 "Ongoing Goal" 并提供暂停/编辑/清除;paused 状态显示 "Paused Goal",把暂停换成一个恢复图标按钮;blocked 状态显示 "Blocked Goal",并把 `blockedReason.message` 作为横条的 `title` 悬浮提示。创建目标的入口在 `/goal` 命令上,不在横条里。铅笔图标把横条切换为内联编辑表单,预填当前目标内容:Enter 或勾选按钮通过 `GoalBarActions.onEdit(objective)` 保存,Esc 取消,目标内容全为空白字符时保存按钮保持禁用。编辑成功后表单才会关闭;编辑失败时保留草稿,并在横条中显示错误。恢复和清除失败也显示在横条中。除此之外,清除直接调用 `onClear`,不做确认——清除会保留 durable 墓碑,没有不可恢复的损失。每次变更都会先取得一个同步的组件内 single-flight 锁,因为 React 的 pending 状态渲染无法关闭同一帧内的点击窗口。清除成功后还会立即抑制该 goal id,直到权威的 null 投影追上,因此已确认的墓碑不会留下陈旧的清除控件并再次提交 `GOAL_NOT_FOUND`;失败则释放锁,并且仍可重试。一个以目标 id 为键的 effect 会在目标身份变化时重置瞬态状态并丢弃编辑表单,因此无论已清除标记还是存留草稿,都不会影响替换目标。
`GoalBarActions` 位于 ui-goal 的槽位契约(`packages/client/ui-goal/src/client/slots.ts`),只携带实际渲染的动词:`onEdit`/`onPause`/`onResume`/`onClear`。每个回调都会异步返回显式成功/失败结果,因此 `GoalBar` 自行负责界面转换和错误显示。`apply.ts` 把它们接到运行时会话方法上;运行时会话在内部解析当前目标的 compare-and-set ref,因此 UI 不传 ref。
运行时会话获得横条(以及未来 UI)所需的目标表面:`fetchGoal` 在打开时填充快照;携带 `goal/change` 元数据的 live `context/message` 触发合并重新拉取——并发触发器共享正在执行的 `goal.get`,读取期间收到的触发器会安排一次合并后的尾随读取,避免彼此独立排序的通知和 GET 响应留下陈旧状态。窗口重放绝不触发重新拉取,且匹配元数据 kind(而不是 goal 键)还能捕获其他客户端写入的清除墓碑。六个变更动词与所有同类会话方法一样,把传输层失败折叠为 `{ ok: false }` 结果;比在拉取途中落地的变更响应更旧的 get 结果会被丢弃
运行时会话通过由 host 计算的 `goal` 投影获得横条(以及未来 UI)所需的 goal 表面。历史尾页会提供完整当前值作为初始状态;持久 `agent/inbox/spliced` 插入项提交 goal 快照或 clear 墓碑时,`session/projection` 帧会更新该值,后续上下文准入与 UI 新鲜度无关。4 个实际渲染的变更动词与所有同类会话方法一样,把传输层失败折叠为 `{ ok: false }` 结果。
横条的背景色用 `--dsw-alias-interactive-bg-hover`,而不是设计稿里的字面值 `#F5F6F7`:这个半透明的悬浮灰在浅色主题的白色底上正好解析为该值,而在深色模式下能把横条从输入框卡片上衬托出来,静态的浅色 token 在深色模式下会沉进去。所有颜色都是 `--dsw-*` token。
## 测试
`packages/client/ui-goal/tests/goalbar.spec.tsx` 仅通过 props 固定这些行为:加载中/无目标/已完成时不渲染;active 横条渲染标签和目标内容并触发清除;编辑表单预填内容、拒绝空值、按 Enter 保存、按 Esc 取消,并在目标身份变化时重置;active 横条触发暂停;paused 横条触发恢复;blocked 横条暴露原因悬浮提示。组件失败路径用例证明编辑失败时保留草稿,并且编辑/恢复/清除错误持续显示在横条中。skeleton 规格测试分别挂载带与不带 `goalActions``ConversationRoot`;未定义的情形预置了一个 active 目标,因此隐藏横条的是缺失的挂载门,而不是缺失的目标。运行时会话规格测试固定折叠错误结果、仅 live 的执行中读取加尾随读取,以及陈旧读取守卫。一个无密钥真实浏览器冒烟测试通过 `boot → RPC → runtime → GoalBar` 启动组装后的应用,并以内联快照记录渲染出的标签、目标内容和操作。
`packages/client/ui-goal/tests/goalbar.spec.tsx` 仅通过 props 固定这些行为:加载中/无目标/已完成时不渲染;active 横条渲染标签和目标内容并触发清除;同一帧内快速连续点击清除只会分发一次,清除成功后横条会在投影收敛前隐藏;编辑表单预填内容、拒绝空值、按 Enter 保存、按 Esc 取消,并在目标身份变化时重置;active 横条触发暂停;paused 横条触发恢复;blocked 横条暴露原因悬浮提示。组件失败路径用例证明编辑失败时保留草稿,并且编辑/恢复/清除错误持续显示在横条中且可重试。skeleton 规格测试分别挂载带与不带 `goalActions``ConversationRoot`;未定义的情形预置了一个 active 目标,因此隐藏横条的是缺失的挂载门,而不是缺失的目标。运行时会话规格测试固定折叠错误结果和投影更新。一个无密钥真实浏览器冒烟测试通过 `boot → RPC → runtime → GoalBar` 启动组装后的应用,并以内联快照记录渲染出的标签、目标内容和操作。
## 考虑过的替代方案
@@ -34,6 +35,7 @@ Web UI 此前没有任何目标相关的界面:目标栈已随模型工具、T
## 后果
- Web UI 中目标的存在形式是独立的 composer 上下文横条:闪光图标、阶段标签、截断的目标内容,以及暂停/编辑/清除(暂停时恢复取代暂停)——这是浏览器客户端的第一个目标界面。
- 运行时会话通过 RPC 暴露目标动词并折叠传输层错误,且在打开时和 live 目标变更元数据到达时刷新快照中的目标(合并拉取,带陈旧读取守卫)
- 目标变更在组件内走 single-flight;清除成功后会在投影投递收敛期间立即隐藏与其 id 完全匹配的目标,既防止重复 CAS 错误,又不会把瞬态 UI 状态视为权威
- 运行时会话通过 RPC 暴露 goal 动词并折叠传输层错误,在打开时和 live 更新时消费 host 的持久完整 goal 投影。
- 目标内容首次可以从 UI 编辑,经由 `goal.edit`,ref 由运行时持有;完成对其他界面(`/goal`、模型工具)照常可用。
- `goal === null` 时不渲染任何内容;输入框不提供常驻的创建入口,创建是 `/goal` 命令的职责。
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-23-tui-file-reference-autocomplete.md
2026-07-23-tui-file-reference-autocomplete.md: 3ceda81858bc3364a2c4ac5b91d599b23479412a
2026-07-23-tui-file-reference-autocomplete.zh.md: c0b947edf6df42d27f5fee40d95eb073029e32b1
@@ -1,6 +1,7 @@
# Agent Note: TUI file-reference autocomplete
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-23-tui-file-reference-autocomplete.zh.md)
@@ -30,4 +31,4 @@ Structured session mentions keep their existing snapshot preparation. Unlike fil
Users can discover and insert paths without making selection itself expensive or model-visible beyond the path. The model preserves agency over whether to inspect a file, and any inspection remains reconstructable through the logged tool transcript. The fixed instruction slightly enlarges TUI system prompts when `read` is present, and content-requiring requests take an additional tool round trip.
Completion is deliberately bounded and advisory: very large workspaces may omit paths beyond the configured index cap, ignored files may still appear, and remote or virtual filesystem deployments must align the TUI host working directory with the `read` namespace or supply a different completion surface. Package tests pin token grammar, ranking, bounds, cancellation, invalidation, and path-only submission; terminal snapshots and the real Loader PTY smoke pin the visible menu and keyboard completion.
Completion is deliberately bounded and advisory: very large workspaces may omit paths beyond the configured index cap, ignored files may still appear, and remote or virtual filesystem deployments must align the TUI host working directory with the `read` namespace or supply a different completion surface. Package tests pin token grammar, ranking, bounds, cancellation, invalidation, path-only submission, the visible menu, and keyboard completion; a deployment shipping the TUI owns its Loader and PTY acceptance.
@@ -1,6 +1,7 @@
# Agent Note: TUI 文件引用自动补全
Status: implemented
Archived: 2026-08-04
[English](2026-07-23-tui-file-reference-autocomplete.md) | 中文
@@ -30,4 +31,4 @@ TUI 维护一个有容量上限且可取消的主机工作区路径索引,以
用户可以发现并插入路径,而选择操作本身不会带来高开销,对模型可见的内容也仅限路径。模型仍可自行决定是否检查文件,任何检查都能通过已记录的工具 transcript 重建。存在 `read` 时,固定指令会略微增大 TUI 系统提示词;需要文件内容的请求还会增加一次工具往返。
补全有意采用有界的提示性设计:超大型工作区可能省略超过配置索引上限的路径,被忽略的文件仍可能出现,远程或虚拟文件系统部署必须让 TUI 的主机工作目录与 `read` 命名空间对齐,否则需要提供不同的补全接口。包(package)测试固定 token 语法、排序、边界、取消、失效仅提交路径的行为;终端快照与真实 Loader PTY 冒烟测试固定可见菜单和键盘补全
补全有意采用有界的提示性设计:超大型工作区可能省略超过配置索引上限的路径,被忽略的文件仍可能出现,远程或虚拟文件系统部署必须让 TUI 的主机工作目录与 `read` 命名空间对齐,否则需要提供不同的补全接口。包(package)测试固定 token 语法、排序、边界、取消、失效仅提交路径的行为、可见菜单和键盘补全;交付 TUI 的部署负责其 Loader PTY 验收
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-23-tui-status-prompt-tools.md
2026-07-23-tui-status-prompt-tools.md: b8e5e9fc4fd8f9d6dafa689512ef991ee1013b39
2026-07-23-tui-status-prompt-tools.zh.md: 3d10073b4332609371fdd1ebed7486970b439251
@@ -1,6 +1,7 @@
# Agent Note: TUI status inspects model request inputs
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-23-tui-status-prompt-tools.zh.md)
@@ -26,4 +27,4 @@ The command can run prompt providers and assembly listeners, just like request p
## Testing
Unit coverage pins scoped assembly output, ordered tool names, empty labels, and terminal-control escaping. The keyless TUI smoke and terminal snapshot exercise `/status` through the assembled application.
Package behavior tests pin scoped assembly output, ordered tool names, empty labels, and terminal-control escaping. The package semantic snapshots exercise `/status` at normal and narrow widths; a deployment shipping the TUI owns its assembled process acceptance.
@@ -1,6 +1,7 @@
# Agent Note: TUI 状态检查模型请求输入
Status: implemented
Archived: 2026-08-04
[English](2026-07-23-tui-status-prompt-tools.md) | 中文
@@ -26,4 +27,4 @@ Status: implemented
## 测试
单元测试固定按作用域组装的输出、工具名称顺序、空值标签和终端控制字符转义。无密钥 TUI 冒烟测试与终端快照通过完整组装的应用执行 `/status`
包(package)行为测试固定按作用域组装的输出、工具名称顺序、空值标签和终端控制字符转义。包级语义快照在正常宽度和窄宽度下执行 `/status`;交付 TUI 的部署负责其组装后的进程验收
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-24-configurable-tui-prompt-theme.md
2026-07-24-configurable-tui-prompt-theme.md: 918eaa617811049a5caa4b9f43a8cc12854471c3
2026-07-24-configurable-tui-prompt-theme.zh.md: eccfed00c3fe866291d2f18bd7cecf117296bd58
@@ -1,6 +1,7 @@
# Agent Note: TUI prompt themes compose mutable plugin values
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-24-configurable-tui-prompt-theme.zh.md)
@@ -36,4 +37,4 @@ Changing `inputPrompt` through a registered value preserves editor text, cursor,
## Testing
Registry tests pin validation, duplicate rejection, updates, unavailable values, coalesced-notification containment, unsubscribe, disposal, interpolation, trailing-literal retention, whitespace cleanup, and ANSI preservation. TUI tests pin nested theme defaults, custom templates, out-of-band value redraw, mutable redraw, Powerline-capable fragments, dynamic input-prefix width, and the static running placeholder. The assembled TUI demo test pins service load order and config forwarding.
Registry tests pin validation, duplicate rejection, updates, unavailable values, coalesced-notification containment, unsubscribe, disposal, interpolation, trailing-literal retention, whitespace cleanup, and ANSI preservation. TUI package tests pin service availability, nested theme defaults, config forwarding, custom templates, out-of-band value redraw, mutable redraw, Powerline-capable fragments, dynamic input-prefix width, and the static running placeholder. A deployment shipping the TUI owns assembled load-order acceptance.
@@ -1,6 +1,7 @@
# Agent Note: TUI 提示符主题组合可变的插件值
Status: implemented
Archived: 2026-08-04
[English](2026-07-24-configurable-tui-prompt-theme.md) | 中文
@@ -36,4 +37,4 @@ TUI 主题把 `color`、`truecolor`、`leftPrompt`、`rightPrompt`、`inputPromp
## 测试
注册表测试固定校验、重名拒绝、更新、不可用值、合并通知的容错、取消订阅、dispose、插值、尾随字面保留、空白清理与 ANSI 保留等行为。TUI 测试固定嵌套主题默认值、自定义模板、带外值重绘、可变重绘、支持 Powerline 的片段、动态输入前缀宽度以及运行状态下的静态占位文本。组装后的 TUI 演示测试固定服务加载顺序与配置转发
注册表测试固定校验、重名拒绝、更新、不可用值、合并通知的容错、取消订阅、dispose、插值、尾随字面保留、空白清理与 ANSI 保留等行为。TUI 包(package)测试固定服务可用性、嵌套主题默认值、配置转发、自定义模板、带外值重绘、可变重绘、支持 Powerline 的片段、动态输入前缀宽度以及运行状态下的静态占位文本。交付 TUI 的部署负责组装后的加载顺序验收
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-24-tui-question-dialog-multiline.md
2026-07-24-tui-question-dialog-multiline.md: 9f1e75325942f28d69d9cadadf938c0f0b5e3ae5
2026-07-24-tui-question-dialog-multiline.zh.md: bd112898367e7a7dd940f2b5fa76e531029ab803
@@ -0,0 +1,42 @@
# Agent Note: TUI QuestionDialog renders options across multiple lines
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-24-tui-question-dialog-multiline.zh.md)
## Problem
`ctx.userInteraction.ask()` must keep question text, supporting `detail`, option labels, descriptions, validation, and controls readable inside configured width and height bounds. The question panel also belongs directly above the editor: placing it at the terminal edge separates the pending decision from both the transcript that prompted it and the input that follows it.
## Decision
The TUI renders a pending question as an inline modal between the transcript/status area and the editor while retaining the shared FIFO with model and plugin overlays:
- `InlineModalComponent` applies `questionDialogWidth` and `questionDialogMaxHeight` inside the normal component flow. The effective question height is additionally clamped to the current viewport after reserving the editor, so the editor remains below the question during resize.
- `renderOptionBlock` wraps each label beneath its cursor/number prefix and renders the muted description on separately wrapped, equally indented lines. The progress header, question, custom-answer hint, validation text, and final rows are width-bounded as well; the final ellipsis clamp is only a safety boundary for prefixes or other indivisible content. The explicit `↑ N lines hidden` fallback is reserved for a viewport below the configured minimum, where the whole semantic layout cannot fit.
- When question text or `detail` exceeds the header allocation, the header becomes a paged line viewport with its own `… lines A-B/N • PgUp/PgDn` status row. Page Up and Page Down traverse both line viewports: forward navigation exhausts the header/detail pages before entering oversized selected-option pages, and backward navigation reverses that order. This keeps plan-review detail reachable rather than leaving it behind the height clamp.
- The option-line budget subtracts padding, header, position, and footer rows before `windowBlocks` runs. The window obeys both `maxQuestionOptions` and the remaining row budget, keeps the selected option visible, and renders omitted options as `↑ N more` / `↓ N more` markers. If fixed chrome would leave fewer than four option rows, the compact header becomes the line pager so selected content, paging status, and both option markers still fit.
- When one selected block exceeds its allocation, it becomes a line viewport with a `lines A-B/N • PgUp/PgDn` status row. Page Up and Page Down expose every wrapped line without allowing the block to hide the option markers, validation, or controls.
Package tests pin count and height bounds, header and selected-block paging order, narrow-width wrapping, selection behavior, and placement relative to retained editor input. Semantic TUI snapshots pin the assembled terminal layout, header/detail and selected-option page transitions, and validation state.
## Alternatives considered
**Ellipsis-only horizontal truncation.** Keeping one option per row would signal lost text without making the description readable and would not address vertical bounds. The implementation wraps readable content and retains an ellipsis only as a final safety boundary.
**Wrap the combined label and description.** A composite row couples their widths, so either side can starve the other. Separate lines keep both widths predictable.
**Keep the question as a bottom-edge overlay.** A terminal-edge anchor can place the panel after the editor or cover lower chrome, depending on transcript and viewport height. The inline modal preserves ordering while the modal manager retains focus and FIFO ownership.
**Push the bounds into pi-tui.** Generic overlay slicing cannot identify option boundaries, selected content, controls, or the inline editor relationship. The owning dialog therefore applies semantic count, row, and paging rules.
**Use only the option-count cap.** `maxQuestionOptions` remains a public count bound, but it cannot contain wrapped blocks by itself. The dialog enforces the count and row bounds together.
## Consequences
- Descriptions consume additional rows, so fewer options can be visible than `maxQuestionOptions`; markers state the omitted option counts.
- Long question text and plan-review detail remain reachable inside a height-bounded panel, at the cost of sharing Page Up and Page Down with selected-option paging.
- An oversized selected block reserves one status row and requires Page Up or Page Down to read beyond the current line page.
- The inline question can displace older transcript rows from a short viewport. Below the configured minimum height, the final fallback can collapse upper rows behind an explicit hidden-line marker so the input controls and editor remain available.
- The model-facing schema, selected labels, abort/cancel behavior, and ACP elicitation path are unchanged.
@@ -0,0 +1,42 @@
# Agent Note: TUI QuestionDialog 以多行方式渲染选项
Status: implemented
Archived: 2026-08-04
[English](2026-07-24-tui-question-dialog-multiline.md) | 中文
## 问题
`ctx.userInteraction.ask()` 必须确保问题正文、`detail` 补充内容、选项标签、描述、校验信息和控件在已配置的宽度与高度边界内均可读。问题面板也直接位于编辑器上方:若将其置于终端边缘,待处理决策就会同时脱离触发该决策的 transcript(文本记录)和后续输入。
## 决策
TUI 将待处理问题渲染为位于 transcript/状态区域与编辑器之间的内联模态框,同时仍与模型浮层和插件浮层共享 FIFO:
- `InlineModalComponent` 在正常组件流内应用 `questionDialogWidth``questionDialogMaxHeight`。系统在为编辑器预留空间后,还会根据当前视口限制问题的实际高度,因此调整窗口大小时,编辑器仍位于问题下方。
- `renderOptionBlock` 将每个标签换行到光标/编号前缀下方,并在另行换行且缩进相同的行上渲染弱化的描述。进度标题、问题、自定义答案提示、校验文本和末尾行也受宽度边界约束;最终的省略号截断仅作为前缀或其他不可拆分内容的安全边界。明确的 `↑ N lines hidden` 回退仅用于低于已配置最小值、无法容纳完整语义布局的视口。
- 当问题正文或 `detail` 超出头部分配的空间时,头部会成为带有独立 `… lines A-B/N • PgUp/PgDn` 状态行的分页行视口。Page Up 和 Page Down 会遍历这两个行视口:向前导航先翻完问题正文/`detail` 页面,再进入超大选中选项页面;向后导航则采用相反顺序。这样可确保计划评审的 `detail` 内容始终可达,而不会被高度边界挡住。
-`windowBlocks` 运行前,选项行预算会扣除内边距、标题行、位置行和页脚行。窗口同时遵守 `maxQuestionOptions` 和剩余行预算,保持选中项可见,并将省略的选项渲染为 `↑ N more``↓ N more` 标记。若固定界面元素会使选项行少于四行,紧凑头部会转为行分页器,从而容纳选中内容、分页状态和上下两个选项标记。
- 当一个选中块超出分配空间时,它会成为带有 `lines A-B/N • PgUp/PgDn` 状态行的行视口。Page Up 和 Page Down 可展示每一行已换行内容,同时防止该块遮住选项标记、校验信息或控件。
包(package)测试固定数量和高度边界、头部与选中块的分页顺序、窄宽度换行、选择行为,以及问题相对于保留的编辑器输入的位置。语义 TUI 快照固定组装后的终端布局、头部/详情与选中选项的分页转换,以及校验状态。
## 备选方案
**仅用省略号进行横向截断。** 保持每个选项占一行,只能提示文本有所丢失,无法使描述变得可读,也无法处理纵向边界。该实现会对可读内容换行,仅将省略号保留为最终安全边界。
**将标签与描述合并后换行。** 组合行会将两者的宽度耦合在一起,任一方都可能挤占另一方的空间。分行渲染可使二者的宽度保持可预测。
**将问题保留为终端底边浮层。** 根据 transcript 和视口高度,锚定在终端边缘的面板可能出现在编辑器之后,也可能遮盖下方界面元素。内联模态框可保留顺序,同时由模态管理器继续负责焦点和 FIFO 所有权。
**将边界处理下推至 pi-tui。** 通用浮层切片无法识别选项边界、选中内容、控件或内联编辑器关系。因此,负责该语义的对话框会应用数量、行数和分页规则。
**仅使用选项数量上限。** `maxQuestionOptions` 仍是公开的数量边界,但仅靠它无法容纳已换行的块。对话框会同时执行数量边界和行数边界。
## 后果
- 描述会占用额外行,因此可见选项数可能少于 `maxQuestionOptions`;标记会说明省略的选项数量。
- 较长的问题正文和计划评审 `detail` 在受高度约束的面板内仍然可达,代价是 Page Up 和 Page Down 需要与选中选项分页共用。
- 超出空间的选中块会预留一行状态信息;若要阅读当前页面之外的行,必须使用 Page Up 或 Page Down。
- 在较矮的视口内,内联问题可能将较早的 transcript 行挤出可见区域。低于已配置最小高度时,最终回退可能将上部行折叠到明确的隐藏行标记之后,从而让输入控件和编辑器仍然可用。
- 面向模型的 schema、选中的标签、中止/取消行为,以及 ACPAgent Client Protocol)的 elicitation 路径均保持不变。
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-24-tui-shell-prompt-editor.md
2026-07-24-tui-shell-prompt-editor.md: a3707d3fcd0ae30517083ed388eb38cb3b92cad7
2026-07-24-tui-shell-prompt-editor.zh.md: ca050e2000ff772389086b5023935c816ec1154e
@@ -1,6 +1,7 @@
# Agent Note: TUI shell-prompt editor
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-24-tui-shell-prompt-editor.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: TUI shell 提示符编辑器
Status: implemented
Archived: 2026-08-04
[English](2026-07-24-tui-shell-prompt-editor.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-27-assistant-timing-header-trailing.md
2026-07-27-assistant-timing-header-trailing.md: 2ef926cff989499aa05ebfa781eea10399c69d18
2026-07-27-assistant-timing-header-trailing.zh.md: 282ac1e65b3c4e185839d3c6b70a085f685e8013
@@ -1,6 +1,7 @@
# Agent Note: Assistant timing line renders after the message body
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-27-assistant-timing-header-trailing.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: Assistant timing line renders after the message body
Status: implemented
Archived: 2026-08-04
[English](2026-07-27-assistant-timing-header-trailing.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-27-tui-running-glyph-smooth-fade.md
2026-07-27-tui-running-glyph-smooth-fade.md: 09a944a5e39d407713da5665a3dd32a171a04bc1
2026-07-27-tui-running-glyph-smooth-fade.zh.md: 5fc6a7e490670ce02d011651741a952cfb97dcce
@@ -1,6 +1,7 @@
# Agent Note: Dim-gray pulse for the running prompt glyph
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-27-tui-running-glyph-smooth-fade.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: Dim-gray pulse for the running prompt glyph
Status: implemented
Archived: 2026-08-04
[English](2026-07-27-tui-running-glyph-smooth-fade.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-27-tui-tool-card-header.md
2026-07-27-tui-tool-card-header.md: f054ca378481be477ac2d261423c3e6af2035b2a
2026-07-27-tui-tool-card-header.zh.md: bf3bf39662f8cc04a28d8f5ea411077bb190c59a
@@ -1,6 +1,7 @@
# Agent Note: Fixed `Tool / <name>` header for tool-call cards
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-27-tui-tool-card-header.zh.md)
@@ -32,4 +33,4 @@ A tool call now shows its identity in one stable place, and status reads as one
## Testing
`packages/ui/tui/tests/tui.spec.ts` pins the new header (`Tool / <name>`), the dropped diff title, the relocated generic title, and the `· N file(s)` footer. The keyless terminal snapshots under `packages/ui/tui/tests/snapshots/` and `examples/tui-agent/tests/snapshots/` — rendered through the real assembled TUI and a pseudo-terminal — were re-recorded and show the new cards for read, bash (described and undescribed), edit, and the other tools.
`packages/ui/tui/tests/tui.spec.ts` pins the new header (`Tool / <name>`), the dropped diff title, the relocated generic title, and the `· N file(s)` footer. Package semantic snapshots cover the card families in a headless terminal. The deleted application journeys formerly supplied assembled tool executions; a future terminal deployment owns equivalent transcript coverage.
@@ -1,6 +1,7 @@
# Agent Note: Fixed `Tool / <name>` header for tool-call cards
Status: implemented
Archived: 2026-08-04
[English](2026-07-27-tui-tool-card-header.md) | 中文
@@ -32,4 +33,4 @@ TUI 曾把每次工具调用渲染为 `{glyph} {title}`,其中 `title` 是 pre
## Testing
`packages/ui/tui/tests/tui.spec.ts` 固定了新表头(`Tool / <name>`)、弃用的 diff 标题、迁移后的 generic 标题以及 `· N file(s)` 页脚。`packages/ui/tui/tests/snapshots/``examples/tui-agent/tests/snapshots/` 下的无密钥终端快照——经由真实组装的 TUI 与伪终端渲染——已重新录制,展示了 read、bash(有描述与无描述)、edit 及其他工具的新卡片
`packages/ui/tui/tests/tui.spec.ts` 固定了新表头(`Tool / <name>`)、弃用的 diff 标题、迁移后的 generic 标题以及 `· N file(s)` 页脚。包级语义快照在无界面终端中覆盖各类卡片。已删除的应用流程此前提供组装后的工具执行;未来的终端部署负责提供等价的 transcript 覆盖
@@ -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-28-dsh-guided-skill-session-commands.md
2026-07-28-dsh-guided-skill-session-commands.md: a11807dca9ad1640857cd95a8b528df691b8a27e
2026-07-28-dsh-guided-skill-session-commands.zh.md: e48420ed77496d8c336031473364f9008fccced7
2026-07-28-dsh-guided-skill-session-commands.md: 454d090a55db8987f8e4987aba67deff1b21b1a0
2026-07-28-dsh-guided-skill-session-commands.zh.md: 64dad4e39c6fd3e3341313c3f8dd2b04947695cf
@@ -1,6 +1,7 @@
# Agent Note: `dsh migrate`/`dsh upgrade` seed the first turn with a skill
Status: implemented
Archived: 2026-08-03
English | [中文](2026-07-28-dsh-guided-skill-session-commands.zh.md)
@@ -16,7 +17,7 @@ The seed reuses the existing TUI skill path, not a new one. `createTuiChat` alre
**Freshness is gated in the launcher, not the TUI.** `runSkillSession` always mints a fresh session and provides the slot only when `resumeSessionId === undefined`, so a later `dsh --resume <id>` of that session is an ordinary TUI session with no re-injection. The TUI stays generic: it invokes whatever skill it is handed, once, at startup.
**`migrate`/`upgrade` take no options.** Unlike `meta`, they carry no `--resume`, `--config`, or `-p`; a guided fresh-session entry has nothing to resume or reconfigure. Any leaked default-surface option fails loud, matching the `web`/`meta` rejection pattern in the Commander adapter. The two modes share one `SkillSessionInvocation` discriminant (`mode: 'migrate' | 'upgrade'`); `bin.ts` maps the mode to `dsh-${mode}`.
**`migrate`/`upgrade` take no default-surface options** (`upgrade` additionally carries the [experimental gate](2026-07-31-experimental-subcommand-gate.md)'s `--experimental`). They carry no `--resume`, `--config`, or `-p`; a guided fresh-session entry has nothing to resume or reconfigure. Any leaked default-surface option fails loud, matching the `web`/`meta` rejection pattern in the Commander adapter. The two modes share one `SkillSessionInvocation` discriminant (`mode: 'migrate' | 'upgrade'`); `bin.ts` maps the mode to `dsh-${mode}`.
The `dsh-migrate` skill is bundled under `skills/` (shipped through `DSH_BUNDLED_SKILL_DIR`, like `dsh-upgrade`). It asks which source agent (opencode/pi/Claude Code/Codex) if unstated, then maps each capability — workspace instructions, personal overlay, skills, hooks, MCP, API/env — to its DSH equivalent, grounded in the actual repo surfaces (the `hooks-claude`/`hooks-codex` bridges, `~/.dsh/{config.yaml,.env,AGENTS.md,skills/}`, `AGENTS.md`/`CLAUDE.md`, `mcporter`), and states plainly when a capability has no equivalent.
@@ -1,6 +1,7 @@
# Agent Note`dsh migrate`/`dsh upgrade` 以 skill 播种首轮
# Agent Note: `dsh migrate`/`dsh upgrade` 以 skill 播种首轮
Status: implemented
Archived: 2026-08-03
[English](2026-07-28-dsh-guided-skill-session-commands.md) | 中文
@@ -16,7 +17,7 @@ Status: implemented
**新鲜性在启动器而非 TUI 中把关。** `runSkillSession` 总是创建全新会话,且仅在 `resumeSessionId === undefined` 时提供该槽,因此之后 `dsh --resume <id>` 恢复该会话时是普通 TUI 会话,不会重复注入。TUI 保持通用:它只是把接到的 skill 在启动时调用一次。
**`migrate`/`upgrade` 不接受任何选项**`meta` 不同,它们不带 `--resume``--config``-p`;引导式全新会话入口没有可恢复或可重配置的内容。任何泄漏的默认界面选项都会明确报错,与 Commander 适配器中 `web`/`meta` 的拒绝模式一致。两个 mode 共用一个 `SkillSessionInvocation` 判别式(`mode: 'migrate' | 'upgrade'`);`bin.ts` 将 mode 映射为 `dsh-${mode}`
**`migrate`/`upgrade` 不接受任何默认界面选项**`upgrade` 另带[实验性门槛](2026-07-31-experimental-subcommand-gate.md)的 `--experimental`)。它们不带 `--resume``--config``-p`;引导式全新会话入口没有可恢复或可重配置的内容。任何泄漏的默认界面选项都会明确报错,与 Commander 适配器中 `web`/`meta` 的拒绝模式一致。两个 mode 共用一个 `SkillSessionInvocation` 判别式(`mode: 'migrate' | 'upgrade'`);`bin.ts` 将 mode 映射为 `dsh-${mode}`
`dsh-migrate` skill 内置于 `skills/`(经 `DSH_BUNDLED_SKILL_DIR` 交付,与 `dsh-upgrade` 相同)。若未说明源 agent,它会先询问是哪个(opencode/pi/Claude Code/Codex),再把每项能力——workspace 指令、个人覆盖、skills、hooks、MCP、API/env——映射到对应的 DSH 等价物,并基于仓库实际的表面(`hooks-claude`/`hooks-codex` 桥、`~/.dsh/{config.yaml,.env,AGENTS.md,skills/}``AGENTS.md`/`CLAUDE.md``mcporter`)落地;当某能力无等价物时明确说明。
@@ -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-28-dsh-meta-source-workspace.md
2026-07-28-dsh-meta-source-workspace.md: d65e0e6ff092b63931dd58c52fa76fb76a071dff
2026-07-28-dsh-meta-source-workspace.zh.md: 72a0c65e6eeda7d63dccc2306a3d7625cdb54362
2026-07-28-dsh-meta-source-workspace.md: 1e433bb50ae5cd5cc893e867d0f4b6edce2bf168
2026-07-28-dsh-meta-source-workspace.zh.md: 95b1edabbbac2e78d09097db614ee4e5d1b33bf7
@@ -0,0 +1,42 @@
# Agent Note: `dsh meta` boots the TUI over the harness checkout
Status: implemented
Archived: 2026-08-03
English | [中文](2026-07-28-dsh-meta-source-workspace.zh.md)
## Problem
`dsh` treats the invoking directory as the workspace, which is what makes it useful on arbitrary projects. Working on dsh itself therefore means `cd`-ing to the checkout first — and the checkout is not a memorable path: the source install keeps it under a container directory as a timestamped staging worktree (`~/.dsh/source/staging-<timestamp>`) behind a `current` symlink, so the target moves on every upgrade. The agent is already *told* where its source lives by the `harness:source` prompt section, and the `cordis` toolset can modify that runtime, but the human still had to locate the directory by hand to start a session there.
## Decision
`dsh meta` boots the ordinary TUI with the harness checkout as the workspace, from any directory.
The target is `SOURCE_ROOT` in `apps/cli/src/tui.ts``fileURLToPath(new URL('../../..', import.meta.url))`, three hops up from `apps/cli/{src,lib}` — the same constant the `harness:source` prompt section already names, so the workspace and the path advertised to the model cannot drift. It follows the launcher's real path, so a PATH symlink through `current` resolves to whichever staging worktree is active.
The mechanism is one `process.chdir(workspace)` inside `runTui`, guarded by an optional third parameter that only the `meta` dispatch passes. The cwd *is* the workspace seam in the shipped tree: `examples/tui-agent/cordis.yml` derives the session cwd (`!!js process.cwd()`), the `./.sessions` persistence root, and the HMR watch root (`root: ['.']`) from it, so one chdir moves all three together and meta sessions land in the checkout's gitignored `.sessions/`. It runs after both `.env` layers are loaded — the bin's invoking-directory load and the personal one — so the ambient > project > personal precedence is untouched. `DEFAULT_CONFIG` and `SOURCE_ROOT` are absolute and TUI mode passes no snapshot mode, so config resolution is chdir-independent.
`meta` always starts a fresh session and accepts no default-surface options; its only option is the [experimental gate](2026-07-31-experimental-subcommand-gate.md)'s `--experimental`. `--config` would boot a foreign tree against the harness workspace, which is the default surface's `--config` case rather than this command; `-p` is not interactive, and resume re-enters the persisted session's own workspace through `dsh --resume <id>`. Any leaked default-surface option fails loud.
## Testing
`apps/cli/tests/args.spec.ts` pins routing for `meta`, rejection of every leaked default-surface option, and rejection of the former `experimental-meta` name. The dispatch itself is composition inside `bin.ts`'s existing `v8 ignore` block.
There is no keyless PTY smoke for this mode. The smoke harness gives each run a temp cwd, but `dsh meta` deliberately chdirs to the real checkout, so a smoke would write `.sessions/` into the live tree mid-test. Covering it properly needs an injectable target directory — a test-only seam this note declines to add for a one-line chdir.
The mode was verified interactively instead. Launched from `$HOME`, a `pwd` tool call reports the checkout, git resolves to its branch, the session log lands under the checkout's `.sessions/` (leaving `~/.sessions` untouched and the tree free of unignored residue), and plain `dsh` from another directory still uses the invoking one.
## Alternatives considered
**Thread an explicit workspace through `boot` and the config tree.** Avoids mutating process-wide state, but the shipped config reads the cwd in three places (`!!js process.cwd()`, `persistenceRoot`, HMR `root`), so each would need its own new plumbing and config key to stay consistent. `chdir` before boot expresses "this is the workspace" once, at the seam that already means it.
**An `--experimental-meta` flag on the default surface.** Rejected: the default surface is option-only so that subcommands do not collide with a positional, and a flag that silently relocates the workspace reads as a modifier of the current directory rather than a different target. `meta` alongside `web` matches the existing shape.
**Resolve `~/.dsh/source/current` instead of the launcher's own path.** Rejected: it would diverge from the `harness:source` prompt path whenever a non-installed checkout's `bin/dsh` is invoked directly, telling the model one source root while working in another.
## Consequences
Starting a session on dsh's own source is `dsh meta --experimental` from anywhere (or bare `dsh meta` under `DSH_EXPERIMENTAL=1`), and the workspace is guaranteed to be the same checkout the model is told about. The command always starts fresh; an ordinary `dsh --resume <id>` later restores the session and enters its persisted workspace.
`runTui` gains an optional third parameter, so the workspace override is visible at the one function that owns TUI composition rather than hidden in a second copy of it.
@@ -0,0 +1,42 @@
# Agent Note: `dsh meta` 以 harness 检出为 workspace 启动 TUI
Status: implemented
Archived: 2026-08-03
[English](2026-07-28-dsh-meta-source-workspace.md) | 中文
## Problem
`dsh` 把调用目录视为 workspace,这正是它能作用于任意项目的原因。但因此,开发 dsh 自身就得先 `cd` 到检出目录——而该目录并不是一个好记的路径:源码安装会把它放在一个容器目录下、作为带时间戳的 staging 工作树(`~/.dsh/source/staging-<timestamp>`),并由 `current` 符号链接指向,因此每次升级后目标都会变化。`harness:source` 提示词段已经*告知* agent 其源码位置,`cordis` 工具集也能修改该运行时,但人类仍需手工定位该目录才能在其中开始会话。
## Decision
`dsh meta` 在任意目录下都以 harness 检出为 workspace 启动普通 TUI。
目标是 `apps/cli/src/tui.ts` 中的 `SOURCE_ROOT`——`fileURLToPath(new URL('../../..', import.meta.url))`,从 `apps/cli/{src,lib}` 向上三级——与 `harness:source` 提示词段所用的常量完全相同,因此 workspace 与告知模型的路径不可能发生偏离。它跟随启动器的真实路径,所以经由 `current` 的 PATH 符号链接会解析到当前生效的那个 staging 工作树。
机制是 `runTui` 内的一次 `process.chdir(workspace)`,由一个可选第三参数把守,只有 `meta` 分派会传入。在已交付的配置树中,cwd *就是* workspace 的接缝:`examples/tui-agent/cordis.yml` 由它派生出会话 cwd`!!js process.cwd()`)、`./.sessions` 持久化根目录以及 HMR 监视根目录(`root: ['.']`),因此一次 chdir 会让三者一并移动,meta 会话则落在检出目录中被 gitignore 的 `.sessions/` 内。它在两层 `.env` 都加载之后执行——bin 对调用目录的加载与个人层加载——因此“环境中已有的值 > 项目 > 个人”的优先级不受影响。`DEFAULT_CONFIG``SOURCE_ROOT` 都是绝对路径,且 TUI 模式不传 snapshot mode,所以配置解析与 chdir 无关。
`meta` 始终启动新会话,且不接受任何默认界面选项;它唯一的选项是[实验性门槛](2026-07-31-experimental-subcommand-gate.md)的 `--experimental``--config` 会针对 harness workspace 启动其他配置树,那是默认界面的 `--config` 场景,而不是该命令的场景;`-p` 并非交互式,恢复则通过 `dsh --resume <id>` 重新进入已持久化会话自身的 workspace。任何泄漏的默认界面选项都会明确报错。
## Testing
`apps/cli/tests/args.spec.ts` 钉住 `meta` 的路由、对每个泄漏的默认界面选项的拒绝,以及对旧名称 `experimental-meta` 的拒绝。该分派本身是 `bin.ts` 既有 `v8 ignore` 块内的组合代码。
该 mode 没有 keyless PTY 冒烟测试。冒烟框架会为每次运行提供临时 cwd,但 `dsh meta` 刻意 chdir 到真实检出目录,因此冒烟测试会在测试中途把 `.sessions/` 写入实际工作树。要正确覆盖它需要一个可注入的目标目录——为了一行 chdir 而引入的测试专用 seam,本 note 不予采纳。
取而代之的是交互式验证。从 `$HOME` 启动后,`pwd` 工具调用报告的是该检出目录,git 解析到其分支,会话日志落在该检出的 `.sessions/` 下(`~/.sessions` 未被触及,工作树也没有未被忽略的残留),并且从其他目录运行的普通 `dsh` 仍使用调用目录。
## Alternatives considered
**通过 `boot` 与配置树显式传递 workspace。** 这可避免修改进程级状态,但已交付的配置在三处读取 cwd(`!!js process.cwd()``persistenceRoot`、HMR `root`),每一处都需要各自新增管线与配置键才能保持一致。启动前 chdir 只在本就表达该含义的接缝上表达一次“这就是 workspace”。
**在默认界面上加一个 `--experimental-meta` 标志。** 拒绝:默认界面是纯选项形式,以免子命令与位置参数冲突;而一个会静默改变 workspace 的标志读起来像是对当前目录的修饰,而非另一个目标。`meta``web` 并列符合既有形态。
**解析 `~/.dsh/source/current` 而非启动器自身路径。** 拒绝:当直接调用某个非安装检出的 `bin/dsh` 时,它会与 `harness:source` 提示词路径产生偏离——告知模型一个源码根目录,却在另一个目录中工作。
## Consequences
在 dsh 自身源码上开启会话变成了在任意位置执行 `dsh meta --experimental`(在 `DSH_EXPERIMENTAL=1` 下可直接执行 `dsh meta`),且该 workspace 必然就是告知模型的那个检出目录。该命令始终启动新会话;之后,普通的 `dsh --resume <id>` 会恢复该会话并进入其已持久化的 workspace。
`runTui` 新增一个可选第三参数,因此 workspace 覆盖是在拥有 TUI 组合逻辑的那唯一一个函数上可见的,而不是隐藏在它的第二份副本中。
@@ -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
2026-07-20-windows-tui-support.md: 6b728486dd50faac067933ce06f883447aae821f
2026-07-20-windows-tui-support.zh.md: 2b53b05ff6231361d79b4304181dc0e6d8e24e68
2026-07-29-addressable-queue-operations.md: 258e4e4e50a4c6386a2e8c28402fe32cb8870e1a
2026-07-29-addressable-queue-operations.zh.md: b4370d87892df2e0477c0897b45b4c7a1030978e
@@ -1,6 +1,7 @@
# Agent Note: Address pending queue occurrences for edit and removal
Status: implemented
Archived: 2026-07-31
English | [中文](2026-07-29-addressable-queue-operations.zh.md)
@@ -1,6 +1,7 @@
# Agent Noteagent 决策记录):为待处理队列项提供编辑与移除操作
# Agent Note: 为待处理队列项提供编辑与移除操作
Status: implemented
Archived: 2026-07-31
[English](2026-07-29-addressable-queue-operations.md) | 中文
@@ -0,0 +1,6 @@
# 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/archived/feature/2026-07-29-tui-hidden-mode-assistant-fold.md
2026-07-29-tui-hidden-mode-assistant-fold.md: 53cd3d812212f8a72c56165c37b806cae58f37f6
2026-07-29-tui-hidden-mode-assistant-fold.zh.md: cf3644478588bbba955fb6539dd99744ef6eb84c
@@ -0,0 +1,26 @@
# Agent Note: TUI hidden mode folds a turn's assistant steps into one message
Status: implemented
Archived: 2026-08-04
English | [中文](2026-07-29-tui-hidden-mode-assistant-fold.zh.md)
## Problem
Ctrl+O's hidden phase ([consolidated TUI presentation](../architecture/2026-07-28-consolidated-tui-presentation.md)) drops tool cards so the transcript reads as a conversation, but each model step still rendered its own `Assistant` header. A multi-step turn (text → tools → text) therefore showed several consecutive `Assistant` blocks with nothing between them — the removed tool cards were the only thing that had justified the repeated headers. Codex-style conversation-only reading wants one assistant message per turn.
## Decision
Hidden mode is also a fold rule, applied purely as TUI presentation: per turn, the first step whose rendered content is visible (text, or reasoning while reasoning display is on) owns the turn's single `Assistant` header; every other step renders as a headerless continuation, and a step with no visible body renders nothing at all — a tool-only step neither consumes the header nor leaves a blank segment. Collapsed and expanded phases keep per-step headers; leaving hidden restores them.
Mechanics: `StreamingAssistantComponent` carries its `StepPosition` and a `setFoldedContinuation` presentation flag; `createTuiChat` keeps a per-turn list of step components and re-derives the fold on Ctrl+O, on each streamed text/reasoning chunk, on message settle, and on retraction of a failed stream (which may hand the header to the next step). Transcript rebuild clears the map and replays the log, so resume, compaction replacement, resize, and theme swaps converge on the same fold. Step timing footers keep their per-step ownership and are unaffected.
## Alternatives considered
- **Merge steps into one component** — collides with per-step streaming lifecycle, retry retraction, and timing footers; the flag on existing components changes only the header/spacer.
- **Fold in the session log or `deriveMessages`** — mutates durable/model-visible history for a UI reading mode; the log stays step-shaped.
- **Always fold (all visibility phases)** — collapsed/expanded interleave tool cards between steps, where per-step headers delimit which output belongs to which step.
## Consequences
Hidden mode now reads as one assistant message per turn; turns stay separated by their headers. The fold is recomputed state, never stored, so no session or persistence format changes. Coverage: TUI unit specs for the Ctrl+O cycle header counts, tool-only first step header handoff, per-turn separation, and live streaming + rebuild convergence; keyless snapshot `tool-cards-hidden-folded` pins the folded frame.

Some files were not shown because too many files have changed in this diff Show More