fix(cli): enable footer text selection in VP mode - #8329
Conversation
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
|
@qwen-code /triage |
|
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 reportPR 8329 Deep Verification — fix(cli): enable footer text selection in VP modeVerdict: 中文摘要结论:
Central claim and A/B proofCentral claim: in Virtualized History mode, the footer/statusline is a selectable A/B 1 — controller level (head tests vs base controller)The PR's new test file was run against both arms in a
The three base-arm reds fail on the intended behavioral assertions — e.g. A/B 2 — real TUI (bundled CLI under a pty, fake
|
| 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
- Enable VP + visible statusline — done (pty run,
ui.useTerminalBufferdefault + custom statusline command). ✅ - Drag across statusline in either direction, exact copy — ✅ both directions, exact 26-byte payload.
- History drag continues to work — ✅ exact word copied, identical on base and head.
- Composer drag does not start a copy — ✅ no copy on any row around the typed text, both arms.
- 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 onlybd1b4241(the shallow boundary silently truncates rather than
erroring), while the metadata lists two commits (620e9822fix +bd1b4241
test). Everything above verifies the aggregateHEAD^1..HEADdiff; 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
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
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 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 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 模式的终端级鼠标跟踪使其落入终端原生选择和应用级选择都无法触及的死区。在现有选择框架内修复完全在范围内,没有引入新方向。 规模:不适用——未触及核心模块路径。所有改动在 方案:范围合理。一个选择控制器从单一视口矩形泛化为有序可选矩形列表,footer 成为第二个矩形,拖拽限制在起始区域内,失效逻辑只观察该区域。首轮之后分支与 main 的 #8739(按词/按行拖拽扩展)完成对齐:合并保留了 #8739 的可拖拽多击路径,用带代际守卫的微任务桥接其同步失效与本 PR 的延迟失效,并让 风险:无升级风险信号——改动文件均未匹配高风险路径模式。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
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
No criticals at this head. The round-4
Test evidenceThe PR's own CI on this head — the
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 ( Sandboxed verification would settle the one residual: 中文说明代码审查(合并后 head 重跑)独立方案: 为控制器增加一个返回额外可选矩形的可选回调,按下时命中测试所有矩形、记录所属矩形、拖拽限制其内、失效按区域划分,footer 通过布局层持有的 ref 用现有
本 head 无 Critical。第 4 轮
测试证据本 head 上 PR 自己的 CI—— 除 CI 外,本评论引用的是维护者的验证(注明出处,非机器人重跑):wenshao 用真实 pty 驱动打包 CLI、注入原始 SGR 鼠标序列,对 merge base 与本 commit 做对照——footer 拖拽/双击/三击复制出精确内容而 BASE 无任何写入,排除区域保持排除,历史区除有意的说明 1 变化外逐字节一致,变异探针( 沙箱验证可以填补唯一剩余缺口: — Qwen Code · qwen3.8-max Reviewed at |
|
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 — 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 This approval supersedes the bot's earlier changes-requested state, which was raised on a prior head for the now-fixed Critical. The sandboxed Approving, pinned to the reviewed commit. ✅ 中文说明置信度:4/5 —— 实现干净且最小化,第 3 轮 Critical 已修复并有回归测试;行为已由维护者在该 commit 上验证;只剩非阻塞的小问题。 退一步看:这个 PR 只做一件事,而且做得好。控制器以最小的接口面获得多矩形支持——一个可选 prop、一个 ref、复用现有测量工具——与 #8739 的合并对齐也足够审慎(代际守卫正是同步失效与延迟失效冲突所需的桥接)。我的独立方案与 diff 一致,因此不存在被错过的更简路径;每处改动都服务于既定目标。 审查过程本身也说明问题。第 3 轮 Critical——历史区三击落在不可选单元格(行号栏、填充、缩进)上时 之所以是 4 而不是 5:失效范围的声明("只观察该区域")相对观测到的行为仍然说过头了(维护者说明 2——历史区活动会丢弃 footer 选择;影响为观感层面,复制早已完成),历史区三击的变化值得在描述中补一句(说明 1), 本次批准取代机器人早前针对旧 head、因该 Critical 提出的 changes-requested 状态。本次运行的沙箱 批准,锁定在被审查的 commit 上。✅ — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
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
Protocol-level precondition confirmed on the PR build: the app requests Evidence — before vs afterIdentical drag, identical fixture, only the build differs.
Extra probes on the PR build, beyond the PR's own test plan:
Statusline selected — baseline (left, dead zone) vs this PR (right):
Finding 1 — the stale footer highlight is not cleared on a footer content changeThe PR's test plan step 5 and Repro — statusline is
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:
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 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);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
Not covered
RecommendationApprove 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 构建上确认了协议层前提:应用确实发出 证据 —— 修复前后对比相同拖拽、相同 fixture,只有构建版本不同。
超出 PR 自身 test plan 的额外探针(均在 PR 构建上):
截图见上方英文部分(基线 vs 本 PR、跨区域钳制、CJK、历史区、窄屏)。 Finding 1 —— footer 内容变化时过期高亮未被清除PR test plan 第 5 条与 复现:状态栏配置为 根因 —— 控制器判断正确,丢失的是重绘。 我在本分支的插桩构建中记录了帧订阅回调。在承载新 footer 文本的那一帧上,日志完全符合预期:
也就是说失效判定是对的,只有重绘被丢弃。这也解释了为什么此前没被发现:其他失效触发路径(滚动、resize、流式输出)之后都还会继续渲染,顺带把画面刷对;而 已验证的修复方案。 把清除操作挪出帧回调即可 —— 同一构建、同一场景,仅用一个环境变量切换两条路径: - clearSelection();
+ // setSelection() 会请求重绘;在 publishFrame() 内部发出会重入渲染流程,
+ // 该次重绘会被丢弃。
+ setImmediate(clearSelection);对应的回归测试需要在渲染层断言(例如断言发布了一帧且高亮已消失),因为当前控制器层的断言本来就能通过。 本分支上的仓库检查
未覆盖范围
建议方案本身建议通过 —— 区域模型清晰,我没能破坏其排除逻辑。希望在合并前修掉 Finding 1:PR 与新增设计文档都承诺了该行为,而当前结果是高亮停留在从未被复制过的文本上。其余部分均无问题。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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)
| expect(containerRef.current).not.toBeNull(); | ||
| unmount(); |
There was a problem hiding this comment.
[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 (DefaultAppLayout → MainContent/Composer → Footer) 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 的矩形。其余传递链(DefaultAppLayout → MainContent/Composer → Footer)也仍然没有任何端到端覆盖(周边测试都把相邻组件 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)
| listener!(makeTwoLineFrame('hullo', 'status')); | ||
| expect(setSelection).not.toHaveBeenCalled(); |
There was a problem hiding this comment.
[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.
| 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)
| if (activeRectIndexRef.current === 0) { | ||
| clearSelection(); | ||
| } |
There was a problem hiding this comment.
[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)
| const row = frame?.cells[y]; | ||
| if (!row) { | ||
| if (!row?.[x]?.selectable) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
[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/DiffRenderer 以 selectable={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) { |
There was a problem hiding this comment.
[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)
| !/\s$/u.test(rowText) && | ||
| !/^\s/u.test(cell.value) |
There was a problem hiding this comment.
[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 !== '' && |
There was a problem hiding this comment.
[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)
| const rectIndex = point | ||
| ? rects.findIndex((rect) => pointInViewport(point, rect)) | ||
| : -1; | ||
| if (!point || rectIndex < 0) { | ||
| clearSelection(); |
There was a problem hiding this comment.
[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(); |
There was a problem hiding this comment.
[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.
| 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)
|
@qwen-code /resolve |
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge summary: PR #8329 ← origin/mainRoot causeMain's #8739 ( Textual AND semanticFive textual conflicts in
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()
// 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;
}
What is load-bearing
Not verified hereNo 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 中文说明冲突根源是 main 上的 #8739(双击按词、三击按行拖拽扩展)与本 PR 的页脚多选区改动同时重写了 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally; 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)
| containerRef, | ||
| ); | ||
|
|
||
| expect(containerRef.current).not.toBeNull(); |
There was a problem hiding this comment.
[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.
| 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)
| } else if (rowText.length > 0) { | ||
| skippedLayoutGap = true; |
There was a problem hiding this comment.
[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)
| getAdditionalSelectableRects={() => | ||
| footerRef?.current | ||
| ? [measureElementPosition(footerRef.current)] | ||
| : [] | ||
| } |
There was a problem hiding this comment.
[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.tsx 把 TextSelectionController 桩成 () => null,Composer.test.tsx mock 掉 Footer(丢弃 containerRef),DefaultAppLayout.test.tsx 同时 mock MainContent 与 Composer,而所有控制器测试都手工注入矩形。与第 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); |
There was a problem hiding this comment.
[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)
| expect(footerWrites.length).toBeGreaterThan(0); | ||
| expect(footerWrites.at(-1)).not.toContain(SELECTION_BG); |
There was a problem hiding this comment.
[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)
| !sameViewportRect(baselineRectRef.current, activeRect) || | ||
| !sameViewportContent(baselineFrameRef.current, frame, activeRect) |
There was a problem hiding this comment.
[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
left a comment
There was a problem hiding this comment.
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)
| recordBaseline(rect); | ||
| applyHighlight(); | ||
| copySelection(); |
There was a problem hiding this comment.
[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)
Maintainer verification — real terminal, BASE vs HEADI 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
Results — every value is the exact clipboard payload the app wrote
Invalidation (HEAD only — BASE has no footer selection to invalidate):
Note 1 — triple-click in the history changed too, which the description does not mention
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 ( Note 2 — footer selection is dropped by history activity, contrary to the stated invalidation scopeThe 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:
The unit test Impact is cosmetic: the copy happens at release, before any of this, so the clipboard payload is always correct — in the scroll probe Automated checks (on HEAD)
Not covered: Windows and Linux terminals, CJK/wide-glyph statuslines, and narrow/wrapped footer layouts. 中文版本维护者验证 —— 真实终端,BASE vs HEAD我在本地重新构建了这个 PR,用真实 pty 跑打包后的 CLI,注入原始 SGR 鼠标序列,并与 merge base 的构建做对照。功能与描述完全一致,被排除的区域确实没被波及,历史区行为未变。下面有两条次要行为说明,都不阻塞合并。 结论:可以合并。 ✅ 验证方法
结果 —— 表中每个值都是应用真正写入剪贴板的内容
失效逻辑(仅 HEAD,BASE 根本产生不了 footer 选择):
说明 1 —— 历史区的三击行为也变了,PR 描述里没有提到
就这个场景而言 HEAD 明显更好。需要明确指出的权衡是:三击不再跨布局间隙覆盖整个视觉行 —— 在中间含有不可选择间隙的行上,现在只会选中光标所在的那一段。这看起来是有意为之( 说明 2 —— footer 选择会被历史区活动清除,与所声明的失效范围不符描述里写的是失效逻辑"只观察该区域的布局和内容",代码注释也写着 footer 选择"位于可滚动视口之外,保持有效"。HEAD 上的实测:
单测 影响是观感层面的:复制发生在释放时,早于上述任何一步,所以剪贴板内容始终正确 —— 滚动那组探针里 自动化检查(HEAD 上)
未覆盖:Windows 与 Linux 终端、CJK/宽字形状态栏、以及窄屏/换行的 footer 布局。 |
|
@qwen-code /triage |
|
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 reportPR 8329 Deep Verification (follow-up round) — fix(cli): enable footer text selection in VP modeVerdict: 中文 — 结论与摘要结论: 本轮为跟进轮:上一轮验证的头是
Previous-finding status (follow-up round)
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 proofCentral claim (unchanged): in Virtualized History mode the footer/statusline is a A/B 1 — controller level (head tests vs base controller)HEAD's test files were copied verbatim into a
The 13 base reds, with their failing assertions:
Note the direction controls: A/B 2 — real TUI (bundled CLI under a pty, fake
|
| 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
- Enable VP + visible statusline — ✅ pty run, default settings + command statusline.
- Drag across statusline in either direction, exact copy — ✅ both directions, exact 26-byte payload on head; dead zone on base.
- 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).
- Composer drag does not start a copy — ✅ zero copies on either arm.
- 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^2returns onlyde895593(shallow boundary truncates silently), while the metadata lists six commits (two prior-round-verified, two upstream merges,964c72a9,de895593). Everything above verifies the aggregateHEAD^1..HEADdiff; 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
HOMEenv breakingloadSettings'realpathSync(resolvedHomeDir), plus a missing<cwd>/tmpthe same boot path lstat's). The crash site ispackages/cli/src/config/settings.tsboot plumbing, which the PR does not touch (git diff HEAD^1..HEADhas 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
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Released in v0.21.11. |




















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
Evidence (Before & After)
Tipscopied exactly (4 bytes)Tipscopied exactly (4 bytes)ISSUE8131_STATUSLINEcopied exactly (20 bytes)ISSUE8131_STATUSLINEcopied exactly (20 bytes)COMPOSER_PROBEAutomated 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
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
pbcopyprobe for exact clipboard payload verification.Risk & Scope
Linked Issues
Fixes #8131
中文说明
本 PR 做了什么
本 PR 扩展了 Virtualized History 模式下的应用级文本选择,使 footer/statusline 与历史视口一样成为可选择区域。拖拽会始终限制在起始区域内,选择失效逻辑也只观察该区域的布局和内容。Composer、滚动条、对话框及其他控件仍然不属于新增的 footer 选择目标。
同时新增了 footer 选择、排除区域和 footer 内容变化后清除选择的回归覆盖,并记录了更新后的选择区域设计。
为什么需要
Virtualized History 会启用终端级 SGR 鼠标跟踪,使终端无法在屏幕任何位置提供原生拖拽选择。此前 Qwen Code 的应用级选择只接受历史视口内的按下事件,因此 footer/statusline 落入死区:终端原生选择和应用级选择都无法复制其中的模型、分支、工作目录、上下文用量或自定义状态栏输出。
Reviewer Test Plan
如何验证
证据(修复前后)
Tips(4 bytes)Tips(4 bytes)ISSUE8131_STATUSLINE(20 bytes)ISSUE8131_STATUSLINE(20 bytes)COMPOSER_PROBE基于 rebase 后分支的自动验证:86 个定向选择/布局/组件测试通过,全 workspace 类型检查通过,仓库构建与 bundle 成功完成,改动文件通过 ESLint 和 Prettier 检查。
测试平台
环境(可选)
Apple Silicon macOS、Node.js 24、tmux 3.7b、100×30 终端,启用 Virtualized History 和鼠标跟踪,使用本地 bundle CLI 及 fake
pbcopy探针精确验证剪贴板 payload。风险与范围
关联 Issue
Fixes #8131