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
|
-
|
|
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
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
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("
|
|
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
|
-
|
|
874
|
-
|
|
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
|
-
|
|
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
|
-
|
|
879
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
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("
|
|
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
|
-
|
|
874
|
-
|
|
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
|
-
|
|
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
|
-
|
|
879
|
-
|
|
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
|
-
|
|
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>
|
package/dist/fsProtocol.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
"-
|
|
4941
|
-
|
|
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
|
-
"
|
|
5026
|
-
"
|
|
5027
|
-
"
|
|
5028
|
-
"
|
|
5029
|
-
"
|
|
5030
|
-
"
|
|
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))
|
|
5037
|
-
|
|
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) {
|
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
|
});
|