fix(cli): window title shows session name instead of model activity status - #5288
Conversation
|
Stepping back: the previous triage's blocker — missing This is a well-executed PR solving a genuine UX problem. The motivation is clear — model-activity titles cycle through transient strings ("reading tokenizer.ts…", "searching for config…") that are useless for tab identification. Session names (from The implementation is thoughtful and thorough:
114 unit tests pass, TypeScript typecheck is clean, and the escape sequences are verified correct across all branches (multiplexer/non-multiplexer, empty/non-empty, sanitized/long titles). The previous concern about the missing import is gone. No new issues found. Approving. ✅ 中文说明上次审查的阻断问题—— 这是一个执行良好的 PR,解决了真实的 UX 问题。动机清晰——模型活动标题在瞬时字符串间循环,对标签页辨识毫无用处。会话名称提供稳定标识。 实现细致且充分:复用器外双 OSC 0+2、复用器内单 OSC 2、80 字符填充防图标抖动、 114 个单元测试通过,TypeScript 类型检查干净,所有分支的转义序列验证正确。无新问题。批准。✅ — Qwen Code · qwen3.7-max |
c3716c8 to
69d6730
Compare
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Build is broken — computeWindowTitle is used in AppContainer.tsx line 3234 but not imported. Causes TS2304 at build time and a ReferenceError at runtime when showStatusInTitle is disabled. One-line fix: add computeWindowTitle to the import from ../utils/windowTitle.js. Everything else looks solid — see the detailed review above. 🙏
e7e24a5 to
69d6730
Compare
69d6730 to
37e1478
Compare
- Show session title in terminal window instead of fixed 'Qwen - <folder>' - Clear terminal title on exit so it reverts to shell default (empty OSC sequence without 80-char padding) - Add try/catch around exit handler to suppress EPIPE when stdout is already closed - Extend multiplexer detection to include Zellij (ZELLIJ) and dvtm (DVTM) - Chain title callbacks so AppContainer preserves Session's existing ACP notification callback instead of overwriting it - Revert terminal title to static fallback when showStatusInTitle is toggled off at runtime - Reuse sanitizeForOsc for BiDi/RTL directional override protection - Update tests for new behavior
37e1478 to
136080b
Compare
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
[Suggestion] Test coverage gaps across multiple files:
windowTitle.ts:53-54: Theprocess.platform === 'win32'branch inwriteTerminalTitleis untested — no mock forprocess.platformexists inwindowTitle.test.ts.gemini.tsx:1214-1233: The rewrittensetWindowTitlefunction has zero test coverage — the newshowStatusInTitle === falseearly-return and the exit handler's try/catch are both untested.chatRecordingService.ts:1271-1274: The newgetTitleRecordedCallback()getter has no dedicated unit test (only exercised indirectly via AppContainer mocks).- Bidi override characters (e.g.,
\u202E) are stripped bysanitizeForOscbut never tested throughwriteTerminalTitle,formatSessionWindowTitle, orcomputeWindowTitle.
— qwen3.7-max via Qwen Code /review
| ): void { | ||
| const clean = sanitizeWindowTitle(title); | ||
| if (process.platform === 'win32') { | ||
| process.title = clean; |
There was a problem hiding this comment.
[Suggestion] process.title is assigned the full sanitized title (up to 200 chars) before the 80-char truncation applied to the OSC escape payload at line 67. On Windows, Task Manager would display more text than the terminal title bar.
| process.title = clean; | |
| if (process.platform === 'win32') { | |
| const truncated = clean.substring(0, 80); | |
| process.title = truncated; | |
| } |
— qwen3.7-max via Qwen Code /review
| if (process.platform === 'win32') { | ||
| process.title = clean; | ||
| } | ||
| const inMultiplexer = MULTIPLEXER_ENV_KEYS.some((k) => !!process.env[k]); |
There was a problem hiding this comment.
[Suggestion] MULTIPLEXER_ENV_KEYS.some(...) performs four process.env lookups on every call to writeTerminalTitle. The multiplexer status is determined by the parent terminal at session start and never changes during the process lifetime. Consider caching at module scope:
const inMultiplexer = MULTIPLEXER_ENV_KEYS.some((k) => !!process.env[k]);— qwen3.7-max via Qwen Code /review
| // process.stdout.write directly (to avoid Ink proxy corruption of | ||
| // OSC escape sequences), so we spy on that. | ||
| mockStdout = { write: vi.fn() }; | ||
| vi.spyOn(process.stdout, 'write').mockImplementation(() => true); |
There was a problem hiding this comment.
[Suggestion] The titleEscape helper (line 2190) hard-codes the non-multiplexer escape format (OSC 0 + OSC 2 with 80-char padding), but this beforeEach does not stub TMUX, STY, ZELLIJ, or DVTM to undefined. When the test suite runs inside tmux, writeTerminalTitle detects the multiplexer and writes only \x1b]2;title\x07 (no OSC 0, no padding), causing every titleEscape assertion to fail.
Contrast with windowTitle.test.ts where each test explicitly stubs all four env vars.
| vi.spyOn(process.stdout, 'write').mockImplementation(() => true); | |
| beforeEach(() => { | |
| mockStdout = { write: vi.fn() }; | |
| vi.spyOn(process.stdout, 'write').mockImplementation(() => true); | |
| vi.stubEnv('TMUX', undefined); | |
| vi.stubEnv('STY', undefined); | |
| vi.stubEnv('ZELLIJ', undefined); | |
| vi.stubEnv('DVTM', undefined); | |
| }); |
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
|
Thanks for the PR! Template looks good ✓ On direction: session naming is a well-established feature in qwen-code (auto-title via fast model #3540, /rename #3093, ACP title broadcast #5035). Surfacing session names in the terminal title is a natural extension — solves a real pain point for multi-tab users who currently see flickering model-activity strings. Clearly aligned. On approach: the scope feels right. The diff is focused on the title problem: a new 中文说明感谢贡献! 模板完整 ✓ 方向:会话命名是 qwen-code 的成熟功能域(fast model 自动标题 #3540、/rename #3093、ACP 标题广播 #5035)。将会话名称展示到终端标题是自然延伸——解决了多标签页用户看到闪烁模型活动字符串的真实痛点。方向明确对齐。 方案:范围合理。diff 聚焦于标题问题:新的 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal: I would have added a No critical blockers found. The implementation is clean:
Unit TestsTypeScript typecheck: clean (exit 0, no errors). Escape Sequence VerificationDirect Node.js test of the built PR code: Interactive Smoke TestStartup succeeds. tmux consumed the OSC 2 sequences (they don't appear in Before (installed build, v0.18.3)中文说明代码审查独立方案对比: PR 的方案与我的独立设计一致并在细节上超越:复用器检测(tmux/screen/zellij/dvtm → 单 OSC 2、不填充以避免污染复用器窗口列表)、Windows 未发现关键阻断问题。实现干净:所有分支处理正确, 单元测试21 + 93 = 114 个测试全部通过。TypeScript 类型检查干净。 转义序列验证直接 Node.js 测试确认:默认标题双 OSC 0+2 带 80 字符填充、会话名称正确、tmux 内单 OSC 2 无填充、CLI_TITLE 生效、控制字符净化正确、填充长度验证。 交互测试
— Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Maintainer verification — local merge build + byte-level title harnessVerified the 3-way merge result ( 1. Build / tests / typecheck / lint (merge result)
2. Byte-level behavior of the new
|
| Context | Title | Bytes written |
|---|---|---|
| outside multiplexer | My Session |
<ESC>]0;My Session<70sp><BEL><ESC>]2;My Session<70sp><BEL> (OSC 0 + OSC 2, padded to 80) |
inside TMUX |
My Session |
<ESC>]2;My Session<BEL> (only OSC 2, not padded) |
| outside multiplexer | "" |
<ESC>]0;<BEL><ESC>]2;<BEL> (clears both) |
| any | tab\t nl\n bell\x07 |
control chars stripped → tabnlbell… |
This matches the documented design: 80-char padding outside multiplexers (prevents taskbar/dock icon resize), OSC-2-only inside tmux/screen (avoids cluttering the window list), and sanitization of control characters.
3. A/B — computeWindowTitle & fallback chain
| Call | before | after |
|---|---|---|
computeWindowTitle('myproj') |
"Qwen - myproj" |
"Qwen - myproj" (unchanged) |
computeWindowTitle('') |
"Qwen - " |
"Qwen - qwen" ✅ |
computeWindowTitle(undefined) |
"Qwen - undefined" |
"Qwen - qwen" ✅ |
CLI_TITLE=MyCustom |
"MyCustom" |
"MyCustom" (unchanged) |
formatSessionWindowTitle('My Session','proj') |
— | "My Session" (session name wins) |
formatSessionWindowTitle(null,'proj') / ('','proj') |
— | "Qwen - proj" (fallback) |
The empty/undefined-folder cases are improved (no more "Qwen - " or "Qwen - undefined"); normal folders and CLI_TITLE are unchanged.
4. One behavioral change to confirm is intended
showStatusInTitle default flips false → true (both settingsSchema.ts and the vscode settings.schema.json). The title feature is now on by default — i.e. by default the terminal title becomes the session name (Qwen - <folder> until a name is recorded) rather than staying whatever it was. This is the main product decision in the PR; everything else is mechanism. Worth a conscious sign-off, but it's consistent with the PR's stated intent.
Scope note
This is verified at the unit + byte level + the AppContainer integration tests (which cover the sessionName wiring through titleRecordedCallback / /resume / /branch / restore-on-mount). I did not drive the full interactive Ink app in a live terminal (auth/onboarding makes that non-deterministic), but the exact OSC bytes are proven deterministically above and the process.stdout.write-direct path (chosen specifically to avoid Ink proxy corruption) is exercised by the helper.
Verdict
Correct and well-tested — the new title helper emits the right OSC sequences for both multiplexer and non-multiplexer terminals, sanitizes input, and the session-name fallback chain behaves as designed. No regressions in the pure helpers. The one thing to consciously approve is the showStatusInTitle default → true (feature on by default). ✅ Safe to merge with that decision in mind. CI is green and the change merges cleanly.
Verified by maintainer @wenshao: 3-way merge build + windowTitle.test.ts (21/21) + AppContainer.test.tsx (93/93) + a byte-capture harness over writeTerminalTitle / computeWindowTitle / formatSessionWindowTitle from origin/main and the PR head (136080b). Env: Darwin arm64, Node v22.22.2.
中文版(点击展开)
维护者验证 —— 本地合并构建 + 字节级标题 harness
在 macOS(Darwin arm64,Node v22.22.2)上验证了三方合并结果(origin/main f3e0dd6 + 本 PR 136080b)。有两个改动文件自 PR 基线以来在 main 上有漂移;合并自动干净解决,diff 正好是 PR 的 +606/-129。由于面向用户的部分(OSC 转义字节)在单测断言里看不出来,我除真实套件外,还用一个字节捕获 harness 驱动了新 helper。
1. 构建 / 测试 / 类型检查 / lint(合并结果)
| 检查 | 结果 |
|---|---|
windowTitle.test.ts |
✅ 21 通过 |
AppContainer.test.tsx |
✅ 93 通过(PR 新增 +334 行 wiring 测试) |
tsc --noEmit core |
✅ 干净 |
tsc --noEmit CLI |
✅ 改动文件干净(重建 core dist 后——见下方说明) |
npm run build core |
✅ 通过 |
eslint + prettier --check |
✅ 干净 |
| CI | ✅ macOS / Windows / Linux 全绿;bot 已 approve |
stale-dist 说明: 在重建
core之前,本地 CLI 类型检查会报AppContainer.tsx:1045处getTitleRecordedCallback does not exist on ChatRecordingService。该方法正是本 PR 在core/src/services/chatRecordingService.ts里新增的;CLI 是对着 core 的构建产物dist做类型检查的,而 CI 会先构建 core 再检查 CLI(所以 CI 是对的)。在 core 里npm run build之后该报错消失——这是环境产物,不是真实问题。
2. 新 writeTerminalTitle 的字节级行为(AFTER)
捕获了写出的精确字节(<ESC>=\x1b,<BEL>=\x07,<Nsp>=N 个空格):
| 场景 | 标题 | 写出的字节 |
|---|---|---|
| 多路复用器之外 | My Session |
<ESC>]0;My Session<70sp><BEL><ESC>]2;My Session<70sp><BEL>(OSC 0 + OSC 2,补齐到 80) |
TMUX 内 |
My Session |
<ESC>]2;My Session<BEL>(只有 OSC 2,不补齐) |
| 多路复用器之外 | "" |
<ESC>]0;<BEL><ESC>]2;<BEL>(两个都清空) |
| 任意 | tab\t nl\n bell\x07 |
控制字符被剥除 → tabnlbell… |
这与文档化的设计一致:多路复用器之外补齐到 80 字符(防止任务栏/Dock 图标因标题长度变化而抖动)、tmux/screen 内只写 OSC 2(避免污染其窗口列表)、并对控制字符做净化。
3. A/B —— computeWindowTitle 与回退链
| 调用 | before | after |
|---|---|---|
computeWindowTitle('myproj') |
"Qwen - myproj" |
"Qwen - myproj"(不变) |
computeWindowTitle('') |
"Qwen - " |
"Qwen - qwen" ✅ |
computeWindowTitle(undefined) |
"Qwen - undefined" |
"Qwen - qwen" ✅ |
CLI_TITLE=MyCustom |
"MyCustom" |
"MyCustom"(不变) |
formatSessionWindowTitle('My Session','proj') |
— | "My Session"(会话名优先) |
formatSessionWindowTitle(null,'proj') / ('','proj') |
— | "Qwen - proj"(回退) |
空/undefined 文件夹的情况得到改善(不再出现 "Qwen - " 或 "Qwen - undefined");正常文件夹与 CLI_TITLE 不变。
4. 一处需确认是否符合预期的行为变化
showStatusInTitle 默认值从 false 翻为 true(settingsSchema.ts 和 vscode 的 settings.schema.json 都改了)。标题特性现在默认开启——即默认情况下终端标题会变成会话名(在记录到名字之前为 Qwen - <folder>),而不是保持原样。这是本 PR 的主要产品决策,其余都是机制。值得有意识地签字确认,但与 PR 的既定意图一致。
范围说明
本次验证覆盖到单元 + 字节级 + AppContainer 集成测试(后者覆盖 sessionName 经 titleRecordedCallback / /resume / /branch / 挂载时恢复的接线)。我没有在真实终端里驱动完整的交互式 Ink 应用(auth/onboarding 使其不确定),但上面已确定性地证明了精确的 OSC 字节,且为避免 Ink 代理破坏而特意选用的 process.stdout.write 直写路径也被 helper 执行到了。
结论
正确且测试充分——新的标题 helper 对多路复用器和非多路复用器终端都发出正确的 OSC 序列、对输入做净化,会话名回退链也按设计工作。纯 helper 无回归。唯一需要有意识确认的是 showStatusInTitle 默认 → true(特性默认开启)。✅ 在确认该决策的前提下可以合并。CI 已绿,合并干净。
维护者 @wenshao 验证:三方合并构建 + windowTitle.test.ts(21/21)+ AppContainer.test.tsx(93/93)+ 一个对 writeTerminalTitle / computeWindowTitle / formatSessionWindowTitle 的字节捕获 harness(分别从 origin/main 与 PR head 136080b)。环境:Darwin arm64,Node v22.22.2。
What this PR does
Changes the terminal window title from model activity status to session name.
Previous behavior — two mechanisms wrote the title:
gemini.tsxsetWindowTitlewroteQwen - <folder>(orCLI_TITLEif the env var was set), via a raw\x1b]2;escape sequence.showStatusInTitlewas enabled in settings,AppContainer'suseEffectoverwrote it withthought.subjectwhile the model was streaming (e.g."analyzing user.py…"), falling back toQwen - <folder>when idle. IfshowStatusInTitlewas disabled, this effect returned early and the title never changed.This PR — the title is driven by
sessionNamestate inAppContainer:sessionNameis updated viaChatRecordingService'stitleRecordedCallback(fires on/renameor auto-title generation), directly on mount when restoring a prior session, and by/resumeand/branchcommands. WhensessionNameisnull(fresh session, no title recorded yet), the title falls back throughCLI_TITLEenv var →Qwen - <folder>→"Qwen - qwen".The title is written through a new
writeTerminalTitlehelper that writes both\x1b]0;(icon+title) and\x1b]2;(title) for broader terminal compatibility, usesprocess.stdout.writedirectly to avoid Ink's proxy corrupting OSC escape sequences, and setsprocess.titleon Windows.Why it's needed
The previous title cycled through transient model thoughts —
"reading tokenizer.ts…"→"searching for config…"→ idle — giving zero context when switching terminal tabs. Session names (set via/rename, auto-generated by the fast model, or restored on/resumeand/branch) provide stable, identifiable labels so you can find the right tab at a glance.Reviewer Test Plan
How to verify
Manual smoke test:
_2026-06-17.201102.mp4
Evidence (Before & After)
Before: title flickers with model activity —
"reading tokenizer.ts…"→"searching for config…"→"Qwen - my-project"— impossible to tell sessions apart across tabs.After: title is stable and session-identifying — starts as
"Qwen - my-project", then becomes the session name once a title is recorded (manual/renameor auto-generated after the first turn).Tested on
Environment (optional)
macOS 15.5, iTerm2, Node 22,
npm run dev.Risk & Scope
setTitleRecordedCallbackis a new API surface onChatRecordingService. TheuseEffectcleanup callssetTitleRecordedCallback(undefined)to release it on unmount.\x1b]0;/\x1b]2;sequences. All major terminals (iTerm2, tmux, Windows Terminal, macOS Terminal) respect the last-written sequence and are unaffected.showStatusInTitlesetting is no longer checked by the title effect — users who had it enabled won't notice a difference since the title now shows the session name rather than activity. The setting key remains in the schema but is semantically dead.computeWindowTitle'sfolderNameparameter is now optional (defaults to"qwen"); all existing callers pass an explicit value so this is backward-compatible.中文说明
这个 PR 做了什么
将终端窗口标题从模型活动状态改为会话名称。
原有行为——两个地方写入标题:
gemini.tsx的setWindowTitle写入Qwen - <folder>(或CLI_TITLE环境变量),通过原始\x1b]2;转义序列。AppContainer的useEffect在showStatusInTitle开启时,用thought.subject(如"analyzing user.py…")覆盖标题,空闲时回退为Qwen - <folder>。若showStatusInTitle关闭,该 effect 直接 return,标题始终保持启动时的值不变。本 PR——标题由
AppContainer中的sessionName状态驱动:sessionName通过ChatRecordingService的titleRecordedCallback更新(在/rename或自动标题生成时触发),在挂载恢复会话时直接从 JSONL 读取,以及由/resume和/branch命令设置。当sessionName为null(全新会话,尚无标题)时,依次回退到CLI_TITLE环境变量 →Qwen - <folder>→"Qwen - qwen"。标题通过新的
writeTerminalTitle辅助函数写入,它同时写入\x1b]0;和\x1b]2;双序列以兼容更多终端,使用process.stdout.write直写避免 Ink 代理损坏 OSC 转义序列,并在 Windows 下额外设置process.title。为什么需要
之前终端标题在模型思考的瞬时内容间切换,多标签页时毫无辨识度。会话名称(
/rename手动、fast model 自动生成、/resume和/branch恢复)提供稳定标识,一眼定位对应标签页。Reviewer Test Plan
(测试步骤同上,此处省略以保持可读性。)
风险与范围
setTitleRecordedCallback是ChatRecordingService的新 API。useEffectcleanup 已调用setTitleRecordedCallback(undefined)防止持有过期状态引用。\x1b]0;/\x1b]2;序列的处理。主流终端(iTerm2、tmux、Windows Terminal、macOS Terminal)均以后一个序列为准。showStatusInTitle设置不再被标题 effect 检查,用户无感知差异。computeWindowTitle的folderName改为可选且默认"qwen",所有现有调用方均传入明确值,向后兼容。