@polderlabs/bizar 5.5.0 → 5.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 (71) hide show
  1. package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js → EnvVarsSection-DoIzqrlb.js} +3 -3
  2. package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js.map → EnvVarsSection-DoIzqrlb.js.map} +1 -1
  3. package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js → MobileChat-CoUCUsbr.js} +1 -1
  4. package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js.map → MobileChat-CoUCUsbr.js.map} +1 -1
  5. package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +1 -0
  6. package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js.map +1 -0
  7. package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js → Toast-D9nv5N6m.js} +1 -1
  8. package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js.map → Toast-D9nv5N6m.js.map} +1 -1
  9. package/bizar-dash/dist/assets/{icons-BsWXWkEF.js → icons-Btipv2pp.js} +100 -90
  10. package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +1 -0
  11. package/bizar-dash/dist/assets/main-DHXPuddY.js +18 -0
  12. package/bizar-dash/dist/assets/main-DHXPuddY.js.map +1 -0
  13. package/bizar-dash/dist/assets/{main-BklYy02e.css → main-UG4jKK-F.css} +1 -1
  14. package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +1 -0
  15. package/bizar-dash/dist/assets/{mobile-CsaN4CxH.js.map → mobile-H5KH9cTL.js.map} +1 -1
  16. package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js → mobile-layout-BqHxVb6_.js} +2 -2
  17. package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js.map → mobile-layout-BqHxVb6_.js.map} +1 -1
  18. package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js → useSlashCommands-C24-O1e7.js} +2 -2
  19. package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js.map → useSlashCommands-C24-O1e7.js.map} +1 -1
  20. package/bizar-dash/dist/index.html +7 -7
  21. package/bizar-dash/dist/mobile.html +3 -3
  22. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  23. package/bizar-dash/src/server/bg-spawner.mjs +522 -305
  24. package/bizar-dash/src/server/headroom.mjs +3 -1
  25. package/bizar-dash/src/server/memory-lightrag.mjs +26 -2
  26. package/bizar-dash/src/server/memory-store.mjs +74 -28
  27. package/bizar-dash/src/server/opencode-sdk.mjs +63 -3
  28. package/bizar-dash/src/server/plugins/registry.mjs +1 -1
  29. package/bizar-dash/src/server/routes/activity.mjs +85 -0
  30. package/bizar-dash/src/server/routes/background.mjs +31 -13
  31. package/bizar-dash/src/server/routes/memory.mjs +91 -38
  32. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +1 -1
  33. package/bizar-dash/src/server/server.mjs +4 -1
  34. package/bizar-dash/src/web/components/Sidebar.tsx +0 -1
  35. package/bizar-dash/src/web/styles/main.css +52 -13
  36. package/bizar-dash/src/web/styles/memory.css +17 -0
  37. package/bizar-dash/src/web/styles/settings.css +6 -56
  38. package/bizar-dash/src/web/views/Marketplace.tsx +192 -31
  39. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +157 -7
  40. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +11 -0
  41. package/bizar-dash/src/web/views/settings/MemorySection.tsx +65 -11
  42. package/bizar-dash/tests/background-sdk-session.test.mjs +94 -0
  43. package/bizar-dash/tests/background-session-events.test.mjs +74 -0
  44. package/bizar-dash/tests/background-steer-sdk.test.mjs +71 -0
  45. package/bizar-dash/tests/background-steer.test.mjs +25 -7
  46. package/bizar-dash/tests/memory-namespace.test.mjs +2 -2
  47. package/bizar-dash/tests/memory-status-root.test.mjs +144 -0
  48. package/bizar-dash/tests/memory-store.test.mjs +5 -4
  49. package/bizar-dash/tests/memory-vault-config-endpoint.test.mjs +173 -0
  50. package/bizar-dash/tests/settings-mode-wiring.test.tsx +1 -1
  51. package/bizar-dash/tests/settings-nav.test.tsx +0 -1
  52. package/bizar-dash/tests/views/Marketplace.test.tsx +200 -0
  53. package/bizar-dash/tests/views/Memory.test.tsx +274 -0
  54. package/bizar-dash/tests/views/sidebar.test.tsx +116 -0
  55. package/bizar-dash/tests/views/topbar.test.tsx +87 -0
  56. package/cli/commands/marketplace.mjs +1 -1
  57. package/cli/memory.mjs +1 -1
  58. package/package.json +1 -1
  59. package/plugins/bizar/src/background-state.ts +11 -0
  60. package/plugins/bizar/src/background.ts +212 -34
  61. package/plugins/bizar/src/opencode-runner.ts +80 -353
  62. package/plugins/bizar/src/tools/bg-send-message.ts +160 -61
  63. package/plugins/bizar/src/tools/bg-spawn.ts +235 -156
  64. package/plugins/bizar/tests/tools/bg-send-message.test.ts +84 -11
  65. package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +223 -0
  66. package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +0 -1
  67. package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js.map +0 -1
  68. package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +0 -1
  69. package/bizar-dash/dist/assets/main-CBTgYSeG.js +0 -18
  70. package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +0 -1
  71. package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +0 -1
