@linxin666/dsh-client-ui-community-plugins 0.2.0 → 0.2.2

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
@@ -5,10 +5,11 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
9
  let react_jsx_runtime = require("react/jsx-runtime");
9
10
  let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
10
11
  //#region \0dsh-css:packages/dsh-community-plugins/src/client/settings-card.module.css.mjs
11
- const css$1 = ".hX6Abq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.hX6Abq_card:hover{border-color:var(--dsw-alias-label-dimmed)}.hX6Abq_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.hX6Abq_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.hX6Abq_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.hX6Abq_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.hX6Abq_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.hX6Abq_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.hX6Abq_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.hX6Abq_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.hX6Abq_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.hX6Abq_chevronOpen{transform:rotate(180deg)}.hX6Abq_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.hX6Abq_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.hX6Abq_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.hX6Abq_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.hX6Abq_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.hX6Abq_discard,.hX6Abq_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.hX6Abq_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.hX6Abq_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.hX6Abq_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.hX6Abq_discard:disabled,.hX6Abq_save:disabled{opacity:.4;cursor:default}.hX6Abq_discard:focus-visible,.hX6Abq_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.hX6Abq_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.hX6Abq_field+.hX6Abq_field{border-top:1px solid var(--dsw-alias-border-l2)}.hX6Abq_head{align-items:center;gap:8px;display:flex}.hX6Abq_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.hX6Abq_badges{align-items:center;gap:8px;display:inline-flex}.hX6Abq_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.hX6Abq_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.hX6Abq_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.hX6Abq_reset:disabled{cursor:default}.hX6Abq_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.hX6Abq_input,.hX6Abq_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.hX6Abq_input:focus-visible,.hX6Abq_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.hX6Abq_input:disabled,.hX6Abq_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.hX6Abq_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.hX6Abq_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.hX6Abq_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.hX6Abq_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.hX6Abq_card,.hX6Abq_header,.hX6Abq_chevron,.hX6Abq_chevronOpen,.hX6Abq_discard,.hX6Abq_save{transition:none}}";
12
+ const css$1 = ".hX6Abq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.hX6Abq_card:hover{border-color:var(--dsw-alias-label-dimmed)}.hX6Abq_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.hX6Abq_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.hX6Abq_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.hX6Abq_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.hX6Abq_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.hX6Abq_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.hX6Abq_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.hX6Abq_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.hX6Abq_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.hX6Abq_chevronOpen{transform:rotate(180deg)}.hX6Abq_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.hX6Abq_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.hX6Abq_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.hX6Abq_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.hX6Abq_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.hX6Abq_discard,.hX6Abq_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.hX6Abq_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.hX6Abq_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.hX6Abq_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.hX6Abq_discard:disabled,.hX6Abq_save:disabled{opacity:.4;cursor:default}.hX6Abq_discard:focus-visible,.hX6Abq_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.hX6Abq_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.hX6Abq_field+.hX6Abq_field{border-top:1px solid var(--dsw-alias-border-l2)}.hX6Abq_head{align-items:center;gap:8px;display:flex}.hX6Abq_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.hX6Abq_badges{align-items:center;gap:8px;display:inline-flex}.hX6Abq_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.hX6Abq_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.hX6Abq_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.hX6Abq_reset:disabled{cursor:default}.hX6Abq_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.hX6Abq_input,.hX6Abq_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.hX6Abq_input:focus-visible,.hX6Abq_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.hX6Abq_input:disabled,.hX6Abq_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.hX6Abq_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.hX6Abq_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.hX6Abq_selectWrap{position:relative}.hX6Abq_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.hX6Abq_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.hX6Abq_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.hX6Abq_selectChevronOpen{transform:rotate(180deg)}.hX6Abq_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.hX6Abq_selectPopupOpen{opacity:1;transform:none}.hX6Abq_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.hX6Abq_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.hX6Abq_selectOption:hover,.hX6Abq_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.hX6Abq_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.hX6Abq_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.hX6Abq_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.hX6Abq_card,.hX6Abq_header,.hX6Abq_chevron,.hX6Abq_chevronOpen,.hX6Abq_discard,.hX6Abq_save,.hX6Abq_selectChevron,.hX6Abq_selectChevronOpen,.hX6Abq_selectPopup{transition:none}}";
12
13
  const tagId$1 = "@linxin666/dsh-client-ui-community-plugins/settings-card.module.css";
13
14
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
14
15
  const tag = document.createElement("style");
@@ -45,7 +46,18 @@ window.__ModuleLoader__.load({
45
46
  "readOnly": "hX6Abq_readOnly",
46
47
  "reset": "hX6Abq_reset",
47
48
  "save": "hX6Abq_save",
48
- "select": "hX6Abq_select"
49
+ "select": "hX6Abq_select",
50
+ "selectButton": "hX6Abq_selectButton",
51
+ "selectChevron": "hX6Abq_selectChevron",
52
+ "selectChevronOpen": "hX6Abq_selectChevronOpen",
53
+ "selectLabel": "hX6Abq_selectLabel",
54
+ "selectOption": "hX6Abq_selectOption",
55
+ "selectOptionActive": "hX6Abq_selectOptionActive",
56
+ "selectOptionSelected": "hX6Abq_selectOptionSelected",
57
+ "selectPopup": "hX6Abq_selectPopup",
58
+ "selectPopupClose": "hX6Abq_selectPopupClose",
59
+ "selectPopupOpen": "hX6Abq_selectPopupOpen",
60
+ "selectWrap": "hX6Abq_selectWrap"
49
61
  };
50
62
  //#endregion
51
63
  //#region src/client/PluginSettingsCard.tsx
@@ -179,6 +191,175 @@ window.__ModuleLoader__.load({
179
191
  }) : null]
180
192
  });
181
193
  }
