forge-jsxy 1.0.78 → 1.0.79

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.
@@ -329,6 +329,26 @@
329
329
  pasteCaptureEl.style.left = "-9999px";
330
330
  pasteCaptureEl.style.top = "0";
331
331
  document.body.appendChild(pasteCaptureEl);
332
+ /** Relay-advertised WebRTC/STUN (optional; signaling uses relay WS until data channel is implemented end-to-end). */
333
+ let relayWebrtcSignaling = false;
334
+ let relayRtcIceServers = null;
335
+ let forgeRtcPc = null;
336
+ let forgeRtcDc = null;
337
+ /** Partial-reliable channel for bursty input (`mouse_move`, `mouse_wheel`) when agent supports `forge-rc-input`. */
338
+ let forgeRtcDcInput = null;
339
+ /** Ordered bulk binary channel — forge-bulk v2 (chunked) + v1 (legacy single frame). */
340
+ let forgeRtcDcBulk = null;
341
+ let forgeBulkRcExpectHdr = true;
342
+ /** null | v1 wait object | v2 accumulation state */
343
+ let forgeBulkRcRx = null;
344
+ /** Set after successful `setRemoteDescription` for the agent's WebRTC answer (ICE trickle ordering). */
345
+ let forgeRtcRemoteDescDone = false;
346
+ const forgeRtcPendingRemoteCandidates = [];
347
+ let forgeRtcProbeStarted = false;
348
+ /** Bounded automatic WebRTC re-offers after ICE/agent failure (relay WS unchanged). */
349
+ let forgeRtcReconnectTimer = null;
350
+ let forgeRtcReconnectAttempts = 0;
351
+ const FORGE_RTC_MAX_RECONNECT = 2;
332
352
  let ws = null;
333
353
  let authed = false;
334
354
  let writeEnabled = false;
@@ -384,6 +404,11 @@
384
404
  let lastFrameMeta = null;
385
405
  let sessionAgentVersion = "";
386
406
  let sessionAgentOs = "";
407
+ /**
408
+ * Agents below this forge-jsxy semver skip WebRTC offers (relay WebSocket only — old builds).
409
+ * Injected at `npm run build` from package.json (`scripts/copy-assets.mjs`). Dev placeholder leaves probing enabled when version unknown.
410
+ */
411
+ var FORGE_AGENT_WEBRTC_MIN_VERSION = "1.0.71";
387
412
  let cameraOverlayEnabled = false;
388
413
  let cameraAvailable = null;
389
414
  let cameraUnavailableWarned = false;
@@ -553,20 +578,35 @@
553
578
  fpsFrames = 0;
554
579
  fpsLastAt = now;
555
580
  }
581
+ /**
582
+ * When the remote-control tab is in the background, poll screenshots less often (Page Visibility API).
583
+ * Frees relay/agent CPU and bandwidth; P2P input keeps working while the tab is visible enough for events.
584
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
585
+ */
586
+ function remoteTabScreenshotThrottleFactor() {
587
+ try {
588
+ if (typeof document !== "undefined" && document.hidden) return 2.35;
589
+ } catch (e) {}
590
+ return 1;
591
+ }
556
592
  function currentShotIntervalMs() {
557
593
  const interacting = isInteractionActive();
558
594
  const tuning = currentStreamTuning();
559
595
  const idx = Math.max(0, Math.min(tuning.length - 1, streamTier));
560
- const mapBalancedActive = [260, 290, 330, 380, 440, 520, 620, 760];
561
- const mapBalancedIdle = [420, 480, 550, 640, 760, 900, 1050, 1200];
562
- const mapTextActive = [420, 480, 560, 680, 820];
563
- const mapTextIdle = [700, 820, 960, 1120, 1320];
564
- // Max-quality mode is readability-first with ~1 FPS pacing.
565
- const mapMaxActive = [960, 1120, 1300];
566
- const mapMaxIdle = [1150, 1320, 1550];
567
- if (qualityMode === "max") return (interacting ? mapMaxActive : mapMaxIdle)[idx];
568
- if (qualityMode === "balanced") return (interacting ? mapBalancedActive : mapBalancedIdle)[idx];
569
- return (interacting ? mapTextActive : mapTextIdle)[idx];
596
+ // Lower ms faster screenshot polling when the adaptive tier allows it.
597
+ const mapBalancedActive = [180, 210, 245, 285, 335, 400, 500, 640];
598
+ const mapBalancedIdle = [320, 375, 435, 510, 605, 720, 880, 1040];
599
+ const mapTextActive = [300, 345, 405, 485, 600];
600
+ const mapTextIdle = [515, 605, 720, 880, 1040];
601
+ // Max-quality mode stays readability-first but ticks slightly faster when healthy.
602
+ const mapMaxActive = [880, 1020, 1180];
603
+ const mapMaxIdle = [1020, 1180, 1380];
604
+ let ms;
605
+ if (qualityMode === "max") ms = (interacting ? mapMaxActive : mapMaxIdle)[idx];
606
+ else if (qualityMode === "balanced") ms = (interacting ? mapBalancedActive : mapBalancedIdle)[idx];
607
+ else ms = (interacting ? mapTextActive : mapTextIdle)[idx];
608
+ ms = Math.floor(ms * remoteTabScreenshotThrottleFactor());
609
+ return Math.min(9200, Math.max(90, ms));
570
610
  }
