@omercnet/paseo-agent-crew 0.2.3
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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/bun.lock +1213 -0
- package/client/crew.ts +267 -0
- package/client/main.tsx +1043 -0
- package/docs/images/agent-crew-action.png +0 -0
- package/docs/images/agent-crew-overview.png +0 -0
- package/index.client.tsx +24 -0
- package/package.json +57 -0
- package/paseo-plugin.json +6 -0
- package/tsconfig.json +16 -0
package/client/main.tsx
ADDED
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
import { type PluginWorkspacePanelProps, usePaseo, useWorkspace } from "@getpaseo/plugin/client";
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
Icon,
|
|
5
|
+
Modal,
|
|
6
|
+
ScrollView,
|
|
7
|
+
TextInput,
|
|
8
|
+
useToast,
|
|
9
|
+
} from "@getpaseo/plugin/client/react-native";
|
|
10
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
|
+
import { useEffect, useMemo, useState } from "react";
|
|
12
|
+
import { type ListRenderItemInfo, Pressable, StyleSheet, Text, View } from "react-native";
|
|
13
|
+
import {
|
|
14
|
+
type AgentEntry,
|
|
15
|
+
agentAgeTimestamp,
|
|
16
|
+
agentTitle,
|
|
17
|
+
buildCrewForest,
|
|
18
|
+
CREW_STATE_LABELS,
|
|
19
|
+
CREW_STATES,
|
|
20
|
+
type CrewNode,
|
|
21
|
+
type CrewState,
|
|
22
|
+
collapseCrewNodes,
|
|
23
|
+
crewCounts,
|
|
24
|
+
crewState,
|
|
25
|
+
formatAge,
|
|
26
|
+
isWorking,
|
|
27
|
+
type PaseoApi,
|
|
28
|
+
type PaseoWorkspace,
|
|
29
|
+
parentAgentId,
|
|
30
|
+
} from "./crew";
|
|
31
|
+
|
|
32
|
+
const PAGE_LIMIT = 200;
|
|
33
|
+
const MAX_PAGES = 10;
|
|
34
|
+
const REFRESH_DEBOUNCE_MS = 500;
|
|
35
|
+
const BACKSTOP_REFRESH_MS = 30_000;
|
|
36
|
+
const CLOCK_INTERVAL_MS = 15_000;
|
|
37
|
+
|
|
38
|
+
type CrewData = {
|
|
39
|
+
entries: AgentEntry[];
|
|
40
|
+
workspaceNames: ReadonlyMap<string, string>;
|
|
41
|
+
truncated: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type CrewAction =
|
|
45
|
+
| { kind: "send"; agentId: string; text: string; interrupted: boolean }
|
|
46
|
+
| { kind: "detach"; agentId: string }
|
|
47
|
+
| { kind: "archive"; agentId: string };
|
|
48
|
+
|
|
49
|
+
type PermissionRequest = AgentEntry["agent"]["pendingPermissions"][number];
|
|
50
|
+
|
|
51
|
+
type PermissionActionInput = {
|
|
52
|
+
agentId: string;
|
|
53
|
+
request: PermissionRequest;
|
|
54
|
+
behavior: "allow" | "deny";
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type PermissionDialogState = { node: CrewNode; request: PermissionRequest } | null;
|
|
58
|
+
type DialogState = { kind: "message" | "detach" | "archive"; node: CrewNode } | null;
|
|
59
|
+
|
|
60
|
+
async function loadAgents(paseo: PaseoApi): Promise<{ entries: AgentEntry[]; truncated: boolean }> {
|
|
61
|
+
const entries: AgentEntry[] = [];
|
|
62
|
+
let cursor: string | undefined;
|
|
63
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
64
|
+
const result = await paseo.agents.list({
|
|
65
|
+
sort: [{ key: "updated_at", direction: "desc" }],
|
|
66
|
+
page: { limit: PAGE_LIMIT, ...(cursor ? { cursor } : {}) },
|
|
67
|
+
});
|
|
68
|
+
entries.push(...result.entries);
|
|
69
|
+
cursor = result.pageInfo.hasMore ? (result.pageInfo.nextCursor ?? undefined) : undefined;
|
|
70
|
+
if (!cursor) return { entries, truncated: false };
|
|
71
|
+
}
|
|
72
|
+
return { entries, truncated: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadWorkspaces(paseo: PaseoApi): Promise<PaseoWorkspace[]> {
|
|
76
|
+
const workspaces: PaseoWorkspace[] = [];
|
|
77
|
+
let cursor: string | undefined;
|
|
78
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
79
|
+
const result = await paseo.workspaces.list({
|
|
80
|
+
page: { limit: PAGE_LIMIT, ...(cursor ? { cursor } : {}) },
|
|
81
|
+
});
|
|
82
|
+
workspaces.push(...result.entries);
|
|
83
|
+
cursor = result.pageInfo.hasMore ? (result.pageInfo.nextCursor ?? undefined) : undefined;
|
|
84
|
+
if (!cursor) break;
|
|
85
|
+
}
|
|
86
|
+
return workspaces;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function loadCrewData(paseo: PaseoApi): Promise<CrewData> {
|
|
90
|
+
const [agents, workspaces] = await Promise.all([loadAgents(paseo), loadWorkspaces(paseo)]);
|
|
91
|
+
return {
|
|
92
|
+
entries: agents.entries,
|
|
93
|
+
truncated: agents.truncated,
|
|
94
|
+
workspaceNames: new Map(workspaces.map((workspace) => [workspace.id, workspace.name])),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ActionButton({
|
|
99
|
+
accessibilityLabel,
|
|
100
|
+
color,
|
|
101
|
+
disabled,
|
|
102
|
+
icon,
|
|
103
|
+
onPress,
|
|
104
|
+
}: {
|
|
105
|
+
accessibilityLabel: string;
|
|
106
|
+
color: string;
|
|
107
|
+
disabled?: boolean;
|
|
108
|
+
icon: string;
|
|
109
|
+
onPress(): void;
|
|
110
|
+
}) {
|
|
111
|
+
return (
|
|
112
|
+
<Pressable
|
|
113
|
+
accessibilityRole="button"
|
|
114
|
+
accessibilityLabel={accessibilityLabel}
|
|
115
|
+
accessibilityState={{ disabled: disabled === true }}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
hitSlop={4}
|
|
118
|
+
onPress={onPress}
|
|
119
|
+
style={({ pressed }) => [
|
|
120
|
+
styles.actionButton,
|
|
121
|
+
pressed && styles.pressed,
|
|
122
|
+
disabled && styles.disabled,
|
|
123
|
+
]}
|
|
124
|
+
>
|
|
125
|
+
<Icon name={icon} size={15} color={color} />
|
|
126
|
+
</Pressable>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stateColor(
|
|
131
|
+
state: CrewState,
|
|
132
|
+
colors: PluginWorkspacePanelProps["theme"]["colors"],
|
|
133
|
+
): string {
|
|
134
|
+
if (state === "failed") return colors.statusDanger;
|
|
135
|
+
if (state === "needs-input") return colors.statusWarning;
|
|
136
|
+
if (state === "ready") return colors.statusSuccess;
|
|
137
|
+
if (state === "working") return colors.accent;
|
|
138
|
+
return colors.foregroundMuted;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function permissionRequestText(request: PermissionRequest): string {
|
|
142
|
+
return request.title ?? request.name;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function permissionRequestDetails(request: PermissionRequest): string {
|
|
146
|
+
return [
|
|
147
|
+
`Kind: ${request.kind}`,
|
|
148
|
+
request.description ? `Description: ${request.description}` : null,
|
|
149
|
+
request.input ? `Input: ${JSON.stringify(request.input, null, 2)}` : null,
|
|
150
|
+
request.detail ? `Detail: ${JSON.stringify(request.detail, null, 2)}` : null,
|
|
151
|
+
request.actions?.length
|
|
152
|
+
? `Actions: ${request.actions.map((action) => action.label).join(" · ")}`
|
|
153
|
+
: null,
|
|
154
|
+
request.suggestions?.length
|
|
155
|
+
? `Suggestions: ${JSON.stringify(request.suggestions, null, 2)}`
|
|
156
|
+
: null,
|
|
157
|
+
]
|
|
158
|
+
.filter((line): line is string => Boolean(line))
|
|
159
|
+
.join("\n\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function permissionErrorMessage(error: unknown): string {
|
|
163
|
+
const message = error instanceof Error ? error.message : "Permission decision failed";
|
|
164
|
+
const lower = message.toLowerCase();
|
|
165
|
+
if (
|
|
166
|
+
lower.includes("already resolved") ||
|
|
167
|
+
lower.includes("not pending") ||
|
|
168
|
+
lower.includes("no longer pending") ||
|
|
169
|
+
lower.includes("request not found")
|
|
170
|
+
) {
|
|
171
|
+
return "Permission request was already resolved.";
|
|
172
|
+
}
|
|
173
|
+
if (lower.includes("network") || lower.includes("transport") || lower.includes("ipc")) {
|
|
174
|
+
return "Could not reach the daemon to respond to the permission request.";
|
|
175
|
+
}
|
|
176
|
+
return message;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function AgentCrew({
|
|
180
|
+
theme,
|
|
181
|
+
layout,
|
|
182
|
+
host,
|
|
183
|
+
workspaceId,
|
|
184
|
+
navigation,
|
|
185
|
+
}: PluginWorkspacePanelProps) {
|
|
186
|
+
const paseo = usePaseo();
|
|
187
|
+
const toast = useToast();
|
|
188
|
+
const queryClient = useQueryClient();
|
|
189
|
+
const workspaceTitle = useWorkspace(workspaceId, ({ name, title }) => title?.trim() || name);
|
|
190
|
+
const queryKey = useMemo(() => ["agent-crew", "directory", host.id], [host.id]);
|
|
191
|
+
const { data, error, isPending, isFetching, refetch } = useQuery({
|
|
192
|
+
queryKey,
|
|
193
|
+
queryFn: () => loadCrewData(paseo),
|
|
194
|
+
refetchInterval: BACKSTOP_REFRESH_MS,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const [selectedState, setSelectedState] = useState<CrewState | null>(null);
|
|
198
|
+
const [query, setQuery] = useState("");
|
|
199
|
+
const [now, setNow] = useState(() => Date.now());
|
|
200
|
+
const [dialog, setDialog] = useState<DialogState>(null);
|
|
201
|
+
const [permissionDialog, setPermissionDialog] = useState<PermissionDialogState>(null);
|
|
202
|
+
const [message, setMessage] = useState("");
|
|
203
|
+
const [collapsedAgentIds, setCollapsedAgentIds] = useState<ReadonlySet<string>>(() => new Set());
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const clock = setInterval(() => setNow(Date.now()), CLOCK_INTERVAL_MS);
|
|
207
|
+
return () => clearInterval(clock);
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
212
|
+
const invalidate = () => {
|
|
213
|
+
if (debounce) return;
|
|
214
|
+
debounce = setTimeout(() => {
|
|
215
|
+
debounce = undefined;
|
|
216
|
+
void queryClient.invalidateQueries({ queryKey });
|
|
217
|
+
}, REFRESH_DEBOUNCE_MS);
|
|
218
|
+
};
|
|
219
|
+
const unsubscribeAgents = paseo.agents.subscribe(invalidate);
|
|
220
|
+
const unsubscribeWorkspaces = paseo.workspaces.subscribe(invalidate);
|
|
221
|
+
return () => {
|
|
222
|
+
clearTimeout(debounce);
|
|
223
|
+
unsubscribeAgents();
|
|
224
|
+
unsubscribeWorkspaces();
|
|
225
|
+
};
|
|
226
|
+
}, [paseo, queryClient, queryKey]);
|
|
227
|
+
|
|
228
|
+
const allNodes = useMemo(
|
|
229
|
+
() =>
|
|
230
|
+
buildCrewForest(data?.entries ?? [], workspaceId, {
|
|
231
|
+
state: null,
|
|
232
|
+
query: "",
|
|
233
|
+
workspaceNames: data?.workspaceNames,
|
|
234
|
+
}),
|
|
235
|
+
[data, workspaceId],
|
|
236
|
+
);
|
|
237
|
+
const nodes = useMemo(
|
|
238
|
+
() =>
|
|
239
|
+
buildCrewForest(data?.entries ?? [], workspaceId, {
|
|
240
|
+
state: selectedState,
|
|
241
|
+
query,
|
|
242
|
+
workspaceNames: data?.workspaceNames,
|
|
243
|
+
}),
|
|
244
|
+
[data, query, selectedState, workspaceId],
|
|
245
|
+
);
|
|
246
|
+
const visibleNodes = useMemo(
|
|
247
|
+
() => collapseCrewNodes(nodes, collapsedAgentIds),
|
|
248
|
+
[collapsedAgentIds, nodes],
|
|
249
|
+
);
|
|
250
|
+
const counts = useMemo(() => crewCounts(allNodes), [allNodes]);
|
|
251
|
+
const memberNodes = useMemo(() => allNodes.filter(({ member }) => member), [allNodes]);
|
|
252
|
+
const crewCount = useMemo(
|
|
253
|
+
() =>
|
|
254
|
+
allNodes.filter(({ depth, descendantCount }) => depth === 0 && descendantCount > 0).length,
|
|
255
|
+
[allNodes],
|
|
256
|
+
);
|
|
257
|
+
const externalCount = useMemo(
|
|
258
|
+
() =>
|
|
259
|
+
memberNodes.filter(
|
|
260
|
+
({ entry }) => entry.agent.workspaceId && entry.agent.workspaceId !== workspaceId,
|
|
261
|
+
).length,
|
|
262
|
+
[memberNodes, workspaceId],
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const action = useMutation({
|
|
266
|
+
mutationFn: async (input: CrewAction) => {
|
|
267
|
+
const handle = paseo.agents.ref(input.agentId);
|
|
268
|
+
if (input.kind === "send") {
|
|
269
|
+
await handle.send(input.text);
|
|
270
|
+
} else if (input.kind === "detach") {
|
|
271
|
+
await handle.detach();
|
|
272
|
+
} else {
|
|
273
|
+
await handle.archive();
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
onSuccess: (_result, input) => {
|
|
277
|
+
if (input.kind === "send") {
|
|
278
|
+
toast.show(input.interrupted ? "Agent redirected" : "Nudge sent", { variant: "success" });
|
|
279
|
+
} else if (input.kind === "detach") {
|
|
280
|
+
toast.show("Subagent detached", { variant: "success" });
|
|
281
|
+
} else {
|
|
282
|
+
toast.show("Subagent archived", { variant: "success" });
|
|
283
|
+
}
|
|
284
|
+
setDialog(null);
|
|
285
|
+
setMessage("");
|
|
286
|
+
},
|
|
287
|
+
onError: (mutationError) => {
|
|
288
|
+
toast.error(mutationError instanceof Error ? mutationError.message : "Agent action failed");
|
|
289
|
+
},
|
|
290
|
+
onSettled: async () => {
|
|
291
|
+
await queryClient.invalidateQueries({ queryKey });
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const permissionAction = useMutation({
|
|
296
|
+
mutationFn: async (input: PermissionActionInput) => {
|
|
297
|
+
await paseo.agents.ref(input.agentId).respondToPermission({
|
|
298
|
+
requestId: input.request.id,
|
|
299
|
+
response:
|
|
300
|
+
input.behavior === "allow"
|
|
301
|
+
? { behavior: "allow" }
|
|
302
|
+
: { behavior: "deny", message: "Denied from Agent Crew" },
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
onSuccess: (_result, input) => {
|
|
306
|
+
toast.show(input.behavior === "allow" ? "Permission allowed" : "Permission denied", {
|
|
307
|
+
variant: "success",
|
|
308
|
+
});
|
|
309
|
+
setPermissionDialog(null);
|
|
310
|
+
},
|
|
311
|
+
onError: (mutationError) => {
|
|
312
|
+
const message = permissionErrorMessage(mutationError);
|
|
313
|
+
toast.error(message);
|
|
314
|
+
if (message === "Permission request was already resolved.") {
|
|
315
|
+
setPermissionDialog(null);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
onSettled: async () => {
|
|
319
|
+
await queryClient.invalidateQueries({ queryKey });
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const panelStyles = useMemo(
|
|
324
|
+
() =>
|
|
325
|
+
StyleSheet.create({
|
|
326
|
+
screen: {
|
|
327
|
+
flex: 1,
|
|
328
|
+
backgroundColor: theme.colors.surface0,
|
|
329
|
+
},
|
|
330
|
+
header: {
|
|
331
|
+
paddingHorizontal: layout.compact ? 14 : 20,
|
|
332
|
+
paddingTop: layout.compact ? 14 : 18,
|
|
333
|
+
paddingBottom: 12,
|
|
334
|
+
gap: 10,
|
|
335
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
336
|
+
borderBottomColor: theme.colors.border,
|
|
337
|
+
},
|
|
338
|
+
titleRow: {
|
|
339
|
+
flexDirection: "row",
|
|
340
|
+
alignItems: "center",
|
|
341
|
+
justifyContent: "space-between",
|
|
342
|
+
gap: 12,
|
|
343
|
+
},
|
|
344
|
+
titleBlock: { flex: 1, minWidth: 0 },
|
|
345
|
+
eyebrow: {
|
|
346
|
+
color: theme.colors.foregroundMuted,
|
|
347
|
+
fontSize: 11,
|
|
348
|
+
fontWeight: "600",
|
|
349
|
+
letterSpacing: 0.8,
|
|
350
|
+
textTransform: "uppercase",
|
|
351
|
+
},
|
|
352
|
+
title: {
|
|
353
|
+
color: theme.colors.foreground,
|
|
354
|
+
fontSize: layout.compact ? 19 : 22,
|
|
355
|
+
fontWeight: "700",
|
|
356
|
+
},
|
|
357
|
+
summary: {
|
|
358
|
+
color: theme.colors.foregroundMuted,
|
|
359
|
+
fontSize: 12,
|
|
360
|
+
},
|
|
361
|
+
toolbar: {
|
|
362
|
+
flexDirection: "row",
|
|
363
|
+
alignItems: "center",
|
|
364
|
+
gap: 8,
|
|
365
|
+
},
|
|
366
|
+
search: {
|
|
367
|
+
flex: 1,
|
|
368
|
+
minWidth: 120,
|
|
369
|
+
height: 36,
|
|
370
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
371
|
+
borderColor: theme.colors.border,
|
|
372
|
+
borderRadius: 8,
|
|
373
|
+
paddingHorizontal: 10,
|
|
374
|
+
color: theme.colors.foreground,
|
|
375
|
+
backgroundColor: theme.colors.surface1,
|
|
376
|
+
fontSize: 13,
|
|
377
|
+
},
|
|
378
|
+
refreshButton: {
|
|
379
|
+
minHeight: 36,
|
|
380
|
+
paddingHorizontal: 10,
|
|
381
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
382
|
+
borderColor: theme.colors.border,
|
|
383
|
+
borderRadius: 8,
|
|
384
|
+
alignItems: "center",
|
|
385
|
+
justifyContent: "center",
|
|
386
|
+
backgroundColor: theme.colors.surface1,
|
|
387
|
+
},
|
|
388
|
+
chipRail: { gap: 6 },
|
|
389
|
+
chip: {
|
|
390
|
+
minHeight: 30,
|
|
391
|
+
flexDirection: "row",
|
|
392
|
+
alignItems: "center",
|
|
393
|
+
gap: 6,
|
|
394
|
+
paddingHorizontal: 9,
|
|
395
|
+
borderRadius: 15,
|
|
396
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
397
|
+
borderColor: theme.colors.border,
|
|
398
|
+
backgroundColor: theme.colors.surface1,
|
|
399
|
+
},
|
|
400
|
+
chipSelected: {
|
|
401
|
+
borderColor: theme.colors.accent,
|
|
402
|
+
backgroundColor: theme.colors.surface2,
|
|
403
|
+
},
|
|
404
|
+
chipText: { color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
405
|
+
chipTextSelected: { color: theme.colors.foreground, fontWeight: "600" },
|
|
406
|
+
dot: { width: 7, height: 7, borderRadius: 4 },
|
|
407
|
+
row: {
|
|
408
|
+
minHeight: layout.compact ? 78 : 68,
|
|
409
|
+
flexDirection: "row",
|
|
410
|
+
alignItems: "center",
|
|
411
|
+
gap: 10,
|
|
412
|
+
paddingVertical: 10,
|
|
413
|
+
paddingRight: layout.compact ? 10 : 16,
|
|
414
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
415
|
+
borderBottomColor: theme.colors.border,
|
|
416
|
+
},
|
|
417
|
+
contextRow: { opacity: 0.58 },
|
|
418
|
+
treeRail: {
|
|
419
|
+
alignSelf: "stretch",
|
|
420
|
+
width: 10,
|
|
421
|
+
borderLeftWidth: 2,
|
|
422
|
+
borderLeftColor: theme.colors.border,
|
|
423
|
+
},
|
|
424
|
+
rowBody: { flex: 1, minWidth: 0, gap: 3 },
|
|
425
|
+
rowTitleLine: { flexDirection: "row", alignItems: "center", gap: 7 },
|
|
426
|
+
rowTitle: {
|
|
427
|
+
flexShrink: 1,
|
|
428
|
+
color: theme.colors.foreground,
|
|
429
|
+
fontSize: 14,
|
|
430
|
+
fontWeight: "600",
|
|
431
|
+
},
|
|
432
|
+
rootRow: { backgroundColor: theme.colors.surface1 },
|
|
433
|
+
childCount: { flexShrink: 0, color: theme.colors.foregroundMuted, fontSize: 11 },
|
|
434
|
+
collapseSpacer: { width: 30, height: 30 },
|
|
435
|
+
metadata: { color: theme.colors.foregroundMuted, fontSize: 11 },
|
|
436
|
+
lastError: { color: theme.colors.statusDanger, fontSize: 11 },
|
|
437
|
+
permissionCount: { color: theme.colors.statusWarning, fontSize: 11, fontWeight: "600" },
|
|
438
|
+
statusColumn: { alignItems: "flex-end", gap: 5 },
|
|
439
|
+
status: { fontSize: 11, fontWeight: "600" },
|
|
440
|
+
age: { color: theme.colors.foregroundMuted, fontSize: 10 },
|
|
441
|
+
rowActions: { flexDirection: "row", alignItems: "center", gap: 2 },
|
|
442
|
+
empty: { padding: 24, gap: 6, alignItems: "center" },
|
|
443
|
+
emptyTitle: { color: theme.colors.foreground, fontSize: 16, fontWeight: "600" },
|
|
444
|
+
emptyBody: { color: theme.colors.foregroundMuted, fontSize: 13, textAlign: "center" },
|
|
445
|
+
error: {
|
|
446
|
+
margin: 12,
|
|
447
|
+
padding: 10,
|
|
448
|
+
color: theme.colors.statusDanger,
|
|
449
|
+
backgroundColor: theme.colors.surface1,
|
|
450
|
+
borderRadius: 8,
|
|
451
|
+
},
|
|
452
|
+
contextBadge: {
|
|
453
|
+
color: theme.colors.foregroundMuted,
|
|
454
|
+
fontSize: 10,
|
|
455
|
+
fontWeight: "600",
|
|
456
|
+
textTransform: "uppercase",
|
|
457
|
+
},
|
|
458
|
+
truncated: { paddingHorizontal: 16, paddingVertical: 8, color: theme.colors.statusWarning },
|
|
459
|
+
modalBody: { gap: 14, padding: 18 },
|
|
460
|
+
modalCopy: { color: theme.colors.foregroundMuted, fontSize: 13, lineHeight: 19 },
|
|
461
|
+
messageInput: {
|
|
462
|
+
minHeight: 112,
|
|
463
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
464
|
+
borderColor: theme.colors.border,
|
|
465
|
+
borderRadius: 8,
|
|
466
|
+
padding: 10,
|
|
467
|
+
color: theme.colors.foreground,
|
|
468
|
+
backgroundColor: theme.colors.surface1,
|
|
469
|
+
textAlignVertical: "top",
|
|
470
|
+
},
|
|
471
|
+
modalActions: { flexDirection: "row", justifyContent: "flex-end", gap: 8 },
|
|
472
|
+
secondaryButton: {
|
|
473
|
+
minHeight: 36,
|
|
474
|
+
justifyContent: "center",
|
|
475
|
+
paddingHorizontal: 12,
|
|
476
|
+
borderRadius: 8,
|
|
477
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
478
|
+
borderColor: theme.colors.border,
|
|
479
|
+
backgroundColor: theme.colors.surface1,
|
|
480
|
+
},
|
|
481
|
+
primaryButton: {
|
|
482
|
+
minHeight: 36,
|
|
483
|
+
justifyContent: "center",
|
|
484
|
+
paddingHorizontal: 12,
|
|
485
|
+
borderRadius: 8,
|
|
486
|
+
backgroundColor: theme.colors.accent,
|
|
487
|
+
},
|
|
488
|
+
dangerButton: { backgroundColor: theme.colors.statusDanger },
|
|
489
|
+
buttonText: { color: theme.colors.foreground, fontWeight: "600", fontSize: 13 },
|
|
490
|
+
primaryButtonText: {
|
|
491
|
+
color: theme.colors.accentForeground,
|
|
492
|
+
fontWeight: "600",
|
|
493
|
+
fontSize: 13,
|
|
494
|
+
},
|
|
495
|
+
permissionBody: { gap: 12 },
|
|
496
|
+
permissionSection: { gap: 4 },
|
|
497
|
+
permissionLabel: { color: theme.colors.foregroundMuted, fontSize: 11, fontWeight: "600" },
|
|
498
|
+
permissionValue: { color: theme.colors.foreground, fontSize: 13, lineHeight: 18 },
|
|
499
|
+
permissionJson: {
|
|
500
|
+
color: theme.colors.foreground,
|
|
501
|
+
fontSize: 12,
|
|
502
|
+
lineHeight: 17,
|
|
503
|
+
fontFamily: "monospace",
|
|
504
|
+
backgroundColor: theme.colors.surface1,
|
|
505
|
+
borderRadius: 8,
|
|
506
|
+
padding: 10,
|
|
507
|
+
},
|
|
508
|
+
permissionActions: { flexDirection: "row", justifyContent: "flex-end", gap: 8 },
|
|
509
|
+
permissionButton: {
|
|
510
|
+
minHeight: 36,
|
|
511
|
+
justifyContent: "center",
|
|
512
|
+
paddingHorizontal: 12,
|
|
513
|
+
borderRadius: 8,
|
|
514
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
515
|
+
borderColor: theme.colors.border,
|
|
516
|
+
backgroundColor: theme.colors.surface1,
|
|
517
|
+
},
|
|
518
|
+
permissionPrimary: { backgroundColor: theme.colors.accent },
|
|
519
|
+
permissionDeny: { backgroundColor: theme.colors.statusDanger },
|
|
520
|
+
permissionButtonText: { color: theme.colors.foreground, fontWeight: "600", fontSize: 13 },
|
|
521
|
+
permissionDenyText: { color: theme.colors.surface0, fontWeight: "600", fontSize: 13 },
|
|
522
|
+
permissionPrimaryText: {
|
|
523
|
+
color: theme.colors.accentForeground,
|
|
524
|
+
fontWeight: "600",
|
|
525
|
+
fontSize: 13,
|
|
526
|
+
},
|
|
527
|
+
}),
|
|
528
|
+
[layout.compact, theme],
|
|
529
|
+
);
|
|
530
|
+
function openDialog(kind: NonNullable<DialogState>["kind"], node: CrewNode) {
|
|
531
|
+
setMessage("");
|
|
532
|
+
setDialog({ kind, node });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function submitDialog() {
|
|
536
|
+
if (!dialog || action.isPending) return;
|
|
537
|
+
const agent = dialog.node.entry.agent;
|
|
538
|
+
if (dialog.kind === "message") {
|
|
539
|
+
const text = message.trim();
|
|
540
|
+
if (!text) return;
|
|
541
|
+
action.mutate({ kind: "send", agentId: agent.id, text, interrupted: isWorking(agent) });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
action.mutate({ kind: dialog.kind, agentId: agent.id });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function toggleCollapsed(agentId: string) {
|
|
548
|
+
setCollapsedAgentIds((current) => {
|
|
549
|
+
const next = new Set(current);
|
|
550
|
+
if (next.has(agentId)) next.delete(agentId);
|
|
551
|
+
else next.add(agentId);
|
|
552
|
+
return next;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function openPermissionDialog(node: CrewNode, request: PermissionRequest) {
|
|
557
|
+
setPermissionDialog({ node, request });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function submitPermissionAction(behavior: "allow" | "deny") {
|
|
561
|
+
if (!permissionDialog || permissionAction.isPending) return;
|
|
562
|
+
permissionAction.mutate({
|
|
563
|
+
agentId: permissionDialog.node.entry.agent.id,
|
|
564
|
+
request: permissionDialog.request,
|
|
565
|
+
behavior,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function renderRow({ item }: ListRenderItemInfo<CrewNode>) {
|
|
570
|
+
const agent = item.entry.agent;
|
|
571
|
+
const state = crewState(agent);
|
|
572
|
+
const color = stateColor(state, theme.colors);
|
|
573
|
+
const workspaceName = agent.workspaceId
|
|
574
|
+
? data?.workspaceNames.get(agent.workspaceId)
|
|
575
|
+
: undefined;
|
|
576
|
+
const external = Boolean(agent.workspaceId && agent.workspaceId !== workspaceId);
|
|
577
|
+
const workspaceDetail = workspaceName
|
|
578
|
+
? `${external ? "Elsewhere: " : ""}${workspaceName}`
|
|
579
|
+
: external
|
|
580
|
+
? "Elsewhere"
|
|
581
|
+
: undefined;
|
|
582
|
+
const providerModel = agent.model ? `${agent.provider}/${agent.model}` : agent.provider;
|
|
583
|
+
const metadata = workspaceDetail ? `${providerModel} · ${workspaceDetail}` : providerModel;
|
|
584
|
+
const age = formatAge(agentAgeTimestamp(agent), now);
|
|
585
|
+
const expandable = item.descendantCount > 0;
|
|
586
|
+
const collapsed = collapsedAgentIds.has(agent.id);
|
|
587
|
+
const pendingPermissions = agent.pendingPermissions ?? [];
|
|
588
|
+
const pendingPermission = pendingPermissions[0];
|
|
589
|
+
const pendingPermissionCount = pendingPermissions.length;
|
|
590
|
+
const rowBody = (
|
|
591
|
+
<>
|
|
592
|
+
<View style={panelStyles.rowTitleLine}>
|
|
593
|
+
<View style={[panelStyles.dot, { backgroundColor: color }]} />
|
|
594
|
+
<Text style={panelStyles.rowTitle} numberOfLines={1}>
|
|
595
|
+
{agentTitle(item.entry)}
|
|
596
|
+
</Text>
|
|
597
|
+
{!item.member ? <Text style={panelStyles.contextBadge}>Context</Text> : null}
|
|
598
|
+
{expandable ? (
|
|
599
|
+
<Text style={panelStyles.childCount} numberOfLines={1}>
|
|
600
|
+
{item.descendantCount} {item.descendantCount === 1 ? "descendant" : "descendants"}
|
|
601
|
+
</Text>
|
|
602
|
+
) : null}
|
|
603
|
+
</View>
|
|
604
|
+
<Text style={panelStyles.metadata} numberOfLines={1}>
|
|
605
|
+
{metadata}
|
|
606
|
+
</Text>
|
|
607
|
+
{agent.lastError ? (
|
|
608
|
+
<Text style={panelStyles.lastError} numberOfLines={1}>
|
|
609
|
+
{agent.lastError}
|
|
610
|
+
</Text>
|
|
611
|
+
) : null}
|
|
612
|
+
</>
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
return (
|
|
616
|
+
<View
|
|
617
|
+
style={[
|
|
618
|
+
panelStyles.row,
|
|
619
|
+
item.depth === 0 && panelStyles.rootRow,
|
|
620
|
+
item.contextOnly && panelStyles.contextRow,
|
|
621
|
+
{ paddingLeft: 12 + Math.min(item.depth, 6) * 16 },
|
|
622
|
+
]}
|
|
623
|
+
>
|
|
624
|
+
{item.depth > 0 ? <View style={panelStyles.treeRail} /> : null}
|
|
625
|
+
{expandable ? (
|
|
626
|
+
<ActionButton
|
|
627
|
+
accessibilityLabel={`${collapsed ? "Expand" : "Collapse"} ${agentTitle(item.entry)}`}
|
|
628
|
+
color={theme.colors.foregroundMuted}
|
|
629
|
+
icon={collapsed ? "ChevronRight" : "ChevronDown"}
|
|
630
|
+
onPress={() => toggleCollapsed(agent.id)}
|
|
631
|
+
/>
|
|
632
|
+
) : (
|
|
633
|
+
<View style={panelStyles.collapseSpacer} />
|
|
634
|
+
)}
|
|
635
|
+
{navigation ? (
|
|
636
|
+
<Pressable
|
|
637
|
+
accessibilityRole="button"
|
|
638
|
+
accessibilityLabel={`Open ${agentTitle(item.entry)}`}
|
|
639
|
+
onPress={() => navigation.openAgent({ agentId: agent.id })}
|
|
640
|
+
style={({ pressed }) => [panelStyles.rowBody, pressed && styles.pressed]}
|
|
641
|
+
>
|
|
642
|
+
{rowBody}
|
|
643
|
+
</Pressable>
|
|
644
|
+
) : (
|
|
645
|
+
<View style={panelStyles.rowBody}>{rowBody}</View>
|
|
646
|
+
)}
|
|
647
|
+
<View style={panelStyles.statusColumn}>
|
|
648
|
+
<Text
|
|
649
|
+
style={[
|
|
650
|
+
panelStyles.status,
|
|
651
|
+
{ color: item.member ? color : theme.colors.foregroundMuted },
|
|
652
|
+
]}
|
|
653
|
+
>
|
|
654
|
+
{item.member ? CREW_STATE_LABELS[state] : "Context"}
|
|
655
|
+
</Text>
|
|
656
|
+
{pendingPermissionCount > 0 ? (
|
|
657
|
+
<Text style={panelStyles.permissionCount} numberOfLines={1}>
|
|
658
|
+
{pendingPermissionCount} pending
|
|
659
|
+
</Text>
|
|
660
|
+
) : null}
|
|
661
|
+
{age ? <Text style={panelStyles.age}>{age}</Text> : null}
|
|
662
|
+
{item.member ? (
|
|
663
|
+
<View style={panelStyles.rowActions}>
|
|
664
|
+
{pendingPermission ? (
|
|
665
|
+
<ActionButton
|
|
666
|
+
accessibilityLabel={`Review ${pendingPermissionCount} pending permission ${pendingPermissionCount === 1 ? "request" : "requests"} for ${agentTitle(item.entry)}`}
|
|
667
|
+
color={theme.colors.statusWarning}
|
|
668
|
+
disabled={permissionAction.isPending}
|
|
669
|
+
icon="Lock"
|
|
670
|
+
onPress={() => openPermissionDialog(item, pendingPermission)}
|
|
671
|
+
/>
|
|
672
|
+
) : null}
|
|
673
|
+
{agent.status !== "closed" ? (
|
|
674
|
+
<ActionButton
|
|
675
|
+
accessibilityLabel={`${isWorking(agent) ? "Interrupt and redirect" : "Nudge"} ${agentTitle(item.entry)}`}
|
|
676
|
+
color={
|
|
677
|
+
isWorking(agent) ? theme.colors.statusWarning : theme.colors.foregroundMuted
|
|
678
|
+
}
|
|
679
|
+
disabled={action.isPending}
|
|
680
|
+
icon={isWorking(agent) ? "CornerDownRight" : "MessageSquareMore"}
|
|
681
|
+
onPress={() => openDialog("message", item)}
|
|
682
|
+
/>
|
|
683
|
+
) : null}
|
|
684
|
+
{parentAgentId(agent) ? (
|
|
685
|
+
<ActionButton
|
|
686
|
+
accessibilityLabel={`Detach ${agentTitle(item.entry)}`}
|
|
687
|
+
color={theme.colors.foregroundMuted}
|
|
688
|
+
disabled={action.isPending}
|
|
689
|
+
icon="Unlink"
|
|
690
|
+
onPress={() => openDialog("detach", item)}
|
|
691
|
+
/>
|
|
692
|
+
) : null}
|
|
693
|
+
<ActionButton
|
|
694
|
+
accessibilityLabel={`Archive ${agentTitle(item.entry)}`}
|
|
695
|
+
color={theme.colors.foregroundMuted}
|
|
696
|
+
disabled={action.isPending}
|
|
697
|
+
icon="Archive"
|
|
698
|
+
onPress={() => openDialog("archive", item)}
|
|
699
|
+
/>
|
|
700
|
+
</View>
|
|
701
|
+
) : null}
|
|
702
|
+
</View>
|
|
703
|
+
</View>
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
const currentWorkspaceTitle = workspaceTitle?.trim() || "Current workspace";
|
|
707
|
+
let dialogTitle = "";
|
|
708
|
+
let dialogCopy = "";
|
|
709
|
+
let confirmLabel = "";
|
|
710
|
+
if (dialog) {
|
|
711
|
+
const target = agentTitle(dialog.node.entry);
|
|
712
|
+
if (dialog.kind === "message") {
|
|
713
|
+
const state = crewState(dialog.node.entry.agent);
|
|
714
|
+
const running = isWorking(dialog.node.entry.agent);
|
|
715
|
+
dialogTitle = running ? `Interrupt and redirect ${target}` : `Nudge ${target}`;
|
|
716
|
+
dialogCopy = running
|
|
717
|
+
? "This agent is working. Sending a message stops its current turn and starts the new direction."
|
|
718
|
+
: state === "needs-input"
|
|
719
|
+
? "This agent is waiting for a permission decision. Sending a message dismisses that request and starts the new direction."
|
|
720
|
+
: "Send a concise follow-up with the missing context or next step.";
|
|
721
|
+
confirmLabel = running
|
|
722
|
+
? "Interrupt & redirect"
|
|
723
|
+
: state === "needs-input"
|
|
724
|
+
? "Dismiss request & nudge"
|
|
725
|
+
: "Send nudge";
|
|
726
|
+
} else if (dialog.kind === "detach") {
|
|
727
|
+
dialogTitle = `Detach ${target}?`;
|
|
728
|
+
dialogCopy =
|
|
729
|
+
dialog.node.descendantCount > 0
|
|
730
|
+
? `This agent and its ${dialog.node.descendantCount} descendants will leave this crew view and continue as standalone agents.`
|
|
731
|
+
: "This agent will leave this crew view and continue as a standalone agent.";
|
|
732
|
+
confirmLabel = "Detach";
|
|
733
|
+
} else {
|
|
734
|
+
dialogTitle = `Archive ${target}?`;
|
|
735
|
+
dialogCopy =
|
|
736
|
+
dialog.node.descendantCount > 0
|
|
737
|
+
? `This agent has ${dialog.node.descendantCount} managed descendants. Same-workspace descendants are archived with it; cross-workspace descendants detach and continue.`
|
|
738
|
+
: isWorking(dialog.node.entry.agent)
|
|
739
|
+
? "This agent is still working. Archiving stops it and removes it from the crew."
|
|
740
|
+
: "This agent will stop and be removed from the crew.";
|
|
741
|
+
confirmLabel = "Archive";
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const permissionDialogTitle = permissionDialog
|
|
745
|
+
? `Permission request from ${agentTitle(permissionDialog.node.entry)}`
|
|
746
|
+
: "Permission request";
|
|
747
|
+
|
|
748
|
+
return (
|
|
749
|
+
<View style={panelStyles.screen}>
|
|
750
|
+
<View style={panelStyles.header}>
|
|
751
|
+
<View style={panelStyles.titleRow}>
|
|
752
|
+
<View style={panelStyles.titleBlock}>
|
|
753
|
+
<Text style={panelStyles.eyebrow}>Agent Crew</Text>
|
|
754
|
+
<Text style={panelStyles.title} numberOfLines={1}>
|
|
755
|
+
{currentWorkspaceTitle}
|
|
756
|
+
</Text>
|
|
757
|
+
<Text style={panelStyles.summary}>
|
|
758
|
+
{memberNodes.length} {memberNodes.length === 1 ? "agent" : "agents"} · {crewCount}{" "}
|
|
759
|
+
{crewCount === 1 ? "crew" : "crews"}
|
|
760
|
+
{externalCount > 0 ? ` · ${externalCount} elsewhere` : ""}
|
|
761
|
+
</Text>
|
|
762
|
+
</View>
|
|
763
|
+
<Pressable
|
|
764
|
+
accessibilityRole="button"
|
|
765
|
+
accessibilityLabel="Refresh Agent Crew"
|
|
766
|
+
onPress={() => void refetch()}
|
|
767
|
+
style={({ pressed }) => [panelStyles.refreshButton, pressed && styles.pressed]}
|
|
768
|
+
>
|
|
769
|
+
<Icon
|
|
770
|
+
name="RefreshCw"
|
|
771
|
+
size={16}
|
|
772
|
+
color={isFetching ? theme.colors.accent : theme.colors.foregroundMuted}
|
|
773
|
+
/>
|
|
774
|
+
</Pressable>
|
|
775
|
+
</View>
|
|
776
|
+
<View style={panelStyles.toolbar}>
|
|
777
|
+
<TextInput
|
|
778
|
+
accessibilityLabel="Filter agents"
|
|
779
|
+
autoCapitalize="none"
|
|
780
|
+
autoCorrect={false}
|
|
781
|
+
onChangeText={setQuery}
|
|
782
|
+
placeholder="Filter by task, model, workspace"
|
|
783
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
784
|
+
value={query}
|
|
785
|
+
style={panelStyles.search}
|
|
786
|
+
/>
|
|
787
|
+
</View>
|
|
788
|
+
<ScrollView
|
|
789
|
+
horizontal
|
|
790
|
+
showsHorizontalScrollIndicator={false}
|
|
791
|
+
contentContainerStyle={panelStyles.chipRail}
|
|
792
|
+
>
|
|
793
|
+
<Pressable
|
|
794
|
+
accessibilityRole="button"
|
|
795
|
+
accessibilityState={{ selected: selectedState === null }}
|
|
796
|
+
onPress={() => setSelectedState(null)}
|
|
797
|
+
style={[panelStyles.chip, selectedState === null && panelStyles.chipSelected]}
|
|
798
|
+
>
|
|
799
|
+
<Text
|
|
800
|
+
style={[panelStyles.chipText, selectedState === null && panelStyles.chipTextSelected]}
|
|
801
|
+
>
|
|
802
|
+
All {memberNodes.length}
|
|
803
|
+
</Text>
|
|
804
|
+
</Pressable>
|
|
805
|
+
{CREW_STATES.map((state) => (
|
|
806
|
+
<Pressable
|
|
807
|
+
key={state}
|
|
808
|
+
accessibilityRole="button"
|
|
809
|
+
accessibilityState={{ selected: selectedState === state }}
|
|
810
|
+
onPress={() => setSelectedState(state)}
|
|
811
|
+
style={[panelStyles.chip, selectedState === state && panelStyles.chipSelected]}
|
|
812
|
+
>
|
|
813
|
+
<View
|
|
814
|
+
style={[panelStyles.dot, { backgroundColor: stateColor(state, theme.colors) }]}
|
|
815
|
+
/>
|
|
816
|
+
<Text
|
|
817
|
+
style={[
|
|
818
|
+
panelStyles.chipText,
|
|
819
|
+
selectedState === state && panelStyles.chipTextSelected,
|
|
820
|
+
]}
|
|
821
|
+
>
|
|
822
|
+
{CREW_STATE_LABELS[state]} {counts[state]}
|
|
823
|
+
</Text>
|
|
824
|
+
</Pressable>
|
|
825
|
+
))}
|
|
826
|
+
</ScrollView>
|
|
827
|
+
</View>
|
|
828
|
+
|
|
829
|
+
{data?.truncated ? (
|
|
830
|
+
<Text style={panelStyles.truncated}>
|
|
831
|
+
Showing the first {PAGE_LIMIT * MAX_PAGES} daemon agents. Some agents in this workspace or
|
|
832
|
+
their crew may be missing.
|
|
833
|
+
</Text>
|
|
834
|
+
) : null}
|
|
835
|
+
{error ? (
|
|
836
|
+
<Text style={panelStyles.error}>
|
|
837
|
+
{error instanceof Error ? error.message : "Could not load agents"}
|
|
838
|
+
</Text>
|
|
839
|
+
) : null}
|
|
840
|
+
<FlatList
|
|
841
|
+
data={visibleNodes}
|
|
842
|
+
keyExtractor={(node) => node.entry.agent.id}
|
|
843
|
+
renderItem={renderRow}
|
|
844
|
+
keyboardShouldPersistTaps="handled"
|
|
845
|
+
ListEmptyComponent={
|
|
846
|
+
<View style={panelStyles.empty}>
|
|
847
|
+
<Icon name="Network" size={24} color={theme.colors.foregroundMuted} />
|
|
848
|
+
<Text style={panelStyles.emptyTitle}>
|
|
849
|
+
{isPending
|
|
850
|
+
? "Loading crew"
|
|
851
|
+
: memberNodes.length === 0
|
|
852
|
+
? "No agents yet"
|
|
853
|
+
: "No matches"}
|
|
854
|
+
</Text>
|
|
855
|
+
<Text style={panelStyles.emptyBody}>
|
|
856
|
+
{isPending
|
|
857
|
+
? "Reading this workspace’s agents."
|
|
858
|
+
: memberNodes.length === 0
|
|
859
|
+
? "Start an agent or ask one to delegate. Crews will appear here."
|
|
860
|
+
: "Change the status filter or search text."}
|
|
861
|
+
</Text>
|
|
862
|
+
</View>
|
|
863
|
+
}
|
|
864
|
+
/>
|
|
865
|
+
|
|
866
|
+
<Modal
|
|
867
|
+
title={dialogTitle}
|
|
868
|
+
open={dialog !== null}
|
|
869
|
+
onOpenChange={(open) => {
|
|
870
|
+
if (!open && !action.isPending) {
|
|
871
|
+
setDialog(null);
|
|
872
|
+
setMessage("");
|
|
873
|
+
}
|
|
874
|
+
}}
|
|
875
|
+
icon={
|
|
876
|
+
dialog ? (
|
|
877
|
+
<Icon
|
|
878
|
+
name={
|
|
879
|
+
dialog.kind === "message"
|
|
880
|
+
? "MessageSquareMore"
|
|
881
|
+
: dialog.kind === "detach"
|
|
882
|
+
? "Unlink"
|
|
883
|
+
: "Archive"
|
|
884
|
+
}
|
|
885
|
+
size={18}
|
|
886
|
+
color={
|
|
887
|
+
dialog.kind === "archive" ? theme.colors.statusDanger : theme.colors.foreground
|
|
888
|
+
}
|
|
889
|
+
/>
|
|
890
|
+
) : undefined
|
|
891
|
+
}
|
|
892
|
+
>
|
|
893
|
+
<Modal.Content>
|
|
894
|
+
<View style={panelStyles.modalBody}>
|
|
895
|
+
<Text style={panelStyles.modalCopy}>{dialogCopy}</Text>
|
|
896
|
+
{dialog?.kind === "message" ? (
|
|
897
|
+
<TextInput
|
|
898
|
+
accessibilityLabel="Message to subagent"
|
|
899
|
+
autoFocus
|
|
900
|
+
multiline
|
|
901
|
+
onChangeText={setMessage}
|
|
902
|
+
placeholder="What should this agent do next?"
|
|
903
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
904
|
+
value={message}
|
|
905
|
+
style={panelStyles.messageInput}
|
|
906
|
+
/>
|
|
907
|
+
) : null}
|
|
908
|
+
<View style={panelStyles.modalActions}>
|
|
909
|
+
<Pressable
|
|
910
|
+
accessibilityRole="button"
|
|
911
|
+
disabled={action.isPending}
|
|
912
|
+
onPress={() => {
|
|
913
|
+
setDialog(null);
|
|
914
|
+
setMessage("");
|
|
915
|
+
}}
|
|
916
|
+
style={({ pressed }) => [panelStyles.secondaryButton, pressed && styles.pressed]}
|
|
917
|
+
>
|
|
918
|
+
<Text style={panelStyles.buttonText}>Cancel</Text>
|
|
919
|
+
</Pressable>
|
|
920
|
+
<Pressable
|
|
921
|
+
accessibilityRole="button"
|
|
922
|
+
disabled={action.isPending || (dialog?.kind === "message" && !message.trim())}
|
|
923
|
+
onPress={submitDialog}
|
|
924
|
+
style={({ pressed }) => [
|
|
925
|
+
panelStyles.primaryButton,
|
|
926
|
+
dialog?.kind === "archive" && panelStyles.dangerButton,
|
|
927
|
+
pressed && styles.pressed,
|
|
928
|
+
(action.isPending || (dialog?.kind === "message" && !message.trim())) &&
|
|
929
|
+
styles.disabled,
|
|
930
|
+
]}
|
|
931
|
+
>
|
|
932
|
+
<Text style={panelStyles.primaryButtonText}>
|
|
933
|
+
{action.isPending ? "Working…" : confirmLabel}
|
|
934
|
+
</Text>
|
|
935
|
+
</Pressable>
|
|
936
|
+
</View>
|
|
937
|
+
</View>
|
|
938
|
+
</Modal.Content>
|
|
939
|
+
</Modal>
|
|
940
|
+
|
|
941
|
+
<Modal
|
|
942
|
+
title={permissionDialogTitle}
|
|
943
|
+
open={permissionDialog !== null}
|
|
944
|
+
onOpenChange={(open) => {
|
|
945
|
+
if (!open && !permissionAction.isPending) {
|
|
946
|
+
setPermissionDialog(null);
|
|
947
|
+
}
|
|
948
|
+
}}
|
|
949
|
+
icon={
|
|
950
|
+
permissionDialog ? (
|
|
951
|
+
<Icon name="Lock" size={18} color={theme.colors.foreground} />
|
|
952
|
+
) : undefined
|
|
953
|
+
}
|
|
954
|
+
>
|
|
955
|
+
<Modal.Content contentContainerStyle={panelStyles.permissionBody}>
|
|
956
|
+
{permissionDialog ? (
|
|
957
|
+
<>
|
|
958
|
+
<Text style={panelStyles.modalCopy}>
|
|
959
|
+
Review the pending permission request below. Allow or deny it explicitly.
|
|
960
|
+
</Text>
|
|
961
|
+
<View style={panelStyles.permissionSection}>
|
|
962
|
+
<Text style={panelStyles.permissionLabel}>Request</Text>
|
|
963
|
+
<Text style={panelStyles.permissionValue}>
|
|
964
|
+
{permissionRequestText(permissionDialog.request)}
|
|
965
|
+
</Text>
|
|
966
|
+
</View>
|
|
967
|
+
<View style={panelStyles.permissionSection}>
|
|
968
|
+
<Text style={panelStyles.permissionLabel}>Kind</Text>
|
|
969
|
+
<Text style={panelStyles.permissionValue}>{permissionDialog.request.kind}</Text>
|
|
970
|
+
</View>
|
|
971
|
+
{permissionDialog.request.description ? (
|
|
972
|
+
<View style={panelStyles.permissionSection}>
|
|
973
|
+
<Text style={panelStyles.permissionLabel}>Description</Text>
|
|
974
|
+
<Text style={panelStyles.permissionValue}>
|
|
975
|
+
{permissionDialog.request.description}
|
|
976
|
+
</Text>
|
|
977
|
+
</View>
|
|
978
|
+
) : null}
|
|
979
|
+
<View style={panelStyles.permissionSection}>
|
|
980
|
+
<Text style={panelStyles.permissionLabel}>Payload</Text>
|
|
981
|
+
<Text selectable style={panelStyles.permissionJson}>
|
|
982
|
+
{permissionRequestDetails(permissionDialog.request)}
|
|
983
|
+
</Text>
|
|
984
|
+
</View>
|
|
985
|
+
<View style={panelStyles.permissionActions}>
|
|
986
|
+
<Pressable
|
|
987
|
+
accessibilityRole="button"
|
|
988
|
+
disabled={permissionAction.isPending}
|
|
989
|
+
onPress={() => setPermissionDialog(null)}
|
|
990
|
+
style={({ pressed }) => [panelStyles.permissionButton, pressed && styles.pressed]}
|
|
991
|
+
>
|
|
992
|
+
<Text style={panelStyles.permissionButtonText}>Cancel</Text>
|
|
993
|
+
</Pressable>
|
|
994
|
+
<Pressable
|
|
995
|
+
accessibilityRole="button"
|
|
996
|
+
disabled={permissionAction.isPending}
|
|
997
|
+
onPress={() => submitPermissionAction("deny")}
|
|
998
|
+
style={({ pressed }) => [
|
|
999
|
+
panelStyles.permissionButton,
|
|
1000
|
+
panelStyles.permissionDeny,
|
|
1001
|
+
pressed && styles.pressed,
|
|
1002
|
+
permissionAction.isPending && styles.disabled,
|
|
1003
|
+
]}
|
|
1004
|
+
>
|
|
1005
|
+
<Text style={panelStyles.permissionDenyText}>
|
|
1006
|
+
{permissionAction.isPending ? "Working…" : "Deny"}
|
|
1007
|
+
</Text>
|
|
1008
|
+
</Pressable>
|
|
1009
|
+
<Pressable
|
|
1010
|
+
accessibilityRole="button"
|
|
1011
|
+
disabled={permissionAction.isPending}
|
|
1012
|
+
onPress={() => submitPermissionAction("allow")}
|
|
1013
|
+
style={({ pressed }) => [
|
|
1014
|
+
panelStyles.permissionButton,
|
|
1015
|
+
panelStyles.permissionPrimary,
|
|
1016
|
+
pressed && styles.pressed,
|
|
1017
|
+
permissionAction.isPending && styles.disabled,
|
|
1018
|
+
]}
|
|
1019
|
+
>
|
|
1020
|
+
<Text style={panelStyles.permissionPrimaryText}>
|
|
1021
|
+
{permissionAction.isPending ? "Working…" : "Allow"}
|
|
1022
|
+
</Text>
|
|
1023
|
+
</Pressable>
|
|
1024
|
+
</View>
|
|
1025
|
+
</>
|
|
1026
|
+
) : null}
|
|
1027
|
+
</Modal.Content>
|
|
1028
|
+
</Modal>
|
|
1029
|
+
</View>
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const styles = StyleSheet.create({
|
|
1034
|
+
actionButton: {
|
|
1035
|
+
width: 30,
|
|
1036
|
+
height: 30,
|
|
1037
|
+
alignItems: "center",
|
|
1038
|
+
justifyContent: "center",
|
|
1039
|
+
borderRadius: 7,
|
|
1040
|
+
},
|
|
1041
|
+
pressed: { opacity: 0.58 },
|
|
1042
|
+
disabled: { opacity: 0.38 },
|
|
1043
|
+
});
|