fix(cli): prevent transcript re-scroll on tab-switch in terminal multiplexers - #6055
fix(cli): prevent transcript re-scroll on tab-switch in terminal multiplexers#6055chiga0 wants to merge 1 commit into
Conversation
…-switch A resize/SIGWINCH whose dimensions are unchanged still forced Ink's renderInteractiveFrame down its overflow path, which writes `clearTerminal + fullStaticOutput + output` — re-streaming the ENTIRE scrollback so the transcript visibly scrolls from top to bottom. Terminal multiplexers (tmux / cmux / screen) routinely re-send SIGWINCH with identical dimensions when a pane regains focus, so every switch back into a tab running a long task replayed the whole transcript before settling at the bottom. Patch upstream ink 7.0.3 (via patch-package) so `resized()` returns early when neither columns nor rows changed: an identical-dimension resize carries no layout work, and multiplexers preserve pane content across focus, so there is nothing to repaint. Track `lastTerminalHeight` alongside `lastTerminalWidth`. Verified with a fake-TTY harness: before, a same-dimension resize while the dynamic frame exceeds the viewport emits `clearTerminal` + the full static scrollback (12/12 history lines re-emitted); after, it is a no-op (0 re-emitted). A genuine dimension change still relayouts and renders as before. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Thanks for the PR! Template looks good ✓ (has all substantive sections — What, Why, Reviewer Test Plan, Risk & Scope. Minor: the "Tested on" OS table and 中文说明 are missing, but the verification content is solid.) On direction: this solves a real, annoying problem — tmux/cmux users switching back to a tab and watching the entire transcript scroll from top to bottom. The project already has resize-related work ( On approach: this is about as minimal as it gets — 48 lines in a single patch file, touching only ink's One note: the PR honestly flags that it couldn't verify in real cmux — just the mechanism via a fake-TTY harness. That's a fair caveat and worth a maintainer sanity-check on a real multiplexer setup before merging. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓(核心部分齐全——What、Why、Reviewer Test Plan、Risk & Scope。小问题:缺少"Tested on"系统测试表和中文说明,但验证内容充分。) 方向:解决了一个真实的痛点——tmux/cmux 用户切回标签时看到整个会话记录从顶部滚到底部。项目已有相关 resize 处理工作(#4891 的 方案:非常精简——48 行,单文件 patch,只修改 ink 的 注意:PR 坦诚说明无法在真实 cmux 中验证——只通过假 TTY 测试验证了机制。这是合理的提醒,合并前值得维护者在真实复用器环境中确认。 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): the bug is in ink's Comparison with the diff: the PR's approach matches my independent proposal exactly. The implementation is surgical and correct:
The 8-line comment explaining the multiplexer SIGWINCH behavior is genuinely useful context for future maintainers — not over-commenting. No correctness bugs, no security issues, no regressions. No reuse concerns — this touches ink internals where no shared utility exists. Test Results
Tmux Testing (limited)The resize-replay bug is a visual scrolling animation that requires a real terminal to observe. The PR author verified the mechanism with a fake-TTY harness (same-dimension resize → Before (installed qwen — no patch): interactive session runs, SIGWINCH sent, static capture unchanged (animation not capturable): After (this PR): patch applies, build succeeds, all rendering tests pass. The 中文说明代码审查独立方案(阅读 diff 前):bug 在 ink 的 与 diff 对比: PR 方案与我的独立方案完全一致。实现精准且正确:
解释复用器 SIGWINCH 行为的注释对未来维护者有实际价值。 无正确性 bug、无安全问题、无回归。 测试结果
Tmux 测试(受限)resize-replay bug 是需要在真实终端中观察的视觉滚动动画。 PR 作者通过假 TTY 测试验证了机制(相同尺寸 resize → 修复前发出 — Qwen Code · qwen3.7-max |
|
Stepping back: this is a clean, minimal patch that solves a real user-facing problem. The scrollback-replay-on-tab-switch is the kind of bug that makes tmux users question whether the tool is stable — it's visually jarring even if functionally harmless. The approach is exactly what I'd have proposed independently: track height alongside the existing width tracking in ink's What I like: the PR author is honest about what they could and couldn't verify. They built a fake-TTY harness to confirm the mechanism (same-dimension resize → fullStaticOutput replay), showed that the fix eliminates it, and explicitly flagged that real cmux verification is still needed. That's the right level of rigor for a contributor who can't access the original reporter's setup. My one reservation: this patches a vendored dependency (ink 7.0.3). If ink releases 7.0.4 with internal changes, this patch may need updating. But the patch is well-isolated (single method, clear intent), and the existing The tmux visual test couldn't be automated in CI (the bug is an animation, not a state change), but the mechanism is verified, the code is correct, and all rendering tests pass. I'm comfortable approving. One request for the maintainer: if possible, do a quick sanity check in real tmux/cmux before merging — just switch tabs a few times during a long session and confirm the transcript stays put. The author was transparent that they couldn't verify this themselves. Approving. ✅ 中文说明总结:这是一个干净、精简的 patch,解决了真实的用户可见问题。tab 切换时的 scrollback 重播是那种让 tmux 用户怀疑工具稳定性的 bug——视觉上令人不安,即使功能上无害。 方案与我的独立提案完全一致:在 ink 的 优点:PR 作者坦诚说明了能验证和不能验证的内容。他们构建了假 TTY 测试工具确认机制(相同尺寸 resize → fullStaticOutput 重播),证明修复消除了该问题,并明确指出仍需要真实 cmux 验证。对于无法访问原始报告者环境的贡献者来说,这是正确的严谨程度。 一个保留意见:这修改了 vendored 依赖(ink 7.0.3)。如果 ink 发布 7.0.4 并更改内部实现,此 patch 可能需要更新。但 patch 隔离良好(单一方法,意图清晰),现有的 tmux 视觉测试无法在 CI 中自动化(bug 是动画,不是状态变化),但机制已验证,代码正确,所有渲染测试通过。可以批准。 对维护者的一个请求:如果可能,在合并前在真实 tmux/cmux 中快速确认——在长会话中切换几次标签,确认记录保持不动。作者坦诚说明自己无法验证这一点。 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Closing after verification. The root cause of the reported 'transcript replays top→bottom on tab-switch' is NOT a resize: it's that ink re-emits |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. The patch is a clean, minimal fix that correctly addresses the transcript re-scroll bug in terminal multiplexers. The early-return guard logic is sound — verified that TmuxBackend's force-repaint path is unaffected, onRender() side effects are irrelevant for no-op resizes, and initialization ordering is correct. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
What this PR does
Stops the whole transcript from re-scrolling top→bottom when you switch back to a tab running a long task in terminal multiplexers (tmux/cmux/screen).
Patches upstream ink 7.0.3 (via
patch-package) soInk.resized()returns early when the terminal dimensions are unchanged, and trackslastTerminalHeightalongside the existinglastTerminalWidth.Why it's needed
In non-VP (default) mode the whole history lives in Ink's
<Static>region, accumulated inthis.fullStaticOutput. When the dynamic (non-<Static>) frame is taller than the viewport,renderInteractiveFrametakes its overflow path and writesclearTerminal + this.fullStaticOutput + output— i.e. it clears the screen+scrollback and re-prints the entire session transcript, which visibly scrolls from top to bottom (and takes a while on long sessions).resized()unconditionally callsonRender()on everyresize/SIGWINCH, even when dimensions didn't change. Terminal multiplexers routinely re-send SIGWINCH with identical dimensions when a pane regains focus, so each switch-back into a running task triggered this full replay. A same-dimension resize has no layout work to do, and multiplexers preserve pane content across focus, so the correct behavior is to do nothing.This is not caused by #6015 (the non-VP scroll-snapback / wheel PR) — that PR never touched
resized()/fullStaticOutput/shouldClearTerminalForFrame; it shares only the same overflow mechanism. This is stock upstream ink behavior.Reviewer Test Plan
How to verify
Fake-TTY harness (viewport 10 rows, dynamic frame 14 rows → overflow), emit a
resizewith unchanged dimensions:clearTerminalemittedA genuine dimension change still relayouts + renders (the early-return only fires when both
columnsandrowsare unchanged). Existing rendering suites pass (InlineParallelAgentsDisplay,ScrollableList,VirtualizedListincl. auto-scroll = 46 tests). Patch applies cleanly from pristine (patch-package→ink@7.0.3 ✔); the existingpackage.jsonexports hunk is preserved.Manual: in tmux/cmux, non-VP mode, run a long task, scroll to the bottom, switch to another tab and back. Before: the transcript replays from the top and scrolls for a while before reaching the bottom. After: the view stays put.
Evidence (Before & After)
N/A — non-UI code change in vendored ink; behavior confirmed via fake-TTY harness output above.
Tested on
Environment
Local runtime:
npm run dev; fake-TTY harness for automated verification. No sandbox involved — patch-only change to vendored ink.Risk & Scope
fullStaticOutputreplay) and that this fix makes that case a no-op, with a fake-TTY harness. Could not run real cmux locally to confirm its tab-switch emits a same-dimension SIGWINCH (the common case for tmux/screen). If cmux instead emits a changed-dimension resize on switch, a deeper follow-up is needed (avoid re-emitting the fullfullStaticOutputon the overflow-clear path). Please sanity-check on the original cmux setup.Linked Issues
Ref #6015 (related non-VP scroll-snapback fix; different root cause — see "Why it's needed" above).
中文说明
本 PR 做了什么
修复了在终端复用器(tmux/cmux/screen)中切换回正在运行长任务的标签页时,整个会话记录从上到下重新滚动的问题。
通过
patch-package修补上游 ink 7.0.3,使Ink.resized()在终端尺寸未变时提前返回,并新增lastTerminalHeight与已有的lastTerminalWidth配套跟踪。为什么需要修复
在非 VP(默认)模式下,完整历史记录存储在 Ink 的
<Static>区域(this.fullStaticOutput)。当动态帧(非<Static>部分)高度超过视口时,renderInteractiveFrame走溢出路径,写入clearTerminal + this.fullStaticOutput + output——即清屏+清滚动缓冲并重新打印整个会话记录,导致可见的从上到下滚动(长会话时耗时明显)。resized()在每次resize/SIGWINCH时无条件调用onRender(),即使尺寸没变。终端复用器在面板重新获得焦点时常会发送尺寸完全相同的 SIGWINCH,因此每次切回运行中的任务标签页都会触发完整重放。尺寸未变的 resize 不需要做任何布局工作,且复用器在焦点切换时保留面板内容,所以正确行为是什么都不做。此问题不是 #6015(非 VP 滚动回弹/滚轮 PR)导致的——那个 PR 从未修改
resized()/fullStaticOutput/shouldClearTerminalForFrame,仅共享了相同的溢出机制。这是上游 ink 的原生行为。审阅者测试计划
如何验证
Fake-TTY 测试工具(视口 10 行,动态帧 14 行 → 溢出),发送尺寸不变的
resize:clearTerminal真正的尺寸变化仍会重新布局和渲染(提前返回仅在
columns和rows均未变时触发)。现有渲染测试套件全部通过(InlineParallelAgentsDisplay、ScrollableList、VirtualizedList含自动滚动 = 46 个测试)。补丁从干净状态应用成功(patch-package→ink@7.0.3 ✔);保留了现有的package.jsonexports hunk。手动验证:在 tmux/cmux 非VP 模式下,运行长任务,滚动到底部,切换到另一个标签页再切回。修复前: 会话记录从顶部重放并滚动一段时间才到达底部。修复后: 视图保持不动。
测试环境
运行环境
本地运行:
npm run dev;使用 fake-TTY 工具进行自动化验证。无沙箱——仅修改 vendored ink 补丁。风险与范围
fullStaticOutput重放)及此修复使该情况变为无操作,使用 fake-TTY 工具。无法在本地运行真正的 cmux 来确认其标签切换是否发送同尺寸 SIGWINCH(tmux/screen 的常见情况)。如果 cmux 在切换时发送的是尺寸变化的 resize,则需要更深入的后续修复(避免在溢出清屏路径上重新输出完整fullStaticOutput)。请在原始 cmux 环境上验证。关联 Issue
参考 #6015(相关的非 VP 滚动回弹修复;根因不同——见上方"为什么需要修复")。