dsh-plugin-shop 0.8.3 → 0.8.4-beta.1

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
@@ -4316,6 +4316,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4316
4316
  })]).optional(),
4317
4317
  "added": union([_undefined(), string()]).optional(),
4318
4318
  "peers": union([_undefined(), array(string())]).optional(),
4319
+ "compatibility": union([_undefined(), object({
4320
+ "dsh": union([_undefined(), string()]).optional(),
4321
+ "profiles": union([_undefined(), array(string())]).optional()
4322
+ })]).optional(),
4323
+ "dshPeers": union([_undefined(), record(string(), string())]).optional(),
4319
4324
  "unpackedSize": union([_undefined(), number()]).optional(),
4320
4325
  "installSize": union([_undefined(), number()]).optional()
4321
4326
  })),
@@ -4326,7 +4331,22 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4326
4331
  })),
4327
4332
  "notAShop": union([_undefined(), array(string())]).optional(),
4328
4333
  "stars": record(string(), number()),
4329
- "incompatible": record(string(), array(string()))
4334
+ "incompatible": record(string(), array(string())),
4335
+ "incompatibleHarness": record(string(), object({
4336
+ "dsh": union([_undefined(), object({
4337
+ "range": string(),
4338
+ "running": string()
4339
+ })]).optional(),
4340
+ "profile": union([_undefined(), object({
4341
+ "declared": array(string()),
4342
+ "running": string()
4343
+ })]).optional(),
4344
+ "peers": union([_undefined(), object({
4345
+ "refused": record(string(), string()),
4346
+ "running": string(),
4347
+ "allowCommand": union([literal(null), string()])
4348
+ })]).optional()
4349
+ }))
4330
4350
  });
4331
4351
  const dsh_plugin_shop_shop_installed_result$schema = array(object({
4332
4352
  "name": string(),
@@ -4373,7 +4393,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4373
4393
  literal("needs-acknowledgement"),
4374
4394
  literal("tarball-integrity"),
4375
4395
  literal("ambiguous-identity"),
4376
- literal("name-taken")
4396
+ literal("name-taken"),
4397
+ literal("desktop-profile")
4377
4398
  ]),
4378
4399
  "detail": string()
4379
4400
  })]);
@@ -4400,11 +4421,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4400
4421
  literal("host-unsupported"),
4401
4422
  literal("timeout"),
4402
4423
  literal("mount-failed"),
4403
- literal("client-half")
4424
+ literal("already-loaded")
4404
4425
  ]).optional(),
4405
4426
  "detail": union([_undefined(), string()]).optional()
4406
4427
  });
4407
- const dsh_plugin_shop_shop_restart_result$schema = union([object({ "ok": literal(true) }), object({
4428
+ const dsh_plugin_shop_shop_restart_result$schema = union([object({
4429
+ "ok": literal(true),
4430
+ "logFile": union([_undefined(), string()]).optional()
4431
+ }), object({
4408
4432
  "ok": literal(false),
4409
4433
  "detail": string()
4410
4434
  })]);
@@ -4451,10 +4475,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4451
4475
  "outdated": boolean(),
4452
4476
  "restartBlocked": union([
4453
4477
  literal(null),
4478
+ literal("desktop"),
4454
4479
  literal("windows"),
4455
4480
  literal("systemd"),
4456
4481
  literal("port-zero")
4457
- ])
4482
+ ]),
4483
+ "pendingVersion": union([
4484
+ _undefined(),
4485
+ literal(null),
4486
+ string()
4487
+ ]).optional()
4458
4488
  });
4459
4489
  const TYPERT_REMOTE = {
4460
4490
  package: "dsh-plugin-shop",
@@ -4473,17 +4503,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4473
4503
  codec: {
4474
4504
  mode: "strict",
4475
4505
  typeSymbol: "dsh-plugin-shop#shop/catalog:args",
4476
- schema: dsh_plugin_shop_shop_catalog_parameter_0$schema
4506
+ schema: dsh_plugin_shop_shop_catalog_parameter_0$schema,
4507
+ create: () => dsh_plugin_shop_shop_catalog_parameter_0$schema
4477
4508
  }
4478
4509
  }],
4479
4510
  result: {
4480
4511
  mode: "strict",
4481
4512
  typeSymbol: "dsh-plugin-shop/types#ShopCatalogResult",
4482
- schema: dsh_plugin_shop_shop_catalog_result$schema
4513
+ schema: dsh_plugin_shop_shop_catalog_result$schema,
4514
+ create: () => dsh_plugin_shop_shop_catalog_result$schema
4483
4515
  },
4484
4516
  sourceLocation: {
4485
4517
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4486
- "line": 867,
4518
+ "line": 1253,
4487
4519
  "column": 9
4488
4520
  }
4489
4521
  },
@@ -4497,11 +4529,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4497
4529
  result: {
4498
4530
  mode: "strict",
4499
4531
  typeSymbol: "dsh-plugin-shop#shop/installed:result",
4500
- schema: dsh_plugin_shop_shop_installed_result$schema
4532
+ schema: dsh_plugin_shop_shop_installed_result$schema,
4533
+ create: () => dsh_plugin_shop_shop_installed_result$schema
4501
4534
  },
4502
4535
  sourceLocation: {
4503
4536
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4504
- "line": 1140,
4537
+ "line": 1720,
4505
4538
  "column": 9
4506
4539
  }
4507
4540
  },
@@ -4515,11 +4548,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4515
4548
  result: {
4516
4549
  mode: "strict",
4517
4550
  typeSymbol: "dsh-plugin-shop#shop/installedSpecs:result",
4518
- schema: dsh_plugin_shop_shop_installedSpecs_result$schema
4551
+ schema: dsh_plugin_shop_shop_installedSpecs_result$schema,
4552
+ create: () => dsh_plugin_shop_shop_installedSpecs_result$schema
4519
4553
  },
4520
4554
  sourceLocation: {
4521
4555
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4522
- "line": 1131,
4556
+ "line": 1711,
4523
4557
  "column": 9
4524
4558
  }
4525
4559
  },
@@ -4537,17 +4571,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4537
4571
  codec: {
4538
4572
  mode: "strict",
4539
4573
  typeSymbol: "dsh-plugin-shop/types#InstallArgs",
4540
- schema: dsh_plugin_shop_shop_installStart_parameter_0$schema
4574
+ schema: dsh_plugin_shop_shop_installStart_parameter_0$schema,
4575
+ create: () => dsh_plugin_shop_shop_installStart_parameter_0$schema
4541
4576
  }
4542
4577
  }],
4543
4578
  result: {
4544
4579
  mode: "strict",
4545
4580
  typeSymbol: "dsh-plugin-shop/types#ShopInstallResult",
4546
- schema: dsh_plugin_shop_shop_installStart_result$schema
4581
+ schema: dsh_plugin_shop_shop_installStart_result$schema,
4582
+ create: () => dsh_plugin_shop_shop_installStart_result$schema
4547
4583
  },
4548
4584
  sourceLocation: {
4549
4585
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4550
- "line": 915,
4586
+ "line": 1379,
4551
4587
  "column": 9
4552
4588
  }
4553
4589
  },
@@ -4564,17 +4600,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4564
4600
  codec: {
4565
4601
  mode: "strict",
4566
4602
  typeSymbol: "dsh-plugin-shop#shop/installStatus:args",
4567
- schema: dsh_plugin_shop_shop_installStatus_parameter_0$schema
4603
+ schema: dsh_plugin_shop_shop_installStatus_parameter_0$schema,
4604
+ create: () => dsh_plugin_shop_shop_installStatus_parameter_0$schema
4568
4605
  }
4569
4606
  }],
4570
4607
  result: {
4571
4608
  mode: "strict",
4572
4609
  typeSymbol: "dsh-plugin-shop/types#ShopInstallStatusResult",
4573
- schema: dsh_plugin_shop_shop_installStatus_result$schema
4610
+ schema: dsh_plugin_shop_shop_installStatus_result$schema,
4611
+ create: () => dsh_plugin_shop_shop_installStatus_result$schema
4574
4612
  },
4575
4613
  sourceLocation: {
4576
4614
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4577
- "line": 1107,
4615
+ "line": 1687,
4578
4616
  "column": 3
4579
4617
  }
4580
4618
  },
@@ -4588,11 +4626,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4588
4626
  result: {
4589
4627
  mode: "strict",
4590
4628
  typeSymbol: "dsh-plugin-shop/types#ShopRestartResult",
4591
- schema: dsh_plugin_shop_shop_restart_result$schema
4629
+ schema: dsh_plugin_shop_shop_restart_result$schema,
4630
+ create: () => dsh_plugin_shop_shop_restart_result$schema
4592
4631
  },
4593
4632
  sourceLocation: {
4594
4633
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4595
- "line": 1319,
4634
+ "line": 1916,
4596
4635
  "column": 9
4597
4636
  }
4598
4637
  },
@@ -4609,17 +4648,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4609
4648
  codec: {
4610
4649
  mode: "strict",
4611
4650
  typeSymbol: "dsh-plugin-shop#shop/setEnabled:args",
4612
- schema: dsh_plugin_shop_shop_setEnabled_parameter_0$schema
4651
+ schema: dsh_plugin_shop_shop_setEnabled_parameter_0$schema,
4652
+ create: () => dsh_plugin_shop_shop_setEnabled_parameter_0$schema
4613
4653
  }
4614
4654
  }],
4615
4655
  result: {
4616
4656
  mode: "strict",
4617
4657
  typeSymbol: "dsh-plugin-shop/types#ShopSetEnabledResult",
4618
- schema: dsh_plugin_shop_shop_setEnabled_result$schema
4658
+ schema: dsh_plugin_shop_shop_setEnabled_result$schema,
4659
+ create: () => dsh_plugin_shop_shop_setEnabled_result$schema
4619
4660
  },
4620
4661
  sourceLocation: {
4621
4662
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4622
- "line": 692,
4663
+ "line": 924,
4623
4664
  "column": 9
4624
4665
  }
4625
4666
  },
@@ -4637,17 +4678,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4637
4678
  codec: {
4638
4679
  mode: "strict",
4639
4680
  typeSymbol: "dsh-plugin-shop#shop/uninstallStart:args",
4640
- schema: dsh_plugin_shop_shop_uninstallStart_parameter_0$schema
4681
+ schema: dsh_plugin_shop_shop_uninstallStart_parameter_0$schema,
4682
+ create: () => dsh_plugin_shop_shop_uninstallStart_parameter_0$schema
4641
4683
  }
4642
4684
  }],
4643
4685
  result: {
4644
4686
  mode: "strict",
4645
4687
  typeSymbol: "dsh-plugin-shop/types#ShopUninstallResult",
4646
- schema: dsh_plugin_shop_shop_uninstallStart_result$schema
4688
+ schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
4689
+ create: () => dsh_plugin_shop_shop_uninstallStart_result$schema
4647
4690
  },
4648
4691
  sourceLocation: {
4649
4692
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4650
- "line": 1241,
4693
+ "line": 1827,
4651
4694
  "column": 9
4652
4695
  }
4653
4696
  },
@@ -4664,17 +4707,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4664
4707
  codec: {
4665
4708
  mode: "strict",
4666
4709
  typeSymbol: "dsh-plugin-shop#shop/updateStart:args",
4667
- schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema
4710
+ schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema,
4711
+ create: () => dsh_plugin_shop_shop_updateStart_parameter_0$schema
4668
4712
  }
4669
4713
  }],
4670
4714
  result: {
4671
4715
  mode: "strict",
4672
4716
  typeSymbol: "dsh-plugin-shop/types#ShopUpdateResult",
4673
- schema: dsh_plugin_shop_shop_updateStart_result$schema
4717
+ schema: dsh_plugin_shop_shop_updateStart_result$schema,
4718
+ create: () => dsh_plugin_shop_shop_updateStart_result$schema
4674
4719
  },
4675
4720
  sourceLocation: {
4676
4721
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4677
- "line": 1387,
4722
+ "line": 2020,
4678
4723
  "column": 9
4679
4724
  }
4680
4725
  },
@@ -4688,11 +4733,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4688
4733
  result: {
4689
4734
  mode: "strict",
4690
4735
  typeSymbol: "dsh-plugin-shop/types#ShopVersionResult",
4691
- schema: dsh_plugin_shop_shop_version_result$schema
4736
+ schema: dsh_plugin_shop_shop_version_result$schema,
4737
+ create: () => dsh_plugin_shop_shop_version_result$schema
4692
4738
  },
4693
4739
  sourceLocation: {
4694
4740
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4695
- "line": 1371,
4741
+ "line": 1978,
4696
4742
  "column": 9
4697
4743
  }
4698
4744
  }
@@ -6290,7 +6336,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6290
6336
  case "host-unsupported": return "hotHostUnsupportedNotice";
6291
6337
  case "timeout": return "hotTimeoutNotice";
6292
6338
  case "mount-failed": return "hotMountFailedNotice";
6293
- case "client-half": return "hotClientHalfNotice";
6339
+ case "already-loaded": return "hotAlreadyLoadedNotice";
6294
6340
  default: return "installedRestartNotice";
6295
6341
  }
6296
6342
  }
@@ -6326,6 +6372,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6326
6372
  * could not have helped. */
6327
6373
  function restartBlockedNoticeKey(reason) {
6328
6374
  switch (reason) {
6375
+ case "desktop": return "restartBlockedDesktopNotice";
6329
6376
  case "windows": return "restartBlockedWindowsNotice";
6330
6377
  case "systemd": return "restartBlockedSystemdNotice";
6331
6378
  case "port-zero": return "restartBlockedPortZeroNotice";
@@ -6346,6 +6393,105 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6346
6393
  function missingPeersOf(incompatible, key) {
6347
6394
  return incompatible[key] ?? [];
6348
6395
  }
6396
+ /** The author's own `dsh.compatibility` that this installation does not meet
6397
+ * (design 2026-09-01 §8.2), or undefined when the host gave no verdict. Keyed
6398
+ * by install identity, never by name, for the reason `missingPeersOf` is.
6399
+ *
6400
+ * The map itself may be absent, whatever the result type says: a tab can
6401
+ * outlive a shop self-update by one reload and then be talking to the OLD
6402
+ * in-process host until dsh restarts, which answers the shape from before the
6403
+ * key existed. That is "no verdict", never a crash. */
6404
+ function harnessVerdictOf(verdicts, key) {
6405
+ return verdicts !== void 0 && Object.hasOwn(verdicts, key) ? verdicts[key] : void 0;
6406
+ }
6407
+ /**
6408
+ * Everything standing in one entry's way, in the order every surface renders
6409
+ * it — the card's detail lines, the badge's accessible name, the outdated
6410
+ * row's refusal — and the one list the incompatible filter reads, through
6411
+ * `readsIncompatible`, and the install button, through `refusesInstall`.
6412
+ *
6413
+ * The refusals read first. The name conflict leads: it is about a plugin the
6414
+ * reader chose and would lose. Then dsh's own refusal, then what this machine
6415
+ * is measured to lack, then what the author declared, range before profiles —
6416
+ * the order the host's verdict carries its halves.
6417
+ */
6418
+ function blockersOf(missing, harness, nameTakenBy) {
6419
+ const blockers = [];
6420
+ if (nameTakenBy !== void 0) blockers.push({
6421
+ kind: "name-taken",
6422
+ holder: nameTakenBy
6423
+ });
6424
+ if (harness?.peers !== void 0) {
6425
+ const { refused, running, allowCommand } = harness.peers;
6426
+ blockers.push({
6427
+ kind: "harness-peers",
6428
+ refused,
6429
+ running,
6430
+ allowCommand
6431
+ });
6432
+ }
6433
+ if (missing.length > 0) blockers.push({
6434
+ kind: "missing-peers",
6435
+ modules: missing
6436
+ });
6437
+ if (harness?.dsh !== void 0) blockers.push({
6438
+ kind: "harness-range",
6439
+ range: harness.dsh.range,
6440
+ running: harness.dsh.running
6441
+ });
6442
+ if (harness?.profile !== void 0) blockers.push({
6443
+ kind: "harness-profile",
6444
+ declared: harness.profile.declared,
6445
+ running: harness.profile.running
6446
+ });
6447
+ return blockers;
6448
+ }
6449
+ /**
6450
+ * The badge's visible word for these blockers, or null when there are none.
6451
+ *
6452
+ * "Incompatible" for anything about this harness, whichever reason put it
6453
+ * there. A taken name is the more serious condition and names a different
6454
+ * remedy, so it decides the word whenever it holds — the card is perfectly
6455
+ * compatible, and a different plugin is in the way.
6456
+ */
6457
+ function blockerBadgeKey(blockers) {
6458
+ if (blockers.length === 0) return null;
6459
+ return blockers.some((blocker) => blocker.kind === "name-taken") ? "nameTakenBadge" : "incompatibleBadge";
6460
+ }
6461
+ /** Whether the badge reads "Incompatible" — exactly the set the incompatible
6462
+ * filter counts and takes away. Derived from the badge's own rule, never
6463
+ * restated beside it: the filter used to test the peer list by hand, which
6464
+ * held only while missing peers were the one reason for the word — a card
6465
+ * badged for its declared range would have read "Incompatible" and survived
6466
+ * the filter that names it. */
6467
+ function readsIncompatible(blockers) {
6468
+ return blockerBadgeKey(blockers) === "incompatibleBadge";
6469
+ }
6470
+ /**
6471
+ * Whether this install is refused before it starts, so its button is
6472
+ * disabled rather than opening the acknowledgement gate for an install that
6473
+ * cannot proceed: by the host, for a taken name, or by the running dsh, for
6474
+ * its harness peers.
6475
+ *
6476
+ * The second is the one harness reason that blocks (design
6477
+ * 2026-09-26-dsh-017-readiness, B1, amending "warn, never block"): dsh 0.1.7
6478
+ * refuses the install itself, before pnpm runs, and the shop cannot install
6479
+ * past it. The verdict is dsh's own rule on the profile's own exemptions, so
6480
+ * a disabled button is dsh's answer given early, never the shop's opinion —
6481
+ * and recording the exemption the card shows is what enables it again.
6482
+ */
6483
+ function refusesInstall(blockers) {
6484
+ return blockers.some((blocker) => blocker.kind === "name-taken" || blocker.kind === "harness-peers");
6485
+ }
6486
+ /**
6487
+ * The refused peers as the refusal's copy names them: each peer and the range
6488
+ * the package declared, in the order dsh reported them. A blank range — which
6489
+ * dsh refuses — is quoted, or the sentence would name the peer and nothing
6490
+ * it asked for.
6491
+ */
6492
+ function refusedPeersText(refused) {
6493
+ return Object.entries(refused).map(([name, range]) => `${name} ${range.trim() === "" ? JSON.stringify(range) : range}`).join(", ");
6494
+ }
6349
6495
  /**
6350
6496
  * The plugin already holding this entry's bundle name, if a DIFFERENT one is,
6351
6497
  * named the way the host's refusal names it.
@@ -6384,6 +6530,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6384
6530
  case "tarball-integrity": return "tarballIntegrityCode";
6385
6531
  case "ambiguous-identity": return "ambiguousIdentityCode";
6386
6532
  case "name-taken": return "nameTakenCode";
6533
+ case "desktop-profile": return "desktopProfileCode";
6387
6534
  }
6388
6535
  }
6389
6536
  /** The version a card displays: full for npm, the short commit for github. */
@@ -6423,6 +6570,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6423
6570
  }
6424
6571
  /** §7.2 once-per-second poll cadence, as a named constant. */
6425
6572
  const INSTALL_POLL_MS = 1e3;
6573
+ /**
6574
+ * The restart monitor's decision after a probe of the origin.
6575
+ *
6576
+ * `stableSinceMs` is when the current unbroken run of good answers began —
6577
+ * null when the last probe failed, or none has succeeded yet — and both times
6578
+ * count from the moment the restart was committed. One answer is not enough:
6579
+ * reloading on it is what landed a reader on a boot that died a moment later,
6580
+ * a blank page in place of the notice naming the log. So the page reloads
6581
+ * only once the run has lasted `RESTART_STABLE_MS`, and the monitor gives up
6582
+ * only when no run is under way past `RESTART_WAIT_MS` — which bounds the
6583
+ * whole wait at the sum of the two, since a run that began in time either
6584
+ * completes or breaks after the deadline.
6585
+ */
6586
+ function restartMonitorVerdict(input) {
6587
+ if (input.stableSinceMs !== null && input.elapsedMs - input.stableSinceMs >= 8e3) return "reload";
6588
+ if (input.stableSinceMs === null && input.elapsedMs > 3e4) return "failed";
6589
+ return "wait";
6590
+ }
6426
6591
  /** How long the check-update button reports "up to date" after a re-check
6427
6592
  * finds no newer release, before reverting to its idle label. */
6428
6593
  const CHECK_UP_TO_DATE_MS = 3e3;
@@ -6465,7 +6630,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6465
6630
  kind: "done",
6466
6631
  activation: status.activation ?? "restart",
6467
6632
  log: status.log,
6468
- ...status.restartReason !== void 0 ? { restartReason: status.restartReason } : {}
6633
+ ...status.restartReason !== void 0 ? { restartReason: status.restartReason } : {},
6634
+ ...status.detail !== void 0 ? { detail: status.detail } : {}
6469
6635
  };
