@linxin666/dsh-client-ui-community-plugins 0.1.18 → 0.1.20

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
@@ -268,6 +268,9 @@ window.__ModuleLoader__.load({
268
268
  specs;
269
269
  staged = /* @__PURE__ */ new Map();
270
270
  listeners = /* @__PURE__ */ new Set();
271
+ /** The scope subscription installed in the constructor; released by dispose(). */
272
+ disposeScope;
273
+ disposed = false;
271
274
  saving = false;
272
275
  failed = false;
273
276
  failedReason;
@@ -275,10 +278,20 @@ window.__ModuleLoader__.load({
275
278
  constructor(scope, specs) {
276
279
  this.scope = scope;
277
280
  this.specs = new Map(specs.map((spec) => [spec.field, spec]));
278
- scope.subscribe(() => {
281
+ this.disposeScope = scope.subscribe(() => {
279
282
  this.publish();
280
283
  });
281
284
  }
285
+ /**
286
+ * Release the scope subscription and every bound store listener. The card
287
+ * must call this on teardown; later calls are no-ops.
288
+ */
289
+ dispose() {
290
+ if (this.disposed) return;
291
+ this.disposed = true;
292
+ this.disposeScope();
293
+ this.listeners.clear();
294
+ }
282
295
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
283
296
  bind(project) {
284
297
  const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
@@ -478,6 +491,23 @@ window.__ModuleLoader__.load({
478
491
  };
479
492
  //#endregion
480
493
  //#region src/client/generated/community.ts
494
+ /**
495
+ * AUTO-GENERATED by scripts/community-index — do not edit by hand.
496
+ * Community plugin index for the settings page: every entry links to the
497
+ * contributor's own repository. This repository never vendors the listed
498
+ * plugins — it only points users at them.
499
+ * Regenerate with `node scripts/community-index`.
500
+ */
501
+ /** Stable category ids rendered as marketplace-style filter pills (the runtime guard imports this). */
502
+ const COMMUNITY_CATEGORIES = [
503
+ "ui",
504
+ "agent",
505
+ "tools",
506
+ "knowledge",
507
+ "integration",
508
+ "security",
509
+ "utility"
510
+ ];
481
511
  /** Every community plugin, in community.json order. */
482
512
  const COMMUNITY_PLUGINS = [
483
513
  {
@@ -487,7 +517,8 @@ window.__ModuleLoader__.load({
487
517
  "author": "omdsh-dev",
488
518
  "repo": "https://github.com/omdsh-dev/dsh-data-agent",
489
519
  "description": "为 DSH 定义专用 Data Agent 预设,让 AI 帮你查询、更新、分析数据。",
490
- "descriptionEn": "Defines a dedicated Data Agent preset for DSH so the AI can query, update and analyze data."
520
+ "descriptionEn": "Defines a dedicated Data Agent preset for DSH so the AI can query, update and analyze data.",
521
+ "category": "agent"
491
522
  },
492
523
  {
493
524
  "id": "dsh-tui",
@@ -496,7 +527,8 @@ window.__ModuleLoader__.load({
496
527
  "author": "ccch1mneyyy",
497
528
  "repo": "https://github.com/ccch1mneyyy/dsh-TUI",
498
529
  "description": "Claude Code 风格全屏交互终端插件:像素鲸鱼顶栏、实时工作状态行、思考流式展开、双击 Esc 回滚、上下文进度条与 TPS 仪表。",
499
- "descriptionEn": "A Claude Code style fullscreen interactive terminal plugin: pixel-whale header, live working-state line, streaming reasoning expansion, double-Esc rollback, context progress bar and TPS gauges."
530
+ "descriptionEn": "A Claude Code style fullscreen interactive terminal plugin: pixel-whale header, live working-state line, streaming reasoning expansion, double-Esc rollback, context progress bar and TPS gauges.",
531
+ "category": "ui"
500
532
  },
501
533
  {
502
534
  "id": "dsh-tianshu-tui",
@@ -505,7 +537,8 @@ window.__ModuleLoader__.load({
505
537
  "author": "huiliyi37",
506
538
  "repo": "https://github.com/huiliyi37/dsh-tianshu-tui",
507
539
  "description": "基于官方 DeepSeek Harness 的交互式终端 UI 插件,在官方基础上增加 TDD 与证据门等工作流。",
508
- "descriptionEn": "An interactive terminal UI plugin for DeepSeek Harness that adds TDD and evidence-gate workflows on top of the official base."
540
+ "descriptionEn": "An interactive terminal UI plugin for DeepSeek Harness that adds TDD and evidence-gate workflows on top of the official base.",
541
+ "category": "ui"
509
542
  },
510
543
  {
511
544
  "id": "dsh-chat-summary",
@@ -515,7 +548,8 @@ window.__ModuleLoader__.load({
515
548
  "repo": "https://github.com/v833/dsh-chat-summary",
516
549
  "description": "总结当前对话并导出为 Markdown / DOCX / PDF,可选 LLM 智能总结(用户自配 API Key)。",
517
550
  "descriptionEn": "Summarize the current conversation and export it as Markdown / DOCX / PDF, with optional LLM summarization using your own API key.",
518
- "npm": "@linxin666/dsh-client-ui-chat-summary"
551
+ "npm": "@linxin666/dsh-client-ui-chat-summary",
552
+ "category": "knowledge"
519
553
  },
520
554
  {
521
555
  "id": "dsh-builtin-toggles",
@@ -525,7 +559,8 @@ window.__ModuleLoader__.load({
525
559
  "repo": "https://github.com/Starfie1d1272/dsh-builtin-toggles",
526
560
  "description": "Evidence-backed 内置 capability Inspector:展示 DSH Web built-in capability 的 provenance、compatibility 与 structural drift;仅对 9 个经过审阅的 UI leaves 提供 fail-closed 开关。",
527
561
  "descriptionEn": "Evidence-backed built-in capability Inspector: surfaces provenance, compatibility and structural drift of DSH Web built-ins, with fail-closed toggles for only the nine reviewed UI leaves.",
528
- "npm": "dsh-builtin-toggles"
562
+ "npm": "dsh-builtin-toggles",
563
+ "category": "tools"
529
564
  },
530
565
  {
531
566
  "id": "dsh-pilot",
@@ -534,7 +569,8 @@ window.__ModuleLoader__.load({
534
569
  "author": "guo6x",
535
570
  "repo": "https://github.com/guo6x/dsh-pilot",
536
571
  "description": "给 agent 一双会开车的手:零依赖 CDP 浏览器操控(8 个 pilot_* 工具:导航/点击/输入/按键/JS/截图)+ Web GUI 可拖拽驾驶舱面板,无需 Playwright、无需 API key。",
537
- "descriptionEn": "Give your agent hands: zero-dependency CDP browser control (8 pilot_* tools: navigate/click/type/keys/eval/screenshot) plus a draggable cockpit panel in the Web GUI - no Playwright, no API key."
572
+ "descriptionEn": "Give your agent hands: zero-dependency CDP browser control (8 pilot_* tools: navigate/click/type/keys/eval/screenshot) plus a draggable cockpit panel in the Web GUI - no Playwright, no API key.",
573
+ "category": "tools"
538
574
  },
539
575
  {
540
576
  "id": "dsh-housekeeper",
@@ -543,7 +579,8 @@ window.__ModuleLoader__.load({
543
579
  "author": "guo6x",
544
580
  "repo": "https://github.com/guo6x/dsh-housekeeper",
545
581
  "description": "管住 agent 的脏手:工具链台账(node/pnpm/git/gh/ffmpeg 等自动探测)、缓存与临时目录扫描 + 白名单安全一键清理、机器规则 AGENTS.md 查看编辑,全在设置面板完成。",
546
- "descriptionEn": "Keep your agent's hands clean: toolchain inventory, scratch/cache scan with whitelist-guarded one-click cleanup, and the machine rules file (AGENTS.md) view/edit - all in the settings panel."
582
+ "descriptionEn": "Keep your agent's hands clean: toolchain inventory, scratch/cache scan with whitelist-guarded one-click cleanup, and the machine rules file (AGENTS.md) view/edit - all in the settings panel.",
583
+ "category": "utility"
547
584
  },
548
585
  {
549
586
  "id": "dsh-deepread",
@@ -551,9 +588,10 @@ window.__ModuleLoader__.load({
551
588
  "nameEn": "DeepRead Assistant",
552
589
  "author": "xiehuan123",
553
590
  "repo": "https://github.com/xiehuan123/dsh-deepread",
554
- "description": "五种模式精读插件(quick / deep / map / feynman / book),支持公众号链接与文件输入,导出 md / mm / html,Web UI 提供工具结果卡片与精读面板。",
555
- "descriptionEn": "A five-mode deep reading plugin (quick / deep / map / feynman / book) for links and files, with md / mm / html exports and Web UI tool-result cards plus a reading panel.",
556
- "npm": "dsh-deepread"
591
+ "description": "五种模式精读插件(quick / deep / map / feynman / book),支持公众号链接与文件输入、批量对比、预算预检与后台任务进度透明,导出 md / mm / html,Web UI 提供工具结果卡片与精读面板。",
592
+ "descriptionEn": "A five-mode deep reading plugin (quick / deep / map / feynman / book) for links and files, with batch comparison, budget preflight, transparent background-job progress, md / mm / html exports, and Web UI tool-result cards plus a reading panel.",
593
+ "npm": "dsh-deepread",
594
+ "category": "knowledge"
557
595
  },
558
596
  {
559
597
  "id": "dsh-mnemon",
@@ -563,11 +601,100 @@ window.__ModuleLoader__.load({
563
601
  "repo": "https://github.com/omdsh-dev/dsh-mnemon",
564
602
  "description": "与 Mnemon CLI 集成的跨 Agent、本地优先持久记忆插件:用户画像 / 工作记忆 / 项目档案与长期 Memory Spaces,支持导入导出。",
565
603
  "descriptionEn": "A cross-agent, local-first persistent memory plugin integrating the Mnemon CLI: profiles, working memory, project documents and long-term Memory Spaces, with import and export.",
566
- "npm": "dsh-mnemon"
604
+ "npm": "dsh-mnemon",
605
+ "category": "knowledge"
606
+ },
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
+ {
618
+ "id": "dsh-genui",
619
+ "name": "GenUI 生成式 UI",
620
+ "nameEn": "GenUI",
621
+ "author": "omdsh-dev",
622
+ "repo": "https://github.com/omdsh-dev/dsh-genui",
623
+ "description": "给模型输出配交互式 UI:助手回复内联渲染 dsh-ui fence(布局、图表、表单、Mermaid、3D),支持流式渲染与面板停靠,组件交互可回传模型。",
624
+ "descriptionEn": "Interactive UI inside assistant replies via the dsh-ui fence: layouts, charts, forms, Mermaid and 3D with streaming rendering, panel docking and actions that loop back to the model.",
625
+ "category": "ui"
626
+ },
627
+ {
628
+ "id": "dsh-annotation",
629
+ "name": "选中批注",
630
+ "nameEn": "Selection Annotation",
631
+ "author": "omdsh-dev",
632
+ "repo": "https://github.com/omdsh-dev/dsh-annotation",
633
+ "description": "选中助手文字即可批注,回车随消息发送;自己的气泡只显示问题与「批注 ×N」标签,模型按 Annotation N 逐条对照回复(悬浮芯片)。",
634
+ "descriptionEn": "Select text in an assistant reply to annotate it; annotations are sent with your next message, hidden from your own bubble behind an Annotations xN chip, and the model replies per Annotation N with hoverable chips.",
635
+ "category": "ui"
636
+ },
637
+ {
638
+ "id": "deepseek-harness-auth",
639
+ "name": "DeepSeek Harness Auth",
640
+ "nameEn": "DeepSeek Harness Auth",
641
+ "author": "taichuy",
642
+ "repo": "https://github.com/taichuy/deepseek-harness-auth",
643
+ "description": "为 DSH Web 公网部署提供登录认证前置代理,支持账号密码、验证码、失败锁定和 IP/CIDR 白名单。",
644
+ "descriptionEn": "An authentication proxy for public DSH Web deployments with password login, captcha, failed-attempt locking, and IP/CIDR allowlists.",
645
+ "npm": "deepseek-harness-auth",
646
+ "category": "security"
647
+ },
648
+ {
649
+ "id": "dsh-cloud-sync",
650
+ "name": "云同步服务",
651
+ "nameEn": "Cloud Sync",
652
+ "author": "dickpy",
653
+ "repo": "https://github.com/dickpy/dsh-cloud-sync",
654
+ "description": "支持 WebDAV、S3、阿里云 OSS、腾讯云 COS 与 MinIO 的 DSH 云同步插件,可同步 profile、插件配置及本地插件源码归档。",
655
+ "descriptionEn": "DSH cloud sync for WebDAV, S3, Alibaba Cloud OSS, Tencent Cloud COS and MinIO, syncing profiles, plugin settings and local plugin source archives.",
656
+ "npm": "@dickpy/dsh-cloud-sync",
657
+ "category": "integration"
658
+ },
659
+ {
660
+ "id": "dsh-auto-memory",
661
+ "name": "自动记忆",
662
+ "nameEn": "Auto Memory",
663
+ "author": "Aik358",
664
+ "repo": "https://github.com/Aik358/dsh-auto-memory",
665
+ "description": "三层自动记忆插件:用户级/项目笔记/每日日志自动注入与检索、每轮自动沉淀、AI 时段问候、智能检索、日历与工作区思维导图,支持继承 WorkBuddy/CodeBuddy/Claude Code/Codex 等其他 AI 工具的记忆。",
666
+ "descriptionEn": "Three-layer auto memory for DSH: user-level/project-notes/daily-logs with automatic injection and retrieval, per-turn auto-consolidation, AI greetings, smart search, calendar, workspace mind map, and inheritance from WorkBuddy/CodeBuddy/Claude Code/Codex.",
667
+ "npm": "@a9i5k4/dsh-auto-memory",
668
+ "category": "knowledge"
669
+ },
670
+ {
671
+ "id": "dsh-memoir",
672
+ "name": "项目记忆 Memoir",
673
+ "nameEn": "Project Memory Memoir",
674
+ "author": "Qinling-Melon-Farmers",
675
+ "repo": "https://github.com/Qinling-Melon-Farmers/dsh-memoir",
676
+ "description": "项目持久化记忆与会话经验沉淀:memoir_record / memoir_read 工具(BM25 相关度排序检索)、每轮工作自动蒸馏、记忆诊断面板与跨项目全局检索,纯本地零外部依赖。",
677
+ "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.",
678
+ "category": "knowledge"
567
679
  }
568
680
  ];
569
681
  //#endregion
570
682
  //#region src/client/community-guard.ts
683
+ /**
684
+ * Runtime guard for the generated community index entries. The index is
685
+ * build-time generated data, but the card renders whatever the module
686
+ * carries; a hand-written narrowing keeps one malformed entry from breaking
687
+ * the whole list at render time.
688
+ */
689
+ /** Category ids the card knows how to label; others are treated as uncategorized. */
690
+ const KNOWN_CATEGORIES = COMMUNITY_CATEGORIES;
691
+ /**
692
+ * The install command is pasted into a shell, so repo/npm must be free of
693
+ * shell metacharacters: repo is a plain https URL of path-safe characters,
694
+ * npm a standard (optionally scoped) package name.
695
+ */
696
+ const REPO_SAFE_RE = /^https:\/\/[A-Za-z0-9._~\/-]+$/;
697
+ const NPM_SAFE_RE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
571
698
  /** True when the value is a well-formed community plugin entry. */
572
699
  function isCommunityPluginEntry(value) {
573
700
  if (typeof value !== "object" || value === null) return false;
@@ -575,15 +702,16 @@ window.__ModuleLoader__.load({
575
702
  if (typeof entry.id !== "string" || entry.id === "") return false;
576
703
  if (typeof entry.name !== "string" || typeof entry.nameEn !== "string") return false;
577
704
  if (typeof entry.author !== "string" || entry.author === "") return false;
578
- if (typeof entry.repo !== "string" || !entry.repo.startsWith("https://")) return false;
705
+ if (typeof entry.repo !== "string" || !REPO_SAFE_RE.test(entry.repo)) return false;
579
706
  if (entry.description !== void 0 && typeof entry.description !== "string") return false;
580
707
  if (entry.descriptionEn !== void 0 && typeof entry.descriptionEn !== "string") return false;
581
- if (entry.npm !== void 0 && typeof entry.npm !== "string") return false;
708
+ if (entry.npm !== void 0 && (typeof entry.npm !== "string" || !NPM_SAFE_RE.test(entry.npm))) return false;
709
+ if (entry.category !== void 0 && (typeof entry.category !== "string" || !KNOWN_CATEGORIES.includes(entry.category))) return false;
582
710
  return true;
583
711
  }
584
712
  //#endregion
585
713
  //#region \0dsh-css:packages/dsh-community-plugins/src/client/community.module.css.mjs
586
- const css = ".z0J_8a_sectionList{margin:0;padding:0;list-style:none}.z0J_8a_entries{flex-direction:column;gap:8px;margin:8px 0 0;padding:0;list-style:none;display:flex}.z0J_8a_entry{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:6px;flex-direction:column;gap:4px;padding:10px 12px;display:flex}.z0J_8a_entryHead{justify-content:space-between;align-items:baseline;gap:8px;min-width:0;display:flex}.z0J_8a_entryName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.z0J_8a_entryAuthor{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:none;font-size:12px;overflow:hidden}.z0J_8a_entryDescription,.z0J_8a_entryDescriptionEn{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.4}.z0J_8a_entryDescriptionEn{color:var(--dsw-alias-label-tertiary);font-size:12px}.z0J_8a_entryLinks{flex-wrap:wrap;align-items:center;gap:10px;margin-top:2px;display:flex}.z0J_8a_entryLink{color:var(--dsw-alias-link-primary,var(--dsw-alias-button-info-fill));font-size:13px;text-decoration:underline}.z0J_8a_entryLink:hover{text-decoration:none}.z0J_8a_entryNpm{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.z0J_8a_entryInstall{flex-wrap:wrap;align-items:center;gap:8px;margin-top:2px;display:flex}.z0J_8a_entryCommand{min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);overflow-wrap:anywhere;border-radius:4px;flex:1;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}.z0J_8a_copyButton{color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background:0 0;border-radius:4px;flex:none;padding:2px 10px;font-size:12px;line-height:1.5}.z0J_8a_copyButton:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.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}";
714
+ 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}";
587
715
  const tagId = "@linxin666/dsh-client-ui-community-plugins/community.module.css";
588
716
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
589
717
  const tag = document.createElement("style");
@@ -593,33 +721,45 @@ window.__ModuleLoader__.load({
593
721
  document.head.appendChild(tag);
594
722
  }
595
723
  var community_module_css_default = {
596
- "copyButton": "z0J_8a_copyButton",
724
+ "badge": "z0J_8a_badge",
725
+ "badgePublished": "z0J_8a_badgePublished",
726
+ "card": "z0J_8a_card",
727
+ "cardAuthor": "z0J_8a_cardAuthor",
728
+ "cardCategory": "z0J_8a_cardCategory",
729
+ "cardCommand": "z0J_8a_cardCommand",
730
+ "cardDescription": "z0J_8a_cardDescription",
731
+ "cardDot": "z0J_8a_cardDot",
732
+ "cardFooter": "z0J_8a_cardFooter",
733
+ "cardHead": "z0J_8a_cardHead",
734
+ "cardLink": "z0J_8a_cardLink",
735
+ "cardMeta": "z0J_8a_cardMeta",
736
+ "cardName": "z0J_8a_cardName",
737
+ "cardTop": "z0J_8a_cardTop",
597
738
  "empty": "z0J_8a_empty",
598
- "entries": "z0J_8a_entries",
599
- "entry": "z0J_8a_entry",
600
- "entryAuthor": "z0J_8a_entryAuthor",
601
- "entryCommand": "z0J_8a_entryCommand",
602
- "entryDescription": "z0J_8a_entryDescription",
603
- "entryDescriptionEn": "z0J_8a_entryDescriptionEn",
604
- "entryHead": "z0J_8a_entryHead",
605
- "entryInstall": "z0J_8a_entryInstall",
606
- "entryLink": "z0J_8a_entryLink",
607
- "entryLinks": "z0J_8a_entryLinks",
608
- "entryName": "z0J_8a_entryName",
609
- "entryNpm": "z0J_8a_entryNpm",
739
+ "filters": "z0J_8a_filters",
740
+ "grid": "z0J_8a_grid",
741
+ "installButton": "z0J_8a_installButton",
742
+ "installButtonCopied": "z0J_8a_installButtonCopied",
610
743
  "installNote": "z0J_8a_installNote",
744
+ "market": "z0J_8a_market",
611
745
  "notice": "z0J_8a_notice",
612
746
  "off": "z0J_8a_off",
613
- "sectionList": "z0J_8a_sectionList"
747
+ "pill": "z0J_8a_pill",
748
+ "pillActive": "z0J_8a_pillActive",
749
+ "resultCount": "z0J_8a_resultCount",
750
+ "search": "z0J_8a_search",
751
+ "sectionList": "z0J_8a_sectionList",
752
+ "toolbar": "z0J_8a_toolbar"
614
753
  };
615
754
  //#endregion
616
755
  //#region src/client/CommunityPluginsCard.tsx
617
756
  /**
618
757
  * The community plugin index card: a first-level settings section that is
619
- * always open (a static header with the index list directly visible). Its own
620
- * enable switch (backed by the community-plugins settings namespace) gates
621
- * the entry list; the list itself points at contributors' own repositories
622
- * this package only indexes them, it never vendors their code.
758
+ * always open. Its own enable switch (backed by the community-plugins
759
+ * settings namespace) gates the list, which is presented marketplace-style:
760
+ * a search box, category filter pills with counts, and a two-column card grid
761
+ * whose entries link to contributors' own repositories this package only
762
+ * indexes them, it never vendors their code.
623
763
  */
624
764
  /** Bridges the community-plugins scope onto the card's staged form. */
625
765
  var CommunityPluginsCardController = class {
@@ -646,12 +786,38 @@ window.__ModuleLoader__.load({
646
786
  ...this.form.actions()
647
787
  };
648
788
  }
789
+ /**
790
+ * Release the card's scope subscription and bound stores; the slot
791
+ * disposer calls this on teardown.
792
+ */
793
+ dispose() {
794
+ this.form.dispose();
795
+ }
649
796
  };
650
797
  /** The one-line install command for an entry: npm package when published, else the contributor repository URL. */
651
798
  function installCommand(entry) {
652
799
  return `dsh plugin --profile web add ${entry.npm ?? entry.repo}`;
653
800
  }
654
801
  /**
802
+ * Localized entry copy read through the card's `t`. Falls back to the raw
803
+ * entry field when the key is missing — the dictionary only carries the
804
+ * generated registry, so injected test entries land on the fallback.
805
+ */
806
+ function entryCopy(t, key, fallback) {
807
+ const value = t(key);
808
+ return value === key ? fallback : value;
809
+ }
810
+ /** Maps a category id onto its locale dictionary key (kept in the generated union). */
811
+ const CATEGORY_KEY = {
812
+ ui: "category.ui",
813
+ agent: "category.agent",
814
+ tools: "category.tools",
815
+ knowledge: "category.knowledge",
816
+ integration: "category.integration",
817
+ security: "category.security",
818
+ utility: "category.utility"
819
+ };
820
+ /**
655
821
  * Render the community plugin index card.
656
822
  * @param props - locale copy, the card snapshot, its form actions, and the
657
823
  * (default-generated) entry list.
@@ -660,7 +826,9 @@ window.__ModuleLoader__.load({
660
826
  function CommunityPluginsCard(props) {
661
827
  const { t } = props;
662
828
  const state = props.useCommunityPluginsCard((snapshot) => snapshot);
663
- const plugins = (props.plugins ?? COMMUNITY_PLUGINS).filter(isCommunityPluginEntry);
829
+ const plugins = (0, react.useMemo)(() => (props.plugins ?? COMMUNITY_PLUGINS).filter(isCommunityPluginEntry), [props.plugins]);
830
+ const [query, setQuery] = (0, react.useState)("");
831
+ const [category, setCategory] = (0, react.useState)(null);
664
832
  const [copiedId, setCopiedId] = (0, react.useState)(null);
665
833
  const copyCommand = (id, command) => {
666
834
  const mark = () => {
@@ -692,6 +860,33 @@ window.__ModuleLoader__.load({
692
860
  invalidLabel: t("settings.invalidNumber"),
693
861
  disabled
694
862
  };
863
+ const categoryCounts = (0, react.useMemo)(() => {
864
+ const counts = /* @__PURE__ */ new Map();
865
+ for (const plugin of plugins) if (plugin.category) counts.set(plugin.category, (counts.get(plugin.category) ?? 0) + 1);
866
+ return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
867
+ }, [plugins]);
868
+ const visiblePlugins = (0, react.useMemo)(() => {
869
+ const needle = query.trim().toLowerCase();
870
+ return plugins.filter((plugin) => {
871
+ if (category !== null && plugin.category !== category) return false;
872
+ if (!needle) return true;
873
+ const categoryLabel = plugin.category ? t(CATEGORY_KEY[plugin.category]) : "";
874
+ return [
875
+ plugin.name,
876
+ plugin.nameEn,
877
+ plugin.author,
878
+ plugin.description,
879
+ plugin.descriptionEn,
880
+ plugin.npm ?? "",
881
+ categoryLabel
882
+ ].join(" ").toLowerCase().includes(needle);
883
+ });
884
+ }, [
885
+ plugins,
886
+ query,
887
+ category,
888
+ t
889
+ ]);
695
890
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PluginSettingsCard, {
696
891
  t,
697
892
  titleKey: "settings.title",
@@ -717,68 +912,147 @@ window.__ModuleLoader__.load({
717
912
  props.resetField("enabled");
718
913
  }
719
914
  }),
720
- visible ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
721
- className: community_module_css_default.entries,
722
- children: plugins.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
723
- className: community_module_css_default.empty,
724
- role: "status",
725
- children: t("empty")
726
- }) : plugins.map((plugin) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
727
- className: community_module_css_default.entry,
728
- children: [
729
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
730
- className: community_module_css_default.entryHead,
731
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
732
- className: community_module_css_default.entryName,
733
- title: plugin.name,
734
- children: plugin.name
735
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
736
- className: community_module_css_default.entryAuthor,
737
- title: plugin.author,
915
+ visible ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
916
+ className: community_module_css_default.market,
917
+ children: [
918
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
919
+ className: community_module_css_default.toolbar,
920
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
921
+ id: "settings-community-search",
922
+ className: community_module_css_default.search,
923
+ type: "search",
924
+ placeholder: t("search.placeholder"),
925
+ "aria-label": t("search.label"),
926
+ value: query,
927
+ disabled,
928
+ onChange: (event) => {
929
+ setQuery(event.target.value);
930
+ }
931
+ })
932
+ }),
933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
934
+ className: community_module_css_default.filters,
935
+ role: "group",
936
+ "aria-label": t("filter.all"),
937
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
938
+ type: "button",
939
+ className: category === null ? `${community_module_css_default.pill} ${community_module_css_default.pillActive}` : community_module_css_default.pill,
940
+ "aria-pressed": category === null,
941
+ disabled,
942
+ onClick: () => {
943
+ setCategory(null);
944
+ },
945
+ children: [
946
+ t("filter.all"),
947
+ " ",
948
+ plugins.length
949
+ ]
950
+ }), categoryCounts.map(([id, count]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
951
+ type: "button",
952
+ className: category === id ? `${community_module_css_default.pill} ${community_module_css_default.pillActive}` : community_module_css_default.pill,
953
+ "aria-pressed": category === id,
954
+ disabled,
955
+ onClick: () => {
956
+ setCategory(category === id ? null : id);
957
+ },
958
+ children: [
959
+ t(CATEGORY_KEY[id]),
960
+ " ",
961
+ count
962
+ ]
963
+ }, id))]
964
+ }),
965
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
966
+ className: community_module_css_default.resultCount,
967
+ role: "status",
968
+ children: t("result.count", {
969
+ shown: visiblePlugins.length,
970
+ total: plugins.length
971
+ })
972
+ }),
973
+ plugins.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
974
+ className: community_module_css_default.empty,
975
+ role: "status",
976
+ children: t("empty")
977
+ }) : visiblePlugins.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
978
+ className: community_module_css_default.empty,
979
+ role: "status",
980
+ children: t("noMatch")
981
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
982
+ className: community_module_css_default.grid,
983
+ children: visiblePlugins.map((plugin) => {
984
+ const command = installCommand(plugin);
985
+ const copied = copiedId === plugin.id;
986
+ const name = entryCopy(t, `name.${plugin.id}`, plugin.nameEn || plugin.name);
987
+ const description = entryCopy(t, `desc.${plugin.id}`, plugin.descriptionEn ?? plugin.description ?? "");
988
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
989
+ className: community_module_css_default.card,
738
990
  children: [
739
- t("author"),
740
- ": ",
741
- plugin.author
991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
992
+ className: community_module_css_default.cardHead,
993
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
994
+ className: community_module_css_default.cardName,
995
+ title: name,
996
+ children: name
997
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
998
+ className: plugin.npm ? `${community_module_css_default.badge} ${community_module_css_default.badgePublished}` : community_module_css_default.badge,
999
+ title: plugin.npm ?? plugin.repo,
1000
+ children: plugin.npm ? t("badge.published") : t("badge.source")
1001
+ })]
1002
+ }),
1003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1004
+ className: community_module_css_default.cardMeta,
1005
+ children: [
1006
+ plugin.category ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1007
+ className: community_module_css_default.cardCategory,
1008
+ children: t(CATEGORY_KEY[plugin.category])
1009
+ }) : null,
1010
+ plugin.category ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1011
+ className: community_module_css_default.cardDot,
1012
+ "aria-hidden": "true",
1013
+ children: "·"
1014
+ }) : null,
1015
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1016
+ className: community_module_css_default.cardAuthor,
1017
+ title: plugin.author,
1018
+ children: plugin.author
1019
+ })
1020
+ ]
1021
+ }),
1022
+ description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1023
+ className: community_module_css_default.cardDescription,
1024
+ children: description
1025
+ }) : null,
1026
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1027
+ className: community_module_css_default.cardFooter,
1028
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1029
+ className: community_module_css_default.cardTop,
1030
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1031
+ className: community_module_css_default.cardLink,
1032
+ href: plugin.repo,
1033
+ target: "_blank",
1034
+ rel: "noreferrer",
1035
+ children: t("repository")
1036
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1037
+ type: "button",
1038
+ className: copied ? `${community_module_css_default.installButton} ${community_module_css_default.installButtonCopied}` : community_module_css_default.installButton,
1039
+ title: command,
1040
+ onClick: () => {
1041
+ copyCommand(plugin.id, command);
1042
+ },
1043
+ children: copied ? t("copied") : t("install")
1044
+ })]
1045
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1046
+ className: community_module_css_default.cardCommand,
1047
+ title: command,
1048
+ children: command
1049
+ })]
1050
+ })
742
1051
  ]
