#178

fix(sidebar): move settled rows at once and keep session churn out of the main pane

Opencalledforthwants to merge intofrom

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.updated echo is held back and only recorded as what the environment confirmed. This stops earlier clicks from pulling the row back and forth.
  • session.updated and 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 of SidebarDataValue. 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 SidebarData no 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 / CPUBaseline worst frameFix worst frameBaseline blocking per moveFix blocking per move
Production, 4x slowed CPU100–217ms50–83ms24–372ms5–61ms
Production, 4x, settling the open session100–200ms50–83ms42–173ms5–43ms
Dev server, normal CPUmostly 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-core sidebar-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

Activity

3
greptile-apps[bot]1mo ago


[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.

  • The websocket and mock clients now show a settle immediately, hold its event echo, and restore the confirmed value on refusal.
  • Web sidebar data and active-session readers subscribe more narrowly so unrelated session changes do not reach the main pane.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Click[Settle click] --> Tracker[Settle tracker]
  Tracker --> Store[Optimistic session state]
  Tracker --> Server[Environment request]
  Server --> Echo[Session update echo]
  Echo --> Tracker
  Server --> Answer[Answer or refusal]
  Answer --> Tracker
  Store --> Sidebar[Sidebar sessions context]

Reviews (1) · Last reviewed commit: "fix(sidebar): settle in one render and k..."