@agent-native/dispatch 0.14.3 → 0.14.5

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 (140) hide show
  1. package/README.md +114 -128
  2. package/dist/actions/list_apps.d.ts +1 -0
  3. package/dist/actions/list_apps.js +16 -8
  4. package/dist/actions/list_apps.js.map +1 -1
  5. package/dist/actions/set-mcp-app-access.js +1 -4
  6. package/dist/actions/set-mcp-app-access.js.map +1 -1
  7. package/dist/components/action-query-error.d.ts +8 -0
  8. package/dist/components/action-query-error.d.ts.map +1 -0
  9. package/dist/components/action-query-error.js +10 -0
  10. package/dist/components/action-query-error.js.map +1 -0
  11. package/dist/components/app-keys-popover.d.ts +4 -0
  12. package/dist/components/app-keys-popover.d.ts.map +1 -1
  13. package/dist/components/app-keys-popover.js +26 -14
  14. package/dist/components/app-keys-popover.js.map +1 -1
  15. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  16. package/dist/components/dispatch-control-plane.js +31 -27
  17. package/dist/components/dispatch-control-plane.js.map +1 -1
  18. package/dist/components/index.d.ts +1 -0
  19. package/dist/components/index.d.ts.map +1 -1
  20. package/dist/components/index.js +1 -0
  21. package/dist/components/index.js.map +1 -1
  22. package/dist/components/layout/Layout.d.ts +1 -0
  23. package/dist/components/layout/Layout.d.ts.map +1 -1
  24. package/dist/components/layout/Layout.js +19 -21
  25. package/dist/components/layout/Layout.js.map +1 -1
  26. package/dist/components/task-queue-health.d.ts.map +1 -1
  27. package/dist/components/task-queue-health.js +9 -1
  28. package/dist/components/task-queue-health.js.map +1 -1
  29. package/dist/components/ui/sheet.js +1 -1
  30. package/dist/components/ui/sheet.js.map +1 -1
  31. package/dist/components/ui/toast.js +1 -1
  32. package/dist/components/ui/toast.js.map +1 -1
  33. package/dist/components/workspace-app-card.d.ts.map +1 -1
  34. package/dist/components/workspace-app-card.js +8 -5
  35. package/dist/components/workspace-app-card.js.map +1 -1
  36. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  37. package/dist/components/workspace-resource-effective-stack.js +6 -1
  38. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  39. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -1
  40. package/dist/components/workspace-resource-impact-preview.js +6 -1
  41. package/dist/components/workspace-resource-impact-preview.js.map +1 -1
  42. package/dist/routes/pages/$appId.d.ts.map +1 -1
  43. package/dist/routes/pages/$appId.js +6 -1
  44. package/dist/routes/pages/$appId.js.map +1 -1
  45. package/dist/routes/pages/agents.d.ts.map +1 -1
  46. package/dist/routes/pages/agents.js +5 -8
  47. package/dist/routes/pages/agents.js.map +1 -1
  48. package/dist/routes/pages/approval.d.ts.map +1 -1
  49. package/dist/routes/pages/approval.js +6 -1
  50. package/dist/routes/pages/approval.js.map +1 -1
  51. package/dist/routes/pages/approvals.d.ts.map +1 -1
  52. package/dist/routes/pages/approvals.js +8 -5
  53. package/dist/routes/pages/approvals.js.map +1 -1
  54. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  55. package/dist/routes/pages/apps.$appId.js +4 -2
  56. package/dist/routes/pages/apps.$appId.js.map +1 -1
  57. package/dist/routes/pages/apps.d.ts.map +1 -1
  58. package/dist/routes/pages/apps.js +10 -42
  59. package/dist/routes/pages/apps.js.map +1 -1
  60. package/dist/routes/pages/audit.d.ts.map +1 -1
  61. package/dist/routes/pages/audit.js +4 -2
  62. package/dist/routes/pages/audit.js.map +1 -1
  63. package/dist/routes/pages/automations.d.ts.map +1 -1
  64. package/dist/routes/pages/automations.js +0 -1
  65. package/dist/routes/pages/automations.js.map +1 -1
  66. package/dist/routes/pages/chat.d.ts.map +1 -1
  67. package/dist/routes/pages/chat.js +16 -2
  68. package/dist/routes/pages/chat.js.map +1 -1
  69. package/dist/routes/pages/destinations.d.ts.map +1 -1
  70. package/dist/routes/pages/destinations.js +5 -3
  71. package/dist/routes/pages/destinations.js.map +1 -1
  72. package/dist/routes/pages/identities.d.ts.map +1 -1
  73. package/dist/routes/pages/identities.js +5 -3
  74. package/dist/routes/pages/identities.js.map +1 -1
  75. package/dist/routes/pages/integrations.d.ts.map +1 -1
  76. package/dist/routes/pages/integrations.js +9 -3
  77. package/dist/routes/pages/integrations.js.map +1 -1
  78. package/dist/routes/pages/metrics.d.ts.map +1 -1
  79. package/dist/routes/pages/metrics.js +1 -1
  80. package/dist/routes/pages/metrics.js.map +1 -1
  81. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  82. package/dist/routes/pages/thread-debug.js +7 -6
  83. package/dist/routes/pages/thread-debug.js.map +1 -1
  84. package/dist/routes/pages/vault.d.ts.map +1 -1
  85. package/dist/routes/pages/vault.js +23 -7
  86. package/dist/routes/pages/vault.js.map +1 -1
  87. package/dist/routes/pages/workspace.d.ts.map +1 -1
  88. package/dist/routes/pages/workspace.js +9 -3
  89. package/dist/routes/pages/workspace.js.map +1 -1
  90. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  91. package/dist/server/lib/dispatch-store.js +2 -2
  92. package/dist/server/lib/dispatch-store.js.map +1 -1
  93. package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
  94. package/dist/server/lib/mcp-access-store.js +30 -2
  95. package/dist/server/lib/mcp-access-store.js.map +1 -1
  96. package/dist/server/plugins/agent-chat.d.ts +1 -1
  97. package/package.json +2 -2
  98. package/src/actions/list_apps.spec.ts +52 -0
  99. package/src/actions/list_apps.ts +16 -8
  100. package/src/actions/set-mcp-app-access.spec.ts +73 -0
  101. package/src/actions/set-mcp-app-access.ts +1 -5
  102. package/src/components/action-query-error.spec.tsx +56 -0
  103. package/src/components/action-query-error.tsx +32 -0
  104. package/src/components/app-keys-popover.spec.tsx +85 -0
  105. package/src/components/app-keys-popover.tsx +76 -50
  106. package/src/components/dispatch-control-plane.spec.tsx +106 -0
  107. package/src/components/dispatch-control-plane.tsx +71 -115
  108. package/src/components/index.ts +1 -0
  109. package/src/components/layout/Layout.spec.tsx +42 -2
  110. package/src/components/layout/Layout.tsx +22 -27
  111. package/src/components/task-queue-health.tsx +9 -1
  112. package/src/components/ui/sheet.tsx +1 -1
  113. package/src/components/ui/toast.tsx +1 -1
  114. package/src/components/workspace-app-card.spec.tsx +80 -0
  115. package/src/components/workspace-app-card.tsx +36 -25
  116. package/src/components/workspace-resource-effective-stack.tsx +13 -1
  117. package/src/components/workspace-resource-impact-preview.tsx +12 -1
  118. package/src/routes/pages/$appId.tsx +17 -1
  119. package/src/routes/pages/agents.tsx +24 -11
  120. package/src/routes/pages/approval.tsx +16 -4
  121. package/src/routes/pages/approvals.tsx +108 -89
  122. package/src/routes/pages/apps.$appId.tsx +9 -2
  123. package/src/routes/pages/apps.tsx +13 -177
  124. package/src/routes/pages/audit.tsx +29 -20
  125. package/src/routes/pages/automations.tsx +0 -1
  126. package/src/routes/pages/chat.spec.tsx +94 -0
  127. package/src/routes/pages/chat.tsx +30 -10
  128. package/src/routes/pages/destinations.tsx +75 -62
  129. package/src/routes/pages/identities.tsx +74 -63
  130. package/src/routes/pages/integrations.tsx +17 -10
  131. package/src/routes/pages/metrics.tsx +0 -1
  132. package/src/routes/pages/thread-debug.tsx +18 -12
  133. package/src/routes/pages/vault.tsx +59 -26
  134. package/src/routes/pages/workspace.tsx +69 -55
  135. package/src/server/lib/approval-fencing.spec.ts +113 -2
  136. package/src/server/lib/dispatch-store.ts +2 -0
  137. package/src/server/lib/mcp-access-store.spec.ts +116 -4
  138. package/src/server/lib/mcp-access-store.ts +37 -2
  139. package/src/server/lib/mcp-gateway.spec.ts +79 -3
  140. package/src/styles/dispatch.css +5 -5
