dsh-remote-bridge 2.6.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.cjs CHANGED
@@ -236,6 +236,33 @@ window.__ModuleLoader__.load({
236
236
  body: JSON.stringify({ subdir })
237
237
  });
238
238
  }
239
+ /**
240
+ * 读插件清单。
241
+ *
242
+ * `checkUpdates` 默认关:版本检查要打 npm registry,十次里九次用户只想看「装了啥」。
243
+ * 面板上那个「检查更新」按钮才把它打开。
244
+ */
245
+ async pluginInventory(checkUpdates = false) {
246
+ return this.json(`/settings/plugins${checkUpdates ? "?checkUpdates=1" : ""}`);
247
+ }
248
+ /**
249
+ * 启用 / 停用插件。
250
+ *
251
+ * 只对 `toggleable` 的条目有效;官方包(`@deepseek-ai/`)会被宿主拒绝并回
252
+ * `PLUGIN_OFFICIAL_IMMUTABLE` —— 它们随 DSH 运行时安装,改 bundles 也加载不出来。
253
+ *
254
+ * 返回 `restartRequired: true` 是常态:bundles 只在启动时读一次,改了文件不等于
255
+ * 改了运行中的进程。界面必须把这句话说出来,别让用户以为点了没反应。
256
+ */
257
+ async togglePlugin(name, loaded) {
258
+ return this.json("/settings/plugins", {
259
+ method: "PATCH",
260
+ body: JSON.stringify({
261
+ name,
262
+ loaded
263
+ })
264
+ });
265
+ }
239
266
  async json(path, init = {}) {
240
267
  const response = await this.fetch(path, init);
241
268
  await ensureOk(response);
@@ -19567,7 +19594,7 @@ window.__ModuleLoader__.load({
19567
19594
  key: "im3w4b"
19568
19595
  }]
19569
19596
  ]);
