@polderlabs/bizar 5.0.0 → 5.0.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 (97) hide show
  1. package/bizar-dash/dist/assets/{icons-CFqu2M-c.js → icons-Bo0iH9EC.js} +166 -121
  2. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
  4. package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
  5. package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
  6. package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
  7. package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
  8. package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js → mobile-DD-FZrTC.js} +1 -1
  9. package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js.map → mobile-DD-FZrTC.js.map} +1 -1
  10. package/bizar-dash/dist/index.html +4 -4
  11. package/bizar-dash/dist/mobile.html +3 -3
  12. package/bizar-dash/node_modules/.package-lock.json +6 -0
  13. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  14. package/bizar-dash/package-lock.json +6 -0
  15. package/bizar-dash/src/server/api.mjs +7 -0
  16. package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
  17. package/bizar-dash/src/server/memory-store.mjs +46 -0
  18. package/bizar-dash/src/server/providers-store.mjs +11 -4
  19. package/bizar-dash/src/server/routes/_shared.mjs +2 -2
  20. package/bizar-dash/src/server/routes/doctor.mjs +71 -0
  21. package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
  22. package/bizar-dash/src/server/routes/memory.mjs +3 -0
  23. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
  24. package/bizar-dash/src/server/routes/schedules.mjs +55 -0
  25. package/bizar-dash/src/server/serve-info.mjs +172 -0
  26. package/bizar-dash/src/web/App.tsx +48 -11
  27. package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
  28. package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
  29. package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
  30. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
  31. package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
  32. package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
  33. package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
  34. package/bizar-dash/src/web/components/Topbar.tsx +11 -0
  35. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
  36. package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
  37. package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
  38. package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
  39. package/bizar-dash/src/web/lib/types.ts +134 -0
  40. package/bizar-dash/src/web/styles/chat.css +8 -5
  41. package/bizar-dash/src/web/styles/main.css +77 -2
  42. package/bizar-dash/src/web/styles/settings.css +265 -0
  43. package/bizar-dash/src/web/views/Chat.tsx +15 -1
  44. package/bizar-dash/src/web/views/Doctor.tsx +317 -0
  45. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
  46. package/bizar-dash/src/web/views/Schedules.tsx +94 -12
  47. package/bizar-dash/src/web/views/Settings.tsx +76 -46
  48. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
  49. package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
  50. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
  51. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
  52. package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
  53. package/bizar-dash/tests/autosave.test.tsx +276 -0
  54. package/bizar-dash/tests/chat-composer.test.tsx +140 -0
  55. package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
  56. package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
  57. package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
  58. package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
  59. package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
  60. package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
  61. package/bizar-dash/tests/minimax-models.test.mjs +123 -0
  62. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
  63. package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
  64. package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
  65. package/bizar-dash/tests/settings-layout.test.tsx +129 -0
  66. package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
  67. package/bizar-dash/tests/settings-nav.test.tsx +126 -0
  68. package/cli/commands/dash.mjs +5 -1
  69. package/cli/commands/minimax.mjs +5 -2
  70. package/cli/tests/minimax-cli.test.mjs +79 -0
  71. package/config/agents/frigg.md +1 -1
  72. package/config/agents/heimdall.md +1 -1
  73. package/config/agents/mimir.md +1 -1
  74. package/config/agents/quick.md +1 -1
  75. package/config/agents/semble-search.md +1 -1
  76. package/config/agents/vor.md +1 -1
  77. package/config/opencode.json +28 -21
  78. package/config/opencode.json.template +8 -1
  79. package/package.json +1 -1
  80. package/plugins/bizar/index.ts +77 -0
  81. package/plugins/bizar/src/compaction.d.mts +48 -0
  82. package/plugins/bizar/src/compaction.mjs +192 -0
  83. package/plugins/bizar/tests/compaction.test.ts +264 -0
  84. package/templates/schedules/daily-backup.json +12 -0
  85. package/templates/schedules/daily-cleanup.json +12 -0
  86. package/templates/schedules/hourly-health-check.json +12 -0
  87. package/templates/schedules/webhook-on-push.json +13 -0
  88. package/templates/schedules/weekly-digest.json +13 -0
  89. package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +0 -1
  90. package/bizar-dash/dist/assets/main-Dl8yY5_H.js +0 -16
  91. package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +0 -1
  92. package/bizar-dash/dist/assets/main-ZAfGKENE.css +0 -1
  93. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +0 -2
  94. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +0 -1
  95. package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
  96. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
  97. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
