refactor(tui): split createTuiChat into chat/ sub-controllers
Extract model-command, questions, and resume sub-machines from the ~1600-line createTuiChat closure into src/chat/ factories that take explicit dependency bundles (shared ChatChannelDeps/ChannelNotice). Reorganize src/ so chat/ holds all chat-channel concerns (former input and session/ files move under it); xml-tool-output moves to components/; TuiRuntime/TuiResumeHost move to runtime.ts. index.ts drops 2067->~1530 lines. Behavior identical: 167 tests and all TUI snapshots pass unchanged.
This commit is contained in:
@@ -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/implemented/architecture/2026-07-27-tui-chat-channel-module-split.md
|
||||
2026-07-27-tui-chat-channel-module-split.md: 56b345b670cd8426780bfdd8c2b2f5719461554c
|
||||
2026-07-27-tui-chat-channel-module-split.zh.md: d74844a762bc519d0f499696fe343567eebfe920
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: dsh-tui chat channel module split
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-27-tui-chat-channel-module-split.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`packages/ui/tui/src/index.ts` had grown past 2000 lines. Most of it was one `createTuiChat` factory: a ~1600-line closure holding roughly forty mutable variables and as many nested closures. Model selection, the ask-user-question queue, and session resume were tangled into that single scope, so a reader could not follow any one concern without holding the whole file in their head, and unrelated edits collided. A prior pass had grouped `src/` into `components/`, `session/`, `extension/`, but the entry file itself and the loose top-level input files (`autocomplete.ts`, `file-autocomplete.ts`, `skill-invocation.ts`, `xml-tool-output.ts`) were untouched.
|
||||
|
||||
## Decision
|
||||
|
||||
The chat channel's cohesive sub-machines are extracted from `createTuiChat` into `src/chat/`, each a factory that takes an explicit dependency bundle instead of closing over the entry scope:
|
||||
|
||||
- `chat/model-command.ts` — `createModelController`: the queued `/model` command, the model+reasoning-effort selector overlay, and the selected model's context-window resolution. Owns the context-window cache that the prompt and status views read.
|
||||
- `chat/questions.ts` — `createQuestionQueue`: the user-interaction provider and the one-at-a-time FIFO ask-user-question overlays.
|
||||
- `chat/resume.ts` — `createResumeController`: the `/resume` selector, per-candidate summary reads, the pre-handoff preflight, the terminal handoff, and the durable resume-hint command.
|
||||
- `chat/helpers.ts` — zero-state helpers (`formatCwd`, `gitBranch`, surface/tool-call derivations, session-reference cards), the `HintEditor`, and banner-reveal constants.
|
||||
- `chat/channel.ts` — `ChatChannelDeps` (the collaborator surface every sub-controller shares) and `ChannelNotice` (mixed in by the controllers that report outcomes). Each `*Deps` extends these, so the shared surface has one definition.
|
||||
|
||||
`src/` is reorganized so `chat/` holds every chat-channel concern: the sub-controllers above plus the former input files and the former `session/` files (`timing.ts`, `tokens.ts`) all move under `chat/`. `xml-tool-output.ts` moves under `components/`. The host/process boundary interfaces (`TuiRuntime`, `TuiResumeHost`) move to `src/runtime.ts`. After the split `src/` is `chat/`, `components/`, `extension/`, and the top-level `index.ts` / `config.ts` / `prompt.ts` / `runtime.ts` / `invariant.ts`; `index.ts` drops from 2067 to ~1530 lines and now constructs and wires the three controllers.
|
||||
|
||||
The convention for a controller's dependency bundle: stable value collaborators (`ctx`, `resolved`, `palette`, `overlayManager`, and each controller's own services) are destructured once; the channel callbacks (`appendNotice`, `requestRender`, `isDisposed`, `agentStatus`) stay on `deps` so a controller always calls the channel's current implementation. `channel.ts`'s JSDoc states this rule.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Free functions taking a shared mutable context object.** Rejected: it would re-expose the same forty-field grab-bag the split set out to remove, just under a parameter name.
|
||||
- **Extracting the status/timing animation controller too.** Deferred: `runningStatus` is read directly by the prompt caret animation in `updatePromptValues`, so a controller boundary there would leak its internal state back through getters — a leaky seam for little gain. It stays inline in `index.ts`.
|
||||
|
||||
## Consequences
|
||||
|
||||
Each concern is now readable and testable in isolation, and the shared dependency surface is defined once instead of copied into three interfaces. The cost: `index.ts` constructs the controllers and threads the callback bundle, and the model controller is a `let` forward-reference (`updatePromptValues` closes over it, but it is built later once `appendNotice`/`overlayManager` exist), carrying one justified `prefer-const` disable and a deferred first paint.
|
||||
|
||||
## Testing
|
||||
|
||||
Behavior is unchanged: all existing package tests and TUI snapshots pass without re-recording, which is the contract for this refactor.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: dsh-tui 聊天通道模块拆分
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-27-tui-chat-channel-module-split.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`packages/ui/tui/src/index.ts` 已超过 2000 行,其中绝大部分是单个 `createTuiChat` 工厂:一个约 1600 行的闭包,持有约四十个可变变量以及同等数量的嵌套闭包。模型选择、ask-user-question 队列、会话恢复都缠绕在这一个作用域里,读者无法在不把整份文件装进脑子的前提下理清任何单一关注点,互不相关的改动也会彼此冲突。此前一轮已把 `src/` 归组为 `components/`、`session/`、`extension/`,但入口文件本身以及散落在顶层的输入相关文件(`autocomplete.ts`、`file-autocomplete.ts`、`skill-invocation.ts`、`xml-tool-output.ts`)未动。
|
||||
|
||||
## Decision
|
||||
|
||||
聊天通道内聚的子机制从 `createTuiChat` 中抽出,迁入 `src/chat/`,每个都是接收显式依赖包的工厂,而非闭包捕获入口作用域:
|
||||
|
||||
- `chat/model-command.ts` — `createModelController`:排队执行的 `/model` 命令、模型加推理力度(reasoning-effort)的选择浮层,以及所选模型上下文窗口的解析。持有供提示行与状态视图读取的上下文窗口缓存。
|
||||
- `chat/questions.ts` — `createQuestionQueue`:user-interaction provider 以及一次仅一个的 FIFO ask-user-question 浮层。
|
||||
- `chat/resume.ts` — `createResumeController`:`/resume` 选择器、逐候选摘要读取、交接前预检、终端交接,以及持久化的恢复提示命令。
|
||||
- `chat/helpers.ts` — 无状态辅助函数(`formatCwd`、`gitBranch`、surface/工具调用派生、会话引用卡片)、`HintEditor`,以及横幅揭示常量。
|
||||
- `chat/channel.ts` — `ChatChannelDeps`(每个子控制器共享的协作者面)与 `ChannelNotice`(由需要上报结果的控制器混入)。各 `*Deps` 继承它们,使共享面只有一处定义。
|
||||
|
||||
`src/` 随之重组,使 `chat/` 汇集所有聊天通道关注点:上述子控制器,加上原来的输入文件与原 `session/` 文件(`timing.ts`、`tokens.ts`)都迁到 `chat/` 之下。`xml-tool-output.ts` 迁到 `components/` 之下。宿主/进程边界接口(`TuiRuntime`、`TuiResumeHost`)迁到 `src/runtime.ts`。拆分后 `src/` 为 `chat/`、`components/`、`extension/`,以及顶层的 `index.ts` / `config.ts` / `prompt.ts` / `runtime.ts` / `invariant.ts`;`index.ts` 从 2067 行降至约 1530 行,现负责构造并接线这三个控制器。
|
||||
|
||||
控制器依赖包的约定:稳定的取值型协作者(`ctx`、`resolved`、`palette`、`overlayManager`,以及各控制器自有的服务)一次性解构;通道回调(`appendNotice`、`requestRender`、`isDisposed`、`agentStatus`)保留在 `deps` 上,使控制器始终调用通道当前的实现。`channel.ts` 的 JSDoc 陈述了此规则。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **接收共享可变上下文对象的自由函数。** 否决:那会把拆分本要消除的四十字段大杂烩,仅换个参数名重新暴露出来。
|
||||
- **同时抽出状态/计时动画控制器。** 推迟:`runningStatus` 被 `updatePromptValues` 中的提示光标动画直接读取,在此设控制器边界会让其内部状态经 getter 反向泄漏——收益甚微的漏隙缝。它继续内联在 `index.ts` 中。
|
||||
|
||||
## Consequences
|
||||
|
||||
每个关注点现可独立阅读与测试,共享依赖面只定义一次,而非复制进三个接口。代价:`index.ts` 负责构造这些控制器并穿针引线地传入回调包;模型控制器是 `let` 前向引用(`updatePromptValues` 闭包捕获它,但它要待 `appendNotice`/`overlayManager` 就绪后才构造),因而带一处有正当理由的 `prefer-const` 禁用与一次延后的首帧绘制。
|
||||
|
||||
## Testing
|
||||
|
||||
行为不变:现有的包测试与 TUI 快照全部无需重录即通过,这正是本次重构的契约。
|
||||
Reference in New Issue
Block a user