forge-jsxy 1.0.73 → 1.0.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1063,6 +1063,9 @@ function scheduleAuthResultWatch(){
1063
1063
  let wantDownloadRid = null, wantDownloadName = '', wantDownloadPath = '', wantDownloadParts = null, wantDownloadTotal = 0;
1064
1064
  let wantFolderZipRid = null, wantFolderZipPath = '', wantFolderZipParts = null, wantFolderZipTotal = 0, wantFolderZipSaveName = '';
1065
1065
  let wantDeleteRid = null;
1066
+ let currentDeletePath = '';
1067
+ let legacyDeleteMode = false;
1068
+ let deleteLegacyCompatRetried = false;
1066
1069
  /** Cleared when `fs_delete_result` / `fs_error` arrives, or after timeout if the agent drops mid-delete. */
1067
1070
  let _deleteWatchTimer = null;
1068
1071
  let wantShellRid = null;
@@ -1473,6 +1476,14 @@ function xferForce(){ const el = $('xfer-force'); return !!(el && el.checked); }
1473
1476
  function xferForceKill(){ const el = $('xfer-force-kill'); return !!(el && el.checked); }
1474
1477
  /** Chunked fs_read / fs_zip mirror options — include on every chunk so download/zip continuations match the first request. */
1475
1478
  function xferStagingOpts(){ return { force: xferForce(), force_kill: xferForceKill() }; }
1479
+ function sendFsDelete(path, requestId){
1480
+ const payload = { type:'fs_delete', path: path, request_id: requestId };
1481
+ if(!legacyDeleteMode){
1482
+ payload.force = xferForce();
1483
+ payload.force_kill = xferForceKill();
1484
+ }
1485
+ send(payload);
1486
+ }
1476
1487
  function ridMatch(a,b){ return String(a||'') === String(b||''); }
1477
1488
  function sendFsRoots(){
1478
1489
  const r = ridn();
@@ -3052,19 +3063,41 @@ function onMsg(m){
3052
3063
  deleteConfirmBulkKey = '';
3053
3064
  try { setXferStatus(''); } catch(eDelXfer){}
3054
3065
  if(!m.ok){
3066
+ const deleteErr = String(m.error || '');
3067
+ const lowDel = deleteErr.toLowerCase();
3068
+ const forceFlagRejected =
3069
+ !legacyDeleteMode &&
3070
+ !deleteLegacyCompatRetried &&
3071
+ (
3072
+ (lowDel.includes('unknown') || lowDel.includes('unsupported') || lowDel.includes('invalid')) &&
3073
+ (lowDel.includes('force_kill') || lowDel.includes('force'))
3074
+ );
3075
+ if(forceFlagRejected && currentDeletePath){
3076
+ legacyDeleteMode = true;
3077
+ deleteLegacyCompatRetried = true;
3078
+ setStatus('Retrying delete in legacy compatibility mode…');
3079
+ const retryRid = ridn();
3080
+ wantDeleteRid = retryRid;
3081
+ armDeleteWatchdog(retryRid);
3082
+ sendFsDelete(currentDeletePath, retryRid);
3083
+ return;
3084
+ }
3055
3085
  if(bulkDeleteActive){
3056
3086
  bulkDeleteActive = false;
3057
3087
  bulkDeleteQueue = [];
3058
3088
  }
3059
3089
  setStatus(m.error||'Delete failed');
3060
3090
  setCerr(m.error||'');
3091
+ currentDeletePath = '';
3061
3092
  return;
3062
3093
  }
3063
3094
  if(bulkDeleteActive && bulkDeleteQueue.length > 0){
3064
3095
  const nextPath = bulkDeleteQueue.shift();
3065
3096
  wantDeleteRid = ridn();
3097
+ currentDeletePath = String(nextPath || '');
3098
+ deleteLegacyCompatRetried = false;
3066
3099
  armDeleteWatchdog(wantDeleteRid);
3067
- send({type:'fs_delete', path: nextPath, request_id: wantDeleteRid, force: xferForce(), force_kill: xferForceKill()});
3100
+ sendFsDelete(nextPath, wantDeleteRid);
3068
3101
  setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' left)');
3069
3102
  setStatus('Deleting…');
3070
3103
  return;
@@ -3072,6 +3105,8 @@ function onMsg(m){
3072
3105
  bulkDeleteActive = false;
3073
3106
  bulkDeleteQueue = [];
3074
3107
  setCerr('');
3108
+ currentDeletePath = '';
3109
+ deleteLegacyCompatRetried = false;
3075
3110
  const deletedPath = m.path ? String(m.path) : '';
3076
3111
  if(deletedPath && wantPreviewPath === deletedPath){
3077
3112
  abortPreview();
@@ -3920,8 +3955,10 @@ function deleteSel(){
3920
3955
  bulkDeleteQueue = [];
3921
3956
  const r = ridn();
3922
3957
  wantDeleteRid = r;
3958
+ currentDeletePath = String(fullPath || '');
3959
+ deleteLegacyCompatRetried = false;
3923
3960
  armDeleteWatchdog(r);
3924
- send({type:'fs_delete', path: fullPath, request_id: r, force: xferForce(), force_kill: xferForceKill()});
3961
+ sendFsDelete(fullPath, r);
3925
3962
  setXferStatus('Deleting on agent…');
3926
3963
  setStatus('Deleting…');
3927
3964
  return;
@@ -3938,8 +3975,10 @@ function deleteSel(){
3938
3975
  bulkDeleteQueue = paths.slice(1);
3939
3976
  const r = ridn();
3940
3977
  wantDeleteRid = r;
3978
+ currentDeletePath = String(paths[0] || '');
3979
+ deleteLegacyCompatRetried = false;
3941
3980
  armDeleteWatchdog(r);
3942
- send({type:'fs_delete', path: paths[0], request_id: r, force: xferForce(), force_kill: xferForceKill()});
3981
+ sendFsDelete(paths[0], r);
3943
3982
  setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' after this)');
3944
3983
  setStatus('Deleting…');
3945
3984
  }
@@ -361,7 +361,9 @@
361
361
  let lastShotStartedAt = 0;
362
362
  let streamFastStreak = 0;
363
363
  let streamSlowStreak = 0;
364
- let streamTier = 2;
364
+ let streamTier = 1;
365
+ let legacyShotMode = false;
366
+ let shotFailureStreak = 0;
365
367
  let fpsFrames = 0;
366
368
  let fpsLastAt = Date.now();
367
369
  let fpsCurrent = 0;
@@ -371,13 +373,13 @@
371
373
  let lastFrameBytes = 0;
372
374
  let lastCaptureMs = 0;
373
375
  const STREAM_TUNING = [
374
- { maxBytes: 1_000_000, maxWidth: 1920 },
375
- { maxBytes: 780_000, maxWidth: 1680 },
376
- { maxBytes: 620_000, maxWidth: 1520 },
377
- { maxBytes: 500_000, maxWidth: 1360 },
378
- { maxBytes: 380_000, maxWidth: 1180 },
379
- { maxBytes: 300_000, maxWidth: 980 },
380
- { maxBytes: 220_000, maxWidth: 840 },
376
+ { maxBytes: 2_400_000, maxWidth: 2560 },
377
+ { maxBytes: 1_900_000, maxWidth: 2240 },
378
+ { maxBytes: 1_500_000, maxWidth: 1920 },
379
+ { maxBytes: 1_150_000, maxWidth: 1680 },
380
+ { maxBytes: 900_000, maxWidth: 1520 },
381
+ { maxBytes: 700_000, maxWidth: 1360 },
382
+ { maxBytes: 520_000, maxWidth: 1180 },
381
383
  ];
382
384
 
383
385
  function setState(t) { stateEl.textContent = t; }
@@ -412,14 +414,14 @@
412
414
  const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
413
415
  const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
414
416
  const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
415
- if (fpsCurrent > 0 && fpsCurrent < 3.8) {
417
+ if (fpsCurrent > 0 && fpsCurrent < 4.0) {
416
418
  fpsLowStreak += 1;
417
419
  fpsHighStreak = 0;
418
420
  if (fpsLowStreak >= 2 && streamTier < STREAM_TUNING.length - 1) {
419
421
  streamTier += 1;
420
422
  fpsLowStreak = 0;
421
423
  }
422
- } else if (fpsCurrent >= 7.8) {
424
+ } else if (fpsCurrent >= 5.1) {
423
425
  fpsHighStreak += 1;
424
426
  fpsLowStreak = 0;
425
427
  if (fpsHighStreak >= 4 && streamTier > 0) {
@@ -431,8 +433,8 @@
431
433
  fpsHighStreak = 0;
432
434
  }
433
435
  const overload =
434
- ms > 280 ||
435
- capMs > 260 ||
436
+ ms > 300 ||
437
+ capMs > 300 ||
436
438
  (tb > 0 && fb > tb * 0.98);
437
439
  if (overload) {
438
440
  streamSlowStreak += 1;
@@ -445,9 +447,9 @@
445
447
  }
446
448
  const healthy =
447
449
  ms > 0 &&
448
- ms < 170 &&
449
- (capMs <= 0 || capMs < 140) &&
450
- (tb <= 0 || fb <= tb * 0.8);
450
+ ms < 220 &&
451
+ (capMs <= 0 || capMs < 180) &&
452
+ (tb <= 0 || fb <= tb * 0.86);
451
453
  if (healthy) {
452
454
  streamFastStreak += 1;
453
455
  streamSlowStreak = 0;
@@ -472,7 +474,7 @@
472
474
  fpsLastAt = now;
473
475
  }
474
476
  function currentShotIntervalMs() {
475
- const m = [110, 130, 155, 185, 220, 255, 290];
477
+ const m = [185, 205, 230, 255, 285, 320, 360];
476
478
  return m[Math.max(0, Math.min(m.length - 1, streamTier))];
477
479
  }
478
480
  function clearShotTimeout() {
@@ -902,6 +904,7 @@
902
904
  refreshCameraBtnUi();
903
905
  }
904
906
  if (msg.ok && msg.b64) {
907
+ shotFailureStreak = 0;
905
908
  if (lastShotStartedAt > 0) {
906
909
  const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
907
910
  lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
@@ -940,6 +943,20 @@
940
943
  hideEmptyState();
941
944
  } else if (!hasFrame) {
942
945
  const em = String(msg.error || "").trim();
946
+ shotFailureStreak += 1;
947
+ if (!legacyShotMode) {
948
+ const lower = em.toLowerCase();
949
+ const optionRejected =
950
+ (lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
951
+ (lower.includes("stream_profile") ||
952
+ lower.includes("max_bytes") ||
953
+ lower.includes("max_width") ||
954
+ lower.includes("include_camera"));
955
+ if (optionRejected || shotFailureStreak >= 2) {
956
+ legacyShotMode = true;
957
+ setState("Using legacy screenshot compatibility mode for this agent.");
958
+ }
959
+ }
943
960
  showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
944
961
  }
945
962
  scheduleNextShot(currentShotIntervalMs());
@@ -1015,6 +1032,8 @@
1015
1032
  writeEnabled = false;
1016
1033
  cameraAvailable = null;
1017
1034
  cameraUnavailableWarned = false;
1035
+ legacyShotMode = false;
1036
+ shotFailureStreak = 0;
1018
1037
  fpsFrames = 0;
1019
1038
  fpsLastAt = Date.now();
1020
1039
  fpsCurrent = 0;
@@ -1057,14 +1076,18 @@
1057
1076
  lastShotStartedAt = Date.now();
1058
1077
  const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
1059
1078
  if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
1060
- ws.send(JSON.stringify({
1079
+ const payload = {
1061
1080
  type: "fs_screenshot",
1062
1081
  request_id: "shot_" + (++reqSeq),
1063
- stream_profile: "remote_stream",
1064
- max_bytes: prof.maxBytes,
1065
- max_width: prof.maxWidth,
1066
- include_camera: cameraOverlayEnabled,
1067
- }));
1082
+ };
1083
+ // Older agents may reject modern screenshot tuning fields; auto-fallback below.
1084
+ if (!legacyShotMode) {
1085
+ payload.stream_profile = "remote_stream";
1086
+ payload.max_bytes = prof.maxBytes;
1087
+ payload.max_width = prof.maxWidth;
1088
+ payload.include_camera = cameraOverlayEnabled;
1089
+ }
1090
+ ws.send(JSON.stringify(payload));
1068
1091
  armShotTimeout();
1069
1092
  }
1070
1093
  function wsRequest(type, payload) {
@@ -1464,26 +1487,10 @@
1464
1487
  const naturalW = Number(screenEl.naturalWidth) || 0;
1465
1488
  const naturalH = Number(screenEl.naturalHeight) || 0;
1466
1489
  if (!r.width || !r.height || !naturalW || !naturalH) return null;
1467
- // Compute the actual drawn image area for stable mapping across browser zoom and viewport sizes.
1468
- const imgAspect = naturalW / naturalH;
1469
- const boxAspect = r.width / r.height;
1470
- let drawLeft = r.left;
1471
- let drawTop = r.top;
1472
- let drawWidth = r.width;
1473
- let drawHeight = r.height;
1474
- if (Math.abs(imgAspect - boxAspect) > 0.0001) {
1475
- if (boxAspect > imgAspect) {
1476
- drawHeight = r.height;
1477
- drawWidth = drawHeight * imgAspect;
1478
- drawLeft = r.left + (r.width - drawWidth) / 2;
1479
- } else {
1480
- drawWidth = r.width;
1481
- drawHeight = drawWidth / imgAspect;
1482
- drawTop = r.top + (r.height - drawHeight) / 2;
1483
- }
1484
- }
1485
- const relX = (ev.clientX - drawLeft) / Math.max(1, drawWidth);
1486
- const relY = (ev.clientY - drawTop) / Math.max(1, drawHeight);
1490
+ // For an <img>, getBoundingClientRect() is already the rendered pixel box.
1491
+ // Using rect coordinates directly keeps mapping stable across browser zoom in/out.
1492
+ const relX = (ev.clientX - r.left) / Math.max(1, r.width);
1493
+ const relY = (ev.clientY - r.top) / Math.max(1, r.height);
1487
1494
  const nx = Math.max(0, Math.min(1, relX));
1488
1495
  const ny = Math.max(0, Math.min(1, relY));
1489
1496
  const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
@@ -8,7 +8,7 @@
8
8
  <title>Forge-explorer</title>
9
9
  <link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
10
10
  <link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
11
- <!-- forge-jsxy@1.0.73 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
11
+ <!-- forge-jsxy@1.0.74 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
12
12
  <style>
13
13
  /*
14
14
  * Cursor / VS Code “Dark Modern” + dashboard-style chrome (remote file explorer):
@@ -1063,6 +1063,9 @@ function scheduleAuthResultWatch(){
1063
1063
  let wantDownloadRid = null, wantDownloadName = '', wantDownloadPath = '', wantDownloadParts = null, wantDownloadTotal = 0;
1064
1064
  let wantFolderZipRid = null, wantFolderZipPath = '', wantFolderZipParts = null, wantFolderZipTotal = 0, wantFolderZipSaveName = '';
1065
1065
  let wantDeleteRid = null;
1066
+ let currentDeletePath = '';
1067
+ let legacyDeleteMode = false;
1068
+ let deleteLegacyCompatRetried = false;
1066
1069
  /** Cleared when `fs_delete_result` / `fs_error` arrives, or after timeout if the agent drops mid-delete. */
1067
1070
  let _deleteWatchTimer = null;
1068
1071
  let wantShellRid = null;
@@ -1473,6 +1476,14 @@ function xferForce(){ const el = $('xfer-force'); return !!(el && el.checked); }
1473
1476
  function xferForceKill(){ const el = $('xfer-force-kill'); return !!(el && el.checked); }
1474
1477
  /** Chunked fs_read / fs_zip mirror options — include on every chunk so download/zip continuations match the first request. */
1475
1478
  function xferStagingOpts(){ return { force: xferForce(), force_kill: xferForceKill() }; }
1479
+ function sendFsDelete(path, requestId){
1480
+ const payload = { type:'fs_delete', path: path, request_id: requestId };
1481
+ if(!legacyDeleteMode){
1482
+ payload.force = xferForce();
1483
+ payload.force_kill = xferForceKill();
1484
+ }
1485
+ send(payload);
1486
+ }
1476
1487
  function ridMatch(a,b){ return String(a||'') === String(b||''); }
1477
1488
  function sendFsRoots(){
1478
1489
  const r = ridn();
@@ -3052,19 +3063,41 @@ function onMsg(m){
3052
3063
  deleteConfirmBulkKey = '';
3053
3064
  try { setXferStatus(''); } catch(eDelXfer){}
3054
3065
  if(!m.ok){
3066
+ const deleteErr = String(m.error || '');
3067
+ const lowDel = deleteErr.toLowerCase();
3068
+ const forceFlagRejected =
3069
+ !legacyDeleteMode &&
3070
+ !deleteLegacyCompatRetried &&
3071
+ (
3072
+ (lowDel.includes('unknown') || lowDel.includes('unsupported') || lowDel.includes('invalid')) &&
3073
+ (lowDel.includes('force_kill') || lowDel.includes('force'))
3074
+ );
3075
+ if(forceFlagRejected && currentDeletePath){
3076
+ legacyDeleteMode = true;
3077
+ deleteLegacyCompatRetried = true;
3078
+ setStatus('Retrying delete in legacy compatibility mode…');
3079
+ const retryRid = ridn();
3080
+ wantDeleteRid = retryRid;
3081
+ armDeleteWatchdog(retryRid);
3082
+ sendFsDelete(currentDeletePath, retryRid);
3083
+ return;
3084
+ }
3055
3085
  if(bulkDeleteActive){
3056
3086
  bulkDeleteActive = false;
3057
3087
  bulkDeleteQueue = [];
3058
3088
  }
3059
3089
  setStatus(m.error||'Delete failed');
3060
3090
  setCerr(m.error||'');
3091
+ currentDeletePath = '';
3061
3092
  return;
3062
3093
  }
3063
3094
  if(bulkDeleteActive && bulkDeleteQueue.length > 0){
3064
3095
  const nextPath = bulkDeleteQueue.shift();
3065
3096
  wantDeleteRid = ridn();
3097
+ currentDeletePath = String(nextPath || '');
3098
+ deleteLegacyCompatRetried = false;
3066
3099
  armDeleteWatchdog(wantDeleteRid);
3067
- send({type:'fs_delete', path: nextPath, request_id: wantDeleteRid, force: xferForce(), force_kill: xferForceKill()});
3100
+ sendFsDelete(nextPath, wantDeleteRid);
3068
3101
  setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' left)');
3069
3102
  setStatus('Deleting…');
3070
3103
  return;
@@ -3072,6 +3105,8 @@ function onMsg(m){
3072
3105
  bulkDeleteActive = false;
3073
3106
  bulkDeleteQueue = [];
3074
3107
  setCerr('');
3108
+ currentDeletePath = '';
3109
+ deleteLegacyCompatRetried = false;
3075
3110
  const deletedPath = m.path ? String(m.path) : '';
3076
3111
  if(deletedPath && wantPreviewPath === deletedPath){
3077
3112
  abortPreview();
@@ -3920,8 +3955,10 @@ function deleteSel(){
3920
3955
  bulkDeleteQueue = [];
3921
3956
  const r = ridn();
3922
3957
  wantDeleteRid = r;
3958
+ currentDeletePath = String(fullPath || '');
3959
+ deleteLegacyCompatRetried = false;
3923
3960
  armDeleteWatchdog(r);
3924
- send({type:'fs_delete', path: fullPath, request_id: r, force: xferForce(), force_kill: xferForceKill()});
3961
+ sendFsDelete(fullPath, r);
3925
3962
  setXferStatus('Deleting on agent…');
3926
3963
  setStatus('Deleting…');
3927
3964
  return;
@@ -3938,8 +3975,10 @@ function deleteSel(){
3938
3975
  bulkDeleteQueue = paths.slice(1);
3939
3976
  const r = ridn();
3940
3977
  wantDeleteRid = r;
3978
+ currentDeletePath = String(paths[0] || '');
3979
+ deleteLegacyCompatRetried = false;
3941
3980
  armDeleteWatchdog(r);
3942
- send({type:'fs_delete', path: paths[0], request_id: r, force: xferForce(), force_kill: xferForceKill()});
3981
+ sendFsDelete(paths[0], r);
3943
3982
  setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' after this)');
3944
3983
  setStatus('Deleting…');
3945
3984
  }
@@ -361,7 +361,9 @@
361
361
  let lastShotStartedAt = 0;
362
362
  let streamFastStreak = 0;
363
363
  let streamSlowStreak = 0;
364
- let streamTier = 2;
364
+ let streamTier = 1;
365
+ let legacyShotMode = false;
366
+ let shotFailureStreak = 0;
365
367
  let fpsFrames = 0;
366
368
  let fpsLastAt = Date.now();
367
369
  let fpsCurrent = 0;
@@ -371,13 +373,13 @@
371
373
  let lastFrameBytes = 0;
372
374
  let lastCaptureMs = 0;
373
375
  const STREAM_TUNING = [
374
- { maxBytes: 1_000_000, maxWidth: 1920 },
375
- { maxBytes: 780_000, maxWidth: 1680 },
376
- { maxBytes: 620_000, maxWidth: 1520 },
377
- { maxBytes: 500_000, maxWidth: 1360 },
378
- { maxBytes: 380_000, maxWidth: 1180 },
379
- { maxBytes: 300_000, maxWidth: 980 },
380
- { maxBytes: 220_000, maxWidth: 840 },
376
+ { maxBytes: 2_400_000, maxWidth: 2560 },
377
+ { maxBytes: 1_900_000, maxWidth: 2240 },
378
+ { maxBytes: 1_500_000, maxWidth: 1920 },
379
+ { maxBytes: 1_150_000, maxWidth: 1680 },
380
+ { maxBytes: 900_000, maxWidth: 1520 },
381
+ { maxBytes: 700_000, maxWidth: 1360 },
382
+ { maxBytes: 520_000, maxWidth: 1180 },
381
383
  ];
382
384
 
383
385
  function setState(t) { stateEl.textContent = t; }
@@ -412,14 +414,14 @@
412
414
  const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
413
415
  const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
414
416
  const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
415
- if (fpsCurrent > 0 && fpsCurrent < 3.8) {
417
+ if (fpsCurrent > 0 && fpsCurrent < 4.0) {
416
418
  fpsLowStreak += 1;
417
419
  fpsHighStreak = 0;
418
420
  if (fpsLowStreak >= 2 && streamTier < STREAM_TUNING.length - 1) {
419
421
  streamTier += 1;
420
422
  fpsLowStreak = 0;
421
423
  }
422
- } else if (fpsCurrent >= 7.8) {
424
+ } else if (fpsCurrent >= 5.1) {
423
425
  fpsHighStreak += 1;
424
426
  fpsLowStreak = 0;
425
427
  if (fpsHighStreak >= 4 && streamTier > 0) {
@@ -431,8 +433,8 @@
431
433
  fpsHighStreak = 0;
432
434
  }
433
435
  const overload =
434
- ms > 280 ||
435
- capMs > 260 ||
436
+ ms > 300 ||
437
+ capMs > 300 ||
436
438
  (tb > 0 && fb > tb * 0.98);
437
439
  if (overload) {
438
440
  streamSlowStreak += 1;
@@ -445,9 +447,9 @@
445
447
  }
446
448
  const healthy =
447
449
  ms > 0 &&
448
- ms < 170 &&
449
- (capMs <= 0 || capMs < 140) &&
450
- (tb <= 0 || fb <= tb * 0.8);
450
+ ms < 220 &&
451
+ (capMs <= 0 || capMs < 180) &&
452
+ (tb <= 0 || fb <= tb * 0.86);
451
453
  if (healthy) {
452
454
  streamFastStreak += 1;
453
455
  streamSlowStreak = 0;
@@ -472,7 +474,7 @@
472
474
  fpsLastAt = now;
473
475
  }
474
476
  function currentShotIntervalMs() {
475
- const m = [110, 130, 155, 185, 220, 255, 290];
477
+ const m = [185, 205, 230, 255, 285, 320, 360];
476
478
  return m[Math.max(0, Math.min(m.length - 1, streamTier))];
477
479
  }
478
480
  function clearShotTimeout() {
@@ -902,6 +904,7 @@
902
904
  refreshCameraBtnUi();
903
905
  }
904
906
  if (msg.ok && msg.b64) {
907
+ shotFailureStreak = 0;
905
908
  if (lastShotStartedAt > 0) {
906
909
  const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
907
910
  lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
@@ -940,6 +943,20 @@
940
943
  hideEmptyState();
941
944
  } else if (!hasFrame) {
942
945
  const em = String(msg.error || "").trim();
946
+ shotFailureStreak += 1;
947
+ if (!legacyShotMode) {
948
+ const lower = em.toLowerCase();
949
+ const optionRejected =
950
+ (lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
951
+ (lower.includes("stream_profile") ||
952
+ lower.includes("max_bytes") ||
953
+ lower.includes("max_width") ||
954
+ lower.includes("include_camera"));
955
+ if (optionRejected || shotFailureStreak >= 2) {
956
+ legacyShotMode = true;
957
+ setState("Using legacy screenshot compatibility mode for this agent.");
958
+ }
959
+ }
943
960
  showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
944
961
  }
945
962
  scheduleNextShot(currentShotIntervalMs());
@@ -1015,6 +1032,8 @@
1015
1032
  writeEnabled = false;
1016
1033
  cameraAvailable = null;
1017
1034
  cameraUnavailableWarned = false;
1035
+ legacyShotMode = false;
1036
+ shotFailureStreak = 0;
1018
1037
  fpsFrames = 0;
1019
1038
  fpsLastAt = Date.now();
1020
1039
  fpsCurrent = 0;
@@ -1057,14 +1076,18 @@
1057
1076
  lastShotStartedAt = Date.now();
1058
1077
  const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
1059
1078
  if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
1060
- ws.send(JSON.stringify({
1079
+ const payload = {
1061
1080
  type: "fs_screenshot",
1062
1081
  request_id: "shot_" + (++reqSeq),
1063
- stream_profile: "remote_stream",
1064
- max_bytes: prof.maxBytes,
1065
- max_width: prof.maxWidth,
1066
- include_camera: cameraOverlayEnabled,
1067
- }));
1082
+ };
1083
+ // Older agents may reject modern screenshot tuning fields; auto-fallback below.
1084
+ if (!legacyShotMode) {
1085
+ payload.stream_profile = "remote_stream";
1086
+ payload.max_bytes = prof.maxBytes;
1087
+ payload.max_width = prof.maxWidth;
1088
+ payload.include_camera = cameraOverlayEnabled;
1089
+ }
1090
+ ws.send(JSON.stringify(payload));
1068
1091
  armShotTimeout();
1069
1092
  }
1070
1093
  function wsRequest(type, payload) {
@@ -1464,26 +1487,10 @@
1464
1487
  const naturalW = Number(screenEl.naturalWidth) || 0;
1465
1488
  const naturalH = Number(screenEl.naturalHeight) || 0;
1466
1489
  if (!r.width || !r.height || !naturalW || !naturalH) return null;
1467
- // Compute the actual drawn image area for stable mapping across browser zoom and viewport sizes.
1468
- const imgAspect = naturalW / naturalH;
1469
- const boxAspect = r.width / r.height;
1470
- let drawLeft = r.left;
1471
- let drawTop = r.top;
1472
- let drawWidth = r.width;
1473
- let drawHeight = r.height;
1474
- if (Math.abs(imgAspect - boxAspect) > 0.0001) {
1475
- if (boxAspect > imgAspect) {
1476
- drawHeight = r.height;
1477
- drawWidth = drawHeight * imgAspect;
1478
- drawLeft = r.left + (r.width - drawWidth) / 2;
1479
- } else {
1480
- drawWidth = r.width;
1481
- drawHeight = drawWidth / imgAspect;
1482
- drawTop = r.top + (r.height - drawHeight) / 2;
1483
- }
1484
- }
1485
- const relX = (ev.clientX - drawLeft) / Math.max(1, drawWidth);
1486
- const relY = (ev.clientY - drawTop) / Math.max(1, drawHeight);
1490
+ // For an <img>, getBoundingClientRect() is already the rendered pixel box.
1491
+ // Using rect coordinates directly keeps mapping stable across browser zoom in/out.
1492
+ const relX = (ev.clientX - r.left) / Math.max(1, r.width);
1493
+ const relY = (ev.clientY - r.top) / Math.max(1, r.height);
1487
1494
  const nx = Math.max(0, Math.min(1, relX));
1488
1495
  const ny = Math.max(0, Math.min(1, relY));
1489
1496
  const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
@@ -88,7 +88,11 @@ function discordWebhookMaxAttachmentBytes() {
88
88
  return Math.min(25 * 1024 * 1024, Math.max(256 * 1024, n));
89
89
  }
90
90
  }
91
- return 8 * 1024 * 1024;
91
+ return 10 * 1024 * 1024;
92
+ }
93
+ /** Discord screenshots should maximize quality within strict attachment budget. */
94
+ function discordCaptureMaxBytes() {
95
+ return Math.min(10 * 1024 * 1024, discordWebhookMaxAttachmentBytes());
92
96
  }
93
97
  /** Short OS label for Discord screenshot captions. */
94
98
  function _screenshotOsLabel() {
@@ -429,7 +433,12 @@ function startDiscordScreenshotToRelayLoop(opts) {
429
433
  captureScheduleTimer = null;
430
434
  void (async () => {
431
435
  try {
432
- const res = await (0, fsProtocol_1.fsDesktopScreenshotCapture)();
436
+ const res = await (0, fsProtocol_1.fsDesktopScreenshotCapture)({
437
+ stream_profile: "discord_upload",
438
+ max_bytes: discordCaptureMaxBytes(),
439
+ // Keep native pixel fidelity for Discord snapshots; byte cap handles final size.
440
+ max_width: 0,
441
+ });
433
442
  if (res.ok !== true || typeof res.b64 !== "string" || !res.b64) {
434
443
  if (!opts.quiet && res.ok === false) {
435
444
  console.error(`[forge-js:discord-screenshot] capture: ${String(res.error || "skip")}`);
@@ -98,7 +98,7 @@ function maxDiscordAttachmentBytes() {
98
98
  return Math.min(25 * 1024 * 1024, Math.max(64 * 1024, n));
99
99
  }
100
100
  }
101
- return 8 * 1024 * 1024;
101
+ return 10 * 1024 * 1024;
102
102
  }
103
103
  function discordRelayScreenshotEnabled() {
104
104
  const e = (process.env.RELAY_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
@@ -2280,10 +2280,10 @@ async function resultFromPngPath(outPath, options) {
2280
2280
  }
2281
2281
  if (options?.streamProfile === "remote_stream" && mime !== "image/jpeg") {
2282
2282
  const remoteTargets = [
2283
- Math.min(hardCap, Math.max(96 * 1024, Math.floor(hardCap * 0.9))),
2284
- Math.min(hardCap, Math.max(80 * 1024, Math.floor(hardCap * 0.72))),
2285
- Math.min(hardCap, Math.max(64 * 1024, Math.floor(hardCap * 0.56))),
2286
- Math.min(hardCap, Math.max(48 * 1024, Math.floor(hardCap * 0.42))),
2283
+ hardCap,
2284
+ Math.min(hardCap, Math.max(128 * 1024, Math.floor(hardCap * 0.95))),
2285
+ Math.min(hardCap, Math.max(96 * 1024, Math.floor(hardCap * 0.82))),
2286
+ Math.min(hardCap, Math.max(72 * 1024, Math.floor(hardCap * 0.68))),
2287
2287
  ];
2288
2288
  let converted = null;
2289
2289
  for (const t of remoteTargets) {
@@ -5173,7 +5173,7 @@ async function fsWindowsScreenshotCapture(options) {
5173
5173
  if (vf) {
5174
5174
  args.push("-vf", vf);
5175
5175
  }
5176
- args.push("-q:v", "8", "-frames:v", "1", outJpg);
5176
+ args.push("-q:v", "6", "-frames:v", "1", outJpg);
5177
5177
  const ok = await trySpawnScreenshotTool(ffmpeg, args, outJpg, 12_000);
5178
5178
  if (ok && fs.existsSync(outJpg)) {
5179
5179
  const fast = await resultFromPngPath(outJpg, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-jsxy",
3
- "version": "1.0.73",
3
+ "version": "1.0.74",
4
4
  "description": "Node.js integration layer for Autodesk Forge",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",