Skip to content

fix(web-shell): keep sidebar sessions synchronized - #9533

Merged
ytahdn merged 1 commit into
QwenLM:mainfrom
ytahdn:codex/web-shell-sidebar-refresh
Aug 20, 2026
Merged

fix(web-shell): keep sidebar sessions synchronized#9533
ytahdn merged 1 commit into
QwenLM:mainfrom
ytahdn:codex/web-shell-sidebar-refresh

Conversation

@ytahdn

@ytahdn ytahdn commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This change makes Web Shell sidebar mutations reconcile the affected workspace immediately after creating, renaming, deleting, archiving, pinning, grouping, or recoloring sessions. User-triggered retries can bypass transient catalog backoff, while background refreshes continue to respect error backoff and coalesce repeated work. It also adds spacing around the session loading indicator and right-side padding when no status icon is present.

Why it's needed

Sidebar state could remain stale when another client changed the same workspace or when a mutation updated data outside the currently cached page. Treating every internal refresh as a manual retry also caused avoidable request bursts during daemon failures. The new distinction keeps explicit user actions responsive without weakening background rate limiting.

Reviewer Test Plan

How to verify

Perform sidebar operations such as rename, pin or unpin, archive or unarchive, delete, group assignment, and color changes, then confirm the affected workspace is fully refreshed. Simulate live-state and catalog failures and confirm background refreshes wait for the retry window while clicking a visible Retry control triggers an immediate attempt. Trigger a refresh while a live-state request is already in flight and confirm the refresh is neither lost nor duplicated. Verify loading rows and rows without right-side icons retain comfortable right spacing.

Evidence (Before & After)

Before: mutation results could remain stale until a later poll, internal refreshes could bypass failure backoff, and session metadata could sit too close to the loading indicator or right edge.

After: sidebar mutations and explicit retries reconcile immediately, background refreshes remain rate-limited during failures, and the session metadata spacing is consistent.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Local workspace with the Web Shell Vitest suite, repository lint, typecheck, and build.

Risk & Scope

  • Main risk or tradeoff: Explicit sidebar mutations perform an additional workspace reconciliation by design; background and lifecycle-driven refreshes remain rate-limited.
  • Not validated / out of scope: Visual confirmation on Windows and Linux; non-sidebar session management flows are unchanged.
  • Breaking changes / migration notes: None.

Linked Issues

N/A

中文说明

此 PR 做了什么

此改动让 Web Shell 侧边栏在新建、重命名、删除、归档、置顶、分组或修改会话颜色后立即对受影响的工作区执行完整同步。用户主动点击重试时可以绕过临时的目录退避,而后台刷新仍会遵守错误退避并合并重复工作。同时调整会话 loading 图标周围的间距,并在右侧没有状态图标时增加留白。

为什么需要

当其他客户端修改同一工作区,或一次操作更新了当前缓存分页之外的数据时,侧边栏状态可能一直过期到后续轮询。此前把所有内部刷新都视为手动重试,也会在 daemon 故障时产生不必要的请求突发。新的区分让用户操作保持即时响应,同时不削弱后台限流。

Reviewer Test Plan

如何验证

执行重命名、置顶或取消置顶、归档或取消归档、删除、分组和颜色修改等侧边栏操作,确认受影响的工作区会被完整刷新。模拟 live-state 和目录请求失败,确认后台刷新会等待重试窗口,而点击可见的 Retry 控件会立即发起请求。在 live-state 请求进行中触发刷新,确认该刷新既不会丢失也不会重复执行。确认 loading 行以及右侧没有图标的行都保留合适的右侧间距。

证据(Before & After)

Before:操作结果可能在后续轮询前保持过期,内部刷新可能绕过故障退避,会话内容也可能靠 loading 图标或右边缘过近。

After:侧边栏操作和显式重试会立即同步,后台刷新在故障期间仍受限流保护,会话内容间距保持一致。

测试平台

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

本地工作区,运行 Web Shell Vitest 完整测试、仓库 lint、typecheck 和 build。

