@agent-native/dispatch 0.33.1-nightly-20260902192636 → 0.33.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/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +2 -2
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.js +5 -1
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +4 -4
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.js +29 -0
- package/dist/lib/workspace-apps.test.js.map +1 -1
- package/package.json +4 -4
- package/src/components/dispatch-control-plane.spec.tsx +1 -0
- package/src/components/layout/Layout.tsx +1 -2
- package/src/components/workspace-app-card.spec.tsx +69 -1
- package/src/components/workspace-app-card.tsx +4 -3
- package/src/components/workspace-app-host.spec.tsx +24 -0
- package/src/lib/workspace-apps.test.ts +36 -0
- package/src/lib/workspace-apps.ts +7 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
-
import { MemoryRouter } from "react-router";
|
|
5
|
+
import { MemoryRouter, useLocation } from "react-router";
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
|
|
8
8
|
import { TooltipProvider } from "./ui/tooltip";
|
|
@@ -54,7 +54,13 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
54
54
|
|
|
55
55
|
const frameState = vi.hoisted(() => ({ inBuilderFrame: false }));
|
|
56
56
|
|
|
57
|
+
function LocationProbe() {
|
|
58
|
+
const location = useLocation();
|
|
59
|
+
return <output data-location-path>{location.pathname}</output>;
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
vi.mock("@agent-native/core/client/host", () => ({
|
|
63
|
+
getClientSurface: () => "web",
|
|
58
64
|
isInBuilderFrame: () => frameState.inBuilderFrame,
|
|
59
65
|
}));
|
|
60
66
|
|
|
@@ -259,6 +265,68 @@ describe("WorkspaceAppCard", () => {
|
|
|
259
265
|
}
|
|
260
266
|
});
|
|
261
267
|
|
|
268
|
+
it("falls back to the Dispatch route when top-window navigation is blocked", async () => {
|
|
269
|
+
frameState.inBuilderFrame = true;
|
|
270
|
+
const originalParent = window.parent;
|
|
271
|
+
const originalTop = window.top;
|
|
272
|
+
const location = {} as Location;
|
|
273
|
+
Object.defineProperty(location, "href", {
|
|
274
|
+
configurable: true,
|
|
275
|
+
set: () => {
|
|
276
|
+
throw new Error("Top navigation is blocked");
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const topWindow = { location } as unknown as Window;
|
|
280
|
+
Object.defineProperty(window, "parent", {
|
|
281
|
+
configurable: true,
|
|
282
|
+
value: {},
|
|
283
|
+
});
|
|
284
|
+
Object.defineProperty(window, "top", {
|
|
285
|
+
configurable: true,
|
|
286
|
+
value: topWindow,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await act(async () => {
|
|
291
|
+
root.render(
|
|
292
|
+
<MemoryRouter>
|
|
293
|
+
<LocationProbe />
|
|
294
|
+
<TooltipProvider>
|
|
295
|
+
<WorkspaceAppCard
|
|
296
|
+
app={{
|
|
297
|
+
id: "feedback-leaderboard",
|
|
298
|
+
name: "Feedback leaderboard",
|
|
299
|
+
path: "/feedback-leaderboard",
|
|
300
|
+
url: "https://agent-workspace.builder.io/feedback-leaderboard/leaderboard",
|
|
301
|
+
status: "ready",
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
</TooltipProvider>
|
|
305
|
+
</MemoryRouter>,
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await act(async () =>
|
|
310
|
+
container
|
|
311
|
+
.querySelector<HTMLButtonElement>(".app-open-actions__primary")
|
|
312
|
+
?.click(),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
expect(container.querySelector("[data-location-path]")?.textContent).toBe(
|
|
316
|
+
"/apps/feedback-leaderboard",
|
|
317
|
+
);
|
|
318
|
+
} finally {
|
|
319
|
+
Object.defineProperty(window, "parent", {
|
|
320
|
+
configurable: true,
|
|
321
|
+
value: originalParent,
|
|
322
|
+
});
|
|
323
|
+
Object.defineProperty(window, "top", {
|
|
324
|
+
configurable: true,
|
|
325
|
+
value: originalTop,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
262
330
|
it("opens Share from the app settings menu instead of the card actions", async () => {
|
|
263
331
|
const animationFrameCallbacks: FrameRequestCallback[] = [];
|
|
264
332
|
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
|
|
@@ -380,9 +380,10 @@ function WorkspaceAppOpenActions({
|
|
|
380
380
|
name={app.name}
|
|
381
381
|
href={href}
|
|
382
382
|
showNewTabOption
|
|
383
|
-
onOpen={() =>
|
|
384
|
-
openDirectly
|
|
385
|
-
|
|
383
|
+
onOpen={() => {
|
|
384
|
+
if (!openDirectly || navigateToWorkspaceApp(href)) return;
|
|
385
|
+
void navigate(appRoute);
|
|
386
|
+
}}
|
|
386
387
|
menuItems={
|
|
387
388
|
onTogglePinned
|
|
388
389
|
? [
|
|
@@ -38,6 +38,7 @@ const clientState = vi.hoisted(() => {
|
|
|
38
38
|
legacyMutateError: null as Error | null,
|
|
39
39
|
legacyErrorMutateAsync,
|
|
40
40
|
inBuilderFrame: false,
|
|
41
|
+
clientSurface: "web" as "web" | "electron" | "tauri",
|
|
41
42
|
frameLoadHandler: null as (() => void) | null,
|
|
42
43
|
suppressFrameLoad: false,
|
|
43
44
|
theme: "dark" as "dark" | "light",
|
|
@@ -95,6 +96,7 @@ vi.mock("@agent-native/core/client/feature-flags", () => ({
|
|
|
95
96
|
}));
|
|
96
97
|
|
|
97
98
|
vi.mock("@agent-native/core/client/host", () => ({
|
|
99
|
+
getClientSurface: () => clientState.clientSurface,
|
|
98
100
|
isInBuilderFrame: () => clientState.inBuilderFrame,
|
|
99
101
|
}));
|
|
100
102
|
|
|
@@ -190,6 +192,7 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
190
192
|
clientState.legacyMutateError = null;
|
|
191
193
|
clientState.legacyErrorMutateAsync.mockReset();
|
|
192
194
|
clientState.inBuilderFrame = false;
|
|
195
|
+
clientState.clientSurface = "web";
|
|
193
196
|
clientState.frameLoadHandler = null;
|
|
194
197
|
clientState.suppressFrameLoad = false;
|
|
195
198
|
clientState.workspaceSsoMutateAsync.mockClear();
|
|
@@ -663,6 +666,27 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
663
666
|
expect(container.querySelector("iframe")).not.toBeNull();
|
|
664
667
|
});
|
|
665
668
|
|
|
669
|
+
it("opens the app in the top window for a native desktop host", async () => {
|
|
670
|
+
const topWindow = { location: { href: "" } } as unknown as Window;
|
|
671
|
+
const expectedUrl = new URL("/mail", window.location.href).href;
|
|
672
|
+
clientState.clientSurface = "electron";
|
|
673
|
+
Object.defineProperty(window, "top", {
|
|
674
|
+
configurable: true,
|
|
675
|
+
value: topWindow,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await act(async () => {
|
|
679
|
+
root.render(
|
|
680
|
+
<WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
|
|
681
|
+
);
|
|
682
|
+
await Promise.resolve();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
expect(topWindow.location.href).toBe(expectedUrl);
|
|
686
|
+
expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
|
|
687
|
+
expect(container.querySelector("iframe")).toBeNull();
|
|
688
|
+
});
|
|
689
|
+
|
|
666
690
|
it("falls back to the embedded app when top-window navigation is blocked", async () => {
|
|
667
691
|
const navigateToTopWindow = vi.fn(() => false);
|
|
668
692
|
clientState.inBuilderFrame = true;
|
|
@@ -8,6 +8,15 @@ import {
|
|
|
8
8
|
workspaceAppEmbedTarget,
|
|
9
9
|
} from "./workspace-apps";
|
|
10
10
|
|
|
11
|
+
const defaultUserAgent = navigator.userAgent;
|
|
12
|
+
|
|
13
|
+
function setUserAgent(userAgent: string) {
|
|
14
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
15
|
+
configurable: true,
|
|
16
|
+
value: userAgent,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
function setAncestorOrigin(origin: string | null) {
|
|
12
21
|
Object.defineProperty(window.location, "ancestorOrigins", {
|
|
13
22
|
configurable: true,
|
|
@@ -88,6 +97,7 @@ describe("navigateToWorkspaceApp", () => {
|
|
|
88
97
|
afterEach(() => {
|
|
89
98
|
window.history.replaceState({}, "", "/");
|
|
90
99
|
setAncestorOrigin(null);
|
|
100
|
+
setUserAgent(defaultUserAgent);
|
|
91
101
|
Object.defineProperty(window, "parent", {
|
|
92
102
|
configurable: true,
|
|
93
103
|
value: window,
|
|
@@ -107,6 +117,32 @@ describe("navigateToWorkspaceApp", () => {
|
|
|
107
117
|
expect(shouldOpenWorkspaceAppInTopWindow()).toBe(false);
|
|
108
118
|
});
|
|
109
119
|
|
|
120
|
+
it("keeps unrelated Electron webviews inline", () => {
|
|
121
|
+
setUserAgent("Mozilla/5.0 Electron/38.0");
|
|
122
|
+
|
|
123
|
+
expect(shouldOpenWorkspaceAppInTopWindow()).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("ignores a generic Electron preload global", () => {
|
|
127
|
+
setUserAgent("Mozilla/5.0 Electron/38.0");
|
|
128
|
+
Object.defineProperty(window, "electronAPI", {
|
|
129
|
+
configurable: true,
|
|
130
|
+
value: {},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
expect(shouldOpenWorkspaceAppInTopWindow()).toBe(false);
|
|
135
|
+
} finally {
|
|
136
|
+
delete (window as unknown as { electronAPI?: unknown }).electronAPI;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("uses the top window in known native desktop hosts", () => {
|
|
141
|
+
setUserAgent("Mozilla/5.0 Electron/38.0 ChatGPT/1.0");
|
|
142
|
+
|
|
143
|
+
expect(shouldOpenWorkspaceAppInTopWindow()).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
110
146
|
it("keeps the hosted Dispatch shell inline", () => {
|
|
111
147
|
Object.defineProperty(window, "parent", {
|
|
112
148
|
configurable: true,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { CHAT_FIRST_DEFAULT_APP_IDS } from "@agent-native/core/client/chat-first";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getClientSurface,
|
|
4
|
+
isInBuilderFrame,
|
|
5
|
+
} from "@agent-native/core/client/host";
|
|
3
6
|
import {
|
|
4
7
|
resolveEnvironmentTargets,
|
|
5
8
|
withBuilderUtmTrackingParams,
|
|
@@ -376,9 +379,9 @@ export function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {
|
|
|
376
379
|
|
|
377
380
|
export function shouldOpenWorkspaceAppInTopWindow(): boolean {
|
|
378
381
|
if (typeof window === "undefined") return false;
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
return isInBuilderFrame();
|
|
382
|
+
// Standard browser iframes stay inline; Builder and native shells need the
|
|
383
|
+
// app as the top-level document so browser APIs such as WebMCP bind to it.
|
|
384
|
+
return isInBuilderFrame() || getClientSurface() !== "web";
|
|
382
385
|
}
|
|
383
386
|
|
|
384
387
|
export function navigateToWorkspaceApp(href: string): boolean {
|