@agent-native/dispatch 0.31.14 → 0.31.15

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 (29) hide show
  1. package/dist/actions/provider-api-register.d.ts +1 -1
  2. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  3. package/dist/components/dispatch-control-plane.js +2 -2
  4. package/dist/components/dispatch-control-plane.js.map +1 -1
  5. package/dist/components/layout/Layout.d.ts +17 -4
  6. package/dist/components/layout/Layout.d.ts.map +1 -1
  7. package/dist/components/layout/Layout.js +45 -20
  8. package/dist/components/layout/Layout.js.map +1 -1
  9. package/dist/components/workspace-app-card.d.ts.map +1 -1
  10. package/dist/components/workspace-app-card.js +18 -1
  11. package/dist/components/workspace-app-card.js.map +1 -1
  12. package/dist/lib/catch-all-target.d.ts.map +1 -1
  13. package/dist/lib/catch-all-target.js +4 -2
  14. package/dist/lib/catch-all-target.js.map +1 -1
  15. package/dist/routes/pages/automations.d.ts.map +1 -1
  16. package/dist/routes/pages/automations.js +19 -16
  17. package/dist/routes/pages/automations.js.map +1 -1
  18. package/package.json +3 -3
  19. package/src/components/dispatch-control-plane.spec.tsx +3 -1
  20. package/src/components/dispatch-control-plane.tsx +2 -1
  21. package/src/components/layout/Layout.spec.tsx +143 -25
  22. package/src/components/layout/Layout.tsx +79 -47
  23. package/src/components/workspace-app-card.tsx +27 -1
  24. package/src/lib/catch-all-target.spec.ts +8 -0
  25. package/src/lib/catch-all-target.ts +4 -2
  26. package/src/routes/pages/automations.spec.tsx +240 -0
  27. package/src/routes/pages/automations.tsx +22 -16
  28. package/src/styles/dispatch-css.spec.ts +3 -0
  29. package/src/styles/dispatch.css +5 -0
@@ -29,6 +29,8 @@ const clientState = vi.hoisted(() => ({
29
29
  }));
30
30
 
