DSH / Atlas
2026-07-24implementedfeaturearchived 2026-08-04

TUI prompt themes compose mutable plugin values

TUI 提示符主题组合可变的插件值

The terminal prompt row and editor prefix were assembled inside the TUI from a fixed set of workspace, model, usage, cache, context, and timing fields. Deployments could change colors globally but could not choose field order, replace the input prefix, add plugin state, or build a Powerline prompt.

English

Problem

The terminal prompt row and editor prefix were assembled inside the TUI from a fixed set of workspace, model, usage, cache, context, and timing fields. Deployments could change colors globally but could not choose field order, replace the input prefix, add plugin state, or build a Powerline prompt.

Decision

The TUI theme groups color, truecolor, leftPrompt, rightPrompt, inputPrompt, and the static running-state inputPlaceholder. The three prompt strings interpolate ${name} references; unknown or unavailable values disappear with adjacent horizontal separator whitespace. The left and right templates share one row, retain the right side on overlap, and use ANSI-aware visible widths. The input template controls the first-line editor prefix and continuation indentation.

ctx.tuiPrompt is a context-global registry supplied by @deepseek-ai/dsh-tui/prompt. register(name, initialValue) returns a handle with set(value) and dispose(). Values are stored strings rather than callbacks: updates are explicit, unchanged strings are ignored, and a registration, mutation, or disposal schedules one coalesced notification. The renderer reads current values with get(name) and subscribes with subscribe(listener) to learn when to redraw. That subscription is a direct in-service callback, not a Cordis event, so a value changing on its own schedule still repaints without a bus entry other consumers would never use. Both subscribe and each registration are owned by the caller's Cordis effect, so they are removed when the subscriber's or contributor's fiber disposes. Each subscribe call is a distinct subscription keyed by record identity, so two fibers may pass the same callback and disposing one leaves the other live. The coalesced notification contains every observer — a synchronous throw, a rejected returned promise, and even an error hostile to string rendering (logs go through the non-throwing errorChain) — so one broken observer cannot starve the rest, and it re-checks each subscription's liveness during delivery so a listener that synchronously unsubscribes another in the same burst silences it immediately. Registration follows Cordis effect ownership, rejects duplicate names, and removes the value on plugin disposal.

Registered fragments are trusted ANSI-capable presentation output. Template literals and ordinary external content remain sanitized, but a prompt-value plugin may emit terminal controls. Composite values own coordinated background transitions and separators, so one ${powerline} value can render a complete Powerline segment without coupling adjacent atomic providers.

The built-in cwd, git/worktree, token_meter/cache_hit_rate, model, context, queued, styled symbol label, and indicator caret values use the same registry. Session and agent events update their handles, while the running timer updates queued — the steering-queue badge, unavailable unless a running turn has queued messages — and the animated indicator each tick. The shipped input template is ${symbol} ${indicator}, preserving the existing dsh > prefix.

Alternatives considered

Evaluate synchronous provider callbacks on every render. Rejected: render-time plugin code adds an avoidable failure boundary; stored strings keep the render pass free of plugin evaluation.

Publish the change notification as a Cordis event. Rejected: the notification has exactly one consumer (the TUI renderer for the current session), so a global typed event adds a bus entry, scoped-dispatch surface, and cross-plugin fan-out no one else observes. A direct subscribe callback contained inside the service carries the same coalesced redraw with less surface.

Expose semantic style roles instead of ANSI. Rejected: semantic roles cannot express arbitrary Powerline background transitions without expanding the shared style protocol for each presentation technique.

Put prompt fields at the top level of TUI config. Rejected: templates and color selection jointly define terminal presentation and belong under one theme object.

Consequences

Prompt contributors depend on the TUI-specific registry and are loaded after the service but before the TUI consumer. The namespace is global to the Cordis context, matching the TUI's current single-session transcript ownership. Arbitrary ANSI is intentionally trusted: unsupported cursor-affecting sequences can disrupt layout, and alignment is reliable only for sequences understood by pi-tui's visible-width utilities.

Changing inputPrompt through a registered value preserves editor text, cursor, history, completion, and focus because pi-tui supports replacing equal-width first and continuation prefixes in place. The static inputPlaceholder is sanitized and appears only while the agent runs and the editor is empty.

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 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.

中文

问题

终端提示符行与编辑器前缀原先在 TUI 内部由一组固定字段拼装而成,涵盖工作区、模型、用量、缓存、上下文与计时。部署方可以全局更改颜色,却无法调整字段顺序、替换输入前缀、加入插件状态,也无法构建 Powerline 风格的提示符。

决策

