@agent-native/dispatch 0.15.20 → 0.15.21

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 (32) hide show
  1. package/dist/actions/provider-api-catalog.d.ts +1 -0
  2. package/dist/actions/provider-api-register.d.ts +14 -14
  3. package/dist/actions/remix-workspace-template.d.ts +20 -32
  4. package/dist/actions/set-app-creation-settings.js +2 -2
  5. package/dist/actions/set-app-creation-settings.js.map +1 -1
  6. package/dist/actions/start-workspace-app-creation.d.ts +1 -45
  7. package/dist/components/create-app-popover.d.ts.map +1 -1
  8. package/dist/components/create-app-popover.js +30 -2
  9. package/dist/components/create-app-popover.js.map +1 -1
  10. package/dist/components/layout/Layout.d.ts.map +1 -1
  11. package/dist/components/layout/Layout.js +1 -1
  12. package/dist/components/layout/Layout.js.map +1 -1
  13. package/dist/components/workspace-template-card.d.ts.map +1 -1
  14. package/dist/components/workspace-template-card.js +14 -1
  15. package/dist/components/workspace-template-card.js.map +1 -1
  16. package/dist/server/lib/app-creation-store.d.ts +36 -37
  17. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  18. package/dist/server/lib/app-creation-store.js +71 -6
  19. package/dist/server/lib/app-creation-store.js.map +1 -1
  20. package/dist/server/lib/provider-api.d.ts +1 -0
  21. package/dist/server/lib/provider-api.d.ts.map +1 -1
  22. package/dist/server/plugins/integrations.js +1 -1
  23. package/dist/server/plugins/integrations.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/actions/set-app-creation-settings.ts +4 -2
  26. package/src/components/create-app-popover.spec.tsx +249 -0
  27. package/src/components/create-app-popover.tsx +77 -10
  28. package/src/components/layout/Layout.tsx +9 -6
  29. package/src/components/workspace-template-card.tsx +11 -1
  30. package/src/server/lib/app-creation-store.spec.ts +252 -0
  31. package/src/server/lib/app-creation-store.ts +148 -8
  32. package/src/server/plugins/integrations.ts +1 -1
@@ -5,6 +5,8 @@ import {
5
5
  generateWorkspaceAppDescription,
6
6
  listAvailableWorkspaceTemplates,
7
7
  listWorkspaceApps,
8
+ setAppCreationSettings,
9
+ startWorkspaceAppCreation,
8
10
  updateWorkspaceAppMetadata,
9
11
  } from "./app-creation-store.js";
10
12
 
@@ -28,6 +30,35 @@ const mocks = vi.hoisted(() => {
28
30
  rows: state.orgRole ? [{ role: state.orgRole }] : [],
29
31
  })),
30
32
  })),
33
+ resolveBuilderCredentialsDetailed: vi.fn(async () => ({
34
+ privateKey: null as string | null,
35
+ publicKey: null as string | null,
36
+ userId: null as string | null,
37
+ orgName: null,
38
+ orgKind: null,
39
+ subscription: null,
40
+ subscriptionLevel: null,
41
+ subscriptionName: null,
42
+ isEnterprise: null,
43
+ isFreeAccount: null,
44
+ source: null,
45
+ lookupFailed: false,
46
+ })),
47
+ runBuilderAgent: vi.fn(),
48
+ resolveBuilderBranchProjectId: vi.fn(async () => ""),
49
+ getBuilderBranchProjectId: vi.fn(() => ""),
50
+ writeAppSecret: vi.fn(async () => "secret-id"),
51
+ deleteAppSecret: vi.fn(async () => true),
52
+ };
53
+ });
54
+
55
+ vi.mock("@agent-native/core/secrets", async (importOriginal) => {
56
+ const actual =
57
+ await importOriginal<typeof import("@agent-native/core/secrets")>();
58
+ return {
59
+ ...actual,
60
+ writeAppSecret: (...args: any[]) => mocks.writeAppSecret(...args),
61
+ deleteAppSecret: (...args: any[]) => mocks.deleteAppSecret(...args),
31
62
  };
32
63
  });
33
64
 
@@ -44,6 +75,21 @@ vi.mock("@agent-native/core/settings", () => ({
44
75
  putSetting: (...args: any[]) => mocks.putSetting(...args),
45
76
  }));
46
77
 
