fix(cli): eliminate banner duplication and drag flicker on resize/wake - #8831
Conversation
useWakeRepaint (#7265) repaints via refreshStatic after sleep/wake or SIGCONT, but in VP mode (default) refreshStatic neither cleared the screen nor repainted anything (<Static> is not rendered in VP), so Ink's next relative erase ran against a stale/rearranged terminal buffer: frame-top residue (banner), frame-height jumps and high-frequency flicker on every terminal. Blank the alternate-screen viewport (2J+H, no 3J so scrollback / Warp block history survives) before the remount-driven repaint; static mode keeps its existing clearTerminal. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Thanks for the PR — the root-cause write-up here is unusually thorough.
Moving on to code review. 🔍 中文说明感谢贡献——这份根因分析写得相当扎实。
进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewThe escape choice checks out. I verified against ansi-escapes 7.3.0 (the version locked in package-lock.json): One concern I can't settle from the diff, and it's about the mechanism the fix relies on:
CI evidence
The red check is not caused by the diff. The ubuntu job died in its setup step, before a single test ran: Sandboxed verificationThe central claim here is behavioural — "one clean repaint on wake, no stuck blank screen" — and nothing so far substantiates it: CI hasn't run the suite, the unit test only asserts the escape write, and the PR itself says the after-state was never captured. Sandboxed verification would settle it: 中文说明代码审查:转义选择已核实无误——对照 package-lock 锁定的 ansi-escapes 7.3.0 源码: 一个无法仅靠 diff 定论的顾虑,也是这个修复依赖的机制本身:ink 7.0.3 的写路径是帧级"全有或全无"——输出与上一帧相同就什么都不写;而 VP 模式下没有任何东西以 另外: CI 证据:见上表。红色检查不是 diff 造成——ubuntu 任务在 setup 步骤就失败( 沙箱验证:核心主张是行为性的("唤醒后一次干净重绘、不卡空白"),目前没有任何证据支撑。 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 2/5 — right direction, right escape, excellent root-cause analysis; but the repaint half of the mechanism is unsubstantiated and may not fire on the one trigger this PR targets, and CI never ran the suite. Stepping back: my independent proposal for this bug was exactly what this PR does — blank the viewport (ED 2, deliberately not 3J) before the remount-driven repaint, scrollback preserved. So the approach isn't what I'm doubting; it's the "repaint" half of that sentence. The clear is guaranteed by the diff. The repaint is asserted but not shown: ink writes nothing when the frame is byte-identical, nothing in the VP path is keyed by the remount bump, and wake — unlike model switch or the Ctrl+O toggle — carries no other state change that would alter the frame. If that read is right, VP mode after wake trades stacked banners for a blank screen until the next keystroke. Better, maybe, but not the fix promised, and the unit test cannot tell the two outcomes apart. Two concrete things would move this to approve:
Requesting changes on those points. The analysis quality is high; I'd much rather see this land verified than bounce. 中文说明置信度:2/5 —— 方向正确、转义选择正确、根因分析出色;但"重绘"这半边机制没有证据,且在本 PR 针对的那个触发点上可能根本不会发生;CI 也一次都没跑过测试套件。 我自己的独立方案与这个 PR 完全一致(唤醒时先用 ED 2 清视口、刻意不用 3J、保留 scrollback),所以我质疑的不是方案,而是"清屏之后靠 remount 重绘"里的重绘:ink 在帧内容逐字节不变时什么都不写;VP 路径没有任何东西以 remount key 为 key;而 wake 与切 model、Ctrl+O 不同,不带任何会改变帧内容的状态变化。如果这个推断成立,VP 模式唤醒后会把"banner 叠印"换成"空白屏直到下一次按键"——就算更好,也不是 PR 承诺的修复,而且单测区分不了这两种结果。 两件事可以把它推到 approve:
基于以上两点 request changes。分析质量很高,更希望它被验证后合入,而不是被打回。 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Two things before this can land: show the wake-time repaint actually happens after the clear (or force it — see my notes above), and merge main so CI can run the suite. Details in the review comments. 🙏
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): PR #8831 (QwenLM/qwen-code): clears the virtual-viewport ...: running the unit tests / typecheck for the changed files (worktree has no installed node_modules ).; PR #8831 (QwenLM/qwen-code): clears the virtual-viewport ...: live-terminal wake/SIGCONT repro of the blank-screen mechanism in Finding 1 (static analysis of ink 7.0.3 only).; PR #8831 (QwenLM/qwen-code): clears the virtual-viewport ...: live-TTY end-to-end reproduction of the blank screen (this CI box has no interactive terminal; the mechanism was verified entirely against ink 7.0.3 sources + t….
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
[Critical] R1-2 The PR body's "Does not close #8557" matches GitHub's closing-keyword parser (it ignores negation), so merging will auto-close #8557 against the author's stated intent. gh pr view 8831 --json closingIssuesReferences returns [#8557] (verified twice; control PR #8832, whose body has no closing keyword adjacent to the number, returns empty). #8557's originally observed behavior (transcript re-printed into scrollback when shrinking the terminal) is not fixed by this diff and is documented in the issue thread as unfixable app-side — merging would close an unresolved bug report's open tracking thread. Fix: reword the PR body so no closing keyword is adjacent to the issue number (e.g. "This PR does not close issue #8557."), then confirm gh pr view 8831 --json closingIssuesReferences returns empty before merging.
中文说明
PR 描述中的“Does not close #8557”命中了 GitHub 的关闭关键字解析器(它忽略否定词),因此合并将违背作者明示的意图自动关闭 #8557。gh pr view 8831 --json closingIssuesReferences 返回 [#8557](已两次验证;关键字未与编号相邻的对照 PR #8832 返回空)。#8557 最初观察到的行为(缩小终端窗口时转录块被重新打印进滚动区)并未被本 diff 修复,且 issue 线程中已记录该问题应用侧无法修复——合并会关闭一个未解决 bug 报告的开放跟踪线程。修复:改写 PR 描述,使关闭关键字不与 issue 编号相邻(例如“This PR does not close issue #8557.”),并在合并前确认 gh pr view 8831 --json closingIssuesReferences 返回为空。
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):PR #8831 (QwenLM/qwen-code): clears the virtual-viewport ...:running the unit tests / typecheck for the changed files (worktree has no installed node_modules ).;PR #8831 (QwenLM/qwen-code): clears the virtual-viewport ...:live-terminal wake/SIGCONT repro of the blank-screen mechanism in Finding 1 (static analysis of ink 7.0.3 only).;PR #8831 (QwenLM/qwen-code): clears the virtual-viewport ...:live-TTY end-to-end reproduction of the blank screen (this CI box has no interactive terminal; the mechanism was verified entirely against ink 7.0.3 sources + t…。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
[Critical] R1-2 The PR body's "Does not close #8557" matches GitHub's closing-keyword parser (it ignores negation), so merging will auto-close #8557 against the author's stated intent. gh pr view 8831 --json closingIssuesReferences returns [#8557] (verified twice; control PR #8832, whose body has no closing keyword adjacent to the number, returns empty). #8557's originally observed behavior (transcript re-printed into scrollback when shrinking the terminal) is not fixed by this diff and is documented in the issue thread as unfixable app-side — merging would close an unresolved bug report's open tracking thread. Fix: reword the PR body so no closing keyword is adjacent to the issue number (e.g. "This PR does not close issue #8557."), then confirm gh pr view 8831 --json closingIssuesReferences returns empty before merging.
中文说明
PR 描述中的“Does not close #8557”命中了 GitHub 的关闭关键字解析器(它忽略否定词),因此合并将违背作者明示的意图自动关闭 #8557。gh pr view 8831 --json closingIssuesReferences 返回 [#8557](已两次验证;关键字未与编号相邻的对照 PR #8832 返回空)。#8557 最初观察到的行为(缩小终端窗口时转录块被重新打印进滚动区)并未被本 diff 修复,且 issue 线程中已记录该问题应用侧无法修复——合并会关闭一个未解决 bug 报告的开放跟踪线程。修复:改写 PR 描述,使关闭关键字不与 issue 编号相邻(例如“This PR does not close issue #8557.”),并在合并前确认 gh pr view 8831 --json closingIssuesReferences 返回为空。
— qwen3.8-max via Qwen Code /review (v0.21.8)
|
Qwen Code review timed out. Qwen review timed out after 5400 seconds (of the 90-minute budget). For large PRs, retry with a longer timeout by commenting: |
On shrink the terminal reflows the printed frame into more physical rows than Ink's stale eraseLines count, so every subsequent redraw under-erases and strands the frame top (banner) as stacked duplicates on all terminals (issue #8557). For a short window after a shrink, start each VP redraw from a clean viewport (2J+H); Static mode keeps a conservative reflow-aware amplification so committed scrollback is never touched.
…cker Warp answers the DECRQM 2026 probe with status 2 (recognized, reset), so synchronized updates are available there. Without them Warp renders ink's erase-then-rewrite frame pattern as visible flicker (issue #8557). Add WarpTerminal to the synchronized-output allowlist; the existing QWEN_CODE_DISABLE_SYNCHRONIZED_OUTPUT escape hatch covers regressions.
|
Tracking the remaining cosmetic off-by-one (input-box one-row jitter at certain widths) as follow-up #8849, per maintainer — this PR is not blocked on it. |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
Ink skips redraws whose output is unchanged, so the VP wake/SIGCONT path's viewport clear could leave the screen blank until the next state change (review #8831). The resize-reflow wrapper now caches the last frame that reached the terminal and repaint() replays it over a clean viewport; the stale design-rationale comment is rewritten to match.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): PR #8831 (QwenLM/qwen-code) fixes terminal resize/wake re...: did not verify Ink 7 internals for the assumption that Static appends arrive as separate writes without an erase prefix (relied on the PR's own tests/comments f…; PR #8831 (QwenLM/qwen-code) fixes terminal resize/wake re...: microbenchmark of frameLineWidths cost per redraw (blocked on missing node_modules)..
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):PR #8831 (QwenLM/qwen-code) fixes terminal resize/wake re...:did not verify Ink 7 internals for the assumption that Static appends arrive as separate writes without an erase prefix (relied on the PR's own tests/comments f…;PR #8831 (QwenLM/qwen-code) fixes terminal resize/wake re...:microbenchmark of frameLineWidths cost per redraw (blocked on missing node_modules).。
— qwen3.8-max via Qwen Code /review (v0.21.8)
…aw model handoff, wake-only repaint - Unwind the stdout.write wrapper stack in LIFO order so the identity- guarded restores do not leak wrappers (Critical). - Reset a pending static-mode amplification on grow so a stale count can never over-erase into committed scrollback (Critical). - Hand the frame model over to Ink's bare post-shrink redraw (log.clear resets its counter, so the redraw carries no erase prefix); consecutive shrinks now amplify from the actual post-shrink frame (Critical). - repaint() skips the replay when the cached frame's width differs from the current viewport (Critical). - Route the clear-and-replay through useWakeRepaint only; refreshStatic's VP branch stays write-free for ordinary callers (/clear, model change, ...) so stale frames never flash back (Critical). - Use ansi-escapes exports instead of hand-rolled ANSI constants; drop the duplicated WarpTerminal test row; add tests for the grow reset, the bare- redraw handoff, the MIN_FRAME_LINES guard and the clear-window expiry.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): PR #8831 fixes terminal-independent resize/wake rendering...: live end-to-end reproduction of the static-mode wipe in a real PTY (no node_modules in the review worktree; verified by reading the wrapper + patched/upstream i…; PR #8831 fixes terminal-independent resize/wake rendering...: could not inspect Ink 7.0.3's runtime log.clear() →redraw sequencing (node_modules not installed in the review worktree) — this is the uncertainty behind Findi….
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):PR #8831 fixes terminal-independent resize/wake rendering...:live end-to-end reproduction of the static-mode wipe in a real PTY (no node_modules in the review worktree; verified by reading the wrapper + patched/upstream i…;PR #8831 fixes terminal-independent resize/wake rendering...:could not inspect Ink 7.0.3's runtime log.clear() →redraw sequencing (node_modules not installed in the review worktree) — this is the uncertainty behind Findi…。
— qwen3.8-max via Qwen Code /review (v0.21.8)
- Frame model now lazy and terminal-faithful: per-character greedy packing (wide chars waste a row-tail cell), physical-row segmentation on shrink (terminals re-wrap rows without re-joining), and Ink's cursor-below line included for frames ending with a newline (R3-3, R3-4, R3-5, R3-9). - expectFrame handoff survives Ink's real write sequence: standalone synchronized-output control writes no longer consume it, and consecutive bare writes re-model with last-wins so static commits model the live frame, not the transcript (R3-1, R3-14). - VP wake path bumps historyRemountKey again so one-shot <Static> history (agent tabs) is re-emitted over the clear; selection extracted into buildWakeRepaint for unit coverage (R3-2, R2-8). - Shared erase grammar helpers exported from terminalRedrawOptimizer (R3-13); tests added for the escape hatch, repaint fallbacks, BSU sequences, static commits, trailing newlines and CJK packing (R3-10, R3-11, R3-12).
…ount bump, hardened frame handoff
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): PR #8831 (QwenLM/qwen-code), round 4 reverse audit round ...: none — all checks above were completed within budget.; PR #8831 (QwenLM/qwen-code), round 4 reverse audit round ...: none — all checks I started were completed within budget.; PR #8831 (QwenLM/qwen-code), round 4 reverse audit round ...: none — all checks above completed within budget; the only thing not inspected by design is the test file (another agent's chunk), which limits my Suggestion to ….
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):PR #8831 (QwenLM/qwen-code), round 4 reverse audit round ...:none — all checks above were completed within budget.;PR #8831 (QwenLM/qwen-code), round 4 reverse audit round ...:none — all checks I started were completed within budget.;PR #8831 (QwenLM/qwen-code), round 4 reverse audit round ...:none — all checks above completed within budget; the only thing not inspected by design is the test file (another agent's chunk), which limits my Suggestion to …。
— qwen3.8-max via Qwen Code /review (v0.21.8)
… write-free, untrusted anchors, full-reset resets - Model widths from ANSI-stripped content (SGR bytes are not cells) while repaint replays the raw styled frame. - QWEN_CODE_LEGACY_RESIZE_ERASE VP wake stays write-free (remount bump only) instead of blanking via a bare viewport clear. - Erase-prefixed printable writes re-model unconditionally (live region can legitimately shrink below MIN_FRAME_LINES); bare full-reset redraws (clearTerminal + full static history) reset the model instead of poisoning it; second printable bare write (live frame after static append) bypasses the line-count guard. - Skip amplification when the return-to-bottom prefix carries cursorDown computed from pre-reflow geometry (untrusted anchor). - Tests for all R4 scenarios plus wrapper-stack contracts (stacked install order, LIFO teardown) and the AppContainer wake wiring. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…anchor amplification, wake/legacy hardening, close test gaps
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: 无 — 在 48 次调用预算内完成了所有核查。.
中文说明
已审查。 建议见行内评论。 未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:无 — 在 48 次调用预算内完成了所有核查。。
— deepseek-v4-flash via Qwen Code /review (v0.21.8)
doudouOUC
left a comment
There was a problem hiding this comment.
Code review — PR #8831 · round 1 (deepseek-v4-flash)
Head: 15d4be60330f628e180be518ea27d516e4ffbed8 · Diff: 8 files, +1112/-28 · Verdict: ISSUES_FOUND (0 Critical · 3 Suggestion)
Build & test gates
- Build: passed (15 workspaces)
- Tests: no net-new failures (796 total; the 14 failures are all in files shared with the merge base)
- PR-changed test files all pass:
AppContainer.test.tsx(143),synchronizedOutput.test.ts(19, verified gated),terminal-resize-reflow.test.ts(32)
Findings (3 × Suggestion)
All three are unnecessary exports in packages/cli/src/ui/utils/terminalRedrawOptimizer.ts — exported symbols with no external consumer:
- [Suggestion] R1-1
terminalRedrawOptimizer.ts:11—CURSOR_UP_ONEis exported but no file imports it (test files define local copies). Fix: dropexport. - [Suggestion] R1-2
terminalRedrawOptimizer.ts:13—CURSOR_LEFTis exported but has no external consumer. Fix: dropexport. - [Suggestion] R1-3
terminalRedrawOptimizer.ts:26—escapeRegExpis exported but no file imports it (other files define their own private copies). Fix: dropexport.
Overall
The change is well-structured; all previously identified blockers were re-verified as fixed at the reviewed commit, and test coverage is thorough. Remaining items are minor API-surface cleanups only.
中文说明
第一轮审查(deepseek-v4-flash):构建通过、无净新增测试失败、PR 改动的测试文件全部通过。发现 3 个 Suggestion,均为 terminalRedrawOptimizer.ts 中无外部消费者的多余导出(CURSOR_UP_ONE、CURSOR_LEFT、escapeRegExp),建议去掉 export。整体结构良好,此前所有 blocker 已在当前 commit 复核确认修复,测试覆盖充分。
— deepseek-v4-flash via Qwen Code /review (round 1)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
— qwen3.8-max via Qwen Code /review (v0.21.9)
…x delta, ungated full-reset, grapheme/tab packing, VP shrink remount, test hardening
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all planned checks completed within budget.; chunk 1: none — finished within budget.; You are review agent reverse-audit — Reverse audit agen...: none — I finished within budget.; chunk 3: did not independently validate string-width 's width for exotic grapheme clusters beyond the file's own emoji test; no E2E against a real reflowing terminal (o….
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all planned checks completed within budget.;chunk 1:none — finished within budget.;You are review agent reverse-audit — Reverse audit agen...:none — I finished within budget.;chunk 3:did not independently validate string-width 's width for exotic grapheme clusters beyond the file's own emoji test; no E2E against a real reflowing terminal (o…。
— qwen3.8-max via Qwen Code /review (v0.21.9)
| stdout.write(eraseLines(10) + emoji); | ||
| stdout.columns = 30; // 12 cells per line -> 1 row; per-code-point gives 2 | ||
| stdout.emit('resize'); |
There was a problem hiding this comment.
[Suggestion] R6-3: This ZWJ-emoji test's geometry cannot distinguish correct 2-cell grapheme packing from zero-width handling: at columns = 30 each 12-cell line never wraps, so both the correct width-2-per-cluster table and a regressed width-0 table yield 1 row/line and the identical eraseLines(10) assertion passes. Probe-verified with string-width mocked both ways: the zero-width mutation slips through green today. — Concrete cost: a string-width upgrade or width-table change returning 0 for the ZWJ cluster ships uncaught — greedyRows skips every width <= 0 and its final flush still yields exactly 1 row/line — so emoji-heavy frames under-count reflowed rows after a shrink, the amplified erase is too small, and the frame top strands (the #8557 artifact class). The per-code-point regression the comment targets IS caught; the zero-width one is not. (The suite's CJK test only catches a GLOBAL zero-width regression.)
Use a wrap-forcing geometry so the two behaviors diverge (probe-verified flip — correct packing gives eraseLines(20), zero-width still eraseLines(10)):
const emoji = Array.from({ length: 10 }, () => family.repeat(16)).join('\n');
// ...
stdout.columns = 30; // 16 clusters = 32 cells -> 2 rows per line
// ...
expect(stdout.written.at(-1)).toBe(eraseLines(20));中文说明
[Suggestion] R6-3:该 ZWJ emoji 测试的几何无法区分正确的「每簇 2 格」打包与零宽度处理:columns = 30 时每行 12 格永不折行,正确的每簇 2 格宽度表与回归为 0 的宽度表都得到每行 1 排,相同的 eraseLines(10) 断言都会通过。已用 mock 的 string-width 双向探针实测:零宽度变异当前可以绿着通过。— 具体代价:string-width 升级或宽度表变更使 ZWJ 簇返回 0 时会无声合入——greedyRows 跳过所有 width <= 0,末尾 flush 仍恰好产生每行 1 排——emoji 密集帧在缩窄后少算 reflow 行数,放大的擦除不足,帧顶残留(即 #8557 缺陷类别)。注释针对的按码点回归确实能被捕获;零宽度回归不能。(套件中的 CJK 测试只能捕获全局零宽度回归。)
改用强制折行的几何使两种行为分叉(探针已验证翻转——正确打包得 eraseLines(20),零宽度仍为 eraseLines(10)),见上方代码。
— qwen3.8-max via Qwen Code /review (v0.21.9)
| const MULTILINE_ERASE_LINES_PATTERN = createEraseLinesPattern('g'); | ||
|
|
||
| function escapeRegExp(value: string): string { | ||
| export function escapeRegExp(value: string): string { |
There was a problem hiding this comment.
[Suggestion] R6-4: escapeRegExp is newly exported with zero consumers anywhere — grep of the worktree and the whole PR diff: the importers of this module are startInteractiveUI.tsx (installTerminalRedrawOptimizer), terminal-resize-reflow.ts (countOccurrences, createEraseLinesPattern, ERASE_LINE), and three test files; none imports escapeRegExp, whose only call sites are inside createEraseLinesPattern in this same module. Note the history: d2e01e8 (the round-1 'drop unused exports' follow-up) made it module-private; this HEAD re-exported it. — Concrete cost: a private regex-escape helper is promoted to the module's public API contract for no caller, so any future change to its escape set must be made as if external callers exist, and it consolidates nothing — the repo already carries six independent local copies of the same helper.
| export function escapeRegExp(value: string): string { | |
| function escapeRegExp(value: string): string { |
中文说明
[Suggestion] R6-4:escapeRegExp 被新导出但没有任何消费者——对工作区与整个 PR diff 的 grep:本模块的导入方为 startInteractiveUI.tsx(installTerminalRedrawOptimizer)、terminal-resize-reflow.ts(countOccurrences、createEraseLinesPattern、ERASE_LINE)与三个测试文件;没有任何文件导入 escapeRegExp,其唯一调用点就在本模块的 createEraseLinesPattern 内部。注意历史:d2e01e8619(第 1 轮『移除无用导出』跟进)已将其改为模块私有;当前 HEAD 又重新导出了它。— 具体代价:一个私有的正则转义辅助函数在无调用者的情况下被提升为模块公开 API 契约,今后对其转义集合的任何修改都必须像存在外部调用者一样对待,且它并未整合任何东西——仓库里已有六处独立的同名本地实现。
— qwen3.8-max via Qwen Code /review (v0.21.9)
| // Match core's editHelper.countOccurrences empty-needle semantics; without | ||
| // this guard indexOf('', 0) never advances and the loop hangs. | ||
| if (search === '') return 0; |
There was a problem hiding this comment.
[Suggestion] R6-5: The changed behavior (empty needle: infinite loop → return 0) has no test coverage, and the function is newly exported, widening its caller surface. terminalRedrawOptimizer.test.ts imports only getTerminalRedrawStatsSnapshot, installTerminalRedrawOptimizer, optimizeMultilineEraseLines, resetTerminalRedrawStats — nothing exercises countOccurrences('abc', ''). — Concrete cost: all current callers pass non-empty constants (ERASE_LINE, ansiEscapes.clearTerminal), so the guard is reachable only via future callers of the now-exported function; if a refactor silently drops the guard, such a caller (e.g. one passing a config-derived needle that can be empty) hangs the write path in an infinite loop with no test to catch the regression.
Add a one-liner mirroring packages/core/src/utils/editHelper.test.ts:167:
expect(countOccurrences('abc', '')).toBe(0);中文说明
[Suggestion] R6-5:行为变更(空 needle:死循环 → 返回 0)没有测试覆盖,且该函数被新导出、扩大了调用面。terminalRedrawOptimizer.test.ts 只导入 getTerminalRedrawStatsSnapshot、installTerminalRedrawOptimizer、optimizeMultilineEraseLines、resetTerminalRedrawStats——没有任何用例执行 countOccurrences('abc', '')。— 具体代价:当前所有调用方都传非空常量(ERASE_LINE、ansiEscapes.clearTerminal),该守卫只能经由这个新导出函数的未来调用方到达;若某次重构悄悄删掉守卫,这样的调用方(例如传入可能为空的配置来源 needle)会让写入路径陷入死循环,且没有测试能捕获该回归。
补一行与 packages/core/src/utils/editHelper.test.ts:167 对应的断言(见上方代码)。
— qwen3.8-max via Qwen Code /review (v0.21.9)
| const prevTerminalWidthRef = useRef(terminalWidth); | ||
| useEffect(() => { | ||
| const prev = prevTerminalWidthRef.current; | ||
| prevTerminalWidthRef.current = terminalWidth; |
There was a problem hiding this comment.
[Suggestion] R6-6: The new VP width-shrink remount effect has no test anywhere in the diff or the existing suite. The only resize-remount assertion is the #8004 test, which GROWS the width (80 → 100) in STATIC mode (useTerminalBuffer: false) and asserts the key does not change — it cannot see this effect. — Concrete cost (mutation-verified): terminalWidth < prev → terminalWidth > prev, or deleting the effect, leaves every test green. The regression that would then ship uncaught: after a VP-mode terminal shrink, the reflow wrapper's 2J+H viewport clear wipes one-shot <Static> agent-tab history, and without the bump that history never re-emits until the next unrelated remount trigger — a visible-loss artifact of the same class this PR fixes. The wake path's identical bump IS covered (buildWakeRepaint unit tests); only the shrink path's is not.
Add a case in the #8004 style with VP settings: deliver a width shrink (e.g. 100 → 80) via the same resizeListeners mechanism and assert capturedUIState.historyRemountKey incremented; optionally a companion case asserting a grow or a static-mode shrink does not bump.
中文说明
[Suggestion] R6-6:新增的 VP 宽度缩窄 remount effect 在 diff 与现有套件中均无测试。唯一的 resize-remount 断言是 #8004 测试,它在 STATIC 模式(useTerminalBuffer: false)下把宽度增大(80 → 100)并断言 key 不变——无法覆盖该 effect。— 具体代价(已变异验证):把 terminalWidth < prev 改成 terminalWidth > prev,或删除整个 effect,所有测试仍为绿。随后会无声合入的回归:VP 模式缩窄终端后,reflow wrapper 的 2J+H 视口清除会清掉一次性 <Static> agent 标签页历史,若没有该自增,历史在下一次无关的 remount 触发之前永不重新发出——与本 PR 修复目标同类的可见丢失缺陷。wake 路径的相同自增已有覆盖(buildWakeRepaint 单测);只有缩窄路径没有。
按 #8004 风格补一个 VP 配置用例:通过同一 resizeListeners 机制投递一次宽度缩窄(如 100 → 80),断言 capturedUIState.historyRemountKey 自增;可再加一个用例断言变宽或 static 模式缩窄不自增。
— qwen3.8-max via Qwen Code /review (v0.21.9)
| // Unwind the stdout.write wrapper stack in LIFO order (resizeReflow is | ||
| // installed last / outermost); the identity-guarded restores silently | ||
| // no-op and leak wrappers otherwise. | ||
| resizeReflow.restore(); |
There was a problem hiding this comment.
[Suggestion] R6-7: The only production call site establishing the wrapper stack — the install gate (isTTY && !getScreenReader()), the repaintViewport={resizeReflow.repaint} pass-through, and this three-call LIFO unwind — has no test; startInteractiveUI has no test file at all. — Concrete cost: the 'wrapper restores unwind in LIFO order only' test proves the contract with its own locally built install order, and its negative half demonstrates exactly this hazard (out-of-order restore makes the identity guards no-op, leaving stdout.write patched), but nothing ties the actual restore sequence here to that contract. If a future edit reorders these three cleanup calls, the identity-guarded restores silently no-op and the stale reflow wrapper keeps intercepting stdout for the rest of the process lifetime, amplifying erases against a stale width model on post-exit output — the failure is silent by design. The same gap covers the non-TTY/screen-reader stub branch.
If a full startInteractiveUI test is out of reach, extract the cleanup ordering into a small testable helper, or add a narrow integration assertion that process.stdout.write is identity-restored after the UI exits.
中文说明
[Suggestion] R6-7:建立 wrapper 栈的唯一生产调用点——安装门(isTTY && !getScreenReader())、repaintViewport={resizeReflow.repaint} 透传、以及这三行的 LIFO 解开——没有测试;startInteractiveUI 根本没有测试文件。— 具体代价:'wrapper restores unwind in LIFO order only' 测试用它自己本地构造的安装顺序证明了该契约,其反例半边恰好演示了此危害(非 LIFO 恢复会使同一性守卫空操作、stdout.write 保持被包装),但没有任何东西把此处真实的恢复顺序与该契约绑定。若未来某次编辑重排这三个清理调用,同一性守卫会静默空操作,陈旧的 reflow wrapper 将在进程余生持续拦截 stdout,用陈旧的宽度模型对退出后的输出放大擦除——该失效按设计就是无声的。同样的缺口也覆盖非 TTY/读屏器 stub 分支。
若完整的 startInteractiveUI 测试不可行,可把清理顺序抽成一个小的可测辅助函数,或加一个窄的集成断言:UI 退出后 process.stdout.write 被同一性恢复。
— qwen3.8-max via Qwen Code /review (v0.21.9)
…w-end static re-bump, deps-capture wiring test
yiliang114
left a comment
There was a problem hiding this comment.
LGTM on the substance. Both round-6 Criticals are verified fixed at head (the expectFrame bell-clobber is bounded by the 50ms handoff window plus the barePrintableCount disarm, with tests; the one-shot-remount-vs-clear-window clash is fixed by the per-shrink debounced remount), and I chased the round-1 blank-screen class: in default VP no runtime write contains clearTerminal, so repaint()'s model is effectively never empty there, and the agent-tab wake bump re-emits keyed Static to force the repaint. The erase arithmetic is sound with a conservative bias, the DEC 2026 allowlist additions are harmless where ignored, and the 780-line suite covers the amplification math, window open/expire, replay/fallback, handoff arm/disarm, CJK/ZWJ/tab widths, and LIFO wrapper chains; CI green on head.
Non-blocking, worth clearing before or right after merge: (1) the 650ms remount re-bump ships with zero test coverage after the round-6 review explicitly asked for one, and mutation-probing shows deleting it leaves the suite green; (2) repaint()'s empty-model fallback is still clear-without-replay — unreachable today but the exact shape round 1 blocked, so skip the write when the model is empty and width unchanged; (3) the MIN_FRAME_LINES gate can freeze a stale taller model in static mode on a narrow sequence (post-shrink redraw of a <8-line live region rejected, then a later shrink amplifies toward committed scrollback — the dangerous direction, exotic trigger); consider treating the sole bare write of a handoff window as authoritative when the window closes; (4) the clear-window flicker on non-allowlist terminals during a drag is a defensible trade but the body's 'at most a brief atomic relayout' only holds on allowlist terminals — doc note; plus the open round-6 Suggestions (ZWJ test geometry, unused escapeRegExp export, empty-needle guard, startInteractiveUI wiring) and the outstanding bot CHANGES_REQUESTED. Nothing here is a P1; ship it and clear the follow-ups.
ytahdn
left a comment
There was a problem hiding this comment.
✅ Approved — two follow-ups to clear
The resize/wake fix is in good shape at head 78469b42. I re-verified the previously reported blockers (rounds 1–6) against the current code — all resolved: the erase-amplification state machine (greedy per-character packing, trailingNewline +1, bare-write handoff bounded by the 50 ms window and disarmed on the second printable), the CLEAR_TERMINAL model drop hoisted above the expectFrame gate, the legacy escape hatch staying write-free, the LIFO wrapper teardown, and the wake clear-and-replay wiring. The 780-line suite is thorough and passes.
Two follow-up items worth clearing (inline, both small):
terminal-resize-reflow.ts:324— the handoff counter increments beforemodelFrame'sMIN_FRAME_LINESgate; two sub-8-line printable strays inside the 50 ms window can poison the frame model. The R6-1 fix covers strays after the live frame, not before it.terminal-resize-reflow.ts:350—repaint()emits a bareCLEAR_VIEWPORTwhen the modeled width differs from the current terminal width — the exact "bare clear would blank the screen" case the module docblock forbids (suspend → resize → resume with a byte-identical redraw).
Also worth tracking: a real-terminal (PTY/tmux) shrink regression test, and the open round-6 threads (R6-3 ZWJ geometry, R6-4 dead escapeRegExp export, R6-5 empty-needle guard test, R6-6 shrink-remount effect coverage, R6-7 wiring test).
中文说明
审查通过。此前各轮(R1–R6)的 24 个 blocker 已在 head 78469b42 全部核实修复;两个需尽快清理的跟进项见行内评论:(1) terminal-resize-reflow.ts:324 交接计数器先于 MIN_FRAME_LINES 接受而递增,50ms 窗口内两个 <8 行可打印杂散可污染帧模型(R6-1 只覆盖了活帧之后的杂散);(2) terminal-resize-reflow.ts:350 模型宽度与当前终端宽度不符时 repaint() 发出裸 CLEAR_VIEWPORT——正是模块 docblock 声明必须避免的"裸清屏变空白"(挂起中改宽度 → 恢复 → 字节相同重绘)。另建议补一个真实终端(PTY/tmux)缩窄回归测试;R6-3~R6-7 开放建议请一并跟进。
| // commit is the live frame and replaces the model even below | ||
| // MIN_FRAME_LINES. Once the live frame is consumed, disarm so later | ||
| // strays cannot clobber the model during idle. | ||
| barePrintableCount++; |
There was a problem hiding this comment.
[Suggestion] The handoff counter is incremented before modelFrame's MIN_FRAME_LINES gate, so a rejected sub-8-line write still consumes a slot. Two such printable strays inside the 50 ms handoff window (console noise, subprocess output — even a \x07 bell passes stripAnsi(chunk).trim() !== '') let the second one model via barePrintableCount > 1 (bypassMin) and disarm expectFrame; the real live frame that follows is never modeled, and the next shrink amplifies from the poisoned model — over-erasing into committed scrollback, the exact outcome this module promises to avoid. The R6-1 fix covers strays after the live frame, not before it.
Failure scenario: shrink → Ink's log.clear() arms the handoff → stray #1 (sub-8-line, rejected but count→1) → stray #2 (count→2, modeled + disarmed) → the actual live frame is skipped → next shrink computes pendingAmplify from the noise (reproduced deterministically: eraseLines(10) instead of the required eraseLines(20)).
| barePrintableCount++; | |
| if (modelFrame(chunk, barePrintableCount > 0)) { | |
| barePrintableCount++; | |
| if (barePrintableCount > 1) expectFrame = false; | |
| } |
中文说明
交接计数器在 modelFrame 的 MIN_FRAME_LINES 门之前递增,被拒绝的 <8 行写入仍然消耗一个计数槽位。50ms 交接窗口内两个可打印杂散(控制台输出、子进程输出——\x07 铃响经 stripAnsi().trim() 后也算可打印)会让第二个杂散走 barePrintableCount > 1 的 bypassMin 成为帧模型并解除交接,真正的活帧之后不再建模——下次缩窄按被污染的模型计算放大,越过帧顶擦进已提交滚动区(本模块 docstring 承诺绝不发生的破坏方向)。R6-1 修复只覆盖了活帧之后的杂散,未覆盖活帧之前的。建议只对 modelFrame 实际接受的写入计数(suggestion 见上)。
There was a problem hiding this comment.
Fixed in 936b1ad: only writes that modelFrame() accepts consume a handoff slot (if (modelFrame(chunk, barePrintableCount > 0)) { barePrintableCount++; ... }), so rejected sub-MIN strays no longer count and two of them cannot bypass the gate before the live frame. Regression test added for the two-stray scenario.
| const columns = stdout.columns ?? lastWidth; | ||
| originalWrite.call( | ||
| stdout, | ||
| model.columns === columns && model.content |
There was a problem hiding this comment.
[Suggestion] The width-mismatch branch emits a bare CLEAR_VIEWPORT with no replay — the exact case the module's own docblock says must not happen ("Ink skips redraws whose output is unchanged, so a bare clear would blank the screen").
Failure scenario: VP mode (default), Ctrl+Z → resize the terminal while suspended → fg: the SIGCONT repaint fires while stdout.columns differs from model.columns (no frame modeled at the new width yet), clears the alternate screen and writes nothing back; if Ink's resize-driven redraw already ran or its output is byte-identical (short lines that don't reflow), the screen stays blank until the next user input. The author's test 'repaint falls back to a bare clear when the width changed' locks this behavior in.
| model.columns === columns && model.content | |
| const columns = stdout.columns ?? lastWidth; | |
| if (model.columns !== columns || !model.content) return; | |
| originalWrite.call(stdout, CLEAR_VIEWPORT + model.content); |
中文说明
宽度不符分支发出裸 CLEAR_VIEWPORT 且不回放——正是模块 docblock 声明必须避免的"裸清屏变空白"。失效场景:VP 模式(默认)下 Ctrl+Z → 挂起中调整终端宽度 → fg:SIGCONT 的 repaint 在模型尚未按新宽度建模时发出裸清屏,若 Ink 的 resize 重绘已执行或其输出字节相同(短行不折行),屏幕保持空白直到下一次输入。作者测试 'repaint falls back to a bare clear when the width changed' 锁定了此行为。建议宽度不符(或模型为空)时跳过写入——陈旧但可见优于空白。
There was a problem hiding this comment.
Fixed in 936b1ad: repaint() now returns without writing when the model is empty or width-mismatched — stale-but-visible beats a blank screen (Ink skips byte-identical redraws). Tests updated to assert no write in both cases.
| useEffect(() => { | ||
| const prev = prevTerminalWidthRef.current; | ||
| prevTerminalWidthRef.current = terminalWidth; | ||
| if (useTerminalBuffer && terminalWidth < prev) { |
There was a problem hiding this comment.
[Suggestion] Three things about the new VP shrink-remount effect (R6-6 already covers the missing test, so not repeated here): (a) it bumps remountStaticHistory() on every shrink render — during a drag-shrink burst each width tick re-emits the full one-shot static history, and each re-emit is wiped by the next in-window CLEAR_VIEWPORT substitute (the 650 ms re-bump is the one that actually lands); (b) it is not gated on the reflow wrapper being active — under QWEN_CODE_LEGACY_RESIZE_ERASE=1 (wrapper is a no-op, no viewport clears) it still bumps twice per shrink, where pre-PR behavior was no bump at all; (c) 650 duplicates CLEAR_WINDOW_MS (600, exported from terminal-resize-reflow.ts) by value, coupled only by a comment.
Suggested fix: bump only on the first shrink of a burst (when no timer is pending); gate on repaintViewport being present (undefined exactly in legacy-hatch mode); derive the delay as CLEAR_WINDOW_MS + 50.
中文说明
新的 VP 缩窄 remount effect 有三个问题(R6-6 已覆盖缺测试,此处不重复):(a) 每次缩窄渲染都立即整体重发 static 历史——拖拽缩窄的每个宽度 tick 都触发一次,而窗口内每次重发又会被下一次 CLEAR_VIEWPORT 替换清掉(真正生效的是 650ms 重 bump);(b) 未门控 reflow wrapper 是否激活——QWEN_CODE_LEGACY_RESIZE_ERASE=1 下(wrapper 为 no-op)每次缩窄仍 bump 两次,而 PR 前该模式不触发任何 bump;(c) 650 与 CLEAR_WINDOW_MS(600,已导出)跨文件硬耦合。建议:burst 首次缩窄才立即 bump;以 repaintViewport 是否存在作为门控;延迟改为 CLEAR_WINDOW_MS + 50。
There was a problem hiding this comment.
Fixed in 936b1ad: (a) the immediate bump fires only on the first shrink of a burst (no timer pending); in-window ticks just reschedule the window-end re-bump; (b) the effect is gated on repaintViewport being present, which is undefined exactly under QWEN_CODE_LEGACY_RESIZE_ERASE (wrapper no-op), restoring pre-PR no-bump behavior there; (c) the delay is CLEAR_WINDOW_MS + 50 via the exported constant, no duplicated literal.
| // rows than Ink's stale erase count (issue #8557); amplify the clear to the | ||
| // reflowed height. Installed before render() so the resize listener runs | ||
| // ahead of Ink's resized(). | ||
| const resizeReflow = |
There was a problem hiding this comment.
[Suggestion] No regression test drives a real terminal through the shrink path. The 38 unit tests validate the erase-amplification model against the wrapper's own reflow assumption — FakeStdout plus the same greedyRows/stringWidth/Intl.Segmenter packing the production code uses — so model and test share any width-modeling error. The #8557 thread itself documents a divergence already observed on Warp (CJK font fallback renders ∴ full-width while any app-side width model counts it narrow).
Failure scenario: the width model diverges from a real terminal's reflow, and the issue's exact symptom (duplicated/stacked transcript on shrink) returns on real terminals while all unit tests stay green.
Suggested fix: commit a PTY/tmux-based regression in the integration harness that shrinks a real terminal mid-session and asserts a single reprint; at minimum, capture the byte-level shrink rig output as a fixture.
中文说明
没有任何回归测试驱动真实终端走缩窄路径:38 个单测用 FakeStdout + 与生产代码同一套打包逻辑验证放大模型,模型与测试共享任何宽度计算错误;#8557 线程里作者自己已记录过 Warp 上的真实分歧(CJK 字体回退把 ∴ 渲染成整宽,任何应用侧宽度模型都按窄字计)。建议在集成测试里提交 PTY/tmux 缩窄回归,或至少把字节级缩窄 rig 输出固化为 fixture。
There was a problem hiding this comment.
Accepted as a known limitation, deferred: a PTY/tmux shrink regression would not share the width-model assumption, but the repo's interactive tmux harness is flaky-prone in CI and the byte-level rig would still bind to one terminal's reflow semantics (Warp's CJK fallback diverges from any app-side model, as documented in this thread). Mitigations in place: 38+ unit tests pin the wrapper contract, and the fix was author-verified on Ghostty and Warp (live-view duplication gone; Warp block-history snapshots remain platform behavior, documented). Tracking a real-terminal shrink regression as follow-up work; not blocking this PR.
…repaint, burst-first remount - Only accepted bare writes consume a handoff slot, so two rejected sub-MIN strays can no longer bypass the gate and poison the model before the live frame. - repaint() is a no-op when the model is empty or width-mismatched: a bare viewport clear with no replay would blank the screen (stale beats blank). - VP shrink remount: bump only on the first shrink of a burst, gate on repaintViewport being present (legacy hatch stays bump-free), delay derived from CLEAR_WINDOW_MS instead of a duplicated literal.
|
Released in v0.21.10. |
What this PR does
Fixes the terminal-independent resize/wake rendering artifacts investigated in #8557: (1) on a width shrink the renderer cleared with the row count computed at the old width, so the reflowed frame top (banner) was stranded and every subsequent redraw stacked another copy on every reflow-capable terminal; (2) the wake/SIGCONT repaint introduced by #7265 neither cleared nor repainted anything in VP mode, leaving the stale buffer visible. VP mode now starts every redraw from a clean viewport for a short window after a shrink, and the wake path clears the viewport and replays the last frame that reached the terminal (Ink skips unchanged-output redraws). Static mode amplifies the shrink clear to the reflowed height of the last printed frame, bounded so committed scrollback is never touched. Warp and Ghostty additionally join the DEC 2026 synchronized-output allowlist so erase-then-rewrite applies atomically where intermediate states would otherwise flicker.
Why it's needed
Reproduced on Ghostty and Warp: stepped shrinks stack 3-10 banner copies and flicker; STOP/CONT left the VP screen stale. Scope note (per review): on Warp specifically, each resize also commits a snapshot of the pre-redraw alternate-screen grid into Warp's block history — a platform behavior no app-side erase can remove. This PR fixes the live-view duplication and the wake regression on all terminals (author-verified on Ghostty and Warp); the Warp block-history snapshot half remains documented in #8557 with the
ui.useTerminalBuffer: falseworkaround, and is NOT claimed fixed here.Reviewer Test Plan
How to verify
Run
npm run devin Ghostty or Warp, send a prompt, drag narrower in steps while thinking: expect a single banner, no stacked copies, no tearing (at most a brief atomic relayout). STOP/CONT from a second terminal: expect one clean repaint./clear, model change, Ctrl+O: expect no stale-frame flash in VP. Escape hatches:QWEN_CODE_LEGACY_RESIZE_ERASE=1,QWEN_CODE_DISABLE_SYNCHRONIZED_OUTPUT=1.Evidence (Before & After)
Before: #8557 thread shows stacked banners after shrink/wake and heavy drag flicker on Warp. After: author-verified zero stacked copies on Ghostty and Warp (live view); Warp block-history snapshots of pre-fix sessions remain (platform behavior, see Scope).
Tested on
Environment (optional)
npm run devon macOS in Ghostty and Warp; byte-level verification via a tmux + script rig.Risk & Scope
Linked Issues
References #8557 (live-view duplication + wake regression fixed; Warp snapshot half remains platform behavior), #7265, #8849; supersedes #8832.
中文说明
这个 PR 做了什么
修复 #8557 中终端无关的两类渲染缺陷:① 缩窄时按旧宽度行数清屏,reflow 后的帧顶(banner)残留并被后续每次重绘叠印(所有 reflow 终端复现);② #7265 的 wake/SIGCONT 重绘在 VP 下既不清屏也不重绘。VP 缩窄后短窗口内每次重绘从干净视口起步;wake 路径清视口并重放最后到达终端的帧(ink 跳过输出不变的重绘)。static 模式把缩窄清屏放大到最后一帧的 reflow 高度(有上界,不碰 scrollback)。Warp/Ghostty 加入 DEC 2026 白名单(原子帧)。
为什么需要
Ghostty/Warp 复现:分步缩窄叠印 3-10 份 banner+拖拽闪烁;STOP/CONT 后 VP 屏幕过期。范围说明(按评审意见):Warp 在每次 resize 时把重绘前 alt-screen 网格快照进块历史——平台行为,应用侧无解;本 PR 修复所有终端的 live-view 叠印与 wake 回归(Ghostty/Warp 实测),Warp 块历史快照半边在 #8557 记录并附 workaround(
ui.useTerminalBuffer: false),不声称已修。评审测试计划
Ghostty/Warp 中
npm run dev,思考中分步拖窄:单一 banner、无叠印、无撕裂。STOP/CONT:一次干净重绘。/clear/切 model/Ctrl+O:VP 无旧帧闪现。逃生门:QWEN_CODE_LEGACY_RESIZE_ERASE=1、QWEN_CODE_DISABLE_SYNCHRONIZED_OUTPUT=1。风险与范围
关联 issue
参考 #8557、#7265、#8849;取代 #8832。