Skip to content

fix(cli): default to virtualized terminal history#5738

Open
ZevGit wants to merge 1 commit into
QwenLM:mainfrom
ZevGit:fix/cli-scroll-flicker
Open

fix(cli): default to virtualized terminal history#5738
ZevGit wants to merge 1 commit into
QwenLM:mainfrom
ZevGit:fix/cli-scroll-flicker

Conversation

@ZevGit

@ZevGit ZevGit commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Turns virtualized history on by default for interactive CLI sessions. New users and existing users without an explicit setting now get the in-app scrollable history viewport, while users who prefer host terminal scrollback can still opt out by setting ui.useTerminalBuffer to false.

The settings schema, generated IDE schema, tests, and user/design docs are updated so the runtime behavior and documented behavior match.

Why it's needed

The legacy terminal scrollback rendering path can visibly flash or make the host terminal scrollbar jump during long sessions, refreshes, resize settle repaint, compact-mode toggles, and similar TUI updates. The virtualized history path already avoids the physical terminal clear/replay behavior that causes this class of flicker, but it was still opt-in, so most users continued to hit the old path unless they discovered the setting.

Defaulting to the virtualized path makes the stable behavior the common path, reduces repeated flicker reports, and keeps a small compatibility escape hatch for terminals or workflows that still prefer native scrollback.

Reviewer Test Plan

How to verify

Start the interactive CLI with no ui.useTerminalBuffer setting and confirm it opens in the virtualized history flow. Scrolling history should use Shift+Up/Down, PgUp/PgDn, Ctrl+Home/End, or the mouse wheel instead of native terminal scrollback.

Start the interactive CLI with ui.useTerminalBuffer: false and confirm the legacy host scrollback path is still available.

Run the focused unit tests and type/build checks listed below.

Evidence (Before & After)

Before: with no explicit setting, the CLI defaulted to the legacy terminal scrollback path, so refreshes could clear/replay terminal history and produce visible scrollbar jumps in long sessions.

After: a local PTY smoke test launched the built CLI twice. With the setting unset, the captured ANSI stream entered the alternate screen and reached the input prompt. With ui.useTerminalBuffer: false, the captured ANSI stream did not enter the alternate screen and still reached the input prompt.

Verification commands run locally:

cd packages/cli && npx vitest run src/config/settingsSchema.test.ts src/ui/AppContainer.test.tsx src/gemini.test.tsx
Test Files 3 passed (3)
Tests 147 passed (147)

npm run typecheck
passed

npm run build
passed with existing warnings only

git diff --check
passed

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Local source checkout, built CLI, sandbox disabled for the PTY smoke. The smoke did not send a model request; it only verified startup rendering and the terminal mode selected by default versus explicit opt-out.

Risk & Scope

  • Main risk or tradeoff: virtualized history uses an in-app viewport and alternate screen by default, so users who rely on native host scrollback or click-drag selection may need the documented opt-out or Shift/Option selection bypass.
  • Not validated / out of scope: exhaustive coverage across every terminal emulator, tmux configuration, and OS combination.
  • Breaking changes / migration notes: no config migration is required; set ui.useTerminalBuffer to false to keep the legacy host scrollback behavior.

Linked Issues

N/A

中文说明

What this PR does

这个 PR 将交互式 CLI 的虚拟化历史视图默认开启。新用户和没有显式配置该选项的现有用户会默认使用应用内可滚动历史视口;如果用户更偏好宿主 terminal scrollback,仍然可以通过设置 ui.useTerminalBufferfalse 选择旧路径。

配置 schema、生成的 IDE schema、测试以及用户/设计文档都同步更新,保证运行时行为和文档描述一致。

Why it's needed

旧的 terminal scrollback 渲染路径在长会话、刷新、resize settle repaint、compact mode 切换等 TUI 更新场景下,可能出现明显闪屏或宿主 terminal 滚动条跳动。虚拟化历史路径已经避免了导致这类 flicker 的物理清屏和历史重放行为,但之前仍是 opt-in,大多数用户如果没有发现这个设置,依然会走旧路径。

