@agent-native/dispatch 0.40.40 → 0.40.41-beta-dispatch-embed-01a15dc9-20261027050558

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 (45) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/actions/create_embed_session.js +1 -0
  3. package/dist/actions/create_embed_session.js.map +1 -1
  4. package/dist/actions/delete-destination.d.ts +12 -12
  5. package/dist/actions/provider-api-register.d.ts +13 -13
  6. package/dist/components/layout/Layout.d.ts.map +1 -1
  7. package/dist/components/layout/Layout.js +8 -11
  8. package/dist/components/layout/Layout.js.map +1 -1
  9. package/dist/components/workspace-app-host.d.ts.map +1 -1
  10. package/dist/components/workspace-app-host.js +23 -10
  11. package/dist/components/workspace-app-host.js.map +1 -1
  12. package/dist/lib/workspace-apps.d.ts +2 -2
  13. package/dist/lib/workspace-apps.d.ts.map +1 -1
  14. package/dist/lib/workspace-apps.js +35 -5
  15. package/dist/lib/workspace-apps.js.map +1 -1
  16. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  17. package/dist/server/lib/app-creation-store.js +173 -48
  18. package/dist/server/lib/app-creation-store.js.map +1 -1
  19. package/dist/server/lib/community-apps.js +1 -1
  20. package/dist/server/lib/community-apps.js.map +1 -1
  21. package/dist/server/lib/mcp-gateway.d.ts +1 -0
  22. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  23. package/dist/server/lib/mcp-gateway.js +170 -38
  24. package/dist/server/lib/mcp-gateway.js.map +1 -1
  25. package/dist/server/lib/provider-api.d.ts +2 -2
  26. package/dist/shared/workspace-app-route.d.ts +2 -0
  27. package/dist/shared/workspace-app-route.d.ts.map +1 -0
  28. package/dist/shared/workspace-app-route.js +4 -0
  29. package/dist/shared/workspace-app-route.js.map +1 -0
  30. package/package.json +4 -4
  31. package/src/actions/create_embed_session.ts +1 -0
  32. package/src/actions/index.spec.ts +3 -0
  33. package/src/components/layout/Layout.spec.tsx +39 -9
  34. package/src/components/layout/Layout.tsx +19 -40
  35. package/src/components/workspace-app-host.spec.tsx +88 -1
  36. package/src/components/workspace-app-host.tsx +30 -9
  37. package/src/lib/workspace-apps.spec.ts +64 -5
  38. package/src/lib/workspace-apps.ts +36 -6
  39. package/src/routes/pages/apps.$appId.spec.tsx +21 -3
  40. package/src/server/lib/app-creation-store.spec.ts +502 -11
  41. package/src/server/lib/app-creation-store.ts +198 -50
  42. package/src/server/lib/community-apps.ts +1 -1
  43. package/src/server/lib/mcp-gateway.spec.ts +396 -9
  44. package/src/server/lib/mcp-gateway.ts +209 -45
  45. package/src/shared/workspace-app-route.ts +3 -0
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { canonicalA2AAudience, getGlobalA2ASecret, signA2AToken, } from "@agent-native/core/a2a";
6
6
  import { ActionContractError, isActionContractError, } from "@agent-native/core/action";
7
7
  import { describeErrorWithCauses } from "@agent-native/core/agent/engine/error-detail";
8
+ import { getAppConfig } from "@agent-native/core/app-config";
8
9
  import { getDbExec } from "@agent-native/core/db";
9
10
  import { and, eq, inArray } from "@agent-native/core/db/schema";
10
11
  import { getOrgA2ASecret, getOrgDomain, isWorkspaceAppAccessAllowed, workspaceApps, } from "@agent-native/core/org";
@@ -14,6 +15,7 @@ import { getSetting, mutateSetting, putSetting, } from "@agent-native/core/setti
14
15
  import { BUILDER_CONNECT_PROVIDER, BUILDER_CONNECT_PROVIDER_LABEL, assertValidWorkspaceAppId, connectRequiredResult, normalizeWorkspaceAppHomePath, parseAgentProfileMetadata, } from "@agent-native/core/shared";
15
16
  import { resolveAccess } from "@agent-native/core/sharing";
16
17
  import { getDb } from "../../db/index.js";