6470
6636
  return {
6471
6637
  kind: "failed",
@@ -6680,6 +6846,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6680
6846
  error: "暂时无法读取目录。",
6681
6847
  installedError: "暂时无法读取已安装列表。",
6682
6848
  retry: "重试",
6849
+ /** The shop's own error boundary caught a render error; the error's message
6850
+ * follows on its own line, and Retry remounts the tab (design
6851
+ * 2026-09-26-market-borrowings §4). */
6852
+ tabCrashed: "插件商店在显示这一页时出错了。",
6683
6853
  search: "搜索插件",
6684
6854
  all: "全部",
6685
6855
  catalog: "插件目录",
@@ -6709,12 +6879,29 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6709
6879
  installedRestartNotice: "已安装;重启 dsh 后生效",
6710
6880
  incompatibleBadge: "不兼容",
6711
6881
  incompatibleDetail: "缺少组件/插件:{modules}\n此插件可能是为其他版本的 DSH 设计的。",
6882
+ /** The author's own `dsh.compatibility`, unmet on this machine (design
6883
+ * 2026-09-01 §8.2). Each names BOTH sides — what the author declared and
6884
+ * what this dsh runs — because the host sends both, and a bare
6885
+ * "unsupported" leaves the reader guessing which side is off. */
6886
+ harnessRangeDetail: "作者声明支持的 dsh 版本:{range};本机运行的是 {running}。",
6887
+ harnessProfileDetail: "作者声明支持的 profile:{declared};此 dsh 以 {running} profile 启动。",
6888
+ /** dsh's OWN refusal (0.1.7 on), not the author's claim: the install button
6889
+ * is disabled beside it (design 2026-09-26-dsh-017-readiness, B1). `{peers}`
6890
+ * is each refused peer with the range the package declared. The remedy
6891
+ * leads into the exemption command, rendered on its own line after it, and
6892
+ * names the Refresh button by its label. */
6893
+ harnessPeersDetail: "本机的 dsh {running} 会拒绝安装此插件:它要求 {peers}。",
6894
+ harnessPeersRemedy: "若愿意承担崩溃或数据丢失的风险,可在终端运行以下命令,为这个确切版本授予豁免,然后点击“刷新”:",
6712
6895
  hotNoPatchNotice: "该插件没有可热挂载的补丁文件;重启 dsh 后生效",
6713
6896
  hotNotSimpleNotice: "该插件的补丁包含无法热挂载的配置;重启 dsh 后生效",
6714
6897
  hotHostUnsupportedNotice: "当前环境不支持热挂载;重启 dsh 后生效",
6715
6898
  hotTimeoutNotice: "热挂载超时;重启 dsh 后生效",
6716
6899
  hotMountFailedNotice: "热挂载失败;重启 dsh 后生效",
6717
- hotClientHalfNotice: "已安装并在运行;它的界面要重启 dsh 才会更新——刷新页面取不到",
6900
+ /** No mount was tried: this dsh already loaded the package once, and Node
6901
+ * will not load a second copy from the same place (design
6902
+ * 2026-09-26-market-borrowings §1). Covers an update and a reinstall alike,
6903
+ * so it names the new files rather than a "new version". */
6904
+ hotAlreadyLoadedNotice: "已安装;这次运行的 dsh 已经加载过这个插件,新装的文件要重启 dsh 后才会生效",
6718
6905
  installedNoRestartNotice: "已安装并热挂载;现在即可使用,无需重启",
6719
6906
  installedReloadNotice: "已生效;刷新页面即可看到",
6720
6907
  reload: "刷新页面",
@@ -6739,9 +6926,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6739
6926
  /** The incompatible filter's label. FIXED — the switch beside it carries the
6740
6927
  * state, so the words name what the switch does and never flip with it. */
6741
6928
  hideIncompatible: "隐藏不兼容 {count}",
6742
- incompatibleFilterTitle: "不显示缺少组件的插件",
6743
- showIncompatibleTitle: "把缺少组件的插件放回列表",
6744
- emptyIncompatibleFiltered: "匹配的插件都缺少组件,已被「隐藏不兼容」筛掉。",
6929
+ /** What the filter takes away is every card whose badge reads 不兼容 —
6930
+ * missing components OR an author's declaration this dsh does not meet —
6931
+ * so these three name the badge, not one of its reasons. */
6932
+ incompatibleFilterTitle: "不显示标为不兼容的插件",
6933
+ showIncompatibleTitle: "把标为不兼容的插件放回列表",
6934
+ emptyIncompatibleFiltered: "匹配的插件都标为不兼容,已被「隐藏不兼容」筛掉。",
6745
6935
  license: "许可",
6746
6936
  customLicense: "自定义许可",
6747
6937
  reviewedVersionLine: "已审核 v{reviewed} / 当前 v{current} 未审核",
@@ -6753,6 +6943,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6753
6943
  tarballIntegrityCode: "压缩包完整性校验失败",
6754
6944
  ambiguousIdentityCode: "无法确定是哪一个条目",
6755
6945
  nameTakenCode: "同名的另一个插件已安装",
6946
+ desktopProfileCode: "由桌面版管理的 profile",
6756
6947
  nameTakenBadge: "名称被占用",
6757
6948
  nameTakenDetail: "已安装同名的另一个插件({holder})。\n两个同名插件声明相同的加载项 id,无法共存;安装它会替换掉已装的那个。",
6758
6949
  npmSource: "npm 包",
@@ -6775,7 +6966,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6775
6966
  restartConfirm: "重启",
6776
6967
  restarting: "正在重启,服务器就绪后页面会自动刷新…",
6777
6968
  restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
6969
+ /** The same failure, when the host said where the new process writes —
6970
+ * which is where a boot that died explains itself (design
6971
+ * 2026-09-26-market-borrowings §3). "Or did not stay up": the page now
6972
+ * waits for the new server to keep answering, so this also covers a boot
6973
+ * that answered and then died. */
6974
+ restartFailedLogNotice: "重启后服务器没有回来,或回来后没能保持运行。新进程的日志在 {log};也可以手动启动:dsh web",
6778
6975
  restartTransportFailed: "重启请求未能送达。请稍后重试。",
6976
+ restartBlockedDesktopNotice: "此 profile 由 DeepSeek Harness 桌面版管理,商店无法重启它。请重启桌面版,以应用更改。",
6779
6977
  restartBlockedWindowsNotice: "商店暂不支持在 Windows 上重启 dsh。请退出 dsh 后重新启动,以应用更改。",
6780
6978
  restartBlockedSystemdNotice: "当前 dsh 由 systemd 服务托管,商店无法安全地重启它。请手动重启服务,或在商店行配置中设置 allowRestart: true。",
6781
6979
  restartBlockedPortZeroNotice: "此 dsh 以 --port 0 启动,重启后会绑定到另一个端口,本页面无法跟上。请手动重启 dsh,以应用更改。",
@@ -6787,7 +6985,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6787
6985
  github: "GitHub",
6788
6986
  enabledSwitch: "启用",
6789
6987
  toggleFailed: "设置失败,请重试。",
6790
- hotApplyNote: "已生效,无需重启"
6988
+ hotApplyNote: "已生效,无需重启",
6989
+ toggleRestartNote: "已保存,重启 dsh 后生效"
6791
6990
  };
6792
6991
  /** English dictionary checked against the Chinese key set. */
6793
6992
  const en = {
@@ -6796,6 +6995,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6796
6995
  error: "The catalog is temporarily unavailable.",
6797
6996
  installedError: "The installed list is temporarily unavailable.",
6798
6997
  retry: "Retry",
6998
+ tabCrashed: "The plugin shop hit an error while showing this page.",
6799
6999
  search: "Search plugins",
6800
7000
  all: "All",
6801
7001
  catalog: "Plugin catalog",
@@ -6825,12 +7025,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6825
7025
  installedRestartNotice: "installed; restart dsh to activate",
6826
7026
  incompatibleBadge: "Incompatible",
6827
7027
  incompatibleDetail: "Missing components/plugins: {modules}\nThis plugin may be designed for another DSH version.",
7028
+ harnessRangeDetail: "The author declares support for dsh {range}; this machine runs {running}.",
7029
+ harnessProfileDetail: "The author declares support for the {declared} profile(s); this dsh was launched with the {running} profile.",
7030
+ harnessPeersDetail: "dsh {running} on this machine refuses to install this plugin: it requires {peers}.",
7031
+ harnessPeersRemedy: "To accept the risk of crashes or data loss for this exact version, run this in a terminal, then press Refresh:",
6828
7032
  hotNoPatchNotice: "no patch file to hot-mount; restart dsh to activate",
6829
7033
  hotNotSimpleNotice: "the patch has config rows that cannot be hot-mounted; restart dsh to activate",
6830
7034
  hotHostUnsupportedNotice: "this harness cannot hot-mount; restart dsh to activate",
6831
7035
  hotTimeoutNotice: "hot-mount timed out; restart dsh to activate",
6832
7036
  hotMountFailedNotice: "hot-mount failed; restart dsh to activate",
6833
- hotClientHalfNotice: "installed and running; its interface needs a restart of dsh — a reload cannot fetch it",
7037
+ hotAlreadyLoadedNotice: "installed; this dsh has already loaded the plugin once, so the files just installed take effect after a restart of dsh",
6834
7038
  installedNoRestartNotice: "installed and hot-mounted; running now, no restart needed",
6835
7039
  installedReloadNotice: "already applied; reload the page to see it",
6836
7040
  reload: "Reload the page",
@@ -6847,9 +7051,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6847
7051
  authorLine: "Author: {author}",
6848
7052
  sizeLabel: "{size} on disk",
6849
7053
  hideIncompatible: "Hide incompatible {count}",
6850
- incompatibleFilterTitle: "Leave out plugins whose components are missing",
6851
- showIncompatibleTitle: "Put plugins whose components are missing back on the shelf",
6852
- emptyIncompatibleFiltered: "Every matching plugin is missing components, and the incompatible filter is leaving them out.",
7054
+ incompatibleFilterTitle: "Leave out plugins marked incompatible with this dsh",
7055
+ showIncompatibleTitle: "Put plugins marked incompatible back on the shelf",
7056
+ emptyIncompatibleFiltered: "Every matching plugin is marked incompatible, and the incompatible filter is leaving them out.",
6853
7057
  license: "License",
6854
7058
  customLicense: "Custom license",
6855
7059
  reviewedVersionLine: "reviewed v{reviewed} / current v{current} unreviewed",
@@ -6861,6 +7065,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6861
7065
  tarballIntegrityCode: "tarball integrity check failed",
6862
7066
  ambiguousIdentityCode: "Ambiguous entry",
6863
7067
  nameTakenCode: "Another plugin holds this name",
7068
+ desktopProfileCode: "Managed by the desktop app",
6864
7069
  nameTakenBadge: "Name taken",
6865
7070
  nameTakenDetail: "A different plugin of this name is installed ({holder}).\nTwo plugins of one name declare the same loader entry id and cannot both load; installing this one would replace it.",
6866
7071
  npmSource: "npm package",
@@ -6883,7 +7088,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6883
7088
  restartConfirm: "Restart",
6884
7089
  restarting: "Restarting; the page will refresh once the server is back…",
6885
7090
  restartFailedNotice: "The server did not come back after the restart. Start it manually: dsh web (see restart.log in the shop cache directory)",
7091
+ restartFailedLogNotice: "The server did not come back after the restart, or did not stay up. The new process logs to {log}; you can also start it manually: dsh web",
6886
7092
  restartTransportFailed: "The restart request could not be delivered. Please retry.",
7093
+ restartBlockedDesktopNotice: "This profile is managed by the DeepSeek Harness desktop app, so the shop cannot restart it. Restart the app to apply the change.",
6887
7094
  restartBlockedWindowsNotice: "The shop cannot restart dsh on Windows yet. Quit dsh and start it again to apply the change.",
6888
7095
  restartBlockedSystemdNotice: "This dsh process runs as a systemd service, so the shop cannot restart it safely. Restart the service manually, or set allowRestart: true in the shop row config.",
6889
7096
  restartBlockedPortZeroNotice: "This dsh was launched with --port 0, so a restart would bind a different port and this page could not follow it. Restart dsh manually to apply the change.",
@@ -6895,9 +7102,409 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6895
7102
  github: "GitHub",
6896
7103
  enabledSwitch: "Enable",
6897
7104
  toggleFailed: "Failed to update. Please retry.",
6898
- hotApplyNote: "applied without a restart"
7105
+ hotApplyNote: "applied without a restart",
7106
+ toggleRestartNote: "saved; it takes effect after dsh restarts"
6899
7107
  };
6900
7108
  //#endregion
