@agent-native/dispatch 0.1.1 → 0.2.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 (146) hide show
  1. package/dist/actions/index.d.ts.map +1 -1
  2. package/dist/actions/index.js +2 -0
  3. package/dist/actions/index.js.map +1 -1
  4. package/dist/actions/list-dispatch-usage-metrics.d.ts +3 -0
  5. package/dist/actions/list-dispatch-usage-metrics.d.ts.map +1 -0
  6. package/dist/actions/list-dispatch-usage-metrics.js +18 -0
  7. package/dist/actions/list-dispatch-usage-metrics.js.map +1 -0
  8. package/dist/actions/navigate.d.ts +1 -0
  9. package/dist/actions/navigate.d.ts.map +1 -1
  10. package/dist/actions/navigate.js +3 -17
  11. package/dist/actions/navigate.js.map +1 -1
  12. package/dist/actions/view-screen.d.ts.map +1 -1
  13. package/dist/actions/view-screen.js +19 -0
  14. package/dist/actions/view-screen.js.map +1 -1
  15. package/dist/components/agents-panel.js +3 -3
  16. package/dist/components/app-keys-popover.js +2 -2
  17. package/dist/components/create-app-popover.js +2 -2
  18. package/dist/components/dispatch-shell.js +2 -2
  19. package/dist/components/index.d.ts +1 -0
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/components/index.js.map +1 -1
  22. package/dist/components/layout/Header.js +5 -5
  23. package/dist/components/layout/Header.js.map +1 -1
  24. package/dist/components/layout/Layout.d.ts +28 -3
  25. package/dist/components/layout/Layout.d.ts.map +1 -1
  26. package/dist/components/layout/Layout.js +138 -28
  27. package/dist/components/layout/Layout.js.map +1 -1
  28. package/dist/components/messaging-setup-panel.js +4 -4
  29. package/dist/components/ui/accordion.js +1 -1
  30. package/dist/components/ui/alert-dialog.js +2 -2
  31. package/dist/components/ui/alert.js +1 -1
  32. package/dist/components/ui/avatar.js +1 -1
  33. package/dist/components/ui/badge.js +1 -1
  34. package/dist/components/ui/breadcrumb.js +1 -1
  35. package/dist/components/ui/button.js +1 -1
  36. package/dist/components/ui/calendar.js +2 -2
  37. package/dist/components/ui/card.js +1 -1
  38. package/dist/components/ui/carousel.d.ts +2 -2
  39. package/dist/components/ui/carousel.js +2 -2
  40. package/dist/components/ui/chart.js +1 -1
  41. package/dist/components/ui/checkbox.js +1 -1
  42. package/dist/components/ui/command.js +2 -2
  43. package/dist/components/ui/context-menu.js +1 -1
  44. package/dist/components/ui/dialog.js +1 -1
  45. package/dist/components/ui/drawer.js +1 -1
  46. package/dist/components/ui/dropdown-menu.js +1 -1
  47. package/dist/components/ui/form.js +2 -2
  48. package/dist/components/ui/hover-card.js +1 -1
  49. package/dist/components/ui/input-otp.js +1 -1
  50. package/dist/components/ui/input.js +1 -1
  51. package/dist/components/ui/label.js +1 -1
  52. package/dist/components/ui/menubar.js +1 -1
  53. package/dist/components/ui/navigation-menu.js +1 -1
  54. package/dist/components/ui/pagination.d.ts +1 -1
  55. package/dist/components/ui/pagination.js +2 -2
  56. package/dist/components/ui/popover.js +1 -1
  57. package/dist/components/ui/progress.js +1 -1
  58. package/dist/components/ui/radio-group.js +1 -1
  59. package/dist/components/ui/resizable.js +1 -1
  60. package/dist/components/ui/scroll-area.js +1 -1
  61. package/dist/components/ui/select.js +1 -1
  62. package/dist/components/ui/separator.js +1 -1
  63. package/dist/components/ui/sheet.js +1 -1
  64. package/dist/components/ui/sidebar.d.ts +2 -2
  65. package/dist/components/ui/sidebar.d.ts.map +1 -1
  66. package/dist/components/ui/sidebar.js +9 -9
  67. package/dist/components/ui/sidebar.js.map +1 -1
  68. package/dist/components/ui/skeleton.js +1 -1
  69. package/dist/components/ui/slider.js +1 -1
  70. package/dist/components/ui/sonner.js +1 -1
  71. package/dist/components/ui/spinner.js +1 -1
  72. package/dist/components/ui/switch.js +1 -1
  73. package/dist/components/ui/table.js +1 -1
  74. package/dist/components/ui/tabs.js +1 -1
  75. package/dist/components/ui/textarea.js +1 -1
  76. package/dist/components/ui/toast.js +1 -1
  77. package/dist/components/ui/toaster.js +2 -2
  78. package/dist/components/ui/toggle-group.js +2 -2
  79. package/dist/components/ui/toggle.js +1 -1
  80. package/dist/components/ui/tooltip.js +1 -1
  81. package/dist/components/ui/use-toast.d.ts +1 -1
  82. package/dist/components/ui/use-toast.js +1 -1
  83. package/dist/hooks/use-navigation-state.d.ts +2 -1
  84. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  85. package/dist/hooks/use-navigation-state.js +36 -8
  86. package/dist/hooks/use-navigation-state.js.map +1 -1
  87. package/dist/hooks/use-toast.d.ts +1 -1
  88. package/dist/routes/index.d.ts.map +1 -1
  89. package/dist/routes/index.js +3 -2
  90. package/dist/routes/index.js.map +1 -1
  91. package/dist/routes/pages/_index.js +1 -1
  92. package/dist/routes/pages/agents.js +2 -2
  93. package/dist/routes/pages/approval.js +2 -2
  94. package/dist/routes/pages/approvals.js +4 -4
  95. package/dist/routes/pages/apps.$appId.js +3 -3
  96. package/dist/routes/pages/apps.js +5 -5
  97. package/dist/routes/pages/audit.js +1 -1
  98. package/dist/routes/pages/destinations.js +6 -6
  99. package/dist/routes/pages/extensions.$id.d.ts +2 -0
  100. package/dist/routes/pages/extensions.$id.d.ts.map +1 -0
  101. package/dist/routes/pages/extensions.$id.js +6 -0
  102. package/dist/routes/pages/extensions.$id.js.map +1 -0
  103. package/dist/routes/pages/extensions._index.d.ts +2 -0
  104. package/dist/routes/pages/extensions._index.d.ts.map +1 -0
  105. package/dist/routes/pages/extensions._index.js +6 -0
  106. package/dist/routes/pages/extensions._index.js.map +1 -0
  107. package/dist/routes/pages/identities.js +2 -2
  108. package/dist/routes/pages/integrations.js +4 -4
  109. package/dist/routes/pages/messaging.js +2 -2
  110. package/dist/routes/pages/metrics.d.ts +5 -0
  111. package/dist/routes/pages/metrics.d.ts.map +1 -0
  112. package/dist/routes/pages/metrics.js +135 -0
  113. package/dist/routes/pages/metrics.js.map +1 -0
  114. package/dist/routes/pages/new-app.js +1 -1
  115. package/dist/routes/pages/overview.d.ts.map +1 -1
  116. package/dist/routes/pages/overview.js +9 -17
  117. package/dist/routes/pages/overview.js.map +1 -1
  118. package/dist/routes/pages/team.js +1 -1
  119. package/dist/routes/pages/vault.js +10 -10
  120. package/dist/routes/pages/workspace.js +10 -10
  121. package/dist/server/lib/pre-auth-routing.d.ts.map +1 -1
  122. package/dist/server/lib/pre-auth-routing.js +9 -2
  123. package/dist/server/lib/pre-auth-routing.js.map +1 -1
  124. package/dist/server/lib/usage-metrics-store.d.ts +93 -0
  125. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -0
  126. package/dist/server/lib/usage-metrics-store.js +386 -0
  127. package/dist/server/lib/usage-metrics-store.js.map +1 -0
  128. package/package.json +11 -6
  129. package/src/actions/index.ts +2 -0
  130. package/src/actions/list-dispatch-usage-metrics.ts +19 -0
  131. package/src/actions/navigate.ts +5 -17
  132. package/src/actions/view-screen.ts +18 -0
  133. package/src/components/index.ts +6 -0
  134. package/src/components/layout/Header.tsx +2 -2
  135. package/src/components/layout/Layout.tsx +197 -48
  136. package/src/components/ui/sidebar.tsx +22 -18
  137. package/src/hooks/use-navigation-state.ts +57 -8
  138. package/src/routes/index.ts +3 -2
  139. package/src/routes/pages/extensions.$id.tsx +5 -0
  140. package/src/routes/pages/extensions._index.tsx +5 -0
  141. package/src/routes/pages/metrics.tsx +667 -0
  142. package/src/routes/pages/overview.tsx +0 -10
  143. package/src/server/lib/pre-auth-routing.ts +10 -2
  144. package/src/server/lib/usage-metrics-store.ts +605 -0
  145. package/src/styles/dispatch-css.spec.ts +55 -0
  146. package/src/styles/dispatch.css +9 -0
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Usage:
7
7
  * pnpm action navigate --view=overview
