Skip to content

feat(web-shell): Improve split-view session navigation - #11250

Merged
yiliang114 merged 11 commits into
mainfrom
codex/web-shell-split-usability
Sep 8, 2026
Merged

yiliang114 merged 11 commits into
mainfrom
codex/web-shell-split-usability

Conversation

@wenshao

@wenshao wenshao commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Open two sessions, type a draft, then hover the other title. Details appear below it without moving focus or the active indicator. Maximize/close buttons do not trigger the popover. Disabling the details action in host configuration disables these title popovers while pending counts and navigation remain available. Sessions with only a creation timestamp still show that time.
  • Click or Tab into another pane, then maximize, restore, add, and close panes. The indicator follows interaction, closing selects a neighbour, and drafts remain intact. Reload restores only this tab’s split; leaving the split clears that restoration.
  • With a tool approval in one pane and a question in another, activate an idle pane between them and cycle forward through the waiting panes, including while maximized. Focus stays outside approval controls. Press Enter, Escape, 2/3, or question Ctrl/Cmd+Enter immediately after navigation: no permission response is sent, and Escape restores the row. Deliberately Tab/click into approval controls to respond normally.
  • Resolve the final hidden request remotely while the pending button is focused. The button disappears and focus moves to Back. Check a narrow embedded shell: title details stay inside their pane. The toolbar has the same height with and without pending requests.
  • Keep the main session awaiting approval while its split pane is still loading or fails to attach: the outer notice must remain available. Once the pane reports the approval, the duplicate notice disappears. If the main session changes while pane requests remain unchanged, the notice must follow the new main session. Check that multi-workspace display names match between header and details, and that the pending button’s accessible name includes its visible count.
  • With CPU slowdown enabled, open older history from the turn navigator and scroll across several 200-record pages in both directions. The reading row stays in place, incoming live messages do not disturb it, and returning to the latest messages remains available.
  • Confirm the PR browser job runs the document gate and the smoke suite, including the three new split-view smoke cases.

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.

Before: existing split view

After: title details while the other pane retains its draft and keyboard focus.

After: session title details and active pane

After: the existing toolbar shows the count even when a sibling pane is hidden by maximization.

After: pending navigation in maximized view

Images are hosted in the wenshao/qwen-code fork on assets/pr11250-split-view, using immutable commit URLs.

Tested on

OS Status
🍏 macOS ✅ Node 22.22.2, Chromium
🪟 Windows ⚠️ Not tested locally
🐧 Linux ⚠️ Not tested locally for this revision

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

  • Main risk or tradeoff: explicit focus transfer and popover positioning; verified with real Chromium and both tool/question approvals. History paging may wait briefly for visible rows before sending a request. Hosted browser CI uses hosted capacity.
  • Not validated / out of scope: physical multi-monitor movement, resizing, and other browser engines.
  • Breaking changes / migration notes: none; per-tab storage and session protocols are unchanged.

Linked Issues

None.

中文说明

此 PR 的改动

分屏标题复用侧栏会话详情浮窗,最近交互的窗格显示头部细线,现有工具栏行中的按钮可以轮流定位等待工具审批或用户回答的窗格。定位后显示目标窗格,并将焦点停在有名称的窗格外层,避开审批键盘处理范围。紧接着按 Enter、Escape 或数字键不会提交答案;用户需要主动进入审批控件后再处理。

标题浮窗遵循宿主的详情白名单,并限制在窗格内。待处理数量和当前窗格对辅助技术可见。其他窗格的审批变化不会引起整套界面的多余渲染。主会话提示会保留到窗格实际报告审批为止,避免窗格尚未连接或连接失败时隐藏唯一提示。关闭当前窗格会选择邻居;有焦点的待处理按钮消失时,焦点交给返回按钮。

浏览器 CI 使用 GitHub hosted Ubuntu,保留现有 20 分钟时限。文档性能门槛、smoke 断言、触发条件和其他 CI 作业的 runner 路由均保持不变。

历史消息分页会先取得可见阅读行,再请求下一页,保证渲染缓慢时阅读位置稳定。用户改变操作意图或离开视图时取消等待。原有 200 条记录的浏览器回归用例现在加入 CPU 限速。

