@agent-native/dispatch 0.11.1 → 0.11.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.
Files changed (33) hide show
  1. package/dist/actions/provider-api-catalog.d.ts +2 -2
  2. package/dist/actions/start-workspace-app-creation.js +1 -1
  3. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  4. package/dist/components/create-app-popover.js +3 -3
  5. package/dist/components/create-app-popover.js.map +1 -1
  6. package/dist/components/dispatch-control-plane.d.ts +2 -0
  7. package/dist/components/dispatch-control-plane.d.ts.map +1 -0
  8. package/dist/components/dispatch-control-plane.js +432 -0
  9. package/dist/components/dispatch-control-plane.js.map +1 -0
  10. package/dist/components/layout/Header.d.ts.map +1 -1
  11. package/dist/components/layout/Header.js +19 -2
  12. package/dist/components/layout/Header.js.map +1 -1
  13. package/dist/lib/automations.d.ts +28 -0
  14. package/dist/lib/automations.d.ts.map +1 -0
  15. package/dist/lib/automations.js +27 -0
  16. package/dist/lib/automations.js.map +1 -0
  17. package/dist/routes/pages/overview.d.ts.map +1 -1
  18. package/dist/routes/pages/overview.js +3 -213
  19. package/dist/routes/pages/overview.js.map +1 -1
  20. package/dist/server/lib/provider-api.d.ts +2 -2
  21. package/dist/server/plugins/agent-chat.js +1 -1
  22. package/dist/server/plugins/agent-chat.js.map +1 -1
  23. package/dist/server/plugins/integrations.js +2 -2
  24. package/dist/server/plugins/integrations.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/actions/start-workspace-app-creation.ts +1 -1
  27. package/src/components/create-app-popover.tsx +3 -3
  28. package/src/components/dispatch-control-plane.tsx +1282 -0
  29. package/src/components/layout/Header.tsx +22 -1
  30. package/src/lib/automations.ts +60 -0
  31. package/src/routes/pages/overview.tsx +2 -755
  32. package/src/server/plugins/agent-chat.ts +1 -1
  33. package/src/server/plugins/integrations.ts +2 -2