194
+ const NON_SKIN_BODY_MARKERS = /* @__PURE__ */ new Set(["dshSkinCenter", "dshSidebarCollapsed"]);
195
+ function isSkinActive() {
196
+ return Object.keys(document.body.dataset).some((key) => key.startsWith("dsh") && !NON_SKIN_BODY_MARKERS.has(key));
197
+ }
198
+ const SELECT_CLOSE_MS = 100;
199
+ /**
200
+ * The shared dual-mode select control. While an appearance skin is active it
201
+ * renders the legacy native `<select>` untouched, so element-level skin
202
+ * selectors keep working; under the default appearance it renders a
203
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
204
+ * Staged cards reach it through BooleanField/ChoiceField; immediate-apply
205
+ * editors (the side-card prefs) bind it directly through onEdit.
206
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
207
+ */
208
+ function SelectField(props) {
209
+ const { id, options, value } = props;
210
+ const [open, setOpen] = (0, react.useState)(false);
211
+ const [closing, setClosing] = (0, react.useState)(false);
212
+ const [phase, setPhase] = (0, react.useState)("initial");
213
+ const [activeIndex, setActiveIndex] = (0, react.useState)(0);
214
+ const closeTimer = (0, react.useRef)(void 0);
215
+ const wrapRef = (0, react.useRef)(null);
216
+ const popupRef = (0, react.useRef)(null);
217
+ const currentIndex = () => {
218
+ const index = options.findIndex((option) => option.value === value);
219
+ return index >= 0 ? index : 0;
220
+ };
221
+ const close = (0, react.useCallback)(() => {
222
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
223
+ setClosing(true);
224
+ closeTimer.current = setTimeout(() => {
225
+ setClosing(false);
226
+ setOpen(false);
227
+ }, SELECT_CLOSE_MS);
228
+ }, []);
229
+ const openPopup = () => {
230
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
231
+ setActiveIndex(currentIndex());
232
+ setPhase("initial");
233
+ setClosing(false);
234
+ setOpen(true);
235
+ };
236
+ const commit = (index) => {
237
+ const option = options[index];
238
+ if (option) props.onEdit(option.value);
239
+ close();
240
+ };
241
+ const onTriggerClick = () => {
242
+ if (props.disabled) return;
243
+ if (open && !closing) close();
244
+ else openPopup();
245
+ };
246
+ const onKeyDown = (event) => {
247
+ if (props.disabled) return;
248
+ const count = options.length;
249
+ switch (event.key) {
250
+ case "ArrowDown":
251
+ case "ArrowUp":
252
+ case "Enter":
253
+ case " ":
254
+ event.preventDefault();
255
+ if (!open) openPopup();
256
+ else if (!closing) if (event.key === "ArrowDown") setActiveIndex((index) => (index + 1) % count);
257
+ else if (event.key === "ArrowUp") setActiveIndex((index) => (index - 1 + count) % count);
258
+ else commit(activeIndex);
259
+ break;
260
+ case "Escape":
261
+ if (open) {
262
+ event.preventDefault();
263
+ event.stopPropagation();
264
+ close();
265
+ }
266
+ break;
267
+ case "Tab":
268
+ if (open) close();
269
+ break;
270
+ }
271
+ };
272
+ (0, react.useEffect)(() => () => {
273
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
274
+ }, []);
275
+ (0, react.useLayoutEffect)(() => {
276
+ if (open && !closing && phase === "initial") {
277
+ popupRef.current?.offsetHeight;
278
+ setPhase("open");
279
+ }
280
+ }, [
281
+ open,
282
+ closing,
283
+ phase
284
+ ]);
285
+ (0, react.useEffect)(() => {
286
+ if (!open) return;
287
+ const onPointerDown = (event) => {
288
+ const target = event.target;
289
+ if (target instanceof Node && !wrapRef.current?.contains(target)) close();
290
+ };
291
+ document.addEventListener("pointerdown", onPointerDown);
292
+ return () => document.removeEventListener("pointerdown", onPointerDown);
293
+ }, [open, close]);
294
+ (0, react.useEffect)(() => {
295
+ if (props.disabled && open) close();
296
+ }, [
297
+ props.disabled,
298
+ open,
299
+ close
300
+ ]);
301
+ if (isSkinActive()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
302
+ id,
303
+ className: settings_card_module_css_default.select,
304
+ value,
305
+ disabled: props.disabled,
306
+ onChange: (event) => {
307
+ props.onEdit(event.target.value);
308
+ },
309
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
310
+ value: option.value,
311
+ children: option.label
312
+ }, option.value))
313
+ });
314
+ const label = options.find((option) => option.value === value)?.label ?? "";
315
+ const popupClass = closing ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupClose}` : phase === "open" ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupOpen}` : settings_card_module_css_default.selectPopup;
316
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
317
+ className: settings_card_module_css_default.selectWrap,
318
+ ref: wrapRef,
319
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
320
+ type: "button",
321
+ id,
322
+ className: `${settings_card_module_css_default.select} ${settings_card_module_css_default.selectButton}`,
323
+ disabled: props.disabled,
324
+ "aria-haspopup": "listbox",
325
+ "aria-expanded": open,
326
+ "aria-activedescendant": open ? `${id}-o${activeIndex}` : void 0,
327
+ "aria-invalid": props.invalid || void 0,
328
+ onClick: onTriggerClick,
329
+ onKeyDown,
330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
331
+ className: settings_card_module_css_default.selectLabel,
332
+ children: label
333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
334
+ width: "14",
335
+ height: "14",
336
+ viewBox: "0 0 14 14",
337
+ fill: "none",
338
+ xmlns: "http://www.w3.org/2000/svg",
339
+ className: open ? `${settings_card_module_css_default.selectChevron} ${settings_card_module_css_default.selectChevronOpen}` : settings_card_module_css_default.selectChevron,
340
+ "aria-hidden": "true",
341
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
342
+ d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
343
+ fill: "currentColor"
344
+ })
345
+ })]
346
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
347
+ className: popupClass,
348
+ role: "listbox",
349
+ ref: popupRef,
350
+ children: options.map((option, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
351
+ id: `${id}-o${index}`,
352
+ role: "option",
353
+ "aria-selected": option.value === value,
354
+ className: `${settings_card_module_css_default.selectOption}${option.value === value ? ` ${settings_card_module_css_default.selectOptionSelected}` : ""}${index === activeIndex && !closing ? ` ${settings_card_module_css_default.selectOptionActive}` : ""}`,
355
+ onClick: () => {
356
+ commit(index);
357
+ },
358
+ children: option.label
359
+ }, option.value))
360
+ }) : null]
361
+ });
362
+ }
182
363
  /** A staged boolean field: 继承 / 开 / 关. */
183
364
  function BooleanField(props) {
184
365
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -204,28 +385,26 @@ window.__ModuleLoader__.load({
204
385
  })]
205
386
  }) : null]
206
387
  }),
207
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
388
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
208
389
  id: props.id,
209
- className: settings_card_module_css_default.select,
210
- value: props.text,
211
- disabled: props.disabled,
212
- onChange: (event) => {
213
- props.onEdit(event.target.value);
214
- },
215
- children: [
216
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
390
+ options: [
391
+ {
217
392
  value: "",
218
- children: props.inheritLabel
219
- }),
220
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
393
+ label: props.inheritLabel
394
+ },
395
+ {
221
396
  value: "true",
222
- children: props.onLabel
223
- }),
224
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
397
+ label: props.onLabel
398
+ },
399
+ {
225
400
  value: "false",
226
- children: props.offLabel
227
- })
228
- ]
401
+ label: props.offLabel
402
+ }
403
+ ],
404
+ value: props.text,
405
+ disabled: props.disabled,
406
+ invalid: props.invalid,
407
+ onEdit: props.onEdit
229
408
  }),
230
409
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
231
410
  className: settings_card_module_css_default.hint,
