Follow-up to #168. Moving a session between Active and Settled still stuttered. The animation timing was fine, but four full React renders ran inside the 240ms row animation and blocked the main thread. Each render also reached well beyond the sidebar.
What changed
One render on the click, instead of four
- The client store moves the session at once and puts it back if the environment refuses (
packages/environment-client/src/settle.ts, used by the websocket and mock clients). This replaces the view's own stand-in (useOptimisticSettle) and the extra render when that stand-in was released. - While a request is in flight, its
session.updatedecho is held back and only recorded as what the environment confirmed. This stops earlier clicks from pulling the row back and forth. session.updatedand re-listing now keep the state as it was when they restate a session, so the redundant echo render is gone.- One small render remains when the answer lands: it swaps the local timestamp for the server's.
No layout projection on the rows
layout: 'position'is dropped from the Active and Settled rows. The height animation already moves the neighbouring rows, and layout made Motion measure every row on each render.- Trade-off: a row that changes places, for example one moving to the top on new activity, now jumps instead of sliding.
The sidebar's session churn stays in the sidebar
- Sessions are now served on their own context,
SidebarSessionsContext/useSidebarSessions, split out ofSidebarDataValue. The web and desktop providers both supply it. - The web provider keeps rows, groups and the whole map as the same objects while nothing the sidebar shows has changed. Composer picks and thread ids no longer reach the sidebar.
- The catalog and commands in
SidebarDatano longer change with every session. So the new-session landing, the ⌘K palette and the Add project dialog stop re-rendering with the list. - The topbar breadcrumb reads only the open session's title.
- SessionState read the whole active session object through
openDraft's dependencies. It now reads only the session's id and workspace. ActiveThread builds its details from the fields it shows. A settle, title change or composer pick on the session you're viewing no longer re-renders the composer, the chat pane or the thread.
Measurements
The probe used real Chrome driven by playwright. It clicked Settle and Move back to active three times each on a copy of real data, with fix and baseline built from the same data and ports. Frame times come from rAF sampling plus the Long Animation Frames API.
| Build / CPU | Baseline worst frame | Fix worst frame | Baseline blocking per move | Fix blocking per move |
|---|---|---|---|---|
| Production, 4x slowed CPU | 100–217ms | 50–83ms | 24–372ms | 5–61ms |
| Production, 4x, settling the open session | 100–200ms | 50–83ms | 42–173ms | 5–43ms |
| Dev server, normal CPU | mostly 50ms (33–67) | mostly 17–34ms (17–50) | – | – |
Painted frames per move went from 2–4 to 3–6 at prod 4x, and from 5–9 to 9–12 in dev. Production at normal CPU was already smooth and still is.
A React commit recorder confirms that a settle now renders only sidebar components, even with the settled session open in the chat pane.
Next candidate (not in this PR)
With the session open, the largest remaining app-level cost in the profile is Fluid's SidebarMenu hover tracking (useMenuScope / use-fluid-hover) on the Settled list. Motion's own row rendering comes next.
Tests
environment-client: added tracker tests (instant move, refusal, the latest click winning, held-back echo), reducer no-op tests, and a websocket test showing that neither the echo nor the answer renders again.app-coresidebar-settle.test.tsx: now runs through the mock environment client and providers.- It checks that the row moves before the environment answers.
- It checks that settling the open session renders none of SessionState, SidebarData, ActiveThread or Composer. This was mutation-checked: re-adding the whole-session dependency makes it fail with each reader rendering twice.
- Local runs:
- environment-client: 172 passed.
- app-core: all passed except
boundary.test.ts, which timed out under load and passes when run alone. - web: 118 passed.
- Typecheck is clean for environment-client, app-core, web and desktop (web). Lint and prettier are clean on the changed files.
🤖 Generated with Claude Code
[Medium risk] Refactors session state updates to optimize sidebar rendering.
The PR appears safe to merge; no actionable issue was identified.
Summary
The PR moves optimistic settling into the environment clients, separates sidebar sessions from catalog context, preserves unchanged session identities, and removes row layout projection to reduce renders during moves.
Diagram
Reviews (1) · Last reviewed commit: "fix(sidebar): settle in one render and k..."