@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,30 +1,13 @@
1
- import {
2
- useActionMutation,
3
- useActionQuery,
4
- useT,
5
- } from "@agent-native/core/client";
1
+ import { useActionQuery, useT } from "@agent-native/core/client";
6
2
  import {
7
3
  IconApps,
8
- IconBrain,
9
- IconBrush,
10
- IconCalendarMonth,
11
- IconChartBar,
12
4
  IconChevronDown,
13
- IconClipboardList,
14
5
  IconEyeOff,
15
- IconFileText,
16
- IconLoader2,
17
- IconMail,
18
- IconPhoto,
19
6
  IconPlus,
20
- IconPresentation,
21
- IconScreenShare,
22
- IconSparkles,
23
- IconStack3,
24
7
  } from "@tabler/icons-react";
25
8
  import { useState } from "react";
26
- import { toast } from "sonner";
27
9
 
10
+ import { ActionQueryError } from "../../components/action-query-error";
28
11
  import { CreateAppPopover } from "../../components/create-app-popover";
29
12
  import { DispatchShell } from "../../components/dispatch-shell";
30
13
  import { Button } from "../../components/ui/button";
@@ -48,48 +31,19 @@ interface WorkspaceInfo {
48
31
  appCount: number;
49
32
  }
50
33
 
