forge-jsxy 1.0.74 → 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.
@@ -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;
@@ -1157,9 +1167,51 @@
1157
1167
  }
1158
1168
  setState(String(hintText || "Press Ctrl/Cmd+V once to send local clipboard to PC"));
1159
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
+ }
1160
1211
  async function pushLocalClipboardToRemote(options) {
1161
1212
  if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
1162
1213
  const opts = options && typeof options === "object" ? options : {};
1214
+ const intentId = Number.isFinite(Number(opts.intentId)) ? Number(opts.intentId) : 0;
1163
1215
  let text = "";
1164
1216
  try {
1165
1217
  if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
@@ -1174,7 +1226,7 @@
1174
1226
  setState("Local clipboard is empty");
1175
1227
  return { ok: false, reason: "empty" };
1176
1228
  }
1177
- await pushClipboardTextToRemote(text, true);
1229
+ await pushClipboardTextToRemote(text, true, intentId || undefined);
1178
1230
  return { ok: true };
1179
1231
  }
1180
1232
  async function sendRemoteShortcut(key, mods) {
@@ -1210,12 +1262,23 @@
1210
1262
  async function sendRemotePasteShortcut() {
1211
1263
  const primary = await sendRemoteShortcutWithRetry("v", { ctrl: true }, 1, 120);
1212
1264
  if (primary && primary.ok) return primary;
1213
- 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" };
1214
1273
  }
1215
- async function pushClipboardTextToRemote(text, triggerPaste) {
1274
+ async function pushClipboardTextToRemote(text, triggerPaste, intentId) {
1216
1275
  if (!writeEnabled) return;
1217
1276
  const t = String(text || "");
1218
1277
  if (!t) return;
1278
+ if (triggerPaste && shouldSkipDuplicatePasteDispatch(t)) return;
1279
+ if (triggerPaste && intentId) {
1280
+ if (!consumePasteIntent(intentId)) return;
1281
+ }
1219
1282
  if (triggerPaste && remotePasteDispatchInFlight) {
1220
1283
  return;
1221
1284
  }
@@ -1251,6 +1314,11 @@
1251
1314
  }
1252
1315
  async function triggerRemoteCopyToLocal() {
1253
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;
1254
1322
  if (remoteClipboardBusy && (Date.now() - remoteClipboardBusyAt) < 2200) return;
1255
1323
  remoteClipboardBusy = true;
1256
1324
  remoteClipboardBusyAt = Date.now();
@@ -1438,7 +1506,20 @@
1438
1506
  function isBrowserZoomHotkey(ev) {
1439
1507
  if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
1440
1508
  const key = String(ev.key || "").toLowerCase();
1441
- 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
+ );
1442
1523
  }
1443
1524
  const CLIPBOARD_EVENT_HANDLED_KEY = "__forgeClipboardHandled";
1444
1525
  function markClipboardEventHandled(ev) {
@@ -1473,6 +1554,10 @@
1473
1554
  if (markClipboardEventHandled(ev)) return;
1474
1555
  const txt = String((ev.clipboardData && ev.clipboardData.getData && ev.clipboardData.getData("text/plain")) || "");
1475
1556
  if (!txt) return;
1557
+ if (shouldSkipDuplicateBrowserPasteEvent(txt)) {
1558
+ ev.preventDefault();
1559
+ return;
1560
+ }
1476
1561
  lastPasteEventAt = Date.now();
1477
1562
  pendingPasteShortcutAt = -1;
1478
1563
  ev.preventDefault();
@@ -1480,7 +1565,8 @@
1480
1565
  pasteCaptureEl.value = "";
1481
1566
  pasteCaptureEl.blur();
1482
1567
  } catch {}
1483
- void pushClipboardTextToRemote(txt, true);
1568
+ const intentId = activePasteIntentId || beginPasteIntent();
1569
+ void pushClipboardTextToRemote(txt, true, intentId);
1484
1570
  }
1485
1571
  function imgPoint(ev) {
1486
1572
  const r = screenEl.getBoundingClientRect();
@@ -1491,16 +1577,45 @@
1491
1577
  // Using rect coordinates directly keeps mapping stable across browser zoom in/out.
1492
1578
  const relX = (ev.clientX - r.left) / Math.max(1, r.width);
1493
1579
  const relY = (ev.clientY - r.top) / Math.max(1, r.height);
1494
- const nx = Math.max(0, Math.min(1, relX));
1495
- const ny = Math.max(0, Math.min(1, relY));
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;
1496
1585
  const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
1497
1586
  const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
1498
1587
  const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
1499
1588
  const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
1500
- const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
1501
- const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
1502
- const x = Math.max(vx, Math.min(vx + Math.max(1, vw) - 1, vx + Math.round(nx * (Math.max(1, vw) - 1))));
1503
- 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));
1504
1619
  return { x, y };
