@linxin666/dsh-remote-web-ui 0.3.24 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +13 -10
  3. package/README.zh.md +13 -10
  4. package/icon.svg +3 -0
  5. package/lib/client.js +263 -129
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +565 -256
  8. package/lib/types/client/FenceNotice.d.ts.map +1 -1
  9. package/lib/types/client/FenceNotice.js +0 -3
  10. package/lib/types/client/PluginSettingsCard.d.ts +4 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.js +3 -2
  13. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  14. package/lib/types/client/RemoteEntry.js +14 -9
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +13 -4
  17. package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +14 -8
  20. package/lib/types/client/UpdateEntry.js +2 -2
  21. package/lib/types/client/UpdatePanel.js +2 -2
  22. package/lib/types/client/deep-link.js +2 -2
  23. package/lib/types/client/index.d.ts +16 -8
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/client/index.js +47 -18
  26. package/lib/types/client/locales.d.ts +0 -2
  27. package/lib/types/client/locales.d.ts.map +1 -1
  28. package/lib/types/client/locales.js +6 -8
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  30. package/lib/types/client/mobile-adapt.js +103 -22
  31. package/lib/types/client/pair-api.d.ts +32 -3
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +44 -6
  34. package/lib/types/client/plugin-card-seat.d.ts +23 -16
  35. package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
  36. package/lib/types/client/plugin-card-seat.js +16 -14
  37. package/lib/types/client/remote-channel.d.ts.map +1 -1
  38. package/lib/types/client/remote-channel.js +18 -5
  39. package/lib/types/client/settings-form.d.ts +17 -17
  40. package/lib/types/client/settings-form.d.ts.map +1 -1
  41. package/lib/types/client/settings-form.js +30 -27
  42. package/lib/types/http.d.ts +18 -1
  43. package/lib/types/http.d.ts.map +1 -1
  44. package/lib/types/http.js +22 -1
  45. package/lib/types/index.d.ts +19 -11
  46. package/lib/types/index.d.ts.map +1 -1
  47. package/lib/types/index.js +91 -76
  48. package/lib/types/inner-auth.d.ts +3 -2
  49. package/lib/types/inner-auth.d.ts.map +1 -1
  50. package/lib/types/inner-auth.js +3 -2
  51. package/lib/types/pair-grant.d.ts +66 -0
  52. package/lib/types/pair-grant.d.ts.map +1 -0
  53. package/lib/types/pair-grant.js +95 -0
  54. package/lib/types/pairing.d.ts +44 -12
  55. package/lib/types/pairing.d.ts.map +1 -1
  56. package/lib/types/pairing.js +69 -20
  57. package/lib/types/pairing.persist.test.js +26 -0
  58. package/lib/types/posture.d.ts +11 -3
  59. package/lib/types/posture.d.ts.map +1 -1
  60. package/lib/types/posture.js +8 -3
  61. package/lib/types/remote-api.d.ts +10 -13
  62. package/lib/types/remote-api.d.ts.map +1 -1
  63. package/lib/types/remote-api.js +34 -34
  64. package/lib/types/remote-channel-boot.d.ts +6 -1
  65. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  66. package/lib/types/remote-channel-boot.js +16 -8
  67. package/lib/types/remote-channel-rules.d.ts +14 -0
  68. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  69. package/lib/types/remote-channel-rules.js +8 -0
  70. package/lib/types/remote-methods.d.ts +1 -1
  71. package/lib/types/remote-methods.d.ts.map +1 -1
  72. package/lib/types/remote-methods.js +1 -0
  73. package/lib/types/routes.d.ts +47 -5
  74. package/lib/types/routes.d.ts.map +1 -1
  75. package/lib/types/routes.js +160 -58
  76. package/lib/types/tunnel.d.ts +21 -6
  77. package/lib/types/tunnel.d.ts.map +1 -1
  78. package/lib/types/tunnel.js +58 -11
  79. package/lib/types/update.d.ts +4 -0
  80. package/lib/types/update.d.ts.map +1 -1
  81. package/lib/types/update.js +74 -26
  82. package/package.json +23 -20
  83. package/src/client/FenceNotice.tsx +0 -2
  84. package/src/client/PluginSettingsCard.tsx +7 -2
  85. package/src/client/RemoteEntry.tsx +14 -9
  86. package/src/client/RemotePanel.tsx +17 -7
  87. package/src/client/RemoteSettingsCard.tsx +16 -9
  88. package/src/client/UpdateEntry.tsx +2 -2
  89. package/src/client/UpdatePanel.tsx +3 -3
  90. package/src/client/deep-link.ts +2 -2
  91. package/src/client/index.ts +61 -27
  92. package/src/client/locales.ts +6 -8
  93. package/src/client/mobile-adapt.ts +97 -20
  94. package/src/client/pair-api.ts +47 -7
  95. package/src/client/plugin-card-seat.ts +24 -17
  96. package/src/client/remote-channel.ts +19 -4
  97. package/src/client/remote.module.css +38 -0
  98. package/src/client/settings-form.ts +34 -31
  99. package/src/http.ts +23 -1
  100. package/src/index.ts +102 -79
  101. package/src/inner-auth.ts +3 -2
  102. package/src/pair-grant.ts +131 -0
  103. package/src/pairing.persist.test.ts +36 -0
  104. package/src/pairing.ts +75 -24
  105. package/src/posture.ts +12 -4
  106. package/src/remote-api.ts +34 -41
  107. package/src/remote-channel-boot.ts +16 -8
  108. package/src/remote-channel-rules.ts +17 -0
  109. package/src/remote-methods.ts +1 -0
  110. package/src/routes.ts +165 -57
  111. package/src/tunnel.ts +56 -11
  112. package/src/update.ts +74 -24
package/lib/client.js CHANGED
@@ -1283,14 +1283,10 @@ window.__ModuleLoader__.load({
1283
1283
  body: JSON.stringify({ token })
1284
1284
  });
1285
1285
  if (response.ok) return { ok: true };
