hermes-agent/apps/desktop/src/lib/use-session-slice.ts
Brooklyn Nicholson 94d70dee54 perf(desktop): stop ChatBar re-rendering on cross-session status/queue churn
Audit follow-up. ChatBar subscribed to the whole `$statusItemsBySession` (a
computed that rebuilds the entire map) + `$previewStatusBySession` maps just to
derive a boolean, so every per-item status mutation (a subagent tick, the 5s
background poll) and every OTHER session's change re-rendered the ~1.4k
component. The queue hook likewise subscribed to the whole `$queuedPromptsBySession`
map.

- Add `useSessionStatusPresence` — a coarse edge (useSyncExternalStore) that
  flips only when the stack shows/hides; ChatBar uses it for the styling
  data-attr instead of the two map subscriptions.
- Add generic `useSessionSlice(store, key)` — subscribes to one session's array,
  bailing out when other sessions churn (the plain atom keeps per-key refs
  stable). The queue hook now reads its slice through it.

Result: ChatBar re-renders only when the stack's presence flips or this session's
queue changes — not on background/subagent status streaming or other sessions.

Verified: typecheck clean, 0 lint errors, composer tests 39/40 (pre-existing
attachments failure unrelated).
2026-06-30 04:19:10 -05:00

31 lines
1.3 KiB
TypeScript

import { useSyncExternalStore } from 'react'
interface SliceStore<T> {
get(): Record<string, T[] | undefined>
listen(listener: () => void): () => void
}
// Stable empty result so an absent key never yields a fresh array (which would
// defeat the snapshot bail-out and re-render on every store write).
const EMPTY: readonly never[] = []
/**
* Subscribe to ONE session's slice of a `Record<sessionId, T[]>` nanostore,
* re-rendering only when *that* slice's reference changes — not on writes to
* other sessions. The map reference churns on every cross-session update, so a
* plain `useStore(map)` re-renders all consumers globally; reading `map[key]`
* through `useSyncExternalStore` bails out whenever the keyed array is
* unchanged (the stores update immutably per key). Returns a shared empty array
* when the key is null/absent.
*
* Note: only helps stores whose per-key arrays are referentially stable across
* unrelated writes (plain atoms with immutable per-key updates). A `computed`
* that rebuilds the whole map churns every slice — use a presence/edge selector
* there instead.
*/
export function useSessionSlice<T>(store: SliceStore<T>, key: string | null): T[] {
return useSyncExternalStore(
onChange => store.listen(onChange),
() => (key ? (store.get()[key] ?? (EMPTY as unknown as T[])) : (EMPTY as unknown as T[]))
)
}