@agent-native/dispatch 0.23.2 → 0.23.5

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 (86) hide show
  1. package/dist/actions/provider-api-register.d.ts +11 -11
  2. package/dist/components/app-list-row.d.ts +1 -1
  3. package/dist/components/app-list-row.d.ts.map +1 -1
  4. package/dist/components/app-list-row.js +2 -2
  5. package/dist/components/app-list-row.js.map +1 -1
  6. package/dist/components/app-open-actions.d.ts.map +1 -1
  7. package/dist/components/app-open-actions.js +3 -2
  8. package/dist/components/app-open-actions.js.map +1 -1
  9. package/dist/components/create-app-popover.d.ts.map +1 -1
  10. package/dist/components/create-app-popover.js +11 -0
  11. package/dist/components/create-app-popover.js.map +1 -1
  12. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  13. package/dist/components/dispatch-control-plane.js +3 -2
  14. package/dist/components/dispatch-control-plane.js.map +1 -1
  15. package/dist/components/layout/Layout.d.ts +5 -2
  16. package/dist/components/layout/Layout.d.ts.map +1 -1
  17. package/dist/components/layout/Layout.js +122 -13
  18. package/dist/components/layout/Layout.js.map +1 -1
  19. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  20. package/dist/components/layout/workspace-apps-rail.js +3 -23
  21. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  22. package/dist/components/other-apps-section.d.ts.map +1 -1
  23. package/dist/components/other-apps-section.js +5 -1
  24. package/dist/components/other-apps-section.js.map +1 -1
  25. package/dist/components/workspace-app-card.d.ts.map +1 -1
  26. package/dist/components/workspace-app-card.js +3 -4
  27. package/dist/components/workspace-app-card.js.map +1 -1
  28. package/dist/components/workspace-template-card.d.ts.map +1 -1
  29. package/dist/components/workspace-template-card.js +1 -1
  30. package/dist/components/workspace-template-card.js.map +1 -1
  31. package/dist/config.d.ts +2 -0
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js.map +1 -1
  34. package/dist/lib/other-apps.d.ts.map +1 -1
  35. package/dist/lib/other-apps.js +2 -0
  36. package/dist/lib/other-apps.js.map +1 -1
  37. package/dist/lib/workspace-apps.d.ts +4 -0
  38. package/dist/lib/workspace-apps.d.ts.map +1 -1
  39. package/dist/lib/workspace-apps.js +23 -0
  40. package/dist/lib/workspace-apps.js.map +1 -1
  41. package/dist/routes/index.js +1 -1
  42. package/dist/routes/index.js.map +1 -1
  43. package/dist/routes/pages/agent-integrations.d.ts +5 -0
  44. package/dist/routes/pages/agent-integrations.d.ts.map +1 -0
  45. package/dist/routes/pages/agent-integrations.js +15 -0
  46. package/dist/routes/pages/agent-integrations.js.map +1 -0
  47. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  48. package/dist/routes/pages/apps.$appId.js +14 -2
  49. package/dist/routes/pages/apps.$appId.js.map +1 -1
  50. package/dist/routes/pages/apps.d.ts.map +1 -1
  51. package/dist/routes/pages/apps.js +2 -1
  52. package/dist/routes/pages/apps.js.map +1 -1
  53. package/dist/routes/pages/overview.d.ts +2 -2
  54. package/dist/routes/pages/settings.d.ts.map +1 -1
  55. package/dist/routes/pages/settings.js +1 -1
  56. package/dist/routes/pages/settings.js.map +1 -1
  57. package/dist/server/plugins/auth.d.ts.map +1 -1
  58. package/dist/server/plugins/auth.js +1 -0
  59. package/dist/server/plugins/auth.js.map +1 -1
  60. package/package.json +3 -3
  61. package/src/components/app-list-row.tsx +3 -2
  62. package/src/components/app-open-actions.tsx +3 -2
  63. package/src/components/create-app-popover.spec.tsx +26 -2
  64. package/src/components/create-app-popover.tsx +10 -0
  65. package/src/components/dispatch-control-plane.spec.tsx +1 -0
  66. package/src/components/dispatch-control-plane.tsx +8 -2
  67. package/src/components/layout/Layout.spec.tsx +9 -0
  68. package/src/components/layout/Layout.tsx +181 -19
  69. package/src/components/layout/workspace-apps-rail.tsx +4 -26
  70. package/src/components/other-apps-section.tsx +8 -1
  71. package/src/components/workspace-app-card.spec.tsx +6 -6
  72. package/src/components/workspace-app-card.tsx +4 -7
  73. package/src/components/workspace-template-card.spec.tsx +4 -16
  74. package/src/components/workspace-template-card.tsx +0 -1
  75. package/src/config.ts +2 -0
  76. package/src/lib/other-apps.spec.ts +26 -0
  77. package/src/lib/other-apps.ts +3 -0
  78. package/src/lib/workspace-apps.spec.ts +44 -0
  79. package/src/lib/workspace-apps.ts +27 -0
  80. package/src/routes/index.ts +1 -1
  81. package/src/routes/pages/agent-integrations.tsx +24 -0
  82. package/src/routes/pages/apps.$appId.tsx +19 -1
  83. package/src/routes/pages/apps.tsx +5 -2
  84. package/src/routes/pages/settings.tsx +1 -19
  85. package/src/server/plugins/auth.spec.ts +45 -0
  86. package/src/server/plugins/auth.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.23.2",
