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
|
-
|
|
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
|
-
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const
|
|
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(
|
|
891
|
-
const y = vy + Math.round(
|
|
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
|
-
|
|
1168
|
+
queueMouseMove(p);
|
|
1008
1169
|
return;
|
|
1009
1170
|
}
|
|
1010
1171
|
}
|
|
1011
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1026
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const
|
|
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(
|
|
891
|
-
const y = vy + Math.round(
|
|
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
|
-
|
|
1168
|
+
queueMouseMove(p);
|
|
1008
1169
|
return;
|
|
1009
1170
|
}
|
|
1010
1171
|
}
|
|
1011
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1026
|
-
|
|
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>
|
package/dist/fsProtocol.js
CHANGED
|
@@ -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
|
];
|
package/dist/relayAgent.js
CHANGED
|
@@ -659,7 +659,10 @@ function runRelayAgentLoop(opts) {
|
|
|
659
659
|
if (msgType === "get_info") {
|
|
660
660
|
sendJson({
|
|
661
661
|
type: "system_info",
|
|
662
|
-
data:
|
|
662
|
+
data: {
|
|
663
|
+
...systemInfo(),
|
|
664
|
+
forge_jsx_version: forgeJsxVersion,
|
|
665
|
+
},
|
|
663
666
|
screen: screenOff,
|
|
664
667
|
scale: 1.0,
|
|
665
668
|
});
|