风险与范围

  • 主要风险或权衡:显式的侧边栏变更会按设计额外执行一次工作区同步;后台和生命周期驱动的刷新仍然受限流保护。
  • 未验证或范围外:未在 Windows 和 Linux 上进行视觉确认;非侧边栏的会话管理流程保持不变。
  • 破坏性变更或迁移说明:无。

关联 Issue

N/A

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on dcb34ca and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— dcb34ca 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: this one checks out against the code, not just the description. Today invalidateWorkspace records an invalidated live-state request without waking the poll loop, and the reconcile that follows waits for the next 2s tick, then stands behind the 30s error backoffs (liveRetryAt / reconcileRetryAt) and the 10s invalidation cooldown. So after a successful sidebar mutation the row can lag for a couple of seconds on a healthy daemon, and up to 30s+ while the daemon is failing. That is a structural staleness window, not a hypothetical. One honest caveat: there is no linked issue or user report — the evidence is the mechanism itself plus the regression tests in this PR.

Direction: aligned. Sidebar responsiveness is core Web Shell surface, and the change stays inside the session-catalog layer — no auth/sandbox/model-selection/telemetry/release or public-contract impact.

Size: no core-module paths touched (packages/web-shell/client/** only). Roughly 164 production lines (including 12 lines of CSS) vs ~191 lines of tests. No escalation thresholds met.

Approach: the scope feels right — threading the existing interactive refresh kind through the mutation paths and letting explicit user actions bypass error backoff while background traffic stays rate-limited is the natural extension of the current architecture. Two observations, neither blocking: the diff also carries a small CSS spacing tweak (loading indicator margins, right padding on empty meta slots) that is unrelated to synchronization — small enough to live here, but worth calling out; and the fix is deliberately sidebar-scoped, so other mutation surfaces (task-panel rename, scoped deletes) keep the rate-limited path for now.

Risk: no elevated risk signals — none of the changed files match the revert-correlated paths.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这个问题在代码层面可以确认,而不只是描述成立。目前 invalidateWorkspace 只会记录一个 invalidated 的 live-state 请求,不会唤醒轮询循环;随后的 reconcile 要等下一个 2s tick,并且排在 30s 错误退避(liveRetryAt / reconcileRetryAt)和 10s invalidation 冷却之后。因此一次成功的侧边栏操作之后,列表在 daemon 正常时会滞后数秒,在 daemon 故障时可能滞后 30s 以上。这是结构性的过期窗口,不是假设性问题。一个诚实的说明:没有关联 issue 或用户报告——证据是机制本身加上 PR 中的回归测试。

方向:对齐。侧边栏响应速度是 Web Shell 的核心界面,改动限制在 session-catalog 层内——不涉及 auth/sandbox/模型选择/telemetry/release 或公共契约。

规模:未触及核心模块路径(仅 packages/web-shell/client/**)。约 164 行生产代码(含 12 行 CSS),约 191 行测试。未达到任何升级阈值。

方案:范围合理——把已有的 interactive 刷新类型接入变更路径,让显式用户操作绕过错误退避、后台流量继续受限流,是对现有架构的自然扩展。两点非阻塞观察:diff 里夹带了一处与同步无关的小 CSS 间距调整(loading 指示器边距、空 meta 槽的右侧留白)——体量小可以留在这里,但值得点明;另外修复刻意限定在侧边栏,其他变更入口(task-panel 重命名、scoped 删除)暂时仍走限流路径。

风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Before reading the diff, my independent take from the description was: reuse the store's existing interactive refresh kind for user-triggered mutations, add a retry-backoff bypass that only explicit actions can set, and make sure a refresh requested while a live-state fetch is already in flight is not swallowed by the inFlight early return. That is exactly what this PR does, so the approach matches my baseline — no simpler path was missed.

Traced the concurrency paths and found no blockers:

  • The interactiveRefreshRequested flag is only cleared after the backoff check passes, and the early returns (disposed / inFlight / document.hidden) leave it set, so a request that arrives at an awkward moment survives to the next eligible poll. finish() re-polls synchronously after inFlight drops, and since poll sets inFlight before its first await, there is no double-poll race.
  • Rapid repeat clicks coalesce on the boolean flag plus one pending interactive request — no request burst, which is the point of keeping background backoff intact.
  • The bypassRetry argument defaults to false everywhere it is not explicitly set: recordSessionActivity wakes, requestWorkspaceLiveStateRefresh, loadOnce({ fresh: true }), and scheduleWorkspaceRefresh all stay backoff-respecting, so lifecycle/background traffic is unchanged.
  • Non-live-state workspaces already bypassed the entry-level retryAt through refresh()ensureScheduled; the new option only extends the bypass to the live-state loop's 30s backoffs, which is the part that was actually gated.

Non-blocking observations:

  1. The rename success path touches the store twice — renamed() does a non-interactive patchSession + invalidateWorkspace, then the sidebar calls refreshWorkspace. Harmless (the interactive request wins), just slightly redundant.
  2. The CSS spacing tweak is a drive-by relative to the title; small enough to keep, but it would have been a cleaner history as its own PR.
  3. Other mutation surfaces (SideTaskPanel rename/invalidate, useScopedSessions deletes, the App-level renames) remain on the non-interactive path and keep the same staleness window. The PR body says that scoping is intentional — reasonable follow-up territory if maintainers want full coverage.

The added tests pin the behavior in both directions: the bypass tests fail if the bypass is removed (no new getLiveState/listSessions calls would occur during backoff), and the "keeps internal fresh loads behind backoff" tests fail if the bypass leaks to non-interactive refreshes. The workspace-removal tests pin that each sidebar mutation (delete, archive, color, group, unpin, rename) calls the new refreshWorkspace with the right workspace.

sequenceDiagram
    participant P1 as WebShellSidebar
    participant P2 as catalog controller
    participant P3 as SessionCatalogStore
    participant P4 as live-state poll loop
    P1->>P2: refreshWorkspace(cwd) after mutation
    P2->>P3: invalidateWorkspace(cwd) with interactive flag
    P3->>P3: mark entries stale and bump revisions
    P3->>P4: wake(cwd) with bypassRetry
    alt no request in flight
        P4->>P4: poll bypasses the 30s error backoff
        P4->>P3: stage and commit fresh pages
    else a request is already in flight
        P4->>P4: set interactiveRefreshRequested
        P4->>P4: finish() re-polls once the flight settles
        P4->>P3: stage and commit fresh pages
    end
Loading
Files changed (10)
File What changed
packages/web-shell/client/session-catalog/session-catalog-store.ts threads an interactive option through refresh and invalidateWorkspace; wake handlers now carry a bypassRetry flag
packages/web-shell/client/session-catalog/workspace-session-live-state.ts explicit refreshes bypass live-state and reconcile error backoff; in-flight requests are preserved via a flag and a finish() re-poll
packages/web-shell/client/session-catalog/session-catalog-hooks.ts new refreshWorkspace controller method; reload accepts an interactive option; useWebShellSessions.invalidate upgraded
packages/web-shell/client/components/sidebar/WebShellSidebar.tsx all sidebar mutation handlers and retry buttons now trigger interactive refreshes
packages/web-shell/client/components/sidebar/WebShellSidebar.module.css spacing around the loading indicator and right padding for empty meta slots
packages/web-shell/client/session-catalog/workspace-session-live-state.test.tsx six new tests covering bypass, preserved in-flight refresh, and backoff still applying to internal refreshes
packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx asserts each mutation type triggers refreshWorkspace for the affected workspace
packages/web-shell/client/session-catalog/session-catalog-store.test.ts wake handler assertions updated for the new second argument
packages/web-shell/client/App.test.tsx controller mock gains refreshWorkspace
packages/web-shell/client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx controller mock gains refreshWorkspace

Test evidence (PR's own CI, read via API — PR code never executed locally)

At review time the two lanes that matter for this PR are still running, and there are no red checks. Test (ubuntu-latest, Node 22.x) is the full unit suite covering the new session-catalog tests; Capture web-shell visuals is the lane that can show the spacing change. The macOS/Windows unit jobs are skipped by workflow design on PR events (they run in the merge queue), so those skips are not a signal. Desktop Shell builds — which compile the web-shell package — are green on both OSes, as are precheck, secret scan, and the CVE audit.

Final CI results for dcb34ca (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

On sandboxed verification: no extra lane is named here, and this is a deliberate skip, not an omission. The central behavioral claim (explicit refreshes bypass backoff, background refreshes do not, in-flight refreshes are preserved) is pinned by the added unit tests in both directions as analyzed above, so a green suite is meaningful evidence rather than a green stamp. The only claim the unit suite cannot render is the visual spacing tweak — the oracle for that is the Web-shell Visuals job already running on this commit; if its captures do not cover the loading-indicator and empty-meta-slot states, a maintainer browser glance is the remaining gap. The author's macOS testing is their claim, not independent evidence.

中文说明

代码审查

在读 diff 之前,我根据描述给出的独立方案是:复用 store 已有的 interactive 刷新类型来处理用户触发的变更,加一个只有显式操作才能设置的退避绕过,并确保 live-state 请求在途时发起的刷新不会被 inFlight 提前返回吞掉。PR 的实现与此完全一致,没有遗漏更简单的路径。

并发路径逐条检查,无阻塞问题:

  • interactiveRefreshRequested 标志只在退避检查通过后清除,disposed / inFlight / document.hidden 的提前返回都会保留它,时机不巧的请求也能存活到下一次合格轮询。finish()inFlight 落下后同步重新轮询,而 poll 在第一个 await 之前就设置 inFlight,不存在重复轮询竞态。
  • 快速重复点击会被布尔标志和唯一的 pending interactive 请求合并——不会产生请求突发,这正是保留后台退避的意义。
  • 未显式设置 bypassRetry 的所有路径默认为 falserecordSessionActivity 唤醒、requestWorkspaceLiveStateRefreshloadOnce({ fresh: true })scheduleWorkspaceRefresh 都继续受退避保护,生命周期/后台流量不变。
  • 非 live-state 工作区本来就能通过 refresh()ensureScheduled 绕过 entry 级 retryAt;新选项只是把绕过能力扩展到 live-state 循环的 30s 退避——那才是真正被限流挡住的部分。

非阻塞观察:

  1. 重命名成功路径会两次触碰 store——renamed() 内部做非交互的 patchSession + invalidateWorkspace,侧边栏又调用 refreshWorkspace。无害(interactive 请求胜出),只是略有冗余。
  2. CSS 间距调整相对标题属于顺手改动;体量小可以保留,但单独成 PR 历史会更干净。
  3. 其他变更入口(SideTaskPanel 重命名/invalidate、useScopedSessions 删除、App 层重命名)仍走非交互路径,存在同样的过期窗口。PR 描述说明这是有意限定范围——如果维护者希望全覆盖,是合理的后续工作。

新增测试双向锁定行为:绕过测试在移除绕过逻辑时会失败(退避期间不会再有新的 getLiveState/listSessions 调用),"内部刷新仍受退避保护"的测试在绕过泄漏到非交互刷新时会失败。workspace-removal 测试锁定每种侧边栏操作(删除、归档、颜色、分组、取消置顶、重命名)都会对正确的工作区调用新的 refreshWorkspace

时序图展示了变更后的流程:侧边栏变更 → controller → store 标记过期并唤醒轮询循环;无在途请求时直接绕过退避执行 reconcile,有在途请求时先记录标志、在本次请求落地后由 finish() 补一次轮询。

测试证据(PR 自身的 CI,通过 API 读取——未在本地执行 PR 代码)

审查时对此 PR 最关键的两条流水线仍在运行,且没有红色检查。Test (ubuntu-latest, Node 22.x) 是覆盖新增 session-catalog 测试的完整单元测试;Capture web-shell visuals 是能体现间距变化的流水线。macOS/Windows 单测在 PR 事件上按工作流设计跳过(仅在合并队列运行),这些跳过不是信号。Desktop Shell 构建(会编译 web-shell 包)在两个操作系统上均为绿色,precheck、密钥扫描、CVE 审计同样通过。

关于沙箱验证:这里没有点名额外的验证通道,这是刻意的跳过而非遗漏。核心行为主张(显式刷新绕过退避、后台刷新不绕过、在途刷新被保留)已被新增单测双向锁定(分析见上),因此绿色的测试套件是有意义的证据,而不是橡皮图章。单测无法呈现的唯一主张是视觉间距调整——其 oracle 是已在该提交上运行的 Web-shell Visuals 任务;如果其截图未覆盖 loading 指示器和空 meta 槽状态,剩余的缺口就是维护者在浏览器里看一眼。作者的 macOS 测试是其自述,不是独立证据。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — a focused, architecturally consistent fix whose tests pin the behavior in both directions; only non-blocking nits (a bundled CSS tweak and deliberately sidebar-scoped coverage).

Stepping back: the problem is real — I verified the staleness window in the base code rather than taking the PR's word for it, and the fix is the smallest natural extension of the machinery already there. Nothing new was invented: the interactive/invalidated distinction, the wake handlers, and the cooldown bypass all predate this PR; the change just lets explicit user actions use them and preserves a refresh that lands while a request is in flight. The flag lifecycle holds up under the awkward timings I could construct, and background traffic genuinely keeps its rate limiting — the negative tests enforce that, which is the part most PRs like this forget.

Reservations, all minor: the rename path double-touches the store, the spacing CSS is a drive-by for this title, and mutation surfaces outside the sidebar still carry the same staleness window until someone extends the treatment. None of that blocks a merge; the first two are cleanliness notes and the third is acknowledged scope.

I am approving this because the mechanism is sound and pinned by tests, not because I ran out of objections. The unit suite and the web-shell visuals job are still in flight on the reviewed commit, so approval is deferred until CI lands green on that commit; the finalize pass will confirm it, and if anything lands red it will flag it instead.

中文说明

回顾整体:问题是真实存在的——我在基础代码里亲自确认了过期窗口,而不是听信 PR 的描述;修复是对现有机制最小且自然的扩展。没有发明任何新东西:interactive/invalidated 的区分、wake 处理器、冷却绕过都是此前就有的;此改动只是让显式用户操作能够使用它们,并保留了在请求在途时到达的刷新。在我能构造的各种刁钻时序下,标志的生命周期都成立,后台流量也确实保留了限流——负向测试强制保证了这一点,而这正是此类 PR 最容易忽略的部分。

保留意见,均为次要:重命名路径会两次触碰 store,间距 CSS 相对标题是顺手改动,侧边栏之外的变更入口仍带有同样的过期窗口、有待后续扩展。这些都不阻塞合并;前两点是整洁性备注,第三点是已声明的范围。

我批准这个 PR 是因为机制可靠且被测试锁定,而不是因为没有反对意见了。在审查的提交上,单元测试和 web-shell 视觉任务仍在运行,因此批准推迟到 CI 在该提交上变绿之后;finalize 流程会予以确认,若有检查变红则会标出。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head dcb34ca. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

⚠️ No preview: one or more scenarios failed to render on this head — see the workflow run. This is not "no visual change" — a scenario that times out or throws produces no image. Fix the failing scenario (or a genuine regression it caught) and the preview returns on the next push.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — CI landed green after the review. ✅

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

中文说明

已审查——无阻断问题。 建议见行内评论。

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

Comment on lines +315 to +318
const reload = useCallback(
async (reloadOptions: { interactive?: boolean } = {}) => {
try {
return (await reloadPage(reloadOptions))?.sessions;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The { interactive: true } option the sidebar retry buttons forward here is never tested through either of the two hook hops — session-catalog-hooks.test.tsx only calls facade.reload() with no argument, and no sidebar test clicks the loadFailed retry button (its mocked reload ignores arguments), so only the store end (store.refresh(query, { interactive: true })) is covered. If a future refactor drops reloadOptions at either hop (e.g. reloadPage(reloadOptions)reloadPage()), the retry button's backoff bypass silently disappears — the user clicks Retry while a backoff window is active and nothing happens until it expires — while the whole suite stays green. That is precisely the user-visible fix this PR adds. Verified with a mutation probe: after dropping options in useSessionCatalogQuery.reload, the full web-shell suite still passes (188 files / 3805 tests). A hooks-level test would pin this: render useWebShellSessions with a live-state-retained workspace, call reload({ interactive: true }), and assert the store's refresh received { interactive: true } (or a wake fired with bypassRetry: true).

中文说明

侧边栏重试按钮在这里传入的 { interactive: true } 选项,在两个 hook 层都没有任何测试覆盖——session-catalog-hooks.test.tsx 只调用无参的 facade.reload(),侧边栏测试也没有点击 loadFailed 重试按钮(mock 的 reload 会忽略参数),因此只有 store 端(store.refresh(query, { interactive: true }))被覆盖。未来重构时如果在任一层丢掉 reloadOptions(例如 reloadPage(reloadOptions)reloadPage()),重试按钮绕过退避的能力会悄悄丢失——用户在退避窗口内点击重试却没有任何反应,直到窗口过期——而整个测试套件依然全绿。这正是本 PR 新增的用户可见修复。已通过变异探针验证:在 useSessionCatalogQuery.reload 中丢掉 options 后,web-shell 完整测试套件仍然全部通过(188 个文件 / 3805 个测试)。建议补一个 hooks 层测试来锁定:在保留了 live-state 的工作区上渲染 useWebShellSessions,调用 reload({ interactive: true }),断言 store 的 refresh 收到了 { interactive: true }(或 wake 以 bypassRetry: true 触发)。

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

Comment on lines 325 to 327
const invalidate = useCallback(() => {
if (workspaceCwd) controller.invalidateWorkspace(workspaceCwd);
if (workspaceCwd) controller.refreshWorkspace(workspaceCwd);
}, [controller, workspaceCwd]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This upgrade is not pinned by any test: the only facade mutation test ('runs mutations directly and performs only the Store resynchronization') runs without retainWorkspaceLiveState, and with live-state disabled invalidateWorkspace and refreshWorkspace are behaviorally identical (interactive only changes requestLiveStateRefresh, which is skipped). A mutation probe reverting this line to controller.invalidateWorkspace(workspaceCwd) keeps the entire web-shell suite green (188 files / 3805 tests) — the facade-driven delete/archive/release actions could silently lose this immediate backoff-bypassing refresh (stale sidebar until the next poll tick during an error backoff) without any test failing. Extending the hooks test to retain live-state for the workspace, perform a facade mutation, and assert the refresh took the interactive path (store.invalidateWorkspace called with { interactive: true }, or a wake handler observing bypassRetry: true) would pin the change.

中文说明

这个升级没有被任何测试锁定:唯一的 facade 变更测试('runs mutations directly and performs only the Store resynchronization')运行时没有 retainWorkspaceLiveState,而在 live-state 未启用时,invalidateWorkspacerefreshWorkspace 的行为完全一致(interactive 只影响被跳过的 requestLiveStateRefresh)。变异探针显示:把这行还原为 controller.invalidateWorkspace(workspaceCwd) 后,web-shell 完整测试套件依然全绿(188 个文件 / 3805 个测试)——也就是说,facade 驱动的删除/归档/释放操作可能会悄悄失去这个立即绕过退避的刷新(错误退避期间侧边栏会一直过期到下一次轮询),而不会有任何测试失败。建议扩展 hooks 测试:保留工作区的 live-state,执行一次 facade 变更,并断言刷新走了 interactive 路径(store.invalidateWorkspace 被以 { interactive: true } 调用,或 wake 处理器观察到 bypassRetry: true),从而锁定这一改动。

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

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers.

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-1 retry-button { interactive: true } wiring untested — already reported (comment 3818624636)
  • R1-2 facade invalidate → refreshWorkspace rerouting untested — already reported (comment 3818624641)
中文说明

已审查——无阻断问题。

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

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

@chiga0 chiga0 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.

Code Review — PR #9533 fix(web-shell): keep sidebar sessions synchronized

HEAD: dcb34ca1 | +300/−55, 10 files

No blocking findings. Mechanism traced end-to-end; tests pass on CI.


What was verified

Core bypass mechanism (workspace-session-live-state.ts)

  • refreshWorkspaceinvalidateWorkspace({interactive:true})requestLiveStateRefresh(kind=interactive, bypassRetry=true) → wake handler → state.interactiveRefreshRequested = truepoll() bypasses liveRetryAt check. Chain is complete and correct.
  • finish() closure correctly re-triggers poll() if a new interactive refresh arrives while a request is in-flight, so the bypass is not lost during in-flight waits.
  • bypassReconcileRetry = bypassRetry || state.interactiveRefreshRequested: if a second interactive request arrives after the live-state request but before reconcile, the reconcile still bypasses the retry window. Correct.
  • requestLiveStateRefresh guard (kind !== interactive && existing === interactive): prevents a non-interactive call from downgrading a pending interactive request. Correct.
  • Background invalidateWorkspace() still uses kind=invalidated with no wake, so background refreshes remain rate-limited. Correct.

9 invalidateWorkspacerefreshWorkspace substitutions in WebShellSidebar.tsx

All are in .finally() blocks or explicit mutation-success positions. Checked the rename flow (new call at line 2495 on success + existing finally call at line 2511 guarded by !renamed): mutually exclusive, no double-fire.

New test cases (workspace-session-live-state.test.tsx)

6 new tests cover: bypass live-state backoff, background loads stay rate-limited, preserve refresh while live request is in-flight, bypass reconciliation backoff, background loads stay behind reconciliation backoff, preserve refresh during in-flight catalog request. All key permutations covered at the store/live-state layer.

CI: Ubuntu Test (18m30s) ✅ · E2E Smoke ✅ · Visual capture ✅. Windows and macOS test suites are SKIPPED — no concern for this change (no platform-specific paths).


Confirming qwen-code-ci-bot suggestions (no new threads)

Both inline suggestions are valid test-validity gaps (class 5):

  • hooks.ts:318reload({ interactive: true }) forwarding across two hook hops (useWebShellSessions.reloadreloadPagestore.refresh) is not tested at the hook layer; only the store end is covered. A refactor dropping reloadOptions at either hop would keep the suite green.
  • hooks.ts:327invalidate → controller.refreshWorkspace change is not pinned by any test with retainWorkspaceLiveState: true; with live-state disabled the two are behaviorally identical, so a mutation probe reverting this line passes the entire suite.

Neither is a blocker; the store-level test coverage for the bypass behavior is thorough. Actionable as follow-up tests.


Reviewed with AI assistance.

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM (AI Generated)

核心机制已在 COMMENT 轮完整验证(见 #pullrequestreview-4979366458),此处补充 Approve。

CI: ubuntu Test ✅ · E2E Smoke ✅ · Visual capture ✅ · Desktop Shell (ubuntu + windows) ✅

两条 qwen-code-ci-bot Suggestion(hook 层转发未测试 / invalidate→refreshWorkspace 未被 live-state 启用的测试覆盖)均为非阻塞,可 follow-up 补测。

✅ Approve

@ytahdn
ytahdn added this pull request to the merge queue Aug 20, 2026
Merged via the queue into QwenLM:main with commit 50a0d2a Aug 20, 2026
138 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.15.

yiliang114 added a commit that referenced this pull request Aug 22, 2026
Resolve the WorkspaceSection conflict by keeping the optimistic-pin
mapSession seam ahead of the filter and adopting main's git-query
search term (sessionMatchesGitQuery) in the same predicate.

Also extend the pinning test harness controller mock with
refreshWorkspace, added by main's #9533 sidebar-sync rework, so the
settle path's workspace refresh no longer rejects in tests.

Verified: web-shell typecheck clean; sidebar suite 13 files, 188/188.
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.

4 participants