@@ -548,7 +727,6 @@ window.__ModuleLoader__.load({
548
727
  "repo": "https://github.com/v833/dsh-chat-summary",
549
728
  "description": "总结当前对话并导出为 Markdown / DOCX / PDF,可选 LLM 智能总结(用户自配 API Key)。",
550
729
  "descriptionEn": "Summarize the current conversation and export it as Markdown / DOCX / PDF, with optional LLM summarization using your own API key.",
551
- "npm": "@linxin666/dsh-client-ui-chat-summary",
552
730
  "category": "knowledge"
553
731
  },
554
732
  {
@@ -604,16 +782,6 @@ window.__ModuleLoader__.load({
604
782
  "npm": "dsh-mnemon",
605
783
  "category": "knowledge"
606
784
  },
607
- {
608
- "id": "dsh-plugin-hub",
609
- "name": "插件中心(dsh-plugin-hub)",
610
- "nameEn": "Plugin Hub",
611
- "author": "Noob-stupid",
612
- "repo": "https://github.com/Noob-stupid/dsh-plugin-hub",
613
- "description": "插件管理面板:已安装插件一键启用/停用,内置 GitHub dsh-plugin 插件市场(官方/聚合识别、子包浏览、一键安装与本地 AI 兜底修复、删除卸载)。",
614
- "descriptionEn": "Plugin management panel: one-click enable/disable, a GitHub dsh-plugin marketplace with official/aggregate detection, subpackage browsing, one-click installs with local-AI fallback repair, and uninstall.",
615
- "category": "tools"
616
- },
617
785
  {
618
786
  "id": "dsh-genui",
619
787
  "name": "GenUI 生成式 UI",
@@ -675,6 +843,7 @@ window.__ModuleLoader__.load({
675
843
  "repo": "https://github.com/Qinling-Melon-Farmers/dsh-memoir",
676
844
  "description": "项目持久化记忆与会话经验沉淀:memoir_record / memoir_read 工具(BM25 相关度排序检索)、每轮工作自动蒸馏、记忆诊断面板与跨项目全局检索,纯本地零外部依赖。",
677
845
  "descriptionEn": "Project persistent memory and session-lessons distillation: memoir_record / memoir_read tools with BM25-ranked retrieval, per-turn auto-distill, a memory diagnostics panel and cross-project search - fully local, no external dependency.",
846
+ "npm": "dsh-memoir",
678
847
  "category": "knowledge"
679
848
  },
680
849
  {
@@ -700,14 +869,162 @@ window.__ModuleLoader__.load({
700
869
  "category": "tools"
701
870
  },
702
871
  {
703
- "id": "dsh-dream-skin",
704
- "name": "dsh-dream-skin",
705
- "nameEn": "dsh-dream-skin",
706
- "author": "RevolutionLA",
707
- "repo": "https://github.com/RevolutionLA/dsh-dream-skin",
708
- "description": "DSH Web 一键换肤插件:8 套原创主题、背景壁纸(透明度/模糊/渐变/URL)、每用户强调色、主题包导入导出与分享链接、收藏与随机,纯原生 token 系统。",
709
- "descriptionEn": "One-command skinning for DSH Web: 8 original themes, wallpaper (opacity/blur/gradient/URL), per-user accent, shareable theme packs with favorites and surprise-me. Purely native token system.",
872
+ "id": "dsh-friendly-steps",
873
+ "name": "过程精简 Friendly Steps",
874
+ "nameEn": "Friendly Steps",
875
+ "author": "dongwenxiu83-web",
876
+ "repo": "https://github.com/dongwenxiu83-web/dsh-friendly-steps",
877
+ "description": "面向文字工作者的过程精简:纯 CSS 折叠 Think / 工具调用等技术过程行,右下角浮条「已完成 N 步」一键展开收起,失败步骤红色计数反馈;不注入 React 列表、无全量扫描,零侵入。",
878
+ "descriptionEn": "Writer-friendly process collapse: hides Think / tool-call rows via pure CSS behind a floating 'N steps done' pill with red failure counts and one-click expand/collapse. No foreign nodes in React lists, no full-DOM scans — zero interference.",
879
+ "category": "ui"
880
+ },
881
+ {
882
+ "id": "dsh-full-remote",
883
+ "name": "全功能远程访问",
884
+ "nameEn": "Full Remote Access",
885
+ "author": "JUANWANG-BUAA",
886
+ "repo": "https://github.com/JUANWANG-BUAA/dsh-full-remote",
887
+ "description": "令牌门控反向代理:公网隧道或局域网下,在手机等设备上远程使用 DeepSeek Harness,设置、凭据与文件访问保持可用,支持按设备会话。",
888
+ "descriptionEn": "Token-gated reverse proxy for remote access: use DeepSeek Harness over public tunnels or LAN from phones and other devices, with settings, credentials and file access kept working and per-device sessions.",
889
+ "npm": "dsh-full-remote",
890
+ "category": "integration"
891
+ },
892
+ {
893
+ "id": "dsh-palate",
894
+ "name": "Palate 品味训练",
895
+ "nameEn": "Palate Design Taste",
896
+ "author": "guo6x",
897
+ "repo": "https://github.com/guo6x/dsh-palate",
898
+ "description": "会长大的眼:可积累的设计品味语料库(好/坏例子 + 编码原则),喂得越多评审越准;6 个 palate_* 工具(review/add/learn/list/principles/stats)+ 成长面板,node:sqlite + 人可读 Markdown 镜像,零运行时依赖。",
899
+ "descriptionEn": "An eye that grows: an accumulated design-taste corpus (good/bad examples plus codified principles) that sharpens with every example you feed; six palate_* tools (review/add/learn/list/principles/stats) plus a growth panel, node:sqlite with human-readable Markdown mirrors, zero runtime dependencies.",
900
+ "category": "knowledge"
901
+ },
902
+ {
903
+ "id": "dsh-archive-manager",
904
+ "name": "归档管理",
905
+ "nameEn": "Archive Manager",
906
+ "author": "z953218350",
907
+ "repo": "https://github.com/z953218350/dsh-archive-manager",
908
+ "description": "设置页 Codex 风格会话归档管理:按项目分组、搜索筛选、预览对话内容、一键恢复与彻底物理删除,内置 zstd 解压引擎。",
909
+ "descriptionEn": "Codex-style archived session manager in settings: group by project, search and filter, preview conversation history, restore to sidebar, and permanently delete from disk; built-in zstd decompression.",
910
+ "npm": "@mlgbnb/dsh-archive-manager",
911
+ "category": "utility"
912
+ },
913
+ {
914
+ "id": "dsh-history-tree",
915
+ "name": "历史树",
916
+ "nameEn": "History Tree",
917
+ "author": "z953218350",
918
+ "repo": "https://github.com/z953218350/dsh-history-tree",
919
+ "description": "在聊天消息左侧提供 Codex 风格对话轮次时间线点阵与悬浮历史概览卡片,支持鱼眼放大动效与点击直达对应轮次。",
920
+ "descriptionEn": "Codex-style conversation turn timeline rail and hover overview cards beside chat messages, with fish-eye magnification and click-to-jump navigation.",
921
+ "npm": "dsh-history-tree",
922
+ "category": "ui"
923
+ },
924
+ {
925
+ "id": "dsh-gzip",
926
+ "name": "dsh-gzip",
927
+ "nameEn": "dsh-gzip",
928
+ "author": "wingsky-1",
929
+ "repo": "https://github.com/wingsky-1/dsh-plugin-hub/tree/main/packages/dsh-gzip",
930
+ "description": "/api 响应 gzip 压缩插件:为远程 / 低带宽链路开启 /api 响应压缩,解决会话历史因大响应无压缩而超时加载失败的问题;SSE / zip / 已编码响应自动豁免,无全局副作用。",
931
+ "descriptionEn": "API response gzip plugin for DSH Web GUI: enables gzip on /api responses for remote / low-bandwidth links, fixing session-history loads that fail on the timeout of large uncompressed responses; SSE, zip and already-encoded responses are passed through untouched, with no global side effects.",
932
+ "npm": "@wingsky-1/dsh-gzip"
933
+ },
934
+ {
935
+ "id": "dsh-wsl-bridge",
936
+ "name": "WSL Windows 桥",
937
+ "nameEn": "WSL Windows Bridge",
938
+ "author": "ch1bug",
939
+ "repo": "https://github.com/ch1bug/dsh-wsl-bridge",
940
+ "description": "让 WSL 里的 agent 访问 Windows 侧:读写 C:\\ 文件、运行 .exe、Explorer 打开、wslpath 路径互转与盘符枚举(win_ls/read/write/run/open/path/drives)。",
941
+ "descriptionEn": "Lets a WSL agent reach the Windows host: read/write C:\\ files, run .exe programs, open in Explorer, convert paths via wslpath and list drives (win_ls/read/write/run/open/path/drives).",
942
+ "category": "integration"
943
+ },
944
+ {
945
+ "id": "dsh-mimo-agent-tools",
946
+ "name": "MiMo 多模态工具",
947
+ "nameEn": "MiMo Multimodal Tools",
948
+ "author": "ch1bug",
949
+ "repo": "https://github.com/ch1bug/dsh-mimo-agent-tools",
950
+ "description": "把小米 MiMo API 封装为 agent 工具:联网搜索、图像/音频/视频理解、语音转写、语音合成与音色克隆(mimo_search/vision/audio/video/asr/tts/voiceclone)。",
951
+ "descriptionEn": "Turns the Xiaomi MiMo API into agent tools: web search, image/audio/video understanding, speech-to-text, text-to-speech and voice cloning (mimo_search/vision/audio/video/asr/tts/voiceclone).",
952
+ "category": "tools"
953
+ },
954
+ {
955
+ "id": "dsh-skill-fuzzy",
956
+ "name": "技能模糊搜索",
957
+ "nameEn": "Skill Fuzzy Search",
958
+ "author": "ch1bug",
959
+ "repo": "https://github.com/ch1bug/dsh-skill-fuzzy",
960
+ "description": "Codex 风格模糊技能搜索:/bug、/diag、/regression 都能在 / 技能菜单里命中 diagnosing-bugs(名称子序列 + 描述连续子串匹配)。",
961
+ "descriptionEn": "Codex-style fuzzy skill search: /bug, /diag and /regression all find diagnosing-bugs in the / skill menu (name subsequence + description substring matching).",
710
962
  "category": "ui"
963
+ },
964
+ {
965
+ "id": "dsh-secret-redactor",
966
+ "name": "敏感信息脱敏",
967
+ "nameEn": "Secret Redactor",
968
+ "author": "DamonKoy",
969
+ "repo": "https://github.com/DamonKoy/dsh-plugins/tree/main/packages/dsh-secret-redactor",
970
+ "description": "自动掩码工具输出中的 API key、token、JWT、私钥与配置密钥。",
971
+ "descriptionEn": "Auto-masks API keys, tokens, JWTs and private keys in tool results shown to the model."
972
+ },
973
+ {
974
+ "id": "dsh-approve-for-me",
975
+ "name": "自动审批审核",
976
+ "nameEn": "Approve for Me",
977
+ "author": "DamonKoy",
978
+ "repo": "https://github.com/DamonKoy/dsh-plugins/tree/main/packages/dsh-approve-for-me",
979
+ "description": "只读工具自动放行、危险命令自动拒绝,可配置全自动模式。",
980
+ "descriptionEn": "Auto-approves read-only tools, auto-denies dangerous commands, optional full-auto mode."
981
+ },
982
+ {
983
+ "id": "dsh-mcp-client-v2",
984
+ "name": "MCP 客户端 v2",
985
+ "nameEn": "MCP Client v2",
986
+ "author": "DamonKoy",
987
+ "repo": "https://github.com/DamonKoy/dsh-plugins/tree/main/packages/dsh-mcp-client-v2",
988
+ "description": "MCP 客户端增强:分页工具发现、非阻塞启动、工具搜索。",
989
+ "descriptionEn": "Enhanced MCP client: paginated tool discovery, non-blocking startup, tool search."
990
+ },
991
+ {
992
+ "id": "dsh-memories",
993
+ "name": "项目记忆",
994
+ "nameEn": "Project Memories",
995
+ "author": "DamonKoy",
996
+ "repo": "https://github.com/DamonKoy/dsh-plugins/tree/main/packages/dsh-memories",
997
+ "description": "按项目作用域持久化的键值记忆,支持增删查列与搜索。",
998
+ "descriptionEn": "Project-scoped persistent key-value memories with set/get/list/delete/search."
999
+ },
1000
+ {
1001
+ "id": "dsh-system-proxy",
1002
+ "name": "系统代理",
1003
+ "nameEn": "System Proxy",
1004
+ "author": "DamonKoy",
1005
+ "repo": "https://github.com/DamonKoy/dsh-plugins/tree/main/packages/dsh-system-proxy",
1006
+ "description": "检测系统代理(scutil/环境变量/PAC)并为子进程导出代理环境变量。",
1007
+ "descriptionEn": "Detects system proxy (scutil/env/PAC) and exports proxy env for child processes."
1008
+ },
1009
+ {
1010
+ "id": "dsh-usage-cost",
1011
+ "name": "用量成本提醒",
1012
+ "nameEn": "Usage & Cost",
1013
+ "author": "DamonKoy",
1014
+ "repo": "https://github.com/DamonKoy/dsh-plugins/tree/main/packages/dsh-usage-cost",
1015
+ "description": "统计会话与每日 token 用量和估算成本,超预算提醒。",
1016
+ "descriptionEn": "Tracks token usage and estimated cost per session/day with budget alerts."
1017
+ },
1018
+ {
1019
+ "id": "dsh-notifier",
1020
+ "name": "dsh-notifier",
1021
+ "nameEn": "dsh-notifier",
1022
+ "author": "wingsky-1",
1023
+ "repo": "https://github.com/wingsky-1/dsh-plugin-hub/tree/main/packages/dsh-notifier",
1024
+ "description": "审批/完成/错误事件通知:浏览器 Notification + 系统原生 toast(Windows PowerShell WinRT / Linux·macOS notify-send,零依赖);提示音可配、错误同类合并、免打扰时段、通知历史与配置面板;非安全上下文自动降级横幅。",
1025
+ "descriptionEn": "Approval / completion / error event notifications: browser Notification API plus native OS toasts (Windows PowerShell WinRT, Linux and macOS notify-send, zero dependencies); configurable sound, error merging, quiet hours, notification history and a settings panel; falls back to in-page banners outside secure contexts.",
1026
+ "npm": "@wingsky-1/dsh-notifier",
1027
+ "category": "utility"
711
1028
  }
712
1029
  ];
713
1030
  //#endregion
@@ -742,8 +1059,106 @@ window.__ModuleLoader__.load({
742
1059
  return true;
743
1060
  }
744
1061
  //#endregion
1062
+ //#region src/client/plugin-manager-bridge.ts
1063
+ let snapshot = {
1064
+ face: null,
1065
+ version: 0
1066
+ };
1067
+ const listeners = /* @__PURE__ */ new Set();
1068
+ /** Replace the held face and notify subscribers. */
1069
+ function setFace(face) {
1070
+ snapshot = {
1071
+ face,
1072
+ version: snapshot.version + 1
1073
+ };
1074
+ for (const listener of listeners) listener();
1075
+ }
1076
+ /** Current bridge snapshot (cached reference, safe for useSyncExternalStore). */
1077
+ function getPluginManagerSnapshot() {
1078
+ return snapshot;
1079
+ }
1080
+ /** Subscribe to face changes; returns the unsubscribe function. */
1081
+ function subscribePluginManager(listener) {
1082
+ listeners.add(listener);
1083
+ return () => {
1084
+ listeners.delete(listener);
1085
+ };
1086
+ }
1087
+ /**
1088
+ * Bridge the optional 'pluginManager' service into the module store. Uses
1089
+ * ctx.inject (NOT the plugin's module-level inject array) so the service
1090
+ * stays optional: the inner callback runs when the sibling plugin provides
1091
+ * the face and is disposed when it goes away, which clears the store.
1092
+ * @param ctx - the client root context.
1093
+ */
1094
+ function bridgePluginManager(ctx) {
1095
+ ctx.inject(["pluginManager"], (inner) => {
1096
+ inner.effect(() => {
1097
+ setFace(inner.pluginManager ?? null);
1098
+ return () => {
1099
+ setFace(null);
1100
+ };
1101
+ }, "community-plugins: pluginManager bridge");
1102
+ });
1103
+ }
1104
+ //#endregion
1105
+ //#region src/client/install-source.ts
1106
+ /**
1107
+ * The spec handed to pluginManager.install(): the npm package name when the
1108
+ * entry is published there, else the repository URL (git install).
1109
+ * @param entry - the community index entry.
1110
+ * @returns the install spec.
1111
+ */
1112
+ function installSpec(entry) {
1113
+ return entry.npm ?? entry.repo;
1114
+ }
1115
+ /**
1116
+ * Normalize a git remote for comparison: lowercase, drop the transport
1117
+ * prefix ('git+' wrapper, 'https://', 'http://', 'git://', 'ssh://git@', the
1118
+ * scp-style 'git@host:' userinfo, and the pnpm host shorthands 'github:' /
1119
+ * 'gitlab:' / 'bitbucket:' that package.json records for shorthand installs),
1120
+ * then strip trailing slashes and one trailing '.git'
1121
+ * suffix (repeated, so 'x/.git/' and 'x.git' converge). Conservative on
1122
+ * purpose: no path rewriting beyond those steps, so distinct repositories
1123
+ * never collapse onto each other.
1124
+ * @param url - the repository URL as recorded by either side.
1125
+ * @returns the comparison key.
1126
+ */
1127
+ function normalizeGitUrl(url) {
1128
+ let key = url.trim().toLowerCase();
1129
+ key = key.replace(/^git[+]/, "");
1130
+ key = key.replace(/^(github|gitlab):/, "$1.com/");
1131
+ key = key.replace(/^bitbucket:/, "bitbucket.org/");
1132
+ key = key.replace(/^(?:https?|git):[/][/]/, "");
1133
+ key = key.replace(/^ssh:[/][/]git@/, "");
1134
+ key = key.replace(/^git@([^/:]+):/, "$1/");
1135
+ for (;;) {
1136
+ const next = key.replace(/[/]+$/, "").replace(/[.]git$/, "");
1137
+ if (next === key) return key;
1138
+ key = next;
1139
+ }
1140
+ }
1141
+ /**
1142
+ * Match one community entry against the installed snapshot. npm-published
1143
+ * entries match on the installed row's package identity (id/name): the npm
1144
+ * source spec records the dependency RANGE ('^0.3.2') or an install-time
1145
+ * spec, not necessarily the bare package name, so it is only a fallback.
1146
+ * Repository entries match on the normalized git URL. An entry with an npm
1147
+ * field ONLY matches npm installs — a same-repository git install is a
1148
+ * different source and stays unmatched (the card then offers install, which
1149
+ * the host reconciles).
1150
+ * @param entry - the community index entry.
1151
+ * @param installed - the installed snapshot from the pluginManager service.
1152
+ * @returns the matching installed row, or null.
1153
+ */
1154
+ function entryInstalled(entry, installed) {
1155
+ if (entry.npm !== void 0) return installed.find((item) => item.source.kind === "npm" && (item.id === entry.npm || item.name === entry.npm || item.source.spec === entry.npm)) ?? null;
1156
+ const key = normalizeGitUrl(entry.repo);
1157
+ return installed.find((item) => item.source.kind === "git" && normalizeGitUrl(item.source.spec) === key) ?? null;
1158
+ }
1159
+ //#endregion
745
1160
  //#region \0dsh-css:packages/dsh-community-plugins/src/client/community.module.css.mjs
746
- const css = ".z0J_8a_sectionList{margin:0;padding:0;list-style:none}.z0J_8a_market{flex-direction:column;gap:10px;margin-top:12px;display:flex}.z0J_8a_toolbar{display:flex}.z0J_8a_search{min-width:0;font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;flex:1;padding:6px 10px;font-size:13px;line-height:1.5}.z0J_8a_search::placeholder{color:var(--dsw-alias-label-tertiary)}.z0J_8a_search:focus{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.z0J_8a_search:disabled{opacity:.5}.z0J_8a_filters{flex-wrap:wrap;gap:6px;display:flex}.z0J_8a_pill{font:inherit;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;border-radius:999px;padding:2px 10px;font-size:12px;line-height:1.6}.z0J_8a_pill:hover:enabled{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.z0J_8a_pill:disabled{opacity:.5;cursor:default}.z0J_8a_pillActive{color:var(--dsw-alias-bg-layer-3);background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);font-weight:600}.z0J_8a_pillActive:hover:enabled{color:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-primary)}.z0J_8a_resultCount{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.4}.z0J_8a_grid{grid-template-columns:repeat(auto-fill,minmax(256px,1fr));gap:8px;margin:0;padding:0;list-style:none;display:grid}.z0J_8a_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:6px;min-width:0;padding:12px;display:flex}.z0J_8a_card:hover{border-color:var(--dsw-alias-label-dimmed)}.z0J_8a_cardHead{justify-content:space-between;align-items:baseline;gap:8px;min-width:0;display:flex}.z0J_8a_cardName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.z0J_8a_badge{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap;border-radius:999px;flex:none;padding:0 8px;font-size:11px;line-height:1.6}.z0J_8a_badgePublished{color:var(--dsw-alias-link-primary,var(--dsw-alias-button-info-fill));border-color:var(--dsw-alias-link-primary,var(--dsw-alias-button-info-fill))}.z0J_8a_cardMeta{min-width:0;color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;margin:0;font-size:12px;line-height:1.4;display:flex}.z0J_8a_cardCategory{color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none}.z0J_8a_cardDot{flex:none}.z0J_8a_cardAuthor{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.z0J_8a_cardDescription{color:var(--dsw-alias-label-secondary);-webkit-line-clamp:2;overflow-wrap:anywhere;-webkit-box-orient:vertical;margin:0;font-size:13px;line-height:1.45;display:-webkit-box;overflow:hidden}.z0J_8a_cardFooter{flex-direction:column;gap:6px;margin-top:auto;padding-top:6px;display:flex}.z0J_8a_cardTop{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.z0J_8a_cardLink{color:var(--dsw-alias-link-primary,var(--dsw-alias-button-info-fill));font-size:13px;text-decoration:none}.z0J_8a_cardLink:hover{text-decoration:underline}.z0J_8a_cardCommand{box-sizing:border-box;width:100%;min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap;text-overflow:ellipsis;border-radius:4px;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;overflow:hidden}.z0J_8a_installButton{font:inherit;color:var(--dsw-alias-bg-layer-3);background:var(--dsw-alias-button-info-fill);border:1px solid var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border-radius:6px;flex:none;padding:4px 12px;font-size:12px;font-weight:600;line-height:1.5}.z0J_8a_installButton:hover:enabled{opacity:.88}.z0J_8a_installButton:disabled{opacity:.5;cursor:default}.z0J_8a_installButtonCopied{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed);background:0 0}.z0J_8a_installNote{color:var(--dsw-alias-label-tertiary);margin:8px 2px 0;font-size:12px;line-height:1.4}.z0J_8a_empty{color:var(--dsw-alias-label-tertiary);margin:0;padding:8px 2px;font-size:13px}.z0J_8a_off{color:var(--dsw-alias-label-tertiary);margin:8px 2px 0;font-size:13px}.z0J_8a_notice{color:var(--dsw-alias-label-tertiary);margin:10px 2px 0;font-size:12px;line-height:1.4}";
1161
+ const css = ".z0J_8a_sectionList{margin:0;padding:0;list-style:none}.z0J_8a_market{flex-direction:column;gap:10px;margin-top:12px;display:flex}.z0J_8a_toolbar{display:flex}.z0J_8a_search{min-width:0;font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;flex:1;padding:6px 10px;font-size:13px;line-height:1.5}.z0J_8a_search::placeholder{color:var(--dsw-alias-label-tertiary)}.z0J_8a_search:focus{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.z0J_8a_search:disabled{opacity:.5}.z0J_8a_filters{flex-wrap:wrap;gap:6px;display:flex}.z0J_8a_pill{font:inherit;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;border-radius:999px;padding:2px 10px;font-size:12px;line-height:1.6}.z0J_8a_pill:hover:enabled{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.z0J_8a_pill:disabled{opacity:.5;cursor:default}.z0J_8a_pillActive{color:var(--dsw-alias-bg-layer-3);background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);font-weight:600}.z0J_8a_pillActive:hover:enabled{color:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-primary)}.z0J_8a_resultCount{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.4}.z0J_8a_grid{grid-template-columns:repeat(auto-fill,minmax(256px,1fr));gap:8px;margin:0;padding:0;list-style:none;display:grid}.z0J_8a_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:6px;min-width:0;padding:12px;display:flex}.z0J_8a_card:hover{border-color:var(--dsw-alias-label-dimmed)}.z0J_8a_cardHead{justify-content:space-between;align-items:baseline;gap:8px;min-width:0;display:flex}.z0J_8a_cardBadges{flex:none;align-items:center;gap:4px;min-width:0;display:flex}.z0J_8a_cardName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.z0J_8a_badge{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap;border-radius:999px;flex:none;padding:0 8px;font-size:11px;line-height:1.6}.z0J_8a_badgePublished{color:var(--dsw-alias-link-primary,var(--dsw-alias-button-info-fill));border-color:var(--dsw-alias-link-primary,var(--dsw-alias-button-info-fill))}.z0J_8a_badgeInstalled{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.z0J_8a_cardMeta{min-width:0;color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;margin:0;font-size:12px;line-height:1.4;display:flex}.z0J_8a_cardCategory{color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none}.z0J_8a_cardDot{flex:none}.z0J_8a_cardAuthor{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.z0J_8a_cardDescription{color:var(--dsw-alias-label-secondary);-webkit-line-clamp:2;overflow-wrap:anywhere;-webkit-box-orient:vertical;margin:0;font-size:13px;line-height:1.45;display:-webkit-box;overflow:hidden}.z0J_8a_cardFooter{flex-direction:column;gap:6px;margin-top:auto;padding-top:6px;display:flex}.z0J_8a_cardTop{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.z0J_8a_cardLink{color:var(--dsw-alias-link-primary,var(--dsw-alias-button-info-fill));font-size:13px;text-decoration:none}.z0J_8a_cardLink:hover{text-decoration:underline}.z0J_8a_cardCommand{box-sizing:border-box;width:100%;min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap;text-overflow:ellipsis;border-radius:4px;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;overflow:hidden}.z0J_8a_installButton{font:inherit;color:var(--dsw-alias-bg-layer-3);background:var(--dsw-alias-button-info-fill);border:1px solid var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border-radius:6px;flex:none;padding:4px 12px;font-size:12px;font-weight:600;line-height:1.5}.z0J_8a_installButton:hover:enabled{opacity:.88}.z0J_8a_installButton:disabled{opacity:.5;cursor:default}.z0J_8a_installButtonCopied{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed);background:0 0}.z0J_8a_installButtonSecondary{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);background:0 0}.z0J_8a_installButtonSecondary:hover:enabled{border-color:var(--dsw-alias-label-dimmed)}.z0J_8a_cardActions{flex:none;align-items:center;gap:6px;display:flex}.z0J_8a_progress{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.4}.z0J_8a_error{color:var(--dsw-alias-label-error,#c53030);overflow-wrap:anywhere;margin:0;font-size:12px;line-height:1.4}.z0J_8a_confirmBody{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.5}.z0J_8a_modalActions{justify-content:flex-end;gap:8px;margin-top:16px;display:flex}.z0J_8a_dangerButton{background:var(--dsw-alias-label-error,#c53030);border-color:var(--dsw-alias-label-error,#c53030)}.z0J_8a_installNote{color:var(--dsw-alias-label-tertiary);margin:8px 2px 0;font-size:12px;line-height:1.4}.z0J_8a_empty{color:var(--dsw-alias-label-tertiary);margin:0;padding:8px 2px;font-size:13px}.z0J_8a_off{color:var(--dsw-alias-label-tertiary);margin:8px 2px 0;font-size:13px}.z0J_8a_notice{color:var(--dsw-alias-label-tertiary);margin:10px 2px 0;font-size:12px;line-height:1.4}";
747
1162
  const tagId = "@linxin666/dsh-client-ui-community-plugins/community.module.css";
748
1163
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
749
1164
  const tag = document.createElement("style");
@@ -754,9 +1169,12 @@ window.__ModuleLoader__.load({
754
1169
  }
755
1170
  var community_module_css_default = {
756
1171
  "badge": "z0J_8a_badge",
1172
+ "badgeInstalled": "z0J_8a_badgeInstalled",
757
1173
  "badgePublished": "z0J_8a_badgePublished",
758
1174
  "card": "z0J_8a_card",
1175
+ "cardActions": "z0J_8a_cardActions",
759
1176
  "cardAuthor": "z0J_8a_cardAuthor",
1177
+ "cardBadges": "z0J_8a_cardBadges",
760
1178
  "cardCategory": "z0J_8a_cardCategory",
761
1179
  "cardCommand": "z0J_8a_cardCommand",
762
1180
  "cardDescription": "z0J_8a_cardDescription",
@@ -767,17 +1185,23 @@ window.__ModuleLoader__.load({
767
1185
  "cardMeta": "z0J_8a_cardMeta",
768
1186
  "cardName": "z0J_8a_cardName",
769
1187
  "cardTop": "z0J_8a_cardTop",
1188
+ "confirmBody": "z0J_8a_confirmBody",
1189
+ "dangerButton": "z0J_8a_dangerButton",
770
1190
  "empty": "z0J_8a_empty",
1191
+ "error": "z0J_8a_error",
771
1192
  "filters": "z0J_8a_filters",
772
1193
  "grid": "z0J_8a_grid",
773
1194
  "installButton": "z0J_8a_installButton",
774
1195
  "installButtonCopied": "z0J_8a_installButtonCopied",
1196
+ "installButtonSecondary": "z0J_8a_installButtonSecondary",
775
1197
  "installNote": "z0J_8a_installNote",
776
1198
  "market": "z0J_8a_market",
1199
+ "modalActions": "z0J_8a_modalActions",
777
1200
  "notice": "z0J_8a_notice",
778
1201
  "off": "z0J_8a_off",
779
1202
  "pill": "z0J_8a_pill",
780
1203
  "pillActive": "z0J_8a_pillActive",
1204
+ "progress": "z0J_8a_progress",
781
1205
  "resultCount": "z0J_8a_resultCount",
782
1206
  "search": "z0J_8a_search",
783
1207
  "sectionList": "z0J_8a_sectionList",
@@ -830,6 +1254,12 @@ window.__ModuleLoader__.load({
830
1254
  function installCommand(entry) {
831
1255
  return `dsh plugin --profile web add ${entry.npm ?? entry.repo}`;
832
1256
  }
1257
+ /** Progress polling cadence while an install is in flight. */
1258
+ const PROGRESS_POLL_MS = 500;
1259
+ /** Extract a displayable reason from an install/uninstall rejection. */
1260
+ function messageOf(reason) {
1261
+ return reason instanceof Error ? reason.message : String(reason);
1262
+ }
833
1263
  /**
834
1264
  * Localized entry copy read through the card's `t`. Falls back to the raw
835
1265
  * entry field when the key is missing — the dictionary only carries the
@@ -862,6 +1292,102 @@ window.__ModuleLoader__.load({
862
1292
  const [query, setQuery] = (0, react.useState)("");
863
1293
  const [category, setCategory] = (0, react.useState)(null);
864
1294
  const [copiedId, setCopiedId] = (0, react.useState)(null);
1295
+ const bridge = (0, react.useSyncExternalStore)(subscribePluginManager, getPluginManagerSnapshot);
1296
+ const face = props.pluginManager !== void 0 ? props.pluginManager : bridge.face;
1297
+ const faceLoopback = face !== null && face.isLoopback;
1298
+ const [installed, setInstalled] = (0, react.useState)(null);
1299
+ const [pending, setPending] = (0, react.useState)(null);
1300
+ const [progress, setProgress] = (0, react.useState)(null);
1301
+ const [errors, setErrors] = (0, react.useState)({});
1302
+ const [uninstallTarget, setUninstallTarget] = (0, react.useState)(null);
1303
+ (0, react.useEffect)(() => {
1304
+ if (face === null || !face.isLoopback) {
1305
+ setInstalled(null);
1306
+ return;
1307
+ }
1308
+ let alive = true;
1309
+ const refresh = () => {
1310
+ face.list().then((list) => {
1311
+ if (alive) setInstalled(list);
1312
+ }, () => {});
1313
+ };
1314
+ refresh();
1315
+ const unsubscribe = face.onChange(refresh);
1316
+ return () => {
1317
+ alive = false;
1318
+ unsubscribe();
1319
+ };
1320
+ }, [face, faceLoopback]);
1321
+ (0, react.useEffect)(() => {
1322
+ if (face === null || pending?.kind !== "install") {
1323
+ setProgress(null);
1324
+ return;
1325
+ }
1326
+ let alive = true;
1327
+ const poll = () => {
1328
+ face.status().then((item) => {
1329
+ if (alive) setProgress(item);
1330
+ }, () => {});
1331
+ };
1332
+ poll();
1333
+ const timer = setInterval(poll, PROGRESS_POLL_MS);
1334
+ return () => {
1335
+ alive = false;
1336
+ clearInterval(timer);
1337
+ };
1338
+ }, [face, pending?.kind === "install"]);
1339
+ const clearError = (id) => {
1340
+ setErrors((previous) => {
1341
+ if (!(id in previous)) return previous;
1342
+ const next = { ...previous };
1343
+ delete next[id];
1344
+ return next;
1345
+ });
1346
+ };
1347
+ const onInstall = (entry) => {
1348
+ if (face === null || !face.isLoopback || pending !== null) return;
1349
+ clearError(entry.id);
1350
+ setPending({
1351
+ kind: "install",
1352
+ id: entry.id
1353
+ });
1354
+ face.install(installSpec(entry)).then(() => {
1355
+ face.list().then(setInstalled, () => {});
1356
+ }, (reason) => {
1357
+ setErrors((previous) => ({
1358
+ ...previous,
1359
+ [entry.id]: t("installFailed", { reason: messageOf(reason) })
1360
+ }));
1361
+ }).finally(() => {
1362
+ setPending(null);
1363
+ });
1364
+ };
1365
+ const onUninstallConfirm = () => {
1366
+ const target = uninstallTarget;
1367
+ if (face === null || target === null || pending !== null) return;
1368
+ const item = entryInstalled(target, installed ?? []);
1369
+ clearError(target.id);
1370
+ if (item === null) {
1371
+ setUninstallTarget(null);
1372
+ return;
1373
+ }
1374
+ setPending({
1375
+ kind: "uninstall",
1376
+ id: target.id
1377
+ });
1378
+ face.uninstall(item.id).then((list) => {
1379
+ setInstalled(list);
1380
+ setUninstallTarget(null);
1381
+ }, (reason) => {
1382
+ setErrors((previous) => ({
1383
+ ...previous,
1384
+ [target.id]: t("uninstallFailed", { reason: messageOf(reason) })
1385
+ }));
1386
+ setUninstallTarget(null);
1387
+ }).finally(() => {
1388
+ setPending(null);
1389
+ });
1390
+ };
865
1391
  const copyCommand = (id, command) => {
866
1392
  const mark = () => {
867
1393
  setCopiedId(id);
@@ -885,6 +1411,7 @@ window.__ModuleLoader__.load({
885
1411
  mark();
886
1412
  };
887
1413
  const disabled = !state.writable;
1414
+ const mutationsDisabled = disabled || pending !== null;
888
1415
  const visible = state.enabled.text !== "false";
889
1416
  const fieldProps = {
890
1417
  overriddenLabel: t("settings.overridden"),
@@ -1017,6 +1544,10 @@ window.__ModuleLoader__.load({
1017
1544
  const copied = copiedId === plugin.id;
1018
1545
  const name = entryCopy(t, `name.${plugin.id}`, plugin.nameEn || plugin.name);
1019
1546
  const description = entryCopy(t, `desc.${plugin.id}`, plugin.descriptionEn ?? plugin.description ?? "");
1547
+ const installedItem = faceLoopback ? entryInstalled(plugin, installed ?? []) : null;
1548
+ const isInstalling = pending?.kind === "install" && pending.id === plugin.id;
1549
+ const isUninstalling = pending?.kind === "uninstall" && pending.id === plugin.id;
1550
+ const error = errors[plugin.id];
1020
1551
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1021
1552
  className: community_module_css_default.card,
1022
1553
  children: [
@@ -1026,10 +1557,16 @@ window.__ModuleLoader__.load({
1026
1557
  className: community_module_css_default.cardName,
1027
1558
  title: name,
1028
1559
  children: name
1029
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1030
- className: plugin.npm ? `${community_module_css_default.badge} ${community_module_css_default.badgePublished}` : community_module_css_default.badge,
1031
- title: plugin.npm ?? plugin.repo,
1032
- children: plugin.npm ? t("badge.published") : t("badge.source")
1560
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1561
+ className: community_module_css_default.cardBadges,
1562
+ children: [installedItem !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1563
+ className: `${community_module_css_default.badge} ${community_module_css_default.badgeInstalled}`,
1564
+ children: t("badge.installed")
1565
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1566
+ className: plugin.npm ? `${community_module_css_default.badge} ${community_module_css_default.badgePublished}` : community_module_css_default.badge,
1567
+ title: plugin.npm ?? plugin.repo,
1568
+ children: plugin.npm ? t("badge.published") : t("badge.source")
1569
+ })]
1033
1570
  })]
1034
1571
  }),
1035
1572
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -1057,28 +1594,64 @@ window.__ModuleLoader__.load({
1057
1594
  }) : null,
1058
1595
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1059
1596
  className: community_module_css_default.cardFooter,
1060
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1061
- className: community_module_css_default.cardTop,
1062
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1063
- className: community_module_css_default.cardLink,
1064
- href: plugin.repo,
1065
- target: "_blank",
1066
- rel: "noreferrer",
1067
- children: t("repository")
1068
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1069
- type: "button",
1070
- className: copied ? `${community_module_css_default.installButton} ${community_module_css_default.installButtonCopied}` : community_module_css_default.installButton,
1597
+ children: [
1598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1599
+ className: community_module_css_default.cardTop,
1600
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1601
+ className: community_module_css_default.cardLink,
1602
+ href: plugin.repo,
1603
+ target: "_blank",
1604
+ rel: "noreferrer",
1605
+ children: t("repository")
1606
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1607
+ className: community_module_css_default.cardActions,
1608
+ children: [
1609
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1610
+ type: "button",
1611
+ className: copied ? `${community_module_css_default.installButton} ${community_module_css_default.installButtonCopied}` : faceLoopback ? `${community_module_css_default.installButton} ${community_module_css_default.installButtonSecondary}` : community_module_css_default.installButton,
1612
+ title: command,
1613
+ onClick: () => {
1614
+ copyCommand(plugin.id, command);
1615
+ },
1616
+ children: copied ? t("copied") : t("install")
1617
+ }),
1618
+ faceLoopback && installedItem === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1619
+ type: "button",
1620
+ className: community_module_css_default.installButton,
1621
+ disabled: mutationsDisabled,
1622
+ onClick: () => {
1623
+ onInstall(plugin);
1624
+ },
1625
+ children: isInstalling ? t("installing") : t("installNow")
1626
+ }) : null,
1627
+ faceLoopback && installedItem !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1628
+ type: "button",
1629
+ className: `${community_module_css_default.installButton} ${community_module_css_default.installButtonSecondary}`,
1630
+ disabled: mutationsDisabled,
1631
+ onClick: () => {
1632
+ setUninstallTarget(plugin);
1633
+ },
1634
+ children: isUninstalling ? t("uninstalling") : t("uninstall")
1635
+ }) : null
1636
+ ]
1637
+ })]
1638
+ }),
1639
+ isInstalling ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1640
+ className: community_module_css_default.progress,
1641
+ role: "status",
1642
+ children: progress !== null && progress.kind !== "idle" ? t(`progress.${progress.stage}`) : t("installing")
1643
+ }) : null,
1644
+ error !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1645
+ className: community_module_css_default.error,
1646
+ role: "alert",
1647
+ children: error
1648
+ }) : null,
1649
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1650
+ className: community_module_css_default.cardCommand,
1071
1651
  title: command,
1072
- onClick: () => {
1073
- copyCommand(plugin.id, command);
1074
- },
1075
- children: copied ? t("copied") : t("install")
1076
- })]
1077
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1078
- className: community_module_css_default.cardCommand,
1079
- title: command,
1080
- children: command
1081
- })]
1652
+ children: command
1653
+ })
1654
+ ]
1082
1655
  })
1083
1656
  ]
1084
1657
  }, plugin.id);
@@ -1090,6 +1663,15 @@ window.__ModuleLoader__.load({
1090
1663
  role: "status",
1091
1664
  children: t("off")
1092
1665
  }),
1666
+ face === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1667
+ className: community_module_css_default.installNote,
1668
+ role: "note",
1669
+ children: t("managerHint")
1670
+ }) : face.isLoopback ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1671
+ className: community_module_css_default.installNote,
1672
+ role: "note",
1673
+ children: t("remoteHint")
1674
+ }),
1093
1675
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1094
1676
  className: community_module_css_default.installNote,
1095
1677
  role: "note",
@@ -1099,13 +1681,41 @@ window.__ModuleLoader__.load({
1099
1681
  className: community_module_css_default.notice,
1100
1682
  role: "note",
1101
1683
  children: t("notice")
1684
+ }),
1685
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1686
+ open: uninstallTarget !== null,
1687
+ onClose: () => {
1688
+ if (pending === null) setUninstallTarget(null);
1689
+ },
1690
+ title: t("uninstallConfirm.title"),
1691
+ closeLabel: t("cancel"),
1692
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1693
+ className: community_module_css_default.confirmBody,
1694
+ children: t("uninstallConfirm.body", { name: uninstallTarget === null ? "" : entryCopy(t, `name.${uninstallTarget.id}`, uninstallTarget.nameEn || uninstallTarget.name) })
1695
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1696
+ className: community_module_css_default.modalActions,
1697
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1698
+ variant: "outline",
1699
+ disabled: pending !== null,
1700
+ onClick: () => {
1701
+ setUninstallTarget(null);
1702
+ },
1703
+ children: t("cancel")
1704
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1705
+ variant: "primary",
1706
+ className: community_module_css_default.dangerButton,
1707
+ disabled: pending !== null,
1708
+ onClick: onUninstallConfirm,
1709
+ children: pending?.kind === "uninstall" ? t("uninstalling") : t("uninstallConfirm.confirm")
1710
+ })]
1711
+ })]
1102
1712
  })
1103
1713
  ]
1104
1714
  });
1105
1715
  }
1106
1716
  /** Render the community plugin index as a first-level settings page. */
1107
1717
  function CommunityPluginsSection(props) {
1108
- const { t, useCommunityPluginsCard, save, discard, edit, resetField, plugins } = props;
1718
+ const { t, useCommunityPluginsCard, save, discard, edit, resetField, plugins, pluginManager } = props;
1109
1719
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1110
1720
  className: community_module_css_default.sectionList,
1111
1721
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommunityPluginsCard, {
@@ -1115,7 +1725,8 @@ window.__ModuleLoader__.load({
1115
1725
  discard,
1116
1726
  edit,
1117
1727
  resetField,
1118
- plugins
1728
+ plugins,
1729
+ pluginManager
1119
1730
  })
1120
1731
  });
1121
1732
  }
@@ -1154,6 +1765,23 @@ window.__ModuleLoader__.load({
1154
1765
  "copy": "复制",
1155
1766
  "copied": "已复制",
1156
1767
  "install": "复制安装命令",
1768
+ "installNow": "安装",
1769
+ "installing": "安装中…",
1770
+ "uninstall": "卸载",
1771
+ "uninstalling": "卸载中…",
1772
+ "cancel": "取消",
1773
+ "badge.installed": "已安装 · 重启后生效",
1774
+ "progress.fetch": "获取插件信息",
1775
+ "progress.download": "下载中",
1776
+ "progress.extract": "解压中",
1777
+ "progress.write": "写入中",
1778
+ "uninstallConfirm.title": "卸载插件",
1779
+ "uninstallConfirm.body": "确定要卸载 {name} 吗?移除将在重启 dsh web 后完全生效。",
1780
+ "uninstallConfirm.confirm": "确认卸载",
1781
+ "installFailed": "安装失败:{reason}",
1782
+ "uninstallFailed": "卸载失败:{reason}",
1783
+ "managerHint": "未检测到「插件管理器」插件:安装 @linxin666/dsh-client-ui-plugin-manager 后,可在本机浏览器中直接从这里安装与卸载。",
1784
+ "remoteHint": "当前是远程浏览器,界面内安装不可用;请在宿主机器的终端执行安装命令。",
1157
1785
  "noMatch": "没有匹配的社区插件。",
1158
1786
  "search.placeholder": "搜索名称、作者或简介",
1159
1787
  "search.label": "搜索社区插件",
@@ -1168,7 +1796,7 @@ window.__ModuleLoader__.load({
1168
1796
  "category.integration": "集成与分享",
1169
1797
  "category.security": "安全与运维",
1170
1798
  "category.utility": "实用工具",
1171
- "installHint": "索引只登记、不安装代码。要运行某个插件,复制它的安装命令到终端执行;安装后,插件自带的开关与配置(若有)会出现在插件配置里。",
1799
+ "installHint": "在本机浏览器且已安装「插件管理器」插件时,可直接点击卡片上的「安装 / 卸载」;否则复制安装命令到终端执行。安装与卸载在重启 dsh web 后生效,插件自带的开关与配置(若有)会出现在插件配置里。",
1172
1800
  "empty": "暂无社区插件登记。",
1173
1801
  "off": "社区插件索引已关闭。",
1174
1802
  "notice": "条目由贡献者自行登记,与 dsh-web-ui 的发布内容无关;使用前请自行评估。"
@@ -1199,6 +1827,23 @@ window.__ModuleLoader__.load({
1199
1827
  "copy": "Copy",
1200
1828
  "copied": "Copied",
1201
1829
  "install": "Copy install command",
1830
+ "installNow": "Install",
1831
+ "installing": "Installing…",
1832
+ "uninstall": "Uninstall",
1833
+ "uninstalling": "Uninstalling…",
1834
+ "cancel": "Cancel",
1835
+ "badge.installed": "Installed · restart to take effect",
1836
+ "progress.fetch": "Fetching plugin info",
1837
+ "progress.download": "Downloading",
1838
+ "progress.extract": "Extracting",
1839
+ "progress.write": "Writing",
1840
+ "uninstallConfirm.title": "Uninstall plugin",
1841
+ "uninstallConfirm.body": "Uninstall {name}? The removal fully takes effect after restarting dsh web.",
1842
+ "uninstallConfirm.confirm": "Confirm uninstall",
1843
+ "installFailed": "Install failed: {reason}",
1844
+ "uninstallFailed": "Uninstall failed: {reason}",
1845
+ "managerHint": "The Plugin manager plugin is not detected: install @linxin666/dsh-client-ui-plugin-manager to install and uninstall entries directly from here in a local browser.",
1846
+ "remoteHint": "This is a remote browser, so in-GUI install is unavailable; run the install command in a terminal on the host machine.",
1202
1847
  "noMatch": "No matching community plugin found.",
1203
1848
  "search.placeholder": "Search name, author or description",
1204
1849
  "search.label": "Search community plugins",
@@ -1213,7 +1858,7 @@ window.__ModuleLoader__.load({
1213
1858
  "category.integration": "Integration & Sharing",
1214
1859
  "category.security": "Security & Ops",
1215
1860
  "category.utility": "Utilities",
1216
- "installHint": "The index only registers entries, it never installs code. To run a plugin, copy its install command into a terminal; once installed, the plugin provides its own switch and config (if any) in the plugin configuration section.",
1861
+ "installHint": "With the Plugin manager plugin present and a local browser, use the Install / Uninstall buttons on each card; otherwise copy the install command into a terminal. Installs and uninstalls take effect after restarting dsh web; once installed, the plugin provides its own switch and config (if any) in the plugin configuration section.",
1217
1862
  "empty": "No community plugins registered yet.",
1218
1863
  "off": "The community plugin index is turned off.",
1219
1864
  "notice": "Entries are contributed by their authors and are separate from dsh-web-ui releases; evaluate before use."
@@ -1253,6 +1898,7 @@ window.__ModuleLoader__.load({
1253
1898
  zh,
1254
1899
  en
1255
1900
  }), "community-plugins: dictionaries");
1901
+ bridgePluginManager(ctx);
1256
1902
  const controller = new CommunityPluginsCardController((ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: COMMUNITY_PLUGINS_NS }));
1257
1903
  ctx.slots.inject("settings.section", () => {
1258
1904
  const unregister = ctx.slots.register({