@saadnvd1/agent-os 0.3.521 → 0.3.524

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 (39) hide show
  1. package/README.md +57 -0
  2. package/app/api/sessions/[id]/simulator/route.ts +32 -0
  3. package/app/api/sessions/[id]/simulator/run/route.ts +37 -0
  4. package/components/Chat/BackgroundTasks.tsx +76 -10
  5. package/components/Chat/ChatPanel.tsx +90 -34
  6. package/components/Chat/Subagent.tsx +25 -20
  7. package/components/Chat/Tools.tsx +70 -39
  8. package/components/Chat/background.tsx +114 -0
  9. package/data/chat/useChat.ts +40 -0
  10. package/lib/chat/activity.ts +4 -2
  11. package/lib/chat/background.ts +77 -0
  12. package/lib/chat/driver.ts +6 -0
  13. package/lib/chat/drivers/claude-mapper.ts +44 -6
  14. package/lib/chat/drivers/claude.ts +4 -0
  15. package/lib/chat/events.ts +14 -0
  16. package/lib/chat/registry.ts +2 -0
  17. package/lib/chat/runner.ts +20 -0
  18. package/lib/chat/settings.ts +1 -0
  19. package/lib/chat/socket.ts +9 -0
  20. package/lib/chat/worker/client.ts +33 -0
  21. package/lib/chat/worker/host.ts +13 -0
  22. package/lib/chat/worker/main.ts +1 -1
  23. package/lib/chat/worker/protocol.ts +2 -0
  24. package/lib/db/migrations.ts +19 -0
  25. package/lib/project-config/agentos.schema.json +19 -0
  26. package/lib/project-config/brief.ts +3 -0
  27. package/lib/project-config/schema.ts +10 -0
  28. package/lib/security/auth.ts +11 -1
  29. package/lib/sessions/end.ts +3 -0
  30. package/lib/sessions/stop.ts +3 -0
  31. package/lib/simulator/agent-device.ts +258 -0
  32. package/lib/simulator/available.ts +5 -0
  33. package/lib/simulator/brief.ts +18 -0
  34. package/lib/simulator/http.ts +16 -0
  35. package/lib/simulator/index.ts +458 -0
  36. package/lib/simulator/lease.ts +169 -0
  37. package/lib/simulator/simctl.ts +167 -0
  38. package/package.json +1 -1
  39. package/server.ts +16 -0
package/README.md CHANGED
@@ -801,6 +801,63 @@ schema, including the fields later releases implement (MCP servers, apps,
801
801
  recipes), is in
802
802
  [docs/decisions/2026-10-10-agentos-json.md](docs/decisions/2026-10-10-agentos-json.md).
803
803
 
804
+ ### iOS simulator for tasks
805
+
806
+ On a Mac, a task on an iOS app can boot a simulator of its own, put a debug
807
+ build of its worktree on it, tap through screens, read the accessibility tree
808
+ and take screenshots, to check a UI fix before anyone installs a release
809
+ build. A project opts in with a `simulator` block in `agentos.json`:
810
+
811
+ ```json
812
+ {
813
+ "ports": { "METRO_PORT": 8100 },
814
+ "simulator": {
815
+ "start": "npm run sim",
816
+ "app": "com.example.app",
817
+ "device": "iPhone 17"
818
+ }
819
+ }
820
+ ```
821
+
822
+ `start` runs in the task's worktree with `SIMULATOR_UDID` (and the task's
823
+ ports) in its environment. It builds if needed, installs the app on that
824
+ device and exits 0 once the app is up; AgentOS builds nothing itself, because
825
+ the project knows its native fingerprint and what to cache. `app` is the
826
+ bundle id, and `device` the model (default `iPhone 17`).
827
+
828
+ A project without the block gets nothing: no brief lines and no tools. A task
829
+ declares nothing and chooses nothing; it runs:
830
+
831
+ ```bash
832
+ aos sim open [--no-wait] # make its simulator, run start; waits until it is up
833
+ aos sim status | close
834
+ aos sim snapshot -i # accessibility tree with @e1, @e2... refs
835
+ aos sim click @e3 | fill @e5 "text" | scroll | swipe | back | home | find | wait
836
+ aos sim screenshot [path] # prints a PNG path under ~/.agent-os/simulators/<task>/shots
837
+ aos sim logs --last 2m # the app's recent log
838
+ ```
839
+
840
+ - **Driving** is [agent-device](https://github.com/callstack/agent-device) (MIT),
841
+ pinned and installed on first use into `~/.agent-os/tools`, with its own
842
+ state dir. Only the commands above are accepted; the device and session are
843
+ filled in by the server, so a task can only reach its own simulator.
844
+ - **One simulator per task**, cloned from a base device AgentOS makes once on
845
+ the newest iOS runtime. At most two are booted at once across all tasks
846
+ (`AGENTOS_SIMULATOR_MAX`); one idle for 15 minutes is shut down and boots
847
+ again on its next command. The XCTest runner builds the first time per Xcode
848
+ version (minutes), then is cached.
849
+ - **Cleanup**: a task's simulator is deleted when it is done, signed off,
850
+ dropped, deleted or moved away, and shut down when stopped. A sweep at start
851
+ and every 5 minutes also removes those of tasks that ended or crashed.
852
+ Devices are named with a per-database prefix and only those are ever
853
+ touched, so a dev instance and the real one never reap each other's.
854
+ - **Mac only.** On any other machine, including a linked one a task was moved
855
+ to, the brief says the simulator is unavailable and `aos sim` says the same;
856
+ nothing is forwarded between machines.
857
+ - **Optional.** No merge, review or release check reads simulator state.
858
+ Screenshots are written outside the worktree, and the brief tells the task
859
+ not to put them in commits or PR bodies unless they belong there.
860
+
804
861
  ### Stacks
805
862
 
806
863
  A project whose board is linked to LumifyHub can run the whole board as
@@ -0,0 +1,32 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ openSimulator,
4
+ releaseSimulator,
5
+ simulatorStatus,
6
+ } from "@/lib/simulator";
7
+ import { simulatorFailure } from "@/lib/simulator/http";
8
+
9
+ type Ctx = { params: Promise<{ id: string }> };
10
+
11
+ // GET /api/sessions/[id]/simulator - the task's simulator: state and latest shots
12
+ export async function GET(_request: NextRequest, { params }: Ctx) {
13
+ const { id } = await params;
14
+ return NextResponse.json(simulatorStatus(id));
15
+ }
16
+
17
+ // POST - start it in the background (`aos sim open`); poll GET
18
+ export async function POST(_request: NextRequest, { params }: Ctx) {
19
+ try {
20
+ const { id } = await params;
21
+ return NextResponse.json(openSimulator(id), { status: 202 });
22
+ } catch (error) {
23
+ return simulatorFailure(error);
24
+ }
25
+ }
26
+
27
+ // DELETE - end it (`aos sim close`): shut down and removed
28
+ export async function DELETE(_request: NextRequest, { params }: Ctx) {
29
+ const { id } = await params;
30
+ await releaseSimulator(id, { remove: true });
31
+ return NextResponse.json(simulatorStatus(id));
32
+ }
@@ -0,0 +1,37 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { runSimulator, simulatorLogs } from "@/lib/simulator";
3
+ import { simulatorFailure } from "@/lib/simulator/http";
4
+
5
+ // POST /api/sessions/[id]/simulator/run - one allow-listed agent-device call
6
+ // on the task's own simulator, or its recent app log: { args } | { logs }
7
+ export async function POST(
8
+ request: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ try {
12
+ const { id } = await params;
13
+ const body = (await request.json().catch(() => ({}))) as {
14
+ args?: unknown;
15
+ logs?: unknown;
16
+ };
17
+ if (typeof body.logs === "string")
18
+ return NextResponse.json({
19
+ stdout: await simulatorLogs(id, body.logs),
20
+ stderr: "",
21
+ code: 0,
22
+ screenshot: null,
23
+ });
24
+ if (
25
+ !Array.isArray(body.args) ||
26
+ !body.args.length ||
27
+ !body.args.every((a) => typeof a === "string")
28
+ )
29
+ return NextResponse.json(
30
+ { error: "args must be a list of strings" },
31
+ { status: 400 }
32
+ );
33
+ return NextResponse.json(await runSimulator(id, body.args as string[]));
34
+ } catch (error) {
35
+ return simulatorFailure(error);
36
+ }
37
+ }
@@ -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(false);
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 (!open) return;
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 className="rounded-lg">
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 tasks = items.filter((i): i is Task => i.kind === "task");
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
- : `${shown.length} background task${shown.length === 1 ? "" : "s"}`}
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
- <ToolBodies.Provider value={bodies}>
486
- <div
487
- // A new conversation starts over, scrolled to the end.
488
- key={sessionId}
489
- ref={listRef}
490
- onScroll={onListScroll}
491
- onCopy={onCopy}
492
- className="h-full overflow-y-auto overscroll-y-contain"
493
- >
494
- <div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-4 pt-5 pb-4">
495
- {hasMore && (
496
- <div className="text-muted-foreground flex min-h-11 items-center justify-center text-xs">
497
- {loadingOlder ? (
498
- <Loader2 className="h-4 w-4 animate-spin" />
499
- ) : (
500
- <button
501
- type="button"
502
- onClick={loadOlder}
503
- className="hover:text-foreground min-h-11 px-3"
504
- >
505
- Show earlier messages
506
- </button>
507
- )}
508
- </div>
509
- )}
510
- <SetupCard sessionId={sessionId} />
511
- {blocks.length === 0 && !queue.length && (
512
- <p className="text-muted-foreground py-16 text-center text-sm">
513
- Ask anything to start.
514
- </p>
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
- <TimelineRows blocks={blocks} actions={actions} />
518
- </div>
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 { StatusIcon, useToolBody } from "./Tools";
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
- <button
27
- type="button"
28
- onClick={() => setOpen(!open)}
29
- className="flex min-h-10 w-full items-center gap-2 px-1.5 text-left text-xs md:min-h-8"
30
- >
31
- <ChevronRight
32
- className={cn(
33
- "text-muted-foreground h-3 w-3 shrink-0 transition-transform",
34
- open && "rotate-90"
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
- <Bot className="text-primary h-3.5 w-3.5 shrink-0" />
38
- <span className="min-w-0 flex-1 truncate">{item.title}</span>
39
- {input.subagent_type && (
40
- <span className="text-muted-foreground hidden font-mono sm:inline">
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 && (