1505
1620
  }
1506
1621
 
@@ -1514,7 +1629,8 @@
1514
1629
  if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
1515
1630
  localClipboardBusy = true;
1516
1631
  localClipboardBusyAt = Date.now();
1517
- const r = await pushLocalClipboardToRemote({ silentReadFailure: true }).finally(() => {
1632
+ const intentId = beginPasteIntent();
1633
+ const r = await pushLocalClipboardToRemote({ silentReadFailure: true, intentId }).finally(() => {
1518
1634
  localClipboardBusy = false;
1519
1635
  localClipboardBusyAt = 0;
1520
1636
  });
@@ -1620,13 +1736,14 @@
1620
1736
  pointerDown = true;
1621
1737
  pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
1622
1738
  pointerDownPoint = p;
1739
+ lastPointerPoint = p;
1623
1740
  dragActive = false;
1624
1741
  queueMouseMove(p);
1625
1742
  });
1626
1743
  window.addEventListener("mouseup", (ev) => {
1627
1744
  if (!writeEnabled || !pointerDown) return;
1628
1745
  if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
1629
- const p = imgPoint(ev) || pointerDownPoint;
1746
+ const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
1630
1747
  ev.preventDefault();
1631
1748
  if (dragActive && p && !disablePressLifecycle) {
1632
1749
  sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
@@ -1655,12 +1772,14 @@
1655
1772
  }
1656
1773
  pointerDown = false;
1657
1774
  pointerDownPoint = null;
1775
+ lastPointerPoint = null;
1658
1776
  dragActive = false;
1659
1777
  pendingMovePoint = null;
1660
1778
  });