把虚拟化路径设为默认,可以让更稳定的行为成为常规路径,减少重复的闪屏反馈,同时保留一个很小的兼容逃生口,给仍然偏好原生 scrollback 的终端或工作流使用。

Reviewer Test Plan

How to verify

在没有设置 ui.useTerminalBuffer 的情况下启动交互式 CLI,确认它进入虚拟化历史流程。历史滚动应使用 Shift+Up/Down、PgUp/PgDn、Ctrl+Home/End 或鼠标滚轮,而不是依赖原生 terminal scrollback。

ui.useTerminalBuffer: false 的情况下启动交互式 CLI,确认旧的宿主 scrollback 路径仍然可用。

运行上面列出的聚焦单测、typecheck 和 build 检查。

Evidence (Before & After)

Before:没有显式配置时,CLI 默认走旧的 terminal scrollback 路径,因此刷新时可能清屏并重放 terminal 历史,在长会话中表现为可见滚动条跳动。

After:本地 PTY smoke test 启动了两次构建后的 CLI。配置未设置时,捕获到的 ANSI 流进入了 alternate screen,并正常到达输入提示。显式设置 ui.useTerminalBuffer: false 时,捕获到的 ANSI 流没有进入 alternate screen,也正常到达输入提示。

本地执行的验证命令:

cd packages/cli && npx vitest run src/config/settingsSchema.test.ts src/ui/AppContainer.test.tsx src/gemini.test.tsx
Test Files 3 passed (3)
Tests 147 passed (147)

npm run typecheck
passed

npm run build
passed with existing warnings only

git diff --check
passed

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

本地源码 checkout,使用构建后的 CLI,PTY smoke 中关闭 sandbox。这个 smoke 没有发送模型请求,只验证启动渲染以及默认配置和显式 opt-out 分别选择的 terminal mode。

Risk & Scope

  • Main risk or tradeoff:虚拟化历史默认使用应用内视口和 alternate screen,因此依赖宿主原生 scrollback 或 click-drag 选择文本的用户,可能需要使用文档中的 opt-out 或 Shift/Option 选择绕过方式。
  • Not validated / out of scope:没有覆盖所有 terminal emulator、tmux 配置和 OS 组合。
  • Breaking changes / migration notes:不需要配置迁移;设置 ui.useTerminalBufferfalse 可以保留旧的宿主 scrollback 行为。

Linked Issues

N/A

@ZevGit

ZevGit commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

E2E / TUI verification report for this PR:

  • Local PTY smoke launched the built CLI with no ui.useTerminalBuffer setting and confirmed the ANSI stream enters alternate screen and reaches the input prompt.
  • The same smoke launched the built CLI with ui.useTerminalBuffer: false and confirmed it does not enter alternate screen while still reaching the input prompt.
  • This smoke did not send a model request; it only verifies startup rendering mode selection for the default path and explicit opt-out path.

Additional local checks:

cd packages/cli && npx vitest run src/config/settingsSchema.test.ts src/ui/AppContainer.test.tsx src/gemini.test.tsx
Test Files 3 passed (3)
Tests 147 passed (147)

npm run typecheck
passed

npm run build
passed with existing warnings only

git diff --check
passed

Tested locally on macOS. Windows/Linux terminal combinations were not manually tested.

@wenshao wenshao 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.

No review findings. Downgraded from Approve to Comment: CI failing: Test (windows-latest, Node 22.x).

CI note: the failing Windows job is in packages/core's gitWorktreeService.symlinks.integ.test.ts (git init -q -b main), outside this PR's changed files.

— GPT-5 Codex via Qwen Code /review

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Verification Report — default to virtualized terminal history

Built and exercised the real CLI from the PR head, with a focus on the one thing a default-flip can silently get wrong: leaving a runtime gate behind. Result: correct, complete, and well-tested. The only open item is a product/UX judgement call (alt‑screen now on by default), not a defect.

Environment: PR head ee5d0ea9f, built via npm ci (full tsc build), node packages/cliv0.18.5, Linux / Node 22.x, tmux 3.5a on a dedicated socket.


Scope check

