dsh-plugin-shop 0.7.5-beta.0 → 0.8.0-beta.0

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.
package/lib/client.js CHANGED
@@ -4324,6 +4324,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4324
4324
  });
4325
4325
  const dsh_plugin_shop_shop_installed_result$schema = array(object({
4326
4326
  "name": string(),
4327
+ "source": union([literal("npm"), literal("github")]),
4328
+ "repo": union([_undefined(), string()]).optional(),
4329
+ "subdir": union([_undefined(), string()]).optional(),
4327
4330
  "installed": string(),
4328
4331
  "latest": string(),
4329
4332
  "outdated": boolean(),
@@ -4336,7 +4339,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4336
4339
  _undefined(),
4337
4340
  literal(false),
4338
4341
  literal(true)
4339
- ]).optional()
4342
+ ]).optional(),
4343
+ "source": union([
4344
+ _undefined(),
4345
+ literal("npm"),
4346
+ literal("github")
4347
+ ]).optional(),
4348
+ "repo": union([_undefined(), string()]).optional(),
4349
+ "subdir": union([_undefined(), string()]).optional()
4340
4350
  });
4341
4351
  const dsh_plugin_shop_shop_installStart_result$schema = union([object({
4342
4352
  "ok": literal(true),
@@ -4348,7 +4358,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4348
4358
  literal("denied"),
4349
4359
  literal("version-mismatch"),
4350
4360
  literal("needs-acknowledgement"),
4351
- literal("tarball-integrity")
4361
+ literal("tarball-integrity"),
4362
+ literal("ambiguous-identity")
4352
4363
  ]),
4353
4364
  "detail": string()
4354
4365
  })]);
@@ -4437,7 +4448,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4437
4448
  },
4438
4449
  sourceLocation: {
4439
4450
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4440
- "line": 623,
4451
+ "line": 678,
4441
4452
  "column": 9
4442
4453
  }
4443
4454
  },
@@ -4455,7 +4466,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4455
4466
  },
4456
4467
  sourceLocation: {
4457
4468
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4458
- "line": 809,
4469
+ "line": 863,
4459
4470
  "column": 9
4460
4471
  }
4461
4472
  },
@@ -4483,7 +4494,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4483
4494
  },
4484
4495
  sourceLocation: {
4485
4496
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4486
- "line": 674,
4497
+ "line": 726,
4487
4498
  "column": 9
4488
4499
  }
4489
4500
  },
@@ -4510,7 +4521,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4510
4521
  },
4511
4522
  sourceLocation: {
4512
4523
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4513
- "line": 798,
4524
+ "line": 852,
4514
4525
  "column": 3
4515
4526
  }
4516
4527
  },
@@ -4528,7 +4539,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4528
4539
  },
4529
4540
  sourceLocation: {
4530
4541
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4531
- "line": 959,
4542
+ "line": 1021,
4532
4543
  "column": 9
4533
4544
  }
4534
4545
  },
@@ -4555,7 +4566,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4555
4566
  },
4556
4567
  sourceLocation: {
4557
4568
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4558
- "line": 515,
4569
+ "line": 535,
4559
4570
  "column": 9
4560
4571
  }
4561
4572
  },
@@ -4583,7 +4594,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4583
4594
  },
4584
4595
  sourceLocation: {
4585
4596
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4586
- "line": 901,
4597
+ "line": 956,
4587
4598
  "column": 9
4588
4599
  }
4589
4600
  },
@@ -4610,7 +4621,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4610
4621
  },
4611
4622
  sourceLocation: {
4612
4623
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4613
- "line": 1027,
4624
+ "line": 1106,
4614
4625
  "column": 9
4615
4626
  }
4616
4627
  },
@@ -4628,7 +4639,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4628
4639
  },
4629
4640
  sourceLocation: {
4630
4641
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4631
- "line": 1010,
4642
+ "line": 1089,
4632
4643
  "column": 9
4633
4644
  }
4634
4645
  }
@@ -4709,6 +4720,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4709
4720
  return hasPlugin || SHOP_KEYWORDS.includes(segments.at(-1));
