Repository navigation
feat(web-shell): Improve split-view session navigation - #11250
Conversation
E2E test reportVerified on macOS with Node 22.22.2 and Chromium. Browser daemon traffic used the existing deterministic mock fixture; the globally installed and locally bundled CLIs served the real Web Shell assets from isolated runtimes. No live model prompts or real user-session mutations were sent.
The new browser cases confirm title details appear below the title and stay within the Web Shell root, hover preserves composer focus and active-pane selection, header action buttons do not trigger the popup, active styling is applied, drafts survive pane controls, and equal-width layout is preserved. Toolbar and pane-header heights remain 51px / 43px. Hidden tool approvals and user questions stay counted, navigation reveals and focuses each target, and permission-request logs remain empty throughout navigation. Resolving or closing a waiting pane updates the count. Final browser command, from PLAYWRIGHT_PORT=5279 PLAYWRIGHT_BASE_URL=http://127.0.0.1:55171 npx playwright test client/e2e/web-shell.split-persist.spec.ts --project=chromium --output /private/tmp/qwen-split-usability-verified/playwright-bundleThe isolated daemon was stopped after verification. Windows, Linux, other browser engines, and physical multi-monitor movement were not tested locally. This report verifies the Web Shell UI against mock daemon events, not model execution or daemon permission enforcement. 中文测试报告在 macOS、Node 22.22.2 和 Chromium 上验证。浏览器 daemon 请求使用现有确定性模拟场景,全局安装及本地打包 CLI 通过隔离运行目录提供真实 Web Shell 资源,没有发送真实模型请求或修改用户会话。 基线持久化测试 2/2、Hover/布局探测 1/1 通过。更新后的浏览器测试在 Vite 和最终 CLI 打包产物上分别 4/4 通过;最终一次耗时 7.6 秒。相关单元测试 175/175 通过,全仓构建、类型检查、打包及改动文件的 ESLint、Prettier、diff 检查通过。 浏览器确认标题下方显示详情且不越过 Web Shell 边界,Hover 保持输入焦点和活动窗格,操作按钮不触发浮窗,活动样式生效,窗格操作保留草稿及等宽布局。工具栏/窗格头部维持基线的 51px / 43px。隐藏的工具审批和用户问题仍被计数,定位会显示目标并聚焦,导航本身不发送审批请求,解决请求或关闭窗格后数量更新。 验证后已关闭隔离 daemon。未在本地测试 Windows、Linux、其他浏览器引擎或物理多显示器移动。此报告验证模拟 daemon 事件下的 Web Shell 交互,不涉及模型执行或 daemon 权限强制。 |
CI fix and verificationPushed 68f017b8d7 to route the actual PR browser job to GitHub hosted Ubuntu. It retains the existing hosted 20-minute job limit, document performance budget, smoke assertions, triggers, artifact upload, and other jobs' routing. Local validation passed: 71 CI regression tests (2 existing skips), full build, typecheck, bundle, actionlint, changed-file ESLint and Prettier. An independent parsed-YAML comparison confirms only this job's runner and job timeout changed semantically. The broad triage helper suite has 42 Linux-shell failures on this macOS machine; all 42 were independently reproduced on an isolated pre-fix HEAD baseline with matching names and assertion fields, including absent Linux absolute tool paths and The new PR CI run completed successfully on Investigation before the routing fixThe evidence below was collected on
Final original-PR retry remains red: attempt 4 spent 29 minutes installing/building dependencies, then stopped at the preceding transcript-document gate. Its maximum-document operation took 72,962.89 ms against the unchanged 60,000 ms budget; the other four document tests passed. The smoke command was therefore skipped. This gate exercises the standalone transcript renderer using a locally built bundle served through an intercepted URL. Its runtime graph excludes the split pane components; shared translation data includes the added split-view labels. The same test passed on the two earlier ECS attempts and on hosted, with total test-case durations of 27.62, 22.38, and 21.89 seconds respectively, versus 80.70 seconds this time. These total test-case durations are distinct from the inner operation timer. This is a different failure from the earlier network interruptions, and the runtime difference does not establish a specific CPU, disk, or other host bottleneck. These historical checks remain failed in the old run. The new PR run linked above verifies the targeted runner-routing fix; the earlier hosted pass is independent validation of the unchanged application code. |
|
@qwen-code /review |
|
Qwen Code review request accepted. Review is running in workflow run. A command-triggered review is not listed under the checks of this PR; the result is posted here as a review when it finishes. |
Review fixes and E2E verificationThe review fixes keep navigation outside approval keyboard handlers, respect the host details allowlist, coordinate the outer-session notice, expose pending/current state to assistive technology, preserve neighbour selection/focus, and constrain details to both the pane and embedded Web Shell root. All three new browser regressions carry The test engineer reproduced six unsafe keyboard cases against the previous implementation: tool Escape/Enter denied requests, 2/3 selected permanent user/project permission, and question Escape/Ctrl+Enter rejected/submitted. All six now pass against the final Local validation: 178 focused component tests, 55 App split/approval tests, and 5 Chromium split regressions passed. Full repository build, typecheck, bundle, and changed-file ESLint/Prettier passed. These browser tests use deterministic mock-daemon sessions; no live model or real-user data is involved. Before committing, reverse-audit pass 1 found a controlled-add selection race masked by composer autofocus; pass 2 found the partially clipped last-pane popup escaping the embedded root. Both received failing regression witnesses and fixes. Passes 3 and 4 were consecutive clean full-diff audits. Independent code review found no actionable issues and additionally passed three built-CLI Chromium cases closing the first, middle and last maximized pane. Eight deliberately broken variants were caught: approval focus, disappearing-button focus, boundary scope, active CSS, real active attribute, live running state, timestamp precedence, and duplicate outer notice. The source was restored after every mutation. Screenshots in the PR body are hosted on the 中文说明本轮修复包括:导航焦点避开审批快捷键范围、遵循宿主详情白名单、协调主会话提示、为辅助技术暴露待处理数量和当前窗格、保留邻居选中与焦点,以及同时约束浮窗到窗格和嵌入根容器。新增三个浏览器用例均加入 测试人员先复现了 6 组危险键盘操作:工具 Escape/Enter 拒绝请求,2/3 选择用户/项目永久授权,问题 Escape/Ctrl+Enter 拒绝/提交。最终 本地验证:178 个组件测试、55 个 App 分屏/审批测试、5 个 Chromium 分屏回归全部通过;全仓 build、typecheck、bundle 和改动文件 ESLint/Prettier 通过。浏览器测试使用确定性的 mock daemon,不涉及真实模型和用户数据。 代码提交前,第 1 轮反向审计发现被输入框自动聚焦掩盖的受控新增窗格状态竞争,第 2 轮发现被裁切的末尾窗格浮窗仍会越过嵌入根容器。两者均有先失败的回归证据并已修复。第 3、4 轮完整审计连续无发现。独立代码审阅未发现可行动问题,并额外在构建后的 CLI 上验证关闭首个、中间和末尾最大化窗格的 3 个 Chromium 场景,全部通过。另外 8 组故意恢复缺陷的变体均被对应断言检出,每次变异后均恢复源码。 PR 描述的配图托管在 |
Round-2 review fixes and verificationThis revision keeps the main-session approval notice available until a pane actually reports its approval, advances from an idle pane in row order, includes the visible pending count in the accessible name, and uses the workspace display name consistently. It also shares the sidebar default allowlist, strengthens the running-state and placement regressions, constructs a typed permission fixture, and restores the retained CI recovery steps’ safety-test coverage. The short pane/root boundary calculation stays inline; both boundaries have real-browser negative witnesses. The test engineer reproduced four user-visible issues against the reviewed revision and verified all four against the rebuilt Validation: 953 App/component tests passed; 15/15 Chromium cases passed (all five split regressions repeated three times with zero retries); CI contract suites passed 71 cases with two platform skips. Full repository build, typecheck and bundle, final Web Shell and standalone E2E typechecking, and changed-file ESLint/Prettier passed. Twelve deliberately broken variants were rejected by the relevant assertions, including both real collision boundaries and the CI worktree traversal guard. One intermediate 14/15 browser run was invalid: I edited a watched test to fix lint while Vite was running. The engineer traced the failure video to a full-page reload before hover, then captured the same test-file-triggered Before committing, two consecutive open-ended full-diff reverse audits were clean. Independent review: No findings; explicitly rechecked both current Criticals and the original approval-keyboard issue. Commit: 中文说明本轮保持主会话审批提示可见,直到窗格实际报告审批;从空闲窗格按横排顺序定位下一个待处理会话;无障碍名称包含可见数量;工作区头部和详情使用一致的显示名。同时统一侧栏默认白名单、加强运行状态和浮窗方向回归测试、使用有类型的审批夹具,并恢复 CI 保留清理步骤的安全测试保护。简短的窗格/根容器边界计算继续内联,两层边界均有真实浏览器反向验证。 独立测试人员复现四项可见问题,并使用重新构建的 验证结果:953 项 App/组件测试通过;五个 Chromium 分屏用例各重复三次、无重试,15/15 通过;CI 契约测试 71 项通过、2 项平台跳过。全仓 build、typecheck、bundle、最终 Web Shell 与 E2E 独立类型检查以及改动文件 ESLint/Prettier 通过。12 组故意破坏实现的变体均被相应断言检出,包括真实碰撞边界和 CI worktree 路径穿越保护。 一次中间的 14/15 浏览器运行失效:我在 Vite 验证过程中修改了被监听的测试文件以修正 lint。独立测试人员从录像确定整页重载发生在悬停之前,又捕获了同一测试文件触发的 提交前连续两轮完整差异反向审计均无发现。独立审查:无发现;重新核对本轮两个 Critical 和原有审批快捷键问题。提交: |
Review-fix verification —
|
| Verification | Result |
|---|---|
| App and related component/visibility suites | 959 passed |
| Chromium split regression suite, retries disabled | 5/5 passed |
| Independent App/report lifecycle probes | 3/3 passed |
Independent browsers using node dist/cli.js |
2/2 passed |
| Deliberately broken variants | 7/7 detected |
| Repository build, typecheck and bundle | Passed |
| Changed-file lint and formatting | Passed |
The independent engineer first tried the globally installed qwen CLI; that installed version has no pending-navigation path. The performance witness therefore uses the actual App module and the exact SplitView reporting effects in an isolated test fixture. With the outer session idle or already waiting, three foreign-pane transitions produced [1, 1, 1] extra App calls before and [0, 0, 0] after, with identical DOM. An explicit parent rerender serves as a positive control. Outer-session changes, callback replacement, and split exit/re-entry preserve correct notice membership.
The two built-CLI browser cases check that a failed pane attachment keeps the outer notice usable, and that a successful pane report hides the duplicate notice until the approval-bearing pane closes. Returning through that notice shows the real outer approval and sends no approval response. The five committed browser regressions retain checks for drafts, title hover, safe keyboard navigation, add/close/maximize, persistence, and embedded containment. Source files stayed frozen throughout browser verification; all runs used zero retries.
Negative checks deliberately removed report memoization, restored raw array state, restored update-time clearing, removed the creation-time fallback, gated reporting on details, keyed the mock identity on metadata, and added a new default action. Each failed its intended assertion; the render-loop mutant is bounded to fail promptly rather than hang.
Platform: macOS, Node 22.22.2, Chromium. Internal lifecycle probes and mock-daemon browser checks are separate evidence; physical multi-monitor movement and other browser engines were not tested. The PR description retains the three immutable screenshots hosted on the wenshao fork assets branch.
中文
待处理状态上报已避免整套界面的多余渲染,并保持审批提示的正确生命周期。本轮 5 条建议均已处理;提交前完成连续两轮完整差异反向审计及独立代码复核。
959 个单元测试、5 个 Chromium 分屏用例、3 个独立内部探针、2 个构建后 CLI 浏览器用例全部通过;7 组故意破坏实现的变体全部被检出。全仓 build、typecheck、bundle 和改动文件 lint/格式检查通过。
独立工程师先尝试全局 qwen,确认已安装版本没有新增待处理导航链路,随后使用真实 App 与实际 SplitView effect 进行内部复现。外层空闲及待处理两种情况下,连续三次其他窗格变化引起的额外 App 调用从 [1,1,1] 降至 [0,0,0],DOM 一致;主会话切换、回调更换和退出重入状态均正确。
构建后浏览器验证确认:窗格连接失败时提示仍可用;正常上报后隐藏重复提示,关闭承载审批的窗格后恢复提示,返回即可看到真实审批,全程未提交审批。分屏浏览器回归继续覆盖草稿、悬停、安全键盘导航、添加/关闭/最大化、恢复和嵌入边界。浏览器验证期间文件冻结,所有运行均无重试。
本机平台为 macOS、Node 22.22.2、Chromium。内部探针与 mock daemon 浏览器测试分别记录;未测试实体多显示器移动和其他浏览器内核。PR 描述保留 wenshao fork assets 分支的三张固定提交配图。
|
Verification after integrating main ( The conflict resolution preserves both the upstream split-bootstrap settlement guard and this PR's approval-report callback. This review follow-up adds the session-switch regression assertions and clarifies the callback contract; production behavior is unchanged.
The built-CLI cases confirm that a failed main pane cannot hide the outer approval notice, and that a healthy pane's report hides the duplicate notice until that pane closes. Following the restored notice reaches the approval with zero permission submissions. The session-switch probes confirm automatic refresh without changing pane pending membership or manually calling the report. The exact preceding complete shell suite passed 765/765 under each of two deliberate regressions: removing the session dependency, and using a ref updated every render with fixed callback identity. The updated tracked test passes intact and fails at its new callback-identity assertion under both regressions. The ref variant reads the new ID correctly when called; what it loses is automatic re-reporting on a session change. Same-session identity and foreign-pane render-count assertions remain green. Two consecutive full-diff reverse-audit passes and an independent review found no actionable issues. The old Critical findings were rechecked against the current code. Tests ran with client source/tests frozen and no concurrent build. Browser transport was mocked; these results do not claim physical multi-monitor or non-Chromium coverage. New-commit CI is separate and is reported by GitHub checks. 中文同步主分支( 冲突解决同时保留了主分支的分屏恢复就绪检查和本 PR 的审批上报回调。本轮评论处理补充会话切换回归断言并澄清回调约定,生产逻辑未改变。
构建后 CLI 用例确认:主窗格连接失败不会隐藏外层审批提示;健康窗格实际上报后隐藏重复提示,关闭它后恢复提示;点击恢复的提示可返回审批,整个流程没有提交审批。会话切换探针确认,不改变窗格待处理集合、不手动调用报告时也能自动刷新。 上一精确提交的完整界面测试在两种故意破坏实现的变体下均为 765/765 通过:删除会话依赖,以及每次更新 ref 但固定回调引用。当前已跟踪测试在正常实现下通过,两种变体均在新增的回调引用身份断言处失败。ref 变体被调用时能够读到新 ID,失去的是会话切换时的自动重新上报。同会话引用稳定性和其他窗格不触发额外渲染的断言继续通过。 连续两轮完整差异反向审计及独立复核均未发现可操作问题,并对照当前代码复核了旧 Critical 评论。测试期间客户端源码和测试文件保持冻结,没有并行构建。浏览器使用模拟 daemon 传输;未声称覆盖实体多显示器或其他浏览器内核。新提交的 CI 以 GitHub 检查结果为准。 |
|
Fixed the history reading-position failure from CI run 34185615052, commit Hosted CI follow-up: Qwen Code CI passed on the unchanged commit. Both hosted browser runs passed all 56 smoke cases and all five document-gate tests without retries, including CPU4 history paging. The first unit job passed every assertion but failed on one Vitest worker A boundary request could start before virtualized visible rows had mounted, so there was no saved reading row. Slow rendering then left a 286-pixel displacement after row measurement, with no anchor for the existing restore loop to correct. Paging now waits for a visible row before requesting the next page; a new interaction, view change or unmount cancels the wait. The shared virtualizer, paging assertions, timeouts and retries are unchanged. The existing 200-record case now runs with CPU slowdown. The exact previous implementation reproduced the same 286-pixel failure in three CPU4 runs. After rebuilding, an independent engineer ran the same reproduction against
Two consecutive full-diff reverse-audit passes and an independent code review completed before commit, with no actionable findings. Client source/tests and build output were frozen throughout browser verification. Local results use macOS Chromium and mocked daemon transport; new-head hosted CI is reported separately in GitHub checks. The PR description preserves all three immutable images hosted on the wenshao fork assets branch. 中文已修复 CI run 34185615052 的历史阅读位置失败,提交 Hosted CI 后续结果: 同一提交的 Qwen Code CI 已通过。两轮 hosted 浏览器检查均为 56 个 smoke、5 个文档门槛用例无重试通过,包含 CPU4 历史分页。第一轮单元测试的所有断言均通过,但因一次 Vitest worker 的 onTaskUpdate RPC 超时失败;重跑失败作业后没有该未处理错误,CLI 29,022 个、Web Shell 6,654 个测试通过。GitHub 自动重跑的下游浏览器检查也通过。处理这次基础设施重试未修改代码、断言或超时。 边界分页可能在虚拟列表可见行挂载之前启动,未保存当前阅读行。慢速渲染导致行高测量后留下 286 像素偏移,原有恢复逻辑也没有锚点可用。现在先取得可见行再请求下一页;用户再次操作、切换视图或卸载时取消等待。共享虚拟列表、分页断言、超时和重试设置保持不变,原有 200 条记录用例加入 CPU 限速。 上一精确实现在 CPU4 下连续三次复现同样的 286 像素失败。重新构建后,独立测试人员使用 node dist/cli.js 运行相同复现,连续三次通过,保留原有 2 像素要求。两个新增回归测试在当前实现下通过,仅在内存中换回上一精确生产组件后均失败,确认旧实现会在可见行出现之前发送分页请求。
提交前完成连续两轮完整差异反向审计及独立代码复核,无可操作发现。浏览器验证全程冻结客户端源码、测试和构建产物。本地结果来自 macOS Chromium 与 mock daemon;新提交的 hosted CI 以下方 GitHub 检查为准。PR 描述保留了 wenshao fork assets 分支托管的三张固定提交配图。 |
yiliang114
left a comment
There was a problem hiding this comment.
[P1] Please split the independent history and CI changes from the split-view feature.
The history fix itself matches the failure I independently reproduced from #11282: pagination can start before the virtual rows mount, so capture() returns no reading anchor; after the prepend, the virtualizer positions the old row using the 19,200 px estimate and later measures it at 18,914 px, leaving the stable 286 px drift seen in CI. Waiting for a visible anchor before viewport.load() addresses the actual race, and the unit plus CPU-throttled browser coverage is useful.
That fix is independent of split-view navigation, as is moving the Web Shell browser job to hosted runners. The current PR is now 23 files and +1,319/-88 across three separately reviewable changes: split-view UX, CI runner routing, and the history-anchor bugfix. The latter was reproduced on an unrelated PR because it already exists on main; it should not need this feature branch to land. Could we move 0de61b5 into a focused fix(web-shell) PR and handle the runner change separately, leaving #11250 scoped to split-view behavior?
yiliang114
left a comment
There was a problem hiding this comment.
Review at head 7c392d2c — no findings. Verification of the two claims this PR most needs to hold
Comment rather than approve: this head is a merge of main landed at 09:13Z and every substantive lane is still in_progress, so there is no CI evidence to approve against yet. What follows is what I read at head.
The keyboard-isolation claim holds, and the code states the invariant itself
The description's strongest claim is that after toolbar navigation, "Enter, Escape, and digits immediately afterward cannot submit an answer". I traced it end to end rather than accepting it, because if it were wrong a user could approve a tool by navigating:
SplitView.tsx:388-392goToPendingPanepicks the next pending pane;:402-404resolves the focus target bydata-pane-session-idand scrolls it into view; the target is the pane slot at:551-555—role="group",tabIndex={-1}— which is an ancestor ofChatPane, not the approval panel.:405-406documents the rule verbatim: "Approval panels submit on Escape, Enter, and digits. Navigation must stop outside those keyboard scopes until the user deliberately enters."- Both approval surfaces scope their handling to the panel, not to
document/window:ToolApproval.tsx:487onKeyDown={handleKeyDown}with the intent recorded at:403("Keyboard handling is scoped to the panel (onKeyDown), so it only fires while focus is inside this approval"), andAskUserQuestion.tsx:668likewise. With focus on the ancestor slot, a keydown does not reach either handler. - The isolation is therefore structural (focus scope), not a suppression flag or a time window. That matters: there is no flag that can fail to be cleared (leaving later approvals dead) or be cleared early (letting a stray Enter through).
keyboardActiveatToolApproval.tsx:371/AskUserQuestion.tsx:589only governs auto-focusing a safe default, not whetheronKeyDownruns, so Tab/click into the panel still works normally. SplitView.tsxhas exactly twodocument-level keydown listeners,:273and:437; neither calls a confirm or submit path.
R3-2 (round 4's only finding) is load-bearing and correct — with corrected line numbers
The ledger anchors it at App.tsx:7958; after this head's merge of main that region is an unrelated shadow-surface focus trap. The callback is now at App.tsx:8062-8066:
const handleSplitPendingPanesChange = useCallback(
(ids: string[]) =>
setOuterSplitPanePending(ids.includes(connection.sessionId ?? '')),
[connection.sessionId],
);
The [connection.sessionId] dependency does the work the finding asked about: when the main session changes the callback identity changes, and SplitView.tsx:208-211 pairs a report effect with useEffect(() => () => onPendingPanesChange?.([]), [onPendingPanesChange]). React runs the cleanup before the re-report within the same commit, so the terminal state is evaluated against the new main session — the notice follows it, as the description claims.
One thing I checked and am recording as not a finding
Pane-level pending reporting (ChatPane.tsx:573-578) uses an approvalActive derived from extractPendingPermission(blocks) at :549-572 with no canActOnPendingApproval gate, while App.tsx:6628-6629 derives approvalOverlayActive from a gated value. That looks like two different notions of "pending" for one session, and it is worth a moment's suspicion — but canActOnPendingApproval is defined at App.tsx:6607-6609 as !(connection.catchingUp && sidebarSwitchingSessionId !== null), i.e. a transient guard for the outer surface's single connection being repurposed mid sidebar-switch. It is not an ownership or shared-session check. A split pane has its own connection and renders its own approval panel ungated, so ChatPane reporting exactly what it renders is self-consistent, and the outer notice being suppressed during catch-up is the guard doing its job. No defect. Writing it down so the next reviewer does not spend the same time.
The CI reroute
.github/workflows/ci.yml moves web_shell_e2e_smoke from the shared-pool expression to runs-on: 'ubuntu-latest' with a flat timeout-minutes: 20, and keeps all three self-hosted recovery steps (workspace ownership, stale .qwen sweep, disk floor gate behind runner.environment == 'self-hosted') so a future reroute is a one-line change. Two details I want to credit explicitly:
scripts/tests/review-worktree-cleanup-workflow.test.jswidens its selection to also match any job carrying theClean stale .qwen before checkoutstep, not just pool-routed jobs — so moving this job to hosted cannot silently drop its sweep out of coverage. That is the right direction for a test that exists to catch reroutes.- The deleted
keeps web_shell_e2e_smoke above its build-plus-browser budget on ECStest carried measured evidence (hk3-9npm ci19m46s of a 20m budget; hk4-23 install 10m33s vs 4m53s on hk5-2). The replacement pinsruns-on == 'ubuntu-latest'plus 20 everywhere, so a reroute back to the pool now fails loudly instead of quietly restoring a flat timeout. The measurements themselves are only in git history now; if the pool is expected to come back, that context is worth preserving in the design doc this PR adds.
Cross-PR observation, flagged as inference
web-shell.history-viewport.spec.ts gains CPU throttling (Emulation.setCPUThrottlingRate: 4) for the 200-record case only, and 0de61b5c4 is "fix(web-shell): Preserve history anchors during slow rendering". On PR #11152 the web-shell Browser Regression lane is red at two consecutive heads on exactly this spec's 200-record case (expect(received).toBeLessThanOrEqual(expected), 3/3 attempts), and I attributed it to main-side #11208/#11323 rather than to that PR. I did not run either, so this is an observation and not a claim: if the slow-render paging race is what that lane is hitting, the fix lives here, which would confirm the attribution and mean #11152 should not be charged for it. Worth saying out loud in whichever PR lands second.
Scope
Three areas travel together: the split-view UX feature, the CI runner reroute, and the history-paging fix. Normally I would flag config/CI changes bundled with application code, but the reroute is motivated by this PR's own browser gates repeatedly losing resources on the shared pool, and the paging fix is in the same package and the same e2e spec. Defensible as one change; noting it only because the reroute has repo-wide blast radius (it decides where every PR's browser gate runs) and a reader of the diff deserves to see it called out rather than discover it.
State
reviewDecision is CHANGES_REQUESTED from ci-bot reviews anchored at 5fb20297b (09-07T09:59Z) and e5202ac3a (09-07T15:18Z) — many heads back. The latest ledger is round 4 at 9098dd333 with floor: "o", no Critical and one Suggestion (R3-2, verified above). 37/37 threads resolved. I will re-check once Lint & Static, Test (ubuntu-latest), Integration Tests (no-AK) and the web-shell lanes report on this head.
|
I extracted the WebShell history-anchor race fix into focused PR #11366, preserving the original commit author. It contains only the four history viewport source/test files and intentionally excludes the CI runner and split-view changes. Please drop commit |
yiliang114
left a comment
There was a problem hiding this comment.
Approving at head 7c392d2c — the one red lane is main's, proven by an identical failure on the base
In pullrequestreview-5139858213 I recorded no findings and held the vote only because the merge of main at 09:13Z had every substantive lane still running. They have now reported.
Result: Lint & Static, Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke, Capture web-shell visuals, Desktop Shell (ubuntu-22.04 and windows-2022), Classify PR, assign, label, route ×2 and Remind on force-push are success. Test (ubuntu-latest, Node 22.x) is failure.
That failure is not this PR's, and I verified it rather than assuming it from file-disjointness:
- At this head:
src/serve/capabilities-docs-contract.test.ts > conditional serve capability documentation > keeps the daemon index capability counts in sync,AssertionError: expected 158 to be 159 // Object.is equality,Tests 1 failed | 29102 passed | 90 skipped (29193). - At
7dedcbfd71— which is main, and is this PR's base: the same job isfailure(09:25:20Z -> 09:45:55Z) with the same test, same assertion, same tallies:expected 158 to be 159,1 failed | 29102 passed | 90 skipped (29193).
Identical failure on the base and on the head, in a file this PR does not touch (its 23 files are web-shell client, .github/workflows/ci.yml, three scripts/tests/*ci* pins, one e2e spec and one design doc — nothing under src/serve/ and no daemon capability docs). So this PR inherits it.
Main is red and someone should own it separately. docs/developers/daemon/00-index.md reads 158 on main while the registry the test compares against has 159, i.e. a capability was added to the registry without the index doc's count being bumped. Every PR that merges main from 7dedcbfd71 onward will show a red Test (ubuntu-latest) until that is reconciled, and it will keep costing reviewers this same attribution pass. Worth its own fix rather than riding along here.
My code position is unchanged from the earlier review and I am not restating it at length: the keyboard-isolation claim holds structurally (focus scope, not a suppression flag), R3-2's [connection.sessionId] dependency is load-bearing and correct at App.tsx:8062-8066, the CI reroute keeps its recovery steps and widens review-worktree-cleanup-workflow.test.js so the reroute cannot silently drop coverage, and the one thing I suspected — pane-level pending reporting bypassing canActOnPendingApproval — is not a defect once that flag's actual definition is read.
To be explicit about what this vote does not cover: it does not certify Test (ubuntu-latest) green, because it is not. It certifies that the failure is main's and that nothing in this diff contributes to it.
reviewDecision is still CHANGES_REQUESTED from ci-bot reviews anchored at 5fb20297b and e5202ac3a, both many heads back and both predating the round-4 ledger that found no Critical. Those need dismissing or a fresh round; main wants two approvals and @wenshao cannot supply one as author.
|
@qwen-code /triage |
qqqys
left a comment
There was a problem hiding this comment.
Read-only review pass at head 52484fec. I re-verified all three recorded Criticals against the code at this exact head and scanned the substantive production logic. All three are fixed, I found no Critical of my own, and CI is green with nothing failing or pending, so this is an approval.
Recorded blocking findings — all verified fixed at this head
- R1-1 (
SplitView.tsx— the "go to next session awaiting input" cycle moved DOM focus onto an option button inside an approval panel whose panel-wide key handler submits a response unconditionally, so Enter/Escape/a digit right after navigating could answer a request the user never chose to answer). Navigation now stops outside every approval keyboard scope. The focus effect resolves the pane container bydata-pane-session-idand focuses that (SplitView.tsx:399-409), the container is genuinely focusable because the pane wrapper carriestabIndex={-1}(:555) besidedata-pane-session-id={sessionId}(:551), and the invariant is written at the site: "Approval panels submit on Escape, Enter, and digits. Navigation must stop outside those keyboard scopes until the user deliberately enters."scrollIntoView({ block: 'nearest', inline: 'nearest' })handles the reveal,setPaneFocusId(null)makes the effect one-shot, and the related focus hand-offs are covered too — a focused pending button that disappears passes focus to Back (:213-219). The@smoketestnavigates hidden tool and question approvals without answering thempins it end to end: it pressesEnter,2,3andEscapeafter navigating and asserts nothing was answered, withexpect(daemon.permissionRequests()).toHaveLength(0)as the backstop. - R2-1 (the
@smokecontainment test sampled the popover's box once, before floating-ui's collision shift had finished, so it asserted against a mid-transition position). The containment check is now a polling assertion —expect.poll(async () => { …boundingBox()… }).toBe(true)over the details dialog against both the pane and the shell root (web-shell.split-persist.spec.ts:461-477) — so it waits for the settled position instead of racing the transition. - R2-2 (the
@smoketest pressedEscapeonce and asserted the tiled row returned, without accounting for a popover its own previous interaction could open). Each iteration of the containment loop now closes the popover it just opened with an explicitEscapebefore moving to the next pane (:478), and in the restore flow theEscapeat:421follows a.focus()-only sequence whose focus landing onbackis asserted first (:418-420), so no popover is open to swallow the keypress.
Critical-only scan of the production diff — no blocking issue found
SplitView.tsx(+144/-2). The active-pane derivation falls back to a neighbour on close by clamping the previous index into the new array, and the sync effect only runs when thepaneIdsidentity actually changed and only takes focus whendocument.activeElement === document.body, so it cannot steal focus from a user mid-interaction.pendingIdsis memoized to keep the report identity stable (the comment names the loop it prevents), the unmount cleanup reports an empty pending set, andhandleApprovalChangereturns the current set unchanged when the value already matches, which is what keeps an approval change in another pane from re-rendering the whole shell.ChatPane.tsx(+40/-3). The approval report is effect-driven with a cleanup that reportsfalseon unmount or session change (:575-578), so a pane that detaches or fails cannot leave a stale pending count behind — the property the main-session notice depends on. The active indicator is exposed asdata-pane-activeplusaria-current="location"and anaria-label, matching the accessibility claims in the description.TranscriptViewport.tsx(+32/-3). History paging now waits for a real reading row before requesting:load()retries throughrequestAnimationFrameup to eight frames, assigns the anchor only oncecapture()succeeds, and bails ifscrollIntentmoved in the meantime; a single-flight guard prevents stacking pending loads,handleScrollIntentcancels the pending frame, and auseLayoutEffectcleanup keyed onviewKeycancels it on unmount or view change. The failure direction is the safe one — a pathologically slow mount skips the page rather than loading with no anchor to restore, which is the 286-pixel jump this closes.ci.yml(+14/-22). The browser job is pinned to hostedubuntu-latestwith the same 20-minute limit it already had on hosted, the shared-pool recovery steps are deliberately retained so a future reroute is a runner change only, and no assertion, budget or trigger is relaxed; the change is confined to this job, so other jobs' routing is untouched, and the workflow-contract suites that pin these lanes were updated in the same commit.
Recorded non-blocking items I did not treat as gates. A human reviewer asked at P1 that the independent history and CI changes be split out of the feature, and then extracted the history-anchor fix into its own PR preserving the original author; this PR still carries both changes, and that reviewer approved this head anyway, so I read the request as scope preference rather than a blocker. The last two bot rounds are Suggestions-only, and their open items — the tooltip timestamp re-derivation, the pane-specific collision-boundary rule, an onSelect handler identity, and the untested surface the triage pass named — are non-blocking on my reading too.
What I did not read line by line. App.tsx (+16/-3, the wiring for showSessionDetails and the pending-pane report), SessionDetailsTooltip.tsx (+11/-4), i18n.tsx (+4), the two CSS modules, and the ~900 lines of new unit and e2e test bodies beyond the regions bearing the three Criticals. I relied on the green lanes for those, including the browser smoke on hosted at this head.
CI at this head. 15 checks pass with 28 skipped and nothing failing, cancelled or pending — the browser job included, on the hosted runner this PR routes it to. Nothing is attributable to a defect introduced by this PR, and no lane I read is a gate either way.
中文说明:本次在 head 52484fec 上评审,结论为 APPROVE。三项历史 Critical 我都对照当前代码逐条确认已修复,且自行扫描了主要生产逻辑,未发现新的 Critical。R1-1("跳到下一个待输入会话"把 DOM 焦点移到审批面板内部的选项按钮上,而该面板的全局键盘处理会无条件提交响应,因此导航后紧接着的 Enter/Escape/数字可能回答一个用户并未选择回答的请求):导航现在停在所有审批键盘作用域之外——焦点副作用按 data-pane-session-id 找到窗格容器并对其调用 focus(SplitView.tsx:399-409),该容器确实可聚焦,因为窗格外层带 tabIndex={-1}(:555)与 data-pane-session-id={sessionId}(:551),现场注释也写明了该不变量:"审批面板会在 Escape、Enter 与数字上提交,导航必须停在那些键盘作用域之外,直到用户主动进入";scrollIntoView({block:'nearest',inline:'nearest'}) 负责揭示,setPaneFocusId(null) 使该副作用只生效一次,相关的焦点交接也已覆盖——处于焦点状态的待处理按钮消失时把焦点交给 Back(:213-219)。@smoke 用例 navigates hidden tool and question approvals without answering them 端到端钉住了它:导航后依次按 Enter、2、3、Escape 并断言没有任何请求被回答,并以 expect(daemon.permissionRequests()).toHaveLength(0) 兜底。R2-1(该 @smoke 包含性用例只采样一次弹层外框,早于 floating-ui 的碰撞位移完成,因而断言的是过渡中途的位置):包含性判定现在改为轮询断言——expect.poll(async () => { …boundingBox()… }).toBe(true),同时对照窗格与外壳根元素(web-shell.split-persist.spec.ts:461-477)——等待位置稳定而不再与过渡竞争。R2-2(该 @smoke 用例只按一次 Escape 就断言平铺行已恢复,却没有考虑它自己前一步交互可能打开的弹层):包含性循环的每一轮现在都会用显式 Escape 关闭刚打开的弹层再进入下一个窗格(:478);而恢复流程中 :421 的 Escape 之前只有 .focus() 序列,且先断言焦点落在 back(:418-420),因此不存在会吞掉该按键的弹层。生产 diff 的 Critical-only 扫描未发现阻塞问题:SplitView.tsx(+144/-2) 的活跃窗格推导在关闭时以"把原索引夹入新数组"的方式回落到相邻窗格,同步副作用只在 paneIds 身份确实变化时运行、且只在 document.activeElement === document.body 时取焦,因此不会从正在交互的用户手中抢走焦点;pendingIds 经 memo 保持上报身份稳定(注释点名了它防止的循环),卸载清理会上报空集合,handleApprovalChange 在值已一致时原样返回当前集合——这正是"其他窗格的审批变化不会导致整个外壳重渲染"的实现方式。ChatPane.tsx(+40/-3) 的审批上报由副作用驱动,并在卸载或会话切换时以清理函数上报 false(:575-578),因此分离或失败的窗格不会留下陈旧的待处理计数,而主会话提示正依赖该属性;活跃指示以 data-pane-active 加 aria-current="location" 与 aria-label 暴露,与描述中的可访问性主张一致。TranscriptViewport.tsx(+32/-3) 的历史翻页现在会等到真正出现可读行才发请求:load() 通过 requestAnimationFrame 最多重试八帧,只有在 capture() 成功后才写入锚点,且期间 scrollIntent 变化即放弃;单飞守卫避免堆积多个待处理加载,handleScrollIntent 会取消待处理帧,键于 viewKey 的 useLayoutEffect 清理会在卸载或视图切换时取消它。失败方向是安全的一侧——渲染极慢时会跳过这一页,而不是在没有可恢复锚点的情况下加载,而那正是本次要闭合的 286 像素跳变。ci.yml(+14/-22) 把浏览器作业固定在托管 ubuntu-latest 上并沿用它在托管环境本就有的 20 分钟上限,刻意保留共享池的恢复步骤以便将来改回只需换 runner,且没有放宽任何断言、预算或触发条件;改动仅限于该作业,因此其他作业的路由未受影响,而钉住这些通道的 workflow 契约测试也在同一提交中更新。我未作为门禁的既有非阻塞项:一位人类评审者以 P1 建议把独立的历史与 CI 改动从本特性中拆出,并随后自行把历史锚点修复抽成独立 PR、保留原作者;本 PR 仍同时携带这两部分改动,而该评审者对本 head 依然投了批准,因此我把该请求读作范围偏好而非阻塞项。最近两轮机器人评审只有 Suggestion,其未决项——tooltip 时间戳的重复推导、窗格特定的碰撞边界规则、某个 onSelect 处理函数的身份,以及 triage 指出的未测试面——按我的读法同样非阻塞。我未逐行阅读的部分:App.tsx(+16/-3,showSessionDetails 与待处理窗格上报的接线)、SessionDetailsTooltip.tsx(+11/-4)、i18n.tsx(+4)、两个 CSS module,以及约 900 行新增单元与 e2e 测试中除三项 Critical 相关区域以外的部分;对这些我依据的是本 head 上的绿色通道,包括跑在本 PR 所路由的托管 runner 上的浏览器冒烟。CI:本 head 上 15 项通过、28 项跳过,无失败、无取消、无 Pending——其中包含浏览器作业,且正跑在本 PR 为其指定的托管 runner 上。没有可归因于本 PR 的缺陷,我读到的通道中也没有任何一项构成门禁。
Main's split-view navigation rework (#11250) made a boundary load wait for the virtualized rows to mount, retrying the anchor capture over a few frames and giving up rather than loading without a reading position to restore. This branch solved the same missing-rows problem from the other end: it hands the viewport an anchor-refresh callback that the store invokes immediately before admitting the new page, while the old rows are still mounted. Keep both. The deferred loop now passes that callback through to the boundary load, so the anchor is captured before the request starts and refreshed at the last moment it can still be observed.
…st (#11086) The test added by #11250 referenced mockUseDaemonActivePromptBridge, which was never defined in this harness (ReferenceError), and the hook it named is only called by ChatPane, which never renders under App.test.tsx's mocks, so even a defined mock could never satisfy toHaveBeenCalled(). Spy on mockUseDaemonSessionActivityBridge instead: App calls useDaemonSessionActivityBridge on every render, so its call count directly witnesses whether reporting foreign pending panes re-renders App. Mutation probe (foreign reports flipping outerSplitPanePending) turns the test red; restoring the guard returns it to green.
What this PR does
Split-view titles reuse the sidebar session-details popover, the last interacted pane has a thin header indicator, and an existing-row toolbar button cycles through panes awaiting tool approval or a user answer. Navigation reveals and focuses the named pane outside the approval keyboard handlers. Enter, Escape, and digits immediately afterward cannot submit an answer; users deliberately enter the approval controls to respond.
The title popover follows the host’s details allowlist and stays inside its pane. Pending counts and the current pane are exposed to assistive technology. Approval changes in other panes avoid redundant rendering of the whole shell. The main-session notice stays visible until a pane actually reports its approval, including when that pane is still attaching or has failed. Closing the selected pane chooses a neighbour, and a focused pending button hands focus to Back when it disappears.
The browser CI job uses GitHub hosted Ubuntu with its existing 20-minute limit. Its document performance budget, smoke assertions, triggers, and other CI jobs’ runner routing are preserved.
History paging waits for a visible reading row before requesting another page, preserving the reading position when rendering is slow. Pending capture is cancelled when the user changes intent or leaves the view. The existing 200-record browser regression now includes CPU slowdown.
Why it's needed
Large displays need quick access to session context and requests hidden behind a maximized pane. This keeps the equal-width horizontal layout, full-height histories, and existing add/close/back/maximize controls without adding a toolbar row or width controls.
The same earlier commit repeatedly lost browser resources or exceeded the document performance budget on ECS, while its document and smoke gates passed on hosted. The browser job therefore uses the verified hosted environment.
A slow-rendering history page could begin loading before any visible row was available to save, then jump by 286 pixels when row measurements settled. Acquiring the reading position first removes that timing dependency.
Reviewer Test Plan
How to verify
Evidence (Before & After)
Real Chromium screenshots using deterministic test sessions. The baseline is the globally installed CLI; the updated screenshots are from the current UI with the mock daemon. They contain no real user sessions.
Before: existing horizontal split and header rows.
After: title details while the other pane retains its draft and keyboard focus.
After: the existing toolbar shows the count even when a sibling pane is hidden by maximization.
Images are hosted in the
wenshao/qwen-codefork onassets/pr11250-split-view, using immutable commit URLs.Tested on
Environment (optional)
Current local revision: 1,182 component tests passed, including the real virtual list and all history-viewport suites. The full 56-case browser smoke suite and the separate split-exit regression passed without retries. The transcript document browser gate passed all five tests with the existing performance limit. CI routing and recovery contracts passed 71 tests with two platform skips. Whole-repository build, typecheck and bundle, changed-file lint and formatting, and standalone strict browser-test typechecking passed.
Independent verification using the built CLI passed the original 200-record history case three consecutive times under CPU slowdown and all three original history cases at natural speed. Both new regression tests pass intact and fail when only the production component is replaced in memory with its exact previous implementation. Client source, tests and build output remained frozen throughout browser verification.
The previous commit failed the hosted history reading-position check; the split cases passed. Current-commit hosted CI is reported by the checks below and is separate from local verification.
Risk & Scope
Linked Issues
None.
中文说明
此 PR 的改动
分屏标题复用侧栏会话详情浮窗,最近交互的窗格显示头部细线,现有工具栏行中的按钮可以轮流定位等待工具审批或用户回答的窗格。定位后显示目标窗格,并将焦点停在有名称的窗格外层,避开审批键盘处理范围。紧接着按 Enter、Escape 或数字键不会提交答案;用户需要主动进入审批控件后再处理。
标题浮窗遵循宿主的详情白名单,并限制在窗格内。待处理数量和当前窗格对辅助技术可见。其他窗格的审批变化不会引起整套界面的多余渲染。主会话提示会保留到窗格实际报告审批为止,避免窗格尚未连接或连接失败时隐藏唯一提示。关闭当前窗格会选择邻居;有焦点的待处理按钮消失时,焦点交给返回按钮。
浏览器 CI 使用 GitHub hosted Ubuntu,保留现有 20 分钟时限。文档性能门槛、smoke 断言、触发条件和其他 CI 作业的 runner 路由均保持不变。
历史消息分页会先取得可见阅读行,再请求下一页,保证渲染缓慢时阅读位置稳定。用户改变操作意图或离开视图时取消等待。原有 200 条记录的浏览器回归用例现在加入 CPU 限速。
为什么需要
大显示器需要快速查看会话信息,以及找到被最大化窗格隐藏的请求。此改动保留等宽横排、完整高度的历史消息和原有添加/关闭/返回/最大化操作,不增加工具栏行数或宽度控件。
此前同一个提交多次在 ECS 上遇到浏览器资源中断或超过文档性能门槛,而 hosted 环境的文档与 smoke 检查通过,因此浏览器作业改用已验证的 hosted 环境。
渲染缓慢时,历史分页可能在可见行出现之前启动,未能保存阅读位置,行高测量完成后便产生 286 像素跳动。先取得阅读位置可以消除这一时序依赖。
审阅者测试计划
如何验证
改动前后证据
以上三张图片来自真实 Chromium 和确定性的测试会话。基线是全局安装的 CLI;改动后图片来自当前 UI 与 mock daemon,不含真实用户会话。第一张展示原有横排和头部行;第二张展示标题详情,同时另一窗格保留草稿和键盘焦点;第三张展示最大化隐藏兄弟窗格时,现有工具栏仍显示待处理数量。
图片托管在
wenshao/qwen-codefork 的assets/pr11250-split-view分支,引用固定提交的 URL。测试平台
测试环境
当前本地版本:1,182 个组件测试通过,包含真实虚拟列表和全部历史消息视口测试。完整 56 个浏览器 smoke 用例及独立的退出分屏回归用例均无重试通过。文档浏览器门槛的 5 个测试在原性能限制下全部通过。CI 路由与清理契约 71 个通过、2 个平台跳过。全仓 build、typecheck、bundle、改动文件 lint/格式检查及浏览器测试的独立严格类型检查通过。
独立验证使用构建后的 CLI,原 200 条记录历史用例在 CPU 限速下连续三次通过,自然速度下原有三个历史用例也全部通过。两个新增回归测试在当前实现下通过,仅在内存中将生产组件换为上一精确版本后均失败。浏览器验证期间客户端源码、测试和构建产物保持冻结。
上一提交在 hosted 历史阅读位置检查中失败,分屏用例通过。当前提交的 hosted CI 状态以下方检查为准,与本地验证分别记录。
风险与范围
关联 Issue
无。