You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Read-only RAM/CPU + code-quality audit of three surfaces — the WorkStation tabs store, the My Station hosts (code / browser / project / simulator), and the chat pane — on develop @ d3c7200ae (2026-08-23). Goal: find work the app does that the user never sees, and refactors that remove it without changing user experience.
Method: react-best-practices + org2-performance-guard priority order; five parallel deep reads (tabs store, shell hosts, chat UI, streaming data path, lifecycle sweep) plus three sub-reads (CodeEditor hooks, CodeEditor panels, browser/simulator/project). Every finding below was traced to owner → start condition → cadence → cleanup; none are bare grep hits. Top claims were re-verified by hand.
Caveat: everything is from reading code. No profiler traces or RSS measurements were taken — mechanisms are confirmed, magnitudes are not. Re-measure before calling any fix a win.
Context that changes the advice: React Compiler is not enabled (esbuild/swc loaders only, no babel plugin, no lint rule), so manual memo/stable-identity discipline genuinely matters.
The big picture
The architecture-level decisions are already right: token streaming bypasses the store (50 ms trailing buffer, per-session selectAtom, sentinel so projection never re-runs per token); Rust batches es:changed at 100 ms with change-journal deltas; hostMountPolicy.ts is bounded and unit-tested; every large list is virtualized; all browser/port polling is visibility-aware and single-flight.
The waste sits one layer below those decisions: identity churn, content-derived keys, and hosts that are kept mounted without being told they're hidden.
Ranked summary
Ordered by estimated steady-state cost for a typical session (one long agent conversation streaming, a few tabs open, the code host hidden behind chat or a browser tab).
#
Finding
Surface
Cost class
Severity
Fix size
1
Chat virtual-row keys embed status + text length → whole-turn DOM remount per tool completion; O(n) key rebuild per render; duplicate per-row ResizeObserver retains detached rows
Chat
CPU spikes + retained DOM
high
small
2
Every tab-layout write synchronously rewrites all session-workspace localStorage keys (N+5 setItem), no no-op short-circuit, payload carries terminal text / JSON blobs
Tabs
Main-thread CPU per tab action; grows with session count
high
small–medium
3
Hidden code host keeps doing real work: file-watch → every open file re-reads disk; file-tree IPC reloads; output-channel sessionStorage stringify; LSP alive; identity-churn chain re-runs effects every render
Station
CPU + IO while user is elsewhere (worst during agent runs)
high
medium
4
Worker projection re-runs the full history on every sourceVersion bump even when the events array is reference-identical (sessions ≥ 2,000 events)
Chat
3×O(n) main-thread + worker + clone per push
high
small
5
mainPaneTabsAtom allocates a fresh array on every store write → ~14 useWorkStationTabs() callers (ChatPanel root, every tool-call card, CodeEditor, AppShell) re-render per tab write
Tabs
Render fan-out
high
small
6
Hidden Agent-Station simulator: es_get_child_sessions IPC per event, full render per event, rAF scroll loop, 5× JSON.stringify in memo comparators
Station
IPC + CPU at agent event rate
medium
small
7
xterm (~300 KB + CSS) lands in the shell chunk through the TerminalCore/exports barrel — two ingress points, both only need a type + one helper
Bundle
Startup RAM/CPU
medium
one-liner
8
Composer draft writes replace the whole sessionsAtom every 300 ms while typing; terminalSessionsAtom whole-list subscribed at ChatPanel root and in every tab pill (plus sync localStorage per OSC title tick)
Chat
Pane-wide re-render while typing / while a CLI agent runs
medium
small
9
Hidden browser host: capture-phase scroll + subtree MutationObserver stay live; hidden session title change → full layout-atom rewrite (#2) + second sync persist of all historyEntries; AppShellContent re-renders on every session mutation
Station
CPU per scroll / per navigation
medium
small
10
Messages view (My Station → Communication) rebuilds its entire view model at 20 Hz during streaming because messagesEventsAtom injects live text instead of the sentinel chatEventsAtom uses
Station
O(all assistant text) per flush
medium
small
11
Memory stranded for app lifetime: dirty file buffers on "Discard & close", base64 image drafts in localStorage per session (never GC'd), detached chat rows in observedRowsRef
All
RAM growth
medium
small
12
Per-line Prism instances in ModernCodeViewer amplify every remount (#1) — a 500-line diff = 500 tokenizer runs per tool completion
Chat
CPU
medium
medium
Tabs store & tab bar
One canonical atom (workstationTabsStateAtom: shared partition + global workspace + per-session workspaces) projected through workstationLayoutAtom into a {mainPane} compatibility view. Every writer goes through that projection and persists synchronously.
Mechanism.persistWorkstationTabsState stringifies and setItems shared, globalWorkspace, legacy seed, everysessionWorkspaces[id], then the manifest — on every setAndPersist, inside the Jotai write, before React renders. The workstationLayoutAtom writer never compares nextLayout to previousLayout, so no-op updaters (clicking the already-active tab, updatePane returning prev, every updatePaneState in useTabContentSync) still pay full price. N grows for life: useLaunchpadTab materializes a session workspace (and a localStorage key) for every session ever viewed in the WorkStation, even with no real tab. Persisted tab data is not metadata-only: terminal-content.content (full terminal text), dom-component-preview.jsonText, git-log.commandOutput, subagent-detail.resultContent, agent-config.entitySnapshot all get re-stringified on every tab switch anywhere in the app.
Refactor (no UX change).
setAndPersist: if (next === state) return;. Layout writer: bail when nextLayout.mainPane === previousLayout.mainPane.
Dirty-tracking: persist only slices whose identity changed (splitPanel already produces fresh objects only for the touched workspace); keep a lastPersisted map.
Coalesce to one write per tick via queueMicrotask; flush on visibilitychange / beforeunload.
Move bulky strings to an in-memory Map<tabId,string> side-store cleared in closeWorkstationTabsAtom; persist a marker and use the existing "no longer available" restore path.
Treat a workspace whose only tab is the shared start:main ref as empty → delete the key instead of writing it; LRU-cap sessionWorkspaces (~50) evicting only tab-less ones.
T2. composePanel allocates a new tabs array on every state write — fan-out to ~14 subscribers — high, confirmed
Mechanism. Jotai's Object.is bail never fires for mainPaneTabsAtom, including on writes to a non-presented workspace, browser isLoading flips and hasUnsavedChanges flips. Confirmed subscribers that re-render per write: AppShell root, AppShellContent, CodeEditor (twice — useWorkStationTabs and useEditorCache), EditorMainPane, BrowserLayout while hidden, ProjectManagerLayout, FocusedChatWorkstationRail, the ChatPanel root, and every OrgtrackEnvelopeCard in the transcript (each also runs a claimLegacySeed effect on mount). 11 of the 14 useWorkStationTabs() callers only use openTab/updateTabData yet pay the array subscription.
Refactor.
Structural memo in composePanel: reuse the previous tabs array when length and every element are ===; reuse PanelState when tabs + activeTabId unchanged (pattern already used in openEditorFilePathsAtom:524-553). A tab switch then leaves mainPaneTabsAtom identity intact.
Split useWorkStationTabActions() (all useSetAtom, zero subscriptions) from the value hook; move claimLegacySeed to a single AppShell mount point.
useEditorRepoCacheSync should not go through useEditorCache (subscribes to layout + cache + repo cache + size); make switchRepo a write-only atom.
projectBrowserSlice returns the previous slice when the filtered list and active id are unchanged.
T3. Unstable onCloseClick re-renders every SortableTab on every tab write — medium-high, confirmed
entries = useAtomValue(tabRegistryAtom) is only read inside the callback but sits in its deps, so closeTab identity changes per write → handleTabClose → handleCloseClick → every memoized SortableTab re-renders (each runs useSortable, useTranslation, icon resolution, favicon, tooltip wiring). Same shape for handleTabReorder.
Refactor. Read lazily via useAtomCallback((get) => get(tabRegistryAtom)); deps become [closeTab, t].
T4. Browser session sync rebuilds every browser tab object per session mutation — medium, confirmed
Effect 1 is ungated on isActive and runs on any browserState.sessions change (isLoading true/false per navigation, URL, title, history). It calls createBrowserSessionTab for all sessions → fresh tab + data objects → setBrowserTabs updater always returns a fresh object → workstationLayoutAtom → T1's N+5 writes. Fresh tab objects also defeat SortableTab memo. sharedBrowserTabsAtom is a bare .filter(), so Effect 3 re-runs on every workstation tab mutation anywhere.
Refactor. Reuse prev tab object when (url, incognito, isLoading, title) are unchanged; return prev when the list is element-wise identical.
T5. Tab-header publishers pass fresh literals → atom write + header re-render per token batch — medium, confirmed
The hook memoizes on content identity only; 20 call sites pass inline object literals and inline JSX. ChatSessionTabRenderer re-renders on every sessionByIdAtom change (streaming), so during a turn the header atom is written and WorkstationTabHeader + its slots re-render per batch.
Refactor. Normalize then shallow-compare the six slot fields against ownedContentRef.current; skip setHeader when equal. Callers useMemoheaderTrailing.
T6. Second legacy tab store (orgii-global-tabs) still written on every browser/terminal change, almost never read — medium, confirmed
Fed by four contexts; every URL/title change → JSON.stringify of the whole store to localStorage, plus a redundant second setActiveBrowserTab write per activation. The only app reader is a URL fallback the primary lookup already covers, and a close_pty side-effect that belongs in TerminalContext. useGlobalTabCounts / useGlobalSessionTabs / useGlobalShortcutTabs / useGlobalDocumentTabs have no non-test consumers.
Refactor. Move close_pty into TerminalContext, drop the fallback (or read sharedBrowserTabsAtom), delete the sync hooks + atom, one-time removeItem("orgii-global-tabs").
T7. Smaller tab-bar costs — low, confirmed
shared/TabBar/hooks/useTabGitInfo.ts:91-116 — builds two full suffix maps over the git status map on every status refresh even with zero file tabs; skip when no file tabs, memo on a joined-paths key.
shared/dnd/useSessionTabDropTarget.ts:107 — every drop target keeps a document pointermove listener for life (early-returns, but runs per mouse move and re-subscribes on eligibleDrag flips); attach in handleStart, detach in reset. shared/TabBar/hooks/useTabDrag.ts:217, 411-416 — window tracker has no unmount cleanup.
AppShell/useWorkstationTabList.ts:54-82, CodeEditor/index.tsx:150-170, 250-254 — pinned/hidden-tab partitioning runs on every registry change though no tab is ever pinned; usePinnedTabs is called with enabled:false yet still subscribes and rebuilds; the launchpad-dashboard scan can never match (type not in VALID_WORKSTATION_TAB_TYPES).
My Station — hosts kept mounted behind display:none
The bounded keep-alive in hostMountPolicy.ts is good. The gap: isActive passed to CodeEditor means "code host mode", not "visible" — it stays true on chat-session / settings / git-diff tabs — and the project host and simulator receive no visibility signal at all. Only useCodeEditorLocalState and set_active_git_polling_repo gate on it today.
S1. Code host: file-watch fan-out re-reads every open file from disk while hidden — high, confirmed
enabled: true is hardcoded; EditorIntegrations isn't even given isActive. Every backend repo:status_updated push while an agent writes files → per changed file: log() → appendToChannel (React state → CodeEditor subtree render) andonExternalFileChange → every mounted useFileContent subscriber re-reads from disk. The match at :325 is changedPath.endsWith("/" + basename), so any index.ts anywhere in the repo reloads every open index.ts. No batching, no rate limit.
Refactor. Thread host visibility into EditorIntegrations; while hidden, buffer the change set and apply once on re-activation. Match on full relative path. Batch log() per push.
S2. Code host: identity-churn chain makes four effects + a connect/disconnect run on every render — high, confirmed
useOutputChannels returns channels = Array.from(map.values()) and a fresh object every render; useGitDiffState, useGitOutputIntegration, useTaskOutputIntegration likewise. CodeEditor itself re-renders on every tab write (T2), every git-status poll (currentGitStatusAtom via useRepoSelection), every layout mutation (useEditorCache). Each render: React.memo(EditorIntegrations) is defeated → GUIAgentService.disconnect()/connect() (drops logs that land in between), setGitOutputIntegration / setTaskOutputIntegration atom writes that re-render the Source Control sidebar and commit form, useTaskOutputIntegration's channel-find effect. gitDiffState: { setFiles, addTab } is an inline literal in useCodeEditorEvents' deps, so all 8 document/window listeners tear down and re-register per render (7 of them ungated by isActive, several doing async IPC).
Refactor.useMemo the four hook returns (callbacks are already stable); useMemo the gitDiffState literal; drop selectedFile/selectFile/gitDiffState from the listener effect's deps (the optionsRef at :95-100 exists precisely for this). Pure identity work — no lifecycle change.
S3. Code host: output channels JSON.stringify up to ~1.6 MB into sessionStorage every 500 ms of activity — medium, confirmed
CodeEditor/hooks/output/useOutputChannels.ts:102-117 — debounced persist of every channel's full content (16 × 100k chars cap), synchronous, 500 ms after any append — driven continuously by S1's logging while an agent works and the editor is hidden.
Refactor. Persist only the changed channel (per-channel keys); lengthen the debounce while hidden, or persist on visibilitychange/unmount only.
S4. Code host: file-tree reload over IPC while hidden; multi-root variant lacks even the document.hidden gate — medium, confirmed
Structural WS deltas trigger a 500 ms-debounced full tree reload (list_directory_tree over the expanded subtree + .gitignore re-read) gated only on document.hidden, not host visibility. The primary sidebar is never unmounted — collapse is width:0 — so useDisplayData deep-copies the whole tree (convertToTreeNode) and flattenTree walks it again on every fileTree identity change, even on chat-session tabs and behind the browser host. loadFileTree has no in-flight guard.
Refactor. Gate the WS reload on host visibility (mark dirty, reload once on activation); skip convertToTreeNode/filterTree when collapsed; single-flight loadFileTree.
S5. Code host: everything refires on repoPath change regardless of visibility; LSP always alive — medium, confirmed
A workspace switch while on a browser tab fires: full readDir tree, checkIsGitRepo, git status force-refresh, gitStashList (only to render a badge in a tab that isn't open), /default-branch HTTP, loadCurrentBranchFast, lspClientManager.setRootPath. LSP servers stay spawned for the whole session (enabled: true).
Refactor. Defer repo-change fan-out until the host is visible (a "stale since" flag); gate useStashCount on the Source Control tab like useGitWorktrees already is; consider idle-stopping LSP after N minutes hidden.
S6. Agent-Station simulator: zero visibility awareness, full work at agent event rate while hidden — medium, confirmed
ActivitySimulator takes no props; mount is gated (hasVisited && hasActiveSession) but once warm it runs at full cost behind display:none: an es_get_child_sessions IPC per eventCount change, N subscription teardown/re-setup per subagent array identity change, six atom subscriptions (sortedEventsAtom, eventStoreVersionAtom…) re-rendering the whole tree per event, O(all events) executionThreads recompute, five JSON.stringify per event in memo comparators (one comparison branch is unreachable dead work), and a 60 fps rAF scrollTop loop on a node with no box (browsers throttle hidden documents, not hidden subtrees).
Refactor. Thread isActive from AppShellContent:196; while hidden, unsubscribe atoms / pause rAF; trailing-debounce the child-sessions query (~250 ms) and keep Date.now() out of mapped identity; compare args/result by reference (events are immutable store snapshots).
S7. Browser host: observers and persistence ungated while hidden; AppShellContent subscribes to the whole context — medium, confirmed
SharedBrowserHostSlot installs a ResizeObserver, a subtree MutationObserver and a capture-phasescroll listener regardless of active — every scroll in chat or the file tree schedules a rAF + getBoundingClientRect. BrowserProvider persists every session including full historyEntries synchronously on each sessions change (a second flush on top of the tab-sync one). The provider value is invalidated by sessions via three paths; AppShellContent reads it just for sessions.length, so a hidden tab's title change re-renders the shell owning all hosts. Status-bar callbacks deliberately clear only on real unmount, so the browser's onToggleDevTools / onPrevSession stay registered after the host hides (plausible bug).
Refactor. Early-return the slot's useLayoutEffect when !active; debounce the provider persist; lift hasBrowserSessions into a boolean atom for the shell; clear callbacks when isActive flips false.
S8. Project host: no visibility signal; fetches on every data-changed event while hidden — low-medium, confirmed
ProjectManagerCore takes only {repoPath, repoName}; the only isActive downstream is tab-level, so the active project tab fetches while the whole host is hidden. useProjectDataChanged refetches the project list on every backend orgii-data-changed. Cancellation discipline is good (generation + mounted guard).
Refactor. Pass host visibility; mark stale while hidden, refetch once on activation.
S9. Terminal: all xterm instances torn down on every terminal↔other tab switch; warm terminals drain at foreground rate — medium, confirmed
shouldMountTerminalContent = isTerminalTabActive, so leaving the terminal tab unmounts TerminalCore → up to 5 TerminalViews dispose WebGL + terminal.dispose() + detach_pty_stream; returning re-creates all 5 and re-attaches. The MAX_WARM_INACTIVE_TERMINALS = 4 window only survives within the terminal tab. The opacity-0 branch at :407-412 is unreachable. Warm (inactive) terminals are not told they're background, so they don't use the output scheduler's 50 ms coalescing path.
Refactor. Keep TerminalMainContent mounted (hidden) while any terminal session exists — the hidden branch already exists; pass isForeground={session.id === activeSessionId} to TerminalView.
S10. Messages view (Communication) rebuilds the full view model at 20 Hz during streaming — medium, mechanism confirmed (ms/flush needs measurement)
messagesEventsAtom injects the real live delta text (unlike chatEventsAtom's sentinel), so it returns a fresh array per flush → isFinalAssistantDuplicate runs normalizeEventText over every assistant message, replayPrefetchEntries remaps O(n), buildMessageLists' WeakMap misses every flush (≤320 convertToMessageEntry), the sort allocates new Date() per comparison, and every visible ChatBubble memo busts.
Refactor. Use the sentinel — LiveChatBubble and MessageViewer already read live text straight from the delta atom, so rendered output is identical; pre-parse timestamps once in convertToMessageEntry.
S11. WorkStation shell re-renders per simulator event via useCurrentTurnLastAgentMessage — low-medium, confirmed
Builds new Map(simulatorEvents.map(...)) in render, unconditionally, for a caption only shown in Agent Station; effectiveSimulatorEventIdsAtom has no reference stability so this re-runs on every push.
Refactor. Read eventIndexAtom (O(1), reference-stable); add a sameIdList guard like chatEventsAtom; only call the hook when isAgentStation.
WorkStationPage is mounted with isActivehard-coded true (src/modules/index.tsx:432-434); only chatPanelFocused varies. A maximized chat animates the workbench to width:0, not display:none, so every host stays "active" behind maximized chat — only portsEnabled and LspInstallPrompt consult chatPanelFocused.
CodeEditor's isActive is codeContentVisible === isCodeMode (AppShell/hooks/useAppShellDerivedState.ts:33) — false only on Browser/Project tabs.
S12a. useWorkspacePortAdvertisedUrls double-decodes every PTY byte and re-subscribes all Tauri listeners on every terminal metadata change — medium, confirmed. src/modules/WorkStation/shared/StatusBar/utils/useWorkspacePortAdvertisedUrls.ts:112-245 (deps [enabled, sessions] at :246), called from AppShell/index.tsx:131. A second pty-output-* listener per session (the xterm host already has one) runs TextDecoder.decode + URL regex on every chunk. Because the dep is the whole terminalSessionsAtom array, every processName / liveCwd / agentStatus update (each command start/finish via useTerminalProcessPoller → updateTerminalSessionInfoAtom) tears down every listener (async IPC each), re-listens, and drops partial-line buffers — and re-renders AppShell. Refactor: move the hook into the already-null-rendering WorkspacePortScanner; key on selectAtom(terminalSessionsAtom, s => s.map(x => x.id), shallowEqual) with an incremental per-session listener Map; longer term, detect advertised URLs once in the terminal output scheduler or in Rust.
S12b. AppShell subscribes to 9 layout atoms via useWorkStationPanels(); sidebar/bottom-panel resize writes localStorage per animation frame — medium, confirmed. AppShell/index.tsx:86 uses only layoutMode, two toggles and bottomPanelCollapsed but subscribes to sidebar width, bottom height, terminal sidebar width, devtools, sidebar tab, bottom tab. src/hooks/ui/useResizeHandle.ts:137 calls onSizeChange on every rAF during drag → src/store/ui/workStationLayout/primarySidebarAtoms.ts:123-131 / bottomPanelAtoms.ts:104-111 call setStoredValue(...) per frame, and AppShell + AppShellContent re-render per frame. Refactor: use usePrimarySidebarState + useBottomPanelState (already exist in useWorkStationPanels.ts:182-247) in AppShell; debounce storage in the persist atoms or write on onResizeEnd.
S12c. BrowserSession.historyEntries is unbounded and re-serialized on every isLoading toggle — medium, confirmed. Browser/Panels/BrowserMainPane/content/WebViewportContent/index.tsx:216-224 appends with spread, no cap; BrowserContext.tsx:171-178, 193-195 stringifies every session including the full history on every sessions change. Refactor: cap at the producing boundary in updateSession (e.g. 200 entries per session).
S12d. Smaller:AppShell/index.tsx:46_titleBarHidden is a dead atom subscription; useAppShellDerivedState.ts:29-35 carries three identical *ContentVisible copies of isCodeMode etc.; useMultiRootFileTree.ts:80rootTreesRef Map is never pruned (bounded only by the 20-root load cap); useMultiRootFileTree.ts:459-460 and useRepoLoader.ts:99-124 read/write refs during render.
Bundle boundaries
B1. xterm + its CSS in the shell chunk via TerminalCore/exports — medium, confirmed
Both files need only getTerminalDisplayTitle (defined in TerminalCore/types.ts:58) and a type, but import from the barrel that re-exports the default component. No "sideEffects": false in package.json, so webpack keeps the chain. TerminalTab.tsx is dead (its hook is commented out) but still reachable from tabs/index.ts, and its "lazy-load to keep xterm out" comment is wrong — it statically imports the same module it lazy-loads.
Refactor. Import from engines/TerminalCore/types; delete TerminalTab.tsx + re-export. Verify with pnpm analyze.
B2. All CodeMirror language packs eager in the editor chunk; dynamic imports are dead weight — low, confirmed
src/features/CodeMirror/shared/languageExtensions.ts:7-19 vs :207-283 — top-level static imports of ~10 Lezer grammars sit next to await import("@codemirror/lang-*") branches that therefore load an already-loaded chunk. Not in the shell chunk, but on the editor chunk's critical path. Drop the static imports.
Chat pane (UI layer)
One chat column; session tabs share a single SessionContentView and re-point the Rust-backed pipeline on switch (so the "kept mounted to preserve the virtualizer cache" comment in ChatPanelContent.tsx:53-55 only holds for alternate views, not tab switches). Fine by design — it just means the remount costs below are paid on every tab switch too.
getItemKey joins chunk_id : displayStatus : activityStatus : displayText.length for every item in the group. Any tool flipping running→completed or output landing changes the React key of the whole turn row → React unmounts and remounts the entire turn (all tool blocks, all code blocks, every block-local state, every effect), Prism re-tokenizes every line, TanStack's size cache misses (keyed by the same string) → falls back to the 360 px estimate → layout shift the follow-tail logic then corrects with 5 scrollTos. getItemKey is a fresh closure each render and is a dep of TanStack's measurements memo, so every list render (every snapshot push, footer change, at-bottom flip) rebuilds measurements from index 0 and runs slice().map().join() over the entire transcript. git log -L shows the content-in-key scheme (33cce0c19) predates the per-row RO that now makes it redundant; the ≤24-item static path already uses stable chunk_id keys.
Separately, measureVirtualRow observes each node in a custom RO and calls virtualizer.measureElement (which registers it with TanStack's own RO) — every resize runs resizeItem twice — and observedRowsRef: Set<Element> strongly holds every row ever mounted (the ref callback ignores null) until virtualListDataKey changes; after that change, still-mounted rows are silently no longer observed by the custom RO.
Refactor. Key groups by identity — turnIds[i] ?? firstItem.chunk_id ?? i — precomputed in a useMemo and served by a useCallbackgetItemKey; height changes already reach the virtualizer via RO. GroupItemRenderer's comparator already includes displayText/displayStatus, so rows update correctly. Delete the custom RO and pass ref={virtualizer.measureElement} (or use React 19 ref-cleanup to unobserve). Regression-check the padded-tail scenario from 33cce0c19.
C2. Worker projection re-runs the full history per sourceVersion bump with identical events — high, confirmed
For sessions ≥ 2,000 events, the effect keys on sourceVersion; hasNewerSourceVersion → projectDelta regardless of whether events changed. Per Rust push during a tool-heavy turn: buildProjectionDelta (Map + Set over all events, main thread), projectDelta (two more O(n) Maps, posts n ids), worker applies the empty delta and calls projectChatHistory over everything, structured-clones the full result back, then sameFlatItems deep-walks every item with allocations on the main thread — and almost always concludes "different" after scanning everything that was equal. chatEventsAtom's fast path already proves most of these frames are unchanged.
Refactor. In useChatProjection: if previous.events === events && previous.options === options, just bump the stored sourceVersion and reuse workerState.projection. In client.projectDelta: short-circuit when upserts.length === 0 && removedIds.length === 0. In list equality: compare projectionRevision/itemShapeDigest (already computed by the worker) before any deep walk.
C3. Draft typing churns the global sessionsAtom every 300 ms — medium, mechanism confirmed
setDraft → usePatchSession optimistic upsertSession replaces the entire sessions array and session object. Per tick: ChatPanel root (usePanelTitle), ChatView (recomputes initialFileChanges, fork handler), ChatHistory (usePinnedSession), every TabPill, ConversationStreamProvider (sessions.find), and every sessions-list subscriber outside the pane. The pending draft is also dropped if sessionId changes before the timer fires (:399-406).
Refactor. Keep the DB patch; make the optimistic mirror a sessionDraftTextAtomFamily(sessionId) seeded from session.draftText; fold into sessionsAtom on blur/send; flush (not cancel) on session change. Composer lifecycle untouched.
C4. terminalSessionsAtom whole-list subscribed at the pane root and in every tab pill — medium, confirmed
TUI agents rewrite the OSC title continuously → updateTerminalSessionInfoAtom → new array + synchronous localStorage.setItem(JSON.stringify(sessions)) per tick. The controller only needs four fields inside a callback; each pill only needs its own agentStatus. Result: the whole ChatPanel (header with ~50 props, content, every pill) re-renders per title tick for a 6 px dot.
Refactor. Controller reads via store.get inside the callback; pills use selectAtom(terminalSessionsAtom, s => s.find(...)?.agentStatus); read chatPanelCreateTargetAtom only in a split StartPagePill; debounce the terminal persist.
C5. Root components subscribe to the raw 30 Hz snapshot / version — medium, confirmed
ChatView reads the whole derivedSnapshotAtom for three derived fields → the 600-line body (~35 hooks) re-runs per Rust envelope, and passes members ?? [] (fresh array) into memo deps. useChatHistory's selector returns a fresh {chatHistory, sourceSessionId, sourceVersion} per push even when chatHistory is reference-stable. useEventStoreSelector subscribes to eventStoreVersionAtom, so hosts re-render per bump and isEqual only stabilizes the return value. useStreamingHud subscribes to the whole delta Map (re-renders the header for any session's deltas) and memos on the full string when only .length is used.
Refactor.selectAtom with equality for each field; hoist EMPTY_MEMBERS; split sourceVersion into its own selector consumed only by useChatProjection; re-implement useEventStoreSelector over selectAtom(eventsAtom, selector, isEqual).
C6. Every visible assistant row subscribes to the live delta; composer does O(n) scans per keystroke — medium, confirmed
useStreamingDeltaForSession(sessionId) is called unconditionally in AgentMessageEvent; only the synthetic live row uses it, but (visible rows + overscan) re-render at 20 Hz and stripThinkTags runs un-memoized (two regex passes over each historical message). LiveActivityRow, ChatBubble, MessageViewer already isolate the subscription correctly. In the composer, sessionHasComposerStopBlockingWork and countChatRounds run over sortedEventsAtom on every render — every keystroke, and every non-streaming snapshot.
Refactor.useStreamingDeltaForSession(isSyntheticLive && isStreaming ? sessionId : null); memo stripThinkTags. Derived atoms chatRoundCountAtom / composerStopBlockingWorkAtom so the composer re-renders only when the value flips. No send/stop semantics touched.
C7. Follow-tail: 5 scrollTos per trigger, three observers on the same scroller — medium, plausible (needs trace)
scheduleSettledFollow does one immediate + four chained-rAF layout-read/scroll-write pairs, triggered by a root RO, a key effect and a new-item effect; each programmatic scroll also fires onScroll → bottom check + active-group report + onEndReached. Two more hooks observe the same root/first child.
Refactor. One rAF that re-checks distance-to-bottom and re-schedules only while > ε (bounded to 4); one shared root observer fanning out; mark programmatic scrolls so onScroll can skip reporting.
src/features/CodeViewer/ModernCodeViewer.tsx:129-168 — each CodeLine mounts its own highlighter (separate tokenizer run + subtree per line; multi-line tokens like block comments are tokenized incorrectly). CodeLine is memo'd so streaming appends are cheap — but every remount from C1 re-pays it for every code block in the turn. Tokenize the whole block once (memo on [content, language]), slice the token stream into lines, keep the row windowing.
C9. Image drafts: base64 payloads in localStorage, one key per session, never GC'd — medium, confirmed
ChatPanel/InputArea/utils/imageDraftCache.ts:33-43 · ChatPanel/hooks/useInputArea/index.ts:420-423 — setItem(JSON.stringify(images)) with data URLs on every attachment-array change; only clearImageDraft on submit; uncaught on quota; parsed synchronously on every session switch. Cap bytes, try/catch, clear from the session-delete path or sweep orphan keys at startup.
C10. Smaller chat costs — low, confirmed
ChatHistory/hooks/useChatHistoryProjectionModel.ts:156-174 — diagnostics memory estimator walks the projected tree (≤5,000 nodes) on every projection change; its only reader is the RAM-stats panel. Gate on "panel mounted".
ChatPanel/index.tsx:199-218 — cloud-org reconciliation refires readOrgs() on every roster refresh because the request always writes a new array; key on a sorted ids string.
ChatItems/AgentChatItemDefault.tsx:87-89 — expand prop mirrored into state per message row. blocks/primitives/BlockOutput.tsx:211-240 — re-creates its RO per streamed chunk and setScrollTops per scroll pixel.
Streaming data path (below the UI)
Ingestion is in good shape: no per-token atom write, RPC, or persistence. The costs are redundant parsing, cache sizing, and switch-time round-trips.
D1. Every Channel frame is JSON-parsed + zod-validated twice (three times with the work-item panel open) — medium, confirmed
validateSessionChannelMessage parses + schema-walks, then discards the result and forwards the raw string; the router parses again; extra subscribers parse again just to check type. Tool-result frames can be 100 KB+. Parse once in subscribeToSessionEvents and hand subscribers {raw, parsed}; replace the two includes() logging scans with parsed.type.
D2. JS snapshot LRU (5) < MULTI_RUNNER_MAX (6): background runners can thrash full-snapshot IPC fetches — medium, mechanism confirmed
A delta for a session with no JS cache triggers a full getSnapshot RPC + normalize + materialize, evicting the oldest entry — possibly another actively-mutating runner. Rust emits es:changed for every session whether or not JS has a consumer. Drop deltas for sessions that are neither active nor have listeners (next mount re-primes), or evict listener-less sessions first, or set the cap ≥ runners + 1.
D3. Session switch round-trips the transcript across IPC ~4× — low-medium, confirmed (switch-time only)
SessionCore/sync/sessionSwitchOrchestrator.ts:136-147 · SessionCore/core/atoms/actions.ts:404-440 — getEvents → loadInitialTurnWindow → getEvents again; JS builds a full DerivedSnapshot, then mergeEvents sends the whole list back into Rust (a no-op dedup on cache hit), which re-emits a full snapshot JS re-normalizes. Return events from loadInitialTurnWindow; skip mergeEvents when nothing synthetic was added.
D4. Per-token string accumulation flattens the V8 rope every token — low, confirmed (bounded at 500 KB)
SessionCore/sync/adapters/shared/streamTextAccumulator.ts:47, 82-100 — findSuffixPrefixOverlap slices the accumulated cons-string every delta → O(accumulated length) per token, quadratic over a turn. Keep a ≤4 KB tail for overlap detection; append with plain +=; flatten at flush.
Correctness bugs found on the way
Not the question asked, but they surfaced while tracing lifecycles and are cheap to fix alongside.
Bug
Where
Effect
(dormant) ⌘W bridge registered by hidden hosts
ProjectManagerLayout/index.tsx:204-207, EditorMainPane/index.tsx:211-214 (enabled: true); bridge at src/hooks/tabHost/useWorkStationTabShortcutBridge.ts:33 is a global listener
Correction after verification: nothing in src/ dispatches workstation-close-active-tab today, so this is dead wiring, not a live bug. The moment a dispatcher is added, with Browser active and Code + Project warm, one ⌘W would fire three close handlers — Code and Project would silently close their active tabs (only Browser gates on isActive). Gate on host visibility before reviving it.
"Discard & close" strands the dirty buffer and resurrects discarded edits
useCloseTabWithGuard.ts:27-46 never calls discardChanges(); fileContent/useFileContent.ts:99-112 caches with dirty:true; fileContent/cache.ts:48-57 never evicts dirty entries
Full file text + edit log retained for app lifetime per discarded file; reopening restores the "discarded" content as unsaved. Fix in closeWorkstationTabsAtom (single canonical close).
Cross-root .gitignore contamination
CodeEditor/hooks/useCodeEditor/helpers.ts:40-55 — one module-scope checker; useMultiRootFileTree calls ensureGitignoreChecker per root inside Promise.all
Files in root B filtered by root A's ignore rules, whichever resolves last wins.
Unmount between the two await listen()s and the ref assignment leaves both Tauri listeners registered for process lifetime.
(plausible) Browser status-bar callbacks stay registered after the host hides
Browser/hooks/useBrowserStatusBar.ts:130-134
Cleanup deliberately only on real unmount; isActive→false leaves onToggleDevTools etc. in the callbacks atom.
(conditional) useNarrowChatFocus document-wide MutationObserver can become permanent
src/modules/useNarrowChatFocus.ts:240-249
Disconnects only once both [data-workbench-surface] and [data-main-content] exist; if either is absent it runs two querySelectors on every DOM mutation for the session. Cap attempts.
composePanel: reuse previous tabs array when element-wise identical
useCloseTabWithGuard: read registry via useAtomCallback
closeWorkstationTabsAtom: clearUnsavedContentCache(filePath) for closed file tabs
My Station
Import getTerminalDisplayTitle from TerminalCore/types (×2) — xterm out of the shell chunk
useMemo the returns of useOutputChannels, useGitDiffState, useGitOutputIntegration, useTaskOutputIntegration; useMemo the gitDiffState literal in CodeEditor/index.tsx:181
SharedBrowserHostSlot: early-return the layout effect when !active
useDisplayData: skip tree conversion when the sidebar is collapsed
useSubagentSessions: trailing-debounce the query key
useWorkStationTabShortcutBridge: pass host isActive as enabled (pre-empts the dormant ⌘W triple-close)
useWorkspacePortAdvertisedUrls: key the listener effect on session ids (selectAtom + shallow array equality), not the whole terminalSessionsAtom
primarySidebarAtoms / bottomPanelAtoms persist atoms: debounce setStoredValue, or persist on onResizeEnd (already supported by useResizeHandle)
BrowserContext.updateSession: cap historyEntries (e.g. 200) at the producing boundary
Chat
getItemKey: identity keys; delete the custom RO
useChatProjection: skip when events reference-equal
AgentMessageEvent: useStreamingDeltaForSession(isLive ? id : null); memo stripThinkTags
useStreamingHud / TabPill: selectAtom to the one field
ChatView: three selectAtoms instead of the raw snapshot; hoist EMPTY_MEMBERS
messagesEventsAtom: use the sentinel like chatEventsAtom
Code quality
Duplicated ownership
Three tab-close-with-guard implementations with divergent semantics: useCloseTabWithGuard (Discard/Cancel, never clears buffers), useEditorPaneState.ts:136-263 (Save/Don't Save/Cancel, own mutations instead of tabMutations.ts), ProjectManagerLayout/index.tsx:70-90. Same file tab → different dialog and different data outcome depending on which affordance closed it.
Two tab DnD implementations: ChatPanelTabBar.tsx:752-836 is a hand-copy of shared/TabBar/hooks/useTabDrag.ts (and lacks its unmount cleanup); scrollIntoView-on-activate duplicates useAutoScrollToActive. Extract useSessionTabDrag.
Two persisted tab models (workstation:tabs:v3:* and orgii-global-tabs).
useNarrowChatFocus.ts:183-205 vs :293-320 — the same four-branch edge FSM implemented twice against different variable sources.
Two file-content hooks (useCodeEditor/useFileContent vs fileContent/useFileContent) with different stale-guard behavior.
Oversized files / prop plumbing
ChatPanelTabBar.tsx (971 lines): four components in one — TabPill with a 160-line icon if/else chain (:330-490), hover card, plus-menu, bar + DnD. Split; replace the chain with a Record<ChatPanelTabType, Icon>.
ChatPanel/index.tsx (747): ChatPanelHeader receives ~50 props, ChatPanelEmptyContent ~30. Let the header read a ChatPanelHeaderActionsContext; move cloud-org reconciliation and work-item mirror effects into hooks.
AppShellContent takes 19 props, most derivable from atoms the children could read themselves.
Dead code & stale comments
useFileWatchHeartbeat.ts:122 — the only setInterval in the editor tree, never started (startWatchHeartbeat has no caller).
EditorBottomPanel/tabs/TerminalTab.tsx — hook commented out, file still exported, comment about lazy-loading is wrong.
TabContent/index.ts:9-25 says "STAGED — NOT YET MOUNTED"; it is mounted in three hosts. TabContent/renderers/index.ts is an unused eager barrel that would collapse all 33 lazy chunks if imported. renderers/terminal.tsx:9-15 describes an overlay that EditorMainPane unmounts.
Pinned-tab machinery (usePinnedTabs, hideWhenOthersExist) has no producer; SidebarSlot's keep-alive branch is dormant (no descriptor sets keepAlive).
codeBlockContainerWidth hard-coded undefined yet threaded through four layers of props and comparators; _isPendingCancelRef accepted and ignored; useAgentWorkingRef's "no re-renders" contract doesn't match the code.
SimpleGridCell tail-signature comparison is unreachable (prior branch already returned).
Patterns to adopt repo-wide
Hooks that return objects consumed by memoized children must useMemo the object (the repo already does this in useTerminalState, useDiagnostics, fileContent/useFileContent, useCodeEditor/index — the output/git integration hooks are the outliers).
Derived atoms that return arrays/objects need a structural-identity guard (openEditorFilePathsAtom, chatEventsAtom are the reference implementations).
Hosts kept mounted need an explicit visibility prop; "hidden" must mean "stop doing work, mark stale". visibilityAwarePoller, agentOrgRunViewStore, linearProjectsCache are the models.
Virtual-list keys are identities, never content.
Already done well — don't re-flag
Streaming
Token path bypasses the store; 50 ms trailing buffer; per-session selectAtom with equality; sentinel keeps chatEventsAtom identity stable mid-stream
Rust 100 ms es:changed batching with change-journal deltas; JS rAF coalescing with pointer-copy materialization
Markdown splits into stable blocks, only the trailing one re-parses; lazy markdown/Prism/diff chunks
Worker projection for ≥2,000 events with explicit disposeSession; module caches reset on session departure
All Tauri listen() sites (except search) handle the async-unlisten race; es:changed has a generation token
Hosts & editor
hostMountPolicy.ts: pure, unit-tested, bounded keep-alive; webview engine hoisted out of the host so unmount is possible
visibilityAwarePoller: hidden-pause, single-flight, rerun-after-flight, immediate refresh on visible — all browser polls route through it; console/network polling is dev-only and devtools-open-only
Terminal mount window (active + 4 warm), WebGL slot cap (8) with canvas fallback, triple-bounded buffer cache, event-driven process poller, foreground/background output scheduler, detach_pty_stream on unmount
8/8 large lists virtualized; collapsed sections don't render children; Source Control sidebar is keepAlive:false so PR/issue panels never poll hidden
set_active_git_polling_repo and useCodeEditorLocalState gate on host isActive; useGitWorktrees gates on the active tab; useTestRunner on the testing tab + one-shot guard
All file/draft caches bounded (500/1000/32; 16 MB CSV/XLSX; 64 language extensions; 100 scroll states; 600 hydrated events); linearProjectsCache is the reference single-flight + TTL + LRU
Generation-counter cancellation applied uniformly in project/work-item/Linear loaders
Tabs
Every tab type is a lazy chunk behind one memoized Suspense dispatcher; same-type switches don't remount
tabMutations.ts short-circuits no-ops (the loss is one layer up); load-time sanitization caps 200 tabs/partition; v2→v3 migration keeps a recovery copy
Summary
Read-only RAM/CPU + code-quality audit of three surfaces — the WorkStation tabs store, the My Station hosts (code / browser / project / simulator), and the chat pane — on
develop @ d3c7200ae(2026-08-23). Goal: find work the app does that the user never sees, and refactors that remove it without changing user experience.Method:
react-best-practices+org2-performance-guardpriority order; five parallel deep reads (tabs store, shell hosts, chat UI, streaming data path, lifecycle sweep) plus three sub-reads (CodeEditor hooks, CodeEditor panels, browser/simulator/project). Every finding below was traced to owner → start condition → cadence → cleanup; none are bare grep hits. Top claims were re-verified by hand.Caveat: everything is from reading code. No profiler traces or RSS measurements were taken — mechanisms are confirmed, magnitudes are not. Re-measure before calling any fix a win.
Context that changes the advice: React Compiler is not enabled (esbuild/swc loaders only, no babel plugin, no lint rule), so manual
memo/stable-identity discipline genuinely matters.The big picture
The architecture-level decisions are already right: token streaming bypasses the store (50 ms trailing buffer, per-session
selectAtom,sentinel so projection never re-runs per token); Rust batcheses:changedat 100 ms with change-journal deltas;hostMountPolicy.tsis bounded and unit-tested; every large list is virtualized; all browser/port polling is visibility-aware and single-flight.The waste sits one layer below those decisions: identity churn, content-derived keys, and hosts that are kept mounted without being told they're hidden.
Ranked summary
Ordered by estimated steady-state cost for a typical session (one long agent conversation streaming, a few tabs open, the code host hidden behind chat or a browser tab).
ResizeObserverretains detached rowssetItem), no no-op short-circuit, payload carries terminal text / JSON blobssessionStoragestringify; LSP alive; identity-churn chain re-runs effects every rendersourceVersionbump even when the events array is reference-identical (sessions ≥ 2,000 events)mainPaneTabsAtomallocates a fresh array on every store write → ~14useWorkStationTabs()callers (ChatPanel root, every tool-call card, CodeEditor, AppShell) re-render per tab writees_get_child_sessionsIPC per event, full render per event, rAF scroll loop, 5×JSON.stringifyin memo comparatorsTerminalCore/exportsbarrel — two ingress points, both only need a type + one helpersessionsAtomevery 300 ms while typing;terminalSessionsAtomwhole-list subscribed at ChatPanel root and in every tab pill (plus sync localStorage per OSC title tick)scroll+ subtreeMutationObserverstay live; hidden session title change → full layout-atom rewrite (#2) + second sync persist of allhistoryEntries;AppShellContentre-renders on every session mutationmessagesEventsAtominjects live text instead of the sentinelchatEventsAtomusesobservedRowsRefModernCodeVieweramplify every remount (#1) — a 500-line diff = 500 tokenizer runs per tool completionTabs store & tab bar
One canonical atom (
workstationTabsStateAtom: shared partition + global workspace + per-session workspaces) projected throughworkstationLayoutAtominto a{mainPane}compatibility view. Every writer goes through that projection and persists synchronously.T1. Persist storm: N+5 synchronous localStorage writes per tab action — high, confirmed
src/store/workstation/tabs/storage.ts:271-298·src/store/workstation/tabs/atoms.ts:169-178, 199-211·src/modules/WorkStation/AppShell/hooks/useLaunchpadTab.ts:32-42Mechanism.
persistWorkstationTabsStatestringifies andsetItemsshared,globalWorkspace, legacy seed, everysessionWorkspaces[id], then the manifest — on everysetAndPersist, inside the Jotai write, before React renders. TheworkstationLayoutAtomwriter never comparesnextLayouttopreviousLayout, so no-op updaters (clicking the already-active tab,updatePanereturningprev, everyupdatePaneStateinuseTabContentSync) still pay full price. N grows for life:useLaunchpadTabmaterializes a session workspace (and a localStorage key) for every session ever viewed in the WorkStation, even with no real tab. Persisted tabdatais not metadata-only:terminal-content.content(full terminal text),dom-component-preview.jsonText,git-log.commandOutput,subagent-detail.resultContent,agent-config.entitySnapshotall get re-stringified on every tab switch anywhere in the app.Refactor (no UX change).
setAndPersist:if (next === state) return;. Layout writer: bail whennextLayout.mainPane === previousLayout.mainPane.splitPanelalready produces fresh objects only for the touched workspace); keep alastPersistedmap.queueMicrotask; flush onvisibilitychange/beforeunload.Map<tabId,string>side-store cleared incloseWorkstationTabsAtom; persist a marker and use the existing "no longer available" restore path.start:mainref as empty → delete the key instead of writing it; LRU-capsessionWorkspaces(~50) evicting only tab-less ones.T2.
composePanelallocates a newtabsarray on every state write — fan-out to ~14 subscribers — high, confirmedsrc/store/workstation/tabs/atoms.ts:75-116, 481·src/store/workstation/tabRegistry/atoms.ts:36-44·src/hooks/tabHost/useWorkStationTabs.ts·src/engines/ChatPanel/blocks/ToolCallBlock/cards/OrgtrackEnvelopeCard.tsx:84Mechanism. Jotai's
Object.isbail never fires formainPaneTabsAtom, including on writes to a non-presented workspace, browserisLoadingflips andhasUnsavedChangesflips. Confirmed subscribers that re-render per write: AppShell root, AppShellContent, CodeEditor (twice —useWorkStationTabsanduseEditorCache), EditorMainPane, BrowserLayout while hidden, ProjectManagerLayout, FocusedChatWorkstationRail, the ChatPanel root, and everyOrgtrackEnvelopeCardin the transcript (each also runs aclaimLegacySeedeffect on mount). 11 of the 14useWorkStationTabs()callers only useopenTab/updateTabDatayet pay the array subscription.Refactor.
composePanel: reuse the previoustabsarray when length and every element are===; reusePanelStatewhen tabs + activeTabId unchanged (pattern already used inopenEditorFilePathsAtom:524-553). A tab switch then leavesmainPaneTabsAtomidentity intact.useWorkStationTabActions()(alluseSetAtom, zero subscriptions) from the value hook; moveclaimLegacySeedto a single AppShell mount point.useEditorRepoCacheSyncshould not go throughuseEditorCache(subscribes to layout + cache + repo cache + size); makeswitchRepoa write-only atom.projectBrowserSlicereturns the previous slice when the filtered list and active id are unchanged.T3. Unstable
onCloseClickre-renders everySortableTabon every tab write — medium-high, confirmedsrc/hooks/tabHost/useCloseTabWithGuard.ts:25, 47·src/modules/WorkStation/AppShell/useWorkstationTabList.ts:101-106·src/modules/WorkStation/shared/TabBar/index.tsx:306-312entries = useAtomValue(tabRegistryAtom)is only read inside the callback but sits in its deps, socloseTabidentity changes per write →handleTabClose→handleCloseClick→ every memoizedSortableTabre-renders (each runsuseSortable,useTranslation, icon resolution, favicon, tooltip wiring). Same shape forhandleTabReorder.Refactor. Read lazily via
useAtomCallback((get) => get(tabRegistryAtom)); deps become[closeTab, t].T4. Browser session sync rebuilds every browser tab object per session mutation — medium, confirmed
src/modules/WorkStation/Browser/BrowserLayout/useBrowserTabSync.ts:66-150·src/store/workstation/browser/tabs/index.ts:262-285Effect 1 is ungated on
isActiveand runs on anybrowserState.sessionschange (isLoadingtrue/false per navigation, URL, title, history). It callscreateBrowserSessionTabfor all sessions → freshtab+dataobjects →setBrowserTabsupdater always returns a fresh object →workstationLayoutAtom→ T1's N+5 writes. Fresh tab objects also defeatSortableTabmemo.sharedBrowserTabsAtomis a bare.filter(), so Effect 3 re-runs on every workstation tab mutation anywhere.Refactor. Reuse
prevtab object when(url, incognito, isLoading, title)are unchanged; returnprevwhen the list is element-wise identical.T5. Tab-header publishers pass fresh literals → atom write + header re-render per token batch — medium, confirmed
src/hooks/tabHost/useWorkstationTabHeader.ts:43-58·src/modules/WorkStation/TabContent/renderers/chatSession.tsx:113-172The hook memoizes on
contentidentity only; 20 call sites pass inline object literals and inline JSX.ChatSessionTabRendererre-renders on everysessionByIdAtomchange (streaming), so during a turn the header atom is written andWorkstationTabHeader+ its slots re-render per batch.Refactor. Normalize then shallow-compare the six slot fields against
ownedContentRef.current; skipsetHeaderwhen equal. CallersuseMemoheaderTrailing.T6. Second legacy tab store (
orgii-global-tabs) still written on every browser/terminal change, almost never read — medium, confirmedsrc/store/ui/navigationSidebarTabsAtom.ts:38-48·src/hooks/ui/tabs/useSyncGlobalTabs.ts:58-117·src/util/contextPillContent.ts:51Fed by four contexts; every URL/title change →
JSON.stringifyof the whole store to localStorage, plus a redundant secondsetActiveBrowserTabwrite per activation. The only app reader is a URL fallback the primary lookup already covers, and aclose_ptyside-effect that belongs inTerminalContext.useGlobalTabCounts/useGlobalSessionTabs/useGlobalShortcutTabs/useGlobalDocumentTabshave no non-test consumers.Refactor. Move
close_ptyintoTerminalContext, drop the fallback (or readsharedBrowserTabsAtom), delete the sync hooks + atom, one-timeremoveItem("orgii-global-tabs").T7. Smaller tab-bar costs — low, confirmed
shared/TabBar/hooks/useTabGitInfo.ts:91-116— builds two full suffix maps over the git status map on every status refresh even with zero file tabs; skip when no file tabs, memo on a joined-paths key.shared/dnd/useSessionTabDropTarget.ts:107— every drop target keeps a documentpointermovelistener for life (early-returns, but runs per mouse move and re-subscribes oneligibleDragflips); attach inhandleStart, detach inreset.shared/TabBar/hooks/useTabDrag.ts:217, 411-416— window tracker has no unmount cleanup.AppShell/useWorkstationTabList.ts:54-82,CodeEditor/index.tsx:150-170, 250-254— pinned/hidden-tab partitioning runs on every registry change though no tab is everpinned;usePinnedTabsis called withenabled:falseyet still subscribes and rebuilds; thelaunchpad-dashboardscan can never match (type not inVALID_WORKSTATION_TAB_TYPES).My Station — hosts kept mounted behind
display:noneThe bounded keep-alive in
hostMountPolicy.tsis good. The gap:isActivepassed toCodeEditormeans "code host mode", not "visible" — it staystrueon chat-session / settings / git-diff tabs — and the project host and simulator receive no visibility signal at all. OnlyuseCodeEditorLocalStateandset_active_git_polling_repogate on it today.S1. Code host: file-watch fan-out re-reads every open file from disk while hidden — high, confirmed
src/modules/WorkStation/CodeEditor/hooks/output/useFileWatchOutputIntegration.ts:107-277 (168, 176)·src/modules/WorkStation/CodeEditor/hooks/fileContent/useFileContent.ts:316-332 (325)·src/modules/WorkStation/CodeEditor/EditorLayout/components/EditorIntegrations/index.tsx:141-146enabled: trueis hardcoded;EditorIntegrationsisn't even givenisActive. Every backendrepo:status_updatedpush while an agent writes files → per changed file:log()→appendToChannel(React state → CodeEditor subtree render) andonExternalFileChange→ every mounteduseFileContentsubscriber re-reads from disk. The match at:325ischangedPath.endsWith("/" + basename), so anyindex.tsanywhere in the repo reloads every openindex.ts. No batching, no rate limit.Refactor. Thread host visibility into
EditorIntegrations; while hidden, buffer the change set and apply once on re-activation. Match on full relative path. Batchlog()per push.S2. Code host: identity-churn chain makes four effects + a connect/disconnect run on every render — high, confirmed
CodeEditor/hooks/output/useOutputChannels.ts:120, 270-281·CodeEditor/hooks/useGitDiffState.ts:325-338·CodeEditor/hooks/gitOutputIntegration/useGitOutputIntegration.ts:67, 86, 189·CodeEditor/hooks/output/useTaskOutputIntegration.ts:178-181·CodeEditor/EditorLayout/components/EditorIntegrations/index.tsx:100-162·CodeEditor/index.tsx:181-184→CodeEditor/hooks/useCodeEditorEvents.ts:466-530useOutputChannelsreturnschannels = Array.from(map.values())and a fresh object every render;useGitDiffState,useGitOutputIntegration,useTaskOutputIntegrationlikewise.CodeEditoritself re-renders on every tab write (T2), every git-status poll (currentGitStatusAtomviauseRepoSelection), every layout mutation (useEditorCache). Each render:React.memo(EditorIntegrations)is defeated →GUIAgentService.disconnect()/connect()(drops logs that land in between),setGitOutputIntegration/setTaskOutputIntegrationatom writes that re-render the Source Control sidebar and commit form,useTaskOutputIntegration's channel-find effect.gitDiffState: { setFiles, addTab }is an inline literal inuseCodeEditorEvents' deps, so all 8 document/window listeners tear down and re-register per render (7 of them ungated byisActive, several doing async IPC).Refactor.
useMemothe four hook returns (callbacks are already stable);useMemothegitDiffStateliteral; dropselectedFile/selectFile/gitDiffStatefrom the listener effect's deps (theoptionsRefat:95-100exists precisely for this). Pure identity work — no lifecycle change.S3. Code host: output channels
JSON.stringifyup to ~1.6 MB intosessionStorageevery 500 ms of activity — medium, confirmedCodeEditor/hooks/output/useOutputChannels.ts:102-117— debounced persist of every channel's full content (16 × 100k chars cap), synchronous, 500 ms after any append — driven continuously by S1's logging while an agent works and the editor is hidden.Refactor. Persist only the changed channel (per-channel keys); lengthen the debounce while hidden, or persist on
visibilitychange/unmount only.S4. Code host: file-tree reload over IPC while hidden; multi-root variant lacks even the
document.hiddengate — medium, confirmedCodeEditor/hooks/useCodeEditor/useFileTree.ts:229-236, 251-313·useMultiRootFileTree.ts:368-372, 387-453·CodeEditor/Panels/EditorPrimarySidebar/hooks/useDisplayData.ts:73·shared/WorkStationShell/index.tsx:226-249Structural WS deltas trigger a 500 ms-debounced full tree reload (
list_directory_treeover the expanded subtree +.gitignorere-read) gated only ondocument.hidden, not host visibility. The primary sidebar is never unmounted — collapse iswidth:0— souseDisplayDatadeep-copies the whole tree (convertToTreeNode) andflattenTreewalks it again on everyfileTreeidentity change, even on chat-session tabs and behind the browser host.loadFileTreehas no in-flight guard.Refactor. Gate the WS reload on host visibility (mark dirty, reload once on activation); skip
convertToTreeNode/filterTreewhen collapsed; single-flightloadFileTree.S5. Code host: everything refires on
repoPathchange regardless of visibility; LSP always alive — medium, confirmedCodeEditor/useSourceControlSetup.ts:92, 128-132, 137-144, 171-174·CodeEditor/hooks/diagnostics/useLspDiagnostics.ts:32-49·src/hooks/git/useRepoSelection/useRepoSelection.ts:210-226A workspace switch while on a browser tab fires: full
readDirtree,checkIsGitRepo,git statusforce-refresh,gitStashList(only to render a badge in a tab that isn't open),/default-branchHTTP,loadCurrentBranchFast,lspClientManager.setRootPath. LSP servers stay spawned for the whole session (enabled: true).Refactor. Defer repo-change fan-out until the host is visible (a "stale since" flag); gate
useStashCounton the Source Control tab likeuseGitWorktreesalready is; consider idle-stopping LSP after N minutes hidden.S6. Agent-Station simulator: zero visibility awareness, full work at agent event rate while hidden — medium, confirmed
src/engines/Simulator/hooks/useSubagentSessions.ts:283-307·hooks/useSubagentEventCounts.ts:76-120·hooks/useSimulatorSession.ts:68-74, 138-161·ActivitySimulatorGrid.tsx:37-52·components/GridCell/SimpleGridCell.tsx:55-80·components/CompactEventView/index.tsx:44-62ActivitySimulatortakes no props; mount is gated (hasVisited && hasActiveSession) but once warm it runs at full cost behinddisplay:none: anes_get_child_sessionsIPC pereventCountchange, N subscription teardown/re-setup per subagent array identity change, six atom subscriptions (sortedEventsAtom,eventStoreVersionAtom…) re-rendering the whole tree per event, O(all events)executionThreadsrecompute, fiveJSON.stringifyper event in memo comparators (one comparison branch is unreachable dead work), and a 60 fps rAFscrollToploop on a node with no box (browsers throttle hidden documents, not hidden subtrees).Refactor. Thread
isActivefromAppShellContent:196; while hidden, unsubscribe atoms / pause rAF; trailing-debounce the child-sessions query (~250 ms) and keepDate.now()out of mapped identity; compareargs/resultby reference (events are immutable store snapshots).S7. Browser host: observers and persistence ungated while hidden;
AppShellContentsubscribes to the whole context — medium, confirmedBrowser/shared/SharedBrowserHostSlot.tsx:136-162·src/contexts/workstation/BrowserContext.tsx:171-178, 259-281·AppShell/AppShellContent.tsx:125, 146·Browser/hooks/useBrowserStatusBar.ts:130-134SharedBrowserHostSlotinstalls aResizeObserver, a subtreeMutationObserverand a capture-phasescrolllistener regardless ofactive— every scroll in chat or the file tree schedules a rAF +getBoundingClientRect.BrowserProviderpersists every session including fullhistoryEntriessynchronously on eachsessionschange (a second flush on top of the tab-sync one). The provider value is invalidated bysessionsvia three paths;AppShellContentreads it just forsessions.length, so a hidden tab's title change re-renders the shell owning all hosts. Status-bar callbacks deliberately clear only on real unmount, so the browser'sonToggleDevTools/onPrevSessionstay registered after the host hides (plausible bug).Refactor. Early-return the slot's
useLayoutEffectwhen!active; debounce the provider persist; lifthasBrowserSessionsinto a boolean atom for the shell; clear callbacks whenisActiveflips false.S8. Project host: no visibility signal; fetches on every data-changed event while hidden — low-medium, confirmed
src/modules/ProjectManager/ProjectManagerCore.tsx:34-42·ProjectManager/Projects/index.tsx:205-213·useProjectStatusBar.ts:34-80ProjectManagerCoretakes only{repoPath, repoName}; the onlyisActivedownstream is tab-level, so the active project tab fetches while the whole host is hidden.useProjectDataChangedrefetches the project list on every backendorgii-data-changed. Cancellation discipline is good (generation + mounted guard).Refactor. Pass host visibility; mark stale while hidden, refetch once on activation.
S9. Terminal: all xterm instances torn down on every terminal↔other tab switch; warm terminals drain at foreground rate — medium, confirmed
CodeEditor/Panels/EditorMainPane/index.tsx:389, 405-423·src/engines/TerminalCore/index.tsx:369shouldMountTerminalContent = isTerminalTabActive, so leaving the terminal tab unmountsTerminalCore→ up to 5TerminalViews dispose WebGL +terminal.dispose()+detach_pty_stream; returning re-creates all 5 and re-attaches. TheMAX_WARM_INACTIVE_TERMINALS = 4window only survives within the terminal tab. The opacity-0 branch at:407-412is unreachable. Warm (inactive) terminals are not told they're background, so they don't use the output scheduler's 50 ms coalescing path.Refactor. Keep
TerminalMainContentmounted (hidden) while any terminal session exists — the hidden branch already exists; passisForeground={session.id === activeSessionId}toTerminalView.S10. Messages view (Communication) rebuilds the full view model at 20 Hz during streaming — medium, mechanism confirmed (ms/flush needs measurement)
src/engines/SessionCore/derived/simulatorEvents.ts:118-137·derived/chatEvents.ts:129-151·src/modules/WorkStation/Chat/Communication/useMessages.ts:119-134·Communication/messageViewModel.ts:26-36·Communication/config.ts:205-210messagesEventsAtominjects the real live delta text (unlikechatEventsAtom'ssentinel), so it returns a fresh array per flush →isFinalAssistantDuplicaterunsnormalizeEventTextover every assistant message,replayPrefetchEntriesremaps O(n),buildMessageLists' WeakMap misses every flush (≤320convertToMessageEntry), the sort allocatesnew Date()per comparison, and every visibleChatBubblememo busts.Refactor. Use the sentinel —
LiveChatBubbleandMessageVieweralready read live text straight from the delta atom, so rendered output is identical; pre-parse timestamps once inconvertToMessageEntry.S11. WorkStation shell re-renders per simulator event via
useCurrentTurnLastAgentMessage— low-medium, confirmedsrc/engines/Simulator/hooks/useCurrentTurnLastAgentMessage.ts:84·derived/simulatorEvents.ts:245-268·AppShell/index.tsx:55Builds
new Map(simulatorEvents.map(...))in render, unconditionally, for a caption only shown in Agent Station;effectiveSimulatorEventIdsAtomhas no reference stability so this re-runs on every push.Refactor. Read
eventIndexAtom(O(1), reference-stable); add asameIdListguard likechatEventsAtom; only call the hook whenisAgentStation.S12. Addendum — AppShell-level subscriptions (late-arriving shell synthesis, verified)
Two framing facts first:
WorkStationPageis mounted withisActivehard-codedtrue(src/modules/index.tsx:432-434); onlychatPanelFocusedvaries. A maximized chat animates the workbench towidth:0, notdisplay:none, so every host stays "active" behind maximized chat — onlyportsEnabledandLspInstallPromptconsultchatPanelFocused.CodeEditor'sisActiveiscodeContentVisible === isCodeMode(AppShell/hooks/useAppShellDerivedState.ts:33) — false only on Browser/Project tabs.S12a.
useWorkspacePortAdvertisedUrlsdouble-decodes every PTY byte and re-subscribes all Tauri listeners on every terminal metadata change — medium, confirmed.src/modules/WorkStation/shared/StatusBar/utils/useWorkspacePortAdvertisedUrls.ts:112-245(deps[enabled, sessions]at:246), called fromAppShell/index.tsx:131. A secondpty-output-*listener per session (the xterm host already has one) runsTextDecoder.decode+ URL regex on every chunk. Because the dep is the wholeterminalSessionsAtomarray, everyprocessName/liveCwd/agentStatusupdate (each command start/finish viauseTerminalProcessPoller→updateTerminalSessionInfoAtom) tears down every listener (async IPC each), re-listens, and drops partial-line buffers — and re-renders AppShell. Refactor: move the hook into the already-null-renderingWorkspacePortScanner; key onselectAtom(terminalSessionsAtom, s => s.map(x => x.id), shallowEqual)with an incremental per-session listener Map; longer term, detect advertised URLs once in the terminal output scheduler or in Rust.S12b. AppShell subscribes to 9 layout atoms via
useWorkStationPanels(); sidebar/bottom-panel resize writes localStorage per animation frame — medium, confirmed.AppShell/index.tsx:86uses onlylayoutMode, two toggles andbottomPanelCollapsedbut subscribes to sidebar width, bottom height, terminal sidebar width, devtools, sidebar tab, bottom tab.src/hooks/ui/useResizeHandle.ts:137callsonSizeChangeon every rAF during drag →src/store/ui/workStationLayout/primarySidebarAtoms.ts:123-131/bottomPanelAtoms.ts:104-111callsetStoredValue(...)per frame, and AppShell + AppShellContent re-render per frame. Refactor: useusePrimarySidebarState+useBottomPanelState(already exist inuseWorkStationPanels.ts:182-247) in AppShell; debounce storage in the persist atoms or write ononResizeEnd.S12c.
BrowserSession.historyEntriesis unbounded and re-serialized on everyisLoadingtoggle — medium, confirmed.Browser/Panels/BrowserMainPane/content/WebViewportContent/index.tsx:216-224appends with spread, no cap;BrowserContext.tsx:171-178, 193-195stringifies every session including the full history on everysessionschange. Refactor: cap at the producing boundary inupdateSession(e.g. 200 entries per session).S12d. Smaller:
AppShell/index.tsx:46_titleBarHiddenis a dead atom subscription;useAppShellDerivedState.ts:29-35carries three identical*ContentVisiblecopies ofisCodeModeetc.;useMultiRootFileTree.ts:80rootTreesRefMap is never pruned (bounded only by the 20-root load cap);useMultiRootFileTree.ts:459-460anduseRepoLoader.ts:99-124read/write refs during render.Bundle boundaries
B1. xterm + its CSS in the shell chunk via
TerminalCore/exports— medium, confirmedCodeEditor/Panels/EditorBottomPanel/index.tsx:20-23·CodeEditor/shared/SidebarModules/Terminal/TerminalSidebarRows.tsx:11-14· chain:src/engines/TerminalCore/exports.ts:4-5→index.tsx:25→src/components/TerminalInteractive/index.tsx:23-24·CodeEditor/Panels/EditorBottomPanel/tabs/TerminalTab.tsx(dead)Both files need only
getTerminalDisplayTitle(defined inTerminalCore/types.ts:58) and a type, but import from the barrel that re-exports the default component. No"sideEffects": falsein package.json, so webpack keeps the chain.TerminalTab.tsxis dead (its hook is commented out) but still reachable fromtabs/index.ts, and its "lazy-load to keep xterm out" comment is wrong — it statically imports the same module it lazy-loads.Refactor. Import from
engines/TerminalCore/types; deleteTerminalTab.tsx+ re-export. Verify withpnpm analyze.B2. All CodeMirror language packs eager in the editor chunk; dynamic imports are dead weight — low, confirmed
src/features/CodeMirror/shared/languageExtensions.ts:7-19vs:207-283— top-level static imports of ~10 Lezer grammars sit next toawait import("@codemirror/lang-*")branches that therefore load an already-loaded chunk. Not in the shell chunk, but on the editor chunk's critical path. Drop the static imports.Chat pane (UI layer)
One chat column; session tabs share a single
SessionContentViewand re-point the Rust-backed pipeline on switch (so the "kept mounted to preserve the virtualizer cache" comment inChatPanelContent.tsx:53-55only holds for alternate views, not tab switches). Fine by design — it just means the remount costs below are paid on every tab switch too.C1. Content-derived virtual-row keys remount whole turns; duplicate
ResizeObserverretains detached rows — high, confirmedsrc/engines/ChatPanel/ChatHistory/components/ChatHistoryList.tsx:148-165(getItemKey) ·:168-205(custom RO,observedRowsRef) ·:498(key={virtualItem.key})getItemKeyjoinschunk_id : displayStatus : activityStatus : displayText.lengthfor every item in the group. Any tool flippingrunning→completedor output landing changes the React key of the whole turn row → React unmounts and remounts the entire turn (all tool blocks, all code blocks, every block-local state, every effect), Prism re-tokenizes every line, TanStack's size cache misses (keyed by the same string) → falls back to the 360 px estimate → layout shift the follow-tail logic then corrects with 5scrollTos.getItemKeyis a fresh closure each render and is a dep of TanStack's measurements memo, so every list render (every snapshot push, footer change, at-bottom flip) rebuilds measurements from index 0 and runsslice().map().join()over the entire transcript.git log -Lshows the content-in-key scheme (33cce0c19) predates the per-row RO that now makes it redundant; the ≤24-item static path already uses stablechunk_idkeys.Separately,
measureVirtualRowobserves each node in a custom RO and callsvirtualizer.measureElement(which registers it with TanStack's own RO) — every resize runsresizeItemtwice — andobservedRowsRef: Set<Element>strongly holds every row ever mounted (the ref callback ignoresnull) untilvirtualListDataKeychanges; after that change, still-mounted rows are silently no longer observed by the custom RO.Refactor. Key groups by identity —
turnIds[i] ?? firstItem.chunk_id ?? i— precomputed in auseMemoand served by auseCallbackgetItemKey; height changes already reach the virtualizer via RO.GroupItemRenderer's comparator already includesdisplayText/displayStatus, so rows update correctly. Delete the custom RO and passref={virtualizer.measureElement}(or use React 19 ref-cleanup to unobserve). Regression-check the padded-tail scenario from33cce0c19.C2. Worker projection re-runs the full history per
sourceVersionbump with identical events — high, confirmedChatHistory/projection/useChatProjection.ts:118-201·projection/delta.ts:21-43·projection/client.ts:108-147·projection/runtime.ts:76-96·ChatHistory/components/ChatHistoryListEquality.ts:140-168For sessions ≥ 2,000 events, the effect keys on
sourceVersion;hasNewerSourceVersion→projectDeltaregardless of whethereventschanged. Per Rust push during a tool-heavy turn:buildProjectionDelta(Map + Set over all events, main thread),projectDelta(two more O(n) Maps, posts n ids), worker applies the empty delta and callsprojectChatHistoryover everything, structured-clones the full result back, thensameFlatItemsdeep-walks every item with allocations on the main thread — and almost always concludes "different" after scanning everything that was equal.chatEventsAtom's fast path already proves most of these frames are unchanged.Refactor. In
useChatProjection: ifprevious.events === events && previous.options === options, just bump the storedsourceVersionand reuseworkerState.projection. Inclient.projectDelta: short-circuit whenupserts.length === 0 && removedIds.length === 0. In list equality: compareprojectionRevision/itemShapeDigest(already computed by the worker) before any deep walk.C3. Draft typing churns the global
sessionsAtomevery 300 ms — medium, mechanism confirmedChatPanel/hooks/useInputArea/index.ts:536-538·src/hooks/session/useSessionPatch.ts:383-447·src/store/session/sessionAtom/mutations.ts:66-93setDraft→usePatchSessionoptimisticupsertSessionreplaces the entire sessions array and session object. Per tick: ChatPanel root (usePanelTitle),ChatView(recomputesinitialFileChanges, fork handler),ChatHistory(usePinnedSession), everyTabPill,ConversationStreamProvider(sessions.find), and every sessions-list subscriber outside the pane. The pending draft is also dropped ifsessionIdchanges before the timer fires (:399-406).Refactor. Keep the DB patch; make the optimistic mirror a
sessionDraftTextAtomFamily(sessionId)seeded fromsession.draftText; fold intosessionsAtomon blur/send; flush (not cancel) on session change. Composer lifecycle untouched.C4.
terminalSessionsAtomwhole-list subscribed at the pane root and in every tab pill — medium, confirmedChatPanel/hooks/useChatPanelTabsController.ts:32·ChatPanel/ChatPanelTabBar.tsx:237, 271-277·src/store/workstation/codeEditor/terminal/index.ts:193-210, 521-538TUI agents rewrite the OSC title continuously →
updateTerminalSessionInfoAtom→ new array + synchronouslocalStorage.setItem(JSON.stringify(sessions))per tick. The controller only needs four fields inside a callback; each pill only needs its ownagentStatus. Result: the wholeChatPanel(header with ~50 props, content, every pill) re-renders per title tick for a 6 px dot.Refactor. Controller reads via
store.getinside the callback; pills useselectAtom(terminalSessionsAtom, s => s.find(...)?.agentStatus); readchatPanelCreateTargetAtomonly in a splitStartPagePill; debounce the terminal persist.C5. Root components subscribe to the raw 30 Hz snapshot / version — medium, confirmed
ChatPanel/ChatView.tsx:298·ChatViewHistorySurface.tsx:90, 97, 118·src/contexts/workspace/ChatContext.tsx:227-240·SessionCore/core/store/hooks.ts:31-32·ChatPanel/hooks/useStreamingHud.ts:43-46, 84-90ChatViewreads the wholederivedSnapshotAtomfor three derived fields → the 600-line body (~35 hooks) re-runs per Rust envelope, and passesmembers ?? [](fresh array) into memo deps.useChatHistory's selector returns a fresh{chatHistory, sourceSessionId, sourceVersion}per push even whenchatHistoryis reference-stable.useEventStoreSelectorsubscribes toeventStoreVersionAtom, so hosts re-render per bump andisEqualonly stabilizes the return value.useStreamingHudsubscribes to the whole delta Map (re-renders the header for any session's deltas) and memos on the full string when only.lengthis used.Refactor.
selectAtomwith equality for each field; hoistEMPTY_MEMBERS; splitsourceVersioninto its own selector consumed only byuseChatProjection; re-implementuseEventStoreSelectoroverselectAtom(eventsAtom, selector, isEqual).C6. Every visible assistant row subscribes to the live delta; composer does O(n) scans per keystroke — medium, confirmed
ChatPanel/events/stream/agent-message/index.tsx:329, 340-342, 365·ChatPanel/hooks/useInputArea/index.ts:223, 269-273, 284-288useStreamingDeltaForSession(sessionId)is called unconditionally inAgentMessageEvent; only the synthetic live row uses it, but (visible rows + overscan) re-render at 20 Hz andstripThinkTagsruns un-memoized (two regex passes over each historical message).LiveActivityRow,ChatBubble,MessageVieweralready isolate the subscription correctly. In the composer,sessionHasComposerStopBlockingWorkandcountChatRoundsrun oversortedEventsAtomon every render — every keystroke, and every non-streaming snapshot.Refactor.
useStreamingDeltaForSession(isSyntheticLive && isStreaming ? sessionId : null); memostripThinkTags. Derived atomschatRoundCountAtom/composerStopBlockingWorkAtomso the composer re-renders only when the value flips. No send/stop semantics touched.C7. Follow-tail: 5
scrollTos per trigger, three observers on the same scroller — medium, plausible (needs trace)ChatHistory/hooks/useChatScroll.ts:177-198, 242-246, 262-279, 299-356·hooks/useChatViewportController.ts:143-159·hooks/useChatFooterSpacer.ts:200·hooks/useChatScrollPin.ts:190scheduleSettledFollowdoes one immediate + four chained-rAF layout-read/scroll-write pairs, triggered by a root RO, a key effect and a new-item effect; each programmatic scroll also firesonScroll→ bottom check + active-group report +onEndReached. Two more hooks observe the same root/first child.Refactor. One rAF that re-checks distance-to-bottom and re-schedules only while > ε (bounded to 4); one shared root observer fanning out; mark programmatic scrolls so
onScrollcan skip reporting.C8. Per-line
PrismLightinstances in code blocks — medium, plausible (needs measurement)src/features/CodeViewer/ModernCodeViewer.tsx:129-168— eachCodeLinemounts its own highlighter (separate tokenizer run + subtree per line; multi-line tokens like block comments are tokenized incorrectly).CodeLineis memo'd so streaming appends are cheap — but every remount from C1 re-pays it for every code block in the turn. Tokenize the whole block once (memo on[content, language]), slice the token stream into lines, keep the row windowing.C9. Image drafts: base64 payloads in localStorage, one key per session, never GC'd — medium, confirmed
ChatPanel/InputArea/utils/imageDraftCache.ts:33-43·ChatPanel/hooks/useInputArea/index.ts:420-423—setItem(JSON.stringify(images))with data URLs on every attachment-array change; onlyclearImageDrafton submit; uncaught on quota; parsed synchronously on every session switch. Cap bytes, try/catch, clear from the session-delete path or sweep orphan keys at startup.C10. Smaller chat costs — low, confirmed
ChatHistory/hooks/useChatHistoryProjectionModel.ts:156-174— diagnostics memory estimator walks the projected tree (≤5,000 nodes) on every projection change; its only reader is the RAM-stats panel. Gate on "panel mounted".ChatPanel/index.tsx:199-218— cloud-org reconciliation refiresreadOrgs()on every roster refresh because the request always writes a new array; key on a sorted ids string.ChatItems/AgentChatItemDefault.tsx:87-89—expandprop mirrored into state per message row.blocks/primitives/BlockOutput.tsx:211-240— re-creates its RO per streamed chunk andsetScrollTops per scroll pixel.Streaming data path (below the UI)
Ingestion is in good shape: no per-token atom write, RPC, or persistence. The costs are redundant parsing, cache sizing, and switch-time round-trips.
D1. Every Channel frame is JSON-parsed + zod-validated twice (three times with the work-item panel open) — medium, confirmed
src/engines/SessionCore/sync/useSessionChannel.ts:112-115, 196-208, 307-310·sessionSyncChannel.ts:22·src/modules/ProjectManager/.../useLiveDiffStats.ts:42·ChatPanel/hooks/useAgentOrgGroupChatLiveSessions.tsx:34validateSessionChannelMessageparses + schema-walks, then discards the result and forwards the raw string; the router parses again; extra subscribers parse again just to checktype. Tool-result frames can be 100 KB+. Parse once insubscribeToSessionEventsand hand subscribers{raw, parsed}; replace the twoincludes()logging scans withparsed.type.D2. JS snapshot LRU (5) <
MULTI_RUNNER_MAX(6): background runners can thrash full-snapshot IPC fetches — medium, mechanism confirmedSessionCore/.../snapshotCacheManager.ts:43-49, 175-191·snapshotCache.ts:56-67·src/features/SessionCreator/multiRunner/contract.ts:29A delta for a session with no JS cache triggers a full
getSnapshotRPC + normalize + materialize, evicting the oldest entry — possibly another actively-mutating runner. Rust emitses:changedfor every session whether or not JS has a consumer. Drop deltas for sessions that are neither active nor have listeners (next mount re-primes), or evict listener-less sessions first, or set the cap ≥ runners + 1.D3. Session switch round-trips the transcript across IPC ~4× — low-medium, confirmed (switch-time only)
SessionCore/sync/sessionSwitchOrchestrator.ts:136-147·SessionCore/core/atoms/actions.ts:404-440—getEvents→loadInitialTurnWindow→getEventsagain; JS builds a fullDerivedSnapshot, thenmergeEventssends the whole list back into Rust (a no-op dedup on cache hit), which re-emits a full snapshot JS re-normalizes. Return events fromloadInitialTurnWindow; skipmergeEventswhen nothing synthetic was added.D4. Per-token string accumulation flattens the V8 rope every token — low, confirmed (bounded at 500 KB)
SessionCore/sync/adapters/shared/streamTextAccumulator.ts:47, 82-100—findSuffixPrefixOverlapslices the accumulated cons-string every delta → O(accumulated length) per token, quadratic over a turn. Keep a ≤4 KB tail for overlap detection; append with plain+=; flatten at flush.Correctness bugs found on the way
Not the question asked, but they surfaced while tracing lifecycles and are cheap to fix alongside.
ProjectManagerLayout/index.tsx:204-207,EditorMainPane/index.tsx:211-214(enabled: true); bridge atsrc/hooks/tabHost/useWorkStationTabShortcutBridge.ts:33is a global listenersrc/dispatchesworkstation-close-active-tabtoday, so this is dead wiring, not a live bug. The moment a dispatcher is added, with Browser active and Code + Project warm, one ⌘W would fire three close handlers — Code and Project would silently close their active tabs (only Browser gates onisActive). Gate on host visibility before reviving it.useCloseTabWithGuard.ts:27-46never callsdiscardChanges();fileContent/useFileContent.ts:99-112caches withdirty:true;fileContent/cache.ts:48-57never evicts dirty entriescloseWorkstationTabsAtom(single canonical close)..gitignorecontaminationCodeEditor/hooks/useCodeEditor/helpers.ts:40-55— one module-scope checker;useMultiRootFileTreecallsensureGitignoreCheckerper root insidePromise.allCodeEditor/hooks/diagnostics/useLspDiagnostics.ts:32-49lspClientManager.shutdown()globally before the new root is set.CodeEditor/hooks/useCodeEditor/useFileContent.ts:69-120(nocurrentPathguard; the otheruseFileContenthas one at:186-188)CodeEditor/.../SearchContent/useSearchContent/useSearchExecution.ts:190-235await listen()s and the ref assignment leaves both Tauri listeners registered for process lifetime.Browser/hooks/useBrowserStatusBar.ts:130-134isActive→falseleavesonToggleDevToolsetc. in the callbacks atom.useNarrowChatFocusdocument-wideMutationObservercan become permanentsrc/modules/useNarrowChatFocus.ts:240-249[data-workbench-surface]and[data-main-content]exist; if either is absent it runs twoquerySelectors on every DOM mutation for the session. Cap attempts.src/modules/WorkStation/Canvas/index.tsx:118-129Quick wins — one-liners and near-one-liners
Each preserves behavior exactly and can ship independently.
Tabs
setAndPersist:if (next === state) return;+ layout-writermainPaneidentity bailcomposePanel: reuse previoustabsarray when element-wise identicaluseCloseTabWithGuard: read registry viauseAtomCallbackcloseWorkstationTabsAtom:clearUnsavedContentCache(filePath)for closed file tabsMy Station
getTerminalDisplayTitlefromTerminalCore/types(×2) — xterm out of the shell chunkuseMemothe returns ofuseOutputChannels,useGitDiffState,useGitOutputIntegration,useTaskOutputIntegration;useMemothegitDiffStateliteral inCodeEditor/index.tsx:181SharedBrowserHostSlot: early-return the layout effect when!activeuseDisplayData: skip tree conversion when the sidebar is collapseduseSubagentSessions: trailing-debounce the query keyuseWorkStationTabShortcutBridge: pass hostisActiveasenabled(pre-empts the dormant ⌘W triple-close)useWorkspacePortAdvertisedUrls: key the listener effect on session ids (selectAtom+ shallow array equality), not the wholeterminalSessionsAtomprimarySidebarAtoms/bottomPanelAtomspersist atoms: debouncesetStoredValue, or persist ononResizeEnd(already supported byuseResizeHandle)BrowserContext.updateSession: caphistoryEntries(e.g. 200) at the producing boundaryChat
getItemKey: identity keys; delete the custom ROuseChatProjection: skip wheneventsreference-equalAgentMessageEvent:useStreamingDeltaForSession(isLive ? id : null); memostripThinkTagsuseStreamingHud/TabPill:selectAtomto the one fieldChatView: threeselectAtoms instead of the raw snapshot; hoistEMPTY_MEMBERSmessagesEventsAtom: use the sentinel likechatEventsAtomCode quality
Duplicated ownership
useCloseTabWithGuard(Discard/Cancel, never clears buffers),useEditorPaneState.ts:136-263(Save/Don't Save/Cancel, own mutations instead oftabMutations.ts),ProjectManagerLayout/index.tsx:70-90. Same file tab → different dialog and different data outcome depending on which affordance closed it.ChatPanelTabBar.tsx:752-836is a hand-copy ofshared/TabBar/hooks/useTabDrag.ts(and lacks its unmount cleanup);scrollIntoView-on-activate duplicatesuseAutoScrollToActive. ExtractuseSessionTabDrag.workstation:tabs:v3:*andorgii-global-tabs).useNarrowChatFocus.ts:183-205vs:293-320— the same four-branch edge FSM implemented twice against different variable sources.useCodeEditor/useFileContentvsfileContent/useFileContent) with different stale-guard behavior.Oversized files / prop plumbing
ChatPanelTabBar.tsx(971 lines): four components in one —TabPillwith a 160-line iconif/elsechain (:330-490), hover card, plus-menu, bar + DnD. Split; replace the chain with aRecord<ChatPanelTabType, Icon>.ChatPanel/index.tsx(747):ChatPanelHeaderreceives ~50 props,ChatPanelEmptyContent~30. Let the header read aChatPanelHeaderActionsContext; move cloud-org reconciliation and work-item mirror effects into hooks.AppShellContenttakes 19 props, most derivable from atoms the children could read themselves.Dead code & stale comments
useFileWatchHeartbeat.ts:122— the onlysetIntervalin the editor tree, never started (startWatchHeartbeathas no caller).EditorBottomPanel/tabs/TerminalTab.tsx— hook commented out, file still exported, comment about lazy-loading is wrong.TabContent/index.ts:9-25says "STAGED — NOT YET MOUNTED"; it is mounted in three hosts.TabContent/renderers/index.tsis an unused eager barrel that would collapse all 33 lazy chunks if imported.renderers/terminal.tsx:9-15describes an overlay thatEditorMainPaneunmounts.usePinnedTabs,hideWhenOthersExist) has no producer;SidebarSlot's keep-alive branch is dormant (no descriptor setskeepAlive).codeBlockContainerWidthhard-codedundefinedyet threaded through four layers of props and comparators;_isPendingCancelRefaccepted and ignored;useAgentWorkingRef's "no re-renders" contract doesn't match the code.SimpleGridCelltail-signature comparison is unreachable (prior branch already returned).Patterns to adopt repo-wide
useMemothe object (the repo already does this inuseTerminalState,useDiagnostics,fileContent/useFileContent,useCodeEditor/index— the output/git integration hooks are the outliers).openEditorFilePathsAtom,chatEventsAtomare the reference implementations).visibilityAwarePoller,agentOrgRunViewStore,linearProjectsCacheare the models.Already done well — don't re-flag
Streaming
selectAtomwith equality;sentinel keepschatEventsAtomidentity stable mid-streames:changedbatching with change-journal deltas; JS rAF coalescing with pointer-copy materializationdisposeSession; module caches reset on session departurelisten()sites (except search) handle the async-unlisten race;es:changedhas a generation tokenHosts & editor
hostMountPolicy.ts: pure, unit-tested, bounded keep-alive; webview engine hoisted out of the host so unmount is possiblevisibilityAwarePoller: hidden-pause, single-flight, rerun-after-flight, immediate refresh on visible — all browser polls route through it; console/network polling is dev-only and devtools-open-onlydetach_pty_streamon unmountkeepAlive:falseso PR/issue panels never poll hiddenset_active_git_polling_repoanduseCodeEditorLocalStategate on hostisActive;useGitWorktreesgates on the active tab;useTestRunneron the testing tab + one-shot guardlinearProjectsCacheis the reference single-flight + TTL + LRUTabs
Suspensedispatcher; same-type switches don't remounttabMutations.tsshort-circuits no-ops (the loss is one layer up); load-time sanitization caps 200 tabs/partition; v2→v3 migration keeps a recovery copymainPaneHasRealTabsAtom,activeHostAtom) bail correctly;activeWorkStationTabAtomreturns the stored referenceSortableTabmemoized;useAutoScrollToActiverAF-coalesced;useWorkingTreeDiffTotalsis one shared external store per repo