dsh-plugin-shop 0.4.3 → 0.4.5

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/README.md CHANGED
@@ -46,10 +46,13 @@ install them once with `npm install -g @deepseek-ai/dsh pnpm` and verify with
46
46
  `dsh --version` and `pnpm --version`.
47
47
 
48
48
  ```sh
49
- # dsh on PATH (global install):
50
- dsh plugin --profile web add dsh-plugin-shop
49
+ # dsh on PATH (global install). The explicit version pin matters: pnpm 11
50
+ # holds back very recent releases, so a bare `add dsh-plugin-shop` can hand
51
+ # you an older version for a while. Pin the current release — refresh it
52
+ # with `npm view dsh-plugin-shop version`.
53
+ dsh plugin --profile web add dsh-plugin-shop@0.4.5
51
54
  # or straight through npx, nothing installed:
52
- npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop
55
+ npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.4.5
53
56
  ```
54
57
 
55
58
  Replace `web` with your profile if you use another one. Then restart `dsh` once — a
@@ -75,10 +78,11 @@ profile.
75
78
  ls -1 "${DSH_HOME:-$HOME/.dsh}/profiles" | grep -v '^node_modules$'
76
79
  ```
77
80
 
78
- **2. Install.**
81
+ **2. Install.** Pin the version — the explicit pin bypasses pnpm's release
82
+ cooldown, and deterministic installs are the point of the agent path.
79
83
 
80
84
  ```sh
