@omercnet/paseo-agent-crew 1.1.0 → 1.2.0-next.214.1
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 +19 -5
- package/client/crew.ts +70 -1
- package/client/main.tsx +114 -40
- package/client/sidebar.tsx +90 -0
- package/index.client.tsx +8 -0
- package/package.json +5 -5
- package/paseo-plugin.json +1 -1
package/README.md
CHANGED
|
@@ -56,8 +56,10 @@ Each row exposes the operations available through the public Paseo SDK:
|
|
|
56
56
|
- **Open** navigates to the selected agent.
|
|
57
57
|
- **Expand or collapse** shows or hides a managed branch without changing agent state.
|
|
58
58
|
- **Nudge** sends a follow-up to an idle or waiting agent.
|
|
59
|
-
- **Interrupt and redirect** sends a new direction to a running agent.
|
|
60
|
-
|
|
59
|
+
- **Interrupt and redirect** sends a new direction to a running agent. The dialog defaults to
|
|
60
|
+
**Interrupt**, which stops the active turn before starting that direction. **Prefer steering**
|
|
61
|
+
asks the provider to deliver the message into the running turn instead; a provider that cannot
|
|
62
|
+
steer replaces the turn. The choice resets to **Interrupt** each time the dialog opens.
|
|
61
63
|
- **Detach** removes a managed child branch from its parent while allowing it to continue
|
|
62
64
|
independently. Root agents and context-only ancestors do not expose this action.
|
|
63
65
|
- **Archive** stops and removes the selected agent. Same-workspace descendants may be archived with
|
|
@@ -69,6 +71,18 @@ Each row exposes the operations available through the public Paseo SDK:
|
|
|
69
71
|
Every mutating control uses a confirmation dialog. Successful actions show a toast; SDK errors are
|
|
70
72
|
shown without hiding the failure.
|
|
71
73
|
|
|
74
|
+
## Active crews sidebar row
|
|
75
|
+
|
|
76
|
+
On Paseo 0.11 and later, an **Active crews** sidebar header row counts the crews that have a member
|
|
77
|
+
working or waiting for input. Pressing it lists those crews with their workspace; choosing one opens
|
|
78
|
+
that workspace's Agent Crew panel in the Explorer. The list is a popover on wide layouts and a
|
|
79
|
+
bottom sheet on compact ones. Paseo's compact layout does not show Explorer panels, so there the
|
|
80
|
+
jump lands on the workspace. Hide the row from Settings > Sidebar.
|
|
81
|
+
|
|
82
|
+
The row reads the same directory query and subscriptions as the panel, so they stay active while
|
|
83
|
+
the sidebar is mounted. Paseo 0.9 and 0.10 do not support sidebar items; the row is not registered
|
|
84
|
+
there.
|
|
85
|
+
|
|
72
86
|
## How it reads state
|
|
73
87
|
|
|
74
88
|
`usePaseo().agents.list()` and `usePaseo().workspaces.list()` page the selected daemon directory at
|
|
@@ -99,8 +113,8 @@ Agent Crew intentionally stays inside the public Paseo plugin SDK.
|
|
|
99
113
|
- Workspace-less top-level agents are outside every workspace crew; workspace-less descendants of
|
|
100
114
|
visible members remain in that crew.
|
|
101
115
|
- It does not add heartbeat controls, private cancellation calls, or direct daemon API access.
|
|
102
|
-
-
|
|
103
|
-
|
|
116
|
+
- Steering is a preference. Paseo does not report whether a message steered the running turn or
|
|
117
|
+
replaced it, so the toast only says the message was sent.
|
|
104
118
|
- If the daemon contains more than 2,000 agents, the panel warns that its directory snapshot may
|
|
105
119
|
omit agents in this workspace or their descendants.
|
|
106
120
|
|
|
@@ -133,7 +147,7 @@ bun run test:coverage
|
|
|
133
147
|
bun run typecheck
|
|
134
148
|
```
|
|
135
149
|
|
|
136
|
-
The manifest supports Paseo `^0.9.0 || ^0.10.0`. Development pins the stable 0.9 SDK line; nightly compatibility checks run the OMP provider against both the current stable and beta SDK channels. React 19.1 and React Native 0.81 match the host.
|
|
150
|
+
The manifest supports Paseo `^0.9.0 || ^0.10.0 || ^0.11.0`. Development pins the stable 0.9 SDK line; nightly compatibility checks run the OMP provider against both the current stable and beta SDK channels. React 19.1 and React Native 0.81 match the host.
|
|
137
151
|
|
|
138
152
|
Release Please maintains versions, changelog entries, component tags, and GitHub releases from
|
|
139
153
|
Conventional Commits in the monorepo.
|
package/client/crew.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
PluginClientContext,
|
|
3
|
+
PluginSidebarItemContribution,
|
|
4
|
+
usePaseo,
|
|
5
|
+
} from "@getpaseo/plugin/client";
|
|
2
6
|
|
|
3
7
|
export type PaseoApi = ReturnType<typeof usePaseo>;
|
|
4
8
|
export type PaseoWorkspace = Awaited<ReturnType<PaseoApi["workspaces"]["list"]>>["entries"][number];
|
|
5
9
|
export type AgentEntry = Awaited<ReturnType<PaseoApi["agents"]["list"]>>["entries"][number];
|
|
6
10
|
type AgentSnapshot = AgentEntry["agent"];
|
|
7
11
|
|
|
12
|
+
/** Sidebar header items arrived in Paseo 0.11; on older hosts the item is not registered. */
|
|
13
|
+
export function addSidebarHeaderItemIfSupported(
|
|
14
|
+
client: Pick<PluginClientContext, "addSidebarHeaderItem">,
|
|
15
|
+
item: PluginSidebarItemContribution,
|
|
16
|
+
): () => void {
|
|
17
|
+
if (typeof client.addSidebarHeaderItem !== "function") return () => {};
|
|
18
|
+
return client.addSidebarHeaderItem(item);
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
export function listenToCrewDirectory(paseo: PaseoApi, invalidate: () => void): () => void {
|
|
9
22
|
let closed = false;
|
|
10
23
|
let removeAgentObserver: (() => void) | undefined;
|
|
@@ -112,6 +125,13 @@ export function crewState(agent: AgentSnapshot): CrewState {
|
|
|
112
125
|
return "idle";
|
|
113
126
|
}
|
|
114
127
|
|
|
128
|
+
export type DeliveryChoice = "steer" | "interrupt";
|
|
129
|
+
|
|
130
|
+
/** Idle agents get a plain send; the choice only applies to a running turn. */
|
|
131
|
+
export function sendOptions(running: boolean, choice: DeliveryChoice) {
|
|
132
|
+
return running ? { activeTurnBehavior: choice } : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
115
135
|
export function isWorking(agent: AgentSnapshot): boolean {
|
|
116
136
|
return agent.status === "running" || agent.status === "initializing";
|
|
117
137
|
}
|
|
@@ -309,3 +329,52 @@ export function collapseCrewNodes(
|
|
|
309
329
|
}
|
|
310
330
|
return visible;
|
|
311
331
|
}
|
|
332
|
+
|
|
333
|
+
export interface ActiveCrew {
|
|
334
|
+
workspaceId: string;
|
|
335
|
+
lead: AgentEntry;
|
|
336
|
+
working: number;
|
|
337
|
+
needsInput: number;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Crews each workspace's panel shows, kept only when a member is working or needs input. */
|
|
341
|
+
export function activeCrews(entries: readonly AgentEntry[]): ActiveCrew[] {
|
|
342
|
+
const workspaceIds = new Set<string>();
|
|
343
|
+
for (const { agent } of entries) {
|
|
344
|
+
if (agent.workspaceId && !agent.archivedAt) workspaceIds.add(agent.workspaceId);
|
|
345
|
+
}
|
|
346
|
+
const crews: ActiveCrew[] = [];
|
|
347
|
+
for (const workspaceId of workspaceIds) {
|
|
348
|
+
let crew: ActiveCrew | undefined;
|
|
349
|
+
for (const node of buildCrewForest(entries, workspaceId)) {
|
|
350
|
+
if (node.depth === 0) {
|
|
351
|
+
crew =
|
|
352
|
+
node.descendantCount > 0
|
|
353
|
+
? { workspaceId, lead: node.entry, working: 0, needsInput: 0 }
|
|
354
|
+
: undefined;
|
|
355
|
+
if (crew) crews.push(crew);
|
|
356
|
+
}
|
|
357
|
+
if (!crew || !node.member) continue;
|
|
358
|
+
const state = crewState(node.entry.agent);
|
|
359
|
+
if (state === "working") crew.working += 1;
|
|
360
|
+
if (state === "needs-input") crew.needsInput += 1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return crews.filter((crew) => crew.working + crew.needsInput > 0);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export type CrewPopoverNotice = "loading" | "error" | "empty";
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The note the Active crews popover shows, or null when its crew list speaks for itself. A failed
|
|
370
|
+
* load always wins, even over cached crews, so an outage never reads as "no crew is active".
|
|
371
|
+
*/
|
|
372
|
+
export function crewPopoverNotice(input: {
|
|
373
|
+
crewCount: number;
|
|
374
|
+
isPending: boolean;
|
|
375
|
+
error: unknown;
|
|
376
|
+
}): CrewPopoverNotice | null {
|
|
377
|
+
if (input.error) return "error";
|
|
378
|
+
if (input.crewCount > 0) return null;
|
|
379
|
+
return input.isPending ? "loading" : "empty";
|
|
380
|
+
}
|
package/client/main.tsx
CHANGED
|
@@ -22,12 +22,14 @@ import {
|
|
|
22
22
|
collapseCrewNodes,
|
|
23
23
|
crewCounts,
|
|
24
24
|
crewState,
|
|
25
|
+
type DeliveryChoice,
|
|
25
26
|
formatAge,
|
|
26
27
|
isWorking,
|
|
27
28
|
listenToCrewDirectory,
|
|
28
29
|
type PaseoApi,
|
|
29
30
|
type PaseoWorkspace,
|
|
30
31
|
parentAgentId,
|
|
32
|
+
sendOptions,
|
|
31
33
|
} from "./crew";
|
|
32
34
|
|
|
33
35
|
const PAGE_LIMIT = 200;
|
|
@@ -43,7 +45,7 @@ type CrewData = {
|
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
type CrewAction =
|
|
46
|
-
| { kind: "send"; agentId: string; text: string;
|
|
48
|
+
| { kind: "send"; agentId: string; text: string; running: boolean; choice: DeliveryChoice }
|
|
47
49
|
| { kind: "detach"; agentId: string }
|
|
48
50
|
| { kind: "archive"; agentId: string };
|
|
49
51
|
|
|
@@ -96,6 +98,36 @@ async function loadCrewData(paseo: PaseoApi): Promise<CrewData> {
|
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/** Host-wide crew directory, refreshed by agent and workspace subscriptions plus a slow backstop. */
|
|
102
|
+
export function useCrewDirectory(hostId: string) {
|
|
103
|
+
const paseo = usePaseo();
|
|
104
|
+
const queryClient = useQueryClient();
|
|
105
|
+
const queryKey = useMemo(() => ["agent-crew", "directory", hostId], [hostId]);
|
|
106
|
+
const directory = useQuery({
|
|
107
|
+
queryKey,
|
|
108
|
+
queryFn: () => loadCrewData(paseo),
|
|
109
|
+
refetchInterval: BACKSTOP_REFRESH_MS,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
114
|
+
const invalidate = () => {
|
|
115
|
+
if (debounce) return;
|
|
116
|
+
debounce = setTimeout(() => {
|
|
117
|
+
debounce = undefined;
|
|
118
|
+
void queryClient.invalidateQueries({ queryKey });
|
|
119
|
+
}, REFRESH_DEBOUNCE_MS);
|
|
120
|
+
};
|
|
121
|
+
const unsubscribeDirectory = listenToCrewDirectory(paseo, invalidate);
|
|
122
|
+
return () => {
|
|
123
|
+
clearTimeout(debounce);
|
|
124
|
+
unsubscribeDirectory();
|
|
125
|
+
};
|
|
126
|
+
}, [paseo, queryClient, queryKey]);
|
|
127
|
+
|
|
128
|
+
return directory;
|
|
129
|
+
}
|
|
130
|
+
|
|
99
131
|
function ActionButton({
|
|
100
132
|
accessibilityLabel,
|
|
101
133
|
color,
|
|
@@ -186,14 +218,8 @@ export function AgentCrew({
|
|
|
186
218
|
}: PluginWorkspacePanelProps) {
|
|
187
219
|
const paseo = usePaseo();
|
|
188
220
|
const toast = useToast();
|
|
189
|
-
const queryClient = useQueryClient();
|
|
190
221
|
const workspaceTitle = useWorkspace(workspaceId, ({ name, title }) => title?.trim() || name);
|
|
191
|
-
const
|
|
192
|
-
const { data, error, isPending, isFetching, refetch } = useQuery({
|
|
193
|
-
queryKey,
|
|
194
|
-
queryFn: () => loadCrewData(paseo),
|
|
195
|
-
refetchInterval: BACKSTOP_REFRESH_MS,
|
|
196
|
-
});
|
|
222
|
+
const { data, error, isPending, isFetching, refetch } = useCrewDirectory(host.id);
|
|
197
223
|
|
|
198
224
|
const [selectedState, setSelectedState] = useState<CrewState | null>(null);
|
|
199
225
|
const [query, setQuery] = useState("");
|
|
@@ -201,6 +227,7 @@ export function AgentCrew({
|
|
|
201
227
|
const [dialog, setDialog] = useState<DialogState>(null);
|
|
202
228
|
const [permissionDialog, setPermissionDialog] = useState<PermissionDialogState>(null);
|
|
203
229
|
const [message, setMessage] = useState("");
|
|
230
|
+
const [delivery, setDelivery] = useState<DeliveryChoice>("interrupt");
|
|
204
231
|
const [collapsedAgentIds, setCollapsedAgentIds] = useState<ReadonlySet<string>>(() => new Set());
|
|
205
232
|
|
|
206
233
|
useEffect(() => {
|
|
@@ -208,22 +235,6 @@ export function AgentCrew({
|
|
|
208
235
|
return () => clearInterval(clock);
|
|
209
236
|
}, []);
|
|
210
237
|
|
|
211
|
-
useEffect(() => {
|
|
212
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
213
|
-
const invalidate = () => {
|
|
214
|
-
if (debounce) return;
|
|
215
|
-
debounce = setTimeout(() => {
|
|
216
|
-
debounce = undefined;
|
|
217
|
-
void queryClient.invalidateQueries({ queryKey });
|
|
218
|
-
}, REFRESH_DEBOUNCE_MS);
|
|
219
|
-
};
|
|
220
|
-
const unsubscribeDirectory = listenToCrewDirectory(paseo, invalidate);
|
|
221
|
-
return () => {
|
|
222
|
-
clearTimeout(debounce);
|
|
223
|
-
unsubscribeDirectory();
|
|
224
|
-
};
|
|
225
|
-
}, [paseo, queryClient, queryKey]);
|
|
226
|
-
|
|
227
238
|
const allNodes = useMemo(
|
|
228
239
|
() =>
|
|
229
240
|
buildCrewForest(data?.entries ?? [], workspaceId, {
|
|
@@ -265,7 +276,7 @@ export function AgentCrew({
|
|
|
265
276
|
mutationFn: async (input: CrewAction) => {
|
|
266
277
|
const handle = paseo.agents.ref(input.agentId);
|
|
267
278
|
if (input.kind === "send") {
|
|
268
|
-
await handle.send(input.text);
|
|
279
|
+
await handle.send(input.text, sendOptions(input.running, input.choice));
|
|
269
280
|
} else if (input.kind === "detach") {
|
|
270
281
|
await handle.detach();
|
|
271
282
|
} else {
|
|
@@ -274,7 +285,12 @@ export function AgentCrew({
|
|
|
274
285
|
},
|
|
275
286
|
onSuccess: (_result, input) => {
|
|
276
287
|
if (input.kind === "send") {
|
|
277
|
-
|
|
288
|
+
const sent = !input.running
|
|
289
|
+
? "Nudge sent"
|
|
290
|
+
: input.choice === "interrupt"
|
|
291
|
+
? "Agent redirected"
|
|
292
|
+
: "Message sent";
|
|
293
|
+
toast.show(sent, { variant: "success" });
|
|
278
294
|
} else if (input.kind === "detach") {
|
|
279
295
|
toast.show("Subagent detached", { variant: "success" });
|
|
280
296
|
} else {
|
|
@@ -287,7 +303,7 @@ export function AgentCrew({
|
|
|
287
303
|
toast.error(mutationError instanceof Error ? mutationError.message : "Agent action failed");
|
|
288
304
|
},
|
|
289
305
|
onSettled: async () => {
|
|
290
|
-
await
|
|
306
|
+
await refetch();
|
|
291
307
|
},
|
|
292
308
|
});
|
|
293
309
|
|
|
@@ -315,7 +331,7 @@ export function AgentCrew({
|
|
|
315
331
|
}
|
|
316
332
|
},
|
|
317
333
|
onSettled: async () => {
|
|
318
|
-
await
|
|
334
|
+
await refetch();
|
|
319
335
|
},
|
|
320
336
|
});
|
|
321
337
|
|
|
@@ -467,6 +483,18 @@ export function AgentCrew({
|
|
|
467
483
|
backgroundColor: theme.colors.surface1,
|
|
468
484
|
textAlignVertical: "top",
|
|
469
485
|
},
|
|
486
|
+
deliveryRow: { flexDirection: "row", gap: 8 },
|
|
487
|
+
deliveryOption: {
|
|
488
|
+
flex: 1,
|
|
489
|
+
minHeight: 36,
|
|
490
|
+
alignItems: "center",
|
|
491
|
+
justifyContent: "center",
|
|
492
|
+
borderRadius: 8,
|
|
493
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
494
|
+
borderColor: theme.colors.border,
|
|
495
|
+
backgroundColor: theme.colors.surface1,
|
|
496
|
+
},
|
|
497
|
+
deliveryOptionActive: { backgroundColor: theme.colors.accent },
|
|
470
498
|
modalActions: { flexDirection: "row", justifyContent: "flex-end", gap: 8 },
|
|
471
499
|
secondaryButton: {
|
|
472
500
|
minHeight: 36,
|
|
@@ -528,6 +556,7 @@ export function AgentCrew({
|
|
|
528
556
|
);
|
|
529
557
|
function openDialog(kind: NonNullable<DialogState>["kind"], node: CrewNode) {
|
|
530
558
|
setMessage("");
|
|
559
|
+
setDelivery("interrupt");
|
|
531
560
|
setDialog({ kind, node });
|
|
532
561
|
}
|
|
533
562
|
|
|
@@ -537,7 +566,13 @@ export function AgentCrew({
|
|
|
537
566
|
if (dialog.kind === "message") {
|
|
538
567
|
const text = message.trim();
|
|
539
568
|
if (!text) return;
|
|
540
|
-
action.mutate({
|
|
569
|
+
action.mutate({
|
|
570
|
+
kind: "send",
|
|
571
|
+
agentId: agent.id,
|
|
572
|
+
text,
|
|
573
|
+
running: isWorking(agent),
|
|
574
|
+
choice: delivery,
|
|
575
|
+
});
|
|
541
576
|
return;
|
|
542
577
|
}
|
|
543
578
|
action.mutate({ kind: dialog.kind, agentId: agent.id });
|
|
@@ -714,17 +749,26 @@ export function AgentCrew({
|
|
|
714
749
|
if (dialog.kind === "message") {
|
|
715
750
|
const state = crewState(dialog.node.entry.agent);
|
|
716
751
|
const running = isWorking(dialog.node.entry.agent);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
?
|
|
720
|
-
:
|
|
721
|
-
?
|
|
722
|
-
:
|
|
723
|
-
|
|
724
|
-
? "
|
|
725
|
-
:
|
|
726
|
-
? "
|
|
727
|
-
:
|
|
752
|
+
const steer = running && delivery === "steer";
|
|
753
|
+
dialogTitle = !running
|
|
754
|
+
? `Nudge ${target}`
|
|
755
|
+
: steer
|
|
756
|
+
? `Message ${target}`
|
|
757
|
+
: `Interrupt and redirect ${target}`;
|
|
758
|
+
dialogCopy = steer
|
|
759
|
+
? "This agent is working. The message is delivered to the running turn if the provider supports steering; otherwise it replaces the turn."
|
|
760
|
+
: running
|
|
761
|
+
? "This agent is working. Sending a message stops its current turn and starts the new direction."
|
|
762
|
+
: state === "needs-input"
|
|
763
|
+
? "This agent is waiting for a permission decision. Sending a message dismisses that request and starts the new direction."
|
|
764
|
+
: "Send a concise follow-up with the missing context or next step.";
|
|
765
|
+
confirmLabel = steer
|
|
766
|
+
? "Send (prefer steering)"
|
|
767
|
+
: running
|
|
768
|
+
? "Interrupt & redirect"
|
|
769
|
+
: state === "needs-input"
|
|
770
|
+
? "Dismiss request & nudge"
|
|
771
|
+
: "Send nudge";
|
|
728
772
|
} else if (dialog.kind === "detach") {
|
|
729
773
|
dialogTitle = `Detach ${target}?`;
|
|
730
774
|
dialogCopy =
|
|
@@ -895,6 +939,36 @@ export function AgentCrew({
|
|
|
895
939
|
<Modal.Content>
|
|
896
940
|
<View style={panelStyles.modalBody}>
|
|
897
941
|
<Text style={panelStyles.modalCopy}>{dialogCopy}</Text>
|
|
942
|
+
{dialog?.kind === "message" && isWorking(dialog.node.entry.agent) ? (
|
|
943
|
+
<View style={panelStyles.deliveryRow}>
|
|
944
|
+
{(
|
|
945
|
+
[
|
|
946
|
+
["interrupt", "Interrupt"],
|
|
947
|
+
["steer", "Prefer steering"],
|
|
948
|
+
] as const
|
|
949
|
+
).map(([value, label]) => (
|
|
950
|
+
<Pressable
|
|
951
|
+
key={value}
|
|
952
|
+
accessibilityRole="button"
|
|
953
|
+
accessibilityState={{ selected: delivery === value }}
|
|
954
|
+
disabled={action.isPending}
|
|
955
|
+
onPress={() => setDelivery(value)}
|
|
956
|
+
style={[
|
|
957
|
+
panelStyles.deliveryOption,
|
|
958
|
+
delivery === value && panelStyles.deliveryOptionActive,
|
|
959
|
+
]}
|
|
960
|
+
>
|
|
961
|
+
<Text
|
|
962
|
+
style={
|
|
963
|
+
delivery === value ? panelStyles.primaryButtonText : panelStyles.buttonText
|
|
964
|
+
}
|
|
965
|
+
>
|
|
966
|
+
{label}
|
|
967
|
+
</Text>
|
|
968
|
+
</Pressable>
|
|
969
|
+
))}
|
|
970
|
+
</View>
|
|
971
|
+
) : null}
|
|
898
972
|
{dialog?.kind === "message" ? (
|
|
899
973
|
<TextInput
|
|
900
974
|
accessibilityLabel="Message to subagent"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PluginClientContext,
|
|
3
|
+
PluginPopoverProps,
|
|
4
|
+
PluginSidebarItemProps,
|
|
5
|
+
} from "@getpaseo/plugin/client";
|
|
6
|
+
import { useToast } from "@getpaseo/plugin/client/react-native";
|
|
7
|
+
import { SidebarRow } from "@getpaseo/plugin/client/ui";
|
|
8
|
+
import { useMemo } from "react";
|
|
9
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
10
|
+
import { activeCrews, agentTitle, crewPopoverNotice } from "./crew";
|
|
11
|
+
import { useCrewDirectory } from "./main";
|
|
12
|
+
|
|
13
|
+
/** The Active crews sidebar row; its popover opens a crew's workspace Agent Crew panel. */
|
|
14
|
+
export function createActiveCrewsItem(client: PluginClientContext) {
|
|
15
|
+
function ActiveCrewsPopover({ theme, host, close }: PluginPopoverProps) {
|
|
16
|
+
const toast = useToast();
|
|
17
|
+
const { data, error, isPending } = useCrewDirectory(host.id);
|
|
18
|
+
const crews = useMemo(() => activeCrews(data?.entries ?? []), [data]);
|
|
19
|
+
const styles = useMemo(
|
|
20
|
+
() =>
|
|
21
|
+
StyleSheet.create({
|
|
22
|
+
row: { gap: 2, paddingHorizontal: 8, paddingVertical: 8, borderRadius: 8 },
|
|
23
|
+
pressed: { backgroundColor: theme.colors.surface2 },
|
|
24
|
+
title: { color: theme.colors.foreground, fontSize: 13, fontWeight: "600" },
|
|
25
|
+
detail: { color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
26
|
+
error: { color: theme.colors.statusDanger, fontSize: 12, marginBottom: 6 },
|
|
27
|
+
}),
|
|
28
|
+
[theme],
|
|
29
|
+
);
|
|
30
|
+
const notice = crewPopoverNotice({ crewCount: crews.length, isPending, error });
|
|
31
|
+
let noticeText = "No crew is working or waiting for input.";
|
|
32
|
+
if (notice === "loading") noticeText = "Loading crews…";
|
|
33
|
+
if (notice === "error") {
|
|
34
|
+
noticeText = error instanceof Error ? error.message : "Could not load crews";
|
|
35
|
+
}
|
|
36
|
+
return (
|
|
37
|
+
<View>
|
|
38
|
+
{notice ? (
|
|
39
|
+
<Text style={notice === "error" ? styles.error : styles.detail}>{noticeText}</Text>
|
|
40
|
+
) : null}
|
|
41
|
+
{crews.map((crew) => (
|
|
42
|
+
<Pressable
|
|
43
|
+
key={`${crew.workspaceId}:${crew.lead.agent.id}`}
|
|
44
|
+
accessibilityRole="button"
|
|
45
|
+
onPress={() => {
|
|
46
|
+
close();
|
|
47
|
+
try {
|
|
48
|
+
client.openPanel("crew", { workspaceId: crew.workspaceId, location: "explorer" });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
toast.error(error instanceof Error ? error.message : "Could not open Agent Crew");
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
53
|
+
style={({ pressed }) => [styles.row, pressed && styles.pressed]}
|
|
54
|
+
>
|
|
55
|
+
<Text style={styles.title} numberOfLines={1}>
|
|
56
|
+
{agentTitle(crew.lead)}
|
|
57
|
+
</Text>
|
|
58
|
+
<Text style={styles.detail} numberOfLines={1}>
|
|
59
|
+
{[
|
|
60
|
+
data?.workspaceNames.get(crew.workspaceId) ?? "Unknown workspace",
|
|
61
|
+
crew.working > 0 ? `${crew.working} working` : null,
|
|
62
|
+
crew.needsInput > 0 ? `${crew.needsInput} need input` : null,
|
|
63
|
+
]
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.join(" · ")}
|
|
66
|
+
</Text>
|
|
67
|
+
</Pressable>
|
|
68
|
+
))}
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ActiveCrewsItem({ theme, host, openPopover }: PluginSidebarItemProps) {
|
|
74
|
+
const { data } = useCrewDirectory(host.id);
|
|
75
|
+
const count = useMemo(() => activeCrews(data?.entries ?? []).length, [data]);
|
|
76
|
+
return (
|
|
77
|
+
<SidebarRow
|
|
78
|
+
icon="Network"
|
|
79
|
+
onPress={() => openPopover(ActiveCrewsPopover)}
|
|
80
|
+
trailing={
|
|
81
|
+
count > 0 ? (
|
|
82
|
+
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{count}</Text>
|
|
83
|
+
) : undefined
|
|
84
|
+
}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return ActiveCrewsItem;
|
|
90
|
+
}
|
package/index.client.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { PluginClientContext, PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
2
2
|
import { createAutoOpenManager } from "./client/auto-open";
|
|
3
|
+
import { addSidebarHeaderItemIfSupported } from "./client/crew";
|
|
3
4
|
import { AgentCrew } from "./client/main";
|
|
4
5
|
import { AgentCrewSettingsScreen } from "./client/settings-screen";
|
|
6
|
+
import { createActiveCrewsItem } from "./client/sidebar";
|
|
5
7
|
import { agentCrewSettings } from "./shared/settings";
|
|
6
8
|
|
|
7
9
|
export default function contribute(client: PluginClientContext) {
|
|
@@ -35,8 +37,14 @@ export default function contribute(client: PluginClientContext) {
|
|
|
35
37
|
openPanel("crew", { location: "explorer" });
|
|
36
38
|
},
|
|
37
39
|
});
|
|
40
|
+
const removeActiveCrews = addSidebarHeaderItemIfSupported(client, {
|
|
41
|
+
id: "active-crews",
|
|
42
|
+
title: "Active crews",
|
|
43
|
+
Component: createActiveCrewsItem(client),
|
|
44
|
+
});
|
|
38
45
|
|
|
39
46
|
return () => {
|
|
47
|
+
removeActiveCrews();
|
|
40
48
|
removeOpenCrew();
|
|
41
49
|
removeWorkspacePanel();
|
|
42
50
|
removeSettings();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-agent-crew",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-next.214.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A Paseo Explorer panel for visualizing and controlling every managed agent crew in a workspace.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@biomejs/biome": "^2.5.10",
|
|
50
|
-
"@getpaseo/cli": "0.
|
|
51
|
-
"@getpaseo/client": "0.
|
|
52
|
-
"@getpaseo/plugin": "0.
|
|
53
|
-
"@getpaseo/protocol": "0.
|
|
50
|
+
"@getpaseo/cli": "0.11.0-beta.3",
|
|
51
|
+
"@getpaseo/client": "0.11.0-beta.3",
|
|
52
|
+
"@getpaseo/plugin": "0.11.0-beta.3",
|
|
53
|
+
"@getpaseo/protocol": "0.11.0-beta.3",
|
|
54
54
|
"@tanstack/react-query": "^5.102.3",
|
|
55
55
|
"@types/node": "^24.10.1",
|
|
56
56
|
"@types/react": "^19.2.18",
|
package/paseo-plugin.json
CHANGED