@agent-native/dispatch 0.23.5 → 0.24.2

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 (224) hide show
  1. package/dist/actions/list-connected-agents.d.ts +1 -0
  2. package/dist/actions/list-connected-agents.js +5 -1
  3. package/dist/actions/list-connected-agents.js.map +1 -1
  4. package/dist/actions/list-dispatch-usage-metrics.d.ts +2 -0
  5. package/dist/actions/list-dispatch-usage-metrics.js +11 -1
  6. package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
  7. package/dist/actions/provider-api-register.d.ts +12 -12
  8. package/dist/actions/remix-workspace-template.js +4 -3
  9. package/dist/actions/remix-workspace-template.js.map +1 -1
  10. package/dist/actions/scaffold-workspace-app.js +3 -2
  11. package/dist/actions/scaffold-workspace-app.js.map +1 -1
  12. package/dist/actions/start-workspace-app-creation.js +3 -2
  13. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  14. package/dist/actions/view-screen.js +11 -1
  15. package/dist/actions/view-screen.js.map +1 -1
  16. package/dist/components/admin-navigation.d.ts.map +1 -1
  17. package/dist/components/admin-navigation.js +21 -5
  18. package/dist/components/admin-navigation.js.map +1 -1
  19. package/dist/components/app-icon.js +1 -1
  20. package/dist/components/app-icon.js.map +1 -1
  21. package/dist/components/app-list-row.d.ts +1 -1
  22. package/dist/components/app-list-row.d.ts.map +1 -1
  23. package/dist/components/app-list-row.js +1 -1
  24. package/dist/components/app-list-row.js.map +1 -1
  25. package/dist/components/app-open-actions.d.ts +1 -17
  26. package/dist/components/app-open-actions.d.ts.map +1 -1
  27. package/dist/components/app-open-actions.js +1 -20
  28. package/dist/components/app-open-actions.js.map +1 -1
  29. package/dist/components/app-row-settings.d.ts +6 -0
  30. package/dist/components/app-row-settings.d.ts.map +1 -0
  31. package/dist/components/app-row-settings.js +11 -0
  32. package/dist/components/app-row-settings.js.map +1 -0
  33. package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
  34. package/dist/components/automation-run-history-dialog.js +3 -2
  35. package/dist/components/automation-run-history-dialog.js.map +1 -1
  36. package/dist/components/connected-app-card.d.ts.map +1 -1
  37. package/dist/components/connected-app-card.js +3 -2
  38. package/dist/components/connected-app-card.js.map +1 -1
  39. package/dist/components/create-app-popover.js +2 -2
  40. package/dist/components/create-app-popover.js.map +1 -1
  41. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  42. package/dist/components/dispatch-control-plane.js +23 -7
  43. package/dist/components/dispatch-control-plane.js.map +1 -1
  44. package/dist/components/dispatch-shell.d.ts.map +1 -1
  45. package/dist/components/dispatch-shell.js +5 -1
  46. package/dist/components/dispatch-shell.js.map +1 -1
  47. package/dist/components/docs-link.d.ts +16 -0
  48. package/dist/components/docs-link.d.ts.map +1 -0
  49. package/dist/components/docs-link.js +34 -0
  50. package/dist/components/docs-link.js.map +1 -0
  51. package/dist/components/layout/Layout.d.ts +1 -0
  52. package/dist/components/layout/Layout.d.ts.map +1 -1
  53. package/dist/components/layout/Layout.js +43 -35
  54. package/dist/components/layout/Layout.js.map +1 -1
  55. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  56. package/dist/components/messaging-setup-panel.js +3 -2
  57. package/dist/components/messaging-setup-panel.js.map +1 -1
  58. package/dist/components/usage-alerts-panel.d.ts +11 -0
  59. package/dist/components/usage-alerts-panel.d.ts.map +1 -0
  60. package/dist/components/usage-alerts-panel.js +125 -0
  61. package/dist/components/usage-alerts-panel.js.map +1 -0
  62. package/dist/components/workspace-app-card.d.ts +3 -1
  63. package/dist/components/workspace-app-card.d.ts.map +1 -1
  64. package/dist/components/workspace-app-card.js +70 -29
  65. package/dist/components/workspace-app-card.js.map +1 -1
  66. package/dist/components/workspace-app-search.d.ts +10 -0
  67. package/dist/components/workspace-app-search.d.ts.map +1 -0
  68. package/dist/components/workspace-app-search.js +27 -0
  69. package/dist/components/workspace-app-search.js.map +1 -0
  70. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  71. package/dist/components/workspace-resource-effective-stack.js +3 -2
  72. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  73. package/dist/components/workspace-template-card.d.ts.map +1 -1
  74. package/dist/components/workspace-template-card.js +10 -15
  75. package/dist/components/workspace-template-card.js.map +1 -1
  76. package/dist/hooks/use-navigation-state.d.ts +2 -0
  77. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  78. package/dist/hooks/use-navigation-state.js +10 -0
  79. package/dist/hooks/use-navigation-state.js.map +1 -1
  80. package/dist/lib/embed-session-recovery.d.ts +3 -0
  81. package/dist/lib/embed-session-recovery.d.ts.map +1 -0
  82. package/dist/lib/embed-session-recovery.js +18 -0
  83. package/dist/lib/embed-session-recovery.js.map +1 -0
  84. package/dist/lib/other-apps.d.ts +2 -0
  85. package/dist/lib/other-apps.d.ts.map +1 -1
  86. package/dist/lib/other-apps.js.map +1 -1
  87. package/dist/lib/workspace-app-layout.d.ts +19 -0
  88. package/dist/lib/workspace-app-layout.d.ts.map +1 -0
  89. package/dist/lib/workspace-app-layout.js +75 -0
  90. package/dist/lib/workspace-app-layout.js.map +1 -0
  91. package/dist/lib/workspace-apps.d.ts +14 -0
  92. package/dist/lib/workspace-apps.d.ts.map +1 -1
  93. package/dist/lib/workspace-apps.js +66 -0
  94. package/dist/lib/workspace-apps.js.map +1 -1
  95. package/dist/lib/workspace-apps.test.d.ts +2 -0
  96. package/dist/lib/workspace-apps.test.d.ts.map +1 -0
  97. package/dist/lib/workspace-apps.test.js +33 -0
  98. package/dist/lib/workspace-apps.test.js.map +1 -0
  99. package/dist/routes/pages/$appId.d.ts.map +1 -1
  100. package/dist/routes/pages/$appId.js +4 -2
  101. package/dist/routes/pages/$appId.js.map +1 -1
  102. package/dist/routes/pages/agents.d.ts.map +1 -1
  103. package/dist/routes/pages/agents.js +2 -3
  104. package/dist/routes/pages/agents.js.map +1 -1
  105. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  106. package/dist/routes/pages/apps.$appId.js +17 -13
  107. package/dist/routes/pages/apps.$appId.js.map +1 -1
  108. package/dist/routes/pages/apps.d.ts.map +1 -1
  109. package/dist/routes/pages/apps.js +31 -16
  110. package/dist/routes/pages/apps.js.map +1 -1
  111. package/dist/routes/pages/audit.d.ts.map +1 -1
  112. package/dist/routes/pages/audit.js +2 -1
  113. package/dist/routes/pages/audit.js.map +1 -1
  114. package/dist/routes/pages/chat.d.ts.map +1 -1
  115. package/dist/routes/pages/chat.js +7 -2
  116. package/dist/routes/pages/chat.js.map +1 -1
  117. package/dist/routes/pages/dreams.d.ts.map +1 -1
  118. package/dist/routes/pages/dreams.js +14 -18
  119. package/dist/routes/pages/dreams.js.map +1 -1
  120. package/dist/routes/pages/integrations.d.ts.map +1 -1
  121. package/dist/routes/pages/integrations.js +51 -14
  122. package/dist/routes/pages/integrations.js.map +1 -1
  123. package/dist/routes/pages/metrics.d.ts.map +1 -1
  124. package/dist/routes/pages/metrics.js +146 -17
  125. package/dist/routes/pages/metrics.js.map +1 -1
  126. package/dist/routes/pages/operations.js +1 -1
  127. package/dist/routes/pages/operations.js.map +1 -1
  128. package/dist/routes/pages/settings.d.ts.map +1 -1
  129. package/dist/routes/pages/settings.js +19 -2
  130. package/dist/routes/pages/settings.js.map +1 -1
  131. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  132. package/dist/routes/pages/thread-debug.js +5 -8
  133. package/dist/routes/pages/thread-debug.js.map +1 -1
  134. package/dist/routes/pages/vault.d.ts.map +1 -1
  135. package/dist/routes/pages/vault.js +1 -1
  136. package/dist/routes/pages/vault.js.map +1 -1
  137. package/dist/routes/pages/workspace.d.ts.map +1 -1
  138. package/dist/routes/pages/workspace.js +13 -10
  139. package/dist/routes/pages/workspace.js.map +1 -1
  140. package/dist/server/index.d.ts.map +1 -1
  141. package/dist/server/index.js +3 -0
  142. package/dist/server/index.js.map +1 -1
  143. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  144. package/dist/server/lib/app-creation-store.js +12 -52
  145. package/dist/server/lib/app-creation-store.js.map +1 -1
  146. package/dist/server/lib/usage-metrics-store.d.ts +17 -0
  147. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
  148. package/dist/server/lib/usage-metrics-store.js +187 -30
  149. package/dist/server/lib/usage-metrics-store.js.map +1 -1
  150. package/dist/server/plugins/agent-chat.js +2 -0
  151. package/dist/server/plugins/agent-chat.js.map +1 -1
  152. package/dist/server/plugins/integrations.d.ts.map +1 -1
  153. package/dist/server/plugins/integrations.js +1 -0
  154. package/dist/server/plugins/integrations.js.map +1 -1
  155. package/dist/shared/app-roles.d.ts +9 -0
  156. package/dist/shared/app-roles.d.ts.map +1 -0
  157. package/dist/shared/app-roles.js +7 -0
  158. package/dist/shared/app-roles.js.map +1 -0
  159. package/package.json +4 -3
  160. package/src/actions/list-connected-agents.spec.ts +73 -0
  161. package/src/actions/list-connected-agents.ts +6 -2
  162. package/src/actions/list-dispatch-usage-metrics.ts +11 -1
  163. package/src/actions/remix-workspace-template.spec.ts +29 -0
  164. package/src/actions/remix-workspace-template.ts +13 -6
  165. package/src/actions/scaffold-workspace-app.ts +12 -5
  166. package/src/actions/start-workspace-app-creation.ts +8 -2
  167. package/src/actions/view-screen.ts +13 -1
  168. package/src/components/admin-navigation.tsx +57 -11
  169. package/src/components/app-icon.spec.tsx +34 -0
  170. package/src/components/app-icon.tsx +1 -1
  171. package/src/components/app-list-row.tsx +1 -2
  172. package/src/components/app-open-actions.tsx +5 -114
  173. package/src/components/app-row-settings.tsx +54 -0
  174. package/src/components/automation-run-history-dialog.tsx +8 -4
  175. package/src/components/connected-app-card.tsx +3 -2
  176. package/src/components/create-app-popover.spec.tsx +1 -1
  177. package/src/components/create-app-popover.tsx +2 -2
  178. package/src/components/dispatch-control-plane.spec.tsx +44 -3
  179. package/src/components/dispatch-control-plane.tsx +83 -27
  180. package/src/components/dispatch-shell.tsx +7 -1
  181. package/src/components/docs-link.tsx +52 -0
  182. package/src/components/layout/Layout.spec.tsx +13 -0
  183. package/src/components/layout/Layout.tsx +63 -32
  184. package/src/components/messaging-setup-panel.tsx +10 -3
  185. package/src/components/usage-alerts-panel.tsx +445 -0
  186. package/src/components/workspace-app-card.spec.tsx +130 -9
  187. package/src/components/workspace-app-card.tsx +128 -71
  188. package/src/components/workspace-app-search.tsx +96 -0
  189. package/src/components/workspace-resource-effective-stack.tsx +10 -5
  190. package/src/components/workspace-template-card.spec.tsx +53 -24
  191. package/src/components/workspace-template-card.tsx +16 -26
  192. package/src/hooks/use-navigation-state.ts +12 -0
  193. package/src/lib/embed-session-recovery.spec.ts +63 -0
  194. package/src/lib/embed-session-recovery.ts +26 -0
  195. package/src/lib/other-apps.ts +2 -0
  196. package/src/lib/workspace-app-layout.spec.ts +47 -0
  197. package/src/lib/workspace-app-layout.ts +121 -0
  198. package/src/lib/workspace-apps.test.ts +64 -0
  199. package/src/lib/workspace-apps.ts +82 -0
  200. package/src/routes/pages/$appId.tsx +3 -6
  201. package/src/routes/pages/agents.tsx +6 -3
  202. package/src/routes/pages/apps.$appId.tsx +16 -17
  203. package/src/routes/pages/apps.tsx +116 -62
  204. package/src/routes/pages/audit.tsx +28 -8
  205. package/src/routes/pages/chat.spec.tsx +10 -0
  206. package/src/routes/pages/chat.tsx +8 -0
  207. package/src/routes/pages/dreams.tsx +92 -142
  208. package/src/routes/pages/integrations.tsx +147 -68
  209. package/src/routes/pages/metrics.tsx +460 -102
  210. package/src/routes/pages/operations.tsx +2 -2
  211. package/src/routes/pages/settings.tsx +27 -10
  212. package/src/routes/pages/thread-debug.tsx +18 -36
  213. package/src/routes/pages/vault.tsx +7 -2
  214. package/src/routes/pages/workspace.tsx +92 -56
  215. package/src/server/index.ts +4 -0
  216. package/src/server/lib/app-creation-store.spec.ts +90 -43
  217. package/src/server/lib/app-creation-store.ts +15 -61
  218. package/src/server/lib/usage-metrics-store.spec.ts +117 -0
  219. package/src/server/lib/usage-metrics-store.ts +249 -30
  220. package/src/server/plugins/agent-chat.spec.ts +4 -0
  221. package/src/server/plugins/agent-chat.ts +2 -0
  222. package/src/server/plugins/integrations.ts +1 -0
  223. package/src/shared/app-roles.ts +8 -0
  224. package/src/styles/dispatch.css +9 -0