@@ -1,16 +1,30 @@
1
1
  // src/web/views/Marketplace.tsx — browse and install plugins from the marketplace registry.
2
-
3
- import { useEffect, useState } from 'react';
4
- import { Store } from 'lucide-react';
2
+ // v6.x — Better UX: explicit loading + registry URL, refresh button, "showing
3
+ // cached" notice when the disk cache is the source, and a friendlier empty
4
+ // state when the registry returns zero plugins. The category filter chips
5
+ // are kept (they were already there but unverified).
6
+ import { useCallback, useEffect, useState } from 'react';
7
+ import { Store, RefreshCw, AlertTriangle, Wifi, Globe, Clock } from 'lucide-react';
5
8
  import { MarketplacePluginCard, type MarketplacePlugin } from '../components/MarketplacePluginCard';
6
9
  import { InstallConfirmDialog } from '../components/InstallConfirmDialog';
7
10
  import { EmptyState } from '../components/EmptyState';
8
11
  import { Spinner } from '../components/Spinner';
12
+ import { Button } from '../components/Button';
9
13
  import { useToast } from '../components/Toast';
10
- import { api } from '../lib/api';
14
+ import { api, ApiError } from '../lib/api';
11
15
 
12
16
  type RegistryResponse = {
13
17
  plugins: MarketplacePlugin[];
18
+ registry?: {
19
+ source?: string | null;
20
+ updatedAt?: string | null;
21
+ };
22
+ /** v6.x — Server indicates when this response came from the on-disk cache. */
23
+ cached?: boolean;
24
+ /** v6.x — When the on-disk cache was last refreshed (epoch ms). */
25
+ cacheTimestamp?: number | null;
26
+ /** v6.x — How many registry URLs were tried. */
27
+ urlsTried?: number;
14
28
  };
15
29
 