7109
+ //#region src/client/module-table.ts
7110
+ /**
7111
+ * How long one probe may take before its answer counts as unknown.
7112
+ *
7113
+ * The work a probe can actually reach is a microtask: a seed-word lookup, or a
7114
+ * factory this page already registered. So this bounds a harness that never
7115
+ * answers, not work — and it exists because the cost of the alternative is the
7116
+ * whole shelf. `refineAgainstModuleTable` promises that a table in any state
7117
+ * cannot cost the reader the catalog, and an `import()` that never settles
7118
+ * would have made that promise false by leaving the tab with nothing to render
7119
+ * (§9's degradation rule: an unavailable fact is silence, never an accusation).
7120
+ * A probe per name, not one deadline for the whole refinement: every other
7121
+ * name is still judged, and the tab waits at most this long however many names
7122
+ * there are.
7123
+ */
7124
+ const MODULE_PROBE_TIMEOUT_MS = 2e3;
7125
+ /** `outcome`, or null once `ms` has passed — whichever settles first. The
7126
+ * timer is cleared either way, so a settled probe leaves nothing pending. */
7127
+ function withDeadline(outcome, ms) {
7128
+ let timer;
7129
+ const deadline = new Promise((resolve) => {
7130
+ timer = setTimeout(() => resolve(null), ms);
7131
+ });
7132
+ return Promise.race([outcome, deadline]).finally(() => {
7133
+ if (timer !== void 0) clearTimeout(timer);
7134
+ });
7135
+ }
7136
+ /**
7137
+ * The host's verdict less every name the oracle provides.
7138
+ *
7139
+ * An entry whose list empties is dropped, and an entry ANY of whose names
7140
+ * comes back unknown gets no verdict at all — the rule `incompatibilityMap`
7141
+ * follows on the host, for the host's reason: an unavailable fact must never
7142
+ * read as an accusation, and keeping the rest of a list whose one unknown name
7143
+ * was dropped would publish a partial answer as the whole one. Each distinct
7144
+ * name is asked once, however many entries declare it. Never rejects: any
7145
+ * failure of the oracle is an unknown.
7146
+ */
7147
+ async function refineIncompatible(incompatible, provides, timeoutMs = MODULE_PROBE_TIMEOUT_MS) {
7148
+ const answers = /* @__PURE__ */ new Map();
7149
+ const ask = (spec) => {
7150
+ const cached = answers.get(spec);
7151
+ if (cached !== void 0) return cached;
7152
+ const answer = withDeadline(new Promise((resolve) => {
7153
+ resolve(provides(spec));
7154
+ }).catch(() => null), timeoutMs);
7155
+ answers.set(spec, answer);
7156
+ return answer;
7157
+ };
7158
+ const judged = await Promise.all(Object.entries(incompatible).map(async ([key, names]) => {
7159
+ const verdicts = await Promise.all(names.map(ask));
7160
+ if (verdicts.some((verdict) => verdict !== true && verdict !== false)) return null;
7161
+ const missing = names.filter((_, index) => verdicts[index] === false);
7162
+ return missing.length > 0 ? {
7163
+ key,
7164
+ missing
7165
+ } : null;
7166
+ }));
7167
+ const refined = {};
7168
+ for (const row of judged) if (row !== null) refined[row.key] = row.missing;
7169
+ return refined;
7170
+ }
7171
+ /** The harness's own normalization (`stripClientSuffix`): a plugin bundle IS
7172
+ * its package's client half, so `x/client` and `x` name one row and one
7173
+ * record. Mirrored rather than imported — this package does not depend on
7174
+ * `@deepseek-ai/dsh-client-modules`. */
7175
+ function stripClientSuffix(spec) {
7176
+ return spec.endsWith("/client") ? spec.slice(0, -7) : spec;
7177
+ }
7178
+ /** The prefix of the one rejection that means "absent":
7179
+ * `ClientModuleSystem.import`'s own `client-modules: cannot resolve "<spec>" —
7180
+ * not a seed word, not a materialized module, and not a row in the boot
7181
+ * graph`. A harness that rewords it makes every probe unknown, which renders
7182
+ * as silence — the safe direction, never a false accusation. */
7183
+ const CANNOT_RESOLVE = "client-modules: cannot resolve";
7184
+ /**
7185
+ * An oracle over the live `modules` service, or null when the page offers no
7186
+ * usable table.
7187
+ *
7188
+ * The service is the harness's `ClientModuleLoader`, published to client
7189
+ * plugins as `ctx.reflect.provide('modules', …)`. It arrives as unknown-typed
7190
+ * foreign state, so only its PUBLIC contract is read, and only after
7191
+ * narrowing: `manifest.modules` must be an array, `loadCache` a Map and
7192
+ * `import` a function. `seed`, `factories` and `graphRows` exist on the
7193
+ * instance too, but they are TypeScript-`private` in the harness's own
7194
+ * declarations, and nothing here names them.
7195
+ *
7196
+ * Per spec, in the table's own order but with every side effect kept out:
7197
+ * 1. A graph row provides it, and `import` is NOT called — for a row it
7198
+ * fetches the bundle and runs the module body, and a probe must never be
7199
+ * what loads a plugin. `manifest.modules` is the row index, but it is not
7200
+ * fixed for the page's whole life on every harness build. On 0.1.5-rc.3
7201
+ * `this.manifest = options.manifest` is the constructor's only write, so a
7202
+ * row placed at boot stands unchanged for as long as the page lives. On
7203
+ * 0.1.7-rc.2 (`next`) `updateManifest` REPLACES `this.manifest` outright
7204
+ * with a new manifest that no longer lists an uninstalled package, and
7205
+ * `reconcile` removes the plugin from the loader — so UNTIL this page's own
7206
+ * uninstall reconciles, the OLD row still stands in `manifest.modules` and
7207
+ * this step clears the entry falsely; AFTER it reconciles, this step can no
7208
+ * longer be the false clear, because `manifest.modules` has already dropped
7209
+ * the package. The false clear then comes from `prune`, which keeps the
7210
+ * removed id only in the PRIVATE `graphRows` and in `loadCache`, and only
7211
+ * while some retained module still references it — that is step 2 below,
7212
+ * or the side effect of `import` in step 3 (a private graph row this
7213
+ * function cannot see).
7214
+ * 2. A `loadCache` record provides it, again without `import`.
7215
+ * 3. Otherwise `import(spec, '', {})`, with the spec as written — the seed
7216
+ * holds `react-dom/client` itself, not its stripped form. Resolving means a
7217
+ * seed word, whose lookup has no side effect; rejecting with the
7218
+ * "cannot resolve" message means absent; any other rejection is unknown.
7219
+ *
7220
+ * A kept-but-unmaterialized row is this function's own remaining blind spot:
7221
+ * asked about here, it still reaches step 3's `import` probe, which fetches
7222
+ * and runs it — the one side effect this module promises never to cause on
7223
+ * its own account. `refineAgainstModuleTable`'s page-removed set is the fix
7224
+ * for the one case a page can know about on its own — a name uninstalled
7225
+ * through THIS shop, on THIS page, since it loaded — by bypassing this oracle
7226
+ * for that name entirely rather than trusting a row that may only be kept. An
7227
+ * uninstall made from another tab, another window, or the CLI is not in that
7228
+ * set and stays unknown to this page until it reloads.
7229
+ *
7230
+ * One side-effect path remains: a registered page-local factory that is
7231
+ * neither a graph row nor yet materialized, which `import` materializes. That
7232
+ * is reachable only for a name the host could not resolve AND the page
7233
+ * registered without a row — the shell's own page-local modules are
7234
+ * node-resolvable, so the host never lists them. And what it does is what the
7235
+ * first real `require` of that name does anyway: run a factory this page
7236
+ * registered precisely so it would be materialized, once, memoized in
7237
+ * `loadCache`, fetching nothing. It moves that moment earlier; it runs nothing
7238
+ * the page did not already hold.
7239
+ */
7240
+ function moduleTableOracle(service) {
7241
+ if (typeof service !== "object" || service === null) return null;
7242
+ let rows;
7243
+ let loadCache;
7244
+ let probe;
7245
+ try {
7246
+ const { manifest, loadCache: cache, import: load } = service;
7247
+ const modules = typeof manifest === "object" && manifest !== null ? manifest.modules : void 0;
7248
+ if (!Array.isArray(modules) || !(cache instanceof Map) || typeof load !== "function") return null;
7249
+ rows = /* @__PURE__ */ new Set();
7250
+ for (const row of modules) {
7251
+ const id = typeof row === "object" && row !== null ? row.id : void 0;
7252
+ if (typeof id === "string") rows.add(id);
7253
+ }
7254
+ loadCache = cache;
7255
+ probe = (spec) => Promise.resolve(load.call(service, spec, "", {}));
7256
+ } catch {
7257
+ return null;
7258
+ }
7259
+ return async (spec) => {
7260
+ const id = stripClientSuffix(spec);
7261
+ if (rows.has(id) || loadCache.has(id)) return true;
7262
+ try {
7263
+ await probe(spec);
7264
+ return true;
7265
+ } catch (error) {
7266
+ return error instanceof Error && error.message.startsWith(CANNOT_RESOLVE) ? false : null;
7267
+ }
7268
+ };
7269
+ }
7270
+ /**
7271
+ * The host's verdict as this page can stand behind it: refined against the
7272
+ * live table, or `{}` — no verdicts at all — when there is no usable table.
7273
+ *
7274
+ * Never the host's list unrefined. Measured, that list is majority-false on
7275
+ * this harness line (see the header), so showing it whenever the table cannot
7276
+ * be read would hand the reader a verdict already known to be unreliable;
7277
+ * silence is this project's documented degradation — an unavailable fact must
7278
+ * never read as an accusation. Never rejects, so a table in any state cannot
7279
+ * cost the reader the catalog.
7280
+ *
7281
+ * `removed` is the page-removed set (design 2026-09-01-harness-compatibility
7282
+ * section 9.1): package names this page has uninstalled since it loaded.
7283
+ * Every name in it is reported missing exactly
7284
+ * as the host said, WITHOUT ever reaching the table — not the rows check, not
7285
+ * `loadCache`, not `import` — because a row or a cache record can outlive a
7286
+ * restart-free uninstall (`moduleTableOracle`'s header), and the table's own
7287
+ * "provided" would be precisely the false clear this set exists to prevent.
7288
+ */
7289
+ async function refineAgainstModuleTable(incompatible, service, removed = /* @__PURE__ */ new Set(), timeoutMs = MODULE_PROBE_TIMEOUT_MS) {
7290
+ const oracle = moduleTableOracle(service);
7291
+ if (oracle === null) return {};
7292
+ const withRemoved = async (spec) => removed.has(stripClientSuffix(spec)) ? false : oracle(spec);
7293
+ return refineIncompatible(incompatible, withRemoved, timeoutMs);
7294
+ }
7295
+ //#endregion
7296
+ //#region src/client/restart-monitor.ts
7297
+ /** Registry symbol, so every module instance on the page finds the same store. */
7298
+ const STORE_KEY = Symbol.for("dsh-plugin-shop.restart-monitor");
7299
+ function monitorStore() {
7300
+ const page = globalThis;
7301
+ let stored = page[STORE_KEY];
7302
+ if (stored === void 0) {
7303
+ stored = {
7304
+ state: null,
7305
+ listeners: /* @__PURE__ */ new Set(),
7306
+ stop: null
7307
+ };
7308
+ page[STORE_KEY] = stored;
7309
+ }
7310
+ return Object.assign(stored, {
7311
+ carriers: stored.carriers ?? [],
7312
+ attempt: stored.attempt ?? null
7313
+ });
7314
+ }
7315
+ function settle(store, state) {
7316
+ store.state = state;
7317
+ for (const listener of [...store.listeners]) listener();
7318
+ }
7319
+ /** The page's restart, or null when it has committed none. The same object
7320
+ * until the state changes, as `useSyncExternalStore` requires. */
7321
+ function readRestartMonitor() {
7322
+ return monitorStore().state;
7323
+ }
7324
+ /** Be told when the page's restart changes state. */
7325
+ function subscribeRestartMonitor(listener) {
7326
+ const store = monitorStore();
7327
+ store.listeners.add(listener);
7328
+ return () => {
7329
+ store.listeners.delete(listener);
7330
+ };
7331
+ }
7332
+ /**
7333
+ * Watch the origin for the restarted server and reload into it: probe once
7334
+ * the grace period is over (the host exits within it, so an answer is the NEW
7335
+ * server), and reload only after it has KEPT answering for the stable window.
7336
+ * A boot that is about to fail can bind the port and answer before its plugin
7337
+ * tree is audited and it exits; reloading on that one answer left the reader
7338
+ * on a blank page instead of the notice naming the log (design
7339
+ * 2026-09-26-market-borrowings §3). `restartMonitorVerdict` decides; this
7340
+ * loop only probes. Probes never overlap: each is scheduled one poll interval
7341
+ * after the previous one settled.
7342
+ *
7343
+ * A restart already being watched is left alone — every offer on the page
7344
+ * renders this state, so a second press can only come from a panel that has
7345
+ * not re-rendered yet, and a second loop would probe the same origin twice.
7346
+ */
7347
+ function startRestartMonitor(options) {
7348
+ const store = monitorStore();
7349
+ if (store.state?.kind === "restarting") return;
7350
+ store.stop?.();
7351
+ const { reload, logFile } = options;
7352
+ const started = Date.now();
7353
+ let stableSince = null;
7354
+ let stopped = false;
7355
+ let timer;
7356
+ const probe = async () => {
7357
+ if (Date.now() - started >= 3e3) {
7358
+ let up = false;
7359
+ try {
7360
+ up = (await fetch(globalThis.location.href, { cache: "no-store" })).ok;
7361
+ } catch {}
7362
+ if (stopped) return;
7363
+ const elapsed = Date.now() - started;
7364
+ stableSince = up ? stableSince ?? elapsed : null;
7365
+ const verdict = restartMonitorVerdict({
7366
+ elapsedMs: elapsed,
7367
+ stableSinceMs: stableSince
7368
+ });
7369
+ if (verdict === "reload") {
7370
+ store.stop = null;
7371
+ reload();
7372
+ return;
7373
+ }
7374
+ if (verdict === "failed") {
7375
+ store.stop = null;
7376
+ settle(store, logFile === void 0 ? { kind: "failed" } : {
7377
+ kind: "failed",
7378
+ logFile
7379
+ });
7380
+ return;
7381
+ }
7382
+ }
7383
+ timer = setTimeout(() => {
7384
+ probe();
7385
+ }, INSTALL_POLL_MS);
7386
+ };
7387
+ store.stop = () => {
7388
+ stopped = true;
7389
+ if (timer !== void 0) clearTimeout(timer);
7390
+ };
7391
+ timer = setTimeout(() => {
7392
+ probe();
7393
+ }, INSTALL_POLL_MS);
7394
+ settle(store, logFile === void 0 ? { kind: "restarting" } : {
7395
+ kind: "restarting",
7396
+ logFile
7397
+ });
7398
+ }
7399
+ /**
7400
+ * Register one module instance's path to the host; the client half's apply
7401
+ * does, with the `restart` it hands the tab. The returned function withdraws
7402
+ * it, and has to run BEFORE the mount it rides is disposed: a request still in
7403
+ * flight on it is then asked again through the newest live path — at once if
7404
+ * one is registered, or as soon as one is — rather than waiting on a call the
7405
+ * dispose is about to abort.
7406
+ */
7407
+ function registerRestartCarrier(restart) {
7408
+ const store = monitorStore();
7409
+ const carrier = {
7410
+ restart,
7411
+ live: true
7412
+ };
7413
+ store.carriers.push(carrier);
7414
+ const attempt = store.attempt;
7415
+ if (attempt !== null && !attempt.carrier.live) ask(store, attempt, carrier);
7416
+ return () => {
7417
+ if (!carrier.live) return;
7418
+ carrier.live = false;
7419
+ const current = monitorStore();
7420
+ current.carriers = current.carriers.filter((each) => each !== carrier);
7421
+ const orphaned = current.attempt;
7422
+ const next = current.carriers.at(-1);
7423
+ if (orphaned !== null && orphaned.carrier === carrier && next !== void 0) ask(current, orphaned, next);
7424
+ };
7425
+ }
7426
+ /**
7427
+ * Ask the host to restart, for the page (design §8). The request is the
7428
+ * page's from the confirm on, not from the answer: every offer renders it as
7429
+ * under way, and a second press joins it rather than asking twice. It rides
7430
+ * the newest path a module instance registered, or — where no client half
7431
+ * registered one — the pressing tab's own `restart`.
7432
+ *
7433
+ * Resolves, never rejects, with what the press should show. A commit starts
7434
+ * the monitor and is `started`. A refusal and an unreachable host leave
7435
+ * nothing under way. A path withdrawn mid-call decides nothing but a commit;
7436
+ * the ask on the path that replaced it decides the rest, and that ask
7437
+ * finding no host is followed as a restart, since the first ask may have
7438
+ * committed and taken its host down with it.
7439
+ */
7440
+ function requestRestart(options) {
7441
+ const store = monitorStore();
7442
+ if (store.state?.kind === "restarting") return Promise.resolve({ kind: "started" });
7443
+ return new Promise((resolve) => {
7444
+ const pending = store.attempt;
7445
+ if (pending !== null) {
7446
+ pending.waiters.push(resolve);
7447
+ return;
7448
+ }
7449
+ const carrier = store.carriers.at(-1) ?? {
7450
+ restart: options.restart,
7451
+ live: true
7452
+ };
7453
+ const attempt = {
7454
+ carrier,
7455
+ reload: options.reload,
7456
+ reissued: false,
7457
+ waiters: [resolve]
7458
+ };
7459
+ store.attempt = attempt;
7460
+ settle(store, { kind: "requesting" });
7461
+ ask(store, attempt, carrier);
7462
+ });
7463
+ }
7464
+ /** Carry the request on one path. Its answer decides only while the request
7465
+ * is still the page's and that path still carries it — a commit excepted. */
7466
+ function ask(store, attempt, carrier) {
7467
+ if (attempt.carrier !== carrier) {
7468
+ attempt.carrier = carrier;
7469
+ attempt.reissued = true;
7470
+ }
7471
+ (async () => carrier.restart())().then((result) => {
7472
+ if (store.attempt !== attempt) return;
7473
+ if (result.ok) {
7474
+ finish(store, attempt, { kind: "started" });
7475
+ startRestartMonitor({
7476
+ reload: attempt.reload,
7477
+ ...typeof result.logFile === "string" ? { logFile: result.logFile } : {}
7478
+ });
7479
+ return;
7480
+ }
7481
+ if (superseded(attempt, carrier)) return;
7482
+ finish(store, attempt, {
7483
+ kind: "refused",
7484
+ detail: result.detail
7485
+ });
7486
+ settle(store, null);
7487
+ }, () => {
7488
+ if (store.attempt !== attempt || superseded(attempt, carrier)) return;
7489
+ if (attempt.reissued) {
7490
+ finish(store, attempt, { kind: "started" });
7491
+ startRestartMonitor({ reload: attempt.reload });
7492
+ return;
7493
+ }
7494
+ finish(store, attempt, { kind: "unreachable" });
7495
+ settle(store, null);
7496
+ });
7497
+ }
7498
+ /** Whether a path's answer is one the request has moved past: the path was
7499
+ * withdrawn, so the dispose aborted it or the ask that replaced it decides. */
7500
+ function superseded(attempt, carrier) {
7501
+ return !carrier.live || attempt.carrier !== carrier;
7502
+ }
7503
+ function finish(store, attempt, outcome) {
7504
+ store.attempt = null;
7505
+ for (const waiter of attempt.waiters) waiter(outcome);
7506
+ }
7507
+ //#endregion
6901
7508
  //#region src/client/useFlows.ts
