@agent-native/dispatch 0.29.1 → 0.29.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 (33) hide show
  1. package/dist/actions/list-workspace-apps.d.ts +1 -0
  2. package/dist/actions/list-workspace-apps.js +3 -0
  3. package/dist/actions/list-workspace-apps.js.map +1 -1
  4. package/dist/components/layout/Layout.js +1 -1
  5. package/dist/components/layout/Layout.js.map +1 -1
  6. package/dist/components/workspace-app-host.d.ts +4 -2
  7. package/dist/components/workspace-app-host.d.ts.map +1 -1
  8. package/dist/components/workspace-app-host.js +54 -10
  9. package/dist/components/workspace-app-host.js.map +1 -1
  10. package/dist/lib/workspace-apps.d.ts +2 -0
  11. package/dist/lib/workspace-apps.d.ts.map +1 -1
  12. package/dist/lib/workspace-apps.js +23 -0
  13. package/dist/lib/workspace-apps.js.map +1 -1
  14. package/dist/lib/workspace-apps.test.js +60 -2
  15. package/dist/lib/workspace-apps.test.js.map +1 -1
  16. package/dist/routes/pages/$appId.d.ts.map +1 -1
  17. package/dist/routes/pages/$appId.js +15 -6
  18. package/dist/routes/pages/$appId.js.map +1 -1
  19. package/dist/routes/pages/apps.$appId.d.ts +1 -0
  20. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  21. package/dist/routes/pages/apps.$appId.js +5 -1
  22. package/dist/routes/pages/apps.$appId.js.map +1 -1
  23. package/package.json +2 -2
  24. package/src/actions/list-workspace-apps.ts +3 -0
  25. package/src/components/layout/Layout.tsx +1 -1
  26. package/src/components/workspace-app-host.spec.tsx +222 -38
  27. package/src/components/workspace-app-host.tsx +89 -7
  28. package/src/lib/workspace-apps.test.ts +70 -1
  29. package/src/lib/workspace-apps.ts +24 -0
  30. package/src/routes/pages/$appId.spec.tsx +115 -0
  31. package/src/routes/pages/$appId.tsx +28 -5
  32. package/src/routes/pages/apps.$appId.spec.tsx +66 -0
  33. package/src/routes/pages/apps.$appId.tsx +11 -1
@@ -31,6 +31,8 @@ import { Link } from "react-router";
31
31
  import { isEmbedSessionExpiredMessage } from "../lib/embed-session-recovery";
