@polderlabs/bizar-dash 3.0.0

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 (59) hide show
  1. package/dist/assets/index-B5X9g8B4.css +1 -0
  2. package/dist/assets/index-LqQuSp9d.js +388 -0
  3. package/dist/assets/index-LqQuSp9d.js.map +1 -0
  4. package/dist/index.html +18 -0
  5. package/package.json +67 -0
  6. package/src/cli.mjs +228 -0
  7. package/src/server/agents-store.mjs +190 -0
  8. package/src/server/api.mjs +913 -0
  9. package/src/server/browser.mjs +40 -0
  10. package/src/server/diagnostics-store.mjs +138 -0
  11. package/src/server/mods-loader.mjs +361 -0
  12. package/src/server/projects-store.mjs +198 -0
  13. package/src/server/providers-store.mjs +183 -0
  14. package/src/server/schedules-runner.mjs +150 -0
  15. package/src/server/schedules-store.mjs +233 -0
  16. package/src/server/search-store.mjs +120 -0
  17. package/src/server/server.mjs +388 -0
  18. package/src/server/state.mjs +357 -0
  19. package/src/server/tailscale-store.mjs +113 -0
  20. package/src/server/tasks-store.mjs +275 -0
  21. package/src/server/tui.mjs +844 -0
  22. package/src/server/watcher.mjs +81 -0
  23. package/src/web/App.tsx +316 -0
  24. package/src/web/components/Button.tsx +55 -0
  25. package/src/web/components/Card.tsx +40 -0
  26. package/src/web/components/EmptyState.tsx +30 -0
  27. package/src/web/components/Modal.tsx +137 -0
  28. package/src/web/components/SearchModal.tsx +185 -0
  29. package/src/web/components/Spinner.tsx +19 -0
  30. package/src/web/components/StatusBadge.tsx +25 -0
  31. package/src/web/components/Tag.tsx +28 -0
  32. package/src/web/components/Toast.tsx +142 -0
  33. package/src/web/components/Topbar.tsx +203 -0
  34. package/src/web/index.html +17 -0
  35. package/src/web/lib/api.ts +71 -0
  36. package/src/web/lib/markdown.tsx +59 -0
  37. package/src/web/lib/types.ts +388 -0
  38. package/src/web/lib/utils.ts +79 -0
  39. package/src/web/lib/ws.ts +132 -0
  40. package/src/web/main.tsx +12 -0
  41. package/src/web/styles/main.css +3148 -0
  42. package/src/web/views/Agents.tsx +406 -0
  43. package/src/web/views/Chat.tsx +527 -0
  44. package/src/web/views/Config.tsx +683 -0
  45. package/src/web/views/Mods.tsx +350 -0
  46. package/src/web/views/Overview.tsx +350 -0
  47. package/src/web/views/Plans.tsx +667 -0
  48. package/src/web/views/Schedules.tsx +299 -0
  49. package/src/web/views/Settings.tsx +571 -0
  50. package/src/web/views/Tasks.tsx +761 -0
  51. package/templates/mod/FORMAT.md +76 -0
  52. package/templates/mod/hello-mod/README.md +19 -0
  53. package/templates/mod/hello-mod/agents/greeter.md +8 -0
  54. package/templates/mod/hello-mod/commands/hello.md +6 -0
  55. package/templates/mod/hello-mod/mod.json +20 -0
  56. package/templates/mod/hello-mod/routes/ping.mjs +9 -0
  57. package/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  58. package/tsconfig.json +23 -0
  59. package/vite.config.ts +24 -0
