@polderlabs/bizar 4.2.4 → 4.4.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 (63) hide show
  1. package/bizar-dash/src/server/api.mjs +2 -0
  2. package/bizar-dash/src/server/opencode-sdk.mjs +72 -0
  3. package/bizar-dash/src/server/routes/background.mjs +92 -0
  4. package/bizar-dash/src/server/routes/chat.mjs +300 -123
  5. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +452 -0
  6. package/bizar-dash/src/server/routes/tasks.mjs +59 -1
  7. package/bizar-dash/src/server/serve-info.mjs +124 -1
  8. package/bizar-dash/src/server/task-delegator.mjs +154 -8
  9. package/bizar-dash/src/server/tasks-store.mjs +50 -2
  10. package/bizar-dash/src/web/components/background/AttachButton.tsx +96 -0
  11. package/bizar-dash/src/web/components/background/TmuxAttachCard.tsx +122 -0
  12. package/bizar-dash/src/web/components/chat/AgentNode.tsx +127 -0
  13. package/bizar-dash/src/web/components/chat/AgentTree.tsx +80 -0
  14. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +76 -0
  15. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +144 -0
  16. package/bizar-dash/src/web/components/chat/ChatRail.tsx +387 -0
  17. package/bizar-dash/src/web/components/chat/ChatThread.tsx +28 -7
  18. package/bizar-dash/src/web/components/chat/JumpToLatest.tsx +58 -0
  19. package/bizar-dash/src/web/components/chat/MessageBlock.tsx +260 -0
  20. package/bizar-dash/src/web/components/chat/SessionList.tsx +15 -5
  21. package/bizar-dash/src/web/components/chat/SessionRowMenu.tsx +206 -0
  22. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +33 -5
  23. package/bizar-dash/src/web/components/chat/_legacy.ts +30 -0
  24. package/bizar-dash/src/web/components/chat/index.ts +11 -0
  25. package/bizar-dash/src/web/components/chat/useChat.ts +426 -62
  26. package/bizar-dash/src/web/components/tasks/BacklogPanel.css +109 -0
  27. package/bizar-dash/src/web/components/tasks/BacklogPanel.tsx +209 -0
  28. package/bizar-dash/src/web/lib/types.ts +1 -0
  29. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +7 -5
  30. package/bizar-dash/src/web/styles/chat.css +1536 -133
  31. package/bizar-dash/src/web/views/BackgroundAgents.tsx +3 -0
  32. package/bizar-dash/src/web/views/Chat.tsx +147 -55
  33. package/bizar-dash/src/web/views/Tasks.tsx +23 -1
  34. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +273 -0
  35. package/cli/bg.mjs +94 -71
  36. package/cli/bin.mjs +19 -7
  37. package/cli/service-controller.mjs +587 -0
  38. package/cli/service-controller.test.mjs +92 -0
  39. package/cli/service.mjs +162 -14
  40. package/config/agents/baldr.md +2 -0
  41. package/config/agents/browser-harness.md +2 -0
  42. package/config/agents/forseti.md +2 -0
  43. package/config/agents/frigg.md +2 -0
  44. package/config/agents/heimdall.md +2 -0
  45. package/config/agents/hermod.md +2 -0
  46. package/config/agents/mimir.md +2 -0
  47. package/config/agents/odin.md +2 -0
  48. package/config/agents/quick.md +2 -0
  49. package/config/agents/semble-search.md +2 -0
  50. package/config/agents/thor.md +2 -0
  51. package/config/agents/tyr.md +2 -0
  52. package/config/agents/vidarr.md +2 -0
  53. package/config/agents/vor.md +2 -0
  54. package/config/opencode.json.template +1 -0
  55. package/install.sh +448 -787
  56. package/package.json +2 -2
  57. package/packages/sdk/package.json +20 -0
  58. package/packages/sdk/src/client.ts +5 -0
  59. package/packages/sdk/src/errors.ts +11 -2
  60. package/packages/sdk/src/index.ts +19 -0
  61. package/packages/sdk/src/opencode-events.ts +134 -0
  62. package/packages/sdk/src/opencode-types.ts +66 -0
  63. package/packages/sdk/src/opencode.ts +335 -0
