@polderlabs/bizar 2.4.0 → 2.6.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 (42) hide show
  1. package/cli/bin.mjs +73 -0
  2. package/cli/dashboard/api.mjs +473 -0
  3. package/cli/dashboard/browser.mjs +40 -0
  4. package/cli/dashboard/server.mjs +366 -0
  5. package/cli/dashboard/state.mjs +438 -0
  6. package/cli/dashboard/tasks-store.mjs +203 -0
  7. package/cli/dashboard/watcher.mjs +81 -0
  8. package/cli/dashboard.mjs +97 -0
  9. package/config/commands/bizar.md +13 -39
  10. package/dist/assets/index-BVvY22Gt.css +1 -0
  11. package/dist/assets/index-CO3c8O32.js +285 -0
  12. package/dist/assets/index-CO3c8O32.js.map +1 -0
  13. package/dist/index.html +18 -0
  14. package/package.json +26 -2
  15. package/src/App.tsx +233 -0
  16. package/src/components/Button.tsx +55 -0
  17. package/src/components/Card.tsx +40 -0
  18. package/src/components/EmptyState.tsx +30 -0
  19. package/src/components/Modal.tsx +137 -0
  20. package/src/components/Spinner.tsx +19 -0
  21. package/src/components/StatusBadge.tsx +25 -0
  22. package/src/components/Tag.tsx +28 -0
  23. package/src/components/Toast.tsx +142 -0
  24. package/src/components/Topbar.tsx +88 -0
  25. package/src/index.html +17 -0
  26. package/src/lib/api.ts +71 -0
  27. package/src/lib/markdown.tsx +59 -0
  28. package/src/lib/types.ts +200 -0
  29. package/src/lib/utils.ts +79 -0
  30. package/src/lib/ws.ts +132 -0
  31. package/src/main.tsx +12 -0
  32. package/src/styles/main.css +2324 -0
  33. package/src/views/Agents.tsx +199 -0
  34. package/src/views/Chat.tsx +255 -0
  35. package/src/views/Config.tsx +250 -0
  36. package/src/views/Overview.tsx +267 -0
  37. package/src/views/Plans.tsx +667 -0
  38. package/src/views/Projects.tsx +155 -0
  39. package/src/views/Settings.tsx +253 -0
  40. package/src/views/Tasks.tsx +567 -0
  41. package/tsconfig.json +23 -0
  42. package/vite.config.ts +24 -0