为什么需要

大显示器需要快速查看会话信息,以及找到被最大化窗格隐藏的请求。此改动保留等宽横排、完整高度的历史消息和原有添加/关闭/返回/最大化操作,不增加工具栏行数或宽度控件。

此前同一个提交多次在 ECS 上遇到浏览器资源中断或超过文档性能门槛,而 hosted 环境的文档与 smoke 检查通过,因此浏览器作业改用已验证的 hosted 环境。

渲染缓慢时,历史分页可能在可见行出现之前启动,未能保存阅读位置,行高测量完成后便产生 286 像素跳动。先取得阅读位置可以消除这一时序依赖。

审阅者测试计划

如何验证

  • 打开两个会话,输入草稿,再悬停另一标题。详情在下方显示,不改变焦点或活动标记。最大化/关闭按钮不触发浮窗。宿主配置禁用详情操作时,标题浮窗应禁用,但待处理数量和导航仍可用;仅有创建时间的会话仍应显示时间。
  • 点击或 Tab 进入另一窗格,再最大化、还原、添加和关闭。活动标记跟随交互,关闭后选择邻居,草稿保持完整。刷新仅恢复当前标签页的分屏;退出分屏会清除恢复状态。
  • 一个窗格等待工具审批,另一个等待回答问题。从它们之间的空闲窗格出发,应按横排顺序向后定位并循环,最大化时也一样;焦点应在审批控件外。定位后立即按 Enter、Escape、2/3 或问题 Ctrl/Cmd+Enter:不发送审批回答,Escape 还原横排。主动 Tab/点击进入审批控件后仍可正常处理。
  • 待处理按钮有焦点时,从远端处理最后一个隐藏请求。按钮消失,焦点转到返回按钮。检查窄嵌入容器中的详情浮窗是否留在窗格内。有无待处理请求时,工具栏高度一致。
  • 主会话等待审批时,让对应分屏窗格停留在加载中或连接失败:外层提示应仍然可用;窗格实际显示审批后,再隐藏重复提示。窗格请求不变但主会话切换时,提示必须跟随新的主会话。多工作区的头部与浮窗应显示相同工作区名称,待处理按钮的无障碍名称应包含可见数量。
  • 开启 CPU 限速,从轮次导航打开旧历史,向两个方向连续跨越多个 200 条记录的分页。阅读行应保持原位,实时新消息不应干扰阅读,返回最新消息的操作仍可用。
  • 确认 PR 浏览器作业执行文档门槛和 smoke 测试,包括新增的三个分屏 smoke 用例。

改动前后证据

以上三张图片来自真实 Chromium 和确定性的测试会话。基线是全局安装的 CLI;改动后图片来自当前 UI 与 mock daemon,不含真实用户会话。第一张展示原有横排和头部行;第二张展示标题详情,同时另一窗格保留草稿和键盘焦点;第三张展示最大化隐藏兄弟窗格时,现有工具栏仍显示待处理数量。

图片托管在 wenshao/qwen-code fork 的 assets/pr11250-split-view 分支,引用固定提交的 URL。

测试平台

OS 状态
🍏 macOS ✅ Node 22.22.2、Chromium
🪟 Windows ⚠️ 未在本地测试
🐧 Linux ⚠️ 此版本未在本地测试

测试环境

当前本地版本:1,182 个组件测试通过,包含真实虚拟列表和全部历史消息视口测试。完整 56 个浏览器 smoke 用例及独立的退出分屏回归用例均无重试通过。文档浏览器门槛的 5 个测试在原性能限制下全部通过。CI 路由与清理契约 71 个通过、2 个平台跳过。全仓 build、typecheck、bundle、改动文件 lint/格式检查及浏览器测试的独立严格类型检查通过。

独立验证使用构建后的 CLI,原 200 条记录历史用例在 CPU 限速下连续三次通过,自然速度下原有三个历史用例也全部通过。两个新增回归测试在当前实现下通过,仅在内存中将生产组件换为上一精确版本后均失败。浏览器验证期间客户端源码、测试和构建产物保持冻结。

