DSH / Atlas
2026-07-27implementedarchitecturearchived 2026-08-04

dsh-tui chat channel module split

dsh-tui 聊天通道模块拆分

`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 pri

English

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.tscreateModelController: 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.tscreateQuestionQueue: the user-interaction provider and the one-at-a-time FIFO ask-user-question overlays.
  • chat/resume.tscreateResumeController: 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.tsChatChannelDeps (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.

中文

Problem

packages/ui/tui/src/index.ts 已超过 2000 行,其中绝大部分是单个 createTuiChat 工厂:一个约 1600 行的闭包,持有约四十个可变变量以及同等数量的嵌套闭包。模型选择、ask-user-question 队列、会话恢复都缠绕在这一个作用域里,读者无法在不把整份文件装进脑子的前提下理清任何单一关注点,互不相关的改动也会彼此冲突。此前一轮已把 src/ 归组为 components/session/extension/,但入口文件本身以及散落在顶层的输入相关文件(autocomplete.tsfile-autocomplete.tsskill-invocation.tsxml-tool-output.ts)未动。

Decision

聊天通道内聚的子机制从 createTuiChat 中抽出,迁入 src/chat/,每个都是接收显式依赖包的工厂,而非闭包捕获入口作用域:

  • chat/model-command.tscreateModelController:排队执行的 /model 命令、模型加推理力度(reasoning-effort)的选择浮层,以及所选模型上下文窗口的解析。持有供提示行与状态视图读取的上下文窗口缓存。
  • chat/questions.tscreateQuestionQueue:user-interaction provider 以及一次仅一个的 FIFO ask-user-question 浮层。
  • chat/resume.tscreateResumeController/resume 选择器、逐候选摘要读取、交接前预检、终端交接,以及持久化的恢复提示命令。
  • chat/helpers.ts — 无状态辅助函数(formatCwdgitBranch、surface/工具调用派生、会话引用卡片)、HintEditor,以及横幅揭示常量。
  • chat/channel.tsChatChannelDeps(每个子控制器共享的协作者面)与 ChannelNotice(由需要上报结果的控制器混入)。各 *Deps 继承它们,使共享面只有一处定义。

src/ 随之重组,使 chat/ 汇集所有聊天通道关注点:上述子控制器,加上原来的输入文件与原 session/ 文件(timing.tstokens.ts)都迁到 chat/ 之下。xml-tool-output.ts 迁到 components/ 之下。宿主/进程边界接口(TuiRuntimeTuiResumeHost)迁到 src/runtime.ts。拆分后 src/chat/components/extension/,以及顶层的 index.ts / config.ts / prompt.ts / runtime.ts / invariant.tsindex.ts 从 2067 行降至约 1530 行,现负责构造并接线这三个控制器。

控制器依赖包的约定:稳定的取值型协作者(ctxresolvedpaletteoverlayManager,以及各控制器自有的服务)一次性解构;通道回调(appendNoticerequestRenderisDisposedagentStatus)保留在 deps 上,使控制器始终调用通道当前的实现。channel.ts 的 JSDoc 陈述了此规则。

Alternatives considered

  • 接收共享可变上下文对象的自由函数。 否决:那会把拆分本要消除的四十字段大杂烩,仅换个参数名重新暴露出来。
  • 同时抽出状态/计时动画控制器。 推迟:runningStatusupdatePromptValues 中的提示光标动画直接读取,在此设控制器边界会让其内部状态经 getter 反向泄漏——收益甚微的漏隙缝。它继续内联在 index.ts 中。

Consequences

每个关注点现可独立阅读与测试,共享依赖面只定义一次,而非复制进三个接口。代价:index.ts 负责构造这些控制器并穿针引线地传入回调包;模型控制器是 let 前向引用(updatePromptValues 闭包捕获它,但它要待 appendNotice/overlayManager 就绪后才构造),因而带一处有正当理由的 prefer-const 禁用与一次延后的首帧绘制。

Testing

行为不变:现有的包测试与 TUI 快照全部无需重录即通过,这正是本次重构的契约。