@agent-native/dispatch 0.12.1 → 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.
Files changed (37) hide show
  1. package/dist/components/dispatch-shell.d.ts.map +1 -1
  2. package/dist/components/dispatch-shell.js +3 -1
  3. package/dist/components/dispatch-shell.js.map +1 -1
  4. package/dist/components/layout/Layout.d.ts.map +1 -1
  5. package/dist/components/layout/Layout.js +39 -20
  6. package/dist/components/layout/Layout.js.map +1 -1
  7. package/dist/routes/pages/$appId.d.ts.map +1 -1
  8. package/dist/routes/pages/$appId.js +3 -2
  9. package/dist/routes/pages/$appId.js.map +1 -1
  10. package/dist/routes/pages/agents.d.ts.map +1 -1
  11. package/dist/routes/pages/agents.js +14 -8
  12. package/dist/routes/pages/agents.js.map +1 -1
  13. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  14. package/dist/routes/pages/apps.$appId.js +3 -2
  15. package/dist/routes/pages/apps.$appId.js.map +1 -1
  16. package/dist/routes/pages/apps.d.ts.map +1 -1
  17. package/dist/routes/pages/apps.js +29 -11
  18. package/dist/routes/pages/apps.js.map +1 -1
  19. package/dist/routes/pages/destinations.d.ts.map +1 -1
  20. package/dist/routes/pages/destinations.js +18 -12
  21. package/dist/routes/pages/destinations.js.map +1 -1
  22. package/dist/routes/pages/metrics.d.ts.map +1 -1
  23. package/dist/routes/pages/metrics.js +8 -5
  24. package/dist/routes/pages/metrics.js.map +1 -1
  25. package/dist/routes/pages/new-app.d.ts.map +1 -1
  26. package/dist/routes/pages/new-app.js +3 -2
  27. package/dist/routes/pages/new-app.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/components/dispatch-shell.tsx +3 -1
  30. package/src/components/layout/Layout.tsx +63 -28
  31. package/src/routes/pages/$appId.tsx +15 -12
  32. package/src/routes/pages/agents.tsx +21 -14
  33. package/src/routes/pages/apps.$appId.tsx +15 -14
  34. package/src/routes/pages/apps.tsx +41 -20
  35. package/src/routes/pages/destinations.tsx +35 -24
  36. package/src/routes/pages/metrics.tsx +13 -10
  37. package/src/routes/pages/new-app.tsx +4 -3
@@ -19,6 +19,8 @@ import {
19
19
  useAgentChatHomeHandoff,
20
20
  useAgentChatHomeHandoffLinks,
21
21
  useChatThreads,
22
+ useFormatters,
23
+ useT,
22
24
  type ChatThreadSummary,
23
25
  } from "@agent-native/core/client";
24
26
  import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
@@ -222,12 +224,6 @@ const OPERATIONS_NAV_ITEMS = [
222
224
 
223
225
  const EMPTY_NAV_ITEMS: readonly DispatchNavItem[] = [];
224
226
 
225
- const SIDEBAR_SUGGESTIONS = [
226
- "Build a workspace app for X",
227
- "Route Slack mentions to my analytics app",
228
- "Grant my OpenAI key to this app",
229
- ];
230
-
231
227
  const CHROMELESS_PATHS = ["/approval"];
232
228
 
233
229
  // Routes whose page renders its own toolbar (with NotificationsBell + AgentToggleButton).
@@ -309,7 +305,10 @@ function threadIdFromPath(pathname: string): string | null {
309
305
  }
310
306
  }
311
307
 
