@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.
- package/CHANGELOG.md +42 -0
- package/dist/actions/create_embed_session.js +1 -0
- package/dist/actions/create_embed_session.js.map +1 -1
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/provider-api-register.d.ts +13 -13
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -11
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +23 -10
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +2 -2
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +35 -5
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +173 -48
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.js +1 -1
- package/dist/server/lib/community-apps.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +1 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +170 -38
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/provider-api.d.ts +2 -2
- package/dist/shared/workspace-app-route.d.ts +2 -0
- package/dist/shared/workspace-app-route.d.ts.map +1 -0
- package/dist/shared/workspace-app-route.js +4 -0
- package/dist/shared/workspace-app-route.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/create_embed_session.ts +1 -0
- package/src/actions/index.spec.ts +3 -0
- package/src/components/layout/Layout.spec.tsx +39 -9
- package/src/components/layout/Layout.tsx +19 -40
- package/src/components/workspace-app-host.spec.tsx +88 -1
- package/src/components/workspace-app-host.tsx +30 -9
- package/src/lib/workspace-apps.spec.ts +64 -5
- package/src/lib/workspace-apps.ts +36 -6
- package/src/routes/pages/apps.$appId.spec.tsx +21 -3
- package/src/server/lib/app-creation-store.spec.ts +502 -11
- package/src/server/lib/app-creation-store.ts +198 -50
- package/src/server/lib/community-apps.ts +1 -1
- package/src/server/lib/mcp-gateway.spec.ts +396 -9
- package/src/server/lib/mcp-gateway.ts +209 -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
414
|
-
const
|
|
493
|
+
function workspaceAppPagePath(appId, urlValue) {
|
|
494
|
+
const route = workspaceAppRoute(appId);
|
|
415
495
|
if (!urlValue)
|
|
416
|
-
return
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
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
|
-
|
|
591
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1490
|
+
const configuredBase = getAppConfig().workspace.gatewayUrl?.trim();
|
|
1370
1491
|
if (!configuredBase)
|
|
1371
1492
|
return null;
|
|
1372
|
-
const
|
|
1373
|
-
|
|
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
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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}
|
|
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
|
|
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": "
|
|
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(
|
|
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.`,
|