From e9018653b884ba7f661a68de933429bf41d3270a Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 1 Nov 2026 09:40:11 -0800 Subject: [PATCH] Session Info scoped to a subagent holds its rows while the reader scrolls The session card already reads the chat's rows through useHeldWhileScrolling; the subagent-scoped Session Info read snapshot.subagentViews live, so history paging in under the reader's scroll changed it mid-gesture. Hold those rows too, keyed by chat and subagent. Fixes #29639. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa --- webviews/src/agent-session/acpmux/App.tsx | 8 +++++-- .../summary-held-while-scrolling.test.tsx | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/webviews/src/agent-session/acpmux/App.tsx b/webviews/src/agent-session/acpmux/App.tsx index dd7ea8a542b1..afd9a3364c39 100644 --- a/webviews/src/agent-session/acpmux/App.tsx +++ b/webviews/src/agent-session/acpmux/App.tsx @@ -8140,19 +8140,23 @@ function AcpmuxPane() { ].join("\u0000") : ""; const scopedRows = shownSubagent === undefined ? undefined : snapshot.subagentViews?.[shownSubagent]; + // Session Info lists the subagent's rows as they stood when the reader started scrolling, until + // the scroll settles, as the chat's card does with `cardRows` (summary/heldWhileScrolling.ts, + // cmux#29639). Another subagent, or another chat, takes its rows at once. + const cardScopedRows = useHeldWhileScrolling(scopedRows, `${shownSessionId ?? ""}\u0000${shownSubagent ?? ""}`); const sessionInfoSubagent = useMemo((): SubagentScope | undefined => { if (!scopedAgent) return undefined; const parent = scopedAgent.parent; return { agent: scopedAgent, - rows: scopedRows ?? [], + rows: cardScopedRows ?? [], parent: parent === null ? chatName : (chatAgents.find((agent) => agent.id === parent)?.name ?? chatName), agents: descendants(chatAgents, scopedAgent.id), // As its "Subagent of" pill: back to its parent subagent's view, else the chat's. onBack: () => setSubagentView(parent ?? undefined), }; // eslint-disable-next-line react-hooks/exhaustive-deps -- a new scope only when what it shows changes - }, [scopedKey, scopedRows, chatName, chatAgents]); + }, [scopedKey, cardScopedRows, chatName, chatAgents]); // How many rows Session Info's Background tasks lists for the view on screen, read as it reads // them (sessionInfoLists drops a transcript shell the registry no longer lists): a subagent's view // from its own rows (none yet: empty) and processes, with how its shells ended in the chat's rows diff --git a/webviews/test/summary-held-while-scrolling.test.tsx b/webviews/test/summary-held-while-scrolling.test.tsx index 30a398253f21..0f3f503e0803 100644 --- a/webviews/test/summary-held-while-scrolling.test.tsx +++ b/webviews/test/summary-held-while-scrolling.test.tsx @@ -149,3 +149,27 @@ test("the card renders no more while history pages in under a 100-event scroll, act(() => root!.render()); expect(host.textContent).toBe("b1"); }); + +test("subagent-scoped Session Info holds its rows; another subagent takes its own at once", async () => { + // App.tsx holds `snapshot.subagentViews[shown]` keyed by chat and subagent (cmux#29639), as it + // holds the chat's rows for the session card. + const { createRoot } = await import("react-dom/client"); + const clock = manualClock(); + const flag = new ReaderScroll(clock, HOLD_SETTLE_MS); + function Scoped({ rows, chat, subagent }: { rows: string[] | undefined; chat: string; subagent: string }) { + const held = useHeldWhileScrolling(rows, `${chat}\u0000${subagent}`, flag); + return {(held ?? []).join(",")}; + } + root = createRoot(host); + act(() => root!.render()); + act(() => flag.scrolled()); + // Older history of the subagent pages in under the reader's scroll: Session Info keeps its rows. + act(() => root!.render()); + expect(host.textContent).toBe("s5"); + act(() => clock.run()); + expect(host.textContent).toBe("s4,s5"); + // Another subagent of the same chat, mid-scroll: its own rows at once. + act(() => flag.scrolled()); + act(() => root!.render()); + expect(host.textContent).toBe("t1"); +});