6902
7509
  /**
6903
7510
  * One keyed registry of install-shaped flows: start, poll to terminal, reset.
@@ -7177,117 +7784,121 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7177
7784
  };
7178
7785
  }
7179
7786
  //#endregion
7180
- //#region \0dsh-plugin-shop-css:e3675a89
7181
- const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel.\n *\n * Clipped on BOTH axes, with a margin, because `.s232ec4_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s646509_toolbar`, itself\n * the first child here, and this rule carries no padding — so its focus\n * ring paints ENTIRELY outside the panel.\n *\n * Both axes is the load-bearing part: `overflow-clip-margin` is inert\n * unless the element clips on both. Clipping x alone left the margin doing\n * nothing — measured against the live app on 0.7.1, the ring reached 0 of\n * its 3px on the left at every device pixel ratio (1, 1.25, 1.5, 2), while\n * keeping the full 3px on the right, where the 280px input stops short of\n * the panel and never meets the clip edge. Raising the margin to 6px\n * changed nothing; clipping both axes with the SAME 3px restored the whole\n * ring. A test that reads declarations cannot see that difference, which is\n * why the first attempt at this shipped broken.\n *\n * `clip` is not a scroll container, so this does not bring back the\n * accidental scrolling `hidden` caused by coercing the other axis to\n * `auto`. Nothing is lost vertically: the panel's scrollHeight equals its\n * height and no descendant paints outside the clip rect (measured).\n * `overflow-x: hidden` stays as the fallback for engines without `clip`,\n * where the ring is clipped as it was before, never worse. */\n overflow-x: hidden;\n overflow: clip;\n overflow-clip-margin: 3px;\n}\n\n.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.s96e910_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa04455_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s614a38_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.sa987f7_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n /* The hue this button carries when selected. `All` and `Installed` select by\n * something other than a category and keep the brand token; the six category\n * buttons override it from the shared hue table below, so a pressed Tool tab\n * turns the same blue as a tool card's spine. */\n --category-hue: var(--dsw-alias-brand-primary);\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Blend text with the theme foreground: darker on light backgrounds, lighter\n * on dark ones. The unblended category hue belongs on the border and fill. */\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-border-l2));\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Selected. Written `.s26d58e_categoryButton.s5f68c4_categoryButtonOn` — TWO classes — so it\n * matches `.s26d58e_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s5f68c4_categoryButtonOn` it lost both `border-color` and `color` to the\n * hover rule for as long as the pointer stayed on the tab that had just been\n * clicked, which is precisely when a reader looks for confirmation.\n *\n * EVERY declaration here is a colour or a shadow: a pressed tab must occupy\n * exactly the same box as an unpressed one, so the bar does not reflow — and\n * with ~9 pills on a wrapping row, one that grows can push the rest onto\n * another line and move the tab out from under the pointer that just clicked\n * it. That is what `font-weight: 600` did (bold metrics are wider than\n * regular), and it is why nothing may be added here that resolves to a length:\n * no padding, no border-width, no font-size, no letter-spacing.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` fails the build on one.\n *\n * `box-shadow` is the exception that earns its place: it paints outside the\n * layout box, so an inset ring doubles the border's weight at zero cost to the\n * metrics. It is what `font-weight: 600` used to be — a second, thickness-\n * based signal that hover does not also produce — and the `other` tab needs it\n * most: its hue IS the neutral gray this file uses for \"no category\", so on\n * the light theme hue alone barely separates pressed from unpressed. */\n.s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n border-color: var(--category-hue);\n background: color-mix(in srgb, var(--category-hue) 14%, transparent);\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n box-shadow: inset 0 0 0 1px var(--category-hue);\n}\n\n/* Forced colours replace every colour above with the system palette, which\n * would leave the three colour declarations saying nothing and the pressed tab\n * indistinguishable from its neighbours. `box-shadow` is dropped in this mode\n * too, so the ring has to come back as an outline. */\n@media (forced-colors: active) {\n .s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The incompatible filter, at the right edge of the category bar: `margin-left:\n * auto` pushes it past the pills, and the bar wraps, so on a narrow panel it\n * drops to its own line rather than squeezing them. It is a MODIFIER, not a\n * ninth category — the categories select what to show and this one subtracts\n * from whatever they selected — so it takes the error hue it filters on rather\n * than joining the hue table.\n *\n * And it is a SWITCH, which is the difference the shape now carries. It used\n * to wear `.s26d58e_categoryButton`: same pill, same row, same size as the eight tabs,\n * differing only in being red. That reads as a ninth tab, and it is not one —\n * the tabs are choose-one and this is on-or-off. So it drops the pill chrome\n * entirely (no border, no fill, nothing to compete with the row it sits\n * beside) and puts the state in a track, the same one every installed card\n * carries. Nothing here restates the switch geometry; `--switch-hue` is the\n * whole of what this control says about it, and it reaches the label too, so\n * one declaration keeps the word and the track the same colour. */\n.sfd887b_incompatibleFilter {\n --switch-hue: var(--dsw-alias-state-error-primary);\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Matches the pills' vertical padding so the bar keeps one line box whether\n * this control wraps onto a row of its own or shares theirs. */\n padding: 3px 0;\n border: 0;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Hover lifts the word to full contrast and warms the track's border. Scoped\n * to the OFF track: `.sfd887b_incompatibleFilter:hover .s11c019_switch` outranks `.s7580e8_switchOn`\n * by two classes, so unscoped it would strip the hue from the border at\n * exactly the moment a reader has the pointer on it to check the state. */\n.sfd887b_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.sfd887b_incompatibleFilter:hover .s11c019_switch:not(.s7580e8_switchOn) {\n border-color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-border-l2));\n}\n\n/* On. Written with both classes for the reason `.s26d58e_categoryButton.s5f68c4_categoryButtonOn`\n * is: it has to match `:hover`'s specificity and come after it, or the colour\n * reverts under a pointer that has not moved since the click. */\n.sfd887b_incompatibleFilter.s6e4cba_incompatibleFilterOn {\n color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Off here means \"nothing is being taken away\" — the resting state — so the\n * knob steps back to the secondary label. The enable switch keeps the full\n * strength dot, where off means \"this plugin is disabled\" and is worth\n * noticing. In this bar it would be the heaviest mark in a row of hairline\n * pills, and on the light theme it is the WHOLE control: bg-layer-2 is the\n * same white as the panel there, so an off track is a hairline outline with a\n * near-black dot sitting in it. */\n.sfd887b_incompatibleFilter .s11c019_switch:not(.s7580e8_switchOn) .saab2b3_switchKnob {\n background: var(--dsw-alias-label-secondary);\n}\n\n/* The ring has no border to trace, so it takes an offset and the pill radius\n * of the tabs it sits beside rather than hugging the text. */\n.sfd887b_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.sebf017_updatableHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.seb0a1e_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.sccc4d1_catalogStats {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Sits beside the 12px stats caption, so it is quieter than `.s73242a_actionButton`:\n * this is an affordance, not a call to action — the catalog reloads on its\n * own at mount and the shelf is already usable without it. Named\n * `catalogRefresh*` (not `reload*`) so it cannot collide with the §4 page\n * reload offer below, which reloads the whole tab rather than refetching\n * the catalog data. */\n.sff243d_catalogRefreshButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.sff243d_catalogRefreshButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.sff243d_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.sff243d_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s194168_catalogRefreshFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sf657f5_cards { animation: none; }\n}\n\n.s65f383_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--category-hue, #8B8E96);\n}\n\n/* The hue table, keyed by `data-category` and read by BOTH surfaces that\n * speak in category colour: a card's spine and badge, and the category bar's\n * own tab. One table, so a tab can never disagree with the cards it filters to\n * — which is the whole point of colouring the tab. `.s26d58e_categoryButton` sets the\n * brand fallback for `All`/`Installed`; these attribute selectors are one\n * specificity step above it and win wherever the attribute is present. */\n.s65f383_card[data-category='tool'], .s26d58e_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'], .s26d58e_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s65f383_card[data-category='ui'], .s26d58e_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'], .s26d58e_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s65f383_card[data-category='integration'], .s26d58e_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s65f383_card[data-category='theme'], .s26d58e_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s65f383_card[data-category='other'], .s26d58e_categoryButton[data-category='other'] { --category-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--category-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--category-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sa97234_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.s513026_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s9b5048_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sc6ec6e_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s4bbe02_incompatibleBadge {\n flex-shrink: 0;\n padding: 3px 9px;\n border-radius: 999px;\n font-size: 12px;\n white-space: nowrap;\n color: var(--dsw-alias-state-error-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);\n}\n\n.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `white-space: pre-line` so the newline in the copy is the copy's own: the\n dictionary decides where the line breaks, and a translator changes it\n without touching the component. Collapses runs of spaces like `normal`, so\n only the explicit \\n survives. */\n.s6eca19_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* What the entry IS rather than what you can do with it — how big it is and\n * who published it — at the right edge of the action row. `margin-left: auto`\n * is what pushes it there, and it lives on this wrapper rather than on either\n * label so the group stays flush right when only one of the two has a value —\n * a packument that named no account has no author, and an entry the registry\n * could not measure honestly has no size. Rarer than it was (every live entry\n * now carries a size, github included), but the wrapper is not what makes it\n * rare: without one, an EMPTY meta group still costs 8px, because\n * `.s9b5048_cardActions` is a flex row with `gap: 8px` and a zero-width item is still\n * an item. The row is a wrapping flex, so an active install flow (which claims\n * the full width) drops the whole group to the next line rather than squeezing\n * it. */\n.s5fbaf9_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The on-disk size, left of the author. Mono and tabular so the digits of one\n * figure line up with the digits of another WHEREVER the two land: `.s5fbaf9_cardMeta`\n * is flush right and the author sits outside the size, so a size's x position\n * moves with the author's width and is different again on the many entries\n * that carry no author at all. There is no column to align to, which is why\n * the reason given here is per-figure legibility rather than the column the\n * first version of this comment claimed. Secondary colour, because it\n * qualifies the entry rather than announcing it.\n *\n * `font-variant-numeric` is safe HERE and would not be on a pill: this element\n * has no state rules, so nothing it does can reflow a wrapping row. */\n.s2d3c05_size {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The publishing account, at the outer end of `.s5fbaf9_cardMeta` on the action row —\n * NOT beside the npm link in the expanded detail, which is where it used to\n * live and what this comment described until the accurate half was deleted\n * with the `margin-left: auto` that moved to `.s5fbaf9_cardMeta`. There is no middot:\n * `.s5fbaf9_cardMeta`'s 8px gap separates it from the size.\n *\n * Secondary label colour carries the distinction — never a background fill,\n * which is invisible in the light theme where every bg-layer token is the\n * same white. */\n.s1ff66f_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.sb00c36_restartSelfButton {\n /* The version row's third state. Geometry matches .s614a38_checkUpdateButton and\n * .sa04455_updateSelfButton exactly — the row is one evolving control and a\n * different pill would read as a different kind of thing. It takes the\n * emphasised fill rather than the neutral one because at that moment the\n * restart is the only thing left to do, the same reason Update has it. */\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sb00c36_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The two activation offers share one recipe: a border in an alias token,\n * never a `bg-layer` fill alone — all four bg-layer tokens collapse to the\n * same white under `body` in the light theme, and a fill-only button would\n * vanish there. Grouped rather than copied, because a copy is a rule that\n * can be corrected on one button and left wrong on the other, silently. */\n.s6ac237_restartButton,\n.s64a5ff_reloadButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover,\n.s64a5ff_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible,\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The §4 reload offer: a notice plus its button, one item of\n * `.sf53ca0_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.sf79aef_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sc323e5_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The Updatable section. The class keeps the Host's field name; only the\n heading the reader sees was renamed. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .s513026_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n /* The reload offer (§4) mounts inside the switch beside the Update button\n * and is a two-line block, not a control: without wrapping, a row reading\n * [switch][notice + Reload][Update] overflows the settings dialog. */\n flex-wrap: wrap;\n gap: 10px;\n}\n\n/* The shop's one switch. It is worn by two controls that mean different\n * things — the enable/disable toggle on an installed card, and the shelf's\n * incompatible filter — so the geometry lives here once and each caller states\n * only its hue.\n *\n * `--switch-hue` is that knob, and it is read from the ANCESTOR rather than\n * declared per state: `.sfd887b_incompatibleFilter` sets it once and it reaches the\n * track and the label together, which is what keeps a red switch from sitting\n * under a neutral word. A declaration on `.s11c019_switch` itself would shadow the\n * inherited value (an element's own rule beats what it inherits), so the\n * default rides in the `var()` fallback and is resolved once into\n * `--track-hue` instead of being spelled out at each use.\n *\n * `box-sizing` and `padding` are stated because the two callers are different\n * elements — a `<button>`, which the UA gives padding, and a `<span>`, which\n * it does not. Without them the same class renders two sizes, and the knob\n * sits 2px above centre in the taller one. */\n.s11c019_switch {\n --track-hue: var(--switch-hue, var(--dsw-alias-state-success-primary));\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n width: 32px;\n height: 18px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n/* The knob's POSITION is the state, which is the reason this control needs no\n * `forced-colors` fallback the way the category pills do: a system palette\n * replaces every colour below and moves nothing. */\n.s7580e8_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n /* Same reason as `.sa1f556_outdatedActions`: `.sf79aef_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.sf53ca0_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
7182
- if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
7787
+ //#region \0dsh-plugin-shop-css:201c343a
7788
+ const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.se24648_client.s4c8568_config.s64bf36_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.se24648_client.scd01a1_spec.s64bf36_ts` pins this. */\n\n.sa18b47_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel.\n *\n * Clipped on BOTH axes, with a margin, because `.seba6a3_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s7df13e_toolbar`, itself\n * the first child here, and this rule carries no padding — so its focus\n * ring paints ENTIRELY outside the panel.\n *\n * Both axes is the load-bearing part: `overflow-clip-margin` is inert\n * unless the element clips on both. Clipping x alone left the margin doing\n * nothing — measured against the live app on 0.7.1, the ring reached 0 of\n * its 3px on the left at every device pixel ratio (1, 1.25, 1.5, 2), while\n * keeping the full 3px on the right, where the 280px input stops short of\n * the panel and never meets the clip edge. Raising the margin to 6px\n * changed nothing; clipping both axes with the SAME 3px restored the whole\n * ring. A test that reads declarations cannot see that difference, which is\n * why the first attempt at this shipped broken.\n *\n * `clip` is not a scroll container, so this does not bring back the\n * accidental scrolling `hidden` caused by coercing the other axis to\n * `auto`. Nothing is lost vertically: the panel's scrollHeight equals its\n * height and no descendant paints outside the clip rect (measured).\n * `overflow-x: hidden` stays as the fallback for engines without `clip`,\n * where the ring is clipped as it was before, never worse. */\n overflow-x: hidden;\n overflow: clip;\n overflow-clip-margin: 3px;\n}\n\n.sd85001_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.se60107_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.sba4a38_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s5aa954_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s5aa954_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s7a0318_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s7a5df4_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s8d4020_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.sbc0e1a_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s5aa954_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s5aa954_skeletonCard span { animation: none; opacity: 0.75; }\n .s5aa954_skeletonCard::after { content: none; }\n}\n\n.s4a2332_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s4a2332_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s4a2332_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s7df13e_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s20709b_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s98c547_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.s2b9f1d_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s394a1f_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.s394a1f_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.s394a1f_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s511e9b_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s511e9b_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s511e9b_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s511e9b_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.se3bcc2_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.se3bcc2_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.se3bcc2_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.s8c6b17_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.seba6a3_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.seba6a3_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc9d2d3_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb4e3b8_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s85e01e_categoryButton {\n /* The hue this button carries when selected. `All` and `Installed` select by\n * something other than a category and keep the brand token; the six category\n * buttons override it from the shared hue table below, so a pressed Tool tab\n * turns the same blue as a tool card's spine. */\n --category-hue: var(--dsw-alias-brand-primary);\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Blend text with the theme foreground: darker on light backgrounds, lighter\n * on dark ones. The unblended category hue belongs on the border and fill. */\n.s85e01e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-border-l2));\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Selected. Written `.s85e01e_categoryButton.s833fec_categoryButtonOn` — TWO classes — so it\n * matches `.s85e01e_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s833fec_categoryButtonOn` it lost both `border-color` and `color` to the\n * hover rule for as long as the pointer stayed on the tab that had just been\n * clicked, which is precisely when a reader looks for confirmation.\n *\n * EVERY declaration here is a colour or a shadow: a pressed tab must occupy\n * exactly the same box as an unpressed one, so the bar does not reflow — and\n * with ~9 pills on a wrapping row, one that grows can push the rest onto\n * another line and move the tab out from under the pointer that just clicked\n * it. That is what `font-weight: 600` did (bold metrics are wider than\n * regular), and it is why nothing may be added here that resolves to a length:\n * no padding, no border-width, no font-size, no letter-spacing.\n * `css-tokens.se24648_client.scd01a1_spec.s64bf36_ts` fails the build on one.\n *\n * `box-shadow` is the exception that earns its place: it paints outside the\n * layout box, so an inset ring doubles the border's weight at zero cost to the\n * metrics. It is what `font-weight: 600` used to be — a second, thickness-\n * based signal that hover does not also produce — and the `other` tab needs it\n * most: its hue IS the neutral gray this file uses for \"no category\", so on\n * the light theme hue alone barely separates pressed from unpressed. */\n.s85e01e_categoryButton.s833fec_categoryButtonOn {\n border-color: var(--category-hue);\n background: color-mix(in srgb, var(--category-hue) 14%, transparent);\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n box-shadow: inset 0 0 0 1px var(--category-hue);\n}\n\n/* Forced colours replace every colour above with the system palette, which\n * would leave the three colour declarations saying nothing and the pressed tab\n * indistinguishable from its neighbours. `box-shadow` is dropped in this mode\n * too, so the ring has to come back as an outline. */\n@media (forced-colors: active) {\n .s85e01e_categoryButton.s833fec_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.s85e01e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The incompatible filter, at the right edge of the category bar: `margin-left:\n * auto` pushes it past the pills, and the bar wraps, so on a narrow panel it\n * drops to its own line rather than squeezing them. It is a MODIFIER, not a\n * ninth category — the categories select what to show and this one subtracts\n * from whatever they selected — so it takes the error hue it filters on rather\n * than joining the hue table.\n *\n * And it is a SWITCH, which is the difference the shape now carries. It used\n * to wear `.s85e01e_categoryButton`: same pill, same row, same size as the eight tabs,\n * differing only in being red. That reads as a ninth tab, and it is not one —\n * the tabs are choose-one and this is on-or-off. So it drops the pill chrome\n * entirely (no border, no fill, nothing to compete with the row it sits\n * beside) and puts the state in a track, the same one every installed card\n * carries. Nothing here restates the switch geometry; `--switch-hue` is the\n * whole of what this control says about it, and it reaches the label too, so\n * one declaration keeps the word and the track the same colour. */\n.sab8b1c_incompatibleFilter {\n --switch-hue: var(--dsw-alias-state-error-primary);\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Matches the pills' vertical padding so the bar keeps one line box whether\n * this control wraps onto a row of its own or shares theirs. */\n padding: 3px 0;\n border: 0;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Hover lifts the word to full contrast and warms the track's border. Scoped\n * to the OFF track: `.sab8b1c_incompatibleFilter:hover .sc4c0fe_switch` outranks `.s383bcc_switchOn`\n * by two classes, so unscoped it would strip the hue from the border at\n * exactly the moment a reader has the pointer on it to check the state. */\n.sab8b1c_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.sab8b1c_incompatibleFilter:hover .sc4c0fe_switch:not(.s383bcc_switchOn) {\n border-color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-border-l2));\n}\n\n/* On. Written with both classes for the reason `.s85e01e_categoryButton.s833fec_categoryButtonOn`\n * is: it has to match `:hover`'s specificity and come after it, or the colour\n * reverts under a pointer that has not moved since the click. */\n.sab8b1c_incompatibleFilter.s6dac80_incompatibleFilterOn {\n color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Off here means \"nothing is being taken away\" — the resting state — so the\n * knob steps back to the secondary label. The enable switch keeps the full\n * strength dot, where off means \"this plugin is disabled\" and is worth\n * noticing. In this bar it would be the heaviest mark in a row of hairline\n * pills, and on the light theme it is the WHOLE control: bg-layer-2 is the\n * same white as the panel there, so an off track is a hairline outline with a\n * near-black dot sitting in it. */\n.sab8b1c_incompatibleFilter .sc4c0fe_switch:not(.s383bcc_switchOn) .s43a686_switchKnob {\n background: var(--dsw-alias-label-secondary);\n}\n\n/* The ring has no border to trace, so it takes an offset and the pill radius\n * of the tabs it sits beside rather than hugging the text. */\n.sab8b1c_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.sefee3c_updatableHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.sff4c1e_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.s927a53_catalogStats {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Sits beside the 12px stats caption, so it is quieter than `.s4a2332_actionButton`:\n * this is an affordance, not a call to action — the catalog reloads on its\n * own at mount and the shelf is already usable without it. Named\n * `catalogRefresh*` (not `reload*`) so it cannot collide with the §4 page\n * reload offer below, which reloads the whole tab rather than refetching\n * the catalog data. */\n.s68cab8_catalogRefreshButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.s68cab8_catalogRefreshButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s68cab8_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s68cab8_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s6dceed_catalogRefreshFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s5748b0_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sb2eae9_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sb2eae9_cards { animation: none; }\n}\n\n.sa1c2f7_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.s203f20_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s795be6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1c2f7_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sa1c2f7_card { transition: none; }\n .sa1c2f7_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.sb00bbe_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--category-hue, #8B8E96);\n}\n\n/* The hue table, keyed by `data-category` and read by BOTH surfaces that\n * speak in category colour: a card's spine and badge, and the category bar's\n * own tab. One table, so a tab can never disagree with the cards it filters to\n * — which is the whole point of colouring the tab. `.s85e01e_categoryButton` sets the\n * brand fallback for `All`/`Installed`; these attribute selectors are one\n * specificity step above it and win wherever the attribute is present. */\n.sa1c2f7_card[data-category='tool'], .s85e01e_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.sa1c2f7_card[data-category='provider'], .s85e01e_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.sa1c2f7_card[data-category='ui'], .s85e01e_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.sa1c2f7_card[data-category='workflow'], .s85e01e_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.sa1c2f7_card[data-category='integration'], .s85e01e_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.sa1c2f7_card[data-category='theme'], .s85e01e_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.sa1c2f7_card[data-category='other'], .s85e01e_categoryButton[data-category='other'] { --category-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.see02fd_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--category-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--category-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sd53924_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sd53924_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc2b129_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.scd0548_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s4f6385_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s4f6385_cardActions > .sc1afc6_gate,\n.s4f6385_cardActions > .s65b9eb_installPanel,\n.s4f6385_cardActions > .s4fbfe0_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.s4b6469_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s5a7549_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s23bf0e_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s23bf0e_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s23bf0e_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s23bf0e_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.sadc37f_incompatibleBadge {\n flex-shrink: 0;\n padding: 3px 9px;\n border-radius: 999px;\n font-size: 12px;\n white-space: nowrap;\n color: var(--dsw-alias-state-error-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);\n}\n\n.s6ba7cc_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s80dda2_chevron, .sde820b_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.sde820b_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s80dda2_chevron, .sde820b_chevronOpen { transition: none; }\n}\n\n.sa4ad4d_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.s7a8821_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.seab456_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s5ab512_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.seca366_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `white-space: pre-line` so the newline in the copy is the copy's own: the\n dictionary decides where the line breaks, and a translator changes it\n without touching the component. Collapses runs of spaces like `normal`, so\n only the explicit \\n survives. */\n.sa6eb0b_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* The way out of dsh's own refusal: the lead-in, then the exemption command\n * on its own line. */\n.s92e71f_remedy {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.sb320b4_remedyLead {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The exemption command, bound for a terminal unchanged: one click selects\n * all of it, and it wraps anywhere rather than widening the card. A border,\n * not a fill, marks the box: the bg layers are one white in the light theme. */\n.s2c3062_allowCommand {\n display: block;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-all;\n user-select: all;\n color: var(--dsw-alias-label-primary);\n}\n\n/* A refused install on a surface that does not state its blockers — the\n * outdated row — writes them out on their own full-width line under the\n * disabled button, the command that clears dsh's refusal included. */\n.s5fe511_refusal {\n display: flex;\n flex-direction: column;\n gap: 6px;\n flex-basis: 100%;\n}\n\n.scaa43b_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s121391_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s57f233_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s57f233_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sbcd943_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.s59866d_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.s4662cb_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.s4662cb_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.s4662cb_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.s4662cb_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.s4662cb_detailRow a:hover {\n text-decoration: underline;\n}\n\n.s4662cb_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* What the entry IS rather than what you can do with it — how big it is and\n * who published it — at the right edge of the action row. `margin-left: auto`\n * is what pushes it there, and it lives on this wrapper rather than on either\n * label so the group stays flush right when only one of the two has a value —\n * a packument that named no account has no author, and an entry the registry\n * could not measure honestly has no size. Rarer than it was (every live entry\n * now carries a size, github included), but the wrapper is not what makes it\n * rare: without one, an EMPTY meta group still costs 8px, because\n * `.s4f6385_cardActions` is a flex row with `gap: 8px` and a zero-width item is still\n * an item. The row is a wrapping flex, so an active install flow (which claims\n * the full width) drops the whole group to the next line rather than squeezing\n * it. */\n.s6e38b0_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The on-disk size, left of the author. Mono and tabular so the digits of one\n * figure line up with the digits of another WHEREVER the two land: `.s6e38b0_cardMeta`\n * is flush right and the author sits outside the size, so a size's x position\n * moves with the author's width and is different again on the many entries\n * that carry no author at all. There is no column to align to, which is why\n * the reason given here is per-figure legibility rather than the column the\n * first version of this comment claimed. Secondary colour, because it\n * qualifies the entry rather than announcing it.\n *\n * `font-variant-numeric` is safe HERE and would not be on a pill: this element\n * has no state rules, so nothing it does can reflow a wrapping row. */\n.s51bec8_size {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The publishing account, at the outer end of `.s6e38b0_cardMeta` on the action row —\n * NOT beside the npm link in the expanded detail, which is where it used to\n * live and what this comment described until the accurate half was deleted\n * with the `margin-left: auto` that moved to `.s6e38b0_cardMeta`. There is no middot:\n * `.s6e38b0_cardMeta`'s 8px gap separates it from the size.\n *\n * Secondary label colour carries the distinction — never a background fill,\n * which is invisible in the light theme where every bg-layer token is the\n * same white. */\n.s7503fc_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sf73a51_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s65b9eb_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.sd07a79_installButton, .s1e8df4_confirmButton, .saa2e75_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.sd07a79_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sd07a79_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s1e8df4_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.s1e8df4_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.saa2e75_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.saa2e75_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.sd07a79_installButton:focus-visible, .s1e8df4_confirmButton:focus-visible, .saa2e75_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.s849adb_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.s4fbfe0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.sd93778_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.sd93778_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.sd93778_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.s3d7804_restartSelfButton {\n /* The version row's third state. Geometry matches .s511e9b_checkUpdateButton and\n * .s394a1f_updateSelfButton exactly — the row is one evolving control and a\n * different pill would read as a different kind of thing. It takes the\n * emphasised fill rather than the neutral one because at that moment the\n * restart is the only thing left to do, the same reason Update has it. */\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.s3d7804_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The two activation offers share one recipe: a border in an alias token,\n * never a `bg-layer` fill alone — all four bg-layer tokens collapse to the\n * same white under `body` in the light theme, and a fill-only button would\n * vanish there. Grouped rather than copied, because a copy is a rule that\n * can be corrected on one button and left wrong on the other, silently. */\n.s66e9a9_restartButton,\n.s4ca1f6_reloadButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s66e9a9_restartButton:hover,\n.s4ca1f6_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s66e9a9_restartButton:focus-visible,\n.s4ca1f6_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The §4 reload offer: a notice plus its button, one item of\n * `.s4fbfe0_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.s42eadb_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n\n.sc1afc6_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.sbbe486_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s119e47_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sadd68a_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s7ac0e2_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.scee95e_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s7bfdc8_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s4bc9ba_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s8692d7_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.s537eee_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.scdb145_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sdaa6f0_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s1c1027_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The Updatable section. The class keeps the Host's field name; only the\n heading the reader sees was renamed. */\n.sc8d6dc_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.se27588_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s46601e_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s54c287_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s54c287_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.sa02632_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .scd0548_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s59c2df_outdatedActions {\n display: flex;\n align-items: center;\n /* The reload offer (§4) mounts inside the switch beside the Update button\n * and is a two-line block, not a control: without wrapping, a row reading\n * [switch][notice + Reload][Update] overflows the settings dialog. */\n flex-wrap: wrap;\n gap: 10px;\n}\n\n/* The shop's one switch. It is worn by two controls that mean different\n * things — the enable/disable toggle on an installed card, and the shelf's\n * incompatible filter — so the geometry lives here once and each caller states\n * only its hue.\n *\n * `--switch-hue` is that knob, and it is read from the ANCESTOR rather than\n * declared per state: `.sab8b1c_incompatibleFilter` sets it once and it reaches the\n * track and the label together, which is what keeps a red switch from sitting\n * under a neutral word. A declaration on `.sc4c0fe_switch` itself would shadow the\n * inherited value (an element's own rule beats what it inherits), so the\n * default rides in the `var()` fallback and is resolved once into\n * `--track-hue` instead of being spelled out at each use.\n *\n * `box-sizing` and `padding` are stated because the two callers are different\n * elements — a `<button>`, which the UA gives padding, and a `<span>`, which\n * it does not. Without them the same class renders two sizes, and the knob\n * sits 2px above centre in the taller one. */\n.sc4c0fe_switch {\n --track-hue: var(--switch-hue, var(--dsw-alias-state-success-primary));\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n width: 32px;\n height: 18px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n/* The knob's POSITION is the state, which is the reason this control needs no\n * `forced-colors` fallback the way the category pills do: a system palette\n * replaces every colour below and moves nothing. */\n.s383bcc_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.s43a686_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s383bcc_switchOn .s43a686_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.sc4c0fe_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sc4c0fe_switch, .s43a686_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9a4e2_switchWrap {\n display: inline-flex;\n align-items: center;\n /* Same reason as `.s59c2df_outdatedActions`: `.s42eadb_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.s4fbfe0_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
7789
+ if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"201c343a\"]")) {
7183
7790
  const tag = document.createElement("style");
7184
- tag.dataset.pluginCss = "e3675a89";
7791
+ tag.dataset.pluginCss = "201c343a";
7185
7792
  tag.textContent = css;
7186
7793
  document.head.appendChild(tag);
7187
7794
  }
7188
- var _dsh_plugin_shop_css_e3675a89_default = {
7189
- "client": "s8583e3_client",
7190
- "config": "s942a79_config",
7191
- "ts": "s5f67aa_ts",
7192
- "spec": "sfd9d8a_spec",
7193
- "panel": "s255e5a_panel",
7194
- "searchInput": "s232ec4_searchInput",
7195
- "toolbar": "s646509_toolbar",
7196
- "stateLine": "s44a3ec_stateLine",
7197
- "srOnly": "s3a0ece_srOnly",
7198
- "skeletonGrid": "se0c3b4_skeletonGrid",
7199
- "skeletonCard": "s520416_skeletonCard",
7200
- "skeletonName": "s8c7d6f_skeletonName",
7201
- "skeletonSummary": "sf5179e_skeletonSummary",
7202
- "skeletonSummaryShort": "sbb0d25_skeletonSummaryShort",
7203
- "skeletonActions": "s96e910_skeletonActions",
7204
- "actionButton": "s73242a_actionButton",
7205
- "toolbarRight": "s745e3c_toolbarRight",
7206
- "versionBlock": "s4fe61f_versionBlock",
7207
- "versionText": "sb9b924_versionText",
7208
- "updateSelfButton": "sa04455_updateSelfButton",
7209
- "checkUpdateButton": "s614a38_checkUpdateButton",
7210
- "githubLink": "sa987f7_githubLink",
7211
- "selfUpdatePanel": "sb81ba5_selfUpdatePanel",
7212
- "staleBadge": "s3946f1_staleBadge",
7213
- "categoryBar": "sb2c859_categoryBar",
7214
- "categoryButton": "s26d58e_categoryButton",
7215
- "categoryButtonOn": "s5f68c4_categoryButtonOn",
7216
- "incompatibleFilter": "sfd887b_incompatibleFilter",
7217
- "switch": "s11c019_switch",
7218
- "switchOn": "s7580e8_switchOn",
7219
- "incompatibleFilterOn": "s6e4cba_incompatibleFilterOn",
7220
- "switchKnob": "saab2b3_switchKnob",
7221
- "updatableHeading": "sebf017_updatableHeading",
7222
- "catalogStatsRow": "seb0a1e_catalogStatsRow",
7223
- "catalogStats": "sccc4d1_catalogStats",
7224
- "catalogRefreshButton": "sff243d_catalogRefreshButton",
7225
- "catalogRefreshFailed": "s194168_catalogRefreshFailed",
7226
- "emptyLine": "s6de1ab_emptyLine",
7227
- "cards": "sf657f5_cards",
7228
- "card": "s65f383_card",
7229
- "cardsSentry": "sd7ee01_cardsSentry",
7230
- "showingLine": "s705cc6_showingLine",
7231
- "cardSpine": "s4ce25d_cardSpine",
7232
- "categoryBadge": "s9c8eaa_categoryBadge",
7233
- "entryHeader": "sa97234_entryHeader",
7234
- "name": "sc3f149_name",
7235
- "badges": "s513026_badges",
7236
- "cardActions": "s9b5048_cardActions",
7237
- "gate": "sc4f2ac_gate",
7238
- "installPanel": "s4d374c_installPanel",
7239
- "installedActions": "sf53ca0_installedActions",
7240
- "cardVersion": "sc6ec6e_cardVersion",
7241
- "sourceBadge": "s71f10f_sourceBadge",
7242
- "tierBadge": "s5e82cb_tierBadge",
7243
- "incompatibleBadge": "s4bbe02_incompatibleBadge",
7244
- "starsBadge": "sff43b4_starsBadge",
7245
- "chevron": "s73f7fb_chevron",
7246
- "chevronOpen": "s61393c_chevronOpen",
7247
- "body": "s820ebf_body",
7248
- "summary": "sb8fbc8_summary",
7249
- "summaryZh": "se507db_summaryZh",
7250
- "summaryExpanded": "s10baf0_summaryExpanded",
7251
- "summaryZhExpanded": "sb9dba9_summaryZhExpanded",
7252
- "incompatibleDetail": "s6eca19_incompatibleDetail",
7253
- "capabilitiesBlock": "s9d2029_capabilitiesBlock",
7254
- "capabilitiesNote": "s3bdbd5_capabilitiesNote",
7255
- "capabilities": "s441702_capabilities",
7256
- "detail": "sde3f0c_detail",
7257
- "detailRows": "se98179_detailRows",
7258
- "detailRow": "sfb4d2f_detailRow",
7259
- "cardMeta": "s5fbaf9_cardMeta",
7260
- "size": "s2d3c05_size",
7261
- "author": "s1ff66f_author",
7262
- "reviewedLine": "sd58c6d_reviewedLine",
7263
- "installButton": "se7aaed_installButton",
7264
- "confirmButton": "sb43a49_confirmButton",
7265
- "cancelButton": "s6e45d1_cancelButton",
7266
- "installedLabel": "sb6fc74_installedLabel",
7267
- "uninstallButton": "s96967f_uninstallButton",
7268
- "restartSelfButton": "sb00c36_restartSelfButton",
7269
- "restartButton": "s6ac237_restartButton",
7270
- "reloadButton": "s64a5ff_reloadButton",
7271
- "reloadPanel": "sf79aef_reloadPanel",
7272
- "gateTitle": "s559c38_gateTitle",
7273
- "gateBody": "s3c0ace_gateBody",
7274
- "gateWarning": "sc323e5_gateWarning",
7275
- "gateActions": "s4b2373_gateActions",
7276
- "installing": "s52b5a5_installing",
7277
- "log": "s64ab71_log",
7278
- "logLine": "s1531a8_logLine",
7279
- "notice": "sf20dda_notice",
7280
- "failedHeading": "seb9b08_failedHeading",
7281
- "failedDetail": "sfd3021_failedDetail",
7282
- "rejectedCode": "s50ba35_rejectedCode",
7283
- "rejectedDetail": "se75489_rejectedDetail",
7284
- "outdatedSection": "sa276cf_outdatedSection",
7285
- "outdatedList": "sceae6e_outdatedList",
7286
- "outdatedRow": "s8d2072_outdatedRow",
7287
- "outdatedInfo": "s5c2545_outdatedInfo",
7288
- "outdatedVersions": "s877c68_outdatedVersions",
7289
- "outdatedActions": "sa1f556_outdatedActions",
7290
- "switchWrap": "sd9f9e4_switchWrap"
7795
+ var _dsh_plugin_shop_css_201c343a_default = {
7796
+ "client": "se24648_client",
7797
+ "config": "s4c8568_config",
7798
+ "ts": "s64bf36_ts",
7799
+ "spec": "scd01a1_spec",
7800
+ "panel": "sa18b47_panel",
7801
+ "searchInput": "seba6a3_searchInput",
7802
+ "toolbar": "s7df13e_toolbar",
7803
+ "stateLine": "sd85001_stateLine",
7804
+ "srOnly": "se60107_srOnly",
7805
+ "skeletonGrid": "sba4a38_skeletonGrid",
7806
+ "skeletonCard": "s5aa954_skeletonCard",
7807
+ "skeletonName": "s7a0318_skeletonName",
7808
+ "skeletonSummary": "s7a5df4_skeletonSummary",
7809
+ "skeletonSummaryShort": "s8d4020_skeletonSummaryShort",
7810
+ "skeletonActions": "sbc0e1a_skeletonActions",
7811
+ "actionButton": "s4a2332_actionButton",
7812
+ "toolbarRight": "s20709b_toolbarRight",
7813
+ "versionBlock": "s98c547_versionBlock",
7814
+ "versionText": "s2b9f1d_versionText",
7815
+ "updateSelfButton": "s394a1f_updateSelfButton",
7816
+ "checkUpdateButton": "s511e9b_checkUpdateButton",
7817
+ "githubLink": "se3bcc2_githubLink",
7818
+ "selfUpdatePanel": "s8c6b17_selfUpdatePanel",
7819
+ "staleBadge": "sc9d2d3_staleBadge",
7820
+ "categoryBar": "sb4e3b8_categoryBar",
7821
+ "categoryButton": "s85e01e_categoryButton",
7822
+ "categoryButtonOn": "s833fec_categoryButtonOn",
7823
+ "incompatibleFilter": "sab8b1c_incompatibleFilter",
7824
+ "switch": "sc4c0fe_switch",
7825
+ "switchOn": "s383bcc_switchOn",
7826
+ "incompatibleFilterOn": "s6dac80_incompatibleFilterOn",
7827
+ "switchKnob": "s43a686_switchKnob",
7828
+ "updatableHeading": "sefee3c_updatableHeading",
7829
+ "catalogStatsRow": "sff4c1e_catalogStatsRow",
7830
+ "catalogStats": "s927a53_catalogStats",
7831
+ "catalogRefreshButton": "s68cab8_catalogRefreshButton",
7832
+ "catalogRefreshFailed": "s6dceed_catalogRefreshFailed",
7833
+ "emptyLine": "s5748b0_emptyLine",
7834
+ "cards": "sb2eae9_cards",
7835
+ "card": "sa1c2f7_card",
7836
+ "cardsSentry": "s203f20_cardsSentry",
7837
+ "showingLine": "s795be6_showingLine",
7838
+ "cardSpine": "sb00bbe_cardSpine",
7839
+ "categoryBadge": "see02fd_categoryBadge",
7840
+ "entryHeader": "sd53924_entryHeader",
7841
+ "name": "sc2b129_name",
7842
+ "badges": "scd0548_badges",
7843
+ "cardActions": "s4f6385_cardActions",
7844
+ "gate": "sc1afc6_gate",
7845
+ "installPanel": "s65b9eb_installPanel",
7846
+ "installedActions": "s4fbfe0_installedActions",
7847
+ "cardVersion": "s4b6469_cardVersion",
7848
+ "sourceBadge": "s5a7549_sourceBadge",
7849
+ "tierBadge": "s23bf0e_tierBadge",
7850
+ "incompatibleBadge": "sadc37f_incompatibleBadge",
7851
+ "starsBadge": "s6ba7cc_starsBadge",
7852
+ "chevron": "s80dda2_chevron",
7853
+ "chevronOpen": "sde820b_chevronOpen",
7854
+ "body": "sa4ad4d_body",
7855
+ "summary": "s7a8821_summary",
7856
+ "summaryZh": "seab456_summaryZh",
7857
+ "summaryExpanded": "s5ab512_summaryExpanded",
7858
+ "summaryZhExpanded": "seca366_summaryZhExpanded",
7859
+ "incompatibleDetail": "sa6eb0b_incompatibleDetail",
7860
+ "remedy": "s92e71f_remedy",
7861
+ "remedyLead": "sb320b4_remedyLead",
7862
+ "allowCommand": "s2c3062_allowCommand",
7863
+ "refusal": "s5fe511_refusal",
7864
+ "capabilitiesBlock": "scaa43b_capabilitiesBlock",
7865
+ "capabilitiesNote": "s121391_capabilitiesNote",
7866
+ "capabilities": "s57f233_capabilities",
7867
+ "detail": "sbcd943_detail",
7868
+ "detailRows": "s59866d_detailRows",
7869
+ "detailRow": "s4662cb_detailRow",
7870
+ "cardMeta": "s6e38b0_cardMeta",
7871
+ "size": "s51bec8_size",
7872
+ "author": "s7503fc_author",
7873
+ "reviewedLine": "sf73a51_reviewedLine",
7874
+ "installButton": "sd07a79_installButton",
7875
+ "confirmButton": "s1e8df4_confirmButton",
7876
+ "cancelButton": "saa2e75_cancelButton",
7877
+ "installedLabel": "s849adb_installedLabel",
7878
+ "uninstallButton": "sd93778_uninstallButton",
7879
+ "restartSelfButton": "s3d7804_restartSelfButton",
7880
+ "restartButton": "s66e9a9_restartButton",
7881
+ "reloadButton": "s4ca1f6_reloadButton",
7882
+ "reloadPanel": "s42eadb_reloadPanel",
7883
+ "gateTitle": "sbbe486_gateTitle",
7884
+ "gateBody": "s119e47_gateBody",
7885
+ "gateWarning": "sadd68a_gateWarning",
7886
+ "gateActions": "s7ac0e2_gateActions",
7887
+ "installing": "scee95e_installing",
7888
+ "log": "s7bfdc8_log",
7889
+ "logLine": "s4bc9ba_logLine",
7890
+ "notice": "s8692d7_notice",
7891
+ "failedHeading": "s537eee_failedHeading",
7892
+ "failedDetail": "scdb145_failedDetail",
7893
+ "rejectedCode": "sdaa6f0_rejectedCode",
7894
+ "rejectedDetail": "s1c1027_rejectedDetail",
7895
+ "outdatedSection": "sc8d6dc_outdatedSection",
7896
+ "outdatedList": "se27588_outdatedList",
7897
+ "outdatedRow": "s46601e_outdatedRow",
7898
+ "outdatedInfo": "s54c287_outdatedInfo",
7899
+ "outdatedVersions": "sa02632_outdatedVersions",
7900
+ "outdatedActions": "s59c2df_outdatedActions",
7901
+ "switchWrap": "sd9a4e2_switchWrap"
7291
7902
  };
7292
7903
  //#endregion
7293
7904
  //#region src/client/ShopTab.tsx
@@ -7298,13 +7909,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7298
7909
  * §11.3.4): no render path here may ever use dangerouslySetInnerHTML. */
7299
7910
  /** The project's home on GitHub, linked from the toolbar. */
7300
7911
  const SHOP_REPO_URL = "https://github.com/LivXue/dsh-plugin-shop";
7912
+ /** The class of every root the tab renders, the crash fallback's included:
7913
+ * the panel, plus `notranslate`, which with `translate="no"` keeps a browser's
7914
+ * page translation out of the text nodes React owns. Translation rewrites
7915
+ * those nodes, and React's next commit throws on the ones it no longer finds
7916
+ * (design 2026-09-26-market-borrowings §4). The tab renders no portals, so its
7917
+ * root is its whole surface. */
7918
+ const PANEL_ROOT = `${_dsh_plugin_shop_css_201c343a_default.panel} notranslate`;
7301
7919
  function ChevronIcon({ open }) {
7302
7920
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
7303
7921
  viewBox: "0 0 16 16",
7304
7922
  width: "14",
7305
7923
  height: "14",
7306
7924
  "aria-hidden": "true",
7307
- className: open ? _dsh_plugin_shop_css_e3675a89_default.chevronOpen : _dsh_plugin_shop_css_e3675a89_default.chevron,
7925
+ className: open ? _dsh_plugin_shop_css_201c343a_default.chevronOpen : _dsh_plugin_shop_css_201c343a_default.chevron,
7308
7926
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
7309
7927
  fill: "currentColor",
7310
7928
  d: "M3.5 6 8 10.5 12.5 6l1 1L8 12.5 2.5 7l1-1Z"
@@ -7317,9 +7935,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7317
7935
  * install controls. An installed plugin's card carries its installed row:
7318
7936
  * current → the non-interactive installed label, behind → the update button;
7319
7937
  * uninstalled → the install button. */
7320
- const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView }) {
7938
+ const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, harness, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView }) {
7321
7939
  const [open, setOpen] = (0, react.useState)(false);
7322
- const blockers = blockersOf(missing, nameTakenBy, t);
7940
+ const blockers = stateBlockers(blockersOf(missing, harness, nameTakenBy), t);
7323
7941
  const detailId = (0, react.useId)();
7324
7942
  const summary = entry.catalog?.summary;
7325
7943
  const npmUrl = npmPageUrl(entry);
@@ -7334,32 +7952,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7334
7952
  subdir: entry.subdir
7335
7953
  };
7336
7954
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7337
- className: _dsh_plugin_shop_css_e3675a89_default.card,
7955
+ className: _dsh_plugin_shop_css_201c343a_default.card,
7338
7956
  "data-shop-entry": entry.name,
7339
7957
  "data-category": category,
7340
7958
  children: [
7341
7959
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7342
- className: _dsh_plugin_shop_css_e3675a89_default.cardSpine,
7960
+ className: _dsh_plugin_shop_css_201c343a_default.cardSpine,
7343
7961
  "aria-hidden": "true"
7344
7962
  }),
7345
7963
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7346
7964
  type: "button",
7347
- className: _dsh_plugin_shop_css_e3675a89_default.entryHeader,
7965
+ className: _dsh_plugin_shop_css_201c343a_default.entryHeader,
7348
7966
  "aria-expanded": open,
7349
7967
  "aria-controls": detailId,
7350
7968
  onClick: () => setOpen((current) => !current),
7351
7969
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7352
- className: _dsh_plugin_shop_css_e3675a89_default.name,
7970
+ className: _dsh_plugin_shop_css_201c343a_default.name,
7353
7971
  children: entry.name
7354
7972
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7355
- className: _dsh_plugin_shop_css_e3675a89_default.badges,
7973
+ className: _dsh_plugin_shop_css_201c343a_default.badges,
7356
7974
  children: [
7357
7975
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7358
- className: _dsh_plugin_shop_css_e3675a89_default.categoryBadge,
7976
+ className: _dsh_plugin_shop_css_201c343a_default.categoryBadge,
7359
7977
  children: t(categoryKey(entry))
7360
7978
  }),
7361
7979
  entry.source === "npm" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7362
- className: _dsh_plugin_shop_css_e3675a89_default.sourceBadge,
7980
+ className: _dsh_plugin_shop_css_201c343a_default.sourceBadge,
7363
7981
  "data-shop-source-npm": true,
7364
7982
  role: "img",
7365
7983
  "aria-label": t("npmSource"),
@@ -7376,7 +7994,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7376
7994
  })
7377
7995
  }),
7378
7996
  hasGithubHome(entry) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7379
- className: _dsh_plugin_shop_css_e3675a89_default.sourceBadge,
7997
+ className: _dsh_plugin_shop_css_201c343a_default.sourceBadge,
7380
7998
  "data-shop-source-github": true,
7381
7999
  role: "img",
7382
8000
  "aria-label": t("githubSource"),
@@ -7390,17 +8008,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7390
8008
  })
7391
8009
  }),
7392
8010
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7393
- className: _dsh_plugin_shop_css_e3675a89_default.cardVersion,
8011
+ className: _dsh_plugin_shop_css_201c343a_default.cardVersion,
7394
8012
  "data-card-version": true,
7395
8013
  children: ["v", displayVersion(entry)]
7396
8014
  }),
7397
8015
  entry.tier !== "community" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7398
- className: _dsh_plugin_shop_css_e3675a89_default.tierBadge,
8016
+ className: _dsh_plugin_shop_css_201c343a_default.tierBadge,
7399
8017
  "data-tier": entry.tier,
7400
8018
  children: t(tierKey(entry.tier))
7401
8019
  }),
7402
8020
  stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7403
- className: _dsh_plugin_shop_css_e3675a89_default.starsBadge,
8021
+ className: _dsh_plugin_shop_css_201c343a_default.starsBadge,
7404
8022
  role: "img",
7405
8023
  "aria-label": t("stars", { count: stars }),
7406
8024
  children: ["★ ", formatStars(stars)]
@@ -7410,39 +8028,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7410
8028
  })]
7411
8029
  }),
7412
8030
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7413
- className: _dsh_plugin_shop_css_e3675a89_default.body,
8031
+ className: _dsh_plugin_shop_css_201c343a_default.body,
7414
8032
  children: [
7415
8033
  summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7416
- className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summary} ${_dsh_plugin_shop_css_e3675a89_default.summaryExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summary,
8034
+ className: open ? `${_dsh_plugin_shop_css_201c343a_default.summary} ${_dsh_plugin_shop_css_201c343a_default.summaryExpanded}` : _dsh_plugin_shop_css_201c343a_default.summary,
7417
8035
  children: summary.en
7418
8036
  }),
7419
8037
  summary?.zh !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7420
- className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summaryZh} ${_dsh_plugin_shop_css_e3675a89_default.summaryZhExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summaryZh,
8038
+ className: open ? `${_dsh_plugin_shop_css_201c343a_default.summaryZh} ${_dsh_plugin_shop_css_201c343a_default.summaryZhExpanded}` : _dsh_plugin_shop_css_201c343a_default.summaryZh,
7421
8039
  children: summary.zh
7422
8040
  }),
7423
- blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7424
- className: _dsh_plugin_shop_css_e3675a89_default.incompatibleDetail,
7425
- "data-shop-incompatible-detail": true,
7426
- children: blocker.text
8041
+ blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
8042
+ blocker,
8043
+ className: _dsh_plugin_shop_css_201c343a_default.incompatibleDetail,
8044
+ surface: "detail"
7427
8045
  }, blocker.kind)),
7428
8046
  open && entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7429
- className: _dsh_plugin_shop_css_e3675a89_default.capabilitiesBlock,
8047
+ className: _dsh_plugin_shop_css_201c343a_default.capabilitiesBlock,
7430
8048
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7431
- className: _dsh_plugin_shop_css_e3675a89_default.capabilitiesNote,
8049
+ className: _dsh_plugin_shop_css_201c343a_default.capabilitiesNote,
7432
8050
  children: t("capabilitiesNote")
7433
8051
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
7434
- className: _dsh_plugin_shop_css_e3675a89_default.capabilities,
8052
+ className: _dsh_plugin_shop_css_201c343a_default.capabilities,
7435
8053
  children: entry.catalog.capabilities.map((capability) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: capability }, capability))
7436
8054
  })]
7437
8055
  }),
7438
8056
  open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7439
8057
  id: detailId,
7440
- className: _dsh_plugin_shop_css_e3675a89_default.detail,
8058
+ className: _dsh_plugin_shop_css_201c343a_default.detail,
7441
8059
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
7442
- className: _dsh_plugin_shop_css_e3675a89_default.detailRows,
8060
+ className: _dsh_plugin_shop_css_201c343a_default.detailRows,
7443
8061
  children: [
7444
8062
  npmUrl !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7445
- className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
8063
+ className: _dsh_plugin_shop_css_201c343a_default.detailRow,
7446
8064
  "data-shop-npm": true,
7447
8065
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("npmPage") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7448
8066
  href: npmUrl,
@@ -7452,7 +8070,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7452
8070
  }) })]
7453
8071
  }),
7454
8072
  entry.repository !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7455
- className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
8073
+ className: _dsh_plugin_shop_css_201c343a_default.detailRow,
7456
8074
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("repository") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /^https?:\/\//.test(entry.repository) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7457
8075
  href: entry.repository,
7458
8076
  target: "_blank",
@@ -7461,12 +8079,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7461
8079
  }) : entry.repository })]
7462
8080
  }),
7463
8081
  entry.license !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7464
- className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
8082
+ className: _dsh_plugin_shop_css_201c343a_default.detailRow,
7465
8083
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("license") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: isCustomLicense(entry.license) ? t("customLicense") : entry.license })]
7466
8084
  })
7467
8085
  ]
7468
8086
  }), entry.tier === "verified-stale" && entry.review !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7469
- className: _dsh_plugin_shop_css_e3675a89_default.reviewedLine,
8087
+ className: _dsh_plugin_shop_css_201c343a_default.reviewedLine,
7470
8088
  children: entry.review.reviewedVersion !== void 0 ? t("reviewedVersionLine", {
7471
8089
  reviewed: entry.review.reviewedVersion,
7472
8090
  current: entry.version
@@ -7479,14 +8097,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7479
8097
  ]
7480
8098
  }),
7481
8099
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7482
- className: _dsh_plugin_shop_css_e3675a89_default.cardActions,
8100
+ className: _dsh_plugin_shop_css_201c343a_default.cardActions,
7483
8101
  "data-shop-actions": true,
7484
8102
  children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!inInstalledView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
7485
8103
  target: installTarget,
7486
8104
  tier: entry.tier,
7487
- missing,
7488
8105
  blockers,
7489
- missingStated: true,
8106
+ blockersStated: true,
7490
8107
  flow,
7491
8108
  t,
7492
8109
  restart,
@@ -7504,16 +8121,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7504
8121
  target: installTarget,
7505
8122
  tier: entry.tier,
7506
8123
  variant: installed.outdated ? "update" : "install",
7507
- missing,
7508
8124
  blockers,
7509
- missingStated: true,
8125
+ blockersStated: true,
7510
8126
  flow,
7511
8127
  t,
7512
8128
  restart,
7513
8129
  restartBlocked,
7514
8130
  reload
7515
8131
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7516
- className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
8132
+ className: _dsh_plugin_shop_css_201c343a_default.installedLabel,
7517
8133
  "data-shop-installed": true,
7518
8134
  children: t("installed")
7519
8135
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
@@ -7524,6 +8140,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7524
8140
  row: installed,
7525
8141
  t,
7526
8142
  setEnabled,
8143
+ restart,
8144
+ restartBlocked,
7527
8145
  reload
7528
8146
  }),
7529
8147
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
@@ -7535,16 +8153,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7535
8153
  flow: uninstallFlow
7536
8154
  })
7537
8155
  ] }), (size !== void 0 || author !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7538
- className: _dsh_plugin_shop_css_e3675a89_default.cardMeta,
8156
+ className: _dsh_plugin_shop_css_201c343a_default.cardMeta,
7539
8157
  children: [size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7540
- className: _dsh_plugin_shop_css_e3675a89_default.size,
8158
+ className: _dsh_plugin_shop_css_201c343a_default.size,
7541
8159
  "data-shop-size": true,
7542
8160
  role: "img",
7543
8161
  "aria-label": t("sizeLabel", { size }),
7544
8162
  title: t("sizeLabel", { size }),
7545
8163
  children: size
7546
8164
  }), author !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7547
- className: _dsh_plugin_shop_css_e3675a89_default.author,
8165
+ className: _dsh_plugin_shop_css_201c343a_default.author,
7548
8166
  "data-shop-author": true,
7549
8167
  children: t("authorLine", { author })
7550
8168
  })]
@@ -7554,41 +8172,95 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7554
8172
  });
7555
8173
  });
7556
8174
  /**
7557
- * Everything standing in this entry's way, already localized.
8175
+ * Everything standing in this entry's way, localized ONCE per surface: the
8176
+ * detail paragraphs and the badge summarizing them read this one list, so
8177
+ * they can never be built from different inputs.
7558
8178
  *
7559
- * The two are independent and both can hold at once, so this is a list and
7560
- * every surface renders the same list. The name conflict reads first: it is
7561
- * about a plugin the reader chose and would lose.
8179
+ * The host sent facts — module names, the declared range and the running
8180
+ * version, the declared and the running profile — and the sentences are this
8181
+ * dictionary's, in the reader's own language (§4: no copy crosses the RPC).
7562
8182
  */
7563
- function blockersOf(missing, nameTakenBy, t) {
7564
- const blockers = [];
7565
- if (nameTakenBy !== void 0) blockers.push({
7566
- kind: "name-taken",
7567
- text: t("nameTakenDetail", { holder: nameTakenBy })
7568
- });
7569
- if (missing.length > 0) blockers.push({
7570
- kind: "missing-peers",
7571
- text: t("incompatibleDetail", { modules: missing.join(", ") })
8183
+ function stateBlockers(blockers, t) {
8184
+ return blockers.map((blocker) => {
8185
+ const text = blockerText(blocker, t);
8186
+ return blocker.kind === "harness-peers" && blocker.allowCommand !== null ? {
8187
+ kind: blocker.kind,
8188
+ text,
8189
+ remedy: {
8190
+ lead: t("harnessPeersRemedy"),
8191
+ command: blocker.allowCommand
8192
+ }
8193
+ } : {
8194
+ kind: blocker.kind,
8195
+ text
8196
+ };
7572
8197
  });
7573
- return blockers;
8198
+ }
8199
+ /**
8200
+ * One blocker as a surface prints it: its sentence in the surface's own
8201
+ * paragraph style, and after it, for a refusal the reader can clear, the
8202
+ * lead-in and the command on a line of its own — selectable as a whole, since
8203
+ * it goes to a terminal unchanged. `surface` names the attribute a locator
8204
+ * reads: the card's detail line, or the warning the gate and the outdated
8205
+ * row's refusal print.
8206
+ */
8207
+ function BlockerLine({ blocker, className, surface }) {
8208
+ const attribute = surface === "detail" ? { "data-shop-incompatible-detail": blocker.kind } : { "data-shop-incompatible-warning": blocker.kind };
8209
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8210
+ className,
8211
+ ...attribute,
8212
+ children: blocker.text
8213
+ }), blocker.remedy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8214
+ className: _dsh_plugin_shop_css_201c343a_default.remedy,
8215
+ "data-shop-remedy": blocker.kind,
8216
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8217
+ className: _dsh_plugin_shop_css_201c343a_default.remedyLead,
8218
+ children: blocker.remedy.lead
8219
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
8220
+ className: _dsh_plugin_shop_css_201c343a_default.allowCommand,
8221
+ "data-shop-allow-command": true,
8222
+ children: blocker.remedy.command
8223
+ })]
8224
+ })] });
8225
+ }
8226
+ /** One blocker's sentence. Exhaustive with no default, so a kind added
8227
+ * to `Blocker` is a type error here rather than a line that renders nothing. */
8228
+ function blockerText(blocker, t) {
8229
+ switch (blocker.kind) {
8230
+ case "name-taken": return t("nameTakenDetail", { holder: blocker.holder });
8231
+ case "harness-peers": return t("harnessPeersDetail", {
8232
+ running: blocker.running,
8233
+ peers: refusedPeersText(blocker.refused)
8234
+ });
8235
+ case "missing-peers": return t("incompatibleDetail", { modules: blocker.modules.join(", ") });
8236
+ case "harness-range": return t("harnessRangeDetail", {
8237
+ range: blocker.range,
8238
+ running: blocker.running
8239
+ });
8240
+ case "harness-profile": return t("harnessProfileDetail", {
8241
+ declared: blocker.declared.join(", "),
8242
+ running: blocker.running
8243
+ });
8244
+ }
7574
8245
  }
7575
8246
  /** The harness-compatibility verdict, rendered beside the control it
7576
8247
  * qualifies. It answers "what happens if I press this", not "what is this",
7577
8248
  * so it belongs to the action row and not among the identity badges in the
7578
8249
  * header — where it also sat inside a <button>, competing with that button's
7579
- * own hit area for the tooltip. Renders nothing when the harness provides
7580
- * everything. */
8250
+ * own hit area for the tooltip. Renders nothing when nothing stands in the
8251
+ * way. */
7581
8252
  function BlockerBadge({ blockers, t }) {
7582
- if (blockers.length === 0) return null;
8253
+ const lead = blockers[0];
8254
+ const word = blockerBadgeKey(blockers);
8255
+ if (lead === void 0 || word === null) return null;
7583
8256
  const detail = blockers.map((blocker) => blocker.text).join("\n");
7584
- const taken = blockers.some((blocker) => blocker.kind === "name-taken");
7585
8257
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7586
- className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
7587
- "data-shop-blocker": taken ? "name-taken" : "missing-peers",
8258
+ className: _dsh_plugin_shop_css_201c343a_default.incompatibleBadge,
8259
+ "data-shop-blocker": lead.kind,
7588
8260
  role: "img",
7589
8261
  "aria-label": detail,
7590
8262
  title: detail,
7591
- children: t(taken ? "nameTakenBadge" : "incompatibleBadge")
8263
+ children: t(word)
7592
8264
  });
7593
8265
  }
7594
8266
  /** One entry's install flow: the button, the §9.3 acknowledgement gate for
@@ -7596,37 +8268,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7596
8268
  * failure detail, rejection detail — driven by `useInstallFlows`. Shared by the
7597
8269
  * catalog cards (`variant: 'install'`) and the outdated rows' update button
7598
8270
  * (`variant: 'update'`, which drives the same install flow for `name@latest`). */
7599
- function InstallPanel({ target, tier, missing, blockers, missingStated = false, variant = "install", flow, t, restart, restartBlocked, reload }) {
8271
+ function InstallPanel({ target, tier, blockers, blockersStated = false, variant = "install", flow, t, restart, restartBlocked, reload }) {
7600
8272
  const [gateOpen, setGateOpen] = (0, react.useState)(false);
7601
8273
  const { view, start } = flow;
7602
8274
  if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7603
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8275
+ className: _dsh_plugin_shop_css_201c343a_default.installPanel,
7604
8276
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7605
- className: _dsh_plugin_shop_css_e3675a89_default.installing,
8277
+ className: _dsh_plugin_shop_css_201c343a_default.installing,
7606
8278
  children: t(installPhaseKey(view.phase))
7607
8279
  }), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7608
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8280
+ className: _dsh_plugin_shop_css_201c343a_default.log,
7609
8281
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7610
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8282
+ className: _dsh_plugin_shop_css_201c343a_default.logLine,
8283
+ "data-shop-log-line": true,
7611
8284
  children: line
7612
8285
  }, index))
7613
8286
  })]
7614
8287
  });
7615
8288
  if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7616
- className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
8289
+ className: _dsh_plugin_shop_css_201c343a_default.installedActions,
7617
8290
  children: [
7618
8291
  view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7619
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8292
+ className: _dsh_plugin_shop_css_201c343a_default.log,
7620
8293
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7621
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8294
+ className: _dsh_plugin_shop_css_201c343a_default.logLine,
8295
+ "data-shop-log-line": true,
7622
8296
  children: line
7623
8297
  }, index))
7624
8298
  }),
7625
8299
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7626
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8300
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
7627
8301
  "data-shop-restart-notice": true,
7628
8302
  children: t(activationNoticeKey(view.activation, view.restartReason))
7629
8303
  }),
8304
+ view.detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8305
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
8306
+ "data-shop-done-note": true,
8307
+ children: view.detail
8308
+ }),
7630
8309
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActivationOffer, {
7631
8310
  activation: view.activation,
7632
8311
  t,
@@ -7638,56 +8317,57 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7638
8317
  ]
7639
8318
  });
7640
8319
  if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7641
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8320
+ className: _dsh_plugin_shop_css_201c343a_default.installPanel,
7642
8321
  children: [
7643
8322
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7644
- className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
8323
+ className: _dsh_plugin_shop_css_201c343a_default.failedHeading,
7645
8324
  children: t("installFailed")
7646
8325
  }),
7647
8326
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7648
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8327
+ className: _dsh_plugin_shop_css_201c343a_default.log,
7649
8328
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7650
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8329
+ className: _dsh_plugin_shop_css_201c343a_default.logLine,
8330
+ "data-shop-log-line": true,
7651
8331
  children: line
7652
8332
  }, index))
7653
8333
  }),
7654
8334
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7655
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
8335
+ className: _dsh_plugin_shop_css_201c343a_default.failedDetail,
7656
8336
  children: view.detail === "" ? t("installTransportFailed") : view.detail
7657
8337
  })
7658
8338
  ]
7659
8339
  });
7660
8340
  if (view.kind === "rejected") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7661
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8341
+ className: _dsh_plugin_shop_css_201c343a_default.installPanel,
7662
8342
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7663
- className: _dsh_plugin_shop_css_e3675a89_default.rejectedCode,
8343
+ className: _dsh_plugin_shop_css_201c343a_default.rejectedCode,
7664
8344
  children: t(rejectionCodeKey(view.code))
7665
8345
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7666
- className: _dsh_plugin_shop_css_e3675a89_default.rejectedDetail,
8346
+ className: _dsh_plugin_shop_css_201c343a_default.rejectedDetail,
7667
8347
  children: view.detail
7668
8348
  })]
7669
8349
  });
7670
8350
  if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7671
- className: _dsh_plugin_shop_css_e3675a89_default.gate,
8351
+ className: _dsh_plugin_shop_css_201c343a_default.gate,
7672
8352
  children: [
7673
8353
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7674
- className: _dsh_plugin_shop_css_e3675a89_default.gateTitle,
8354
+ className: _dsh_plugin_shop_css_201c343a_default.gateTitle,
7675
8355
  children: t("acknowledgementTitle")
7676
8356
  }),
7677
8357
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7678
- className: _dsh_plugin_shop_css_e3675a89_default.gateBody,
8358
+ className: _dsh_plugin_shop_css_201c343a_default.gateBody,
7679
8359
  children: t("acknowledgementBody")
7680
8360
  }),
7681
- !missingStated && blockersOf(missing, void 0, t).map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7682
- className: _dsh_plugin_shop_css_e3675a89_default.gateWarning,
7683
- "data-shop-incompatible-warning": true,
7684
- children: blocker.text
8361
+ !blockersStated && blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
8362
+ blocker,
8363
+ className: _dsh_plugin_shop_css_201c343a_default.gateWarning,
8364
+ surface: "warning"
7685
8365
  }, blocker.kind)),
7686
8366
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7687
- className: _dsh_plugin_shop_css_e3675a89_default.gateActions,
8367
+ className: _dsh_plugin_shop_css_201c343a_default.gateActions,
7688
8368
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7689
8369
  type: "button",
7690
- className: _dsh_plugin_shop_css_e3675a89_default.confirmButton,
8370
+ className: _dsh_plugin_shop_css_201c343a_default.confirmButton,
7691
8371
  "data-shop-confirm": true,
7692
8372
  onClick: () => {
7693
8373
  setGateOpen(false);
@@ -7699,7 +8379,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7699
8379
  children: t("confirm")
7700
8380
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7701
8381
  type: "button",
7702
- className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
8382
+ className: _dsh_plugin_shop_css_201c343a_default.cancelButton,
7703
8383
  onClick: () => setGateOpen(false),
7704
8384
  children: t("cancel")
7705
8385
  })]
@@ -7707,25 +8387,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7707
8387
  ]
7708
8388
  });
7709
8389
  const update = variant === "update";
7710
- const refused = blockers.some((blocker) => blocker.kind === "name-taken");
7711
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7712
- type: "button",
7713
- className: _dsh_plugin_shop_css_e3675a89_default.installButton,
7714
- disabled: refused,
7715
- ...refused ? { "data-shop-refused": true } : {},
7716
- ...update ? { "data-shop-update": true } : { "data-shop-install": true },
7717
- onClick: () => {
7718
- if (tier === "verified") start({
7719
- ...target,
7720
- acknowledged: void 0
7721
- });
7722
- else setGateOpen(true);
7723
- },
7724
- children: t(update ? "update" : "install")
7725
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
7726
- blockers,
7727
- t
7728
- })] });
8390
+ const refused = refusesInstall(blockers);
8391
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
8392
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8393
+ type: "button",
8394
+ className: _dsh_plugin_shop_css_201c343a_default.installButton,
8395
+ disabled: refused,
8396
+ ...refused ? { "data-shop-refused": true } : {},
8397
+ ...update ? { "data-shop-update": true } : { "data-shop-install": true },
8398
+ onClick: () => {
8399
+ if (tier === "verified") start({
8400
+ ...target,
8401
+ acknowledged: void 0
8402
+ });
8403
+ else setGateOpen(true);
8404
+ },
8405
+ children: t(update ? "update" : "install")
8406
+ }),
8407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
8408
+ blockers,
8409
+ t
8410
+ }),
8411
+ refused && !blockersStated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8412
+ className: _dsh_plugin_shop_css_201c343a_default.refusal,
8413
+ "data-shop-refusal": true,
8414
+ children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
8415
+ blocker,
8416
+ className: _dsh_plugin_shop_css_201c343a_default.gateWarning,
8417
+ surface: "warning"
8418
+ }, blocker.kind))
8419
+ })
8420
+ ] });
7729
8421
  }
7730
8422
  /** One installed entry's uninstall flow: remove from the profile through the
7731
8423
  * same executor records and poll loop as installs. Uninstalling revokes
@@ -7736,23 +8428,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7736
8428
  function UninstallPanel({ name, t, restart, restartBlocked, reload, flow }) {
7737
8429
  const { view, start, reset } = flow;
7738
8430
  if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7739
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8431
+ className: _dsh_plugin_shop_css_201c343a_default.installPanel,
7740
8432
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7741
- className: _dsh_plugin_shop_css_e3675a89_default.installing,
8433
+ className: _dsh_plugin_shop_css_201c343a_default.installing,
7742
8434
  children: t("uninstalling")
7743
8435
  }), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7744
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8436
+ className: _dsh_plugin_shop_css_201c343a_default.log,
7745
8437
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7746
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8438
+ className: _dsh_plugin_shop_css_201c343a_default.logLine,
8439
+ "data-shop-log-line": true,
7747
8440
  children: line
7748
8441
  }, index))
7749
8442
  })]
7750
8443
  });
7751
8444
  if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7752
- className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
8445
+ className: _dsh_plugin_shop_css_201c343a_default.installedActions,
7753
8446
  children: [
8447
+ view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8448
+ className: _dsh_plugin_shop_css_201c343a_default.log,
8449
+ children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8450
+ className: _dsh_plugin_shop_css_201c343a_default.logLine,
8451
+ "data-shop-log-line": true,
8452
+ children: line
8453
+ }, index))
8454
+ }),
7754
8455
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7755
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8456
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
7756
8457
  "data-shop-uninstall-done": true,
7757
8458
  children: t(uninstallActivationNoticeKey(view.activation))
7758
8459
  }),
@@ -7766,7 +8467,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7766
8467
  }),
7767
8468
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7768
8469
  type: "button",
7769
- className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
8470
+ className: _dsh_plugin_shop_css_201c343a_default.cancelButton,
7770
8471
  "data-shop-uninstall-dismiss": true,
7771
8472
  onClick: reset,
7772
8473
  children: t("dismiss")
@@ -7774,26 +8475,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7774
8475
  ]
7775
8476
  });
7776
8477
  if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7777
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8478
+ className: _dsh_plugin_shop_css_201c343a_default.installPanel,
7778
8479
  children: [
7779
8480
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7780
- className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
8481
+ className: _dsh_plugin_shop_css_201c343a_default.failedHeading,
7781
8482
  children: t("uninstallFailed")
7782
8483
  }),
7783
8484
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7784
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8485
+ className: _dsh_plugin_shop_css_201c343a_default.log,
7785
8486
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7786
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8487
+ className: _dsh_plugin_shop_css_201c343a_default.logLine,
8488
+ "data-shop-log-line": true,
7787
8489
  children: line
7788
8490
  }, index))
7789
8491
  }),
7790
8492
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7791
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
8493
+ className: _dsh_plugin_shop_css_201c343a_default.failedDetail,
7792
8494
  children: view.detail === "" ? t("uninstallTransportFailed") : view.detail
7793
8495
  }),
7794
8496
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7795
8497
  type: "button",
7796
- className: _dsh_plugin_shop_css_e3675a89_default.uninstallButton,
8498
+ className: _dsh_plugin_shop_css_201c343a_default.uninstallButton,
7797
8499
  "data-shop-uninstall-retry": true,
7798
8500
  onClick: () => void start({ name }),
7799
8501
  children: t("retry")
@@ -7803,113 +8505,97 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7803
8505
  if (view.kind !== "idle") return null;
7804
8506
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7805
8507
  type: "button",
7806
- className: _dsh_plugin_shop_css_e3675a89_default.uninstallButton,
8508
+ className: _dsh_plugin_shop_css_201c343a_default.uninstallButton,
7807
8509
  "data-shop-uninstall": true,
7808
8510
  onClick: () => void start({ name }),
7809
8511
  children: t("uninstall")
7810
8512
  });
7811
8513
  }
8514
+ /** The page's restart, re-rendering whenever it changes state. Shared by
8515
+ * every restart offer and by the version row, in this tab and in whatever
8516
+ * tab an HMR swap mounts in its place. */
8517
+ function useRestartMonitor() {
8518
+ return (0, react.useSyncExternalStore)(subscribeRestartMonitor, readRestartMonitor);
8519
+ }
7812
8520
  /** The §8 restart flow (amendment 2026-08-27): after an install, update, or
7813
8521
  * uninstall reports done, this panel offers a restart of dsh. The
7814
8522
  * confirmation gate states the cost — the page disconnects and in-flight
7815
- * conversations/tasks are interrupted — and on confirm the restart RPC
7816
- * commits the two-phase handoff: the host exits, a helper re-runs dsh, and
7817
- * this panel polls the origin after a grace period, refreshing the page
7818
- * once the NEW server answers. A refused restart renders the host's
7819
- * published detail; a server that never comes back names the manual
7820
- * command. */
7821
- function RestartPanel({ t, restart, gate }) {
8523
+ * conversations/tasks are interrupted — and on confirm the page asks the host
8524
+ * (restart-monitor.ts), which commits the two-phase handoff: the host exits,
8525
+ * a helper re-runs dsh, and the page's monitor polls the origin after a grace
8526
+ * period, refreshing the page once the NEW server keeps answering. A refused
8527
+ * restart renders the host's published detail above the offer, which stays;
8528
+ * a server that never comes back names the manual command. */
8529
+ function RestartPanel({ t, restart, reload, gate }) {
7822
8530
  const [ownGateOpen, setOwnGateOpen] = (0, react.useState)(false);
7823
8531
  const gateOpen = gate === void 0 ? ownGateOpen : gate.open;
7824
8532
  const setGateOpen = (open) => {
7825
8533
  if (gate === void 0) setOwnGateOpen(open);
7826
8534
  else if (!open) gate.close();
7827
8535
  };
7828
- const [state, setState] = (0, react.useState)({ kind: "idle" });
8536
+ const [refusal, setRefusal] = (0, react.useState)(null);
8537
+ const monitor = useRestartMonitor();
7829
8538
  const onConfirm = async () => {
7830
8539
  setGateOpen(false);
7831
- try {
7832
- const result = await restart();
7833
- if (!result.ok) {
7834
- setState({
7835
- kind: "failed",
7836
- detail: result.detail
7837
- });
7838
- return;
7839
- }
7840
- setState({ kind: "restarting" });
7841
- } catch {
7842
- setState({
7843
- kind: "failed",
7844
- detail: t("restartTransportFailed")
7845
- });
7846
- }
8540
+ setRefusal(null);
8541
+ const outcome = await requestRestart({
8542
+ reload,
8543
+ restart
8544
+ });
8545
+ if (outcome.kind === "refused") setRefusal(outcome.detail);
8546
+ else if (outcome.kind === "unreachable") setRefusal(t("restartTransportFailed"));
7847
8547
  };
7848
- (0, react.useEffect)(() => {
7849
- if (state.kind !== "restarting") return;
7850
- const started = Date.now();
7851
- const timer = setInterval(() => {
7852
- const elapsed = Date.now() - started;
7853
- if (elapsed < 3e3) return;
7854
- fetch(window.location.href, { cache: "no-store" }).then(() => {
7855
- window.location.reload();
7856
- }, () => {});
7857
- if (elapsed > 3e4) {
7858
- clearInterval(timer);
7859
- setState({
7860
- kind: "failed",
7861
- detail: t("restartFailedNotice")
7862
- });
7863
- }
7864
- }, INSTALL_POLL_MS);
7865
- return () => clearInterval(timer);
7866
- }, [state, t]);
7867
- if (state.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7868
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8548
+ if (monitor?.kind === "requesting" || monitor?.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8549
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
7869
8550
  "data-shop-restarting": true,
7870
8551
  children: t("restarting")
7871
8552
  });
7872
- if (state.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7873
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
8553
+ if (monitor?.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8554
+ className: _dsh_plugin_shop_css_201c343a_default.failedDetail,
7874
8555
  "data-shop-restart-error": true,
7875
- children: state.detail
8556
+ children: monitor.logFile === void 0 ? t("restartFailedNotice") : t("restartFailedLogNotice", { log: monitor.logFile })
7876
8557
  });
7877
8558
  if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7878
- className: _dsh_plugin_shop_css_e3675a89_default.gate,
8559
+ className: _dsh_plugin_shop_css_201c343a_default.gate,
7879
8560
  children: [
7880
8561
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7881
- className: _dsh_plugin_shop_css_e3675a89_default.gateTitle,
8562
+ className: _dsh_plugin_shop_css_201c343a_default.gateTitle,
7882
8563
  children: t("restartTitle")
7883
8564
  }),
7884
8565
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7885
- className: _dsh_plugin_shop_css_e3675a89_default.gateBody,
8566
+ className: _dsh_plugin_shop_css_201c343a_default.gateBody,
7886
8567
  children: t("restartBody")
7887
8568
  }),
