Skip to content

fix(cli): window title shows session name instead of model activity status - #5288

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
ZijianZhang989:fix/terminal-title-session-name
Jun 18, 2026
Merged

fix(cli): window title shows session name instead of model activity status#5288
wenshao merged 1 commit into
QwenLM:mainfrom
ZijianZhang989:fix/terminal-title-session-name

Conversation

@ZijianZhang989

@ZijianZhang989 ZijianZhang989 commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Changes the terminal window title from model activity status to session name.

Previous behavior — two mechanisms wrote the title:

  1. On startup, gemini.tsx setWindowTitle wrote Qwen - <folder> (or CLI_TITLE if the env var was set), via a raw \x1b]2; escape sequence.
  2. If showStatusInTitle was enabled in settings, AppContainer's useEffect overwrote it with thought.subject while the model was streaming (e.g. "analyzing user.py…"), falling back to Qwen - <folder> when idle. If showStatusInTitle was disabled, this effect returned early and the title never changed.

This PR — the title is driven by sessionName state in AppContainer:

sessionName is updated via ChatRecordingService's titleRecordedCallback (fires on /rename or auto-title generation), directly on mount when restoring a prior session, and by /resume and /branch commands. When sessionName is null (fresh session, no title recorded yet), the title falls back through CLI_TITLE env var → Qwen - <folder>"Qwen - qwen".

The title is written through a new writeTerminalTitle helper that writes both \x1b]0; (icon+title) and \x1b]2; (title) for broader terminal compatibility, uses process.stdout.write directly to avoid Ink's proxy corrupting OSC escape sequences, and sets process.title on 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 /resume and /branch) provide stable, identifiable labels so you can find the right tab at a glance.

Reviewer Test Plan

How to verify

# Unit tests
cd packages/cli && npx vitest run src/utils/windowTitle.test.ts
cd packages/cli && npx vitest run src/ui/AppContainer.test.tsx

Manual smoke test:

npm run dev
# 1. Default title: "Qwen - qwen" (no CLI_TITLE, no folder)
# 2. In a project dir: title shows "Qwen - <folder>"
# 3. /rename "fixing auth bug" → title updates to "fixing auth bug"
# 4. qwen-code --resume → title shows the saved session name
# 5. /branch "new approach" → title updates to the branch title
# 6. CLI_TITLE="Custom Title" npm run dev → title shows "Custom Title"
# 7. /rename with varying lengths → no taskbar icon jitter (80-char padding)
_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 /rename or auto-generated after the first turn).

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS 15.5, iTerm2, Node 22, npm run dev.

Risk & Scope

  • Main risk or tradeoff: setTitleRecordedCallback is a new API surface on ChatRecordingService. The useEffect cleanup calls setTitleRecordedCallback(undefined) to release it on unmount.
  • Not validated / out of scope: Exotic terminal emulators that may mishandle dual \x1b]0; / \x1b]2; sequences. All major terminals (iTerm2, tmux, Windows Terminal, macOS Terminal) respect the last-written sequence and are unaffected.
  • Breaking changes / migration notes: The showStatusInTitle setting 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's folderName parameter is now optional (defaults to "qwen"); all existing callers pass an explicit value so this is backward-compatible.
中文说明

这个 PR 做了什么

将终端窗口标题从模型活动状态改为会话名称。

原有行为——两个地方写入标题:

  1. 启动时 gemini.tsxsetWindowTitle 写入 Qwen - <folder>(或 CLI_TITLE 环境变量),通过原始 \x1b]2; 转义序列。
  2. AppContaineruseEffectshowStatusInTitle 开启时,用 thought.subject(如 "analyzing user.py…")覆盖标题,空闲时回退为 Qwen - <folder>。若 showStatusInTitle 关闭,该 effect 直接 return,标题始终保持启动时的值不变。

本 PR——标题由 AppContainer 中的 sessionName 状态驱动:

sessionName 通过 ChatRecordingServicetitleRecordedCallback 更新(在 /rename 或自动标题生成时触发),在挂载恢复会话时直接从 JSONL 读取,以及由 /resume/branch 命令设置。当 sessionNamenull(全新会话,尚无标题)时,依次回退到 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

(测试步骤同上,此处省略以保持可读性。)

