@polderlabs/bizar 2.6.1 → 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 (43) hide show
  1. package/cli/bin.mjs +158 -130
  2. package/cli/plan.test.mjs +2331 -0
  3. package/cli/service.mjs +309 -0
  4. package/package.json +19 -27
  5. package/cli/dashboard/api.mjs +0 -473
  6. package/cli/dashboard/browser.mjs +0 -40
  7. package/cli/dashboard/server.mjs +0 -366
  8. package/cli/dashboard/state.mjs +0 -438
  9. package/cli/dashboard/tasks-store.mjs +0 -203
  10. package/cli/dashboard/watcher.mjs +0 -81
  11. package/cli/dashboard.mjs +0 -97
  12. package/dist/assets/index-BVvY22Gt.css +0 -1
  13. package/dist/assets/index-CO3c8O32.js +0 -285
  14. package/dist/assets/index-CO3c8O32.js.map +0 -1
  15. package/dist/index.html +0 -18
  16. package/src/App.tsx +0 -233
  17. package/src/components/Button.tsx +0 -55
  18. package/src/components/Card.tsx +0 -40
  19. package/src/components/EmptyState.tsx +0 -30
  20. package/src/components/Modal.tsx +0 -137
  21. package/src/components/Spinner.tsx +0 -19
  22. package/src/components/StatusBadge.tsx +0 -25
  23. package/src/components/Tag.tsx +0 -28
  24. package/src/components/Toast.tsx +0 -142
  25. package/src/components/Topbar.tsx +0 -88
  26. package/src/index.html +0 -17
  27. package/src/lib/api.ts +0 -71
  28. package/src/lib/markdown.tsx +0 -59
  29. package/src/lib/types.ts +0 -200
  30. package/src/lib/utils.ts +0 -79
  31. package/src/lib/ws.ts +0 -132
  32. package/src/main.tsx +0 -12
  33. package/src/styles/main.css +0 -2324
  34. package/src/views/Agents.tsx +0 -199
  35. package/src/views/Chat.tsx +0 -255
  36. package/src/views/Config.tsx +0 -250
  37. package/src/views/Overview.tsx +0 -267
  38. package/src/views/Plans.tsx +0 -667
  39. package/src/views/Projects.tsx +0 -155
  40. package/src/views/Settings.tsx +0 -253
  41. package/src/views/Tasks.tsx +0 -567
  42. package/tsconfig.json +0 -23
  43. package/vite.config.ts +0 -24