@@ -1,14 +1,13 @@
1
- // src/web/views/memory/ConfigPanel.tsx — global memory config + per-system toggles.
1
+ // src/web/views/memory/ConfigPanel.tsx — simplified memory config.
2
2
  import { useEffect, useState } from 'react';
3
3
  import {
4
- Brain,
5
- Database,
6
- FileText,
7
4
  GitBranch,
5
+ GitCommitHorizontal,
6
+ Download,
7
+ Upload,
8
8
  Loader2,
9
9
  Plug,
10
10
  Save,
11
- Sparkles,
12
11
  } from 'lucide-react';
13
12
  import { Button } from '../../components/Button';
14
13
  import { Card, CardMeta, CardTitle } from '../../components/Card';
@@ -17,10 +16,19 @@ import { useToast } from '../../components/Toast';
17
16
  import { api } from '../../lib/api';
18
17
  import { cn } from '../../lib/utils';
19
18
 
19
+ type GitConfig = {
20
+ remoteUrl?: string;
21
+ };
22
+
20
23
  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
+ git?: GitConfig;
25
+ };
26
+
27
+ type SyncStatus = {
28
+ clean?: boolean;
29
+ branch?: string;
30
+ modified?: number;
31
+ untracked?: number;
24
32
  };
25
33
 
26
34
  type Props = { refreshKey: number };
