Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/memory-audit-2026-08-16/ram-optimization-findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,46 @@ multi-repo surface), browser-tab webview
discarding (see 2.5 note above), chat-panel CLI terminal tabs (agent-owned, turn
lifetime), i18n namespace deferral and lazy zod schemas (2.4), the 4 MB
`App` static graph itself.

### 2026-08-17 — heavy-component boundaries (branch `perf/heavy-component-leaks`, stacked on the above)

Method: `src/test/staticImportGraph.ts` (regex import walker) run per lazy
chunk root (every dynamic-`import()` target in `src/`) reporting which heavy
packages are statically reachable. Before → after:

| Surface (chunk root) | Before | After |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| `engines/ChatPanel/events/stream/agent-message` (every chat message) | xterm, CodeMirror + langs, sql-formatter, react-syntax-highlighter, highlight.js, recharts, @a2ui, framer-motion, mammoth, jszip (1 747 files) | none |
| `modules/MainApp/TeamInbox` | xterm, CodeMirror, sql-formatter, framer-motion | none |
| `modules/MainApp/Settings/SettingsSlot` | CodeMirror, sql-formatter | none |
| `modules/MainApp/AgentOrgs` | CodeMirror, sql-formatter | none |
| `modules/ProjectManager/{Projects,WorkItems,LinearProjects}` | xterm, CodeMirror, sql-formatter, framer-motion | none |
| `engines/Simulator/index` | xterm, CodeMirror, sql-formatter, framer-motion | none |
| `modules/WorkStation/shared/index.ts` (barrel, ~80 importers) | xterm, CodeMirror, framer-motion | none |

Root causes and fixes:

- `modules/WorkStation/shared/index.ts` re-exported `GitFileDiffSplit` (dead
code → all of `features/CodeMirror`), the `SidebarModules` block (module
evaluation registers the Terminal tab sidebar → xterm) and
`QuickActionsPanel` (framer-motion). Re-exports removed with explanatory
comments; `CodeEditor/index.tsx` imports `SidebarSlot` from
`../shared/SidebarModules` (the import that already carried the
registrations); `GitFileDiffSplit` deleted.
- Eager imports of on-demand views made lazy (`React.lazy` + `Suspense
fallback={null}`, matching neighbouring precedents): `SimulatorMessages` in
`agent-message`; transcript content in `SessionRawTranscriptDialog`;
`A2UIRenderer` (recharts/@a2ui) and `ReactArtifactRunner` (sucrase +
embedded React runtime) in `CanvasPreviewSurface`; `SkillEditorPanel` in
`SkillsCategoryView`; `CodeMirrorEditor` inside `MarkdownEditor`; the canvas
"source" tab viewer in `CanvasApp`.
- Editor-only consumers deep-import `@src/features/CodeMirror/Editor` instead
of the barrel (which also carries Diff, ConflictEditor, SqlEditor +
sql-formatter).
- Guard: `src/app/root/__tests__/featureBoundaries.test.ts` — nine surfaces
asserted free of the editor/terminal/highlighter/chart stacks; failures
print the import chain.

Still statically reaching CodeMirror by design: `modules/WorkStation/index.tsx`
(the code editor), `engines/Simulator/apps/canvas/CanvasApp` only via the lazy
source viewer, and the editor-internal panes.
143 changes: 143 additions & 0 deletions src/app/root/__tests__/featureBoundaries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { describe, expect, it } from "vitest";

import {
importersOfPackage,
walkStaticImports,
} from "@src/test/staticImportGraph";

/**
* Heavy-feature boundary guards for lazily loaded surfaces.
*
* Each root below is its own async chunk (a route page, workstation app,
* or event renderer). Before this guard existed, several of them statically
* reached the editor/terminal/highlighter/chart stacks through barrel
* re-exports (`modules/WorkStation/shared`, `features/CodeMirror`) or a
* single eager import — e.g. the agent-message renderer (every chat) pulled
* xterm, CodeMirror, react-syntax-highlighter, highlight.js, recharts,
* mammoth and jszip. See docs/memory-audit-2026-08-16 (Fix log).
*
* If a surface genuinely needs one of these packages, load it behind a
* `React.lazy` / dynamic `import()` at the point of use, or move the root
* to the allow-list below with a reason.
*/
const EDITOR_STACK = [
"@codemirror/view",
"@codemirror/state",
"@uiw/react-codemirror",
"sql-formatter",
];
const TERMINAL_STACK = ["@xterm/xterm", "@xterm/addon-webgl"];
const HIGHLIGHTERS = ["react-syntax-highlighter", "refractor", "highlight.js"];
const MISC_HEAVY = [
"framer-motion",
"recharts",
"mermaid",
"mammoth",
"jszip",
"sucrase",
];

interface Boundary {
root: string;
forbidden: string[];
reason: string;
}

