Skip to content

Perf audit: tabs store, My Station hosts, and chat pane — RAM/CPU weak spots (no-UX-change refactors) #889

Description

@Harry19081

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-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.

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-42

Mechanism. persistWorkstationTabsState stringifies and setItems shared, globalWorkspace, legacy seed, every sessionWorkspaces[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

src/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:84

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

src/hooks/tabHost/useCloseTabWithGuard.ts:25, 47 · src/modules/WorkStation/AppShell/useWorkstationTabList.ts:101-106 · src/modules/WorkStation/shared/TabBar/index.tsx:306-312

entries = useAtomValue(tabRegistryAtom) is only read inside the callback but sits in its deps, so closeTab identity changes per write → handleTabClosehandleCloseClick → 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

src/modules/WorkStation/Browser/BrowserLayout/useBrowserTabSync.ts:66-150 · src/store/workstation/browser/tabs/index.ts:262-285

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

src/hooks/tabHost/useWorkstationTabHeader.ts:43-58 · src/modules/WorkStation/TabContent/renderers/chatSession.tsx:113-172

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 useMemo headerTrailing.

T6. Second legacy tab store (orgii-global-tabs) still written on every browser/terminal change, almost never read — medium, confirmed

src/store/ui/navigationSidebarTabsAtom.ts:38-48 · src/hooks/ui/tabs/useSyncGlobalTabs.ts:58-117 · src/util/contextPillContent.ts:51

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

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-146

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) and onExternalFileChange → 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

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-184CodeEditor/hooks/useCodeEditorEvents.ts:466-530

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

CodeEditor/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-249

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

CodeEditor/useSourceControlSetup.ts:92, 128-132, 137-144, 171-174 · CodeEditor/hooks/diagnostics/useLspDiagnostics.ts:32-49 · src/hooks/git/useRepoSelection/useRepoSelection.ts:210-226

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

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-62

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

Browser/shared/SharedBrowserHostSlot.tsx:136-162 · src/contexts/workstation/BrowserContext.tsx:171-178, 259-281 · AppShell/AppShellContent.tsx:125, 146 · Browser/hooks/useBrowserStatusBar.ts:130-134

SharedBrowserHostSlot installs a ResizeObserver, a subtree MutationObserver and a capture-phase scroll 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

src/modules/ProjectManager/ProjectManagerCore.tsx:34-42 · ProjectManager/Projects/index.tsx:205-213 · useProjectStatusBar.ts:34-80

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

CodeEditor/Panels/EditorMainPane/index.tsx:389, 405-423 · src/engines/TerminalCore/index.tsx:369

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)

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-210

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

src/engines/Simulator/hooks/useCurrentTurnLastAgentMessage.ts:84 · derived/simulatorEvents.ts:245-268 · AppShell/index.tsx:55

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.

S12. Addendum — AppShell-level subscriptions (late-arriving shell synthesis, verified)

Two framing facts first:

  • WorkStationPage is mounted with isActive hard-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 useTerminalProcessPollerupdateTerminalSessionInfoAtom) 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:80 rootTreesRef 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

CodeEditor/Panels/EditorBottomPanel/index.tsx:20-23 · CodeEditor/shared/SidebarModules/Terminal/TerminalSidebarRows.tsx:11-14 · chain: src/engines/TerminalCore/exports.ts:4-5index.tsx:25src/components/TerminalInteractive/index.tsx:23-24 · CodeEditor/Panels/EditorBottomPanel/tabs/TerminalTab.tsx (dead)

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.

C1. Content-derived virtual-row keys remount whole turns; duplicate ResizeObserver retains detached rows — high, confirmed

src/engines/ChatPanel/ChatHistory/components/ChatHistoryList.tsx:148-165 (getItemKey) · :168-205 (custom RO, observedRowsRef) · :498 (key={virtualItem.key})

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 useCallback getItemKey; 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

ChatHistory/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-168

For sessions ≥ 2,000 events, the effect keys on sourceVersion; hasNewerSourceVersionprojectDelta 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

ChatPanel/hooks/useInputArea/index.ts:536-538 · src/hooks/session/useSessionPatch.ts:383-447 · src/store/session/sessionAtom/mutations.ts:66-93

setDraftusePatchSession 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

ChatPanel/hooks/useChatPanelTabsController.ts:32 · ChatPanel/ChatPanelTabBar.tsx:237, 271-277 · src/store/workstation/codeEditor/terminal/index.ts:193-210, 521-538

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

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-90

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

ChatPanel/events/stream/agent-message/index.tsx:329, 340-342, 365 · ChatPanel/hooks/useInputArea/index.ts:223, 269-273, 284-288

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)

ChatHistory/hooks/useChatScroll.ts:177-198, 242-246, 262-279, 299-356 · hooks/useChatViewportController.ts:143-159 · hooks/useChatFooterSpacer.ts:200 · hooks/useChatScrollPin.ts:190

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.

C8. Per-line PrismLight instances in code blocks — medium, plausible (needs measurement)

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-423setItem(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-89expand 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

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:34

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

SessionCore/.../snapshotCacheManager.ts:43-49, 175-191 · snapshotCache.ts:56-67 · src/features/SessionCreator/multiRunner/contract.ts:29

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-440getEventsloadInitialTurnWindowgetEvents 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-100findSuffixPrefixOverlap 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.
Every repo switch shuts down all language servers CodeEditor/hooks/diagnostics/useLspDiagnostics.ts:32-49 Old cleanup runs lspClientManager.shutdown() globally before the new root is set.
Stale-response race in the atom-backed file loader CodeEditor/hooks/useCodeEditor/useFileContent.ts:69-120 (no currentPath guard; the other useFileContent has one at :186-188) Click A then B quickly → A's late resolve overwrites B's content.
Search listener async-registration leak CodeEditor/.../SearchContent/useSearchContent/useSearchExecution.ts:190-235 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.
Canvas A2UI content grows unbounded, O(n²) re-join src/modules/WorkStation/Canvas/index.tsx:118-129 Every push re-joins the whole accumulated string; only reset/close clears it.

Quick wins — one-liners and near-one-liners

Each preserves behavior exactly and can ship independently.

Tabs

  • setAndPersist: if (next === state) return; + layout-writer mainPane identity bail
  • 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
  • fix(RAM): replace full hydration with bounded replay across all external CLI histories #443 landed: 1-body turn window on switch, 8 loaded bodies cap, batched cloud import without materialization
  • 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
  • Boolean/string derived atoms (mainPaneHasRealTabsAtom, activeHostAtom) bail correctly; activeWorkStationTabAtom returns the stored reference
  • SortableTab memoized; useAutoScrollToActive rAF-coalesced; useWorkingTreeDiffTotals is one shared external store per repo
  • Editor repo cache 5 × 20 with per-session dispose

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions