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.
- package/assets/files-explorer-template.html +42 -3
- package/assets/remote-control-template.html +192 -57
- package/dist/assets/files-explorer-template.html +43 -4
- package/dist/assets/remote-control-template.html +192 -57
- package/dist/discordAgentScreenshot.js +11 -2
- package/dist/discordRelayUpload.js +1 -1
- package/dist/fsProtocol.js +70 -14
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
375
|
-
{ maxBytes:
|
|
376
|
-
{ maxBytes:
|
|
377
|
-
{ maxBytes:
|
|
378
|
-
{ maxBytes:
|
|
379
|
-
{ maxBytes:
|
|
380
|
-
{ maxBytes:
|
|
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 <
|
|
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 >=
|
|
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 >
|
|
435
|
-
capMs >
|
|
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 <
|
|
449
|
-
(capMs <= 0 || capMs <
|
|
450
|
-
(tb <= 0 || fb <= tb * 0.
|
|
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 = [
|
|
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
|
-
|
|
1089
|
+
const payload = {
|
|
1061
1090
|
type: "fs_screenshot",
|
|
1062
1091
|
request_id: "shot_" + (++reqSeq),
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
375
|
-
{ maxBytes:
|
|
376
|
-
{ maxBytes:
|
|
377
|
-
{ maxBytes:
|
|
378
|
-
{ maxBytes:
|
|
379
|
-
{ maxBytes:
|
|
380
|
-
{ maxBytes:
|
|
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 <
|
|
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 >=
|
|
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 >
|
|
435
|
-
capMs >
|
|
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 <
|
|
449
|
-
(capMs <= 0 || capMs <
|
|
450
|
-
(tb <= 0 || fb <= tb * 0.
|
|
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 = [
|
|
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
|
-
|
|
1089
|
+
const payload = {
|
|
1061
1090
|
type: "fs_screenshot",
|
|
1062
1091
|
request_id: "shot_" + (++reqSeq),
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
|
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
|
|
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
|
|
101
|
+
return 10 * 1024 * 1024;
|
|
102
102
|
}
|
|
103
103
|
function discordRelayScreenshotEnabled() {
|
|
104
104
|
const e = (process.env.RELAY_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
|
package/dist/fsProtocol.js
CHANGED
|
@@ -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
|
-
|
|
2284
|
-
Math.min(hardCap, Math.max(
|
|
2285
|
-
Math.min(hardCap, Math.max(
|
|
2286
|
-
Math.min(hardCap, Math.max(
|
|
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", "
|
|
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
|
|
5182
|
-
const
|
|
5183
|
-
const
|
|
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:
|
|
5187
|
-
virtual_y:
|
|
5188
|
-
virtual_width:
|
|
5189
|
-
virtual_height:
|
|
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(
|
|
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)
|