4710
4721
  }
4711
4722
  //#endregion
4723
+ //#region src/shared/identity.ts
4724
+ /** The registry's uniqueness rule, shared with the presentation layer. */
4725
+ function identityKey(identity) {
4726
+ return identity.source === "npm" ? `npm:${identity.name}` : `github:${identity.repo ?? identity.name}#${identity.subdir ?? ""}`;
4727
+ }
4728
+ /** `owner/slug`, lowercased, or null when this is not a repository reference. */
4729
+ const REPO = "([\\w.-]+)\\/([\\w.-]+?)";
4730
+ new RegExp(`^github:${REPO}(?:[#&].*)?$`);
4731
+ new RegExp(`^(?:git\\+)?https?:\\/\\/(?:www\\.)?github\\.com\\/${REPO}(?:\\.git)?(?:[\\/?#].*)?$`);
4732
+ //#endregion
4712
4733
  //#region src/client/present.ts
4713
4734
  /** Hot-mount reason code → locale key, so the notice reads in the language
4714
4735
  * the person set in dsh. An absent (or unrecognized) reason keeps the generic
@@ -4733,9 +4754,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4733
4754
  }
4734
4755
  /** The peers the Host said this installation does not provide, or none when it
4735
4756
  * said nothing — a plugin that runs here and one the Host could not judge are
4736
- * both rendered as no warning at all. */
4737
- function missingPeersOf(incompatible, name) {
4738
- return incompatible[name] ?? [];
4757
+ * both rendered as no warning at all. The map is keyed by install identity,
4758
+ * never by name, because two entries can share a name. */
4759
+ function missingPeersOf(incompatible, key) {
4760
+ return incompatible[key] ?? [];
4739
4761
  }
4740
4762
  /** Spec §9.3 verbatim — the community-tier acknowledgement. The zh dictionary
4741
4763
  * states the same facts in its own register; never soften this wording. */
@@ -4751,6 +4773,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4751
4773
  case "version-mismatch": return "versionMismatchCode";
4752
4774
  case "needs-acknowledgement": return "needsAcknowledgementCode";
4753
4775
  case "tarball-integrity": return "tarballIntegrityCode";
4776
+ case "ambiguous-identity": return "ambiguousIdentityCode";
4754
4777
  }
4755
4778
  }
4756
4779
  /** The version a card displays: full for npm, the short commit for github. */
@@ -4934,7 +4957,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4934
4957
  * This mirrors the registry's identity verbatim; the two must not drift.
4935
4958
  */
4936
4959
  function entryKey(entry) {
4937
- return entry.source === "npm" ? `npm:${entry.name}` : `github:${entry.repo ?? entry.name}#${entry.subdir ?? ""}`;
4960
+ return identityKey(entry);
4938
4961
  }
4939
4962
  /**
4940
4963
  * One entry's star count from the sidecar, or undefined when it has none.
@@ -4951,8 +4974,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4951
4974
  * repo, one the catalog never even listed.
4952
4975
  */
4953
4976
  function starsOf(entry, stars) {
4954
- return stars[entry.repo ?? entry.name];
4955
- }
4977
+ const key = entry.repo ?? entry.name;
4978
+ if (!Object.hasOwn(stars, key)) return void 0;
4979
+ return stars[key];
4980
+ }
4981
+ /** One collator for the whole shelf. Passing an options object to
4982
+ * localeCompare creates a collator per comparison, which dominates the sort
4983
+ * for a catalog with thousands of entries. */
4984
+ const NAME_COLLATOR = new Intl.Collator(void 0, { sensitivity: "base" });
4956
4985
  /** Sort the shelf: stars descending, un-starred entries last, name ascending
4957
4986
  * (case-insensitive) on ties (spec 2026-08-26-github-stars-design.md D1).
4958
4987
  * Display-time only — the catalog's own name sort is untouched. */
@@ -4961,7 +4990,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4961
4990
  return [...entries].sort((a, b) => {
4962
4991
  const byStars = count(b) - count(a);
4963
4992
  if (byStars !== 0) return byStars;
4964
- return a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
4993
+ return NAME_COLLATOR.compare(a.name, b.name);
4965
4994
  });
4966
4995
  }
4967
4996
  /** 999 → "999"; 1000 → "1k"; 1234 → "1.2k"; 1500 → "1.5k"; 99999 → "100k". */
@@ -5030,6 +5059,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5030
5059
  versionMismatchCode: "版本不匹配",
5031
5060
  needsAcknowledgementCode: "需要确认",
5032
5061
  tarballIntegrityCode: "压缩包完整性校验失败",
5062
+ ambiguousIdentityCode: "无法确定是哪一个条目",
5033
5063
  npmSource: "npm 包",
5034
5064
  githubSource: "源码在 GitHub",
5035
5065
  installedSection: "已安装",
@@ -5119,6 +5149,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5119
5149
  versionMismatchCode: "Version mismatch",
5120
5150
  needsAcknowledgementCode: "Acknowledgement required",
5121
5151
  tarballIntegrityCode: "tarball integrity check failed",
5152
+ ambiguousIdentityCode: "Ambiguous entry",
5122
5153
  npmSource: "npm package",
5123
5154
  githubSource: "source on GitHub",
5124
5155
  installedSection: "Installed",
@@ -5176,42 +5207,93 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5176
5207
  installStatus
5177
5208
  ]);
5178
5209
  }
5179
- /** Drive one install: rejections, the polling loop at INSTALL_POLL_MS, and
5180
- * terminal states. The interval is cleared on unmount and whenever the view
5181
- * leaves `running`, so a done/failed/rejected install never polls again. */
5182
- function useInstall(install, installStatus) {
5183
- const [view, setView] = (0, react.useState)({ kind: "idle" });
5184
- const start = (0, react.useCallback)(async (args) => {
5185
- setView({ kind: "idle" });
5210
+ /**
5211
+ * Install flows owned by the tab and keyed by install identity. Keeping the
5212
+ * state above individual cards preserves a running operation when filtering
5213
+ * unmounts its card, and makes the shelf and Outdated panels agree.
5214
+ */
5215
+ function useInstallFlows(install, installStatus, onSettled) {
5216
+ const [views, setViews] = (0, react.useState)(() => /* @__PURE__ */ new Map());
5217
+ const settled = (0, react.useRef)(onSettled);
5218
+ settled.current = onSettled;
5219
+ const put = (0, react.useCallback)((key, view) => {
5220
+ setViews((current) => {
5221
+ const next = new Map(current);
5222
+ next.set(key, view);
5223
+ return next;
5224
+ });
5225
+ }, []);
5226
+ const apply = (0, react.useCallback)((key, event) => {
5227
+ setViews((current) => {
5228
+ const before = current.get(key) ?? { kind: "idle" };
5229
+ const after = reduceInstall(before, event);
5230
+ if (after === before) return current;
5231
+ const next = new Map(current);
5232
+ next.set(key, after);
5233
+ return next;
5234
+ });
5235
+ }, []);
5236
+ const start = (0, react.useCallback)(async (key, args) => {
5237
+ put(key, { kind: "idle" });
5186
5238
  try {
5187
5239
  const result = await install(args);
5188
5240
  if (!result.ok) {
5189
- setView({
5241
+ put(key, {
5190
5242
  kind: "rejected",
5191
5243
  code: result.code,
5192
5244
  detail: result.detail
5193
5245
  });
5194
5246
  return;
5195
5247
  }
5196
- setView({
5248
+ put(key, {
5197
5249
  kind: "running",
5198
5250
  installId: result.installId,
5199
5251
  log: []
5200
5252
  });
5201
5253
  } catch {
5202
- setView({
5254
+ put(key, {
5203
5255
  kind: "failed",
5204
5256
  detail: "",
5205
5257
  log: []
5206
5258
  });
5207
5259
  }
5208
- }, [install]);
5209
- usePollStatus(view, setView, installStatus);
5210
- return {
5211
- view,
5260
+ }, [install, put]);
5261
+ const reset = (0, react.useCallback)((key) => {
5262
+ setViews((current) => {
5263
+ if (!current.has(key)) return current;
5264
+ const next = new Map(current);
5265
+ next.delete(key);
5266
+ return next;
5267
+ });
5268
+ }, []);
5269
+ (0, react.useEffect)(() => {
5270
+ const running = [];
5271
+ for (const [key, view] of views) if (view.kind === "running") running.push([key, view.installId]);
5272
+ if (running.length === 0) return;
5273
+ const timer = setInterval(() => {
5274
+ for (const [key, installId] of running) installStatus({ installId }).then((status) => {
5275
+ apply(key, {
5276
+ type: "status",
5277
+ status
5278
+ });
5279
+ if (status.found && status.state !== "running") settled.current?.(key);
5280
+ }, () => {});
5281
+ }, INSTALL_POLL_MS);
5282
+ return () => clearInterval(timer);
5283
+ }, [
5284
+ views,
5285
+ installStatus,
5286
+ apply
5287
+ ]);
5288
+ return { flowFor: (0, react.useCallback)((key) => ({
5289
+ view: views.get(key) ?? { kind: "idle" },
5290
+ start: (args) => start(key, args),
5291
+ reset: () => reset(key)
5292
+ }), [
5293
+ views,
5212
5294
  start,
5213
- reset: (0, react.useCallback)(() => setView({ kind: "idle" }), [])
5214
- };
5295
+ reset
5296
+ ]) };
5215
5297
  }
5216
5298
  //#endregion
5217
5299
  //#region src/client/useUninstall.ts
@@ -5433,13 +5515,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5433
5515
  * install controls. An installed plugin's card carries its installed row:
5434
5516
  * current → the non-interactive installed label, behind → the update button;
5435
5517
  * uninstalled → the install button. */
5436
- const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, t, install, installStatus, uninstall, restart, restartSupported, setEnabled }) {
5518
+ const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, t, flowFor, installStatus, uninstall, restart, restartSupported, setEnabled, onSettled }) {
5437
5519
  const [open, setOpen] = (0, react.useState)(false);
5438
5520
  const detailId = (0, react.useId)();
5439
5521
  const summary = entry.catalog?.summary;
5440
5522
  const npmUrl = npmPageUrl(entry);
5441
5523
  const author = authorOf(entry);
5442
5524
  const category = entry.catalog?.category ?? "other";
5525
+ const installTarget = {
5526
+ name: entry.name,
5527
+ version: entry.version,
5528
+ source: entry.source,
5529
+ repo: entry.repo,
5530
+ subdir: entry.subdir
5531
+ };
5532
+ const flow = flowFor(entryKey(entry));
5533
+ const uninstallSettled = (0, react.useCallback)(() => {
5534
+ flow.reset();
5535
+ onSettled();
5536
+ }, [flow, onSettled]);
5443
5537
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5444
5538
  className: _dsh_plugin_shop_css_e3675a89_default.card,
5445
5539
  "data-shop-entry": entry.name,
@@ -5589,27 +5683,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5589
5683
  className: _dsh_plugin_shop_css_e3675a89_default.cardActions,
5590
5684
  "data-shop-actions": true,
5591
5685
  children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5592
- name: entry.name,
5593
- version: entry.version,
5686
+ target: installTarget,
5594
5687
  tier: entry.tier,
5595
5688
  missing,
5596
5689
  missingStated: true,
5690
+ flow,
5597
5691
  t,
5598
- install,
5599
- installStatus,
5600
5692
  restart,
5601
5693
  restartSupported
5602
5694
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5603
- installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5604
- name: entry.name,
5605
- version: entry.version,
5695
+ installed.outdated || flow.view.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5696
+ target: installTarget,
5606
5697
  tier: entry.tier,
5607
- variant: "update",
5698
+ variant: installed.outdated ? "update" : "install",
5608
5699
  missing,
5609
5700
  missingStated: true,
5701
+ flow,
5610
5702
  t,
5611
- install,
5612
- installStatus,
5613
5703
  restart,
5614
5704
  restartSupported
5615
5705
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -5631,7 +5721,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5631
5721
  uninstall,
5632
5722
  installStatus,
5633
5723
  restart,
5634
- restartSupported
5724
+ restartSupported,
5725
+ onSettled: uninstallSettled
5635
5726
  })
5636
5727
  ] }), author !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5637