@@ -0,0 +1,571 @@
1
+ // src/views/Settings.tsx — v3 settings: theme colors, UI layout, defaults, Tailscale, service.
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ Sliders,
5
+ Save,
6
+ RefreshCw,
7
+ Sun,
8
+ Moon,
9
+ Monitor,
10
+ Info,
11
+ Globe,
12
+ Palette,
13
+ Layout as LayoutIcon,
14
+ Server as ServerIcon,
15
+ RotateCcw,
16
+ Plug,
17
+ } from 'lucide-react';
18
+ import { Button } from '../components/Button';
19
+ import { Card, CardTitle, CardMeta } from '../components/Card';
20
+ import { useToast } from '../components/Toast';
21
+ import { api } from '../lib/api';
22
+ import { cn } from '../lib/utils';
23
+ import {
24
+ applyTheme,
25
+ applyThemeTokens,
26
+ type Settings,
27
+ type SettingsResponse,
28
+ type Snapshot,
29
+ type ThemeName,
30
+ type TailscaleStatus,
31
+ } from '../lib/types';
32
+
33
+ type Props = {
34
+ snapshot: Snapshot;
35
+ settings: Settings;
36
+ activeTab: string;
37
+ setActiveTab: (id: string) => void;
38
+ refreshSnapshot: () => Promise<void>;
39
+ };
40
+
41
+ const THEMES: { id: ThemeName; label: string; Icon: typeof Sun }[] = [
42
+ { id: 'dark', label: 'Dark', Icon: Moon },
43
+ { id: 'light', label: 'Light', Icon: Sun },
44
+ { id: 'system', label: 'System', Icon: Monitor },
45
+ ];
46
+
47
+ const FONT_FAMILIES = [
48
+ 'Inter',
49
+ 'system-ui',
50
+ 'Segoe UI',
51
+ 'Roboto',
52
+ 'JetBrains Mono',
53
+ 'SF Mono',
54
+ 'Cascadia Code',
55
+ ];
56
+
57
+ const LAYOUTS = [
58
+ { id: 'topnav', label: 'Top nav' },
59
+ { id: 'sidebar', label: 'Sidebar' },
60
+ { id: 'both', label: 'Both' },
61
+ ] as const;
62
+
63
+ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
64
+ const toast = useToast();
65
+ const [settings, setSettings] = useState<Settings>(initial);
66
+ const [dirty, setDirty] = useState(false);
67
+ const [saving, setSaving] = useState(false);
68
+ const [tailscale, setTailscale] = useState<TailscaleStatus | null>(null);
69
+
70
+ useEffect(() => {
71
+ setSettings(initial);
72
+ setDirty(false);
73
+ if (initial.theme) applyThemeTokens(initial.theme);
74
+ }, [initial]);
75
+
76
+ useEffect(() => {
77
+ api.get<TailscaleStatus>('/tailscale/status').then(setTailscale).catch(() => undefined);
78
+ }, []);
79
+
80
+ const patchTheme = (patch: Partial<Settings['theme']>) => {
81
+ setSettings((cur) => {
82
+ const next = { ...cur, theme: { ...cur.theme, ...patch } };
83
+ applyThemeTokens(next.theme);
84
+ return next;
85
+ });
86
+ setDirty(true);
87
+ };
88
+
89
+ const patchUi = (patch: Partial<Settings['ui']>) => {
90
+ setSettings((cur) => ({ ...cur, ui: { ...cur.ui, ...patch } }));
91
+ setDirty(true);
92
+ };
93
+
94
+ const patchTop = <K extends keyof Settings>(key: K, value: Settings[K]) => {
95
+ setSettings((cur) => ({ ...cur, [key]: value }));
96
+ setDirty(true);
97
+ };
98
+
99
+ const onSave = async () => {
100
+ setSaving(true);
101
+ try {
102
+ const r = await api.put<SettingsResponse>('/settings', settings);
103
+ setSettings(r.data);
104
+ setDirty(false);
105
+ applyTheme(r.data.theme);
106
+ applyThemeTokens(r.data.theme);
107
+ toast.success('Settings saved.');
108
+ await refreshSnapshot();
109
+ } catch (err) {
110
+ toast.error(`Save failed: ${(err as Error).message}`);
111
+ } finally {
112
+ setSaving(false);
113
+ }
114
+ };
115
+
116
+ const onReload = async () => {
117
+ try {
118
+ const r = await api.get<SettingsResponse>('/settings');
119
+ setSettings(r.data);
120
+ setDirty(false);
121
+ applyTheme(r.data.theme);
122
+ applyThemeTokens(r.data.theme);
123
+ toast.info('Settings reloaded.', 1500);
124
+ } catch (err) {
125
+ toast.error(`Reload failed: ${(err as Error).message}`);
126
+ }
127
+ };
128
+
129
+ const onReset = async () => {
130
+ if (!confirm('Reset all settings to defaults?')) return;
131
+ try {
132
+ const r = await api.post<SettingsResponse>('/settings/reset');
133
+ setSettings(r.data);
134
+ setDirty(false);
135
+ applyTheme(r.data.theme);
136
+ applyThemeTokens(r.data.theme);
137
+ toast.success('Settings reset.');
138
+ await refreshSnapshot();
139
+ } catch (err) {
140
+ toast.error(`Reset failed: ${(err as Error).message}`);
141
+ }
142
+ };
143
+
144
+ const onTailscaleToggle = async () => {
145
+ try {
146
+ if (tailscale?.settings.enabled) {
147
+ await api.post('/tailscale/disable');
148
+ toast.success('Tailscale serve disabled.');
149
+ } else {
150
+ await api.post('/tailscale/enable', {
151
+ port: tailscale?.settings.port || 4321,
152
+ https: tailscale?.settings.https !== false,
153
+ hostname: tailscale?.settings.hostname || '',
154
+ });
155
+ toast.success('Tailscale serve enabled.');
156
+ }
157
+ const r = await api.get<TailscaleStatus>('/tailscale/status');
158
+ setTailscale(r);
159
+ } catch (err) {
160
+ toast.error(`Tailscale failed: ${(err as Error).message}`);
161
+ }
162
+ };
163
+
164
+ const about = settings.about || {
165
+ version: '3.0.0',
166
+ homepage: 'https://github.com/DrB0rk/BizarHarness',
167
+ license: 'MIT',
168
+ };
169
+
170
+ return (
171
+ <div className="view view-settings">
172
+ <header className="view-header">
173
+ <div className="view-header-text">
174
+ <h2 className="view-title">
175
+ <Sliders size={18} /> Settings
176
+ </h2>
177
+ <p className="view-subtitle">
178
+ Personal preferences. Changes are saved to{' '}
179
+ <code>~/.config/bizar/settings.json</code>.
180
+ </p>
181
+ </div>
182
+ <div className="view-actions">
183
+ <Button variant="ghost" size="sm" onClick={onReset}>
184
+ <RotateCcw size={14} /> Reset
185
+ </Button>
186
+ <Button variant="secondary" size="sm" onClick={onReload}>
187
+ <RefreshCw size={14} /> Reload
188
+ </Button>
189
+ <Button
190
+ variant="primary"
191
+ size="sm"
192
+ disabled={!dirty || saving}
193
+ onClick={onSave}
194
+ >
195
+ {saving ? <span className="btn-spinner" /> : <Save size={14} />}
196
+ {saving ? 'Saving…' : 'Save'}
197
+ </Button>
198
+ </div>
199
+ </header>
200
+
201
+ <div className="settings-grid">
202
+ <Card>
203
+ <CardTitle><Palette size={14} /> Theme</CardTitle>
204
+ <CardMeta>Mode, accent, and colors. Live preview as you tweak.</CardMeta>
205
+
206
+ <div className="field">
207
+ <label className="field-label">Mode</label>
208
+ <div className="theme-row">
209
+ {THEMES.map(({ id, label, Icon }) => {
210
+ const active = settings.theme.mode === id;
211
+ return (
212
+ <button
213
+ key={id}
214
+ type="button"
215
+ className={cn('theme-card', active && 'theme-card-active')}
216
+ onClick={() => patchTheme({ mode: id })}
217
+ >
218
+ <Icon size={16} />
219
+ <span className="theme-card-label">{label}</span>
220
+ <span
221
+ className={cn(
222
+ 'theme-card-swatch',
223
+ `theme-card-swatch-${id}`,
224
+ )}
225
+ />
226
+ </button>
227
+ );
228
+ })}
229
+ </div>
230
+ </div>
231
+
232
+ <div className="theme-colors">
233
+ <div className="field">
234
+ <label className="field-label">Accent</label>
235
+ <div className="color-row">
236
+ <input
237
+ type="color"
238
+ className="input color-input"
239
+ value={settings.theme.accent}
240
+ onChange={(e) => patchTheme({ accent: e.target.value })}
241
+ />
242
+ <input
243
+ type="text"
244
+ className="input"
245
+ value={settings.theme.accent}
246
+ onChange={(e) => patchTheme({ accent: e.target.value })}
247
+ />
248
+ </div>
249
+ </div>
250
+ <div className="field">
251
+ <label className="field-label">Success</label>
252
+ <div className="color-row">
253
+ <input
254
+ type="color"
255
+ className="input color-input"
256
+ value={settings.theme.success}
257
+ onChange={(e) => patchTheme({ success: e.target.value })}
258
+ />
259
+ <input
260
+ type="text"
261
+ className="input"
262
+ value={settings.theme.success}
263
+ onChange={(e) => patchTheme({ success: e.target.value })}
264
+ />
265
+ </div>
266
+ </div>
267
+ <div className="field">
268
+ <label className="field-label">Warning</label>
269
+ <div className="color-row">
270
+ <input
271
+ type="color"
272
+ className="input color-input"
273
+ value={settings.theme.warning}
274
+ onChange={(e) => patchTheme({ warning: e.target.value })}
275
+ />
276
+ <input
277
+ type="text"
278
+ className="input"
279
+ value={settings.theme.warning}
280
+ onChange={(e) => patchTheme({ warning: e.target.value })}
281
+ />
282
+ </div>
283
+ </div>
284
+ <div className="field">
285
+ <label className="field-label">Error</label>
286
+ <div className="color-row">
287
+ <input
288
+ type="color"
289
+ className="input color-input"
290
+ value={settings.theme.error}
291
+ onChange={(e) => patchTheme({ error: e.target.value })}
292
+ />
293
+ <input
294
+ type="text"
295
+ className="input"
296
+ value={settings.theme.error}
297
+ onChange={(e) => patchTheme({ error: e.target.value })}
298
+ />
299
+ </div>
300
+ </div>
301
+ <div className="field">
302
+ <label className="field-label">Info</label>
303
+ <div className="color-row">
304
+ <input
305
+ type="color"
306
+ className="input color-input"
307
+ value={settings.theme.info}
308
+ onChange={(e) => patchTheme({ info: e.target.value })}
309
+ />
310
+ <input
311
+ type="text"
312
+ className="input"
313
+ value={settings.theme.info}
314
+ onChange={(e) => patchTheme({ info: e.target.value })}
315
+ />
316
+ </div>
317
+ </div>
318
+ </div>
319
+
320
+ <div className="field">
321
+ <label className="field-label">Font family</label>
322
+ <select
323
+ className="select"
324
+ value={settings.theme.fontFamily}
325
+ onChange={(e) => patchTheme({ fontFamily: e.target.value })}
326
+ >
327
+ {FONT_FAMILIES.map((f) => (
328
+ <option key={f} value={f}>{f}</option>
329
+ ))}
330
+ </select>
331
+ </div>
332
+ <div className="field">
333
+ <label className="field-label">Font size: {settings.theme.fontSize}px</label>
334
+ <input
335
+ type="range"
336
+ min={12}
337
+ max={20}
338
+ value={settings.theme.fontSize}
339
+ onChange={(e) => patchTheme({ fontSize: Number(e.target.value) })}
340
+ />
341
+ </div>
342
+ <label className="checkbox-row">
343
+ <input
344
+ type="checkbox"
345
+ checked={settings.theme.compactMode}
346
+ onChange={(e) => patchTheme({ compactMode: e.target.checked })}
347
+ />
348
+ <span>Compact mode (denser UI)</span>
349
+ </label>
350
+ <label className="checkbox-row">
351
+ <input
352
+ type="checkbox"
353
+ checked={settings.theme.animations}
354
+ onChange={(e) => patchTheme({ animations: e.target.checked })}
355
+ />
356
+ <span>Enable animations</span>
357
+ </label>
358
+ </Card>
359
+
360
+ <Card>
361
+ <CardTitle><LayoutIcon size={14} /> UI layout</CardTitle>
362
+ <CardMeta>Choose how the dashboard's navigation is presented.</CardMeta>
363
+ <div className="layout-row">
364
+ {LAYOUTS.map((l) => (
365
+ <button
366
+ key={l.id}
367
+ type="button"
368
+ className={cn('layout-card', settings.ui.layout === l.id && 'layout-card-active')}
369
+ onClick={() => patchUi({ layout: l.id })}
370
+ >
371
+ <span className="layout-card-label">{l.label}</span>
372
+ </button>
373
+ ))}
374
+ </div>
375
+ <label className="checkbox-row">
376
+ <input
377
+ type="checkbox"
378
+ checked={settings.ui.showHeader}
379
+ onChange={(e) => patchUi({ showHeader: e.target.checked })}
380
+ />
381
+ <span>Show header</span>
382
+ </label>
383
+ <label className="checkbox-row">
384
+ <input
385
+ type="checkbox"
386
+ checked={settings.ui.showStatusBar}
387
+ onChange={(e) => patchUi({ showStatusBar: e.target.checked })}
388
+ />
389
+ <span>Show status bar</span>
390
+ </label>
391
+ <div className="field">
392
+ <label className="field-label">Default tab</label>
393
+ <select
394
+ className="select"
395
+ value={settings.ui.defaultTab}
396
+ onChange={(e) => patchUi({ defaultTab: e.target.value })}
397
+ >
398
+ <option value="overview">Overview</option>
399
+ <option value="chat">Chat</option>
400
+ <option value="agents">Agents</option>
401
+ <option value="plans">Plans</option>
402
+ <option value="projects">Projects</option>
403
+ <option value="tasks">Tasks</option>
404
+ <option value="config">Config</option>
405
+ <option value="settings">Settings</option>
406
+ <option value="mods">Mods</option>
407
+ <option value="schedules">Schedules</option>
408
+ </select>
409
+ </div>
410
+ </Card>
411
+
412
+ <Card>
413
+ <CardTitle>General</CardTitle>
414
+ <CardMeta>Default agent + model override.</CardMeta>
415
+ <div className="field">
416
+ <label className="field-label" htmlFor="set-default-agent">Default agent</label>
417
+ <input
418
+ id="set-default-agent"
419
+ className="input"
420
+ type="text"
421
+ placeholder="e.g. odin"
422
+ value={settings.defaultAgent || ''}
423
+ onChange={(e) => patchTop('defaultAgent', e.target.value)}
424
+ />
425
+ </div>
426
+ <div className="field">
427
+ <label className="field-label" htmlFor="set-default-model">Model override</label>
428
+ <input
429
+ id="set-default-model"
430
+ className="input"
431
+ type="text"
432
+ placeholder="(leave empty for provider default)"
433
+ value={settings.defaultModel || ''}
434
+ onChange={(e) => patchTop('defaultModel', e.target.value)}
435
+ />
436
+ </div>
437
+ </Card>
438
+
439
+ <Card>
440
+ <CardTitle><ServerIcon size={14} /> Service</CardTitle>
441
+ <CardMeta>Background daemon that runs schedules.</CardMeta>
442
+ {tailscale ? (
443
+ <div className="service-card">
444
+ <p>
445
+ Status: <strong>{tailscale?.settings.enabled ? 'enabled' : 'disabled'}</strong>
446
+ {' '}· Tailscale installed: <strong>{tailscale.installed ? 'yes' : 'no'}</strong>
447
+ {' '}· authenticated: <strong>{tailscale.authenticated ? 'yes' : 'no'}</strong>
448
+ </p>
449
+ <p className="muted">
450
+ Use <code>bizar service start</code> / <code>bizar service stop</code> in
451
+ your terminal to control the daemon.
452
+ </p>
453
+ </div>
454
+ ) : (
455
+ <p className="muted">Loading service status…</p>
456
+ )}
457
+ </Card>
458
+
459
+ <Card>
460
+ <CardTitle><Plug size={14} /> Tailscale serve</CardTitle>
461
+ <CardMeta>Expose the dashboard over your Tailscale network.</CardMeta>
462
+ {tailscale ? (
463
+ <>
464
+ <p>
465
+ Installed: <strong>{tailscale.installed ? 'yes' : 'no'}</strong>{' '}
466
+ {tailscale.version && <span className="muted">({tailscale.version})</span>}
467
+ </p>
468
+ <p>
469
+ Authenticated: <strong>{tailscale.authenticated ? 'yes' : 'no'}</strong>
470
+ </p>
471
+ <p>
472
+ Serve enabled: <strong>{tailscale.settings.enabled ? 'yes' : 'no'}</strong>
473
+ </p>
474
+ <div className="task-form-row">
475
+ <div className="task-form-field">
476
+ <label className="field-label">Port</label>
477
+ <input
478
+ type="number"
479
+ className="input"
480
+ defaultValue={tailscale.settings.port}
481
+ onChange={(e) => {
482
+ // Update local state via patchUi is unrelated; just inform
483
+ }}
484
+ />
485
+ </div>
486
+ <div className="task-form-field">
487
+ <label className="field-label">Use HTTPS</label>
488
+ <input
489
+ type="checkbox"
490
+ defaultChecked={tailscale.settings.https === true}
491
+ />
492
+ </div>
493
+ </div>
494
+ <Button variant="primary" onClick={onTailscaleToggle}>
495
+ {tailscale.settings.enabled ? 'Disable serve' : 'Enable serve'}
496
+ </Button>
497
+ </>
498
+ ) : (
499
+ <p className="muted">Loading Tailscale status…</p>
500
+ )}
501
+ </Card>
502
+
503
+ <Card>
504
+ <CardTitle>Notifications</CardTitle>
505
+ <CardMeta>Toast triggers inside the dashboard.</CardMeta>
506
+ <label className="checkbox-row">
507
+ <input
508
+ type="checkbox"
509
+ checked={!!settings.notifications.onAgentComplete}
510
+ onChange={(e) =>
511
+ setSettings((cur) => ({
512
+ ...cur,
513
+ notifications: { ...cur.notifications, onAgentComplete: e.target.checked },
514
+ }))
515
+ }
516
+ />
517
+ <span>Notify when an agent invocation completes</span>
518
+ </label>
519
+ <label className="checkbox-row">
520
+ <input
521
+ type="checkbox"
522
+ checked={!!settings.notifications.onPlanApproval}
523
+ onChange={(e) =>
524
+ setSettings((cur) => ({
525
+ ...cur,
526
+ notifications: { ...cur.notifications, onPlanApproval: e.target.checked },
527
+ }))
528
+ }
529
+ />
530
+ <span>Notify when a plan needs approval</span>
531
+ </label>
532
+ </Card>
533
+
534
+ <Card>
535
+ <CardTitle><Globe size={14} /> Dashboard</CardTitle>
536
+ <CardMeta>Controls how <code>bizar</code> starts up.</CardMeta>
537
+ <label className="checkbox-row">
538
+ <input
539
+ type="checkbox"
540
+ checked={settings.dashboard.autoLaunchWeb !== false}
541
+ onChange={(e) =>
542
+ setSettings((cur) => ({
543
+ ...cur,
544
+ dashboard: { ...cur.dashboard, autoLaunchWeb: e.target.checked },
545
+ }))
546
+ }
547
+ />
548
+ <span>Auto-launch web UI alongside TUI</span>
549
+ </label>
550
+ </Card>
551
+ </div>
552
+
553
+ <Card>
554
+ <CardTitle><Info size={14} /> About</CardTitle>
555
+ <CardMeta>Build metadata.</CardMeta>
556
+ <dl className="about-table">
557
+ <dt>Version</dt>
558
+ <dd className="mono">{about.version}</dd>
559
+ <dt>Homepage</dt>
560
+ <dd>
561
+ <a href={about.homepage} target="_blank" rel="noopener noreferrer">
562
+ {about.homepage}
563
+ </a>
564
+ </dd>
565
+ <dt>License</dt>
566
+ <dd>{about.license}</dd>
567
+ </dl>
568
+ </Card>
569
+ </div>
570
+ );
571
+ }