@agent-native/dispatch 0.29.1 → 0.29.2
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/workspace-app-host.d.ts +4 -2
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +37 -5
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +2 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +23 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.js +60 -2
- package/dist/lib/workspace-apps.test.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +15 -6
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts +1 -0
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +5 -1
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/package.json +2 -2
- package/src/components/workspace-app-host.spec.tsx +118 -0
- package/src/components/workspace-app-host.tsx +54 -3
- package/src/lib/workspace-apps.test.ts +70 -1
- package/src/lib/workspace-apps.ts +24 -0
- package/src/routes/pages/$appId.spec.tsx +115 -0
- package/src/routes/pages/$appId.tsx +28 -5
- package/src/routes/pages/apps.$appId.spec.tsx +66 -0
- package/src/routes/pages/apps.$appId.tsx +11 -1
|
@@ -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":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AACtD,OAAO,EAAE,4BAA4B,EAAE,MAAM,iDAAiD,CAAC;AAC/F,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,SAAS,GAGV,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACtD,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,gBAAgB,GAEjB,MAAM,0BAA0B,CAAC;AAElC,MAAM,UAAU,IAAI;IAClB,OAAO,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,SAAS,oBAAoB,CAAC,KAAyB;IACrD,IAAI,KAAK,KAAK,UAAU;QAAE,OAAO,OAAO,CAAC,WAAW,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,EAAE,MAAM,EAAsB;IACzD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,UAAU,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,UAAU;QAAE,MAAM,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,2BAA2B,CAAC,KAAK,CAAC,CAAC;IACxD,IAAI,MAAM;QAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EACjC,MAAM,EACN,YAAY,GACa;IACzB,MAAM,UAAU,GAAG,oBAAoB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACtD,IAAI,UAAU;QAAE,MAAM,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC3C,uEAAuE;IACvE,oEAAoE;IACpE,yEAAyE;IACzE,wEAAwE;IACxE,oDAAoD;IACpD,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,yBAAyB;IAC/C,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,cAAc,CAAC,qBAAqB,EAAE;QACtD,iBAAiB,EAAE,KAAK;KACzB,CAAC,CAAC;IACH,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,SAAS,CAAC;IACjD,MAAM,GAAG,GAAG,OAAO,CACjB,GAAG,EAAE,CACF,IAA8B,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,KAAK,CAAC,IAAI,IAAI,EAC3E,CAAC,KAAK,EAAE,IAAI,CAAC,CACd,CAAC;IACF,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAChD,MAAM,eAAe,GAAG,KAAK,KAAK,UAAU,CAAC;IAC7C,MAAM,MAAM,GAAG,GAAG,KAAK,IAAI,CAAC;IAC5B,MAAM,YAAY,GAAG,GAAG,EAAE,MAAM,KAAK,SAAS,CAAC;IAC/C,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEhE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,eAAe;YAAE,OAAO;QAC5B,IAAI,CAAC,MAAM,IAAI,YAAY,IAAI,CAAC,IAAI,EAAE,CAAC;YACrC,mBAAmB,CAAC,KAAK,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,mBAAmB,CAAC,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC;IAElD,IAAI,eAAe,EAAE,CAAC;QACpB,OAAO,KAAC,QAAQ,IAAC,EAAE,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,OAAO,SAAG,CAAC;IACxD,CAAC;IAED,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;QACtB,OAAO,CACL,KAAC,aAAa,IACZ,KAAK,EAAE,CAAC,CAAC,+BAA+B,CAAC,EACzC,WAAW,EAAE,CAAC,CAAC,wCAAwC,CAAC,YAExD,KAAC,gBAAgB,IACf,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC,OAAO,EAAE,GACvC,GACY,CACjB,CAAC;IACJ,CAAC;IAED,IAAI,gBAAgB,IAAI,IAAI,EAAE,CAAC;QAC7B,OAAO,CACL,KAAC,aAAa,IACZ,KAAK,EAAE,CAAC,CAAC,+BAA+B,CAAC,EACzC,WAAW,EAAE,CAAC,CAAC,wCAAwC,CAAC,YAExD,KAAC,gBAAgB,IACf,OAAO,EAAE,GAAG,EAAE,CAAC,mBAAmB,CAAC,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,GACjE,GACY,CACjB,CAAC;IACJ,CAAC;IAED,IACE,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC;QACnB,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,IAAI,CAAC,gBAAgB,CAAC,EAC9D,CAAC;QACD,OAAO,CACL,cAAK,SAAS,EAAC,kDAAkD,YAC/D,KAAC,OAAO,IAAC,SAAS,EAAC,QAAQ,GAAG,GAC1B,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,KAAC,aAAa,IACZ,KAAK,EAAE,GAAG,EAAE,IAAI,IAAI,CAAC,CAAC,6BAA6B,CAAC,EACpD,WAAW,EAAE,CAAC,CAAC,wCAAwC,CAAC,YAExD,eAAK,SAAS,EAAC,kCAAkC,aAC/C,KAAC,MAAM,IAAC,OAAO,QAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,OAAO,EAAC,SAAS,EAAC,YAAY,YAC9D,MAAC,IAAI,IAAC,EAAE,EAAE,OAAO,CAAC,WAAW,CAAC,aAC5B,KAAC,aAAa,IAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAC,QAAQ,GAAG,EAC7C,CAAC,CAAC,uBAAuB,CAAC,IACtB,GACA,EAER,GAAG,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAC3B,eAAK,SAAS,EAAC,WAAW,aACxB,eAAK,SAAS,EAAC,mCAAmC,aAChD,aAAI,SAAS,EAAC,yCAAyC,YACpD,GAAG,CAAC,IAAI,GACN,EACL,MAAC,KAAK,IACJ,OAAO,EAAC,SAAS,EACjB,SAAS,EAAC,8EAA8E,aAExF,KAAC,cAAc,IAAC,IAAI,EAAE,EAAE,GAAI,EAC3B,CAAC,CAAC,yBAAyB,CAAC,IACvB,IACJ,EACN,aAAG,SAAS,EAAC,+BAA+B,aACzC,CAAC,CAAC,kCAAkC,CAAC,EAAE,GAAG,EAC3C,eAAM,SAAS,EAAC,2BAA2B,YAAE,GAAG,CAAC,IAAI,GAAQ,EAAC,GAAG,EAChE,CAAC,CAAC,kCAAkC,CAAC,IACpC,EACH,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAChB,KAAC,MAAM,IAAC,OAAO,kBACb,aACE,IAAI,EAAE,4BAA4B,CAAC,GAAG,CAAC,UAAU,EAAE;oCACjD,QAAQ,EAAE,SAAS;oCACnB,OAAO,EAAE,iBAAiB;iCAC3B,CAAC,EACF,MAAM,EAAC,QAAQ,EACf,GAAG,EAAC,YAAY,aAEf,CAAC,CAAC,kCAAkC,EAAE;wCACrC,YAAY,EAAE,iBAAiB;qCAChC,CAAC,EACF,KAAC,gBAAgB,IAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAC,QAAQ,GAAG,IAC/C,GACG,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAC,CAAC,CAAC,CACF,eAAK,SAAS,EAAC,WAAW,aACxB,aAAI,SAAS,EAAC,yCAAyC,YACpD,CAAC,CAAC,6BAA6B,CAAC,GAC9B,EACL,aAAG,SAAS,EAAC,+BAA+B,aAC1C,gBAAM,SAAS,EAAC,2BAA2B,kBAAG,KAAK,IAAQ,YAC1D,CAAC,CAAC,0CAA0C,CAAC,IAC5C,EACJ,KAAC,MAAM,IAAC,OAAO,kBACb,KAAC,IAAI,IAAC,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,YACvB,CAAC,CAAC,2BAA2B,CAAC,GAC1B,GACA,IACL,CACP,IACG,GACQ,CACjB,CAAC;AACJ,CAAC","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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps.$appId.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/apps.$appId.tsx"],"names":[],"mappings":"
|
|
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;
|
|
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.
|
|
3
|
+
"version": "0.29.2",
|
|
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.
|
|
104
|
+
"@agent-native/core": "0.161.16"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -25,6 +25,7 @@ const clientState = vi.hoisted(() => {
|
|
|
25
25
|
legacyMutateAsync,
|
|
26
26
|
legacyMutateError: null as Error | null,
|
|
27
27
|
legacyErrorMutateAsync,
|
|
28
|
+
inBuilderFrame: false,
|
|
28
29
|
frameLoadHandler: null as (() => void) | null,
|
|
29
30
|
suppressFrameLoad: false,
|
|
30
31
|
theme: "dark" as "dark" | "light",
|
|
@@ -81,6 +82,10 @@ vi.mock("@agent-native/core/client/feature-flags", () => ({
|
|
|
81
82
|
useFeatureFlag: () => clientState.workspaceSsoEnabled,
|
|
82
83
|
}));
|
|
83
84
|
|
|
85
|
+
vi.mock("@agent-native/core/client/host", () => ({
|
|
86
|
+
isInBuilderFrame: () => clientState.inBuilderFrame,
|
|
87
|
+
}));
|
|
88
|
+
|
|
84
89
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
85
90
|
useActionMutation: (name: string) => {
|
|
86
91
|
clientState.actionNames.push(name);
|
|
@@ -156,6 +161,7 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
156
161
|
clientState.legacyMutateAsync.mockClear();
|
|
157
162
|
clientState.legacyMutateError = null;
|
|
158
163
|
clientState.legacyErrorMutateAsync.mockReset();
|
|
164
|
+
clientState.inBuilderFrame = false;
|
|
159
165
|
clientState.frameLoadHandler = null;
|
|
160
166
|
clientState.suppressFrameLoad = false;
|
|
161
167
|
clientState.workspaceSsoMutateAsync.mockClear();
|
|
@@ -166,6 +172,14 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
166
172
|
afterEach(() => {
|
|
167
173
|
act(() => root.unmount());
|
|
168
174
|
container.remove();
|
|
175
|
+
Object.defineProperty(window, "parent", {
|
|
176
|
+
configurable: true,
|
|
177
|
+
value: window,
|
|
178
|
+
});
|
|
179
|
+
Object.defineProperty(window, "top", {
|
|
180
|
+
configurable: true,
|
|
181
|
+
value: window,
|
|
182
|
+
});
|
|
169
183
|
vi.unstubAllGlobals();
|
|
170
184
|
});
|
|
171
185
|
|
|
@@ -354,6 +368,110 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
354
368
|
).toBe("Workspace SSO is temporarily unavailable");
|
|
355
369
|
});
|
|
356
370
|
|
|
371
|
+
it("opens the app in the top window when Dispatch is inside an iframe", async () => {
|
|
372
|
+
const topWindow = { location: { href: "" } } as unknown as Window;
|
|
373
|
+
const expectedUrl = new URL("/mail", window.location.href).href;
|
|
374
|
+
Object.defineProperty(window, "parent", {
|
|
375
|
+
configurable: true,
|
|
376
|
+
value: {},
|
|
377
|
+
});
|
|
378
|
+
Object.defineProperty(window, "top", {
|
|
379
|
+
configurable: true,
|
|
380
|
+
value: topWindow,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await act(async () => {
|
|
384
|
+
root.render(
|
|
385
|
+
<WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
|
|
386
|
+
);
|
|
387
|
+
await Promise.resolve();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(topWindow.location.href).toBe(expectedUrl);
|
|
391
|
+
expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
|
|
392
|
+
expect(container.querySelector("iframe")).toBeNull();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("falls back to the embedded app when top-window navigation is blocked", async () => {
|
|
396
|
+
const navigateToTopWindow = vi.fn(() => false);
|
|
397
|
+
Object.defineProperty(window, "parent", {
|
|
398
|
+
configurable: true,
|
|
399
|
+
value: {},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await act(async () => {
|
|
403
|
+
root.render(
|
|
404
|
+
<WorkspaceAppFrame
|
|
405
|
+
app={{ id: "mail", name: "Mail", path: "/mail" }}
|
|
406
|
+
navigateToTopWindow={navigateToTopWindow}
|
|
407
|
+
/>,
|
|
408
|
+
);
|
|
409
|
+
await Promise.resolve();
|
|
410
|
+
await Promise.resolve();
|
|
411
|
+
await Promise.resolve();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(navigateToTopWindow).toHaveBeenCalledWith("/mail");
|
|
415
|
+
expect(clientState.legacyMutateAsync).toHaveBeenCalledWith({
|
|
416
|
+
app: "mail",
|
|
417
|
+
path: "/mail",
|
|
418
|
+
chrome: "minimal",
|
|
419
|
+
});
|
|
420
|
+
expect(container.querySelector("iframe")).not.toBeNull();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("preserves a chat-first deep route in the top window when embedded", async () => {
|
|
424
|
+
const topWindow = { location: { href: "" } } as unknown as Window;
|
|
425
|
+
const embedPath = "/emails?status=failed#latest";
|
|
426
|
+
const expectedUrl = new URL(
|
|
427
|
+
"/mail/emails?status=failed#latest",
|
|
428
|
+
window.location.href,
|
|
429
|
+
).href;
|
|
430
|
+
Object.defineProperty(window, "parent", {
|
|
431
|
+
configurable: true,
|
|
432
|
+
value: {},
|
|
433
|
+
});
|
|
434
|
+
Object.defineProperty(window, "top", {
|
|
435
|
+
configurable: true,
|
|
436
|
+
value: topWindow,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await act(async () => {
|
|
440
|
+
root.render(
|
|
441
|
+
<WorkspaceAppFrame
|
|
442
|
+
app={{ id: "mail", name: "Mail", path: "/mail" }}
|
|
443
|
+
embedPath={embedPath}
|
|
444
|
+
/>,
|
|
445
|
+
);
|
|
446
|
+
await Promise.resolve();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(topWindow.location.href).toBe(expectedUrl);
|
|
450
|
+
expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
|
|
451
|
+
expect(container.querySelector("iframe")).toBeNull();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("opens the app in the top window for a Builder webview", async () => {
|
|
455
|
+
const topWindow = { location: { href: "" } } as unknown as Window;
|
|
456
|
+
const expectedUrl = new URL("/mail", window.location.href).href;
|
|
457
|
+
clientState.inBuilderFrame = true;
|
|
458
|
+
Object.defineProperty(window, "top", {
|
|
459
|
+
configurable: true,
|
|
460
|
+
value: topWindow,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await act(async () => {
|
|
464
|
+
root.render(
|
|
465
|
+
<WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
|
|
466
|
+
);
|
|
467
|
+
await Promise.resolve();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
expect(topWindow.location.href).toBe(expectedUrl);
|
|
471
|
+
expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
|
|
472
|
+
expect(container.querySelector("iframe")).toBeNull();
|
|
473
|
+
});
|
|
474
|
+
|
|
357
475
|
it("evicts the oldest inactive app after reaching the keep-alive limit", async () => {
|
|
358
476
|
await act(async () => {
|
|
359
477
|
root.render(<WorkspaceAppKeepAlive activeAppId="mail" />);
|
|
@@ -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 (!
|
|
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,7 +466,13 @@ export function WorkspaceAppFrame({
|
|
|
424
466
|
);
|
|
425
467
|
}
|
|
426
468
|
|
|
427
|
-
export function WorkspaceAppHost({
|
|
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",
|
|
@@ -575,7 +623,10 @@ export function WorkspaceAppHost({ appId }: { appId?: string }) {
|
|
|
575
623
|
className="flex h-full min-h-0 flex-col bg-background"
|
|
576
624
|
>
|
|
577
625
|
<div className="min-h-0 flex-1 bg-muted/20">
|
|
578
|
-
<WorkspaceAppFrame
|
|
626
|
+
<WorkspaceAppFrame
|
|
627
|
+
app={app}
|
|
628
|
+
navigateToTopWindow={navigateToTopWindow}
|
|
629
|
+
/>
|
|
579
630
|
</div>
|
|
580
631
|
</div>
|
|
581
632
|
);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
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
|
+
});
|