@agent-native/dispatch 0.29.0 → 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.
Files changed (35) hide show
  1. package/dist/actions/provider-api-register.d.ts +12 -12
  2. package/dist/components/workspace-app-card.js +2 -2
  3. package/dist/components/workspace-app-card.js.map +1 -1
  4. package/dist/components/workspace-app-host.d.ts +4 -2
  5. package/dist/components/workspace-app-host.d.ts.map +1 -1
  6. package/dist/components/workspace-app-host.js +37 -5
  7. package/dist/components/workspace-app-host.js.map +1 -1
  8. package/dist/components/workspace-app-search.js +1 -1
  9. package/dist/components/workspace-app-search.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/components/dispatch-control-plane.spec.tsx +3 -0
  25. package/src/components/workspace-app-card.spec.tsx +2 -1
  26. package/src/components/workspace-app-card.tsx +2 -2
  27. package/src/components/workspace-app-host.spec.tsx +118 -0
  28. package/src/components/workspace-app-host.tsx +54 -3
  29. package/src/components/workspace-app-search.tsx +1 -1
  30. package/src/lib/workspace-apps.test.ts +70 -1
  31. package/src/lib/workspace-apps.ts +24 -0
  32. package/src/routes/pages/$appId.spec.tsx +115 -0
  33. package/src/routes/pages/$appId.tsx +28 -5
  34. package/src/routes/pages/apps.$appId.spec.tsx +66 -0
  35. package/src/routes/pages/apps.$appId.tsx +11 -1
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-app-search.js","sourceRoot":"","sources":["../../src/components/workspace-app-search.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,UAAU,kBAAkB,CAAC,EACjC,KAAK,EACL,aAAa,EACb,SAAS,GAKV;IACC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IACxB,MAAM,KAAK,GAAG,CAAC,CAAC,2BAA2B,EAAE;QAC3C,YAAY,EAAE,aAAa;KAC5B,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,CAAC,CAAC,sCAAsC,EAAE;QAC5D,YAAY,EAAE,+BAA+B;KAC9C,CAAC,CAAC;IAEH,OAAO,CACL,eAAK,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAE,EAAE,CAAC,yBAAyB,EAAE,SAAS,CAAC,aACpE,eAAK,SAAS,EAAC,yBAAyB,aACtC,gBAAO,OAAO,EAAE,OAAO,EAAE,SAAS,EAAC,SAAS,YACzC,KAAK,GACA,EACR,KAAC,UAAU,IACT,IAAI,EAAE,EAAE,iBACI,MAAM,EAClB,SAAS,EAAC,qFAAqF,GAC/F,EACF,KAAC,KAAK,IACJ,EAAE,EAAE,OAAO,EACX,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EACtD,WAAW,EAAE,WAAW,EACxB,SAAS,EAAC,wBAAwB,EAClC,YAAY,EAAC,KAAK,GAClB,IACE,EACL,KAAK,CAAC,CAAC,CAAC,CACP,MAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,wCAAwC,EAClD,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,aAEhC,KAAC,KAAK,IAAC,IAAI,EAAE,EAAE,iBAAc,MAAM,GAAG,EACtC,yBACG,CAAC,CAAC,+BAA+B,EAAE;4BAClC,YAAY,EAAE,OAAO;yBACtB,CAAC,GACG,IACA,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,EACtC,KAAK,EACL,OAAO,GAIR;IACC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,OAAO,CACL,eAAK,SAAS,EAAC,+DAA+D,aAC5E,YAAG,SAAS,EAAC,qCAAqC,YAC/C,CAAC,CAAC,4BAA4B,EAAE;oBAC/B,YAAY,EAAE,2BAA2B;iBAC1C,CAAC,GACA,EACJ,aAAG,SAAS,EAAC,8DAA8D,uBACvE,KAAK,cACL,EACJ,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,MAAM,EAChB,OAAO,EAAE,OAAO,YAEf,CAAC,CAAC,+BAA+B,EAAE,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC,GAC9D,IACL,CACP,CAAC;AACJ,CAAC","sourcesContent":["import { useT } from \"@agent-native/core/client/i18n\";\nimport { IconSearch, IconX } from \"@tabler/icons-react\";\nimport { useId } from \"react\";\n\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"./ui/button\";\nimport { Input } from \"./ui/input\";\n\nexport function WorkspaceAppSearch({\n query,\n onQueryChange,\n className,\n}: {\n query: string;\n onQueryChange: (query: string) => void;\n className?: string;\n}) {\n const t = useT();\n const inputId = useId();\n const label = t(\"dispatch.pages.searchApps\", {\n defaultValue: \"Search apps\",\n });\n const placeholder = t(\"dispatch.pages.searchAppsPlaceholder\", {\n defaultValue: \"Search by name or description\",\n });\n\n return (\n <div role=\"search\" className={cn(\"flex items-center gap-2\", className)}>\n <div className=\"relative min-w-0 flex-1\">\n <label htmlFor={inputId} className=\"sr-only\">\n {label}\n </label>\n <IconSearch\n size={16}\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n />\n <Input\n id={inputId}\n value={query}\n onChange={(event) => onQueryChange(event.target.value)}\n placeholder={placeholder}\n className=\"h-9 bg-background ps-9\"\n autoComplete=\"off\"\n />\n </div>\n {query ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"shrink-0 gap-1.5 text-muted-foreground\"\n onClick={() => onQueryChange(\"\")}\n >\n <IconX size={14} aria-hidden=\"true\" />\n <span>\n {t(\"dispatch.pages.clearAppSearch\", {\n defaultValue: \"Clear\",\n })}\n </span>\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport function WorkspaceAppSearchEmpty({\n query,\n onClear,\n}: {\n query: string;\n onClear: () => void;\n}) {\n const t = useT();\n return (\n <div className=\"rounded-lg border border-dashed bg-card px-4 py-8 text-center\">\n <p className=\"text-sm font-medium text-foreground\">\n {t(\"dispatch.pages.noAppsMatch\", {\n defaultValue: \"No apps match your search\",\n })}\n </p>\n <p className=\"mx-auto mt-1 max-w-md truncate text-xs text-muted-foreground\">\n “{query}”\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-4\"\n onClick={onClear}\n >\n {t(\"dispatch.pages.clearAppSearch\", { defaultValue: \"Clear search\" })}\n </Button>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"workspace-app-search.js","sourceRoot":"","sources":["../../src/components/workspace-app-search.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,UAAU,kBAAkB,CAAC,EACjC,KAAK,EACL,aAAa,EACb,SAAS,GAKV;IACC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IACxB,MAAM,KAAK,GAAG,CAAC,CAAC,2BAA2B,EAAE;QAC3C,YAAY,EAAE,aAAa;KAC5B,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,CAAC,CAAC,sCAAsC,EAAE;QAC5D,YAAY,EAAE,aAAa;KAC5B,CAAC,CAAC;IAEH,OAAO,CACL,eAAK,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAE,EAAE,CAAC,yBAAyB,EAAE,SAAS,CAAC,aACpE,eAAK,SAAS,EAAC,yBAAyB,aACtC,gBAAO,OAAO,EAAE,OAAO,EAAE,SAAS,EAAC,SAAS,YACzC,KAAK,GACA,EACR,KAAC,UAAU,IACT,IAAI,EAAE,EAAE,iBACI,MAAM,EAClB,SAAS,EAAC,qFAAqF,GAC/F,EACF,KAAC,KAAK,IACJ,EAAE,EAAE,OAAO,EACX,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EACtD,WAAW,EAAE,WAAW,EACxB,SAAS,EAAC,wBAAwB,EAClC,YAAY,EAAC,KAAK,GAClB,IACE,EACL,KAAK,CAAC,CAAC,CAAC,CACP,MAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,wCAAwC,EAClD,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,aAEhC,KAAC,KAAK,IAAC,IAAI,EAAE,EAAE,iBAAc,MAAM,GAAG,EACtC,yBACG,CAAC,CAAC,+BAA+B,EAAE;4BAClC,YAAY,EAAE,OAAO;yBACtB,CAAC,GACG,IACA,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,EACtC,KAAK,EACL,OAAO,GAIR;IACC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,OAAO,CACL,eAAK,SAAS,EAAC,+DAA+D,aAC5E,YAAG,SAAS,EAAC,qCAAqC,YAC/C,CAAC,CAAC,4BAA4B,EAAE;oBAC/B,YAAY,EAAE,2BAA2B;iBAC1C,CAAC,GACA,EACJ,aAAG,SAAS,EAAC,8DAA8D,uBACvE,KAAK,cACL,EACJ,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,MAAM,EAChB,OAAO,EAAE,OAAO,YAEf,CAAC,CAAC,+BAA+B,EAAE,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC,GAC9D,IACL,CACP,CAAC;AACJ,CAAC","sourcesContent":["import { useT } from \"@agent-native/core/client/i18n\";\nimport { IconSearch, IconX } from \"@tabler/icons-react\";\nimport { useId } from \"react\";\n\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"./ui/button\";\nimport { Input } from \"./ui/input\";\n\nexport function WorkspaceAppSearch({\n query,\n onQueryChange,\n className,\n}: {\n query: string;\n onQueryChange: (query: string) => void;\n className?: string;\n}) {\n const t = useT();\n const inputId = useId();\n const label = t(\"dispatch.pages.searchApps\", {\n defaultValue: \"Search apps\",\n });\n const placeholder = t(\"dispatch.pages.searchAppsPlaceholder\", {\n defaultValue: \"Search apps\",\n });\n\n return (\n <div role=\"search\" className={cn(\"flex items-center gap-2\", className)}>\n <div className=\"relative min-w-0 flex-1\">\n <label htmlFor={inputId} className=\"sr-only\">\n {label}\n </label>\n <IconSearch\n size={16}\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n />\n <Input\n id={inputId}\n value={query}\n onChange={(event) => onQueryChange(event.target.value)}\n placeholder={placeholder}\n className=\"h-9 bg-background ps-9\"\n autoComplete=\"off\"\n />\n </div>\n {query ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"shrink-0 gap-1.5 text-muted-foreground\"\n onClick={() => onQueryChange(\"\")}\n >\n <IconX size={14} aria-hidden=\"true\" />\n <span>\n {t(\"dispatch.pages.clearAppSearch\", {\n defaultValue: \"Clear\",\n })}\n </span>\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport function WorkspaceAppSearchEmpty({\n query,\n onClear,\n}: {\n query: string;\n onClear: () => void;\n}) {\n const t = useT();\n return (\n <div className=\"rounded-lg border border-dashed bg-card px-4 py-8 text-center\">\n <p className=\"text-sm font-medium text-foreground\">\n {t(\"dispatch.pages.noAppsMatch\", {\n defaultValue: \"No apps match your search\",\n })}\n </p>\n <p className=\"mx-auto mt-1 max-w-md truncate text-xs text-muted-foreground\">\n “{query}”\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-4\"\n onClick={onClear}\n >\n {t(\"dispatch.pages.clearAppSearch\", { defaultValue: \"Clear search\" })}\n </Button>\n </div>\n );\n}\n"]}
@@ -43,6 +43,8 @@ export declare function workspaceAppEmbedTarget(app: Pick<WorkspaceAppSummary, "
43
43
  */
44
44
  export declare function workspaceAppDirectHref(app: WorkspaceAppHrefSource, targetPath: string): string | null;
45
45
  export declare function isPendingBuilderHref(app: WorkspaceAppSummary): boolean;
46
+ export declare function shouldOpenWorkspaceAppInTopWindow(): boolean;
47
+ export declare function navigateToWorkspaceApp(href: string): boolean;
46
48
  /**
47
49
  * Keep the chat-first rail useful before a workspace manifest is populated.
48
50
  * Mounted workspace rows still win, so custom names and routes remain the
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-apps.d.ts","sourceRoot":"","sources":["../../src/lib/workspace-apps.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,sBAAsB;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGpE;AAED,wBAAgB,uCAAuC,CACrD,GAAG,EAAE,IAAI,CAAC,mBAAmB,EAAE,IAAI,GAAG,YAAY,CAAC,GAClD,OAAO,CAET;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUvE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,GAAG,IAAI,CAUxE;AAED,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,KAAK,CAAC,GAC7C;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAMjC;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,sBAAsB,EAC3B,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CA0Df;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,SAAS,mBAAmB,EAAE,GAAG,SAAS,GAC/C,mBAAmB,EAAE,CAiBvB"}
1
+ {"version":3,"file":"workspace-apps.d.ts","sourceRoot":"","sources":["../../src/lib/workspace-apps.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,sBAAsB;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGpE;AAED,wBAAgB,uCAAuC,CACrD,GAAG,EAAE,IAAI,CAAC,mBAAmB,EAAE,IAAI,GAAG,YAAY,CAAC,GAClD,OAAO,CAET;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUvE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,GAAG,IAAI,CAUxE;AAED,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,KAAK,CAAC,GAC7C;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAMjC;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,sBAAsB,EAC3B,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CA0Df;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAEtE;AAED,wBAAgB,iCAAiC,IAAI,OAAO,CAG3D;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAgB5D;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,SAAS,mBAAmB,EAAE,GAAG,SAAS,GAC/C,mBAAmB,EAAE,CAiBvB"}
@@ -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
  import { CANONICAL_WORKSPACE_SSO_APP_ORIGINS } from "../shared/workspace-sso.js";
4
5
  export function isDispatchWorkspaceAppId(appId) {
@@ -107,6 +108,28 @@ export function workspaceAppDirectHref(app, targetPath) {
107
108
  export function isPendingBuilderHref(app) {
108
109
  return app.status === "pending" && !!app.builderUrl;
109
110
  }
111
+ export function shouldOpenWorkspaceAppInTopWindow() {
112
+ if (typeof window === "undefined")
113
+ return false;
114
+ return isInBuilderFrame() || window.parent !== window;
115
+ }
116
+ export function navigateToWorkspaceApp(href) {
117
+ if (typeof window === "undefined")
118
+ return false;
119
+ try {
120
+ const targetUrl = new URL(href, window.location.href);
121
+ if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
122
+ return false;
123
+ }
124
+ const targetWindow = shouldOpenWorkspaceAppInTopWindow() && window.top ? window.top : window;
125
+ targetWindow.location.href = targetUrl.href;
126
+ return true;
127
+ }
128
+ catch {
129
+ // coercion-ok: a blocked top-window assignment is an expected fallback signal.
130
+ return false;
131
+ }
132
+ }
110
133
  /**
111
134
  * Keep the chat-first rail useful before a workspace manifest is populated.
112
135
  * Mounted workspace rows still win, so custom names and routes remain the
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-apps.js","sourceRoot":"","sources":["../../src/lib/workspace-apps.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHAT_FIRST_DEFAULT_APP_IDS } from \"@agent-native/core/client/chat-first\";\nimport { withBuilderUtmTrackingParams } from \"@agent-native/core/shared/builder-link-tracking\";\n\nimport { CANONICAL_WORKSPACE_SSO_APP_ORIGINS } from \"../shared/workspace-sso\";\n\nexport interface WorkspaceAppSummary {\n id: string;\n name: string;\n description?: string;\n path: string;\n url?: string | null;\n isDispatch?: boolean;\n audience?: \"internal\" | \"public\";\n publicPaths?: string[];\n protectedPaths?: string[];\n status?: \"ready\" | \"pending\";\n statusLabel?: string;\n builderUrl?: string | null;\n branchName?: string | null;\n createdAt?: string | null;\n createdBy?: string | null;\n owner?: string | null;\n teams?: string[];\n agentCardUrl?: string | null;\n agentCardReachable?: boolean;\n a2aEndpointUrl?: string | null;\n agentName?: string | null;\n agentSkillsCount?: number | null;\n archived?: boolean;\n}\n\ninterface WorkspaceAppHrefSource {\n path?: string | null;\n url?: string | null;\n}\n\nexport function isDispatchWorkspaceAppId(appId: string): boolean {\n return appId.trim().toLowerCase() === \"dispatch\";\n}\n\nexport function isDefaultWorkspaceAppHiddenId(appId: string): boolean {\n const normalized = appId.trim().toLowerCase();\n return normalized === \"chat\" || isDispatchWorkspaceAppId(normalized);\n}\n\nexport function isWorkspaceAppVisibleInDefaultLaunchers(\n app: Pick<WorkspaceAppSummary, \"id\" | \"isDispatch\">,\n): boolean {\n return !app.isDispatch && !isDefaultWorkspaceAppHiddenId(app.id);\n}\n\nexport function workspaceAppRoute(appId: string): string {\n return `/apps/${encodeURIComponent(appId)}`;\n}\n\nexport function workspaceAppIdFromRoute(pathname: string): string | null {\n const match = pathname.match(/^\\/apps\\/([^/]+)(?:\\/|$)/);\n if (!match) return null;\n try {\n const appId = decodeURIComponent(match[1]).trim();\n return appId || null;\n } catch {\n // coercion-ok: malformed app routes are inactive, not app ids.\n return null;\n }\n}\n\nexport function workspaceAppHref(app: WorkspaceAppSummary): string | null {\n if (app.status === \"pending\") {\n return app.builderUrl\n ? withBuilderUtmTrackingParams(app.builderUrl, {\n campaign: \"product\",\n content: \"dispatch_branch\",\n })\n : null;\n }\n return app.path || app.url || null;\n}\n\nexport function workspaceAppEmbedTarget(\n app: Pick<WorkspaceAppSummary, \"path\" | \"url\">,\n): { path?: string; url?: string } {\n const url = app.url?.trim();\n if (url) return { url };\n\n const path = app.path.trim();\n return path.startsWith(\"/\") ? { path } : path ? { url: path } : {};\n}\n\n/**\n * Resolve an app route without an embed ticket so the target can render its\n * own error document when session setup fails.\n */\nexport function workspaceAppDirectHref(\n app: WorkspaceAppHrefSource,\n targetPath: string,\n): string | null {\n const target = targetPath.trim();\n if (!target || !target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return null;\n }\n\n let targetUrl: URL;\n try {\n targetUrl = new URL(target, \"https://agent-native.invalid\");\n } catch {\n // coercion-ok: invalid relative target input has no safe href.\n return null;\n }\n const targetPathname = targetUrl.pathname || \"/\";\n\n let absoluteBase: URL | null = null;\n const rawUrl = app.url?.trim();\n if (rawUrl) {\n try {\n const parsedBase = new URL(rawUrl);\n if (parsedBase.protocol === \"http:\" || parsedBase.protocol === \"https:\") {\n absoluteBase = parsedBase;\n }\n // coercion-ok: invalid app URLs use the mounted path fallback below.\n } catch {\n absoluteBase = null;\n }\n }\n\n const mountedPath = app.path?.trim();\n const basePath = absoluteBase\n ? absoluteBase.pathname\n : mountedPath\n ? `/${mountedPath.replace(/^[/\\\\]+/, \"\").split(/[?#]/, 1)[0]}`\n : null;\n if (!basePath) return null;\n\n const normalizedBasePath = basePath.replace(/\\/+$/, \"\") || \"/\";\n const targetIsMountedPath =\n normalizedBasePath !== \"/\" &&\n (targetPathname === normalizedBasePath ||\n targetPathname.startsWith(`${normalizedBasePath}/`));\n const resolvedPath = targetIsMountedPath\n ? targetPathname\n : normalizedBasePath === \"/\"\n ? targetPathname\n : targetPathname === \"/\"\n ? normalizedBasePath\n : `${normalizedBasePath}/${targetPathname.replace(/^\\/+/, \"\")}`;\n\n if (absoluteBase) {\n absoluteBase.pathname = resolvedPath;\n absoluteBase.search = targetUrl.search;\n absoluteBase.hash = targetUrl.hash;\n return absoluteBase.toString();\n }\n\n return `${resolvedPath}${targetUrl.search}${targetUrl.hash}`;\n}\n\nexport function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {\n return app.status === \"pending\" && !!app.builderUrl;\n}\n\n/**\n * Keep the chat-first rail useful before a workspace manifest is populated.\n * Mounted workspace rows still win, so custom names and routes remain the\n * source of truth once an app exists in the workspace.\n */\nexport function mergeChatFirstWorkspaceApps(\n apps: readonly WorkspaceAppSummary[] | undefined,\n): WorkspaceAppSummary[] {\n const merged = new Map<string, WorkspaceAppSummary>();\n for (const id of CHAT_FIRST_DEFAULT_APP_IDS) {\n merged.set(id, {\n id,\n name: id.charAt(0).toUpperCase() + id.slice(1),\n // The five default rows are hosted sibling apps, not routes owned by\n // Dispatch. Keep a mounted path for legacy callers, but give embed\n // session resolution the exact canonical origin.\n path: \"/\",\n url: CANONICAL_WORKSPACE_SSO_APP_ORIGINS[id],\n status: \"ready\",\n });\n }\n for (const app of apps ?? []) merged.set(app.id, app);\n\n return [...merged.values()];\n}\n"]}
1
+ {"version":3,"file":"workspace-apps.js","sourceRoot":"","sources":["../../src/lib/workspace-apps.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHAT_FIRST_DEFAULT_APP_IDS } from \"@agent-native/core/client/chat-first\";\nimport { isInBuilderFrame } from \"@agent-native/core/client/host\";\nimport { withBuilderUtmTrackingParams } from \"@agent-native/core/shared/builder-link-tracking\";\n\nimport { CANONICAL_WORKSPACE_SSO_APP_ORIGINS } from \"../shared/workspace-sso\";\n\nexport interface WorkspaceAppSummary {\n id: string;\n name: string;\n description?: string;\n path: string;\n url?: string | null;\n isDispatch?: boolean;\n audience?: \"internal\" | \"public\";\n publicPaths?: string[];\n protectedPaths?: string[];\n status?: \"ready\" | \"pending\";\n statusLabel?: string;\n builderUrl?: string | null;\n branchName?: string | null;\n createdAt?: string | null;\n createdBy?: string | null;\n owner?: string | null;\n teams?: string[];\n agentCardUrl?: string | null;\n agentCardReachable?: boolean;\n a2aEndpointUrl?: string | null;\n agentName?: string | null;\n agentSkillsCount?: number | null;\n archived?: boolean;\n}\n\ninterface WorkspaceAppHrefSource {\n path?: string | null;\n url?: string | null;\n}\n\nexport function isDispatchWorkspaceAppId(appId: string): boolean {\n return appId.trim().toLowerCase() === \"dispatch\";\n}\n\nexport function isDefaultWorkspaceAppHiddenId(appId: string): boolean {\n const normalized = appId.trim().toLowerCase();\n return normalized === \"chat\" || isDispatchWorkspaceAppId(normalized);\n}\n\nexport function isWorkspaceAppVisibleInDefaultLaunchers(\n app: Pick<WorkspaceAppSummary, \"id\" | \"isDispatch\">,\n): boolean {\n return !app.isDispatch && !isDefaultWorkspaceAppHiddenId(app.id);\n}\n\nexport function workspaceAppRoute(appId: string): string {\n return `/apps/${encodeURIComponent(appId)}`;\n}\n\nexport function workspaceAppIdFromRoute(pathname: string): string | null {\n const match = pathname.match(/^\\/apps\\/([^/]+)(?:\\/|$)/);\n if (!match) return null;\n try {\n const appId = decodeURIComponent(match[1]).trim();\n return appId || null;\n } catch {\n // coercion-ok: malformed app routes are inactive, not app ids.\n return null;\n }\n}\n\nexport function workspaceAppHref(app: WorkspaceAppSummary): string | null {\n if (app.status === \"pending\") {\n return app.builderUrl\n ? withBuilderUtmTrackingParams(app.builderUrl, {\n campaign: \"product\",\n content: \"dispatch_branch\",\n })\n : null;\n }\n return app.path || app.url || null;\n}\n\nexport function workspaceAppEmbedTarget(\n app: Pick<WorkspaceAppSummary, \"path\" | \"url\">,\n): { path?: string; url?: string } {\n const url = app.url?.trim();\n if (url) return { url };\n\n const path = app.path.trim();\n return path.startsWith(\"/\") ? { path } : path ? { url: path } : {};\n}\n\n/**\n * Resolve an app route without an embed ticket so the target can render its\n * own error document when session setup fails.\n */\nexport function workspaceAppDirectHref(\n app: WorkspaceAppHrefSource,\n targetPath: string,\n): string | null {\n const target = targetPath.trim();\n if (!target || !target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return null;\n }\n\n let targetUrl: URL;\n try {\n targetUrl = new URL(target, \"https://agent-native.invalid\");\n } catch {\n // coercion-ok: invalid relative target input has no safe href.\n return null;\n }\n const targetPathname = targetUrl.pathname || \"/\";\n\n let absoluteBase: URL | null = null;\n const rawUrl = app.url?.trim();\n if (rawUrl) {\n try {\n const parsedBase = new URL(rawUrl);\n if (parsedBase.protocol === \"http:\" || parsedBase.protocol === \"https:\") {\n absoluteBase = parsedBase;\n }\n // coercion-ok: invalid app URLs use the mounted path fallback below.\n } catch {\n absoluteBase = null;\n }\n }\n\n const mountedPath = app.path?.trim();\n const basePath = absoluteBase\n ? absoluteBase.pathname\n : mountedPath\n ? `/${mountedPath.replace(/^[/\\\\]+/, \"\").split(/[?#]/, 1)[0]}`\n : null;\n if (!basePath) return null;\n\n const normalizedBasePath = basePath.replace(/\\/+$/, \"\") || \"/\";\n const targetIsMountedPath =\n normalizedBasePath !== \"/\" &&\n (targetPathname === normalizedBasePath ||\n targetPathname.startsWith(`${normalizedBasePath}/`));\n const resolvedPath = targetIsMountedPath\n ? targetPathname\n : normalizedBasePath === \"/\"\n ? targetPathname\n : targetPathname === \"/\"\n ? normalizedBasePath\n : `${normalizedBasePath}/${targetPathname.replace(/^\\/+/, \"\")}`;\n\n if (absoluteBase) {\n absoluteBase.pathname = resolvedPath;\n absoluteBase.search = targetUrl.search;\n absoluteBase.hash = targetUrl.hash;\n return absoluteBase.toString();\n }\n\n return `${resolvedPath}${targetUrl.search}${targetUrl.hash}`;\n}\n\nexport function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {\n return app.status === \"pending\" && !!app.builderUrl;\n}\n\nexport function shouldOpenWorkspaceAppInTopWindow(): boolean {\n if (typeof window === \"undefined\") return false;\n return isInBuilderFrame() || window.parent !== window;\n}\n\nexport function navigateToWorkspaceApp(href: string): boolean {\n if (typeof window === \"undefined\") return false;\n\n try {\n const targetUrl = new URL(href, window.location.href);\n if (targetUrl.protocol !== \"http:\" && targetUrl.protocol !== \"https:\") {\n return false;\n }\n const targetWindow =\n shouldOpenWorkspaceAppInTopWindow() && window.top ? window.top : window;\n targetWindow.location.href = targetUrl.href;\n return true;\n } catch {\n // coercion-ok: a blocked top-window assignment is an expected fallback signal.\n return false;\n }\n}\n\n/**\n * Keep the chat-first rail useful before a workspace manifest is populated.\n * Mounted workspace rows still win, so custom names and routes remain the\n * source of truth once an app exists in the workspace.\n */\nexport function mergeChatFirstWorkspaceApps(\n apps: readonly WorkspaceAppSummary[] | undefined,\n): WorkspaceAppSummary[] {\n const merged = new Map<string, WorkspaceAppSummary>();\n for (const id of CHAT_FIRST_DEFAULT_APP_IDS) {\n merged.set(id, {\n id,\n name: id.charAt(0).toUpperCase() + id.slice(1),\n // The five default rows are hosted sibling apps, not routes owned by\n // Dispatch. Keep a mounted path for legacy callers, but give embed\n // session resolution the exact canonical origin.\n path: \"/\",\n url: CANONICAL_WORKSPACE_SSO_APP_ORIGINS[id],\n status: \"ready\",\n });\n }\n for (const app of apps ?? []) merged.set(app.id, app);\n\n return [...merged.values()];\n}\n"]}
@@ -1,5 +1,6 @@
1
- import { describe, expect, it } from "vitest";
2
- import { workspaceAppDirectHref, workspaceAppEmbedTarget, } from "./workspace-apps.js";
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { navigateToWorkspaceApp, workspaceAppDirectHref, workspaceAppEmbedTarget, } from "./workspace-apps.js";
3
4
  describe("workspaceAppEmbedTarget", () => {
4
5
  it("uses the app URL as the embed root when a mount path is also present", () => {
5
6
  expect(workspaceAppEmbedTarget({
@@ -30,4 +31,61 @@ describe("workspaceAppDirectHref", () => {
30
31
  expect(workspaceAppDirectHref({ path: "/atlas" }, "//evil.example")).toBe(null);
31
32
  });
32
33
  });
34
+ describe("navigateToWorkspaceApp", () => {
35
+ afterEach(() => {
36
+ Object.defineProperty(window, "parent", {
37
+ configurable: true,
38
+ value: window,
39
+ });
40
+ Object.defineProperty(window, "top", {
41
+ configurable: true,
42
+ value: window,
43
+ });
44
+ });
45
+ it("uses the top window when Dispatch is embedded", () => {
46
+ const topWindow = { location: { href: "" } };
47
+ Object.defineProperty(window, "parent", {
48
+ configurable: true,
49
+ value: {},
50
+ });
51
+ Object.defineProperty(window, "top", {
52
+ configurable: true,
53
+ value: topWindow,
54
+ });
55
+ navigateToWorkspaceApp("/mail");
56
+ expect(topWindow.location.href).toBe(new URL("/mail", window.location.href).href);
57
+ });
58
+ it("reports when the embedded top window rejects navigation", () => {
59
+ const location = {};
60
+ Object.defineProperty(location, "href", {
61
+ configurable: true,
62
+ set: () => {
63
+ throw new Error("Top navigation is blocked");
64
+ },
65
+ });
66
+ const topWindow = { location };
67
+ Object.defineProperty(window, "parent", {
68
+ configurable: true,
69
+ value: {},
70
+ });
71
+ Object.defineProperty(window, "top", {
72
+ configurable: true,
73
+ value: topWindow,
74
+ });
75
+ expect(navigateToWorkspaceApp("/mail")).toBe(false);
76
+ });
77
+ it("rejects non-http navigation targets", () => {
78
+ const topWindow = { location: { href: "" } };
79
+ Object.defineProperty(window, "parent", {
80
+ configurable: true,
81
+ value: {},
82
+ });
83
+ Object.defineProperty(window, "top", {
84
+ configurable: true,
85
+ value: topWindow,
86
+ });
87
+ expect(navigateToWorkspaceApp("javascript:alert(1)")).toBe(false);
88
+ expect(topWindow.location.href).toBe("");
89
+ });
90
+ });
33
91
  //# sourceMappingURL=workspace-apps.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-apps.test.js","sourceRoot":"","sources":["../../src/lib/workspace-apps.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EACL,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAE1B,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CACJ,uBAAuB,CAAC;YACtB,IAAI,EAAE,UAAU;YAChB,GAAG,EAAE,wCAAwC;SAC9C,CAAC,CACH,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,wCAAwC,EAAE,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,CAAC,uBAAuB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;YACvE,IAAI,EAAE,UAAU;SACjB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,CACJ,sBAAsB,CACpB,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,sCAAsC,EAAE,EAC/D,8BAA8B,CAC/B,CACF,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,CACJ,sBAAsB,CACpB,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,sCAAsC,EAAE,EAC/D,QAAQ,CACT,CACF,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAChE,eAAe,CAChB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,CACJ,sBAAsB,CACpB,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,qBAAqB,EAAE,EAC9C,GAAG,CACJ,CACF,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACnB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC,IAAI,CACvE,IAAI,CACL,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from \"vitest\";\n\nimport {\n workspaceAppDirectHref,\n workspaceAppEmbedTarget,\n} from \"./workspace-apps\";\n\ndescribe(\"workspaceAppEmbedTarget\", () => {\n it(\"uses the app URL as the embed root when a mount path is also present\", () => {\n expect(\n workspaceAppEmbedTarget({\n path: \"/content\",\n url: \"https://workspace.example.test/content\",\n }),\n ).toEqual({ url: \"https://workspace.example.test/content\" });\n });\n\n it(\"falls back to the mounted path when no app URL is available\", () => {\n expect(workspaceAppEmbedTarget({ path: \"/content\", url: null })).toEqual({\n path: \"/content\",\n });\n });\n});\n\ndescribe(\"workspaceAppDirectHref\", () => {\n it(\"joins an app-relative route onto an absolute app URL\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"https://workspace.example.test/atlas\" },\n \"/emails?status=failed#latest\",\n ),\n ).toBe(\"https://workspace.example.test/atlas/emails?status=failed#latest\");\n });\n\n it(\"does not duplicate a mount path already present in the target\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"https://workspace.example.test/atlas\" },\n \"/atlas\",\n ),\n ).toBe(\"https://workspace.example.test/atlas\");\n });\n\n it(\"resolves a mounted relative app path\", () => {\n expect(workspaceAppDirectHref({ path: \"/atlas\" }, \"/emails\")).toBe(\n \"/atlas/emails\",\n );\n });\n\n it(\"falls back to a safe mount path when the app URL is invalid\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"javascript:alert(1)\" },\n \"/\",\n ),\n ).toBe(\"/atlas\");\n });\n\n it(\"rejects an unsafe target path\", () => {\n expect(workspaceAppDirectHref({ path: \"/atlas\" }, \"//evil.example\")).toBe(\n null,\n );\n });\n});\n"]}
1
+ {"version":3,"file":"workspace-apps.test.js","sourceRoot":"","sources":["../../src/lib/workspace-apps.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment happy-dom\nimport { afterEach, describe, expect, it } from \"vitest\";\n\nimport {\n navigateToWorkspaceApp,\n workspaceAppDirectHref,\n workspaceAppEmbedTarget,\n} from \"./workspace-apps\";\n\ndescribe(\"workspaceAppEmbedTarget\", () => {\n it(\"uses the app URL as the embed root when a mount path is also present\", () => {\n expect(\n workspaceAppEmbedTarget({\n path: \"/content\",\n url: \"https://workspace.example.test/content\",\n }),\n ).toEqual({ url: \"https://workspace.example.test/content\" });\n });\n\n it(\"falls back to the mounted path when no app URL is available\", () => {\n expect(workspaceAppEmbedTarget({ path: \"/content\", url: null })).toEqual({\n path: \"/content\",\n });\n });\n});\n\ndescribe(\"workspaceAppDirectHref\", () => {\n it(\"joins an app-relative route onto an absolute app URL\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"https://workspace.example.test/atlas\" },\n \"/emails?status=failed#latest\",\n ),\n ).toBe(\"https://workspace.example.test/atlas/emails?status=failed#latest\");\n });\n\n it(\"does not duplicate a mount path already present in the target\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"https://workspace.example.test/atlas\" },\n \"/atlas\",\n ),\n ).toBe(\"https://workspace.example.test/atlas\");\n });\n\n it(\"resolves a mounted relative app path\", () => {\n expect(workspaceAppDirectHref({ path: \"/atlas\" }, \"/emails\")).toBe(\n \"/atlas/emails\",\n );\n });\n\n it(\"falls back to a safe mount path when the app URL is invalid\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"javascript:alert(1)\" },\n \"/\",\n ),\n ).toBe(\"/atlas\");\n });\n\n it(\"rejects an unsafe target path\", () => {\n expect(workspaceAppDirectHref({ path: \"/atlas\" }, \"//evil.example\")).toBe(\n null,\n );\n });\n});\n\ndescribe(\"navigateToWorkspaceApp\", () => {\n afterEach(() => {\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: window,\n });\n Object.defineProperty(window, \"top\", {\n configurable: true,\n value: window,\n });\n });\n\n it(\"uses the top window when Dispatch is embedded\", () => {\n const topWindow = { location: { href: \"\" } } as unknown as Window;\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: {},\n });\n Object.defineProperty(window, \"top\", {\n configurable: true,\n value: topWindow,\n });\n\n navigateToWorkspaceApp(\"/mail\");\n\n expect(topWindow.location.href).toBe(\n new URL(\"/mail\", window.location.href).href,\n );\n });\n\n it(\"reports when the embedded top window rejects navigation\", () => {\n const location = {} as Location;\n Object.defineProperty(location, \"href\", {\n configurable: true,\n set: () => {\n throw new Error(\"Top navigation is blocked\");\n },\n });\n const topWindow = { location } as unknown as Window;\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: {},\n });\n Object.defineProperty(window, \"top\", {\n configurable: true,\n value: topWindow,\n });\n\n expect(navigateToWorkspaceApp(\"/mail\")).toBe(false);\n });\n\n it(\"rejects non-http navigation targets\", () => {\n const topWindow = { location: { href: \"\" } } as unknown as Window;\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: {},\n });\n Object.defineProperty(window, \"top\", {\n configurable: true,\n value: topWindow,\n });\n\n expect(navigateToWorkspaceApp(\"javascript:alert(1)\")).toBe(false);\n expect(topWindow.location.href).toBe(\"\");\n });\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"$appId.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/$appId.tsx"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AAatB,wBAAgB,IAAI;;IAEnB;AA2CD,wBAAsB,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,kBAAkB,iBAQ1D;AAED,wBAAsB,YAAY,CAAC,EACjC,MAAM,EACN,YAAY,GACb,EAAE,wBAAwB,oBAS1B;AAED,MAAM,CAAC,OAAO,UAAU,yBAAyB,oDAoHhD"}
1
+ {"version":3,"file":"$appId.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/$appId.tsx"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AActB,wBAAgB,IAAI;;IAEnB;AA2CD,wBAAsB,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,kBAAkB,iBAQ1D;AAED,wBAAsB,YAAY,CAAC,EACjC,MAAM,EACN,YAAY,GACb,EAAE,wBAAwB,oBAS1B;AAED,MAAM,CAAC,OAAO,UAAU,yBAAyB,oDA0IhD"}
@@ -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":";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,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.0",
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.2"
104
+ "@agent-native/core": "0.161.16"
105
105
  },
106
106
  "peerDependencies": {
107
107
  "@agent-native/core": ">=0.8.0",
@@ -291,6 +291,9 @@ describe("DispatchControlPlane", () => {
291
291
  expect(container.textContent).toContain("Analytics");
292
292
  expect(container.textContent).toContain("Apps");
293
293
  expect(container.textContent).toContain("New");
294
+ expect(
295
+ container.querySelector('input[placeholder="Search apps"]'),
296
+ ).not.toBeNull();
294
297
  const viewAllLink = Array.from(container.querySelectorAll("a")).find(
295
298
  (link) => link.textContent?.trim() === "View all",
296
299
  );
@@ -94,6 +94,7 @@ describe("WorkspaceAppCard", () => {
94
94
  expect(newTabItem?.getAttribute("target")).toBe("_blank");
95
95
  const openMenu = document.querySelector<HTMLElement>('[role="menu"]');
96
96
  expect(openMenu?.className).toContain("w-48");
97
+ expect(openMenu?.className).toContain("min-w-max");
97
98
  expect(openMenu?.className).toContain("bg-popover");
98
99
  expect(openMenu?.className).toContain("shadow-md");
99
100
  expect(newTabItem?.querySelector("svg")).not.toBeNull();
@@ -149,7 +150,7 @@ describe("WorkspaceAppCard", () => {
149
150
 
150
151
  const pinItem = Array.from(
151
152
  document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
152
- ).find((item) => item.textContent?.includes("Unpin Analytics"));
153
+ ).find((item) => item.textContent?.includes("Unpin this app"));
153
154
  expect(pinItem).not.toBeUndefined();
154
155
 
155
156
  await act(async () => pinItem?.click());
@@ -146,7 +146,7 @@ export function WorkspaceAppCard({
146
146
  isPinned ? "dispatch.pages.unpinApp" : "dispatch.pages.pinApp",
147
147
  {
148
148
  name: app.name,
149
- defaultValue: `${isPinned ? "Unpin" : "Pin"} ${app.name}`,
149
+ defaultValue: isPinned ? "Unpin this app" : "Pin this app",
150
150
  },
151
151
  );
152
152
 
@@ -396,7 +396,7 @@ function WorkspaceAppSettings({
396
396
  </Tooltip>
397
397
  <DropdownMenuContent
398
398
  align="end"
399
- className={APP_ACTION_MENU_CONTENT_CLASS}
399
+ className={cn(APP_ACTION_MENU_CONTENT_CLASS, "min-w-max")}
400
400
  >
401
401
  <DropdownMenuItem onSelect={onEdit}>
402
402
  <IconEdit size={14} aria-hidden="true" />