@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.
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +361 -0
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +1 -0
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
- package/bizar-dash/dist/assets/{mobile--0FBIKX3.js → mobile-BRhoDOUz.js} +1 -2
- package/bizar-dash/dist/assets/{mobile--0FBIKX3.js.map → mobile-BRhoDOUz.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-OgRp8VIb.js → mobile-lbH6szyX.js} +2 -3
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/bizar/SKILL.md +20 -0
- package/bizar-dash/skills/headroom/SKILL.md +94 -0
- package/bizar-dash/skills/lightrag/SKILL.md +11 -0
- package/bizar-dash/skills/obsidian/SKILL.md +13 -0
- package/bizar-dash/src/server/api.mjs +6 -0
- package/bizar-dash/src/server/headroom.mjs +647 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +281 -6
- package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
- package/bizar-dash/src/server/memory-store.mjs +151 -0
- package/bizar-dash/src/server/mods-loader.mjs +13 -5
- package/bizar-dash/src/server/providers-store.mjs +73 -1
- package/bizar-dash/src/server/routes/_shared.mjs +17 -0
- package/bizar-dash/src/server/routes/chat.mjs +38 -0
- package/bizar-dash/src/server/routes/headroom.mjs +126 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +6 -2
- package/bizar-dash/src/server/routes/memory.mjs +439 -4
- package/bizar-dash/src/server/schedules-runner.mjs +4 -4
- package/bizar-dash/src/server/server.mjs +55 -37
- package/bizar-dash/src/server/watcher.mjs +2 -2
- package/bizar-dash/src/web/App.tsx +42 -6
- package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
- package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
- package/bizar-dash/src/web/components/Toast.tsx +1 -1
- package/bizar-dash/src/web/components/Topbar.tsx +3 -1
- package/bizar-dash/src/web/lib/api.ts +12 -11
- package/bizar-dash/src/web/lib/types.ts +16 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/styles/chat.css +2 -0
- package/bizar-dash/src/web/styles/main.css +52 -23
- package/bizar-dash/src/web/styles/memory.css +955 -0
- package/bizar-dash/src/web/styles/tasks.css +2 -0
- package/bizar-dash/src/web/views/Memory.tsx +141 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
- package/bizar-dash/src/web/views/Overview.tsx +7 -2
- package/bizar-dash/src/web/views/Settings.tsx +32 -1
- package/bizar-dash/src/web/views/Skills.tsx +3 -2
- package/bizar-dash/src/web/views/Tasks.tsx +4 -3
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
- package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +356 -0
- package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
- package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/headroom-install.test.mjs +173 -0
- package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
- package/bizar-dash/tests/headroom-status.test.mjs +117 -0
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
- package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
- package/bizar-dash/tests/memory-tab.test.mjs +322 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
- package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
- package/cli/artifact.mjs +11 -4
- package/cli/bin.mjs +211 -12
- package/cli/doctor.mjs +5 -3
- package/cli/memory.mjs +65 -21
- package/cli/provision.mjs +8 -2
- package/install.sh +1 -2
- package/package.json +3 -3
- package/bizar-dash/dist/assets/main-CDFKHzBg.css +0 -1
- package/bizar-dash/dist/assets/main-NYFpS2wY.js +0 -312
- package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|