@agent-native/dispatch 0.13.10 → 0.13.12

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 (254) hide show
  1. package/dist/actions/navigate.js +1 -1
  2. package/dist/actions/navigate.js.map +1 -1
  3. package/dist/components/agents-panel.js +3 -3
  4. package/dist/components/agents-panel.js.map +1 -1
  5. package/dist/components/app-keys-popover.d.ts.map +1 -1
  6. package/dist/components/app-keys-popover.js +3 -3
  7. package/dist/components/app-keys-popover.js.map +1 -1
  8. package/dist/components/create-app-popover.d.ts.map +1 -1
  9. package/dist/components/create-app-popover.js +2 -2
  10. package/dist/components/create-app-popover.js.map +1 -1
  11. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  12. package/dist/components/dispatch-control-plane.js +19 -7
  13. package/dist/components/dispatch-control-plane.js.map +1 -1
  14. package/dist/components/dispatch-shell.d.ts.map +1 -1
  15. package/dist/components/dispatch-shell.js +2 -2
  16. package/dist/components/dispatch-shell.js.map +1 -1
  17. package/dist/components/layout/Header.js +2 -1
  18. package/dist/components/layout/Header.js.map +1 -1
  19. package/dist/components/layout/Layout.d.ts.map +1 -1
  20. package/dist/components/layout/Layout.js +12 -5
  21. package/dist/components/layout/Layout.js.map +1 -1
  22. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  23. package/dist/components/messaging-setup-panel.js +4 -4
  24. package/dist/components/messaging-setup-panel.js.map +1 -1
  25. package/dist/components/task-queue-health.d.ts +2 -0
  26. package/dist/components/task-queue-health.d.ts.map +1 -0
  27. package/dist/components/task-queue-health.js +49 -0
  28. package/dist/components/task-queue-health.js.map +1 -0
  29. package/dist/components/ui/accordion.js.map +1 -1
  30. package/dist/components/ui/alert-dialog.js +1 -1
  31. package/dist/components/ui/alert-dialog.js.map +1 -1
  32. package/dist/components/ui/alert.js.map +1 -1
  33. package/dist/components/ui/avatar.js.map +1 -1
  34. package/dist/components/ui/badge.js.map +1 -1
  35. package/dist/components/ui/breadcrumb.js.map +1 -1
  36. package/dist/components/ui/button.js.map +1 -1
  37. package/dist/components/ui/calendar.js +1 -1
  38. package/dist/components/ui/calendar.js.map +1 -1
  39. package/dist/components/ui/card.js.map +1 -1
  40. package/dist/components/ui/carousel.d.ts +2 -2
  41. package/dist/components/ui/carousel.d.ts.map +1 -1
  42. package/dist/components/ui/carousel.js +1 -1
  43. package/dist/components/ui/carousel.js.map +1 -1
  44. package/dist/components/ui/chart.js.map +1 -1
  45. package/dist/components/ui/checkbox.js.map +1 -1
  46. package/dist/components/ui/command.js +1 -1
  47. package/dist/components/ui/command.js.map +1 -1
  48. package/dist/components/ui/context-menu.js.map +1 -1
  49. package/dist/components/ui/dialog.js.map +1 -1
  50. package/dist/components/ui/drawer.js.map +1 -1
  51. package/dist/components/ui/dropdown-menu.js.map +1 -1
  52. package/dist/components/ui/form.js +1 -1
  53. package/dist/components/ui/form.js.map +1 -1
  54. package/dist/components/ui/hover-card.js.map +1 -1
  55. package/dist/components/ui/input-otp.js.map +1 -1
  56. package/dist/components/ui/input.js.map +1 -1
  57. package/dist/components/ui/label.js.map +1 -1
  58. package/dist/components/ui/menubar.js.map +1 -1
  59. package/dist/components/ui/navigation-menu.js.map +1 -1
  60. package/dist/components/ui/pagination.d.ts +1 -1
  61. package/dist/components/ui/pagination.d.ts.map +1 -1
  62. package/dist/components/ui/pagination.js +1 -1
  63. package/dist/components/ui/pagination.js.map +1 -1
  64. package/dist/components/ui/popover.js.map +1 -1
  65. package/dist/components/ui/progress.js.map +1 -1
  66. package/dist/components/ui/radio-group.js.map +1 -1
  67. package/dist/components/ui/resizable.js.map +1 -1
  68. package/dist/components/ui/scroll-area.js.map +1 -1
  69. package/dist/components/ui/select.js.map +1 -1
  70. package/dist/components/ui/separator.js.map +1 -1
  71. package/dist/components/ui/sheet.js.map +1 -1
  72. package/dist/components/ui/sidebar.d.ts +2 -2
  73. package/dist/components/ui/sidebar.d.ts.map +1 -1
  74. package/dist/components/ui/sidebar.js +6 -6
  75. package/dist/components/ui/sidebar.js.map +1 -1
  76. package/dist/components/ui/skeleton.js.map +1 -1
  77. package/dist/components/ui/slider.js.map +1 -1
  78. package/dist/components/ui/sonner.js.map +1 -1
  79. package/dist/components/ui/spinner.js.map +1 -1
  80. package/dist/components/ui/switch.js.map +1 -1
  81. package/dist/components/ui/table.js.map +1 -1
  82. package/dist/components/ui/tabs.js.map +1 -1
  83. package/dist/components/ui/textarea.js.map +1 -1
  84. package/dist/components/ui/toast.js.map +1 -1
  85. package/dist/components/ui/toaster.js +1 -1
  86. package/dist/components/ui/toaster.js.map +1 -1
  87. package/dist/components/ui/toggle-group.js +1 -1
  88. package/dist/components/ui/toggle-group.js.map +1 -1
  89. package/dist/components/ui/toggle.js.map +1 -1
  90. package/dist/components/ui/tooltip.js.map +1 -1
  91. package/dist/components/ui/use-toast.d.ts.map +1 -1
  92. package/dist/components/ui/use-toast.js.map +1 -1
  93. package/dist/components/workspace-app-card.d.ts.map +1 -1
  94. package/dist/components/workspace-app-card.js +10 -10
  95. package/dist/components/workspace-app-card.js.map +1 -1
  96. package/dist/components/workspace-resource-effective-stack.js +1 -1
  97. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  98. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -1
  99. package/dist/components/workspace-resource-impact-preview.js +2 -2
  100. package/dist/components/workspace-resource-impact-preview.js.map +1 -1
  101. package/dist/hooks/use-navigation-state.js +5 -0
  102. package/dist/hooks/use-navigation-state.js.map +1 -1
  103. package/dist/hooks/use-toast.d.ts.map +1 -1
  104. package/dist/hooks/use-toast.js.map +1 -1
  105. package/dist/lib/automation-display.d.ts +13 -0
  106. package/dist/lib/automation-display.d.ts.map +1 -0
  107. package/dist/lib/automation-display.js +62 -0
  108. package/dist/lib/automation-display.js.map +1 -0
  109. package/dist/lib/task-queue.d.ts +19 -0
  110. package/dist/lib/task-queue.d.ts.map +1 -0
  111. package/dist/lib/task-queue.js +54 -0
  112. package/dist/lib/task-queue.js.map +1 -0
  113. package/dist/routes/index.d.ts.map +1 -1
  114. package/dist/routes/index.js +1 -0
  115. package/dist/routes/index.js.map +1 -1
  116. package/dist/routes/pages/_index.js.map +1 -1
  117. package/dist/routes/pages/agents.d.ts.map +1 -1
  118. package/dist/routes/pages/agents.js +1 -1
  119. package/dist/routes/pages/agents.js.map +1 -1
  120. package/dist/routes/pages/approval.js.map +1 -1
  121. package/dist/routes/pages/approvals.js.map +1 -1
  122. package/dist/routes/pages/apps.$appId.js.map +1 -1
  123. package/dist/routes/pages/apps.js.map +1 -1
  124. package/dist/routes/pages/audit.js.map +1 -1
  125. package/dist/routes/pages/automations.d.ts +5 -0
  126. package/dist/routes/pages/automations.d.ts.map +1 -0
  127. package/dist/routes/pages/automations.js +106 -0
  128. package/dist/routes/pages/automations.js.map +1 -0
  129. package/dist/routes/pages/chat.js.map +1 -1
  130. package/dist/routes/pages/destinations.d.ts.map +1 -1
  131. package/dist/routes/pages/destinations.js +31 -27
  132. package/dist/routes/pages/destinations.js.map +1 -1
  133. package/dist/routes/pages/dreams.d.ts.map +1 -1
  134. package/dist/routes/pages/dreams.js +1 -1
  135. package/dist/routes/pages/dreams.js.map +1 -1
  136. package/dist/routes/pages/identities.js.map +1 -1
  137. package/dist/routes/pages/integrations.js.map +1 -1
  138. package/dist/routes/pages/messaging.js.map +1 -1
  139. package/dist/routes/pages/metrics.js.map +1 -1
  140. package/dist/routes/pages/new-app.js.map +1 -1
  141. package/dist/routes/pages/overview.d.ts +2 -2
  142. package/dist/routes/pages/overview.js.map +1 -1
  143. package/dist/routes/pages/settings.d.ts.map +1 -1
  144. package/dist/routes/pages/settings.js +2 -2
  145. package/dist/routes/pages/settings.js.map +1 -1
  146. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  147. package/dist/routes/pages/thread-debug.js +1 -1
  148. package/dist/routes/pages/thread-debug.js.map +1 -1
  149. package/dist/routes/pages/vault.d.ts.map +1 -1
  150. package/dist/routes/pages/vault.js +1 -1
  151. package/dist/routes/pages/vault.js.map +1 -1
  152. package/dist/routes/pages/workspace.d.ts.map +1 -1
  153. package/dist/routes/pages/workspace.js +1 -1
  154. package/dist/routes/pages/workspace.js.map +1 -1
  155. package/dist/server/lib/pre-auth-routing.d.ts.map +1 -1
  156. package/dist/server/lib/pre-auth-routing.js +1 -0
  157. package/dist/server/lib/pre-auth-routing.js.map +1 -1
  158. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
  159. package/dist/server/lib/usage-metrics-store.js +12 -3
  160. package/dist/server/lib/usage-metrics-store.js.map +1 -1
  161. package/package.json +7 -7
  162. package/src/actions/navigate.ts +1 -1
  163. package/src/components/agents-panel.tsx +3 -3
  164. package/src/components/app-keys-popover.tsx +3 -7
  165. package/src/components/create-app-popover.tsx +2 -6
  166. package/src/components/dispatch-control-plane.tsx +48 -7
  167. package/src/components/dispatch-shell.tsx +2 -6
  168. package/src/components/layout/Header.tsx +2 -2
  169. package/src/components/layout/Layout.tsx +13 -15
  170. package/src/components/messaging-setup-panel.tsx +4 -8
  171. package/src/components/task-queue-health.tsx +166 -0
  172. package/src/components/ui/accordion.tsx +1 -1
  173. package/src/components/ui/alert-dialog.tsx +2 -2
  174. package/src/components/ui/alert.tsx +1 -1
  175. package/src/components/ui/avatar.tsx +1 -1
  176. package/src/components/ui/badge.tsx +1 -1
  177. package/src/components/ui/breadcrumb.tsx +1 -1
  178. package/src/components/ui/button.tsx +1 -1
  179. package/src/components/ui/calendar.tsx +2 -2
  180. package/src/components/ui/card.tsx +1 -1
  181. package/src/components/ui/carousel.tsx +2 -2
  182. package/src/components/ui/chart.tsx +1 -1
  183. package/src/components/ui/checkbox.tsx +1 -1
  184. package/src/components/ui/command.tsx +2 -2
  185. package/src/components/ui/context-menu.tsx +1 -1
  186. package/src/components/ui/dialog.tsx +1 -1
  187. package/src/components/ui/drawer.tsx +1 -1
  188. package/src/components/ui/dropdown-menu.tsx +1 -1
  189. package/src/components/ui/form.tsx +2 -2
  190. package/src/components/ui/hover-card.tsx +1 -1
  191. package/src/components/ui/input-otp.tsx +1 -1
  192. package/src/components/ui/input.tsx +1 -1
  193. package/src/components/ui/label.tsx +1 -1
  194. package/src/components/ui/menubar.tsx +1 -1
  195. package/src/components/ui/navigation-menu.tsx +1 -1
  196. package/src/components/ui/pagination.tsx +2 -2
  197. package/src/components/ui/popover.tsx +1 -1
  198. package/src/components/ui/progress.tsx +1 -1
  199. package/src/components/ui/radio-group.tsx +1 -1
  200. package/src/components/ui/resizable.tsx +1 -1
  201. package/src/components/ui/scroll-area.tsx +1 -1
  202. package/src/components/ui/select.tsx +1 -1
  203. package/src/components/ui/separator.tsx +1 -1
  204. package/src/components/ui/sheet.tsx +1 -1
  205. package/src/components/ui/sidebar.tsx +8 -13
  206. package/src/components/ui/skeleton.tsx +1 -1
  207. package/src/components/ui/slider.tsx +1 -1
  208. package/src/components/ui/sonner.tsx +1 -1
  209. package/src/components/ui/spinner.tsx +1 -1
  210. package/src/components/ui/switch.tsx +1 -1
  211. package/src/components/ui/table.tsx +1 -1
  212. package/src/components/ui/tabs.tsx +1 -1
  213. package/src/components/ui/textarea.tsx +1 -1
  214. package/src/components/ui/toast.tsx +1 -1
  215. package/src/components/ui/toaster.tsx +2 -2
  216. package/src/components/ui/toggle-group.tsx +2 -2
  217. package/src/components/ui/toggle.tsx +1 -1
  218. package/src/components/ui/tooltip.tsx +1 -1
  219. package/src/components/ui/use-toast.ts +1 -1
  220. package/src/components/workspace-app-card.tsx +16 -20
  221. package/src/components/workspace-resource-effective-stack.tsx +2 -2
  222. package/src/components/workspace-resource-impact-preview.tsx +2 -3
  223. package/src/hooks/use-navigation-state.spec.ts +7 -0
  224. package/src/hooks/use-navigation-state.ts +4 -0
  225. package/src/hooks/use-toast.ts +1 -1
  226. package/src/lib/automation-display.spec.ts +95 -0
  227. package/src/lib/automation-display.ts +70 -0
  228. package/src/lib/task-queue.spec.ts +57 -0
  229. package/src/lib/task-queue.ts +72 -0
  230. package/src/routes/index.ts +1 -0
  231. package/src/routes/pages/_index.tsx +1 -1
  232. package/src/routes/pages/agents.tsx +8 -5
  233. package/src/routes/pages/approval.tsx +4 -4
  234. package/src/routes/pages/approvals.tsx +4 -4
  235. package/src/routes/pages/apps.$appId.tsx +5 -5
  236. package/src/routes/pages/apps.tsx +8 -8
  237. package/src/routes/pages/audit.tsx +1 -1
  238. package/src/routes/pages/automations.tsx +280 -0
  239. package/src/routes/pages/chat.tsx +1 -1
  240. package/src/routes/pages/destinations.tsx +163 -154
  241. package/src/routes/pages/dreams.tsx +22 -18
  242. package/src/routes/pages/identities.tsx +2 -2
  243. package/src/routes/pages/integrations.tsx +8 -8
  244. package/src/routes/pages/messaging.tsx +2 -2
  245. package/src/routes/pages/metrics.tsx +6 -6
  246. package/src/routes/pages/new-app.tsx +1 -1
  247. package/src/routes/pages/overview.tsx +1 -1
  248. package/src/routes/pages/settings.tsx +37 -6
  249. package/src/routes/pages/thread-debug.tsx +14 -9
  250. package/src/routes/pages/vault.tsx +17 -12
  251. package/src/routes/pages/workspace.tsx +17 -12
  252. package/src/server/lib/pre-auth-routing.ts +1 -0
  253. package/src/server/lib/usage-metrics-store.spec.ts +102 -0
  254. package/src/server/lib/usage-metrics-store.ts +12 -3
@@ -0,0 +1,280 @@
1
+ import {
2
+ PromptComposer,
3
+ sendToAgentChat,
4
+ useChangeVersions,
5
+ } from "@agent-native/core/client";
6
+ import { IconPlus, IconSettingsAutomation } from "@tabler/icons-react";
7
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
8
+ import { useMemo, useState } from "react";
9
+ import { toast } from "sonner";
10
+
11
+ import { DispatchShell } from "../../components/dispatch-shell";
12
+ import { Badge } from "../../components/ui/badge";
13
+ import { Button } from "../../components/ui/button";
14
+ import {
15
+ Popover,
16
+ PopoverContent,
17
+ PopoverTrigger,
18
+ } from "../../components/ui/popover";
19
+ import { Skeleton } from "../../components/ui/skeleton";
20
+ import { Switch } from "../../components/ui/switch";
21
+ import {
22
+ automationIdentity,
23
+ automationLastRun,
24
+ automationNextRun,
25
+ automationStatus,
26
+ automationTarget,
27
+ sortAutomations,
28
+ type AutomationStatusTone,
29
+ } from "../../lib/automation-display";
30
+ import {
31
+ listDispatchAutomations,
32
+ setDispatchAutomationEnabled,
33
+ type DispatchAutomationItem,
34
+ type SetDispatchAutomationEnabledInput,
35
+ } from "../../lib/automations";
36
+ import { cn } from "../../lib/utils";
37
+
38
+ const AUTOMATIONS_QUERY_KEY = ["dispatch-automations"] as const;
39
+
40
+ export function meta() {
41
+ return [{ title: "Automations — Dispatch" }];
42
+ }
43
+
44
+ function StatusDot({ tone }: { tone: AutomationStatusTone }) {
45
+ return (
46
+ <span
47
+ className={cn(
48
+ "size-2 shrink-0 rounded-full",
49
+ tone === "success" && "bg-emerald-500",
50
+ tone === "warning" && "bg-amber-500",
51
+ tone === "danger" && "bg-destructive",
52
+ tone === "muted" && "bg-muted-foreground/35",
53
+ tone === "default" && "bg-primary",
54
+ )}
55
+ />
56
+ );
57
+ }
58
+
59
+ function useAutomations() {
60
+ const version = useChangeVersions(["action", "screen-refresh"]);
61
+ return useQuery<DispatchAutomationItem[]>({
62
+ queryKey: [...AUTOMATIONS_QUERY_KEY, version],
63
+ queryFn: listDispatchAutomations,
64
+ placeholderData: (prev) => prev,
65
+ refetchInterval: 15_000,
66
+ staleTime: 5_000,
67
+ });
68
+ }
69
+
70
+ function useToggleAutomation() {
71
+ const queryClient = useQueryClient();
72
+
73
+ return useMutation({
74
+ mutationFn: setDispatchAutomationEnabled,
75
+ onMutate: async (input: SetDispatchAutomationEnabledInput) => {
76
+ await queryClient.cancelQueries({ queryKey: AUTOMATIONS_QUERY_KEY });
77
+ const snapshots = queryClient.getQueriesData<DispatchAutomationItem[]>({
78
+ queryKey: AUTOMATIONS_QUERY_KEY,
79
+ });
80
+
81
+ queryClient.setQueriesData<DispatchAutomationItem[]>(
82
+ { queryKey: AUTOMATIONS_QUERY_KEY },
83
+ (rows) =>
84
+ rows?.map((item) =>
85
+ automationIdentity(item) === automationIdentity(input)
86
+ ? { ...item, enabled: input.enabled }
87
+ : item,
88
+ ),
89
+ );
90
+
91
+ return { snapshots };
92
+ },
93
+ onError: (err, _input, context) => {
94
+ for (const [queryKey, data] of context?.snapshots ?? []) {
95
+ queryClient.setQueryData(queryKey, data);
96
+ }
97
+ toast.error(
98
+ `Could not update automation: ${
99
+ err instanceof Error ? err.message : String(err)
100
+ }`,
101
+ );
102
+ },
103
+ onSuccess: (updated) => {
104
+ queryClient.setQueriesData<DispatchAutomationItem[]>(
105
+ { queryKey: AUTOMATIONS_QUERY_KEY },
106
+ (rows) =>
107
+ rows?.map((item) =>
108
+ automationIdentity(item) === automationIdentity(updated)
109
+ ? updated
110
+ : item,
111
+ ),
112
+ );
113
+ },
114
+ onSettled: () => {
115
+ void queryClient.invalidateQueries({ queryKey: AUTOMATIONS_QUERY_KEY });
116
+ },
117
+ });
118
+ }
119
+
120
+ function CreateAutomationButton() {
121
+ const [open, setOpen] = useState(false);
122
+ const [scope, setScope] = useState<"personal" | "organization">("personal");
123
+
124
+ function handleSubmit(text: string) {
125
+ const trimmed = text.trim();
126
+ if (!trimmed) return;
127
+ window.dispatchEvent(
128
+ new CustomEvent("agent-panel:set-mode", {
129
+ detail: { mode: "chat" },
130
+ }),
131
+ );
132
+ sendToAgentChat({
133
+ message: trimmed,
134
+ context: `The user wants to create a new automation. Scope: ${scope}. Use manage-automations with action=define to create it. Ask clarifying questions if needed about what event to trigger on, conditions, and what actions to take.`,
135
+ submit: true,
136
+ newTab: true,
137
+ });
138
+ setOpen(false);
139
+ }
140
+
141
+ return (
142
+ <Popover open={open} onOpenChange={setOpen}>
143
+ <PopoverTrigger asChild>
144
+ <Button size="sm">
145
+ <IconPlus size={14} />
146
+ New automation
147
+ </Button>
148
+ </PopoverTrigger>
149
+ <PopoverContent align="end" className="w-[min(100vw-2rem,24rem)] p-3">
150
+ <p className="pb-2 text-sm font-semibold text-foreground">
151
+ New automation
152
+ </p>
153
+ <PromptComposer
154
+ autoFocus
155
+ placeholder="Describe what you want to automate..."
156
+ draftScope="dispatch-automations:create"
157
+ onSubmit={handleSubmit}
158
+ />
159
+ <select
160
+ value={scope}
161
+ onChange={(event) =>
162
+ setScope(event.target.value as "personal" | "organization")
163
+ }
164
+ className="mt-2 w-full cursor-pointer rounded-md border border-input bg-background px-3 py-1.5 text-xs text-foreground"
165
+ >
166
+ <option value="personal">Personal</option>
167
+ <option value="organization">Organization</option>
168
+ </select>
169
+ </PopoverContent>
170
+ </Popover>
171
+ );
172
+ }
173
+
174
+ export default function AutomationsRoute() {
175
+ const automationsQuery = useAutomations();
176
+ const toggleAutomation = useToggleAutomation();
177
+ const automations = automationsQuery.data ?? [];
178
+ const ordered = useMemo(() => sortAutomations(automations), [automations]);
179
+ const enabledCount = automations.filter((item) => item.enabled).length;
180
+ const errorCount = automations.filter(
181
+ (item) => item.enabled && item.lastStatus === "error",
182
+ ).length;
183
+ const pendingToggleIdentity = toggleAutomation.isPending
184
+ ? toggleAutomation.variables
185
+ ? automationIdentity(toggleAutomation.variables)
186
+ : null
187
+ : null;
188
+
189
+ return (
190
+ <DispatchShell
191
+ title="Automations"
192
+ description="See scheduled and event-triggered jobs, pause them, or ask the agent to create one."
193
+ >
194
+ <section className="flex flex-col gap-4">
195
+ <div className="flex flex-wrap items-center justify-between gap-3">
196
+ <div className="flex min-w-0 items-center gap-2 text-sm text-muted-foreground">
197
+ <IconSettingsAutomation size={16} className="shrink-0" />
198
+ <span>
199
+ {enabledCount} enabled
200
+ {errorCount > 0 ? ` · ${errorCount} errors` : ""}
201
+ </span>
202
+ </div>
203
+ <CreateAutomationButton />
204
+ </div>
205
+
206
+ <div className="divide-y rounded-lg border bg-card">
207
+ {automationsQuery.isLoading && ordered.length === 0 ? (
208
+ Array.from({ length: 4 }).map((_, index) => (
209
+ <div key={index} className="px-4 py-3">
210
+ <Skeleton className="h-4 w-40" />
211
+ <Skeleton className="mt-2 h-3 w-28" />
212
+ </div>
213
+ ))
214
+ ) : ordered.length > 0 ? (
215
+ ordered.map((item) => {
216
+ const status = automationStatus(item);
217
+ const canUpdate = item.canUpdate !== false;
218
+ const isToggling =
219
+ pendingToggleIdentity === automationIdentity(item);
220
+ return (
221
+ <div
222
+ key={item.id}
223
+ className="grid grid-cols-[minmax(0,1fr)_auto] gap-3 px-4 py-3"
224
+ >
225
+ <div className="min-w-0">
226
+ <div className="flex min-w-0 items-center gap-2">
227
+ <StatusDot tone={status.tone} />
228
+ <span className="truncate text-sm font-medium text-foreground">
229
+ {item.name}
230
+ </span>
231
+ </div>
232
+ <div className="mt-1 truncate text-xs text-muted-foreground">
233
+ {automationTarget(item)}
234
+ </div>
235
+ <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
236
+ <span>Last {automationLastRun(item)}</span>
237
+ <span>Next {automationNextRun(item)}</span>
238
+ </div>
239
+ {item.lastError ? (
240
+ <div className="mt-1 truncate text-xs text-destructive">
241
+ {item.lastError}
242
+ </div>
243
+ ) : null}
244
+ </div>
245
+ <div className="flex flex-col items-end gap-2">
246
+ <Badge
247
+ variant={
248
+ status.tone === "danger" ? "destructive" : "outline"
249
+ }
250
+ className="h-5"
251
+ >
252
+ {status.label}
253
+ </Badge>
254
+ <Switch
255
+ checked={!!item.enabled}
256
+ disabled={!canUpdate || isToggling}
257
+ aria-label={`${item.enabled ? "Disable" : "Enable"} automation ${item.name}`}
258
+ onCheckedChange={(checked) =>
259
+ toggleAutomation.mutate({
260
+ owner: item.owner,
261
+ path: item.path,
262
+ enabled: checked,
263
+ })
264
+ }
265
+ />
266
+ </div>
267
+ </div>
268
+ );
269
+ })
270
+ ) : (
271
+ <div className="px-4 py-10 text-center text-sm text-muted-foreground">
272
+ No automations yet. Create one here, or ask Dispatch to set up a
273
+ scheduled or event-triggered job.
274
+ </div>
275
+ )}
276
+ </div>
277
+ </section>
278
+ </DispatchShell>
279
+ );
280
+ }
@@ -13,7 +13,7 @@ import {
13
13
  } from "react";
14
14
  import { useLocation, useNavigate } from "react-router";
15
15
 
16
- import { submitOverviewPrompt } from "@/lib/overview-chat";
16
+ import { submitOverviewPrompt } from "../../lib/overview-chat";
17
17
 
18
18
  function chatThreadPath(threadId: string | null): string {
19
19
  return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/chat";
@@ -6,7 +6,8 @@ import {
6
6
  import { useState } from "react";
7
7
  import { toast } from "sonner";
8
8
 
9
- import { DispatchShell } from "@/components/dispatch-shell";
9
+ import { DispatchShell } from "../../components/dispatch-shell";
10
+ import { TaskQueueHealth } from "../../components/task-queue-health";
10
11
  import {
11
12
  AlertDialog,
12
13
  AlertDialogAction,
@@ -17,17 +18,17 @@ import {
17
18
  AlertDialogHeader,
18
19
  AlertDialogTitle,
19
20
  AlertDialogTrigger,
20
- } from "@/components/ui/alert-dialog";
21
- import { Button } from "@/components/ui/button";
22
- import { Input } from "@/components/ui/input";
21
+ } from "../../components/ui/alert-dialog";
22
+ import { Button } from "../../components/ui/button";
23
+ import { Input } from "../../components/ui/input";
23
24
  import {
24
25
  Select,
25
26
  SelectContent,
26
27
  SelectItem,
27
28
  SelectTrigger,
28
29
  SelectValue,
29
- } from "@/components/ui/select";
30
- import { Textarea } from "@/components/ui/textarea";
30
+ } from "../../components/ui/select";
31
+ import { Textarea } from "../../components/ui/textarea";
31
32
 
32
33
  export function meta() {
33
34
  return [{ title: "Destinations — Dispatch" }];
@@ -107,158 +108,166 @@ export default function DestinationsRoute() {
107
108
  title={t("dispatch.nav.destinations")}
108
109
  description={t("dispatch.pages.destinationsDescription")}
109
110
  >
110
- <div className="grid gap-4 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
111
- <section className="rounded-2xl border bg-card p-5">
112
- <h2 className="text-lg font-semibold text-foreground">
113
- {t("dispatch.pages.savedDestinations")}
114
- </h2>
115
- <div className="mt-4 space-y-3">
116
- {(data || []).map((destination: any) => (
117
- <div
118
- key={destination.id}
119
- className="rounded-xl border bg-muted/30 p-4"
120
- >
121
- <div className="flex items-start justify-between gap-3">
122
- <div>
123
- <div className="text-sm font-medium text-foreground">
124
- {destination.name}
125
- </div>
126
- <div className="mt-1 text-xs text-muted-foreground">
127
- {destination.platform} · {destination.destination}
128
- {destination.threadRef
129
- ? ` · thread ${destination.threadRef}`
130
- : ""}
111
+ <div className="flex flex-col gap-4">
112
+ <TaskQueueHealth />
113
+ <div className="grid gap-4 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
114
+ <section className="rounded-2xl border bg-card p-5">
115
+ <h2 className="text-lg font-semibold text-foreground">
116
+ {t("dispatch.pages.savedDestinations")}
117
+ </h2>
118
+ <div className="mt-4 space-y-3">
119
+ {(data || []).map((destination: any) => (
120
+ <div
121
+ key={destination.id}
122
+ className="rounded-xl border bg-muted/30 p-4"
123
+ >
124
+ <div className="flex items-start justify-between gap-3">
125
+ <div>
126
+ <div className="text-sm font-medium text-foreground">
127
+ {destination.name}
128
+ </div>
129
+ <div className="mt-1 text-xs text-muted-foreground">
130
+ {destination.platform} · {destination.destination}
131
+ {destination.threadRef
132
+ ? ` · thread ${destination.threadRef}`
133
+ : ""}
134
+ </div>
135
+ {destination.notes && (
136
+ <p className="mt-2 text-sm text-muted-foreground">
137
+ {destination.notes}
138
+ </p>
139
+ )}
131
140
  </div>
132
- {destination.notes && (
133
- <p className="mt-2 text-sm text-muted-foreground">
134
- {destination.notes}
135
- </p>
136
- )}
137
- </div>
138
- <AlertDialog>
139
- <AlertDialogTrigger asChild>
140
- <Button variant="outline" size="sm">
141
- {t("dispatch.pages.delete")}
142
- </Button>
143
- </AlertDialogTrigger>
144
- <AlertDialogContent>
145
- <AlertDialogHeader>
146
- <AlertDialogTitle>
147
- {t("dispatch.pages.deleteDestinationTitle")}
148
- </AlertDialogTitle>
149
- <AlertDialogDescription>
150
- {t("dispatch.pages.deleteDestinationDescription", {
151
- name: destination.name,
152
- })}
153
- </AlertDialogDescription>
154
- </AlertDialogHeader>
155
- <AlertDialogFooter>
156
- <AlertDialogCancel>
157
- {t("dispatch.pages.cancel")}
158
- </AlertDialogCancel>
159
- <AlertDialogAction
160
- onClick={() => remove.mutate({ id: destination.id })}
161
- >
141
+ <AlertDialog>
142
+ <AlertDialogTrigger asChild>
143
+ <Button variant="outline" size="sm">
162
144
  {t("dispatch.pages.delete")}
163
- </AlertDialogAction>
164
- </AlertDialogFooter>
165
- </AlertDialogContent>
166
- </AlertDialog>
145
+ </Button>
146
+ </AlertDialogTrigger>
147
+ <AlertDialogContent>
148
+ <AlertDialogHeader>
149
+ <AlertDialogTitle>
150
+ {t("dispatch.pages.deleteDestinationTitle")}
151
+ </AlertDialogTitle>
152
+ <AlertDialogDescription>
153
+ {t("dispatch.pages.deleteDestinationDescription", {
154
+ name: destination.name,
155
+ })}
156
+ </AlertDialogDescription>
157
+ </AlertDialogHeader>
158
+ <AlertDialogFooter>
159
+ <AlertDialogCancel>
160
+ {t("dispatch.pages.cancel")}
161
+ </AlertDialogCancel>
162
+ <AlertDialogAction
163
+ onClick={() =>
164
+ remove.mutate({ id: destination.id })
165
+ }
166
+ >
167
+ {t("dispatch.pages.delete")}
168
+ </AlertDialogAction>
169
+ </AlertDialogFooter>
170
+ </AlertDialogContent>
171
+ </AlertDialog>
172
+ </div>
173
+ <QuickSendRow destination={destination} />
167
174
  </div>
168
- <QuickSendRow destination={destination} />
169
- </div>
170
- ))}
171
- {(data?.length || 0) === 0 && (
172
- <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
173
- {t("dispatch.pages.noDestinations")}
174
- </div>
175
- )}
176
- </div>
177
- </section>
175
+ ))}
176
+ {(data?.length || 0) === 0 && (
177
+ <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
178
+ {t("dispatch.pages.noDestinations")}
179
+ </div>
180
+ )}
181
+ </div>
182
+ </section>
178
183
 
179
- <section className="rounded-2xl border bg-card p-5">
180
- <h2 className="text-lg font-semibold text-foreground">
181
- {t("dispatch.pages.addDestination")}
182
- </h2>
183
- <div className="mt-4 space-y-3">
184
- <Input
185
- value={form.name}
186
- onChange={(event) =>
187
- setForm((current) => ({ ...current, name: event.target.value }))
188
- }
189
- placeholder={t("dispatch.pages.dailyDigestChannel")}
190
- />
191
- <Select
192
- value={form.platform}
193
- onValueChange={(value) =>
194
- setForm((current) => ({
195
- ...current,
196
- platform: value,
197
- }))
198
- }
199
- >
200
- <SelectTrigger>
201
- <SelectValue />
202
- </SelectTrigger>
203
- <SelectContent>
204
- <SelectItem value="slack">Slack</SelectItem>
205
- <SelectItem value="telegram">Telegram</SelectItem>
206
- <SelectItem value="email">Email</SelectItem>
207
- </SelectContent>
208
- </Select>
209
- <Input
210
- value={form.destination}
211
- onChange={(event) =>
212
- setForm((current) => ({
213
- ...current,
214
- destination: event.target.value,
215
- }))
216
- }
217
- placeholder={
218
- form.platform === "slack"
219
- ? "C0123456789"
220
- : form.platform === "email"
221
- ? "teammate+qa@agent-native.test"
222
- : "123456789"
223
- }
224
- />
225
- <Input
226
- value={form.threadRef}
227
- onChange={(event) =>
228
- setForm((current) => ({
229
- ...current,
230
- threadRef: event.target.value,
231
- }))
232
- }
233
- placeholder={t("dispatch.pages.optionalThreadOrTopicId")}
234
- />
235
- <Textarea
236
- value={form.notes}
237
- onChange={(event) =>
238
- setForm((current) => ({
239
- ...current,
240
- notes: event.target.value,
241
- }))
242
- }
243
- placeholder={t("dispatch.pages.destinationNotes")}
244
- />
245
- <Button
246
- className="w-full"
247
- onClick={() =>
248
- upsert.mutate({
249
- name: form.name,
250
- platform: form.platform as "slack" | "telegram" | "email",
251
- destination: form.destination,
252
- threadRef: form.threadRef || undefined,
253
- notes: form.notes || undefined,
254
- })
255
- }
256
- disabled={!form.name || !form.destination}
257
- >
258
- {t("dispatch.pages.saveDestination")}
259
- </Button>
260
- </div>
261
- </section>
184
+ <section className="rounded-2xl border bg-card p-5">
185
+ <h2 className="text-lg font-semibold text-foreground">
186
+ {t("dispatch.pages.addDestination")}
187
+ </h2>
188
+ <div className="mt-4 space-y-3">
189
+ <Input
190
+ value={form.name}
191
+ onChange={(event) =>
192
+ setForm((current) => ({
193
+ ...current,
194
+ name: event.target.value,
195
+ }))
196
+ }
197
+ placeholder={t("dispatch.pages.dailyDigestChannel")}
198
+ />
199
+ <Select
200
+ value={form.platform}
201
+ onValueChange={(value) =>
202
+ setForm((current) => ({
203
+ ...current,
204
+ platform: value,
205
+ }))
206
+ }
207
+ >
208
+ <SelectTrigger>
209
+ <SelectValue />
210
+ </SelectTrigger>
211
+ <SelectContent>
212
+ <SelectItem value="slack">Slack</SelectItem>
213
+ <SelectItem value="telegram">Telegram</SelectItem>
214
+ <SelectItem value="email">Email</SelectItem>
215
+ </SelectContent>
216
+ </Select>
217
+ <Input
218
+ value={form.destination}
219
+ onChange={(event) =>
220
+ setForm((current) => ({
221
+ ...current,
222
+ destination: event.target.value,
223
+ }))
224
+ }
225
+ placeholder={
226
+ form.platform === "slack"
227
+ ? "C0123456789"
228
+ : form.platform === "email"
229
+ ? "teammate+qa@agent-native.test"
230
+ : "123456789"
231
+ }
232
+ />
233
+ <Input
234
+ value={form.threadRef}
235
+ onChange={(event) =>
236
+ setForm((current) => ({
237
+ ...current,
238
+ threadRef: event.target.value,
239
+ }))
240
+ }
241
+ placeholder={t("dispatch.pages.optionalThreadOrTopicId")}
242
+ />
243
+ <Textarea
244
+ value={form.notes}
245
+ onChange={(event) =>
246
+ setForm((current) => ({
247
+ ...current,
248
+ notes: event.target.value,
249
+ }))
250
+ }
251
+ placeholder={t("dispatch.pages.destinationNotes")}
252
+ />
253
+ <Button
254
+ className="w-full"
255
+ onClick={() =>
256
+ upsert.mutate({
257
+ name: form.name,
258
+ platform: form.platform as "slack" | "telegram" | "email",
259
+ destination: form.destination,
260
+ threadRef: form.threadRef || undefined,
261
+ notes: form.notes || undefined,
262
+ })
263
+ }
264
+ disabled={!form.name || !form.destination}
265
+ >
266
+ {t("dispatch.pages.saveDestination")}
267
+ </Button>
268
+ </div>
269
+ </section>
270
+ </div>
262
271
  </div>
263
272
  </DispatchShell>
264
273
  );