@@ -1,114 +1,5 @@
1
- import { IconChevronDown, IconPlus } from "@tabler/icons-react";
2
-
3
- import { Button } from "./ui/button";
4
- import {
5
- DropdownMenu,
6
- DropdownMenuContent,
7
- DropdownMenuItem,
8
- DropdownMenuTrigger,
9
- } from "./ui/dropdown-menu";
10
-
11
- export interface AppOpenActionLabels {
12
- addApp: string;
13
- openApp: string;
14
- openInline: string;
15
- openInNewTab: string;
16
- }
17
-
18
- const DEFAULT_LABELS: AppOpenActionLabels = {
19
- addApp: "Add app",
20
- openApp: "Open app",
21
- openInline: "Open inline",
22
- openInNewTab: "Open in new tab",
23
- };
24
-
25
- export function AppOpenActions({
26
- name,
27
- href,
28
- target,
29
- rel,
30
- labels: labelOverrides,
31
- onAddApp,
32
- onOpenInline,
33
- showInlineOption = false,
34
- showNewTabOption = false,
35
- }: {
36
- name: string;
37
- href: string | null;
38
- target?: "_blank";
39
- rel?: string;
40
- labels?: Partial<AppOpenActionLabels>;
41
- onAddApp?: () => void;
42
- onOpenInline?: () => void;
43
- showInlineOption?: boolean;
44
- showNewTabOption?: boolean;
45
- }) {
46
- const labels = { ...DEFAULT_LABELS, ...labelOverrides };
47
- const canOfferNewTab = showNewTabOption && target !== "_blank";
48
- const hasMenu =
49
- Boolean(onAddApp) ||
50
- (Boolean(href) && (showInlineOption || canOfferNewTab));
51
-
52
- if (!href && !hasMenu) {
53
- return (
54
- <Button size="sm" variant="outline" disabled>
55
- {labels.openApp}
56
- </Button>
57
- );
58
- }
59
-
60
- return (
61
- <div className="flex shrink-0 overflow-hidden rounded-md border border-border focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
62
- <Button
63
- asChild={Boolean(href)}
64
- size="sm"
65
- variant="outline"
66
- className="rounded-none border-0 border-e border-border"
67
- disabled={!href}
68
- >
69
- {href ? (
70
- <a href={href} target={target} rel={rel}>
71
- {labels.openApp}
72
- </a>
73
- ) : (
74
- <span>{labels.openApp}</span>
75
- )}
76
- </Button>
77
- {hasMenu ? (
78
- <DropdownMenu>
79
- <DropdownMenuTrigger asChild>
80
- <Button
81
- type="button"
82
- size="sm"
83
- variant="outline"
84
- className="size-9 rounded-none border-0 p-0"
85
- aria-label={`Open options for ${name}`}
86
- >
87
- <IconChevronDown size={15} />
88
- </Button>
89
- </DropdownMenuTrigger>
90
- <DropdownMenuContent align="end" className="min-w-40">
91
- {onAddApp ? (
92
- <DropdownMenuItem onSelect={onAddApp}>
93
- <IconPlus size={14} className="mr-2" />
94
- {labels.addApp}
95
- </DropdownMenuItem>
96
- ) : null}
97
- {showInlineOption && href ? (
98
- <DropdownMenuItem onSelect={onOpenInline}>
99
- {labels.openInline}
100
- </DropdownMenuItem>
101
- ) : null}
102
- {canOfferNewTab && href ? (
103
- <DropdownMenuItem asChild>
104
- <a href={href} target="_blank" rel="noreferrer">
105
- {labels.openInNewTab}
106
- </a>
107
- </DropdownMenuItem>
108
- ) : null}
109
- </DropdownMenuContent>
110
- </DropdownMenu>
111
- ) : null}
112
- </div>
113
- );
114
- }
1
+ export {
2
+ AppOpenActions,
3
+ type AppOpenActionLabels,
4
+ type AppOpenActionMenuItem,
5
+ } from "@agent-native/core/client/chat-first";
@@ -0,0 +1,54 @@
1
+ import { APP_ACTION_MENU_CONTENT_CLASS } from "@agent-native/core/client/chat-first";
2
+ import { IconPlus, IconSettings } from "@tabler/icons-react";
3
+
4
+ import { Button } from "./ui/button";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ } from "./ui/dropdown-menu";
11
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
12
+
13
+ const APP_ROW_ACTION_CLASS =
14
+ "size-7 rounded-md p-0 text-muted-foreground transition-[background-color,color] hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-accent data-[state=open]:text-foreground";
15
+
16
+ export function AppRowSettings({
17
+ name,
18
+ addAppLabel = "Add app",
19
+ onAddApp,
20
+ }: {
21
+ name: string;
22
+ addAppLabel?: string;
23
+ onAddApp: () => void;
24
+ }) {
25
+ return (
26
+ <DropdownMenu>
27
+ <Tooltip>
28
+ <TooltipTrigger asChild>
29
+ <DropdownMenuTrigger asChild>
30
+ <Button
31
+ type="button"
32
+ variant="ghost"
33
+ size="sm"
34
+ aria-label={`Settings for ${name}`}
35
+ className={APP_ROW_ACTION_CLASS}
36
+ >
37
+ <IconSettings size={15} />
38
+ </Button>
39
+ </DropdownMenuTrigger>
40
+ </TooltipTrigger>
41
+ <TooltipContent>App settings</TooltipContent>
42
+ </Tooltip>
43
+ <DropdownMenuContent
44
+ align="end"
45
+ className={APP_ACTION_MENU_CONTENT_CLASS}
46
+ >
47
+ <DropdownMenuItem onSelect={onAddApp}>
48
+ <IconPlus size={14} aria-hidden="true" />
49
+ {addAppLabel}
50
+ </DropdownMenuItem>
51
+ </DropdownMenuContent>
52
+ </DropdownMenu>
53
+ );
54
+ }
@@ -3,7 +3,6 @@ import {
3
3
  IconAlertTriangle,
4
4
  IconClock,
5
5
  IconExternalLink,
6
- IconLoader2,
7
6
  } from "@tabler/icons-react";