上一提交在 hosted 历史阅读位置检查中失败,分屏用例通过。当前提交的 hosted CI 状态以下方检查为准,与本地验证分别记录。

风险与范围

  • 主要风险或取舍:显式焦点转移和浮窗定位,已在真实 Chromium 中验证工具审批与提问两种路径;历史分页可能短暂等待可见行后再发送请求;浏览器 CI 使用 hosted 资源。
  • 未验证/范围外:实体多显示器移动、调整宽度和其他浏览器内核。
  • 兼容性/迁移:无破坏性变更;标签页存储及会话协议不变。

关联 Issue

无。

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

E2E test report

Verified 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.

Check Result
Baseline global CLI split persistence 2/2 passed
Baseline hover/layout probe 1/1 passed; native title only, toolbar 51px, pane header 43px
Updated Vite browser regression suite 4/4 passed
Final node dist/cli.js browser regression suite 4/4 passed, 7.6s
Related component unit tests 175/175 passed
Repository build / typecheck / bundle Passed
Changed-file ESLint / Prettier / diff checks Passed

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 packages/web-shell:

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-bundle

The 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 权限强制。

@wenshao
wenshao marked this pull request as ready for review September 7, 2026 04:07
@wenshao

wenshao commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI fix and verification

Pushed 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 /proc. This is not being reported as a passing local Linux suite.

The new PR CI run completed successfully on 68f017b8d7a7aa6d006e010668afabc404d53836. The actual PR browser job used GitHub hosted Ubuntu and passed all 5 document tests and 50 smoke tests (2 workers, 2.6 minutes for smoke, no flaky cases). No-AK passed all 187 tests in 145.76 seconds. Linux unit tests, both Desktop Shell jobs, visual capture, and Lint & Static passed; the Linux helper suite passed all 557 tests, including the platform-specific triage cases discussed above. The final PR check snapshot has 11 passing checks and 3 configured skips, with no failures or pending checks.

Investigation before the routing fix

The evidence below was collected on 5fb20297b194eab44d1c3cf7a1c4636a85dda917, before changing the workflow.

  • No-AK integration gate recovered: the original attempt reached its 20-minute step deadline without a failed assertion. The unchanged-head rerun passed all 187 tests in 217.49 seconds. The same full gate passed locally in 86.84 seconds.
  • ECS browser startup failures confirmed: the original smoke artifact contains 1,904 ERR_NETWORK_CHANGED resource errors across 25 retry traces. The second attempt contains another 505 across 10 retry traces; all 42 failed attempts stop at initial visibility checks, and 34 show the startup failure page. Some other attempts are still loading modules at the deadline. These failures precede split-view interaction assertions; the underlying host network event is not established.
  • Independent hosted validation passed: the repository's existing manual linux_runner=hosted option ran the same immutable head. Browser validation passed: 5 transcript-document tests and all 50 smoke tests, with no flaky cases, in 3.3 minutes for smoke. Hosted used the default two workers; the exact-head local smoke also passed all 50 with eight workers and zero retries. Hosted Linux unit tests, macOS unit tests, and Lint & Static passed as well.
  • Additional Windows result is not green: manual dispatch enables platform lanes that the original PR run skips. Windows passed all 6,367 Web Shell tests, including the changed components, but reports 40 failures in other packages. Of these, 39 match the latest main nightly by test name and error summary. The additional directory-identity revalidation failure was not present in that baseline and remains unexplained; it is not being labeled a confirmed baseline failure.

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.

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

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.

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review fixes and E2E verification

The 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 @smoke so they run in PR CI. The earlier 50-case smoke result did not cover the original two untagged feature tests; that gap is now closed.

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 node dist/cli.js build with zero permission requests after navigation. Each case also checks deliberate Tab/click into the approval followed by submission, producing exactly one intended request. Test fixtures include the daemon’s actual option kind, which the old fixture omitted.

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 wenshao/qwen-code fork’s assets/pr11250-split-view branch with immutable commit URLs. Windows, other browser engines, and physical multi-monitor movement were not tested locally for this revision.

