@agent-native/dispatch 0.13.8 → 0.13.10
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 +30 -24
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +8 -389
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +39 -0
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +12 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/components/dispatch-control-plane.tsx +37 -1129
- package/src/routes/pages/$appId.tsx +6 -6
- package/src/server/lib/dispatch-integrations.spec.ts +92 -1
- package/src/server/lib/dispatch-integrations.ts +48 -0
- package/src/server/plugins/integrations.ts +13 -1
|
@@ -1,336 +1,63 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PromptComposer,
|
|
3
|
-
agentNativePath,
|
|
4
3
|
isInBuilderFrame,
|
|
5
4
|
useActionQuery,
|
|
6
|
-
useChangeVersions,
|
|
7
5
|
useChatModels,
|
|
8
|
-
useChatThreads,
|
|
9
|
-
type ChatThreadSummary,
|
|
10
6
|
} from "@agent-native/core/client";
|
|
11
|
-
import { RunsTray } from "@agent-native/core/client/progress";
|
|
12
7
|
import {
|
|
13
|
-
IconActivity,
|
|
14
|
-
IconAlertTriangle,
|
|
15
8
|
IconArrowUpRight,
|
|
16
|
-
IconBolt,
|
|
17
9
|
IconBroadcast,
|
|
18
|
-
IconCheck,
|
|
19
|
-
IconClockHour4,
|
|
20
|
-
IconListCheck,
|
|
21
|
-
IconMessages,
|
|
22
|
-
IconPlayerPlay,
|
|
23
|
-
IconPlugConnected,
|
|
24
|
-
IconPlus,
|
|
25
|
-
IconRobot,
|
|
26
|
-
IconRocket,
|
|
27
|
-
IconSettingsAutomation,
|
|
28
|
-
IconShieldCheck,
|
|
29
10
|
IconStack3,
|
|
30
|
-
IconUsersGroup,
|
|
31
11
|
type IconProps,
|
|
32
12
|
} from "@tabler/icons-react";
|
|
33
|
-
import {
|
|
34
|
-
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
13
|
+
import type { ReactNode } from "react";
|
|
35
14
|
import { Link, useNavigate } from "react-router";
|
|
36
|
-
import { toast } from "sonner";
|
|
37
15
|
|
|
38
|
-
import type { ConnectedAgent } from "@/components/agents-panel";
|
|
39
16
|
import { CreateAppPopover } from "@/components/create-app-popover";
|
|
40
17
|
import { DispatchShell } from "@/components/dispatch-shell";
|
|
41
|
-
import { Badge } from "@/components/ui/badge";
|
|
42
18
|
import { Button } from "@/components/ui/button";
|
|
43
19
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
44
|
-
import { Switch } from "@/components/ui/switch";
|
|
45
20
|
import { WorkspaceAppCard } from "@/components/workspace-app-card";
|
|
46
|
-
import {
|
|
47
|
-
listDispatchAutomations,
|
|
48
|
-
setDispatchAutomationEnabled,
|
|
49
|
-
type DispatchAutomationItem,
|
|
50
|
-
type SetDispatchAutomationEnabledInput,
|
|
51
|
-
} from "@/lib/automations";
|
|
52
21
|
import { submitOverviewPrompt } from "@/lib/overview-chat";
|
|
53
|
-
import { cn } from "@/lib/utils";
|
|
54
22
|
import type { WorkspaceAppSummary } from "@/lib/workspace-apps";
|
|
55
23
|
|
|
56
|
-
interface DispatchOverview {
|
|
57
|
-
counts?: {
|
|
58
|
-
destinations?: number;
|
|
59
|
-
pendingApprovals?: number;
|
|
60
|
-
linkedIdentities?: number;
|
|
61
|
-
activeTokens?: number;
|
|
62
|
-
};
|
|
63
|
-
recentApprovals?: Array<{
|
|
64
|
-
id: string;
|
|
65
|
-
summary: string;
|
|
66
|
-
status: string;
|
|
67
|
-
requestedBy?: string;
|
|
68
|
-
}>;
|
|
69
|
-
recentAudit?: RecentAuditEvent[];
|
|
70
|
-
settings?: {
|
|
71
|
-
enabled?: boolean;
|
|
72
|
-
};
|
|
73
|
-
vault?: {
|
|
74
|
-
secretCount?: number;
|
|
75
|
-
activeGrantCount?: number;
|
|
76
|
-
accessMode?: string;
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface RecentAuditEvent {
|
|
81
|
-
id: string;
|
|
82
|
-
summary: string;
|
|
83
|
-
actor: string;
|
|
84
|
-
action?: string;
|
|
85
|
-
createdAt: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface TaskQueueRecentFailure {
|
|
89
|
-
id: string;
|
|
90
|
-
platform: string;
|
|
91
|
-
error: string;
|
|
92
|
-
attempts: number;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface TaskQueueStats {
|
|
96
|
-
pending: number;
|
|
97
|
-
processing: number;
|
|
98
|
-
completed_last_hour: number;
|
|
99
|
-
failed_last_hour: number;
|
|
100
|
-
oldest_pending_age_seconds: number;
|
|
101
|
-
recent_failures: TaskQueueRecentFailure[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
type AutomationItem = DispatchAutomationItem;
|
|
105
|
-
|
|
106
|
-
const ZERO_TASK_QUEUE_STATS: TaskQueueStats = {
|
|
107
|
-
pending: 0,
|
|
108
|
-
processing: 0,
|
|
109
|
-
completed_last_hour: 0,
|
|
110
|
-
failed_last_hour: 0,
|
|
111
|
-
oldest_pending_age_seconds: 0,
|
|
112
|
-
recent_failures: [],
|
|
113
|
-
};
|
|
114
|
-
|
|
115
24
|
const PROMPT_SUGGESTIONS = [
|
|
116
25
|
"Summarize the current workspace health",
|
|
117
26
|
"Create an app for onboarding requests",
|
|
118
27
|
"Check which agents can help with analytics",
|
|
119
28
|
];
|
|
120
29
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
function formatNumber(value: number): string {
|
|
127
|
-
return new Intl.NumberFormat(undefined, {
|
|
128
|
-
notation: value >= 10_000 ? "compact" : "standard",
|
|
129
|
-
maximumFractionDigits: value >= 10_000 ? 1 : 0,
|
|
130
|
-
}).format(value);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function formatAgeSeconds(seconds: number): string {
|
|
134
|
-
if (!seconds || seconds < 0) return "0s";
|
|
135
|
-
if (seconds < 60) return `${Math.floor(seconds)}s`;
|
|
136
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
137
|
-
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function timeAgo(value: number | string | null | undefined): string {
|
|
141
|
-
if (value == null || value === "") return "never";
|
|
142
|
-
const timestamp =
|
|
143
|
-
typeof value === "number" ? value : new Date(value).getTime();
|
|
144
|
-
if (!Number.isFinite(timestamp)) return "never";
|
|
145
|
-
const diff = Math.max(0, Date.now() - timestamp);
|
|
146
|
-
if (diff < 60_000) return "now";
|
|
147
|
-
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`;
|
|
148
|
-
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`;
|
|
149
|
-
return `${Math.floor(diff / 86_400_000)}d`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function relativeRunTime(value: string | null | undefined): string {
|
|
153
|
-
if (!value) return "never";
|
|
154
|
-
const timestamp = new Date(value).getTime();
|
|
155
|
-
if (!Number.isFinite(timestamp)) return "never";
|
|
156
|
-
const diff = timestamp - Date.now();
|
|
157
|
-
const abs = Math.abs(diff);
|
|
158
|
-
const suffix = diff >= 0 ? "from now" : "ago";
|
|
159
|
-
if (abs < 60_000) return diff >= 0 ? "soon" : "now";
|
|
160
|
-
if (abs < 3_600_000) return `${Math.floor(abs / 60_000)}m ${suffix}`;
|
|
161
|
-
if (abs < 86_400_000) return `${Math.floor(abs / 3_600_000)}h ${suffix}`;
|
|
162
|
-
return `${Math.floor(abs / 86_400_000)}d ${suffix}`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function dateTimeTitle(value: string | null | undefined): string | undefined {
|
|
166
|
-
if (!value) return undefined;
|
|
167
|
-
const timestamp = new Date(value).getTime();
|
|
168
|
-
if (!Number.isFinite(timestamp)) return undefined;
|
|
169
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
170
|
-
dateStyle: "medium",
|
|
171
|
-
timeStyle: "short",
|
|
172
|
-
}).format(timestamp);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function threadUpdatedAt(thread: ChatThreadSummary): number {
|
|
176
|
-
return Number.isFinite(thread.updatedAt)
|
|
177
|
-
? thread.updatedAt
|
|
178
|
-
: Number.isFinite(thread.createdAt)
|
|
179
|
-
? thread.createdAt
|
|
180
|
-
: 0;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function threadTitle(thread: ChatThreadSummary): string {
|
|
184
|
-
return thread.title || thread.preview || "New chat";
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function automationTarget(item: AutomationItem): string {
|
|
188
|
-
if (item.triggerType === "event" && item.event) return item.event;
|
|
189
|
-
if (item.scheduleDescription) return item.scheduleDescription;
|
|
190
|
-
if (item.schedule) return item.schedule;
|
|
191
|
-
return item.triggerType || "schedule";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function automationLastRun(item: AutomationItem): string {
|
|
195
|
-
return item.lastRun ? relativeRunTime(item.lastRun) : "never";
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function automationNextRun(item: AutomationItem): string {
|
|
199
|
-
if (!item.enabled) return "paused";
|
|
200
|
-
if (item.triggerType === "event") return "on event";
|
|
201
|
-
return item.nextRun ? relativeRunTime(item.nextRun) : "not scheduled";
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function automationStatus(item: AutomationItem): {
|
|
205
|
-
label: string;
|
|
206
|
-
tone: "default" | "success" | "warning" | "danger" | "muted";
|
|
207
|
-
} {
|
|
208
|
-
if (!item.enabled) return { label: "Paused", tone: "muted" };
|
|
209
|
-
if (item.lastStatus === "error") return { label: "Error", tone: "danger" };
|
|
210
|
-
if (item.lastStatus === "running")
|
|
211
|
-
return { label: "Running", tone: "warning" };
|
|
212
|
-
if (item.lastStatus === "skipped")
|
|
213
|
-
return { label: "Skipped", tone: "warning" };
|
|
214
|
-
if (item.lastStatus === "success")
|
|
215
|
-
return { label: "Healthy", tone: "success" };
|
|
216
|
-
return { label: "Ready", tone: "default" };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function automationIdentity(
|
|
220
|
-
item: Pick<AutomationItem, "owner" | "path">,
|
|
221
|
-
): string {
|
|
222
|
-
return `${item.owner}:${item.path}`;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function StatusDot({
|
|
226
|
-
tone = "default",
|
|
30
|
+
function SectionHeader({
|
|
31
|
+
icon: Icon,
|
|
32
|
+
title,
|
|
33
|
+
detail,
|
|
34
|
+
action,
|
|
227
35
|
}: {
|
|
228
|
-
|
|
36
|
+
icon: React.ComponentType<IconProps>;
|
|
37
|
+
title: string;
|
|
38
|
+
detail?: string;
|
|
39
|
+
action?: ReactNode;
|
|
229
40
|
}) {
|
|
230
41
|
return (
|
|
231
|
-
<
|
|
232
|
-
className=
|
|
233
|
-
"
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
42
|
+
<div className="flex min-w-0 items-center justify-between gap-3">
|
|
43
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
44
|
+
<Icon size={16} className="shrink-0 text-muted-foreground" />
|
|
45
|
+
<div className="min-w-0">
|
|
46
|
+
<h2 className="truncate text-sm font-semibold text-foreground">
|
|
47
|
+
{title}
|
|
48
|
+
</h2>
|
|
49
|
+
{detail ? (
|
|
50
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
51
|
+
{detail}
|
|
52
|
+
</p>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
{action ? <div className="shrink-0">{action}</div> : null}
|
|
57
|
+
</div>
|
|
241
58
|
);
|
|
242
59
|
}
|
|
243
60
|
|
|
244
|
-
function useAutomationsStatus() {
|
|
245
|
-
const version = useChangeVersions(["action", "screen-refresh"]);
|
|
246
|
-
return useQuery<AutomationItem[]>({
|
|
247
|
-
queryKey: [...AUTOMATIONS_QUERY_KEY, version],
|
|
248
|
-
queryFn: listDispatchAutomations,
|
|
249
|
-
placeholderData: (prev) => prev,
|
|
250
|
-
refetchInterval: 15_000,
|
|
251
|
-
staleTime: 5_000,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function useToggleAutomation() {
|
|
256
|
-
const queryClient = useQueryClient();
|
|
257
|
-
|
|
258
|
-
return useMutation({
|
|
259
|
-
mutationFn: setDispatchAutomationEnabled,
|
|
260
|
-
onMutate: async (input: SetDispatchAutomationEnabledInput) => {
|
|
261
|
-
await queryClient.cancelQueries({ queryKey: AUTOMATIONS_QUERY_KEY });
|
|
262
|
-
const snapshots = queryClient.getQueriesData<AutomationItem[]>({
|
|
263
|
-
queryKey: AUTOMATIONS_QUERY_KEY,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
queryClient.setQueriesData<AutomationItem[]>(
|
|
267
|
-
{ queryKey: AUTOMATIONS_QUERY_KEY },
|
|
268
|
-
(rows) =>
|
|
269
|
-
rows?.map((item) =>
|
|
270
|
-
automationIdentity(item) === automationIdentity(input)
|
|
271
|
-
? { ...item, enabled: input.enabled }
|
|
272
|
-
: item,
|
|
273
|
-
),
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
return { snapshots };
|
|
277
|
-
},
|
|
278
|
-
onError: (err, _input, context) => {
|
|
279
|
-
for (const [queryKey, data] of context?.snapshots ?? []) {
|
|
280
|
-
queryClient.setQueryData(queryKey, data);
|
|
281
|
-
}
|
|
282
|
-
toast.error(
|
|
283
|
-
`Could not update automation: ${
|
|
284
|
-
err instanceof Error ? err.message : String(err)
|
|
285
|
-
}`,
|
|
286
|
-
);
|
|
287
|
-
},
|
|
288
|
-
onSuccess: (updated) => {
|
|
289
|
-
queryClient.setQueriesData<AutomationItem[]>(
|
|
290
|
-
{ queryKey: AUTOMATIONS_QUERY_KEY },
|
|
291
|
-
(rows) =>
|
|
292
|
-
rows?.map((item) =>
|
|
293
|
-
automationIdentity(item) === automationIdentity(updated)
|
|
294
|
-
? updated
|
|
295
|
-
: item,
|
|
296
|
-
),
|
|
297
|
-
);
|
|
298
|
-
},
|
|
299
|
-
onSettled: () => {
|
|
300
|
-
void queryClient.invalidateQueries({ queryKey: AUTOMATIONS_QUERY_KEY });
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function useTaskQueueStats() {
|
|
306
|
-
return useQuery<TaskQueueStats>({
|
|
307
|
-
queryKey: ["dispatch-control-plane", "task-queue"],
|
|
308
|
-
queryFn: async () => {
|
|
309
|
-
const res = await fetch(
|
|
310
|
-
agentNativePath("/_agent-native/integrations/task-queue/status"),
|
|
311
|
-
);
|
|
312
|
-
if (!res.ok) return ZERO_TASK_QUEUE_STATS;
|
|
313
|
-
const stats = await res.json();
|
|
314
|
-
if (!stats || typeof stats !== "object") return ZERO_TASK_QUEUE_STATS;
|
|
315
|
-
return {
|
|
316
|
-
pending: Number(stats.pending ?? 0),
|
|
317
|
-
processing: Number(stats.processing ?? 0),
|
|
318
|
-
completed_last_hour: Number(stats.completed_last_hour ?? 0),
|
|
319
|
-
failed_last_hour: Number(stats.failed_last_hour ?? 0),
|
|
320
|
-
oldest_pending_age_seconds: Number(
|
|
321
|
-
stats.oldest_pending_age_seconds ?? 0,
|
|
322
|
-
),
|
|
323
|
-
recent_failures: Array.isArray(stats.recent_failures)
|
|
324
|
-
? stats.recent_failures
|
|
325
|
-
: [],
|
|
326
|
-
};
|
|
327
|
-
},
|
|
328
|
-
placeholderData: (prev) => prev,
|
|
329
|
-
refetchInterval: 15_000,
|
|
330
|
-
staleTime: 5_000,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
61
|
function CommandPanel() {
|
|
335
62
|
const { selectedModel } = useChatModels();
|
|
336
63
|
const navigate = useNavigate();
|
|
@@ -391,371 +118,6 @@ function CommandPanel() {
|
|
|
391
118
|
);
|
|
392
119
|
}
|
|
393
120
|
|
|
394
|
-
function MetricCard({
|
|
395
|
-
label,
|
|
396
|
-
value,
|
|
397
|
-
detail,
|
|
398
|
-
icon: Icon,
|
|
399
|
-
to,
|
|
400
|
-
tone = "default",
|
|
401
|
-
}: {
|
|
402
|
-
label: string;
|
|
403
|
-
value: ReactNode;
|
|
404
|
-
detail: string;
|
|
405
|
-
icon: React.ComponentType<IconProps>;
|
|
406
|
-
to?: string;
|
|
407
|
-
tone?: "default" | "success" | "warning" | "danger";
|
|
408
|
-
}) {
|
|
409
|
-
const body = (
|
|
410
|
-
<>
|
|
411
|
-
<div className="flex items-center justify-between gap-3">
|
|
412
|
-
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
|
413
|
-
<Icon size={15} />
|
|
414
|
-
<span className="truncate">{label}</span>
|
|
415
|
-
</div>
|
|
416
|
-
<StatusDot
|
|
417
|
-
tone={
|
|
418
|
-
tone === "success"
|
|
419
|
-
? "success"
|
|
420
|
-
: tone === "warning"
|
|
421
|
-
? "warning"
|
|
422
|
-
: tone === "danger"
|
|
423
|
-
? "danger"
|
|
424
|
-
: "default"
|
|
425
|
-
}
|
|
426
|
-
/>
|
|
427
|
-
</div>
|
|
428
|
-
<div className="mt-3 text-2xl font-semibold tracking-tight text-foreground">
|
|
429
|
-
{value}
|
|
430
|
-
</div>
|
|
431
|
-
<div className="mt-1 truncate text-xs text-muted-foreground">
|
|
432
|
-
{detail}
|
|
433
|
-
</div>
|
|
434
|
-
</>
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
const className = cn(
|
|
438
|
-
"block min-w-0 rounded-lg border bg-card p-4 transition",
|
|
439
|
-
to && "hover:border-foreground/30 hover:bg-muted/20",
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
if (to) {
|
|
443
|
-
return (
|
|
444
|
-
<Link to={to} className={className}>
|
|
445
|
-
{body}
|
|
446
|
-
</Link>
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return <div className={className}>{body}</div>;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function ControlPlaneMetrics({
|
|
454
|
-
overview,
|
|
455
|
-
apps,
|
|
456
|
-
agents,
|
|
457
|
-
automations,
|
|
458
|
-
threads,
|
|
459
|
-
taskQueue,
|
|
460
|
-
}: {
|
|
461
|
-
overview?: DispatchOverview;
|
|
462
|
-
apps: WorkspaceAppSummary[];
|
|
463
|
-
agents: ConnectedAgent[];
|
|
464
|
-
automations: AutomationItem[];
|
|
465
|
-
threads: ChatThreadSummary[];
|
|
466
|
-
taskQueue: TaskQueueStats;
|
|
467
|
-
}) {
|
|
468
|
-
const activeApps = apps.filter((app) => !app.isDispatch && !app.archived);
|
|
469
|
-
const pendingApps = activeApps.filter((app) => app.status === "pending");
|
|
470
|
-
const enabledAutomations = automations.filter((item) => item.enabled);
|
|
471
|
-
const automationErrors = automations.filter(
|
|
472
|
-
(item) => item.enabled && item.lastStatus === "error",
|
|
473
|
-
);
|
|
474
|
-
const pendingApprovals = overview?.counts?.pendingApprovals ?? 0;
|
|
475
|
-
const activeRuns = taskQueue.processing;
|
|
476
|
-
|
|
477
|
-
return (
|
|
478
|
-
<section className="grid gap-3 sm:grid-cols-2 xl:grid-cols-6">
|
|
479
|
-
<MetricCard
|
|
480
|
-
label="Chats"
|
|
481
|
-
value={formatNumber(threads.length)}
|
|
482
|
-
detail={`${threads.filter((thread) => thread.messageCount > 0).length} with messages`}
|
|
483
|
-
icon={IconMessages}
|
|
484
|
-
to="/chat"
|
|
485
|
-
/>
|
|
486
|
-
<MetricCard
|
|
487
|
-
label="Runs"
|
|
488
|
-
value={formatNumber(activeRuns)}
|
|
489
|
-
detail={`${formatNumber(taskQueue.pending)} queued`}
|
|
490
|
-
icon={IconPlayerPlay}
|
|
491
|
-
tone={
|
|
492
|
-
taskQueue.failed_last_hour > 0
|
|
493
|
-
? "danger"
|
|
494
|
-
: taskQueue.pending > 5
|
|
495
|
-
? "warning"
|
|
496
|
-
: "default"
|
|
497
|
-
}
|
|
498
|
-
/>
|
|
499
|
-
<MetricCard
|
|
500
|
-
label="Apps"
|
|
501
|
-
value={formatNumber(activeApps.length)}
|
|
502
|
-
detail={`${formatNumber(pendingApps.length)} building`}
|
|
503
|
-
icon={IconStack3}
|
|
504
|
-
to="/apps"
|
|
505
|
-
tone={pendingApps.length > 0 ? "warning" : "default"}
|
|
506
|
-
/>
|
|
507
|
-
<MetricCard
|
|
508
|
-
label="Agents"
|
|
509
|
-
value={formatNumber(agents.length)}
|
|
510
|
-
detail={`${formatNumber(agents.filter((agent) => agent.source === "custom").length)} custom`}
|
|
511
|
-
icon={IconRobot}
|
|
512
|
-
to="/agents"
|
|
513
|
-
/>
|
|
514
|
-
<MetricCard
|
|
515
|
-
label="Automations"
|
|
516
|
-
value={formatNumber(enabledAutomations.length)}
|
|
517
|
-
detail={`${formatNumber(automationErrors.length)} need attention`}
|
|
518
|
-
icon={IconSettingsAutomation}
|
|
519
|
-
tone={automationErrors.length > 0 ? "danger" : "success"}
|
|
520
|
-
/>
|
|
521
|
-
<MetricCard
|
|
522
|
-
label="Approvals"
|
|
523
|
-
value={formatNumber(pendingApprovals)}
|
|
524
|
-
detail={
|
|
525
|
-
overview?.settings?.enabled ? "review mode" : "immediate changes"
|
|
526
|
-
}
|
|
527
|
-
icon={IconShieldCheck}
|
|
528
|
-
to="/approvals"
|
|
529
|
-
tone={pendingApprovals > 0 ? "warning" : "default"}
|
|
530
|
-
/>
|
|
531
|
-
</section>
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function SectionHeader({
|
|
536
|
-
icon: Icon,
|
|
537
|
-
title,
|
|
538
|
-
detail,
|
|
539
|
-
action,
|
|
540
|
-
}: {
|
|
541
|
-
icon: React.ComponentType<IconProps>;
|
|
542
|
-
title: string;
|
|
543
|
-
detail?: string;
|
|
544
|
-
action?: ReactNode;
|
|
545
|
-
}) {
|
|
546
|
-
return (
|
|
547
|
-
<div className="flex min-w-0 items-center justify-between gap-3">
|
|
548
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
549
|
-
<Icon size={16} className="shrink-0 text-muted-foreground" />
|
|
550
|
-
<div className="min-w-0">
|
|
551
|
-
<h2 className="truncate text-sm font-semibold text-foreground">
|
|
552
|
-
{title}
|
|
553
|
-
</h2>
|
|
554
|
-
{detail ? (
|
|
555
|
-
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
556
|
-
{detail}
|
|
557
|
-
</p>
|
|
558
|
-
) : null}
|
|
559
|
-
</div>
|
|
560
|
-
</div>
|
|
561
|
-
{action ? <div className="shrink-0">{action}</div> : null}
|
|
562
|
-
</div>
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function RecentChatsPanel() {
|
|
567
|
-
const navigate = useNavigate();
|
|
568
|
-
const { threads, activeThreadId, createThread, switchThread } =
|
|
569
|
-
useChatThreads(undefined, undefined, undefined, { autoCreate: false });
|
|
570
|
-
const [creating, setCreating] = useState(false);
|
|
571
|
-
|
|
572
|
-
const visibleThreads = useMemo(
|
|
573
|
-
() =>
|
|
574
|
-
threads
|
|
575
|
-
.filter(
|
|
576
|
-
(thread) => thread.messageCount > 0 || thread.id === activeThreadId,
|
|
577
|
-
)
|
|
578
|
-
.sort((a, b) => threadUpdatedAt(b) - threadUpdatedAt(a))
|
|
579
|
-
.slice(0, 5),
|
|
580
|
-
[activeThreadId, threads],
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
function openThread(threadId: string, isNew = false) {
|
|
584
|
-
switchThread(threadId);
|
|
585
|
-
navigate("/chat", {
|
|
586
|
-
state: {
|
|
587
|
-
dispatchThread: {
|
|
588
|
-
id: `${Date.now()}-${threadId}`,
|
|
589
|
-
threadId,
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
});
|
|
593
|
-
window.requestAnimationFrame(() => {
|
|
594
|
-
window.dispatchEvent(
|
|
595
|
-
new CustomEvent("agent-chat:open-thread", {
|
|
596
|
-
detail: { threadId, newThread: isNew },
|
|
597
|
-
}),
|
|
598
|
-
);
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async function handleNewChat() {
|
|
603
|
-
setCreating(true);
|
|
604
|
-
try {
|
|
605
|
-
const threadId = await createThread();
|
|
606
|
-
if (threadId) openThread(threadId, true);
|
|
607
|
-
} finally {
|
|
608
|
-
setCreating(false);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return (
|
|
613
|
-
<section className="rounded-lg border bg-card p-4">
|
|
614
|
-
<SectionHeader
|
|
615
|
-
icon={IconMessages}
|
|
616
|
-
title="Chats"
|
|
617
|
-
detail={`${visibleThreads.length} recent`}
|
|
618
|
-
action={
|
|
619
|
-
<Button
|
|
620
|
-
variant="outline"
|
|
621
|
-
size="sm"
|
|
622
|
-
onClick={handleNewChat}
|
|
623
|
-
disabled={creating}
|
|
624
|
-
>
|
|
625
|
-
<IconPlus size={14} />
|
|
626
|
-
New
|
|
627
|
-
</Button>
|
|
628
|
-
}
|
|
629
|
-
/>
|
|
630
|
-
<div className="mt-3 divide-y rounded-md border">
|
|
631
|
-
{visibleThreads.length > 0 ? (
|
|
632
|
-
visibleThreads.map((thread) => (
|
|
633
|
-
<button
|
|
634
|
-
key={thread.id}
|
|
635
|
-
type="button"
|
|
636
|
-
onClick={() => openThread(thread.id)}
|
|
637
|
-
className="grid w-full grid-cols-[minmax(0,1fr)_auto] gap-3 px-3 py-2.5 text-left transition hover:bg-muted/40"
|
|
638
|
-
>
|
|
639
|
-
<span className="min-w-0">
|
|
640
|
-
<span className="block truncate text-sm font-medium text-foreground">
|
|
641
|
-
{threadTitle(thread)}
|
|
642
|
-
</span>
|
|
643
|
-
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
|
|
644
|
-
{thread.preview || `${thread.messageCount} messages`}
|
|
645
|
-
</span>
|
|
646
|
-
</span>
|
|
647
|
-
<span className="text-[11px] text-muted-foreground">
|
|
648
|
-
{timeAgo(threadUpdatedAt(thread))}
|
|
649
|
-
</span>
|
|
650
|
-
</button>
|
|
651
|
-
))
|
|
652
|
-
) : (
|
|
653
|
-
<button
|
|
654
|
-
type="button"
|
|
655
|
-
onClick={handleNewChat}
|
|
656
|
-
className="block w-full px-3 py-8 text-center text-sm text-muted-foreground transition hover:bg-muted/40"
|
|
657
|
-
>
|
|
658
|
-
Start a Dispatch chat
|
|
659
|
-
</button>
|
|
660
|
-
)}
|
|
661
|
-
</div>
|
|
662
|
-
</section>
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function RunsPanel({ taskQueue }: { taskQueue: TaskQueueStats }) {
|
|
667
|
-
const hasFailure = taskQueue.failed_last_hour > 0;
|
|
668
|
-
const hasBacklog =
|
|
669
|
-
taskQueue.pending > 5 || taskQueue.oldest_pending_age_seconds > 300;
|
|
670
|
-
|
|
671
|
-
return (
|
|
672
|
-
<section className="rounded-lg border bg-card p-4">
|
|
673
|
-
<SectionHeader
|
|
674
|
-
icon={IconPlayerPlay}
|
|
675
|
-
title="Runs"
|
|
676
|
-
detail={
|
|
677
|
-
hasFailure
|
|
678
|
-
? `${taskQueue.failed_last_hour} failed in the last hour`
|
|
679
|
-
: `${taskQueue.processing} processing`
|
|
680
|
-
}
|
|
681
|
-
action={
|
|
682
|
-
<RunsTray
|
|
683
|
-
triggerVariant="pill"
|
|
684
|
-
hideWhenIdle={false}
|
|
685
|
-
showRecent
|
|
686
|
-
limit={8}
|
|
687
|
-
/>
|
|
688
|
-
}
|
|
689
|
-
/>
|
|
690
|
-
<div className="mt-3 grid grid-cols-4 gap-2">
|
|
691
|
-
<QueueCell label="Queued" value={taskQueue.pending} />
|
|
692
|
-
<QueueCell label="Active" value={taskQueue.processing} />
|
|
693
|
-
<QueueCell label="Done 1h" value={taskQueue.completed_last_hour} />
|
|
694
|
-
<QueueCell
|
|
695
|
-
label="Failed 1h"
|
|
696
|
-
value={taskQueue.failed_last_hour}
|
|
697
|
-
danger={hasFailure}
|
|
698
|
-
/>
|
|
699
|
-
</div>
|
|
700
|
-
<div
|
|
701
|
-
className={cn(
|
|
702
|
-
"mt-3 rounded-md border px-3 py-2 text-xs",
|
|
703
|
-
hasFailure
|
|
704
|
-
? "border-destructive/30 bg-destructive/5 text-destructive"
|
|
705
|
-
: hasBacklog
|
|
706
|
-
? "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
|
707
|
-
: "bg-muted/20 text-muted-foreground",
|
|
708
|
-
)}
|
|
709
|
-
>
|
|
710
|
-
Oldest queued: {formatAgeSeconds(taskQueue.oldest_pending_age_seconds)}
|
|
711
|
-
</div>
|
|
712
|
-
{taskQueue.recent_failures.length > 0 ? (
|
|
713
|
-
<div className="mt-3 divide-y rounded-md border">
|
|
714
|
-
{taskQueue.recent_failures.slice(0, 3).map((failure) => (
|
|
715
|
-
<div key={failure.id} className="px-3 py-2">
|
|
716
|
-
<div className="flex items-center justify-between gap-3 text-xs">
|
|
717
|
-
<span className="font-medium text-foreground">
|
|
718
|
-
{failure.platform}
|
|
719
|
-
</span>
|
|
720
|
-
<span className="text-muted-foreground">
|
|
721
|
-
{failure.attempts} attempts
|
|
722
|
-
</span>
|
|
723
|
-
</div>
|
|
724
|
-
<div className="mt-1 truncate text-xs text-muted-foreground">
|
|
725
|
-
{failure.error || "(no error message)"}
|
|
726
|
-
</div>
|
|
727
|
-
</div>
|
|
728
|
-
))}
|
|
729
|
-
</div>
|
|
730
|
-
) : null}
|
|
731
|
-
</section>
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function QueueCell({
|
|
736
|
-
label,
|
|
737
|
-
value,
|
|
738
|
-
danger,
|
|
739
|
-
}: {
|
|
740
|
-
label: string;
|
|
741
|
-
value: number;
|
|
742
|
-
danger?: boolean;
|
|
743
|
-
}) {
|
|
744
|
-
return (
|
|
745
|
-
<div className="rounded-md border bg-background px-2 py-2">
|
|
746
|
-
<div
|
|
747
|
-
className={cn(
|
|
748
|
-
"text-lg font-semibold text-foreground",
|
|
749
|
-
danger && "text-destructive",
|
|
750
|
-
)}
|
|
751
|
-
>
|
|
752
|
-
{formatNumber(value)}
|
|
753
|
-
</div>
|
|
754
|
-
<div className="truncate text-[11px] text-muted-foreground">{label}</div>
|
|
755
|
-
</div>
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
121
|
function AppsPanel({
|
|
760
122
|
apps,
|
|
761
123
|
isLoading,
|
|
@@ -763,17 +125,17 @@ function AppsPanel({
|
|
|
763
125
|
apps: WorkspaceAppSummary[];
|
|
764
126
|
isLoading: boolean;
|
|
765
127
|
}) {
|
|
766
|
-
const visibleApps = apps
|
|
767
|
-
.filter((app) => !app.isDispatch && !app.archived)
|
|
768
|
-
.slice(0, 4);
|
|
128
|
+
const visibleApps = apps.filter((app) => !app.isDispatch && !app.archived);
|
|
769
129
|
const showSkeletons = isLoading && visibleApps.length === 0;
|
|
770
130
|
|
|
771
131
|
return (
|
|
772
|
-
<section className="
|
|
132
|
+
<section className="flex flex-col gap-3">
|
|
773
133
|
<SectionHeader
|
|
774
134
|
icon={IconStack3}
|
|
775
|
-
title="
|
|
776
|
-
detail={
|
|
135
|
+
title="Apps"
|
|
136
|
+
detail={
|
|
137
|
+
visibleApps.length === 1 ? "1 active" : `${visibleApps.length} active`
|
|
138
|
+
}
|
|
777
139
|
action={
|
|
778
140
|
<Button variant="outline" size="sm" asChild>
|
|
779
141
|
<Link to="/apps">
|
|
@@ -806,371 +168,7 @@ function AppsPanel({
|
|
|
806
168
|
);
|
|
807
169
|
}
|
|
808
170
|
|
|
809
|
-
function AutomationsPanel({
|
|
810
|
-
automations,
|
|
811
|
-
isLoading,
|
|
812
|
-
}: {
|
|
813
|
-
automations: AutomationItem[];
|
|
814
|
-
isLoading: boolean;
|
|
815
|
-
}) {
|
|
816
|
-
const toggleAutomation = useToggleAutomation();
|
|
817
|
-
const ordered = useMemo(
|
|
818
|
-
() =>
|
|
819
|
-
[...automations].sort((a, b) => {
|
|
820
|
-
const aError = a.enabled && a.lastStatus === "error" ? 1 : 0;
|
|
821
|
-
const bError = b.enabled && b.lastStatus === "error" ? 1 : 0;
|
|
822
|
-
if (aError !== bError) return bError - aError;
|
|
823
|
-
return (b.lastRun || "").localeCompare(a.lastRun || "");
|
|
824
|
-
}),
|
|
825
|
-
[automations],
|
|
826
|
-
);
|
|
827
|
-
const enabled = automations.filter((item) => item.enabled).length;
|
|
828
|
-
const errors = automations.filter(
|
|
829
|
-
(item) => item.enabled && item.lastStatus === "error",
|
|
830
|
-
).length;
|
|
831
|
-
const pendingToggleIdentity = toggleAutomation.isPending
|
|
832
|
-
? toggleAutomation.variables
|
|
833
|
-
? automationIdentity(toggleAutomation.variables)
|
|
834
|
-
: null
|
|
835
|
-
: null;
|
|
836
|
-
|
|
837
|
-
function handleToggle(item: AutomationItem, enabled: boolean) {
|
|
838
|
-
toggleAutomation.mutate({
|
|
839
|
-
owner: item.owner,
|
|
840
|
-
path: item.path,
|
|
841
|
-
enabled,
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
return (
|
|
846
|
-
<section className="rounded-lg border bg-card p-4">
|
|
847
|
-
<SectionHeader
|
|
848
|
-
icon={IconSettingsAutomation}
|
|
849
|
-
title="Automations"
|
|
850
|
-
detail={`${enabled} enabled · ${errors} errors`}
|
|
851
|
-
action={
|
|
852
|
-
<Button variant="outline" size="sm" asChild>
|
|
853
|
-
<Link to="/chat">
|
|
854
|
-
Create
|
|
855
|
-
<IconArrowUpRight size={14} />
|
|
856
|
-
</Link>
|
|
857
|
-
</Button>
|
|
858
|
-
}
|
|
859
|
-
/>
|
|
860
|
-
<div className="mt-3 divide-y rounded-md border">
|
|
861
|
-
{isLoading && ordered.length === 0 ? (
|
|
862
|
-
Array.from({ length: 4 }).map((_, index) => (
|
|
863
|
-
<div key={index} className="px-3 py-2.5">
|
|
864
|
-
<Skeleton className="h-4 w-40" />
|
|
865
|
-
<Skeleton className="mt-2 h-3 w-28" />
|
|
866
|
-
</div>
|
|
867
|
-
))
|
|
868
|
-
) : ordered.length > 0 ? (
|
|
869
|
-
ordered.slice(0, 6).map((item) => {
|
|
870
|
-
const status = automationStatus(item);
|
|
871
|
-
const canUpdate = item.canUpdate !== false;
|
|
872
|
-
const isToggling =
|
|
873
|
-
pendingToggleIdentity === automationIdentity(item);
|
|
874
|
-
return (
|
|
875
|
-
<div
|
|
876
|
-
key={item.id}
|
|
877
|
-
className="grid grid-cols-[minmax(0,1fr)_auto] gap-3 px-3 py-2.5"
|
|
878
|
-
>
|
|
879
|
-
<div className="min-w-0">
|
|
880
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
881
|
-
<StatusDot tone={status.tone} />
|
|
882
|
-
<span className="truncate text-sm font-medium text-foreground">
|
|
883
|
-
{item.name}
|
|
884
|
-
</span>
|
|
885
|
-
</div>
|
|
886
|
-
<div className="mt-1 truncate text-xs text-muted-foreground">
|
|
887
|
-
{automationTarget(item)}
|
|
888
|
-
</div>
|
|
889
|
-
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
|
|
890
|
-
<span title={dateTimeTitle(item.lastRun)}>
|
|
891
|
-
Last {automationLastRun(item)}
|
|
892
|
-
</span>
|
|
893
|
-
<span title={dateTimeTitle(item.nextRun)}>
|
|
894
|
-
Next {automationNextRun(item)}
|
|
895
|
-
</span>
|
|
896
|
-
</div>
|
|
897
|
-
{item.lastError ? (
|
|
898
|
-
<div className="mt-1 truncate text-xs text-destructive">
|
|
899
|
-
{item.lastError}
|
|
900
|
-
</div>
|
|
901
|
-
) : null}
|
|
902
|
-
</div>
|
|
903
|
-
<div className="flex flex-col items-end gap-1">
|
|
904
|
-
<Badge
|
|
905
|
-
variant={
|
|
906
|
-
status.tone === "danger" ? "destructive" : "outline"
|
|
907
|
-
}
|
|
908
|
-
className="h-5"
|
|
909
|
-
>
|
|
910
|
-
{status.label}
|
|
911
|
-
</Badge>
|
|
912
|
-
<Switch
|
|
913
|
-
checked={!!item.enabled}
|
|
914
|
-
disabled={!canUpdate || isToggling}
|
|
915
|
-
aria-label={`${item.enabled ? "Disable" : "Enable"} automation ${item.name}`}
|
|
916
|
-
onCheckedChange={(checked) => handleToggle(item, checked)}
|
|
917
|
-
/>
|
|
918
|
-
</div>
|
|
919
|
-
</div>
|
|
920
|
-
);
|
|
921
|
-
})
|
|
922
|
-
) : (
|
|
923
|
-
<div className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
924
|
-
No automations yet
|
|
925
|
-
</div>
|
|
926
|
-
)}
|
|
927
|
-
</div>
|
|
928
|
-
</section>
|
|
929
|
-
);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function AgentsPanelSummary({ agents }: { agents: ConnectedAgent[] }) {
|
|
933
|
-
const builtin = agents.filter((agent) => agent.source === "builtin");
|
|
934
|
-
const extra = agents.filter((agent) => agent.source !== "builtin");
|
|
935
|
-
|
|
936
|
-
return (
|
|
937
|
-
<section className="rounded-lg border bg-card p-4">
|
|
938
|
-
<SectionHeader
|
|
939
|
-
icon={IconPlugConnected}
|
|
940
|
-
title="Agents"
|
|
941
|
-
detail={`${builtin.length} built in · ${extra.length} added`}
|
|
942
|
-
action={
|
|
943
|
-
<Button variant="outline" size="sm" asChild>
|
|
944
|
-
<Link to="/agents">
|
|
945
|
-
Manage
|
|
946
|
-
<IconArrowUpRight size={14} />
|
|
947
|
-
</Link>
|
|
948
|
-
</Button>
|
|
949
|
-
}
|
|
950
|
-
/>
|
|
951
|
-
<div className="mt-3 flex flex-wrap gap-2">
|
|
952
|
-
{agents.slice(0, 12).map((agent) => (
|
|
953
|
-
<span
|
|
954
|
-
key={agent.id}
|
|
955
|
-
className="inline-flex max-w-full items-center gap-2 rounded-md border bg-background px-2.5 py-1.5 text-xs text-muted-foreground"
|
|
956
|
-
>
|
|
957
|
-
<span
|
|
958
|
-
className="size-2 shrink-0 rounded-full"
|
|
959
|
-
style={{ backgroundColor: agent.color || "hsl(var(--primary))" }}
|
|
960
|
-
/>
|
|
961
|
-
<span className="truncate">{agent.name}</span>
|
|
962
|
-
</span>
|
|
963
|
-
))}
|
|
964
|
-
{agents.length === 0 ? (
|
|
965
|
-
<div className="w-full rounded-md border border-dashed px-3 py-6 text-center text-sm text-muted-foreground">
|
|
966
|
-
No agents detected
|
|
967
|
-
</div>
|
|
968
|
-
) : null}
|
|
969
|
-
</div>
|
|
970
|
-
</section>
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function ApprovalsAndAuditPanel({
|
|
975
|
-
overview,
|
|
976
|
-
isLoading,
|
|
977
|
-
}: {
|
|
978
|
-
overview?: DispatchOverview;
|
|
979
|
-
isLoading: boolean;
|
|
980
|
-
}) {
|
|
981
|
-
const approvals = overview?.recentApprovals ?? [];
|
|
982
|
-
const audit = overview?.recentAudit ?? [];
|
|
983
|
-
|
|
984
|
-
return (
|
|
985
|
-
<section className="rounded-lg border bg-card p-4">
|
|
986
|
-
<SectionHeader
|
|
987
|
-
icon={IconActivity}
|
|
988
|
-
title="Activity"
|
|
989
|
-
detail={`${approvals.length} approvals · ${audit.length} audit rows`}
|
|
990
|
-
action={
|
|
991
|
-
<Button variant="outline" size="sm" asChild>
|
|
992
|
-
<Link to="/audit">
|
|
993
|
-
Audit
|
|
994
|
-
<IconArrowUpRight size={14} />
|
|
995
|
-
</Link>
|
|
996
|
-
</Button>
|
|
997
|
-
}
|
|
998
|
-
/>
|
|
999
|
-
<div className="mt-3 grid gap-3 lg:grid-cols-2">
|
|
1000
|
-
<div className="rounded-md border">
|
|
1001
|
-
<div className="flex items-center justify-between gap-3 border-b px-3 py-2">
|
|
1002
|
-
<div className="text-xs font-medium uppercase text-muted-foreground">
|
|
1003
|
-
Approvals
|
|
1004
|
-
</div>
|
|
1005
|
-
<Button variant="ghost" size="sm" asChild>
|
|
1006
|
-
<Link to="/approvals">Open</Link>
|
|
1007
|
-
</Button>
|
|
1008
|
-
</div>
|
|
1009
|
-
<div className="divide-y">
|
|
1010
|
-
{isLoading && approvals.length === 0 ? (
|
|
1011
|
-
Array.from({ length: 3 }).map((_, index) => (
|
|
1012
|
-
<div key={index} className="px-3 py-2.5">
|
|
1013
|
-
<Skeleton className="h-4 w-40" />
|
|
1014
|
-
<Skeleton className="mt-2 h-3 w-24" />
|
|
1015
|
-
</div>
|
|
1016
|
-
))
|
|
1017
|
-
) : approvals.length > 0 ? (
|
|
1018
|
-
approvals.slice(0, 4).map((approval) => (
|
|
1019
|
-
<div key={approval.id} className="px-3 py-2.5">
|
|
1020
|
-
<div className="truncate text-sm font-medium text-foreground">
|
|
1021
|
-
{approval.summary}
|
|
1022
|
-
</div>
|
|
1023
|
-
<div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
|
|
1024
|
-
<StatusDot
|
|
1025
|
-
tone={approval.status === "pending" ? "warning" : "muted"}
|
|
1026
|
-
/>
|
|
1027
|
-
<span>{approval.status}</span>
|
|
1028
|
-
</div>
|
|
1029
|
-
</div>
|
|
1030
|
-
))
|
|
1031
|
-
) : (
|
|
1032
|
-
<div className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
1033
|
-
No approval requests
|
|
1034
|
-
</div>
|
|
1035
|
-
)}
|
|
1036
|
-
</div>
|
|
1037
|
-
</div>
|
|
1038
|
-
|
|
1039
|
-
<div className="rounded-md border">
|
|
1040
|
-
<div className="border-b px-3 py-2 text-xs font-medium uppercase text-muted-foreground">
|
|
1041
|
-
Recent audit
|
|
1042
|
-
</div>
|
|
1043
|
-
<div className="divide-y">
|
|
1044
|
-
{isLoading && audit.length === 0 ? (
|
|
1045
|
-
Array.from({ length: 3 }).map((_, index) => (
|
|
1046
|
-
<div key={index} className="px-3 py-2.5">
|
|
1047
|
-
<Skeleton className="h-4 w-44" />
|
|
1048
|
-
<Skeleton className="mt-2 h-3 w-28" />
|
|
1049
|
-
</div>
|
|
1050
|
-
))
|
|
1051
|
-
) : audit.length > 0 ? (
|
|
1052
|
-
audit.slice(0, 4).map((event) => (
|
|
1053
|
-
<div key={event.id} className="px-3 py-2.5">
|
|
1054
|
-
<div className="truncate text-sm font-medium text-foreground">
|
|
1055
|
-
{event.summary}
|
|
1056
|
-
</div>
|
|
1057
|
-
<div className="mt-1 truncate text-xs text-muted-foreground">
|
|
1058
|
-
{event.actor} · {timeAgo(event.createdAt)}
|
|
1059
|
-
</div>
|
|
1060
|
-
</div>
|
|
1061
|
-
))
|
|
1062
|
-
) : (
|
|
1063
|
-
<div className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
1064
|
-
No audit entries
|
|
1065
|
-
</div>
|
|
1066
|
-
)}
|
|
1067
|
-
</div>
|
|
1068
|
-
</div>
|
|
1069
|
-
</div>
|
|
1070
|
-
</section>
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function ReadinessPanel({
|
|
1075
|
-
overview,
|
|
1076
|
-
apps,
|
|
1077
|
-
agents,
|
|
1078
|
-
automations,
|
|
1079
|
-
}: {
|
|
1080
|
-
overview?: DispatchOverview;
|
|
1081
|
-
apps: WorkspaceAppSummary[];
|
|
1082
|
-
agents: ConnectedAgent[];
|
|
1083
|
-
automations: AutomationItem[];
|
|
1084
|
-
}) {
|
|
1085
|
-
const rows = [
|
|
1086
|
-
{
|
|
1087
|
-
label: "Vault",
|
|
1088
|
-
detail:
|
|
1089
|
-
(overview?.vault?.secretCount ?? 0) > 0
|
|
1090
|
-
? `${overview?.vault?.secretCount ?? 0} secrets`
|
|
1091
|
-
: "no secrets",
|
|
1092
|
-
ok: (overview?.vault?.secretCount ?? 0) > 0,
|
|
1093
|
-
to: "/vault",
|
|
1094
|
-
icon: IconShieldCheck,
|
|
1095
|
-
},
|
|
1096
|
-
{
|
|
1097
|
-
label: "Apps",
|
|
1098
|
-
detail: `${apps.filter((app) => !app.isDispatch && !app.archived).length} active`,
|
|
1099
|
-
ok: apps.some((app) => !app.isDispatch && !app.archived),
|
|
1100
|
-
to: "/apps",
|
|
1101
|
-
icon: IconRocket,
|
|
1102
|
-
},
|
|
1103
|
-
{
|
|
1104
|
-
label: "Agents",
|
|
1105
|
-
detail: `${agents.length} available`,
|
|
1106
|
-
ok: agents.length > 0,
|
|
1107
|
-
to: "/agents",
|
|
1108
|
-
icon: IconRobot,
|
|
1109
|
-
},
|
|
1110
|
-
{
|
|
1111
|
-
label: "Automations",
|
|
1112
|
-
detail: `${automations.filter((item) => item.enabled).length} enabled`,
|
|
1113
|
-
ok: automations.every(
|
|
1114
|
-
(item) => !item.enabled || item.lastStatus !== "error",
|
|
1115
|
-
),
|
|
1116
|
-
to: "/chat",
|
|
1117
|
-
icon: IconBolt,
|
|
1118
|
-
},
|
|
1119
|
-
{
|
|
1120
|
-
label: "Team",
|
|
1121
|
-
detail: `${overview?.counts?.linkedIdentities ?? 0} linked identities`,
|
|
1122
|
-
ok: (overview?.counts?.linkedIdentities ?? 0) > 0,
|
|
1123
|
-
to: "/settings#team",
|
|
1124
|
-
icon: IconUsersGroup,
|
|
1125
|
-
},
|
|
1126
|
-
];
|
|
1127
|
-
|
|
1128
|
-
return (
|
|
1129
|
-
<section className="rounded-lg border bg-card p-4">
|
|
1130
|
-
<SectionHeader icon={IconListCheck} title="Readiness" />
|
|
1131
|
-
<div className="mt-3 divide-y rounded-md border">
|
|
1132
|
-
{rows.map((row) => {
|
|
1133
|
-
const Icon = row.icon;
|
|
1134
|
-
return (
|
|
1135
|
-
<Link
|
|
1136
|
-
key={row.label}
|
|
1137
|
-
to={row.to}
|
|
1138
|
-
className="flex items-center gap-3 px-3 py-2.5 transition hover:bg-muted/40"
|
|
1139
|
-
>
|
|
1140
|
-
<span
|
|
1141
|
-
className={cn(
|
|
1142
|
-
"flex size-7 shrink-0 items-center justify-center rounded-md border",
|
|
1143
|
-
row.ok
|
|
1144
|
-
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
|
|
1145
|
-
: "bg-background text-muted-foreground",
|
|
1146
|
-
)}
|
|
1147
|
-
>
|
|
1148
|
-
{row.ok ? <IconCheck size={14} /> : <Icon size={14} />}
|
|
1149
|
-
</span>
|
|
1150
|
-
<span className="min-w-0 flex-1">
|
|
1151
|
-
<span className="block truncate text-sm font-medium text-foreground">
|
|
1152
|
-
{row.label}
|
|
1153
|
-
</span>
|
|
1154
|
-
<span className="block truncate text-xs text-muted-foreground">
|
|
1155
|
-
{row.detail}
|
|
1156
|
-
</span>
|
|
1157
|
-
</span>
|
|
1158
|
-
<IconArrowUpRight size={14} className="text-muted-foreground" />
|
|
1159
|
-
</Link>
|
|
1160
|
-
);
|
|
1161
|
-
})}
|
|
1162
|
-
</div>
|
|
1163
|
-
</section>
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
171
|
export function DispatchControlPlane() {
|
|
1168
|
-
const { data: overviewData, isLoading: overviewLoading } =
|
|
1169
|
-
useActionQuery<DispatchOverview>("list-dispatch-overview", {});
|
|
1170
|
-
const { data: connectedAgents = [] } = useActionQuery<ConnectedAgent[]>(
|
|
1171
|
-
"list-connected-agents",
|
|
1172
|
-
{},
|
|
1173
|
-
);
|
|
1174
172
|
const { data: workspaceApps = [], isLoading: appsLoading } = useActionQuery<
|
|
1175
173
|
WorkspaceAppSummary[]
|
|
1176
174
|
>(
|
|
@@ -1178,105 +176,15 @@ export function DispatchControlPlane() {
|
|
|
1178
176
|
{ includeAgentCards: false, includeArchived: true },
|
|
1179
177
|
{ refetchInterval: 2_000 },
|
|
1180
178
|
);
|
|
1181
|
-
const { threads } = useChatThreads(undefined, undefined, undefined, {
|
|
1182
|
-
autoCreate: false,
|
|
1183
|
-
});
|
|
1184
|
-
const automationsQuery = useAutomationsStatus();
|
|
1185
|
-
const taskQueueQuery = useTaskQueueStats();
|
|
1186
|
-
|
|
1187
|
-
const overview = overviewData;
|
|
1188
|
-
const apps = workspaceApps ?? [];
|
|
1189
|
-
const agents = connectedAgents ?? [];
|
|
1190
|
-
const automations = automationsQuery.data ?? [];
|
|
1191
|
-
const taskQueue = taskQueueQuery.data ?? ZERO_TASK_QUEUE_STATS;
|
|
1192
|
-
const visibleThreads = threads.filter((thread) => thread.messageCount > 0);
|
|
1193
|
-
|
|
1194
|
-
useEffect(() => {
|
|
1195
|
-
const handleRunning = () => {
|
|
1196
|
-
void taskQueueQuery.refetch();
|
|
1197
|
-
};
|
|
1198
|
-
window.addEventListener("agentNative.chatRunning", handleRunning);
|
|
1199
|
-
return () => {
|
|
1200
|
-
window.removeEventListener("agentNative.chatRunning", handleRunning);
|
|
1201
|
-
};
|
|
1202
|
-
}, [taskQueueQuery]);
|
|
1203
179
|
|
|
1204
180
|
return (
|
|
1205
181
|
<DispatchShell
|
|
1206
|
-
title="
|
|
1207
|
-
description="Dispatch
|
|
182
|
+
title="Overview"
|
|
183
|
+
description="Ask Dispatch or jump into a workspace app."
|
|
1208
184
|
>
|
|
1209
|
-
<div className="
|
|
1210
|
-
<
|
|
1211
|
-
|
|
1212
|
-
<div className="grid gap-3 sm:grid-cols-2">
|
|
1213
|
-
<MetricCard
|
|
1214
|
-
label="Vault"
|
|
1215
|
-
value={formatNumber(overview?.vault?.secretCount ?? 0)}
|
|
1216
|
-
detail={`${formatNumber(overview?.vault?.activeGrantCount ?? 0)} active grants`}
|
|
1217
|
-
icon={IconShieldCheck}
|
|
1218
|
-
to="/vault"
|
|
1219
|
-
tone={
|
|
1220
|
-
(overview?.vault?.secretCount ?? 0) > 0 ? "success" : "default"
|
|
1221
|
-
}
|
|
1222
|
-
/>
|
|
1223
|
-
<MetricCard
|
|
1224
|
-
label="Resources"
|
|
1225
|
-
value={formatNumber(overview?.counts?.destinations ?? 0)}
|
|
1226
|
-
detail="destinations"
|
|
1227
|
-
icon={IconArrowUpRight}
|
|
1228
|
-
to="/destinations"
|
|
1229
|
-
/>
|
|
1230
|
-
</div>
|
|
1231
|
-
</div>
|
|
1232
|
-
|
|
1233
|
-
<ControlPlaneMetrics
|
|
1234
|
-
overview={overview}
|
|
1235
|
-
apps={apps}
|
|
1236
|
-
agents={agents}
|
|
1237
|
-
automations={automations}
|
|
1238
|
-
threads={visibleThreads}
|
|
1239
|
-
taskQueue={taskQueue}
|
|
1240
|
-
/>
|
|
1241
|
-
|
|
1242
|
-
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.15fr)_minmax(340px,0.85fr)]">
|
|
1243
|
-
<div className="space-y-4">
|
|
1244
|
-
<div className="grid gap-4 lg:grid-cols-2">
|
|
1245
|
-
<RecentChatsPanel />
|
|
1246
|
-
<RunsPanel taskQueue={taskQueue} />
|
|
1247
|
-
</div>
|
|
1248
|
-
<AppsPanel apps={apps} isLoading={appsLoading} />
|
|
1249
|
-
<ApprovalsAndAuditPanel
|
|
1250
|
-
overview={overview}
|
|
1251
|
-
isLoading={overviewLoading}
|
|
1252
|
-
/>
|
|
1253
|
-
</div>
|
|
1254
|
-
|
|
1255
|
-
<div className="space-y-4">
|
|
1256
|
-
<AutomationsPanel
|
|
1257
|
-
automations={automations}
|
|
1258
|
-
isLoading={automationsQuery.isLoading}
|
|
1259
|
-
/>
|
|
1260
|
-
<AgentsPanelSummary agents={agents} />
|
|
1261
|
-
<ReadinessPanel
|
|
1262
|
-
overview={overview}
|
|
1263
|
-
apps={apps}
|
|
1264
|
-
agents={agents}
|
|
1265
|
-
automations={automations}
|
|
1266
|
-
/>
|
|
1267
|
-
{taskQueue.failed_last_hour > 0 ? (
|
|
1268
|
-
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
|
1269
|
-
<div className="flex items-center gap-2 font-medium">
|
|
1270
|
-
<IconAlertTriangle size={16} />
|
|
1271
|
-
Integration failures detected
|
|
1272
|
-
</div>
|
|
1273
|
-
<div className="mt-1 text-xs">
|
|
1274
|
-
Check credentials, destinations, and recent queue errors.
|
|
1275
|
-
</div>
|
|
1276
|
-
</div>
|
|
1277
|
-
) : null}
|
|
1278
|
-
</div>
|
|
1279
|
-
</div>
|
|
185
|
+
<div className="flex flex-col gap-6">
|
|
186
|
+
<CommandPanel />
|
|
187
|
+
<AppsPanel apps={workspaceApps ?? []} isLoading={appsLoading} />
|
|
1280
188
|
</div>
|
|
1281
189
|
</DispatchShell>
|
|
1282
190
|
);
|