@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.
- package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js → EnvVarsSection-DoIzqrlb.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js.map → EnvVarsSection-DoIzqrlb.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js → MobileChat-CoUCUsbr.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js.map → MobileChat-CoUCUsbr.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js.map +1 -0
- package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js → Toast-D9nv5N6m.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js.map → Toast-D9nv5N6m.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-BsWXWkEF.js → icons-Btipv2pp.js} +100 -90
- package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +1 -0
- package/bizar-dash/dist/assets/main-DHXPuddY.js +18 -0
- package/bizar-dash/dist/assets/main-DHXPuddY.js.map +1 -0
- package/bizar-dash/dist/assets/{main-BklYy02e.css → main-UG4jKK-F.css} +1 -1
- package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +1 -0
- package/bizar-dash/dist/assets/{mobile-CsaN4CxH.js.map → mobile-H5KH9cTL.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js → mobile-layout-BqHxVb6_.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js.map → mobile-layout-BqHxVb6_.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js → useSlashCommands-C24-O1e7.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js.map → useSlashCommands-C24-O1e7.js.map} +1 -1
- package/bizar-dash/dist/index.html +7 -7
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/bg-spawner.mjs +522 -305
- package/bizar-dash/src/server/headroom.mjs +3 -1
- package/bizar-dash/src/server/memory-lightrag.mjs +26 -2
- package/bizar-dash/src/server/memory-store.mjs +74 -28
- package/bizar-dash/src/server/opencode-sdk.mjs +63 -3
- package/bizar-dash/src/server/plugins/registry.mjs +1 -1
- package/bizar-dash/src/server/routes/activity.mjs +85 -0
- package/bizar-dash/src/server/routes/background.mjs +31 -13
- package/bizar-dash/src/server/routes/memory.mjs +91 -38
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +1 -1
- package/bizar-dash/src/server/server.mjs +4 -1
- package/bizar-dash/src/web/components/Sidebar.tsx +0 -1
- package/bizar-dash/src/web/styles/main.css +52 -13
- package/bizar-dash/src/web/styles/memory.css +17 -0
- package/bizar-dash/src/web/styles/settings.css +6 -56
- package/bizar-dash/src/web/views/Marketplace.tsx +192 -31
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +157 -7
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +11 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +65 -11
- package/bizar-dash/tests/background-sdk-session.test.mjs +94 -0
- package/bizar-dash/tests/background-session-events.test.mjs +74 -0
- package/bizar-dash/tests/background-steer-sdk.test.mjs +71 -0
- package/bizar-dash/tests/background-steer.test.mjs +25 -7
- package/bizar-dash/tests/memory-namespace.test.mjs +2 -2
- package/bizar-dash/tests/memory-status-root.test.mjs +144 -0
- package/bizar-dash/tests/memory-store.test.mjs +5 -4
- package/bizar-dash/tests/memory-vault-config-endpoint.test.mjs +173 -0
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +1 -1
- package/bizar-dash/tests/settings-nav.test.tsx +0 -1
- package/bizar-dash/tests/views/Marketplace.test.tsx +200 -0
- package/bizar-dash/tests/views/Memory.test.tsx +274 -0
- package/bizar-dash/tests/views/sidebar.test.tsx +116 -0
- package/bizar-dash/tests/views/topbar.test.tsx +87 -0
- package/cli/commands/marketplace.mjs +1 -1
- package/cli/memory.mjs +1 -1
- package/package.json +1 -1
- package/plugins/bizar/src/background-state.ts +11 -0
- package/plugins/bizar/src/background.ts +212 -34
- package/plugins/bizar/src/opencode-runner.ts +80 -353
- package/plugins/bizar/src/tools/bg-send-message.ts +160 -61
- package/plugins/bizar/src/tools/bg-spawn.ts +235 -156
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +84 -11
- package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +223 -0
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js.map +0 -1
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +0 -1
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +0 -18
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +0 -1
- 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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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-
|
|
56
|
-
<
|
|
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
|
-
|
|
87
|
-
<
|
|
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(
|
|
99
|
-
className={category
|
|
223
|
+
onClick={() => setCategory(null)}
|
|
224
|
+
className={!category ? 'active' : ''}
|
|
225
|
+
data-testid="marketplace-category-all"
|
|
100
226
|
>
|
|
101
|
-
|
|
227
|
+
All
|
|
102
228
|
</button>
|
|
103
|
-
|
|
104
|
-
|
|
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=
|
|
110
|
-
message={
|
|
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 [
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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>('');
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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">
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|