@agent-native/dispatch 0.18.0 → 0.19.0

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 (128) hide show
  1. package/dist/actions/create-workspace-resource.d.ts +13 -13
  2. package/dist/actions/delete-workspace-resource.d.ts +13 -13
  3. package/dist/actions/grant-vault-secrets-to-app.js +1 -1
  4. package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
  5. package/dist/actions/list-vault-audit.js +1 -1
  6. package/dist/actions/list-vault-audit.js.map +1 -1
  7. package/dist/actions/list-vault-secret-options.js +2 -9
  8. package/dist/actions/list-vault-secret-options.js.map +1 -1
  9. package/dist/actions/list-vault-secrets.js +1 -1
  10. package/dist/actions/list-vault-secrets.js.map +1 -1
  11. package/dist/actions/provider-api-register.d.ts +7 -7
  12. package/dist/actions/set-app-creation-settings.js +1 -1
  13. package/dist/actions/set-app-creation-settings.js.map +1 -1
  14. package/dist/actions/set-vault-access-settings.js +1 -1
  15. package/dist/actions/set-vault-access-settings.js.map +1 -1
  16. package/dist/actions/sync-vault-to-app.js +1 -1
  17. package/dist/actions/sync-vault-to-app.js.map +1 -1
  18. package/dist/actions/view-screen.js +4 -3
  19. package/dist/actions/view-screen.js.map +1 -1
  20. package/dist/components/admin-navigation.d.ts +15 -0
  21. package/dist/components/admin-navigation.d.ts.map +1 -0
  22. package/dist/components/admin-navigation.js +174 -0
  23. package/dist/components/admin-navigation.js.map +1 -0
  24. package/dist/components/automation-run-history-dialog.js +1 -1
  25. package/dist/components/automation-run-history-dialog.js.map +1 -1
  26. package/dist/components/index.d.ts +1 -0
  27. package/dist/components/index.d.ts.map +1 -1
  28. package/dist/components/index.js +1 -0
  29. package/dist/components/index.js.map +1 -1
  30. package/dist/components/layout/Header.d.ts.map +1 -1
  31. package/dist/components/layout/Header.js +9 -0
  32. package/dist/components/layout/Header.js.map +1 -1
  33. package/dist/components/layout/Layout.d.ts +4 -1
  34. package/dist/components/layout/Layout.d.ts.map +1 -1
  35. package/dist/components/layout/Layout.js +14 -117
  36. package/dist/components/layout/Layout.js.map +1 -1
  37. package/dist/components/layout/workspace-apps-rail.d.ts +5 -0
  38. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -0
  39. package/dist/components/layout/workspace-apps-rail.js +76 -0
  40. package/dist/components/layout/workspace-apps-rail.js.map +1 -0
  41. package/dist/hooks/use-navigation-state.js +39 -16
  42. package/dist/hooks/use-navigation-state.js.map +1 -1
  43. package/dist/lib/automation-display.js +1 -1
  44. package/dist/lib/automation-display.js.map +1 -1
  45. package/dist/routes/index.d.ts.map +1 -1
  46. package/dist/routes/index.js +22 -0
  47. package/dist/routes/index.js.map +1 -1
  48. package/dist/routes/pages/admin._index.d.ts +5 -0
  49. package/dist/routes/pages/admin._index.d.ts.map +1 -0
  50. package/dist/routes/pages/admin._index.js +19 -0
  51. package/dist/routes/pages/admin._index.js.map +1 -0
  52. package/dist/routes/pages/admin.d.ts +5 -0
  53. package/dist/routes/pages/admin.d.ts.map +1 -0
  54. package/dist/routes/pages/admin.js +10 -0
  55. package/dist/routes/pages/admin.js.map +1 -0
  56. package/dist/routes/pages/approval.js +2 -2
  57. package/dist/routes/pages/approval.js.map +1 -1
  58. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  59. package/dist/routes/pages/apps.$appId.js +3 -2
  60. package/dist/routes/pages/apps.$appId.js.map +1 -1
  61. package/dist/routes/pages/integrations.d.ts.map +1 -1
  62. package/dist/routes/pages/integrations.js +2 -1
  63. package/dist/routes/pages/integrations.js.map +1 -1
  64. package/dist/routes/pages/operations.js +3 -3
  65. package/dist/routes/pages/operations.js.map +1 -1
  66. package/dist/routes/pages/settings.d.ts.map +1 -1
  67. package/dist/routes/pages/settings.js +1 -1
  68. package/dist/routes/pages/settings.js.map +1 -1
  69. package/dist/routes/pages/team.js +1 -1
  70. package/dist/routes/pages/team.js.map +1 -1
  71. package/dist/routes/pages/transactional-email.$appId.$id.d.ts.map +1 -1
  72. package/dist/routes/pages/transactional-email.$appId.$id.js +5 -2
  73. package/dist/routes/pages/transactional-email.$appId.$id.js.map +1 -1
  74. package/dist/routes/pages/transactional-email.d.ts.map +1 -1
  75. package/dist/routes/pages/transactional-email.js +6 -2
  76. package/dist/routes/pages/transactional-email.js.map +1 -1
  77. package/dist/routes/pages/vault.d.ts.map +1 -1
  78. package/dist/routes/pages/vault.js +20 -16
  79. package/dist/routes/pages/vault.js.map +1 -1
  80. package/dist/server/lib/app-creation-store.js +2 -2
  81. package/dist/server/lib/app-creation-store.js.map +1 -1
  82. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  83. package/dist/server/lib/thread-debug-store.js +2 -1
  84. package/dist/server/lib/thread-debug-store.js.map +1 -1
  85. package/dist/server/lib/vault-store.d.ts +15 -0
  86. package/dist/server/lib/vault-store.d.ts.map +1 -1
  87. package/dist/server/lib/vault-store.js +110 -17
  88. package/dist/server/lib/vault-store.js.map +1 -1
  89. package/package.json +3 -3
  90. package/src/actions/grant-vault-secrets-to-app.ts +1 -1
  91. package/src/actions/list-vault-audit.ts +1 -1
  92. package/src/actions/list-vault-secret-options.ts +2 -9
  93. package/src/actions/list-vault-secrets.ts +1 -1
  94. package/src/actions/set-app-creation-settings.ts +1 -1
  95. package/src/actions/set-vault-access-settings.ts +1 -1
  96. package/src/actions/sync-vault-to-app.ts +1 -1
  97. package/src/actions/view-screen.spec.ts +1 -0
  98. package/src/actions/view-screen.ts +5 -3
  99. package/src/components/admin-navigation.tsx +278 -0
  100. package/src/components/automation-run-history-dialog.tsx +1 -1
  101. package/src/components/index.ts +1 -0
  102. package/src/components/layout/Header.tsx +8 -0
  103. package/src/components/layout/Layout.spec.tsx +85 -6
  104. package/src/components/layout/Layout.tsx +62 -206
  105. package/src/components/layout/workspace-apps-rail.tsx +154 -0
  106. package/src/hooks/use-navigation-state.spec.ts +11 -0
  107. package/src/hooks/use-navigation-state.ts +37 -16
  108. package/src/lib/automation-display.spec.ts +1 -1
  109. package/src/lib/automation-display.ts +1 -1
  110. package/src/routes/index.spec.ts +8 -0
  111. package/src/routes/index.ts +25 -0
  112. package/src/routes/pages/admin._index.tsx +39 -0
  113. package/src/routes/pages/admin.tsx +15 -0
  114. package/src/routes/pages/approval.tsx +2 -2
  115. package/src/routes/pages/apps.$appId.tsx +7 -2
  116. package/src/routes/pages/integrations.tsx +3 -2
  117. package/src/routes/pages/operations.tsx +3 -3
  118. package/src/routes/pages/settings.tsx +7 -3
  119. package/src/routes/pages/team.tsx +1 -1
  120. package/src/routes/pages/transactional-email.$appId.$id.tsx +9 -2
  121. package/src/routes/pages/transactional-email.tsx +6 -2
  122. package/src/routes/pages/vault.tsx +152 -89
  123. package/src/server/lib/app-creation-store.ts +2 -2
  124. package/src/server/lib/approval-fencing.spec.ts +69 -0
  125. package/src/server/lib/thread-debug-store.spec.ts +7 -3
  126. package/src/server/lib/thread-debug-store.ts +2 -1
  127. package/src/server/lib/vault-store.spec.ts +59 -0
  128. package/src/server/lib/vault-store.ts +123 -17
@@ -18,38 +18,26 @@ import {
18
18
  type ChatHistoryItem,
19
19
  } from "@agent-native/toolkit/chat-history";
20
20
  import {
21
- IconActivity,
22
- IconArrowUpRight,
23
21
  IconApps,
24
- IconBrain,
25
22
  IconBrandSlack,
26
- IconChartBar,
27
23
  IconBrandTelegram,
28
- IconKey,
29
- IconChevronDown,
30
- IconLayersSubtract,
31
- IconMail,
32
24
  IconMessageQuestion,
33
- IconMessages,
34
- IconPlugConnected,
35
25
  IconBroadcast,
36
- IconFingerprint,
37
- IconHistory,
38
26
  IconLayoutSidebarLeftCollapse,
39
27
  IconLayoutSidebarLeftExpand,
40
- IconPuzzle,
41
28
  IconSettings,
42
- IconSettingsAutomation,
43
- IconShieldCheck,
29
+ IconShield,
44
30
  IconSearch,
45
31
  IconWorld,
46
32
  IconDeviceDesktop,
47
33
  } from "@tabler/icons-react";
48
34
  import {
35
+ createContext,
49
36
  useEffect,
50
37
  useMemo,
51
38
  useRef,
52
39
  useState,
40
+ useContext,
53
41
  type ComponentType,
54
42
  type ReactNode,
55
43
  } from "react";
@@ -67,6 +55,7 @@ import { Skeleton } from "../ui/skeleton";
67
55
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
68
56
  import { Header } from "./Header";
69
57
  import { HeaderActionsProvider } from "./HeaderActions";
58
+ import { WorkspaceAppsRail } from "./workspace-apps-rail";
70
59
 
71
60
  export type DispatchNavSection = "primary" | "operations";
72
61
 
@@ -82,10 +71,12 @@ export interface DispatchNavItem {
82
71
  to: string;
83
72
  label: string;
84
73
  icon?: DispatchNavIcon;
85
- /** Defaults to "operations", which is where local management tools usually fit. */
74
+ /** Defaults to "operations", which renders under the Admin control plane. */
86
75
  section?: DispatchNavSection;
87
76
  /** Override active matching for nested or multi-route tools. */
88
77
  match?: (pathname: string) => boolean;
78
+ /** Canonical path inside the Admin shell for management tabs. */
79
+ adminTo?: string;
89
80
  }
90
81
 
91
82
  export interface DispatchExtensionConfig {
@@ -119,80 +110,13 @@ const PRIMARY_NAV_ITEMS = [
119
110
  },
120
111
  ] as const satisfies readonly DispatchNavItem[];
121
112
 
122
- const OPERATIONS_NAV_ITEMS = [
123
- {
124
- id: "operations",
125
- to: "/operations",
126
- label: "Operations",
127
- icon: IconActivity,
128
- section: "operations",
129
- },
130
- {
131
- id: "metrics",
132
- to: "/metrics",
133
- label: "Metrics",
134
- icon: IconChartBar,
135
- section: "operations",
136
- },
137
- {
138
- id: "automations",
139
- to: "/automations",
140
- label: "Automations",
141
- icon: IconSettingsAutomation,
142
- section: "operations",
143
- },
144
- {
145
- id: "approvals",
146
- to: "/approvals",
147
- label: "Approvals",
148
- icon: IconShieldCheck,
149
- section: "operations",
150
- },
151
- {
152
- id: "destinations",
153
- to: "/destinations",
154
- label: "Destinations",
155
- icon: IconArrowUpRight,
156
- section: "operations",
157
- },
158
- {
159
- id: "integrations",
160
- to: "/integrations",
161
- label: "Integrations",
162
- icon: IconPuzzle,
163
- section: "operations",
164
- },
165
- {
166
- id: "transactional-email",
167
- to: "/transactional-email",
168
- label: "Transactional email",
169
- icon: IconMail,
170
- section: "operations",
171
- },
172
- {
173
- id: "vault",
174
- to: "/vault",
175
- label: "Vault",
176
- icon: IconKey,
177
- section: "operations",
178
- },
179
- {
180
- id: "agents",
181
- to: "/agents",
182
- label: "Agents",
183
- icon: IconPlugConnected,
184
- section: "operations",
185
- },
113
+ const BOTTOM_NAV_ITEMS = [
186
114
  {
187
- id: "workspace",
188
- to: "/workspace",
189
- label: "Resources",
190
- icon: IconLayersSubtract,
191
- section: "operations",
115
+ id: "admin",
116
+ to: "/admin",
117
+ label: "Admin",
118
+ icon: IconShield,
192
119
  },
193
- ] as const satisfies readonly DispatchNavItem[];
194
-
195
- const BOTTOM_NAV_ITEMS = [
196
120
  {
197
121
  id: "settings",
198
122
  to: "/settings",
@@ -201,44 +125,6 @@ const BOTTOM_NAV_ITEMS = [
201
125
  },
202
126
  ] as const satisfies readonly DispatchNavItem[];
203
127
 
204
- const ADVANCED_NAV_ITEMS = [
205
- {
206
- id: "messaging",
207
- to: "/messaging",
208
- label: "Messaging",
209
- icon: IconBrandTelegram,
210
- section: "operations",
211
- },
212
- {
213
- id: "identities",
214
- to: "/identities",
215
- label: "Identities",
216
- icon: IconFingerprint,
217
- section: "operations",
218
- },
219
- {
220
- id: "audit",
221
- to: "/audit",
222
- label: "Audit",
223
- icon: IconHistory,
224
- section: "operations",
225
- },
226
- {
227
- id: "dreams",
228
- to: "/dreams",
229
- label: "Dreams",
230
- icon: IconBrain,
231
- section: "operations",
232
- },
233
- {
234
- id: "thread-debug",
235
- to: "/thread-debug",
236
- label: "Thread Debug",
237
- icon: IconMessages,
238
- section: "operations",
239
- },
240
- ] as const satisfies readonly DispatchNavItem[];
241
-
242
128
  const EMPTY_NAV_ITEMS: readonly DispatchNavItem[] = [];
243
129
  const DISPATCH_SIDEBAR_LABEL = "Dispatch";
244
130
 
@@ -246,6 +132,14 @@ const CHROMELESS_PATHS = ["/approval", "/browser-chat", "/browser-connect"];
246
132
  const SIDEBAR_COLLAPSE_KEY = "dispatch.sidebar.collapsed";
247
133
  const CHAT_HISTORY_SOURCE_KEY = "dispatch.chat-history.source";
248
134
 
135
+ const DispatchExtensionsContext = createContext<
136
+ DispatchExtensionConfig | undefined
137
+ >(undefined);
138
+
139
+ export function useDispatchExtensions(): DispatchExtensionConfig | undefined {
140
+ return useContext(DispatchExtensionsContext);
141
+ }
142
+
249
143
  // Routes whose page renders its own toolbar.
250
144
  // Layout still mounts the sidebar + AgentSidebar, but skips its own Header so
251
145
  // there's no double-header.
@@ -594,14 +488,7 @@ export function NavContent({
594
488
  ...PRIMARY_NAV_ITEMS,
595
489
  ...navItemsForSection(extensionNavItems, "primary"),
596
490
  ];
597
- const operationsNavItems = [
598
- ...OPERATIONS_NAV_ITEMS,
599
- ...navItemsForSection(extensionNavItems, "operations"),
600
- ];
601
491
  const localPathname = localDispatchPath(location.pathname);
602
- const advancedOpen = ADVANCED_NAV_ITEMS.some((item) =>
603
- navItemMatchesPath(item, localPathname),
604
- );
605
492
  const navLabel = (item: DispatchNavItem) => {
606
493
  const key =
607
494
  item.id === "thread-debug"
@@ -807,40 +694,7 @@ export function NavContent({
807
694
  {primaryNavItems.map(renderNavItem)}
808
695
  </ul>
809
696
 
810
- {collapsed ? (
811
- <>
812
- <ul className="mt-5 flex flex-col items-center gap-1">
813
- {operationsNavItems.map(renderNavItem)}
814
- </ul>
815
- <ul className="mt-3 flex flex-col items-center gap-1">
816
- {ADVANCED_NAV_ITEMS.map(renderNavItem)}
817
- </ul>
818
- </>
819
- ) : (
820
- <div className="mt-5">
821
- <p className="px-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-sidebar-foreground/45">
822
- {t("dispatch.nav.operate", { defaultValue: "Operate" })}
823
- </p>
824
- <ul className="space-y-0.5">
825
- {operationsNavItems.map(renderNavItem)}
826
- </ul>
827
-
828
- <details className="group mt-3" open={advancedOpen}>
829
- <summary className="flex h-8 cursor-pointer list-none items-center justify-between rounded-md px-2 text-xs font-medium text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&::-webkit-details-marker]:hidden">
830
- <span>
831
- {t("dispatch.nav.advanced", { defaultValue: "Advanced" })}
832
- </span>
833
- <IconChevronDown
834
- size={14}
835
- className="transition-transform group-open:rotate-180"
836
- />
837
- </summary>
838
- <ul className="mt-1 space-y-0.5">
839
- {ADVANCED_NAV_ITEMS.map(renderNavItem)}
840
- </ul>
841
- </details>
842
- </div>
843
- )}
697
+ <WorkspaceAppsRail collapsed={collapsed} onNavigate={onNavigate} />
844
698
  </nav>
845
699
 
846
700
  <div className="mt-auto shrink-0">
@@ -980,46 +834,48 @@ export function Layout({
980
834
  );
981
835
 
982
836
  return (
983
- <HeaderActionsProvider>
984
- <div className="agent-layout-shell flex h-screen w-full overflow-hidden bg-background">
985
- <aside
986
- data-collapsed={sidebarCollapsed ? "true" : "false"}
987
- className={cn(
988
- "agent-layout-left-drawer hidden shrink-0 flex-col border-e bg-sidebar text-sidebar-foreground transition-[width] duration-200 ease-out lg:flex",
989
- sidebarCollapsed ? "w-14" : "w-56",
990
- )}
991
- >
992
- <NavContent
993
- extensions={extensions}
994
- collapsed={sidebarCollapsed}
995
- collapsible
996
- onCollapsedChange={setSidebarCollapsed}
997
- />
998
- </aside>
999
-
1000
- <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
1001
- <SheetContent
1002
- side="left"
1003
- className="w-72 p-0 bg-sidebar text-sidebar-foreground [&>button]:hidden"
837
+ <DispatchExtensionsContext.Provider value={extensions}>
838
+ <HeaderActionsProvider>
839
+ <div className="agent-layout-shell flex h-screen w-full overflow-hidden bg-background">
840
+ <aside
841
+ data-collapsed={sidebarCollapsed ? "true" : "false"}
842
+ className={cn(
843
+ "agent-layout-left-drawer hidden shrink-0 flex-col border-e bg-sidebar text-sidebar-foreground transition-[width] duration-200 ease-out lg:flex",
844
+ sidebarCollapsed ? "w-14" : "w-56",
845
+ )}
1004
846
  >
1005
- <SheetTitle className="sr-only">
1006
- {t("dispatch.nav.navigation")}
1007
- </SheetTitle>
1008
- <SheetDescription className="sr-only">
1009
- {t("dispatch.nav.navigationDescription")}
1010
- </SheetDescription>
1011
- <div className="flex h-full w-full flex-col">
1012
- <NavContent
1013
- extensions={extensions}
1014
- collapsed={false}
1015
- onNavigate={() => setMobileOpen(false)}
1016
- />
1017
- </div>
1018
- </SheetContent>
1019
- </Sheet>
847
+ <NavContent
848
+ extensions={extensions}
849
+ collapsed={sidebarCollapsed}
850
+ collapsible
851
+ onCollapsedChange={setSidebarCollapsed}
852
+ />
853
+ </aside>
854
+
855
+ <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
856
+ <SheetContent
857
+ side="left"
858
+ className="w-72 p-0 bg-sidebar text-sidebar-foreground [&>button]:hidden"
859
+ >
860
+ <SheetTitle className="sr-only">
861
+ {t("dispatch.nav.navigation")}
862
+ </SheetTitle>
863
+ <SheetDescription className="sr-only">
864
+ {t("dispatch.nav.navigationDescription")}
865
+ </SheetDescription>
866
+ <div className="flex h-full w-full flex-col">
867
+ <NavContent
868
+ extensions={extensions}
869
+ collapsed={false}
870
+ onNavigate={() => setMobileOpen(false)}
871
+ />
872
+ </div>
873
+ </SheetContent>
874
+ </Sheet>
1020
875
 
1021
- {content}
1022
- </div>
1023
- </HeaderActionsProvider>
876
+ {content}
877
+ </div>
878
+ </HeaderActionsProvider>
879
+ </DispatchExtensionsContext.Provider>
1024
880
  );
1025
881
  }
@@ -0,0 +1,154 @@
1
+ import { useActionQuery } from "@agent-native/core/client/hooks";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import { IconApps } from "@tabler/icons-react";
4
+ import { useLocation } from "react-router";
5
+
6
+ import { cn } from "../../lib/utils";
7
+ import {
8
+ workspaceAppHref,
9
+ type WorkspaceAppSummary,
10
+ } from "../../lib/workspace-apps";
11
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
12
+
13
+ function pathFromValue(value: string | null | undefined): string | null {
14
+ const trimmed = value?.trim();
15
+ if (!trimmed) return null;
16
+
17
+ try {
18
+ const pathname = new URL(trimmed, "https://dispatch.local").pathname;
19
+ return pathname.replace(/\/+$/, "") || "/";
20
+ } catch {
21
+ // coercion-ok: malformed app URLs cannot match a workspace route.
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function appMatchesPath(app: WorkspaceAppSummary, pathname: string): boolean {
27
+ const candidatePaths = [app.path, app.url]
28
+ .map(pathFromValue)
29
+ .filter((path): path is string => !!path);
30
+
31
+ return candidatePaths.some(
32
+ (path) => pathname === path || pathname.startsWith(`${path}/`),
33
+ );
34
+ }
35
+
36
+ function appInitial(name: string): string {
37
+ return name.trim().charAt(0).toUpperCase() || "A";
38
+ }
39
+
40
+ function appLabel(app: WorkspaceAppSummary): string {
41
+ return app.name.trim() || app.id;
42
+ }
43
+
44
+ export function WorkspaceAppsRail({
45
+ collapsed = false,
46
+ onNavigate,
47
+ }: {
48
+ collapsed?: boolean;
49
+ onNavigate?: () => void;
50
+ }) {
51
+ const t = useT();
52
+ const location = useLocation();
53
+ const appsQuery = useActionQuery<WorkspaceAppSummary[]>(
54
+ "list-workspace-apps",
55
+ {
56
+ includeAgentCards: false,
57
+ },
58
+ );
59
+ if (appsQuery.isError || !appsQuery.data) return null;
60
+
61
+ const apps = appsQuery.data
62
+ .filter(
63
+ (app) =>
64
+ !app.isDispatch &&
65
+ !app.archived &&
66
+ app.status !== "pending" &&
67
+ !!workspaceAppHref(app),
68
+ )
69
+ .sort((a, b) => appLabel(a).localeCompare(appLabel(b)));
70
+
71
+ if (apps.length === 0) return null;
72
+
73
+ const renderAppLink = (app: WorkspaceAppSummary) => {
74
+ const href = workspaceAppHref(app);
75
+ if (!href) return null;
76
+
77
+ const label = appLabel(app);
78
+ const active = appMatchesPath(app, location.pathname);
79
+ const link = (
80
+ <a
81
+ href={href}
82
+ aria-current={active ? "page" : undefined}
83
+ aria-label={collapsed ? label : undefined}
84
+ onClick={onNavigate}
85
+ className={cn(
86
+ "flex h-8 items-center rounded-md text-sm transition-colors",
87
+ collapsed ? "w-8 justify-center" : "w-full gap-2 px-2 text-start",
88
+ active
89
+ ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
90
+ : "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
91
+ )}
92
+ >
93
+ <span
94
+ className={cn(
95
+ "flex size-5 shrink-0 items-center justify-center rounded text-[10px] font-semibold",
96
+ active
97
+ ? "bg-sidebar-primary text-sidebar-primary-foreground"
98
+ : "bg-sidebar-accent text-sidebar-foreground/70",
99
+ )}
100
+ >
101
+ {appInitial(label)}
102
+ </span>
103
+ {!collapsed ? <span className="truncate">{label}</span> : null}
104
+ </a>
105
+ );
106
+
107
+ if (!collapsed) return link;
108
+
109
+ return (
110
+ <Tooltip>
111
+ <TooltipTrigger asChild>{link}</TooltipTrigger>
112
+ <TooltipContent side="right">{label}</TooltipContent>
113
+ </Tooltip>
114
+ );
115
+ };
116
+
117
+ return (
118
+ <div
119
+ className={cn(
120
+ "mt-4 border-t border-sidebar-border pt-3",
121
+ collapsed ? "px-1" : "px-2",
122
+ )}
123
+ data-dispatch-apps-rail
124
+ >
125
+ {!collapsed ? (
126
+ <div className="mb-1 flex items-center gap-1.5 px-2 text-[11px] font-medium uppercase tracking-wide text-sidebar-foreground/45">
127
+ <IconApps size={13} />
128
+ <span>
129
+ {t("dispatch.pages.workspaceApps", {
130
+ defaultValue: "Workspace apps",
131
+ })}
132
+ </span>
133
+ </div>
134
+ ) : (
135
+ <span className="sr-only">
136
+ {t("dispatch.pages.workspaceApps", {
137
+ defaultValue: "Workspace apps",
138
+ })}
139
+ </span>
140
+ )}
141
+ <ul
142
+ className={cn(
143
+ collapsed ? "flex flex-col items-center gap-1" : "space-y-0.5",
144
+ )}
145
+ >
146
+ {apps.map((app) => {
147
+ const href = workspaceAppHref(app);
148
+ if (!href) return null;
149
+ return <li key={app.id}>{renderAppLink(app)}</li>;
150
+ })}
151
+ </ul>
152
+ </div>
153
+ );
154
+ }
@@ -50,6 +50,17 @@ describe("buildDispatchNavigationState", () => {
50
50
  });
51
51
  });
52
52
 
53
+ it("recognizes Admin routes without losing the underlying view", () => {
54
+ expect(buildDispatchNavigationState("/admin/metrics")).toEqual({
55
+ view: "metrics",
56
+ path: "/admin/metrics",
57
+ });
58
+ expect(buildDispatchNavigationState("/admin")).toEqual({
59
+ view: "admin",
60
+ path: "/admin",
61
+ });
62
+ });
63
+
53
64
  it("preserves thread debug filters and selection", () => {
54
65
  expect(
55
66
  buildDispatchNavigationState(
@@ -224,7 +224,8 @@ function resolveExtensionPath(
224
224
  extensions?: DispatchExtensionConfig,
225
225
  ): string | undefined {
226
226
  if (!view) return undefined;
227
- return extensions?.navItems?.find((item) => item.id === view)?.to;
227
+ const item = extensions?.navItems?.find((candidate) => candidate.id === view);
228
+ return item?.adminTo ?? item?.to;
228
229
  }
229
230
 
230
231
  function resolveView(
@@ -236,9 +237,15 @@ function resolveView(
236
237
  if (pathname === "/extensions" || pathname.startsWith("/extensions/")) {
237
238
  return "extensions";
238
239
  }
240
+ if (pathname === "/admin") return "admin";
241
+ if (pathname.startsWith("/admin/")) {
242
+ const adminView = resolveView(pathname.slice("/admin".length), extensions);
243
+ return adminView === "overview" ? "admin" : adminView;
244
+ }
239
245
  if (pathname.startsWith("/browser-chat")) return "browser-chat";
240
246
  if (pathname.startsWith("/chat")) return "chat";
241
247
  if (pathname.startsWith("/apps")) return "apps";
248
+ if (pathname.startsWith("/operations")) return "operations";
242
249
  if (pathname.startsWith("/metrics")) return "metrics";
243
250
  if (pathname.startsWith("/new-app")) return "new-app";
244
251
  if (pathname.startsWith("/vault")) return "vault";
@@ -250,6 +257,9 @@ function resolveView(
250
257
  if (pathname.startsWith("/identities")) return "identities";
251
258
  if (pathname.startsWith("/approvals")) return "approvals";
252
259
  if (pathname.startsWith("/automations")) return "automations";
260
+ if (pathname.startsWith("/transactional-email")) {
261
+ return "transactional-email";
262
+ }
253
263
  if (pathname.startsWith("/audit")) return "audit";
254
264
  if (pathname.startsWith("/dreams")) return "dreams";
255
265
  if (pathname.startsWith("/thread-debug")) return "thread-debug";
@@ -263,6 +273,8 @@ function resolvePath(
263
273
  command?: Pick<NavigationState, "extensionId" | "threadId">,
264
274
  ): string | undefined {
265
275
  switch (view) {
276
+ case "admin":
277
+ return "/admin";
266
278
  case "chat":
267
279
  case "ask":
268
280
  return command?.threadId && command.threadId.trim()
@@ -272,43 +284,52 @@ function resolvePath(
272
284
  return "/overview";
273
285
  case "apps":
274
286
  return "/apps";
287
+ case "operations":
288
+ case "monitoring":
289
+ case "observability":
290
+ case "database":
291
+ return view === "database"
292
+ ? "/admin/operations?view=database"
293
+ : "/admin/operations";
275
294
  case "metrics":
276
295
  case "usage":
277
- return "/metrics";
296
+ return "/admin/metrics";
278
297
  case "new-app":
279
298
  case "create-app":
280
- return "/new-app";
299
+ return "/admin/new-app";
281
300
  case "vault":
282
301
  case "secrets":
283
- return "/vault";
302
+ return "/admin/vault";
284
303
  case "integrations":
285
- return "/integrations";
304
+ return "/admin/integrations";
286
305
  case "workspace":
287
306
  case "resources":
288
- return "/workspace";
307
+ return "/admin/workspace";
289
308
  case "agents":
290
- return "/agents";
309
+ return "/admin/agents";
291
310
  case "messaging":
292
- return "/messaging";
311
+ return "/admin/messaging";
293
312
  case "destinations":
294
313
  case "routes":
295
- return "/destinations";
314
+ return "/admin/destinations";
296
315
  case "identities":
297
- return "/identities";
316
+ return "/admin/identities";
298
317
  case "approvals":
299
- return "/approvals";
318
+ return "/admin/approvals";
300
319
  case "automations":
301
320
  case "jobs":
302
- return "/automations";
321
+ return "/admin/automations";
322
+ case "transactional-email":
323
+ return "/admin/transactional-email";
303
324
  case "audit":
304
- return "/audit";
325
+ return "/admin/audit";
305
326
  case "dreams":
306
- return "/dreams";
327
+ return "/admin/dreams";
307
328
  case "thread-debug":
308
329
  case "threads":
309
- return "/thread-debug";
330
+ return "/admin/thread-debug";
310
331
  case "team":
311
- return "/settings#organization";
332
+ return "/settings/organization";
312
333
  case "extensions":
313
334
  return command?.extensionId
314
335
  ? `/extensions/${encodeURIComponent(command.extensionId)}`
@@ -31,7 +31,7 @@ describe("automation-display", () => {
31
31
  it("prepares a thread-debug search for an automation", () => {
32
32
  expect(
33
33
  automationTroubleshootPath({ name: "coach onboarding reminder" }),
34
- ).toBe("/thread-debug?query=coach+onboarding+reminder");
34
+ ).toBe("/admin/thread-debug?query=coach+onboarding+reminder");
35
35
  });
36
36
 
37
37
  it("prefers event names, then schedule descriptions", () => {
@@ -17,7 +17,7 @@ export function automationTroubleshootPath(
17
17
  item: Pick<DispatchAutomationItem, "name">,
18
18
  ): string {
19
19
  const params = new URLSearchParams({ query: item.name });
20
- return `/thread-debug?${params.toString()}`;
20
+ return `/admin/thread-debug?${params.toString()}`;
21
21
  }
22
22
 
23
23
  export function automationTarget(item: DispatchAutomationItem): string {
@@ -8,6 +8,7 @@ describe("Dispatch route registration", () => {
8
8
  path?: string;
9
9
  file?: string;
10
10
  index?: boolean;
11
+ children?: Array<{ path?: string; index?: boolean }>;
11
12
  }>;
12
13
  const paths = routes.map((route) => route.path);
13
14
 
@@ -16,6 +17,7 @@ describe("Dispatch route registration", () => {
16
17
  expect(paths).toContain("browser-chat");
17
18
  expect(paths).toContain("browser-connect");
18
19
  expect(paths).toContain("operations");
20
+ expect(paths).toContain("admin");
19
21
  expect(paths.indexOf("chat")).toBeLessThan(paths.indexOf(":appId"));
20
22
  expect(paths.indexOf("chat/:threadId")).toBeLessThan(
21
23
  paths.indexOf(":appId"),
@@ -25,5 +27,11 @@ describe("Dispatch route registration", () => {
25
27
  paths.indexOf(":appId"),
26
28
  );
27
29
  expect(paths.indexOf("operations")).toBeLessThan(paths.indexOf(":appId"));
30
+
31
+ const admin = routes.find((route) => route.path === "admin");
32
+ const adminPaths = admin?.children?.map((route) => route.path) ?? [];
33
+ expect(adminPaths).toContain("operations");
34
+ expect(adminPaths).toContain("apps/:appId");
35
+ expect(adminPaths.indexOf("operations")).toBeGreaterThanOrEqual(0);
28
36
  });
29
37
  });