@@ -0,0 +1,432 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { Link, useNavigate } from "react-router";
4
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { PromptComposer, agentNativePath, isInBuilderFrame, useActionQuery, useChangeVersions, useChatModels, useChatThreads, } from "@agent-native/core/client";
6
+ import { RunsTray } from "@agent-native/core/client/progress";
7
+ import { IconActivity, IconAlertTriangle, IconArrowUpRight, IconBolt, IconBroadcast, IconCheck, IconListCheck, IconMessages, IconPlayerPlay, IconPlugConnected, IconPlus, IconRobot, IconRocket, IconSettingsAutomation, IconShieldCheck, IconStack3, IconUsersGroup, } from "@tabler/icons-react";
8
+ import { toast } from "sonner";
9
+ import { CreateAppPopover } from "../components/create-app-popover.js";
10
+ import { DispatchShell } from "../components/dispatch-shell.js";
11
+ import { WorkspaceAppCard } from "../components/workspace-app-card.js";
12
+ import { Badge } from "../components/ui/badge.js";
13
+ import { Button } from "../components/ui/button.js";
14
+ import { Skeleton } from "../components/ui/skeleton.js";
15
+ import { Switch } from "../components/ui/switch.js";
16
+ import { cn } from "../lib/utils.js";
17
+ import { listDispatchAutomations, setDispatchAutomationEnabled, } from "../lib/automations.js";
18
+ import { submitOverviewPrompt } from "../lib/overview-chat.js";
19
+ const ZERO_TASK_QUEUE_STATS = {
20
+ pending: 0,
21
+ processing: 0,
22
+ completed_last_hour: 0,
23
+ failed_last_hour: 0,
24
+ oldest_pending_age_seconds: 0,
25
+ recent_failures: [],
26
+ };
27
+ const PROMPT_SUGGESTIONS = [
28
+ "Summarize the current workspace health",
29
+ "Create an app for onboarding requests",
30
+ "Check which agents can help with analytics",
31
+ ];
32
+ const AUTOMATIONS_QUERY_KEY = [
33
+ "dispatch-control-plane",
34
+ "automations",
35
+ ];
36
+ function formatNumber(value) {
37
+ return new Intl.NumberFormat(undefined, {
38
+ notation: value >= 10_000 ? "compact" : "standard",
39
+ maximumFractionDigits: value >= 10_000 ? 1 : 0,
40
+ }).format(value);
41
+ }
42
+ function formatAgeSeconds(seconds) {
43
+ if (!seconds || seconds < 0)
44
+ return "0s";
45
+ if (seconds < 60)
46
+ return `${Math.floor(seconds)}s`;
47
+ if (seconds < 3600)
48
+ return `${Math.floor(seconds / 60)}m`;
49
+ return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
50
+ }
51
+ function timeAgo(value) {
52
+ if (value == null || value === "")
53
+ return "never";
54
+ const timestamp = typeof value === "number" ? value : new Date(value).getTime();
55
+ if (!Number.isFinite(timestamp))
56
+ return "never";
57
+ const diff = Math.max(0, Date.now() - timestamp);
58
+ if (diff < 60_000)
59
+ return "now";
60
+ if (diff < 3_600_000)
61
+ return `${Math.floor(diff / 60_000)}m`;
62
+ if (diff < 86_400_000)
63
+ return `${Math.floor(diff / 3_600_000)}h`;
64
+ return `${Math.floor(diff / 86_400_000)}d`;
65
+ }
66
+ function relativeRunTime(value) {
67
+ if (!value)
68
+ return "never";
69
+ const timestamp = new Date(value).getTime();
70
+ if (!Number.isFinite(timestamp))
71
+ return "never";
72
+ const diff = timestamp - Date.now();
73
+ const abs = Math.abs(diff);
74
+ const suffix = diff >= 0 ? "from now" : "ago";
75
+ if (abs < 60_000)
76
+ return diff >= 0 ? "soon" : "now";
77
+ if (abs < 3_600_000)
78
+ return `${Math.floor(abs / 60_000)}m ${suffix}`;
79
+ if (abs < 86_400_000)
80
+ return `${Math.floor(abs / 3_600_000)}h ${suffix}`;
81
+ return `${Math.floor(abs / 86_400_000)}d ${suffix}`;
82
+ }
83
+ function dateTimeTitle(value) {
84
+ if (!value)
85
+ return undefined;
86
+ const timestamp = new Date(value).getTime();
87
+ if (!Number.isFinite(timestamp))
88
+ return undefined;
89
+ return new Intl.DateTimeFormat(undefined, {
90
+ dateStyle: "medium",
91
+ timeStyle: "short",
92
+ }).format(timestamp);
93
+ }
94
+ function threadUpdatedAt(thread) {
95
+ return Number.isFinite(thread.updatedAt)
96
+ ? thread.updatedAt
97
+ : Number.isFinite(thread.createdAt)
98
+ ? thread.createdAt
99
+ : 0;
100
+ }
101
+ function threadTitle(thread) {
102
+ return thread.title || thread.preview || "New chat";
103
+ }
104
+ function automationTarget(item) {
105
+ if (item.triggerType === "event" && item.event)
106
+ return item.event;
107
+ if (item.scheduleDescription)
108
+ return item.scheduleDescription;
109
+ if (item.schedule)
110
+ return item.schedule;
111
+ return item.triggerType || "schedule";
112
+ }
113
+ function automationLastRun(item) {
114
+ return item.lastRun ? relativeRunTime(item.lastRun) : "never";
115
+ }
116
+ function automationNextRun(item) {
117
+ if (!item.enabled)
118
+ return "paused";
119
+ if (item.triggerType === "event")
120
+ return "on event";
121
+ return item.nextRun ? relativeRunTime(item.nextRun) : "not scheduled";
122
+ }
123
+ function automationStatus(item) {
124
+ if (!item.enabled)
125
+ return { label: "Paused", tone: "muted" };
126
+ if (item.lastStatus === "error")
127
+ return { label: "Error", tone: "danger" };
128
+ if (item.lastStatus === "running")
129
+ return { label: "Running", tone: "warning" };
130
+ if (item.lastStatus === "skipped")
131
+ return { label: "Skipped", tone: "warning" };
132
+ if (item.lastStatus === "success")
133
+ return { label: "Healthy", tone: "success" };
134
+ return { label: "Ready", tone: "default" };
135
+ }
136
+ function automationIdentity(item) {
137
+ return `${item.owner}:${item.path}`;
138
+ }
139
+ function StatusDot({ tone = "default", }) {
140
+ return (_jsx("span", { className: cn("size-2 rounded-full", tone === "success" && "bg-emerald-500", tone === "warning" && "bg-amber-500", tone === "danger" && "bg-destructive", tone === "muted" && "bg-muted-foreground/35", tone === "default" && "bg-primary") }));
141
+ }
142
+ function useAutomationsStatus() {
143
+ const version = useChangeVersions(["action", "screen-refresh"]);
144
+ return useQuery({
145
+ queryKey: [...AUTOMATIONS_QUERY_KEY, version],
146
+ queryFn: listDispatchAutomations,
147
+ placeholderData: (prev) => prev,
148
+ refetchInterval: 15_000,
149
+ staleTime: 5_000,
150
+ });
151
+ }
152
+ function useToggleAutomation() {
153
+ const queryClient = useQueryClient();
154
+ return useMutation({
155
+ mutationFn: setDispatchAutomationEnabled,
156
+ onMutate: async (input) => {
157
+ await queryClient.cancelQueries({ queryKey: AUTOMATIONS_QUERY_KEY });
158
+ const snapshots = queryClient.getQueriesData({
159
+ queryKey: AUTOMATIONS_QUERY_KEY,
160
+ });
161
+ queryClient.setQueriesData({ queryKey: AUTOMATIONS_QUERY_KEY }, (rows) => rows?.map((item) => automationIdentity(item) === automationIdentity(input)
162
+ ? { ...item, enabled: input.enabled }
163
+ : item));
164
+ return { snapshots };
165
+ },
166
+ onError: (err, _input, context) => {
167
+ for (const [queryKey, data] of context?.snapshots ?? []) {
168
+ queryClient.setQueryData(queryKey, data);
169
+ }
170
+ toast.error(`Could not update automation: ${err instanceof Error ? err.message : String(err)}`);
171
+ },
172
+ onSuccess: (updated) => {
173
+ queryClient.setQueriesData({ queryKey: AUTOMATIONS_QUERY_KEY }, (rows) => rows?.map((item) => automationIdentity(item) === automationIdentity(updated)
174
+ ? updated
175
+ : item));
176
+ },
177
+ onSettled: () => {
178
+ void queryClient.invalidateQueries({ queryKey: AUTOMATIONS_QUERY_KEY });
179
+ },
180
+ });
181
+ }
182
+ function useTaskQueueStats() {
183
+ return useQuery({
184
+ queryKey: ["dispatch-control-plane", "task-queue"],
185
+ queryFn: async () => {
186
+ const res = await fetch(agentNativePath("/_agent-native/integrations/task-queue/status"));
187
+ if (!res.ok)
188
+ return ZERO_TASK_QUEUE_STATS;
189
+ const stats = await res.json();
190
+ if (!stats || typeof stats !== "object")
191
+ return ZERO_TASK_QUEUE_STATS;
192
+ return {
193
+ pending: Number(stats.pending ?? 0),
194
+ processing: Number(stats.processing ?? 0),
195
+ completed_last_hour: Number(stats.completed_last_hour ?? 0),
196
+ failed_last_hour: Number(stats.failed_last_hour ?? 0),
197
+ oldest_pending_age_seconds: Number(stats.oldest_pending_age_seconds ?? 0),
198
+ recent_failures: Array.isArray(stats.recent_failures)
199
+ ? stats.recent_failures
200
+ : [],
201
+ };
202
+ },
203
+ placeholderData: (prev) => prev,
204
+ refetchInterval: 15_000,
205
+ staleTime: 5_000,
206
+ });
207
+ }
208
+ function CommandPanel() {
209
+ const { selectedModel } = useChatModels();
210
+ const navigate = useNavigate();
211
+ function send(message) {
212
+ const trimmed = message.trim();
213
+ if (!trimmed)
214
+ return;
215
+ if (isInBuilderFrame()) {
216
+ submitOverviewPrompt(trimmed, selectedModel);
217
+ return;
218
+ }
219
+ navigate("/chat", {
220
+ state: {
221
+ dispatchPrompt: {
222
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
223
+ message: trimmed,
224
+ selectedModel,
225
+ },
226
+ },
227
+ });
228
+ }
229
+ return (_jsxs("section", { className: "rounded-lg border bg-card p-4", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(IconBroadcast, { size: 16, className: "text-muted-foreground" }), _jsx("h2", { className: "text-sm font-semibold text-foreground", children: "Ask Dispatch" })] }), _jsx(Button, { variant: "outline", size: "sm", asChild: true, children: _jsxs(Link, { to: "/chat", children: ["Open chat", _jsx(IconArrowUpRight, { size: 14 })] }) })] }), _jsx(PromptComposer, { placeholder: "Route work, inspect status, or create an app...", onSubmit: (text) => send(text) }), _jsx("div", { className: "mt-3 flex flex-wrap gap-2", children: PROMPT_SUGGESTIONS.map((suggestion) => (_jsx("button", { type: "button", onClick: () => send(suggestion), className: "cursor-pointer rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-muted-foreground transition hover:border-foreground/30 hover:text-foreground", children: suggestion }, suggestion))) })] }));
230
+ }
231
+ function MetricCard({ label, value, detail, icon: Icon, to, tone = "default", }) {
232
+ const body = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground", children: [_jsx(Icon, { size: 15 }), _jsx("span", { className: "truncate", children: label })] }), _jsx(StatusDot, { tone: tone === "success"
233
+ ? "success"
234
+ : tone === "warning"
235
+ ? "warning"
236
+ : tone === "danger"
237
+ ? "danger"
238
+ : "default" })] }), _jsx("div", { className: "mt-3 text-2xl font-semibold tracking-tight text-foreground", children: value }), _jsx("div", { className: "mt-1 truncate text-xs text-muted-foreground", children: detail })] }));
239
+ const className = cn("block min-w-0 rounded-lg border bg-card p-4 transition", to && "hover:border-foreground/30 hover:bg-muted/20");
240
+ if (to) {
241
+ return (_jsx(Link, { to: to, className: className, children: body }));
242
+ }
243
+ return _jsx("div", { className: className, children: body });
244
+ }
245
+ function ControlPlaneMetrics({ overview, apps, agents, automations, threads, taskQueue, }) {
246
+ const activeApps = apps.filter((app) => !app.isDispatch && !app.archived);
247
+ const pendingApps = activeApps.filter((app) => app.status === "pending");
248
+ const enabledAutomations = automations.filter((item) => item.enabled);
249
+ const automationErrors = automations.filter((item) => item.enabled && item.lastStatus === "error");
250
+ const pendingApprovals = overview?.counts?.pendingApprovals ?? 0;
251
+ const activeRuns = taskQueue.processing;
252
+ return (_jsxs("section", { className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-6", children: [_jsx(MetricCard, { label: "Chats", value: formatNumber(threads.length), detail: `${threads.filter((thread) => thread.messageCount > 0).length} with messages`, icon: IconMessages, to: "/chat" }), _jsx(MetricCard, { label: "Runs", value: formatNumber(activeRuns), detail: `${formatNumber(taskQueue.pending)} queued`, icon: IconPlayerPlay, tone: taskQueue.failed_last_hour > 0
253
+ ? "danger"
254
+ : taskQueue.pending > 5
255
+ ? "warning"
256
+ : "default" }), _jsx(MetricCard, { label: "Apps", value: formatNumber(activeApps.length), detail: `${formatNumber(pendingApps.length)} building`, icon: IconStack3, to: "/apps", tone: pendingApps.length > 0 ? "warning" : "default" }), _jsx(MetricCard, { label: "Agents", value: formatNumber(agents.length), detail: `${formatNumber(agents.filter((agent) => agent.source === "custom").length)} custom`, icon: IconRobot, to: "/agents" }), _jsx(MetricCard, { label: "Automations", value: formatNumber(enabledAutomations.length), detail: `${formatNumber(automationErrors.length)} need attention`, icon: IconSettingsAutomation, tone: automationErrors.length > 0 ? "danger" : "success" }), _jsx(MetricCard, { label: "Approvals", value: formatNumber(pendingApprovals), detail: overview?.settings?.enabled ? "review mode" : "immediate changes", icon: IconShieldCheck, to: "/approvals", tone: pendingApprovals > 0 ? "warning" : "default" })] }));
257
+ }
258
+ function SectionHeader({ icon: Icon, title, detail, action, }) {
259
+ return (_jsxs("div", { className: "flex min-w-0 items-center justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Icon, { size: 16, className: "shrink-0 text-muted-foreground" }), _jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-sm font-semibold text-foreground", children: title }), detail ? (_jsx("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: detail })) : null] })] }), action ? _jsx("div", { className: "shrink-0", children: action }) : null] }));
260
+ }
261
+ function RecentChatsPanel() {
262
+ const navigate = useNavigate();
263
+ const { threads, activeThreadId, createThread, switchThread } = useChatThreads(undefined, undefined, undefined, { autoCreate: false });
264
+ const [creating, setCreating] = useState(false);
265
+ const visibleThreads = useMemo(() => threads
266
+ .filter((thread) => thread.messageCount > 0 || thread.id === activeThreadId)
267
+ .sort((a, b) => threadUpdatedAt(b) - threadUpdatedAt(a))
268
+ .slice(0, 5), [activeThreadId, threads]);
269
+ function openThread(threadId, isNew = false) {
270
+ switchThread(threadId);
271
+ navigate("/chat", {
272
+ state: {
273
+ dispatchThread: {
274
+ id: `${Date.now()}-${threadId}`,
275
+ threadId,
276
+ },
277
+ },
278
+ });
279
+ window.requestAnimationFrame(() => {
280
+ window.dispatchEvent(new CustomEvent("agent-chat:open-thread", {
281
+ detail: { threadId, newThread: isNew },
282
+ }));
283
+ });
284
+ }
285
+ async function handleNewChat() {
286
+ setCreating(true);
287
+ try {
288
+ const threadId = await createThread();
289
+ if (threadId)
290
+ openThread(threadId, true);
291
+ }
292
+ finally {
293
+ setCreating(false);
294
+ }
295
+ }
296
+ return (_jsxs("section", { className: "rounded-lg border bg-card p-4", children: [_jsx(SectionHeader, { icon: IconMessages, title: "Chats", detail: `${visibleThreads.length} recent`, action: _jsxs(Button, { variant: "outline", size: "sm", onClick: handleNewChat, disabled: creating, children: [_jsx(IconPlus, { size: 14 }), "New"] }) }), _jsx("div", { className: "mt-3 divide-y rounded-md border", children: visibleThreads.length > 0 ? (visibleThreads.map((thread) => (_jsxs("button", { type: "button", onClick: () => openThread(thread.id), className: "grid w-full grid-cols-[minmax(0,1fr)_auto] gap-3 px-3 py-2.5 text-left transition hover:bg-muted/40", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: threadTitle(thread) }), _jsx("span", { className: "mt-0.5 block truncate text-xs text-muted-foreground", children: thread.preview || `${thread.messageCount} messages` })] }), _jsx("span", { className: "text-[11px] text-muted-foreground", children: timeAgo(threadUpdatedAt(thread)) })] }, thread.id)))) : (_jsx("button", { type: "button", onClick: handleNewChat, className: "block w-full px-3 py-8 text-center text-sm text-muted-foreground transition hover:bg-muted/40", children: "Start a Dispatch chat" })) })] }));
297
+ }
298
+ function RunsPanel({ taskQueue }) {
299
+ const hasFailure = taskQueue.failed_last_hour > 0;
300
+ const hasBacklog = taskQueue.pending > 5 || taskQueue.oldest_pending_age_seconds > 300;
301
+ return (_jsxs("section", { className: "rounded-lg border bg-card p-4", children: [_jsx(SectionHeader, { icon: IconPlayerPlay, title: "Runs", detail: hasFailure
302
+ ? `${taskQueue.failed_last_hour} failed in the last hour`
303
+ : `${taskQueue.processing} processing`, action: _jsx(RunsTray, { triggerVariant: "pill", hideWhenIdle: false, showRecent: true, limit: 8 }) }), _jsxs("div", { className: "mt-3 grid grid-cols-4 gap-2", children: [_jsx(QueueCell, { label: "Queued", value: taskQueue.pending }), _jsx(QueueCell, { label: "Active", value: taskQueue.processing }), _jsx(QueueCell, { label: "Done 1h", value: taskQueue.completed_last_hour }), _jsx(QueueCell, { label: "Failed 1h", value: taskQueue.failed_last_hour, danger: hasFailure })] }), _jsxs("div", { className: cn("mt-3 rounded-md border px-3 py-2 text-xs", hasFailure
304
+ ? "border-destructive/30 bg-destructive/5 text-destructive"
305
+ : hasBacklog
306
+ ? "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
307
+ : "bg-muted/20 text-muted-foreground"), children: ["Oldest queued: ", formatAgeSeconds(taskQueue.oldest_pending_age_seconds)] }), taskQueue.recent_failures.length > 0 ? (_jsx("div", { className: "mt-3 divide-y rounded-md border", children: taskQueue.recent_failures.slice(0, 3).map((failure) => (_jsxs("div", { className: "px-3 py-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 text-xs", children: [_jsx("span", { className: "font-medium text-foreground", children: failure.platform }), _jsxs("span", { className: "text-muted-foreground", children: [failure.attempts, " attempts"] })] }), _jsx("div", { className: "mt-1 truncate text-xs text-muted-foreground", children: failure.error || "(no error message)" })] }, failure.id))) })) : null] }));
308
+ }
309
+ function QueueCell({ label, value, danger, }) {
310
+ return (_jsxs("div", { className: "rounded-md border bg-background px-2 py-2", children: [_jsx("div", { className: cn("text-lg font-semibold text-foreground", danger && "text-destructive"), children: formatNumber(value) }), _jsx("div", { className: "truncate text-[11px] text-muted-foreground", children: label })] }));
311
+ }
312
+ function AppsPanel({ apps, isLoading, }) {
313
+ const visibleApps = apps
314
+ .filter((app) => !app.isDispatch && !app.archived)
315
+ .slice(0, 4);
316
+ const showSkeletons = isLoading && visibleApps.length === 0;
317
+ return (_jsxs("section", { className: "space-y-3", children: [_jsx(SectionHeader, { icon: IconStack3, title: "Projects and apps", detail: `${apps.filter((app) => !app.isDispatch && !app.archived).length} active`, action: _jsx(Button, { variant: "outline", size: "sm", asChild: true, children: _jsxs(Link, { to: "/apps", children: ["View all", _jsx(IconArrowUpRight, { size: 14 })] }) }) }), showSkeletons ? (_jsx("div", { className: "grid gap-3 md:grid-cols-2", children: Array.from({ length: 4 }).map((_, index) => (_jsxs("div", { className: "rounded-lg border bg-card p-4", children: [_jsx(Skeleton, { className: "h-4 w-32" }), _jsx(Skeleton, { className: "mt-3 h-3 w-24" }), _jsx(Skeleton, { className: "mt-3 h-3 w-full" })] }, index))) })) : visibleApps.length > 0 ? (_jsx("div", { className: "grid gap-3 md:grid-cols-2", children: visibleApps.map((app) => (_jsx(WorkspaceAppCard, { app: app, className: "min-h-32" }, app.id))) })) : (_jsx(CreateAppPopover, {}))] }));
318
+ }
319
+ function AutomationsPanel({ automations, isLoading, }) {
320
+ const toggleAutomation = useToggleAutomation();
321
+ const ordered = useMemo(() => [...automations].sort((a, b) => {
322
+ const aError = a.enabled && a.lastStatus === "error" ? 1 : 0;
323
+ const bError = b.enabled && b.lastStatus === "error" ? 1 : 0;
324
+ if (aError !== bError)
325
+ return bError - aError;
326
+ return (b.lastRun || "").localeCompare(a.lastRun || "");
327
+ }), [automations]);
328
+ const enabled = automations.filter((item) => item.enabled).length;
329
+ const errors = automations.filter((item) => item.enabled && item.lastStatus === "error").length;
330
+ const pendingToggleIdentity = toggleAutomation.isPending
331
+ ? toggleAutomation.variables
332
+ ? automationIdentity(toggleAutomation.variables)
333
+ : null
334
+ : null;
335
+ function handleToggle(item, enabled) {
336
+ toggleAutomation.mutate({
337
+ owner: item.owner,
338
+ path: item.path,
339
+ enabled,
340
+ });
341
+ }
342
+ return (_jsxs("section", { className: "rounded-lg border bg-card p-4", children: [_jsx(SectionHeader, { icon: IconSettingsAutomation, title: "Automations", detail: `${enabled} enabled · ${errors} errors`, action: _jsx(Button, { variant: "outline", size: "sm", asChild: true, children: _jsxs(Link, { to: "/chat", children: ["Create", _jsx(IconArrowUpRight, { size: 14 })] }) }) }), _jsx("div", { className: "mt-3 divide-y rounded-md border", children: isLoading && ordered.length === 0 ? (Array.from({ length: 4 }).map((_, index) => (_jsxs("div", { className: "px-3 py-2.5", children: [_jsx(Skeleton, { className: "h-4 w-40" }), _jsx(Skeleton, { className: "mt-2 h-3 w-28" })] }, index)))) : ordered.length > 0 ? (ordered.slice(0, 6).map((item) => {
343
+ const status = automationStatus(item);
344
+ const canUpdate = item.canUpdate !== false;
345
+ const isToggling = pendingToggleIdentity === automationIdentity(item);
346
+ return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_auto] gap-3 px-3 py-2.5", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(StatusDot, { tone: status.tone }), _jsx("span", { className: "truncate text-sm font-medium text-foreground", children: item.name })] }), _jsx("div", { className: "mt-1 truncate text-xs text-muted-foreground", children: automationTarget(item) }), _jsxs("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground", children: [_jsxs("span", { title: dateTimeTitle(item.lastRun), children: ["Last ", automationLastRun(item)] }), _jsxs("span", { title: dateTimeTitle(item.nextRun), children: ["Next ", automationNextRun(item)] })] }), item.lastError ? (_jsx("div", { className: "mt-1 truncate text-xs text-destructive", children: item.lastError })) : null] }), _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(Badge, { variant: status.tone === "danger" ? "destructive" : "outline", className: "h-5", children: status.label }), _jsx(Switch, { checked: !!item.enabled, disabled: !canUpdate || isToggling, "aria-label": `${item.enabled ? "Disable" : "Enable"} automation ${item.name}`, onCheckedChange: (checked) => handleToggle(item, checked) })] })] }, item.id));
347
+ })) : (_jsx("div", { className: "px-3 py-8 text-center text-sm text-muted-foreground", children: "No automations yet" })) })] }));
348
+ }
349
+ function AgentsPanelSummary({ agents }) {
350
+ const builtin = agents.filter((agent) => agent.source === "builtin");
351
+ const extra = agents.filter((agent) => agent.source !== "builtin");
352
+ return (_jsxs("section", { className: "rounded-lg border bg-card p-4", children: [_jsx(SectionHeader, { icon: IconPlugConnected, title: "Agents", detail: `${builtin.length} built in · ${extra.length} added`, action: _jsx(Button, { variant: "outline", size: "sm", asChild: true, children: _jsxs(Link, { to: "/agents", children: ["Manage", _jsx(IconArrowUpRight, { size: 14 })] }) }) }), _jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [agents.slice(0, 12).map((agent) => (_jsxs("span", { 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", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { backgroundColor: agent.color || "hsl(var(--primary))" } }), _jsx("span", { className: "truncate", children: agent.name })] }, agent.id))), agents.length === 0 ? (_jsx("div", { className: "w-full rounded-md border border-dashed px-3 py-6 text-center text-sm text-muted-foreground", children: "No agents detected" })) : null] })] }));
353
+ }
354
+ function ApprovalsAndAuditPanel({ overview, isLoading, }) {
355
+ const approvals = overview?.recentApprovals ?? [];
356
+ const audit = overview?.recentAudit ?? [];
357
+ return (_jsxs("section", { className: "rounded-lg border bg-card p-4", children: [_jsx(SectionHeader, { icon: IconActivity, title: "Activity", detail: `${approvals.length} approvals · ${audit.length} audit rows`, action: _jsx(Button, { variant: "outline", size: "sm", asChild: true, children: _jsxs(Link, { to: "/audit", children: ["Audit", _jsx(IconArrowUpRight, { size: 14 })] }) }) }), _jsxs("div", { className: "mt-3 grid gap-3 lg:grid-cols-2", children: [_jsxs("div", { className: "rounded-md border", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 border-b px-3 py-2", children: [_jsx("div", { className: "text-xs font-medium uppercase text-muted-foreground", children: "Approvals" }), _jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsx(Link, { to: "/approvals", children: "Open" }) })] }), _jsx("div", { className: "divide-y", children: isLoading && approvals.length === 0 ? (Array.from({ length: 3 }).map((_, index) => (_jsxs("div", { className: "px-3 py-2.5", children: [_jsx(Skeleton, { className: "h-4 w-40" }), _jsx(Skeleton, { className: "mt-2 h-3 w-24" })] }, index)))) : approvals.length > 0 ? (approvals.slice(0, 4).map((approval) => (_jsxs("div", { className: "px-3 py-2.5", children: [_jsx("div", { className: "truncate text-sm font-medium text-foreground", children: approval.summary }), _jsxs("div", { className: "mt-1 flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx(StatusDot, { tone: approval.status === "pending" ? "warning" : "muted" }), _jsx("span", { children: approval.status })] })] }, approval.id)))) : (_jsx("div", { className: "px-3 py-8 text-center text-sm text-muted-foreground", children: "No approval requests" })) })] }), _jsxs("div", { className: "rounded-md border", children: [_jsx("div", { className: "border-b px-3 py-2 text-xs font-medium uppercase text-muted-foreground", children: "Recent audit" }), _jsx("div", { className: "divide-y", children: isLoading && audit.length === 0 ? (Array.from({ length: 3 }).map((_, index) => (_jsxs("div", { className: "px-3 py-2.5", children: [_jsx(Skeleton, { className: "h-4 w-44" }), _jsx(Skeleton, { className: "mt-2 h-3 w-28" })] }, index)))) : audit.length > 0 ? (audit.slice(0, 4).map((event) => (_jsxs("div", { className: "px-3 py-2.5", children: [_jsx("div", { className: "truncate text-sm font-medium text-foreground", children: event.summary }), _jsxs("div", { className: "mt-1 truncate text-xs text-muted-foreground", children: [event.actor, " \u00B7 ", timeAgo(event.createdAt)] })] }, event.id)))) : (_jsx("div", { className: "px-3 py-8 text-center text-sm text-muted-foreground", children: "No audit entries" })) })] })] })] }));
358
+ }
359
+ function ReadinessPanel({ overview, apps, agents, automations, }) {
360
+ const rows = [
361
+ {
362
+ label: "Vault",
363
+ detail: (overview?.vault?.secretCount ?? 0) > 0
364
+ ? `${overview?.vault?.secretCount ?? 0} secrets`
365
+ : "no secrets",
366
+ ok: (overview?.vault?.secretCount ?? 0) > 0,
367
+ to: "/vault",
368
+ icon: IconShieldCheck,
369
+ },
370
+ {
371
+ label: "Apps",
372
+ detail: `${apps.filter((app) => !app.isDispatch && !app.archived).length} active`,
373
+ ok: apps.some((app) => !app.isDispatch && !app.archived),
374
+ to: "/apps",
375
+ icon: IconRocket,
376
+ },
377
+ {
378
+ label: "Agents",
379
+ detail: `${agents.length} available`,
380
+ ok: agents.length > 0,
381
+ to: "/agents",
382
+ icon: IconRobot,
383
+ },
384
+ {
385
+ label: "Automations",
386
+ detail: `${automations.filter((item) => item.enabled).length} enabled`,
387
+ ok: automations.every((item) => !item.enabled || item.lastStatus !== "error"),
388
+ to: "/chat",
389
+ icon: IconBolt,
390
+ },
391
+ {
392
+ label: "Team",
393
+ detail: `${overview?.counts?.linkedIdentities ?? 0} linked identities`,
394
+ ok: (overview?.counts?.linkedIdentities ?? 0) > 0,
395
+ to: "/team",
396
+ icon: IconUsersGroup,
397
+ },
398
+ ];
399
+ return (_jsxs("section", { className: "rounded-lg border bg-card p-4", children: [_jsx(SectionHeader, { icon: IconListCheck, title: "Readiness" }), _jsx("div", { className: "mt-3 divide-y rounded-md border", children: rows.map((row) => {
400
+ const Icon = row.icon;
401
+ return (_jsxs(Link, { to: row.to, className: "flex items-center gap-3 px-3 py-2.5 transition hover:bg-muted/40", children: [_jsx("span", { className: cn("flex size-7 shrink-0 items-center justify-center rounded-md border", row.ok
402
+ ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
403
+ : "bg-background text-muted-foreground"), children: row.ok ? _jsx(IconCheck, { size: 14 }) : _jsx(Icon, { size: 14 }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: row.label }), _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: row.detail })] }), _jsx(IconArrowUpRight, { size: 14, className: "text-muted-foreground" })] }, row.label));
404
+ }) })] }));
405
+ }
406
+ export function DispatchControlPlane() {
407
+ const { data: overviewData, isLoading: overviewLoading } = useActionQuery("list-dispatch-overview", {});
408
+ const { data: connectedAgents = [] } = useActionQuery("list-connected-agents", {});
409
+ const { data: workspaceApps = [], isLoading: appsLoading } = useActionQuery("list-workspace-apps", { includeAgentCards: false, includeArchived: true }, { refetchInterval: 2_000 });
410
+ const { threads } = useChatThreads(undefined, undefined, undefined, {
411
+ autoCreate: false,
412
+ });
413
+ const automationsQuery = useAutomationsStatus();
414
+ const taskQueueQuery = useTaskQueueStats();
415
+ const overview = overviewData;
416
+ const apps = workspaceApps ?? [];
417
+ const agents = connectedAgents ?? [];
418
+ const automations = automationsQuery.data ?? [];
419
+ const taskQueue = taskQueueQuery.data ?? ZERO_TASK_QUEUE_STATS;
420
+ const visibleThreads = threads.filter((thread) => thread.messageCount > 0);
421
+ useEffect(() => {
422
+ const handleRunning = () => {
423
+ void taskQueueQuery.refetch();
424
+ };
425
+ window.addEventListener("agentNative.chatRunning", handleRunning);
426
+ return () => {
427
+ window.removeEventListener("agentNative.chatRunning", handleRunning);
428
+ };
429
+ }, [taskQueueQuery]);
430
+ return (_jsx(DispatchShell, { title: "Control plane", description: "Dispatch is the workspace shell for chats, runs, apps, agents, automations, approvals, and resources.", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "grid gap-4 xl:grid-cols-[minmax(0,1.08fr)_minmax(360px,0.92fr)]", children: [_jsx(CommandPanel, {}), _jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [_jsx(MetricCard, { label: "Vault", value: formatNumber(overview?.vault?.secretCount ?? 0), detail: `${formatNumber(overview?.vault?.activeGrantCount ?? 0)} active grants`, icon: IconShieldCheck, to: "/vault", tone: (overview?.vault?.secretCount ?? 0) > 0 ? "success" : "default" }), _jsx(MetricCard, { label: "Resources", value: formatNumber(overview?.counts?.destinations ?? 0), detail: "destinations", icon: IconArrowUpRight, to: "/destinations" })] })] }), _jsx(ControlPlaneMetrics, { overview: overview, apps: apps, agents: agents, automations: automations, threads: visibleThreads, taskQueue: taskQueue }), _jsxs("div", { className: "grid gap-4 xl:grid-cols-[minmax(0,1.15fr)_minmax(340px,0.85fr)]", children: [_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsx(RecentChatsPanel, {}), _jsx(RunsPanel, { taskQueue: taskQueue })] }), _jsx(AppsPanel, { apps: apps, isLoading: appsLoading }), _jsx(ApprovalsAndAuditPanel, { overview: overview, isLoading: overviewLoading })] }), _jsxs("div", { className: "space-y-4", children: [_jsx(AutomationsPanel, { automations: automations, isLoading: automationsQuery.isLoading }), _jsx(AgentsPanelSummary, { agents: agents }), _jsx(ReadinessPanel, { overview: overview, apps: apps, agents: agents, automations: automations }), taskQueue.failed_last_hour > 0 ? (_jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive", children: [_jsxs("div", { className: "flex items-center gap-2 font-medium", children: [_jsx(IconAlertTriangle, { size: 16 }), "Integration failures detected"] }), _jsx("div", { className: "mt-1 text-xs", children: "Check credentials, destinations, and recent queue errors." })] })) : null] })] })] }) }));
431
+ }
432
+ //# sourceMappingURL=dispatch-control-plane.js.map