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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +17 -8
  3. package/README.zh.md +5 -5
  4. package/lib/client.js +259 -43
  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/UpdateEntry.d.ts.map +1 -1
  16. package/lib/types/client/UpdateEntry.js +8 -5
  17. package/lib/types/client/UpdatePanel.d.ts +3 -1
  18. package/lib/types/client/UpdatePanel.d.ts.map +1 -1
  19. package/lib/types/client/UpdatePanel.js +3 -3
  20. package/lib/types/client/locales.d.ts +4 -0
  21. package/lib/types/client/locales.d.ts.map +1 -1
  22. package/lib/types/client/locales.js +8 -4
  23. package/lib/types/client/pair-api.d.ts +1 -6
  24. package/lib/types/client/pair-api.d.ts.map +1 -1
  25. package/lib/types/client/pair-api.js +1 -10
  26. package/lib/types/gate.d.ts.map +1 -1
  27. package/lib/types/gate.js +2 -0
  28. package/lib/types/mobile/api.d.ts +10 -0
  29. package/lib/types/mobile/api.d.ts.map +1 -1
  30. package/lib/types/mobile/api.js +4 -0
  31. package/lib/types/mobile/messages.d.ts +24 -0
  32. package/lib/types/mobile/messages.d.ts.map +1 -1
  33. package/lib/types/mobile/messages.js +60 -23
  34. package/lib/types/mobile/messages.test.js +49 -1
  35. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  36. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  37. package/lib/types/mobile/mobile-styles.js +38 -0
  38. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  39. package/lib/types/mobile/views/ChatView.js +34 -11
  40. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  41. package/lib/types/mobile/views/SessionListView.js +30 -4
  42. package/lib/types/mobile/views/SessionListView.test.js +58 -1
  43. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  44. package/lib/types/mobile/views/WorkspaceView.js +4 -2
  45. package/lib/types/mobile-api.d.ts.map +1 -1
  46. package/lib/types/mobile-api.js +10 -1
  47. package/lib/types/routes.d.ts.map +1 -1
  48. package/lib/types/routes.js +22 -2
  49. package/lib/types/tunnel.d.ts +1 -0
  50. package/lib/types/tunnel.d.ts.map +1 -1
  51. package/lib/types/tunnel.js +7 -2
  52. package/lib/types/update.d.ts +1 -1
  53. package/lib/types/update.d.ts.map +1 -1
  54. package/lib/types/update.js +1 -1
  55. package/package.json +1 -2
  56. package/src/client/PluginSettingsCard.tsx +219 -18
  57. package/src/client/RemoteEntry.tsx +10 -0
  58. package/src/client/RemoteSettingsCard.tsx +2 -2
  59. package/src/client/UpdateEntry.tsx +9 -5
  60. package/src/client/UpdatePanel.tsx +12 -1
  61. package/src/client/locales.ts +8 -4
  62. package/src/client/pair-api.ts +1 -10
  63. package/src/client/settings-card.module.css +90 -1
  64. package/src/gate.ts +2 -0
  65. package/src/mobile/api.ts +14 -1
  66. package/src/mobile/messages.test.ts +52 -1
  67. package/src/mobile/messages.ts +65 -22
  68. package/src/mobile/mobile-styles.ts +38 -0
  69. package/src/mobile/mobile-theme.test.ts +1 -1
  70. package/src/mobile/views/ChatView.tsx +33 -10
  71. package/src/mobile/views/SessionListView.test.tsx +69 -1
  72. package/src/mobile/views/SessionListView.tsx +58 -5
  73. package/src/mobile/views/WorkspaceView.tsx +4 -2
  74. package/src/mobile-api.ts +9 -1
  75. package/src/routes.ts +22 -2
  76. package/src/tunnel.ts +7 -2
  77. 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: f93e37eae9e0b0293d9f78b590dff7a237b72ad6
6
+ README.zh.md: cfea9d0a66185db3af570255a349d01f82fd5efb
package/README.md CHANGED
@@ -45,7 +45,8 @@ actions, and the update panel that probes and runs the update.
45
45
  `@linxin666/dsh-*` family releases. Without the aggregate package, checks and
46
46
  updates cover every registry-managed direct `@linxin666/*` dependency in the
