@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.9

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 (238) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +77 -468
  3. package/README.zh.md +83 -113
  4. package/lib/client.js +955 -203
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1136 -1466
  7. package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
  8. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
  9. package/lib/types/client/FooterRemoteEntry.js +1 -1
  10. package/lib/types/client/RemoteEntry.d.ts +7 -4
  11. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  12. package/lib/types/client/RemoteEntry.js +7 -10
  13. package/lib/types/client/RemotePanel.d.ts +2 -2
  14. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  15. package/lib/types/client/RemotePanel.js +2 -2
  16. package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
  17. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  18. package/lib/types/client/RemoteSettingsCard.js +56 -3
  19. package/lib/types/client/deep-link.d.ts +10 -13
  20. package/lib/types/client/deep-link.d.ts.map +1 -1
  21. package/lib/types/client/deep-link.js +14 -74
  22. package/lib/types/client/index.d.ts +2 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +73 -39
  25. package/lib/types/client/locales.d.ts +26 -12
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +36 -22
  28. package/lib/types/client/mobile-adapt.d.ts +40 -0
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -0
  30. package/lib/types/client/mobile-adapt.js +967 -0
  31. package/lib/types/client/pair-api.d.ts +24 -6
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +9 -14
  34. package/lib/types/client/remote-channel.d.ts +3 -0
  35. package/lib/types/client/remote-channel.d.ts.map +1 -1
  36. package/lib/types/client/remote-channel.js +35 -3
  37. package/lib/types/client/settings-form.d.ts +35 -43
  38. package/lib/types/client/settings-form.d.ts.map +1 -1
  39. package/lib/types/client/settings-form.js +60 -53
  40. package/lib/types/firewall.d.ts +75 -0
  41. package/lib/types/firewall.d.ts.map +1 -0
  42. package/lib/types/firewall.js +184 -0
  43. package/lib/types/gate.d.ts +11 -5
  44. package/lib/types/gate.d.ts.map +1 -1
  45. package/lib/types/gate.js +11 -5
  46. package/lib/types/index.d.ts +39 -10
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +206 -58
  49. package/lib/types/inner-auth.d.ts +32 -0
  50. package/lib/types/inner-auth.d.ts.map +1 -0
  51. package/lib/types/inner-auth.js +72 -0
  52. package/lib/types/lan-bind-plan.d.ts +55 -0
  53. package/lib/types/lan-bind-plan.d.ts.map +1 -0
  54. package/lib/types/lan-bind-plan.js +60 -0
  55. package/lib/types/lan-bind.d.ts +69 -0
  56. package/lib/types/lan-bind.d.ts.map +1 -0
  57. package/lib/types/lan-bind.js +128 -0
  58. package/lib/types/loopback-proxy.d.ts +9 -2
  59. package/lib/types/loopback-proxy.d.ts.map +1 -1
  60. package/lib/types/loopback-proxy.js +24 -2
  61. package/lib/types/pairing.d.ts +15 -3
  62. package/lib/types/pairing.d.ts.map +1 -1
  63. package/lib/types/pairing.js +20 -6
  64. package/lib/types/remote-api.d.ts +30 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +55 -42
  67. package/lib/types/remote-channel-boot.d.ts +21 -2
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +65 -3
  70. package/lib/types/remote-channel-rules.d.ts +8 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +9 -2
  73. package/lib/types/remote-methods.d.ts +45 -11
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +56 -27
  76. package/lib/types/remote-presence-pet.d.ts +51 -0
  77. package/lib/types/remote-presence-pet.d.ts.map +1 -0
  78. package/lib/types/remote-presence-pet.js +101 -0
  79. package/lib/types/routes.d.ts +76 -4
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +336 -19
  82. package/lib/types/tunnel.d.ts.map +1 -1
  83. package/lib/types/tunnel.js +7 -1
  84. package/package.json +20 -18
  85. package/src/client/FooterRemoteEntry.tsx +9 -10
  86. package/src/client/RemoteEntry.tsx +13 -14
  87. package/src/client/RemotePanel.tsx +5 -16
  88. package/src/client/RemoteSettingsCard.tsx +72 -13
  89. package/src/client/deep-link.ts +14 -75
  90. package/src/client/index.ts +77 -39
  91. package/src/client/locales.ts +36 -22
  92. package/src/client/mobile-adapt.ts +913 -0
  93. package/src/client/pair-api.ts +26 -15
  94. package/src/client/remote-channel.ts +31 -3
  95. package/src/client/settings-form.ts +78 -92
  96. package/src/dsh-home.ts +1 -1
  97. package/src/firewall.ts +230 -0
  98. package/src/gate.ts +11 -5
  99. package/src/index.ts +235 -65
  100. package/src/inner-auth.ts +85 -0
  101. package/src/lan-bind-plan.ts +81 -0
  102. package/src/lan-bind.ts +145 -0
  103. package/src/loopback-proxy.ts +34 -0
  104. package/src/pairing.persist.test.ts +1 -1
  105. package/src/pairing.ts +20 -6
  106. package/src/remote-api.ts +67 -45
  107. package/src/remote-channel-boot.ts +70 -3
  108. package/src/remote-channel-rules.ts +17 -2
  109. package/src/remote-methods.ts +58 -27
  110. package/src/remote-presence-pet.ts +129 -0
  111. package/src/routes.ts +358 -19
  112. package/src/tunnel.ts +7 -1
  113. package/assets/apple-touch-icon.png +0 -0
  114. package/assets/mobile-icon-192.png +0 -0
  115. package/assets/mobile-icon-512.png +0 -0
  116. package/assets/mobile-service-worker.js +0 -63
  117. package/lib/mobile.js +0 -17658
  118. package/lib/mobile.js.map +0 -1
  119. package/lib/types/mobile/PairRequiredView.d.ts +0 -7
  120. package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
  121. package/lib/types/mobile/PairRequiredView.js +0 -28
  122. package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
  123. package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
  124. package/lib/types/mobile/PairRequiredView.test.js +0 -24
  125. package/lib/types/mobile/api.d.ts +0 -145
  126. package/lib/types/mobile/api.d.ts.map +0 -1
  127. package/lib/types/mobile/api.js +0 -103
  128. package/lib/types/mobile/index.d.ts +0 -10
  129. package/lib/types/mobile/index.d.ts.map +0 -1
  130. package/lib/types/mobile/index.js +0 -38
  131. package/lib/types/mobile/markdown.d.ts +0 -34
  132. package/lib/types/mobile/markdown.d.ts.map +0 -1
  133. package/lib/types/mobile/markdown.js +0 -285
  134. package/lib/types/mobile/markdown.test.d.ts +0 -2
  135. package/lib/types/mobile/markdown.test.d.ts.map +0 -1
  136. package/lib/types/mobile/markdown.test.js +0 -79
  137. package/lib/types/mobile/messages.d.ts +0 -149
  138. package/lib/types/mobile/messages.d.ts.map +0 -1
  139. package/lib/types/mobile/messages.js +0 -561
  140. package/lib/types/mobile/messages.test.d.ts +0 -2
  141. package/lib/types/mobile/messages.test.d.ts.map +0 -1
  142. package/lib/types/mobile/messages.test.js +0 -356
  143. package/lib/types/mobile/mobile-styles.d.ts +0 -6
  144. package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
  145. package/lib/types/mobile/mobile-styles.js +0 -1438
  146. package/lib/types/mobile/mobile-theme.d.ts +0 -22
  147. package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
  148. package/lib/types/mobile/mobile-theme.js +0 -70
  149. package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
  150. package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
  151. package/lib/types/mobile/mobile-theme.test.js +0 -71
  152. package/lib/types/mobile/mux.d.ts +0 -128
  153. package/lib/types/mobile/mux.d.ts.map +0 -1
  154. package/lib/types/mobile/mux.js +0 -305
  155. package/lib/types/mobile/mux.test.d.ts +0 -2
  156. package/lib/types/mobile/mux.test.d.ts.map +0 -1
  157. package/lib/types/mobile/mux.test.js +0 -267
  158. package/lib/types/mobile/pairing.d.ts +0 -31
  159. package/lib/types/mobile/pairing.d.ts.map +0 -1
  160. package/lib/types/mobile/pairing.js +0 -62
  161. package/lib/types/mobile/rpc.d.ts +0 -38
  162. package/lib/types/mobile/rpc.d.ts.map +0 -1
  163. package/lib/types/mobile/rpc.js +0 -83
  164. package/lib/types/mobile/theme-toggle.d.ts +0 -12
  165. package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
  166. package/lib/types/mobile/theme-toggle.js +0 -26
  167. package/lib/types/mobile/views/App.d.ts +0 -57
  168. package/lib/types/mobile/views/App.d.ts.map +0 -1
  169. package/lib/types/mobile/views/App.js +0 -150
  170. package/lib/types/mobile/views/App.test.d.ts +0 -2
  171. package/lib/types/mobile/views/App.test.d.ts.map +0 -1
  172. package/lib/types/mobile/views/App.test.js +0 -52
  173. package/lib/types/mobile/views/ChatView.d.ts +0 -63
  174. package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
  175. package/lib/types/mobile/views/ChatView.js +0 -821
  176. package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
  177. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
  178. package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
  179. package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
  180. package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
  181. package/lib/types/mobile/views/ChatView.test.js +0 -718
  182. package/lib/types/mobile/views/SessionListView.d.ts +0 -27
  183. package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
  184. package/lib/types/mobile/views/SessionListView.js +0 -139
  185. package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
  186. package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
  187. package/lib/types/mobile/views/SessionListView.test.js +0 -167
  188. package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
  189. package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
  190. package/lib/types/mobile/views/WorkspaceView.js +0 -93
  191. package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
  192. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
  193. package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
  194. package/lib/types/mobile-api.d.ts +0 -65
  195. package/lib/types/mobile-api.d.ts.map +0 -1
  196. package/lib/types/mobile-api.js +0 -404
  197. package/lib/types/mobile-pending.d.ts +0 -38
  198. package/lib/types/mobile-pending.d.ts.map +0 -1
  199. package/lib/types/mobile-pending.js +0 -59
  200. package/lib/types/mobile-pwa.d.ts +0 -6
  201. package/lib/types/mobile-pwa.d.ts.map +0 -1
  202. package/lib/types/mobile-pwa.js +0 -17
  203. package/lib/types/mobile-routes.d.ts +0 -13
  204. package/lib/types/mobile-routes.d.ts.map +0 -1
  205. package/lib/types/mobile-routes.js +0 -217
  206. package/lib/types/paired-model-catalog.d.ts +0 -17
  207. package/lib/types/paired-model-catalog.d.ts.map +0 -1
  208. package/lib/types/paired-model-catalog.js +0 -311
  209. package/src/mobile/PairRequiredView.test.tsx +0 -28
  210. package/src/mobile/PairRequiredView.tsx +0 -58
  211. package/src/mobile/api.ts +0 -229
  212. package/src/mobile/index.tsx +0 -41
  213. package/src/mobile/markdown.test.ts +0 -91
  214. package/src/mobile/markdown.ts +0 -291
  215. package/src/mobile/messages.test.ts +0 -402
  216. package/src/mobile/messages.ts +0 -685
  217. package/src/mobile/mobile-styles.ts +0 -1439
  218. package/src/mobile/mobile-theme.test.ts +0 -84
  219. package/src/mobile/mobile-theme.ts +0 -76
  220. package/src/mobile/mux.test.ts +0 -314
  221. package/src/mobile/mux.ts +0 -336
  222. package/src/mobile/pairing.ts +0 -77
  223. package/src/mobile/rpc.ts +0 -90
  224. package/src/mobile/theme-toggle.tsx +0 -47
  225. package/src/mobile/views/App.test.tsx +0 -62
  226. package/src/mobile/views/App.tsx +0 -213
  227. package/src/mobile/views/ChatView.stream.test.tsx +0 -268
  228. package/src/mobile/views/ChatView.test.tsx +0 -816
  229. package/src/mobile/views/ChatView.tsx +0 -1342
  230. package/src/mobile/views/SessionListView.test.tsx +0 -207
  231. package/src/mobile/views/SessionListView.tsx +0 -244
  232. package/src/mobile/views/WorkspaceView.test.tsx +0 -227
  233. package/src/mobile/views/WorkspaceView.tsx +0 -245
  234. package/src/mobile-api.ts +0 -418
  235. package/src/mobile-pending.ts +0 -88
  236. package/src/mobile-pwa.ts +0 -19
  237. package/src/mobile-routes.ts +0 -231
  238. package/src/paired-model-catalog.ts +0 -272