const BOUNDARIES: Boundary[] = [
{
root: "engines/ChatPanel/events/stream/agent-message/index.tsx",
forbidden: [
...EDITOR_STACK,
...TERMINAL_STACK,
...HIGHLIGHTERS,
...MISC_HEAVY,
],
reason:
"renders every agent message; simulator/canvas/code surfaces are lazy",
},
{
root: "engines/ChatPanel/events/stream/user-message/index.tsx",
forbidden: [
...EDITOR_STACK,
...TERMINAL_STACK,
...HIGHLIGHTERS,
...MISC_HEAVY,
],
reason: "renders every user message",
},
{
root: "modules/MainApp/TeamInbox/index.ts",
forbidden: [
...EDITOR_STACK,
...TERMINAL_STACK,
...HIGHLIGHTERS,
"framer-motion",
],
reason: "inbox list/detail; no editor or terminal",
},
{
root: "modules/MainApp/Settings/SettingsSlot.tsx",
forbidden: [...EDITOR_STACK, ...TERMINAL_STACK, ...HIGHLIGHTERS],
reason: "settings; the skill/policy editors load CodeMirror lazily",
},
{
root: "modules/MainApp/AgentOrgs/index.tsx",
forbidden: [...EDITOR_STACK, ...TERMINAL_STACK, ...HIGHLIGHTERS],
reason: "agent orgs; MarkdownEditor loads CodeMirror lazily",
},
{
root: "modules/ProjectManager/Projects/index.tsx",
forbidden: [
...EDITOR_STACK,
...TERMINAL_STACK,
...HIGHLIGHTERS,
"framer-motion",
],
reason: "project manager pages",
},
{
root: "modules/ProjectManager/WorkItems/index.tsx",
forbidden: [
...EDITOR_STACK,
...TERMINAL_STACK,
...HIGHLIGHTERS,
"framer-motion",
],
reason: "project manager pages",
},
{
root: "engines/Simulator/index.ts",
forbidden: [...EDITOR_STACK, ...TERMINAL_STACK, ...HIGHLIGHTERS],
reason: "simulator shell; individual apps lazy-load their editors",
},
{
root: "modules/WorkStation/shared/index.ts",
forbidden: [
...EDITOR_STACK,
...TERMINAL_STACK,
...HIGHLIGHTERS,
"framer-motion",
],
reason:
"shared barrel imported by ~80 files; heavy components must not be re-exported here",
},
];

