diff --git a/app/src/components/intelligence/MemoryGraph.tsx b/app/src/components/intelligence/MemoryGraph.tsx
index 1cadfb1ca6..46e7406d79 100644
--- a/app/src/components/intelligence/MemoryGraph.tsx
+++ b/app/src/components/intelligence/MemoryGraph.tsx
@@ -45,7 +45,7 @@ import {
levelColor,
nodeColor,
nodeRadius,
- supportsWebGL,
+ SOURCE_COLOR,
VIEWPORT_H,
VIEWPORT_W,
ZOOM_MAX,
@@ -54,8 +54,18 @@ import {
import { summaryWorkspacePath } from './memoryWorkspacePaths';
import { PixiGraph } from './PixiGraph';
-/** Detected once — WebGL availability decides Pixi vs the SVG fallback. */
-const HAS_WEBGL = supportsWebGL();
+/** Use WebGL (Pixi) in production; fall back to SVG in test (jsdom). */
+const HAS_WEBGL =
+ typeof document !== 'undefined' &&
+ typeof document.createElement === 'function' &&
+ (() => {
+ try {
+ const c = document.createElement('canvas');
+ return !!(c.getContext('webgl2') || c.getContext('webgl'));
+ } catch {
+ return false;
+ }
+ })();
interface SimNode extends GraphNode {
x: number;
@@ -357,6 +367,9 @@ export function MemoryGraph({ nodes, edges, mode, emptyHint }: MemoryGraphProps)
const legend =
mode === 'tree'
? [
+ ...(nodes.some(n => n.kind === 'source')
+ ? [{ label: t('graph.source', 'Source'), color: SOURCE_COLOR }]
+ : []),
...Array.from(new Set(nodes.filter(n => n.kind === 'summary').map(n => n.level ?? 0)))
.sort((a, b) => a - b)
.map(lvl => ({ label: `L${lvl}`, color: levelColor(lvl) })),
@@ -486,7 +499,11 @@ export function MemoryGraph({ nodes, edges, mode, emptyHint }: MemoryGraphProps)
- {hovered.kind === 'summary' ? (
+ {hovered.kind === 'source' ? (
+
+ {hovered.label}
+
+ ) : hovered.kind === 'summary' ? (
<>
L{hovered.level ?? '?'}
·
diff --git a/app/src/components/intelligence/MemorySourcesRegistry.tsx b/app/src/components/intelligence/MemorySourcesRegistry.tsx
index 892937bd46..988a6e0f0b 100644
--- a/app/src/components/intelligence/MemorySourcesRegistry.tsx
+++ b/app/src/components/intelligence/MemorySourcesRegistry.tsx
@@ -24,6 +24,7 @@ import {
updateMemorySource,
} from '../../services/memorySourcesService';
import type { ToastNotification } from '../../types/intelligence';
+import { memoryTreeFlushSource } from '../../utils/tauriCommands/memoryTree';
import { AddMemorySourceDialog } from './AddMemorySourceDialog';
interface MemorySourcesRegistryProps {
@@ -31,6 +32,21 @@ interface MemorySourcesRegistryProps {
pollIntervalMs?: number;
}
+interface SyncProgress {
+ stage: string;
+ detail: string | null;
+ percent: number | null;
+}
+
+function parseSyncProgress(detail: string | null): number | null {
+ if (!detail) return null;
+ const match = detail.match(/^(\d+)\/(\d+)\s/);
+ if (!match) return null;
+ const current = parseInt(match[1], 10);
+ const total = parseInt(match[2], 10);
+ return total > 0 ? Math.round((current / total) * 100) : null;
+}
+
export function MemorySourcesRegistry({
onToast,
pollIntervalMs = 5000,
@@ -41,6 +57,43 @@ export function MemorySourcesRegistry({
const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const [syncingId, setSyncingId] = useState
(null);
+ const [buildingId, setBuildingId] = useState(null);
+ const [syncProgress, setSyncProgress] = useState