32
32
  import {
33
33
  mergeChatFirstWorkspaceApps,
34
+ navigateToWorkspaceApp,
35
+ shouldOpenWorkspaceAppInTopWindow,
34
36
  workspaceAppDirectHref,
35
37
  workspaceAppEmbedTarget,
36
38
  workspaceAppHref,
@@ -244,6 +246,7 @@ export interface WorkspaceAppFrameApp {
244
246
 
245
247
  interface WorkspaceAppFrameProps {
246
248
  app: WorkspaceAppFrameApp;
249
+ navigateToTopWindow?: (href: string) => boolean | void;
247
250
  /** Chat-first app tabs use their own route while standalone hosts use app metadata. */
248
251
  embedPath?: string;
249
252
  /** Chat-first app surfaces own the parent chat rail around the iframe. */
@@ -253,6 +256,7 @@ interface WorkspaceAppFrameProps {
253
256
 
254
257
  export function WorkspaceAppFrame({
255
258
  app,
259
+ navigateToTopWindow = navigateToWorkspaceApp,
256
260
  embedPath,
257
261
  chatSidebar = false,
258
262
  copy = defaultChatFirstCopy,
@@ -269,6 +273,8 @@ export function WorkspaceAppFrame({
269
273
  const [embedError, setEmbedError] = useState<Error | null>(null);
270
274
  const [isDirectFallback, setIsDirectFallback] = useState(false);
271
275
  const [embedAttempt, setEmbedAttempt] = useState(0);
276
+ const [topWindowNavigationFailed, setTopWindowNavigationFailed] =
277
+ useState(false);
272
278
  const embedFrameRef = useRef<HTMLIFrameElement>(null);
273
279
  const postThemeToFrame = useCallback(() => {
274
280
  embedFrameRef.current?.contentWindow?.postMessage(
@@ -299,6 +305,21 @@ export function WorkspaceAppFrame({
299
305
  path: app.path ?? "",
300
306
  url: app.url,
301
307
  });
308
+ const topWindowHref = useMemo(() => {
309
+ if (embedPath !== undefined) {
310
+ return workspaceAppDirectHref(
311
+ { path: app.path, url: app.url },
312
+ embedPath,
313
+ );
314
+ }
315
+
316
+ const target = workspaceAppEmbedTarget({
317
+ path: app.path ?? "",
318
+ url: app.url,
319
+ });
320
+ return target.url ?? target.path ?? null;
321
+ }, [app.path, app.url, embedPath]);
322
+ const openInTopWindow = shouldOpenWorkspaceAppInTopWindow();
302
323
  const embedInput = useMemo<EmbedSessionInput | null>(() => {
303
324
  if (embedPath !== undefined) {
304
325
  return buildChatFirstEmbedSessionInput(app.id, embedPath);
@@ -312,7 +333,26 @@ export function WorkspaceAppFrame({
312
333
  }, [app.id, app.path, app.url, appHref, embedPath]);
313
334
 
314
335
  useEffect(() => {
315
- if (!embedInput) return;
336
+ if (!openInTopWindow) {
337
+ setTopWindowNavigationFailed(false);
338
+ return;
339
+ }
340
+ if (!topWindowHref) {
341
+ setTopWindowNavigationFailed(true);
342
+ return;
343
+ }
344
+
345
+ let didNavigate = false;
346
+ try {
347
+ didNavigate = navigateToTopWindow(topWindowHref) !== false;
348
+ } catch {
349
+ didNavigate = false;
350
+ }
351
+ setTopWindowNavigationFailed(!didNavigate);
352
+ }, [navigateToTopWindow, openInTopWindow, topWindowHref]);
353
+
354
+ useEffect(() => {
355
+ if (!embedInput || (openInTopWindow && !topWindowNavigationFailed)) return;
316
356
  let cancelled = false;
317
357
  setEmbedUrl(null);
318
358
  setEmbedError(null);
@@ -359,6 +399,8 @@ export function WorkspaceAppFrame({
359
399
  embedInput,
360
400
  embedPath,
361
401
  embedAttempt,
402
+ openInTopWindow,
403
+ topWindowNavigationFailed,
362
404
  workspaceSsoEnabled,
363
405
  ]);
364
406
 
@@ -424,25 +466,62 @@ export function WorkspaceAppFrame({
424
466
  );
425
467
  }
426
468
 
427
- export function WorkspaceAppHost({ appId }: { appId?: string }) {
469
+ export function WorkspaceAppHost({
470
+ appId,
471
+ navigateToTopWindow = navigateToWorkspaceApp,
472
+ }: {
473
+ appId?: string;
474
+ navigateToTopWindow?: (href: string) => boolean | void;
475
+ }) {
428
476
  const t = useT();
429
477
  const workspaceAppsQuery = useActionQuery<WorkspaceAppSummary[]>(
430
478
  "list-workspace-apps",
431
- { includeAgentCards: false },
479
+ { includeAgentCards: false, includeArchived: true },
480
+ );
481
+ const workspaceApps = useMemo(
482
+ () => mergeChatFirstWorkspaceApps(workspaceAppsQuery.data),
483
+ [workspaceAppsQuery.data],
484
+ );
485
+ const visibleWorkspaceApps = useMemo(
486
+ () => workspaceApps.filter((item) => !item.archived),
487
+ [workspaceApps],
488
+ );
489
+ const workspaceAppIds = useMemo(
490
+ () => new Set(workspaceApps.map((item) => item.id.trim().toLowerCase())),
491
+ [workspaceApps],
492
+ );
493
+ const workspaceApp = useMemo(
494
+ () =>
495
+ visibleWorkspaceApps.find(
496
+ (item) => item.id.trim().toLowerCase() === appId?.trim().toLowerCase(),
497
+ ) ?? null,
498
+ [appId, visibleWorkspaceApps],
432
499
  );
433
500
  const grantedAppsQuery = useActionQuery<GrantedWorkspaceAppsResult>(
434
501
  "list_apps",
435
502
  {},
503
+ {
504
+ // Mounted workspace apps are already fully described by the workspace
505
+ // registry. Defer the broader MCP grant/discovery scan until that
506
+ // lookup misses; it is only needed for externally granted apps.
507
+ enabled: !workspaceAppsQuery.isLoading && !workspaceApp,
508
+ },
436
509
  );
437
510
  const apps = useMemo(() => {
438
511
  const merged = new Map<string, WorkspaceAppSummary>();
439
512
 
440
- for (const app of mergeChatFirstWorkspaceApps(workspaceAppsQuery.data)) {
513
+ for (const app of visibleWorkspaceApps) {
441
514
  merged.set(app.id.trim().toLowerCase(), app);
442
515
  }
443
516
  for (const app of grantedAppsQuery.data?.apps ?? []) {
444
517
  const id = app.id.trim();
445
- if (!id || merged.has(id.toLowerCase())) continue;
518
+ if (
519
+ !id ||
520
+ workspaceAppIds.has(id.toLowerCase()) ||
521
+ merged.has(id.toLowerCase())
522
+ ) {
523
+ continue;
524
+ }
446
525
  merged.set(id.toLowerCase(), {
447
526
  id,
448
527
  name: app.name.trim() || id,
@@ -453,7 +532,7 @@ export function WorkspaceAppHost({ appId }: { appId?: string }) {
453
532
  }
454
533
 
455
534
  return [...merged.values()];
456
- }, [grantedAppsQuery.data?.apps, workspaceAppsQuery.data]);
535
+ }, [grantedAppsQuery.data?.apps, visibleWorkspaceApps, workspaceAppIds]);
457
536
  const app = useMemo(
458
537
  () =>
459
538
  apps.find(
@@ -575,7 +654,10 @@ export function WorkspaceAppHost({ appId }: { appId?: string }) {
575
654
  className="flex h-full min-h-0 flex-col bg-background"
576
655
  >
577
656
  <div className="min-h-0 flex-1 bg-muted/20">
578
- <WorkspaceAppFrame app={app} />
657
+ <WorkspaceAppFrame
658
+ app={app}
659
+ navigateToTopWindow={navigateToTopWindow}
660
+ />
579
661
  </div>
580
662
  </div>
581
663
  );
@@ -1,6 +1,8 @@
1
- import { describe, expect, it } from "vitest";
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it } from "vitest";
2
3
 
3
4
  import {
5
+ navigateToWorkspaceApp,
4
6
  workspaceAppDirectHref,
5
7
  workspaceAppEmbedTarget,
6
8
  } from "./workspace-apps";
@@ -62,3 +64,70 @@ describe("workspaceAppDirectHref", () => {
62
64
  );
63
65
  });
64
66
  });
67
+
68
+ describe("navigateToWorkspaceApp", () => {
69
+ afterEach(() => {
70
+ Object.defineProperty(window, "parent", {
71
+ configurable: true,
72
+ value: window,
73
+ });
74
+ Object.defineProperty(window, "top", {
75
+ configurable: true,
76
+ value: window,
77
+ });
78
+ });
79
+
80
+ it("uses the top window when Dispatch is embedded", () => {
81
+ const topWindow = { location: { href: "" } } as unknown as Window;
82
+ Object.defineProperty(window, "parent", {
83
+ configurable: true,
84
+ value: {},
85
+ });
86
+ Object.defineProperty(window, "top", {
87
+ configurable: true,
88
+ value: topWindow,
89
+ });
90
+
91
+ navigateToWorkspaceApp("/mail");
92
+
93
+ expect(topWindow.location.href).toBe(
94
+ new URL("/mail", window.location.href).href,
95
+ );
96
+ });
97
+
98
+ it("reports when the embedded top window rejects navigation", () => {
99
+ const location = {} as Location;
100
+ Object.defineProperty(location, "href", {
101
+ configurable: true,
102
+ set: () => {
103
+ throw new Error("Top navigation is blocked");
104
+ },
105
+ });
106
+ const topWindow = { location } as unknown as Window;
107
+ Object.defineProperty(window, "parent", {
108
+ configurable: true,
109
+ value: {},
110
+ });
111
+ Object.defineProperty(window, "top", {
112
+ configurable: true,
113
+ value: topWindow,
114
+ });
115
+
116
+ expect(navigateToWorkspaceApp("/mail")).toBe(false);
117
+ });
118
+
119
+ it("rejects non-http navigation targets", () => {
120
+ const topWindow = { location: { href: "" } } as unknown as Window;
121
+ Object.defineProperty(window, "parent", {
122
+ configurable: true,
123
+ value: {},
124
+ });
125
+ Object.defineProperty(window, "top", {
126
+ configurable: true,
127
+ value: topWindow,
128
+ });
129
+
130
+ expect(navigateToWorkspaceApp("javascript:alert(1)")).toBe(false);
131
+ expect(topWindow.location.href).toBe("");
132
+ });
133
+ });
@@ -1,4 +1,5 @@
1
1
  import { CHAT_FIRST_DEFAULT_APP_IDS } from "@agent-native/core/client/chat-first";
2
+ import { isInBuilderFrame } from "@agent-native/core/client/host";
2
3
  import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
3
4
 
4
5
  import { CANONICAL_WORKSPACE_SSO_APP_ORIGINS } from "../shared/workspace-sso";
@@ -158,6 +159,29 @@ export function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {
158
159
  return app.status === "pending" && !!app.builderUrl;
159
160
  }
160
161
 
162
+ export function shouldOpenWorkspaceAppInTopWindow(): boolean {
163
+ if (typeof window === "undefined") return false;
164
+ return isInBuilderFrame() || window.parent !== window;
165
+ }
166
+
167
+ export function navigateToWorkspaceApp(href: string): boolean {
168
+ if (typeof window === "undefined") return false;
169
+
170
+ try {
171
+ const targetUrl = new URL(href, window.location.href);
172
+ if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
173
+ return false;
174
+ }
175
+ const targetWindow =
176
+ shouldOpenWorkspaceAppInTopWindow() && window.top ? window.top : window;
177
+ targetWindow.location.href = targetUrl.href;
178
+ return true;
179
+ } catch {
180
+ // coercion-ok: a blocked top-window assignment is an expected fallback signal.
181
+ return false;
182
+ }
183
+ }
184
+
161
185
  /**
162
186
  * Keep the chat-first rail useful before a workspace manifest is populated.
163
187
  * Mounted workspace rows still win, so custom names and routes remain the
@@ -0,0 +1,115 @@
1
+ // @vitest-environment happy-dom
2
+ import React, { act } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const routeState = vi.hoisted(() => ({
7
+ navigateToWorkspaceApp: vi.fn(() => false),
8
+ }));
9
+
10
+ vi.mock("@agent-native/core/client/api-path", () => ({
11
+ appPath: (path: string) => path,
12
+ }));
13
+
14
+ vi.mock("@agent-native/core/client/hooks", () => ({
15
+ useActionQuery: () => ({
16
+ data: [
17
+ {
18
+ id: "mail",
19
+ name: "Mail",
20
+ path: "/mail",
21
+ url: null,
22
+ status: "ready",
23
+ },
24
+ ],
25
+ isError: false,
26
+ isLoading: false,
27
+ refetch: vi.fn(),
28
+ }),
29
+ }));
30
+
31
+ vi.mock("@agent-native/core/client/i18n", () => ({
32
+ useT: () => (key: string) => key,
33
+ }));
34
+
35
+ vi.mock("@agent-native/core/shared/builder-link-tracking", () => ({
36
+ withBuilderUtmTrackingParams: (href: string) => href,
37
+ }));
38
+
39
+ vi.mock("react-router", () => ({
40
+ Link: ({ children }: { children: React.ReactNode }) => <a>{children}</a>,
41
+ Navigate: () => null,
42
+ redirect: (target: string) => target,
43
+ useParams: () => ({ appId: "mail" }),
44
+ }));
45
+
46
+ vi.mock("../../components/action-query-error", () => ({
47
+ ActionQueryError: ({ onRetry }: { onRetry: () => unknown }) => (
48
+ <button data-navigation-retry onClick={onRetry}>
49
+ retry
50
+ </button>
51
+ ),
52
+ }));
53
+
54
+ vi.mock("../../components/dispatch-shell", () => ({
55
+ DispatchShell: ({ children }: { children: React.ReactNode }) => (
56
+ <div data-dispatch-shell>{children}</div>
57
+ ),
58
+ }));
59
+
60
+ vi.mock("../../lib/catch-all-target", () => ({
61
+ resolveServerCatchAllTarget: vi.fn(),
62
+ }));
63
+
64
+ vi.mock("../../lib/workspace-apps", () => ({
65
+ navigateToWorkspaceApp: routeState.navigateToWorkspaceApp,
66
+ workspaceAppHref: (app: { path?: string | null }) => app.path ?? null,
67
+ }));
68
+
69
+ vi.mock("../../components/ui/badge", () => ({ Badge: () => null }));
70
+ vi.mock("../../components/ui/button", () => ({
71
+ Button: ({ children }: { children: React.ReactNode }) => (
72
+ <button>{children}</button>
73
+ ),
74
+ }));
75
+ vi.mock("../../components/ui/spinner", () => ({ Spinner: () => null }));
76
+
77
+ import WorkspaceAppCatchAllRoute from "./$appId";
78
+
79
+ describe("WorkspaceAppCatchAllRoute", () => {
80
+ let container: HTMLDivElement;
81
+ let root: Root;
82
+
83
+ beforeEach(() => {
84
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
85
+ routeState.navigateToWorkspaceApp.mockClear();
86
+ container = document.createElement("div");
87
+ document.body.appendChild(container);
88
+ root = createRoot(container);
89
+ });
90
+
91
+ afterEach(() => {
92
+ act(() => root.unmount());
93
+ container.remove();
94
+ vi.unstubAllGlobals();
95
+ });
96
+
97
+ it("shows a retryable error when navigation cannot reach the target", async () => {
98
+ await act(async () => {
99
+ root.render(<WorkspaceAppCatchAllRoute />);
100
+ await Promise.resolve();
101
+ });
102
+
103
+ expect(routeState.navigateToWorkspaceApp).toHaveBeenCalledWith("/mail");
104
+ expect(container.querySelector("[data-navigation-retry]")).not.toBeNull();
105
+
106
+ await act(async () => {
107
+ container
108
+ .querySelector<HTMLButtonElement>("[data-navigation-retry]")
109
+ ?.click();
110
+ await Promise.resolve();
111
+ });
112
+
113
+ expect(routeState.navigateToWorkspaceApp).toHaveBeenCalledTimes(2);
114
+ });
115
+ });
@@ -7,7 +7,7 @@ import {
7
7
  IconArrowUpRight,
8
8
  IconClockHour4,
9
9
  } from "@tabler/icons-react";
10
- import { useEffect, useMemo } from "react";
10
+ import { useEffect, useMemo, useState } from "react";
11
11
  import {
12
12
  Link,
13
13
  Navigate,
@@ -24,6 +24,7 @@ import { Button } from "../../components/ui/button";
24
24
  import { Spinner } from "../../components/ui/spinner";
25
25
  import { resolveServerCatchAllTarget } from "../../lib/catch-all-target";
26
26
  import {
27
+ navigateToWorkspaceApp,
27
28
  workspaceAppHref,
28
29
  type WorkspaceAppSummary,
29
30
  } from "../../lib/workspace-apps";
@@ -111,12 +112,18 @@ export default function WorkspaceAppCatchAllRoute() {
111
112
  );
112
113
  const href = app ? workspaceAppHref(app) : null;
113
114
  const isSelfReference = appId === "dispatch";
115
+ const hasApp = app !== null;
116
+ const appIsPending = app?.status === "pending";
117
+ const [navigationFailed, setNavigationFailed] = useState(false);
114
118
 
115
119
  useEffect(() => {
116
120
  if (isSelfReference) return;
117
- if (!app || app.status === "pending" || !href) return;
118
- window.location.assign(href);
119
- }, [app, href, isSelfReference]);
121
+ if (!hasApp || appIsPending || !href) {
122
+ setNavigationFailed(false);
123
+ return;
124
+ }
125
+ setNavigationFailed(!navigateToWorkspaceApp(href));
126
+ }, [appIsPending, hasApp, href, isSelfReference]);
120
127
 
121
128
  if (isSelfReference) {
122
129
  return <Navigate to={appPath("/overview")} replace />;
@@ -136,7 +143,23 @@ export default function WorkspaceAppCatchAllRoute() {
136
143
  );
137
144
  }
138
145
 
139
- if ((isLoading && !app) || (app && app.status !== "pending" && href)) {
146
+ if (navigationFailed && href) {
147
+ return (
148
+ <DispatchShell
149
+ title={t("dispatch.pages.dataLoadFailed")}
150
+ description={t("dispatch.pages.pageNotFoundDescription")}
151
+ >
152
+ <ActionQueryError
153
+ onRetry={() => setNavigationFailed(!navigateToWorkspaceApp(href))}
154
+ />
155
+ </DispatchShell>
156
+ );
157
+ }
158
+
159
+ if (
160
+ (isLoading && !app) ||
161
+ (app && app.status !== "pending" && href && !navigationFailed)
162
+ ) {
140
163
  return (
141
164
  <div className="flex h-screen w-full items-center justify-center">
142
165
  <Spinner className="size-8" />
@@ -0,0 +1,66 @@
1
+ // @vitest-environment happy-dom
2
+ import React, { act } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const routeState = vi.hoisted(() => ({
7
+ appId: "mail",
8
+ hostProps: null as {
9
+ appId?: string;
10
+ navigateToTopWindow?: (href: string) => boolean | void;
11
+ } | null,
12
+ navigateToWorkspaceApp: vi.fn(() => true),
13
+ }));
14
+
15
+ vi.mock("react-router", () => ({
16
+ useParams: () => ({ appId: routeState.appId }),
17
+ }));
18
+
19
+ vi.mock("../../components/workspace-app-host", () => ({
20
+ WorkspaceAppHost: (props: typeof routeState.hostProps) => {
21
+ routeState.hostProps = props;
22
+ return <div data-workspace-app-id={props?.appId} />;
23
+ },
24
+ }));
25
+
26
+ vi.mock("../../lib/workspace-apps", () => ({
27
+ navigateToWorkspaceApp: routeState.navigateToWorkspaceApp,
28
+ }));
29
+
30
+ import WorkspaceAppRoute from "./apps.$appId";
31
+
32
+ describe("WorkspaceAppRoute", () => {
33
+ let container: HTMLDivElement;
34
+ let root: Root;
35
+
36
+ beforeEach(() => {
37
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
38
+ routeState.appId = "mail";
39
+ routeState.hostProps = null;
40
+ routeState.navigateToWorkspaceApp.mockClear();
41
+ container = document.createElement("div");
42
+ document.body.appendChild(container);
43
+ root = createRoot(container);
44
+ });
45
+
46
+ afterEach(() => {
47
+ act(() => root.unmount());
48
+ container.remove();
49
+ vi.unstubAllGlobals();
50
+ });
51
+
52
+ it("passes the route app id and top-window helper to the workspace host", async () => {
53
+ await act(async () => {
54
+ root.render(<WorkspaceAppRoute />);
55
+ });
56
+
57
+ expect(routeState.hostProps?.appId).toBe("mail");
58
+ expect(routeState.hostProps?.navigateToTopWindow).toEqual(
59
+ expect.any(Function),
60
+ );
61
+
62
+ routeState.hostProps?.navigateToTopWindow?.("/mail");
63
+
64
+ expect(routeState.navigateToWorkspaceApp).toHaveBeenCalledWith("/mail");
65
+ });
66
+ });
@@ -1,6 +1,11 @@
1
1
  import { useParams } from "react-router";
2
2
 
3
3
  import { WorkspaceAppHost } from "../../components/workspace-app-host";
4
+ import { navigateToWorkspaceApp } from "../../lib/workspace-apps";
5
+
6
+ export function navigateWorkspaceAppFromRoute(href: string): boolean {
7
+ return navigateToWorkspaceApp(href);
8
+ }
4
9
 
5
10
  export function meta() {
6
11
  return [{ title: "Workspace app - Dispatch" }];
@@ -8,5 +13,10 @@ export function meta() {
8
13
 
9
14
  export default function WorkspaceAppRoute() {
10
15
  const { appId } = useParams();
11
- return <WorkspaceAppHost appId={appId} />;
16
+ return (
17
+ <WorkspaceAppHost
18
+ appId={appId}
19
+ navigateToTopWindow={navigateWorkspaceAppFromRoute}
20
+ />
21
+ );
12
22
  }