@agent-native/dispatch 0.40.40 → 0.40.41-beta-dispatch-embed-01a15dc9-20261027050558
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/CHANGELOG.md +42 -0
- package/dist/actions/create_embed_session.js +1 -0
- package/dist/actions/create_embed_session.js.map +1 -1
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/provider-api-register.d.ts +13 -13
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -11
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +23 -10
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +2 -2
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +35 -5
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +173 -48
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.js +1 -1
- package/dist/server/lib/community-apps.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +1 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +170 -38
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/provider-api.d.ts +2 -2
- package/dist/shared/workspace-app-route.d.ts +2 -0
- package/dist/shared/workspace-app-route.d.ts.map +1 -0
- package/dist/shared/workspace-app-route.js +4 -0
- package/dist/shared/workspace-app-route.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/create_embed_session.ts +1 -0
- package/src/actions/index.spec.ts +3 -0
- package/src/components/layout/Layout.spec.tsx +39 -9
- package/src/components/layout/Layout.tsx +19 -40
- package/src/components/workspace-app-host.spec.tsx +88 -1
- package/src/components/workspace-app-host.tsx +30 -9
- package/src/lib/workspace-apps.spec.ts +64 -5
- package/src/lib/workspace-apps.ts +36 -6
- package/src/routes/pages/apps.$appId.spec.tsx +21 -3
- package/src/server/lib/app-creation-store.spec.ts +502 -11
- package/src/server/lib/app-creation-store.ts +198 -50
- package/src/server/lib/community-apps.ts +1 -1
- package/src/server/lib/mcp-gateway.spec.ts +396 -9
- package/src/server/lib/mcp-gateway.ts +209 -45
- package/src/shared/workspace-app-route.ts +3 -0
|
@@ -68,13 +68,12 @@ import {
|
|
|
68
68
|
AppSidebarHeader,
|
|
69
69
|
openCommandMenu,
|
|
70
70
|
} from "@agent-native/toolkit/app/shared";
|
|
71
|
+
import { ChatHistoryMenuItem } from "@agent-native/toolkit/chat-history";
|
|
71
72
|
import { writeClipboardText } from "@agent-native/toolkit/clipboard";
|
|
72
73
|
import {
|
|
73
74
|
IconApps,
|
|
74
75
|
IconBrandSlack,
|
|
75
76
|
IconBrandTelegram,
|
|
76
|
-
IconCopy,
|
|
77
|
-
IconEye,
|
|
78
77
|
IconHierarchy2,
|
|
79
78
|
IconMessageQuestion,
|
|
80
79
|
IconBroadcast,
|
|
@@ -833,14 +832,10 @@ function DispatchChatsSection({
|
|
|
833
832
|
unpin: t("agentChat.contextXray.unpin"),
|
|
834
833
|
delete: t("extensions.delete"),
|
|
835
834
|
}}
|
|
836
|
-
renderAdditionalRowActions={(item
|
|
835
|
+
renderAdditionalRowActions={(item) => (
|
|
837
836
|
<>
|
|
838
|
-
<
|
|
839
|
-
|
|
840
|
-
role="menuitem"
|
|
841
|
-
className="an-chat-history-row__menu-item"
|
|
842
|
-
onClick={() => {
|
|
843
|
-
closeMenu();
|
|
837
|
+
<ChatHistoryMenuItem
|
|
838
|
+
onSelect={() => {
|
|
844
839
|
void writeClipboardText(item.id).then((copied) => {
|
|
845
840
|
toast[copied ? "success" : "error"](
|
|
846
841
|
copied
|
|
@@ -850,16 +845,11 @@ function DispatchChatsSection({
|
|
|
850
845
|
});
|
|
851
846
|
}}
|
|
852
847
|
>
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
</button>
|
|
848
|
+
{chatFirstCopy("copySessionId")}
|
|
849
|
+
</ChatHistoryMenuItem>
|
|
856
850
|
{!chatFirstEmbedded ? (
|
|
857
|
-
<
|
|
858
|
-
|
|
859
|
-
role="menuitem"
|
|
860
|
-
className="an-chat-history-row__menu-item"
|
|
861
|
-
onClick={() => {
|
|
862
|
-
closeMenu();
|
|
851
|
+
<ChatHistoryMenuItem
|
|
852
|
+
onSelect={() => {
|
|
863
853
|
emitChatFirstSessionWatch({
|
|
864
854
|
sessionId: item.id,
|
|
865
855
|
title: item.titleText,
|
|
@@ -868,9 +858,8 @@ function DispatchChatsSection({
|
|
|
868
858
|
});
|
|
869
859
|
}}
|
|
870
860
|
>
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
</button>
|
|
861
|
+
{chatFirstCopy("watchSession")}
|
|
862
|
+
</ChatHistoryMenuItem>
|
|
874
863
|
) : null}
|
|
875
864
|
</>
|
|
876
865
|
)}
|
|
@@ -901,14 +890,10 @@ function DispatchChatsSection({
|
|
|
901
890
|
unpin: t("agentChat.contextXray.unpin"),
|
|
902
891
|
delete: t("extensions.delete"),
|
|
903
892
|
}}
|
|
904
|
-
renderAdditionalRowActions={(item
|
|
893
|
+
renderAdditionalRowActions={(item) => (
|
|
905
894
|
<>
|
|
906
|
-
<
|
|
907
|
-
|
|
908
|
-
role="menuitem"
|
|
909
|
-
className="an-chat-history-row__menu-item"
|
|
910
|
-
onClick={() => {
|
|
911
|
-
closeMenu();
|
|
895
|
+
<ChatHistoryMenuItem
|
|
896
|
+
onSelect={() => {
|
|
912
897
|
void writeClipboardText(item.id).then((copied) => {
|
|
913
898
|
toast[copied ? "success" : "error"](
|
|
914
899
|
copied
|
|
@@ -918,16 +903,11 @@ function DispatchChatsSection({
|
|
|
918
903
|
});
|
|
919
904
|
}}
|
|
920
905
|
>
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
</button>
|
|
906
|
+
{chatFirstCopy("copySessionId")}
|
|
907
|
+
</ChatHistoryMenuItem>
|
|
924
908
|
{chatFirstMode && !chatFirstEmbedded ? (
|
|
925
|
-
<
|
|
926
|
-
|
|
927
|
-
role="menuitem"
|
|
928
|
-
className="an-chat-history-row__menu-item"
|
|
929
|
-
onClick={() => {
|
|
930
|
-
closeMenu();
|
|
909
|
+
<ChatHistoryMenuItem
|
|
910
|
+
onSelect={() => {
|
|
931
911
|
emitChatFirstSessionWatch({
|
|
932
912
|
sessionId: item.id,
|
|
933
913
|
title: item.titleText,
|
|
@@ -936,9 +916,8 @@ function DispatchChatsSection({
|
|
|
936
916
|
});
|
|
937
917
|
}}
|
|
938
918
|
>
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
</button>
|
|
919
|
+
{chatFirstCopy("watchSession")}
|
|
920
|
+
</ChatHistoryMenuItem>
|
|
942
921
|
) : null}
|
|
943
922
|
</>
|
|
944
923
|
)}
|
|
@@ -62,6 +62,7 @@ vi.mock("@agent-native/toolkit/app/chat/chat-first/app-pane", () => ({
|
|
|
62
62
|
status: string;
|
|
63
63
|
}) => (
|
|
64
64
|
<div
|
|
65
|
+
data-chat-first-app-embed-url={embedUrl ?? ""}
|
|
65
66
|
data-chat-first-app-error={errorMessage ?? ""}
|
|
66
67
|
data-chat-first-app-status={status}
|
|
67
68
|
>
|
|
@@ -381,6 +382,92 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
381
382
|
});
|
|
382
383
|
});
|
|
383
384
|
|
|
385
|
+
it.each([
|
|
386
|
+
["coach", "/", "/"],
|
|
387
|
+
["onboarding", "/accounts", "/accounts"],
|
|
388
|
+
])(
|
|
389
|
+
"embeds %s by its own home path when its registry url is this Dispatch's page",
|
|
390
|
+
async (id, homePath, expectedPath) => {
|
|
391
|
+
await act(async () => {
|
|
392
|
+
root.render(
|
|
393
|
+
<WorkspaceAppFrame
|
|
394
|
+
app={{
|
|
395
|
+
id,
|
|
396
|
+
name: id,
|
|
397
|
+
path: `/${id}`,
|
|
398
|
+
homePath,
|
|
399
|
+
url: `${window.location.origin}/dispatch/apps/${id}`,
|
|
400
|
+
}}
|
|
401
|
+
/>,
|
|
402
|
+
);
|
|
403
|
+
await Promise.resolve();
|
|
404
|
+
await Promise.resolve();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(clientState.legacyMutateAsync).toHaveBeenCalledWith({
|
|
408
|
+
app: id,
|
|
409
|
+
path: expectedPath,
|
|
410
|
+
chrome: "minimal",
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
it("never falls back to framing this Dispatch's own page for the app", async () => {
|
|
416
|
+
clientState.legacyMutateError = new Error("Embed failed");
|
|
417
|
+
clientState.legacyErrorMutateAsync.mockRejectedValue(
|
|
418
|
+
clientState.legacyMutateError,
|
|
419
|
+
);
|
|
420
|
+
clientState.suppressFrameLoad = true;
|
|
421
|
+
|
|
422
|
+
await act(async () => {
|
|
423
|
+
root.render(
|
|
424
|
+
<WorkspaceAppFrame
|
|
425
|
+
app={{
|
|
426
|
+
id: "onboarding",
|
|
427
|
+
name: "Onboarding",
|
|
428
|
+
path: "/onboarding",
|
|
429
|
+
homePath: "/accounts",
|
|
430
|
+
url: `${window.location.origin}/dispatch/apps/onboarding`,
|
|
431
|
+
}}
|
|
432
|
+
/>,
|
|
433
|
+
);
|
|
434
|
+
await Promise.resolve();
|
|
435
|
+
await Promise.resolve();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
expect(
|
|
439
|
+
container
|
|
440
|
+
.querySelector("[data-chat-first-app-embed-url]")
|
|
441
|
+
?.getAttribute("data-chat-first-app-embed-url"),
|
|
442
|
+
).toBe("/onboarding/accounts");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("keeps a Dispatch page url on another origin as the app url", async () => {
|
|
446
|
+
const url = "https://gateway.example.test/dispatch/apps/coach";
|
|
447
|
+
|
|
448
|
+
await act(async () => {
|
|
449
|
+
root.render(
|
|
450
|
+
<WorkspaceAppFrame
|
|
451
|
+
app={{
|
|
452
|
+
id: "coach",
|
|
453
|
+
name: "Coach",
|
|
454
|
+
path: "/coach",
|
|
455
|
+
homePath: "/",
|
|
456
|
+
url,
|
|
457
|
+
}}
|
|
458
|
+
/>,
|
|
459
|
+
);
|
|
460
|
+
await Promise.resolve();
|
|
461
|
+
await Promise.resolve();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(clientState.legacyMutateAsync).toHaveBeenCalledWith({
|
|
465
|
+
app: "coach",
|
|
466
|
+
url,
|
|
467
|
+
chrome: "minimal",
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
384
471
|
it("uses the granted-app session action for mounted apps outside the SSO registry", async () => {
|
|
385
472
|
clientState.workspaceSsoEnabled = true;
|
|
386
473
|
|
|
@@ -549,7 +636,7 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
549
636
|
});
|
|
550
637
|
|
|
551
638
|
expect(onChildRouteChange).toHaveBeenCalledWith(
|
|
552
|
-
"/apps/design
|
|
639
|
+
"/apps/design?__workspace_app_path=%2Ffoobar%3Fmode%3Dedit%23canvas",
|
|
553
640
|
);
|
|
554
641
|
|
|
555
642
|
await act(async () => {
|
|
@@ -28,6 +28,7 @@ import { Link } from "react-router";
|
|
|
28
28
|
|
|
29
29
|
import { isEmbedSessionExpiredMessage } from "../lib/embed-session-recovery";
|
|
30
30
|
import {
|
|
31
|
+
isWorkspaceAppHostPageUrl,
|
|
31
32
|
isWorkspaceSsoApp,
|
|
32
33
|
isDispatchWorkspaceAppId,
|
|
33
34
|
navigateToWorkspaceApp,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
workspaceAppRouteForChildPath,
|
|
36
37
|
workspaceAppDirectHref,
|
|
37
38
|
workspaceAppHref,
|
|
39
|
+
workspaceAppTargetPath,
|
|
38
40
|
type WorkspaceAppSummary,
|
|
39
41
|
} from "../lib/workspace-apps";
|
|
40
42
|
import { DISPATCH_WORKSPACE_SSO_FLAG } from "../shared/feature-flags";
|
|
@@ -284,30 +286,35 @@ export function WorkspaceAppFrame({
|
|
|
284
286
|
skipActionQueryInvalidation: true,
|
|
285
287
|
timeoutMs: EMBED_SESSION_TIMEOUT_MS,
|
|
286
288
|
});
|
|
289
|
+
const isHostPageUrl = isWorkspaceAppHostPageUrl(
|
|
290
|
+
app,
|
|
291
|
+
typeof window === "undefined" ? undefined : window.location.origin,
|
|
292
|
+
);
|
|
293
|
+
const appUrl = isHostPageUrl ? null : (app.url ?? null);
|
|
287
294
|
const appHref = workspaceAppHref({
|
|
288
295
|
id: app.id,
|
|
289
296
|
name: app.name,
|
|
290
297
|
path: app.path ?? "",
|
|
291
298
|
homePath: app.homePath ?? undefined,
|
|
292
|
-
url:
|
|
299
|
+
url: appUrl,
|
|
293
300
|
isDispatch: app.isDispatch ?? isDispatchWorkspaceAppId(app.id),
|
|
294
301
|
});
|
|
295
302
|
const topWindowHref = useMemo(() => {
|
|
296
303
|
if (embedPath !== undefined) {
|
|
297
304
|
return workspaceAppDirectHref(
|
|
298
|
-
{ path: app.path, url:
|
|
305
|
+
{ path: app.path ?? null, url: appUrl },
|
|
299
306
|
embedPath,
|
|
300
307
|
);
|
|
301
308
|
}
|
|
302
309
|
if (initialPath !== undefined) {
|
|
303
310
|
return workspaceAppDirectHref(
|
|
304
|
-
{ path: app.path, url:
|
|
311
|
+
{ path: app.path ?? null, url: appUrl },
|
|
305
312
|
initialPath,
|
|
306
313
|
);
|
|
307
314
|
}
|
|
308
315
|
|
|
309
316
|
return appHref;
|
|
310
|
-
}, [appHref, embedPath, initialPath]);
|
|
317
|
+
}, [app.path, appHref, appUrl, embedPath, initialPath]);
|
|
311
318
|
const openInTopWindow = shouldOpenWorkspaceAppInTopWindow();
|
|
312
319
|
const topWindowSsoAttemptKey = `${app.id}\u0000${app.path ?? ""}\u0000${app.url ?? ""}\u0000${embedPath ?? ""}\u0000${initialPath ?? ""}\u0000${embedAttempt}`;
|
|
313
320
|
const topWindowSsoAttemptedRef = useRef<string | null>(null);
|
|
@@ -319,12 +326,26 @@ export function WorkspaceAppFrame({
|
|
|
319
326
|
return buildChatFirstEmbedSessionInput(app.id, initialPath);
|
|
320
327
|
}
|
|
321
328
|
if (!appHref) return null;
|
|
329
|
+
if (isHostPageUrl) {
|
|
330
|
+
return buildChatFirstEmbedSessionInput(
|
|
331
|
+
app.id,
|
|
332
|
+
workspaceAppTargetPath({ homePath: app.homePath ?? null }),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
322
335
|
return {
|
|
323
336
|
app: app.id,
|
|
324
|
-
...(
|
|
337
|
+
...(appUrl?.trim() ? { url: appHref } : { path: appHref }),
|
|
325
338
|
chrome: "minimal",
|
|
326
339
|
};
|
|
327
|
-
}, [
|
|
340
|
+
}, [
|
|
341
|
+
app.homePath,
|
|
342
|
+
app.id,
|
|
343
|
+
appHref,
|
|
344
|
+
appUrl,
|
|
345
|
+
embedPath,
|
|
346
|
+
initialPath,
|
|
347
|
+
isHostPageUrl,
|
|
348
|
+
]);
|
|
328
349
|
|
|
329
350
|
useEffect(() => {
|
|
330
351
|
if (openInTopWindow && useWorkspaceSso && embedInput) {
|
|
@@ -410,7 +431,7 @@ export function WorkspaceAppFrame({
|
|
|
410
431
|
const fallbackHref =
|
|
411
432
|
initialPath !== undefined || embedPath !== undefined
|
|
412
433
|
? workspaceAppDirectHref(
|
|
413
|
-
{ path: app.path ?? "", url:
|
|
434
|
+
{ path: app.path ?? "", url: appUrl },
|
|
414
435
|
initialPath ?? embedPath ?? "/",
|
|
415
436
|
)
|
|
416
437
|
: appHref;
|
|
@@ -485,7 +506,7 @@ export function WorkspaceAppFrame({
|
|
|
485
506
|
}
|
|
486
507
|
|
|
487
508
|
const route = workspaceAppRouteForChildPath(
|
|
488
|
-
{ id: app.id, path: app.path ?? "", url:
|
|
509
|
+
{ id: app.id, path: app.path ?? "", url: appUrl },
|
|
489
510
|
message.path,
|
|
490
511
|
);
|
|
491
512
|
if (route) onChildRouteChange(route);
|
|
@@ -493,7 +514,7 @@ export function WorkspaceAppFrame({
|
|
|
493
514
|
|
|
494
515
|
window.addEventListener("message", handleWorkspaceAppRoute);
|
|
495
516
|
return () => window.removeEventListener("message", handleWorkspaceAppRoute);
|
|
496
|
-
}, [app.id, app.path,
|
|
517
|
+
}, [app.id, app.path, appUrl, embedUrl, onChildRouteChange]);
|
|
497
518
|
|
|
498
519
|
useEffect(() => {
|
|
499
520
|
postThemeToFrame();
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isDefaultWorkspaceAppHiddenId,
|
|
7
7
|
isDispatchWorkspaceAppId,
|
|
8
8
|
isPathMountedWorkspaceApp,
|
|
9
|
+
isWorkspaceAppHostPageUrl,
|
|
9
10
|
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
10
11
|
isWorkspaceSsoApp,
|
|
11
12
|
mergeChatFirstWorkspaceApps,
|
|
@@ -148,6 +149,38 @@ describe("workspace app routes", () => {
|
|
|
148
149
|
expect(workspaceAppIdFromRoute("/apps/mail/inbox")).toBe("mail");
|
|
149
150
|
});
|
|
150
151
|
|
|
152
|
+
it("recognizes the Dispatch page that hosts an app only on Dispatch's own origin", () => {
|
|
153
|
+
const origin = "https://beta.agent-workspace.builder.io";
|
|
154
|
+
const hostPage = { id: "coach", url: `${origin}/dispatch/apps/coach` };
|
|
155
|
+
|
|
156
|
+
expect(isWorkspaceAppHostPageUrl(hostPage, origin)).toBe(true);
|
|
157
|
+
expect(
|
|
158
|
+
isWorkspaceAppHostPageUrl(
|
|
159
|
+
{ id: "Coach", url: `${origin}/dispatch/apps/coach/` },
|
|
160
|
+
origin,
|
|
161
|
+
),
|
|
162
|
+
).toBe(true);
|
|
163
|
+
expect(
|
|
164
|
+
isWorkspaceAppHostPageUrl(hostPage, "https://beta.dispatch.example.test"),
|
|
165
|
+
).toBe(false);
|
|
166
|
+
expect(isWorkspaceAppHostPageUrl(hostPage, undefined)).toBe(false);
|
|
167
|
+
expect(
|
|
168
|
+
isWorkspaceAppHostPageUrl(
|
|
169
|
+
{ id: "coach", url: `${origin}/coach` },
|
|
170
|
+
origin,
|
|
171
|
+
),
|
|
172
|
+
).toBe(false);
|
|
173
|
+
expect(
|
|
174
|
+
isWorkspaceAppHostPageUrl(
|
|
175
|
+
{ id: "coach", url: `${origin}/dispatch/apps/mycoach` },
|
|
176
|
+
origin,
|
|
177
|
+
),
|
|
178
|
+
).toBe(false);
|
|
179
|
+
expect(isWorkspaceAppHostPageUrl({ id: "coach", url: null }, origin)).toBe(
|
|
180
|
+
false,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
151
184
|
it("preserves the initial app suffix for a standalone deep link", () => {
|
|
152
185
|
expect(
|
|
153
186
|
workspaceAppInitialPathFromSplat("inbox", "?view=unread", "#top"),
|
|
@@ -155,13 +188,39 @@ describe("workspace app routes", () => {
|
|
|
155
188
|
expect(workspaceAppInitialPathFromSplat(undefined, "", "")).toBeUndefined();
|
|
156
189
|
});
|
|
157
190
|
|
|
191
|
+
it("restores a child route from the flat Workspace app shell query", () => {
|
|
192
|
+
expect(
|
|
193
|
+
workspaceAppInitialPathFromSplat(
|
|
194
|
+
undefined,
|
|
195
|
+
"?__workspace_app_path=%2Ftriage%3Fview%3Dopen%23latest",
|
|
196
|
+
"",
|
|
197
|
+
),
|
|
198
|
+
).toBe("/triage?view=open#latest");
|
|
199
|
+
expect(
|
|
200
|
+
workspaceAppInitialPathFromSplat(
|
|
201
|
+
undefined,
|
|
202
|
+
"?__workspace_app_path=%2F%2Fevil.example",
|
|
203
|
+
"",
|
|
204
|
+
),
|
|
205
|
+
).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
|
|
158
208
|
it("maps child routes to shareable Dispatch app routes", () => {
|
|
209
|
+
const childPath = "/foobar?mode=edit#canvas";
|
|
210
|
+
const route = workspaceAppRouteForChildPath(
|
|
211
|
+
{ id: "design", path: "/", url: "https://design.example.test" },
|
|
212
|
+
childPath,
|
|
213
|
+
);
|
|
214
|
+
expect(route).toBe(
|
|
215
|
+
"/apps/design?__workspace_app_path=%2Ffoobar%3Fmode%3Dedit%23canvas",
|
|
216
|
+
);
|
|
159
217
|
expect(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"
|
|
218
|
+
workspaceAppInitialPathFromSplat(
|
|
219
|
+
undefined,
|
|
220
|
+
new URL(route!, "https://dispatch.example.test").search,
|
|
221
|
+
"",
|
|
163
222
|
),
|
|
164
|
-
).toBe(
|
|
223
|
+
).toBe(childPath);
|
|
165
224
|
expect(
|
|
166
225
|
workspaceAppRouteForChildPath(
|
|
167
226
|
{
|
|
@@ -171,7 +230,7 @@ describe("workspace app routes", () => {
|
|
|
171
230
|
},
|
|
172
231
|
"/design/foobar",
|
|
173
232
|
),
|
|
174
|
-
).toBe("/apps/internal-design
|
|
233
|
+
).toBe("/apps/internal-design?__workspace_app_path=%2Ffoobar");
|
|
175
234
|
expect(
|
|
176
235
|
workspaceAppRouteForChildPath(
|
|
177
236
|
{ id: "design", path: "/", url: "https://design.example.test" },
|
|
@@ -9,11 +9,14 @@ import {
|
|
|
9
9
|
withBuilderUtmTrackingParams,
|
|
10
10
|
} from "@agent-native/core/shared";
|
|
11
11
|
|
|
12
|
+
import { workspaceAppRoute } from "../shared/workspace-app-route";
|
|
12
13
|
import {
|
|
13
14
|
CANONICAL_WORKSPACE_SSO_APP_ORIGINS,
|
|
14
15
|
isWorkspaceSsoAppUrl,
|
|
15
16
|
} from "../shared/workspace-sso";
|
|
16
17
|
|
|
18
|
+
export { workspaceAppRoute } from "../shared/workspace-app-route";
|
|
19
|
+
|
|
17
20
|
export interface WorkspaceAppSummary {
|
|
18
21
|
id: string;
|
|
19
22
|
name: string;
|
|
@@ -220,10 +223,6 @@ export function defaultWorkspaceAppUrl(rawUrl: string): string {
|
|
|
220
223
|
}
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
export function workspaceAppRoute(appId: string): string {
|
|
224
|
-
return `/apps/${encodeURIComponent(appId)}`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
226
|
function normalizeWorkspaceAppRoutePath(rawPath: string): URL | null {
|
|
228
227
|
if (
|
|
229
228
|
typeof rawPath !== "string" ||
|
|
@@ -281,8 +280,10 @@ export function workspaceAppRouteForChildPath(
|
|
|
281
280
|
? parsed.pathname.slice(mountPath.length) || "/"
|
|
282
281
|
: parsed.pathname;
|
|
283
282
|
const routePath = workspaceAppRoute(app.id);
|
|
284
|
-
const
|
|
285
|
-
return
|
|
283
|
+
const childRoute = `${relativePathname}${parsed.search}${parsed.hash}`;
|
|
284
|
+
if (childRoute === "/") return routePath;
|
|
285
|
+
const search = new URLSearchParams([["__workspace_app_path", childRoute]]);
|
|
286
|
+
return `${routePath}?${search}`;
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
export function workspaceAppInitialPathFromSplat(
|
|
@@ -290,6 +291,14 @@ export function workspaceAppInitialPathFromSplat(
|
|
|
290
291
|
search: string,
|
|
291
292
|
hash: string,
|
|
292
293
|
): string | undefined {
|
|
294
|
+
const appPath = new URLSearchParams(search).get("__workspace_app_path");
|
|
295
|
+
if (appPath !== null) {
|
|
296
|
+
const parsed = normalizeWorkspaceAppRoutePath(appPath);
|
|
297
|
+
if (!parsed) return undefined;
|
|
298
|
+
const path = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
299
|
+
return path === "/" ? undefined : path;
|
|
300
|
+
}
|
|
301
|
+
|
|
293
302
|
const pathname = routeSplat?.trim()
|
|
294
303
|
? `/${routeSplat.trim().replace(/^[/\\]+/, "")}`
|
|
295
304
|
: "/";
|
|
@@ -311,6 +320,27 @@ export function workspaceAppIdFromRoute(pathname: string): string | null {
|
|
|
311
320
|
}
|
|
312
321
|
}
|
|
313
322
|
|
|
323
|
+
// A mounted app's registry `url` is the Dispatch page that hosts it
|
|
324
|
+
// (`<dispatch>/apps/<id>`). Read from that same Dispatch, it is the page doing
|
|
325
|
+
// the framing, so framing it again nests Dispatch inside itself.
|
|
326
|
+
export function isWorkspaceAppHostPageUrl(
|
|
327
|
+
app: Pick<WorkspaceAppSummary, "id" | "url">,
|
|
328
|
+
dispatchOrigin: string | undefined,
|
|
329
|
+
): boolean {
|
|
330
|
+
const rawUrl = app.url?.trim();
|
|
331
|
+
if (!rawUrl || !dispatchOrigin) return false;
|
|
332
|
+
let url: URL;
|
|
333
|
+
try {
|
|
334
|
+
url = new URL(rawUrl);
|
|
335
|
+
} catch {
|
|
336
|
+
// coercion-ok: a URL that does not parse is not Dispatch's page for the app.
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
if (url.origin !== dispatchOrigin) return false;
|
|
340
|
+
const pathname = url.pathname.replace(/\/+$/, "").toLowerCase();
|
|
341
|
+
return pathname.endsWith(workspaceAppRoute(app.id.trim()).toLowerCase());
|
|
342
|
+
}
|
|
343
|
+
|
|
314
344
|
export function workspaceAppHref(app: WorkspaceAppSummary): string | null {
|
|
315
345
|
if (app.status === "pending") {
|
|
316
346
|
return app.builderUrl
|
|
@@ -36,10 +36,13 @@ vi.mock("../../lib/workspace-apps", () => ({
|
|
|
36
36
|
routeSplat: string | undefined,
|
|
37
37
|
search: string,
|
|
38
38
|
hash: string,
|
|
39
|
-
) =>
|
|
40
|
-
|
|
39
|
+
) => {
|
|
40
|
+
const appPath = new URLSearchParams(search).get("__workspace_app_path");
|
|
41
|
+
if (appPath !== null) return appPath;
|
|
42
|
+
return routeSplat || search || hash
|
|
41
43
|
? `/${routeSplat ?? ""}${search}${hash}`
|
|
42
|
-
: undefined
|
|
44
|
+
: undefined;
|
|
45
|
+
},
|
|
43
46
|
}));
|
|
44
47
|
|
|
45
48
|
import WorkspaceAppRoute from "./apps.$appId";
|
|
@@ -112,4 +115,19 @@ describe("WorkspaceAppRoute", () => {
|
|
|
112
115
|
replace: true,
|
|
113
116
|
});
|
|
114
117
|
});
|
|
118
|
+
|
|
119
|
+
it("passes a flat-shell child path from the workspace route query to the host", async () => {
|
|
120
|
+
routeState.routeSplat = undefined;
|
|
121
|
+
routeState.location = {
|
|
122
|
+
pathname: "/apps/mail",
|
|
123
|
+
search: "?__workspace_app_path=%2Ftriage%3Fview%3Dopen%23latest",
|
|
124
|
+
hash: "",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
await act(async () => {
|
|
128
|
+
root.render(<WorkspaceAppRoute />);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(routeState.hostProps?.initialPath).toBe("/triage?view=open#latest");
|
|
132
|
+
});
|
|
115
133
|
});
|