dsh-plugin-shop 0.4.1 → 0.4.3

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
@@ -4306,6 +4306,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4306
4306
  })),
4307
4307
  "stars": record(string(), number())
4308
4308
  });
4309
+ const dsh_plugin_shop_shop_installed_result$schema = array(object({
4310
+ "name": string(),
4311
+ "installed": string(),
4312
+ "latest": string(),
4313
+ "outdated": boolean()
4314
+ }));
4309
4315
  const dsh_plugin_shop_shop_installStart_parameter_0$schema = object({
4310
4316
  "name": string(),
4311
4317
  "version": string(),
@@ -4344,11 +4350,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4344
4350
  ]).optional(),
4345
4351
  "detail": union([_undefined(), string()]).optional()
4346
4352
  });
4347
- const dsh_plugin_shop_shop_outdated_result$schema = array(object({
4348
- "name": string(),
4349
- "installed": string(),
4350
- "latest": string()
4351
- }));
4353
+ const dsh_plugin_shop_shop_restart_result$schema = union([object({
4354
+ "ok": literal(true),
4355
+ "url": string()
4356
+ }), object({
4357
+ "ok": literal(false),
4358
+ "detail": string()
4359
+ })]);
4352
4360
  const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = object({
4353
4361
  "name": string(),
4354
4362
  "enabled": boolean()
@@ -4357,6 +4365,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4357
4365
  "ok": boolean(),
4358
4366
  "detail": union([_undefined(), string()]).optional()
4359
4367
  });
4368
+ const dsh_plugin_shop_shop_uninstallStart_parameter_0$schema = object({ "name": string() });
4369
+ const dsh_plugin_shop_shop_uninstallStart_result$schema = union([object({
4370
+ "ok": literal(true),
4371
+ "installId": string()
4372
+ }), object({
4373
+ "ok": literal(false),
4374
+ "detail": string()
4375
+ })]);
4360
4376
  const TYPERT_REMOTE = {
4361
4377
  package: "dsh-plugin-shop",
4362
4378
  descriptors: [
@@ -4384,7 +4400,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4384
4400
  },
4385
4401
  sourceLocation: {
4386
4402
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4387
- "line": 180,
4403
+ "line": 218,
4404
+ "column": 9
4405
+ }
4406
+ },
4407
+ {
4408
+ id: "dsh-plugin-shop#shop/installed",
4409
+ service: "shop",
4410
+ namespace: "shop",
4411
+ method: "installed",
4412
+ invocation: { kind: "direct" },
4413
+ parameters: [],
4414
+ result: {
4415
+ mode: "strict",
4416
+ typeSymbol: "dsh-plugin-shop#shop/installed:result",
4417
+ schema: dsh_plugin_shop_shop_installed_result$schema
4418
+ },
4419
+ sourceLocation: {
4420
+ "file": "packages/dsh-plugin-shop/src/host/index.ts",
4421
+ "line": 293,
4388
4422
  "column": 9
4389
4423
  }
4390
4424
  },
@@ -4412,7 +4446,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4412
4446
  },
4413
4447
  sourceLocation: {
4414
4448
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4415
- "line": 206,
4449
+ "line": 244,
4416
4450
  "column": 9
4417
4451
  }
4418
4452
  },
@@ -4439,25 +4473,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4439
4473
  },
4440
4474
  sourceLocation: {
4441
4475
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4442
- "line": 244,
4476
+ "line": 282,
4443
4477
  "column": 3
4444
4478
  }
4445
4479
  },
4446
4480
  {
4447
- id: "dsh-plugin-shop#shop/outdated",
4481
+ id: "dsh-plugin-shop#shop/restart",
4448
4482
  service: "shop",
4449
4483
  namespace: "shop",
4450
- method: "outdated",
4484
+ method: "restart",
4451
4485
  invocation: { kind: "direct" },
4452
4486
  parameters: [],
4453
4487
  result: {
4454
4488
  mode: "strict",
4455
- typeSymbol: "dsh-plugin-shop#shop/outdated:result",
4456
- schema: dsh_plugin_shop_shop_outdated_result$schema
4489
+ typeSymbol: "dsh-plugin-shop/types#ShopRestartResult",
4490
+ schema: dsh_plugin_shop_shop_restart_result$schema
4457
4491
  },
4458
4492
  sourceLocation: {
4459
4493
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4460
- "line": 252,
4494
+ "line": 367,
4461
4495
  "column": 9
4462
4496
  }
4463
4497
  },