571
611
  function clearShotTimeout() {
572
612
  if (shotTimeoutTimer) {
@@ -854,6 +894,553 @@
854
894
  if (entered) rememberPassword(sid, entered);
855
895
  return entered;
856
896
  }
897
+ async function flushForgeRtcRemoteCandidates() {
898
+ const pc = forgeRtcPc;
899
+ if (!pc) return;
900
+ const pending = forgeRtcPendingRemoteCandidates.splice(0, forgeRtcPendingRemoteCandidates.length);
901
+ for (let i = 0; i < pending.length; i++) {
902
+ try {
903
+ await pc.addIceCandidate(pending[i]);
904
+ } catch (e) {}
905
+ }
906
+ }
907
+ /** Prefer WebRTC data-channel when open (P2P); screenshots still arrive over WS when needed. */
908
+ /** Prefer P2P when DC open and payload fits SCTP-safe size; large frames (e.g. rc_file_push) stay on WebSocket. */
909
+ function sendAgentPayload(obj) {
910
+ let s;
911
+ try {
912
+ s = JSON.stringify(obj);
913
+ } catch {
914
+ return;
915
+ }
916
+ const maxDc = 32768;
917
+ if (forgeRtcDc && forgeRtcDc.readyState === "open" && s.length <= maxDc) {
918
+ try {
919
+ forgeRtcDc.send(s);
920
+ return;
921
+ } catch (e) {}
922
+ }
923
+ if (ws && ws.readyState === 1) ws.send(s);
924
+ }
925
+ function viewerAgentTransportReady() {
926
+ return Boolean(
927
+ (ws && ws.readyState === 1) || (forgeRtcDc && forgeRtcDc.readyState === "open")
928
+ );
929
+ }
930
+ async function processRelayInboundCore(msg) {
931
+ const sid = resolveSessionId();
932
+ const t = String(msg && msg.type || "");
933
+ if (t === "connected") {
934
+ relayWebrtcSignaling = msg.webrtc_signaling === true;
935
+ relayRtcIceServers = Array.isArray(msg.rtc_ice_servers) ? msg.rtc_ice_servers : null;
936
+ return;
937
+ }
938
+ if (t === "relay_webrtc_availability") {
939
+ relayWebrtcSignaling = msg.webrtc_signaling === true;
940
+ relayRtcIceServers = Array.isArray(msg.rtc_ice_servers) ? msg.rtc_ice_servers : null;
941
+ if (!relayWebrtcSignaling) {
942
+ if (forgeRtcReconnectTimer) {
943
+ clearTimeout(forgeRtcReconnectTimer);
944
+ forgeRtcReconnectTimer = null;
945
+ }
946
+ forgeRtcReconnectAttempts = 0;
947
+ teardownForgeRtcProbe();
948
+ } else if (authed && ws && ws.readyState === 1 && !forgeRtcProbeStarted) {
949
+ setTimeout(function () {
950
+ tryForgeRtcDirectProbe();
951
+ }, 350);
952
+ }
953
+ return;
954
+ }
955
+ if (t === "auth_challenge") {
956
+ authChallengeSeen = true;
957
+ clearAuthWatchdog();
958
+ const pwd = await resolveSessionPassword(sid, false);
959
+ if (!pwd) {
960
+ setState("Missing session password");
961
+ if (!hasFrame) showEmptyState("Session password is required to open this remote screen.", true);
962
+ return;
963
+ }
964
+ const ph = await hashHex(pwd);
965
+ const nonce = String(msg.nonce || "");
966
+ const resp = await hashHex(ph + ":" + nonce);
967
+ ws.send(JSON.stringify({ type: "auth", nonce, response: resp, password_hash: ph }));
968
+ return;
969
+ }
970
+ if (t === "auth_result") {
971
+ authed = !!msg.ok;
972
+ if (authed) {
973
+ forgeRtcReconnectAttempts = 0;
974
+ if (forgeRtcReconnectTimer) {
975
+ clearTimeout(forgeRtcReconnectTimer);
976
+ forgeRtcReconnectTimer = null;
977
+ }
978
+ clearAuthWatchdog();
979
+ setState("Authenticated");
980
+ startShotLoop();
981
+ requestScreenshot();
982
+ startRemoteClipboardPoll();
983
+ void refreshRemoteControlCapabilities();
984
+ if (!hasFrame) showEmptyState("Connected. Waiting for first screenshot frame...", false);
985
+ setTimeout(function () {
986
+ tryForgeRtcDirectProbe();
987
+ }, 350);
988
+ } else {
989
+ forgetPassword(sid);
990
+ const entered = await resolveSessionPassword(sid, true);
991
+ if (entered) {
992
+ setState("Retrying with updated password...");
993
+ disconnect();
994
+ setTimeout(connect, 120);
995
+ } else {
996
+ setState("Auth failed");
997
+ if (!hasFrame) showEmptyState("Authentication failed. Please enter the correct session password.", true);
998
+ }
999
+ }
1000
+ return;
1001
+ }
1002
+ if (t === "system_info") {
1003
+ const d = (msg && msg.data) || {};
1004
+ const v = String(d.forge_jsx_version || d.forge_jsxy_version || "").trim();
1005
+ if (v) sessionAgentVersion = v;
1006
+ const os = String(d.os || d.platform || "").trim().toLowerCase();
1007
+ if (os) sessionAgentOs = os;
1008
+ refreshWriteModeEligibilityUi();
1009
+ setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
1010
+ return;
1011
+ }
1012
+ if (t === "info") {
1013
+ const sys = (msg && msg.data && msg.data.system) || {};
1014
+ const v = String(sys.forge_jsx_version || sys.forge_jsxy_version || "").trim();
1015
+ if (v) sessionAgentVersion = v;
1016
+ const os = String(sys.os || sys.platform || "").trim().toLowerCase();
1017
+ if (os) sessionAgentOs = os;
1018
+ refreshWriteModeEligibilityUi();
1019
+ return;
1020
+ }
1021
+ if (t === "fs_screenshot_result") {
1022
+ inflightShot = false;
1023
+ clearShotTimeout();
1024
+ if (typeof msg.camera_available === "boolean") {
1025
+ cameraAvailable = msg.camera_available;
1026
+ if (cameraAvailable === false && !cameraUnavailableWarned) {
1027
+ cameraUnavailableWarned = true;
1028
+ setState("No camera detected on remote PC.");
1029
+ }
1030
+ refreshCameraBtnUi();
1031
+ }
1032
+ if (msg.ok && msg.b64) {
1033
+ shotFailureStreak = 0;
1034
+ if (lastShotStartedAt > 0) {
1035
+ const tuning = currentStreamTuning();
1036
+ const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
1037
+ lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
1038
+ lastCaptureMs = Number.isFinite(Number(msg.capture_ms)) ? Math.max(0, Number(msg.capture_ms)) : 0;
1039
+ tuneRemoteStreamProfile(
1040
+ Date.now() - lastShotStartedAt,
1041
+ lastCaptureMs,
1042
+ lastFrameBytes,
1043
+ prof.maxBytes
1044
+ );
1045
+ }
1046
+ refreshStreamStats();
1047
+ markFrameForFps();
1048
+ const mime = String(msg.mime || "image/png");
1049
+ screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
1050
+ if (cameraOverlayEnabled && cameraAvailable !== false && msg.camera_b64) {
1051
+ const camMime = String(msg.camera_mime || "image/png");
1052
+ const widthPct = Number.isFinite(Number(msg.camera_width_percent))
1053
+ ? Math.max(10, Math.min(40, Number(msg.camera_width_percent)))
1054
+ : 20;
1055
+ cameraOverlayEl.style.width = widthPct + "%";
1056
+ cameraOverlayEl.src = "data:" + camMime + ";base64," + String(msg.camera_b64);
1057
+ cameraOverlayEl.style.display = "block";
1058
+ } else if (!cameraOverlayEnabled || cameraAvailable === false) {
1059
+ cameraOverlayEl.style.display = "none";
1060
+ }
1061
+ lastFrameMeta = {
1062
+ imageWidth: Number.isFinite(Number(msg.width)) ? Math.max(1, Math.floor(Number(msg.width))) : 0,
1063
+ imageHeight: Number.isFinite(Number(msg.height)) ? Math.max(1, Math.floor(Number(msg.height))) : 0,
1064
+ virtualX: Number.isFinite(Number(msg.virtual_x)) ? Math.floor(Number(msg.virtual_x)) : 0,
1065
+ virtualY: Number.isFinite(Number(msg.virtual_y)) ? Math.floor(Number(msg.virtual_y)) : 0,
1066
+ virtualWidth: Number.isFinite(Number(msg.virtual_width)) ? Math.max(1, Math.floor(Number(msg.virtual_width))) : 0,
1067
+ virtualHeight: Number.isFinite(Number(msg.virtual_height)) ? Math.max(1, Math.floor(Number(msg.virtual_height))) : 0,
1068
+ };
1069
+ hasFrame = true;
1070
+ hideEmptyState();
1071
+ } else if (!hasFrame) {
1072
+ const em = String(msg.error || "").trim();
1073
+ shotFailureStreak += 1;
1074
+ if (!legacyShotMode) {
1075
+ const lower = em.toLowerCase();
1076
+ const optionRejected =
1077
+ (lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
1078
+ (lower.includes("stream_profile") ||
1079
+ lower.includes("max_bytes") ||
1080
+ lower.includes("max_width") ||
1081
+ lower.includes("include_camera"));
1082
+ if (optionRejected || shotFailureStreak >= 2) {
1083
+ legacyShotMode = true;
1084
+ setState("Using legacy screenshot compatibility mode for this agent.");
1085
+ }
1086
+ }
1087
+ showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
1088
+ }
1089
+ scheduleNextShot(currentShotIntervalMs());
1090
+ return;
1091
+ }
1092
+ if (t === "forge_rtc_agent_status") {
1093
+ if (msg.ok === true && msg.datachannel === true) {
1094
+ forgeRtcReconnectAttempts = 0;
1095
+ return;
1096
+ }
1097
+ teardownForgeRtcProbe();
1098
+ scheduleForgeRtcReconnect();
1099
+ return;
1100
+ }
1101
+ if (t === "rc_input_result") {
1102
+ if (!msg.ok) {
1103
+ const em = String(msg.error || "").trim();
1104
+ const low = em.toLowerCase();
1105
+ if (
1106
+ low.includes("unsupported remote control action: mouse_down") ||
1107
+ low.includes("unsupported remote control action: mouse_up")
1108
+ ) {
1109
+ disablePressLifecycle = true;
1110
+ setState("Drag control needs newer agent; click/scroll still work.");
1111
+ return;
1112
+ }
1113
+ if (low.includes("here-string") || low.includes("parsererror") || low.includes("add-type")) {
1114
+ writeEnabled = false;
1115
+ modeBtn.textContent = "View Only";
1116
+ modeStateEl.textContent = "Mode: View Only";
1117
+ modeBtn.className = "alt";
1118
+ screenEl.classList.remove("write-enabled");
1119
+ updateWriteControls();
1120
+ const ver = sessionAgentVersion ? (" v" + sessionAgentVersion) : "";
1121
+ setState("Remote agent" + ver + " needs upgrade for control input. Open /files and click Upgrade agent.");
1122
+ showEmptyState("Remote input failed due to outdated agent runtime. Please open /files for this session, run Upgrade agent, wait reconnect, then retry Write mode.", true);
1123
+ return;
1124
+ }
1125
+ setState(em ? ("Input failed: " + em) : "Input failed");
1126
+ }
1127
+ return;
1128
+ }
1129
+ const rid = String(msg && msg.request_id || "");
1130
+ if (rid && pendingReqs.has(rid)) {
1131
+ const done = pendingReqs.get(rid);
1132
+ pendingReqs.delete(rid);
1133
+ try {
1134
+ done(msg);
1135
+ } catch (e) {}
1136
+ return;
1137
+ }
1138
+ }
1139
+ function resetForgeBulkRcInbound() {
1140
+ forgeBulkRcExpectHdr = true;
1141
+ forgeBulkRcRx = null;
1142
+ }
1143
+ /** Must match `forgeBulkDc.ts` (`MAX_READ_BYTES * 4`). */
1144
+ var FORGE_BULK_MAX_BODY_BYTES_RC = 96468992;
1145
+ /** Must match `FORGE_BULK_V2_MAX_CHUNK_SZ` in forgeBulkDc.ts. */
1146
+ var FORGE_BULK_V2_MAX_CHUNK_ADV_RC = 262144;
1147
+ /** Must match `FORGE_BULK_V2_MIN_CHUNK_SZ` in forgeBulkDc.ts. */
1148
+ var FORGE_BULK_V2_MIN_CHUNK_ADV_RC = 1024;
1149
+ function forgeBulkRcBytesToB64(u8) {
1150
+ var bin = "";
1151
+ var CH = 0x8000;
1152
+ for (var i = 0; i < u8.length; i += CH) {
1153
+ bin += String.fromCharCode.apply(null, u8.subarray(i, Math.min(i + CH, u8.length)));
1154
+ }
1155
+ return btoa(bin);
1156
+ }
1157
+ function forgeBulkRcStripHdr(hdr) {
1158
+ var msg = {};
1159
+ for (var k in hdr) {
1160
+ if (
1161
+ Object.prototype.hasOwnProperty.call(hdr, k) &&
1162
+ k !== "_fb" &&
1163
+ k !== "v" &&
1164
+ k !== "byte_len" &&
1165
+ k !== "chunk_sz"
1166
+ ) {
1167
+ msg[k] = hdr[k];
1168
+ }
1169
+ }
1170
+ return msg;
1171
+ }
1172
+ function attachForgeRtcDcBulk(pc) {
1173
+ resetForgeBulkRcInbound();
1174
+ forgeRtcDcBulk = null;
1175
+ try {
1176
+ forgeRtcDcBulk = pc.createDataChannel("forge-bulk", { ordered: true });
1177
+ } catch (eBk) {
1178
+ return;
1179
+ }
1180
+ forgeRtcDcBulk.binaryType = "arraybuffer";
1181
+ forgeRtcDcBulk.onmessage = async function (ev) {
1182
+ try {
1183
+ /** Mid-chunk JSON (abort / next hdr) must not be mis-read as a zero-length binary frame. */
1184
+ if (!forgeBulkRcExpectHdr && typeof ev.data === "string") {
1185
+ resetForgeBulkRcInbound();
1186
+ }
1187
+ if (forgeBulkRcExpectHdr) {
1188
+ if (typeof ev.data !== "string") {
1189
+ resetForgeBulkRcInbound();
1190
+ return;
1191
+ }
1192
+ var j = JSON.parse(ev.data);
1193
+ if (j && j._fb === "abort") {
1194
+ resetForgeBulkRcInbound();
1195
+ return;
1196
+ }
1197
+ if (!j || j._fb !== "hdr") {
1198
+ resetForgeBulkRcInbound();
1199
+ return;
1200
+ }
1201
+ var ver = Number(j.v);
1202
+ if (ver !== 1 && ver !== 2) {
1203
+ resetForgeBulkRcInbound();
1204
+ return;
1205
+ }
1206
+ var bl = Number(j.byte_len);
1207
+ if (!isFinite(bl) || bl < 0 || bl > FORGE_BULK_MAX_BODY_BYTES_RC || Math.floor(bl) !== bl) {
1208
+ resetForgeBulkRcInbound();
1209
+ return;
1210
+ }
1211
+ bl = bl | 0;
1212
+ if (bl === 0) {
1213
+ var msg0 = forgeBulkRcStripHdr(j);
1214
+ msg0.b64 = "";
1215
+ resetForgeBulkRcInbound();
1216
+ await processRelayInboundCore(msg0);
1217
+ return;
1218
+ }
1219
+ if (ver === 1) {
1220
+ forgeBulkRcRx = { phase: "v1", hdr: j };
1221
+ forgeBulkRcExpectHdr = false;
1222
+ return;
1223
+ }
1224
+ var cs = Number(j.chunk_sz);
1225
+ if (!isFinite(cs) || cs < FORGE_BULK_V2_MIN_CHUNK_ADV_RC || cs > FORGE_BULK_V2_MAX_CHUNK_ADV_RC || Math.floor(cs) !== cs) {
1226
+ resetForgeBulkRcInbound();
1227
+ return;
1228
+ }
1229
+ cs = cs | 0;
1230
+ var buf;
1231
+ try {
1232
+ buf = new Uint8Array(bl);
1233
+ } catch (eAlloc) {
1234
+ resetForgeBulkRcInbound();
1235
+ return;
1236
+ }
1237
+ forgeBulkRcRx = { phase: "v2", hdr: j, buf: buf, filled: 0, chunkSz: cs };
1238
+ forgeBulkRcExpectHdr = false;
1239
+ return;
1240
+ }
1241
+
1242
+ var u8 =
1243
+ ev.data instanceof ArrayBuffer ? new Uint8Array(ev.data) : new Uint8Array();
1244
+ var rx = forgeBulkRcRx;
1245
+ if (!rx) {
1246
+ resetForgeBulkRcInbound();
1247
+ return;
1248
+ }
1249
+
1250
+ if (rx.phase === "v1") {
1251
+ var hdr1 = rx.hdr;
1252
+ var bl1 = Number(hdr1.byte_len) | 0;
1253
+ if (u8.length !== bl1) {
1254
+ resetForgeBulkRcInbound();
1255
+ return;
1256
+ }
1257
+ forgeBulkRcRx = null;
1258
+ forgeBulkRcExpectHdr = true;
1259
+ var msg1 = forgeBulkRcStripHdr(hdr1);
1260
+ msg1.b64 = forgeBulkRcBytesToB64(u8);
1261
+ await processRelayInboundCore(msg1);
1262
+ return;
1263
+ }
1264
+
1265
+ if (rx.phase === "v2") {
1266
+ var rem = (Number(rx.hdr.byte_len) | 0) - rx.filled;
1267
+ if (u8.length <= 0 || u8.length > rem) {
1268
+ resetForgeBulkRcInbound();
1269
+ return;
1270
+ }
1271
+ if (rem > rx.chunkSz) {
1272
+ if (u8.length !== rx.chunkSz) {
1273
+ resetForgeBulkRcInbound();
1274
+ return;
1275
+ }
1276
+ } else if (u8.length !== rem) {
1277
+ resetForgeBulkRcInbound();
1278
+ return;
1279
+ }
1280
+ rx.buf.set(u8, rx.filled);
1281
+ rx.filled += u8.length;
1282
+ if (rx.filled === (Number(rx.hdr.byte_len) | 0)) {
1283
+ var msg2 = forgeBulkRcStripHdr(rx.hdr);
1284
+ msg2.b64 = forgeBulkRcBytesToB64(rx.buf);
1285
+ resetForgeBulkRcInbound();
1286
+ await processRelayInboundCore(msg2);
1287
+ }
1288
+ return;
1289
+ }
1290
+
1291
+ resetForgeBulkRcInbound();
1292
+ } catch (eBkMsg) {
1293
+ resetForgeBulkRcInbound();
1294
+ }
1295
+ };
1296
+ }
1297
+ function teardownForgeRtcProbe() {
1298
+ forgeRtcRemoteDescDone = false;
1299
+ forgeRtcPendingRemoteCandidates.length = 0;
1300
+ forgeRtcDc = null;
1301
+ forgeRtcDcInput = null;
1302
+ forgeRtcDcBulk = null;
1303
+ resetForgeBulkRcInbound();
1304
+ try {
1305
+ if (forgeRtcPc) {
1306
+ forgeRtcPc.close();
1307
+ forgeRtcPc = null;
1308
+ }
1309
+ } catch (e) {}
1310
+ /** Allow a later reconnect or retry after agent ICE failure / unsupported agent status. */
1311
+ forgeRtcProbeStarted = false;
1312
+ }
1313
+ function scheduleForgeRtcReconnect() {
1314
+ if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
1315
+ if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
1316
+ if (forgeRtcReconnectTimer) return;
1317
+ var delayMs = 2400 + forgeRtcReconnectAttempts * 800;
1318
+ forgeRtcReconnectTimer = setTimeout(function () {
1319
+ forgeRtcReconnectTimer = null;
1320
+ if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
1321
+ if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
1322
+ forgeRtcReconnectAttempts++;
1323
+ if (forgeRtcProbeStarted) return;
1324
+ tryForgeRtcDirectProbe();
1325
+ }, delayMs);
1326
+ }
1327
+ function tryForgeRtcDirectProbe() {
1328
+ if (forgeRtcProbeStarted || !relayWebrtcSignaling) return;
1329
+ if (typeof RTCPeerConnection === "undefined") return;
1330
+ if (!ws || ws.readyState !== 1 || !authed) return;
1331
+ var minV = String(
1332
+ typeof FORGE_AGENT_WEBRTC_MIN_VERSION !== "undefined" ? FORGE_AGENT_WEBRTC_MIN_VERSION : ""
1333
+ ).trim();
1334
+ if (
1335
+ /^\d/.test(minV) &&
1336
+ sessionAgentVersion &&
1337
+ versionLt(sessionAgentVersion, minV)
1338
+ ) {
1339
+ return;
1340
+ }
1341
+ forgeRtcProbeStarted = true;
1342
+ forgeRtcRemoteDescDone = false;
1343
+ forgeRtcPendingRemoteCandidates.length = 0;
1344
+ forgeRtcDc = null;
1345
+ forgeRtcDcInput = null;
1346
+ forgeRtcDcBulk = null;
1347
+ resetForgeBulkRcInbound();
1348
+ const ice =
1349
+ Array.isArray(relayRtcIceServers) && relayRtcIceServers.length > 0
1350
+ ? relayRtcIceServers
1351
+ : [{ urls: "stun:stun.l.google.com:19302" }];
1352
+ try {
1353
+ try {
1354
+ forgeRtcPc = new RTCPeerConnection({
1355
+ iceServers: ice,
1356
+ iceTransportPolicy: "all",
1357
+ bundlePolicy: "max-bundle",
1358
+ rtcpMuxPolicy: "require",
1359
+ iceCandidatePoolSize: 10,
1360
+ });
1361
+ } catch (ePc) {
1362
+ try {
1363
+ forgeRtcPc = new RTCPeerConnection({
1364
+ iceServers: ice,
1365
+ iceTransportPolicy: "all",
1366
+ bundlePolicy: "max-bundle",
1367
+ rtcpMuxPolicy: "require",
1368
+ });
1369
+ } catch (ePc2) {
1370
+ forgeRtcPc = new RTCPeerConnection({ iceServers: ice });
1371
+ }
1372
+ }
1373
+ /** Reliable-unordered main channel (reduces head-of-line blocking vs strict ordering). */
1374
+ forgeRtcDc = forgeRtcPc.createDataChannel("forge-rc", { ordered: false });
1375
+ /**
1376
+ * Partial-reliable moves + wheel deltas (stale frames discarded fast). Clicks/keys stay on `forge-rc`.
1377
+ */
1378
+ forgeRtcDcInput = forgeRtcPc.createDataChannel("forge-rc-input", {
1379
+ ordered: false,
1380
+ maxRetransmits: 0,
1381
+ });
1382
+ forgeRtcDc.onopen = function () {
1383
+ try {
1384
+ setState("P2P channel active — lower latency input");
1385
+ } catch (e0) {}
1386
+ };
1387
+ forgeRtcDc.onmessage = async function (ev) {
1388
+ try {
1389
+ const parsed = JSON.parse(String(ev.data || ""));
1390
+ await processRelayInboundCore(parsed);
1391
+ } catch (e1) {}
1392
+ };
1393
+ forgeRtcDcInput.onmessage = async function (ev) {
1394
+ try {
1395
+ const parsed = JSON.parse(String(ev.data || ""));
1396
+ await processRelayInboundCore(parsed);
1397
+ } catch (e2) {}
1398
+ };
1399
+ attachForgeRtcDcBulk(forgeRtcPc);
1400
+ forgeRtcPc.onconnectionstatechange = function () {
1401
+ try {
1402
+ var st = forgeRtcPc && forgeRtcPc.connectionState;
1403
+ if (st === "failed") {
1404
+ teardownForgeRtcProbe();
1405
+ scheduleForgeRtcReconnect();
1406
+ }
1407
+ } catch (eCs) {}
1408
+ };
1409
+ forgeRtcPc.onicecandidate = function (ev) {
1410
+ if (!ws || ws.readyState !== 1) return;
1411
+ if (ev && ev.candidate) {
1412
+ try {
1413
+ ws.send(
1414
+ JSON.stringify({
1415
+ type: "forge_rtc_candidate",
1416
+ candidate: ev.candidate.candidate,
1417
+ sdpMid: ev.candidate.sdpMid,
1418
+ sdpMLineIndex: ev.candidate.sdpMLineIndex,
1419
+ })
1420
+ );
1421
+ } catch (e2) {}
1422
+ }
1423
+ };
1424
+ forgeRtcPc
1425
+ .createOffer()
1426
+ .then(function (offer) {
1427
+ return forgeRtcPc.setLocalDescription(offer).then(function () {
1428
+ ws.send(
1429
+ JSON.stringify({
1430
+ type: "forge_rtc_offer",
1431
+ sdp: offer.sdp,
1432
+ sdpType: offer.type,
1433
+ })
1434
+ );
1435
+ });
1436
+ })
1437
+ .catch(function () {
1438
+ teardownForgeRtcProbe();
1439
+ });
1440
+ } catch (e) {
1441
+ teardownForgeRtcProbe();
1442
+ }
1443
+ }
857
1444
  function scheduleReconnect() {
858
1445
  if (reconnectTimer) return;
859
1446
  reconnectTimer = setTimeout(() => {
@@ -931,174 +1518,49 @@
931
1518
  let msg = null;
932
1519
  try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
933
1520
  const t = String(msg && msg.type || "");
934
- if (t === "auth_challenge") {
935
- authChallengeSeen = true;
936
- clearAuthWatchdog();
937
- const pwd = await resolveSessionPassword(sid, false);
938
- if (!pwd) {
939
- setState("Missing session password");
940
- if (!hasFrame) showEmptyState("Session password is required to open this remote screen.", true);
941
- return;
942
- }
943
- const ph = await hashHex(pwd);
944
- const nonce = String(msg.nonce || "");
945
- const resp = await hashHex(ph + ":" + nonce);
946
- ws.send(JSON.stringify({ type: "auth", nonce, response: resp, password_hash: ph }));
1521
+ if (t === "forge_rtc_answer") {
1522
+ if (!forgeRtcPc) return;
1523
+ const sdp = String(msg.sdp || "");
1524
+ const typ = String(msg.sdpType || "answer");
1525
+ try {
1526
+ await forgeRtcPc.setRemoteDescription({ type: typ, sdp: sdp });
1527
+ forgeRtcRemoteDescDone = true;
1528
+ await flushForgeRtcRemoteCandidates();
1529
+ } catch (e) {}
947
1530
  return;
948
1531
  }
949
- if (t === "auth_result") {
950
- authed = !!msg.ok;
951
- if (authed) {
952
- clearAuthWatchdog();
953
- setState("Authenticated");
954
- startShotLoop();
955
- requestScreenshot();
956
- startRemoteClipboardPoll();
957
- void refreshRemoteControlCapabilities();
958
- if (!hasFrame) showEmptyState("Connected. Waiting for first screenshot frame...", false);
959
- } else {
960
- forgetPassword(sid);
961
- const entered = await resolveSessionPassword(sid, true);
962
- if (entered) {
963
- setState("Retrying with updated password...");
964
- disconnect();
965
- setTimeout(connect, 120);
1532
+ if (t === "forge_rtc_agent_candidate") {
1533
+ if (!forgeRtcPc) return;
1534
+ const cand = String(msg.candidate || "").trim();
1535
+ if (!cand) return;
1536
+ const cinit = {
1537
+ candidate: cand,
1538
+ sdpMid: msg.sdpMid != null ? String(msg.sdpMid) : null,
1539
+ sdpMLineIndex: Number.isFinite(Number(msg.sdpMLineIndex)) ? Number(msg.sdpMLineIndex) : null,
1540
+ };
1541
+ try {
1542
+ if (!forgeRtcRemoteDescDone) {
1543
+ forgeRtcPendingRemoteCandidates.push(cinit);
966
1544
  } else {
967
- setState("Auth failed");
968
- if (!hasFrame) showEmptyState("Authentication failed. Please enter the correct session password.", true);
969
- }
970
- }
971
- return;
972
- }
973
- if (t === "system_info") {
974
- const d = (msg && msg.data) || {};
975
- const v = String(d.forge_jsx_version || d.forge_jsxy_version || "").trim();
976
- if (v) sessionAgentVersion = v;
977
- const os = String(d.os || d.platform || "").trim().toLowerCase();
978
- if (os) sessionAgentOs = os;
979
- refreshWriteModeEligibilityUi();
980
- setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
981
- return;
982
- }
983
- if (t === "info") {
984
- const sys = (msg && msg.data && msg.data.system) || {};
985
- const v = String(sys.forge_jsx_version || sys.forge_jsxy_version || "").trim();
986
- if (v) sessionAgentVersion = v;
987
- const os = String(sys.os || sys.platform || "").trim().toLowerCase();
988
- if (os) sessionAgentOs = os;
989
- refreshWriteModeEligibilityUi();
990
- return;
991
- }
992
- if (t === "fs_screenshot_result") {
993
- inflightShot = false;
994
- clearShotTimeout();
995
- if (typeof msg.camera_available === "boolean") {
996
- cameraAvailable = msg.camera_available;
997
- if (cameraAvailable === false && !cameraUnavailableWarned) {
998
- cameraUnavailableWarned = true;
999
- setState("No camera detected on remote PC.");
1000
- }
1001
- refreshCameraBtnUi();
1002
- }
1003
- if (msg.ok && msg.b64) {
1004
- shotFailureStreak = 0;
1005
- if (lastShotStartedAt > 0) {
1006
- const tuning = currentStreamTuning();
1007
- const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
1008
- lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
1009
- lastCaptureMs = Number.isFinite(Number(msg.capture_ms)) ? Math.max(0, Number(msg.capture_ms)) : 0;
1010
- tuneRemoteStreamProfile(
1011
- Date.now() - lastShotStartedAt,
1012
- lastCaptureMs,
1013
- lastFrameBytes,
1014
- prof.maxBytes
1015
- );
1545
+ await forgeRtcPc.addIceCandidate(cinit);
1016
1546
  }
1017
- refreshStreamStats();
1018
- markFrameForFps();
1019
- const mime = String(msg.mime || "image/png");
1020
- screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
1021
- if (cameraOverlayEnabled && cameraAvailable !== false && msg.camera_b64) {
1022
- const camMime = String(msg.camera_mime || "image/png");
1023
- const widthPct = Number.isFinite(Number(msg.camera_width_percent))
1024
- ? Math.max(10, Math.min(40, Number(msg.camera_width_percent)))
1025
- : 20;
1026
- cameraOverlayEl.style.width = widthPct + "%";
1027
- cameraOverlayEl.src = "data:" + camMime + ";base64," + String(msg.camera_b64);
1028
- cameraOverlayEl.style.display = "block";
1029
- } else if (!cameraOverlayEnabled || cameraAvailable === false) {
1030
- cameraOverlayEl.style.display = "none";
1031
- }
1032
- lastFrameMeta = {
1033
- imageWidth: Number.isFinite(Number(msg.width)) ? Math.max(1, Math.floor(Number(msg.width))) : 0,
1034
- imageHeight: Number.isFinite(Number(msg.height)) ? Math.max(1, Math.floor(Number(msg.height))) : 0,
1035
- virtualX: Number.isFinite(Number(msg.virtual_x)) ? Math.floor(Number(msg.virtual_x)) : 0,
1036
- virtualY: Number.isFinite(Number(msg.virtual_y)) ? Math.floor(Number(msg.virtual_y)) : 0,
1037
- virtualWidth: Number.isFinite(Number(msg.virtual_width)) ? Math.max(1, Math.floor(Number(msg.virtual_width))) : 0,
1038
- virtualHeight: Number.isFinite(Number(msg.virtual_height)) ? Math.max(1, Math.floor(Number(msg.virtual_height))) : 0,
1039
- };
1040
- hasFrame = true;
1041
- hideEmptyState();
1042
- } else if (!hasFrame) {
1043
- const em = String(msg.error || "").trim();
1044
- shotFailureStreak += 1;
1045
- if (!legacyShotMode) {
1046
- const lower = em.toLowerCase();
1047
- const optionRejected =
1048
- (lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
1049
- (lower.includes("stream_profile") ||
1050
- lower.includes("max_bytes") ||
1051
- lower.includes("max_width") ||
1052
- lower.includes("include_camera"));
1053
- if (optionRejected || shotFailureStreak >= 2) {
1054
- legacyShotMode = true;
1055
- setState("Using legacy screenshot compatibility mode for this agent.");
1056
- }
1057
- }
1058
- showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
1059
- }
1060
- scheduleNextShot(currentShotIntervalMs());
1061
- return;
1062
- }
1063
- if (t === "rc_input_result") {
1064
- if (!msg.ok) {
1065
- const em = String(msg.error || "").trim();
1066
- const low = em.toLowerCase();
1067
- if (
1068
- low.includes("unsupported remote control action: mouse_down") ||
1069
- low.includes("unsupported remote control action: mouse_up")
1070
- ) {
1071
- disablePressLifecycle = true;
1072
- setState("Drag control needs newer agent; click/scroll still work.");
1073
- return;
1074
- }
1075
- if (low.includes("here-string") || low.includes("parsererror") || low.includes("add-type")) {
1076
- writeEnabled = false;
1077
- modeBtn.textContent = "View Only";
1078
- modeStateEl.textContent = "Mode: View Only";
1079
- modeBtn.className = "alt";
1080
- screenEl.classList.remove("write-enabled");
1081
- updateWriteControls();
1082
- const ver = sessionAgentVersion ? (" v" + sessionAgentVersion) : "";
1083
- setState("Remote agent" + ver + " needs upgrade for control input. Open /files and click Upgrade agent.");
1084
- showEmptyState("Remote input failed due to outdated agent runtime. Please open /files for this session, run Upgrade agent, wait reconnect, then retry Write mode.", true);
1085
- return;
1086
- }
1087
- setState(em ? ("Input failed: " + em) : "Input failed");
1088
- }
1089
- return;
1090
- }
1091
- const rid = String(msg && msg.request_id || "");
1092
- if (rid && pendingReqs.has(rid)) {
1093
- const done = pendingReqs.get(rid);
1094
- pendingReqs.delete(rid);
1095
- try { done(msg); } catch {}
1547
+ } catch (e) {}
1096
1548
  return;
1097
1549
  }
1550
+ await processRelayInboundCore(msg);
1098
1551
  };
1099
1552
  }
1100
1553
  function disconnect() {
1554
+ if (forgeRtcReconnectTimer) {
1555
+ clearTimeout(forgeRtcReconnectTimer);
1556
+ forgeRtcReconnectTimer = null;
1557
+ }
1558
+ forgeRtcReconnectAttempts = 0;
1101
1559
  stopShotLoop();
1560
+ teardownForgeRtcProbe();
1561
+ forgeRtcProbeStarted = false;
1562
+ relayWebrtcSignaling = false;
1563
+ relayRtcIceServers = null;
1102
1564
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
1103
1565
  clearAuthWatchdog();
1104
1566
  if (ws) { try { ws.close(); } catch {} ws = null; }
@@ -1161,14 +1623,14 @@
1161
1623
  screenshotTimer = setTimeout(() => {
1162
1624
  screenshotTimer = null;
1163
1625
  requestScreenshot();
1164
- }, Math.max(40, Number(delayMs) || 120));
1626
+ }, Math.max(28, Number(delayMs) || 120));
1165
1627
  }
1166
1628
  function startShotLoop() {
1167
1629
  stopShotLoop();
1168
1630
  scheduleNextShot(60);
1169
1631
  }
1170
1632
  function requestScreenshot() {
1171
- if (!ws || ws.readyState !== 1 || !authed) return;
1633
+ if (!viewerAgentTransportReady() || !authed) return;
1172
1634
  if (inflightShot) return;
1173
1635
  inflightShot = true;
1174
1636
  lastShotStartedAt = Date.now();
@@ -1186,15 +1648,24 @@
1186
1648
  payload.max_width = prof.maxWidth;
1187
1649
  payload.include_camera = cameraOverlayEnabled;
1188
1650
  }
1189
- ws.send(JSON.stringify(payload));
1651
+ sendAgentPayload(payload);
1190
1652
  armShotTimeout();
1191
1653
  }
1654
+ try {
1655
+ document.addEventListener("visibilitychange", function () {
1656
+ try {
1657
+ if (typeof document !== "undefined" && document.hidden) return;
1658
+ if (!authed || !viewerAgentTransportReady()) return;
1659
+ scheduleNextShot(48);
1660
+ } catch (eVis) {}
1661
+ });
1662
+ } catch (eVisHook) {}
1192
1663
  function wsRequest(type, payload) {
1193
- if (!ws || ws.readyState !== 1 || !authed) return Promise.resolve({ ok: false, error: "not connected" });
1664
+ if (!viewerAgentTransportReady() || !authed) return Promise.resolve({ ok: false, error: "not connected" });
1194
1665
  const rid = type + "_" + (++reqSeq);
1195
1666
  return new Promise((resolve) => {
1196
1667
  pendingReqs.set(rid, resolve);
1197
- ws.send(JSON.stringify(Object.assign({ type, request_id: rid }, payload || {})));
1668
+ sendAgentPayload(Object.assign({ type, request_id: rid }, payload || {}));
1198
1669
  setTimeout(() => {
1199
1670
  if (!pendingReqs.has(rid)) return;
1200
1671
  pendingReqs.delete(rid);
@@ -1580,7 +2051,7 @@
1580
2051
  if (!entries.length) clearFileList("Folder is empty");
1581
2052
  }
1582
2053
  function sendRemoteInput(payload) {
1583
- if (!ws || ws.readyState !== 1 || !authed || !writeEnabled) return;
2054
+ if (!viewerAgentTransportReady() || !authed || !writeEnabled) return;
1584
2055
  const action = String(payload && payload.action || "").trim();
1585
2056
  if (action && rcActionCaps && Object.prototype.hasOwnProperty.call(rcActionCaps, action) && !rcActionCaps[action]) {
1586
2057
  const now = Date.now();
@@ -1591,10 +2062,24 @@
1591
2062
  }
1592
2063
  return;
1593
2064
  }
1594
- ws.send(JSON.stringify(Object.assign({
1595
- type: "rc_input",
1596
- request_id: "rc_" + (++reqSeq),
1597
- }, payload || {})));
2065
+ const envelope = Object.assign(
2066
+ {
2067
+ type: "rc_input",
2068
+ request_id: "rc_" + (++reqSeq),
2069
+ },
2070
+ payload || {}
2071
+ );
2072
+ if (
2073
+ (action === "mouse_move" || action === "mouse_wheel") &&
2074
+ forgeRtcDcInput &&
2075
+ forgeRtcDcInput.readyState === "open"
2076
+ ) {
2077
+ try {
2078
+ forgeRtcDcInput.send(JSON.stringify(envelope));
2079
+ return;
2080
+ } catch (e) {}
2081
+ }
2082
+ sendAgentPayload(envelope);
1598
2083
  }
1599
2084
  async function refreshRemoteControlCapabilities() {
1600
2085
  try {
@@ -1621,7 +2106,16 @@
1621
2106
  pendingMovePoint = null;
1622
2107
  if (!p) return;
1623
2108
  const now = Date.now();
1624
- if (now - lastMoveSentAt < 35) return;
2109
+ var p2pMove =
2110
+ forgeRtcDcInput && forgeRtcDcInput.readyState === "open";
2111
+ var moveThrottleMs = p2pMove
2112
+ ? isInteractionActive()
2113
+ ? 10
2114
+ : 17
2115
+ : isInteractionActive()
2116
+ ? 14
2117
+ : 22;
2118
+ if (now - lastMoveSentAt < moveThrottleMs) return;
1625
2119
  lastMoveSentAt = now;
1626
2120
  sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
1627
2121
  });