19570
- createLucideIcon("Package", [
19597
+ const Package = createLucideIcon("Package", [
19571
19598
  ["path", {
19572
19599
  d: "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z",
19573
19600
  key: "1a0edw"
@@ -21689,7 +21716,7 @@ window.__ModuleLoader__.load({
21689
21716
  key: "1r8tn9"
21690
21717
  }]
21691
21718
  ]);
21692
- createLucideIcon("RefreshCw", [
21719
+ const RefreshCw = createLucideIcon("RefreshCw", [
21693
21720
  ["path", {
21694
21721
  d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
21695
21722
  key: "v9h5vc"
@@ -29804,6 +29831,8 @@ window.__ModuleLoader__.load({
29804
29831
  exports.HardDrive = HardDrive;
29805
29832
  exports.Image = Image;
29806
29833
  exports.Languages = Languages;
29834
+ exports.Package = Package;
29835
+ exports.RefreshCw = RefreshCw;
29807
29836
  exports.Smartphone = Smartphone;
29808
29837
  exports.Trash2 = Trash2;
29809
29838
  exports.Upload = Upload;
@@ -29951,6 +29980,34 @@ window.__ModuleLoader__.load({
29951
29980
  pairedDevices: "已配对设备",
29952
29981
  pluginTrash: "插件回收站",
29953
29982
  pluginTrashHint: "这里只列出来自手机 App 或独立文件页(/dsh-workspace)的删除记录 —— 桌面端这个面板不含文件树,无法删除文件。文件先移到这里而不是直接消失,可以在这里恢复;永久清理也由你在此发起。",
29983
+ pluginsTab: "插件",
29984
+ pluginsTabDesc: "看装了哪些插件、启用或停用,以及检查更新。",
29985
+ pluginsHint: "这里列出这台机器 profile 里装的所有插件。官方插件随 DSH 运行时安装、不能停用;其余可以在这里开关,改完要重启 DSH 才生效。",
29986
+ pluginsEmpty: "这个 profile 里没有插件。",
29987
+ pluginsUnavailable: "读不到插件清单:{reason}",
29988
+ pluginsProblems: "{count} 项需要留意",
29989
+ pluginsCheckUpdates: "检查更新",
29990
+ pluginsChecking: "正在检查…",
29991
+ pluginsOpenList: "打开插件列表",
29992
+ pluginsOpenListUnavailable: "当前 DSH 版本还不支持直接跳转插件列表。",
29993
+ pluginsEnable: "启用",
29994
+ pluginsDisable: "停用",
29995
+ pluginsEnabled: "已启用",
29996
+ pluginsDisabled: "已停用",
29997
+ pluginsToggled: "{name} {state}。",
29998
+ pluginsRestartRequired: "改动已写进 profile 的 package.json(原文件已备份)。插件列表只在 DSH 启动时读一次,重启后生效。",
29999
+ pluginsStateLoaded: "已加载",
30000
+ pluginsStateRuntimeProvided: "运行时自带",
30001
+ pluginsStateInstalledNotLoaded: "已安装未启用",
30002
+ pluginsStateDeclaredMissing: "声明加载但缺失",
30003
+ pluginsUpdateAvailable: "可更新到 {version}",
30004
+ pluginsLatest: "已是最新",
30005
+ pluginsNoDescription: "这个包没写描述",
30006
+ pluginsErrorOfficial: "官方插件随 DSH 运行时安装,不能在这里停用。",
30007
+ pluginsErrorNameInvalid: "插件名不合法。",
30008
+ pluginsErrorProfileUnknown: "找不到当前 profile 的目录,无法修改插件列表。",
30009
+ pluginsErrorManifestUnreadable: "读不懂 profile 的 package.json,为安全起见没有改动它。",
30010
+ pluginsErrorNotInstalled: "这个插件没有装进 profile,先安装再启用。",
29954
30011
  restore: "恢复",
29955
30012
  deletePermanently: "永久删除",
29956
30013
  purgeConfirm: "确定永久删除此回收项吗?此操作无法撤销。",
@@ -30182,6 +30239,34 @@ window.__ModuleLoader__.load({
30182
30239
  pairedDevices: "Paired devices",
30183
30240
  pluginTrash: "Plugin trash",
30184
30241
  pluginTrashHint: "Only deletions made from the phone app or the standalone file page (/dsh-workspace) show up here — this desktop panel has no file tree and cannot delete anything. Files land here instead of vanishing, so they can be restored; permanent cleanup is also started from here.",
30242
+ pluginsTab: "Plugins",
30243
+ pluginsTabDesc: "See what is installed, enable or disable it, and check for updates.",
30244
+ pluginsHint: "Every plugin installed in this machine's profile is listed here. Official plugins ship with the DSH runtime and cannot be disabled; the rest can be toggled — changes take effect after DSH restarts.",
30245
+ pluginsEmpty: "No plugins in this profile.",
30246
+ pluginsUnavailable: "Cannot read the plugin inventory: {reason}",
30247
+ pluginsProblems: "{count} need attention",
30248
+ pluginsCheckUpdates: "Check for updates",
30249
+ pluginsChecking: "Checking…",
30250
+ pluginsOpenList: "Open plugin list",
30251
+ pluginsOpenListUnavailable: "This DSH version cannot jump to the plugin list yet.",
30252
+ pluginsEnable: "Enable",
30253
+ pluginsDisable: "Disable",
30254
+ pluginsEnabled: "enabled",
30255
+ pluginsDisabled: "disabled",
30256
+ pluginsToggled: "{name} {state}.",
30257
+ pluginsRestartRequired: "The change was written to the profile package.json (the original is backed up). The plugin list is read once at DSH startup, so restart to apply it.",
30258
+ pluginsStateLoaded: "Loaded",
30259
+ pluginsStateRuntimeProvided: "Shipped with the runtime",
30260
+ pluginsStateInstalledNotLoaded: "Installed, not loaded",
30261
+ pluginsStateDeclaredMissing: "Declared but missing",
30262
+ pluginsUpdateAvailable: "Update to {version}",
30263
+ pluginsLatest: "Up to date",
30264
+ pluginsNoDescription: "This package has no description",
30265
+ pluginsErrorOfficial: "Official plugins ship with the DSH runtime and cannot be disabled here.",
30266
+ pluginsErrorNameInvalid: "That plugin name is not valid.",
30267
+ pluginsErrorProfileUnknown: "The profile directory could not be found, so the plugin list was left untouched.",
30268
+ pluginsErrorManifestUnreadable: "The profile package.json could not be parsed, so it was left untouched.",
30269
+ pluginsErrorNotInstalled: "That plugin is not installed in this profile — install it before enabling it.",
30185
30270
  restore: "Restore",
30186
30271
  deletePermanently: "Delete permanently",
30187
30272
  purgeConfirm: "Permanently delete this trash item? This cannot be undone.",
@@ -30826,82 +30911,6 @@ window.__ModuleLoader__.load({
30826
30911
  }
30827
30912
  };
30828
30913
  //#endregion
30829
- //#region src/client/styles.ts
30830
- const STYLE_ID = "dsh-workspace-style";
30831
- /**
30832
- * 插件的全部样式。
30833
- *
30834
- * 导出出来是为了让测试能守住一条容易失守的不变量:**CSS 变量块必须同时覆盖
30835
- * `.daw-root` 与 `.daw-settings-section`**。
30836
- *
30837
- * 这条防线来自一个真实发生过的隐形 bug:设置页内联渲染时,那一节没有 `.daw-root` 祖先,
30838
- * 于是 `var(--daw-accent)` 解析失败 → `.daw-command.primary` 的 `background` 失效变透明、
30839
- * 而 `color` 仍是 `#fff` → **白字落在白底上,按钮彻底看不见**。
30840
- * 类型检查、单测、构建全都发现不了,只有肉眼看渲染或量计算样式才抓得到。
30841
- */
30842
- const WORKSPACE_CSS = `
30843
- .daw-root,.daw-settings-section{--daw-bg:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-2,#f7f8fa));--daw-panel:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));--daw-ink:var(--dsw-alias-label-primary,#18202a);--daw-muted:var(--dsw-alias-label-tertiary,#687381);--daw-line:var(--dsw-alias-border-l2,#dfe4e8);--daw-accent:var(--dsw-alias-brand-primary,#087f8c);--daw-accent-soft:var(--dsw-alias-bg-module-platform,#e5f5f6);--daw-good:var(--dsw-alias-state-success-primary,#2f7d4a);--daw-danger:var(--dsw-alias-state-error-primary,#b33a3a);color:var(--daw-ink);background:transparent;font:13px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:0;min-width:0}
30844
- .daw-root *,.daw-settings-section *{box-sizing:border-box;letter-spacing:0}
30845
- /* 输入控件基线(UI-04):此前只有 :focus-visible 一条,非聚焦态全靠浏览器默认(2px inset/Arial/灰底),
30846
- 深色令牌下实测白底黑字与面板割裂。这里给齐边框/底/字/内边距,聚焦态只换描边。 */
30847
- .daw-input{width:100%;min-width:0;height:30px;padding:4px 9px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit}
30848
- .daw-input::placeholder{color:var(--dsw-alias-label-tertiary,var(--daw-muted))}
30849
- .daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}
30850
- .daw-input:disabled{opacity:.55;cursor:not-allowed}.daw-icon{width:30px;padding:0}
30851
- .daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
30852
- .daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
30853
- .daw-icon:disabled{opacity:.45;cursor:not-allowed}
30854
- /* 普通命令按钮基线(UI-04):旧版写死 background:#fff/border:#cbd2d8,深色令牌下成了白块黑字。
30855
- 全部改走 DSH 令牌;.primary 沿用面板底色做次按钮、真正的主操作由调用方加 .primary 配合。 */
30856
- .daw-command{height:30px;padding:0 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit;cursor:pointer}
30857
- .daw-command:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
30858
- .daw-command:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:1px}
30859
- /* 禁用态(UI-05):此前无任何 :disabled 样式,只剩浏览器默认的 30% 文字且仍是箭头手型。 */
30860
- .daw-command:disabled{opacity:.5;cursor:not-allowed}
30861
- .daw-command.primary{color:var(--dsw-alias-label-primary,var(--daw-ink));background:var(--dsw-alias-bg-module-platform,var(--daw-panel));border-color:var(--dsw-alias-border-l2,var(--daw-line));font-weight:500}
30862
- .daw-command.primary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
30863
- .daw-command.danger{color:var(--daw-danger)}
30864
- .daw-command.danger:hover:not(:disabled){border-color:var(--daw-danger)}
30865
- /* 图标+文字的小复制按钮:图标和文字对齐,窄屏只露图标也不改变按钮高度。 */
30866
- .daw-command.icon-as-command{display:inline-flex;align-items:center;gap:6px;padding:0 10px;white-space:nowrap}
30867
- .daw-copy-label{font-size:12px}
30868
- .daw-empty{padding:14px 2px;text-align:center}
30869
- .daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
30870
- .daw-card{border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
30871
- .daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
30872
- .daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
30873
- .daw-card-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}
30874
- .daw-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:-2px}
30875
- .daw-card-icon{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;display:flex;align-items:center}
30876
- .daw-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
30877
- .daw-card-name{color:var(--dsw-alias-label-primary,var(--daw-ink));font-size:15px;font-weight:600;line-height:1.4}
30878
- .daw-card-desc{color:var(--dsw-alias-label-tertiary,var(--daw-muted));font-size:13px;line-height:1.5}
30879
- .daw-card-chevron{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;transition:transform .16s;font-size:12px;line-height:1}
30880
- .daw-card-chevron-open{transform:rotate(180deg)}
30881
- .daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
30882
- .daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
30883
- .daw-builtin-hint{text-align:right;align-self:center}
30884
- .daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-command{margin-left:8px}
30885
- /* 成功反馈条(UI-07):与 .daw-warning 同形,但走成功色,操作成功不再静默。 */
30886
- .daw-success{flex:none;border:1px solid var(--dsw-alias-state-success-primary,var(--daw-good));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--dsw-alias-state-success-primary,var(--daw-good));margin-bottom:0}
30887
- .daw-admin-topbar{display:flex;justify-content:flex-end;margin-bottom:8px}
30888
- .daw-url-row{display:flex;align-items:center;gap:8px;margin-top:8px}.daw-url-row .daw-tunnel-url{flex:1 1 auto;margin:0;min-width:0}
30889
- .daw-code-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0}.daw-code-row .daw-code{margin:0;flex:1 1 220px;min-width:0}.daw-copy-inline{flex:none}
30890
- /* 隧道/监听进行中的流动条(UI-08):percent=null 时铺一段高光在轨道里循环滑动,
30891
- 不再把「未知进度」伪装成恒定 30%。 */
30892
- .daw-progress-fill.indeterminate{width:40%;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line))),transparent);animation:daw-indeterminate 1.1s ease-in-out infinite}
30893
- @keyframes daw-indeterminate{0%{transform:translateX(-120%)}100%{transform:translateX(280%)}}
30894
- @media (max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
30895
- `;
30896
- function installWorkspaceStyles() {
30897
- if (document.getElementById(STYLE_ID) !== null) return;
30898
- const style = document.createElement("style");
30899
- style.id = STYLE_ID;
30900
- style.dataset.plugin = "dsh-workspace";
30901
- style.textContent = WORKSPACE_CSS;
30902
- document.head.appendChild(style);
30903
- }
30904
- //#endregion
30905
30914
  //#region src/shared/contracts.ts
30906
30915
  const DEVICE_SCOPES = [
30907
30916
  "chat.read",
@@ -31006,6 +31015,7 @@ window.__ModuleLoader__.load({
31006
31015
  devices: "devicesTabDesc",
31007
31016
  image: "imageTabDesc",
31008
31017
  trash: "trashTabDesc",
31018
+ plugins: "pluginsTabDesc",
31009
31019
  "app-download": "appDownloadTabDesc"
31010
31020
  };
31011
31021
  function tabDescKey(tab) {
@@ -31048,7 +31058,8 @@ window.__ModuleLoader__.load({
31048
31058
  function PhoneSettingsBody(props) {
31049
31059
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminPanel, {
31050
31060
  api: props.api,
31051
- initialOpen: props.section
31061
+ initialOpen: props.section,
31062
+ onOpenPluginList: props.onOpenPluginList
31052
31063
  });
31053
31064
  }
31054
31065
  /**
@@ -31956,6 +31967,12 @@ window.__ModuleLoader__.load({
31956
31967
  }, item.id))
31957
31968
  })]
31958
31969
  }, "trash"),
31970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginsSection, {
31971
+ api: props.api,
31972
+ open: isOpen("plugins"),
31973
+ onToggle: () => toggleCard("plugins"),
31974
+ onOpenPluginList: props.onOpenPluginList
31975
+ }),
31959
31976
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDownloadSection, {
31960
31977
  open: isOpen("app-download"),
31961
31978
  onToggle: () => toggleCard("app-download")
@@ -31964,6 +31981,190 @@ window.__ModuleLoader__.load({
31964
31981
  });
31965
31982
  }
31966
31983
  /**
31984
+ * 插件卡片:这台机器 profile 里装了哪些插件、能不能停用、有没有更新。
31985
+ *
31986
+ * ## 为什么加这张卡
31987
+ *
31988
+ * 用户的反馈是「功能单调点,无法查看装的插件情况」—— 想知道装了哪些插件、有没有出问题的,
31989
+ * 必须回到电脑前的 WebUI。宿主的 `GET /settings/plugins` 与 `PATCH /settings/plugins` 早就有,
31990
+ * 只是浏览器半边从来没有消费它们;这张卡补的就是那半截。
31991
+ *
31992
+ * ## 三个必须说清楚的地方
31993
+ *
31994
+ * 1. **官方包不能停**(`toggleable === false`)。它们随 DSH 运行时安装、不由 profile 管理,
31995
+ * 改 bundles 也加载不出来。服务端会拒绝并回 `PLUGIN_OFFICIAL_IMMUTABLE`,这里干脆不给按钮。
31996
+ * 2. **`runtime-provided` 不是故障**。官方包出现在 `dsh.profile.bundles` 里但不在 profile 的
31997
+ * node_modules 下,早期实现把它误判成「声明加载但缺失」,真机上一次冒出 5 条假警报。
31998
+ * 3. **改完要重启**。bundles 只在启动时读一次,所以成功提示不是「已停用」而是
31999
+ * 「已写进 package.json,重启后生效」—— 否则用户会以为点了没反应,再点几次。
32000
+ *
32001
+ * ## 更新检查为什么是按钮而不是自动
32002
+ *
32003
+ * 它要打 npm registry,十次里九次用户只想看「装了啥」。默认不查,`updateAvailable` 因此是
32004
+ * 三态(`null` = 没查),而不是压成 `false` 去谎称「已是最新」。
32005
+ */
32006
+ function PluginsSection(props) {
32007
+ const { t } = useWorkspaceI18n();
32008
+ const [inventory, setInventory] = (0, react.useState)(void 0);
32009
+ const [busy, setBusy] = (0, react.useState)(false);
32010
+ const [checking, setChecking] = (0, react.useState)(false);
32011
+ const [toggling, setToggling] = (0, react.useState)(void 0);
32012
+ const [error, setError] = (0, react.useState)(void 0);
32013
+ const [restartRequired, setRestartRequired] = (0, react.useState)(false);
32014
+ const [openListMissing, setOpenListMissing] = (0, react.useState)(false);
32015
+ const load = (0, react.useCallback)(async (checkUpdates) => {
32016
+ if (checkUpdates) setChecking(true);
32017
+ else setBusy(true);
32018
+ setError(void 0);
32019
+ try {
32020
+ setInventory(await props.api.pluginInventory(checkUpdates));
32021
+ } catch (cause) {
32022
+ setError(messageOf(cause, t));
32023
+ } finally {
32024
+ if (checkUpdates) setChecking(false);
32025
+ else setBusy(false);
32026
+ }
32027
+ }, [props.api, t]);
32028
+ (0, react.useEffect)(() => {
32029
+ if (!props.open || inventory !== void 0) return;
32030
+ load(false);
32031
+ }, [
32032
+ props.open,
32033
+ inventory,
32034
+ load
32035
+ ]);
32036
+ const toggle = (0, react.useCallback)(async (entry) => {
32037
+ setToggling(entry.name);
32038
+ setError(void 0);
32039
+ try {
32040
+ await props.api.togglePlugin(entry.name, !entry.loaded);
32041
+ setInventory(await props.api.pluginInventory(false));
32042
+ setRestartRequired(true);
32043
+ } catch (cause) {
32044
+ setError(messageOf(cause, t));
32045
+ } finally {
32046
+ setToggling(void 0);
32047
+ }
32048
+ }, [props.api, t]);
32049
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
32050
+ tab: "plugins",
32051
+ nameKey: tabLabelKey("plugins"),
32052
+ descKey: tabDescKey("plugins"),
32053
+ open: props.open,
32054
+ onToggle: props.onToggle,
32055
+ children: [
32056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
32057
+ className: "daw-list-meta",
32058
+ children: t("pluginsHint")
32059
+ }),
32060
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32061
+ className: "daw-warning",
32062
+ role: "alert",
32063
+ children: error
32064
+ }),
32065
+ restartRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32066
+ className: "daw-success",
32067
+ role: "status",
32068
+ children: t("pluginsRestartRequired")
32069
+ }),
32070
+ openListMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32071
+ className: "daw-warning",
32072
+ role: "status",
32073
+ children: t("pluginsOpenListUnavailable")
32074
+ }),
32075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32076
+ className: "daw-form-row",
32077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
32078
+ type: "button",
32079
+ className: "daw-command",
32080
+ disabled: checking,
32081
+ onClick: () => {
32082
+ load(true);
32083
+ },
32084
+ children: [
32085
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.RefreshCw, { size: 14 }),
32086
+ " ",
32087
+ checking ? t("pluginsChecking") : t("pluginsCheckUpdates")
32088
+ ]
32089
+ }), props.onOpenPluginList !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32090
+ type: "button",
32091
+ className: "daw-command",
32092
+ onClick: () => {
32093
+ setOpenListMissing(props.onOpenPluginList?.() === false);
32094
+ },
32095
+ children: t("pluginsOpenList")
32096
+ })]
32097
+ }),
32098
+ inventory === void 0 ? busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32099
+ className: "daw-list-meta",
32100
+ children: t("loading")
32101
+ }) : null : !inventory.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32102
+ className: "daw-list-meta",
32103
+ children: t("pluginsUnavailable", { reason: inventory.reason ?? "" })
32104
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32105
+ className: "daw-list-meta",
32106
+ children: [
32107
+ inventory.profile !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [inventory.profile, " · "] }),
32108
+ inventory.loadedCount,
32109
+ " / ",
32110
+ inventory.items.length,
32111
+ inventory.problemCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsProblems", { count: inventory.problemCount })] })
32112
+ ]
32113
+ }), inventory.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32114
+ className: "daw-list-meta daw-empty",
32115
+ children: t("pluginsEmpty")
32116
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32117
+ className: "daw-list",
32118
+ children: inventory.items.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32119
+ className: "daw-list-row",
32120
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
32121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32122
+ className: "daw-list-title",
32123
+ children: [entry.name, entry.installed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
32124
+ className: "daw-list-meta",
32125
+ children: [" · ", entry.installed]
32126
+ })]
32127
+ }),
32128
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32129
+ className: "daw-list-meta",
32130
+ children: entry.description ?? t("pluginsNoDescription")
32131
+ }),
32132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32133
+ className: "daw-list-meta",
32134
+ children: [
32135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32136
+ className: `daw-pill${entry.loaded ? " on" : ""}`,
32137
+ children: t(pluginStateKey(entry.state))
32138
+ }),
32139
+ entry.updateAvailable === true && entry.latestVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32140
+ className: "daw-pill",
32141
+ children: t("pluginsUpdateAvailable", { version: entry.latestVersion })
32142
+ })] }),
32143
+ entry.updateAvailable === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsLatest")] })
32144
+ ]
32145
+ })
32146
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entry.toggleable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32147
+ type: "button",
32148
+ className: "daw-command",
32149
+ disabled: toggling === entry.name,
32150
+ onClick: () => {
32151
+ toggle(entry);
32152
+ },
32153
+ children: entry.loaded ? t("pluginsDisable") : t("pluginsEnable")
32154
+ }) })]
32155
+ }, entry.name))
32156
+ })] })
32157
+ ]
32158
+ });
32159
+ }
32160
+ /** 四种 `PluginState` 各自的中文/英文说法 —— 状态本身是产品语义,不该在界面上露出裸码。 */
32161
+ function pluginStateKey(state) {
32162
+ if (state === "loaded") return "pluginsStateLoaded";
32163
+ if (state === "runtime-provided") return "pluginsStateRuntimeProvided";
32164
+ if (state === "installed-not-loaded") return "pluginsStateInstalledNotLoaded";
32165
+ return "pluginsStateDeclaredMissing";
32166
+ }
32167
+ /**
31967
32168
  * The App download page: one QR card per release host.
31968
32169
  *
31969
32170
  * The codes come from `APP_QR_CODES` as inlined SVG data URLs, so this page makes **no network
@@ -32261,6 +32462,7 @@ window.__ModuleLoader__.load({
32261
32462
  if (tab === "devices") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Smartphone, { size: 15 });
32262
32463
  if (tab === "image") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Image, { size: 15 });
32263
32464
  if (tab === "trash") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 });
32465
+ if (tab === "plugins") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Package, { size: 15 });
32264
32466
  if (tab === "remote") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Upload, { size: 15 });
32265
32467
  if (tab === "app-download") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Download, { size: 15 });
32266
32468
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.File, { size: 15 });
@@ -32295,6 +32497,11 @@ window.__ModuleLoader__.load({
32295
32497
  if (error.code === "ADMIN_PROXY_FORWARDED") return "errorAdminProxyForwarded";
32296
32498
  if (error.code === "ADMIN_HOST_NOT_LOOPBACK") return "errorAdminHostNotLoopback";
32297
32499
  if (error.code === "ADMIN_LOOPBACK_REQUIRED") return "errorAdminLoopbackRequired";
32500
+ if (error.code === "PLUGIN_OFFICIAL_IMMUTABLE") return "pluginsErrorOfficial";
32501
+ if (error.code === "PLUGIN_NAME_INVALID") return "pluginsErrorNameInvalid";
32502
+ if (error.code === "PROFILE_UNKNOWN") return "pluginsErrorProfileUnknown";
32503
+ if (error.code === "MANIFEST_UNREADABLE") return "pluginsErrorManifestUnreadable";
32504
+ if (error.code === "PLUGIN_NOT_INSTALLED") return "pluginsErrorNotInstalled";
32298
32505
  if (error.code.includes("PATH") || error.code.includes("SYMLINK") || error.code.includes("REPARSE")) return "errorPathInvalid";
32299
32506
  if (error.status === 403) return "errorForbidden";
32300
32507
  if (error.status === 404) return "errorNotFound";
@@ -32309,9 +32516,329 @@ window.__ModuleLoader__.load({
32309
32516
  return new Promise((resolve) => setTimeout(resolve, ms));
32310
32517
  }
32311
32518
  //#endregion
32519
+ //#region src/client/plugins.tsx
32520
+ /**
32521
+ * 插件清单的右侧栏形态:一个真正的 `SidebarRightTabDefinition`。
32522
+ *
32523
+ * ## 为什么不是「再注册一个 slot」
32524
+ *
32525
+ * 报告里第 2 项讲的是 DSH 0.2.1-rc.3 的三处 pin 机制(`view-pins` / `panel-pins` /
32526
+ * `guide-pins`)。pin 只作用于**注册了 panel / view / guide 的插件**——桥原来那两个座位
32527
+ * (`conversation.chat.turnTail` 与 `settings.section`)都不在这三者的管辖内,所以哪怕把
32528
+ * 三个新 slot 全注册一遍,桥自己的东西也一个都 pin 不到,只能消费别人的条目。
32529
+ *
32530
+ * 要真拿到 pin,唯一的路是**成为 tab 类型提供方**:往 `sidebarRightTabs` 注册一个
32531
+ * definition,并在 `sidebar.right.pane.tab` / `.title` 两个 keyed seat 上按 `definition.id`
32532
+ * 挂上 body 与标题。这样 guide 页会列出这张卡片,用户能在 rc.3 的 More 菜单里把它
32533
+ * 固定 / 取消固定(`guide-pins.d.ts` 的 `FIXED_GUIDE_KIND = "files"` 只豁免文件卡片,
32534
+ * 桥的 kind 不在豁免名单里)。
32535
+ *
32536
+ * ## 为什么单独一个文件
32537
+ *
32538
+ * `workspace.tsx` 已经 1200 多行,而 `docs/project/TECH-DEBT.md` 的行数预算写的是
32539
+ * 600 行/文件、超预算要先登记。把新 UI 面拆出来既不加剧那笔债,也让「设置页里的折叠卡」
32540
+ * 与「右侧栏的常驻 tab」这两种形态各自独立演进。
32541
+ *
32542
+ * ## rc.2 上的降级
32543
+ *
32544
+ * `sidebarRightTabs` 在 rc.2 与 rc.3 都提供,definition 契约两版一致,所以 tab 本身能用;
32545
+ * 但 rc.2 的 `GuideBody` 没有 `entryPins` / `entryActions` / More 菜单,卡片会正常列出、
32546
+ * 点开也正常,只是**不能 pin**。这是可接受的降级:能力随宿主版本生长,桥不做版本判断。
32547
+ */
32548
+ /** definition 的 id:tab 系统的实现身份,同时是 body/title 两个 keyed seat 的 key。 */
32549
+ const PLUGINS_TAB_ID = "dsh-remote-bridge-plugins";
32550
+ /**
32551
+ * tab 类型名。
32552
+ *
32553
+ * 必须与 DSH 内置类型不冲突 —— 内置的有 `guide` / `files` / `terminal` / `browser` /
32554
+ * `plan` / `schedule` / `deliverables` 等。冲突时 `sidebarRightTabs.register` 会抛
32555
+ * `sidebarRight: tab kind "…" is already registered`,整个插件起不来,所以带上插件前缀。
32556
+ */
32557
+ const PLUGINS_TAB_KIND = "remote-bridge-plugins";
32558
+ /**
32559
+ * 构造 tab 类型定义。
32560
+ *
32561
+ * 文案用 thunk(`() => string`)而不是现取的字符串:契约里写明 thunk 每次使用都会重读,
32562
+ * 所以切换语言不需要重新注册。`title` 是 tab chip 的初始文字,开 tab 的那一刻被捕获进
32563
+ * layout record。
32564
+ *
32565
+ * `patterns` 故意省略 —— 省略即「page type」,只能按 kind 打开(guide 卡片点一下就是这条路),
32566
+ * 不会被文件地址意外匹配到。`priority` 用默认的 `extension`。
32567
+ */
32568
+ function pluginsTabDefinition() {
32569
+ const label = () => translate(getWorkspaceLocale(), "pluginsTab");
32570
+ const description = () => translate(getWorkspaceLocale(), "pluginsTabDesc");
32571
+ return {
32572
+ id: PLUGINS_TAB_ID,
32573
+ kind: PLUGINS_TAB_KIND,
32574
+ title: label,
32575
+ guide: [{
32576
+ id: "plugins",
32577
+ order: 40,
32578
+ title: label,
32579
+ description,
32580
+ icon: import_lucide_react.Package
32581
+ }]
32582
+ };
32583
+ }
32584
+ /**
32585
+ * tab chip 里的文字。
32586
+ *
32587
+ * 不读 `useTabInfo` 是刻意的:标题在开 tab 时就被捕获了,跟着当前语言走没有意义;
32588
+ * 而且 rc.2 / rc.3 的 hook 注入形状不同,少依赖一处就少一处版本耦合。
32589
+ */
32590
+ function PluginsTabTitle() {
32591
+ const { t } = useWorkspaceI18n();
32592
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("pluginsTab") });
32593
+ }
32594
+ /**
32595
+ * 右侧栏里的插件清单。
32596
+ *
32597
+ * 与设置页那张折叠卡读同一对端点(`GET/PATCH /settings/plugins`),但形态不同:
32598
+ * 这里是**常驻面板**,没有卡片外壳,清单直接铺开;刷新按钮常驻而不是「展开才拉」——
32599
+ * 用户既然把这张卡 pin 到了 guide 页,就是打算经常看。
32600
+ *
32601
+ * 三条与设置页一致的产品约束(详见 `workspace.tsx` 的 `PluginsSection` 注释):
32602
+ * 官方包不可停用、`runtime-provided` 不是故障、改完要重启才生效。
32603
+ */
32604
+ function makePluginsTabBody(api) {
32605
+ return function PluginsTabBody() {
32606
+ const { t } = useWorkspaceI18n();
32607
+ const [inventory, setInventory] = (0, react.useState)(void 0);
32608
+ const [busy, setBusy] = (0, react.useState)(false);
32609
+ const [checking, setChecking] = (0, react.useState)(false);
32610
+ const [toggling, setToggling] = (0, react.useState)(void 0);
32611
+ const [error, setError] = (0, react.useState)(void 0);
32612
+ const [restartRequired, setRestartRequired] = (0, react.useState)(false);
32613
+ const load = (0, react.useCallback)(async (checkUpdates) => {
32614
+ if (checkUpdates) setChecking(true);
32615
+ else setBusy(true);
32616
+ setError(void 0);
32617
+ try {
32618
+ setInventory(await api.pluginInventory(checkUpdates));
32619
+ } catch (cause) {
32620
+ setError(messageOf(cause, t));
32621
+ } finally {
32622
+ if (checkUpdates) setChecking(false);
32623
+ else setBusy(false);
32624
+ }
32625
+ }, [t]);
32626
+ (0, react.useEffect)(() => {
32627
+ load(false);
32628
+ }, [load]);
32629
+ const toggle = (0, react.useCallback)(async (entry) => {
32630
+ setToggling(entry.name);
32631
+ setError(void 0);
32632
+ try {
32633
+ await api.togglePlugin(entry.name, !entry.loaded);
32634
+ setInventory(await api.pluginInventory(false));
32635
+ setRestartRequired(true);
32636
+ } catch (cause) {
32637
+ setError(messageOf(cause, t));
32638
+ } finally {
32639
+ setToggling(void 0);
32640
+ }
32641
+ }, [t]);
32642
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32643
+ className: "daw-plugins-tab",
32644
+ children: [
32645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
32646
+ className: "daw-list-meta",
32647
+ children: t("pluginsHint")
32648
+ }),
32649
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32650
+ className: "daw-warning",
32651
+ role: "alert",
32652
+ children: error
32653
+ }),
32654
+ restartRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32655
+ className: "daw-success",
32656
+ role: "status",
32657
+ children: t("pluginsRestartRequired")
32658
+ }),
32659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32660
+ className: "daw-form-row",
32661
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
32662
+ type: "button",
32663
+ className: "daw-command",
32664
+ disabled: busy,
32665
+ onClick: () => {
32666
+ load(false);
32667
+ },
32668
+ children: [
32669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.RefreshCw, { size: 14 }),
32670
+ " ",
32671
+ busy ? t("loading") : t("refresh")
32672
+ ]
32673
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32674
+ type: "button",
32675
+ className: "daw-command",
32676
+ disabled: checking,
32677
+ onClick: () => {
32678
+ load(true);
32679
+ },
32680
+ children: checking ? t("pluginsChecking") : t("pluginsCheckUpdates")
32681
+ })]
32682
+ }),
32683
+ inventory === void 0 ? busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32684
+ className: "daw-list-meta",
32685
+ children: t("loading")
32686
+ }) : null : !inventory.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32687
+ className: "daw-list-meta",
32688
+ children: t("pluginsUnavailable", { reason: inventory.reason ?? "" })
32689
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32690
+ className: "daw-list-meta",
32691
+ children: [
32692
+ inventory.profile !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [inventory.profile, " · "] }),
32693
+ inventory.loadedCount,
32694
+ " / ",
32695
+ inventory.items.length,
32696
+ inventory.problemCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsProblems", { count: inventory.problemCount })] })
32697
+ ]
32698
+ }), inventory.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32699
+ className: "daw-list-meta daw-empty",
32700
+ children: t("pluginsEmpty")
32701
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32702
+ className: "daw-list",
32703
+ children: inventory.items.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32704
+ className: "daw-list-row",
32705
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
32706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32707
+ className: "daw-list-title",
32708
+ children: [entry.name, entry.installed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
32709
+ className: "daw-list-meta",
32710
+ children: [" · ", entry.installed]
32711
+ })]
32712
+ }),
32713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32714
+ className: "daw-list-meta",
32715
+ children: entry.description ?? t("pluginsNoDescription")
32716
+ }),
32717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32718
+ className: "daw-list-meta",
32719
+ children: [
32720
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32721
+ className: `daw-pill${entry.loaded ? " on" : ""}`,
32722
+ children: t(pluginStateKey(entry.state))
32723
+ }),
32724
+ entry.updateAvailable === true && entry.latestVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32725
+ className: "daw-pill",
32726
+ children: t("pluginsUpdateAvailable", { version: entry.latestVersion })
32727
+ })] }),
32728
+ entry.updateAvailable === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsLatest")] })
32729
+ ]
32730
+ })
32731
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entry.toggleable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32732
+ type: "button",
32733
+ className: "daw-command",
32734
+ disabled: toggling === entry.name,
32735
+ onClick: () => {
32736
+ toggle(entry);
32737
+ },
32738
+ children: entry.loaded ? t("pluginsDisable") : t("pluginsEnable")
32739
+ }) })]
32740
+ }, entry.name))
32741
+ })] })
32742
+ ]
32743
+ });
32744
+ };
32745
+ }
32746
+ //#endregion
32747
+ //#region src/client/styles.ts
32748
+ const STYLE_ID = "dsh-workspace-style";
32749
+ /**
32750
+ * 插件的全部样式。
32751
+ *
32752
+ * 导出出来是为了让测试能守住一条容易失守的不变量:**CSS 变量块必须同时覆盖
32753
+ * `.daw-root` 与 `.daw-settings-section`**。
32754
+ *
32755
+ * 这条防线来自一个真实发生过的隐形 bug:设置页内联渲染时,那一节没有 `.daw-root` 祖先,
32756
+ * 于是 `var(--daw-accent)` 解析失败 → `.daw-command.primary` 的 `background` 失效变透明、
32757
+ * 而 `color` 仍是 `#fff` → **白字落在白底上,按钮彻底看不见**。
32758
+ * 类型检查、单测、构建全都发现不了,只有肉眼看渲染或量计算样式才抓得到。
32759
+ */
32760
+ const WORKSPACE_CSS = `
32761
+ .daw-root,.daw-settings-section,.daw-plugins-tab{--daw-bg:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-2,#f7f8fa));--daw-panel:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));--daw-ink:var(--dsw-alias-label-primary,#18202a);--daw-muted:var(--dsw-alias-label-tertiary,#687381);--daw-line:var(--dsw-alias-border-l2,#dfe4e8);--daw-accent:var(--dsw-alias-brand-primary,#087f8c);--daw-accent-soft:var(--dsw-alias-bg-module-platform,#e5f5f6);--daw-good:var(--dsw-alias-state-success-primary,#2f7d4a);--daw-danger:var(--dsw-alias-state-error-primary,#b33a3a);color:var(--daw-ink);background:transparent;font:13px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:0;min-width:0}
32762
+ .daw-root *,.daw-settings-section *,.daw-plugins-tab *{box-sizing:border-box;letter-spacing:0}
32763
+ /* 输入控件基线(UI-04):此前只有 :focus-visible 一条,非聚焦态全靠浏览器默认(2px inset/Arial/灰底),
32764
+ 深色令牌下实测白底黑字与面板割裂。这里给齐边框/底/字/内边距,聚焦态只换描边。 */
32765
+ .daw-input{width:100%;min-width:0;height:30px;padding:4px 9px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit}
32766
+ .daw-input::placeholder{color:var(--dsw-alias-label-tertiary,var(--daw-muted))}
32767
+ .daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}
32768
+ .daw-input:disabled{opacity:.55;cursor:not-allowed}.daw-icon{width:30px;padding:0}
32769
+ .daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
32770
+ .daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
32771
+ .daw-icon:disabled{opacity:.45;cursor:not-allowed}
32772
+ /* 普通命令按钮基线(UI-04):旧版写死 background:#fff/border:#cbd2d8,深色令牌下成了白块黑字。
32773
+ 全部改走 DSH 令牌;.primary 沿用面板底色做次按钮、真正的主操作由调用方加 .primary 配合。 */
32774
+ .daw-command{height:30px;padding:0 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit;cursor:pointer}
32775
+ .daw-command:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
32776
+ .daw-command:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:1px}
32777
+ /* 禁用态(UI-05):此前无任何 :disabled 样式,只剩浏览器默认的 30% 文字且仍是箭头手型。 */
32778
+ .daw-command:disabled{opacity:.5;cursor:not-allowed}
32779
+ .daw-command.primary{color:var(--dsw-alias-label-primary,var(--daw-ink));background:var(--dsw-alias-bg-module-platform,var(--daw-panel));border-color:var(--dsw-alias-border-l2,var(--daw-line));font-weight:500}
32780
+ .daw-command.primary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
32781
+ .daw-command.danger{color:var(--daw-danger)}
32782
+ .daw-command.danger:hover:not(:disabled){border-color:var(--daw-danger)}
32783
+ /* 图标+文字的小复制按钮:图标和文字对齐,窄屏只露图标也不改变按钮高度。 */
32784
+ .daw-command.icon-as-command{display:inline-flex;align-items:center;gap:6px;padding:0 10px;white-space:nowrap}
32785
+ .daw-copy-label{font-size:12px}
32786
+ .daw-empty{padding:14px 2px;text-align:center}
32787
+ .daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
32788
+ .daw-card{border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
32789
+ .daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
32790
+ .daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
32791
+ .daw-card-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}
32792
+ .daw-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:-2px}
32793
+ .daw-card-icon{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;display:flex;align-items:center}
32794
+ .daw-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
32795
+ .daw-card-name{color:var(--dsw-alias-label-primary,var(--daw-ink));font-size:15px;font-weight:600;line-height:1.4}
32796
+ .daw-card-desc{color:var(--dsw-alias-label-tertiary,var(--daw-muted));font-size:13px;line-height:1.5}
32797
+ .daw-card-chevron{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;transition:transform .16s;font-size:12px;line-height:1}
32798
+ .daw-card-chevron-open{transform:rotate(180deg)}
32799
+ .daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-plugins-tab{overflow:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
32800
+ .daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
32801
+ .daw-builtin-hint{text-align:right;align-self:center}
32802
+ .daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-command{margin-left:8px}
32803
+ /* 成功反馈条(UI-07):与 .daw-warning 同形,但走成功色,操作成功不再静默。 */
32804
+ .daw-success{flex:none;border:1px solid var(--dsw-alias-state-success-primary,var(--daw-good));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--dsw-alias-state-success-primary,var(--daw-good));margin-bottom:0}
32805
+ .daw-admin-topbar{display:flex;justify-content:flex-end;margin-bottom:8px}
32806
+ .daw-url-row{display:flex;align-items:center;gap:8px;margin-top:8px}.daw-url-row .daw-tunnel-url{flex:1 1 auto;margin:0;min-width:0}
32807
+ .daw-code-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0}.daw-code-row .daw-code{margin:0;flex:1 1 220px;min-width:0}.daw-copy-inline{flex:none}
32808
+ /* 隧道/监听进行中的流动条(UI-08):percent=null 时铺一段高光在轨道里循环滑动,
32809
+ 不再把「未知进度」伪装成恒定 30%。 */
32810
+ .daw-progress-fill.indeterminate{width:40%;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line))),transparent);animation:daw-indeterminate 1.1s ease-in-out infinite}
32811
+ @keyframes daw-indeterminate{0%{transform:translateX(-120%)}100%{transform:translateX(280%)}}
32812
+ @media (max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
32813
+ `;
32814
+ function installWorkspaceStyles() {
32815
+ if (document.getElementById(STYLE_ID) !== null) return;
32816
+ const style = document.createElement("style");
32817
+ style.id = STYLE_ID;
32818
+ style.dataset.plugin = "dsh-workspace";
32819
+ style.textContent = WORKSPACE_CSS;
32820
+ document.head.appendChild(style);
32821
+ }
32822
+ //#endregion
32312
32823
  //#region src/client/index.tsx
