@agent-native/dispatch 0.8.5 → 0.8.6

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 (78) hide show
  1. package/dist/actions/ask_app.d.ts +3 -0
  2. package/dist/actions/ask_app.d.ts.map +1 -0
  3. package/dist/actions/ask_app.js +12 -0
  4. package/dist/actions/ask_app.js.map +1 -0
  5. package/dist/actions/create_embed_session.d.ts +3 -0
  6. package/dist/actions/create_embed_session.d.ts.map +1 -0
  7. package/dist/actions/create_embed_session.js +28 -0
  8. package/dist/actions/create_embed_session.js.map +1 -0
  9. package/dist/actions/index.d.ts.map +1 -1
  10. package/dist/actions/index.js +12 -0
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/actions/list-mcp-app-access.d.ts +3 -0
  13. package/dist/actions/list-mcp-app-access.d.ts.map +1 -0
  14. package/dist/actions/list-mcp-app-access.js +25 -0
  15. package/dist/actions/list-mcp-app-access.js.map +1 -0
  16. package/dist/actions/list_apps.d.ts +3 -0
  17. package/dist/actions/list_apps.d.ts.map +1 -0
  18. package/dist/actions/list_apps.js +26 -0
  19. package/dist/actions/list_apps.js.map +1 -0
  20. package/dist/actions/open_app.d.ts +3 -0
  21. package/dist/actions/open_app.d.ts.map +1 -0
  22. package/dist/actions/open_app.js +59 -0
  23. package/dist/actions/open_app.js.map +1 -0
  24. package/dist/actions/set-mcp-app-access.d.ts +3 -0
  25. package/dist/actions/set-mcp-app-access.d.ts.map +1 -0
  26. package/dist/actions/set-mcp-app-access.js +46 -0
  27. package/dist/actions/set-mcp-app-access.js.map +1 -0
  28. package/dist/actions/start-workspace-app-creation.js +1 -1
  29. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  30. package/dist/actions/view-screen.d.ts.map +1 -1
  31. package/dist/actions/view-screen.js +8 -0
  32. package/dist/actions/view-screen.js.map +1 -1
  33. package/dist/components/create-app-popover.d.ts.map +1 -1
  34. package/dist/components/create-app-popover.js +1 -0
  35. package/dist/components/create-app-popover.js.map +1 -1
  36. package/dist/routes/pages/agents.d.ts.map +1 -1
  37. package/dist/routes/pages/agents.js +74 -3
  38. package/dist/routes/pages/agents.js.map +1 -1
  39. package/dist/routes/pages/apps.d.ts.map +1 -1
  40. package/dist/routes/pages/apps.js +23 -8
  41. package/dist/routes/pages/apps.js.map +1 -1
  42. package/dist/routes/pages/overview.d.ts.map +1 -1
  43. package/dist/routes/pages/overview.js +1 -3
  44. package/dist/routes/pages/overview.js.map +1 -1
  45. package/dist/server/lib/mcp-access-store.d.ts +16 -0
  46. package/dist/server/lib/mcp-access-store.d.ts.map +1 -0
  47. package/dist/server/lib/mcp-access-store.js +64 -0
  48. package/dist/server/lib/mcp-access-store.js.map +1 -0
  49. package/dist/server/lib/mcp-gateway.d.ts +47 -0
  50. package/dist/server/lib/mcp-gateway.d.ts.map +1 -0
  51. package/dist/server/lib/mcp-gateway.js +237 -0
  52. package/dist/server/lib/mcp-gateway.js.map +1 -0
  53. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  54. package/dist/server/plugins/agent-chat.js +1 -0
  55. package/dist/server/plugins/agent-chat.js.map +1 -1
  56. package/dist/server/plugins/integrations.d.ts.map +1 -1
  57. package/dist/server/plugins/integrations.js +2 -1
  58. package/dist/server/plugins/integrations.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/actions/ask_app.ts +13 -0
  61. package/src/actions/create_embed_session.ts +29 -0
  62. package/src/actions/index.spec.ts +6 -0
  63. package/src/actions/index.ts +12 -0
  64. package/src/actions/list-mcp-app-access.ts +26 -0
  65. package/src/actions/list_apps.ts +27 -0
  66. package/src/actions/open_app.ts +61 -0
  67. package/src/actions/set-mcp-app-access.ts +59 -0
  68. package/src/actions/start-workspace-app-creation.ts +1 -1
  69. package/src/actions/view-screen.ts +8 -0
  70. package/src/components/create-app-popover.tsx +1 -0
  71. package/src/routes/pages/agents.tsx +187 -5
  72. package/src/routes/pages/apps.tsx +209 -67
  73. package/src/routes/pages/overview.tsx +16 -10
  74. package/src/server/lib/mcp-access-store.spec.ts +58 -0
  75. package/src/server/lib/mcp-access-store.ts +104 -0
  76. package/src/server/lib/mcp-gateway.ts +333 -0
  77. package/src/server/plugins/agent-chat.ts +1 -0
  78. package/src/server/plugins/integrations.ts +2 -1