81
- dsh plugin --profile <profile> add dsh-plugin-shop
85
+ dsh plugin --profile <profile> add dsh-plugin-shop@0.4.5
82
86
  ```
83
87
 
84
88
  **3. Verify — do not skip this.** A zero exit from step 2 means pnpm resolved the
@@ -101,7 +105,7 @@ bundle is not.
101
105
  | What you see | What it means | What to do |
102
106
  |---|---|---|
103
107
  | `error: required option '--profile <name>' not specified` | `--profile` was omitted | Pass it; there is no default |
104
- | A version older than npm's `latest` gets installed | pnpm 11 holds back very recently published versions | Expected, not an error; the newest lands once its cooldown passes |
108
+ | A version older than npm's `latest` gets installed | pnpm 11 holds back very recently published versions | Pin the version explicitly: `dsh plugin --profile <p> add dsh-plugin-shop@<version>` |
105
109
  | `client bundles not found ... lib/client.js` | the copy on disk was built without its browser half | Install from npm rather than from a source checkout, or run `pnpm build` in that checkout |
106
110
  | `dsh: pnpm not found on PATH — install pnpm to manage profile plugins` | `dsh plugin` forwards to pnpm, and pnpm is missing | `npm install -g pnpm` |
107
111
  | `no profile directory found above <path>` | the plugin could not locate its profile | Please report it — this is resolved from `ctx.baseUrl` and should not fail |
@@ -116,6 +120,7 @@ bundle is not.
116
120
  | **Enable / disable** | Applies to an installed plugin without a restart |
117
121
  | **Installed state** | An installed plugin shows an Installed label on its card — or an Update button when the catalog has a newer version — plus an Uninstall button; the Installed filter in the category bar shows only installed plugins |
118
122
  | **Restart** | After an install, update, or uninstall, the shop offers to restart dsh — stating the cost first: the page disconnects and in-flight work is interrupted |
123
+ | **Self-update** | The shop shows its own version next to the search box, checks npm for a newer release, and updates itself with the pinned version — then the usual restart |
119
124
 
120
125
  ## 🧩 How it is put together
121
126
 
@@ -125,9 +130,9 @@ boundary:
125
130
  | Half | Entry | Can reach | Cannot reach |
126
131
  |---|---|---|---|
127
132
  | **Host** | `dsh-plugin-shop` | The network (catalog fetch and sha256 verify), the filesystem (cache), `dsh plugin add` under a per-profile mutex | — |
128
- | **Client** | `dsh-plugin-shop/client` | Exactly seven `shop/*` Remote methods | The network, the filesystem |
133
+ | **Client** | `dsh-plugin-shop/client` | Exactly nine `shop/*` Remote methods | The network, the filesystem |
129
134
 
130
- Compromising the browser half buys an attacker those seven calls and nothing more.
135
+ Compromising the browser half buys an attacker those nine calls and nothing more.
131
136
 
132
137
  ## ⚙️ Configuration
133
138
 
package/lib/client.js CHANGED
@@ -4350,10 +4350,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4350
4350
  ]).optional(),
4351
4351
  "detail": union([_undefined(), string()]).optional()
4352
4352
  });
4353
- const dsh_plugin_shop_shop_restart_result$schema = union([object({
4354
- "ok": literal(true),
4355
- "url": string()
4356
- }), object({
4353
+ const dsh_plugin_shop_shop_restart_result$schema = union([object({ "ok": literal(true) }), object({
4357
4354
  "ok": literal(false),
4358
4355
  "detail": string()
4359
4356
  })]);
@@ -4373,6 +4370,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4373
4370
  "ok": literal(false),
4374
4371
  "detail": string()
4375
4372
  })]);
4373
+ const dsh_plugin_shop_shop_updateStart_parameter_0$schema = object({ "version": string() });
4374
+ const dsh_plugin_shop_shop_updateStart_result$schema = union([object({
4375
+ "ok": literal(true),
4376
+ "installId": string()
4377
+ }), object({
4378
+ "ok": literal(false),
4379
+ "detail": string()
4380
+ })]);
4381
+ const dsh_plugin_shop_shop_version_result$schema = object({
4382
+ "installed": string(),
4383
+ "latest": union([literal(null), string()]),
4384
+ "outdated": boolean()
4385
+ });
4376
4386
  const TYPERT_REMOTE = {
4377
4387
  package: "dsh-plugin-shop",
4378
4388
  descriptors: [
@@ -4400,7 +4410,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4400
4410
  },
4401
4411
  sourceLocation: {
4402
4412
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4403
- "line": 218,
4413
+ "line": 244,
4404
4414
  "column": 9
4405
4415
  }
4406
4416
  },
@@ -4418,7 +4428,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4418
4428
  },
4419
4429
  sourceLocation: {
4420
4430
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4421
- "line": 293,
4431
+ "line": 319,
4422
4432
  "column": 9
4423
4433
  }
4424
4434
  },
@@ -4446,7 +4456,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4446
4456
  },
4447
4457
  sourceLocation: {
4448
4458
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4449
- "line": 244,
4459
+ "line": 270,
4450
4460
  "column": 9
4451
4461
  }
4452
4462
  },
@@ -4473,7 +4483,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4473
4483
  },
4474
4484
  sourceLocation: {
4475
4485
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4476
- "line": 282,
4486
+ "line": 308,
4477
4487
  "column": 3
4478
4488
  }
4479
4489
  },
@@ -4491,7 +4501,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4491
4501
  },
4492
4502
  sourceLocation: {
4493
4503
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4494
- "line": 367,
4504
+ "line": 392,
4495
4505
  "column": 9
4496
4506
  }
4497
4507
  },
@@ -4518,7 +4528,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4518
4528
  },
4519
4529
  sourceLocation: {
4520
4530
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4521
- "line": 187,
4531
+ "line": 213,
4522
4532
  "column": 3
4523
4533
  }
4524
4534
  },
@@ -4546,7 +4556,52 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4546
4556
  },
4547
4557
  sourceLocation: {
4548
4558
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4549
- "line": 333,
4559
+ "line": 359,
4560
+ "column": 9
4561
+ }
4562
+ },
4563
+ {
4564
+ id: "dsh-plugin-shop#shop/updateStart",
4565
+ service: "shop",
4566
+ namespace: "shop",
4567
+ method: "updateStart",
4568
+ invocation: { kind: "direct" },
4569
+ parameters: [{
4570
+ name: "args",
4571
+ wire: "args",
4572
+ source: "json",
4573
+ codec: {
4574
+ mode: "strict",
4575
+ typeSymbol: "dsh-plugin-shop#shop/updateStart:args",
4576
+ schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema
4577
+ }
4578
+ }],
4579
+ result: {
4580
+ mode: "strict",
4581
+ typeSymbol: "dsh-plugin-shop/types#ShopUpdateResult",
4582
+ schema: dsh_plugin_shop_shop_updateStart_result$schema
4583
+ },
4584
+ sourceLocation: {
4585
+ "file": "packages/dsh-plugin-shop/src/host/index.ts",
4586
+ "line": 441,
4587
+ "column": 9
4588
+ }
4589
+ },
4590
+ {
4591
+ id: "dsh-plugin-shop#shop/version",
4592
+ service: "shop",
4593
+ namespace: "shop",
4594
+ method: "version",
4595
+ invocation: { kind: "direct" },
4596
+ parameters: [],
4597
+ result: {
4598
+ mode: "strict",
4599
+ typeSymbol: "dsh-plugin-shop/types#ShopVersionResult",
4600
+ schema: dsh_plugin_shop_shop_version_result$schema
4601
+ },
4602
+ sourceLocation: {
4603
+ "file": "packages/dsh-plugin-shop/src/host/index.ts",
4604
+ "line": 426,
4550
4605
  "column": 9
4551
4606
  }
4552
4607
  }
@@ -4637,6 +4692,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4637
4692
  "shop",
4638
4693
  "marketplace"
4639
4694
  ];
4695
+ /** Competing marketplaces whose names carry no store/market keyword and so
4696
+ * escape the pattern rules below. Named explicitly rather than guessed:
4697
+ * `dsh-plugin` is the npm package of github.com/dshplugin/dsh-plugin-hub —
4698
+ * a community plugin marketplace for DeepSeek Harness. */
4699
+ const SHOP_LIKE_NAMES = ["dsh-plugin"];
4640
4700
  /**
4641
4701
  * Whether a package name reads as a plugin shop (a marketplace for dsh
4642
4702
  * plugins, e.g. `dsh-plugin-shop`, `dsh-store`, `pluginstore`). The shop
@@ -4649,6 +4709,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4649
4709
  * it matches other people's package names, not ours.
4650
4710
  */