32313
32824
  const DEFAULT_SECTION = DEFAULT_PANEL_SECTION;
32314
32825
  const api = new WorkspaceApi("/dsh-workspace-api/api/v1", "/dsh-workspace-api/manage", void 0, true);
32826
+ /**
32827
+ * 打开 DSH 自己的插件主列表。
32828
+ *
32829
+ * **特性检测,不是版本判断**:`pluginNavigation.openList` 是 DSH 0.2.1-rc.3 才加的
32830
+ * (rc.2 只有 `openBundle`),而服务本身在更早的版本就有了。写死版本号会在
32831
+ * 自定义构建 / 未来版本上误判,直接看方法在不在更可靠。
32832
+ *
32833
+ * 返回 `false` 让调用方知道「这次没跳成」,卡片据此给出提示 —— 静默失败是最糟的
32834
+ * 交互,用户会以为按钮坏了。
32835
+ */
32836
+ function openPluginList(ctx) {
32837
+ const navigation = ctx.get?.("pluginNavigation", false);
32838
+ if (typeof navigation?.openList !== "function") return false;
32839
+ navigation.openList();
32840
+ return true;
32841
+ }
32315
32842
  const inject = ["slots", "uiConversation"];
32316
32843
  function apply(ctx) {
32317
32844
  installWorkspaceStyles();
@@ -32327,7 +32854,37 @@ window.__ModuleLoader__.load({
32327
32854
  name: "settings.section",
32328
32855
  id: "dsh-remote-bridge",
32329
32856
  order: 2
32330
- }, SettingsSection, "phoneSettings"), "dsh remote bridge settings section");
32857
+ }, makeSettingsSection(ctx), "phoneSettings"), "dsh remote bridge settings section");
32858
+ registerPluginsTab(ctx);
32859
+ }
32860
+ /**
32861
+ * 把插件清单注册成右侧栏的一个 tab 类型,并在 guide 页列出可 pin 的卡片。
32862
+ *
32863
+ * 三件事必须同时成立,少一件这张卡就「有入口、点开空白」或「根本不出现在 guide 页」:
32864
+ *
32865
+ * 1. `sidebarRightTabs.register(definition)` —— 声明类型身份与 guide 条目;
32866
+ * 2. `sidebar.right.pane.tab` 按 `key = definition.id` 挂 body;
32867
+ * 3. `sidebar.right.pane.tab.title` 同样按 `key` 挂标题。
32868
+ *
32869
+ * 两个 keyed seat 用的是 `definition?.id ?? tab.kind` 做 dispatch key,所以 `key` 必须等于
32870
+ * definition 的 `id`(不是 `kind`)—— 写错的表现是点开 tab 显示「不可用」占位。
32871
+ *
32872
+ * 依赖走 `ctx.inject` 而不是静态 `inject`:`sidebarRightTabs` 来自可选安装的
32873
+ * `@deepseek-ai/dsh-client-ui-sidebar-right`,宿主没装时这里整段静默不执行,
32874
+ * 设置页那张卡与交接图集照常工作。
32875
+ */
32876
+ function registerPluginsTab(ctx) {
32877
+ ctx.inject?.(["sidebarRightTabs"], (scope) => {
32878
+ scope.effect(() => scope.sidebarRightTabs.register(pluginsTabDefinition()), "dsh remote bridge plugins tab type");
32879
+ scope.effect(() => scope.slots.inject("sidebar.right.pane.tab", () => scope.slots.register({
32880
+ name: "sidebar.right.pane.tab",
32881
+ key: PLUGINS_TAB_ID
32882
+ }, makePluginsTabBody(api))), "dsh remote bridge plugins tab body");
32883
+ scope.effect(() => scope.slots.inject("sidebar.right.pane.tab.title", () => scope.slots.register({
32884
+ name: "sidebar.right.pane.tab.title",
32885
+ key: PLUGINS_TAB_ID
32886
+ }, PluginsTabTitle)), "dsh remote bridge plugins tab title");
32887
+ });
32331
32888
  }
