@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.
- package/cli/bin.mjs +73 -0
- package/cli/dashboard/api.mjs +473 -0
- package/cli/dashboard/browser.mjs +40 -0
- package/cli/dashboard/server.mjs +366 -0
- package/cli/dashboard/state.mjs +438 -0
- package/cli/dashboard/tasks-store.mjs +203 -0
- package/cli/dashboard/watcher.mjs +81 -0
- package/cli/dashboard.mjs +97 -0
- package/config/commands/bizar.md +13 -39
- package/dist/assets/index-BVvY22Gt.css +1 -0
- package/dist/assets/index-CO3c8O32.js +285 -0
- package/dist/assets/index-CO3c8O32.js.map +1 -0
- package/dist/index.html +18 -0
- package/package.json +26 -2
- package/src/App.tsx +233 -0
- package/src/components/Button.tsx +55 -0
- package/src/components/Card.tsx +40 -0
- package/src/components/EmptyState.tsx +30 -0
- package/src/components/Modal.tsx +137 -0
- package/src/components/Spinner.tsx +19 -0
- package/src/components/StatusBadge.tsx +25 -0
- package/src/components/Tag.tsx +28 -0
- package/src/components/Toast.tsx +142 -0
- package/src/components/Topbar.tsx +88 -0
- package/src/index.html +17 -0
- package/src/lib/api.ts +71 -0
- package/src/lib/markdown.tsx +59 -0
- package/src/lib/types.ts +200 -0
- package/src/lib/utils.ts +79 -0
- package/src/lib/ws.ts +132 -0
- package/src/main.tsx +12 -0
- package/src/styles/main.css +2324 -0
- package/src/views/Agents.tsx +199 -0
- package/src/views/Chat.tsx +255 -0
- package/src/views/Config.tsx +250 -0
- package/src/views/Overview.tsx +267 -0
- package/src/views/Plans.tsx +667 -0
- package/src/views/Projects.tsx +155 -0
- package/src/views/Settings.tsx +253 -0
- package/src/views/Tasks.tsx +567 -0
- package/tsconfig.json +23 -0
- 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
|
+
}
|