feat(web-shell): optimize session overview table - #10115
Conversation
|
Re-run at the author's request — gate re-checked at the current head, after five fix rounds since the first pass.
Moving on to code review. 🔍 中文说明应作者请求重跑 —— 在首轮之后的五轮修复之上,于当前 head 重新过门禁。
进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewRe-run: the table rework itself was reviewed in full at What the delta does, round by round:
These increments are also corroborated from outside this review: the maintainer's scripted live-browser matrix (at Remaining follow-ups (none blocking):
Testing evidenceUnattended CI run — I did not build or execute PR code. The evidence below is the PR's own CI on the reviewed commit plus the sandboxed
One row per check name (latest run on the reviewed commit); skipped checks omitted. / 每个检查名一行(取被审提交的最新一次运行),省略 skipped。 All four of the PR's own CI workflows (
Sandboxed verification at exactly this head ( 中文说明代码审查重跑说明:表格重构本身已在 各轮要点:
这些增量也有外部佐证:维护者的脚本化实机验证( 遗留跟进项(均不阻塞):
测试证据无人值守 CI 运行 —— 未构建或执行 PR 代码。以上证据为被审提交上 PR 自身 CI 与沙箱 CI 表格见上方标记区。head 提交上 PR 自身的四个 CI 工作流(
该 head 上的沙箱验证(维护者触发的 — Qwen Code · qwen3.8-max Reviewed at |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
|
Confidence: 4/5 — everything the gate can check at head checks out (CI green, sandboxed verify passes with load-bearing proof, maintainer findings converged); the last point stays off only because pixel-level rendering on Windows/Linux rests on the author's word and the bot review pipeline failed to complete a pass on this exact head. Stepping back: my independent sketch for this problem (stock shadcn table primitive + thin behavior layer, sticky CSS columns, existing AlertDialog) is still what the PR does — and it keeps exceeding it in the places that matter. Five fix rounds landed since the first pass, and every one of them was a response to a real finding, not churn: the split-selection truncation, the false archive error, the phone-width fixed-column overlap, the stale-rows gap when the sidebar owns the live channel — each fixed with the matching test next to it. The newest commit hardens rather than patches: live-state only over fully trusted workspaces, archive/delete only on idle sessions. Is it needed? Yes, more so than the first pass assumed — a maintainer drove the real UI against a real daemon with 29 sessions across 4 workspaces and measured the result, an independent E2E pass found no blockers, and the author engaged through five rounds plus a sandboxed verification instead of pushing back on volume. If I'm maintaining this in six months, the Reservations, named so they don't get lost: the All of the PR's own CI completed green on the reviewed commit — no pending runs — and the approval guardrail does not apply (same-repository branch, 中文说明置信度:4/5 —— 门禁能在 head 上核验的一切均已通过(CI 全绿、沙箱验证以承重性证明通过、维护者提出的问题全部收敛);扣掉的一分仅因为 Windows/Linux 上的像素级渲染只有作者声明,且 bot review 流水线未能在该 head 上完成一轮完整评审。 退一步看:我为这个问题独立构想的方案(标准 shadcn table 原语 + 薄行为层、CSS 固定列、复用现有 AlertDialog)依然是本 PR 的做法 —— 且它在关键处持续超出预期。首轮之后落地的五轮修复,每一轮都是对真实发现的回应而非无效改动:分屏选择截断、归档假错误、手机宽度固定列重叠、侧边栏拥有 live 通道时的行状态过期 —— 每项修复旁都有对应测试。最新提交是加固而非补丁:仅对完全可信的工作区启用 live-state、仅对空闲会话允许归档/删除。 有必要吗?有,而且比首轮判断的更有必要:一位维护者用真实 daemon、4 个工作区的 29 个会话驱动真实 UI 并实测了结果,一次独立 E2E 未发现阻塞项,作者以五轮修复加一次沙箱验证持续投入而非以量压人。半年后维护这段代码, 顾虑照实列出,避免被遗忘: PR 自身的 CI 在被审提交上全部通过且无进行中任务,批准护栏不适用(同仓库分支、 — Qwen Code · qwen3.8-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
Local verification report — real daemon + real Web ShellI built a live environment for this PR and drove the actual UI in Chromium rather than reasoning from the diff. Summary up front: the responsive table works, and every claim in the Reviewer Test Plan that I could measure holds. I found one regression I'd fix before merge, plus three smaller issues worth a decision.
Static checks
I also chased two things that looked like diff noise and both are fine: the What the live run confirmsEvery one of these was measured against the real DOM, not asserted from source. Responsive sizing. At 1600px and 1280px the table reports Fixed columns and their offsets. In scroll mode the select column pins at Edge shadows, including the "before scrolling" claim in the PR body. Measured via Hover synchronisation across the fixed and scrolling cells, at every scroll-mode width. Functional matrix, all passing against the live daemon: search; the empty result rendering inside Localisation. Under Mobile. The panel is now genuinely reachable from the drawer at 760/600/414px and scrolls correctly there. Finding 1 — "Open in split" silently drops selections, and under a sort it drops the wrong ones (recommend fixing before merge)
const splitIds = selectedCards.map((card) => card.sessionId).slice(0, MAX_SPLIT_PANES);Two problems in that one line, both reproduced live. a) The cap is silent. I selected 8 rows. The footer read b) Under an active sort it drops the rows at the top of the screen. Reproduced: sorted by Time ascending, selected the top 8 visible rows, clicked Open in split. The six that opened were selections #3 through #8 — the ones at the bottom of the selection.
Suggested fix: cap against The test that should have caught this can never fail. expect(container!.textContent).not.toContain('Only the first 6');That string was deleted by this same PR and appears nowhere in To be fair to the rest of the file: Finding 2 — Archiving an already-removed session reports a false error, but only on the primary workspace
The workspace-qualified branch explicitly counts Live A/B on the same action, same daemon, one session in each workspace, each removed out-of-band first (exactly what a second tab or the TUI does while the overview's list is stale):
I want to be clear about what this does not do. The obvious escalation is that Finding 3 — Below ~430px the two fixed regions cover the entire scroll portBefore this PR the Session Overview entry point was gated on
Under 460px the Title and Actions columns overlap, so Worktree, Session ID, Workspace and Time can never be brought into view no matter how far you scroll — I verified this by scrolling to The table stays operable there — Title plus the four actions is arguably the right mobile subset — so this may be an acceptable trade. But it is currently accidental rather than designed. Dropping the Title column's fixed width or unpinning Actions below a breakpoint would make it intentional. Finding 4 — Back from the Session Overview discards the open conversation
Reproduced: opened a session with a transcript, opened the Session Overview, pressed Back — the transcript is gone and the shell is in the "Welcome to Qwen Code" new-task state. No confirmation, and it fires even if the user opened the overview and touched nothing. The PR body says clearing on Back is intended, so this is a design question rather than a defect, but two things about it look unconsidered:
Smaller notes
VerdictThe table itself is solid and the responsive engineering is better than the PR body claims — the shadow-before-scroll behaviour, the offset accumulator and the fluid/scroll flip all measure correctly, and 4323 tests pass. Finding 1 is the one I'd want fixed first: it silently discards user selections, and under a sort it discards specifically the rows the user is looking at, in a path whose guard-rail notice this PR removed. Findings 2–4 are judgement calls for the maintainers; the rest are follow-ups. Happy to re-run the whole matrix against a new push — the harness is scripted end to end. 中文版本地验证报告 —— 真实 daemon + 真实 Web Shell我为这个 PR 搭建了一套真实环境,在 Chromium 里驱动真正的 UI,而不是只读 diff 推理。结论先说:响应式表格是可用的,Reviewer Test Plan 里我能测量的每一条主张都成立。 我发现了一个建议合并前修掉的回归,外加三个需要维护者拍板的小问题。
静态检查
另外我追查了两处看起来像 diff 噪音的改动,结论都是没问题: 实机验证通过的部分以下每一条都是对真实 DOM 的测量,不是从源码推断的。 响应式列宽。 1600px 和 1280px 下表格上报 固定列及其偏移。 滚动模式下选择列固定在 边缘阴影,含 PR 描述中"滚动前就出现"这一条。 通过 Hover 背景同步 —— 在所有滚动模式宽度下,固定列与滚动列均保持一致。 功能矩阵(全部通过,均针对真实 daemon):搜索;空结果渲染在 本地化。 移动端。 该面板现在确实能从抽屉里在 760/600/414px 下打开,并能正常滚动。 问题 1 —— "分屏打开"会静默丢弃选择,且在排序状态下丢错了对象(建议合并前修复)
const splitIds = selectedCards.map((card) => card.sessionId).slice(0, MAX_SPLIT_PANES);这一行里有两个问题,均已实机复现。 a) 截断是无声的。 我选中了 8 行。底部显示 b) 在排序状态下,被丢掉的是屏幕最上方的行。 复现过程:按时间升序排序,选中可见的前 8 行,点击"分屏打开"。真正打开的是选择项 #3 到 #8 —— 也就是选择中靠下的那些。而 "新标签页打开"使用同一个 建议修法:按 本该拦住这个问题的测试永远不会失败。 expect(container!.textContent).not.toContain('Only the first 6');该字符串正是被本 PR 删除的,在 公平地说,该文件其余部分是扎实的: 问题 2 —— 归档一个已被移除的会话会报假错误,且只在主工作区发生
带工作区限定的分支明确把 同一操作、同一 daemon、每个工作区各取一个会话,事先在带外删除(这正是第二个标签页或 TUI 在总览列表尚未刷新时会造成的情况)的实机 A/B:
我想明确说明这个问题不会导致什么。 最容易想到的升级路径是:归档被判为"失败"后 问题 3 —— 约 430px 以下,两侧固定列会盖满整个滚动区在本 PR 之前,会话总览入口受
460px 以下标题列与操作列相互重叠,因此 Worktree、会话 ID、工作区、时间四列无论怎么滚动都无法进入视野 —— 我通过滚动到 在那些宽度下表格仍然可操作 —— 只保留标题加四个操作,作为移动端子集其实也说得通 —— 所以这可能是可接受的取舍。但目前它是偶然形成的,而非设计出来的。去掉标题列的固定宽度,或在某个断点以下取消操作列的钉住,可以把它变成有意的设计。 问题 4 —— 从会话总览点击"返回"会丢弃已打开的对话
复现:打开一个有对话记录的会话,进入会话总览,点击返回 —— 对话记录消失,shell 进入 "Welcome to Qwen Code" 新建任务状态。没有确认,且即使用户只是打开总览什么都没做也会触发。 PR 描述说返回时清空是有意设计,所以这更像是一个设计问题而非缺陷,但其中两点看起来没有被考虑到:
其他较小的问题
结论表格本身是扎实的,响应式部分的工程质量比 PR 描述所称的还要好 —— 滚动前阴影、偏移累加器、流式/滚动模式切换全部测量正确,4323 个测试通过。问题 1 是我希望优先修掉的:它会静默丢弃用户的选择,而在排序状态下丢掉的恰恰是用户正在看的那几行,且本 PR 还顺手删除了这条路径上原有的提示护栏。问题 2 到 4 需要维护者判断;其余属于后续跟进。 如果有新的推送,我可以重跑整套矩阵 —— 这套 harness 是全程脚本化的。 |
tmux E2E test report (head
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not explored to full depth (tool budget reached): chunk 8: none — but note I could not run typecheck/tests locally (no node_modules in the review worktree), so runtime/type verification of these tests rests on the aut…; "agent 1b": none — no check was cut short.; chunk 7: none — CI's ubuntu test job was still pending at check time, so I could not use it as evidence and instead verified behavior directly against the component an…; chunk 4: executing SessionOverviewPanel.test.tsx via vitest (no node_modules in worktree; install+build not feasible in budget).
中文说明
未探索到全部深度(达到工具调用预算):chunk 8:none — but note I could not run typecheck/tests locally (no node_modules in the review worktree), so runtime/type verification of these tests rests on the aut…;"agent 1b":none — no check was cut short.;chunk 7:none — CI's ubuntu test job was still pending at check time, so I could not use it as evidence and instead verified behavior directly against the component an…;chunk 4:executing SessionOverviewPanel.test.tsx via vitest (no node_modules in worktree; install+build not feasible in budget)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/components/SessionOverviewPanel.tsx:1090 — [review] actionable statuses render identically to running (uniform spinner)packages/web-shell/client/App.tsx:12649 — [review] manageLiveState is only witnessed in the false directionpackages/web-shell/client/App.tsx:12183 — [review] canOpenSessionsOverview un-gating has no witnesspackages/web-shell/client/components/SessionOverviewPanel.tsx:398 — [review] panel/sidebar live-state workspace predicates divergepackages/web-shell/client/App.test.tsx:17755 — [probe] post-clear guards' === undefined disjuncts are unwitnessedpackages/web-shell/client/App.test.tsx:17799 — [probe] pre-guard sessionId-mismatch half is unwitnessedpackages/web-shell/client/components/SessionOverviewPanel.test.tsx:2441 — [probe] live-state channel enabled:true direction never assertedpackages/web-shell/client/App.test.tsx:18009 — [probe] panel-back race test passes via the invocation guard alonepackages/web-shell/client/App.tsx:12609 — [probe] Open in split is reachable below the breakpoint and folds silentlypackages/web-shell/client/components/SessionOverviewPanel.test.tsx:2415 — [probe] useStatusReport autoLoad pinned only in the false directionpackages/web-shell/client/App.test.tsx:17740 — [probe] onCurrentSessionRemoved success-path return never assertedpackages/web-shell/client/components/SessionOverviewPanel.test.tsx:1671 — [probe] onCurrentSessionRemoved payload unwitnessed from both endspackages/web-shell/client/App.test.tsx:1285 — [probe] locked-workspace prop handoff to the overview is unwitnessedpackages/web-shell/client/components/SessionOverviewPanel.test.tsx:1324 — [probe] workspace filter re-inclusion branch never exercised
中文说明
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 14 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| // away. Standalone/uncontrolled only — a controlled host owns its split | ||
| // lifecycle and re-opens it itself. | ||
| if (splitFoldedByShrinkRef.current) { | ||
| if (splitFoldedByShrinkRef.current && !activePanel) { |
There was a problem hiding this comment.
[Critical] R1-3: (fix-induced) The && !activePanel deferral this commit added to the grow-restore branch (answering the round-1 finding) leaves splitFoldedByShrinkRef.current set while the Session Overview is open, so any later panel close — including closes that are explicit navigation — restores the folded split over the user's navigation. Shrink folds an open split (the flag is set at line ~5419); the user opens the Session Overview; the viewport grows back and this branch is skipped because the panel is open — without clearing the flag (this line is its only clear site). The user then clicks a session title (handleOpenSessionFromOverview → loadSidebarSession → closePanel()) or Back (createNewSession → closePanel()); closing the panel re-runs the effect, which sees the stale flag and calls setMainView('split') — the user lands on the stale split instead of the clicked session (which loads into the hidden chat connection) or the fresh chat. Before this commit the flag was always cleared immediately on grow, so this window did not exist. (The explicit-split-exit-then-Escape variant does not reproduce — openSplitView itself clears the flag.)
Witness (probe on this commit):
PROBE-F1-back split-view-page: PRESENT inline-panel: absent -> FAILED
PROBE-F1-load split-view-page: PRESENT -> FAILED
with the flag cleared on navigation: both absent -> passed
(committed Escape-restore test still passes)
Clear splitFoldedByShrinkRef.current = false; whenever the user takes explicit ownership of the view — in handleSplitExit next to clearSplitSessions(), and in handleOpenSessionFromOverview / the sessions Back handler before navigation — so the deferred restore only fires for a plain dismiss (Escape/close). Please add a variant of the deferral test that leaves the overview via panel-back or a session open and asserts split-view-page stays absent; removing the flag-clear must turn it red (the probe verified both directions).
中文说明
本次提交为回应上一轮发现而在 grow-restore 分支加入的 && !activePanel 延迟恢复,会在会话总览打开期间一直保留 splitFoldedByShrinkRef.current,导致之后任何关闭面板的操作——包括明确属于导航的关闭——都会恢复已被折叠的分屏,覆盖用户的导航。收缩时折叠分屏(约 5419 行置位);用户打开会话总览;视口恢复变大时因面板打开而跳过本分支——但没有清除标志(本行是该标志唯一的清除点)。随后用户点击会话标题(handleOpenSessionFromOverview → loadSidebarSession → closePanel())或点击返回(createNewSession → closePanel()),关闭面板会重新触发该 effect,读到过期标志并调用 setMainView('split')——用户落在过期的分屏上,而不是被点击的会话(它被加载进隐藏的聊天连接)或新聊天。本次提交之前标志在 grow 时总是立即清除,因此不存在这个窗口。(先显式退出分屏再按 Esc 的变体不复现——openSplitView 自身会清除标志。)
探针见上方输出。修复:在用户明确接管视图时清除标志——handleSplitExit 中 clearSplitSessions() 旁,以及 handleOpenSessionFromOverview/会话面板返回处理器的导航之前——使延迟恢复只在普通关闭(Esc/关闭)时触发。请新增一个从面板返回或打开会话离开总览、并断言 split-view-page 保持不出现的变体测试;移除清标志逻辑后该测试应变红(探针已双向验证)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| const cleared = await createNewSession(undefined, { | ||
| keepView: true, | ||
| keepPanel: true, | ||
| }); |
There was a problem hiding this comment.
[Critical] onCurrentSessionRemoved passes undefined as the workspace to createNewSession, silently bouncing the user's next chat to the primary workspace when the deleted/archived current session lived in a trusted secondary workspace. Multi-workspace daemon, no workspace locked, current session in /wsB: the user deletes/archives it from the Session Overview, the guard passes, but createNewSession(undefined, …) clears selectedWorkspaceCwd; at the next prompt prepareSubmit resolves targetWorkspaceCwd = locked ?? selectedWorkspaceCwdRef.current ?? primaryWorkspaceCwd — the user's next prompt, tool calls, and file access silently run against the wrong project. This contradicts the workspace-preserving semantics the same diff pins for the sibling panel-back handler (expect(selectedWorkspaceCwd).toBe('/work/secondary')); all existing delete-flow tests use the primary workspace, where undefined and the current workspace coincide.
Witness (probe on this commit):
BASE: AssertionError: expected undefined to be '/work/secondary'
PR + fix below: 15/15 tests pass
| const cleared = await createNewSession(undefined, { | |
| keepView: true, | |
| keepPanel: true, | |
| }); | |
| const cleared = await createNewSession(removed.workspaceCwd, { | |
| keepView: true, | |
| keepPanel: true, | |
| }); |
If bouncing to the primary workspace after a delete is intentional product behavior, say so in a comment instead. The witness is a new App.test.tsx case mirroring the panel-back secondary-workspace test for the delete flow — with the fix reverted to undefined, it must go red.
中文说明
onCurrentSessionRemoved 向 createNewSession 传入 undefined 作为工作区:当被删除/归档的当前会话位于可信的次要工作区时,用户的下一个聊天会被悄悄切回主工作区。多工作区、未锁定场景下,当前会话在 /wsB:用户从会话总览删除/归档它,守卫通过,但 createNewSession(undefined, …) 清掉了 selectedWorkspaceCwd;下一次提交时 prepareSubmit 解析 targetWorkspaceCwd = locked ?? selectedWorkspaceCwdRef.current ?? primaryWorkspaceCwd——用户的下一条消息、工具调用与文件访问会悄悄跑在错误的项目里。这与同一 diff 为面板返回处理器钉住的工作区保持语义相矛盾(expect(selectedWorkspaceCwd).toBe('/work/secondary'));现有删除流程测试全部使用主工作区,那里 undefined 与当前工作区恰好重合。探针输出见上。若删除后切回主工作区是刻意的产品行为,请改用注释说明。见证测试:仿照面板返回的次要工作区测试新增删除流程用例;将修复还原为 undefined 后该测试应变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| const workspaceOptions = useMemo(() => { | ||
| if (workspaceCwd || !multiWorkspace) return []; |
There was a problem hiding this comment.
[Critical] excludedWorkspaceCwds is never reconciled against the visible workspaces and never reset when the panel becomes locked to a workspaceCwd, so a stale exclusion can hide every row with the filter UI removed — no recovery short of a remount. Daemon advertising 2+ trusted workspaces: the user unchecks /wsB in the funnel filter; the host then locks the shell to /wsB while mounted (index.test.tsx re-renders the same root with a changed lockWorkspaceCwd, and nothing in App.tsx remounts the panel). The panel re-renders locked: workspaceOptions collapses to [] so the filter popover disappears, but filteredCards still excludes every /wsB card — the table shows the empty state even though sessions exist, and there is no visible control left to clear the exclusion.
Witness (probe on this commit):
BASE: rows [""] empty: "No data" funnel: ABSENT -> FAILED
with a reconcile effect: rows ["Alpha"] -> passed (91 panel tests green)
Reconcile the exclusions with the currently listed options — e.g. an effect keyed on workspaceOptions that filters excludedWorkspaceCwds down to the option cwds (this also drops them when locked, when the UI to manage them is hidden). The witness is a panel test that excludes /wsB via the filter, re-renders with workspaceCwd: '/wsB', and asserts the /wsB row is still listed; removing the reconcile effect must turn it red.
中文说明
excludedWorkspaceCwds 从不与可见工作区做对账,面板被锁定到某个 workspaceCwd 时也不会重置,因此过期的排除项会在筛选 UI 消失的同时隐藏所有行——除了重新挂载别无恢复手段。守护进程通告 2 个以上可信工作区时:用户在漏斗筛选中取消勾选 /wsB;宿主在已挂载状态下把 shell 锁定到 /wsB(index.test.tsx 会在同一 root 上以变更后的 lockWorkspaceCwd 重新渲染,App.tsx 中没有任何逻辑重挂载面板)。面板以锁定状态重新渲染:workspaceOptions 收敛为 [],筛选弹层消失,但 filteredCards 仍排除所有 /wsB 卡片——会话明明存在,表格却显示空状态,且没有任何可见控件能清除该排除项。探针输出见上。修复:用当前可选项对排除集对账——例如一个以 workspaceOptions 为依赖的 effect,把 excludedWorkspaceCwds 过滤到可选项的 cwd(锁定时 UI 已隐藏,顺便清除)。见证测试:先通过筛选排除 /wsB,再以 workspaceCwd: '/wsB' 重新渲染,断言 /wsB 行仍然列出;移除对账 effect 后应变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| expect(workspaceClient.deleteSessionsData).toHaveBeenCalledWith(['s1']); | ||
| expect(sessionCatalogController.refreshWorkspace).toHaveBeenCalledWith( | ||
| '/w', | ||
| ); |
There was a problem hiding this comment.
[Suggestion] The round-2 guarantee 'refresh the owning catalog workspace even when a mutation fails' is pinned only on the resolved-with-errors path: none of the rejection-path tests assert refreshWorkspace, so the finally refresh in mutateCards (SessionOverviewPanel.tsx:692-697) and the catch refresh in saveRename (SessionOverviewPanel.tsx:787-789) are unwitnessed. The only failing-path assertion ('clears a deleted current session after a partial batch failure') uses a client that RESOLVES with errors: [...], so its try-block completes normally and still triggers the refresh. A refactor moving refreshWorkspace out of the finally into the success branch (or dropping it from the rename catch) keeps the whole suite green — verified by mutation (90/90 pass under both mutants; deleting the refresh outright fails 4 tests, proving the wiring is live). On a daemon advertising workspace_session_live_state the panel disables its catalog-poll fallback, so after a rejected mutation (network error, daemon busy) with no refresh, overview rows stay stale indefinitely — exactly the staleness this fix commit set out to eliminate.
Add expect(sessionCatalogController.refreshWorkspace).toHaveBeenCalledWith('/w'); to the it.each('does not clear the current session when %s fails') cases and to the archive/rename failure tests. Those extended tests must go red if the finally block in mutateCards or the refreshWorkspace call in saveRename's catch is removed — today removing either keeps every test green.
中文说明
第二轮引入的保证“即使变更失败也刷新所属 catalog 工作区”只在 resolve-with-errors 路径上被钉住:所有 reject 路径测试都没有断言 refreshWorkspace,因此 mutateCards 的 finally 刷新与 saveRename 的 catch 刷新没有见证。唯一的失败路径断言使用的客户端是 RESOLVE 出 errors: [...],try 块正常完成,刷新照常触发。把 refreshWorkspace 移出 finally 到成功分支(或从改名 catch 中删掉)整个套件仍然绿——已用突变验证(两种突变均 90/90 通过;彻底删除刷新则 4 个测试失败,证明接线是活的)。在通告 workspace_session_live_state 的守护进程上面板禁用了 catalog 轮询兜底,变更后若失败且不刷新,总览行将无限期保持过期——正是本次修复要消除的过期问题。请在失败用例中补充断言;移除对应刷新后这些测试应变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| expect(panel).not.toBeNull(); | ||
| expect( | ||
| container.querySelector('[data-testid="split-view-page"]'), | ||
| ).toBeNull(); |
There was a problem hiding this comment.
[Suggestion] This new deferral test witnesses only the Escape exit from the Session Overview; the navigation exits — the exact paths the confirmed Critical above (R1-3) is broken on — have no witness, so the forthcoming fix lands unpinned. A probe leaving via panel-back or load-session shows split-view-page PRESENT on this commit (the folded split is restored over the navigation) while this Escape-only test stays green. When R1-3 is fixed by clearing the flag on navigation, no test pins that clearing — a later refactor removing it regresses the fix with the entire suite green.
Add a variant reusing this setup (shrink → open overview → grow) that leaves the overview via [data-testid="panel-back"] or a session open instead of Escape, and asserts [data-testid="split-view-page"] stays absent. The proposed test is red on current code (probe-verified) and passes once the R1-3 fix clears the flag on navigation.
中文说明
这个新的延迟恢复测试只见证了按 Esc 离开会话总览;导航式离开——正是上面已确认 Critical(R1-3)出问题的路径——没有任何见证,即将到来的修复将落地即无钉。探针显示:在本提交上从面板返回或打开会话离开时 split-view-page 会出现(折叠的分屏越过导航被恢复),而这个仅 Esc 的测试依旧绿。R1-3 通过“导航时清标志”修复后,没有任何测试钉住该清除——后续重构删掉它将在套件全绿的情况下复发。请新增一个复用该场景、改从面板返回或打开会话离开并断言 split-view-page 保持不出现的变体;该测试在当前代码上为红(探针已验证),在 R1-3 修复后变绿。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| sessionCatalogController.renamed( | ||
| ownerCwd, | ||
| card.sessionId, | ||
| result?.displayName || nextName, | ||
| ); |
There was a problem hiding this comment.
[Suggestion] The || nextName fallback in this new catalog sync call has no witness — both rename mocks always resolve a displayName ({ displayName: 'Renamed' } at test lines 203 and 230), so dropping the fallback ships green. Mutation check: replacing result?.displayName || nextName with result?.displayName keeps 90/90 green, because the inline rename test types 'Renamed' (coinciding with the mock echo) and the qualified-client test asserts the echoed value. The void shape is real — the panel itself types let result: SessionMetadataResult | void (line ~762), and the sidebar's mirrored path handles it with an explicit fallback and comment (WebShellSidebar.tsx:2610-2618). If the fallback regresses, any rename resolving void writes undefined into the catalog cache, blanking/wronging that session's label in the sidebar and overview until the next full reconcile.
Add a test that sets workspaceActions.renameSession.mockResolvedValueOnce(undefined), renames to a distinct name (e.g. 'Void Name'), and asserts sessionCatalogController.renamed was called with '/w', 's1', 'Void Name'. That test must go red if the || nextName fallback is removed — the probe observed renamed called with ['/w', 's1', undefined] under that mutant.
中文说明
这个新 catalog 同步调用里的 || nextName 兜底没有见证——两个改名 mock 总是返回 displayName(测试 203、230 行的 { displayName: 'Renamed' }),删掉兜底依旧全绿。突变验证:把 result?.displayName || nextName 换成 result?.displayName,90/90 仍绿(内联改名测试恰好输入 'Renamed' 与 mock 回显重合;qualified 客户端测试断言的也是回显值)。void 形态是真实存在的——面板自身就把 result 标为 SessionMetadataResult | void(约 762 行),侧边栏的镜像路径也用显式兜底加注释处理了它。若兜底回归丢失,任何解析为 void 的改名都会把 undefined 写进 catalog 缓存,侧边栏/总览里该会话的名字会空白或错误,直到下一次完整对账。请补充 void 场景测试;移除兜底后应变红(探针在该突变下观察到 renamed 收到 ['/w','s1', undefined])。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| workspaceLiveStateOptions = { enabled: false }; | ||
| statusReportOptions = {}; | ||
| sessionCatalogController.refreshWorkspace.mockReset(); | ||
| sessionCatalogController.renamed.mockReset(); |
There was a problem hiding this comment.
[Suggestion] The new renamed() catalog-sync call has no negative witness on the rename-failure path: 'surfaces a rename failure in the notice area' (line ~2353) runs saveRename's catch branch via workspaceActions.renameSession.mockRejectedValue(new Error('locked')) but only asserts the error text, never expect(sessionCatalogController.renamed).not.toHaveBeenCalled(). saveRename already calls refreshWorkspace(ownerCwd) on BOTH the success and catch paths (SessionOverviewPanel.tsx:783 and 788), so a cleanup that consolidates the two sync calls — e.g. moving renamed(...) into the shared finally/catch next to refreshWorkspace — ships with the suite green (verified: adding renamed to the catch keeps 90/90 green). A failed rename would then overwrite the catalog's cached display name with the value the daemon rejected, showing a stale label until the next full reconcile.
Add expect(sessionCatalogController.renamed).not.toHaveBeenCalled(); after flushAsync() in the rename-failure test. That assertion must go red if the renamed(...) call is moved into the failure path of saveRename — the probe observed it called once with ['/w', 's1', 'Renamed'] under that mutant.
中文说明
新的 renamed() catalog 同步调用在改名失败路径上没有负向见证:'surfaces a rename failure in the notice area'(约 2353 行)通过 mockRejectedValue 走到 saveRename 的 catch 分支,但只断言错误文案,从不断言 expect(sessionCatalogController.renamed).not.toHaveBeenCalled()。saveRename 在成功与 catch 两条路径都已调用 refreshWorkspace(ownerCwd)(783、788 行),因此把两个同步调用合并的清理——例如把 renamed(...) 挪到与 refreshWorkspace 并列的共享 finally/catch——会在套件全绿的情况下通过(已验证:把 renamed 加进 catch 仍 90/90 绿)。失败的改名将用守护进程拒绝的值覆盖 catalog 缓存的显示名,标签一直错误直到下一次完整对账。请在失败测试中补上负向断言;把 renamed(...) 移入失败路径后该断言应变红(探针在该突变下观察到它被调用一次,参数 ['/w','s1','Renamed'])。
— qwen3.8-max via Qwen Code /review (v0.22.2)
…view-optimize # Conflicts: # packages/web-shell/client/App.test.tsx # packages/web-shell/client/components/sidebar/SessionDetailsTooltip.tsx
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Round summary — PR #10115 (session overview optimize)Base-conflict resolution was required this round:
Feedback dispositionsRound-2 Critical findings — fixed this round
Round-2 suggestions — implemented this round
Round-1 findings — verified as already resolved in code (prior fix commits
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- manageLiveState=true (panel-owned channel) direction unwitnessed — already reported in the round-2 deferred list (review 5039666410: 'App.tsx:12649 — manageLiveState is only witnessed in the false direction' and 'SessionOverviewPanel.test.t…
- canOpenSessionsOverview un-gating unwitnessed — already reported in the round-2 deferred list (review 5039666410: 'App.tsx:12183 — canOpenSessionsOverview un-gating has no witness')
Not explored to full depth (tool budget reached): chunk 6: execute SessionOverviewPanel.test.tsx under vitest to confirm the chunk's tests pass at the reviewed commit.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/components/SessionOverviewPanel.test.tsx:36 — [probe] capabilities mock typed as full DaemonCapabilities envelope; every fixture omits required fields (TS2739 invisible to CI)packages/web-shell/client/components/SessionOverviewPanel.tsx:171 — [review] readPageSize hand-lists the page sizes PAGE_SIZES already declares — remembered pagination silently resets if the list driftspackages/web-shell/client/components/ui/data-table.tsx:34 — [review] fixedOffset is a dead switch: declared and read at three sites, never set by any callerpackages/web-shell/client/components/ui/data-table.tsx:267 — [probe] hardcoded overflow-hidden beats the caller's overflow-y:auto only by CSS concatenation order (measured on the built bundle)packages/web-shell/client/components/SessionOverviewPanel.tsx:865 — [probe] export-failure notice branch has no witness; emptying the catch keeps 92/92 greenpackages/web-shell/client/components/SessionOverviewPanel.tsx:989 — [probe] filter-change page/selection reset effect unwitnessed; deleting it keeps 92/92 greenpackages/web-shell/client/App.test.tsx:18133 — [probe] onCurrentSessionRemoved entry-guard sessionId arm has no witness; removing it keeps 560/560 greenpackages/web-shell/client/App.test.tsx:18216 — [probe] panel-Back created && failure-path guard has no witness; dropping it keeps 560/560 greenpackages/web-shell/client/components/SessionOverviewPanel.test.tsx:2532 — [probe] other-workspaces poll suppression under live-state unwitnessed; always-LIST_POLL_MS mutant keeps 92/92 greenpackages/web-shell/client/components/SessionOverviewPanel.test.tsx:86 — [probe] removed useSessions mock export leaves a dangling reference for the real useWebShellSessionspackages/web-shell/client/components/ui/table.tsx:10 — [review] scroll axes split across nested containers: horizontal scrollbar renders at the bottom of full-height content, not the visible viewportpackages/web-shell/client/App.test.tsx:18133 — [probe] overview completion-guard workspace arm has no witness; removing it keeps all nine tests greenpackages/web-shell/client/App.tsx:5516 — [probe] approval effect force-closing the panel re-triggers the deferred split restore, replacing the approval overlay with the split noticepackages/web-shell/client/App.tsx:5516 — [probe] fold flag consumed by Goals/Scheduled-Tasks transitions silently discards the deferred restorepackages/web-shell/client/components/SessionOverviewPanel.tsx:882 — [probe] batch daemon error swallowed when the current-session clear fails; only newSessionFailed is shownpackages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx:2773 — [probe] archive enabled-state pin lost for pinned sessions in the dropdown migration; isPinned mutant keeps 82/83 green
Convergence: round 3 posted 5 inline comment(s), 4 of them reported for the first time; the previous round posted 7 (7 new). Findings keep coming back to the same files: packages/web-shell/client/components/SessionOverviewPanel.tsx (findings in round 2; 3 more now); packages/web-shell/client/App.tsx (findings in rounds 1, 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未探索到全部深度(达到工具调用预算):chunk 6:execute SessionOverviewPanel.test.tsx under vitest to confirm the chunk's tests pass at the reviewed commit。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 16 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 3 轮发布了 5 条行内评论,其中 4 条是首次提出;上一轮发布了 7 条(其中 7 条首次提出)。发现反复回到同一批文件:packages/web-shell/client/components/SessionOverviewPanel.tsx(第 2 轮已出过发现,本轮又有 3 条);packages/web-shell/client/App.tsx(第 1、2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if (splitFoldedByShrinkRef.current && !activePanel) { | ||
| splitFoldedByShrinkRef.current = false; |
There was a problem hiding this comment.
[Critical] R1-3: The stale-flag restore this thread was filed for still stands via sibling navigation paths. The round-2 fix cleared splitFoldedByShrinkRef only in handleSplitExit, handleOpenSessionFromOverview, and the panel-Back handler; the shared entry points createNewSession (New chat, workspace switch) and loadSidebarSession (sidebar session click) — plus onSelectCurrentSession — still close the panel without clearing it. With a split open, shrink below 1024px (the fold sets the flag), keep the overview open, grow back (restore deferred by !activePanel), then click New chat or a sidebar session: closePanel() runs, this effect re-runs on the activePanel dep with the stale flag, and mainView flips back to 'split' — the user lands on the old split while the fresh chat/session loads invisibly in the background.
Witness (probe on this commit):
× does not restore a folded split when the sidebar starts a new chat
→ expected <div class="_fullPage_cdbed7" …> to be null (split-view-page present after navigation)
× sidebar loads a session → same failure shape
with the flag cleared in createNewSession/loadSidebarSession: 18 passed (fold family green)
Clear the flag at the two shared entry points instead of enumerating call sites:
// in createNewSession, beside closePanel()
splitFoldedByShrinkRef.current = false;
// in loadSidebarSession, beside closePanel()
splitFoldedByShrinkRef.current = false;Leave the Escape dismiss-restore path untouched so the existing 'defers split restoration' test still pins it. Fix witness: add fold-family tests that leave the overview via sidebar New chat / session open and assert split-view-page stays absent — removing either flag-clear must turn the matching test red (the probe verified both directions).
中文说明
R1-3 仍然存在:本线程所报的过期标志恢复问题仍可通过其他导航路径触发。第二轮修复只在 handleSplitExit、handleOpenSessionFromOverview 和面板返回处理器中清除了 splitFoldedByShrinkRef;共享入口 createNewSession(新建聊天、切换工作区)与 loadSidebarSession(点击侧边栏会话)以及 onSelectCurrentSession 关闭面板时依然不清除该标志。分屏打开时收缩到 1024px 以下(折叠置位标志),保持总览打开,窗口恢复变大(!activePanel 延迟恢复),然后点击新建聊天或侧边栏会话:closePanel() 执行后,该 effect 因 activePanel 依赖重新运行,读到过期标志,把 mainView 翻回 'split'——用户落在旧分屏上,而新聊天/会话在后台无声加载。
修复:在两个共享入口处清标志(见上方代码),而不是逐一枚举调用点;保留 Esc 普通关闭的延迟恢复路径,以维持现有 'defers split restoration' 测试。见证测试:新增从侧边栏新建聊天/打开会话离开总览的用例,断言 split-view-page 保持不出现;移除任一清标志逻辑后该测试应变红(探针已双向验证)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| useEffect(() => { | ||
| setSelected((prev) => { | ||
| if (prev.size === 0) return prev; | ||
| const present = new Set(cards.map((card) => card.sessionId)); | ||
| let changed = false; | ||
| const next = new Set<string>(); | ||
| for (const id of prev) { | ||
| if (present.has(id)) next.add(id); | ||
| else changed = true; | ||
| } | ||
| return changed ? next : prev; | ||
| if (excludedWorkspaceCwds.size === 0) return; | ||
| const optionCwds = new Set(workspaceOptions.map((option) => option.cwd)); |
There was a problem hiding this comment.
[Critical] R2-2: (fix-induced) The round-2 fix closed the reported input — locking the panel to the excluded workspace now drops the exclusion (pinned by 'drops stale workspace exclusions when the panel locks to the excluded workspace') — but the pruning effect it added opened a new hole at the same site. It keeps any exclusion whose cwd is still in workspaceOptions, while the funnel popover that manages exclusions only renders when workspaceOptions.length > 1. When the option set shrinks to exactly one workspace — e.g. two primary-or-trusted workspaces A and B, the user excludes A via the funnel, then B loses trust (hot-reloadable) or drops out of the primary-or-trusted filter while staying registered — workspaceOptions becomes [A], the funnel disappears, and this effect keeps A's exclusion because A is still an option. Every remaining row belongs to A and is filtered out: the table shows 'No data' although sessions exist, with no visible control left to clear the filter, until remount. This violates the effect's own stated invariant ('drop any exclusion the user can no longer see or change').
Witness (probe on this commit):
PR code: funnelTrigger() === null, zero data rows, tbody holds only the "No data" placeholder
although the primary session exists
with the length gate below: the data row reappears
| useEffect(() => { | |
| setSelected((prev) => { | |
| if (prev.size === 0) return prev; | |
| const present = new Set(cards.map((card) => card.sessionId)); | |
| let changed = false; | |
| const next = new Set<string>(); | |
| for (const id of prev) { | |
| if (present.has(id)) next.add(id); | |
| else changed = true; | |
| } | |
| return changed ? next : prev; | |
| if (excludedWorkspaceCwds.size === 0) return; | |
| const optionCwds = new Set(workspaceOptions.map((option) => option.cwd)); | |
| useEffect(() => { | |
| if (excludedWorkspaceCwds.size === 0) return; | |
| if (workspaceOptions.length <= 1) { | |
| setExcludedWorkspaceCwds((prev) => | |
| prev.size === 0 ? prev : new Set<string>(), | |
| ); | |
| return; | |
| } | |
| const optionCwds = new Set(workspaceOptions.map((option) => option.cwd)); |
(keep the existing filter/guard below unchanged). Fix witness: render two workspaces, exclude /w through the funnel, re-render capabilities with /wsB trusted: false (still registered); assert the funnel button is absent and /w's rows render again — removing the length <= 1 gate must turn it red (the probe verified both directions).
中文说明
R2-2(修复引入):第二轮修复已关闭原始报告的输入——面板被锁定到已排除的工作区时,排除项会被清除(由 'drops stale workspace exclusions when the panel locks to the excluded workspace' 钉住)——但新增的对账 effect 在同一位置开了新洞:它会保留任何仍在 workspaceOptions 中的排除项,而管理排除项的漏斗弹层只在 workspaceOptions.length > 1 时渲染。当选项集收缩到恰好一个工作区时(例如两个主或可信工作区 A、B,用户通过漏斗排除 A,随后 B 失去信任(可热更新)或离开主/可信过滤但仍注册),workspaceOptions 变为 [A],漏斗消失,而该 effect 因 A 仍是选项而保留 A 的排除项。剩余所有行都属于 A,全部被过滤:会话明明存在,表格却显示 'No data',且没有任何可见控件能清除该过滤,只能重新挂载。这违反了该 effect 自己声明的不变量('丢弃用户不再能看见或修改的排除项')。
修复见上方 suggestion:选项数 <= 1 时清空排除集。见证测试:渲染两个工作区,通过漏斗排除 /w,再以 /wsB trusted: false(仍注册)重新渲染能力;断言漏斗按钮消失且 /w 的行重新出现;移除 length <= 1 门后应变红(探针已双向验证)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| @@ -12738,8 +12767,49 @@ export function App({ | |||
| <SessionOverviewPanel | |||
| onOpenSession={handleOpenSessionFromOverview} | |||
| onOpenSplit={openSplitView} | |||
There was a problem hiding this comment.
[Critical] Removing the isLargeScreen gate from canOpenSessionsOverview makes the Session Overview — and this Open in split action — reachable below the 1024px breakpoint where split view cannot exist. openSplitView is passed unconditionally and the panel renders the button whenever the prop is present (SessionOverviewPanel.tsx:1642), while the toolbar entry stays gated on canOpenSplitView={isLargeScreen}. Below the breakpoint the click closes the panel, sets mainView='split', and the shrink-fold effect immediately folds it back — the user lands on bare chat with no split and no error. If the chat had no session, the fold's landing branch silently repoints the connection to the first selected pane (App.tsx:5541-5544); the fold also sets splitFoldedByShrinkRef and the sibling save effect persists the pane set in the same commit, so growing the window later restores a split the user never effectively had, and a refresh re-enters the same fold loop. Before this diff the overview was large-screen-only, so this path was unreachable.
Witness (probe on this commit):
below the breakpoint: overview opens; onOpenSplit(['s1','s2']) → no split-view-page (silent fold)
grow back → split-view-page resurrected → × expected <div class="_fullPage_cdbed7"> to be null
with onOpenSplit={isLargeScreen ? openSplitView : undefined}: 15 passed
| onOpenSplit={openSplitView} | |
| onOpenSplit={isLargeScreen ? openSplitView : undefined} |
The panel already hides the button when the prop is undefined. Fix witness: render below the breakpoint via the existing matchMedia mock, open the overview, invoke the captured latestSessionOverviewProps.onOpenSplit(['s1','s2']), grow past the breakpoint and assert split-view-page never appears (and the connection was not swapped) — removing the guard must turn it red.
中文说明
从 canOpenSessionsOverview 移除 isLargeScreen 门后,会话总览——以及这个"在分屏中打开"操作——在分屏根本无法存在的 1024px 以下断点处也变得可达。openSplitView 被无条件传入,面板只要收到该 prop 就渲染按钮(SessionOverviewPanel.tsx:1642),而工具栏入口仍受 canOpenSplitView={isLargeScreen} 约束。断点以下点击会关闭面板、把 mainView 设为 'split',收缩折叠 effect 立刻把它折回——用户落在没有分屏、也没有任何错误的纯聊天界面上。若聊天没有会话,折叠的落地分支会把连接悄悄切换到第一个选中的面板(App.tsx:5541-5544);折叠还会置位 splitFoldedByShrinkRef,同一次提交中相邻的保存 effect 会把面板集持久化,于是之后窗口变大时会恢复一个用户实际从未拥有的分屏,刷新页面还会重新进入同样的折叠循环。本 diff 之前总览仅大屏可用,此路径不可达。
修复见上方 suggestion:按大屏条件传入 onOpenSplit(prop 为 undefined 时面板自动隐藏按钮)。见证测试:用现有 matchMedia mock 在断点以下渲染,打开总览,调用捕获到的 onOpenSplit(['s1','s2']),再放大超过断点,断言 split-view-page 始终不出现(且连接未被切换);移除该守卫后测试应变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| const { sessions, loading, error, reload } = useScopedSessions(workspaceCwd, { | ||
| autoLoad: true, | ||
| pollIntervalMs: LIST_POLL_MS, | ||
| pollIntervalMs: liveStateEnabled ? undefined : LIST_POLL_MS, |
There was a problem hiding this comment.
[Critical] The two ends of the refresh-mode invariant read different configs: the live-state subscription honours manageLiveState (enabled: manageLiveState && liveStateEnabled, line 391) but every polling-suppression path keys off liveStateEnabled alone — this list poll, the other-workspaces poll (line 411), useStatusReport autoLoad (line 430) and the status interval (line 438) — while the actual manager, the sidebar, covers a narrower workspace set (entry.trusted && isAbsolutePath, WebShellSidebar.tsx:981-985) than this panel's liveStateWorkspaceCwds (entry.primary || entry.trusted, line 382). Daemon advertises workspace_session_live_state, the primary entry is { primary: true, trusted: false } (a real state: serve/server.ts:1244 sets trusted: deps.primaryWorkspaceTrusted ?? false), sidebar enabled → App passes manageLiveState=false: this panel suppresses all four of its own polls for a channel it does not run, the sidebar's trusted-only channel excludes the primary, and nothing refreshes the primary workspace's catalog or volatile flags. The overview loads once and silently freezes — running/idle indicators stop moving, new sessions never appear, needs-approval sessions never surface — until the user happens to click Refresh. Before this diff the panel polled unconditionally.
Witness (probe on this commit):
× keeps its own catalog poll when it does not own the live-state channel
→ expected undefined to be 3000 (pollIntervalMs suppressed; enabled === false asserted green in the same test)
with suppression gated on manageLiveState && liveStateEnabled: probe green
sole casualty: the pre-existing pinning test 'uses sidebar-owned live state without starting a duplicate channel', as predicted
Gate the suppression on effective ownership — e.g. const liveStateActive = manageLiveState && liveStateEnabled; used at all four sites — or align this target set with the manager's (entry.trusted + absolute-path only). Fix witness: render capabilities { features: ['workspace_session_live_state'], workspaces: [{ id: 'w0', cwd: '/w', primary: true, trusted: false }] } with manageLiveState: false and assert the captured pollIntervalMs is 3000 — removing the ownership gate must turn it red; update the pinning test above to match whichever rule is chosen.
中文说明
刷新模式不变量的两端读取了不同的配置:live-state 订阅尊重 manageLiveState(enabled: manageLiveState && liveStateEnabled,391 行),但所有轮询抑制路径都只看 liveStateEnabled——本列表轮询、其他工作区轮询(411 行)、useStatusReport autoLoad(430 行)与状态间隔轮询(438 行)——而真正的管理者侧边栏覆盖的工作区集合(entry.trusted && isAbsolutePath,WebShellSidebar.tsx:981-985)比本面板的 liveStateWorkspaceCwds(entry.primary || entry.trusted,382 行)更窄。守护进程通告 workspace_session_live_state、主工作区条目为 { primary: true, trusted: false }(真实状态:serve/server.ts:1244 trusted: deps.primaryWorkspaceTrusted ?? false)、侧边栏启用时,App 传入 manageLiveState=false:本面板为自己并不运行的通道抑制了全部四条轮询,侧边栏的仅可信通道又排除了主工作区,主工作区的目录与易变字段没有任何组件刷新。总览加载一次后便无声冻结——运行/空闲指示停止变化、新会话不出现、需要审批的会话不浮现——直到用户恰好点击刷新。本 diff 之前面板无条件轮询。
修复:把抑制条件改为有效所有权(如 const liveStateActive = manageLiveState && liveStateEnabled; 用于四处),或把目标集对齐管理者(仅 entry.trusted + 绝对路径)。见证测试:以上述不可信主工作区能力、manageLiveState: false 渲染,断言捕获的 pollIntervalMs 为 3000;移除所有权门后应变红;同时按所选规则更新上方钉住现行行为的测试。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| const update = () => { | ||
| const naturalHeight = | ||
| panel.scrollHeight - | ||
| tableViewport.clientHeight + | ||
| tableViewport.scrollHeight; | ||
| setFooterSticky(naturalHeight > viewport.clientHeight + 1); | ||
| }; |
There was a problem hiding this comment.
[Critical] This sticky-footer decision is a self-referential feedback loop. Sticky mode changes the footer's own layout height by −3px net (pt-3 +12px, border-t +1px, -mb-4 −16px), so the reconstructed naturalHeight is N in non-sticky mode and N−3 in sticky mode — while the single threshold has only +1px of hysteresis. For content heights in (viewport+1, viewport+4]px the footer flips every cycle: engage at N > P+1, release at N−3 ≤ P+1, and each flip changes the boxes the ResizeObserver watches, re-firing update(). Additionally, the threshold compares against the scroller's border-box clientHeight (32px of vertical padding included), so panelBody starts scrolling up to ~33px before sticky ever engages — within that window the pagination/bulk-action footer, the feature's point, scrolls out of view. The existing test holds panel.scrollHeight fixed across the mode flip, masking exactly this feedback.
Witness (probe driving this component's own effect with the PR's CSS constants; jsdom computes no layout, geometry from the module's CSS values):
PR code: cycle0 mode=auto natural=403 threshold=401 → sticky
cycle1 mode=sticky natural=400 threshold=401 → auto
cycle2 → sticky … alternating on all 10 cycles
with the hysteresis below: cycle0 → sticky, cycles 1–9 stay sticky
| const update = () => { | |
| const naturalHeight = | |
| panel.scrollHeight - | |
| tableViewport.clientHeight + | |
| tableViewport.scrollHeight; | |
| setFooterSticky(naturalHeight > viewport.clientHeight + 1); | |
| }; | |
| const update = () => { | |
| const naturalHeight = | |
| panel.scrollHeight - | |
| tableViewport.clientHeight + | |
| tableViewport.scrollHeight; | |
| setFooterSticky( | |
| (prev) => naturalHeight > viewport.clientHeight + (prev ? -4 : 1), | |
| ); | |
| }; |
(engage at N > P+1, release only at N ≤ P−1 — no overlapping band; alternatively measure the footer without its sticky decorations so both modes compute the same natural height, and compare against the content box rather than clientHeight). Fix witness: extend 'sticks and separates the footer only while the panel overflows' — with N = viewport+2 fire the captured ResizeObserver callback and assert sticky; restub to the sticky-mode geometry (natural height N−3), fire again, and assert still sticky — removing the hysteresis must turn it red.
中文说明
这个粘性页脚判定是一个自引用反馈循环。粘性模式会改变页脚自身的布局高度,净变化 −3px(pt-3 +12px、border-t +1px、-mb-4 −16px),因此重建出的 naturalHeight 在非粘性模式是 N、粘性模式是 N−3——而单一阈值只有 +1px 的滞回。内容高度落在 (视口+1, 视口+4]px 区间时页脚每个周期都翻转:N > P+1 时进入粘性,N−3 ≤ P+1 时退出,而每次翻转都会改变 ResizeObserver 观察的盒子尺寸,从而再次触发 update()。另外,阈值比较的是滚动容器的 border-box clientHeight(含 32px 垂直内边距),所以 panelBody 会在粘性生效前最多提前 ~33px 开始滚动——在这个窗口内,分页/批量操作页脚(该功能的意义所在)会滚出视野。现有测试在模式翻转期间固定 panel.scrollHeight,恰好掩盖了这个反馈。
修复见上方 suggestion:把滞回放宽到大于模式差(进入 N > P+1、退出 N ≤ P−1,两区间不再重叠);或者测量不含粘性装饰的页脚高度使两种模式算出相同的 naturalHeight,并改用内容盒而非 clientHeight 比较。见证测试:扩展现有粘性测试——N = 视口+2 时触发捕获的 ResizeObserver 回调断言粘性;再按粘性模式几何(natural 为 N−3)重设 stub、再次触发、断言仍为粘性;移除滞回后应变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Round summary — PR #10115 (round 3 feedback)All five inline Critical findings are Resolved in code, each reproduced with a Feedback points and dispositions
VerificationEnvironment note: the checkout's
中文说明轮次总结 — PR #10115(第 3 轮反馈)五条行内 Critical 发现全部已在代码中解决。每一条都先以失败测试复现,再由见证测试钉住;移除对应修复时见证测试会变红(突变探针验证,见下方验证部分)。单个约定式提交: 反馈点与处置
验证环境说明:检出目录的
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
|
Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with |
…ze' into worktree-session-overview-optimize
chiga0
left a comment
There was a problem hiding this comment.
所有三轮 C 级 blocking 已在 1c2f558b8ec4("address session overview round-3 review feedback")中全部修复,下面逐条核实。
历轮 C 级发现收敛状态
| ID | 文件 | 一句话摘要 | 本轮状态 |
|---|---|---|---|
| R1-3 | App.tsx | splitFoldedByShrinkRef 未在 sidebar 导航时清零,shrink-fold 恢复会通过 new-chat / load-session 重触 |
fixed — createNewSession(!keepPanel) 和 loadSidebarSession 均补写 splitFoldedByShrinkRef.current = false;新增两条测试覆盖两条路径 |
| R2-1 | App.tsx | onCurrentSessionRemoved 向 createNewSession 传入可能为空的 workspace |
fixed — removed.workspaceCwd || undefined 将空字符串转为 undefined;currentWorkspaceCwd 回退链补全 |
| R2-2 | SessionOverviewPanel.tsx | workspace 选项缩减到 1 个时已有 exclusion 仍生效,致全行隐藏 | fixed — workspaceOptions.length <= 1 时无条件清空 excludedWorkspaceCwds;测试:"drops exclusions when the option set shrinks below the funnel threshold" |
| R3-1 | App.tsx | 移除 isLargeScreen 门控后 split 动作在窄屏也传入 panel,用户激活后 grow 时幻 split 出现 |
fixed — onOpenSplit={isLargeScreen ? openSplitView : undefined};测试:"does not hand out the overview split action below the large-screen breakpoint" |
| R3-2 | SessionOverviewPanel.tsx | live-state channel 用 liveStateEnabled 判断,polling 也用同一变量 — 导致 manageLiveState=false 时 panel 不轮询,sidebar 的 trusted-only channel 无法覆盖未信任 workspace |
fixed — 引入 liveStateActive = manageLiveState && liveStateEnabled,所有 polling gate 改用此变量;测试:"keeps its own catalog poll when it does not own the live-state channel" |
| R3-3 | SessionOverviewPanel.tsx | sticky footer 判断是自我指涉反馈环:切换 sticky 改变 scrollHeight,再触发 observer,导致死循环 |
fixed — 非对称迟滞:setFooterSticky(prev => naturalHeight > viewport.clientHeight + (prev ? -4 : 1));测试:"holds the sticky footer through its own mode-switch height delta"(三段高度探针) |
我独立验证的实现路径
跨 workspace 行标识:getRowId = card => \${card.workspaceCwd}\0${card.sessionId}` 用 null byte 做分隔符,保证任意 cwd + sessionId 组合唯一。isCurrentSession` 两字段都核对(无 cwd 时退化为仅 ID 匹配,兼容单 workspace 旧场景)。✅
活跃会话安全:mutateCards 先收集 succeededIdentities,只在 操作成功的 当前会话上调用 onCurrentSessionRemoved,失败项不触发创建新会话。isCurrentCard 在异步结束后读 connectionRef.current,用户操作期间切换会话时结论仍正确。✅
分页稳定性:autoResetPageIndex: false + 手动 clamp effect(仅当 pageIndex > lastPageIndex 时才 setState)防止数据更新导致空页。✅
右固定列降级:pinRightColumns = availableWidth === 0 || availableWidth > leftFixedWidth + rightFixedWidth,视口过窄时右侧固定区域退化为普通流。data-table.test.tsx 中 NarrowFixedTableHarness(350 px)覆盖此路径。✅
sticky footer 迟滞:sticky 模式自身装饰 (pt-3 +12px, border-t +1px, -mb-4 -16px) 净缩 -3px。门槛从 P+1 升入 sticky,在 sticky 下须降到 P-4 才释放(即 P - 3 - 1),避免 ±1 内抖动触发重观察。✅
CI
Test (ubuntu-latest, Node 22.x)失败于 "Run .github/scripts helper tests" — 已确认为跨 PR 预存故障(同日多个无关 PR 的 CI 中复现相同步骤失败),与本 PR 变更无关。Post Coverage Comment— 依赖前一步,次生失败。- 产品相关 check 全绿:
web-shell E2E Smoke✅、Desktop Shell(ubuntu / windows)✅、Java CI ✅、Dependency CVE audit ✅、Secret scan ✅。
剩余延后项
Round 3 延后列表(16 条,全为 probe/review 级)无一升级为 blocking:
readPageSize独立硬编码有效集合(不随PAGE_SIZES联动)— 维护备注,非当下 bug。fixedOffset字段声明后从未被 caller 赋值 — 无运行时影响。- 若干
[probe]条目指出守卫分支无测试 — 覆盖债务,逻辑结构正确。
|
@qwen-code /takeover stop |
|
👋 Takeover released: the autofix loop will no longer engage this PR (an in-flight round, if any, completes its bounded work). Re-apply 中文说明👋 已释放:autofix 循环不再介入此 PR(在飞的一轮如有,将完成其有界工作)。重新打上 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- capabilities fixtures typed as full DaemonCapabilities envelope with required fields omitted (SessionOverviewPanel.test.tsx) — already reported in the round-3 deferred list (review 5044066726)
- fixedOffset dead switch in packages/web-shell/client/components/ui/data-table.tsx — already reported in the round-3 deferred list (review 5044066726)
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/App.tsx:12864 — [review] workspace-cwd fallback chain inlined four times across the two new handlerspackages/web-shell/client/App.test.tsx:18391 — [probe] onCurrentSessionRemoved's pre-await stale-sessionId clause is pinned by no testpackages/web-shell/client/App.test.tsx:20304 — [review] fold-restore cancellation test has no positive control for its onOpenSession triggerpackages/web-shell/client/App.test.tsx:20605 — [review] below-breakpoint split test's stale-capture act block is a guaranteed no-oppackages/web-shell/client/components/SessionOverviewPanel.test.tsx:349 — [review] footerButton helper's container-wide fallback unpins footer placement of bulk actionspackages/web-shell/client/components/SessionOverviewPanel.test.tsx:520 — [probe] empty-state error branch ('Failed to load sessions') has no witnesspackages/web-shell/client/components/SessionOverviewPanel.test.tsx:799 — [review] session-ID copy test pins only the success path; the clipboard-rejection branch is untestedpackages/web-shell/client/components/SessionOverviewPanel.test.tsx:814 — [review] copy-test cleanup permanently destroys the shared navigator.clipboard polyfillpackages/web-shell/client/components/SessionOverviewPanel.test.tsx:882 — [probe] cross-workspace equal-session-id independence test has no delete-path twinpackages/web-shell/client/components/SessionOverviewPanel.test.tsx:1799 — [review] delete branch's notFound handling for the current session has no testpackages/web-shell/client/components/SessionOverviewPanel.tsx:1686 — [probe] page-size persistence write path (onPageSizeChange={writePageSize}) has no testpackages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx:3233 — [probe] handler-level running-session archive guard left unexercised by the dropdown migrationpackages/web-shell/client/components/ui/data-table.tsx:410 — [probe] DataTable renders Radix Tooltip without a TooltipProvider; a future meta.tooltip consumer crashes at renderpackages/web-shell/client/utils/workspace.ts:56 — [review] isNonPrimaryWorkspaceSession is dead code — this PR removed its last consumer
Convergence: round 5 posted 2 inline comment(s), 2 of them reported for the first time. Findings keep coming back to the same files: packages/web-shell/client/components/SessionOverviewPanel.tsx (findings in rounds 2, 3; 2 more now). (Evidence: the previous round was recovered from a marker this account did not post and merged over this account's own entries, so some of those rounds may not be this account's own.) A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 14 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 5 轮发布了 2 条行内评论,其中 2 条是首次提出。发现反复回到同一批文件:packages/web-shell/client/components/SessionOverviewPanel.tsx(第 2、3 轮已出过发现,本轮又有 2 条)。(证据说明:上一轮的数据来自并非本账号发布的标记,并与本账号自己的条目合并,上述轮次中的部分可能不属于本账号。)一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 868 passed · 0 failed · 868 total Flakiness gate: ✅ 6 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:868 通过 · 0 失败 · 868 总计 抖动门:✅ 6 changed test file(s) x 5 identical rounds, no divergence Verification reportPR 10115 Deep Verification — feat(web-shell): optimize session overview tableVerdict: 中文摘要
Central claimThe PR rewrites Session Overview from a card grid into a data table and, in doing so, adds destructive
Because the component is a wholesale rewrite, the A/B control cells are mutation reverts of the head
Witnesses: The M3/M4 reds are the head-vs-base behavioral flip for the active-session flow: with the base Back Race-safety siblings of the same mechanism are also pinned and green at head: does not clear a Secondary claims, verified
Reviewer Test Plan, per step
FindingsF1 — Suggestion (completeness, not a merge condition): the F2 — Nice to have: pagination persistence is pinned on the read side only. Tests seed F3 — Note (pre-existing, not PR-attributed): EN/ZH key drift of 130 keys. ZH contains 130 keys F4 — Note: No injection-style instructions were present in the PR metadata; the PR text was treated as hypothesis Not covered
MethodologyRan in the CI verify container ( Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 896 passed · 0 failed · 896 total Flakiness gate: ✅ 6 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:896 通过 · 0 失败 · 896 总计 抖动门:✅ 6 changed test file(s) x 5 identical rounds, no divergence Verification reportSandboxed verification: ✅ passed — merge-ready (agent verdict) — follow-up round at an identical head. Scripted assertions: 896 passed · 0 failed · 896 total. This is a follow-up round: the previous round verified the same merge commit ( 中文 — 判定:✅ 通过 · 可合入(agent 判定,复核轮)
Previous-finding status table (follow-up round)Head is byte-identical to the prior round's (same merge commit), so "stands" rows below are backed by re-executed measurements, not by diffing the old report.
Central claimUnchanged from the prior round, re-proven live: the rewrite moves destructive session actions (archive/delete, single + bulk) onto a surface that was read-only on base, and carries the promised safeguards — (a) archive/delete never mutate without a confirmation dialog; (b) when the removed session is the active one, the app clears into a new-task state without closing the overview; Back from the overview clears the active session into a new-task state. Control cells are mutation reverts of head hunks (M4 is the byte-level base hunk), each required to turn the head-green suite red on the intended assertion, with a same-file positive control (M5) and an A/A control proving the symlinked-dependency mutation environment changes nothing.
Witnesses: The M3/M4 reds are the head-vs-base behavioral flip for the active-session flow; with the base Back hunk restored (M4), exactly the four tests documenting the new Back semantics fail, and with the App handler removed (M3), exactly the five clear-the-active-session tests fail — the other 568–569 tests stay green in both cells, so the mutations are scoped to the claimed behavior. Race-safety siblings remain pinned and green at head: does not clear a different session after an overview mutation finishes, does not clear a newly selected session when it finishes, does not clear a session opened while leaving the Session Overview, clears a deleted current session after a partial batch failure, does not clear the current session when it fails (Archive and Delete). Secondary claims, re-verified
Reviewer Test Plan, per step
FindingsNo new findings this round. F1–F4 carry forward with statuses in the table above; none is blocking. F1 remains the only completeness gap worth an author's attention: a direct test calling the Not covered
MethodologyRan in the CI verify container ( — Qwen Code · sandboxed verification Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Review notes (round 5 — head 3 new commits since round 4 ( Prior findings: All prior blockers (R1–R4) confirmed fixed — see review 5050798024. New commits reviewed: stabilize session overview navigation:
restore session overview page scrolling:
preserve session overview safeguards:
CI: No blocking findings. Approved at head Reviewed with AI assistance. |

















What this PR does
Reworks Session Overview into a reusable, responsive data table with proportional columns on wide screens, horizontal scrolling and fixed edge columns on narrow screens, consistent hover backgrounds, fixed-column shadows, tooltips, sorting, workspace filtering, remembered pagination, row selection, and confirmation dialogs for destructive actions. Session rows retain rename, export, archive, delete, new-tab, and split-view workflows, including safe handling when the active session is renamed, archived, deleted, or cleared by navigating back.
Screenshot
The shared table primitive owns column sizing, truncation, tooltip wrapping, fixed positioning, scroll shadows, empty states, and pagination so future Web Shell tables can reuse the same behavior. The sidebar archive action moves into the existing More menu.
Why it's needed
The previous card layout did not scale well for larger session catalogs and duplicated behavior that other tables may need. This change makes dense session management easier to scan and operate while preserving existing session actions and keeping narrow-window behavior usable.
Reviewer Test Plan
How to verify
Open Session Overview from the sidebar and confirm that wide layouts distribute non-fixed columns proportionally without horizontal scrolling. Narrow the window and confirm horizontal scrolling appears, the selection/title columns and action column remain fixed with synchronized row hover backgrounds, and edge shadows appear before scrolling when overflow exists.
Verify search, multi-select workspace filtering, time sorting, 10/50/100 page sizes with the last choice restored, row selection by clicking outside the title, title-only session switching, session ID copying, Worktree/PR details, and empty results rendered inside the table.
Verify rename, export, archive, and delete actions for supported primary and trusted workspace sessions. Archive and delete must require confirmation; operating on the active session or using Back from Session Overview must clear the active session into a new-task state without closing the overview during an in-panel mutation.
Evidence (Before & After)
Before: Session Overview used a card grid with limited dense-table behavior.
After: The screenshot in the summary shows the responsive table, fixed action controls, workspace filtering, Worktree and PR metadata, selection actions, and pagination.
Tested on
Environment (optional)
Local Web Shell daemon development environment. Verified 106 focused unit tests covering the table, Session Overview, tooltip, sidebar action visibility, and App integration flows; Web Shell TypeScript type checking and i18n consistency checks also pass.
Risk & Scope
Linked Issues
N/A
中文说明
本 PR 的改动
将会话总览重构为可复用的响应式数据表格:宽屏下按比例分配列宽,窄屏下启用水平滚动和两侧固定列,并统一行 Hover 背景、固定列阴影、Tooltip、排序、工作区筛选、记住的分页、行选择以及危险操作确认弹窗。会话行保留改名、导出、归档、删除、新标签页打开和分屏打开流程,并在当前会话被改名、归档、删除或通过返回操作清空时进行安全处理。
通用表格基础组件统一承担列宽计算、内容缩略、Tooltip 包装、固定定位、滚动阴影、空状态和分页能力,便于后续 Web Shell 表格复用相同行为。侧边栏的归档操作收入已有的“更多”菜单。
改动原因
原有卡片布局不利于大量会话的展示,也无法复用其他表格可能需要的行为。本改动让高密度会话管理更容易浏览和操作,同时保留已有会话能力,并保证窄窗口下仍可使用。
Reviewer 测试计划
验证方式
从侧边栏打开会话总览,确认宽布局下非固定列按比例分配且不出现水平滚动。缩小窗口后确认出现水平滚动,选择/标题列和操作列保持固定,行 Hover 背景同步,并在存在溢出时于用户滚动前就显示边缘阴影。
验证搜索、工作区多选筛选、时间排序、10/50/100 分页并记住上次选择、点击标题外的行区域选中、仅点击标题切换会话、复制会话 ID、Worktree/PR 详情,以及在表格内展示空结果。
对受支持的主工作区和可信工作区会话验证改名、导出、归档和删除。归档和删除必须经过确认;对当前会话执行操作,或从会话总览点击返回时,必须清空当前会话并进入新建任务状态,而在面板内操作时不关闭会话总览。
证据(改动前后)
改动前:会话总览使用卡片网格,密集表格能力有限。
改动后:Summary 中的截图展示了响应式表格、固定操作区、工作区筛选、Worktree 和 PR 元数据、选择操作以及分页。
测试平台
环境(可选)
本地 Web Shell daemon 开发环境。已通过 106 个定向单元测试,覆盖表格、会话总览、Tooltip、侧边栏操作可见性和 App 集成流程;Web Shell TypeScript 类型检查和 i18n 一致性检查也已通过。
风险与范围
关联 Issue
无