@@ -1,6 +1,5 @@
1
1
  import { useActionMutation, useActionQuery } from "@agent-native/core/client";
2
2
  import {
3
- IconArrowUpRight,
4
3
  IconChevronDown,
5
4
  IconChevronRight,
6
5
  IconClockHour4,
@@ -21,6 +20,7 @@ import {
21
20
  workspaceAppHref,
22
21
  type WorkspaceAppSummary,
23
22
  } from "../lib/workspace-apps";
23
+ import { ActionQueryError } from "./action-query-error";
24
24
  import { AppKeysPopover } from "./app-keys-popover";
25
25
  import { Badge } from "./ui/badge";
26
26
  import { Button } from "./ui/button";
@@ -45,6 +45,9 @@ import { Textarea } from "./ui/textarea";
45
45
  import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
46
46
  import { AppResourceEffectiveStack } from "./workspace-resource-effective-stack";
47
47
 
48
+ const APP_CARD_ACTION_CLASS =
49
+ "size-7 rounded-md p-0 text-muted-foreground transition-[background-color,color] hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-accent data-[state=open]:text-foreground";
50
+
48
51
  export function WorkspaceAppCard({
49
52
  app,
50
53
  className,
@@ -121,7 +124,7 @@ export function WorkspaceAppCard({
121
124
  <div
122
125
  aria-disabled={!href}
123
126
  className={cn(
124
- "group relative rounded-lg border bg-card p-4 transition hover:border-foreground/30 aria-disabled:opacity-60",
127
+ "group relative rounded-xl border border-border/60 bg-card/40 p-4 transition-[background-color,border-color] hover:border-foreground/20 hover:bg-accent/15 focus-within:border-foreground/20 focus-within:bg-accent/15 aria-disabled:opacity-60",
125
128
  isArchived && "opacity-70",
126
129
  className,
127
130
  )}
@@ -132,7 +135,7 @@ export function WorkspaceAppCard({
132
135
  target={openInNewTab ? "_blank" : undefined}
133
136
  rel={openInNewTab ? "noreferrer" : undefined}
134
137
  aria-label={`Open ${app.name}`}
135
- className="absolute inset-0 z-0 rounded-lg"
138
+ className="absolute inset-0 z-0 rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
136
139
  />
137
140
  ) : null}
138
141
 
@@ -164,7 +167,7 @@ export function WorkspaceAppCard({
164
167
  </Badge>
165
168
  ) : null}
166
169
  </div>
167
- <p className="mt-1 truncate font-mono text-xs text-muted-foreground">
170
+ <p className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
168
171
  {app.path}
169
172
  </p>
170
173
  {isPending && app.branchName ? (
@@ -173,7 +176,7 @@ export function WorkspaceAppCard({
173
176
  </p>
174
177
  ) : null}
175
178
  {app.description ? (
176
- <p className="mt-2 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
179
+ <p className="mt-2 min-h-10 line-clamp-2 text-[13px] leading-5 text-muted-foreground">
177
180
  {app.description}
178
181
  </p>
179
182
  ) : null}
@@ -191,16 +194,24 @@ export function WorkspaceAppCard({
191
194
  ) : null}
192
195
  <div className="pointer-events-auto">
193
196
  <DropdownMenu>
194
- <DropdownMenuTrigger asChild>
195
- <button
196
- type="button"
197
- aria-label={`More actions for ${app.name}`}
198
- className="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition hover:bg-accent hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100 data-[state=open]:opacity-100"
199
- onClick={(e) => e.stopPropagation()}
200
- >
201
- <IconDots size={15} />
202
- </button>
203
- </DropdownMenuTrigger>
197
+ <Tooltip>
198
+ <TooltipTrigger asChild>
199
+ <DropdownMenuTrigger asChild>
200
+ <button
201
+ type="button"
202
+ aria-label={`More actions for ${app.name}`}
203
+ className={cn(
204
+ APP_CARD_ACTION_CLASS,
205
+ "inline-flex cursor-pointer items-center justify-center",
206
+ )}
207
+ onClick={(e) => e.stopPropagation()}
208
+ >
209
+ <IconDots size={15} />
210
+ </button>
211
+ </DropdownMenuTrigger>
212
+ </TooltipTrigger>
213
+ <TooltipContent>More actions</TooltipContent>
214
+ </Tooltip>
204
215
  <DropdownMenuContent align="end" className="w-44">
205
216
  <DropdownMenuItem
206
217
  onSelect={(event) => {
@@ -233,12 +244,6 @@ export function WorkspaceAppCard({
233
244
  </DropdownMenuContent>
234
245
  </DropdownMenu>
235
246
  </div>
236
- {href && !isArchived ? (
237
- <IconArrowUpRight
238
- size={16}
239
- className="text-muted-foreground transition group-hover:text-foreground"
240
- />
241
- ) : null}
242
247
  </div>
243
248
  </div>
244
249
  <Dialog open={editOpen} onOpenChange={setEditOpen}>
@@ -290,11 +295,12 @@ function AppResourcesDialog({ app }: { app: WorkspaceAppSummary }) {
290
295
  const [inspectedResourceId, setInspectedResourceId] = useState<string | null>(
291
296
  null,
292
297
  );
293
- const { data, isLoading } = useActionQuery(
298
+ const query = useActionQuery(
294
299
  "list-workspace-resources-for-app",
295
300
  { appId: app.id },
296
301
  { enabled: open },
297
302
  );
303
+ const { data, isLoading } = query;
298
304
 
299
305
  const resources = ((data as any)?.resources ?? []) as any[];
300
306
  const counts = (data as any)?.counts;
@@ -315,14 +321,14 @@ function AppResourcesDialog({ app }: { app: WorkspaceAppSummary }) {
315
321
  variant="ghost"
316
322
  size="sm"
317
323
  aria-label={`View context resources for ${app.name}`}
318
- className="h-7 w-7 p-0"
324
+ className={APP_CARD_ACTION_CLASS}
319
325
  onClick={(e) => e.stopPropagation()}
320
326
  >
321
327
  <IconFileText size={14} />
322
328
  </Button>
323
329
  </DialogTrigger>
324
330
  </TooltipTrigger>
325
- <TooltipContent>Context</TooltipContent>
331
+ <TooltipContent>View context</TooltipContent>
326
332
  </Tooltip>
327
333
  <DialogContent className="max-w-2xl">
328
334
  <DialogHeader>
@@ -349,7 +355,12 @@ function AppResourcesDialog({ app }: { app: WorkspaceAppSummary }) {
349
355
  </Badge>
350
356
  </div>
351
357
 
352
- {isLoading ? (
358
+ {query.isError ? (
359
+ <ActionQueryError
360
+ error={query.error}
361
+ onRetry={() => void query.refetch()}
362
+ />
363
+ ) : isLoading ? (
353
364
  <div className="space-y-2">
354
365
  <div className="h-14 rounded-lg border bg-muted/30" />
355
366
  <div className="h-14 rounded-lg border bg-muted/30" />
@@ -1,6 +1,7 @@
1
1
  import { useActionQuery } from "@agent-native/core/client";
2
2
 
3
3
  import { cn } from "../lib/utils";
4
+ import { ActionQueryError } from "./action-query-error";
4
5
  import { Badge } from "./ui/badge";
5
6
 
6
7
  export function appAvailabilityLabel(value?: string) {
@@ -54,11 +55,12 @@ export function AppResourceEffectiveStack({
54
55
  appId: string;
55
56
  resource: any;
56
57
  }) {
57
- const { data: context, isLoading } = useActionQuery(
58
+ const query = useActionQuery(
58
59
  "get-workspace-resource-effective-context",
59
60
  { resourceId: resource.id, appId },
60
61
  { enabled: !!resource.id },
61
62
  );
63
+ const { data: context, isLoading } = query;
62
64
  const layers = ((context as any)?.layers ?? []) as any[];
63
65
  const active = (context as any)?.effectiveResource;
64
66
  const availability = (context as any)?.availability;
@@ -76,6 +78,16 @@ export function AppResourceEffectiveStack({
76
78
  );
77
79
  }
78
80
 
81
+ if (query.isError) {
82
+ return (
83
+ <ActionQueryError
84
+ className="mt-3"
85
+ error={query.error}
86
+ onRetry={() => void query.refetch()}
87
+ />
88
+ );
89
+ }
90
+
79
91
  return (
80
92
  <div className="mt-3 rounded-lg border bg-muted/20 p-3">
81
93
  <div className="flex flex-wrap items-start justify-between gap-2">
@@ -1,5 +1,6 @@
1
1
  import { useActionQuery } from "@agent-native/core/client";
2
2
 
3
+ import { ActionQueryError } from "./action-query-error";
3
4
  import { Badge } from "./ui/badge";
4
5
  import { Skeleton } from "./ui/skeleton";
5
6
  import { formatResourceTimestamp } from "./workspace-resource-effective-stack";
@@ -32,7 +33,7 @@ export function ImpactPreview({
32
33
  scope?: "all" | "selected";
33
34
  enabled?: boolean;
34
35
  }) {
35
- const { data: impact, isLoading } = useActionQuery(
36
+ const query = useActionQuery(
36
37
  "preview-workspace-resource-change",
37
38
  {
38
39
  operation,
@@ -42,6 +43,7 @@ export function ImpactPreview({
42
43
  },
43
44
  { enabled: enabled && Boolean(resourceId || path) },
44
45
  );
46
+ const { data: impact, isLoading } = query;
45
47
 
46
48
  if (!enabled || (!resourceId && !path)) return null;
47
49
 
@@ -54,6 +56,15 @@ export function ImpactPreview({
54
56
  );
55
57
  }
56
58
 
59
+ if (query.isError) {
60
+ return (
61
+ <ActionQueryError
62
+ error={query.error}
63
+ onRetry={() => void query.refetch()}
64
+ />
65
+ );
66
+ }
67
+
57
68
  const data = impact as any;
58
69
  if (!data) return null;
59
70
  const affectsAllApps = data.affectsAllApps === true;
@@ -14,6 +14,7 @@ import {
14
14
  type LoaderFunctionArgs,
15
15
  } from "react-router";
16
16
 
17
+ import { ActionQueryError } from "../../components/action-query-error";
17
18
  import { DispatchShell } from "../../components/dispatch-shell";
18
19
  import { Badge } from "../../components/ui/badge";
19
20
  import { Button } from "../../components/ui/button";
@@ -96,9 +97,10 @@ export async function clientLoader({
96
97
  export default function WorkspaceAppCatchAllRoute() {
97
98
  const t = useT();
98
99
  const { appId } = useParams();
99
- const { data: apps = [], isLoading } = useActionQuery("list-workspace-apps", {
100
+ const appsQuery = useActionQuery("list-workspace-apps", {
100
101
  includeAgentCards: false,
101
102
  });
103
+ const { data: apps = [], isLoading } = appsQuery;
102
104
  const app = useMemo(
103
105
  () =>
104
106
  (apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,
@@ -117,6 +119,20 @@ export default function WorkspaceAppCatchAllRoute() {
117
119
  return <Navigate to={appPath("/overview")} replace />;
118
120
  }
119
121
 
122
+ if (appsQuery.isError) {
123
+ return (
124
+ <DispatchShell
125
+ title={t("dispatch.pages.dataLoadFailed")}
126
+ description={t("dispatch.pages.pageNotFoundDescription")}
127
+ >
128
+ <ActionQueryError
129
+ error={appsQuery.error}
130
+ onRetry={() => void appsQuery.refetch()}
131
+ />
132
+ </DispatchShell>
133
+ );
134
+ }
135
+
120
136
  if ((isLoading && !app) || (app && app.status !== "pending" && href)) {
121
137
  return (
122
138
  <div className="flex h-screen w-full items-center justify-center">
@@ -8,6 +8,7 @@ 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 { ActionQueryError } from "../../components/action-query-error";
11
12
  import {
12
13
  AgentsPanel,
13
14
  type ConnectedAgent,
@@ -45,7 +46,10 @@ function dispatchMcpUrl(): string {
45
46
 
46
47
  function DispatchMcpAccessPanel() {
47
48
  const t = useT();
48
- const { data, isLoading } = useActionQuery("list-mcp-app-access", {});
49
+ const { data, isLoading, isError, error, refetch } = useActionQuery(
50
+ "list-mcp-app-access",
51
+ {},
52
+ );
49
53
  const [optimistic, setOptimistic] = useState<McpAccessState | null>(null);
50
54
  const saveAccess = useActionMutation("set-mcp-app-access", {
51
55
  onSuccess: () => {
@@ -78,10 +82,6 @@ function DispatchMcpAccessPanel() {
78
82
  const mcpUrl = dispatchMcpUrl();
79
83
 
80
84
  function persist(next: McpAccessState) {
81
- if (next.mode === "selected-apps" && next.selectedAppIds.length === 0) {
82
- toast.error(t("dispatch.pages.selectAppForMcp"));
83
- return;
84
- }
85
85
  setOptimistic(next);
86
86
  saveAccess.mutate(next);
87
87
  }
@@ -153,7 +153,13 @@ function DispatchMcpAccessPanel() {
153
153
  </Button>
154
154
  </div>
155
155
 
156
- {access.mode === "selected-apps" ? (
156
+ {isError ? (
157
+ <ActionQueryError
158
+ className="mt-4"
159
+ error={error}
160
+ onRetry={() => void refetch()}
161
+ />
162
+ ) : access.mode === "selected-apps" ? (
157
163
  <div className="mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
158
164
  {apps.map((app) => {
159
165
  const isSelected = selected.has(app.id);
@@ -197,7 +203,7 @@ function DispatchMcpAccessPanel() {
197
203
 
198
204
  export default function AgentsRoute() {
199
205
  const t = useT();
200
- const { data, refetch } = useActionQuery("list-connected-agents", {});
206
+ const agentsQuery = useActionQuery("list-connected-agents", {});
201
207
 
202
208
  return (
203
209
  <DispatchShell
@@ -206,10 +212,17 @@ export default function AgentsRoute() {
206
212
  >
207
213
  <div className="space-y-4">
208
214
  <DispatchMcpAccessPanel />
209
- <AgentsPanel
210
- agents={(data || []) as ConnectedAgent[]}
211
- onRefresh={refetch}
212
- />
215
+ {agentsQuery.isError ? (
216
+ <ActionQueryError
217
+ error={agentsQuery.error}
218
+ onRetry={() => void agentsQuery.refetch()}
219
+ />
220
+ ) : (
221
+ <AgentsPanel
222
+ agents={(agentsQuery.data || []) as ConnectedAgent[]}
223
+ onRefresh={agentsQuery.refetch}
224
+ />
225
+ )}
213
226
  </div>
214
227
  </DispatchShell>
215
228
  );
@@ -16,6 +16,7 @@ import {
16
16
  import { useSearchParams } from "react-router";
17
17
  import { toast } from "sonner";
18
18
 
19
+ import { ActionQueryError } from "../../components/action-query-error";
19
20
  import {
20
21
  ApprovalValueBlock,
21
22
  parseApprovalValue,
@@ -66,10 +67,8 @@ export default function ApprovalPreviewRoute() {
66
67
  const [searchParams] = useSearchParams();
67
68
  const id = searchParams.get("id") ?? "";
68
69
 
69
- const { data: approvals, isLoading } = useActionQuery(
70
- "list-dispatch-approvals",
71
- {},
72
- );
70
+ const approvalsQuery = useActionQuery("list-dispatch-approvals", {});
71
+ const { data: approvals, isLoading } = approvalsQuery;
73
72
 
74
73
  const approve = useActionMutation("approve-dispatch-change", {
75
74
  onSuccess: () => toast.success("Change approved"),
@@ -141,6 +140,19 @@ export default function ApprovalPreviewRoute() {
141
140
  );
142
141
  }
143
142
 
143
+ if (approvalsQuery.isError) {
144
+ return (
145
+ <div className="flex min-h-screen items-start justify-center bg-background p-6">
146
+ <div className="w-full max-w-md">
147
+ <ActionQueryError
148
+ error={approvalsQuery.error}
149
+ onRetry={() => void approvalsQuery.refetch()}
150
+ />
151
+ </div>
152
+ </div>
153
+ );
154
+ }
155
+
144
156
  if (!approval) {
145
157
  return (
146
158
  <div className="flex min-h-screen items-center justify-center bg-background p-6">
@@ -3,6 +3,7 @@ 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 { ActionQueryError } from "../../components/action-query-error";
6
7
  import { DispatchShell } from "../../components/dispatch-shell";
7
8
  import { Button } from "../../components/ui/button";
8
9
  import { Input } from "../../components/ui/input";
@@ -13,8 +14,10 @@ export function meta() {
13
14
  }
14
15
 
15
16
  export default function ApprovalsRoute() {
16
- const { data: settings } = useActionQuery("get-dispatch-settings", {});
17
- const { data: approvals } = useActionQuery("list-dispatch-approvals", {});
17
+ const settingsQuery = useActionQuery("get-dispatch-settings", {});
18
+ const approvalsQuery = useActionQuery("list-dispatch-approvals", {});
19
+ const { data: settings } = settingsQuery;
20
+ const { data: approvals } = approvalsQuery;
18
21
  const { data: org } = useOrg();
19
22
  const hasOrg = !!org?.orgId;
20
23
  const [emails, setEmails] = useState("");
@@ -51,106 +54,122 @@ export default function ApprovalsRoute() {
51
54
  <h2 className="text-lg font-semibold text-foreground">
52
55
  Approval policy
53
56
  </h2>
54
- <div className="mt-4 space-y-4">
55
- <label className="flex items-center justify-between rounded-xl border px-4 py-3">
56
- <div>
57
- <div className="text-sm font-medium text-foreground">
58
- Require approval for durable changes
57
+ {settingsQuery.isError ? (
58
+ <ActionQueryError
59
+ className="mt-4"
60
+ error={settingsQuery.error}
61
+ onRetry={() => void settingsQuery.refetch()}
62
+ />
63
+ ) : (
64
+ <div className="mt-4 space-y-4">
65
+ <label className="flex items-center justify-between rounded-xl border px-4 py-3">
66
+ <div>
67
+ <div className="text-sm font-medium text-foreground">
68
+ Require approval for durable changes
69
+ </div>
70
+ <div className="mt-1 text-xs text-muted-foreground">
71
+ {hasOrg
72
+ ? "Applies to saved destinations, shared dream proposals, All-app workspace resources, and dispatch settings."
73
+ : "Requires a team workspace. Set one up on the Team page."}
74
+ </div>
59
75
  </div>
60
- <div className="mt-1 text-xs text-muted-foreground">
61
- {hasOrg
62
- ? "Applies to saved destinations, shared dream proposals, All-app workspace resources, and dispatch settings."
63
- : "Requires a team workspace. Set one up on the Team page."}
76
+ <Switch
77
+ checked={settings?.enabled || false}
78
+ disabled={!hasOrg || savePolicy.isPending}
79
+ onCheckedChange={(checked) =>
80
+ savePolicy.mutate({
81
+ enabled: checked,
82
+ approverEmails: settings?.approverEmails || approverList,
83
+ })
84
+ }
85
+ />
86
+ </label>
87
+ <div className="space-y-2">
88
+ <div className="text-sm font-medium text-foreground">
89
+ Approver emails
64
90
  </div>
91
+ <Input
92
+ value={emails}
93
+ onChange={(event) => setEmails(event.target.value)}
94
+ placeholder={(settings?.approverEmails || []).join(", ")}
95
+ disabled={!hasOrg}
96
+ />
97
+ <Button
98
+ className="w-full"
99
+ variant="outline"
100
+ disabled={!hasOrg || savePolicy.isPending}
101
+ onClick={() =>
102
+ savePolicy.mutate({
103
+ enabled: settings?.enabled || false,
104
+ approverEmails: approverList,
105
+ })
106
+ }
107
+ >
108
+ Save approvers
109
+ </Button>
65
110
  </div>
66
- <Switch
67
- checked={settings?.enabled || false}
68
- disabled={!hasOrg || savePolicy.isPending}
69
- onCheckedChange={(checked) =>
70
- savePolicy.mutate({
71
- enabled: checked,
72
- approverEmails: settings?.approverEmails || approverList,
73
- })
74
- }
75
- />
76
- </label>
77
- <div className="space-y-2">
78
- <div className="text-sm font-medium text-foreground">
79
- Approver emails
80
- </div>
81
- <Input
82
- value={emails}
83
- onChange={(event) => setEmails(event.target.value)}
84
- placeholder={(settings?.approverEmails || []).join(", ")}
85
- disabled={!hasOrg}
86
- />
87
- <Button
88
- className="w-full"
89
- variant="outline"
90
- disabled={!hasOrg || savePolicy.isPending}
91
- onClick={() =>
92
- savePolicy.mutate({
93
- enabled: settings?.enabled || false,
94
- approverEmails: approverList,
95
- })
96
- }
97
- >
98
- Save approvers
99
- </Button>
100
111
  </div>
101
- </div>
112
+ )}
102
113
  </section>
103
114
 
104
115
  <section className="rounded-2xl border bg-card p-5">
105
116
  <h2 className="text-lg font-semibold text-foreground">
106
117
  Pending and recent requests
107
118
  </h2>
108
- <div className="mt-4 space-y-3">
109
- {(approvals || []).map((approval: any) => (
110
- <div
111
- key={approval.id}
112
- className="rounded-xl border bg-muted/30 px-4 py-3"
113
- >
114
- <div className="flex items-start justify-between gap-3">
115
- <div>
116
- <div className="text-sm font-medium text-foreground">
117
- {approval.summary}
118
- </div>
119
- <div className="mt-1 text-xs text-muted-foreground">
120
- {approval.status} · requested by {approval.requestedBy}
119
+ {approvalsQuery.isError ? (
120
+ <ActionQueryError
121
+ className="mt-4"
122
+ error={approvalsQuery.error}
123
+ onRetry={() => void approvalsQuery.refetch()}
124
+ />
125
+ ) : (
126
+ <div className="mt-4 space-y-3">
127
+ {(approvals || []).map((approval: any) => (
128
+ <div
129
+ key={approval.id}
130
+ className="rounded-xl border bg-muted/30 px-4 py-3"
131
+ >
132
+ <div className="flex items-start justify-between gap-3">
133
+ <div>
134
+ <div className="text-sm font-medium text-foreground">
135
+ {approval.summary}
136
+ </div>
137
+ <div className="mt-1 text-xs text-muted-foreground">
138
+ {approval.status} · requested by {approval.requestedBy}
139
+ </div>
121
140
  </div>
141
+ {approval.status === "pending" && (
142
+ <div className="flex gap-2">
143
+ <Button
144
+ size="sm"
145
+ onClick={() => approve.mutate({ id: approval.id })}
146
+ >
147
+ Approve
148
+ </Button>
149
+ <Button
150
+ size="sm"
151
+ variant="outline"
152
+ onClick={() =>
153
+ reject.mutate({
154
+ id: approval.id,
155
+ reason: "Rejected in dispatch UI",
156
+ })
157
+ }
158
+ >
159
+ Reject
160
+ </Button>
161
+ </div>
162
+ )}
122
163
  </div>
123
- {approval.status === "pending" && (
124
- <div className="flex gap-2">
125
- <Button
126
- size="sm"
127
- onClick={() => approve.mutate({ id: approval.id })}
128
- >
129
- Approve
130
- </Button>
131
- <Button
132
- size="sm"
133
- variant="outline"
134
- onClick={() =>
135
- reject.mutate({
136
- id: approval.id,
137
- reason: "Rejected in dispatch UI",
138
- })
139
- }
140
- >
141
- Reject
142
- </Button>
143
- </div>
144
- )}
145
164
  </div>
146
- </div>
147
- ))}
148
- {(approvals?.length || 0) === 0 && (
149
- <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
150
- No approval requests yet.
151
- </div>
152
- )}
153
- </div>
165
+ ))}
166
+ {(approvals?.length || 0) === 0 && (
167
+ <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
168
+ No approval requests yet.
169
+ </div>
170
+ )}
171
+ </div>
172
+ )}
154
173
  </section>
155
174
  </div>
156
175
  </DispatchShell>
@@ -7,6 +7,7 @@ import {
7
7
  import { useEffect, useMemo } from "react";
8
8
  import { Link, useParams } from "react-router";
9
9
 
10
+ import { ActionQueryError } from "../../components/action-query-error";
10
11
  import { DispatchShell } from "../../components/dispatch-shell";
11
12
  import { Badge } from "../../components/ui/badge";
12
13
  import { Button } from "../../components/ui/button";
@@ -23,9 +24,10 @@ export function meta() {
23
24
  export default function WorkspaceAppRoute() {
24
25
  const t = useT();
25
26
  const { appId } = useParams();
26
- const { data: apps = [], isLoading } = useActionQuery("list-workspace-apps", {
27
+ const appsQuery = useActionQuery("list-workspace-apps", {
27
28
  includeAgentCards: false,
28
29
  });
30
+ const { data: apps = [], isLoading } = appsQuery;
29
31
  const app = useMemo(
30
32
  () =>
31
33
  (apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,
@@ -51,7 +53,12 @@ export default function WorkspaceAppRoute() {
51
53
  </Link>
52
54
  </Button>
53
55
 
54
- {isLoading && !app ? (
56
+ {appsQuery.isError ? (
57
+ <ActionQueryError
58
+ error={appsQuery.error}
59
+ onRetry={() => void appsQuery.refetch()}
60
+ />
61
+ ) : isLoading && !app ? (
55
62
  <div className="space-y-3">
56
63
  <Skeleton className="h-5 w-48" />
57
64
  <Skeleton className="h-4 w-full" />