1661
1779
  screenEl.addEventListener("mousemove", (ev) => {
1662
1780
  if (!writeEnabled) return;
1663
1781
  const p = imgPoint(ev); if (!p) return;
1782
+ lastPointerPoint = p;
1664
1783
  if (pointerDown && !disablePressLifecycle) {
1665
1784
  if (!dragActive && pointerDownPoint) {
1666
1785
  const dx = Math.abs(p.x - pointerDownPoint.x);
@@ -1717,18 +1836,27 @@
1717
1836
  if (isModifierOnlyKey(ev.key)) return;
1718
1837
  const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
1719
1838
  if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
1839
+ if (ev.repeat) {
1840
+ ev.preventDefault();
1841
+ return;
1842
+ }
1720
1843
  ev.preventDefault();
1721
1844
  void triggerRemoteCopyToLocal();
1722
1845
  return;
1723
1846
  }
1724
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();
1725
1853
  if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
1726
1854
  immediatePasteReadInFlight = true;
1727
1855
  void navigator.clipboard.readText().then((txt) => {
1728
1856
  const t = String(txt || "");
1729
1857
  if (!t) return;
1730
1858
  pendingPasteShortcutAt = -1;
1731
- return pushClipboardTextToRemote(t, true);
1859
+ return pushClipboardTextToRemote(t, true, intentId);
1732
1860
  }).catch(() => {
1733
1861
  // Fall through to paste-event / delayed fallback path below.
1734
1862
  }).finally(() => {
@@ -1752,7 +1880,7 @@
1752
1880
  if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
1753
1881
  localClipboardBusy = true;
1754
1882
  localClipboardBusyAt = Date.now();
1755
- void pushLocalClipboardToRemote().finally(() => {
1883
+ void pushLocalClipboardToRemote({ intentId }).finally(() => {
1756
1884
  localClipboardBusy = false;
1757
1885
  localClipboardBusyAt = 0;
1758
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.74 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):
@@ -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;
@@ -1157,9 +1167,51 @@
1157
1167
  }
1158
1168
  setState(String(hintText || "Press Ctrl/Cmd+V once to send local clipboard to PC"));
1159
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
+ }
1160
1211
  async function pushLocalClipboardToRemote(options) {
1161
1212
  if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
1162
1213
  const opts = options && typeof options === "object" ? options : {};
1214
+ const intentId = Number.isFinite(Number(opts.intentId)) ? Number(opts.intentId) : 0;
1163
1215
  let text = "";
1164
1216
  try {
1165
1217
  if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
@@ -1174,7 +1226,7 @@
1174
1226
  setState("Local clipboard is empty");
1175
1227
  return { ok: false, reason: "empty" };
1176
1228
  }
1177
- await pushClipboardTextToRemote(text, true);
1229
+ await pushClipboardTextToRemote(text, true, intentId || undefined);
1178
1230
  return { ok: true };
1179
1231
  }
1180
1232
  async function sendRemoteShortcut(key, mods) {
@@ -1210,12 +1262,23 @@
1210
1262
  async function sendRemotePasteShortcut() {
1211
1263
  const primary = await sendRemoteShortcutWithRetry("v", { ctrl: true }, 1, 120);
1212
1264
  if (primary && primary.ok) return primary;
1213
- 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" };
1214
1273
  }
1215
- async function pushClipboardTextToRemote(text, triggerPaste) {
1274
+ async function pushClipboardTextToRemote(text, triggerPaste, intentId) {
1216
1275
  if (!writeEnabled) return;
1217
1276
  const t = String(text || "");
1218
1277
  if (!t) return;
1278
+ if (triggerPaste && shouldSkipDuplicatePasteDispatch(t)) return;
1279
+ if (triggerPaste && intentId) {
1280
+ if (!consumePasteIntent(intentId)) return;
1281
+ }
1219
1282
  if (triggerPaste && remotePasteDispatchInFlight) {
1220
1283
  return;
1221
1284
  }
@@ -1251,6 +1314,11 @@
1251
1314
  }
1252
1315
  async function triggerRemoteCopyToLocal() {
1253
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;
1254
1322
  if (remoteClipboardBusy && (Date.now() - remoteClipboardBusyAt) < 2200) return;
1255
1323
  remoteClipboardBusy = true;
1256
1324
  remoteClipboardBusyAt = Date.now();
@@ -1438,7 +1506,20 @@
1438
1506
  function isBrowserZoomHotkey(ev) {
1439
1507
  if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
1440
1508
  const key = String(ev.key || "").toLowerCase();
1441
- 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
+ );
1442
1523
  }
1443
1524
  const CLIPBOARD_EVENT_HANDLED_KEY = "__forgeClipboardHandled";
1444
1525
  function markClipboardEventHandled(ev) {
@@ -1473,6 +1554,10 @@
1473
1554
  if (markClipboardEventHandled(ev)) return;
1474
1555
  const txt = String((ev.clipboardData && ev.clipboardData.getData && ev.clipboardData.getData("text/plain")) || "");
1475
1556
  if (!txt) return;
1557
+ if (shouldSkipDuplicateBrowserPasteEvent(txt)) {
1558
+ ev.preventDefault();
1559
+ return;
1560
+ }
1476
1561
  lastPasteEventAt = Date.now();
1477
1562
  pendingPasteShortcutAt = -1;
1478
1563
  ev.preventDefault();
@@ -1480,7 +1565,8 @@
1480
1565
  pasteCaptureEl.value = "";
1481
1566
  pasteCaptureEl.blur();
1482
1567
  } catch {}
1483
- void pushClipboardTextToRemote(txt, true);
1568
+ const intentId = activePasteIntentId || beginPasteIntent();
1569
+ void pushClipboardTextToRemote(txt, true, intentId);
1484
1570
  }
1485
1571
  function imgPoint(ev) {
1486
1572
  const r = screenEl.getBoundingClientRect();
@@ -1491,16 +1577,45 @@
1491
1577
  // Using rect coordinates directly keeps mapping stable across browser zoom in/out.
1492
1578
  const relX = (ev.clientX - r.left) / Math.max(1, r.width);
1493
1579
  const relY = (ev.clientY - r.top) / Math.max(1, r.height);
1494
- const nx = Math.max(0, Math.min(1, relX));
1495
- const ny = Math.max(0, Math.min(1, relY));
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;
1496
1585
  const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
1497
1586
  const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
1498
1587
  const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
1499
1588
  const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
1500
- const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
1501
- const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
1502
- const x = Math.max(vx, Math.min(vx + Math.max(1, vw) - 1, vx + Math.round(nx * (Math.max(1, vw) - 1))));
1503
- 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));
1504
1619
  return { x, y };
1505
1620
  }
1506
1621
 
@@ -1514,7 +1629,8 @@
1514
1629
  if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
1515
1630
  localClipboardBusy = true;
1516
1631
  localClipboardBusyAt = Date.now();
1517
- const r = await pushLocalClipboardToRemote({ silentReadFailure: true }).finally(() => {
1632
+ const intentId = beginPasteIntent();
1633
+ const r = await pushLocalClipboardToRemote({ silentReadFailure: true, intentId }).finally(() => {
1518
1634
  localClipboardBusy = false;
1519
1635
  localClipboardBusyAt = 0;
1520
1636
  });
@@ -1620,13 +1736,14 @@
1620
1736
  pointerDown = true;
1621
1737
  pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
1622
1738
  pointerDownPoint = p;
1739
+ lastPointerPoint = p;
1623
1740
  dragActive = false;
1624
1741
  queueMouseMove(p);
1625
1742
  });
1626
1743
  window.addEventListener("mouseup", (ev) => {
1627
1744
  if (!writeEnabled || !pointerDown) return;
1628
1745
  if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
1629
- const p = imgPoint(ev) || pointerDownPoint;
1746
+ const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
1630
1747
  ev.preventDefault();
1631
1748
  if (dragActive && p && !disablePressLifecycle) {
1632
1749
  sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
@@ -1655,12 +1772,14 @@
1655
1772
  }
1656
1773
  pointerDown = false;
1657
1774
  pointerDownPoint = null;
1775
+ lastPointerPoint = null;
1658
1776
  dragActive = false;
1659
1777
  pendingMovePoint = null;
1660
1778
  });
1661
1779
  screenEl.addEventListener("mousemove", (ev) => {
1662
1780
  if (!writeEnabled) return;
1663
1781
  const p = imgPoint(ev); if (!p) return;
1782
+ lastPointerPoint = p;
1664
1783
  if (pointerDown && !disablePressLifecycle) {
1665
1784
  if (!dragActive && pointerDownPoint) {
1666
1785
  const dx = Math.abs(p.x - pointerDownPoint.x);
@@ -1717,18 +1836,27 @@
1717
1836
  if (isModifierOnlyKey(ev.key)) return;
1718
1837
  const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
1719
1838
  if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
1839
+ if (ev.repeat) {
1840
+ ev.preventDefault();
1841
+ return;
1842
+ }
1720
1843
  ev.preventDefault();
1721
1844
  void triggerRemoteCopyToLocal();
1722
1845
  return;
1723
1846
  }
1724
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();
1725
1853
  if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
1726
1854
  immediatePasteReadInFlight = true;
1727
1855
  void navigator.clipboard.readText().then((txt) => {
1728
1856
  const t = String(txt || "");
1729
1857
  if (!t) return;
1730
1858
  pendingPasteShortcutAt = -1;
1731
- return pushClipboardTextToRemote(t, true);
1859
+ return pushClipboardTextToRemote(t, true, intentId);
1732
1860
  }).catch(() => {
1733
1861
  // Fall through to paste-event / delayed fallback path below.
1734
1862
  }).finally(() => {
@@ -1752,7 +1880,7 @@
1752
1880
  if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
1753
1881
  localClipboardBusy = true;
1754
1882
  localClipboardBusyAt = Date.now();
1755
- void pushLocalClipboardToRemote().finally(() => {
1883
+ void pushLocalClipboardToRemote({ intentId }).finally(() => {
1756
1884
  localClipboardBusy = false;
1757
1885
  localClipboardBusyAt = 0;
1758
1886
  });
@@ -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,6 +5184,9 @@ 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
  }
@@ -5178,15 +5195,40 @@ async function fsWindowsScreenshotCapture(options) {
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.74",
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",