@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
@@ -162,6 +162,8 @@
162
162
  cursor: grab;
163
163
  transition: transform var(--motion-fast) var(--ease),
164
164
  border-color var(--motion-base) var(--ease);
165
+ content-visibility: auto;
166
+ contain-intrinsic-size: 0 80px;
165
167
  }
166
168
 
167
169
  .task-card:hover {
@@ -0,0 +1,141 @@
1
+ // src/web/views/Memory.tsx — v4.7.0 dedicated Memory tab.
2
+ //
3
+ // Three-column layout: left rail (source picker), main panel (per-source UI),
4
+ // and a top stat row + a health hero at the top of every panel for context.
5
+ //
6
+ // The Memory subsystem has 4 sources (LightRAG, Obsidian vault, git sync,
7
+ // semantic search) plus a config panel and a high-level overview. Each is a
8
+ // standalone component under views/memory/.
9
+
10
+ import React, { useCallback, useState } from 'react';
11
+ import {
12
+ Brain,
13
+ FileText,
14
+ GitBranch,
15
+ LayoutDashboard,
16
+ RefreshCw,
17
+ Search as SearchIcon,
18
+ Sliders,
19
+ } from 'lucide-react';
20
+ import { Card } from '../components/Card';
21
+ import { useToast } from '../components/Toast';
22
+ import { cn } from '../lib/utils';
23
+ import { MemoryOverview } from './memory/MemoryOverview';
24
+ import { LightragPanel } from './memory/LightragPanel';
25
+ import { ObsidianPanel } from './memory/ObsidianPanel';
26
+ import { GitSyncPanel } from './memory/GitSyncPanel';
27
+ import { SemanticSearchPanel } from './memory/SemanticSearchPanel';
28
+ import { ConfigPanel } from './memory/ConfigPanel';
29
+
30
+ type SubPanel = 'overview' | 'lightrag' | 'obsidian' | 'git' | 'semantic' | 'config';
31
+
32
+ type Props = {
33
+ snapshot: unknown;
34
+ settings: unknown;
35
+ activeTab: string;
36
+ setActiveTab: (id: string) => void;
37
+ refreshSnapshot: () => Promise<void>;
38
+ };
39
+
40
+ const SOURCES: Array<{
41
+ id: SubPanel;
42
+ label: string;
43
+ icon: typeof LayoutDashboard;
44
+ }> = [
45
+ { id: 'overview', label: 'Overview', icon: LayoutDashboard },
46
+ { id: 'lightrag', label: 'LightRAG', icon: Brain },
47
+ { id: 'obsidian', label: 'Obsidian Vault', icon: FileText },
48
+ { id: 'git', label: 'Git Sync', icon: GitBranch },
49
+ { id: 'semantic', label: 'Semantic Search', icon: SearchIcon },
50
+ { id: 'config', label: 'Config', icon: Sliders },
51
+ ];
52
+
53
+ function MemoryInner(_props: Props) {
54
+ const toast = useToast();
55
+ const [active, setActive] = useState<SubPanel>('overview');
56
+ const [refreshKey, setRefreshKey] = useState(0);
57
+
58
+ const refresh = useCallback(() => {
59
+ setRefreshKey((k) => k + 1);
60
+ }, []);
61
+
62
+ const onRefreshAll = useCallback(async () => {
63
+ refresh();
64
+ toast.info('Refreshing memory…', 1200);
65
+ }, [refresh, toast]);
66
+
67
+ const renderPanel = () => {
68
+ switch (active) {
69
+ case 'overview':
70
+ return <MemoryOverview refreshKey={refreshKey} onRefresh={refresh} setActiveSubPanel={setActive} />;
71
+ case 'lightrag':
72
+ return <LightragPanel refreshKey={refreshKey} />;
73
+ case 'obsidian':
74
+ return <ObsidianPanel refreshKey={refreshKey} />;
75
+ case 'git':
76
+ return <GitSyncPanel refreshKey={refreshKey} />;
77
+ case 'semantic':
78
+ return <SemanticSearchPanel refreshKey={refreshKey} />;
79
+ case 'config':
80
+ return <ConfigPanel refreshKey={refreshKey} />;
81
+ default:
82
+ return <Card>Unknown panel: {active}</Card>;
83
+ }
84
+ };
85
+
86
+ return (
87
+ <div className="view view-memory memory-tab">
88
+ {/* ── Header ─────────────────────────────────────────────── */}
89
+ <header className="view-header">
90
+ <div className="view-header-text">
91
+ <h2 className="view-title">
92
+ <Brain size={18} /> Memory
93
+ </h2>
94
+ <p className="view-subtitle">
95
+ LightRAG, Obsidian vault, git sync, and semantic search — all in one place.
96
+ </p>
97
+ </div>
98
+ <div className="view-actions">
99
+ <button
100
+ type="button"
101
+ className="icon-btn"
102
+ onClick={onRefreshAll}
103
+ aria-label="Refresh memory"
104
+ title="Refresh"
105
+ >
106
+ <RefreshCw size={14} />
107
+ </button>
108
+ </div>
109
+ </header>
110
+
111
+ <div className="memory-tab-body">
112
+ {/* ── Source rail ─────────────────────────────────────── */}
113
+ <nav className="memory-source-rail" aria-label="Memory sources">
114
+ {SOURCES.map((s) => {
115
+ const Icon = s.icon;
116
+ const isActive = active === s.id;
117
+ return (
118
+ <button
119
+ key={s.id}
120
+ type="button"
121
+ role="tab"
122
+ aria-selected={isActive}
123
+ className={cn('memory-source-button', isActive && 'memory-source-button-active')}
124
+ onClick={() => setActive(s.id)}
125
+ >
126
+ <Icon size={16} aria-hidden />
127
+ <span>{s.label}</span>
128
+ </button>
129
+ );
130
+ })}
131
+ </nav>
132
+
133
+ {/* ── Main panel ─────────────────────────────────────── */}
134
+ <div className="memory-main" key={active}>
135
+ {renderPanel()}
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+ export const Memory = React.memo(MemoryInner);
@@ -8,7 +8,7 @@
8
8
  // (written by minimax.mjs after every chatCompletion / fetchRemains call).
9
9
  // The Usage Analytics tab is only shown when the key is configured.
10
10
 
11
- import { useCallback, useEffect, useMemo, useState } from 'react';
11
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
12
12
  import {
13
13
  Coins,
14
14
  RefreshCw,
@@ -196,7 +196,7 @@ type TimeRange = '24h' | '7d' | '30d' | 'custom';
196
196
 
197
197
  // ─── Main component ────────────────────────────────────────────────────
198
198
 
199
- export function MiniMaxUsage({ activeTab, setActiveTab }: Props) {
199
+ function MiniMaxUsageInner({ activeTab, setActiveTab }: Props) {
200
200
  // Share state between tabs.
201
201
  const [view, setView] = useState<'quota' | 'analytics'>('quota');
202
202
  const toast = useToast();
@@ -921,3 +921,4 @@ function OnboardingWizard({ step, setStep, keyDraft, setKeyDraft, showKey, setSh
921
921
  </div>
922
922
  );
923
923
  }
924
+ export const MiniMaxUsage = React.memo(MiniMaxUsageInner);
@@ -1,5 +1,5 @@
1
1
  // src/views/Overview.tsx — v3.7.0 activity cards + SSE stream.
2
- import { useEffect, useRef, useState } from 'react';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
3
  import {
4
4
  Bot,
5
5
  CheckSquare,
@@ -30,6 +30,7 @@ import {
30
30
  } from 'lucide-react';
31
31
  import { Card, CardTitle, CardMeta } from '../components/Card';
32
32
  import { Button } from '../components/Button';
33
+ import { MemoryStatusCard } from './memory/MemoryStatusCard';
33
34
  import { EmptyState } from '../components/EmptyState';
34
35
  import { Spinner } from '../components/Spinner';
35
36
  import { useToast } from '../components/Toast';
@@ -48,7 +49,7 @@ type Props = {
48
49
  refreshSnapshot: () => Promise<void>;
49
50
  };
50
51
 
51
- export function Overview({
52
+ function OverviewInner({
52
53
  snapshot,
53
54
  settings,
54
55
  setActiveTab,
@@ -461,6 +462,8 @@ export function Overview({
461
462
  )}
462
463
  </Card>
463
464
 
465
+ <MemoryStatusCard setActiveTab={setActiveTab} />
466
+
464
467
  <div className="overview-cols">
465
468
  <Card>
466
469
  <CardTitle>Mods</CardTitle>
@@ -810,3 +813,5 @@ function ActivityFeedItem({
810
813
  </div>
811
814
  );
812
815
  }
816
+ const OverviewMemo = React.memo(OverviewInner);
817
+ export { OverviewMemo as Overview };
@@ -36,6 +36,7 @@ import { Card, CardTitle, CardMeta } from '../components/Card';
36
36
  import { useToast } from '../components/Toast';
37
37
  import { api } from '../lib/api';
38
38
  import { cn } from '../lib/utils';
39
+ import { HeadroomSettingsCard } from '../components/HeadroomSettings';
39
40
  import { Ws } from '../lib/ws';
40
41
  import {
41
42
  applyTheme,
@@ -73,6 +74,21 @@ const PRESET_THEMES = [
73
74
  { name: 'Mono', accent: '#6b7280' },
74
75
  ];
75
76
 
77
+ // v5.0.0 — Headroom default settings
78
+ const DEFAULT_HEADROOM_SETTINGS = {
79
+ enabled: true,
80
+ autoInstall: true,
81
+ port: 8787,
82
+ host: '127.0.0.1',
83
+ outputShaper: false,
84
+ telemetry: false,
85
+ budget: 0,
86
+ backend: 'anthropic',
87
+ autoStart: true,
88
+ autoWrap: true,
89
+ routeAllProviders: true,
90
+ };
91
+
76
92
  const FONT_FAMILIES = [
77
93
  'Inter',
78
94
  'system-ui',
@@ -469,7 +485,7 @@ function UpdatesCard() {
469
485
  );
470
486
  }
471
487
 
472
- export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
488
+ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
473
489
  const toast = useToast();
474
490
  const [settings, setSettings] = useState<Settings>(initial);
475
491
  const [dirty, setDirty] = useState(false);
@@ -552,6 +568,7 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
552
568
  { id: 'dashboard', label: 'Dashboard' },
553
569
  { id: 'background', label: 'Background' },
554
570
  { id: 'system-llm', label: 'System LLM' },
571
+ { id: 'headroom', label: 'Headroom' },
555
572
  { id: 'updates', label: 'Updates' },
556
573
  { id: 'activity-log', label: 'Activity' },
557
574
  { id: 'about', label: 'About' },
@@ -1586,6 +1603,19 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
1586
1603
  </Card>
1587
1604
  </div>
1588
1605
 
1606
+ <div data-section="headroom" style={{display: (activeSection === null || activeSection === 'headroom') ? 'block' : 'none'}}>
1607
+ <HeadroomSettingsCard
1608
+ settings={settings.headroom || DEFAULT_HEADROOM_SETTINGS}
1609
+ onPatch={(patch) => {
1610
+ setSettings((cur) => ({
1611
+ ...cur,
1612
+ headroom: { ...(cur.headroom || DEFAULT_HEADROOM_SETTINGS), ...patch },
1613
+ }));
1614
+ setDirty(true);
1615
+ }}
1616
+ />
1617
+ </div>
1618
+
1589
1619
  <PairDeviceCard />
1590
1620
 
1591
1621
  <div style={{display: (activeSection === null || activeSection === 'activity-log') ? 'block' : 'none'}}>
@@ -1791,3 +1821,4 @@ function ActivityLogCard() {
1791
1821
  </Card>
1792
1822
  );
1793
1823
  }
1824
+ export const SettingsView = React.memo(SettingsViewInner);
@@ -1,5 +1,5 @@
1
1
  // src/views/Skills.tsx — v4.0.0 skills browser: scans all local SKILL.md sources.
2
- import { useEffect, useMemo, useState } from 'react';
2
+ import React, { useEffect, useMemo, useState } from 'react';
3
3
  import {
4
4
  Sparkles,
5
5
  Search,
@@ -58,7 +58,7 @@ const SOURCE_ICON: Record<string, typeof Package> = {
58
58
  project: Folder,
59
59
  };
60
60
 
61
- export function Skills({ snapshot, refreshSnapshot }: Props) {
61
+ function SkillsInner({ snapshot, refreshSnapshot }: Props) {
62
62
  const toast = useToast();
63
63
  const modal = useModal();
64
64
  const [skills, setSkills] = useState<Skill[]>([]);
@@ -377,3 +377,4 @@ function highlightMatch(text: string, q: string): React.ReactNode {
377
377
  </>
378
378
  );
379
379
  }
380
+ export const Skills = React.memo(SkillsInner);
@@ -20,7 +20,7 @@
20
20
  // We keep the API contract (statuses: queued/doing/done/blocked/backlog)
21
21
  // so the dashboard doesn't break the existing server, tasks store, and
22
22
  // tests. The UI maps API statuses to friendlier labels.
23
- import { useEffect, useMemo, useState } from 'react';
23
+ import React, { useEffect, useMemo, useState } from 'react';
24
24
  import {
25
25
  CheckSquare,
26
26
  Plus,
@@ -76,7 +76,7 @@ const COLUMNS: { id: Task['status']; label: string; kind: StatusKind }[] = [
76
76
  const PRIORITIES = ['low', 'normal', 'high'] as const;
77
77
  type Priority = (typeof PRIORITIES)[number];
78
78
 
79
- export function Tasks({ snapshot, refreshSnapshot, setActiveTab }: Props) {
79
+ function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
80
80
  const toast = useToast();
81
81
  const modal = useModal();
82
82
  const [tasks, setTasks] = useState<Task[]>(snapshot.tasks || []);
@@ -771,4 +771,5 @@ function openEditTaskModal(
771
771
  </div>
772
772
  ),
773
773
  });
774
- }
774
+ }
775
+ export const Tasks = React.memo(TasksInner);
@@ -0,0 +1,289 @@
1
+ // src/web/views/memory/ConfigPanel.tsx — global memory config + per-system toggles.
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ Brain,
5
+ Database,
6
+ FileText,
7
+ GitBranch,
8
+ Loader2,
9
+ Plug,
10
+ Save,
11
+ Sparkles,
12
+ } from 'lucide-react';
13
+ import { Button } from '../../components/Button';
14
+ import { Card, CardMeta, CardTitle } from '../../components/Card';
15
+ import { Spinner } from '../../components/Spinner';
16
+ import { useToast } from '../../components/Toast';
17
+ import { api } from '../../lib/api';
18
+ import { cn } from '../../lib/utils';
19
+
20
+ type GlobalConfig = {
21
+ lightrag?: { enabled?: boolean; url?: string; llm?: string; embedding?: string };
22
+ obsidian?: { vaultPath?: string; syncInterval?: number };
23
+ git?: { repoPath?: string; remoteUrl?: string; branch?: string; autoSync?: boolean };
24
+ };
25
+
26
+ type Props = { refreshKey: number };
27
+
28
+ export function ConfigPanel({ refreshKey }: Props) {
29
+ const toast = useToast();
30
+ const [cfg, setCfg] = useState<GlobalConfig | null>(null);
31
+ const [loading, setLoading] = useState(true);
32
+ const [saving, setSaving] = useState(false);
33
+ const [tests, setTests] = useState<Array<{ name: string; pass: boolean; detail: string }> | null>(null);
34
+ const [testing, setTesting] = useState(false);
35
+
36
+ const reload = async () => {
37
+ setLoading(true);
38
+ try {
39
+ const r = await api.get<{ config: GlobalConfig }>('/memory/config/global');
40
+ setCfg(r.config);
41
+ } catch (err) {
42
+ toast.error(`Config load failed: ${(err as Error).message}`);
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+
48
+ useEffect(() => {
49
+ reload();
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [refreshKey]);
52
+
53
+ const update = (block: keyof GlobalConfig, key: string, value: unknown) => {
54
+ setCfg((cur) => cur ? { ...cur, [block]: { ...(cur[block] || {}), [key]: value } } : cur);
55
+ };
56
+
57
+ const onSave = async () => {
58
+ if (!cfg) return;
59
+ setSaving(true);
60
+ try {
61
+ await api.put('/memory/config/global', cfg);
62
+ toast.success('Config saved.');
63
+ } catch (err) {
64
+ toast.error(`Save failed: ${(err as Error).message}`);
65
+ } finally {
66
+ setSaving(false);
67
+ }
68
+ };
69
+
70
+ const onTest = async () => {
71
+ setTesting(true);
72
+ try {
73
+ const r = await api.post<{ ok: boolean; checks: Array<{ name: string; pass: boolean; detail: string }> }>(
74
+ '/memory/test-git',
75
+ {},
76
+ );
77
+ setTests(r.checks);
78
+ } catch (err) {
79
+ toast.error(`Test failed: ${(err as Error).message}`);
80
+ } finally {
81
+ setTesting(false);
82
+ }
83
+ };
84
+
85
+ if (loading && !cfg) {
86
+ return (
87
+ <div className="view-loading">
88
+ <Spinner size="md" />
89
+ <p>Loading config…</p>
90
+ </div>
91
+ );
92
+ }
93
+ if (!cfg) return <div className="muted">No config available.</div>;
94
+
95
+ return (
96
+ <div className="memory-panel-content">
97
+ {/* ── LightRAG ──────────────────────────────────────────────── */}
98
+ <Card>
99
+ <CardTitle><Brain size={14} /> LightRAG</CardTitle>
100
+ <CardMeta>URL + model overrides. Server-side defaults from <code>opencode Zen free tier</code>.</CardMeta>
101
+ <div className="memory-config-form">
102
+ <Row label="Enabled" inline>
103
+ <label className="memory-switch">
104
+ <input
105
+ type="checkbox"
106
+ checked={!!cfg.lightrag?.enabled}
107
+ onChange={(e) => update('lightrag', 'enabled', e.target.checked)}
108
+ />
109
+ <span>{cfg.lightrag?.enabled ? 'on' : 'off'}</span>
110
+ </label>
111
+ </Row>
112
+ <Row label="URL">
113
+ <input
114
+ type="text"
115
+ className="input mono"
116
+ value={cfg.lightrag?.url || ''}
117
+ onChange={(e) => update('lightrag', 'url', e.target.value)}
118
+ placeholder="http://127.0.0.1:9621"
119
+ />
120
+ </Row>
121
+ <Row label="LLM model">
122
+ <input
123
+ type="text"
124
+ className="input mono"
125
+ value={cfg.lightrag?.llm || ''}
126
+ onChange={(e) => update('lightrag', 'llm', e.target.value)}
127
+ placeholder="opencode/gpt-5-nano"
128
+ />
129
+ </Row>
130
+ <Row label="Embedding">
131
+ <input
132
+ type="text"
133
+ className="input mono"
134
+ value={cfg.lightrag?.embedding || ''}
135
+ onChange={(e) => update('lightrag', 'embedding', e.target.value)}
136
+ placeholder="opencode/text-embedding-3-small"
137
+ />
138
+ </Row>
139
+ </div>
140
+ </Card>
141
+
142
+ {/* ── Obsidian ──────────────────────────────────────────────── */}
143
+ <Card>
144
+ <CardTitle><FileText size={14} /> Obsidian vault</CardTitle>
145
+ <CardMeta>Path + sync cadence for the markdown vault.</CardMeta>
146
+ <div className="memory-config-form">
147
+ <Row label="Vault path">
148
+ <input
149
+ type="text"
150
+ className="input mono"
151
+ value={cfg.obsidian?.vaultPath || ''}
152
+ onChange={(e) => update('obsidian', 'vaultPath', e.target.value)}
153
+ placeholder="/home/me/vault"
154
+ />
155
+ </Row>
156
+ <Row label="Sync interval (seconds)">
157
+ <input
158
+ type="number"
159
+ min={0}
160
+ max={2592000}
161
+ className="input mono"
162
+ value={cfg.obsidian?.syncInterval ?? 300}
163
+ onChange={(e) => update('obsidian', 'syncInterval', Number(e.target.value))}
164
+ />
165
+ </Row>
166
+ </div>
167
+ </Card>
168
+
169
+ {/* ── Git ───────────────────────────────────────────────────── */}
170
+ <Card>
171
+ <CardTitle><GitBranch size={14} /> Git sync</CardTitle>
172
+ <CardMeta>Repo path, remote, branch, and auto-sync toggle.</CardMeta>
173
+ <div className="memory-config-form">
174
+ <Row label="Repo path">
175
+ <input
176
+ type="text"
177
+ className="input mono"
178
+ value={cfg.git?.repoPath || ''}
179
+ onChange={(e) => update('git', 'repoPath', e.target.value)}
180
+ placeholder="/path/to/repo"
181
+ />
182
+ </Row>
183
+ <Row label="Remote URL">
184
+ <input
185
+ type="text"
186
+ className="input mono"
187
+ value={cfg.git?.remoteUrl || ''}
188
+ onChange={(e) => update('git', 'remoteUrl', e.target.value)}
189
+ placeholder="git@github.com:org/repo.git"
190
+ />
191
+ </Row>
192
+ <Row label="Branch">
193
+ <input
194
+ type="text"
195
+ className="input mono"
196
+ value={cfg.git?.branch || 'main'}
197
+ onChange={(e) => update('git', 'branch', e.target.value)}
198
+ placeholder="main"
199
+ />
200
+ </Row>
201
+ <Row label="Auto-sync" inline>
202
+ <label className="memory-switch">
203
+ <input
204
+ type="checkbox"
205
+ checked={!!cfg.git?.autoSync}
206
+ onChange={(e) => update('git', 'autoSync', e.target.checked)}
207
+ />
208
+ <span>{cfg.git?.autoSync ? 'on' : 'off'}</span>
209
+ </label>
210
+ </Row>
211
+ </div>
212
+ </Card>
213
+
214
+ {/* ── CCR (placeholder when Headroom is installed) ─────────── */}
215
+ <Card>
216
+ <CardTitle>
217
+ <Sparkles size={14} /> CCR (Compress-Cache-Retrieve)
218
+ </CardTitle>
219
+ <CardMeta>
220
+ Reversible compression for very long conversations. Enabled automatically
221
+ when the Headroom mod is installed.
222
+ </CardMeta>
223
+ <div className="muted text-sm">
224
+ See Settings → Headroom for CCR configuration. This panel will surface
225
+ retention + compression-ratio controls when the mod is active.
226
+ </div>
227
+ </Card>
228
+
229
+ {/* ── Test all connections ─────────────────────────────────── */}
230
+ <Card>
231
+ <CardTitle>
232
+ <Plug size={14} /> Connection tests
233
+ </CardTitle>
234
+ <CardMeta>Verify the configured git repo, remote, and path.</CardMeta>
235
+ <div className="memory-action-row">
236
+ <Button variant="secondary" onClick={onTest} disabled={testing}>
237
+ {testing ? <Loader2 size={12} className="memory-spin" /> : <Plug size={12} />}
238
+ Test git connection
239
+ </Button>
240
+ <Button variant="primary" onClick={onSave} disabled={saving}>
241
+ {saving ? <Loader2 size={12} className="memory-spin" /> : <Save size={12} />}
242
+ Save config
243
+ </Button>
244
+ </div>
245
+ {tests && tests.length > 0 && (
246
+ <ul className="memory-check-list" style={{ marginTop: 12 }}>
247
+ {tests.map((t, i) => (
248
+ <li key={i} className="memory-check-row">
249
+ <span className="memory-check-icon">
250
+ <span className={cn('memory-pill', t.pass ? 'memory-pill-ok' : 'memory-pill-warn')}>
251
+ {t.pass ? 'OK' : 'FAIL'}
252
+ </span>
253
+ </span>
254
+ <span className="memory-check-name">{t.name}</span>
255
+ <span className="memory-check-detail muted">{t.detail}</span>
256
+ </li>
257
+ ))}
258
+ </ul>
259
+ )}
260
+ </Card>
261
+ </div>
262
+ );
263
+ }
264
+
265
+ function Row({
266
+ label,
267
+ children,
268
+ inline,
269
+ }: {
270
+ label: string;
271
+ children: React.ReactNode;
272
+ inline?: boolean;
273
+ }) {
274
+ if (inline) {
275
+ return (
276
+ <div className="memory-config-row-inline">
277
+ <div className="memory-config-row-label">{label}</div>
278
+ <div className="memory-config-row-control">{children}</div>
279
+ </div>
280
+ );
281
+ }
282
+ return (
283
+ <label className="memory-config-row-stack">
284
+ <span>{label}</span>
285
+ {children}
286
+ </label>
287
+ );
288
+ }
289
+