@polderlabs/bizar 4.7.0 → 4.7.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/main-DHZmbnxQ.js +361 -0
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
- package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
- package/bizar-dash/dist/assets/{mobile-CWqPoGaT.js → mobile-BK8-ythT.js} +2 -2
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-i4Uv9eW8.js → mobile-Chvf9u_B.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-i4Uv9eW8.js.map → mobile-Chvf9u_B.js.map} +1 -1
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/skills/publishing/SKILL.md +146 -0
- package/bizar-dash/src/server/api.mjs +8 -0
- package/bizar-dash/src/server/backup-store.mjs +525 -0
- package/bizar-dash/src/server/digest-store.mjs +558 -0
- package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
- package/bizar-dash/src/server/routes/backup.mjs +112 -0
- package/bizar-dash/src/server/routes/chat.mjs +14 -0
- package/bizar-dash/src/server/routes/digests.mjs +82 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
- package/bizar-dash/src/server/schedules-runner.mjs +126 -0
- package/bizar-dash/src/server/server.mjs +17 -0
- package/bizar-dash/src/web/App.tsx +8 -1
- package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +3 -1
- package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
- package/bizar-dash/src/web/styles/main.css +70 -8
- package/bizar-dash/src/web/views/Activity.tsx +11 -1
- package/bizar-dash/src/web/views/Agents.tsx +57 -42
- package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
- package/bizar-dash/src/web/views/Chat.tsx +8 -0
- package/bizar-dash/src/web/views/History.tsx +4 -1
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
- package/bizar-dash/src/web/views/Mods.tsx +30 -17
- package/bizar-dash/src/web/views/Overview.tsx +8 -1
- package/bizar-dash/src/web/views/Providers.tsx +16 -16
- package/bizar-dash/src/web/views/Schedules.tsx +33 -15
- package/bizar-dash/src/web/views/Settings.tsx +97 -1751
- package/bizar-dash/src/web/views/Skills.tsx +4 -1
- package/bizar-dash/src/web/views/Tasks.tsx +11 -2
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
- package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
- package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
- package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
- package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
- package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
- package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
- package/bizar-dash/tests/a11y.test.tsx +206 -0
- package/bizar-dash/tests/backup-restore.test.mjs +217 -0
- package/bizar-dash/tests/backup-restore.test.tsx +123 -0
- package/bizar-dash/tests/backup-store.test.mjs +300 -0
- package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
- package/bizar-dash/tests/digest-generation.test.mjs +191 -0
- package/bizar-dash/tests/digest-store.test.mjs +264 -0
- package/bizar-dash/tests/rate-limit.test.mjs +298 -0
- package/cli/bin.mjs +96 -2
- package/cli/commands/minimax.mjs +20 -0
- package/cli/commands/util.mjs +154 -1
- package/cli/digest.mjs +149 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
- package/bizar-dash/dist/assets/main-DGGq-iZI.js +0 -361
- package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +0 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/web/views/settings/AgentSection.tsx
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Server as ServerIcon, Globe, Save } from 'lucide-react';
|
|
4
|
+
import { Card, CardTitle, CardMeta } from '../../components/Card';
|
|
5
|
+
import { Button } from '../../components/Button';
|
|
6
|
+
import { useToast } from '../../components/Toast';
|
|
7
|
+
import { api } from '../../lib/api';
|
|
8
|
+
import type { Settings } from '../../lib/types';
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
settings: Settings;
|
|
12
|
+
patchAgents: (patch: Partial<Settings['agents']>) => void;
|
|
13
|
+
patchDashboard: (patch: Partial<Settings['dashboard']>) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function AgentSection({ settings, patchAgents, patchDashboard }: Props) {
|
|
17
|
+
const toast = useToast();
|
|
18
|
+
const [pluginOptions, setPluginOptions] = useState<Record<string, number>>({});
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
api.get<Record<string, number>>('/settings/plugin-options')
|
|
22
|
+
.then(setPluginOptions)
|
|
23
|
+
.catch(() => { /* not persisted yet — use defaults */ });
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const onSavePluginOptions = async () => {
|
|
27
|
+
try {
|
|
28
|
+
await api.put('/settings/plugin-options', pluginOptions);
|
|
29
|
+
toast.success('Saved — restart opencode for changes to take effect.');
|
|
30
|
+
} catch (err) {
|
|
31
|
+
toast.error(`Save failed: ${(err as Error).message}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const onCleanupBackground = async () => {
|
|
36
|
+
try {
|
|
37
|
+
const r = await fetch('/api/background/cleanup', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ maxAgeDays: 7 }),
|
|
41
|
+
});
|
|
42
|
+
const result = await r.json();
|
|
43
|
+
toast.success(`Cleaned up ${result.deleted} old instances.`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
toast.error(`Cleanup failed: ${(err as Error).message}`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
{/* Agents */}
|
|
52
|
+
<Card id="settings-agents" data-section="agents">
|
|
53
|
+
<CardTitle><ServerIcon size={14} /> Agent Behavior</CardTitle>
|
|
54
|
+
<CardMeta>Limits and timeouts for background agent dispatch.</CardMeta>
|
|
55
|
+
<div className="form-row">
|
|
56
|
+
<label htmlFor="agents-maxParallel">
|
|
57
|
+
Max parallel agents
|
|
58
|
+
<span className="meta-badge">default: 6</span>
|
|
59
|
+
</label>
|
|
60
|
+
<input
|
|
61
|
+
id="agents-maxParallel"
|
|
62
|
+
type="number"
|
|
63
|
+
min={1}
|
|
64
|
+
max={20}
|
|
65
|
+
value={settings.agents?.maxParallel ?? 6}
|
|
66
|
+
onChange={(e) => patchAgents({ maxParallel: Math.max(1, Math.min(20, parseInt(e.target.value, 10) || 6)) })}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="form-row">
|
|
70
|
+
<label htmlFor="agents-stuckThresholdMs">
|
|
71
|
+
Stuck threshold (ms)
|
|
72
|
+
<span className="meta-badge">default: 600000 (10 min)</span>
|
|
73
|
+
</label>
|
|
74
|
+
<input
|
|
75
|
+
id="agents-stuckThresholdMs"
|
|
76
|
+
type="number"
|
|
77
|
+
min={60000}
|
|
78
|
+
max={3600000}
|
|
79
|
+
step={60000}
|
|
80
|
+
value={settings.agents?.stuckThresholdMs ?? 600000}
|
|
81
|
+
onChange={(e) => patchAgents({ stuckThresholdMs: Math.max(60000, Math.min(3600000, parseInt(e.target.value, 10) || 600000)) })}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
<label className="checkbox-row" data-setting-id="agents.autoRestart">
|
|
85
|
+
<input
|
|
86
|
+
type="checkbox"
|
|
87
|
+
checked={!!settings.agents?.autoRestart}
|
|
88
|
+
onChange={(e) => patchAgents({ autoRestart: e.target.checked })}
|
|
89
|
+
/>
|
|
90
|
+
<span>Auto-restart stuck agents</span>
|
|
91
|
+
</label>
|
|
92
|
+
</Card>
|
|
93
|
+
|
|
94
|
+
{/* Dashboard */}
|
|
95
|
+
<Card id="settings-dashboard" data-section="dashboard">
|
|
96
|
+
<CardTitle><Globe size={14} /> Dashboard</CardTitle>
|
|
97
|
+
<CardMeta>Controls how <code>bizar</code> starts up.</CardMeta>
|
|
98
|
+
<label className="checkbox-row" data-setting-id="dashboard.autoLaunchWeb">
|
|
99
|
+
<input
|
|
100
|
+
type="checkbox"
|
|
101
|
+
checked={settings.dashboard.autoLaunchWeb !== false}
|
|
102
|
+
onChange={(e) => patchDashboard({ autoLaunchWeb: e.target.checked })}
|
|
103
|
+
/>
|
|
104
|
+
<span>Auto-launch web UI alongside TUI</span>
|
|
105
|
+
</label>
|
|
106
|
+
|
|
107
|
+
<div className="field" data-setting-id="dashboard.projectsDirectory" style={{ marginTop: 'var(--space-4)' }}>
|
|
108
|
+
<label className="field-label" htmlFor="set-projects-directory">Projects directory</label>
|
|
109
|
+
<input
|
|
110
|
+
id="set-projects-directory"
|
|
111
|
+
className="input"
|
|
112
|
+
type="text"
|
|
113
|
+
placeholder="/home/user/projects"
|
|
114
|
+
value={settings.dashboard.projectsDirectory ?? ''}
|
|
115
|
+
onChange={(e) => patchDashboard({ projectsDirectory: e.target.value })}
|
|
116
|
+
/>
|
|
117
|
+
<p className="field-help">
|
|
118
|
+
New projects created via the dashboard will land here, and existing project
|
|
119
|
+
directories inside this folder are auto-recognized on startup.
|
|
120
|
+
</p>
|
|
121
|
+
{settings.dashboard.projectsDirectory && (
|
|
122
|
+
<>
|
|
123
|
+
{!/^\/|^[A-Za-z]:/.test(settings.dashboard.projectsDirectory) && (
|
|
124
|
+
<p style={{ color: 'var(--warning)', fontSize: 11, marginTop: 4 }}>
|
|
125
|
+
Path should be absolute (start with / on Linux/Mac, or a drive letter on Windows).
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
{settings.dashboard.projectsDirectory.includes('..') && (
|
|
129
|
+
<p style={{ color: 'var(--error)', fontSize: 11, marginTop: 4 }}>
|
|
130
|
+
Path traversal not allowed — this will be rejected server-side.
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* allowedRoots textarea */}
|
|
138
|
+
<div className="field" data-setting-id="dashboard.allowedRoots" style={{ marginTop: 'var(--space-4)' }}>
|
|
139
|
+
<label className="field-label" htmlFor="set-allowed-roots">
|
|
140
|
+
Additional allowed roots <span className="muted">(advanced)</span>
|
|
141
|
+
</label>
|
|
142
|
+
<textarea
|
|
143
|
+
id="set-allowed-roots"
|
|
144
|
+
className="textarea"
|
|
145
|
+
rows={4}
|
|
146
|
+
placeholder="/workspace /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);
|
|
153
|
+
patchDashboard({ allowedRoots: lines });
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
<p className="field-help">
|
|
157
|
+
Optional. Add filesystem roots beyond your home directory that the file
|
|
158
|
+
browser and project scanner can access. Each path must be inside your
|
|
159
|
+
home directory. One per line.
|
|
160
|
+
</p>
|
|
161
|
+
{(() => {
|
|
162
|
+
const rawLines = (settings.dashboard.allowedRoots ?? []).join('\n').split('\n');
|
|
163
|
+
const warnings: { key: string; msg: React.ReactNode }[] = [];
|
|
164
|
+
rawLines.forEach((line, i) => {
|
|
165
|
+
if (!line.trim()) return;
|
|
166
|
+
if (!/^\/|^[A-Za-z]:/.test(line)) {
|
|
167
|
+
warnings.push({
|
|
168
|
+
key: `noabs-${i}`,
|
|
169
|
+
msg: (
|
|
170
|
+
<p style={{ color: 'var(--warning)', fontSize: 11, marginTop: 2 }}>
|
|
171
|
+
Line {i + 1}: "{line}" — should be absolute (start with / or a drive letter).
|
|
172
|
+
</p>
|
|
173
|
+
),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (line.includes('..')) {
|
|
177
|
+
warnings.push({
|
|
178
|
+
key: `dots-${i}`,
|
|
179
|
+
msg: (
|
|
180
|
+
<p style={{ color: 'var(--error)', fontSize: 11, marginTop: 2 }}>
|
|
181
|
+
Line {i + 1}: "{line}" — contains '..' (server will reject this).
|
|
182
|
+
</p>
|
|
183
|
+
),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return warnings.map((w) => w.msg);
|
|
188
|
+
})()}
|
|
189
|
+
</div>
|
|
190
|
+
</Card>
|
|
191
|
+
|
|
192
|
+
{/* Background agents */}
|
|
193
|
+
<Card id="settings-background" data-section="background">
|
|
194
|
+
<CardTitle><ServerIcon size={14} /> Background Agents</CardTitle>
|
|
195
|
+
<CardMeta>Tune plugin options. Changes take effect on next plugin restart.</CardMeta>
|
|
196
|
+
|
|
197
|
+
<div className="form-row">
|
|
198
|
+
<label htmlFor="bg-maxConcurrent">
|
|
199
|
+
Max concurrent instances
|
|
200
|
+
<span className="meta-badge">default: 8</span>
|
|
201
|
+
</label>
|
|
202
|
+
<input
|
|
203
|
+
id="bg-maxConcurrent"
|
|
204
|
+
type="number"
|
|
205
|
+
min={1}
|
|
206
|
+
max={32}
|
|
207
|
+
value={pluginOptions.maxConcurrentInstances ?? 8}
|
|
208
|
+
onChange={(e) => setPluginOptions((cur) => ({ ...cur, maxConcurrentInstances: Math.max(1, Math.min(32, parseInt(e.target.value, 10) || 8)) }))}
|
|
209
|
+
/>
|
|
210
|
+
<small className="muted">Plugin option: <code>maxConcurrentInstances</code></small>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div className="form-row">
|
|
214
|
+
<label htmlFor="bg-toolCallCap">
|
|
215
|
+
Tool-call cap
|
|
216
|
+
<span className="meta-badge">default: 500</span>
|
|
217
|
+
</label>
|
|
218
|
+
<input
|
|
219
|
+
id="bg-toolCallCap"
|
|
220
|
+
type="number"
|
|
221
|
+
min={1}
|
|
222
|
+
max={5000}
|
|
223
|
+
value={pluginOptions.backgroundToolCallCap ?? 500}
|
|
224
|
+
onChange={(e) => setPluginOptions((cur) => ({ ...cur, backgroundToolCallCap: Math.max(1, Math.min(5000, parseInt(e.target.value, 10) || 500)) }))}
|
|
225
|
+
/>
|
|
226
|
+
<small className="muted">Plugin option: <code>backgroundToolCallCap</code></small>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div className="form-row">
|
|
230
|
+
<label htmlFor="bg-stallTimeout">
|
|
231
|
+
Stall timeout (ms)
|
|
232
|
+
<span className="meta-badge">default: 180000</span>
|
|
233
|
+
</label>
|
|
234
|
+
<input
|
|
235
|
+
id="bg-stallTimeout"
|
|
236
|
+
type="number"
|
|
237
|
+
min={10000}
|
|
238
|
+
max={600000}
|
|
239
|
+
step={1000}
|
|
240
|
+
value={pluginOptions.backgroundStallTimeoutMs ?? 180000}
|
|
241
|
+
onChange={(e) => setPluginOptions((cur) => ({ ...cur, backgroundStallTimeoutMs: Math.max(10000, Math.min(600000, parseInt(e.target.value, 10) || 180000)) }))}
|
|
242
|
+
/>
|
|
243
|
+
<small className="muted">Plugin option: <code>backgroundStallTimeoutMs</code></small>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<div className="form-row">
|
|
247
|
+
<label htmlFor="bg-thinkingLoopTimeout">
|
|
248
|
+
Thinking-loop timeout (ms)
|
|
249
|
+
<span className="meta-badge">default: 300000</span>
|
|
250
|
+
</label>
|
|
251
|
+
<input
|
|
252
|
+
id="bg-thinkingLoopTimeout"
|
|
253
|
+
type="number"
|
|
254
|
+
min={30000}
|
|
255
|
+
max={900000}
|
|
256
|
+
step={1000}
|
|
257
|
+
value={pluginOptions.backgroundThinkingLoopTimeoutMs ?? 300000}
|
|
258
|
+
onChange={(e) => setPluginOptions((cur) => ({ ...cur, backgroundThinkingLoopTimeoutMs: Math.max(30000, Math.min(900000, parseInt(e.target.value, 10) || 300000)) }))}
|
|
259
|
+
/>
|
|
260
|
+
<small className="muted">Plugin option: <code>backgroundThinkingLoopTimeoutMs</code></small>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div className="form-row">
|
|
264
|
+
<label htmlFor="bg-maxInterventions">
|
|
265
|
+
Max interventions
|
|
266
|
+
<span className="meta-badge">default: 1</span>
|
|
267
|
+
</label>
|
|
268
|
+
<input
|
|
269
|
+
id="bg-maxInterventions"
|
|
270
|
+
type="number"
|
|
271
|
+
min={1}
|
|
272
|
+
max={3}
|
|
273
|
+
value={pluginOptions.backgroundMaxInterventions ?? 1}
|
|
274
|
+
onChange={(e) => setPluginOptions((cur) => ({ ...cur, backgroundMaxInterventions: Math.max(1, Math.min(3, parseInt(e.target.value, 10) || 1)) }))}
|
|
275
|
+
/>
|
|
276
|
+
<small className="muted">Plugin option: <code>backgroundMaxInterventions</code></small>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="form-row">
|
|
280
|
+
<Button variant="secondary" size="sm" onClick={onSavePluginOptions}>
|
|
281
|
+
<Save size={14} /> Save plugin options
|
|
282
|
+
</Button>
|
|
283
|
+
<Button variant="secondary" size="sm" onClick={onCleanupBackground}>
|
|
284
|
+
Cleanup old instances (>7 days)
|
|
285
|
+
</Button>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div className="form-row">
|
|
289
|
+
<small>Plugin options are read at startup. Save changes and run <code>bizar update</code> to apply.</small>
|
|
290
|
+
</div>
|
|
291
|
+
</Card>
|
|
292
|
+
</>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/web/views/settings/AuthSection.tsx
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Shield, Copy, KeyRound, RotateCcw } from 'lucide-react';
|
|
4
|
+
import { Card, CardTitle, CardMeta } from '../../components/Card';
|
|
5
|
+
import { Button } from '../../components/Button';
|
|
6
|
+
import { useToast } from '../../components/Toast';
|
|
7
|
+
import { api } from '../../lib/api';
|
|
8
|
+
import type { Settings } from '../../lib/types';
|
|
9
|
+
|
|
10
|
+
type AuthStatus = { required: boolean; loopback: boolean; peer: string };
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
settings: Settings;
|
|
14
|
+
authStatus: AuthStatus | null;
|
|
15
|
+
setAuthStatus: React.Dispatch<React.SetStateAction<AuthStatus | null>>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function AuthSection({ authStatus, setAuthStatus }: Props) {
|
|
19
|
+
const toast = useToast();
|
|
20
|
+
const [authToken, setAuthToken] = useState<string>(api.getToken());
|
|
21
|
+
const [revealedToken, setRevealedToken] = useState<string>('');
|
|
22
|
+
|
|
23
|
+
// Probe the server once on mount
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const r = await api.probeAuthStatus();
|
|
29
|
+
if (!cancelled) setAuthStatus(r);
|
|
30
|
+
} catch {
|
|
31
|
+
if (!cancelled) {
|
|
32
|
+
setAuthStatus({ required: true, loopback: false, peer: '' });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
return () => { cancelled = true; };
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const onCopyToken = async () => {
|
|
40
|
+
try {
|
|
41
|
+
const r = await api.get<{ token: string }>('/auth/reveal');
|
|
42
|
+
setRevealedToken(r.token);
|
|
43
|
+
setAuthToken(r.token);
|
|
44
|
+
api.setToken(r.token);
|
|
45
|
+
try {
|
|
46
|
+
await navigator.clipboard.writeText(r.token);
|
|
47
|
+
toast.success('Token copied to clipboard.');
|
|
48
|
+
} catch {
|
|
49
|
+
toast.success('Token revealed — copy from the field below.');
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
toast.error(`Reveal failed: ${(err as Error).message}`);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const onRegenerateToken = async () => {
|
|
57
|
+
if (!confirm('Regenerate the auth token? Anything still using the old token will start getting 401 errors immediately.')) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const r = await api.post<{ token: string }>('/auth/regenerate');
|
|
62
|
+
setRevealedToken(r.token);
|
|
63
|
+
setAuthToken(r.token);
|
|
64
|
+
api.setToken(r.token);
|
|
65
|
+
try {
|
|
66
|
+
await navigator.clipboard.writeText(r.token);
|
|
67
|
+
toast.success('New token generated and copied to clipboard.');
|
|
68
|
+
} catch {
|
|
69
|
+
toast.success('New token generated — copy from the field below. Old token is now invalid.');
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
toast.error(`Regenerate failed: ${(err as Error).message}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onSaveToken = () => {
|
|
77
|
+
api.setToken(authToken.trim());
|
|
78
|
+
toast.success('Token saved. The dashboard will use it on the next request.');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Card id="settings-auth" data-section="auth">
|
|
83
|
+
<CardTitle><Shield size={14} /> Authentication</CardTitle>
|
|
84
|
+
<CardMeta>
|
|
85
|
+
Localhost and Tailscale browser access are auto-trusted via loopback.
|
|
86
|
+
A bearer token is still available for non-loopback clients and
|
|
87
|
+
forced-auth mode.
|
|
88
|
+
</CardMeta>
|
|
89
|
+
|
|
90
|
+
<div className="field" data-setting-id="auth.status">
|
|
91
|
+
<label className="field-label">Server status</label>
|
|
92
|
+
<p style={{ margin: '4px 0' }}>
|
|
93
|
+
Auth required:{' '}
|
|
94
|
+
<strong>{authStatus ? (authStatus.required ? 'yes' : 'no') : 'probing…'}</strong>
|
|
95
|
+
</p>
|
|
96
|
+
<p style={{ margin: '4px 0' }}>
|
|
97
|
+
Connection:{' '}
|
|
98
|
+
<strong>
|
|
99
|
+
{authStatus
|
|
100
|
+
? (authStatus.loopback ? 'loopback (auto-trusted)' : 'remote')
|
|
101
|
+
: 'probing…'}
|
|
102
|
+
</strong>
|
|
103
|
+
</p>
|
|
104
|
+
<p style={{ margin: '4px 0' }}>
|
|
105
|
+
Peer address:{' '}
|
|
106
|
+
{authStatus?.peer ? <code>{authStatus.peer}</code> : <span className="muted">probing…</span>}
|
|
107
|
+
</p>
|
|
108
|
+
<p className="muted" style={{ fontSize: 12, margin: '4px 0' }}>
|
|
109
|
+
Localhost and Tailscale browser access are auto-trusted because the
|
|
110
|
+
dashboard sees a loopback peer. Paste a token only for non-loopback
|
|
111
|
+
API clients/scripts, or if you force auth for every connection with{' '}
|
|
112
|
+
<code>BIZAR_DASHBOARD_REQUIRE_AUTH=1</code>.
|
|
113
|
+
</p>
|
|
114
|
+
<p className="muted" style={{ fontSize: 12, margin: '4px 0' }}>
|
|
115
|
+
Dashboard tokens are generated on first boot and saved to{' '}
|
|
116
|
+
<code>~/.config/bizar/dashboard-secret</code> (mode 0600).
|
|
117
|
+
</p>
|
|
118
|
+
<p className="muted" style={{ fontSize: 12, margin: '4px 0' }}>
|
|
119
|
+
For Tailscale Serve or any reverse-proxy access, paste this token
|
|
120
|
+
once via the boot screen — it is saved per-origin and works for all
|
|
121
|
+
subsequent visits.
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="field" data-setting-id="auth.token">
|
|
126
|
+
<label className="field-label">Token (this browser)</label>
|
|
127
|
+
<input
|
|
128
|
+
type="password"
|
|
129
|
+
className="input mono"
|
|
130
|
+
value={authToken}
|
|
131
|
+
onChange={(e) => setAuthToken(e.target.value)}
|
|
132
|
+
placeholder="Paste token from server stderr or another browser"
|
|
133
|
+
spellCheck={false}
|
|
134
|
+
autoComplete="off"
|
|
135
|
+
/>
|
|
136
|
+
<div className="task-form-row" style={{ marginTop: 8 }}>
|
|
137
|
+
<Button variant="secondary" size="sm" onClick={onSaveToken}>
|
|
138
|
+
<KeyRound size={14} /> Save token
|
|
139
|
+
</Button>
|
|
140
|
+
<Button variant="ghost" size="sm" onClick={onCopyToken}>
|
|
141
|
+
<Copy size={14} /> Reveal & copy server token
|
|
142
|
+
</Button>
|
|
143
|
+
<Button variant="ghost" size="sm" onClick={onRegenerateToken}>
|
|
144
|
+
<RotateCcw size={14} /> Regenerate
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
{revealedToken ? (
|
|
148
|
+
<p className="muted" style={{ fontSize: 12, marginTop: 8 }}>
|
|
149
|
+
Last revealed token (one-time): <code className="mono">{revealedToken}</code>
|
|
150
|
+
</p>
|
|
151
|
+
) : null}
|
|
152
|
+
<p className="muted" style={{ fontSize: 12, marginTop: 8 }}>
|
|
153
|
+
Regenerating invalidates the current token immediately. Anything
|
|
154
|
+
still using the old token will see 401 until it's updated.
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
</Card>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/web/views/settings/BackupSection.tsx
|
|
2
|
+
// Consumes BackupRestore from thor-backup if available; shows a placeholder otherwise.
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Card, CardTitle, CardMeta } from '../../components/Card';
|
|
5
|
+
|
|
6
|
+
export function BackupSection() {
|
|
7
|
+
return (
|
|
8
|
+
<Card id="settings-backup" data-section="backup">
|
|
9
|
+
<CardTitle>Backup & Restore</CardTitle>
|
|
10
|
+
<CardMeta>Export and restore your Bizar configuration.</CardMeta>
|
|
11
|
+
<p className="muted" style={{ fontSize: 12 }}>
|
|
12
|
+
Backup and restore is coming soon.
|
|
13
|
+
</p>
|
|
14
|
+
</Card>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/web/views/settings/EnvVarsSection.tsx
|
|
2
|
+
// Placeholder — EnvVarManager integration to be added in a future PR.
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Card, CardTitle, CardMeta } from '../../components/Card';
|
|
5
|
+
|
|
6
|
+
export function EnvVarsSection() {
|
|
7
|
+
return (
|
|
8
|
+
<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>
|
|
14
|
+
</Card>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/web/views/settings/GeneralSection.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Layout as LayoutIcon } from 'lucide-react';
|
|
4
|
+
import { Card, CardTitle, CardMeta } from '../../components/Card';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import type { Settings } from '../../lib/types';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
settings: Settings;
|
|
10
|
+
patchUi: (patch: Partial<Settings['ui']>) => void;
|
|
11
|
+
patchTop: <K extends keyof Settings>(key: K, value: Settings[K]) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const LAYOUTS = [
|
|
15
|
+
{ id: 'topnav', label: 'Top nav' },
|
|
16
|
+
{ id: 'sidebar', label: 'Sidebar' },
|
|
17
|
+
{ id: 'both', label: 'Both' },
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export function GeneralSection({ settings, patchUi, patchTop }: Props) {
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
{/* UI Layout */}
|
|
24
|
+
<Card id="settings-layout" data-section="layout">
|
|
25
|
+
<CardTitle><LayoutIcon size={14} /> UI layout</CardTitle>
|
|
26
|
+
<CardMeta>Choose how the dashboard's navigation is presented.</CardMeta>
|
|
27
|
+
<div className="layout-row" data-setting-id="ui.layout">
|
|
28
|
+
{LAYOUTS.map((l) => (
|
|
29
|
+
<button
|
|
30
|
+
key={l.id}
|
|
31
|
+
type="button"
|
|
32
|
+
className={cn('layout-card', settings.ui.layout === l.id && 'layout-card-active')}
|
|
33
|
+
onClick={() => patchUi({ layout: l.id })}
|
|
34
|
+
>
|
|
35
|
+
<span className="layout-card-label">{l.label}</span>
|
|
36
|
+
</button>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
<label className="checkbox-row" data-setting-id="ui.showHeader">
|
|
40
|
+
<input
|
|
41
|
+
type="checkbox"
|
|
42
|
+
checked={settings.ui.showHeader}
|
|
43
|
+
onChange={(e) => patchUi({ showHeader: e.target.checked })}
|
|
44
|
+
/>
|
|
45
|
+
<span>Show header</span>
|
|
46
|
+
</label>
|
|
47
|
+
<label className="checkbox-row" data-setting-id="ui.showStatusBar">
|
|
48
|
+
<input
|
|
49
|
+
type="checkbox"
|
|
50
|
+
checked={settings.ui.showStatusBar}
|
|
51
|
+
onChange={(e) => patchUi({ showStatusBar: e.target.checked })}
|
|
52
|
+
/>
|
|
53
|
+
<span>Show status bar</span>
|
|
54
|
+
</label>
|
|
55
|
+
<div className="field" data-setting-id="ui.defaultTab">
|
|
56
|
+
<label className="field-label">Default tab</label>
|
|
57
|
+
<select
|
|
58
|
+
className="select"
|
|
59
|
+
value={settings.ui.defaultTab}
|
|
60
|
+
onChange={(e) => patchUi({ defaultTab: e.target.value })}
|
|
61
|
+
>
|
|
62
|
+
<option value="overview">Overview</option>
|
|
63
|
+
<option value="chat">Chat</option>
|
|
64
|
+
<option value="agents">Agents</option>
|
|
65
|
+
<option value="artifacts">Plans</option>
|
|
66
|
+
<option value="projects">Projects</option>
|
|
67
|
+
<option value="tasks">Tasks</option>
|
|
68
|
+
<option value="config">Config</option>
|
|
69
|
+
<option value="settings">Settings</option>
|
|
70
|
+
<option value="mods">Mods</option>
|
|
71
|
+
<option value="schedules">Schedules</option>
|
|
72
|
+
</select>
|
|
73
|
+
</div>
|
|
74
|
+
</Card>
|
|
75
|
+
|
|
76
|
+
{/* General */}
|
|
77
|
+
<Card id="settings-general" data-section="general">
|
|
78
|
+
<CardTitle>General</CardTitle>
|
|
79
|
+
<CardMeta>Default agent + model override.</CardMeta>
|
|
80
|
+
<div className="field" data-setting-id="defaultAgent">
|
|
81
|
+
<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)}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="field" data-setting-id="defaultModel">
|
|
92
|
+
<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)}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</Card>
|
|
103
|
+
</>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/web/views/settings/HeadroomSection.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { HeadroomSettingsCard } from '../../components/HeadroomSettings';
|
|
4
|
+
import { type HeadroomSettings, type Settings } from '../../lib/types';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HEADROOM_SETTINGS: HeadroomSettings = {
|
|
7
|
+
enabled: true,
|
|
8
|
+
autoInstall: true,
|
|
9
|
+
port: 8787,
|
|
10
|
+
host: '127.0.0.1',
|
|
11
|
+
outputShaper: false,
|
|
12
|
+
telemetry: false,
|
|
13
|
+
budget: 0,
|
|
14
|
+
backend: 'anthropic',
|
|
15
|
+
autoStart: true,
|
|
16
|
+
autoWrap: true,
|
|
17
|
+
routeAllProviders: true,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
settings: Settings;
|
|
22
|
+
setSettings: React.Dispatch<React.SetStateAction<Settings>>;
|
|
23
|
+
setDirty: React.Dispatch<React.SetStateAction<boolean>>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function HeadroomSection({ settings, setSettings, setDirty }: Props) {
|
|
27
|
+
return (
|
|
28
|
+
<HeadroomSettingsCard
|
|
29
|
+
settings={settings.headroom || DEFAULT_HEADROOM_SETTINGS}
|
|
30
|
+
onPatch={(patch) => {
|
|
31
|
+
setSettings((cur) => ({
|
|
32
|
+
...cur,
|
|
33
|
+
headroom: { ...(cur.headroom || DEFAULT_HEADROOM_SETTINGS), ...patch },
|
|
34
|
+
}));
|
|
35
|
+
setDirty(true);
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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';
|
|
4
|
+
import { Card, CardTitle, CardMeta } from '../../components/Card';
|
|
5
|
+
|
|
6
|
+
export function MemorySection() {
|
|
7
|
+
return (
|
|
8
|
+
<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>
|
|
14
|
+
</Card>
|
|
15
|
+
);
|
|
16
|
+
}
|