1286
- if (response.status === 404) return {
1286
+ if (response.status === 404 || response.status === 409) return {
1287
1287
  ok: false,
1288
1288
  code: "invalid"
1289
1289
  };
1290
- if (response.status === 409) return {
1291
- ok: false,
1292
- code: "used"
1293
- };
1294
1290
  return {
1295
1291
  ok: false,
1296
1292
  code: "forbidden"
@@ -1314,21 +1310,58 @@ window.__ModuleLoader__.load({
1314
1310
  if (response.status === 404) return;
1315
1311
  if (!response.ok) throw new Error(`remote-web-ui: revoke failed with ${String(response.status)}`);
1316
1312
  }
1317
- /** Presence heartbeat from a paired phone (unpaired heartbeats 401 harmlessly). */
1313
+ /**
1314
+ * Presence heartbeat from a paired phone.
1315
+ * @returns the response status, so the caller can stop polling once the server
1316
+ * proves this page is not paired (see {@link shouldStopHeartbeat}).
1317
+ */
1318
1318
  async function sendHeartbeat() {
1319
- await fetch("/api/pair/heartbeat", { method: "POST" });
1319
+ return (await fetch("/api/pair/heartbeat", { method: "POST" })).status;
1320
+ }
1321
+ /**
1322
+ * Whether a heartbeat answer means "this page can never be accepted again" and
1323
+ * the 10 s wake source should stop: 401 (unpaired, or the device was revoked)
1324
+ * and 403 (the fence refused it) are permanent for this page, while a network
1325
+ * error or a 5xx is transient and keeps the cadence.
1326
+ * @param status - the heartbeat response status.
1327
+ */
1328
+ function shouldStopHeartbeat(status) {
1329
+ return status === 401 || status === 403;
1320
1330
  }
1321
1331
  /** Whether the current page URL carries a pairing token. */
1322
1332
  function readPairParams(search) {
1323
1333
  const pair = new URLSearchParams(search).get("pair");
1324
1334
  return pair !== null && pair !== "" ? { pair } : {};
1325
1335
  }
1336
+ /** A failed LAN-bind read, carrying the HTTP status when the server answered. */
1337
+ var LanBindStatusError = class extends Error {
1338
+ status;
1339
+ /**
1340
+ * @param status - the response status, or undefined when the request failed
1341
+ * before a response (network error).
1342
+ * @param message - the diagnostic message.
1343
+ */
1344
+ constructor(status, message) {
1345
+ super(message);
1346
+ this.status = status;
1347
+ this.name = "LanBindStatusError";
1348
+ }
1349
+ };
1326
1350
  /** Read the LAN-bind facts (loopback-only endpoint). */
1327
1351
  async function readLanBindStatus() {
1328
1352
  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)}`);
1353
+ if (!response.ok) throw new LanBindStatusError(response.status, `remote-web-ui: lan-bind status failed with ${String(response.status)}`);
1330
1354
  return await response.json();
1331
1355
  }
1356
+ /**
1357
+ * Whether a LAN-bind status failure means this origin can never read the
1358
+ * endpoint (401/403 from the loopback-only fence), so a poll should stop
1359
+ * instead of retrying a known refusal; a transient error keeps the cadence.
1360
+ * @param status - the response status, or undefined for a network failure.
1361
+ */
1362
+ function shouldStopLanBindPoll(status) {
1363
+ return status === 401 || status === 403;
1364
+ }
1332
1365
  /** Human-readable expiry clock, e.g. "10:35". */
1333
1366
  function formatClock(epochMs) {
1334
1367
  const date = new Date(epochMs);
@@ -1376,7 +1409,7 @@ window.__ModuleLoader__.load({
1376
1409
  }
1377
1410
  //#endregion
1378
1411
  //#region \0dsh-css:packages/dsh-remote-web-ui/src/client/remote.module.css.mjs
1379
- const css$1 = ".fThDlq_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.fThDlq_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}.fThDlq_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex;position:relative}.fThDlq_trigger[data-update-available]{color:var(--dsw-alias-brand-primary)}.fThDlq_trigger[data-update-available]:after{box-sizing:border-box;border:2px solid var(--dsw-alias-bg-layer-1);background:var(--dsw-alias-brand-primary);content:\"\";pointer-events:none;border-radius:50%;width:8px;height:8px;position:absolute;top:4px;right:4px}.fThDlq_trigger[data-wide=wide]{border-radius:999px;flex:auto;justify-content:flex-start;gap:8px;width:auto;min-width:0;padding:0 10px}.fThDlq_trigger[data-update-available][data-wide=wide]{flex:none;gap:6px}.fThDlq_trigger[data-update-available][data-wide=wide]:after{display:none}.fThDlq_updateBadgeText{white-space:nowrap;font-size:13px;font-weight:500;line-height:1}.fThDlq_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_trigger:disabled{opacity:.5;cursor:default}.fThDlq_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}.fThDlq_header{align-items:flex-start;gap:12px;display:flex}.fThDlq_heading{flex:1;min-width:0}.fThDlq_title{margin:0;font-size:18px;font-weight:600;line-height:26px}.fThDlq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}.fThDlq_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_close:disabled{opacity:.5;cursor:default}.fThDlq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}.fThDlq_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.fThDlq_cardTitle{font-weight:500}.fThDlq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.fThDlq_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}.fThDlq_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badges{flex:none;align-items:center;gap:6px;display:inline-flex}.fThDlq_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}.fThDlq_qr{display:block}.fThDlq_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.fThDlq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;display:block;overflow:hidden}.fThDlq_pairLinks{flex-direction:column;gap:8px;display:flex}.fThDlq_pairLinkRow{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:center;gap:10px;min-width:0;padding:10px 12px;display:flex}.fThDlq_pairLinkText{flex:1;min-width:0}.fThDlq_pairLinkLabel{color:var(--dsw-alias-label-secondary);margin-bottom:3px;font-size:12px;display:block}.fThDlq_copyLink{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);min-height:30px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:5px;padding:0 10px;display:inline-flex}.fThDlq_oneTimeHint{color:var(--dsw-alias-label-caption);margin:0;font-size:12px}.fThDlq_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_actions{gap:8px;display:flex}.fThDlq_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}.fThDlq_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}.fThDlq_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_action:disabled{opacity:.5;cursor:default}.fThDlq_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}.fThDlq_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}.fThDlq_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_fencePage{z-index:2000;box-sizing:border-box;background:var(--dsw-alias-bg-base);text-align:center;flex-direction:column;justify-content:center;align-items:center;padding:40px 24px;display:flex;position:fixed;inset:0;overflow:auto}.fThDlq_fenceCard{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:min(520px,100%);box-shadow:var(--dsw-shadow-lv3);text-align:center;border-radius:20px;margin-inline:auto;padding:36px 40px}.fThDlq_fenceMark{background:var(--dsw-alias-state-error-secondary);width:44px;height:44px;color:var(--dsw-alias-state-error-primary);border-radius:50%;place-items:center;margin-inline:auto;font-size:24px;line-height:1;display:grid}.fThDlq_fenceEyebrow{color:var(--dsw-alias-state-error-primary);margin:22px 0 8px;font-size:13px;font-weight:600}.fThDlq_fenceTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:24px;line-height:1.35}.fThDlq_fenceDetail{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:14px;line-height:1.65}.fThDlq_fenceSteps{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);text-align:left;border-radius:12px;margin:24px auto 0;padding:20px 20px 20px 42px;font-size:14px;line-height:1.65}.fThDlq_fenceSteps li+li{margin-top:8px}.fThDlq_fenceForm{width:100%;margin-top:20px}.fThDlq_fenceInputRow{gap:8px;width:100%;display:flex}.fThDlq_fenceInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:0;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;flex:1;padding:10px 14px;font-size:13px;transition:border-color .12s,box-shadow .12s}.fThDlq_fenceInput:focus{border-color:var(--dsw-alias-brand-primary);outline:none;box-shadow:0 0 0 2px #0066ff26}.fThDlq_fencePairButton{border:1px solid var(--dsw-alias-button-primary-fill);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;cursor:pointer;border-radius:10px;flex-shrink:0;padding:10px 18px;font-size:13px;font-weight:500;transition:filter .12s,opacity .12s}.fThDlq_fencePairButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill));border-color:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill))}.fThDlq_fencePairButton:disabled{opacity:.55;cursor:not-allowed}.fThDlq_fencePairButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.fThDlq_fenceError{color:var(--dsw-alias-state-error-primary);text-align:left;margin:10px 0 0;font-size:13px;line-height:1.4}.fThDlq_fenceRetry{border:1px solid var(--dsw-alias-border-l2);width:100%;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:10px;margin-top:12px;padding:10px 16px;font-weight:500;transition:background-color .12s,color .12s}.fThDlq_fenceRetry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_fenceRetry:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.fThDlq_fenceFootnote{color:var(--dsw-alias-label-tertiary);margin:14px 0 0;font-size:12px;line-height:1.55}.fThDlq_addresses{border:none;margin:12px 0 0;padding:0}.fThDlq_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}.fThDlq_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}.fThDlq_address:hover{background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}.fThDlq_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}.fThDlq_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}.fThDlq_devices{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:8px;padding:12px 16px 14px;display:flex}.fThDlq_devicesTitle{margin:0;font-size:13px;font-weight:500;line-height:20px}.fThDlq_devicesEmpty{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_deviceList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.fThDlq_deviceRow{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.fThDlq_deviceMeta{flex-direction:column;gap:2px;min-width:0;display:flex}.fThDlq_deviceName{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.fThDlq_devicePresence{font-size:12px;line-height:18px}.fThDlq_deviceOnline{color:var(--dsw-alias-state-success-primary)}.fThDlq_deviceOffline{color:var(--dsw-alias-label-secondary)}.fThDlq_deviceSeen{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px}.fThDlq_deviceRevoke{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;padding:6px 10px;font-size:12px;transition:background-color .12s,color .12s}.fThDlq_deviceRevoke:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_deviceRevoke:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address,.fThDlq_deviceRevoke{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;min-width:0;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}.fThDlq_updateStatus{margin:0;font-weight:600}.fThDlq_updateDetail{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_updateError{color:var(--dsw-alias-text-danger,var(--dsw-alias-label-primary));margin:0;font-weight:600}.fThDlq_updateOutput{background:var(--dsw-alias-bg-layer-1);max-height:180px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all;border-radius:10px;margin:10px 0 0;padding:10px 12px;font-size:12px;line-height:18px;overflow:auto}.fThDlq_updateList{flex-direction:column;gap:6px;margin:12px 0 0;padding:0;list-style:none;display:flex}.fThDlq_updateListItem{justify-content:space-between;align-items:baseline;gap:12px;font-size:13px;display:flex}.fThDlq_updateListName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);overflow:hidden}.fThDlq_updateListVersions{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none}.fThDlq_updateNotes{margin-top:14px}.fThDlq_updateNotesTitle{margin:0 0 8px;font-size:14px;font-weight:600}.fThDlq_updateNoteSection{flex-direction:column;gap:4px;margin-top:10px;display:flex}.fThDlq_updateNoteHeading{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;font-weight:600}.fThDlq_updateNoteList{flex-direction:column;gap:4px;margin:0;padding-left:18px;font-size:13px;display:flex}.fThDlq_updateNoteItem{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}.fThDlq_updateVersions{margin-top:12px}.fThDlq_updateVersionsSummary{cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none;font-size:12px}.fThDlq_updateActions{justify-content:flex-end;margin-top:16px;display:flex}.fThDlq_updateRetry{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:6px;padding:8px 14px;font-size:13px;transition:background-color .12s,color .12s;display:inline-flex}.fThDlq_updateRetry:hover{background:var(--dsw-alias-interactive-bg-active)}@media (prefers-reduced-motion:reduce){.fThDlq_updateRetry{transition:none}}";
1412
+ const css$1 = ".fThDlq_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.fThDlq_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}.fThDlq_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex;position:relative}.fThDlq_trigger[data-update-available]{color:var(--dsw-alias-brand-primary)}.fThDlq_trigger[data-update-available]:after{box-sizing:border-box;border:2px solid var(--dsw-alias-bg-layer-1);background:var(--dsw-alias-brand-primary);content:\"\";pointer-events:none;border-radius:50%;width:8px;height:8px;position:absolute;top:4px;right:4px}.fThDlq_trigger[data-wide=wide]{border-radius:999px;flex:auto;justify-content:flex-start;gap:8px;width:auto;min-width:0;padding:0 10px}.fThDlq_trigger[data-update-available][data-wide=wide]{flex:none;gap:6px}.fThDlq_trigger[data-update-available][data-wide=wide]:after{display:none}.fThDlq_updateBadgeText{white-space:nowrap;font-size:13px;font-weight:500;line-height:1}.fThDlq_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_trigger:disabled{opacity:.5;cursor:default}.fThDlq_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}.fThDlq_header{align-items:flex-start;gap:12px;display:flex}.fThDlq_heading{flex:1;min-width:0}.fThDlq_title{margin:0;font-size:18px;font-weight:600;line-height:26px}.fThDlq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}.fThDlq_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_close:disabled{opacity:.5;cursor:default}.fThDlq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}.fThDlq_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.fThDlq_cardTitle{font-weight:500}.fThDlq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.fThDlq_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}.fThDlq_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badges{flex:none;align-items:center;gap:6px;display:inline-flex}.fThDlq_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}.fThDlq_qr{display:block}.fThDlq_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.fThDlq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;display:block;overflow:hidden}.fThDlq_pairLinks{flex-direction:column;gap:8px;display:flex}.fThDlq_pairLinkRow{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:center;gap:10px;min-width:0;padding:10px 12px;display:flex}.fThDlq_pairLinkText{flex:1;min-width:0}.fThDlq_pairLinkLabel{color:var(--dsw-alias-label-secondary);margin-bottom:3px;font-size:12px;display:block}.fThDlq_copyLink{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);min-height:30px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:5px;padding:0 10px;display:inline-flex}.fThDlq_oneTimeHint{color:var(--dsw-alias-label-caption);margin:0;font-size:12px}.fThDlq_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_actions{gap:8px;display:flex}.fThDlq_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}.fThDlq_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}.fThDlq_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_action:disabled{opacity:.5;cursor:default}.fThDlq_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}.fThDlq_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}.fThDlq_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_fencePage{z-index:2000;box-sizing:border-box;background:var(--dsw-alias-bg-base);text-align:center;flex-direction:column;justify-content:center;align-items:center;padding:40px 24px;display:flex;position:fixed;inset:0;overflow:auto}.fThDlq_fenceCard{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:min(520px,100%);box-shadow:var(--dsw-shadow-lv3);text-align:center;border-radius:20px;margin-inline:auto;padding:36px 40px}.fThDlq_fenceMark{background:var(--dsw-alias-state-error-secondary);width:44px;height:44px;color:var(--dsw-alias-state-error-primary);border-radius:50%;place-items:center;margin-inline:auto;font-size:24px;line-height:1;display:grid}.fThDlq_fenceEyebrow{color:var(--dsw-alias-state-error-primary);margin:22px 0 8px;font-size:13px;font-weight:600}.fThDlq_fenceTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:24px;line-height:1.35}.fThDlq_fenceDetail{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:14px;line-height:1.65}.fThDlq_fenceSteps{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);text-align:left;border-radius:12px;margin:24px auto 0;padding:20px 20px 20px 42px;font-size:14px;line-height:1.65}.fThDlq_fenceSteps li+li{margin-top:8px}.fThDlq_fenceForm{width:100%;margin-top:20px}.fThDlq_fenceInputRow{gap:8px;width:100%;display:flex}.fThDlq_fenceInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:0;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;flex:1;padding:10px 14px;font-size:13px;transition:border-color .12s,box-shadow .12s}.fThDlq_fenceInput:focus{border-color:var(--dsw-alias-brand-primary);outline:none;box-shadow:0 0 0 2px #0066ff26}.fThDlq_fencePairButton{border:1px solid var(--dsw-alias-button-primary-fill);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;cursor:pointer;border-radius:10px;flex-shrink:0;padding:10px 18px;font-size:13px;font-weight:500;transition:filter .12s,opacity .12s}.fThDlq_fencePairButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill));border-color:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill))}.fThDlq_fencePairButton:disabled{opacity:.55;cursor:not-allowed}.fThDlq_fencePairButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.fThDlq_fenceError{color:var(--dsw-alias-state-error-primary);text-align:left;margin:10px 0 0;font-size:13px;line-height:1.4}.fThDlq_fenceRetry{border:1px solid var(--dsw-alias-border-l2);width:100%;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:10px;margin-top:12px;padding:10px 16px;font-weight:500;transition:background-color .12s,color .12s}.fThDlq_fenceRetry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_fenceRetry:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.fThDlq_fenceFootnote{color:var(--dsw-alias-label-tertiary);margin:14px 0 0;font-size:12px;line-height:1.55}.fThDlq_addresses{border:none;margin:12px 0 0;padding:0}.fThDlq_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}.fThDlq_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}.fThDlq_address:hover{background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}.fThDlq_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}.fThDlq_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}.fThDlq_devices{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:8px;padding:12px 16px 14px;display:flex}.fThDlq_devicesTitle{margin:0;font-size:13px;font-weight:500;line-height:20px}.fThDlq_devicesEmpty{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_deviceList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.fThDlq_deviceRow{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.fThDlq_deviceMeta{flex-direction:column;gap:2px;min-width:0;display:flex}.fThDlq_deviceName{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.fThDlq_devicePresence{font-size:12px;line-height:18px}.fThDlq_deviceOnline{color:var(--dsw-alias-state-success-primary)}.fThDlq_deviceOffline{color:var(--dsw-alias-label-secondary)}.fThDlq_deviceSeen{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px}.fThDlq_deviceRevoke{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;padding:6px 10px;font-size:12px;transition:background-color .12s,color .12s}.fThDlq_deviceRevoke:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_deviceRevoke:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address,.fThDlq_deviceRevoke{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;min-width:0;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]{flex-flow:wrap;align-items:center}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]>:not([class*=settingsArea]):not([class*=footerActions]){flex:100%;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=settingsArea]{flex:auto;order:1;width:auto;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]{flex:none;order:2;align-items:center;width:auto;min-width:0}.fThDlq_updateStatus{margin:0;font-weight:600}.fThDlq_updateDetail{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_updateError{color:var(--dsw-alias-text-danger,var(--dsw-alias-label-primary));margin:0;font-weight:600}.fThDlq_updateOutput{background:var(--dsw-alias-bg-layer-1);max-height:180px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all;border-radius:10px;margin:10px 0 0;padding:10px 12px;font-size:12px;line-height:18px;overflow:auto}.fThDlq_updateList{flex-direction:column;gap:6px;margin:12px 0 0;padding:0;list-style:none;display:flex}.fThDlq_updateListItem{justify-content:space-between;align-items:baseline;gap:12px;font-size:13px;display:flex}.fThDlq_updateListName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);overflow:hidden}.fThDlq_updateListVersions{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none}.fThDlq_updateNotes{margin-top:14px}.fThDlq_updateNotesTitle{margin:0 0 8px;font-size:14px;font-weight:600}.fThDlq_updateNoteSection{flex-direction:column;gap:4px;margin-top:10px;display:flex}.fThDlq_updateNoteHeading{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;font-weight:600}.fThDlq_updateNoteList{flex-direction:column;gap:4px;margin:0;padding-left:18px;font-size:13px;display:flex}.fThDlq_updateNoteItem{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}.fThDlq_updateVersions{margin-top:12px}.fThDlq_updateVersionsSummary{cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none;font-size:12px}.fThDlq_updateActions{justify-content:flex-end;margin-top:16px;display:flex}.fThDlq_updateRetry{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:6px;padding:8px 14px;font-size:13px;transition:background-color .12s,color .12s;display:inline-flex}.fThDlq_updateRetry:hover{background:var(--dsw-alias-interactive-bg-active)}@media (prefers-reduced-motion:reduce){.fThDlq_updateRetry{transition:none}}";
1380
1413
  const tagId$1 = "@linxin666/dsh-remote-web-ui/packages/dsh-remote-web-ui/src/client/remote.module.css";
1381
1414
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1382
1415
  const tag = document.createElement("style");
@@ -1484,6 +1517,20 @@ window.__ModuleLoader__.load({
1484
1517
  * actions (stop / refresh / copy). Pure presentation — all state and
1485
1518
  * actions arrive through props from the entry's behavior component.
1486
1519
  */
1520
+ /**
1521
+ * The QR symbol, memoized on the link: the panel re-renders on every SSE state
1522
+ * frame (a paired phone heartbeats every 10s), and qrcode.react rebuilds the
1523
+ * SVG path and element tree on each render.
1524
+ */
1525
+ const PairQrCode = (0, react.memo)(function PairQrCode({ url, className }) {
1526
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QRCodeSVG, {
1527
+ value: url,
1528
+ size: 184,
1529
+ level: "M",
1530
+ marginSize: 1,
1531
+ className
1532
+ });
1533
+ });
1487
1534
  /** Badge text + tone per phase (ready states only). */
1488
1535
  function statusOf(t, state) {
1489
1536
  switch (state.phase) {
@@ -1536,7 +1583,7 @@ window.__ModuleLoader__.load({
1536
1583
  className: remote_module_css_default.close,
1537
1584
  "aria-label": t("close.label"),
1538
1585
  onClick: onClose,
1539
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
1586
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutlineRegular, { size: 14 })
1540
1587
  })]
1541
1588
  }), state.kind === "lan-required" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1542
1589
  className: remote_module_css_default.banner,
@@ -1602,11 +1649,8 @@ window.__ModuleLoader__.load({
1602
1649
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1603
1650
  className: remote_module_css_default.qrWrap,
1604
1651
  "data-testid": "remote-qr",
1605
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QRCodeSVG, {
1606
- value: state.url,
1607
- size: 184,
1608
- level: "M",
1609
- marginSize: 1,
1652
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PairQrCode, {
1653
+ url: state.url,
1610
1654
  className: remote_module_css_default.qr
1611
1655
  })
1612
1656
  }),
@@ -1641,7 +1685,7 @@ window.__ModuleLoader__.load({
1641
1685
  type: "button",
1642
1686
  className: remote_module_css_default.copyLink,
1643
1687
  onClick: () => onCopy(state.url),
1644
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }), copied ? t("action.copied") : t("action.copyLink")]
1688
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutlineRegular, { size: 14 }), copied ? t("action.copied") : t("action.copyLink")]
1645
1689
  })]
1646
1690
  }), state.token !== void 0 && state.token !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1647
1691
  className: remote_module_css_default.pairLinkRow,
@@ -1659,7 +1703,7 @@ window.__ModuleLoader__.load({
1659
1703
  type: "button",
1660
1704
  className: remote_module_css_default.copyLink,
1661
1705
  onClick: () => onCopyToken ? onCopyToken(state.token) : onCopy(state.token),
1662
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }), copiedToken ? t("action.copiedToken") : t("action.copyToken")]
1706
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutlineRegular, { size: 14 }), copiedToken ? t("action.copiedToken") : t("action.copyToken")]
1663
1707
  })]
1664
1708
  })]
1665
1709
  }),
@@ -1740,12 +1784,12 @@ window.__ModuleLoader__.load({
1740
1784
  type: "button",
1741
1785
  className: remote_module_css_default.action,
1742
1786
  onClick: onStop,
1743
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 14 }), t("action.stop")]
1787
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFillRegular, { size: 14 }), t("action.stop")]
1744
1788
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1745
1789
  type: "button",
1746
1790
  className: remote_module_css_default.action,
1747
1791
  onClick: onRefresh,
1748
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }), t("action.refresh")]
1792
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 14 }), t("action.refresh")]
1749
1793
  })]
1750
1794
  }),
1751
1795
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -1897,7 +1941,7 @@ window.__ModuleLoader__.load({
1897
1941
  className: remote_module_css_default.close,
1898
1942
  "aria-label": t("update.close"),
1899
1943
  onClick: onClose,
1900
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
1944
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutlineRegular, {})
1901
1945
  })]
1902
1946
  }),
1903
1947
  view.kind === "checking" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1952,7 +1996,7 @@ window.__ModuleLoader__.load({
1952
1996
  className: remote_module_css_default.updateRetry,
1953
1997
  onClick: onRecheck,
1954
1998
  children: [
1955
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {}),
1999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {}),
1956
2000
  " ",
1957
2001
  t("update.retry")
1958
2002
  ]
@@ -2258,7 +2302,7 @@ window.__ModuleLoader__.load({
2258
2302
  "aria-expanded": open,
2259
2303
  title: updateLabel,
2260
2304
  onClick: openPanel,
2261
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { size: wide ? 16 : 18 }), updateAvailable && wide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2305
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineRegular, { size: wide ? 16 : 18 }), updateAvailable && wide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2262
2306
  className: remote_module_css_default.updateBadgeText,
2263
2307
  children: t("update.badge")
2264
2308
  })]
@@ -2414,19 +2458,21 @@ window.__ModuleLoader__.load({
2414
2458
  }, [state]);
2415
2459
  (0, react.useEffect)(() => closeEventSource, [closeEventSource]);
2416
2460
  const handleStop = (0, react.useCallback)(() => {
2417
- stopPair().catch(() => {});
2418
- setState((previous) => previous.kind === "ready" ? {
2419
- ...previous,
2420
- phase: "stopped",
2421
- devices: []
2422
- } : previous);
2461
+ stopPair().then(() => {
2462
+ setState((previous) => previous.kind === "ready" ? {
2463
+ ...previous,
2464
+ phase: "stopped",
2465
+ devices: []
2466
+ } : previous);
2467
+ }).catch(() => {});
2423
2468
  }, []);
2424
2469
  const handleRevoke = (0, react.useCallback)((deviceId) => {
2425
- revokePair(deviceId).catch(() => {});
2426
- setState((previous) => previous.kind === "ready" ? {
2427
- ...previous,
2428
- devices: previous.devices.filter((device) => device.id !== deviceId)
2429
- } : previous);
2470
+ revokePair(deviceId).then(() => {
2471
+ setState((previous) => previous.kind === "ready" ? {
2472
+ ...previous,
2473
+ devices: previous.devices.filter((device) => device.id !== deviceId)
2474
+ } : previous);
2475
+ }).catch(() => {});
2430
2476
  }, []);
2431
2477
  const handleRefresh = (0, react.useCallback)(() => {
2432
2478
  mint().then(setState);
@@ -2688,17 +2734,20 @@ window.__ModuleLoader__.load({
2688
2734
  })
2689
2735
  ]
2690
2736
  });
2691
- if (!state.exposed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2692
- className: cardClass,
2693
- children: [header, expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2694
- className: settings_card_module_css_default.body,
2695
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2696
- className: settings_card_module_css_default.notExposed,
2697
- role: "status",
2698
- children: props.t("settings.notExposed")
2699
- })
2700
- }) : null]
2701
- });
2737
+ if (!state.exposed && props.renderChildrenWhenNotExposed !== true) {
2738
+ const showNotice = props.hideNotExposedNotice !== true;
2739
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2740
+ className: cardClass,
2741
+ children: [header, expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2742
+ className: settings_card_module_css_default.body,
2743
+ children: showNotice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2744
+ className: settings_card_module_css_default.notExposed,
2745
+ role: "status",
2746
+ children: props.t("settings.notExposed")
2747
+ }) : null
2748
+ }) : null]
2749
+ });
2750
+ }
2702
2751
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2703
2752
  className: cardClass,
2704
2753
  children: [header, expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3098,31 +3147,31 @@ window.__ModuleLoader__.load({
3098
3147
  specs;
3099
3148
  staged = /* @__PURE__ */ new Map();
3100
3149
  listeners = /* @__PURE__ */ new Set();
3101
- /** The scope subscription installed in the constructor; released by dispose(). */
3102
- disposeScope;
3150
+ /** The form subscription installed in the constructor; released by dispose(). */
3151
+ disposeForm;
3103
3152
  disposed = false;
3104
3153
  saving = false;
3105
3154
  failed = false;
3106
3155
  failedReason;
3107
- /** @param scope - the bound settings scope for this card's namespace. */
3156
+ /** @param scope - the bound configuration form for this card's namespace. */
3108
3157
  constructor(scope, specs) {
3109
3158
  this.scope = scope;
3110
3159
  this.specs = new Map(specs.map((spec) => [spec.field, spec]));
3111
- this.disposeScope = scope.subscribe(() => {
3160
+ this.disposeForm = scope.subscribe(() => {
3112
3161
  this.publish();
3113
3162
  });
3114
3163
  }
3115
3164
  /**
3116
- * Release the scope subscription and every bound store listener. The card
3165
+ * Release the form subscription and every bound store listener. The card
3117
3166
  * must call this on teardown; later calls are no-ops.
3118
3167
  */
3119
3168
  dispose() {
3120
3169
  if (this.disposed) return;
3121
3170
  this.disposed = true;
3122
- this.disposeScope();
3171
+ this.disposeForm();
3123
3172
  this.listeners.clear();
3124
3173
  }
3125
- /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
3174
+ /** Publish a projection of this form, rebuilt whenever the form or a draft changes. */
3126
3175
  bind(project) {
3127
3176
  const store = createSnapshotStore(project());
3128
3177
  this.listeners.add(() => {
@@ -3189,19 +3238,19 @@ window.__ModuleLoader__.load({
3189
3238
  };
3190
3239
  }
3191
3240
  /**
3192
- * Write every staged edit in one atomic scope mutation, then re-seed from
3241
+ * Write every staged edit in one atomic form mutation, then re-seed from
3193
3242
  * what the Host accepted.
3194
3243
  *
3195
3244
  * The whole batch rides one mutate, so cross-field validate hooks
3196
3245
  * (baseURL+model) judge it as a unit: the Host either applies every write
3197
- * or refuses the batch. The 0.1.2 scope contract never rejects a refused
3198
- * mutation — the scope recovers with a fresh Host view and resolves — so
3199
- * resolution alone proves nothing: the outcome is judged by reading the
3200
- * settled snapshot back, one planned write at a time, and one missed write
3201
- * fails the whole save. A scope that still rejects on refusal (the dsh-web
3202
- * bridge scope) reports through the same failure path with its rejection
3203
- * message. A save that did not land keeps its drafts, so the user can
3204
- * correct them instead of retyping.
3246
+ * or refuses the batch. The form contract answers a refusal or a skipped
3247
+ * write with `false` (it recovers with a fresh Host view instead of
3248
+ * throwing), so the outcome is judged twice: the answer itself, and then the
3249
+ * settled snapshot read back one planned write at a time. One missed write
3250
+ * fails the whole save. A transport that rejects instead (the dsh-web bridge
3251
+ * controller on a dead connection) reports through the same failure path
3252
+ * with its rejection message. A save that did not land keeps its drafts, so
3253
+ * the user can correct them instead of retyping.
3205
3254
  * @returns settlement after the mutation and the read-back.
3206
3255
  */
3207
3256
  async save() {
@@ -3223,12 +3272,13 @@ window.__ModuleLoader__.load({
3223
3272
  path: [item.field]
3224
3273
  });
3225
3274
  let failedReason;
3275
+ let accepted = false;
3226
3276
  try {
3227
- await this.scope.mutate(ops);
3277
+ accepted = await this.scope.mutate(ops);
3228
3278
  } catch (error) {
3229
3279
  failedReason = error instanceof Error ? error.message : String(error);
3230
3280
  }
3231
- const landed = failedReason === void 0 && valid.every((item) => item.judge());
3281
+ const landed = accepted && failedReason === void 0 && valid.every((item) => item.judge());
3232
3282
  for (const [field, before] of pending) if (landed && this.staged.get(field) === before) this.staged.delete(field);
3233
3283
  this.saving = false;
3234
3284
  this.failed = !landed;
@@ -3338,13 +3388,13 @@ window.__ModuleLoader__.load({
3338
3388
  /**
3339
3389
  * The remote-control settings card: pairing security and device limits.
3340
3390
  * Registers into the `web-ui.plugin.item` child slot the Web UI plugin group
3341
- * renders, bound to the `remote-web-ui` settings namespace.
3391
+ * renders, bound to the `remote-web-ui` profile entry's configuration form.
3342
3392
  */
3343
- /** Bridges the `remote-web-ui` scope onto the card's staged form. */
3393
+ /** Bridges the `remote-web-ui` form onto the card's staged form. */
3344
3394
  var RemoteSettingsCardController = class {
3345
3395
  form;
3346
3396
  store;
3347
- /** @param scope - the bound settings scope for the `remote-web-ui` namespace. */
3397
+ /** @param scope - the configuration form for the `remote-web-ui` entry. */
3348
3398
  constructor(scope) {
3349
3399
  this.form = new CardForm(scope, [
3350
3400
  booleanField("enabled"),
@@ -3390,7 +3440,7 @@ window.__ModuleLoader__.load({
3390
3440
  };
3391
3441
  }
3392
3442
  /**
3393
- * Release the card's scope subscription and bound stores; the slot
3443
+ * Release the card's form subscription and bound stores; the slot
3394
3444
  * disposer calls this on teardown.
3395
3445
  */
3396
3446
  dispose() {
@@ -3418,6 +3468,8 @@ window.__ModuleLoader__.load({
3418
3468
  descriptionKey: "settings.description",
3419
3469
  defaultOpen: false,
3420
3470
  state,
3471
+ renderChildrenWhenNotExposed: true,
3472
+ hideNotExposedNotice: true,
3421
3473
  onSave: props.save,
3422
3474
  onDiscard: props.discard,
3423
3475
  children: [
@@ -3615,10 +3667,12 @@ window.__ModuleLoader__.load({
3615
3667
  const read = () => {
3616
3668
  readLanBindStatus().then((value) => {
3617
3669
  if (alive) setFrame(value);
3618
- }).catch(() => {});
3670
+ }).catch((error) => {
3671
+ if (error instanceof LanBindStatusError && shouldStopLanBindPoll(error.status)) window.clearInterval(timer);
3672
+ });
3619
3673
  };
3620
- read();
3621
3674
  const timer = window.setInterval(read, 1e4);
3675
+ read();
3622
3676
  return () => {
3623
3677
  alive = false;
3624
3678
  window.clearInterval(timer);
@@ -3684,9 +3738,9 @@ window.__ModuleLoader__.load({
3684
3738
  "pair.linkLabel": "配对链接",
3685
3739
  "pair.tokenLabel": "配对令牌",
3686
3740
  "pair.dockerHint": "Docker 或反向代理环境下,可直接复制此令牌并在目标设备配对页面输入。",
3687
- "pair.oneTimeHint": "链接含一次性令牌;任一设备配对成功后立即失效,配对下一台设备请刷新二维码。",
3741
+ "pair.oneTimeHint": "链接含限时令牌;在令牌过期或刷新二维码之前,同一链接可为多台设备完成配对(每次配对各自生成独立设备会话)。",
3688
3742
  "pair.failed.title": "配对失败",
3689
- "pair.failed.detail": "链接无效或已使用,请回到电脑端刷新二维码后重新扫码。",
3743
+ "pair.failed.detail": "链接无效或已过期,请回到电脑端刷新二维码后重新扫码。",
3690
3744
  "fence.unpaired.title": "此设备未配对,无法访问工作区数据",
3691
3745
  "fence.unpaired.eyebrow": "需要设备配对",
3692
3746
  "fence.unpaired.hint": "为保护工作区、会话与插件数据,远程电脑必须先通过主电脑授权。",
@@ -3698,7 +3752,6 @@ window.__ModuleLoader__.load({
3698
3752
  "fence.unpaired.pairAction": "立即配对",
3699
3753
  "fence.unpaired.pairing": "配对中…",
3700
3754
  "fence.unpaired.tokenInvalid": "配对链接或 Token 无效已过期",
3701
- "fence.unpaired.tokenUsed": "该配对链接已被使用",
3702
3755
  "fence.unpaired.tokenFailed": "配对失败,请检查网络或重新获取链接",
3703
3756
  "fence.unpaired.footnote": "请勿使用他人提供的配对链接;管理员可随时取消此设备的授权。",
3704
3757
  "posture.exposed": "/api 通道对未配对设备敞开",
@@ -3849,9 +3902,9 @@ window.__ModuleLoader__.load({
3849
3902
  "pair.linkLabel": "Pairing link",
3850
3903
  "pair.tokenLabel": "Pairing token",
3851
3904
  "pair.dockerHint": "In Docker or reverse proxy environments, copy this token to pair directly on the target device.",
3852
- "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.",
3905
+ "pair.oneTimeHint": "The link carries a time-limited token: until it expires or the QR is refreshed, the same link can pair several devices, each with its own device session.",
3853
3906
  "pair.failed.title": "Pairing failed",
3854
- "pair.failed.detail": "The link is invalid or was already used. Refresh the QR code on your computer and scan again.",
3907
+ "pair.failed.detail": "The link is invalid or has expired. Refresh the QR code on your computer and scan again.",
3855
3908
  "fence.unpaired.title": "This device is not paired and cannot reach workspace data",
3856
3909
  "fence.unpaired.eyebrow": "Device pairing required",
3857
3910
  "fence.unpaired.hint": "To protect workspace, session, and plugin data, a remote computer must first be authorized by the primary computer.",
@@ -3863,7 +3916,6 @@ window.__ModuleLoader__.load({
3863
3916
  "fence.unpaired.pairAction": "Pair Now",
3864
3917
  "fence.unpaired.pairing": "Pairing…",
3865
3918
  "fence.unpaired.tokenInvalid": "Pairing link or token is invalid or expired",
3866
- "fence.unpaired.tokenUsed": "This pairing link has already been used",
3867
3919
  "fence.unpaired.tokenFailed": "Pairing failed, please check network or issue a new link",
3868
3920
  "fence.unpaired.footnote": "Do not use pairing links from people you do not trust. An administrator can revoke this device at any time.",
3869
3921
  "posture.exposed": "The /api channel is open to unpaired devices",
@@ -4066,13 +4118,15 @@ window.__ModuleLoader__.load({
4066
4118
  "/api/remote.mux",
4067
4119
  "/sidebar/ws/terminal",
4068
4120
  "/sidebar/ws/agent-terminals",
4121
+ "/sidebar/ws/agent-opens",
4069
4122
  "/api/dsh-ssh/terminal"
4070
4123
  ],
4071
4124
  deviceHeader: REMOTE_DEVICE_HEADER,
4072
4125
  deviceKey: "dsh-remote-device",
4073
4126
  deviceQuery: REMOTE_DEVICE_QUERY,
4074
4127
  uploadPath: "/api/session/uploadFileBinary",
4075
- uploadHookGlobal: "__DSH_FILE_UPLOAD__"
4128
+ uploadHookGlobal: "__DSH_FILE_UPLOAD__",
4129
+ hostGrantGlobal: "__DSH_REMOTE_HOST_GRANT__"
4076
4130
  };
4077
4131
  /** The window global the boot patch publishes its seat under. */
4078
4132
  const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
@@ -4115,7 +4169,7 @@ window.__ModuleLoader__.load({
4115
4169
  * @returns true for localhost, IPv6 loopback, or any 127/8 literal.
4116
4170
  */
4117
4171
  function isLoopbackHostname(hostname) {
4118
- if (hostname === "localhost" || hostname === "::1") return true;
4172
+ if (hostname === "localhost" || hostname === "::1" || hostname === "[::1]") return true;
4119
4173
  const parts = hostname.split(".");
4120
4174
  return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
4121
4175
  }
@@ -4233,10 +4287,14 @@ window.__ModuleLoader__.load({
4233
4287
  const attach = (init) => {
4234
4288
  if (device === null) return init;
4235
4289
  const headers = init?.headers;
4236
- if (typeof Headers !== "undefined" && headers instanceof Headers) {
4237
- try {
4238
- headers.set(RULES.deviceHeader, device);
4239
- } catch {}
4290
+ if (typeof Headers !== "undefined" && headers instanceof Headers) try {
4291
+ const copy = new Headers(headers);
4292
+ copy.set(RULES.deviceHeader, device);
4293
+ return {
4294
+ ...init,
4295
+ headers: copy
4296
+ };
4297
+ } catch {
4240
4298
  return init;
4241
4299
  }
4242
4300
  if (typeof headers === "object" && headers !== null) return {
@@ -4259,7 +4317,7 @@ window.__ModuleLoader__.load({
4259
4317
  rewritten.pathname = rewritePath(url.pathname);
4260
4318
  const next = typeof input === "string" || input instanceof URL ? rewritten.toString() : new Request(rewritten, input);
4261
4319
  return Promise.resolve(originalFetch.call(window, next, attach(init))).then(async (response) => {
4262
- if (await isUnpairedDenied(response.clone())) options.onUnpaired?.();
4320
+ if (response.status === 403 && await isUnpairedDenied(response.clone())) options.onUnpaired?.();
4263
4321
  else options.onPaired?.();
4264
4322
  return response;
4265
4323
  });
@@ -4403,7 +4461,6 @@ window.__ModuleLoader__.load({
4403
4461
  return;
4404
4462
  }
4405
4463
  if (result.code === "invalid") setErrorMsg(t("fence.unpaired.tokenInvalid"));
4406
- else if (result.code === "used") setErrorMsg(t("fence.unpaired.tokenUsed"));
4407
4464
  else setErrorMsg(t("fence.unpaired.tokenFailed"));
4408
4465
  } catch {
4409
4466
  setErrorMsg(t("fence.unpaired.tokenFailed"));
@@ -4496,7 +4553,7 @@ window.__ModuleLoader__.load({
4496
4553
  /** The building package's version, when the bundle carries it. */
4497
4554
  function bakedVersion() {
4498
4555
  try {
4499
- return "0.3.24";
4556
+ return "0.4.1";
4500
4557
  } catch {
4501
4558
  return;
4502
4559
  }
@@ -4755,7 +4812,7 @@ window.__ModuleLoader__.load({
4755
4812
  * rail compaction) while the body class stays.
4756
4813
  */
4757
4814
  function ensureAdaptStyle() {
4758
- if (document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`) !== null) return;
4815
+ if (nodeOf(`style[data-plugin-css="${ADAPT_CSS_ID}"]`) !== null) return;
4759
4816
  const tag = document.createElement("style");
