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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -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": 1134,
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": 1494,
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": 1485,
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": 1260,
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": 1461,
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": 1680,
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": 887,
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": 1601,
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": 1777,
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": 1735,
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;
@@ -6680,6 +6845,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6680
6845
  error: "暂时无法读取目录。",
6681
6846
  installedError: "暂时无法读取已安装列表。",
6682
6847
  retry: "重试",
6848
+ /** The shop's own error boundary caught a render error; the error's message
6849
+ * follows on its own line, and Retry remounts the tab (design
6850
+ * 2026-09-26-market-borrowings §4). */
6851
+ tabCrashed: "插件商店在显示这一页时出错了。",
6683
6852
  search: "搜索插件",
6684
6853
  all: "全部",
6685
6854
  catalog: "插件目录",
@@ -6709,12 +6878,29 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6709
6878
  installedRestartNotice: "已安装;重启 dsh 后生效",
6710
6879
  incompatibleBadge: "不兼容",
6711
6880
  incompatibleDetail: "缺少组件/插件:{modules}\n此插件可能是为其他版本的 DSH 设计的。",
6881
+ /** The author's own `dsh.compatibility`, unmet on this machine (design
6882
+ * 2026-09-01 §8.2). Each names BOTH sides — what the author declared and
6883
+ * what this dsh runs — because the host sends both, and a bare
6884
+ * "unsupported" leaves the reader guessing which side is off. */
6885
+ harnessRangeDetail: "作者声明支持的 dsh 版本:{range};本机运行的是 {running}。",
6886
+ harnessProfileDetail: "作者声明支持的 profile:{declared};此 dsh 以 {running} profile 启动。",
6887
+ /** dsh's OWN refusal (0.1.7 on), not the author's claim: the install button
6888
+ * is disabled beside it (design 2026-09-26-dsh-017-readiness, B1). `{peers}`
6889
+ * is each refused peer with the range the package declared. The remedy
6890
+ * leads into the exemption command, rendered on its own line after it, and
6891
+ * names the Refresh button by its label. */
6892
+ harnessPeersDetail: "本机的 dsh {running} 会拒绝安装此插件:它要求 {peers}。",
6893
+ harnessPeersRemedy: "若愿意承担崩溃或数据丢失的风险,可在终端运行以下命令,为这个确切版本授予豁免,然后点击“刷新”:",
6712
6894
  hotNoPatchNotice: "该插件没有可热挂载的补丁文件;重启 dsh 后生效",
6713
6895
  hotNotSimpleNotice: "该插件的补丁包含无法热挂载的配置;重启 dsh 后生效",
6714
6896
  hotHostUnsupportedNotice: "当前环境不支持热挂载;重启 dsh 后生效",
6715
6897
  hotTimeoutNotice: "热挂载超时;重启 dsh 后生效",
6716
6898
  hotMountFailedNotice: "热挂载失败;重启 dsh 后生效",
6717
- hotClientHalfNotice: "已安装并在运行;它的界面要重启 dsh 才会更新——刷新页面取不到",
6899
+ /** No mount was tried: this dsh already loaded the package once, and Node
6900
+ * will not load a second copy from the same place (design
6901
+ * 2026-09-26-market-borrowings §1). Covers an update and a reinstall alike,
6902
+ * so it names the new files rather than a "new version". */
6903
+ hotAlreadyLoadedNotice: "已安装;这次运行的 dsh 已经加载过这个插件,新装的文件要重启 dsh 后才会生效",
6718
6904
  installedNoRestartNotice: "已安装并热挂载;现在即可使用,无需重启",
6719
6905
  installedReloadNotice: "已生效;刷新页面即可看到",
6720
6906
  reload: "刷新页面",
@@ -6739,9 +6925,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6739
6925
  /** The incompatible filter's label. FIXED — the switch beside it carries the
6740
6926
  * state, so the words name what the switch does and never flip with it. */
6741
6927
  hideIncompatible: "隐藏不兼容 {count}",
6742
- incompatibleFilterTitle: "不显示缺少组件的插件",
6743
- showIncompatibleTitle: "把缺少组件的插件放回列表",
6744
- emptyIncompatibleFiltered: "匹配的插件都缺少组件,已被「隐藏不兼容」筛掉。",
6928
+ /** What the filter takes away is every card whose badge reads 不兼容 —
6929
+ * missing components OR an author's declaration this dsh does not meet —
6930
+ * so these three name the badge, not one of its reasons. */
6931
+ incompatibleFilterTitle: "不显示标为不兼容的插件",
6932
+ showIncompatibleTitle: "把标为不兼容的插件放回列表",
6933
+ emptyIncompatibleFiltered: "匹配的插件都标为不兼容,已被「隐藏不兼容」筛掉。",
6745
6934
  license: "许可",
6746
6935
  customLicense: "自定义许可",
6747
6936
  reviewedVersionLine: "已审核 v{reviewed} / 当前 v{current} 未审核",
@@ -6753,6 +6942,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6753
6942
  tarballIntegrityCode: "压缩包完整性校验失败",
6754
6943
  ambiguousIdentityCode: "无法确定是哪一个条目",
6755
6944
  nameTakenCode: "同名的另一个插件已安装",
6945
+ desktopProfileCode: "由桌面版管理的 profile",
6756
6946
  nameTakenBadge: "名称被占用",
6757
6947
  nameTakenDetail: "已安装同名的另一个插件({holder})。\n两个同名插件声明相同的加载项 id,无法共存;安装它会替换掉已装的那个。",
6758
6948
  npmSource: "npm 包",
@@ -6775,7 +6965,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6775
6965
  restartConfirm: "重启",
6776
6966
  restarting: "正在重启,服务器就绪后页面会自动刷新…",
6777
6967
  restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
6968
+ /** The same failure, when the host said where the new process writes —
6969
+ * which is where a boot that died explains itself (design
6970
+ * 2026-09-26-market-borrowings §3). "Or did not stay up": the page now
6971
+ * waits for the new server to keep answering, so this also covers a boot
6972
+ * that answered and then died. */
6973
+ restartFailedLogNotice: "重启后服务器没有回来,或回来后没能保持运行。新进程的日志在 {log};也可以手动启动:dsh web",
6778
6974
  restartTransportFailed: "重启请求未能送达。请稍后重试。",
6975
+ restartBlockedDesktopNotice: "此 profile 由 DeepSeek Harness 桌面版管理,商店无法重启它。请重启桌面版,以应用更改。",
6779
6976
  restartBlockedWindowsNotice: "商店暂不支持在 Windows 上重启 dsh。请退出 dsh 后重新启动,以应用更改。",
6780
6977
  restartBlockedSystemdNotice: "当前 dsh 由 systemd 服务托管,商店无法安全地重启它。请手动重启服务,或在商店行配置中设置 allowRestart: true。",
6781
6978
  restartBlockedPortZeroNotice: "此 dsh 以 --port 0 启动,重启后会绑定到另一个端口,本页面无法跟上。请手动重启 dsh,以应用更改。",
@@ -6796,6 +6993,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6796
6993
  error: "The catalog is temporarily unavailable.",
6797
6994
  installedError: "The installed list is temporarily unavailable.",
6798
6995
  retry: "Retry",
6996
+ tabCrashed: "The plugin shop hit an error while showing this page.",
6799
6997
  search: "Search plugins",
6800
6998
  all: "All",
6801
6999
  catalog: "Plugin catalog",
@@ -6825,12 +7023,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6825
7023
  installedRestartNotice: "installed; restart dsh to activate",
6826
7024
  incompatibleBadge: "Incompatible",
6827
7025
  incompatibleDetail: "Missing components/plugins: {modules}\nThis plugin may be designed for another DSH version.",
7026
+ harnessRangeDetail: "The author declares support for dsh {range}; this machine runs {running}.",
7027
+ harnessProfileDetail: "The author declares support for the {declared} profile(s); this dsh was launched with the {running} profile.",
7028
+ harnessPeersDetail: "dsh {running} on this machine refuses to install this plugin: it requires {peers}.",
7029
+ harnessPeersRemedy: "To accept the risk of crashes or data loss for this exact version, run this in a terminal, then press Refresh:",
6828
7030
  hotNoPatchNotice: "no patch file to hot-mount; restart dsh to activate",
6829
7031
  hotNotSimpleNotice: "the patch has config rows that cannot be hot-mounted; restart dsh to activate",
6830
7032
  hotHostUnsupportedNotice: "this harness cannot hot-mount; restart dsh to activate",
6831
7033
  hotTimeoutNotice: "hot-mount timed out; restart dsh to activate",
6832
7034
  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",
7035
+ hotAlreadyLoadedNotice: "installed; this dsh has already loaded the plugin once, so the files just installed take effect after a restart of dsh",
6834
7036
  installedNoRestartNotice: "installed and hot-mounted; running now, no restart needed",
6835
7037
  installedReloadNotice: "already applied; reload the page to see it",
6836
7038
  reload: "Reload the page",
@@ -6847,9 +7049,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6847
7049
  authorLine: "Author: {author}",
6848
7050
  sizeLabel: "{size} on disk",
6849
7051
  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.",
7052
+ incompatibleFilterTitle: "Leave out plugins marked incompatible with this dsh",
7053
+ showIncompatibleTitle: "Put plugins marked incompatible back on the shelf",
7054
+ emptyIncompatibleFiltered: "Every matching plugin is marked incompatible, and the incompatible filter is leaving them out.",
6853
7055
  license: "License",
6854
7056
  customLicense: "Custom license",
6855
7057
  reviewedVersionLine: "reviewed v{reviewed} / current v{current} unreviewed",
@@ -6861,6 +7063,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6861
7063
  tarballIntegrityCode: "tarball integrity check failed",
6862
7064
  ambiguousIdentityCode: "Ambiguous entry",
6863
7065
  nameTakenCode: "Another plugin holds this name",
7066
+ desktopProfileCode: "Managed by the desktop app",
6864
7067
  nameTakenBadge: "Name taken",
6865
7068
  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
7069
  npmSource: "npm package",
@@ -6883,7 +7086,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6883
7086
  restartConfirm: "Restart",
6884
7087
  restarting: "Restarting; the page will refresh once the server is back…",
6885
7088
  restartFailedNotice: "The server did not come back after the restart. Start it manually: dsh web (see restart.log in the shop cache directory)",
7089
+ 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
7090
  restartTransportFailed: "The restart request could not be delivered. Please retry.",
7091
+ 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
7092
  restartBlockedWindowsNotice: "The shop cannot restart dsh on Windows yet. Quit dsh and start it again to apply the change.",
6888
7093
  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
7094
  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.",
@@ -6898,6 +7103,314 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6898
7103
  hotApplyNote: "applied without a restart"
6899
7104
  };
6900
7105
  //#endregion
7106
+ //#region src/client/module-table.ts
7107
+ /**
7108
+ * How long one probe may take before its answer counts as unknown.
7109
+ *
7110
+ * The work a probe can actually reach is a microtask: a seed-word lookup, or a
7111
+ * factory this page already registered. So this bounds a harness that never
7112
+ * answers, not work — and it exists because the cost of the alternative is the
7113
+ * whole shelf. `refineAgainstModuleTable` promises that a table in any state
7114
+ * cannot cost the reader the catalog, and an `import()` that never settles
7115
+ * would have made that promise false by leaving the tab with nothing to render
7116
+ * (§9's degradation rule: an unavailable fact is silence, never an accusation).
7117
+ * A probe per name, not one deadline for the whole refinement: every other
7118
+ * name is still judged, and the tab waits at most this long however many names
7119
+ * there are.
7120
+ */
7121
+ const MODULE_PROBE_TIMEOUT_MS = 2e3;
7122
+ /** `outcome`, or null once `ms` has passed — whichever settles first. The
7123
+ * timer is cleared either way, so a settled probe leaves nothing pending. */
7124
+ function withDeadline(outcome, ms) {
7125
+ let timer;
7126
+ const deadline = new Promise((resolve) => {
7127
+ timer = setTimeout(() => resolve(null), ms);
7128
+ });
7129
+ return Promise.race([outcome, deadline]).finally(() => {
7130
+ if (timer !== void 0) clearTimeout(timer);
7131
+ });
7132
+ }
7133
+ /**
7134
+ * The host's verdict less every name the oracle provides.
7135
+ *
7136
+ * An entry whose list empties is dropped, and an entry ANY of whose names
7137
+ * comes back unknown gets no verdict at all — the rule `incompatibilityMap`
7138
+ * follows on the host, for the host's reason: an unavailable fact must never
7139
+ * read as an accusation, and keeping the rest of a list whose one unknown name
7140
+ * was dropped would publish a partial answer as the whole one. Each distinct
7141
+ * name is asked once, however many entries declare it. Never rejects: any
7142
+ * failure of the oracle is an unknown.
7143
+ */
7144
+ async function refineIncompatible(incompatible, provides, timeoutMs = MODULE_PROBE_TIMEOUT_MS) {
7145
+ const answers = /* @__PURE__ */ new Map();
7146
+ const ask = (spec) => {
7147
+ const cached = answers.get(spec);
7148
+ if (cached !== void 0) return cached;
7149
+ const answer = withDeadline(new Promise((resolve) => {
7150
+ resolve(provides(spec));
7151
+ }).catch(() => null), timeoutMs);
7152
+ answers.set(spec, answer);
7153
+ return answer;
7154
+ };
7155
+ const judged = await Promise.all(Object.entries(incompatible).map(async ([key, names]) => {
7156
+ const verdicts = await Promise.all(names.map(ask));
7157
+ if (verdicts.some((verdict) => verdict !== true && verdict !== false)) return null;
7158
+ const missing = names.filter((_, index) => verdicts[index] === false);
7159
+ return missing.length > 0 ? {
7160
+ key,
7161
+ missing
7162
+ } : null;
7163
+ }));
7164
+ const refined = {};
7165
+ for (const row of judged) if (row !== null) refined[row.key] = row.missing;
7166
+ return refined;
7167
+ }
7168
+ /** The harness's own normalization (`stripClientSuffix`): a plugin bundle IS
7169
+ * its package's client half, so `x/client` and `x` name one row and one
7170
+ * record. Mirrored rather than imported — this package does not depend on
7171
+ * `@deepseek-ai/dsh-client-modules`. */
7172
+ function stripClientSuffix(spec) {
7173
+ return spec.endsWith("/client") ? spec.slice(0, -7) : spec;
7174
+ }
7175
+ /** The prefix of the one rejection that means "absent":
7176
+ * `ClientModuleSystem.import`'s own `client-modules: cannot resolve "<spec>" —
7177
+ * not a seed word, not a materialized module, and not a row in the boot
7178
+ * graph`. A harness that rewords it makes every probe unknown, which renders
7179
+ * as silence — the safe direction, never a false accusation. */
7180
+ const CANNOT_RESOLVE = "client-modules: cannot resolve";
7181
+ /**
7182
+ * An oracle over the live `modules` service, or null when the page offers no
7183
+ * usable table.
7184
+ *
7185
+ * The service is the harness's `ClientModuleLoader`, published to client
7186
+ * plugins as `ctx.reflect.provide('modules', …)`. It arrives as unknown-typed
7187
+ * foreign state, so only its PUBLIC contract is read, and only after
7188
+ * narrowing: `manifest.modules` must be an array, `loadCache` a Map and
7189
+ * `import` a function. `seed`, `factories` and `graphRows` exist on the
7190
+ * instance too, but they are TypeScript-`private` in the harness's own
7191
+ * declarations, and nothing here names them.
7192
+ *
7193
+ * Per spec, in the table's own order but with every side effect kept out:
7194
+ * 1. A graph row provides it, and `import` is NOT called — for a row it
7195
+ * fetches the bundle and runs the module body, and a probe must never be
7196
+ * what loads a plugin. `manifest.modules` is the row index, but it is not
7197
+ * fixed for the page's whole life on every harness build. On 0.1.5-rc.3
7198
+ * `this.manifest = options.manifest` is the constructor's only write, so a
7199
+ * row placed at boot stands unchanged for as long as the page lives. On
7200
+ * 0.1.7-rc.2 (`next`) `updateManifest` REPLACES `this.manifest` outright
7201
+ * with a new manifest that no longer lists an uninstalled package, and
7202
+ * `reconcile` removes the plugin from the loader — so UNTIL this page's own
7203
+ * uninstall reconciles, the OLD row still stands in `manifest.modules` and
7204
+ * this step clears the entry falsely; AFTER it reconciles, this step can no
7205
+ * longer be the false clear, because `manifest.modules` has already dropped
7206
+ * the package. The false clear then comes from `prune`, which keeps the
7207
+ * removed id only in the PRIVATE `graphRows` and in `loadCache`, and only
7208
+ * while some retained module still references it — that is step 2 below,
7209
+ * or the side effect of `import` in step 3 (a private graph row this
7210
+ * function cannot see).
7211
+ * 2. A `loadCache` record provides it, again without `import`.
7212
+ * 3. Otherwise `import(spec, '', {})`, with the spec as written — the seed
7213
+ * holds `react-dom/client` itself, not its stripped form. Resolving means a
7214
+ * seed word, whose lookup has no side effect; rejecting with the
7215
+ * "cannot resolve" message means absent; any other rejection is unknown.
7216
+ *
7217
+ * A kept-but-unmaterialized row is this function's own remaining blind spot:
7218
+ * asked about here, it still reaches step 3's `import` probe, which fetches
7219
+ * and runs it — the one side effect this module promises never to cause on
7220
+ * its own account. `refineAgainstModuleTable`'s page-removed set is the fix
7221
+ * for the one case a page can know about on its own — a name uninstalled
7222
+ * through THIS shop, on THIS page, since it loaded — by bypassing this oracle
7223
+ * for that name entirely rather than trusting a row that may only be kept. An
7224
+ * uninstall made from another tab, another window, or the CLI is not in that
7225
+ * set and stays unknown to this page until it reloads.
7226
+ *
7227
+ * One side-effect path remains: a registered page-local factory that is
7228
+ * neither a graph row nor yet materialized, which `import` materializes. That
7229
+ * is reachable only for a name the host could not resolve AND the page
7230
+ * registered without a row — the shell's own page-local modules are
7231
+ * node-resolvable, so the host never lists them. And what it does is what the
7232
+ * first real `require` of that name does anyway: run a factory this page
7233
+ * registered precisely so it would be materialized, once, memoized in
7234
+ * `loadCache`, fetching nothing. It moves that moment earlier; it runs nothing
7235
+ * the page did not already hold.
7236
+ */
7237
+ function moduleTableOracle(service) {
7238
+ if (typeof service !== "object" || service === null) return null;
7239
+ let rows;
7240
+ let loadCache;
7241
+ let probe;
7242
+ try {
7243
+ const { manifest, loadCache: cache, import: load } = service;
7244
+ const modules = typeof manifest === "object" && manifest !== null ? manifest.modules : void 0;
7245
+ if (!Array.isArray(modules) || !(cache instanceof Map) || typeof load !== "function") return null;
7246
+ rows = /* @__PURE__ */ new Set();
7247
+ for (const row of modules) {
7248
+ const id = typeof row === "object" && row !== null ? row.id : void 0;
7249
+ if (typeof id === "string") rows.add(id);
7250
+ }
7251
+ loadCache = cache;
7252
+ probe = (spec) => Promise.resolve(load.call(service, spec, "", {}));
7253
+ } catch {
7254
+ return null;
7255
+ }
7256
+ return async (spec) => {
7257
+ const id = stripClientSuffix(spec);
7258
+ if (rows.has(id) || loadCache.has(id)) return true;
7259
+ try {
7260
+ await probe(spec);
7261
+ return true;
7262
+ } catch (error) {
7263
+ return error instanceof Error && error.message.startsWith(CANNOT_RESOLVE) ? false : null;
7264
+ }
7265
+ };
7266
+ }
7267
+ /**
7268
+ * The host's verdict as this page can stand behind it: refined against the
7269
+ * live table, or `{}` — no verdicts at all — when there is no usable table.
7270
+ *
7271
+ * Never the host's list unrefined. Measured, that list is majority-false on
7272
+ * this harness line (see the header), so showing it whenever the table cannot
7273
+ * be read would hand the reader a verdict already known to be unreliable;
7274
+ * silence is this project's documented degradation — an unavailable fact must
7275
+ * never read as an accusation. Never rejects, so a table in any state cannot
7276
+ * cost the reader the catalog.
7277
+ *
7278
+ * `removed` is the page-removed set (design 2026-09-01-harness-compatibility
7279
+ * section 9.1): package names this page has uninstalled since it loaded.
7280
+ * Every name in it is reported missing exactly
7281
+ * as the host said, WITHOUT ever reaching the table — not the rows check, not
7282
+ * `loadCache`, not `import` — because a row or a cache record can outlive a
7283
+ * restart-free uninstall (`moduleTableOracle`'s header), and the table's own
7284
+ * "provided" would be precisely the false clear this set exists to prevent.
7285
+ */
7286
+ async function refineAgainstModuleTable(incompatible, service, removed = /* @__PURE__ */ new Set(), timeoutMs = MODULE_PROBE_TIMEOUT_MS) {
7287
+ const oracle = moduleTableOracle(service);
7288
+ if (oracle === null) return {};
7289
+ const withRemoved = async (spec) => removed.has(stripClientSuffix(spec)) ? false : oracle(spec);
7290
+ return refineIncompatible(incompatible, withRemoved, timeoutMs);
7291
+ }
7292
+ //#endregion
7293
+ //#region src/client/restart-monitor.ts
7294
+ /**
7295
+ * The restart handoff's browser half (design §8), held by the page rather
7296
+ * than by the tab that confirmed the restart.
7297
+ *
7298
+ * It lived in a RestartPanel effect until 2026-09-27, and a self-update
7299
+ * killed it. dsh's client HMR (`@deepseek-ai/dsh-client-hmr`, mounted by the
7300
+ * web bundle unconditionally) stat-polls every client bundle and hot-swaps
7301
+ * one whose file changed; a self-update rewrites this package's own
7302
+ * `lib/client.js`, so the swap disposes the tab's fiber and mounts a fresh
7303
+ * instance of a freshly imported module. The effect's cleanup then stopped
7304
+ * the probe, and once the restart finished nothing reloaded the page: it sat
7305
+ * on the old process's answers while dsh's socket quietly reconnected to the
7306
+ * new one. Measured on dsh 0.1.5-rc.3 updating 0.8.2 to 0.8.3; with the
7307
+ * `/plugins/events` channel blocked the same flow reloaded.
7308
+ *
7309
+ * So the state sits on `globalThis` under a registry symbol — the one thing a
7310
+ * swapped-in module instance shares with the one it replaced — and the probe
7311
+ * loop is plain timers owned by no fiber. Any tab reads the state, so the
7312
+ * replacement renders the restart under way instead of offering another.
7313
+ */
7314
+ /** Registry symbol, so every module instance on the page finds the same store. */
7315
+ const STORE_KEY = Symbol.for("dsh-plugin-shop.restart-monitor");
7316
+ function monitorStore() {
7317
+ const page = globalThis;
7318
+ let store = page[STORE_KEY];
7319
+ if (store === void 0) {
7320
+ store = {
7321
+ state: null,
7322
+ listeners: /* @__PURE__ */ new Set(),
7323
+ stop: null
7324
+ };
7325
+ page[STORE_KEY] = store;
7326
+ }
7327
+ return store;
7328
+ }
7329
+ function settle(store, state) {
7330
+ store.state = state;
7331
+ for (const listener of [...store.listeners]) listener();
7332
+ }
7333
+ /** The page's restart, or null when it has committed none. The same object
7334
+ * until the state changes, as `useSyncExternalStore` requires. */
7335
+ function readRestartMonitor() {
7336
+ return monitorStore().state;
7337
+ }
7338
+ /** Be told when the page's restart changes state. */
7339
+ function subscribeRestartMonitor(listener) {
7340
+ const store = monitorStore();
7341
+ store.listeners.add(listener);
7342
+ return () => {
7343
+ store.listeners.delete(listener);
7344
+ };
7345
+ }
7346
+ /**
7347
+ * Watch the origin for the restarted server and reload into it: probe once
7348
+ * the grace period is over (the host exits within it, so an answer is the NEW
7349
+ * server), and reload only after it has KEPT answering for the stable window.
7350
+ * A boot that is about to fail can bind the port and answer before its plugin
7351
+ * tree is audited and it exits; reloading on that one answer left the reader
7352
+ * on a blank page instead of the notice naming the log (design
7353
+ * 2026-09-26-market-borrowings §3). `restartMonitorVerdict` decides; this
7354
+ * loop only probes. Probes never overlap: each is scheduled one poll interval
7355
+ * after the previous one settled.
7356
+ *
7357
+ * A restart already being watched is left alone — every offer on the page
7358
+ * renders this state, so a second press can only come from a panel that has
7359
+ * not re-rendered yet, and a second loop would probe the same origin twice.
7360
+ */
7361
+ function startRestartMonitor(options) {
7362
+ const store = monitorStore();
7363
+ if (store.state?.kind === "restarting") return;
7364
+ store.stop?.();
7365
+ const { reload, logFile } = options;
7366
+ const started = Date.now();
7367
+ let stableSince = null;
7368
+ let stopped = false;
7369
+ let timer;
7370
+ const probe = async () => {
7371
+ if (Date.now() - started >= 3e3) {
7372
+ let up = false;
7373
+ try {
7374
+ up = (await fetch(globalThis.location.href, { cache: "no-store" })).ok;
7375
+ } catch {}
7376
+ if (stopped) return;
7377
+ const elapsed = Date.now() - started;
7378
+ stableSince = up ? stableSince ?? elapsed : null;
7379
+ const verdict = restartMonitorVerdict({
7380
+ elapsedMs: elapsed,
7381
+ stableSinceMs: stableSince
7382
+ });
7383
+ if (verdict === "reload") {
7384
+ store.stop = null;
7385
+ reload();
7386
+ return;
7387
+ }
7388
+ if (verdict === "failed") {
7389
+ store.stop = null;
7390
+ settle(store, logFile === void 0 ? { kind: "failed" } : {
7391
+ kind: "failed",
7392
+ logFile
7393
+ });
7394
+ return;
7395
+ }
7396
+ }
7397
+ timer = setTimeout(() => {
7398
+ probe();
7399
+ }, INSTALL_POLL_MS);
7400
+ };
7401
+ store.stop = () => {
7402
+ stopped = true;
7403
+ if (timer !== void 0) clearTimeout(timer);
7404
+ };
7405
+ timer = setTimeout(() => {
7406
+ probe();
7407
+ }, INSTALL_POLL_MS);
7408
+ settle(store, logFile === void 0 ? { kind: "restarting" } : {
7409
+ kind: "restarting",
7410
+ logFile
7411
+ });
7412
+ }
7413
+ //#endregion
6901
7414
  //#region src/client/useFlows.ts
