@agent-native/dispatch 0.14.2 → 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 (138) hide show
  1. package/README.md +114 -122
  2. package/dist/actions/approve-dispatch-change.d.ts +10 -10
  3. package/dist/actions/list_apps.d.ts +1 -0
  4. package/dist/actions/list_apps.js +16 -8
  5. package/dist/actions/list_apps.js.map +1 -1
  6. package/dist/actions/provider-api-register.js +64 -2
  7. package/dist/actions/provider-api-register.js.map +1 -1
  8. package/dist/actions/reject-dispatch-change.d.ts +10 -10
  9. package/dist/actions/set-mcp-app-access.js +1 -4
  10. package/dist/actions/set-mcp-app-access.js.map +1 -1
  11. package/dist/components/action-query-error.d.ts +8 -0
  12. package/dist/components/action-query-error.d.ts.map +1 -0
  13. package/dist/components/action-query-error.js +10 -0
  14. package/dist/components/action-query-error.js.map +1 -0
  15. package/dist/components/app-keys-popover.d.ts +4 -0
  16. package/dist/components/app-keys-popover.d.ts.map +1 -1
  17. package/dist/components/app-keys-popover.js +26 -14
  18. package/dist/components/app-keys-popover.js.map +1 -1
  19. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  20. package/dist/components/dispatch-control-plane.js +31 -27
  21. package/dist/components/dispatch-control-plane.js.map +1 -1
  22. package/dist/components/index.d.ts +1 -0
  23. package/dist/components/index.d.ts.map +1 -1
  24. package/dist/components/index.js +1 -0
  25. package/dist/components/index.js.map +1 -1
  26. package/dist/components/layout/Layout.d.ts +1 -0
  27. package/dist/components/layout/Layout.d.ts.map +1 -1
  28. package/dist/components/layout/Layout.js +19 -21
  29. package/dist/components/layout/Layout.js.map +1 -1
  30. package/dist/components/ui/sheet.js +1 -1
  31. package/dist/components/ui/sheet.js.map +1 -1
  32. package/dist/components/ui/toast.js +1 -1
  33. package/dist/components/ui/toast.js.map +1 -1
  34. package/dist/components/workspace-app-card.d.ts.map +1 -1
  35. package/dist/components/workspace-app-card.js +8 -5
  36. package/dist/components/workspace-app-card.js.map +1 -1
  37. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  38. package/dist/components/workspace-resource-effective-stack.js +6 -1
  39. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  40. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -1
  41. package/dist/components/workspace-resource-impact-preview.js +6 -1
  42. package/dist/components/workspace-resource-impact-preview.js.map +1 -1
  43. package/dist/routes/pages/$appId.d.ts.map +1 -1
  44. package/dist/routes/pages/$appId.js +6 -1
  45. package/dist/routes/pages/$appId.js.map +1 -1
  46. package/dist/routes/pages/agents.d.ts.map +1 -1
  47. package/dist/routes/pages/agents.js +5 -8
  48. package/dist/routes/pages/agents.js.map +1 -1
  49. package/dist/routes/pages/approval.d.ts.map +1 -1
  50. package/dist/routes/pages/approval.js +6 -1
  51. package/dist/routes/pages/approval.js.map +1 -1
  52. package/dist/routes/pages/approvals.d.ts.map +1 -1
  53. package/dist/routes/pages/approvals.js +8 -5
  54. package/dist/routes/pages/approvals.js.map +1 -1
  55. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  56. package/dist/routes/pages/apps.$appId.js +4 -2
  57. package/dist/routes/pages/apps.$appId.js.map +1 -1
  58. package/dist/routes/pages/apps.d.ts.map +1 -1
  59. package/dist/routes/pages/apps.js +9 -3
  60. package/dist/routes/pages/apps.js.map +1 -1
  61. package/dist/routes/pages/audit.d.ts.map +1 -1
  62. package/dist/routes/pages/audit.js +4 -2
  63. package/dist/routes/pages/audit.js.map +1 -1
  64. package/dist/routes/pages/chat.d.ts.map +1 -1
  65. package/dist/routes/pages/chat.js +16 -2
  66. package/dist/routes/pages/chat.js.map +1 -1
  67. package/dist/routes/pages/destinations.d.ts.map +1 -1
  68. package/dist/routes/pages/destinations.js +5 -3
  69. package/dist/routes/pages/destinations.js.map +1 -1
  70. package/dist/routes/pages/identities.d.ts.map +1 -1
  71. package/dist/routes/pages/identities.js +5 -3
  72. package/dist/routes/pages/identities.js.map +1 -1
  73. package/dist/routes/pages/integrations.d.ts.map +1 -1
  74. package/dist/routes/pages/integrations.js +9 -3
  75. package/dist/routes/pages/integrations.js.map +1 -1
  76. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  77. package/dist/routes/pages/thread-debug.js +7 -6
  78. package/dist/routes/pages/thread-debug.js.map +1 -1
  79. package/dist/routes/pages/vault.d.ts.map +1 -1
  80. package/dist/routes/pages/vault.js +23 -7
  81. package/dist/routes/pages/vault.js.map +1 -1
  82. package/dist/routes/pages/workspace.d.ts.map +1 -1
  83. package/dist/routes/pages/workspace.js +9 -3
  84. package/dist/routes/pages/workspace.js.map +1 -1
  85. package/dist/server/lib/dispatch-store.d.ts +24 -21
  86. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  87. package/dist/server/lib/dispatch-store.js +59 -23
  88. package/dist/server/lib/dispatch-store.js.map +1 -1
  89. package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
  90. package/dist/server/lib/mcp-access-store.js +30 -2
  91. package/dist/server/lib/mcp-access-store.js.map +1 -1
  92. package/dist/server/lib/vault-store.d.ts.map +1 -1
  93. package/dist/server/lib/vault-store.js +102 -37
  94. package/dist/server/lib/vault-store.js.map +1 -1
  95. package/dist/server/plugins/agent-chat.d.ts +1 -1
  96. package/package.json +2 -3
  97. package/src/actions/list_apps.spec.ts +52 -0
  98. package/src/actions/list_apps.ts +16 -8
  99. package/src/actions/provider-api-register.spec.ts +193 -0
  100. package/src/actions/provider-api-register.ts +66 -1
  101. package/src/actions/set-mcp-app-access.spec.ts +73 -0
  102. package/src/actions/set-mcp-app-access.ts +1 -5
  103. package/src/components/action-query-error.spec.tsx +56 -0
  104. package/src/components/action-query-error.tsx +32 -0
  105. package/src/components/app-keys-popover.spec.tsx +85 -0
  106. package/src/components/app-keys-popover.tsx +76 -50
  107. package/src/components/dispatch-control-plane.spec.tsx +106 -0
  108. package/src/components/dispatch-control-plane.tsx +71 -115
  109. package/src/components/index.ts +1 -0
  110. package/src/components/layout/Layout.spec.tsx +42 -2
  111. package/src/components/layout/Layout.tsx +22 -27
  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 +20 -7
  124. package/src/routes/pages/audit.tsx +29 -20
  125. package/src/routes/pages/chat.spec.tsx +94 -0
  126. package/src/routes/pages/chat.tsx +30 -10
  127. package/src/routes/pages/destinations.tsx +75 -62
  128. package/src/routes/pages/identities.tsx +74 -63
  129. package/src/routes/pages/integrations.tsx +17 -10
  130. package/src/routes/pages/thread-debug.tsx +18 -12
  131. package/src/routes/pages/vault.tsx +59 -26
  132. package/src/routes/pages/workspace.tsx +69 -55
  133. package/src/server/lib/approval-fencing.spec.ts +420 -0
  134. package/src/server/lib/dispatch-store.ts +94 -21
  135. package/src/server/lib/mcp-access-store.spec.ts +116 -4
  136. package/src/server/lib/mcp-access-store.ts +37 -2
  137. package/src/server/lib/mcp-gateway.spec.ts +79 -3
  138. package/src/server/lib/vault-store.ts +151 -45
@@ -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" />
@@ -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>