47
47
  profile; local link/file development dependencies are skipped. When a newer
48
- release exists, the panel runs the update automatically (`pnpm update --latest` inside the
48
+ release exists, the panel shows the version comparison and waits for
49
+ confirmation — clicking "Update now" runs the update (`pnpm update --latest` inside the
49
50
  owning dsh profile; when pnpm is missing it falls back to `corepack pnpm`
50
51
  and then `npx --yes pnpm`, and on Windows the command runs through
51
52
  `cmd.exe` so npm-installed `.cmd` shims resolve; the loopback-only
@@ -103,7 +104,7 @@ Install the family aggregate package `@linxin666/dsh-web-ui-all` (all plugins an
103
104
 
104
105
  ```sh
105
106
  # Recommended: install directly from npm
106
- dsh plugin --profile web add @linxin666/dsh-remote-web-ui
107
+ dsh plugin --profile web add @linxin666/dsh-remote-web-ui@latest
107
108
 
108
109
  # Or from the repository (development loop)
109
110
  git clone https://github.com/zhu1090093659/dsh-web-ui.git
@@ -130,9 +131,11 @@ mounts both halves.
130
131
  3. Scan with the phone (or open the copied link): the phone binds and
131
132
  lands on the **standalone mobile surface at `/m/`** — no desktop UI on a
132
133
  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),
134
+ - workspaces straight away (each workspace's session list offers an Agent
135
+ mode picker before 新建会话: it selects the usable default preset, or the
136
+ first usable preset, and passes that roster id with the workspace id to
137
+ the host's `session.create`; an empty or unavailable roster keeps the
138
+ host-default creation flow),
136
139
  - one workspace's sessions load **incrementally** (20 rows per page,
137
140
  "加载更多会话" continues; never the whole list at once),
138
141
  - opening a session fetches its chat content **on demand** (history
@@ -172,9 +175,10 @@ pages `session.list` itself), so the tunneled Host never has to enter the
172
175
  connection plugin's trust fence. The phone is gated by its paired-device
173
176
  cookie and an explicit method allowlist (settings/credentials/host-action
174
177
  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
178
+ to the advisory `session.models` / `session.selectModel` pair, agent preset
179
+ access to read-only `agentPreset.list`, creation to `session.create`
180
+ (workspace id plus an optional id from that roster — the phone never names a
181
+ working directory of its own), and the permission picker only ever sends the
178
182
  mode-agnostic `/permission` command
179
183
  through the already-allowlisted `session.prompt`); the live stream arrives
180
184
  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 +246,11 @@ pairing fence trusts the tunneled host. Two knobs are involved:
242
246
  card): the public origin, e.g. `https://foo.trycloudflare.com`. The QR
243
247
  link is built from it, and `accept`/`heartbeat`/`status` accept its host.
244
248
  Malformed values are ignored with a warning (LAN-only behavior kept).
249
+ Unpaired callers of `status` see only the pairing-relevant fields (phase /
250
+ LAN bases); token expiry, device roster, and the tunnel URL require a live
251
+ device cookie. The accept rate limit partitions its buckets by the
252
+ client-visible `X-Forwarded-For` hop so one internet client cannot
253
+ exhaust the shared bucket behind the tunnel.
245
254
  - **`--trusted-host <authority>`** (dsh web flag): the transport-level
246
255
  `/api` fence of the connection plugin must accept the public host too —
247
256
  without it every `/api` request through the tunnel 403s *before* the
package/README.zh.md CHANGED
@@ -13,7 +13,7 @@
13
13
  - **手机侧**:扫码将手机与一次性、限时令牌绑定,并落地到 **`/m/` 独立移动端界面**——一款专为小屏设计的轻客户端(见[截图](#截图)),而不是把桌面 UI 塞进手机。该页面可安装为 PWA;每个已安装应用使用自己的已配对设备 cookie,存储隔离的移动端 Web App 需要在该上下文打开新的二维码链接,或粘贴桌面端新复制的链接完成配对。链接携带 `workspace` 参数,手机落地到桌面正在查看的同一工作区。
14
14
  - **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。当插件 `requirePairingForLan` 门开启(默认)时,每个非 loopback 的 `/api` 请求必须携带有效的已配对设备 cookie,因此二维码是进入暴露在局域网上的 dsh web 的唯一途径。
15
15
  - **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开)。
16
- - **远程更新**:侧边栏加载后,底部的下载触发按钮(手机图标左侧)会静默探测 npm registry 上已安装的 `@linxin666/dsh-*` 全家桶版本;发现可自动更新的新版本时,按钮显示圆点并提供“发现新版本,检查更新”提示。点击按钮打开更新面板;未安装聚合包时,检查和更新覆盖 profile 中 registry 管理的全部 `@linxin666/*` 直接依赖,本地 link / file 开发依赖会被跳过。当存在较新版本时,面板自动执行更新(在所属 dsh profile 内 `pnpm update --latest`;pnpm 缺失时依次回退 `corepack pnpm`、`npx --yes pnpm`,Windows 上经 `cmd.exe` 执行以解析 npm 安装的 `.cmd` shim;由仅 loopback 的 `/api/update/status` + `/api/update/run` 端点驱动)并请求重启 dsh web 以生效。pnpm 绿色退出后还会对照 registry 复核已装版本:绿色退出但版本纹丝不动(例如 pnpm 的 `minimumReleaseAge` 门禁静默跳过同日发布的新版本)会报告为「未更新成功」并附配置指引,而不是误报成功。锚点自身是本地 link 安装(开发模式)时,只报告 npm 状态而不更新。
16
+ - **远程更新**:侧边栏加载后,底部的下载触发按钮(手机图标左侧)会静默探测 npm registry 上已安装的 `@linxin666/dsh-*` 全家桶版本;发现可自动更新的新版本时,按钮显示圆点并提供“发现新版本,检查更新”提示。点击按钮打开更新面板;未安装聚合包时,检查和更新覆盖 profile 中 registry 管理的全部 `@linxin666/*` 直接依赖,本地 link / file 开发依赖会被跳过。当存在较新版本时,面板展示版本对比并等待确认——点击「开始更新」后执行更新(在所属 dsh profile 内 `pnpm update --latest`;pnpm 缺失时依次回退 `corepack pnpm`、`npx --yes pnpm`,Windows 上经 `cmd.exe` 执行以解析 npm 安装的 `.cmd` shim;由仅 loopback 的 `/api/update/status` + `/api/update/run` 端点驱动)并请求重启 dsh web 以生效。pnpm 绿色退出后还会对照 registry 复核已装版本:绿色退出但版本纹丝不动(例如 pnpm 的 `minimumReleaseAge` 门禁静默跳过同日发布的新版本)会报告为「未更新成功」并附配置指引,而不是误报成功。锚点自身是本地 link 安装(开发模式)时,只报告 npm 状态而不更新。
17
17
 
18
18
  ## 截图
19
19
 
@@ -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
@@ -1312,6 +1312,7 @@ window.__ModuleLoader__.load({
1312
1312
  ...workspace !== null && workspace !== "" ? { workspace } : {}
1313
1313
  };
1314
1314
  }
1315
+ /** Human-readable expiry clock, e.g. "10:35". */
1315
1316
  function formatClock(epochMs) {
1316
1317
  const date = new Date(epochMs);
1317
1318
  return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
@@ -1703,7 +1704,7 @@ window.__ModuleLoader__.load({
1703
1704
  * @param props - copy, view state, and actions.
1704
1705
  * @returns the panel element tree.
1705
1706
  */
1706
- function UpdatePanel({ t, view, onClose, onRecheck }) {
1707
+ function UpdatePanel({ t, view, onClose, onRecheck, onStartUpdate }) {
1707
1708
  const status = view.kind === "result" || view.kind === "updating" ? view.status : void 0;
1708
1709
  const title = view.kind === "done" && view.result.ok ? t("update.done") : t("update.title");
1709
1710
  const subtitle = subtitleOf(t, view);
@@ -1740,13 +1741,29 @@ window.__ModuleLoader__.load({
1740
1741
  t,
1741
1742
  status
1742
1743
  }),
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) ?? ""
1744
+ view.kind === "result" && status !== void 0 && status.mode === "npm" && status.outdated && status.error === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1745
+ className: remote_module_css_default.updateActions,
1746
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1747
+ type: "button",
1748
+ className: remote_module_css_default.updateRetry,
1749
+ onClick: () => onStartUpdate(status),
1750
+ children: t("update.start")
1748
1751
  })
1749
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackageList, { status })] }),
1752
+ }),
1753
+ view.kind === "updating" && status !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1754
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1755
+ className: remote_module_css_default.updateStatus,
1756
+ children: t("update.updating", {
1757
+ name: anchorName(status) ?? "",
1758
+ version: anchorLatest(status) ?? ""
1759
+ })
1760
+ }),
1761
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1762
+ className: remote_module_css_default.updateDetail,
1763
+ children: t("update.cooldownNotice")
1764
+ }),
1765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackageList, { status })
1766
+ ] }),
1750
1767
  view.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoneBody, {
1751
1768
  t,
1752
1769
  result: view.result
@@ -1809,16 +1826,23 @@ window.__ModuleLoader__.load({
1809
1826
  className: remote_module_css_default.updateDetail,
1810
1827
  children: t("update.offlineDetail")
1811
1828
  })] });
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
- })] });
1829
+ if (status.outdated) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1831
+ className: remote_module_css_default.updateStatus,
1832
+ children: t("update.found")
1833
+ }),
1834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1835
+ className: remote_module_css_default.updateDetail,
1836
+ children: anchor !== void 0 ? t("update.foundDetail", {
1837
+ name: anchor,
1838
+ version: latest ?? ""
1839
+ }) : ""
1840
+ }),
1841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1842
+ className: remote_module_css_default.updateDetail,
1843
+ children: t("update.cooldownNotice")
1844
+ })
1845
+ ] });
1822
1846
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1823
1847
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1824
1848
  className: remote_module_css_default.updateStatus,
