@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.
Files changed (75) hide show
  1. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +361 -0
  2. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
  4. package/bizar-dash/dist/assets/{mobile-CWqPoGaT.js → mobile-BK8-ythT.js} +2 -2
  5. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
  6. package/bizar-dash/dist/assets/{mobile-i4Uv9eW8.js → mobile-Chvf9u_B.js} +1 -1
  7. package/bizar-dash/dist/assets/{mobile-i4Uv9eW8.js.map → mobile-Chvf9u_B.js.map} +1 -1
  8. package/bizar-dash/dist/index.html +3 -3
  9. package/bizar-dash/dist/mobile.html +2 -2
  10. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  11. package/bizar-dash/skills/publishing/SKILL.md +146 -0
  12. package/bizar-dash/src/server/api.mjs +8 -0
  13. package/bizar-dash/src/server/backup-store.mjs +525 -0
  14. package/bizar-dash/src/server/digest-store.mjs +558 -0
  15. package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
  16. package/bizar-dash/src/server/routes/backup.mjs +112 -0
  17. package/bizar-dash/src/server/routes/chat.mjs +14 -0
  18. package/bizar-dash/src/server/routes/digests.mjs +82 -0
  19. package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
  20. package/bizar-dash/src/server/schedules-runner.mjs +126 -0
  21. package/bizar-dash/src/server/server.mjs +17 -0
  22. package/bizar-dash/src/web/App.tsx +8 -1
  23. package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
  24. package/bizar-dash/src/web/components/SearchModal.tsx +3 -1
  25. package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
  26. package/bizar-dash/src/web/styles/main.css +70 -8
  27. package/bizar-dash/src/web/views/Activity.tsx +11 -1
  28. package/bizar-dash/src/web/views/Agents.tsx +57 -42
  29. package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
  30. package/bizar-dash/src/web/views/Chat.tsx +8 -0
  31. package/bizar-dash/src/web/views/History.tsx +4 -1
  32. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
  33. package/bizar-dash/src/web/views/Mods.tsx +30 -17
  34. package/bizar-dash/src/web/views/Overview.tsx +8 -1
  35. package/bizar-dash/src/web/views/Providers.tsx +16 -16
  36. package/bizar-dash/src/web/views/Schedules.tsx +33 -15
  37. package/bizar-dash/src/web/views/Settings.tsx +97 -1751
  38. package/bizar-dash/src/web/views/Skills.tsx +4 -1
  39. package/bizar-dash/src/web/views/Tasks.tsx +11 -2
  40. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
  41. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
  42. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
  43. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
  44. package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
  45. package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
  46. package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
  47. package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
  48. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
  49. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
  50. package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
  51. package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
  52. package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
  53. package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
  54. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
  55. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
  56. package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
  57. package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
  58. package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
  59. package/bizar-dash/tests/a11y.test.tsx +206 -0
  60. package/bizar-dash/tests/backup-restore.test.mjs +217 -0
  61. package/bizar-dash/tests/backup-restore.test.tsx +123 -0
  62. package/bizar-dash/tests/backup-store.test.mjs +300 -0
  63. package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
  64. package/bizar-dash/tests/digest-generation.test.mjs +191 -0
  65. package/bizar-dash/tests/digest-store.test.mjs +264 -0
  66. package/bizar-dash/tests/rate-limit.test.mjs +298 -0
  67. package/cli/bin.mjs +96 -2
  68. package/cli/commands/minimax.mjs +20 -0
  69. package/cli/commands/util.mjs +154 -1
  70. package/cli/digest.mjs +149 -0
  71. package/package.json +1 -1
  72. package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
  73. package/bizar-dash/dist/assets/main-DGGq-iZI.js +0 -361
  74. package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +0 -1
  75. 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&#10;/srv/projects"
147
+ value={(settings.dashboard.allowedRoots ?? []).join('\n')}
148
+ onChange={(e) => {
149
+ const lines = e.target.value
150
+ .split('\n')
151
+ .map((l) => l.trim())
152
+ .filter(Boolean);
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}: &quot;{line}&quot; — 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}: &quot;{line}&quot; — 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 (&gt;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 &amp; 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 &amp; 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
+ }