中文说明

本轮修复包括:导航焦点避开审批快捷键范围、遵循宿主详情白名单、协调主会话提示、为辅助技术暴露待处理数量和当前窗格、保留邻居选中与焦点,以及同时约束浮窗到窗格和嵌入根容器。新增三个浏览器用例均加入 @smoke,确保 PR CI 执行。此前 50 个 smoke 通过的结果没有覆盖原来两个未加标签的功能测试,这一缺口现已补齐。

测试人员先复现了 6 组危险键盘操作:工具 Escape/Enter 拒绝请求,2/3 选择用户/项目永久授权,问题 Escape/Ctrl+Enter 拒绝/提交。最终 node dist/cli.js 构建的 6 个用例全部通过,导航后的按键均未发送审批请求;每例还验证主动 Tab/点击进入审批后提交,只发送一个预期请求。夹具已包含真实 daemon 选项的 kind,此前夹具遗漏了该字段。

本地验证:178 个组件测试、55 个 App 分屏/审批测试、5 个 Chromium 分屏回归全部通过;全仓 build、typecheck、bundle 和改动文件 ESLint/Prettier 通过。浏览器测试使用确定性的 mock daemon,不涉及真实模型和用户数据。

代码提交前,第 1 轮反向审计发现被输入框自动聚焦掩盖的受控新增窗格状态竞争,第 2 轮发现被裁切的末尾窗格浮窗仍会越过嵌入根容器。两者均有先失败的回归证据并已修复。第 3、4 轮完整审计连续无发现。独立代码审阅未发现可行动问题,并额外在构建后的 CLI 上验证关闭首个、中间和末尾最大化窗格的 3 个 Chromium 场景,全部通过。另外 8 组故意恢复缺陷的变体均被对应断言检出,每次变异后均恢复源码。

PR 描述的配图托管在 wenshao/qwen-code fork 的 assets/pr11250-split-view 分支,使用固定提交 URL。此版本未在本地验证 Windows、其他浏览器内核和实体多显示器移动。

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Round-2 review fixes and verification

This 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 node dist/cli.js. Standalone strict E2E typechecking also changed from TS18046 to success. Both previously flaky CI cases passed three consecutive built-CLI runs without retries. The original hosted failures in run 34113392757 were checked directly; they did not reproduce locally, so local runs are not presented as proof that removing the waits always fails.

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 full-reload and B-editor focus without any hover. The final 15/15 run froze watched files throughout; no product focus workaround was introduced.

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: 59683cccb9.

中文说明

本轮保持主会话审批提示可见,直到窗格实际报告审批;从空闲窗格按横排顺序定位下一个待处理会话;无障碍名称包含可见数量;工作区头部和详情使用一致的显示名。同时统一侧栏默认白名单、加强运行状态和浮窗方向回归测试、使用有类型的审批夹具,并恢复 CI 保留清理步骤的安全测试保护。简短的窗格/根容器边界计算继续内联,两层边界均有真实浏览器反向验证。

独立测试人员复现四项可见问题,并使用重新构建的 node dist/cli.js 验证全部修复。独立严格 E2E 类型检查从 TS18046 转为成功。此前 CI 波动的两个用例,各连续三次在构建后 CLI 中无重试通过。原始 hosted run 34113392757 的失败已直接核实,但在本机未复现,因此不把本机通过当作删除等待后必然失败的证明。

验证结果:953 项 App/组件测试通过;五个 Chromium 分屏用例各重复三次、无重试,15/15 通过;CI 契约测试 71 项通过、2 项平台跳过。全仓 build、typecheck、bundle、最终 Web Shell 与 E2E 独立类型检查以及改动文件 ESLint/Prettier 通过。12 组故意破坏实现的变体均被相应断言检出,包括真实碰撞边界和 CI worktree 路径穿越保护。

一次中间的 14/15 浏览器运行失效:我在 Vite 验证过程中修改了被监听的测试文件以修正 lint。独立测试人员从录像确定整页重载发生在悬停之前,又捕获了同一测试文件触发的 full-reload,没有悬停也会把焦点移到 B。最终 15/15 的运行全程冻结被监听文件,没有为此修改产品焦点逻辑。

