@agent-native/dispatch 0.6.1 → 0.7.0
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/README.md +1 -1
- package/dist/actions/create-pylon-ticket.d.ts +3 -0
- package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
- package/dist/actions/create-pylon-ticket.js +94 -0
- package/dist/actions/create-pylon-ticket.js.map +1 -0
- package/dist/actions/create-vault-grant.js +1 -1
- package/dist/actions/create-vault-grant.js.map +1 -1
- package/dist/actions/create-vault-secret.d.ts.map +1 -1
- package/dist/actions/create-vault-secret.js +4 -3
- package/dist/actions/create-vault-secret.js.map +1 -1
- package/dist/actions/get-vault-access-settings.d.ts +3 -0
- package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
- package/dist/actions/get-vault-access-settings.js +10 -0
- package/dist/actions/get-vault-access-settings.js.map +1 -0
- package/dist/actions/grant-vault-secrets-to-app.js +1 -1
- package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-integrations-catalog.js +1 -1
- package/dist/actions/list-integrations-catalog.js.map +1 -1
- package/dist/actions/list-vault-grants.js +1 -1
- package/dist/actions/list-vault-grants.js.map +1 -1
- package/dist/actions/list-workspace-apps.d.ts.map +1 -1
- package/dist/actions/list-workspace-apps.js +5 -1
- package/dist/actions/list-workspace-apps.js.map +1 -1
- package/dist/actions/set-vault-access-settings.d.ts +3 -0
- package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
- package/dist/actions/set-vault-access-settings.js +13 -0
- package/dist/actions/set-vault-access-settings.js.map +1 -0
- package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
- package/dist/actions/start-workspace-app-creation.js +6 -0
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/actions/sync-vault-to-app.js +1 -1
- package/dist/actions/sync-vault-to-app.js.map +1 -1
- package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
- package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
- package/dist/actions/update-workspace-app-metadata.js +30 -0
- package/dist/actions/update-workspace-app-metadata.js.map +1 -0
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +4 -2
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/app-keys-popover.js +16 -5
- package/dist/components/app-keys-popover.js.map +1 -1
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +38 -14
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-shell.d.ts +4 -4
- package/dist/components/dispatch-shell.d.ts.map +1 -1
- package/dist/components/dispatch-shell.js +6 -6
- package/dist/components/dispatch-shell.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +10 -3
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/messaging-setup-panel.d.ts.map +1 -1
- package/dist/components/messaging-setup-panel.js +2 -2
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +41 -2
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/hooks/use-navigation-state.js +12 -5
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/catch-all-target.d.ts +2 -0
- package/dist/lib/catch-all-target.d.ts.map +1 -0
- package/dist/lib/catch-all-target.js +95 -0
- package/dist/lib/catch-all-target.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +9 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts +2 -2
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +17 -8
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +20 -15
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/new-app.js +1 -1
- package/dist/routes/pages/new-app.js.map +1 -1
- package/dist/routes/pages/overview.d.ts.map +1 -1
- package/dist/routes/pages/overview.js +5 -1
- package/dist/routes/pages/overview.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +23 -5
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +13 -0
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +295 -9
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/env-config.d.ts.map +1 -1
- package/dist/server/lib/env-config.js +5 -0
- package/dist/server/lib/env-config.js.map +1 -1
- package/dist/server/lib/onboarding-steps.d.ts +12 -0
- package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
- package/dist/server/lib/onboarding-steps.js +47 -0
- package/dist/server/lib/onboarding-steps.js.map +1 -0
- package/dist/server/lib/vault-store.d.ts +55 -0
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +210 -41
- package/dist/server/lib/vault-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.d.ts.map +1 -1
- package/dist/server/plugins/agent-chat.js +2 -1
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/core-routes.d.ts.map +1 -1
- package/dist/server/plugins/core-routes.js +4 -0
- package/dist/server/plugins/core-routes.js.map +1 -1
- package/dist/server/plugins/integrations.js +2 -2
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +13 -11
- package/src/actions/create-pylon-ticket.ts +109 -0
- package/src/actions/create-vault-grant.ts +1 -1
- package/src/actions/create-vault-secret.ts +4 -3
- package/src/actions/get-vault-access-settings.ts +11 -0
- package/src/actions/grant-vault-secrets-to-app.ts +1 -1
- package/src/actions/index.ts +8 -0
- package/src/actions/list-integrations-catalog.ts +1 -1
- package/src/actions/list-vault-grants.ts +1 -1
- package/src/actions/list-workspace-apps.ts +5 -1
- package/src/actions/set-vault-access-settings.ts +16 -0
- package/src/actions/start-workspace-app-creation.ts +8 -0
- package/src/actions/sync-vault-to-app.ts +1 -1
- package/src/actions/update-workspace-app-metadata.ts +32 -0
- package/src/actions/view-screen.ts +4 -1
- package/src/components/app-keys-popover.tsx +23 -7
- package/src/components/create-app-popover.tsx +47 -14
- package/src/components/dispatch-shell.tsx +16 -15
- package/src/components/layout/Layout.tsx +11 -5
- package/src/components/messaging-setup-panel.tsx +54 -39
- package/src/components/workspace-app-card.tsx +102 -0
- package/src/hooks/use-navigation-state.ts +10 -4
- package/src/lib/catch-all-target.spec.ts +218 -0
- package/src/lib/catch-all-target.ts +99 -0
- package/src/lib/workspace-apps.ts +9 -0
- package/src/routes/pages/$appId.tsx +21 -8
- package/src/routes/pages/integrations.tsx +57 -18
- package/src/routes/pages/new-app.tsx +1 -1
- package/src/routes/pages/overview.tsx +11 -3
- package/src/routes/pages/vault.tsx +76 -9
- package/src/server/lib/app-creation-store.spec.ts +61 -2
- package/src/server/lib/app-creation-store.ts +386 -11
- package/src/server/lib/env-config.ts +5 -0
- package/src/server/lib/onboarding-steps.ts +49 -0
- package/src/server/lib/vault-store.spec.ts +69 -0
- package/src/server/lib/vault-store.ts +266 -49
- package/src/server/plugins/agent-chat.ts +2 -1
- package/src/server/plugins/core-routes.ts +5 -0
- package/src/server/plugins/integrations.ts +2 -2
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { getSetting, putSetting } from "@agent-native/core/settings";
|
|
6
|
+
import { assertValidWorkspaceAppId } from "@agent-native/core/shared";
|
|
6
7
|
import {
|
|
7
8
|
getBuilderBranchProjectId,
|
|
8
9
|
getRequestContext,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
runBuilderAgent,
|
|
13
14
|
} from "@agent-native/core/server";
|
|
14
15
|
import { getDbExec } from "@agent-native/core/db";
|
|
15
|
-
import { assertValidWorkspaceAppId } from "@agent-native/core/shared";
|
|
16
16
|
import {
|
|
17
17
|
currentOrgId,
|
|
18
18
|
currentOwnerEmail,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "./workspace-resources-store.js";
|
|
29
29
|
|
|
30
30
|
const SETTINGS_KEY = "dispatch-app-creation-settings";
|
|
31
|
+
const WORKSPACE_APP_METADATA_SETTINGS_KEY = "workspace-app-metadata";
|
|
31
32
|
const WORKSPACE_APPS_ENV_KEY = "AGENT_NATIVE_WORKSPACE_APPS_JSON";
|
|
32
33
|
const WORKSPACE_APPS_MANIFEST_FILE = "workspace-apps.json";
|
|
33
34
|
const WORKSPACE_APPS_GATEWAY_PATH = "/_workspace/apps";
|
|
@@ -35,6 +36,9 @@ const WORKSPACE_APPS_GATEWAY_TIMEOUT_MS = 1_000;
|
|
|
35
36
|
const MAX_PENDING_APPS = 50;
|
|
36
37
|
const AGENT_CARD_PATH = "/.well-known/agent-card.json";
|
|
37
38
|
const AGENT_CARD_FETCH_TIMEOUT_MS = 1_500;
|
|
39
|
+
const DEFAULT_WORKSPACE_APP_AUDIENCE = "internal";
|
|
40
|
+
|
|
41
|
+
type WorkspaceAppAudience = "internal" | "public";
|
|
38
42
|
|
|
39
43
|
export interface WorkspaceAppSummary {
|
|
40
44
|
id: string;
|
|
@@ -43,6 +47,9 @@ export interface WorkspaceAppSummary {
|
|
|
43
47
|
path: string;
|
|
44
48
|
url: string | null;
|
|
45
49
|
isDispatch: boolean;
|
|
50
|
+
audience: WorkspaceAppAudience;
|
|
51
|
+
publicPaths: string[];
|
|
52
|
+
protectedPaths: string[];
|
|
46
53
|
status?: "ready" | "pending";
|
|
47
54
|
statusLabel?: string;
|
|
48
55
|
builderUrl?: string | null;
|
|
@@ -64,6 +71,7 @@ export interface ListWorkspaceAppsOptions {
|
|
|
64
71
|
* when rendering the "Hidden apps" expander.
|
|
65
72
|
*/
|
|
66
73
|
includeArchived?: boolean;
|
|
74
|
+
audience?: WorkspaceAppAudience | "all";
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
export interface AvailableWorkspaceTemplate {
|
|
@@ -103,10 +111,24 @@ interface PendingWorkspaceApp {
|
|
|
103
111
|
builderUrl: string | null;
|
|
104
112
|
branchName: string | null;
|
|
105
113
|
projectId: string | null;
|
|
114
|
+
audience?: WorkspaceAppAudience;
|
|
106
115
|
createdAt: string;
|
|
107
116
|
updatedAt: string;
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
interface WorkspaceAppMetadataOverride {
|
|
120
|
+
name?: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
generated?: boolean;
|
|
123
|
+
sourcePrompt?: string;
|
|
124
|
+
updatedAt?: string;
|
|
125
|
+
updatedBy?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface WorkspaceAppMetadataSettings {
|
|
129
|
+
apps: Record<string, WorkspaceAppMetadataOverride>;
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
function readJson(file: string): any {
|
|
111
133
|
try {
|
|
112
134
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -137,12 +159,56 @@ function titleCase(value: string): string {
|
|
|
137
159
|
.join(" ");
|
|
138
160
|
}
|
|
139
161
|
|
|
162
|
+
function normalizeWhitespace(value: string): string {
|
|
163
|
+
return value.replace(/\s+/g, " ").trim();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ensureSentence(value: string): string {
|
|
167
|
+
if (!value) return value;
|
|
168
|
+
const capitalized = value.charAt(0).toUpperCase() + value.slice(1);
|
|
169
|
+
return /[.!?]$/.test(capitalized) ? capitalized : `${capitalized}.`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function clipSentence(value: string, max = 180): string {
|
|
173
|
+
if (value.length <= max) return value;
|
|
174
|
+
const clipped = value
|
|
175
|
+
.slice(0, max - 1)
|
|
176
|
+
.replace(/\s+\S*$/, "")
|
|
177
|
+
.trim();
|
|
178
|
+
return `${clipped || value.slice(0, max - 1).trim()}…`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function generateWorkspaceAppDescription(
|
|
182
|
+
prompt: string,
|
|
183
|
+
appId: string,
|
|
184
|
+
): string {
|
|
185
|
+
const cleaned = normalizeWhitespace(prompt)
|
|
186
|
+
.replace(
|
|
187
|
+
/^(please\s+)?(build|create|make|generate|scaffold)\s+(me\s+|us\s+)?/i,
|
|
188
|
+
"",
|
|
189
|
+
)
|
|
190
|
+
.replace(
|
|
191
|
+
/^(an?\s+)?(workspace\s+)?(agent-native\s+)?(app|tool)\s+(that|to|for)\s+/i,
|
|
192
|
+
"",
|
|
193
|
+
)
|
|
194
|
+
.replace(/^(an?\s+)?(dashboard|workspace|agent)\s+(that|to|for)\s+/i, "");
|
|
195
|
+
|
|
196
|
+
if (!cleaned) return `Workspace app for ${titleCase(appId)}.`;
|
|
197
|
+
return clipSentence(ensureSentence(cleaned));
|
|
198
|
+
}
|
|
199
|
+
|
|
140
200
|
function scopedSettingsKey(): string {
|
|
141
201
|
const orgId = currentOrgId();
|
|
142
202
|
if (orgId) return `${SETTINGS_KEY}:org:${orgId}`;
|
|
143
203
|
return `${SETTINGS_KEY}:user:${currentOwnerEmail()}`;
|
|
144
204
|
}
|
|
145
205
|
|
|
206
|
+
function workspaceAppMetadataSettingsKey(): string {
|
|
207
|
+
const orgId = currentOrgId();
|
|
208
|
+
if (orgId) return `${WORKSPACE_APP_METADATA_SETTINGS_KEY}:org:${orgId}`;
|
|
209
|
+
return `${WORKSPACE_APP_METADATA_SETTINGS_KEY}:user:${currentOwnerEmail()}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
146
212
|
async function readSettingsRecord(): Promise<Record<string, any>> {
|
|
147
213
|
const raw = await getSetting(scopedSettingsKey()).catch(() => null);
|
|
148
214
|
return raw && typeof raw === "object" && !Array.isArray(raw)
|
|
@@ -150,6 +216,114 @@ async function readSettingsRecord(): Promise<Record<string, any>> {
|
|
|
150
216
|
: {};
|
|
151
217
|
}
|
|
152
218
|
|
|
219
|
+
function cleanOptionalText(value: unknown): string | undefined {
|
|
220
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseWorkspaceAppMetadataSettings(
|
|
224
|
+
raw: unknown,
|
|
225
|
+
): WorkspaceAppMetadataSettings {
|
|
226
|
+
const record =
|
|
227
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
228
|
+
? (raw as Record<string, unknown>)
|
|
229
|
+
: {};
|
|
230
|
+
const rawApps =
|
|
231
|
+
record.apps &&
|
|
232
|
+
typeof record.apps === "object" &&
|
|
233
|
+
!Array.isArray(record.apps)
|
|
234
|
+
? (record.apps as Record<string, unknown>)
|
|
235
|
+
: {};
|
|
236
|
+
const apps: Record<string, WorkspaceAppMetadataOverride> = {};
|
|
237
|
+
|
|
238
|
+
for (const [id, value] of Object.entries(rawApps)) {
|
|
239
|
+
if (!id.trim() || !value || typeof value !== "object") continue;
|
|
240
|
+
const item = value as Record<string, unknown>;
|
|
241
|
+
const override: WorkspaceAppMetadataOverride = {};
|
|
242
|
+
const name = cleanOptionalText(item.name);
|
|
243
|
+
const description = cleanOptionalText(item.description);
|
|
244
|
+
const sourcePrompt = cleanOptionalText(item.sourcePrompt);
|
|
245
|
+
const updatedAt = cleanOptionalText(item.updatedAt);
|
|
246
|
+
const updatedBy = cleanOptionalText(item.updatedBy);
|
|
247
|
+
|
|
248
|
+
if (name) override.name = name;
|
|
249
|
+
if (description) override.description = description;
|
|
250
|
+
if (item.generated === true) override.generated = true;
|
|
251
|
+
if (sourcePrompt) override.sourcePrompt = sourcePrompt;
|
|
252
|
+
if (updatedAt) override.updatedAt = updatedAt;
|
|
253
|
+
if (updatedBy) override.updatedBy = updatedBy;
|
|
254
|
+
|
|
255
|
+
if (Object.keys(override).length > 0) apps[id.trim()] = override;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { apps };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function readWorkspaceAppMetadataSettings(): Promise<WorkspaceAppMetadataSettings> {
|
|
262
|
+
const raw = await getSetting(workspaceAppMetadataSettingsKey()).catch(
|
|
263
|
+
() => null,
|
|
264
|
+
);
|
|
265
|
+
return parseWorkspaceAppMetadataSettings(raw);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function writeWorkspaceAppMetadataOverride(input: {
|
|
269
|
+
appId: string;
|
|
270
|
+
name?: string | null;
|
|
271
|
+
description?: string | null;
|
|
272
|
+
generated?: boolean;
|
|
273
|
+
sourcePrompt?: string | null;
|
|
274
|
+
updatedBy?: string | null;
|
|
275
|
+
}): Promise<WorkspaceAppMetadataSettings> {
|
|
276
|
+
const key = workspaceAppMetadataSettingsKey();
|
|
277
|
+
const current = parseWorkspaceAppMetadataSettings(
|
|
278
|
+
await getSetting(key).catch(() => null),
|
|
279
|
+
);
|
|
280
|
+
const appId = input.appId.trim();
|
|
281
|
+
const existing = current.apps[appId] ?? {};
|
|
282
|
+
const next: WorkspaceAppMetadataOverride = {
|
|
283
|
+
...existing,
|
|
284
|
+
updatedAt: new Date().toISOString(),
|
|
285
|
+
};
|
|
286
|
+
const name = cleanOptionalText(input.name);
|
|
287
|
+
const description = cleanOptionalText(input.description);
|
|
288
|
+
const sourcePrompt = cleanOptionalText(input.sourcePrompt);
|
|
289
|
+
const updatedBy = cleanOptionalText(input.updatedBy);
|
|
290
|
+
|
|
291
|
+
if (name) next.name = name;
|
|
292
|
+
else delete next.name;
|
|
293
|
+
if (description) next.description = description;
|
|
294
|
+
else delete next.description;
|
|
295
|
+
if (input.generated === true) next.generated = true;
|
|
296
|
+
else if (input.generated === false) delete next.generated;
|
|
297
|
+
if (sourcePrompt) next.sourcePrompt = sourcePrompt;
|
|
298
|
+
if (updatedBy) next.updatedBy = updatedBy;
|
|
299
|
+
|
|
300
|
+
current.apps[appId] = next;
|
|
301
|
+
await putSetting(key, { apps: current.apps });
|
|
302
|
+
return current;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function applyWorkspaceAppMetadataOverride(
|
|
306
|
+
app: WorkspaceAppSummary,
|
|
307
|
+
settings: WorkspaceAppMetadataSettings,
|
|
308
|
+
): WorkspaceAppSummary {
|
|
309
|
+
const override = settings.apps[app.id];
|
|
310
|
+
if (!override) return app;
|
|
311
|
+
|
|
312
|
+
const name = cleanOptionalText(override.name);
|
|
313
|
+
const description = cleanOptionalText(override.description);
|
|
314
|
+
const generated = override.generated === true;
|
|
315
|
+
const shouldApplyName = !!name && !generated;
|
|
316
|
+
const shouldApplyDescription =
|
|
317
|
+
!!description && (!generated || !cleanOptionalText(app.description));
|
|
318
|
+
if (!shouldApplyName && !shouldApplyDescription) return app;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
...app,
|
|
322
|
+
...(shouldApplyName ? { name } : {}),
|
|
323
|
+
...(shouldApplyDescription ? { description } : {}),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
153
327
|
function workspaceAppUrl(appPath: string): string | null {
|
|
154
328
|
const base =
|
|
155
329
|
process.env.WORKSPACE_GATEWAY_URL ||
|
|
@@ -180,6 +354,85 @@ function workspaceAppLink(
|
|
|
180
354
|
}
|
|
181
355
|
}
|
|
182
356
|
|
|
357
|
+
function normalizeWorkspaceAppAudience(value: unknown): WorkspaceAppAudience {
|
|
358
|
+
return value === "public" ? "public" : DEFAULT_WORKSPACE_APP_AUDIENCE;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function normalizeWorkspaceAppPathList(value: unknown): string[] {
|
|
362
|
+
let rawPaths: unknown[] = [];
|
|
363
|
+
if (Array.isArray(value)) {
|
|
364
|
+
rawPaths = value;
|
|
365
|
+
} else if (typeof value === "string") {
|
|
366
|
+
const trimmed = value.trim();
|
|
367
|
+
if (!trimmed) return [];
|
|
368
|
+
try {
|
|
369
|
+
const parsed = JSON.parse(trimmed);
|
|
370
|
+
rawPaths = Array.isArray(parsed) ? parsed : [trimmed];
|
|
371
|
+
} catch {
|
|
372
|
+
rawPaths = trimmed.split(",");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const paths = rawPaths
|
|
377
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
378
|
+
.filter((entry) => entry.startsWith("/"))
|
|
379
|
+
.map((entry) =>
|
|
380
|
+
entry.length > 1 && entry.endsWith("/") ? entry.slice(0, -1) : entry,
|
|
381
|
+
);
|
|
382
|
+
return Array.from(new Set(paths));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function workspaceAppAudienceFromPackageJson(
|
|
386
|
+
pkg: unknown,
|
|
387
|
+
): WorkspaceAppAudience | undefined {
|
|
388
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return undefined;
|
|
389
|
+
const record = pkg as Record<string, any>;
|
|
390
|
+
const config = record["agent-native"] ?? record.agentNative;
|
|
391
|
+
const nested =
|
|
392
|
+
config && typeof config === "object" && !Array.isArray(config)
|
|
393
|
+
? (config as Record<string, any>)
|
|
394
|
+
: {};
|
|
395
|
+
const raw =
|
|
396
|
+
nested.workspaceApp?.audience ??
|
|
397
|
+
nested.workspace?.audience ??
|
|
398
|
+
nested.audience ??
|
|
399
|
+
record.workspaceAppAudience;
|
|
400
|
+
if (raw === undefined) return undefined;
|
|
401
|
+
return normalizeWorkspaceAppAudience(raw);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function workspaceAppRouteAccessFromPackageJson(pkg: unknown): {
|
|
405
|
+
publicPaths: string[];
|
|
406
|
+
protectedPaths: string[];
|
|
407
|
+
} {
|
|
408
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
|
|
409
|
+
return { publicPaths: [], protectedPaths: [] };
|
|
410
|
+
}
|
|
411
|
+
const record = pkg as Record<string, any>;
|
|
412
|
+
const config = record["agent-native"] ?? record.agentNative;
|
|
413
|
+
const nested =
|
|
414
|
+
config && typeof config === "object" && !Array.isArray(config)
|
|
415
|
+
? (config as Record<string, any>)
|
|
416
|
+
: {};
|
|
417
|
+
return {
|
|
418
|
+
publicPaths: normalizeWorkspaceAppPathList(
|
|
419
|
+
nested.workspaceApp?.publicPaths ??
|
|
420
|
+
nested.workspaceApp?.publicPagePaths ??
|
|
421
|
+
nested.workspace?.publicPaths ??
|
|
422
|
+
nested.publicPaths ??
|
|
423
|
+
record.workspaceAppPublicPaths,
|
|
424
|
+
),
|
|
425
|
+
protectedPaths: normalizeWorkspaceAppPathList(
|
|
426
|
+
nested.workspaceApp?.protectedPaths ??
|
|
427
|
+
nested.workspaceApp?.privatePaths ??
|
|
428
|
+
nested.workspaceApp?.authRequiredPaths ??
|
|
429
|
+
nested.workspace?.protectedPaths ??
|
|
430
|
+
nested.protectedPaths ??
|
|
431
|
+
record.workspaceAppProtectedPaths,
|
|
432
|
+
),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
183
436
|
function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
|
|
184
437
|
const rawApps = Array.isArray(parsed?.apps)
|
|
185
438
|
? parsed.apps
|
|
@@ -208,6 +461,12 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
|
|
|
208
461
|
typeof entry.isDispatch === "boolean"
|
|
209
462
|
? entry.isDispatch
|
|
210
463
|
: id === "dispatch",
|
|
464
|
+
audience:
|
|
465
|
+
entry.audience === undefined
|
|
466
|
+
? DEFAULT_WORKSPACE_APP_AUDIENCE
|
|
467
|
+
: normalizeWorkspaceAppAudience(entry.audience),
|
|
468
|
+
publicPaths: normalizeWorkspaceAppPathList(entry.publicPaths),
|
|
469
|
+
protectedPaths: normalizeWorkspaceAppPathList(entry.protectedPaths),
|
|
211
470
|
status: "ready",
|
|
212
471
|
} satisfies WorkspaceAppSummary;
|
|
213
472
|
})
|
|
@@ -228,7 +487,7 @@ function sortWorkspaceApps(a: WorkspaceAppSummary, b: WorkspaceAppSummary) {
|
|
|
228
487
|
function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
|
|
229
488
|
if (!Array.isArray(value)) return [];
|
|
230
489
|
return value
|
|
231
|
-
.map((entry) => {
|
|
490
|
+
.map((entry): PendingWorkspaceApp | null => {
|
|
232
491
|
if (!entry || typeof entry !== "object") return null;
|
|
233
492
|
const record = entry as Record<string, unknown>;
|
|
234
493
|
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
@@ -259,6 +518,9 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
|
|
|
259
518
|
typeof record.projectId === "string" && record.projectId.trim()
|
|
260
519
|
? record.projectId.trim()
|
|
261
520
|
: null,
|
|
521
|
+
...(record.audience === undefined
|
|
522
|
+
? {}
|
|
523
|
+
: { audience: normalizeWorkspaceAppAudience(record.audience) }),
|
|
262
524
|
createdAt:
|
|
263
525
|
typeof record.createdAt === "string" && record.createdAt.trim()
|
|
264
526
|
? record.createdAt.trim()
|
|
@@ -356,6 +618,9 @@ function pendingAppToSummary(app: PendingWorkspaceApp): WorkspaceAppSummary {
|
|
|
356
618
|
path: app.path,
|
|
357
619
|
url: app.builderUrl,
|
|
358
620
|
isDispatch: false,
|
|
621
|
+
audience: app.audience ?? DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
622
|
+
publicPaths: [],
|
|
623
|
+
protectedPaths: [],
|
|
359
624
|
status: "pending",
|
|
360
625
|
statusLabel: "Building in Builder",
|
|
361
626
|
builderUrl: app.builderUrl,
|
|
@@ -494,6 +759,8 @@ async function maybeIncludeAgentCards(
|
|
|
494
759
|
async function recordPendingWorkspaceApp(input: {
|
|
495
760
|
appId: string;
|
|
496
761
|
projectId: string | null;
|
|
762
|
+
description: string;
|
|
763
|
+
sourcePrompt: string;
|
|
497
764
|
branchName?: string | null;
|
|
498
765
|
builderUrl?: string | null;
|
|
499
766
|
}) {
|
|
@@ -505,6 +772,7 @@ async function recordPendingWorkspaceApp(input: {
|
|
|
505
772
|
id: input.appId,
|
|
506
773
|
name: titleCase(input.appId),
|
|
507
774
|
description:
|
|
775
|
+
input.description ||
|
|
508
776
|
"Builder is creating this app. The workspace path becomes live after the branch is merged and deployed.",
|
|
509
777
|
path: `/${input.appId}`,
|
|
510
778
|
builderUrl: input.builderUrl?.trim() || null,
|
|
@@ -522,6 +790,14 @@ async function recordPendingWorkspaceApp(input: {
|
|
|
522
790
|
].slice(0, MAX_PENDING_APPS),
|
|
523
791
|
});
|
|
524
792
|
|
|
793
|
+
await writeWorkspaceAppMetadataOverride({
|
|
794
|
+
appId: input.appId,
|
|
795
|
+
description: input.description,
|
|
796
|
+
generated: true,
|
|
797
|
+
sourcePrompt: input.sourcePrompt,
|
|
798
|
+
updatedBy: currentOwnerEmail(),
|
|
799
|
+
});
|
|
800
|
+
|
|
525
801
|
await recordAudit({
|
|
526
802
|
action: "workspace-app.pending",
|
|
527
803
|
targetType: "workspace-app",
|
|
@@ -614,11 +890,12 @@ function readWorkspaceAppsFromFilesystem(
|
|
|
614
890
|
|
|
615
891
|
const apps = fs
|
|
616
892
|
.readdirSync(appsDir, { withFileTypes: true })
|
|
617
|
-
.filter((entry) => entry.isDirectory())
|
|
893
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
618
894
|
.map((entry): WorkspaceAppSummary | null => {
|
|
619
895
|
const appDir = path.join(appsDir, entry.name);
|
|
620
896
|
const pkg = readJson(path.join(appDir, "package.json"));
|
|
621
897
|
if (!pkg) return null;
|
|
898
|
+
const routeAccess = workspaceAppRouteAccessFromPackageJson(pkg);
|
|
622
899
|
return {
|
|
623
900
|
id: entry.name,
|
|
624
901
|
name: pkg.displayName || titleCase(entry.name),
|
|
@@ -626,6 +903,11 @@ function readWorkspaceAppsFromFilesystem(
|
|
|
626
903
|
path: `/${entry.name}`,
|
|
627
904
|
url: workspaceAppUrl(`/${entry.name}`),
|
|
628
905
|
isDispatch: entry.name === "dispatch",
|
|
906
|
+
audience:
|
|
907
|
+
workspaceAppAudienceFromPackageJson(pkg) ??
|
|
908
|
+
DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
909
|
+
publicPaths: routeAccess.publicPaths,
|
|
910
|
+
protectedPaths: routeAccess.protectedPaths,
|
|
629
911
|
status: "ready",
|
|
630
912
|
} satisfies WorkspaceAppSummary;
|
|
631
913
|
})
|
|
@@ -689,17 +971,92 @@ async function applyArchivedAndPending(
|
|
|
689
971
|
apps: WorkspaceAppSummary[],
|
|
690
972
|
options: ListWorkspaceAppsOptions,
|
|
691
973
|
): Promise<WorkspaceAppSummary[]> {
|
|
692
|
-
const [withPending, archivedIds] = await Promise.all([
|
|
974
|
+
const [withPending, archivedIds, metadataSettings] = await Promise.all([
|
|
693
975
|
appendPendingWorkspaceApps(apps),
|
|
694
976
|
listArchivedAppIds(),
|
|
977
|
+
readWorkspaceAppMetadataSettings(),
|
|
695
978
|
]);
|
|
696
979
|
const archivedSet = new Set(archivedIds);
|
|
697
|
-
const annotated = withPending.map((app) =>
|
|
698
|
-
|
|
699
|
-
|
|
980
|
+
const annotated = withPending.map((app) => {
|
|
981
|
+
const withMetadata = applyWorkspaceAppMetadataOverride(
|
|
982
|
+
app,
|
|
983
|
+
metadataSettings,
|
|
984
|
+
);
|
|
985
|
+
return archivedSet.has(app.id)
|
|
986
|
+
? { ...withMetadata, archived: true }
|
|
987
|
+
: withMetadata;
|
|
988
|
+
});
|
|
700
989
|
return options.includeArchived
|
|
701
|
-
? annotated
|
|
702
|
-
:
|
|
990
|
+
? filterAppsByAudience(annotated, options.audience)
|
|
991
|
+
: filterAppsByAudience(
|
|
992
|
+
annotated.filter((app) => !app.archived),
|
|
993
|
+
options.audience,
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function filterAppsByAudience(
|
|
998
|
+
apps: WorkspaceAppSummary[],
|
|
999
|
+
audience: ListWorkspaceAppsOptions["audience"],
|
|
1000
|
+
): WorkspaceAppSummary[] {
|
|
1001
|
+
if (!audience || audience === "all") return apps;
|
|
1002
|
+
return apps.filter(
|
|
1003
|
+
(app) =>
|
|
1004
|
+
(app.audience ?? DEFAULT_WORKSPACE_APP_AUDIENCE) ===
|
|
1005
|
+
normalizeWorkspaceAppAudience(audience),
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
export async function updateWorkspaceAppMetadata(input: {
|
|
1010
|
+
appId: string;
|
|
1011
|
+
name?: string | null;
|
|
1012
|
+
description?: string | null;
|
|
1013
|
+
}): Promise<WorkspaceAppSummary> {
|
|
1014
|
+
await assertCanManageAppCreationSettings();
|
|
1015
|
+
const appId = input.appId.trim();
|
|
1016
|
+
assertValidWorkspaceAppId(appId);
|
|
1017
|
+
|
|
1018
|
+
const apps = await listWorkspaceApps({
|
|
1019
|
+
includeAgentCards: false,
|
|
1020
|
+
includeArchived: true,
|
|
1021
|
+
});
|
|
1022
|
+
const app = apps.find((candidate) => candidate.id === appId);
|
|
1023
|
+
if (!app) throw new Error(`Workspace app "${appId}" was not found.`);
|
|
1024
|
+
|
|
1025
|
+
// Treat undefined/null as "field omitted, leave existing value alone"; an
|
|
1026
|
+
// explicit empty string clears the override (the app reverts to its
|
|
1027
|
+
// built-in name / no description). Without this, a partial update that
|
|
1028
|
+
// only touches one field silently wipes the other.
|
|
1029
|
+
const name = input.name == null ? app.name : input.name.trim();
|
|
1030
|
+
const description =
|
|
1031
|
+
input.description == null
|
|
1032
|
+
? (app.description ?? undefined)
|
|
1033
|
+
: input.description.trim();
|
|
1034
|
+
await writeWorkspaceAppMetadataOverride({
|
|
1035
|
+
appId,
|
|
1036
|
+
name,
|
|
1037
|
+
description,
|
|
1038
|
+
generated: false,
|
|
1039
|
+
updatedBy: currentOwnerEmail(),
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
await recordAudit({
|
|
1043
|
+
action: "workspace-app.metadata-updated",
|
|
1044
|
+
targetType: "workspace-app",
|
|
1045
|
+
targetId: appId,
|
|
1046
|
+
summary: `Updated workspace app details for ${name}`,
|
|
1047
|
+
metadata: {
|
|
1048
|
+
name,
|
|
1049
|
+
descriptionConfigured: !!description,
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
const updated = (
|
|
1054
|
+
await listWorkspaceApps({
|
|
1055
|
+
includeAgentCards: false,
|
|
1056
|
+
includeArchived: true,
|
|
1057
|
+
})
|
|
1058
|
+
).find((candidate) => candidate.id === appId);
|
|
1059
|
+
return updated ?? { ...app, name, description };
|
|
703
1060
|
}
|
|
704
1061
|
|
|
705
1062
|
export async function listWorkspaceApps(
|
|
@@ -745,6 +1102,9 @@ export async function listWorkspaceApps(
|
|
|
745
1102
|
path: "/dispatch",
|
|
746
1103
|
url: workspaceAppUrl("/dispatch"),
|
|
747
1104
|
isDispatch: true,
|
|
1105
|
+
audience: DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
1106
|
+
publicPaths: [],
|
|
1107
|
+
protectedPaths: [],
|
|
748
1108
|
status: "ready",
|
|
749
1109
|
},
|
|
750
1110
|
],
|
|
@@ -1166,6 +1526,7 @@ async function remoteAppCreationAuthorization(): Promise<
|
|
|
1166
1526
|
function buildWorkspaceAppPrompt(input: {
|
|
1167
1527
|
prompt: string;
|
|
1168
1528
|
appId?: string | null;
|
|
1529
|
+
description?: string | null;
|
|
1169
1530
|
template?: string | null;
|
|
1170
1531
|
selectedKeys?: string[];
|
|
1171
1532
|
selectedResources?: WorkspaceResourceOption[];
|
|
@@ -1176,6 +1537,9 @@ function buildWorkspaceAppPrompt(input: {
|
|
|
1176
1537
|
input.prompt.replace(/\b(build|create|make|an?|the|app|tool)\b/gi, " "),
|
|
1177
1538
|
) ||
|
|
1178
1539
|
"new-app";
|
|
1540
|
+
const appDescription =
|
|
1541
|
+
input.description?.trim() ||
|
|
1542
|
+
generateWorkspaceAppDescription(input.prompt, appId);
|
|
1179
1543
|
const selectedKeys = input.selectedKeys || [];
|
|
1180
1544
|
const selectedResources = input.selectedResources || [];
|
|
1181
1545
|
const resourceList = selectedResources.length
|
|
@@ -1192,6 +1556,7 @@ function buildWorkspaceAppPrompt(input: {
|
|
|
1192
1556
|
"Create a new agent-native app in this workspace.",
|
|
1193
1557
|
"",
|
|
1194
1558
|
`App name: ${appId}`,
|
|
1559
|
+
`App description: ${appDescription}`,
|
|
1195
1560
|
`Template to start from: ${input.template || "starter"}`,
|
|
1196
1561
|
`User prompt: ${input.prompt.trim()}`,
|
|
1197
1562
|
"If the user mentions a product or company such as Granola, Loom, Superhuman, Linear, or Notion, treat it as product inspiration unless they explicitly ask to connect to that service. Do not invent or require third-party API keys like GRANOLA_API_KEY just because a product is named.",
|
|
@@ -1222,11 +1587,13 @@ function buildWorkspaceAppPrompt(input: {
|
|
|
1222
1587
|
"",
|
|
1223
1588
|
"Branch readiness requirements before handing off:",
|
|
1224
1589
|
"- The CLI auto-fills package.json name and displayName from the app id; only edit the description / scripts / dependencies if the app actually needs more than the template provides.",
|
|
1590
|
+
`- Save a concise, human-readable app description in apps/${appId}/package.json "description" so Dispatch, A2A discovery, and connected agents can describe what this app does. Use the description above or improve it based on the prompt.`,
|
|
1225
1591
|
"- Do not add or update workspace-apps.json or .agent-native/workspace-apps.json unless the app needs an explicit external URL override; the root deploy generates the workspace app registry from apps/* and deploy metadata.",
|
|
1226
1592
|
"- Update pnpm-lock.yaml when adding or changing dependencies so Netlify can install the branch reliably.",
|
|
1227
1593
|
"- Update the app manifest/package/deploy metadata needed by the existing workspace deployment model; do not leave the branch relying only on uncommitted local state.",
|
|
1228
|
-
"- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment.",
|
|
1229
|
-
"-
|
|
1594
|
+
"- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment. Every sibling workspace app should be usable over A2A by default through call-agent.",
|
|
1595
|
+
"- Give the app agent context that sibling workspace apps are available over A2A with names and descriptions from the workspace app registry; do not hardcode a stale app list.",
|
|
1596
|
+
"- Include a final verification note covering the registry entry, manifest/deploy metadata, relative same-origin routing, and agent-card readiness.",
|
|
1230
1597
|
`When it is ready, start or update the workspace dev server and navigate the user to /${appId}.`,
|
|
1231
1598
|
].join("\n"),
|
|
1232
1599
|
};
|
|
@@ -1270,6 +1637,7 @@ async function grantSelectedWorkspaceResources(input: {
|
|
|
1270
1637
|
export async function startWorkspaceAppCreation(input: {
|
|
1271
1638
|
prompt: string;
|
|
1272
1639
|
appId?: string | null;
|
|
1640
|
+
description?: string | null;
|
|
1273
1641
|
template?: string | null;
|
|
1274
1642
|
secretIds?: string[];
|
|
1275
1643
|
resourceIds?: string[];
|
|
@@ -1277,6 +1645,7 @@ export async function startWorkspaceAppCreation(input: {
|
|
|
1277
1645
|
const initial = buildWorkspaceAppPrompt({
|
|
1278
1646
|
prompt: input.prompt,
|
|
1279
1647
|
appId: input.appId,
|
|
1648
|
+
description: input.description,
|
|
1280
1649
|
template: input.template,
|
|
1281
1650
|
});
|
|
1282
1651
|
assertValidWorkspaceAppId(initial.appId);
|
|
@@ -1304,11 +1673,15 @@ export async function startWorkspaceAppCreation(input: {
|
|
|
1304
1673
|
const built = buildWorkspaceAppPrompt({
|
|
1305
1674
|
prompt: input.prompt,
|
|
1306
1675
|
appId: input.appId,
|
|
1676
|
+
description: input.description,
|
|
1307
1677
|
template: input.template,
|
|
1308
1678
|
selectedKeys,
|
|
1309
1679
|
selectedResources,
|
|
1310
1680
|
});
|
|
1311
1681
|
const prompt = built.prompt;
|
|
1682
|
+
const appDescription =
|
|
1683
|
+
input.description?.trim() ||
|
|
1684
|
+
generateWorkspaceAppDescription(input.prompt, built.appId);
|
|
1312
1685
|
|
|
1313
1686
|
if (isLocal) {
|
|
1314
1687
|
await requestSelectedVaultKeys({
|
|
@@ -1375,6 +1748,8 @@ export async function startWorkspaceAppCreation(input: {
|
|
|
1375
1748
|
await recordPendingWorkspaceApp({
|
|
1376
1749
|
appId: built.appId,
|
|
1377
1750
|
projectId: settings.builderProjectId,
|
|
1751
|
+
description: appDescription,
|
|
1752
|
+
sourcePrompt: input.prompt,
|
|
1378
1753
|
branchName: result.branchName,
|
|
1379
1754
|
builderUrl: result.url,
|
|
1380
1755
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch-specific onboarding steps.
|
|
3
|
+
*
|
|
4
|
+
* Slack/Telegram/etc. are auto-registered at order 60 by the framework when
|
|
5
|
+
* their env keys are declared `required: true` in `env-config.ts`. Without
|
|
6
|
+
* any earlier dispatch-specific step, a brand-new workspace lands on
|
|
7
|
+
* "Connect Slack" as the first visible to-do — which is intimidating before
|
|
8
|
+
* the user has even created a real app. This step nudges them at adding
|
|
9
|
+
* their first workspace app first.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { registerOnboardingStep } from "@agent-native/core/onboarding";
|
|
13
|
+
import { listWorkspaceApps } from "./app-creation-store.js";
|
|
14
|
+
|
|
15
|
+
let registered = false;
|
|
16
|
+
|
|
17
|
+
export function registerDispatchOnboardingSteps(): void {
|
|
18
|
+
if (registered) return;
|
|
19
|
+
registered = true;
|
|
20
|
+
|
|
21
|
+
registerOnboardingStep({
|
|
22
|
+
id: "dispatch:create-first-app",
|
|
23
|
+
title: "Create your first app",
|
|
24
|
+
description:
|
|
25
|
+
"Add a workspace app like Mail, Calendar, or Slides — or describe a custom app from the Apps page.",
|
|
26
|
+
order: 5,
|
|
27
|
+
required: false,
|
|
28
|
+
methods: [
|
|
29
|
+
{
|
|
30
|
+
id: "open-apps",
|
|
31
|
+
kind: "link",
|
|
32
|
+
primary: true,
|
|
33
|
+
label: "Browse apps",
|
|
34
|
+
payload: { url: "/dispatch/apps", external: false },
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
isComplete: async () => {
|
|
38
|
+
try {
|
|
39
|
+
const apps = await listWorkspaceApps({
|
|
40
|
+
includeAgentCards: false,
|
|
41
|
+
includeArchived: true,
|
|
42
|
+
});
|
|
43
|
+
return apps.some((app) => !app.isDispatch);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|