5728
  className: _dsh_plugin_shop_css_e3675a89_default.author,
@@ -5665,9 +5756,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5665
5756
  * failure detail, rejection detail — driven by `useInstall`. Shared by the
5666
5757
  * catalog cards (`variant: 'install'`) and the outdated rows' update button
5667
5758
  * (`variant: 'update'`, which drives the same install flow for `name@latest`). */
5668
- function InstallPanel({ name, version, tier, missing, missingStated = false, variant = "install", t, install, installStatus, restart, restartSupported }) {
5759
+ function InstallPanel({ target, tier, missing, missingStated = false, variant = "install", flow, t, restart, restartSupported }) {
5669
5760
  const [gateOpen, setGateOpen] = (0, react.useState)(false);
5670
- const { view, start } = useInstall(install, installStatus);
5761
+ const { view, start } = flow;
5671
5762
  if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5672
5763
  className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
5673
5764
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -5755,8 +5846,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5755
5846
  onClick: () => {
5756
5847
  setGateOpen(false);
5757
5848
  start({
5758
- name,
5759
- version,
5849
+ ...target,
5760
5850
  acknowledged: true
5761
5851
  });
5762
5852
  },
@@ -5777,8 +5867,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5777
5867
  ...update ? { "data-shop-update": true } : { "data-shop-install": true },
5778
5868
  onClick: () => {
5779
5869
  if (tier === "verified") start({
5780
- name,
5781
- version,
5870
+ ...target,
5782
5871
  acknowledged: void 0
5783
5872
  });
5784
5873
  else setGateOpen(true);
@@ -5795,8 +5884,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5795
5884
  * §9.3 is about granting. A business failure (not in the catalog / not
5796
5885
  * installed) lands in the failed view with the host's published detail; a
5797
5886
  * transport failure carries the empty detail and the localized fallback. */
5798
- function UninstallPanel({ name, t, uninstall, installStatus, restart, restartSupported }) {
5887
+ function UninstallPanel({ name, t, uninstall, installStatus, restart, restartSupported, onSettled }) {
5799
5888
  const { view, start } = useUninstall(uninstall, installStatus);
5889
+ const settled = (0, react.useRef)(onSettled);
5890
+ settled.current = onSettled;
5891
+ (0, react.useEffect)(() => {
5892
+ if (view.kind === "done") settled.current();
5893
+ }, [view.kind]);
5800
5894
  if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5801
5895
  className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
5802
5896
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -6024,7 +6118,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6024
6118
  /** One outdated install row (§7.3): the name, the installed and latest
6025
6119
  * versions, the hot enable/disable switch, and the update button (the
6026
6120
  * install flow for `name@latest`, reusing `InstallPanel`). */
6027
- function OutdatedRow({ row, tier, source, missing, t, setEnabled, install, installStatus, restart, restartSupported }) {
6121
+ function OutdatedRow({ row, tier, missing, t, setEnabled, flowFor, restart, restartSupported }) {
6028
6122
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6029
6123
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedRow,
6030
6124
  "data-shop-outdated-entry": row.name,
@@ -6035,7 +6129,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6035
6129
  children: row.name
6036
6130
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6037
6131
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersions,
6038
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("installedVersion", { version: source === "github" ? row.installed.slice(0, 7) : row.installed }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("latestVersion", { version: row.latest }) })]
6132
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("installedVersion", { version: row.source === "github" ? row.installed.slice(0, 7) : row.installed }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("latestVersion", { version: row.latest }) })]
6039
6133
  })]
