Goal
Stop O(N·M·C) rebuilds on every session tick. Each panel re-renders only when the slice it actually consumes has changed.
Today
web/components/agent-visualizer/session-stats-provider.tsx:31 — setSessionStats allocates a fresh outer Map on every publish, re-rendering every consumer of useSessionStatsData.
web/components/agent-visualizer/session-canvas-panel.tsx:118-122 — each session publishes once a second regardless of whether anything changed at the outer-Map level.
web/components/agent-visualizer/index.tsx:337-370 — feedConversations, feedAgents, agentToSession rebuild by iterating all sessions × all agents × all conversations on any stats change. The TODO at :368 flags this.
Plan
- Add
web/lib/session-stats-store.ts — a tiny store backed by useSyncExternalStore, keyed by sessionId. Subscribers register (state) => slice selectors with shallow equality.
setSessionStats writes only the changed session entry; subscribers whose selector result didn't change (shallow Map reference equality on agents/toolCalls/conversations) don't re-render.
- Convert
MessageFeedPanel, CostSummaryPanel, TopBar to selector-based reads. Drop the denormalized maps in index.tsx — consumers iterate the store directly, or maintain incrementally per session.
- Switch publish from interval-based to event-driven now that fan-out is cheap.
STATS_PUBLISH_INTERVAL_MS becomes the minimum gap, not the cadence.
- Sub-task: hoist
<ControlBar> reads (session-canvas-panel.tsx:301) onto a useSyncExternalStore view of frameRef.currentTime so the chrome doesn't re-render at 4 Hz with the simulation state.
Acceptance
- React DevTools profiler: with 5 sessions running,
MessageFeedPanel re-renders only when its visible session messages change, not on every other session's tick.
CostSummaryPanel re-renders only when total cost shifts.
- Per-frame React commit count flat as session count grows (visible in profiler).
Parallelism
Fully independent — no file conflicts with #2, #4, #5, or #6. Wave A.
Goal
Stop O(N·M·C) rebuilds on every session tick. Each panel re-renders only when the slice it actually consumes has changed.
Today
web/components/agent-visualizer/session-stats-provider.tsx:31—setSessionStatsallocates a fresh outerMapon every publish, re-rendering every consumer ofuseSessionStatsData.web/components/agent-visualizer/session-canvas-panel.tsx:118-122— each session publishes once a second regardless of whether anything changed at the outer-Map level.web/components/agent-visualizer/index.tsx:337-370—feedConversations,feedAgents,agentToSessionrebuild by iterating all sessions × all agents × all conversations on any stats change. TheTODOat:368flags this.Plan
web/lib/session-stats-store.ts— a tiny store backed byuseSyncExternalStore, keyed bysessionId. Subscribers register(state) => sliceselectors with shallow equality.setSessionStatswrites only the changed session entry; subscribers whose selector result didn't change (shallowMapreference equality onagents/toolCalls/conversations) don't re-render.MessageFeedPanel,CostSummaryPanel,TopBarto selector-based reads. Drop the denormalized maps inindex.tsx— consumers iterate the store directly, or maintain incrementally per session.STATS_PUBLISH_INTERVAL_MSbecomes the minimum gap, not the cadence.<ControlBar>reads (session-canvas-panel.tsx:301) onto auseSyncExternalStoreview offrameRef.currentTimeso the chrome doesn't re-render at 4 Hz with the simulation state.Acceptance
MessageFeedPanelre-renders only when its visible session messages change, not on every other session's tick.CostSummaryPanelre-renders only when total cost shifts.Parallelism
Fully independent — no file conflicts with #2, #4, #5, or #6. Wave A.