18
+ import { workspaceAppRoute } from "../../shared/workspace-app-route.js";
17
19
  import { isWorkspaceSsoAppUrl } from "../../shared/workspace-sso.js";
18
20
  import { getDispatchDefaultOwnerEmail } from "./admin-config.js";
19
21
  import { identityKeyForIncoming } from "./dispatch-integrations.js";
@@ -83,6 +85,14 @@ class WorkspaceAppOrgRequiredError extends Error {
83
85
  this.name = "WorkspaceAppOrgRequiredError";
84
86
  }
85
87
  }
88
+ class WorkspaceGatewayConfigurationError extends Error {
89
+ constructor(cause) {
90
+ super("WORKSPACE_GATEWAY_URL must be a valid absolute HTTP(S) URL.", {
91
+ cause,
92
+ });
93
+ this.name = "WorkspaceGatewayConfigurationError";
94
+ }
95
+ }
86
96
  function requireWorkspaceAppOrgId() {
87
97
  const orgId = currentOrgId();
88
98
  if (!orgId)
@@ -101,17 +111,20 @@ function warnWorkspaceAppsGatewayDenial(denial, source) {
101
111
  console.warn(`[dispatch] workspace apps gateway denied the registry read with HTTP ${denial.statusCode}; served the ${source} instead`);
102
112
  }
103
113
  function readJson(file) {
104
- try {
105
- return JSON.parse(fs.readFileSync(file, "utf8"));
106
- }
107
- catch {
114
+ if (!fs.existsSync(file))
108
115
  return null;
109
- }
116
+ return JSON.parse(fs.readFileSync(file, "utf8"));
110
117
  }
111
118
  function findWorkspaceRoot(startDir = process.cwd()) {
112
119
  let dir = path.resolve(startDir);
113
120
  for (let i = 0; i < 20; i++) {
114
- const pkg = readJson(path.join(dir, "package.json"));
121
+ let pkg;
122
+ try {
123
+ pkg = readJson(path.join(dir, "package.json"));
124
+ }
125
+ catch (error) {
126
+ console.warn(`[dispatch] Could not read workspace package metadata in ${dir}; continuing parent search`, error);
127
+ }
115
128
  if (typeof pkg?.["agent-native"]?.workspaceCore === "string") {
116
129
  return dir;
117
130
  }
@@ -393,16 +406,83 @@ function applyWorkspaceAppMetadataOverride(app, settings) {
393
406
  // code belongs here; changes to authorship, content trust, or sharing require
394
407
  // an explicit auth, origin, or sandbox boundary before this invariant changes.
395
408
  function workspaceAppUrl(appPath) {
409
+ const gateway = workspaceGatewayBaseUrl();
410
+ if (gateway)
411
+ return appendWorkspaceAppPath(gateway, appPath);
396
412
  const base = resolveAppRuntimeUrl();
397
413
  if (!base)
398
414
  return null;
399
415
  try {
400
- return new URL(appPath, `${projectEnvironmentUrl(base).replace(/\/$/, "")}/`).toString();
416
+ const baseUrl = new URL(addBasePath(projectEnvironmentUrl(base), getConfiguredAppBasePath()));
417
+ return appendWorkspaceAppPath(baseUrl, appPath);
401
418
  }
402
419
  catch {
403
420
  return null;
404
421
  }
405
422
  }
423
+ function appendWorkspaceAppPath(baseUrl, appPath) {
424
+ const parsedPath = new URL(appPath, "http://agent-native.invalid");
425
+ const basePath = baseUrl.pathname.replace(/\/+$/, "");
426
+ let appPathname = parsedPath.pathname;
427
+ if (basePath &&
428
+ (appPathname === basePath || appPathname.startsWith(`${basePath}/`))) {
429
+ appPathname = appPathname.slice(basePath.length) || "/";
430
+ }
431
+ baseUrl.pathname =
432
+ `${basePath}${appPathname.startsWith("/") ? appPathname : `/${appPathname}`}` ||
433
+ "/";
434
+ baseUrl.search = parsedPath.search;
435
+ baseUrl.hash = parsedPath.hash;
436
+ return baseUrl.toString().replace(/\/$/, "");
437
+ }
438
+ function workspaceGatewayBaseUrl() {
439
+ const configured = getAppConfig().workspace.gatewayUrl?.trim();
440
+ if (!configured)
441
+ return null;
442
+ let configuredUrl;
443
+ try {
444
+ configuredUrl = new URL(configured);
445
+ }
446
+ catch (error) {
447
+ throw new WorkspaceGatewayConfigurationError(error);
448
+ }
449
+ if (configuredUrl.protocol !== "http:" &&
450
+ configuredUrl.protocol !== "https:") {
451
+ throw new WorkspaceGatewayConfigurationError();
452
+ }
453
+ let url;
454
+ try {
455
+ url = new URL(projectEnvironmentUrl(configuredUrl.toString()));
456
+ }
457
+ catch (error) {
458
+ throw new WorkspaceGatewayConfigurationError(error);
459
+ }
460
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
461
+ throw new WorkspaceGatewayConfigurationError();
462
+ }
463
+ return new URL(addBasePath(url.toString(), getConfiguredAppBasePath()));
464
+ }
465
+ function workspaceGatewayAppUrl(appPath) {
466
+ const baseUrl = workspaceGatewayBaseUrl();
467
+ if (!baseUrl)
468
+ return null;
469
+ const requestOrigin = getRequestContext()?.requestOrigin;
470
+ if (requestOrigin && new URL(requestOrigin).origin === baseUrl.origin) {
471
+ return null;
472
+ }
473
+ return appendWorkspaceAppPath(baseUrl, appPath);
474
+ }
475
+ function addBasePath(urlValue, basePath) {
476
+ if (!basePath)
477
+ return urlValue;
478
+ const url = new URL(urlValue);
479
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
480
+ if (pathname === basePath || pathname.startsWith(`${basePath}/`)) {
481
+ return url.toString();
482
+ }
483
+ url.pathname = `${basePath}${pathname === "/" ? "" : pathname}` || "/";
484
+ return url.toString();
485
+ }
406
486
  function isLocalWorkspaceGateway(url) {
407
487
  const hostname = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
408
488
  return (hostname === "localhost" ||
@@ -410,18 +490,39 @@ function isLocalWorkspaceGateway(url) {
410
490
  hostname === "0.0.0.0" ||
411
491
  hostname === "::1");
412
492
  }
413
- function workspaceAppLink(appPath, explicitUrl) {
414
- const urlValue = typeof explicitUrl === "string" ? explicitUrl.trim() : "";
493
+ function workspaceAppPagePath(appId, urlValue) {
494
+ const route = workspaceAppRoute(appId);
415
495
  if (!urlValue)
416
- return workspaceAppUrl(appPath);
417
- if (urlValue.startsWith("/"))
418
- return workspaceAppUrl(urlValue) ?? urlValue;
419
- try {
420
- return projectEnvironmentUrl(new URL(urlValue).toString());
421
- }
422
- catch {
423
- return urlValue;
424
- }
496
+ return route;
497
+ const parsed = new URL(urlValue, "https://agent-native.invalid");
498
+ return `${route}${parsed.search}${parsed.hash}`;
499
+ }
500
+ function workspaceAppLink(appId, explicitUrl) {
501
+ const urlValue = typeof explicitUrl === "string" ? explicitUrl.trim() : "";
502
+ const routePath = workspaceAppPagePath(appId, urlValue || undefined);
503
+ if (!urlValue) {
504
+ return (workspaceGatewayAppUrl(routePath) ??
505
+ workspaceAppUrl(routePath) ??
506
+ routePath);
507
+ }
508
+ if (urlValue.startsWith("/")) {
509
+ return (workspaceGatewayAppUrl(routePath) ??
510
+ workspaceAppUrl(routePath) ??
511
+ routePath);
512
+ }
513
+ const resolved = projectEnvironmentUrl(new URL(urlValue).toString());
514
+ const gateway = workspaceGatewayBaseUrl();
515
+ if (gateway && new URL(resolved).origin === gateway.origin) {
516
+ return (workspaceGatewayAppUrl(routePath) ??
517
+ workspaceAppUrl(routePath) ??
518
+ routePath);
519
+ }
520
+ const runtimeBase = resolveAppRuntimeUrl();
521
+ if (runtimeBase && new URL(resolved).origin === new URL(runtimeBase).origin) {
522
+ const runtimeUrl = new URL(addBasePath(projectEnvironmentUrl(runtimeBase), getConfiguredAppBasePath()));
523
+ return appendWorkspaceAppPath(runtimeUrl, routePath);
524
+ }
525
+ return resolved;
425
526
  }
426
527
  function normalizeWorkspaceAppAudience(value) {
427
528
  return value === "public" ? "public" : DEFAULT_WORKSPACE_APP_AUDIENCE;
@@ -528,7 +629,7 @@ function workspaceAppRouteAccessFromPackageJson(pkg) {
528
629
  record.workspaceAppProtectedPaths),
529
630
  };
530
631
  }
531
- function parseWorkspaceAppsManifest(parsed) {
632
+ function parseWorkspaceAppsManifest(parsed, onInvalidAppUrl) {
532
633
  const rawApps = Array.isArray(parsed?.apps)
533
634
  ? parsed.apps
534
635
  : Array.isArray(parsed)
@@ -550,6 +651,17 @@ function parseWorkspaceAppsManifest(parsed) {
550
651
  if (!profile)
551
652
  return null;
552
653
  const metadata = workspaceAppMetadataFromRecord(entry);
654
+ let url;
655
+ try {
656
+ url = workspaceAppLink(id, entry.url);
657
+ }
658
+ catch (error) {
659
+ if (error instanceof WorkspaceGatewayConfigurationError)
660
+ throw error;
661
+ onInvalidAppUrl?.();
662
+ console.warn(`[dispatch] Could not resolve Workspace app ${id}; skipping app`, error);
663
+ return null;
664
+ }
553
665
  return {
554
666
  id,
555
667
  name: typeof entry.name === "string" && entry.name.trim()
@@ -559,7 +671,7 @@ function parseWorkspaceAppsManifest(parsed) {
559
671
  ...(profile.whenToUse ? { whenToUse: profile.whenToUse } : {}),
560
672
  path: pathValue,
561
673
  homePath: normalizeWorkspaceAppHomePath(entry.homePath),
562
- url: workspaceAppLink(pathValue, entry.url),
674
+ url,
563
675
  isDispatch: typeof entry.isDispatch === "boolean"
564
676
  ? entry.isDispatch
565
677
  : id === "dispatch",
@@ -587,8 +699,11 @@ function parseWorkspaceAppsGatewayResponse(parsed) {
587
699
  return null;
588
700
  if (rawApps.length === 0)
589
701
  return [];
590
- const apps = parseWorkspaceAppsManifest(parsed);
591
- if (!apps || apps.length !== rawApps.length)
702
+ let invalidUrlCount = 0;
703
+ const apps = parseWorkspaceAppsManifest(parsed, () => {
704
+ invalidUrlCount += 1;
705
+ });
706
+ if (!apps || apps.length + invalidUrlCount !== rawApps.length)
592
707
  return null;
593
708
  const ids = new Set(apps.map((app) => app.id.toLowerCase()));
594
709
  return ids.size === apps.length ? apps : null;
@@ -1359,29 +1474,25 @@ function readWorkspaceAppsFromEnv() {
1359
1474
  if (!raw)
1360
1475
  return null;
1361
1476
  try {
1362
- return parseWorkspaceAppsManifest(JSON.parse(raw));
1477
+ const apps = parseWorkspaceAppsManifest(JSON.parse(raw));
1478
+ if (apps)
1479
+ return apps;
1480
+ console.warn(`[dispatch] Could not parse Workspace app manifest from ${WORKSPACE_APPS_ENV_KEY}; skipping source`);
1363
1481
  }
1364
- catch {
1365
- return null;
1482
+ catch (error) {
1483
+ if (error instanceof WorkspaceGatewayConfigurationError)
1484
+ throw error;
1485
+ console.warn(`[dispatch] Could not read Workspace app manifest from ${WORKSPACE_APPS_ENV_KEY}; skipping source`);
1366
1486
  }
1487
+ return null;
1367
1488
  }
1368
1489
  async function readWorkspaceAppsFromGateway() {
1369
- const configuredBase = process.env.WORKSPACE_GATEWAY_URL;
1490
+ const configuredBase = getAppConfig().workspace.gatewayUrl?.trim();
1370
1491
  if (!configuredBase)
1371
1492
  return null;
1372
- const base = projectEnvironmentUrl(configuredBase);
1373
- let baseUrl;
1374
- try {
1375
- baseUrl = new URL(base);
1376
- }
1377
- catch {
1378
- // coercion-ok: malformed gateway configuration is an unavailable registry
1379
- // and falls back to local sources.
1380
- return null;
1381
- }
1382
- if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
1493
+ const baseUrl = workspaceGatewayBaseUrl();
1494
+ if (!baseUrl)
1383
1495
  return null;
1384
- }
1385
1496
  const requestContext = getRequestContext();
1386
1497
  const isLocalGateway = isLocalWorkspaceGateway(baseUrl);
1387
1498
  const requestOrigin = requestContext?.requestOrigin;
@@ -1563,9 +1674,16 @@ function readWorkspaceAppsFromManifestFile() {
1563
1674
  for (const file of workspaceAppsManifestCandidates()) {
1564
1675
  if (!fs.existsSync(file))
1565
1676
  continue;
1566
- const apps = parseWorkspaceAppsManifest(readJson(file));
1567
- if (apps)
1568
- return apps;
1677
+ try {
1678
+ const apps = parseWorkspaceAppsManifest(readJson(file));
1679
+ if (apps)
1680
+ return apps;
1681
+ }
1682
+ catch (error) {
1683
+ if (error instanceof WorkspaceGatewayConfigurationError)
1684
+ throw error;
1685
+ console.warn(`[dispatch] Could not read Workspace app manifest ${file}; skipping source`);
1686
+ }
1569
1687
  }
1570
1688
  return null;
1571
1689
  }
@@ -1578,7 +1696,14 @@ async function readWorkspaceAppsFromFilesystem(workspaceRoot) {
1578
1696
  .readdirSync(appsDir, { withFileTypes: true })
1579
1697
  .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))) {
1580
1698
  const appDir = path.join(appsDir, entry.name);
1581
- const pkg = readJson(path.join(appDir, "package.json"));
1699
+ let pkg;
1700
+ try {
1701
+ pkg = readJson(path.join(appDir, "package.json"));
1702
+ }
1703
+ catch (error) {
1704
+ console.warn(`[dispatch] Could not read workspace app ${entry.name}; skipping app`, error);
1705
+ continue;
1706
+ }
1582
1707
  if (!pkg)
1583
1708
  continue;
1584
1709
  const routeAccess = workspaceAppRouteAccessFromPackageJson(pkg);
@@ -1601,7 +1726,7 @@ async function readWorkspaceAppsFromFilesystem(workspaceRoot) {
1601
1726
  description: pkg.description || "",
1602
1727
  path: `/${entry.name}`,
1603
1728
  homePath: normalizeWorkspaceAppHomePath(configuredHomePath ?? inferredHomePath),
1604
- url: workspaceAppUrl(`/${entry.name}`),
1729
+ url: workspaceAppUrl(workspaceAppRoute(entry.name)),
1605
1730
  isDispatch: entry.name === "dispatch",
1606
1731
  audience: workspaceAppAudienceFromPackageJson(pkg) ??
1607
1732
  DEFAULT_WORKSPACE_APP_AUDIENCE,
@@ -1998,7 +2123,7 @@ export async function scaffoldWorkspaceAppFromTemplate(input) {
1998
2123
  description: "",
1999
2124
  path: `/${appId}`,
2000
2125
  homePath: "/home",
2001
- url: workspaceAppUrl(`/${appId}`),
2126
+ url: workspaceAppUrl(workspaceAppRoute(appId)),
2002
2127
  isDispatch: false,
2003
2128
  audience: DEFAULT_WORKSPACE_APP_AUDIENCE,
2004
2129
  publicPaths: [],
@@ -2293,8 +2418,8 @@ function buildWorkspaceAppPrompt(input) {
2293
2418
  `Dispatch workspace resources selected for this app:\n${resourceList}`,
2294
2419
  `Dispatch workspace resources with scope=all are global. After the app exists, sync workspace resources to appId "${appId}" so global skills, guardrail instructions, and reference resources reach the new app even when no per-app resources were selected.`,
2295
2420
  "",
2296
- `Use the workspace app layout: create it under apps/${appId}, mount it at /${appId}, keep it on the shared workspace database/hosting model, and avoid table-name collisions by namespacing any new domain tables to the app.`,
2297
- `Important routing rule: from outside the app, link to /${appId}; inside apps/${appId}, React Router routes are app-local. Use <Link to="/review"> and navigate("/review"), not "/${appId}/review"; APP_BASE_PATH supplies the mounted prefix, and hardcoding it causes doubled URLs like /${appId}/${appId}/review.`,
2421
+ `Use the workspace app layout: create it under apps/${appId}; Dispatch mounts it at /apps/${appId}. Keep it on the shared workspace database/hosting model, and avoid table-name collisions by namespacing any new domain tables to the app.`,
2422
+ `Important routing rule: from outside the app, link to /apps/${appId}; inside apps/${appId}, React Router routes are app-local. Use <Link to="/review"> and navigate("/review"), not "/apps/${appId}/review"; APP_BASE_PATH supplies the mounted prefix, and hardcoding it causes doubled URLs.`,
2298
2423
  `Home route contract: Dispatch opens the registered app.homePath. If the main screen is app/routes/_index.tsx and there is no app/routes/home.tsx or app/routes/_app.home.tsx, set app.homePath to "/" in server/plugins/config.ts. If the app uses a /home route, keep the default "/home". Never leave the registry pointing at /home when that route does not exist.`,
2299
2424
  "Existing first-party apps are neighbors, not implementation details for this app. If the user prompt mentions Mail, Calendar, Analytics, Dispatch, or other templates, treat them as existing hosted/connected apps that this app can link to or call through A2A/default connected agents. For example, Mail, Calendar, and Analytics already exist at https://mail.agent-native.com, https://calendar.agent-native.com, and https://analytics.agent-native.com.",
2300
2425
  `Do not create wrapper apps or scaffold child apps/routes for Mail, Calendar, Analytics, etc. inside apps/${appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
@@ -2313,7 +2438,7 @@ function buildWorkspaceAppPrompt(input) {
2313
2438
  "- Use Tabler Icons (@tabler/icons-react) for every icon. Never use emojis as icons.",
2314
2439
  `- Expose what the user is looking at via application_state (navigation.view, selection, etc.) so the agent has live context. Mirror the patterns in templates/mail or templates/slides.`,
2315
2440
  "- Optimistic UI for every mutation: update the React Query cache immediately, navigate immediately, run the mutation in the background, roll back on error. Don't await a server round-trip before re-rendering.",
2316
- `- Commit an agent-native.json at apps/${appId}/agent-native.json with { "version": 1, "onboarding": { "firstRun": { "development": "connect", "production": "connect-and-integrations" } } }. Keep the shared Use Builder.io / Add your own keys onboarding visible; never build a second, custom credential form or hardcode a provider key.`,
2441
+ `- Commit an agent-native.json at apps/${appId}/agent-native.json with { "version": 1, "onboarding": { "firstRun": { "development": "off", "production": "connect-and-integrations" } } }, as the first-party templates do, so local runs open straight into the app. Keep the shared Use Builder.io / Add your own keys onboarding for production; never build a second, custom credential form or hardcode a provider key.`,
2317
2442
  "- Every AI-labeled button must call sendToAgentChat with openSidebar: true — plus submit: true for one-click work, or submit: false when the user should review/edit the proposed prompt first. Keep follow-ups in that same sidebar thread; don't add a second freeform input beside the result. Never use sparkle, wand, magic, robot, or other decorative AI icons on those buttons — a message or neutral action icon, or no icon, instead.",
2318
2443
  "- Choose a named visual direction in DESIGN.md before styling the first screen and build to it. Don't inherit a sibling app's palette unbuilt.",
2319
2444
  '- Left navigation must name real domain destinations, not "Chat" as the only or default entry.',
@@ -2637,7 +2762,7 @@ export async function startWorkspaceAppCreation(input) {
2637
2762
  projectId: builderProjectId,
2638
2763
  branchName: result.branchName,
2639
2764
  url: result.url,
2640
- workspaceUrl: workspaceAppUrl(`/${built.appId}`),
2765
+ workspaceUrl: workspaceAppUrl(workspaceAppRoute(built.appId)),
2641
2766
  status: result.status,
2642
2767
  message: `Builder started a branch for /${built.appId}. Use the Builder branch URL to track creation now. ` +
2643
2768
  `The workspace path will be live after that branch is merged and the workspace deploy finishes, so it may 404 until then.`,