8
+ * pnpm action navigate --view=<custom-dispatch-extension-id>
8
9
  * pnpm action navigate --path=/some/route
9
10
  *
10
11
  * Options:
@@ -21,24 +22,11 @@ export default defineAction({
21
22
  "Navigate the UI to a specific view or path. Writes a navigate command to application state which the UI reads and auto-deletes.",
22
23
  schema: z.object({
23
24
  view: z
24
- .enum([
25
- "overview",
26
- "apps",
27
- "new-app",
28
- "vault",
29
- "integrations",
30
- "messaging",
31
- "workspace",
32
- "agents",
33
- "destinations",
34
- "routes",
35
- "identities",
36
- "approvals",
37
- "audit",
38
- "team",
39
- ])
25
+ .string()
40
26
  .optional()
41
- .describe("Named dispatch view to navigate to"),
27
+ .describe(
28
+ "Named dispatch view to navigate to. Built-in views include overview, apps, metrics, new-app, vault, integrations, messaging, workspace, agents, destinations, identities, approvals, audit, and team. Generated Dispatch extension tabs can also use their nav item id.",
29
+ ),
42
30
  path: z.string().optional().describe("URL path to navigate to"),
43
31
  }),
44
32
  http: false,
@@ -18,6 +18,7 @@ import {
18
18
  listRequests,
19
19
  } from "../server/lib/vault-store.js";
20
20
  import { listWorkspaceApps } from "../server/lib/app-creation-store.js";
21
+ import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
21
22
 
22
23
  export default defineAction({
23
24
  description:
@@ -45,6 +46,7 @@ export default defineAction({
45
46
  }
46
47
  if (
47
48
  navigation?.view === "overview" ||
49
+ navigation?.view === "metrics" ||
48
50
  navigation?.view === "apps" ||
49
51
  navigation?.view === "new-app"
50
52
  ) {
@@ -52,6 +54,22 @@ export default defineAction({
52
54
  includeAgentCards: true,
53
55
  });
54
56
  }
57
+ if (navigation?.view === "metrics") {
58
+ try {
59
+ const metrics = await listDispatchUsageMetrics({ sinceDays: 30 });
60
+ screen.usageMetrics = {
61
+ totals: metrics.totals,
62
+ byApp: metrics.byApp.slice(0, 8),
63
+ byUser: metrics.byUser.slice(0, 8),
64
+ appAccess: metrics.appAccess
65
+ .filter((app) => !app.isDispatch)
66
+ .slice(0, 8),
67
+ };
68
+ } catch (error) {
69
+ screen.usageMetricsError =
70
+ error instanceof Error ? error.message : String(error);
71
+ }
72
+ }
55
73
  if (navigation?.view === "vault" || navigation?.view === "new-app") {
56
74
  const [secrets, grants, requests] = await Promise.all([
57
75
  listSecrets(),
@@ -7,5 +7,11 @@
7
7
  */
8
8
  export { DispatchShell } from "./dispatch-shell.js";
9
9
  export { Layout, NavContent } from "./layout/Layout.js";
10
+ export type {
11
+ DispatchExtensionConfig,
12
+ DispatchNavIcon,
13
+ DispatchNavItem,
14
+ DispatchNavSection,
15
+ } from "./layout/Layout.js";
10
16
  export { CreateAppPopover, CreateAppFlow } from "./create-app-popover.js";
11
17
  export { AppKeysPopover } from "./app-keys-popover.js";
@@ -22,7 +22,7 @@ const pageTitles: Record<string, string> = {
22
22
  function resolveTitle(pathname: string): string {
23
23
  if (pageTitles[pathname]) return pageTitles[pathname];
24
24
 
25
- if (pathname.startsWith("/tools")) return "Tools";
25
+ if (pathname.startsWith("/extensions")) return "Extensions";
26
26
 
27
27
  return "Dispatch";
28
28
  }
@@ -44,7 +44,7 @@ export function Header({
44
44
  <Button
45
45
  variant="ghost"
46
46
  size="icon"
47
- className="h-8 w-8 2xl:hidden cursor-pointer"
47
+ className="h-8 w-8 lg:hidden cursor-pointer"
48
48
  onClick={onOpenMobile}
49
49
  aria-label="Open navigation"
50
50
  >
@@ -1,4 +1,4 @@
1
- import { useState, type ReactNode } from "react";
1
+ import { useState, type ComponentType, type ReactNode } from "react";
2
2
  import { NavLink, useLocation } from "react-router";
3
3
  import {
4
4
  AgentSidebar,
@@ -11,6 +11,7 @@ import { ToolsSidebarSection } from "@agent-native/core/client/tools";
11
11
  import {
12
12
  IconArrowUpRight,
13
13
  IconApps,
14
+ IconChartBar,
14
15
  IconBrandTelegram,
15
16
  IconKey,
16
17
  IconChevronDown,
@@ -33,27 +34,131 @@ import {
33
34
  import { Header } from "./Header";
34
35
  import { HeaderActionsProvider } from "./HeaderActions";
35
36
 
37
+ export type DispatchNavSection = "primary" | "operations";
38
+
39
+ export type DispatchNavIcon = ComponentType<{
40
+ size?: number | string;
41
+ className?: string;
42
+ }>;
43
+
44
+ export interface DispatchNavItem {
45
+ /** Stable id used for keys and navigation.view. Avoid built-in ids. */
46
+ id: string;
47
+ /** React Router path for the tab, usually backed by an app/routes/*.tsx file. */
48
+ to: string;
49
+ label: string;
50
+ icon?: DispatchNavIcon;
51
+ /** Defaults to "operations", which is where local management tools usually fit. */
52
+ section?: DispatchNavSection;
53
+ /** Override active matching for nested or multi-route tools. */
54
+ match?: (pathname: string) => boolean;
55
+ }
56
+
57
+ export interface DispatchExtensionConfig {
58
+ /** Extra sidebar tabs supplied by the generated workspace. */
59
+ navItems?: readonly DispatchNavItem[];
60
+ /** Extra React Query keys to invalidate when Dispatch receives DB sync events. */
61
+ queryKeys?: readonly string[];
62
+ }
63
+
36
64
  const PRIMARY_NAV_ITEMS = [
37
- { to: "/overview", label: "Overview", icon: IconBroadcast },
38
- { to: "/apps", label: "Apps", icon: IconApps },
39
- { to: "/vault", label: "Vault", icon: IconKey },
40
- { to: "/integrations", label: "Integrations", icon: IconPuzzle },
41
- { to: "/agents", label: "Agents", icon: IconPlugConnected },
42
- ] as const;
65
+ {
66
+ id: "overview",
67
+ to: "/overview",
68
+ label: "Overview",
69
+ icon: IconBroadcast,
70
+ section: "primary",
71
+ },
72
+ {
73
+ id: "apps",
74
+ to: "/apps",
75
+ label: "Apps",
76
+ icon: IconApps,
77
+ section: "primary",
78
+ },
79
+ {
80
+ id: "metrics",
81
+ to: "/metrics",
82
+ label: "Metrics",
83
+ icon: IconChartBar,
84
+ section: "primary",
85
+ },
86
+ {
87
+ id: "vault",
88
+ to: "/vault",
89
+ label: "Vault",
90
+ icon: IconKey,
91
+ section: "primary",
92
+ },
93
+ {
94
+ id: "integrations",
95
+ to: "/integrations",
96
+ label: "Integrations",
97
+ icon: IconPuzzle,
98
+ section: "primary",
99
+ },
100
+ {
101
+ id: "agents",
102
+ to: "/agents",
103
+ label: "Agents",
104
+ icon: IconPlugConnected,
105
+ section: "primary",
106
+ },
107
+ ] as const satisfies readonly DispatchNavItem[];
43
108
 
44
109
  const OPERATIONS_NAV_ITEMS = [
45
- { to: "/workspace", label: "Resources", icon: IconLayersSubtract },
46
- { to: "/messaging", label: "Messaging", icon: IconBrandTelegram },
47
- { to: "/destinations", label: "Destinations", icon: IconArrowUpRight },
48
- { to: "/identities", label: "Identities", icon: IconFingerprint },
49
- { to: "/approvals", label: "Approvals", icon: IconShieldCheck },
50
- { to: "/audit", label: "Audit", icon: IconHistory },
51
- { to: "/team", label: "Team", icon: IconUsersGroup },
52
- ] as const;
53
-
54
- type NavItem =
55
- | (typeof PRIMARY_NAV_ITEMS)[number]
56
- | (typeof OPERATIONS_NAV_ITEMS)[number];
110
+ {
111
+ id: "workspace",
112
+ to: "/workspace",
113
+ label: "Resources",
114
+ icon: IconLayersSubtract,
115
+ section: "operations",
116
+ },
117
+ {
118
+ id: "messaging",
119
+ to: "/messaging",
120
+ label: "Messaging",
121
+ icon: IconBrandTelegram,
122
+ section: "operations",
123
+ },
124
+ {
125
+ id: "destinations",
126
+ to: "/destinations",
127
+ label: "Destinations",
128
+ icon: IconArrowUpRight,
129
+ section: "operations",
130
+ },
131
+ {
132
+ id: "identities",
133
+ to: "/identities",
134
+ label: "Identities",
135
+ icon: IconFingerprint,
136
+ section: "operations",
137
+ },
138
+ {
139
+ id: "approvals",
140
+ to: "/approvals",
141
+ label: "Approvals",
142
+ icon: IconShieldCheck,
143
+ section: "operations",
144
+ },
145
+ {
146
+ id: "audit",
147
+ to: "/audit",
148
+ label: "Audit",
149
+ icon: IconHistory,
150
+ section: "operations",
151
+ },
152
+ {
153
+ id: "team",
154
+ to: "/team",
155
+ label: "Team",
156
+ icon: IconUsersGroup,
157
+ section: "operations",
158
+ },
159
+ ] as const satisfies readonly DispatchNavItem[];
160
+
161
+ const EMPTY_NAV_ITEMS: readonly DispatchNavItem[] = [];
57
162
 
58
163
  const SIDEBAR_SUGGESTIONS = [
59
164
  "Create a new app",
@@ -68,6 +173,8 @@ const CHROMELESS_PATHS = ["/approval"];
68
173
  // there's no double-header.
69
174
  function pageOwnsToolbar(pathname: string): boolean {
70
175
  if (pathname === "/tools" || pathname.startsWith("/tools/")) return true;
176
+ if (pathname === "/extensions" || pathname.startsWith("/extensions/"))
177
+ return true;
71
178
  return false;
72
179
  }
73
180
 
@@ -77,7 +184,35 @@ interface WorkspaceInfo {
77
184
  appCount: number;
78
185
  }
79
186
 
80
- export function NavContent({ onNavigate }: { onNavigate?: () => void }) {
187
+ function sectionFor(item: DispatchNavItem): DispatchNavSection {
188
+ return item.section ?? "operations";
189
+ }
190
+
191
+ function navItemMatchesPath(item: DispatchNavItem, pathname: string): boolean {
192
+ if (item.match) {
193
+ try {
194
+ if (item.match(pathname)) return true;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+ return pathname === item.to || pathname.startsWith(`${item.to}/`);
200
+ }
201
+
202
+ function navItemsForSection(
203
+ items: readonly DispatchNavItem[],
204
+ section: DispatchNavSection,
205
+ ): DispatchNavItem[] {
206
+ return items.filter((item) => sectionFor(item) === section);
207
+ }
208
+
209
+ export function NavContent({
210
+ onNavigate,
211
+ extensions,
212
+ }: {
213
+ onNavigate?: () => void;
214
+ extensions?: DispatchExtensionConfig;
215
+ }) {
81
216
  const location = useLocation();
82
217
  const { data: workspace } = useActionQuery(
83
218
  "get-workspace-info",
@@ -86,29 +221,42 @@ export function NavContent({ onNavigate }: { onNavigate?: () => void }) {
86
221
  );
87
222
  const ws = workspace as WorkspaceInfo | undefined;
88
223
  const workspaceLabel = ws?.displayName ?? ws?.name ?? null;
89
- const operationsOpen = OPERATIONS_NAV_ITEMS.some(
90
- (item) =>
91
- location.pathname === item.to ||
92
- location.pathname.startsWith(`${item.to}/`),
224
+ const extensionNavItems = extensions?.navItems ?? EMPTY_NAV_ITEMS;
225
+ const primaryNavItems = [
226
+ ...PRIMARY_NAV_ITEMS,
227
+ ...navItemsForSection(extensionNavItems, "primary"),
228
+ ];
229
+ const operationsNavItems = [
230
+ ...OPERATIONS_NAV_ITEMS,
231
+ ...navItemsForSection(extensionNavItems, "operations"),
232
+ ];
233
+ const operationsOpen = operationsNavItems.some((item) =>
234
+ navItemMatchesPath(item, location.pathname),
93
235
  );
94
236
 
95
- const renderNavItem = (item: NavItem) => {
237
+ const renderNavItem = (item: DispatchNavItem) => {
96
238
  const Icon = item.icon;
97
239
  return (
98
- <li key={item.to}>
240
+ <li key={item.id}>
99
241
  <NavLink
100
242
  to={item.to}
101
243
  onClick={onNavigate}
102
- className={({ isActive }) =>
103
- cn(
244
+ className={({ isActive }) => {
245
+ const active =
246
+ isActive || navItemMatchesPath(item, location.pathname);
247
+ return cn(
104
248
  "flex h-8 w-full items-center gap-2 rounded-md px-2 text-sm",
105
- isActive
249
+ active
106
250
  ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
107
251
  : "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
108
- )
109
- }
252
+ );
253
+ }}
110
254
  >
111
- <Icon size={16} className="shrink-0" />
255
+ {Icon ? (
256
+ <Icon size={16} className="shrink-0" />
257
+ ) : (
258
+ <span className="h-4 w-4 shrink-0" aria-hidden="true" />
259
+ )}
112
260
  <span className="truncate">{item.label}</span>
113
261
  </NavLink>
114
262
  </li>
@@ -147,7 +295,7 @@ export function NavContent({ onNavigate }: { onNavigate?: () => void }) {
147
295
  </div>
148
296
 
149
297
  <nav className="flex-1 overflow-y-auto px-2 py-3">
150
- <ul className="space-y-0.5">{PRIMARY_NAV_ITEMS.map(renderNavItem)}</ul>
298
+ <ul className="space-y-0.5">{primaryNavItems.map(renderNavItem)}</ul>
151
299
  <details className="group mt-4" open={operationsOpen}>
152
300
  <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">
153
301
  <span>Operations</span>
@@ -157,7 +305,7 @@ export function NavContent({ onNavigate }: { onNavigate?: () => void }) {
157
305
  />
158
306
  </summary>
159
307
  <ul className="mt-1 space-y-0.5">
160
- {OPERATIONS_NAV_ITEMS.map(renderNavItem)}
308
+ {operationsNavItems.map(renderNavItem)}
161
309
  </ul>
162
310
  </details>
163
311
  </nav>
@@ -170,11 +318,15 @@ export function NavContent({ onNavigate }: { onNavigate?: () => void }) {
170
318
  );
171
319
  }
172
320
 
173
- export function Layout({ children }: { children: ReactNode }) {
321
+ export function Layout({
322
+ children,
323
+ extensions,
324
+ }: {
325
+ children: ReactNode;
326
+ extensions?: DispatchExtensionConfig;
327
+ }) {
174
328
  const location = useLocation();
175
329
  const [mobileOpen, setMobileOpen] = useState(false);
176
- const hasEmbeddedAgentChat =
177
- location.pathname === "/" || location.pathname === "/overview";
178
330
 
179
331
  if (CHROMELESS_PATHS.some((path) => location.pathname === path)) {
180
332
  return <>{children}</>;
@@ -183,12 +335,7 @@ export function Layout({ children }: { children: ReactNode }) {
183
335
  const showHeader = !pageOwnsToolbar(location.pathname);
184
336
  const appContent = (
185
337
  <div className="flex h-full flex-1 flex-col overflow-hidden">
186
- {showHeader ? (
187
- <Header
188
- onOpenMobile={() => setMobileOpen(true)}
189
- showAgentToggle={!hasEmbeddedAgentChat}
190
- />
191
- ) : null}
338
+ {showHeader ? <Header onOpenMobile={() => setMobileOpen(true)} /> : null}
192
339
  <InvitationBanner />
193
340
  <main className="flex-1 overflow-y-auto">
194
341
  {showHeader ? (
@@ -205,8 +352,8 @@ export function Layout({ children }: { children: ReactNode }) {
205
352
  return (
206
353
  <HeaderActionsProvider>
207
354
  <div className="flex h-screen w-full overflow-hidden bg-background">
208
- <aside className="hidden 2xl:flex w-64 shrink-0 flex-col border-r bg-sidebar text-sidebar-foreground">
209
- <NavContent />
355
+ <aside className="hidden lg:flex w-64 shrink-0 flex-col border-r bg-sidebar text-sidebar-foreground">
356
+ <NavContent extensions={extensions} />
210
357
  </aside>
211
358
 
212
359
  <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
@@ -219,15 +366,17 @@ export function Layout({ children }: { children: ReactNode }) {
219
366
  Workspace navigation links
220
367
  </SheetDescription>
221
368
  <div className="flex h-full w-full flex-col">
222
- <NavContent onNavigate={() => setMobileOpen(false)} />
369
+ <NavContent
370
+ extensions={extensions}
371
+ onNavigate={() => setMobileOpen(false)}
372
+ />
223
373
  </div>
224
374
  </SheetContent>
225
375
  </Sheet>
226
376
 
227
377
  {/*
228
378
  * Always mount AgentSidebar so home composer's sendToAgentChat
229
- * fallback can pop it via agent-panel:open. The toggle button stays
230
- * hidden on overview because the home composer is the primary input.
379
+ * fallback can pop it via agent-panel:open.
231
380
  */}
232
381
  <AgentSidebar
233
382
  position="right"
@@ -307,24 +307,28 @@ const SidebarRail = React.forwardRef<
307
307
  const { toggleSidebar } = useSidebar();
308
308
 
309
309
  return (
310
- <button
311
- ref={ref}
312
- data-sidebar="rail"
313
- aria-label="Toggle Sidebar"
314
- tabIndex={-1}
315
- onClick={toggleSidebar}
316
- title="Toggle Sidebar"
317
- className={cn(
318
- "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
319
- "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
320
- "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
321
- "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
322
- "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
323
- "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
324
- className,
325
- )}
326
- {...props}
327
- />
310
+ <Tooltip>
311
+ <TooltipTrigger asChild>
312
+ <button
313
+ ref={ref}
314
+ data-sidebar="rail"
315
+ aria-label="Toggle Sidebar"
316
+ tabIndex={-1}
317
+ onClick={toggleSidebar}
318
+ className={cn(
319
+ "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
320
+ "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
321
+ "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
322
+ "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
323
+ "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
324
+ "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
325
+ className,
326
+ )}
327
+ {...props}
328
+ />
329
+ </TooltipTrigger>
330
+ <TooltipContent>Toggle Sidebar</TooltipContent>
331
+ </Tooltip>
328
332
  );
329
333
  });
330
334
  SidebarRail.displayName = "SidebarRail";
@@ -6,13 +6,17 @@ import {
6
6
  appBasePath,
7
7
  appPath,
8
8
  } from "@agent-native/core/client";
9
+ import type {
10
+ DispatchExtensionConfig,
11
+ DispatchNavItem,
12
+ } from "../components/index.js";
9
13
 
10
14
  export interface NavigationState {
11
15
  view: string;
12
16
  path?: string;
13
17
  }
14
18
 
15
- export function useNavigationState() {
19
+ export function useNavigationState(extensions?: DispatchExtensionConfig) {
16
20
  const location = useLocation();
17
21
  const navigate = useNavigate();
18
22
  const qc = useQueryClient();
@@ -20,7 +24,7 @@ export function useNavigationState() {
20
24
  // Sync current route to application state
21
25
  useEffect(() => {
22
26
  const state: NavigationState = {
23
- view: resolveView(location.pathname),
27
+ view: resolveView(location.pathname, extensions),
24
28
  path: appPath(location.pathname),
25
29
  };
26
30
 
@@ -30,7 +34,7 @@ export function useNavigationState() {
30
34
  headers: { "Content-Type": "application/json" },
31
35
  body: JSON.stringify(state),
32
36
  }).catch(() => {});
33
- }, [location.pathname]);
37
+ }, [extensions, location.pathname]);
34
38
 
35
39
  // Listen for navigate commands from agent
36
40
  const { data: navCommand } = useQuery({
@@ -62,10 +66,12 @@ export function useNavigationState() {
62
66
  const cmd = navCommand as NavigationState;
63
67
 
64
68
  // Navigate to a specific path or resolve view name to path
65
- const path = routerPath(cmd.path || resolvePath(cmd.view) || "/overview");
69
+ const path = routerPath(
70
+ cmd.path || resolvePath(cmd.view, extensions) || "/overview",
71
+ );
66
72
  navigate(path);
67
73
  qc.setQueryData(["navigate-command"], null);
68
- }, [navCommand, navigate, qc]);
74
+ }, [extensions, navCommand, navigate, qc]);
69
75
  }
70
76
 
71
77
  function routerPath(path: string): string {
@@ -78,8 +84,45 @@ function routerPath(path: string): string {
78
84
  return path;
79
85
  }
80
86
 
81
- function resolveView(pathname: string): string {
87
+ function extensionItemMatchesPath(
88
+ item: DispatchNavItem,
89
+ pathname: string,
90
+ ): boolean {
91
+ if (item.match) {
92
+ try {
93
+ if (item.match(pathname)) return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+ return pathname === item.to || pathname.startsWith(`${item.to}/`);
99
+ }
100
+
101
+ function resolveExtensionView(
102
+ pathname: string,
103
+ extensions?: DispatchExtensionConfig,
104
+ ): string | undefined {
105
+ return extensions?.navItems?.find((item) =>
106
+ extensionItemMatchesPath(item, pathname),
107
+ )?.id;
108
+ }
109
+
110
+ function resolveExtensionPath(
111
+ view: string | undefined,
112
+ extensions?: DispatchExtensionConfig,
113
+ ): string | undefined {
114
+ if (!view) return undefined;
115
+ return extensions?.navItems?.find((item) => item.id === view)?.to;
116
+ }
117
+
118
+ function resolveView(
119
+ pathname: string,
120
+ extensions?: DispatchExtensionConfig,
121
+ ): string {
122
+ const extensionView = resolveExtensionView(pathname, extensions);
123
+ if (extensionView) return extensionView;
82
124
  if (pathname.startsWith("/apps")) return "apps";
125
+ if (pathname.startsWith("/metrics")) return "metrics";
83
126
  if (pathname.startsWith("/new-app")) return "new-app";
84
127
  if (pathname.startsWith("/vault")) return "vault";
85
128
  if (pathname.startsWith("/integrations")) return "integrations";
@@ -94,12 +137,18 @@ function resolveView(pathname: string): string {
94
137
  return "overview";
95
138
  }
96
139
 
97
- function resolvePath(view?: string): string | undefined {
140
+ function resolvePath(
141
+ view?: string,
142
+ extensions?: DispatchExtensionConfig,
143
+ ): string | undefined {
98
144
  switch (view) {
99
145
  case "overview":
100
146
  return "/overview";
101
147
  case "apps":
102
148
  return "/apps";
149
+ case "metrics":
150
+ case "usage":
151
+ return "/metrics";
103
152
  case "new-app":
104
153
  case "create-app":
105
154
  return "/new-app";
@@ -127,6 +176,6 @@ function resolvePath(view?: string): string | undefined {
127
176
  case "team":
128
177
  return "/team";
129
178
  default:
130
- return undefined;
179
+ return resolveExtensionPath(view, extensions);
131
180
  }
132
181
  }
@@ -32,6 +32,7 @@ import { type RouteConfig, route, index } from "@react-router/dev/routes";
32
32
  export const dispatchRoutes: RouteConfig = [
33
33
  index("./pages/_index.js"),
34
34
  route("overview", "./pages/overview.js"),
35
+ route("metrics", "./pages/metrics.js"),
35
36
  route("apps", "./pages/apps.js"),
36
37
  route("apps/:appId", "./pages/apps.$appId.js"),
37
38
  route("new-app", "./pages/new-app.js"),
@@ -46,6 +47,6 @@ export const dispatchRoutes: RouteConfig = [
46
47
  route("approvals", "./pages/approvals.js"),
47
48
  route("audit", "./pages/audit.js"),
48
49
  route("team", "./pages/team.js"),
49
- route("tools", "./pages/tools._index.js"),
50
- route("tools/:id", "./pages/tools.$id.js"),
50
+ route("extensions", "./pages/extensions._index.js"),
51
+ route("extensions/:id", "./pages/extensions.$id.js"),
51
52
  ];
@@ -0,0 +1,5 @@
1
+ import { ToolViewerPage } from "@agent-native/core/client/tools";
2
+
3
+ export default function ExtensionViewerRoute() {
4
+ return <ToolViewerPage />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { ToolsListPage } from "@agent-native/core/client/tools";
2
+
3
+ export default function ExtensionsRoute() {
4
+ return <ToolsListPage />;
5
+ }