7888
8569
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7889
- className: _dsh_plugin_shop_css_e3675a89_default.gateActions,
8570
+ className: _dsh_plugin_shop_css_201c343a_default.gateActions,
7890
8571
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7891
8572
  type: "button",
7892
- className: _dsh_plugin_shop_css_e3675a89_default.confirmButton,
8573
+ className: _dsh_plugin_shop_css_201c343a_default.confirmButton,
7893
8574
  "data-shop-restart-confirm": true,
7894
8575
  onClick: () => void onConfirm(),
7895
8576
  children: t("restartConfirm")
7896
8577
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7897
8578
  type: "button",
7898
- className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
8579
+ className: _dsh_plugin_shop_css_201c343a_default.cancelButton,
7899
8580
  onClick: () => setGateOpen(false),
7900
8581
  children: t("cancel")
7901
8582
  })]
7902
8583
  })
7903
8584
  ]
7904
8585
  });
7905
- if (gate !== void 0) return null;
7906
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8586
+ const refused = refusal === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8587
+ className: _dsh_plugin_shop_css_201c343a_default.failedDetail,
8588
+ "data-shop-restart-error": true,
8589
+ children: refusal
8590
+ });
8591
+ if (gate !== void 0) return refused;
8592
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [refused, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7907
8593
  type: "button",
7908
- className: _dsh_plugin_shop_css_e3675a89_default.restartButton,
8594
+ className: _dsh_plugin_shop_css_201c343a_default.restartButton,
7909
8595
  "data-shop-restart": true,
7910
8596
  onClick: () => setGateOpen(true),
7911
8597
  children: t("restart")
7912
- });
8598
+ })] });
7913
8599
  }
7914
8600
  /**
7915
8601
  * The §4/§8 offer under a settled install, update or uninstall: a reload
@@ -7933,9 +8619,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7933
8619
  if (activation !== "restart") return null;
7934
8620
  return restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
7935
8621
  t,
7936
- restart
8622
+ restart,
8623
+ reload
7937
8624
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7938
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8625
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
7939
8626
  "data-shop-restart-disabled": true,
7940
8627
  children: t(restartBlockedNoticeKey(restartBlocked))
7941
8628
  });
@@ -7946,14 +8633,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7946
8633
  * would help without knowing that now is a good time. */
7947
8634
  function ReloadPanel({ t, reload, where }) {
7948
8635
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7949
- className: _dsh_plugin_shop_css_e3675a89_default.reloadPanel,
8636
+ className: _dsh_plugin_shop_css_201c343a_default.reloadPanel,
7950
8637
  "data-shop-reload": where,
7951
8638
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7952
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8639
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
7953
8640
  children: t("reloadNote")
7954
8641
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7955
8642
  type: "button",
7956
- className: _dsh_plugin_shop_css_e3675a89_default.reloadButton,
8643
+ className: _dsh_plugin_shop_css_201c343a_default.reloadButton,
7957
8644
  onClick: reload,
7958
8645
  children: t("reload")
7959
8646
  })]
@@ -7966,10 +8653,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7966
8653
  * transport throw renders the localized failure line — its private detail
7967
8654
  * (which can name hosts and ports) never reaches the UI.
7968
8655
  */
7969
- function EnabledSwitch({ row, t, setEnabled, reload }) {
8656
+ function EnabledSwitch({ row, t, setEnabled, restart, restartBlocked, reload }) {
7970
8657
  const [enabled, setEnabledState] = (0, react.useState)(row.enabled);
7971
8658
  const [toggle, setToggle] = (0, react.useState)({ kind: "idle" });
7972
- const [needsReload, setNeedsReload] = (0, react.useState)(false);
8659
+ const [owed, setOwed] = (0, react.useState)("nothing");
7973
8660
  const onToggle = async () => {
7974
8661
  if (toggle.kind === "saving") return;
7975
8662
  const next = !enabled;
@@ -7982,7 +8669,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7982
8669
  if (result.ok) {
7983
8670
  setEnabledState(next);
7984
8671
  setToggle({ kind: "saved" });
7985
- if (result.activation !== "live") setNeedsReload(true);
8672
+ if (result.activation === "restart") setOwed("restart");
8673
+ else if (result.activation !== "live") setOwed((previous) => previous === "restart" ? previous : "reload");
7986
8674
  } else setToggle({
7987
8675
  kind: "error",
7988
8676
  detail: result.detail || t("toggleFailed")
@@ -7995,7 +8683,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7995
8683
  }
7996
8684
  };
7997
8685
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7998
- className: _dsh_plugin_shop_css_e3675a89_default.switchWrap,
8686
+ className: _dsh_plugin_shop_css_201c343a_default.switchWrap,
7999
8687
  "data-shop-enabled-switch": row.name,
8000
8688
  children: [
8001
8689
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -8004,23 +8692,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8004
8692
  "aria-checked": enabled,
8005
8693
  "aria-label": t("enabledSwitch"),
8006
8694
  "data-shop-toggle": true,
8007
- className: `${_dsh_plugin_shop_css_e3675a89_default.switch} ${enabled ? _dsh_plugin_shop_css_e3675a89_default.switchOn : ""}`,
8695
+ className: `${_dsh_plugin_shop_css_201c343a_default.switch} ${enabled ? _dsh_plugin_shop_css_201c343a_default.switchOn : ""}`,
8008
8696
  onClick: () => void onToggle(),
8009
8697
  disabled: toggle.kind === "saving",
8010
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.switchKnob })
8698
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_201c343a_default.switchKnob })
8011
8699
  }),
8012
- toggle.kind === "saved" && !needsReload && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8013
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8700
+ toggle.kind === "saved" && owed === "nothing" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8701
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
8014
8702
  "data-shop-hot-apply": true,
8015
8703
  children: t("hotApplyNote")
8016
8704
  }),
8017
- needsReload && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReloadPanel, {
8705
+ owed === "restart" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8706
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
8707
+ "data-shop-toggle-restart": true,
8708
+ children: t("toggleRestartNote")
8709
+ }),
8710
+ owed !== "nothing" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActivationOffer, {
8711
+ activation: owed,
8018
8712
  t,
8713
+ restart,
8714
+ restartBlocked,
8019
8715
  reload,
8020
8716
  where: "toggle"
8021
8717
  }),
8022
8718
  toggle.kind === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8023
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
8719
+ className: _dsh_plugin_shop_css_201c343a_default.failedDetail,
8024
8720
  "data-shop-toggle-error": true,
8025
8721
  children: toggle.detail
8026
8722
  })
@@ -8030,25 +8726,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8030
8726
  /** One outdated install row (§7.3): the name, the installed and latest
8031
8727
  * versions, the hot enable/disable switch, and the update button (the
8032
8728
  * install flow for `name@latest`, reusing `InstallPanel`). */
8033
- function OutdatedRow({ row, tier, missing, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
8729
+ function OutdatedRow({ row, tier, missing, harness, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
8034
8730
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8035
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedRow,
8731
+ className: _dsh_plugin_shop_css_201c343a_default.outdatedRow,
8036
8732
  "data-shop-outdated-entry": row.name,
8037
8733
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8038
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedInfo,
8734
+ className: _dsh_plugin_shop_css_201c343a_default.outdatedInfo,
8039
8735
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8040
- className: _dsh_plugin_shop_css_e3675a89_default.name,
8736
+ className: _dsh_plugin_shop_css_201c343a_default.name,
8041
8737
  children: row.name
8042
8738
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8043
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersions,
8739
+ className: _dsh_plugin_shop_css_201c343a_default.outdatedVersions,
8044
8740
  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 }) })]
8045
8741
  })]
8046
8742
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8047
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedActions,
8743
+ className: _dsh_plugin_shop_css_201c343a_default.outdatedActions,
8048
8744
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
8049
8745
  row,
8050
8746
  t,
8051
8747
  setEnabled,
8748
+ restart,
8749
+ restartBlocked,
8052
8750
  reload
8053
8751
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
8054
8752
  target: {
@@ -8060,8 +8758,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8060
8758
  },
8061
8759
  tier,
8062
8760
  variant: "update",
8063
- missing,
8064
- blockers: blockersOf(missing, void 0, t),
8761
+ blockers: stateBlockers(blockersOf(missing, harness, void 0), t),
8065
8762
  flow: flowFor(identityKey(row)),
8066
8763
  t,
8067
8764
  restart,
@@ -8088,29 +8785,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8088
8785
  * tier for the update gate is looked up from the catalog by install IDENTITY,
8089
8786
  * never by name (community → acknowledgement); an entry absent from the
8090
8787
  * catalog defaults to the community gate (the safer read). */
8091
- function OutdatedSection({ state, entriesByKey, missingByKey, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
8788
+ function OutdatedSection({ state, entriesByKey, missingByKey, harnessVerdicts, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
8092
8789
  if (state.kind === "loading") return null;
8093
8790
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8094
- className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
8791
+ className: _dsh_plugin_shop_css_201c343a_default.stateLine,
8095
8792
  "data-shop-outdated-error": true,
8096
8793
  children: t("installedError")
8097
8794
  });
8098
8795
  const outdated = state.entries.filter((entry) => entry.outdated);
8099
8796
  if (outdated.length === 0) return null;
8100
8797
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8101
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedSection,
8798
+ className: _dsh_plugin_shop_css_201c343a_default.outdatedSection,
8102
8799
  "data-shop-outdated": true,
8103
8800
  "aria-labelledby": UPDATABLE_HEADING_ID,
8104
8801
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
8105
- className: _dsh_plugin_shop_css_e3675a89_default.updatableHeading,
8802
+ className: _dsh_plugin_shop_css_201c343a_default.updatableHeading,
8106
8803
  id: UPDATABLE_HEADING_ID,
8107
8804
  children: t("updatableSection")
8108
8805
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
8109
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedList,
8806
+ className: _dsh_plugin_shop_css_201c343a_default.outdatedList,
8110
8807
  children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
8111
8808
  row,
8112
8809
  tier: entriesByKey.get(identityKey(row))?.tier ?? "community",
8113
8810
  missing: missingByKey.get(identityKey(row)) ?? [],
8811
+ harness: harnessVerdictOf(harnessVerdicts, identityKey(row)),
8114
8812
  t,
8115
8813
  setEnabled,
8116
8814
  flowFor,
@@ -8121,10 +8819,71 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8121
8819
  })]
8122
8820
  });
8123
8821
  }
8124
- /** The shop tab root: browse, search, refresh, and render one card per
8125
- * entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
8822
+ /**
8823
+ * The shop's own error boundary (design 2026-09-26-market-borrowings §4).
8824
+ *
8825
+ * The harness catches a crashing slot entry as well, but renders an empty
8826
+ * `<div data-slot-error>` that stays until the page reloads: a blank tab, with
8827
+ * no message and no way back. This one says what happened and offers a Retry
8828
+ * that remounts the tab from scratch. Its fallback carries no `data-shop-tab`,
8829
+ * so a crashed tab cannot satisfy anything waiting for a working one. A class,
8830
+ * because React offers error boundaries in no other form.
8831
+ */
8832
+ var ShopErrorBoundary = class extends react.Component {
8833
+ state = { error: null };
8834
+ static getDerivedStateFromError(error) {
8835
+ return { error: error instanceof Error ? error : new Error(String(error)) };
8836
+ }
8837
+ componentDidCatch(error, info) {
8838
+ console.error("dsh-plugin-shop: the shop tab crashed", error, info.componentStack);
8839
+ }
8840
+ render() {
8841
+ const { error } = this.state;
8842
+ if (error === null) return this.props.children;
8843
+ const say = (key, fallback) => {
8844
+ try {
8845
+ return this.props.t(key);
8846
+ } catch {
8847
+ return fallback;
8848
+ }
8849
+ };
8850
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8851
+ className: PANEL_ROOT,
8852
+ translate: "no",
8853
+ "data-shop-crashed": true,
8854
+ role: "alert",
8855
+ children: [
8856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8857
+ className: _dsh_plugin_shop_css_201c343a_default.stateLine,
8858
+ children: say("tabCrashed", en.tabCrashed)
8859
+ }),
8860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8861
+ className: _dsh_plugin_shop_css_201c343a_default.failedDetail,
8862
+ children: error.message
8863
+ }),
8864
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8865
+ type: "button",
8866
+ className: _dsh_plugin_shop_css_201c343a_default.actionButton,
8867
+ onClick: () => this.setState({ error: null }),
8868
+ children: say("retry", en.retry)
8869
+ })
8870
+ ]
8871
+ });
8872
+ }
8873
+ };
8874
+ /** The tab as the settings slot registers it (`index.ts`): the body inside the
8875
+ * shop's own error boundary. Exported under the name the body used to carry,
8876
+ * so nothing can register the tab without the boundary. */
8126
8877
  function ShopTab(props) {
8127
- const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, restart, version, updateStart, reload: injectedReload } = props;
8878
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopErrorBoundary, {
8879
+ t: props.t,
8880
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopTabBody, { ...props })
8881
+ });
8882
+ }
8883
+ /** The shop tab body: browse, search, refresh, and render one card per
8884
+ * entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
8885
+ function ShopTabBody(props) {
8886
+ const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, noteUninstalled, restart, version, updateStart, reload: injectedReload } = props;
8128
8887
  const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
8129
8888
  const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
8130
8889
  /** The install gate's own input, or undefined while it is unknown — loading,
@@ -8139,7 +8898,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8139
8898
  }), [injectedReload]);
8140
8899
  const [selfRestartGate, setSelfRestartGate] = (0, react.useState)(false);
8141
8900
  const selfUpdate = useUpdateSelf(updateStart, installStatus);
8901
+ const restartMonitor = useRestartMonitor();
8902
+ const hostPending = selfVersion !== null && selfVersion.pendingVersion !== null;
8903
+ const selfPending = selfUpdate.view.kind === "done" || selfUpdate.view.kind === "idle" && hostPending;
8142
8904
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
8905
+ const [versionReload, setVersionReload] = (0, react.useState)(0);
8143
8906
  const [mutations, setMutations] = (0, react.useState)(0);
8144
8907
  const noteMutation = (0, react.useCallback)(() => {
8145
8908
  setMutations((current) => current + 1);
@@ -8148,12 +8911,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8148
8911
  const flows = useInstallFlows(install, installStatus, (0, react.useCallback)((key, outcome) => {
8149
8912
  if (outcome === "done") uninstallResetRef.current(key);
8150
8913
  noteMutation();
8914
+ if (outcome === "done") setRequest({ kind: "reverdict" });
8151
8915
  }, [noteMutation]));
8152
8916
  const uninstallFlows = useUninstallFlows(uninstall, installStatus, (0, react.useCallback)((key, outcome) => {
8153
8917
  if (outcome !== "done") return;
8154
8918
  flows.flowFor(key).reset();
8155
8919
  noteMutation();
8156
- }, [flows, noteMutation]));
8920
+ const name = catalogState.kind === "ready" ? catalogState.result.plugins.find((entry) => entryKey(entry) === key)?.name : void 0;
8921
+ if (name !== void 0) noteUninstalled?.(name);
8922
+ setRequest({ kind: "reverdict" });
8923
+ }, [
8924
+ flows,
8925
+ noteMutation,
8926
+ catalogState,
8927
+ noteUninstalled
8928
+ ]));
8157
8929
  uninstallResetRef.current = uninstallFlows.resetFlow;
8158
8930
  const [reloading, setReloading] = (0, react.useState)(false);
8159
8931
  const [reloadFailed, setReloadFailed] = (0, react.useState)(false);
@@ -8164,21 +8936,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8164
8936
  const [visibleCount, setVisibleCount] = (0, react.useState)(48);
8165
8937
  const sentinelRef = (0, react.useRef)(null);
8166
8938
  const filteredLenRef = (0, react.useRef)(0);
8939
+ const pendingRefreshRef = (0, react.useRef)(null);
8167
8940
  (0, react.useEffect)(() => {
8168
8941
  let cancelled = false;
8169
- if (request.kind !== "refresh") setCatalogState((current) => current.kind === "ready" ? current : { kind: "loading" });
8942
+ if (request.kind !== "refresh" && request.kind !== "reverdict") setCatalogState((current) => current.kind === "ready" ? current : { kind: "loading" });
8943
+ if (request.kind === "refresh") pendingRefreshRef.current = request;
8170
8944
  const load = async () => {
8171
8945
  try {
8172
- const result = await catalog(request.kind === "refresh" ? { refresh: true } : void 0);
8946
+ const result = await catalog(request.kind === "refresh" ? { refresh: true } : request.kind === "reverdict" ? { reverdict: true } : void 0);
8173
8947
  if (!cancelled) setCatalogState({
8174
8948
  kind: "ready",
8175
8949
  result
8176
8950
  });
8177
8951
  } catch {
8178
- if (cancelled) return;
8952
+ if (cancelled) {
8953
+ if (request.kind === "refresh" && pendingRefreshRef.current === request) setReloadFailed(true);
8954
+ return;
8955
+ }
8179
8956
  setCatalogState((current) => current.kind === "ready" ? current : { kind: "error" });
8180
- setReloadFailed(true);
8957
+ if (request.kind !== "reverdict") setReloadFailed(true);
8181
8958
  } finally {
8959
+ if (pendingRefreshRef.current === request) pendingRefreshRef.current = null;
8182
8960
  if (!cancelled) setReloading(false);
8183
8961
  }
8184
8962
  };
@@ -8199,7 +8977,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8199
8977
  return () => {
8200
8978
  cancelled = true;
8201
8979
  };
8202
- }, [version, request]);
8980
+ }, [version, versionReload]);
8203
8981
  const [checkState, setCheckState] = (0, react.useState)("idle");
8204
8982
  const upToDateTimer = (0, react.useRef)(null);
8205
8983
  (0, react.useEffect)(() => () => {
@@ -8272,7 +9050,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8272
9050
  }
8273
9051
  return map;
8274
9052
  }, [catalogState, specs]);
8275
- const badgedIncompatible = (0, react.useCallback)((key) => (missingByKey.get(key) ?? []).length > 0 && !nameTakenByKey.has(key), [missingByKey, nameTakenByKey]);
9053
+ const harnessVerdicts = (0, react.useMemo)(() => catalogState.kind === "ready" ? catalogState.result.incompatibleHarness : void 0, [catalogState]);
9054
+ const badgedIncompatible = (0, react.useCallback)((key) => readsIncompatible(blockersOf(missingByKey.get(key) ?? [], harnessVerdictOf(harnessVerdicts, key), nameTakenByKey.get(key))), [
9055
+ missingByKey,
9056
+ harnessVerdicts,
9057
+ nameTakenByKey
9058
+ ]);
8276
9059
  const matched = (0, react.useMemo)(() => {
8277
9060
  const q = query.trim().toLowerCase();
8278
9061
  return (category === "installed" ? sortedCatalog : browsable).filter((entry) => {
@@ -8332,50 +9115,56 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8332
9115
  return map;
8333
9116
  }, [catalogState]);
8334
9117
  if (catalogState.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8335
- className: _dsh_plugin_shop_css_e3675a89_default.panel,
9118
+ className: PANEL_ROOT,
9119
+ translate: "no",
8336
9120
  "data-shop-tab": true,
8337
9121
  "aria-busy": "true",
8338
9122
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8339
- className: _dsh_plugin_shop_css_e3675a89_default.srOnly,
9123
+ className: _dsh_plugin_shop_css_201c343a_default.srOnly,
8340
9124
  children: t("loading")
8341
9125
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8342
- className: _dsh_plugin_shop_css_e3675a89_default.skeletonGrid,
9126
+ className: _dsh_plugin_shop_css_201c343a_default.skeletonGrid,
8343
9127
  "data-shop-skeleton": true,
8344
9128
  "aria-hidden": "true",
8345
9129
  children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8346
- className: _dsh_plugin_shop_css_e3675a89_default.skeletonCard,
9130
+ className: _dsh_plugin_shop_css_201c343a_default.skeletonCard,
8347
9131
  children: [
8348
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonName }),
8349
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonSummary }),
8350
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonSummaryShort }),
8351
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.skeletonActions })
9132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_201c343a_default.skeletonName }),
9133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_201c343a_default.skeletonSummary }),
9134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_201c343a_default.skeletonSummaryShort }),
9135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_201c343a_default.skeletonActions })
8352
9136
  ]
8353
9137
  }, i))
8354
9138
  })]
8355
9139
  });
8356
9140
  if (catalogState.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8357
- className: _dsh_plugin_shop_css_e3675a89_default.panel,
9141
+ className: PANEL_ROOT,
9142
+ translate: "no",
8358
9143
  "data-shop-tab": true,
8359
9144
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8360
- className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
9145
+ className: _dsh_plugin_shop_css_201c343a_default.stateLine,
8361
9146
  children: t("error")
8362
9147
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8363
9148
  type: "button",
8364
- className: _dsh_plugin_shop_css_e3675a89_default.actionButton,
8365
- onClick: () => setRequest({ kind: "retry" }),
9149
+ className: _dsh_plugin_shop_css_201c343a_default.actionButton,
9150
+ onClick: () => {
9151
+ setRequest({ kind: "retry" });
9152
+ setVersionReload((current) => current + 1);
9153
+ },
8366
9154
  children: t("retry")
8367
9155
  })]
8368
9156
  });
8369
9157
  const { result } = catalogState;
8370
9158
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8371
- className: _dsh_plugin_shop_css_e3675a89_default.panel,
9159
+ className: PANEL_ROOT,
9160
+ translate: "no",
8372
9161
  "data-shop-tab": true,
8373
9162
  children: [
8374
9163
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8375
- className: _dsh_plugin_shop_css_e3675a89_default.toolbar,
9164
+ className: _dsh_plugin_shop_css_201c343a_default.toolbar,
8376
9165
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8377
9166
  type: "search",
8378
- className: _dsh_plugin_shop_css_e3675a89_default.searchInput,
9167
+ className: _dsh_plugin_shop_css_201c343a_default.searchInput,
8379
9168
  "aria-label": t("search"),
8380
9169
  placeholder: t("search"),
8381
9170
  value: query,
@@ -8384,27 +9173,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8384
9173
  setVisibleCount(48);
8385
9174
  }
8386
9175
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8387
- className: _dsh_plugin_shop_css_e3675a89_default.toolbarRight,
9176
+ className: _dsh_plugin_shop_css_201c343a_default.toolbarRight,
8388
9177
  children: [
8389
9178
  result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8390
- className: _dsh_plugin_shop_css_e3675a89_default.staleBadge,
9179
+ className: _dsh_plugin_shop_css_201c343a_default.staleBadge,
8391
9180
  children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
8392
9181
  }),
8393
9182
  selfVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8394
- className: _dsh_plugin_shop_css_e3675a89_default.versionBlock,
9183
+ className: _dsh_plugin_shop_css_201c343a_default.versionBlock,
8395
9184
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8396
- className: _dsh_plugin_shop_css_e3675a89_default.versionText,
9185
+ className: _dsh_plugin_shop_css_201c343a_default.versionText,
8397
9186
  "data-shop-version": true,
8398
9187
  children: ["v", selfVersion.installed]
8399
- }), selfUpdate.view.kind === "done" && restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9188
+ }), restartMonitor !== null ? null : selfPending && restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8400
9189
  type: "button",
8401
- className: _dsh_plugin_shop_css_e3675a89_default.restartSelfButton,
9190
+ className: _dsh_plugin_shop_css_201c343a_default.restartSelfButton,
8402
9191
  "data-shop-restart": true,
8403
9192
  onClick: () => setSelfRestartGate(true),
8404
9193
  children: t("restart")
8405
- }) : selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9194
+ }) : !selfPending && selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8406
9195
  type: "button",
8407
- className: _dsh_plugin_shop_css_e3675a89_default.updateSelfButton,
9196
+ className: _dsh_plugin_shop_css_201c343a_default.updateSelfButton,
8408
9197
  "data-shop-update-self": true,
8409
9198
  onClick: () => {
8410
9199
  if (selfVersion.latest !== null) selfUpdate.start({ version: selfVersion.latest });
@@ -8412,7 +9201,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8412
9201
  children: t("update")
8413
9202
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8414
9203
  type: "button",
8415
- className: _dsh_plugin_shop_css_e3675a89_default.checkUpdateButton,
9204
+ className: _dsh_plugin_shop_css_201c343a_default.checkUpdateButton,
8416
9205
  "data-shop-check-update": true,
8417
9206
  title: t("checkUpdateTitle"),
8418
9207
  disabled: checkState !== "idle",
@@ -8421,7 +9210,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8421
9210
  })]
8422
9211
  }),
8423
9212
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
8424
- className: _dsh_plugin_shop_css_e3675a89_default.githubLink,
9213
+ className: _dsh_plugin_shop_css_201c343a_default.githubLink,
8425
9214
  href: SHOP_REPO_URL,
8426
9215
  target: "_blank",
8427
9216
  rel: "noopener noreferrer",
@@ -8440,39 +9229,41 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8440
9229
  })]
8441
9230
  }),
8442
9231
  selfUpdate.view.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8443
- className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
9232
+ className: _dsh_plugin_shop_css_201c343a_default.selfUpdatePanel,
8444
9233
  "data-shop-self-updating": true,
8445
9234
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8446
- className: _dsh_plugin_shop_css_e3675a89_default.installing,
9235
+ className: _dsh_plugin_shop_css_201c343a_default.installing,
8447
9236
  children: t(installPhaseKey(selfUpdate.view.phase))
8448
9237
  }), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8449
- className: _dsh_plugin_shop_css_e3675a89_default.log,
9238
+ className: _dsh_plugin_shop_css_201c343a_default.log,
8450
9239
  children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8451
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
9240
+ className: _dsh_plugin_shop_css_201c343a_default.logLine,
9241
+ "data-shop-log-line": true,
8452
9242
  children: line
8453
9243
  }, index))
8454
9244
  })]
8455
9245
  }),
8456
- selfUpdate.view.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8457
- className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
9246
+ selfPending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9247
+ className: _dsh_plugin_shop_css_201c343a_default.selfUpdatePanel,
8458
9248
  "data-shop-self-update-done": true,
8459
9249
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8460
- className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
9250
+ className: _dsh_plugin_shop_css_201c343a_default.installedActions,
8461
9251
  children: [
8462
9252
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8463
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
9253
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
8464
9254
  children: t("installedRestartNotice")
8465
9255
  }),
8466
9256
  restartBlocked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
8467
9257
  t,
8468
9258
  restart,
9259
+ reload,
8469
9260
  gate: {
8470
9261
  open: selfRestartGate,
8471
9262
  close: () => setSelfRestartGate(false)
8472
9263
  }
8473
9264
  }),
8474
9265
  restartBlocked !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8475
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
9266
+ className: _dsh_plugin_shop_css_201c343a_default.notice,
8476
9267
  "data-shop-restart-disabled": true,
8477
9268
  children: t(restartBlockedNoticeKey(restartBlocked))
8478
9269
  })
@@ -8480,34 +9271,35 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8480
9271
  })
8481
9272
  }),
8482
9273
  selfUpdate.view.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8483
- className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
9274
+ className: _dsh_plugin_shop_css_201c343a_default.selfUpdatePanel,
8484
9275
  "data-shop-self-update-failed": true,
8485
9276
  children: [
8486
9277
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8487
- className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
9278
+ className: _dsh_plugin_shop_css_201c343a_default.failedHeading,
8488
9279
  children: t("updateFailed")
8489
9280
  }),
8490
9281
  selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8491
- className: _dsh_plugin_shop_css_e3675a89_default.log,
9282
+ className: _dsh_plugin_shop_css_201c343a_default.log,
8492
9283
  children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8493
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
9284
+ className: _dsh_plugin_shop_css_201c343a_default.logLine,
9285
+ "data-shop-log-line": true,
8494
9286
  children: line
8495
9287
  }, index))
8496
9288
  }),
8497
9289
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8498
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
9290
+ className: _dsh_plugin_shop_css_201c343a_default.failedDetail,
8499
9291
  children: selfUpdate.view.detail === "" ? t("updateTransportFailed") : selfUpdate.view.detail
8500
9292
  })
8501
9293
  ]
8502
9294
  }),
8503
9295
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8504
- className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
9296
+ className: _dsh_plugin_shop_css_201c343a_default.categoryBar,
8505
9297
  role: "group",
8506
9298
  "aria-label": t("catalog"),
8507
9299
  children: [
8508
9300
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8509
9301
  type: "button",
8510
- className: category === null ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
9302
+ className: category === null ? `${_dsh_plugin_shop_css_201c343a_default.categoryButton} ${_dsh_plugin_shop_css_201c343a_default.categoryButtonOn}` : _dsh_plugin_shop_css_201c343a_default.categoryButton,
8511
9303
  "aria-pressed": category === null,
8512
9304
  onClick: () => {
8513
9305
  setCategory(null);
@@ -8522,7 +9314,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8522
9314
  }),
8523
9315
  CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8524
9316
  type: "button",
8525
- className: category === key ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
9317
+ className: category === key ? `${_dsh_plugin_shop_css_201c343a_default.categoryButton} ${_dsh_plugin_shop_css_201c343a_default.categoryButtonOn}` : _dsh_plugin_shop_css_201c343a_default.categoryButton,
8526
9318
  "aria-pressed": category === key,
8527
9319
  "data-category": key,
8528
9320
  "data-shop-category-tab": key,
@@ -8538,7 +9330,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8538
9330
  }, key)),
8539
9331
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8540
9332
  type: "button",
8541
- className: category === "installed" ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
9333
+ className: category === "installed" ? `${_dsh_plugin_shop_css_201c343a_default.categoryButton} ${_dsh_plugin_shop_css_201c343a_default.categoryButtonOn}` : _dsh_plugin_shop_css_201c343a_default.categoryButton,
8542
9334
  "aria-pressed": category === "installed",
8543
9335
  "data-shop-category-installed": true,
8544
9336
  onClick: () => {
@@ -8555,7 +9347,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8555
9347
  type: "button",
8556
9348
  role: "switch",
8557
9349
  "aria-checked": hideIncompatible,
8558
- className: hideIncompatible ? `${_dsh_plugin_shop_css_e3675a89_default.incompatibleFilter} ${_dsh_plugin_shop_css_e3675a89_default.incompatibleFilterOn}` : _dsh_plugin_shop_css_e3675a89_default.incompatibleFilter,
9350
+ className: hideIncompatible ? `${_dsh_plugin_shop_css_201c343a_default.incompatibleFilter} ${_dsh_plugin_shop_css_201c343a_default.incompatibleFilterOn}` : _dsh_plugin_shop_css_201c343a_default.incompatibleFilter,
8559
9351
  title: t(hideIncompatible ? "showIncompatibleTitle" : "incompatibleFilterTitle"),
8560
9352
  "data-shop-hide-incompatible": true,
8561
9353
  onClick: () => {
@@ -8563,18 +9355,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8563
9355
  setVisibleCount(48);
8564
9356
  },
8565
9357
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8566
- className: hideIncompatible ? `${_dsh_plugin_shop_css_e3675a89_default.switch} ${_dsh_plugin_shop_css_e3675a89_default.switchOn}` : _dsh_plugin_shop_css_e3675a89_default.switch,
9358
+ className: hideIncompatible ? `${_dsh_plugin_shop_css_201c343a_default.switch} ${_dsh_plugin_shop_css_201c343a_default.switchOn}` : _dsh_plugin_shop_css_201c343a_default.switch,
8567
9359
  "aria-hidden": "true",
8568
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.switchKnob })
9360
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_201c343a_default.switchKnob })
8569
9361
  }), t("hideIncompatible", { count: incompatibleCount })]
8570
9362
  })
8571
9363
  ]
8572
9364
  }),
8573
9365
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8574
- className: _dsh_plugin_shop_css_e3675a89_default.catalogStatsRow,
9366
+ className: _dsh_plugin_shop_css_201c343a_default.catalogStatsRow,
8575
9367
  children: [
8576
9368
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8577
- className: _dsh_plugin_shop_css_e3675a89_default.catalogStats,
9369
+ className: _dsh_plugin_shop_css_201c343a_default.catalogStats,
8578
9370
  "data-shop-catalog-stats": true,
8579
9371
  children: t("catalogStats", {
8580
9372
  count: String(browsable.length),
@@ -8583,32 +9375,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8583
9375
  }),
8584
9376
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8585
9377
  type: "button",
8586
- className: _dsh_plugin_shop_css_e3675a89_default.catalogRefreshButton,
9378
+ className: _dsh_plugin_shop_css_201c343a_default.catalogRefreshButton,
8587
9379
  "data-shop-catalog-refresh": true,
8588
9380
  disabled: reloading,
8589
9381
  onClick: () => {
8590
9382
  setReloading(true);
8591
9383
  setReloadFailed(false);
8592
9384
  setRequest({ kind: "refresh" });
9385
+ setVersionReload((current) => current + 1);
8593
9386
  },
8594
9387
  children: reloading ? t("refreshing") : t("refresh")
8595
9388
  }),
8596
9389
  reloadFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8597
- className: _dsh_plugin_shop_css_e3675a89_default.catalogRefreshFailed,
9390
+ className: _dsh_plugin_shop_css_201c343a_default.catalogRefreshFailed,
8598
9391
  "data-shop-catalog-refresh-failed": true,
8599
9392
  children: t("refreshFailed")
8600
9393
  })
8601
9394
  ]
8602
9395
  }),
8603
9396
  result.plugins.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8604
- className: _dsh_plugin_shop_css_e3675a89_default.emptyLine,
9397
+ className: _dsh_plugin_shop_css_201c343a_default.emptyLine,
8605
9398
  children: t("empty")
8606
9399
  }) : filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8607
- className: _dsh_plugin_shop_css_e3675a89_default.emptyLine,
9400
+ className: _dsh_plugin_shop_css_201c343a_default.emptyLine,
8608
9401
  "data-shop-empty": true,
8609
9402
  children: matched.length > 0 ? t("emptyIncompatibleFiltered") : t("emptySearch")
8610
9403
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
8611
- className: _dsh_plugin_shop_css_e3675a89_default.cards,
9404
+ className: _dsh_plugin_shop_css_201c343a_default.cards,
8612
9405
  children: [visible.map((entry) => {
8613
9406
  const key = entryKey(entry);
8614
9407
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
@@ -8616,6 +9409,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8616
9409
  stars: starsOf(entry, stars),
8617
9410
  installed: installedByKey.get(key),
8618
9411
  missing: missingByKey.get(key) ?? [],
9412
+ harness: harnessVerdictOf(harnessVerdicts, key),
8619
9413
  nameTakenBy: nameTakenByKey.get(key),
8620
9414
  t,
8621
9415
  flow: flows.flowFor(key),
@@ -8628,12 +9422,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8628
9422
  }) }, key);
8629
9423
  }), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
8630
9424
  ref: sentinelRef,
8631
- className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
9425
+ className: _dsh_plugin_shop_css_201c343a_default.cardsSentry,
8632
9426
  "data-shop-sentry": true,
8633
9427
  "aria-hidden": "true"
8634
9428
  })]
8635
9429
  }), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8636
- className: _dsh_plugin_shop_css_e3675a89_default.showingLine,
9430
+ className: _dsh_plugin_shop_css_201c343a_default.showingLine,
8637
9431
  "aria-live": "polite",
8638
9432
  children: t("showing", {
8639
9433
  shown: String(visibleCount),
@@ -8644,6 +9438,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8644
9438
  state: installedState,
8645
9439
  entriesByKey,
8646
9440
  missingByKey,
9441
+ harnessVerdicts,
8647
9442
  t,
8648
9443
  setEnabled,
8649
9444
  flowFor: flows.flowFor,
@@ -8670,6 +9465,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8670
9465
  * fire-and-forget from surfacing as an unhandled rejection. Every later
8671
9466
  * result, including an explicit refresh, replaces this timestamped stash. */
8672
9467
  let warmCatalog = null;
9468
+ /** Package names THIS PAGE has uninstalled since it loaded (design
9469
+ * 2026-09-01-harness-compatibility section 9.1): the module table can still
9470
+ * hold a graph row or a `loadCache` record for one after a restart-free
9471
+ * uninstall (module-table.ts's header explains why), so
9472
+ * the refinement must never clear a name here on the table's say-so — the
9473
+ * host's verdict stands unconditionally. Module-level so it survives the tab
9474
+ * closing and reopening; reset in `apply` beside `warmCatalog`, because a
9475
+ * re-applied bundle is a new page that has uninstalled nothing yet. An
9476
+ * uninstall made from another tab, another window, or the CLI is never added
9477
+ * here and stays unknown to this page until it reloads. */
9478
+ let pageRemoved = /* @__PURE__ */ new Set();
8673
9479
  /** Services required by the tab registration and the Remote mount.
8674
9480
  *
8675
9481
  * `remote.shop` is deliberately ABSENT: this package both mounts its own
@@ -8692,7 +9498,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8692
9498
  en
8693
9499
  }), "dsh-plugin-shop: dictionaries");
8694
9500
  const dispose = await ctx.remote.$mount(TYPERT_REMOTE);
8695
- ctx.effect(() => dispose, "dsh-plugin-shop: shop remote mount");
9501
+ let withdrawRestart = () => {};
9502
+ ctx.effect(() => () => {
9503
+ withdrawRestart();
9504
+ return dispose();
9505
+ }, "dsh-plugin-shop: shop remote mount");
8696
9506
  const ns = ctx.get("remote.shop");
8697
9507
  if (ns === void 0) throw new Error("dsh-plugin-shop: the shop remote did not register (service \"remote.shop\" missing after $mount)");
8698
9508
  const unwrap = (result) => {
@@ -8700,6 +9510,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8700
9510
  return result.value;
8701
9511
  };
8702
9512
  warmCatalog = null;
9513
+ pageRemoved = /* @__PURE__ */ new Set();
8703
9514
  const warmed = Promise.resolve(ns.catalog(void 0)).then((result) => unwrap(result));
8704
9515
  warmCatalog = {
8705
9516
  at: Date.now(),
@@ -8708,36 +9519,64 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8708
9519
  warmed.catch(() => {});
8709
9520
  Promise.resolve(ns.installed()).then((result) => unwrap(result)).catch(() => {});
8710
9521
  Promise.resolve(ns.version()).then((result) => unwrap(result)).catch(() => {});
8711
- const injected = () => ({
8712
- catalog: async (args) => {
8713
- if (args?.refresh === true) {
8714
- const refreshed = unwrap(await ns.catalog(args));
8715
- warmCatalog = {
8716
- at: Date.now(),
8717
- result: Promise.resolve(refreshed)
8718
- };
8719
- return refreshed;
8720
- }
8721
- const warm = warmCatalog;
8722
- if (warm !== null && Date.now() - warm.at < 3e5) try {
8723
- return await warm.result;
8724
- } catch {}
8725
- const fresh = unwrap(await ns.catalog(args));
9522
+ const hostCatalog = async (args) => {
9523
+ if (args?.refresh === true) {
9524
+ const refreshed = unwrap(await ns.catalog({ refresh: true }));
8726
9525
  warmCatalog = {
8727
9526
  at: Date.now(),
8728
- result: Promise.resolve(fresh)
9527
+ result: Promise.resolve(refreshed)
8729
9528
  };
8730
- return fresh;
9529
+ return refreshed;
9530
+ }
9531
+ if (args?.reverdict === true) {
9532
+ warmCatalog = null;
9533
+ const judged = unwrap(await ns.catalog(void 0));
9534
+ warmCatalog = {
9535
+ at: Date.now(),
9536
+ result: Promise.resolve(judged)
9537
+ };
9538
+ return judged;
9539
+ }
9540
+ const warm = warmCatalog;
9541
+ if (warm !== null && Date.now() - warm.at < 3e5) try {
9542
+ return await warm.result;
9543
+ } catch {}
9544
+ const fresh = unwrap(await ns.catalog(void 0));
9545
+ warmCatalog = {
9546
+ at: Date.now(),
9547
+ result: Promise.resolve(fresh)
9548
+ };
9549
+ return fresh;
9550
+ };
9551
+ const handOver = async (result) => ({
9552
+ ...result,
9553
+ incompatible: await refineAgainstModuleTable(result.incompatible ?? {}, ctx.get("modules"), pageRemoved)
9554
+ });
9555
+ const restart = async () => unwrap(await ns.restart());
9556
+ withdrawRestart = registerRestartCarrier(restart);
9557
+ const injected = () => ({
9558
+ catalog: async (args) => handOver(await hostCatalog(args)),
9559
+ install: async (args) => {
9560
+ warmCatalog = null;
9561
+ return unwrap(await ns.installStart(args));
8731
9562
  },
8732
- install: async (args) => unwrap(await ns.installStart(args)),
8733
9563
  installStatus: async (args) => unwrap(await ns.installStatus(args)),
8734
9564
  setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
8735
9565
  installed: async () => unwrap(await ns.installed()),
8736
9566
  installedSpecs: async () => unwrap(await ns.installedSpecs()),
8737
- uninstall: async (args) => unwrap(await ns.uninstallStart(args)),
8738
- restart: async () => unwrap(await ns.restart()),
9567
+ uninstall: async (args) => {
9568
+ warmCatalog = null;
9569
+ return unwrap(await ns.uninstallStart(args));
9570
+ },
9571
+ noteUninstalled: (name) => {
9572
+ pageRemoved.add(name);
9573
+ },
9574
+ restart,
8739
9575
  version: async () => unwrap(await ns.version()),
8740
- updateStart: async (args) => unwrap(await ns.updateStart(args))
9576
+ updateStart: async (args) => {
9577
+ warmCatalog = null;
9578
+ return unwrap(await ns.updateStart(args));
9579
+ }
8741
9580
  });
8742
9581
  ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
8743
9582
  name: "settings.plugins.tab",