6040
6134
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6041
6135
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedActions,
@@ -6044,14 +6138,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6044
6138
  t,
6045
6139
  setEnabled
6046
6140
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
6047
- name: row.name,
6048
- version: row.latest,
6141
+ target: {
6142
+ name: row.name,
6143
+ version: row.latest,
6144
+ source: row.source,
6145
+ repo: row.repo,
6146
+ subdir: row.subdir
6147
+ },
6049
6148
  tier,
6050
6149
  variant: "update",
6051
6150
  missing,
6151
+ flow: flowFor(identityKey(row)),
6052
6152
  t,
6053
- install,
6054
- installStatus,
6055
6153
  restart,
6056
6154
  restartSupported
6057
6155
  })]
@@ -6065,7 +6163,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6065
6163
  * tier for the update gate is looked up from the catalog by name (community →
6066
6164
  * acknowledgement); an entry absent from the catalog defaults to the
6067
6165
  * community gate (the safer read). */
6068
- function OutdatedSection({ state, tiers, sources, missingByName, t, setEnabled, install, installStatus, restart, restartSupported }) {
6166
+ function OutdatedSection({ state, entriesByKey, missingByKey, t, setEnabled, flowFor, restart, restartSupported }) {
6069
6167
  if (state.kind === "loading") return null;
6070
6168
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6071
6169
  className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
@@ -6084,16 +6182,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6084
6182
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedList,
6085
6183
  children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
6086
6184
  row,
6087
- tier: tiers.get(row.name) ?? "community",
6088
- source: sources.get(row.name) ?? "npm",
6089
- missing: missingByName.get(row.name) ?? [],
6185
+ tier: entriesByKey.get(identityKey(row))?.tier ?? "community",
6186
+ missing: missingByKey.get(identityKey(row)) ?? [],
6090
6187
  t,
6091
6188
  setEnabled,
6092
- install,
6093
- installStatus,
6189
+ flowFor,
6094
6190
  restart,
6095
6191
  restartSupported
6096
- }) }, row.name))
6192
+ }) }, identityKey(row)))
6097
6193
  })]
