@linxin666/dsh-remote-web-ui 0.3.23 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) 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 +396 -132
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +604 -262
  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 +56 -32
  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 +143 -28
  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 +104 -0
  35. package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
  36. package/lib/types/client/plugin-card-seat.js +143 -0
  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/console-output.d.ts +41 -0
  43. package/lib/types/console-output.d.ts.map +1 -0
  44. package/lib/types/console-output.js +89 -0
  45. package/lib/types/http.d.ts +18 -1
  46. package/lib/types/http.d.ts.map +1 -1
  47. package/lib/types/http.js +22 -1
  48. package/lib/types/index.d.ts +19 -11
  49. package/lib/types/index.d.ts.map +1 -1
  50. package/lib/types/index.js +91 -76
  51. package/lib/types/inner-auth.d.ts +3 -2
  52. package/lib/types/inner-auth.d.ts.map +1 -1
  53. package/lib/types/inner-auth.js +3 -2
  54. package/lib/types/pair-grant.d.ts +66 -0
  55. package/lib/types/pair-grant.d.ts.map +1 -0
  56. package/lib/types/pair-grant.js +95 -0
  57. package/lib/types/pairing.d.ts +44 -12
  58. package/lib/types/pairing.d.ts.map +1 -1
  59. package/lib/types/pairing.js +69 -20
  60. package/lib/types/pairing.persist.test.js +26 -0
  61. package/lib/types/posture.d.ts +11 -3
  62. package/lib/types/posture.d.ts.map +1 -1
  63. package/lib/types/posture.js +8 -3
  64. package/lib/types/remote-api.d.ts +10 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +34 -34
  67. package/lib/types/remote-channel-boot.d.ts +6 -1
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +16 -8
  70. package/lib/types/remote-channel-rules.d.ts +14 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +8 -0
  73. package/lib/types/remote-methods.d.ts +1 -1
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +1 -0
  76. package/lib/types/routes.d.ts +47 -5
  77. package/lib/types/routes.d.ts.map +1 -1
  78. package/lib/types/routes.js +160 -58
  79. package/lib/types/tunnel.d.ts +21 -6
  80. package/lib/types/tunnel.d.ts.map +1 -1
  81. package/lib/types/tunnel.js +58 -11
  82. package/lib/types/update.d.ts +4 -0
  83. package/lib/types/update.d.ts.map +1 -1
  84. package/lib/types/update.js +74 -33
  85. package/package.json +23 -20
  86. package/src/client/FenceNotice.tsx +0 -2
  87. package/src/client/PluginSettingsCard.tsx +7 -2
  88. package/src/client/RemoteEntry.tsx +14 -9
  89. package/src/client/RemotePanel.tsx +17 -7
  90. package/src/client/RemoteSettingsCard.tsx +16 -9
  91. package/src/client/UpdateEntry.tsx +2 -2
  92. package/src/client/UpdatePanel.tsx +3 -3
  93. package/src/client/deep-link.ts +2 -2
  94. package/src/client/index.ts +70 -40
  95. package/src/client/locales.ts +6 -8
  96. package/src/client/mobile-adapt.ts +136 -25
  97. package/src/client/pair-api.ts +47 -7
  98. package/src/client/plugin-card-seat.ts +192 -0
  99. package/src/client/remote-channel.ts +19 -4
  100. package/src/client/remote.module.css +38 -0
  101. package/src/client/settings-form.ts +34 -31
  102. package/src/console-output.ts +97 -0
  103. package/src/http.ts +23 -1
  104. package/src/index.ts +102 -79
  105. package/src/inner-auth.ts +3 -2
  106. package/src/pair-grant.ts +131 -0
  107. package/src/pairing.persist.test.ts +36 -0
  108. package/src/pairing.ts +75 -24
  109. package/src/posture.ts +12 -4
  110. package/src/remote-api.ts +34 -41
  111. package/src/remote-channel-boot.ts +16 -8
  112. package/src/remote-channel-rules.ts +17 -0
  113. package/src/remote-methods.ts +1 -0
  114. package/src/routes.ts +165 -57
  115. package/src/tunnel.ts +56 -11
  116. package/src/update.ts +74 -29
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.23";
4556
+ return "0.4.0";
4500
4557
  } catch {
4501
4558
  return;
4502
4559
  }