6902
7415
  /**
6903
7416
  * One keyed registry of install-shaped flows: start, poll to terminal, reset.
@@ -7177,117 +7690,121 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7177
7690
  };
7178
7691
  }
7179
7692
  //#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\"]")) {
7693
+ //#region \0dsh-plugin-shop-css:c4028823
7694
+ 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.sc389ac_client.sc41672_config.s8eefa1_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.sc389ac_client.sfe36ad_spec.s8eefa1_ts` pins this. */\n\n.s688753_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 `.s9f28cb_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.sbc2e10_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.s7237a9_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.s601d42_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.sa26d1f_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.sceffd1_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.sceffd1_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.s6f1efd_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.s4db475_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.se9c3a9_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.s3cbb84_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.sceffd1_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 .sceffd1_skeletonCard span { animation: none; opacity: 0.75; }\n .sceffd1_skeletonCard::after { content: none; }\n}\n\n.se4623a_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.se4623a_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.se4623a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sbc2e10_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.sb9d854_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.s17e39d_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sff4480_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.sf22160_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.sf22160_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sf22160_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.s1abadc_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.s1abadc_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s1abadc_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s1abadc_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.se9af4e_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.se9af4e_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.se9af4e_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.s11efce_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s9f28cb_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.s9f28cb_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.scfe278_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.sfd2800_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.saa7982_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.saa7982_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 `.saa7982_categoryButton.s47a604_categoryButtonOn` — TWO classes — so it\n * matches `.saa7982_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s47a604_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.sc389ac_client.sfe36ad_spec.s8eefa1_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.saa7982_categoryButton.s47a604_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 .saa7982_categoryButton.s47a604_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.saa7982_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 `.saa7982_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.s6000bb_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: `.s6000bb_incompatibleFilter:hover .s21d1d6_switch` outranks `.sfaf305_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.s6000bb_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.s6000bb_incompatibleFilter:hover .s21d1d6_switch:not(.sfaf305_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 `.saa7982_categoryButton.s47a604_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.s6000bb_incompatibleFilter.s9fb743_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.s6000bb_incompatibleFilter .s21d1d6_switch:not(.sfaf305_switchOn) .s3e9566_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.s6000bb_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.s3627e7_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.s879949_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.s7f1200_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 `.se4623a_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.s7f50e0_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.s7f50e0_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.s7f50e0_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s7f50e0_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s284f4c_catalogRefreshFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s894d35_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.s73be4c_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 .s73be4c_cards { animation: none; }\n}\n\n.s7ce0b5_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.se9f199_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s571a2d_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s7ce0b5_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 .s7ce0b5_card { transition: none; }\n .s7ce0b5_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.s3afd93_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. `.saa7982_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.s7ce0b5_card[data-category='tool'], .saa7982_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s7ce0b5_card[data-category='provider'], .saa7982_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s7ce0b5_card[data-category='ui'], .saa7982_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s7ce0b5_card[data-category='workflow'], .saa7982_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s7ce0b5_card[data-category='integration'], .saa7982_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s7ce0b5_card[data-category='theme'], .saa7982_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s7ce0b5_card[data-category='other'], .saa7982_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.sda4302_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.sf9a029_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.sf9a029_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.scde6f0_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.s5b12f5_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.sbfb66e_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.sbfb66e_cardActions > .sbf0f49_gate,\n.sbfb66e_cardActions > .se45c46_installPanel,\n.sbfb66e_cardActions > .s6a7b50_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.sa3f608_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.s908192_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sda8b40_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.sda8b40_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.sda8b40_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.sda8b40_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.s6eb4e5_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.s3e85ad_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sef80ea_chevron, .sc0d8c8_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.sc0d8c8_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .sef80ea_chevron, .sc0d8c8_chevronOpen { transition: none; }\n}\n\n.safe8c4_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sbd2517_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.s15225a_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.s273e04_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.s20dbc5_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.s927eb9_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.sa35ed7_remedy {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s2fcacb_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.se87ea1_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.sbccf5d_refusal {\n display: flex;\n flex-direction: column;\n gap: 6px;\n flex-basis: 100%;\n}\n\n.sca4074_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3ed23b_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sbd7053_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.sbd7053_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.se69ccd_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.s71ed47_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.s488064_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.s488064_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.s488064_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.s488064_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.s488064_detailRow a:hover {\n text-decoration: underline;\n}\n\n.s488064_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 * `.sbfb66e_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.sd941dc_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: `.sd941dc_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.sff4055_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 `.sd941dc_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 `.sd941dc_cardMeta`. There is no middot:\n * `.sd941dc_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.s74b808_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.s5a1b52_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.se45c46_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.s7bcc5e_installButton, .sb977c6_confirmButton, .s3cfd9a_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.s7bcc5e_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.s7bcc5e_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb977c6_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.sb977c6_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s3cfd9a_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.s3cfd9a_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s7bcc5e_installButton:focus-visible, .sb977c6_confirmButton:focus-visible, .s3cfd9a_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.sfaf388_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.s6a7b50_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.sb0c03c_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.sb0c03c_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.sb0c03c_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.se1e373_restartSelfButton {\n /* The version row's third state. Geometry matches .s1abadc_checkUpdateButton and\n * .sf22160_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.se1e373_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.s785196_restartButton,\n.s578d0b_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.s785196_restartButton:hover,\n.s578d0b_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s785196_restartButton:focus-visible,\n.s578d0b_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 * `.s6a7b50_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.s168719_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n\n.sbf0f49_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.s62b4ea_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s8a8304_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s9a546e_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.secbb11_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s2fe11f_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s597ed4_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.s2fbc07_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.se1ef74_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.s878e49_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.scaf26f_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sb46af7_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se50b2b_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.s103f71_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.s2ba5be_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s2283c2_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.se41dca_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.se41dca_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.s970b51_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 .s5b12f5_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.sfe5725_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: `.s6000bb_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 `.s21d1d6_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.s21d1d6_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.sfaf305_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.s3e9566_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.sfaf305_switchOn .s3e9566_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s21d1d6_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 .s21d1d6_switch, .s3e9566_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.s98de45_switchWrap {\n display: inline-flex;\n align-items: center;\n /* Same reason as `.sfe5725_outdatedActions`: `.s168719_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.s6a7b50_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
7695
+ if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"c4028823\"]")) {
7183
7696
  const tag = document.createElement("style");
7184
- tag.dataset.pluginCss = "e3675a89";
7697
+ tag.dataset.pluginCss = "c4028823";
7185
7698
  tag.textContent = css;
7186
7699
  document.head.appendChild(tag);
7187
7700
  }
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"
7701
+ var _dsh_plugin_shop_css_c4028823_default = {
7702
+ "client": "sc389ac_client",
7703
+ "config": "sc41672_config",
7704
+ "ts": "s8eefa1_ts",
7705
+ "spec": "sfe36ad_spec",
7706
+ "panel": "s688753_panel",
7707
+ "searchInput": "s9f28cb_searchInput",
7708
+ "toolbar": "sbc2e10_toolbar",
7709
+ "stateLine": "s7237a9_stateLine",
7710
+ "srOnly": "s601d42_srOnly",
7711
+ "skeletonGrid": "sa26d1f_skeletonGrid",
7712
+ "skeletonCard": "sceffd1_skeletonCard",
7713
+ "skeletonName": "s6f1efd_skeletonName",
7714
+ "skeletonSummary": "s4db475_skeletonSummary",
7715
+ "skeletonSummaryShort": "se9c3a9_skeletonSummaryShort",
7716
+ "skeletonActions": "s3cbb84_skeletonActions",
7717
+ "actionButton": "se4623a_actionButton",
7718
+ "toolbarRight": "sb9d854_toolbarRight",
7719
+ "versionBlock": "s17e39d_versionBlock",
7720
+ "versionText": "sff4480_versionText",
7721
+ "updateSelfButton": "sf22160_updateSelfButton",
7722
+ "checkUpdateButton": "s1abadc_checkUpdateButton",
7723
+ "githubLink": "se9af4e_githubLink",
7724
+ "selfUpdatePanel": "s11efce_selfUpdatePanel",
7725
+ "staleBadge": "scfe278_staleBadge",
7726
+ "categoryBar": "sfd2800_categoryBar",
7727
+ "categoryButton": "saa7982_categoryButton",
7728
+ "categoryButtonOn": "s47a604_categoryButtonOn",
7729
+ "incompatibleFilter": "s6000bb_incompatibleFilter",
7730
+ "switch": "s21d1d6_switch",
7731
+ "switchOn": "sfaf305_switchOn",
7732
+ "incompatibleFilterOn": "s9fb743_incompatibleFilterOn",
7733
+ "switchKnob": "s3e9566_switchKnob",
7734
+ "updatableHeading": "s3627e7_updatableHeading",
7735
+ "catalogStatsRow": "s879949_catalogStatsRow",
7736
+ "catalogStats": "s7f1200_catalogStats",
7737
+ "catalogRefreshButton": "s7f50e0_catalogRefreshButton",
7738
+ "catalogRefreshFailed": "s284f4c_catalogRefreshFailed",
7739
+ "emptyLine": "s894d35_emptyLine",
7740
+ "cards": "s73be4c_cards",
7741
+ "card": "s7ce0b5_card",
7742
+ "cardsSentry": "se9f199_cardsSentry",
7743
+ "showingLine": "s571a2d_showingLine",
7744
+ "cardSpine": "s3afd93_cardSpine",
7745
+ "categoryBadge": "sda4302_categoryBadge",
7746
+ "entryHeader": "sf9a029_entryHeader",
7747
+ "name": "scde6f0_name",
7748
+ "badges": "s5b12f5_badges",
7749
+ "cardActions": "sbfb66e_cardActions",
7750
+ "gate": "sbf0f49_gate",
7751
+ "installPanel": "se45c46_installPanel",
7752
+ "installedActions": "s6a7b50_installedActions",
7753
+ "cardVersion": "sa3f608_cardVersion",
7754
+ "sourceBadge": "s908192_sourceBadge",
7755
+ "tierBadge": "sda8b40_tierBadge",
7756
+ "incompatibleBadge": "s6eb4e5_incompatibleBadge",
7757
+ "starsBadge": "s3e85ad_starsBadge",
7758
+ "chevron": "sef80ea_chevron",
7759
+ "chevronOpen": "sc0d8c8_chevronOpen",
7760
+ "body": "safe8c4_body",
7761
+ "summary": "sbd2517_summary",
7762
+ "summaryZh": "s15225a_summaryZh",
7763
+ "summaryExpanded": "s273e04_summaryExpanded",
7764
+ "summaryZhExpanded": "s20dbc5_summaryZhExpanded",
7765
+ "incompatibleDetail": "s927eb9_incompatibleDetail",
7766
+ "remedy": "sa35ed7_remedy",
7767
+ "remedyLead": "s2fcacb_remedyLead",
7768
+ "allowCommand": "se87ea1_allowCommand",
7769
+ "refusal": "sbccf5d_refusal",
7770
+ "capabilitiesBlock": "sca4074_capabilitiesBlock",
7771
+ "capabilitiesNote": "s3ed23b_capabilitiesNote",
7772
+ "capabilities": "sbd7053_capabilities",
7773
+ "detail": "se69ccd_detail",
7774
+ "detailRows": "s71ed47_detailRows",
7775
+ "detailRow": "s488064_detailRow",
7776
+ "cardMeta": "sd941dc_cardMeta",
7777
+ "size": "sff4055_size",
7778
+ "author": "s74b808_author",
7779
+ "reviewedLine": "s5a1b52_reviewedLine",
7780
+ "installButton": "s7bcc5e_installButton",
7781
+ "confirmButton": "sb977c6_confirmButton",
7782
+ "cancelButton": "s3cfd9a_cancelButton",
7783
+ "installedLabel": "sfaf388_installedLabel",
7784
+ "uninstallButton": "sb0c03c_uninstallButton",
7785
+ "restartSelfButton": "se1e373_restartSelfButton",
7786
+ "restartButton": "s785196_restartButton",
7787
+ "reloadButton": "s578d0b_reloadButton",
7788
+ "reloadPanel": "s168719_reloadPanel",
7789
+ "gateTitle": "s62b4ea_gateTitle",
7790
+ "gateBody": "s8a8304_gateBody",
7791
+ "gateWarning": "s9a546e_gateWarning",
7792
+ "gateActions": "secbb11_gateActions",
7793
+ "installing": "s2fe11f_installing",
7794
+ "log": "s597ed4_log",
7795
+ "logLine": "s2fbc07_logLine",
7796
+ "notice": "se1ef74_notice",
7797
+ "failedHeading": "s878e49_failedHeading",
7798
+ "failedDetail": "scaf26f_failedDetail",
7799
+ "rejectedCode": "sb46af7_rejectedCode",
7800
+ "rejectedDetail": "se50b2b_rejectedDetail",
7801
+ "outdatedSection": "s103f71_outdatedSection",
7802
+ "outdatedList": "s2ba5be_outdatedList",
7803
+ "outdatedRow": "s2283c2_outdatedRow",
7804
+ "outdatedInfo": "se41dca_outdatedInfo",
7805
+ "outdatedVersions": "s970b51_outdatedVersions",
7806
+ "outdatedActions": "sfe5725_outdatedActions",
7807
+ "switchWrap": "s98de45_switchWrap"
7291
7808
  };
7292
7809
  //#endregion
7293
7810
  //#region src/client/ShopTab.tsx
@@ -7298,13 +7815,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7298
7815
  * §11.3.4): no render path here may ever use dangerouslySetInnerHTML. */
7299
7816
  /** The project's home on GitHub, linked from the toolbar. */
7300
7817
  const SHOP_REPO_URL = "https://github.com/LivXue/dsh-plugin-shop";
7818
+ /** The class of every root the tab renders, the crash fallback's included:
7819
+ * the panel, plus `notranslate`, which with `translate="no"` keeps a browser's
7820
+ * page translation out of the text nodes React owns. Translation rewrites
7821
+ * those nodes, and React's next commit throws on the ones it no longer finds
7822
+ * (design 2026-09-26-market-borrowings §4). The tab renders no portals, so its
7823
+ * root is its whole surface. */
7824
+ const PANEL_ROOT = `${_dsh_plugin_shop_css_c4028823_default.panel} notranslate`;
7301
7825
  function ChevronIcon({ open }) {
7302
7826
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
7303
7827
  viewBox: "0 0 16 16",
7304
7828
  width: "14",
7305
7829
  height: "14",
7306
7830
  "aria-hidden": "true",
7307
- className: open ? _dsh_plugin_shop_css_e3675a89_default.chevronOpen : _dsh_plugin_shop_css_e3675a89_default.chevron,
7831
+ className: open ? _dsh_plugin_shop_css_c4028823_default.chevronOpen : _dsh_plugin_shop_css_c4028823_default.chevron,
7308
7832
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
7309
7833
  fill: "currentColor",
7310
7834
  d: "M3.5 6 8 10.5 12.5 6l1 1L8 12.5 2.5 7l1-1Z"
@@ -7317,9 +7841,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7317
7841
  * install controls. An installed plugin's card carries its installed row:
7318
7842
  * current → the non-interactive installed label, behind → the update button;
7319
7843
  * 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 }) {
7844
+ const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, harness, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView }) {
7321
7845
  const [open, setOpen] = (0, react.useState)(false);
7322
- const blockers = blockersOf(missing, nameTakenBy, t);
7846
+ const blockers = stateBlockers(blockersOf(missing, harness, nameTakenBy), t);
7323
7847
  const detailId = (0, react.useId)();
7324
7848
  const summary = entry.catalog?.summary;
7325
7849
  const npmUrl = npmPageUrl(entry);
@@ -7334,32 +7858,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7334
7858
  subdir: entry.subdir
7335
7859
  };
7336
7860
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7337
- className: _dsh_plugin_shop_css_e3675a89_default.card,
7861
+ className: _dsh_plugin_shop_css_c4028823_default.card,
7338
7862
  "data-shop-entry": entry.name,
7339
7863
  "data-category": category,
7340
7864
  children: [
7341
7865
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7342
- className: _dsh_plugin_shop_css_e3675a89_default.cardSpine,
7866
+ className: _dsh_plugin_shop_css_c4028823_default.cardSpine,
7343
7867
  "aria-hidden": "true"
7344
7868
  }),
7345
7869
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7346
7870
  type: "button",
7347
- className: _dsh_plugin_shop_css_e3675a89_default.entryHeader,
7871
+ className: _dsh_plugin_shop_css_c4028823_default.entryHeader,
7348
7872
  "aria-expanded": open,
7349
7873
  "aria-controls": detailId,
7350
7874
  onClick: () => setOpen((current) => !current),
7351
7875
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7352
- className: _dsh_plugin_shop_css_e3675a89_default.name,
7876
+ className: _dsh_plugin_shop_css_c4028823_default.name,
7353
7877
  children: entry.name
7354
7878
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7355
- className: _dsh_plugin_shop_css_e3675a89_default.badges,
7879
+ className: _dsh_plugin_shop_css_c4028823_default.badges,
7356
7880
  children: [
7357
7881
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7358
- className: _dsh_plugin_shop_css_e3675a89_default.categoryBadge,
7882
+ className: _dsh_plugin_shop_css_c4028823_default.categoryBadge,
7359
7883
  children: t(categoryKey(entry))
7360
7884
  }),
7361
7885
  entry.source === "npm" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7362
- className: _dsh_plugin_shop_css_e3675a89_default.sourceBadge,
7886
+ className: _dsh_plugin_shop_css_c4028823_default.sourceBadge,
7363
7887
  "data-shop-source-npm": true,
7364
7888
  role: "img",
7365
7889
  "aria-label": t("npmSource"),
@@ -7376,7 +7900,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7376
7900
  })
7377
7901
  }),
7378
7902
  hasGithubHome(entry) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7379
- className: _dsh_plugin_shop_css_e3675a89_default.sourceBadge,
7903
+ className: _dsh_plugin_shop_css_c4028823_default.sourceBadge,
7380
7904
  "data-shop-source-github": true,
7381
7905
  role: "img",
7382
7906
  "aria-label": t("githubSource"),
@@ -7390,17 +7914,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7390
7914
  })
7391
7915
  }),
7392
7916
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7393
- className: _dsh_plugin_shop_css_e3675a89_default.cardVersion,
7917
+ className: _dsh_plugin_shop_css_c4028823_default.cardVersion,
7394
7918
  "data-card-version": true,
7395
7919
  children: ["v", displayVersion(entry)]
7396
7920
  }),
7397
7921
  entry.tier !== "community" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7398
- className: _dsh_plugin_shop_css_e3675a89_default.tierBadge,
7922
+ className: _dsh_plugin_shop_css_c4028823_default.tierBadge,
7399
7923
  "data-tier": entry.tier,
7400
7924
  children: t(tierKey(entry.tier))
7401
7925
  }),
7402
7926
  stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7403
- className: _dsh_plugin_shop_css_e3675a89_default.starsBadge,
7927
+ className: _dsh_plugin_shop_css_c4028823_default.starsBadge,
7404
7928
  role: "img",
7405
7929
  "aria-label": t("stars", { count: stars }),
7406
7930
  children: ["★ ", formatStars(stars)]
@@ -7410,39 +7934,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7410
7934
  })]
7411
7935
  }),
7412
7936
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7413
- className: _dsh_plugin_shop_css_e3675a89_default.body,
7937
+ className: _dsh_plugin_shop_css_c4028823_default.body,
7414
7938
  children: [
7415
7939
  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,
7940
+ className: open ? `${_dsh_plugin_shop_css_c4028823_default.summary} ${_dsh_plugin_shop_css_c4028823_default.summaryExpanded}` : _dsh_plugin_shop_css_c4028823_default.summary,
7417
7941
  children: summary.en
7418
7942
  }),
7419
7943
  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,
7944
+ className: open ? `${_dsh_plugin_shop_css_c4028823_default.summaryZh} ${_dsh_plugin_shop_css_c4028823_default.summaryZhExpanded}` : _dsh_plugin_shop_css_c4028823_default.summaryZh,
7421
7945
  children: summary.zh
7422
7946
  }),
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
7947
+ blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
7948
+ blocker,
7949
+ className: _dsh_plugin_shop_css_c4028823_default.incompatibleDetail,
7950
+ surface: "detail"
7427
7951
  }, blocker.kind)),
7428
7952
  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,
7953
+ className: _dsh_plugin_shop_css_c4028823_default.capabilitiesBlock,
7430
7954
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7431
- className: _dsh_plugin_shop_css_e3675a89_default.capabilitiesNote,
7955
+ className: _dsh_plugin_shop_css_c4028823_default.capabilitiesNote,
7432
7956
  children: t("capabilitiesNote")
7433
7957
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
7434
- className: _dsh_plugin_shop_css_e3675a89_default.capabilities,
7958
+ className: _dsh_plugin_shop_css_c4028823_default.capabilities,
7435
7959
  children: entry.catalog.capabilities.map((capability) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: capability }, capability))
7436
7960
  })]
7437
7961
  }),
7438
7962
  open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7439
7963
  id: detailId,
7440
- className: _dsh_plugin_shop_css_e3675a89_default.detail,
7964
+ className: _dsh_plugin_shop_css_c4028823_default.detail,
7441
7965
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
7442
- className: _dsh_plugin_shop_css_e3675a89_default.detailRows,
7966
+ className: _dsh_plugin_shop_css_c4028823_default.detailRows,
7443
7967
  children: [
7444
7968
  npmUrl !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7445
- className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
7969
+ className: _dsh_plugin_shop_css_c4028823_default.detailRow,
7446
7970
  "data-shop-npm": true,
7447
7971
  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
7972
  href: npmUrl,
@@ -7452,7 +7976,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7452
7976
  }) })]
7453
7977
  }),
7454
7978
  entry.repository !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7455
- className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
7979
+ className: _dsh_plugin_shop_css_c4028823_default.detailRow,
7456
7980
  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
7981
  href: entry.repository,
7458
7982
  target: "_blank",
@@ -7461,12 +7985,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7461
7985
  }) : entry.repository })]
7462
7986
  }),
7463
7987
  entry.license !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7464
- className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
7988
+ className: _dsh_plugin_shop_css_c4028823_default.detailRow,
7465
7989
  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
7990
  })
7467
7991
  ]
7468
7992
  }), entry.tier === "verified-stale" && entry.review !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7469
- className: _dsh_plugin_shop_css_e3675a89_default.reviewedLine,
7993
+ className: _dsh_plugin_shop_css_c4028823_default.reviewedLine,
7470
7994
  children: entry.review.reviewedVersion !== void 0 ? t("reviewedVersionLine", {
7471
7995
  reviewed: entry.review.reviewedVersion,
7472
7996
  current: entry.version
@@ -7479,14 +8003,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7479
8003
  ]
7480
8004
  }),
7481
8005
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7482
- className: _dsh_plugin_shop_css_e3675a89_default.cardActions,
8006
+ className: _dsh_plugin_shop_css_c4028823_default.cardActions,
7483
8007
  "data-shop-actions": true,
7484
8008
  children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!inInstalledView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
7485
8009
  target: installTarget,
7486
8010
  tier: entry.tier,
7487
- missing,
7488
8011
  blockers,
7489
- missingStated: true,
8012
+ blockersStated: true,
7490
8013
  flow,
7491
8014
  t,
7492
8015
  restart,
@@ -7504,16 +8027,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7504
8027
  target: installTarget,
7505
8028
  tier: entry.tier,
7506
8029
  variant: installed.outdated ? "update" : "install",
7507
- missing,
7508
8030
  blockers,
7509
- missingStated: true,
8031
+ blockersStated: true,
7510
8032
  flow,
7511
8033
  t,
7512
8034
  restart,
7513
8035
  restartBlocked,
7514
8036
  reload
7515
8037
  }) : /* @__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,
8038
+ className: _dsh_plugin_shop_css_c4028823_default.installedLabel,
7517
8039
  "data-shop-installed": true,
7518
8040
  children: t("installed")
7519
8041
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
@@ -7535,16 +8057,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7535
8057
  flow: uninstallFlow
7536
8058
  })
7537
8059
  ] }), (size !== void 0 || author !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7538
- className: _dsh_plugin_shop_css_e3675a89_default.cardMeta,
8060
+ className: _dsh_plugin_shop_css_c4028823_default.cardMeta,
7539
8061
  children: [size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7540
- className: _dsh_plugin_shop_css_e3675a89_default.size,
8062
+ className: _dsh_plugin_shop_css_c4028823_default.size,
7541
8063
  "data-shop-size": true,
7542
8064
  role: "img",
7543
8065
  "aria-label": t("sizeLabel", { size }),
7544
8066
  title: t("sizeLabel", { size }),
7545
8067
  children: size
7546
8068
  }), author !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7547
- className: _dsh_plugin_shop_css_e3675a89_default.author,
8069
+ className: _dsh_plugin_shop_css_c4028823_default.author,
7548
8070
  "data-shop-author": true,
7549
8071
  children: t("authorLine", { author })
7550
8072
  })]
@@ -7554,41 +8076,95 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7554
8076
  });
7555
8077
  });
7556
8078
  /**
7557
- * Everything standing in this entry's way, already localized.
8079
+ * Everything standing in this entry's way, localized ONCE per surface: the
8080
+ * detail paragraphs and the badge summarizing them read this one list, so
8081
+ * they can never be built from different inputs.
7558
8082
  *
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.
8083
+ * The host sent facts — module names, the declared range and the running
8084
+ * version, the declared and the running profile — and the sentences are this
8085
+ * dictionary's, in the reader's own language (§4: no copy crosses the RPC).
7562
8086
  */
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(", ") })
8087
+ function stateBlockers(blockers, t) {
8088
+ return blockers.map((blocker) => {
8089
+ const text = blockerText(blocker, t);
8090
+ return blocker.kind === "harness-peers" && blocker.allowCommand !== null ? {
8091
+ kind: blocker.kind,
8092
+ text,
8093
+ remedy: {
8094
+ lead: t("harnessPeersRemedy"),
8095
+ command: blocker.allowCommand
8096
+ }
8097
+ } : {
8098
+ kind: blocker.kind,
8099
+ text
8100
+ };
7572
8101
  });
7573
- return blockers;
8102
+ }
8103
+ /**
8104
+ * One blocker as a surface prints it: its sentence in the surface's own
8105
+ * paragraph style, and after it, for a refusal the reader can clear, the
8106
+ * lead-in and the command on a line of its own — selectable as a whole, since
8107
+ * it goes to a terminal unchanged. `surface` names the attribute a locator
8108
+ * reads: the card's detail line, or the warning the gate and the outdated
8109
+ * row's refusal print.
8110
+ */
8111
+ function BlockerLine({ blocker, className, surface }) {
8112
+ const attribute = surface === "detail" ? { "data-shop-incompatible-detail": blocker.kind } : { "data-shop-incompatible-warning": blocker.kind };
8113
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8114
+ className,
8115
+ ...attribute,
8116
+ children: blocker.text
8117
+ }), blocker.remedy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8118
+ className: _dsh_plugin_shop_css_c4028823_default.remedy,
8119
+ "data-shop-remedy": blocker.kind,
8120
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8121
+ className: _dsh_plugin_shop_css_c4028823_default.remedyLead,
8122
+ children: blocker.remedy.lead
8123
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
8124
+ className: _dsh_plugin_shop_css_c4028823_default.allowCommand,
8125
+ "data-shop-allow-command": true,
8126
+ children: blocker.remedy.command
8127
+ })]
8128
+ })] });
8129
+ }
8130
+ /** One blocker's sentence. Exhaustive with no default, so a kind added
8131
+ * to `Blocker` is a type error here rather than a line that renders nothing. */
8132
+ function blockerText(blocker, t) {
8133
+ switch (blocker.kind) {
8134
+ case "name-taken": return t("nameTakenDetail", { holder: blocker.holder });
8135
+ case "harness-peers": return t("harnessPeersDetail", {
8136
+ running: blocker.running,
8137
+ peers: refusedPeersText(blocker.refused)
8138
+ });
8139
+ case "missing-peers": return t("incompatibleDetail", { modules: blocker.modules.join(", ") });
8140
+ case "harness-range": return t("harnessRangeDetail", {
8141
+ range: blocker.range,
8142
+ running: blocker.running
8143
+ });
8144
+ case "harness-profile": return t("harnessProfileDetail", {
8145
+ declared: blocker.declared.join(", "),
8146
+ running: blocker.running
8147
+ });
8148
+ }
7574
8149
  }
7575
8150
  /** The harness-compatibility verdict, rendered beside the control it
7576
8151
  * qualifies. It answers "what happens if I press this", not "what is this",
7577
8152
  * so it belongs to the action row and not among the identity badges in the
7578
8153
  * 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. */
8154
+ * own hit area for the tooltip. Renders nothing when nothing stands in the
8155
+ * way. */
7581
8156
  function BlockerBadge({ blockers, t }) {
7582
- if (blockers.length === 0) return null;
8157
+ const lead = blockers[0];
8158
+ const word = blockerBadgeKey(blockers);
8159
+ if (lead === void 0 || word === null) return null;
7583
8160
  const detail = blockers.map((blocker) => blocker.text).join("\n");
7584
- const taken = blockers.some((blocker) => blocker.kind === "name-taken");
7585
8161
  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",
8162
+ className: _dsh_plugin_shop_css_c4028823_default.incompatibleBadge,
8163
+ "data-shop-blocker": lead.kind,
7588
8164
  role: "img",
7589
8165
  "aria-label": detail,
7590
8166
  title: detail,
7591
- children: t(taken ? "nameTakenBadge" : "incompatibleBadge")
8167
+ children: t(word)
7592
8168
  });
7593
8169
  }
7594
8170
  /** One entry's install flow: the button, the §9.3 acknowledgement gate for
@@ -7596,34 +8172,34 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7596
8172
  * failure detail, rejection detail — driven by `useInstallFlows`. Shared by the
7597
8173
  * catalog cards (`variant: 'install'`) and the outdated rows' update button
7598
8174
  * (`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 }) {
8175
+ function InstallPanel({ target, tier, blockers, blockersStated = false, variant = "install", flow, t, restart, restartBlocked, reload }) {
7600
8176
  const [gateOpen, setGateOpen] = (0, react.useState)(false);
7601
8177
  const { view, start } = flow;
7602
8178
  if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7603
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8179
+ className: _dsh_plugin_shop_css_c4028823_default.installPanel,
7604
8180
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7605
- className: _dsh_plugin_shop_css_e3675a89_default.installing,
8181
+ className: _dsh_plugin_shop_css_c4028823_default.installing,
7606
8182
  children: t(installPhaseKey(view.phase))
7607
8183
  }), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7608
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8184
+ className: _dsh_plugin_shop_css_c4028823_default.log,
7609
8185
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7610
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8186
+ className: _dsh_plugin_shop_css_c4028823_default.logLine,
7611
8187
  children: line
7612
8188
  }, index))
7613
8189
  })]
7614
8190
  });
7615
8191
  if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7616
- className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
8192
+ className: _dsh_plugin_shop_css_c4028823_default.installedActions,
7617
8193
  children: [
7618
8194
  view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7619
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8195
+ className: _dsh_plugin_shop_css_c4028823_default.log,
7620
8196
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7621
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8197
+ className: _dsh_plugin_shop_css_c4028823_default.logLine,
7622
8198
  children: line
7623
8199
  }, index))
7624
8200
  }),
7625
8201
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7626
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8202
+ className: _dsh_plugin_shop_css_c4028823_default.notice,
7627
8203
  "data-shop-restart-notice": true,
7628
8204
  children: t(activationNoticeKey(view.activation, view.restartReason))
7629
8205
  }),
@@ -7638,56 +8214,56 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7638
8214
  ]
7639
8215
  });
7640
8216
  if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7641
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8217
+ className: _dsh_plugin_shop_css_c4028823_default.installPanel,
7642
8218
  children: [
7643
8219
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7644
- className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
8220
+ className: _dsh_plugin_shop_css_c4028823_default.failedHeading,
7645
8221
  children: t("installFailed")
7646
8222
  }),
7647
8223
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7648
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8224
+ className: _dsh_plugin_shop_css_c4028823_default.log,
7649
8225
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7650
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8226
+ className: _dsh_plugin_shop_css_c4028823_default.logLine,
7651
8227
  children: line
7652
8228
  }, index))
7653
8229
  }),
7654
8230
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7655
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
8231
+ className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
7656
8232
  children: view.detail === "" ? t("installTransportFailed") : view.detail
7657
8233
  })
7658
8234
  ]
7659
8235
  });
7660
8236
  if (view.kind === "rejected") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7661
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8237
+ className: _dsh_plugin_shop_css_c4028823_default.installPanel,
7662
8238
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7663
- className: _dsh_plugin_shop_css_e3675a89_default.rejectedCode,
8239
+ className: _dsh_plugin_shop_css_c4028823_default.rejectedCode,
7664
8240
  children: t(rejectionCodeKey(view.code))
7665
8241
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7666
- className: _dsh_plugin_shop_css_e3675a89_default.rejectedDetail,
8242
+ className: _dsh_plugin_shop_css_c4028823_default.rejectedDetail,
7667
8243
  children: view.detail
7668
8244
  })]
7669
8245
  });
7670
8246
  if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7671
- className: _dsh_plugin_shop_css_e3675a89_default.gate,
8247
+ className: _dsh_plugin_shop_css_c4028823_default.gate,
7672
8248
  children: [
7673
8249
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7674
- className: _dsh_plugin_shop_css_e3675a89_default.gateTitle,
8250
+ className: _dsh_plugin_shop_css_c4028823_default.gateTitle,
7675
8251
  children: t("acknowledgementTitle")
7676
8252
  }),
7677
8253
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7678
- className: _dsh_plugin_shop_css_e3675a89_default.gateBody,
8254
+ className: _dsh_plugin_shop_css_c4028823_default.gateBody,
7679
8255
  children: t("acknowledgementBody")
7680
8256
  }),
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
8257
+ !blockersStated && blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
8258
+ blocker,
8259
+ className: _dsh_plugin_shop_css_c4028823_default.gateWarning,
8260
+ surface: "warning"
7685
8261
  }, blocker.kind)),
7686
8262
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7687
- className: _dsh_plugin_shop_css_e3675a89_default.gateActions,
8263
+ className: _dsh_plugin_shop_css_c4028823_default.gateActions,
7688
8264
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7689
8265
  type: "button",
7690
- className: _dsh_plugin_shop_css_e3675a89_default.confirmButton,
8266
+ className: _dsh_plugin_shop_css_c4028823_default.confirmButton,
7691
8267
  "data-shop-confirm": true,
7692
8268
  onClick: () => {
7693
8269
  setGateOpen(false);
@@ -7699,7 +8275,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7699
8275
  children: t("confirm")
7700
8276
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7701
8277
  type: "button",
7702
- className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
8278
+ className: _dsh_plugin_shop_css_c4028823_default.cancelButton,
7703
8279
  onClick: () => setGateOpen(false),
7704
8280
  children: t("cancel")
7705
8281
  })]
@@ -7707,25 +8283,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7707
8283
  ]
7708
8284
  });
7709
8285
  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
- })] });
8286
+ const refused = refusesInstall(blockers);
8287
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
8288
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8289
+ type: "button",
8290
+ className: _dsh_plugin_shop_css_c4028823_default.installButton,
8291
+ disabled: refused,
8292
+ ...refused ? { "data-shop-refused": true } : {},
8293
+ ...update ? { "data-shop-update": true } : { "data-shop-install": true },
8294
+ onClick: () => {
8295
+ if (tier === "verified") start({
8296
+ ...target,
8297
+ acknowledged: void 0
8298
+ });
8299
+ else setGateOpen(true);
8300
+ },
8301
+ children: t(update ? "update" : "install")
8302
+ }),
8303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
8304
+ blockers,
8305
+ t
8306
+ }),
8307
+ refused && !blockersStated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8308
+ className: _dsh_plugin_shop_css_c4028823_default.refusal,
8309
+ "data-shop-refusal": true,
8310
+ children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
8311
+ blocker,
8312
+ className: _dsh_plugin_shop_css_c4028823_default.gateWarning,
8313
+ surface: "warning"
8314
+ }, blocker.kind))
8315
+ })
8316
+ ] });
7729
8317
  }
7730
8318
  /** One installed entry's uninstall flow: remove from the profile through the
7731
8319
  * same executor records and poll loop as installs. Uninstalling revokes
@@ -7736,23 +8324,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7736
8324
  function UninstallPanel({ name, t, restart, restartBlocked, reload, flow }) {
7737
8325
  const { view, start, reset } = flow;
7738
8326
  if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7739
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8327
+ className: _dsh_plugin_shop_css_c4028823_default.installPanel,
7740
8328
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7741
- className: _dsh_plugin_shop_css_e3675a89_default.installing,
8329
+ className: _dsh_plugin_shop_css_c4028823_default.installing,
7742
8330
  children: t("uninstalling")
7743
8331
  }), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7744
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8332
+ className: _dsh_plugin_shop_css_c4028823_default.log,
7745
8333
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7746
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8334
+ className: _dsh_plugin_shop_css_c4028823_default.logLine,
7747
8335
  children: line
7748
8336
  }, index))
7749
8337
  })]
7750
8338
  });
7751
8339
  if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7752
- className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
8340
+ className: _dsh_plugin_shop_css_c4028823_default.installedActions,
7753
8341
  children: [
7754
8342
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7755
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8343
+ className: _dsh_plugin_shop_css_c4028823_default.notice,
7756
8344
  "data-shop-uninstall-done": true,
7757
8345
  children: t(uninstallActivationNoticeKey(view.activation))
7758
8346
  }),
@@ -7766,7 +8354,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7766
8354
  }),
7767
8355
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7768
8356
  type: "button",
7769
- className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
8357
+ className: _dsh_plugin_shop_css_c4028823_default.cancelButton,
7770
8358
  "data-shop-uninstall-dismiss": true,
7771
8359
  onClick: reset,
7772
8360
  children: t("dismiss")
@@ -7774,26 +8362,26 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7774
8362
  ]
7775
8363
  });
7776
8364
  if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7777
- className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
8365
+ className: _dsh_plugin_shop_css_c4028823_default.installPanel,
7778
8366
  children: [
7779
8367
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7780
- className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
8368
+ className: _dsh_plugin_shop_css_c4028823_default.failedHeading,
7781
8369
  children: t("uninstallFailed")
7782
8370
  }),
7783
8371
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7784
- className: _dsh_plugin_shop_css_e3675a89_default.log,
8372
+ className: _dsh_plugin_shop_css_c4028823_default.log,
7785
8373
  children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7786
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
8374
+ className: _dsh_plugin_shop_css_c4028823_default.logLine,
7787
8375
  children: line
7788
8376
  }, index))
7789
8377
  }),
7790
8378
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7791
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
8379
+ className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
7792
8380
  children: view.detail === "" ? t("uninstallTransportFailed") : view.detail
7793
8381
  }),
7794
8382
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7795
8383
  type: "button",
7796
- className: _dsh_plugin_shop_css_e3675a89_default.uninstallButton,
8384
+ className: _dsh_plugin_shop_css_c4028823_default.uninstallButton,
7797
8385
  "data-shop-uninstall-retry": true,
7798
8386
  onClick: () => void start({ name }),
7799
8387
  children: t("retry")
@@ -7803,99 +8391,89 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7803
8391
  if (view.kind !== "idle") return null;
7804
8392
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7805
8393
  type: "button",
7806
- className: _dsh_plugin_shop_css_e3675a89_default.uninstallButton,
8394
+ className: _dsh_plugin_shop_css_c4028823_default.uninstallButton,
7807
8395
  "data-shop-uninstall": true,
7808
8396
  onClick: () => void start({ name }),
7809
8397
  children: t("uninstall")
7810
8398
  });
7811
8399
  }
8400
+ /** The page's restart, re-rendering whenever it changes state. Shared by
8401
+ * every restart offer and by the version row, in this tab and in whatever
8402
+ * tab an HMR swap mounts in its place. */
8403
+ function useRestartMonitor() {
8404
+ return (0, react.useSyncExternalStore)(subscribeRestartMonitor, readRestartMonitor);
8405
+ }
7812
8406
  /** The §8 restart flow (amendment 2026-08-27): after an install, update, or
7813
8407
  * uninstall reports done, this panel offers a restart of dsh. The
7814
8408
  * confirmation gate states the cost — the page disconnects and in-flight
7815
8409
  * conversations/tasks are interrupted — and on confirm the restart RPC
7816
8410
  * 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 }) {
8411
+ * the page's restart monitor (restart-monitor.ts) polls the origin after a
8412
+ * grace period, refreshing the page once the NEW server keeps answering.
8413
+ * A refused restart renders the host's published detail; a server that
8414
+ * never comes back names the manual command. */
8415
+ function RestartPanel({ t, restart, reload, gate }) {
7822
8416
  const [ownGateOpen, setOwnGateOpen] = (0, react.useState)(false);
7823
8417
  const gateOpen = gate === void 0 ? ownGateOpen : gate.open;
7824
8418
  const setGateOpen = (open) => {
7825
8419
  if (gate === void 0) setOwnGateOpen(open);
7826
8420
  else if (!open) gate.close();
7827
8421
  };
7828
- const [state, setState] = (0, react.useState)({ kind: "idle" });
8422
+ const [refusal, setRefusal] = (0, react.useState)(null);
8423
+ const monitor = useRestartMonitor();
7829
8424
  const onConfirm = async () => {
7830
8425
  setGateOpen(false);
7831
8426
  try {
7832
8427
  const result = await restart();
7833
8428
  if (!result.ok) {
7834
- setState({
7835
- kind: "failed",
7836
- detail: result.detail
7837
- });
8429
+ setRefusal(result.detail);
7838
8430
  return;
7839
8431
  }
7840
- setState({ kind: "restarting" });
7841
- } catch {
7842
- setState({
7843
- kind: "failed",
7844
- detail: t("restartTransportFailed")
8432
+ startRestartMonitor({
8433
+ reload,
8434
+ ...typeof result.logFile === "string" ? { logFile: result.logFile } : {}
7845
8435
  });
8436
+ } catch {
8437
+ setRefusal(t("restartTransportFailed"));
7846
8438
  }
7847
8439
  };
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,
8440
+ if (monitor?.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8441
+ className: _dsh_plugin_shop_css_c4028823_default.notice,
7869
8442
  "data-shop-restarting": true,
7870
8443
  children: t("restarting")
7871
8444
  });
7872
- if (state.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7873
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
8445
+ if (monitor?.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8446
+ className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
8447
+ "data-shop-restart-error": true,
8448
+ children: monitor.logFile === void 0 ? t("restartFailedNotice") : t("restartFailedLogNotice", { log: monitor.logFile })
8449
+ });
8450
+ if (refusal !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8451
+ className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
7874
8452
  "data-shop-restart-error": true,
7875
- children: state.detail
8453
+ children: refusal
7876
8454
  });
7877
8455
  if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7878
- className: _dsh_plugin_shop_css_e3675a89_default.gate,
8456
+ className: _dsh_plugin_shop_css_c4028823_default.gate,
7879
8457
  children: [
7880
8458
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7881
- className: _dsh_plugin_shop_css_e3675a89_default.gateTitle,
8459
+ className: _dsh_plugin_shop_css_c4028823_default.gateTitle,
7882
8460
  children: t("restartTitle")
7883
8461
  }),
7884
8462
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7885
- className: _dsh_plugin_shop_css_e3675a89_default.gateBody,
8463
+ className: _dsh_plugin_shop_css_c4028823_default.gateBody,
7886
8464
  children: t("restartBody")
7887
8465
  }),
7888
8466
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7889
- className: _dsh_plugin_shop_css_e3675a89_default.gateActions,
8467
+ className: _dsh_plugin_shop_css_c4028823_default.gateActions,
7890
8468
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7891
8469
  type: "button",
7892
- className: _dsh_plugin_shop_css_e3675a89_default.confirmButton,
8470
+ className: _dsh_plugin_shop_css_c4028823_default.confirmButton,
7893
8471
  "data-shop-restart-confirm": true,
7894
8472
  onClick: () => void onConfirm(),
7895
8473
  children: t("restartConfirm")
7896
8474
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7897
8475
  type: "button",
7898
- className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
8476
+ className: _dsh_plugin_shop_css_c4028823_default.cancelButton,
7899
8477
  onClick: () => setGateOpen(false),
7900
8478
  children: t("cancel")
7901
8479
  })]
@@ -7905,7 +8483,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7905
8483
  if (gate !== void 0) return null;
7906
8484
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7907
8485
  type: "button",
7908
- className: _dsh_plugin_shop_css_e3675a89_default.restartButton,
8486
+ className: _dsh_plugin_shop_css_c4028823_default.restartButton,
7909
8487
  "data-shop-restart": true,
7910
8488
  onClick: () => setGateOpen(true),
7911
8489
  children: t("restart")
@@ -7933,9 +8511,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7933
8511
  if (activation !== "restart") return null;
7934
8512
  return restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
7935
8513
  t,
7936
- restart
8514
+ restart,
8515
+ reload
7937
8516
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7938
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8517
+ className: _dsh_plugin_shop_css_c4028823_default.notice,
7939
8518
  "data-shop-restart-disabled": true,
7940
8519
  children: t(restartBlockedNoticeKey(restartBlocked))
7941
8520
  });
@@ -7946,14 +8525,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7946
8525
  * would help without knowing that now is a good time. */
7947
8526
  function ReloadPanel({ t, reload, where }) {
7948
8527
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7949
- className: _dsh_plugin_shop_css_e3675a89_default.reloadPanel,
8528
+ className: _dsh_plugin_shop_css_c4028823_default.reloadPanel,
7950
8529
  "data-shop-reload": where,
7951
8530
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7952
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8531
+ className: _dsh_plugin_shop_css_c4028823_default.notice,
7953
8532
  children: t("reloadNote")
7954
8533
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7955
8534
  type: "button",
7956
- className: _dsh_plugin_shop_css_e3675a89_default.reloadButton,
8535
+ className: _dsh_plugin_shop_css_c4028823_default.reloadButton,
7957
8536
  onClick: reload,
7958
8537
  children: t("reload")
7959
8538
  })]
@@ -7995,7 +8574,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7995
8574
  }
7996
8575
  };
7997
8576
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7998
- className: _dsh_plugin_shop_css_e3675a89_default.switchWrap,
8577
+ className: _dsh_plugin_shop_css_c4028823_default.switchWrap,
7999
8578
  "data-shop-enabled-switch": row.name,
8000
8579
  children: [
8001
8580
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -8004,13 +8583,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8004
8583
  "aria-checked": enabled,
8005
8584
  "aria-label": t("enabledSwitch"),
8006
8585
  "data-shop-toggle": true,
8007
- className: `${_dsh_plugin_shop_css_e3675a89_default.switch} ${enabled ? _dsh_plugin_shop_css_e3675a89_default.switchOn : ""}`,
8586
+ className: `${_dsh_plugin_shop_css_c4028823_default.switch} ${enabled ? _dsh_plugin_shop_css_c4028823_default.switchOn : ""}`,
8008
8587
  onClick: () => void onToggle(),
8009
8588
  disabled: toggle.kind === "saving",
8010
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.switchKnob })
8589
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.switchKnob })
8011
8590
  }),
8012
8591
  toggle.kind === "saved" && !needsReload && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8013
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
8592
+ className: _dsh_plugin_shop_css_c4028823_default.notice,
8014
8593
  "data-shop-hot-apply": true,
8015
8594
  children: t("hotApplyNote")
8016
8595
  }),
@@ -8020,7 +8599,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8020
8599
  where: "toggle"
8021
8600
  }),
8022
8601
  toggle.kind === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8023
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
8602
+ className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
8024
8603
  "data-shop-toggle-error": true,
8025
8604
  children: toggle.detail
8026
8605
  })
@@ -8030,21 +8609,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8030
8609
  /** One outdated install row (§7.3): the name, the installed and latest
8031
8610
  * versions, the hot enable/disable switch, and the update button (the
8032
8611
  * install flow for `name@latest`, reusing `InstallPanel`). */
8033
- function OutdatedRow({ row, tier, missing, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
8612
+ function OutdatedRow({ row, tier, missing, harness, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
8034
8613
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8035
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedRow,
8614
+ className: _dsh_plugin_shop_css_c4028823_default.outdatedRow,
8036
8615
  "data-shop-outdated-entry": row.name,
8037
8616
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8038
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedInfo,
8617
+ className: _dsh_plugin_shop_css_c4028823_default.outdatedInfo,
8039
8618
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8040
- className: _dsh_plugin_shop_css_e3675a89_default.name,
8619
+ className: _dsh_plugin_shop_css_c4028823_default.name,
8041
8620
  children: row.name
8042
8621
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8043
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersions,
8622
+ className: _dsh_plugin_shop_css_c4028823_default.outdatedVersions,
8044
8623
  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
8624
  })]
8046
8625
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8047
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedActions,
8626
+ className: _dsh_plugin_shop_css_c4028823_default.outdatedActions,
8048
8627
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
8049
8628
  row,
8050
8629
  t,
@@ -8060,8 +8639,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8060
8639
  },
8061
8640
  tier,
8062
8641
  variant: "update",
8063
- missing,
8064
- blockers: blockersOf(missing, void 0, t),
8642
+ blockers: stateBlockers(blockersOf(missing, harness, void 0), t),
8065
8643
  flow: flowFor(identityKey(row)),
8066
8644
  t,
8067
8645
  restart,
@@ -8088,29 +8666,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8088
8666
  * tier for the update gate is looked up from the catalog by install IDENTITY,
8089
8667
  * never by name (community → acknowledgement); an entry absent from the
8090
8668
  * catalog defaults to the community gate (the safer read). */
8091
- function OutdatedSection({ state, entriesByKey, missingByKey, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
8669
+ function OutdatedSection({ state, entriesByKey, missingByKey, harnessVerdicts, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
8092
8670
  if (state.kind === "loading") return null;
8093
8671
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8094
- className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
8672
+ className: _dsh_plugin_shop_css_c4028823_default.stateLine,
8095
8673
  "data-shop-outdated-error": true,
8096
8674
  children: t("installedError")
8097
8675
  });
8098
8676
  const outdated = state.entries.filter((entry) => entry.outdated);
8099
8677
  if (outdated.length === 0) return null;
8100
8678
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8101
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedSection,
8679
+ className: _dsh_plugin_shop_css_c4028823_default.outdatedSection,
8102
8680
  "data-shop-outdated": true,
8103
8681
  "aria-labelledby": UPDATABLE_HEADING_ID,
8104
8682
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
8105
- className: _dsh_plugin_shop_css_e3675a89_default.updatableHeading,
8683
+ className: _dsh_plugin_shop_css_c4028823_default.updatableHeading,
8106
8684
  id: UPDATABLE_HEADING_ID,
8107
8685
  children: t("updatableSection")
8108
8686
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
8109
- className: _dsh_plugin_shop_css_e3675a89_default.outdatedList,
8687
+ className: _dsh_plugin_shop_css_c4028823_default.outdatedList,
8110
8688
  children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
8111
8689
  row,
8112
8690
  tier: entriesByKey.get(identityKey(row))?.tier ?? "community",
8113
8691
  missing: missingByKey.get(identityKey(row)) ?? [],
8692
+ harness: harnessVerdictOf(harnessVerdicts, identityKey(row)),
8114
8693
  t,
8115
8694
  setEnabled,
8116
8695
  flowFor,
@@ -8121,10 +8700,71 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8121
8700
  })]
8122
8701
  });
8123
8702
  }
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. */
8703
+ /**
8704
+ * The shop's own error boundary (design 2026-09-26-market-borrowings §4).
8705
+ *
8706
+ * The harness catches a crashing slot entry as well, but renders an empty
8707
+ * `<div data-slot-error>` that stays until the page reloads: a blank tab, with
8708
+ * no message and no way back. This one says what happened and offers a Retry
8709
+ * that remounts the tab from scratch. Its fallback carries no `data-shop-tab`,
8710
+ * so a crashed tab cannot satisfy anything waiting for a working one. A class,
8711
+ * because React offers error boundaries in no other form.
8712
+ */
8713
+ var ShopErrorBoundary = class extends react.Component {
8714
+ state = { error: null };
8715
+ static getDerivedStateFromError(error) {
8716
+ return { error: error instanceof Error ? error : new Error(String(error)) };
8717
+ }
8718
+ componentDidCatch(error, info) {
8719
+ console.error("dsh-plugin-shop: the shop tab crashed", error, info.componentStack);
8720
+ }
8721
+ render() {
8722
+ const { error } = this.state;
8723
+ if (error === null) return this.props.children;
8724
+ const say = (key, fallback) => {
8725
+ try {
8726
+ return this.props.t(key);
8727
+ } catch {
8728
+ return fallback;
8729
+ }
8730
+ };
8731
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8732
+ className: PANEL_ROOT,
8733
+ translate: "no",
8734
+ "data-shop-crashed": true,
8735
+ role: "alert",
8736
+ children: [
8737
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8738
+ className: _dsh_plugin_shop_css_c4028823_default.stateLine,
8739
+ children: say("tabCrashed", en.tabCrashed)
8740
+ }),
8741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8742
+ className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
8743
+ children: error.message
8744
+ }),
8745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8746
+ type: "button",
8747
+ className: _dsh_plugin_shop_css_c4028823_default.actionButton,
8748
+ onClick: () => this.setState({ error: null }),
8749
+ children: say("retry", en.retry)
8750
+ })
8751
+ ]
8752
+ });
8753
+ }
8754
+ };
8755
+ /** The tab as the settings slot registers it (`index.ts`): the body inside the
8756
+ * shop's own error boundary. Exported under the name the body used to carry,
8757
+ * so nothing can register the tab without the boundary. */
8126
8758
  function ShopTab(props) {
8127
- const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, restart, version, updateStart, reload: injectedReload } = props;
8759
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopErrorBoundary, {
8760
+ t: props.t,
8761
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopTabBody, { ...props })
8762
+ });
8763
+ }
8764
+ /** The shop tab body: browse, search, refresh, and render one card per
8765
+ * entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
8766
+ function ShopTabBody(props) {
8767
+ const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, noteUninstalled, restart, version, updateStart, reload: injectedReload } = props;
8128
8768
  const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
8129
8769
  const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
8130
8770
  /** The install gate's own input, or undefined while it is unknown — loading,
@@ -8139,7 +8779,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8139
8779
  }), [injectedReload]);
8140
8780
  const [selfRestartGate, setSelfRestartGate] = (0, react.useState)(false);
8141
8781
  const selfUpdate = useUpdateSelf(updateStart, installStatus);
8782
+ const restartMonitor = useRestartMonitor();
8783
+ const hostPending = selfVersion !== null && selfVersion.pendingVersion !== null;
8784
+ const selfPending = selfUpdate.view.kind === "done" || selfUpdate.view.kind === "idle" && hostPending;
8142
8785
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
8786
+ const [versionReload, setVersionReload] = (0, react.useState)(0);
8143
8787
  const [mutations, setMutations] = (0, react.useState)(0);
8144
8788
  const noteMutation = (0, react.useCallback)(() => {
8145
8789
  setMutations((current) => current + 1);
@@ -8148,12 +8792,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8148
8792
  const flows = useInstallFlows(install, installStatus, (0, react.useCallback)((key, outcome) => {
8149
8793
  if (outcome === "done") uninstallResetRef.current(key);
8150
8794
  noteMutation();
8795
+ if (outcome === "done") setRequest({ kind: "reverdict" });
8151
8796
  }, [noteMutation]));
8152
8797
  const uninstallFlows = useUninstallFlows(uninstall, installStatus, (0, react.useCallback)((key, outcome) => {
8153
8798
  if (outcome !== "done") return;
8154
8799
  flows.flowFor(key).reset();
8155
8800
  noteMutation();
8156
- }, [flows, noteMutation]));
8801
+ const name = catalogState.kind === "ready" ? catalogState.result.plugins.find((entry) => entryKey(entry) === key)?.name : void 0;
8802
+ if (name !== void 0) noteUninstalled?.(name);
8803
+ setRequest({ kind: "reverdict" });
8804
+ }, [
8805
+ flows,
8806
+ noteMutation,
8807
+ catalogState,
8808
+ noteUninstalled
8809
+ ]));
8157
8810
  uninstallResetRef.current = uninstallFlows.resetFlow;
8158
8811
  const [reloading, setReloading] = (0, react.useState)(false);
8159
8812
  const [reloadFailed, setReloadFailed] = (0, react.useState)(false);
@@ -8164,21 +8817,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8164
8817
  const [visibleCount, setVisibleCount] = (0, react.useState)(48);
8165
8818
  const sentinelRef = (0, react.useRef)(null);
8166
8819
  const filteredLenRef = (0, react.useRef)(0);
8820
+ const pendingRefreshRef = (0, react.useRef)(null);
8167
8821
  (0, react.useEffect)(() => {
8168
8822
  let cancelled = false;
8169
- if (request.kind !== "refresh") setCatalogState((current) => current.kind === "ready" ? current : { kind: "loading" });
8823
+ if (request.kind !== "refresh" && request.kind !== "reverdict") setCatalogState((current) => current.kind === "ready" ? current : { kind: "loading" });
8824
+ if (request.kind === "refresh") pendingRefreshRef.current = request;
8170
8825
  const load = async () => {
8171
8826
  try {
8172
- const result = await catalog(request.kind === "refresh" ? { refresh: true } : void 0);
8827
+ const result = await catalog(request.kind === "refresh" ? { refresh: true } : request.kind === "reverdict" ? { reverdict: true } : void 0);
8173
8828
  if (!cancelled) setCatalogState({
8174
8829
  kind: "ready",
8175
8830
  result
8176
8831
  });
8177
8832
  } catch {
8178
- if (cancelled) return;
8833
+ if (cancelled) {
8834
+ if (request.kind === "refresh" && pendingRefreshRef.current === request) setReloadFailed(true);
8835
+ return;
8836
+ }
8179
8837
  setCatalogState((current) => current.kind === "ready" ? current : { kind: "error" });
8180
- setReloadFailed(true);
8838
+ if (request.kind !== "reverdict") setReloadFailed(true);
8181
8839
  } finally {
8840
+ if (pendingRefreshRef.current === request) pendingRefreshRef.current = null;
8182
8841
  if (!cancelled) setReloading(false);
8183
8842
  }
8184
8843
  };
@@ -8199,7 +8858,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8199
8858
  return () => {
8200
8859
  cancelled = true;
8201
8860
  };
8202
- }, [version, request]);
8861
+ }, [version, versionReload]);
8203
8862
  const [checkState, setCheckState] = (0, react.useState)("idle");
8204
8863
  const upToDateTimer = (0, react.useRef)(null);
8205
8864
  (0, react.useEffect)(() => () => {
@@ -8272,7 +8931,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8272
8931
  }
8273
8932
  return map;
8274
8933
  }, [catalogState, specs]);
8275
- const badgedIncompatible = (0, react.useCallback)((key) => (missingByKey.get(key) ?? []).length > 0 && !nameTakenByKey.has(key), [missingByKey, nameTakenByKey]);
8934
+ const harnessVerdicts = (0, react.useMemo)(() => catalogState.kind === "ready" ? catalogState.result.incompatibleHarness : void 0, [catalogState]);
8935
+ const badgedIncompatible = (0, react.useCallback)((key) => readsIncompatible(blockersOf(missingByKey.get(key) ?? [], harnessVerdictOf(harnessVerdicts, key), nameTakenByKey.get(key))), [
8936
+ missingByKey,
8937
+ harnessVerdicts,
8938
+ nameTakenByKey
8939
+ ]);
8276
8940
  const matched = (0, react.useMemo)(() => {
8277
8941
  const q = query.trim().toLowerCase();
8278
8942
  return (category === "installed" ? sortedCatalog : browsable).filter((entry) => {
@@ -8332,50 +8996,56 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8332
8996
  return map;
8333
8997
  }, [catalogState]);
8334
8998
  if (catalogState.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8335
- className: _dsh_plugin_shop_css_e3675a89_default.panel,
8999
+ className: PANEL_ROOT,
9000
+ translate: "no",
8336
9001
  "data-shop-tab": true,
8337
9002
  "aria-busy": "true",
8338
9003
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8339
- className: _dsh_plugin_shop_css_e3675a89_default.srOnly,
9004
+ className: _dsh_plugin_shop_css_c4028823_default.srOnly,
8340
9005
  children: t("loading")
8341
9006
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8342
- className: _dsh_plugin_shop_css_e3675a89_default.skeletonGrid,
9007
+ className: _dsh_plugin_shop_css_c4028823_default.skeletonGrid,
8343
9008
  "data-shop-skeleton": true,
8344
9009
  "aria-hidden": "true",
8345
9010
  children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8346
- className: _dsh_plugin_shop_css_e3675a89_default.skeletonCard,
9011
+ className: _dsh_plugin_shop_css_c4028823_default.skeletonCard,
8347
9012
  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 })
9013
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.skeletonName }),
9014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.skeletonSummary }),
9015
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.skeletonSummaryShort }),
9016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.skeletonActions })
8352
9017
  ]
8353
9018
  }, i))
8354
9019
  })]
8355
9020
  });
8356
9021
  if (catalogState.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8357
- className: _dsh_plugin_shop_css_e3675a89_default.panel,
9022
+ className: PANEL_ROOT,
9023
+ translate: "no",
8358
9024
  "data-shop-tab": true,
8359
9025
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8360
- className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
9026
+ className: _dsh_plugin_shop_css_c4028823_default.stateLine,
8361
9027
  children: t("error")
8362
9028
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8363
9029
  type: "button",
8364
- className: _dsh_plugin_shop_css_e3675a89_default.actionButton,
8365
- onClick: () => setRequest({ kind: "retry" }),
9030
+ className: _dsh_plugin_shop_css_c4028823_default.actionButton,
9031
+ onClick: () => {
9032
+ setRequest({ kind: "retry" });
9033
+ setVersionReload((current) => current + 1);
9034
+ },
8366
9035
  children: t("retry")
8367
9036
  })]
8368
9037
  });
8369
9038
  const { result } = catalogState;
8370
9039
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8371
- className: _dsh_plugin_shop_css_e3675a89_default.panel,
9040
+ className: PANEL_ROOT,
9041
+ translate: "no",
8372
9042
  "data-shop-tab": true,
8373
9043
  children: [
8374
9044
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8375
- className: _dsh_plugin_shop_css_e3675a89_default.toolbar,
9045
+ className: _dsh_plugin_shop_css_c4028823_default.toolbar,
8376
9046
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8377
9047
  type: "search",
8378
- className: _dsh_plugin_shop_css_e3675a89_default.searchInput,
9048
+ className: _dsh_plugin_shop_css_c4028823_default.searchInput,
8379
9049
  "aria-label": t("search"),
8380
9050
  placeholder: t("search"),
8381
9051
  value: query,
@@ -8384,27 +9054,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8384
9054
  setVisibleCount(48);
8385
9055
  }
8386
9056
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8387
- className: _dsh_plugin_shop_css_e3675a89_default.toolbarRight,
9057
+ className: _dsh_plugin_shop_css_c4028823_default.toolbarRight,
8388
9058
  children: [
8389
9059
  result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8390
- className: _dsh_plugin_shop_css_e3675a89_default.staleBadge,
9060
+ className: _dsh_plugin_shop_css_c4028823_default.staleBadge,
8391
9061
  children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
8392
9062
  }),
8393
9063
  selfVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8394
- className: _dsh_plugin_shop_css_e3675a89_default.versionBlock,
9064
+ className: _dsh_plugin_shop_css_c4028823_default.versionBlock,
8395
9065
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8396
- className: _dsh_plugin_shop_css_e3675a89_default.versionText,
9066
+ className: _dsh_plugin_shop_css_c4028823_default.versionText,
8397
9067
  "data-shop-version": true,
8398
9068
  children: ["v", selfVersion.installed]
8399
- }), selfUpdate.view.kind === "done" && restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9069
+ }), restartMonitor !== null ? null : selfPending && restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8400
9070
  type: "button",
8401
- className: _dsh_plugin_shop_css_e3675a89_default.restartSelfButton,
9071
+ className: _dsh_plugin_shop_css_c4028823_default.restartSelfButton,
8402
9072
  "data-shop-restart": true,
8403
9073
  onClick: () => setSelfRestartGate(true),
8404
9074
  children: t("restart")
8405
- }) : selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9075
+ }) : !selfPending && selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8406
9076
  type: "button",
8407
- className: _dsh_plugin_shop_css_e3675a89_default.updateSelfButton,
9077
+ className: _dsh_plugin_shop_css_c4028823_default.updateSelfButton,
8408
9078
  "data-shop-update-self": true,
8409
9079
  onClick: () => {
8410
9080
  if (selfVersion.latest !== null) selfUpdate.start({ version: selfVersion.latest });
@@ -8412,7 +9082,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8412
9082
  children: t("update")
8413
9083
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8414
9084
  type: "button",
8415
- className: _dsh_plugin_shop_css_e3675a89_default.checkUpdateButton,
9085
+ className: _dsh_plugin_shop_css_c4028823_default.checkUpdateButton,
8416
9086
  "data-shop-check-update": true,
8417
9087
  title: t("checkUpdateTitle"),
8418
9088
  disabled: checkState !== "idle",
@@ -8421,7 +9091,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8421
9091
  })]
8422
9092
  }),
8423
9093
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
8424
- className: _dsh_plugin_shop_css_e3675a89_default.githubLink,
9094
+ className: _dsh_plugin_shop_css_c4028823_default.githubLink,
8425
9095
  href: SHOP_REPO_URL,
8426
9096
  target: "_blank",
8427
9097
  rel: "noopener noreferrer",
@@ -8440,39 +9110,40 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8440
9110
  })]
8441
9111
  }),
8442
9112
  selfUpdate.view.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8443
- className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
9113
+ className: _dsh_plugin_shop_css_c4028823_default.selfUpdatePanel,
8444
9114
  "data-shop-self-updating": true,
8445
9115
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8446
- className: _dsh_plugin_shop_css_e3675a89_default.installing,
9116
+ className: _dsh_plugin_shop_css_c4028823_default.installing,
8447
9117
  children: t(installPhaseKey(selfUpdate.view.phase))
8448
9118
  }), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8449
- className: _dsh_plugin_shop_css_e3675a89_default.log,
9119
+ className: _dsh_plugin_shop_css_c4028823_default.log,
8450
9120
  children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8451
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
9121
+ className: _dsh_plugin_shop_css_c4028823_default.logLine,
8452
9122
  children: line
8453
9123
  }, index))
8454
9124
  })]
8455
9125
  }),
8456
- selfUpdate.view.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8457
- className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
9126
+ selfPending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9127
+ className: _dsh_plugin_shop_css_c4028823_default.selfUpdatePanel,
8458
9128
  "data-shop-self-update-done": true,
8459
9129
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8460
- className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
9130
+ className: _dsh_plugin_shop_css_c4028823_default.installedActions,
8461
9131
  children: [
8462
9132
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8463
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
9133
+ className: _dsh_plugin_shop_css_c4028823_default.notice,
8464
9134
  children: t("installedRestartNotice")
8465
9135
  }),
8466
9136
  restartBlocked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
8467
9137
  t,
8468
9138
  restart,
9139
+ reload,
8469
9140
  gate: {
8470
9141
  open: selfRestartGate,
8471
9142
  close: () => setSelfRestartGate(false)
8472
9143
  }
8473
9144
  }),
8474
9145
  restartBlocked !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8475
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
9146
+ className: _dsh_plugin_shop_css_c4028823_default.notice,
8476
9147
  "data-shop-restart-disabled": true,
8477
9148
  children: t(restartBlockedNoticeKey(restartBlocked))
8478
9149
  })
@@ -8480,34 +9151,34 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8480
9151
  })
8481
9152
  }),
8482
9153
  selfUpdate.view.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8483
- className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
9154
+ className: _dsh_plugin_shop_css_c4028823_default.selfUpdatePanel,
8484
9155
  "data-shop-self-update-failed": true,
8485
9156
  children: [
8486
9157
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8487
- className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
9158
+ className: _dsh_plugin_shop_css_c4028823_default.failedHeading,
8488
9159
  children: t("updateFailed")
8489
9160
  }),
8490
9161
  selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8491
- className: _dsh_plugin_shop_css_e3675a89_default.log,
9162
+ className: _dsh_plugin_shop_css_c4028823_default.log,
8492
9163
  children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8493
- className: _dsh_plugin_shop_css_e3675a89_default.logLine,
9164
+ className: _dsh_plugin_shop_css_c4028823_default.logLine,
8494
9165
  children: line
8495
9166
  }, index))
8496
9167
  }),
8497
9168
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8498
- className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
9169
+ className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
8499
9170
  children: selfUpdate.view.detail === "" ? t("updateTransportFailed") : selfUpdate.view.detail
8500
9171
  })
8501
9172
  ]
8502
9173
  }),
8503
9174
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8504
- className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
9175
+ className: _dsh_plugin_shop_css_c4028823_default.categoryBar,
8505
9176
  role: "group",
8506
9177
  "aria-label": t("catalog"),
8507
9178
  children: [
8508
9179
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8509
9180
  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,
9181
+ className: category === null ? `${_dsh_plugin_shop_css_c4028823_default.categoryButton} ${_dsh_plugin_shop_css_c4028823_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4028823_default.categoryButton,
8511
9182
  "aria-pressed": category === null,
8512
9183
  onClick: () => {
8513
9184
  setCategory(null);
@@ -8522,7 +9193,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8522
9193
  }),
8523
9194
  CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8524
9195
  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,
9196
+ className: category === key ? `${_dsh_plugin_shop_css_c4028823_default.categoryButton} ${_dsh_plugin_shop_css_c4028823_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4028823_default.categoryButton,
8526
9197
  "aria-pressed": category === key,
8527
9198
  "data-category": key,
8528
9199
  "data-shop-category-tab": key,
@@ -8538,7 +9209,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8538
9209
  }, key)),
8539
9210
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8540
9211
  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,
9212
+ className: category === "installed" ? `${_dsh_plugin_shop_css_c4028823_default.categoryButton} ${_dsh_plugin_shop_css_c4028823_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4028823_default.categoryButton,
8542
9213
  "aria-pressed": category === "installed",
8543
9214
  "data-shop-category-installed": true,
8544
9215
  onClick: () => {
@@ -8555,7 +9226,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8555
9226
  type: "button",
8556
9227
  role: "switch",
8557
9228
  "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,
9229
+ className: hideIncompatible ? `${_dsh_plugin_shop_css_c4028823_default.incompatibleFilter} ${_dsh_plugin_shop_css_c4028823_default.incompatibleFilterOn}` : _dsh_plugin_shop_css_c4028823_default.incompatibleFilter,
8559
9230
  title: t(hideIncompatible ? "showIncompatibleTitle" : "incompatibleFilterTitle"),
8560
9231
  "data-shop-hide-incompatible": true,
8561
9232
  onClick: () => {
@@ -8563,18 +9234,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8563
9234
  setVisibleCount(48);
8564
9235
  },
8565
9236
  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,
9237
+ className: hideIncompatible ? `${_dsh_plugin_shop_css_c4028823_default.switch} ${_dsh_plugin_shop_css_c4028823_default.switchOn}` : _dsh_plugin_shop_css_c4028823_default.switch,
8567
9238
  "aria-hidden": "true",
8568
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.switchKnob })
9239
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.switchKnob })
8569
9240
  }), t("hideIncompatible", { count: incompatibleCount })]
8570
9241
  })
8571
9242
  ]
8572
9243
  }),
8573
9244
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8574
- className: _dsh_plugin_shop_css_e3675a89_default.catalogStatsRow,
9245
+ className: _dsh_plugin_shop_css_c4028823_default.catalogStatsRow,
8575
9246
  children: [
8576
9247
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8577
- className: _dsh_plugin_shop_css_e3675a89_default.catalogStats,
9248
+ className: _dsh_plugin_shop_css_c4028823_default.catalogStats,
8578
9249
  "data-shop-catalog-stats": true,
8579
9250
  children: t("catalogStats", {
8580
9251
  count: String(browsable.length),
@@ -8583,32 +9254,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8583
9254
  }),
8584
9255
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8585
9256
  type: "button",
8586
- className: _dsh_plugin_shop_css_e3675a89_default.catalogRefreshButton,
9257
+ className: _dsh_plugin_shop_css_c4028823_default.catalogRefreshButton,
8587
9258
  "data-shop-catalog-refresh": true,
8588
9259
  disabled: reloading,
8589
9260
  onClick: () => {
8590
9261
  setReloading(true);
8591
9262
  setReloadFailed(false);
8592
9263
  setRequest({ kind: "refresh" });
9264
+ setVersionReload((current) => current + 1);
8593
9265
  },
8594
9266
  children: reloading ? t("refreshing") : t("refresh")
8595
9267
  }),
8596
9268
  reloadFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8597
- className: _dsh_plugin_shop_css_e3675a89_default.catalogRefreshFailed,
9269
+ className: _dsh_plugin_shop_css_c4028823_default.catalogRefreshFailed,
8598
9270
  "data-shop-catalog-refresh-failed": true,
8599
9271
  children: t("refreshFailed")
8600
9272
  })
8601
9273
  ]
8602
9274
  }),
8603
9275
  result.plugins.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8604
- className: _dsh_plugin_shop_css_e3675a89_default.emptyLine,
9276
+ className: _dsh_plugin_shop_css_c4028823_default.emptyLine,
8605
9277
  children: t("empty")
8606
9278
  }) : filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8607
- className: _dsh_plugin_shop_css_e3675a89_default.emptyLine,
9279
+ className: _dsh_plugin_shop_css_c4028823_default.emptyLine,
8608
9280
  "data-shop-empty": true,
8609
9281
  children: matched.length > 0 ? t("emptyIncompatibleFiltered") : t("emptySearch")
8610
9282
  }) : /* @__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,
9283
+ className: _dsh_plugin_shop_css_c4028823_default.cards,
8612
9284
  children: [visible.map((entry) => {
8613
9285
  const key = entryKey(entry);
8614
9286
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
@@ -8616,6 +9288,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8616
9288
  stars: starsOf(entry, stars),
8617
9289
  installed: installedByKey.get(key),
8618
9290
  missing: missingByKey.get(key) ?? [],
9291
+ harness: harnessVerdictOf(harnessVerdicts, key),
8619
9292
  nameTakenBy: nameTakenByKey.get(key),
8620
9293
  t,
8621
9294
  flow: flows.flowFor(key),
@@ -8628,12 +9301,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8628
9301
  }) }, key);
8629
9302
  }), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
8630
9303
  ref: sentinelRef,
8631
- className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
9304
+ className: _dsh_plugin_shop_css_c4028823_default.cardsSentry,
8632
9305
  "data-shop-sentry": true,
8633
9306
  "aria-hidden": "true"
8634
9307
  })]
8635
9308
  }), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8636
- className: _dsh_plugin_shop_css_e3675a89_default.showingLine,
9309
+ className: _dsh_plugin_shop_css_c4028823_default.showingLine,
8637
9310
  "aria-live": "polite",
8638
9311
  children: t("showing", {
8639
9312
  shown: String(visibleCount),
@@ -8644,6 +9317,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8644
9317
  state: installedState,
8645
9318
  entriesByKey,
8646
9319
  missingByKey,
9320
+ harnessVerdicts,
8647
9321
  t,
8648
9322
  setEnabled,
8649
9323
  flowFor: flows.flowFor,
@@ -8670,6 +9344,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8670
9344
  * fire-and-forget from surfacing as an unhandled rejection. Every later
8671
9345
  * result, including an explicit refresh, replaces this timestamped stash. */
8672
9346
  let warmCatalog = null;
9347
+ /** Package names THIS PAGE has uninstalled since it loaded (design
9348
+ * 2026-09-01-harness-compatibility section 9.1): the module table can still
9349
+ * hold a graph row or a `loadCache` record for one after a restart-free
9350
+ * uninstall (module-table.ts's header explains why), so
9351
+ * the refinement must never clear a name here on the table's say-so — the
9352
+ * host's verdict stands unconditionally. Module-level so it survives the tab
9353
+ * closing and reopening; reset in `apply` beside `warmCatalog`, because a
9354
+ * re-applied bundle is a new page that has uninstalled nothing yet. An
9355
+ * uninstall made from another tab, another window, or the CLI is never added
9356
+ * here and stays unknown to this page until it reloads. */
9357
+ let pageRemoved = /* @__PURE__ */ new Set();
8673
9358
  /** Services required by the tab registration and the Remote mount.
8674
9359
  *
8675
9360
  * `remote.shop` is deliberately ABSENT: this package both mounts its own
@@ -8700,6 +9385,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8700
9385
  return result.value;
8701
9386
  };
8702
9387
  warmCatalog = null;
9388
+ pageRemoved = /* @__PURE__ */ new Set();
8703
9389
  const warmed = Promise.resolve(ns.catalog(void 0)).then((result) => unwrap(result));
8704
9390
  warmCatalog = {
8705
9391
  at: Date.now(),
@@ -8708,36 +9394,62 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8708
9394
  warmed.catch(() => {});
8709
9395
  Promise.resolve(ns.installed()).then((result) => unwrap(result)).catch(() => {});
8710
9396
  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));
9397
+ const hostCatalog = async (args) => {
9398
+ if (args?.refresh === true) {
9399
+ const refreshed = unwrap(await ns.catalog({ refresh: true }));
8726
9400
  warmCatalog = {
8727
9401
  at: Date.now(),
8728
- result: Promise.resolve(fresh)
9402
+ result: Promise.resolve(refreshed)
8729
9403
  };
8730
- return fresh;
9404
+ return refreshed;
9405
+ }
9406
+ if (args?.reverdict === true) {
9407
+ warmCatalog = null;
9408
+ const judged = unwrap(await ns.catalog(void 0));
9409
+ warmCatalog = {
9410
+ at: Date.now(),
9411
+ result: Promise.resolve(judged)
9412
+ };
9413
+ return judged;
9414
+ }
9415
+ const warm = warmCatalog;
9416
+ if (warm !== null && Date.now() - warm.at < 3e5) try {
9417
+ return await warm.result;
9418
+ } catch {}
9419
+ const fresh = unwrap(await ns.catalog(void 0));
9420
+ warmCatalog = {
9421
+ at: Date.now(),
9422
+ result: Promise.resolve(fresh)
9423
+ };
9424
+ return fresh;
9425
+ };
9426
+ const handOver = async (result) => ({
9427
+ ...result,
9428
+ incompatible: await refineAgainstModuleTable(result.incompatible ?? {}, ctx.get("modules"), pageRemoved)
9429
+ });
9430
+ const injected = () => ({
9431
+ catalog: async (args) => handOver(await hostCatalog(args)),
9432
+ install: async (args) => {
9433
+ warmCatalog = null;
9434
+ return unwrap(await ns.installStart(args));
8731
9435
  },
8732
- install: async (args) => unwrap(await ns.installStart(args)),
8733
9436
  installStatus: async (args) => unwrap(await ns.installStatus(args)),
8734
9437
  setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
8735
9438
  installed: async () => unwrap(await ns.installed()),
8736
9439
  installedSpecs: async () => unwrap(await ns.installedSpecs()),
8737
- uninstall: async (args) => unwrap(await ns.uninstallStart(args)),
9440
+ uninstall: async (args) => {
9441
+ warmCatalog = null;
9442
+ return unwrap(await ns.uninstallStart(args));
9443
+ },
9444
+ noteUninstalled: (name) => {
9445
+ pageRemoved.add(name);
9446
+ },
8738
9447
  restart: async () => unwrap(await ns.restart()),
8739
9448
  version: async () => unwrap(await ns.version()),
8740
- updateStart: async (args) => unwrap(await ns.updateStart(args))
9449
+ updateStart: async (args) => {
9450
+ warmCatalog = null;
9451
+ return unwrap(await ns.updateStart(args));
9452
+ }
8741
9453
  });
8742
9454
  ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
8743
9455
  name: "settings.plugins.tab",