6098
6194
  });
6099
6195
  }
@@ -6108,6 +6204,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6108
6204
  const [selfRestartGate, setSelfRestartGate] = (0, react.useState)(false);
6109
6205
  const selfUpdate = useUpdateSelf(updateStart, installStatus);
6110
6206
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
6207
+ const [mutations, setMutations] = (0, react.useState)(0);
6208
+ const noteMutation = (0, react.useCallback)(() => {
6209
+ setMutations((current) => current + 1);
6210
+ }, []);
6211
+ const flows = useInstallFlows(install, installStatus, noteMutation);
6111
6212
  const [reloading, setReloading] = (0, react.useState)(false);
6112
6213
  const [reloadFailed, setReloadFailed] = (0, react.useState)(false);
6113
6214
  const [query, setQuery] = (0, react.useState)("");
@@ -6188,10 +6289,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6188
6289
  return () => {
6189
6290
  cancelled = true;
6190
6291
  };
6191
- }, [installed, request]);
6192
- const installedByName = (0, react.useMemo)(() => {
6292
+ }, [
6293
+ installed,
6294
+ request,
6295
+ mutations
6296
+ ]);
6297
+ const installedByKey = (0, react.useMemo)(() => {
6193
6298
  const map = /* @__PURE__ */ new Map();
6194
- if (installedState.kind === "ready") for (const entry of installedState.entries) map.set(entry.name, entry);
6299
+ if (installedState.kind === "ready") for (const entry of installedState.entries) map.set(identityKey(entry), entry);
6195
6300
  return map;
6196
6301
  }, [installedState]);
6197
6302
  const browsable = (0, react.useMemo)(() => {
@@ -6199,11 +6304,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6199
6304
  const cleared = new Set(catalogState.result.notAShop ?? []);
6200
6305
  return catalogState.result.plugins.filter((entry) => cleared.has(entry.name) || !isShopLike(entry.name) && (entry.repo === void 0 || !isShopLike(entry.repo)));
6201
6306
  }, [catalogState]);
