Skip to content

fix(desktop): resolve O(n²) render cascade in activity sidebar#555

Merged
wesbillman merged 1 commit into
mainfrom
fix/activity-sidebar-perf
May 12, 2026
Merged

fix(desktop): resolve O(n²) render cascade in activity sidebar#555
wesbillman merged 1 commit into
mainfrom
fix/activity-sidebar-perf

Conversation

@wesbillman
Copy link
Copy Markdown
Collaborator

Summary

  • Fixes the activity sidebar becoming unresponsive after 1-2 minutes when an agent is actively streaming events
  • Root cause was an O(n²) render cascade: every new WebSocket event triggered a full transcript rebuild over all events and re-rendered every markdown block
  • Four-layer fix: store-level incremental transcript, useSyncExternalStore with snapshot caching, component cleanup, and React.memo on transcript items

Approach

Layer 1 — Store-level incremental transcript (observerRelayStore.ts)
Transcript is now computed incrementally per event (O(1)) instead of O(n) full rebuild per render. Falls back to full rebuild for out-of-order arrivals and when the 800-event cap trims.

Layer 2 — useSyncExternalStore hooks (useObserverEvents.ts)
Replaced useState/useEffect subscription pattern. Snapshot caching gives referential stability — React only re-renders when the snapshot reference actually changes.

Layer 3 — Component cleanup (ManagedAgentSessionPanel.tsx)
Removed buildTranscript useMemo and the [...events].reverse().find() scan. Lightweight channelId filter on pre-computed transcript.

Layer 4 — React.memo on transcript items (AgentSessionTranscriptList.tsx)
Spread-copy pattern ensures unchanged items keep their object reference, so memo skips re-render. Only items with new content get new refs.

Correctness safeguards

  • Out-of-order event arrivals trigger full transcript rebuild (not incremental)
  • 800-event cap trim also triggers rebuild to stay in sync
  • latestSessionId propagates even when no transcript items change
  • Cross-channel transcript IDs include channelId prefix to prevent collisions
  • latestSessionId derived from channel-scoped events (not global)

Notable NITs (not addressed — cosmetic)

  • React.memo shallow comparison on TranscriptItemView is intentional: memo saves renders for unchanged items, which is the win

Test plan

  • pnpm build passes in desktop/
  • Pre-commit hooks pass (biome, formatting)
  • Pre-push hooks pass (builds, clippy, tests)
  • Manual: open activity sidebar while agent is streaming events, confirm no slowdown after 2+ minutes
  • Manual: verify transcript renders correctly with multiple channels active

🤖 Generated with Claude Code

The activity sidebar became unresponsive after 1-2 minutes when an agent
was actively streaming events. Every new WebSocket event triggered a full
transcript rebuild over all events and re-rendered every markdown block.

Four-layer fix:
- Store-level incremental transcript computation via processTranscriptEvent,
  with full-rebuild fallback for out-of-order arrivals and 800-event cap trim
- useSyncExternalStore with snapshot caching for referential stability
- Component cleanup: removed redundant buildTranscript useMemo and reverse-scan
- React.memo on transcript items with spread-copy pattern (no in-place mutation)

Correctness safeguards: out-of-order events trigger full rebuild, transcript
stays in sync with event cap, cross-channel IDs include channelId prefix,
latestSessionId remains channel-scoped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wesbillman wesbillman merged commit f41b973 into main May 12, 2026
15 checks passed
@wesbillman wesbillman deleted the fix/activity-sidebar-perf branch May 12, 2026 22:48
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.

1 participant