@polderlabs/bizar 5.4.0 → 5.5.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/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js → EnvVarsSection-L413CQEI.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js.map → EnvVarsSection-L413CQEI.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js → MobileChat-0kGQb1S6.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js.map → MobileChat-0kGQb1S6.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CVW9VhEo.js.map → MobileSettings-Cq1oFIdO.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js → Toast-C6f1zFJ2.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js.map → Toast-C6f1zFJ2.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-OMEGx5KA.js → icons-BsWXWkEF.js} +121 -126
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +1 -0
- package/bizar-dash/dist/assets/main-BklYy02e.css +1 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +18 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +1 -0
- package/bizar-dash/dist/assets/{mobile-XN2P7YfM.js.map → mobile-CsaN4CxH.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-D6V-7WnA.css → mobile-layout-CJnZNLy3.css} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js.map +1 -0
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js → useSlashCommands-DGJcC9iQ.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js.map → useSlashCommands-DGJcC9iQ.js.map} +1 -1
- package/bizar-dash/dist/index.html +8 -8
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/backup-store.mjs +6 -1
- package/bizar-dash/src/server/bg-spawner.mjs +574 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +242 -0
- package/bizar-dash/src/server/memory-store.mjs +43 -3
- package/bizar-dash/src/server/routes/background.mjs +143 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +10 -0
- package/bizar-dash/src/server/routes/memory.mjs +8 -0
- package/bizar-dash/src/server/server.mjs +22 -0
- package/bizar-dash/src/web/App.tsx +3 -0
- package/bizar-dash/src/web/MobileApp.tsx +1 -10
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
- package/bizar-dash/src/web/components/SettingsNav.tsx +1 -10
- package/bizar-dash/src/web/lib/types.ts +39 -1
- package/bizar-dash/src/web/styles/main.css +43 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +7 -5
- package/bizar-dash/src/web/styles/settings.css +46 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
- package/bizar-dash/src/web/views/Settings.tsx +12 -12
- package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
- package/bizar-dash/tests/BackgroundAgents.test.tsx +160 -0
- package/bizar-dash/tests/background-pause-resume.test.mjs +46 -0
- package/bizar-dash/tests/background-spawn-from-ui.test.mjs +36 -0
- package/bizar-dash/tests/background-steer.test.mjs +42 -0
- package/bizar-dash/tests/lightrag-startup-hook.test.mjs +206 -0
- package/bizar-dash/tests/memory-auto-reindex.test.mjs +196 -0
- package/bizar-dash/tests/memory-cli.test.mjs +1 -1
- package/bizar-dash/tests/memory-default-vault.test.mjs +90 -3
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +131 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +36 -0
- package/bizar-dash/tests/settings-layout.test.tsx +3 -3
- package/bizar-dash/tests/settings-nav.test.tsx +7 -5
- package/cli/bin.mjs +16 -0
- package/cli/commands/deploy/docker.mjs +2 -1
- package/cli/commands/lightrag.mjs +171 -0
- package/cli/commands/service.mjs +7 -0
- package/cli/commands/util.mjs +8 -1
- package/cli/init.mjs +1 -1
- package/cli/post-install-smoke.mjs +230 -0
- package/cli/provision.mjs +177 -0
- package/cli/provision.test.mjs +180 -0
- package/cli/service-controller.mjs +378 -28
- package/cli/service-controller.test.mjs +100 -0
- package/cli/service-env.mjs +139 -0
- package/cli/service.mjs +23 -0
- package/config/agents/_shared/AGENT_BASELINE.md +6 -6
- package/install.sh +87 -6
- package/package.json +1 -1
- package/plugins/bizar/index.ts +147 -0
- package/plugins/bizar/src/background-state.ts +84 -5
- package/plugins/bizar/src/background.ts +388 -10
- package/plugins/bizar/src/hooks/memory-inject.ts +247 -0
- package/plugins/bizar/src/hooks/memory-write-on-end.ts +188 -0
- package/plugins/bizar/src/opencode-runner.ts +87 -1
- package/plugins/bizar/src/tools/bg-pause.ts +77 -0
- package/plugins/bizar/src/tools/bg-report-progress.ts +101 -0
- package/plugins/bizar/src/tools/bg-resume.ts +72 -0
- package/plugins/bizar/src/tools/bg-send-message.ts +105 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +13 -6
- package/plugins/bizar/src/tools/bg-status.ts +10 -0
- package/plugins/bizar/src/tools/memory-list.ts +135 -0
- package/plugins/bizar/src/tools/memory-read.ts +142 -0
- package/plugins/bizar/src/tools/memory-search.ts +228 -0
- package/plugins/bizar/src/tools/memory-write.ts +183 -0
- package/plugins/bizar/tests/memory-write-on-end.test.ts +92 -0
- package/plugins/bizar/tests/tools/bg-pause.test.ts +64 -0
- package/plugins/bizar/tests/tools/bg-report-progress.test.ts +82 -0
- package/plugins/bizar/tests/tools/bg-resume.test.ts +43 -0
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +46 -0
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +0 -1
- package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +0 -1
- package/bizar-dash/dist/assets/main-CMAGZouj.js +0 -16
- package/bizar-dash/dist/assets/main-CMAGZouj.js.map +0 -1
- package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +0 -1
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
// src/views/BackgroundAgents.tsx — dashboard tab listing active background-agent
|
|
2
|
-
// instances
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// instances.
|
|
3
|
+
//
|
|
4
|
+
// v5.x — full UI redesign for the 7 background-agent integration gaps:
|
|
5
|
+
// - "+ Spawn" button + `SpawnAgentModal` for direct dashboard spawns
|
|
6
|
+
// - Pause / Resume / Steer actions per instance
|
|
7
|
+
// - WebSocket-driven streaming output (`bg:output` events)
|
|
8
|
+
// - Tool-call history collapsible list per instance
|
|
9
|
+
// - Polling 5s safety net preserved
|
|
6
10
|
import { useCallback, useEffect, useRef, useState, type UIEvent } from 'react';
|
|
7
11
|
import {
|
|
8
12
|
Activity as ActivityIcon,
|
|
9
13
|
Copy,
|
|
10
14
|
Eye,
|
|
11
15
|
Loader2,
|
|
16
|
+
Pause,
|
|
17
|
+
PauseCircle,
|
|
18
|
+
Play,
|
|
19
|
+
Plus,
|
|
12
20
|
RefreshCw,
|
|
21
|
+
Send,
|
|
13
22
|
Terminal,
|
|
14
23
|
Trash2,
|
|
15
24
|
} from 'lucide-react';
|
|
@@ -22,6 +31,7 @@ import { useToast } from '../components/Toast';
|
|
|
22
31
|
import { BgStatusBadge } from '../components/BgStatusBadge';
|
|
23
32
|
import { openKillConfirmDialog } from '../components/KillConfirmDialog';
|
|
24
33
|
import { TmuxAttachCard } from '../components/background/TmuxAttachCard';
|
|
34
|
+
import { SpawnAgentModal } from './SpawnAgentModal';
|
|
25
35
|
import { api } from '../lib/api';
|
|
26
36
|
import { Ws } from '../lib/ws';
|
|
27
37
|
import { cn, formatTime, truncate } from '../lib/utils';
|
|
@@ -30,6 +40,7 @@ import type {
|
|
|
30
40
|
BackgroundOutputResponse,
|
|
31
41
|
BackgroundTmuxResponse,
|
|
32
42
|
BgInstance,
|
|
43
|
+
BgToolCall,
|
|
33
44
|
Settings,
|
|
34
45
|
Snapshot,
|
|
35
46
|
} from '../lib/types';
|
|
@@ -43,11 +54,16 @@ type Props = {
|
|
|
43
54
|
};
|
|
44
55
|
|
|
45
56
|
const POLL_INTERVAL_MS = 5_000;
|
|
46
|
-
const OUTPUT_REFRESH_MS =
|
|
57
|
+
const OUTPUT_REFRESH_MS = 3_000; // fallback poll (used if WS is unavailable)
|
|
47
58
|
const PROMPT_TRUNCATE = 120;
|
|
59
|
+
const STREAM_BUFFER_LIMIT = 32_000; // chars
|
|
48
60
|
|
|
49
61
|
function isActive(b: BgInstance): boolean {
|
|
50
|
-
return b.status === 'pending' || b.status === 'running';
|
|
62
|
+
return b.status === 'pending' || b.status === 'running' || b.status === 'paused';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isPaused(b: BgInstance): boolean {
|
|
66
|
+
return b.status === 'paused';
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
function formatDuration(ms: number): string {
|
|
@@ -68,6 +84,7 @@ export function BackgroundAgents(_props: Props) {
|
|
|
68
84
|
const [loading, setLoading] = useState(true);
|
|
69
85
|
const [error, setError] = useState<string | null>(null);
|
|
70
86
|
const [, setTick] = useState(0);
|
|
87
|
+
const [showSpawn, setShowSpawn] = useState(false);
|
|
71
88
|
|
|
72
89
|
const load = useCallback(async () => {
|
|
73
90
|
try {
|
|
@@ -94,15 +111,17 @@ export function BackgroundAgents(_props: Props) {
|
|
|
94
111
|
return () => clearInterval(id);
|
|
95
112
|
}, []);
|
|
96
113
|
|
|
97
|
-
// WS subscription — refetch on any background
|
|
98
|
-
// Matches the Settings.tsx pattern of opening a private WS instance.
|
|
99
|
-
// Acceptable because background change volume is low and the dashboard
|
|
100
|
-
// already creates parallel WS connections for other tabs.
|
|
114
|
+
// WS subscription — refetch on any background:* event.
|
|
101
115
|
useEffect(() => {
|
|
102
116
|
const ws = new Ws();
|
|
103
117
|
const off = ws.on((msg) => {
|
|
104
118
|
const t = (msg as { type?: string }).type;
|
|
105
|
-
if (
|
|
119
|
+
if (
|
|
120
|
+
t === 'background:change' ||
|
|
121
|
+
t === 'background:cleanup' ||
|
|
122
|
+
t === 'bg:progress' ||
|
|
123
|
+
t === 'bg:tool-call'
|
|
124
|
+
) {
|
|
106
125
|
load();
|
|
107
126
|
}
|
|
108
127
|
});
|
|
@@ -133,11 +152,40 @@ export function BackgroundAgents(_props: Props) {
|
|
|
133
152
|
|
|
134
153
|
const onKill = (b: BgInstance) => {
|
|
135
154
|
openKillConfirmDialog(modal, toast, b.instanceId, b.agent || b.prompt, () => {
|
|
136
|
-
// Optimistic remove — the WS broadcast + polling safety net will reconcile.
|
|
137
155
|
setInstances((cur) => cur.filter((x) => x.instanceId !== b.instanceId));
|
|
138
156
|
});
|
|
139
157
|
};
|
|
140
158
|
|
|
159
|
+
const onPause = async (b: BgInstance) => {
|
|
160
|
+
try {
|
|
161
|
+
const r = await api.post<{ ok: boolean; error?: string }>(
|
|
162
|
+
`/background/${encodeURIComponent(b.instanceId)}/pause`,
|
|
163
|
+
{},
|
|
164
|
+
);
|
|
165
|
+
if (!r?.ok) toast.error(r?.error || 'Pause failed.');
|
|
166
|
+
else toast.info(`Paused ${b.instanceId.slice(0, 14)}…`);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
toast.error(`Pause failed: ${(err as Error).message}`);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const onResume = async (b: BgInstance) => {
|
|
173
|
+
try {
|
|
174
|
+
const r = await api.post<{ ok: boolean; error?: string }>(
|
|
175
|
+
`/background/${encodeURIComponent(b.instanceId)}/resume`,
|
|
176
|
+
{},
|
|
177
|
+
);
|
|
178
|
+
if (!r?.ok) toast.error(r?.error || 'Resume failed.');
|
|
179
|
+
else toast.info(`Resumed ${b.instanceId.slice(0, 14)}…`);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
toast.error(`Resume failed: ${(err as Error).message}`);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const onSteer = (b: BgInstance) => {
|
|
186
|
+
openSteerModal(modal, toast, b, load);
|
|
187
|
+
};
|
|
188
|
+
|
|
141
189
|
// ─── Render ───────────────────────────────────────────────────────────
|
|
142
190
|
|
|
143
191
|
if (loading) {
|
|
@@ -177,16 +225,34 @@ export function BackgroundAgents(_props: Props) {
|
|
|
177
225
|
{active.length} active · {instances.length} total in store
|
|
178
226
|
</p>
|
|
179
227
|
</div>
|
|
180
|
-
<
|
|
181
|
-
<
|
|
182
|
-
|
|
228
|
+
<div className="bg-active-header-actions">
|
|
229
|
+
<Button variant="primary" size="sm" onClick={() => setShowSpawn(true)}>
|
|
230
|
+
<Plus size={14} /> Spawn
|
|
231
|
+
</Button>
|
|
232
|
+
<Button variant="ghost" size="sm" onClick={load} title="Refresh now">
|
|
233
|
+
<RefreshCw size={14} /> Refresh
|
|
234
|
+
</Button>
|
|
235
|
+
</div>
|
|
183
236
|
</div>
|
|
184
237
|
|
|
238
|
+
<SpawnAgentModal
|
|
239
|
+
open={showSpawn}
|
|
240
|
+
onClose={() => setShowSpawn(false)}
|
|
241
|
+
onCreated={() => {
|
|
242
|
+
load();
|
|
243
|
+
}}
|
|
244
|
+
/>
|
|
245
|
+
|
|
185
246
|
{active.length === 0 ? (
|
|
186
247
|
<EmptyState
|
|
187
248
|
icon={<ActivityIcon size={32} />}
|
|
188
249
|
title="No active background agents"
|
|
189
|
-
message="
|
|
250
|
+
message="Use the Spawn button above or call bizar_spawn_background from an agent."
|
|
251
|
+
action={
|
|
252
|
+
<Button variant="primary" onClick={() => setShowSpawn(true)}>
|
|
253
|
+
<Plus size={14} /> Spawn your first agent
|
|
254
|
+
</Button>
|
|
255
|
+
}
|
|
190
256
|
/>
|
|
191
257
|
) : (
|
|
192
258
|
<div className="bg-active-list">
|
|
@@ -197,6 +263,9 @@ export function BackgroundAgents(_props: Props) {
|
|
|
197
263
|
onViewOutput={() => onOpenOutput(b)}
|
|
198
264
|
onOpenTmux={() => onOpenTmux(b)}
|
|
199
265
|
onKill={() => onKill(b)}
|
|
266
|
+
onPause={isPaused(b) ? undefined : () => onPause(b)}
|
|
267
|
+
onResume={isPaused(b) ? () => onResume(b) : undefined}
|
|
268
|
+
onSteer={() => onSteer(b)}
|
|
200
269
|
/>
|
|
201
270
|
))}
|
|
202
271
|
</div>
|
|
@@ -212,16 +281,23 @@ function BackgroundAgentCard({
|
|
|
212
281
|
onViewOutput,
|
|
213
282
|
onOpenTmux,
|
|
214
283
|
onKill,
|
|
284
|
+
onPause,
|
|
285
|
+
onResume,
|
|
286
|
+
onSteer,
|
|
215
287
|
}: {
|
|
216
288
|
inst: BgInstance;
|
|
217
289
|
onViewOutput: () => void;
|
|
218
290
|
onOpenTmux: () => void;
|
|
219
291
|
onKill: () => void;
|
|
292
|
+
onPause?: () => void;
|
|
293
|
+
onResume?: () => void;
|
|
294
|
+
onSteer: () => void;
|
|
220
295
|
}) {
|
|
221
296
|
const startedMs = typeof inst.startedAt === 'number' ? inst.startedAt : 0;
|
|
222
297
|
const startedLabel = startedMs ? formatTime(startedMs) : '—';
|
|
223
298
|
const durationMs = startedMs ? Date.now() - startedMs : 0;
|
|
224
299
|
const prompt = truncate(inst.prompt, PROMPT_TRUNCATE);
|
|
300
|
+
const progress = typeof inst.progress === 'number' ? inst.progress : null;
|
|
225
301
|
|
|
226
302
|
return (
|
|
227
303
|
<Card className="bg-active-card" variant="elevated">
|
|
@@ -229,6 +305,11 @@ function BackgroundAgentCard({
|
|
|
229
305
|
<div className="bg-active-card-id">
|
|
230
306
|
<code className="mono">{inst.instanceId}</code>
|
|
231
307
|
<BgStatusBadge status={inst.status || 'pending'} dot />
|
|
308
|
+
{isPaused(inst) && (
|
|
309
|
+
<span className="bg-active-card-paused-tag">
|
|
310
|
+
<PauseCircle size={12} /> paused
|
|
311
|
+
</span>
|
|
312
|
+
)}
|
|
232
313
|
</div>
|
|
233
314
|
<div className="bg-active-card-agent">
|
|
234
315
|
<span className="bg-active-card-agent-label">Agent</span>
|
|
@@ -245,13 +326,24 @@ function BackgroundAgentCard({
|
|
|
245
326
|
</div>
|
|
246
327
|
)}
|
|
247
328
|
|
|
248
|
-
{
|
|
329
|
+
{progress !== null && (
|
|
249
330
|
<div className="progress-bar" aria-label="Progress">
|
|
250
331
|
<div
|
|
251
332
|
className="progress-fill"
|
|
252
|
-
style={{
|
|
333
|
+
style={{
|
|
334
|
+
width: `${
|
|
335
|
+
progress < 0
|
|
336
|
+
? 100
|
|
337
|
+
: Math.max(0, Math.min(100, progress))
|
|
338
|
+
}%`,
|
|
339
|
+
}}
|
|
253
340
|
/>
|
|
254
|
-
<span className="progress-label">
|
|
341
|
+
<span className="progress-label">
|
|
342
|
+
{progress < 0
|
|
343
|
+
? '…'
|
|
344
|
+
: `${Math.round(progress)}%`}
|
|
345
|
+
{inst.progressMessage ? ` · ${inst.progressMessage}` : ''}
|
|
346
|
+
</span>
|
|
255
347
|
</div>
|
|
256
348
|
)}
|
|
257
349
|
|
|
@@ -265,6 +357,22 @@ function BackgroundAgentCard({
|
|
|
265
357
|
<span>
|
|
266
358
|
<strong>Tool calls:</strong> {inst.toolCallCount ?? 0}
|
|
267
359
|
</span>
|
|
360
|
+
{inst.processId && (
|
|
361
|
+
<span>
|
|
362
|
+
<strong>PID:</strong> <code className="mono">{inst.processId}</code>
|
|
363
|
+
</span>
|
|
364
|
+
)}
|
|
365
|
+
{Array.isArray(inst.tags) && inst.tags.length > 0 && (
|
|
366
|
+
<span>
|
|
367
|
+
<strong>Tags:</strong>{' '}
|
|
368
|
+
{inst.tags.map((t, i) => (
|
|
369
|
+
<span key={t} className="tag-pill">
|
|
370
|
+
{t}
|
|
371
|
+
{i < inst.tags!.length - 1 ? ' ' : ''}
|
|
372
|
+
</span>
|
|
373
|
+
))}
|
|
374
|
+
</span>
|
|
375
|
+
)}
|
|
268
376
|
{inst.tmuxSession && (
|
|
269
377
|
<span className={cn(inst.tmuxActive && 'bg-active-meta-tmux-live')}>
|
|
270
378
|
<strong>tmux:</strong> <code className="mono">{inst.tmuxSession}</code>
|
|
@@ -277,6 +385,29 @@ function BackgroundAgentCard({
|
|
|
277
385
|
<Button variant="secondary" size="sm" onClick={onViewOutput}>
|
|
278
386
|
<Eye size={14} /> View output
|
|
279
387
|
</Button>
|
|
388
|
+
{onPause && (
|
|
389
|
+
<Button
|
|
390
|
+
variant="secondary"
|
|
391
|
+
size="sm"
|
|
392
|
+
onClick={onPause}
|
|
393
|
+
title="Pause subprocess (POSIX)"
|
|
394
|
+
>
|
|
395
|
+
<Pause size={14} /> Pause
|
|
396
|
+
</Button>
|
|
397
|
+
)}
|
|
398
|
+
{onResume && (
|
|
399
|
+
<Button
|
|
400
|
+
variant="secondary"
|
|
401
|
+
size="sm"
|
|
402
|
+
onClick={onResume}
|
|
403
|
+
title="Resume subprocess (POSIX)"
|
|
404
|
+
>
|
|
405
|
+
<Play size={14} /> Resume
|
|
406
|
+
</Button>
|
|
407
|
+
)}
|
|
408
|
+
<Button variant="ghost" size="sm" onClick={onSteer} title="Steer with new instructions">
|
|
409
|
+
<Send size={14} /> Steer
|
|
410
|
+
</Button>
|
|
280
411
|
<Button variant="ghost" size="sm" onClick={onOpenTmux}>
|
|
281
412
|
<Terminal size={14} /> Open in tmux
|
|
282
413
|
</Button>
|
|
@@ -305,27 +436,45 @@ function openOutputModal(
|
|
|
305
436
|
|
|
306
437
|
function OutputModalBody({ instanceId }: { instanceId: string }) {
|
|
307
438
|
const toast = useToast();
|
|
439
|
+
// Buffer of recent lines for streaming output.
|
|
440
|
+
const bufRef = useRef<string[]>([]);
|
|
308
441
|
const [output, setOutput] = useState<string>('');
|
|
309
442
|
const [loading, setLoading] = useState(true);
|
|
310
443
|
const [error, setError] = useState<string | null>(null);
|
|
311
444
|
const [available, setAvailable] = useState(true);
|
|
445
|
+
const [streamingActive, setStreamingActive] = useState(true);
|
|
446
|
+
const [toolCalls, setToolCalls] = useState<BgToolCall[]>([]);
|
|
447
|
+
const [showToolCalls, setShowToolCalls] = useState(true);
|
|
312
448
|
const refBottom = useRef<HTMLDivElement | null>(null);
|
|
313
449
|
const refAutoscroll = useRef(true);
|
|
314
450
|
|
|
451
|
+
const recomputeOutput = useCallback(() => {
|
|
452
|
+
setOutput(bufRef.current.join('\n'));
|
|
453
|
+
}, []);
|
|
454
|
+
|
|
455
|
+
// Seed from /output, then optionally from disk. The endpoint is the
|
|
456
|
+
// tmux `capture-pane` for legacy instances; for dashboard-spawned
|
|
457
|
+
// ones it gracefully returns `available: false` and we rely on WS.
|
|
315
458
|
const load = useCallback(async () => {
|
|
316
459
|
try {
|
|
317
460
|
const r = await api.get<BackgroundOutputResponse>(
|
|
318
|
-
`/background/${encodeURIComponent(instanceId)}/output?lines=
|
|
461
|
+
`/background/${encodeURIComponent(instanceId)}/output?lines=300`,
|
|
319
462
|
);
|
|
320
|
-
setOutput(r?.output ?? '');
|
|
321
463
|
setAvailable(r?.available !== false);
|
|
464
|
+
if (r?.output && bufRef.current.length === 0) {
|
|
465
|
+
// Seed the buffer exactly once. Subsequent loads are no-ops
|
|
466
|
+
// for the buffer (WS appends will keep it current).
|
|
467
|
+
const lines = r.output.split(/\r?\n/);
|
|
468
|
+
bufRef.current = lines.slice(-300);
|
|
469
|
+
recomputeOutput();
|
|
470
|
+
}
|
|
322
471
|
setError(null);
|
|
323
472
|
} catch (err) {
|
|
324
473
|
setError((err as Error)?.message || 'Failed to load output.');
|
|
325
474
|
} finally {
|
|
326
475
|
setLoading(false);
|
|
327
476
|
}
|
|
328
|
-
}, [instanceId]);
|
|
477
|
+
}, [instanceId, recomputeOutput]);
|
|
329
478
|
|
|
330
479
|
useEffect(() => {
|
|
331
480
|
load();
|
|
@@ -333,6 +482,68 @@ function OutputModalBody({ instanceId }: { instanceId: string }) {
|
|
|
333
482
|
return () => clearInterval(id);
|
|
334
483
|
}, [load]);
|
|
335
484
|
|
|
485
|
+
// Subscribe to bg:output events for true streaming. Falls back to
|
|
486
|
+
// polling if the WS never connects (we still render the seeded
|
|
487
|
+
// text). The OutputModalBody owns its own WS so streams survive
|
|
488
|
+
// modal re-opens and don't share state with the list WS.
|
|
489
|
+
useEffect(() => {
|
|
490
|
+
const ws = new Ws();
|
|
491
|
+
let isOpen = false;
|
|
492
|
+
const offStatus = ws.onStatus((s) => {
|
|
493
|
+
isOpen = s === 'connected';
|
|
494
|
+
setStreamingActive(isOpen);
|
|
495
|
+
});
|
|
496
|
+
const offMsg = ws.on((msg) => {
|
|
497
|
+
const m = msg as { type?: string; instanceId?: string; line?: string };
|
|
498
|
+
if (m.type === 'bg:output' && m.instanceId === instanceId && typeof m.line === 'string') {
|
|
499
|
+
bufRef.current.push(m.line);
|
|
500
|
+
// Cap buffer length; keep most recent N chars.
|
|
501
|
+
let total = 0;
|
|
502
|
+
for (let i = bufRef.current.length - 1; i >= 0; i--) {
|
|
503
|
+
total += (bufRef.current[i]?.length ?? 0) + 1;
|
|
504
|
+
if (total > STREAM_BUFFER_LIMIT) {
|
|
505
|
+
bufRef.current = bufRef.current.slice(i + 1);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
setOutput(bufRef.current.join('\n'));
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
return () => {
|
|
513
|
+
offStatus();
|
|
514
|
+
offMsg();
|
|
515
|
+
ws.close();
|
|
516
|
+
};
|
|
517
|
+
}, [instanceId]);
|
|
518
|
+
|
|
519
|
+
// Fetch tool-call history once (and refresh on bg:tool-call events).
|
|
520
|
+
useEffect(() => {
|
|
521
|
+
let cancelled = false;
|
|
522
|
+
const refresh = async () => {
|
|
523
|
+
try {
|
|
524
|
+
const r = await api.get<{ toolCalls: BgToolCall[] }>(
|
|
525
|
+
`/background/${encodeURIComponent(instanceId)}/tool-calls`,
|
|
526
|
+
);
|
|
527
|
+
if (!cancelled && Array.isArray(r?.toolCalls)) {
|
|
528
|
+
setToolCalls(r.toolCalls);
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
/* fall through; the list is optional */
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
refresh();
|
|
535
|
+
const ws = new Ws();
|
|
536
|
+
const off = ws.on((msg) => {
|
|
537
|
+
const m = msg as { type?: string; instanceId?: string };
|
|
538
|
+
if (m.type === 'bg:tool-call' && m.instanceId === instanceId) refresh();
|
|
539
|
+
});
|
|
540
|
+
return () => {
|
|
541
|
+
cancelled = true;
|
|
542
|
+
off();
|
|
543
|
+
ws.close();
|
|
544
|
+
};
|
|
545
|
+
}, [instanceId]);
|
|
546
|
+
|
|
336
547
|
// Auto-scroll to bottom only if user is already near the bottom.
|
|
337
548
|
useEffect(() => {
|
|
338
549
|
if (!refAutoscroll.current) return;
|
|
@@ -358,9 +569,21 @@ function OutputModalBody({ instanceId }: { instanceId: string }) {
|
|
|
358
569
|
<div className="bg-output-modal">
|
|
359
570
|
<div className="bg-output-toolbar">
|
|
360
571
|
<span className="muted">
|
|
361
|
-
{
|
|
572
|
+
{streamingActive
|
|
573
|
+
? `streaming · ${output.length} chars`
|
|
574
|
+
: available
|
|
575
|
+
? `${output.length} chars · auto-refresh ${OUTPUT_REFRESH_MS / 1000}s (WS idle)`
|
|
576
|
+
: 'Output unavailable (no tmux session)'}
|
|
362
577
|
</span>
|
|
363
578
|
<div className="bg-output-toolbar-actions">
|
|
579
|
+
<Button
|
|
580
|
+
variant="ghost"
|
|
581
|
+
size="sm"
|
|
582
|
+
onClick={() => setShowToolCalls((v) => !v)}
|
|
583
|
+
title="Toggle tool-call history"
|
|
584
|
+
>
|
|
585
|
+
{showToolCalls ? 'Hide tools' : 'Show tools'}
|
|
586
|
+
</Button>
|
|
364
587
|
<Button variant="ghost" size="sm" onClick={load} title="Refresh now">
|
|
365
588
|
<RefreshCw size={14} /> Refresh
|
|
366
589
|
</Button>
|
|
@@ -375,20 +598,69 @@ function OutputModalBody({ instanceId }: { instanceId: string }) {
|
|
|
375
598
|
</div>
|
|
376
599
|
) : error ? (
|
|
377
600
|
<div className="bg-output-error">{error}</div>
|
|
378
|
-
) : !output ? (
|
|
379
|
-
<div className="bg-output-empty muted">No output captured yet.</div>
|
|
380
601
|
) : (
|
|
381
|
-
<div className="bg-output-
|
|
382
|
-
<
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
602
|
+
<div className="bg-output-body">
|
|
603
|
+
<div className="bg-output-scroll" onScroll={onScroll}>
|
|
604
|
+
<pre className="bg-output-pre mono">
|
|
605
|
+
<code>
|
|
606
|
+
{output || (
|
|
607
|
+
<span className="muted">No output captured yet — waiting for the agent to start…</span>
|
|
608
|
+
)}
|
|
609
|
+
</code>
|
|
610
|
+
</pre>
|
|
611
|
+
<div ref={refBottom} />
|
|
612
|
+
</div>
|
|
613
|
+
{showToolCalls && (
|
|
614
|
+
<ToolCallHistory calls={toolCalls} />
|
|
615
|
+
)}
|
|
386
616
|
</div>
|
|
387
617
|
)}
|
|
388
618
|
</div>
|
|
389
619
|
);
|
|
390
620
|
}
|
|
391
621
|
|
|
622
|
+
// ─── Tool-call history panel ───────────────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
function ToolCallHistory({ calls }: { calls: BgToolCall[] }) {
|
|
625
|
+
return (
|
|
626
|
+
<aside className="bg-tool-calls" aria-label="Tool call history">
|
|
627
|
+
<h4 className="bg-tool-calls-title">Tool calls ({calls.length})</h4>
|
|
628
|
+
{calls.length === 0 ? (
|
|
629
|
+
<p className="muted bg-tool-calls-empty">No tool calls recorded yet.</p>
|
|
630
|
+
) : (
|
|
631
|
+
<ol className="bg-tool-calls-list">
|
|
632
|
+
{calls.slice().reverse().map((c) => {
|
|
633
|
+
const dur =
|
|
634
|
+
c.endedAt && c.startedAt
|
|
635
|
+
? `${Math.max(0, c.endedAt - c.startedAt)}ms`
|
|
636
|
+
: '…';
|
|
637
|
+
return (
|
|
638
|
+
<li key={c.id} className={`bg-tool-call bg-tool-call-${c.status}`}>
|
|
639
|
+
<div className="bg-tool-call-head">
|
|
640
|
+
<code className="mono bg-tool-call-name">{c.name}</code>
|
|
641
|
+
<span className={`badge badge-${c.status === 'error' ? 'danger' : c.status === 'ok' ? 'success' : 'neutral'}`}>
|
|
642
|
+
{c.status}
|
|
643
|
+
</span>
|
|
644
|
+
<span className="bg-tool-call-dur muted">{dur}</span>
|
|
645
|
+
</div>
|
|
646
|
+
{c.args && (
|
|
647
|
+
<pre className="bg-tool-call-args mono">{c.args}</pre>
|
|
648
|
+
)}
|
|
649
|
+
{c.result && (
|
|
650
|
+
<pre className="bg-tool-call-result mono">{c.result.slice(0, 240)}</pre>
|
|
651
|
+
)}
|
|
652
|
+
{c.error && (
|
|
653
|
+
<pre className="bg-tool-call-error mono">{c.error}</pre>
|
|
654
|
+
)}
|
|
655
|
+
</li>
|
|
656
|
+
);
|
|
657
|
+
})}
|
|
658
|
+
</ol>
|
|
659
|
+
)}
|
|
660
|
+
</aside>
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
392
664
|
// ─── Tmux attach modal ─────────────────────────────────────────────────────
|
|
393
665
|
|
|
394
666
|
function openTmuxModal(
|
|
@@ -403,6 +675,103 @@ function openTmuxModal(
|
|
|
403
675
|
});
|
|
404
676
|
}
|
|
405
677
|
|
|
678
|
+
// ─── Steer modal ──────────────────────────────────────────────────────────
|
|
679
|
+
|
|
680
|
+
function openSteerModal(
|
|
681
|
+
modal: ReturnType<typeof useModal>,
|
|
682
|
+
toast: ReturnType<typeof useToast>,
|
|
683
|
+
inst: BgInstance,
|
|
684
|
+
refresh: () => void,
|
|
685
|
+
) {
|
|
686
|
+
modal.open({
|
|
687
|
+
title: `Steer ${inst.agent || inst.instanceId}`,
|
|
688
|
+
width: 600,
|
|
689
|
+
children: (
|
|
690
|
+
<SteerModalBody
|
|
691
|
+
instanceId={inst.instanceId}
|
|
692
|
+
toast={toast}
|
|
693
|
+
onCreated={refresh}
|
|
694
|
+
onClose={() => modal.close()}
|
|
695
|
+
/>
|
|
696
|
+
),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function SteerModalBody({
|
|
701
|
+
instanceId,
|
|
702
|
+
toast,
|
|
703
|
+
onCreated,
|
|
704
|
+
onClose,
|
|
705
|
+
}: {
|
|
706
|
+
instanceId: string;
|
|
707
|
+
toast: ReturnType<typeof useToast>;
|
|
708
|
+
onCreated: () => void;
|
|
709
|
+
onClose: () => void;
|
|
710
|
+
}) {
|
|
711
|
+
const [message, setMessage] = useState('');
|
|
712
|
+
const [submitting, setSubmitting] = useState(false);
|
|
713
|
+
const [error, setError] = useState<string | null>(null);
|
|
714
|
+
|
|
715
|
+
const onSubmit = async () => {
|
|
716
|
+
if (!message.trim()) {
|
|
717
|
+
setError('Message is required.');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
setSubmitting(true);
|
|
721
|
+
try {
|
|
722
|
+
const r = await api.post<{
|
|
723
|
+
ok: boolean;
|
|
724
|
+
newInstanceId?: string;
|
|
725
|
+
error?: string;
|
|
726
|
+
}>(`/background/${encodeURIComponent(instanceId)}/steer`, { message });
|
|
727
|
+
if (!r?.ok) {
|
|
728
|
+
setError(r?.error || 'Steer failed.');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
toast.success(
|
|
732
|
+
`Steered → new instance ${(r.newInstanceId ?? '').slice(0, 14)}…`,
|
|
733
|
+
);
|
|
734
|
+
onCreated();
|
|
735
|
+
onClose();
|
|
736
|
+
} catch (err) {
|
|
737
|
+
setError((err as Error).message);
|
|
738
|
+
} finally {
|
|
739
|
+
setSubmitting(false);
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
return (
|
|
744
|
+
<div className="bg-steer-modal">
|
|
745
|
+
<p className="muted">
|
|
746
|
+
Steer kills the current subprocess and spawns a new one with the
|
|
747
|
+
original prompt + your message appended. The new instance is
|
|
748
|
+
linked to this one as <code>parentInstanceId</code>.
|
|
749
|
+
</p>
|
|
750
|
+
<textarea
|
|
751
|
+
className="field-input bg-steer-textarea"
|
|
752
|
+
rows={6}
|
|
753
|
+
value={message}
|
|
754
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
755
|
+
placeholder="What do you want the agent to do differently?"
|
|
756
|
+
/>
|
|
757
|
+
{error && <div className="bg-steer-error">{error}</div>}
|
|
758
|
+
<div className="bg-steer-actions">
|
|
759
|
+
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
760
|
+
Cancel
|
|
761
|
+
</Button>
|
|
762
|
+
<Button
|
|
763
|
+
variant="primary"
|
|
764
|
+
size="sm"
|
|
765
|
+
onClick={onSubmit}
|
|
766
|
+
disabled={submitting || !message.trim()}
|
|
767
|
+
>
|
|
768
|
+
{submitting ? 'Steering…' : 'Steer'}
|
|
769
|
+
</Button>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
406
775
|
function TmuxModalBody({ info, cmd }: { info: BackgroundTmuxResponse; cmd: string }) {
|
|
407
776
|
const [copied, setCopied] = useState(false);
|
|
408
777
|
const onCopy = async () => {
|
|
@@ -286,19 +286,19 @@ function SettingsViewInner({ settings: initial, refreshSnapshot, settingsMode, s
|
|
|
286
286
|
*/}
|
|
287
287
|
|
|
288
288
|
<div className={cn('settings-grid', activeSection && 'settings-grid-filtered')} data-active-section={activeSection || undefined}>
|
|
289
|
-
{(showAll || sectionOf('theme')) && <ThemeSection {...sp}
|
|
290
|
-
{(showAll || sectionOf('updates')) && <UpdatesSection
|
|
291
|
-
{(showAll || sectionOf('layout') || sectionOf('general')) && <GeneralSection {...sp} autoSave={autoSave}
|
|
292
|
-
{(showAll || sectionOf('env-vars')) && <EnvVarsSection
|
|
293
|
-
{(showAll || sectionOf('network') || sectionOf('service') || sectionOf('tailscale')) && <NetworkSection tailscale={tailscale} tailscaleDraft={tailscaleDraft} setTailscaleDraft={setTailscaleDraft} onTailscaleToggle={onTailscaleToggle}
|
|
289
|
+
{(showAll || sectionOf('theme')) && <div id="settings-theme"><ThemeSection {...sp} /></div>}
|
|
290
|
+
{(showAll || sectionOf('updates')) && <div id="settings-updates"><UpdatesSection /></div>}
|
|
291
|
+
{(showAll || sectionOf('layout') || sectionOf('general')) && <div id="settings-general"><GeneralSection {...sp} autoSave={autoSave} /></div>}
|
|
292
|
+
{(showAll || sectionOf('env-vars')) && <div id="settings-env-vars"><EnvVarsSection /></div>}
|
|
293
|
+
{(showAll || sectionOf('network') || sectionOf('service') || sectionOf('tailscale')) && <div id="settings-network"><NetworkSection tailscale={tailscale} tailscaleDraft={tailscaleDraft} setTailscaleDraft={setTailscaleDraft} onTailscaleToggle={onTailscaleToggle} /></div>}
|
|
294
294
|
{(showAll || sectionOf('network') || sectionOf('tailscale')) && <TailscaleSettings initialStatus={tailscale} />}
|
|
295
|
-
{(showAll || sectionOf('notifications')) && <NotificationsSection {...sp}
|
|
296
|
-
{(showAll || sectionOf('auth')) && <AuthSection settings={settings} authStatus={authStatus} setAuthStatus={setAuthStatus}
|
|
297
|
-
{(showAll || sectionOf('agents') || sectionOf('dashboard') || sectionOf('background')) && <AgentSection {...sp} autoSave={autoSave}
|
|
298
|
-
{(showAll || sectionOf('system-llm')) && <SystemLlmSection {...sp}
|
|
299
|
-
{(showAll || sectionOf('headroom')) && <HeadroomSection settings={settings} setSettings={setSettings} setDirty={setDirty}
|
|
300
|
-
{(showAll || sectionOf('activity-log') || sectionOf('about')) && <ActivitySection about={about}
|
|
301
|
-
{(showAll || sectionOf('workspaces')) && <WorkspacesSection
|
|
295
|
+
{(showAll || sectionOf('notifications')) && <div id="settings-notifications"><NotificationsSection {...sp} /></div>}
|
|
296
|
+
{(showAll || sectionOf('auth')) && <div id="settings-auth"><AuthSection settings={settings} authStatus={authStatus} setAuthStatus={setAuthStatus} /></div>}
|
|
297
|
+
{(showAll || sectionOf('agents') || sectionOf('dashboard') || sectionOf('background')) && <div id="settings-agents"><AgentSection {...sp} autoSave={autoSave} /></div>}
|
|
298
|
+
{(showAll || sectionOf('system-llm')) && <div id="settings-system-llm"><SystemLlmSection {...sp} /></div>}
|
|
299
|
+
{(showAll || sectionOf('headroom')) && <div id="settings-headroom"><HeadroomSection settings={settings} setSettings={setSettings} setDirty={setDirty} /></div>}
|
|
300
|
+
{(showAll || sectionOf('activity-log') || sectionOf('about')) && <div id="settings-activity-log"><ActivitySection about={about} /></div>}
|
|
301
|
+
{(showAll || sectionOf('workspaces')) && <div id="settings-workspaces"><WorkspacesSection /></div>}
|
|
302
302
|
</div>
|
|
303
303
|
</div>
|
|
304
304
|
);
|