@polderlabs/bizar 4.4.13 → 4.5.1

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 (118) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
  3. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
  4. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
  5. package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
  6. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
  8. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
  9. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
  10. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
  11. package/bizar-dash/dist/index.html +3 -3
  12. package/bizar-dash/dist/mobile.html +2 -2
  13. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  14. package/bizar-dash/skills/bizar/SKILL.md +116 -0
  15. package/bizar-dash/skills/chat/SKILL.md +74 -0
  16. package/bizar-dash/skills/headroom/SKILL.md +94 -0
  17. package/bizar-dash/skills/lightrag/SKILL.md +86 -0
  18. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  19. package/bizar-dash/skills/obsidian/SKILL.md +68 -0
  20. package/bizar-dash/skills/providers/SKILL.md +75 -0
  21. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  22. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  23. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  24. package/bizar-dash/skills/usage/SKILL.md +62 -0
  25. package/bizar-dash/src/server/api.mjs +18 -0
  26. package/bizar-dash/src/server/headroom.mjs +645 -0
  27. package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
  28. package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
  29. package/bizar-dash/src/server/memory-store.mjs +189 -0
  30. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  31. package/bizar-dash/src/server/minimax.mjs +196 -5
  32. package/bizar-dash/src/server/providers-store.mjs +956 -0
  33. package/bizar-dash/src/server/routes/_shared.mjs +17 -0
  34. package/bizar-dash/src/server/routes/config.mjs +52 -1
  35. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  36. package/bizar-dash/src/server/routes/headroom.mjs +126 -0
  37. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +668 -1
  39. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  40. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  41. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  42. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  43. package/bizar-dash/src/server/routes/update.mjs +340 -0
  44. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  45. package/bizar-dash/src/server/serve-info.mjs +135 -4
  46. package/bizar-dash/src/server/server.mjs +20 -0
  47. package/bizar-dash/src/server/skills-store.mjs +152 -262
  48. package/bizar-dash/src/web/App.tsx +120 -29
  49. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  50. package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
  51. package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
  52. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  53. package/bizar-dash/src/web/components/Topbar.tsx +2 -1
  54. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  55. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  56. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  57. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  58. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  59. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  60. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  61. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  62. package/bizar-dash/src/web/lib/api.ts +43 -0
  63. package/bizar-dash/src/web/lib/types.ts +16 -0
  64. package/bizar-dash/src/web/main.tsx +2 -0
  65. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  66. package/bizar-dash/src/web/styles/chat.css +135 -1
  67. package/bizar-dash/src/web/styles/main.css +46 -0
  68. package/bizar-dash/src/web/styles/memory.css +955 -0
  69. package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
  70. package/bizar-dash/src/web/styles/settings.css +418 -0
  71. package/bizar-dash/src/web/styles/skills.css +302 -0
  72. package/bizar-dash/src/web/styles/tasks.css +288 -0
  73. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  74. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  75. package/bizar-dash/src/web/views/Memory.tsx +140 -0
  76. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
  77. package/bizar-dash/src/web/views/Overview.tsx +3 -0
  78. package/bizar-dash/src/web/views/Settings.tsx +36 -0
  79. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  80. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
  82. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
  83. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
  84. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
  85. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
  86. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
  87. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
  88. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  89. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  90. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  91. package/bizar-dash/tests/headroom-install.test.mjs +173 -0
  92. package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
  93. package/bizar-dash/tests/headroom-status.test.mjs +117 -0
  94. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  95. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
  96. package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
  97. package/bizar-dash/tests/memory-tab.test.mjs +322 -0
  98. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  99. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  100. package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
  101. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  102. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  103. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  104. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  105. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  106. package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
  107. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  108. package/bizar-dash/tests/update-check.test.mjs +127 -0
  109. package/bizar-dash/tests/update-run.test.mjs +266 -0
  110. package/cli/bin.mjs +247 -1
  111. package/cli/provision.mjs +118 -4
  112. package/config/agents/_shared/SKILLS.md +109 -0
  113. package/package.json +1 -1
  114. package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
  115. package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
  116. package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
  117. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  118. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -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