4651
4711
  function isShopLike(name) {
4712
+ if (SHOP_LIKE_NAMES.includes(name.toLowerCase())) return true;
4652
4713
  const segments = name.toLowerCase().split(/[-_.]+/);
4653
4714
  const hasPlugin = segments.includes("plugin");
4654
4715
  if (segments.some((segment) => /plugin(store|market|mall|shop|marketplace)/.test(segment) || /(store|market|mall|shop|marketplace)plugin/.test(segment))) return true;
@@ -4763,8 +4824,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4763
4824
  restartTitle: "确认重启 dsh",
4764
4825
  restartBody: "重启会断开当前页面与服务器的连接,正在进行的对话和任务会中断;浏览器会自动跳转到重启后的地址,稍等片刻即可继续使用。",
4765
4826
  restartConfirm: "重启",
4766
- restarting: "正在重启,浏览器即将跳转…",
4827
+ restarting: "正在重启,服务器就绪后页面会自动刷新…",
4828
+ restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
4767
4829
  restartTransportFailed: "重启请求未能送达。请稍后重试。",
4830
+ updateFailed: "更新失败",
4831
+ updateTransportFailed: "更新请求未能送达。请稍后重试。",
4768
4832
  enabledSwitch: "启用",
4769
4833
  toggleFailed: "设置失败,请重试。",
4770
4834
  hotApplyNote: "无需重启,立即生效"
@@ -4828,8 +4892,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4828
4892
  restartTitle: "Restart dsh?",
4829
4893
  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
4894
  restartConfirm: "Restart",
4831
- restarting: "Restarting; the browser will jump in a moment…",
4895
+ restarting: "Restarting; the page will refresh once the server is back…",
4896
+ restartFailedNotice: "The server did not come back after the restart. Start it manually: dsh web (see restart.log in the shop cache directory)",
4832
4897
  restartTransportFailed: "The restart request could not be delivered. Please retry.",
4898
+ updateFailed: "Update failed",
4899
+ updateTransportFailed: "The update request could not be delivered. Please retry.",
4833
4900
  enabledSwitch: "Enable",
4834
4901
  toggleFailed: "Failed to update. Please retry.",
4835
4902
  hotApplyNote: "takes effect without a restart"
@@ -4941,8 +5008,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4941
5008
  };