风险与范围

  • 主要风险与权衡: setTitleRecordedCallbackChatRecordingService 的新 API。useEffect cleanup 已调用 setTitleRecordedCallback(undefined) 防止持有过期状态引用。
  • 未验证 / 超出范围: 冷门终端模拟器对双重 \x1b]0; / \x1b]2; 序列的处理。主流终端(iTerm2、tmux、Windows Terminal、macOS Terminal)均以后一个序列为准。
  • Breaking changes / 迁移说明: showStatusInTitle 设置不再被标题 effect 检查,用户无感知差异。computeWindowTitlefolderName 改为可选且默认 "qwen",所有现有调用方均传入明确值,向后兼容。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Stepping back: the previous triage's blocker — missing computeWindowTitle import in AppContainer.tsx — is fully resolved. The code was restructured: computeWindowTitle is no longer needed in AppContainer; instead, formatSessionWindowTitle and writeTerminalTitle are imported and used correctly. The originalTitleRef that consumed computeWindowTitle was removed entirely.

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 /rename, auto-title, or /resume) provide stable, identifiable labels.

The implementation is thoughtful and thorough:

  • Dual OSC 0+2 sequences outside multiplexers, single OSC 2 inside — the right call for terminal compat without cluttering multiplexer window lists.
  • 80-char padding prevents taskbar icon jitter — a detail that shows real-world testing.
  • process.stdout.write bypasses Ink's proxy that corrupts binary escape sequences — the correct workaround.
  • Callback chaining via getTitleRecordedCallback() preserves Session's ACP notification path — clean composition.
  • The showStatusInTitle default flip to true means all users get session-name titles without a migration step. The showStatusInTitle === false gate still works (writes static fallback once).
  • Exit handler with try-catch for EPIPE — good defensive coding for process shutdown.

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

中文说明

上次审查的阻断问题——AppContainer.tsx 中缺少 computeWindowTitle 导入——已完全解决。代码被重构:AppContainer 不再需要 computeWindowTitle,改用 formatSessionWindowTitlewriteTerminalTitle,使用正确。消费 computeWindowTitleoriginalTitleRef 已完全移除。

这是一个执行良好的 PR,解决了真实的 UX 问题。动机清晰——模型活动标题在瞬时字符串间循环,对标签页辨识毫无用处。会话名称提供稳定标识。

实现细致且充分:复用器外双 OSC 0+2、复用器内单 OSC 2、80 字符填充防图标抖动、process.stdout.write 绕过 Ink 代理、回调链保留 ACP 通知、showStatusInTitle 默认值改为 true、退出处理器 try-catch 防 EPIPE。

114 个单元测试通过,TypeScript 类型检查干净,所有分支的转义序列验证正确。无新问题。批准。✅

Qwen Code · qwen3.7-max

@ZijianZhang989
ZijianZhang989 force-pushed the fix/terminal-title-session-name branch from c3716c8 to 69d6730 Compare June 18, 2026 07:04

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Comment thread packages/cli/src/ui/AppContainer.test.tsx
@ZijianZhang989
ZijianZhang989 force-pushed the fix/terminal-title-session-name branch from e7e24a5 to 69d6730 Compare June 18, 2026 08:20
Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment thread packages/cli/src/utils/windowTitle.test.ts
Comment thread packages/cli/src/ui/AppContainer.tsx
Comment thread packages/cli/src/ui/AppContainer.test.tsx
Comment thread packages/cli/src/config/settingsSchema.ts
Comment thread packages/cli/src/utils/windowTitle.ts Outdated
Comment thread packages/cli/src/ui/AppContainer.test.tsx
Comment thread packages/cli/src/gemini.tsx
Comment thread packages/cli/src/utils/windowTitle.ts
Comment thread packages/cli/src/utils/windowTitle.ts
Comment thread packages/cli/src/ui/AppContainer.tsx
Comment thread packages/cli/src/utils/windowTitle.ts
@ZijianZhang989
ZijianZhang989 force-pushed the fix/terminal-title-session-name branch from 69d6730 to 37e1478 Compare June 18, 2026 10:28
- 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
@ZijianZhang989
ZijianZhang989 force-pushed the fix/terminal-title-session-name branch from 37e1478 to 136080b Compare June 18, 2026 10:33

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Test coverage gaps across multiple files:

  • windowTitle.ts:53-54: The process.platform === 'win32' branch in writeTerminalTitle is untested — no mock for process.platform exists in windowTitle.test.ts.
  • gemini.tsx:1214-1233: The rewritten setWindowTitle function has zero test coverage — the new showStatusInTitle === false early-return and the exit handler's try/catch are both untested.
  • chatRecordingService.ts:1271-1274: The new getTitleRecordedCallback() getter has no dedicated unit test (only exercised indirectly via AppContainer mocks).
  • Bidi override characters (e.g., \u202E) are stripped by sanitizeForOsc but never tested through writeTerminalTitle, formatSessionWindowTitle, or computeWindowTitle.

