DSH / Atlas
2026-07-21implementedfeaturearchived 2026-07-26

Auto-titled terminal from the first message

从首条消息自动命名终端

The TUI's terminal title is a single static string (`title`, default `DeepSeek Harness`) shared by every session. A user who runs one agent per tmux pane or terminal tab sees the same label on all of them, so panes are indistinguishable at a glance and the tab bar carries no signal about what each session is doing.

English

Problem

The TUI's terminal title is a single static string (title, default DeepSeek Harness) shared by every session. A user who runs one agent per tmux pane or terminal tab sees the same label on all of them, so panes are indistinguishable at a glance and the tab bar carries no signal about what each session is doing.

Decision

  • TuiConfig gains an autoTitle boolean (default false). When it is on, the TUI issues one background model call after the first user message of a fresh session and replaces the terminal title with a short, model-generated label; the static title is the pre-title and the fallback.
  • The label is a model summary, not a truncation of the prompt. The request carries a fixed task instruction (summarize the request as a short title of two to five lowercase words, no punctuation) plus the user's first message and no tools; the TUI takes the first non-empty line of the reply and caps it at 40 characters (39 plus an ellipsis).
  • The title is set through runtime.terminal.setTitle, the same OSC 0 path the static title already uses. No new terminal-control surface is introduced, and pi-tui keeps ownership of terminal writes.
  • The call is fire-and-forget and one-shot per session. A titleSettled latch guards it: with autoTitle off it is pre-settled and never runs; on a resumed session whose first user/message is already logged it is pre-settled so the static title stands; a whitespace-only first message is skipped without consuming the slot. Any failure, an empty reply, a missing llm service, or a missing agent provider/model leaves the static title untouched. A dedicated AbortController cancels an in-flight request on shutdown.
  • The title call reaches ctx.llm.stream directly rather than through agent.send, so it never appends to the session or transcript and cannot perturb the agent loop.
  • The feature defaults off and is enabled only in the interactive product config (examples/tui-agent/cordis.yml) and the scripted PTY fixture. Enabling it in the shared dsh-tui-demo schema default would fire an extra model call in keyless replay and boot scenarios that send no user message.

Alternatives considered

Truncate the first user message instead of a model title. Rejected: the user chose a short model-made label; a truncated raw prompt is noisy, often begins with boilerplate, and rarely reads as a title.

Rename the window (OSC 2) or the tmux window. Rejected: OSC 0 sets only pane_title, so it labels the pane without renaming or leaking into the user's window title; the user confirmed OSC is the right lever.

Default the feature on. Rejected: enabling it in the shared demo schema perturbs keyless replay and boot snapshots and spends a model call on every fresh session; opt-in per deployment keeps the default surface inert.