@@ -0,0 +1,109 @@
1
+ /* src/web/components/tasks/BacklogPanel.css — v3.22 */
2
+
3
+ .backlog-panel {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 8px;
7
+ padding: 12px 16px;
8
+ background: var(--card-bg, #1a1a2e);
9
+ border-bottom: 1px solid var(--border-color, #2d2d44);
10
+ }
11
+
12
+ .backlog-panel-header {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ gap: 8px;
17
+ }
18
+
19
+ .backlog-panel-title {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 6px;
23
+ font-size: 13px;
24
+ font-weight: 600;
25
+ color: var(--text-secondary, #a0a0b8);
26
+ }
27
+
28
+ .backlog-panel-header-actions {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 4px;
32
+ }
33
+
34
+ .backlog-list {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 4px;
38
+ max-height: 280px;
39
+ overflow-y: auto;
40
+ }
41
+
42
+ .backlog-item {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 4px;
46
+ padding: 8px 10px;
47
+ background: var(--bg-secondary, #16162a);
48
+ border: 1px solid var(--border-color, #2d2d44);
49
+ border-radius: 6px;
50
+ }
51
+
52
+ .backlog-item-head {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 6px;
56
+ flex-wrap: wrap;
57
+ }
58
+
59
+ .backlog-item-title {
60
+ font-size: 13px;
61
+ font-weight: 500;
62
+ color: var(--text-primary, #e0e0f0);
63
+ flex: 1;
64
+ min-width: 0;
65
+ overflow: hidden;
66
+ text-overflow: ellipsis;
67
+ white-space: nowrap;
68
+ }
69
+
70
+ .backlog-item-badge {
71
+ font-size: 10px;
72
+ padding: 1px 5px;
73
+ border-radius: 3px;
74
+ background: var(--bg-tertiary, #1e1e38);
75
+ color: var(--text-muted, #7070a0);
76
+ white-space: nowrap;
77
+ }
78
+
79
+ .backlog-item-desc {
80
+ font-size: 12px;
81
+ color: var(--text-muted, #7070a0);
82
+ line-height: 1.4;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ .backlog-item-footer {
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: space-between;
92
+ gap: 8px;
93
+ }
94
+
95
+ .backlog-item-actions {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 2px;
99
+ }
100
+
101
+ .backlog-empty {
102
+ display: flex;
103
+ flex-direction: column;
104
+ align-items: center;
105
+ gap: 6px;
106
+ padding: 20px 0;
107
+ color: var(--text-muted, #7070a0);
108
+ font-size: 13px;
109
+ }
@@ -0,0 +1,209 @@
1
+ // src/web/components/tasks/BacklogPanel.tsx — v3.22
2
+ // Backlog panel: lists parked tasks, promote individually or all-at-once.
3
+ import { useEffect, useState } from 'react';
4
+ import {
5
+ ArrowUp,
6
+ Trash2,
7
+ Sparkles,
8
+ RefreshCw,
9
+ Inbox,
10
+ } from 'lucide-react';
11
+ import { Button } from '../Button';
12
+ import { useToast } from '../Toast';
13
+ import { api } from '../../lib/api';
14
+ import { formatRelative } from '../../lib/utils';
15
+ import type { Task, Agent } from '../../lib/types';
16
+
17
+ type BacklogItemProps = {
18
+ task: Task;
19
+ agents: Agent[];
20
+ onPromote: (id: string) => void;
21
+ onDelete: (id: string) => void;
22
+ onEdit: (task: Task) => void;
23
+ onRefresh: () => Promise<void>;
24
+ };
25
+
26
+ function BacklogItem({ task, agents, onPromote, onDelete, onEdit, onRefresh }: BacklogItemProps) {
27
+ const toast = useToast();
28
+
29
+ const handlePromote = async () => {
30
+ try {
31
+ await api.post(`/tasks/${encodeURIComponent(task.id)}/promote`);
32
+ toast.success('Promoted to queued.', 1500);
33
+ onPromote(task.id);
34
+ } catch (err) {
35
+ toast.error(`Promote failed: ${(err as Error).message}`);
36
+ }
37
+ };
38
+
39
+ const handleDelete = async () => {
40
+ if (!confirm('Delete this task?')) return;
41
+ try {
42
+ await api.del(`/tasks/${encodeURIComponent(task.id)}`);
43
+ toast.success('Deleted.', 1500);
44
+ onDelete(task.id);
45
+ } catch (err) {
46
+ toast.error(`Delete failed: ${(err as Error).message}`);
47
+ }
48
+ };
49
+
50
+ const priorityDot: Record<string, string> = {
51
+ high: 'var(--warning)',
52
+ normal: 'var(--info)',
53
+ low: 'var(--muted)',
54
+ };
55
+
56
+ return (
57
+ <div className="backlog-item" data-task-id={task.id}>
58
+ <div className="backlog-item-head">
59
+ <span
60
+ className="priority-dot"
61
+ style={{ background: priorityDot[task.priority] || 'var(--info)' }}
62
+ />
63
+ <span className="backlog-item-title">{task.title}</span>
64
+ {task.assignee && (
65
+ <span className="backlog-item-badge">@{task.assignee}</span>
66
+ )}
67
+ {task.tags?.length > 0 && (
68
+ <span className="backlog-item-badge">{task.tags.join(', ')}</span>
69
+ )}
70
+ </div>
71
+ {task.description && (
72
+ <div className="backlog-item-desc">{task.description.slice(0, 120)}</div>
73
+ )}
74
+ <div className="backlog-item-footer">
75
+ <span className="muted text-sm tabular-nums">
76
+ {formatRelative(task.createdAt)}
77
+ </span>
78
+ <div className="backlog-item-actions">
79
+ <button
80
+ type="button"
81
+ className="icon-btn"
82
+ title="Promote to queued"
83
+ onClick={handlePromote}
84
+ >
85
+ <ArrowUp size={13} />
86
+ </button>
87
+ <button
88
+ type="button"
89
+ className="icon-btn"
90
+ title="Edit"
91
+ onClick={() => onEdit(task)}
92
+ >
93
+ <Sparkles size={13} />
94
+ </button>
95
+ <button
96
+ type="button"
97
+ className="icon-btn icon-btn-danger"
98
+ title="Delete"
99
+ onClick={handleDelete}
100
+ >
101
+ <Trash2 size={13} />
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ type Props = {
110
+ /** Current list of agents for the edit modal. */
111
+ agents: Agent[];
112
+ /** Called after any mutation so the parent can refresh snapshot. */
113
+ onRefresh: () => Promise<void>;
114
+ };
115
+
116
+ export function BacklogPanel({ agents, onRefresh }: Props) {
117
+ const toast = useToast();
118
+ const [tasks, setTasks] = useState<Task[]>([]);
119
+ const [loading, setLoading] = useState(false);
120
+
121
+ const load = async () => {
122
+ try {
123
+ setLoading(true);
124
+ const r = await api.get<{ tasks: Task[] }>('/tasks/backlog');
125
+ setTasks(Array.isArray(r.tasks) ? r.tasks : []);
126
+ } catch (err) {
127
+ toast.error(`Backlog load failed: ${(err as Error).message}`);
128
+ } finally {
129
+ setLoading(false);
130
+ }
131
+ };
132
+
133
+ useEffect(() => { load(); }, []);
134
+
135
+ const handlePromote = async (id: string) => {
136
+ setTasks((cur) => cur.filter((t) => t.id !== id));
137
+ await onRefresh();
138
+ };
139
+
140
+ const handleDelete = async (id: string) => {
141
+ setTasks((cur) => cur.filter((t) => t.id !== id));
142
+ await onRefresh();
143
+ };
144
+
145
+ const handleEdit = (_task: Task) => {
146
+ // Editing is wired through the parent Tasks view's modal helper.
147
+ // Keep this no-op so the icon click is at least visible; the parent
148
+ // provides the real edit affordance via the kanban.
149
+ };
150
+
151
+ const handlePromoteAll = async () => {
152
+ if (tasks.length === 0) return;
153
+ try {
154
+ const r = await api.post<{ affected: { id: string; ok: boolean }[] }>('/tasks/promote-batch', {
155
+ ids: tasks.map((t) => t.id),
156
+ });
157
+ const ok = r.affected?.filter((a) => a.ok).length ?? 0;
158
+ toast.success(`Promoted ${ok} task(s).`, 2000);
159
+ setTasks([]);
160
+ await onRefresh();
161
+ } catch (err) {
162
+ toast.error(`Promote all failed: ${(err as Error).message}`);
163
+ }
164
+ };
165
+
166
+ return (
167
+ <div className="backlog-panel">
168
+ <div className="backlog-panel-header">
169
+ <span className="backlog-panel-title">
170
+ <Inbox size={15} />
171
+ Backlog ({tasks.length})
172
+ </span>
173
+ <div className="backlog-panel-header-actions">
174
+ {tasks.length > 0 && (
175
+ <Button variant="ghost" size="sm" onClick={handlePromoteAll} title="Promote all to queued">
176
+ <ArrowUp size={12} /> Promote all
177
+ </Button>
178
+ )}
179
+ <Button variant="ghost" size="sm" onClick={load} title="Refresh backlog" aria-label="Refresh backlog">
180
+ <RefreshCw size={12} className={loading ? 'animate-spin' : ''} />
181
+ </Button>
182
+ </div>
183
+ </div>
184
+
185
+ {loading ? (
186
+ <div className="backlog-empty">Loading…</div>
187
+ ) : tasks.length === 0 ? (
188
+ <div className="backlog-empty">
189
+ <Inbox size={28} />
190
+ <span>Backlog is empty.</span>
191
+ </div>
192
+ ) : (
193
+ <div className="backlog-list">
194
+ {tasks.map((t) => (
195
+ <BacklogItem
196
+ key={t.id}
197
+ task={t}
198
+ agents={agents}
199
+ onPromote={handlePromote}
200
+ onDelete={handleDelete}
201
+ onEdit={handleEdit}
202
+ onRefresh={onRefresh}
203
+ />
204
+ ))}
205
+ </div>
206
+ )}
207
+ </div>
208
+ );
209
+ }
@@ -302,6 +302,7 @@ export type Overview = {
302
302
  };
303
303
 
304
304
  export type ChatMessage = {
305
+ id?: string;
305
306
  role: 'user' | 'assistant' | 'system' | string;
306
307
  content?: string;
307
308
  message?: string;
@@ -121,19 +121,21 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
121
121
  sessions={chat.sessions}
122
122
  opencodeSessions={chat.opencodeSessions}
123
123
  activeSessionId={chat.sessionId}
124
+ activeOpencodeSessionId={chat.activeOpencodeSessionId}
124
125
  activeProject={snapshot.activeProject}
125
126
  onCreateSession={handleCreateSession}
126
- onSelectSession={chat.loadChat}
127
+ onSelectSession={chat.selectBizarSession}
128
+ onSelectOpencodeSession={(s) => chat.loadOpencodeSession(s.id)}
127
129
  creating={creating}
128
130
  />
129
131
  </aside>
130
132
 
131
133
  <main className="chat-main">
132
134
  <ChatThread
133
- messages={chat.messages}
135
+ messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
134
136
  loading={chat.loading}
135
137
  activeProject={snapshot.activeProject}
136
- sessionId={chat.sessionId}
138
+ sessionId={chat.activeSource === 'opencode' ? (chat.activeOpencodeSessionId ?? chat.sessionId) : chat.sessionId}
137
139
  pinned={chat.pinned}
138
140
  onPickSuggestion={(t) => setText(t)}
139
141
  onCopy={(m) => chat.copyMessage(m as Parameters<typeof chat.copyMessage>[0])}
@@ -164,8 +166,8 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
164
166
 
165
167
  <aside className={`chat-info ${!infoOpen ? 'chat-info-hidden' : ''}`}>
166
168
  <InfoPanel
167
- sessionId={chat.sessionId}
168
- messages={chat.messages}
169
+ sessionId={chat.activeSource === 'opencode' ? (chat.activeOpencodeSessionId ?? chat.sessionId) : chat.sessionId}
170
+ messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
169
171
  pinned={chat.pinned}
170
172
  agent={agent}
171
173
  model={model}