@tutti-os/workspace-app-center 0.0.9 → 0.0.10

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.
@@ -0,0 +1,1742 @@
1
+ import {
2
+ workspaceAppManifestSchemaVersion
3
+ } from "./chunk-QSAEUGFC.js";
4
+
5
+ // src/core/appCenterControllerHelpers.ts
6
+ function resolveAppRunDurationMs(startedAtUnixMs, now) {
7
+ if (typeof startedAtUnixMs !== "number" || !Number.isFinite(startedAtUnixMs) || startedAtUnixMs <= 0) {
8
+ return null;
9
+ }
10
+ return Math.max(0, now - startedAtUnixMs);
11
+ }
12
+ function sortWorkspaceAppCenterApps(apps) {
13
+ return [...apps].sort(
14
+ (left, right) => left.name.localeCompare(right.name, void 0, { sensitivity: "base" })
15
+ );
16
+ }
17
+ function mergeWorkspaceAppCatalogFields(currentApp, snapshotApp) {
18
+ return {
19
+ ...currentApp,
20
+ availableIconUrl: snapshotApp.availableIconUrl,
21
+ availableVersion: snapshotApp.availableVersion,
22
+ description: snapshotApp.description,
23
+ iconUrl: snapshotApp.iconUrl,
24
+ localizations: snapshotApp.localizations,
25
+ minimizeBehavior: snapshotApp.minimizeBehavior,
26
+ name: snapshotApp.name,
27
+ source: snapshotApp.source,
28
+ tags: snapshotApp.tags,
29
+ updateAvailable: snapshotApp.updateAvailable
30
+ };
31
+ }
32
+ function areWorkspaceAppCenterAppsEqual(left, right) {
33
+ if (left.length !== right.length) {
34
+ return false;
35
+ }
36
+ return left.every((leftApp, index) => {
37
+ const rightApp = right[index];
38
+ return rightApp !== void 0 && leftApp.appId === rightApp.appId && leftApp.availableIconUrl === rightApp.availableIconUrl && leftApp.availableVersion === rightApp.availableVersion && leftApp.createdAtUnixMs === rightApp.createdAtUnixMs && leftApp.description === rightApp.description && leftApp.enabled === rightApp.enabled && leftApp.exportable === rightApp.exportable && leftApp.iconUrl === rightApp.iconUrl && leftApp.installed === rightApp.installed && areWorkspaceAppCenterLocalizationsEqual(
39
+ leftApp.localizations ?? [],
40
+ rightApp.localizations ?? []
41
+ ) && leftApp.minimizeBehavior === rightApp.minimizeBehavior && leftApp.name === rightApp.name && leftApp.runtimeStatus === rightApp.runtimeStatus && leftApp.source === rightApp.source && leftApp.stateRevision === rightApp.stateRevision && areStringArraysEqual(leftApp.tags ?? [], rightApp.tags ?? []) && (leftApp.updateAvailable ?? false) === (rightApp.updateAvailable ?? false) && leftApp.url === rightApp.url && leftApp.version === rightApp.version;
42
+ });
43
+ }
44
+ function normalizeWorkspaceAppCenterViewState(value) {
45
+ return {
46
+ activeAppTab: value?.activeAppTab === "my" ? "my" : "recommended"
47
+ };
48
+ }
49
+ function areWorkspaceAppCenterViewStatesEqual(left, right) {
50
+ return left.activeAppTab === right.activeAppTab;
51
+ }
52
+ function areWorkspaceAppCenterLocalizationsEqual(left, right) {
53
+ if (left.length !== right.length) {
54
+ return false;
55
+ }
56
+ return left.every((leftLocalization, index) => {
57
+ const rightLocalization = right[index];
58
+ return rightLocalization !== void 0 && leftLocalization.locale === rightLocalization.locale && leftLocalization.name === rightLocalization.name && leftLocalization.description === rightLocalization.description && areStringArraysEqual(leftLocalization.tags, rightLocalization.tags);
59
+ });
60
+ }
61
+ function areStringArraysEqual(left, right) {
62
+ if (left.length !== right.length) {
63
+ return false;
64
+ }
65
+ return left.every((value, index) => value === right[index]);
66
+ }
67
+ function appRuntimeKey(workspaceId, appId) {
68
+ return `${workspaceId}\0${appId}`;
69
+ }
70
+ function factoryJobKey(workspaceId, jobId) {
71
+ return `${workspaceId}\0${jobId}`;
72
+ }
73
+ function removedOrUninstalledAppIds(previousApps, nextApps) {
74
+ const nextAppsById = new Map(nextApps.map((app) => [app.appId, app]));
75
+ const appIds = [];
76
+ for (const previousApp of previousApps) {
77
+ if (!previousApp.installed) {
78
+ continue;
79
+ }
80
+ const nextApp = nextAppsById.get(previousApp.appId);
81
+ if (!nextApp?.installed) {
82
+ appIds.push(previousApp.appId);
83
+ }
84
+ }
85
+ return appIds;
86
+ }
87
+ function sortWorkspaceAppFactoryJobs(jobs) {
88
+ return [...jobs].sort(
89
+ (left, right) => right.updatedAtUnixMs - left.updatedAtUnixMs
90
+ );
91
+ }
92
+ function areWorkspaceAppFactoryJobsEqual(left, right) {
93
+ if (left.length !== right.length) {
94
+ return false;
95
+ }
96
+ return left.every((leftJob, index) => {
97
+ const rightJob = right[index];
98
+ return rightJob !== void 0 && leftJob.agentSessionId === rightJob.agentSessionId && leftJob.appId === rightJob.appId && leftJob.createdAtUnixMs === rightJob.createdAtUnixMs && leftJob.description === rightJob.description && leftJob.displayName === rightJob.displayName && leftJob.failureReason === rightJob.failureReason && leftJob.jobId === rightJob.jobId && leftJob.model === rightJob.model && leftJob.prompt === rightJob.prompt && leftJob.provider === rightJob.provider && leftJob.publishedVersion === rightJob.publishedVersion && leftJob.reasoningEffort === rightJob.reasoningEffort && leftJob.status === rightJob.status && leftJob.updatedAtUnixMs === rightJob.updatedAtUnixMs && leftJob.workspaceId === rightJob.workspaceId;
99
+ });
100
+ }
101
+ function noop() {
102
+ }
103
+
104
+ // src/core/appCenterControllerTypes.ts
105
+ var defaultCatalogLoadingRefreshDelayMs = 750;
106
+ var defaultAppOpenLaunchWaitTimeoutMs = 35e3;
107
+ var defaultInstallRefreshDelayMs = 750;
108
+ function createWorkspaceAppCenterStoreState() {
109
+ return {
110
+ apps: [],
111
+ catalogLastError: null,
112
+ catalogStatus: "disabled",
113
+ catalogUpdatedAtUnixMs: null,
114
+ error: null,
115
+ factoryJobs: [],
116
+ loadStatus: "idle",
117
+ openingFolderAppId: null,
118
+ revision: 0,
119
+ viewStateByWorkspaceId: {},
120
+ workspaceId: null
121
+ };
122
+ }
123
+
124
+ // src/core/appCenterControllerBase.ts
125
+ var WorkspaceAppCenterControllerBase = class {
126
+ store;
127
+ dependencies;
128
+ listeners = /* @__PURE__ */ new Set();
129
+ catalogRefreshTimer = null;
130
+ installRefreshTimers = /* @__PURE__ */ new Map();
131
+ pendingFactoryPublishKeys = /* @__PURE__ */ new Set();
132
+ pendingInstallKeys = /* @__PURE__ */ new Set();
133
+ pendingInstallReportKeys = /* @__PURE__ */ new Set();
134
+ appLoadSequence = 0;
135
+ factoryLoadSequence = 0;
136
+ pollingWorkspaceId = null;
137
+ constructor(dependencies) {
138
+ this.dependencies = dependencies;
139
+ this.store = dependencies.store ?? createWorkspaceAppCenterStoreState();
140
+ }
141
+ get readableStore() {
142
+ return this.store;
143
+ }
144
+ consumeError() {
145
+ const error = this.store.error;
146
+ if (error === null) {
147
+ return null;
148
+ }
149
+ this.store.error = null;
150
+ this.bumpRevision();
151
+ return error;
152
+ }
153
+ subscribe(listener) {
154
+ this.listeners.add(listener);
155
+ return () => {
156
+ this.listeners.delete(listener);
157
+ };
158
+ }
159
+ beginWorkspacePolling(workspaceId) {
160
+ const normalizedWorkspaceId = workspaceId.trim();
161
+ if (!normalizedWorkspaceId) {
162
+ return;
163
+ }
164
+ if (this.pollingWorkspaceId === normalizedWorkspaceId) {
165
+ return;
166
+ }
167
+ this.clearCatalogRefreshTimer();
168
+ this.clearInstallRefreshTimers();
169
+ this.pollingWorkspaceId = normalizedWorkspaceId;
170
+ }
171
+ endWorkspacePolling(workspaceId) {
172
+ if (this.pollingWorkspaceId !== workspaceId.trim()) {
173
+ return;
174
+ }
175
+ this.pollingWorkspaceId = null;
176
+ this.clearCatalogRefreshTimer();
177
+ this.clearInstallRefreshTimers();
178
+ }
179
+ getViewState(workspaceId, restoredState) {
180
+ const normalizedWorkspaceId = workspaceId.trim();
181
+ if (!normalizedWorkspaceId) {
182
+ return normalizeWorkspaceAppCenterViewState(restoredState);
183
+ }
184
+ const existing = this.store.viewStateByWorkspaceId[normalizedWorkspaceId];
185
+ if (existing) {
186
+ return existing;
187
+ }
188
+ const nextState = normalizeWorkspaceAppCenterViewState(restoredState);
189
+ this.store.viewStateByWorkspaceId = {
190
+ ...this.store.viewStateByWorkspaceId,
191
+ [normalizedWorkspaceId]: nextState
192
+ };
193
+ return nextState;
194
+ }
195
+ setViewState(input) {
196
+ const normalizedWorkspaceId = input.workspaceId.trim();
197
+ if (!normalizedWorkspaceId) {
198
+ return;
199
+ }
200
+ const previous = this.getViewState(normalizedWorkspaceId);
201
+ const nextState = normalizeWorkspaceAppCenterViewState({
202
+ ...previous,
203
+ ...input.state
204
+ });
205
+ if (areWorkspaceAppCenterViewStatesEqual(previous, nextState)) {
206
+ return;
207
+ }
208
+ this.store.viewStateByWorkspaceId = {
209
+ ...this.store.viewStateByWorkspaceId,
210
+ [normalizedWorkspaceId]: nextState
211
+ };
212
+ this.bumpRevision();
213
+ }
214
+ setOperationError(error, details) {
215
+ const message = this.dependencies.formatError(error, details);
216
+ this.recordOperationFailure(error, message, details);
217
+ this.store.error = message;
218
+ this.bumpRevision();
219
+ }
220
+ setUnavailableError(error, details) {
221
+ const message = this.dependencies.formatError(error, details);
222
+ this.recordOperationFailure(error, message, details);
223
+ this.store.error = message;
224
+ this.store.loadStatus = "unavailable";
225
+ this.bumpRevision();
226
+ }
227
+ clearCatalogRefreshTimer() {
228
+ if (!this.catalogRefreshTimer) {
229
+ return;
230
+ }
231
+ clearTimeout(this.catalogRefreshTimer);
232
+ this.catalogRefreshTimer = null;
233
+ }
234
+ clearInstallRefreshTimer(workspaceId, appId) {
235
+ const key = appRuntimeKey(workspaceId, appId);
236
+ const timer = this.installRefreshTimers.get(key);
237
+ if (!timer) {
238
+ return;
239
+ }
240
+ clearTimeout(timer);
241
+ this.installRefreshTimers.delete(key);
242
+ }
243
+ clearInstallRefreshTimers() {
244
+ for (const timer of this.installRefreshTimers.values()) {
245
+ clearTimeout(timer);
246
+ }
247
+ this.installRefreshTimers.clear();
248
+ this.pendingInstallKeys.clear();
249
+ this.pendingInstallReportKeys.clear();
250
+ }
251
+ recordOperationFailure(error, toastMessage, details) {
252
+ this.dependencies.hooks?.onOperationFailure?.({
253
+ details,
254
+ error,
255
+ toastMessage
256
+ });
257
+ }
258
+ bumpRevision() {
259
+ this.store.revision += 1;
260
+ for (const listener of this.listeners) {
261
+ listener();
262
+ }
263
+ }
264
+ closeWorkspaceAppViews(workspaceId, appIds) {
265
+ if (appIds.length === 0) {
266
+ return;
267
+ }
268
+ this.dependencies.hooks?.onCloseWorkspaceAppViews?.({
269
+ appIds,
270
+ workspaceId
271
+ });
272
+ }
273
+ getErrorReason(error) {
274
+ return this.dependencies.getErrorReason?.(error) ?? null;
275
+ }
276
+ };
277
+
278
+ // src/core/appCenterControllerState.ts
279
+ var WorkspaceAppCenterControllerState = class extends WorkspaceAppCenterControllerBase {
280
+ applySnapshot(workspaceId, snapshot) {
281
+ const nextApps = sortWorkspaceAppCenterApps(
282
+ this.mergeSnapshotAppsByStateRevision(
283
+ workspaceId,
284
+ this.withPendingInstallState(workspaceId, snapshot.apps)
285
+ )
286
+ );
287
+ for (const app of nextApps) {
288
+ this.settlePendingInstallReport({
289
+ app,
290
+ failureReason: app.failureReason ?? app.lastError ?? null,
291
+ workspaceId
292
+ });
293
+ }
294
+ const appIdsToClose = this.store.workspaceId === workspaceId ? removedOrUninstalledAppIds(this.store.apps, nextApps) : [];
295
+ this.scheduleCatalogLoadingRefresh(workspaceId, snapshot);
296
+ const changed = this.store.workspaceId !== workspaceId || this.store.catalogLastError !== (snapshot.catalogLastError ?? null) || this.store.catalogStatus !== snapshot.catalogStatus || this.store.catalogUpdatedAtUnixMs !== (snapshot.catalogUpdatedAtUnixMs ?? null) || this.store.error !== null || this.store.loadStatus !== "ready" || !areWorkspaceAppCenterAppsEqual(this.store.apps, nextApps);
297
+ if (!changed) {
298
+ return;
299
+ }
300
+ this.store.apps = nextApps;
301
+ this.store.catalogLastError = snapshot.catalogLastError ?? null;
302
+ this.store.catalogStatus = snapshot.catalogStatus;
303
+ this.store.catalogUpdatedAtUnixMs = snapshot.catalogUpdatedAtUnixMs ?? null;
304
+ this.store.error = null;
305
+ this.store.loadStatus = "ready";
306
+ this.store.workspaceId = workspaceId;
307
+ this.bumpRevision();
308
+ this.closeWorkspaceAppViews(workspaceId, appIdsToClose);
309
+ }
310
+ applyFactorySnapshot(workspaceId, snapshot) {
311
+ if (this.store.workspaceId !== workspaceId) {
312
+ this.store.workspaceId = workspaceId;
313
+ }
314
+ const nextJobs = sortWorkspaceAppFactoryJobs(snapshot.jobs);
315
+ if (areWorkspaceAppFactoryJobsEqual(this.store.factoryJobs, nextJobs)) {
316
+ return;
317
+ }
318
+ const previousJobs = this.store.factoryJobs;
319
+ this.store.factoryJobs = nextJobs;
320
+ this.dependencies.hooks?.onFactorySnapshotApplied?.({
321
+ nextJobs,
322
+ previousJobs,
323
+ workspaceId
324
+ });
325
+ this.bumpRevision();
326
+ }
327
+ applyAppUpdate(input) {
328
+ if (this.store.workspaceId !== input.workspaceId) {
329
+ return;
330
+ }
331
+ const currentApp = this.store.apps.find(
332
+ (candidate) => candidate.appId === input.app.appId
333
+ );
334
+ if (!currentApp) {
335
+ return;
336
+ }
337
+ const acceptedRuntimeTransition = input.app.stateRevision > currentApp.stateRevision;
338
+ if (acceptedRuntimeTransition && input.app.runtimeStatus === "failed" && currentApp.runtimeStatus !== "failed") {
339
+ this.dependencies.hooks?.onAppRuntimeFailed?.({
340
+ app: input.app,
341
+ failureReason: input.failureReason ?? null
342
+ });
343
+ }
344
+ if (acceptedRuntimeTransition && currentApp.runtimeStatus === "running" && input.app.runtimeStatus !== "running") {
345
+ this.dependencies.hooks?.onAppStopped?.({
346
+ app: currentApp,
347
+ runDurationMs: resolveAppRunDurationMs(
348
+ input.startedAtUnixMs,
349
+ this.dependencies.now?.() ?? Date.now()
350
+ )
351
+ });
352
+ }
353
+ this.applyAppSnapshot(input.workspaceId, input.app, {
354
+ installFailureReason: input.failureReason ?? null
355
+ });
356
+ }
357
+ applyAppSnapshot(workspaceId, nextApp, options = {}) {
358
+ if (this.store.workspaceId !== workspaceId) {
359
+ return;
360
+ }
361
+ const currentApp = this.store.apps.find(
362
+ (candidate) => candidate.appId === nextApp.appId
363
+ );
364
+ if (currentApp && nextApp.stateRevision <= currentApp.stateRevision) {
365
+ this.settlePendingInstallReport({
366
+ app: currentApp,
367
+ failureReason: options.installFailureReason ?? currentApp.failureReason ?? currentApp.lastError ?? null,
368
+ workspaceId
369
+ });
370
+ return;
371
+ }
372
+ const appIdsToClose = currentApp?.installed === true && !nextApp.installed ? [nextApp.appId] : [];
373
+ const nextApps = sortWorkspaceAppCenterApps([
374
+ ...this.store.apps.filter(
375
+ (candidate) => candidate.appId !== nextApp.appId
376
+ ),
377
+ nextApp
378
+ ]);
379
+ if (areWorkspaceAppCenterAppsEqual(this.store.apps, nextApps)) {
380
+ return;
381
+ }
382
+ this.store.apps = nextApps;
383
+ this.store.error = null;
384
+ this.store.loadStatus = "ready";
385
+ this.bumpRevision();
386
+ this.settlePendingInstallReport({
387
+ app: nextApp,
388
+ failureReason: options.installFailureReason ?? nextApp.failureReason ?? nextApp.lastError ?? null,
389
+ workspaceId
390
+ });
391
+ this.closeWorkspaceAppViews(workspaceId, appIdsToClose);
392
+ }
393
+ applyFactoryJobUpdate(workspaceId, job) {
394
+ if (this.store.workspaceId !== workspaceId) {
395
+ return;
396
+ }
397
+ const currentJob = this.store.factoryJobs.find(
398
+ (candidate) => candidate.jobId === job.jobId
399
+ );
400
+ if (currentJob && job.updatedAtUnixMs < currentJob.updatedAtUnixMs) {
401
+ return;
402
+ }
403
+ const nextJobs = sortWorkspaceAppFactoryJobs([
404
+ ...this.store.factoryJobs.filter(
405
+ (candidate) => candidate.jobId !== job.jobId
406
+ ),
407
+ job
408
+ ]);
409
+ if (areWorkspaceAppFactoryJobsEqual(this.store.factoryJobs, nextJobs)) {
410
+ return;
411
+ }
412
+ this.store.factoryJobs = nextJobs;
413
+ this.bumpRevision();
414
+ }
415
+ scheduleInstallRefresh(workspaceId, appId) {
416
+ const key = appRuntimeKey(workspaceId, appId);
417
+ if (this.installRefreshTimers.has(key)) {
418
+ return;
419
+ }
420
+ const timer = setTimeout(() => {
421
+ this.installRefreshTimers.delete(key);
422
+ if (!this.pendingInstallKeys.has(key)) {
423
+ return;
424
+ }
425
+ void this.refreshInstallState(workspaceId, appId);
426
+ }, this.dependencies.installRefreshDelayMs ?? defaultInstallRefreshDelayMs);
427
+ this.installRefreshTimers.set(key, timer);
428
+ }
429
+ markAppStarting(appId) {
430
+ this.store.apps = this.store.apps.map(
431
+ (app) => app.appId === appId ? {
432
+ ...app,
433
+ enabled: true,
434
+ installed: true,
435
+ runtimeStatus: "preparing"
436
+ } : app
437
+ );
438
+ this.bumpRevision();
439
+ }
440
+ markLaunchWaitTimedOut(input) {
441
+ if (this.store.workspaceId !== input.workspaceId) {
442
+ return;
443
+ }
444
+ let changed = false;
445
+ this.store.apps = this.store.apps.map((app) => {
446
+ if (app.appId !== input.appId || !app.installed || app.runtimeStatus !== "preparing" && app.runtimeStatus !== "starting") {
447
+ return app;
448
+ }
449
+ changed = true;
450
+ return {
451
+ ...app,
452
+ runtimeStatus: "failed"
453
+ };
454
+ });
455
+ if (changed) {
456
+ this.bumpRevision();
457
+ }
458
+ }
459
+ resolveLaunchableApp(input) {
460
+ if (this.store.workspaceId !== input.workspaceId) {
461
+ return null;
462
+ }
463
+ const app = this.store.apps.find(
464
+ (candidate) => candidate.appId === input.appId
465
+ );
466
+ return app?.installed && app.runtimeStatus === "running" && app.url ? app : null;
467
+ }
468
+ markAppInstalling(appId, options = {}) {
469
+ this.store.apps = this.store.apps.map(
470
+ (app) => app.appId === appId ? {
471
+ ...app,
472
+ availableVersion: null,
473
+ enabled: true,
474
+ installed: options.preserveInstalled === true ? app.installed : false,
475
+ runtimeStatus: "installing",
476
+ updateAvailable: false
477
+ } : app
478
+ );
479
+ this.bumpRevision();
480
+ }
481
+ markEnabledAppsStarting() {
482
+ let changed = false;
483
+ this.store.apps = this.store.apps.map((app) => {
484
+ if (!app.enabled || !app.installed || app.runtimeStatus === "running" || app.runtimeStatus === "preparing" || app.runtimeStatus === "starting") {
485
+ return app;
486
+ }
487
+ changed = true;
488
+ return {
489
+ ...app,
490
+ runtimeStatus: "preparing"
491
+ };
492
+ });
493
+ if (changed) {
494
+ this.bumpRevision();
495
+ }
496
+ }
497
+ isPendingInstallSettled(installKey, app) {
498
+ if (app.runtimeStatus === "failed") {
499
+ return true;
500
+ }
501
+ if (this.pendingInstallReportKeys.has(installKey)) {
502
+ return app.installed;
503
+ }
504
+ return app.installed && !app.updateAvailable && !app.availableVersion;
505
+ }
506
+ mergeSnapshotAppsByStateRevision(workspaceId, snapshotApps) {
507
+ if (this.store.workspaceId !== workspaceId) {
508
+ return [...snapshotApps];
509
+ }
510
+ const currentAppsById = new Map(
511
+ this.store.apps.map((app) => [app.appId, app])
512
+ );
513
+ return snapshotApps.map((snapshotApp) => {
514
+ const currentApp = currentAppsById.get(snapshotApp.appId);
515
+ if (!currentApp || currentApp.stateRevision < snapshotApp.stateRevision) {
516
+ return snapshotApp;
517
+ }
518
+ const installKey = appRuntimeKey(workspaceId, snapshotApp.appId);
519
+ if (this.pendingInstallKeys.has(installKey)) {
520
+ const pendingSettled = this.isPendingInstallSettled(
521
+ installKey,
522
+ snapshotApp
523
+ );
524
+ if (pendingSettled && currentApp.stateRevision <= snapshotApp.stateRevision) {
525
+ return snapshotApp;
526
+ }
527
+ }
528
+ return mergeWorkspaceAppCatalogFields(currentApp, snapshotApp);
529
+ });
530
+ }
531
+ scheduleCatalogLoadingRefresh(workspaceId, snapshot) {
532
+ if (snapshot.catalogStatus !== "loading") {
533
+ this.clearCatalogRefreshTimer();
534
+ return;
535
+ }
536
+ if (this.pollingWorkspaceId !== workspaceId || this.catalogRefreshTimer !== null) {
537
+ return;
538
+ }
539
+ this.catalogRefreshTimer = setTimeout(() => {
540
+ this.catalogRefreshTimer = null;
541
+ if (this.pollingWorkspaceId !== workspaceId) {
542
+ return;
543
+ }
544
+ void this.refresh(workspaceId);
545
+ }, this.dependencies.catalogLoadingRefreshDelayMs ?? defaultCatalogLoadingRefreshDelayMs);
546
+ }
547
+ withPendingInstallState(workspaceId, apps) {
548
+ return apps.map((app) => {
549
+ const installKey = appRuntimeKey(workspaceId, app.appId);
550
+ if (!this.pendingInstallKeys.has(installKey)) {
551
+ return app;
552
+ }
553
+ if (this.isPendingInstallSettled(installKey, app)) {
554
+ return app;
555
+ }
556
+ return {
557
+ ...app,
558
+ enabled: true,
559
+ installed: this.pendingInstallReportKeys.has(installKey) ? false : app.installed,
560
+ runtimeStatus: "installing"
561
+ };
562
+ });
563
+ }
564
+ async refreshInstallState(workspaceId, appId) {
565
+ try {
566
+ const snapshot = await this.dependencies.gateway.listWorkspaceApps(workspaceId);
567
+ this.applySnapshot(workspaceId, snapshot);
568
+ } catch (error) {
569
+ this.setOperationError(error, {
570
+ appId,
571
+ operation: "workspace_app.refresh_install_state",
572
+ uiAction: "refresh_install_state",
573
+ workspaceId
574
+ });
575
+ }
576
+ if (this.pendingInstallKeys.has(appRuntimeKey(workspaceId, appId))) {
577
+ this.scheduleInstallRefresh(workspaceId, appId);
578
+ }
579
+ }
580
+ settlePendingInstallReport(input) {
581
+ const installKey = appRuntimeKey(input.workspaceId, input.app.appId);
582
+ if (!this.pendingInstallKeys.has(installKey)) {
583
+ return;
584
+ }
585
+ if (!this.isPendingInstallSettled(installKey, input.app)) {
586
+ return;
587
+ }
588
+ this.pendingInstallKeys.delete(installKey);
589
+ this.clearInstallRefreshTimer(input.workspaceId, input.app.appId);
590
+ if (!this.pendingInstallReportKeys.delete(installKey)) {
591
+ return;
592
+ }
593
+ if (input.app.installed) {
594
+ this.dependencies.hooks?.onAppInstalled?.(input.app);
595
+ return;
596
+ }
597
+ this.dependencies.hooks?.onAppInstallFailed?.({
598
+ app: input.app,
599
+ appId: input.app.appId,
600
+ failureReason: input.failureReason ?? input.app.failureReason ?? input.app.lastError ?? null
601
+ });
602
+ }
603
+ };
604
+
605
+ // src/core/appCenterController.ts
606
+ function createWorkspaceAppCenterController(dependencies) {
607
+ return new WorkspaceAppCenterController(dependencies);
608
+ }
609
+ var WorkspaceAppCenterController = class extends WorkspaceAppCenterControllerState {
610
+ async installApp(input) {
611
+ const installKey = appRuntimeKey(input.workspaceId, input.appId);
612
+ if (this.pendingInstallKeys.has(installKey)) {
613
+ return;
614
+ }
615
+ const previousApps = this.store.apps;
616
+ const appBeforeInstall = previousApps.find((app) => app.appId === input.appId) ?? null;
617
+ this.pendingInstallKeys.add(installKey);
618
+ this.pendingInstallReportKeys.add(installKey);
619
+ this.markAppInstalling(input.appId);
620
+ try {
621
+ const snapshot = await this.dependencies.gateway.installWorkspaceApp(
622
+ input.workspaceId,
623
+ input.appId
624
+ );
625
+ this.applySnapshot(input.workspaceId, snapshot);
626
+ if (this.pendingInstallKeys.has(installKey)) {
627
+ this.scheduleInstallRefresh(input.workspaceId, input.appId);
628
+ }
629
+ } catch (error) {
630
+ this.pendingInstallKeys.delete(installKey);
631
+ this.pendingInstallReportKeys.delete(installKey);
632
+ this.clearInstallRefreshTimer(input.workspaceId, input.appId);
633
+ this.store.apps = previousApps;
634
+ this.dependencies.hooks?.onAppInstallFailed?.({
635
+ app: appBeforeInstall,
636
+ appId: input.appId,
637
+ failureReason: this.getErrorReason(error)
638
+ });
639
+ this.setOperationError(error, {
640
+ appId: input.appId,
641
+ operation: "workspace_app.install",
642
+ uiAction: "install_app",
643
+ workspaceId: input.workspaceId
644
+ });
645
+ }
646
+ }
647
+ async prepareAppLaunch(input) {
648
+ const app = this.store.apps.find(
649
+ (candidate) => candidate.appId === input.appId
650
+ );
651
+ if (this.store.workspaceId !== input.workspaceId || !app?.installed) {
652
+ return null;
653
+ }
654
+ const currentLaunchableApp = this.resolveLaunchableApp(input);
655
+ if (currentLaunchableApp) {
656
+ return currentLaunchableApp;
657
+ }
658
+ this.markAppStarting(input.appId);
659
+ try {
660
+ const snapshot = await this.dependencies.gateway.retryWorkspaceApp(
661
+ input.workspaceId,
662
+ input.appId
663
+ );
664
+ this.applySnapshot(input.workspaceId, snapshot);
665
+ } catch (error) {
666
+ this.setOperationError(error, {
667
+ appId: input.appId,
668
+ operation: "workspace_app.prepare_launch",
669
+ uiAction: "open_app",
670
+ workspaceId: input.workspaceId
671
+ });
672
+ return null;
673
+ }
674
+ return await this.waitForLaunchableApp(input);
675
+ }
676
+ async createFactoryJob(input) {
677
+ const previousJobIds = new Set(
678
+ this.store.factoryJobs.map((job) => job.jobId)
679
+ );
680
+ const snapshot = await this.dependencies.gateway.createWorkspaceAppFactoryJob(
681
+ input.workspaceId,
682
+ {
683
+ displayName: input.displayName,
684
+ ...input.model?.trim() ? { model: input.model.trim() } : {},
685
+ ...input.permissionModeId?.trim() ? { permissionModeId: input.permissionModeId.trim() } : {},
686
+ ...input.provider?.trim() ? { provider: input.provider.trim() } : {},
687
+ prompt: input.prompt,
688
+ ...input.reasoningEffort?.trim() ? { reasoningEffort: input.reasoningEffort.trim() } : {}
689
+ }
690
+ );
691
+ this.applyFactorySnapshot(input.workspaceId, snapshot);
692
+ this.dependencies.hooks?.onFactoryJobCreated?.(
693
+ snapshot.jobs.find((job) => !previousJobIds.has(job.jobId)) ?? null
694
+ );
695
+ }
696
+ async cancelFactoryJob(input) {
697
+ const snapshot = await this.dependencies.gateway.cancelWorkspaceAppFactoryJob(
698
+ input.workspaceId,
699
+ input.jobId
700
+ );
701
+ this.applyFactorySnapshot(input.workspaceId, snapshot);
702
+ }
703
+ async deleteFactoryJob(input) {
704
+ const snapshot = await this.dependencies.gateway.deleteWorkspaceAppFactoryJob(
705
+ input.workspaceId,
706
+ input.jobId
707
+ );
708
+ this.applyFactorySnapshot(input.workspaceId, snapshot);
709
+ }
710
+ async deleteApp(input) {
711
+ const app = this.store.apps.find(
712
+ (candidate) => candidate.appId === input.appId
713
+ );
714
+ try {
715
+ const snapshot = await this.dependencies.gateway.deleteWorkspaceApp(
716
+ input.workspaceId,
717
+ input.appId
718
+ );
719
+ this.pendingInstallKeys.delete(
720
+ appRuntimeKey(input.workspaceId, input.appId)
721
+ );
722
+ this.pendingInstallReportKeys.delete(
723
+ appRuntimeKey(input.workspaceId, input.appId)
724
+ );
725
+ this.clearInstallRefreshTimer(input.workspaceId, input.appId);
726
+ this.applySnapshot(input.workspaceId, snapshot);
727
+ this.dependencies.hooks?.onAppDeleted?.(app ?? null);
728
+ } catch (error) {
729
+ this.setOperationError(error, {
730
+ appId: input.appId,
731
+ operation: "workspace_app.delete",
732
+ uiAction: "delete_app",
733
+ workspaceId: input.workspaceId
734
+ });
735
+ }
736
+ }
737
+ async retryFactoryValidation(input) {
738
+ const snapshot = await this.dependencies.gateway.retryWorkspaceAppFactoryJobValidation(
739
+ input.workspaceId,
740
+ input.jobId
741
+ );
742
+ this.applyFactorySnapshot(input.workspaceId, snapshot);
743
+ }
744
+ async fixFactoryJob(input) {
745
+ const snapshot = await this.dependencies.gateway.fixWorkspaceAppFactoryJob(
746
+ input.workspaceId,
747
+ input.jobId,
748
+ { prompt: input.prompt }
749
+ );
750
+ this.applyFactorySnapshot(input.workspaceId, snapshot);
751
+ }
752
+ async prepareFactoryJobModification(input) {
753
+ try {
754
+ const snapshot = await this.dependencies.gateway.prepareWorkspaceAppFactoryJobModification(
755
+ input.workspaceId,
756
+ input.jobId
757
+ );
758
+ this.applyFactorySnapshot(input.workspaceId, snapshot);
759
+ return snapshot.jobs.find((candidate) => candidate.jobId === input.jobId) ?? null;
760
+ } catch (error) {
761
+ this.setOperationError(error, {
762
+ jobId: input.jobId,
763
+ operation: "app_factory.prepare_modification",
764
+ uiAction: "prepare_factory_job_modification",
765
+ workspaceId: input.workspaceId
766
+ });
767
+ return null;
768
+ }
769
+ }
770
+ async publishFactoryJob(input) {
771
+ const publishKey = factoryJobKey(input.workspaceId, input.jobId);
772
+ if (this.pendingFactoryPublishKeys.has(publishKey)) {
773
+ return null;
774
+ }
775
+ this.pendingFactoryPublishKeys.add(publishKey);
776
+ let result;
777
+ try {
778
+ result = await this.dependencies.gateway.publishWorkspaceAppFactoryJob(
779
+ input.workspaceId,
780
+ input.jobId
781
+ );
782
+ } catch (error) {
783
+ if (this.pendingFactoryPublishKeys.delete(publishKey)) {
784
+ this.setOperationError(error, {
785
+ jobId: input.jobId,
786
+ operation: "app_factory.publish",
787
+ uiAction: "publish_factory_job",
788
+ workspaceId: input.workspaceId
789
+ });
790
+ }
791
+ return null;
792
+ }
793
+ this.pendingFactoryPublishKeys.delete(publishKey);
794
+ this.applyFactorySnapshot(input.workspaceId, result.factorySnapshot);
795
+ this.applySnapshot(input.workspaceId, result.appSnapshot);
796
+ return result.factorySnapshot.jobs.find(
797
+ (candidate) => candidate.jobId === input.jobId
798
+ ) ?? null;
799
+ }
800
+ async refresh(workspaceId) {
801
+ const normalizedWorkspaceId = workspaceId.trim();
802
+ if (!normalizedWorkspaceId) {
803
+ return;
804
+ }
805
+ const appSequence = ++this.appLoadSequence;
806
+ const factorySequence = ++this.factoryLoadSequence;
807
+ const wasIdle = this.store.loadStatus === "idle";
808
+ this.store.workspaceId = normalizedWorkspaceId;
809
+ this.store.error = null;
810
+ if (wasIdle) {
811
+ this.store.loadStatus = "loading";
812
+ }
813
+ try {
814
+ const [appResult, factoryResult] = await Promise.allSettled([
815
+ this.dependencies.gateway.listWorkspaceApps(normalizedWorkspaceId),
816
+ this.dependencies.gateway.listWorkspaceAppFactoryJobs(
817
+ normalizedWorkspaceId
818
+ )
819
+ ]);
820
+ if (appResult.status === "fulfilled" && appSequence !== this.appLoadSequence) {
821
+ this.dependencies.hooks?.onRefreshDiscard?.({
822
+ currentSequence: this.appLoadSequence,
823
+ itemCount: appResult.value.apps.length,
824
+ operation: "app_center.refresh",
825
+ sequence: appSequence,
826
+ snapshotKind: "apps",
827
+ workspaceId: normalizedWorkspaceId
828
+ });
829
+ }
830
+ if (factoryResult.status === "fulfilled" && factorySequence !== this.factoryLoadSequence) {
831
+ this.dependencies.hooks?.onRefreshDiscard?.({
832
+ currentSequence: this.factoryLoadSequence,
833
+ itemCount: factoryResult.value.jobs.length,
834
+ operation: "app_center.refresh",
835
+ sequence: factorySequence,
836
+ snapshotKind: "factory_jobs",
837
+ workspaceId: normalizedWorkspaceId
838
+ });
839
+ }
840
+ let error = null;
841
+ if (appResult.status === "rejected" && appSequence === this.appLoadSequence) {
842
+ error = appResult.reason;
843
+ } else if (factoryResult.status === "rejected" && factorySequence === this.factoryLoadSequence) {
844
+ error = factoryResult.reason;
845
+ }
846
+ if (error) {
847
+ this.setUnavailableError(error, {
848
+ operation: "app_center.refresh",
849
+ workspaceId: normalizedWorkspaceId
850
+ });
851
+ return;
852
+ }
853
+ if (appResult.status === "fulfilled" && appSequence === this.appLoadSequence) {
854
+ this.applySnapshot(normalizedWorkspaceId, appResult.value);
855
+ }
856
+ if (factoryResult.status === "fulfilled" && factorySequence === this.factoryLoadSequence) {
857
+ this.applyFactorySnapshot(normalizedWorkspaceId, factoryResult.value);
858
+ }
859
+ } catch (error) {
860
+ this.setUnavailableError(error, {
861
+ operation: "app_center.refresh",
862
+ workspaceId: normalizedWorkspaceId
863
+ });
864
+ }
865
+ }
866
+ async refreshCatalog(workspaceId) {
867
+ const normalizedWorkspaceId = workspaceId.trim();
868
+ if (!normalizedWorkspaceId) {
869
+ return;
870
+ }
871
+ const sequence = ++this.appLoadSequence;
872
+ this.store.workspaceId = normalizedWorkspaceId;
873
+ this.store.error = null;
874
+ try {
875
+ const snapshot = await this.dependencies.gateway.refreshWorkspaceAppCatalog(
876
+ normalizedWorkspaceId
877
+ );
878
+ if (sequence !== this.appLoadSequence) {
879
+ this.dependencies.hooks?.onRefreshDiscard?.({
880
+ currentSequence: this.appLoadSequence,
881
+ itemCount: snapshot.apps.length,
882
+ operation: "app_center.refresh_catalog",
883
+ sequence,
884
+ snapshotKind: "catalog_apps",
885
+ workspaceId: normalizedWorkspaceId
886
+ });
887
+ return;
888
+ }
889
+ this.applySnapshot(normalizedWorkspaceId, snapshot);
890
+ this.dependencies.hooks?.onCatalogRefreshed?.({
891
+ appCount: snapshot.apps.length,
892
+ errorReason: null,
893
+ success: true
894
+ });
895
+ } catch (error) {
896
+ if (sequence !== this.appLoadSequence) {
897
+ this.dependencies.hooks?.onRefreshDiscard?.({
898
+ currentSequence: this.appLoadSequence,
899
+ operation: "app_center.refresh_catalog",
900
+ sequence,
901
+ snapshotKind: "catalog_apps",
902
+ workspaceId: normalizedWorkspaceId
903
+ });
904
+ return;
905
+ }
906
+ this.setUnavailableError(error, {
907
+ operation: "app_center.refresh_catalog",
908
+ workspaceId: normalizedWorkspaceId
909
+ });
910
+ this.dependencies.hooks?.onCatalogRefreshed?.({
911
+ appCount: null,
912
+ errorReason: this.getErrorReason(error),
913
+ success: false
914
+ });
915
+ }
916
+ }
917
+ async startEnabledApps(workspaceId) {
918
+ this.markEnabledAppsStarting();
919
+ try {
920
+ const snapshot = await this.dependencies.gateway.startEnabledWorkspaceApps(workspaceId);
921
+ this.applySnapshot(workspaceId, snapshot);
922
+ } catch (error) {
923
+ this.setUnavailableError(error, {
924
+ operation: "workspace_app.start_enabled",
925
+ workspaceId
926
+ });
927
+ }
928
+ }
929
+ async uninstallApp(input) {
930
+ const app = this.store.apps.find(
931
+ (candidate) => candidate.appId === input.appId
932
+ );
933
+ const snapshot = await this.dependencies.gateway.uninstallWorkspaceApp(
934
+ input.workspaceId,
935
+ input.appId
936
+ );
937
+ this.applySnapshot(input.workspaceId, snapshot);
938
+ this.dependencies.hooks?.onAppUninstalled?.(app ?? null);
939
+ }
940
+ async updateApp(input) {
941
+ const installKey = appRuntimeKey(input.workspaceId, input.appId);
942
+ if (this.pendingInstallKeys.has(installKey)) {
943
+ return;
944
+ }
945
+ const app = this.store.apps.find(
946
+ (candidate) => candidate.appId === input.appId
947
+ );
948
+ const previousApps = this.store.apps;
949
+ this.pendingInstallKeys.add(installKey);
950
+ this.markAppInstalling(input.appId, { preserveInstalled: true });
951
+ try {
952
+ const snapshot = await this.dependencies.gateway.installWorkspaceApp(
953
+ input.workspaceId,
954
+ input.appId
955
+ );
956
+ this.applySnapshot(input.workspaceId, snapshot);
957
+ this.dependencies.hooks?.onAppUpdated?.({
958
+ app,
959
+ trigger: input.trigger
960
+ });
961
+ if (this.pendingInstallKeys.has(installKey)) {
962
+ this.scheduleInstallRefresh(input.workspaceId, input.appId);
963
+ }
964
+ } catch (error) {
965
+ this.pendingInstallKeys.delete(installKey);
966
+ this.clearInstallRefreshTimer(input.workspaceId, input.appId);
967
+ this.store.apps = previousApps;
968
+ this.setOperationError(error, {
969
+ appId: input.appId,
970
+ operation: "workspace_app.update",
971
+ uiAction: "update_app",
972
+ workspaceId: input.workspaceId
973
+ });
974
+ }
975
+ }
976
+ async retryApp(input) {
977
+ this.markAppStarting(input.appId);
978
+ const snapshot = await this.dependencies.gateway.retryWorkspaceApp(
979
+ input.workspaceId,
980
+ input.appId
981
+ );
982
+ this.applySnapshot(input.workspaceId, snapshot);
983
+ }
984
+ waitForLaunchableApp(input) {
985
+ const current = this.resolveLaunchableApp(input);
986
+ if (current) {
987
+ return Promise.resolve(current);
988
+ }
989
+ if (this.shouldAbortLaunchWait(input)) {
990
+ return Promise.resolve(null);
991
+ }
992
+ const launchWaitTimeoutMs = this.dependencies.appOpenLaunchWaitTimeoutMs ?? defaultAppOpenLaunchWaitTimeoutMs;
993
+ return new Promise((resolve) => {
994
+ let settled = false;
995
+ let unsubscribe = noop;
996
+ let timeout = null;
997
+ const settle = (app) => {
998
+ if (settled) {
999
+ return;
1000
+ }
1001
+ settled = true;
1002
+ if (timeout) {
1003
+ clearTimeout(timeout);
1004
+ }
1005
+ unsubscribe();
1006
+ resolve(app);
1007
+ };
1008
+ timeout = setTimeout(() => {
1009
+ void this.refreshLaunchWaitState(input).then((app) => {
1010
+ if (!app) {
1011
+ this.markLaunchWaitTimedOut(input);
1012
+ }
1013
+ settle(app);
1014
+ });
1015
+ }, launchWaitTimeoutMs);
1016
+ unsubscribe = this.subscribe(() => {
1017
+ const app = this.store.apps.find(
1018
+ (candidate) => candidate.appId === input.appId
1019
+ );
1020
+ if (this.shouldAbortLaunchWait(input, app)) {
1021
+ settle(null);
1022
+ return;
1023
+ }
1024
+ const launchable = this.resolveLaunchableApp(input);
1025
+ if (launchable) {
1026
+ settle(launchable);
1027
+ }
1028
+ });
1029
+ });
1030
+ }
1031
+ async refreshLaunchWaitState(input) {
1032
+ try {
1033
+ const snapshot = await this.dependencies.gateway.listWorkspaceApps(
1034
+ input.workspaceId
1035
+ );
1036
+ this.applySnapshot(input.workspaceId, snapshot);
1037
+ } catch (error) {
1038
+ this.setOperationError(error, {
1039
+ appId: input.appId,
1040
+ operation: "workspace_app.refresh_launch_wait_state",
1041
+ uiAction: "refresh_launch_wait_state",
1042
+ workspaceId: input.workspaceId
1043
+ });
1044
+ return null;
1045
+ }
1046
+ return this.resolveLaunchableApp(input);
1047
+ }
1048
+ shouldAbortLaunchWait(input, app = this.store.apps.find((candidate) => candidate.appId === input.appId)) {
1049
+ return this.store.workspaceId !== input.workspaceId || !app?.installed || app.runtimeStatus === "failed";
1050
+ }
1051
+ };
1052
+
1053
+ // src/core/appIdentity.ts
1054
+ var workspaceAppIdPattern = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
1055
+ function isWorkspaceAppId(value) {
1056
+ return workspaceAppIdPattern.test(value);
1057
+ }
1058
+ function normalizeWorkspaceAppId(value) {
1059
+ return value.trim().toLowerCase();
1060
+ }
1061
+ function createWorkspaceAppIdentity(input) {
1062
+ return normalizeWorkspaceAppId(input.id);
1063
+ }
1064
+
1065
+ // src/core/manifestValidation.ts
1066
+ var appWindowMinWidth = 280;
1067
+ var appWindowMinHeight = 160;
1068
+ var appWindowMaxWidth = 1600;
1069
+ var appWindowMaxHeight = 1200;
1070
+ function validateWorkspaceAppManifest(value) {
1071
+ const issues = [];
1072
+ if (!isRecord(value)) {
1073
+ return {
1074
+ issues: [
1075
+ {
1076
+ code: "manifest.notObject",
1077
+ message: "Manifest must be an object.",
1078
+ path: "$"
1079
+ }
1080
+ ],
1081
+ valid: false
1082
+ };
1083
+ }
1084
+ if (value.schemaVersion !== workspaceAppManifestSchemaVersion) {
1085
+ issues.push({
1086
+ code: "manifest.schemaVersion",
1087
+ message: `schemaVersion must be ${workspaceAppManifestSchemaVersion}.`,
1088
+ path: "$.schemaVersion"
1089
+ });
1090
+ }
1091
+ const appId = readOptionalString(value.appId);
1092
+ if (!appId || !isWorkspaceAppId(normalizeWorkspaceAppId(appId))) {
1093
+ issues.push({
1094
+ code: "manifest.appId",
1095
+ message: "appId must contain lowercase letters, numbers, dots, hyphens, or underscores.",
1096
+ path: "$.appId"
1097
+ });
1098
+ }
1099
+ const name = readOptionalString(value.name);
1100
+ if (!name) {
1101
+ issues.push({
1102
+ code: "manifest.name",
1103
+ message: "name is required.",
1104
+ path: "$.name"
1105
+ });
1106
+ }
1107
+ const version = readOptionalString(value.version);
1108
+ if (!version) {
1109
+ issues.push({
1110
+ code: "manifest.version",
1111
+ message: "version is required.",
1112
+ path: "$.version"
1113
+ });
1114
+ }
1115
+ const description = readOptionalString(value.description);
1116
+ if (!description) {
1117
+ issues.push({
1118
+ code: "manifest.description",
1119
+ message: "description is required.",
1120
+ path: "$.description"
1121
+ });
1122
+ }
1123
+ const icon = validateIcon(value.icon, issues);
1124
+ const runtime = validateRuntime(value.runtime, issues);
1125
+ const window = validateWindow(value.window, issues);
1126
+ const author = validateAuthor(value.author, issues);
1127
+ const tags = validateTags(value.tags, issues);
1128
+ const localizationInfo = validateLocalizationInfo(
1129
+ value.localizationInfo,
1130
+ issues
1131
+ );
1132
+ if (issues.length > 0 || !appId || !name || !version || !description || !runtime) {
1133
+ return {
1134
+ issues,
1135
+ valid: false
1136
+ };
1137
+ }
1138
+ return {
1139
+ issues,
1140
+ manifest: {
1141
+ schemaVersion: workspaceAppManifestSchemaVersion,
1142
+ appId: normalizeWorkspaceAppId(appId),
1143
+ name,
1144
+ version,
1145
+ description,
1146
+ ...icon ? { icon } : {},
1147
+ runtime,
1148
+ ...window ? { window } : {},
1149
+ ...author ? { author } : {},
1150
+ ...tags ? { tags } : {},
1151
+ ...localizationInfo ? { localizationInfo } : {}
1152
+ },
1153
+ valid: true
1154
+ };
1155
+ }
1156
+ function validateIcon(value, issues) {
1157
+ if (value === void 0) {
1158
+ return void 0;
1159
+ }
1160
+ if (!isRecord(value)) {
1161
+ issues.push({
1162
+ code: "manifest.icon",
1163
+ message: "icon must be an object when provided.",
1164
+ path: "$.icon"
1165
+ });
1166
+ return void 0;
1167
+ }
1168
+ const type = value.type;
1169
+ const src = readOptionalString(value.src);
1170
+ if (type !== "asset" || !src || !isRelativePackagePath(src)) {
1171
+ issues.push({
1172
+ code: "manifest.icon",
1173
+ message: "icon must include type=asset and a relative src.",
1174
+ path: "$.icon"
1175
+ });
1176
+ return void 0;
1177
+ }
1178
+ return {
1179
+ type,
1180
+ src
1181
+ };
1182
+ }
1183
+ function validateRuntime(value, issues) {
1184
+ if (!isRecord(value)) {
1185
+ issues.push({
1186
+ code: "manifest.runtime",
1187
+ message: "runtime is required.",
1188
+ path: "$.runtime"
1189
+ });
1190
+ return void 0;
1191
+ }
1192
+ const bootstrap = readOptionalString(value.bootstrap);
1193
+ const healthcheckPath = readOptionalString(value.healthcheckPath);
1194
+ if (!bootstrap || !isRelativePackagePath(bootstrap)) {
1195
+ issues.push({
1196
+ code: "manifest.runtime",
1197
+ message: "runtime.bootstrap must be a relative package path.",
1198
+ path: "$.runtime.bootstrap"
1199
+ });
1200
+ }
1201
+ if (!healthcheckPath || !healthcheckPath.startsWith("/")) {
1202
+ issues.push({
1203
+ code: "manifest.runtime",
1204
+ message: "runtime.healthcheckPath must start with /.",
1205
+ path: "$.runtime.healthcheckPath"
1206
+ });
1207
+ }
1208
+ if (!bootstrap || !healthcheckPath || !isRelativePackagePath(bootstrap) || !healthcheckPath.startsWith("/")) {
1209
+ return void 0;
1210
+ }
1211
+ return {
1212
+ bootstrap,
1213
+ healthcheckPath
1214
+ };
1215
+ }
1216
+ function validateWindow(value, issues) {
1217
+ if (value === void 0) {
1218
+ return void 0;
1219
+ }
1220
+ if (!isRecord(value)) {
1221
+ issues.push({
1222
+ code: "manifest.window",
1223
+ message: "window must be an object when provided.",
1224
+ path: "$.window"
1225
+ });
1226
+ return void 0;
1227
+ }
1228
+ const minimizeBehavior = value.minimizeBehavior;
1229
+ if (minimizeBehavior !== void 0 && minimizeBehavior !== "keep-mounted" && minimizeBehavior !== "hibernate") {
1230
+ issues.push({
1231
+ code: "manifest.window",
1232
+ message: "window.minimizeBehavior must be keep-mounted or hibernate when provided.",
1233
+ path: "$.window.minimizeBehavior"
1234
+ });
1235
+ return void 0;
1236
+ }
1237
+ const minWidth = validateWindowSize(
1238
+ value.minWidth,
1239
+ "minWidth",
1240
+ appWindowMinWidth,
1241
+ appWindowMaxWidth,
1242
+ issues
1243
+ );
1244
+ const minHeight = validateWindowSize(
1245
+ value.minHeight,
1246
+ "minHeight",
1247
+ appWindowMinHeight,
1248
+ appWindowMaxHeight,
1249
+ issues
1250
+ );
1251
+ if (minWidth === null || minHeight === null) {
1252
+ return void 0;
1253
+ }
1254
+ return {
1255
+ ...minimizeBehavior ? { minimizeBehavior } : {},
1256
+ ...minHeight === void 0 ? {} : { minHeight },
1257
+ ...minWidth === void 0 ? {} : { minWidth }
1258
+ };
1259
+ }
1260
+ function validateWindowSize(value, field, minimum, maximum, issues) {
1261
+ if (value === void 0) {
1262
+ return void 0;
1263
+ }
1264
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) {
1265
+ issues.push({
1266
+ code: "manifest.window",
1267
+ message: `window.${field} must be an integer between ${minimum} and ${maximum} when provided.`,
1268
+ path: `$.window.${field}`
1269
+ });
1270
+ return null;
1271
+ }
1272
+ return value;
1273
+ }
1274
+ function validateAuthor(value, issues) {
1275
+ if (value === void 0) {
1276
+ return void 0;
1277
+ }
1278
+ if (!isRecord(value)) {
1279
+ issues.push({
1280
+ code: "manifest.author",
1281
+ message: "author must be an object when provided.",
1282
+ path: "$.author"
1283
+ });
1284
+ return void 0;
1285
+ }
1286
+ const name = readOptionalString(value.name);
1287
+ const url = readOptionalString(value.url);
1288
+ if (!name || value.url !== void 0 && !url) {
1289
+ issues.push({
1290
+ code: "manifest.author",
1291
+ message: "author must include name and an optional non-empty url.",
1292
+ path: "$.author"
1293
+ });
1294
+ return void 0;
1295
+ }
1296
+ return {
1297
+ name,
1298
+ ...url ? { url } : {}
1299
+ };
1300
+ }
1301
+ function validateTags(value, issues) {
1302
+ if (value === void 0) {
1303
+ return void 0;
1304
+ }
1305
+ if (!Array.isArray(value)) {
1306
+ issues.push({
1307
+ code: "manifest.tags",
1308
+ message: "tags must be an array when provided.",
1309
+ path: "$.tags"
1310
+ });
1311
+ return void 0;
1312
+ }
1313
+ const tags = value.map((tag) => readOptionalString(tag)).filter((tag) => Boolean(tag));
1314
+ if (tags.length !== value.length) {
1315
+ issues.push({
1316
+ code: "manifest.tags",
1317
+ message: "tags must only contain non-empty strings.",
1318
+ path: "$.tags"
1319
+ });
1320
+ }
1321
+ return Array.from(new Set(tags));
1322
+ }
1323
+ function validateLocalizationInfo(value, issues) {
1324
+ if (value === void 0) {
1325
+ return void 0;
1326
+ }
1327
+ if (!isRecord(value)) {
1328
+ issues.push({
1329
+ code: "manifest.localizationInfo",
1330
+ message: "localizationInfo must be an object when provided.",
1331
+ path: "$.localizationInfo"
1332
+ });
1333
+ return void 0;
1334
+ }
1335
+ const defaultLocale = readOptionalString(value.defaultLocale);
1336
+ if (!defaultLocale) {
1337
+ issues.push({
1338
+ code: "manifest.localizationInfo",
1339
+ message: "localizationInfo.defaultLocale is required.",
1340
+ path: "$.localizationInfo.defaultLocale"
1341
+ });
1342
+ }
1343
+ const additionalLocalesValue = value.additionalLocales;
1344
+ if (additionalLocalesValue !== void 0 && !Array.isArray(additionalLocalesValue)) {
1345
+ issues.push({
1346
+ code: "manifest.localizationInfo",
1347
+ message: "localizationInfo.additionalLocales must be an array.",
1348
+ path: "$.localizationInfo.additionalLocales"
1349
+ });
1350
+ return void 0;
1351
+ }
1352
+ const seenLocales = new Set(
1353
+ defaultLocale ? [defaultLocale.toLowerCase()] : []
1354
+ );
1355
+ const additionalLocales = [];
1356
+ for (const [index, entry] of (additionalLocalesValue ?? []).entries()) {
1357
+ if (!isRecord(entry)) {
1358
+ issues.push({
1359
+ code: "manifest.localizationInfo",
1360
+ message: "localizationInfo.additionalLocales entries must be objects.",
1361
+ path: `$.localizationInfo.additionalLocales[${index}]`
1362
+ });
1363
+ continue;
1364
+ }
1365
+ const locale = readOptionalString(entry.locale);
1366
+ const file = readOptionalString(entry.file);
1367
+ if (!locale || !file || !isRelativePackagePath(file)) {
1368
+ issues.push({
1369
+ code: "manifest.localizationInfo",
1370
+ message: "localizationInfo.additionalLocales entries must include locale and a relative file path.",
1371
+ path: `$.localizationInfo.additionalLocales[${index}]`
1372
+ });
1373
+ continue;
1374
+ }
1375
+ const localeKey = locale.toLowerCase();
1376
+ if (seenLocales.has(localeKey)) {
1377
+ issues.push({
1378
+ code: "manifest.localizationInfo",
1379
+ message: "localizationInfo locales must be unique.",
1380
+ path: `$.localizationInfo.additionalLocales[${index}].locale`
1381
+ });
1382
+ continue;
1383
+ }
1384
+ seenLocales.add(localeKey);
1385
+ additionalLocales.push({ locale, file });
1386
+ }
1387
+ if (!defaultLocale || issues.some((issue) => issue.code === "manifest.localizationInfo")) {
1388
+ return void 0;
1389
+ }
1390
+ return {
1391
+ defaultLocale,
1392
+ ...additionalLocales.length > 0 ? { additionalLocales } : {}
1393
+ };
1394
+ }
1395
+ function readOptionalString(value) {
1396
+ if (typeof value !== "string") {
1397
+ return void 0;
1398
+ }
1399
+ const trimmed = value.trim();
1400
+ return trimmed.length > 0 ? trimmed : void 0;
1401
+ }
1402
+ function isRelativePackagePath(value) {
1403
+ return !value.startsWith("/") && !value.split(/[\\/]/u).includes("..");
1404
+ }
1405
+ function isRecord(value) {
1406
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1407
+ }
1408
+
1409
+ // src/core/statusMapping.ts
1410
+ var statusAliases = /* @__PURE__ */ new Map([
1411
+ ["active", "running"],
1412
+ ["created", "idle"],
1413
+ ["crashed", "failed"],
1414
+ ["error", "failed"],
1415
+ ["failed", "failed"],
1416
+ ["idle", "idle"],
1417
+ ["installed", "idle"],
1418
+ ["installing", "installing"],
1419
+ ["downloading_runtime", "preparing"],
1420
+ ["launching", "starting"],
1421
+ ["pending", "starting"],
1422
+ ["preparing", "preparing"],
1423
+ ["ready", "idle"],
1424
+ ["running", "running"],
1425
+ ["started", "running"],
1426
+ ["starting", "starting"],
1427
+ ["stopped", "idle"],
1428
+ ["stopping", "stopping"],
1429
+ ["terminated", "idle"],
1430
+ ["terminating", "stopping"]
1431
+ ]);
1432
+ function mapWorkspaceAppRuntimeStatus(value) {
1433
+ if (typeof value !== "string") {
1434
+ return "idle";
1435
+ }
1436
+ return statusAliases.get(value.trim().toLowerCase()) ?? "idle";
1437
+ }
1438
+ function normalizeWorkspaceAppRuntimeState(state) {
1439
+ return {
1440
+ ...state,
1441
+ status: mapWorkspaceAppRuntimeStatus(state.status)
1442
+ };
1443
+ }
1444
+ function resolveWorkspaceAppStatusPresentation(status) {
1445
+ switch (status) {
1446
+ case "installing":
1447
+ return {
1448
+ labelKey: "status.installing",
1449
+ pulse: true,
1450
+ tone: "blue"
1451
+ };
1452
+ case "preparing":
1453
+ return {
1454
+ labelKey: "status.preparing",
1455
+ pulse: true,
1456
+ tone: "blue"
1457
+ };
1458
+ case "starting":
1459
+ return {
1460
+ labelKey: "status.starting",
1461
+ pulse: true,
1462
+ tone: "blue"
1463
+ };
1464
+ case "running":
1465
+ return {
1466
+ labelKey: "status.running",
1467
+ pulse: false,
1468
+ tone: "green"
1469
+ };
1470
+ case "failed":
1471
+ return {
1472
+ labelKey: "status.failed",
1473
+ pulse: false,
1474
+ tone: "red"
1475
+ };
1476
+ case "stopping":
1477
+ return {
1478
+ labelKey: "status.stopping",
1479
+ pulse: true,
1480
+ tone: "amber"
1481
+ };
1482
+ case "idle":
1483
+ return {
1484
+ labelKey: "actions.openApp",
1485
+ pulse: false,
1486
+ tone: "neutral"
1487
+ };
1488
+ }
1489
+ }
1490
+
1491
+ // src/core/appCenterViewModel.ts
1492
+ function createAppCenterViewModel({
1493
+ apps,
1494
+ factoryJobs = [],
1495
+ locale = null,
1496
+ replaceableIconAppIds = [],
1497
+ runtimeStates = []
1498
+ }) {
1499
+ const runtimeByAppId = new Map(
1500
+ runtimeStates.map((state) => [
1501
+ state.appId,
1502
+ {
1503
+ ...state,
1504
+ status: mapWorkspaceAppRuntimeStatus(state.status)
1505
+ }
1506
+ ])
1507
+ );
1508
+ const appFactoryJobByAppId = createAppFactoryJobByAppId(factoryJobs);
1509
+ const replaceableIconAppIdSet = new Set(
1510
+ replaceableIconAppIds.map((appId) => appId.trim()).filter(Boolean)
1511
+ );
1512
+ const appCards = apps.map((app) => {
1513
+ const runtime = runtimeByAppId.get(app.manifest.appId);
1514
+ const factoryJob = appFactoryJobByAppId.get(app.manifest.appId);
1515
+ const metadata = resolveWorkspaceAppCatalogMetadata({
1516
+ catalog: app.catalog,
1517
+ locale,
1518
+ manifest: app.manifest
1519
+ });
1520
+ const factoryAgentSessionId = factoryJob?.agentSessionId?.trim() || null;
1521
+ const status = runtime?.status ?? "idle";
1522
+ const presentation = resolveWorkspaceAppStatusPresentation(status);
1523
+ const installed = Boolean(app.install);
1524
+ const sourceKind = resolveCatalogSourceKind(app.catalog);
1525
+ const localApp = sourceKind === "local";
1526
+ const comingSoon = isComingSoonApp(metadata.tags) || isComingSoonApp(app.manifest.tags ?? []);
1527
+ const busy = isBusyRuntimeStatus(status);
1528
+ const canUpdate = !comingSoon && !busy && installed && (app.updateAvailable ?? false);
1529
+ const canOpen = !comingSoon && installed && canOpenInstalledApp(status);
1530
+ const canRetry = installed && status === "failed";
1531
+ const primaryAction = resolvePrimaryAction({
1532
+ canOpen,
1533
+ canRetry,
1534
+ canUpdate,
1535
+ comingSoon,
1536
+ installed,
1537
+ status
1538
+ });
1539
+ return {
1540
+ id: app.manifest.appId,
1541
+ name: metadata.name,
1542
+ createdAtUnixMs: app.createdAtUnixMs ?? null,
1543
+ ...metadata.description ? { description: metadata.description } : {},
1544
+ ...app.manifest.version && !comingSoon ? { version: app.manifest.version } : {},
1545
+ ...app.availableVersion && !comingSoon ? { availableVersion: app.availableVersion } : {},
1546
+ ...app.category?.trim() ? { category: app.category.trim() } : {},
1547
+ updateAvailable: app.updateAvailable ?? false,
1548
+ ...app.manifest.icon ? { icon: app.manifest.icon } : {},
1549
+ tags: metadata.tags,
1550
+ installed,
1551
+ status,
1552
+ statusLabelKey: comingSoon ? "status.comingSoon" : resolvePrimaryActionLabelKey(primaryAction, presentation.labelKey),
1553
+ statusTone: presentation.tone,
1554
+ statusPulse: presentation.pulse,
1555
+ primaryAction,
1556
+ sourceKind,
1557
+ canOpen,
1558
+ canExport: localApp,
1559
+ canDelete: localApp,
1560
+ canReplaceIcon: replaceableIconAppIdSet.has(app.manifest.appId),
1561
+ canOpenFolder: installed,
1562
+ canOpenPackageFolder: installed && localApp && Boolean(app.manifest.version?.trim()),
1563
+ canOpenFactorySession: Boolean(factoryAgentSessionId),
1564
+ canPublishFactoryUpdate: installed && factoryJob?.status === "ready" && Boolean(factoryJob.publishedVersion?.trim()),
1565
+ canUninstall: installed,
1566
+ canRetry,
1567
+ canUpdate,
1568
+ ...factoryAgentSessionId ? { factoryAgentSessionId } : {},
1569
+ ...factoryJob?.jobId ? { factoryJobId: factoryJob.jobId } : {},
1570
+ ...factoryJob?.provider ? { factoryProvider: factoryJob.provider } : {},
1571
+ ...runtime?.error?.message ? { errorMessage: runtime.error.message } : {}
1572
+ };
1573
+ }).sort((left, right) => left.name.localeCompare(right.name));
1574
+ return {
1575
+ apps: appCards,
1576
+ factoryJobs: factoryJobs.filter((job) => !isPublishedAppFactoryJob(job)).map(createFactoryJobViewModel),
1577
+ empty: appCards.length === 0,
1578
+ failedCount: appCards.filter((app) => app.status === "failed").length,
1579
+ installedCount: appCards.filter((app) => app.installed).length,
1580
+ runningCount: appCards.filter((app) => app.status === "running").length
1581
+ };
1582
+ }
1583
+ function resolveCatalogSourceKind(catalog) {
1584
+ return catalog?.source?.kind ?? "bundled";
1585
+ }
1586
+ function resolveWorkspaceAppCatalogMetadata(input) {
1587
+ const manifest = input.manifest;
1588
+ const localization = findWorkspaceAppCatalogLocalization(
1589
+ input.catalog,
1590
+ input.locale
1591
+ );
1592
+ const name = localization?.name?.trim() || manifest.name;
1593
+ const description = localization?.description?.trim() || manifest.description || "";
1594
+ const tags = localization?.tags?.map((tag) => tag.trim()).filter((tag) => tag.length > 0) ?? manifest.tags ?? [];
1595
+ return {
1596
+ description,
1597
+ name,
1598
+ tags: Array.from(new Set(tags))
1599
+ };
1600
+ }
1601
+ function findWorkspaceAppCatalogLocalization(catalog, locale) {
1602
+ const localizations = catalog?.localizations ?? [];
1603
+ const normalizedLocale = normalizeLocale(locale);
1604
+ if (!normalizedLocale || localizations.length === 0) {
1605
+ return null;
1606
+ }
1607
+ const exact = localizations.find(
1608
+ (localization) => normalizeLocale(localization.locale) === normalizedLocale
1609
+ );
1610
+ if (exact) {
1611
+ return exact;
1612
+ }
1613
+ const language = normalizedLocale.split("-")[0];
1614
+ if (!language) {
1615
+ return null;
1616
+ }
1617
+ return localizations.find(
1618
+ (localization) => normalizeLocale(localization.locale)?.split("-")[0] === language
1619
+ ) ?? null;
1620
+ }
1621
+ function normalizeLocale(value) {
1622
+ const normalized = value?.trim().replace(/_/gu, "-").toLowerCase() ?? "";
1623
+ return normalized.length > 0 ? normalized : null;
1624
+ }
1625
+ function resolvePrimaryAction(input) {
1626
+ if (input.comingSoon) {
1627
+ return "none";
1628
+ }
1629
+ if (isBusyRuntimeStatus(input.status)) {
1630
+ return "none";
1631
+ }
1632
+ if (!input.installed) {
1633
+ return "install";
1634
+ }
1635
+ if (input.canUpdate) {
1636
+ return "update";
1637
+ }
1638
+ if (input.canRetry) {
1639
+ return "retry";
1640
+ }
1641
+ if (input.canOpen) {
1642
+ return "open";
1643
+ }
1644
+ return "none";
1645
+ }
1646
+ function isComingSoonApp(tags) {
1647
+ return tags.some((tag) => tag.trim().toLowerCase() === "coming-soon");
1648
+ }
1649
+ function canOpenInstalledApp(status) {
1650
+ return status === "idle" || status === "running";
1651
+ }
1652
+ function isBusyRuntimeStatus(status) {
1653
+ return status === "installing" || status === "preparing" || status === "starting" || status === "stopping";
1654
+ }
1655
+ function resolvePrimaryActionLabelKey(primaryAction, fallbackLabelKey) {
1656
+ switch (primaryAction) {
1657
+ case "install":
1658
+ return "actions.installApp";
1659
+ case "open":
1660
+ return "actions.openApp";
1661
+ case "retry":
1662
+ return "actions.retryApp";
1663
+ case "update":
1664
+ return "actions.updateApp";
1665
+ case "none":
1666
+ return fallbackLabelKey;
1667
+ }
1668
+ }
1669
+ function createAppFactoryJobByAppId(factoryJobs) {
1670
+ const result = /* @__PURE__ */ new Map();
1671
+ for (const job of [...factoryJobs].sort(
1672
+ (left, right) => right.updatedAtUnixMs - left.updatedAtUnixMs
1673
+ )) {
1674
+ if (!isPublishedAppFactoryJob(job)) {
1675
+ continue;
1676
+ }
1677
+ const appId = job.appId?.trim();
1678
+ if (!appId || result.has(appId)) {
1679
+ continue;
1680
+ }
1681
+ result.set(appId, job);
1682
+ }
1683
+ return result;
1684
+ }
1685
+ function isPublishedAppFactoryJob(job) {
1686
+ return job.status === "published" || Boolean(job.publishedVersion?.trim());
1687
+ }
1688
+ function createFactoryJobViewModel(job) {
1689
+ const statusLabelKey = `factory.status.${job.status}`;
1690
+ const title = job.displayName.trim();
1691
+ const agentSessionId = job.agentSessionId?.trim() || null;
1692
+ const canFixValidationFailure = job.status === "failed" && job.validationResult != null;
1693
+ return {
1694
+ id: job.jobId,
1695
+ agentSessionId,
1696
+ appId: job.appId,
1697
+ title,
1698
+ prompt: job.prompt,
1699
+ provider: job.provider,
1700
+ status: job.status,
1701
+ statusLabelKey,
1702
+ canCancel: job.status === "queued" || job.status === "generating" || job.status === "preparing" || job.status === "validating",
1703
+ canDelete: job.status === "canceled" || job.status === "failed" || job.status === "published" || job.status === "ready",
1704
+ canFix: canFixValidationFailure,
1705
+ canOpenAgentSession: Boolean(agentSessionId),
1706
+ canPublish: job.status === "ready",
1707
+ canRetryValidation: canFixValidationFailure,
1708
+ failureReason: job.failureReason,
1709
+ updatedAtUnixMs: job.updatedAtUnixMs
1710
+ };
1711
+ }
1712
+ function createWorkspaceAppRecord(input) {
1713
+ const manifest = input.catalog?.manifest;
1714
+ if (!manifest) {
1715
+ throw new Error("catalog manifest is required.");
1716
+ }
1717
+ return {
1718
+ catalog: input.catalog,
1719
+ category: input.category,
1720
+ createdAtUnixMs: input.createdAtUnixMs ?? null,
1721
+ install: input.install ?? null,
1722
+ manifest
1723
+ };
1724
+ }
1725
+
1726
+ export {
1727
+ createWorkspaceAppCenterStoreState,
1728
+ createWorkspaceAppCenterController,
1729
+ WorkspaceAppCenterController,
1730
+ workspaceAppIdPattern,
1731
+ isWorkspaceAppId,
1732
+ normalizeWorkspaceAppId,
1733
+ createWorkspaceAppIdentity,
1734
+ validateWorkspaceAppManifest,
1735
+ mapWorkspaceAppRuntimeStatus,
1736
+ normalizeWorkspaceAppRuntimeState,
1737
+ resolveWorkspaceAppStatusPresentation,
1738
+ createAppCenterViewModel,
1739
+ resolveWorkspaceAppCatalogMetadata,
1740
+ createWorkspaceAppRecord
1741
+ };
1742
+ //# sourceMappingURL=chunk-INI5PAWK.js.map