@agent-native/dispatch 0.23.0 → 0.23.1
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 +1 -1
- package/dist/components/app-icon.d.ts.map +1 -1
- package/dist/components/app-icon.js +50 -30
- package/dist/components/app-icon.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +60 -17
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/routes/pages/chat.js +1 -1
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +14 -1
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/package.json +3 -3
- package/src/components/app-icon.tsx +68 -44
- package/src/components/dispatch-control-plane.spec.tsx +2 -0
- package/src/components/layout/Layout.spec.tsx +8 -1
- package/src/components/layout/Layout.tsx +82 -19
- package/src/routes/pages/chat.tsx +1 -1
- package/src/server/lib/mcp-gateway.spec.ts +64 -2
- package/src/server/lib/mcp-gateway.ts +19 -1
- package/src/styles/dispatch-css.spec.ts +11 -0
- package/src/styles/dispatch.css +1 -1
|
@@ -392,6 +392,7 @@ describe("Dispatch NavContent", () => {
|
|
|
392
392
|
<TooltipProvider>
|
|
393
393
|
<NavContent
|
|
394
394
|
chatFirstMode
|
|
395
|
+
collapsible
|
|
395
396
|
chatFirstApps={[{ id: "mail", name: "Mail" }]}
|
|
396
397
|
/>
|
|
397
398
|
</TooltipProvider>
|
|
@@ -403,7 +404,13 @@ describe("Dispatch NavContent", () => {
|
|
|
403
404
|
expect(container.querySelector("[data-chat-first-app]")).not.toBeNull();
|
|
404
405
|
expect(
|
|
405
406
|
container.querySelector("[data-chat-first-app] span[style]"),
|
|
406
|
-
).toBeNull();
|
|
407
|
+
).not.toBeNull();
|
|
408
|
+
expect(
|
|
409
|
+
container.querySelector("[data-sidebar-footer-feedback]"),
|
|
410
|
+
).not.toBeNull();
|
|
411
|
+
expect(
|
|
412
|
+
container.querySelector("[data-sidebar-footer-collapse]"),
|
|
413
|
+
).not.toBeNull();
|
|
407
414
|
});
|
|
408
415
|
|
|
409
416
|
it("opens a workspace app in the main app route from the left rail", async () => {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type ChatFirstAppRegistration,
|
|
24
24
|
type ChatFirstAppLayoutPreference,
|
|
25
25
|
type ChatFirstAppResolution,
|
|
26
|
+
type ChatFirstAppSurfacePlacement,
|
|
26
27
|
type ChatFirstOpenBrowserDetail,
|
|
27
28
|
type ChatFirstOpenAppDetail,
|
|
28
29
|
type ChatFirstSessionReference,
|
|
@@ -199,6 +200,7 @@ const CHAT_FIRST_PANE_STATE_KEY = "chat-first-pane";
|
|
|
199
200
|
|
|
200
201
|
interface DispatchChatFirstPane {
|
|
201
202
|
appId: string;
|
|
203
|
+
placement?: ChatFirstAppSurfacePlacement;
|
|
202
204
|
path?: string;
|
|
203
205
|
view?: string;
|
|
204
206
|
}
|
|
@@ -461,6 +463,9 @@ function persistedChatFirstPane(value: unknown): DispatchChatFirstPane | null {
|
|
|
461
463
|
if (typeof record.appId !== "string" || !record.appId.trim()) return null;
|
|
462
464
|
return {
|
|
463
465
|
appId: record.appId,
|
|
466
|
+
...(record.placement === "main" || record.placement === "side"
|
|
467
|
+
? { placement: record.placement }
|
|
468
|
+
: {}),
|
|
464
469
|
...(typeof record.path === "string" ? { path: record.path } : {}),
|
|
465
470
|
...(typeof record.view === "string" ? { view: record.view } : {}),
|
|
466
471
|
};
|
|
@@ -678,7 +683,7 @@ function DispatchChatsSection({
|
|
|
678
683
|
)}
|
|
679
684
|
>
|
|
680
685
|
{showNewChat && chatFirstNavigation ? (
|
|
681
|
-
<nav className="space-y-0.5 px-
|
|
686
|
+
<nav className="space-y-0.5 px-0 py-2">
|
|
682
687
|
<ChatFirstPrimaryNavigation
|
|
683
688
|
copy={chatFirstCopy}
|
|
684
689
|
onNewChat={() => void handleNewChat()}
|
|
@@ -1025,8 +1030,7 @@ export function NavContent({
|
|
|
1025
1030
|
id={app.id}
|
|
1026
1031
|
name={app.name}
|
|
1027
1032
|
size="sm"
|
|
1028
|
-
|
|
1029
|
-
className="size-5 rounded-md"
|
|
1033
|
+
className="size-7 rounded-lg"
|
|
1030
1034
|
/>
|
|
1031
1035
|
)}
|
|
1032
1036
|
copy={chatFirstCopy}
|
|
@@ -1257,7 +1261,7 @@ export function NavContent({
|
|
|
1257
1261
|
<div className="mt-auto shrink-0">
|
|
1258
1262
|
{bottomNavigation}
|
|
1259
1263
|
{organizationPicker}
|
|
1260
|
-
{
|
|
1264
|
+
{sidebarFooterActions}
|
|
1261
1265
|
</div>
|
|
1262
1266
|
) : null}
|
|
1263
1267
|
</>
|
|
@@ -1302,6 +1306,7 @@ export function Layout({
|
|
|
1302
1306
|
new URLSearchParams(window.location.search).get("chatFirst") === "1";
|
|
1303
1307
|
const chatFirstMode =
|
|
1304
1308
|
extensions?.chatFirst === true || chatFirstPreference || chatFirstEmbedded;
|
|
1309
|
+
const chatFirstHasActiveChat = chatFirstSurfaceScope !== "new";
|
|
1305
1310
|
const [chatFirstAppLayout, setChatFirstAppLayout] =
|
|
1306
1311
|
useState<ChatFirstAppLayoutPreference>(() => readChatFirstAppLayout());
|
|
1307
1312
|
const chatFirstAppLayoutHydratedRef = useRef(false);
|
|
@@ -1344,11 +1349,13 @@ export function Layout({
|
|
|
1344
1349
|
}, [chatFirstGrantedAppsQuery.data?.apps, chatFirstWorkspaceApps]);
|
|
1345
1350
|
const chatFirstAppItems = useMemo<ChatFirstAppItem[]>(
|
|
1346
1351
|
() =>
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
+
chatFirstAppRegistrations
|
|
1353
|
+
.filter((app) => app.enabled)
|
|
1354
|
+
.map((app) => ({
|
|
1355
|
+
id: app.id,
|
|
1356
|
+
name: app.name ?? app.id,
|
|
1357
|
+
})),
|
|
1358
|
+
[chatFirstAppRegistrations],
|
|
1352
1359
|
);
|
|
1353
1360
|
const chatFirstCopy = useMemo(() => createDispatchChatFirstCopy(t), [t]);
|
|
1354
1361
|
const createChatFirstEmbedSession = useActionMutation<
|
|
@@ -1399,7 +1406,10 @@ export function Layout({
|
|
|
1399
1406
|
[chatFirstSurfaceTabs],
|
|
1400
1407
|
);
|
|
1401
1408
|
const chatFirstAppTakesMain =
|
|
1402
|
-
chatFirstMode &&
|
|
1409
|
+
chatFirstMode &&
|
|
1410
|
+
isChatRoute &&
|
|
1411
|
+
activeChatFirstSurfaceTab?.kind === "app" &&
|
|
1412
|
+
activeChatFirstSurfaceTab.placement === "main";
|
|
1403
1413
|
const activeChatFirstAppRegistration = useMemo(
|
|
1404
1414
|
() =>
|
|
1405
1415
|
activeChatFirstSurfaceTab?.kind === "app" &&
|
|
@@ -1498,7 +1508,11 @@ export function Layout({
|
|
|
1498
1508
|
[],
|
|
1499
1509
|
);
|
|
1500
1510
|
const openChatFirstPane = useCallback(
|
|
1501
|
-
(
|
|
1511
|
+
(
|
|
1512
|
+
pane: DispatchChatFirstPane,
|
|
1513
|
+
placement: ChatFirstAppSurfacePlacement = "side",
|
|
1514
|
+
) => {
|
|
1515
|
+
if (placement === "side" && !chatFirstHasActiveChat) return;
|
|
1502
1516
|
setChatFirstNotice(null);
|
|
1503
1517
|
closeChatFirstSessionWatch();
|
|
1504
1518
|
if (!isChatRoute) {
|
|
@@ -1518,15 +1532,17 @@ export function Layout({
|
|
|
1518
1532
|
kind: "app",
|
|
1519
1533
|
title: app?.name ?? pane.appId,
|
|
1520
1534
|
appId: pane.appId,
|
|
1535
|
+
placement,
|
|
1521
1536
|
...(pane.path ? { path: pane.path } : {}),
|
|
1522
1537
|
...(pane.view ? { view: pane.view } : {}),
|
|
1523
1538
|
});
|
|
1524
1539
|
setChatFirstSurfacePanelOpen(true);
|
|
1525
|
-
persistChatFirstPane(pane);
|
|
1540
|
+
persistChatFirstPane({ ...pane, placement });
|
|
1526
1541
|
},
|
|
1527
1542
|
[
|
|
1528
1543
|
chatFirstAppRegistrations,
|
|
1529
1544
|
chatFirstSurfaceTabsStore,
|
|
1545
|
+
chatFirstHasActiveChat,
|
|
1530
1546
|
persistChatFirstPane,
|
|
1531
1547
|
isChatRoute,
|
|
1532
1548
|
navigate,
|
|
@@ -1535,6 +1551,17 @@ export function Layout({
|
|
|
1535
1551
|
);
|
|
1536
1552
|
const openChatFirstSurface = useCallback(
|
|
1537
1553
|
(kind: ChatFirstSurfaceKind) => {
|
|
1554
|
+
if (kind === "browser") {
|
|
1555
|
+
persistChatFirstPane(null);
|
|
1556
|
+
closeChatFirstSessionWatch();
|
|
1557
|
+
chatFirstSurfaceTabsStore.open({
|
|
1558
|
+
id: chatFirstSurfaceTabId("browser", "homepage"),
|
|
1559
|
+
kind: "browser",
|
|
1560
|
+
title: "Browser",
|
|
1561
|
+
url: "https://www.google.com/",
|
|
1562
|
+
});
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1538
1565
|
if (kind !== "agents") return;
|
|
1539
1566
|
persistChatFirstPane(null);
|
|
1540
1567
|
closeChatFirstSessionWatch();
|
|
@@ -1555,6 +1582,24 @@ export function Layout({
|
|
|
1555
1582
|
);
|
|
1556
1583
|
return;
|
|
1557
1584
|
}
|
|
1585
|
+
if (resolution.target.openExternally && typeof window !== "undefined") {
|
|
1586
|
+
try {
|
|
1587
|
+
// Builder's Visual Editor rejects iframe ancestors with CSP/X-Frame-
|
|
1588
|
+
// Options. Prefer the real browser tab; the browser pane below is a
|
|
1589
|
+
// visible fallback when popup policy blocks this non-click event.
|
|
1590
|
+
if (
|
|
1591
|
+
window.open(resolution.target.url, "_blank", "noopener,noreferrer")
|
|
1592
|
+
) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
console.warn(
|
|
1597
|
+
"[chat-first] external browser open failed; keeping link fallback",
|
|
1598
|
+
error,
|
|
1599
|
+
);
|
|
1600
|
+
// Fall through to the non-iframe browser pane below.
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1558
1603
|
persistChatFirstPane(null);
|
|
1559
1604
|
closeChatFirstSessionWatch();
|
|
1560
1605
|
setChatFirstNotice(null);
|
|
@@ -1731,7 +1776,9 @@ export function Layout({
|
|
|
1731
1776
|
: window.location.origin,
|
|
1732
1777
|
},
|
|
1733
1778
|
);
|
|
1734
|
-
if (resolution.status === "ready")
|
|
1779
|
+
if (resolution.status === "ready") {
|
|
1780
|
+
openChatFirstPane(resolution.target, persisted.placement);
|
|
1781
|
+
}
|
|
1735
1782
|
})
|
|
1736
1783
|
.catch(() => {
|
|
1737
1784
|
if (active) {
|
|
@@ -1777,7 +1824,7 @@ export function Layout({
|
|
|
1777
1824
|
useEffect(() => {
|
|
1778
1825
|
const tabCount = chatFirstSurfaceTabs.tabs.length;
|
|
1779
1826
|
const previousTabCount = previousChatFirstSurfaceTabCountRef.current;
|
|
1780
|
-
if (!chatFirstMode) {
|
|
1827
|
+
if (!chatFirstMode || !chatFirstHasActiveChat) {
|
|
1781
1828
|
setChatFirstSurfacePanelOpen(false);
|
|
1782
1829
|
} else if (
|
|
1783
1830
|
tabCount > 0 &&
|
|
@@ -1794,6 +1841,7 @@ export function Layout({
|
|
|
1794
1841
|
previousChatFirstSurfaceTabCountRef.current = tabCount;
|
|
1795
1842
|
}, [
|
|
1796
1843
|
chatFirstMode,
|
|
1844
|
+
chatFirstHasActiveChat,
|
|
1797
1845
|
setChatFirstSurfacePanelOpen,
|
|
1798
1846
|
chatFirstSurfaceTabs.tabs.length,
|
|
1799
1847
|
isChatRoute,
|
|
@@ -1864,6 +1912,7 @@ export function Layout({
|
|
|
1864
1912
|
closeChatFirstSessionWatch();
|
|
1865
1913
|
persistChatFirstPane({
|
|
1866
1914
|
appId: tab.appId,
|
|
1915
|
+
placement: tab.placement,
|
|
1867
1916
|
...(tab.path ? { path: tab.path } : {}),
|
|
1868
1917
|
...(tab.view ? { view: tab.view } : {}),
|
|
1869
1918
|
});
|
|
@@ -2096,7 +2145,7 @@ export function Layout({
|
|
|
2096
2145
|
<div className="relative flex h-full min-w-0 flex-1 flex-col overflow-hidden">
|
|
2097
2146
|
{showHeader ? <Header onOpenMobile={() => setMobileOpen(true)} /> : null}
|
|
2098
2147
|
<InvitationBanner />
|
|
2099
|
-
{isChatRoute && chatFirstMode ? (
|
|
2148
|
+
{isChatRoute && chatFirstMode && chatFirstHasActiveChat ? (
|
|
2100
2149
|
<ChatFirstSurfacePanelToggle
|
|
2101
2150
|
open={chatFirstSurfacePanel.open}
|
|
2102
2151
|
onToggle={chatFirstSurfacePanel.toggle}
|
|
@@ -2160,6 +2209,16 @@ export function Layout({
|
|
|
2160
2209
|
}}
|
|
2161
2210
|
onCloseAll={closeAllChatFirstSurfaceTabs}
|
|
2162
2211
|
onOpenSurface={openChatFirstSurface}
|
|
2212
|
+
apps={chatFirstAppItems}
|
|
2213
|
+
onOpenApp={(app) => openChatFirstPane({ appId: app.id }, "side")}
|
|
2214
|
+
renderAppIcon={(app) => (
|
|
2215
|
+
<AppIcon
|
|
2216
|
+
id={app.id}
|
|
2217
|
+
name={app.name}
|
|
2218
|
+
size="sm"
|
|
2219
|
+
className="size-7 rounded-lg"
|
|
2220
|
+
/>
|
|
2221
|
+
)}
|
|
2163
2222
|
copy={chatFirstCopy}
|
|
2164
2223
|
/>
|
|
2165
2224
|
);
|
|
@@ -2188,7 +2247,9 @@ export function Layout({
|
|
|
2188
2247
|
) : (
|
|
2189
2248
|
<>
|
|
2190
2249
|
{appContent}
|
|
2191
|
-
{chatFirstMode &&
|
|
2250
|
+
{chatFirstMode &&
|
|
2251
|
+
chatFirstHasActiveChat &&
|
|
2252
|
+
chatFirstSurfacePanel.open ? (
|
|
2192
2253
|
<ChatFirstSurfacePanel
|
|
2193
2254
|
width={chatFirstSurfaceResize.width}
|
|
2194
2255
|
onResizePointerDown={chatFirstSurfaceResize.onPointerDown}
|
|
@@ -2249,7 +2310,9 @@ export function Layout({
|
|
|
2249
2310
|
? activeChatFirstSurfaceTab.appId
|
|
2250
2311
|
: undefined
|
|
2251
2312
|
}
|
|
2252
|
-
onChatFirstAppOpen={(app) =>
|
|
2313
|
+
onChatFirstAppOpen={(app) =>
|
|
2314
|
+
openChatFirstPane({ appId: app.id }, "main")
|
|
2315
|
+
}
|
|
2253
2316
|
onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
|
|
2254
2317
|
collapsible
|
|
2255
2318
|
onCollapsedChange={setSidebarCollapsed}
|
|
@@ -2259,7 +2322,7 @@ export function Layout({
|
|
|
2259
2322
|
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
|
2260
2323
|
<SheetContent
|
|
2261
2324
|
side="left"
|
|
2262
|
-
className="w-72 p-0
|
|
2325
|
+
className="w-72 bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
|
2263
2326
|
>
|
|
2264
2327
|
<SheetTitle className="sr-only">
|
|
2265
2328
|
{t("dispatch.nav.navigation")}
|
|
@@ -2288,7 +2351,7 @@ export function Layout({
|
|
|
2288
2351
|
: undefined
|
|
2289
2352
|
}
|
|
2290
2353
|
onChatFirstAppOpen={(app) =>
|
|
2291
|
-
openChatFirstPane({ appId: app.id })
|
|
2354
|
+
openChatFirstPane({ appId: app.id }, "main")
|
|
2292
2355
|
}
|
|
2293
2356
|
onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
|
|
2294
2357
|
onNavigate={() => setMobileOpen(false)}
|
|
@@ -174,7 +174,7 @@ export default function ChatRoute() {
|
|
|
174
174
|
<DispatchAgentChatSurface
|
|
175
175
|
mode="page"
|
|
176
176
|
chatViewTransition
|
|
177
|
-
className="dispatch-chat-panel"
|
|
177
|
+
className="dispatch-chat-panel px-4 sm:px-6"
|
|
178
178
|
defaultMode="chat"
|
|
179
179
|
storageKey="dispatch"
|
|
180
180
|
threadUrlSync={threadUrlSync}
|
|
@@ -880,6 +880,54 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
880
880
|
).rejects.toThrow(/safe app-relative route/);
|
|
881
881
|
});
|
|
882
882
|
|
|
883
|
+
it("resolves target-relative embed start URLs against the granted app", async () => {
|
|
884
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
885
|
+
structuredContent: {
|
|
886
|
+
startUrl: "/_agent-native/embed/start?ticket=relative",
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const result = await runWithRequestContext(
|
|
891
|
+
{
|
|
892
|
+
userEmail: "owner@example.test",
|
|
893
|
+
requestOrigin: "http://localhost:8092",
|
|
894
|
+
},
|
|
895
|
+
() =>
|
|
896
|
+
createGrantedDispatchMcpEmbedSession({
|
|
897
|
+
app: "analytics",
|
|
898
|
+
path: "/overview",
|
|
899
|
+
chrome: "minimal",
|
|
900
|
+
}),
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
expect(result.startUrl).toBe(
|
|
904
|
+
"http://localhost:8086/_agent-native/embed/start?ticket=relative",
|
|
905
|
+
);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it("rejects cross-origin embed start URLs from a granted app", async () => {
|
|
909
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
910
|
+
structuredContent: {
|
|
911
|
+
startUrl: "https://attacker.example/steal?ticket=remote",
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
await expect(
|
|
916
|
+
runWithRequestContext(
|
|
917
|
+
{
|
|
918
|
+
userEmail: "owner@example.test",
|
|
919
|
+
requestOrigin: "http://localhost:8092",
|
|
920
|
+
},
|
|
921
|
+
() =>
|
|
922
|
+
createGrantedDispatchMcpEmbedSession({
|
|
923
|
+
app: "analytics",
|
|
924
|
+
path: "/overview",
|
|
925
|
+
chrome: "minimal",
|
|
926
|
+
}),
|
|
927
|
+
),
|
|
928
|
+
).rejects.toThrow(/invalid embed start URL/);
|
|
929
|
+
});
|
|
930
|
+
|
|
883
931
|
it("rejects a URL that does not belong to the explicitly named app", async () => {
|
|
884
932
|
await expect(
|
|
885
933
|
runWithRequestContext(
|
|
@@ -905,6 +953,12 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
905
953
|
url: "http://localhost:8092/analytics",
|
|
906
954
|
},
|
|
907
955
|
]);
|
|
956
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
957
|
+
structuredContent: {
|
|
958
|
+
startUrl:
|
|
959
|
+
"http://localhost:8092/analytics/_agent-native/embed/start?ticket=remote",
|
|
960
|
+
},
|
|
961
|
+
});
|
|
908
962
|
|
|
909
963
|
const result = await runWithRequestContext(
|
|
910
964
|
{
|
|
@@ -934,7 +988,8 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
934
988
|
);
|
|
935
989
|
expect(result).toEqual({
|
|
936
990
|
app: "analytics",
|
|
937
|
-
startUrl:
|
|
991
|
+
startUrl:
|
|
992
|
+
"http://localhost:8092/analytics/_agent-native/embed/start?ticket=remote",
|
|
938
993
|
});
|
|
939
994
|
});
|
|
940
995
|
|
|
@@ -955,6 +1010,12 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
955
1010
|
color: "#2563EB",
|
|
956
1011
|
},
|
|
957
1012
|
]);
|
|
1013
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
1014
|
+
structuredContent: {
|
|
1015
|
+
startUrl:
|
|
1016
|
+
"https://mail.agent-native.com/_agent-native/embed/start?ticket=remote",
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
958
1019
|
|
|
959
1020
|
const result = await runWithRequestContext(
|
|
960
1021
|
{
|
|
@@ -976,7 +1037,8 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
976
1037
|
});
|
|
977
1038
|
expect(result).toEqual({
|
|
978
1039
|
app: "mail",
|
|
979
|
-
startUrl:
|
|
1040
|
+
startUrl:
|
|
1041
|
+
"https://mail.agent-native.com/_agent-native/embed/start?ticket=remote",
|
|
980
1042
|
});
|
|
981
1043
|
});
|
|
982
1044
|
|
|
@@ -451,6 +451,24 @@ function appBaseUrl(app: DispatchMcpAccessibleApp): string {
|
|
|
451
451
|
return app.url.replace(/\/+$/, "");
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
function resolveGrantedAppEmbedStartUrl(
|
|
455
|
+
app: DispatchMcpAccessibleApp,
|
|
456
|
+
startUrl: string,
|
|
457
|
+
): string {
|
|
458
|
+
try {
|
|
459
|
+
const baseUrl = appBaseUrl(app);
|
|
460
|
+
const resolved = new URL(startUrl, `${baseUrl}/`);
|
|
461
|
+
if (!appMatchesUrlPath(app, resolved)) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
"Target app returned an embed start URL outside the granted app.",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return resolved.toString();
|
|
467
|
+
} catch {
|
|
468
|
+
throw new Error("Target app returned an invalid embed start URL.");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
454
472
|
function appBasePath(app: DispatchMcpAccessibleApp): string {
|
|
455
473
|
const pathname = new URL(appBaseUrl(app)).pathname.replace(/\/+$/, "");
|
|
456
474
|
return pathname === "/" ? "" : pathname;
|
|
@@ -994,7 +1012,7 @@ export async function createGrantedDispatchMcpEmbedSession(input: {
|
|
|
994
1012
|
expiresAt?: number;
|
|
995
1013
|
app: string;
|
|
996
1014
|
} = {
|
|
997
|
-
startUrl: parsed.startUrl,
|
|
1015
|
+
startUrl: resolveGrantedAppEmbedStartUrl(target.app, parsed.startUrl),
|
|
998
1016
|
app: target.app.id,
|
|
999
1017
|
};
|
|
1000
1018
|
if (parsed.targetPath) output.targetPath = parsed.targetPath;
|
|
@@ -39,6 +39,17 @@ describe("dispatch Tailwind styles", () => {
|
|
|
39
39
|
'@import "@agent-native/dispatch/styles/dispatch.css";',
|
|
40
40
|
);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it("keeps the full-page composer on the shared wide width contract", () => {
|
|
44
|
+
const stylesheet = fs.readFileSync(
|
|
45
|
+
path.join(packageRoot, "src/styles/dispatch.css"),
|
|
46
|
+
"utf-8",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(stylesheet).toMatch(
|
|
50
|
+
/\.dispatch-chat-panel \[data-agent-empty-state="centered"\] \.agent-composer-area \{[\s\S]*?max-width: min\(750px, 100%\);/,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
42
53
|
});
|
|
43
54
|
|
|
44
55
|
describe("dispatch route shells", () => {
|