Skip to content

fix(web-shell): keep split-view session list fresh and preserve panes across view switches - #6418

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:fix/web-shell-split-session-list-refresh
Jul 7, 2026
Merged

fix(web-shell): keep split-view session list fresh and preserve panes across view switches#6418
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:fix/web-shell-split-session-list-refresh

Conversation

@wenshao

@wenshao wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Fixes two rough edges in the in-window split view (the side-by-side multi-session surface reached from the Session Overview or the "Open Split View" toolbar button): the "Add session" picker now stays fresh, and the split now survives leaving it and coming back.

  • The picker reloads its session list when it opens (and when the app's session list changes elsewhere), so it always offers the current sessions — never a snapshot frozen at the moment you entered the split.
  • The set of open panes is now mirrored up to the app, so switching to chat / a panel / the scheduled-tasks page and returning restores exactly the panes you had, and the no-arg "Open Split View" button restores an in-progress split instead of clearing it.

Why it's needed

useSessions only fetches on mount. The split view read that snapshot once and never refreshed, so a session created after you entered the split never appeared in the "Add session" picker, and a session deleted elsewhere was still offered. Meanwhile the live pane set lived in SplitView's local state, which is destroyed when the split unmounts (it is conditionally rendered on mainView === 'split'). The seed it re-mounted from was never updated with panes you added, and the toolbar "Open Split View" button reset that seed to empty — so switching away from the split and back collapsed it down to just the current session. Both are things a user hits quickly once they actually work in the split for a while.

Reviewer Test Plan

How to verify

  1. Open the web shell on a large screen, create a few sessions, and open the split view (Session Overview → select 2+ → open split, or the "Open Split View" toolbar button).
  2. Picker freshness: create another session (e.g. from the sidebar), then open the split's "+ Add session" picker — the new session is listed. Before this change it was absent until you left and re-entered the split.
  3. Pane preservation: with 2–3 panes open, switch to the chat / Settings / Scheduled Tasks view, then click "Open Split View" again — the same panes are restored. Before this change the split collapsed to a single pane.

Automated coverage (run in packages/web-shell): npx vitest run client/components/SplitView.test.tsx client/App.test.tsx — 54 pass, including three new SplitView tests (reloads the session list when the picker opens, reloads the picker list when the parent bumps the reload token, mirrors the live pane set up to the parent as panes change). The four split-related suites (SplitView, App, SessionOverviewPanel, WebShellSidebar) total 110 passing.

The screenshots below are from a small harness that mounts the real SplitView (real CSS module + i18n) driven in real Chrome via Playwright, with only the chat-pane body and the daemon session list stubbed — so it exercises the exact component and picker/pane wiring this PR changes. The "before" image is the same harness with the pre-fix app wiring (?mode=old).

Evidence (Before & After)

The split view — each pane is an independent session (own transcript, approvals, streaming):

split view — three independent panes

Fix 1 — "Add session" picker stays fresh. A session is created elsewhere; reopening the picker reloads and now offers it (before the fix the list was frozen at entry and it would be missing):

Picker open — current sessions After a new session is created elsewhere → reopen
picker before picker after refresh

Fix 2 — panes survive a view switch. Switch away from the split and reopen it:

Before — collapses to the current session After — the split is restored
before fix — one pane after fix — three panes restored

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Unit tests + a Vite/Playwright screenshot harness (real Chrome, channel:'chrome'). Not run against a live daemon on Windows/Linux — covered by CI.

Risk & Scope

  • Main risk or tradeoff: SplitView now reports its pane set up via onPanesChange. To avoid a render loop the parent must pass a referentially stable setter (it passes setSplitSessionIds directly); this is documented on the prop and covered by the tests.
  • Not validated / out of scope: a session created in a different browser tab still won't live-update an open split picker — that path never bumps the app's session-list token. This matches the sidebar; only the Session Overview polls. The picker refreshes on open and on the app's own session-change events, which covers the common cases.
  • Breaking changes / migration notes: none. onPanesChange and sessionListReloadToken are optional props; existing behavior is unchanged when they are omitted.

Linked Issues

N/A — found during manual testing of the split view.

中文说明

这个 PR 做了什么

修复窗口内分屏视图(从「会话总览」或工具栏「打开分屏」按钮进入的多会话并排界面)的两个问题:「添加会话」选择器现在保持最新,且分屏在切走再切回后不再丢失

  • 选择器在打开时(以及应用内会话列表在别处变化时)重新拉取会话列表,因此始终列出当前的会话——不再是进入分屏那一刻冻结的快照。
  • 打开的面板集合现在会上报给应用,因此切换到聊天 / 某个面板 / 定时任务页再返回时,会精确还原原有面板;无参的「打开分屏」按钮也会恢复进行中的分屏,而不是清空。

为什么需要

useSessions 只在挂载时抓取一次。分屏读到这份快照后再不刷新,所以进入分屏后新建的会话不会出现在「添加会话」选择器里,别处删除的会话却仍被列出。同时,实时的面板集合存在 SplitView 的本地 state 中,而分屏是按 mainView === 'split' 条件渲染的,切走即卸载、state 丢失;它重新挂载所依据的种子既不反映用户新加的面板,工具栏「打开分屏」按钮还会把该种子重置为空——于是切走再切回时分屏塌缩到只剩当前会话。用户真正在分屏里工作一会儿后很容易撞到这两点。

审阅者验证步骤

如何验证

  1. 在大屏打开 web shell,建几个会话,进入分屏(会话总览 → 选 2 个以上 → 打开分屏,或工具栏「打开分屏」按钮)。
  2. 选择器新鲜度: 再新建一个会话(例如从侧栏),然后打开分屏的「+ 添加会话」选择器——新会话已列出。改动前它要等你离开并重新进入分屏才出现。
  3. 面板保留: 开着 2–3 个面板时,切到聊天 / 设置 / 定时任务页,再点「打开分屏」——原有面板被还原。改动前分屏会塌缩为单个面板。

自动化覆盖(在 packages/web-shell 下运行):npx vitest run client/components/SplitView.test.tsx client/App.test.tsx——54 项通过,含 3 个新增 SplitView 用例(选择器打开时 reload、父级 reload token 变化时 reload、面板增减上报给父级)。四个分屏相关套件(SplitView、App、SessionOverviewPanel、WebShellSidebar)共 110 项通过。

下方截图来自一个小型 harness:它挂载真实SplitView(真实 CSS module + i18n),用 Playwright 在真实 Chrome 中驱动,仅将聊天面板正文与 daemon 会话列表打桩——因此运行的正是本 PR 改动的组件与选择器/面板接线逻辑。「修复前」那张是同一 harness 套用修复前的应用接线(?mode=old)。

证据(前后对比)

分屏视图——每个面板都是独立会话(各自的 transcript、审批、流式输出):

分屏——三个独立面板

修复 1 —— 「添加会话」选择器保持最新。 别处新建了一个会话,重新打开选择器时会 reload 并把它列出(修复前列表冻结在进入时,会缺这一项):

选择器打开——当前会话 别处新建会话后 → 重新打开
选择器 before 选择器刷新后

修复 2 —— 面板在切换视图后仍在。 切走再重新打开分屏:

修复前——塌缩到当前会话 修复后——分屏被还原
修复前——1 个面板 修复后——3 个面板还原

风险与范围

  • 主要风险:SplitView 现在通过 onPanesChange 上报面板集合。为避免渲染循环,父级必须传入引用稳定的 setter(现直接传 setSplitSessionIds);该约束已写在 prop 注释里并有测试覆盖。
  • 未验证 / 范围外:在另一个浏览器标签页新建的会话仍不会实时刷新已打开的分屏选择器——该路径不会触发应用的会话列表 token(与侧栏一致,只有会话总览做轮询)。选择器在打开时以及应用自身的会话变更事件上刷新,已覆盖常见场景。
  • 破坏性变更 / 迁移说明:无。onPanesChangesessionListReloadToken 均为可选 prop,省略时行为不变。

… across view switches

The in-window split view's "add pane" picker read a stale session snapshot —
`useSessions` only fetches on mount — so sessions created after entering the
split never appeared. And switching away from the split and back cleared the
panes, because the live pane set lived in local state that died on unmount while
the seed it re-mounted from was never updated (and the no-arg "Open Split View"
button reset it to empty).

- Reload the picker list when it opens and when the parent's session-list reload
  token changes, so it never offers a removed session or misses a new one.
- Mirror the live pane set up to the app via onPanesChange so it survives
  SplitView unmounting; restore it (instead of reseeding empty) when the split is
  reopened without an explicit selection.
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR @wenshao — re-running the gate after the follow-up commits.

Template looks good ✓ — thorough description, bilingual body, reviewer test plan with Playwright-driven before/after screenshots against the real SplitView.

Problem: observed and well-evidenced. Both rough edges are real UX issues — the picker freezing at the entry-time snapshot and panes collapsing on view switch. The before/after screenshots in the PR body demonstrate both concretely.

Direction: aligned. Split view is a core web-shell surface, and "picker shows current state" + "panes survive a view switch" are baseline expectations for any multi-pane UI. No mission drift.

Size: 4 files changed, all within packages/web-shell/. Not applicable for core-module gates. Production diff is compact — ~42 lines in SplitView.tsx (props, three effects) and ~22 lines in App.tsx (openSplitView preservation + prop wiring), plus ~140 lines of test additions.

Approach: minimal and clean. Two optional props added to SplitView (onPanesChange, sessionListReloadToken), so existing callers are unaffected. openSplitView in App.tsx now preserves the live pane set via the functional updater form of setSplitSessionIds instead of blindly resetting. The follow-up commits add an in-flight reload guard for burst coalescing and two new integration tests that prove re-render (not just callback invocation). No scope creep — every line serves the stated goal.

Moving on to code review. 🔍

中文说明

感谢 @wenshao 的 PR —— 在后续提交后重新跑门禁。

模板完整 ✓ —— 详尽的描述、双语正文、附带基于 Playwright 驱动真实 SplitView 的 before/after 截图。

问题: 已观察到且有充分证据。两个粗糙边缘都是真实的 UX 问题——选择器在进入时冻结、切走视图后面板塌缩。PR 正文里的 before/after 截图具体展示了两个问题。

方向: 对齐。分屏是 web-shell 的核心界面,"选择器显示当前状态"+"面板在视图切换后保留"是任何多面板 UI 的基线期望。无使命偏移。

规模: 4 个文件,全部在 packages/web-shell/ 内。不涉及核心模块门禁。生产 diff 紧凑——SplitView.tsx 约 42 行(props、三个 effect),App.tsx 约 22 行(openSplitView 保留 + prop 接线),加约 140 行测试。

方案: 最小且干净。给 SplitView 新增两个可选 prop(onPanesChangesessionListReloadToken),现有调用方不受影响。App.tsx 中的 openSplitView 现在通过 setSplitSessionIds 的函数式更新保留实时面板集合。后续提交加了 in-flight reload 防抖和两个新集成测试(证明重渲染而非仅回调调用)。无范围蔓延。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Code Review

I read the diff against an independent baseline. Before looking at the code, I'd have: (1) added a reload() call when the picker opens, (2) threaded a stable callback from the parent to mirror the pane set, and (3) preserved the pane set in openSplitView by not resetting to [] when no selection is passed. The PR matches this exactly and goes further with sessionListReloadToken for mid-session freshness — a clean addition I hadn't thought of.

Critical blockers: none.

Traced the effect loops that could have gone wrong:

  • Picker reload on open — gated by if (pickerOpen), so the initial render (picker closed) is a no-op. reload from useSessions is useCallback-wrapped with [reloadPage] deps; reloadPage is result.reload from useDaemonResource (deps [enabled, load] — both static in SplitView's usage). Verified the full stability chain through the source. No spurious re-fetch risk.
  • Token-driven reload — uses useRef(prevReloadTokenRef) to skip the initial value, only reacting to genuine bumps. The follow-up commit removed the in-flight guard and documented why: useDaemonResource serializes responses via its sequence counter (last write wins), so overlapping reloads are safe, and dropping a mid-reload bump would leave the picker stale. Correct reasoning.
  • onPanesChange reporting — runs on mount and every pane add/remove. App passes setSplitSessionIds directly (a useState setter — React guarantees referential stability). initialSessionIds in SplitView only seeds the initial useState, so the parent's state change doesn't bounce back into paneIds. No feedback loop possible.

Reuse / conventions: no issues. MAX_SPLIT_PANES was already exported from splitUrl; the PR just uses it for the explicit-selection cap. sessionListReloadToken was already defined in App.tsx and passed to SessionOverviewPanel — the PR reuses the existing mechanism. Props are optional, so the change is backward-compatible. TypeScript strict mode satisfied.

Test coverage: the follow-up commits strengthened the suite significantly. The useSessions mock is now stateful (calling reload() actually re-renders with the current store), and the freshness test asserts a new session actually appears in the picker's rendered options — not just that reload() was called. The App-level integration test reports panes up, leaves the split, reopens, and asserts restoration. The ?split= URL test covers dedupe + cap. All tests use realistic DOM interactions (dispatchEvent(new MouseEvent(...))) rather than calling handler props directly.

Testing

This is a React web-shell component — it runs in a browser under Vite, not in a terminal. Tmux real-scenario testing is not applicable here. The PR body includes Playwright-driven before/after screenshots from a harness that mounts the real SplitView against real Chrome (channel: 'chrome') — that's the visual equivalent of a tmux capture for this surface.

I was able to install dependencies and run the SplitView test suite locally:

 ✓ components/SplitView.test.tsx > SplitView > renders one pane per initial session, each under its own provider
 ✓ components/SplitView.test.tsx > SplitView > seeds with the current session when no initial sessions are given
 ✓ components/SplitView.test.tsx > SplitView > dedupes initial sessions
 ✓ components/SplitView.test.tsx > SplitView > adds a pane from the picker
 ✓ components/SplitView.test.tsx > SplitView > closes the picker on Escape
 ✓ components/SplitView.test.tsx > SplitView > closes the picker on a click outside it
 ✓ components/SplitView.test.tsx > SplitView > keeps the picker open on a click inside it
 ✓ components/SplitView.test.tsx > SplitView > removes a pane via its close button
 ✓ components/SplitView.test.tsx > SplitView > auto-exits to the overview when the last pane is closed
 ✓ components/SplitView.test.tsx > SplitView > exits via the back button
 ✓ components/SplitView.test.tsx > SplitView > caps the number of panes at MAX_PANES (6)
 ✓ components/SplitView.test.tsx > SplitView > isolates a crashing pane so the rest of the split survives
 ✓ components/SplitView.test.tsx > SplitView > reloads the session list when the picker opens (never a stale list)
 ✓ components/SplitView.test.tsx > SplitView > renders the refreshed session list on reopen — not the entry snapshot
 ✓ components/SplitView.test.tsx > SplitView > reloads the picker list when the parent bumps the reload token
 ✓ components/SplitView.test.tsx > SplitView > mirrors the live pane set up to the parent as panes change

 Test Files  1 passed (1)
      Tests  16 passed (16)

All 16 pass, including the 4 new tests that directly cover both fixes. App.test.tsx could not run in this worktree due to @qwen-code/acp-bridge/mcpTimeouts not being resolvable (an environment issue, not a code issue) — CI will be the authoritative runner for that suite. The code review itself is clean — no blockers, no concerns.

中文说明

代码审查

我把 diff 与独立基线做了对比。看代码之前,我会:(1) 在 picker 打开时加 reload(),(2) 从父级传一个稳定回调来镜像面板集合,(3) 在 openSplitView 中不将 splitSessionIds 重置为 [] 来保留面板集合。PR 完全吻合,并进一步加了 sessionListReloadToken 用于会话中的列表新鲜度——干净的补充。

关键阻塞: 无。

追踪了三个可能出问题的 effect 循环:

  • picker 打开时 reload —— 被 if (pickerOpen) 保护,初始渲染无操作。reload 引用稳定(通过源码验证了完整稳定性链)。无重复拉取风险。
  • token 驱动的 reload —— 用 useRef 跳过初始值。后续提交移除了 in-flight 守卫并解释了原因:useDaemonResource 通过序列号串行化响应(最后写入胜出),所以重叠 reload 安全,丢弃 mid-reload 的 bump 会让 picker 保持过时。推理正确。
  • onPanesChange 上报 —— App 传 setSplitSessionIds(React 保证引用稳定)。无反馈循环可能。

复用/规范: 无问题。sessionListReloadToken 复用 App 已有的机制。Props 可选,向后兼容。

测试覆盖: 后续提交显著加强了测试。useSessions mock 改为有状态,freshness 测试断言新会话确实出现在 picker 渲染的选项里。App 层集成测试上报面板、离开分屏、重开、断言还原。

测试

React web-shell 组件——在浏览器里由 Vite 驱动。tmux 真实场景测试不适用。 PR 包含基于 Playwright 的 before/after 截图。

本地成功运行 SplitView 测试:16 项全部通过,含 4 个新测试直接覆盖两个修复。App.test.tsx 在此 worktree 因 @qwen-code/acp-bridge 不可解析而无法运行(环境问题,非代码问题)——CI 将是权威跑测。代码审查本身干净。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Stepping back to reflect before deciding.

This is a focused, ~64-line production fix for two concrete UX rough edges in the split view that any user who actually works in the surface would hit within minutes. The problem is observed (not theoretical), the evidence is in the PR's before/after screenshots, and the fix matches what I'd have done independently — with the bonus of sessionListReloadToken for mid-session freshness.

The follow-up commits show a conscientious author: the in-flight reload guard was removed with a well-reasoned explanation (sequence counter in useDaemonResource makes overlapping reloads safe; an in-flight guard would drop legitimate bumps), the useSessions mock was made stateful so the freshness test proves re-render rather than just callback invocation, and an App-level integration test was added for the restore path.

The code is minimal: two optional props, three small effects, one preservation tweak to openSplitView. No scope creep, no drive-by refacts, no "while I'm here" additions. Every change in the diff is necessary for the stated goal, and I can't name a simpler path that covers both freshness and preservation.

I ran the SplitView test suite locally — 16/16 pass, including the 4 new tests. App.test.tsx couldn't run in this worktree (environment issue), but the code review traced every effect's dependency graph and found no feedback loops, stale closures, or unnecessary re-renders. CI will be the authoritative run.

Independent proposal vs. PR's approach: the PR matches or exceeds it. Approving.

中文说明

决定前先退一步反思。

这是一个聚焦的约 64 行生产代码修复,针对分屏视图中两个具体的 UX 粗糙边缘——任何真正在该界面工作的用户几分钟内就会撞到。问题是观察到的(不是理论性的),证据在 before/after 截图里,修复方案与我独立想到的吻合,并额外加了 sessionListReloadToken

后续提交展示了认真负责的作者:in-flight reload 守卫被移除并附有充分推理(useDaemonResource 的序列号使重叠 reload 安全;in-flight 守卫会丢弃合法 bump),useSessions mock 改为有状态以证明重渲染,App 层集成测试覆盖了还原路径。

代码最小化:两个可选 prop、三个小 effect、一个对 openSplitView 的保留调整。无范围蔓延、无顺手重构。diff 里每一行都服务于既定目标,我无法命名一个同时覆盖新鲜度和保留的更简路径。

本地跑了 SplitView 测试:16/16 通过,含 4 个新测试。App.test.tsx 在此 worktree 无法运行(环境问题),但代码审查追踪了每个 effect 的依赖图,未发现反馈循环、过时闭包或不必要重渲染。CI 将是权威跑测。

独立方案 vs PR 方案:PR 匹配或超过之。批准。

Qwen Code · qwen3.7-max

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

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Downgraded from Request changes to Comment: self-PR; CI still running.

Comment thread packages/web-shell/client/components/SplitView.tsx
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/components/SplitView.test.tsx
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/components/SplitView.tsx
…e token reloads

Addresses review feedback on QwenLM#6418:
- SplitView: skip a token-driven reload while one is already in flight, so a
  burst of session-list changes (bulk create/delete) doesn't fire a redundant
  concurrent round-trip per bump (matches the sidebar's poll guard).
- SplitView test: the freshness test now proves the picker re-renders with the
  refreshed list — a session appearing only after reload shows up — not just
  that reload() was called.
- App test: cover the openSplitView preserve/restore path end-to-end — a reported
  pane set survives leaving the split and is restored on reopen.
@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review in 7092e70 (tests + a small robustness tweak):

  • Token reload coalescing (SplitView.tsx) — added an in-flight guard so a burst of session-list changes doesn't fire redundant concurrent reloads, matching the sidebar's poll guard.
  • Freshness test now proves re-render (SplitView.test.tsx) — the mock's useSessions is stateful, and a new test asserts a session created after entry actually appears in the picker's rendered options (not just that reload() fired).
  • App-level restore coverage (App.test.tsx) — a new integration test reports a pane set up, leaves the split, reopens from the toolbar, and asserts the set is restored (not reset). The explicit-selection dedupe/cap and empty→current fallback branches are covered by existing tests.

On the two [Critical] stale-session threads I've pushed back rather than changed code: a pane/picker option referencing a session removed elsewhere is a pre-existing edge (the seed could always hold a stale id), each pane is contained by a per-pane ErrorBoundary + onError so it degrades to a closeable error rather than crashing the split, and filtering against sessions isn't safe because that list is a single page capped at SESSION_LIST_PAGE_SIZE (1000, truncated) and could wrongly drop valid panes. Details in the two threads — happy to revisit if a maintainer prefers explicit guarding.

All four web-shell split-related suites stay green (112 tests); the two new tests are included.

中文

已在 7092e70 处理评审(测试 + 一处小的健壮性改动):

  • token reload 去重(SplitView.tsx)——加 in-flight 守卫,一批会话列表变更不再并发发冗余 reload,与侧栏轮询守卫一致。
  • freshness 测试证明重渲染(SplitView.test.tsx)——mock 的 useSessions 改为有状态,新测试断言进入后新建的会话确实出现在 picker 渲染出的选项里(不只是 reload() 被调用)。
  • App 层还原覆盖(App.test.tsx)——新集成测试上报面板集合、离开分屏、从工具栏重开,断言集合被还原(而非重置)。显式选择去重/上限与空→当前会话兜底分支由既有测试覆盖。

两条 [Critical] stale-session 线程我选择有据反驳而非改代码:pane/picker 选项引用别处已删除的会话是既有边角(种子本来就可能含 stale id),每个 pane 由独立 ErrorBoundary + onError 兜底,降级为可关闭错误而非拖垮分屏;而按 sessions 过滤并不安全,因为该列表是上限 SESSION_LIST_PAGE_SIZE(1000,truncated)的单页,可能误删有效 pane。细节见两条线程——若维护者更倾向显式守卫,我乐意再处理。

四个 web-shell 分屏相关套件保持全绿(112 项),含两个新测试。

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Downgraded from Request changes to Comment: self-PR; CI still running.

Comment thread packages/web-shell/client/components/SplitView.tsx Outdated
…ht guard)

The in-flight guard added in the previous commit could drop a session-list
reload token that arrives while a reload is still running: the effect has
already run for that token value, and clearing the in-flight flag in `finally`
doesn't re-run it, so the picker could stay stale after burst create/delete/
rename activity — and the split has no polling fallback to recover.

Reload on every distinct token bump instead. `useDaemonResource` serializes
responses via its sequence counter (last write wins), so overlapping reloads are
correct, and the token is bumped only on discrete session-change events — an
occasional redundant fetch is far cheaper than a lost refresh.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ Downgraded from Request changes to Comment: self-PR; CI failing: review-pr.

— qwen3.7-max via Qwen Code /review

Comment thread packages/web-shell/client/App.test.tsx
Comment thread packages/web-shell/client/components/SplitView.tsx
@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

No new Suggestion-level findings this round — all prior suggestions have been addressed or superseded.

— qwen3.7-max via Qwen Code /review

…e + cap)

Per review: the restore branch of openSplitView was covered but the
explicit-selection branch (dedupe + MAX_SPLIT_PANES cap, replacing any prior
set) was only exercised, not asserted. Add a `?split=` URL test with duplicate
and over-cap ids that asserts the split seeds exactly the deduped, capped
selection.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ Downgraded from Approve to Comment: self-PR; CI still running.

Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.

— qwen3.7-max via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅ Downgraded from Approve to Comment: self-PR; CI still running.

— qwen3.7-max via Qwen Code /review

// Mirror the live pane set up to the parent so it outlives this component
// unmounting when the user switches views. On re-entry the parent reseeds
// `initialSessionIds` from it, restoring the exact panes instead of clearing.
useEffect(() => {

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 reporting effect includes onPanesChange directly in its dependency array. Today the parent passes setSplitSessionIds (a stable useState setter), so this works correctly. However, the contract is enforced only by a JSDoc comment — if a future refactor wraps the callback in an inline arrow (e.g. onPanesChange={(ids) => setSplitSessionIds(ids)}), the effect fires every render, each call triggers a parent state update, and the new callback identity re-fires the effect, producing an infinite render loop.

Stabilize the callback inside SplitView with a ref so the component is safe regardless of what the parent passes:

const onPanesChangeRef = useRef(onPanesChange);
onPanesChangeRef.current = onPanesChange;

useEffect(() => {
  onPanesChangeRef.current?.(paneIds);
}, [paneIds]);

— qwen3.7-max via Qwen Code /review

) ?? false;
const { sessions } = useSessions({
const { sessions, reload } = useSessions({
autoLoad: true,

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] useSessions returns an error field (from useDaemonResource's state) that is silently discarded here. When a reload fails (daemon timeout, network issue), the picker shows stale data with no console warning or UI signal. Unlike the sidebar (which has polling as a fallback), SplitView has no fallback — stale data persists until the user remounts.

Consider destructuring error and logging it:

const { sessions, reload, error } = useSessions({ ... });
useEffect(() => { if (error) console.warn('[SplitView] reload error:', error); }, [error]);

— qwen3.7-max via Qwen Code /review

// Open the in-window split view showing 2+ sessions side by side. `splitSessionIds`
// is the live pane set — SplitView mirrors add/remove back into it via
// onPanesChange — so it must be preserved across entries, not blindly reset.
const openSplitView = useCallback(

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] openSplitView includes connection.sessionId in its useCallback deps, giving it a new identity on every reconnection. The ?split= URL effect depends on openSplitView, so it re-runs on each reconnect. Safe today (the URL param is already consumed), but the coupling is fragile — any future side effect added to that effect would fire far more often than expected.

Use a ref to keep the callback identity stable:

const sessionIdRef = useRef(connection.sessionId);
sessionIdRef.current = connection.sessionId;
const openSplitView = useCallback((sessionIds?: string[]) => {
  setActivePanel(null);
  setSplitSessionIds((prev) => {
    const requested = Array.from(new Set((sessionIds ?? []).filter(Boolean))).slice(0, MAX_SPLIT_PANES);
    if (requested.length > 0) return requested;
    if (prev.length > 0) return prev;
    return sessionIdRef.current ? [sessionIdRef.current] : [];
  });
  setMainView('split');
}, []); // stable identity

— qwen3.7-max via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No review findings. Downgraded from Approve to Comment: self-PR.

— GPT-5 Codex via Qwen Code /review

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

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

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Critical-only review pass on current head 70a203c1d548bb1fe36e07380d68dd6ee991bd81: no critical correctness, security, data-loss, build-breaking, or severe regression issues found. The latest Qwen Code CI workflow run is also green.

— GPT-5 Codex via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Jul 7, 2026
Merged via the queue into QwenLM:main with commit 55652d4 Jul 7, 2026
112 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants