@linxin666/dsh-remote-web-ui 0.2.0 → 0.2.1

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.
Files changed (68) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +15 -7
  3. package/README.zh.md +4 -4
  4. package/lib/client.js +235 -37
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +26 -7
  7. package/lib/mobile.js +209 -39
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts +22 -0
  10. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  11. package/lib/types/client/PluginSettingsCard.js +142 -3
  12. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  13. package/lib/types/client/RemoteEntry.js +11 -0
  14. package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
  15. package/lib/types/client/UpdatePanel.d.ts.map +1 -1
  16. package/lib/types/client/UpdatePanel.js +2 -2
  17. package/lib/types/client/locales.d.ts +2 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/locales.js +2 -0
  20. package/lib/types/gate.d.ts.map +1 -1
  21. package/lib/types/gate.js +2 -0
  22. package/lib/types/mobile/api.d.ts +10 -0
  23. package/lib/types/mobile/api.d.ts.map +1 -1
  24. package/lib/types/mobile/api.js +4 -0
  25. package/lib/types/mobile/messages.d.ts +24 -0
  26. package/lib/types/mobile/messages.d.ts.map +1 -1
  27. package/lib/types/mobile/messages.js +60 -23
  28. package/lib/types/mobile/messages.test.js +49 -1
  29. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  30. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  31. package/lib/types/mobile/mobile-styles.js +38 -0
  32. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  33. package/lib/types/mobile/views/ChatView.js +34 -11
  34. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  35. package/lib/types/mobile/views/SessionListView.js +30 -4
  36. package/lib/types/mobile/views/SessionListView.test.js +58 -1
  37. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  38. package/lib/types/mobile/views/WorkspaceView.js +4 -2
  39. package/lib/types/mobile-api.d.ts.map +1 -1
  40. package/lib/types/mobile-api.js +10 -1
  41. package/lib/types/routes.d.ts.map +1 -1
  42. package/lib/types/routes.js +22 -2
  43. package/lib/types/tunnel.d.ts +1 -0
  44. package/lib/types/tunnel.d.ts.map +1 -1
  45. package/lib/types/tunnel.js +7 -2
  46. package/lib/types/update.d.ts +1 -1
  47. package/lib/types/update.d.ts.map +1 -1
  48. package/lib/types/update.js +1 -1
  49. package/package.json +1 -1
  50. package/src/client/PluginSettingsCard.tsx +219 -18
  51. package/src/client/RemoteEntry.tsx +10 -0
  52. package/src/client/RemoteSettingsCard.tsx +2 -2
  53. package/src/client/UpdatePanel.tsx +2 -0
  54. package/src/client/locales.ts +2 -0
  55. package/src/client/settings-card.module.css +90 -1
  56. package/src/gate.ts +2 -0
  57. package/src/mobile/api.ts +14 -1
  58. package/src/mobile/messages.test.ts +52 -1
  59. package/src/mobile/messages.ts +65 -22
  60. package/src/mobile/mobile-styles.ts +38 -0
  61. package/src/mobile/views/ChatView.tsx +33 -10
  62. package/src/mobile/views/SessionListView.test.tsx +69 -1
  63. package/src/mobile/views/SessionListView.tsx +58 -5
  64. package/src/mobile/views/WorkspaceView.tsx +4 -2
  65. package/src/mobile-api.ts +9 -1
  66. package/src/routes.ts +22 -2
  67. package/src/tunnel.ts +7 -2
  68. package/src/update.ts +2 -2
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: ed09a3bb6b09641147fecd3a465a6766f22fb298
6
- README.zh.md: d95d6fefe6b583c90169cd201464a62ac52381e0
5
+ README.md: 30f19949c347321da330c22bee0f61ba42b70bcf
6
+ README.zh.md: 5221d455f62613b62795a1340e64bbdd7ed63937
package/README.md CHANGED
@@ -103,7 +103,7 @@ Install the family aggregate package `@linxin666/dsh-web-ui-all` (all plugins an
103
103
 
104
104
  ```sh
105
105
  # Recommended: install directly from npm
106
- dsh plugin --profile web add @linxin666/dsh-remote-web-ui
106
+ dsh plugin --profile web add @linxin666/dsh-remote-web-ui@latest
107
107
 
108
108
  # Or from the repository (development loop)
109
109
  git clone https://github.com/zhu1090093659/dsh-web-ui.git
@@ -130,9 +130,11 @@ mounts both halves.
130
130
  3. Scan with the phone (or open the copied link): the phone binds and
131
131
  lands on the **standalone mobile surface at `/m/`** — no desktop UI on a
132
132
  small screen. The surface is deliberately thin:
133
- - workspaces straight away (a 新建会话 button lives on each workspace's
134
- session list: it creates a blank session attached to that workspace via
135
- the host's `session.create` and opens the new chat immediately),
133
+ - workspaces straight away (each workspace's session list offers an Agent
134
+ mode picker before 新建会话: it selects the usable default preset, or the
135
+ first usable preset, and passes that roster id with the workspace id to
136
+ the host's `session.create`; an empty or unavailable roster keeps the
137
+ host-default creation flow),
136
138
  - one workspace's sessions load **incrementally** (20 rows per page,
137
139
  "加载更多会话" continues; never the whole list at once),
138
140
  - opening a session fetches its chat content **on demand** (history
@@ -172,9 +174,10 @@ pages `session.list` itself), so the tunneled Host never has to enter the
172
174
  connection plugin's trust fence. The phone is gated by its paired-device
173
175
  cookie and an explicit method allowlist (settings/credentials/host-action
174
176
  domains are never reachable from the phone; model reads/writes are limited
175
- to the advisory `session.models` / `session.selectModel` pair, creation to
176
- `session.create` (workspace id only — the phone never names a working
177
- directory of its own), and the permission picker only ever sends the
177
+ to the advisory `session.models` / `session.selectModel` pair, agent preset
178
+ access to read-only `agentPreset.list`, creation to `session.create`
179
+ (workspace id plus an optional id from that roster — the phone never names a
180
+ working directory of its own), and the permission picker only ever sends the
178
181
  mode-agnostic `/permission` command
179
182
  through the already-allowlisted `session.prompt`); the live stream arrives
180
183
  over Server-Sent Events on `/m/api/events.mux`. The canonical `/m/` page owns a same-scope manifest and Service Worker; its cache is limited to the static shell and an offline page, never mobile API responses, session data, or commands.
@@ -242,6 +245,11 @@ pairing fence trusts the tunneled host. Two knobs are involved:
242
245
  card): the public origin, e.g. `https://foo.trycloudflare.com`. The QR
243
246
  link is built from it, and `accept`/`heartbeat`/`status` accept its host.
244
247
  Malformed values are ignored with a warning (LAN-only behavior kept).
248
+ Unpaired callers of `status` see only the pairing-relevant fields (phase /
249
+ LAN bases); token expiry, device roster, and the tunnel URL require a live
250
+ device cookie. The accept rate limit partitions its buckets by the
251
+ client-visible `X-Forwarded-For` hop so one internet client cannot
252
+ exhaust the shared bucket behind the tunnel.
245
253
  - **`--trusted-host <authority>`** (dsh web flag): the transport-level
246
254
  `/api` fence of the connection plugin must accept the public host too —
247
255
  without it every `/api` request through the tunnel 403s *before* the
package/README.zh.md CHANGED
@@ -37,7 +37,7 @@
37
37
 
38
38
  ```sh
39
39
  ### 从 npm 安装(推荐)
40
- dsh plugin --profile web add @linxin666/dsh-remote-web-ui
40
+ dsh plugin --profile web add @linxin666/dsh-remote-web-ui@latest
41
41
 
42
42
  ### 从仓库安装(开发调试)
43
43
  git clone https://github.com/zhu1090093659/dsh-web-ui.git
@@ -56,7 +56,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
56
56
  1. `dsh web --host 0.0.0.0`(打印的局域网 URL 确认可达性)。
57
57
  2. 点击手机图标 → 面板铸一枚新的二维码。
58
58
  3. 用手机扫码(或打开复制的链接):手机绑定并落到 **`/m/` 独立移动端界面**——不在小屏显示桌面 UI。该界面刻意精简:
59
- - 直接进入工作区(每个工作区的会话列表上有 新建会话 按钮:它经 host 的 `session.create` 创建附加到该工作区的空白会话,并立即打开新聊天),
59
+ - 直接进入工作区(每个工作区的会话列表在 新建会话 前提供 Agent 模式选择器:默认选择可用的默认预设,否则选择第一个可用预设,并把该清单 id 与工作区 id 一起传给 host 的 `session.create`;清单为空或不可用时保留 host 默认创建流程),
60
60
  - 一个工作区的会话**增量**加载(每页 20 行,"加载更多会话"继续;绝不同时加载整份列表),
61
61
  - 打开会话**按需**抓取聊天内容(历史分页,"加载更早的消息"继续往回翻),
62
62
  - 实时流随消息到达展示新消息,带发送自己消息的输入框(默认 **Enter 发送、Shift+Enter 换行**;设 `mobileEnterToSend: false` 后 Enter 改为换行,发送仅走「发送」按钮),
@@ -67,7 +67,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
67
67
  5. 桌面徽标实时翻到 已连接;手机离开时回落到离线/断开。
68
68
  6. 刷新二维码 使旧链接失效并铸一枚新的。停止 撤销移动端访问:已配对设备下一次请求 403,包括其实时流。
69
69
 
70
- 该移动端界面完全自包含在本插件内:`/m/` 页面及其数据通道(`/m/api`)由插件自己的路由伺服,**无需任何 harness 源码改动**——手机的 RPC 调用走插件的 `/m/api` 代理(它委托给 host 的 ApiProxy 服务并自己分页 `session.list`),因此被隧道化的 Host 永远不必进入连接插件的信任围栏。手机受其已配对设备 cookie 与显式方法白名单门控(settings/credentials/host-action 域手机永远不可达;模型读写限制于建议性的 `session.models` / `session.selectModel` 对,创建限制于 `session.create`(仅工作区 id——手机绝不自命名工作目录),权限选择器只通过已放行的 `session.prompt` 发送模式无关的 `/permission` 命令);实时流在 `/m/api/events.mux` 上经 Server-Sent Events 送达。规范的 `/m/` 页面拥有同 scope 的 manifest 与 Service Worker;其缓存仅包含静态壳和离线页,绝不包含移动端 API 响应、会话数据或命令。
70
+ 该移动端界面完全自包含在本插件内:`/m/` 页面及其数据通道(`/m/api`)由插件自己的路由伺服,**无需任何 harness 源码改动**——手机的 RPC 调用走插件的 `/m/api` 代理(它委托给 host 的 ApiProxy 服务并自己分页 `session.list`),因此被隧道化的 Host 永远不必进入连接插件的信任围栏。手机受其已配对设备 cookie 与显式方法白名单门控(settings/credentials/host-action 域手机永远不可达;模型读写限制于建议性的 `session.models` / `session.selectModel` 对,Agent 预设访问限制于只读 `agentPreset.list`,创建限制于 `session.create`(工作区 id 加清单中的可选 id——手机绝不自命名工作目录),权限选择器只通过已放行的 `session.prompt` 发送模式无关的 `/permission` 命令);实时流在 `/m/api/events.mux` 上经 Server-Sent Events 送达。规范的 `/m/` 页面拥有同 scope 的 manifest 与 Service Worker;其缓存仅包含静态壳和离线页,绝不包含移动端 API 响应、会话数据或命令。
71
71
 
72
72
  ### 行为说明
73
73
 
@@ -92,7 +92,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
92
92
 
93
93
  二维码链接通常是局域网 URL,所以家外的手机无法使用。把隧道指向 dsh web 端口,并告知插件其公网地址——二维码随后由隧道 URL 构建,面向手机的配对围栏信任隧道化的主机。涉及两个钮:
94
94
 
95
- - **`publicBaseUrl`**(插件配置,在 profile patch 或设置卡片里):公网 origin,如 `https://foo.trycloudflare.com`。二维码链接由它构建,`accept`/`heartbeat`/`status` 接受它的主机。畸形值被忽略并告警(保持仅局域网行为)。
95
+ - **`publicBaseUrl`**(插件配置,在 profile patch 或设置卡片里):公网 origin,如 `https://foo.trycloudflare.com`。二维码链接由它构建,`accept`/`heartbeat`/`status` 接受它的主机。畸形值被忽略并告警(保持仅局域网行为)。未配对调用方的 `status` 只看到配对相关字段(phase / 局域网地址);token 过期时间、设备列表与隧道 URL 需要有效设备 cookie。accept 限速按客户端可见的 `X-Forwarded-For` 跳点分桶,避免隧道背后的单个来源耗尽共享桶。
96
96
  - **`--trusted-host <authority>`**(dsh web flag):连接插件传输层 `/api` 围栏也必须接受公网主机——否则经隧道的每个 `/api` 请求在**配对层之前**就 403(插件自己的围栏只覆盖 `/api/pair` 路由)。请像隧道转发那样精确传入公网主机(或 `host:port`)。
97
97
 
98
98
  ### Cloudflare 隧道(quick tunnel——无账号、无域名)
package/lib/client.js CHANGED
@@ -1740,13 +1740,20 @@ window.__ModuleLoader__.load({
1740
1740
  t,
1741
1741
  status
1742
1742
  }),
1743
- view.kind === "updating" && status !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1744
- className: remote_module_css_default.updateStatus,
1745
- children: t("update.updating", {
1746
- name: anchorName(status) ?? "",
1747
- version: anchorLatest(status) ?? ""
1748
- })
1749
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackageList, { status })] }),
1743
+ view.kind === "updating" && status !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1744
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1745
+ className: remote_module_css_default.updateStatus,
1746
+ children: t("update.updating", {
1747
+ name: anchorName(status) ?? "",
1748
+ version: anchorLatest(status) ?? ""
1749
+ })
1750
+ }),
1751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1752
+ className: remote_module_css_default.updateDetail,
1753
+ children: t("update.cooldownNotice")
1754
+ }),
1755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackageList, { status })
1756
+ ] }),
1750
1757
  view.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoneBody, {
1751
1758
  t,
1752
1759
  result: view.result
@@ -1809,16 +1816,23 @@ window.__ModuleLoader__.load({
1809
1816
  className: remote_module_css_default.updateDetail,
1810
1817
  children: t("update.offlineDetail")
1811
1818
  })] });
1812
- if (status.outdated) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1813
- className: remote_module_css_default.updateStatus,
1814
- children: t("update.found")
1815
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1816
- className: remote_module_css_default.updateDetail,
1817
- children: anchor !== void 0 ? t("update.foundDetail", {
1818
- name: anchor,
1819
- version: latest ?? ""
1820
- }) : ""
1821
- })] });
1819
+ if (status.outdated) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1821
+ className: remote_module_css_default.updateStatus,
1822
+ children: t("update.found")
1823
+ }),
1824
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1825
+ className: remote_module_css_default.updateDetail,
1826
+ children: anchor !== void 0 ? t("update.foundDetail", {
1827
+ name: anchor,
1828
+ version: latest ?? ""
1829
+ }) : ""
1830
+ }),
1831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1832
+ className: remote_module_css_default.updateDetail,
1833
+ children: t("update.cooldownNotice")
1834
+ })
1835
+ ] });
1822
1836
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1823
1837
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1824
1838
  className: remote_module_css_default.updateStatus,
@@ -2058,6 +2072,7 @@ window.__ModuleLoader__.load({
2058
2072
  }, [state]);
2059
2073
  const [copied, setCopied] = (0, react.useState)(false);
2060
2074
  const eventSource = (0, react.useRef)(void 0);
2075
+ const openSeq = (0, react.useRef)(0);
2061
2076
  const workspaceId = useWorkspaces((s) => s.recentWorkspaceId);
2062
2077
  const closeEventSource = (0, react.useCallback)(() => {
2063
2078
  eventSource.current?.close();
@@ -2091,8 +2106,10 @@ window.__ModuleLoader__.load({
2091
2106
  };
2092
2107
  }, [workspaceId]);
2093
2108
  const openPanel = (0, react.useCallback)(async () => {
2109
+ const seq = ++openSeq.current;
2094
2110
  setOpen(true);
2095
2111
  const next = await mint();
2112
+ if (seq !== openSeq.current) return;
2096
2113
  setState(next);
2097
2114
  if (next.kind !== "ready" && next.kind !== "lan-required") return;
2098
2115
  const source = new EventSource("/api/pair/events");
@@ -2111,6 +2128,7 @@ window.__ModuleLoader__.load({
2111
2128
  };
2112
2129
  }, [mint]);
2113
2130
  const closePanel = (0, react.useCallback)(() => {
2131
+ openSeq.current += 1;
2114
2132
  closeEventSource();
2115
2133
  setOpen(false);
2116
2134
  }, [closeEventSource]);
@@ -2260,7 +2278,7 @@ window.__ModuleLoader__.load({
2260
2278
  }
2261
2279
  //#endregion
2262
2280
  //#region \0dsh-css:packages/dsh-remote-web-ui/src/client/settings-card.module.css.mjs
2263
- const css = ".Kwoi6G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Kwoi6G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_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}.Kwoi6G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Kwoi6G_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Kwoi6G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Kwoi6G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Kwoi6G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Kwoi6G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Kwoi6G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Kwoi6G_chevronOpen{transform:rotate(180deg)}.Kwoi6G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Kwoi6G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Kwoi6G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Kwoi6G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Kwoi6G_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Kwoi6G_discard,.Kwoi6G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Kwoi6G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Kwoi6G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Kwoi6G_discard:disabled,.Kwoi6G_save:disabled{opacity:.4;cursor:default}.Kwoi6G_discard:focus-visible,.Kwoi6G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Kwoi6G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Kwoi6G_field+.Kwoi6G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Kwoi6G_head{align-items:center;gap:8px;display:flex}.Kwoi6G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Kwoi6G_badges{align-items:center;gap:8px;display:inline-flex}.Kwoi6G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Kwoi6G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Kwoi6G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Kwoi6G_reset:disabled{cursor:default}.Kwoi6G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Kwoi6G_input,.Kwoi6G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Kwoi6G_input:focus-visible,.Kwoi6G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Kwoi6G_input:disabled,.Kwoi6G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Kwoi6G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Kwoi6G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Kwoi6G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Kwoi6G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Kwoi6G_card,.Kwoi6G_header,.Kwoi6G_chevron,.Kwoi6G_chevronOpen,.Kwoi6G_discard,.Kwoi6G_save{transition:none}}";
2281
+ const css = ".Kwoi6G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Kwoi6G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_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}.Kwoi6G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Kwoi6G_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Kwoi6G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Kwoi6G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Kwoi6G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Kwoi6G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Kwoi6G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Kwoi6G_chevronOpen{transform:rotate(180deg)}.Kwoi6G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Kwoi6G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Kwoi6G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Kwoi6G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Kwoi6G_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Kwoi6G_discard,.Kwoi6G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Kwoi6G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Kwoi6G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Kwoi6G_discard:disabled,.Kwoi6G_save:disabled{opacity:.4;cursor:default}.Kwoi6G_discard:focus-visible,.Kwoi6G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Kwoi6G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Kwoi6G_field+.Kwoi6G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Kwoi6G_head{align-items:center;gap:8px;display:flex}.Kwoi6G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Kwoi6G_badges{align-items:center;gap:8px;display:inline-flex}.Kwoi6G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Kwoi6G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Kwoi6G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Kwoi6G_reset:disabled{cursor:default}.Kwoi6G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Kwoi6G_input,.Kwoi6G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Kwoi6G_input:focus-visible,.Kwoi6G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Kwoi6G_input:disabled,.Kwoi6G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Kwoi6G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Kwoi6G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Kwoi6G_selectWrap{position:relative}.Kwoi6G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Kwoi6G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Kwoi6G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Kwoi6G_selectChevronOpen{transform:rotate(180deg)}.Kwoi6G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Kwoi6G_selectPopupOpen{opacity:1;transform:none}.Kwoi6G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Kwoi6G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Kwoi6G_selectOption:hover,.Kwoi6G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Kwoi6G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Kwoi6G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Kwoi6G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Kwoi6G_card,.Kwoi6G_header,.Kwoi6G_chevron,.Kwoi6G_chevronOpen,.Kwoi6G_discard,.Kwoi6G_save,.Kwoi6G_selectChevron,.Kwoi6G_selectChevronOpen,.Kwoi6G_selectPopup{transition:none}}";
2264
2282
  const tagId = "@linxin666/dsh-remote-web-ui/settings-card.module.css";
2265
2283
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2266
2284
  const tag = document.createElement("style");
@@ -2297,7 +2315,18 @@ window.__ModuleLoader__.load({
2297
2315
  "readOnly": "Kwoi6G_readOnly",
2298
2316
  "reset": "Kwoi6G_reset",
2299
2317
  "save": "Kwoi6G_save",
2300
- "select": "Kwoi6G_select"
2318
+ "select": "Kwoi6G_select",
2319
+ "selectButton": "Kwoi6G_selectButton",
2320
+ "selectChevron": "Kwoi6G_selectChevron",
2321
+ "selectChevronOpen": "Kwoi6G_selectChevronOpen",
2322
+ "selectLabel": "Kwoi6G_selectLabel",
2323
+ "selectOption": "Kwoi6G_selectOption",
2324
+ "selectOptionActive": "Kwoi6G_selectOptionActive",
2325
+ "selectOptionSelected": "Kwoi6G_selectOptionSelected",
2326
+ "selectPopup": "Kwoi6G_selectPopup",
2327
+ "selectPopupClose": "Kwoi6G_selectPopupClose",
2328
+ "selectPopupOpen": "Kwoi6G_selectPopupOpen",
2329
+ "selectWrap": "Kwoi6G_selectWrap"
2301
2330
  };
2302
2331
  //#endregion
2303
2332
  //#region src/client/PluginSettingsCard.tsx
@@ -2476,6 +2505,175 @@ window.__ModuleLoader__.load({
2476
2505
  ]
2477
2506
  });
2478
2507
  }
2508
+ const NON_SKIN_BODY_MARKERS = /* @__PURE__ */ new Set(["dshSkinCenter", "dshSidebarCollapsed"]);
2509
+ function isSkinActive() {
2510
+ return Object.keys(document.body.dataset).some((key) => key.startsWith("dsh") && !NON_SKIN_BODY_MARKERS.has(key));
2511
+ }
2512
+ const SELECT_CLOSE_MS = 100;
2513
+ /**
2514
+ * The shared dual-mode select control. While an appearance skin is active it
2515
+ * renders the legacy native `<select>` untouched, so element-level skin
2516
+ * selectors keep working; under the default appearance it renders a
2517
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
2518
+ * Staged cards reach it through BooleanField/ChoiceField; immediate-apply
2519
+ * editors (the side-card prefs) bind it directly through onEdit.
2520
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
2521
+ */
2522
+ function SelectField(props) {
2523
+ const { id, options, value } = props;
2524
+ const [open, setOpen] = (0, react.useState)(false);
2525
+ const [closing, setClosing] = (0, react.useState)(false);
2526
+ const [phase, setPhase] = (0, react.useState)("initial");
2527
+ const [activeIndex, setActiveIndex] = (0, react.useState)(0);
2528
+ const closeTimer = (0, react.useRef)(void 0);
2529
+ const wrapRef = (0, react.useRef)(null);
2530
+ const popupRef = (0, react.useRef)(null);
2531
+ const currentIndex = () => {
2532
+ const index = options.findIndex((option) => option.value === value);
2533
+ return index >= 0 ? index : 0;
2534
+ };
2535
+ const close = (0, react.useCallback)(() => {
2536
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
2537
+ setClosing(true);
2538
+ closeTimer.current = setTimeout(() => {
2539
+ setClosing(false);
2540
+ setOpen(false);
2541
+ }, SELECT_CLOSE_MS);
2542
+ }, []);
2543
+ const openPopup = () => {
2544
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
2545
+ setActiveIndex(currentIndex());
2546
+ setPhase("initial");
2547
+ setClosing(false);
2548
+ setOpen(true);
2549
+ };
2550
+ const commit = (index) => {
2551
+ const option = options[index];
2552
+ if (option) props.onEdit(option.value);
2553
+ close();
2554
+ };
2555
+ const onTriggerClick = () => {
2556
+ if (props.disabled) return;
2557
+ if (open && !closing) close();
2558
+ else openPopup();
2559
+ };
2560
+ const onKeyDown = (event) => {
2561
+ if (props.disabled) return;
2562
+ const count = options.length;
2563
+ switch (event.key) {
2564
+ case "ArrowDown":
2565
+ case "ArrowUp":
2566
+ case "Enter":
2567
+ case " ":
2568
+ event.preventDefault();
2569
+ if (!open) openPopup();
2570
+ else if (!closing) if (event.key === "ArrowDown") setActiveIndex((index) => (index + 1) % count);
2571
+ else if (event.key === "ArrowUp") setActiveIndex((index) => (index - 1 + count) % count);
2572
+ else commit(activeIndex);
2573
+ break;
2574
+ case "Escape":
2575
+ if (open) {
2576
+ event.preventDefault();
2577
+ event.stopPropagation();
2578
+ close();
2579
+ }
2580
+ break;
2581
+ case "Tab":
2582
+ if (open) close();
2583
+ break;
2584
+ }
2585
+ };
2586
+ (0, react.useEffect)(() => () => {
2587
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
2588
+ }, []);
2589
+ (0, react.useLayoutEffect)(() => {
2590
+ if (open && !closing && phase === "initial") {
2591
+ popupRef.current?.offsetHeight;
2592
+ setPhase("open");
2593
+ }
2594
+ }, [
2595
+ open,
2596
+ closing,
2597
+ phase
2598
+ ]);
2599
+ (0, react.useEffect)(() => {
2600
+ if (!open) return;
2601
+ const onPointerDown = (event) => {
2602
+ const target = event.target;
2603
+ if (target instanceof Node && !wrapRef.current?.contains(target)) close();
2604
+ };
2605
+ document.addEventListener("pointerdown", onPointerDown);
2606
+ return () => document.removeEventListener("pointerdown", onPointerDown);
2607
+ }, [open, close]);
2608
+ (0, react.useEffect)(() => {
2609
+ if (props.disabled && open) close();
2610
+ }, [
2611
+ props.disabled,
2612
+ open,
2613
+ close
2614
+ ]);
2615
+ if (isSkinActive()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2616
+ id,
2617
+ className: settings_card_module_css_default.select,
2618
+ value,
2619
+ disabled: props.disabled,
2620
+ onChange: (event) => {
2621
+ props.onEdit(event.target.value);
2622
+ },
2623
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2624
+ value: option.value,
2625
+ children: option.label
2626
+ }, option.value))
2627
+ });
2628
+ const label = options.find((option) => option.value === value)?.label ?? "";
2629
+ const popupClass = closing ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupClose}` : phase === "open" ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupOpen}` : settings_card_module_css_default.selectPopup;
2630
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2631
+ className: settings_card_module_css_default.selectWrap,
2632
+ ref: wrapRef,
2633
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2634
+ type: "button",
2635
+ id,
2636
+ className: `${settings_card_module_css_default.select} ${settings_card_module_css_default.selectButton}`,
2637
+ disabled: props.disabled,
2638
+ "aria-haspopup": "listbox",
2639
+ "aria-expanded": open,
2640
+ "aria-activedescendant": open ? `${id}-o${activeIndex}` : void 0,
2641
+ "aria-invalid": props.invalid || void 0,
2642
+ onClick: onTriggerClick,
2643
+ onKeyDown,
2644
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2645
+ className: settings_card_module_css_default.selectLabel,
2646
+ children: label
2647
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2648
+ width: "14",
2649
+ height: "14",
2650
+ viewBox: "0 0 14 14",
2651
+ fill: "none",
2652
+ xmlns: "http://www.w3.org/2000/svg",
2653
+ className: open ? `${settings_card_module_css_default.selectChevron} ${settings_card_module_css_default.selectChevronOpen}` : settings_card_module_css_default.selectChevron,
2654
+ "aria-hidden": "true",
2655
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2656
+ d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
2657
+ fill: "currentColor"
2658
+ })
2659
+ })]
2660
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2661
+ className: popupClass,
2662
+ role: "listbox",
2663
+ ref: popupRef,
2664
+ children: options.map((option, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2665
+ id: `${id}-o${index}`,
2666
+ role: "option",
2667
+ "aria-selected": option.value === value,
2668
+ className: `${settings_card_module_css_default.selectOption}${option.value === value ? ` ${settings_card_module_css_default.selectOptionSelected}` : ""}${index === activeIndex && !closing ? ` ${settings_card_module_css_default.selectOptionActive}` : ""}`,
2669
+ onClick: () => {
2670
+ commit(index);
2671
+ },
2672
+ children: option.label
2673
+ }, option.value))
2674
+ }) : null]
2675
+ });
2676
+ }
2479
2677
  /** A staged boolean field: 继承 / 开 / 关. */
