@agent-native/dispatch 0.14.3 → 0.14.4

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 (127) 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/ui/sheet.js +1 -1
  27. package/dist/components/ui/sheet.js.map +1 -1
  28. package/dist/components/ui/toast.js +1 -1
  29. package/dist/components/ui/toast.js.map +1 -1
  30. package/dist/components/workspace-app-card.d.ts.map +1 -1
  31. package/dist/components/workspace-app-card.js +8 -5
  32. package/dist/components/workspace-app-card.js.map +1 -1
  33. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  34. package/dist/components/workspace-resource-effective-stack.js +6 -1
  35. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  36. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -1
  37. package/dist/components/workspace-resource-impact-preview.js +6 -1
  38. package/dist/components/workspace-resource-impact-preview.js.map +1 -1
  39. package/dist/routes/pages/$appId.d.ts.map +1 -1
  40. package/dist/routes/pages/$appId.js +6 -1
  41. package/dist/routes/pages/$appId.js.map +1 -1
  42. package/dist/routes/pages/agents.d.ts.map +1 -1
  43. package/dist/routes/pages/agents.js +5 -8
  44. package/dist/routes/pages/agents.js.map +1 -1
  45. package/dist/routes/pages/approval.d.ts.map +1 -1
  46. package/dist/routes/pages/approval.js +6 -1
  47. package/dist/routes/pages/approval.js.map +1 -1
  48. package/dist/routes/pages/approvals.d.ts.map +1 -1
  49. package/dist/routes/pages/approvals.js +8 -5
  50. package/dist/routes/pages/approvals.js.map +1 -1
  51. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  52. package/dist/routes/pages/apps.$appId.js +4 -2
  53. package/dist/routes/pages/apps.$appId.js.map +1 -1
  54. package/dist/routes/pages/apps.d.ts.map +1 -1
  55. package/dist/routes/pages/apps.js +9 -3
  56. package/dist/routes/pages/apps.js.map +1 -1
  57. package/dist/routes/pages/audit.d.ts.map +1 -1
  58. package/dist/routes/pages/audit.js +4 -2
  59. package/dist/routes/pages/audit.js.map +1 -1
  60. package/dist/routes/pages/chat.d.ts.map +1 -1
  61. package/dist/routes/pages/chat.js +16 -2
  62. package/dist/routes/pages/chat.js.map +1 -1
  63. package/dist/routes/pages/destinations.d.ts.map +1 -1
  64. package/dist/routes/pages/destinations.js +5 -3
  65. package/dist/routes/pages/destinations.js.map +1 -1
  66. package/dist/routes/pages/identities.d.ts.map +1 -1
  67. package/dist/routes/pages/identities.js +5 -3
  68. package/dist/routes/pages/identities.js.map +1 -1
  69. package/dist/routes/pages/integrations.d.ts.map +1 -1
  70. package/dist/routes/pages/integrations.js +9 -3
  71. package/dist/routes/pages/integrations.js.map +1 -1
  72. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  73. package/dist/routes/pages/thread-debug.js +7 -6
  74. package/dist/routes/pages/thread-debug.js.map +1 -1
  75. package/dist/routes/pages/vault.d.ts.map +1 -1
  76. package/dist/routes/pages/vault.js +23 -7
  77. package/dist/routes/pages/vault.js.map +1 -1
  78. package/dist/routes/pages/workspace.d.ts.map +1 -1
  79. package/dist/routes/pages/workspace.js +9 -3
  80. package/dist/routes/pages/workspace.js.map +1 -1
  81. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  82. package/dist/server/lib/dispatch-store.js +2 -2
  83. package/dist/server/lib/dispatch-store.js.map +1 -1
  84. package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
  85. package/dist/server/lib/mcp-access-store.js +30 -2
  86. package/dist/server/lib/mcp-access-store.js.map +1 -1
  87. package/dist/server/plugins/agent-chat.d.ts +1 -1
  88. package/package.json +2 -2
  89. package/src/actions/list_apps.spec.ts +52 -0
  90. package/src/actions/list_apps.ts +16 -8
  91. package/src/actions/set-mcp-app-access.spec.ts +73 -0
  92. package/src/actions/set-mcp-app-access.ts +1 -5
  93. package/src/components/action-query-error.spec.tsx +56 -0
  94. package/src/components/action-query-error.tsx +32 -0
  95. package/src/components/app-keys-popover.spec.tsx +85 -0
  96. package/src/components/app-keys-popover.tsx +76 -50
  97. package/src/components/dispatch-control-plane.spec.tsx +106 -0
  98. package/src/components/dispatch-control-plane.tsx +71 -115
  99. package/src/components/index.ts +1 -0
  100. package/src/components/layout/Layout.spec.tsx +42 -2
  101. package/src/components/layout/Layout.tsx +22 -27
  102. package/src/components/ui/sheet.tsx +1 -1
  103. package/src/components/ui/toast.tsx +1 -1
  104. package/src/components/workspace-app-card.spec.tsx +80 -0
  105. package/src/components/workspace-app-card.tsx +36 -25
  106. package/src/components/workspace-resource-effective-stack.tsx +13 -1
  107. package/src/components/workspace-resource-impact-preview.tsx +12 -1
  108. package/src/routes/pages/$appId.tsx +17 -1
  109. package/src/routes/pages/agents.tsx +24 -11
  110. package/src/routes/pages/approval.tsx +16 -4
  111. package/src/routes/pages/approvals.tsx +108 -89
  112. package/src/routes/pages/apps.$appId.tsx +9 -2
  113. package/src/routes/pages/apps.tsx +20 -7
  114. package/src/routes/pages/audit.tsx +29 -20
  115. package/src/routes/pages/chat.spec.tsx +94 -0
  116. package/src/routes/pages/chat.tsx +30 -10
  117. package/src/routes/pages/destinations.tsx +75 -62
  118. package/src/routes/pages/identities.tsx +74 -63
  119. package/src/routes/pages/integrations.tsx +17 -10
  120. package/src/routes/pages/thread-debug.tsx +18 -12
  121. package/src/routes/pages/vault.tsx +59 -26
  122. package/src/routes/pages/workspace.tsx +69 -55
  123. package/src/server/lib/approval-fencing.spec.ts +113 -2
  124. package/src/server/lib/dispatch-store.ts +2 -0
  125. package/src/server/lib/mcp-access-store.spec.ts +116 -4
  126. package/src/server/lib/mcp-access-store.ts +37 -2
  127. package/src/server/lib/mcp-gateway.spec.ts +79 -3
