@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.
- package/dist/actions/provider-api-register.d.ts +4 -4
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +3 -2
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +4 -2
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +83 -12
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
- package/dist/components/layout/workspace-apps-rail.js +2 -22
- package/dist/components/layout/workspace-apps-rail.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +3 -4
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +1 -1
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +13 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/index.js +1 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/agent-integrations.d.ts +5 -0
- package/dist/routes/pages/agent-integrations.d.ts.map +1 -0
- package/dist/routes/pages/agent-integrations.js +15 -0
- package/dist/routes/pages/agent-integrations.js.map +1 -0
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +14 -2
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/overview.d.ts +2 -2
- package/dist/routes/pages/settings.d.ts.map +1 -1
- package/dist/routes/pages/settings.js +1 -1
- package/dist/routes/pages/settings.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +2 -2
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/components/app-open-actions.tsx +3 -2
- package/src/components/layout/Layout.tsx +111 -16
- package/src/components/layout/workspace-apps-rail.tsx +2 -25
- package/src/components/workspace-app-card.spec.tsx +6 -6
- package/src/components/workspace-app-card.tsx +4 -7
- package/src/components/workspace-template-card.spec.tsx +4 -16
- package/src/components/workspace-template-card.tsx +0 -1
- package/src/lib/workspace-apps.spec.ts +21 -0
- package/src/lib/workspace-apps.ts +12 -0
- package/src/routes/index.ts +1 -1
- package/src/routes/pages/agent-integrations.tsx +24 -0
- package/src/routes/pages/apps.$appId.tsx +19 -1
- package/src/routes/pages/settings.tsx +1 -19
- 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-
|
|
716
|
+
<nav className="space-y-0.5 px-2 py-2">
|
|
687
717
|
<ChatFirstPrimaryNavigation
|
|
688
718
|
copy={chatFirstCopy}
|
|
689
|
-
onNewChat={() =>
|
|
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
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2399
|
+
chatFirstActiveAppId={chatFirstActiveAppId}
|
|
2400
|
+
chatFirstActivePrimaryTab={chatFirstActivePrimaryTab}
|
|
2401
|
+
onChatFirstNewChat={() => {
|
|
2402
|
+
closeChatFirstSessionWatch();
|
|
2403
|
+
chatFirstSurfaceTabsStore.closeAll();
|
|
2404
|
+
setChatFirstSurfacePanelOpen(false);
|
|
2405
|
+
}}
|
|
2313
2406
|
onChatFirstAppOpen={(app) =>
|
|
2314
|
-
|
|
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
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2441
|
+
chatFirstActiveAppId={chatFirstActiveAppId}
|
|
2442
|
+
chatFirstActivePrimaryTab={chatFirstActivePrimaryTab}
|
|
2443
|
+
onChatFirstNewChat={() => {
|
|
2444
|
+
closeChatFirstSessionWatch();
|
|
2445
|
+
chatFirstSurfaceTabsStore.closeAll();
|
|
2446
|
+
setChatFirstSurfacePanelOpen(false);
|
|
2447
|
+
}}
|
|
2353
2448
|
onChatFirstAppOpen={(app) =>
|
|
2354
|
-
|
|
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
|
-
|
|
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="/
|
|
65
|
+
'a[href="/analytics"]',
|
|
66
66
|
);
|
|
67
67
|
expect(appLink).not.toBeNull();
|
|
68
68
|
expect(appLink?.textContent).toContain("Open app");
|
|
69
|
-
expect(appLink?.
|
|
70
|
-
expect(appLink?.getAttribute("
|
|
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={
|
|
294
|
-
target=
|
|
295
|
-
rel=
|
|
296
|
-
showInlineOption
|
|
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
|
|
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 () => {
|
|
@@ -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
|
package/src/routes/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
437
|
-
//
|
|
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",
|