78
+ vi.mock("@agent-native/core/server", async (importOriginal) => {
79
+ const actual =
80
+ await importOriginal<typeof import("@agent-native/core/server")>();
81
+ return {
82
+ ...actual,
83
+ resolveBuilderCredentialsDetailed: (...args: any[]) =>
84
+ mocks.resolveBuilderCredentialsDetailed(...args),
85
+ runBuilderAgent: (...args: any[]) => mocks.runBuilderAgent(...args),
86
+ resolveBuilderBranchProjectId: (...args: any[]) =>
87
+ mocks.resolveBuilderBranchProjectId(...args),
88
+ getBuilderBranchProjectId: (...args: any[]) =>
89
+ mocks.getBuilderBranchProjectId(...args),
90
+ };
91
+ });
92
+
47
93
  vi.mock("./dispatch-store.js", async (importOriginal) => {
48
94
  const actual = await importOriginal<typeof import("./dispatch-store.js")>();
49
95
  return {
@@ -58,6 +104,22 @@ afterEach(() => {
58
104
  vi.clearAllMocks();
59
105
  mocks.settings.clear();
60
106
  mocks.state.orgRole = "admin";
107
+ mocks.resolveBuilderCredentialsDetailed.mockResolvedValue({
108
+ privateKey: null,
109
+ publicKey: null,
110
+ userId: null,
111
+ orgName: null,
112
+ orgKind: null,
113
+ subscription: null,
114
+ subscriptionLevel: null,
115
+ subscriptionName: null,
116
+ isEnterprise: null,
117
+ isFreeAccount: null,
118
+ source: null,
119
+ lookupFailed: false,
120
+ });
121
+ mocks.resolveBuilderBranchProjectId.mockResolvedValue("");
122
+ mocks.getBuilderBranchProjectId.mockReturnValue("");
61
123
  globalThis.fetch = originalFetch;
62
124
  });
63
125
 
@@ -377,3 +439,193 @@ describe("listWorkspaceApps", () => {
377
439
  expect(templates).toEqual([]);
378
440
  });
379
441
  });
442
+
443
+ describe("startWorkspaceAppCreation", () => {
444
+ const leakedProjectId = "940ebc5a83164aa6a37dde445e494f3a";
445
+
446
+ function stubHostedRuntime() {
447
+ vi.stubEnv("NODE_ENV", "production");
448
+ }
449
+
450
+ function stubBuilderProjectConfigured() {
451
+ vi.stubEnv("DISPATCH_BUILDER_PROJECT_ID", leakedProjectId);
452
+ }
453
+
454
+ function credentials(overrides: Record<string, unknown> = {}) {
455
+ return {
456
+ privateKey: null,
457
+ publicKey: null,
458
+ userId: null,
459
+ orgName: null,
460
+ orgKind: null,
461
+ subscription: null,
462
+ subscriptionLevel: null,
463
+ subscriptionName: null,
464
+ isEnterprise: null,
465
+ isFreeAccount: null,
466
+ source: null,
467
+ lookupFailed: false,
468
+ ...overrides,
469
+ };
470
+ }
471
+
472
+ function create(appId = "onboarding") {
473
+ return runWithRequestContext({ userEmail: "dev@example.test" }, () =>
474
+ startWorkspaceAppCreation({ prompt: "Track onboarding tasks", appId }),
475
+ );
476
+ }
477
+
478
+ it("returns builder-not-connected without leaking the project id when no Builder credentials are configured", async () => {
479
+ stubHostedRuntime();
480
+ stubBuilderProjectConfigured();
481
+ mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(credentials());
482
+
483
+ const result = (await create()) as any;
484
+
485
+ expect(result.mode).toBe("builder-unavailable");
486
+ expect(result.reason).toBe("builder-not-connected");
487
+ expect(result.message).not.toContain(leakedProjectId);
488
+ expect(mocks.runBuilderAgent).not.toHaveBeenCalled();
489
+ });
490
+
491
+ it("returns credential-store-unavailable when the credential lookup itself fails", async () => {
492
+ stubHostedRuntime();
493
+ stubBuilderProjectConfigured();
494
+ mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
495
+ credentials({ lookupFailed: true }),
496
+ );
497
+
498
+ const result = (await create()) as any;
499
+
500
+ expect(result.mode).toBe("builder-unavailable");
501
+ expect(result.reason).toBe("credential-store-unavailable");
502
+ expect(mocks.runBuilderAgent).not.toHaveBeenCalled();
503
+ });
504
+
505
+ it("returns builder-error with the raw failure in detail when runBuilderAgent throws", async () => {
506
+ stubHostedRuntime();
507
+ stubBuilderProjectConfigured();
508
+ mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
509
+ credentials({
510
+ privateKey: "priv",
511
+ publicKey: "pub",
512
+ userId: "builder-user-1",
513
+ }),
514
+ );
515
+ mocks.runBuilderAgent.mockRejectedValue(
516
+ new Error("Builder keys are not configured"),
517
+ );
518
+
519
+ const result = (await create()) as any;
520
+
521
+ expect(result.mode).toBe("builder-unavailable");
522
+ expect(result.reason).toBe("builder-error");
523
+ expect(result.detail).toBe("Builder keys are not configured");
524
+ expect(result.message).not.toContain(leakedProjectId);
525
+ });
526
+
527
+ it("starts the Builder branch and passes the resolved userId through", async () => {
528
+ stubHostedRuntime();
529
+ stubBuilderProjectConfigured();
530
+ mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
531
+ credentials({
532
+ privateKey: "priv",
533
+ publicKey: "pub",
534
+ userId: "builder-user-42",
535
+ }),
536
+ );
537
+ mocks.runBuilderAgent.mockResolvedValue({
538
+ branchName: "onboarding1",
539
+ url: "https://builder.io/app/projects/project-1/branch/onboarding1",
540
+ status: "processing",
541
+ });
542
+
543
+ const result = (await create()) as any;
544
+
545
+ expect(result.mode).toBe("builder");
546
+ expect(mocks.runBuilderAgent).toHaveBeenCalledWith(
547
+ expect.objectContaining({ userId: "builder-user-42" }),
548
+ );
549
+ });
550
+
551
+ it("returns coming-soon when no Builder project is configured", async () => {
552
+ stubHostedRuntime();
553
+
554
+ const result = (await create()) as any;
555
+
556
+ expect(result.mode).toBe("coming-soon");
557
+ expect(mocks.resolveBuilderCredentialsDetailed).not.toHaveBeenCalled();
558
+ });
559
+ });
560
+
561
+ describe("setAppCreationSettings", () => {
562
+ const projectId = "274d28fec94b48f2b2d68f2274d390eb";
563
+ const orgId = "builder_io";
564
+
565
+ function save(
566
+ builderProjectId: string | null,
567
+ ctx: { userEmail: string; orgId?: string } = {
568
+ userEmail: "dev@example.test",
569
+ orgId,
570
+ },
571
+ ) {
572
+ return runWithRequestContext(ctx, () =>
573
+ setAppCreationSettings({ builderProjectId }),
574
+ );
575
+ }
576
+
577
+ it("stores the project id as an org-scoped credential so member apps resolve it", async () => {
578
+ await save(projectId);
579
+
580
+ expect(mocks.writeAppSecret).toHaveBeenCalledWith(
581
+ expect.objectContaining({
582
+ key: "BUILDER_BRANCH_PROJECT_ID",
583
+ value: projectId,
584
+ scope: "org",
585
+ scopeId: orgId,
586
+ }),
587
+ );
588
+ expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
589
+ });
590
+
591
+ it("scopes the credential to one organization rather than every tenant", async () => {
592
+ await save(projectId);
593
+
594
+ const [args] = mocks.writeAppSecret.mock.calls.at(-1) as [
595
+ { scope: string; scopeId: string },
596
+ ];
597
+ expect(args.scope).not.toBe("user");
598
+ expect(args.scopeId).toBe(orgId);
599
+ });
600
+
601
+ it("falls back to a solo workspace scope when there is no active org", async () => {
602
+ await save(projectId, { userEmail: "dev@example.test" });
603
+
604
+ expect(mocks.writeAppSecret).toHaveBeenCalledWith(
605
+ expect.objectContaining({
606
+ scope: "workspace",
607
+ scopeId: "solo:dev@example.test",
608
+ }),
609
+ );
610
+ });
611
+
612
+ it("removes the credential when the project id is cleared", async () => {
613
+ await save(null);
614
+
615
+ expect(mocks.deleteAppSecret).toHaveBeenCalledWith({
616
+ key: "BUILDER_BRANCH_PROJECT_ID",
617
+ scope: "org",
618
+ scopeId: orgId,
619
+ });
620
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
621
+ });
622
+
623
+ it("does not save the setting when the credential write fails", async () => {
624
+ mocks.writeAppSecret.mockRejectedValueOnce(
625
+ new Error("credential store down"),
626
+ );
627
+
628
+ await expect(save(projectId)).rejects.toThrow("credential store down");
629
+ expect(mocks.putSetting).not.toHaveBeenCalled();
630
+ });
631
+ });
@@ -4,12 +4,17 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  import { getDbExec } from "@agent-native/core/db";
7
+ import {
8
+ deleteAppSecret,
9
+ writeAppSecret,
10
+ type SecretScope,
11
+ } from "@agent-native/core/secrets";
7
12
  import {
8
13
  getBuilderBranchProjectId,
9
14
  getRequestContext,
10
15
  isIntegrationCallerRequest,
11
16
  resolveBuilderBranchProjectId,
12
- resolveBuilderCredentials,
17
+ resolveBuilderCredentialsDetailed,
13
18
  runBuilderAgent,
14
19
  } from "@agent-native/core/server";
