feat(tui): animate terminal title while agent is busy - #2238
Conversation
Prefix the terminal title with a spinner while a turn is streaming or compacting, so progress stays visible on the terminal tab even when the window is unfocused. The style is chosen once via KIMI_TITLE_SPINNER (moon by default; also sparkle, braille, orbit, pulse, line, hourglass, a state-mapped phase, and off to disable).
🦋 Changeset detectedLatest commit: a33e03c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f51f495936
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| private syncTerminalTitleSpinner(): void { | ||
| const busy = this.state.appState.streamingPhase !== 'idle' || this.state.appState.isCompacting; |
There was a problem hiding this comment.
Extract terminal-title animation from KimiTUI
This block makes KimiTUI own environment resolution, timer lifecycle, frame selection, and title rendering even though the behavior is self-contained and independently testable. Move this state machine into a controller with observable title/timer tests, leaving the coordinator responsible only for syncing and disposing it.
AGENTS.md reference: apps/kimi-code/AGENTS.md:L38-L39
Useful? React with 👍 / 👎.
| this.tasksBrowserController.close(); | ||
| this.btwPanelController.clear(); | ||
| this.stopActivitySpinner(); | ||
| this.stopTerminalTitleSpinner(); |
There was a problem hiding this comment.
Restore the static terminal title during shutdown
When double Ctrl-D or SIGTERM invokes stop() while a turn or compaction is active, this only clears the interval and leaves its last animated frame as the terminal title. Neither TUI.stop() nor ProcessTerminal.stop() replaces the title, so terminals whose shells do not set their own prompt title continue to display a stale busy indicator after Kimi exits; write the static title before terminal teardown.
Useful? React with 👍 / 👎.
| readonly interval: number; | ||
| } | ||
|
|
||
| export const TITLE_SPINNER_ENV = 'KIMI_TITLE_SPINNER'; |
There was a problem hiding this comment.
Document the new spinner environment variable
The repository's English and Chinese environment-variable references present themselves as the complete list of runtime variables, but KIMI_TITLE_SPINNER and its supported values appear only in the implementation and this release changeset. Once the release entry is no longer prominent, users cannot discover how to select a style or disable the new default animation; add the variable and accepted values to both mirrored configuration/env-vars.md pages.
Useful? React with 👍 / 👎.
| const id = raw?.trim().toLowerCase(); | ||
| if (id === OFF_TITLE_SPINNER_ID) return OFF_TITLE_SPINNER_ID; | ||
| if (id === PHASE_TITLE_SPINNER_ID) return PHASE_TITLE_SPINNER_ID; | ||
| if (id !== undefined && id in TITLE_SPINNER_STYLES) return id; |
There was a problem hiding this comment.
Check only own spinner registry entries
When KIMI_TITLE_SPINNER is set to an inherited object key such as constructor, toString, or __proto__, the in check accepts it as a valid style instead of falling back to moon. On the first busy transition, the inherited function/object has no frames, so frameTerminalTitle() throws and crashes the TUI; use an own-property check or a null-prototype registry.
Useful? React with 👍 / 👎.
Address review feedback on MoonshotAI#2238: - Move the title animation state machine (style resolution, timer, frame selection, rendering) out of KimiTUI into TerminalTitleSpinnerController; the coordinator now only syncs and disposes it. - Restore the static title on dispose so a busy indicator is not left behind when exiting mid-turn (Ctrl-D / SIGTERM). - Guard the spinner registry with an own-property check so inherited keys (constructor, toString, ...) fall back to the default instead of crashing. - Document KIMI_TITLE_SPINNER in the EN/ZH env-vars references.
|
感谢审查!四条建议已在 a33e03c 中全部处理:
验证:typecheck、oxlint 通过;单元测试与相关集成测试(221 例)全部通过。 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a33e03cb7e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| | `KIMI_MODEL_THINKING_KEEP` | 保留思考透传;在 `kimi` 上以 `thinking.keep` 发送,在 `anthropic`(Claude 以及 Kimi 的 Anthropic 兼容模式)上以 `context_management` 的 `clear_thinking_20251015` 编辑发送(开启 keep 会让 Anthropic 请求走 beta Messages API);覆盖 `[thinking] keep`(其默认值为 `"all"`);仅在 Thinking 开启时注入 | API 接受的值,如 `all`;传入关值(`false`/`0`/`no`/`off`/`none`/`null`)可禁用 | | ||
| | `KIMI_CODE_NO_AUTO_UPDATE` | 完全禁用更新预检——不检查、不后台安装、不提示。同时兼容旧名 `KIMI_CLI_NO_AUTO_UPDATE` | 真值:`1`/`true`/`yes`/`on` | | ||
| | `KIMI_DISABLE_CRON` | 禁用定时任务工具(`CronCreate` 拒绝新计划,已有任务不触发) | `1` 表示禁用 | | ||
| | `KIMI_TITLE_SPINNER` | agent 忙碌时让终端标题动起来,使工作状态在终端标签页上可见;空闲时恢复静态标题 | `moon`(默认)、`sparkle`、`braille`、`orbit`、`pulse`、`line`、`hourglass`、`phase`(显示 streaming 相位而非旋转)、`off`(不动画);未知值回退到 `moon` | |
There was a problem hiding this comment.
Use the required Chinese Agent spelling
For Chinese readers, this new row spells the repository-defined Chinese term as lowercase agent; the documentation term table requires Agent in Chinese, so capitalize it to keep terminology consistent across the locale.
AGENTS.md reference: docs/AGENTS.md:L69-L69
Useful? React with 👍 / 👎.
| setTitle: (label) => this.state.terminal.setTitle(label), | ||
| staticTitle: () => { | ||
| const trimmed = this.state.appState.sessionTitle?.trim() ?? ''; | ||
| return trimmed.length > 0 ? trimmed.slice(0, MAX_TERMINAL_TITLE_LENGTH) : PRODUCT_NAME; |
There was a problem hiding this comment.
Keep spinner titles within the terminal-title cap
When a session title already reaches MAX_TERMINAL_TITLE_LENGTH (32), this callback returns all 32 characters and frameTitle() or phaseTitle() then prepends a glyph and space, producing a title longer than the repository's stated bound. Reserve space for the prefix or truncate the final composed title so long session names do not defeat the readability cap while busy.
Useful? React with 👍 / 👎.
| this.timer ??= setInterval(() => { | ||
| this.frame = (this.frame + 1) % style.frames.length; | ||
| this.host.setTitle(this.frameTitle(style)); | ||
| }, style.interval); |
There was a problem hiding this comment.
Tear down spinner intervals in KimiTUI tests
When a KimiTUI test leaves the app busy, this referenced interval outlives the test because the shared makeDriver() fixture in kimi-tui-message-flow.test.ts creates drivers while its afterEach never calls stop() or titleSpinner.dispose(); for example, the reload test starts a request and then directly mutates streamingPhase, bypassing spinner synchronization. As the suite progresses, these timers retain each driver and continuously write OSC titles, accumulating work and polluting test output, so register fixture teardown or disable the spinner in the suite environment.
Useful? React with 👍 / 👎.
相关 Issue
Implements #1885
提交后才发现 @ColorC 已在 #1885 中更早、更完整地提出了相同需求(含参考补丁与 Claude Code 先例)。本 PR 可作为 #1885 的一个完整实现供讨论;不使用自动关闭关键字,关闭与否留给维护者决定。我此前另开的 #2239 与之重复,已标记为重复并关闭。
问题
当 agent 在后台长时间工作(多轮 goal、大型重构、跑测试)时,如果终端窗口不在前台,用户无法只凭终端标签页判断它"还在干活"还是"已经完成 / 卡住"。当前终端标题只显示静态会话标题,缺少一个轻量的工作状态指示。
改动内容
在 agent 忙碌期间(turn 流式输出或上下文压缩时),给终端标题加上一个转动的图形前缀,让用户即使不看窗口、只瞥一眼终端标签页也能知道 agent 正在工作;空闲时自动恢复为原来的静态标题。
设计遵循三个原则:
apps/kimi-code/src/tui/constant/title-spinners.ts,每个样式是注册表里一条独立记录,增删任一样式互不影响;kimi-tui.ts只负责定时器编排。setTitle()(OSC 0)与会话标题逻辑,原有静态标题行为完整保留;只在setAppState已有的busyChanged分支和关闭清理处各新增一个调用点。setInterval,且仅在忙碌时运行,每次 tick 只向 stdout 写一小段转义序列。phase样式直接由 streaming phase 推导图形、完全不开定时器;off则完全不改变现有行为。样式通过环境变量
KIMI_TITLE_SPINNER选择(启动时解析一次):moon(默认)sparklebrailleorbitpulselinehourglassphaseoff验证
test/tui/constant/title-spinners.test.ts(样式解析与注册表不变量,7 个用例全部通过)。pnpm --filter @moonshot-ai/kimi-code run typecheck通过。oxlint --type-aware对新增 / 修改文件 0 警告 0 错误。editor-keyboard-image-paste因本机 CPU 争用偶发超时,单独运行稳定通过,与本改动无关)。Checklist
gen-changesets(新增一个 patch changeset)。KIMI_TITLE_SPINNER的使用说明可作为后续独立 PR 补充。