743
- })]
744
- }),
745
- plugin.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
746
- className: community_module_css_default.entryDescription,
747
- children: plugin.description
748
- }) : null,
749
- plugin.descriptionEn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
750
- className: community_module_css_default.entryDescriptionEn,
751
- children: plugin.descriptionEn
752
- }) : null,
753
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
754
- className: community_module_css_default.entryLinks,
755
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
756
- className: community_module_css_default.entryLink,
757
- href: plugin.repo,
758
- target: "_blank",
759
- rel: "noreferrer",
760
- children: t("repository")
761
- }), plugin.npm ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
762
- className: community_module_css_default.entryNpm,
763
- children: plugin.npm
764
- }) : null]
765
- }),
766
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
767
- className: community_module_css_default.entryInstall,
768
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
769
- className: community_module_css_default.entryCommand,
770
- children: installCommand(plugin)
771
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
772
- type: "button",
773
- className: community_module_css_default.copyButton,
774
- onClick: () => {
775
- copyCommand(plugin.id, installCommand(plugin));
776
- },
777
- children: copiedId === plugin.id ? t("copied") : t("copy")
778
- })]
1052
+ }, plugin.id);
779
1053
  })
780
- ]
781
- }, plugin.id))
1054
+ })
1055
+ ]
782
1056
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
783
1057
  className: community_module_css_default.off,