@@ -4580,6 +4637,12 @@ window.__ModuleLoader__.load({
4580
4637
  const COMPACT_CLASS = "dsh-remote-compact-picker";
4581
4638
  /** Body class while the header actions are seated in the tabs row. */
4582
4639
  const HEADER_SEATED_CLASS = "dsh-remote-header-seated";
4640
+ /**
4641
+ * Width the seated header actions paint over the tabs row, as a CSS variable
4642
+ * on <html>. The row caps its own width with it (see the v80 rules) instead of
4643
+ * reserving the space with padding.
4644
+ */
4645
+ const HEADER_RESERVE_VAR = "--dsh-remote-header-actions-reserve";
4583
4646
  /** Locale-dependent fast path for the official picker cells (zh/en). */
4584
4647
  const PICKER_CELL_PATTERN = {
4585
4648
  model: /模型|Model/,
@@ -4678,6 +4741,9 @@ window.__ModuleLoader__.load({
4678
4741
  "[class$=\"_composerSeat\"] [class$=\"_frame\"] [class$=\"_card\"]{max-width:none;width:100%}",
4679
4742
  `body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_titleCluster"] [class$="_headerActions"]{position:absolute;left:0;top:0;margin:0;display:flex;align-items:center;gap:6px;flex:none;z-index:2}`,
4680
4743
  "[class$=\"_header\"] [class$=\"_tabs\"] [class*=\"_tab\"]{font-size:12px;white-space:nowrap}",
4744
+ `body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"]{max-width:max(0px,calc(100% - var(${HEADER_RESERVE_VAR},0px)));overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch;padding-bottom:2px;margin-bottom:-2px}`,
4745
+ `body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"]::-webkit-scrollbar{display:none}`,
4746
+ `body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"] > [class*="_tab"]{flex:0 0 auto}`,
4681
4747
  `body.${ACTIVE_CLASS} [class$=\"_detailsCol\"]{display:none !important}`,
4682
4748
  `body.${ACTIVE_CLASS} [data-dsh-plugin=\"ssh\"],`,
4683
4749
  `body.${ACTIVE_CLASS} [data-dsh-plugin=\"skill-explorer\"],`,
@@ -4746,7 +4812,7 @@ window.__ModuleLoader__.load({
4746
4812
  * rail compaction) while the body class stays.
4747
4813
  */
4748
4814
  function ensureAdaptStyle() {
4749
- if (document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`) !== null) return;
4815
+ if (nodeOf(`style[data-plugin-css="${ADAPT_CSS_ID}"]`) !== null) return;
4750
4816
  const tag = document.createElement("style");
4751
4817
  tag.dataset.plugin = "remote-web-ui";
4752
4818
  tag.dataset.pluginCss = ADAPT_CSS_ID;
@@ -4786,6 +4852,7 @@ window.__ModuleLoader__.load({
4786
4852
  savedViewportContent = null;
4787
4853
  }
4788
4854
  if (whaleEl !== null) whaleEl.style.display = "none";
4855
+ whaleShown = false;
4789
4856
  setWhaleTimer(false);
4790
4857
  if (whaleObserver !== null) {
4791
4858
  whaleObserver.disconnect();
@@ -4848,7 +4915,7 @@ window.__ModuleLoader__.load({
4848
4915
  }
4849
4916
  function syncCompactPicker() {
4850
4917
  if (!active) return;
4851
- const tools = document.querySelector("[class$=\"_composerSeat\"] [class$=\"_tools\"]");
4918
+ const tools = nodeOf("[class$=\"_composerSeat\"] [class$=\"_tools\"]");
4852
4919
  const trigger = tools?.parentElement?.querySelector("[class$=\"_triggerEffort\"]")?.parentElement;
4853
4920
  if (tools === null || trigger === null) {
4854
4921
  removeCompactPicker();
@@ -5002,17 +5069,23 @@ window.__ModuleLoader__.load({
5002
5069
  document.body.appendChild(whale);
5003
5070
  }
5004
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
+ /**
5005
5081
  * Restore the official draggable state this layer overrode while active.
5006
5082
  * Rows are React-owned and may have been re-created meanwhile, so only the
5007
- * 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).
5008
5085
  */
5009
5086
  function restoreRowDrag() {
5010
5087
  if (dragOverridden.size === 0) return;
5011
- for (const [row, original] of dragOverridden) {
5012
- if (!row.isConnected) continue;
5013
- if (original === null) row.removeAttribute("draggable");
5014
- else row.setAttribute("draggable", original);
5015
- }
5088
+ for (const [row, original] of dragOverridden) restoreRowDragState(row, original);
5016
5089
  dragOverridden.clear();
5017
5090
  }
5018
5091
  /** Restore a dragged position when the whale becomes visible again. */
@@ -5038,6 +5111,34 @@ window.__ModuleLoader__.load({
5038
5111
  if (!dragOverridden.has(row)) dragOverridden.set(row, row.getAttribute("draggable"));
5039
5112
  row.setAttribute("draggable", "false");
5040
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;
5041
5142
  }
5042
5143
  function syncWhale() {
5043
5144
  if (active) ensureAdaptStyle();
@@ -5046,8 +5147,8 @@ window.__ModuleLoader__.load({
5046
5147
  whaleEl.style.display = "none";
5047
5148
  return;
5048
5149
  }
5049
- const collapsed = appFrame()?.hasAttribute("data-sidebar-collapsed") === true;
5050
- const overlayUp = document.querySelector("[class$=\"_overlay\"]") !== null;
5150
+ const collapsed = frameEl()?.hasAttribute("data-sidebar-collapsed") === true;
5151
+ const overlayUp = nodeOf("[class$=\"_overlay\"]") !== null;
5051
5152
  const show = collapsed && !overlayUp;
5052
5153
  if (show && !whaleShown) applyWhalePos();
5053
5154
  whaleEl.style.display = show ? "" : "none";
@@ -5063,7 +5164,7 @@ window.__ModuleLoader__.load({
5063
5164
  }
5064
5165
  function seatHeaderActions() {
5065
5166
  if (!active) return;
5066
- const header = document.querySelector("[class$=\"_header\"]");
5167
+ const header = nodeOf("[class$=\"_header\"]");
5067
5168
  const tabs = header !== null ? header.querySelector("[class$=\"_tabs\"]") : null;
5068
5169
  const actions = header !== null ? header.querySelector("[class$=\"_titleCluster\"] [class$=\"_headerActions\"]") : null;
5069
5170
  const seated = header !== null && tabs !== null && actions !== null;
@@ -5072,7 +5173,7 @@ window.__ModuleLoader__.load({
5072
5173
  }
5073
5174
  function alignActionsText() {
5074
5175
  if (!active) return;
5075
- const header = document.querySelector("[class$=\"_header\"]");
5176
+ const header = nodeOf("[class$=\"_header\"]");
5076
5177
  if (header === null) return;
5077
5178
  const tabs = header.querySelector("[class$=\"_tabs\"]");
5078
5179
  const actions = header.querySelector("[class$=\"_titleCluster\"] [class$=\"_headerActions\"]");
@@ -5096,7 +5197,9 @@ window.__ModuleLoader__.load({
5096
5197
  let dx = translate !== null ? parseFloat(translate[1] ?? "0") : 0;
5097
5198
  let dy = translate !== null ? parseFloat(translate[2] ?? "0") : 0;
5098
5199
  const actionsRect = actions.getBoundingClientRect();
5099
- const rightDiff = tabs.getBoundingClientRect().right - actionsRect.right;
5200
+ const headerRect = header.getBoundingClientRect();
5201
+ const headerStyle = getComputedStyle(header);
5202
+ const rightDiff = headerRect.right - (parseFloat(headerStyle.paddingRight) || 0) - (parseFloat(headerStyle.borderRightWidth) || 0) - actionsRect.right;
5100
5203
  if (Math.abs(rightDiff) >= .5) dx = Math.round((dx + rightDiff) * 10) / 10;
5101
5204
  const tabBottom = textBottom(tabBtn);
5102
5205
  if (tabBottom !== null) {
@@ -5113,10 +5216,12 @@ window.__ModuleLoader__.load({
5113
5216
  const next = `translate(${dx}px, ${dy}px)`;
5114
5217
  if (actions.style.transform !== next) actions.style.transform = next;
5115
5218
  const reserve = `${Math.ceil(actionsRect.width) + 8}px`;
5116
- if (tabs.style.paddingRight !== reserve) tabs.style.paddingRight = reserve;
5219
+ const rootStyle = document.documentElement.style;
5220
+ if (rootStyle.getPropertyValue(HEADER_RESERVE_VAR) !== reserve) rootStyle.setProperty(HEADER_RESERVE_VAR, reserve);
5117
5221
  }
5118
5222
  function unseatHeaderActions() {
5119
5223
  document.body.classList.remove(HEADER_SEATED_CLASS);
5224
+ document.documentElement.style.removeProperty(HEADER_RESERVE_VAR);
5120
5225
  const header = document.querySelector("[class$=\"_header\"]");
5121
5226
  const tabs = header !== null ? header.querySelector("[class$=\"_tabs\"]") : null;
5122
5227
  const actions = header !== null ? header.querySelector("[class$=\"_titleCluster\"] [class$=\"_headerActions\"]") : null;
@@ -5175,6 +5280,7 @@ window.__ModuleLoader__.load({
5175
5280
  if (isMobilePortrait()) {
5176
5281
  apply();
5177
5282
  ensureWhaleObserver();
5283
+ if (active && drag === null) applyWhalePos();
5178
5284
  } else revert();
5179
5285
  }
5180
5286
  evaluate();
@@ -5242,7 +5348,7 @@ window.__ModuleLoader__.load({
5242
5348
  }
5243
5349
  const t = e.target;
5244
5350
  const editable = t instanceof HTMLElement && t.isContentEditable;
5245
- 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)) {
5246
5352
  swipeTouch = null;
5247
5353
  return;
5248
5354
  }
@@ -5251,7 +5357,8 @@ window.__ModuleLoader__.load({
5251
5357
  swipeTouch = {
5252
5358
  x: ct.clientX,
5253
5359
  y: ct.clientY,
5254
- id: ct.identifier
5360
+ id: ct.identifier,
5361
+ el: t instanceof Element ? t : null
5255
5362
  };
5256
5363
  }, {
5257
5364
  capture: true,
@@ -5264,12 +5371,14 @@ window.__ModuleLoader__.load({
5264
5371
  if (ct === void 0 || ct.identifier !== swipeTouch.id) return;
5265
5372
  const dx = ct.clientX - swipeTouch.x;
5266
5373
  const dy = ct.clientY - swipeTouch.y;
5374
+ const start = swipeTouch;
5267
5375
  swipeTouch = null;
5268
5376
  if (!active) return;
5269
5377
  if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
5270
- const frame = appFrame();
5378
+ if (start.el !== null && insideHScrollable(start.el)) return;
5379
+ const frame = frameEl();
5271
5380
  if (frame === null) return;
5272
- 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;
5273
5382
  const collapsed = frame.hasAttribute("data-sidebar-collapsed");
5274
5383
  if (dx < 0 && !collapsed) collapseSidebar();
5275
5384
  else if (dx > 0 && collapsed) toggleSidebarVerified();
@@ -5401,10 +5510,23 @@ window.__ModuleLoader__.load({
5401
5510
  return isComposerField(el) ? el : null;
5402
5511
  };
5403
5512
  const lanOrigFocus = HTMLElement.prototype.focus;
5404
- HTMLElement.prototype.focus = function(options) {
5513
+ const patchedFocus = function(options) {
5405
5514
  if (active && isComposerField(this) && Date.now() - lastComposerTap >= 800) return;
5406
5515
  lanOrigFocus.call(this, options);
5407
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();
5408
5530
  document.addEventListener("pointerdown", (e) => {
5409
5531
  if (!active) return;
5410
5532
  if (composerFieldOf(e.target) !== null) lastComposerTap = Date.now();
@@ -5416,8 +5538,13 @@ window.__ModuleLoader__.load({
5416
5538
  translate: null,
5417
5539
  setEnabled(on) {
5418
5540
  adaptEnabled = on;
5419
- if (on) evaluate();
5420
- else revert();
5541
+ if (on) {
5542
+ installFocusPatch();
5543
+ evaluate();
5544
+ } else {
5545
+ revert();
5546
+ restoreFocusPatch();
5547
+ }
5421
5548
  },
5422
5549
  flushCloseDetails() {
5423
5550
  if (active) w.__dshRemoteAdapt?.closeDetails?.();
@@ -5425,6 +5552,131 @@ window.__ModuleLoader__.load({
5425
5552
  };
5426
5553
  }
5427
5554
  //#endregion
5555
+ //#region src/client/plugin-card-seat.ts
5556
+ /**
5557
+ * Family plugin-card seat.
5558
+ *
5559
+ * A family plugin contributes its settings card to whichever plugin-card seat
5560
+ * the running host actually renders:
5561
+ *
5562
+ * - `web-ui.plugin.item` — the list seat declared by the dsh-web-settings
5563
+ * group section (this family's own first-level "Web UI plugins" section);
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.
5569
+ *
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.
5577
+ *
5578
+ * The signal that actually distinguishes the two deployments is whether
5579
+ * dsh-web-settings is loaded: it is the package that owns the group section and
5580
+ * it publishes the `webUiSettings` service during `apply()`, which every
5581
+ * family plugin already reads for its settings form. Group loaded -> the family
5582
+ * seat; group absent -> the official seat.
5583
+ *
5584
+ * The decision is re-evaluated on every `slots/changed` because the group may
5585
+ * apply after this plugin (the family aggregate orders it first, a profile that
5586
+ * installs the group separately need not): the initial contribution goes to the
5587
+ * official seat, then moves to the family seat the moment the group's section
5588
+ * registers. The entry is disposed before the replacement is registered, so a
5589
+ * card is never in two seats at once.
5590
+ *
5591
+ * The shared tree has no client-SDK dependency, so this module reads its
5592
+ * context through the structural shape below; callers pass the plugin's own
5593
+ * `ctx`.
5594
+ */
5595
+ /** The family list seat key. */
5596
+ const FAMILY_PLUGIN_CARD_SEAT = "web-ui.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";
5599
+ /** The service dsh-web-settings publishes while it is loaded. */
5600
+ const FAMILY_GROUP_SERVICE = "webUiSettings";
5601
+ /**
5602
+ * Whether the family group (dsh-web-settings) is loaded in this page. The
5603
+ * service is the group package's own contract, so the probe cannot be fooled
5604
+ * by a harness release that starts declaring the official seat differently.
5605
+ */
5606
+ function familyGroupLoaded(ctx) {
5607
+ const get = ctx.get;
5608
+ if (typeof get !== "function") return false;
5609
+ try {
5610
+ return get.call(ctx, FAMILY_GROUP_SERVICE) !== void 0;
5611
+ } catch {
5612
+ return false;
5613
+ }
5614
+ }
5615
+ /** Report a refused registration instead of leaving the user with no card. */
5616
+ function warnRefusedSeat(seat, error) {
5617
+ try {
5618
+ console.warn(`[dsh-web] plugin card registration into "${seat}" was refused; the card will not render`, error);
5619
+ } catch {}
5620
+ }
5621
+ /**
5622
+ * Contribute one family plugin card to the seat this host renders, following
5623
+ * the group if it loads later. The entry is disposed and re-registered on a
5624
+ * seat change, never duplicated.
5625
+ * @param ctx - client context (its slot registry decides the seat).
5626
+ * @param seat - the card contribution.
5627
+ */
5628
+ function installPluginCard(ctx, seat) {
5629
+ const slots = ctx.slots;
5630
+ const component = seat.component;
5631
+ const inject = seat.inject;
5632
+ let dispose;
5633
+ let current;
5634
+ /**
5635
+ * Re-entrancy latch. The registry emits a change event synchronously from
5636
+ * inside both `register` and the previous entry's disposer, so an unguarded
5637
+ * reconcile would re-enter itself mid-move and register the card twice into
5638
+ * the seat it is leaving ("already has an entry for key ...").
5639
+ */
5640
+ let reconciling = false;
5641
+ /** Reconcile the contribution with the currently live seat (no-op when unchanged). */
5642
+ const reconcile = () => {
5643
+ if (reconciling) return;
5644
+ const target = familyGroupLoaded(ctx) ? FAMILY_PLUGIN_CARD_SEAT : OFFICIAL_PLUGIN_CARD_SEAT;
5645
+ if (current === target) return;
5646
+ reconciling = true;
5647
+ const previous = dispose;
5648
+ dispose = void 0;
5649
+ current = void 0;
5650
+ previous?.();
5651
+ try {
5652
+ dispose = slots.register(target === "web-ui.plugin.item" ? {
5653
+ name: FAMILY_PLUGIN_CARD_SEAT,
5654
+ id: seat.id,
5655
+ ...seat.order === void 0 ? {} : { order: seat.order },
5656
+ ...seat.label === void 0 ? {} : { label: seat.label },
5657
+ locale: seat.locale,
5658
+ ...seat.inject === void 0 ? {} : { inject }
5659
+ } : {
5660
+ name: OFFICIAL_PLUGIN_CARD_SEAT,
5661
+ key: seat.bundle,
5662
+ locale: seat.locale,
5663
+ ...seat.inject === void 0 ? {} : { inject }
5664
+ }, component);
5665
+ current = target;
5666
+ } catch (error) {
5667
+ warnRefusedSeat(target, error);
5668
+ } finally {
5669
+ reconciling = false;
5670
+ }
5671
+ };
5672
+ if (typeof ctx.on === "function") try {
5673
+ ctx.on("slots/changed", () => {
5674
+ reconcile();
5675
+ });
5676
+ } catch {}
5677
+ reconcile();
5678
+ }
5679
+ //#endregion
5428
5680
  //#region src/client/index.ts
5429
5681
  /**
5430
5682
  * Remote control — browser half. Registers the `remote` dictionaries, the
@@ -5439,6 +5691,21 @@ window.__ModuleLoader__.load({
5439
5691
  const NS = "remote";
5440
5692
  /** Settings namespace the remote-control card edits (the Host plugin registers it). */
5441
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
+ }
5442
5709
  /** Heartbeat cadence from a paired phone (presence + revocation liveness). */
5443
5710
  const HEARTBEAT_INTERVAL_MS = 1e4;
5444
5711
  /** Services required by this plugin. */
@@ -5446,7 +5713,7 @@ window.__ModuleLoader__.load({
5446
5713
  "slots",
5447
5714
  "locale",
5448
5715
  "connection",
5449
- "settingsScope",
5716
+ "configForms",
5450
5717
  "remote"
5451
5718
  ];
5452
5719
  /**
@@ -5489,15 +5756,16 @@ window.__ModuleLoader__.load({
5489
5756
  };
5490
5757
  const t = ctx.locale.bind(NS);
5491
5758
  if (adapt !== void 0) adapt.translate = t;
5492
- 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));
5493
5761
  const enabled = () => {
5494
- const snapshot = settingsScope.getSnapshot();
5762
+ const snapshot = settingsForm.getSnapshot();
5495
5763
  return snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable";
5496
5764
  };
5497
5765
  const syncAdaptEnabled = () => {
5498
5766
  window.__dshRemoteAdapt?.setEnabled?.(enabled());
5499
5767
  };
5500
- settingsScope.subscribe(syncAdaptEnabled);
5768
+ settingsForm.subscribe(syncAdaptEnabled);
5501
5769
  syncAdaptEnabled();
5502
5770
  try {
5503
5771
  adapt?.flushCloseDetails?.();
@@ -5517,31 +5785,25 @@ window.__ModuleLoader__.load({
5517
5785
  disposeEntry = void 0;
5518
5786
  }
5519
5787
  };
5520
- const unsubscribe = settingsScope.subscribe(syncEntry);
5788
+ const unsubscribe = settingsForm.subscribe(syncEntry);
5521
5789
  syncEntry();
5522
5790
  return () => {
5523
5791
  unsubscribe();
5524
5792
  disposeEntry?.();
5525
5793
  };
5526
5794
  });
5527
- const remoteSettings = new RemoteSettingsCardController(settingsScope);
5528
- ctx.slots.inject("web-ui.plugin.item", () => {
5529
- try {
5530
- const unregister = ctx.slots.register({
5531
- name: "web-ui.plugin.item",
5532
- id: "remote-web-ui",
5533
- order: 90,
5534
- locale: NS,
5535
- inject: () => remoteSettings.inject()
5536
- }, RemoteSettingsCard);
5537
- return () => {
5538
- remoteSettings.dispose();
5539
- unregister();
5540
- };
5541
- } catch {
5542
- return () => {};
5543
- }
5795
+ const remoteSettings = new RemoteSettingsCardController(settingsForm);
5796
+ installPluginCard(ctx, {
5797
+ bundle: "@linxin666/dsh-remote-web-ui",
5798
+ id: "remote-web-ui",
5799
+ order: 90,
5800
+ locale: NS,
5801
+ inject: () => remoteSettings.inject(),
5802
+ component: RemoteSettingsCard
5544
5803
  });
5804
+ ctx.effect(() => () => {
5805
+ remoteSettings.dispose();
5806
+ }, "remote-web-ui: settings card");
5545
5807
  let disposeRuntime;
5546
5808
  const syncRuntime = () => {
5547
5809
  if (enabled() && disposeRuntime === void 0) disposeRuntime = ctx.effect(() => {
@@ -5549,7 +5811,9 @@ window.__ModuleLoader__.load({
5549
5811
  runPairBootFlow(ctx, window.location.search);
5550
5812
  if (loopback) return () => {};
5551
5813
  const timer = window.setInterval(() => {
5552
- sendHeartbeat().catch(() => {});
5814
+ sendHeartbeat().then((status) => {
5815
+ if (shouldStopHeartbeat(status)) window.clearInterval(timer);
5816
+ }).catch(() => {});
5553
5817
  }, HEARTBEAT_INTERVAL_MS);
5554
5818
  return () => {
5555
5819
  window.clearInterval(timer);
@@ -5560,7 +5824,7 @@ window.__ModuleLoader__.load({
5560
5824
  disposeRuntime = void 0;
5561
5825
  }
5562
5826
  };
5563
- settingsScope.subscribe(syncRuntime);
5827
+ settingsForm.subscribe(syncRuntime);
5564
5828
  syncRuntime();
5565
5829
  let disposeChannel;
5566
5830
  let hostPairingPolicy;
@@ -5590,13 +5854,13 @@ window.__ModuleLoader__.load({
5590
5854
  fenceNotice = void 0;
5591
5855
  };
5592
5856
  const handleUnpaired = () => {
5593
- if (settingsScope.getSnapshot().status !== "ready" && hostPairingPolicy === void 0) {
5857
+ if (settingsForm.getSnapshot().status !== "ready" && hostPairingPolicy === void 0) {
5594
5858
  unpairedWhilePolicyPending = true;
5595
5859
  return;
5596
5860
  }
5597
5861
  showFenceNotice();
5598
5862
  };
5599
- const channelActive = () => remoteChannelRequired(window.location.hostname, settingsScope.getSnapshot(), hostPairingPolicy);
5863
+ const channelActive = () => remoteChannelRequired(window.location.hostname, settingsForm.getSnapshot(), hostPairingPolicy);
5600
5864
  const bootSeat = () => window[REMOTE_CHANNEL_BOOT_GLOBAL];
5601
5865
  const syncChannel = () => {
5602
5866
  const transition = channelTransition(channelActive(), disposeChannel !== void 0);
@@ -5626,9 +5890,9 @@ window.__ModuleLoader__.load({
5626
5890
  hideFenceNotice();
5627
5891
  } else if (transition === "none" && !channelActive()) bootSeat()?.restore();
5628
5892
  };
5629
- settingsScope.subscribe(syncChannel);
5893
+ settingsForm.subscribe(syncChannel);
5630
5894
  syncChannel();
5631
- 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) => {
5632
5896
  hostPairingPolicy = policy.requirePairingForLan;
5633
5897
  syncChannel();
5634
5898
  if (hostPairingPolicy && unpairedWhilePolicyPending) showFenceNotice();