@@ -1,142 +0,0 @@
1
- // src/components/Toast.tsx — toast context, provider hook, container.
2
-
3
- import {
4
- createContext,
5
- useCallback,
6
- useContext,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from 'react';
11
- import { CheckCircle2, AlertTriangle, AlertCircle, Info, X } from 'lucide-react';
12
-
13
- export type ToastKind = 'info' | 'success' | 'error' | 'warning';
14
-
15
- export type Toast = {
16
- id: number;
17
- kind: ToastKind;
18
- message: string;
19
- };
20
-
21
- export type ToastApi = {
22
- show: (message: string, kind?: ToastKind, durationMs?: number) => void;
23
- info: (message: string, durationMs?: number) => void;
24
- success: (message: string, durationMs?: number) => void;
25
- error: (message: string, durationMs?: number) => void;
26
- warning: (message: string, durationMs?: number) => void;
27
- dismiss: (id: number) => void;
28
- toasts: Toast[];
29
- };
30
-
31
- const ToastContext = createContext<ToastApi | null>(null);
32
-
33
- export function useToast(): ToastApi {
34
- const ctx = useContext(ToastContext);
35
- if (!ctx) {
36
- // Fallback no-op so tests/Storybook don't crash
37
- return {
38
- show: () => undefined,
39
- info: () => undefined,
40
- success: () => undefined,
41
- error: () => undefined,
42
- warning: () => undefined,
43
- dismiss: () => undefined,
44
- toasts: [],
45
- };
46
- }
47
- return ctx;
48
- }
49
-
50
- const ICONS: Record<ToastKind, typeof Info> = {
51
- info: Info,
52
- success: CheckCircle2,
53
- error: AlertCircle,
54
- warning: AlertTriangle,
55
- };
56
-
57
- export function ToastProvider({ children }: { children: React.ReactNode }) {
58
- const [toasts, setToasts] = useState<Toast[]>([]);
59
- const idRef = useRef(1);
60
- const timersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
61
-
62
- const dismiss = useCallback((id: number) => {
63
- setToasts((cur) => cur.filter((t) => t.id !== id));
64
- const timer = timersRef.current.get(id);
65
- if (timer) {
66
- clearTimeout(timer);
67
- timersRef.current.delete(id);
68
- }
69
- }, []);
70
-
71
- const show = useCallback<ToastApi['show']>(
72
- (message, kind = 'info', durationMs = 4000) => {
73
- const id = idRef.current++;
74
- setToasts((cur) => [...cur, { id, kind, message }]);
75
- if (durationMs > 0) {
76
- const timer = setTimeout(() => dismiss(id), durationMs);
77
- timersRef.current.set(id, timer);
78
- }
79
- },
80
- [dismiss],
81
- );
82
-
83
- const api = useMemo<ToastApi>(
84
- () => ({
85
- show,
86
- info: (m, d) => show(m, 'info', d),
87
- success: (m, d) => show(m, 'success', d),
88
- error: (m, d) => show(m, 'error', d),
89
- warning: (m, d) => show(m, 'warning', d),
90
- dismiss,
91
- toasts,
92
- }),
93
- [show, dismiss, toasts],
94
- );
95
-
96
- return (
97
- <ToastContext.Provider value={api}>
98
- {children}
99
- <ToastContainer toasts={toasts} onDismiss={dismiss} />
100
- </ToastContext.Provider>
101
- );
102
- }
103
-
104
- export function ToastContainer({
105
- toasts,
106
- onDismiss,
107
- }: {
108
- toasts: Toast[];
109
- onDismiss: (id: number) => void;
110
- }) {
111
- return (
112
- <div className="toast-stack" aria-live="polite">
113
- {toasts.map((t) => (
114
- <ToastItem key={t.id} toast={t} onDismiss={onDismiss} />
115
- ))}
116
- </div>
117
- );
118
- }
119
-
120
- function ToastItem({
121
- toast,
122
- onDismiss,
123
- }: {
124
- toast: Toast;
125
- onDismiss: (id: number) => void;
126
- }) {
127
- const Icon = ICONS[toast.kind];
128
- return (
129
- <div className={`toast toast-${toast.kind}`} role="status">
130
- <Icon size={16} className="toast-icon" />
131
- <span className="toast-message">{toast.message}</span>
132
- <button
133
- type="button"
134
- className="toast-close"
135
- aria-label="Dismiss"
136
- onClick={() => onDismiss(toast.id)}
137
- >
138
- <X size={14} />
139
- </button>
140
- </div>
141
- );
142
- }
@@ -1,88 +0,0 @@
1
- // src/components/Topbar.tsx — header with brand, tab nav, ws status.
2
-
3
- import type { ReactNode } from 'react';
4
- import {
5
- LayoutDashboard,
6
- MessageSquare,
7
- Bot,
8
- Map,
9
- Folder,
10
- CheckSquare,
11
- Settings2,
12
- Sliders,
13
- type LucideIcon,
14
- } from 'lucide-react';
15
- import { cn } from '../lib/utils';
16
- import type { WsStatus } from '../lib/types';
17
-
18
- export type TabDef = {
19
- id: string;
20
- label: string;
21
- icon: LucideIcon;
22
- };
23
-
24
- export const TABS: TabDef[] = [
25
- { id: 'overview', label: 'Overview', icon: LayoutDashboard },
26
- { id: 'chat', label: 'Chat', icon: MessageSquare },
27
- { id: 'agents', label: 'Agents', icon: Bot },
28
- { id: 'plans', label: 'Plans', icon: Map },
29
- { id: 'projects', label: 'Projects', icon: Folder },
30
- { id: 'tasks', label: 'Tasks', icon: CheckSquare },
31
- { id: 'config', label: 'Config', icon: Settings2 },
32
- { id: 'settings', label: 'Settings', icon: Sliders },
33
- ];
34
-
35
- export type TopbarProps = {
36
- activeTab: string;
37
- onTabChange: (id: string) => void;
38
- wsStatus: WsStatus;
39
- version: string;
40
- rightSlot?: ReactNode;
41
- };
42
-
43
- export function Topbar({
44
- activeTab,
45
- onTabChange,
46
- wsStatus,
47
- version,
48
- rightSlot,
49
- }: TopbarProps) {
50
- return (
51
- <header className="topbar">
52
- <div className="brand">
53
- <span className="brand-logo" aria-hidden>
54
- 🪩
55
- </span>
56
- <span className="brand-title">Bizar</span>
57
- <span className="brand-version">{version}</span>
58
- </div>
59
- <nav className="tabs" role="tablist">
60
- {TABS.map((tab) => {
61
- const Icon = tab.icon;
62
- const active = tab.id === activeTab;
63
- return (
64
- <button
65
- key={tab.id}
66
- type="button"
67
- role="tab"
68
- aria-selected={active}
69
- className={cn('tab', active && 'tab-active')}
70
- onClick={() => onTabChange(tab.id)}
71
- title={`${tab.label} (${TABS.findIndex((t) => t.id === tab.id) + 1})`}
72
- >
73
- <Icon size={14} className="tab-icon" />
74
- <span className="tab-label">{tab.label}</span>
75
- </button>
76
- );
77
- })}
78
- </nav>
79
- <div className="topbar-right">
80
- {rightSlot}
81
- <div className={cn('ws-status', `ws-${wsStatus}`)} title={`WebSocket: ${wsStatus}`}>
82
- <span className="ws-dot" />
83
- <span className="ws-label">{wsStatus}</span>
84
- </div>
85
- </div>
86
- </header>
87
- );
88
- }
package/src/index.html DELETED
@@ -1,17 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Bizar Dashboard</title>
7
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🪩</text></svg>" />
8
- <link rel="preconnect" href="https://rsms.me/" />
9
- <link rel="preconnect" href="https://fonts.googleapis.com" />
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
- <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
12
- </head>
13
- <body>
14
- <div id="root"></div>
15
- <script type="module" src="/main.tsx"></script>
16
- </body>
17
- </html>
package/src/lib/api.ts DELETED
@@ -1,71 +0,0 @@
1
- // src/lib/api.ts — REST client. Single instance, type-safe wrappers.
2
-
3
- export class ApiError extends Error {
4
- status: number;
5
- data: unknown;
6
- constructor(message: string, status: number, data?: unknown) {
7
- super(message);
8
- this.name = 'ApiError';
9
- this.status = status;
10
- this.data = data;
11
- }
12
- }
13
-
14
- class ApiClient {
15
- base = '/api';
16
-
17
- async get<T>(path: string): Promise<T> {
18
- return this.req<T>('GET', path);
19
- }
20
- async post<T>(path: string, body?: unknown): Promise<T> {
21
- return this.req<T>('POST', path, body);
22
- }
23
- async put<T>(path: string, body?: unknown): Promise<T> {
24
- return this.req<T>('PUT', path, body);
25
- }
26
- async patch<T>(path: string, body?: unknown): Promise<T> {
27
- return this.req<T>('PATCH', path, body);
28
- }
29
- async del<T = unknown>(path: string): Promise<T> {
30
- return this.req<T>('DELETE', path);
31
- }
32
-
33
- private async req<T>(method: string, path: string, body?: unknown): Promise<T> {
34
- const opts: RequestInit = {
35
- method,
36
- headers: { 'Content-Type': 'application/json' },
37
- };
38
- if (body !== undefined && body !== null) {
39
- opts.body = typeof body === 'string' ? body : JSON.stringify(body);
40
- }
41
- const r = await fetch(this.base + path, opts);
42
- const ct = r.headers.get('content-type') || '';
43
- if (ct.includes('application/json')) {
44
- const data = await r.json();
45
- if (!r.ok) {
46
- const msg =
47
- (data && typeof data === 'object' && 'message' in data
48
- ? (data as { message?: string }).message
49
- : undefined) || `${method} ${path}: ${r.status}`;
50
- throw new ApiError(msg, r.status, data);
51
- }
52
- return data as T;
53
- }
54
- const text = await r.text();
55
- if (!r.ok) {
56
- throw new ApiError(
57
- `${method} ${path}: ${r.status} — ${text.slice(0, 200)}`,
58
- r.status,
59
- text,
60
- );
61
- }
62
- try {
63
- return JSON.parse(text) as T;
64
- } catch {
65
- return text as unknown as T;
66
- }
67
- }
68
- }
69
-
70
- export const api = new ApiClient();
71
- export { ApiClient };
@@ -1,59 +0,0 @@
1
- // src/lib/markdown.ts — pretty JSON syntax highlighter for the JSON tree panel.
2
- // Replaces the old utils.highlightJSON; we render to React nodes, not strings.
3
-
4
- import { Fragment } from 'react';
5
-
6
- type Token = { kind: 'key' | 'string' | 'number' | 'boolean' | 'null' | 'punct' | 'space'; value: string };
7
-
8
- function tokenize(json: string): Token[] {
9
- const out: Token[] = [];
10
- const re = /("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b)|(\bnull\b)|([{}\[\],])|(\s+)/g;
11
- let m: RegExpExecArray | null;
12
- while ((m = re.exec(json)) !== null) {
13
- if (m[1] !== undefined) {
14
- out.push({ kind: m[2] ? 'key' : 'string', value: m[1] });
15
- if (m[2]) out.push({ kind: 'punct', value: m[2] });
16
- } else if (m[3] !== undefined) {
17
- out.push({ kind: 'number', value: m[3] });
18
- } else if (m[4] !== undefined) {
19
- out.push({ kind: 'boolean', value: m[4] });
20
- } else if (m[5] !== undefined) {
21
- out.push({ kind: 'null', value: m[5] });
22
- } else if (m[6] !== undefined) {
23
- out.push({ kind: 'punct', value: m[6] });
24
- } else if (m[7] !== undefined) {
25
- out.push({ kind: 'space', value: m[7] });
26
- }
27
- }
28
- return out;
29
- }
30
-
31
- const COLOR: Record<Token['kind'], string | undefined> = {
32
- key: 'var(--syntax-key)',
33
- string: 'var(--syntax-string)',
34
- number: 'var(--syntax-number)',
35
- boolean: 'var(--syntax-boolean)',
36
- null: 'var(--syntax-null)',
37
- punct: undefined,
38
- space: undefined,
39
- };
40
-
41
- /** Render highlighted JSON as React fragments. */
42
- export function JsonHighlight({ value }: { value: unknown }) {
43
- const json = JSON.stringify(value, null, 2);
44
- if (json === undefined) return null;
45
- const tokens = tokenize(json);
46
- return (
47
- <>
48
- {tokens.map((t, i) => {
49
- const color = COLOR[t.kind];
50
- if (!color) return <Fragment key={i}>{t.value}</Fragment>;
51
- return (
52
- <span key={i} style={{ color }}>
53
- {t.value}
54
- </span>
55
- );
56
- })}
57
- </>
58
- );
59
- }
package/src/lib/types.ts DELETED
@@ -1,200 +0,0 @@
1
- // src/lib/types.ts — TypeScript types for the Bizar dashboard.
2
- // All shapes here mirror the JSON returned by cli/dashboard/api.mjs.
3
-
4
- export type ThemeName = 'dark' | 'light' | 'system';
5
-
6
- export type Agent = {
7
- name: string;
8
- description: string;
9
- model: string;
10
- mode: string;
11
- file: string;
12
- path: string;
13
- mtime: number;
14
- };
15
-
16
- export type Project = {
17
- name: string;
18
- path: string;
19
- projectMdSize: number;
20
- hindsightCount: number;
21
- mtime: number;
22
- active: boolean;
23
- };
24
-
25
- export type Plan = {
26
- slug: string;
27
- title: string;
28
- status: string;
29
- source: 'worktree' | 'global' | string;
30
- elementCount: number | null;
31
- commentCount: number | null;
32
- mtime: number;
33
- planUrl: string | null;
34
- };
35
-
36
- export type CanvasElement = {
37
- id: string;
38
- type: string;
39
- title?: string;
40
- content?: string;
41
- x: number;
42
- y: number;
43
- width: number;
44
- height: number;
45
- };
46
-
47
- export type CanvasConnection = {
48
- id: string;
49
- fromElementId?: string;
50
- from?: string;
51
- toElementId?: string;
52
- to?: string;
53
- };
54
-
55
- export type CanvasComment = {
56
- id: string;
57
- elementId?: string;
58
- text: string;
59
- author: string;
60
- created: string;
61
- thread?: { author: string; text: string }[];
62
- };
63
-
64
- export type CanvasViewport = { x: number; y: number; zoom: number };
65
-
66
- export type Canvas = {
67
- schemaVersion?: number;
68
- title: string;
69
- elements: CanvasElement[];
70
- connections: CanvasConnection[];
71
- comments: CanvasComment[];
72
- viewport: CanvasViewport;
73
- };
74
-
75
- export type ConfigResponse = {
76
- path: string;
77
- data: unknown;
78
- raw: string;
79
- exists: boolean;
80
- };
81
-
82
- export type Settings = {
83
- theme: ThemeName;
84
- defaultAgent: string;
85
- defaultModel: string;
86
- notifications: {
87
- onAgentComplete: boolean;
88
- onPlanApproval: boolean;
89
- };
90
- about: {
91
- version: string;
92
- homepage: string;
93
- license: string;
94
- };
95
- };
96
-
97
- export type SettingsResponse = {
98
- path: string;
99
- data: Settings;
100
- exists: boolean;
101
- };
102
-
103
- export type OverviewCounts = {
104
- agents: number;
105
- plans: number;
106
- projects: number;
107
- sessions: number;
108
- };
109
-
110
- export type OverviewVersions = {
111
- node: string;
112
- platform: string;
113
- projectRoot: string;
114
- bizarRoot: string;
115
- };
116
-
117
- export type ActivityItem = {
118
- ts: string;
119
- kind: string;
120
- [k: string]: unknown;
121
- };
122
-
123
- export type Overview = {
124
- counts: OverviewCounts;
125
- recentActivity: ActivityItem[];
126
- versions: OverviewVersions;
127
- generatedAt: string;
128
- };
129
-
130
- export type ChatMessage = {
131
- role: 'user' | 'assistant' | 'system' | string;
132
- content?: string;
133
- message?: string;
134
- agent?: string;
135
- ts?: string | number;
136
- };
137
-
138
- export type ChatSession = {
139
- id: string;
140
- file: string;
141
- mtime: number;
142
- size: number;
143
- };
144
-
145
- export type ChatResponse = {
146
- messages: ChatMessage[];
147
- sessions: ChatSession[];
148
- };
149
-
150
- export type Task = {
151
- id: string;
152
- title: string;
153
- description: string;
154
- status: 'queued' | 'doing' | 'done' | string;
155
- tags: string[];
156
- priority: 'low' | 'normal' | 'high' | string;
157
- createdAt: string;
158
- updatedAt: string;
159
- };
160
-
161
- export type Snapshot = {
162
- overview: Overview;
163
- agents: Agent[];
164
- plans: Plan[];
165
- projects: Project[];
166
- config: ConfigResponse;
167
- settings: SettingsResponse;
168
- tasks: Task[];
169
- };
170
-
171
- export type WsStatus = 'connecting' | 'connected' | 'disconnected';
172
-
173
- export type WsMessage =
174
- | { type: 'snapshot'; ts: number; data: Snapshot }
175
- | { type: 'change'; event: string; path: string; ts: number }
176
- | { type: 'tasks:change'; task: Task }
177
- | { type: 'tasks:delete'; id: string }
178
- | { type: 'settings:change'; settings: Settings }
179
- | { type: 'pong'; ts: number }
180
- | { type: 'ping' }
181
- | { type: 'refresh' };
182
-
183
- /** Resolve a theme to the actual key applied to <html data-theme="..."> */
184
- export function applyTheme(theme: ThemeName): 'dark' | 'light' {
185
- const resolved: 'dark' | 'light' =
186
- theme === 'system'
187
- ? typeof window !== 'undefined' &&
188
- window.matchMedia('(prefers-color-scheme: light)').matches
189
- ? 'light'
190
- : 'dark'
191
- : theme;
192
- if (typeof document !== 'undefined') {
193
- if (resolved === 'light') {
194
- document.documentElement.setAttribute('data-theme', 'light');
195
- } else {
196
- document.documentElement.removeAttribute('data-theme');
197
- }
198
- }
199
- return resolved;
200
- }
package/src/lib/utils.ts DELETED
@@ -1,79 +0,0 @@
1
- // src/lib/utils.ts — tiny shared helpers.
2
-
3
- export function cn(...parts: (string | false | null | undefined)[]): string {
4
- return parts.filter(Boolean).join(' ');
5
- }
6
-
7
- /** Compact relative time. */
8
- export function formatRelative(ts: number | string | Date | undefined | null): string {
9
- if (!ts && ts !== 0) return '';
10
- const t = typeof ts === 'number' ? ts : new Date(ts).getTime();
11
- if (Number.isNaN(t)) return '';
12
- const diff = Date.now() - t;
13
- if (diff < 0) return 'just now';
14
- if (diff < 60_000) return 'just now';
15
- if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
16
- if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
17
- if (diff < 7 * 86_400_000) return `${Math.floor(diff / 86_400_000)}d ago`;
18
- return new Date(t).toLocaleDateString();
19
- }
20
-
21
- /** Full localized timestamp. */
22
- export function formatTime(ts: number | string | Date | undefined | null): string {
23
- if (!ts) return '';
24
- const d = typeof ts === 'number' ? new Date(ts) : new Date(ts);
25
- if (Number.isNaN(d.getTime())) return '';
26
- return d.toLocaleString();
27
- }
28
-
29
- /** Trailing-suffix truncation. */
30
- export function truncate(s: string | undefined | null, max = 160): string {
31
- if (!s) return '';
32
- return s.length > max ? s.slice(0, max) + '…' : s;
33
- }
34
-
35
- /** Debounce. */
36
- export function debounce<T extends (...args: never[]) => void>(
37
- fn: T,
38
- ms = 200,
39
- ): (...args: Parameters<T>) => void {
40
- let timer: ReturnType<typeof setTimeout> | null = null;
41
- return (...args: Parameters<T>) => {
42
- if (timer) clearTimeout(timer);
43
- timer = setTimeout(() => fn(...args), ms);
44
- };
45
- }
46
-
47
- /** Color helpers (status badges). */
48
- export const priorityColors: Record<string, string> = {
49
- low: 'var(--text-dim)',
50
- normal: 'var(--info)',
51
- high: 'var(--error)',
52
- };
53
-
54
- export const statusBadgeKind = (
55
- status: string,
56
- ): 'success' | 'warning' | 'error' | 'info' | 'accent' | '' => {
57
- switch (status) {
58
- case 'approved':
59
- case 'done':
60
- return 'success';
61
- case 'draft':
62
- case 'queued':
63
- return '';
64
- case 'in-progress':
65
- case 'doing':
66
- return 'info';
67
- case 'rejected':
68
- return 'error';
69
- default:
70
- return 'accent';
71
- }
72
- };
73
-
74
- /** Cheap hash for change-detection on text (e.g. raw JSON). */
75
- export function hashText(s: string): string {
76
- let h = 5381;
77
- for (let i = 0; i < s.length; i++) h = ((h << 5) + h) ^ s.charCodeAt(i);
78
- return (h >>> 0).toString(36);
79
- }