6307
+ const stars = (0, react.useMemo)(() => catalogState.kind === "ready" ? catalogState.result.stars : {}, [catalogState]);
6308
+ const sortedBrowsable = (0, react.useMemo)(() => sortByStars(browsable, stars), [browsable, stars]);
6202
6309
  const filtered = (0, react.useMemo)(() => {
6203
6310
  const q = query.trim().toLowerCase();
6204
- return browsable.filter((entry) => {
6311
+ return sortedBrowsable.filter((entry) => {
6205
6312
  if (category === "installed") {
6206
- if (!installedByName.has(entry.name)) return false;
6313
+ if (!installedByKey.has(entryKey(entry))) return false;
6207
6314
  } else if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
6208
6315
  if (q === "") return true;
6209
6316
  const summaryEn = entry.catalog?.summary.en ?? "";
@@ -6211,14 +6318,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6211
6318
  return entry.name.toLowerCase().includes(q) || summaryEn.toLowerCase().includes(q) || summaryZh.toLowerCase().includes(q);
6212
6319
  });
6213
6320
  }, [
6214
- browsable,
6321
+ sortedBrowsable,
6215
6322
  query,
6216
6323
  category,
6217
- installedByName
6324
+ installedByKey
6218
6325
  ]);
6219
6326
  filteredLenRef.current = filtered.length;
6220
- const stars = catalogState.kind === "ready" ? catalogState.result.stars : {};
6221
- const sorted = (0, react.useMemo)(() => sortByStars(filtered, stars), [filtered, stars]);
6222
6327
  (0, react.useEffect)(() => {
6223
6328
  if (!incremental) return;
6224
6329
  const node = sentinelRef.current;
@@ -6233,7 +6338,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6233
6338
  visibleCount,
6234
6339
  filtered.length
6235
6340
  ]);
6236
- const visible = incremental ? sorted.slice(0, visibleCount) : sorted;
6341
+ const visible = incremental ? filtered.slice(0, visibleCount) : filtered;
6237
6342
  const categoryCounts = (0, react.useMemo)(() => {
6238
6343
  const counts = /* @__PURE__ */ new Map();
6239
6344
  for (const entry of browsable) {
@@ -6247,19 +6352,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6247
6352
  if (installedState.kind !== "ready") return 0;
6248
6353
  return installedState.entries.filter((entry) => !isShopLike(entry.name)).length;
6249
6354
  }, [installedState]);
6250
- const tiers = (0, react.useMemo)(() => {
6355
+ const entriesByKey = (0, react.useMemo)(() => {
6251
6356
  const map = /* @__PURE__ */ new Map();
6252
- if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.tier);
6357
+ if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entryKey(entry), entry);
6253
6358
  return map;
6254
6359
  }, [catalogState]);
6255
- const sources = (0, react.useMemo)(() => {
6360
+ const missingByKey = (0, react.useMemo)(() => {
6256
6361
  const map = /* @__PURE__ */ new Map();
6257
- if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.source);
6258
- return map;
6259
- }, [catalogState]);
6260
- const missingByName = (0, react.useMemo)(() => {
6261
- const map = /* @__PURE__ */ new Map();
6262
- if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, missingPeersOf(catalogState.result.incompatible, entry.name));
6362
+ if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entryKey(entry), missingPeersOf(catalogState.result.incompatible, entryKey(entry)));
6263
6363
  return map;
6264
6364
  }, [catalogState]);
6265
6365
  if (catalogState.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -6522,15 +6622,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6522
6622
  children: [visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
6523
6623
  entry,
6524
6624
  stars: starsOf(entry, stars),
6525
- installed: installedByName.get(entry.name),
6526
- missing: missingByName.get(entry.name) ?? [],
6625
+ installed: installedByKey.get(entryKey(entry)),
6626
+ missing: missingByKey.get(entryKey(entry)) ?? [],
6527
6627
  t,
6528
- install,
6628
+ flowFor: flows.flowFor,
6529
6629
  installStatus,
6530
6630
  uninstall,
6531
6631
  restart,
6532
6632
  restartSupported,
6533
- setEnabled
6633
+ setEnabled,
6634
+ onSettled: noteMutation
6534
6635
  }) }, entryKey(entry))), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
