+
+ {/* Page header */}
+
+
+ {t('nav.brain')}
+
+ {t('brain.subtitle')}
+
+
+ {/* Graph toolbar — mode toggle + actions. */}
+
+
+ {/* Hero graph — mounted as soon as data arrives (even while the overlay
+ covers it) so its force layout settles and fires onReady underneath. */}
+
+ {/* A loaded graph wins over a transient error so a failed background
+ refetch never clobbers an already-rendered graph. */}
+ {graph ? (
+
+
+
+ ) : error ? (
+
+ {t('brain.error')}
+
+ ) : null}
+
+
+ {/* Memory workspace — revealed once the graph is in. The panels own
+ their own polling, so deferring their mount keeps the load light. */}
+ {overlayDone ? (
+
+ ) : null}
+
+
+ {/* Loading overlay — the "brain collecting information" flourish. Opaque
+ and viewport-filling so the whole page (header, toolbar, graph, and the
+ panels below) stays hidden until the graph is ready. `fixed` keeps it
+ centered in the viewport regardless of page height; z-40 sits below the
+ bottom nav bar (z-50) so navigation stays available. */}
+ {!overlayDone && (
+
+ {!reduceMotion.current && (
+
+ )}
+
+ {t('brain.loading')}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/app/src/pages/__tests__/Brain.test.tsx b/app/src/pages/__tests__/Brain.test.tsx
new file mode 100644
index 000000000..771163a5c
--- /dev/null
+++ b/app/src/pages/__tests__/Brain.test.tsx
@@ -0,0 +1,116 @@
+/**
+ * Tests for the Brain page orchestration: it shows the loading overlay, then
+ * reveals the memory graph once data is fetched, the layout has settled
+ * (mocked MemoryGraph fires onReady), and the minimum animation window passes.
+ * Heavy children (the Pixi/SVG graph and the Lottie player) and the RPC are
+ * mocked so this stays a fast unit test of the gate logic.
+ */
+import { act, render, screen } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import Brain from '../Brain';
+
+const graphExportMock = vi.hoisted(() => vi.fn());
+
+vi.mock('../../utils/tauriCommands', () => ({ memoryTreeGraphExport: graphExportMock }));
+
+// Mocked graph: fires onReady on mount to simulate the layout settling.
+vi.mock('../../components/intelligence/MemoryGraph', async () => {
+ const React = await import('react');
+ return {
+ MemoryGraph: ({ nodes, onReady }: { nodes: unknown[]; onReady?: () => void }) => {
+ React.useEffect(() => {
+ onReady?.();
+ }, [onReady]);
+ return React.createElement('div', { 'data-testid': 'memory-graph' }, `nodes:${nodes.length}`);
+ },
+ };
+});
+
+vi.mock('../../components/LottieAnimation', async () => {
+ const React = await import('react');
+ return {
+ default: ({ src }: { src: string }) =>
+ React.createElement('div', { 'data-testid': 'brain-lottie', 'data-src': src }),
+ };
+});
+
+// The memory workspace panels own their own polling/RPCs — stub them so this
+// stays an isolated test of the Brain page's loading/reveal orchestration.
+vi.mock('../../components/intelligence/MemoryControls', () => ({ MemoryControls: () => null }));
+vi.mock('../../components/intelligence/MemoryTreeStatusPanel', () => ({
+ MemoryTreeStatusPanel: () => null,
+}));
+vi.mock('../../components/intelligence/MemorySourcesRegistry', () => ({
+ MemorySourcesRegistry: () => null,
+}));
+vi.mock('../../components/intelligence/Toast', () => ({ ToastContainer: () => null }));
+
+const makeGraph = (n: number) => ({
+ nodes: Array.from({ length: n }, (_, i) => ({ id: `n${i}`, kind: 'summary', label: `N${i}` })),
+ edges: [],
+ content_root_abs: '/tmp/content',
+});
+
+describe('Brain page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.useFakeTimers();
+ // jsdom has no matchMedia — default to "motion allowed".
+ window.matchMedia = vi
+ .fn()
+ .mockReturnValue({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ }) as unknown as typeof window.matchMedia;
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('shows the loading overlay first, then reveals the graph once ready', async () => {
+ graphExportMock.mockResolvedValue(makeGraph(3));
+ render(