@dsh-workbench/core 0.2.6 → 0.2.7

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.
Files changed (32) hide show
  1. package/README.md +7 -7
  2. package/bin/dsh-workbench +2 -0
  3. package/client.js +252 -36
  4. package/dist/client/client.body.js +252 -36
  5. package/dist/client/index.js +252 -36
  6. package/dist/host/index.js +1054 -737
  7. package/dist/types/app/client.d.ts +21 -15
  8. package/dist/types/app/layouts/rightbar/__tests__/market-order.test.d.ts +1 -0
  9. package/dist/types/app/layouts/rightbar/__tests__/market-running-display.test.d.ts +1 -0
  10. package/dist/types/app/layouts/rightbar/__tests__/market-workbench-act.test.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/market-act.d.ts +2 -1
  12. package/dist/types/app/layouts/rightbar/market-rows.d.ts +3 -0
  13. package/dist/types/app/layouts/rightbar/market-settle-hold.d.ts +13 -0
  14. package/dist/types/app/layouts/rightbar/market-settle.d.ts +22 -0
  15. package/dist/types/app/layouts/rightbar/market-transient-state.d.ts +22 -0
  16. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +26 -1
  17. package/dist/types/modules/panels/__tests__/market-tools-harness.d.ts +3 -0
  18. package/dist/types/modules/panels/model/market-workbench-update.d.ts +19 -0
  19. package/dist/types/modules/panels/model/market.d.ts +4 -0
  20. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  21. package/dist/types/shared/api/config-sync.d.ts +0 -2
  22. package/dist/types/shared/api/host/__tests__/post-channels.test.d.ts +1 -0
  23. package/dist/types/shared/api/host/__tests__/self-update.test.d.ts +1 -0
  24. package/dist/types/shared/api/host/bundle-manager.d.ts +1 -0
  25. package/dist/types/shared/api/host/bundle-routes.d.ts +1 -1
  26. package/dist/types/shared/api/host/config-sync.d.ts +2 -2
  27. package/dist/types/shared/api/host/post-channels.d.ts +18 -0
  28. package/dist/types/shared/api/host/self-update.d.ts +45 -0
  29. package/docs/HANDOFF.md +3 -3
  30. package/docs/OPERATIONS.md +33 -3
  31. package/package.json +4 -4
  32. package/scripts/seed-profile.sh +9 -0