4942
5009
  }
4943
5010
  //#endregion
5011
+ //#region src/client/useUpdateSelf.ts
5012
+ /** Self-update driving hook for the shop itself: start, poll to terminal.
5013
+ * Shares the install view machine and poll loop; the start mapping is the
5014
+ * uninstall one — a business refusal (a non-semver version) lands in the
5015
+ * `failed` view with the host's detail, never in the install `rejected`
5016
+ * codes. */
5017
+ /** Drive one self-update: the host's business union, the shared polling
5018
+ * loop, and terminal states. The `rejected` install state never occurs. */
5019
+ function useUpdateSelf(updateStart, installStatus) {
5020
+ const [view, setView] = (0, react.useState)({ kind: "idle" });
5021
+ const start = (0, react.useCallback)(async (args) => {
5022
+ setView({ kind: "idle" });
5023
+ try {
5024
+ const result = await updateStart(args);
5025
+ if (!result.ok) {
5026
+ setView({
5027
+ kind: "failed",
5028
+ detail: result.detail,
5029
+ log: []
5030
+ });
5031
+ return;
5032
+ }
5033
+ setView({
5034
+ kind: "running",
5035
+ installId: result.installId,
5036
+ log: []
5037
+ });
5038
+ } catch {
5039
+ setView({
5040
+ kind: "failed",
5041
+ detail: "",
5042
+ log: []
5043
+ });
5044
+ }
5045
+ }, [updateStart]);
5046
+ usePollStatus(view, setView, installStatus);
5047
+ return {
5048
+ view,
5049
+ start
5050
+ };
5051
+ }
5052
+ //#endregion
4944
5053
  //#region \0dsh-plugin-shop-css:e3675a89
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";
5054
+ 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.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 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.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";
4946
5055
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
4947
5056
  const tag = document.createElement("style");
4948
5057
  tag.dataset.pluginCss = "e3675a89";
@@ -4965,6 +5074,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4965
5074
  "skeletonSummaryShort": "sbb0d25_skeletonSummaryShort",
4966
5075
  "actionButton": "s73242a_actionButton",
4967
5076
  "toolbar": "s646509_toolbar",
5077
+ "toolbarRight": "s745e3c_toolbarRight",
5078
+ "versionBlock": "s4fe61f_versionBlock",
5079
+ "versionText": "sb9b924_versionText",
5080
+ "updateSelfButton": "sa04455_updateSelfButton",
5081
+ "selfUpdatePanel": "sb81ba5_selfUpdatePanel",
4968
5082
  "searchInput": "s232ec4_searchInput",
4969
5083
  "staleBlock": "s1555aa_staleBlock",
4970
5084
  "staleBadge": "s3946f1_staleBadge",
@@ -5376,22 +5490,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5376
5490
  /** The §8 restart flow (amendment 2026-08-27): after an install, update, or
5377
5491
  * uninstall reports done, this panel offers a restart of dsh. The
5378
5492
  * 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. */
5493
+ * conversations/tasks are interrupted — and on confirm the restart RPC
5494
+ * commits the two-phase handoff: the host exits, a helper re-runs dsh, and
5495
+ * this panel polls the origin after a grace period, refreshing the page
5496
+ * once the NEW server answers. A refused restart renders the host's
5497
+ * published detail; a server that never comes back names the manual
5498
+ * command. */
5382
5499
  function RestartPanel({ t, restart }) {
5383
5500
  const [gateOpen, setGateOpen] = (0, react.useState)(false);
5384
5501
  const [state, setState] = (0, react.useState)({ kind: "idle" });
5385
5502
  const onConfirm = async () => {
5386
5503
  setGateOpen(false);
5387
- setState({ kind: "restarting" });
5388
5504
  try {
5389
5505
  const result = await restart();
5390
- if (result.ok) window.location.href = result.url;
5391
- else setState({
5392
- kind: "failed",
5393
- detail: result.detail
5394
- });
5506
+ if (!result.ok) {
5507
+ setState({
5508
+ kind: "failed",
5509
+ detail: result.detail
5510
+ });
5511
+ return;
5512
+ }
5513
+ setState({ kind: "restarting" });
5395
5514
  } catch {
5396
5515
  setState({
5397
5516
  kind: "failed",
@@ -5399,6 +5518,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5399
5518
  });
5400
5519
  }
5401
5520
  };
5521
+ (0, react.useEffect)(() => {
5522
+ if (state.kind !== "restarting") return;
5523
+ const started = Date.now();
5524
+ const timer = setInterval(() => {
5525
+ const elapsed = Date.now() - started;
5526
+ if (elapsed < 3e3) return;
5527
+ fetch(window.location.href, { cache: "no-store" }).then(() => {
5528
+ window.location.reload();
5529
+ }, () => {});
5530
+ if (elapsed > 3e4) {
5531
+ clearInterval(timer);
5532
+ setState({
5533
+ kind: "failed",
5534
+ detail: t("restartFailedNotice")
5535
+ });
5536
+ }
5537
+ }, INSTALL_POLL_MS);
5538
+ return () => clearInterval(timer);
5539
+ }, [state, t]);
5402
5540
  if (state.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5403
5541
  className: _dsh_plugin_shop_css_e3675a89_default.notice,
5404
5542
  "data-shop-restarting": true,
@@ -5572,9 +5710,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5572
5710
  /** The shop tab root: browse, search, refresh, and render one card per
5573
5711
  * entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
5574
5712
  function ShopTab(props) {
5575
- const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart } = props;
5713
+ const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart, version, updateStart } = props;
5576
5714
  const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
5577
5715
  const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
5716
+ const [selfVersion, setSelfVersion] = (0, react.useState)(null);
5717
+ const selfUpdate = useUpdateSelf(updateStart, installStatus);
5578
5718
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
5579
5719
  const [query, setQuery] = (0, react.useState)("");
5580
5720
  const [category, setCategory] = (0, react.useState)(null);
@@ -5601,6 +5741,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5601
5741
  cancelled = true;
5602
5742
  };
5603
5743
  }, [catalog, request]);
5744
+ (0, react.useEffect)(() => {
5745
+ let cancelled = false;
5746
+ const load = async () => {
5747
+ try {
5748
+ const result = await version();
5749
+ if (!cancelled) setSelfVersion(result);
5750
+ } catch {}
5751
+ };
5752
+ load();
5753
+ return () => {
5754
+ cancelled = true;
5755
+ };
5756
+ }, [version, request]);
5604
5757
  (0, react.useEffect)(() => {
5605
5758
  let cancelled = false;
5606
5759
  const load = async () => {
@@ -5732,19 +5885,86 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5732
5885
  setQuery(event.target.value);
5733
5886
  setVisibleCount(48);
5734
5887
  }
5735
- }), result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5736
- className: _dsh_plugin_shop_css_e3675a89_default.staleBlock,
5737
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5738
- className: _dsh_plugin_shop_css_e3675a89_default.staleBadge,
5739
- children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
5740
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5741
- type: "button",
5742
- className: _dsh_plugin_shop_css_e3675a89_default.actionButton,
5743
- onClick: () => setRequest({ kind: "refresh" }),
5744
- children: t("refresh")
5888
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5889
+ className: _dsh_plugin_shop_css_e3675a89_default.toolbarRight,
5890
+ children: [result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5891
+ className: _dsh_plugin_shop_css_e3675a89_default.staleBlock,
5892
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5893
+ className: _dsh_plugin_shop_css_e3675a89_default.staleBadge,
5894
+ children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
5895
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5896
+ type: "button",
5897
+ className: _dsh_plugin_shop_css_e3675a89_default.actionButton,
5898
+ onClick: () => setRequest({ kind: "refresh" }),
5899
+ children: t("refresh")
5900
+ })]
5901
+ }), selfVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5902
+ className: _dsh_plugin_shop_css_e3675a89_default.versionBlock,
5903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5904
+ className: _dsh_plugin_shop_css_e3675a89_default.versionText,
5905
+ "data-shop-version": true,
5906
+ children: ["v", selfVersion.installed]
5907
+ }), selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5908
+ type: "button",
5909
+ className: _dsh_plugin_shop_css_e3675a89_default.updateSelfButton,
5910
+ "data-shop-update-self": true,
5911
+ onClick: () => {
5912
+ if (selfVersion.latest !== null) selfUpdate.start({ version: selfVersion.latest });
5913
+ },
5914
+ children: t("update")
5915
+ })]
5745
5916
  })]
5746
5917
  })]
5747
5918
  }),
5919
+ selfUpdate.view.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5920
+ className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
5921
+ "data-shop-self-updating": true,
5922
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5923
+ className: _dsh_plugin_shop_css_e3675a89_default.installing,
5924
+ children: t("installing")
5925
+ }), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5926
+ className: _dsh_plugin_shop_css_e3675a89_default.log,
5927
+ children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5928
+ className: _dsh_plugin_shop_css_e3675a89_default.logLine,
5929
+ children: line
5930
+ }, index))
5931
+ })]
5932
+ }),
5933
+ selfUpdate.view.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5934
+ className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
5935
+ "data-shop-self-update-done": true,
5936
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5937
+ className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
5938
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5939
+ className: _dsh_plugin_shop_css_e3675a89_default.notice,
5940
+ children: t("installedRestartNotice")
5941
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
5942
+ t,
5943
+ restart
5944
+ })]
5945
+ })
5946
+ }),
5947
+ selfUpdate.view.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5948
+ className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
5949
+ "data-shop-self-update-failed": true,
5950
+ children: [
5951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5952
+ className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
5953
+ children: t("updateFailed")
5954
+ }),
5955
+ selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5956
+ className: _dsh_plugin_shop_css_e3675a89_default.log,
5957
+ children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5958
+ className: _dsh_plugin_shop_css_e3675a89_default.logLine,
5959
+ children: line
5960
+ }, index))
5961
+ }),
5962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5963
+ className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
5964
+ children: selfUpdate.view.detail === "" ? t("updateTransportFailed") : selfUpdate.view.detail
5965
+ })
5966
+ ]
5967
+ }),
5748
5968
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5749
5969
  className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
5750
5970
  role: "group",
@@ -5884,7 +6104,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5884
6104
  setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
5885
6105
  installed: async () => unwrap(await ns.installed()),
5886
6106
  uninstall: async (args) => unwrap(await ns.uninstallStart(args)),
5887
- restart: async () => unwrap(await ns.restart())
6107
+ restart: async () => unwrap(await ns.restart()),
6108
+ version: async () => unwrap(await ns.version()),
6109
+ updateStart: async (args) => unwrap(await ns.updateStart(args))
5888
6110
  });
5889
6111
  ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
5890
6112
  name: "settings.plugins.tab",