crewx 0.9.0-rc.136 → 0.9.0-rc.137

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.
@@ -33,6 +33,8 @@ const SESSION_FILE_NAME = 'session.json';
33
33
  const DEV_DB_FILE_NAME = 'ui-instances.db';
34
34
  const DEV_DB_FILE_PATTERN = /^ui-instances(?:-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?\.db$/;
35
35
  const SESSION_CALL_LIMIT = 50;
36
+ // A dev session that nobody has touched for this long gives its workspace snapshot back.
37
+ const APP_DEV_IDLE_RELEASE_MS = 10 * 60 * 1000;
36
38
  const REGISTERED_APP_ENTRY_PATH = 'ui/index.html';
37
39
  const APP_DEV_AGENT_ACTOR_ID = 'network:runtime:app-dev';
38
40
  const APP_DEV_APPROVAL_REASON = 'Workspace-source App code only; no conversation data; same power as the agent shell.';
@@ -42,6 +44,8 @@ let AppDevService = class AppDevService {
42
44
  workspaceAssetCatalogResolver;
43
45
  appServices = new Map();
44
46
  locks = new Map();
47
+ latestDevCaptures = new Map();
48
+ idleReleases = new Map();
45
49
  contentChannel;
46
50
  constructor(projectRepository, workspaceEventBus, contentChannel, workspaceAssetCatalogResolver = mcp_app_workspace_asset_catalog_resolver_js_1.mcpAppWorkspaceAssetCatalogResolver) {
47
51
  this.projectRepository = projectRepository;
@@ -66,7 +70,7 @@ let AppDevService = class AppDevService {
66
70
  ?? this.newSession(workspace, appId);
67
71
  document.workspaceSlug = workspace.slug;
68
72
  const context = this.contextFor(paths, workspace, document);
69
- const capture = await context.service.captureWorkspaceSnapshot(workspace.path, { definitionPolicy: 'latest-dev' });
73
+ const capture = await this.captureLatestDev(context);
70
74
  const registration = this.requireWorkspaceSourceApp(context, capture, true);
71
75
  if (registration !== null) {
72
76
  await this.prepareApp(context, capture);
@@ -83,9 +87,9 @@ let AppDevService = class AppDevService {
83
87
  }
84
88
  async getApp(workspace, appId) {
85
89
  const context = await this.requireSession(workspace, appId);
86
- return this.withLock(context.appDirectory, async () => {
90
+ return this.withLock(context.appDirectory, async (arrivedAt) => {
87
91
  const current = await this.requireSession(workspace, appId);
88
- const prepared = await this.prepareApp(current);
92
+ const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
89
93
  return {
90
94
  appId,
91
95
  sessionId: current.document.sessionId,
@@ -102,9 +106,9 @@ let AppDevService = class AppDevService {
102
106
  }
103
107
  async putContent(workspace, appId, actionId, requestId, mediaType, bytes) {
104
108
  const context = await this.requireSession(workspace, appId);
105
- return this.withLock(context.appDirectory, async () => {
109
+ return this.withLock(context.appDirectory, async (arrivedAt) => {
106
110
  const current = await this.requireSession(workspace, appId);
107
- const prepared = await this.prepareApp(current);
111
+ const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
108
112
  const actions = prepared.contentActions;
109
113
  if (!actions || !(0, sdk_1.contentActionsMatch)(actions, prepared.instanceId, prepared.resolved.instance.projection.descriptor.definition)
110
114
  || actions.actions.find((action) => action.actionId === actionId)?.in?.mediaType !== mediaType) {
@@ -123,9 +127,9 @@ let AppDevService = class AppDevService {
123
127
  }
124
128
  async getContent(workspace, appId, requestId) {
125
129
  const context = await this.requireSession(workspace, appId);
126
- return this.withLock(context.appDirectory, async () => {
130
+ return this.withLock(context.appDirectory, async (arrivedAt) => {
127
131
  const current = await this.requireSession(workspace, appId);
128
- const prepared = await this.prepareApp(current);
132
+ const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
129
133
  const store = new sdk_1.UiInstanceStore(current.databasePath);
130
134
  let receipt;
131
135
  try {
@@ -160,9 +164,9 @@ let AppDevService = class AppDevService {
160
164
  }
161
165
  async getResource(workspace, appId) {
162
166
  const context = await this.requireSession(workspace, appId);
163
- return this.withLock(context.appDirectory, async () => {
167
+ return this.withLock(context.appDirectory, async (arrivedAt) => {
164
168
  const current = await this.requireSession(workspace, appId);
165
- const prepared = await this.prepareApp(current);
169
+ const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
166
170
  const resource = prepared.service.getInstanceResourceForSnapshot(current.workspace.id, devThreadId(current.document.sessionId), prepared.instanceId, prepared.capture);
167
171
  if (!resource)
168
172
  throw new common_1.NotFoundException({ code: 'MCP_APP_RESOURCE_NOT_FOUND' });
@@ -268,9 +272,9 @@ let AppDevService = class AppDevService {
268
272
  }
269
273
  async getState(workspace, appId) {
270
274
  const context = await this.requireSession(workspace, appId);
271
- return this.withLock(context.appDirectory, async () => {
275
+ return this.withLock(context.appDirectory, async (arrivedAt) => {
272
276
  const current = await this.requireSession(workspace, appId);
273
- const prepared = await this.prepareApp(current);
277
+ const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
274
278
  const projections = await prepared.service.buildAgentContextProjectionsForSnapshot(current.workspace.id, devThreadId(current.document.sessionId), prepared.instanceId, prepared.capture);
275
279
  const projection = projections?.find((candidate) => candidate.instanceId === prepared.instanceId);
276
280
  if (!projection)
@@ -298,9 +302,9 @@ let AppDevService = class AppDevService {
298
302
  }
299
303
  async getDebug(workspace, appId, probe, after) {
300
304
  const context = await this.requireSession(workspace, appId);
301
- return this.withLock(context.appDirectory, async () => {
305
+ return this.withLock(context.appDirectory, async (arrivedAt) => {
302
306
  const current = await this.requireSession(workspace, appId);
303
- const prepared = await this.prepareApp(current);
307
+ const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
304
308
  const logPath = (0, app_debug_log_js_1.appDebugLogPath)(current.workspacePath, appId);
305
309
  if (logPath === null)
306
310
  throw new common_1.NotFoundException({ code: 'DEBUG_UNAVAILABLE' });
@@ -368,7 +372,7 @@ let AppDevService = class AppDevService {
368
372
  const context = await this.requireSession(workspace, appId);
369
373
  return this.withLock(context.appDirectory, async () => {
370
374
  const current = await this.requireSession(workspace, appId);
371
- const capture = await current.service.captureWorkspaceSnapshot(current.workspacePath, { definitionPolicy: 'latest-dev' });
375
+ const capture = await this.captureLatestDev(current);
372
376
  const registration = this.requireWorkspaceSourceApp(current, capture, true);
373
377
  const items = [];
374
378
  const diagnostic = this.findDiagnostic(capture, appId);
@@ -444,7 +448,7 @@ let AppDevService = class AppDevService {
444
448
  const context = await this.requireSession(workspace, appId);
445
449
  return this.withLock(context.appDirectory, async () => {
446
450
  const current = await this.requireSession(workspace, appId);
447
- const capture = await current.service.captureWorkspaceSnapshot(current.workspacePath, { definitionPolicy: 'latest-dev' });
451
+ const capture = await this.captureLatestDev(current);
448
452
  this.requireWorkspaceSourceApp(current, capture, false);
449
453
  this.closeAppService(current.databasePath);
450
454
  assertSafeAppDirectoryForDeletion(current.devRoot, current.appDirectory);
@@ -471,13 +475,33 @@ let AppDevService = class AppDevService {
471
475
  });
472
476
  }
473
477
  onModuleDestroy() {
478
+ for (const { timer } of this.idleReleases.values())
479
+ clearTimeout(timer);
480
+ this.idleReleases.clear();
481
+ this.latestDevCaptures.clear();
474
482
  for (const service of this.appServices.values())
475
483
  service.onModuleDestroy();
476
484
  this.appServices.clear();
477
485
  }
478
- async prepareApp(context, suppliedCapture, captureOptions = { definitionPolicy: 'latest-dev' }) {
486
+ /**
487
+ * Takes the latest-dev capture for a session. A request that queued behind
488
+ * others reuses a capture that began after it arrived: that capture already
489
+ * saw every edit made before the request, so a burst of polls costs one scan.
490
+ */
491
+ async captureLatestDev(context, arrivedAt) {
492
+ const shared = this.latestDevCaptures.get(context.databasePath);
493
+ if (arrivedAt !== undefined && shared !== undefined && shared.startedAt >= arrivedAt)
494
+ return shared.capture;
495
+ const startedAt = process.hrtime.bigint();
496
+ const capture = await context.service.captureWorkspaceSnapshot(context.workspacePath, { definitionPolicy: 'latest-dev' });
497
+ this.latestDevCaptures.set(context.databasePath, { startedAt, capture });
498
+ return capture;
499
+ }
500
+ async prepareApp(context, suppliedCapture, captureOptions, arrivedAt) {
479
501
  const capture = suppliedCapture
480
- ?? await context.service.captureWorkspaceSnapshot(context.workspacePath, captureOptions);
502
+ ?? (captureOptions === undefined
503
+ ? await this.captureLatestDev(context, arrivedAt)
504
+ : await context.service.captureWorkspaceSnapshot(context.workspacePath, captureOptions));
481
505
  const registration = this.requireWorkspaceSourceApp(context, capture, false);
482
506
  if (registration === null || !registration.definition)
483
507
  throw new common_1.HttpException({ code: 'APP_INVALID' }, 409);
@@ -651,8 +675,33 @@ let AppDevService = class AppDevService {
651
675
  service.markBootReceiptsRecovered();
652
676
  this.appServices.set(databasePath, service);
653
677
  }
678
+ this.armIdleRelease(paths.appDirectory, databasePath);
654
679
  return { ...paths, databasePath, workspace, document, service };
655
680
  }
681
+ /**
682
+ * Nothing tells the server that a dev tab or an agent is done with a session,
683
+ * so a session left alone gives back its workspace snapshot (the bulk of its
684
+ * memory). The next request rebuilds it the way a restarted server would.
685
+ */
686
+ armIdleRelease(appDirectory, databasePath) {
687
+ const previous = this.idleReleases.get(appDirectory);
688
+ if (previous)
689
+ clearTimeout(previous.timer);
690
+ const timer = setTimeout(() => this.releaseIdleSession(appDirectory), APP_DEV_IDLE_RELEASE_MS);
691
+ timer.unref();
692
+ this.idleReleases.set(appDirectory, { timer, databasePath });
693
+ }
694
+ releaseIdleSession(appDirectory) {
695
+ const entry = this.idleReleases.get(appDirectory);
696
+ if (!entry)
697
+ return;
698
+ if (this.locks.has(appDirectory)) {
699
+ this.armIdleRelease(appDirectory, entry.databasePath);
700
+ return;
701
+ }
702
+ this.idleReleases.delete(appDirectory);
703
+ this.closeAppService(entry.databasePath);
704
+ }
656
705
  newSession(workspace, appId) {
657
706
  return {
658
707
  schemaVersion: 1,
@@ -715,15 +764,17 @@ let AppDevService = class AppDevService {
715
764
  closeAppService(databasePath) {
716
765
  this.appServices.get(databasePath)?.onModuleDestroy();
717
766
  this.appServices.delete(databasePath);
767
+ this.latestDevCaptures.delete(databasePath);
718
768
  }
719
769
  async withLock(key, operation) {
770
+ const arrivedAt = process.hrtime.bigint();
720
771
  const previous = this.locks.get(key) ?? Promise.resolve();
721
772
  let release;
722
773
  const current = new Promise((resolveLock) => { release = resolveLock; });
723
774
  this.locks.set(key, current);
724
775
  await previous;
725
776
  try {
726
- return await operation();
777
+ return await operation(arrivedAt);
727
778
  }
728
779
  finally {
729
780
  release();
@@ -19,6 +19,7 @@ exports.resolveSnapshotDispatchCapabilities = resolveSnapshotDispatchCapabilitie
19
19
  exports.isUserLaunchableRegistration = isUserLaunchableRegistration;
20
20
  exports.isMcpAppLauncherLaunchable = isMcpAppLauncherLaunchable;
21
21
  exports.getLaunchableMcpAppIds = getLaunchableMcpAppIds;
22
+ exports.actionInputSchemasFromTools = actionInputSchemasFromTools;
22
23
  exports.defaultAppButtons = defaultAppButtons;
23
24
  exports.createCrewxMcpAppCatalog = createCrewxMcpAppCatalog;
24
25
  exports.createCrewxMcpAppRegistry = createCrewxMcpAppRegistry;
@@ -259,6 +260,10 @@ function createBundleAwareMcpAppRegistry(coreRegistry) {
259
260
  },
260
261
  };
261
262
  }
263
+ /** Agent-facing App description: single line, bounded. */
264
+ function promptAppDescription(description) {
265
+ return description.replace(/\s+/g, ' ').trim().slice(0, 300);
266
+ }
262
267
  function hasNoRequestedCspOrigins(csp) {
263
268
  return csp.connectDomains.length === 0
264
269
  && csp.resourceDomains.length === 0
@@ -466,12 +471,33 @@ function actionLabel(actionId) {
466
471
  .replace(/[_-]+/g, ' ')
467
472
  .replace(/\b\w/g, (letter) => letter.toUpperCase());
468
473
  }
469
- function defaultAppButtons(definition) {
474
+ /**
475
+ * Per-action input schemas carried by a registration's tools. Dispatcher tools
476
+ * (several action ids behind one wrapper schema) name no single action and are
477
+ * skipped, so their actions keep the keys-only fallback in `defaultAppButtons`.
478
+ */
479
+ function actionInputSchemasFromTools(tools) {
480
+ const schemas = new Map();
481
+ for (const tool of tools) {
482
+ if (tool.kind !== 'action' || typeof tool.actionId !== 'string')
483
+ continue;
484
+ if (tool.actionIds !== undefined && (tool.actionIds.length !== 1 || tool.actionIds[0] !== tool.actionId))
485
+ continue;
486
+ schemas.set(tool.actionId, tool.inputSchema);
487
+ }
488
+ return schemas;
489
+ }
490
+ /**
491
+ * Buttons derived from the definition's actions. `inputSchemas` supplies each
492
+ * action's real schema (types, enum, required, bounds); an action without one
493
+ * falls back to the keys-only string schema this function always produced.
494
+ */
495
+ function defaultAppButtons(definition, inputSchemas) {
470
496
  return Object.entries(definition.actions).map(([id, action]) => ({
471
497
  id,
472
498
  label: actionLabel(id),
473
499
  enabled: true,
474
- inputSchema: {
500
+ inputSchema: inputSchemas?.get(id) ?? {
475
501
  type: 'object',
476
502
  properties: Object.fromEntries((action.inputKeys ?? []).map((key) => [key, { type: 'string' }])),
477
503
  additionalProperties: false,
@@ -767,7 +793,13 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
767
793
  const registeredProductBundle = await this.ensureBundleCatalog(workspaceRoot);
768
794
  const skillBackend = this.workspaceSkillBackendForCapture(workspaceRoot, options.onSkillBackendInspection, productBundle);
769
795
  const current = this.snapshotHolders.get(key);
770
- if (!current || options.definitionPolicy === 'latest-dev') {
796
+ // latest-dev still sees every edit: reconfirm re-stats the scanned files and
797
+ // returns a new generation when any fingerprint moved, but hands back the
798
+ // held snapshot untouched otherwise. Only an inspecting capture must be
799
+ // rebuilt, because the skill backend port is baked into the snapshot.
800
+ const needsFreshDiscovery = options.definitionPolicy === 'latest-dev'
801
+ && options.onSkillBackendInspection !== undefined;
802
+ if (!current || needsFreshDiscovery) {
771
803
  const [snapshot] = await Promise.all([
772
804
  (0, sdk_1.discoverWorkspaceAppSnapshot)(workspaceRoot, {
773
805
  builtins: this.catalog.coreRegistry,
@@ -966,6 +998,10 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
966
998
  content: candidate.definition.actions[actionId]?.content,
967
999
  backend: candidate.normalized.manifest.actions.find((action) => action.actionId === actionId)?.backend,
968
1000
  }, [], verifiedBundle) === 'allowed'));
1001
+ const bundleInputSchemas = new Map(candidate.normalized.manifest.actions.map((action) => [
1002
+ action.actionId,
1003
+ action.inputSchema,
1004
+ ]));
969
1005
  const canOpen = candidate.definition.usage !== 'card'
970
1006
  && (candidate.initialState !== undefined || candidate.normalized.manifest.actions.length > 0);
971
1007
  // Publish the descriptor only after every server-side action adapter is ready.
@@ -982,7 +1018,7 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
982
1018
  } : {}),
983
1019
  actions,
984
1020
  modelActions,
985
- buttons: () => defaultAppButtons(candidate.definition),
1021
+ buttons: () => defaultAppButtons(candidate.definition, bundleInputSchemas),
986
1022
  summaryAllow: candidate.definition.summaryProjection?.allow ?? [],
987
1023
  ...(candidate.definition.usage === 'card' ? { openPolicy: 'anchored_only' } : {}),
988
1024
  });
@@ -1332,6 +1368,44 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
1332
1368
  return null;
1333
1369
  return registration;
1334
1370
  }
1371
+ /**
1372
+ * Effective agent class of one App in this snapshot, folded from the same
1373
+ * per-action decision the action path applies (`getTaskAgentAccess`):
1374
+ * `direct` when any action runs without human approval, `approval` when
1375
+ * agent-callable actions exist but all need approval, else `none`.
1376
+ */
1377
+ classifyAppAgentClassForSnapshot(appId, capture) {
1378
+ const definition = capture.snapshot.registry.getRegistration(appId)?.definition
1379
+ ?? this.getServerRegistrationForSnapshot(appId, capture)?.descriptor.definition;
1380
+ if (!definition)
1381
+ return 'none';
1382
+ let approval = false;
1383
+ for (const actionId of Object.keys(definition.actions)) {
1384
+ const access = this.getTaskAgentAccess(appId, actionId, capture);
1385
+ if (access === 'allowed')
1386
+ return 'direct';
1387
+ if (access === 'approval_required')
1388
+ approval = true;
1389
+ }
1390
+ return approval ? 'approval' : 'none';
1391
+ }
1392
+ /**
1393
+ * Every launchable App of the snapshot (the `MCP.APP.LIST` set) with its
1394
+ * effective agent class. Read-only: it reuses the caller's capture and never
1395
+ * captures a second snapshot.
1396
+ */
1397
+ listAvailableAppsForSnapshot(capture) {
1398
+ return this.listLaunchersForSnapshot(capture).flatMap((launcher) => {
1399
+ if (!launcher.definition)
1400
+ return [];
1401
+ return [{
1402
+ id: launcher.appId,
1403
+ name: launcher.name,
1404
+ description: promptAppDescription(launcher.description),
1405
+ agent: this.classifyAppAgentClassForSnapshot(launcher.appId, capture),
1406
+ }];
1407
+ });
1408
+ }
1335
1409
  getTaskAgentActions(appId, capture) {
1336
1410
  const bundleCandidate = capture
1337
1411
  ? this.bundleCandidateForSnapshot(appId, capture)
@@ -2141,7 +2215,8 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
2141
2215
  source: 'server',
2142
2216
  instance,
2143
2217
  definition,
2144
- buttons: registration.buttons?.(instance.state) ?? defaultAppButtons(definition),
2218
+ buttons: registration.buttons?.(instance.state)
2219
+ ?? defaultAppButtons(definition, actionInputSchemasFromTools(registration.descriptor.tools)),
2145
2220
  });
2146
2221
  return {
2147
2222
  kind: 'app',
@@ -2243,6 +2318,41 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
2243
2318
  const capture = await this.captureWorkspaceSnapshot((0, workspace_context_store_js_1.getWorkspacePath)());
2244
2319
  return this.buildAgentContextProjectionsForSnapshot(workspaceId, threadId, focusedUiInstanceId, capture);
2245
2320
  }
2321
+ /**
2322
+ * The open-App projections and the launchable-App list for one MSG.SEND, both
2323
+ * read from a single workspace snapshot capture.
2324
+ */
2325
+ async buildAgentContext(workspaceId, threadId, focusedUiInstanceId) {
2326
+ if (!isMcpAppsEnabled())
2327
+ return { projections: undefined, available: [] };
2328
+ assertWorkspaceThreadScope(workspaceId, threadId);
2329
+ const capture = await this.captureWorkspaceSnapshot((0, workspace_context_store_js_1.getWorkspacePath)());
2330
+ const projections = await this.buildAgentContextProjectionsForSnapshot(workspaceId, threadId, focusedUiInstanceId, capture);
2331
+ let available = [];
2332
+ try {
2333
+ available = this.listAvailableAppsForSnapshot(capture);
2334
+ }
2335
+ catch (error) {
2336
+ // The list is additive prompt context; a failure must not drop the open-App projections.
2337
+ mcpAppsLogger.warn(`Available App list skipped [thread=${threadId}]: ${error instanceof Error ? error.message : String(error)}`);
2338
+ }
2339
+ return { projections, available };
2340
+ }
2341
+ /**
2342
+ * Agent-context view of a descriptor: the App identity plus, for each button,
2343
+ * the effective agent access the action path applies (`getTaskAgentAccess`,
2344
+ * the same value `crewx app state` reports).
2345
+ */
2346
+ withAgentPromptData(descriptor, app, capture) {
2347
+ return {
2348
+ ...descriptor,
2349
+ app: { id: app.appId, name: app.name, description: promptAppDescription(app.description) },
2350
+ buttons: descriptor.buttons.map((button) => ({
2351
+ ...button,
2352
+ agentAccess: this.getTaskAgentAccess(app.appId, button.id, capture),
2353
+ })),
2354
+ };
2355
+ }
2246
2356
  async buildAgentContextProjectionsForSnapshot(workspaceId, threadId, focusedUiInstanceId, capture) {
2247
2357
  if (!isMcpAppsEnabled())
2248
2358
  return undefined;
@@ -2268,11 +2378,15 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
2268
2378
  if (!resolved)
2269
2379
  continue;
2270
2380
  const projections = resolved.appProjection;
2381
+ const workflowApp = this.catalog.serverApps.get(exports.MCP_APP_WORKFLOW_ID);
2382
+ const aiDescriptor = workflowApp
2383
+ ? this.withAgentPromptData(projections.ai.descriptor, workflowApp.descriptor, capture)
2384
+ : projections.ai.descriptor;
2271
2385
  if (focusedUiInstanceId !== undefined && focusedUiInstanceId === instance.id) {
2272
- output.push(projections.ai.descriptor);
2386
+ output.push(aiDescriptor);
2273
2387
  }
2274
2388
  else {
2275
- output.push(summarizeDescriptor(projections.ai.descriptor, this.catalog.serverApps.get(exports.MCP_APP_WORKFLOW_ID)?.summaryAllow ?? []));
2389
+ output.push(summarizeDescriptor(aiDescriptor, workflowApp?.summaryAllow ?? []));
2276
2390
  }
2277
2391
  }
2278
2392
  catch {
@@ -2289,14 +2403,15 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
2289
2403
  const projections = await this.projectSnapshotInstance(instance, registration, capture, context);
2290
2404
  if (!projections)
2291
2405
  continue;
2406
+ const aiDescriptor = this.withAgentPromptData(projections.ai.descriptor, registration, capture);
2292
2407
  if (focusedUiInstanceId !== undefined && focusedUiInstanceId === instance.id) {
2293
- output.push(projections.ai.descriptor);
2408
+ output.push(aiDescriptor);
2294
2409
  }
2295
2410
  else {
2296
2411
  const allow = registration.resourceBinding.kind === 'mcp'
2297
2412
  ? this.catalog.serverApps.get(registration.appId)?.summaryAllow ?? []
2298
2413
  : registration.definition?.summaryProjection?.allow ?? [];
2299
- output.push(summarizeDescriptor(projections.ai.descriptor, allow));
2414
+ output.push(summarizeDescriptor(aiDescriptor, allow));
2300
2415
  }
2301
2416
  }
2302
2417
  catch {
@@ -2418,7 +2533,7 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
2418
2533
  source: 'server',
2419
2534
  instance,
2420
2535
  definition,
2421
- buttons: defaultAppButtons(definition),
2536
+ buttons: defaultAppButtons(definition, actionInputSchemasFromTools(registration.tools)),
2422
2537
  });
2423
2538
  }
2424
2539
  readThreadInstances(workspaceId, threadId, capture, previousDefinition) {
@@ -2590,7 +2705,8 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
2590
2705
  source: 'server',
2591
2706
  instance,
2592
2707
  definition,
2593
- buttons: registration.buttons?.(instance.state) ?? defaultAppButtons(definition),
2708
+ buttons: registration.buttons?.(instance.state)
2709
+ ?? defaultAppButtons(definition, actionInputSchemasFromTools(registration.descriptor.tools)),
2594
2710
  });
2595
2711
  }
2596
2712
  return await registration.projectionFactory({
@@ -389,7 +389,7 @@ let MessageService = MessageService_1 = class MessageService {
389
389
  // MSG.SEND owns the server-side context boundary for normal agent input.
390
390
  // The UI never supplies this projection: resolveMcpApp rereads the current
391
391
  // instance, workflow run, definition, and anchor before returning it.
392
- const mcpAppProjections = await this.resolveMcpAppProjections(threadId, ensureWorkspaceId, focusedUiInstanceId);
392
+ const { projections: mcpAppProjections, available: mcpAppAvailable } = await this.resolveMcpAppContext(threadId, ensureWorkspaceId, focusedUiInstanceId);
393
393
  const frontAppModelContext = focusedUiInstanceId
394
394
  && mcpAppProjections?.some((descriptor) => (descriptor.instanceId === focusedUiInstanceId && descriptor.status === 'active'))
395
395
  ? this.mcpAppWorkspaceService?.getModelContext(ensureWorkspaceId, threadId, focusedUiInstanceId)
@@ -475,6 +475,7 @@ let MessageService = MessageService_1 = class MessageService {
475
475
  metadata,
476
476
  cwd: workspaceRoot,
477
477
  ...(mcpAppProjections ? { mcpAppProjections } : {}),
478
+ ...(mcpAppAvailable.length > 0 ? { mcpAppAvailable } : {}),
478
479
  ...(frontAppModelContext ? { mcpAppModelContext: frontAppModelContext } : {}),
479
480
  ...(model ? { model } : {}),
480
481
  ...(effort !== undefined ? { effort } : {}),
@@ -524,19 +525,22 @@ let MessageService = MessageService_1 = class MessageService {
524
525
  }
525
526
  }
526
527
  /**
527
- * Resolve server-owned MCP App projections for this thread. The normal Nest
528
- * path reads the SDK ledger; the task-metadata branch only preserves behavior
529
- * for older direct MessageService hosts that do not provide the adapter.
528
+ * Resolve server-owned MCP App context for this thread: the open-App
529
+ * projections and the launchable-App list, from one workspace snapshot. The
530
+ * normal Nest path reads the SDK ledger; the task-metadata branch only
531
+ * preserves behavior for older direct MessageService hosts that do not
532
+ * provide the adapter (and have no App list to offer).
530
533
  */
531
- async resolveMcpAppProjections(threadId, workspaceId, focusedUiInstanceId) {
534
+ async resolveMcpAppContext(threadId, workspaceId, focusedUiInstanceId) {
535
+ const none = { projections: undefined, available: [] };
532
536
  if (!(0, mcp_apps_js_1.isMcpAppsEnabled)())
533
- return undefined;
537
+ return none;
534
538
  try {
535
539
  if (this.mcpAppWorkspaceService) {
536
- return await this.mcpAppWorkspaceService.buildAgentContextProjections(workspaceId, threadId, focusedUiInstanceId);
540
+ return await this.mcpAppWorkspaceService.buildAgentContext(workspaceId, threadId, focusedUiInstanceId);
537
541
  }
538
542
  if (!this.workflowApprovalBroker)
539
- return undefined;
543
+ return none;
540
544
  const candidates = this.threadRepo
541
545
  .findAllTasks(threadId, workspaceId)
542
546
  .map(parseWorkflowUiInstanceTaskCandidate)
@@ -557,7 +561,7 @@ let MessageService = MessageService_1 = class MessageService {
557
561
  actor: { kind: 'authenticated', id: 'network:session' },
558
562
  });
559
563
  if (resolved)
560
- return [resolved.appProjection.ai.descriptor];
564
+ return { projections: [resolved.appProjection.ai.descriptor], available: [] };
561
565
  }
562
566
  }
563
567
  catch (error) {
@@ -565,7 +569,7 @@ let MessageService = MessageService_1 = class MessageService {
565
569
  // send fail. The legacy prompt remains the safe fallback.
566
570
  this.logger.warn(`MCP App projection skipped [thread=${threadId}]: ${error.message}`);
567
571
  }
568
- return undefined;
572
+ return none;
569
573
  }
570
574
  saveImages(threadId, images, preparedImages) {
571
575
  return this.savePreparedImages(threadId, preparedImages ?? this.prepareImages(threadId, images));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewx",
3
- "version": "0.9.0-rc.136",
3
+ "version": "0.9.0-rc.137",
4
4
  "description": "CrewX — AI agent team dashboard with Electron UI and CLI (Web + Electron + Global CLI)",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -109,20 +109,22 @@
109
109
  "ws": "8.20.0",
110
110
  "yargs": "17.7.0",
111
111
  "zod": "3.25.76",
112
- "@crewx/chromex": "0.1.0-rc.169",
113
- "@crewx/cron": "0.1.10-rc.171",
114
- "@crewx/cli": "0.9.0-rc.136",
115
- "@crewx/doc": "0.1.9-rc.129",
116
- "@crewx/dreaming": "0.1.0-rc.162",
117
- "@crewx/knowledge-core": "0.1.17-rc.146",
118
- "@crewx/memory": "0.1.23-rc.153",
119
- "@crewx/search": "0.1.10-rc.132",
120
- "@crewx/skill": "0.1.20-rc.30",
121
- "@crewx/sdk": "0.9.0-rc.136",
122
- "@crewx/shared": "0.0.6-rc.51",
123
- "@crewx/wi": "0.1.10-rc.156",
124
- "@crewx/wbs": "0.1.10-rc.162",
125
- "@crewx/workflow": "0.3.22-rc.182"
112
+ "@crewx/app": "0.1.0-rc.18",
113
+ "@crewx/chromex": "0.1.0-rc.170",
114
+ "@crewx/cli": "0.9.0-rc.137",
115
+ "@crewx/cron": "0.1.10-rc.172",
116
+ "@crewx/doc": "0.1.9-rc.130",
117
+ "@crewx/dreaming": "0.1.0-rc.163",
118
+ "@crewx/knowledge-core": "0.1.17-rc.147",
119
+ "@crewx/sdk": "0.9.0-rc.137",
120
+ "@crewx/memory": "0.1.23-rc.154",
121
+ "@crewx/skill": "0.1.20-rc.31",
122
+ "@crewx/notify": "0.1.0-rc.108",
123
+ "@crewx/wbs": "0.1.10-rc.163",
124
+ "@crewx/workflow": "0.3.22-rc.183",
125
+ "@crewx/search": "0.1.10-rc.133",
126
+ "@crewx/wi": "0.1.10-rc.157",
127
+ "@crewx/shared": "0.0.6-rc.52"
126
128
  },
127
129
  "devDependencies": {
128
130
  "@anthropic-ai/claude-agent-sdk": "0.2.132",
@@ -390,6 +392,8 @@
390
392
  "test:grok-poc": "node --test scripts/grok-poc/capture.test.mjs scripts/grok-poc/resume-proof.test.mjs",
391
393
  "test:server": "vitest run --config vitest.server.config.ts",
392
394
  "test:bundled-apps": "node --test scripts/build-bundled-apps.spec.mjs",
395
+ "test:app-build": "node --test scripts/check-sdk-app-imports.test.mjs scripts/build-app.test.mjs",
396
+ "check:sdk-app-imports": "node scripts/check-sdk-app-imports.mjs",
393
397
  "type-check": "tsc --noEmit",
394
398
  "build:manual": "node scripts/build-manual.mjs"
395
399
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewx/cli",
3
- "version": "0.9.0-rc.136",
3
+ "version": "0.9.0-rc.137",
4
4
  "license": "UNLICENSED",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"