4760
4817
  tag.dataset.plugin = "remote-web-ui";
4761
4818
  tag.dataset.pluginCss = ADAPT_CSS_ID;
@@ -4795,6 +4852,7 @@ window.__ModuleLoader__.load({
4795
4852
  savedViewportContent = null;
4796
4853
  }
4797
4854
  if (whaleEl !== null) whaleEl.style.display = "none";
4855
+ whaleShown = false;
4798
4856
  setWhaleTimer(false);
4799
4857
  if (whaleObserver !== null) {
4800
4858
  whaleObserver.disconnect();
@@ -4857,7 +4915,7 @@ window.__ModuleLoader__.load({
4857
4915
  }
4858
4916
  function syncCompactPicker() {
4859
4917
  if (!active) return;
4860
- const tools = document.querySelector("[class$=\"_composerSeat\"] [class$=\"_tools\"]");
4918
+ const tools = nodeOf("[class$=\"_composerSeat\"] [class$=\"_tools\"]");
4861
4919
  const trigger = tools?.parentElement?.querySelector("[class$=\"_triggerEffort\"]")?.parentElement;
4862
4920
  if (tools === null || trigger === null) {
4863
4921
  removeCompactPicker();
@@ -5011,17 +5069,23 @@ window.__ModuleLoader__.load({
5011
5069
  document.body.appendChild(whale);
5012
5070
  }
5013
5071
  /**
5072
+ * Write one row's recorded official draggable state back onto it.
5073
+ * @param row - the tracked row element.
5074
+ * @param original - the recorded attribute value, or null when it was absent.
5075
+ */
5076
+ function restoreRowDragState(row, original) {
5077
+ if (original === null) row.removeAttribute("draggable");
5078
+ else row.setAttribute("draggable", original);
5079
+ }
5080
+ /**
5014
5081
  * Restore the official draggable state this layer overrode while active.
5015
5082
  * Rows are React-owned and may have been re-created meanwhile, so only the
5016
- * tracked elements are touched; a detached element is skipped.
5083
+ * tracked elements are touched; a detached element is written back too
5084
+ * (harmless, and it keeps the entry prunable at every tick).
5017
5085
  */
5018
5086
  function restoreRowDrag() {
5019
5087
  if (dragOverridden.size === 0) return;
5020
- for (const [row, original] of dragOverridden) {
5021
- if (!row.isConnected) continue;
5022
- if (original === null) row.removeAttribute("draggable");
5023
- else row.setAttribute("draggable", original);
5024
- }
5088
+ for (const [row, original] of dragOverridden) restoreRowDragState(row, original);
5025
5089
  dragOverridden.clear();
5026
5090
  }
5027
5091
  /** Restore a dragged position when the whale becomes visible again. */
@@ -5047,6 +5111,34 @@ window.__ModuleLoader__.load({
5047
5111
  if (!dragOverridden.has(row)) dragOverridden.set(row, row.getAttribute("draggable"));
5048
5112
  row.setAttribute("draggable", "false");
5049
5113
  }
5114
+ if (dragOverridden.size === 0) return;
5115
+ for (const [row, original] of dragOverridden) {
5116
+ if (row.isConnected) continue;
5117
+ restoreRowDragState(row, original);
5118
+ dragOverridden.delete(row);
5119
+ }
5120
+ }
5121
+ /**
5122
+ * Official nodes resolved through the document and cached while they stay
5123
+ * connected. The sync tick runs every 600ms for the page lifetime, and these
5124
+ * selectors (suffix class matches, :has()) cannot use Blink's fast paths, so
5125
+ * every miss walks the whole mounted DOM — the official chat keeps the whole
5126
+ * conversation mounted, i.e. tens of thousands of elements. React replaces a
5127
+ * node on a major re-render, which the isConnected guard detects.
5128
+ */
5129
+ const nodeCache = /* @__PURE__ */ new Map();
5130
+ function nodeOf(selector) {
5131
+ const cached = nodeCache.get(selector);
5132
+ if (cached !== void 0 && cached.isConnected) return cached;
5133
+ const found = document.querySelector(selector);
5134
+ if (found === null) nodeCache.delete(selector);
5135
+ else nodeCache.set(selector, found);
5136
+ return found;
5137
+ }
5138
+ /** The official application frame, through the same cached lookup. */
5139
+ function frameEl() {
5140
+ const frame = nodeOf(APP_FRAME_SELECTOR);
5141
+ return frame instanceof HTMLElement ? frame : null;
5050
5142
  }
5051
5143
  function syncWhale() {
5052
5144
  if (active) ensureAdaptStyle();
@@ -5055,8 +5147,8 @@ window.__ModuleLoader__.load({
5055
5147
  whaleEl.style.display = "none";
5056
5148
  return;
5057
5149
  }
5058
- const collapsed = appFrame()?.hasAttribute("data-sidebar-collapsed") === true;
5059
- const overlayUp = document.querySelector("[class$=\"_overlay\"]") !== null;
5150
+ const collapsed = frameEl()?.hasAttribute("data-sidebar-collapsed") === true;
5151
+ const overlayUp = nodeOf("[class$=\"_overlay\"]") !== null;
5060
5152
  const show = collapsed && !overlayUp;
5061
5153
  if (show && !whaleShown) applyWhalePos();
5062
5154
  whaleEl.style.display = show ? "" : "none";
@@ -5072,7 +5164,7 @@ window.__ModuleLoader__.load({
5072
5164
  }
5073
5165
  function seatHeaderActions() {
5074
5166
  if (!active) return;
5075
- const header = document.querySelector("[class$=\"_header\"]");
5167
+ const header = nodeOf("[class$=\"_header\"]");
5076
5168
  const tabs = header !== null ? header.querySelector("[class$=\"_tabs\"]") : null;
5077
5169
  const actions = header !== null ? header.querySelector("[class$=\"_titleCluster\"] [class$=\"_headerActions\"]") : null;
5078
5170
  const seated = header !== null && tabs !== null && actions !== null;
@@ -5081,7 +5173,7 @@ window.__ModuleLoader__.load({
5081
5173
  }
5082
5174
  function alignActionsText() {
5083
5175
  if (!active) return;
5084
- const header = document.querySelector("[class$=\"_header\"]");
5176
+ const header = nodeOf("[class$=\"_header\"]");
5085
5177
  if (header === null) return;
5086
5178
  const tabs = header.querySelector("[class$=\"_tabs\"]");
5087
5179
  const actions = header.querySelector("[class$=\"_titleCluster\"] [class$=\"_headerActions\"]");
@@ -5188,6 +5280,7 @@ window.__ModuleLoader__.load({
5188
5280
  if (isMobilePortrait()) {
5189
5281
  apply();
5190
5282
  ensureWhaleObserver();
5283
+ if (active && drag === null) applyWhalePos();
5191
5284
  } else revert();
5192
5285
  }
5193
5286
  evaluate();
@@ -5255,7 +5348,7 @@ window.__ModuleLoader__.load({
5255
5348
  }
5256
5349
  const t = e.target;
5257
5350
  const editable = t instanceof HTMLElement && t.isContentEditable;
5258
- if (t instanceof Element && (t.tagName === "TEXTAREA" || t.tagName === "INPUT" || editable || t.closest(`#${WHALE_ID}`) !== null || insideHScrollable(t) || t.closest("table, [class$=\"_table\"], [class$=\"_tablePane\"]") !== null)) {
5351
+ if (t instanceof Element && (t.tagName === "TEXTAREA" || t.tagName === "INPUT" || editable || t.closest(`#${WHALE_ID}`) !== null || t.closest("table, [class$=\"_table\"], [class$=\"_tablePane\"]") !== null)) {
5259
5352
  swipeTouch = null;
5260
5353
  return;
5261
5354
  }
@@ -5264,7 +5357,8 @@ window.__ModuleLoader__.load({
5264
5357
  swipeTouch = {
5265
5358
  x: ct.clientX,
5266
5359
  y: ct.clientY,
5267
- id: ct.identifier
5360
+ id: ct.identifier,
5361
+ el: t instanceof Element ? t : null
5268
5362
  };
5269
5363
  }, {
5270
5364
  capture: true,
@@ -5277,12 +5371,14 @@ window.__ModuleLoader__.load({
5277
5371
  if (ct === void 0 || ct.identifier !== swipeTouch.id) return;
5278
5372
  const dx = ct.clientX - swipeTouch.x;
5279
5373
  const dy = ct.clientY - swipeTouch.y;
5374
+ const start = swipeTouch;
5280
5375
  swipeTouch = null;
5281
5376
  if (!active) return;
5282
5377
  if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
5283
- const frame = appFrame();
5378
+ if (start.el !== null && insideHScrollable(start.el)) return;
5379
+ const frame = frameEl();
5284
5380
  if (frame === null) return;
5285
- if (document.querySelector("[class$=\"_overlay\"], [class$=\"_dialog\"], [class$=\"_menu\"], [class*=\"_portal\"]") !== null) return;
5381
+ if (nodeOf("[class$=\"_overlay\"], [class$=\"_dialog\"], [class$=\"_menu\"], [class*=\"_portal\"]") !== null) return;
5286
5382
  const collapsed = frame.hasAttribute("data-sidebar-collapsed");
5287
5383
  if (dx < 0 && !collapsed) collapseSidebar();
5288
5384
  else if (dx > 0 && collapsed) toggleSidebarVerified();
@@ -5414,10 +5510,23 @@ window.__ModuleLoader__.load({
5414
5510
  return isComposerField(el) ? el : null;
5415
5511
  };
5416
5512
  const lanOrigFocus = HTMLElement.prototype.focus;
5417
- HTMLElement.prototype.focus = function(options) {
5513
+ const patchedFocus = function(options) {
5418
5514
  if (active && isComposerField(this) && Date.now() - lastComposerTap >= 800) return;
5419
5515
  lanOrigFocus.call(this, options);
5420
5516
  };
5517
+ /** (Re-)install the composer-focus guard (see setEnabled). */
5518
+ const installFocusPatch = () => {
5519
+ HTMLElement.prototype.focus = patchedFocus;
5520
+ };
5521
+ /**
5522
+ * Remove the guard when the layer is disabled. A patch another plugin
5523
+ * installed after ours is left alone (identity check), so disabling this
5524
+ * layer never removes someone else's behavior.
5525
+ */
5526
+ const restoreFocusPatch = () => {
5527
+ if (HTMLElement.prototype.focus === patchedFocus) HTMLElement.prototype.focus = lanOrigFocus;
5528
+ };
5529
+ installFocusPatch();
5421
5530
  document.addEventListener("pointerdown", (e) => {
5422
5531
  if (!active) return;
5423
5532
  if (composerFieldOf(e.target) !== null) lastComposerTap = Date.now();
@@ -5429,8 +5538,13 @@ window.__ModuleLoader__.load({
5429
5538
  translate: null,
5430
5539
  setEnabled(on) {
5431
5540
  adaptEnabled = on;
5432
- if (on) evaluate();
5433
- else revert();
5541
+ if (on) {
5542
+ installFocusPatch();
5543
+ evaluate();
5544
+ } else {
5545
+ revert();
5546
+ restoreFocusPatch();
5547
+ }
5434
5548
  },
5435
5549
  flushCloseDetails() {
5436
5550
  if (active) w.__dshRemoteAdapt?.closeDetails?.();
@@ -5447,22 +5561,24 @@ window.__ModuleLoader__.load({
5447
5561
  *
5448
5562
  * - `web-ui.plugin.item` — the list seat declared by the dsh-web-settings
5449
5563
  * group section (this family's own first-level "Web UI plugins" section);
5450
- * - `settings.plugin.item` — the official keyed seat of the harness's
5451
- * `ui-settings-plugins` tab, keyed by the settings namespace the card edits.
5564
+ * - `plugins.bundle.config` — the official keyed seat of the harness's plugin
5565
+ * manager page, keyed by the bundle's package name and rendered on that
5566
+ * bundle's page. alpha.2 removed the `settings.plugin.item` keyed seat of the
5567
+ * `ui-settings-plugins` tab that this helper used before, so a card keyed by
5568
+ * its settings namespace has no seat to land in any more.
5452
5569
  *
5453
- * SEAT SELECTION IS NOT A DECLARATION PROBE. The official `ui-settings-plugins`
5454
- * row belongs to the harness bundle and its `configurable` tab always declares
5455
- * `settings.plugin.item` before any external plugin's `apply()` runs, so
5456
- * "is the official seat declared?" answers yes even in the deployment whose
5457
- * whole point is the family group. Choosing on that probe sends every family
5458
- * card to the official Plugins tab and leaves the group's own section
5459
- * permanently empty — the family of reports where the section renders its
5460
- * heading and zero cards.
5570
+ * SEAT SELECTION IS NOT A DECLARATION PROBE. The official plugin surface
5571
+ * belongs to the harness bundle and declares its seats before any external
5572
+ * plugin's `apply()` runs, so "is the official seat declared?" answers yes even
5573
+ * in the deployment whose whole point is the family group. Choosing on that
5574
+ * probe sends every family card to the official page and leaves the group's own
5575
+ * section permanently empty — the family of reports where the section renders
5576
+ * its heading and zero cards.
5461
5577
  *
5462
5578
  * The signal that actually distinguishes the two deployments is whether
5463
5579
  * dsh-web-settings is loaded: it is the package that owns the group section and
5464
5580
  * it publishes the `webUiSettings` service during `apply()`, which every
5465
- * family plugin already reads for its settings scope. Group loaded -> the family
5581
+ * family plugin already reads for its settings form. Group loaded -> the family
5466
5582
  * seat; group absent -> the official seat.
5467
5583
  *
5468
5584
  * The decision is re-evaluated on every `slots/changed` because the group may
@@ -5478,8 +5594,8 @@ window.__ModuleLoader__.load({
5478
5594
  */
5479
5595
  /** The family list seat key. */
5480
5596
  const FAMILY_PLUGIN_CARD_SEAT = "web-ui.plugin.item";
5481
- /** The official keyed plugin-card seat key. */
5482
- const OFFICIAL_PLUGIN_CARD_SEAT = "settings.plugin.item";
5597
+ /** The official keyed plugin-card seat key (the alpha.2 bundle-configuration seat). */
5598
+ const OFFICIAL_PLUGIN_CARD_SEAT = "plugins.bundle.config";
5483
5599
  /** The service dsh-web-settings publishes while it is loaded. */
5484
5600
  const FAMILY_GROUP_SERVICE = "webUiSettings";
5485
5601
  /**
@@ -5542,7 +5658,7 @@ window.__ModuleLoader__.load({
5542
5658
  ...seat.inject === void 0 ? {} : { inject }
5543
5659
  } : {
5544
5660
  name: OFFICIAL_PLUGIN_CARD_SEAT,
5545
- key: seat.namespace,
5661
+ key: seat.bundle,
5546
5662
  locale: seat.locale,
5547
5663
  ...seat.inject === void 0 ? {} : { inject }
5548
5664
  }, component);
@@ -5575,6 +5691,21 @@ window.__ModuleLoader__.load({
5575
5691
  const NS = "remote";
5576
5692
  /** Settings namespace the remote-control card edits (the Host plugin registers it). */
5577
5693
  const REMOTE_WEB_UI_NS = "remote-web-ui";
5694
+ const REMOTE_WEB_UI_ENTRY_IDS = [
5695
+ "web-ui-remote-web-ui",
5696
+ "ui-remote-web-ui",
5697
+ REMOTE_WEB_UI_NS
5698
+ ];
5699
+ function servedEntryId(forms) {
5700
+ let served;
5701
+ try {
5702
+ served = forms.describe().getSnapshot().view?.namespaces.map((view) => view.ns);
5703
+ } catch {
5704
+ served = void 0;
5705
+ }
5706
+ if (!served || served.length === 0) return REMOTE_WEB_UI_NS;
5707
+ return REMOTE_WEB_UI_ENTRY_IDS.find((id) => served.includes(id)) ?? REMOTE_WEB_UI_NS;
5708
+ }
5578
5709
  /** Heartbeat cadence from a paired phone (presence + revocation liveness). */
5579
5710
  const HEARTBEAT_INTERVAL_MS = 1e4;
5580
5711
  /** Services required by this plugin. */
@@ -5582,7 +5713,7 @@ window.__ModuleLoader__.load({
5582
5713
  "slots",
5583
5714
  "locale",
5584
5715
  "connection",
5585
- "settingsScope",
5716
+ "configForms",
5586
5717
  "remote"
5587
5718
  ];
5588
5719
  /**
@@ -5625,15 +5756,16 @@ window.__ModuleLoader__.load({
5625
5756
  };
5626
5757
  const t = ctx.locale.bind(NS);
5627
5758
  if (adapt !== void 0) adapt.translate = t;
5628
- const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: REMOTE_WEB_UI_NS });
5759
+ const family = ctx.get("webUiSettings");
5760
+ const settingsForm = family !== void 0 && typeof family.bind === "function" ? family.bind({ namespace: REMOTE_WEB_UI_NS }) : ctx.configForms.get(servedEntryId(ctx.configForms));
5629
5761
  const enabled = () => {
5630
- const snapshot = settingsScope.getSnapshot();
5762
+ const snapshot = settingsForm.getSnapshot();
5631
5763
  return snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable";
5632
5764
  };
5633
5765
  const syncAdaptEnabled = () => {
5634
5766
  window.__dshRemoteAdapt?.setEnabled?.(enabled());
5635
5767
  };
5636
- settingsScope.subscribe(syncAdaptEnabled);
5768
+ settingsForm.subscribe(syncAdaptEnabled);
5637
5769
  syncAdaptEnabled();
5638
5770
  try {
5639
5771
  adapt?.flushCloseDetails?.();
@@ -5653,16 +5785,16 @@ window.__ModuleLoader__.load({
5653
5785
  disposeEntry = void 0;
5654
5786
  }
5655
5787
  };
5656
- const unsubscribe = settingsScope.subscribe(syncEntry);
5788
+ const unsubscribe = settingsForm.subscribe(syncEntry);
5657
5789
  syncEntry();
5658
5790
  return () => {
5659
5791
  unsubscribe();
5660
5792
  disposeEntry?.();
5661
5793
  };
5662
5794
  });
5663
- const remoteSettings = new RemoteSettingsCardController(settingsScope);
5795
+ const remoteSettings = new RemoteSettingsCardController(settingsForm);
5664
5796
  installPluginCard(ctx, {
5665
- namespace: REMOTE_WEB_UI_NS,
5797
+ bundle: "@linxin666/dsh-remote-web-ui",
5666
5798
  id: "remote-web-ui",
5667
5799
  order: 90,
5668
5800
  locale: NS,
@@ -5679,7 +5811,9 @@ window.__ModuleLoader__.load({
5679
5811
  runPairBootFlow(ctx, window.location.search);
5680
5812
  if (loopback) return () => {};
5681
5813
  const timer = window.setInterval(() => {
5682
- sendHeartbeat().catch(() => {});
5814
+ sendHeartbeat().then((status) => {
5815
+ if (shouldStopHeartbeat(status)) window.clearInterval(timer);
5816
+ }).catch(() => {});
5683
5817
  }, HEARTBEAT_INTERVAL_MS);
5684
5818
  return () => {
5685
5819
  window.clearInterval(timer);
@@ -5690,7 +5824,7 @@ window.__ModuleLoader__.load({
5690
5824
  disposeRuntime = void 0;
5691
5825
  }
5692
5826
  };
5693
- settingsScope.subscribe(syncRuntime);
5827
+ settingsForm.subscribe(syncRuntime);
5694
5828
  syncRuntime();
5695
5829
  let disposeChannel;
5696
5830
  let hostPairingPolicy;
@@ -5720,13 +5854,13 @@ window.__ModuleLoader__.load({
5720
5854
  fenceNotice = void 0;
5721
5855
  };
5722
5856
  const handleUnpaired = () => {
5723
- if (settingsScope.getSnapshot().status !== "ready" && hostPairingPolicy === void 0) {
5857
+ if (settingsForm.getSnapshot().status !== "ready" && hostPairingPolicy === void 0) {
5724
5858
  unpairedWhilePolicyPending = true;
5725
5859
  return;
5726
5860
  }
5727
5861
  showFenceNotice();
5728
5862
  };
5729
- const channelActive = () => remoteChannelRequired(window.location.hostname, settingsScope.getSnapshot(), hostPairingPolicy);
5863
+ const channelActive = () => remoteChannelRequired(window.location.hostname, settingsForm.getSnapshot(), hostPairingPolicy);
5730
5864
  const bootSeat = () => window[REMOTE_CHANNEL_BOOT_GLOBAL];
5731
5865
  const syncChannel = () => {
5732
5866
  const transition = channelTransition(channelActive(), disposeChannel !== void 0);
@@ -5756,9 +5890,9 @@ window.__ModuleLoader__.load({
5756
5890
  hideFenceNotice();
5757
5891
  } else if (transition === "none" && !channelActive()) bootSeat()?.restore();
5758
5892
  };
5759
- settingsScope.subscribe(syncChannel);
5893
+ settingsForm.subscribe(syncChannel);
5760
5894
  syncChannel();
5761
- if (!isLoopbackHostname(window.location.hostname) && settingsScope.getSnapshot().status !== "ready") readPairGatePolicy().then((policy) => {
5895
+ if (!isLoopbackHostname(window.location.hostname) && settingsForm.getSnapshot().status !== "ready") readPairGatePolicy().then((policy) => {
5762
5896
  hostPairingPolicy = policy.requirePairingForLan;
5763
5897
  syncChannel();
5764
5898
  if (hostPairingPolicy && unpairedWhilePolicyPending) showFenceNotice();