describe("heavy-feature boundaries", () => {
for (const boundary of BOUNDARIES) {
it(`${boundary.root} stays free of heavy stacks (${boundary.reason})`, () => {
const graph = walkStaticImports([boundary.root]);
const present = boundary.forbidden.filter((pkg) =>
graph.packages.has(pkg)
);
const explanation = present.map(
(pkg) =>
`${pkg}:\n ${importersOfPackage(graph, pkg).slice(0, 3).join("\n ")}`
);
expect(
explanation,
"heavy package reachable (import chains shown)"
).toEqual([]);
});
}
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Layout, SquareArrowOutUpRight } from "lucide-react";
import React, {
Suspense,
forwardRef,
lazy,
useCallback,
useEffect,
useImperativeHandle,
Expand All @@ -13,10 +15,8 @@ import { useTranslation } from "react-i18next";
import Button from "@src/components/Button";

import type { A2UIActionHandler } from "./A2UIActionContext";
import A2UIRenderer, { type A2UIRendererHandle } from "./A2UIRenderer";
import ReactArtifactRunner, {
type ReactArtifactError,
} from "./ReactArtifactRunner";
import type { A2UIRendererHandle } from "./A2UIRenderer";
import type { ReactArtifactError } from "./ReactArtifactRunner";
import {
type CanvasPreviewPayload,
type CanvasPreviewSurfaceVariant,
Expand All @@ -29,6 +29,13 @@ import {
sanitizeStaticHtmlBody,
} from "./staticHtmlCanvas";

// Lazy render kinds. CanvasInlineCard is reached from the agent-message
// renderer (every chat), but A2UI (recharts + @a2ui) and React artifacts
// (sucrase + the embedded React 18 runtime text) are only needed when a
// canvas of that kind is actually shown.
const A2UIRenderer = lazy(() => import("./A2UIRenderer"));
const ReactArtifactRunner = lazy(() => import("./ReactArtifactRunner"));

export interface CanvasPreviewSurfaceHandle {
evalScript: (javascript: string) => void;
}
Expand Down Expand Up @@ -158,24 +165,28 @@ const CanvasPreviewSurface = forwardRef<
a2uiLines.length === 0 ? (
<>{payload?.streaming ? loadingFallback : emptyFallback}</>
) : (
<A2UIRenderer
ref={rendererRef}
lines={a2uiLines}
isStreaming={payload?.streaming}
onAction={onAction}
sessionId={sessionId}
className={a2uiClassName}
/>
<Suspense fallback={loadingFallback}>
<A2UIRenderer
ref={rendererRef}
lines={a2uiLines}
isStreaming={payload?.streaming}
onAction={onAction}
sessionId={sessionId}
className={a2uiClassName}
/>
</Suspense>
);
} else if (renderKind === "html" && payloadContent) {
content = <StaticHtmlCanvas content={payloadContent} />;
} else if (renderKind === "react" && payloadContent) {
content = (
<ReactArtifactRunner
key={reloadKey === undefined ? undefined : `react-${reloadKey}`}
source={payloadContent}
onError={handleReactArtifactError}
/>
<Suspense fallback={loadingFallback}>
<ReactArtifactRunner
key={reloadKey === undefined ? undefined : `react-${reloadKey}`}
source={payloadContent}
onError={handleReactArtifactError}
/>
</Suspense>
);
} else {
content = emptyFallback;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { memo } from "react";
import { useTranslation } from "react-i18next";

import { CodeMirrorEditor } from "@src/features/CodeMirror";
import { CodeMirrorEditor } from "@src/features/CodeMirror/Editor";

export interface SessionRawTranscriptContentProps {
error: string | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Clipboard, RefreshCw } from "lucide-react";
import React, { memo } from "react";
import React, { Suspense, lazy, memo } from "react";
import { useTranslation } from "react-i18next";

import Button from "@src/components/Button";
import Modal from "@src/scaffold/ModalSystem";

import SessionRawTranscriptContent from "./SessionRawTranscriptContent";
import { useSessionRawTranscript } from "./useSessionRawTranscript";

// Lazy (same as SessionRawTranscriptView): the transcript content pulls
// CodeMirror, and this dialog is imported by the WorkStation TabBar — which
// every workstation surface renders — but only opens on demand.
const SessionRawTranscriptContent = lazy(
() => import("./SessionRawTranscriptContent")
);

export interface SessionRawTranscriptDialogProps {
sessionId: string | null;
visible: boolean;
Expand Down Expand Up @@ -55,15 +61,17 @@ const SessionRawTranscriptDialog: React.FC<SessionRawTranscriptDialogProps> =
}
>
<div className="flex min-h-0 flex-1 flex-col gap-2 px-4 pb-4">
<SessionRawTranscriptContent
error={transcript.error}
filePath={
sessionId ? `raw-transcript-${sessionId}.json` : undefined
}
loaded={Boolean(transcript.snapshot)}
loading={transcript.loading}
transcriptJson={transcript.transcriptJson}
/>
<Suspense fallback={null}>
<SessionRawTranscriptContent
error={transcript.error}
filePath={
sessionId ? `raw-transcript-${sessionId}.json` : undefined
}
loaded={Boolean(transcript.snapshot)}
loading={transcript.loading}
transcriptJson={transcript.transcriptJson}
/>
</Suspense>
</div>
</Modal>
);
Expand Down
23 changes: 16 additions & 7 deletions src/engines/ChatPanel/events/stream/agent-message/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* is now `agent_message` to better reflect the actual purpose.
*/
import { useAtomValue } from "jotai";
import React, { useMemo } from "react";
import React, { Suspense, lazy, useMemo } from "react";
import { useTranslation } from "react-i18next";

import Markdown from "@src/components/MarkDown";
Expand Down Expand Up @@ -53,9 +53,16 @@ import {
extractThinkContent,
stripThinkTags,
} from "@src/engines/SessionCore/sync/adapters/shared/streamingParsers";
import { SimulatorMessages } from "@src/modules/WorkStation/Chat/Communication";
import { parseGitArtifactsFromText } from "@src/shared/git/sessionGitArtifacts";

// Lazy (same as user-message / thinking): SimulatorMessages is only used by
// the simulator variant, but a static import here made every chat message
// renderer pull the whole Communication app — SessionReplay CodePanel,
// CodeMirror, react-syntax-highlighter, highlight.js, file previewers.
const LazySimulatorMessages = lazy(
() => import("@src/modules/WorkStation/Chat/Communication")
);

// ============================================
// Types
// ============================================
Expand Down Expand Up @@ -282,11 +289,13 @@ const SimulatorVariant: React.FC<SimulatorVariantProps> = ({
const eventSessionId =
(event as { event?: { sessionId?: string } })?.event?.sessionId ?? null;
return (
<SimulatorMessages
currentEvent={event}
mode={mode}
sessionId={eventSessionId}
/>
<Suspense fallback={null}>
<LazySimulatorMessages
currentEvent={event}
mode={mode}
sessionId={eventSessionId}
/>
</Suspense>
);
};

Expand Down
Loading
Loading