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

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":[]}
@@ -4,4 +4,4 @@ var workspaceAppManifestSchemaVersion = "tutti.app.manifest.v1";
4
4
  export {
5
5
  workspaceAppManifestSchemaVersion
6
6
  };
7
- //# sourceMappingURL=chunk-Y63TZ5JT.js.map
7
+ //# sourceMappingURL=chunk-AWA3NSBR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contracts/manifest.ts"],"sourcesContent":["export const workspaceAppManifestSchemaVersion = \"tutti.app.manifest.v1\";\n\nexport type WorkspaceAppManifestSchemaVersion =\n typeof workspaceAppManifestSchemaVersion;\n\nexport interface WorkspaceAppManifestIcon {\n readonly type: \"asset\";\n readonly src: string;\n}\n\nexport interface WorkspaceAppManifestRuntime {\n readonly bootstrap: string;\n readonly healthcheckPath: string;\n}\n\nexport interface WorkspaceAppManifestReferences {\n readonly listEndpoint: string;\n readonly searchEndpoint?: string;\n}\n\nexport type WorkspaceAppManifestWindowMinimizeBehavior =\n | \"hibernate\"\n | \"keep-mounted\";\n\nexport interface WorkspaceAppManifestWindow {\n readonly minimizeBehavior?: WorkspaceAppManifestWindowMinimizeBehavior;\n readonly minHeight?: number;\n readonly minWidth?: number;\n}\n\nexport interface WorkspaceAppManifestAuthor {\n readonly name: string;\n readonly url?: string;\n}\n\nexport interface WorkspaceAppManifestLocalizationInfo {\n readonly defaultLocale: string;\n readonly additionalLocales?: readonly WorkspaceAppManifestLocalizationFile[];\n}\n\nexport interface WorkspaceAppManifestLocalizationFile {\n readonly locale: string;\n readonly file: string;\n}\n\nexport interface WorkspaceAppManifest {\n readonly schemaVersion: WorkspaceAppManifestSchemaVersion;\n readonly appId: string;\n readonly name: string;\n readonly version: string;\n readonly description: string;\n readonly icon?: WorkspaceAppManifestIcon;\n readonly runtime: WorkspaceAppManifestRuntime;\n readonly references?: WorkspaceAppManifestReferences;\n readonly window?: WorkspaceAppManifestWindow;\n readonly author?: WorkspaceAppManifestAuthor;\n readonly tags?: readonly string[];\n readonly localizationInfo?: WorkspaceAppManifestLocalizationInfo;\n}\n"],"mappings":";AAAO,IAAM,oCAAoC;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  workspaceAppManifestSchemaVersion
3
- } from "./chunk-Y63TZ5JT.js";
3
+ } from "./chunk-AWA3NSBR.js";
4
4
 
5
5
  // src/core/appCenterControllerHelpers.ts
6
6
  function resolveAppRunDurationMs(startedAtUnixMs, now) {
@@ -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
@@ -1252,7 +1394,7 @@ function validateReferences(value, issues) {
1252
1394
  return void 0;
1253
1395
  }
1254
1396
  const unsupportedKey = Object.keys(value).find(
1255
- (key) => key !== "listEndpoint"
1397
+ (key) => key !== "listEndpoint" && key !== "searchEndpoint"
1256
1398
  );
1257
1399
  if (unsupportedKey) {
1258
1400
  issues.push({
@@ -1271,6 +1413,18 @@ function validateReferences(value, issues) {
1271
1413
  });
1272
1414
  return void 0;
1273
1415
  }
1416
+ if (value.searchEndpoint !== void 0) {
1417
+ const searchEndpoint = readOptionalString(value.searchEndpoint);
1418
+ if (!searchEndpoint || !isRelativeUrlPath(searchEndpoint)) {
1419
+ issues.push({
1420
+ code: "manifest.references",
1421
+ message: "references.searchEndpoint must be a relative URL path without query or fragment.",
1422
+ path: "$.references.searchEndpoint"
1423
+ });
1424
+ return void 0;
1425
+ }
1426
+ return { listEndpoint, searchEndpoint };
1427
+ }
1274
1428
  return { listEndpoint };
1275
1429
  }
1276
1430
  function validateWindow(value, issues) {
@@ -1600,9 +1754,11 @@ function createAppCenterViewModel({
1600
1754
  manifest: app.manifest
1601
1755
  });
1602
1756
  const factoryAgentSessionId = factoryJob?.agentSessionId?.trim() || null;
1603
- const status = runtime?.status ?? "idle";
1757
+ const factoryEditAction = factoryJob && factoryAgentSessionId ? factoryJob.status === "published" ? "prepare_modification" : "open_session" : null;
1758
+ const status = resolveRuntimeStatusForViewModel(runtime);
1604
1759
  const presentation = resolveWorkspaceAppStatusPresentation(status);
1605
1760
  const installed = Boolean(app.install);
1761
+ const installBusy = runtime?.installProgress != null || status === "installing";
1606
1762
  const sourceKind = resolveCatalogSourceKind(app.catalog);
1607
1763
  const localApp = sourceKind === "local";
1608
1764
  const runtimeId = normalizeOptionalString(runtime?.runtimeId);
@@ -1624,6 +1780,7 @@ function createAppCenterViewModel({
1624
1780
  installed,
1625
1781
  status
1626
1782
  });
1783
+ const iconUrl = resolveWorkspaceAppIconUrl(app);
1627
1784
  return {
1628
1785
  id: app.manifest.appId,
1629
1786
  installationId,
@@ -1636,30 +1793,37 @@ function createAppCenterViewModel({
1636
1793
  ...app.availableVersion && !comingSoon ? { availableVersion: app.availableVersion } : {},
1637
1794
  ...app.category?.trim() ? { category: app.category.trim() } : {},
1638
1795
  updateAvailable: app.updateAvailable ?? false,
1639
- ...app.manifest.icon ? { icon: app.manifest.icon } : {},
1796
+ ...iconUrl ? {
1797
+ icon: {
1798
+ type: "asset",
1799
+ src: iconUrl
1800
+ }
1801
+ } : {},
1640
1802
  tags: metadata.tags,
1641
1803
  installed,
1642
1804
  status,
1643
1805
  statusLabelKey: comingSoon ? "status.comingSoon" : resolvePrimaryActionLabelKey(primaryAction, presentation.labelKey),
1644
1806
  statusTone: presentation.tone,
1645
- statusPulse: presentation.pulse,
1807
+ statusPulse: runtime?.installProgress ? false : presentation.pulse,
1646
1808
  primaryAction,
1647
1809
  sourceKind,
1648
1810
  canOpen,
1649
1811
  canExport: localApp,
1650
1812
  canDelete: localApp,
1651
1813
  canReplaceIcon: replaceableIconAppIdSet.has(app.manifest.appId),
1652
- canOpenFolder: installed,
1653
- canOpenPackageFolder: installed && localApp && Boolean(displayVersion),
1814
+ canOpenFolder: installed && !installBusy,
1815
+ canOpenPackageFolder: installed && localApp && Boolean(displayVersion) && !installBusy,
1654
1816
  canOpenFactorySession: Boolean(factoryAgentSessionId),
1655
1817
  canPublishFactoryUpdate: installed && factoryJob?.status === "ready" && Boolean(factoryJob.publishedVersion?.trim()),
1656
- canUninstall: installed,
1818
+ canUninstall: installed && !installBusy,
1657
1819
  canRetry,
1658
1820
  canUpdate,
1659
1821
  ...factoryAgentSessionId ? { factoryAgentSessionId } : {},
1822
+ ...factoryEditAction ? { factoryEditAction } : {},
1660
1823
  ...factoryJob?.jobId ? { factoryJobId: factoryJob.jobId } : {},
1661
1824
  ...factoryJob?.provider ? { factoryProvider: factoryJob.provider } : {},
1662
- ...runtime?.error?.message ? { errorMessage: runtime.error.message } : {}
1825
+ ...runtime?.error?.message ? { errorMessage: runtime.error.message } : {},
1826
+ ...runtime?.installProgress ? { installProgress: runtime.installProgress } : {}
1663
1827
  };
1664
1828
  }).sort((left, right) => left.name.localeCompare(right.name));
1665
1829
  return {
@@ -1677,7 +1841,8 @@ function createRuntimeStateMaps(runtimeStates) {
1677
1841
  for (const state of runtimeStates) {
1678
1842
  const runtime = {
1679
1843
  ...state,
1680
- status: mapWorkspaceAppRuntimeStatus(state.status)
1844
+ status: mapWorkspaceAppRuntimeStatus(state.status),
1845
+ installProgress: state.installProgress ?? null
1681
1846
  };
1682
1847
  const installationId = normalizeOptionalString(runtime.installationId);
1683
1848
  if (installationId) {
@@ -1703,6 +1868,15 @@ function findRuntimeStateForApp(maps, appId, installationId) {
1703
1868
  }
1704
1869
  return maps.fallbackByAppId.get(appId);
1705
1870
  }
1871
+ function resolveRuntimeStatusForViewModel(runtime) {
1872
+ if (!runtime) {
1873
+ return "idle";
1874
+ }
1875
+ if (runtime.installProgress != null && runtime.status === "idle") {
1876
+ return "installing";
1877
+ }
1878
+ return runtime.status;
1879
+ }
1706
1880
  function resolveCatalogSourceKind(catalog) {
1707
1881
  return catalog?.source?.kind ?? "bundled";
1708
1882
  }
@@ -1749,6 +1923,16 @@ function normalizeOptionalString(value) {
1749
1923
  const normalized = value?.trim() ?? "";
1750
1924
  return normalized.length > 0 ? normalized : null;
1751
1925
  }
1926
+ function resolveWorkspaceAppIconUrl(app) {
1927
+ const manifestIconUrl = normalizeOptionalString(app.manifest.icon?.src);
1928
+ if (manifestIconUrl && isResolvedIconUrl(manifestIconUrl)) {
1929
+ return manifestIconUrl;
1930
+ }
1931
+ return normalizeOptionalString(app.availableIconUrl) ?? manifestIconUrl;
1932
+ }
1933
+ function isResolvedIconUrl(value) {
1934
+ return /^[a-z][a-z0-9+.-]*:/iu.test(value) || value.startsWith("/");
1935
+ }
1752
1936
  function resolvePrimaryAction(input) {
1753
1937
  if (input.comingSoon) {
1754
1938
  return "none";
@@ -1869,4 +2053,4 @@ export {
1869
2053
  resolveWorkspaceAppCatalogMetadata,
1870
2054
  createWorkspaceAppRecord
1871
2055
  };
1872
- //# sourceMappingURL=chunk-O3XCZVHE.js.map
2056
+ //# sourceMappingURL=chunk-F76FG674.js.map