perf(desktop): freeze hidden tab transcripts during streaming

Keep-alive keeps every ever-active tab mounted, but each hidden tab's
ChatRuntimeBoundary still subscribed to its view's $messages — so every
streaming delta flush (~30x/s) re-rendered every busy tab's whole thread,
and five concurrent sessions dropped the app to a crawl.

Flow the pane layer's visibility down as PaneVisibleContext and gate the
$messages subscription on it: a hidden tab freezes its transcript (status
dots stay live through the separate status atoms) and catches up in one
commit on reveal, since subscribe fires immediately with the current value.
This commit is contained in:
Brooklyn Nicholson 2026-07-26 00:20:40 -05:00
parent 559d0849a9
commit 47f0cdf3a2
3 changed files with 44 additions and 4 deletions

View file

@ -1,14 +1,16 @@
import { type AppendMessage, AssistantRuntimeProvider, type ThreadMessage } from '@assistant-ui/react'
import { useStore } from '@nanostores/react'
import { useQuery } from '@tanstack/react-query'
import type { ReadableAtom } from 'nanostores'
import type * as React from 'react'
import { Suspense, useCallback, useEffect, useMemo } from 'react'
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'
import type { SubmitTextOptions } from '@/app/session/hooks/use-prompt-actions/utils'
import { Thread } from '@/components/assistant-ui/thread'
import { Backdrop } from '@/components/Backdrop'
import { COMPOSER_HEART_CONFIG, HeartField } from '@/components/chat/vibe-hearts'
import { usePaneVisible } from '@/components/pane-shell/pane-visibility'
import { $sessionTileDragging, $sessionTileEdgeHover } from '@/components/pane-shell/tree/store'
import { PromptOverlays } from '@/components/prompt-overlays'
import { Button } from '@/components/ui/button'
@ -174,6 +176,30 @@ interface ChatRuntimeBoundaryProps {
const NO_MESSAGES: ChatMessage[] = []
/**
* The view's $messages, live only while this surface is the VISIBLE tab.
*
* Keep-alive keeps every ever-active tab MOUNTED (tree-group.tsx), so without
* this gate a hidden tab re-renders its entire thread on every streaming
* delta flush (~30×/s) five busy tabs quintuple the per-token render cost
* and the app crawls. Hidden tabs freeze their transcript instead (status
* dots stay live through the separate status atoms) and catch up in one
* commit on reveal the subscribe fires immediately with the current value.
*/
function useMessagesWhileVisible($messages: ReadableAtom<ChatMessage[]>): ChatMessage[] {
const visible = usePaneVisible()
const [messages, setMessages] = useState(() => $messages.get())
// nanostores types the listener value ReadonlyIfObject; the store publishes
// a fresh array per flush, so the cast is safe and avoids a per-token clone.
useEffect(
() => (visible ? $messages.subscribe(value => setMessages(value as ChatMessage[])) : undefined),
[$messages, visible]
)
return messages
}
/**
* Owns the $messages subscription and the assistant-ui external-store runtime.
*
@ -193,7 +219,7 @@ function ChatRuntimeBoundary({
onThreadMessagesChange,
suppressMessages
}: ChatRuntimeBoundaryProps) {
const storeMessages = useStore(useSessionView().$messages)
const storeMessages = useMessagesWhileVisible(useSessionView().$messages)
const messages = suppressMessages ? NO_MESSAGES : storeMessages
const runtimeMessageRepository = useRuntimeMessageRepository(messages)

View file

@ -10,6 +10,8 @@
* has to skip hidden panes, or it silently answers with the wrong tab.
*/
import { createContext, useContext } from 'react'
/** Marks a mounted-but-hidden pane layer (an inactive tab in a stack). */
export const PANE_HIDDEN_ATTR = 'data-pane-hidden'
@ -18,6 +20,14 @@ const HIDDEN_PANE = `[${PANE_HIDDEN_ATTR}]`
/** Spread onto a kept pane layer so the lookups below can skip it. */
export const hiddenPaneProps = (hidden: boolean): Record<string, string> => (hidden ? { [PANE_HIDDEN_ATTR]: '' } : {})
/** React face of the same policy: the pane layer provides its visibility so a
* kept-alive surface can gate hot subscriptions (streaming re-renders) off
* while it's an inactive tab. Default TRUE surfaces outside a tab stack
* (secondary windows, plain routes) are always visible. */
export const PaneVisibleContext = createContext(true)
export const usePaneVisible = (): boolean => useContext(PaneVisibleContext)
/** `querySelectorAll` minus anything inside an inactive tab. */
export const queryAllVisible = <T extends HTMLElement>(selector: string, root: ParentNode = document): T[] =>
[...root.querySelectorAll<T>(selector)].filter(el => !el.closest(HIDDEN_PANE))

View file

@ -30,7 +30,7 @@ import { cn } from '@/lib/utils'
import { $layoutEditMode } from '../../edit-mode'
import { useWindowControlsOverlap } from '../../geometry'
import { hiddenPaneProps } from '../../pane-visibility'
import { hiddenPaneProps, PaneVisibleContext } from '../../pane-visibility'
import type { DropPosition, GroupNode, RootEdge } from '../model'
import { adjacentGroup } from '../model'
import {
@ -544,7 +544,11 @@ export function TreeGroup({
{...hiddenPaneProps(!isActive)}
>
{pane?.render ? (
<ContribBoundary id={pane.id}>{pane.render()}</ContribBoundary>
// Visibility flows to the pane so a kept-alive chat surface
// can gate its hot (per-token) subscriptions while hidden.
<PaneVisibleContext.Provider value={isActive}>
<ContribBoundary id={pane.id}>{pane.render()}</ContribBoundary>
</PaneVisibleContext.Provider>
) : (
isActive && (
<div className="p-3 font-mono text-[11px] text-(--ui-text-quaternary)">