@@ -1887,8 +1911,9 @@ window.__ModuleLoader__.load({
1887
1911
  /**
1888
1912
  * The sidebar update seat: the download trigger beside the remote-control
1889
1913
  * trigger plus the update panel modal. Owns the flow — probe the registry
1890
- * on open, auto-run the update when a newer release exists, report the
1891
- * outcome (restart hint on success, translated failure on error).
1914
+ * on open, show the result, and let the user start the update from the
1915
+ * result view (#507), then report the outcome (restart hint on success,
1916
+ * translated failure on error).
1892
1917
  * Component-local state per the client stack rules.
1893
1918
  */
1894
1919
  /**
@@ -1947,6 +1972,8 @@ window.__ModuleLoader__.load({
1947
1972
  kind: "result",
1948
1973
  status
1949
1974
  });
1975
+ }, [t]);
1976
+ const startUpdate = (0, react.useCallback)(async (status) => {
1950
1977
  if (status.mode !== "npm" || !status.outdated) return;
1951
1978
  setView({
1952
1979
  kind: "updating",
@@ -2010,6 +2037,9 @@ window.__ModuleLoader__.load({
2010
2037
  onClose: closePanel,
2011
2038
  onRecheck: () => {
2012
2039
  check();
2040
+ },
2041
+ onStartUpdate: (status) => {
2042
+ startUpdate(status);
2013
2043
  }
2014
2044
  })]
2015
2045
  }), document.body)] });
@@ -2058,6 +2088,7 @@ window.__ModuleLoader__.load({
2058
2088
  }, [state]);
2059
2089
  const [copied, setCopied] = (0, react.useState)(false);
2060
2090
  const eventSource = (0, react.useRef)(void 0);
2091
+ const openSeq = (0, react.useRef)(0);
2061
2092
  const workspaceId = useWorkspaces((s) => s.recentWorkspaceId);
2062
2093
  const closeEventSource = (0, react.useCallback)(() => {
2063
2094
  eventSource.current?.close();
@@ -2091,8 +2122,10 @@ window.__ModuleLoader__.load({
2091
2122
  };
2092
2123
  }, [workspaceId]);
2093
2124
  const openPanel = (0, react.useCallback)(async () => {
2125
+ const seq = ++openSeq.current;
2094
2126
  setOpen(true);
2095
2127
  const next = await mint();
2128
+ if (seq !== openSeq.current) return;
2096
2129
  setState(next);
2097
2130
  if (next.kind !== "ready" && next.kind !== "lan-required") return;
2098
2131
  const source = new EventSource("/api/pair/events");
@@ -2111,6 +2144,7 @@ window.__ModuleLoader__.load({
2111
2144
  };
2112
2145
  }, [mint]);
2113
2146
  const closePanel = (0, react.useCallback)(() => {
2147
+ openSeq.current += 1;
2114
2148
  closeEventSource();
2115
2149
  setOpen(false);
2116
2150
  }, [closeEventSource]);
@@ -2260,7 +2294,7 @@ window.__ModuleLoader__.load({
2260
2294
  }
2261
2295
  //#endregion
2262
2296
  //#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}}";
2297
+ 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
2298
  const tagId = "@linxin666/dsh-remote-web-ui/settings-card.module.css";
2265
2299
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2266
2300
  const tag = document.createElement("style");
@@ -2297,7 +2331,18 @@ window.__ModuleLoader__.load({
2297
2331
  "readOnly": "Kwoi6G_readOnly",
2298
2332
  "reset": "Kwoi6G_reset",
2299
2333
  "save": "Kwoi6G_save",
2300
- "select": "Kwoi6G_select"
2334
+ "select": "Kwoi6G_select",
2335
+ "selectButton": "Kwoi6G_selectButton",
2336
+ "selectChevron": "Kwoi6G_selectChevron",
2337
+ "selectChevronOpen": "Kwoi6G_selectChevronOpen",
2338
+ "selectLabel": "Kwoi6G_selectLabel",
2339
+ "selectOption": "Kwoi6G_selectOption",
2340
+ "selectOptionActive": "Kwoi6G_selectOptionActive",
2341
+ "selectOptionSelected": "Kwoi6G_selectOptionSelected",
2342
+ "selectPopup": "Kwoi6G_selectPopup",
2343
+ "selectPopupClose": "Kwoi6G_selectPopupClose",
2344
+ "selectPopupOpen": "Kwoi6G_selectPopupOpen",
2345
+ "selectWrap": "Kwoi6G_selectWrap"
2301
2346
  };
2302
2347
  //#endregion
2303
2348
  //#region src/client/PluginSettingsCard.tsx
@@ -2476,6 +2521,175 @@ window.__ModuleLoader__.load({
2476
2521
  ]
2477
2522
  });
2478
2523
  }
2524
+ const NON_SKIN_BODY_MARKERS = /* @__PURE__ */ new Set(["dshSkinCenter", "dshSidebarCollapsed"]);
2525
+ function isSkinActive() {
2526
+ return Object.keys(document.body.dataset).some((key) => key.startsWith("dsh") && !NON_SKIN_BODY_MARKERS.has(key));
2527
+ }
2528
+ const SELECT_CLOSE_MS = 100;
2529
+ /**
2530
+ * The shared dual-mode select control. While an appearance skin is active it
2531
+ * renders the legacy native `<select>` untouched, so element-level skin
2532
+ * selectors keep working; under the default appearance it renders a
2533
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
2534
+ * Staged cards reach it through BooleanField/ChoiceField; immediate-apply
2535
+ * editors (the side-card prefs) bind it directly through onEdit.
2536
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
2537
+ */
2538
+ function SelectField(props) {
2539
+ const { id, options, value } = props;
2540
+ const [open, setOpen] = (0, react.useState)(false);
2541
+ const [closing, setClosing] = (0, react.useState)(false);
2542
+ const [phase, setPhase] = (0, react.useState)("initial");
2543
+ const [activeIndex, setActiveIndex] = (0, react.useState)(0);
2544
+ const closeTimer = (0, react.useRef)(void 0);
2545
+ const wrapRef = (0, react.useRef)(null);
2546
+ const popupRef = (0, react.useRef)(null);
2547
+ const currentIndex = () => {
2548
+ const index = options.findIndex((option) => option.value === value);
2549
+ return index >= 0 ? index : 0;
2550
+ };
2551
+ const close = (0, react.useCallback)(() => {
2552
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
2553
+ setClosing(true);
2554
+ closeTimer.current = setTimeout(() => {
2555
+ setClosing(false);
2556
+ setOpen(false);
2557
+ }, SELECT_CLOSE_MS);
2558
+ }, []);
2559
+ const openPopup = () => {
2560
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
2561
+ setActiveIndex(currentIndex());
2562
+ setPhase("initial");
2563
+ setClosing(false);
2564
+ setOpen(true);
2565
+ };
2566
+ const commit = (index) => {
2567
+ const option = options[index];
2568
+ if (option) props.onEdit(option.value);
2569
+ close();
2570
+ };
2571
+ const onTriggerClick = () => {
2572
+ if (props.disabled) return;
2573
+ if (open && !closing) close();
2574
+ else openPopup();
2575
+ };
2576
+ const onKeyDown = (event) => {
2577
+ if (props.disabled) return;
2578
+ const count = options.length;
2579
+ switch (event.key) {
2580
+ case "ArrowDown":
2581
+ case "ArrowUp":
2582
+ case "Enter":
2583
+ case " ":
2584
+ event.preventDefault();
2585
+ if (!open) openPopup();
2586
+ else if (!closing) if (event.key === "ArrowDown") setActiveIndex((index) => (index + 1) % count);
2587
+ else if (event.key === "ArrowUp") setActiveIndex((index) => (index - 1 + count) % count);
2588
+ else commit(activeIndex);
2589
+ break;
2590
+ case "Escape":
2591
+ if (open) {
2592
+ event.preventDefault();
2593
+ event.stopPropagation();
2594
+ close();
2595
+ }
2596
+ break;
2597
+ case "Tab":
2598
+ if (open) close();
2599
+ break;
2600
+ }
2601
+ };
2602
+ (0, react.useEffect)(() => () => {
2603
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
2604
+ }, []);
2605
+ (0, react.useLayoutEffect)(() => {
2606
+ if (open && !closing && phase === "initial") {
2607
+ popupRef.current?.offsetHeight;
2608
+ setPhase("open");
2609
+ }
2610
+ }, [
2611
+ open,
2612
+ closing,
2613
+ phase
2614
+ ]);
2615
+ (0, react.useEffect)(() => {
2616
+ if (!open) return;
2617
+ const onPointerDown = (event) => {
2618
+ const target = event.target;
2619
+ if (target instanceof Node && !wrapRef.current?.contains(target)) close();
2620
+ };
2621
+ document.addEventListener("pointerdown", onPointerDown);
2622
+ return () => document.removeEventListener("pointerdown", onPointerDown);
2623
+ }, [open, close]);
2624
+ (0, react.useEffect)(() => {
2625
+ if (props.disabled && open) close();
2626
+ }, [
2627
+ props.disabled,
2628
+ open,
2629
+ close
2630
+ ]);
2631
+ if (isSkinActive()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2632
+ id,
2633
+ className: settings_card_module_css_default.select,
2634
+ value,
2635
+ disabled: props.disabled,
2636
+ onChange: (event) => {
2637
+ props.onEdit(event.target.value);
2638
+ },
2639
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2640
+ value: option.value,
2641
+ children: option.label
2642
+ }, option.value))
2643
+ });
2644
+ const label = options.find((option) => option.value === value)?.label ?? "";
2645
+ 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;
2646
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2647
+ className: settings_card_module_css_default.selectWrap,
2648
+ ref: wrapRef,
2649
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2650
+ type: "button",
2651
+ id,
2652
+ className: `${settings_card_module_css_default.select} ${settings_card_module_css_default.selectButton}`,
2653
+ disabled: props.disabled,
2654
+ "aria-haspopup": "listbox",
2655
+ "aria-expanded": open,
2656
+ "aria-activedescendant": open ? `${id}-o${activeIndex}` : void 0,
2657
+ "aria-invalid": props.invalid || void 0,
2658
+ onClick: onTriggerClick,
2659
+ onKeyDown,
2660
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2661
+ className: settings_card_module_css_default.selectLabel,
2662
+ children: label
2663
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2664
+ width: "14",
2665
+ height: "14",
2666
+ viewBox: "0 0 14 14",
2667
+ fill: "none",
2668
+ xmlns: "http://www.w3.org/2000/svg",
2669
+ className: open ? `${settings_card_module_css_default.selectChevron} ${settings_card_module_css_default.selectChevronOpen}` : settings_card_module_css_default.selectChevron,
2670
+ "aria-hidden": "true",
2671
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2672
+ 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",
2673
+ fill: "currentColor"
2674
+ })
2675
+ })]
2676
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2677
+ className: popupClass,
2678
+ role: "listbox",
2679
+ ref: popupRef,
2680
+ children: options.map((option, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2681
+ id: `${id}-o${index}`,
2682
+ role: "option",
2683
+ "aria-selected": option.value === value,
2684
+ 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}` : ""}`,
2685
+ onClick: () => {
2686
+ commit(index);
2687
+ },
2688
+ children: option.label
2689
+ }, option.value))
2690
+ }) : null]
2691
+ });
2692
+ }
2479
2693
  /** A staged boolean field: 继承 / 开 / 关. */
