@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
package/dist/index.html DELETED
@@ -1,18 +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
- <script type="module" crossorigin src="./assets/index-CO3c8O32.js"></script>
13
- <link rel="stylesheet" crossorigin href="./assets/index-BVvY22Gt.css">
14
- </head>
15
- <body>
16
- <div id="root"></div>
17
- </body>
18
- </html>
package/src/App.tsx DELETED
@@ -1,233 +0,0 @@
1
- // src/App.tsx — root shell. Wires data + contexts + tab routing.
2
-
3
- import { useEffect, useMemo, useRef, useState } from 'react';
4
- import { Topbar, TABS } from './components/Topbar';
5
- import { ModalProvider } from './components/Modal';
6
- import { ToastProvider, useToast } from './components/Toast';
7
- import { api } from './lib/api';
8
- import { Ws } from './lib/ws';
9
- import {
10
- applyTheme,
11
- type Settings,
12
- type SettingsResponse,
13
- type Snapshot,
14
- type WsMessage,
15
- type WsStatus,
16
- } from './lib/types';
17
- import { Overview } from './views/Overview';
18
- import { Chat } from './views/Chat';
19
- import { Agents } from './views/Agents';
20
- import { Plans } from './views/Plans';
21
- import { Projects } from './views/Projects';
22
- import { Tasks } from './views/Tasks';
23
- import { Config } from './views/Config';
24
- import { SettingsView } from './views/Settings';
25
- import { Spinner } from './components/Spinner';
26
- import './styles/main.css';
27
-
28
- type ViewProps = {
29
- snapshot: Snapshot;
30
- settings: Settings;
31
- activeTab: string;
32
- setActiveTab: (id: string) => void;
33
- refreshSnapshot: () => Promise<void>;
34
- };
35
-
36
- const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
37
- overview: Overview,
38
- chat: Chat,
39
- agents: Agents,
40
- plans: Plans,
41
- projects: Projects,
42
- tasks: Tasks,
43
- config: Config,
44
- settings: SettingsView,
45
- };
46
-
47
- const VERSION = 'v2.6.0';
48
-
49
- export function App() {
50
- return (
51
- <ToastProvider>
52
- <ModalProvider>
53
- <Shell />
54
- </ModalProvider>
55
- </ToastProvider>
56
- );
57
- }
58
-
59
- function Shell() {
60
- const [activeTab, setActiveTab] = useState<string>('overview');
61
- const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
62
- const [settings, setSettings] = useState<Settings | null>(null);
63
- const [wsStatus, setWsStatus] = useState<WsStatus>('connecting');
64
- const [bootError, setBootError] = useState<string | null>(null);
65
- const wsRef = useRef<Ws | null>(null);
66
- const toast = useToast();
67
-
68
- // Apply theme whenever settings are loaded or theme changes
69
- useEffect(() => {
70
- if (settings?.theme) applyTheme(settings.theme);
71
- }, [settings?.theme]);
72
-
73
- // React to system preference when in 'system' mode
74
- useEffect(() => {
75
- if (settings?.theme !== 'system') return;
76
- const mql = window.matchMedia('(prefers-color-scheme: light)');
77
- const handler = () => applyTheme('system');
78
- mql.addEventListener('change', handler);
79
- return () => mql.removeEventListener('change', handler);
80
- }, [settings?.theme]);
81
-
82
- // Initial fetch
83
- useEffect(() => {
84
- let cancelled = false;
85
- Promise.all([
86
- api.get<Snapshot>('/snapshot').catch(() => null),
87
- api.get<SettingsResponse>('/settings').catch(() => null),
88
- ])
89
- .then(([snap, set]) => {
90
- if (cancelled) return;
91
- if (snap) setSnapshot(snap);
92
- if (set?.data) setSettings(set.data);
93
- if (!snap && !set) {
94
- setBootError('Dashboard server unreachable.');
95
- }
96
- })
97
- .catch((err) => {
98
- if (cancelled) return;
99
- const msg = (err as Error)?.message ?? 'unknown error';
100
- setBootError(msg);
101
- toast.error(`Failed to load: ${msg}`);
102
- });
103
- return () => {
104
- cancelled = true;
105
- };
106
- }, [toast]);
107
-
108
- // WebSocket lifecycle
109
- useEffect(() => {
110
- const ws = new Ws();
111
- wsRef.current = ws;
112
- const offStatus = ws.onStatus((s) => setWsStatus(s));
113
- const offMsg = ws.on((msg: WsMessage) => {
114
- if (msg.type === 'snapshot' && 'data' in msg && msg.data) {
115
- setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...msg.data }));
116
- } else if (msg.type === 'change') {
117
- const m = msg;
118
- const file = m.path?.split('/').pop() || m.path || '';
119
- toast.info(`File changed: ${file}`, 2500);
120
- api
121
- .get<Snapshot>('/snapshot')
122
- .then((s) =>
123
- setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })),
124
- )
125
- .catch(() => undefined);
126
- } else if (msg.type === 'tasks:change') {
127
- const m = msg;
128
- setSnapshot((cur) => {
129
- if (!cur) return cur;
130
- const tasks = (cur.tasks || []).map((t) =>
131
- t.id === m.task.id ? m.task : t,
132
- );
133
- const exists = tasks.some((t) => t.id === m.task.id);
134
- return { ...cur, tasks: exists ? tasks : [m.task, ...tasks] };
135
- });
136
- } else if (msg.type === 'tasks:delete') {
137
- const m = msg;
138
- setSnapshot((cur) =>
139
- cur
140
- ? { ...cur, tasks: (cur.tasks || []).filter((t) => t.id !== m.id) }
141
- : cur,
142
- );
143
- } else if (msg.type === 'settings:change') {
144
- const m = msg;
145
- if (m.settings) setSettings(m.settings);
146
- }
147
- });
148
- return () => {
149
- offStatus();
150
- offMsg();
151
- ws.close();
152
- };
153
- }, [toast]);
154
-
155
- // Keyboard shortcuts — 1..8 → switch tabs
156
- useEffect(() => {
157
- const map: Record<string, string> = {};
158
- TABS.forEach((t, i) => {
159
- map[String(i + 1)] = t.id;
160
- });
161
- const handler = (e: KeyboardEvent) => {
162
- const t = (e.target as HTMLElement | null)?.tagName?.toLowerCase();
163
- if (
164
- t === 'input' ||
165
- t === 'textarea' ||
166
- (e.target as HTMLElement)?.isContentEditable ||
167
- e.metaKey ||
168
- e.ctrlKey ||
169
- e.altKey
170
- )
171
- return;
172
- const id = map[e.key];
173
- if (id) {
174
- e.preventDefault();
175
- setActiveTab(id);
176
- }
177
- };
178
- document.addEventListener('keydown', handler);
179
- return () => document.removeEventListener('keydown', handler);
180
- }, []);
181
-
182
- const View = VIEW_MAP[activeTab];
183
-
184
- const refreshSnapshot = useMemo(
185
- () => async () => {
186
- try {
187
- const s = await api.get<Snapshot>('/snapshot');
188
- setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s }));
189
- } catch (err) {
190
- toast.error(`Refresh failed: ${(err as Error).message}`);
191
- }
192
- },
193
- [toast],
194
- );
195
-
196
- return (
197
- <div className="app">
198
- <Topbar
199
- activeTab={activeTab}
200
- onTabChange={setActiveTab}
201
- wsStatus={wsStatus}
202
- version={VERSION}
203
- />
204
- <main className="content">
205
- {bootError && (
206
- <div className="boot-error">
207
- <h2>Dashboard unavailable</h2>
208
- <p>{bootError}</p>
209
- <p className="boot-error-hint">
210
- Make sure the Bizar dashboard server is running. Try{' '}
211
- <code>bizar dashboard start</code> in your terminal.
212
- </p>
213
- </div>
214
- )}
215
- {!bootError && (!snapshot || !settings) && (
216
- <div className="loading">
217
- <Spinner size="lg" />
218
- <p>Loading Bizarâ€Ļ</p>
219
- </div>
220
- )}
221
- {snapshot && settings && View && (
222
- <View
223
- snapshot={snapshot}
224
- settings={settings}
225
- activeTab={activeTab}
226
- setActiveTab={setActiveTab}
227
- refreshSnapshot={refreshSnapshot}
228
- />
229
- )}
230
- </main>
231
- </div>
232
- );
233
- }
@@ -1,55 +0,0 @@
1
- // src/components/Button.tsx — typed button with variants + sizes.
2
-
3
- import { type ButtonHTMLAttributes, forwardRef } from 'react';
4
- import { cn } from '../lib/utils';
5
-
6
- export type ButtonVariant =
7
- | 'primary'
8
- | 'secondary'
9
- | 'ghost'
10
- | 'danger'
11
- | 'accent';
12
- export type ButtonSize = 'sm' | 'md' | 'lg';
13
-
14
- export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
15
- variant?: ButtonVariant;
16
- size?: ButtonSize;
17
- iconOnly?: boolean;
18
- loading?: boolean;
19
- };
20
-
21
- export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
22
- function Button(
23
- {
24
- variant = 'secondary',
25
- size = 'md',
26
- iconOnly = false,
27
- loading = false,
28
- disabled,
29
- className,
30
- children,
31
- ...rest
32
- },
33
- ref,
34
- ) {
35
- return (
36
- <button
37
- ref={ref}
38
- type={rest.type ?? 'button'}
39
- disabled={disabled || loading}
40
- className={cn(
41
- 'btn',
42
- `btn-${variant}`,
43
- `btn-size-${size}`,
44
- iconOnly && 'btn-icon',
45
- loading && 'btn-loading',
46
- className,
47
- )}
48
- {...rest}
49
- >
50
- {loading ? <span className="btn-spinner" aria-hidden /> : null}
51
- {children}
52
- </button>
53
- );
54
- },
55
- );
@@ -1,40 +0,0 @@
1
- // src/components/Card.tsx — wrapper card.
2
-
3
- import type { HTMLAttributes, ReactNode } from 'react';
4
- import { cn } from '../lib/utils';
5
-
6
- export type CardProps = HTMLAttributes<HTMLDivElement> & {
7
- variant?: 'elevated' | 'outlined' | 'filled';
8
- interactive?: boolean;
9
- children?: ReactNode;
10
- };
11
-
12
- export function Card({
13
- variant = 'elevated',
14
- interactive = false,
15
- className,
16
- children,
17
- ...rest
18
- }: CardProps) {
19
- return (
20
- <div
21
- className={cn(
22
- 'card',
23
- `card-${variant}`,
24
- interactive && 'card-interactive',
25
- className,
26
- )}
27
- {...rest}
28
- >
29
- {children}
30
- </div>
31
- );
32
- }
33
-
34
- export function CardTitle({ children, className }: { children: ReactNode; className?: string }) {
35
- return <h3 className={cn('card-title', className)}>{children}</h3>;
36
- }
37
-
38
- export function CardMeta({ children, className }: { children: ReactNode; className?: string }) {
39
- return <div className={cn('card-meta', className)}>{children}</div>;
40
- }
@@ -1,30 +0,0 @@
1
- // src/components/EmptyState.tsx — friendly empty / error placeholder.
2
-
3
- import type { ReactNode } from 'react';
4
- import { Inbox } from 'lucide-react';
5
- import { cn } from '../lib/utils';
6
-
7
- export type EmptyStateProps = {
8
- icon?: ReactNode;
9
- title: string;
10
- message?: ReactNode;
11
- action?: ReactNode;
12
- className?: string;
13
- };
14
-
15
- export function EmptyState({
16
- icon,
17
- title,
18
- message,
19
- action,
20
- className,
21
- }: EmptyStateProps) {
22
- return (
23
- <div className={cn('empty-state', className)}>
24
- <div className="empty-icon">{icon ?? <Inbox size={32} />}</div>
25
- <div className="empty-title">{title}</div>
26
- {message && <div className="empty-message">{message}</div>}
27
- {action && <div className="empty-action">{action}</div>}
28
- </div>
29
- );
30
- }
@@ -1,137 +0,0 @@
1
- // src/components/Modal.tsx — modal context + provider + portal.
2
-
3
- import {
4
- createContext,
5
- useCallback,
6
- useContext,
7
- useEffect,
8
- useState,
9
- type ReactNode,
10
- } from 'react';
11
- import { createPortal } from 'react-dom';
12
- import { X } from 'lucide-react';
13
-
14
- export type ModalProps = {
15
- title?: string;
16
- children: ReactNode;
17
- footer?: ReactNode;
18
- onClose?: () => void;
19
- width?: number;
20
- };
21
-
22
- export type ModalApi = {
23
- open: (props: ModalProps) => string;
24
- close: (id?: string) => void;
25
- };
26
-
27
- const ModalContext = createContext<ModalApi | null>(null);
28
-
29
- export function useModal(): ModalApi {
30
- const ctx = useContext(ModalContext);
31
- if (!ctx) {
32
- return { open: () => '', close: () => undefined };
33
- }
34
- return ctx;
35
- }
36
-
37
- type OpenModal = ModalProps & { id: string };
38
-
39
- export function ModalProvider({ children }: { children: ReactNode }) {
40
- const [stack, setStack] = useState<OpenModal[]>([]);
41
-
42
- const close = useCallback((id?: string) => {
43
- setStack((cur) => {
44
- if (!id) return [];
45
- const idx = cur.findIndex((m) => m.id === id);
46
- if (idx === -1) return cur;
47
- // close only the top-most matching
48
- if (idx !== cur.length - 1) return cur;
49
- return cur.slice(0, -1);
50
- });
51
- }, []);
52
-
53
- const open = useCallback((props: ModalProps) => {
54
- const id = `m${Math.random().toString(36).slice(2, 9)}`;
55
- setStack((cur) => [...cur, { ...props, id }]);
56
- return id;
57
- }, []);
58
-
59
- // Escape key — close the topmost
60
- useEffect(() => {
61
- if (stack.length === 0) return;
62
- const handler = (e: KeyboardEvent) => {
63
- if (e.key === 'Escape') {
64
- e.stopPropagation();
65
- close();
66
- }
67
- };
68
- document.addEventListener('keydown', handler);
69
- return () => document.removeEventListener('keydown', handler);
70
- }, [stack.length, close]);
71
-
72
- return (
73
- <ModalContext.Provider value={{ open, close }}>
74
- {children}
75
- {typeof document !== 'undefined' &&
76
- createPortal(
77
- <div className="modal-stack" aria-hidden={stack.length === 0}>
78
- {stack.map((m, idx) => (
79
- <ModalShell
80
- key={m.id}
81
- modal={m}
82
- onClose={() => {
83
- m.onClose?.();
84
- close(m.id);
85
- }}
86
- depth={idx}
87
- />
88
- ))}
89
- </div>,
90
- document.body,
91
- )}
92
- </ModalContext.Provider>
93
- );
94
- }
95
-
96
- function ModalShell({
97
- modal,
98
- onClose,
99
- depth,
100
- }: {
101
- modal: OpenModal;
102
- onClose: () => void;
103
- depth: number;
104
- }) {
105
- return (
106
- <div
107
- className="modal-backdrop"
108
- onClick={(e) => {
109
- if (e.target === e.currentTarget) onClose();
110
- }}
111
- style={depth > 0 ? { background: 'rgba(0,0,0,0.4)' } : undefined}
112
- >
113
- <div
114
- className="modal"
115
- role="dialog"
116
- aria-modal="true"
117
- style={modal.width ? { maxWidth: modal.width } : undefined}
118
- >
119
- {modal.title && (
120
- <header className="modal-header">
121
- <h2 className="modal-title">{modal.title}</h2>
122
- <button
123
- type="button"
124
- className="icon-btn"
125
- aria-label="Close"
126
- onClick={onClose}
127
- >
128
- <X size={16} />
129
- </button>
130
- </header>
131
- )}
132
- <div className="modal-body">{modal.children}</div>
133
- {modal.footer && <footer className="modal-footer">{modal.footer}</footer>}
134
- </div>
135
- </div>
136
- );
137
- }
@@ -1,19 +0,0 @@
1
- // src/components/Spinner.tsx — loading indicator.
2
-
3
- import { cn } from '../lib/utils';
4
-
5
- export type SpinnerProps = {
6
- size?: 'sm' | 'md' | 'lg';
7
- className?: string;
8
- label?: string;
9
- };
10
-
11
- export function Spinner({ size = 'md', className, label }: SpinnerProps) {
12
- return (
13
- <span
14
- className={cn('spinner', `spinner-${size}`, className)}
15
- role="status"
16
- aria-label={label || 'Loading'}
17
- />
18
- );
19
- }
@@ -1,25 +0,0 @@
1
- // src/components/StatusBadge.tsx — small colored pill.
2
-
3
- import type { ReactNode } from 'react';
4
- import { cn } from '../lib/utils';
5
-
6
- export type StatusBadgeProps = {
7
- kind?: 'success' | 'warning' | 'error' | 'info' | 'accent' | 'neutral';
8
- children: ReactNode;
9
- className?: string;
10
- dot?: boolean;
11
- };
12
-
13
- export function StatusBadge({
14
- kind = 'neutral',
15
- children,
16
- className,
17
- dot = false,
18
- }: StatusBadgeProps) {
19
- return (
20
- <span className={cn('badge', `badge-${kind}`, className)}>
21
- {dot && <span className="badge-dot" />}
22
- {children}
23
- </span>
24
- );
25
- }
@@ -1,28 +0,0 @@
1
- // src/components/Tag.tsx — pill for tags.
2
-
3
- import type { ReactNode } from 'react';
4
- import { cn } from '../lib/utils';
5
-
6
- export type TagProps = {
7
- children: ReactNode;
8
- className?: string;
9
- onRemove?: () => void;
10
- };
11
-
12
- export function Tag({ children, className, onRemove }: TagProps) {
13
- return (
14
- <span className={cn('tag', className)}>
15
- {children}
16
- {onRemove && (
17
- <button
18
- type="button"
19
- className="tag-remove"
20
- aria-label={`Remove ${typeof children === 'string' ? children : 'tag'}`}
21
- onClick={onRemove}
22
- >
23
- ×
24
- </button>
25
- )}
26
- </span>
27
- );
28
- }