@polderlabs/bizar 4.5.0 → 4.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  2. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-B4OfGAwz.js +361 -0
  4. package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +1 -0
  5. package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
  6. package/bizar-dash/dist/assets/{mobile--0FBIKX3.js → mobile-BRhoDOUz.js} +1 -2
  7. package/bizar-dash/dist/assets/{mobile--0FBIKX3.js.map → mobile-BRhoDOUz.js.map} +1 -1
  8. package/bizar-dash/dist/assets/{mobile-OgRp8VIb.js → mobile-lbH6szyX.js} +2 -3
  9. package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +1 -0
  10. package/bizar-dash/dist/index.html +3 -3
  11. package/bizar-dash/dist/mobile.html +2 -2
  12. package/bizar-dash/skills/bizar/SKILL.md +20 -0
  13. package/bizar-dash/skills/headroom/SKILL.md +94 -0
  14. package/bizar-dash/skills/lightrag/SKILL.md +11 -0
  15. package/bizar-dash/skills/obsidian/SKILL.md +13 -0
  16. package/bizar-dash/src/server/api.mjs +6 -0
  17. package/bizar-dash/src/server/headroom.mjs +647 -0
  18. package/bizar-dash/src/server/memory-lightrag.mjs +281 -6
  19. package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
  20. package/bizar-dash/src/server/memory-store.mjs +151 -0
  21. package/bizar-dash/src/server/mods-loader.mjs +13 -5
  22. package/bizar-dash/src/server/providers-store.mjs +73 -1
  23. package/bizar-dash/src/server/routes/_shared.mjs +17 -0
  24. package/bizar-dash/src/server/routes/chat.mjs +38 -0
  25. package/bizar-dash/src/server/routes/headroom.mjs +126 -0
  26. package/bizar-dash/src/server/routes/lightrag.mjs +6 -2
  27. package/bizar-dash/src/server/routes/memory.mjs +439 -4
  28. package/bizar-dash/src/server/schedules-runner.mjs +4 -4
  29. package/bizar-dash/src/server/server.mjs +55 -37
  30. package/bizar-dash/src/server/watcher.mjs +2 -2
  31. package/bizar-dash/src/web/App.tsx +42 -6
  32. package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
  33. package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
  34. package/bizar-dash/src/web/components/Toast.tsx +1 -1
  35. package/bizar-dash/src/web/components/Topbar.tsx +3 -1
  36. package/bizar-dash/src/web/lib/api.ts +12 -11
  37. package/bizar-dash/src/web/lib/types.ts +16 -0
  38. package/bizar-dash/src/web/main.tsx +1 -0
  39. package/bizar-dash/src/web/styles/chat.css +2 -0
  40. package/bizar-dash/src/web/styles/main.css +52 -23
  41. package/bizar-dash/src/web/styles/memory.css +955 -0
  42. package/bizar-dash/src/web/styles/tasks.css +2 -0
  43. package/bizar-dash/src/web/views/Memory.tsx +141 -0
  44. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
  45. package/bizar-dash/src/web/views/Overview.tsx +7 -2
  46. package/bizar-dash/src/web/views/Settings.tsx +32 -1
  47. package/bizar-dash/src/web/views/Skills.tsx +3 -2
  48. package/bizar-dash/src/web/views/Tasks.tsx +4 -3
  49. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
  50. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
  51. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
  52. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +356 -0
  53. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
  54. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
  55. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
  56. package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
  57. package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
  58. package/bizar-dash/tests/headroom-install.test.mjs +173 -0
  59. package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
  60. package/bizar-dash/tests/headroom-status.test.mjs +117 -0
  61. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
  62. package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
  63. package/bizar-dash/tests/memory-tab.test.mjs +322 -0
  64. package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
  65. package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
  66. package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
  67. package/cli/artifact.mjs +11 -4
  68. package/cli/bin.mjs +211 -12
  69. package/cli/doctor.mjs +5 -3
  70. package/cli/memory.mjs +65 -21
  71. package/cli/provision.mjs +8 -2
  72. package/install.sh +1 -2
  73. package/package.json +3 -3
  74. package/bizar-dash/dist/assets/main-CDFKHzBg.css +0 -1
  75. package/bizar-dash/dist/assets/main-NYFpS2wY.js +0 -312
  76. package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +0 -1
  77. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +0 -1