提交前连续两轮完整差异反向审计均无发现。独立审查:无发现;重新核对本轮两个 Critical 和原有审批快捷键问题。提交:59683cccb9。

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review-fix verification — 9098dd3330

The pending-state report now avoids redundant whole-shell renders and preserves approval-notice lifetimes. All five round-3 suggestions are addressed. Two consecutive full-diff self-audits and an independent code review completed before this commit.

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 分支的三张固定提交配图。

@wenshao

wenshao commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Verification after integrating main (3ecfaffdf8), commit e47b5b3318.

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.

Check Result
Repository build, typecheck, bundle; staged lint/format Passed
Shell and related component tests, including the merged history viewport 1,012 passed across nine files
Chromium split regressions 5 passed, zero retries
Chromium history-viewport regressions 3 passed, zero retries
CI routing and recovery contracts 71 passed, 2 platform skips
Independent session-switch/report probes 3 passed
Browser cases served by the built CLI 2 passed, zero retries

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.

中文

同步主分支(3ecfaffdf8)后的验证,提交 e47b5b3318。

冲突解决同时保留了主分支的分屏恢复就绪检查和本 PR 的审批上报回调。本轮评论处理补充会话切换回归断言并澄清回调约定,生产逻辑未改变。

检查 结果
全仓 build、typecheck、bundle;暂存文件 lint/格式 通过
界面与相关组件测试,包含同步后的历史消息视口 9 个文件共 1,012 个通过
Chromium 分屏回归 5 个通过,无重试
Chromium 历史消息视口回归 3 个通过,无重试
CI 路由与清理契约 71 个通过,2 个平台跳过
独立会话切换/上报探针 3 个通过
已构建 CLI 提供页面的浏览器用例 2 个通过,无重试

构建后 CLI 用例确认:主窗格连接失败不会隐藏外层审批提示;健康窗格实际上报后隐藏重复提示,关闭它后恢复提示;点击恢复的提示可返回审批,整个流程没有提交审批。会话切换探针确认,不改变窗格待处理集合、不手动调用报告时也能自动刷新。

上一精确提交的完整界面测试在两种故意破坏实现的变体下均为 765/765 通过:删除会话依赖,以及每次更新 ref 但固定回调引用。当前已跟踪测试在正常实现下通过,两种变体均在新增的回调引用身份断言处失败。ref 变体被调用时能够读到新 ID,失去的是会话切换时的自动重新上报。同会话引用稳定性和其他窗格不触发额外渲染的断言继续通过。

连续两轮完整差异反向审计及独立复核均未发现可操作问题,并对照当前代码复核了旧 Critical 评论。测试期间客户端源码和测试文件保持冻结,没有并行构建。浏览器使用模拟 daemon 传输;未声称覆盖实体多显示器或其他浏览器内核。新提交的 CI 以 GitHub 检查结果为准。

@wenshao

wenshao commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Fixed the history reading-position failure from CI run 34185615052, commit 0de61b5c47.

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 onTaskUpdate RPC timeout; rerunning the failed job passed without that unhandled error (CLI 29,022 passed; Web Shell 6,654 passed). GitHub automatically reran the dependent browser gate, which also passed. No code, assertion or timeout was changed for that infrastructure retry.

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 node dist/cli.js: three consecutive passes, retaining the original two-pixel bound. Both new regression tests pass intact and fail when only the production component is replaced in memory with its exact previous version; the failure shows the old implementation starts a request before a visible row exists.

Verification Result
Repository build, typecheck, bundle; lint/format; strict E2E typecheck Passed
Related component suites including the real virtual list 1,182 passed
Full browser smoke, zero retries 56 passed
Separate split-exit regression, zero retries 1 passed
Transcript document browser gate, original performance limit 5 passed
CI routing/recovery contracts 71 passed, 2 platform skips
Independent built CLI: CPU4 reproduction / natural history suite 3 passed / 3 passed
New unit regressions against the exact old component 2 expected failures

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 的历史阅读位置失败,提交 0de61b5c47。

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 像素要求。两个新增回归测试在当前实现下通过,仅在内存中换回上一精确生产组件后均失败,确认旧实现会在可见行出现之前发送分页请求。