@@ -0,0 +1,104 @@
1
+ import {
2
+ getOrgSetting,
3
+ getUserSetting,
4
+ putOrgSetting,
5
+ putUserSetting,
6
+ } from "@agent-native/core/settings";
7
+ import {
8
+ getRequestOrgId,
9
+ getRequestUserEmail,
10
+ } from "@agent-native/core/server";
11
+
12
+ export const MCP_APP_ACCESS_SETTINGS_KEY = "dispatch-mcp-app-access";
13
+
14
+ export type DispatchMcpAppAccessMode = "all-apps" | "selected-apps";
15
+
16
+ export interface DispatchMcpAppAccessSettings {
17
+ mode: DispatchMcpAppAccessMode;
18
+ selectedAppIds: string[];
19
+ updatedAt?: string;
20
+ updatedBy?: string;
21
+ }
22
+
23
+ interface AccessScope {
24
+ kind: "org" | "user";
25
+ id: string;
26
+ actor: string;
27
+ }
28
+
29
+ function uniqueAppIds(values: unknown): string[] {
30
+ const input = Array.isArray(values) ? values : [];
31
+ return Array.from(
32
+ new Set(
33
+ input
34
+ .filter((value): value is string => typeof value === "string")
35
+ .map((value) => value.trim().toLowerCase())
36
+ .filter(Boolean),
37
+ ),
38
+ );
39
+ }
40
+
41
+ export function normalizeMcpAppAccessSettings(
42
+ raw: unknown,
43
+ ): DispatchMcpAppAccessSettings {
44
+ const record =
45
+ raw && typeof raw === "object" && !Array.isArray(raw)
46
+ ? (raw as Record<string, unknown>)
47
+ : {};
48
+ const mode = record.mode === "selected-apps" ? "selected-apps" : "all-apps";
49
+ return {
50
+ mode,
51
+ selectedAppIds: uniqueAppIds(record.selectedAppIds),
52
+ updatedAt:
53
+ typeof record.updatedAt === "string" ? record.updatedAt : undefined,
54
+ updatedBy:
55
+ typeof record.updatedBy === "string" ? record.updatedBy : undefined,
56
+ };
57
+ }
58
+
59
+ function currentAccessScope(): AccessScope {
60
+ const actor = getRequestUserEmail();
61
+ if (!actor) throw new Error("no authenticated user");
62
+ const orgId = getRequestOrgId();
63
+ if (orgId) return { kind: "org", id: orgId, actor };
64
+ return { kind: "user", id: actor, actor };
65
+ }
66
+
67
+ export async function getDispatchMcpAppAccessSettings(): Promise<DispatchMcpAppAccessSettings> {
68
+ const scope = currentAccessScope();
69
+ const raw =
70
+ scope.kind === "org"
71
+ ? await getOrgSetting(scope.id, MCP_APP_ACCESS_SETTINGS_KEY)
72
+ : await getUserSetting(scope.id, MCP_APP_ACCESS_SETTINGS_KEY);
73
+ return normalizeMcpAppAccessSettings(raw);
74
+ }
75
+
76
+ export async function setDispatchMcpAppAccessSettings(input: {
77
+ mode: DispatchMcpAppAccessMode;
78
+ selectedAppIds?: string[];
79
+ }): Promise<DispatchMcpAppAccessSettings> {
80
+ const scope = currentAccessScope();
81
+ const next: DispatchMcpAppAccessSettings = {
82
+ mode: input.mode,
83
+ selectedAppIds: uniqueAppIds(input.selectedAppIds),
84
+ updatedAt: new Date().toISOString(),
85
+ updatedBy: scope.actor,
86
+ };
87
+ const value = next as unknown as Record<string, unknown>;
88
+ if (scope.kind === "org") {
89
+ await putOrgSetting(scope.id, MCP_APP_ACCESS_SETTINGS_KEY, value);
90
+ } else {
91
+ await putUserSetting(scope.id, MCP_APP_ACCESS_SETTINGS_KEY, value);
92
+ }
93
+ return next;
94
+ }
95
+
96
+ export function isAppAllowedByMcpAccess(
97
+ appId: string,
98
+ settings: DispatchMcpAppAccessSettings,
99
+ ): boolean {
100
+ const normalized = appId.trim().toLowerCase();
101
+ if (!normalized) return false;
102
+ if (settings.mode === "all-apps") return true;
103
+ return settings.selectedAppIds.includes(normalized);
104
+ }
@@ -0,0 +1,333 @@
1
+ import { callAgent, signA2AToken } from "@agent-native/core/a2a";
2
+ import {
3
+ buildMcpToolName,
4
+ McpClientManager,
5
+ } from "@agent-native/core/mcp-client";
6
+ import { buildDeepLink } from "@agent-native/core/server";
7
+ import {
8
+ discoverAgents,
9
+ type DiscoveredAgent,
10
+ } from "@agent-native/core/server/agent-discovery";
11
+ import {
12
+ getRequestOrgId,
13
+ getRequestUserEmail,
14
+ } from "@agent-native/core/server";
15
+ import { getOrgA2ASecret, getOrgDomain } from "@agent-native/core/org";
16
+ import {
17
+ getDispatchMcpAppAccessSettings,
18
+ isAppAllowedByMcpAccess,
19
+ type DispatchMcpAppAccessSettings,
20
+ } from "./mcp-access-store.js";
21
+
22
+ export interface DispatchMcpAccessibleApp {
23
+ id: string;
24
+ name: string;
25
+ description: string;
26
+ url: string;
27
+ color: string;
28
+ granted: boolean;
29
+ }
30
+
31
+ function normalizeAppId(value: string): string {
32
+ return value.trim().toLowerCase();
33
+ }
34
+
35
+ const CONTROL_CHARS = new RegExp("[\\u0000-\\u001f\\u007f]");
36
+
37
+ function safeAppPath(raw: unknown): string | null {
38
+ if (typeof raw !== "string" || !raw.trim()) return null;
39
+ const value = raw.trim();
40
+ if (CONTROL_CHARS.test(value)) return null;
41
+ if (!value.startsWith("/")) return null;
42
+ if (value.startsWith("//") || value.startsWith("/\\")) return null;
43
+ if (/^\/[a-z][a-z0-9+.-]*:/i.test(value)) return null;
44
+ return value;
45
+ }
46
+
47
+ function appendParamsToPath(
48
+ path: string,
49
+ params: Record<string, string | number | boolean> | undefined,
50
+ ): string {
51
+ if (!params || Object.keys(params).length === 0) return path;
52
+ const url = new URL(path, "http://agent-native.invalid");
53
+ for (const [key, value] of Object.entries(params)) {
54
+ url.searchParams.set(key, String(value));
55
+ }
56
+ return `${url.pathname}${url.search}${url.hash}`;
57
+ }
58
+
59
+ function appOrigin(app: DispatchMcpAccessibleApp): string {
60
+ return new URL(app.url).origin;
61
+ }
62
+
63
+ function appBaseUrl(app: DispatchMcpAccessibleApp): string {
64
+ return app.url.replace(/\/+$/, "");
65
+ }
66
+
67
+ function toAccessibleApp(
68
+ agent: DiscoveredAgent,
69
+ settings: DispatchMcpAppAccessSettings,
70
+ ): DispatchMcpAccessibleApp {
71
+ return {
72
+ id: agent.id,
73
+ name: agent.name,
74
+ description: agent.description,
75
+ url: agent.url,
76
+ color: agent.color,
77
+ granted: isAppAllowedByMcpAccess(agent.id, settings),
78
+ };
79
+ }
80
+
81
+ export async function listDispatchMcpApps(): Promise<{
82
+ settings: DispatchMcpAppAccessSettings;
83
+ apps: DispatchMcpAccessibleApp[];
84
+ }> {
85
+ const [settings, agents] = await Promise.all([
86
+ getDispatchMcpAppAccessSettings(),
87
+ discoverAgents("dispatch"),
88
+ ]);
89
+ return {
90
+ settings,
91
+ apps: agents.map((agent) => toAccessibleApp(agent, settings)),
92
+ };
93
+ }
94
+
95
+ export async function listGrantedDispatchMcpApps(): Promise<
96
+ DispatchMcpAccessibleApp[]
97
+ > {
98
+ const { apps } = await listDispatchMcpApps();
99
+ return apps.filter((app) => app.granted);
100
+ }
101
+
102
+ export async function resolveGrantedDispatchMcpApp(
103
+ app: string,
104
+ ): Promise<DispatchMcpAccessibleApp> {
105
+ const target = normalizeAppId(app);
106
+ if (!target) throw new Error("app is required");
107
+ const { apps } = await listDispatchMcpApps();
108
+ const match = apps.find(
109
+ (candidate) =>
110
+ candidate.id === target || candidate.name.toLowerCase() === target,
111
+ );
112
+ if (!match) {
113
+ throw new Error(
114
+ `Unknown app "${app}". Call list_apps to see apps available through Dispatch MCP.`,
115
+ );
116
+ }
117
+ if (!match.granted) {
118
+ throw new Error(
119
+ `Dispatch MCP access to "${match.id}" is not granted. Open Dispatch > Agents to change MCP app access.`,
120
+ );
121
+ }
122
+ return match;
123
+ }
124
+
125
+ export async function askGrantedDispatchMcpApp(
126
+ app: string,
127
+ message: string,
128
+ ): Promise<{ app: string; routedVia: "a2a"; response: string }> {
129
+ const trimmedMessage = message.trim();
130
+ if (!trimmedMessage) throw new Error("message is required");
131
+ const target = await resolveGrantedDispatchMcpApp(app);
132
+ const userEmail = getRequestUserEmail();
133
+ if (!userEmail) throw new Error("no authenticated user");
134
+
135
+ const orgId = getRequestOrgId();
136
+ const [orgDomain, orgSecret] = orgId
137
+ ? await Promise.all([
138
+ getOrgDomain(orgId).catch(() => null),
139
+ getOrgA2ASecret(orgId).catch(() => null),
140
+ ])
141
+ : [null, null];
142
+
143
+ const response = await callAgent(target.url, trimmedMessage, {
144
+ userEmail,
145
+ orgDomain: orgDomain ?? undefined,
146
+ orgSecret: orgSecret ?? undefined,
147
+ timeoutMs: 5 * 60_000,
148
+ });
149
+ return { app: target.id, routedVia: "a2a", response };
150
+ }
151
+
152
+ export async function openGrantedDispatchMcpApp(input: {
153
+ app: string;
154
+ view?: string;
155
+ path?: string;
156
+ params?: Record<string, string | number | boolean>;
157
+ embed?: boolean;
158
+ chrome?: "full" | "minimal";
159
+ }): Promise<{
160
+ app: string;
161
+ view?: string;
162
+ path?: string;
163
+ url: string;
164
+ embed?: boolean;
165
+ chrome?: "full" | "minimal";
166
+ }> {
167
+ const view = input.view?.trim() ?? "";
168
+ const path = safeAppPath(input.path);
169
+ if (!view && !path) throw new Error("open_app requires view or path");
170
+ const target = await resolveGrantedDispatchMcpApp(input.app);
171
+ const relUrl = path
172
+ ? appendParamsToPath(path, input.params)
173
+ : buildDeepLink({
174
+ app: target.id,
175
+ view,
176
+ params: input.params,
177
+ });
178
+ return {
179
+ app: target.id,
180
+ ...(view ? { view } : {}),
181
+ ...(path ? { path } : {}),
182
+ url: `${appBaseUrl(target)}${relUrl}`,
183
+ ...(input.embed === true ? { embed: true } : {}),
184
+ ...(input.chrome ? { chrome: input.chrome } : {}),
185
+ };
186
+ }
187
+
188
+ function parseMcpToolTextResult(result: unknown): Record<string, unknown> {
189
+ if (result && typeof result === "object") {
190
+ const structured = (result as any).structuredContent;
191
+ if (structured && typeof structured === "object") return structured;
192
+ const parts = Array.isArray((result as any).content)
193
+ ? ((result as any).content as Array<Record<string, unknown>>)
194
+ : [];
195
+ const text = parts.find(
196
+ (part) => part?.type === "text" && typeof part.text === "string",
197
+ )?.text;
198
+ if (typeof text === "string" && text.trim()) {
199
+ const parsed = JSON.parse(text);
200
+ if (parsed && typeof parsed === "object") return parsed;
201
+ }
202
+ }
203
+ throw new Error("Target app did not return an embed session.");
204
+ }
205
+
206
+ async function resolveDispatchEmbedTarget(input: {
207
+ app?: string;
208
+ url?: string;
209
+ path?: string;
210
+ }): Promise<{ app: DispatchMcpAccessibleApp; path: string; url: string }> {
211
+ const explicitApp = input.app?.trim()
212
+ ? await resolveGrantedDispatchMcpApp(input.app)
213
+ : null;
214
+ if (explicitApp && input.path) {
215
+ const path = safeAppPath(input.path);
216
+ if (!path) throw new Error("path must be a safe app-relative route");
217
+ return {
218
+ app: explicitApp,
219
+ path,
220
+ url: `${appBaseUrl(explicitApp)}${path}`,
221
+ };
222
+ }
223
+
224
+ if (!input.url) {
225
+ throw new Error("create_embed_session requires a url or app + path.");
226
+ }
227
+
228
+ let parsed: URL;
229
+ try {
230
+ parsed = new URL(input.url);
231
+ } catch {
232
+ if (!explicitApp) {
233
+ throw new Error("Relative embed paths require an app id.");
234
+ }
235
+ const path = safeAppPath(input.url);
236
+ if (!path) throw new Error("url must be a safe app route.");
237
+ return {
238
+ app: explicitApp,
239
+ path,
240
+ url: `${appBaseUrl(explicitApp)}${path}`,
241
+ };
242
+ }
243
+
244
+ const apps = explicitApp ? [explicitApp] : await listGrantedDispatchMcpApps();
245
+ const target = apps.find((app) => parsed.origin === appOrigin(app));
246
+ if (!target) {
247
+ throw new Error(
248
+ "Embed URL must belong to an app granted through Dispatch.",
249
+ );
250
+ }
251
+ const path = safeAppPath(`${parsed.pathname}${parsed.search}${parsed.hash}`);
252
+ if (!path) throw new Error("Embed URL path is not safe.");
253
+ return { app: target, path, url: `${appBaseUrl(target)}${path}` };
254
+ }
255
+
256
+ export async function createGrantedDispatchMcpEmbedSession(input: {
257
+ app?: string;
258
+ url?: string;
259
+ path?: string;
260
+ chrome?: "full" | "minimal";
261
+ }): Promise<{
262
+ startUrl: string;
263
+ targetPath?: string;
264
+ expiresAt?: number;
265
+ app: string;
266
+ }> {
267
+ const userEmail = getRequestUserEmail();
268
+ if (!userEmail) throw new Error("no authenticated user");
269
+ const target = await resolveDispatchEmbedTarget(input);
270
+
271
+ const orgId = getRequestOrgId();
272
+ const [orgDomain, orgSecret] = orgId
273
+ ? await Promise.all([
274
+ getOrgDomain(orgId).catch(() => null),
275
+ getOrgA2ASecret(orgId).catch(() => null),
276
+ ])
277
+ : [null, null];
278
+ const token = await signA2AToken(
279
+ userEmail,
280
+ orgDomain ?? undefined,
281
+ orgSecret ?? undefined,
282
+ {
283
+ expiresIn: "5m",
284
+ preferGlobalSecret: !orgSecret,
285
+ },
286
+ );
287
+
288
+ const serverId = "target";
289
+ const manager = new McpClientManager({
290
+ servers: {
291
+ [serverId]: {
292
+ type: "http",
293
+ url: `${appBaseUrl(target.app)}/_agent-native/mcp`,
294
+ headers: {
295
+ Authorization: `Bearer ${token}`,
296
+ },
297
+ },
298
+ },
299
+ });
300
+ await manager.start();
301
+ try {
302
+ const result = await manager.callTool(
303
+ buildMcpToolName(serverId, "create_embed_session"),
304
+ {
305
+ url: target.url,
306
+ chrome: input.chrome ?? "full",
307
+ },
308
+ );
309
+ const parsed = parseMcpToolTextResult(result) as {
310
+ startUrl?: string;
311
+ targetPath?: string;
312
+ expiresAt?: number;
313
+ };
314
+ if (!parsed.startUrl) {
315
+ throw new Error("Target app did not return an embed start URL.");
316
+ }
317
+ const output: {
318
+ startUrl: string;
319
+ targetPath?: string;
320
+ expiresAt?: number;
321
+ app: string;
322
+ } = {
323
+ startUrl: parsed.startUrl,
324
+ app: target.app.id,
325
+ };
326
+ if (parsed.targetPath) output.targetPath = parsed.targetPath;
327
+ if (typeof parsed.expiresAt === "number")
328
+ output.expiresAt = parsed.expiresAt;
329
+ return output;
330
+ } finally {
331
+ await manager.stop();
332
+ }
333
+ }
@@ -30,6 +30,7 @@ Use the standard workspace primitives:
30
30
  - You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.
31
31
  - When answering whether workspace apps expose agent cards or A2A endpoints, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.
32
32
  - When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.
33
+ - If the starter 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 "Starter", "Blank app", or "New app" UI behind.
33
34
  - Treat first-party apps such as Mail, Calendar, Analytics, Brain, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
34
35
 
35
36
  When a user asks for something like a digest, reminder, routing rule, or saved behavior:
@@ -23,7 +23,8 @@ When a user asks for something:
23
23
  - Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
24
24
  - 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.
25
25
  - If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, 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.
26
- - 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 when possible. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/starter 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" or "builder-unavailable", explain the missing Builder setup and ask them to connect/configure Builder.
26
+ - If the starter 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 "Starter", "Blank app", or "New app" UI behind.
27
+ - 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/starter 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" or "builder-unavailable", explain the missing Builder setup and ask them to connect/configure Builder.
27
28
  - For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.
28
29
  - Keep responses concise and operational — messaging platforms have character limits.
29
30
  - Use markdown sparingly (bold and lists are fine, avoid complex formatting).