3
+ "version": "0.23.5",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -84,7 +84,7 @@
84
84
  "tailwind-merge": "^3.5.0",
85
85
  "vaul": "^1.1.2",
86
86
  "zod": "^4.3.6",
87
- "@agent-native/toolkit": "0.13.9"
87
+ "@agent-native/toolkit": "0.13.10"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@react-router/dev": "^8.1.0",
@@ -98,7 +98,7 @@
98
98
  "typescript-7": "npm:typescript@^7.0.2",
99
99
  "vite": "8.1.0",
100
100
  "vitest": "^4.1.5",
101
- "@agent-native/core": "0.149.3"
101
+ "@agent-native/core": "0.151.0"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "@agent-native/core": ">=0.8.0",
@@ -4,7 +4,8 @@ import { cn } from "../lib/utils";
4
4
 
5
5
  export const APP_LIST_GRID_CLASS =
6
6
  "mx-auto w-full max-w-[1000px] xl:grid xl:grid-cols-2 xl:gap-3 xl:overflow-visible xl:rounded-none xl:bg-transparent";
7
- export const APP_LIST_GRID_ROW_CLASS = "xl:rounded-2xl xl:border-0 xl:bg-card";
7
+ export const APP_LIST_GRID_ROW_CLASS =
8
+ "xl:rounded-2xl xl:border xl:border-transparent xl:bg-card xl:transition-[background-color,border-color] xl:hover:border-border xl:focus-within:border-ring/50";
8
9
 
