@polderlabs/bizar 4.3.0 → 4.4.1
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/bizar-dash/src/server/opencode-sdk.mjs +72 -0
- package/bizar-dash/src/server/routes/background.mjs +92 -0
- package/bizar-dash/src/server/routes/chat.mjs +300 -123
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +26 -0
- package/bizar-dash/src/server/routes/tasks.mjs +59 -1
- package/bizar-dash/src/server/task-delegator.mjs +154 -8
- package/bizar-dash/src/server/tasks-store.mjs +50 -2
- package/bizar-dash/src/web/components/background/AttachButton.tsx +96 -0
- package/bizar-dash/src/web/components/background/TmuxAttachCard.tsx +122 -0
- package/bizar-dash/src/web/components/chat/AgentNode.tsx +127 -0
- package/bizar-dash/src/web/components/chat/AgentTree.tsx +80 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +76 -0
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +144 -0
- package/bizar-dash/src/web/components/chat/ChatRail.tsx +387 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +28 -7
- package/bizar-dash/src/web/components/chat/JumpToLatest.tsx +58 -0
- package/bizar-dash/src/web/components/chat/MessageBlock.tsx +260 -0
- package/bizar-dash/src/web/components/chat/SessionRowMenu.tsx +206 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +33 -5
- package/bizar-dash/src/web/components/chat/_legacy.ts +30 -0
- package/bizar-dash/src/web/components/chat/index.ts +11 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +345 -167
- package/bizar-dash/src/web/components/tasks/BacklogPanel.css +109 -0
- package/bizar-dash/src/web/components/tasks/BacklogPanel.tsx +209 -0
- package/bizar-dash/src/web/styles/chat.css +1536 -133
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +3 -0
- package/bizar-dash/src/web/views/Chat.tsx +147 -57
- package/bizar-dash/src/web/views/Tasks.tsx +23 -1
- package/cli/bg.mjs +94 -71
- package/cli/bin.mjs +36 -8
- package/cli/service-controller.mjs +587 -0
- package/cli/service-controller.test.mjs +92 -0
- package/cli/service.mjs +162 -14
- package/config/agents/baldr.md +2 -0
- package/config/agents/browser-harness.md +2 -0
- package/config/agents/forseti.md +2 -0
- package/config/agents/frigg.md +2 -0
- package/config/agents/heimdall.md +2 -0
- package/config/agents/hermod.md +2 -0
- package/config/agents/mimir.md +2 -0
- package/config/agents/odin.md +2 -0
- package/config/agents/quick.md +2 -0
- package/config/agents/semble-search.md +2 -0
- package/config/agents/thor.md +2 -0
- package/config/agents/tyr.md +2 -0
- package/config/agents/vidarr.md +2 -0
- package/config/agents/vor.md +2 -0
- package/config/opencode.json.template +1 -0
- package/install.sh +448 -787
- package/package.json +2 -2
- package/packages/sdk/package.json +20 -0
- package/packages/sdk/src/client.ts +5 -0
- package/packages/sdk/src/errors.ts +11 -2
- package/packages/sdk/src/index.ts +19 -0
- package/packages/sdk/src/opencode-events.ts +134 -0
- package/packages/sdk/src/opencode-types.ts +66 -0
- package/packages/sdk/src/opencode.ts +335 -0
|
@@ -21,6 +21,7 @@ import { useModal } from '../components/Modal';
|
|
|
21
21
|
import { useToast } from '../components/Toast';
|
|
22
22
|
import { BgStatusBadge } from '../components/BgStatusBadge';
|
|
23
23
|
import { openKillConfirmDialog } from '../components/KillConfirmDialog';
|
|
24
|
+
import { TmuxAttachCard } from '../components/background/TmuxAttachCard';
|
|
24
25
|
import { api } from '../lib/api';
|
|
25
26
|
import { Ws } from '../lib/ws';
|
|
26
27
|
import { cn, formatTime, truncate } from '../lib/utils';
|
|
@@ -283,6 +284,8 @@ function BackgroundAgentCard({
|
|
|
283
284
|
<Trash2 size={14} /> Kill
|
|
284
285
|
</Button>
|
|
285
286
|
</div>
|
|
287
|
+
|
|
288
|
+
<TmuxAttachCard instance={inst} />
|
|
286
289
|
</Card>
|
|
287
290
|
);
|
|
288
291
|
}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
// src/views/Chat.tsx — Gemini-inspired dark chat orchestrator.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// src/views/Chat.tsx — Gemini-inspired dark chat orchestrator (v3.22).
|
|
2
|
+
//
|
|
3
|
+
// v3.22 — redesigned layout: 3-column grid (rail / thread / info).
|
|
4
|
+
// The legacy `.chat-shell / .chat-body / .chat-sessions / .chat-main /
|
|
5
|
+
// .chat-info` wrapper classes are still available for MobileChat (which
|
|
6
|
+
// uses its own view); this file uses the new `.chat-page` grid.
|
|
7
|
+
//
|
|
8
|
+
// The `chat-thread` section is a vertical grid (head / scroll / composer).
|
|
9
|
+
// The scrollable message list lives inside the legacy `ChatThread`
|
|
10
|
+
// component (still wrapped in its own `.chat-thread` div with className
|
|
11
|
+
// `legacy` to opt into the original styles — see chat.css).
|
|
4
12
|
|
|
5
|
-
import { useEffect, useRef, useState } from 'react';
|
|
13
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
14
|
import { ChatTopBar } from '../components/chat/ChatTopBar';
|
|
7
|
-
import {
|
|
15
|
+
import { ChatRail } from '../components/chat/ChatRail';
|
|
8
16
|
import { ChatThread } from '../components/chat/ChatThread';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
17
|
+
import { ChatComposer } from '../components/chat/ChatComposer';
|
|
18
|
+
import { ChatInfoPanel } from '../components/chat/ChatInfoPanel';
|
|
19
|
+
import { JumpToLatest } from '../components/chat/JumpToLatest';
|
|
11
20
|
import { useChat } from '../components/chat/useChat';
|
|
12
21
|
import { useSlashCommands } from '../components/chat/useSlashCommands';
|
|
13
22
|
import { useToast } from '../components/Toast';
|
|
@@ -28,7 +37,6 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
28
37
|
const toast = useToast();
|
|
29
38
|
const modal = useModal();
|
|
30
39
|
|
|
31
|
-
// ── Delegate to useChat: messages, sessions, send, polling ──────────────
|
|
32
40
|
const chat = useChat(snapshot, settings, initialTaskId ?? '');
|
|
33
41
|
|
|
34
42
|
// ── Composer state (local) ───────────────────────────────────────────────
|
|
@@ -39,12 +47,13 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
39
47
|
const [creating, setCreating] = useState(false);
|
|
40
48
|
|
|
41
49
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
42
|
-
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
43
50
|
|
|
44
51
|
// ── Slash commands ───────────────────────────────────────────────────────
|
|
45
52
|
const { allCommands, suggestions, setQuery } = useSlashCommands(snapshot);
|
|
46
53
|
|
|
47
|
-
useEffect(() => {
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
setQuery(text);
|
|
56
|
+
}, [text, setQuery]);
|
|
48
57
|
|
|
49
58
|
// ── File attach ──────────────────────────────────────────────────────────
|
|
50
59
|
const onAttach = () => fileInputRef.current?.click();
|
|
@@ -68,6 +77,7 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
68
77
|
setText('');
|
|
69
78
|
setQuery('');
|
|
70
79
|
chat.onSend(msg, agent, model, attachments);
|
|
80
|
+
chat.jumpToLatest();
|
|
71
81
|
};
|
|
72
82
|
|
|
73
83
|
// ── Create session ───────────────────────────────────────────────────────
|
|
@@ -96,7 +106,9 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
96
106
|
children: <p style={{ margin: 0 }}>This action cannot be undone.</p>,
|
|
97
107
|
footer: (
|
|
98
108
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
99
|
-
<Button variant="secondary" size="sm" onClick={() => modal.close()}>
|
|
109
|
+
<Button variant="secondary" size="sm" onClick={() => modal.close()}>
|
|
110
|
+
Cancel
|
|
111
|
+
</Button>
|
|
100
112
|
<Button
|
|
101
113
|
variant="danger"
|
|
102
114
|
size="sm"
|
|
@@ -112,7 +124,42 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
112
124
|
});
|
|
113
125
|
};
|
|
114
126
|
|
|
115
|
-
// ──
|
|
127
|
+
// ── Derived: sessions with display state overlaid ────────────────────────
|
|
128
|
+
const displaySessions = useMemo(
|
|
129
|
+
() => chat.sessions.map((s) => chat.getSessionDisplay(s)),
|
|
130
|
+
[chat.sessions, chat.getSessionDisplay],
|
|
131
|
+
);
|
|
132
|
+
const displayOpencodeSessions = useMemo(
|
|
133
|
+
() => chat.opencodeSessions.map((s) => chat.getSessionDisplay(s)),
|
|
134
|
+
[chat.opencodeSessions, chat.getSessionDisplay],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// ── Current session for the thread head subtitle ────────────────────────
|
|
138
|
+
const activeSessionDisplay = useMemo(() => {
|
|
139
|
+
const id = chat.activeSource === 'opencode'
|
|
140
|
+
? chat.activeOpencodeSessionId ?? ''
|
|
141
|
+
: chat.sessionId;
|
|
142
|
+
if (!id) return null;
|
|
143
|
+
return (
|
|
144
|
+
displaySessions.find((s) => s.id === id) ??
|
|
145
|
+
displayOpencodeSessions.find((s) => s.id === id) ??
|
|
146
|
+
null
|
|
147
|
+
);
|
|
148
|
+
}, [
|
|
149
|
+
chat.activeSource,
|
|
150
|
+
chat.activeOpencodeSessionId,
|
|
151
|
+
chat.sessionId,
|
|
152
|
+
displaySessions,
|
|
153
|
+
displayOpencodeSessions,
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
const threadSubtitle = (() => {
|
|
157
|
+
const base = `${agent || 'Odin'} · ${snapshot.activeProject?.name ?? 'no project'}`;
|
|
158
|
+
const state = activeSessionDisplay?.state ?? 'idle';
|
|
159
|
+
if (state === 'streaming') return `Replying · ${base}`;
|
|
160
|
+
if (state === 'awaiting') return `Your turn · ${base}`;
|
|
161
|
+
return `${base} · idle`;
|
|
162
|
+
})();
|
|
116
163
|
|
|
117
164
|
return (
|
|
118
165
|
<div className="chat-shell">
|
|
@@ -125,35 +172,71 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
125
172
|
onToggleInfo={() => {}}
|
|
126
173
|
onOpenOverview={() => setActiveTab?.('overview')}
|
|
127
174
|
/>
|
|
128
|
-
<div className="chat-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
<div className="chat-page">
|
|
176
|
+
{/* ── rail ──────────────────────────────────────────────────────── */}
|
|
177
|
+
<ChatRail
|
|
178
|
+
sessions={displaySessions}
|
|
179
|
+
opencodeSessions={displayOpencodeSessions}
|
|
180
|
+
activeSessionId={chat.sessionId}
|
|
181
|
+
activeOpencodeSessionId={chat.activeOpencodeSessionId}
|
|
182
|
+
activeProject={snapshot.activeProject}
|
|
183
|
+
creating={creating}
|
|
184
|
+
onCreateSession={handleCreateSession}
|
|
185
|
+
onSelectSession={chat.selectBizarSession}
|
|
186
|
+
onSelectOpencodeSession={(s) => chat.loadOpencodeSession(s.id)}
|
|
187
|
+
onRenameSession={(id, title) => chat.renameSession(id, title)}
|
|
188
|
+
onDeleteSession={(id) => chat.deleteSession(id)}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
{/* ── thread ───────────────────────────────────────────────────── */}
|
|
192
|
+
<section className="chat-thread-section">
|
|
193
|
+
<div className="chat-thread-head">
|
|
194
|
+
<div>
|
|
195
|
+
<div className="chat-thread-title">
|
|
196
|
+
{activeSessionDisplay?.title ?? chat.sessionId ?? 'New chat'}
|
|
197
|
+
</div>
|
|
198
|
+
<div
|
|
199
|
+
className={`chat-thread-sub chat-muted state-${activeSessionDisplay?.state ?? 'idle'}`}
|
|
200
|
+
>
|
|
201
|
+
<span className="chat-thread-dot" />
|
|
202
|
+
{threadSubtitle}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<div className="chat-thread-actions">
|
|
206
|
+
<button className="btn btn-ghost" title="Rename" type="button">
|
|
207
|
+
<span className="mono">rename</span>
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="chat-thread-scroll" ref={chat.listRef} onScroll={chat.handleScroll}>
|
|
213
|
+
<ChatThread
|
|
214
|
+
messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
|
|
215
|
+
loading={chat.loading}
|
|
216
|
+
activeProject={snapshot.activeProject}
|
|
217
|
+
sessionId={
|
|
218
|
+
chat.activeSource === 'opencode'
|
|
219
|
+
? (chat.activeOpencodeSessionId ?? chat.sessionId)
|
|
220
|
+
: chat.sessionId
|
|
221
|
+
}
|
|
222
|
+
pinned={chat.pinned}
|
|
223
|
+
onPickSuggestion={(t) => setText(t)}
|
|
224
|
+
onCopy={(m) => chat.copyMessage(m as Parameters<typeof chat.copyMessage>[0])}
|
|
225
|
+
onDelete={handleDelete}
|
|
226
|
+
onTogglePin={chat.togglePin}
|
|
227
|
+
onRegenerate={chat.onRegenerate}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{!chat.stickToBottom && (
|
|
232
|
+
<JumpToLatest
|
|
233
|
+
streaming={activeSessionDisplay?.state === 'streaming'}
|
|
234
|
+
newMessageCount={chat.newMessageCount}
|
|
235
|
+
onClick={chat.jumpToLatest}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
<ChatComposer
|
|
157
240
|
agent={agent}
|
|
158
241
|
setAgent={setAgent}
|
|
159
242
|
model={model}
|
|
@@ -168,24 +251,31 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
168
251
|
onPickSuggestion={(cmd) => setText(`${cmd.split(' ')[0]} `)}
|
|
169
252
|
agents={snapshot.agents || []}
|
|
170
253
|
onAttach={onAttach}
|
|
171
|
-
sessionsOpen={true}
|
|
172
|
-
infoOpen={true}
|
|
173
254
|
/>
|
|
174
|
-
<input
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
|
|
181
|
-
pinned={chat.pinned}
|
|
182
|
-
agent={agent}
|
|
183
|
-
model={model}
|
|
184
|
-
agents={snapshot.agents || []}
|
|
185
|
-
mcps={snapshot.mcps || []}
|
|
186
|
-
allCommands={allCommands}
|
|
255
|
+
<input
|
|
256
|
+
ref={fileInputRef}
|
|
257
|
+
type="file"
|
|
258
|
+
multiple
|
|
259
|
+
style={{ display: 'none' }}
|
|
260
|
+
onChange={onFiles}
|
|
187
261
|
/>
|
|
188
|
-
</
|
|
262
|
+
</section>
|
|
263
|
+
|
|
264
|
+
{/* ── info panel ──────────────────────────────────────────────── */}
|
|
265
|
+
<ChatInfoPanel
|
|
266
|
+
sessionId={
|
|
267
|
+
chat.activeSource === 'opencode'
|
|
268
|
+
? (chat.activeOpencodeSessionId ?? chat.sessionId)
|
|
269
|
+
: chat.sessionId
|
|
270
|
+
}
|
|
271
|
+
messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
|
|
272
|
+
pinned={chat.pinned}
|
|
273
|
+
agent={agent}
|
|
274
|
+
model={model}
|
|
275
|
+
agents={snapshot.agents || []}
|
|
276
|
+
mcps={snapshot.mcps || []}
|
|
277
|
+
allCommands={allCommands}
|
|
278
|
+
/>
|
|
189
279
|
</div>
|
|
190
280
|
</div>
|
|
191
281
|
);
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
RefreshCw,
|
|
31
31
|
Send,
|
|
32
32
|
FileText,
|
|
33
|
+
Inbox,
|
|
33
34
|
} from 'lucide-react';
|
|
34
35
|
import { Button } from '../components/Button';
|
|
35
36
|
import { Card, CardTitle } from '../components/Card';
|
|
@@ -45,6 +46,7 @@ import { api, enhancePrompt } from '../lib/api';
|
|
|
45
46
|
import { cn, formatRelative, priorityColors, autoTitleFromContent } from '../lib/utils';
|
|
46
47
|
import type { Agent, Settings, Snapshot, Task } from '../lib/types';
|
|
47
48
|
import { openArtifactViewer } from '../components/ArtifactViewer';
|
|
49
|
+
import { BacklogPanel } from '../components/tasks/BacklogPanel';
|
|
48
50
|
|
|
49
51
|
type Props = {
|
|
50
52
|
snapshot: Snapshot;
|
|
@@ -319,6 +321,10 @@ export function Tasks({ snapshot, refreshSnapshot, setActiveTab }: Props) {
|
|
|
319
321
|
openArtifactViewer(modal, artifactId);
|
|
320
322
|
};
|
|
321
323
|
|
|
324
|
+
// v3.22 — Backlog panel visibility.
|
|
325
|
+
const [showBacklog, setShowBacklog] = useState(false);
|
|
326
|
+
const backlogCount = (snapshot.tasks || []).filter((t) => t.status === 'backlog').length;
|
|
327
|
+
|
|
322
328
|
return (
|
|
323
329
|
<div className="view view-tasks">
|
|
324
330
|
<header className="view-header">
|
|
@@ -447,9 +453,25 @@ export function Tasks({ snapshot, refreshSnapshot, setActiveTab }: Props) {
|
|
|
447
453
|
>
|
|
448
454
|
<Plus size={14} /> Add
|
|
449
455
|
</Button>
|
|
456
|
+
{backlogCount > 0 && (
|
|
457
|
+
<Button
|
|
458
|
+
variant={showBacklog ? 'accent' : 'ghost'}
|
|
459
|
+
size="sm"
|
|
460
|
+
onClick={() => setShowBacklog((v) => !v)}
|
|
461
|
+
title={showBacklog ? 'Hide backlog' : 'Show backlog'}
|
|
462
|
+
>
|
|
463
|
+
<Inbox size={14} />
|
|
464
|
+
Backlog
|
|
465
|
+
<span className="badge">{backlogCount}</span>
|
|
466
|
+
</Button>
|
|
467
|
+
)}
|
|
450
468
|
</div>
|
|
451
469
|
</div>
|
|
452
470
|
|
|
471
|
+
{showBacklog && backlogCount > 0 && (
|
|
472
|
+
<BacklogPanel agents={snapshot.agents || []} onRefresh={refreshSnapshot} />
|
|
473
|
+
)}
|
|
474
|
+
|
|
453
475
|
{selected.size > 0 && (
|
|
454
476
|
<div className="task-bulk-bar">
|
|
455
477
|
<span className="text-sm">
|
|
@@ -900,7 +922,7 @@ function formatDuration(seconds: number): string {
|
|
|
900
922
|
return `${m}m`;
|
|
901
923
|
}
|
|
902
924
|
|
|
903
|
-
function openTaskModal(
|
|
925
|
+
export function openTaskModal(
|
|
904
926
|
modal: ReturnType<typeof useModal>,
|
|
905
927
|
toast: ReturnType<typeof useToast>,
|
|
906
928
|
task: Task | null,
|
package/cli/bg.mjs
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* kill <id> Kill a running agent
|
|
16
16
|
* logs <id> Tail the agent's log file
|
|
17
17
|
*
|
|
18
|
-
* The "view" subcommand is the headline feature: it
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* anything?" — you can
|
|
18
|
+
* The "view" subcommand (v3.22) is the headline feature: it creates a
|
|
19
|
+
* tmux control session (`bizar-bg-view`) with one pane per running
|
|
20
|
+
* agent (each pane attached to the agent's own tmux session, tiled).
|
|
21
|
+
* This is the antidote to "is the agent doing anything?" — you can
|
|
22
|
+
* SEE them all working in parallel.
|
|
22
23
|
*/
|
|
23
24
|
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
24
25
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -245,59 +246,112 @@ async function runKill(instanceId) {
|
|
|
245
246
|
// --- view ----------------------------------------------------------------
|
|
246
247
|
|
|
247
248
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
249
|
+
* View subcommand (v3.22):
|
|
250
|
+
* - Detects tmux. If missing, prints per-instance `bizar bg logs <id>`
|
|
251
|
+
* fallback instructions and exits 0.
|
|
252
|
+
* - Creates a fresh tmux control session `bizar-bg-view` with one pane
|
|
253
|
+
* per running instance, each pane running `tmux attach -t <session>`.
|
|
254
|
+
* - Caps at 16 panes (beyond that, tmux tiled layout degrades).
|
|
255
|
+
* - Zero running instances → prints "no running agents" and exits 0.
|
|
251
256
|
*/
|
|
252
|
-
|
|
257
|
+
|
|
258
|
+
async function runView() {
|
|
253
259
|
const all = readAllBgInstances();
|
|
254
|
-
const
|
|
260
|
+
const tmuxSessions = listBgrTmuxSessions();
|
|
255
261
|
const running = all.filter(
|
|
262
|
+
(e) => e.data.status === 'running' || e.data.status === 'pending',
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Zero running agents — not an error.
|
|
266
|
+
if (running.length === 0) {
|
|
267
|
+
console.log(chalk.dim('\n No running background agents.\n'));
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Tmux presence check. If missing, print fallback instructions and exit 0
|
|
272
|
+
// (not an error — the operator can still use per-instance logs).
|
|
273
|
+
if (!which('tmux')) {
|
|
274
|
+
console.log(chalk.yellow('\n ⚠ tmux is not installed on this host.\n'));
|
|
275
|
+
console.log(chalk.dim(' You can inspect individual agents via:\n'));
|
|
276
|
+
for (const r of running) {
|
|
277
|
+
console.log(chalk.cyan(` bizar bg logs ${r.data.instanceId}`));
|
|
278
|
+
}
|
|
279
|
+
console.log();
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Filter to instances that actually have a live tmux session.
|
|
284
|
+
const active = running.filter(
|
|
256
285
|
(e) =>
|
|
257
|
-
(e.data.status === 'running' || e.data.status === 'pending') &&
|
|
258
286
|
e.data.sessionId &&
|
|
259
|
-
|
|
287
|
+
tmuxSessions.includes(tmuxSessionForSessionId(e.data.sessionId)),
|
|
260
288
|
);
|
|
261
289
|
|
|
262
|
-
if (
|
|
263
|
-
|
|
290
|
+
if (active.length === 0) {
|
|
291
|
+
console.log(chalk.dim('\n No agents with active tmux sessions found.\n'));
|
|
292
|
+
console.log(chalk.dim(' State files exist but no tmux sessions are running.\n'));
|
|
293
|
+
console.log(chalk.dim(' Run `bizar bg list` to inspect state.\n'));
|
|
294
|
+
return 0;
|
|
264
295
|
}
|
|
265
296
|
|
|
266
|
-
//
|
|
297
|
+
// Cap at 16 panes — beyond that, tiled layout is unusable.
|
|
298
|
+
const capped = active.slice(0, 16);
|
|
299
|
+
const overflow = active.length - capped.length;
|
|
300
|
+
|
|
301
|
+
// Kill any stale bizar-bg-view session, then create a fresh one.
|
|
267
302
|
try {
|
|
268
|
-
execFileSync('tmux', ['kill-session', '-t', '
|
|
303
|
+
execFileSync('tmux', ['kill-session', '-t', 'bizar-bg-view'], { stdio: 'pipe' });
|
|
269
304
|
} catch {
|
|
270
|
-
|
|
305
|
+
/* didn't exist */
|
|
271
306
|
}
|
|
272
307
|
execFileSync('tmux', [
|
|
273
|
-
'new-session', '-d', '-s', '
|
|
308
|
+
'new-session', '-d', '-s', 'bizar-bg-view',
|
|
274
309
|
'-x', '220', '-y', '50',
|
|
275
310
|
], { stdio: 'pipe' });
|
|
276
311
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const r = running[i];
|
|
289
|
-
const tmuxName = tmuxSessionForSessionId(r.data.sessionId);
|
|
290
|
-
const logPath = r.data.logPath || join(homedir(), '.cache', 'bizar', 'logs', `${r.data.sessionId}.log`);
|
|
291
|
-
const header = `echo "═══ ${r.data.agent} │ ${r.data.instanceId} │ ${tmuxName} ═══"; tail -n 200 -F "${logPath}"`;
|
|
292
|
-
// Use send-keys so the echo+tail are sent in sequence, then Enter
|
|
293
|
-
// to run them. Target the pane by index in the bgr_view session.
|
|
312
|
+
// Pane 0 → first instance
|
|
313
|
+
const firstSession = tmuxSessionForSessionId(capped[0].data.sessionId);
|
|
314
|
+
execFileSync('tmux', [
|
|
315
|
+
'send-keys', '-t', 'bizar-bg-view:0.0',
|
|
316
|
+
`tmux attach -t ${firstSession}`, 'Enter',
|
|
317
|
+
], { stdio: 'pipe' });
|
|
318
|
+
|
|
319
|
+
// Split for remaining instances (2..N)
|
|
320
|
+
for (let i = 1; i < capped.length; i++) {
|
|
321
|
+
execFileSync('tmux', ['split-window', '-v', '-t', 'bizar-bg-view'], { stdio: 'pipe' });
|
|
322
|
+
const sessionName = tmuxSessionForSessionId(capped[i].data.sessionId);
|
|
294
323
|
execFileSync('tmux', [
|
|
295
|
-
'send-keys', '-t', `
|
|
296
|
-
|
|
324
|
+
'send-keys', '-t', `bizar-bg-view:0.${i}`,
|
|
325
|
+
`tmux attach -t ${sessionName}`, 'Enter',
|
|
297
326
|
], { stdio: 'pipe' });
|
|
298
327
|
}
|
|
299
328
|
|
|
300
|
-
|
|
329
|
+
// Tiled layout for a clean grid.
|
|
330
|
+
execFileSync('tmux', ['select-layout', '-t', 'bizar-bg-view', 'tiled'], { stdio: 'pipe' });
|
|
331
|
+
|
|
332
|
+
console.log(chalk.green(`\n ✓ Built tmux view session with ${capped.length} pane(s)\n`));
|
|
333
|
+
|
|
334
|
+
if (overflow > 0) {
|
|
335
|
+
console.log(chalk.yellow(` ⚠ ${overflow} more agent(s) not shown (16-pane cap):\n`));
|
|
336
|
+
for (const r of active.slice(16)) {
|
|
337
|
+
const sn = tmuxSessionForSessionId(r.data.sessionId);
|
|
338
|
+
console.log(chalk.dim(` bizar bg logs ${r.data.instanceId} — tmux attach -t ${sn}`));
|
|
339
|
+
}
|
|
340
|
+
console.log();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Open an OS terminal window attached to the control session.
|
|
344
|
+
const open = openTerminalAttached('bizar-bg-view');
|
|
345
|
+
if (open.ok) {
|
|
346
|
+
console.log(chalk.green(` ✓ Opened ${open.terminal} attached to "bizar-bg-view"\n`));
|
|
347
|
+
console.log(chalk.dim(' Tip: `tmux attach -t bizar-bg-view` from any terminal.\n'));
|
|
348
|
+
} else {
|
|
349
|
+
console.log(chalk.yellow(` ⚠ Could not open terminal: ${open.error}\n`));
|
|
350
|
+
console.log(chalk.dim(' Run in a terminal you can see:\n'));
|
|
351
|
+
console.log(chalk.cyan(` tmux attach -t bizar-bg-view\n`));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return 0;
|
|
301
355
|
}
|
|
302
356
|
|
|
303
357
|
/**
|
|
@@ -362,39 +416,7 @@ function openTerminalAttached(tmuxSession) {
|
|
|
362
416
|
return { ok: false, error: `unsupported platform: ${platform}` };
|
|
363
417
|
}
|
|
364
418
|
|
|
365
|
-
async function runView() {
|
|
366
|
-
if (!which('tmux')) {
|
|
367
|
-
console.log(chalk.red('\n ✗ tmux is not installed. Install it and retry.'));
|
|
368
|
-
console.log(chalk.dim(' macOS: brew install tmux'));
|
|
369
|
-
console.log(chalk.dim(' Linux: apt-get install tmux (or your distro equivalent)'));
|
|
370
|
-
console.log(chalk.dim(' Windows: choco install tmux\n'));
|
|
371
|
-
return 1;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const build = buildTmuxControlSession();
|
|
375
|
-
if (!build.ok) {
|
|
376
|
-
if (build.reason === 'no-running-agents') {
|
|
377
|
-
console.log(chalk.dim('\n No running background agents to view.\n'));
|
|
378
|
-
console.log(chalk.dim(' Spawn one with `bizar_spawn_background` (Odin) or via the dashboard,\n'));
|
|
379
|
-
console.log(chalk.dim(' then re-run this command.\n'));
|
|
380
|
-
return 1;
|
|
381
|
-
}
|
|
382
|
-
console.log(chalk.red(`\n ✗ Failed to build control session: ${build.error || build.reason}\n`));
|
|
383
|
-
return 1;
|
|
384
|
-
}
|
|
385
419
|
|
|
386
|
-
console.log(chalk.green(`\n ✓ Built tmux control session with ${build.count} pane(s)\n`));
|
|
387
|
-
const open = openTerminalAttached(build.session);
|
|
388
|
-
if (open.ok) {
|
|
389
|
-
console.log(chalk.green(` ✓ Opened ${open.terminal} attached to tmux session "${build.session}"\n`));
|
|
390
|
-
console.log(chalk.dim(` Tip: \`tmux attach -t ${build.session}\` from any terminal.\n`));
|
|
391
|
-
} else {
|
|
392
|
-
console.log(chalk.yellow(` ⚠ Could not open a terminal window: ${open.error}\n`));
|
|
393
|
-
console.log(chalk.dim(' Run one of the following in a terminal you can see:\n'));
|
|
394
|
-
console.log(chalk.cyan(` tmux attach -t ${build.session}\n`));
|
|
395
|
-
}
|
|
396
|
-
return 0;
|
|
397
|
-
}
|
|
398
420
|
|
|
399
421
|
// --- Help ----------------------------------------------------------------
|
|
400
422
|
|
|
@@ -416,8 +438,9 @@ function showHelp() {
|
|
|
416
438
|
and a log file (default ~/.cache/bizar/logs/<sessionId>.log).
|
|
417
439
|
|
|
418
440
|
The \`view\` subcommand is the headline feature: it creates a new
|
|
419
|
-
tmux session (\`
|
|
420
|
-
|
|
441
|
+
tmux session (\`bizar-bg-view\`) with one pane per running agent,
|
|
442
|
+
each pane attached to the agent's own tmux session (tiled layout),
|
|
443
|
+
then opens a new OS terminal window attached to it. You can see all
|
|
421
444
|
your agents working in parallel at a glance.
|
|
422
445
|
`);
|
|
423
446
|
}
|
package/cli/bin.mjs
CHANGED
|
@@ -291,17 +291,27 @@ function showServiceHelp() {
|
|
|
291
291
|
bizar service — Manage the background service daemon
|
|
292
292
|
|
|
293
293
|
Usage:
|
|
294
|
-
bizar service start
|
|
295
|
-
bizar service stop
|
|
296
|
-
bizar service status
|
|
297
|
-
bizar service logs
|
|
298
|
-
bizar service follow
|
|
294
|
+
bizar service start Start the service in background
|
|
295
|
+
bizar service stop Stop the running service
|
|
296
|
+
bizar service status Show whether the service is running
|
|
297
|
+
bizar service logs Tail the service log
|
|
298
|
+
bizar service follow Follow the service log until Ctrl-C
|
|
299
|
+
bizar service install Register with systemd / launchd / scheduled task
|
|
300
|
+
bizar service install --force Re-install even when the unit matches
|
|
301
|
+
bizar service uninstall Remove the OS-level autostart
|
|
302
|
+
bizar service uninstall --force Force-uninstall even when nothing is registered
|
|
299
303
|
|
|
300
304
|
Description:
|
|
301
305
|
The service watches per-project schedules (cron / interval / once)
|
|
302
306
|
and runs them at the right time. It logs to
|
|
303
307
|
${bizarConfigDir}/service.log and writes its PID to
|
|
304
308
|
${bizarConfigDir}/service.pid.
|
|
309
|
+
|
|
310
|
+
install registers the daemon under the OS init system — systemd user
|
|
311
|
+
unit on Linux, launchd LaunchAgent on macOS, scheduled task
|
|
312
|
+
("BizarDashboardService", ONSTART, HIGHEST) on Windows. After
|
|
313
|
+
install, a normal user does not need to run \`bizar service start\`
|
|
314
|
+
for the dashboard background process — the OS does it at login.
|
|
305
315
|
`);
|
|
306
316
|
}
|
|
307
317
|
|
|
@@ -613,9 +623,11 @@ async function runTestGate() {
|
|
|
613
623
|
}
|
|
614
624
|
|
|
615
625
|
/**
|
|
616
|
-
* Service commands — start / stop / status / logs.
|
|
626
|
+
* Service commands — start / stop / status / logs / install / uninstall.
|
|
617
627
|
*
|
|
618
|
-
* Implementation lives in cli/service.mjs
|
|
628
|
+
* Implementation lives in cli/service.mjs. The install / uninstall
|
|
629
|
+
* branches delegate to cli/service-controller.mjs for OS-level
|
|
630
|
+
* registration (systemd / launchd / schtasks).
|
|
619
631
|
*/
|
|
620
632
|
async function runServiceCommand(sub) {
|
|
621
633
|
const { runService } = await import('./service.mjs');
|
|
@@ -820,7 +832,23 @@ async function runDash(dashArgs) {
|
|
|
820
832
|
|
|
821
833
|
switch (sub) {
|
|
822
834
|
case 'start':
|
|
823
|
-
|
|
835
|
+
// v4.4.0 — When `--bg` is passed, spawn the dashboard as a
|
|
836
|
+
// detached child process via the dash module's startInBackground
|
|
837
|
+
// helper, then return. Without this, runDash calls startDashboard
|
|
838
|
+
// in-process; once the bin's main() resolves, the bin process
|
|
839
|
+
// exits and takes the dashboard's HTTP server with it. The
|
|
840
|
+
// downstream effect is "(intermediate value)(intermediate value)
|
|
841
|
+
// (intermediate value) is not a function or its return value is
|
|
842
|
+
// not iterable" — a pre-existing crash signature in this codepath.
|
|
843
|
+
if (subOpts.bg) {
|
|
844
|
+
// Spawn the dashboard as a detached child process. The child's
|
|
845
|
+
// argv is `node cli.mjs start --bg` so the dash module's own
|
|
846
|
+
// CLI dispatch hits the background branch and runs
|
|
847
|
+
// startDashboard in the child, not the parent.
|
|
848
|
+
await dashModule.startInBackground(['start', ...(subOpts.subArgs || [])]);
|
|
849
|
+
} else {
|
|
850
|
+
await dashModule.start(subOpts);
|
|
851
|
+
}
|
|
824
852
|
break;
|
|
825
853
|
case 'stop':
|
|
826
854
|
await dashModule.stop(subOpts);
|