@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
@@ -4,7 +4,7 @@ import { useActionQuery } from "@agent-native/core/client/hooks";
4
4
  import { useT } from "@agent-native/core/client/i18n";
5
5
  import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
6
6
  import { IconArrowLeft, IconArrowUpRight, IconClockHour4, } from "@tabler/icons-react";
7
- import { useEffect, useMemo } from "react";
7
+ import { useEffect, useMemo, useState } from "react";
8
8
  import { Link, Navigate, redirect, useParams, } from "react-router";
9
9
  import { ActionQueryError } from "../../components/action-query-error.js";
10
10
  import { DispatchShell } from "../../components/dispatch-shell.js";
@@ -12,7 +12,7 @@ import { Badge } from "../../components/ui/badge.js";
12
12
  import { Button } from "../../components/ui/button.js";
13
13
  import { Spinner } from "../../components/ui/spinner.js";
14
14
  import { resolveServerCatchAllTarget } from "../../lib/catch-all-target.js";
15
- import { workspaceAppHref, } from "../../lib/workspace-apps.js";
15
+ import { navigateToWorkspaceApp, workspaceAppHref, } from "../../lib/workspace-apps.js";
16
16
  export function meta() {
17
17
  return [{ title: "Workspace app - Dispatch" }];
18
18
  }
@@ -90,20 +90,29 @@ export default function WorkspaceAppCatchAllRoute() {
90
90
  const app = useMemo(() => apps.find((item) => item.id === appId) ?? null, [appId, apps]);
91
91
  const href = app ? workspaceAppHref(app) : null;
92
92
  const isSelfReference = appId === "dispatch";
93
+ const hasApp = app !== null;
94
+ const appIsPending = app?.status === "pending";
95
+ const [navigationFailed, setNavigationFailed] = useState(false);
93
96
  useEffect(() => {
94
97
  if (isSelfReference)
95
98
  return;
96
- if (!app || app.status === "pending" || !href)
99
+ if (!hasApp || appIsPending || !href) {
100
+ setNavigationFailed(false);
97
101
  return;
98
- window.location.assign(href);
99
- }, [app, href, isSelfReference]);
102
+ }
103
+ setNavigationFailed(!navigateToWorkspaceApp(href));
104
+ }, [appIsPending, hasApp, href, isSelfReference]);
100
105
  if (isSelfReference) {
101
106
  return _jsx(Navigate, { to: appPath("/overview"), replace: true });
102
107
  }
103
108
  if (appsQuery.isError) {
104
109
  return (_jsx(DispatchShell, { title: t("dispatch.pages.dataLoadFailed"), description: t("dispatch.pages.pageNotFoundDescription"), children: _jsx(ActionQueryError, { error: appsQuery.error, onRetry: () => void appsQuery.refetch() }) }));
105
110
  }