package/lib/client.js CHANGED
@@ -32,7 +32,6 @@ window.__ModuleLoader__.load({
32
32
  let react_dom = require("react-dom");
33
33
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
34
34
  let react_jsx_runtime = require("react/jsx-runtime");
35
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
36
35
  //#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
37
36
  function r(e) {
38
37
  var t, f, n = "";
@@ -1241,7 +1240,6 @@ window.__ModuleLoader__.load({
1241
1240
  /**
1242
1241
  * Mint a fresh pairing token (one active token at a time — this invalidates
1243
1242
  * any previous link).
1244
- * @param workspaceId - optional current workspace to deep-link the phone into.
1245
1243
  * @param address - optional LAN IP literal the QR must be built from (the
1246
1244
  * default is the first interface); unknown literals refuse with
1247
1245
  * 'unknown-address'.
@@ -1249,14 +1247,11 @@ window.__ModuleLoader__.load({
1249
1247
  * 0.0.0.0), or the forbidden refusal (the loopback-only fence rejected this
1250
1248
  * origin — the panel is a desktop control endpoint).
1251
1249
  */
1252
- async function issuePair(workspaceId, address) {
1250
+ async function issuePair(address) {
1253
1251
  const response = await fetch("/api/pair/issue", {
1254
1252
  method: "POST",
1255
1253
  headers: { "content-type": "application/json" },
1256
- body: JSON.stringify({
1257
- ...workspaceId !== void 0 ? { workspaceId } : {},
1258
- ...address !== void 0 ? { address } : {}
1259
- })
1254
+ body: JSON.stringify({ ...address !== void 0 ? { address } : {} })
1260
1255
  });
1261
1256
  if (!response.ok) {
1262
1257
  if (response.status === 409) return {
@@ -1323,21 +1318,16 @@ window.__ModuleLoader__.load({
1323
1318
  async function sendHeartbeat() {
1324
1319
  await fetch("/api/pair/heartbeat", { method: "POST" });
1325
1320
  }
1326
- /** Whether the current page URL carries a pairing token / workspace target. */
1321
+ /** Whether the current page URL carries a pairing token. */
1327
1322
  function readPairParams(search) {
1328
- const params = new URLSearchParams(search);
1329
- const pair = params.get("pair");
1330
- const workspace = params.get("workspace");
1331
- return {
1332
- ...pair !== null && pair !== "" ? { pair } : {},
1333
- ...workspace !== null && workspace !== "" ? { workspace } : {}
1334
- };
1323
+ const pair = new URLSearchParams(search).get("pair");
1324
+ return pair !== null && pair !== "" ? { pair } : {};
1335
1325
  }
1336
- /** Convert an issued `/m/` link into the desktop pairing form. */
1337
- function desktopPairUrl(mobileUrl) {
1338
- const url = new URL(mobileUrl);
1339
- url.pathname = "/";
1340
- return url.href;
1326
+ /** Read the LAN-bind facts (loopback-only endpoint). */
1327
+ async function readLanBindStatus() {
1328
+ const response = await fetch("/api/pair/lan-bind");
1329
+ if (!response.ok) throw new Error(`remote-web-ui: lan-bind status failed with ${String(response.status)}`);
1330
+ return await response.json();
1341
1331
  }
1342
1332
  /** Human-readable expiry clock, e.g. "10:35". */
1343
1333
  function formatClock(epochMs) {
@@ -1632,15 +1622,15 @@ window.__ModuleLoader__.load({
1632
1622
  className: remote_module_css_default.hint,
1633
1623
  children: state.public ? t("pair.publicHint") : t("pair.hint")
1634
1624
  }),
1635
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1636
1626
  className: remote_module_css_default.pairLinks,
1637
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1627
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1638
1628
  className: remote_module_css_default.pairLinkRow,
1639
1629
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1640
1630
  className: remote_module_css_default.pairLinkText,
1641
1631
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1642
1632
  className: remote_module_css_default.pairLinkLabel,
1643
- children: t("pair.phoneLabel")
1633
+ children: t("pair.linkLabel")
1644
1634
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1645
1635
  className: remote_module_css_default.link,
1646
1636
  title: state.url,
@@ -1649,28 +1639,10 @@ window.__ModuleLoader__.load({
1649
1639
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1650
1640
  type: "button",
1651
1641
  className: remote_module_css_default.copyLink,
1652
- onClick: () => onCopy("phone", state.url),
1653
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }), copied === "phone" ? t("action.copied") : t("action.copyPhone")]
1642
+ onClick: () => onCopy(state.url),
1643
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }), copied ? t("action.copied") : t("action.copyLink")]
1654
1644
  })]
1655
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1656
- className: remote_module_css_default.pairLinkRow,
1657
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1658
- className: remote_module_css_default.pairLinkText,
1659
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1660
- className: remote_module_css_default.pairLinkLabel,
1661
- children: t("pair.desktopLabel")
1662
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1663
- className: remote_module_css_default.link,
1664
- title: desktopPairUrl(state.url),
1665
- children: desktopPairUrl(state.url)
1666
- })]
1667
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1668
- type: "button",
1669
- className: remote_module_css_default.copyLink,
1670
- onClick: () => onCopy("desktop", desktopPairUrl(state.url)),
1671
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }), copied === "desktop" ? t("action.copied") : t("action.copyDesktop")]
1672
- })]
1673
- })]
1645
+ })
1674
1646
  }),
1675
1647
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1676
1648
  className: remote_module_css_default.oneTimeHint,