@@ -0,0 +1,155 @@
1
+ // src/views/Projects.tsx — discovered projects + activate.
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { Folder, RefreshCw, FolderOpen } from 'lucide-react';
4
+ import { Button } from '../components/Button';
5
+ import { Card, CardTitle, CardMeta } from '../components/Card';
6
+ import { EmptyState } from '../components/EmptyState';
7
+ import { Spinner } from '../components/Spinner';
8
+ import { StatusBadge } from '../components/StatusBadge';
9
+ import { useToast } from '../components/Toast';
10
+ import { api } from '../lib/api';
11
+ import { cn, formatRelative } from '../lib/utils';
12
+ import type { Project, Settings, Snapshot } from '../lib/types';
13
+
14
+ type Props = {
15
+ snapshot: Snapshot;
16
+ settings: Settings;
17
+ activeTab: string;
18
+ setActiveTab: (id: string) => void;
19
+ refreshSnapshot: () => Promise<void>;
20
+ };
21
+
22
+ export function Projects({ snapshot }: Props) {
23
+ const toast = useToast();
24
+ const [projects, setProjects] = useState<Project[]>(snapshot.projects || []);
25
+ const [loading, setLoading] = useState(!snapshot.projects);
26
+
27
+ useEffect(() => {
28
+ if (snapshot.projects?.length) {
29
+ setProjects(snapshot.projects);
30
+ setLoading(false);
31
+ return;
32
+ }
33
+ let cancelled = false;
34
+ api
35
+ .get<{ projects: Project[] }>('/projects')
36
+ .then((d) => {
37
+ if (!cancelled) {
38
+ setProjects(d.projects || []);
39
+ setLoading(false);
40
+ }
41
+ })
42
+ .catch((err) => {
43
+ if (!cancelled) {
44
+ setLoading(false);
45
+ toast.error(`Could not load projects: ${(err as Error).message}`);
46
+ }
47
+ });
48
+ return () => {
49
+ cancelled = true;
50
+ };
51
+ }, [snapshot.projects, toast]);
52
+
53
+ const sorted = useMemo(
54
+ () =>
55
+ [...projects].sort((a, b) => {
56
+ if (a.active && !b.active) return -1;
57
+ if (!a.active && b.active) return 1;
58
+ return b.mtime - a.mtime;
59
+ }),
60
+ [projects],
61
+ );
62
+
63
+ const refresh = async () => {
64
+ try {
65
+ const d = await api.get<{ projects: Project[] }>('/projects');
66
+ setProjects(d.projects || []);
67
+ toast.info('Projects refreshed.', 1500);
68
+ } catch (err) {
69
+ toast.error(`Refresh failed: ${(err as Error).message}`);
70
+ }
71
+ };
72
+
73
+ const activate = async (name: string) => {
74
+ try {
75
+ await api.post(`/projects/${encodeURIComponent(name)}/activate`);
76
+ toast.success(
77
+ `Activated "${name}". Restart the TUI in that directory to fully switch.`,
78
+ );
79
+ await refresh();
80
+ } catch (err) {
81
+ toast.error(`Activate failed: ${(err as Error).message}`);
82
+ }
83
+ };
84
+
85
+ return (
86
+ <div className="view view-projects">
87
+ <header className="view-header">
88
+ <div className="view-header-text">
89
+ <h2 className="view-title">
90
+ <Folder size={18} /> Projects ({projects.length})
91
+ </h2>
92
+ <p className="view-subtitle">
93
+ Discovered from .bizar/PROJECT.md markers in cwd and ~/Projects.
94
+ </p>
95
+ </div>
96
+ <div className="view-actions">
97
+ <Button variant="secondary" size="sm" onClick={refresh}>
98
+ <RefreshCw size={14} /> Refresh
99
+ </Button>
100
+ </div>
101
+ </header>
102
+
103
+ {loading ? (
104
+ <div className="view-loading">
105
+ <Spinner size="lg" />
106
+ </div>
107
+ ) : sorted.length === 0 ? (
108
+ <EmptyState
109
+ icon={<FolderOpen size={32} />}
110
+ title="No projects found"
111
+ message={
112
+ <>
113
+ No <code>.bizar/PROJECT.md</code> files in cwd or <code>~/Projects</code>.
114
+ </>
115
+ }
116
+ />
117
+ ) : (
118
+ <div className="project-grid">
119
+ {sorted.map((p) => (
120
+ <Card
121
+ key={p.path}
122
+ variant="elevated"
123
+ className={cn('project-card', p.active && 'project-card-active')}
124
+ >
125
+ <div className="project-card-head">
126
+ <CardTitle>{p.name}</CardTitle>
127
+ {p.active && <StatusBadge kind="accent">active</StatusBadge>}
128
+ </div>
129
+ <div className="project-card-path mono" title={p.path}>
130
+ {p.path}
131
+ </div>
132
+ <CardMeta>
133
+ {p.projectMdSize
134
+ ? `PROJECT.md: ${p.projectMdSize} bytes`
135
+ : 'no PROJECT.md'}
136
+ {p.hindsightCount > 0 && ` · .hindsight: ${p.hindsightCount}`}
137
+ {' · '}accessed {formatRelative(p.mtime)}
138
+ </CardMeta>
139
+ <div className="project-card-actions">
140
+ <Button
141
+ variant={p.active ? 'ghost' : 'primary'}
142
+ size="sm"
143
+ disabled={p.active}
144
+ onClick={() => activate(p.name)}
145
+ >
146
+ <FolderOpen size={12} /> Activate
147
+ </Button>
148
+ </div>
149
+ </Card>
150
+ ))}
151
+ </div>
152
+ )}
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,253 @@
1
+ // src/views/Settings.tsx — user settings form.
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import {
4
+ Sliders,
5
+ Save,
6
+ RefreshCw,
7
+ Sun,
8
+ Moon,
9
+ Monitor,
10
+ Info,
11
+ } from 'lucide-react';
12
+ import { Button } from '../components/Button';
13
+ import { Card, CardTitle, CardMeta } from '../components/Card';
14
+ import { useToast } from '../components/Toast';
15
+ import { api } from '../lib/api';
16
+ import { cn } from '../lib/utils';
17
+ import { applyTheme, type Settings, type SettingsResponse, type Snapshot, type ThemeName } from '../lib/types';
18
+
19
+ type Props = {
20
+ snapshot: Snapshot;
21
+ settings: Settings;
22
+ activeTab: string;
23
+ setActiveTab: (id: string) => void;
24
+ refreshSnapshot: () => Promise<void>;
25
+ };
26
+
27
+ const THEMES: { id: ThemeName; label: string; Icon: typeof Sun }[] = [
28
+ { id: 'dark', label: 'Dark', Icon: Moon },
29
+ { id: 'light', label: 'Light', Icon: Sun },
30
+ { id: 'system', label: 'System', Icon: Monitor },
31
+ ];
32
+
33
+ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
34
+ const toast = useToast();
35
+ const [settings, setSettings] = useState<Settings>(initial);
36
+ const [dirty, setDirty] = useState(false);
37
+ const [saving, setSaving] = useState(false);
38
+
39
+ useEffect(() => {
40
+ setSettings(initial);
41
+ setDirty(false);
42
+ }, [initial]);
43
+
44
+ const patch = <K extends keyof Settings>(key: K, value: Settings[K]) => {
45
+ setSettings((cur) => ({ ...cur, [key]: value }));
46
+ setDirty(true);
47
+ };
48
+
49
+ const onSave = async () => {
50
+ setSaving(true);
51
+ try {
52
+ const r = await api.put<SettingsResponse>('/settings', settings);
53
+ setSettings(r.data);
54
+ setDirty(false);
55
+ applyTheme(r.data.theme);
56
+ toast.success('Settings saved.');
57
+ await refreshSnapshot();
58
+ } catch (err) {
59
+ toast.error(`Save failed: ${(err as Error).message}`);
60
+ } finally {
61
+ setSaving(false);
62
+ }
63
+ };
64
+
65
+ const onReload = async () => {
66
+ try {
67
+ const r = await api.get<SettingsResponse>('/settings');
68
+ setSettings(r.data);
69
+ setDirty(false);
70
+ applyTheme(r.data.theme);
71
+ toast.info('Settings reloaded.', 1500);
72
+ } catch (err) {
73
+ toast.error(`Reload failed: ${(err as Error).message}`);
74
+ }
75
+ };
76
+
77
+ const about = settings.about || {
78
+ version: '2.6.0',
79
+ homepage: 'https://github.com/DrB0rk/BizarHarness',
80
+ license: 'MIT',
81
+ };
82
+ const notif = settings.notifications || {
83
+ onAgentComplete: true,
84
+ onPlanApproval: true,
85
+ };
86
+
87
+ return (
88
+ <div className="view view-settings">
89
+ <header className="view-header">
90
+ <div className="view-header-text">
91
+ <h2 className="view-title">
92
+ <Sliders size={18} /> Settings
93
+ </h2>
94
+ <p className="view-subtitle">
95
+ Personal preferences. Changes are saved to{' '}
96
+ <code>~/.config/bizar/settings.json</code>.
97
+ </p>
98
+ </div>
99
+ <div className="view-actions">
100
+ <Button variant="secondary" size="sm" onClick={onReload}>
101
+ <RefreshCw size={14} /> Reload
102
+ </Button>
103
+ <Button
104
+ variant="primary"
105
+ size="sm"
106
+ disabled={!dirty || saving}
107
+ onClick={onSave}
108
+ >
109
+ {saving ? <span className="btn-spinner" /> : <Save size={14} />}
110
+ {saving ? 'Saving…' : 'Save'}
111
+ </Button>
112
+ </div>
113
+ </header>
114
+
115
+ <div className="settings-grid">
116
+ <Card>
117
+ <CardTitle>General</CardTitle>
118
+ <CardMeta>Theme, default agent, model override.</CardMeta>
119
+
120
+ <div className="field">
121
+ <label className="field-label">Theme</label>
122
+ <div className="theme-row">
123
+ {THEMES.map(({ id, label, Icon }) => {
124
+ const active = settings.theme === id;
125
+ return (
126
+ <button
127
+ key={id}
128
+ type="button"
129
+ className={cn(
130
+ 'theme-card',
131
+ active && 'theme-card-active',
132
+ )}
133
+ onClick={() => {
134
+ patch('theme', id);
135
+ applyTheme(id);
136
+ }}
137
+ >
138
+ <Icon size={16} />
139
+ <span className="theme-card-label">{label}</span>
140
+ <span
141
+ className={cn(
142
+ 'theme-card-swatch',
143
+ `theme-card-swatch-${id}`,
144
+ )}
145
+ />
146
+ </button>
147
+ );
148
+ })}
149
+ </div>
150
+ <span className="field-help">
151
+ Dark is the default; Light is a low-contrast variant; System follows your OS.
152
+ </span>
153
+ </div>
154
+
155
+ <div className="field">
156
+ <label className="field-label" htmlFor="set-default-agent">
157
+ Default agent
158
+ </label>
159
+ <input
160
+ id="set-default-agent"
161
+ className="input"
162
+ type="text"
163
+ placeholder="e.g. odin"
164
+ value={settings.defaultAgent || ''}
165
+ onChange={(e) => patch('defaultAgent', e.target.value)}
166
+ />
167
+ <span className="field-help">
168
+ Agent used when none is specified.
169
+ </span>
170
+ </div>
171
+
172
+ <div className="field">
173
+ <label className="field-label" htmlFor="set-default-model">
174
+ Model override
175
+ </label>
176
+ <input
177
+ id="set-default-model"
178
+ className="input"
179
+ type="text"
180
+ placeholder="(leave empty to use provider default)"
181
+ value={settings.defaultModel || ''}
182
+ onChange={(e) => patch('defaultModel', e.target.value)}
183
+ />
184
+ </div>
185
+ </Card>
186
+
187
+ <Card>
188
+ <CardTitle>Notifications</CardTitle>
189
+ <CardMeta>Toast triggers inside the dashboard.</CardMeta>
190
+
191
+ <label className="checkbox-row">
192
+ <input
193
+ type="checkbox"
194
+ checked={!!notif.onAgentComplete}
195
+ onChange={(e) =>
196
+ setSettings((cur) => ({
197
+ ...cur,
198
+ notifications: {
199
+ ...(cur.notifications || {
200
+ onAgentComplete: false,
201
+ onPlanApproval: false,
202
+ }),
203
+ onAgentComplete: e.target.checked,
204
+ },
205
+ }))
206
+ }
207
+ />
208
+ <span>Notify when an agent invocation completes</span>
209
+ </label>
210
+
211
+ <label className="checkbox-row">
212
+ <input
213
+ type="checkbox"
214
+ checked={!!notif.onPlanApproval}
215
+ onChange={(e) =>
216
+ setSettings((cur) => ({
217
+ ...cur,
218
+ notifications: {
219
+ ...(cur.notifications || {
220
+ onAgentComplete: false,
221
+ onPlanApproval: false,
222
+ }),
223
+ onPlanApproval: e.target.checked,
224
+ },
225
+ }))
226
+ }
227
+ />
228
+ <span>Notify when a plan needs approval</span>
229
+ </label>
230
+ </Card>
231
+ </div>
232
+
233
+ <Card>
234
+ <CardTitle>
235
+ <Info size={14} /> About
236
+ </CardTitle>
237
+ <CardMeta>Build metadata.</CardMeta>
238
+ <dl className="about-table">
239
+ <dt>Version</dt>
240
+ <dd className="mono">{about.version}</dd>
241
+ <dt>Homepage</dt>
242
+ <dd>
243
+ <a href={about.homepage} target="_blank" rel="noopener noreferrer">
244
+ {about.homepage}
245
+ </a>
246
+ </dd>
247
+ <dt>License</dt>
248
+ <dd>{about.license}</dd>
249
+ </dl>
250
+ </Card>
251
+ </div>
252
+ );
253
+ }