@@ -4484,9 +4518,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4484
4518
  },
4485
4519
  sourceLocation: {
4486
4520
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4487
- "line": 149,
4521
+ "line": 187,
4488
4522
  "column": 3
4489
4523
  }
4524
+ },
4525
+ {
4526
+ id: "dsh-plugin-shop#shop/uninstallStart",
4527
+ service: "shop",
4528
+ namespace: "shop",
4529
+ method: "uninstallStart",
4530
+ implementation: "uninstall",
4531
+ invocation: { kind: "direct" },
4532
+ parameters: [{
4533
+ name: "args",
4534
+ wire: "args",
4535
+ source: "json",
4536
+ codec: {
4537
+ mode: "strict",
4538
+ typeSymbol: "dsh-plugin-shop#shop/uninstallStart:args",
4539
+ schema: dsh_plugin_shop_shop_uninstallStart_parameter_0$schema
4540
+ }
4541
+ }],
4542
+ result: {
4543
+ mode: "strict",
4544
+ typeSymbol: "dsh-plugin-shop/types#ShopUninstallResult",
4545
+ schema: dsh_plugin_shop_shop_uninstallStart_result$schema
4546
+ },
4547
+ sourceLocation: {
4548
+ "file": "packages/dsh-plugin-shop/src/host/index.ts",
4549
+ "line": 333,
4550
+ "column": 9
4551
+ }
4490
4552
  }
4491
4553
  ]
4492
4554
  };
@@ -4691,6 +4753,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4691
4753
  installedVersion: "已安装 v{version}",
4692
4754
  latestVersion: "最新 v{version}",
4693
4755
  update: "更新",
4756
+ installed: "已安装",
4757
+ uninstall: "卸载",
4758
+ uninstalling: "正在卸载…",
4759
+ uninstallFailed: "卸载失败",
4760
+ uninstallTransportFailed: "卸载请求未能送达。请稍后重试。",
4761
+ uninstalledRestartNotice: "已卸载;重启 dsh 后生效",
4762
+ restart: "重启 dsh",
4763
+ restartTitle: "确认重启 dsh",
4764
+ restartBody: "重启会断开当前页面与服务器的连接,正在进行的对话和任务会中断;浏览器会自动跳转到重启后的地址,稍等片刻即可继续使用。",
4765
+ restartConfirm: "重启",
4766
+ restarting: "正在重启,浏览器即将跳转…",
4767
+ restartTransportFailed: "重启请求未能送达。请稍后重试。",
4694
4768
  enabledSwitch: "启用",
4695
4769
  toggleFailed: "设置失败,请重试。",
4696
4770
  hotApplyNote: "无需重启,立即生效"
@@ -4744,6 +4818,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4744
4818
  installedVersion: "installed v{version}",
4745
4819
  latestVersion: "latest v{version}",
4746
4820
  update: "Update",
4821
+ installed: "Installed",
4822
+ uninstall: "Uninstall",
4823
+ uninstalling: "Uninstalling…",
4824
+ uninstallFailed: "Uninstall failed",
4825
+ uninstallTransportFailed: "The uninstall request could not be delivered. Please retry.",
4826
+ uninstalledRestartNotice: "uninstalled; restart dsh to activate",
4827
+ restart: "Restart dsh",
4828
+ restartTitle: "Restart dsh?",
4829
+ restartBody: "Restarting disconnects this page and interrupts any conversation or task in progress; the browser will jump to the restarted address automatically, so give it a few seconds.",
4830
+ restartConfirm: "Restart",
4831
+ restarting: "Restarting; the browser will jump in a moment…",
4832
+ restartTransportFailed: "The restart request could not be delivered. Please retry.",
4747
4833
  enabledSwitch: "Enable",
4748
4834
  toggleFailed: "Failed to update. Please retry.",
4749
4835
  hotApplyNote: "takes effect without a restart"
@@ -4751,6 +4837,29 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4751
4837
  //#endregion
4752
4838
  //#region src/client/useInstall.ts
4753
4839
  /** Install driving hook for one entry: start, poll to terminal, reset. */
4840
+ /** The poll loop shared by the install and uninstall drivers: while the view
4841
+ * is `running`, poll once per second and fold each status through the
4842
+ * reducer. A poll failure is transient — the host retains the record, so the
4843
+ * next tick finds it. The rejection handler must be present: an unhandled
4844
+ * rejection here would escape the poll loop. */
4845
+ function usePollStatus(view, setView, installStatus) {
4846
+ (0, react.useEffect)(() => {
4847
+ if (view.kind !== "running") return;
4848
+ const timer = setInterval(() => {
4849
+ installStatus({ installId: view.installId }).then((status) => {
4850
+ setView((current) => reduceInstall(current, {
4851
+ type: "status",
4852
+ status
4853
+ }));
4854
+ }, () => {});
4855
+ }, INSTALL_POLL_MS);
4856
+ return () => clearInterval(timer);
4857
+ }, [
4858
+ view,
4859
+ setView,
4860
+ installStatus
4861
+ ]);
4862
+ }
4754
4863
  /** Drive one install: rejections, the polling loop at INSTALL_POLL_MS, and
4755
4864
  * terminal states. The interval is cleared on unmount and whenever the view
4756
4865
  * leaves `running`, so a done/failed/rejected install never polls again. */
@@ -4781,18 +4890,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4781
4890
  });
4782
4891
  }
4783
4892
  }, [install]);
4784
- (0, react.useEffect)(() => {
4785
- if (view.kind !== "running") return;
4786
- const timer = setInterval(() => {
4787
- installStatus({ installId: view.installId }).then((status) => {
4788
- setView((current) => reduceInstall(current, {
4789
- type: "status",
4790
- status
4791
- }));
4792
- }, () => {});
4793
- }, INSTALL_POLL_MS);
4794
- return () => clearInterval(timer);
4795
- }, [view, installStatus]);
4893
+ usePollStatus(view, setView, installStatus);
4796
4894
  return {
4797
4895
  view,
4798
4896
  start,
@@ -4800,8 +4898,51 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4800
4898
  };
4801
4899
  }
4802
4900
  //#endregion
4901
+ //#region src/client/useUninstall.ts
4902
+ /** Uninstall driving hook for one entry: start, poll to terminal. Shares the
4903
+ * install view machine and poll loop; the only difference is the start
4904
+ * mapping — an uninstall business failure (not in the catalog / not
4905
+ * installed) lands in the `failed` view with the host's published detail,
4906
+ * never in the install `rejected` codes, which belong to the install gate
4907
+ * (§7.2). */
4908
+ /** Drive one uninstall: the host's business union, the shared polling loop,
4909
+ * and terminal states. The `rejected` install state never occurs here. */
4910
+ function useUninstall(uninstall, installStatus) {
4911
+ const [view, setView] = (0, react.useState)({ kind: "idle" });
4912
+ const start = (0, react.useCallback)(async (args) => {
4913
+ setView({ kind: "idle" });
4914
+ try {
4915
+ const result = await uninstall(args);
4916
+ if (!result.ok) {
4917
+ setView({
4918
+ kind: "failed",
4919
+ detail: result.detail,
4920
+ log: []
4921
+ });
4922
+ return;
4923
+ }
4924
+ setView({
4925
+ kind: "running",
4926
+ installId: result.installId,
4927
+ log: []
4928
+ });
4929
+ } catch {
4930
+ setView({
4931
+ kind: "failed",
4932
+ detail: "",
4933
+ log: []
4934
+ });
4935
+ }
4936
+ }, [uninstall]);
4937
+ usePollStatus(view, setView, installStatus);
4938
+ return {
4939
+ view,
4940
+ start
4941
+ };
4942
+ }
4943
+ //#endregion
4803
4944
  //#region \0dsh-plugin-shop-css:e3675a89
4804
- 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 (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * 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}\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: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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.s30e662_skeletonCover {\n display: block;\n height: 26px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 12px 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 14px;\n border-radius: 4px;\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.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.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\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 gap: 6px;\n margin-top: 10px;\n}\n\n.s26d58e_categoryButton {\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.s26d58e_categoryButton: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.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\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.sccc4d1_catalogStats {\n margin: -8px 0 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.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.sf657f5_cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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 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 240px;\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(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n.s9e2bec_cardCover {\n display: block;\n padding: 5px 12px 5px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--spine-hue, #8B8E96) 12%, transparent);\n}\n\n.s1a7c5b_coverLabel {\n display: block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 10px 12px 6px;\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 owns the full header width; badges sit on their own line below. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n overflow-wrap: anywhere;\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.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.s23e3e1_unclaimedBadge {\n padding: 1px 7px;\n border-radius: 999px;\n border: 1px dashed var(--dsw-alias-border-l2);\n font-size: 11px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\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 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.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.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.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.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/* Installed section. */\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 font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\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.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 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";
4945
+ 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 (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * 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}\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: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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.s30e662_skeletonCover {\n display: block;\n height: 26px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 12px 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 14px;\n border-radius: 4px;\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.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.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\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 gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\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.s26d58e_categoryButton: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.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\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.sccc4d1_catalogStats {\n margin: 6px 0 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.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.sf657f5_cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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 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 240px;\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(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n.s9e2bec_cardCover {\n display: block;\n padding: 5px 12px 5px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--spine-hue, #8B8E96) 12%, transparent);\n}\n\n.s1a7c5b_coverLabel {\n display: block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 10px 12px 6px;\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 owns the full header width; badges sit on their own line below. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n overflow-wrap: anywhere;\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.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.s23e3e1_unclaimedBadge {\n padding: 1px 7px;\n border-radius: 999px;\n border: 1px dashed var(--dsw-alias-border-l2);\n font-size: 11px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\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 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.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.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.s6ac237_restartButton {\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 border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\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.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/* Installed section. */\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 font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\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.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 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";
4805
4946
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
4806
4947
  const tag = document.createElement("style");
4807
4948
  tag.dataset.pluginCss = "e3675a89";
@@ -4864,6 +5005,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4864
5005
  "installButton": "se7aaed_installButton",
4865
5006
  "confirmButton": "sb43a49_confirmButton",
4866
5007
  "cancelButton": "s6e45d1_cancelButton",
5008
+ "installedLabel": "sb6fc74_installedLabel",
5009
+ "installedActions": "sf53ca0_installedActions",
5010
+ "uninstallButton": "s96967f_uninstallButton",
5011
+ "restartButton": "s6ac237_restartButton",
4867
5012
  "gate": "sc4f2ac_gate",
4868
5013
  "gateTitle": "s559c38_gateTitle",
4869
5014
  "gateBody": "s3c0ace_gateBody",
@@ -4909,8 +5054,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4909
5054
  /** One entry card: the category spine and cover band, the expandable header
4910
5055
  * (name, tier, category, unclaimed marker), the plain-text summary in both
4911
5056
  * languages, the self-declared capabilities, the detail section, and the
4912
- * install controls. */
4913
- const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, t, install, installStatus }) {
5057
+ * install controls. An installed plugin's card carries its installed row:
5058
+ * current the non-interactive installed label, behind the update button;
5059
+ * uninstalled → the install button. */
5060
+ const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, t, install, installStatus, uninstall, restart }) {
4914
5061
  const [open, setOpen] = (0, react.useState)(false);
4915
5062
  const detailId = (0, react.useId)();
4916
5063
  const summary = entry.catalog?.summary;
@@ -5016,13 +5163,36 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5016
5163
  })
5017
5164
  })]
5018
5165
  }),
5019
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5166
+ installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5020
5167
  name: entry.name,
5021
5168
  version: entry.version,
5022
5169
  tier: entry.tier,
5023
5170
  t,
5024
5171
  install,
5025
- installStatus
5172
+ installStatus,
5173
+ restart
5174
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5175
+ className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
5176
+ children: [installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5177
+ name: entry.name,
5178
+ version: entry.version,
5179
+ tier: entry.tier,
5180
+ variant: "update",
5181
+ t,
5182
+ install,
5183
+ installStatus,
5184
+ restart
5185
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5186
+ className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
5187
+ "data-shop-installed": true,
5188
+ children: t("installed")
5189
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
5190
+ name: entry.name,
5191
+ t,
5192
+ uninstall,
5193
+ installStatus,
5194
+ restart
5195
+ })]
5026
5196
  })
5027
5197
  ]
5028
5198
  })
@@ -5034,7 +5204,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5034
5204
  * failure detail, rejection detail — driven by `useInstall`. Shared by the
5035
5205
  * catalog cards (`variant: 'install'`) and the outdated rows' update button
5036
5206
  * (`variant: 'update'`, which drives the same install flow for `name@latest`). */
5037
- function InstallPanel({ name, version, tier, variant = "install", t, install, installStatus }) {
5207
+ function InstallPanel({ name, version, tier, variant = "install", t, install, installStatus, restart }) {
5038
5208
  const [gateOpen, setGateOpen] = (0, react.useState)(false);
5039
5209
  const { view, start } = useInstall(install, installStatus);
5040
5210
  if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5050,10 +5220,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5050
5220
  }, index))
5051
5221
  })]
5052
5222
  });
5053
- if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5054
- className: _dsh_plugin_shop_css_e3675a89_default.notice,
5055
- "data-shop-restart-notice": true,
5056
- children: view.needsRestart ? t("installedRestartNotice") : t("installedNoRestartNotice")
5223
+ if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5224
+ className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
5225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5226
+ className: _dsh_plugin_shop_css_e3675a89_default.notice,
5227
+ "data-shop-restart-notice": true,
5228
+ children: view.needsRestart ? t("installedRestartNotice") : t("installedNoRestartNotice")
5229
+ }), view.needsRestart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
5230
+ t,
5231
+ restart
5232
+ })]
5057
5233
  });
5058
5234
  if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5059
5235
  className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
@@ -5136,13 +5312,146 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5136
5312
  children: t(update ? "update" : "install")
5137
5313
  });
5138
5314
  }
5315
+ /** One installed entry's uninstall flow: remove from the profile through the
5316
+ * same executor records and poll loop as installs. Uninstalling revokes
5317
+ * privilege rather than granting it, so there is no acknowledgement gate —
5318
+ * §9.3 is about granting. A business failure (not in the catalog / not
5319
+ * installed) lands in the failed view with the host's published detail; a
5320
+ * transport failure carries the empty detail and the localized fallback. */
5321
+ function UninstallPanel({ name, t, uninstall, installStatus, restart }) {
5322
+ const { view, start } = useUninstall(uninstall, installStatus);
5323
+ if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5324
+ className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
5325
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5326
+ className: _dsh_plugin_shop_css_e3675a89_default.installing,
5327
+ children: t("uninstalling")
5328
+ }), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5329
+ className: _dsh_plugin_shop_css_e3675a89_default.log,
5330
+ children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5331
+ className: _dsh_plugin_shop_css_e3675a89_default.logLine,
5332
+ children: line
5333
+ }, index))
5334
+ })]
5335
+ });
5336
+ if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5337
+ className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
5338
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5339
+ className: _dsh_plugin_shop_css_e3675a89_default.notice,
5340
+ "data-shop-uninstall-done": true,
5341
+ children: t("uninstalledRestartNotice")
5342
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
5343
+ t,
5344
+ restart
5345
+ })]
5346
+ });
5347
+ if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5348
+ className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
5349
+ children: [
5350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5351
+ className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
5352
+ children: t("uninstallFailed")
5353
+ }),
5354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5355
+ className: _dsh_plugin_shop_css_e3675a89_default.log,
5356
+ children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5357
+ className: _dsh_plugin_shop_css_e3675a89_default.logLine,
5358
+ children: line
5359
+ }, index))
5360
+ }),
5361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5362
+ className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
5363
+ children: view.detail === "" ? t("uninstallTransportFailed") : view.detail
5364
+ })
5365
+ ]
5366
+ });
5367
+ if (view.kind !== "idle") return null;
5368
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5369
+ type: "button",
5370
+ className: _dsh_plugin_shop_css_e3675a89_default.uninstallButton,
5371
+ "data-shop-uninstall": true,
5372
+ onClick: () => void start({ name }),
5373
+ children: t("uninstall")
5374
+ });
5375
+ }
5376
+ /** The §8 restart flow (amendment 2026-08-27): after an install, update, or
5377
+ * uninstall reports done, this panel offers a restart of dsh. The
5378
+ * confirmation gate states the cost — the page disconnects and in-flight
5379
+ * conversations/tasks are interrupted — and on confirm the restart RPC runs;
5380
+ * the returned URL is where the browser jumps. A failed restart renders the
5381
+ * host's published detail: the old server is still up, nothing was lost. */
5382
+ function RestartPanel({ t, restart }) {
5383
+ const [gateOpen, setGateOpen] = (0, react.useState)(false);
5384
+ const [state, setState] = (0, react.useState)({ kind: "idle" });
5385
+ const onConfirm = async () => {
5386
+ setGateOpen(false);
5387
+ setState({ kind: "restarting" });
5388
+ try {
5389
+ const result = await restart();
5390
+ if (result.ok) window.location.href = result.url;
5391
+ else setState({
5392
+ kind: "failed",
5393
+ detail: result.detail
5394
+ });
5395
+ } catch {
5396
+ setState({
5397
+ kind: "failed",
5398
+ detail: t("restartTransportFailed")
5399
+ });
5400
+ }
5401
+ };
5402
+ if (state.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5403
+ className: _dsh_plugin_shop_css_e3675a89_default.notice,
5404
+ "data-shop-restarting": true,
5405
+ children: t("restarting")
5406
+ });
5407
+ if (state.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5408
+ className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
5409
+ "data-shop-restart-error": true,
5410
+ children: state.detail
5411
+ });
5412
+ if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5413
+ className: _dsh_plugin_shop_css_e3675a89_default.gate,
5414
+ children: [
5415
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5416
+ className: _dsh_plugin_shop_css_e3675a89_default.gateTitle,
5417
+ children: t("restartTitle")
5418
+ }),
5419
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5420
+ className: _dsh_plugin_shop_css_e3675a89_default.gateBody,
5421
+ children: t("restartBody")
5422
+ }),
5423
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5424
+ className: _dsh_plugin_shop_css_e3675a89_default.gateActions,
5425
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5426
+ type: "button",
5427
+ className: _dsh_plugin_shop_css_e3675a89_default.confirmButton,
5428
+ "data-shop-restart-confirm": true,
5429
+ onClick: () => void onConfirm(),
5430
+ children: t("restartConfirm")
5431
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5432
+ type: "button",
5433
+ className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
5434
+ onClick: () => setGateOpen(false),
5435
+ children: t("cancel")
5436
+ })]
5437
+ })
5438
+ ]
5439
+ });
5440
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5441
+ type: "button",
5442
+ className: _dsh_plugin_shop_css_e3675a89_default.restartButton,
5443
+ "data-shop-restart": true,
5444
+ onClick: () => setGateOpen(true),
5445
+ children: t("restart")
5446
+ });
5447
+ }
5139
5448
  /** One outdated install row (§7.3): the name, the installed and latest
5140
5449
  * versions, an optimistic enable/disable switch, and the update button (the
5141
5450
  * install flow for `name@latest`, reusing `InstallPanel`). The switch has no
5142
5451
  * installed-state RPC behind it — the tab assumes an installed plugin is on,
5143
5452
  * so the first toggle sends the inverted value. After a successful `setEnabled`
5144
5453
  * the §8 hot note renders. */
5145
- function OutdatedRow({ row, tier, t, setEnabled, install, installStatus }) {
5454
+ function OutdatedRow({ row, tier, t, setEnabled, install, installStatus, restart }) {
5146
5455
  const [enabled, setEnabledState] = (0, react.useState)(true);
5147
5456
  const [toggle, setToggle] = (0, react.useState)({ kind: "idle" });
5148
5457
  const onToggle = async () => {
@@ -5207,7 +5516,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5207
5516
  variant: "update",
5208
5517
  t,
5209
5518
  install,
5210
- installStatus
5519
+ installStatus,
5520
+ restart
5211
5521
  })]
5212
5522
  }),
5213
5523
  toggle.kind === "saved" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -5223,18 +5533,22 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5223
5533
  ]
5224
5534
  });
5225
5535
  }
5226
- /** The §7.3 outdated list, rendered as the "installed" section: each row shows
5227
- * both versions, a switch, and an update button. The tier for the update gate
5228
- * is looked up from the catalog by name (community acknowledgement); an entry
5229
- * absent from the catalog defaults to the community gate (the safer read). */
5230
- function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus }) {
5536
+ /** The §7.3 installed list, rendered as the "installed" section: each row
5537
+ * shows both versions, a switch, and an update button. The rows are the
5538
+ * installed entries filtered to `outdated` a current install is already
5539
+ * spoken for by its shelf card's installed label, and has no row here. The
5540
+ * tier for the update gate is looked up from the catalog by name (community →
5541
+ * acknowledgement); an entry absent from the catalog defaults to the
5542
+ * community gate (the safer read). */
5543
+ function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus, restart }) {
5231
5544
  if (state.kind === "loading") return null;
5232
5545
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5233
5546
  className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
5234
5547
  "data-shop-outdated-error": true,
5235
5548
  children: t("error")
5236
5549
  });