检查 结果
全仓 build、typecheck、bundle;lint/格式;E2E 严格类型检查 通过
相关组件测试,包含真实虚拟列表 1,182 个通过
完整浏览器 smoke,无重试 56 个通过
独立退出分屏回归,无重试 1 个通过
文档浏览器门槛,原性能限制 5 个通过
CI 路由/清理契约 71 个通过,2 个平台跳过
独立构建后 CLI:CPU4 复现/自然历史测试 3 个通过/3 个通过
新增单测换回精确旧组件 2 个预期失败

提交前完成连续两轮完整差异反向审计及独立代码复核,无可操作发现。浏览器验证全程冻结客户端源码、测试和构建产物。本地结果来自 macOS Chromium 与 mock daemon;新提交的 hosted CI 以下方 GitHub 检查为准。PR 描述保留了 wenshao fork assets 分支托管的三张固定提交配图。

@yiliang114
yiliang114 enabled auto-merge September 8, 2026 09:13

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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-392 goToPendingPane picks the next pending pane; :402-404 resolves the focus target by data-pane-session-id and scrolls it into view; the target is the pane slot at :551-555 — role="group", tabIndex={-1} — which is an ancestor of ChatPane, not the approval panel.
  • :405-406 documents 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:487 onKeyDown={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"), and AskUserQuestion.tsx:668 likewise. 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). keyboardActive at ToolApproval.tsx:371 / AskUserQuestion.tsx:589 only governs auto-focusing a safe default, not whether onKeyDown runs, so Tab/click into the panel still works normally.
  • SplitView.tsx has exactly two document-level keydown listeners, :273 and :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.js widens its selection to also match any job carrying the Clean stale .qwen before checkout step, 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 ECS test carried measured evidence (hk3-9 npm ci 19m46s of a 20m budget; hk4-23 install 10m33s vs 4m53s on hk5-2). The replacement pins runs-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.

@yiliang114

Copy link
Copy Markdown
Collaborator

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 0de61b5 from this PR when convenient so the fixes can be reviewed and landed independently.

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 is failure (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.

@wenshao

wenshao commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  1. 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 by data-pane-session-id and focuses that (SplitView.tsx:399-409), the container is genuinely focusable because the pane wrapper carries tabIndex={-1} (:555) beside data-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 @smoke test navigates hidden tool and question approvals without answering them pins it end to end: it presses Enter, 2, 3 and Escape after navigating and asserts nothing was answered, with expect(daemon.permissionRequests()).toHaveLength(0) as the backstop.
  2. R2-1 (the @smoke containment 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.
  3. R2-2 (the @smoke test pressed Escape once 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 explicit Escape before moving to the next pane (:478), and in the restore flow the Escape at :421 follows a .focus()-only sequence whose focus landing on back is 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 the paneIds identity actually changed and only takes focus when document.activeElement === document.body, so it cannot steal focus from a user mid-interaction. pendingIds is memoized to keep the report identity stable (the comment names the loop it prevents), the unmount cleanup reports an empty pending set, and handleApprovalChange returns 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 reports false on 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 as data-pane-active plus aria-current="location" and an aria-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 through requestAnimationFrame up to eight frames, assigns the anchor only once capture() succeeds, and bails if scrollIntent moved in the meantime; a single-flight guard prevents stacking pending loads, handleScrollIntent cancels the pending frame, and a useLayoutEffect cleanup keyed on viewKey cancels 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 hosted ubuntu-latest with 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 的缺陷,我读到的通道中也没有任何一项构成门禁。

@wenshao
wenshao dismissed a stale review September 8, 2026 17:33

fixed

@yiliang114
yiliang114 added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 70cf363 Sep 8, 2026
63 checks passed
qwen-code-dev-bot added a commit that referenced this pull request Sep 8, 2026
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.
qwen-code-dev-bot pushed a commit that referenced this pull request Sep 8, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants