@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.
- package/README.md +57 -0
- package/app/api/sessions/[id]/simulator/route.ts +32 -0
- package/app/api/sessions/[id]/simulator/run/route.ts +37 -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/data/chat/useChat.ts +40 -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 +19 -0
- package/lib/project-config/agentos.schema.json +19 -0
- package/lib/project-config/brief.ts +3 -0
- package/lib/project-config/schema.ts +10 -0
- package/lib/security/auth.ts +11 -1
- package/lib/sessions/end.ts +3 -0
- package/lib/sessions/stop.ts +3 -0
- package/lib/simulator/agent-device.ts +258 -0
- package/lib/simulator/available.ts +5 -0
- package/lib/simulator/brief.ts +18 -0
- package/lib/simulator/http.ts +16 -0
- package/lib/simulator/index.ts +458 -0
- package/lib/simulator/lease.ts +169 -0
- package/lib/simulator/simctl.ts +167 -0
- package/package.json +1 -1
- 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(
|
|
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 && (
|