9
10
  export function AppList({
10
11
  children,
@@ -30,7 +31,7 @@ export function AppListRow({
30
31
  return (
31
32
  <div
32
33
  className={cn(
33
- "group flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0 transition-[background-color] hover:bg-muted/30 focus-within:bg-muted/30",
34
+ "group flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0 transition-[background-color,border-color] hover:bg-muted/30 focus-within:bg-muted/30",
34
35
  className,
35
36
  )}
36
37
  >
@@ -44,9 +44,10 @@ export function AppOpenActions({
44
44
  showNewTabOption?: boolean;
45
45
  }) {
46
46
  const labels = { ...DEFAULT_LABELS, ...labelOverrides };
47
+ const canOfferNewTab = showNewTabOption && target !== "_blank";
47
48
  const hasMenu =
48
49
  Boolean(onAddApp) ||
49
- (Boolean(href) && (showInlineOption || showNewTabOption));
50
+ (Boolean(href) && (showInlineOption || canOfferNewTab));
50
51
 
51
52
  if (!href && !hasMenu) {
52
53
  return (
@@ -98,7 +99,7 @@ export function AppOpenActions({
98
99
  {labels.openInline}
99
100
  </DropdownMenuItem>
100
101
  ) : null}
101
- {showNewTabOption && href ? (
102
+ {canOfferNewTab && href ? (
102
103
  <DropdownMenuItem asChild>
103
104
  <a href={href} target="_blank" rel="noreferrer">
104
105
  {labels.openInNewTab}
@@ -152,9 +152,12 @@ describe("CreateAppFlow", () => {
152
152
  vi.unstubAllGlobals();
153
153
  });
154
154
 
155
- async function renderAndSubmit(prompt: string) {
155
+ async function renderAndSubmit(
156
+ prompt: string,
157
+ props: { onClose?: () => void } = {},
158
+ ) {
156
159
  await act(async () => {
157
- root.render(React.createElement(CreateAppFlow, {}));
160
+ root.render(React.createElement(CreateAppFlow, props));
158
161
  });
159
162
 
160
163
  changeValue(
@@ -213,6 +216,27 @@ describe("CreateAppFlow", () => {
213
216
  ).toBe(false);
214
217
  });
215
218
 
219
+ it("opens a fresh local chat when the server hands off app creation", async () => {
220
+ startWorkspaceAppCreationResponse.result = {
221
+ mode: "local-agent",
222
+ appId: "quality-dashboard",
223
+ prompt: "Create the quality dashboard in the new workspace app.",
224
+ message: "Starting the local coding chat.",
225
+ };
226
+ const onClose = vi.fn();
227
+
228
+ await renderAndSubmit("Build a quality dashboard", { onClose });
229
+
230
+ expect(sendToAgentChatMock).toHaveBeenCalledWith({
231
+ message: "Create the quality dashboard in the new workspace app.",
232
+ submit: true,
233
+ type: "code",
234
+ newTab: true,
235
+ reuseEmptyTab: true,
236
+ });
237
+ expect(onClose).toHaveBeenCalledTimes(1);
238
+ });
239
+
216
240
  it("renders the error affordance and a Try again control for builder-error, without a Connect Builder control", async () => {
217
241
  startWorkspaceAppCreationResponse.result = {
218
242
  mode: "builder-unavailable",
@@ -269,6 +269,16 @@ export function CreateAppFlow({
269
269
  if (result?.mode === "builder") {
270
270
  setBranchUrl(result?.url || null);
271
271
  setStatusMessage("Builder branch created.");
272
+ } else if (result?.mode === "local-agent") {
273
+ sendToAgentChat({
274
+ message: result.prompt ?? message,
275
+ submit: true,
276
+ type: "code",
277
+ newTab: true,
278
+ reuseEmptyTab: true,
279
+ });
280
+ setStatusMessage("Sent to the local agent.");
281
+ onClose?.();
272
282
  } else {
273
283
  setStatusMessage(
274
284
  result?.message ||
@@ -142,6 +142,7 @@ describe("DispatchControlPlane", () => {
142
142
  selectedEffort: "medium",
143
143
  selectedEngine: "",
144
144
  selectedModel: "auto",
145
+ rootClassName: "bg-card",
145
146
  });
146
147
 
147
148
  await act(async () => {
@@ -12,7 +12,10 @@ 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 type { WorkspaceAppSummary } from "../lib/workspace-apps";
15
+ import {
16
+ isWorkspaceAppVisibleInDefaultLaunchers,
17
+ type WorkspaceAppSummary,
18
+ } from "../lib/workspace-apps";
16
19
  import { ActionQueryError } from "./action-query-error";
17
20
  import {
18
21
  APP_LIST_GRID_CLASS,
@@ -115,6 +118,7 @@ function CommandPanel() {
115
118
  selectedEffort={selectedEffort}
116
119
  selectedEngine={selectedEngine}
117
120
  selectedModel={selectedModel}
121
+ rootClassName="bg-card"
118
122
  onEffortChange={onEffortChange}
119
123
  onModelChange={onModelChange}
120
124
  onSubmit={(text) => send(text)}
@@ -161,7 +165,9 @@ function AppsPanel({
161
165
  }) {
162
166
  const t = useT();
163
167
  const [showPending, setShowPending] = useState(false);
164
- const visibleApps = apps.filter((app) => !app.isDispatch && !app.archived);
168
+ const visibleApps = apps.filter(
169
+ (app) => isWorkspaceAppVisibleInDefaultLaunchers(app) && !app.archived,
170
+ );
165
171
  const activeApps = visibleApps.filter((app) => app.status !== "pending");
166
172
  const pendingApps = visibleApps.filter((app) => app.status === "pending");
167
173
  const otherAppEntries = mergeOtherAppEntries({
@@ -9,6 +9,7 @@ import { TooltipProvider } from "../ui/tooltip";
9
9
  import {
10
10
  buildChatFirstEmbedSessionInput,
11
11
  formatThreadAge,
12
+ isElectronEmbeddedSearch,
12
13
  NavContent,
13
14
  } from "./Layout";
14
15
 
@@ -119,6 +120,14 @@ describe("chat-first embed sessions", () => {
119
120
  });
120
121
  });
121
122
 
123
+ describe("Electron control-plane mode", () => {
124
+ it("recognizes only the explicit Electron query flag", () => {
125
+ expect(isElectronEmbeddedSearch("?electron=1")).toBe(true);
126
+ expect(isElectronEmbeddedSearch("?electron=0")).toBe(false);
127
+ expect(isElectronEmbeddedSearch("?chatFirst=1")).toBe(false);
128
+ });
129
+ });
130
+
122
131
  describe("Dispatch NavContent", () => {
123
132
  let container: HTMLDivElement;
124
133
  let root: Root;
@@ -55,6 +55,7 @@ import {
55
55
  type ChatFirstAppItem,
56
56
  type ChatFirstCopy,
57
57
  type ChatFirstEmbedTarget,
58
+ type ChatFirstPrimaryTab,
58
59
  } from "@agent-native/core/client/chat-first";
59
60
  import { writeClipboardText } from "@agent-native/core/client/clipboard";
60
61
  import {
@@ -103,7 +104,11 @@ import { toast } from "sonner";
103
104
 
104
105
  import { cn } from "../../lib/utils";
105
106
  import {
107
+ isDispatchWorkspaceAppId,
108
+ isWorkspaceAppVisibleInDefaultLaunchers,
106
109
  mergeChatFirstWorkspaceApps,
110
+ workspaceAppIdFromRoute,
111
+ workspaceAppRoute,
107
112
  type WorkspaceAppSummary,
108
113
  } from "../../lib/workspace-apps";
109
114
  import { AppIcon } from "../app-icon";
@@ -296,6 +301,35 @@ function localDispatchPath(pathname: string): string {
296
301
  return pathname;
297
302
  }
298
303
 
304
+ export function isElectronEmbeddedSearch(search: string): boolean {
305
+ return new URLSearchParams(search).get("electron") === "1";
306
+ }
307
+
308
+ function chatFirstPrimaryTabForPath(
309
+ pathname: string,
310
+ ): ChatFirstPrimaryTab | undefined {
311
+ if (pathname === "/chat" || pathname.startsWith("/chat/")) {
312
+ return "new-chat";
313
+ }
314
+ if (
315
+ pathname === "/integrations" ||
316
+ pathname.startsWith("/integrations/") ||
317
+ pathname === "/admin/integrations" ||
318
+ pathname.startsWith("/admin/integrations/")
319
+ ) {
320
+ return "integrations";
321
+ }
322
+ if (
323
+ pathname === "/automations" ||
324
+ pathname.startsWith("/automations/") ||
325
+ pathname === "/admin/automations" ||
326
+ pathname.startsWith("/admin/automations/")
327
+ ) {
328
+ return "scheduled";
329
+ }
330
+ return undefined;
331
+ }
332
+
299
333
  function dispatchNavLinkTarget(path: string): string {
300
334
  if (typeof window === "undefined") return path;
301
335
  const basePath = appBasePath();
@@ -543,6 +577,8 @@ function DispatchChatsSection({
543
577
  chatFirstMode?: boolean;
544
578
  chatFirstEmbedded?: boolean;
545
579
  chatFirstNavigation?: {
580
+ activeTab?: ChatFirstPrimaryTab;
581
+ onNewChat?: () => void;
546
582
  onOpenIntegrations: () => void;
547
583
  onOpenScheduled: () => void;
548
584
  };
@@ -683,13 +719,17 @@ function DispatchChatsSection({
683
719
  )}
684
720
  >
685
721
  {showNewChat && chatFirstNavigation ? (
686
- <nav className="space-y-0.5 px-0 py-2">
722
+ <nav className="space-y-0.5 px-2 py-2">
687
723
  <ChatFirstPrimaryNavigation
688
724
  copy={chatFirstCopy}
689
- onNewChat={() => void handleNewChat()}
725
+ onNewChat={() => {
726
+ chatFirstNavigation.onNewChat?.();
727
+ void handleNewChat();
728
+ }}
690
729
  onOpenIntegrations={chatFirstNavigation.onOpenIntegrations}
691
730
  onOpenScheduled={chatFirstNavigation.onOpenScheduled}
692
731
  onSearch={openCommandMenu}
732
+ activeTab={chatFirstNavigation.activeTab}
693
733
  />
694
734
  </nav>
695
735
  ) : null}
@@ -816,7 +856,7 @@ function DispatchChatsSection({
816
856
  ) : null}
817
857
  </>
818
858
  )}
819
- className="min-w-0"
859
+ className="min-w-0 px-2"
820
860
  />
821
861
  ) : (
822
862
  <ChatHistoryRail
@@ -883,7 +923,7 @@ function DispatchChatsSection({
883
923
  ) : null}
884
924
  </>
885
925
  )}
886
- className="min-w-0"
926
+ className={cn("min-w-0", chatFirstMode && "px-2")}
887
927
  />
888
928
  )}
889
929
  </div>
@@ -904,6 +944,8 @@ export function NavContent({
904
944
  chatFirstAppsLoading = false,
905
945
  chatFirstAppsError,
906
946
  chatFirstActiveAppId,
947
+ chatFirstActivePrimaryTab,
948
+ onChatFirstNewChat,
907
949
  onChatFirstAppOpen,
908
950
  onChatFirstAppsRetry,
909
951
  }: {
@@ -920,6 +962,8 @@ export function NavContent({
920
962
  chatFirstAppsLoading?: boolean;
921
963
  chatFirstAppsError?: string | null;
922
964
  chatFirstActiveAppId?: string;
965
+ chatFirstActivePrimaryTab?: ChatFirstPrimaryTab;
966
+ onChatFirstNewChat?: () => void;
923
967
  onChatFirstAppOpen?: (app: ChatFirstAppItem) => void;
924
968
  onChatFirstAppsRetry?: () => void;
925
969
  }) {
@@ -1219,6 +1263,8 @@ export function NavContent({
1219
1263
  chatFirstMode
1220
1264
  chatFirstEmbedded={chatFirstEmbedded}
1221
1265
  chatFirstNavigation={{
1266
+ activeTab: chatFirstActivePrimaryTab,
1267
+ onNewChat: onChatFirstNewChat,
1222
1268
  onOpenIntegrations: () => {
1223
1269
  navigate(dispatchNavLinkTarget("/admin/integrations"));
1224
1270
  onNavigate?.();
@@ -1290,6 +1336,11 @@ export function Layout({
1290
1336
  }
1291
1337
  });
1292
1338
  const localPathname = localDispatchPath(location.pathname);
1339
+ const [electronEmbedded] = useState(() =>
1340
+ typeof window !== "undefined"
1341
+ ? isElectronEmbeddedSearch(window.location.search)
1342
+ : false,
1343
+ );
1293
1344
  const isChatRoute =
1294
1345
  localPathname === "/chat" || localPathname.startsWith("/chat/");
1295
1346
  const chatFirstSurfaceScope = threadIdFromPath(localPathname) ?? "new";
@@ -1307,6 +1358,23 @@ export function Layout({
1307
1358
  const chatFirstMode =
1308
1359
  extensions?.chatFirst === true || chatFirstPreference || chatFirstEmbedded;
1309
1360
  const chatFirstHasActiveChat = chatFirstSurfaceScope !== "new";
1361
+ useEffect(() => {
1362
+ if (!electronEmbedded || typeof window === "undefined") return;
1363
+ const url = new URL(window.location.href);
1364
+ if (isElectronEmbeddedSearch(url.search)) return;
1365
+ url.searchParams.set("electron", "1");
1366
+ window.history.replaceState(
1367
+ window.history.state,
1368
+ "",
1369
+ `${url.pathname}${url.search}${url.hash}`,
1370
+ );
1371
+ }, [electronEmbedded, location.pathname, location.search]);
1372
+ useEffect(() => {
1373
+ if (!chatFirstMode || chatFirstEmbedded || localPathname !== "/overview") {
1374
+ return;
1375
+ }
1376
+ navigate(dispatchNavLinkTarget("/chat"), { replace: true });
1377
+ }, [chatFirstEmbedded, chatFirstMode, localPathname, navigate]);
1310
1378
  const [chatFirstAppLayout, setChatFirstAppLayout] =
1311
1379
  useState<ChatFirstAppLayoutPreference>(() => readChatFirstAppLayout());
1312
1380
  const chatFirstAppLayoutHydratedRef = useRef(false);
@@ -1350,7 +1418,9 @@ export function Layout({
1350
1418
  const chatFirstAppItems = useMemo<ChatFirstAppItem[]>(
1351
1419
  () =>
1352
1420
  chatFirstAppRegistrations
1353
- .filter((app) => app.enabled)
1421
+ .filter(
1422
+ (app) => app.enabled && isWorkspaceAppVisibleInDefaultLaunchers(app),
1423
+ )
1354
1424
  .map((app) => ({
1355
1425
  id: app.id,
1356
1426
  name: app.name ?? app.id,
@@ -1371,6 +1441,7 @@ export function Layout({
1371
1441
  null,
1372
1442
  );
1373
1443
  const [chatFirstEmbedAttempt, setChatFirstEmbedAttempt] = useState(0);
1444
+ const chatFirstAppFrameRef = useRef<HTMLIFrameElement>(null);
1374
1445
  const [chatFirstNotice, setChatFirstNotice] = useState<string | null>(null);
1375
1446
  const chatFirstSessionWatch = useChatFirstSessionWatch();
1376
1447
  const chatFirstSurfaceTabs = useChatFirstSurfaceTabs(chatFirstSurfaceScope);
@@ -1410,6 +1481,19 @@ export function Layout({
1410
1481
  isChatRoute &&
1411
1482
  activeChatFirstSurfaceTab?.kind === "app" &&
1412
1483
  activeChatFirstSurfaceTab.placement === "main";
1484
+ const chatFirstAppSelected =
1485
+ chatFirstMode && activeChatFirstSurfaceTab?.kind === "app";
1486
+ const chatFirstActivePrimaryTab = chatFirstAppSelected
1487
+ ? activeChatFirstSurfaceTab?.kind === "app" &&
1488
+ activeChatFirstSurfaceTab.appId === "dispatch"
1489
+ ? chatFirstPrimaryTabForPath(activeChatFirstSurfaceTab.path ?? "")
1490
+ : undefined
1491
+ : chatFirstPrimaryTabForPath(localPathname);
1492
+ const chatFirstActiveAppId = isWorkspaceAppRoute
1493
+ ? (workspaceAppIdFromRoute(localPathname) ?? undefined)
1494
+ : chatFirstAppSelected && activeChatFirstSurfaceTab?.kind === "app"
1495
+ ? activeChatFirstSurfaceTab.appId
1496
+ : undefined;
1413
1497
  const activeChatFirstAppRegistration = useMemo(
1414
1498
  () =>
1415
1499
  activeChatFirstSurfaceTab?.kind === "app" &&
@@ -1496,6 +1580,25 @@ export function Layout({
1496
1580
  createChatFirstEmbedSession.mutateAsync,
1497
1581
  isChatRoute,
1498
1582
  ]);
1583
+
1584
+ useEffect(() => {
1585
+ if (!chatFirstMode || !isChatRoute || !activeChatFirstApp) return;
1586
+
1587
+ const handleEmbedSessionExpired = (event: MessageEvent) => {
1588
+ if (
1589
+ event.data?.type !== "agentNative.embedSessionExpired" ||
1590
+ event.source !== chatFirstAppFrameRef.current?.contentWindow
1591
+ ) {
1592
+ return;
1593
+ }
1594
+ setChatFirstEmbedAttempt((attempt) => attempt + 1);
1595
+ };
1596
+
1597
+ window.addEventListener("message", handleEmbedSessionExpired);
1598
+ return () =>
1599
+ window.removeEventListener("message", handleEmbedSessionExpired);
1600
+ }, [activeChatFirstApp, chatFirstMode, isChatRoute]);
1601
+
1499
1602
  const persistChatFirstPane = useCallback(
1500
1603
  (pane: DispatchChatFirstPane | null) => {
1501
1604
  setChatFirstPane(pane);
@@ -1507,6 +1610,17 @@ export function Layout({
1507
1610
  },
1508
1611
  [],
1509
1612
  );
1613
+ useEffect(() => {
1614
+ if (!chatFirstMode || isChatRoute) return;
1615
+ persistChatFirstPane(null);
1616
+ closeChatFirstSessionWatch();
1617
+ chatFirstSurfaceTabsStore.closeAll();
1618
+ }, [
1619
+ chatFirstMode,
1620
+ chatFirstSurfaceTabsStore,
1621
+ isChatRoute,
1622
+ persistChatFirstPane,
1623
+ ]);
1510
1624
  const openChatFirstPane = useCallback(
1511
1625
  (
1512
1626
  pane: DispatchChatFirstPane,
@@ -1639,6 +1753,19 @@ export function Layout({
1639
1753
  setChatFirstNotice(chatFirstResolutionMessage(resolution.reason));
1640
1754
  return;
1641
1755
  }
1756
+ if (isDispatchWorkspaceAppId(resolution.target.appId)) {
1757
+ closeChatFirstSessionWatch();
1758
+ chatFirstSurfaceTabsStore.closeAll();
1759
+ setChatFirstSurfacePanelOpen(false);
1760
+ persistChatFirstPane(null);
1761
+ if (resolution.target.path) {
1762
+ navigateWithAgentChatViewTransition(
1763
+ navigate,
1764
+ dispatchNavLinkTarget(resolution.target.path),
1765
+ );
1766
+ }
1767
+ return;
1768
+ }
1642
1769
  openChatFirstPane(resolution.target);
1643
1770
  },
1644
1771
  [
@@ -1646,7 +1773,12 @@ export function Layout({
1646
1773
  chatFirstAppsQuery.isError,
1647
1774
  chatFirstAppsQuery.isLoading,
1648
1775
  chatFirstGrantedAppsQuery.isLoading,
1776
+ chatFirstSurfaceTabsStore,
1777
+ closeChatFirstSessionWatch,
1649
1778
  openChatFirstPane,
1779
+ navigate,
1780
+ persistChatFirstPane,
1781
+ setChatFirstSurfacePanelOpen,
1650
1782
  ],
1651
1783
  );
1652
1784
  const chatHomeHandoffActive = useAgentChatHomeHandoff({
@@ -2023,6 +2155,7 @@ export function Layout({
2023
2155
  data-dispatch-chat-first-app-frame
2024
2156
  src={url}
2025
2157
  title={title ?? chatFirstCopy("appUnavailable")}
2158
+ ref={chatFirstAppFrameRef}
2026
2159
  referrerPolicy="no-referrer"
2027
2160
  allow="clipboard-read; clipboard-write"
2028
2161
  className="h-full w-full border-0 bg-background"
@@ -2113,6 +2246,29 @@ export function Layout({
2113
2246
  return <>{children}</>;
2114
2247
  }
2115
2248
 
2249
+ if (electronEmbedded) {
2250
+ return (
2251
+ <DispatchExtensionsContext.Provider value={extensions}>
2252
+ <HeaderActionsProvider>
2253
+ <div
2254
+ data-dispatch-electron-control-plane
2255
+ className="flex h-screen w-full overflow-hidden bg-background"
2256
+ >
2257
+ <div className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
2258
+ <Header showAgentToggle={false} />
2259
+ <InvitationBanner />
2260
+ <main className="flex-1 overflow-y-auto">
2261
+ <div className="mx-auto max-w-7xl space-y-10 px-4 py-6 sm:px-6">
2262
+ {children}
2263
+ </div>
2264
+ </main>
2265
+ </div>
2266
+ </div>
2267
+ </HeaderActionsProvider>
2268
+ </DispatchExtensionsContext.Provider>
2269
+ );
2270
+ }
2271
+
2116
2272
  if (isWorkspaceAppHostRoute) {
2117
2273
  return (
2118
2274
  <DispatchExtensionsContext.Provider value={extensions}>
@@ -2305,14 +2461,18 @@ export function Layout({
2305
2461
  ? chatFirstCopy("appsLoadError")
2306
2462
  : null
2307
2463
  }
2308
- chatFirstActiveAppId={
2309
- activeChatFirstSurfaceTab?.kind === "app"
2310
- ? activeChatFirstSurfaceTab.appId
2311
- : undefined
2312
- }
2313
- onChatFirstAppOpen={(app) =>
2314
- openChatFirstPane({ appId: app.id }, "main")
2315
- }
2464
+ chatFirstActiveAppId={chatFirstActiveAppId}
2465
+ chatFirstActivePrimaryTab={chatFirstActivePrimaryTab}
2466
+ onChatFirstNewChat={() => {
2467
+ closeChatFirstSessionWatch();
2468
+ chatFirstSurfaceTabsStore.closeAll();
2469
+ setChatFirstSurfacePanelOpen(false);
2470
+ }}
2471
+ onChatFirstAppOpen={(app) => {
2472
+ if (isDispatchWorkspaceAppId(app.id)) return;
2473
+ setSidebarCollapsed(true);
2474
+ navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)));
2475
+ }}
2316
2476
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2317
2477
  collapsible
2318
2478
  onCollapsedChange={setSidebarCollapsed}
@@ -2345,13 +2505,15 @@ export function Layout({
2345
2505
  ? chatFirstCopy("appsLoadError")
2346
2506
  : null
2347
2507
  }
2348
- chatFirstActiveAppId={
2349
- activeChatFirstSurfaceTab?.kind === "app"
2350
- ? activeChatFirstSurfaceTab.appId
2351
- : undefined
2352
- }
2508
+ chatFirstActiveAppId={chatFirstActiveAppId}
2509
+ chatFirstActivePrimaryTab={chatFirstActivePrimaryTab}
2510
+ onChatFirstNewChat={() => {
2511
+ closeChatFirstSessionWatch();
2512
+ chatFirstSurfaceTabsStore.closeAll();
2513
+ setChatFirstSurfacePanelOpen(false);
2514
+ }}
2353
2515
  onChatFirstAppOpen={(app) =>
2354
- openChatFirstPane({ appId: app.id }, "main")
2516
+ navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)))
2355
2517
  }
2356
2518
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2357
2519
  onNavigate={() => setMobileOpen(false)}
@@ -5,6 +5,8 @@ import { Link, useLocation } from "react-router";
5
5
 
6
6
  import { cn } from "../../lib/utils";
7
7
  import {
8
+ isWorkspaceAppVisibleInDefaultLaunchers,
9
+ workspaceAppIdFromRoute,
8
10
  workspaceAppRoute,
9
11
  workspaceAppHref,
10
12
  type WorkspaceAppSummary,
@@ -12,32 +14,8 @@ import {
12
14
  import { AppIcon } from "../app-icon";
13
15
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
14
16
 
15
- function pathFromValue(value: string | null | undefined): string | null {
16
- const trimmed = value?.trim();
17
- if (!trimmed) return null;
18
-
19
- try {
20
- const pathname = new URL(trimmed, "https://dispatch.local").pathname;
21
- return pathname.replace(/\/+$/, "") || "/";
22
- } catch {
23
- // coercion-ok: malformed app URLs cannot match a workspace route.
24
- return null;
25
- }
26
- }
27
-
28
17
  function appMatchesPath(app: WorkspaceAppSummary, pathname: string): boolean {
29
- const appRoute = workspaceAppRoute(app.id);
30
- if (pathname === appRoute || pathname.startsWith(`${appRoute}/`)) {
31
- return true;
32
- }
33
-
34
- const candidatePaths = [app.path, app.url]
35
- .map(pathFromValue)
36
- .filter((path): path is string => !!path);
37
-
38
- return candidatePaths.some(
39
- (path) => pathname === path || pathname.startsWith(`${path}/`),
40
- );
18
+ return workspaceAppIdFromRoute(pathname) === app.id;
41
19
  }
42
20
 
43
21
  function appLabel(app: WorkspaceAppSummary): string {
@@ -64,7 +42,7 @@ export function WorkspaceAppsRail({
64
42
  const apps = appsQuery.data
65
43
  .filter(
66
44
  (app) =>
67
- !app.isDispatch &&
45
+ isWorkspaceAppVisibleInDefaultLaunchers(app) &&
68
46
  !app.archived &&
69
47
  app.status !== "pending" &&
70
48
  !!workspaceAppHref(app),
@@ -3,6 +3,7 @@ import { useT } from "@agent-native/core/client/i18n";
3
3
  import { filterOtherApps, type ConnectedAppSummary } from "../lib/other-apps";
4
4
  import type { WorkspaceAppId } from "../lib/other-apps";
5
5
  import { cn } from "../lib/utils";
6
+ import { isDefaultWorkspaceAppHiddenId } from "../lib/workspace-apps";
6
7
  import { ActionQueryError } from "./action-query-error";
7
8
  import {
8
9
  APP_LIST_GRID_CLASS,
@@ -52,7 +53,13 @@ export function mergeOtherAppEntries({
52
53
 
53
54
  for (const template of getTemplateItems(templates)) {
54
55
  const id = templateKey(template);
55
- if (!id || template.installed || workspaceAppIds.has(id)) continue;
56
+ if (
57
+ !id ||
58
+ isDefaultWorkspaceAppHiddenId(id) ||
59
+ template.installed ||
60
+ workspaceAppIds.has(id)
61
+ )
62
+ continue;
56
63
  seen.add(id);
57
64
  entries.push({ kind: "template", template });
58
65
  }
@@ -62,21 +62,21 @@ describe("WorkspaceAppCard", () => {
62
62
  });
63
63
 
64
64
  const appLink = container.querySelector<HTMLAnchorElement>(
65
- 'a[href="/apps/analytics"]',
65
+ 'a[href="/analytics"]',
66
66
  );
67
67
  expect(appLink).not.toBeNull();
68
68
  expect(appLink?.textContent).toContain("Open app");
69
- expect(appLink?.className).toContain("bg-accent");
70
- expect(appLink?.getAttribute("target")).toBeNull();
71
- expect(appLink?.getAttribute("rel")).toBeNull();
69
+ expect(appLink?.getAttribute("target")).toBe("_blank");
70
+ expect(appLink?.getAttribute("rel")).toBe("noreferrer");
72
71
  expect(appLink?.querySelector("svg")).toBeNull();
73
- expect(container.textContent).not.toContain("/apps/analytics");
74
72
 
75
73
  expect(
76
74
  container.querySelector(
77
75
  'button[aria-label="Open options for Analytics"]',
78
76
  ),
79
- ).toBeNull();
77
+ ).not.toBeNull();
78
+ expect(document.body.textContent).not.toContain("Open in new tab");
79
+ expect(document.body.textContent).not.toContain("Add app");
80
80
 
81
81
  const settingsButton = container.querySelector<HTMLButtonElement>(
82
82
  'button[aria-label="Settings for Analytics"]',