@@ -2311,17 +2283,16 @@ window.__ModuleLoader__.load({
2311
2283
  * @param props - composed slot props (contract in this package).
2312
2284
  * @returns the entry element tree.
2313
2285
  */
2314
- function RemoteEntry({ wide, useWorkspaces, t }) {
2286
+ function RemoteEntry({ wide, t }) {
2315
2287
  const [open, setOpen] = (0, react.useState)(false);
2316
2288
  const [state, setState] = (0, react.useState)({ kind: "lan-required" });
2317
2289
  const stateRef = (0, react.useRef)(state);
2318
2290
  (0, react.useEffect)(() => {
2319
2291
  stateRef.current = state;
2320
2292
  }, [state]);
2321
- const [copied, setCopied] = (0, react.useState)(void 0);
2293
+ const [copied, setCopied] = (0, react.useState)(false);
2322
2294
  const eventSource = (0, react.useRef)(void 0);
2323
2295
  const openSeq = (0, react.useRef)(0);
2324
- const workspaceId = useWorkspaces((s) => s.recentWorkspaceId);
2325
2296
  const closeEventSource = (0, react.useCallback)(() => {
2326
2297
  eventSource.current?.close();
2327
2298
  eventSource.current = void 0;
@@ -2329,7 +2300,7 @@ window.__ModuleLoader__.load({
2329
2300
  const mint = (0, react.useCallback)(async (address) => {
2330
2301
  let result;
2331
2302
  try {
2332
- result = await issuePair(workspaceId, address);
2303
+ result = await issuePair(address);
2333
2304
  } catch {
2334
2305
  return { kind: "unreachable" };
2335
2306
  }
@@ -2353,7 +2324,7 @@ window.__ModuleLoader__.load({
2353
2324
  address: address ?? result.lanAddresses[0] ?? "",
2354
2325
  lanAddresses: result.lanAddresses
2355
2326
  };
2356
- }, [workspaceId]);
2327
+ }, []);
2357
2328
  const openPanel = (0, react.useCallback)(async () => {
2358
2329
  const seq = ++openSeq.current;
2359
2330
  setOpen(true);
@@ -2429,12 +2400,12 @@ window.__ModuleLoader__.load({
2429
2400
  const handlePickPublic = (0, react.useCallback)(() => {
2430
2401
  mint().then(setState);
2431
2402
  }, [mint]);
2432
- const handleCopy = (0, react.useCallback)((target, url) => {
2403
+ const handleCopy = (0, react.useCallback)((url) => {
2433
2404
  copyText(url).then((ok) => {
2434
2405
  if (!ok) return;
2435
- setCopied(target);
2406
+ setCopied(true);
2436
2407
  window.setTimeout(() => {
2437
- setCopied(void 0);
2408
+ setCopied(false);
2438
2409
  }, 1500);
2439
2410
  });
2440
2411
  }, []);
@@ -2494,8 +2465,6 @@ window.__ModuleLoader__.load({
2494
2465
  function FooterRemoteEntry(props) {
2495
2466
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemoteEntry, {
2496
2467
  wide: props.wide,
2497
- useWorkspaces: () => void 0,
2498
- useSessions: () => void 0,
2499
2468
  t: props.t
2500
2469
  });
2501
2470
  }
@@ -2987,6 +2956,19 @@ window.__ModuleLoader__.load({
2987
2956
  });
2988
2957
  }
2989
2958
  //#endregion
2959
+ //#region \0dsh-store-engine
2960
+ const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
2961
+ const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
2962
+ let engine;
2963
+ try {
2964
+ engine = require(platform);
2965
+ } catch {
2966
+ engine = require(legacy);
2967
+ }
2968
+ const createSnapshotStore = engine.createSnapshotStore;
2969
+ engine.defineStore;
2970
+ engine.shallowEqual;
2971
+ //#endregion
2990
2972
  //#region src/client/settings-form.ts
2991
2973
  /** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
2992
2974
  function numberField(field, constraints = {}) {
@@ -3080,7 +3062,7 @@ window.__ModuleLoader__.load({
3080
3062
  }
3081
3063
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
3082
3064
  bind(project) {
3083
- const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
3065
+ const store = createSnapshotStore(project());
3084
3066
  this.listeners.add(() => {
3085
3067
  store.set(project());
3086
3068
  });
@@ -3095,7 +3077,7 @@ window.__ModuleLoader__.load({
3095
3077
  exposed: snapshot.status === "ready",
3096
3078
  writable: snapshot.writable,
3097
3079
  dirty: plan.length > 0,
3098
- invalid: plan.some((item) => item.run === void 0),
3080
+ invalid: plan.some((item) => item.judge === void 0),
3099
3081
  saving: this.saving,
3100
3082
  failed: this.failed,
3101
3083
  ...this.failedReason === void 0 ? {} : { failedReason: this.failedReason }
@@ -3145,45 +3127,52 @@ window.__ModuleLoader__.load({
3145
3127
  };
3146
3128
  }
3147
3129
  /**
3148
- * Write every staged edit, then re-seed from what the Host accepted.
3130
+ * Write every staged edit in one atomic scope mutation, then re-seed from
3131
+ * what the Host accepted.
3149
3132
  *
3150
- * When the scope carries the optional batch surface (the dsh-web
3151
- * bridge scope), every planned write rides one mutation so cross-field
3152
- * validate hooks (baseURL+model) judge the batch as a unit instead of
3153
- * deadlocking on per-field writes. Otherwise the per-field loop runs.
3154
- * A field lands only when the Host reports it held the staged value; a
3155
- * landed field's draft is dropped, a failed one stays staged for the user.
3156
- * @returns settlement after every write and the read-back.
3133
+ * The whole batch rides one mutate, so cross-field validate hooks
3134
+ * (baseURL+model) judge it as a unit: the Host either applies every write
3135
+ * or refuses the batch. The 0.1.2 scope contract never rejects a refused
3136
+ * mutation — the scope recovers with a fresh Host view and resolves — so
3137
+ * resolution alone proves nothing: the outcome is judged by reading the
3138
+ * settled snapshot back, one planned write at a time, and one missed write
3139
+ * fails the whole save. A scope that still rejects on refusal (the dsh-web
3140
+ * bridge scope) reports through the same failure path with its rejection
3141
+ * message. A save that did not land keeps its drafts, so the user can
3142
+ * correct them instead of retyping.
3143
+ * @returns settlement after the mutation and the read-back.
3157
3144
  */
3158
3145
  async save() {
3159
3146
  const plan = this.plan();
3160
- const valid = plan.filter((item) => item.run !== void 0);
3147
+ const valid = plan.filter((item) => item.judge !== void 0);
3161
3148
  if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
3162
- const plannedWrites = valid.map((item) => item.op);
3163
3149
  const pending = /* @__PURE__ */ new Map();
3164
3150
  for (const item of plan) pending.set(item.field, this.staged.get(item.field));
3165
3151
  this.saving = true;
3166
3152
  this.failed = false;
3167
3153
  this.failedReason = void 0;
3168
3154
  this.publish();
3169
- const landed = /* @__PURE__ */ new Set();
3170
- const batch = this.batchedScope();
3171
- if (batch !== void 0) {
3172
- const result = await batch.mutate(plannedWrites);
3173
- if (result.ok) {
3174
- for (const field of result.fields) if (field.landed) landed.add(field.field);
3175
- } else this.failedReason = result.message;
3176
- } else for (const item of valid) if (await item.run()) landed.add(item.field);
3177
- for (const [field, before] of pending) if (landed.has(field) && this.staged.get(field) === before) this.staged.delete(field);
3155
+ const ops = valid.map((item) => item.op.op === "set" ? {
3156
+ op: "set",
3157
+ path: [item.field],
3158
+ value: item.op.value
3159
+ } : {
3160
+ op: "unset",
3161
+ path: [item.field]
3162
+ });
3163
+ let failedReason;
3164
+ try {
3165
+ await this.scope.mutate(ops);
3166
+ } catch (error) {
3167
+ failedReason = error instanceof Error ? error.message : String(error);
3168
+ }
3169
+ const landed = failedReason === void 0 && valid.every((item) => item.judge());
3170
+ for (const [field, before] of pending) if (landed && this.staged.get(field) === before) this.staged.delete(field);
3178
3171
  this.saving = false;
3179
- this.failed = landed.size !== pending.size;
3172
+ this.failed = !landed;
3173
+ this.failedReason = failedReason;
3180
3174
  this.publish();
3181
3175
  }
3182
- /** The scope's batch surface when it supports one; undefined conservatively otherwise. */
3183
- batchedScope() {
3184
- const candidate = this.scope;
3185
- return typeof candidate?.mutate === "function" ? candidate : void 0;
3186
- }
3187
3176
  /**
3188
3177
  * Every staged edit a save would write. An entry whose draft is not a value
3189
3178
  * its field accepts carries no write: the form is still dirty, and the save
@@ -3202,7 +3191,7 @@ window.__ModuleLoader__.load({
3202
3191
  field,
3203
3192
  op: "unset"
3204
3193
  },
3205
- run: () => this.clear(field)
3194
+ judge: () => this.landedUnset(field)
3206
3195
  });
3207
3196
  continue;
3208
3197
  }
@@ -3214,7 +3203,7 @@ window.__ModuleLoader__.load({
3214
3203
  field,
3215
3204
  op: "unset"
3216
3205
  },
3217
- run: void 0
3206
+ judge: void 0
3218
3207
  });
3219
3208
  else if (write.kind === "clear") plan.push({
3220
3209
  field,
@@ -3222,7 +3211,7 @@ window.__ModuleLoader__.load({
3222
3211
  field,
3223
3212
  op: "unset"
3224
3213
  },
3225
- run: () => this.clear(field)
3214
+ judge: () => this.landedUnset(field)
3226
3215
  });
3227
3216
  else plan.push({
3228
3217
  field,
@@ -3231,20 +3220,26 @@ window.__ModuleLoader__.load({
3231
3220
  op: "set",
3232
3221
  value: write.value
3233
3222
  },
3234
- run: () => this.store(field, write.value)
3223
+ judge: () => this.landedSet(field, write.value)
3235
3224
  });
3236
3225
  }
3237
3226
  return plan;
3238
3227
  }
3239
- async clear(field) {
3240
- await this.scope.unset(field);
3241
- return !this.stored(field);
3242
- }
3243
- async store(field, value) {
3244
- await this.scope.set(field, value);
3228
+ /**
3229
+ * Read-back judgment for a planned set: the user layer must hold the
3230
+ * intended value once the mutation has settled.
3231
+ */
3232
+ landedSet(field, value) {
3245
3233
  if (this.specOf(field).secret) return true;
3246
3234
  return this.userLayer()?.[field] === value;
3247
3235
  }
3236
+ /**
3237
+ * Read-back judgment for a planned unset: the field must be gone from the
3238
+ * user layer once the mutation has settled.
3239
+ */
3240
+ landedUnset(field) {
3241
+ return !this.stored(field);
3242
+ }
3248
3243
  stage(field, edit) {
3249
3244
  this.staged.set(field, edit);
3250
3245
  this.failed = false;
@@ -3278,6 +3273,11 @@ window.__ModuleLoader__.load({
3278
3273
  };
3279
3274
  //#endregion
3280
3275
  //#region src/client/RemoteSettingsCard.tsx
3276
+ /**
3277
+ * The remote-control settings card: pairing security and device limits.
3278
+ * Registers into the `web-ui.plugin.item` child slot the Web UI plugin group
3279
+ * renders, bound to the `remote-web-ui` settings namespace.
3280
+ */
3281
3281
  /** Bridges the `remote-web-ui` scope onto the card's staged form. */
3282
3282
  var RemoteSettingsCardController = class {
3283
3283
  form;
@@ -3294,7 +3294,7 @@ window.__ModuleLoader__.load({
3294
3294
  booleanField("requirePairingForLan"),
3295
3295
  textField("publicBaseUrl"),
3296
3296
  booleanField("autoTunnel"),
3297
- booleanField("mobileEnterToSend")
3297
+ booleanField("lanBind")
3298
3298
  ]);
3299
3299
  this.store = this.form.bind(() => this.projection());
3300
3300
  }
@@ -3310,7 +3310,7 @@ window.__ModuleLoader__.load({
3310
3310
  requirePairingForLan: this.form.field("requirePairingForLan"),
3311
3311
  publicBaseUrl: this.form.field("publicBaseUrl"),
3312
3312
  autoTunnel: this.form.field("autoTunnel"),
3313
- mobileEnterToSend: this.form.field("mobileEnterToSend")
3313
+ lanBind: this.form.field("lanBind")
3314
3314
  };
3315
3315
  }
3316
3316
  /**
@@ -3486,25 +3486,74 @@ window.__ModuleLoader__.load({
3486
3486
  props.resetField("autoTunnel");
3487
3487
  }
3488
3488
  }),
3489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LanBindStatus, { t }),
3489
3490
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
3490
- id: "settings-remote-mobile-enter",
3491
- label: t("settings.mobileEnterToSend"),
3492
- hint: t("settings.mobileEnterToSendHint"),
3491
+ id: "settings-remote-lan-bind",
3492
+ label: t("settings.lanBind"),
3493
+ hint: t("settings.lanBindHint"),
3493
3494
  inheritLabel: t("settings.inherit"),
3494
3495
  onLabel: t("settings.on"),
3495
3496
  offLabel: t("settings.off"),
3496
3497
  ...fieldProps,
3497
- ...state.mobileEnterToSend,
3498
+ ...state.lanBind,
3498
3499
  onEdit: (text) => {
3499
- props.edit("mobileEnterToSend", text);
3500
+ props.edit("lanBind", text);
3500
3501
  },
3501
3502
  onReset: () => {
3502
- props.resetField("mobileEnterToSend");
3503
+ props.resetField("lanBind");
3503
3504
  }
3504
3505
  })
3505
3506
  ]
3506
3507
  });
3507
3508
  }
3509
+ /**
3510
+ * Live LAN-bind facts above the toggle: the managed block's host, the live
3511
+ * bind, the firewall summary, and the reachable LAN URLs. Polls the
3512
+ * loopback-only status endpoint; a failure (non-loopback origin) renders
3513
+ * nothing — the pairing panel carries the loopback banner instead.
3514
+ */
3515
+ function LanBindStatus({ t }) {
3516
+ const [frame, setFrame] = (0, react.useState)(void 0);
3517
+ (0, react.useEffect)(() => {
3518
+ let alive = true;
3519
+ const read = () => {
3520
+ readLanBindStatus().then((value) => {
3521
+ if (alive) setFrame(value);
3522
+ }).catch(() => {});
3523
+ };
3524
+ read();
3525
+ const timer = window.setInterval(read, 1e4);
3526
+ return () => {
3527
+ alive = false;
3528
+ window.clearInterval(timer);
3529
+ };
3530
+ }, []);
3531
+ if (frame === void 0) return null;
3532
+ const lanOn = frame.blockHost === "0.0.0.0";
3533
+ const firewallText = frame.firewall.managed ? t(frame.firewall.ok ? "lan.firewall.ok" : "lan.firewall.bad") : t("lan.firewall.unmanaged");
3534
+ const lines = [t("lan.bind", {
3535
+ host: frame.bindHost,
3536
+ port: String(frame.port)
3537
+ }) + " · " + firewallText];
3538
+ if (lanOn && frame.lanUrls.length > 0) lines.push(t("lan.urls", { urls: frame.lanUrls.join(" ") }));
3539
+ if (!lanOn) lines.push(t("lan.off"));
3540
+ if (frame.setting === null) lines.push(t("lan.untouched"));
3541
+ if (frame.pendingRestart === true) lines.push(t("lan.pendingRestart"));
3542
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3543
+ style: {
3544
+ display: "flex",
3545
+ flexDirection: "column",
3546
+ gap: 2,
3547
+ fontSize: 12,
3548
+ lineHeight: "18px",
3549
+ opacity: .85
3550
+ },
3551
+ children: lines.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3552
+ style: { wordBreak: "break-all" },
3553
+ children: line
3554
+ }, index))
3555
+ });
3556
+ }
3508
3557
  //#endregion
3509
3558
  //#region src/client/locales.ts
3510
3559
  /** `remote` namespace dictionaries: the remote-access surface copy. */
@@ -3512,15 +3561,15 @@ window.__ModuleLoader__.load({
3512
3561
  const zh = {
3513
3562
  "entry.label": "远程访问",
3514
3563
  "title": "远程访问",
3515
- "subtitle": "通过手机或另一台电脑配对,远程访问当前工作区",
3564
+ "subtitle": "通过手机或另一台电脑配对,远程使用同一份 Web 界面(官方界面 + 移动端适配)",
3516
3565
  "card.title": "设备配对",
3517
3566
  "public.badge": "公网",
3518
3567
  "status.waiting": "等待设备连接",
3519
3568
  "status.connected": "已连接 {n} 台设备",
3520
3569
  "status.disconnected": "已配对设备离线",
3521
3570
  "status.stopped": "已停止远程访问",
3522
- "status.lanRequired": "此功能需要以 --host 0.0.0.0 启动 dsh web,或配置公网地址才能使用",
3523
- "status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请用 dsh web --host 0.0.0.0 重新启动,或在设置中填写内网穿透的公网地址。",
3571
+ "status.lanRequired": "此功能需要局域网绑定或公网地址才能使用",
3572
+ "status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。可在下方设置卡片打开“局域网访问”,或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。",
3524
3573
  "status.loopbackRequired": "配对面板仅限本机使用",
3525
3574
  "status.loopbackRequiredHint": "请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。",
3526
3575
  "status.unreachable": "无法连接配对服务",
@@ -3533,9 +3582,8 @@ window.__ModuleLoader__.load({
3533
3582
  "address.hint": "远程设备不在同一网络时请使用公网地址;局域网地址仅限同一网络内使用。",
3534
3583
  "pair.expires": "二维码有效至 {time}",
3535
3584
  "pair.expired": "二维码已过期,请刷新",
3536
- "pair.phoneLabel": "手机配对链接",
3537
- "pair.desktopLabel": "电脑配对链接",
3538
- "pair.oneTimeHint": "两条链接共用同一枚一次性令牌;任意设备配对成功后,另一条链接立即失效。",
3585
+ "pair.linkLabel": "配对链接",
3586
+ "pair.oneTimeHint": "链接含一次性令牌;任一设备配对成功后立即失效,配对下一台设备请刷新二维码。",
3539
3587
  "pair.failed.title": "配对失败",
3540
3588
  "pair.failed.detail": "链接无效或已使用,请回到电脑端刷新二维码后重新扫码。",
3541
3589
  "fence.unpaired.title": "此设备未配对,无法访问工作区数据",
@@ -3557,8 +3605,7 @@ window.__ModuleLoader__.load({
3557
3605
  "action.stop": "停止",
3558
3606
  "action.refresh": "刷新二维码",
3559
3607
  "action.copy": "复制链接",
3560
- "action.copyPhone": "复制手机链接",
3561
- "action.copyDesktop": "复制电脑链接",
3608
+ "action.copyLink": "复制链接",
3562
3609
  "action.copied": "已复制",
3563
3610
  "devices.title": "已授权设备",
3564
3611
  "devices.empty": "还没有已配对的设备。扫码或打开链接后会出现在这里。",
@@ -3593,14 +3640,23 @@ window.__ModuleLoader__.load({
3593
3640
  "settings.publicBaseUrlHint": "内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com。填写后二维码将生成公网链接,手机不在同一网络也能配对;留空则仅局域网可用。",
3594
3641
  "settings.autoTunnel": "自动公网隧道",
3595
3642
  "settings.autoTunnelHint": "开启后插件自动启动 Cloudflare quick tunnel(无需安装任何工具),并自动更新公网地址与信任配置,手机随时可用公网配对;关闭时使用上方手动填写的公网地址。",
3596
- "settings.mobileEnterToSend": "移动端回车发送",
3597
- "settings.mobileEnterToSendHint": "开启:移动端输入框按 Enter 直接发送,Shift+Enter 换行;关闭:Enter 换行,仅点发送按钮发送。",
3643
+ "settings.lanBind": "局域网访问(绑定 0.0.0.0)",
3644
+ "settings.lanBindHint": "开启后插件把绑定默认改写为 0.0.0.0 并写入 profile 补丁(显式 --host 仍优先),同时维护主机防火墙放行(Windows/Linux;macOS 无需管理);关闭回退 127.0.0.1。绑定变化通常在重启 dsh web 后生效。",
3645
+ "lan.cardTitle": "局域网访问",
3646
+ "lan.bind": "绑定:{host}:{port}",
3647
+ "lan.off": "当前仅本机可访问。",
3648
+ "lan.urls": "局域网地址:{urls}",
3649
+ "lan.firewall.ok": "防火墙已放行",
3650
+ "lan.firewall.bad": "防火墙未放行(需管理员权限)",
3651
+ "lan.firewall.unmanaged": "防火墙不由插件管理(未检测到受支持的防火墙)",
3652
+ "lan.untouched": "开关尚未使用:打开后插件才会改写 profile 补丁。",
3653
+ "lan.pendingRestart": "绑定将在重启 dsh web 后生效。",
3598
3654
  "settings.inherit": "继承",
3599
3655
  "settings.on": "开",
3600
3656
  "settings.off": "关",
3601
3657
  "settings.overridden": "已覆盖",
3602
3658
  "settings.reset": "恢复默认",
3603
- "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置,或为 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单补充本命名空间后重启。",
3659
+ "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 $DSH_HOME/settings.yaml 直接配置,或确认提供该命名空间的插件已挂载其设置域并重启。",
3604
3660
  "settings.readOnly": "当前部署的设置只读。",
3605
3661
  "settings.expand": "展开设置",
3606
3662
  "settings.collapse": "收起设置",
@@ -3656,15 +3712,15 @@ window.__ModuleLoader__.load({
3656
3712
  const en = {
3657
3713
  "entry.label": "Remote access",
3658
3714
  "title": "Remote access",
3659
- "subtitle": "Pair a phone or another computer to access this workspace remotely",
3715
+ "subtitle": "Pair a phone or another computer to share the same Web GUI (official UI + mobile adaptation)",
3660
3716
  "card.title": "Pair a device",
3661
3717
  "public.badge": "Public",
3662
3718
  "status.waiting": "Waiting for a device",
3663
3719
  "status.connected": "{n} device(s) connected",
3664
3720
  "status.disconnected": "Paired devices offline",
3665
3721
  "status.stopped": "Remote access stopped",
3666
- "status.lanRequired": "This feature needs dsh web started with --host 0.0.0.0, or a configured public address",
3667
- "status.lanRequiredHint": "The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Restart with dsh web --host 0.0.0.0, or set the tunneled public address in settings.",
3722
+ "status.lanRequired": "This feature needs a LAN bind or a public address",
3723
+ "status.lanRequiredHint": "The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in the settings card below, restart with dsh web --host 0.0.0.0, or set the tunneled public address in settings.",
3668
3724
  "status.loopbackRequired": "The pairing panel works on this machine only",
3669
3725
  "status.loopbackRequiredHint": "Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.",
3670
3726
  "status.unreachable": "Cannot reach the pairing service",
@@ -3677,9 +3733,8 @@ window.__ModuleLoader__.load({
3677
3733
  "address.hint": "Use the public address when the remote device is on another network; LAN addresses only work on the same network.",
3678
3734
  "pair.expires": "QR code valid until {time}",
3679
3735
  "pair.expired": "QR code expired — refresh it",
3680
- "pair.phoneLabel": "Phone pairing link",
3681
- "pair.desktopLabel": "Computer pairing link",
3682
- "pair.oneTimeHint": "Both links share one single-use token. Pairing either device invalidates the other link immediately.",
3736
+ "pair.linkLabel": "Pairing link",
3737
+ "pair.oneTimeHint": "The link carries one single-use token; it dies as soon as any device pairs. Refresh the QR to pair the next device.",
3683
3738
  "pair.failed.title": "Pairing failed",
3684
3739
  "pair.failed.detail": "The link is invalid or was already used. Refresh the QR code on your computer and scan again.",
3685
3740
  "fence.unpaired.title": "This device is not paired and cannot reach workspace data",
@@ -3701,8 +3756,7 @@ window.__ModuleLoader__.load({
3701
3756
  "action.stop": "Stop",
3702
3757
  "action.refresh": "Refresh QR",
3703
3758
  "action.copy": "Copy link",
3704
- "action.copyPhone": "Copy phone link",
3705
- "action.copyDesktop": "Copy computer link",
3759
+ "action.copyLink": "Copy link",
3706
3760
  "action.copied": "Copied",
3707
3761
  "devices.title": "Authorized devices",
3708
3762
  "devices.empty": "No paired devices yet. Scan the QR or open the link to add one.",
@@ -3737,14 +3791,23 @@ window.__ModuleLoader__.load({
3737
3791
  "settings.publicBaseUrlHint": "The public URL of a tunnel in front of this server, e.g. a Cloudflare Tunnel: https://xxx.trycloudflare.com. When set, the QR link is built from it so a phone anywhere can pair; leave blank for LAN-only usage.",
3738
3792
  "settings.autoTunnel": "Auto public tunnel",
3739
3793
  "settings.autoTunnelHint": "When on, the plugin runs its own Cloudflare quick tunnel (no tool installation needed) and keeps the public address and trust config in sync automatically, so a phone anywhere can pair at any time; when off, the manually entered public address above applies.",
3740
- "settings.mobileEnterToSend": "Enter to send on mobile",
3741
- "settings.mobileEnterToSendHint": "On: Enter in the mobile composer sends immediately and Shift+Enter inserts a newline. Off: Enter inserts a newline and only the send button sends.",
3794
+ "settings.lanBind": "LAN access (bind 0.0.0.0)",
3795
+ "settings.lanBindHint": "When on, the plugin writes a managed block into the profile patch defaulting the bind to 0.0.0.0 (an explicit --host flag still wins), and maintains the matching host firewall rule (Windows/Linux; other platforms need none). When off, the block pins 127.0.0.1. The bind change usually takes effect after dsh web restarts.",
3796
+ "lan.cardTitle": "LAN access",
3797
+ "lan.bind": "Bind: {host}:{port}",
3798
+ "lan.off": "Currently localhost-only.",
3799
+ "lan.urls": "LAN URLs: {urls}",
3800
+ "lan.firewall.ok": "firewall open",
3801
+ "lan.firewall.bad": "firewall blocked (needs admin)",
3802
+ "lan.firewall.unmanaged": "firewall not managed (no supported firewall detected)",
3803
+ "lan.untouched": "The toggle has never been used; the profile patch is only written once you flip it.",
3804
+ "lan.pendingRestart": "The bind change takes effect after dsh web restarts.",
3742
3805
  "settings.inherit": "Inherit",
3743
3806
  "settings.on": "On",
3744
3807
  "settings.off": "Off",
3745
3808
  "settings.overridden": "Overridden",
3746
3809
  "settings.reset": "Reset to default",
3747
- "settings.notExposed": "This DSH version does not expose this plugin's settings namespace to the configuration page, so the form is unavailable. Edit ~/.dsh/settings.yaml directly, or add the namespace to dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES allowlist and restart.",
3810
+ "settings.notExposed": "This DSH version does not expose this plugin's settings namespace to the configuration page, so the form is unavailable. Edit $DSH_HOME/settings.yaml directly, or confirm that the plugin owning the namespace is mounted with its settings domain and restart.",
3748
3811
  "settings.readOnly": "This deployment stores settings read-only.",
3749
3812
  "settings.expand": "Show settings",
3750
3813
  "settings.collapse": "Hide settings",
@@ -3800,8 +3863,6 @@ window.__ModuleLoader__.load({
3800
3863
  //#region src/client/deep-link.ts
3801
3864
  /** sessionStorage key for the failed-pair notice. */
3802
3865
  const PAIR_FAILED_MARKER = "dsh-remote-pair-failed";
3803
- /** Poll budget for the runtime services (activation order is unconstrained). */
3804
- const SERVICE_WAIT_MS = 1e4;
3805
3866
  /** The browser implementation of {@link PageSurface}. */
3806
3867
  const browserPage = {
3807
3868
  get href() {
@@ -3817,26 +3878,18 @@ window.__ModuleLoader__.load({
3817
3878
  window.location.reload();
3818
3879
  }
3819
3880
  };
3820
- /** Whether this browser looks like a phone/tablet (the simplified mobile surface). */
3821
- function isMobileSurface() {
3822
- if (typeof navigator === "undefined") return false;
3823
- return /Android|iPhone|iPad|iPod|Mobile|mobile/i.test(navigator.userAgent);
3824
- }
3825
3881
  /**
3826
- * Run the pair/workspace boot flow for this page load.
3827
- * @param ctx - client root context (workspaces/sessions read at need time).
3882
+ * Run the pair boot flow for this page load.
3883
+ * @param ctx - client root context (unused since the deep-link retirement;
3884
+ * kept for call-site stability).
3828
3885
  * @param search - the current location.search.
3829
3886
  * @param page - the page surface (defaults to the browser).
3830
3887
  */
3831
3888
  function runPairBootFlow(ctx, search, page = browserPage) {
3832
3889
  const params = readPairParams(search);
3833
- if (params.pair !== void 0) {
3834
- runAccept(params.pair, page);
3835
- return;
3836
- }
3837
- if (params.workspace !== void 0) runDeepLink(ctx, params.workspace, page);
3890
+ if (params.pair !== void 0) runAccept(params.pair, page);
3838
3891
  }
3839
- /** Accept the token, then enter the matching desktop/mobile surface. */
3892
+ /** Accept the token, then reload into the paired official UI. */
3840
3893
  async function runAccept(token, page) {
3841
3894
  let ok = false;
3842
3895
  try {
@@ -3848,42 +3901,22 @@ window.__ModuleLoader__.load({
3848
3901
  const url = new URL(page.href);
3849
3902
  url.searchParams.delete("pair");
3850
3903
  page.replaceState(`${url.pathname}${url.search}${url.hash}`);
3851
- if (ok) if (isMobileSurface()) {
3852
- url.pathname = "/m/";
3853
- page.navigate(`${url.pathname}${url.search}${url.hash}`);
3854
- } else page.reload();
3904
+ if (ok) page.reload();
3855
3905
  }
3906
+ /** Connection-plugin method prefix under the gated channel. */
3907
+ const REMOTE_API_PREFIX$1 = `/remote/api`;
3908
+ ({ mux: `${REMOTE_API_PREFIX$1}/remote.mux` }).mux, `${REMOTE_API_PREFIX$1}`;
3856
3909
  /**
3857
- * Connect the deep-linked workspace and open its session. Waits for the
3858
- * runtime services AND for the target workspace to appear in the workspace
3859
- * list (both are asynchronous after boot), then connects and opens; gives
3860
- * up silently within the budget — the workspace param is stripped either
3861
- * way, so a late failure cannot loop.
3862
- * @param ctx - client root context.
3863
- * @param workspaceId - the target workspace.
3864
- * @param page - the page surface.
3910
+ * The cookieless device credential: the boot patch reads the device id from
3911
+ * the /pair-app URL, keeps it in sessionStorage, and attaches it to every
3912
+ * gated HTTP call as this header (and to WebSocket upgrades as the `device`
3913
+ * query parameter - WS handshakes cannot carry headers from the Web API).
3914
+ * The channel gate accepts it exactly like the device cookie, so the mobile
3915
+ * flow works even with browser cookies fully blocked; the cookie remains the
3916
+ * primary credential on normal browsers.
3865
3917
  */
3866
- async function runDeepLink(ctx, workspaceId, page) {
3867
- const target = workspaceId;
3868
- const deadline = Date.now() + SERVICE_WAIT_MS;
3869
- while (Date.now() < deadline) {
3870
- const workspaces = ctx.get("workspaces");
3871
- const sessions = ctx.get("sessions");
3872
- if (workspaces !== void 0 && sessions !== void 0) {
3873
- if (workspaces.list.getSnapshot().items.some((item) => item.workspaceId === target)) {
3874
- try {
3875
- const sessionId = await workspaces.connectWorkspace(target);
3876
- sessions.open(sessionId);
3877
- } catch {}
3878
- break;
3879
- }
3880
- }
3881
- await new Promise((resolve) => setTimeout(resolve, 200));
3882
- }
3883
- const url = new URL(page.href);
3884
- url.searchParams.delete("workspace");
3885
- page.replaceState(`${url.pathname}${url.search}${url.hash}`);
3886
- }
3918
+ const REMOTE_DEVICE_HEADER = "x-dsh-remote-device";
3919
+ const REMOTE_DEVICE_QUERY = "device";
3887
3920
  //#endregion
3888
3921
  //#region src/remote-channel-rules.ts
3889
3922
  /**
@@ -3907,12 +3940,14 @@ window.__ModuleLoader__.load({
3907
3940
  gitPrefix: "/git/",
3908
3941
  petPrefix: "/pet/",
3909
3942
  wsPaths: [
3910
- "/api/events.mux",
3911
- "/api/events.host",
3943
+ "/api/remote.mux",
3912
3944
  "/sidebar/ws/terminal",
3913
3945
  "/sidebar/ws/agent-terminals",
3914
3946
  "/api/dsh-ssh/terminal"
3915
- ]
3947
+ ],
3948
+ deviceHeader: REMOTE_DEVICE_HEADER,
3949
+ deviceKey: "dsh-remote-device",
3950
+ deviceQuery: REMOTE_DEVICE_QUERY
3916
3951
  };
3917
3952
  /** The window global the boot patch publishes its seat under. */
3918
3953
  const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
@@ -4065,13 +4100,42 @@ window.__ModuleLoader__.load({
4065
4100
  const OriginalEventSource = window.EventSource;
4066
4101
  const sameOrigin = (url) => url.origin === window.location.origin;
4067
4102
  const rewrite = (raw) => rewriteRawUrl(raw, window.location.href, window.location.origin);
4103
+ const device = (() => {
4104
+ try {
4105
+ return window.sessionStorage?.getItem(RULES.deviceKey) ?? null;
4106
+ } catch {
4107
+ return null;
4108
+ }
4109
+ })();
4110
+ const attach = (init) => {
4111
+ if (device === null) return init;
4112
+ const headers = init?.headers;
4113
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
4114
+ try {
4115
+ headers.set(RULES.deviceHeader, device);
4116
+ } catch {}
4117
+ return init;
4118
+ }
4119
+ if (typeof headers === "object" && headers !== null) return {
4120
+ ...init,
4121
+ headers: {
4122
+ ...headers,
4123
+ [RULES.deviceHeader]: device
4124
+ }
4125
+ };
4126
+ return init;
4127
+ };
4128
+ const withDeviceQuery = (url) => {
4129
+ if (device !== null) url.searchParams.set(RULES.deviceQuery, device);
4130
+ return url;
4131
+ };
4068
4132
  const patchedFetch = (input, init) => {
4069
4133
  const url = new URL(typeof input === "string" || input instanceof URL ? input.toString() : input.url, window.location.href);
4070
4134
  if (sameOrigin(url) && shouldRewriteFetchPath(url.pathname)) {
4071
4135
  const rewritten = new URL(url);
4072
4136
  rewritten.pathname = rewritePath(url.pathname);
4073
4137
  const next = typeof input === "string" || input instanceof URL ? rewritten.toString() : new Request(rewritten, input);
4074
- return Promise.resolve(originalFetch.call(window, next, init)).then(async (response) => {
4138
+ return Promise.resolve(originalFetch.call(window, next, attach(init))).then(async (response) => {
4075
4139
  if (await isUnpairedDenied(response.clone())) options.onUnpaired?.();
4076
4140
  else options.onPaired?.();
4077
4141
  return response;
@@ -4086,7 +4150,7 @@ window.__ModuleLoader__.load({
4086
4150
  if (wsOrigin !== "" && wsOrigin === window.location.origin && shouldRewriteWsPath(parsed.pathname)) {
4087
4151
  const rewritten = new URL(parsed);
4088
4152
  rewritten.pathname = rewritePath(parsed.pathname);
4089
- super(rewritten, protocols);
4153
+ super(withDeviceQuery(rewritten), protocols);
4090
4154
  return;
4091
4155
  }
4092
4156
  super(url, protocols);
@@ -4106,7 +4170,7 @@ window.__ModuleLoader__.load({
4106
4170
  if (sameOrigin(parsed) && shouldRewriteFetchPath(parsed.pathname)) {
4107
4171
  const rewritten = new URL(parsed);
4108
4172
  rewritten.pathname = rewritePath(parsed.pathname);
4109
- super(rewritten, eventSourceInitDict);
4173
+ super(withDeviceQuery(rewritten), eventSourceInitDict);
4110
4174
  return;
4111
4175
  }
4112
4176
  super(url, eventSourceInitDict);
@@ -4269,7 +4333,7 @@ window.__ModuleLoader__.load({
4269
4333
  /** The building package's version, when the bundle carries it. */
4270
4334
  function bakedVersion() {
4271
4335
  try {
4272
- return "0.3.6";
4336
+ return "0.3.9";
4273
4337
  } catch {
4274
4338
  return;
4275
4339
  }
@@ -4332,15 +4396,698 @@ window.__ModuleLoader__.load({
4332
4396
  } catch {}
4333
4397
  }
4334
4398
  //#endregion
4399
+ //#region src/client/mobile-adapt.ts
4400
+ /** Storage key for the manual desktop opt-out. */
4401
+ const FORCE_DESKTOP_KEY = "dsh-remote-force-desktop";
4402
+ /** Storage key for the dragged whale position. */
4403
+ const WHALE_POS_KEY = "dsh-remote-whale-pos";
4404
+ /** Injected stylesheet identity. */
4405
+ const ADAPT_CSS_ID = "dsh-remote-web-ui/mobile-adapt.css";
4406
+ /** Body class while the adaptation is active. */
4407
+ const ACTIVE_CLASS = "dsh-remote-portrait";
4408
+ /** Body class while the collapsed rail is hidden behind the whale. */
4409
+ const RAIL_HIDDEN_CLASS = "dsh-remote-rail-hidden";
4410
+ /** Whale button id. */
4411
+ const WHALE_ID = "dshRemoteWhale";
4412
+ /** Compact picker: synthesized model button id. */
4413
+ const MODEL_BTN_ID = "dshRemoteModelPick";
4414
+ /** Compact picker: synthesized effort button id. */
4415
+ const EFFORT_BTN_ID = "dshRemoteEffortPick";
4416
+ /** Body class while the compact picker buttons are wired. */
4417
+ const COMPACT_CLASS = "dsh-remote-compact-picker";
4418
+ /**
4419
+ * Whether the current viewport is a portrait touch device small enough to
4420
+ * need the adaptation.
4421
+ */
4422
+ function isMobilePortrait() {
4423
+ if (typeof window === "undefined") return false;
4424
+ if (typeof window.matchMedia !== "function") return false;
4425
+ if (!window.matchMedia("(orientation: portrait)").matches) return false;
4426
+ if (!window.matchMedia("(pointer: coarse)").matches) return false;
4427
+ if (window.innerWidth >= 1100) return false;
4428
+ if (window.sessionStorage.getItem(FORCE_DESKTOP_KEY) === "1") return false;
4429
+ return true;
4430
+ }
4431
+ /**
4432
+ * The injected rules. Grouped by surface; version comments trace rules back
4433
+ * to the dsh-LAN reference they were ported from.
4434
+ */
4435
+ const ADAPT_CSS = [
4436
+ "html,body{height:100%}",
4437
+ "[class$=\"_frame\"]{width:100%;height:100dvh}",
4438
+ "[class$=\"_railFish\"] button,[class$=\"_panelIcon\"],[class$=\"_newSession\"]{min-width:44px;min-height:44px}",
4439
+ "[class$=\"_scroll\"]{padding:8px 10px}",
4440
+ "[class$=\"_input\"],textarea,input{font-size:16px}",
4441
+ "[class$=\"_composer\"]{padding-bottom:calc(4px + env(safe-area-inset-bottom))}",
4442
+ "[class$=\"_scrollBody\"] [class$=\"_root\"]{font-size:14.5px}",
4443
+ "[class$=\"_scrollBody\"] [class$=\"_bubble\"]{font-size:14.5px}",
4444
+ "[class$=\"_titleRow\"] *{font-size:13px}",
4445
+ "[class$=\"_sidebarCol\"] [class$=\"_root\"],[class$=\"_sidebarCol\"] [class$=\"_newSession\"],[class$=\"_sidebarCol\"] [class$=\"_trigger\"],[class$=\"_sidebarCol\"] [class$=\"_title\"]{font-size:13px}",
4446
+ "[class$=\"_sidebarCol\"] [class$=\"_meta\"],[class$=\"_sidebarCol\"] [class$=\"_time\"]{font-size:11.5px}",
4447
+ "[class$=\"_frame\"][data-sidebar-collapsed]{grid-template-columns:0 minmax(0,1fr) 0 !important}",
4448
+ "[class$=\"_frame\"][data-sidebar-collapsed] [class$=\"_sidebarCol\"]{grid-column:1/2}",
4449
+ "[class$=\"_frame\"][data-sidebar-collapsed] [class$=\"_centerCol\"]{grid-column:2/3}",
4450
+ "[class$=\"_frame\"][data-sidebar-collapsed] [class$=\"_detailsCol\"]{grid-column:3/4}",
4451
+ `body.${RAIL_HIDDEN_CLASS} [class$="_titleRow"]{padding-left:52px}`,
4452
+ `#${WHALE_ID}{position:fixed;top:calc(4px + env(safe-area-inset-top));left:calc(8px + env(safe-area-inset-left));z-index:2147482999;width:34px;height:34px;min-width:34px;padding:0;border-radius:10px;background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 1px 6px rgba(0,0,0,.25)}`,
4453
+ `#${WHALE_ID} svg{width:20px;height:15px;display:block}`,
4454
+ `#${WHALE_ID}:active{opacity:.72}`,
4455
+ `#${WHALE_ID}{touch-action:none}`,
4456
+ "[class$=\"_sidebarCol\"] [class$=\"_logoRow\"] [class*=\"_iconButton\"]{width:18px;height:18px}",
4457
+ "[class$=\"_sidebarCol\"] [class$=\"_logoRow\"] [class*=\"_iconButton\"] svg{width:13px;height:13px;min-width:0;min-height:0}",
4458
+ "[class$=\"_sidebarCol\"] [class$=\"_projectRow\"] [class$=\"_rowActions\"]{display:inline-flex}",
4459
+ "[class$=\"_sidebarCol\"] [class$=\"_sessionRow\"],[class$=\"_sidebarCol\"] [class$=\"_projectRow\"]{-webkit-user-drag:none;user-select:none}",
4460
+ "[class$=\"_headerUtilities\"]{display:none}",
4461
+ "[class$=\"_composerSeat\"] [class$=\"_card\"]{margin-bottom:1px}",
4462
+ "[class$=\"_composerSeat\"] [class$=\"_row\"]{flex-wrap:wrap;row-gap:0;padding:2px 8px 1px;position:relative}",
4463
+ "[class$=\"_composerSeat\"] [class$=\"_add\"]{position:absolute;left:8px;top:50%;transform:translateY(-50%)}",
4464
+ "[class$=\"_composerSeat\"] [class$=\"_modes\"]{min-width:0;padding-left:38px}",
4465
+ "[class$=\"_composerSeat\"] [class$=\"_trailing\"]{flex-basis:100%;position:relative;min-height:32px;justify-content:flex-start;padding-left:38px;padding-right:78px}",
4466
+ "[class$=\"_composerSeat\"] [class$=\"_trailing\"] *{font-size:12px}",
4467
+ "[class$=\"_composerSeat\"] [class$=\"_modes\"] [class$=\"_trigger\"]{height:24px;min-height:24px;font-size:12px}",
4468
+ "[class$=\"_composerSeat\"] [class$=\"_trailing\"] [class$=\"_trigger\"]{height:24px;min-height:24px;font-size:11px}",
4469
+ "[class$=\"_composerSeat\"] [class$=\"_trailing\"] > [class$=\"_root\"]:has([class$=\"_track\"]){position:absolute;right:52px;top:50%;transform:translateY(-50%)}",
4470
+ "[class$=\"_composerSeat\"] [class$=\"_primary\"]{position:absolute;right:8px;top:50%;transform:translateY(-50%)}",
4471
+ "[class$=\"_composerSeat\"]{transform:none !important}",
4472
+ "[class$=\"_composerSeat\"] [class$=\"_menu\"]{position:fixed !important;left:8px !important;right:8px !important;top:auto !important;bottom:calc(8px + env(safe-area-inset-bottom)) !important;width:auto !important;max-width:none !important;max-height:70dvh !important;overflow-y:auto !important;z-index:2147482000}",
4473
+ "[class$=\"_composerSeat\"] [class$=\"_menu\"] [class$=\"_cell\"]{height:44px;min-height:44px;font-size:13px}",
4474
+ `body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]:has([class$="_triggerEffort"]){display:none}`,
4475
+ `body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"]{flex-basis:auto;position:static;min-height:0;padding:0;width:0}`,
4476
+ `body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] > [class$="_root"]:has([class$="_track"]){right:44px}`,
4477
+ `#${MODEL_BTN_ID},#${EFFORT_BTN_ID}{width:26px;height:32px;min-width:26px;padding:0;border-radius:9px;background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:center;cursor:pointer;flex:none;margin-left:4px}`,
4478
+ `#${MODEL_BTN_ID} svg,#${EFFORT_BTN_ID} svg{width:16px;height:16px;display:block}`,
4479
+ `#${MODEL_BTN_ID}:active,#${EFFORT_BTN_ID}:active{opacity:.7}`,
4480
+ "[class$=\"_composerSeat\"] [data-slot=\"conversation.composer.dock\"] [class$=\"_root\"]{font-size:10px;white-space:normal;word-break:break-word;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-align:left;line-height:13px;letter-spacing:-0.2px;padding-left:0;padding-right:0}",
4481
+ "[class$=\"_scrollBody\"] [class$=\"_root\"]{font-size:13px}",
4482
+ "[class*=\"_bubble\"][role=\"tooltip\"]{display:none}",
4483
+ "[class$=\"_composerSeat\"] [class$=\"_frame\"]{box-sizing:border-box;width:100%;max-width:100%;padding-left:12px;padding-right:12px;height:auto;max-height:calc(100dvh - 96px);align-items:flex-start;overflow-y:auto}",
4484
+ "[class$=\"_composerSeat\"] [class$=\"_frame\"] [class$=\"_card\"]{max-width:none;width:100%}",
4485
+ "[class$=\"_header\"] [class$=\"_titleCluster\"] [class$=\"_headerActions\"]{display:none}",
4486
+ "[class$=\"_header\"] [class$=\"_tabs\"]{margin-right:-58px}",
4487
+ "[class$=\"_header\"] [class$=\"_tabs\"] [class*=\"_tab\"]{font-size:12px;white-space:nowrap}",
4488
+ "[class$=\"_header\"] [class$=\"_tabs\"] [class$=\"_headerActions\"]{margin-left:auto;display:flex;align-items:center;gap:6px;flex:none}",
4489
+ `body.${ACTIVE_CLASS} [class$=\"_detailsCol\"]{display:none !important}`,
4490
+ `body.${ACTIVE_CLASS} [data-dsh-plugin=\"ssh\"],`,
4491
+ `body.${ACTIVE_CLASS} [data-dsh-plugin=\"skill-explorer\"],`,
4492
+ `body.${ACTIVE_CLASS} [data-dsh-plugin=\"task-board\"],`,
4493
+ `body.${ACTIVE_CLASS} [data-dsh-plugin=\"git-graph\"],`,
4494
+ `body.${ACTIVE_CLASS} [data-dsh-plugin=\"pet\"],`,
4495
+ `body.${ACTIVE_CLASS} [data-dsh-plugin=\"perf\"],`,
4496
+ `body.${ACTIVE_CLASS} [data-dsh-plugin=\"usage\"]{display:none !important}`,
4497
+ `body.${ACTIVE_CLASS} [class$=\"_overlayLayer\"] [class$=\"_workbench\"]{display:none !important}`,
4498
+ "[class$=\"_overlay\"] [class$=\"_panel\"]{flex-direction:column;max-height:calc(100dvh - 32px)}",
4499
+ "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_nav\"]{flex-direction:row;gap:4px;width:100%;padding:12px 12px 0;overflow-x:auto;overflow-y:hidden}",
4500
+ "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_navTitle\"]{display:none}",
4501
+ "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_navList\"]{flex-direction:row;gap:4px}",
4502
+ "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_navCell\"]{height:34px;padding:0 12px;gap:6px;flex:none;border-radius:10px}",
4503
+ "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_navLabel\"]{font-size:13px}",
4504
+ "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_content\"]{flex:1;min-height:0}"
4505
+ ];
4506
+ /** Cube glyph for the compact model button (a plain box outline). */
4507
+ const CUBE_ICON = "<path d=\"M21 8a2 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.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>";
4508
+ /** Level glyph for the compact effort button (three rising bars). */
4509
+ const LEVELS_ICON = "<path d=\"M4 6h16\"/><path d=\"M7 12h10\"/><path d=\"M10 18h4\"/>";
4510
+ /** The DeepSeek fish glyph (the official brand mark path). */
4511
+ const FISH_PATH = "M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018Z";
4512
+ /**
4513
+ * Install the adaptation layer. Runs once per page (idempotent); the
4514
+ * evaluate loop re-applies on orientation/resize and reverts off-portrait.
4515
+ */
4516
+ function startMobileAdapt() {
4517
+ if (typeof window === "undefined" || typeof document === "undefined") return;
4518
+ if (window.__dshRemoteAdaptInstalled === true) return;
4519
+ window.__dshRemoteAdaptInstalled = true;
4520
+ const w = window;
4521
+ let active = false;
4522
+ let savedViewportContent = null;
4523
+ let whaleEl = null;
4524
+ let whaleObserver = null;
4525
+ let whaleTimer = null;
4526
+ let whaleSuppressClick = false;
4527
+ let whaleShown = false;
4528
+ let drag = null;
4529
+ let swipeTouch = null;
4530
+ let lastComposerTap = 0;
4531
+ let adaptEnabled = true;
4532
+ /**
4533
+ * Idempotently (re-)install the adaptation stylesheet. The rules live in
4534
+ * one <style> tag keyed by data-plugin-css; the sync tick re-runs this so
4535
+ * a tag lost to any external DOM cleanup is restored within one tick
4536
+ * instead of silently dropping the suppressions (portrait pet hiding,
4537
+ * rail compaction) while the body class stays.
4538
+ */
4539
+ function ensureAdaptStyle() {
4540
+ if (document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`) !== null) return;
4541
+ const tag = document.createElement("style");
4542
+ tag.dataset.plugin = "remote-web-ui";
4543
+ tag.dataset.pluginCss = ADAPT_CSS_ID;
4544
+ tag.textContent = ADAPT_CSS.join("");
4545
+ document.head.appendChild(tag);
4546
+ }
4547
+ function apply() {
4548
+ if (active) return;
4549
+ active = true;
4550
+ document.body.classList.add(ACTIVE_CLASS);
4551
+ try {
4552
+ w.__dshRemoteAdapt?.closeDetails?.();
4553
+ } catch {}
4554
+ ensureAdaptStyle();
4555
+ const meta = document.querySelector("meta[name=viewport]");
4556
+ if (meta instanceof HTMLMetaElement && meta.getAttribute("content") !== null && !(meta.getAttribute("content") ?? "").includes("viewport-fit")) {
4557
+ savedViewportContent = meta.getAttribute("content");
4558
+ meta.setAttribute("content", `${savedViewportContent}, viewport-fit=cover`);
4559
+ }
4560
+ ensureWhale();
4561
+ syncWhale();
4562
+ setWhaleTimer(true);
4563
+ seatHeaderActions();
4564
+ }
4565
+ function revert() {
4566
+ if (!active) return;
4567
+ active = false;
4568
+ unseatHeaderActions();
4569
+ removeCompactPicker();
4570
+ document.body.classList.remove(ACTIVE_CLASS);
4571
+ document.body.classList.remove(RAIL_HIDDEN_CLASS);
4572
+ document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`)?.remove();
4573
+ const meta = document.querySelector("meta[name=viewport]");
4574
+ if (meta instanceof HTMLMetaElement && savedViewportContent !== null) {
4575
+ meta.setAttribute("content", savedViewportContent);
4576
+ savedViewportContent = null;
4577
+ }
4578
+ if (whaleEl !== null) whaleEl.style.display = "none";
4579
+ setWhaleTimer(false);
4580
+ }
4581
+ /** Start/stop the 600ms sync tick; a no-op when already in the asked state. */
4582
+ function setWhaleTimer(on) {
4583
+ if (on && whaleTimer === null && active) whaleTimer = window.setInterval(syncWhale, 600);
4584
+ else if (!on && whaleTimer !== null) {
4585
+ window.clearInterval(whaleTimer);
4586
+ whaleTimer = null;
4587
+ }
4588
+ }
4589
+ /**
4590
+ * Compact picker (v79): a phone row cannot fit the desktop text triggers,
4591
+ * so the model/effort entries become two icon buttons in the trailing
4592
+ * row. Both forward to the official picker trigger (its menu renders as
4593
+ * the bottom sheet) and then drill straight into the asked cell — model
4594
+ * list or effort list — so one tap lands on the list, matching the
4595
+ * cube-model / brain-effort mapping. The official context ring next to
4596
+ * the send button keeps its own semantics untouched.
4597
+ */
4598
+ function removeCompactPicker() {
4599
+ document.body.classList.remove(COMPACT_CLASS);
4600
+ document.getElementById(MODEL_BTN_ID)?.remove();
4601
+ document.getElementById(EFFORT_BTN_ID)?.remove();
4602
+ }
4603
+ function drillIntoPicker(cellPattern) {
4604
+ const trigger = document.querySelector("[class$=\"_composerSeat\"] [class$=\"_trailing\"] [class$=\"_trigger\"]:has([class$=\"_triggerEffort\"])");
4605
+ if (trigger === null) return;
4606
+ trigger.click();
4607
+ let tries = 0;
4608
+ const tapCell = () => {
4609
+ tries += 1;
4610
+ const cell = Array.from(document.querySelectorAll("[class$=\"_composerSeat\"] [class$=\"_menu\"] [class$=\"_cell\"]")).find((c) => cellPattern.test(c.textContent ?? ""));
4611
+ if (cell !== void 0) {
4612
+ cell.click();
4613
+ return;
4614
+ }
4615
+ if (tries < 8) window.setTimeout(tapCell, 150);
4616
+ };
4617
+ window.setTimeout(tapCell, 150);
4618
+ }
4619
+ function makeCompactButton(id, title, icon, cellPattern) {
4620
+ const btn = document.createElement("button");
4621
+ btn.id = id;
4622
+ btn.type = "button";
4623
+ btn.dataset.dshPlugin = "remote-web-ui";
4624
+ btn.title = title;
4625
+ btn.setAttribute("aria-label", title);
4626
+ btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon}</svg>`;
4627
+ btn.addEventListener("click", () => {
4628
+ drillIntoPicker(cellPattern);
4629
+ });
4630
+ return btn;
4631
+ }
4632
+ function syncCompactPicker() {
4633
+ if (!active) return;
4634
+ const tools = document.querySelector("[class$=\"_composerSeat\"] [class$=\"_tools\"]");
4635
+ const trigger = tools?.parentElement?.querySelector("[class$=\"_triggerEffort\"]")?.parentElement;
4636
+ if (tools === null || trigger === null) {
4637
+ removeCompactPicker();
4638
+ return;
4639
+ }
4640
+ const zh = (navigator.language ?? "").toLowerCase().startsWith("zh");
4641
+ if (document.getElementById(MODEL_BTN_ID) === null) tools.appendChild(makeCompactButton(MODEL_BTN_ID, zh ? "选择模型" : "Pick model", CUBE_ICON, /模型|Model/));
4642
+ if (document.getElementById(EFFORT_BTN_ID) === null) tools.appendChild(makeCompactButton(EFFORT_BTN_ID, zh ? "选择推理等级" : "Pick reasoning effort", LEVELS_ICON, /推理等级|Reasoning|Effort/i));
4643
+ document.body.classList.add(COMPACT_CLASS);
4644
+ }
4645
+ /**
4646
+ * Toggle the sidebar through the wired layout face, then verify the flip:
4647
+ * the official LayoutController can be mounted yet inert (its bound store
4648
+ * actions race the root entry on this cohort — observed on the running
4649
+ * local build, where the face call silently did nothing and the whale
4650
+ * was dead). When the frame state did not change shortly after the call,
4651
+ * drive the official rail/logo toggle instead: that button owns its own
4652
+ * store actions and flips the same state on every cohort we support.
4653
+ */
4654
+ function toggleSidebarVerified() {
4655
+ const frame = document.querySelector("[class$=\"_frame\"]");
4656
+ const collapsedBefore = frame instanceof HTMLElement ? frame.hasAttribute("data-sidebar-collapsed") : null;
4657
+ w.__dshRemoteAdapt?.toggleSidebar?.();
4658
+ if (collapsedBefore === null) return;
4659
+ window.setTimeout(() => {
4660
+ if (!active) return;
4661
+ const frameNow = document.querySelector("[class$=\"_frame\"]");
4662
+ if (!(frameNow instanceof HTMLElement)) return;
4663
+ if (frameNow.hasAttribute("data-sidebar-collapsed") !== collapsedBefore) return;
4664
+ document.querySelector("[class$=\"_railFish\"] button, [class$=\"_logoRow\"] [class*=\"_iconButton\"]")?.click();
4665
+ }, 150);
4666
+ }
4667
+ function ensureWhale() {
4668
+ if (whaleEl !== null || !document.body) return;
4669
+ const whale = document.createElement("button");
4670
+ whale.id = WHALE_ID;
4671
+ whale.type = "button";
4672
+ whale.dataset.dshPlugin = "remote-web-ui";
4673
+ const whaleLabel = (navigator.language ?? "").toLowerCase().startsWith("zh") ? "打开侧边栏" : "Open sidebar";
4674
+ whale.title = whaleLabel;
4675
+ whale.setAttribute("aria-label", whaleLabel);
4676
+ whale.innerHTML = `<svg viewBox="0 0 23.16 17.04" fill="none" aria-hidden="true"><path d="${FISH_PATH}" fill="currentColor"/></svg>`;
4677
+ whale.addEventListener("click", () => {
4678
+ if (whaleSuppressClick) {
4679
+ whaleSuppressClick = false;
4680
+ return;
4681
+ }
4682
+ toggleSidebarVerified();
4683
+ });
4684
+ whale.addEventListener("pointerdown", (e) => {
4685
+ if (whaleEl === null) return;
4686
+ if (e.button !== 0 && e.pointerType === "mouse") return;
4687
+ if (active) {
4688
+ const ta = document.querySelector("[class$=\"_composerSeat\"] textarea, [class$=\"_composerSeat\"] input");
4689
+ if (ta !== null && document.activeElement === ta) {
4690
+ ta.blur();
4691
+ lastComposerTap = 0;
4692
+ }
4693
+ }
4694
+ drag = {
4695
+ x: e.clientX,
4696
+ y: e.clientY,
4697
+ left: whale.offsetLeft,
4698
+ top: whale.offsetTop,
4699
+ moved: false
4700
+ };
4701
+ try {
4702
+ whale.setPointerCapture(e.pointerId);
4703
+ } catch {}
4704
+ e.preventDefault();
4705
+ e.stopPropagation();
4706
+ });
4707
+ whale.addEventListener("pointermove", (e) => {
4708
+ if (whaleEl === null || drag === null) return;
4709
+ const dx = e.clientX - drag.x;
4710
+ const dy = e.clientY - drag.y;
4711
+ drag.moved = drag.moved || Math.abs(dx) > 3 || Math.abs(dy) > 3;
4712
+ whaleEl.style.left = `${Math.min(Math.max(4, drag.left + dx), window.innerWidth - 38)}px`;
4713
+ whaleEl.style.top = `${Math.min(Math.max(4, drag.top + dy), window.innerHeight - 38)}px`;
4714
+ e.preventDefault();
4715
+ e.stopPropagation();
4716
+ });
4717
+ const endWhaleDrag = () => {
4718
+ if (whaleEl === null || drag === null) return;
4719
+ const moved = drag.moved;
4720
+ drag = null;
4721
+ whaleSuppressClick = moved;
4722
+ try {
4723
+ window.localStorage.setItem(WHALE_POS_KEY, JSON.stringify({
4724
+ x: whaleEl.offsetLeft,
4725
+ y: whaleEl.offsetTop
4726
+ }));
4727
+ } catch {}
4728
+ };
4729
+ whale.addEventListener("pointerup", endWhaleDrag);
4730
+ whale.addEventListener("pointercancel", endWhaleDrag);
4731
+ whaleEl = whale;
4732
+ document.body.appendChild(whale);
4733
+ }
4734
+ /** Restore a dragged position when the whale becomes visible again. */
4735
+ function applyWhalePos() {
4736
+ if (whaleEl === null) return;
4737
+ let pos = null;
4738
+ try {
4739
+ pos = JSON.parse(window.localStorage.getItem(WHALE_POS_KEY) ?? "null");
4740
+ } catch {}
4741
+ if (pos !== null && typeof pos.x === "number" && typeof pos.y === "number") {
4742
+ const x = Math.min(Math.max(4, pos.x), window.innerWidth - 38);
4743
+ const y = Math.min(Math.max(4, pos.y), window.innerHeight - 38);
4744
+ whaleEl.style.left = `${x}px`;
4745
+ whaleEl.style.top = `${y}px`;
4746
+ }
4747
+ }
4748
+ function disableRowDrag() {
4749
+ if (!active) return;
4750
+ const rows = document.querySelectorAll("[class$=\"_sidebarCol\"] [class$=\"_sessionRow\"], [class$=\"_sidebarCol\"] [class$=\"_projectRow\"]");
4751
+ for (const row of rows) if (row.getAttribute("draggable") !== "false") row.setAttribute("draggable", "false");
4752
+ }
4753
+ function syncWhale() {
4754
+ if (active) ensureAdaptStyle();
4755
+ if (whaleEl === null) return;
4756
+ if (!active) {
4757
+ whaleEl.style.display = "none";
4758
+ return;
4759
+ }
4760
+ const collapsed = document.querySelector("[class$=\"_frame\"][data-sidebar-collapsed]") !== null;
4761
+ const overlayUp = document.querySelector("[class$=\"_overlay\"]") !== null;
4762
+ const show = collapsed && !overlayUp;
4763
+ if (show && !whaleShown) applyWhalePos();
4764
+ whaleEl.style.display = show ? "" : "none";
4765
+ whaleShown = show;
4766
+ document.body.classList.toggle(RAIL_HIDDEN_CLASS, collapsed);
4767
+ disableRowDrag();
4768
+ seatHeaderActions();
4769
+ alignActionsText();
4770
+ syncCompactPicker();
4771
+ }
4772
+ function seatHeaderActions() {
4773
+ if (!active) return;
4774
+ const tabs = document.querySelector("[class$=\"_header\"] [class$=\"_tabs\"]");
4775
+ const fresh = document.querySelector("[class$=\"_titleCluster\"] [class$=\"_headerActions\"]");
4776
+ const seated = tabs !== null ? tabs.querySelector(":scope > [class$=\"_headerActions\"]") : null;
4777
+ if (fresh !== null && tabs !== null) {
4778
+ seated?.remove();
4779
+ tabs.appendChild(fresh);
4780
+ }
4781
+ }
4782
+ function alignActionsText() {
4783
+ if (!active) return;
4784
+ const tabs = document.querySelector("[class$=\"_header\"] [class$=\"_tabs\"]");
4785
+ if (tabs === null) return;
4786
+ const actions = tabs.querySelector(":scope > [class$=\"_headerActions\"]");
4787
+ const tabBtn = tabs.querySelector(":scope > [class*=\"_tab\"]");
4788
+ if (actions === null || tabBtn === null) return;
4789
+ const textBottom = (el) => {
4790
+ if (el === null) return null;
4791
+ const text = Array.from(el.childNodes).find((n) => n.nodeType === 3 && n.textContent !== null && n.textContent.trim() !== "");
4792
+ if (text === void 0) return null;
4793
+ try {
4794
+ const range = document.createRange();
4795
+ range.selectNode(text);
4796
+ return range.getBoundingClientRect().bottom;
4797
+ } catch {
4798
+ return null;
4799
+ }
4800
+ };
4801
+ const tabBottom = textBottom(tabBtn);
4802
+ if (tabBottom === null) return;
4803
+ const curMatch = /translateY\((-?[\d.]+)px\)/.exec(actions.style.transform ?? "");
4804
+ const cur = curMatch !== null ? parseFloat(curMatch[1] ?? "0") : 0;
4805
+ let maxBottom = null;
4806
+ for (const sel of ["[class$=\"_label\"]", "[class$=\"_count\"]"]) {
4807
+ const b = textBottom(actions.querySelector(sel));
4808
+ if (b !== null && (maxBottom === null || b - cur > maxBottom)) maxBottom = b - cur;
4809
+ }
4810
+ if (maxBottom === null) return;
4811
+ const delta = Math.round((tabBottom - maxBottom) * 10) / 10;
4812
+ if (Math.abs(delta) < .5) {
4813
+ if (actions.style.transform !== "") actions.style.transform = "";
4814
+ return;
4815
+ }
4816
+ actions.style.transform = `translateY(${delta}px)`;
4817
+ }
4818
+ function unseatHeaderActions() {
4819
+ const tabs = document.querySelector("[class$=\"_header\"] [class$=\"_tabs\"]");
4820
+ const seated = tabs !== null ? tabs.querySelector(":scope > [class$=\"_headerActions\"]") : null;
4821
+ const wrap = document.querySelector("[class$=\"_titleCluster\"] > div");
4822
+ if (seated !== null) if (wrap !== null) wrap.appendChild(seated);
4823
+ else seated.remove();
4824
+ }
4825
+ function ensureWhaleObserver() {
4826
+ if (whaleObserver !== null || typeof MutationObserver === "undefined" || !document.body) return;
4827
+ whaleObserver = new MutationObserver(() => {
4828
+ syncWhale();
4829
+ });
4830
+ whaleObserver.observe(document.body, {
4831
+ attributes: true,
4832
+ attributeFilter: ["data-sidebar-collapsed"],
4833
+ subtree: true
4834
+ });
4835
+ }
4836
+ function evaluate() {
4837
+ if (!adaptEnabled) {
4838
+ revert();
4839
+ return;
4840
+ }
4841
+ if (isMobilePortrait()) {
4842
+ apply();
4843
+ ensureWhaleObserver();
4844
+ } else revert();
4845
+ }
4846
+ evaluate();
4847
+ window.addEventListener("orientationchange", evaluate);
4848
+ window.addEventListener("resize", evaluate);
4849
+ document.addEventListener("visibilitychange", () => {
4850
+ setWhaleTimer(document.visibilityState !== "hidden");
4851
+ });
4852
+ function onKeydownCapture(e) {
4853
+ if (!active) return;
4854
+ if (e.key !== "Enter" || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
4855
+ const t = e.target;
4856
+ if (!(t instanceof HTMLElement)) return;
4857
+ if (t.closest("[class$=\"_input\"]") === null) return;
4858
+ if (e.isComposing || e.keyCode === 229) return;
4859
+ e.preventDefault();
4860
+ e.stopPropagation();
4861
+ try {
4862
+ document.execCommand("insertText", false, "\n");
4863
+ } catch {}
4864
+ }
4865
+ document.addEventListener("keydown", onKeydownCapture, true);
4866
+ function collapseSidebar() {
4867
+ toggleSidebarVerified();
4868
+ }
4869
+ function onClickCapture(e) {
4870
+ if (!active) return;
4871
+ const frame = document.querySelector("[class$=\"_frame\"]");
4872
+ if (frame === null || frame.hasAttribute("data-sidebar-collapsed")) return;
4873
+ const t = e.target;
4874
+ if (!(t instanceof Element)) return;
4875
+ if (t.closest("[class$=\"_sidebarCol\"]") !== null) {
4876
+ const topNewSessionClicked = t.closest("[class$=\"_newSession\"], [class$=\"_brand\"]") !== null;
4877
+ let projectNewSessionClicked = false;
4878
+ const projectActions = t.closest("[class$=\"_projectRow\"] [class$=\"_rowActions\"]");
4879
+ if (projectActions !== null) {
4880
+ const btn = t.closest("button");
4881
+ const btns = projectActions.querySelectorAll("button");
4882
+ projectNewSessionClicked = btn !== null && btns.length > 0 && btn === btns[btns.length - 1];
4883
+ }
4884
+ let shouldCollapse = false;
4885
+ if (t.closest("[class$=\"_sessionRow\"]") !== null) shouldCollapse = t.closest("[class$=\"_rowActions\"]") === null;
4886
+ else if (t.closest("[class$=\"_settingsArea\"]") !== null || topNewSessionClicked || projectNewSessionClicked) shouldCollapse = true;
4887
+ if (shouldCollapse) collapseSidebar();
4888
+ return;
4889
+ }
4890
+ if (t.closest("[class$=\"_overlay\"], [class$=\"_dialog\"], [class$=\"_menu\"], [class*=\"_portal\"], [id=\"dshRemoteWhale\"], [id=\"dshLanGate\"]") !== null) return;
4891
+ collapseSidebar();
4892
+ }
4893
+ document.addEventListener("click", onClickCapture, true);
4894
+ function insideHScrollable(el) {
4895
+ let n = el;
4896
+ while (n !== null && n !== document.body) {
4897
+ const cs = getComputedStyle(n);
4898
+ if ((cs.overflowX === "auto" || cs.overflowX === "scroll") && n.scrollWidth > n.clientWidth + 4) return true;
4899
+ n = n.parentElement;
4900
+ }
4901
+ return false;
4902
+ }
4903
+ document.addEventListener("touchstart", (e) => {
4904
+ if (!active) return;
4905
+ const t = e.target;
4906
+ if (t instanceof Element && (t.tagName === "TEXTAREA" || t.tagName === "INPUT" || t.closest(`#${WHALE_ID}`) !== null || insideHScrollable(t) || t.closest("table, [class$=\"_table\"], [class$=\"_tablePane\"]") !== null)) {
4907
+ swipeTouch = null;
4908
+ return;
4909
+ }
4910
+ const ct = e.changedTouches[0];
4911
+ if (ct === void 0) return;
4912
+ swipeTouch = {
4913
+ x: ct.clientX,
4914
+ y: ct.clientY,
4915
+ id: ct.identifier
4916
+ };
4917
+ }, {
4918
+ capture: true,
4919
+ passive: true
4920
+ });
4921
+ document.addEventListener("touchend", (e) => {
4922
+ if (swipeTouch === null) return;
4923
+ const ct = e.changedTouches[0];
4924
+ if (ct === void 0 || ct.identifier !== swipeTouch.id) return;
4925
+ const dx = ct.clientX - swipeTouch.x;
4926
+ const dy = ct.clientY - swipeTouch.y;
4927
+ swipeTouch = null;
4928
+ if (!active) return;
4929
+ if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
4930
+ const frame = document.querySelector("[class$=\"_frame\"]");
4931
+ if (frame === null) return;
4932
+ if (document.querySelector("[class$=\"_overlay\"], [class$=\"_dialog\"], [class$=\"_menu\"], [class*=\"_portal\"]") !== null) return;
4933
+ const collapsed = frame.hasAttribute("data-sidebar-collapsed");
4934
+ if (dx < 0 && !collapsed) collapseSidebar();
4935
+ else if (dx > 0 && collapsed) toggleSidebarVerified();
4936
+ }, {
4937
+ capture: true,
4938
+ passive: true
4939
+ });
4940
+ document.addEventListener("touchcancel", () => {
4941
+ swipeTouch = null;
4942
+ }, true);
4943
+ const LONG_PRESS_MS = 500;
4944
+ const LONG_PRESS_MOVE = 10;
4945
+ let longPress = null;
4946
+ let suppressSessionClickUntil = 0;
4947
+ let suppressSessionClickRow = null;
4948
+ let longPressMenuGuardUntil = 0;
4949
+ function clearLongPress() {
4950
+ if (longPress !== null) {
4951
+ if (longPress.timer !== null) window.clearTimeout(longPress.timer);
4952
+ longPress = null;
4953
+ }
4954
+ }
4955
+ function openSessionMenu(row) {
4956
+ const actions = row.querySelector("[class$=\"_rowActions\"]");
4957
+ const btn = actions?.querySelector("button");
4958
+ if (actions === null || btn === null) return;
4959
+ longPressMenuGuardUntil = Date.now() + 1200;
4960
+ actions.style.display = "inline-flex";
4961
+ try {
4962
+ btn.click();
4963
+ } finally {
4964
+ window.setTimeout(() => {
4965
+ actions.style.display = "";
4966
+ }, 50);
4967
+ }
4968
+ }
4969
+ document.addEventListener("touchstart", (e) => {
4970
+ if (!active) return;
4971
+ const t = e.target;
4972
+ if (!(t instanceof Element)) {
4973
+ clearLongPress();
4974
+ return;
4975
+ }
4976
+ const row = t.closest("[class$=\"_sessionRow\"]");
4977
+ if (row === null || t.closest("[class$=\"_rowActions\"]") !== null) {
4978
+ clearLongPress();
4979
+ return;
4980
+ }
4981
+ const ct = e.changedTouches[0];
4982
+ if (ct === void 0) return;
4983
+ clearLongPress();
4984
+ const lp = {
4985
+ row,
4986
+ x: ct.clientX,
4987
+ y: ct.clientY,
4988
+ timer: null,
4989
+ triggered: false
4990
+ };
4991
+ lp.timer = window.setTimeout(() => {
4992
+ if (longPress !== lp) return;
4993
+ lp.triggered = true;
4994
+ openSessionMenu(lp.row);
4995
+ try {
4996
+ navigator.vibrate?.(10);
4997
+ } catch {}
4998
+ }, LONG_PRESS_MS);
4999
+ longPress = lp;
5000
+ }, {
5001
+ capture: true,
5002
+ passive: true
5003
+ });
5004
+ document.addEventListener("touchmove", (e) => {
5005
+ if (!active || longPress === null) return;
5006
+ const ct = e.changedTouches[0];
5007
+ if (ct === void 0) return;
5008
+ if (Math.abs(ct.clientX - longPress.x) > LONG_PRESS_MOVE || Math.abs(ct.clientY - longPress.y) > LONG_PRESS_MOVE) clearLongPress();
5009
+ }, {
5010
+ capture: true,
5011
+ passive: true
5012
+ });
5013
+ document.addEventListener("touchend", (e) => {
5014
+ if (!active || longPress === null) return;
5015
+ if (e.changedTouches[0] === void 0) return;
5016
+ const lp = longPress;
5017
+ clearLongPress();
5018
+ if (lp.triggered) {
5019
+ e.preventDefault();
5020
+ suppressSessionClickUntil = Date.now() + 800;
5021
+ suppressSessionClickRow = lp.row;
5022
+ }
5023
+ }, {
5024
+ capture: true,
5025
+ passive: false
5026
+ });
5027
+ document.addEventListener("touchcancel", () => {
5028
+ clearLongPress();
5029
+ }, true);
5030
+ window.addEventListener("click", (e) => {
5031
+ if (!active || Date.now() > suppressSessionClickUntil) return;
5032
+ const t = e.target;
5033
+ if (t instanceof Element && suppressSessionClickRow !== null && (t === suppressSessionClickRow || suppressSessionClickRow.contains(t))) {
5034
+ e.stopPropagation();
5035
+ e.preventDefault();
5036
+ suppressSessionClickUntil = 0;
5037
+ suppressSessionClickRow = null;
5038
+ }
5039
+ }, true);
5040
+ document.addEventListener("contextmenu", (e) => {
5041
+ if (!active) return;
5042
+ const t = e.target;
5043
+ if (!(t instanceof Element)) return;
5044
+ if (t.closest("[class$=\"_sessionRow\"]") !== null && (longPress !== null && longPress.triggered || Date.now() <= suppressSessionClickUntil)) e.preventDefault();
5045
+ }, true);
5046
+ document.addEventListener("pointerleave", (e) => {
5047
+ if (!active || Date.now() > longPressMenuGuardUntil) return;
5048
+ const t = e.target;
5049
+ if (t instanceof Element && (t.closest("[class$=\"_rowActions\"]") !== null || t.closest("[class$=\"_sessionRow\"]") !== null || t.closest("[class$=\"_projectRow\"]") !== null)) e.stopPropagation();
5050
+ }, true);
5051
+ const lanOrigFocus = HTMLElement.prototype.focus;
5052
+ HTMLElement.prototype.focus = function(options) {
5053
+ if (active && (this.tagName === "TEXTAREA" || this.tagName === "INPUT") && this.closest("[class$=\"_composerSeat\"]") !== null && Date.now() - lastComposerTap >= 800) return;
5054
+ lanOrigFocus.call(this, options);
5055
+ };
5056
+ let lastComposerTapRegistered = false;
5057
+ if (!lastComposerTapRegistered) {
5058
+ lastComposerTapRegistered = true;
5059
+ document.addEventListener("pointerdown", (e) => {
5060
+ if (!active) return;
5061
+ const t = e.target;
5062
+ if (t instanceof Element && (t.tagName === "TEXTAREA" || t.tagName === "INPUT") && t.closest("[class$=\"_composerSeat\"]") !== null) lastComposerTap = Date.now();
5063
+ }, true);
5064
+ }
5065
+ w.__dshRemoteAdapt = {
5066
+ evaluate,
5067
+ toggleSidebar: null,
5068
+ closeDetails: null,
5069
+ setEnabled(on) {
5070
+ adaptEnabled = on;
5071
+ if (on) evaluate();
5072
+ else revert();
5073
+ },
5074
+ flushCloseDetails() {
5075
+ if (active) w.__dshRemoteAdapt?.closeDetails?.();
5076
+ }
5077
+ };
5078
+ }
5079
+ //#endregion
4335
5080
  //#region src/client/index.ts
4336
5081
  /**
4337
- * Mobile remote control — browser half. Registers the `remote` dictionaries,
4338
- * the sidebar-foot entry (phone trigger + pairing panel) into the
4339
- * ui-sidebar-declared `sidebar.remote` seat, and runs the phone-side boot
4340
- * flow (pair accept + workspace deep-link + presence heartbeats) plus the
4341
- * one-time failed-pair notice. Export discipline: packages/client/AGENTS.md
4342
- * — the /client surface carries only what cordis loading needs plus types.
5082
+ * Remote control — browser half. Registers the `remote` dictionaries, the
5083
+ * sidebar-foot entry (phone trigger + pairing panel + update trigger), and
5084
+ * the pair boot flow (accept + presence heartbeats) plus the one-time
5085
+ * failed-pair notice. The portrait-touch adaptation of the official UI
5086
+ * starts at module scope (startMobileAdapt) so its focus guard is installed
5087
+ * before the app boots. Export discipline: packages/client/AGENTS.md — the
5088
+ * /client surface carries only what cordis loading needs plus types.
4343
5089
  */
5090
+ startMobileAdapt();
4344
5091
  /** Dictionary namespace owned by this plugin. */
4345
5092
  const NS = "remote";
4346
5093
  /** Settings namespace the remote-control card edits (the Host plugin registers it). */
@@ -4371,33 +5118,38 @@ window.__ModuleLoader__.load({
4371
5118
  return () => {};
4372
5119
  }
4373
5120
  }, "remote-web-ui: dictionaries");
5121
+ const layout = ctx.get("layout");
5122
+ const adapt = window.__dshRemoteAdapt;
5123
+ const layoutCall = (call) => {
5124
+ try {
5125
+ call?.();
5126
+ } catch {}
5127
+ };
5128
+ if (layout !== void 0 && adapt !== void 0) {
5129
+ if (typeof layout.toggleSidebar === "function") adapt.toggleSidebar = () => {
5130
+ layoutCall(layout.toggleSidebar);
5131
+ };
5132
+ if (typeof layout.closeDetails === "function") adapt.closeDetails = () => {
5133
+ layoutCall(layout.closeDetails);
5134
+ };
5135
+ }
5136
+ if (adapt !== void 0 && adapt.toggleSidebar === null) adapt.toggleSidebar = () => {
5137
+ document.querySelector("[class$=\"_railFish\"] button, [class$=\"_logoRow\"] [class*=\"_iconButton\"]")?.click();
5138
+ };
4374
5139
  const t = ctx.locale.bind(NS);
4375
5140
  const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: REMOTE_WEB_UI_NS });
4376
5141
  const enabled = () => {
4377
5142
  const snapshot = settingsScope.getSnapshot();
4378
5143
  return snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable";
4379
5144
  };
4380
- ctx.slots.inject("sidebar.remote", () => {
4381
- let disposeEntry;
4382
- const syncEntry = () => {
4383
- if (enabled() && disposeEntry === void 0) try {
4384
- disposeEntry = ctx.slots.register({
4385
- name: "sidebar.remote",
4386
- locale: NS
4387
- }, RemoteEntry);
4388
- } catch {}
4389
- else if (!enabled() && disposeEntry !== void 0) {
4390
- disposeEntry();
4391
- disposeEntry = void 0;
4392
- }
4393
- };
4394
- const unsubscribe = settingsScope.subscribe(syncEntry);
4395
- syncEntry();
4396
- return () => {
4397
- unsubscribe();
4398
- disposeEntry?.();
4399
- };
4400
- });
5145
+ const syncAdaptEnabled = () => {
5146
+ window.__dshRemoteAdapt?.setEnabled?.(enabled());
5147
+ };
5148
+ settingsScope.subscribe(syncAdaptEnabled);
5149
+ syncAdaptEnabled();
5150
+ try {
5151
+ adapt?.flushCloseDetails?.();
5152
+ } catch {}
4401
5153
  ctx.slots.inject("sidebar.footer.action", () => {
4402
5154
  let disposeEntry;
4403
5155
  const syncEntry = () => {