TUI 主题把 colortruecolorleftPromptrightPromptinputPrompt 以及运行状态下的静态 inputPlaceholder 归为一组。三个提示符字符串通过插值引用 ${name};未知或不可用的值连同相邻的横向分隔空白一起消失。左右模板共用一行,重叠时保留右侧,宽度计算使用可识别 ANSI 的可见宽度。输入模板控制编辑器首行前缀与续行缩进。

ctx.tuiPrompt 是由 @deepseek-ai/dsh-tui/prompt 提供的上下文全局注册表。register(name, initialValue) 返回带 set(value)dispose() 的句柄。存储的值是字符串而非回调:更新必须显式发起,未变化的字符串会被忽略,而一次注册、变更或 dispose 会安排一次合并后的通知。渲染器用 get(name) 读取当前值,并用 subscribe(listener) 订阅何时重绘。该订阅是服务内部的直接回调,而非 Cordis 事件,因此一个自行变化的值仍能重绘,而不需要一个其他消费方永远不会观察的总线条目。subscribe 与每个注册都由调用方的 Cordis effect 拥有,因此在订阅方或贡献方的 fiber dispose 时一并移除。每次 subscribe 都是一个按记录身份区分的独立订阅,因此两个 fiber 可以传入同一个回调,而 dispose 其中一个不会影响另一个。合并通知会容错每个观察者——同步抛出、返回被拒 promise,甚至一个对字符串渲染也会抛异常的错误(日志走不抛异常的 errorChain)——因此一个损坏的观察者不会饿死其余观察者;并且在派发过程中会重新校验每个订阅的存活性,因此同一批次中同步取消了另一个订阅的监听器会立即使其静默。注册遵循 Cordis 的 effect 所有权模型,拒绝重复名称,并在插件 dispose(资源释放)时移除对应的值。

注册的片段被视为可信的、允许携带 ANSI 的呈现输出。模板中的字面文本与普通外部内容仍会被清洗,但提供提示符值的插件可以输出终端控制序列。复合值自行负责协调背景色过渡与分隔符,因此一个 ${powerline} 值就能渲染完整的 Powerline 段,而无需与相邻的原子提供方耦合。

内置的 cwdgit/worktreetoken_meter/cache_hit_ratemodelcontextqueued、带样式的 symbol 标签与 indicator 光标符值使用同一个注册表。会话与 agent(智能体)事件更新各自的句柄,运行计时器每一拍更新 queued——转向队列徽标,仅在运行中的一轮有排队消息时才可用——与带动画的 indicator。随附的输入模板为 ${symbol} ${indicator},保留了原有的 dsh > 前缀。

曾考虑的替代方案

每次渲染时求值同步的提供方回调。 不予采纳:在渲染期执行插件代码会引入一个本可避免的故障边界;存储字符串能让渲染过程不涉及插件求值。

把变更通知发布为 Cordis 事件。 已否决:该通知只有一个消费方(当前会话的 TUI 渲染器),因此全局类型事件会增加一个总线条目、scope 分发面以及无人观察的跨插件扇出。服务内部包裹的直接 subscribe 回调以更小的面积承载同样的合并重绘。

暴露语义化的样式角色而非 ANSI。 不予采纳:语义角色无法表达任意的 Powerline 背景色过渡,除非为每种呈现技巧扩展共享的样式协议。

把提示符字段放在 TUI 配置顶层。 不予采纳:模板与颜色选择共同定义终端呈现,应归属于同一个 theme 对象之下。

后果

提示符值的贡献插件依赖 TUI 专属的注册表,加载顺序位于该服务之后、TUI 消费方之前。命名空间对整个 Cordis 上下文全局生效,与 TUI 当前的单会话 transcript(文本记录)所有权一致。允许任意 ANSI 是有意的信任决策:不受支持的、影响光标的序列可能破坏布局,只有 pi-tui 可见宽度工具能理解的序列才能保证对齐可靠。

通过注册值更改 inputPrompt 时,编辑器文本、光标、历史、自动补全与焦点均得以保留,因为 pi-tui 支持原地替换等宽的首行与续行前缀。静态的 inputPlaceholder 会被清洗,且仅在 agent 运行且编辑器为空时显示。

测试

注册表测试固定校验、重名拒绝、更新、不可用值、合并通知的容错、取消订阅、dispose、插值、尾随字面保留、空白清理与 ANSI 保留等行为。TUI 包(package)测试固定服务可用性、嵌套主题默认值、配置转发、自定义模板、带外值重绘、可变重绘、支持 Powerline 的片段、动态输入前缀宽度以及运行状态下的静态占位文本。交付 TUI 的部署负责组装后的加载顺序验收。