31
31
  vi.mock("@agent-native/core/client/agent-chat", () => ({
32
+ chatModelSelectionStorageKey: (namespace: string) =>
33
+ `agent-native:chat-models:selection:${namespace}`,
32
34
  navigateWithAgentChatViewTransition: (
33
35
  navigate: unknown,
34
36
  path: string,
@@ -161,7 +163,7 @@ describe("DispatchControlPlane", () => {
161
163
  container.querySelector('[data-placeholder="Ask Dispatch anything..."]'),
162
164
  ).not.toBeNull();
163
165
  expect(clientState.useChatModels).toHaveBeenCalledWith({
164
- storageKey: "dispatch",
166
+ storageKey: "agent-native:chat-models:selection:dispatch",
165
167
  });
166
168
  expect(clientState.promptComposerProps).toMatchObject({
167
169
  availableModels: [],
@@ -1,4 +1,5 @@
1
1
  import {
2
+ chatModelSelectionStorageKey,
2
3
  navigateWithAgentChatViewTransition,
3
4
  useChatModels,
4
5
  } from "@agent-native/core/client/agent-chat";
@@ -71,7 +72,7 @@ function CommandPanel() {
71
72
  selectedEffort,
72
73
  selectedEngine,
73
74
  selectedModel,
74
- } = useChatModels({ storageKey: "dispatch" });
75
+ } = useChatModels({ storageKey: chatModelSelectionStorageKey("dispatch") });
75
76
  const navigate = useNavigate();
76
77
  const promptSuggestions = [
77
78
  t("dispatch.pages.suggestionOnboardingApp", {
@@ -8,9 +8,11 @@ import { AdminShell } from "../admin-navigation";
8
8
  import { TooltipProvider } from "../ui/tooltip";
9
9
  import {
10
10
  buildChatFirstEmbedSessionInput,
11
+ CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME,
11
12
  formatThreadAge,
12
13
  isElectronEmbeddedSearch,
13
14
  NavContent,
15
+ renderChatFirstAppSurfaceTab,
14
16
  shouldAutoCollapseDispatchSidebar,
15
17
  } from "./Layout";
16
18
 
@@ -19,10 +21,17 @@ const clientState = vi.hoisted(() => ({
19
21
  switchThread: vi.fn(),
20
22
  threads: [] as Array<Record<string, unknown>>,
21
23
  workspaceApps: [] as Array<Record<string, unknown>>,
24
+ // Stable identity: WorkspaceAppFrame's embed effect depends on this
25
+ // function, so a fresh mock per render would re-run the effect forever.
26
+ createEmbedSessionMutateAsync: vi
27
+ .fn()
28
+ .mockResolvedValue({ startUrl: "about:blank" }),
22
29
  }));
23
30
 
24
31
  vi.mock("@agent-native/core/client/agent-chat", () => ({
25
- AgentSidebar: ({ children }: { children: React.ReactNode }) => children,
32
+ AgentSidebar: ({ children }: { children: React.ReactNode }) => (
33
+ <div data-agent-sidebar>{children}</div>
34
+ ),
26
35
  focusAgentChat: vi.fn(),
27
36
  navigateWithAgentChatViewTransition: (
28
37
  navigate: (path: string) => void,
@@ -53,6 +62,17 @@ vi.mock("@agent-native/core/client/hooks", () => ({
53
62
  action === "list-workspace-apps" ? clientState.workspaceApps : undefined,
54
63
  isLoading: false,
55
64
  }),
65
+ useActionMutation: () => ({
66
+ mutateAsync: clientState.createEmbedSessionMutateAsync,
67
+ }),
68
+ }));
69
+
70
+ vi.mock("@agent-native/core/client/feature-flags", () => ({
71
+ useFeatureFlag: () => false,
72
+ }));
73
+
74
+ vi.mock("next-themes", () => ({
75
+ useTheme: () => ({ resolvedTheme: "light" }),
56
76
  }));
57
77
 
58
78
  vi.mock("@agent-native/core/client/i18n", () => ({
@@ -226,7 +246,7 @@ describe("Dispatch NavContent", () => {
226
246
  { mode: "chat-first", chatFirstMode: true, collapsed: false },
227
247
  { mode: "chat-first", chatFirstMode: true, collapsed: true },
228
248
  ])(
229
- "reserves environment pill space in the $mode desktop footer when collapsed is $collapsed",
249
+ "keeps footer controls flush with the bottom in the $mode desktop footer when collapsed is $collapsed",
230
250
  async ({ mode, chatFirstMode, collapsed }) => {
231
251
  await act(async () => {
232
252
  root.render(
@@ -234,11 +254,7 @@ describe("Dispatch NavContent", () => {
234
254
  initialEntries={[chatFirstMode ? "/chat" : "/overview"]}
235
255
  >
236
256
  <TooltipProvider>
237
- <NavContent
238
- chatFirstMode={chatFirstMode}
239
- collapsed={collapsed}
240
- reserveEnvironmentBadgeSpace
241
- />
257
+ <NavContent chatFirstMode={chatFirstMode} collapsed={collapsed} />
242
258
  </TooltipProvider>
243
259
  </MemoryRouter>,
244
260
  );
@@ -258,7 +274,7 @@ describe("Dispatch NavContent", () => {
258
274
 
259
275
  expect(footer?.className).toContain("mt-auto");
260
276
  expect(footer?.className).toContain("shrink-0");
261
- expect(footer?.className).toContain("pb-10");
277
+ expect(footer?.className).not.toContain("pb-10");
262
278
  if (mode === "standard") {
263
279
  expect(footer?.closest(".overflow-y-auto")).toBeNull();
264
280
  }
@@ -278,23 +294,6 @@ describe("Dispatch NavContent", () => {
278
294
  },
279
295
  );
280
296
 
281
- it("reserves environment pill space in the mobile footer", async () => {
282
- await act(async () => {
283
- root.render(
284
- <MemoryRouter initialEntries={["/overview"]}>
285
- <TooltipProvider>
286
- <NavContent reserveEnvironmentBadgeSpace />
287
- </TooltipProvider>
288
- </MemoryRouter>,
289
- );
290
- });
291
-
292
- expect(
293
- container.querySelector('[data-dispatch-sidebar-footer="standard"]')
294
- ?.className,
295
- ).toContain("pb-10");
296
- });
297
-
298
297
  it("keeps chat-first primary actions in the collapsed sidebar", async () => {
299
298
  await act(async () => {
300
299
  root.render(
@@ -622,3 +621,122 @@ describe("Dispatch NavContent", () => {
622
621
  expect(clientState.switchThread).toHaveBeenCalledWith("new-thread");
623
622
  });
624
623
  });
624
+
625
+ function readUnprefixedZIndexClass(className: string): number | null {
626
+ const match = className.match(/(?:^|\s)z-\[?(\d+)\]?(?=\s|$)/);
627
+ return match ? Number(match[1]) : null;
628
+ }
629
+
630
+ function readMobileZIndexClass(className: string): number | null {
631
+ const match = className.match(/max-\[767px\]:z-\[?(\d+)\]?(?=\s|$)/);
632
+ return match ? Number(match[1]) : null;
633
+ }
634
+
635
+ describe("chat-first surface panel toggle stacking", () => {
636
+ it("keeps the toggle above the mobile full-screen surface panel overlay", async () => {
637
+ const { ChatFirstSurfacePanelToggle: RealChatFirstSurfacePanelToggle } =
638
+ await vi.importActual<
639
+ typeof import("@agent-native/core/client/agent-chat")
640
+ >("@agent-native/core/client/agent-chat");
641
+ const { ChatFirstSurfacePanel } =
642
+ await import("@agent-native/core/client/chat-first");
643
+
644
+ const container = document.createElement("div");
645
+ document.body.appendChild(container);
646
+ const root = createRoot(container);
647
+
648
+ await act(async () => {
649
+ root.render(
650
+ <>
651
+ <ChatFirstSurfacePanel width={320} onResizePointerDown={() => {}}>
652
+ <div>side surface content</div>
653
+ </ChatFirstSurfacePanel>
654
+ <RealChatFirstSurfacePanelToggle
655
+ open={false}
656
+ onToggle={() => {}}
657
+ className={CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME}
658
+ />
659
+ </>,
660
+ );
661
+ });
662
+
663
+ const panelClassName =
664
+ container.querySelector("[data-chat-first-surface-panel]")?.className ??
665
+ "";
666
+ const toggleClassName =
667
+ container.querySelector("[data-chat-first-surface-toggle]")?.className ??
668
+ "";
669
+
670
+ // Below 768px the panel becomes a full-screen absolute overlay at this
671
+ // z-index (surface-panel.tsx). The toggle is the only control that can
672
+ // dismiss it, so it must always paint above that overlay.
673
+ const panelMobileZIndex = readMobileZIndexClass(panelClassName);
674
+ const toggleZIndex = readUnprefixedZIndexClass(toggleClassName);
675
+ expect(panelMobileZIndex).not.toBeNull();
676
+ expect(toggleZIndex).not.toBeNull();
677
+ expect(toggleZIndex as number).toBeGreaterThan(panelMobileZIndex as number);
678
+
679
+ act(() => root.unmount());
680
+ container.remove();
681
+ });
682
+ });
683
+
684
+ describe("chat-first app surface tab chat rail", () => {
685
+ const registration = { id: "mail", name: "Mail", path: "/mail" };
686
+
687
+ beforeEach(() => {
688
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
689
+ });
690
+
691
+ afterEach(() => {
692
+ vi.unstubAllGlobals();
693
+ });
694
+
695
+ async function renderAppTab(isMobileSurface: boolean) {
696
+ const { defaultChatFirstCopy } =
697
+ await import("@agent-native/core/client/chat-first");
698
+ vi.stubGlobal(
699
+ "fetch",
700
+ vi.fn().mockResolvedValue(new Response("{}", { status: 200 })),
701
+ );
702
+ const container = document.createElement("div");
703
+ document.body.appendChild(container);
704
+ const root = createRoot(container);
705
+ await act(async () => {
706
+ root.render(
707
+ renderChatFirstAppSurfaceTab({
708
+ registration,
709
+ embedPath: "/mail",
710
+ loading: false,
711
+ isMobileSurface,
712
+ copy: defaultChatFirstCopy,
713
+ }),
714
+ );
715
+ });
716
+ return { container, root };
717
+ }
718
+
719
+ it("does not mount a second full-screen chat rail while the mobile surface panel already covers the screen", async () => {
720
+ const { container, root } = await renderAppTab(true);
721
+
722
+ // ChatFirstSurfacePanel is already a full-screen overlay below 768px
723
+ // (surface-panel.tsx). A nested AgentSidebar chat rail here would stack a
724
+ // second full-screen shell on top of it.
725
+ expect(container.querySelector("[data-agent-sidebar]")).toBeNull();
726
+ expect(
727
+ container.querySelector("[data-chat-first-app-pane]"),
728
+ ).not.toBeNull();
729
+
730
+ act(() => root.unmount());
731
+ container.remove();
732
+ });
733
+
734
+ it("still gives the app its own chat rail on desktop, where the surface panel is inline", async () => {
735
+ const { container, root } = await renderAppTab(false);
736
+
737
+ expect(container.querySelector("[data-agent-sidebar]")).not.toBeNull();
738
+
739
+ act(() => root.unmount());
740
+ container.remove();
741
+ });
742
+ });
@@ -99,6 +99,7 @@ import {
99
99
  import { Link, useLocation, useNavigate } from "react-router";
100
100
  import { toast } from "sonner";
101
101
 
102
+ import { useIsMobile } from "../../hooks/use-mobile";
102
103
  import { cn } from "../../lib/utils";
103
104
  import {
104
105
  isDispatchWorkspaceAppId,
@@ -211,6 +212,12 @@ const DISPATCH_SIDEBAR_LABEL = "Dispatch";
211
212
  const CHROMELESS_PATHS = ["/approval", "/browser-chat", "/browser-connect"];
212
213
  const SIDEBAR_COLLAPSE_KEY = "dispatch.sidebar.collapsed";
213
214
  const CHAT_HISTORY_SOURCE_KEY = "dispatch.chat-history.source";
215
+ // Below 768px, ChatFirstSurfacePanel becomes a max-[767px]:z-10 full-screen
216
+ // overlay (surface-panel.tsx). This toggle is the only way to dismiss it, so
217
+ // its z-index must stay above that overlay in every stacking context or the
218
+ // panel becomes undismissable on mobile.
219
+ export const CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME =
220
+ "absolute right-3 top-2 z-20";
214
221
 
215
222
  interface DispatchChatFirstPane {
216
223
  appId: string;
@@ -953,7 +960,6 @@ export function NavContent({
953
960
  chatFirstEmbedded = false,
954
961
  collapsed = false,
955
962
  collapsible = false,
956
- reserveEnvironmentBadgeSpace = false,
957
963
  onCollapsedChange,
958
964
  chatFirstAppLayout,
959
965
  onChatFirstAppLayoutChange,
@@ -972,7 +978,6 @@ export function NavContent({
972
978
  chatFirstEmbedded?: boolean;
973
979
  collapsed?: boolean;
974
980
  collapsible?: boolean;
975
- reserveEnvironmentBadgeSpace?: boolean;
976
981
  onCollapsedChange?: (collapsed: boolean) => void;
977
982
  chatFirstAppLayout?: ChatFirstAppLayoutPreference;
978
983
  onChatFirstAppLayoutChange?: (layout: ChatFirstAppLayoutPreference) => void;
@@ -1314,10 +1319,7 @@ export function NavContent({
1314
1319
  </div>
1315
1320
  ) : null}
1316
1321
  <div
1317
- className={cn(
1318
- "mt-auto shrink-0",
1319
- reserveEnvironmentBadgeSpace && "pb-10",
1320
- )}
1322
+ className="mt-auto shrink-0"
1321
1323
  data-dispatch-sidebar-footer={chatFirstMode ? "chat-first" : "standard"}
1322
1324
  >
1323
1325
  {bottomNavigation}
@@ -1328,6 +1330,64 @@ export function NavContent({
1328
1330
  );
1329
1331
  }
1330
1332
 
1333
+ /**
1334
+ * Below 768px `ChatFirstSurfacePanel` is already a full-screen overlay
1335
+ * (surface-panel.tsx). Mounting the sub-app's own `AgentSidebar` chat rail
1336
+ * inside it too would stack a second full-screen shell on top of it, so the
1337
+ * rail only gets its own chat surface once there is room beside the panel.
1338
+ */
1339
+ export function renderChatFirstAppSurfaceTab({
1340
+ registration,
1341
+ embedPath,
1342
+ loading,
1343
+ isMobileSurface,
1344
+ copy,
1345
+ }: {
1346
+ registration: ChatFirstAppRegistration | null;
1347
+ embedPath: string;
1348
+ loading: boolean;
1349
+ isMobileSurface: boolean;
1350
+ copy: ChatFirstCopy;
1351
+ }): ReactNode {
1352
+ if (!registration) {
1353
+ return (
1354
+ <ChatFirstAppPane
1355
+ app={null}
1356
+ status={loading ? "loading" : "unresolved"}
1357
+ renderEmbed={() => null}
1358
+ copy={copy}
1359
+ />
1360
+ );
1361
+ }
1362
+ if (!embedPath.startsWith("/")) {
1363
+ return (
1364
+ <ChatFirstAppPane
1365
+ app={{
1366
+ id: registration.id,
1367
+ name: registration.name ?? registration.id,
1368
+ }}
1369
+ status="error"
1370
+ errorMessage={copy("appUnavailable")}
1371
+ renderEmbed={() => null}
1372
+ copy={copy}
1373
+ />
1374
+ );
1375
+ }
1376
+ return (
1377
+ <WorkspaceAppFrame
1378
+ app={{
1379
+ id: registration.id,
1380
+ name: registration.name ?? registration.id,
1381
+ path: registration.path,
1382
+ url: registration.url,
1383
+ }}
1384
+ embedPath={embedPath}
1385
+ chatSidebar={!isMobileSurface}
1386
+ copy={copy}
1387
+ />
1388
+ );
1389
+ }
1390
+
1331
1391
  export function Layout({
1332
1392
  children,
1333
1393
  extensions,
@@ -1341,6 +1401,10 @@ export function Layout({
1341
1401
  const location = useLocation();
1342
1402
  const navigate = useNavigate();
1343
1403
  const [mobileOpen, setMobileOpen] = useState(false);
1404
+ // Drives renderChatFirstSurfaceTab's app-tab chatSidebar decision below —
1405
+ // the chat-first surface panel is already a full-screen overlay at this
1406
+ // width, so an app tab must not also mount its own full-screen chat rail.
1407
+ const isMobileSurface = useIsMobile();
1344
1408
  const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
1345
1409
  if (typeof window === "undefined") return false;
1346
1410
  try {
@@ -2093,44 +2157,13 @@ export function Layout({
2093
2157
  (candidate) => candidate.id === tab.appId,
2094
2158
  ) ?? null)
2095
2159
  : null;
2096
- if (!registration) {
2097
- return (
2098
- <ChatFirstAppPane
2099
- app={null}
2100
- status={chatFirstAppsQuery.isLoading ? "loading" : "unresolved"}
2101
- renderEmbed={() => null}
2102
- copy={chatFirstCopy}
2103
- />
2104
- );
2105
- }
2106
- const embedPath = tab.path ?? registration.path ?? "/";
2107
- if (!embedPath.startsWith("/")) {
2108
- return (
2109
- <ChatFirstAppPane
2110
- app={{
2111
- id: registration.id,
2112
- name: registration.name ?? registration.id,
2113
- }}
2114
- status="error"
2115
- errorMessage={chatFirstCopy("appUnavailable")}
2116
- renderEmbed={() => null}
2117
- copy={chatFirstCopy}
2118
- />
2119
- );
2120
- }
2121
- return (
2122
- <WorkspaceAppFrame
2123
- app={{
2124
- id: registration.id,
2125
- name: registration.name ?? registration.id,
2126
- path: registration.path,
2127
- url: registration.url,
2128
- }}
2129
- embedPath={embedPath}
2130
- chatSidebar
2131
- copy={chatFirstCopy}
2132
- />
2133
- );
2160
+ return renderChatFirstAppSurfaceTab({
2161
+ registration,
2162
+ embedPath: tab.path ?? registration?.path ?? "/",
2163
+ loading: chatFirstAppsQuery.isLoading,
2164
+ isMobileSurface,
2165
+ copy: chatFirstCopy,
2166
+ });
2134
2167
  }
2135
2168
  if (tab.kind === "browser" && tab.url) {
2136
2169
  return (
@@ -2203,6 +2236,7 @@ export function Layout({
2203
2236
  chatFirstSurfaceTabs.activeTabId,
2204
2237
  chatFirstAppRegistrations,
2205
2238
  closeChatFirstSurfaceTab,
2239
+ isMobileSurface,
2206
2240
  renderChatFirstWatchChat,
2207
2241
  ],
2208
2242
  );
@@ -2270,7 +2304,7 @@ export function Layout({
2270
2304
  <ChatFirstSurfacePanelToggle
2271
2305
  open={chatFirstSurfacePanel.open}
2272
2306
  onToggle={chatFirstSurfacePanel.toggle}
2273
- className="absolute right-3 top-2 z-[4]"
2307
+ className={CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME}
2274
2308
  />
2275
2309
  ) : null}
2276
2310
  {isChatRoute && chatFirstMode && chatFirstNotice ? (
@@ -2463,7 +2497,6 @@ export function Layout({
2463
2497
  }}
2464
2498
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2465
2499
  collapsible
2466
- reserveEnvironmentBadgeSpace
2467
2500
  onCollapsedChange={setSidebarCollapsed}
2468
2501
  />
2469
2502
  </aside>
@@ -2485,7 +2518,6 @@ export function Layout({
2485
2518
  chatFirstMode={chatFirstMode}
2486
2519
  chatFirstEmbedded={chatFirstEmbedded}
2487
2520
  collapsed={false}
2488
- reserveEnvironmentBadgeSpace
2489
2521
  chatFirstAppLayout={chatFirstAppLayout}
2490
2522
  onChatFirstAppLayoutChange={persistChatFirstAppLayout}
2491
2523
  chatFirstApps={chatFirstAppItems}
@@ -85,6 +85,22 @@ function deferWorkspaceAppOverlayOpen(
85
85
  }
86
86
  }
87
87
 
88
+ // The settings menu's own DropdownMenuContent restores focus to its trigger
89
+ // once its FocusScope unmounts, independent of the requestAnimationFrame
90
+ // above. If that restore lands after the deferred overlay's DismissableLayer
91
+ // has already mounted, the resulting focusin event reads as an outside
92
+ // interaction and immediately dismisses the overlay we just opened. Skip the
93
+ // default restore whenever we're mid-handoff to a sibling overlay — mirrors
94
+ // AgentPanel's identical guard (consumeAgentPanelOverlayFocusRestore).
95
+ function consumeWorkspaceAppOverlayFocusRestore(
96
+ pendingOverlayRef: { current: boolean },
97
+ event: { preventDefault: () => void },
98
+ ): void {
99
+ if (!pendingOverlayRef.current) return;
100
+ pendingOverlayRef.current = false;
101
+ event.preventDefault();
102
+ }
103
+
88
104
  export function WorkspaceAppCard({
89
105
  app,
90
106
  className,
@@ -413,6 +429,7 @@ function WorkspaceAppSettings({
413
429
  const [shareOpen, setShareOpen] = useState(false);
414
430
  const [settingsOpen, setSettingsOpen] = useState(false);
415
431
  const settingsTriggerRef = useRef<HTMLButtonElement>(null);
432
+ const pendingShareOverlayRef = useRef(false);
416
433
 
417
434
  const handleShareOpenChange = (open: boolean) => {
418
435
  setShareOpen(open);
@@ -452,6 +469,12 @@ function WorkspaceAppSettings({
452
469
  <DropdownMenuContent
453
470
  align="end"
454
471
  className={cn(APP_ACTION_MENU_CONTENT_CLASS, "min-w-max")}
472
+ onCloseAutoFocus={(event) =>
473
+ consumeWorkspaceAppOverlayFocusRestore(
474
+ pendingShareOverlayRef,
475
+ event,
476
+ )
477
+ }
455
478
  >
456
479
  <DropdownMenuItem onSelect={onEdit}>
457
480
  <IconEdit size={14} aria-hidden="true" />
@@ -462,7 +485,10 @@ function WorkspaceAppSettings({
462
485
  onSelect={(event) =>
463
486
  deferWorkspaceAppOverlayOpen(
464
487
  event,
465
- () => setSettingsOpen(false),
488
+ () => {
489
+ pendingShareOverlayRef.current = true;
490
+ setSettingsOpen(false);
491
+ },
466
492
  () => setShareOpen(true),
467
493
  )
468
494
  }
@@ -12,6 +12,14 @@ describe("resolveCatchAllTarget", () => {
12
12
  ).toBe("/todo");
13
13
  });
14
14
 
15
+ it("keeps the legacy mounted path for normalized agent IDs", () => {
16
+ expect(
17
+ resolveCatchAllTarget("clips", {
18
+ workspaceApps: [{ id: "clips", path: "/videos" }],
19
+ }),
20
+ ).toBe("/videos");
21
+ });
22
+
15
23
  it("falls back to the built-in template URL when no workspace manifest exists", () => {
16
24
  expect(
17
25
  resolveCatchAllTarget("forms", {
@@ -116,9 +116,11 @@ export async function resolveServerCatchAllTarget(
116
116
  appId: string,
117
117
  ): Promise<string | null> {
118
118
  if (!import.meta.env.SSR) return null;
119
- const { getBuiltinAgents, loadWorkspaceAppsManifest } =
119
+ const { getBuiltinAgents, loadWorkspaceAppsManifest, normalizeAgentId } =
120
120
  await import("@agent-native/core/server/agent-discovery");
121
- return resolveCatchAllTarget(appId, {
121
+ // Dispatch receives legacy route segments before discovery normalizes the
122
+ // manifest IDs, so normalize the lookup key at this boundary as well.
123
+ return resolveCatchAllTarget(normalizeAgentId(appId), {
122
124
  workspaceApps: loadWorkspaceAppsManifest(),
123
125
  builtinAgents: getBuiltinAgents("dispatch"),
124
126
  });