@tutti-os/workspace-app-center 0.0.12 → 0.0.13

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.
@@ -13,4 +13,4 @@ var workspaceAppRuntimeStatuses = [
13
13
  export {
14
14
  workspaceAppRuntimeStatuses
15
15
  };
16
- //# sourceMappingURL=chunk-PD2WLRHE.js.map
16
+ //# sourceMappingURL=chunk-7BJAJ7F3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contracts/runtime.ts"],"sourcesContent":["export const workspaceAppRuntimeStatuses = [\n \"idle\",\n \"installing\",\n \"preparing\",\n \"starting\",\n \"running\",\n \"failed\",\n \"stopping\",\n \"unavailable\"\n] as const;\n\nexport type WorkspaceAppRuntimeStatus =\n (typeof workspaceAppRuntimeStatuses)[number];\n\nexport interface WorkspaceAppRuntimeError {\n readonly code?: string;\n readonly message: string;\n}\n\nexport type WorkspaceAppInstallUserPhase =\n | \"downloading\"\n | \"installing\"\n | \"starting\";\n\nexport interface WorkspaceAppInstallProgress {\n readonly userPhase: WorkspaceAppInstallUserPhase;\n readonly overallPercent: number;\n readonly downloadedBytes?: number | null;\n readonly totalBytes?: number | null;\n readonly indeterminate: boolean;\n}\n\nexport interface WorkspaceAppRuntimeState {\n readonly runtimeId?: string;\n readonly installationId?: string;\n readonly appId: string;\n readonly status: WorkspaceAppRuntimeStatus;\n readonly launchUrl?: string | null;\n readonly installProgress?: WorkspaceAppInstallProgress | null;\n readonly error?: WorkspaceAppRuntimeError | null;\n readonly startedAt?: string | null;\n readonly updatedAt?: string | null;\n}\n"],"mappings":";AAAO,IAAM,8BAA8B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":[]}
@@ -30,13 +30,25 @@ function mergeWorkspaceAppCatalogFields(currentApp, snapshotApp) {
30
30
  updateAvailable: snapshotApp.updateAvailable
31
31
  };
32
32
  }