5237
- if (state.entries.length === 0) return null;
5550
+ const outdated = state.entries.filter((entry) => entry.outdated);
5551
+ if (outdated.length === 0) return null;
5238
5552
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5239
5553
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedSection,
5240
5554
  "data-shop-outdated": true,
@@ -5243,13 +5557,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5243
5557
  children: t("installedSection")
5244
5558
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5245
5559
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedList,
5246
- children: state.entries.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
5560
+ children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
5247
5561
  row,
5248
5562
  tier: tiers.get(row.name) ?? "community",
5249
5563
  t,
5250
5564
  setEnabled,
5251
5565
  install,
5252
- installStatus
5566
+ installStatus,
5567
+ restart
5253
5568
  }) }, row.name))
5254
5569
  })]
5255
5570
  });
@@ -5257,9 +5572,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5257
5572
  /** The shop tab root: browse, search, refresh, and render one card per
5258
5573
  * entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
5259
5574
  function ShopTab(props) {
5260
- const { t, catalog, install, installStatus, setEnabled, outdated } = props;
5575
+ const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart } = props;
5261
5576
  const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
5262
- const [outdatedState, setOutdatedState] = (0, react.useState)({ kind: "loading" });
5577
+ const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
5263
5578
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
5264
5579
  const [query, setQuery] = (0, react.useState)("");
5265
5580
  const [category, setCategory] = (0, react.useState)(null);
@@ -5290,26 +5605,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5290
5605
  let cancelled = false;
5291
5606
  const load = async () => {
5292
5607
  try {
5293
- const entries = await outdated();
5294
- if (!cancelled) setOutdatedState({
5608
+ const entries = await installed();
5609
+ if (!cancelled) setInstalledState({
5295
5610
  kind: "ready",
5296
5611
  entries
5297
5612
  });
5298
5613
  } catch {
5299
- if (!cancelled) setOutdatedState({ kind: "error" });
5614
+ if (!cancelled) setInstalledState({ kind: "error" });
5300
5615
  }
5301
5616
  };
5302
5617
  load();
5303
5618
  return () => {
5304
5619
  cancelled = true;
5305
5620
  };
5306
- }, [outdated, request]);
5621
+ }, [installed, request]);
5622
+ const installedByName = (0, react.useMemo)(() => {
5623
+ const map = /* @__PURE__ */ new Map();
5624
+ if (installedState.kind === "ready") for (const entry of installedState.entries) map.set(entry.name, entry);
5625
+ return map;
5626
+ }, [installedState]);
5307
5627
  const filtered = (0, react.useMemo)(() => {
5308
5628
  if (catalogState.kind !== "ready") return [];
5309
5629
  const q = query.trim().toLowerCase();
5310
5630
  return catalogState.result.plugins.filter((entry) => {
5311
5631
  if (isShopLike(entry.name)) return false;
5312
- if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
5632
+ if (category === "installed") {
5633
+ if (!installedByName.has(entry.name)) return false;
5634
+ } else if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
5313
5635
  if (q === "") return true;
5314
5636
  const summaryEn = entry.catalog?.summary.en ?? "";
5315
5637
  const summaryZh = entry.catalog?.summary.zh ?? "";
@@ -5318,7 +5640,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5318
5640
  }, [
5319
5641
  catalogState,
5320
5642
  query,
5321
- category
5643
+ category,
5644
+ installedByName
5322
5645
  ]);
5323
5646
  filteredLenRef.current = filtered.length;
5324
5647
  const stars = catalogState.kind === "ready" ? catalogState.result.stars : {};
@@ -5348,6 +5671,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5348
5671
  }
5349
5672
  return counts;
5350
5673
  }, [catalogState]);
5674
+ const installedCount = (0, react.useMemo)(() => {
5675
+ if (installedState.kind !== "ready") return 0;
5676
+ return installedState.entries.filter((entry) => !isShopLike(entry.name)).length;
5677
+ }, [installedState]);
5351
5678
  const tiers = (0, react.useMemo)(() => {
5352
5679
  const map = /* @__PURE__ */ new Map();
5353
5680
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.tier);
@@ -5422,37 +5749,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5422
5749
  className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
5423
5750
  role: "group",
5424
5751
  "aria-label": t("catalog"),
5425
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5426
- type: "button",
5427
- className: category === null ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5428
- "aria-pressed": category === null,
5429
- onClick: () => {
5430
- setCategory(null);
5431
- setVisibleCount(48);
5432
- },
5433
- children: [
5434
- t("all"),
5435
- " ",
5436
- [...categoryCounts.values()].reduce((a, b) => a + b, 0)
5437
- ]
5438
- }), CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5439
- type: "button",
5440
- className: category === key ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5441
- "aria-pressed": category === key,
5442
- onClick: () => {
5443
- setCategory(key);
5444
- setVisibleCount(48);
5445
- },
5446
- children: [
5447
- t(categoryLocaleKey(key)),
5448
- " ",
5449
- categoryCounts.get(key) ?? 0
5450
- ]
5451
- }, key))]
5452
- }),
5453
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
5454
- className: _dsh_plugin_shop_css_e3675a89_default.catalogHeading,
5455
- children: t("catalog")
5752
+ children: [
5753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5754
+ type: "button",
5755
+ className: category === null ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5756
+ "aria-pressed": category === null,
5757
+ onClick: () => {
5758
+ setCategory(null);
5759
+ setVisibleCount(48);
5760
+ },
5761
+ children: [
5762
+ t("all"),
5763
+ " ",
5764
+ [...categoryCounts.values()].reduce((a, b) => a + b, 0)
5765
+ ]
5766
+ }),
5767
+ CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5768
+ type: "button",
5769
+ className: category === key ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5770
+ "aria-pressed": category === key,
5771
+ onClick: () => {
5772
+ setCategory(key);
5773
+ setVisibleCount(48);
5774
+ },
5775
+ children: [
5776
+ t(categoryLocaleKey(key)),
5777
+ " ",
5778
+ categoryCounts.get(key) ?? 0
5779
+ ]
5780
+ }, key)),
5781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5782
+ type: "button",
5783
+ className: category === "installed" ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5784
+ "aria-pressed": category === "installed",
5785
+ onClick: () => {
5786
+ setCategory("installed");
5787
+ setVisibleCount(48);
5788
+ },
5789
+ children: [
5790
+ t("installed"),
5791
+ " ",
5792
+ installedCount
5793
+ ]
5794
+ })
5795
+ ]
5456
5796
  }),
5457
5797
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5458
5798
  className: _dsh_plugin_shop_css_e3675a89_default.catalogStats,
@@ -5472,9 +5812,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5472
5812
  children: [visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
5473
5813
  entry,
5474
5814
  stars: stars[entry.name],
5815
+ installed: installedByName.get(entry.name),
5475
5816
  t,
5476
5817
  install,
5477
- installStatus
5818
+ installStatus,
5819
+ uninstall,
5820
+ restart
5478
5821
  }) }, entry.name)), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
5479
5822
  ref: sentinelRef,
5480
5823
  className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
@@ -5490,12 +5833,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5490
5833
  })
5491
5834
  })] }),
5492
5835
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedSection, {
5493
- state: outdatedState,
5836
+ state: installedState,
5494
5837
  tiers,
5495
5838
  t,
5496
5839
  setEnabled,
5497
5840
  install,
5498
- installStatus
5841
+ installStatus,
5842
+ restart
5499
5843
  })
5500
5844
  ]
5501
5845
  });
@@ -5538,7 +5882,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5538
5882
  install: async (args) => unwrap(await ns.installStart(args)),
5539
5883
  installStatus: async (args) => unwrap(await ns.installStatus(args)),
5540
5884
  setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
5541
- outdated: async () => unwrap(await ns.outdated())
5885
+ installed: async () => unwrap(await ns.installed()),
5886
+ uninstall: async (args) => unwrap(await ns.uninstallStart(args)),
5887
+ restart: async () => unwrap(await ns.restart())
5542
5888
  });
5543
5889
  ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
5544
5890
  name: "settings.plugins.tab",