@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,406 @@
1
+ // src/views/Agents.tsx — editable agent grid with CRUD modal.
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { Bot, Plus, RefreshCw, Pencil, Trash2, Play, Save, X } from 'lucide-react';
4
+ import { Button } from '../components/Button';
5
+ import { Card, CardTitle } from '../components/Card';
6
+ import { EmptyState } from '../components/EmptyState';
7
+ import { Spinner } from '../components/Spinner';
8
+ import { StatusBadge } from '../components/StatusBadge';
9
+ import { useModal } from '../components/Modal';
10
+ import { useToast } from '../components/Toast';
11
+ import { api } from '../lib/api';
12
+ import { formatRelative, truncate } from '../lib/utils';
13
+ import type { Agent, Settings, Snapshot } from '../lib/types';
14
+
15
+ type Props = {
16
+ snapshot: Snapshot;
17
+ settings: Settings;
18
+ activeTab: string;
19
+ setActiveTab: (id: string) => void;
20
+ refreshSnapshot: () => Promise<void>;
21
+ };
22
+
23
+ const TOOL_OPTIONS = ['bash', 'read', 'edit', 'write', 'webfetch', 'websearch', 'task', 'glob', 'grep'];
24
+ const MODELS = [
25
+ 'anthropic/claude-3-5-sonnet',
26
+ 'anthropic/claude-3-5-haiku',
27
+ 'openai/gpt-4o',
28
+ 'openai/gpt-4o-mini',
29
+ 'minimax/MiniMax-M3',
30
+ 'minimax/MiniMax-M2.7',
31
+ ];
32
+
33
+ export function Agents({ snapshot, refreshSnapshot }: Props) {
34
+ const toast = useToast();
35
+ const modal = useModal();
36
+ const [agents, setAgents] = useState<Agent[]>(snapshot.agents || []);
37
+ const [loading, setLoading] = useState(!snapshot.agents);
38
+
39
+ useEffect(() => {
40
+ if (snapshot.agents?.length || snapshot.agents) {
41
+ setAgents(snapshot.agents || []);
42
+ setLoading(false);
43
+ return;
44
+ }
45
+ reload();
46
+ // eslint-disable-next-line react-hooks/exhaustive-deps
47
+ }, [snapshot.agents]);
48
+
49
+ const reload = async () => {
50
+ try {
51
+ const d = await api.get<{ agents: Agent[] }>('/agents');
52
+ setAgents(d.agents || []);
53
+ } catch (err) {
54
+ toast.error(`Agents load failed: ${(err as Error).message}`);
55
+ } finally {
56
+ setLoading(false);
57
+ }
58
+ };
59
+
60
+ const sorted = useMemo(
61
+ () => [...agents].sort((a, b) => a.name.localeCompare(b.name)),
62
+ [agents],
63
+ );
64
+
65
+ const onCreate = () => {
66
+ let nameEl: HTMLInputElement | null = null;
67
+ let descEl: HTMLTextAreaElement | null = null;
68
+ let modelEl: HTMLSelectElement | null = null;
69
+ let modeEl: HTMLSelectElement | null = null;
70
+ let colorEl: HTMLInputElement | null = null;
71
+ let promptEl: HTMLTextAreaElement | null = null;
72
+ let toolsContainer: HTMLDivElement | null = null;
73
+
74
+ modal.open({
75
+ title: 'New agent',
76
+ width: 640,
77
+ children: (
78
+ <div className="agent-form">
79
+ <label className="field-label">Name (a-z, 0-9, dashes)</label>
80
+ <input
81
+ ref={(el) => (nameEl = el)}
82
+ className="input"
83
+ type="text"
84
+ placeholder="my-agent"
85
+ autoFocus
86
+ />
87
+ <label className="field-label">Description</label>
88
+ <input
89
+ ref={(el) => { descEl = el as unknown as HTMLTextAreaElement | null; }}
90
+ className="input"
91
+ type="text"
92
+ placeholder="What does this agent do?"
93
+ />
94
+ <div className="task-form-row">
95
+ <div className="task-form-field">
96
+ <label className="field-label">Model</label>
97
+ <select ref={(el) => (modelEl = el)} className="select" defaultValue="">
98
+ <option value="">(provider default)</option>
99
+ {MODELS.map((m) => (
100
+ <option key={m} value={m}>{m}</option>
101
+ ))}
102
+ </select>
103
+ </div>
104
+ <div className="task-form-field">
105
+ <label className="field-label">Mode</label>
106
+ <select ref={(el) => (modeEl = el)} className="select" defaultValue="subagent">
107
+ <option value="primary">primary</option>
108
+ <option value="subagent">subagent</option>
109
+ <option value="all">all</option>
110
+ </select>
111
+ </div>
112
+ <div className="task-form-field" style={{ flex: '0 0 80px' }}>
113
+ <label className="field-label">Color</label>
114
+ <input ref={(el) => (colorEl = el)} className="input" type="color" defaultValue="#8b5cf6" />
115
+ </div>
116
+ </div>
117
+ <label className="field-label">Tools</label>
118
+ <div ref={(el) => (toolsContainer = el)} className="agent-tools">
119
+ {TOOL_OPTIONS.map((t) => (
120
+ <label key={t} className="checkbox-row">
121
+ <input type="checkbox" value={t} />
122
+ <span>{t}</span>
123
+ </label>
124
+ ))}
125
+ </div>
126
+ <label className="field-label">System prompt</label>
127
+ <textarea
128
+ ref={(el) => (promptEl = el)}
129
+ className="textarea"
130
+ rows={6}
131
+ placeholder="You are a..."
132
+ />
133
+ </div>
134
+ ),
135
+ footer: (
136
+ <div className="modal-footer-actions">
137
+ <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
138
+ <Button
139
+ variant="primary"
140
+ onClick={async () => {
141
+ const name = (nameEl?.value || '').trim();
142
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(name)) {
143
+ toast.warning('Invalid name (a-z, 0-9, dashes).');
144
+ return;
145
+ }
146
+ const tools: string[] = [];
147
+ if (toolsContainer) {
148
+ toolsContainer.querySelectorAll<HTMLInputElement>('input[type="checkbox"]:checked').forEach((cb) => {
149
+ tools.push(cb.value);
150
+ });
151
+ }
152
+ try {
153
+ const created = await api.post<Agent>('/agents', {
154
+ name,
155
+ description: (descEl?.value || '').trim(),
156
+ model: modelEl?.value || '',
157
+ mode: modeEl?.value || 'subagent',
158
+ color: colorEl?.value || '',
159
+ tools,
160
+ prompt: promptEl?.value || '',
161
+ });
162
+ setAgents((cur) => [...cur, created]);
163
+ toast.success('Agent created.');
164
+ modal.close();
165
+ await refreshSnapshot();
166
+ } catch (err) {
167
+ toast.error(`Create failed: ${(err as Error).message}`);
168
+ }
169
+ }}
170
+ >
171
+ <Save size={12} /> Create
172
+ </Button>
173
+ </div>
174
+ ),
175
+ });
176
+ };
177
+
178
+ const onEdit = async (a: Agent) => {
179
+ let descEl: HTMLTextAreaElement | null = null;
180
+ let modelEl: HTMLSelectElement | null = null;
181
+ let modeEl: HTMLSelectElement | null = null;
182
+ let colorEl: HTMLInputElement | null = null;
183
+ let promptEl: HTMLTextAreaElement | null = null;
184
+ let toolsContainer: HTMLDivElement | null = null;
185
+ try {
186
+ const full = await api.get<Agent>(`/agents/${encodeURIComponent(a.name)}`);
187
+ modal.open({
188
+ title: `Edit ${a.name}`,
189
+ width: 640,
190
+ children: (
191
+ <div className="agent-form">
192
+ <div className="muted">
193
+ File: <code>{full.path}</code>
194
+ </div>
195
+ <label className="field-label">Description</label>
196
+ <input
197
+ ref={(el) => { descEl = el as unknown as HTMLTextAreaElement | null; }}
198
+ className="input"
199
+ type="text"
200
+ defaultValue={full.description}
201
+ />
202
+ <div className="task-form-row">
203
+ <div className="task-form-field">
204
+ <label className="field-label">Model</label>
205
+ <select ref={(el) => (modelEl = el)} className="select" defaultValue={full.model}>
206
+ <option value="">(provider default)</option>
207
+ {MODELS.map((m) => (
208
+ <option key={m} value={m}>{m}</option>
209
+ ))}
210
+ </select>
211
+ </div>
212
+ <div className="task-form-field">
213
+ <label className="field-label">Mode</label>
214
+ <select ref={(el) => (modeEl = el)} className="select" defaultValue={full.mode || 'subagent'}>
215
+ <option value="primary">primary</option>
216
+ <option value="subagent">subagent</option>
217
+ <option value="all">all</option>
218
+ </select>
219
+ </div>
220
+ <div className="task-form-field" style={{ flex: '0 0 80px' }}>
221
+ <label className="field-label">Color</label>
222
+ <input ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={full.color || '#8b5cf6'} />
223
+ </div>
224
+ </div>
225
+ <label className="field-label">Tools</label>
226
+ <div ref={(el) => (toolsContainer = el)} className="agent-tools">
227
+ {TOOL_OPTIONS.map((t) => (
228
+ <label key={t} className="checkbox-row">
229
+ <input type="checkbox" value={t} defaultChecked={full.tools?.includes(t)} />
230
+ <span>{t}</span>
231
+ </label>
232
+ ))}
233
+ </div>
234
+ <label className="field-label">System prompt</label>
235
+ <textarea
236
+ ref={(el) => (promptEl = el)}
237
+ className="textarea"
238
+ rows={8}
239
+ defaultValue={full.prompt || ''}
240
+ />
241
+ </div>
242
+ ),
243
+ footer: (
244
+ <div className="modal-footer-actions">
245
+ <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
246
+ <Button
247
+ variant="primary"
248
+ onClick={async () => {
249
+ const tools: string[] = [];
250
+ if (toolsContainer) {
251
+ toolsContainer.querySelectorAll<HTMLInputElement>('input[type="checkbox"]:checked').forEach((cb) => {
252
+ tools.push(cb.value);
253
+ });
254
+ }
255
+ try {
256
+ const updated = await api.put<Agent>(
257
+ `/agents/${encodeURIComponent(a.name)}`,
258
+ {
259
+ description: (descEl?.value || '').trim(),
260
+ model: modelEl?.value || '',
261
+ mode: modeEl?.value || 'subagent',
262
+ color: colorEl?.value || '',
263
+ tools,
264
+ prompt: promptEl?.value || '',
265
+ },
266
+ );
267
+ setAgents((cur) => cur.map((x) => (x.name === a.name ? updated : x)));
268
+ toast.success('Agent saved.');
269
+ modal.close();
270
+ await refreshSnapshot();
271
+ } catch (err) {
272
+ toast.error(`Save failed: ${(err as Error).message}`);
273
+ }
274
+ }}
275
+ >
276
+ <Save size={12} /> Save
277
+ </Button>
278
+ </div>
279
+ ),
280
+ });
281
+ } catch (err) {
282
+ toast.error(`Load failed: ${(err as Error).message}`);
283
+ }
284
+ };
285
+
286
+ const onDelete = async (a: Agent) => {
287
+ if (!confirm(`Delete agent "${a.name}"? This removes ${a.path}.`)) return;
288
+ try {
289
+ await api.del(`/agents/${encodeURIComponent(a.name)}`);
290
+ setAgents((cur) => cur.filter((x) => x.name !== a.name));
291
+ toast.success('Agent deleted.');
292
+ } catch (err) {
293
+ toast.error(`Delete failed: ${(err as Error).message}`);
294
+ }
295
+ };
296
+
297
+ const onInvoke = async (a: Agent) => {
298
+ let promptEl: HTMLTextAreaElement | null = null;
299
+ modal.open({
300
+ title: `Invoke ${a.name}`,
301
+ children: (
302
+ <div className="invoke-form">
303
+ <p className="muted invoke-form-meta mono">{a.model || '—'} · {a.path}</p>
304
+ <p className="invoke-form-desc">{a.description}</p>
305
+ <label className="field-label" htmlFor="invoke-prompt">Prompt</label>
306
+ <textarea
307
+ ref={(el) => (promptEl = el)}
308
+ id="invoke-prompt"
309
+ className="textarea"
310
+ rows={5}
311
+ placeholder="What should this agent do?"
312
+ autoFocus
313
+ />
314
+ </div>
315
+ ),
316
+ footer: (
317
+ <div className="modal-footer-actions">
318
+ <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
319
+ <Button
320
+ variant="primary"
321
+ onClick={async () => {
322
+ const prompt = (promptEl?.value || '').trim();
323
+ if (!prompt) {
324
+ toast.warning('Prompt is required.');
325
+ return;
326
+ }
327
+ try {
328
+ await api.post(`/agents/${encodeURIComponent(a.name)}/invoke`, { prompt });
329
+ toast.success(`Invoked ${a.name}.`);
330
+ modal.close();
331
+ } catch (err) {
332
+ toast.error(`Invoke failed: ${(err as Error).message}`);
333
+ }
334
+ }}
335
+ >
336
+ <Play size={14} /> Invoke
337
+ </Button>
338
+ </div>
339
+ ),
340
+ });
341
+ };
342
+
343
+ return (
344
+ <div className="view view-agents">
345
+ <header className="view-header">
346
+ <div className="view-header-text">
347
+ <h2 className="view-title">
348
+ <Bot size={18} /> Agents ({sorted.length})
349
+ </h2>
350
+ <p className="view-subtitle">
351
+ The Norse pantheon — click <kbd>Edit</kbd> to modify or <kbd>Invoke</kbd> to dispatch.
352
+ </p>
353
+ </div>
354
+ <div className="view-actions">
355
+ <Button variant="secondary" size="sm" onClick={reload}>
356
+ <RefreshCw size={14} /> Refresh
357
+ </Button>
358
+ <Button variant="primary" size="sm" onClick={onCreate}>
359
+ <Plus size={14} /> New agent
360
+ </Button>
361
+ </div>
362
+ </header>
363
+
364
+ {loading ? (
365
+ <div className="view-loading"><Spinner size="lg" /></div>
366
+ ) : sorted.length === 0 ? (
367
+ <EmptyState
368
+ icon={<Bot size={32} />}
369
+ title="No agents found"
370
+ message="Run bizar in the terminal to install Bizar."
371
+ />
372
+ ) : (
373
+ <div className="agent-grid">
374
+ {sorted.map((a) => (
375
+ <Card key={a.name} variant="elevated" interactive className="agent-card">
376
+ <div className="agent-card-head">
377
+ <div className="agent-card-name">{a.name}</div>
378
+ <StatusBadge kind={a.mode === 'primary' ? 'accent' : 'neutral'}>
379
+ {a.mode || 'agent'}
380
+ </StatusBadge>
381
+ </div>
382
+ <p className="agent-card-desc">{truncate(a.description, 200)}</p>
383
+ <div className="agent-card-meta">
384
+ <span className="mono" title={a.model || ''}>
385
+ {a.model || '—'}
386
+ </span>
387
+ <span className="tabular-nums muted">{formatRelative(a.mtime)}</span>
388
+ </div>
389
+ <div className="agent-card-actions">
390
+ <Button variant="primary" size="sm" onClick={() => onInvoke(a)}>
391
+ <Play size={12} /> Invoke
392
+ </Button>
393
+ <Button variant="secondary" size="sm" onClick={() => onEdit(a)}>
394
+ <Pencil size={12} /> Edit
395
+ </Button>
396
+ <Button variant="ghost" size="sm" onClick={() => onDelete(a)}>
397
+ <Trash2 size={12} /> Delete
398
+ </Button>
399
+ </div>
400
+ </Card>
401
+ ))}
402
+ </div>
403
+ )}
404
+ </div>
405
+ );
406
+ }