16
30
  export function Marketplace() {
@@ -21,14 +35,33 @@ export function Marketplace() {
21
35
  const [installing, setInstalling] = useState<MarketplacePlugin | null>(null);
22
36
  const [selected, setSelected] = useState<MarketplacePlugin | null>(null);
23
37
  const [loading, setLoading] = useState(true);
38
+ const [refreshing, setRefreshing] = useState(false);
39
+ const [error, setError] = useState<{ message: string; url?: string } | null>(null);
24
40
 
25
- useEffect(() => {
26
- api.get<RegistryResponse>('/plugins/registry')
27
- .then((r) => setRegistry(r))
28
- .catch((err) => toast.error(`Failed to load marketplace: ${err.message}`))
29
- .finally(() => setLoading(false));
41
+ const fetchRegistry = useCallback(async (opts: { refresh?: boolean } = {}) => {
42
+ if (opts.refresh) setRefreshing(true);
43
+ else setLoading(true);
44
+ setError(null);
45
+ try {
46
+ const r = await api.get<RegistryResponse>('/plugins/registry');
47
+ setRegistry(r);
48
+ } catch (err) {
49
+ const e = err as ApiError;
50
+ const data = e?.data as { registryUrl?: string } | null;
51
+ const url = data?.registryUrl;
52
+ const message = e?.message || 'unknown error';
53
+ setError({ message, url });
54
+ toast.error(`Failed to load marketplace: ${message}`);
55
+ } finally {
56
+ setLoading(false);
57
+ setRefreshing(false);
58
+ }
30
59
  }, [toast]);
31
60
 
61
+ useEffect(() => {
62
+ fetchRegistry();
63
+ }, [fetchRegistry]);
64
+
32
65
  const install = async (plugin: MarketplacePlugin) => {
33
66
  setInstalling(plugin);
34
67
  try {
@@ -50,14 +83,79 @@ export function Marketplace() {
50
83
  );
51
84
  const categories = [...new Set(plugins.map((p) => p.category))];
52
85
 
86
+ const sourceUrl = registry?.registry?.source;
87
+ const isCached = !!registry?.cached;
88
+ const cacheTimestamp = registry?.cacheTimestamp ?? null;
89
+
90
+ // ── Loading state ──────────────────────────────────────────────────────
53
91
  if (loading) {
54
92
  return (
55
- <div className="view-loading">
56
- <Spinner size="lg" />
93
+ <div className="view view-marketplace">
94
+ <header className="view-header">
95
+ <div className="view-header-text">
96
+ <h2 className="view-title">
97
+ <Store size={18} aria-hidden />
98
+ Plugin Marketplace
99
+ </h2>
100
+ <p className="view-subtitle">
101
+ Fetching registry from the community server…
102
+ </p>
103
+ </div>
104
+ </header>
105
+ <div className="view-loading" data-testid="marketplace-loading">
106
+ <Spinner size="lg" />
107
+ <p>Fetching registry from <code>{sourceUrl ?? 'https://github.com/DrB0rk/bizar-mods'}</code>…</p>
108
+ </div>
57
109
  </div>
58
110
  );
59
111
  }
60
112
 
113
+ // ── Error state ────────────────────────────────────────────────────────
114
+ if (error) {
115
+ return (
116
+ <div className="view view-marketplace">
117
+ <header className="view-header">
118
+ <div className="view-header-text">
119
+ <h2 className="view-title">
120
+ <Store size={18} aria-hidden />
121
+ Plugin Marketplace
122
+ </h2>
123
+ <p className="view-subtitle">
124
+ Couldn't reach the community registry.
125
+ </p>
126
+ </div>
127
+ <div className="view-actions">
128
+ <Button variant="secondary" size="sm" onClick={() => fetchRegistry({ refresh: true })} disabled={refreshing}>
129
+ {refreshing ? <Spinner size="sm" /> : <RefreshCw size={14} />}
130
+ Retry
131
+ </Button>
132
+ </div>
133
+ </header>
134
+ <EmptyState
135
+ icon={<AlertTriangle size={32} />}
136
+ title="Registry unreachable"
137
+ message={
138
+ <>
139
+ <p>All registry URLs failed to respond.</p>
140
+ {error.url && (
141
+ <p className="mono muted" style={{ marginTop: 8, fontSize: 12 }}>
142
+ Last tried: {error.url}
143
+ </p>
144
+ )}
145
+ <p className="muted" style={{ marginTop: 8, fontSize: 12 }}>{error.message}</p>
146
+ </>
147
+ }
148
+ action={
149
+ <Button variant="primary" size="sm" onClick={() => fetchRegistry({ refresh: true })} disabled={refreshing}>
150
+ {refreshing ? <Spinner size="sm" /> : <RefreshCw size={14} />} Try again
151
+ </Button>
152
+ }
153
+ />
154
+ </div>
155
+ );
156
+ }
157
+
158
+ // ── Success state ──────────────────────────────────────────────────────
61
159
  return (
62
160
  <div className="view view-marketplace">
63
161
  <header className="view-header">
@@ -70,8 +168,42 @@ export function Marketplace() {
70
168
  Discover and install plugins from the community registry.
71
169
  </p>
72
170
  </div>
171
+ <div className="view-actions">
172
+ <Button
173
+ variant="secondary"
174
+ size="sm"
175
+ onClick={() => fetchRegistry({ refresh: true })}
176
+ disabled={refreshing}
177
+ data-testid="marketplace-refresh"
178
+ >
179
+ {refreshing ? <Spinner size="sm" /> : <RefreshCw size={14} />}
180
+ Refresh
181
+ </Button>
182
+ </div>
73
183
  </header>
74
184
 
185
+ {/* Registry source banner */}
186
+ <div className="marketplace-source-banner muted" data-testid="marketplace-source">
187
+ <Globe size={12} aria-hidden />
188
+ <span>
189
+ Source:{' '}
190
+ {sourceUrl ? (
191
+ <code className="mono">{sourceUrl}</code>
192
+ ) : (
193
+ <span>community registry</span>
194
+ )}
195
+ </span>
196
+ {isCached && (
197
+ <span className="marketplace-cache-notice" data-testid="marketplace-cache-notice">
198
+ <Clock size={12} aria-hidden /> Showing cached results
199
+ {cacheTimestamp && ` from ${new Date(cacheTimestamp).toLocaleString()}`}
200
+ </span>
201
+ )}
202
+ <span className="marketplace-count-pill" data-testid="marketplace-count">
203
+ {plugins.length} plugin{plugins.length === 1 ? '' : 's'}
204
+ </span>
205
+ </div>
206
+
75
207
  <div className="marketplace-search-row">
76
208
  <input
77
209
  className="input"
@@ -80,37 +212,66 @@ export function Marketplace() {
80
212
  value={search}
81
213
  onChange={(e) => setSearch(e.target.value)}
82
214
  style={{ maxWidth: 320 }}
215
+ aria-label="Search marketplace plugins"
83
216
  />
84
217
  </div>
85
218
 
86
- <div className="category-chips">
87
- <button
88
- type="button"
89
- onClick={() => setCategory(null)}
90
- className={!category ? 'active' : ''}
91
- >
92
- All
93
- </button>
94
- {categories.map((c) => (
219
+ {categories.length > 0 && (
220
+ <div className="category-chips">
95
221
  <button
96
- key={c}
97
222
  type="button"
98
- onClick={() => setCategory(c)}
99
- className={category === c ? 'active' : ''}
223
+ onClick={() => setCategory(null)}
224
+ className={!category ? 'active' : ''}
225
+ data-testid="marketplace-category-all"
100
226
  >
101
- {c}
227
+ All
102
228
  </button>
103
- ))}
104
- </div>
229
+ {categories.map((c) => (
230
+ <button
231
+ key={c}
232
+ type="button"
233
+ onClick={() => setCategory(c)}
234
+ className={category === c ? 'active' : ''}
235
+ data-testid={`marketplace-category-${c.toLowerCase().replace(/\s+/g, '-')}`}
236
+ >
237
+ {c}
238
+ </button>
239
+ ))}
240
+ </div>
241
+ )}
105
242
 
106
243
  {filtered.length === 0 ? (
107
244
  <EmptyState
108
- icon={<Store size={32} />}
109
- title="No plugins found"
110
- message={search ? `No plugins match "${search}".` : 'The marketplace is empty.'}
245
+ icon={plugins.length === 0 ? <Wifi size={32} /> : <Store size={32} />}
246
+ title={plugins.length === 0 ? 'No plugins published yet' : 'No plugins match your filter'}
247
+ message={
248
+ plugins.length === 0 ? (
249
+ <>
250
+ <p>The community registry is empty right now.</p>
251
+ {sourceUrl && (
252
+ <p className="mono muted" style={{ marginTop: 8, fontSize: 12 }}>
253
+ Check back at <code>{sourceUrl}</code> later, or publish your own.
254
+ </p>
255
+ )}
256
+ </>
257
+ ) : search ? (
258
+ `No plugins match "${search}".`
259
+ ) : category ? (
260
+ `No plugins in "${category}".`
261
+ ) : (
262
+ 'Try a different search or category filter.'
263
+ )
264
+ }
265
+ action={
266
+ plugins.length === 0 ? (
267
+ <Button variant="secondary" size="sm" onClick={() => fetchRegistry({ refresh: true })} disabled={refreshing}>
268
+ {refreshing ? <Spinner size="sm" /> : <RefreshCw size={14} />} Refresh
269
+ </Button>
270
+ ) : null
271
+ }
111
272
  />
112
273
  ) : (
113
- <div className="marketplace-grid">
274
+ <div className="marketplace-grid" data-testid="marketplace-grid">
114
275
  {filtered.map((p) => (
115
276
  <MarketplacePluginCard
116
277
  key={p.id}
@@ -132,4 +293,4 @@ export function Marketplace() {
132
293
  )}
133
294
  </div>
134
295
  );
135
- }
296
+ }
@@ -1,4 +1,7 @@
1
1
  // src/web/views/memory/ConfigPanel.tsx — simplified memory config.
2
+ // v6.x — Vault path is now fetched from /api/memory/status so the row
3
+ // stops saying "(loading…)" forever. Users can change the vault path
4
+ // inline and initialise the vault if it doesn't exist yet.
2
5
  import { useEffect, useState } from 'react';
3
6
  import {
4
7
  GitBranch,
@@ -8,6 +11,9 @@ import {
8
11
  Loader2,
9
12
  Plug,
10
13
  Save,
14
+ FolderInput,
15
+ CheckCircle2,
16
+ XCircle,
11
17
  } from 'lucide-react';
12
18
  import { Button } from '../../components/Button';
13
19
  import { Card, CardMeta, CardTitle } from '../../components/Card';
@@ -31,6 +37,17 @@ type SyncStatus = {
31
37
  untracked?: number;
32
38
  };
33
39
 
40
+ type MemoryStatus = {
41
+ initialized: boolean;
42
+ mode?: string | null;
43
+ projectId?: string | null;
44
+ vaultRoot?: string | null;
45
+ branch?: string | null;
46
+ gitClean?: boolean | null;
47
+ noteCount?: number;
48
+ lastSecretScan?: string | null;
49
+ };
50
+
34
51
  type Props = { refreshKey: number };
35
52
 
36
53
  export function ConfigPanel({ refreshKey }: Props) {
@@ -39,7 +56,10 @@ export function ConfigPanel({ refreshKey }: Props) {
39
56
  const [loading, setLoading] = useState(true);
40
57
  const [saving, setSaving] = useState(false);
41
58
  const [remoteUrl, setRemoteUrl] = useState('');
42
- const [vaultPath] = useState<string>('');
59
+ const [memoryStatus, setMemoryStatus] = useState<MemoryStatus | null>(null);
60
+ const [vaultPathDraft, setVaultPathDraft] = useState<string>('');
61
+ const [pathInitialising, setPathInitialising] = useState(false);
62
+ const [pathSaving, setPathSaving] = useState(false);
43
63
  const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
44
64
  const [actionLoading, setActionLoading] = useState<string | null>(null);
45
65
  const [tests, setTests] = useState<Array<{ name: string; pass: boolean; detail: string }> | null>(null);
@@ -48,9 +68,22 @@ export function ConfigPanel({ refreshKey }: Props) {
48
68
  const reload = async () => {
49
69
  setLoading(true);
50
70
  try {
51
- const r = await api.get<{ config: GlobalConfig }>('/memory/config/global');
52
- setCfg(r.config);
53
- setRemoteUrl(r.config?.git?.remoteUrl || '');
71
+ const [cfgRes, statusRes] = await Promise.all([
72
+ api.get<{ config: GlobalConfig }>('/memory/config/global').catch(() => null),
73
+ api.get<MemoryStatus>('/memory/status').catch(() => null),
74
+ ]);
75
+ if (cfgRes) {
76
+ setCfg(cfgRes.config);
77
+ setRemoteUrl(cfgRes.config?.git?.remoteUrl || '');
78
+ } else {
79
+ setCfg({});
80
+ }
81
+ if (statusRes) {
82
+ setMemoryStatus(statusRes);
83
+ // Initialise the draft input from the server's current vaultRoot.
84
+ // Only overwrite while the user hasn't started editing.
85
+ setVaultPathDraft((cur) => cur || statusRes.vaultRoot || '');
86
+ }
54
87
  // Also fetch git status from the per-project vault
55
88
  await loadSyncStatus();
56
89
  } catch (err) {
@@ -81,6 +114,50 @@ export function ConfigPanel({ refreshKey }: Props) {
81
114
  // eslint-disable-next-line react-hooks/exhaustive-deps
82
115
  }, [refreshKey]);
83
116
 
117
+ // v6.x — Save the new vault path. If the folder doesn't exist, the
118
+ // backend's /memory/init endpoint can be used to create it (handled
119
+ // by ensureVaultExists()).
120
+ const onSaveVaultPath = async () => {
121
+ const target = vaultPathDraft.trim();
122
+ if (!target) {
123
+ toast.error('Vault path cannot be empty.');
124
+ return;
125
+ }
126
+ setPathSaving(true);
127
+ try {
128
+ const status = await api.get<MemoryStatus & { exists?: boolean }>('/memory/status').catch(() => null);
129
+ const initialised = status?.initialized;
130
+ if (!initialised) {
131
+ // Initialise the vault at the chosen path.
132
+ await api.post('/memory/init', { vaultRoot: target });
133
+ toast.success(`Vault initialised at ${target}.`);
134
+ } else {
135
+ // v6.x — Call the new vault config endpoint to persist the path.
136
+ await api.post('/memory/config/vault', { vaultRoot: target });
137
+ toast.success(`Vault path updated to ${target}.`);
138
+ }
139
+ setVaultPathDraft(target);
140
+ await reload();
141
+ } catch (err) {
142
+ toast.error(`Vault path save failed: ${(err as Error).message}`);
143
+ } finally {
144
+ setPathSaving(false);
145
+ }
146
+ };
147
+
148
+ const onInitVault = async () => {
149
+ setPathInitialising(true);
150
+ try {
151
+ await api.post('/memory/init', {});
152
+ toast.success('Vault initialised.');
153
+ await reload();
154
+ } catch (err) {
155
+ toast.error(`Init failed: ${(err as Error).message}`);
156
+ } finally {
157
+ setPathInitialising(false);
158
+ }
159
+ };
160
+
84
161
  const onSave = async () => {
85
162
  if (!cfg) return;
86
163
  setSaving(true);
@@ -143,16 +220,89 @@ export function ConfigPanel({ refreshKey }: Props) {
143
220
  }
144
221
  if (!cfg) return <div className="muted">No config available.</div>;
145
222
 
223
+ const initialised = !!memoryStatus?.initialized;
224
+ const currentVaultPath = memoryStatus?.vaultRoot ?? '';
225
+
146
226
  return (
147
227
  <div className="memory-panel-content">
228
+ {/* ── Vault path ──────────────────────────────────────────────────── */}
229
+ <Card>
230
+ <CardTitle>
231
+ <FolderInput size={14} /> Vault location
232
+ </CardTitle>
233
+ <CardMeta>
234
+ The on-disk folder where notes and memory metadata live. Default:{' '}
235
+ <code>~/.bizar_memory</code>.
236
+ </CardMeta>
237
+ <div className="memory-config-form">
238
+ <Row label="Current path" inline>
239
+ <span className="memory-vault-path mono" data-testid="memory-current-vault-path">
240
+ {currentVaultPath || <span className="muted">not initialised</span>}
241
+ </span>
242
+ {initialised ? (
243
+ <span
244
+ className="memory-pill memory-pill-ok"
245
+ style={{ marginLeft: 8 }}
246
+ title="Vault folder exists on disk"
247
+ >
248
+ <CheckCircle2 size={10} /> initialised
249
+ </span>
250
+ ) : (
251
+ <span
252
+ className="memory-pill memory-pill-warn"
253
+ style={{ marginLeft: 8 }}
254
+ title="Vault folder does not exist yet"
255
+ >
256
+ <XCircle size={10} /> not initialised
257
+ </span>
258
+ )}
259
+ </Row>
260
+ <Row label="Set path">
261
+ <input
262
+ type="text"
263
+ className="input mono"
264
+ value={vaultPathDraft}
265
+ onChange={(e) => setVaultPathDraft(e.target.value)}
266
+ placeholder="~/.bizar_memory"
267
+ aria-label="Vault path"
268
+ data-testid="memory-vault-path-input"
269
+ />
270
+ <span className="field-hint">
271
+ {initialised
272
+ ? 'Save to update the working path.'
273
+ : 'Save and initialise will create this folder if it does not exist.'}
274
+ </span>
275
+ </Row>
276
+ </div>
277
+ <div className="memory-action-row" style={{ marginTop: 12 }}>
278
+ <Button
279
+ variant="primary"
280
+ onClick={onSaveVaultPath}
281
+ disabled={pathSaving || pathInitialising || !vaultPathDraft.trim()}
282
+ data-testid="memory-save-vault-path"
283
+ >
284
+ {pathSaving ? <Loader2 size={12} className="memory-spin" /> : <Save size={12} />}
285
+ {initialised ? 'Save path' : 'Save and initialise'}
286
+ </Button>
287
+ {!initialised && (
288
+ <Button
289
+ variant="secondary"
290
+ onClick={onInitVault}
291
+ disabled={pathInitialising || pathSaving}
292
+ data-testid="memory-init-vault"
293
+ >
294
+ {pathInitialising ? <Loader2 size={12} className="memory-spin" /> : <FolderInput size={12} />}
295
+ Initialise default
296
+ </Button>
297
+ )}
298
+ </div>
299
+ </Card>
300
+
148
301
  {/* ── Git sync ───────────────────────────────────────────────────── */}
149
302
  <Card>
150
303
  <CardTitle><GitBranch size={14} /> Git sync</CardTitle>
151
304
  <CardMeta>Configure the git remote and sync your memory vault.</CardMeta>
152
305
  <div className="memory-config-form">
153
- <Row label="Vault path" inline>
154
- <span className="memory-vault-path mono muted">{vaultPath || '(loading…)'}</span>
155
- </Row>
156
306
  <Row label="Remote URL">
157
307
  <input
158
308
  type="text"
@@ -96,6 +96,7 @@ type Props = {
96
96
  export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Props) {
97
97
  const toast = useToast();
98
98
  const [data, setData] = useState<MemoryOverviewData | null>(null);
99
+ const [vaultPath, setVaultPath] = useState<string>('');
99
100
  const [loading, setLoading] = useState(true);
100
101
 
101
102
  const reload = async (signal?: AbortSignal) => {
@@ -107,6 +108,10 @@ export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Pro
107
108
  api.get<LightragStats>('/memory/lightrag/stats', signal).catch(() => null),
108
109
  api.get<StorageStats>('/memory/storage', signal).catch(() => null),
109
110
  ]);
111
+ // v6.x — Surface the resolved vault path as a top-level piece of state
112
+ // so the section header can display it instead of relying on
113
+ // derived fields. Resolves the "(loading…)" UI bug in the Config panel.
114
+ setVaultPath(status?.vaultRoot || '');
110
115
  setData({
111
116
  health: health || {
112
117
  score: 0,
@@ -186,6 +191,12 @@ export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Pro
186
191
  <span style={{ color: scoreColor }}>{health.status}</span>
187
192
  </h3>
188
193
  <p className="memory-health-message">{health.message}</p>
194
+ {/* v6.x — Vault path surfaced as a top-level piece of context.
195
+ Resolves the "(loading…)" stuck-state in the Config panel. */}
196
+ <p className="memory-health-vault mono muted" data-testid="memory-overview-vault-path">
197
+ <span className="memory-health-vault-label">Vault:</span>{' '}
198
+ {vaultPath || <em>not initialised</em>}
199
+ </p>
189
200
  <Button variant="ghost" size="sm" onClick={onRefresh}>
190
201
  <RefreshCw size={12} /> Refresh
191
202
  </Button>
@@ -1,6 +1,8 @@
1
1
  // src/web/views/settings/MemorySection.tsx — memory vault + git config for Settings.
2
+ // v6.x — Vault path now loads from /api/memory/status (was hardcoded to '').
3
+ // We also surface a small "Initialise" button when the vault does not exist.
2
4
  import React, { useEffect, useState } from 'react';
3
- import { Brain, GitBranch, Loader2, Save } from 'lucide-react';
5
+ import { Brain, GitBranch, Loader2, Save, FolderInput } from 'lucide-react';
4
6
  import { Button } from '../../components/Button';
5
7
  import { Card, CardTitle, CardMeta } from '../../components/Card';
6
8
  import { Spinner } from '../../components/Spinner';
@@ -15,21 +17,41 @@ type GlobalConfig = {
15
17
  git?: GitConfig;
16
18
  };
17
19
 
20
+ type MemoryStatus = {
21
+ initialized: boolean;
22
+ vaultRoot?: string | null;
23
+ mode?: string | null;
24
+ };
25
+
18
26
  export function MemorySection() {
19
27
  const toast = useToast();
20
28
  const [loading, setLoading] = useState(true);
21
29
  const [saving, setSaving] = useState(false);
30
+ const [initialising, setInitialising] = useState(false);
22
31
  const [remoteUrl, setRemoteUrl] = useState('');
23
- const [vaultPath] = useState<string>(''); // server-side default, not user-editable
32
+ const [vaultPath, setVaultPath] = useState<string>('');
33
+ const [vaultInitialised, setVaultInitialised] = useState(false);
24
34
  const [refreshKey, setRefreshKey] = useState(0);
25
35
 
26
36
  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));
37
+ let cancelled = false;
38
+ (async () => {
39
+ try {
40
+ const [cfgRes, statusRes] = await Promise.all([
41
+ api.get<{ config: GlobalConfig; path?: string }>('/memory/config/global').catch(() => null),
42
+ api.get<MemoryStatus>('/memory/status').catch(() => null),
43
+ ]);
44
+ if (cancelled) return;
45
+ if (cfgRes) setRemoteUrl(cfgRes.config?.git?.remoteUrl || '');
46
+ if (statusRes) {
47
+ setVaultPath(statusRes.vaultRoot || '');
48
+ setVaultInitialised(!!statusRes.initialized);
49
+ }
50
+ } finally {
51
+ if (!cancelled) setLoading(false);
52
+ }
53
+ })();
54
+ return () => { cancelled = true; };
33
55
  }, [refreshKey]);
34
56
 
35
57
  const onSave = async () => {
@@ -45,6 +67,19 @@ export function MemorySection() {
45
67
  }
46
68
  };
47
69
 
70
+ const onInitialise = async () => {
71
+ setInitialising(true);
72
+ try {
73
+ await api.post('/memory/init', {});
74
+ toast.success('Vault initialised.');
75
+ setRefreshKey((k) => k + 1);
76
+ } catch (err) {
77
+ toast.error(`Initialise failed: ${(err as Error).message}`);
78
+ } finally {
79
+ setInitialising(false);
80
+ }
81
+ };
82
+
48
83
  return (
49
84
  <Card id="settings-memory" data-section="memory">
50
85
  <CardTitle><Brain size={14} /> Memory vault</CardTitle>
@@ -60,9 +95,28 @@ export function MemorySection() {
60
95
  ) : (
61
96
  <div className="settings-fields">
62
97
  <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'}
98
+ <label className="field-label">
99
+ <FolderInput size={12} style={{ display: 'inline', marginRight: 4 }} />
100
+ Vault path
101
+ </label>
102
+ <div
103
+ className="field-readonly mono"
104
+ data-testid="settings-memory-vault-path"
105
+ style={{ fontSize: 12, padding: '6px 0' }}
106
+ >
107
+ {vaultPath || <span className="muted">not initialised</span>}
108
+ {vaultInitialised ? null : (
109
+ <Button
110
+ variant="ghost"
111
+ size="sm"
112
+ onClick={onInitialise}
113
+ disabled={initialising}
114
+ style={{ marginLeft: 12 }}
115
+ >
116
+ {initialising ? <Loader2 size={12} className="memory-spin" /> : <FolderInput size={12} />}
117
+ Initialise
118
+ </Button>
119
+ )}
66
120
  </div>
67
121
  </div>
68
122