@@ -30,6 +38,10 @@ export function ConfigPanel({ refreshKey }: Props) {
30
38
  const [cfg, setCfg] = useState<GlobalConfig | null>(null);
31
39
  const [loading, setLoading] = useState(true);
32
40
  const [saving, setSaving] = useState(false);
41
+ const [remoteUrl, setRemoteUrl] = useState('');
42
+ const [vaultPath] = useState<string>('');
43
+ const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
44
+ const [actionLoading, setActionLoading] = useState<string | null>(null);
33
45
  const [tests, setTests] = useState<Array<{ name: string; pass: boolean; detail: string }> | null>(null);
34
46
  const [testing, setTesting] = useState(false);
35
47
 
@@ -38,6 +50,9 @@ export function ConfigPanel({ refreshKey }: Props) {
38
50
  try {
39
51
  const r = await api.get<{ config: GlobalConfig }>('/memory/config/global');
40
52
  setCfg(r.config);
53
+ setRemoteUrl(r.config?.git?.remoteUrl || '');
54
+ // Also fetch git status from the per-project vault
55
+ await loadSyncStatus();
41
56
  } catch (err) {
42
57
  toast.error(`Config load failed: ${(err as Error).message}`);
43
58
  } finally {
@@ -45,20 +60,32 @@ export function ConfigPanel({ refreshKey }: Props) {
45
60
  }
46
61
  };
47
62
 
63
+ const loadSyncStatus = async () => {
64
+ try {
65
+ const r = await api.get<{ ok: boolean; clean?: boolean; branch?: string; modified?: number; untracked?: number }>('/memory/git/status');
66
+ if (r.ok) {
67
+ setSyncStatus({
68
+ clean: r.clean,
69
+ branch: r.branch,
70
+ modified: r.modified,
71
+ untracked: r.untracked,
72
+ });
73
+ }
74
+ } catch {
75
+ // git status is best-effort
76
+ }
77
+ };
78
+
48
79
  useEffect(() => {
49
80
  reload();
50
81
  // eslint-disable-next-line react-hooks/exhaustive-deps
51
82
  }, [refreshKey]);
52
83
 
53
- const update = (block: keyof GlobalConfig, key: string, value: unknown) => {
54
- setCfg((cur) => cur ? { ...cur, [block]: { ...(cur[block] || {}), [key]: value } } : cur);
55
- };
56
-
57
84
  const onSave = async () => {
58
85
  if (!cfg) return;
59
86
  setSaving(true);
60
87
  try {
61
- await api.put('/memory/config/global', cfg);
88
+ await api.put('/memory/config/global', { git: { remoteUrl } });
62
89
  toast.success('Config saved.');
63
90
  } catch (err) {
64
91
  toast.error(`Save failed: ${(err as Error).message}`);
@@ -67,6 +94,30 @@ export function ConfigPanel({ refreshKey }: Props) {
67
94
  }
68
95
  };
69
96
 
97
+ const runAction = async (action: 'pull' | 'push' | 'commit') => {
98
+ setActionLoading(action);
99
+ try {
100
+ if (action === 'pull') {
101
+ const r = await api.post<{ ok: boolean; output?: string }>('/memory/git/pull', {});
102
+ if (r.ok) toast.success('Pull successful.');
103
+ else toast.error(`Pull failed: ${r.output || 'unknown error'}`);
104
+ } else if (action === 'push') {
105
+ const r = await api.post<{ ok: boolean }>('/memory/git/push', {});
106
+ if (r.ok) toast.success('Push successful.');
107
+ else toast.error('Push failed.');
108
+ } else if (action === 'commit') {
109
+ const r = await api.post<{ ok: boolean; message?: string }>('/memory/git/commit', {});
110
+ if (r.ok) toast.success(`Committed: ${r.message}`);
111
+ else toast.error('Commit failed.');
112
+ }
113
+ await loadSyncStatus();
114
+ } catch (err) {
115
+ toast.error(`${action} failed: ${(err as Error).message}`);
116
+ } finally {
117
+ setActionLoading(null);
118
+ }
119
+ };
120
+
70
121
  const onTest = async () => {
71
122
  setTesting(true);
72
123
  try {
@@ -94,144 +145,75 @@ export function ConfigPanel({ refreshKey }: Props) {
94
145
 
95
146
  return (
96
147
  <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 ───────────────────────────────────────────────────── */}
148
+ {/* ── Git sync ───────────────────────────────────────────────────── */}
170
149
  <Card>
171
150
  <CardTitle><GitBranch size={14} /> Git sync</CardTitle>
172
- <CardMeta>Repo path, remote, branch, and auto-sync toggle.</CardMeta>
151
+ <CardMeta>Configure the git remote and sync your memory vault.</CardMeta>
173
152
  <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
- />
153
+ <Row label="Vault path" inline>
154
+ <span className="memory-vault-path mono muted">{vaultPath || '(loading…)'}</span>
182
155
  </Row>
183
156
  <Row label="Remote URL">
184
157
  <input
185
158
  type="text"
186
159
  className="input mono"
187
- value={cfg.git?.remoteUrl || ''}
188
- onChange={(e) => update('git', 'remoteUrl', e.target.value)}
160
+ value={remoteUrl}
161
+ onChange={(e) => setRemoteUrl(e.target.value)}
189
162
  placeholder="git@github.com:org/repo.git"
190
163
  />
191
164
  </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>
165
+ {syncStatus && (
166
+ <Row label="Status" inline>
167
+ <span className={cn('memory-pill', syncStatus.clean ? 'memory-pill-ok' : 'memory-pill-warn')}>
168
+ {syncStatus.clean ? 'clean' : 'dirty'}
169
+ </span>
170
+ {syncStatus.branch && (
171
+ <span className="muted" style={{ marginLeft: 8 }}>
172
+ <GitBranch size={12} style={{ display: 'inline' }} /> {syncStatus.branch}
173
+ </span>
174
+ )}
175
+ {!syncStatus.clean && syncStatus.modified !== undefined && (
176
+ <span className="muted" style={{ marginLeft: 8 }}>
177
+ {syncStatus.modified} modified, {syncStatus.untracked} untracked
178
+ </span>
179
+ )}
180
+ </Row>
181
+ )}
211
182
  </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.
183
+ <div className="memory-action-row" style={{ marginTop: 12 }}>
184
+ <Button
185
+ variant="secondary"
186
+ onClick={() => runAction('pull')}
187
+ disabled={actionLoading !== null}
188
+ >
189
+ {actionLoading === 'pull' ? <Loader2 size={12} className="memory-spin" /> : <Download size={12} />}
190
+ Pull
191
+ </Button>
192
+ <Button
193
+ variant="secondary"
194
+ onClick={() => runAction('commit')}
195
+ disabled={actionLoading !== null}
196
+ >
197
+ {actionLoading === 'commit' ? <Loader2 size={12} className="memory-spin" /> : <GitCommitHorizontal size={12} />}
198
+ Commit
199
+ </Button>
200
+ <Button
201
+ variant="secondary"
202
+ onClick={() => runAction('push')}
203
+ disabled={actionLoading !== null}
204
+ >
205
+ {actionLoading === 'push' ? <Loader2 size={12} className="memory-spin" /> : <Upload size={12} />}
206
+ Push
207
+ </Button>
226
208
  </div>
227
209
  </Card>
228
210
 
229
- {/* ── Test all connections ─────────────────────────────────── */}
211
+ {/* ── Connection tests ─────────────────────────────────────────── */}
230
212
  <Card>
231
213
  <CardTitle>
232
214
  <Plug size={14} /> Connection tests
233
215
  </CardTitle>
234
- <CardMeta>Verify the configured git repo, remote, and path.</CardMeta>
216
+ <CardMeta>Verify the configured git remote is reachable.</CardMeta>
235
217
  <div className="memory-action-row">
236
218
  <Button variant="secondary" onClick={onTest} disabled={testing}>
237
219
  {testing ? <Loader2 size={12} className="memory-spin" /> : <Plug size={12} />}
@@ -292,4 +274,3 @@ function Row({
292
274
  </label>
293
275
  );
294
276
  }
295
-
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
3
3
  import { Server as ServerIcon, Globe, Save } from 'lucide-react';
4
4
  import { Card, CardTitle, CardMeta } from '../../components/Card';
5
5
  import { Button } from '../../components/Button';
6
+ import { AutosaveField } from '../../components/AutosaveField';
6
7
  import { useToast } from '../../components/Toast';
7
8
  import { api } from '../../lib/api';
8
9
  import type { Settings } from '../../lib/types';
@@ -11,9 +12,11 @@ type Props = {
11
12
  settings: Settings;
12
13
  patchAgents: (patch: Partial<Settings['agents']>) => void;
13
14
  patchDashboard: (patch: Partial<Settings['dashboard']>) => void;
15
+ /** Called after state is patched; parent debounces the API save */
16
+ autoSave?: (key: keyof Settings, value: Settings[keyof Settings]) => void;
14
17
  };
15
18
 
16
- export function AgentSection({ settings, patchAgents, patchDashboard }: Props) {
19
+ export function AgentSection({ settings, patchAgents, patchDashboard, autoSave }: Props) {
17
20
  const toast = useToast();
18
21
  const [pluginOptions, setPluginOptions] = useState<Record<string, number>>({});
19
22
 
@@ -139,19 +142,27 @@ export function AgentSection({ settings, patchAgents, patchDashboard }: Props) {
139
142
  <label className="field-label" htmlFor="set-allowed-roots">
140
143
  Additional allowed roots <span className="muted">(advanced)</span>
141
144
  </label>
142
- <textarea
143
- id="set-allowed-roots"
144
- className="textarea"
145
- rows={4}
146
- placeholder="/workspace&#10;/srv/projects"
147
- value={(settings.dashboard.allowedRoots ?? []).join('\n')}
148
- onChange={(e) => {
149
- const lines = e.target.value
150
- .split('\n')
151
- .map((l) => l.trim())
152
- .filter(Boolean);
145
+ {/* key= forces re-mount when allowedRoots changes externally (e.g. reset/reload) */}
146
+ <AutosaveField
147
+ key={settings.dashboard.allowedRoots?.join('\n')}
148
+ initialValue={(settings.dashboard.allowedRoots ?? []).join('\n')}
149
+ delay={1500}
150
+ saveFn={async (v) => {
151
+ const lines = v.split('\n').map((l) => l.trim()).filter(Boolean);
153
152
  patchDashboard({ allowedRoots: lines });
153
+ autoSave?.('dashboard', { allowedRoots: lines } as Settings['dashboard']);
154
154
  }}
155
+ render={({ value, onChange, onBlur }) => (
156
+ <textarea
157
+ id="set-allowed-roots"
158
+ className="textarea"
159
+ rows={4}
160
+ placeholder="/workspace&#10;/srv/projects"
161
+ value={value}
162
+ onChange={e => onChange(e.target.value)}
163
+ onBlur={onBlur}
164
+ />
165
+ )}
155
166
  />
156
167
  <p className="field-help">
157
168
  Optional. Add filesystem roots beyond your home directory that the file
@@ -1,16 +1,15 @@
1
1
  // src/web/views/settings/EnvVarsSection.tsx
2
- // Placeholder — EnvVarManager integration to be added in a future PR.
3
2
  import React from 'react';
3
+ import { KeyRound } from 'lucide-react';
4
4
  import { Card, CardTitle, CardMeta } from '../../components/Card';
5
+ import { EnvVarManager } from '../../components/EnvVarManager';
5
6
 
6
7
  export function EnvVarsSection() {
7
8
  return (
8
9
  <Card id="settings-env-vars" data-section="env-vars">
9
- <CardTitle>Environment Variables</CardTitle>
10
- <CardMeta>Configure environment variables for the Bizar runtime.</CardMeta>
11
- <p className="muted" style={{ fontSize: 12 }}>
12
- Environment variable management is coming soon.
13
- </p>
10
+ <CardTitle><KeyRound size={14} /> Environment Variables</CardTitle>
11
+ <CardMeta>Manage BIZAR_* env vars (API keys, secrets). Values are stored in <code>~/.config/bizar/env.json</code> (mode 0600).</CardMeta>
12
+ <EnvVarManager />
14
13
  </Card>
15
14
  );
16
15
  }
@@ -2,6 +2,7 @@
2
2
  import React from 'react';
3
3
  import { Layout as LayoutIcon } from 'lucide-react';
4
4
  import { Card, CardTitle, CardMeta } from '../../components/Card';
5
+ import { AutosaveField } from '../../components/AutosaveField';
5
6
  import { cn } from '../../lib/utils';
6
7
  import type { Settings } from '../../lib/types';
7
8
 
@@ -9,6 +10,8 @@ type Props = {
9
10
  settings: Settings;
10
11
  patchUi: (patch: Partial<Settings['ui']>) => void;
11
12
  patchTop: <K extends keyof Settings>(key: K, value: Settings[K]) => void;
13
+ /** Called by AutosaveField after state is patched; the parent debounces the API save */
14
+ autoSave?: (key: keyof Settings, value: Settings[keyof Settings]) => void;
12
15
  };
13
16
 
14
17
  const LAYOUTS = [
@@ -17,7 +20,7 @@ const LAYOUTS = [
17
20
  { id: 'both', label: 'Both' },
18
21
  ] as const;
19
22
 
20
- export function GeneralSection({ settings, patchUi, patchTop }: Props) {
23
+ export function GeneralSection({ settings, patchUi, patchTop, autoSave }: Props) {
21
24
  return (
22
25
  <>
23
26
  {/* UI Layout */}
@@ -79,24 +82,44 @@ export function GeneralSection({ settings, patchUi, patchTop }: Props) {
79
82
  <CardMeta>Default agent + model override.</CardMeta>
80
83
  <div className="field" data-setting-id="defaultAgent">
81
84
  <label className="field-label" htmlFor="set-default-agent">Default agent</label>
82
- <input
83
- id="set-default-agent"
84
- className="input"
85
- type="text"
86
- placeholder="e.g. odin"
87
- value={settings.defaultAgent || ''}
88
- onChange={(e) => patchTop('defaultAgent', e.target.value)}
85
+ <AutosaveField
86
+ initialValue={settings.defaultAgent || ''}
87
+ saveFn={async (v) => {
88
+ patchTop('defaultAgent', v);
89
+ autoSave?.('defaultAgent', v);
90
+ }}
91
+ render={({ value, onChange, onBlur }) => (
92
+ <input
93
+ id="set-default-agent"
94
+ className="input"
95
+ type="text"
96
+ placeholder="e.g. odin"
97
+ value={value}
98
+ onChange={e => onChange(e.target.value)}
99
+ onBlur={onBlur}
100
+ />
101
+ )}
89
102
  />
90
103
  </div>
91
104
  <div className="field" data-setting-id="defaultModel">
92
105
  <label className="field-label" htmlFor="set-default-model">Model override</label>
93
- <input
94
- id="set-default-model"
95
- className="input"
96
- type="text"
97
- placeholder="(leave empty for provider default)"
98
- value={settings.defaultModel || ''}
99
- onChange={(e) => patchTop('defaultModel', e.target.value)}
106
+ <AutosaveField
107
+ initialValue={settings.defaultModel || ''}
108
+ saveFn={async (v) => {
109
+ patchTop('defaultModel', v);
110
+ autoSave?.('defaultModel', v);
111
+ }}
112
+ render={({ value, onChange, onBlur }) => (
113
+ <input
114
+ id="set-default-model"
115
+ className="input"
116
+ type="text"
117
+ placeholder="(leave empty for provider default)"
118
+ value={value}
119
+ onChange={e => onChange(e.target.value)}
120
+ onBlur={onBlur}
121
+ />
122
+ )}
100
123
  />
101
124
  </div>
102
125
  </Card>
@@ -1,16 +1,100 @@
1
- // src/web/views/settings/MemorySection.tsx
2
- // Placeholder LightRAG / Obsidian / git memory config to be added in a future PR.
3
- import React from 'react';
1
+ // src/web/views/settings/MemorySection.tsx — memory vault + git config for Settings.
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Brain, GitBranch, Loader2, Save } from 'lucide-react';
4
+ import { Button } from '../../components/Button';
4
5
  import { Card, CardTitle, CardMeta } from '../../components/Card';
6
+ import { Spinner } from '../../components/Spinner';
7
+ import { useToast } from '../../components/Toast';
8
+ import { api } from '../../lib/api';
9
+
10
+ type GitConfig = {
11
+ remoteUrl?: string;
12
+ };
13
+
14
+ type GlobalConfig = {
15
+ git?: GitConfig;
16
+ };
5
17
 
6
18
  export function MemorySection() {
19
+ const toast = useToast();
20
+ const [loading, setLoading] = useState(true);
21
+ const [saving, setSaving] = useState(false);
22
+ const [remoteUrl, setRemoteUrl] = useState('');
23
+ const [vaultPath] = useState<string>(''); // server-side default, not user-editable
24
+ const [refreshKey, setRefreshKey] = useState(0);
25
+
26
+ useEffect(() => {
27
+ api.get<{ config: GlobalConfig; path?: string }>('/memory/config/global')
28
+ .then((r) => {
29
+ setRemoteUrl(r.config?.git?.remoteUrl || '');
30
+ setLoading(false);
31
+ })
32
+ .catch(() => setLoading(false));
33
+ }, [refreshKey]);
34
+
35
+ const onSave = async () => {
36
+ setSaving(true);
37
+ try {
38
+ await api.put('/memory/config/global', { git: { remoteUrl } });
39
+ toast.success('Memory config saved.');
40
+ setRefreshKey((k) => k + 1);
41
+ } catch (err) {
42
+ toast.error(`Save failed: ${(err as Error).message}`);
43
+ } finally {
44
+ setSaving(false);
45
+ }
46
+ };
47
+
7
48
  return (
8
49
  <Card id="settings-memory" data-section="memory">
9
- <CardTitle>Memory</CardTitle>
10
- <CardMeta>Configure LightRAG, Obsidian vault, and git integration.</CardMeta>
11
- <p className="muted" style={{ fontSize: 12 }}>
12
- Memory configuration is coming soon.
13
- </p>
50
+ <CardTitle><Brain size={14} /> Memory vault</CardTitle>
51
+ <CardMeta>
52
+ The vault lives at a default location on disk. Only the git remote URL
53
+ needs to be configured to enable sync across machines.
54
+ </CardMeta>
55
+
56
+ {loading ? (
57
+ <div className="flex-center" style={{ padding: 24 }}>
58
+ <Spinner size="sm" />
59
+ </div>
60
+ ) : (
61
+ <div className="settings-fields">
62
+ <div className="field">
63
+ <label className="field-label">Vault path</label>
64
+ <div className="field-readonly mono muted" style={{ fontSize: 12, padding: '6px 0' }}>
65
+ {vaultPath || '~/.local/share/bizar/memory'}
66
+ </div>
67
+ </div>
68
+
69
+ <div className="field">
70
+ <label className="field-label" htmlFor="set-memory-git-remote">
71
+ <GitBranch size={12} style={{ display: 'inline', marginRight: 4 }} />
72
+ Git remote URL
73
+ </label>
74
+ <input
75
+ id="set-memory-git-remote"
76
+ className="input mono"
77
+ type="text"
78
+ value={remoteUrl}
79
+ onChange={(e) => setRemoteUrl(e.target.value)}
80
+ placeholder="git@github.com:org/repo.git"
81
+ />
82
+ <span className="field-hint">Leave empty to use local-only mode (no git sync).</span>
83
+ </div>
84
+
85
+ <div style={{ marginTop: 12 }}>
86
+ <Button
87
+ variant="primary"
88
+ size="sm"
89
+ onClick={onSave}
90
+ disabled={saving}
91
+ >
92
+ {saving ? <Loader2 size={12} className="memory-spin" /> : <Save size={12} />}
93
+ Save memory config
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ )}
14
98
  </Card>
15
99
  );
16
100
  }