6535
6636
  ref: sentinelRef,
6536
6637
  className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
@@ -6547,13 +6648,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6547
6648
  })] }),
6548
6649
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedSection, {
6549
6650
  state: installedState,
6550
- tiers,
6551
- sources,
6552
- missingByName,
6651
+ entriesByKey,
6652
+ missingByKey,
6553
6653
  t,
6554
6654
  setEnabled,
6555
- install,
6556
- installStatus,
6655
+ flowFor: flows.flowFor,
6557
6656
  restart,
6558
6657
  restartSupported
6559
6658
  })
@@ -6564,13 +6663,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6564
6663
  //#region src/client/index.ts
6565
6664
  /** Dictionary namespace owned by this plugin. */
6566
6665
  const NS = "settings.shop";
6666
+ /** How long a stashed catalog may satisfy a plain open. The host applies the
6667
+ * same five-minute freshness window, so the client stash must not outlive it. */
6668
+ const WARM_TTL_MS = 3e5;
6567
6669
  /** The boot-time warm fetch: the catalog the tab wants on its first open.
6568
6670
  * Started in apply — the client bundle boots with the web app, long before
6569
6671
  * the user reaches Settings — so the host's slow network fetch runs while
6570
6672
  * nobody is looking at the shop, and the tab's mount consumes this promise
6571
6673
  * instead of starting its own fetch. A rejection stays stored (the injected
6572
6674
  * catalog falls back to a fresh call); the extra catch keeps the
6573
- * fire-and-forget from surfacing as an unhandled rejection. */
6675
+ * fire-and-forget from surfacing as an unhandled rejection. Every later
6676
+ * result, including an explicit refresh, replaces this timestamped stash. */
6574
6677
  let warmCatalog = null;
6575
6678
  /** Services required by the tab registration and the Remote mount.
6576
6679
  *
@@ -6602,19 +6705,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6602
6705
  return result.value;
6603
6706
  };
6604
6707
  warmCatalog = null;
6605
- warmCatalog = Promise.resolve(ns.catalog(void 0)).then((result) => unwrap(result));
6606
- warmCatalog.catch(() => {});
6708
+ const warmed = Promise.resolve(ns.catalog(void 0)).then((result) => unwrap(result));
6709
+ warmCatalog = {
6710
+ at: Date.now(),
6711
+ result: warmed
6712
+ };
6713
+ warmed.catch(() => {});
6607
6714
  Promise.resolve(ns.installed()).then((result) => unwrap(result)).catch(() => {});
6608
6715
  Promise.resolve(ns.version()).then((result) => unwrap(result)).catch(() => {});
6609
6716
  const injected = () => ({
6610
6717
  catalog: async (args) => {
6611
- if (args?.refresh === true) return unwrap(await ns.catalog(args));
6718
+ if (args?.refresh === true) {
6719
+ const refreshed = unwrap(await ns.catalog(args));
6720
+ warmCatalog = {
6721
+ at: Date.now(),
6722
+ result: Promise.resolve(refreshed)
6723
+ };
6724
+ return refreshed;
6725
+ }
6612
6726
  const warm = warmCatalog;
6613
- if (warm !== null) try {
6614
- return await warm;
6727
+ if (warm !== null && Date.now() - warm.at < 3e5) try {
6728
+ return await warm.result;
6615
6729
  } catch {}
6616
6730
  const fresh = unwrap(await ns.catalog(args));
6617
- warmCatalog = Promise.resolve(fresh);
6731
+ warmCatalog = {
6732
+ at: Date.now(),
6733
+ result: Promise.resolve(fresh)
6734
+ };
6618
6735
  return fresh;
6619
6736
  },
6620
6737
  install: async (args) => unwrap(await ns.installStart(args)),
@@ -6637,6 +6754,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6637
6754
  }
6638
6755
  //#endregion
6639
6756
  exports.NS = NS;
6757
+ exports.WARM_TTL_MS = WARM_TTL_MS;
6640
6758
  exports.apply = apply;
6641
6759
  exports.inject = inject;
6642
6760
  return module.exports;