+
@@ -0,0 +1,220 @@
1
+ // src/web/views/memory/GitSyncPanel.tsx — git status, pull/push/commit/fetch, diff viewer.
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ ArrowDown,
5
+ ArrowUp,
6
+ CheckCircle2,
7
+ GitBranch,
8
+ GitCommit,
9
+ GitMerge,
10
+ GitPullRequest,
11
+ Loader2,
12
+ RefreshCw,
13
+ Settings,
14
+ } from 'lucide-react';
15
+ import { Button } from '../../components/Button';
16
+ import { Card, CardMeta, CardTitle } from '../../components/Card';
17
+ import { Spinner } from '../../components/Spinner';
18
+ import { useToast } from '../../components/Toast';
19
+ import { api } from '../../lib/api';
20
+ import { formatTime, cn } from '../../lib/utils';
21
+
22
+ type GitStatus = {
23
+ ok: boolean;
24
+ clean?: boolean;
25
+ branch?: string;
26
+ ahead?: number;
27
+ behind?: number;
28
+ modified?: string[];
29
+ untracked?: string[];
30
+ mode?: string;
31
+ };
32
+
33
+ type GitDiff = {
34
+ hasDiff: boolean;
35
+ lines: string[];
36
+ files: string[];
37
+ mode: string;
38
+ };
39
+
40
+ type Props = { refreshKey: number };
41
+
42
+ export function GitSyncPanel({ refreshKey }: Props) {
43
+ const toast = useToast();
44
+ const [status, setStatus] = useState<GitStatus | null>(null);
45
+ const [diff, setDiff] = useState<GitDiff | null>(null);
46
+ const [loading, setLoading] = useState(true);
47
+ const [busy, setBusy] = useState<string | null>(null);
48
+ const [config, setConfig] = useState<{ repoPath?: string; remoteUrl?: string; branch?: string; autoSync?: boolean } | null>(null);
49
+
50
+ const reload = async () => {
51
+ setLoading(true);
52
+ try {
53
+ const [s, d, g] = await Promise.all([
54
+ api.get<GitStatus>('/memory/git/status').catch(() => null),
55
+ api.get<GitDiff>('/memory/git/diff').catch(() => null),
56
+ api.get<{ config?: { git?: { repoPath?: string; remoteUrl?: string; branch?: string; autoSync?: boolean } } }>(
57
+ '/memory/config/global',
58
+ ).catch(() => null),
59
+ ]);
60
+ setStatus(s);
61
+ setDiff(d);
62
+ setConfig(g?.config?.git || null);
63
+ } catch (err) {
64
+ toast.error(`Git status failed: ${(err as Error).message}`);
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 run = async (key: string, fn: () => Promise<unknown>, msg: string) => {
76
+ setBusy(key);
77
+ try {
78
+ await fn();
79
+ toast.success(msg);
80
+ await reload();
81
+ } catch (err) {
82
+ toast.error(`${key} failed: ${(err as Error).message}`);
83
+ } finally {
84
+ setBusy(null);
85
+ }
86
+ };
87
+
88
+ const onPull = () => run('pull', () => api.post('/memory/git/pull', {}), 'Pull complete.');
89
+ const onPush = () => run('push', () => api.post('/memory/git/push', {}), 'Push complete.');
90
+ const onCommit = () => {
91
+ const message = prompt('Commit message:', `[memory-sync] ${new Date().toISOString().slice(0, 10)} vault sync`);
92
+ if (!message) return;
93
+ run('commit', () => api.post('/memory/git/commit', { message }), 'Committed.');
94
+ };
95
+ const onFetch = () => run('fetch', () => api.post('/memory/git/sync', { push: false }), 'Fetch complete.');
96
+
97
+ if (loading && !status) {
98
+ return (
99
+ <div className="view-loading">
100
+ <Spinner size="lg" />
101
+ <p>Loading git status…</p>
102
+ </div>
103
+ );
104
+ }
105
+ if (!status) {
106
+ return <div className="muted">No git data available.</div>;
107
+ }
108
+
109
+ const isLocal = status.mode === 'local-only';
110
+
111
+ return (
112
+ <div className="memory-panel-content">
113
+ {/* ── Status card ───────────────────────────────────────────── */}
114
+ <Card variant="elevated">
115
+ <CardTitle>
116
+ <GitBranch size={14} /> Git sync
117
+ {isLocal ? (
118
+ <span className="memory-source-pill-status na">local-only</span>
119
+ ) : (
120
+ <span className={cn('memory-source-pill-status', status.clean ? 'on' : 'warn')}>
121
+ {status.clean ? 'clean' : 'dirty'}
122
+ </span>
123
+ )}
124
+ </CardTitle>
125
+ <CardMeta>
126
+ {isLocal
127
+ ? 'Memory is in local-only mode. Configure a shared repo in Settings → Memory to enable git sync.'
128
+ : <>Branch <code>{status.branch || '—'}</code>{status.ahead ? <> · ahead {status.ahead}</> : null}{status.behind ? <> · behind {status.behind}</> : null}</>}
129
+ </CardMeta>
130
+
131
+ {!isLocal && (
132
+ <div className="memory-git-status-row">
133
+ <Pill ok={status.clean ?? false} label="Working tree" />
134
+ <Pill ok={(status.ahead ?? 0) === 0} label={`Ahead: ${status.ahead ?? 0}`} />
135
+ <Pill ok={(status.behind ?? 0) === 0} label={`Behind: ${status.behind ?? 0}`} />
136
+ <Pill ok={(status.modified?.length ?? 0) === 0} label={`Modified: ${status.modified?.length ?? 0}`} />
137
+ <Pill ok={(status.untracked?.length ?? 0) === 0} label={`Untracked: ${status.untracked?.length ?? 0}`} />
138
+ </div>
139
+ )}
140
+
141
+ <div className="memory-action-row">
142
+ <Button variant="secondary" size="sm" onClick={onPull} disabled={!!busy || isLocal}>
143
+ {busy === 'pull' ? <Loader2 size={12} className="memory-spin" /> : <ArrowDown size={12} />}
144
+ Pull
145
+ </Button>
146
+ <Button variant="secondary" size="sm" onClick={onPush} disabled={!!busy || isLocal}>
147
+ {busy === 'push' ? <Loader2 size={12} className="memory-spin" /> : <ArrowUp size={12} />}
148
+ Push
149
+ </Button>
150
+ <Button variant="secondary" size="sm" onClick={onCommit} disabled={!!busy || isLocal}>
151
+ {busy === 'commit' ? <Loader2 size={12} className="memory-spin" /> : <GitCommit size={12} />}
152
+ Commit
153
+ </Button>
154
+ <Button variant="ghost" size="sm" onClick={onFetch} disabled={!!busy || isLocal}>
155
+ {busy === 'fetch' ? <Loader2 size={12} className="memory-spin" /> : <GitPullRequest size={12} />}
156
+ Fetch + sync
157
+ </Button>
158
+ <Button variant="ghost" size="sm" onClick={reload} disabled={!!busy}>
159
+ <RefreshCw size={12} /> Refresh
160
+ </Button>
161
+ </div>
162
+ </Card>
163
+
164
+ {/* ── Repo config snapshot ──────────────────────────────────── */}
165
+ {config && (
166
+ <Card>
167
+ <CardTitle>
168
+ <Settings size={14} /> Repository
169
+ </CardTitle>
170
+ <CardMeta>Snapshot of <code>~/.config/bizar/memory-config.json</code> → git block.</CardMeta>
171
+ <dl className="memory-config-row">
172
+ <dt>Path</dt>
173
+ <dd className="mono ellipsis" title={config.repoPath}>{config.repoPath || '—'}</dd>
174
+ <dt>Remote</dt>
175
+ <dd className="mono ellipsis" title={config.remoteUrl}>{config.remoteUrl || '—'}</dd>
176
+ <dt>Branch</dt>
177
+ <dd><code>{config.branch || 'main'}</code></dd>
178
+ <dt>Auto-sync</dt>
179
+ <dd>{config.autoSync ? 'enabled' : 'disabled'}</dd>
180
+ </dl>
181
+ </Card>
182
+ )}
183
+
184
+ {/* ── Working-tree diff ─────────────────────────────────────── */}
185
+ <Card>
186
+ <CardTitle>
187
+ <GitMerge size={14} /> Working-tree diff
188
+ </CardTitle>
189
+ <CardMeta>
190
+ {diff?.hasDiff
191
+ ? `${diff.files.length} file(s) changed`
192
+ : 'no working-tree changes'}
193
+ </CardMeta>
194
+ {diff && diff.files.length > 0 && (
195
+ <ul className="memory-diff-files">
196
+ {diff.files.map((f) => (
197
+ <li key={f}>
198
+ <code>{f}</code>
199
+ </li>
200
+ ))}
201
+ </ul>
202
+ )}
203
+ {diff && diff.lines.length > 1 && (
204
+ <pre className="memory-log-tail mono text-xs">
205
+ {diff.lines.slice(0, 400).join('\n')}
206
+ </pre>
207
+ )}
208
+ </Card>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ function Pill({ ok, label }: { ok: boolean; label: string }) {
214
+ return (
215
+ <span className={cn('memory-git-status-pill', ok ? 'ok' : 'warn')}>
216
+ {ok ? <CheckCircle2 size={11} /> : <Loader2 size={11} className="memory-spin" />}
217
+ {label}
218
+ </span>
219
+ );
220
+ }