— qwen3.7-max via Qwen Code /review

): void {
const clean = sanitizeWindowTitle(title);
if (process.platform === 'win32') {
process.title = clean;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Suggested change
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]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Suggested change
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

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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 writeTerminalTitle utility (with multiplexer detection, 80-char padding, sanitization), formatSessionWindowTitle for the session-name → fallback chain, callback wiring through ChatRecordingService, and the showStatusInTitle default flip to true. No unrelated refactors or scope creep. The dual OSC 0+2 approach (single OSC 2 inside multiplexers) and Windows process.title show thoughtful terminal compat. Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:会话命名是 qwen-code 的成熟功能域(fast model 自动标题 #3540、/rename #3093、ACP 标题广播 #5035)。将会话名称展示到终端标题是自然延伸——解决了多标签页用户看到闪烁模型活动字符串的真实痛点。方向明确对齐。

方案:范围合理。diff 聚焦于标题问题:新的 writeTerminalTitle 工具函数(复用器检测、80 字符填充、净化)、formatSessionWindowTitle 实现会话名→回退链、通过 ChatRecordingService 的回调链接、showStatusInTitle 默认值改为 true。无无关重构或范围蔓延。双 OSC 0+2 方案(复用器内单 OSC 2)和 Windows process.title 体现了细致的终端兼容性考量。进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I would have added a writeTerminalTitle helper writing both OSC 0 and OSC 2 sequences with padding, created a sessionName state in AppContainer driven by the ChatRecordingService callback, and gated it behind showStatusInTitle. The PR matches this approach and exceeds it: multiplexer detection (tmux/screen/zellij/dvtm → single OSC 2, no padding to avoid cluttering multiplexer window lists), process.title on Windows for Task Manager, sanitizeForOsc reuse from the existing OSC 8 module, and try-catch in the exit handler for EPIPE resilience.

No critical blockers found. The implementation is clean:

  • writeTerminalTitle correctly handles all branches: multiplexer vs standalone, empty vs non-empty titles, long titles (truncate to 80 chars), Windows vs POSIX.
  • AppContainer's title useEffect dependencies are correct — sessionName, settings, config. streamingState and thought are correctly removed (no more model-activity flicker).
  • Callback chaining in AppContainer preserves Session's existing ACP notification callback via getTitleRecordedCallback() — clean composition, not replacement.
  • showStatusInTitle === false path writes the static fallback once and returns, preventing title writes for users who opt out.
  • gemini.tsx setWindowTitle exit handler uses try-catch to handle EPIPE when stdout is closed — good defensive coding.

Unit Tests

$ cd packages/cli && npx vitest run src/utils/windowTitle.test.ts
 ✓ src/utils/windowTitle.test.ts (21 tests) 11ms
 Test Files  1 passed (1)
      Tests  21 passed (21)

$ cd packages/cli && npx vitest run src/ui/AppContainer.test.tsx
 ✓ src/ui/AppContainer.test.tsx (93 tests) 2927ms
 Test Files  1 passed (1)
      Tests  93 passed (93)

TypeScript typecheck: clean (exit 0, no errors).

Escape Sequence Verification

Direct Node.js test of the built PR code:

Test 1 - Default (no session, with folder):
  Title: "Qwen - qwen-code"
  Seq: "\x1b]0;Qwen - qwen-code<padded to 80>\x07\x1b]2;Qwen - qwen-code<padded to 80>\x07"

Test 2 - Session name set:
  Title: "Fix terminal title"
  Seq: "\x1b]0;Fix terminal title<padded to 80>\x07\x1b]2;Fix terminal title<padded to 80>\x07"

Test 3 - Inside tmux:
  Title: "My Session"
  Seq: "\x1b]2;My Session\x07"          ← single OSC 2, no padding (correct for multiplexers)

Test 4 - CLI_TITLE set:
  Title: "Custom Title"
  Seq: "\x1b]0;Custom Title<padded to 80>\x07\x1b]2;Custom Title<padded to 80>\x07"

Test 5 - Sanitized:
  Title: "BadTitle[31m"                  ← \x07 and \x1b stripped
  Seq: "\x1b]0;BadTitle[31m<padded to 80>\x07\x1b]2;BadTitle[31m<padded to 80>\x07"

Test 6 - Padding:
  Padded length: 80                      ← verified

Interactive Smoke Test

$ npm run dev  # from worktree with PR code

   ▄▄▄▄▄▄  ▄▄     ▄▄ ▄▄▄▄▄▄▄ ▄▄▄    ▄▄   ┌──────────────────────────────────────────┐
  ██╔═══██╗██║    ██║██╔════╝████╗  ██║  │ >_ Qwen Code (vdev)                      │
  ██║   ██║██║ █╗ ██║█████╗  ██╔██╗ ██║  │                                          │
  ██║▄▄ ██║██║███╗██║██╔══╝  ██║╚██╗██║  │ API Key | qwen3.7-max (/model to change) │
  ╚██████╔╝╚███╔███╔╝███████╗██║ ╚████║  │ ~/work/.../worktrees/triage              │
   ╚══▀▀═╝  ╚══╝╚════╝ ╚══════╝╚═╝  ╚═══╝  └──────────────────────────────────────────┘

  Tips: You can resume a previous conversation by running qwen --continue or qwen --resume.

Startup succeeds. tmux consumed the OSC 2 sequences (they don't appear in capture-pane output, confirming they were processed by the multiplexer).

Before (installed build, v0.18.3)

$ qwen  # interactive mode in tmux

   ▄▄▄▄▄▄  ▄▄     ▄▄ ▄▄▄▄▄▄▄ ▄▄▄    ▄▄   ┌──────────────────────────────────────────┐
  ██╔═══██╗██║    ██║██╔════╝████╗  ██║  │ >_ Qwen Code (v0.18.3)                   │
  ...
>   Type your message or @path/to/file

tmux window_name: "node"  ← old code's OSC 2 overridden by automatic-rename
中文说明

代码审查

独立方案对比: PR 的方案与我的独立设计一致并在细节上超越:复用器检测(tmux/screen/zellij/dvtm → 单 OSC 2、不填充以避免污染复用器窗口列表)、Windows process.title、复用现有 sanitizeForOsc、退出处理器中的 try-catch 防 EPIPE。

未发现关键阻断问题。实现干净:所有分支处理正确,useEffect 依赖正确,回调链保留 ACP 通知,showStatusInTitle === false 路径正确。

单元测试

21 + 93 = 114 个测试全部通过。TypeScript 类型检查干净。

转义序列验证

直接 Node.js 测试确认:默认标题双 OSC 0+2 带 80 字符填充、会话名称正确、tmux 内单 OSC 2 无填充、CLI_TITLE 生效、控制字符净化正确、填充长度验证。

交互测试

npm run dev 启动成功,tmux 消费了 OSC 2 序列。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — local merge build + byte-level title harness

Verified the 3-way merge result (origin/main f3e0dd6 + this PR 136080b) on macOS (Darwin arm64, Node v22.22.2). Two touched files had drifted on main since the PR's base; the merge auto-resolved cleanly and the diff is exactly the PR's +606/-129. Since the user-facing surface (OSC escape bytes) isn't visible in unit assertions, I drove the new helpers with a byte-capture harness in addition to the real suites.

1. Build / tests / typecheck / lint (merge result)

Check Result
windowTitle.test.ts 21 passed
AppContainer.test.tsx 93 passed (PR added +334 lines of wiring tests)
tsc --noEmit core ✅ clean
tsc --noEmit CLI ✅ changed files clean after rebuilding core dist — see note below
npm run build core ✅ OK
eslint + prettier --check ✅ clean
CI ✅ green on macOS / Windows / Linux; bot approved

Stale-dist note: before rebuilding core, the local CLI typecheck flagged getTitleRecordedCallback does not exist on ChatRecordingService at AppContainer.tsx:1045. That method is added by this PR in core/src/services/chatRecordingService.ts; the CLI typechecks against core's built dist, and CI builds core before typechecking the CLI (so CI is correct). After npm run build in core, the error clears — it's an environment artifact, not a real issue.

2. Byte-level behavior of the new writeTerminalTitle (AFTER)

Captured the exact bytes written (<ESC>=\x1b, <BEL>=\x07, <Nsp>=N spaces):

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 falsetrue (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:1045getTitleRecordedCallback 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 翻为 truesettingsSchema.ts 和 vscode 的 settings.schema.json 都改了)。标题特性现在默认开启——即默认情况下终端标题会变成会话名(在记录到名字之前为 Qwen - <folder>),而不是保持原样。这是本 PR 的主要产品决策,其余都是机制。值得有意识地签字确认,但与 PR 的既定意图一致。

范围说明

本次验证覆盖到单元 + 字节级 + AppContainer 集成测试(后者覆盖 sessionNametitleRecordedCallback / /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。

@wenshao
wenshao merged commit dab62e5 into QwenLM:main Jun 18, 2026
164 checks passed
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.

3 participants