32332
32889
  /**
32333
32890
  * DSH 设置页里的「手机设置」一节。
@@ -32341,20 +32898,24 @@ window.__ModuleLoader__.load({
32341
32898
  * 用户的另一条要求也靠内联才落地:二维码要**单独一页**(`app-download`),
32342
32899
  * 它是导航栏里可点的一项,而不是藏在按钮后面。
32343
32900
  */
32344
- function SettingsSection() {
32345
- const { t } = useWorkspaceI18n();
32346
- const [section, setSection] = (0, react.useState)(DEFAULT_SECTION);
32347
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32348
- className: "daw-settings-section",
32349
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
32350
- className: "daw-list-meta",
32351
- children: t("phoneSettingsHint")
32352
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneSettingsBody, {
32353
- api,
32354
- section,
32355
- onSection: setSection
32356
- })]
32357
- });
32901
+ function makeSettingsSection(ctx) {
32902
+ return function SettingsSection() {
32903
+ const { t } = useWorkspaceI18n();
32904
+ const [section, setSection] = (0, react.useState)(DEFAULT_SECTION);
32905
+ const openList = (0, react.useCallback)(() => openPluginList(ctx), [ctx]);
32906
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32907
+ className: "daw-settings-section",
32908
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
32909
+ className: "daw-list-meta",
32910
+ children: t("phoneSettingsHint")
32911
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneSettingsBody, {
32912
+ api,
32913
+ section,
32914
+ onSection: setSection,
32915
+ onOpenPluginList: openList
32916
+ })]
32917
+ });
32918
+ };
32358
32919
  }
32359
32920
  function localizedSlot(ctx, name, options, component, labelKey) {
32360
32921
  return ctx.slots.inject(name, () => {