2480
2678
  function BooleanField(props) {
2481
2679
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2501,28 +2699,26 @@ window.__ModuleLoader__.load({
2501
2699
  })]
2502
2700
  }) : null]
2503
2701
  }),
2504
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
2505
2703
  id: props.id,
2506
- className: settings_card_module_css_default.select,
2507
- value: props.text,
2508
- disabled: props.disabled,
2509
- onChange: (event) => {
2510
- props.onEdit(event.target.value);
2511
- },
2512
- children: [
2513
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2704
+ options: [
2705
+ {
2514
2706
  value: "",
2515
- children: props.inheritLabel
2516
- }),
2517
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2707
+ label: props.inheritLabel
2708
+ },
2709
+ {
2518
2710
  value: "true",
2519
- children: props.onLabel
2520
- }),
2521
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2711
+ label: props.onLabel
2712
+ },
2713
+ {
2522
2714
  value: "false",
2523
- children: props.offLabel
2524
- })
2525
- ]
2715
+ label: props.offLabel
2716
+ }
2717
+ ],
2718
+ value: props.text,
2719
+ disabled: props.disabled,
2720
+ invalid: props.invalid,
2721
+ onEdit: props.onEdit
2526
2722
  }),
2527
2723
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2528
2724
  className: settings_card_module_css_default.hint,
