@agent-native/dispatch 0.23.1 → 0.23.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 (51) hide show
  1. package/dist/actions/provider-api-register.d.ts +4 -4
  2. package/dist/components/app-open-actions.d.ts.map +1 -1
  3. package/dist/components/app-open-actions.js +3 -2
  4. package/dist/components/app-open-actions.js.map +1 -1
  5. package/dist/components/layout/Layout.d.ts +4 -2
  6. package/dist/components/layout/Layout.d.ts.map +1 -1
  7. package/dist/components/layout/Layout.js +83 -12
  8. package/dist/components/layout/Layout.js.map +1 -1
  9. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  10. package/dist/components/layout/workspace-apps-rail.js +2 -22
  11. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  12. package/dist/components/workspace-app-card.d.ts.map +1 -1
  13. package/dist/components/workspace-app-card.js +3 -4
  14. package/dist/components/workspace-app-card.js.map +1 -1
  15. package/dist/components/workspace-template-card.d.ts.map +1 -1
  16. package/dist/components/workspace-template-card.js +1 -1
  17. package/dist/components/workspace-template-card.js.map +1 -1
  18. package/dist/lib/workspace-apps.d.ts +1 -0
  19. package/dist/lib/workspace-apps.d.ts.map +1 -1
  20. package/dist/lib/workspace-apps.js +13 -0
  21. package/dist/lib/workspace-apps.js.map +1 -1
  22. package/dist/routes/index.js +1 -1
  23. package/dist/routes/index.js.map +1 -1
  24. package/dist/routes/pages/agent-integrations.d.ts +5 -0
  25. package/dist/routes/pages/agent-integrations.d.ts.map +1 -0
  26. package/dist/routes/pages/agent-integrations.js +15 -0
  27. package/dist/routes/pages/agent-integrations.js.map +1 -0
  28. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  29. package/dist/routes/pages/apps.$appId.js +14 -2
  30. package/dist/routes/pages/apps.$appId.js.map +1 -1
  31. package/dist/routes/pages/overview.d.ts +2 -2
  32. package/dist/routes/pages/settings.d.ts.map +1 -1
  33. package/dist/routes/pages/settings.js +1 -1
  34. package/dist/routes/pages/settings.js.map +1 -1
  35. package/dist/server/lib/dispatch-integrations.js +2 -2
  36. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  37. package/package.json +2 -2
  38. package/src/components/app-open-actions.tsx +3 -2
  39. package/src/components/layout/Layout.tsx +111 -16
  40. package/src/components/layout/workspace-apps-rail.tsx +2 -25
  41. package/src/components/workspace-app-card.spec.tsx +6 -6
  42. package/src/components/workspace-app-card.tsx +4 -7
  43. package/src/components/workspace-template-card.spec.tsx +4 -16
  44. package/src/components/workspace-template-card.tsx +0 -1
  45. package/src/lib/workspace-apps.spec.ts +21 -0
  46. package/src/lib/workspace-apps.ts +12 -0
  47. package/src/routes/index.ts +1 -1
  48. package/src/routes/pages/agent-integrations.tsx +24 -0
  49. package/src/routes/pages/apps.$appId.tsx +19 -1
  50. package/src/routes/pages/settings.tsx +1 -19
  51. package/src/server/lib/dispatch-integrations.ts +2 -2
@@ -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 {
@@ -104,6 +105,8 @@ import { toast } from "sonner";
104
105
  import { cn } from "../../lib/utils";
105
106
  import {
106
107
  mergeChatFirstWorkspaceApps,
108
+ workspaceAppIdFromRoute,
109
+ workspaceAppRoute,
107
110
  type WorkspaceAppSummary,
108
111
  } from "../../lib/workspace-apps";
109
112
  import { AppIcon } from "../app-icon";
@@ -296,6 +299,31 @@ function localDispatchPath(pathname: string): string {
296
299
  return pathname;
297
300
  }
298
301
 
302
+ function chatFirstPrimaryTabForPath(
303
+ pathname: string,
304
+ ): ChatFirstPrimaryTab | undefined {
305
+ if (pathname === "/chat" || pathname.startsWith("/chat/")) {
306
+ return "new-chat";
307
+ }
308
+ if (
309
+ pathname === "/integrations" ||
310
+ pathname.startsWith("/integrations/") ||
311
+ pathname === "/admin/integrations" ||
312
+ pathname.startsWith("/admin/integrations/")
313
+ ) {
314
+ return "integrations";
315
+ }
316
+ if (
317
+ pathname === "/automations" ||
318
+ pathname.startsWith("/automations/") ||
319
+ pathname === "/admin/automations" ||
320
+ pathname.startsWith("/admin/automations/")
321
+ ) {
322
+ return "scheduled";
323
+ }
324
+ return undefined;
325
+ }
326
+
299
327
  function dispatchNavLinkTarget(path: string): string {
300
328
  if (typeof window === "undefined") return path;
301
329
  const basePath = appBasePath();
@@ -543,6 +571,8 @@ function DispatchChatsSection({
543
571
  chatFirstMode?: boolean;
544
572
  chatFirstEmbedded?: boolean;
545
573
  chatFirstNavigation?: {
574
+ activeTab?: ChatFirstPrimaryTab;
575
+ onNewChat?: () => void;
546
576
  onOpenIntegrations: () => void;
547
577
  onOpenScheduled: () => void;
548
578
  };
@@ -683,13 +713,17 @@ function DispatchChatsSection({
683
713
  )}
684
714
  >
685
715
  {showNewChat && chatFirstNavigation ? (
686
- <nav className="space-y-0.5 px-0 py-2">
716
+ <nav className="space-y-0.5 px-2 py-2">
687
717
  <ChatFirstPrimaryNavigation
688
718
  copy={chatFirstCopy}
689
- onNewChat={() => void handleNewChat()}
719
+ onNewChat={() => {
720
+ chatFirstNavigation.onNewChat?.();
721
+ void handleNewChat();
722
+ }}
690
723
  onOpenIntegrations={chatFirstNavigation.onOpenIntegrations}
691
724
  onOpenScheduled={chatFirstNavigation.onOpenScheduled}
692
725
  onSearch={openCommandMenu}
726
+ activeTab={chatFirstNavigation.activeTab}
693
727
  />
694
728
  </nav>
695
729
  ) : null}
@@ -816,7 +850,7 @@ function DispatchChatsSection({
816
850
  ) : null}
817
851
  </>
818
852
  )}
819
- className="min-w-0"
853
+ className="min-w-0 px-2"
820
854
  />
821
855
  ) : (
822
856
  <ChatHistoryRail
@@ -883,7 +917,7 @@ function DispatchChatsSection({
883
917
  ) : null}
884
918
  </>
885
919
  )}
886
- className="min-w-0"
920
+ className={cn("min-w-0", chatFirstMode && "px-2")}
887
921
  />
888
922
  )}
889
923
  </div>