106
- if ((isLoading && !app) || (app && app.status !== "pending" && href)) {
111
+ if (navigationFailed && href) {
112
+ return (_jsx(DispatchShell, { title: t("dispatch.pages.dataLoadFailed"), description: t("dispatch.pages.pageNotFoundDescription"), children: _jsx(ActionQueryError, { onRetry: () => setNavigationFailed(!navigateToWorkspaceApp(href)) }) }));
113
+ }
114
+ if ((isLoading && !app) ||
115
+ (app && app.status !== "pending" && href && !navigationFailed)) {
107
116
  return (_jsx("div", { className: "flex h-screen w-full items-center justify-center", children: _jsx(Spinner, { className: "size-8" }) }));
108
117
  }
109
118
  return (_jsx(DispatchShell, { title: app?.name || t("dispatch.pages.pageNotFound"), description: t("dispatch.pages.pageNotFoundDescription"), children: _jsxs("div", { className: "max-w-2xl rounded-lg bg-card p-5", children: [_jsx(Button, { asChild: true, size: "sm", variant: "ghost", className: "-ml-2 mb-4", children: _jsxs(Link, { to: appPath("/overview"), children: [_jsx(IconArrowLeft, { size: 15, className: "mr-1.5" }), t("dispatch.nav.overview")] }) }), app?.status === "pending" ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("h2", { className: "text-base font-semibold text-foreground", children: app.name }), _jsxs(Badge, { variant: "outline", className: "gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300", children: [_jsx(IconClockHour4, { size: 12 }), t("dispatch.pages.building")] })] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [t("dispatch.pages.appBuildingPrefix"), " ", _jsx("span", { className: "font-mono text-foreground", children: app.path }), " ", t("dispatch.pages.appBuildingSuffix")] }), app.builderUrl ? (_jsx(Button, { asChild: true, children: _jsxs("a", { href: withBuilderUtmTrackingParams(app.builderUrl, {
@@ -1 +1 @@
1
- {"version":3,"file":"$appId.js","sourceRoot":"","sources":["../../../src/routes/pages/$appId.tsx"],"names":[],"mappings":"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","sourcesContent":["import { appPath } from \"@agent-native/core/client/api-path\";\nimport { useActionQuery } from \"@agent-native/core/client/hooks\";\nimport { useT } from \"@agent-native/core/client/i18n\";\nimport { withBuilderUtmTrackingParams } from \"@agent-native/core/shared/builder-link-tracking\";\nimport {\n IconArrowLeft,\n IconArrowUpRight,\n IconClockHour4,\n} from \"@tabler/icons-react\";\nimport { useEffect, useMemo } from \"react\";\nimport {\n Link,\n Navigate,\n redirect,\n useParams,\n type ClientLoaderFunctionArgs,\n type LoaderFunctionArgs,\n} from \"react-router\";\n\nimport { ActionQueryError } from \"../../components/action-query-error\";\nimport { DispatchShell } from \"../../components/dispatch-shell\";\nimport { Badge } from \"../../components/ui/badge\";\nimport { Button } from \"../../components/ui/button\";\nimport { Spinner } from \"../../components/ui/spinner\";\nimport { resolveServerCatchAllTarget } from \"../../lib/catch-all-target\";\nimport {\n workspaceAppHref,\n type WorkspaceAppSummary,\n} from \"../../lib/workspace-apps\";\n\nexport function meta() {\n return [{ title: \"Workspace app - Dispatch\" }];\n}\n\n/**\n * Catch-all for `/dispatch/<segment>` paths that don't match an explicit\n * Dispatch route. When `<segment>` is the id of a workspace app sibling\n * (e.g. `/dispatch/todo` after Builder.io routes a \"navigate to /todo\"\n * call through Dispatch's mount point), bounce to the absolute `/<appId>`\n * so the user lands on the actual app instead of a 404 inside Dispatch.\n *\n * Server-side redirect: we resolve the workspace app manifest via the\n * shared `loadWorkspaceAppsManifest()` helper, which checks the\n * `AGENT_NATIVE_WORKSPACE_APPS_JSON` env var, then the\n * `.agent-native/workspace-apps.json` file written by `workspace-deploy.ts`,\n * then a live filesystem scan of `apps/` for local dev. We then throw\n * `redirect(\"/<appId>\")`. React Router 7 does not prepend the basename to\n * absolute paths returned from a loader, so the redirect escapes Dispatch's\n * `/dispatch` mount cleanly.\n *\n * Why a catch-all instead of fixing the agent prompt: Builder.io currently\n * resolves \"navigate to /todo\" relative to Dispatch's mount, sending the\n * user to /dispatch/todo. The same wrong path then gets captured as the\n * OAuth callbackURL, so Google sign-in completes back at /dispatch/todo\n * and looks broken. This route fixes both the post-creation navigation\n * and the OAuth round-trip from a single place.\n *\n * Built-in template fallback: when no workspace manifest is available\n * (framework dev with each template on its own port, hosted dispatch with\n * no sibling apps), redirect to the matching first-party template's deploy\n * URL — `http://localhost:<devPort>` in dev, `https://<id>.agent-native.com`\n * in production. Without this, a user visiting `/forms` on dispatch is\n * forced to sign in (auth guard) and then lands on this route's \"Page not\n * found\" pane after the post-login reload.\n *\n * `appId === \"dispatch\"` short-circuit: when the segment matches Dispatch\n * itself (e.g. `/dispatch/dispatch`), we go straight to the overview rather\n * than chaining through `/dispatch` (which polled `useActionQuery` re-fired\n * `window.location.assign` against and looped forever in production).\n */\nfunction dispatchSelfRedirect(appId: string | undefined): string | null {\n if (appId === \"dispatch\") return appPath(\"/overview\");\n return null;\n}\n\nexport async function loader({ params }: LoaderFunctionArgs) {\n const appId = params.appId;\n if (!appId) return null;\n const selfTarget = dispatchSelfRedirect(appId);\n if (selfTarget) throw redirect(selfTarget);\n const target = await resolveServerCatchAllTarget(appId);\n if (target) throw redirect(target);\n return null;\n}\n\nexport async function clientLoader({\n params,\n serverLoader,\n}: ClientLoaderFunctionArgs) {\n const selfTarget = dispatchSelfRedirect(params.appId);\n if (selfTarget) throw redirect(selfTarget);\n // Defer to the server loader so the built-in template fallback runs on\n // SPA navigations too (e.g. clicking a `/<template-id>` link inside\n // dispatch). Without this the client side would only check the workspace\n // apps query, which never lists the static first-party templates and so\n // the user would land on the \"Page not found\" pane.\n return serverLoader();\n}\n\nexport default function WorkspaceAppCatchAllRoute() {\n const t = useT();\n const { appId } = useParams();\n const appsQuery = useActionQuery(\"list-workspace-apps\", {\n includeAgentCards: false,\n });\n const { data: apps = [], isLoading } = appsQuery;\n const app = useMemo(\n () =>\n (apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,\n [appId, apps],\n );\n const href = app ? workspaceAppHref(app) : null;\n const isSelfReference = appId === \"dispatch\";\n\n useEffect(() => {\n if (isSelfReference) return;\n if (!app || app.status === \"pending\" || !href) return;\n window.location.assign(href);\n }, [app, href, isSelfReference]);\n\n if (isSelfReference) {\n return <Navigate to={appPath(\"/overview\")} replace />;\n }\n\n if (appsQuery.isError) {\n return (\n <DispatchShell\n title={t(\"dispatch.pages.dataLoadFailed\")}\n description={t(\"dispatch.pages.pageNotFoundDescription\")}\n >\n <ActionQueryError\n error={appsQuery.error}\n onRetry={() => void appsQuery.refetch()}\n />\n </DispatchShell>\n );\n }\n\n if ((isLoading && !app) || (app && app.status !== \"pending\" && href)) {\n return (\n <div className=\"flex h-screen w-full items-center justify-center\">\n <Spinner className=\"size-8\" />\n </div>\n );\n }\n\n return (\n <DispatchShell\n title={app?.name || t(\"dispatch.pages.pageNotFound\")}\n description={t(\"dispatch.pages.pageNotFoundDescription\")}\n >\n <div className=\"max-w-2xl rounded-lg bg-card p-5\">\n <Button asChild size=\"sm\" variant=\"ghost\" className=\"-ml-2 mb-4\">\n <Link to={appPath(\"/overview\")}>\n <IconArrowLeft size={15} className=\"mr-1.5\" />\n {t(\"dispatch.nav.overview\")}\n </Link>\n </Button>\n\n {app?.status === \"pending\" ? (\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-base font-semibold text-foreground\">\n {app.name}\n </h2>\n <Badge\n variant=\"outline\"\n className=\"gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300\"\n >\n <IconClockHour4 size={12} />\n {t(\"dispatch.pages.building\")}\n </Badge>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"dispatch.pages.appBuildingPrefix\")}{\" \"}\n <span className=\"font-mono text-foreground\">{app.path}</span>{\" \"}\n {t(\"dispatch.pages.appBuildingSuffix\")}\n </p>\n {app.builderUrl ? (\n <Button asChild>\n <a\n href={withBuilderUtmTrackingParams(app.builderUrl, {\n campaign: \"product\",\n content: \"dispatch_branch\",\n })}\n target=\"_blank\"\n rel=\"noreferrer\"\n >\n {t(\"dispatch.pages.openBuilderBranch\", {\n defaultValue: \"Open in Builder\",\n })}\n <IconArrowUpRight size={15} className=\"ml-1.5\" />\n </a>\n </Button>\n ) : null}\n </div>\n ) : (\n <div className=\"space-y-3\">\n <h2 className=\"text-base font-semibold text-foreground\">\n {t(\"dispatch.pages.pageNotFound\")}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n <span className=\"font-mono text-foreground\">/{appId}</span> isn't\n {t(\"dispatch.pages.notDispatchOrWorkspaceApp\")}\n </p>\n <Button asChild>\n <Link to={appPath(\"/apps\")}>\n {t(\"dispatch.pages.browseApps\")}\n </Link>\n </Button>\n </div>\n )}\n </div>\n </DispatchShell>\n );\n}\n"]}
1
+ {"version":3,"file":"$appId.js","sourceRoot":"","sources":["../../../src/routes/pages/$appId.tsx"],"names":[],"mappings":"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","sourcesContent":["import { appPath } from \"@agent-native/core/client/api-path\";\nimport { useActionQuery } from \"@agent-native/core/client/hooks\";\nimport { useT } from \"@agent-native/core/client/i18n\";\nimport { withBuilderUtmTrackingParams } from \"@agent-native/core/shared/builder-link-tracking\";\nimport {\n IconArrowLeft,\n IconArrowUpRight,\n IconClockHour4,\n} from \"@tabler/icons-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n Link,\n Navigate,\n redirect,\n useParams,\n type ClientLoaderFunctionArgs,\n type LoaderFunctionArgs,\n} from \"react-router\";\n\nimport { ActionQueryError } from \"../../components/action-query-error\";\nimport { DispatchShell } from \"../../components/dispatch-shell\";\nimport { Badge } from \"../../components/ui/badge\";\nimport { Button } from \"../../components/ui/button\";\nimport { Spinner } from \"../../components/ui/spinner\";\nimport { resolveServerCatchAllTarget } from \"../../lib/catch-all-target\";\nimport {\n navigateToWorkspaceApp,\n workspaceAppHref,\n type WorkspaceAppSummary,\n} from \"../../lib/workspace-apps\";\n\nexport function meta() {\n return [{ title: \"Workspace app - Dispatch\" }];\n}\n\n/**\n * Catch-all for `/dispatch/<segment>` paths that don't match an explicit\n * Dispatch route. When `<segment>` is the id of a workspace app sibling\n * (e.g. `/dispatch/todo` after Builder.io routes a \"navigate to /todo\"\n * call through Dispatch's mount point), bounce to the absolute `/<appId>`\n * so the user lands on the actual app instead of a 404 inside Dispatch.\n *\n * Server-side redirect: we resolve the workspace app manifest via the\n * shared `loadWorkspaceAppsManifest()` helper, which checks the\n * `AGENT_NATIVE_WORKSPACE_APPS_JSON` env var, then the\n * `.agent-native/workspace-apps.json` file written by `workspace-deploy.ts`,\n * then a live filesystem scan of `apps/` for local dev. We then throw\n * `redirect(\"/<appId>\")`. React Router 7 does not prepend the basename to\n * absolute paths returned from a loader, so the redirect escapes Dispatch's\n * `/dispatch` mount cleanly.\n *\n * Why a catch-all instead of fixing the agent prompt: Builder.io currently\n * resolves \"navigate to /todo\" relative to Dispatch's mount, sending the\n * user to /dispatch/todo. The same wrong path then gets captured as the\n * OAuth callbackURL, so Google sign-in completes back at /dispatch/todo\n * and looks broken. This route fixes both the post-creation navigation\n * and the OAuth round-trip from a single place.\n *\n * Built-in template fallback: when no workspace manifest is available\n * (framework dev with each template on its own port, hosted dispatch with\n * no sibling apps), redirect to the matching first-party template's deploy\n * URL — `http://localhost:<devPort>` in dev, `https://<id>.agent-native.com`\n * in production. Without this, a user visiting `/forms` on dispatch is\n * forced to sign in (auth guard) and then lands on this route's \"Page not\n * found\" pane after the post-login reload.\n *\n * `appId === \"dispatch\"` short-circuit: when the segment matches Dispatch\n * itself (e.g. `/dispatch/dispatch`), we go straight to the overview rather\n * than chaining through `/dispatch` (which polled `useActionQuery` re-fired\n * `window.location.assign` against and looped forever in production).\n */\nfunction dispatchSelfRedirect(appId: string | undefined): string | null {\n if (appId === \"dispatch\") return appPath(\"/overview\");\n return null;\n}\n\nexport async function loader({ params }: LoaderFunctionArgs) {\n const appId = params.appId;\n if (!appId) return null;\n const selfTarget = dispatchSelfRedirect(appId);\n if (selfTarget) throw redirect(selfTarget);\n const target = await resolveServerCatchAllTarget(appId);\n if (target) throw redirect(target);\n return null;\n}\n\nexport async function clientLoader({\n params,\n serverLoader,\n}: ClientLoaderFunctionArgs) {\n const selfTarget = dispatchSelfRedirect(params.appId);\n if (selfTarget) throw redirect(selfTarget);\n // Defer to the server loader so the built-in template fallback runs on\n // SPA navigations too (e.g. clicking a `/<template-id>` link inside\n // dispatch). Without this the client side would only check the workspace\n // apps query, which never lists the static first-party templates and so\n // the user would land on the \"Page not found\" pane.\n return serverLoader();\n}\n\nexport default function WorkspaceAppCatchAllRoute() {\n const t = useT();\n const { appId } = useParams();\n const appsQuery = useActionQuery(\"list-workspace-apps\", {\n includeAgentCards: false,\n });\n const { data: apps = [], isLoading } = appsQuery;\n const app = useMemo(\n () =>\n (apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,\n [appId, apps],\n );\n const href = app ? workspaceAppHref(app) : null;\n const isSelfReference = appId === \"dispatch\";\n const hasApp = app !== null;\n const appIsPending = app?.status === \"pending\";\n const [navigationFailed, setNavigationFailed] = useState(false);\n\n useEffect(() => {\n if (isSelfReference) return;\n if (!hasApp || appIsPending || !href) {\n setNavigationFailed(false);\n return;\n }\n setNavigationFailed(!navigateToWorkspaceApp(href));\n }, [appIsPending, hasApp, href, isSelfReference]);\n\n if (isSelfReference) {\n return <Navigate to={appPath(\"/overview\")} replace />;\n }\n\n if (appsQuery.isError) {\n return (\n <DispatchShell\n title={t(\"dispatch.pages.dataLoadFailed\")}\n description={t(\"dispatch.pages.pageNotFoundDescription\")}\n >\n <ActionQueryError\n error={appsQuery.error}\n onRetry={() => void appsQuery.refetch()}\n />\n </DispatchShell>\n );\n }\n\n if (navigationFailed && href) {\n return (\n <DispatchShell\n title={t(\"dispatch.pages.dataLoadFailed\")}\n description={t(\"dispatch.pages.pageNotFoundDescription\")}\n >\n <ActionQueryError\n onRetry={() => setNavigationFailed(!navigateToWorkspaceApp(href))}\n />\n </DispatchShell>\n );\n }\n\n if (\n (isLoading && !app) ||\n (app && app.status !== \"pending\" && href && !navigationFailed)\n ) {\n return (\n <div className=\"flex h-screen w-full items-center justify-center\">\n <Spinner className=\"size-8\" />\n </div>\n );\n }\n\n return (\n <DispatchShell\n title={app?.name || t(\"dispatch.pages.pageNotFound\")}\n description={t(\"dispatch.pages.pageNotFoundDescription\")}\n >\n <div className=\"max-w-2xl rounded-lg bg-card p-5\">\n <Button asChild size=\"sm\" variant=\"ghost\" className=\"-ml-2 mb-4\">\n <Link to={appPath(\"/overview\")}>\n <IconArrowLeft size={15} className=\"mr-1.5\" />\n {t(\"dispatch.nav.overview\")}\n </Link>\n </Button>\n\n {app?.status === \"pending\" ? (\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-base font-semibold text-foreground\">\n {app.name}\n </h2>\n <Badge\n variant=\"outline\"\n className=\"gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300\"\n >\n <IconClockHour4 size={12} />\n {t(\"dispatch.pages.building\")}\n </Badge>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"dispatch.pages.appBuildingPrefix\")}{\" \"}\n <span className=\"font-mono text-foreground\">{app.path}</span>{\" \"}\n {t(\"dispatch.pages.appBuildingSuffix\")}\n </p>\n {app.builderUrl ? (\n <Button asChild>\n <a\n href={withBuilderUtmTrackingParams(app.builderUrl, {\n campaign: \"product\",\n content: \"dispatch_branch\",\n })}\n target=\"_blank\"\n rel=\"noreferrer\"\n >\n {t(\"dispatch.pages.openBuilderBranch\", {\n defaultValue: \"Open in Builder\",\n })}\n <IconArrowUpRight size={15} className=\"ml-1.5\" />\n </a>\n </Button>\n ) : null}\n </div>\n ) : (\n <div className=\"space-y-3\">\n <h2 className=\"text-base font-semibold text-foreground\">\n {t(\"dispatch.pages.pageNotFound\")}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n <span className=\"font-mono text-foreground\">/{appId}</span> isn't\n {t(\"dispatch.pages.notDispatchOrWorkspaceApp\")}\n </p>\n <Button asChild>\n <Link to={appPath(\"/apps\")}>\n {t(\"dispatch.pages.browseApps\")}\n </Link>\n </Button>\n </div>\n )}\n </div>\n </DispatchShell>\n );\n}\n"]}
@@ -1,3 +1,4 @@
1
+ export declare function navigateWorkspaceAppFromRoute(href: string): boolean;
1
2
  export declare function meta(): {
2
3
  title: string;
3
4
  }[];
@@ -1 +1 @@
1
- {"version":3,"file":"apps.$appId.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/apps.$appId.tsx"],"names":[],"mappings":"AAIA,wBAAgB,IAAI;;IAEnB;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB,oDAGxC"}
1
+ {"version":3,"file":"apps.$appId.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/apps.$appId.tsx"],"names":[],"mappings":"AAKA,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wBAAgB,IAAI;;IAEnB;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB,oDAQxC"}
@@ -1,11 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useParams } from "react-router";
3
3
  import { WorkspaceAppHost } from "../../components/workspace-app-host.js";
4
+ import { navigateToWorkspaceApp } from "../../lib/workspace-apps.js";
5
+ export function navigateWorkspaceAppFromRoute(href) {
6
+ return navigateToWorkspaceApp(href);
7
+ }
4
8
  export function meta() {
5
9
  return [{ title: "Workspace app - Dispatch" }];
6
10
  }
7
11
  export default function WorkspaceAppRoute() {
8
12
  const { appId } = useParams();
9
- return _jsx(WorkspaceAppHost, { appId: appId });
13
+ return (_jsx(WorkspaceAppHost, { appId: appId, navigateToTopWindow: navigateWorkspaceAppFromRoute }));
10
14
  }
11
15
  //# sourceMappingURL=apps.$appId.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"apps.$appId.js","sourceRoot":"","sources":["../../../src/routes/pages/apps.$appId.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAEvE,MAAM,UAAU,IAAI;IAClB,OAAO,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB;IACvC,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;IAC9B,OAAO,KAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,GAAI,CAAC;AAC5C,CAAC","sourcesContent":["import { useParams } from \"react-router\";\n\nimport { WorkspaceAppHost } from \"../../components/workspace-app-host\";\n\nexport function meta() {\n return [{ title: \"Workspace app - Dispatch\" }];\n}\n\nexport default function WorkspaceAppRoute() {\n const { appId } = useParams();\n return <WorkspaceAppHost appId={appId} />;\n}\n"]}
1
+ {"version":3,"file":"apps.$appId.js","sourceRoot":"","sources":["../../../src/routes/pages/apps.$appId.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAElE,MAAM,UAAU,6BAA6B,CAAC,IAAY;IACxD,OAAO,sBAAsB,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,IAAI;IAClB,OAAO,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB;IACvC,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;IAC9B,OAAO,CACL,KAAC,gBAAgB,IACf,KAAK,EAAE,KAAK,EACZ,mBAAmB,EAAE,6BAA6B,GAClD,CACH,CAAC;AACJ,CAAC","sourcesContent":["import { useParams } from \"react-router\";\n\nimport { WorkspaceAppHost } from \"../../components/workspace-app-host\";\nimport { navigateToWorkspaceApp } from \"../../lib/workspace-apps\";\n\nexport function navigateWorkspaceAppFromRoute(href: string): boolean {\n return navigateToWorkspaceApp(href);\n}\n\nexport function meta() {\n return [{ title: \"Workspace app - Dispatch\" }];\n}\n\nexport default function WorkspaceAppRoute() {\n const { appId } = useParams();\n return (\n <WorkspaceAppHost\n appId={appId}\n navigateToTopWindow={navigateWorkspaceAppFromRoute}\n />\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.29.1",
3
+ "version": "0.29.3",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -101,7 +101,7 @@
101
101
  "typescript-7": "npm:typescript@^7.0.2",
102
102
  "vite": "8.1.0",
103
103
  "vitest": "^4.1.5",
104
- "@agent-native/core": "0.161.14"
104
+ "@agent-native/core": "0.161.17"
105
105
  },
106
106
  "peerDependencies": {
107
107
  "@agent-native/core": ">=0.8.0",
@@ -20,6 +20,9 @@ export default defineAction({
20
20
  .describe(
21
21
  "Fetch each ready app's /.well-known/agent-card.json with a short non-throwing timeout and include agentCardUrl, agentCardReachable, a2aEndpointUrl, agentName, and agentSkillsCount. Defaults to true for agent calls; UI polling should pass false. Pending Builder apps are not probed.",
22
22
  ),
23
+ includeArchived: httpBoolean
24
+ .default(false)
25
+ .describe("Include archived workspace apps in the result."),
23
26
  audience: z
24
27
  .enum(["all", "internal", "public"])
25
28
  .default("all")
@@ -1403,7 +1403,7 @@ export function Layout({
1403
1403
  const chatFirstAppLayoutHydratedRef = useRef(false);
1404
1404
  const chatFirstAppsQuery = useActionQuery<WorkspaceAppSummary[]>(
1405
1405
  "list-workspace-apps",
1406
- { includeAgentCards: false },
1406
+ { includeAgentCards: false, includeArchived: true },
1407
1407
  { enabled: chatFirstMode },
1408
1408
  );
1409
1409
  const chatFirstGrantedAppsQuery = useActionQuery<ChatFirstGrantedAppsResult>(
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { act } from "react";
4
4
  import { createRoot, type Root } from "react-dom/client";
5
+ import { MemoryRouter } from "react-router";
5
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
7
 
7
8
  const clientState = vi.hoisted(() => {
@@ -13,8 +14,19 @@ const clientState = vi.hoisted(() => {
13
14
  }));
14
15
  const legacyErrorMutateAsync = vi.fn();
15
16
  const actionNames: string[] = [];
17
+ const actionQueryParams: Array<{
18
+ name: string;
19
+ params?: unknown;
20
+ }> = [];
21
+ const actionQueryOptions: Array<{
22
+ name: string;
23
+ options?: Record<string, unknown>;
24
+ }> = [];
16
25
  return {
17
26
  actionNames,
27
+ actionQueryParams,
28
+ actionQueryOptions,
29
+ workspaceApps: null as Array<Record<string, unknown>> | null,
18
30
  grantedApps: [
19
31
  {
20
32
  id: "analytics.agent-native.com",
@@ -25,6 +37,7 @@ const clientState = vi.hoisted(() => {
25
37
  legacyMutateAsync,
26
38
  legacyMutateError: null as Error | null,
27
39
  legacyErrorMutateAsync,
40
+ inBuilderFrame: false,
28
41
  frameLoadHandler: null as (() => void) | null,
29
42
  suppressFrameLoad: false,
30
43
  theme: "dark" as "dark" | "light",
@@ -81,6 +94,10 @@ vi.mock("@agent-native/core/client/feature-flags", () => ({
81
94
  useFeatureFlag: () => clientState.workspaceSsoEnabled,
82
95
  }));
83
96
 
97
+ vi.mock("@agent-native/core/client/host", () => ({
98
+ isInBuilderFrame: () => clientState.inBuilderFrame,
99
+ }));
100
+
84
101
  vi.mock("@agent-native/core/client/hooks", () => ({
85
102
  useActionMutation: (name: string) => {
86
103
  clientState.actionNames.push(name);
@@ -93,44 +110,54 @@ vi.mock("@agent-native/core/client/hooks", () => ({
93
110
  : clientState.legacyMutateAsync,
94
111
  };
95
112
  },
96
- useActionQuery: (name: string) => ({
97
- data:
98
- name === "list_apps"
99
- ? { apps: clientState.grantedApps }
100
- : [
101
- {
102
- id: "mail",
103
- name: "Mail",
104
- path: "/mail",
105
- url: null,
106
- status: "ready",
107
- },
108
- {
109
- id: "calendar",
110
- name: "Calendar",
111
- path: "/calendar",
112
- url: null,
113
- status: "ready",
114
- },
115
- {
116
- id: "documents",
117
- name: "Documents",
118
- path: "/documents",
119
- url: null,
120
- status: "ready",
121
- },
122
- {
123
- id: "settings",
124
- name: "Settings",
125
- path: "/settings",
126
- url: null,
127
- status: "ready",
128
- },
129
- ],
130
- isError: false,
131
- isLoading: false,
132
- refetch: vi.fn(),
133
- }),
113
+ useActionQuery: (name: string, params?: unknown, options?: unknown) => {
114
+ clientState.actionQueryParams.push({ name, params });
115
+ clientState.actionQueryOptions.push({
116
+ name,
117
+ options:
118
+ options && typeof options === "object"
119
+ ? (options as Record<string, unknown>)
120
+ : undefined,
121
+ });
122
+ return {
123
+ data:
124
+ name === "list_apps"
125
+ ? { apps: clientState.grantedApps }
126
+ : (clientState.workspaceApps ?? [
127
+ {
128
+ id: "mail",
129
+ name: "Mail",
130
+ path: "/mail",
131
+ url: null,
132
+ status: "ready",
133
+ },
134
+ {
135
+ id: "calendar",
136
+ name: "Calendar",
137
+ path: "/calendar",
138
+ url: null,
139
+ status: "ready",
140
+ },
141
+ {
142
+ id: "documents",
143
+ name: "Documents",
144
+ path: "/documents",
145
+ url: null,
146
+ status: "ready",
147
+ },
148
+ {
149
+ id: "settings",
150
+ name: "Settings",
151
+ path: "/settings",
152
+ url: null,
153
+ status: "ready",
154
+ },
155
+ ]),
156
+ isError: false,
157
+ isLoading: false,
158
+ refetch: vi.fn(),
159
+ };
160
+ },
134
161
  }));
135
162
 
136
163
  vi.mock("@agent-native/core/client/i18n", () => ({
@@ -153,9 +180,13 @@ describe("WorkspaceAppKeepAlive", () => {
153
180
  document.body.appendChild(container);
154
181
  root = createRoot(container);
155
182
  clientState.actionNames.length = 0;
183
+ clientState.actionQueryParams.length = 0;
184
+ clientState.actionQueryOptions.length = 0;
185
+ clientState.workspaceApps = null;
156
186
  clientState.legacyMutateAsync.mockClear();
157
187
  clientState.legacyMutateError = null;
158
188
  clientState.legacyErrorMutateAsync.mockReset();
189
+ clientState.inBuilderFrame = false;
159
190
  clientState.frameLoadHandler = null;
160
191
  clientState.suppressFrameLoad = false;
161
192
  clientState.workspaceSsoMutateAsync.mockClear();
@@ -166,6 +197,14 @@ describe("WorkspaceAppKeepAlive", () => {
166
197
  afterEach(() => {
167
198
  act(() => root.unmount());
168
199
  container.remove();
200
+ Object.defineProperty(window, "parent", {
201
+ configurable: true,
202
+ value: window,
203
+ });
204
+ Object.defineProperty(window, "top", {
205
+ configurable: true,
206
+ value: window,
207
+ });
169
208
  vi.unstubAllGlobals();
170
209
  });
171
210
 
@@ -198,6 +237,15 @@ describe("WorkspaceAppKeepAlive", () => {
198
237
  expect(calendarEntry?.classList.contains("hidden")).toBe(false);
199
238
  expect(calendarEntry?.querySelector("iframe")).not.toBeNull();
200
239
  expect(container.querySelectorAll("iframe")).toHaveLength(2);
240
+ expect(
241
+ clientState.actionQueryParams.find(
242
+ (query) => query.name === "list-workspace-apps",
243
+ )?.params,
244
+ ).toEqual({ includeAgentCards: false, includeArchived: true });
245
+ expect(
246
+ clientState.actionQueryOptions.find((query) => query.name === "list_apps")
247
+ ?.options?.enabled,
248
+ ).toBe(false);
201
249
  });
202
250
 
203
251
  it("resolves a granted external app instead of showing app not found", async () => {
@@ -222,6 +270,38 @@ describe("WorkspaceAppKeepAlive", () => {
222
270
  url: "https://analytics.agent-native.com",
223
271
  chrome: "minimal",
224
272
  });
273
+ expect(
274
+ clientState.actionQueryOptions.find((query) => query.name === "list_apps")
275
+ ?.options?.enabled,
276
+ ).toBe(true);
277
+ });
278
+
279
+ it("does not resolve archived workspace apps from granted discovery", async () => {
280
+ clientState.workspaceApps = [
281
+ {
282
+ id: "analytics.agent-native.com",
283
+ name: "Hidden app",
284
+ path: "/hidden-app",
285
+ url: null,
286
+ status: "ready",
287
+ archived: true,
288
+ },
289
+ ];
290
+
291
+ await act(async () => {
292
+ root.render(
293
+ <MemoryRouter>
294
+ <WorkspaceAppKeepAlive activeAppId="analytics.agent-native.com" />
295
+ </MemoryRouter>,
296
+ );
297
+ await Promise.resolve();
298
+ await Promise.resolve();
299
+ });
300
+
301
+ expect(
302
+ container.querySelector('[data-chat-first-app-status="ready"]'),
303
+ ).toBeNull();
304
+ expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
225
305
  });
226
306
 
227
307
  it("uses the app-scoped workspace session action when the rollout is enabled", async () => {
@@ -354,6 +434,110 @@ describe("WorkspaceAppKeepAlive", () => {
354
434
  ).toBe("Workspace SSO is temporarily unavailable");
355
435
  });
356
436
 
437
+ it("opens the app in the top window when Dispatch is inside an iframe", async () => {
438
+ const topWindow = { location: { href: "" } } as unknown as Window;
439
+ const expectedUrl = new URL("/mail", window.location.href).href;
440
+ Object.defineProperty(window, "parent", {
441
+ configurable: true,
442
+ value: {},
443
+ });
444
+ Object.defineProperty(window, "top", {
445
+ configurable: true,
446
+ value: topWindow,
447
+ });
448
+
449
+ await act(async () => {
450
+ root.render(
451
+ <WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
452
+ );
453
+ await Promise.resolve();
454
+ });
455
+
456
+ expect(topWindow.location.href).toBe(expectedUrl);
457
+ expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
458
+ expect(container.querySelector("iframe")).toBeNull();
459
+ });
460
+
461
+ it("falls back to the embedded app when top-window navigation is blocked", async () => {
462
+ const navigateToTopWindow = vi.fn(() => false);
463
+ Object.defineProperty(window, "parent", {
464
+ configurable: true,
465
+ value: {},
466
+ });
467
+
468
+ await act(async () => {
469
+ root.render(
470
+ <WorkspaceAppFrame
471
+ app={{ id: "mail", name: "Mail", path: "/mail" }}
472
+ navigateToTopWindow={navigateToTopWindow}
473
+ />,
474
+ );
475
+ await Promise.resolve();
476
+ await Promise.resolve();
477
+ await Promise.resolve();
478
+ });
479
+
480
+ expect(navigateToTopWindow).toHaveBeenCalledWith("/mail");
481
+ expect(clientState.legacyMutateAsync).toHaveBeenCalledWith({
482
+ app: "mail",
483
+ path: "/mail",
484
+ chrome: "minimal",
485
+ });
486
+ expect(container.querySelector("iframe")).not.toBeNull();
487
+ });
488
+
489
+ it("preserves a chat-first deep route in the top window when embedded", async () => {
490
+ const topWindow = { location: { href: "" } } as unknown as Window;
491
+ const embedPath = "/emails?status=failed#latest";
492
+ const expectedUrl = new URL(
493
+ "/mail/emails?status=failed#latest",
494
+ window.location.href,
495
+ ).href;
496
+ Object.defineProperty(window, "parent", {
497
+ configurable: true,
498
+ value: {},
499
+ });
500
+ Object.defineProperty(window, "top", {
501
+ configurable: true,
502
+ value: topWindow,
503
+ });
504
+
505
+ await act(async () => {
506
+ root.render(
507
+ <WorkspaceAppFrame
508
+ app={{ id: "mail", name: "Mail", path: "/mail" }}
509
+ embedPath={embedPath}
510
+ />,
511
+ );
512
+ await Promise.resolve();
513
+ });
514
+
515
+ expect(topWindow.location.href).toBe(expectedUrl);
516
+ expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
517
+ expect(container.querySelector("iframe")).toBeNull();
518
+ });
519
+
520
+ it("opens the app in the top window for a Builder webview", async () => {
521
+ const topWindow = { location: { href: "" } } as unknown as Window;
522
+ const expectedUrl = new URL("/mail", window.location.href).href;
523
+ clientState.inBuilderFrame = true;
524
+ Object.defineProperty(window, "top", {
525
+ configurable: true,
526
+ value: topWindow,
527
+ });
528
+
529
+ await act(async () => {
530
+ root.render(
531
+ <WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
532
+ );
533
+ await Promise.resolve();
534
+ });
535
+
536
+ expect(topWindow.location.href).toBe(expectedUrl);
537
+ expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
538
+ expect(container.querySelector("iframe")).toBeNull();
539
+ });
540
+
357
541
  it("evicts the oldest inactive app after reaching the keep-alive limit", async () => {
358
542
  await act(async () => {
359
543
  root.render(<WorkspaceAppKeepAlive activeAppId="mail" />);