@saadnvd1/agent-os 0.3.520 → 0.3.523
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/README.md +11 -0
- package/app/api/welcome/route.ts +22 -0
- package/app/page.tsx +6 -0
- package/app/welcome/page.tsx +6 -0
- package/components/Chat/BackgroundTasks.tsx +76 -10
- package/components/Chat/ChatPanel.tsx +90 -34
- package/components/Chat/Subagent.tsx +25 -20
- package/components/Chat/Tools.tsx +70 -39
- package/components/Chat/background.tsx +114 -0
- package/components/Projects/AddProject/AddProjectDialog.tsx +2 -2
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Settings/SetupSection.tsx +37 -0
- package/components/Welcome/AgentList.tsx +73 -0
- package/components/Welcome/AgentRow.tsx +102 -0
- package/components/Welcome/ProjectStep.tsx +100 -0
- package/components/Welcome/SetupTerminal.tsx +64 -0
- package/components/Welcome/WelcomeDialog.tsx +153 -0
- package/data/agents/index.ts +16 -1
- package/data/chat/useChat.ts +40 -0
- package/data/welcome/index.ts +36 -0
- package/hooks/useWelcome.ts +70 -0
- package/lib/agents/probe.ts +27 -0
- package/lib/agents/setup-commands.ts +54 -0
- package/lib/chat/activity.ts +4 -2
- package/lib/chat/background.ts +77 -0
- package/lib/chat/driver.ts +6 -0
- package/lib/chat/drivers/claude-mapper.ts +44 -6
- package/lib/chat/drivers/claude.ts +4 -0
- package/lib/chat/events.ts +14 -0
- package/lib/chat/registry.ts +2 -0
- package/lib/chat/runner.ts +20 -0
- package/lib/chat/settings.ts +1 -0
- package/lib/chat/socket.ts +9 -0
- package/lib/chat/worker/client.ts +33 -0
- package/lib/chat/worker/host.ts +13 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/chat/worker/protocol.ts +2 -0
- package/lib/db/migrations.ts +14 -0
- package/lib/security/auth.ts +11 -1
- package/lib/settings-url.ts +1 -0
- package/lib/welcome-url.ts +33 -0
- package/lib/welcome.ts +88 -0
- package/package.json +1 -1
- package/stores/addProject.ts +19 -1
- package/stores/welcomeUi.ts +26 -0
package/README.md
CHANGED
|
@@ -213,6 +213,17 @@ its WSL note, and Windows reached it at both `localhost:3011` and
|
|
|
213
213
|
`wslview`/`explorer.exe`, mirrored networking, `agent-os enable` under
|
|
214
214
|
systemd, the idle-timeout settings above, and Debian or other distros.
|
|
215
215
|
|
|
216
|
+
### First run
|
|
217
|
+
|
|
218
|
+
On a fresh install (no sessions, no projects of your own) the web app opens a
|
|
219
|
+
short setup at `/?welcome`: it shows each agent CLI as ready, needing a
|
|
220
|
+
sign-in, or missing, installs or signs one in from an embedded terminal, adds
|
|
221
|
+
a first project (a folder, a clone or an empty one), then opens a session in
|
|
222
|
+
it. Every step after the first can be skipped. It is remembered on the server,
|
|
223
|
+
so a phone you pair later doesn't see it, and an install that already has work
|
|
224
|
+
never does. Reopen it from Settings → Setup (**Run setup again**) or at
|
|
225
|
+
`/welcome`; `AGENTOS_SKIP_WELCOME=1` turns it off.
|
|
226
|
+
|
|
216
227
|
### Supported agents
|
|
217
228
|
|
|
218
229
|
| Agent | Chat | Resume | Fork | Auto-approve |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { demoMode } from "@/lib/security/demo";
|
|
3
|
+
import { completeWelcome, readWelcomeGate } from "@/lib/welcome";
|
|
4
|
+
|
|
5
|
+
// Whether the first-run welcome should open on this server. An install
|
|
6
|
+
// already in use is marked done here, so emptying it later doesn't bring it back.
|
|
7
|
+
export async function GET() {
|
|
8
|
+
return NextResponse.json(readWelcomeGate(demoMode()));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// { action: "complete" }: setup finished or skipped.
|
|
12
|
+
export async function POST(request: NextRequest) {
|
|
13
|
+
try {
|
|
14
|
+
const body = await request.json();
|
|
15
|
+
if (body?.action !== "complete") throw new Error("Unknown action");
|
|
16
|
+
completeWelcome(new Date().toISOString(), undefined, true);
|
|
17
|
+
return NextResponse.json(readWelcomeGate(demoMode()));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
return NextResponse.json({ error: message }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
}
|
package/app/page.tsx
CHANGED
|
@@ -78,6 +78,11 @@ const SettingsDialog = dynamic(
|
|
|
78
78
|
),
|
|
79
79
|
{ ssr: false }
|
|
80
80
|
);
|
|
81
|
+
const WelcomeDialog = dynamic(
|
|
82
|
+
() =>
|
|
83
|
+
import("@/components/Welcome/WelcomeDialog").then((m) => m.WelcomeDialog),
|
|
84
|
+
{ ssr: false }
|
|
85
|
+
);
|
|
81
86
|
const MessagesDialog = dynamic(
|
|
82
87
|
() => import("@/components/Bus/MessagesDialog").then((m) => m.MessagesDialog),
|
|
83
88
|
{ ssr: false }
|
|
@@ -122,6 +127,7 @@ const AppDialogs = memo(function AppDialogs() {
|
|
|
122
127
|
return (
|
|
123
128
|
<>
|
|
124
129
|
<TasksDialog />
|
|
130
|
+
<WelcomeDialog />
|
|
125
131
|
<AddProjectDialog />
|
|
126
132
|
<MessagesDialog />
|
|
127
133
|
<ArchivedDialog />
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
X,
|
|
14
14
|
} from "lucide-react";
|
|
15
15
|
import type { ChatItem } from "@/lib/chat/events";
|
|
16
|
+
import { outputTail } from "@/lib/chat/background";
|
|
16
17
|
import { formatElapsed } from "@/lib/chat/elapsed";
|
|
17
18
|
import { cn } from "@/lib/utils";
|
|
18
19
|
import { useNow } from "./useNow";
|
|
@@ -22,6 +23,8 @@ type Task = Extract<ChatItem, { kind: "task" }>;
|
|
|
22
23
|
// Finished tasks stay listed this long, then the chip clears.
|
|
23
24
|
const RECENT_MS = 5 * 60 * 1000;
|
|
24
25
|
const POLL_MS = 2000;
|
|
26
|
+
// Running tasks whose last line of output is followed while the tray is open.
|
|
27
|
+
const TAILED = 5;
|
|
25
28
|
|
|
26
29
|
function TaskIcon({ task }: { task: Task }) {
|
|
27
30
|
if (task.status === "running")
|
|
@@ -49,16 +52,23 @@ function TaskRow({
|
|
|
49
52
|
task,
|
|
50
53
|
now,
|
|
51
54
|
output,
|
|
55
|
+
tail,
|
|
56
|
+
highlight,
|
|
52
57
|
onStop,
|
|
53
58
|
onLoadOutput,
|
|
54
59
|
}: {
|
|
55
60
|
task: Task;
|
|
56
61
|
now: number;
|
|
57
62
|
output: string | null | undefined;
|
|
63
|
+
// Follows its output for the line under its name.
|
|
64
|
+
tail: boolean;
|
|
65
|
+
// Brought here from the call it came from.
|
|
66
|
+
highlight: boolean;
|
|
58
67
|
onStop: () => void;
|
|
59
68
|
onLoadOutput: (taskId: string) => void;
|
|
60
69
|
}) {
|
|
61
|
-
const [open, setOpen] = useState(
|
|
70
|
+
const [open, setOpen] = useState(highlight);
|
|
71
|
+
const rowRef = useRef<HTMLDivElement>(null);
|
|
62
72
|
const running = task.status === "running";
|
|
63
73
|
const { taskId } = task;
|
|
64
74
|
const outputRef = useRef<HTMLPreElement>(null);
|
|
@@ -69,18 +79,32 @@ function TaskRow({
|
|
|
69
79
|
if (el) el.scrollTop = el.scrollHeight;
|
|
70
80
|
}, [output]);
|
|
71
81
|
|
|
72
|
-
// While open, keep the output fresh for as long as the task runs.
|
|
73
82
|
useEffect(() => {
|
|
74
|
-
if (!
|
|
83
|
+
if (!highlight) return;
|
|
84
|
+
setOpen(true);
|
|
85
|
+
rowRef.current?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
86
|
+
}, [highlight]);
|
|
87
|
+
|
|
88
|
+
// While open (or tailed), keep the output fresh for as long as it runs.
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!open && !(tail && running)) return;
|
|
75
91
|
onLoadOutput(taskId);
|
|
76
92
|
if (!running) return;
|
|
77
93
|
const t = setInterval(() => onLoadOutput(taskId), POLL_MS);
|
|
78
94
|
return () => clearInterval(t);
|
|
79
|
-
}, [open, running, taskId, onLoadOutput]);
|
|
95
|
+
}, [open, tail, running, taskId, onLoadOutput]);
|
|
80
96
|
|
|
81
97
|
const took = formatElapsed((task.endedAt ?? now) - task.createdAt);
|
|
98
|
+
const line =
|
|
99
|
+
running && !open ? (outputTail(output) ?? task.lastToolName) : null;
|
|
82
100
|
return (
|
|
83
|
-
<div
|
|
101
|
+
<div
|
|
102
|
+
ref={rowRef}
|
|
103
|
+
className={cn(
|
|
104
|
+
"rounded-lg transition-colors duration-700",
|
|
105
|
+
highlight && "bg-foreground/[0.05]"
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
84
108
|
<div className="flex min-h-11 items-center gap-2 px-1.5 md:min-h-9">
|
|
85
109
|
<button
|
|
86
110
|
type="button"
|
|
@@ -113,6 +137,11 @@ function TaskRow({
|
|
|
113
137
|
</button>
|
|
114
138
|
)}
|
|
115
139
|
</div>
|
|
140
|
+
{line && (
|
|
141
|
+
<p className="text-muted-foreground truncate pr-2 pb-1.5 pl-9 font-mono text-[11px]">
|
|
142
|
+
{line}
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
116
145
|
{open && (
|
|
117
146
|
<div className="space-y-1 pb-2 pl-7">
|
|
118
147
|
{task.summary && task.summary !== task.description && (
|
|
@@ -143,16 +172,24 @@ function TaskRow({
|
|
|
143
172
|
export function BackgroundTasks({
|
|
144
173
|
items,
|
|
145
174
|
outputs,
|
|
175
|
+
reveal,
|
|
146
176
|
onStop,
|
|
147
177
|
onLoadOutput,
|
|
148
178
|
}: {
|
|
149
179
|
items: ChatItem[];
|
|
180
|
+
// A task to open the tray on, from the call that became it.
|
|
181
|
+
reveal?: { taskId: string; at: number } | null;
|
|
150
182
|
outputs: Record<string, string | null>;
|
|
151
183
|
onStop: (taskId: string) => void;
|
|
152
184
|
onLoadOutput: (taskId: string) => void;
|
|
153
185
|
}) {
|
|
154
186
|
const [open, setOpen] = useState(false);
|
|
155
|
-
const
|
|
187
|
+
const [pulse, setPulse] = useState(false);
|
|
188
|
+
const [lit, setLit] = useState<string | null>(null);
|
|
189
|
+
// One still blocking its call isn't in the background yet.
|
|
190
|
+
const tasks = items.filter(
|
|
191
|
+
(i): i is Task => i.kind === "task" && i.backgrounded !== false
|
|
192
|
+
);
|
|
156
193
|
const running = tasks.filter((t) => t.status === "running" && !t.ambient);
|
|
157
194
|
const now = useNow(running.length > 0);
|
|
158
195
|
const shown = tasks
|
|
@@ -162,6 +199,29 @@ export function BackgroundTasks({
|
|
|
162
199
|
Number(b.status === "running") - Number(a.status === "running") ||
|
|
163
200
|
b.createdAt - a.createdAt
|
|
164
201
|
);
|
|
202
|
+
const failed = shown.filter((t) => t.status === "failed").length;
|
|
203
|
+
|
|
204
|
+
// A task arriving gives the chip one soft pulse.
|
|
205
|
+
const count = running.length;
|
|
206
|
+
const seen = useRef(count);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const more = count > seen.current;
|
|
209
|
+
seen.current = count;
|
|
210
|
+
if (!more) return;
|
|
211
|
+
setPulse(true);
|
|
212
|
+
const t = setTimeout(() => setPulse(false), 700);
|
|
213
|
+
return () => clearTimeout(t);
|
|
214
|
+
}, [count]);
|
|
215
|
+
|
|
216
|
+
// Asked to show a task: open the tray on it for a moment.
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!reveal) return;
|
|
219
|
+
setOpen(true);
|
|
220
|
+
setLit(reveal.taskId);
|
|
221
|
+
const t = setTimeout(() => setLit(null), 1200);
|
|
222
|
+
return () => clearTimeout(t);
|
|
223
|
+
}, [reveal]);
|
|
224
|
+
|
|
165
225
|
if (!shown.length) return null;
|
|
166
226
|
|
|
167
227
|
return (
|
|
@@ -170,24 +230,30 @@ export function BackgroundTasks({
|
|
|
170
230
|
type="button"
|
|
171
231
|
onClick={() => setOpen(!open)}
|
|
172
232
|
className={cn(
|
|
173
|
-
"text-muted-foreground hover:text-foreground hover:bg-foreground/[0.05] flex h-9 items-center gap-1.5 rounded-lg px-2 text-xs md:h-7",
|
|
174
|
-
running.length && "text-primary"
|
|
233
|
+
"text-muted-foreground hover:text-foreground hover:bg-foreground/[0.05] flex h-9 items-center gap-1.5 rounded-lg px-2 text-xs transition-colors duration-500 md:h-7",
|
|
234
|
+
running.length && "text-primary",
|
|
235
|
+
!running.length && failed > 0 && "text-destructive",
|
|
236
|
+
pulse && "bg-foreground/[0.08]"
|
|
175
237
|
)}
|
|
176
238
|
>
|
|
177
239
|
<Layers className="h-3.5 w-3.5" />
|
|
178
240
|
{running.length
|
|
179
241
|
? `${running.length} running in background`
|
|
180
|
-
:
|
|
242
|
+
: failed
|
|
243
|
+
? `${failed} failed in background`
|
|
244
|
+
: `${shown.length} finished in background`}
|
|
181
245
|
<ChevronRight
|
|
182
246
|
className={cn("h-3 w-3 transition-transform", open && "rotate-90")}
|
|
183
247
|
/>
|
|
184
248
|
</button>
|
|
185
249
|
{open && (
|
|
186
250
|
<div className="bg-foreground/[0.025] mt-1 max-h-80 overflow-y-auto rounded-xl px-1 py-1">
|
|
187
|
-
{shown.map((t) => (
|
|
251
|
+
{shown.map((t, i) => (
|
|
188
252
|
<TaskRow
|
|
189
253
|
key={t.id}
|
|
190
254
|
task={t}
|
|
255
|
+
tail={i < TAILED}
|
|
256
|
+
highlight={lit === t.taskId}
|
|
191
257
|
now={now}
|
|
192
258
|
output={outputs[t.taskId]}
|
|
193
259
|
onStop={() => onStop(t.taskId)}
|
|
@@ -42,6 +42,8 @@ import { useFreshStartQuery } from "@/data/fresh-start";
|
|
|
42
42
|
import { QuoteButton } from "./QuoteButton";
|
|
43
43
|
import { ToolBodies, ToolGroup } from "./Tools";
|
|
44
44
|
import { RowState, RowStore } from "./rowState";
|
|
45
|
+
import { Background, type BackgroundValue } from "./background";
|
|
46
|
+
import { backgroundKey } from "@/lib/chat/background";
|
|
45
47
|
import { FindBar } from "./FindBar";
|
|
46
48
|
import { FIND_EVENT, isFindKey, useFind } from "./useFind";
|
|
47
49
|
import { McpServers } from "./McpServers";
|
|
@@ -253,6 +255,9 @@ export function ChatPanel({
|
|
|
253
255
|
undoPreview,
|
|
254
256
|
clearUndo,
|
|
255
257
|
stopTask,
|
|
258
|
+
canBackground,
|
|
259
|
+
backgroundTask,
|
|
260
|
+
movingToBackground,
|
|
256
261
|
loadTaskOutput,
|
|
257
262
|
taskOutputs,
|
|
258
263
|
queue,
|
|
@@ -380,6 +385,54 @@ export function ChatPanel({
|
|
|
380
385
|
}),
|
|
381
386
|
[toolBodies, requestToolBody, pinToolBody, connected]
|
|
382
387
|
);
|
|
388
|
+
// The calls that block the turn and can be moved to the background.
|
|
389
|
+
const [reveal, setReveal] = useState<{ taskId: string; at: number } | null>(
|
|
390
|
+
null
|
|
391
|
+
);
|
|
392
|
+
const foregroundTasks = useMemo(() => {
|
|
393
|
+
const byTool = new Map<string, Extract<ChatItem, { kind: "task" }>>();
|
|
394
|
+
const byId = new Map<string, Extract<ChatItem, { kind: "task" }>>();
|
|
395
|
+
for (const i of tasks)
|
|
396
|
+
if (i.kind === "task") {
|
|
397
|
+
byId.set(i.taskId, i);
|
|
398
|
+
if (i.toolUseId) byTool.set(i.toolUseId, i);
|
|
399
|
+
}
|
|
400
|
+
return { byTool, byId };
|
|
401
|
+
}, [tasks]);
|
|
402
|
+
const background = useMemo<BackgroundValue>(
|
|
403
|
+
() => ({
|
|
404
|
+
supported: canBackground,
|
|
405
|
+
taskOfTool: (id) => foregroundTasks.byTool.get(id),
|
|
406
|
+
taskById: (id) => foregroundTasks.byId.get(id),
|
|
407
|
+
moving: movingToBackground,
|
|
408
|
+
send: backgroundTask,
|
|
409
|
+
reveal: (taskId) => setReveal({ taskId, at: Date.now() }),
|
|
410
|
+
}),
|
|
411
|
+
[canBackground, foregroundTasks, movingToBackground, backgroundTask]
|
|
412
|
+
);
|
|
413
|
+
// Ctrl+B moves every call that blocks the turn, from anywhere in this panel.
|
|
414
|
+
const canMove = useMemo(
|
|
415
|
+
() =>
|
|
416
|
+
canBackground &&
|
|
417
|
+
[...foregroundTasks.byId.values()].some(
|
|
418
|
+
(t) => t.status === "running" && t.backgrounded === false
|
|
419
|
+
),
|
|
420
|
+
[canBackground, foregroundTasks]
|
|
421
|
+
);
|
|
422
|
+
const moveRef = useRef({ canMove, backgroundTask });
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
moveRef.current = { canMove, backgroundTask };
|
|
425
|
+
});
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
const onKey = (e: KeyboardEvent) => {
|
|
428
|
+
if (!rootRef.current?.contains(e.target as Node)) return;
|
|
429
|
+
if (!backgroundKey(e, moveRef.current.canMove)) return;
|
|
430
|
+
e.preventDefault();
|
|
431
|
+
moveRef.current.backgroundTask();
|
|
432
|
+
};
|
|
433
|
+
window.addEventListener("keydown", onKey, true);
|
|
434
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
435
|
+
}, []);
|
|
383
436
|
const itemsById = useMemo(
|
|
384
437
|
() => new Map(items.map((i) => [i.id, i])),
|
|
385
438
|
[items]
|
|
@@ -482,41 +535,43 @@ export function ChatPanel({
|
|
|
482
535
|
{orchestratorOf && <OrchestratorBar workspaceId={orchestratorOf} />}
|
|
483
536
|
<div ref={scrollRef} className="relative min-h-0 flex-1">
|
|
484
537
|
<RowState.Provider value={rowState}>
|
|
485
|
-
<
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
538
|
+
<Background.Provider value={background}>
|
|
539
|
+
<ToolBodies.Provider value={bodies}>
|
|
540
|
+
<div
|
|
541
|
+
// A new conversation starts over, scrolled to the end.
|
|
542
|
+
key={sessionId}
|
|
543
|
+
ref={listRef}
|
|
544
|
+
onScroll={onListScroll}
|
|
545
|
+
onCopy={onCopy}
|
|
546
|
+
className="h-full overflow-y-auto overscroll-y-contain"
|
|
547
|
+
>
|
|
548
|
+
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-4 pt-5 pb-4">
|
|
549
|
+
{hasMore && (
|
|
550
|
+
<div className="text-muted-foreground flex min-h-11 items-center justify-center text-xs">
|
|
551
|
+
{loadingOlder ? (
|
|
552
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
553
|
+
) : (
|
|
554
|
+
<button
|
|
555
|
+
type="button"
|
|
556
|
+
onClick={loadOlder}
|
|
557
|
+
className="hover:text-foreground min-h-11 px-3"
|
|
558
|
+
>
|
|
559
|
+
Show earlier messages
|
|
560
|
+
</button>
|
|
561
|
+
)}
|
|
562
|
+
</div>
|
|
563
|
+
)}
|
|
564
|
+
<SetupCard sessionId={sessionId} />
|
|
565
|
+
{blocks.length === 0 && !queue.length && (
|
|
566
|
+
<p className="text-muted-foreground py-16 text-center text-sm">
|
|
567
|
+
Ask anything to start.
|
|
568
|
+
</p>
|
|
569
|
+
)}
|
|
570
|
+
</div>
|
|
571
|
+
<TimelineRows blocks={blocks} actions={actions} />
|
|
516
572
|
</div>
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
</ToolBodies.Provider>
|
|
573
|
+
</ToolBodies.Provider>
|
|
574
|
+
</Background.Provider>
|
|
520
575
|
</RowState.Provider>
|
|
521
576
|
{find.open && (
|
|
522
577
|
<FindBar
|
|
@@ -551,6 +606,7 @@ export function ChatPanel({
|
|
|
551
606
|
/>
|
|
552
607
|
<BackgroundTasks
|
|
553
608
|
items={tasks}
|
|
609
|
+
reveal={reveal}
|
|
554
610
|
outputs={taskOutputs}
|
|
555
611
|
onStop={stopTask}
|
|
556
612
|
onLoadOutput={loadTaskOutput}
|
|
@@ -4,7 +4,8 @@ import { Bot, ChevronRight } from "lucide-react";
|
|
|
4
4
|
import type { ToolItem } from "@/lib/chat/group";
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
6
|
import { Markdown } from "./Markdown";
|
|
7
|
-
import {
|
|
7
|
+
import { ToolIcon, useToolBody } from "./Tools";
|
|
8
|
+
import { BackgroundButton, BackgroundState } from "./background";
|
|
8
9
|
import { useRowState } from "./rowState";
|
|
9
10
|
|
|
10
11
|
// Its report, loaded when the card opens if the page left it out.
|
|
@@ -23,26 +24,30 @@ export function SubagentCard({ item }: { item: ToolItem }) {
|
|
|
23
24
|
};
|
|
24
25
|
return (
|
|
25
26
|
<div className="bg-foreground/[0.025] rounded-xl px-2 py-1">
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
<div className="flex items-center gap-1">
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
onClick={() => setOpen(!open)}
|
|
31
|
+
className="flex min-h-10 min-w-0 flex-1 items-center gap-2 px-1.5 text-left text-xs md:min-h-8"
|
|
32
|
+
>
|
|
33
|
+
<ChevronRight
|
|
34
|
+
className={cn(
|
|
35
|
+
"text-muted-foreground h-3 w-3 shrink-0 transition-transform",
|
|
36
|
+
open && "rotate-90"
|
|
37
|
+
)}
|
|
38
|
+
/>
|
|
39
|
+
<Bot className="text-primary h-3.5 w-3.5 shrink-0" />
|
|
40
|
+
<span className="min-w-0 flex-1 truncate">{item.title}</span>
|
|
41
|
+
{input.subagent_type && (
|
|
42
|
+
<span className="text-muted-foreground hidden font-mono sm:inline">
|
|
43
|
+
{input.subagent_type}
|
|
44
|
+
</span>
|
|
35
45
|
)}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
{input.subagent_type}
|
|
42
|
-
</span>
|
|
43
|
-
)}
|
|
44
|
-
<StatusIcon status={item.status} />
|
|
45
|
-
</button>
|
|
46
|
+
{item.backgroundTaskId ? <BackgroundState tool={item} /> : null}
|
|
47
|
+
<ToolIcon tool={item} />
|
|
48
|
+
</button>
|
|
49
|
+
<BackgroundButton tool={item} />
|
|
50
|
+
</div>
|
|
46
51
|
{open && (
|
|
47
52
|
<div className="space-y-3 px-1.5 pt-1 pb-2 pl-7">
|
|
48
53
|
{input.prompt && (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, useEffect } from "react";
|
|
4
|
-
import { Check, ChevronRight, Loader2, Square, X } from "lucide-react";
|
|
4
|
+
import { Check, ChevronRight, Layers, Loader2, Square, X } from "lucide-react";
|
|
5
5
|
import type { FileDiff, ToolBody } from "@/lib/chat/events";
|
|
6
6
|
import type { ToolItem } from "@/lib/chat/group";
|
|
7
7
|
import { lineDiff, shortPath, type DiffLine } from "@/lib/chat/diff";
|
|
@@ -9,6 +9,7 @@ import { needsBody } from "@/lib/chat/bodyCache";
|
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
10
|
import { formatElapsed } from "@/lib/chat/elapsed";
|
|
11
11
|
import { Highlighted } from "./Code";
|
|
12
|
+
import { BackgroundButton, BackgroundState, useBackground } from "./background";
|
|
12
13
|
import { useNow } from "./useNow";
|
|
13
14
|
import { useRowState } from "./rowState";
|
|
14
15
|
|
|
@@ -48,6 +49,18 @@ export function StatusIcon({ status }: { status: ToolItem["status"] }) {
|
|
|
48
49
|
return <Check className="text-muted-foreground/70 h-3.5 w-3.5 shrink-0" />;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// The status of a call; one moved to the background and still running there
|
|
53
|
+
// shows as handed off instead of done.
|
|
54
|
+
export function ToolIcon({ tool }: { tool: ToolItem }) {
|
|
55
|
+
const bg = useBackground();
|
|
56
|
+
const task = tool.backgroundTaskId
|
|
57
|
+
? bg.taskById(tool.backgroundTaskId)
|
|
58
|
+
: undefined;
|
|
59
|
+
if (tool.backgroundTaskId && (!task || task.status === "running"))
|
|
60
|
+
return <Layers className="text-muted-foreground h-3.5 w-3.5 shrink-0" />;
|
|
61
|
+
return <StatusIcon status={tool.status} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
const TONE: Record<DiffLine["op"], string> = {
|
|
52
65
|
" ": "text-muted-foreground",
|
|
53
66
|
"-": "bg-red-500/10 text-red-700 dark:text-red-300",
|
|
@@ -139,17 +152,24 @@ function ToolRow({ tool }: { tool: ToolItem }) {
|
|
|
139
152
|
);
|
|
140
153
|
return (
|
|
141
154
|
<div data-item={tool.id}>
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
<div className="hover:bg-foreground/[0.04] flex items-center gap-1 rounded-lg pr-1">
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={() => setOpen(!open)}
|
|
159
|
+
className="flex min-h-9 min-w-0 flex-1 items-center gap-2 px-1.5 text-left md:min-h-7"
|
|
160
|
+
>
|
|
161
|
+
<ToolIcon tool={tool} />
|
|
162
|
+
<span className="min-w-0 flex-1 truncate font-mono text-xs">
|
|
163
|
+
{tool.title}
|
|
164
|
+
</span>
|
|
165
|
+
{tool.backgroundTaskId ? (
|
|
166
|
+
<BackgroundState tool={tool} />
|
|
167
|
+
) : (
|
|
168
|
+
<Elapsed tool={tool} />
|
|
169
|
+
)}
|
|
170
|
+
</button>
|
|
171
|
+
<BackgroundButton tool={tool} />
|
|
172
|
+
</div>
|
|
153
173
|
{open && <ToolDetail tool={tool} />}
|
|
154
174
|
</div>
|
|
155
175
|
);
|
|
@@ -161,39 +181,50 @@ export function ToolGroup({ tools }: { tools: ToolItem[] }) {
|
|
|
161
181
|
const failed = tools.filter((t) => t.status === "error").length;
|
|
162
182
|
const stopped = tools.filter((t) => t.status === "stopped").length;
|
|
163
183
|
const edits = tools.filter((t) => t.diff || t.hasDiff).length;
|
|
184
|
+
const moved = tools.filter((t) => t.backgroundTaskId).length;
|
|
164
185
|
const [open, setOpen] = useRowState(`steps:${tools[0].id}`, false);
|
|
165
186
|
const latest =
|
|
166
187
|
tools.findLast((t) => t.status === "running") ?? tools[tools.length - 1];
|
|
167
188
|
return (
|
|
168
189
|
<div className="bg-foreground/[0.025] rounded-xl px-2 py-1">
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
<div className="flex items-center gap-1">
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => setOpen(!open)}
|
|
194
|
+
className="text-muted-foreground hover:text-foreground flex min-h-10 min-w-0 flex-1 items-center gap-2 px-1.5 text-left text-xs md:min-h-8"
|
|
195
|
+
>
|
|
196
|
+
<ChevronRight
|
|
197
|
+
className={cn(
|
|
198
|
+
"h-3 w-3 shrink-0 transition-transform",
|
|
199
|
+
open && "rotate-90"
|
|
200
|
+
)}
|
|
201
|
+
/>
|
|
202
|
+
{running ? (
|
|
203
|
+
<Loader2 className="text-primary h-3.5 w-3.5 shrink-0 animate-spin" />
|
|
204
|
+
) : null}
|
|
205
|
+
<span className="min-w-0 flex-1 truncate">
|
|
206
|
+
{running
|
|
207
|
+
? latest.title
|
|
208
|
+
: `${tools.length} step${tools.length === 1 ? "" : "s"}`}
|
|
209
|
+
{!running &&
|
|
210
|
+
edits > 0 &&
|
|
211
|
+
` · ${edits} edit${edits === 1 ? "" : "s"}`}
|
|
212
|
+
</span>
|
|
213
|
+
{running && <Elapsed tool={latest} />}
|
|
214
|
+
{failed > 0 && (
|
|
215
|
+
<span className="text-destructive">{failed} failed</span>
|
|
178
216
|
)}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
{failed > 0 && (
|
|
191
|
-
<span className="text-destructive">{failed} failed</span>
|
|
192
|
-
)}
|
|
193
|
-
{stopped > 0 && failed === 0 && (
|
|
194
|
-
<span className="text-muted-foreground/70">stopped</span>
|
|
195
|
-
)}
|
|
196
|
-
</button>
|
|
217
|
+
{stopped > 0 && failed === 0 && (
|
|
218
|
+
<span className="text-muted-foreground/70">stopped</span>
|
|
219
|
+
)}
|
|
220
|
+
{!running && moved > 0 && (
|
|
221
|
+
<span className="text-muted-foreground/70">
|
|
222
|
+
{moved} in background
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
225
|
+
</button>
|
|
226
|
+
{running && <BackgroundButton tool={latest} />}
|
|
227
|
+
</div>
|
|
197
228
|
{open && (
|
|
198
229
|
<div className="pb-1">
|
|
199
230
|
{tools.map((t) => (
|