@@ -904,6 +938,8 @@ export function NavContent({
904
938
  chatFirstAppsLoading = false,
905
939
  chatFirstAppsError,
906
940
  chatFirstActiveAppId,
941
+ chatFirstActivePrimaryTab,
942
+ onChatFirstNewChat,
907
943
  onChatFirstAppOpen,
908
944
  onChatFirstAppsRetry,
909
945
  }: {
@@ -920,6 +956,8 @@ export function NavContent({
920
956
  chatFirstAppsLoading?: boolean;
921
957
  chatFirstAppsError?: string | null;
922
958
  chatFirstActiveAppId?: string;
959
+ chatFirstActivePrimaryTab?: ChatFirstPrimaryTab;
960
+ onChatFirstNewChat?: () => void;
923
961
  onChatFirstAppOpen?: (app: ChatFirstAppItem) => void;
924
962
  onChatFirstAppsRetry?: () => void;
925
963
  }) {
@@ -1219,6 +1257,8 @@ export function NavContent({
1219
1257
  chatFirstMode
1220
1258
  chatFirstEmbedded={chatFirstEmbedded}
1221
1259
  chatFirstNavigation={{
1260
+ activeTab: chatFirstActivePrimaryTab,
1261
+ onNewChat: onChatFirstNewChat,
1222
1262
  onOpenIntegrations: () => {
1223
1263
  navigate(dispatchNavLinkTarget("/admin/integrations"));
1224
1264
  onNavigate?.();
@@ -1307,6 +1347,12 @@ export function Layout({
1307
1347
  const chatFirstMode =
1308
1348
  extensions?.chatFirst === true || chatFirstPreference || chatFirstEmbedded;
1309
1349
  const chatFirstHasActiveChat = chatFirstSurfaceScope !== "new";
1350
+ useEffect(() => {
1351
+ if (!chatFirstMode || chatFirstEmbedded || localPathname !== "/overview") {
1352
+ return;
1353
+ }
1354
+ navigate(dispatchNavLinkTarget("/chat"), { replace: true });
1355
+ }, [chatFirstEmbedded, chatFirstMode, localPathname, navigate]);
1310
1356
  const [chatFirstAppLayout, setChatFirstAppLayout] =
1311
1357
  useState<ChatFirstAppLayoutPreference>(() => readChatFirstAppLayout());
1312
1358
  const chatFirstAppLayoutHydratedRef = useRef(false);
@@ -1371,6 +1417,7 @@ export function Layout({
1371
1417
  null,
1372
1418
  );
1373
1419
  const [chatFirstEmbedAttempt, setChatFirstEmbedAttempt] = useState(0);
1420
+ const chatFirstAppFrameRef = useRef<HTMLIFrameElement>(null);
1374
1421
  const [chatFirstNotice, setChatFirstNotice] = useState<string | null>(null);
1375
1422
  const chatFirstSessionWatch = useChatFirstSessionWatch();
1376
1423
  const chatFirstSurfaceTabs = useChatFirstSurfaceTabs(chatFirstSurfaceScope);
@@ -1410,6 +1457,19 @@ export function Layout({
1410
1457
  isChatRoute &&
1411
1458
  activeChatFirstSurfaceTab?.kind === "app" &&
1412
1459
  activeChatFirstSurfaceTab.placement === "main";
1460
+ const chatFirstAppSelected =
1461
+ chatFirstMode && activeChatFirstSurfaceTab?.kind === "app";
1462
+ const chatFirstActivePrimaryTab = chatFirstAppSelected
1463
+ ? activeChatFirstSurfaceTab?.kind === "app" &&
1464
+ activeChatFirstSurfaceTab.appId === "dispatch"
1465
+ ? chatFirstPrimaryTabForPath(activeChatFirstSurfaceTab.path ?? "")
1466
+ : undefined
1467
+ : chatFirstPrimaryTabForPath(localPathname);
1468
+ const chatFirstActiveAppId = isWorkspaceAppRoute
1469
+ ? (workspaceAppIdFromRoute(localPathname) ?? undefined)
1470
+ : chatFirstAppSelected && activeChatFirstSurfaceTab?.kind === "app"
1471
+ ? activeChatFirstSurfaceTab.appId
1472
+ : undefined;
1413
1473
  const activeChatFirstAppRegistration = useMemo(
1414
1474
  () =>
1415
1475
  activeChatFirstSurfaceTab?.kind === "app" &&
@@ -1496,6 +1556,25 @@ export function Layout({
1496
1556
  createChatFirstEmbedSession.mutateAsync,
1497
1557
  isChatRoute,
1498
1558
  ]);
1559
+
1560
+ useEffect(() => {
1561
+ if (!chatFirstMode || !isChatRoute || !activeChatFirstApp) return;
1562
+
1563
+ const handleEmbedSessionExpired = (event: MessageEvent) => {
1564
+ if (
1565
+ event.data?.type !== "agentNative.embedSessionExpired" ||
1566
+ event.source !== chatFirstAppFrameRef.current?.contentWindow
1567
+ ) {
1568
+ return;
1569
+ }
1570
+ setChatFirstEmbedAttempt((attempt) => attempt + 1);
1571
+ };
1572
+
1573
+ window.addEventListener("message", handleEmbedSessionExpired);
1574
+ return () =>
1575
+ window.removeEventListener("message", handleEmbedSessionExpired);
1576
+ }, [activeChatFirstApp, chatFirstMode, isChatRoute]);
1577
+
1499
1578
  const persistChatFirstPane = useCallback(
1500
1579
  (pane: DispatchChatFirstPane | null) => {
1501
1580
  setChatFirstPane(pane);
@@ -1507,6 +1586,17 @@ export function Layout({
1507
1586
  },
1508
1587
  [],
1509
1588
  );
1589
+ useEffect(() => {
1590
+ if (!chatFirstMode || isChatRoute) return;
1591
+ persistChatFirstPane(null);
1592
+ closeChatFirstSessionWatch();
1593
+ chatFirstSurfaceTabsStore.closeAll();
1594
+ }, [
1595
+ chatFirstMode,
1596
+ chatFirstSurfaceTabsStore,
1597
+ isChatRoute,
1598
+ persistChatFirstPane,
1599
+ ]);
1510
1600
  const openChatFirstPane = useCallback(
1511
1601
  (
1512
1602
  pane: DispatchChatFirstPane,
@@ -2023,6 +2113,7 @@ export function Layout({
2023
2113
  data-dispatch-chat-first-app-frame
2024
2114
  src={url}
2025
2115
  title={title ?? chatFirstCopy("appUnavailable")}
2116
+ ref={chatFirstAppFrameRef}
2026
2117
  referrerPolicy="no-referrer"
2027
2118
  allow="clipboard-read; clipboard-write"
2028
2119
  className="h-full w-full border-0 bg-background"
@@ -2305,13 +2396,15 @@ export function Layout({
2305
2396
  ? chatFirstCopy("appsLoadError")
2306
2397
  : null
2307
2398
  }
2308
- chatFirstActiveAppId={
2309
- activeChatFirstSurfaceTab?.kind === "app"
2310
- ? activeChatFirstSurfaceTab.appId
2311
- : undefined
2312
- }
2399
+ chatFirstActiveAppId={chatFirstActiveAppId}
2400
+ chatFirstActivePrimaryTab={chatFirstActivePrimaryTab}
2401
+ onChatFirstNewChat={() => {
2402
+ closeChatFirstSessionWatch();
2403
+ chatFirstSurfaceTabsStore.closeAll();
2404
+ setChatFirstSurfacePanelOpen(false);
2405
+ }}
2313
2406
  onChatFirstAppOpen={(app) =>
2314
- openChatFirstPane({ appId: app.id }, "main")
2407
+ navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)))
2315
2408
  }
2316
2409
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2317
2410
  collapsible
@@ -2345,13 +2438,15 @@ export function Layout({
2345
2438
  ? chatFirstCopy("appsLoadError")
2346
2439
  : null
2347
2440
  }
2348
- chatFirstActiveAppId={
2349
- activeChatFirstSurfaceTab?.kind === "app"
2350
- ? activeChatFirstSurfaceTab.appId
2351
- : undefined
2352
- }
2441
+ chatFirstActiveAppId={chatFirstActiveAppId}
2442
+ chatFirstActivePrimaryTab={chatFirstActivePrimaryTab}
2443
+ onChatFirstNewChat={() => {
2444
+ closeChatFirstSessionWatch();
2445
+ chatFirstSurfaceTabsStore.closeAll();
2446
+ setChatFirstSurfacePanelOpen(false);
2447
+ }}
2353
2448
  onChatFirstAppOpen={(app) =>
2354
- openChatFirstPane({ appId: app.id }, "main")
2449
+ navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)))
2355
2450
  }
2356
2451
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2357
2452
  onNavigate={() => setMobileOpen(false)}
@@ -5,6 +5,7 @@ import { Link, useLocation } from "react-router";
5
5
 
6
6
  import { cn } from "../../lib/utils";
7
7
  import {
8
+ workspaceAppIdFromRoute,
8
9
  workspaceAppRoute,
9
10
  workspaceAppHref,
10
11
  type WorkspaceAppSummary,
@@ -12,32 +13,8 @@ import {
12
13
  import { AppIcon } from "../app-icon";
13
14
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
14
15
 
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
16
  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
- );
17
+ return workspaceAppIdFromRoute(pathname) === app.id;
41
18
  }
42
19
 
43
20
  function appLabel(app: WorkspaceAppSummary): string {
@@ -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"]',
@@ -25,7 +25,6 @@ import { toast } from "sonner";
25
25
 
26
26
  import { cn } from "../lib/utils";
27
27
  import {
28
- isPendingBuilderHref,
29
28
  workspaceAppHref,
30
29
  workspaceAppRoute,
31
30
  type WorkspaceAppSummary,
@@ -276,7 +275,6 @@ function WorkspaceAppOpenActions({
276
275
  href: string | null;
277
276
  }) {
278
277
  const navigate = useNavigate();
279
- const openInNewTab = isPendingBuilderHref(app);
280
278
  const appRoute = workspaceAppRoute(app.id);
281
279
 
282
280
  if (!href) {
@@ -290,14 +288,13 @@ function WorkspaceAppOpenActions({
290
288
  return (
291
289
  <AppOpenActions
292
290
  name={app.name}
293
- href={openInNewTab ? href : appRoute}
294
- target={openInNewTab ? "_blank" : undefined}
295
- rel={openInNewTab ? "noreferrer" : undefined}
296
- showInlineOption={openInNewTab}
291
+ href={href}
292
+ target="_blank"
293
+ rel="noreferrer"
294
+ showInlineOption
297
295
  onOpenInline={() => {
298
296
  navigate(appRoute);
299
297
  }}
300
- showNewTabOption={openInNewTab}
301
298
  />
302
299
  );
303
300
  }
@@ -153,7 +153,7 @@ describe("WorkspaceTemplateCard", () => {
153
153
  );
154
154
  });
155
155
 
156
- it("keeps add app out of the catalog split-button menu", async () => {
156
+ it("keeps add app available in catalog cards", async () => {
157
157
  await act(async () => {
158
158
  root.render(<WorkspaceTemplateCard template={template} catalog />);
159
159
  });
@@ -162,23 +162,11 @@ describe("WorkspaceTemplateCard", () => {
162
162
  (button) => button.textContent?.includes("Add app"),
163
163
  );
164
164
  expect(addButton).toBeDefined();
165
-
166
- const optionsButton = container.querySelector<HTMLButtonElement>(
167
- 'button[aria-label="Open options for Weekly report"]',
168
- );
169
- expect(optionsButton).not.toBeNull();
170
- await act(async () => {
171
- optionsButton?.dispatchEvent(
172
- new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
173
- );
174
- });
175
-
176
- const addItem = Array.from(
177
- document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
178
- ).find((item) => item.textContent?.includes("Add app"));
179
- expect(addItem).toBeUndefined();
180
165
  await act(async () => addButton?.click());
181
166
  expect(document.body.textContent).toContain("Choose the URL-safe id");
167
+ expect(
168
+ container.querySelector('a[href="https://reports.example.test"]'),
169
+ ).not.toBeNull();
182
170
  });
183
171
 
184
172
  it("accepts the list action envelope and renders installed state", async () => {
@@ -222,7 +222,6 @@ export function WorkspaceTemplateCard({
222
222
  target="_blank"
223
223
  rel="noreferrer"
224
224
  labels={{ openApp: labels.openApp }}
225
- showNewTabOption
226
225
  />
227
226
  ) : null}
228
227
  <Button
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { workspaceAppIdFromRoute, workspaceAppRoute } from "./workspace-apps";
4
+
5
+ describe("workspace app routes", () => {
6
+ it("round-trips encoded app ids", () => {
7
+ const route = workspaceAppRoute("sales ops");
8
+ expect(route).toBe("/apps/sales%20ops");
9
+ expect(workspaceAppIdFromRoute(route)).toBe("sales ops");
10
+ });
11
+
12
+ it("does not mark app paths or the apps index as an active app route", () => {
13
+ expect(workspaceAppIdFromRoute("/sales-ops")).toBeNull();
14
+ expect(workspaceAppIdFromRoute("/apps")).toBeNull();
15
+ expect(workspaceAppIdFromRoute("/overview")).toBeNull();
16
+ });
17
+
18
+ it("accepts nested app routes while preserving the app id", () => {
19
+ expect(workspaceAppIdFromRoute("/apps/mail/inbox")).toBe("mail");
20
+ });
21
+ });
@@ -31,6 +31,18 @@ export function workspaceAppRoute(appId: string): string {
31
31
  return `/apps/${encodeURIComponent(appId)}`;
32
32
  }
33
33
 
34
+ export function workspaceAppIdFromRoute(pathname: string): string | null {
35
+ const match = pathname.match(/^\/apps\/([^/]+)(?:\/|$)/);
36
+ if (!match) return null;
37
+ try {
38
+ const appId = decodeURIComponent(match[1]).trim();
39
+ return appId || null;
40
+ } catch {
41
+ // coercion-ok: malformed app routes are inactive, not app ids.
42
+ return null;
43
+ }
44
+ }
45
+
34
46
  export function workspaceAppHref(app: WorkspaceAppSummary): string | null {
35
47
  if (app.status === "pending") {
36
48
  return app.builderUrl
@@ -44,7 +44,7 @@ export const dispatchRoutes: RouteConfig = [
44
44
  route("automations", "./pages/automations.js"),
45
45
  route("approvals", "./pages/approvals.js"),
46
46
  route("destinations", "./pages/destinations.js"),
47
- route("integrations", "./pages/integrations.js"),
47
+ route("integrations", "./pages/agent-integrations.js"),
48
48
  route("transactional-email", "./pages/transactional-email.js"),
49
49
  route(
50
50
  "transactional-email/:appId/:id",
@@ -0,0 +1,24 @@
1
+ import { useAgentSettingsTabs } from "@agent-native/core/client/settings";
2
+
3
+ import { DispatchShell } from "../../components/dispatch-shell";
4
+
5
+ export function meta() {
6
+ return [{ title: "Integrations — Dispatch" }];
7
+ }
8
+
9
+ export default function AgentIntegrationsRoute() {
10
+ const settingsTabs = useAgentSettingsTabs();
11
+ const integrationsTab = settingsTabs.find((tab) => tab.id === "integrations");
12
+ if (!integrationsTab) {
13
+ throw new Error("The shared Integrations settings view is unavailable.");
14
+ }
15
+
16
+ return (
17
+ <DispatchShell
18
+ title="Integrations"
19
+ description="Connect the tools and services available to your agent."
20
+ >
21
+ <div className="mx-auto w-full max-w-5xl">{integrationsTab.content}</div>
22
+ </DispatchShell>
23
+ );
24
+ }
@@ -5,7 +5,7 @@ import {
5
5
  import { useT } from "@agent-native/core/client/i18n";
6
6
  import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
7
7
  import { IconArrowLeft, IconClockHour4 } from "@tabler/icons-react";
8
- import { useEffect, useMemo, useState } from "react";
8
+ import { useEffect, useMemo, useRef, useState } from "react";
9
9
  import { Link, useParams } from "react-router";
10
10
 
11
11
  import { ActionQueryError } from "../../components/action-query-error";
@@ -52,6 +52,7 @@ export default function WorkspaceAppRoute() {
52
52
  const [embedUrl, setEmbedUrl] = useState<string | null>(null);
53
53
  const [embedError, setEmbedError] = useState<Error | null>(null);
54
54
  const [embedAttempt, setEmbedAttempt] = useState(0);
55
+ const embedFrameRef = useRef<HTMLIFrameElement>(null);
55
56
  const createEmbedSession = useActionMutation<
56
57
  EmbedSessionResult,
57
58
  EmbedSessionInput
@@ -96,6 +97,22 @@ export default function WorkspaceAppRoute() {
96
97
  embedInput,
97
98
  ]);
98
99
 
100
+ useEffect(() => {
101
+ const handleEmbedSessionExpired = (event: MessageEvent) => {
102
+ if (
103
+ event.data?.type !== "agentNative.embedSessionExpired" ||
104
+ event.source !== embedFrameRef.current?.contentWindow
105
+ ) {
106
+ return;
107
+ }
108
+ setEmbedAttempt((attempt) => attempt + 1);
109
+ };
110
+
111
+ window.addEventListener("message", handleEmbedSessionExpired);
112
+ return () =>
113
+ window.removeEventListener("message", handleEmbedSessionExpired);
114
+ }, []);
115
+
99
116
  if (appsQuery.isError) {
100
117
  return (
101
118
  <div className="flex h-full min-h-0 items-center justify-center p-6">
@@ -208,6 +225,7 @@ export default function WorkspaceAppRoute() {
208
225
  data-dispatch-workspace-app-frame
209
226
  src={embedUrl}
210
227
  title={app.name}
228
+ ref={embedFrameRef}
211
229
  referrerPolicy="no-referrer"
212
230
  className="h-full w-full border-0 bg-background"
213
231
  />
@@ -68,7 +68,7 @@ export default function SettingsRoute() {
68
68
  description={t("settings.description")}
69
69
  >
70
70
  <SettingsTabsPage
71
- extraTabs={agentSettingsTabs}
71
+ extraTabs={agentSettingsTabs.filter((tab) => tab.id !== "integrations")}
72
72
  general={
73
73
  <div className="mx-auto w-full max-w-3xl space-y-6">
74
74
  <Card>
@@ -141,24 +141,6 @@ export default function SettingsRoute() {
141
141
  </CardContent>
142
142
  </Card>
143
143
 
144
- <Card>
145
- <CardHeader>
146
- <CardTitle className="text-base">
147
- {t("settings.automationsTitle")}
148
- </CardTitle>
149
- <CardDescription>
150
- {t("settings.automationsDescription")}
151
- </CardDescription>
152
- </CardHeader>
153
- <CardContent>
154
- <Button variant="outline" asChild>
155
- <Link to="/admin/automations">
156
- {t("settings.openAutomations")}
157
- </Link>
158
- </Button>
159
- </CardContent>
160
- </Card>
161
-
162
144
  <Card>
163
145
  <CardHeader>
164
146
  <CardTitle className="text-base">
@@ -433,8 +433,8 @@ export async function resolveDispatchExecutionContext(
433
433
  }
434
434
  if (!scope) throw new Error("Slack channel is not enabled for Agent Native");
435
435
  const decision = evaluateIntegrationScopePolicy(scope, {
436
- // Thread replies reached this point only after the adapter's active-task
437
- // gate, so they are continuation steering rather than ambient chatter.
436
+ // Slack channel turns only arrive here after an explicit current-message
437
+ // mention; keep the generic thread_reply trigger for other adapters.
438
438
  mentioned:
439
439
  incoming.triggerKind === "mention" ||
440
440
  incoming.triggerKind === "thread_reply",