@@ -1384,6 +1384,7 @@ var en = {
1384
1384
  "panels.market.lead": "Installed on this machine, shared by every project.",
1385
1385
  "panels.market.install": "Install",
1386
1386
  "panels.market.update": "Update",
1387
+ "panels.market.swapSlow": "Not swapped yet — maybe a slow or failed download. Press Update again; if it stays like this, restart dsh.",
1387
1388
  "panels.market.uninstall": "Uninstall",
1388
1389
  "panels.market.uninstallLabel": "Uninstall {name}",
1389
1390
  "panels.market.installing": "Installing…",
@@ -1635,7 +1636,7 @@ var en = {
1635
1636
  "panel.conversation": "Conversation",
1636
1637
  "panels.label": "Global panels",
1637
1638
  "configSync.nav": "Config sync",
1638
- "configSync.lead": "Bring over the rows another profile configures explicitly — pick any profile as the source. A restart applies them.",
1639
+ "configSync.lead": "Bring over the rows another profile configures explicitly — pick any profile as the source.",
1639
1640
  "configSync.source": "Source",
1640
1641
  "configSync.sourceBroken": "its config is unreadable",
1641
1642
  "configSync.sourceRows": "{name}: {count} rows are new or different for the workbench",
@@ -1659,7 +1660,7 @@ var en = {
1659
1660
  "configSync.apply": "Sync {count} selected",
1660
1661
  "configSync.applyNone": "Pick some rows first",
1661
1662
  "configSync.applying": "Writing…",
1662
- "configSync.applied": "Written to the workbench config · restart to apply",
1663
+ "configSync.applied": "Written to the workbench config · applied",
1663
1664
  "configSync.appliedIds": "Written this time: {ids}",
1664
1665
  "configSync.skippedIds": "Left alone (already here): {ids}",
1665
1666
  "configSync.nothing": "Nothing to sync",
@@ -1778,6 +1779,7 @@ var zh = {
1778
1779
  "panels.market.lead": "装在这台机器上,所有项目共用。",
1779
1780
  "panels.market.install": "安装",
1780
1781
  "panels.market.update": "更新",
1782
+ "panels.market.swapSlow": "还没换上:可能网慢或没装上。再点一次「更新」;一直这样就把 dsh 重启一遍。",
1781
1783
  "panels.market.uninstall": "卸载",
1782
1784
  "panels.market.uninstallLabel": "卸载 {name}",
1783
1785
  "panels.market.installing": "正在安装…",
@@ -2029,7 +2031,7 @@ var zh = {
2029
2031
  "panel.conversation": "对话",
2030
2032
  "panels.label": "全局面板",
2031
2033
  "configSync.nav": "配置同步",
2032
- "configSync.lead": "把另一个 profile 里显式配过的行搬过来 —— 来源随你选。搬完要重启工作台才生效。",
2034
+ "configSync.lead": "把另一个 profile 里显式配过的行搬过来 —— 来源随你选。",
2033
2035
  "configSync.source": "来源",
2034
2036
  "configSync.sourceBroken": "读不到它的配置",
2035
2037
  "configSync.sourceRows": "{name} 这份里你有 {count} 行可搬",
@@ -2053,7 +2055,7 @@ var zh = {
2053
2055
  "configSync.apply": "同步选中的 {count} 行",
2054
2056
  "configSync.applyNone": "先勾几行",
2055
2057
  "configSync.applying": "正在写入…",
2056
- "configSync.applied": "已写入工作台配置 · 重启工作台后生效",
2058
+ "configSync.applied": "已写入工作台配置 · 已生效",
2057
2059
  "configSync.appliedIds": "这次写进去:{ids}",
2058
2060
  "configSync.skippedIds": "这几行工作台自己已有,没动:{ids}",
2059
2061
  "configSync.nothing": "没有需要同步的内容",
@@ -2883,6 +2885,38 @@ function holdBack(installed, roster, active, previous) {
2883
2885
  const fresh = previous === null ? [] : pending.filter((unit) => !previous.has(unit));
2884
2886
  return pending.filter((unit) => active.has(unit) || fresh.includes(unit));
2885
2887
  }
2888
+ var WORKBENCH_SWAP_MS = 18e4;
2889
+ function swapLanded(until, installed) {
2890
+ return until !== void 0 && installed === until;
2891
+ }
2892
+ function swapUntil(action, application, latest) {
2893
+ if (action !== "update-workbench" || application !== "restart-required") return void 0;
2894
+ return latest;
2895
+ }
2896
+ var READ_BACK_ACTIONS = /* @__PURE__ */ new Set(["update", "update-bundle"]);
2897
+ function awaitsReadBack(action, application, version) {
2898
+ return application === "applied" && version !== void 0 && READ_BACK_ACTIONS.has(action);
2899
+ }
2900
+ function readBackDone(input) {
2901
+ if (input.action === "update") return !input.outdated;
2902
+ if (input.entry === void 0 || input.entry.local === true || input.entry.latest === void 0) return true;
2903
+ if (input.entry.latest === input.entry.installed) return true;
2904
+ if (input.clicked === void 0) return true;
2905
+ return input.entry.installed !== input.clicked;
2906
+ }
2907
+ function settleLanded(input) {
2908
+ if (input.until !== void 0) return swapLanded(input.until, input.installed);
2909
+ if (input.readBack !== true) return true;
2910
+ return readBackDone({
2911
+ action: input.action,
2912
+ entry: input.entry,
2913
+ clicked: input.clicked,
2914
+ outdated: input.outdated
2915
+ });
2916
+ }
2917
+ function holdWindowMs(until) {
2918
+ return until === void 0 ? MODULE_SETTLE_MS : WORKBENCH_SWAP_MS;
2919
+ }
2886
2920
  //#endregion
2887
2921
  //#region src/app/layouts/rightbar/market-installed-mock.ts
2888
2922
  function installedMocks(input) {
@@ -4015,6 +4049,7 @@ function workbenchRow(version) {
4015
4049
  key: "panels.market.workbenchLocal",
4016
4050
  params: {}
4017
4051
  } : void 0;
4052
+ const stale = !local && version.running !== void 0 && version.running !== version.installed;
4018
4053
  return {
4019
4054
  id: WORKBENCH_ROW_ID,
4020
4055
  titleKey: WORKBENCH_TITLE_KEY,
@@ -4024,6 +4059,7 @@ function workbenchRow(version) {
4024
4059
  canUpdate: update !== void 0,
4025
4060
  canUninstall: false,
4026
4061
  ...note === void 0 ? {} : { note },
4062
+ ...stale ? { hint: "panels.market.moduleRestartHint" } : {},
4027
4063
  version: {
4028
4064
  installed: version.installed,
4029
4065
  ...latest === void 0 ? {} : { latest }
@@ -4086,6 +4122,10 @@ function mergeTitles(known, manifests) {
4086
4122
  }
4087
4123
  return next ?? known;
4088
4124
  }
4125
+ function installedIdSet(installed) {
4126
+ if (installed === void 0) return null;
4127
+ return new Set(installed.map((item) => item.unit));
4128
+ }
4089
4129
  function installedRows(modules, manifests, listed, known, defaults = /* @__PURE__ */ new Set(), changed = {}) {
4090
4130
  const titles = new Map(manifests.map((manifest) => [manifest.id, manifest.card?.titleKey]));
4091
4131
  const rows = [];
@@ -4148,6 +4188,9 @@ function isOutdated(capability) {
4148
4188
  if (capability === void 0) return false;
4149
4189
  return capability.status === "missing-commands";
4150
4190
  }
4191
+ function capabilityOutdated(capabilities, unit) {
4192
+ return isOutdated(capabilities?.[unit]);
4193
+ }
4151
4194
  function envNameOf(source) {
4152
4195
  if (source === void 0) return "";
4153
4196
  if (source.kind === "local") return source.env;
@@ -4209,6 +4252,12 @@ function marketHintVisible(manifests, capabilities, modules, workbench) {
4209
4252
  const self = workbenchRow(workbench);
4210
4253
  return marketHasUpdates([...marketRows(manifests, capabilities, marketUpdatesOf(modules)), ...self === void 0 ? [] : [self]], sharedRows(manifests, capabilities));
4211
4254
  }
4255
+ function marketOrder(rows, titleOf) {
4256
+ return [...rows].sort((left, right) => {
4257
+ const byTitle = titleOf(left).localeCompare(titleOf(right), "zh-Hans-CN");
4258
+ return byTitle === 0 ? left.id.localeCompare(right.id) : byTitle;
4259
+ });
4260
+ }
4212
4261
  function marketSections(rows) {
4213
4262
  const installed = rows.filter((row) => isOnMachine(row.state));
4214
4263
  const rest = rows.filter((row) => !isOnMachine(row.state));
@@ -4346,6 +4395,74 @@ function useVisibleBusy(active, floorMs) {
4346
4395
  return busyView(active, visible);
4347
4396
  }
4348
4397
  //#endregion
4398
+ //#region src/app/layouts/rightbar/market-settle-hold.ts
4399
+ function useSettleHold(input) {
4400
+ const { pending, landed, t, onChanged, setPending, setFailed } = input;
4401
+ const until = pending?.until;
4402
+ const id = pending?.id ?? "";
4403
+ const watching = pending !== null && (pending.until !== void 0 || pending.readBack === true);
4404
+ const sinceRef = useRef(0);
4405
+ useEffect(() => {
4406
+ if (!watching || !landed) return;
4407
+ setPending(null);
4408
+ }, [
4409
+ watching,
4410
+ landed,
4411
+ setPending
4412
+ ]);
4413
+ useEffect(() => {
4414
+ if (!watching) {
4415
+ sinceRef.current = 0;
4416
+ return;
4417
+ }
4418
+ if (sinceRef.current === 0) sinceRef.current = Date.now();
4419
+ const deadline = sinceRef.current + holdWindowMs(until);
4420
+ const tick = window.setInterval(() => {
4421
+ onChanged();
4422
+ if (Date.now() < deadline) return;
4423
+ window.clearInterval(tick);
4424
+ setPending(null);
4425
+ setFailed({
4426
+ id,
4427
+ message: t("panels.market.swapSlow")
4428
+ });
4429
+ }, 800);
4430
+ return () => window.clearInterval(tick);
4431
+ }, [
4432
+ watching,
4433
+ landed,
4434
+ until,
4435
+ id,
4436
+ onChanged,
4437
+ setPending,
4438
+ setFailed,
4439
+ t
4440
+ ]);
4441
+ }
4442
+ //#endregion
4443
+ //#region src/app/layouts/rightbar/market-transient-state.ts
4444
+ function useMarketTransient(input) {
4445
+ const { mock, onPending } = input;
4446
+ const [pending, setPending] = useState(mock?.pending ?? null);
4447
+ const [confirming, setConfirming] = useState(mock?.confirming ?? null);
4448
+ const [failed, setFailed] = useState(mock?.failure ?? null);
4449
+ const [notice, setNotice] = useState(mock?.notice ?? null);
4450
+ useEffect(() => {
4451
+ onPending?.(pending !== null);
4452
+ return () => onPending?.(false);
4453
+ }, [pending, onPending]);
4454
+ return {
4455
+ pending,
4456
+ setPending,
4457
+ confirming,
4458
+ setConfirming,
4459
+ failed,
4460
+ setFailed,
4461
+ notice,
4462
+ setNotice
4463
+ };
4464
+ }
4465
+ //#endregion
4349
4466
  //#region src/app/layouts/rightbar/market-plugin-rows.ts
4350
4467
  function installedPluginKept(value) {
4351
4468
  if (typeof value !== "object" || value === null) return void 0;
@@ -4508,6 +4625,80 @@ var ACTION_LABEL_KEY = {
4508
4625
  "uninstall-plugin": "panels.market.uninstall",
4509
4626
  "update-workbench": "panels.market.update"
4510
4627
  };
4628
+ function holdOf(pending, application) {
4629
+ if (pending === null || pending.row === void 0) return void 0;
4630
+ if (application !== "applied") return void 0;
4631
+ return {
4632
+ id: pending.id,
4633
+ row: pending.row
4634
+ };
4635
+ }
4636
+ function pendingLanded(pending, updates, capabilities, installed) {
4637
+ if (pending === null) return true;
4638
+ return settleLanded({
4639
+ action: pending.action,
4640
+ until: pending.until,
4641
+ readBack: pending.readBack,
4642
+ installed,
4643
+ entry: updates[pending.id],
4644
+ clicked: pending.version,
4645
+ outdated: capabilityOutdated(capabilities, pending.id)
4646
+ });
4647
+ }
4648
+ function holdExpired(held, backInRoster, elapsedMs) {
4649
+ if (held === void 0) return true;
4650
+ return backInRoster || elapsedMs >= 15e3;
4651
+ }
4652
+ function heldIdOf(pending, held) {
4653
+ return pending?.id ?? held?.id ?? null;
4654
+ }
4655
+ function pinnedRowOf(pending, held) {
4656
+ return pending?.row ?? held?.row;
4657
+ }
4658
+ function withPinnedRow(rows, pinned) {
4659
+ if (pinned === void 0) return [...rows];
4660
+ if (!rows.some((row) => row.id === pinned.id)) return [...rows, pinned];
4661
+ return rows.map((row) => row.id === pinned.id ? pinned : row);
4662
+ }
4663
+ function marketFootVisible(shown, installed) {
4664
+ return shown.length > 0 || installed.length > 0;
4665
+ }
4666
+ function useMarketTitles(manifests, mocked) {
4667
+ const [titles, setTitles] = useState(/* @__PURE__ */ new Map());
4668
+ useEffect(() => {
4669
+ if (!mocked) setTitles((known) => mergeTitles(known, manifests));
4670
+ }, [manifests, mocked]);
4671
+ return titles;
4672
+ }
4673
+ function useHeldRow(pending, backInRoster) {
4674
+ const [held, setHeld] = useState(void 0);
4675
+ const heldAtRef = useRef(0);
4676
+ const hold = useCallback((next) => {
4677
+ if (next !== void 0) heldAtRef.current = Date.now();
4678
+ setHeld(next);
4679
+ }, []);
4680
+ const back = held === void 0 ? false : backInRoster(held.id);
4681
+ useEffect(() => {
4682
+ if (held === void 0) return;
4683
+ if (holdExpired(held, back, Date.now() - heldAtRef.current)) {
4684
+ setHeld(void 0);
4685
+ return;
4686
+ }
4687
+ const timer = setTimeout(() => setHeld(void 0), MODULE_SETTLE_MS);
4688
+ return () => clearTimeout(timer);
4689
+ }, [held, back]);
4690
+ useEffect(() => {
4691
+ if (pending !== null) setHeld(void 0);
4692
+ }, [pending]);
4693
+ return {
4694
+ held,
4695
+ hold
4696
+ };
4697
+ }
4698
+ function removingIdOf(pending, failed) {
4699
+ if (pending === null || pending.action !== "remove-bundle") return null;
4700
+ return failed?.id === pending.id ? null : pending.id;
4701
+ }
4511
4702
  function pendingRowOf(input) {
4512
4703
  const { pending, removing } = input;
4513
4704
  if (removing === null || pending === null) return void 0;
@@ -4904,18 +5095,21 @@ function changeFailure(change, t) {
4904
5095
  }
4905
5096
  //#endregion
4906
5097
  //#region src/app/layouts/rightbar/market-act.ts
5098
+ var STEADY_ACTIONS = /* @__PURE__ */ new Set(["update", "update-bundle"]);
4907
5099
  function marketActOf(options) {
4908
5100
  const { t, mocked, rowOf, seats, send, onChanged } = options;
4909
5101
  return async (id, action) => {
4910
5102
  if (mocked) return;
4911
5103
  const row = rowOf(id);
4912
- seats.setPending({
5104
+ const next = {
4913
5105
  id,
4914
5106
  action,
4915
5107
  ...row?.titleKey === void 0 ? {} : { titleKey: row.titleKey },
4916
5108
  ...row?.pkg === void 0 ? {} : { pkg: row.pkg },
4917
- ...row?.version?.installed === void 0 ? {} : { version: row.version.installed }
4918
- });
5109
+ ...row?.version?.installed === void 0 ? {} : { version: row.version.installed },
5110
+ ...row === void 0 || !STEADY_ACTIONS.has(action) ? {} : { row }
5111
+ };
5112
+ seats.setPending(next);
4919
5113
  seats.setConfirming(null);
4920
5114
  seats.setFailed(null);
4921
5115
  if (action !== "uninstall-plugin") seats.setNotice(null);
@@ -4929,8 +5123,9 @@ function marketActOf(options) {
4929
5123
  action,
4930
5124
  unit: id
4931
5125
  });
4932
- seats.setPending(null);
4933
5126
  if (!outcome.ok) {
5127
+ seats.setPending(null);
5128
+ seats.setHeld(void 0);
4934
5129
  onChanged();
4935
5130
  seats.setFailed({
4936
5131
  id,
@@ -4938,8 +5133,17 @@ function marketActOf(options) {
4938
5133
  });
4939
5134
  return;
4940
5135
  }
5136
+ const change = changeOf(outcome.value);
5137
+ const until = swapUntil(action, change?.application, row?.version?.latest);
5138
+ const readBack = awaitsReadBack(action, change?.application, next.version);
5139
+ seats.setPending(until === void 0 && !readBack ? null : {
5140
+ ...next,
5141
+ ...until === void 0 ? {} : { until },
5142
+ ...readBack ? { readBack: true } : {}
5143
+ });
5144
+ seats.setHeld(readBack ? void 0 : holdOf(next, change?.application));
4941
5145
  if (action === "uninstall-plugin") seats.setNotice(installedPluginNotice(outcome.value, t));
4942
- const reason = changeFailure(changeOf(outcome.value), t);
5146
+ const reason = changeFailure(change, t);
4943
5147
  onChanged();
4944
5148
  if (reason !== void 0) seats.setFailed({
4945
5149
  id,
@@ -4982,6 +5186,7 @@ function ConfirmUninstall({ t, name, onConfirm, onCancel }) {
4982
5186
  }
4983
5187
  function MarketBody({ t, self, shown, pluginList, installedList, shared, pending, failed, notice, act, onConfirming, setDefault, boardBusy }) {
4984
5188
  const failureOf = (id) => failed?.id === id ? failed.message : null;
5189
+ const foot = marketFootVisible(shown, installedList);
4985
5190
  return /* @__PURE__ */ h(Fragment, null, notice === null ? null : /* @__PURE__ */ h("div", { className: MarketPage_module_default.marketNote }, notice), /* @__PURE__ */ h(MarketSection, {
4986
5191
  t,
4987
5192
  rows: self === void 0 ? [] : [self],
@@ -4995,7 +5200,7 @@ function MarketBody({ t, self, shown, pluginList, installedList, shared, pending
4995
5200
  }), self === void 0 ? null : /* @__PURE__ */ h("div", {
4996
5201
  className: MarketPage_module_default.marketSelfDivider,
4997
5202
  "aria-hidden": "true"
4998
- }), marketSections([...shown, ...installedList]).map((section) => /* @__PURE__ */ h(MarketSection, {
5203
+ }), marketSections(marketOrder([...shown, ...installedList], (row) => marketRowTitle(row, t))).map((section) => /* @__PURE__ */ h(MarketSection, {
4999
5204
  key: section.id,
5000
5205
  t,
5001
5206
  titleKey: section.titleKey,
@@ -5020,10 +5225,10 @@ function MarketBody({ t, self, shown, pluginList, installedList, shared, pending
5020
5225
  act,
5021
5226
  confirmActions: CONFIRM_UNINSTALL,
5022
5227
  onConfirming
5023
- }), /* @__PURE__ */ h(PanelFoot, {
5228
+ }), foot ? /* @__PURE__ */ h(PanelFoot, {
5024
5229
  size: "md",
5025
5230
  text: t("panels.market.foot")
5026
- }), /* @__PURE__ */ h(SharedSection, {
5231
+ }) : null, /* @__PURE__ */ h(SharedSection, {
5027
5232
  t,
5028
5233
  rows: shared,
5029
5234
  pending,
@@ -5032,22 +5237,19 @@ function MarketBody({ t, self, shown, pluginList, installedList, shared, pending
5032
5237
  }));
5033
5238
  }
5034
5239
  function MarketView({ t, manifests, installable, capabilities, freshness, onChanged, onPending, updates, installed, installedRead, workbench, plugins, settling, mock, defaults = [], changed = {} }) {
5035
- const [titles, setTitles] = useState(/* @__PURE__ */ new Map());
5036
- useEffect(() => {
5037
- if (mock === void 0) setTitles((known) => mergeTitles(known, manifests));
5038
- }, [manifests, mock]);
5240
+ const titles = useMarketTitles(manifests, mock !== void 0);
5039
5241
  const catalog = mock?.manifests ?? manifests;
5040
5242
  const marketable = mock === void 0 ? marketCatalog(manifests, installable) : catalog;
5041
5243
  const effective = mock === void 0 ? capabilities : mock.capabilities;
5042
- const source = liveManifests(marketable, effective, mock === void 0 && installed !== void 0 ? new Set(installed.map((item) => item.unit)) : null);
5244
+ const source = liveManifests(marketable, effective, installedIdSet(mock === void 0 ? installed : void 0));
5043
5245
  const byDefault = new Set(defaults);
5044
- const rows = marketRows(source, effective, mock?.updates ?? updates ?? {}, byDefault, changed);
5246
+ const updateMap = mock?.updates ?? updates ?? {};
5247
+ const rows = marketRows(source, effective, updateMap, byDefault, changed);
5045
5248
  const shared = sharedRows(source, effective);
5046
- const [pending, setPending] = useState(mock?.pending ?? null);
5047
- useEffect(() => {
5048
- onPending?.(pending !== null);
5049
- return () => onPending?.(false);
5050
- }, [pending, onPending]);
5249
+ const { pending, setPending, confirming, setConfirming, failed, setFailed, notice, setNotice } = useMarketTransient({
5250
+ mock,
5251
+ onPending
5252
+ });
5051
5253
  const provision = useHostWrite({
5052
5254
  channel: "provision",
5053
5255
  networkText: t("panels.failed")
@@ -5075,19 +5277,19 @@ function MarketView({ t, manifests, installable, capabilities, freshness, onChan
5075
5277
  setNotice(t("panels.failed"));
5076
5278
  }
5077
5279
  };
5078
- const [confirming, setConfirming] = useState(mock?.confirming ?? null);
5079
- const [failed, setFailed] = useState(mock?.failure ?? null);
5080
- const [notice, setNotice] = useState(mock?.notice ?? null);
5081
- const removing = pending !== null && pending.action === "remove-bundle" && failed?.id !== pending.id ? pending.id : null;
5082
- const installedList = installedRows((mock?.installed ?? installed ?? []).filter((module) => !(settling?.has(module.unit) ?? false) && module.unit !== removing), catalog, new Set(rows.map((row) => row.id)), titles, byDefault, changed);
5280
+ const removing = removingIdOf(pending, failed);
5281
+ const steady = useHeldRow(pending, (id) => catalog.some((manifest) => manifest.id === id));
5282
+ const pinned = pinnedRowOf(pending, steady.held);
5283
+ const installedList = installedRows((mock?.installed ?? installed ?? []).filter((module) => module.unit !== pinned?.id && (!(settling?.has(module.unit) ?? false) || module.unit === heldIdOf(pending, steady.held)) && module.unit !== removing), catalog, new Set(rows.map((row) => row.id)), titles, byDefault, changed);
5083
5284
  const pendingRow = pendingRowOf({
5084
5285
  pending,
5085
5286
  removing,
5086
5287
  rows,
5087
5288
  titles
5088
5289
  });
5089
- const shown = pendingRow === void 0 ? rows : [...rows, pendingRow];
5090
- const self = workbenchRow(mock === void 0 ? workbench : mock.workbench);
5290
+ const selfRow = workbenchRow(mock === void 0 ? workbench : mock.workbench);
5291
+ const shown = withPinnedRow(pendingRow === void 0 ? rows : [...rows, pendingRow], pinned);
5292
+ const self = selfRow;
5091
5293
  const pluginList = installedPluginRows(mock?.plugins ?? plugins ?? [], byDefault, changed);
5092
5294
  const rowById = new Map([
5093
5295
  ...shown,
@@ -5096,19 +5298,29 @@ function MarketView({ t, manifests, installable, capabilities, freshness, onChan
5096
5298
  ...pluginList
5097
5299
  ].map((row) => [row.id, row]));
5098
5300
  const confirmingRow = confirming === null ? void 0 : rowById.get(confirming);
5301
+ const seats = {
5302
+ setPending,
5303
+ setHeld: steady.hold,
5304
+ setConfirming,
5305
+ setFailed,
5306
+ setNotice
5307
+ };
5099
5308
  const act = marketActOf({
5100
5309
  t,
5101
5310
  mocked: mock !== void 0,
5102
5311
  rowOf: (id) => rowById.get(id),
5103
- seats: {
5104
- setPending,
5105
- setConfirming,
5106
- setFailed,
5107
- setNotice
5108
- },
5312
+ seats,
5109
5313
  send: (body) => provision.send(body),
5110
5314
  onChanged
5111
5315
  });
5316
+ useSettleHold({
5317
+ pending,
5318
+ landed: pendingLanded(pending, updateMap, effective, workbench?.installed),
5319
+ t,
5320
+ onChanged,
5321
+ setPending,
5322
+ setFailed
5323
+ });
5112
5324
  const settled = marketSettled(freshness, installedRead);
5113
5325
  const nothing = shown.length === 0 && self === void 0 && installedList.length === 0 && pluginList.length === 0;
5114
5326
  if (!settled || (settling?.size ?? 0) > 0 && shown.length === 0 && installedList.length === 0) return /* @__PURE__ */ h(MarketChecking, { t });
@@ -6867,6 +7079,10 @@ function ConfigSyncSection({ t }) {
6867
7079
  }) : null, rows.length > 0 ? /* @__PURE__ */ h("div", { className: ConfigSyncSection_module_default.foot }, /* @__PURE__ */ h(Button, {
6868
7080
  variant: "primary",
6869
7081
  size: "md",
7082
+ icon: /* @__PURE__ */ h(IconRefreshOutlineRegular, {
7083
+ size: 16,
7084
+ "aria-hidden": "true"
7085
+ }),
6870
7086
  disabled: chosen.length === 0 || busy.busy,
6871
7087
  onClick: apply
6872
7088
  }, chosen.length === 0 ? t("configSync.applyNone") : busy.busy ? t("configSync.applying") : t("configSync.apply", { count: chosen.length }))) : null) : null));