784
1058
  role: "status",
@@ -816,10 +1090,14 @@ window.__ModuleLoader__.load({
816
1090
  //#endregion
817
1091
  //#region src/client/locales.ts
818
1092
  /**
819
- * The community-plugins locale dictionaries for the index card.
1093
+ * The community-plugins locale dictionaries for the index card. Static copy
1094
+ * for the card chrome plus one localized name/description pair per index
1095
+ * entry (`name.<id>` / `desc.<id>`), so the card localizes data-driven text
1096
+ * through the same `t` the rest of the settings UI uses — the document
1097
+ * `<html lang>` attribute is not a reliable locale signal in the GUI.
820
1098
  */
821
- /** Simplified Chinese dictionary (the key-set source of truth). */
822
- const zh = {
1099
+ /** Simplified Chinese copy for the card chrome (the key-set source of truth). */
1100
+ const STATIC_ZH = {
823
1101
  "settings.title": "社区插件",
824
1102
  "settings.description": "社区贡献者开发与维护的插件,链接指向作者自己的仓库。",
825
1103
  "settings.enabled": "启用社区插件索引",
@@ -843,13 +1121,28 @@ window.__ModuleLoader__.load({
843
1121
  "repository": "仓库",
844
1122
  "copy": "复制",
845
1123
  "copied": "已复制",
1124
+ "install": "复制安装命令",
1125
+ "noMatch": "没有匹配的社区插件。",
1126
+ "search.placeholder": "搜索名称、作者或简介",
1127
+ "search.label": "搜索社区插件",
1128
+ "filter.all": "全部",
1129
+ "result.count": "显示 {shown} / {total} 个",
1130
+ "badge.published": "npm 已发布",
1131
+ "badge.source": "仓库安装",
1132
+ "category.ui": "界面与体验",
1133
+ "category.agent": "Agent 与自动化",
1134
+ "category.tools": "开发者工具",
1135
+ "category.knowledge": "记忆与知识",
1136
+ "category.integration": "集成与分享",
1137
+ "category.security": "安全与运维",
1138
+ "category.utility": "实用工具",
846
1139
  "installHint": "索引只登记、不安装代码。要运行某个插件,复制它的安装命令到终端执行;安装后,插件自带的开关与配置(若有)会出现在插件配置里。",
847
1140
  "empty": "暂无社区插件登记。",
848
1141
  "off": "社区插件索引已关闭。",
849
1142
  "notice": "条目由贡献者自行登记,与 dsh-web-ui 的发布内容无关;使用前请自行评估。"
850
1143
  };
851
- /** English dictionary, checked complete against the zh key set. */
852
- const en = {
1144
+ /** English copy for the card chrome, checked complete against the zh key set. */
1145
+ const STATIC_EN = {
853
1146
  "settings.title": "Community Plugins",
854
1147
  "settings.description": "Plugins developed and maintained by community contributors, linking to each author's own repository.",
855
1148
  "settings.enabled": "Enable the community plugin index",
@@ -873,11 +1166,39 @@ window.__ModuleLoader__.load({
873
1166
  "repository": "Repository",
874
1167
  "copy": "Copy",
875
1168
  "copied": "Copied",
1169
+ "install": "Copy install command",
1170
+ "noMatch": "No matching community plugin found.",
1171
+ "search.placeholder": "Search name, author or description",
1172
+ "search.label": "Search community plugins",
1173
+ "filter.all": "All",
1174
+ "result.count": "Showing {shown} / {total}",
1175
+ "badge.published": "Published on npm",
1176
+ "badge.source": "Install from repo",
1177
+ "category.ui": "UI & Experience",
1178
+ "category.agent": "Agents & Automation",
1179
+ "category.tools": "Developer Tools",
1180
+ "category.knowledge": "Memory & Knowledge",
1181
+ "category.integration": "Integration & Sharing",
1182
+ "category.security": "Security & Ops",
1183
+ "category.utility": "Utilities",
876
1184
  "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.",
877
1185
  "empty": "No community plugins registered yet.",
878
1186
  "off": "The community plugin index is turned off.",
879
1187
  "notice": "Entries are contributed by their authors and are separate from dsh-web-ui releases; evaluate before use."
880
1188
  };
1189
+ /** Build the runtime dictionary: static copy plus one localized name/description pair per index entry. */
1190
+ function build(base, lang) {
1191
+ const dict = { ...base };
1192
+ for (const entry of COMMUNITY_PLUGINS) {
1193
+ dict[`name.${entry.id}`] = lang === "zh" ? entry.name : entry.nameEn;
1194
+ dict[`desc.${entry.id}`] = lang === "zh" ? entry.description ?? entry.descriptionEn ?? "" : entry.descriptionEn ?? entry.description ?? "";
1195
+ }
1196
+ return dict;
1197
+ }
1198
+ /** Simplified Chinese dictionary for the index card (chrome + per-entry copy). */
1199
+ const zh = build(STATIC_ZH, "zh");
1200
+ /** English dictionary, built from the same key set. */
1201
+ const en = build(STATIC_EN, "en");
881
1202
  //#endregion
882
1203
  //#region src/client/index.ts
883
1204
  /** Settings namespace the card's enable switch edits (the Host plugin registers it). */
@@ -901,14 +1222,20 @@ window.__ModuleLoader__.load({
901
1222
  en
902
1223
  }), "community-plugins: dictionaries");
903
1224
  const controller = new CommunityPluginsCardController((ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: COMMUNITY_PLUGINS_NS }));
904
- ctx.slots.inject("settings.section", () => ctx.slots.register({
905
- name: "settings.section",
906
- id: "community-plugins",
907
- order: 140,
908
- label: () => ctx.locale.bind("community-plugins")("settings.title"),
909
- locale: "community-plugins",
910
- inject: () => controller.inject()
911
- }, CommunityPluginsSection));
1225
+ ctx.slots.inject("settings.section", () => {
1226
+ const unregister = ctx.slots.register({
1227
+ name: "settings.section",
1228
+ id: "community-plugins",
1229
+ order: 140,
1230
+ label: () => ctx.locale.bind("community-plugins")("settings.title"),
1231
+ locale: "community-plugins",
1232
+ inject: () => controller.inject()
1233
+ }, CommunityPluginsSection);
1234
+ return () => {
1235
+ controller.dispose();
1236
+ unregister();
1237
+ };
1238
+ });
912
1239
  }
913
1240
  //#endregion
914
1241
  exports.apply = apply;