@@ -3116,6 +3312,7 @@ window.__ModuleLoader__.load({
3116
3312
  "update.found": "发现新版本",
3117
3313
  "update.foundDetail": "{name} 有新版本 v{version},正在自动更新…",
3118
3314
  "update.updating": "正在更新 {name} 至 v{version},可能需要 1-2 分钟…",
3315
+ "update.cooldownNotice": "本次更新由你主动触发,将跳过 pnpm 的 24 小时发布冷却期(供应链保护)直接安装所选版本。",
3119
3316
  "update.updatingTitle": "正在自动更新…",
3120
3317
  "update.done": "更新完成",
3121
3318
  "update.doneDetail": "所有组件已更新到最新版本。",
@@ -3221,6 +3418,7 @@ window.__ModuleLoader__.load({
3221
3418
  "update.found": "A new version is available",
3222
3419
  "update.foundDetail": "{name} has a newer release v{version}; updating automatically…",
3223
3420
  "update.updating": "Updating {name} to v{version}; this may take 1-2 minutes…",
3421
+ "update.cooldownNotice": "This user-initiated update bypasses pnpm's 24-hour release cooldown (supply-chain protection) and installs the selected version immediately.",
3224
3422
  "update.updatingTitle": "Updating automatically…",
3225
3423
  "update.done": "Update complete",
3226
3424
  "update.doneDetail": "All components are up to date.",