@@ -0,0 +1,356 @@
1
+ // src/web/views/memory/MemoryOverview.tsx — Memory tab overview panel (KPIs + sub-system pills).
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ Activity,
5
+ Brain,
6
+ CheckCircle2,
7
+ Database,
8
+ FileText,
9
+ GitBranch,
10
+ RefreshCw,
11
+ Search,
12
+ ShieldCheck,
13
+ Sparkles,
14
+ XCircle,
15
+ AlertTriangle,
16
+ } from 'lucide-react';
17
+ import { Button } from '../../components/Button';
18
+ import { Card, CardMeta, CardTitle } from '../../components/Card';
19
+ import { Spinner } from '../../components/Spinner';
20
+ import { useToast } from '../../components/Toast';
21
+ import { api } from '../../lib/api';
22
+ import { formatTime, cn } from '../../lib/utils';
23
+
24
+ export type HealthResponse = {
25
+ score: number;
26
+ status: 'healthy' | 'degraded' | 'unhealthy' | 'unconfigured';
27
+ checks: Array<{ name: string; pass: boolean; detail: string }>;
28
+ message: string;
29
+ };
30
+
31
+ export type VaultStats = {
32
+ exists: boolean;
33
+ vaultRoot: string;
34
+ mode: string;
35
+ noteCount: number;
36
+ totalSize: number;
37
+ folderCount: number;
38
+ folders: string[];
39
+ lastModified: number | null;
40
+ gitClean: boolean | null;
41
+ gitBranch: string | null;
42
+ };
43
+
44
+ type MemoryStatus = {
45
+ initialized: boolean;
46
+ mode?: string;
47
+ projectId?: string;
48
+ vaultRoot?: string;
49
+ branch?: string;
50
+ gitClean?: boolean | null;
51
+ noteCount?: number;
52
+ lastSecretScan?: string | null;
53
+ };
54
+
55
+ type LightragStats = {
56
+ running: boolean;
57
+ pid: number | null;
58
+ host: string;
59
+ port: number;
60
+ indexedApprox: number;
61
+ queryCountLast24h: number;
62
+ lastReindexAt: string | null;
63
+ avgResponseMs?: number | null;
64
+ noteCount?: number;
65
+ };
66
+
67
+ type StorageStats = {
68
+ total: number;
69
+ breakdown: Array<{ name: string; path: string; size: number }>;
70
+ };
71
+
72
+ export type MemoryOverviewData = {
73
+ health: HealthResponse;
74
+ vault: VaultStats;
75
+ lightrag: {
76
+ running: boolean;
77
+ pid: number | null;
78
+ host: string;
79
+ port: number;
80
+ indexedApprox: number;
81
+ queryCountLast24h: number;
82
+ lastReindexAt: string | null;
83
+ };
84
+ storage: {
85
+ total: number;
86
+ breakdown: Array<{ name: string; path: string; size: number }>;
87
+ };
88
+ };
89
+
90
+ type Props = {
91
+ refreshKey: number;
92
+ onRefresh: () => void;
93
+ setActiveSubPanel: (panel: 'overview' | 'lightrag' | 'obsidian' | 'git' | 'semantic' | 'config') => void;
94
+ };
95
+
96
+ export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Props) {
97
+ const toast = useToast();
98
+ const [data, setData] = useState<MemoryOverviewData | null>(null);
99
+ const [loading, setLoading] = useState(true);
100
+
101
+ const reload = async (signal?: AbortSignal) => {
102
+ setLoading(true);
103
+ try {
104
+ const [health, status, lightrag, storage] = await Promise.all([
105
+ api.get<HealthResponse>('/memory/health', signal).catch(() => null),
106
+ api.get<MemoryStatus>('/memory/status', signal).catch(() => null),
107
+ api.get<LightragStats>('/memory/lightrag/stats', signal).catch(() => null),
108
+ api.get<StorageStats>('/memory/storage', signal).catch(() => null),
109
+ ]);
110
+ setData({
111
+ health: health || {
112
+ score: 0,
113
+ status: 'unconfigured',
114
+ checks: [],
115
+ message: 'memory not initialised',
116
+ },
117
+ vault: {
118
+ exists: !!status?.initialized,
119
+ vaultRoot: status?.vaultRoot || '',
120
+ mode: status?.mode || 'local-only',
121
+ noteCount: status?.noteCount || 0,
122
+ totalSize: storage?.total || 0,
123
+ folderCount: 0,
124
+ folders: [],
125
+ lastModified: null,
126
+ gitClean: status?.gitClean ?? null,
127
+ gitBranch: status?.branch || null,
128
+ },
129
+ lightrag: lightrag || {
130
+ running: false,
131
+ pid: null,
132
+ host: '127.0.0.1',
133
+ port: 9621,
134
+ indexedApprox: 0,
135
+ queryCountLast24h: 0,
136
+ lastReindexAt: null,
137
+ },
138
+ storage: storage || { total: 0, breakdown: [] },
139
+ });
140
+ } catch (err) {
141
+ toast.error(`Memory overview failed: ${(err as Error).message}`);
142
+ } finally {
143
+ setLoading(false);
144
+ }
145
+ };
146
+
147
+ useEffect(() => {
148
+ const ctrl = new AbortController();
149
+ void reload(ctrl.signal);
150
+ return () => ctrl.abort();
151
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
+ }, [refreshKey]);
153
+
154
+ if (loading && !data) {
155
+ return (
156
+ <div className="view-loading">
157
+ <Spinner size="lg" />
158
+ <p>Loading memory overview…</p>
159
+ </div>
160
+ );
161
+ }
162
+ if (!data) {
163
+ return <div className="muted">No memory data available.</div>;
164
+ }
165
+
166
+ const { health, vault, lightrag, storage } = data;
167
+ const scoreColor =
168
+ health.score >= 80 ? 'var(--success)' :
169
+ health.score >= 50 ? 'var(--warning)' :
170
+ 'var(--error)';
171
+
172
+ return (
173
+ <div className="memory-overview">
174
+ {/* ── Health score hero ─────────────────────────────────────────── */}
175
+ <Card variant="elevated" className="memory-health-hero">
176
+ <div className="memory-health-score" style={{ borderColor: scoreColor }}>
177
+ <span className="memory-health-score-value" style={{ color: scoreColor }}>
178
+ {health.score}
179
+ </span>
180
+ <span className="memory-health-score-label">/ 100</span>
181
+ </div>
182
+ <div className="memory-health-meta">
183
+ <h3 className="memory-health-title">
184
+ <Activity size={16} />
185
+ Memory health ·{' '}
186
+ <span style={{ color: scoreColor }}>{health.status}</span>
187
+ </h3>
188
+ <p className="memory-health-message">{health.message}</p>
189
+ <Button variant="ghost" size="sm" onClick={onRefresh}>
190
+ <RefreshCw size={12} /> Refresh
191
+ </Button>
192
+ </div>
193
+ </Card>
194
+
195
+ {/* ── Source pills ─────────────────────────────────────────────── */}
196
+ <div className="memory-source-pills">
197
+ <button
198
+ type="button"
199
+ className="memory-source-pill"
200
+ onClick={() => setActiveSubPanel('lightrag')}
201
+ >
202
+ <Brain size={14} />
203
+ <span>LightRAG</span>
204
+ <span className={cn('memory-source-pill-status', lightrag.running ? 'on' : 'off')}>
205
+ {lightrag.running ? 'running' : 'stopped'}
206
+ </span>
207
+ </button>
208
+ <button
209
+ type="button"
210
+ className="memory-source-pill"
211
+ onClick={() => setActiveSubPanel('obsidian')}
212
+ >
213
+ <FileText size={14} />
214
+ <span>Obsidian Vault</span>
215
+ <span className={cn('memory-source-pill-status', vault.exists ? 'on' : 'off')}>
216
+ {vault.noteCount} notes
217
+ </span>
218
+ </button>
219
+ <button
220
+ type="button"
221
+ className="memory-source-pill"
222
+ onClick={() => setActiveSubPanel('git')}
223
+ >
224
+ <GitBranch size={14} />
225
+ <span>Git Sync</span>
226
+ <span className={cn(
227
+ 'memory-source-pill-status',
228
+ vault.gitClean === null ? 'na' : vault.gitClean ? 'on' : 'warn',
229
+ )}>
230
+ {vault.gitClean === null ? '—' : vault.gitClean ? 'clean' : 'dirty'}
231
+ </span>
232
+ </button>
233
+ <button
234
+ type="button"
235
+ className="memory-source-pill"
236
+ onClick={() => setActiveSubPanel('semantic')}
237
+ >
238
+ <Search size={14} />
239
+ <span>Semantic Search</span>
240
+ <span className="memory-source-pill-status na">cross-source</span>
241
+ </button>
242
+ </div>
243
+
244
+ {/* ── KPI grid ─────────────────────────────────────────────────── */}
245
+ <div className="memory-stat-grid">
246
+ <StatCard
247
+ icon={<Brain size={16} />}
248
+ label="LightRAG"
249
+ value={lightrag.running ? `running · ${lightrag.queryCountLast24h} q/24h` : 'stopped'}
250
+ sub={lightrag.running ? `${lightrag.indexedApprox} indexed chunks` : 'start from the LightRAG panel'}
251
+ onClick={() => setActiveSubPanel('lightrag')}
252
+ />
253
+ <StatCard
254
+ icon={<FileText size={16} />}
255
+ label="Obsidian"
256
+ value={`${vault.noteCount} notes`}
257
+ sub={vault.folderCount > 0 ? `${vault.folderCount} folders` : 'no folders yet'}
258
+ onClick={() => setActiveSubPanel('obsidian')}
259
+ />
260
+ <StatCard
261
+ icon={<GitBranch size={16} />}
262
+ label="Git"
263
+ value={vault.gitBranch ? `branch ${vault.gitBranch}` : 'local-only'}
264
+ sub={
265
+ vault.gitClean === null ? 'not configured' :
266
+ vault.gitClean ? 'clean working tree' :
267
+ 'dirty — commit or pull'
268
+ }
269
+ onClick={() => setActiveSubPanel('git')}
270
+ />
271
+ <StatCard
272
+ icon={<Database size={16} />}
273
+ label="Storage"
274
+ value={formatBytes(storage.total)}
275
+ sub={storage.breakdown.length > 0
276
+ ? `${storage.breakdown.length} dirs tracked`
277
+ : 'no memory data on disk'}
278
+ />
279
+ <StatCard
280
+ icon={<Sparkles size={16} />}
281
+ label="Last reindex"
282
+ value={lightrag.lastReindexAt ? formatTime(lightrag.lastReindexAt) : 'never'}
283
+ sub="lightrag ingest"
284
+ onClick={() => setActiveSubPanel('lightrag')}
285
+ />
286
+ <StatCard
287
+ icon={<ShieldCheck size={16} />}
288
+ label="Health"
289
+ value={`${health.score}/100`}
290
+ sub={health.status}
291
+ onClick={() => setActiveSubPanel('config')}
292
+ />
293
+ </div>
294
+
295
+ {/* ── Health checks detail ─────────────────────────────────────── */}
296
+ {health.checks.length > 0 && (
297
+ <Card>
298
+ <CardTitle>Health checks</CardTitle>
299
+ <CardMeta>Composite score across all memory subsystems</CardMeta>
300
+ <ul className="memory-check-list">
301
+ {health.checks.map((c) => (
302
+ <li key={c.name} className="memory-check-row">
303
+ <span className="memory-check-icon">
304
+ {c.pass
305
+ ? <CheckCircle2 size={14} style={{ color: 'var(--success)' }} />
306
+ : c.name.includes('secrets') || c.name.includes('schema')
307
+ ? <AlertTriangle size={14} style={{ color: 'var(--warning)' }} />
308
+ : <XCircle size={14} style={{ color: 'var(--error)' }} />}
309
+ </span>
310
+ <span className="memory-check-name">{c.name}</span>
311
+ <span className="memory-check-detail muted">{c.detail}</span>
312
+ </li>
313
+ ))}
314
+ </ul>
315
+ </Card>
316
+ )}
317
+ </div>
318
+ );
319
+ }
320
+
321
+ function StatCard({
322
+ icon,
323
+ label,
324
+ value,
325
+ sub,
326
+ onClick,
327
+ }: {
328
+ icon: React.ReactNode;
329
+ label: string;
330
+ value: string;
331
+ sub: string;
332
+ onClick?: () => void;
333
+ }) {
334
+ const Comp = onClick ? 'button' : 'div';
335
+ return (
336
+ <Comp
337
+ type={onClick ? 'button' : undefined}
338
+ onClick={onClick}
339
+ className={cn('memory-stat-card', onClick && 'memory-stat-card-clickable')}
340
+ >
341
+ <div className="memory-stat-icon">{icon}</div>
342
+ <div className="memory-stat-body">
343
+ <div className="memory-stat-label">{label}</div>
344
+ <div className="memory-stat-value">{value}</div>
345
+ <div className="memory-stat-sub muted">{sub}</div>
346
+ </div>
347
+ </Comp>
348
+ );
349
+ }
350
+
351
+ function formatBytes(n: number): string {
352
+ if (n < 1024) return `${n} B`;
353
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
354
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
355
+ return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
356
+ }
@@ -0,0 +1,160 @@
1
+ // src/web/views/memory/MemoryStatusCard.tsx — overview status card for the Memory subsystem.
2
+ //
3
+ // Slimmed-down version of MemoryOverview: a single Card with lightrag status,
4
+ // vault note count, git sync state, and a button to open the full Memory tab.
5
+ import { useEffect, useState } from 'react';
6
+ import {
7
+ Activity,
8
+ Brain,
9
+ ExternalLink,
10
+ FileText,
11
+ GitBranch,
12
+ RefreshCw,
13
+ } from 'lucide-react';
14
+ import { Button } from '../../components/Button';
15
+ import { Card, CardMeta, CardTitle } from '../../components/Card';
16
+ import { api } from '../../lib/api';
17
+ import { formatTime, cn } from '../../lib/utils';
18
+
19
+ type MemoryCardData = {
20
+ initialized: boolean;
21
+ mode?: string;
22
+ projectId?: string;
23
+ vaultRoot?: string;
24
+ branch?: string;
25
+ gitClean?: boolean | null;
26
+ noteCount?: number;
27
+ healthScore?: number;
28
+ healthStatus?: 'healthy' | 'degraded' | 'unhealthy' | 'unconfigured';
29
+ lightrag?: { running: boolean; indexedApprox: number };
30
+ };
31
+
32
+ type Props = {
33
+ setActiveTab: (id: string) => void;
34
+ refreshKey?: number;
35
+ };
36
+
37
+ export function MemoryStatusCard({ setActiveTab, refreshKey = 0 }: Props) {
38
+ const [data, setData] = useState<MemoryCardData | null>(null);
39
+ const [loading, setLoading] = useState(true);
40
+
41
+ const reload = async () => {
42
+ setLoading(true);
43
+ try {
44
+ const [status, health, lightrag] = await Promise.all([
45
+ api.get<MemoryCardData>('/memory/status').catch(() => null),
46
+ api.get<{ score: number; status: 'healthy' | 'degraded' | 'unhealthy' | 'unconfigured' }>(
47
+ '/memory/health',
48
+ ).catch(() => null),
49
+ api.get<{ running: boolean; indexedApprox: number }>('/memory/lightrag/stats').catch(() => null),
50
+ ]);
51
+ setData({
52
+ initialized: !!status?.initialized,
53
+ mode: status?.mode,
54
+ projectId: status?.projectId,
55
+ vaultRoot: status?.vaultRoot,
56
+ branch: status?.branch,
57
+ gitClean: status?.gitClean,
58
+ noteCount: status?.noteCount || 0,
59
+ healthScore: health?.score,
60
+ healthStatus: health?.status,
61
+ lightrag: lightrag ? { running: lightrag.running, indexedApprox: lightrag.indexedApprox } : undefined,
62
+ });
63
+ } catch {
64
+ setData({ initialized: false });
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ };
69
+
70
+ useEffect(() => {
71
+ reload();
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [refreshKey]);
74
+
75
+ const openMemory = () => setActiveTab('memory');
76
+
77
+ const healthColor =
78
+ data?.healthStatus === 'healthy' ? 'var(--success)' :
79
+ data?.healthStatus === 'degraded' ? 'var(--warning)' :
80
+ data?.healthStatus === 'unhealthy' ? 'var(--error)' :
81
+ 'var(--text-dim)';
82
+
83
+ return (
84
+ <Card className="memory-overview-card">
85
+ <CardTitle>
86
+ <Brain size={14} /> Memory
87
+ <Button variant="ghost" size="sm" style={{ marginLeft: 'auto' }} onClick={openMemory}>
88
+ Open Memory <ExternalLink size={12} />
89
+ </Button>
90
+ <Button variant="ghost" size="sm" onClick={reload} aria-label="Refresh" title="Refresh">
91
+ <RefreshCw size={12} />
92
+ </Button>
93
+ </CardTitle>
94
+ <CardMeta>
95
+ LightRAG + Obsidian vault + git sync — opens the dedicated Memory tab.
96
+ </CardMeta>
97
+
98
+ {!data?.initialized ? (
99
+ <div className="muted">
100
+ Memory not initialised. Click <strong>Open Memory</strong> to set it up.
101
+ </div>
102
+ ) : (
103
+ <div className="memory-status-card-grid">
104
+ <div className="memory-status-card-cell">
105
+ <div className="memory-status-card-label">
106
+ <Activity size={11} /> Health
107
+ </div>
108
+ <div className="memory-status-card-value" style={{ color: healthColor }}>
109
+ {data.healthScore !== undefined ? `${data.healthScore}/100` : '—'}
110
+ </div>
111
+ <div className="memory-status-card-sub muted">{data.healthStatus || '—'}</div>
112
+ </div>
113
+
114
+ <div className="memory-status-card-cell">
115
+ <div className="memory-status-card-label">
116
+ <Brain size={11} /> LightRAG
117
+ </div>
118
+ <div className="memory-status-card-value">
119
+ {data.lightrag?.running ? 'running' : 'stopped'}
120
+ </div>
121
+ <div className="memory-status-card-sub muted">
122
+ {data.lightrag ? `${data.lightrag.indexedApprox} chunks` : '—'}
123
+ </div>
124
+ </div>
125
+
126
+ <div className="memory-status-card-cell">
127
+ <div className="memory-status-card-label">
128
+ <FileText size={11} /> Vault
129
+ </div>
130
+ <div className="memory-status-card-value">{data.noteCount} notes</div>
131
+ <div className="memory-status-card-sub muted ellipsis" title={data.vaultRoot || ''}>
132
+ {data.vaultRoot ? data.vaultRoot.split('/').slice(-2).join('/') : '—'}
133
+ </div>
134
+ </div>
135
+
136
+ <div className="memory-status-card-cell">
137
+ <div className="memory-status-card-label">
138
+ <GitBranch size={11} /> Git
139
+ </div>
140
+ <div className="memory-status-card-value">
141
+ {data.gitClean === null
142
+ ? data.mode === 'local-only' ? 'local-only' : '—'
143
+ : data.gitClean ? 'clean' : 'dirty'}
144
+ </div>
145
+ <div className="memory-status-card-sub muted">
146
+ {data.branch ? `branch ${data.branch}` : formatTime(data.branch || '') || '—'}
147
+ </div>
148
+ </div>
149
+ </div>
150
+ )}
151
+
152
+ {loading && !data && (
153
+ <div className="muted text-sm">Loading…</div>
154
+ )}
155
+ </Card>
156
+ );
157
+ }
158
+
159
+ // Allow css class composition without TS complaining about missing module declarations.
160
+ void cn;