Skip to content

feat(tui): animate terminal title while agent is busy - #2238

Open
AidenNovak wants to merge 2 commits into
MoonshotAI:mainfrom
AidenNovak:feat/animated-terminal-title
Open

feat(tui): animate terminal title while agent is busy#2238
AidenNovak wants to merge 2 commits into
MoonshotAI:mainfrom
AidenNovak:feat/animated-terminal-title

Conversation

@AidenNovak

@AidenNovak AidenNovak commented Jul 27, 2026

Copy link
Copy Markdown

相关 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(默认) 月相 🌑→🌕,与 Kimi 月亮意象同源
sparkle 星光 ✦✧ 轻盈明灭
braille 经典盲文旋转 ⠋⠙⠹
orbit 点绕八向轨道 ⠁⠂⠄
pulse 电平表式律动 ▁▄█
line 复古 Unix 旋转线 |/-\
hourglass 沙漏翻转 ⧗⧖
phase 按状态显示 waiting◌ / thinking✦ / composing≋ / shell▸(无定时器)
off 关闭,保持原始静态标题

验证

  • 新增 test/tui/constant/title-spinners.test.ts(样式解析与注册表不变量,7 个用例全部通过)。
  • pnpm --filter @moonshot-ai/kimi-code run typecheck 通过。
  • oxlint --type-aware 对新增 / 修改文件 0 警告 0 错误。
  • 现有 TUI 测试套件通过(全量并行执行时 editor-keyboard-image-paste 因本机 CPU 争用偶发超时,单独运行稳定通过,与本改动无关)。

Checklist

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

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a33e03c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/kimi-code/src/tui/kimi-tui.ts Outdated
Comment on lines +1712 to +1713
private syncTerminalTitleSpinner(): void {
const busy = this.state.appState.streamingPhase !== 'idle' || this.state.appState.isCompacting;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread apps/kimi-code/src/tui/kimi-tui.ts Outdated
this.tasksBrowserController.close();
this.btwPanelController.clear();
this.stopActivitySpinner();
this.stopTerminalTitleSpinner();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@AidenNovak

Copy link
Copy Markdown
Author

感谢审查!四条建议已在 a33e03c 中全部处理:

  • P1 抽成 controller:标题动画的状态机(样式解析 / 定时器 / 帧选择 / 渲染)已移入新的 TerminalTitleSpinnerControllercontrollers/terminal-title-spinner.ts),KimiTUI 现在只负责 sync()dispose(),并补充了 7 个可独立观察 title/timer 的测试。
  • P2 退出恢复静态标题dispose() 现在会停止定时器并写回静态标题,turn 进行中退出(Ctrl-D / SIGTERM)不会再残留最后一帧动画。
  • P2 自有属性检查resolveTitleSpinnerIdin 改为 Object.hasOwnconstructor / toString / __proto__ 等继承键会回退到 moon,不再因缺少 frames 而崩溃(已加回归测试)。
  • P2 文档补充:已在中英文 configuration/env-vars.md 的「运行时开关」表格中加入 KIMI_TITLE_SPINNER 及全部取值说明。

验证:typecheck、oxlint 通过;单元测试与相关集成测试(221 例)全部通过。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +60 to +63
this.timer ??= setInterval(() => {
this.frame = (this.frame + 1) % style.frames.length;
this.host.setTitle(this.frameTitle(style));
}, style.interval);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant