@agent-native/dispatch 0.13.9 → 0.13.11

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 (261) hide show
  1. package/README.md +7 -28
  2. package/dist/actions/navigate.js +1 -1
  3. package/dist/actions/navigate.js.map +1 -1
  4. package/dist/components/agents-panel.js +3 -3
  5. package/dist/components/agents-panel.js.map +1 -1
  6. package/dist/components/app-keys-popover.d.ts.map +1 -1
  7. package/dist/components/app-keys-popover.js +3 -3
  8. package/dist/components/app-keys-popover.js.map +1 -1
  9. package/dist/components/create-app-popover.d.ts.map +1 -1
  10. package/dist/components/create-app-popover.js +2 -2
  11. package/dist/components/create-app-popover.js.map +1 -1
  12. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  13. package/dist/components/dispatch-control-plane.js +19 -7
  14. package/dist/components/dispatch-control-plane.js.map +1 -1
  15. package/dist/components/dispatch-shell.d.ts.map +1 -1
  16. package/dist/components/dispatch-shell.js +2 -2
  17. package/dist/components/dispatch-shell.js.map +1 -1
  18. package/dist/components/layout/Header.js +2 -1
  19. package/dist/components/layout/Header.js.map +1 -1
  20. package/dist/components/layout/Layout.d.ts.map +1 -1
  21. package/dist/components/layout/Layout.js +12 -5
  22. package/dist/components/layout/Layout.js.map +1 -1
  23. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  24. package/dist/components/messaging-setup-panel.js +4 -4
  25. package/dist/components/messaging-setup-panel.js.map +1 -1
  26. package/dist/components/task-queue-health.d.ts +2 -0
  27. package/dist/components/task-queue-health.d.ts.map +1 -0
  28. package/dist/components/task-queue-health.js +49 -0
  29. package/dist/components/task-queue-health.js.map +1 -0
  30. package/dist/components/ui/accordion.js.map +1 -1
  31. package/dist/components/ui/alert-dialog.js +1 -1
  32. package/dist/components/ui/alert-dialog.js.map +1 -1
  33. package/dist/components/ui/alert.js.map +1 -1
  34. package/dist/components/ui/avatar.js.map +1 -1
  35. package/dist/components/ui/badge.js.map +1 -1
  36. package/dist/components/ui/breadcrumb.js.map +1 -1
  37. package/dist/components/ui/button.js.map +1 -1
  38. package/dist/components/ui/calendar.js +1 -1
  39. package/dist/components/ui/calendar.js.map +1 -1
  40. package/dist/components/ui/card.js.map +1 -1
  41. package/dist/components/ui/carousel.d.ts +2 -2
  42. package/dist/components/ui/carousel.d.ts.map +1 -1
  43. package/dist/components/ui/carousel.js +1 -1
  44. package/dist/components/ui/carousel.js.map +1 -1
  45. package/dist/components/ui/chart.js.map +1 -1
  46. package/dist/components/ui/checkbox.js.map +1 -1
  47. package/dist/components/ui/command.js +1 -1
  48. package/dist/components/ui/command.js.map +1 -1
  49. package/dist/components/ui/context-menu.js.map +1 -1
  50. package/dist/components/ui/dialog.js.map +1 -1
  51. package/dist/components/ui/drawer.js.map +1 -1
  52. package/dist/components/ui/dropdown-menu.js.map +1 -1
  53. package/dist/components/ui/form.js +1 -1
  54. package/dist/components/ui/form.js.map +1 -1
  55. package/dist/components/ui/hover-card.js.map +1 -1
  56. package/dist/components/ui/input-otp.js.map +1 -1
  57. package/dist/components/ui/input.js.map +1 -1
  58. package/dist/components/ui/label.js.map +1 -1
  59. package/dist/components/ui/menubar.js.map +1 -1
  60. package/dist/components/ui/navigation-menu.js.map +1 -1
  61. package/dist/components/ui/pagination.d.ts +1 -1
  62. package/dist/components/ui/pagination.d.ts.map +1 -1
  63. package/dist/components/ui/pagination.js +1 -1
  64. package/dist/components/ui/pagination.js.map +1 -1
  65. package/dist/components/ui/popover.js.map +1 -1
  66. package/dist/components/ui/progress.js.map +1 -1
  67. package/dist/components/ui/radio-group.js.map +1 -1
  68. package/dist/components/ui/resizable.js.map +1 -1
  69. package/dist/components/ui/scroll-area.js.map +1 -1
  70. package/dist/components/ui/select.js.map +1 -1
  71. package/dist/components/ui/separator.js.map +1 -1
  72. package/dist/components/ui/sheet.js.map +1 -1
  73. package/dist/components/ui/sidebar.d.ts +2 -2
  74. package/dist/components/ui/sidebar.d.ts.map +1 -1
  75. package/dist/components/ui/sidebar.js +6 -6
  76. package/dist/components/ui/sidebar.js.map +1 -1
  77. package/dist/components/ui/skeleton.js.map +1 -1
  78. package/dist/components/ui/slider.js.map +1 -1
  79. package/dist/components/ui/sonner.js.map +1 -1
  80. package/dist/components/ui/spinner.js.map +1 -1
  81. package/dist/components/ui/switch.js.map +1 -1
  82. package/dist/components/ui/table.js.map +1 -1
  83. package/dist/components/ui/tabs.js.map +1 -1
  84. package/dist/components/ui/textarea.js.map +1 -1
  85. package/dist/components/ui/toast.js.map +1 -1
  86. package/dist/components/ui/toaster.js +1 -1
  87. package/dist/components/ui/toaster.js.map +1 -1
  88. package/dist/components/ui/toggle-group.js +1 -1
  89. package/dist/components/ui/toggle-group.js.map +1 -1
  90. package/dist/components/ui/toggle.js.map +1 -1
  91. package/dist/components/ui/tooltip.js.map +1 -1
  92. package/dist/components/ui/use-toast.d.ts.map +1 -1
  93. package/dist/components/ui/use-toast.js.map +1 -1
  94. package/dist/components/workspace-app-card.d.ts.map +1 -1
  95. package/dist/components/workspace-app-card.js +10 -10
  96. package/dist/components/workspace-app-card.js.map +1 -1
  97. package/dist/components/workspace-resource-effective-stack.js +1 -1
  98. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  99. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -1
  100. package/dist/components/workspace-resource-impact-preview.js +2 -2
  101. package/dist/components/workspace-resource-impact-preview.js.map +1 -1
  102. package/dist/hooks/use-navigation-state.js +5 -0
  103. package/dist/hooks/use-navigation-state.js.map +1 -1
  104. package/dist/hooks/use-toast.d.ts.map +1 -1
  105. package/dist/hooks/use-toast.js.map +1 -1
  106. package/dist/lib/automation-display.d.ts +13 -0
  107. package/dist/lib/automation-display.d.ts.map +1 -0
  108. package/dist/lib/automation-display.js +62 -0
  109. package/dist/lib/automation-display.js.map +1 -0
  110. package/dist/lib/task-queue.d.ts +19 -0
  111. package/dist/lib/task-queue.d.ts.map +1 -0
  112. package/dist/lib/task-queue.js +54 -0
  113. package/dist/lib/task-queue.js.map +1 -0
  114. package/dist/routes/index.d.ts.map +1 -1
  115. package/dist/routes/index.js +1 -0
  116. package/dist/routes/index.js.map +1 -1
  117. package/dist/routes/pages/$appId.js.map +1 -1
  118. package/dist/routes/pages/_index.js.map +1 -1
  119. package/dist/routes/pages/agents.d.ts.map +1 -1
  120. package/dist/routes/pages/agents.js +1 -1
  121. package/dist/routes/pages/agents.js.map +1 -1
  122. package/dist/routes/pages/approval.js.map +1 -1
  123. package/dist/routes/pages/approvals.js.map +1 -1
  124. package/dist/routes/pages/apps.$appId.js.map +1 -1
  125. package/dist/routes/pages/apps.js.map +1 -1
  126. package/dist/routes/pages/audit.js.map +1 -1
  127. package/dist/routes/pages/automations.d.ts +5 -0
  128. package/dist/routes/pages/automations.d.ts.map +1 -0
  129. package/dist/routes/pages/automations.js +106 -0
  130. package/dist/routes/pages/automations.js.map +1 -0
  131. package/dist/routes/pages/chat.js.map +1 -1
  132. package/dist/routes/pages/destinations.d.ts.map +1 -1
  133. package/dist/routes/pages/destinations.js +31 -27
  134. package/dist/routes/pages/destinations.js.map +1 -1
  135. package/dist/routes/pages/dreams.d.ts.map +1 -1
  136. package/dist/routes/pages/dreams.js +1 -1
  137. package/dist/routes/pages/dreams.js.map +1 -1
  138. package/dist/routes/pages/identities.js.map +1 -1
  139. package/dist/routes/pages/integrations.js.map +1 -1
  140. package/dist/routes/pages/messaging.js.map +1 -1
  141. package/dist/routes/pages/metrics.js.map +1 -1
  142. package/dist/routes/pages/new-app.js.map +1 -1
  143. package/dist/routes/pages/overview.d.ts +2 -2
  144. package/dist/routes/pages/overview.js.map +1 -1
  145. package/dist/routes/pages/settings.d.ts.map +1 -1
  146. package/dist/routes/pages/settings.js +2 -2
  147. package/dist/routes/pages/settings.js.map +1 -1
  148. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  149. package/dist/routes/pages/thread-debug.js +1 -1
  150. package/dist/routes/pages/thread-debug.js.map +1 -1
  151. package/dist/routes/pages/vault.d.ts.map +1 -1
  152. package/dist/routes/pages/vault.js +1 -1
  153. package/dist/routes/pages/vault.js.map +1 -1
  154. package/dist/routes/pages/workspace.d.ts.map +1 -1
  155. package/dist/routes/pages/workspace.js +1 -1
  156. package/dist/routes/pages/workspace.js.map +1 -1
  157. package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
  158. package/dist/server/lib/dispatch-integrations.js +39 -0
  159. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  160. package/dist/server/lib/pre-auth-routing.d.ts.map +1 -1
  161. package/dist/server/lib/pre-auth-routing.js +1 -0
  162. package/dist/server/lib/pre-auth-routing.js.map +1 -1
  163. package/dist/server/plugins/integrations.d.ts.map +1 -1
  164. package/dist/server/plugins/integrations.js +12 -1
  165. package/dist/server/plugins/integrations.js.map +1 -1
  166. package/package.json +7 -7
  167. package/src/actions/navigate.ts +1 -1
  168. package/src/components/agents-panel.tsx +3 -3
  169. package/src/components/app-keys-popover.tsx +3 -7
  170. package/src/components/create-app-popover.tsx +2 -6
  171. package/src/components/dispatch-control-plane.tsx +48 -7
  172. package/src/components/dispatch-shell.tsx +2 -6
  173. package/src/components/layout/Header.tsx +2 -2
  174. package/src/components/layout/Layout.tsx +13 -15
  175. package/src/components/messaging-setup-panel.tsx +4 -8
  176. package/src/components/task-queue-health.tsx +166 -0
  177. package/src/components/ui/accordion.tsx +1 -1
  178. package/src/components/ui/alert-dialog.tsx +2 -2
  179. package/src/components/ui/alert.tsx +1 -1
  180. package/src/components/ui/avatar.tsx +1 -1
  181. package/src/components/ui/badge.tsx +1 -1
  182. package/src/components/ui/breadcrumb.tsx +1 -1
  183. package/src/components/ui/button.tsx +1 -1
  184. package/src/components/ui/calendar.tsx +2 -2
  185. package/src/components/ui/card.tsx +1 -1
  186. package/src/components/ui/carousel.tsx +2 -2
  187. package/src/components/ui/chart.tsx +1 -1
  188. package/src/components/ui/checkbox.tsx +1 -1
  189. package/src/components/ui/command.tsx +2 -2
  190. package/src/components/ui/context-menu.tsx +1 -1
  191. package/src/components/ui/dialog.tsx +1 -1
  192. package/src/components/ui/drawer.tsx +1 -1
  193. package/src/components/ui/dropdown-menu.tsx +1 -1
  194. package/src/components/ui/form.tsx +2 -2
  195. package/src/components/ui/hover-card.tsx +1 -1
  196. package/src/components/ui/input-otp.tsx +1 -1
  197. package/src/components/ui/input.tsx +1 -1
  198. package/src/components/ui/label.tsx +1 -1
  199. package/src/components/ui/menubar.tsx +1 -1
  200. package/src/components/ui/navigation-menu.tsx +1 -1
  201. package/src/components/ui/pagination.tsx +2 -2
  202. package/src/components/ui/popover.tsx +1 -1
  203. package/src/components/ui/progress.tsx +1 -1
  204. package/src/components/ui/radio-group.tsx +1 -1
  205. package/src/components/ui/resizable.tsx +1 -1
  206. package/src/components/ui/scroll-area.tsx +1 -1
  207. package/src/components/ui/select.tsx +1 -1
  208. package/src/components/ui/separator.tsx +1 -1
  209. package/src/components/ui/sheet.tsx +1 -1
  210. package/src/components/ui/sidebar.tsx +8 -13
  211. package/src/components/ui/skeleton.tsx +1 -1
  212. package/src/components/ui/slider.tsx +1 -1
  213. package/src/components/ui/sonner.tsx +1 -1
  214. package/src/components/ui/spinner.tsx +1 -1
  215. package/src/components/ui/switch.tsx +1 -1
  216. package/src/components/ui/table.tsx +1 -1
  217. package/src/components/ui/tabs.tsx +1 -1
  218. package/src/components/ui/textarea.tsx +1 -1
  219. package/src/components/ui/toast.tsx +1 -1
  220. package/src/components/ui/toaster.tsx +2 -2
  221. package/src/components/ui/toggle-group.tsx +2 -2
  222. package/src/components/ui/toggle.tsx +1 -1
  223. package/src/components/ui/tooltip.tsx +1 -1
  224. package/src/components/ui/use-toast.ts +1 -1
  225. package/src/components/workspace-app-card.tsx +16 -20
  226. package/src/components/workspace-resource-effective-stack.tsx +2 -2
  227. package/src/components/workspace-resource-impact-preview.tsx +2 -3
  228. package/src/hooks/use-navigation-state.spec.ts +7 -0
  229. package/src/hooks/use-navigation-state.ts +4 -0
  230. package/src/hooks/use-toast.ts +1 -1
  231. package/src/lib/automation-display.spec.ts +95 -0
  232. package/src/lib/automation-display.ts +70 -0
  233. package/src/lib/task-queue.spec.ts +57 -0
  234. package/src/lib/task-queue.ts +72 -0
  235. package/src/routes/index.ts +1 -0
  236. package/src/routes/pages/$appId.tsx +6 -6
  237. package/src/routes/pages/_index.tsx +1 -1
  238. package/src/routes/pages/agents.tsx +8 -5
  239. package/src/routes/pages/approval.tsx +4 -4
  240. package/src/routes/pages/approvals.tsx +4 -4
  241. package/src/routes/pages/apps.$appId.tsx +5 -5
  242. package/src/routes/pages/apps.tsx +8 -8
  243. package/src/routes/pages/audit.tsx +1 -1
  244. package/src/routes/pages/automations.tsx +280 -0
  245. package/src/routes/pages/chat.tsx +1 -1
  246. package/src/routes/pages/destinations.tsx +163 -154
  247. package/src/routes/pages/dreams.tsx +22 -18
  248. package/src/routes/pages/identities.tsx +2 -2
  249. package/src/routes/pages/integrations.tsx +8 -8
  250. package/src/routes/pages/messaging.tsx +2 -2
  251. package/src/routes/pages/metrics.tsx +6 -6
  252. package/src/routes/pages/new-app.tsx +1 -1
  253. package/src/routes/pages/overview.tsx +1 -1
  254. package/src/routes/pages/settings.tsx +37 -6
  255. package/src/routes/pages/thread-debug.tsx +14 -9
  256. package/src/routes/pages/vault.tsx +17 -12
  257. package/src/routes/pages/workspace.tsx +17 -12
  258. package/src/server/lib/dispatch-integrations.spec.ts +92 -1
  259. package/src/server/lib/dispatch-integrations.ts +48 -0
  260. package/src/server/lib/pre-auth-routing.ts +1 -0
  261. package/src/server/plugins/integrations.ts +13 -1
@@ -0,0 +1,70 @@
1
+ import type { DispatchAutomationItem } from "./automations.js";
2
+
3
+ export type AutomationStatusTone =
4
+ | "default"
5
+ | "success"
6
+ | "warning"
7
+ | "danger"
8
+ | "muted";
9
+
10
+ export function automationIdentity(
11
+ item: Pick<DispatchAutomationItem, "owner" | "path">,
12
+ ): string {
13
+ return `${item.owner}:${item.path}`;
14
+ }
15
+
16
+ export function automationTarget(item: DispatchAutomationItem): string {
17
+ if (item.triggerType === "event" && item.event) return item.event;
18
+ if (item.scheduleDescription) return item.scheduleDescription;
19
+ if (item.schedule) return item.schedule;
20
+ return item.triggerType || "schedule";
21
+ }
22
+
23
+ export function relativeRunTime(value: string | null | undefined): string {
24
+ if (!value) return "never";
25
+ const timestamp = new Date(value).getTime();
26
+ if (!Number.isFinite(timestamp)) return "never";
27
+ const diff = timestamp - Date.now();
28
+ const abs = Math.abs(diff);
29
+ const suffix = diff >= 0 ? "from now" : "ago";
30
+ if (abs < 60_000) return diff >= 0 ? "soon" : "now";
31
+ if (abs < 3_600_000) return `${Math.floor(abs / 60_000)}m ${suffix}`;
32
+ if (abs < 86_400_000) return `${Math.floor(abs / 3_600_000)}h ${suffix}`;
33
+ return `${Math.floor(abs / 86_400_000)}d ${suffix}`;
34
+ }
35
+
36
+ export function automationLastRun(item: DispatchAutomationItem): string {
37
+ return item.lastRun ? relativeRunTime(item.lastRun) : "never";
38
+ }
39
+
40
+ export function automationNextRun(item: DispatchAutomationItem): string {
41
+ if (!item.enabled) return "paused";
42
+ if (item.triggerType === "event") return "on event";
43
+ return item.nextRun ? relativeRunTime(item.nextRun) : "not scheduled";
44
+ }
45
+
46
+ export function automationStatus(item: DispatchAutomationItem): {
47
+ label: string;
48
+ tone: AutomationStatusTone;
49
+ } {
50
+ if (!item.enabled) return { label: "Paused", tone: "muted" };
51
+ if (item.lastStatus === "error") return { label: "Error", tone: "danger" };
52
+ if (item.lastStatus === "running")
53
+ return { label: "Running", tone: "warning" };
54
+ if (item.lastStatus === "skipped")
55
+ return { label: "Skipped", tone: "warning" };
56
+ if (item.lastStatus === "success")
57
+ return { label: "Healthy", tone: "success" };
58
+ return { label: "Ready", tone: "default" };
59
+ }
60
+
61
+ export function sortAutomations(
62
+ automations: DispatchAutomationItem[],
63
+ ): DispatchAutomationItem[] {
64
+ return [...automations].sort((a, b) => {
65
+ const aError = a.enabled && a.lastStatus === "error" ? 1 : 0;
66
+ const bError = b.enabled && b.lastStatus === "error" ? 1 : 0;
67
+ if (aError !== bError) return bError - aError;
68
+ return (b.lastRun || "").localeCompare(a.lastRun || "");
69
+ });
70
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ formatQueueAgeSeconds,
5
+ normalizeTaskQueueStats,
6
+ ZERO_TASK_QUEUE_STATS,
7
+ } from "./task-queue";
8
+
9
+ describe("normalizeTaskQueueStats", () => {
10
+ it("returns zeros for invalid payloads", () => {
11
+ expect(normalizeTaskQueueStats(null)).toEqual(ZERO_TASK_QUEUE_STATS);
12
+ expect(normalizeTaskQueueStats("bad")).toEqual(ZERO_TASK_QUEUE_STATS);
13
+ });
14
+
15
+ it("normalizes numeric fields and recent failures", () => {
16
+ expect(
17
+ normalizeTaskQueueStats({
18
+ pending: "2",
19
+ processing: 1,
20
+ completed_last_hour: 4,
21
+ failed_last_hour: "1",
22
+ oldest_pending_age_seconds: 90,
23
+ recent_failures: [
24
+ {
25
+ id: "f1",
26
+ platform: "slack",
27
+ error: "rate limited",
28
+ attempts: "3",
29
+ },
30
+ ],
31
+ }),
32
+ ).toEqual({
33
+ pending: 2,
34
+ processing: 1,
35
+ completed_last_hour: 4,
36
+ failed_last_hour: 1,
37
+ oldest_pending_age_seconds: 90,
38
+ recent_failures: [
39
+ {
40
+ id: "f1",
41
+ platform: "slack",
42
+ error: "rate limited",
43
+ attempts: 3,
44
+ },
45
+ ],
46
+ });
47
+ });
48
+ });
49
+
50
+ describe("formatQueueAgeSeconds", () => {
51
+ it("formats common ages", () => {
52
+ expect(formatQueueAgeSeconds(0)).toBe("none");
53
+ expect(formatQueueAgeSeconds(12)).toBe("12s");
54
+ expect(formatQueueAgeSeconds(120)).toBe("2m");
55
+ expect(formatQueueAgeSeconds(7200)).toBe("2.0h");
56
+ });
57
+ });
@@ -0,0 +1,72 @@
1
+ import { agentNativePath } from "@agent-native/core/client";
2
+
3
+ export interface TaskQueueFailure {
4
+ id: string;
5
+ platform: string;
6
+ error: string;
7
+ attempts: number;
8
+ }
9
+
10
+ export interface TaskQueueStats {
11
+ pending: number;
12
+ processing: number;
13
+ completed_last_hour: number;
14
+ failed_last_hour: number;
15
+ oldest_pending_age_seconds: number;
16
+ recent_failures: TaskQueueFailure[];
17
+ }
18
+
19
+ export const ZERO_TASK_QUEUE_STATS: TaskQueueStats = {
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
+
28
+ function asNumber(value: unknown): number {
29
+ const n = Number(value ?? 0);
30
+ return Number.isFinite(n) ? n : 0;
31
+ }
32
+
33
+ export function normalizeTaskQueueStats(value: unknown): TaskQueueStats {
34
+ if (!value || typeof value !== "object") return ZERO_TASK_QUEUE_STATS;
35
+ const stats = value as Record<string, unknown>;
36
+ return {
37
+ pending: asNumber(stats.pending),
38
+ processing: asNumber(stats.processing),
39
+ completed_last_hour: asNumber(stats.completed_last_hour),
40
+ failed_last_hour: asNumber(stats.failed_last_hour),
41
+ oldest_pending_age_seconds: asNumber(stats.oldest_pending_age_seconds),
42
+ recent_failures: Array.isArray(stats.recent_failures)
43
+ ? stats.recent_failures.map((failure, index) => {
44
+ const row =
45
+ failure && typeof failure === "object"
46
+ ? (failure as Record<string, unknown>)
47
+ : {};
48
+ return {
49
+ id: String(row.id ?? `failure-${index}`),
50
+ platform: String(row.platform ?? ""),
51
+ error: String(row.error ?? ""),
52
+ attempts: asNumber(row.attempts),
53
+ };
54
+ })
55
+ : [],
56
+ };
57
+ }
58
+
59
+ export async function getDispatchTaskQueueStats(): Promise<TaskQueueStats> {
60
+ const response = await fetch(
61
+ agentNativePath("/_agent-native/integrations/task-queue/status"),
62
+ );
63
+ if (!response.ok) return ZERO_TASK_QUEUE_STATS;
64
+ return normalizeTaskQueueStats(await response.json().catch(() => null));
65
+ }
66
+
67
+ export function formatQueueAgeSeconds(seconds: number): string {
68
+ if (!Number.isFinite(seconds) || seconds <= 0) return "none";
69
+ if (seconds < 60) return `${Math.round(seconds)}s`;
70
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
71
+ return `${(seconds / 3600).toFixed(1)}h`;
72
+ }
@@ -47,6 +47,7 @@ export const dispatchRoutes: RouteConfig = [
47
47
  route("identities", "./pages/identities.js"),
48
48
  route("approval", "./pages/approval.js"),
49
49
  route("approvals", "./pages/approvals.js"),
50
+ route("automations", "./pages/automations.js"),
50
51
  route("audit", "./pages/audit.js"),
51
52
  route("settings", "./pages/settings.js"),
52
53
  route("dreams", "./pages/dreams.js"),
@@ -14,15 +14,15 @@ import {
14
14
  type LoaderFunctionArgs,
15
15
  } from "react-router";
16
16
 
17
- import { DispatchShell } from "@/components/dispatch-shell";
18
- import { Badge } from "@/components/ui/badge";
19
- import { Button } from "@/components/ui/button";
20
- import { Spinner } from "@/components/ui/spinner";
21
- import { resolveServerCatchAllTarget } from "@/lib/catch-all-target";
17
+ import { DispatchShell } from "../../components/dispatch-shell";
18
+ import { Badge } from "../../components/ui/badge";
19
+ import { Button } from "../../components/ui/button";
20
+ import { Spinner } from "../../components/ui/spinner";
21
+ import { resolveServerCatchAllTarget } from "../../lib/catch-all-target";
22
22
  import {
23
23
  workspaceAppHref,
24
24
  type WorkspaceAppSummary,
25
- } from "@/lib/workspace-apps";
25
+ } from "../../lib/workspace-apps";
26
26
 
27
27
  export function meta() {
28
28
  return [{ title: "Workspace app - Dispatch" }];
@@ -1,7 +1,7 @@
1
1
  import { appPath } from "@agent-native/core/client";
2
2
  import { redirect, type LoaderFunctionArgs } from "react-router";
3
3
 
4
- import { Spinner } from "@/components/ui/spinner";
4
+ import { Spinner } from "../../components/ui/spinner";
5
5
 
6
6
  const SEO_TITLE =
7
7
  "Agent-Native Dispatch - Open Source workspace control plane for AI agents";
@@ -8,11 +8,14 @@ import { IconCheck, IconCopy, IconPlugConnected } from "@tabler/icons-react";
8
8
  import { useMemo, useState } from "react";
9
9
  import { toast } from "sonner";
10
10
 
11
- import { AgentsPanel, type ConnectedAgent } from "@/components/agents-panel";
12
- import { DispatchShell } from "@/components/dispatch-shell";
13
- import { Button } from "@/components/ui/button";
14
- import { Input } from "@/components/ui/input";
15
- import { Switch } from "@/components/ui/switch";
11
+ import {
12
+ AgentsPanel,
13
+ type ConnectedAgent,
14
+ } from "../../components/agents-panel";
15
+ import { DispatchShell } from "../../components/dispatch-shell";
16
+ import { Button } from "../../components/ui/button";
17
+ import { Input } from "../../components/ui/input";
18
+ import { Switch } from "../../components/ui/switch";
16
19
 
17
20
  export function meta() {
18
21
  return [{ title: "Agents — Dispatch" }];
@@ -19,10 +19,10 @@ import { toast } from "sonner";
19
19
  import {
20
20
  ApprovalValueBlock,
21
21
  parseApprovalValue,
22
- } from "@/components/approval-value-block";
23
- import { Badge } from "@/components/ui/badge";
24
- import { Button } from "@/components/ui/button";
25
- import { Skeleton } from "@/components/ui/skeleton";
22
+ } from "../../components/approval-value-block";
23
+ import { Badge } from "../../components/ui/badge";
24
+ import { Button } from "../../components/ui/button";
25
+ import { Skeleton } from "../../components/ui/skeleton";
26
26
 
27
27
  export function meta() {
28
28
  return [{ title: "Approval — Dispatch" }];
@@ -3,10 +3,10 @@ import { useOrg } from "@agent-native/core/client/org";
3
3
  import { useMemo, useState } from "react";
4
4
  import { toast } from "sonner";
5
5
 
6
- import { DispatchShell } from "@/components/dispatch-shell";
7
- import { Button } from "@/components/ui/button";
8
- import { Input } from "@/components/ui/input";
9
- import { Switch } from "@/components/ui/switch";
6
+ import { DispatchShell } from "../../components/dispatch-shell";
7
+ import { Button } from "../../components/ui/button";
8
+ import { Input } from "../../components/ui/input";
9
+ import { Switch } from "../../components/ui/switch";
10
10
 
11
11
  export function meta() {
12
12
  return [{ title: "Approvals — Dispatch" }];
@@ -7,14 +7,14 @@ import {
7
7
  import { useEffect, useMemo } from "react";
8
8
  import { Link, useParams } from "react-router";
9
9
 
10
- import { DispatchShell } from "@/components/dispatch-shell";
11
- import { Badge } from "@/components/ui/badge";
12
- import { Button } from "@/components/ui/button";
13
- import { Skeleton } from "@/components/ui/skeleton";
10
+ import { DispatchShell } from "../../components/dispatch-shell";
11
+ import { Badge } from "../../components/ui/badge";
12
+ import { Button } from "../../components/ui/button";
13
+ import { Skeleton } from "../../components/ui/skeleton";
14
14
  import {
15
15
  workspaceAppHref,
16
16
  type WorkspaceAppSummary,
17
- } from "@/lib/workspace-apps";
17
+ } from "../../lib/workspace-apps";
18
18
 
19
19
  export function meta() {
20
20
  return [{ title: "Workspace app - Dispatch" }];
@@ -25,18 +25,18 @@ import {
25
25
  import { useState } from "react";
26
26
  import { toast } from "sonner";
27
27
 
28
- import { CreateAppPopover } from "@/components/create-app-popover";
29
- import { DispatchShell } from "@/components/dispatch-shell";
30
- import { Button } from "@/components/ui/button";
28
+ import { CreateAppPopover } from "../../components/create-app-popover";
29
+ import { DispatchShell } from "../../components/dispatch-shell";
30
+ import { Button } from "../../components/ui/button";
31
31
  import {
32
32
  Collapsible,
33
33
  CollapsibleContent,
34
34
  CollapsibleTrigger,
35
- } from "@/components/ui/collapsible";
36
- import { Skeleton } from "@/components/ui/skeleton";
37
- import { WorkspaceAppCard } from "@/components/workspace-app-card";
38
- import { cn } from "@/lib/utils";
39
- import type { WorkspaceAppSummary } from "@/lib/workspace-apps";
35
+ } from "../../components/ui/collapsible";
36
+ import { Skeleton } from "../../components/ui/skeleton";
37
+ import { WorkspaceAppCard } from "../../components/workspace-app-card";
38
+ import { cn } from "../../lib/utils";
39
+ import type { WorkspaceAppSummary } from "../../lib/workspace-apps";
40
40
 
41
41
  export function meta() {
42
42
  return [{ title: "Apps — Dispatch" }];
@@ -1,6 +1,6 @@
1
1
  import { useActionQuery } from "@agent-native/core/client";
2
2
 
3
- import { DispatchShell } from "@/components/dispatch-shell";
3
+ import { DispatchShell } from "../../components/dispatch-shell";
4
4
 
5
5
  export function meta() {
6
6
  return [{ title: "Audit — Dispatch" }];
@@ -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";