@@ -25,6 +25,7 @@ import {
25
25
  import { useState } from "react";
26
26
  import { toast } from "sonner";
27
27
 
28
+ import { ActionQueryError } from "../../components/action-query-error";
28
29
  import { CreateAppPopover } from "../../components/create-app-popover";
29
30
  import { DispatchShell } from "../../components/dispatch-shell";
30
31
  import { Button } from "../../components/ui/button";
@@ -75,20 +76,22 @@ export default function AppsRoute() {
75
76
  const t = useT();
76
77
  const [showHidden, setShowHidden] = useState(false);
77
78
  const [templatesOpen, setTemplatesOpen] = useState(false);
78
- const { data: apps = [], isLoading: appsLoading } = useActionQuery(
79
- "list-workspace-apps",
80
- { includeAgentCards: false, includeArchived: true },
81
- );
79
+ const appsQuery = useActionQuery("list-workspace-apps", {
80
+ includeAgentCards: false,
81
+ includeArchived: true,
82
+ });
83
+ const { data: apps = [], isLoading: appsLoading } = appsQuery;
82
84
  const { data: workspace } = useActionQuery(
83
85
  "get-workspace-info",
84
86
  {},
85
87
  { staleTime: 60_000 },
86
88
  );
87
- const { data: templates = [], isLoading: templatesLoading } = useActionQuery(
89
+ const templatesQuery = useActionQuery(
88
90
  "list-available-workspace-templates",
89
91
  {},
90
92
  { refetchInterval: 5_000 },
91
93
  );
94
+ const { data: templates = [], isLoading: templatesLoading } = templatesQuery;
92
95
 
93
96
  const ws = workspace as WorkspaceInfo | undefined;
94
97
  const workspaceLabel = ws?.displayName ?? ws?.name ?? null;
@@ -152,7 +155,12 @@ export default function AppsRoute() {
152
155
  ) : null}
153
156
  </div>
154
157
 
155
- {showAppSkeletons ? (
158
+ {appsQuery.isError ? (
159
+ <ActionQueryError
160
+ error={appsQuery.error}
161
+ onRetry={() => void appsQuery.refetch()}
162
+ />
163
+ ) : showAppSkeletons ? (
156
164
  <AppsSkeletonGrid />
157
165
  ) : visibleApps.length > 0 ? (
158
166
  <div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
@@ -165,7 +173,12 @@ export default function AppsRoute() {
165
173
  )}
166
174
  </section>
167
175
 
168
- {typedTemplates.length > 0 || templatesLoading ? (
176
+ {templatesQuery.isError ? (
177
+ <ActionQueryError
178
+ error={templatesQuery.error}
179
+ onRetry={() => void templatesQuery.refetch()}
180
+ />
181
+ ) : typedTemplates.length > 0 || templatesLoading ? (
169
182
  <Collapsible open={templatesOpen} onOpenChange={setTemplatesOpen}>
170
183
  <section className="space-y-3">
171
184
  <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
@@ -1,5 +1,6 @@
1
1
  import { useActionQuery } from "@agent-native/core/client";
2
2
 
3
+ import { ActionQueryError } from "../../components/action-query-error";
3
4
  import { DispatchShell } from "../../components/dispatch-shell";
4
5
 
5
6
  export function meta() {
@@ -7,7 +8,8 @@ export function meta() {
7
8
  }
8
9
 
9
10
  export default function AuditRoute() {
10
- const { data } = useActionQuery("list-dispatch-audit", { limit: 100 });
11
+ const query = useActionQuery("list-dispatch-audit", { limit: 100 });
12
+ const { data } = query;
11
13
 
12
14
  return (
13
15
  <DispatchShell
@@ -15,27 +17,34 @@ export default function AuditRoute() {
15
17
  description="Change history for routes, settings, and approvals."
16
18
  >
17
19
  <section className="rounded-2xl border bg-card p-5">
18
- <div className="space-y-3">
19
- {(data || []).map((event: any) => (
20
- <div
21
- key={event.id}
22
- className="rounded-xl border bg-muted/30 px-4 py-3"
23
- >
24
- <div className="text-sm font-medium text-foreground">
25
- {event.summary}
20
+ {query.isError ? (
21
+ <ActionQueryError
22
+ error={query.error}
23
+ onRetry={() => void query.refetch()}
24
+ />
25
+ ) : (
26
+ <div className="space-y-3">
27
+ {(data || []).map((event: any) => (
28
+ <div
29
+ key={event.id}
30
+ className="rounded-xl border bg-muted/30 px-4 py-3"
31
+ >
32
+ <div className="text-sm font-medium text-foreground">
33
+ {event.summary}
34
+ </div>
35
+ <div className="mt-1 text-xs text-muted-foreground">
36
+ {event.actor} · {event.action} ·{" "}
37
+ {new Date(event.createdAt).toLocaleString()}
38
+ </div>
26
39
  </div>
27
- <div className="mt-1 text-xs text-muted-foreground">
28
- {event.actor} · {event.action} ·{" "}
29
- {new Date(event.createdAt).toLocaleString()}
40
+ ))}
41
+ {(data?.length || 0) === 0 && (
42
+ <div className="rounded-xl border border-dashed px-4 py-6 text-sm text-muted-foreground">
43
+ No audit entries yet.
30
44
  </div>
31
- </div>
32
- ))}
33
- {(data?.length || 0) === 0 && (
34
- <div className="rounded-xl border border-dashed px-4 py-6 text-sm text-muted-foreground">
35
- No audit entries yet.
36
- </div>
37
- )}
38
- </div>
45
+ )}
46
+ </div>
47
+ )}
39
48
  </section>
40
49
  </DispatchShell>
41
50
  );
@@ -0,0 +1,94 @@
1
+ // @vitest-environment happy-dom
2
+ import React, { act, type ReactNode } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { MemoryRouter } from "react-router";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import ChatRoute from "./chat";
8
+
9
+ const clientState = vi.hoisted(() => ({
10
+ surfaceProps: null as Record<string, unknown> | null,
11
+ }));
12
+
13
+ vi.mock("@agent-native/core/client", () => ({
14
+ AgentChatSurface: (props: Record<string, unknown>) => {
15
+ clientState.surfaceProps = props;
16
+ return <>{props.composerSlot as ReactNode}</>;
17
+ },
18
+ appBasePath: () => "",
19
+ appPath: (path: string) => path,
20
+ isInBuilderFrame: () => false,
21
+ markAgentChatHomeHandoff: vi.fn(),
22
+ sendToAgentChat: vi.fn(),
23
+ useT: () => (key: string, values?: { defaultValue?: string }) =>
24
+ values?.defaultValue ?? key,
25
+ }));
26
+
27
+ describe("Dispatch ChatRoute", () => {
28
+ let container: HTMLDivElement;
29
+ let root: Root;
30
+
31
+ beforeEach(() => {
32
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
33
+ clientState.surfaceProps = null;
34
+ container = document.createElement("div");
35
+ document.body.appendChild(container);
36
+ root = createRoot(container);
37
+ });
38
+
39
+ afterEach(() => {
40
+ act(() => root.unmount());
41
+ container.remove();
42
+ vi.unstubAllGlobals();
43
+ });
44
+
45
+ it("keeps the centered hero layout for a direct new Chat", async () => {
46
+ await act(async () => {
47
+ root.render(
48
+ <MemoryRouter initialEntries={["/chat"]}>
49
+ <ChatRoute />
50
+ </MemoryRouter>,
51
+ );
52
+ });
53
+
54
+ expect(clientState.surfaceProps).toMatchObject({
55
+ mode: "page",
56
+ chatViewTransition: true,
57
+ centerComposerWhenEmpty: true,
58
+ composerLayoutVariant: "hero",
59
+ composerPlaceholder: "Ask Dispatch...",
60
+ });
61
+ expect(container.textContent).toContain("Chat across your apps");
62
+ });
63
+
64
+ it("starts bottom-pinned when an Overview prompt is transitioning in", async () => {
65
+ await act(async () => {
66
+ root.render(
67
+ <MemoryRouter
68
+ initialEntries={[
69
+ {
70
+ pathname: "/chat",
71
+ state: {
72
+ dispatchPrompt: {
73
+ id: "overview-prompt",
74
+ message: "Route this across my apps",
75
+ selectedModel: "auto",
76
+ },
77
+ },
78
+ },
79
+ ]}
80
+ >
81
+ <ChatRoute />
82
+ </MemoryRouter>,
83
+ );
84
+ });
85
+
86
+ expect(clientState.surfaceProps).not.toHaveProperty(
87
+ "centerComposerWhenEmpty",
88
+ );
89
+ expect(clientState.surfaceProps).not.toHaveProperty(
90
+ "composerLayoutVariant",
91
+ );
92
+ expect(container.textContent).not.toContain("Chat across your apps");
93
+ });
94
+ });
@@ -3,6 +3,7 @@ import {
3
3
  appBasePath,
4
4
  appPath,
5
5
  markAgentChatHomeHandoff,
6
+ useT,
6
7
  } from "@agent-native/core/client";
7
8
  import {
8
9
  useCallback,
@@ -86,6 +87,7 @@ export function meta() {
86
87
  }
87
88
 
88
89
  export default function ChatRoute() {
90
+ const t = useT();
89
91
  const location = useLocation();
90
92
  const navigate = useNavigate();
91
93
  const routeThreadId = threadIdFromPath(location.pathname);
@@ -175,18 +177,36 @@ export default function ChatRoute() {
175
177
  showTabBar={false}
176
178
  dynamicSuggestions={false}
177
179
  suggestions={[]}
178
- emptyStateText="Ask Dispatch to create apps, route work, or manage the workspace."
180
+ emptyStateText={t("dispatch.pages.chatAcrossAppsDescription", {
181
+ defaultValue:
182
+ "Route work, inspect status, or create something new from one place.",
183
+ })}
179
184
  emptyStateDisplay="hidden"
180
- centerComposerWhenEmpty
181
- composerLayoutVariant="hero"
182
- composerPlaceholder="Ask Dispatch..."
185
+ {...(!prompt?.message
186
+ ? {
187
+ centerComposerWhenEmpty: true,
188
+ composerLayoutVariant: "hero" as const,
189
+ }
190
+ : {})}
191
+ composerPlaceholder={t("dispatch.pages.chatPromptPlaceholder", {
192
+ defaultValue: "Ask Dispatch...",
193
+ })}
183
194
  composerSlot={
184
- <div className="dispatch-chat-intro">
185
- <h1>What should Dispatch do next?</h1>
186
- <p>
187
- Create apps, manage shared keys, and route work across agents.
188
- </p>
189
- </div>
195
+ !prompt?.message ? (
196
+ <div className="dispatch-chat-intro">
197
+ <h1>
198
+ {t("dispatch.pages.chatAcrossApps", {
199
+ defaultValue: "Chat across your apps",
200
+ })}
201
+ </h1>
202
+ <p>
203
+ {t("dispatch.pages.chatAcrossAppsDescription", {
204
+ defaultValue:
205
+ "Route work, inspect status, or create something new from one place.",
206
+ })}
207
+ </p>
208
+ </div>
209
+ ) : null
190
210
  }
191
211
  />
192
212
  </div>
@@ -6,6 +6,7 @@ import {
6
6
  import { useState } from "react";
7
7
  import { toast } from "sonner";
8
8
 
9
+ import { ActionQueryError } from "../../components/action-query-error";
9
10
  import { DispatchShell } from "../../components/dispatch-shell";
10
11
  import { TaskQueueHealth } from "../../components/task-queue-health";
11
12
  import {
@@ -78,7 +79,8 @@ function QuickSendRow({
78
79
 
79
80
  export default function DestinationsRoute() {
80
81
  const t = useT();
81
- const { data } = useActionQuery("list-destinations", {});
82
+ const destinationsQuery = useActionQuery("list-destinations", {});
83
+ const { data } = destinationsQuery;
82
84
  const [form, setForm] = useState({
83
85
  name: "",
84
86
  platform: "slack",
@@ -115,70 +117,81 @@ export default function DestinationsRoute() {
115
117
  <h2 className="text-lg font-semibold text-foreground">
116
118
  {t("dispatch.pages.savedDestinations")}
117
119
  </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
- : ""}
120
+ {destinationsQuery.isError ? (
121
+ <ActionQueryError
122
+ className="mt-4"
123
+ error={destinationsQuery.error}
124
+ onRetry={() => void destinationsQuery.refetch()}
125
+ />
126
+ ) : (
127
+ <div className="mt-4 space-y-3">
128
+ {(data || []).map((destination: any) => (
129
+ <div
130
+ key={destination.id}
131
+ className="rounded-xl border bg-muted/30 p-4"
132
+ >
133
+ <div className="flex items-start justify-between gap-3">
134
+ <div>
135
+ <div className="text-sm font-medium text-foreground">
136
+ {destination.name}
137
+ </div>
138
+ <div className="mt-1 text-xs text-muted-foreground">
139
+ {destination.platform} · {destination.destination}
140
+ {destination.threadRef
141
+ ? ` · thread ${destination.threadRef}`
142
+ : ""}
143
+ </div>
144
+ {destination.notes && (
145
+ <p className="mt-2 text-sm text-muted-foreground">
146
+ {destination.notes}
147
+ </p>
148
+ )}
134
149
  </div>
135
- {destination.notes && (
136
- <p className="mt-2 text-sm text-muted-foreground">
137
- {destination.notes}
138
- </p>
139
- )}
140
- </div>
141
- <AlertDialog>
142
- <AlertDialogTrigger asChild>
143
- <Button variant="outline" size="sm">
144
- {t("dispatch.pages.delete")}
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
- >
150
+ <AlertDialog>
151
+ <AlertDialogTrigger asChild>
152
+ <Button variant="outline" size="sm">
167
153
  {t("dispatch.pages.delete")}
168
- </AlertDialogAction>
169
- </AlertDialogFooter>
170
- </AlertDialogContent>
171
- </AlertDialog>
154
+ </Button>
155
+ </AlertDialogTrigger>
156
+ <AlertDialogContent>
157
+ <AlertDialogHeader>
158
+ <AlertDialogTitle>
159
+ {t("dispatch.pages.deleteDestinationTitle")}
160
+ </AlertDialogTitle>
161
+ <AlertDialogDescription>
162
+ {t(
163
+ "dispatch.pages.deleteDestinationDescription",
164
+ {
165
+ name: destination.name,
166
+ },
167
+ )}
168
+ </AlertDialogDescription>
169
+ </AlertDialogHeader>
170
+ <AlertDialogFooter>
171
+ <AlertDialogCancel>
172
+ {t("dispatch.pages.cancel")}
173
+ </AlertDialogCancel>
174
+ <AlertDialogAction
175
+ onClick={() =>
176
+ remove.mutate({ id: destination.id })
177
+ }
178
+ >
179
+ {t("dispatch.pages.delete")}
180
+ </AlertDialogAction>
181
+ </AlertDialogFooter>
182
+ </AlertDialogContent>
183
+ </AlertDialog>
184
+ </div>
185
+ <QuickSendRow destination={destination} />
172
186
  </div>
173
- <QuickSendRow destination={destination} />
174
- </div>
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>
187
+ ))}
188
+ {(data?.length || 0) === 0 && (
189
+ <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
190
+ {t("dispatch.pages.noDestinations")}
191
+ </div>
192
+ )}
193
+ </div>
194
+ )}
182
195
  </section>
183
196
 
184
197
  <section className="rounded-2xl border bg-card p-5">
@@ -1,6 +1,7 @@
1
1
  import { useActionMutation, useActionQuery } from "@agent-native/core/client";
2
2
  import { toast } from "sonner";
3
3
 
4
+ import { ActionQueryError } from "../../components/action-query-error";
4
5
  import { DispatchShell } from "../../components/dispatch-shell";
5
6
  import { Button } from "../../components/ui/button";
6
7
 
@@ -9,7 +10,8 @@ export function meta() {
9
10
  }
10
11
 
11
12
  export default function IdentitiesRoute() {
12
- const { data } = useActionQuery("list-linked-identities", {});
13
+ const query = useActionQuery("list-linked-identities", {});
14
+ const { data } = query;
13
15
  const createToken = useActionMutation("create-link-token", {
14
16
  onSuccess: () => toast.success("Link token created"),
15
17
  });
@@ -19,74 +21,83 @@ export default function IdentitiesRoute() {
19
21
  title="Identities"
20
22
  description="Link external senders to workspace users."
21
23
  >
22
- <div className="grid gap-4 xl:grid-cols-2">
23
- <section className="rounded-2xl border bg-card p-5">
24
- <div className="flex flex-wrap items-center justify-between gap-2">
25
- <h2 className="text-lg font-semibold text-foreground">
26
- Active links
27
- </h2>
28
- <div className="flex shrink-0 gap-2">
29
- <Button
30
- variant="outline"
31
- onClick={() => createToken.mutate({ platform: "slack" })}
32
- >
33
- New Slack token
34
- </Button>
35
- <Button
36
- onClick={() => createToken.mutate({ platform: "telegram" })}
37
- >
38
- New Telegram token
39
- </Button>
24
+ {query.isError ? (
25
+ <ActionQueryError
26
+ error={query.error}
27
+ onRetry={() => void query.refetch()}
28
+ />
29
+ ) : (
30
+ <div className="grid gap-4 xl:grid-cols-2">
31
+ <section className="rounded-2xl border bg-card p-5">
32
+ <div className="flex flex-wrap items-center justify-between gap-2">
33
+ <h2 className="text-lg font-semibold text-foreground">
34
+ Active links
35
+ </h2>
36
+ <div className="flex shrink-0 gap-2">
37
+ <Button
38
+ variant="outline"
39
+ onClick={() => createToken.mutate({ platform: "slack" })}
40
+ >
41
+ New Slack token
42
+ </Button>
43
+ <Button
44
+ onClick={() => createToken.mutate({ platform: "telegram" })}
45
+ >
46
+ New Telegram token
47
+ </Button>
48
+ </div>
40
49
  </div>
41
- </div>
42
- <div className="mt-4 space-y-3">
43
- {(data?.links || []).map((link: any) => (
44
- <div
45
- key={link.id}
46
- className="rounded-xl border bg-muted/30 px-4 py-3"
47
- >
48
- <div className="text-sm font-medium text-foreground">
49
- {link.externalUserName || link.externalUserId}
50
+ <div className="mt-4 space-y-3">
51
+ {(data?.links || []).map((link: any) => (
52
+ <div
53
+ key={link.id}
54
+ className="rounded-xl border bg-muted/30 px-4 py-3"
55
+ >
56
+ <div className="text-sm font-medium text-foreground">
57
+ {link.externalUserName || link.externalUserId}
58
+ </div>
59
+ <div className="mt-1 text-xs text-muted-foreground">
60
+ {link.platform} → {link.ownerEmail}
61
+ </div>
50
62
  </div>
51
- <div className="mt-1 text-xs text-muted-foreground">
52
- {link.platform} {link.ownerEmail}
63
+ ))}
64
+ {(data?.links?.length || 0) === 0 && (
65
+ <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
66
+ No linked identities yet. Generate a token and ask the user to
67
+ send <code>/link TOKEN</code> from Slack or Telegram.
53
68
  </div>
54
- </div>
55
- ))}
56
- {(data?.links?.length || 0) === 0 && (
57
- <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
58
- No linked identities yet. Generate a token and ask the user to
59
- send <code>/link TOKEN</code> from Slack or Telegram.
60
- </div>
61
- )}
62
- </div>
63
- </section>
69
+ )}
70
+ </div>
71
+ </section>
64
72
 
65
- <section className="rounded-2xl border bg-card p-5">
66
- <h2 className="text-lg font-semibold text-foreground">Link tokens</h2>
67
- <div className="mt-4 space-y-3">
68
- {(data?.tokens || []).map((token: any) => (
69
- <div key={token.id} className="rounded-xl border px-4 py-3">
70
- <div className="text-sm font-medium text-foreground">
71
- /link {token.token}
73
+ <section className="rounded-2xl border bg-card p-5">
74
+ <h2 className="text-lg font-semibold text-foreground">
75
+ Link tokens
76
+ </h2>
77
+ <div className="mt-4 space-y-3">
78
+ {(data?.tokens || []).map((token: any) => (
79
+ <div key={token.id} className="rounded-xl border px-4 py-3">
80
+ <div className="text-sm font-medium text-foreground">
81
+ /link {token.token}
82
+ </div>
83
+ <div className="mt-1 text-xs text-muted-foreground">
84
+ {token.platform} · expires{" "}
85
+ {new Date(token.expiresAt).toLocaleString()}
86
+ {token.claimedAt
87
+ ? ` · claimed by ${token.claimedByExternalUserName || token.claimedByExternalUserId}`
88
+ : " · waiting to be claimed"}
89
+ </div>
72
90
  </div>
73
- <div className="mt-1 text-xs text-muted-foreground">
74
- {token.platform} · expires{" "}
75
- {new Date(token.expiresAt).toLocaleString()}
76
- {token.claimedAt
77
- ? ` · claimed by ${token.claimedByExternalUserName || token.claimedByExternalUserId}`
78
- : " · waiting to be claimed"}
91
+ ))}
92
+ {(data?.tokens?.length || 0) === 0 && (
93
+ <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
94
+ No active link tokens.
79
95
  </div>
80
- </div>
81
- ))}
82
- {(data?.tokens?.length || 0) === 0 && (
83
- <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
84
- No active link tokens.
85
- </div>
86
- )}
87
- </div>
88
- </section>
89
- </div>
96
+ )}
97
+ </div>
98
+ </section>
99
+ </div>
100
+ )}
90
101
  </DispatchShell>
91
102
  );
92
103
  }