2480
2694
  function BooleanField(props) {
2481
2695
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2501,28 +2715,26 @@ window.__ModuleLoader__.load({
2501
2715
  })]
2502
2716
  }) : null]
2503
2717
  }),
2504
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
2505
2719
  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", {
2720
+ options: [
2721
+ {
2514
2722
  value: "",
2515
- children: props.inheritLabel
2516
- }),
2517
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2723
+ label: props.inheritLabel
2724
+ },
2725
+ {
2518
2726
  value: "true",
2519
- children: props.onLabel
2520
- }),
2521
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2727
+ label: props.onLabel
2728
+ },
2729
+ {
2522
2730
  value: "false",
2523
- children: props.offLabel
2524
- })
2525
- ]
2731
+ label: props.offLabel
2732
+ }
2733
+ ],
2734
+ value: props.text,
2735
+ disabled: props.disabled,
2736
+ invalid: props.invalid,
2737
+ onEdit: props.onEdit
2526
2738
  }),
2527
2739
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2528
2740
  className: settings_card_module_css_default.hint,
@@ -3114,9 +3326,11 @@ window.__ModuleLoader__.load({
3114
3326
  "update.upToDate": "当前已是最新版本",
3115
3327
  "update.upToDateDetail": "{name} 已是最新版本 v{version}。",
3116
3328
  "update.found": "发现新版本",
3117
- "update.foundDetail": "{name} 有新版本 v{version},正在自动更新…",
3329
+ "update.foundDetail": "{name} 有新版本 v{version}。确认后开始更新。",
3330
+ "update.start": "开始更新",
3118
3331
  "update.updating": "正在更新 {name} 至 v{version},可能需要 1-2 分钟…",
3119
- "update.updatingTitle": "正在自动更新…",
3332
+ "update.cooldownNotice": "本次更新由你主动触发,将跳过 pnpm 的 24 小时发布冷却期(供应链保护)直接安装所选版本。",
3333
+ "update.updatingTitle": "正在更新…",
3120
3334
  "update.done": "更新完成",
3121
3335
  "update.doneDetail": "所有组件已更新到最新版本。",
3122
3336
  "update.restartHint": "请重启 dsh web 使新版本生效:在终端按 Ctrl+C 停止后重新运行 dsh web。",
@@ -3219,9 +3433,11 @@ window.__ModuleLoader__.load({
3219
3433
  "update.upToDate": "Everything is up to date",
3220
3434
  "update.upToDateDetail": "{name} is at the latest version v{version}.",
3221
3435
  "update.found": "A new version is available",
3222
- "update.foundDetail": "{name} has a newer release v{version}; updating automatically…",
3436
+ "update.foundDetail": "{name} has a newer release v{version}. Confirm to start the update.",
3437
+ "update.start": "Update now",
3223
3438
  "update.updating": "Updating {name} to v{version}; this may take 1-2 minutes…",
3224
- "update.updatingTitle": "Updating automatically…",
3439
+ "update.cooldownNotice": "This user-initiated update bypasses pnpm's 24-hour release cooldown (supply-chain protection) and installs the selected version immediately.",
3440
+ "update.updatingTitle": "Updating…",
3225
3441
  "update.done": "Update complete",
3226
3442
  "update.doneDetail": "All components are up to date.",
3227
3443
  "update.restartHint": "Restart dsh web for the new version to take effect: stop it with Ctrl+C in the terminal, then run dsh web again.",