312
- function formatThreadAge(updatedAt: number) {
308
+ function formatThreadAge(
309
+ updatedAt: number,
310
+ formatDate: ReturnType<typeof useFormatters>["formatDate"],
311
+ ) {
313
312
  const diffMs = Math.max(0, Date.now() - updatedAt);
314
313
  const minutes = Math.floor(diffMs / 60_000);
315
314
  if (minutes < 1) return "now";
@@ -318,14 +317,14 @@ function formatThreadAge(updatedAt: number) {
318
317
  if (hours < 24) return `${hours}h`;
319
318
  const days = Math.floor(hours / 24);
320
319
  if (days < 7) return `${days}d`;
321
- return new Date(updatedAt).toLocaleDateString([], {
320
+ return formatDate(updatedAt, {
322
321
  month: "short",
323
322
  day: "numeric",
324
323
  });
325
324
  }
326
325
 
327
- function threadTitle(thread: ChatThreadSummary) {
328
- return thread.title || thread.preview || "New chat";
326
+ function threadTitle(thread: ChatThreadSummary, fallback: string) {
327
+ return thread.title || thread.preview || fallback;
329
328
  }
330
329
 
331
330
  function threadUpdatedAt(thread: ChatThreadSummary) {
@@ -337,6 +336,8 @@ function threadUpdatedAt(thread: ChatThreadSummary) {
337
336
  }
338
337
 
339
338
  function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
339
+ const t = useT();
340
+ const { formatDate } = useFormatters();
340
341
  const navigate = useNavigate();
341
342
  const location = useLocation();
342
343
  const {
@@ -415,7 +416,7 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
415
416
 
416
417
  function startRenameThread(thread: ChatThreadSummary) {
417
418
  committingRenameRef.current = false;
418
- setRenameDraft(threadTitle(thread));
419
+ setRenameDraft(threadTitle(thread, t("dispatch.sidebar.newChat")));
419
420
  setRenamingThreadId(thread.id);
420
421
  }
421
422
 
@@ -446,7 +447,7 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
446
447
  <div className="mt-2 border-s border-sidebar-border/70 ps-3">
447
448
  <div className="mb-1 flex h-7 items-center gap-2 pe-1">
448
449
  <div className="min-w-0 flex-1 text-xs font-medium text-sidebar-foreground/70">
449
- Chats
450
+ {t("dispatch.sidebar.chats")}
450
451
  </div>
451
452
  <Tooltip>
452
453
  <TooltipTrigger asChild>
@@ -454,12 +455,12 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
454
455
  type="button"
455
456
  onClick={handleNewChat}
456
457
  className="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-sidebar-foreground/65 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
457
- aria-label="New Dispatch chat"
458
+ aria-label={t("dispatch.sidebar.newDispatchChat")}
458
459
  >
459
460
  <IconPlus className="size-3.5" />
460
461
  </button>
461
462
  </TooltipTrigger>
462
- <TooltipContent>New chat</TooltipContent>
463
+ <TooltipContent>{t("dispatch.sidebar.newChat")}</TooltipContent>
463
464
  </Tooltip>
464
465
  </div>
465
466
  <div className="grid gap-0.5">
@@ -498,7 +499,12 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
498
499
  }
499
500
  }}
500
501
  maxLength={160}
501
- aria-label={`Rename ${threadTitle(thread)}`}
502
+ aria-label={t("dispatch.sidebar.renameThread", {
503
+ title: threadTitle(
504
+ thread,
505
+ t("dispatch.sidebar.newChat"),
506
+ ),
507
+ })}
502
508
  className="h-6 min-w-0 rounded-sm border-sidebar-border bg-background px-1.5 text-xs"
503
509
  />
504
510
  </form>
@@ -510,20 +516,28 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
510
516
  className="flex h-full min-w-0 flex-1 cursor-pointer items-center px-2 text-start outline-none focus-visible:ring-2 focus-visible:ring-ring"
511
517
  >
512
518
  <span className="min-w-0 flex-1 truncate">
513
- {threadTitle(thread)}
519
+ {threadTitle(thread, t("dispatch.sidebar.newChat"))}
514
520
  </span>
515
521
  </button>
516
522
  <div className="relative flex size-7 shrink-0 items-center justify-end pe-1">
517
523
  <span className="text-[11px] text-sidebar-foreground/50 transition-opacity group-hover:opacity-0 group-focus-within:opacity-0">
518
524
  {isActive
519
525
  ? ""
520
- : formatThreadAge(threadUpdatedAt(thread))}
526
+ : formatThreadAge(
527
+ threadUpdatedAt(thread),
528
+ formatDate,
529
+ )}
521
530
  </span>
522
531
  <DropdownMenu>
523
532
  <DropdownMenuTrigger asChild>
524
533
  <button
525
534
  type="button"
526
- aria-label={`Chat options for ${threadTitle(thread)}`}
535
+ aria-label={t("dispatch.sidebar.chatOptions", {
536
+ title: threadTitle(
537
+ thread,
538
+ t("dispatch.sidebar.newChat"),
539
+ ),
540
+ })}
527
541
  className="absolute end-1 flex size-6 cursor-pointer items-center justify-center rounded-md text-sidebar-foreground/65 opacity-0 transition-opacity hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100 group-focus-within:opacity-100 data-[state=open]:opacity-100"
528
542
  >
529
543
  <IconDots className="size-4" />
@@ -538,7 +552,7 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
538
552
  onSelect={() => startRenameThread(thread)}
539
553
  >
540
554
  <IconEdit className="size-4" />
541
- Rename chat
555
+ {t("dispatch.sidebar.renameChat")}
542
556
  </DropdownMenuItem>
543
557
  </DropdownMenuContent>
544
558
  </DropdownMenu>
@@ -554,7 +568,7 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
554
568
  onClick={handleNewChat}
555
569
  className="flex h-8 cursor-pointer items-center rounded-md px-2 text-start text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent/65 hover:text-sidebar-accent-foreground"
556
570
  >
557
- <span className="truncate">New chat</span>
571
+ <span className="truncate">{t("dispatch.sidebar.newChat")}</span>
558
572
  </button>
559
573
  )}
560
574
  </div>
@@ -569,6 +583,7 @@ export function NavContent({
569
583
  onNavigate?: () => void;
570
584
  extensions?: DispatchExtensionConfig;
571
585
  }) {
586
+ const t = useT();
572
587
  const location = useLocation();
573
588
  const navigate = useNavigate();
574
589
  const { data: workspace } = useActionQuery(
@@ -591,10 +606,20 @@ export function NavContent({
591
606
  const operationsOpen = operationsNavItems.some((item) =>
592
607
  navItemMatchesPath(item, localPathname),
593
608
  );
609
+ const navLabel = (item: DispatchNavItem) => {
610
+ const key =
611
+ item.id === "thread-debug"
612
+ ? "threadDebug"
613
+ : item.id === "workspace"
614
+ ? "resources"
615
+ : item.id;
616
+ return t(`dispatch.nav.${key}`, { defaultValue: item.label });
617
+ };
594
618
 
595
619
  const renderNavItem = (item: DispatchNavItem) => {
596
620
  const Icon = item.icon;
597
621
  const itemMatchesLocalPath = navItemMatchesPath(item, localPathname);
622
+ const label = navLabel(item);
598
623
  return (
599
624
  <li key={item.id}>
600
625
  <NavLink
@@ -633,7 +658,7 @@ export function NavContent({
633
658
  ) : (
634
659
  <span className="h-4 w-4 shrink-0" aria-hidden="true" />
635
660
  )}
636
- <span className="truncate">{item.label}</span>
661
+ <span className="truncate">{label}</span>
637
662
  </NavLink>
638
663
  {item.id === "chat" && itemMatchesLocalPath ? (
639
664
  <DispatchChatsSection onNavigate={onNavigate} />
@@ -666,8 +691,10 @@ export function NavContent({
666
691
  </div>
667
692
  <div className="truncate text-xs text-muted-foreground">
668
693
  {workspaceLabel
669
- ? `Workspace · ${ws?.appCount ?? 0} app${ws?.appCount === 1 ? "" : "s"}`
670
- : "Workspace control plane"}
694
+ ? t("dispatch.sidebar.workspaceSubtitle", {
695
+ count: ws?.appCount ?? 0,
696
+ })
697
+ : t("dispatch.sidebar.workspaceControlPlane")}
671
698
  </div>
672
699
  </div>
673
700
  </div>
@@ -682,7 +709,7 @@ export function NavContent({
682
709
  <div className="border-t px-2 py-2">
683
710
  <details className="group" open={operationsOpen}>
684
711
  <summary className="flex h-8 cursor-pointer list-none items-center justify-between rounded-md px-2 text-xs font-medium uppercase text-sidebar-foreground/50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&::-webkit-details-marker]:hidden">
685
- <span>Operations</span>
712
+ <span>{t("dispatch.nav.operations")}</span>
686
713
  <IconChevronDown
687
714
  size={14}
688
715
  className="transition-transform group-open:rotate-180"
@@ -718,6 +745,7 @@ export function Layout({
718
745
  children: ReactNode;
719
746
  extensions?: DispatchExtensionConfig;
720
747
  }) {
748
+ const t = useT();
721
749
  const location = useLocation();
722
750
  const navigate = useNavigate();
723
751
  const [mobileOpen, setMobileOpen] = useState(false);
@@ -748,6 +776,11 @@ export function Layout({
748
776
  dispatchNavLinkTarget("/chat"),
749
777
  );
750
778
  }
779
+ const sidebarSuggestions = [
780
+ t("dispatch.sidebar.suggestionBuildApp"),
781
+ t("dispatch.sidebar.suggestionRouteSlack"),
782
+ t("dispatch.sidebar.suggestionGrantKey"),
783
+ ];
751
784
  const appContent = (
752
785
  <div className="flex h-full min-w-0 flex-1 flex-col overflow-hidden">
753
786
  {showHeader ? <Header onOpenMobile={() => setMobileOpen(true)} /> : null}
@@ -778,8 +811,8 @@ export function Layout({
778
811
  storageKey="dispatch"
779
812
  openOnChatRunning={chatHomeHandoffActive}
780
813
  onFullscreenRequest={openAskAgentFullscreen}
781
- emptyStateText="Create apps, manage vault keys, and route work across the workspace."
782
- suggestions={SIDEBAR_SUGGESTIONS}
814
+ emptyStateText={t("dispatch.sidebar.emptyAgentText")}
815
+ suggestions={sidebarSuggestions}
783
816
  >
784
817
  {appContent}
785
818
  </AgentSidebar>
@@ -797,9 +830,11 @@ export function Layout({
797
830
  side="left"
798
831
  className="w-72 p-0 bg-sidebar text-sidebar-foreground [&>button]:hidden"
799
832
  >
800
- <SheetTitle className="sr-only">Navigation</SheetTitle>
833
+ <SheetTitle className="sr-only">
834
+ {t("dispatch.nav.navigation")}
835
+ </SheetTitle>
801
836
  <SheetDescription className="sr-only">
802
- Workspace navigation links
837
+ {t("dispatch.nav.navigationDescription")}
803
838
  </SheetDescription>
804
839
  <div className="flex h-full w-full flex-col">
805
840
  <NavContent
@@ -7,7 +7,7 @@ import {
7
7
  type ClientLoaderFunctionArgs,
8
8
  type LoaderFunctionArgs,
9
9
  } from "react-router";
10
- import { useActionQuery, appPath } from "@agent-native/core/client";
10
+ import { useActionQuery, appPath, useT } from "@agent-native/core/client";
11
11
  import {
12
12
  IconArrowLeft,
13
13
  IconArrowUpRight,
@@ -93,6 +93,7 @@ export async function clientLoader({
93
93
  }
94
94
 
95
95
  export default function WorkspaceAppCatchAllRoute() {
96
+ const t = useT();
96
97
  const { appId } = useParams();
97
98
  const { data: apps = [], isLoading } = useActionQuery(
98
99
  "list-workspace-apps",
@@ -127,14 +128,14 @@ export default function WorkspaceAppCatchAllRoute() {
127
128
 
128
129
  return (
129
130
  <DispatchShell
130
- title={app?.name || "Page not found"}
131
- description="This route is not in the workspace app list yet."
131
+ title={app?.name || t("dispatch.pages.pageNotFound")}
132
+ description={t("dispatch.pages.pageNotFoundDescription")}
132
133
  >
133
134
  <div className="max-w-2xl rounded-lg border bg-card p-5">
134
135
  <Button asChild size="sm" variant="ghost" className="-ml-2 mb-4">
135
136
  <Link to={appPath("/overview")}>
136
137
  <IconArrowLeft size={15} className="mr-1.5" />
137
- Overview
138
+ {t("dispatch.nav.overview")}
138
139
  </Link>
139
140
  </Button>
140
141
 
@@ -149,23 +150,23 @@ export default function WorkspaceAppCatchAllRoute() {
149
150
  className="gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
150
151
  >
151
152
  <IconClockHour4 size={12} />
152
- Building
153
+ {t("dispatch.pages.building")}
153
154
  </Badge>
154
155
  </div>
155
156
  <p className="text-sm text-muted-foreground">
156
- This app is being created. It will be available at{" "}
157
+ {t("dispatch.pages.appBuildingPrefix")}{" "}
157
158
  <span className="font-mono text-foreground">{app.path}</span>{" "}
158
- after its branch is merged and the workspace deploy finishes.
159
+ {t("dispatch.pages.appBuildingSuffix")}
159
160
  </p>
160
161
  {app.branchName ? (
161
162
  <p className="text-xs text-muted-foreground">
162
- Branch: {app.branchName}
163
+ {t("dispatch.pages.branch", { branch: app.branchName })}
163
164
  </p>
164
165
  ) : null}
165
166
  {app.builderUrl ? (
166
167
  <Button asChild>
167
168
  <a href={app.builderUrl} target="_blank" rel="noreferrer">
168
- Open Builder branch
169
+ {t("dispatch.pages.openBuilderBranch")}
169
170
  <IconArrowUpRight size={15} className="ml-1.5" />
170
171
  </a>
171
172
  </Button>
@@ -174,14 +175,16 @@ export default function WorkspaceAppCatchAllRoute() {
174
175
  ) : (
175
176
  <div className="space-y-3">
176
177
  <h2 className="text-base font-semibold text-foreground">
177
- Page not found
178
+ {t("dispatch.pages.pageNotFound")}
178
179
  </h2>
179
180
  <p className="text-sm text-muted-foreground">
180
181
  <span className="font-mono text-foreground">/{appId}</span> isn't
181
- a Dispatch tab or a workspace app in this workspace.
182
+ {t("dispatch.pages.notDispatchOrWorkspaceApp")}
182
183
  </p>
183
184
  <Button asChild>
184
- <Link to={appPath("/apps")}>Browse apps</Link>
185
+ <Link to={appPath("/apps")}>
186
+ {t("dispatch.pages.browseApps")}
187
+ </Link>
185
188
  </Button>
186
189
  </div>
187
190
  )}
@@ -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>