The PR is a focused 10‑file, single‑commit change that flips ui.useTerminalBuffer default false → true. (Note for reviewers: a naïve 3‑dot diff can show an extra acp-integration.test.ts change — that is real‑main being ahead of a stale local ref, not part of this PR. Verified against freshly‑fetched origin/main: the commit touches exactly the 10 files listed in the diff.)

1. Gate‑completeness audit ✅ (avoids the schema‑only trap)

A schema default: in settingsSchema.ts is display‑onlymergeSettings() does not seed settings.merged from it. So flipping only the schema default would be a runtime no‑op and create a "dialog shows on / runtime still off" split (the failure mode seen in #5145).

This PR does not fall into that trap. There are exactly two runtime reads of settings.merged.ui?.useTerminalBuffer, and both are flipped from ?? false to ?? true:

Gate Drives Status
gemini.tsx:367 (useVP) render(..., { alternateScreen: useVP }) ?? false → ?? true
AppContainer.tsx:957 (useTerminalBuffer) VP render path + refreshStatic; propagated to MainContent/ScrollableList via uiState.useTerminalBuffer ?? false → ?? true

MainContent.tsx inherits the value through uiState (no independent read). The settingsSchema.ts + vscode‑ide JSON defaults are flipped too, so the Settings dialog stays consistent with runtime. The serve/routes/workspace-settings.ts reference is only a TUI‑only classification set (its default field auto‑derives from the schema). No ACP / serve / non‑interactive path reads this setting — the change is TUI‑only. Because the gates use ??, an unset key correctly resolves to the new default regardless of merge seeding.

2. Real tmux E2E (A/B) ✅ — the headline

Two server‑side probes were used because they independently exercise the two different gates: #{alternate_on}gemini.tsx, #{mouse_any_flag}AppContainer → ScrollableList. Same built CLI, two isolated $HOMEs, both arms reached the chat prompt cleanly (no auth dialog).

Build / config #{alternate_on} #{mouse_any_flag} meaning
PR + setting unset (new default) 1 1 VP on by default
PR + ui.useTerminalBuffer: false (opt‑out) 0 0 legacy path preserved
base (PR delta reverted in dist) + unset 0 0 proves the 2‑line delta is the sole cause

The third row is a dist‑level swap (?? true?? false on the two compiled lines, same unset $HOME) — isolating the PR's change as the only variable. Both gates verified, escape hatch verified.

3. UX consequence reviewers should weigh ⚠️ (not a bug)

The new default enters the alternate screen. Demonstrated concretely: a host marker printed before launch is hidden while qwen runs (alternate_on=1) and restored after /quit, with the qwen UI gone:

while running:  alternate_on=1  | qwen banner visible | host marker hidden
after /quit:    alternate_on=0  | qwen banner gone     | host marker restored

So on exit, the entire qwen session is wiped from the host terminal scrollback (standard vim/less‑style alt‑screen behavior). This is the intended trade to kill flicker, and the documented opt‑out (ui.useTerminalBuffer: false) works — but it is a meaningful change for users who scroll back through their terminal after quitting, or who rely on native click‑drag selection. Worth a conscious product sign‑off.

4. Unit tests ✅

  • The 3 PR‑touched files: 147 / 147 passed (matches the PR body).
  • Reverse‑audit sweep of 6 default‑flip‑sensitive suites (MainContent, App, DefaultAppLayout, useResizeSettleRepaint, terminalRedrawOptimizer, InputPrompt): 221 / 221 passed. The PR correctly pins useTerminalBuffer: false in the shared AppContainer fixture so legacy‑path tests (e.g. the Terminal resize during streaming leaves fragmented content at wrong widths in scrollback #4891 resize‑clearTerminal contract) stay valid. No collateral breakage found.

5. CI status

Test (ubuntu) and Test (macos) pass. Test (windows) fails, but the failure is gitWorktreeService.symlinks.integ.test.ts (1/15) plus an exit code 127 (shell command‑not‑found) — a Windows symlink/tooling issue in a file this PR does not touch (byte‑identical between merge‑base and PR head). Not a regression from this PR.


Verdict

From a correctness standpoint this is mergeable: the gate audit is clean, both runtime fallbacks and the display defaults are flipped consistently, tests are green on Linux/macOS, and the real‑terminal A/B confirms both the new default and the opt‑out. The remaining decision is the product call on defaulting to the alternate screen (§3). The Windows CI red is pre‑existing/unrelated.

中文版(点击展开)

验证报告 — 默认开启虚拟化历史视图

从 PR head 构建并运行了真实 CLI,重点验证 default 翻转最容易悄悄出错的地方:有没有漏掉某个 runtime gate。结论:实现正确、完整、测试充分。 唯一待定项是一个产品/UX 取舍(默认进入 alt‑screen),而不是缺陷。

环境: PR head ee5d0ea9f,npm ci(完整 tsc 构建),node packages/cliv0.18.5,Linux / Node 22.x,专用 socket 上的 tmux 3.5a

范围确认

这是一个聚焦的 10 文件、单 commit 改动,把 ui.useTerminalBuffer 默认值从 false 翻成 true。(提醒 reviewer:简单的 3‑dot diff 可能多出一个 acp-integration.test.ts 改动 —— 那是真实 origin/main 领先于本地过期 ref 造成的,不属于本 PR。重新 fetch origin/main 后确认:该 commit 恰好只动 diff 列出的 10 个文件。)

1. Gate 完整性审计 ✅(避开了 schema‑only 陷阱)

settingsSchema.ts 里的 default: 只用于显示 —— mergeSettings() 不会用它去 seed settings.merged。所以只翻 schema default 在运行时是 no‑op,会造成"对话框显示开启 / 运行时仍关闭"的割裂(即 #5145 的问题)。

本 PR 没有掉进这个陷阱。读取 settings.merged.ui?.useTerminalBuffer 的 runtime gate 恰好两处,且?? false 翻成了 ?? true:

Gate 控制 状态
gemini.tsx:367(useVP) render(..., { alternateScreen: useVP }) ?? false → ?? true
AppContainer.tsx:957(useTerminalBuffer) VP 渲染路径 + refreshStatic;经 uiState.useTerminalBuffer 传给 MainContent/ScrollableList ?? false → ?? true

MainContent.tsx 通过 uiState 继承该值(没有独立读取)。settingsSchema.ts 和 vscode‑ide 的 JSON 默认值也一并翻转,所以设置对话框与运行时保持一致。serve/routes/workspace-settings.ts 那处只是一个 TUI‑only 分类集合(default 字段自动从 schema 派生)。ACP / serve / 非交互路径都不读这个设置 —— 改动仅限 TUI。由于用的是 ??,未设置的 key 会正确落到新默认值,与 merge 是否 seed 无关。

2. 真实 tmux E2E(A/B)✅ —— 核心证据

用了两个 server 端探针,因为它们分别独立地命中两个不同的 gate:#{alternate_on}gemini.tsx,#{mouse_any_flag}AppContainer → ScrollableList。同一个构建,两个隔离的 $HOME,两个 arm 都干净地到达聊天输入框(无 auth 对话框)。

构建 / 配置 #{alternate_on} #{mouse_any_flag} 含义
PR + 设置未设(新默认) 1 1 默认开启 VP
PR + ui.useTerminalBuffer: false(opt‑out) 0 0 旧路径保留
base(在 dist 里还原 PR 改动)+ 未设 0 0 证明这 2 行改动是唯一原因

第三行是 dist 级替换(把两行编译产物 ?? true?? false,$HOME 同样未设),从而把 PR 改动隔离成唯一变量。两个 gate 都验证通过,逃生开关也验证通过。

3. Reviewer 需要权衡的 UX 影响 ⚠️(非 bug)

新默认会进入 alternate screen。已具体演示:在启动打印的 host marker,在 qwen 运行期间被隐藏(alternate_on=1),/quit 之后被还原,且 qwen 界面消失:

运行期间:  alternate_on=1  | qwen banner 可见 | host marker 被隐藏
/quit 之后: alternate_on=0  | qwen banner 消失 | host marker 还原

也就是说退出时整个 qwen 会话会从宿主终端 scrollback 中抹掉(标准的 vim/less 式 alt‑screen 行为)。这是为消除闪屏而做的取舍,文档里的 opt‑out(ui.useTerminalBuffer: false)有效 —— 但对那些退出后还要在终端往回滚、或依赖原生 click‑drag 选择文本的用户来说是个实质变化。建议产品侧明确签字确认。

4. 单元测试 ✅

  • PR 改动的 3 个文件:147 / 147 通过(与 PR 描述一致)。
  • 对 6 个受 default 翻转影响的测试套件做反向审计扫描(MainContentAppDefaultAppLayoutuseResizeSettleRepaintterminalRedrawOptimizerInputPrompt):221 / 221 通过。PR 正确地在共享的 AppContainer fixture 里钉死 useTerminalBuffer: false,使依赖旧路径的测试(如 Terminal resize during streaming leaves fragmented content at wrong widths in scrollback #4891 的 resize‑clearTerminal 契约)继续有效。未发现连带破坏。

5. CI 状态

Test (ubuntu)Test (macos) 通过Test (windows) 失败,但失败项是 gitWorktreeService.symlinks.integ.test.ts(15 中 1 个)外加 exit code 127(shell command‑not‑found)—— 这是 Windows symlink/工具链问题,而该文件本 PR 完全没动(merge‑base 与 PR head 逐字节相同)。不是本 PR 引入的回归。

结论

从正确性角度,本 PR 可以合并:gate 审计干净,两个 runtime fallback 与显示默认值一致翻转,Linux/macOS 测试全绿,真实终端 A/B 同时验证了新默认与 opt‑out。剩下的只是"是否默认进入 alternate screen"的产品决定(见 §3)。Windows CI 的红是既有/无关问题。

Verified by building the PR head and running it under tmux on Linux/Node 22. Probes: #{alternate_on} (gemini.tsx gate) and #{mouse_any_flag} (AppContainer→ScrollableList gate), plus a dist‑level base A/B isolating the PR delta.

@ZevGit

ZevGit commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on the remaining UX point:

Defaulting ui.useTerminalBuffer to true intentionally makes the interactive CLI enter the in-app virtualized history viewport / alternate screen by default. The tradeoff is explicit: it avoids the legacy terminal clear/replay path that causes flicker and scrollbar jumps, but host terminal scrollback is no longer the default history surface while qwen is running.

The compatibility path is preserved: users who prefer native host scrollback can set ui.useTerminalBuffer to false, and the PR tests that this opt-out still avoids alternate screen. The settings description and user docs also call out the mouse-selection bypass and the fact that the in-app viewport replaces native terminal scrollback while enabled.

I do not plan further code changes for this unless maintainers prefer a more conservative rollout strategy than default-on.

CI note: the current Windows red is in packages/core/src/services/gitWorktreeService.symlinks.integ.test.ts (git init -q -b main) plus follow-on workspace exits. That file is outside this PR's changed files, and Linux/macOS plus lint/CodeQL/no-AK smoke are green, so I am treating the Windows failure as unrelated to this PR.

wenshao
wenshao previously approved these changes Jun 23, 2026
@chiga0

chiga0 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

关于剩余的用户体验要点,需要跟进:

默认设置ui.useTerminalBuffer为 true 会使交互式 CLI 默认进入应用内的虚拟历史记录视口/备用屏幕。这样做的利弊显而易见:它避免了传统终端的清除/重放路径(该路径会导致闪烁和滚动条跳动),但主机终端的滚动条在 qwen 运行时不再是默认的历史记录界面。

兼容性路径得以保留:偏好原生主机滚动回溯的用户可以进行设置ui.useTerminalBufferfalse并且 PR 测试表明,即使选择禁用此功能,也不会出现屏幕切换的情况。设置说明和用户文档也指出了鼠标选择绕过机制,以及启用此功能后应用内视口会替换原生终端滚动回溯的事实。

除非维护人员更倾向于采用比默认启用更保守的推广策略,否则我不打算对此进行进一步的代码更改。

CI 注:当前 Windows 红色警告位于packages/core/src/services/gitWorktreeService.symlinks.integ.test.ts( git init -q -b main) 以及后续工作区退出项中。该文件不在本 PR 的更改文件范围内,而 Linux/macOS 以及 lint/CodeQL/no-AK smoke 检查结果均为绿色,因此我将 Windows 故障视为与本 PR 无关。

hi, thank you for the PR. But I think it's not time to change the default setting of useTerminalBuffer to true, because there is some problems I found with the virtual scroll witch I need to fix first.

  1. the scrollbar unable to scroll when output is larger than viewport with mouse scroll; and can't drag to scroll too.
  2. the mouse event handler is not correct, so the collapsed thinking block cannot be open with mouse click.
  3. other bugs I'm testing which haven't beed found

If you glad with this, you can help to test and welcome pr with this.

@doudouOUC doudouOUC 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] docs/design/virtual-viewport/README.md:326 — The paragraph after the PR-sequence table still reads "V.3 (integration tests) is the remaining critical-path item before flipping the default." However, V.3 remains "pending" in the table while this PR flips the default. Future readers will conclude the default was flipped prematurely or that V.3 was silently dropped. Suggest updating to reflect the actual decision, e.g.: "V.3 (integration tests) remains desirable for long-session regression coverage but is no longer a gating prerequisite for the default flip."

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
// re-reading `mergedHistory` / `allVirtualItems` on whatever state
// change triggered refreshStatic (Ctrl+O, model change, etc.).
const useTerminalBuffer = settings.merged.ui?.useTerminalBuffer ?? false;
const useTerminalBuffer = settings.merged.ui?.useTerminalBuffer ?? 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] Accessibility regression for screen-reader users. Flipping the default to true silently routes screen-reader users into the virtualized-viewport path, which drops the append-only <Static> rendering that screen-reader mode is built around.

Chain: with ui.useTerminalBuffer unset, this is now trueuiState.useTerminalBuffer is trueMainContent takes the if (useVirtualScroll) branch (MainContent.tsx:596) and renders <ScrollableList> with no <Static> (the legacy <Static> branch is at :626). Screen-reader users reach this same MainContent — App.tsx routes them to ScreenReaderAppLayout, which itself renders <MainContent />. Ink's screen-reader render path emits committed history to the terminal only via staticOutput (a <Static> node); with no <Static>, history is no longer presented append-only. Separately, gemini.tsx:367 now passes alternateScreen: true for screen-reader + TTY (Ink's resolveAlternateScreenOption doesn't exclude screen-reader mode), entering the alternate screen that assistive tech / native scrollback rely on.

Before this PR (default false) screen-reader users got the <Static> path. Suggest gating the default on screen-reader mode here (and mirror it at gemini.tsx:367 for alternateScreen):

Suggested change
const useTerminalBuffer = settings.merged.ui?.useTerminalBuffer ?? true;
const useTerminalBuffer =
(settings.merged.ui?.useTerminalBuffer ?? true) && !config.getScreenReader();

No test currently covers the screen-reader + unset-useTerminalBuffer combination.

— claude-opus-4-8[1m] via Qwen Code /qreview

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 90ea976.

Screen-reader mode now stays out of the VP and alternate-screen path in both runtime gates: AppContainer keeps uiState.useTerminalBuffer false and startInteractiveUI passes alternateScreen false when config.getScreenReader() is true. Added regression coverage for screen reader mode with unset useTerminalBuffer in AppContainer and gemini.

@ZevGit

ZevGit commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Updated PR head to 90ea976 after #5751 landed.

Handled review items:

  • Rebased onto latest upstream/main, so the merged fix(cli): stabilize VP mouse interactions #5751 mouse scroll, drag, and collapsed-thinking click fixes are now in the base.
  • Screen-reader mode now keeps the legacy append-only Static path and disables alternate screen instead of silently entering VP mode.
  • Updated the virtual viewport design doc V.3 wording and the user-facing docs/schema text for the screen-reader exception.

Verification:

  • cd packages/cli && npx vitest run src/ui/AppContainer.test.tsx src/gemini.test.tsx src/config/settingsSchema.test.ts: 3 files / 150 tests passed.
  • npx eslint on the changed TS/TSX files: passed.
  • npx prettier --check on changed docs/schema JSON: passed.
  • git diff --check: passed.
  • npm run typecheck: passed.

Local build note:

  • npm run build progressed through CLI/schema generation, then failed in packages/sdk-typescript because the browser daemon SDK bundle is 128651 bytes with a 128000-byte limit. That failure is outside this PR changed files.

@wenshao

wenshao commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Build failure root cause — it's a main regression (SDK bundle over its size cap), not this PR

The failing Test legs on all three platforms (run 28069535002) die at the Install dependencies step. npm ci runs the prepare/build lifecycle (husky && npm run build && npm run bundle), and the build throws:

Error: Browser daemon SDK bundle is 128651 bytes; expected <= 128000
npm error workspace @qwen-code/sdk@0.1.8
npm error command sh -c node scripts/build.js

packages/sdk-typescript/scripts/build.js enforces a hard cap MAX_DAEMON_BROWSER_BUNDLE_BYTES = 125 * 1024 = 128000. The browser daemon bundle is now 128651 bytes — 651 over the limit, so the build aborts on every platform and npm ci fails.

Why this is not your PR:

The fix belongs on main, not in this PR: either trim the daemon browser bundle back under 128000, or bump MAX_DAEMON_BROWSER_BUNDLE_BYTES (e.g. to 126 * 1024) in packages/sdk-typescript/scripts/build.js. Once main is green again, rebasing this PR will clear the failure — there is nothing to change here for it.

中文

构建失败根因 —— 是 main 分支的回归(SDK bundle 超过体积上限),不是本 PR

三个平台失败的 Test 都挂在 Install dependencies 步骤(run 28069535002)。npm ci 会跑 prepare/build 生命周期(husky && npm run build && npm run bundle),构建抛错:

Error: Browser daemon SDK bundle is 128651 bytes; expected <= 128000
npm error workspace @qwen-code/sdk@0.1.8
npm error command sh -c node scripts/build.js

packages/sdk-typescript/scripts/build.js 里有硬上限 MAX_DAEMON_BROWSER_BUNDLE_BYTES = 125 * 1024 = 128000。现在 browser daemon bundle 是 128651 字节,超了 651 字节,于是构建在每个平台都中止,npm ci 失败。

为什么判定与本 PR 无关:

修复应该在 main,不在本 PR: 要么把 daemon browser bundle 压回 128000 以内,要么在 packages/sdk-typescript/scripts/build.js 里上调 MAX_DAEMON_BROWSER_BUNDLE_BYTES(例如调到 126 * 1024)。等 main 重新变绿后,把本 PR rebase 一下就能消除这个失败 —— 本 PR 自身不需要改动。

@wenshao wenshao 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] packages/cli/index.ts (not in diff) — The uncaughtException handler calls process.exit(1) without runExitCleanup(), so instance.unmount() (which writes exitAlternateScreen) never runs. Pre-existing bug, but now that VP is the default and all users enter alt-screen, any crash leaves the terminal stuck in alt-screen with no visible prompt (user must run reset or close the tab).

Fix: add a process.on('exit') handler that unconditionally writes \x1b[?1049l as a safety net, or call runExitCleanup() in the uncaughtException handler.

CI note: 3 Test jobs failing (ubuntu, macos, windows) — pre-existing, not caused by this PR.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/gemini.tsx Outdated

const useVP = settings.merged.ui?.useTerminalBuffer ?? false;
const useVP =
(settings.merged.ui?.useTerminalBuffer ?? 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] TOCTOU between one-shot alternateScreen and per-render useTerminalBuffer: useVP is computed once here and passed to ink's render(), locking the terminal into alt-screen for the entire session. But AppContainer.tsx:958 recomputes useTerminalBuffer on every render from live settings.merged. If a user toggles useTerminalBuffer off mid-session (the schema declares requiresRestart: false), the React tree switches to the legacy <Static> path while ink stays in alt-screen — history is silently written to the alt-buffer and lost on exit.

Pre-existing, but the default flip makes this the common path. Consider either locking useTerminalBuffer at startup (matching alternateScreen's lifetime), or setting requiresRestart: true so the settings dialog prompts a restart.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8f5eb98.

The VP decision is now startup-scoped in AppContainer, matching Ink alternateScreen lifetime. ui.useTerminalBuffer is also marked requiresRestart, so SettingsDialog no longer presents this as a live-safe toggle.

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
// change triggered refreshStatic (Ctrl+O, model change, etc.).
const useTerminalBuffer = settings.merged.ui?.useTerminalBuffer ?? false;
const useTerminalBuffer =
(settings.merged.ui?.useTerminalBuffer ?? 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 VP-mode guard (settings.merged.ui?.useTerminalBuffer ?? true) && !config.getScreenReader() is duplicated verbatim in gemini.tsx:368. These two sites control different but tightly coupled decisions (ink's alternateScreen vs React rendering path) and must always agree.

Consider extracting a shared helper:

// e.g., packages/cli/src/ui/utils/terminalBuffer.ts
export function shouldUseVirtualViewport(
  settings: LoadedSettings,
  config: Config,
): boolean {
  return (settings.merged.ui?.useTerminalBuffer ?? true) && !config.getScreenReader();
}

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8f5eb98.

Both runtime gates now use a shared shouldUseVirtualViewport helper, so the alternate-screen decision and React VP path stay coupled.

expect(options).toMatchObject({ alternateScreen: false });
});

it('should not use alternate screen in screen reader mode when VP mode is unset', async () => {

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 gap: the screen-reader guard is tested when useTerminalBuffer is unset (defaults to true), but not when it's explicitly true. Add a test with useTerminalBuffer: true + getScreenReader(): true to cover the true && !true branch:

it('should not use alternate screen in screen reader mode even when VP mode is explicitly enabled', async () => {
  // useTerminalBuffer: true, getScreenReader: () => true
  // expect alternateScreen: false
});

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Covered in 8f5eb98.

Added the explicit useTerminalBuffer: true + screen-reader case for startInteractiveUI, asserting alternateScreen remains false.

@ZevGit ZevGit force-pushed the fix/cli-scroll-flicker branch from 90ea976 to 8f5eb98 Compare June 24, 2026 03:28
@ZevGit

ZevGit commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Updated PR head to 8f5eb98 after reviewing the latest comments and CI failure.

What changed:

  • Locked the virtual viewport decision for the running session so it cannot diverge from Ink alternateScreen after a settings update.
  • Marked ui.useTerminalBuffer as restart-required, matching the runtime behavior.
  • Shared the VP decision between gemini.tsx and AppContainer through a small helper.
  • Added a TTY-only exit safety net that restores cursor/alt-screen state if the process exits through an uncaught exception path.
  • Added regression coverage for explicit screen-reader opt-in, startup-mode locking, restart-required schema, and the alt-screen exit fallback.

Verification:

  • cd packages/cli && npx vitest run src/ui/AppContainer.test.tsx src/gemini.test.tsx src/config/settingsSchema.test.ts: 3 files / 153 tests passed.
  • npx eslint on changed TS/TSX files: passed.
  • npx prettier --check on changed docs/schema/helper files: passed.
  • git diff --check: passed.
  • npm run typecheck: passed.
  • npm run build still fails at the known unrelated SDK bundle cap: Browser daemon SDK bundle is 128651 bytes; expected <= 128000, after CLI/schema build has completed.

@doudouOUC doudouOUC 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.

No review findings. LGTM! ✅

All prior suggestions (duplicated VP formula, missing screen-reader test coverage) have been addressed — shared shouldUseVirtualViewport() helper, session-locked VP mode via useState, alternate-screen exit handler, and requiresRestart: true are clean improvements.

— qwen3.7-max via Qwen Code /review

@doudouOUC doudouOUC 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.

CI failed.

@doudouOUC doudouOUC self-requested a review June 24, 2026 05:15
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.

4 participants