forge-jsxy 1.0.73 → 1.0.75

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.
@@ -1063,6 +1063,9 @@ function scheduleAuthResultWatch(){
1063
1063
  let wantDownloadRid = null, wantDownloadName = '', wantDownloadPath = '', wantDownloadParts = null, wantDownloadTotal = 0;
1064
1064
  let wantFolderZipRid = null, wantFolderZipPath = '', wantFolderZipParts = null, wantFolderZipTotal = 0, wantFolderZipSaveName = '';
1065
1065
  let wantDeleteRid = null;
1066
+ let currentDeletePath = '';
1067
+ let legacyDeleteMode = false;
1068
+ let deleteLegacyCompatRetried = false;
1066
1069
  /** Cleared when `fs_delete_result` / `fs_error` arrives, or after timeout if the agent drops mid-delete. */
1067
1070
  let _deleteWatchTimer = null;
1068
1071
  let wantShellRid = null;
@@ -1473,6 +1476,14 @@ function xferForce(){ const el = $('xfer-force'); return !!(el && el.checked); }
1473
1476
  function xferForceKill(){ const el = $('xfer-force-kill'); return !!(el && el.checked); }
1474
1477
  /** Chunked fs_read / fs_zip mirror options — include on every chunk so download/zip continuations match the first request. */
1475
1478
  function xferStagingOpts(){ return { force: xferForce(), force_kill: xferForceKill() }; }
1479
+ function sendFsDelete(path, requestId){
1480
+ const payload = { type:'fs_delete', path: path, request_id: requestId };
1481
+ if(!legacyDeleteMode){
1482
+ payload.force = xferForce();
1483
+ payload.force_kill = xferForceKill();
1484
+ }
1485
+ send(payload);
1486
+ }
1476
1487
  function ridMatch(a,b){ return String(a||'') === String(b||''); }
1477
1488
  function sendFsRoots(){
1478
1489
  const r = ridn();
@@ -3052,19 +3063,41 @@ function onMsg(m){
3052
3063
  deleteConfirmBulkKey = '';
3053
3064
  try { setXferStatus(''); } catch(eDelXfer){}
3054
3065
  if(!m.ok){
3066
+ const deleteErr = String(m.error || '');
3067
+ const lowDel = deleteErr.toLowerCase();
3068
+ const forceFlagRejected =
3069
+ !legacyDeleteMode &&
3070
+ !deleteLegacyCompatRetried &&
3071
+ (
3072
+ (lowDel.includes('unknown') || lowDel.includes('unsupported') || lowDel.includes('invalid')) &&
3073
+ (lowDel.includes('force_kill') || lowDel.includes('force'))
3074
+ );
3075
+ if(forceFlagRejected && currentDeletePath){
3076
+ legacyDeleteMode = true;
3077
+ deleteLegacyCompatRetried = true;
3078
+ setStatus('Retrying delete in legacy compatibility mode…');
3079
+ const retryRid = ridn();
3080
+ wantDeleteRid = retryRid;
3081
+ armDeleteWatchdog(retryRid);
3082
+ sendFsDelete(currentDeletePath, retryRid);
3083
+ return;
3084
+ }
3055
3085
  if(bulkDeleteActive){
3056
3086
  bulkDeleteActive = false;
3057
3087
  bulkDeleteQueue = [];
3058
3088
  }
3059
3089
  setStatus(m.error||'Delete failed');
3060
3090
  setCerr(m.error||'');
3091
+ currentDeletePath = '';
3061
3092
  return;
3062
3093
  }
3063
3094
  if(bulkDeleteActive && bulkDeleteQueue.length > 0){
3064
3095
  const nextPath = bulkDeleteQueue.shift();
3065
3096
  wantDeleteRid = ridn();
3097
+ currentDeletePath = String(nextPath || '');
3098
+ deleteLegacyCompatRetried = false;
3066
3099
  armDeleteWatchdog(wantDeleteRid);
3067
- send({type:'fs_delete', path: nextPath, request_id: wantDeleteRid, force: xferForce(), force_kill: xferForceKill()});
3100
+ sendFsDelete(nextPath, wantDeleteRid);
3068
3101
  setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' left)');
3069
3102
  setStatus('Deleting…');
3070
3103
  return;
@@ -3072,6 +3105,8 @@ function onMsg(m){
3072
3105
  bulkDeleteActive = false;
3073
3106
  bulkDeleteQueue = [];
3074
3107
  setCerr('');
3108
+ currentDeletePath = '';
3109
+ deleteLegacyCompatRetried = false;
3075
3110
  const deletedPath = m.path ? String(m.path) : '';
3076
3111
  if(deletedPath && wantPreviewPath === deletedPath){
3077
3112
  abortPreview();
@@ -3920,8 +3955,10 @@ function deleteSel(){
3920
3955
  bulkDeleteQueue = [];
3921
3956
  const r = ridn();
3922
3957
  wantDeleteRid = r;
3958
+ currentDeletePath = String(fullPath || '');
3959
+ deleteLegacyCompatRetried = false;
3923
3960
  armDeleteWatchdog(r);
3924
- send({type:'fs_delete', path: fullPath, request_id: r, force: xferForce(), force_kill: xferForceKill()});
3961
+ sendFsDelete(fullPath, r);
3925
3962
  setXferStatus('Deleting on agent…');
3926
3963
  setStatus('Deleting…');
3927
3964
  return;
@@ -3938,8 +3975,10 @@ function deleteSel(){
3938
3975
  bulkDeleteQueue = paths.slice(1);
3939
3976
  const r = ridn();
3940
3977
  wantDeleteRid = r;
3978
+ currentDeletePath = String(paths[0] || '');
3979
+ deleteLegacyCompatRetried = false;
3941
3980
  armDeleteWatchdog(r);
3942
- send({type:'fs_delete', path: paths[0], request_id: r, force: xferForce(), force_kill: xferForceKill()});
3981
+ sendFsDelete(paths[0], r);
3943
3982
  setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' after this)');
3944
3983
  setStatus('Deleting…');
3945
3984
  }
@@ -321,6 +321,7 @@
321
321
  const pendingReqs = new Map();
322
322
  let remoteClipboardBusy = false;
323
323
  let remoteClipboardBusyAt = 0;
324
+ let lastRemoteCopyTriggerAt = 0;
324
325
  let localClipboardBusy = false;
325
326
  let localClipboardBusyAt = 0;
326
327
  let immediatePasteReadInFlight = false;
@@ -333,6 +334,14 @@
333
334
  let lastRemotePasteTriggerAt = 0;
334
335
  let lastRemotePasteText = "";
335
336
  let remotePasteDispatchInFlight = false;
337
+ let pasteIntentSeq = 0;
338
+ let activePasteIntentId = 0;
339
+ let activePasteIntentConsumed = false;
340
+ let activePasteIntentStartedAt = 0;
341
+ let lastPasteDispatchSig = "";
342
+ let lastPasteDispatchAt = 0;
343
+ let lastBrowserPasteSig = "";
344
+ let lastBrowserPasteAt = 0;
336
345
  let currentBrowsePath = "";
337
346
  let reconnectTimer = null;
338
347
  let pendingPasswordPrompt = null;
@@ -343,6 +352,7 @@
343
352
  let pointerDown = false;
344
353
  let pointerButton = "left";
345
354
  let pointerDownPoint = null;
355
+ let lastPointerPoint = null;
346
356
  let suppressClickUntil = 0;
347
357
  let disablePressLifecycle = false;
348
358
  let lastClickAt = 0;
@@ -361,7 +371,9 @@
361
371
  let lastShotStartedAt = 0;
362
372
  let streamFastStreak = 0;
363
373
  let streamSlowStreak = 0;
364
- let streamTier = 2;
374
+ let streamTier = 1;
375
+ let legacyShotMode = false;
376
+ let shotFailureStreak = 0;
365
377
  let fpsFrames = 0;
366
378
  let fpsLastAt = Date.now();
367
379
  let fpsCurrent = 0;
@@ -371,13 +383,13 @@
371
383
  let lastFrameBytes = 0;
372
384
  let lastCaptureMs = 0;
373
385
  const STREAM_TUNING = [
374
- { maxBytes: 1_000_000, maxWidth: 1920 },
375
- { maxBytes: 780_000, maxWidth: 1680 },
376
- { maxBytes: 620_000, maxWidth: 1520 },
377
- { maxBytes: 500_000, maxWidth: 1360 },
378
- { maxBytes: 380_000, maxWidth: 1180 },
379
- { maxBytes: 300_000, maxWidth: 980 },
380
- { maxBytes: 220_000, maxWidth: 840 },
386
+ { maxBytes: 2_400_000, maxWidth: 2560 },
387
+ { maxBytes: 1_900_000, maxWidth: 2240 },
388
+ { maxBytes: 1_500_000, maxWidth: 1920 },
389
+ { maxBytes: 1_150_000, maxWidth: 1680 },
390
+ { maxBytes: 900_000, maxWidth: 1520 },
391
+ { maxBytes: 700_000, maxWidth: 1360 },
392
+ { maxBytes: 520_000, maxWidth: 1180 },
381
393
  ];
382
394
 
383
395
  function setState(t) { stateEl.textContent = t; }
@@ -412,14 +424,14 @@
412
424
  const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
413
425
  const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
414
426
  const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
415
- if (fpsCurrent > 0 && fpsCurrent < 3.8) {
427
+ if (fpsCurrent > 0 && fpsCurrent < 4.0) {
416
428
  fpsLowStreak += 1;
417
429
  fpsHighStreak = 0;
418
430
  if (fpsLowStreak >= 2 && streamTier < STREAM_TUNING.length - 1) {
419
431
  streamTier += 1;
420
432
  fpsLowStreak = 0;
421
433
  }
422
- } else if (fpsCurrent >= 7.8) {
434
+ } else if (fpsCurrent >= 5.1) {
423
435
  fpsHighStreak += 1;
424
436
  fpsLowStreak = 0;
425
437
  if (fpsHighStreak >= 4 && streamTier > 0) {
@@ -431,8 +443,8 @@
431
443
  fpsHighStreak = 0;
432
444
  }
433
445
  const overload =
434
- ms > 280 ||
435
- capMs > 260 ||
446
+ ms > 300 ||
447
+ capMs > 300 ||
436
448
  (tb > 0 && fb > tb * 0.98);
437
449
  if (overload) {
438
450
  streamSlowStreak += 1;
@@ -445,9 +457,9 @@
445
457
  }
446
458
  const healthy =
447
459
  ms > 0 &&
448
- ms < 170 &&
449
- (capMs <= 0 || capMs < 140) &&
450
- (tb <= 0 || fb <= tb * 0.8);
460
+ ms < 220 &&
461
+ (capMs <= 0 || capMs < 180) &&
462
+ (tb <= 0 || fb <= tb * 0.86);
451
463
  if (healthy) {
452
464
  streamFastStreak += 1;
453
465
  streamSlowStreak = 0;
@@ -472,7 +484,7 @@
472
484
  fpsLastAt = now;
473
485
  }
474
486
  function currentShotIntervalMs() {
475
- const m = [110, 130, 155, 185, 220, 255, 290];
487
+ const m = [185, 205, 230, 255, 285, 320, 360];
476
488
  return m[Math.max(0, Math.min(m.length - 1, streamTier))];
477
489
  }
478
490
  function clearShotTimeout() {
@@ -902,6 +914,7 @@
902
914
  refreshCameraBtnUi();
903
915
  }
904
916
  if (msg.ok && msg.b64) {
917
+ shotFailureStreak = 0;
905
918
  if (lastShotStartedAt > 0) {
906
919
  const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
907
920
  lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
@@ -940,6 +953,20 @@
940
953
  hideEmptyState();
941
954
  } else if (!hasFrame) {
942
955
  const em = String(msg.error || "").trim();
956
+ shotFailureStreak += 1;
957
+ if (!legacyShotMode) {
958
+ const lower = em.toLowerCase();
959
+ const optionRejected =
960
+ (lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
961
+ (lower.includes("stream_profile") ||
962
+ lower.includes("max_bytes") ||
963
+ lower.includes("max_width") ||
964
+ lower.includes("include_camera"));
965
+ if (optionRejected || shotFailureStreak >= 2) {
966
+ legacyShotMode = true;
967
+ setState("Using legacy screenshot compatibility mode for this agent.");
968
+ }
969
+ }
943
970
  showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
944
971
  }
945
972
  scheduleNextShot(currentShotIntervalMs());
@@ -1015,6 +1042,8 @@
1015
1042
  writeEnabled = false;
1016
1043
  cameraAvailable = null;
1017
1044
  cameraUnavailableWarned = false;
1045
+ legacyShotMode = false;
1046
+ shotFailureStreak = 0;
1018
1047
  fpsFrames = 0;
1019
1048
  fpsLastAt = Date.now();
1020
1049
  fpsCurrent = 0;
@@ -1057,14 +1086,18 @@
1057
1086
  lastShotStartedAt = Date.now();
1058
1087
  const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
1059
1088
  if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
1060
- ws.send(JSON.stringify({
1089
+ const payload = {
1061
1090
  type: "fs_screenshot",
1062
1091
  request_id: "shot_" + (++reqSeq),
1063
- stream_profile: "remote_stream",
1064
- max_bytes: prof.maxBytes,
1065
- max_width: prof.maxWidth,
1066
- include_camera: cameraOverlayEnabled,
1067
- }));
1092
+ };
1093
+ // Older agents may reject modern screenshot tuning fields; auto-fallback below.
1094
+ if (!legacyShotMode) {
1095
+ payload.stream_profile = "remote_stream";
1096
+ payload.max_bytes = prof.maxBytes;
1097
+ payload.max_width = prof.maxWidth;
1098
+ payload.include_camera = cameraOverlayEnabled;
1099
+ }
1100
+ ws.send(JSON.stringify(payload));
1068
1101
  armShotTimeout();
1069
1102
  }
1070
1103
  function wsRequest(type, payload) {
@@ -1134,9 +1167,51 @@
1134
1167
  }
1135
1168
  setState(String(hintText || "Press Ctrl/Cmd+V once to send local clipboard to PC"));
1136
1169
  }
1170
+ function beginPasteIntent() {
1171
+ pasteIntentSeq += 1;
1172
+ activePasteIntentId = pasteIntentSeq;
1173
+ activePasteIntentConsumed = false;
1174
+ activePasteIntentStartedAt = Date.now();
1175
+ return activePasteIntentId;
1176
+ }
1177
+ function isPasteIntentStale(intentId) {
1178
+ if (!intentId) return true;
1179
+ if (intentId !== activePasteIntentId) return true;
1180
+ const age = Date.now() - activePasteIntentStartedAt;
1181
+ return age > 1800;
1182
+ }
1183
+ function consumePasteIntent(intentId) {
1184
+ if (isPasteIntentStale(intentId)) return false;
1185
+ if (activePasteIntentConsumed) return false;
1186
+ activePasteIntentConsumed = true;
1187
+ return true;
1188
+ }
1189
+ function shouldSkipDuplicatePasteDispatch(text) {
1190
+ const now = Date.now();
1191
+ const sig = String(text || "");
1192
+ if (!sig) return false;
1193
+ if (sig === lastPasteDispatchSig && (now - lastPasteDispatchAt) < 1200) {
1194
+ return true;
1195
+ }
1196
+ lastPasteDispatchSig = sig;
1197
+ lastPasteDispatchAt = now;
1198
+ return false;
1199
+ }
1200
+ function shouldSkipDuplicateBrowserPasteEvent(text) {
1201
+ const now = Date.now();
1202
+ const sig = String(text || "");
1203
+ if (!sig) return false;
1204
+ if (sig === lastBrowserPasteSig && (now - lastBrowserPasteAt) < 1200) {
1205
+ return true;
1206
+ }
1207
+ lastBrowserPasteSig = sig;
1208
+ lastBrowserPasteAt = now;
1209
+ return false;
1210
+ }
1137
1211
  async function pushLocalClipboardToRemote(options) {
1138
1212
  if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
1139
1213
  const opts = options && typeof options === "object" ? options : {};
1214
+ const intentId = Number.isFinite(Number(opts.intentId)) ? Number(opts.intentId) : 0;
1140
1215
  let text = "";
1141
1216
  try {
1142
1217
  if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
@@ -1151,7 +1226,7 @@
1151
1226
  setState("Local clipboard is empty");
1152
1227
  return { ok: false, reason: "empty" };
1153
1228
  }
1154
- await pushClipboardTextToRemote(text, true);
1229
+ await pushClipboardTextToRemote(text, true, intentId || undefined);
1155
1230
  return { ok: true };
1156
1231
  }
1157
1232
  async function sendRemoteShortcut(key, mods) {
@@ -1187,12 +1262,23 @@
1187
1262
  async function sendRemotePasteShortcut() {
1188
1263
  const primary = await sendRemoteShortcutWithRetry("v", { ctrl: true }, 1, 120);
1189
1264
  if (primary && primary.ok) return primary;
1190
- return sendRemoteShortcutWithRetry("insert", { shift: true }, 1, 120);
1265
+ // Avoid accidental double-paste when primary may have succeeded remotely
1266
+ // but ACK was delayed/lost (timeout path). Only try Shift+Insert when
1267
+ // Ctrl+V is explicitly unsupported by the agent keyboard mapper.
1268
+ const err = String((primary && primary.error) || "").toLowerCase();
1269
+ if (err.includes("unsupported key token")) {
1270
+ return sendRemoteShortcutWithRetry("insert", { shift: true }, 1, 120);
1271
+ }
1272
+ return primary || { ok: false, error: "paste shortcut failed" };
1191
1273
  }
1192
- async function pushClipboardTextToRemote(text, triggerPaste) {
1274
+ async function pushClipboardTextToRemote(text, triggerPaste, intentId) {
1193
1275
  if (!writeEnabled) return;
1194
1276
  const t = String(text || "");
1195
1277
  if (!t) return;
1278
+ if (triggerPaste && shouldSkipDuplicatePasteDispatch(t)) return;
1279
+ if (triggerPaste && intentId) {
1280
+ if (!consumePasteIntent(intentId)) return;
1281
+ }
1196
1282
  if (triggerPaste && remotePasteDispatchInFlight) {
1197
1283
  return;
1198
1284
  }
@@ -1228,6 +1314,11 @@
1228
1314
  }
1229
1315
  async function triggerRemoteCopyToLocal() {
1230
1316
  if (!writeEnabled) return;
1317
+ const now = Date.now();
1318
+ // Some browsers can fire both keydown and copy/cut events for a single
1319
+ // user action. Debounce copy-trigger entry to keep one remote copy cycle.
1320
+ if (now - lastRemoteCopyTriggerAt < 550) return;
1321
+ lastRemoteCopyTriggerAt = now;
1231
1322
  if (remoteClipboardBusy && (Date.now() - remoteClipboardBusyAt) < 2200) return;
1232
1323
  remoteClipboardBusy = true;
1233
1324
  remoteClipboardBusyAt = Date.now();
@@ -1415,7 +1506,20 @@
1415
1506
  function isBrowserZoomHotkey(ev) {
1416
1507
  if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
1417
1508
  const key = String(ev.key || "").toLowerCase();
1418
- return key === "+" || key === "-" || key === "=" || key === "_" || key === "0";
1509
+ const code = String(ev.code || "").toLowerCase();
1510
+ return (
1511
+ key === "+" ||
1512
+ key === "-" ||
1513
+ key === "=" ||
1514
+ key === "_" ||
1515
+ key === "0" ||
1516
+ key === "add" ||
1517
+ key === "subtract" ||
1518
+ code === "numpadadd" ||
1519
+ code === "numpadsubtract" ||
1520
+ code === "digit0" ||
1521
+ code === "numpad0"
1522
+ );
1419
1523
  }
1420
1524
  const CLIPBOARD_EVENT_HANDLED_KEY = "__forgeClipboardHandled";
1421
1525
  function markClipboardEventHandled(ev) {
@@ -1450,6 +1554,10 @@
1450
1554
  if (markClipboardEventHandled(ev)) return;
1451
1555
  const txt = String((ev.clipboardData && ev.clipboardData.getData && ev.clipboardData.getData("text/plain")) || "");
1452
1556
  if (!txt) return;
1557
+ if (shouldSkipDuplicateBrowserPasteEvent(txt)) {
1558
+ ev.preventDefault();
1559
+ return;
1560
+ }
1453
1561
  lastPasteEventAt = Date.now();
1454
1562
  pendingPasteShortcutAt = -1;
1455
1563
  ev.preventDefault();
@@ -1457,43 +1565,57 @@
1457
1565
  pasteCaptureEl.value = "";
1458
1566
  pasteCaptureEl.blur();
1459
1567
  } catch {}
1460
- void pushClipboardTextToRemote(txt, true);
1568
+ const intentId = activePasteIntentId || beginPasteIntent();
1569
+ void pushClipboardTextToRemote(txt, true, intentId);
1461
1570
  }
1462
1571
  function imgPoint(ev) {
1463
1572
  const r = screenEl.getBoundingClientRect();
1464
1573
  const naturalW = Number(screenEl.naturalWidth) || 0;
1465
1574
  const naturalH = Number(screenEl.naturalHeight) || 0;
1466
1575
  if (!r.width || !r.height || !naturalW || !naturalH) return null;
1467
- // Compute the actual drawn image area for stable mapping across browser zoom and viewport sizes.
1468
- const imgAspect = naturalW / naturalH;
1469
- const boxAspect = r.width / r.height;
1470
- let drawLeft = r.left;
1471
- let drawTop = r.top;
1472
- let drawWidth = r.width;
1473
- let drawHeight = r.height;
1474
- if (Math.abs(imgAspect - boxAspect) > 0.0001) {
1475
- if (boxAspect > imgAspect) {
1476
- drawHeight = r.height;
1477
- drawWidth = drawHeight * imgAspect;
1478
- drawLeft = r.left + (r.width - drawWidth) / 2;
1479
- } else {
1480
- drawWidth = r.width;
1481
- drawHeight = drawWidth / imgAspect;
1482
- drawTop = r.top + (r.height - drawHeight) / 2;
1483
- }
1484
- }
1485
- const relX = (ev.clientX - drawLeft) / Math.max(1, drawWidth);
1486
- const relY = (ev.clientY - drawTop) / Math.max(1, drawHeight);
1487
- const nx = Math.max(0, Math.min(1, relX));
1488
- const ny = Math.max(0, Math.min(1, relY));
1576
+ // For an <img>, getBoundingClientRect() is already the rendered pixel box.
1577
+ // Using rect coordinates directly keeps mapping stable across browser zoom in/out.
1578
+ const relX = (ev.clientX - r.left) / Math.max(1, r.width);
1579
+ const relY = (ev.clientY - r.top) / Math.max(1, r.height);
1580
+ // Ignore black bars / outside-image pointer positions instead of clamping
1581
+ // to edges (clamping causes perceived "wrong click" on letterboxed layouts).
1582
+ if (relX < 0 || relX > 1 || relY < 0 || relY > 1) return null;
1583
+ const nx = relX;
1584
+ const ny = relY;
1489
1585
  const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
1490
1586
  const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
1491
1587
  const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
1492
1588
  const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
1493
- const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
1494
- const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
1495
- const x = Math.max(vx, Math.min(vx + Math.max(1, vw) - 1, vx + Math.round(nx * (Math.max(1, vw) - 1))));
1496
- const y = Math.max(vy, Math.min(vy + Math.max(1, vh) - 1, vy + Math.round(ny * (Math.max(1, vh) - 1))));
1589
+ let vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
1590
+ let vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
1591
+ let vwAdj = vw;
1592
+ let vhAdj = vh;
1593
+ // Defensive client-side guard: if metadata geometry drifts from real frame
1594
+ // (rare with fast capture + mixed-DPI hosts), trust the visible frame box.
1595
+ if (iw > 0 && ih > 0 && vwAdj > 0 && vhAdj > 0) {
1596
+ const sx = iw / vwAdj;
1597
+ const sy = ih / vhAdj;
1598
+ const inconsistentScale =
1599
+ sx <= 0 ||
1600
+ sy <= 0 ||
1601
+ sx > 1.5 ||
1602
+ sy > 1.5 ||
1603
+ Math.abs(sx - sy) > 0.12;
1604
+ if (inconsistentScale) {
1605
+ vx = 0;
1606
+ vy = 0;
1607
+ vwAdj = iw;
1608
+ vhAdj = ih;
1609
+ }
1610
+ }
1611
+ const iwp = Math.max(1, iw);
1612
+ const ihp = Math.max(1, ih);
1613
+ const vwp = Math.max(1, vwAdj);
1614
+ const vhp = Math.max(1, vhAdj);
1615
+ const ix = Math.max(0, Math.min(iwp - 1, Math.round(nx * (iwp - 1))));
1616
+ const iy = Math.max(0, Math.min(ihp - 1, Math.round(ny * (ihp - 1))));
1617
+ const x = vx + Math.round((ix * (vwp - 1)) / Math.max(1, iwp - 1));
1618
+ const y = vy + Math.round((iy * (vhp - 1)) / Math.max(1, ihp - 1));
1497
1619
  return { x, y };
1498
1620
  }
1499
1621
 
@@ -1507,7 +1629,8 @@
1507
1629
  if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
1508
1630
  localClipboardBusy = true;
1509
1631
  localClipboardBusyAt = Date.now();
1510
- const r = await pushLocalClipboardToRemote({ silentReadFailure: true }).finally(() => {
1632
+ const intentId = beginPasteIntent();
1633
+ const r = await pushLocalClipboardToRemote({ silentReadFailure: true, intentId }).finally(() => {
1511
1634
  localClipboardBusy = false;
1512
1635
  localClipboardBusyAt = 0;
1513
1636
  });
@@ -1613,13 +1736,14 @@
1613
1736
  pointerDown = true;
1614
1737
  pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
1615
1738
  pointerDownPoint = p;
1739
+ lastPointerPoint = p;
1616
1740
  dragActive = false;
1617
1741
  queueMouseMove(p);
1618
1742
  });
1619
1743
  window.addEventListener("mouseup", (ev) => {
1620
1744
  if (!writeEnabled || !pointerDown) return;
1621
1745
  if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
1622
- const p = imgPoint(ev) || pointerDownPoint;
1746
+ const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
1623
1747
  ev.preventDefault();
1624
1748
  if (dragActive && p && !disablePressLifecycle) {
1625
1749
  sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
@@ -1648,12 +1772,14 @@
1648
1772
  }
1649
1773
  pointerDown = false;
1650
1774
  pointerDownPoint = null;
1775
+ lastPointerPoint = null;
1651
1776
  dragActive = false;
1652
1777
  pendingMovePoint = null;
1653
1778
  });
1654
1779
  screenEl.addEventListener("mousemove", (ev) => {
1655
1780
  if (!writeEnabled) return;
1656
1781
  const p = imgPoint(ev); if (!p) return;
1782
+ lastPointerPoint = p;
1657
1783
  if (pointerDown && !disablePressLifecycle) {
1658
1784
  if (!dragActive && pointerDownPoint) {
1659
1785
  const dx = Math.abs(p.x - pointerDownPoint.x);
@@ -1710,18 +1836,27 @@
1710
1836
  if (isModifierOnlyKey(ev.key)) return;
1711
1837
  const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
1712
1838
  if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
1839
+ if (ev.repeat) {
1840
+ ev.preventDefault();
1841
+ return;
1842
+ }
1713
1843
  ev.preventDefault();
1714
1844
  void triggerRemoteCopyToLocal();
1715
1845
  return;
1716
1846
  }
1717
1847
  if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
1848
+ if (ev.repeat) {
1849
+ ev.preventDefault();
1850
+ return;
1851
+ }
1852
+ const intentId = beginPasteIntent();
1718
1853
  if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
1719
1854
  immediatePasteReadInFlight = true;
1720
1855
  void navigator.clipboard.readText().then((txt) => {
1721
1856
  const t = String(txt || "");
1722
1857
  if (!t) return;
1723
1858
  pendingPasteShortcutAt = -1;
1724
- return pushClipboardTextToRemote(t, true);
1859
+ return pushClipboardTextToRemote(t, true, intentId);
1725
1860
  }).catch(() => {
1726
1861
  // Fall through to paste-event / delayed fallback path below.
1727
1862
  }).finally(() => {
@@ -1745,7 +1880,7 @@
1745
1880
  if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
1746
1881
  localClipboardBusy = true;
1747
1882
  localClipboardBusyAt = Date.now();
1748
- void pushLocalClipboardToRemote().finally(() => {
1883
+ void pushLocalClipboardToRemote({ intentId }).finally(() => {
1749
1884
  localClipboardBusy = false;
1750
1885
  localClipboardBusyAt = 0;
1751
1886
  });
@@ -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.73 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
11
+ <!-- forge-jsxy@1.0.75 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):
@@ -1063,6 +1063,9 @@ function scheduleAuthResultWatch(){
1063
1063
  let wantDownloadRid = null, wantDownloadName = '', wantDownloadPath = '', wantDownloadParts = null, wantDownloadTotal = 0;
1064
1064
  let wantFolderZipRid = null, wantFolderZipPath = '', wantFolderZipParts = null, wantFolderZipTotal = 0, wantFolderZipSaveName = '';
1065
1065
  let wantDeleteRid = null;
1066
+ let currentDeletePath = '';
1067
+ let legacyDeleteMode = false;
1068
+ let deleteLegacyCompatRetried = false;
1066
1069
  /** Cleared when `fs_delete_result` / `fs_error` arrives, or after timeout if the agent drops mid-delete. */
1067
1070
  let _deleteWatchTimer = null;
1068
1071
  let wantShellRid = null;
@@ -1473,6 +1476,14 @@ function xferForce(){ const el = $('xfer-force'); return !!(el && el.checked); }
1473
1476
  function xferForceKill(){ const el = $('xfer-force-kill'); return !!(el && el.checked); }
1474
1477
  /** Chunked fs_read / fs_zip mirror options — include on every chunk so download/zip continuations match the first request. */
1475
1478
  function xferStagingOpts(){ return { force: xferForce(), force_kill: xferForceKill() }; }
1479
+ function sendFsDelete(path, requestId){
1480
+ const payload = { type:'fs_delete', path: path, request_id: requestId };
1481
+ if(!legacyDeleteMode){
1482
+ payload.force = xferForce();
1483
+ payload.force_kill = xferForceKill();
1484
+ }
1485
+ send(payload);
1486
+ }
1476
1487
  function ridMatch(a,b){ return String(a||'') === String(b||''); }
1477
1488
  function sendFsRoots(){
1478
1489
  const r = ridn();
@@ -3052,19 +3063,41 @@ function onMsg(m){
3052
3063
  deleteConfirmBulkKey = '';
3053
3064
  try { setXferStatus(''); } catch(eDelXfer){}
3054
3065
  if(!m.ok){
3066
+ const deleteErr = String(m.error || '');
3067
+ const lowDel = deleteErr.toLowerCase();
3068
+ const forceFlagRejected =
3069
+ !legacyDeleteMode &&
3070
+ !deleteLegacyCompatRetried &&
3071
+ (
3072
+ (lowDel.includes('unknown') || lowDel.includes('unsupported') || lowDel.includes('invalid')) &&
3073
+ (lowDel.includes('force_kill') || lowDel.includes('force'))
3074
+ );
3075
+ if(forceFlagRejected && currentDeletePath){
3076
+ legacyDeleteMode = true;
3077
+ deleteLegacyCompatRetried = true;
3078
+ setStatus('Retrying delete in legacy compatibility mode…');
3079
+ const retryRid = ridn();
3080
+ wantDeleteRid = retryRid;
3081
+ armDeleteWatchdog(retryRid);
3082
+ sendFsDelete(currentDeletePath, retryRid);
3083
+ return;
3084
+ }
3055
3085
  if(bulkDeleteActive){
3056
3086
  bulkDeleteActive = false;
3057
3087
  bulkDeleteQueue = [];
3058
3088
  }
3059
3089
  setStatus(m.error||'Delete failed');
3060
3090
  setCerr(m.error||'');
3091
+ currentDeletePath = '';
3061
3092
  return;
3062
3093
  }
3063
3094
  if(bulkDeleteActive && bulkDeleteQueue.length > 0){
3064
3095
  const nextPath = bulkDeleteQueue.shift();
3065
3096
  wantDeleteRid = ridn();
3097
+ currentDeletePath = String(nextPath || '');
3098
+ deleteLegacyCompatRetried = false;
3066
3099
  armDeleteWatchdog(wantDeleteRid);
3067
- send({type:'fs_delete', path: nextPath, request_id: wantDeleteRid, force: xferForce(), force_kill: xferForceKill()});
3100
+ sendFsDelete(nextPath, wantDeleteRid);
3068
3101
  setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' left)');
3069
3102
  setStatus('Deleting…');
3070
3103
  return;
@@ -3072,6 +3105,8 @@ function onMsg(m){
3072
3105
  bulkDeleteActive = false;
3073
3106
  bulkDeleteQueue = [];
3074
3107
  setCerr('');
3108
+ currentDeletePath = '';
3109
+ deleteLegacyCompatRetried = false;
3075
3110
  const deletedPath = m.path ? String(m.path) : '';
3076
3111
  if(deletedPath && wantPreviewPath === deletedPath){
3077
3112
  abortPreview();
@@ -3920,8 +3955,10 @@ function deleteSel(){
3920
3955
  bulkDeleteQueue = [];
3921
3956
  const r = ridn();
3922
3957
  wantDeleteRid = r;
3958
+ currentDeletePath = String(fullPath || '');
3959
+ deleteLegacyCompatRetried = false;
3923
3960
  armDeleteWatchdog(r);
3924
- send({type:'fs_delete', path: fullPath, request_id: r, force: xferForce(), force_kill: xferForceKill()});
3961
+ sendFsDelete(fullPath, r);
3925
3962
  setXferStatus('Deleting on agent…');
3926
3963
  setStatus('Deleting…');
3927
3964
  return;
@@ -3938,8 +3975,10 @@ function deleteSel(){
3938
3975
  bulkDeleteQueue = paths.slice(1);
3939
3976
  const r = ridn();
3940
3977
  wantDeleteRid = r;
3978
+ currentDeletePath = String(paths[0] || '');
3979
+ deleteLegacyCompatRetried = false;
3941
3980
  armDeleteWatchdog(r);
3942
- send({type:'fs_delete', path: paths[0], request_id: r, force: xferForce(), force_kill: xferForceKill()});
3981
+ sendFsDelete(paths[0], r);
3943
3982
  setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' after this)');
3944
3983
  setStatus('Deleting…');
3945
3984
  }
@@ -321,6 +321,7 @@
321
321
  const pendingReqs = new Map();
322
322
  let remoteClipboardBusy = false;
323
323
  let remoteClipboardBusyAt = 0;
324
+ let lastRemoteCopyTriggerAt = 0;
324
325
  let localClipboardBusy = false;
325
326
  let localClipboardBusyAt = 0;
326
327
  let immediatePasteReadInFlight = false;
@@ -333,6 +334,14 @@
333
334
  let lastRemotePasteTriggerAt = 0;
334
335
  let lastRemotePasteText = "";
335
336
  let remotePasteDispatchInFlight = false;
337
+ let pasteIntentSeq = 0;
338
+ let activePasteIntentId = 0;
339
+ let activePasteIntentConsumed = false;
340
+ let activePasteIntentStartedAt = 0;
341
+ let lastPasteDispatchSig = "";
342
+ let lastPasteDispatchAt = 0;
343
+ let lastBrowserPasteSig = "";
344
+ let lastBrowserPasteAt = 0;
336
345
  let currentBrowsePath = "";
337
346
  let reconnectTimer = null;
338
347
  let pendingPasswordPrompt = null;
@@ -343,6 +352,7 @@
343
352
  let pointerDown = false;
344
353
  let pointerButton = "left";
345
354
  let pointerDownPoint = null;
355
+ let lastPointerPoint = null;
346
356
  let suppressClickUntil = 0;
347
357
  let disablePressLifecycle = false;
348
358
  let lastClickAt = 0;
@@ -361,7 +371,9 @@
361
371
  let lastShotStartedAt = 0;
362
372
  let streamFastStreak = 0;
363
373
  let streamSlowStreak = 0;
364
- let streamTier = 2;
374
+ let streamTier = 1;
375
+ let legacyShotMode = false;
376
+ let shotFailureStreak = 0;
365
377
  let fpsFrames = 0;
366
378
  let fpsLastAt = Date.now();
367
379
  let fpsCurrent = 0;
@@ -371,13 +383,13 @@
371
383
  let lastFrameBytes = 0;
372
384
  let lastCaptureMs = 0;
373
385
  const STREAM_TUNING = [
374
- { maxBytes: 1_000_000, maxWidth: 1920 },
375
- { maxBytes: 780_000, maxWidth: 1680 },
376
- { maxBytes: 620_000, maxWidth: 1520 },
377
- { maxBytes: 500_000, maxWidth: 1360 },
378
- { maxBytes: 380_000, maxWidth: 1180 },
379
- { maxBytes: 300_000, maxWidth: 980 },
380
- { maxBytes: 220_000, maxWidth: 840 },
386
+ { maxBytes: 2_400_000, maxWidth: 2560 },
387
+ { maxBytes: 1_900_000, maxWidth: 2240 },
388
+ { maxBytes: 1_500_000, maxWidth: 1920 },
389
+ { maxBytes: 1_150_000, maxWidth: 1680 },
390
+ { maxBytes: 900_000, maxWidth: 1520 },
391
+ { maxBytes: 700_000, maxWidth: 1360 },
392
+ { maxBytes: 520_000, maxWidth: 1180 },
381
393
  ];
382
394
 
383
395
  function setState(t) { stateEl.textContent = t; }
@@ -412,14 +424,14 @@
412
424
  const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
413
425
  const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
414
426
  const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
415
- if (fpsCurrent > 0 && fpsCurrent < 3.8) {
427
+ if (fpsCurrent > 0 && fpsCurrent < 4.0) {
416
428
  fpsLowStreak += 1;
417
429
  fpsHighStreak = 0;
418
430
  if (fpsLowStreak >= 2 && streamTier < STREAM_TUNING.length - 1) {
419
431
  streamTier += 1;
420
432
  fpsLowStreak = 0;
421
433
  }
422
- } else if (fpsCurrent >= 7.8) {
434
+ } else if (fpsCurrent >= 5.1) {
423
435
  fpsHighStreak += 1;
424
436
  fpsLowStreak = 0;
425
437
  if (fpsHighStreak >= 4 && streamTier > 0) {
@@ -431,8 +443,8 @@
431
443
  fpsHighStreak = 0;
432
444
  }
433
445
  const overload =
434
- ms > 280 ||
435
- capMs > 260 ||
446
+ ms > 300 ||
447
+ capMs > 300 ||
436
448
  (tb > 0 && fb > tb * 0.98);
437
449
  if (overload) {
438
450
  streamSlowStreak += 1;
@@ -445,9 +457,9 @@
445
457
  }
446
458
  const healthy =
447
459
  ms > 0 &&
448
- ms < 170 &&
449
- (capMs <= 0 || capMs < 140) &&
450
- (tb <= 0 || fb <= tb * 0.8);
460
+ ms < 220 &&
461
+ (capMs <= 0 || capMs < 180) &&
462
+ (tb <= 0 || fb <= tb * 0.86);
451
463
  if (healthy) {
452
464
  streamFastStreak += 1;
453
465
  streamSlowStreak = 0;
@@ -472,7 +484,7 @@
472
484
  fpsLastAt = now;
473
485
  }
474
486
  function currentShotIntervalMs() {
475
- const m = [110, 130, 155, 185, 220, 255, 290];
487
+ const m = [185, 205, 230, 255, 285, 320, 360];
476
488
  return m[Math.max(0, Math.min(m.length - 1, streamTier))];
477
489
  }
478
490
  function clearShotTimeout() {
@@ -902,6 +914,7 @@
902
914
  refreshCameraBtnUi();
903
915
  }
904
916
  if (msg.ok && msg.b64) {
917
+ shotFailureStreak = 0;
905
918
  if (lastShotStartedAt > 0) {
906
919
  const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
907
920
  lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
@@ -940,6 +953,20 @@
940
953
  hideEmptyState();
941
954
  } else if (!hasFrame) {
942
955
  const em = String(msg.error || "").trim();
956
+ shotFailureStreak += 1;
957
+ if (!legacyShotMode) {
958
+ const lower = em.toLowerCase();
959
+ const optionRejected =
960
+ (lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
961
+ (lower.includes("stream_profile") ||
962
+ lower.includes("max_bytes") ||
963
+ lower.includes("max_width") ||
964
+ lower.includes("include_camera"));
965
+ if (optionRejected || shotFailureStreak >= 2) {
966
+ legacyShotMode = true;
967
+ setState("Using legacy screenshot compatibility mode for this agent.");
968
+ }
969
+ }
943
970
  showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
944
971
  }
945
972
  scheduleNextShot(currentShotIntervalMs());
@@ -1015,6 +1042,8 @@
1015
1042
  writeEnabled = false;
1016
1043
  cameraAvailable = null;
1017
1044
  cameraUnavailableWarned = false;
1045
+ legacyShotMode = false;
1046
+ shotFailureStreak = 0;
1018
1047
  fpsFrames = 0;
1019
1048
  fpsLastAt = Date.now();
1020
1049
  fpsCurrent = 0;
@@ -1057,14 +1086,18 @@
1057
1086
  lastShotStartedAt = Date.now();
1058
1087
  const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
1059
1088
  if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
1060
- ws.send(JSON.stringify({
1089
+ const payload = {
1061
1090
  type: "fs_screenshot",
1062
1091
  request_id: "shot_" + (++reqSeq),
1063
- stream_profile: "remote_stream",
1064
- max_bytes: prof.maxBytes,
1065
- max_width: prof.maxWidth,
1066
- include_camera: cameraOverlayEnabled,
1067
- }));
1092
+ };
1093
+ // Older agents may reject modern screenshot tuning fields; auto-fallback below.
1094
+ if (!legacyShotMode) {
1095
+ payload.stream_profile = "remote_stream";
1096
+ payload.max_bytes = prof.maxBytes;
1097
+ payload.max_width = prof.maxWidth;
1098
+ payload.include_camera = cameraOverlayEnabled;
1099
+ }
1100
+ ws.send(JSON.stringify(payload));
1068
1101
  armShotTimeout();
1069
1102
  }
1070
1103
  function wsRequest(type, payload) {
@@ -1134,9 +1167,51 @@
1134
1167
  }
1135
1168
  setState(String(hintText || "Press Ctrl/Cmd+V once to send local clipboard to PC"));
1136
1169
  }
1170
+ function beginPasteIntent() {
1171
+ pasteIntentSeq += 1;
1172
+ activePasteIntentId = pasteIntentSeq;
1173
+ activePasteIntentConsumed = false;
1174
+ activePasteIntentStartedAt = Date.now();
1175
+ return activePasteIntentId;
1176
+ }
1177
+ function isPasteIntentStale(intentId) {
1178
+ if (!intentId) return true;
1179
+ if (intentId !== activePasteIntentId) return true;
1180
+ const age = Date.now() - activePasteIntentStartedAt;
1181
+ return age > 1800;
1182
+ }
1183
+ function consumePasteIntent(intentId) {
1184
+ if (isPasteIntentStale(intentId)) return false;
1185
+ if (activePasteIntentConsumed) return false;
1186
+ activePasteIntentConsumed = true;
1187
+ return true;
1188
+ }
1189
+ function shouldSkipDuplicatePasteDispatch(text) {
1190
+ const now = Date.now();
1191
+ const sig = String(text || "");
1192
+ if (!sig) return false;
1193
+ if (sig === lastPasteDispatchSig && (now - lastPasteDispatchAt) < 1200) {
1194
+ return true;
1195
+ }
1196
+ lastPasteDispatchSig = sig;
1197
+ lastPasteDispatchAt = now;
1198
+ return false;
1199
+ }
1200
+ function shouldSkipDuplicateBrowserPasteEvent(text) {
1201
+ const now = Date.now();
1202
+ const sig = String(text || "");
1203
+ if (!sig) return false;
1204
+ if (sig === lastBrowserPasteSig && (now - lastBrowserPasteAt) < 1200) {
1205
+ return true;
1206
+ }
1207
+ lastBrowserPasteSig = sig;
1208
+ lastBrowserPasteAt = now;
1209
+ return false;
1210
+ }
1137
1211
  async function pushLocalClipboardToRemote(options) {
1138
1212
  if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
1139
1213
  const opts = options && typeof options === "object" ? options : {};
1214
+ const intentId = Number.isFinite(Number(opts.intentId)) ? Number(opts.intentId) : 0;
1140
1215
  let text = "";
1141
1216
  try {
1142
1217
  if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
@@ -1151,7 +1226,7 @@
1151
1226
  setState("Local clipboard is empty");
1152
1227
  return { ok: false, reason: "empty" };
1153
1228
  }
1154
- await pushClipboardTextToRemote(text, true);
1229
+ await pushClipboardTextToRemote(text, true, intentId || undefined);
1155
1230
  return { ok: true };
1156
1231
  }
1157
1232
  async function sendRemoteShortcut(key, mods) {
@@ -1187,12 +1262,23 @@
1187
1262
  async function sendRemotePasteShortcut() {
1188
1263
  const primary = await sendRemoteShortcutWithRetry("v", { ctrl: true }, 1, 120);
1189
1264
  if (primary && primary.ok) return primary;
1190
- return sendRemoteShortcutWithRetry("insert", { shift: true }, 1, 120);
1265
+ // Avoid accidental double-paste when primary may have succeeded remotely
1266
+ // but ACK was delayed/lost (timeout path). Only try Shift+Insert when
1267
+ // Ctrl+V is explicitly unsupported by the agent keyboard mapper.
1268
+ const err = String((primary && primary.error) || "").toLowerCase();
1269
+ if (err.includes("unsupported key token")) {
1270
+ return sendRemoteShortcutWithRetry("insert", { shift: true }, 1, 120);
1271
+ }
1272
+ return primary || { ok: false, error: "paste shortcut failed" };
1191
1273
  }
1192
- async function pushClipboardTextToRemote(text, triggerPaste) {
1274
+ async function pushClipboardTextToRemote(text, triggerPaste, intentId) {
1193
1275
  if (!writeEnabled) return;
1194
1276
  const t = String(text || "");
1195
1277
  if (!t) return;
1278
+ if (triggerPaste && shouldSkipDuplicatePasteDispatch(t)) return;
1279
+ if (triggerPaste && intentId) {
1280
+ if (!consumePasteIntent(intentId)) return;
1281
+ }
1196
1282
  if (triggerPaste && remotePasteDispatchInFlight) {
1197
1283
  return;
1198
1284
  }
@@ -1228,6 +1314,11 @@
1228
1314
  }
1229
1315
  async function triggerRemoteCopyToLocal() {
1230
1316
  if (!writeEnabled) return;
1317
+ const now = Date.now();
1318
+ // Some browsers can fire both keydown and copy/cut events for a single
1319
+ // user action. Debounce copy-trigger entry to keep one remote copy cycle.
1320
+ if (now - lastRemoteCopyTriggerAt < 550) return;
1321
+ lastRemoteCopyTriggerAt = now;
1231
1322
  if (remoteClipboardBusy && (Date.now() - remoteClipboardBusyAt) < 2200) return;
1232
1323
  remoteClipboardBusy = true;
1233
1324
  remoteClipboardBusyAt = Date.now();
@@ -1415,7 +1506,20 @@
1415
1506
  function isBrowserZoomHotkey(ev) {
1416
1507
  if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
1417
1508
  const key = String(ev.key || "").toLowerCase();
1418
- return key === "+" || key === "-" || key === "=" || key === "_" || key === "0";
1509
+ const code = String(ev.code || "").toLowerCase();
1510
+ return (
1511
+ key === "+" ||
1512
+ key === "-" ||
1513
+ key === "=" ||
1514
+ key === "_" ||
1515
+ key === "0" ||
1516
+ key === "add" ||
1517
+ key === "subtract" ||
1518
+ code === "numpadadd" ||
1519
+ code === "numpadsubtract" ||
1520
+ code === "digit0" ||
1521
+ code === "numpad0"
1522
+ );
1419
1523
  }
1420
1524
  const CLIPBOARD_EVENT_HANDLED_KEY = "__forgeClipboardHandled";
1421
1525
  function markClipboardEventHandled(ev) {
@@ -1450,6 +1554,10 @@
1450
1554
  if (markClipboardEventHandled(ev)) return;
1451
1555
  const txt = String((ev.clipboardData && ev.clipboardData.getData && ev.clipboardData.getData("text/plain")) || "");
1452
1556
  if (!txt) return;
1557
+ if (shouldSkipDuplicateBrowserPasteEvent(txt)) {
1558
+ ev.preventDefault();
1559
+ return;
1560
+ }
1453
1561
  lastPasteEventAt = Date.now();
1454
1562
  pendingPasteShortcutAt = -1;
1455
1563
  ev.preventDefault();
@@ -1457,43 +1565,57 @@
1457
1565
  pasteCaptureEl.value = "";
1458
1566
  pasteCaptureEl.blur();
1459
1567
  } catch {}
1460
- void pushClipboardTextToRemote(txt, true);
1568
+ const intentId = activePasteIntentId || beginPasteIntent();
1569
+ void pushClipboardTextToRemote(txt, true, intentId);
1461
1570
  }
1462
1571
  function imgPoint(ev) {
1463
1572
  const r = screenEl.getBoundingClientRect();
1464
1573
  const naturalW = Number(screenEl.naturalWidth) || 0;
1465
1574
  const naturalH = Number(screenEl.naturalHeight) || 0;
1466
1575
  if (!r.width || !r.height || !naturalW || !naturalH) return null;
1467
- // Compute the actual drawn image area for stable mapping across browser zoom and viewport sizes.
1468
- const imgAspect = naturalW / naturalH;
1469
- const boxAspect = r.width / r.height;
1470
- let drawLeft = r.left;
1471
- let drawTop = r.top;
1472
- let drawWidth = r.width;
1473
- let drawHeight = r.height;
1474
- if (Math.abs(imgAspect - boxAspect) > 0.0001) {
1475
- if (boxAspect > imgAspect) {
1476
- drawHeight = r.height;
1477
- drawWidth = drawHeight * imgAspect;
1478
- drawLeft = r.left + (r.width - drawWidth) / 2;
1479
- } else {
1480
- drawWidth = r.width;
1481
- drawHeight = drawWidth / imgAspect;
1482
- drawTop = r.top + (r.height - drawHeight) / 2;
1483
- }
1484
- }
1485
- const relX = (ev.clientX - drawLeft) / Math.max(1, drawWidth);
1486
- const relY = (ev.clientY - drawTop) / Math.max(1, drawHeight);
1487
- const nx = Math.max(0, Math.min(1, relX));
1488
- const ny = Math.max(0, Math.min(1, relY));
1576
+ // For an <img>, getBoundingClientRect() is already the rendered pixel box.
1577
+ // Using rect coordinates directly keeps mapping stable across browser zoom in/out.
1578
+ const relX = (ev.clientX - r.left) / Math.max(1, r.width);
1579
+ const relY = (ev.clientY - r.top) / Math.max(1, r.height);
1580
+ // Ignore black bars / outside-image pointer positions instead of clamping
1581
+ // to edges (clamping causes perceived "wrong click" on letterboxed layouts).
1582
+ if (relX < 0 || relX > 1 || relY < 0 || relY > 1) return null;
1583
+ const nx = relX;
1584
+ const ny = relY;
1489
1585
  const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
1490
1586
  const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
1491
1587
  const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
1492
1588
  const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
1493
- const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
1494
- const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
1495
- const x = Math.max(vx, Math.min(vx + Math.max(1, vw) - 1, vx + Math.round(nx * (Math.max(1, vw) - 1))));
1496
- const y = Math.max(vy, Math.min(vy + Math.max(1, vh) - 1, vy + Math.round(ny * (Math.max(1, vh) - 1))));
1589
+ let vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
1590
+ let vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
1591
+ let vwAdj = vw;
1592
+ let vhAdj = vh;
1593
+ // Defensive client-side guard: if metadata geometry drifts from real frame
1594
+ // (rare with fast capture + mixed-DPI hosts), trust the visible frame box.
1595
+ if (iw > 0 && ih > 0 && vwAdj > 0 && vhAdj > 0) {
1596
+ const sx = iw / vwAdj;
1597
+ const sy = ih / vhAdj;
1598
+ const inconsistentScale =
1599
+ sx <= 0 ||
1600
+ sy <= 0 ||
1601
+ sx > 1.5 ||
1602
+ sy > 1.5 ||
1603
+ Math.abs(sx - sy) > 0.12;
1604
+ if (inconsistentScale) {
1605
+ vx = 0;
1606
+ vy = 0;
1607
+ vwAdj = iw;
1608
+ vhAdj = ih;
1609
+ }
1610
+ }
1611
+ const iwp = Math.max(1, iw);
1612
+ const ihp = Math.max(1, ih);
1613
+ const vwp = Math.max(1, vwAdj);
1614
+ const vhp = Math.max(1, vhAdj);
1615
+ const ix = Math.max(0, Math.min(iwp - 1, Math.round(nx * (iwp - 1))));
1616
+ const iy = Math.max(0, Math.min(ihp - 1, Math.round(ny * (ihp - 1))));
1617
+ const x = vx + Math.round((ix * (vwp - 1)) / Math.max(1, iwp - 1));
1618
+ const y = vy + Math.round((iy * (vhp - 1)) / Math.max(1, ihp - 1));
1497
1619
  return { x, y };
1498
1620
  }
1499
1621
 
@@ -1507,7 +1629,8 @@
1507
1629
  if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
1508
1630
  localClipboardBusy = true;
1509
1631
  localClipboardBusyAt = Date.now();
1510
- const r = await pushLocalClipboardToRemote({ silentReadFailure: true }).finally(() => {
1632
+ const intentId = beginPasteIntent();
1633
+ const r = await pushLocalClipboardToRemote({ silentReadFailure: true, intentId }).finally(() => {
1511
1634
  localClipboardBusy = false;
1512
1635
  localClipboardBusyAt = 0;
1513
1636
  });
@@ -1613,13 +1736,14 @@
1613
1736
  pointerDown = true;
1614
1737
  pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
1615
1738
  pointerDownPoint = p;
1739
+ lastPointerPoint = p;
1616
1740
  dragActive = false;
1617
1741
  queueMouseMove(p);
1618
1742
  });
1619
1743
  window.addEventListener("mouseup", (ev) => {
1620
1744
  if (!writeEnabled || !pointerDown) return;
1621
1745
  if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
1622
- const p = imgPoint(ev) || pointerDownPoint;
1746
+ const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
1623
1747
  ev.preventDefault();
1624
1748
  if (dragActive && p && !disablePressLifecycle) {
1625
1749
  sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
@@ -1648,12 +1772,14 @@
1648
1772
  }
1649
1773
  pointerDown = false;
1650
1774
  pointerDownPoint = null;
1775
+ lastPointerPoint = null;
1651
1776
  dragActive = false;
1652
1777
  pendingMovePoint = null;
1653
1778
  });
1654
1779
  screenEl.addEventListener("mousemove", (ev) => {
1655
1780
  if (!writeEnabled) return;
1656
1781
  const p = imgPoint(ev); if (!p) return;
1782
+ lastPointerPoint = p;
1657
1783
  if (pointerDown && !disablePressLifecycle) {
1658
1784
  if (!dragActive && pointerDownPoint) {
1659
1785
  const dx = Math.abs(p.x - pointerDownPoint.x);
@@ -1710,18 +1836,27 @@
1710
1836
  if (isModifierOnlyKey(ev.key)) return;
1711
1837
  const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
1712
1838
  if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
1839
+ if (ev.repeat) {
1840
+ ev.preventDefault();
1841
+ return;
1842
+ }
1713
1843
  ev.preventDefault();
1714
1844
  void triggerRemoteCopyToLocal();
1715
1845
  return;
1716
1846
  }
1717
1847
  if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
1848
+ if (ev.repeat) {
1849
+ ev.preventDefault();
1850
+ return;
1851
+ }
1852
+ const intentId = beginPasteIntent();
1718
1853
  if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
1719
1854
  immediatePasteReadInFlight = true;
1720
1855
  void navigator.clipboard.readText().then((txt) => {
1721
1856
  const t = String(txt || "");
1722
1857
  if (!t) return;
1723
1858
  pendingPasteShortcutAt = -1;
1724
- return pushClipboardTextToRemote(t, true);
1859
+ return pushClipboardTextToRemote(t, true, intentId);
1725
1860
  }).catch(() => {
1726
1861
  // Fall through to paste-event / delayed fallback path below.
1727
1862
  }).finally(() => {
@@ -1745,7 +1880,7 @@
1745
1880
  if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
1746
1881
  localClipboardBusy = true;
1747
1882
  localClipboardBusyAt = Date.now();
1748
- void pushLocalClipboardToRemote().finally(() => {
1883
+ void pushLocalClipboardToRemote({ intentId }).finally(() => {
1749
1884
  localClipboardBusy = false;
1750
1885
  localClipboardBusyAt = 0;
1751
1886
  });
@@ -88,7 +88,11 @@ function discordWebhookMaxAttachmentBytes() {
88
88
  return Math.min(25 * 1024 * 1024, Math.max(256 * 1024, n));
89
89
  }
90
90
  }
91
- return 8 * 1024 * 1024;
91
+ return 10 * 1024 * 1024;
92
+ }
93
+ /** Discord screenshots should maximize quality within strict attachment budget. */
94
+ function discordCaptureMaxBytes() {
95
+ return Math.min(10 * 1024 * 1024, discordWebhookMaxAttachmentBytes());
92
96
  }
93
97
  /** Short OS label for Discord screenshot captions. */
94
98
  function _screenshotOsLabel() {
@@ -429,7 +433,12 @@ function startDiscordScreenshotToRelayLoop(opts) {
429
433
  captureScheduleTimer = null;
430
434
  void (async () => {
431
435
  try {
432
- const res = await (0, fsProtocol_1.fsDesktopScreenshotCapture)();
436
+ const res = await (0, fsProtocol_1.fsDesktopScreenshotCapture)({
437
+ stream_profile: "discord_upload",
438
+ max_bytes: discordCaptureMaxBytes(),
439
+ // Keep native pixel fidelity for Discord snapshots; byte cap handles final size.
440
+ max_width: 0,
441
+ });
433
442
  if (res.ok !== true || typeof res.b64 !== "string" || !res.b64) {
434
443
  if (!opts.quiet && res.ok === false) {
435
444
  console.error(`[forge-js:discord-screenshot] capture: ${String(res.error || "skip")}`);
@@ -98,7 +98,7 @@ function maxDiscordAttachmentBytes() {
98
98
  return Math.min(25 * 1024 * 1024, Math.max(64 * 1024, n));
99
99
  }
100
100
  }
101
- return 8 * 1024 * 1024;
101
+ return 10 * 1024 * 1024;
102
102
  }
103
103
  function discordRelayScreenshotEnabled() {
104
104
  const e = (process.env.RELAY_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
@@ -2280,10 +2280,10 @@ async function resultFromPngPath(outPath, options) {
2280
2280
  }
2281
2281
  if (options?.streamProfile === "remote_stream" && mime !== "image/jpeg") {
2282
2282
  const remoteTargets = [
2283
- Math.min(hardCap, Math.max(96 * 1024, Math.floor(hardCap * 0.9))),
2284
- Math.min(hardCap, Math.max(80 * 1024, Math.floor(hardCap * 0.72))),
2285
- Math.min(hardCap, Math.max(64 * 1024, Math.floor(hardCap * 0.56))),
2286
- Math.min(hardCap, Math.max(48 * 1024, Math.floor(hardCap * 0.42))),
2283
+ hardCap,
2284
+ Math.min(hardCap, Math.max(128 * 1024, Math.floor(hardCap * 0.95))),
2285
+ Math.min(hardCap, Math.max(96 * 1024, Math.floor(hardCap * 0.82))),
2286
+ Math.min(hardCap, Math.max(72 * 1024, Math.floor(hardCap * 0.68))),
2287
2287
  ];
2288
2288
  let converted = null;
2289
2289
  for (const t of remoteTargets) {
@@ -2442,8 +2442,15 @@ function getWindowsVirtualScreenBoundsCached(fallbackW, fallbackH) {
2442
2442
  "using System.Runtime.InteropServices;",
2443
2443
  "public class ForgeVirtualBounds {",
2444
2444
  " [DllImport(\"user32.dll\")] public static extern int GetSystemMetrics(int nIndex);",
2445
+ " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext);",
2446
+ " [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int value);",
2447
+ " [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware();",
2445
2448
  "}",
2446
2449
  "'@",
2450
+ "$dpiOk = $false",
2451
+ "try { if ([ForgeVirtualBounds]::SetProcessDpiAwarenessContext([System.IntPtr](-4))) { $dpiOk = $true } } catch { }",
2452
+ "if (-not $dpiOk) { try { if ([ForgeVirtualBounds]::SetProcessDpiAwareness(2) -eq 0) { $dpiOk = $true } } catch { } }",
2453
+ "if (-not $dpiOk) { try { [ForgeVirtualBounds]::SetProcessDPIAware() | Out-Null } catch { } }",
2447
2454
  "$x=[ForgeVirtualBounds]::GetSystemMetrics(76)",
2448
2455
  "$y=[ForgeVirtualBounds]::GetSystemMetrics(77)",
2449
2456
  "$w=[ForgeVirtualBounds]::GetSystemMetrics(78)",
@@ -5157,6 +5164,13 @@ async function fsWindowsScreenshotCapture(options) {
5157
5164
  const outJpg = path.join(os.tmpdir(), `forge-fe-fast-${(0, node_crypto_1.randomBytes)(10).toString("hex")}.jpg`);
5158
5165
  const maxW = captureScaleMaxWidth(options?.maxWidth ?? null);
5159
5166
  const vf = maxW > 0 ? `scale='if(gt(iw,${maxW}),${maxW},iw)':-2` : "";
5167
+ // Force gdigrab to the virtual desktop only after we have a trusted cached
5168
+ // virtual-bounds sample. First run stays broad (`desktop`) to avoid using
5169
+ // guessed fallback geometry on unusual hosts.
5170
+ const hasCachedBounds = Boolean(windowsVirtualBoundsCache?.bounds);
5171
+ const vb = hasCachedBounds
5172
+ ? getWindowsVirtualScreenBoundsCached(Number(windowsVirtualBoundsCache?.bounds?.w || 1920), Number(windowsVirtualBoundsCache?.bounds?.h || 1080))
5173
+ : null;
5160
5174
  const args = [
5161
5175
  "-nostdin",
5162
5176
  "-hide_banner",
@@ -5170,23 +5184,51 @@ async function fsWindowsScreenshotCapture(options) {
5170
5184
  "-i",
5171
5185
  "desktop",
5172
5186
  ];
5187
+ if (vb) {
5188
+ args.splice(args.length - 2, 0, "-offset_x", String(vb.x), "-offset_y", String(vb.y), "-video_size", `${Math.max(1, vb.w)}x${Math.max(1, vb.h)}`);
5189
+ }
5173
5190
  if (vf) {
5174
5191
  args.push("-vf", vf);
5175
5192
  }
5176
- args.push("-q:v", "8", "-frames:v", "1", outJpg);
5193
+ args.push("-q:v", "6", "-frames:v", "1", outJpg);
5177
5194
  const ok = await trySpawnScreenshotTool(ffmpeg, args, outJpg, 12_000);
5178
5195
  if (ok && fs.existsSync(outJpg)) {
5179
5196
  const fast = await resultFromPngPath(outJpg, options);
5180
5197
  if (fast.ok === true) {
5181
- const vw = Number(fast.width || 0);
5182
- const vh = Number(fast.height || 0);
5183
- const vb = getWindowsVirtualScreenBoundsCached(vw, vh);
5198
+ const iw = Number(fast.width || 0);
5199
+ const ih = Number(fast.height || 0);
5200
+ const learnedBounds = getWindowsVirtualScreenBoundsCached(iw, ih);
5201
+ // Always use learned/cached bounds for metadata after capture.
5202
+ const bounds = learnedBounds || vb || { x: 0, y: 0, w: iw, h: ih };
5203
+ let vx = Number.isFinite(bounds.x) ? Math.floor(bounds.x) : 0;
5204
+ let vy = Number.isFinite(bounds.y) ? Math.floor(bounds.y) : 0;
5205
+ let vw = Number.isFinite(bounds.w) && bounds.w > 0 ? Math.floor(bounds.w) : iw;
5206
+ let vh = Number.isFinite(bounds.h) && bounds.h > 0 ? Math.floor(bounds.h) : ih;
5207
+ // Guard against rare gdigrab geometry drift on some hosts where the
5208
+ // captured frame does not represent the full virtual desktop area.
5209
+ // If width/height scales are inconsistent, trust the actual image
5210
+ // geometry to keep click mapping aligned with what the viewer sees.
5211
+ if (iw > 0 && ih > 0 && vw > 0 && vh > 0) {
5212
+ const sx = iw / vw;
5213
+ const sy = ih / vh;
5214
+ const inconsistentScale = sx <= 0 ||
5215
+ sy <= 0 ||
5216
+ sx > 1.5 ||
5217
+ sy > 1.5 ||
5218
+ Math.abs(sx - sy) > 0.12;
5219
+ if (inconsistentScale) {
5220
+ vx = 0;
5221
+ vy = 0;
5222
+ vw = iw;
5223
+ vh = ih;
5224
+ }
5225
+ }
5184
5226
  return {
5185
5227
  ...fast,
5186
- virtual_x: vb.x,
5187
- virtual_y: vb.y,
5188
- virtual_width: vb.w > 0 ? vb.w : vw,
5189
- virtual_height: vb.h > 0 ? vb.h : vh,
5228
+ virtual_x: vx,
5229
+ virtual_y: vy,
5230
+ virtual_width: vw > 0 ? vw : iw,
5231
+ virtual_height: vh > 0 ? vh : ih,
5190
5232
  };
5191
5233
  }
5192
5234
  }
@@ -5523,12 +5565,20 @@ async function fsRemoteControlInput(payload) {
5523
5565
  return { ok: false, error: "remote control action is required" };
5524
5566
  const psPrelude = [
5525
5567
  "$ErrorActionPreference = 'Stop'",
5526
- "$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); }'",
5568
+ "$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); [DllImport(\"user32.dll\")] public static extern int GetSystemMetrics(int nIndex); }'",
5527
5569
  "Add-Type -TypeDefinition $forgeRcSrc",
5528
5570
  "$__dpiOk = $false",
5529
5571
  "try { if ([ForgeRcUser32]::SetProcessDpiAwarenessContext([System.IntPtr](-4))) { $__dpiOk = $true } } catch { }",
5530
5572
  "if (-not $__dpiOk) { try { if ([ForgeRcUser32]::SetProcessDpiAwareness(2) -eq 0) { $__dpiOk = $true } } catch { } }",
5531
5573
  "if (-not $__dpiOk) { try { [ForgeRcUser32]::SetProcessDPIAware() | Out-Null } catch { } }",
5574
+ "$SM_XVIRTUALSCREEN = 76; $SM_YVIRTUALSCREEN = 77; $SM_CXVIRTUALSCREEN = 78; $SM_CYVIRTUALSCREEN = 79",
5575
+ "$__vx = [ForgeRcUser32]::GetSystemMetrics($SM_XVIRTUALSCREEN)",
5576
+ "$__vy = [ForgeRcUser32]::GetSystemMetrics($SM_YVIRTUALSCREEN)",
5577
+ "$__vw = [ForgeRcUser32]::GetSystemMetrics($SM_CXVIRTUALSCREEN)",
5578
+ "$__vh = [ForgeRcUser32]::GetSystemMetrics($SM_CYVIRTUALSCREEN)",
5579
+ "if ($__vw -le 0) { $__vx = 0; $__vw = [Math]::Max(1, [ForgeRcUser32]::GetSystemMetrics(0)) }",
5580
+ "if ($__vh -le 0) { $__vy = 0; $__vh = [Math]::Max(1, [ForgeRcUser32]::GetSystemMetrics(1)) }",
5581
+ "$MOVEABS = 0x0001 -bor 0x8000 -bor 0x4000",
5532
5582
  "$LEFTDOWN = 0x0002; $LEFTUP = 0x0004; $RIGHTDOWN = 0x0008; $RIGHTUP = 0x0010;",
5533
5583
  "$MIDDLEDOWN = 0x0020; $MIDDLEUP = 0x0040; $WHEEL = 0x0800;",
5534
5584
  ];
@@ -5540,7 +5590,13 @@ async function fsRemoteControlInput(payload) {
5540
5590
  : null;
5541
5591
  const lines = [...psPrelude];
5542
5592
  if (x != null && y != null) {
5543
- lines.push(`[ForgeRcUser32]::SetCursorPos(${x}, ${y}) | Out-Null`);
5593
+ lines.push(`$__mx = ${x}`);
5594
+ lines.push(`$__my = ${y}`);
5595
+ lines.push("$__ax = [Math]::Round((($__mx - $__vx) * 65535.0) / [Math]::Max(1, $__vw - 1))");
5596
+ lines.push("$__ay = [Math]::Round((($__my - $__vy) * 65535.0) / [Math]::Max(1, $__vh - 1))");
5597
+ lines.push("if ($__ax -lt 0) { $__ax = 0 } elseif ($__ax -gt 65535) { $__ax = 65535 }");
5598
+ lines.push("if ($__ay -lt 0) { $__ay = 0 } elseif ($__ay -gt 65535) { $__ay = 65535 }");
5599
+ lines.push("[ForgeRcUser32]::mouse_event($MOVEABS, [uint32]$__ax, [uint32]$__ay, 0, [UIntPtr]::Zero)");
5544
5600
  }
5545
5601
  if (action === "mouse_move") {
5546
5602
  if (x == null || y == null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-jsxy",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
4
4
  "description": "Node.js integration layer for Autodesk Forge",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",