Skip to content

fix(cli): enable footer text selection in VP mode - #8329

Merged
DragonnZhang merged 6 commits into
QwenLM:mainfrom
DragonnZhang:dragon/fix-8131-statusline-selection
Aug 12, 2026
Merged

fix(cli): enable footer text selection in VP mode#8329
DragonnZhang merged 6 commits into
QwenLM:mainfrom
DragonnZhang:dragon/fix-8131-statusline-selection

Conversation

@DragonnZhang

Copy link
Copy Markdown
Collaborator

What this PR does

This PR expands application-level text selection in Virtualized History mode so the footer/statusline is a selectable region alongside the history viewport. A drag stays clamped to the region where it started, and selection invalidation watches only that region's layout and content. The composer, scrollbar, dialogs, and other controls remain outside the new footer selection target.

It also adds regression coverage for footer selection, excluded regions, and footer content invalidation, and records the updated selection-region design.

Why it's needed

Virtualized History enables terminal-wide SGR mouse tracking, which prevents the terminal from providing native drag selection anywhere on screen. Qwen Code's application-level selection previously accepted presses only inside the history viewport, so the footer/statusline was left in a dead zone: neither terminal-native nor application-level selection could copy its model, branch, working directory, context usage, or custom statusline output.

Reviewer Test Plan

How to verify

  1. Enable Virtualized History and configure a visible statusline.
  2. Drag across statusline text in either direction; the selected cells should highlight and the exact text should be copied on release.
  3. Drag across history text; history selection and copy should continue to work.
  4. Type visible text in the composer and drag across it; application-level text copy should not start, and composer mouse behavior should remain available.
  5. Change footer content after selecting it; the stale highlight should clear.

Evidence (Before & After)

Probe Before After
History drag Tips copied exactly (4 bytes) Tips copied exactly (4 bytes)
Statusline forward drag Clipboard probe not invoked ISSUE8131_STATUSLINE copied exactly (20 bytes)
Statusline reverse drag Clipboard probe not invoked ISSUE8131_STATUSLINE copied exactly (20 bytes)
Composer drag over COMPOSER_PROBE No application-level copy No application-level copy

Automated validation on the rebased branch: 86 targeted selection/layout/component tests passed, full workspace type checking passed, the repository build and bundle completed successfully, and the changed files passed ESLint and Prettier checks.

Tested on

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

Environment (optional)

macOS on Apple Silicon, Node.js 24, tmux 3.7b, 100×30 terminal, Virtualized History and mouse tracking enabled, local bundled CLI with a fake pbcopy probe for exact clipboard payload verification.

Risk & Scope

  • Main risk or tradeoff: The selection controller now arbitrates one additional UI region. The footer is measured precisely and each drag is clamped to its starting region so composer and scrollbar interactions are not widened.
  • Not validated / out of scope: Real-terminal interaction was not run on Windows or Linux. Cross-screen selection, edge auto-scroll, and semantic-copy follow-ups remain out of scope.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #8131

中文说明

本 PR 做了什么

本 PR 扩展了 Virtualized History 模式下的应用级文本选择,使 footer/statusline 与历史视口一样成为可选择区域。拖拽会始终限制在起始区域内,选择失效逻辑也只观察该区域的布局和内容。Composer、滚动条、对话框及其他控件仍然不属于新增的 footer 选择目标。

同时新增了 footer 选择、排除区域和 footer 内容变化后清除选择的回归覆盖,并记录了更新后的选择区域设计。

为什么需要

Virtualized History 会启用终端级 SGR 鼠标跟踪,使终端无法在屏幕任何位置提供原生拖拽选择。此前 Qwen Code 的应用级选择只接受历史视口内的按下事件,因此 footer/statusline 落入死区:终端原生选择和应用级选择都无法复制其中的模型、分支、工作目录、上下文用量或自定义状态栏输出。

Reviewer Test Plan

如何验证

  1. 启用 Virtualized History,并配置可见的 statusline。
  2. 沿任意方向拖过 statusline 文本;选中的单元格应高亮,并在释放时复制精确文本。
  3. 拖过历史区文本;历史区的选择和复制应继续正常工作。
  4. 在 Composer 中输入可见文本并拖过它;不应启动应用级文本复制,Composer 的鼠标行为应保持可用。
  5. 选择 footer 后改变其内容;过期的高亮应被清除。

证据(修复前后)

探针 修复前 修复后
历史区拖拽 精确复制 Tips(4 bytes) 精确复制 Tips(4 bytes)
状态栏正向拖拽 未调用剪贴板探针 精确复制 ISSUE8131_STATUSLINE(20 bytes)
状态栏反向拖拽 未调用剪贴板探针 精确复制 ISSUE8131_STATUSLINE(20 bytes)
在 Composer 中拖过 COMPOSER_PROBE 未触发应用级复制 未触发应用级复制

基于 rebase 后分支的自动验证:86 个定向选择/布局/组件测试通过,全 workspace 类型检查通过,仓库构建与 bundle 成功完成,改动文件通过 ESLint 和 Prettier 检查。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

Apple Silicon macOS、Node.js 24、tmux 3.7b、100×30 终端,启用 Virtualized History 和鼠标跟踪,使用本地 bundle CLI 及 fake pbcopy 探针精确验证剪贴板 payload。

风险与范围

  • 主要风险或权衡:选择控制器现在会协调一个额外 UI 区域。Footer 使用精确测量,且每次拖拽都限制在其起始区域,因此不会扩大 Composer 和滚动条的交互范围。
  • 未验证 / 范围外:未在 Windows 或 Linux 真实终端中运行交互验证。跨屏选择、边缘自动滚动和语义复制后续工作仍不在本 PR 范围内。
  • Breaking changes / 迁移说明:无。

关联 Issue

Fixes #8131

@DragonnZhang
DragonnZhang marked this pull request as ready for review August 1, 2026 16:58
@DragonnZhang
DragonnZhang enabled auto-merge August 1, 2026 16:58
@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 1, 2026

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline. Not reviewed: integration test suite — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally; the changed selection controller is unit-tested (12/12 pass via the build/test agent) but has no integration coverage.

中文说明

已审查。 建议见行内评论。 未审查:integration test suite — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally; the changed selection controller is unit-tested (12/12 pass via the build/test agent) but has no integration coverage。

— qwen3.8-max-preview via Qwen Code /review

Comment thread packages/cli/src/ui/selection/use-text-selection.test.tsx

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

已审查。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

— qwen3.8-max-preview via Qwen Code /review

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 194 passed · 0 failed · 194 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:194 通过 · 0 失败 · 194 总计

Verification report

PR 8329 Deep Verification — fix(cli): enable footer text selection in VP mode

Verdict: merge-ready — 194/194 scripted assertions passed, 0 unexpected failures.
Verified head: bd1b42410e656f17b9e1d7bb7baabe4b7aa0915c (merge commit bf640fd2c, base tip 74dc5547f).
Two non-blocking findings (one coverage gap, one dead-code nit).

中文摘要

结论:merge-ready。194/194 脚本化断言通过,无意外失败。

  • A/B 结论(单元层):HEAD 上选择套件 13/13 全绿;把 HEAD 的测试文件放到 BASE 控制器上跑,恰好 3 个新增 footer 测试变红(footer 拖拽、跨区域钳制、footer 内容变化清除),其余 10 个(9 个既有 + 1 个"区域外不选择"对照)两臂均绿 —— 改动是承重的,且新测试非空泛(base 上的失败信息正是期望值 vs 实际值的行为断言)。
  • A/B 结论(真实 TUI 层):用真实 bundle + pty + 伪 xclip 剪贴板探针做逐行扫描。BASE bundle 在 statusline 行(终端第 19 行)拖拽无任何复制(死区复现),HEAD bundle 精确复制 ISSUE8131_STATUSLINE_PROBE(26 字节,正反向均可);历史区拖拽两臂都精确复制;composer 行两臂都不复制。唯一行为差异就是 statusline 行,见 05-e2e-ab-base-vs-head.png
  • Findings
    1. 【建议】move 阶段的钳制(clampToViewport)没有任何测试钉住:删掉它的突变体 13/13 全绿,因为 release 分支也会钳制,最终状态与复制文本不变;唯一可观测差异是拖拽中途的高亮会短暂越界。已用探针钉住该轴(head 绿、突变体红),附上的 fixture 可直接加入测试文件。
    2. 【nit】clearSelection 里的 activeRectIndexRef.current = null 是防御性死代码:没有任何路径能读到过期的 index(move/release 以 dragging 为门、失效订阅以 isEmpty 为门、下次 press 会覆写)。无害,可留可删。
  • 未覆盖:逐 commit 归因(CI 浅克隆 depth=2,第一个 commit 不可达,已验证聚合 diff);Windows 真实终端(作者标 ⚠️)—— 本轮在 Linux 容器 pty 中完成,覆盖了 Linux 路径但用的是合成 SGR 事件而非真实终端模拟器;footer 上的双击/三击选词/选行未单独演练;E2E 未现场演练"footer 内容变化后清除高亮"(由单元 A/B 覆盖,M3 突变体被杀)。

Central claim and A/B proof

Central claim: in Virtualized History mode, the footer/statusline is a selectable
region alongside the history viewport — a drag there highlights and copies its exact
text, where previously neither terminal-native nor application-level selection could
reach it. Secondary claims: (1) a drag stays clamped to the region where it
started, and unregistered regions (composer) stay dead; (2) selection invalidation
watches only the active region's layout/content, with no regression to history
selection.

A/B 1 — controller level (head tests vs base controller)

The PR's new test file was run against both arms in a git worktree at the base tip
(HEAD^1), differing only by use-text-selection.tsx. The base worktree reused the
root node_modules (the PR touches no package.json/lockfile and no
packages/core file — git diff --stat HEAD^1..HEAD -- packages/core package.json package-lock.json is empty — so readlink -f node_modules/@​qwen-code/qwen-code-core
resolving into the head tree is a clean control).

Arm Result Detail
HEAD 13/13 pass 9 pre-existing + 4 new tests (01-ab-head-suite.png)
BASE (head tests, base controller) 10 pass / 3 fail exactly the 3 new footer-behavior tests fail; the 9 pre-existing + the "outside registered regions" control pass (02-ab-base-suite.png)

The three base-arm reds fail on the intended behavioral assertions — e.g.
expected last "spy" call to have been called with [ { sx: +0, sy: 1, ex: 5, ey: 1 } ]
and expected "spy" to be called with arguments: [ null ] — Number of calls: 0
not on setup/import errors, which doubles as the vacuity check: the new tests are
load-bearing, and the base-arm greens prove the harness environment is sound.

A/B 2 — real TUI (bundled CLI under a pty, fake xclip oracle)

Both bundles were built from source (esbuild, dist/cli.js; the base bundle was
confirmed to contain zero occurrences of getAdditionalSelectableRects, the head
bundle one). The CLI ran under a 100×30 pty with VP mode on (CI* env vars stripped —
the app treats any CI key as non-interactive and falls out of VP), a statusline
command emitting a marker, and a fake xclip first on PATH writing the exact
clipboard payload to a file. Drags are synthesized SGR bytes (press 0, motion
32, lowercase-m release). A row scan dragged every terminal row 12–26 on each
bundle (04-e2e-pty-results.png, 05-e2e-ab-base-vs-head.png):