8
7
  import { Link } from "react-router";
9
8
 
@@ -21,6 +20,7 @@ import {
21
20
  DialogHeader,
22
21
  DialogTitle,
23
22
  } from "./ui/dialog";
23
+ import { Skeleton } from "./ui/skeleton";
24
24
 
25
25
  interface AutomationRun {
26
26
  id: string;
@@ -178,9 +178,13 @@ export function AutomationRunHistoryDialog({
178
178
 
179
179
  <div className="mt-3">
180
180
  {runsQuery.isLoading ? (
181
- <div className="flex items-center gap-2 rounded-lg border p-4 text-sm text-muted-foreground">
182
- <IconLoader2 className="size-4 animate-spin" />
183
- Loading run history...
181
+ <div className="space-y-3 rounded-lg border p-4">
182
+ {Array.from({ length: 3 }).map((_, index) => (
183
+ <div key={index} className="space-y-2">
184
+ <Skeleton className="h-4 w-2/3" />
185
+ <Skeleton className="h-3 w-1/2" />
186
+ </div>
187
+ ))}
184
188
  </div>
185
189
  ) : runsQuery.isError ? (
186
190
  <ActionQueryError
@@ -12,6 +12,8 @@ export function ConnectedAppCard({
12
12
  app: ConnectedAppSummary;
13
13
  className?: string;
14
14
  }) {
15
+ const launchUrl = app.homeUrl ?? app.url;
16
+
15
17
  return (
16
18
  <AppListRow className={className}>
17
19
  <AppIcon id={app.id} name={app.name} color={app.color} size="sm" />
@@ -25,11 +27,10 @@ export function ConnectedAppCard({
25
27
  </div>
26
28
  <AppOpenActions
27
29
  name={app.name}
28
- href={withBuilderUtmTrackingParams(app.url, {
30
+ href={withBuilderUtmTrackingParams(launchUrl, {
29
31
  campaign: "product",
30
32
  content: "dispatch_app",
31
33
  })}
32
- target="_blank"
33
34
  rel="noopener noreferrer"
34
35
  showNewTabOption
35
36
  />
@@ -339,7 +339,7 @@ describe("CreateAppFlow", () => {
339
339
  });
340
340
  });
341
341
  const branchLink = Array.from(container.querySelectorAll("a")).find(
342
- (candidate) => candidate.textContent?.includes("Open Builder branch"),
342
+ (candidate) => candidate.textContent?.includes("Open in Builder"),
343
343
  );
344
344
  expect(branchLink?.getAttribute("href")).toBe(
345
345
  "https://branch.example.test",
@@ -319,7 +319,7 @@ export function CreateAppFlow({
319
319
  </div>
320
320
  <Button asChild className="w-full sm:w-auto">
321
321
  <a href={branchUrl} target="_blank" rel="noreferrer">
322
- Open Builder branch <IconArrowUpRight aria-hidden="true" />
322
+ Open in Builder <IconArrowUpRight aria-hidden="true" />
323
323
  </a>
324
324
  </Button>
325
325
  </div>
@@ -583,7 +583,7 @@ export function CreateAppFlow({
583
583
  rel="noreferrer"
584
584
  className="inline-flex items-center gap-1 font-medium text-foreground underline"
585
585
  >
586
- Open branch <IconArrowUpRight className="h-3 w-3" />
586
+ Open in Builder <IconArrowUpRight className="h-3 w-3" />
587
587
  </a>
588
588
  ) : null}
589
589
  </div>
@@ -33,6 +33,9 @@ vi.mock("@agent-native/core/client/agent-chat", () => ({
33
33
  path: string,
34
34
  options?: unknown,
35
35
  ) => clientState.navigateWithTransition(navigate, path, options),
36
+ orderChatFirstAppIds: (appIds: string[]) => appIds,
37
+ readChatFirstAppLayout: () => ({ pinnedIds: [], orderedIds: [] }),
38
+ writeChatFirstAppLayout: () => ({ ok: true }),
36
39
  useChatModels: clientState.useChatModels,
37
40
  }));
38
41
 
@@ -53,6 +56,11 @@ vi.mock("@agent-native/core/client/composer", () => ({
53
56
  },
54
57
  }));
55
58
 
59
+ vi.mock("@agent-native/core/client/application-state", () => ({
60
+ readClientAppState: vi.fn(async () => null),
61
+ writeClientAppState: vi.fn(async () => null),
62
+ }));
63
+
56
64
  vi.mock("@agent-native/core/client/hooks", () => ({
57
65
  useActionQuery: (name: string) => ({
58
66
  data:
@@ -129,6 +137,15 @@ describe("DispatchControlPlane", () => {
129
137
  expect(container.textContent).not.toContain("Open chat");
130
138
  expect(container.textContent).not.toContain("Also");
131
139
  expect(container.textContent).not.toContain("active");
140
+ expect(container.textContent).not.toContain(
141
+ "Summarize the current workspace health",
142
+ );
143
+ expect(container.textContent).toContain(
144
+ "Create an app for onboarding requests",
145
+ );
146
+ expect(container.textContent).toContain(
147
+ "Check which agents can help with analytics",
148
+ );
132
149
  expect(container.querySelector("nav")).toBeNull();
133
150
  expect(
134
151
  container.querySelector('[data-placeholder="Ask Dispatch anything..."]'),
@@ -231,7 +248,9 @@ describe("DispatchControlPlane", () => {
231
248
  id: "clips",
232
249
  name: "Clips",
233
250
  description: "Record and share",
234
- url: "https://clips.agent-native.com",
251
+ url: "https://clips.agent-native.com/share/WrA8ZQ3oxa2T?ref=clip_share",
252
+ homeUrl: "https://clips.agent-native.com",
253
+ source: "builtin",
235
254
  },
236
255
  {
237
256
  id: "onboarding",
@@ -272,15 +291,37 @@ describe("DispatchControlPlane", () => {
272
291
  expect(container.textContent).toContain("Analytics");
273
292
  expect(container.textContent).toContain("Apps");
274
293
  expect(container.textContent).toContain("New");
294
+ const viewAllLink = Array.from(container.querySelectorAll("a")).find(
295
+ (link) => link.textContent?.trim() === "View all",
296
+ );
297
+ expect(viewAllLink?.className).toContain("text-muted-foreground");
275
298
  expect(container.textContent).not.toContain("Other apps");
276
299
  expect(container.textContent).not.toContain("available");
277
300
  expect(container.textContent).not.toContain("Archived app");
278
301
  expect(container.textContent).not.toContain("Duplicate onboarding");
279
302
  expect(container.textContent).not.toContain("CRM");
280
303
  expect(
281
- Array.from(container.querySelectorAll("a")).filter((anchor) =>
282
- anchor.getAttribute("href")?.includes("onboarding"),
304
+ container.querySelectorAll(
305
+ 'button[aria-label="Open options for Onboarding"]',
283
306
  ),
284
307
  ).toHaveLength(1);
308
+ await act(async () => {
309
+ container
310
+ .querySelector<HTMLButtonElement>(
311
+ 'button[aria-label="Open options for Onboarding"]',
312
+ )
313
+ ?.dispatchEvent(
314
+ new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
315
+ );
316
+ });
317
+ const onboardingNewTabLink = document.querySelector<HTMLAnchorElement>(
318
+ 'a[href="/onboarding"][target="_blank"]',
319
+ );
320
+ expect(onboardingNewTabLink).not.toBeNull();
321
+ const clipsHref = Array.from(container.querySelectorAll("a"))
322
+ .map((anchor) => anchor.getAttribute("href"))
323
+ .find((href) => href?.includes("clips.agent-native.com"));
324
+ expect(clipsHref).toContain("https://clips.agent-native.com");
325
+ expect(clipsHref).not.toContain("/share/");
285
326
  });
286
327
  });
@@ -12,6 +12,11 @@ import { Link, useNavigate } from "react-router";
12
12
 
13
13
  import type { ConnectedAppSummary } from "../lib/other-apps";
14
14
  import { cn } from "../lib/utils";
15
+ import {
16
+ orderWorkspaceApps,
17
+ useWorkspaceAppLayout,
18
+ workspaceAppMatchesQuery,
19
+ } from "../lib/workspace-app-layout";
15
20
  import {
16
21
  isWorkspaceAppVisibleInDefaultLaunchers,
17
22
  type WorkspaceAppSummary,
@@ -33,6 +38,10 @@ import {
33
38
  } from "./ui/collapsible";
34
39
  import { Skeleton } from "./ui/skeleton";
35
40
  import { WorkspaceAppCard } from "./workspace-app-card";
41
+ import {
42
+ WorkspaceAppSearch,
43
+ WorkspaceAppSearchEmpty,
44
+ } from "./workspace-app-search";
36
45
  import type { CuratedWorkspaceTemplatesResult } from "./workspace-template-card";
37
46
 
38
47
  function SectionHeader({
@@ -65,9 +74,6 @@ function CommandPanel() {
65
74
  } = useChatModels({ storageKey: "dispatch" });
66
75
  const navigate = useNavigate();
67
76
  const promptSuggestions = [
68
- t("dispatch.pages.suggestionWorkspaceHealth", {
69
- defaultValue: "Summarize the current workspace health",
70
- }),
71
77
  t("dispatch.pages.suggestionOnboardingApp", {
72
78
  defaultValue: "Create an app for onboarding requests",
73
79
  }),
@@ -165,11 +171,23 @@ function AppsPanel({
165
171
  }) {
166
172
  const t = useT();
167
173
  const [showPending, setShowPending] = useState(false);
174
+ const [searchQuery, setSearchQuery] = useState("");
175
+ const { layout, persistenceError, togglePinned } = useWorkspaceAppLayout();
168
176
  const visibleApps = apps.filter(
169
177
  (app) => isWorkspaceAppVisibleInDefaultLaunchers(app) && !app.archived,
170
178
  );
171
179
  const activeApps = visibleApps.filter((app) => app.status !== "pending");
172
180
  const pendingApps = visibleApps.filter((app) => app.status === "pending");
181
+ const orderedActiveApps = orderWorkspaceApps(activeApps, layout);
182
+ const orderedPendingApps = orderWorkspaceApps(pendingApps, layout);
183
+ const filteredActiveApps = orderedActiveApps.filter((app) =>
184
+ workspaceAppMatchesQuery(app, searchQuery),
185
+ );
186
+ const filteredPendingApps = orderedPendingApps.filter((app) =>
187
+ workspaceAppMatchesQuery(app, searchQuery),
188
+ );
189
+ const hasSearchResults =
190
+ filteredActiveApps.length > 0 || filteredPendingApps.length > 0;
173
191
  const otherAppEntries = mergeOtherAppEntries({
174
192
  templates: curatedTemplates,
175
193
  connectedApps,
@@ -183,14 +201,23 @@ function AppsPanel({
183
201
  <SectionHeader
184
202
  title="Apps"
185
203
  action={
186
- <div className="flex items-center gap-1.5">
187
- <Button variant="outline" size="sm" asChild>
188
- <Link to="/apps">View all</Link>
204
+ <div className="flex min-w-0 flex-wrap items-center justify-end gap-1.5">
205
+ <Button variant="ghost" size="sm" asChild>
206
+ <Link to="/apps" className="text-muted-foreground">
207
+ View all
208
+ </Link>
189
209
  </Button>
210
+ {!showSkeletons && visibleApps.length > 0 ? (
211
+ <WorkspaceAppSearch
212
+ className="w-[220px]"
213
+ query={searchQuery}
214
+ onQueryChange={setSearchQuery}
215
+ />
216
+ ) : null}
190
217
  <CreateAppPopover
191
218
  align="end"
192
219
  trigger={
193
- <Button variant="ghost" size="sm" className="gap-1.5">
220
+ <Button variant="default" size="sm" className="gap-1.5">
194
221
  <IconPlus size={14} />
195
222
  {t("dispatch.pages.newApp", { defaultValue: "New" })}
196
223
  </Button>
@@ -199,19 +226,38 @@ function AppsPanel({
199
226
  </div>
200
227
  }
201
228
  />
229
+ {!showSkeletons && visibleApps.length > 0 && persistenceError ? (
230
+ <p className="text-xs text-muted-foreground" role="status">
231
+ {persistenceError === "both"
232
+ ? t("dispatch.pages.appPinSaveFailed", {
233
+ defaultValue: "App pins could not be saved.",
234
+ })
235
+ : t("dispatch.pages.appPinSavedLocally", {
236
+ defaultValue: "App pins are saved on this device only.",
237
+ })}
238
+ </p>
239
+ ) : null}
202
240
  {showSkeletons ? (
203
241
  <OverviewAppsSkeleton />
242
+ ) : searchQuery.trim() && !hasSearchResults ? (
243
+ <WorkspaceAppSearchEmpty
244
+ query={searchQuery}
245
+ onClear={() => setSearchQuery("")}
246
+ />
204
247
  ) : (
205
248
  <>
206
249
  <AppList className={APP_LIST_GRID_CLASS}>
207
- {activeApps.map((app) => (
250
+ {filteredActiveApps.map((app) => (
208
251
  <WorkspaceAppCard
209
252
  key={app.id}
210
253
  app={app}
211
254
  className={APP_LIST_GRID_ROW_CLASS}
255
+ isPinned={layout.pinnedIds.includes(app.id)}
256
+ onTogglePinned={() => togglePinned(app.id)}
212
257
  />
213
258
  ))}
214
- {activeApps.length === 0 &&
259
+ {filteredActiveApps.length === 0 &&
260
+ !searchQuery.trim() &&
215
261
  otherAppEntries.length === 0 &&
216
262
  !curatedTemplatesLoading &&
217
263
  !connectedAppsLoading &&
@@ -223,24 +269,30 @@ function AppsPanel({
223
269
  })}
224
270
  </p>
225
271
  ) : null}
226
- <OtherAppsSection
227
- templates={curatedTemplates}
228
- connectedApps={connectedApps}
229
- workspaceApps={apps}
230
- templatesLoading={curatedTemplatesLoading}
231
- connectedAppsLoading={connectedAppsLoading}
232
- templatesError={curatedTemplatesError}
233
- connectedAppsError={connectedAppsError}
234
- onRetryTemplates={onRetryCuratedTemplates}
235
- onRetryConnectedApps={onRetryConnectedApps}
236
- heading={null}
237
- embeddedInList
238
- />
272
+ {!searchQuery.trim() ? (
273
+ <OtherAppsSection
274
+ templates={curatedTemplates}
275
+ connectedApps={connectedApps}
276
+ workspaceApps={apps}
277
+ templatesLoading={curatedTemplatesLoading}
278
+ connectedAppsLoading={connectedAppsLoading}
279
+ templatesError={curatedTemplatesError}
280
+ connectedAppsError={connectedAppsError}
281
+ onRetryTemplates={onRetryCuratedTemplates}
282
+ onRetryConnectedApps={onRetryConnectedApps}
283
+ heading={null}
284
+ embeddedInList
285
+ />
286
+ ) : null}
239
287
  </AppList>
240
288
  </>
241
289
  )}
242
- {pendingApps.length > 0 ? (
243
- <Collapsible open={showPending} onOpenChange={setShowPending}>
290
+ {pendingApps.length > 0 &&
291
+ (!searchQuery.trim() || filteredPendingApps.length > 0) ? (
292
+ <Collapsible
293
+ open={showPending || Boolean(searchQuery.trim())}
294
+ onOpenChange={setShowPending}
295
+ >
244
296
  <div className="space-y-3 border-t pt-3">
245
297
  <div className="flex flex-wrap items-center justify-between gap-3">
246
298
  <div className="flex min-w-0 items-center gap-2">
@@ -266,19 +318,23 @@ function AppsPanel({
266
318
  size="sm"
267
319
  className="h-7 gap-1.5 px-2 text-xs"
268
320
  >
269
- {showPending
321
+ {showPending || Boolean(searchQuery.trim())
270
322
  ? t("dispatch.pages.hidePendingApps")
271
323
  : t("dispatch.pages.showPendingApps")}
272
324
  <IconChevronDown
273
325
  size={13}
274
- className={showPending ? "rotate-180" : undefined}
326
+ className={
327
+ showPending || searchQuery.trim()
328
+ ? "rotate-180"
329
+ : undefined
330
+ }
275
331
  />
276
332
  </Button>
277
333
  </CollapsibleTrigger>
278
334
  </div>
279
335
  <CollapsibleContent>
280
336
  <AppList className={APP_LIST_GRID_CLASS}>
281
- {pendingApps.map((app) => (
337
+ {filteredPendingApps.map((app) => (
282
338
  <WorkspaceAppCard
283
339
  key={app.id}
284
340
  app={app}
@@ -1,7 +1,9 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import { IconInfoCircle } from "@tabler/icons-react";
3
3
  import { type ReactNode } from "react";
4
+ import { useLocation } from "react-router";
4
5
 
6
+ import { dispatchDocsHrefForPath, DocsLink } from "./docs-link";
5
7
  import { useSetPageTitle } from "./layout/HeaderActions";
6
8
  import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
7
9
 
@@ -21,12 +23,16 @@ export function DispatchShell({
21
23
  children: ReactNode;
22
24
  }) {
23
25
  const t = useT();
26
+ const location = useLocation();
27
+ const docsHref = dispatchDocsHrefForPath(location.pathname);
24
28
  useSetPageTitle(
25
29
  <div className="flex items-center gap-2 min-w-0">
26
30
  <h1 className="text-lg font-semibold tracking-tight truncate text-foreground">
27
31
  {title}
28
32
  </h1>
29
- {description ? (
33
+ {docsHref ? (
34
+ <DocsLink href={docsHref} label={`Open ${title} documentation`} />
35
+ ) : description ? (
30
36
  <Popover>
31
37
  <PopoverTrigger asChild>
32
38
  <button
@@ -0,0 +1,52 @@
1
+ import { IconHelpCircle } from "@tabler/icons-react";
2
+
3
+ import { cn } from "../lib/utils";
4
+
5
+ export const DISPATCH_DOCS = {
6
+ dispatch: "https://agent-native.com/docs/dispatch#what-it-does",
7
+ workspace: "https://agent-native.com/docs/agent-resources#global-resources",
8
+ integrations: "https://agent-native.com/docs/dispatch#secret-vault",
9
+ audit: "https://agent-native.com/docs/audit-log",
10
+ threadDebug: "https://agent-native.com/docs/dispatch#reliability",
11
+ dreams: "https://agent-native.com/docs/dispatch#dreams",
12
+ settings: "https://agent-native.com/docs/agent-surfaces",
13
+ } as const;
14
+
15
+ export function dispatchDocsHrefForPath(pathname: string): string | undefined {
16
+ if (pathname === "/admin/workspace") return DISPATCH_DOCS.workspace;
17
+ if (pathname === "/admin/integrations") return DISPATCH_DOCS.integrations;
18
+ if (pathname === "/admin/audit") return DISPATCH_DOCS.audit;
19
+ if (pathname === "/admin/thread-debug") return DISPATCH_DOCS.threadDebug;
20
+ if (pathname === "/admin/dreams") return DISPATCH_DOCS.dreams;
21
+ if (pathname.startsWith("/admin/")) return DISPATCH_DOCS.dispatch;
22
+ if (pathname === "/settings" || pathname.startsWith("/settings/")) {
23
+ return DISPATCH_DOCS.settings;
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ export function DocsLink({
29
+ href,
30
+ label,
31
+ className,
32
+ }: {
33
+ href: string;
34
+ label: string;
35
+ className?: string;
36
+ }) {
37
+ return (
38
+ <a
39
+ href={href}
40
+ target="_blank"
41
+ rel="noopener noreferrer"
42
+ aria-label={label}
43
+ title={label}
44
+ className={cn(
45
+ "inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent/60 hover:text-foreground",
46
+ className,
47
+ )}
48
+ >
49
+ <IconHelpCircle className="size-3" />
50
+ </a>
51
+ );
52
+ }
@@ -11,6 +11,7 @@ import {
11
11
  formatThreadAge,
12
12
  isElectronEmbeddedSearch,
13
13
  NavContent,
14
+ shouldAutoCollapseDispatchSidebar,
14
15
  } from "./Layout";
15
16
 
16
17
  const clientState = vi.hoisted(() => ({
@@ -128,6 +129,15 @@ describe("Electron control-plane mode", () => {
128
129
  });
129
130
  });
130
131
 
132
+ describe("Dispatch workspace app sidebar", () => {
133
+ it("auto-collapses for app host routes but not the app catalog", () => {
134
+ expect(shouldAutoCollapseDispatchSidebar("/apps/mail")).toBe(true);
135
+ expect(shouldAutoCollapseDispatchSidebar("/apps/mail/settings")).toBe(true);
136
+ expect(shouldAutoCollapseDispatchSidebar("/apps")).toBe(false);
137
+ expect(shouldAutoCollapseDispatchSidebar("/chat")).toBe(false);
138
+ });
139
+ });
140
+
131
141
  describe("Dispatch NavContent", () => {
132
142
  let container: HTMLDivElement;
133
143
  let root: Root;
@@ -302,6 +312,9 @@ describe("Dispatch NavContent", () => {
302
312
  );
303
313
  expect(sidebarLabel?.textContent?.trim()).toBe("Dispatch");
304
314
  expect(container.textContent).not.toContain("Agent-Native Dispatch");
315
+ expect(
316
+ sidebarLabel?.closest('a[data-dispatch-logo][href="/overview"]'),
317
+ ).not.toBeNull();
305
318
 
306
319
  const settingsLink = container.querySelector('a[href="/settings"]');
307
320
  const adminLink = container.querySelector('a[href="/admin"]');