15
20
  import { getSetting, putSetting } from "@agent-native/core/settings";
@@ -30,6 +35,9 @@ import {
30
35
  } from "./workspace-resources-store.js";
31
36
 
32
37
  const SETTINGS_KEY = "dispatch-app-creation-settings";
38
+ const BUILDER_BRANCH_PROJECT_SECRET_KEY = "BUILDER_BRANCH_PROJECT_ID";
39
+ const BUILDER_BRANCH_PROJECT_SECRET_DESCRIPTION =
40
+ "Builder project for cloud code-change branches (set in Dispatch)";
33
41
  const WORKSPACE_APP_METADATA_SETTINGS_KEY = "workspace-app-metadata";
34
42
  const WORKSPACE_APPS_ENV_KEY = "AGENT_NATIVE_WORKSPACE_APPS_JSON";
35
43
  const WORKSPACE_APPS_MANIFEST_FILE = "workspace-apps.json";
@@ -209,6 +217,16 @@ function scopedSettingsKey(): string {
209
217
  return `${SETTINGS_KEY}:user:${currentOwnerEmail()}`;
210
218
  }
211
219
 
220
+ function builderProjectSecretTarget(): {
221
+ scope: Extract<SecretScope, "org" | "workspace">;
222
+ scopeId: string;
223
+ } | null {
224
+ const orgId = currentOrgId();
225
+ if (orgId) return { scope: "org", scopeId: orgId };
226
+ const email = currentOwnerEmail();
227
+ return email ? { scope: "workspace", scopeId: `solo:${email}` } : null;
228
+ }
229
+
212
230
  function workspaceAppMetadataSettingsKey(): string {
213
231
  const orgId = currentOrgId();
214
232
  if (orgId) return `${WORKSPACE_APP_METADATA_SETTINGS_KEY}:org:${orgId}`;
@@ -1500,6 +1518,29 @@ export async function setAppCreationSettings(input: {
1500
1518
  await assertCanManageAppCreationSettings();
1501
1519
  const builderProjectId = input.builderProjectId?.trim() || null;
1502
1520
  const raw = await readSettingsRecord();
1521
+
1522
+ // The credential store, not this settings row, is what
1523
+ // `resolveBuilderBranchProjectId()` reads. Write it first: a saved setting
1524
+ // whose secret never landed reports the project as configured while cloud
1525
+ // code changes stay silently disabled.
1526
+ const secretTarget = builderProjectSecretTarget();
1527
+ if (secretTarget) {
1528
+ const ref = {
1529
+ key: BUILDER_BRANCH_PROJECT_SECRET_KEY,
1530
+ scope: secretTarget.scope,
1531
+ scopeId: secretTarget.scopeId,
1532
+ };
1533
+ if (builderProjectId) {
1534
+ await writeAppSecret({
1535
+ ...ref,
1536
+ value: builderProjectId,
1537
+ description: BUILDER_BRANCH_PROJECT_SECRET_DESCRIPTION,
1538
+ });
1539
+ } else {
1540
+ await deleteAppSecret(ref);
1541
+ }
1542
+ }
1543
+
1503
1544
  await putSetting(scopedSettingsKey(), { ...raw, builderProjectId });
1504
1545
  await recordAudit({
1505
1546
  action: "settings.updated",
@@ -1649,7 +1690,7 @@ function normalizeBuilderRunResult(result: unknown): {
1649
1690
  }
1650
1691
 
1651
1692
  async function remoteAppCreationAuthorization(): Promise<
1652
- { ok: true } | { ok: false; message: string }
1693
+ { ok: true } | { ok: false; reason: "identity-not-linked"; message: string }
1653
1694
  > {
1654
1695
  const ownerEmail = currentOwnerEmail();
1655
1696
  const isIntegrationCaller = isIntegrationCallerRequest();
@@ -1658,6 +1699,7 @@ async function remoteAppCreationAuthorization(): Promise<
1658
1699
  if (await defaultOwnerAppCreationAllowed()) return { ok: true };
1659
1700
  return {
1660
1701
  ok: false,
1702
+ reason: "identity-not-linked",
1661
1703
  message:
1662
1704
  "Messaging-triggered app creation is using the deployment default Dispatch owner. " +
1663
1705
  "Link the messaging identity to a Dispatch user with /link, start the app from Dispatch while signed in, or explicitly set ENABLE_BUILDER=true for this deployment.",
@@ -1670,6 +1712,7 @@ async function remoteAppCreationAuthorization(): Promise<
1670
1712
  : "Synthetic integration";
1671
1713
  return {
1672
1714
  ok: false,
1715
+ reason: "identity-not-linked",
1673
1716
  message:
1674
1717
  `${source} app creation needs a trusted Dispatch owner before Builder can start a branch. ` +
1675
1718
  "Link the messaging identity to a Dispatch user with /link, start the app from Dispatch while signed in, or explicitly set ENABLE_BUILDER=true for this deployment.",
@@ -1788,6 +1831,65 @@ async function grantSelectedWorkspaceResources(input: {
1788
1831
  await grantWorkspaceResourcesToApp(input);
1789
1832
  }
1790
1833
 
1834
+ /**
1835
+ * Discriminates why `startWorkspaceAppCreation` could not hand off to Builder.
1836
+ * UIs and agents should branch on this instead of parsing `message` text.
1837
+ */
1838
+ export type AppCreationUnavailableReason =
1839
+ | "identity-not-linked"
1840
+ | "builder-not-connected"
1841
+ | "credential-store-unavailable"
1842
+ | "builder-error";
1843
+
1844
+ export interface AppCreationIdentityUnavailableResult {
1845
+ mode: "builder-unavailable";
1846
+ appId: string;
1847
+ reason: "identity-not-linked";
1848
+ message: string;
1849
+ }
1850
+
1851
+ export interface AppCreationBuilderUnavailableResult {
1852
+ mode: "builder-unavailable";
1853
+ appId: string;
1854
+ reason: Exclude<AppCreationUnavailableReason, "identity-not-linked">;
1855
+ message: string;
1856
+ /** Raw underlying error text for agents/operators debugging the deployment. */
1857
+ detail?: string;
1858
+ projectId: string;
1859
+ }
1860
+
1861
+ export interface AppCreationLocalAgentResult {
1862
+ mode: "local-agent";
1863
+ appId: string;
1864
+ prompt: string;
1865
+ message: string;
1866
+ }
1867
+
1868
+ export interface AppCreationComingSoonResult {
1869
+ mode: "coming-soon";
1870
+ appId: string;
1871
+ message: string;
1872
+ }
1873
+
1874
+ export interface AppCreationBuilderResult {
1875
+ mode: "builder";
1876
+ appId: string;
1877
+ path: string;
1878
+ projectId: string;
1879
+ branchName: string;
1880
+ url: string;
1881
+ workspaceUrl: string | null;
1882
+ status: string;
1883
+ message: string;
1884
+ }
1885
+
1886
+ export type StartWorkspaceAppCreationResult =
1887
+ | AppCreationIdentityUnavailableResult
1888
+ | AppCreationBuilderUnavailableResult
1889
+ | AppCreationLocalAgentResult
1890
+ | AppCreationComingSoonResult
1891
+ | AppCreationBuilderResult;
1892
+
1791
1893
  export async function startWorkspaceAppCreation(input: {
1792
1894
  prompt: string;
1793
1895
  appId?: string | null;
@@ -1795,7 +1897,7 @@ export async function startWorkspaceAppCreation(input: {
1795
1897
  template?: string | null;
1796
1898
  secretIds?: string[];
1797
1899
  resourceIds?: string[];
1798
- }) {
1900
+ }): Promise<StartWorkspaceAppCreationResult> {
1799
1901
  const initial = buildWorkspaceAppPrompt({
1800
1902
  prompt: input.prompt,
1801
1903
  appId: input.appId,
@@ -1811,6 +1913,7 @@ export async function startWorkspaceAppCreation(input: {
1811
1913
  return {
1812
1914
  mode: "builder-unavailable",
1813
1915
  appId: initial.appId,
1916
+ reason: authorization.reason,
1814
1917
  message: authorization.message,
1815
1918
  };
1816
1919
  }
@@ -1866,14 +1969,51 @@ export async function startWorkspaceAppCreation(input: {
1866
1969
  };
1867
1970
  }
1868
1971
 
1972
+ let builderCreds: Awaited<
1973
+ ReturnType<typeof resolveBuilderCredentialsDetailed>
1974
+ >;
1975
+ try {
1976
+ builderCreds = await resolveBuilderCredentialsDetailed();
1977
+ } catch {
1978
+ return {
1979
+ mode: "builder-unavailable",
1980
+ appId: built.appId,
1981
+ reason: "credential-store-unavailable",
1982
+ projectId: settings.builderProjectId,
1983
+ message:
1984
+ "Could not read your Builder connection just now. Try creating the app again in a moment.",
1985
+ };
1986
+ }
1987
+
1988
+ if (builderCreds.lookupFailed) {
1989
+ return {
1990
+ mode: "builder-unavailable",
1991
+ appId: built.appId,
1992
+ reason: "credential-store-unavailable",
1993
+ projectId: settings.builderProjectId,
1994
+ message:
1995
+ "Could not read your Builder connection just now. Try creating the app again in a moment.",
1996
+ };
1997
+ }
1998
+
1999
+ if (!builderCreds.privateKey || !builderCreds.publicKey) {
2000
+ return {
2001
+ mode: "builder-unavailable",
2002
+ appId: built.appId,
2003
+ reason: "builder-not-connected",
2004
+ projectId: settings.builderProjectId,
2005
+ message: "Connect your Builder account to create apps from Dispatch.",
2006
+ };
2007
+ }
2008
+
2009
+ const builderUserId = builderCreds.userId || undefined;
2010
+
1869
2011
  let result: {
1870
2012
  branchName: string;
1871
2013
  url: string;
1872
2014
  status: string;
1873
2015
  };
1874
2016
  try {
1875
- const builderCreds = await resolveBuilderCredentials().catch(() => null);
1876
- const builderUserId = builderCreds?.userId || undefined;
1877
2017
  result = normalizeBuilderRunResult(
1878
2018
  await runBuilderAgent({
1879
2019
  prompt,
@@ -1891,11 +2031,11 @@ export async function startWorkspaceAppCreation(input: {
1891
2031
  return {
1892
2032
  mode: "builder-unavailable",
1893
2033
  appId: built.appId,
2034
+ reason: "builder-error",
1894
2035
  projectId: settings.builderProjectId,
2036
+ detail,
1895
2037
  message:
1896
- `Builder app creation is configured for project ${settings.builderProjectId}, ` +
1897
- `but it could not start yet: ${detail}. Connect Builder for this user, ` +
1898
- `link the messaging identity to that user, or configure deployment-managed Builder credentials for this workspace.`,
2038
+ "Builder could not start the app branch. This is usually temporary try again.",
1899
2039
  };
1900
2040
  }
1901
2041
 
@@ -42,7 +42,7 @@ When a user asks for something:
42
42
  - If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
43
43
  - If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.
44
44
  - If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
45
- - If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode "builder-unavailable", report the action's message exactly enough to preserve the missing identity/credential detail.
45
+ - If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode "builder-unavailable", the action also returns a \`reason\` code plus a user-facing \`message\` and (for some reasons) an operator-facing \`detail\`. Relay the \`message\` to the user as-is; only surface \`detail\` when the user is clearly an operator debugging the deployment (e.g. troubleshooting a broken Builder connection for the workspace), never as routine detail for an end user.
46
46
  - For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.
47
47
  - Keep responses concise and operational — messaging platforms have character limits.
48
48
  - Use markdown sparingly (bold and lists are fine, avoid complex formatting).