fix(cli): pop the kitty keyboard protocol after leaving the alternate screen - #7115
Conversation
… screen The kitty keyboard protocol is enabled (ESC[>1u) on the main screen before Ink renders, but the exit cleanup wrote the pop (ESC[<u) before instance.unmount(). With ui.useTerminalBuffer enabled, Ink runs in the alternate screen, and the kitty spec tracks keyboard flags per screen: the pop landed on the alternate screen's empty stack, unmount then left the alternate screen, and the main screen's flags stayed set. After exit, the shell received kitty escape codes (e.g. "9;5u" for Ctrl-C) until the user ran `reset` or `printf '\e[<u'`. Move disableKittyProtocol() after instance.unmount() so the pop is written once the alternate screen (when enabled) has been left and applies to the main screen where the protocol was pushed. The default (no alternate screen) path is unaffected — verified with a PTY harness that emulates a kitty-capable terminal in both configurations. Fixes QwenLM#6776 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verification ReportAll verification was done locally on this branch (base: 1. E2E reproduction — real CLI in a PTY, no mocksA Python harness forks the real bundled CLI (
Since the kitty spec tracks keyboard flags per screen, the unpatched order means the main screen keeps flags=1 after the process exits — the shell then receives kitty key encodings such as 2. Unit tests3. Static checks
Behavior notes for reviewers
中文版本验证报告所有验证均在本分支本地完成(基线: 1. E2E 复现 — PTY 中运行真实 CLI,非 mockPython harness 在伪终端中 fork 真实打包产物(
由于 kitty 规范按屏幕独立跟踪键盘 flags,未打补丁的顺序意味着进程退出后主屏 flags=1 仍生效——shell 收到 2. 单元测试
3. 静态检查
审阅要点
🤖 Generated with Claude Code |
|
Thanks for the PR! Template looks good ✓ Problem: observed bug with strong evidence. #6776 reports garbled terminal after Ctrl-C exit, and the PR includes a PTY harness that captures byte-level ordering of the push/pop/alt-screen-off sequences — confirming the pop lands inside the alternate screen. The linked issue is open with a clear reproduction. Direction: aligned. Terminal state restoration on exit is a core CLI responsibility, and the kitty keyboard protocol ordering is a well-understood spec requirement (per-screen flag tracking). No CHANGELOG reference, but this is squarely within CLI output hygiene. Size: not applicable — changes are in Approach: minimal and focused. The fix is a single-line reordering — Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测到的 bug,有充分证据。#6776 报告了 Ctrl-C 退出后终端乱码,PR 包含一个 PTY harness,在字节级别捕获了 push/pop/离开备屏序列的顺序——确认 pop 落在了备用屏幕内。关联 issue 处于 open 状态,复现清晰。 方向:对齐。退出时的终端状态恢复是 CLI 的核心职责,kitty 键盘协议的顺序是规范中明确的要求(按屏幕独立跟踪 flags)。CHANGELOG 中没有直接引用,但这完全属于 CLI 输出规范范畴。 规模:不适用——改动在 方案:最小化且聚焦。修复是一行重排—— 进入代码审查 🔍 — Qwen Code · qwen3.7-max Reviewed at |
Code ReviewIndependent proposal: the protocol push ( The PR's approach matches this exactly. The production change is 1 line of logic (reordering Reuse-before-new-code: not applicable — no new logic is introduced. Test ResultsApplied the PR diff locally and ran the full test suite from All 42 tests pass, including the new regression test. The regression test uses Real-Scenario Testingtmux testing is not applicable for this fix: the bug is about escape sequence ordering in the output stream (kitty keyboard protocol per-screen flag tracking), not a visual TUI change. tmux does not implement the kitty keyboard protocol, so the bug cannot be reproduced in a tmux session. The PR's PTY harness — which runs the real bundled CLI inside a pseudo-terminal that answers kitty protocol detection probes — provides stronger evidence than tmux could: byte-level ordering proof showing push@14 → alt-on@41 → alt-off@22593 → pop@22613 with this PR (vs push@14 → alt-on@41 → pop@33919 → alt-off@33945 on unpatched main). Note: the pre-existing typecheck errors in 中文说明代码审查独立方案:协议的 push( PR 的方案与此完全一致。生产代码改动是 1 行逻辑(重排 测试结果本地应用 PR diff 后从 真实场景测试tmux 测试不适用于此修复:bug 是关于输出流中转义序列的顺序(kitty 键盘协议按屏幕 flag 跟踪),而非可视化 TUI 变更。tmux 不实现 kitty 键盘协议,无法在 tmux 会话中复现此 bug。PR 的 PTY harness——在应答 kitty 协议检测探测的伪终端中运行真实打包产物——提供了比 tmux 更强的证据:字节级别的顺序证明,显示 push@14 → alt-on@41 → alt-off@22593 → pop@22613(而 main 上是 push@14 → alt-on@41 → pop@33919 → alt-off@33945)。 注: — Qwen Code · qwen3.7-max Reviewed at |
|
Confidence: 5/5 — clean, minimal fix for a documented bug with byte-level evidence and a solid regression test. This is exactly the kind of PR the gate exists to let through: a focused, one-line reorder that solves a real user-facing problem (terminal corruption on exit in kitty-protocol terminals), backed by a PTY harness that proves the fix at the byte level, and a regression test that locks in the ordering constraint. The comment on the moved line explains the why clearly — per-screen flag tracking in the kitty spec — so future maintainers won't need to re-derive the reasoning. Approving. ✅ 中文说明信心度:5/5 — 干净、最小化的修复,针对一个有文档记录的 bug,有字节级证据和可靠的回归测试。 这正是 gate 存在的意义——让这类 PR 通过:聚焦的单行重排,解决真实的用户问题(kitty 协议终端退出后终端乱码),有 PTY harness 在字节级别证明修复有效,回归测试锁定了顺序约束。移动行的注释清晰解释了原因——kitty 规范中的按屏幕 flag 跟踪——未来的维护者无需重新推导。 批准 ✅ — Qwen Code · qwen3.7-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
Independent Verification Report (local build + real E2E)Independently verified at head Verdict: the fix is confirmed end-to-end and is structurally sound, not timing-dependent. LGTM — ready to merge. 1 · A/B byte-order E2E (real bundle in a PTY)The harness emulates a kitty-protocol terminal (answers the CLI's
Matches the author's numbers in shape exactly, from an independent harness. Bundle provenance was checked before driving: the PR chunk 2 · Unit tests + mutation check of the new regression test
3 · Why the fix is structural, not lucky timingIn the patched ink 7.0.3 actually shipped ( Two more review checks that came back clean:
4 · Mergeability
Not covered hereSame scope limits the PR itself states: Linux/Windows not re-verified locally (the mechanism is platform-independent escape-sequence ordering), and abnormal terminations that never reach the cleanup chain while the alt screen is active remain a pre-existing broader issue. 中文版本(Chinese version)独立验证报告(本地构建 + 真实 E2E)在隔离 worktree 中于 head 结论:修复端到端确认有效,且在结构上有保证、不依赖时序运气。LGTM,可以合并。 1 · A/B 字节序 E2E(PTY 中驱动真实产物)harness 模拟 kitty 协议终端(应答 CLI 的
与作者报告的形态完全一致,且来自独立实现的 harness。驱动前已核验产物来源:PR chunk 2 · 单元测试 + 新回归测试的突变检验
3 · 为什么修复是结构性的、而非时序运气实际发布使用的补丁版 ink 7.0.3( 另外两项审查检查均无问题:
4 · 可合并性
未覆盖范围与 PR 自述一致:未在 Linux/Windows 本地复验(机制为平台无关的转义序列顺序);备屏激活期间未进入清理链的异常终止属既有更大问题,不在本 PR 范围内。 |



What this PR does
Restores the terminal's keyboard state correctly on exit when the alternate screen buffer is in use. The Kitty keyboard protocol pop sequence is now written after Ink has unmounted — that is, after the alternate screen (when
ui.useTerminalBufferis enabled) has been left — so the pop applies to the main screen, where the protocol was originally pushed. Previously the pop was written while the alternate screen was still active, popping that screen's empty stack and leaving the main screen's keyboard flags permanently set.Why it's needed
The kitty keyboard protocol spec tracks progressive-enhancement flags per screen (main and alternate screens have independent state). Qwen Code pushes the flags (
ESC[>1u) on the main screen at startup, before Ink renders. Withui.useTerminalBuffer: true, Ink then enters the alternate screen, and the exit cleanup randisableKittyProtocol()beforeinstance.unmount()— so the pop (ESC[<u) landed inside the alternate screen and the main screen's flags survived the exit. On kitty-protocol terminals (Ghostty, Kitty, WezTerm), the user's shell then receives kitty escape fragments like9;5uon Ctrl-C until they runresetorprintf '\e[<u'— exactly the symptom in #6776, including the follow-up report that a clean/quitalso reproduces it.Reviewer Test Plan
How to verify
ui.useTerminalBuffer: truein~/.qwen/settings.json.qwen, then exit (double Ctrl-C or/quit).9;5u;printf '\e[<u'fixes it. After this PR: the shell behaves normally.useTerminalBuffer(default) — behavior is unchanged and the terminal is restored correctly.Automated coverage: a new regression test asserts
disableKittyProtocol()is invoked only afterinstance.unmount()in the exit cleanup (gemini.test.tsx, 42/42 pass).npm run typecheck, eslint and prettier are clean on the changed files.Evidence (Before & After)
A PTY harness runs the real bundled CLI (
dist/cli.js, no mocks) inside a pseudo-terminal that answers the kitty-protocol detection probes (ESC[?u/ESC[c), drives a double-Ctrl-C exit, captures every output byte, and checks the order of the push (ESC[>1u), pop (ESC[<u), and alt-screen off (ESC[?1049l) sequences:useTerminalBuffer: true)origin/main@30984a2f5)Tested on
Environment (optional)
macOS (Darwin 24.6), Node v22.23.1; PTY harness via Python
ptymodule against the esbuild bundle, plus vitest unit tests.Risk & Scope
process.on('exit'/'SIGINT'/'SIGTERM')fallbacks inkittyProtocolDetector.tsare untouched and still cover abnormal exits.Linked Issues
Fixes #6776
中文说明
本 PR 做了什么
修复使用备用屏幕缓冲区时退出后终端键盘状态未恢复的问题。Kitty 键盘协议的 pop 序列现在改为在 Ink unmount 之后写入——即(启用
ui.useTerminalBuffer时)已离开备用屏幕之后——使 pop 作用于最初 push 协议的主屏幕。此前 pop 在备用屏幕仍激活时写入,弹的是备屏的空栈,主屏的键盘 flags 被永久残留。为什么需要
kitty 键盘协议规范中,渐进增强 flags 是按屏幕独立跟踪的(主屏与备屏状态互不影响)。Qwen Code 在启动时(Ink 渲染之前)在主屏 push flags(
ESC[>1u)。当ui.useTerminalBuffer: true时,Ink 随后进入备屏,而退出清理在instance.unmount()之前调用了disableKittyProtocol()——pop(ESC[<u)落在备屏内,主屏的 flags 在退出后仍然生效。在 kitty 协议终端(Ghostty、Kitty、WezTerm)上,退出后 shell 里按 Ctrl-C 会出现9;5u这类片段,必须运行reset或printf '\e[<u'才能恢复——与 #6776 的症状完全一致,包括后续评论中"干净的/quit也复现"。审阅测试计划
如何验证
~/.qwen/settings.json的ui.useTerminalBuffer: true;qwen后退出(双击 Ctrl-C 或/quit);9;5u之类的片段,printf '\e[<u'可恢复。本 PR 之后:shell 行为正常;useTerminalBuffer(默认)重复上述步骤——行为不变,终端正确恢复。自动化覆盖:新增回归测试断言退出清理中
disableKittyProtocol()仅在instance.unmount()之后调用(gemini.test.tsx,42/42 通过)。typecheck / eslint / prettier 全部通过。证据(Before & After)
PTY harness 在伪终端中运行真实打包产物(
dist/cli.js,非 mock),应答 kitty 协议探测查询,驱动双 Ctrl-C 退出,捕获全部输出字节并检查 push / pop / 离开备屏序列的顺序(见上方表格与截图)。测试平台
macOS 已本地验证(✅);Windows / Linux 依赖 CI(⚠️ )。
环境
macOS(Darwin 24.6)、Node v22.23.1;Python
pty模块驱动 esbuild 打包产物 + vitest 单测。风险与范围
kittyProtocolDetector.ts中process.on('exit'/'SIGINT'/'SIGTERM')的兜底逻辑未改动,异常退出仍有覆盖。关联 Issue
Fixes #6776
🤖 Generated with Claude Code