33
+ function areWorkspaceAppInstallProgressEqual(left, right) {
34
+ if (left == null && right == null) {
35
+ return true;
36
+ }
37
+ if (left == null || right == null) {
38
+ return false;
39
+ }
40
+ return left.userPhase === right.userPhase && left.overallPercent === right.overallPercent && left.downloadedBytes === right.downloadedBytes && left.totalBytes === right.totalBytes && left.indeterminate === right.indeterminate;
41
+ }
33
42
  function areWorkspaceAppCenterAppsEqual(left, right) {
34
43
  if (left.length !== right.length) {
35
44
  return false;
36
45
  }
37
46
  return left.every((leftApp, index) => {
38
47
  const rightApp = right[index];
39
- 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 && leftApp.installationId === rightApp.installationId && areWorkspaceAppCenterLocalizationsEqual(
48
+ 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 && areWorkspaceAppInstallProgressEqual(
49
+ leftApp.installProgress,
50
+ rightApp.installProgress
51
+ ) && leftApp.installationId === rightApp.installationId && areWorkspaceAppCenterLocalizationsEqual(
40
52
  leftApp.localizations ?? [],
41
53
  rightApp.localizations ?? []
42
54
  ) && leftApp.minimizeBehavior === rightApp.minimizeBehavior && leftApp.name === rightApp.name && leftApp.references.listSupported === rightApp.references.listSupported && leftApp.runtimeId === rightApp.runtimeId && leftApp.runtimeStatus === rightApp.runtimeStatus && leftApp.source === rightApp.source && leftApp.stateRevision === rightApp.stateRevision && areStringArraysEqual(leftApp.tags ?? [], rightApp.tags ?? []) && (leftApp.updateAvailable ?? false) === (rightApp.updateAvailable ?? false) && leftApp.launchUrl === rightApp.launchUrl && leftApp.version === rightApp.version && leftApp.windowMinHeight === rightApp.windowMinHeight && leftApp.windowMinWidth === rightApp.windowMinWidth;
@@ -276,6 +288,85 @@ var WorkspaceAppCenterControllerBase = class {
276
288
  }
277
289
  };
278
290
 
291
+ // src/core/installProgressMerge.ts
292
+ var installUserPhaseOrder = {
293
+ downloading: 0,
294
+ installing: 1,
295
+ starting: 2
296
+ };
297
+ var defaultDownloadCompletePercent = 70;
298
+ var defaultInstallingPartialPercent = 79;
299
+ var defaultStartingPartialPercent = 96;
300
+ function reconcilePendingInstallProgress(input) {
301
+ const runtimePhase = runtimeStatusToInstallUserPhase(input.runtimeStatus);
302
+ const progress = input.incoming ?? input.previous ?? (runtimePhase == null ? null : createProgressForRuntimePhase(runtimePhase));
303
+ if (progress == null) {
304
+ return null;
305
+ }
306
+ if (runtimePhase == null) {
307
+ return clearDownloadBytesUnlessDownloading(progress);
308
+ }
309
+ if (compareInstallUserPhase(progress.userPhase, runtimePhase) >= 0) {
310
+ return clearDownloadBytesUnlessDownloading(progress);
311
+ }
312
+ return advanceInstallProgressToPhase(progress, runtimePhase);
313
+ }
314
+ function runtimeStatusToInstallUserPhase(runtimeStatus) {
315
+ switch (runtimeStatus) {
316
+ case "preparing":
317
+ return "installing";
318
+ case "starting":
319
+ return "starting";
320
+ default:
321
+ return null;
322
+ }
323
+ }
324
+ function compareInstallUserPhase(left, right) {
325
+ return installUserPhaseOrder[left] - installUserPhaseOrder[right];
326
+ }
327
+ function advanceInstallProgressToPhase(progress, targetPhase) {
328
+ let overallPercent = progress.overallPercent;
329
+ let userPhase = progress.userPhase;
330
+ if (targetPhase === "installing" || targetPhase === "starting") {
331
+ overallPercent = Math.max(overallPercent, defaultDownloadCompletePercent);
332
+ userPhase = "installing";
333
+ }
334
+ if (targetPhase === "starting") {
335
+ overallPercent = Math.max(overallPercent, defaultStartingPartialPercent);
336
+ userPhase = "starting";
337
+ } else if (targetPhase === "installing") {
338
+ overallPercent = Math.max(overallPercent, defaultInstallingPartialPercent);
339
+ }
340
+ return clearDownloadBytesUnlessDownloading({
341
+ ...progress,
342
+ indeterminate: false,
343
+ overallPercent,
344
+ userPhase
345
+ });
346
+ }
347
+ function createProgressForRuntimePhase(userPhase) {
348
+ return advanceInstallProgressToPhase(
349
+ {
350
+ downloadedBytes: null,
351
+ indeterminate: false,
352
+ overallPercent: 0,
353
+ totalBytes: null,
354
+ userPhase: "downloading"
355
+ },
356
+ userPhase
357
+ );
358
+ }
359
+ function clearDownloadBytesUnlessDownloading(progress) {
360
+ if (progress.userPhase === "downloading") {
361
+ return progress;
362
+ }
363
+ return {
364
+ ...progress,
365
+ downloadedBytes: null,
366
+ totalBytes: null
367
+ };
368
+ }
369
+
279
370
  // src/core/appCenterControllerState.ts
280
371
  var WorkspaceAppCenterControllerState = class extends WorkspaceAppCenterControllerBase {
281
372
  applySnapshot(workspaceId, snapshot) {
@@ -371,11 +462,16 @@ var WorkspaceAppCenterControllerState = class extends WorkspaceAppCenterControll
371
462
  return;
372
463
  }
373
464
  const appIdsToClose = currentApp?.installed === true && !nextApp.installed ? [nextApp.appId] : [];
465
+ const mergedApp = this.mergeActiveInstallAppState(
466
+ workspaceId,
467
+ nextApp,
468
+ currentApp
469
+ );
374
470
  const nextApps = sortWorkspaceAppCenterApps([
375
471
  ...this.store.apps.filter(
376
472
  (candidate) => candidate.appId !== nextApp.appId
377
473
  ),
378
- nextApp
474
+ mergedApp
379
475
  ]);
380
476
  if (areWorkspaceAppCenterAppsEqual(this.store.apps, nextApps)) {
381
477
  return;
@@ -385,8 +481,8 @@ var WorkspaceAppCenterControllerState = class extends WorkspaceAppCenterControll
385
481
  this.store.loadStatus = "ready";
386
482
  this.bumpRevision();
387
483
  this.settlePendingInstallReport({
388
- app: nextApp,
389
- failureReason: options.installFailureReason ?? nextApp.failureReason ?? nextApp.lastError ?? null,
484
+ app: mergedApp,
485
+ failureReason: options.installFailureReason ?? mergedApp.failureReason ?? mergedApp.lastError ?? null,
390
486
  workspaceId
391
487
  });
392
488
  this.closeWorkspaceAppViews(workspaceId, appIdsToClose);
@@ -495,15 +591,24 @@ var WorkspaceAppCenterControllerState = class extends WorkspaceAppCenterControll
495
591
  this.bumpRevision();
496
592
  }
497
593
  }
498
- isPendingInstallSettled(installKey, app) {
594
+ isPendingInstallSettled(installKey, app, previousApp) {
595
+ if (this.isPendingInstallAbandoned(installKey, app, previousApp)) {
596
+ return true;
597
+ }
499
598
  if (app.runtimeStatus === "failed") {
500
599
  return true;
501
600
  }
601
+ if (this.isRuntimeInstallBusy(app.runtimeStatus)) {
602
+ return false;
603
+ }
502
604
  if (this.pendingInstallReportKeys.has(installKey)) {
503
605
  return app.installed;
504
606
  }
505
607
  return app.installed && !app.updateAvailable && !app.availableVersion;
506
608
  }
609
+ isRuntimeInstallBusy(runtimeStatus) {
610
+ return runtimeStatus === "installing" || runtimeStatus === "preparing" || runtimeStatus === "starting";
611
+ }
507
612
  mergeSnapshotAppsByStateRevision(workspaceId, snapshotApps) {
508
613
  if (this.store.workspaceId !== workspaceId) {
509
614
  return [...snapshotApps];
@@ -520,7 +625,8 @@ var WorkspaceAppCenterControllerState = class extends WorkspaceAppCenterControll
520
625
  if (this.pendingInstallKeys.has(installKey)) {
521
626
  const pendingSettled = this.isPendingInstallSettled(
522
627
  installKey,
523
- snapshotApp
628
+ snapshotApp,
629
+ currentApp
524
630
  );
525
631
  if (pendingSettled && currentApp.stateRevision <= snapshotApp.stateRevision) {
526
632
  return snapshotApp;
@@ -545,22 +651,50 @@ var WorkspaceAppCenterControllerState = class extends WorkspaceAppCenterControll
545
651
  void this.refresh(workspaceId);
546
652
  }, this.dependencies.catalogLoadingRefreshDelayMs ?? defaultCatalogLoadingRefreshDelayMs);
547
653
  }
654
+ mergeActiveInstallAppState(workspaceId, app, previousApp) {
655
+ const installKey = appRuntimeKey(workspaceId, app.appId);
656
+ if (!this.pendingInstallKeys.has(installKey)) {
657
+ return app;
658
+ }
659
+ if (this.isPendingInstallSettled(installKey, app, previousApp)) {
660
+ return app;
661
+ }
662
+ const incomingRuntimeStatus = app.runtimeStatus;
663
+ const runtimeStatus = this.isRuntimeInstallLifecycleStatus(
664
+ incomingRuntimeStatus
665
+ ) ? incomingRuntimeStatus : "installing";
666
+ return {
667
+ ...app,
668
+ enabled: true,
669
+ installed: this.pendingInstallReportKeys.has(installKey) ? false : app.installed,
670
+ installProgress: reconcilePendingInstallProgress({
671
+ incoming: app.installProgress,
672
+ previous: previousApp?.installProgress ?? null,
673
+ runtimeStatus: incomingRuntimeStatus
674
+ }),
675
+ runtimeStatus
676
+ };
677
+ }
678
+ isRuntimeInstallLifecycleStatus(runtimeStatus) {
679
+ return runtimeStatus === "installing" || runtimeStatus === "preparing" || runtimeStatus === "starting";
680
+ }
681
+ isPendingInstallAbandoned(installKey, app, previousApp) {
682
+ return this.pendingInstallReportKeys.has(installKey) && !app.installed && app.runtimeStatus === "idle" && app.installProgress == null && this.hasObservedInstallActivity(previousApp);
683
+ }
684
+ hasObservedInstallActivity(app) {
685
+ return app?.installProgress != null || app?.runtimeStatus === "preparing" || app?.runtimeStatus === "starting";
686
+ }
548
687
  withPendingInstallState(workspaceId, apps) {
549
- return apps.map((app) => {
550
- const installKey = appRuntimeKey(workspaceId, app.appId);
551
- if (!this.pendingInstallKeys.has(installKey)) {
552
- return app;
553
- }
554
- if (this.isPendingInstallSettled(installKey, app)) {
555
- return app;
556
- }
557
- return {
558
- ...app,
559
- enabled: true,
560
- installed: this.pendingInstallReportKeys.has(installKey) ? false : app.installed,
561
- runtimeStatus: "installing"
562
- };
563
- });
688
+ const previousAppsById = new Map(
689
+ this.store.apps.map((app) => [app.appId, app])
690
+ );
691
+ return apps.map(
692
+ (app) => this.mergeActiveInstallAppState(
693
+ workspaceId,
694
+ app,
695
+ previousAppsById.get(app.appId)
696
+ )
697
+ );
564
698
  }
565
699
  async refreshInstallState(workspaceId, appId) {
566
700
  try {
@@ -595,6 +729,9 @@ var WorkspaceAppCenterControllerState = class extends WorkspaceAppCenterControll
595
729
  this.dependencies.hooks?.onAppInstalled?.(input.app);
596
730
  return;
597
731
  }
732
+ if (input.app.runtimeStatus === "idle" && input.app.installProgress == null && input.failureReason == null && input.app.failureReason == null && input.app.lastError == null) {
733
+ return;
734
+ }
598
735
  this.dependencies.hooks?.onAppInstallFailed?.({
599
736
  app: input.app,
600
737
  appId: input.app.appId,
@@ -954,11 +1091,16 @@ var WorkspaceAppCenterController = class extends WorkspaceAppCenterControllerSta
954
1091
  this.pendingInstallKeys.add(installKey);
955
1092
  this.markAppInstalling(input.appId, { preserveInstalled: true });
956
1093
  try {
1094
+ const restartRunning = app?.installed === true && app.runtimeStatus === "running";
957
1095
  const snapshot = await this.dependencies.gateway.installWorkspaceApp(
958
1096
  input.workspaceId,
959
- input.appId
1097
+ input.appId,
1098
+ restartRunning ? { restartRunning: true } : void 0
960
1099
  );
961
1100
  this.applySnapshot(input.workspaceId, snapshot);
1101
+ if (restartRunning) {
1102
+ this.closeWorkspaceAppViews(input.workspaceId, [input.appId]);
1103
+ }
962
1104
  this.dependencies.hooks?.onAppUpdated?.({
963
1105
  app,
964
1106
  trigger: input.trigger
@@ -1600,9 +1742,11 @@ function createAppCenterViewModel({
1600
1742
  manifest: app.manifest
1601
1743
  });
1602
1744
  const factoryAgentSessionId = factoryJob?.agentSessionId?.trim() || null;
1603
- const status = runtime?.status ?? "idle";
1745
+ const factoryEditAction = factoryJob && factoryAgentSessionId ? factoryJob.status === "published" ? "prepare_modification" : "open_session" : null;
1746
+ const status = resolveRuntimeStatusForViewModel(runtime);
1604
1747
  const presentation = resolveWorkspaceAppStatusPresentation(status);
1605
1748
  const installed = Boolean(app.install);
1749
+ const installBusy = runtime?.installProgress != null || status === "installing";
1606
1750
  const sourceKind = resolveCatalogSourceKind(app.catalog);
1607
1751
  const localApp = sourceKind === "local";
1608
1752
  const runtimeId = normalizeOptionalString(runtime?.runtimeId);
@@ -1624,6 +1768,7 @@ function createAppCenterViewModel({
1624
1768
  installed,
1625
1769
  status
1626
1770
  });
1771
+ const iconUrl = resolveWorkspaceAppIconUrl(app);
1627
1772
  return {
1628
1773
  id: app.manifest.appId,
1629
1774
  installationId,
@@ -1636,30 +1781,37 @@ function createAppCenterViewModel({
1636
1781
  ...app.availableVersion && !comingSoon ? { availableVersion: app.availableVersion } : {},
1637
1782
  ...app.category?.trim() ? { category: app.category.trim() } : {},
1638
1783
  updateAvailable: app.updateAvailable ?? false,
1639
- ...app.manifest.icon ? { icon: app.manifest.icon } : {},
1784
+ ...iconUrl ? {
1785
+ icon: {
1786
+ type: "asset",
1787
+ src: iconUrl
1788
+ }
1789
+ } : {},
1640
1790
  tags: metadata.tags,
1641
1791
  installed,
1642
1792
  status,
1643
1793
  statusLabelKey: comingSoon ? "status.comingSoon" : resolvePrimaryActionLabelKey(primaryAction, presentation.labelKey),
1644
1794
  statusTone: presentation.tone,
1645
- statusPulse: presentation.pulse,
1795
+ statusPulse: runtime?.installProgress ? false : presentation.pulse,
1646
1796
  primaryAction,
1647
1797
  sourceKind,
1648
1798
  canOpen,
1649
1799
  canExport: localApp,
1650
1800
  canDelete: localApp,
1651
1801
  canReplaceIcon: replaceableIconAppIdSet.has(app.manifest.appId),
1652
- canOpenFolder: installed,
1653
- canOpenPackageFolder: installed && localApp && Boolean(displayVersion),
1802
+ canOpenFolder: installed && !installBusy,
1803
+ canOpenPackageFolder: installed && localApp && Boolean(displayVersion) && !installBusy,
1654
1804
  canOpenFactorySession: Boolean(factoryAgentSessionId),
1655
1805
  canPublishFactoryUpdate: installed && factoryJob?.status === "ready" && Boolean(factoryJob.publishedVersion?.trim()),
1656
- canUninstall: installed,
1806
+ canUninstall: installed && !installBusy,
1657
1807
  canRetry,
1658
1808
  canUpdate,
1659
1809
  ...factoryAgentSessionId ? { factoryAgentSessionId } : {},
1810
+ ...factoryEditAction ? { factoryEditAction } : {},
1660
1811
  ...factoryJob?.jobId ? { factoryJobId: factoryJob.jobId } : {},
1661
1812
  ...factoryJob?.provider ? { factoryProvider: factoryJob.provider } : {},
1662
- ...runtime?.error?.message ? { errorMessage: runtime.error.message } : {}
1813
+ ...runtime?.error?.message ? { errorMessage: runtime.error.message } : {},
1814
+ ...runtime?.installProgress ? { installProgress: runtime.installProgress } : {}
1663
1815
  };
1664
1816
  }).sort((left, right) => left.name.localeCompare(right.name));
1665
1817
  return {
@@ -1677,7 +1829,8 @@ function createRuntimeStateMaps(runtimeStates) {
1677
1829
  for (const state of runtimeStates) {
1678
1830
  const runtime = {
1679
1831
  ...state,
1680
- status: mapWorkspaceAppRuntimeStatus(state.status)
1832
+ status: mapWorkspaceAppRuntimeStatus(state.status),
1833
+ installProgress: state.installProgress ?? null
1681
1834
  };
1682
1835
  const installationId = normalizeOptionalString(runtime.installationId);
1683
1836
  if (installationId) {
@@ -1703,6 +1856,15 @@ function findRuntimeStateForApp(maps, appId, installationId) {
1703
1856
  }
1704
1857
  return maps.fallbackByAppId.get(appId);
1705
1858
  }
1859
+ function resolveRuntimeStatusForViewModel(runtime) {
1860
+ if (!runtime) {
1861
+ return "idle";
1862
+ }
1863
+ if (runtime.installProgress != null && runtime.status === "idle") {
1864
+ return "installing";
1865
+ }
1866
+ return runtime.status;
1867
+ }
1706
1868
  function resolveCatalogSourceKind(catalog) {
1707
1869
  return catalog?.source?.kind ?? "bundled";
1708
1870
  }
@@ -1749,6 +1911,16 @@ function normalizeOptionalString(value) {
1749
1911
  const normalized = value?.trim() ?? "";
1750
1912
  return normalized.length > 0 ? normalized : null;
1751
1913
  }
1914
+ function resolveWorkspaceAppIconUrl(app) {
1915
+ const manifestIconUrl = normalizeOptionalString(app.manifest.icon?.src);
1916
+ if (manifestIconUrl && isResolvedIconUrl(manifestIconUrl)) {
1917
+ return manifestIconUrl;
1918
+ }
1919
+ return normalizeOptionalString(app.availableIconUrl) ?? manifestIconUrl;
1920
+ }
1921
+ function isResolvedIconUrl(value) {
1922
+ return /^[a-z][a-z0-9+.-]*:/iu.test(value) || value.startsWith("/");
1923
+ }
1752
1924
  function resolvePrimaryAction(input) {
1753
1925
  if (input.comingSoon) {
1754
1926
  return "none";
@@ -1869,4 +2041,4 @@ export {
1869
2041
  resolveWorkspaceAppCatalogMetadata,
1870
2042
  createWorkspaceAppRecord
1871
2043
  };
1872
- //# sourceMappingURL=chunk-O3XCZVHE.js.map
2044
+ //# sourceMappingURL=chunk-DPJRK63W.js.map