@agent-native/dispatch 0.12.2 → 0.12.3

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.
@@ -3,6 +3,7 @@ import {
3
3
  agentNativePath,
4
4
  useActionMutation,
5
5
  useActionQuery,
6
+ useT,
6
7
  } from "@agent-native/core/client";
7
8
  import { AgentsPanel, type ConnectedAgent } from "@/components/agents-panel";
8
9
  import { DispatchShell } from "@/components/dispatch-shell";
@@ -39,12 +40,13 @@ function dispatchMcpUrl(): string {
39
40
  }
40
41
 
41
42
  function DispatchMcpAccessPanel() {
43
+ const t = useT();
42
44
  const { data, isLoading } = useActionQuery("list-mcp-app-access", {});
43
45
  const [optimistic, setOptimistic] = useState<McpAccessState | null>(null);
44
46
  const saveAccess = useActionMutation("set-mcp-app-access", {
45
47
  onSuccess: () => {
46
48
  setOptimistic(null);
47
- toast.success("MCP app access updated");
49
+ toast.success(t("dispatch.pages.mcpAccessUpdated"));
48
50
  },
49
51
  onError: (error) => {
50
52
  setOptimistic(null);
@@ -73,7 +75,7 @@ function DispatchMcpAccessPanel() {
73
75
 
74
76
  function persist(next: McpAccessState) {
75
77
  if (next.mode === "selected-apps" && next.selectedAppIds.length === 0) {
76
- toast.error("Select at least one app, or expose all apps.");
78
+ toast.error(t("dispatch.pages.selectAppForMcp"));
77
79
  return;
78
80
  }
79
81
  setOptimistic(next);
@@ -90,9 +92,9 @@ function DispatchMcpAccessPanel() {
90
92
  async function copyUrl() {
91
93
  try {
92
94
  await navigator.clipboard.writeText(mcpUrl);
93
- toast.success("MCP URL copied");
95
+ toast.success(t("dispatch.pages.mcpUrlCopied"));
94
96
  } catch {
95
- toast.error("Could not copy MCP URL");
97
+ toast.error(t("dispatch.pages.mcpUrlCopyFailed"));
96
98
  }
97
99
  }
98
100
 
@@ -102,21 +104,25 @@ function DispatchMcpAccessPanel() {
102
104
  <div className="min-w-0">
103
105
  <div className="flex items-center gap-2 text-sm font-medium text-foreground">
104
106
  <IconPlugConnected size={16} />
105
- Unified MCP gateway
107
+ {t("dispatch.pages.unifiedMcpGateway")}
106
108
  </div>
107
109
  <div className="mt-1 max-w-2xl text-sm text-muted-foreground">
108
- Connect external agents to Dispatch once, then route to granted
109
- workspace apps through <code>list_apps</code>, <code>ask_app</code>,
110
- and <code>open_app</code>.
110
+ {t("dispatch.pages.unifiedMcpGatewayDescription")}{" "}
111
+ <code>list_apps</code>, <code>ask_app</code>, and{" "}
112
+ <code>open_app</code>.
111
113
  </div>
112
114
  </div>
113
115
  <div className="flex items-center gap-3 rounded-xl border px-3 py-2">
114
116
  <div>
115
117
  <div className="text-xs font-medium text-foreground">
116
- {access.mode === "all-apps" ? "All apps" : "Selected apps"}
118
+ {access.mode === "all-apps"
119
+ ? t("dispatch.pages.allApps")
120
+ : t("dispatch.pages.selectedApps")}
117
121
  </div>
118
122
  <div className="text-xs text-muted-foreground">
119
- {isLoading ? "Loading" : `${grantedCount} granted`}
123
+ {isLoading
124
+ ? t("dispatch.pages.loading")
125
+ : t("dispatch.pages.grantedCount", { count: grantedCount })}
120
126
  </div>
121
127
  </div>
122
128
  <Switch
@@ -130,7 +136,7 @@ function DispatchMcpAccessPanel() {
130
136
  : apps.map((app) => app.id),
131
137
  })
132
138
  }
133
- aria-label="Expose all apps through Dispatch MCP"
139
+ aria-label={t("dispatch.pages.exposeAllAppsMcp")}
134
140
  />
135
141
  </div>
136
142
  </div>
@@ -139,7 +145,7 @@ function DispatchMcpAccessPanel() {
139
145
  <Input readOnly value={mcpUrl} className="font-mono text-xs" />
140
146
  <Button type="button" variant="outline" onClick={copyUrl}>
141
147
  <IconCopy size={15} />
142
- Copy URL
148
+ {t("dispatch.pages.copyUrl")}
143
149
  </Button>
144
150
  </div>
145
151
 
@@ -186,12 +192,13 @@ function DispatchMcpAccessPanel() {
186
192
  }
187
193
 
188
194
  export default function AgentsRoute() {
195
+ const t = useT();
189
196
  const { data, refetch } = useActionQuery("list-connected-agents", {});
190
197
 
191
198
  return (
192
199
  <DispatchShell
193
- title="Agents"
194
- description="Dispatch can delegate to the built-in app suite over A2A by default. Add extra agents here only if you want to route work to apps outside that built-in set."
200
+ title={t("dispatch.nav.agents")}
201
+ description={t("dispatch.pages.agentsDescription")}
195
202
  >
196
203
  <div className="space-y-4">
197
204
  <DispatchMcpAccessPanel />
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo } from "react";
2
2
  import { Link, useParams } from "react-router";
3
- import { useActionQuery } from "@agent-native/core/client";
3
+ import { useActionQuery, useT } from "@agent-native/core/client";
4
4
  import {
5
5
  IconArrowLeft,
6
6
  IconArrowUpRight,
@@ -20,6 +20,7 @@ export function meta() {
20
20
  }
21
21
 
22
22
  export default function WorkspaceAppRoute() {
23
+ const t = useT();
23
24
  const { appId } = useParams();
24
25
  const { data: apps = [], isLoading } = useActionQuery(
25
26
  "list-workspace-apps",
@@ -42,14 +43,14 @@ export default function WorkspaceAppRoute() {
42
43
 
43
44
  return (
44
45
  <DispatchShell
45
- title={app?.name || "Workspace App"}
46
- description="Open a deployed app or check the status of an app being created."
46
+ title={app?.name || t("dispatch.pages.workspaceAppFallback")}
47
+ description={t("dispatch.pages.workspaceAppDescription")}
47
48
  >
48
49
  <div className="max-w-2xl rounded-lg border bg-card p-5">
49
50
  <Button asChild size="sm" variant="ghost" className="-ml-2 mb-4">
50
51
  <Link to="/apps">
51
52
  <IconArrowLeft size={15} className="mr-1.5" />
52
- Apps
53
+ {t("dispatch.nav.apps")}
53
54
  </Link>
54
55
  </Button>
55
56
 
@@ -62,10 +63,10 @@ export default function WorkspaceAppRoute() {
62
63
  ) : !app ? (
63
64
  <div className="space-y-3">
64
65
  <h2 className="text-base font-semibold text-foreground">
65
- App not found
66
+ {t("dispatch.pages.appNotFound")}
66
67
  </h2>
67
68
  <p className="text-sm text-muted-foreground">
68
- This route is not in the workspace app list yet.
69
+ {t("dispatch.pages.pageNotFoundDescription")}
69
70
  </p>
70
71
  </div>
71
72
  ) : app.status === "pending" ? (
@@ -79,23 +80,23 @@ export default function WorkspaceAppRoute() {
79
80
  className="gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
80
81
  >
81
82
  <IconClockHour4 size={12} />
82
- Building
83
+ {t("dispatch.pages.building")}
83
84
  </Badge>
84
85
  </div>
85
86
  <p className="text-sm text-muted-foreground">
86
- This app is being created. It will be available at{" "}
87
+ {t("dispatch.pages.appBuildingPrefix")}{" "}
87
88
  <span className="font-mono text-foreground">{app.path}</span>{" "}
88
- after its branch is merged and the workspace deploy finishes.
89
+ {t("dispatch.pages.appBuildingSuffix")}
89
90
  </p>
90
91
  {app.branchName ? (
91
92
  <p className="text-xs text-muted-foreground">
92
- Branch: {app.branchName}
93
+ {t("dispatch.pages.branch", { branch: app.branchName })}
93
94
  </p>
94
95
  ) : null}
95
96
  {app.builderUrl ? (
96
97
  <Button asChild>
97
98
  <a href={app.builderUrl} target="_blank" rel="noreferrer">
98
- Open Builder branch
99
+ {t("dispatch.pages.openBuilderBranch")}
99
100
  <IconArrowUpRight size={15} className="ml-1.5" />
100
101
  </a>
101
102
  </Button>
@@ -104,16 +105,16 @@ export default function WorkspaceAppRoute() {
104
105
  ) : (
105
106
  <div className="space-y-3">
106
107
  <h2 className="text-base font-semibold text-foreground">
107
- Opening {app.name}
108
+ {t("dispatch.pages.openingApp", { name: app.name })}
108
109
  </h2>
109
110
  <p className="text-sm text-muted-foreground">
110
- Redirecting to{" "}
111
+ {t("dispatch.pages.redirectingTo")}{" "}
111
112
  <span className="font-mono text-foreground">{app.path}</span>.
112
113
  </p>
113
114
  {href ? (
114
115
  <Button asChild>
115
116
  <a href={href}>
116
- Open app
117
+ {t("dispatch.pages.openApp")}
117
118
  <IconArrowUpRight size={15} className="ml-1.5" />
118
119
  </a>
119
120
  </Button>
@@ -1,5 +1,9 @@
1
1
  import { useState } from "react";
2
- import { useActionMutation, useActionQuery } from "@agent-native/core/client";
2
+ import {
3
+ useActionMutation,
4
+ useActionQuery,
5
+ useT,
6
+ } from "@agent-native/core/client";
3
7
  import {
4
8
  IconApps,
5
9
  IconBrain,
@@ -71,6 +75,7 @@ const TEMPLATE_ICONS: Record<string, typeof IconMail> = {
71
75
  };
72
76
 
73
77
  export default function AppsRoute() {
78
+ const t = useT();
74
79
  const [showHidden, setShowHidden] = useState(false);
75
80
  const [templatesOpen, setTemplatesOpen] = useState(false);
76
81
  const { data: apps = [], isLoading: appsLoading } = useActionQuery(
@@ -103,11 +108,13 @@ export default function AppsRoute() {
103
108
 
104
109
  return (
105
110
  <DispatchShell
106
- title="Apps"
111
+ title={t("dispatch.nav.apps")}
107
112
  description={
108
113
  workspaceLabel
109
- ? `Apps in the "${workspaceLabel}" workspace. Each app gets its own route under this workspace and shares its database, auth, and agent chat.`
110
- : "Open workspace apps and start new app creation from Dispatch."
114
+ ? t("dispatch.pages.appsDescriptionWithWorkspace", {
115
+ workspace: workspaceLabel,
116
+ })
117
+ : t("dispatch.pages.appsDescription")
111
118
  }
112
119
  >
113
120
  <div className="space-y-8">
@@ -121,13 +128,19 @@ export default function AppsRoute() {
121
128
  <div className="min-w-0">
122
129
  <h2 className="truncate text-sm font-semibold text-foreground">
123
130
  {workspaceLabel
124
- ? `Apps in ${workspaceLabel}`
125
- : "Workspace apps"}
131
+ ? t("dispatch.pages.appsInWorkspace", {
132
+ workspace: workspaceLabel,
133
+ })
134
+ : t("dispatch.pages.workspaceApps")}
126
135
  </h2>
127
136
  <p className="mt-0.5 text-xs text-muted-foreground">
128
- {visibleApps.length} active
137
+ {t("dispatch.pages.activeCount", {
138
+ count: visibleApps.length,
139
+ })}
129
140
  {archivedApps.length > 0
130
- ? ` · ${archivedApps.length} hidden`
141
+ ? ` · ${t("dispatch.pages.hiddenCount", {
142
+ count: archivedApps.length,
143
+ })}`
131
144
  : ""}
132
145
  </p>
133
146
  </div>
@@ -138,7 +151,7 @@ export default function AppsRoute() {
138
151
  trigger={
139
152
  <Button size="sm">
140
153
  <IconPlus size={15} className="mr-1.5" />
141
- Create app
154
+ {t("dispatch.pages.createApp")}
142
155
  </Button>
143
156
  }
144
157
  />
@@ -169,12 +182,14 @@ export default function AppsRoute() {
169
182
  />
170
183
  <div className="min-w-0">
171
184
  <h2 className="text-sm font-semibold text-foreground">
172
- Templates
185
+ {t("dispatch.pages.templates")}
173
186
  </h2>
174
187
  <p className="text-xs text-muted-foreground">
175
188
  {templatesLoading
176
- ? "Checking available templates"
177
- : `${typedTemplates.length} available to scaffold`}
189
+ ? t("dispatch.pages.checkingTemplates")
190
+ : t("dispatch.pages.templatesAvailable", {
191
+ count: typedTemplates.length,
192
+ })}
178
193
  </p>
179
194
  </div>
180
195
  </div>
@@ -185,7 +200,9 @@ export default function AppsRoute() {
185
200
  size="sm"
186
201
  className="gap-1.5"
187
202
  >
188
- {templatesOpen ? "Hide" : "Show"}
203
+ {templatesOpen
204
+ ? t("dispatch.pages.hide")
205
+ : t("dispatch.pages.show")}
189
206
  <IconChevronDown
190
207
  size={14}
191
208
  className={cn(
@@ -225,11 +242,12 @@ export default function AppsRoute() {
225
242
  />
226
243
  <div className="min-w-0">
227
244
  <h2 className="text-sm font-semibold text-foreground">
228
- Hidden apps
245
+ {t("dispatch.pages.hiddenApps")}
229
246
  </h2>
230
247
  <p className="text-xs text-muted-foreground">
231
- {archivedApps.length} hidden{" "}
232
- {archivedApps.length === 1 ? "app" : "apps"}
248
+ {t("dispatch.pages.hiddenAppCount", {
249
+ count: archivedApps.length,
250
+ })}
233
251
  </p>
234
252
  </div>
235
253
  </div>
@@ -240,7 +258,9 @@ export default function AppsRoute() {
240
258
  size="sm"
241
259
  className="gap-1.5"
242
260
  >
243
- {showHidden ? "Hide" : "Show"}
261
+ {showHidden
262
+ ? t("dispatch.pages.hide")
263
+ : t("dispatch.pages.show")}
244
264
  <IconChevronDown
245
265
  size={14}
246
266
  className={cn(
@@ -293,23 +313,24 @@ function AppsSkeletonGrid() {
293
313
  }
294
314
 
295
315
  function EmptyAppsState() {
316
+ const t = useT();
296
317
  return (
297
318
  <div className="rounded-lg border border-dashed bg-card px-4 py-10 text-center">
298
319
  <div className="mx-auto flex size-10 items-center justify-center rounded-lg bg-muted text-muted-foreground">
299
320
  <IconApps size={18} />
300
321
  </div>
301
322
  <h3 className="mt-3 text-sm font-semibold text-foreground">
302
- No workspace apps yet
323
+ {t("dispatch.pages.noWorkspaceApps")}
303
324
  </h3>
304
325
  <p className="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
305
- Create an app when a workflow needs its own focused place to live.
326
+ {t("dispatch.pages.noWorkspaceAppsDescription")}
306
327
  </p>
307
328
  <div className="mt-4">
308
329
  <CreateAppPopover
309
330
  trigger={
310
331
  <Button size="sm">
311
332
  <IconPlus size={15} className="mr-1.5" />
312
- Create app
333
+ {t("dispatch.pages.createApp")}
313
334
  </Button>
314
335
  }
315
336
  />
@@ -1,5 +1,9 @@
1
1
  import { useState } from "react";
2
- import { useActionMutation, useActionQuery } from "@agent-native/core/client";
2
+ import {
3
+ useActionMutation,
4
+ useActionQuery,
5
+ useT,
6
+ } from "@agent-native/core/client";
3
7
  import { toast } from "sonner";
4
8
  import { DispatchShell } from "@/components/dispatch-shell";
5
9
  import { Button } from "@/components/ui/button";
@@ -33,15 +37,18 @@ function QuickSendRow({
33
37
  }: {
34
38
  destination: { id: string; name: string };
35
39
  }) {
40
+ const t = useT();
36
41
  const [text, setText] = useState("");
37
42
  const send = useActionMutation("send-platform-message", {
38
43
  onSuccess: () => {
39
- toast.success("Message sent");
44
+ toast.success(t("dispatch.pages.messageSent"));
40
45
  setText("");
41
46
  },
42
47
  onError: (error) => {
43
48
  toast.error(
44
- error instanceof Error ? error.message : "Unable to send message",
49
+ error instanceof Error
50
+ ? error.message
51
+ : t("dispatch.pages.unableToSendMessage"),
45
52
  );
46
53
  },
47
54
  });
@@ -50,7 +57,7 @@ function QuickSendRow({
50
57
  <Input
51
58
  value={text}
52
59
  onChange={(event) => setText(event.target.value)}
53
- placeholder="Quick test message"
60
+ placeholder={t("dispatch.pages.quickTestMessage")}
54
61
  />
55
62
  <Button
56
63
  onClick={() =>
@@ -61,13 +68,14 @@ function QuickSendRow({
61
68
  }
62
69
  disabled={send.isPending}
63
70
  >
64
- Send
71
+ {t("dispatch.pages.send")}
65
72
  </Button>
66
73
  </div>
67
74
  );
68
75
  }
69
76
 
70
77
  export default function DestinationsRoute() {
78
+ const t = useT();
71
79
  const { data } = useActionQuery("list-destinations", {});
72
80
  const [form, setForm] = useState({
73
81
  name: "",
@@ -79,7 +87,7 @@ export default function DestinationsRoute() {
79
87
 
80
88
  const upsert = useActionMutation("upsert-destination", {
81
89
  onSuccess: () => {
82
- toast.success("Destination saved");
90
+ toast.success(t("dispatch.pages.destinationSaved"));
83
91
  setForm((current) => ({
84
92
  ...current,
85
93
  name: "",
@@ -90,18 +98,18 @@ export default function DestinationsRoute() {
90
98
  },
91
99
  });
92
100
  const remove = useActionMutation("delete-destination", {
93
- onSuccess: () => toast.success("Destination removed"),
101
+ onSuccess: () => toast.success(t("dispatch.pages.destinationRemoved")),
94
102
  });
95
103
 
96
104
  return (
97
105
  <DispatchShell
98
- title="Destinations"
99
- description="Saved outbound Slack channels, Telegram chats, and thread targets."
106
+ title={t("dispatch.nav.destinations")}
107
+ description={t("dispatch.pages.destinationsDescription")}
100
108
  >
101
109
  <div className="grid gap-4 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
102
110
  <section className="rounded-2xl border bg-card p-5">
103
111
  <h2 className="text-lg font-semibold text-foreground">
104
- Saved destinations
112
+ {t("dispatch.pages.savedDestinations")}
105
113
  </h2>
106
114
  <div className="mt-4 space-y-3">
107
115
  {(data || []).map((destination: any) => (
@@ -129,24 +137,28 @@ export default function DestinationsRoute() {
129
137
  <AlertDialog>
130
138
  <AlertDialogTrigger asChild>
131
139
  <Button variant="outline" size="sm">
132
- Delete
140
+ {t("dispatch.pages.delete")}
133
141
  </Button>
134
142
  </AlertDialogTrigger>
135
143
  <AlertDialogContent>
136
144
  <AlertDialogHeader>
137
- <AlertDialogTitle>Delete destination?</AlertDialogTitle>
145
+ <AlertDialogTitle>
146
+ {t("dispatch.pages.deleteDestinationTitle")}
147
+ </AlertDialogTitle>
138
148
  <AlertDialogDescription>
139
- {destination.name}” will be removed. Any saved
140
- workflows or jobs that target this destination will
141
- start failing on the next send.
149
+ {t("dispatch.pages.deleteDestinationDescription", {
150
+ name: destination.name,
151
+ })}
142
152
  </AlertDialogDescription>
143
153
  </AlertDialogHeader>
144
154
  <AlertDialogFooter>
145
- <AlertDialogCancel>Cancel</AlertDialogCancel>
155
+ <AlertDialogCancel>
156
+ {t("dispatch.pages.cancel")}
157
+ </AlertDialogCancel>
146
158
  <AlertDialogAction
147
159
  onClick={() => remove.mutate({ id: destination.id })}
148
160
  >
149
- Delete
161
+ {t("dispatch.pages.delete")}
150
162
  </AlertDialogAction>
151
163
  </AlertDialogFooter>
152
164
  </AlertDialogContent>
@@ -157,8 +169,7 @@ export default function DestinationsRoute() {
157
169
  ))}
158
170
  {(data?.length || 0) === 0 && (
159
171
  <div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
160
- No destinations saved yet. Add your first Slack channel or
161
- Telegram chat on the right.
172
+ {t("dispatch.pages.noDestinations")}
162
173
  </div>
163
174
  )}
164
175
  </div>
@@ -166,7 +177,7 @@ export default function DestinationsRoute() {
166
177
 
167
178
  <section className="rounded-2xl border bg-card p-5">
168
179
  <h2 className="text-lg font-semibold text-foreground">
169
- Add destination
180
+ {t("dispatch.pages.addDestination")}
170
181
  </h2>
171
182
  <div className="mt-4 space-y-3">
172
183
  <Input
@@ -174,7 +185,7 @@ export default function DestinationsRoute() {
174
185
  onChange={(event) =>
175
186
  setForm((current) => ({ ...current, name: event.target.value }))
176
187
  }
177
- placeholder="Daily digest channel"
188
+ placeholder={t("dispatch.pages.dailyDigestChannel")}
178
189
  />
179
190
  <Select
180
191
  value={form.platform}
@@ -218,7 +229,7 @@ export default function DestinationsRoute() {
218
229
  threadRef: event.target.value,
219
230
  }))
220
231
  }
221
- placeholder="Optional thread or topic id"
232
+ placeholder={t("dispatch.pages.optionalThreadOrTopicId")}
222
233
  />
223
234
  <Textarea
224
235
  value={form.notes}
@@ -228,7 +239,7 @@ export default function DestinationsRoute() {
228
239
  notes: event.target.value,
229
240
  }))
230
241
  }
231
- placeholder="What should use this destination?"
242
+ placeholder={t("dispatch.pages.destinationNotes")}
232
243
  />
233
244
  <Button
234
245
  className="w-full"
@@ -243,7 +254,7 @@ export default function DestinationsRoute() {
243
254
  }
244
255
  disabled={!form.name || !form.destination}
245
256
  >
246
- Save destination
257
+ {t("dispatch.pages.saveDestination")}
247
258
  </Button>
248
259
  </div>
249
260
  </section>
@@ -1,5 +1,5 @@
1
1
  import { useMemo, useState, type ReactNode } from "react";
2
- import { useActionQuery } from "@agent-native/core/client";
2
+ import { useActionQuery, useT } from "@agent-native/core/client";
3
3
  import {
4
4
  IconActivity,
5
5
  IconAlertTriangle,
@@ -637,6 +637,7 @@ function RecentTable({
637
637
  }
638
638
 
639
639
  export default function MetricsRoute() {
640
+ const t = useT();
640
641
  const [sinceDays, setSinceDays] = useState(30);
641
642
  const { data, isLoading, error } = useActionQuery(
642
643
  "list-dispatch-usage-metrics",
@@ -657,11 +658,11 @@ export default function MetricsRoute() {
657
658
 
658
659
  return (
659
660
  <DispatchShell
660
- title="Metrics"
661
+ title={t("dispatch.nav.metrics")}
661
662
  description={
662
663
  billing.unit === "builder-credits"
663
- ? "Workspace-wide Builder.io credit spend, chat volume, user activity, and app access."
664
- : "Workspace-wide LLM spend, chat volume, user activity, and app access."
664
+ ? t("dispatch.pages.metricsDescriptionBuilder")
665
+ : t("dispatch.pages.metricsDescriptionLlm")
665
666
  }
666
667
  >
667
668
  <div className="space-y-4">
@@ -677,9 +678,11 @@ export default function MetricsRoute() {
677
678
  {error ? (
678
679
  <Alert variant="destructive">
679
680
  <IconAlertTriangle className="h-4 w-4" />
680
- <AlertTitle>Metrics unavailable</AlertTitle>
681
+ <AlertTitle>{t("dispatch.pages.metricsUnavailable")}</AlertTitle>
681
682
  <AlertDescription>
682
- {error instanceof Error ? error.message : "Unable to load usage."}
683
+ {error instanceof Error
684
+ ? error.message
685
+ : t("dispatch.pages.unableToLoadUsage")}
683
686
  </AlertDescription>
684
687
  </Alert>
685
688
  ) : null}
@@ -696,25 +699,25 @@ export default function MetricsRoute() {
696
699
  icon={<IconCoin size={17} />}
697
700
  />
698
701
  <MetricCard
699
- label="LLM calls"
702
+ label={t("dispatch.pages.llmCalls")}
700
703
  value={formatNumber(metrics.totals.calls)}
701
704
  detail={`${formatNumber(metrics.totals.chatCalls)} chat turns`}
702
705
  icon={<IconActivity size={17} />}
703
706
  />
704
707
  <MetricCard
705
- label="Active users"
708
+ label={t("dispatch.pages.activeUsers")}
706
709
  value={formatNumber(metrics.totals.activeUsers)}
707
710
  detail={`${formatNumber(metrics.access.totalUsers)} users with access`}
708
711
  icon={<IconUsersGroup size={17} />}
709
712
  />
710
713
  <MetricCard
711
- label="Workspace apps"
714
+ label={t("dispatch.pages.workspaceAppsStat")}
712
715
  value={formatNumber(metrics.totals.workspaceApps)}
713
716
  detail={`${formatNumber(metrics.byApp.length)} with usage`}
714
717
  icon={<IconApps size={17} />}
715
718
  />
716
719
  <MetricCard
717
- label="Chat threads"
720
+ label={t("dispatch.pages.chatThreads")}
718
721
  value={formatNumber(metrics.totals.chatThreads)}
719
722
  detail={`${formatNumber(metrics.totals.chatMessages)} messages`}
720
723
  icon={<IconMessages size={17} />}
@@ -1,4 +1,4 @@
1
- import { NewWorkspaceAppFlow } from "@agent-native/core/client";
1
+ import { NewWorkspaceAppFlow, useT } from "@agent-native/core/client";
2
2
  import { DispatchShell } from "@/components/dispatch-shell";
3
3
 
4
4
  export function meta() {
@@ -6,10 +6,11 @@ export function meta() {
6
6
  }
7
7
 
8
8
  export default function NewAppRoute() {
9
+ const t = useT();
9
10
  return (
10
11
  <DispatchShell
11
- title="New App"
12
- description="Create a workspace app from a prompt and apply the workspace vault policy."
12
+ title={t("dispatch.pages.newApp")}
13
+ description={t("dispatch.pages.newAppDescription")}
13
14
  >
14
15
  <NewWorkspaceAppFlow sourceApp="dispatch" className="px-0 py-0" />
15
16
  </DispatchShell>