Fold this into the log-backed session-title work (PR #451). Rejected: that change is session metadata persisted to the log; this is a terminal label with no persistence. Keeping them independent leaves each self-contained and avoids a shared dependency.

Block the first turn until the title resolves. Rejected: awaiting the title before sending the user's message adds latency to the actual request; fire-and-forget makes the rename invisible to the turn.

Consequences

  • When enabled, a fresh session spends one extra, tool-less model call with a single short user message and a few output tokens; off by default, it costs nothing.
  • Because the title call stamps sessionId, it shares the session's llm-replay cursor: enabling autoTitle in a replay-backed snapshot scenario would consume a recorded script entry. This is why the default is off and the scripted PTY fixture answers the call with a tool-branching adapter rather than replay.
  • packages/ui/tui/tests/tui.spec.ts pins the behavior with a mock llm adapter: a generated title replaces the static one, over-long output is truncated with an ellipsis, a whitespace-only first message keeps the one-shot slot, empty or failing replies leave the title, a resumed session never fires, and the feature-off / no-service / missing-provider / missing-model paths keep the static title. A shutdown test asserts the in-flight request is aborted.
  • examples/tui-agent/tests/tui-keyless-smoke.e2e.ts proves the real Loader-booted path: the scripted adapter answers the tool-less title call with a fixed string, and the conversation scenario asserts the OSC 0 sequence reaches the PTY. Boot scenarios send no user message, so they never fire the call.

中文

Problem

TUI 的终端标题是一个所有会话共用的静态字符串(title,默认 DeepSeek Harness)。在 tmux 每个窗格或每个终端标签页各跑一个 agent(智能体)的用户看来,它们的标签全都一样,因此窗格一眼看去无从区分,标签栏也不携带任何关于各会话正在做什么的信号。

Decision

  • TuiConfig 新增布尔字段 autoTitle(默认 false)。开启后,TUI 会在全新会话的首条用户消息之后发起一次后台模型调用,并用一个简短的、模型生成的标签替换终端标题;静态 title 是替换前的初值,也是兜底。
  • 该标签是模型概括,而非对提示词的截断。请求携带一段固定的任务指令(将该请求概括为两到五个小写单词、不含标点的简短标题)加上用户的首条消息,且不带工具;TUI 取回复的首个非空行并截断到 40 个字符(39 个字符加一个省略号)。
  • 标题通过 runtime.terminal.setTitle 设置——静态 title 已经在用的同一条 OSC 0 路径。不引入任何新的终端控制面,终端写入仍归 pi-tui 所有。
  • 该调用发出后不等待其返回,且每会话仅一次。一个 titleSettled 门闩守护它:autoTitle 关闭时它预先置为已结算、从不运行;在首条 user/message 已入日志的恢复会话中它预先结算,因此静态标题得以保留;仅含空白的首条消息被跳过且不消耗名额。任何失败、空回复、缺少 llm 服务、或缺少 agent 的 providermodel,都会让静态标题保持不动。一个专用的 AbortController 在关闭时取消尚在进行的请求。
  • 标题调用直接抵达 ctx.llm.stream,而非经由 agent.send,因此它从不追加进会话或 transcript(文本记录),也无法扰动 agent loop(智能体循环)。
  • 该功能默认关闭,仅在交互式产品配置(examples/tui-agent/cordis.yml)与脚本化 PTY fixture(测试前置数据)中开启。若在共享的 dsh-tui-demo schema 默认值里开启,会在不发送任何用户消息的无密钥回放与启动场景中多发一次模型调用。

Alternatives considered

截断首条用户消息,而非用模型生成标题。 否决:用户选择的是简短的、模型制作的标签;截断后的原始提示词嘈杂、常以样板文字开头,且很少读起来像标题。

重命名窗口(OSC 2)或 tmux 窗口。 否决:OSC 0 只设置 pane_title,因此它标记窗格而不重命名、也不泄漏进用户的窗口标题;用户确认 OSC 是正确的手段。

让该功能默认开启。 否决:在共享的 demo schema 里开启会扰动无密钥回放与启动快照,并在每个全新会话上花掉一次模型调用;按部署选择性开启可让默认面保持惰性。

并入日志支撑的会话标题工作(PR #451)。 否决:那项改动是持久化到日志的会话元数据;本项是不做持久化的终端标签。让二者相互独立可使各自自成一体,并避免共享依赖。

阻塞首轮直到标题就绪。 否决:在发送用户消息前先等待标题,会给实际请求增加延迟;发出后不等待其返回可让重命名对该轮次不可见。

Consequences

  • 开启时,全新会话会多花一次无工具的模型调用,只带单条简短的用户消息和少量输出 token;默认关闭时它不产生任何开销。
  • 由于标题调用会打上 sessionId,它与会话的 llm-replay 游标共享:在以回放支撑的快照场景中开启 autoTitle 会消耗一条录制脚本条目。这正是它默认关闭、且脚本化 PTY fixture 用按工具分支的适配器而非回放来回答该调用的原因。
  • packages/ui/tui/tests/tui.spec.ts 用一个 mock llm 适配器固定该行为:生成的标题替换静态标题、过长输出以省略号截断、仅含空白的首条消息保留一次性名额、空回复或失败回复保留标题、恢复的会话从不触发,以及功能关闭 / 无服务 / 缺提供方 / 缺模型各路径都保留静态标题。一项关闭测试断言尚在进行的请求被中止。
  • examples/tui-agent/tests/tui-keyless-smoke.e2e.ts 证明真实的经 Loader 启动的路径:脚本化适配器以固定字符串回答无工具的标题调用,对话场景断言 OSC 0 序列抵达 PTY。启动场景不发送用户消息,因此它们从不触发该调用。