51
- interface AvailableTemplate {
52
- name: string;
53
- label: string;
54
- hint: string;
55
- icon: string;
56
- color: string;
57
- colorRgb: string;
58
- core: boolean;
59
- }
60
-
61
- const TEMPLATE_ICONS: Record<string, typeof IconMail> = {
62
- Mail: IconMail,
63
- CalendarMonth: IconCalendarMonth,
64
- FileText: IconFileText,
65
- Presentation: IconPresentation,
66
- ScreenShare: IconScreenShare,
67
- Brain: IconBrain,
68
- Photo: IconPhoto,
69
- ChartBar: IconChartBar,
70
- ClipboardList: IconClipboardList,
71
- Brush: IconBrush,
72
- };
73
-
74
34
  export default function AppsRoute() {
75
35
  const t = useT();
76
36
  const [showHidden, setShowHidden] = useState(false);
77
- const [templatesOpen, setTemplatesOpen] = useState(false);
78
- const { data: apps = [], isLoading: appsLoading } = useActionQuery(
79
- "list-workspace-apps",
80
- { includeAgentCards: false, includeArchived: true },
81
- );
37
+ const appsQuery = useActionQuery("list-workspace-apps", {
38
+ includeAgentCards: false,
39
+ includeArchived: true,
40
+ });
41
+ const { data: apps = [], isLoading: appsLoading } = appsQuery;
82
42
  const { data: workspace } = useActionQuery(
83
43
  "get-workspace-info",
84
44
  {},
85
45
  { staleTime: 60_000 },
86
46
  );
87
- const { data: templates = [], isLoading: templatesLoading } = useActionQuery(
88
- "list-available-workspace-templates",
89
- {},
90
- { refetchInterval: 5_000 },
91
- );
92
-
93
47
  const ws = workspace as WorkspaceInfo | undefined;
94
48
  const workspaceLabel = ws?.displayName ?? ws?.name ?? null;
95
49
  const allApps = (apps as WorkspaceAppSummary[]).filter(
@@ -97,7 +51,6 @@ export default function AppsRoute() {
97
51
  );
98
52
  const visibleApps = allApps.filter((app) => !app.archived);
99
53
  const archivedApps = allApps.filter((app) => app.archived);
100
- const typedTemplates = templates as AvailableTemplate[];
101
54
  const showAppSkeletons = appsLoading && allApps.length === 0;
102
55
 
103
56
  return (
@@ -152,7 +105,12 @@ export default function AppsRoute() {
152
105
  ) : null}
153
106
  </div>
154
107
 
155
- {showAppSkeletons ? (
108
+ {appsQuery.isError ? (
109
+ <ActionQueryError
110
+ error={appsQuery.error}
111
+ onRetry={() => void appsQuery.refetch()}
112
+ />
113
+ ) : showAppSkeletons ? (
156
114
  <AppsSkeletonGrid />
157
115
  ) : visibleApps.length > 0 ? (
158
116
  <div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
@@ -165,66 +123,6 @@ export default function AppsRoute() {
165
123
  )}
166
124
  </section>
167
125
 
168
- {typedTemplates.length > 0 || templatesLoading ? (
169
- <Collapsible open={templatesOpen} onOpenChange={setTemplatesOpen}>
170
- <section className="space-y-3">
171
- <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
172
- <div className="flex min-w-0 items-center gap-2">
173
- <IconStack3
174
- size={16}
175
- className="shrink-0 text-muted-foreground"
176
- />
177
- <div className="min-w-0">
178
- <h2 className="text-sm font-semibold text-foreground">
179
- {t("dispatch.pages.templates")}
180
- </h2>
181
- <p className="text-xs text-muted-foreground">
182
- {templatesLoading
183
- ? t("dispatch.pages.checkingTemplates")
184
- : t("dispatch.pages.templatesAvailable", {
185
- count: typedTemplates.length,
186
- })}
187
- </p>
188
- </div>
189
- </div>
190
- <CollapsibleTrigger asChild>
191
- <Button
192
- type="button"
193
- variant="outline"
194
- size="sm"
195
- className="gap-1.5"
196
- >
197
- {templatesOpen
198
- ? t("dispatch.pages.hide")
199
- : t("dispatch.pages.show")}
200
- <IconChevronDown
201
- size={14}
202
- className={cn(
203
- "transition-transform",
204
- templatesOpen && "rotate-180",
205
- )}
206
- />
207
- </Button>
208
- </CollapsibleTrigger>
209
- </div>
210
- <CollapsibleContent>
211
- {templatesLoading && typedTemplates.length === 0 ? (
212
- <AppsSkeletonGrid />
213
- ) : (
214
- <div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
215
- {typedTemplates.map((template) => (
216
- <AddTemplateCard
217
- key={template.name}
218
- template={template}
219
- />
220
- ))}
221
- </div>
222
- )}
223
- </CollapsibleContent>
224
- </section>
225
- </Collapsible>
226
- ) : null}
227
-
228
126
  {archivedApps.length > 0 ? (
229
127
  <Collapsible open={showHidden} onOpenChange={setShowHidden}>
230
128
  <section className="space-y-3">
@@ -332,65 +230,3 @@ function EmptyAppsState() {
332
230
  </div>
333
231
  );
334
232
  }
335
-
336
- function AddTemplateCard({ template }: { template: AvailableTemplate }) {
337
- const Icon = TEMPLATE_ICONS[template.icon] ?? IconSparkles;
338
- const scaffold = useActionMutation("scaffold-workspace-app", {
339
- onSuccess: (result: any) => {
340
- toast.success(
341
- `Scaffolded apps/${result?.appId || template.name}. The gateway will pick it up shortly.`,
342
- );
343
- },
344
- onError: (err) => {
345
- toast.error(
346
- `Could not scaffold ${template.label}: ${
347
- err instanceof Error ? err.message : String(err)
348
- }`,
349
- );
350
- },
351
- });
352
-
353
- return (
354
- <div className="group relative flex h-full min-h-36 items-stretch gap-3 rounded-lg border bg-card p-4 transition hover:border-foreground/30">
355
- <div
356
- className="flex h-9 w-9 shrink-0 items-center justify-center self-start rounded-md"
357
- style={{
358
- backgroundColor: `rgb(${template.colorRgb} / 0.12)`,
359
- color: template.color,
360
- }}
361
- >
362
- <Icon size={18} />
363
- </div>
364
- <div className="flex min-w-0 flex-1 flex-col">
365
- <div className="flex min-w-0 items-center gap-2">
366
- <h3 className="truncate text-sm font-semibold text-foreground">
367
- {template.label}
368
- </h3>
369
- </div>
370
- <p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
371
- {template.hint}
372
- </p>
373
- <div className="mt-auto pt-3">
374
- <Button
375
- size="sm"
376
- variant="outline"
377
- disabled={scaffold.isPending}
378
- onClick={() => scaffold.mutate({ template: template.name })}
379
- >
380
- {scaffold.isPending ? (
381
- <>
382
- <IconLoader2 size={14} className="mr-1.5 animate-spin" />
383
- Adding...
384
- </>
385
- ) : (
386
- <>
387
- <IconPlus size={14} className="mr-1.5" />
388
- Add to workspace
389
- </>
390
- )}
391
- </Button>
392
- </div>
393
- </div>
394
- </div>
395
- );
396
- }
@@ -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
  );
@@ -62,7 +62,6 @@ function useAutomations() {
62
62
  queryKey: [...AUTOMATIONS_QUERY_KEY, version],
63
63
  queryFn: listDispatchAutomations,
64
64
  placeholderData: (prev) => prev,
65
- refetchInterval: 15_000,
66
65
  staleTime: 5_000,
67
66
  });
68
67
  }
@@ -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">