Terminal row Region BASE bundle HEAD bundle
12–14 notifications inside the viewport copy (history content) copy (history content)
15–18 separators + composer input no copy no copy
19 statusline (the fix) no copy — dead zone exact marker, 26 bytes
20 footer hints row no copy copy (Auto mode (shift + tab to)
21–26 blank no copy no copy

Plus the calibrated per-gesture run: 9/9 — forward and reverse footer drags copy
the exact marker, a history drag copies the exact word (Qwen, 4 bytes), and no row
around typed composer text copies anything. Mouse modes were identical on both arms
(?1002h ?1006h ?1049h), so the delta is the code, not the environment. Row 20
(hints) becoming selectable is expected: the measured rectangle is the whole Footer
Box, which contains the statusline and hints rows — the PR's "footer/statusline"
framing covers it, and the composer/scrollbar exclusion held.

Mutation matrix (head)

Positive control landed first (disabling copy flipped 7 tests red, so the harness can
fail). 03-mutation-matrix.png:

Mutant Suite Flipped red Verdict
M0: copySelection never copies 6/13 7 copy-asserting tests killed (positive control)
M1: press hit-test ignores additional rects 10/13 footer drag; clamp; clear-on-change killed
M2: move extends raw point (no clamp) 13/13 survivor → coverage gap (Finding 1)
M3: invalidation watches rects[0], not the active rect 12/13 clears-footer-on-footer-change killed
M4: clearSelection keeps stale rect index 13/13 survivor → dead code (Finding 2)

M2 was escalated to a finer probe before being called a gap: a fixture asserting
every intermediate highlight stays in the footer row is green on head and red under
M2
— the move-time clamp is load-bearing for the transient highlight, the suite
simply never asserts an intermediate state. The history→footer direction (the sibling
of the PR's footer→history clamp test) was exercised too: green on head, and also
green under M2 for the same final-state reason — the PR's tests pin the clamp only
through the release-time copy, in one direction.

Reviewer Test Plan, per step

  1. Enable VP + visible statusline — done (pty run, ui.useTerminalBuffer default + custom statusline command). ✅
  2. Drag across statusline in either direction, exact copy — ✅ both directions, exact 26-byte payload.
  3. History drag continues to work — ✅ exact word copied, identical on base and head.
  4. Composer drag does not start a copy — ✅ no copy on any row around the typed text, both arms.
  5. Change footer content after selecting → stale highlight clears — covered at unit level only (M3 kills the corresponding test); not exercised live in the pty.

Findings

1. [Suggestion] The move-time drag clamp is unpinned — intermediate highlight can transiently leave the starting region

Deleting clampToViewport from the move branch survives the whole suite (13/13)
because the release branch also clamps, so the final selection range and the
copied text are identical. The only observable the mutant changes is the highlight
rendered during the drag: a footer drag that wanders up into history briefly
highlights a range spanning both rows ({sx:0,sy:0,ex:2,ey:1} in the test fixture)
before the release snaps it back. Behavior on head is correct; nothing asserts it.

Reproduce: apply M2 from mutation-driver.mjs (replace selection.extend(clampToViewport(point, rect))
with selection.extend(point) in the move branch only) and run
npx vitest run src/ui/selection/use-text-selection.test.tsx → 13/13 green.

Fixture that pins the axis (measured: green on head, red under M2)
it('keeps the drag highlight inside the footer during a move into history', () => {
  frame = makeTwoLineFrame('hello', 'status');
  viewportRect = { x: 0, y: 0, width: 5, height: 1 };
  additionalSelectableRects = [{ x: 0, y: 1, width: 6, height: 1 }];
  const handler = mount();

  handler(makeEvent('left-press', 1, 2));
  handler(makeEvent('move', 3, 1));

  for (const call of setSelection.mock.calls) {
    const range = call[0];
    if (range) {
      expect(range.sy).toBe(1);
      expect(range.ey).toBe(1);
    }
  }
});

2. [Nit] activeRectIndexRef.current = null in clearSelection is defensive dead code

M4 (deleting that line) survives 13/13. Tracing every reader of
activeRectIndexRef: getActiveRect() is called from the move handler (gated on
selection.dragging, false after any clear), the release handler (same gate), and
the invalidation subscription (early-returns on selection.isEmpty, true after any
clear); the next left-press overwrites the index unconditionally. No interleaving
lets a stale index decide an outcome. Harmless — keep as defense-in-depth or drop;
either way it is not load-bearing and its deletion would need no test.

Not covered

  • Per-commit attribution: the CI checkout is depth 2 — git rev-list HEAD^1..HEAD^2
    returns only bd1b4241 (the shallow boundary silently truncates rather than
    erroring), while the metadata lists two commits (620e9822 fix + bd1b4241
    test). Everything above verifies the aggregate HEAD^1..HEAD diff; the fix/test
    split was not exercised per commit.
  • Windows real-terminal validation (author-flagged ⚠️): this round ran on Linux,
    in a container pty with synthesized SGR events — it exercises the real code path
    (pty TTY detection, alt-screen, SGR parsing, clipboard subprocess) but is not a
    human terminal on Windows or Linux; terminal-specific input quirks remain untested.
  • Footer double/triple-click word/line selection: the multi-click path shares
    the rect logic proven above and the span logic is pre-existing, but no cell
    exercised a double-click inside the footer specifically.
  • Reviewer Test Plan step 5 live: footer-content-change invalidation was proven
    at unit level (M3 killed) but not driven through the pty.
  • Repo-wide test suite: only the affected surface was run (selection directory +
    Footer/Composer/MainContent/DefaultAppLayout — 134 tests), plus workspace-wide
    typecheck and ESLint on the six changed files (liveness-proven: a planted unused
    variable was reported, then removed).
  • Cross-screen selection, edge auto-scroll, semantic copy — declared out of scope
    by the PR; not probed.

Methodology

Environment: the CI verify container (node:22-bookworm, no tmux/zstd),
merge-ref checkout at depth 2; npm ci + npm run build pre-run. Unit A/B: head
suite at HEAD, then HEAD's test file copied into a git worktree at HEAD^1
(root + per-package node_modules symlinked; core realpath verified, core unchanged
by the PR). Mutations: single-point source edits applied/restored by
mutation-driver.mjs and m2-probe.mjs with git checkout + empty-git diff
verification after each. E2E: pty-e2e.py spawns the bundled CLI under a
pty.openpty() pty (100×30, TIOCSWINSZ) with a minimal ANSI screen emulator;
because the emulator drifted ~1 row against ink's diff renderer, absolute drag rows
were calibrated against the app's own clipboard response (±2-row scan per region) —
the oracle is the exact payload the real copyToClipboard→fake-xclip pipeline
wrote to disk, never the emulator's grid. Raw per-cell logs, JSON reporter output,
and both full session byte streams (e2e-session-raw-head.log,
e2e-session-raw-base.log) are in logs/; harness scripts are in the artifact
directory root.

Evidence images

01-ab-head-suite

02-ab-base-suite

03-mutation-matrix

04-e2e-pty-results

05-e2e-ab-base-vs-head

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Re-run at the post-merge head — gate re-checked from scratch.

Template looks good ✓

Problem: observed bug with a linked issue (#8131) and clear reproduction steps — enable Virtualized History, drag across the statusline, nothing selects. Independently confirmed since the first pass: a maintainer real-pty run against the merge base recorded 0 clipboard writes for statusline drags, while this head copies the exact selected text. No doubt the problem exists.

Direction: aligned. The statusline shows model, branch, working directory, context usage — information users routinely copy — and VP mode's terminal-wide mouse tracking had left it in a dead zone reachable by neither terminal-native nor application-level selection. Fixing that within the existing selection framework is squarely in scope; no new direction introduced.

Size: not applicable — no core module paths touched. Everything is packages/cli/src/ui/** plus two design docs: ~268 production lines (additions + deletions), ~468 test lines, 33 doc lines.

Approach: the scope feels right. One selection controller generalizes from a single viewport rect to an ordered list of selectable rects; the footer becomes the second rect, drags clamp to the starting region, invalidation watches only that region. Since the first pass the branch was reconciled with main's #8739 (word/line drag extension): the merge keeps #8739's drag-capable multi-click path, bridges its synchronous invalidation with this PR's deferred one via a generation-guarded microtask, and makes lineSpanAt resolve the contiguous selectable run around the click. No unrelated changes spotted; the design doc update records the decision.

Risk: no elevated risk signals — none of the changed files match the high-risk path patterns.

Moving on to code review. 🔍

中文说明

在合并后的 head 上重跑——重新完整检查准入门槛。

模板完整 ✓

问题:已观测到的 bug,有关联 issue(#8131)和清晰的复现步骤——启用 Virtualized History 后拖过状态栏,无法选中文本。首轮之后已被独立证实:维护者在真实 pty 中对 merge base 的运行显示状态栏拖拽 0 次剪贴板写入,而本 head 能精确复制选中文本。问题真实存在,没有疑问。

方向:对齐。状态栏显示模型、分支、工作目录、上下文用量等用户经常需要复制的信息,而 VP 模式的终端级鼠标跟踪使其落入终端原生选择和应用级选择都无法触及的死区。在现有选择框架内修复完全在范围内,没有引入新方向。

规模:不适用——未触及核心模块路径。所有改动在 packages/cli/src/ui/** 和两个设计文档内:生产代码约 268 行(增+删),测试约 468 行,文档 33 行。

方案:范围合理。一个选择控制器从单一视口矩形泛化为有序可选矩形列表,footer 成为第二个矩形,拖拽限制在起始区域内,失效逻辑只观察该区域。首轮之后分支与 main 的 #8739(按词/按行拖拽扩展)完成对齐:合并保留了 #8739 的可拖拽多击路径,用带代际守卫的微任务桥接其同步失效与本 PR 的延迟失效,并让 lineSpanAt 解析点击位置周围连续的可选择片段。未发现无关改动;设计文档更新记录了该决策。

风险:无升级风险信号——改动文件均未匹配高风险路径模式。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at de8955930bc23093e10de2369e84b88d2a9d7025 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Code review (re-run at the post-merge head)

Independent proposal: extend the controller with an optional callback supplying extra selectable rects, hit-test all rects on press, record the owning rect, clamp drags to it, scope invalidation per rect, and measure the footer through a layout-owned ref with the existing measureElementPosition utility. The diff still matches this line for line — and the three things added since the first pass are each the right call:

  • Generation-guarded deferred invalidation (the merge bridge with feat(ui): word-wise drag after double-click, line-wise extension after triple-click #8739): pendingInvalidationGenRef is set at schedule time, cleared inside the microtask, and compared against the current generation at release; presses and clearSelection() bump the generation. The ordering holds up: microtasks drain before the next input event, so a release never races the queue, and the same-generation release path correctly drops the drag without copying content the repaint already replaced. does not let stale invalidation clear a newer selection pins it.
  • lineSpanAt(frame, x, y) resolving the nearest contiguous selectable run — this is what closed the round-3 Critical (R2-3). Triple-clicking non-selectable cells in the history region (blank trailing padding, line-number gutters, hanging-indent prefixes) previously fell through to "nothing happens"; it now snaps to the adjacent content. New regression tests cover gutter, trailing padding, and wide-character spacers, and the maintainer's pty probes confirm history triple-click still selects and copies.
  • getSelectedText joining layout-gap-split runs with a single space, with guards that avoid leading-space artifacts when a row starts with non-selectable cells.

No criticals at this head. The round-4 /review pass and my own read agree; what remains is Suggestions, none blocking:

  • R4-2: double/triple-click inside the footer rect works today (probe-verified by the review agent) but has no test pinning it — same for several test-efficacy gaps carried from earlier rounds (weak ref-attachment assertion, no wiring end-to-end test, render-test .at(-1) oracle). Worth a follow-up, not a merge blocker.
  • R3-5 overlaps maintainer Note 2: invalidation compares every cell of the owning rect, and in a real terminal a footer selection is still dropped by history scrolling/streaming — contrary to the code comment ("remain valid") and the PR description's "watches only that region's layout and content". The copy lands on release before any of this, so impact is cosmetic (the maintainer accepted it as non-blocking), but the claim should be fixed or loosened. The unit test that "keeps a footer selection while history scrolls" passes only because it replays the same frame object, which can't reproduce a real scroll.
  • Maintainer Note 1: triple-click no longer spans a whole visual line across a layout gap (it also stops gluing decoration glyphs in history). Intentional and arguably nicer, but it changes an existing gesture — worth a line in the PR description.

Test evidence

The PR's own CI on this head — the Qwen Code CI workflow (event pull_request) completed success; 14 checks green, 71 skipped (platform-specific and conditional jobs). The two still in flight (precheck-pr / precheck, review-config) belong to the bot's pull_request_target orchestration, not the PR's CI.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
precheck-pr / precheck ✅ success
review-pr ✅ success
Classify PR · label · route · authorize ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped

Beyond CI, this comment carries a maintainer attribution, not a bot re-run: wenshao drove the bundled CLI in a real pty with raw SGR mouse sequences, BASE (merge base) vs HEAD (this commit) — footer drags/double/triple-click copy exact payloads where BASE wrote nothing, excluded regions stay excluded, history is byte-identical except the intentional Note 1 change, and mutation probes (return [viewportRect], reverted lineSpanAt) show the new tests are load-bearing. See the verification comment in this thread for the full tables. A sandboxed /verify stage of this triage run is also in flight and will post its own report.

Sandboxed verification would settle the one residual: @qwen-code /verify (sponsored run — a maintainer triggers it, and the report deserves the same skepticism as the fork's own CI logs, since the code under verification is adversarial input) — that footer selection behaves the same on Windows/Linux terminals is untested by both the author and the maintainer; the central macOS behaviour is already substantiated by the BASE-vs-HEAD run above.

中文说明

代码审查(合并后 head 重跑)

独立方案: 为控制器增加一个返回额外可选矩形的可选回调,按下时命中测试所有矩形、记录所属矩形、拖拽限制其内、失效按区域划分,footer 通过布局层持有的 ref 用现有 measureElementPosition 测量。diff 仍与该方案逐行一致——首轮之后新增的三处改动也都是正确选择:

  • 带代际守卫的延迟失效(与 feat(ui): word-wise drag after double-click, line-wise extension after triple-click #8739 的合并桥接):pendingInvalidationGenRef 在调度时记录、微任务内清空、release 时与当前代际比较;按下和 clearSelection() 都会递增代际。时序成立:微任务先于下一个输入事件执行,release 不会与队列竞争;同代 release 会正确放弃拖拽、不复制已被重绘替换的内容。已有测试钉住该行为。
  • lineSpanAt(frame, x, y) 解析最近的连续可选片段——这正是第 3 轮 Critical(R2-3)的修复。此前在历史区三击不可选单元格(行尾空白填充、行号栏、悬挂缩进前缀)会落入"无任何反应";现在会吸附到相邻内容。新增回归测试覆盖行号栏、行尾填充和宽字符占位;维护者 pty 探针确认历史区三击仍可选中并复制。
  • getSelectedText 用单个空格连接被布局间隙拆开的片段,守卫条件避免了行首为不可选单元格时的前导空格。

本 head 无 Critical。第 4 轮 /review 与我的独立阅读结论一致;剩余均为 Suggestion,不阻塞合并:

  • R4-2:footer 区域内的双击/三击当前可用(review 代理已探针验证)但没有测试钉住——连同此前各轮遗留的若干测试效力缺口(ref 附着断言过弱、无接线端到端测试、render 测试 .at(-1) 判据),值得后续跟进,不构成合并阻塞。
  • R3-5 与维护者说明 2 重合:失效逻辑比较所属矩形的全部单元格,真实终端中滚动历史/流式输出仍会丢弃 footer 选择——与代码注释("保持有效")和 PR 描述"只观察该区域的布局和内容"不符。复制发生在释放时、早于上述任何一步,影响是观感层面的(维护者已接受为非阻塞),但说法要么修掉要么放宽。"历史滚动时保留 footer 选择"的单测之所以通过,是因为它重放同一个 frame 对象,无法复现真实滚动。
  • 维护者说明 1:三击不再跨布局间隙覆盖整行(历史区也不再粘连装饰字形)。属有意为之且体验更好,但改变了既有手势——值得在 PR 描述中补一句。

测试证据

本 head 上 PR 自己的 CI——Qwen Code CI 工作流(event 为 pull_request)已 success 完成;14 项检查通过,71 项跳过(平台特定与条件任务)。仍在进行的两项(precheck-pr / precheckreview-config)属于机器人的 pull_request_target 编排,不是 PR 的 CI。

除 CI 外,本评论引用的是维护者的验证(注明出处,非机器人重跑):wenshao 用真实 pty 驱动打包 CLI、注入原始 SGR 鼠标序列,对 merge base 与本 commit 做对照——footer 拖拽/双击/三击复制出精确内容而 BASE 无任何写入,排除区域保持排除,历史区除有意的说明 1 变化外逐字节一致,变异探针(return [viewportRect]、回退 lineSpanAt)证明新增测试真实有效。完整表格见本线程的验证评论。本次 triage 运行的沙箱 /verify 阶段也在进行中,会单独发布报告。

沙箱验证可以填补唯一剩余缺口:@qwen-code /verify(赞助运行——由维护者触发;被验证的代码是对抗性输入,报告应以审视 fork CI 日志的同样怀疑态度阅读)——footer 选择在 Windows/Linux 终端下的行为作者与维护者均未测试;核心 macOS 行为已由上述 BASE vs HEAD 运行证实。

Qwen Code · qwen3.8-max

Reviewed at de8955930bc23093e10de2369e84b88d2a9d7025 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal implementation with the round-3 Critical fixed and regression-tested; behaviour verified by the maintainer on this exact commit; only non-blocking nits remain.

Stepping back: this PR does one thing and does it well. The controller gains multi-rect support through the smallest possible surface — one optional prop, one ref, the existing measurement utility — and the merge reconciliation with #8739 is careful rather than expedient (the generation guard is exactly the bridge the sync-vs-deferred invalidation collision needed). My independent proposal matched the diff, so there was no simpler path being missed; every edit serves the stated goal.

The arc of this review matters too. The round-3 Critical — lineSpanAt returning null when a history triple-click landed on non-selectable cells (gutters, padding, indents) — was not hand-waved away: the fix (snap to the nearest selectable run) ships with targeted tests, and the round-4 pass over this head found nothing above Suggestion level. My own read of the generation-guard ordering, the clamping, and the gap-join guards agrees.

What keeps this at 4 rather than 5: the invalidation-scope claim ("watches only that region") is still overstated relative to observed behaviour (maintainer Note 2 — history activity drops a footer selection; cosmetic, copy already landed), the history triple-click change deserves a line in the description (Note 1), several test-efficacy gaps flagged by /review remain open, and Windows/Linux terminals are untested by everyone. None of that blocks: the maintainer ran the real product BASE-vs-HEAD on this commit, mutation-tested the new tests, and approved.

This approval supersedes the bot's earlier changes-requested state, which was raised on a prior head for the now-fixed Critical. The sandboxed /verify stage of this run is still in flight and will post its report separately; the PR's own CI is already green, so nothing is deferred on.

Approving, pinned to the reviewed commit. ✅

中文说明

置信度:4/5 —— 实现干净且最小化,第 3 轮 Critical 已修复并有回归测试;行为已由维护者在该 commit 上验证;只剩非阻塞的小问题。

退一步看:这个 PR 只做一件事,而且做得好。控制器以最小的接口面获得多矩形支持——一个可选 prop、一个 ref、复用现有测量工具——与 #8739 的合并对齐也足够审慎(代际守卫正是同步失效与延迟失效冲突所需的桥接)。我的独立方案与 diff 一致,因此不存在被错过的更简路径;每处改动都服务于既定目标。

审查过程本身也说明问题。第 3 轮 Critical——历史区三击落在不可选单元格(行号栏、填充、缩进)上时 lineSpanAt 返回 null——没有被敷衍带过:修复(吸附到最近的可选片段)连同针对性测试一起提交,第 4 轮在该 head 上未发现高于 Suggestion 的问题。我对代际守卫时序、拖拽限制和间隙拼接守卫的独立阅读结论相同。

之所以是 4 而不是 5:失效范围的声明("只观察该区域")相对观测到的行为仍然说过头了(维护者说明 2——历史区活动会丢弃 footer 选择;影响为观感层面,复制早已完成),历史区三击的变化值得在描述中补一句(说明 1),/review 指出的若干测试效力缺口仍未处理,且 Windows/Linux 终端所有人都未测试。这些都不构成阻塞:维护者已在该 commit 上对真实产品做了 BASE vs HEAD 对照、对新增测试做了变异验证,并已批准。

本次批准取代机器人早前针对旧 head、因该 Critical 提出的 changes-requested 状态。本次运行的沙箱 /verify 阶段仍在进行,会单独发布报告;PR 自己的 CI 已经全绿,没有任何事项被挂起。

批准,锁定在被审查的 commit 上。✅

Qwen Code · qwen3.8-max

Reviewed at de8955930bc23093e10de2369e84b88d2a9d7025 · re-run with @qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Local real-stack verification (maintainer)

I built this PR and its merge-base from source and drove both in a real pty with real SGR mouse bytes, so every result below comes from the actual terminal protocol path rather than from mocks.

Verdict: the feature works and the scoping is tight — footer selection is exact (including reverse drags, multi-line footers, narrow layout, wide/CJK cells), and the composer / divider / history behaviours are unchanged. One documented behaviour does not hold in a real terminal: the stale footer highlight is not cleared when footer content changes (test-plan step 5 and the new design doc's last bullet). The controller logic is correct; the repaint it schedules is swallowed. A one-line change fixes it, and I verified the fix in the same harness — details in Finding 1.

How it was verified

PR head bd1b424 (dragon/fix-8131-statusline-selection)
Baseline 8bdb058 (merge-base with main) — built and driven identically
Build npm run build:packages && npm run bundle, then node dist/cli.js (production bundle, not scripts/dev.js)
Terminal real pty via @lydell/node-pty, TERM=xterm-256color, 100×30 (and 60×26 for the narrow case)
Input raw SGR sequences written to the pty — ESC[<0;c;r M press, ESC[<32;c;r M drag, ESC[<0;c;r m release
Screen readback @xterm/headless mirror of the pty output; highlight measured as a per-cell background-attribute diff, not by eyeballing
Clipboard fake pbcopy earlier on PATHbyte-exact payload capture
Isolation throwaway HOME, throwaway workspace, fake OPENAI_* env — no user config touched

Protocol-level precondition confirmed on the PR build: the app requests ?1049h, ?1002h, ?1006h — i.e. Virtualized History + SGR mouse tracking really are active.

Evidence — before vs after

Identical drag, identical fixture, only the build differs.

# Probe Baseline 8bdb058 This PR bd1b424
1 Statusline drag, left→right clipboard probe not invoked ISSUE8131_STATUSLINE20 bytes exact, highlight covers exactly cols 3–22
2 Statusline drag, right→left clipboard probe not invoked ISSUE8131_STATUSLINE — 20 bytes exact
3 Footer hint row (2nd footer line) clipboard probe not invoked Auto mode — 9 bytes exact
4 Narrow terminal (60 cols, column-layout footer) clipboard probe not invoked ISSUE8131_STATUSLINE — 20 bytes exact
5 Drag starting in footer, dragged up into history clipboard probe not invoked clamped to the footer row: SUE8131_ (8 bytes), no history text, highlight confined to the footer row
6 History drag over Tips Tips — 4 bytes Tips — 4 bytes (unchanged)
7 Drag over COMPOSER_PROBE in the composer no app-level copy no app-level copy (unchanged; the composer's own text-input mouse selection still works)
8 Press+drag on the ──── divider between input and footer no selection no selection (unchanged)
9 Footer selection while unrelated (composer) content changes n/a highlight persists — invalidation is correctly scoped to the active region

Extra probes on the PR build, beyond the PR's own test plan:

Probe Result
Wide/CJK statusline 模型:Qwen 分支:main copied exactly, 23 bytes UTF-8; highlight spans all wide cells correctly
Triple-click on statusline (line select) ISSUE8131_STATUSLINE, 20 bytes
Double-click on footer hint (word select) Auto, 4 bytes
Statusline re-runs every 1s with identical output highlight correctly survives — invalidation is content-based, not refresh-based

Statusline selected — baseline (left, dead zone) vs this PR (right):

Before 8bdb058 After bd1b424
Cross-region clamp (drag from footer up into history) Wide/CJK statusline
History drag still works Narrow (60-col) footer

Finding 1 — the stale footer highlight is not cleared on a footer content change

The PR's test plan step 5 and docs/design/statusline-text-selection.md both state that a footer selection is cleared when its content changes. In a real terminal it is not: the highlight stays on the same cells and now covers text that was never selected or copied.

Repro — statusline is cat <file> with refreshInterval: 1; select it, then change the file's contents:

1. Selected ISSUE8131_STATUSLINE 2. Content changed → highlight still painted, now over ISSUE8131_CHANGED!!!

Root cause — the controller is right, the repaint is lost. I instrumented the frame subscriber in an instrumented build of this branch. On the frame that carries the new footer text it logs exactly what you'd want:

{"idx":1,"activeRect":{"x":0,"y":14,"width":100,"height":2},
 "sameRect":true,"sameContent":false,
 "baseRowAtRectY":"  ISSUE8131_STATUSLINE","curRowAtRectY":"  ISSUE8131_CHANGED!!!"}

sameContent:falseclearSelection() runs → setSelection(null)requestRender(). But that request is issued re-entrantly, from inside publishFrame, which is itself called from within Ink's onRender. It never produces another frame:

  • frame counter: exactly one publishFrame after the content change, then none for 5s;
  • pty byte trace: the last write is the one painting the new text with the selection background, then zero bytes for 6s;
  • the highlight disappears the moment any unrelated render happens (one keystroke is enough).

So the invalidation decision is correct and only the repaint is dropped. That also explains why nothing caught this earlier: every other invalidation trigger (scroll, resize, streaming) is followed by further renders that repaint anyway, and use-text-selection.test.tsx asserts the controller state (setSelection(null)), which is correct — a unit test at that layer cannot observe the missing repaint. Mechanically this lives in pre-existing shared code, but the footer is the first selectable region whose content can change on its own with no follow-up render, so this PR is where it becomes reachable.

Verified fix. Deferring the clear out of the frame callback is enough — same build, same scenario, only an env flag switching the two paths:

-        clearSelection();
+        // setSelection() requests a repaint; issuing it from inside
+        // publishFrame() re-enters the render pass and the repaint is dropped.
+        setImmediate(clearSelection);
SEL_DEFER=off  selected: ..####################....  afterChange: ..####################....   <- stale
SEL_DEFER=on   selected: ..####################....  afterChange: ..........................   <- cleared

A regression test would need to assert at the render layer (e.g. that a frame is published with the highlight gone), since the current controller-level assertion already passes.

Repo checks on this branch

Check Result
Targeted suites (src/ui/selection, Footer, MainContent, Composer, src/ui/layouts) 143/143 passed (10 files)
npm run typecheck pass
eslint on the 6 changed source files clean
prettier --check on all 8 changed files clean
Full packages/cli suite 16692 passed / 40 failed in 8 files — the same 8 files fail on the base commit too (cli.test.ts, serve.test.ts, gemini.test.tsx, systemController.test.ts, capabilities-docs-contract.test.ts, fast-path.test.ts, AuthDialog.test.tsx, standalone-update.test.ts), mostly timeouts under parallel load. Pre-existing / environment-dependent, none related to this change.

Not covered

  • macOS only. Windows and Linux terminals were not exercised (same caveat as the PR).
  • Mouse bytes were injected into the pty rather than generated by a physical mouse; the byte sequences are the ones a terminal emits, and mouse-mode negotiation was verified separately at the protocol level.
  • Cross-screen selection, edge auto-scroll and semantic copy are out of scope here, as the PR states.

Recommendation

Approve the approach — the region model is clean and I could not break the exclusions. I'd like Finding 1 fixed before merge, since the PR and the new design doc both claim that behaviour and the current result is a highlight sitting on text that was never copied. Everything else checks out.

中文版本

本地真实环境验证(维护者)

我从源码分别构建了本 PR 与其 merge-base,并在真实 pty 中用真实 SGR 鼠标字节驱动两个版本,因此下面所有结论都来自真实的终端协议路径,而非 mock。

结论: 功能可用,范围控制得当 —— footer 选择精确(反向拖拽、多行 footer、窄屏布局、宽字符/CJK 均正确),composer / 分隔线 / 历史区行为均无变化。但有一条文档中承诺的行为在真实终端中不成立: footer 内容变化时,过期高亮不会被清除(对应 test plan 第 5 条与新增设计文档的最后一个要点)。控制器逻辑本身是对的,问题在于它请求的重绘被吞掉了。一行改动即可修复,我已在同一套 harness 中验证 —— 详见 Finding 1

验证方式

PR head bd1b424dragon/fix-8131-statusline-selection
基线 8bdb058(与 main 的 merge-base),以完全相同方式构建与驱动
构建 npm run build:packages && npm run bundle,再跑 node dist/cli.js(生产包,不用 scripts/dev.js
终端 @lydell/node-pty 真实 pty,TERM=xterm-256color,100×30(窄屏场景 60×26)
输入 直接向 pty 写入原始 SGR 序列 —— ESC[<0;c;r M 按下、ESC[<32;c;r M 拖拽、ESC[<0;c;r m 释放
画面读取 @xterm/headless 镜像 pty 输出;高亮通过逐单元格背景属性 diff 判定,而非肉眼观察
剪贴板 PATH 前置 fake pbcopy精确到字节捕获 payload
隔离 独立 HOME、独立工作区、伪造 OPENAI_* 环境变量 —— 不触碰用户任何配置

在 PR 构建上确认了协议层前提:应用确实发出 ?1049h?1002h?1006h,即 Virtualized History 与 SGR 鼠标跟踪均已生效。

证据 —— 修复前后对比

相同拖拽、相同 fixture,只有构建版本不同。

# 探针 基线 8bdb058 本 PR bd1b424
1 状态栏正向拖拽 剪贴板探针未被调用 ISSUE8131_STATUSLINE —— 精确 20 字节,高亮恰好覆盖第 3–22 列
2 状态栏反向拖拽 剪贴板探针未被调用 ISSUE8131_STATUSLINE —— 精确 20 字节
3 footer 第二行提示行 剪贴板探针未被调用 Auto mode —— 精确 9 字节
4 窄终端(60 列,footer 走 column 布局) 剪贴板探针未被调用 ISSUE8131_STATUSLINE —— 精确 20 字节
5 从 footer 起拖,向上拖入历史区 剪贴板探针未被调用 被钳制在 footer 行:SUE8131_(8 字节),未复制任何历史区文本,高亮仅限 footer 行
6 历史区拖过 Tips Tips —— 4 字节 Tips —— 4 字节(无变化)
7 在 composer 中拖过 COMPOSER_PROBE 无应用级复制 无应用级复制(无变化;composer 自身的输入框鼠标选择仍可用)
8 在输入框与 footer 之间的 ──── 分隔线上按下并拖拽 无选择 无选择(无变化)
9 footer 选中期间,无关区域(composer)内容变化 不适用 高亮保持 —— 失效判定正确地只作用于当前活动区域

超出 PR 自身 test plan 的额外探针(均在 PR 构建上):

探针 结果
宽字符/CJK 状态栏 模型:Qwen 分支:main 精确复制,UTF-8 23 字节;高亮正确覆盖全部宽字符单元
状态栏三击(整行选择) ISSUE8131_STATUSLINE,20 字节
footer 提示行双击(单词选择) Auto,4 字节
状态栏每秒重跑但输出完全相同 高亮正确保留 —— 失效判定基于内容而非刷新次数

截图见上方英文部分(基线 vs 本 PR、跨区域钳制、CJK、历史区、窄屏)。

Finding 1 —— footer 内容变化时过期高亮未被清除

PR test plan 第 5 条与 docs/design/statusline-text-selection.md 都声明 footer 内容变化时选择会被清除。真实终端中并非如此:高亮停留在原单元格上,此时它覆盖的是从未被选中、也从未被复制过的文本。

复现:状态栏配置为 cat <文件>refreshInterval: 1;选中状态栏后修改该文件内容(截图见上方英文部分)。

根因 —— 控制器判断正确,丢失的是重绘。 我在本分支的插桩构建中记录了帧订阅回调。在承载新 footer 文本的那一帧上,日志完全符合预期:

{"idx":1,"activeRect":{"x":0,"y":14,"width":100,"height":2},
 "sameRect":true,"sameContent":false,
 "baseRowAtRectY":"  ISSUE8131_STATUSLINE","curRowAtRectY":"  ISSUE8131_CHANGED!!!"}

sameContent:false → 触发 clearSelection()setSelection(null)requestRender()。但这个请求是publishFrame 内部重入式发出的,而 publishFrame 本身又是从 Ink 的 onRender 里调用的,因此它不会再产生新的一帧:

  • 帧计数:内容变化后只有一次 publishFrame,之后 5 秒内没有;
  • pty 字节追踪:最后一次写入正是「带选择背景地画出新文本」,之后 6 秒零字节
  • 只要发生任何无关重绘(敲一个键即可),高亮立刻消失。

也就是说失效判定是对的,只有重绘被丢弃。这也解释了为什么此前没被发现:其他失效触发路径(滚动、resize、流式输出)之后都还会继续渲染,顺带把画面刷对;而 use-text-selection.test.tsx 断言的是控制器状态(setSelection(null)),这一点本来就是对的 —— 该层级的单测无法观察到缺失的重绘。机制上这段代码是既有共享逻辑,但 footer 是第一个「内容可自行变化且之后没有后续渲染」的可选择区域,所以问题在本 PR 中才变得可达。

已验证的修复方案。 把清除操作挪出帧回调即可 —— 同一构建、同一场景,仅用一个环境变量切换两条路径:

-        clearSelection();
+        // setSelection() 会请求重绘;在 publishFrame() 内部发出会重入渲染流程,
+        // 该次重绘会被丢弃。
+        setImmediate(clearSelection);
SEL_DEFER=off  selected: ..####################....  afterChange: ..####################....   <- 过期高亮
SEL_DEFER=on   selected: ..####################....  afterChange: ..........................   <- 已清除

对应的回归测试需要在渲染层断言(例如断言发布了一帧且高亮已消失),因为当前控制器层的断言本来就能通过。

本分支上的仓库检查

检查项 结果
定向测试(src/ui/selectionFooterMainContentComposersrc/ui/layouts 143/143 通过(10 个文件)
npm run typecheck 通过
对 6 个改动源文件跑 eslint 无问题
对全部 8 个改动文件跑 prettier --check 无问题
packages/cli 全量测试 16692 通过 / 8 个文件中 40 条失败 —— 这 8 个文件在 base commit 上同样失败cli.test.tsserve.test.tsgemini.test.tsxsystemController.test.tscapabilities-docs-contract.test.tsfast-path.test.tsAuthDialog.test.tsxstandalone-update.test.ts),多为并发压力下的超时。属于既有 / 环境相关问题,与本改动无关。

未覆盖范围

  • 仅 macOS。未在 Windows 与 Linux 终端上验证(与 PR 自身说明一致)。
  • 鼠标字节是注入 pty 的,而非物理鼠标产生;但注入的正是终端会发出的字节序列,且鼠标模式协商已在协议层单独验证。
  • 跨屏选择、边缘自动滚动、语义复制按 PR 声明属于范围外。

建议

方案本身建议通过 —— 区域模型清晰,我没能破坏其排除逻辑。希望在合并前修掉 Finding 1:PR 与新增设计文档都承诺了该行为,而当前结果是高亮停留在从未被复制过的文本上。其余部分均无问题。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not explored to full depth (tool budget reached): Change summary: PR #8329 (fixes #8131) makes the footer/s...: did not fully trace whether Virtualized History mode is strictly alternate-screen; however, even if an inline path existed, the footer rect shares the exact map….

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

中文说明

未探索到全部深度(达到工具调用预算):Change summary: PR #8329 (fixes #8131) makes the footer/s...:did not fully trace whether Virtualized History mode is strictly alternate-screen; however, even if an inline path existed, the footer rect shares the exact map…

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment thread packages/cli/src/ui/selection/use-text-selection.tsx Outdated
Comment thread packages/cli/src/ui/selection/use-text-selection.tsx Outdated
Comment thread packages/cli/src/ui/components/MainContent.tsx
Comment thread packages/cli/src/ui/selection/use-text-selection.tsx Outdated
Comment thread packages/cli/src/ui/selection/use-text-selection.tsx Outdated

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

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 #8329 makes the footer/statusline a selectable region ...: none — all planned checks completed within budget.; PR #8329 makes the footer/statusline a selectable region ...: none — all checks I planned were completed (~13 of ~38 tool calls used).; PR #8329 makes the footer/statusline a selectable region ...: none — all checks I started were completed within budget..

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。

未探索到全部深度(达到工具调用预算):PR #8329 makes the footer/statusline a selectable region ...:none — all planned checks completed within budget.;PR #8329 makes the footer/statusline a selectable region ...:none — all checks I planned were completed (~13 of ~38 tool calls used).;PR #8329 makes the footer/statusline a selectable region ...:none — all checks I started were completed within budget.

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +295 to +296
expect(containerRef.current).not.toBeNull();
unmount();

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] R1-3 (round 1, still stands — partially addressed): the added ref test asserts only that containerRef.current is non-null, which passes no matter which element inside Footer carries the ref — probe-verified: moving ref={containerRef} from the outer Box to an inner Box keeps 34/34 Footer tests green while measureElementPosition would then return a partial-footer rect. The remaining wiring hops (DefaultAppLayoutMainContent/ComposerFooter) also still have zero end-to-end coverage (every surrounding test mocks the adjacent component). — Concrete cost: a refactor that moves the ref to the inner left-column box (which already carries statusLineRef), or drops any wiring hop, makes the footer unselectable again — the linked issue silently regresses while every test stays green.

Suggested fix: pin the ref to the outer box — e.g. expect(containerRef.current?.yogaNode.getComputedWidth()).toBe(80) under renderAtLayoutWidth(80, …) (only the outer width="100%" box spans the full layout width) — plus one integration-style test rendering the virtualized layout with a real footerRef, asserting the controller receives a non-empty additional rect once Footer mounts (the MainContent.tsx:471 wiring).

中文说明

R1-3(第 1 轮,仍然存在——仅部分解决):新增的 ref 测试只断言 containerRef.current 非空,无论 ref 挂在 Footer 内哪个元素上都能通过——已用探针验证:把 ref={containerRef} 从外层 Box 移到内层 Box,34/34 个 Footer 测试依然全绿,而 measureElementPosition 届时只会返回部分 footer 的矩形。其余传递链(DefaultAppLayoutMainContent/ComposerFooter)也仍然没有任何端到端覆盖(周边测试都把相邻组件 mock 掉了)。— 具体代价:任何把 ref 移到内层左列 box(已挂 statusLineRef)、或丢掉任一传递跳的重构,都会让 footer 重新变得不可选——关联 issue 会静默回归,而所有测试仍是绿的。建议修复:把断言钉到外层 box——例如在 renderAtLayoutWidth(80, …) 下断言 expect(containerRef.current?.yogaNode.getComputedWidth()).toBe(80)(只有 width="100%" 的外层 box 才有完整布局宽度)——并新增一个集成式测试:以真实 footerRef 渲染虚拟化布局,断言 Footer 挂载后控制器收到非空的附加矩形(即 MainContent.tsx:471 的接线)。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +358 to +359
listener!(makeTwoLineFrame('hullo', 'status'));
expect(setSelection).not.toHaveBeenCalled();

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] R2-1: the negative assertion runs before the queueMicrotask-deferred clear can land, so this test cannot catch over-invalidation — probe-verified: mutating the subscription to compare the whole frame instead of only the active rect's region leaves 17/17 tests green, and inserting await flushMicrotasks() makes that mutant fail while the PR code stays green. The new deferral also masks the two pre-existing synchronous negative tests at lines 334 and 346 of this file, which never flush microtasks either. — Failure scenario: a future change that over-invalidates on history-only frame changes queues a clear; the synchronous negative assertion passes before the microtask runs, and the regression ships green.

Suggested change
listener!(makeTwoLineFrame('hullo', 'status'));
expect(setSelection).not.toHaveBeenCalled();
listener!(makeTwoLineFrame('hullo', 'status'));
await flushMicrotasks();
expect(setSelection).not.toHaveBeenCalled();

(apply the same flush to the negative assertions at lines 334 and 346)

中文说明

R2-1:负向断言在 queueMicrotask 延迟清除落地之前就执行了,因此本测试无法捕获过度失效——已用探针验证:把订阅改为比较整帧(而非仅比较活动区域)后 17/17 测试仍全绿;而插入 await flushMicrotasks() 后,该变异体会失败,PR 代码则保持绿色。新的延迟机制同样掩盖了本文件中 334、346 行两个既有的同步负向测试——它们也从未 flush 微任务。— 失败场景:未来某个在「仅历史区内容变化」时过度失效的改动会入队一次清除,而同步负向断言在微任务运行前就已通过,回归会带着全绿的测试上线。对 334、346 行的负向断言也应用同样的 flush。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +212 to +214
if (activeRectIndexRef.current === 0) {
clearSelection();
}

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] R2-2: the wheel-scroll branch (scroll-up/scroll-down cases, now gated on activeRectIndexRef.current === 0) has no test that ever emits a scroll-* mouse event — every scroll test mutates scrollState and fires listener!(frame) (the frame-listener path) instead. Probe-verified: mutating the guard to unconditional clearSelection() leaves 18/18 tests green, while temporary wheel-event probe tests fail under the mutation and pass against the PR code. — Failure scenario: if the guard regressed to the old unconditional clear (or was inverted), the full suite stays green, yet a user who selects statusline text and then scrolls history with the wheel would have the selection silently dropped — the exact regression this PR exists to fix.

Suggested fix — add two controller tests emitting real wheel events:

// footer selection survives wheel scroll (select in the footer first)
handler(makeEvent('scroll-up', 5, 1));
expect(setSelection).not.toHaveBeenCalledWith(null);

// history selection still clears (select in the viewport first)
handler(makeEvent('scroll-down', 5, 1));
expect(setSelection).toHaveBeenCalledWith(null);
中文说明

R2-2:滚轮分支(scroll-up/scroll-down,现已以 activeRectIndexRef.current === 0 为门槛)没有任何测试真正发出 scroll-* 鼠标事件——所有滚动测试都是修改 scrollState 后触发 listener!(frame)(帧订阅路径)。已用探针验证:把该守卫变异为无条件 clearSelection() 后 18/18 测试仍全绿;而临时滚轮事件探针测试在该变异下失败、在 PR 代码上通过。— 失败场景:若该守卫回退为旧的无条件清除(或被写反),整个测试套件保持绿色,但用户选中状态栏文本后用滚轮滚动历史区,选择会被静默丢弃——这正是本 PR 要修复的回归。建议修复:新增两个发出真实滚轮事件的控制器测试(见上方代码)。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines 70 to 73
const row = frame?.cells[y];
if (!row) {
if (!row?.[x]?.selectable) {
return null;
}

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] R2-3: the rewritten lineSpanAt narrows pre-existing history-region behavior: triple-clicking a non-selectable cell now returns null (selects nothing), where the old code selected the whole visual line unconditionally. Non-selectable cells exist in the history viewport by default: CodeColorizer/DiffRenderer render line-number gutters with selectable={false} (showLineNumbers ?? true). — Failure scenario: in VP mode, a user triple-clicks a code line and lands on the line-number column: previously the whole line was selected and copied (gutter skipped on copy); now the press falls through to selection.start and the collapsed click is cleared on release — the gesture silently does nothing. This may be intentional under the new "anchor must be selectable" rule, but it changes behavior outside the footer scope this PR targets.

Suggested fix: if the old affordance should survive, snap x to the nearest selectable cell in the row before computing the run (preserving whole-line copy for gutter clicks); otherwise note the narrowing in the design doc so it is a conscious decision.

中文说明

R2-3:重写后的 lineSpanAt 收窄了历史区的既有行为:三击不可选择的单元格现在返回 null(不选择任何内容),而旧代码会无条件选中整行可见内容。历史视口中默认可存在不可选择的单元格:CodeColorizer/DiffRendererselectable={false} 渲染行号栏(showLineNumbers ?? true)。— 失败场景:VP 模式下,用户三击某行代码并落在行号列上:此前整行会被选中并复制(复制时跳过行号);现在按下事件落入 selection.start,折叠的点击在释放时被清除——原本可用的手势静默失效。这在新的「锚点必须可选择」规则下也许是有意为之,但它改变的是本 PR 目标(footer)之外的既有行为。建议修复:若要保留原有交互,在计算连续段之前把 x 吸附到该行最近的可选择单元格(保留点击行号复制整行的能力);否则在设计文档中注明这一收窄,使其成为有意识的决定。

— qwen3.8-max via Qwen Code /review (v0.21.9)

}
rowText += cell.value;
skippedLayoutGap = false;
} else if (rowText.length > 0) {

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] R2-4 (pattern, location 1/3 — the rowText.length > 0 flag guard): every guard condition of the layout-gap space insertion is individually unpinned by tests. Mutant-verified: (1) replacing this condition with true leaves every test green (test-efficacy harness-validated survivor); (2) deleting the two whitespace regexes leaves 54 selection + 313 consumer tests green, and a probe then yields doubled spaces ('label: 42%' instead of 'label: 42%'); (3) deleting cell.value !== '' flips a probe from 'ab' to 'a b' while all 67 selection tests stay green (today's frame builder cannot produce that trigger shape, so that guard is defensive). The existing gap tests only cover whitespace-free runs, a trailing gap with no following run, and carrier rows where the insertion never fires. — Failure scenario: a future edit that breaks or simplifies any of these conditions ships green; copying a selection bordering a non-selectable whitespace gap (a real shape — MaxSizedBox emits non-selectable ' '.repeat(...) padding) then yields doubled or missing spaces in the clipboard payload.

Suggested fix — pin each condition in selection-text.test.ts: a leading-gap row (assert no leading space), 'label: ' + gap + '42%' asserting 'label: 42%' (single space), the mirrored case where the run after the gap begins with a selectable space, and 'a' + gap + selectable '' spacer + 'b' asserting 'ab'.

中文说明

R2-4(模式类发现,位置 1/3——rowText.length > 0 标志守卫):布局空白处插入空格的每一个守卫条件都各自没有任何测试钉住。已变异验证:(1) 把此条件替换为 true,所有测试仍全绿(测试有效性 harness 确认的存活变异体);(2) 删除两个空白正则守卫,54 个选择测试 + 313 个消费方测试仍全绿,探针则产生双空格('label: 42%' 而非 'label: 42%');(3) 删除 cell.value !== '' 会使探针从 'ab' 翻转为 'a b',而 67 个选择测试依旧全绿(当前帧构建器无法产生该触发形状,因此该守卫是防御性的)。现有空白测试只覆盖无空白文本段、无后续文本段的尾部空白、以及插入永不触发的承载行。— 失败场景:未来任何破坏或简化这些条件的改动都会带着全绿测试上线;届时复制毗邻不可选择空白区的选择(真实存在的形状——MaxSizedBox 会输出不可选择的 ' '.repeat(...) 填充)会在剪贴板 payload 中产生多余或缺失的空格。建议修复:在 selection-text.test.ts 中为每个条件补充用例(见英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +46 to +47
!/\s$/u.test(rowText) &&
!/^\s/u.test(cell.value)

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] R2-4 (pattern, location 2/3 — the two whitespace-adjacency regexes): every guard condition of the layout-gap space insertion is individually unpinned by tests. Mutant-verified: deleting these two regex conditions leaves 54 selection + 313 consumer tests green, and a probe then yields doubled spaces ('label: 42%' instead of 'label: 42%'); the sibling conditions (rowText.length > 0 flag guard and cell.value !== '') are likewise unpinned (see their locations). The existing gap tests only cover whitespace-free runs, a trailing gap with no following run, and carrier rows where the insertion never fires. — Failure scenario: a future edit that breaks or simplifies these conditions ships green; copying a selection bordering a non-selectable whitespace gap (a real shape — MaxSizedBox emits non-selectable ' '.repeat(...) padding) then yields doubled spaces in the clipboard payload.

Suggested fix — pin each condition in selection-text.test.ts: 'label: ' + gap + '42%' asserting 'label: 42%' (single space), the mirrored case where the run after the gap begins with a selectable space, plus a leading-gap row (no leading space) and 'a' + gap + selectable '' spacer + 'b' asserting 'ab'.

中文说明

R2-4(模式类发现,位置 2/3——两个空白邻接正则):布局空白处插入空格的每一个守卫条件都各自没有任何测试钉住。已变异验证:删除这两个正则条件后,54 个选择测试 + 313 个消费方测试仍全绿,探针则产生双空格('label: 42%' 而非 'label: 42%');其余条件(rowText.length > 0 标志守卫与 cell.value !== '')同样没有被钉住(见对应位置的评论)。现有空白测试只覆盖无空白文本段、无后续文本段的尾部空白、以及插入永不触发的承载行。— 失败场景:未来任何破坏或简化这些条件的改动都会带着全绿测试上线;届时复制毗邻不可选择空白区的选择(真实存在的形状——MaxSizedBox 会输出不可选择的 ' '.repeat(...) 填充)会在剪贴板 payload 中产生双空格。建议修复:在 selection-text.test.ts 中为每个条件补充用例(见英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.9)

if (
skippedLayoutGap &&
rowText.length > 0 &&
cell.value !== '' &&

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] R2-4 (pattern, location 3/3 — the cell.value !== '' guard, defensive): every guard condition of the layout-gap space insertion is individually unpinned by tests. Mutant-verified: deleting this condition flips a probe from 'ab' to 'a b' while all 67 selection tests stay green. Reachability caveat (verified against ink's frame builder): today a wide glyph's leading and continuation cells share one selectable flag, so a selectable empty spacer right after a non-selectable gap is not currently producible — this guard is defensive regression-pinning. The sibling conditions are likewise unpinned (see their locations). — Failure scenario: a future frame-builder change or refactor that makes that shape reachable, or that deletes this guard, ships green and then silently corrupts clipboard payloads for selections bordering wide glyphs after layout gaps.

Suggested fix — pin the guard in selection-text.test.ts: 'a' + non-selectable gap + selectable '' spacer + selectable 'b', asserting 'ab' (no inserted space before the empty continuation cell).

中文说明

R2-4(模式类发现,位置 3/3——cell.value !== '' 守卫,防御性):布局空白处插入空格的每一个守卫条件都各自没有任何测试钉住。已变异验证:删除该条件会使探针从 'ab' 翻转为 'a b',而 67 个选择测试依旧全绿。可达性说明(已对照 ink 帧构建器核实):当前宽字符的首单元格与延续单元格共享同一可选择标志,因此「不可选择空白之后紧跟可选择空延续格」的形状目前无法产生——该守卫属于防御性回归钉扎。其余条件同样没有被钉住(见对应位置的评论)。— 失败场景:未来帧构建器的变化或重构使该形状变得可达、或该守卫被删除时,测试仍全绿,而毗邻「空白后宽字符」的选择的剪贴板 payload 会被静默破坏。建议修复:在 selection-text.test.ts 中补充用例钉住该守卫(见英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +228 to 232
const rectIndex = point
? rects.findIndex((rect) => pointInViewport(point, rect))
: -1;
if (!point || rectIndex < 0) {
clearSelection();

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] R2-5: both left-press clearSelection() paths that drop an existing selection — press outside every registered region (!point || rectIndex < 0) and the scrollbar hit-test branch above it — are pinned by zero tests: the only outside-region press test presses with no prior selection (where clearSelection hits its selection.isEmpty early-return and nothing observable happens), and every test mocks hitTestScrollbar={() => false}. Probe-verified: deleting either clearSelection() call leaves 18/18 tests green. The HEAD code is correct — this is a missing-test gap, not a current defect. — Failure scenario: if either clear were deleted by a regression, a user who selects footer/statusline text and then clicks the composer or a blank area keeps the stale highlight and stale selectionRef/activeRectIndexRef state until a later frame change clears them; while they persist, wheel events route through the stale index (a leftover index ≠ 0 skips the history-scroll clear).

Suggested fix — add two controller tests: (1) complete a drag selection, press at a coordinate inside no registered rect, assert setSelection was called with null; (2) same, but with hitTestScrollbar={() => true} for the pressed cell.

中文说明

R2-5:left-press 中两条会丢弃已有选择的 clearSelection() 路径——按在所有已注册区域之外(!point || rectIndex < 0)与其上方的滚动条命中分支——都没有任何测试钉住:唯一的区域外按下测试在没有先建立选择的情况下按下(此时 clearSelection 命中 selection.isEmpty 提前返回,没有任何可观察行为),且所有测试都把 hitTestScrollbar mock 为 () => false。已用探针验证:删除任一 clearSelection() 调用,18/18 测试仍全绿。HEAD 代码本身是正确的——这是缺测试的缺口,而非当前缺陷。— 失败场景:若任一清除被回归性删除,用户选中 footer/状态栏文本后点击 composer 或空白区域,过期高亮与 selectionRef/activeRectIndexRef 状态会保留到下一次帧变化才清除;期间滚轮事件会走旧的索引(残留的非 0 索引会跳过历史区滚动清除)。建议修复:新增两个控制器测试(见英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.9)

ex: 5,
ey: 0,
});
expect(lineSpanAt(frame, 7, 0)).toBeNull();

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] R2-6: the new gap test pins only left-of-gap (x=2) and in-gap (x=7) clicks; no assertion covers the segment RIGHT of the gap, so lineSpanAt's leftward start-scan is unpinned — probe-verified: the mutant let start = 0; keeps all current assertions green, and the assertion below fails under that mutant with exactly the gap-spanning span ({sx:0, ex:12}) while passing against the PR's correct implementation. — Failure scenario: shipped, that mutant makes triple-click on text right of a layout gap — e.g. the footer's right pills, exactly the content this PR newly makes selectable — span from column 0 across the non-selectable gap, highlighting the whole row including the blank gap instead of the contiguous run.

Suggested change
expect(lineSpanAt(frame, 7, 0)).toBeNull();
expect(lineSpanAt(frame, 7, 0)).toBeNull();
expect(lineSpanAt(frame, 11, 0)).toEqual({ sx: 10, sy: 0, ex: 12, ey: 0 });
中文说明

R2-6:新的空白测试只钉住了空白左侧(x=2)与空白内部(x=7)的点击;没有任何断言覆盖空白右侧的文本段,因此 lineSpanAt 向左扩展起点的扫描未被钉住——已用探针验证:变异体 let start = 0; 下现有全部断言仍为绿,而下方补充的断言在该变异体下会以恰好横跨空白的 span({sx:0, ex:12})失败,在 PR 的正确实现上通过。— 失败场景:若该变异体被带入线上,三击空白右侧的文本——例如 footer 右侧的指示区(正是本 PR 新近变为可选择的内容)——会从第 0 列横跨不可选择的空白取 span,高亮整行(含空白),而不是连续的可选择文本段。

— qwen3.8-max via Qwen Code /review (v0.21.9)

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge summary: PR #8329 ← origin/main

Root cause

Main's #8739 (feat(ui): word-wise drag after double-click, line-wise extension after triple-click, 26407f5) collided with this PR's footer-selection work. Both rewrote the multi-click/drag paths of use-text-selection.tsx: #8739 made double/triple clicks drag-extendable (removing SelectionState.selectSpan for start/extend + anchorSpanRef); this PR generalized the controller to multiple selectable rects with recordBaseline(rect) and generation-gated invalidation.

Textual AND semantic

Five textual conflicts in use-text-selection.tsx, plus two semantic collisions:

  1. selectSpan no longer exists. The multi-click branch takes feat(ui): word-wise drag after double-click, line-wise extension after triple-click #8739's drag-capable pattern with the PR's rect-aware baseline:
selection.start({ x: span.sx, y: span.sy }, mode);
selection.extend({ x: span.ex, y: span.ey });
anchorSpanRef.current = { span, mode };
recordBaseline(rect); // PR signature; main had recordBaseline()
  1. Sync vs deferred invalidation. feat(ui): word-wise drag after double-click, line-wise extension after triple-click #8739 copies at multi-click press and relies on a streaming repaint clearing the selection synchronously so the release skips re-copying; this PR defers that clear into a generation-gated queueMicrotask. Together they made the release re-copy stale content. Bridged inside the conflicted file:
// effect: remember the queued invalidation's generation
pendingInvalidationGenRef.current = invalidatedGeneration;
queueMicrotask(() => { pendingInvalidationGenRef.current = null; ... });
// left-release: content already replaced -> no extend, no copy
if (pendingInvalidationGenRef.current === selectionGenerationRef.current) {
  clearSelection();
  return;
}
  1. selection-span.ts auto-merge was silently broken: git kept the PR's 3-arg lineSpanAt(frame, x, y) but main's spanAtForMode still called the 2-arg form (type error, always null at runtime). Fixed to lineSpanAt(frame, point.x, point.y).

What is load-bearing

  • pendingInvalidationGenRef is set when scheduling the microtask, cleared inside it, and compared to the current generation at release. Presses and clearSelection() bump the generation so stale pendings never match. Breaking this ordering revives the stale-copy bug or the "stale invalidation clears a newer selection" regression.
  • Scroll-based clearing stays restricted to activeRectIndexRef.current === 0; footer selections survive history scrolls.
  • spanAtForMode is now the only span resolver; footer drags rely on its selectable-run semantics.

Not verified here

No build/tests were run (per instructions). Both sides' selection tests auto-merged conflict-free and were traced manually against the resolved code; CI should watch use-text-selection.test.tsx, use-text-selection-render.test.tsx, Footer.test.tsx, MainContent.test.tsx. Behavioral note: triple-click now selects the contiguous selectable run (PR semantics) instead of the whole visual line, which also shapes #8739's line-drag extension.

中文说明

冲突根源是 main 上的 #8739(双击按词、三击按行拖拽扩展)与本 PR 的页脚多选区改动同时重写了 use-text-selection.tsx。除 5 处文本冲突外还有两处语义冲突:① main 删除了 selectSpan,多击分支采用 main 的可拖拽模式并保留本 PR 的 recordBaseline(rect);② main 依赖"流式重绘同步清除选区"以避免 release 重复复制,而本 PR 将清除推迟到带代际守卫的 queueMicrotask——通过新增 pendingInvalidationGenRef(调度时记录代际、微任务内清空、release 时代际相同则直接清除且不复制)弥合。另修正了 selection-span.ts 自动合并遗留的旧两参 lineSpanAt 调用(类型错误),改为三参。未运行构建/测试;两侧测试均无冲突自动合并并已人工推演。三击现选择"可选单元格连续区间"而非整行。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not reviewed: build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally; the changed selection controller is unit-tested (packages/cli suite green) but has no integration coverage.

Not explored to full depth (tool budget reached): chunk 2: I didn't rerun the failing combination under high parallelism to reproduce the one-off failure, and I didn't run the full packages/cli suite (out of scope for t…; chunk 2: did not attempt to force-reproduce the one-off failure under heavier parallel/coverage load beyond the 8 clean reruns.; Change summary: This PR makes the footer/statusline a sel...: none — all checks I started completed within budget.; You are review agent reverse-audit — Reverse audit agen...: nothing significant — I completed all planned checks. Actually, one thing I didn't do: read chunk 1's implementation hunks (out of scope by design — chunk 3/oth…; You are review agent reverse-audit — Reverse audit agen...: none — all checks I started were completed within budget., and 1 more.

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; the changed selection controller is unit-tested (packages/cli suite green) but has no integration coverage。

未探索到全部深度(达到工具调用预算):chunk 2:I didn't rerun the failing combination under high parallelism to reproduce the one-off failure, and I didn't run the full packages/cli suite (out of scope for t…;chunk 2:did not attempt to force-reproduce the one-off failure under heavier parallel/coverage load beyond the 8 clean reruns.;Change summary: This PR makes the footer/statusline a sel...:none — all checks I started completed within budget.;You are review agent reverse-audit — Reverse audit agen...:nothing significant — I completed all planned checks. Actually, one thing I didn't do: read chunk 1's implementation hunks (out of scope by design — chunk 3/oth…;You are review agent reverse-audit — Reverse audit agen...:none — all checks I started were completed within budget.,另有 1 条。

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment thread packages/cli/src/ui/selection/selection-span.ts Outdated
containerRef,
);

expect(containerRef.current).not.toBeNull();

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] R1-3 (round 1, still stands): the added ref test asserts only that containerRef.current is non-null, which passes no matter which element inside Footer carries the ref — the test name says "outer box" but the body only proves "some box". — Failure scenario: a future edit moves ref={containerRef} from the outer width="100%" Box to an inner section box → measureElementPosition returns a rect covering only that section, so part of the statusline silently becomes unselectable and drags clamp mid-footer, while this test stays green.

Suggested change
expect(containerRef.current).not.toBeNull();
expect(containerRef.current).not.toBeNull();
expect(containerRef.current?.yogaNode?.getComputedWidth()).toBe(80);

(the outer box spans the full 80-column layout width; an inner section box would not)

中文说明

[Suggestion] R1-3(第 1 轮提出,仍然存在):新增的 ref 测试只断言 containerRef.current 非空——无论 ref 挂在 Footer 内哪个元素上都会通过;测试名说的是 "outer box",但测试体只能证明 "某个 box"。— 失败场景:未来某次修改把 ref={containerRef} 从外层 width="100%" Box 移到某个内部区块 box → measureElementPosition 返回的矩形只覆盖该区块,状态栏的一部分会静默变得不可选择、拖拽会在 footer 中间被钳制,而本测试仍然全绿。建议断言几何:外层 box 在 80 列布局下计算宽度为 80,内部区块则不是。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +53 to +54
} else if (rowText.length > 0) {
skippedLayoutGap = 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] R2-4 (pattern 1/3 — re-confirmed this round by a test-efficacy mutant): the rowText.length > 0 arming guard of the layout-gap space insertion is pinned by no test — forcing this condition true leaves every affected test green; nothing pins a row whose leading cells are non-selectable layout cells before any text (line-number gutters, hanging indents). — Concrete cost: any future change to the arming condition (unconditional arming, inverted guard, deletion) ships green while copied text gains a spurious leading space or loses a real gap-space.

Suggested fix — add a getSelectedText case for a row starting with non-selectable cells followed by selectable text, asserting no leading space is inserted (pins the guard's false branch).

中文说明

[Suggestion] R2-4(模式 1/3 —— 本轮经测试效力变异体再次确认):布局间隙空格插入的 rowText.length > 0 激活守卫没有任何测试钉住——把该条件强制为 true 后所有受影响的测试仍全绿;没有用例覆盖「行首是若干不可选择的布局单元格、之后才有文本」的行(行号栏、悬挂缩进)。— 具体代价:未来对激活条件的任何改动(无条件激活、反转守卫、删除)都会带着全绿的测试上线,而复制出的文本可能多出前导空格或丢失真实的间隙空格。建议修复:新增一个 getSelectedText 用例,行首为不可选择单元格、随后是可选择文本,断言不会插入前导空格(钉住该守卫的 false 分支)。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +476 to +480
getAdditionalSelectableRects={() =>
footerRef?.current
? [measureElementPosition(footerRef.current)]
: []
}

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] R3-2: the production wiring that computes the footer's selectable rect has no end-to-end test — MainContent.test.tsx stubs TextSelectionController (() => null), Composer.test.tsx mocks Footer (dropping containerRef), DefaultAppLayout.test.tsx mocks both MainContent and Composer, and every controller test hand-supplies rects. Distinct from the round-1 ref-threading thread (answered by the Footer attachment test): that test pins the attachment hop; nothing pins this callback or that the SAME ref object reaches both the measurer and the attacher. — Failure scenario: a refactor that drops or splits footerRef (Composer stops forwarding it; DefaultAppLayout passes distinct refs to the two children) silently disables footer selection or measures a wrong element in the real layout, with the entire suite green.

Suggested fix: add one integration-level test asserting TextSelectionController receives a getAdditionalSelectableRects whose result matches the real Footer's measured position — or, cheaper, assert in Composer.test.tsx that the mocked Footer receives the same ref object passed as footerRef.

中文说明

[Suggestion] R3-2:计算 footer 可选择矩形的生产接线没有任何端到端测试——MainContent.test.tsxTextSelectionController 桩成 () => nullComposer.test.tsx mock 掉 Footer(丢弃 containerRef),DefaultAppLayout.test.tsx 同时 mock MainContentComposer,而所有控制器测试都手工注入矩形。与第 1 轮的 ref 传递线程问题不同(那个已由 Footer 附着测试回应):那个测试钉住的是附着跳点;本回调本身、以及「同一个 ref 对象同时到达测量方与附着方」没有任何测试钉住。— 失败场景:某次重构丢弃或拆分了 footerRef(Composer 不再转发;DefaultAppLayout 给两个子组件传入不同的 ref),真实布局中 footer 选择会被静默禁用或测量到错误元素,而整个测试套件全绿。建议修复:新增一个集成层测试,断言 TextSelectionController 收到的 getAdditionalSelectableRects 结果与真实 Footer 的测量位置一致——或更便宜地,在 Composer.test.tsx 中断言 mock 的 Footer 收到的 ref 对象与传入的 footerRef 是同一个。

— qwen3.8-max via Qwen Code /review (v0.21.10)

return mode === 'word'
? wordSpanAt(frame, point.x, point.y)
: lineSpanAt(frame, point.y);
: lineSpanAt(frame, point.x, point.y);

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] R3-3: test-efficacy probe — reverting this spanAtForMode line-mode dispatch hunk (dropping point.x back to the 2-arg form) survives with every test green; spanAtForMode has zero references in any selection test file. — Failure scenario: a signature refactor that drops point.x here ships green through CI and silently kills whole-line triple-click selection in Virtualized History mode at runtime (y becomes undefined, frame?.cells[y] is always undefined, so every line-mode dispatch returns null).

Suggested fix:

it('resolves line spans through spanAtForMode', () => {
  const frame = frameFromLines(['status 42%']);
  expect(spanAtForMode(frame, 'line', { x: 2, y: 0 })).toEqual({
    sx: 0,
    sy: 0,
    ex: 9,
    ey: 0,
  });
});
中文说明

[Suggestion] R3-3:测试效力探针——单独回退 spanAtForMode 的 line 模式分发这一 hunk(把 point.x 丢回两参形式)后所有测试仍全绿;所有选择相关测试文件中 spanAtForMode 的引用数为零。— 失败场景:某次签名重构在这里丢掉 point.x,会带着全绿的 CI 上线,并在运行时静默杀死 Virtualized History 模式下的三击整行选择(y 变为 undefined,frame?.cells[y] 恒为 undefined,line 模式分发永远返回 null)。建议修复:新增一个直接调用 spanAtForMode(frame, 'line', {x, y}) 并断言返回连续行 span 的用例,端到端钉住分发路径。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +129 to +130
expect(footerWrites.length).toBeGreaterThan(0);
expect(footerWrites.at(-1)).not.toContain(SELECTION_BG);

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] R3-4: this render regression test asserts only footerWrites.at(-1) lacks the selection background — but probe-measured at HEAD, every footer content change under an active selection paints exactly ONE stale frame (the new text WITH the selection background) before the clean repaint: ink serializes the frame before publishFrame fires the subscriber that queues the deferred clear, so the clear can never land before the first paint of the changed content. The .at(-1) oracle therefore masks the one stale frame the test's own name ("without stale highlight") claims to catch — probe flip confirmed: the same scenario asserting .every(...) fails at HEAD. — Failure scenario: any future change that keeps eventual cleanness passes this test no matter how many stale intermediate frames it paints (a clear deferred ~80ms paints 2-3 visible stale frames; the 120ms window still ends on a clean final frame).

Suggested fix (a pair): strengthen to expect(footerWrites.every((write) => !write.includes(SELECTION_BG))).toBe(true); AND make the clear land before serialization — or keep .at(-1) and rename the test to say it asserts eventual clearing.

中文说明

[Suggestion] R3-4:该渲染回归测试只断言 footerWrites.at(-1) 不含选择背景色——但在 HEAD 上实测:激活选择下 footer 内容每次变化都会先画出恰好一帧过期画面(新文本仍带选择背景),随后才是干净的重绘;因为 ink 先序列化帧、再由 publishFrame 触发订阅回调入队延迟清除,清除永远赶不上变化内容的首次绘制。因此 .at(-1) 这个断言恰好掩盖了测试名("without stale highlight")声称要捕获的那一帧过期高亮——探针翻转已确认:同一场景改用 .every(...) 断言在 HEAD 上会失败。— 失败场景:未来任何「最终会清干净」的改动无论中途画出多少帧过期高亮都能通过本测试(清除若被推迟约 80ms,会画出 2-3 帧可见的过期画面,而 120ms 窗口结束时最后一帧仍是干净的)。建议修复(成对):把断言加强为 expect(footerWrites.every((write) => !write.includes(SELECTION_BG))).toBe(true); 并让清除赶在序列化之前落地——或者保留 .at(-1) 但把测试改名为「断言最终清除」。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +429 to +430
!sameViewportRect(baselineRectRef.current, activeRect) ||
!sameViewportContent(baselineFrameRef.current, frame, activeRect)

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] R3-5: the frame subscriber invalidates a footer-owned selection whenever ANY cell inside the whole footer rect changes (sameViewportContent compares every cell of the owning rect, never the selection span) — and the footer holds independent live elements: GoalPill re-renders every 1000 ms while a goal is active, plus context counters and changing custom statusline output. Probe-verified in the worktree harness: publishing a change to unrelated footer cells clears a completed selection whose own text never changed; published between press and release, the drag yields no copy at all (both orderings tested; the span-scoped fix flips it). — Failure scenario: with a goal active, a user drags across static statusline text; a GoalPill tick lands mid-drag → deferred clear before release → !selection.dragging at release → nothing copied. A completed selection's highlight is wiped within <1 s of the next tick (copy already succeeded, so that half is cosmetic). The maintainer's real-pty matrix tested "identical output survives" and "composer changes persist", but never an in-region live element changing.

Suggested fix: for non-viewport regions, invalidate only when the cells covered by the normalized selection change (span-scoped compare), or give footer char-drags a copy-at-press like multi-click has — note a naive span-scoped flip breaks the viewport's streaming copy-cancel (probe-verified), so the scoping must be region-aware.

中文说明

[Suggestion] R3-5:帧订阅回调会在整个 footer 矩形内任意单元格变化时失效 footer 拥有的选择(sameViewportContent 比较所属矩形的全部单元格,从不看选择 span 本身)——而 footer 里存在独立的动态元素:goal 激活时 GoalPill 每 1000ms 重渲染一次,此外还有上下文用量计数与会变化的自定义 statusline 输出。已在 worktree 探针中验证:发布一次只改变 footer 无关单元格的帧,会清掉一个自身文本从未变化的已完成选择;若该变化落在按下与释放之间,整个拖拽完全不会产生复制(两种时序均已测试;按 span 限定的修复可翻转结果)。— 失败场景:goal 激活时,用户拖选静态的状态栏文本;GoalPill 的计时刷新恰好落在拖拽中途 → 延迟清除先于释放落地 → 释放时 !selection.dragging → 什么都不复制。已完成选择的高亮也会在下一秒的刷新内被抹掉(此时复制已成功,这一半只是观感问题)。维护者的真实 pty 探针矩阵测试过「相同输出重跑高亮保留」与「composer 变化高亮保留」,但从未测试过区域内动态元素变化的情形。建议修复:对非视口区域,仅当规范化选择所覆盖的单元格发生变化时才失效(按 span 限定比较),或像多击那样给 footer 字符拖拽加上按下即复制——注意朴素的按 span 限定会破坏视口的流式取消复制语义(已探针验证),因此限定必须区分区域。

— qwen3.8-max via Qwen Code /review (v0.21.10)

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Not reviewed: build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally; the changed selection controller is unit-tested (packages/cli suite green) but has no integration coverage.

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; Change summary: PR #8329 (fixes issue #8131) makes the fo...: none — all planned checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — no check was left unfinished..

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; the changed selection controller is unit-tested (packages/cli suite green) but has no integration coverage。

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;Change summary: PR #8329 (fixes issue #8131) makes the fo...:none — all planned checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — no check was left unfinished.

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +314 to 316
recordBaseline(rect);
applyHighlight();
copySelection();

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] R4-2: double/triple-click (word/line) selection inside an additional selectable rect — behavior this diff newly enables — is exercised by zero tests: all 14 multi-click tests run against the viewport rect (index 0), and all six footer-region tests use char-mode drags only. — Failure scenario: probe-verified at HEAD: the path works today (a footer double-click copies the word, a triple-click copies the line), but a mutant adding activeRectIndexRef.current === 0 to the count >= 2 gate — the same guard shape this diff introduces at two other sites — survives all 58 selection tests while the probe flips, so a future scoping change would silently disable footer word/line selection (a double-click on a statusline word falls through to char mode; the press-time copy and span extension vanish) with the whole suite green.

Suggested fix: add controller tests that double-click a word and triple-click a line inside additionalSelectableRects, asserting the span coordinates, the press-time copy, and drag extension from the footer span (mirroring extends a double-click word selection word-wise on drag at rect index 1).

中文说明

[Suggestion] R4-2:在附加可选择区域(additional selectable rect)内的双击/三击(词/行)选择——本 diff 新启用的行为——没有任何测试覆盖:全部 14 个多击测试都运行在视口矩形(index 0)上,全部 6 个 footer 区域测试都只使用字符模式拖拽。— 失败场景:已在 HEAD 上用探针验证:该路径当前可用(footer 双击可复制单词、三击可复制整行),但给 count >= 2 分支加上 activeRectIndexRef.current === 0 的变异体(与本 diff 在另外两处引入的守卫形状相同)可以在全部 58 个选择测试保持绿色存活,而探针结果翻转——未来任何类似的区域限定改动都会静默禁用 footer 的词/行选择(双击状态栏单词会落入字符模式,按下即复制与 span 扩展都会消失),且整个测试套件全绿。

建议修复:新增控制器测试,在 additionalSelectableRects 内双击一个单词、三击一整行,断言 span 坐标、按下时的复制以及从 footer span 出发的拖拽扩展(可仿照 rect index 1 上的 extends a double-click word selection word-wise on drag)。

— qwen3.8-max via Qwen Code /review (v0.21.10)

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real terminal, BASE vs HEAD

I rebuilt this PR locally and drove the bundled CLI in a real pty with raw SGR mouse sequences, against a build of the merge base for comparison. The feature works exactly as described, the excluded regions stay excluded, and the history path is unchanged. I found two minor behavioural notes below — neither blocks merge.

Verdict: looks good to merge.

How this was verified

  • BASE = merge base ac78acd3c5, HEAD = de8955930b. HEAD merges cleanly into current main, and main has not touched any file this PR touches, so the merge base is a fair comparison point.
  • Both arms run the real npm run build && npm run bundle output (dist/cli.js) inside a real pty, Virtualized History + mouse tracking on, 100×32, isolated HOME. pty output is mirrored into a headless xterm so the painted grid is readable at any instant.
  • Mouse events are written straight into the pty as raw SGR sequences (tmux send-keys -H silently drops these).
  • Highlight is measured by scanning for the selection background the app actually paints (xterm colour 240), not by a frame diff.
  • Clipboard is measured with a recording pbcopy shim on the CLI's PATH, so every write is counted with its payload — "copied nothing" is distinguishable from "copied twice".
  • Statusline is a command statusline printing SLALPHA SLBETA SLGAMMA / SLDELTA SLEPSILON with refreshInterval: 1, so footer content can be changed on demand.

Results — every value is the exact clipboard payload the app wrote

Probe BASE (merge base) HEAD (#8329)
Statusline drag → nothing, 0 writes SLALPHA SLBETA (14 B, 1 write) ✅
Statusline drag ← (reverse) nothing, 0 writes SLALPHA SLBETA (14 B, 1 write) ✅
Statusline double-click nothing, 0 writes SLALPHA (7 B) ✅
Statusline triple-click nothing, 0 writes SLALPHA SLBETA SLGAMMA (22 B) ✅
Statusline drag across two rows nothing, 0 writes SLALPHA SLBETA SLGAMMA\nSLDELTA (30 B) ✅
Drag across the footer layout gap nothing, 0 writes SLALPHA SLBETA SLGAMMA (22 B) — stops at content end ✅
History char drag LINE-30 (7 B) LINE-30 (7 B) — unchanged
History triple-click ◆︎alpha beta gamma (22 B) alpha beta gamma (16 B) — changed, see Note 1
Composer drag over visible text nothing, 0 writes nothing, 0 writes ✅
Press + drag in the history/footer gap no selection no selection ✅
Press in footer → drag up into history no selection at all clamped to the footer row, SLALPHA S
Press in history → drag down into footer alpha beta gamma\ndelta byte-identical

Invalidation (HEAD only — BASE has no footer selection to invalidate):

Probe HEAD
Footer selected, 6 s idle with the statusline re-running every second held at 20/20 samples ✅
Footer selected, statusline text then changed cleared in ~200–400 ms ✅
Footer selected, history scrolled cleared, and does not return — Note 2
Footer selected, new history content streamed in cleared in ~300 ms — Note 2

feature

clamping and exclusions

invalidation

Note 1 — triple-click in the history changed too, which the description does not mention

lineSpanAt went from "column 0 → last content column" to "the contiguous selectable run around the cursor". That is what makes multi-run footer rows work, but it also changes an existing gesture:

  • BASE: triple-click on an answer's first line copies ◆︎alpha beta gamma — the decoration glyph glued to the text.
  • HEAD: copies alpha beta gamma.

For this case HEAD is clearly nicer. The trade-off worth stating explicitly: triple-click no longer spans a whole visual line across a layout gap — on a row with an internal non-selectable gap it now selects only the run under the cursor. That seems intentional (fix(cli): preserve line selection from layout cells), but it deserves a line in the PR description since it affects history, not just the footer.

Note 2 — footer selection is dropped by history activity, contrary to the stated invalidation scope

The description says invalidation "watches only that region's layout and content", and the code comment says footer selections "live outside the scrollable viewport and remain valid". Observed on HEAD:

  • Select statusline text, then wheel-scroll the history → the footer highlight disappears. The footer's content and screen position are byte-identical before and after (only the history rows and the scrollbar changed), and scrolling back does not bring it back — so the selection state was dropped, not merely un-painted.
  • Same when new history content streams in (~300 ms).

The unit test keeps a footer selection while history scrolls passes because it simulates a scroll by mutating scrollState while replaying the same unchanged frame object — that cannot reproduce what a real scroll does to the composited frame. I did not isolate the root cause.

Impact is cosmetic: the copy happens at release, before any of this, so the clipboard payload is always correct — in the scroll probe SLALPHA was already recorded. The user just loses the highlight and must re-select to copy again. Not a blocker; worth either fixing or relaxing the claim.

Automated checks (on HEAD)

  • The PR's 5 changed test files: 92/92 pass.
  • Mutation probes confirm the new tests are load-bearing, not decorative:
    • dropping the extra selectable rects (return [viewportRect]) → 6 tests fail, all footer ones;
    • reverting lineSpanAt to the pre-PR whole-line behaviour → 2 tests fail.
    • Both mutations reverted; working tree verified clean afterwards.
  • Full workspace npm run build + npm run bundle (incl. tsc --noEmit) succeed; Prettier and ESLint clean on all 12 changed files.

Not covered: Windows and Linux terminals, CJK/wide-glyph statuslines, and narrow/wrapped footer layouts.

中文版本

维护者验证 —— 真实终端,BASE vs HEAD

我在本地重新构建了这个 PR,用真实 pty 跑打包后的 CLI,注入原始 SGR 鼠标序列,并与 merge base 的构建做对照。功能与描述完全一致,被排除的区域确实没被波及,历史区行为未变。下面有两条次要行为说明,都不阻塞合并。

结论:可以合并。

验证方法

  • BASE = merge base ac78acd3c5HEAD = de8955930b。HEAD 能干净合入当前 main,且 main 没有改动本 PR 涉及的任何文件,因此 merge base 是公平的对照点。
  • 两条腿都跑真实的 npm run build && npm run bundle 产物(dist/cli.js),在真实 pty 中启用 Virtualized History + 鼠标跟踪,100×32,隔离 HOME。pty 输出实时喂给 headless xterm,可在任意时刻读取真实屏幕。
  • 鼠标事件直接写入 ptytmux send-keys -H 会静默丢弃 SGR 鼠标序列)。
  • 高亮判据是扫描应用真正绘制的选择底色(xterm 256 色 240 号),不用帧间 diff。
  • 剪贴板判据是挂在 CLI PATH 上的记录型 pbcopy 垫片,每次写入的次数与内容都会入账 —— 能区分"没复制"和"复制了两次"。
  • 状态栏用 command 类型输出 SLALPHA SLBETA SLGAMMA / SLDELTA SLEPSILON,配 refreshInterval: 1,以便按需改变 footer 内容。

结果 —— 表中每个值都是应用真正写入剪贴板的内容

探针 BASE(merge base) HEAD(#8329
状态栏正向拖拽 无,0 次写入 SLALPHA SLBETA(14 B,1 次写入)✅
状态栏反向拖拽 无,0 次写入 SLALPHA SLBETA(14 B,1 次写入)✅
状态栏双击 无,0 次写入 SLALPHA(7 B)✅
状态栏三击 无,0 次写入 SLALPHA SLBETA SLGAMMA(22 B)✅
状态栏跨两行拖拽 无,0 次写入 SLALPHA SLBETA SLGAMMA\nSLDELTA(30 B)✅
跨 footer 布局间隙拖拽 无,0 次写入 SLALPHA SLBETA SLGAMMA(22 B),停在内容末尾 ✅
历史区字符拖拽 LINE-30(7 B) LINE-30(7 B)—— 未变
历史区三击 ◆︎alpha beta gamma(22 B) alpha beta gamma(16 B)—— 有变化,见说明 1
在 Composer 可见文本上拖拽 无,0 次写入 无,0 次写入 ✅
在历史区与 footer 之间的空隙按下并拖拽 不产生选择 不产生选择 ✅
在 footer 按下 → 向上拖进历史区 完全没有选择 被限制在 footer 行,SLALPHA S
在历史区按下 → 向下拖进 footer alpha beta gamma\ndelta 逐字节相同

失效逻辑(仅 HEAD,BASE 根本产生不了 footer 选择):

探针 HEAD
选中 footer 后静置 6 秒(状态栏每秒重跑) 20/20 次采样均保持 ✅
选中 footer 后状态栏文本真的改变 ~200–400 ms 内清除 ✅
选中 footer 后滚动历史区 被清除,且不会恢复 —— 见说明 2
选中 footer 后有新的历史内容流入 ~300 ms 内被清除 —— 见说明 2

说明 1 —— 历史区的三击行为也变了,PR 描述里没有提到

lineSpanAt 从"第 0 列 → 最后一个内容列"改成了"光标周围连续的可选择片段"。这正是多片段 footer 行能工作的原因,但它同时改变了一个既有手势:

  • BASE:在回答首行三击,复制到的是 ◆︎alpha beta gamma —— 装饰字形和正文粘在一起。
  • HEAD:复制到 alpha beta gamma

就这个场景而言 HEAD 明显更好。需要明确指出的权衡是:三击不再跨布局间隙覆盖整个视觉行 —— 在中间含有不可选择间隙的行上,现在只会选中光标所在的那一段。这看起来是有意为之(fix(cli): preserve line selection from layout cells),但它影响的是历史区而不只是 footer,值得在 PR 描述里补一句。

说明 2 —— footer 选择会被历史区活动清除,与所声明的失效范围不符

描述里写的是失效逻辑"只观察该区域的布局和内容",代码注释也写着 footer 选择"位于可滚动视口之外,保持有效"。HEAD 上的实测:

  • 选中状态栏文本后滚动历史区 → footer 高亮消失。前后两帧 footer 的内容和屏幕位置逐字节相同(变的只有历史区行和滚动条),并且滚回原位也不会恢复 —— 说明被丢弃的是选择状态本身,而不只是没有重绘。
  • 有新的历史内容流入时同样如此(~300 ms)。

单测 keeps a footer selection while history scrolls 之所以通过,是因为它模拟滚动的方式是改 scrollState 再把同一个未变的 frame 对象重放一遍 —— 这无法复现真实滚动对合成帧的影响。我没有定位到根因。

影响是观感层面的:复制发生在释放时,早于上述任何一步,所以剪贴板内容始终正确 —— 滚动那组探针里 SLALPHA 已经先被记录下来了。用户损失的只是高亮,想再复制一次得重新选。不阻塞合并;要么修掉,要么把说法放宽。

自动化检查(HEAD 上)

  • PR 改动的 5 个测试文件:92/92 通过
  • 变异测试证明新增测试是有效的,不是摆设:
    • 去掉额外可选择区域(return [viewportRect])→ 6 个测试失败,全是 footer 相关的;
    • lineSpanAt 回退成 PR 之前的整行行为 → 2 个测试失败
    • 两处变异均已还原,事后确认工作区干净。
  • 全 workspace npm run build + npm run bundle(含 tsc --noEmit)均成功;12 个改动文件 Prettier 与 ESLint 全部通过。

未覆盖:Windows 与 Linux 终端、CJK/宽字形状态栏、以及窄屏/换行的 footer 布局。

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 208 passed · 0 failed · 208 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:208 通过 · 0 失败 · 208 总计

Verification report

PR 8329 Deep Verification (follow-up round) — fix(cli): enable footer text selection in VP mode

Verdict: merge-ready — 208/208 scripted assertions passed, 0 unexpected failures (see assertions.json).
Verified head: de8955930bc23093e10de2369e84b88d2a9d7025 (merge commit 4f43bae, base tip 9259c35; the metadata snapshot's baseRefOid ac78acd drifted — the merge ref was rebuilt against the newer main tip, and HEAD^1..HEAD is what was verified).
Three non-blocking findings, all coverage gaps/dead code, none behavioral: two carried over from the previous round (re-measured, stand) and one new (the scroll-event guard is unpinned).

中文 — 结论与摘要

结论:merge-ready(脚本断言 0 意外失败;数字见 assertions.json)。

本轮为跟进轮:上一轮验证的头是 bd1b4241,此后 PR 新增两个修复提交(964c72a9 稳定 footer 选择失效逻辑、de895593 从布局单元保留行选择)并两次合并 main。所有既往测量均在新头上重新执行,未沿用旧数据。

  • 上一轮 findings 状态:F1(move 阶段钳制无测试钉住)仍存在(M2 突变体 84/84 全绿,探针夹具 head 绿/突变体红);F2(clearSelectionactiveRectIndexRef.current = null 防御性死代码)仍存在(M4 突变体 84/84 全绿,读者路径分析同上一轮)。两者均为覆盖缺口/死代码,非行为缺陷。
  • A/B 结论(单元层):HEAD 受影响表面 171/171 全绿;HEAD 测试文件放入 BASE worktree 后 118 个测试中恰好 13 个新行为测试变红(footer 拖拽/钳制/失效/滚动保留/布局单元行选择/间隙空格/ref 接线),其余 105 两臂均绿。base 侧失败信息均为期望值 vs 实际值的行为断言(唯一例外:does not let stale invalidation… 在 base 上因不存在延迟队列而以 TypeError: queued[0] is not a function 失败——机制本身在 base 不存在)。
  • A/B 结论(真实 TUI 层):真实 bundle + pty + 伪 xclip 剪贴板探针。HEAD 在 statusline 行(终端第 18 行)正/反向拖拽均精确复制 26 字节 marker,整行拖拽无重复空格;BASE 扫描全部行无任何 marker 复制(死区复现,marker 在屏幕上可见);历史区拖拽两臂同 10 行均复制,9/10 行字节一致,唯一差异是 bullet 后的可见空格——base 的提取器丢弃、head 的间隙空格规则恢复(即 M9 钉住的机制在真实内容上的表现,非回归);composer 两臂均无复制;鼠标模式两臂一致(?1002h ?1006h ?1049h)。
  • 新发现(建议):滚动事件分支的区域守卫(footer 选择遇到 scroll-* 事件不清除)没有任何测试钉住——M7 突变体 84/84 全绿;已提供探针夹具(head 绿、M7 下红)。
  • 未覆盖:逐 commit 归因(浅克隆 depth=2,仅 de895593 可达);Windows 真实终端;E2E 未现场演练"footer 内容变化清除高亮"与"历史滚动时 footer 选择存活"(均由单元 A/B + 突变矩阵覆盖)。

Previous-finding status (follow-up round)

# Finding (previous round) Severity Status at de895593
F1 Move-time drag clamp (clampToViewport in the move branch) unpinned — mutant survived the whole suite because the release branch also clamps Suggestion Stands — re-measured: M2 (move branch extendActiveDrag(point)) survives 84/84 at the new head. The new render test (use-text-selection-render.test.tsx) pins repaint cleanliness, not the transient highlight. Probe fixture green on head / red under M2 (see Finding 1).
F2 activeRectIndexRef.current = null in clearSelection is defensive dead code Nit Stands — re-measured: M4 (delete the line) survives 84/84. Reader census unchanged plus the new generation machinery: scroll branch (=== 0 compares identically for null), move/release gated on dragging, repaint listener gated on isEmpty, next press overwrites.

Both re-measurements were re-run at the new head (no carry-forward shortcut: the hook changed materially since the previous round).

Central claim and A/B proof

Central claim (unchanged): in Virtualized History mode the footer/statusline is a
selectable region — a drag there highlights and copies its exact text, where base has a
dead zone. New secondary claims this round: (A) invalidation is deferred and
generation-guarded — footer selections survive history scrolls, stale invalidation cannot
clear a newer selection, and a release during a pending repaint refuses to copy stale
content; (B) line selection snaps from non-selectable layout cells (padding, gutters,
wide-character spacers) to the nearest selectable run, and copies join layout gaps with a
single space.

A/B 1 — controller level (head tests vs base controller)

HEAD's test files were copied verbatim into a git worktree at HEAD^1; the only code
difference between arms is the PR's. The base worktree reuses the root + per-package
node_modules (PR touches no package.json/lockfile/packages/core — verified empty
diff; core content byte-identical by diff -q, so the @qwen-code/qwen-code-core
symlink resolving into the head tree is a clean control; ink realpath is the shared
root copy on both arms).

Arm Result Detail
HEAD (affected surface, 10 files) 171/171 pass incl. selection dir 84/84
HEAD (paired subset: selection dir + Footer) 118/118 pass 03-ab-head-suite.png (selection dir re-run: 84/84)
BASE (head tests on base code) 105 pass / 13 red exactly the new-behavior tests, 02-ab-base-suite-reds.png

The 13 base reds, with their failing assertions:

Test (base arm) Failure
turns a footer drag into a highlight and clipboard payload expected last "spy" call to have been called with [ { sx: +0, sy: 1, ex: 5, ey: 1 } ]
clamps a footer drag to the footer when it enters history expected last "spy" call … { sx: +0, sy: 1, ex: 2, ey: 1 }
clears a footer selection when footer content changes expected "spy" to be called with arguments: [ null ] — Number of calls: 0
clears a footer selection when its layout changes same shape
keeps a footer drag active while history scrolls expected last "spy" call … { sx: +0, sy: 1, ex: 4, ey: 1 }
does not let stale invalidation clear a newer selection TypeError: queued[0] is not a function — base has no deferred-invalidation queue, the test's own fixture cannot engage (mechanism absence, not a value assertion)
publishes a repaint without stale highlight after footer content changes (render) expected false to be true — no highlight ever renders on base
spans from column 0 to the last non-space cell expected null to deeply equal { sx: +0, sy: +0, ex: 9, ey: +0 } (old lineSpanAt(frame, y) signature)
stops at a non-selectable layout gap expected null …
snaps non-selectable history padding and gutters to visible content expected null …
snaps a non-selectable wide-character spacer to its glyph expected null …
separates selectable runs split by a layout gap expected 'status42%' to be 'status 42%'
attaches the selectable-region ref to its outer box (Footer) expected null not to be null

Note the direction controls: does not select frame content outside registered regions
and triple-clicks history text from non-selectable trailing padding pass on BOTH arms
(they pin behavior base already had); the base-arm greens are the harness-soundness
control, and the two whole-line span tests passing on head while their siblings fail on
base show the new lineSpanAt did not regress the plain-whole-line case.

A/B 2 — real TUI (bundled CLI under a pty, fake xclip oracle)

Both bundles rebuilt from source with the repo's own esbuild config (head:
dist/cli.js + chunks; base: same inside the base worktree, after regenerating the two
git-ignored generated inputs). Sanity gate: getAdditionalSelectableRects appears in 1
head chunk and 0 base chunks. The CLI ran under a 100×30 pty with an isolated HOME
(VP + mouse tracking on, statusline command emitting a unique marker), CI*/QWEN_*
env stripped, and a fake xclip first on PATH writing the exact clipboard payload to
a file — the oracle is that file, never a screen decode. Drags are synthesized SGR
bytes (press 0, motion 32, lowercase-m release); rows located by calibrated scan
(05-e2e-ab-base-vs-head.png).

Cell BASE bundle HEAD bundle
Marker visible on screen yes yes
Footer row located by scan never (21 rows scanned, dead zone) row 18
Footer forward drag no copy exact marker, 26 bytes
Footer reverse drag no copy exact marker, 26 bytes
Footer full-width drag no copy marker, single-space join, no double spaces
History drags (banner + notifications) copied, 10 rows copied, same 10 rows
Composer drag over typed probe no copy no copy
Mouse modes on the wire ?1002h ?1006h ?1049h identical

History payloads are byte-identical on 9 of 10 rows; the single delta is row 10
('●︎Auto mode ' on base vs '●︎ Auto mode ' on head, and the same bullet on row 13 of
the scan phase): the space between the bullet glyph and the text is a non-selectable
layout cell that base's extractor silently drops, and the PR's gap-space rule (M9-pinned)
restores. Head's payload matches what is on screen; this is the mechanism working on
real viewport content, not a regression — the scripted parity check normalizes exactly
this shape and passes.

Mutation matrix (head; unmutated control 84/84 green first)

04-mutation-matrix.png. Positive control M0 (copy disabled) flipped 21 tests red, so
the harness can fail.

Mutant Guard under test Suite Verdict
M0 copySelection disabled (positive control) 63/84 killed (21 tests)
M1 press hit-test ignores additional rects 78/84 killed (6 footer tests)
M2 move branch clamp removed 84/84 survivor → coverage gap (Finding 1, carried over)
M3 invalidation watches rect 0 instead of the active rect 83/84 killed by keeps a footer selection while history scrolls
M4 clearSelection keeps stale rect index 84/84 survivor → dead code (Finding 2, carried over)
M5 microtask generation guard removed 83/84 killed by does not let stale invalidation clear a newer selection
M6 release-time pending-invalidation check removed 83/84 killed by keeps the double-click copy when streaming clears the selection before release — a pre-existing test whose timing the deferred clear changed now pins the new guard
M7 scroll-event branch clears all regions 84/84 survivor → coverage gap (Finding 3, new)
M8 lineSpanAt whole-row revert (keeps new signature) 82/84 killed by layout-gap + padding/gutter snap tests
M9 gap-space insertion removed from getSelectedText 83/84 killed by separates selectable runs split by a layout gap

M8 note: the wide-character spacer test does not flip under M8 (whole-row logic returns
the same span when the spacer is adjacent to content) — it pins a different regression
shape (an implementation that rejects non-selectable clicks outright) and was red on base.

Reviewer Test Plan, per step

  1. Enable VP + visible statusline — ✅ pty run, default settings + command statusline.
  2. Drag across statusline in either direction, exact copy — ✅ both directions, exact 26-byte payload on head; dead zone on base.
  3. History drag continues to work — ✅ same 10 rows copy on both arms; 9 byte-identical, 1 differs only by the intended gap-space restoration (see A/B 2).
  4. Composer drag does not start a copy — ✅ zero copies on either arm.
  5. Change footer content after selecting → stale highlight clears — unit level only: clears a footer selection when footer content changes (red on base, green on head) + the render test + M1/M3 kills; not driven live through the pty.

Findings

1. [Suggestion, carried over] Move-time drag clamp remains unpinned

Re-measured at the new head: deleting clampToViewport from the move branch only
(M2) survives 84/84 because the release branch also clamps — final range and copied text
are unchanged; only the highlight rendered during the drag can transiently span both
regions. The PR's new render test does not cover this axis. Probe fixture (temporary,
exercised in this round, not committed): green on head, red under M2
(expected range.sy toBe 1 — the intermediate highlight escapes the footer row). The
fixture from the previous round's report still applies verbatim.

2. [Nit, carried over] activeRectIndexRef.current = null in clearSelection remains dead code

M4 survives 84/84 at the new head. The new generation machinery added one more writer
(selectionGenerationRef.current += 1 in clearSelection) but no new reader of the
index that could observe a stale value: every read path is gated on dragging
(move/release), isEmpty (repaint listener), or compares === 0 where null behaves
identically (scroll branches); the next press overwrites. Harmless either way.

3. [Suggestion, new] The scroll-event region guard is unpinned

The scroll-* branch keeps footer selections alive (if (activeRectIndexRef.current === 0)),
but no test drives a scroll event with an active footer selection — the existing
keeps a footer selection while history scrolls drives the repaint listener, a
different path. M7 (unconditional clearSelection() on scroll events) survives 84/84.
Probe exercised this round: select footer → scroll-up → expect no setSelection
call; head green, red under M7 (paired control: a history selection still clears on
scroll events on both). Behavior on head is correct; nothing pins it.

Probe fixtures that pin the two surviving axes (measured: green on head, red under the matching mutant)
it('probe A: keeps the drag highlight inside the footer during a move into history', () => {
  // fixture: two-line frame, viewport row 0, footer rect row 1
  handler(makeEvent('left-press', 1, 2));
  handler(makeEvent('move', 3, 1));
  for (const call of setSelection.mock.calls) {
    const range = call[0];
    if (range) { expect(range.sy).toBe(1); expect(range.ey).toBe(1); }
  }
});

it('probe B: a scroll event keeps a completed footer selection', () => {
  handler(makeEvent('left-press', 1, 2));
  handler(makeEvent('left-release', 6, 2));
  setSelection.mockClear();
  handler(makeEvent('scroll-up', 3, 1));
  expect(setSelection).not.toHaveBeenCalled();
});

Not covered

  • Per-commit attribution — depth-2 checkout: git rev-list HEAD^1..HEAD^2 returns only de895593 (shallow boundary truncates silently), while the metadata lists six commits (two prior-round-verified, two upstream merges, 964c72a9, de895593). Everything above verifies the aggregate HEAD^1..HEAD diff; the two new fix commits were not isolated from each other (both sit behind the same aggregate A/B).
  • Windows real-terminal validation (author-flagged ⚠️) — this round ran on Linux in a container pty with synthesized SGR events; the mechanism is terminal-agnostic SGR, but real-terminal input quirks on Windows/Linux remain untested. This round's pty run supersedes the previous round's as the Linux-side witness (new head, rebuilt bundles).
  • Footer survival across a live wheel scroll in the pty — proven at unit level (M3 killed; probe B pins the event path) but not driven through the pty; a live scroll also moves viewport content, and the repaint-listener half of that combination is the M3-killed test.
  • Reviewer Test Plan step 5 live — footer-content invalidation exercised at unit level only (see above), not through the pty.
  • Repo-wide test suite — only the affected surface ran (171 tests across selection dir + Footer/Composer/MainContent/DefaultAppLayout), plus workspace-wide typecheck and ESLint/Prettier on the changed files (liveness-proven: a planted unused variable was reported, then removed).
  • The layout-gap single space with visible text on both sides in the real footer — the E2E full-width footer drag copied only the marker (no right-section content rendered inside the measured footer rect in this boot), so the single-space join was exercised by the unit fixture ('status 42%') and mutant M9, not by a two-sided real-footer payload.
  • Cross-screen selection, edge auto-scroll, semantic copy — declared out of scope by the PR; not probed.
  • E2E harness boot failures — two early harness iterations failed before any gesture ran, for harness-side reasons (a relative HOME env breaking loadSettings' realpathSync(resolvedHomeDir), plus a missing <cwd>/tmp the same boot path lstat's). The crash site is packages/cli/src/config/settings.ts boot plumbing, which the PR does not touch (git diff HEAD^1..HEAD has no config files), and the identical boot succeeds on both arms once the harness passes an absolute HOME — so these iterations produced no A/B evidence and are reported here rather than as PR behavior.

Methodology

Environment: the CI verify container (node:22-bookworm), merge-ref checkout at depth 2 (HEAD merge commit, HEAD^1 base tip 9259c35, HEAD^2 = verified head de895593); npm ci + npm run build pre-run. Unit A/B: head suite at HEAD, then HEAD's test files copied into a git worktree at HEAD^1 (root + per-package node_modules symlinked; @qwen-code/qwen-code-core resolves into the head tree, content-identical because the PR leaves packages/core untouched — verified by empty git diff --stat and diff -q). Mutations: single-point source edits applied/restored by mutation-driver.mjs (each mutant's anchor verified unique before applying; git checkout + empty-diff verification after each) plus two manual probe runs. E2E: pty-e2e.py spawns each bundle (node dist/cli.js) under a pty.openpty() pty (100×30, TIOCSWINSZ) with an isolated HOME, fake xclip/xsel/wl-copy shims writing the payload to a file (the sole oracle), a command statusline emitting a unique marker, and synthesized SGR gestures; rows are located by calibrated scan because ink's diff renderer makes absolute grid decode unreliable. Gates: npm run typecheck (exit 0), npx eslint + npx prettier --check on the 12 changed files. Raw per-arm JSON reports, mutation logs, both full session byte streams (logs/e2e-session-raw-{head,base}.log), and per-cell JSON results live in logs/; harness scripts are in the artifact directory root.

Evidence images

02-ab-base-suite-reds

03-ab-head-suite

04-mutation-matrix

05-e2e-ab-base-vs-head

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@DragonnZhang
DragonnZhang added this pull request to the merge queue Aug 12, 2026
Merged via the queue into QwenLM:main with commit 50097c1 Aug 12, 2026
101 of 102 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.11.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(cli): statusline text cannot be selected in Virtualized History mode

4 participants