@midscene/android 1.7.4 → 1.7.5-beta-20260420031652.0

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/dist/es/cli.mjs CHANGED
@@ -676,6 +676,9 @@ const defaultNormalScrollDuration = 1000;
676
676
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
677
677
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
678
678
  const debugDevice = (0, logger_.getDebug)('android:device');
679
+ const warnDevice = (0, logger_.getDebug)('android:device', {
680
+ console: true
681
+ });
679
682
  function escapeForShell(text) {
680
683
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
681
684
  }
@@ -855,7 +858,7 @@ class AndroidDevice {
855
858
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
856
859
  } catch (error) {
857
860
  const msg = error instanceof Error ? error.message : String(error);
858
- console.warn(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
861
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
859
862
  }
860
863
  return adb;
861
864
  }
@@ -1223,6 +1226,22 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1223
1226
  y: endY
1224
1227
  };
1225
1228
  }
1229
+ warnScrollDistanceClamped(direction, requestedDistance, appliedDistance) {
1230
+ if (requestedDistance <= appliedDistance) return;
1231
+ const scrollToSuggestion = {
1232
+ down: 'scrollToBottom',
1233
+ up: 'scrollToTop',
1234
+ left: 'scrollToLeft',
1235
+ right: 'scrollToRight'
1236
+ };
1237
+ const edgeLabel = {
1238
+ down: 'bottom',
1239
+ up: 'top',
1240
+ left: 'left edge',
1241
+ right: 'right edge'
1242
+ };
1243
+ warnDevice(`[midscene] Android ADB swipe coordinates must stay within the screen bounds. The requested scroll distance (${requestedDistance}px) exceeds the maximum single swipe distance (${appliedDistance}px) from the current start point, so it will be clamped. If you want to scroll to the ${edgeLabel[direction]}, use ${scrollToSuggestion[direction]} instead.`);
1244
+ }
1226
1245
  async screenshotBase64() {
1227
1246
  debugDevice('screenshotBase64 begin');
1228
1247
  const adapter = this.getScrcpyAdapter();
@@ -1407,58 +1426,66 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1407
1426
  async scrollUp(distance, startPoint) {
1408
1427
  const { height } = await this.size();
1409
1428
  const scrollDistance = Math.round(distance || height);
1429
+ const hasExplicitDistance = void 0 !== distance;
1410
1430
  if (startPoint) {
1411
1431
  const start = {
1412
1432
  x: Math.round(startPoint.left),
1413
1433
  y: Math.round(startPoint.top)
1414
1434
  };
1415
1435
  const end = this.calculateScrollEndPoint(start, 0, scrollDistance, 0, height);
1436
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('up', scrollDistance, Math.abs(end.y - start.y));
1416
1437
  await this.mouseDrag(start, end);
1417
1438
  return;
1418
1439
  }
1419
- await this.scroll(0, -scrollDistance);
1440
+ await this.scroll(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
1420
1441
  }
1421
1442
  async scrollDown(distance, startPoint) {
1422
1443
  const { height } = await this.size();
1423
1444
  const scrollDistance = Math.round(distance || height);
1445
+ const hasExplicitDistance = void 0 !== distance;
1424
1446
  if (startPoint) {
1425
1447
  const start = {
1426
1448
  x: Math.round(startPoint.left),
1427
1449
  y: Math.round(startPoint.top)
1428
1450
  };
1429
1451
  const end = this.calculateScrollEndPoint(start, 0, -scrollDistance, 0, height);
1452
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('down', scrollDistance, Math.abs(end.y - start.y));
1430
1453
  await this.mouseDrag(start, end);
1431
1454
  return;
1432
1455
  }
1433
- await this.scroll(0, scrollDistance);
1456
+ await this.scroll(0, scrollDistance, void 0, hasExplicitDistance, 'down');
1434
1457
  }
1435
1458
  async scrollLeft(distance, startPoint) {
1436
1459
  const { width } = await this.size();
1437
1460
  const scrollDistance = Math.round(distance || width);
1461
+ const hasExplicitDistance = void 0 !== distance;
1438
1462
  if (startPoint) {
1439
1463
  const start = {
1440
1464
  x: Math.round(startPoint.left),
1441
1465
  y: Math.round(startPoint.top)
1442
1466
  };
1443
1467
  const end = this.calculateScrollEndPoint(start, scrollDistance, 0, width, 0);
1468
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('left', scrollDistance, Math.abs(end.x - start.x));
1444
1469
  await this.mouseDrag(start, end);
1445
1470
  return;
1446
1471
  }
1447
- await this.scroll(-scrollDistance, 0);
1472
+ await this.scroll(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
1448
1473
  }
1449
1474
  async scrollRight(distance, startPoint) {
1450
1475
  const { width } = await this.size();
1451
1476
  const scrollDistance = Math.round(distance || width);
1477
+ const hasExplicitDistance = void 0 !== distance;
1452
1478
  if (startPoint) {
1453
1479
  const start = {
1454
1480
  x: Math.round(startPoint.left),
1455
1481
  y: Math.round(startPoint.top)
1456
1482
  };
1457
1483
  const end = this.calculateScrollEndPoint(start, -scrollDistance, 0, width, 0);
1484
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('right', scrollDistance, Math.abs(end.x - start.x));
1458
1485
  await this.mouseDrag(start, end);
1459
1486
  return;
1460
1487
  }
1461
- await this.scroll(scrollDistance, 0);
1488
+ await this.scroll(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
1462
1489
  }
1463
1490
  async ensureYadb() {
1464
1491
  if (!this.yadbPushed) {
@@ -1558,7 +1585,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1558
1585
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1559
1586
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1560
1587
  }
1561
- async scroll(deltaX, deltaY, duration) {
1588
+ async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
1562
1589
  if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
1563
1590
  const { width, height } = await this.size();
1564
1591
  const n = 4;
@@ -1568,8 +1595,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1568
1595
  const maxNegativeDeltaX = width - startX;
1569
1596
  const maxPositiveDeltaY = startY;
1570
1597
  const maxNegativeDeltaY = height - startY;
1598
+ const originalDeltaX = deltaX;
1599
+ const originalDeltaY = deltaY;
1571
1600
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1572
1601
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1602
+ if (warnOnClamp && direction && (deltaX !== originalDeltaX || deltaY !== originalDeltaY)) {
1603
+ const requestedDistance = 'left' === direction || 'right' === direction ? Math.abs(originalDeltaX) : Math.abs(originalDeltaY);
1604
+ const appliedDistance = 'left' === direction || 'right' === direction ? Math.abs(deltaX) : Math.abs(deltaY);
1605
+ this.warnScrollDistanceClamped(direction, requestedDistance, appliedDistance);
1606
+ }
1573
1607
  const endX = Math.round(startX - deltaX);
1574
1608
  const endY = Math.round(startY - deltaY);
1575
1609
  const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
@@ -1726,7 +1760,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1726
1760
  }
1727
1761
  debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1728
1762
  }
1729
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1763
+ warnDevice('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1730
1764
  return false;
1731
1765
  }
1732
1766
  constructor(deviceId, options){
@@ -1952,7 +1986,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
1952
1986
  const tools = new AndroidMidsceneTools();
1953
1987
  runToolsCLI(tools, 'midscene-android', {
1954
1988
  stripPrefix: 'android_',
1955
- version: "1.7.4",
1989
+ version: "1.7.5-beta-20260420031652.0",
1956
1990
  extraCommands: createReportCliCommands()
1957
1991
  }).catch((e)=>{
1958
1992
  if (!(e instanceof CLIError)) console.error(e);
package/dist/es/index.mjs CHANGED
@@ -579,6 +579,9 @@ const defaultNormalScrollDuration = 1000;
579
579
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
580
580
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
581
581
  const debugDevice = (0, logger_.getDebug)('android:device');
582
+ const warnDevice = (0, logger_.getDebug)('android:device', {
583
+ console: true
584
+ });
582
585
  function escapeForShell(text) {
583
586
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
584
587
  }
@@ -758,7 +761,7 @@ class AndroidDevice {
758
761
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
759
762
  } catch (error) {
760
763
  const msg = error instanceof Error ? error.message : String(error);
761
- console.warn(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
764
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
762
765
  }
763
766
  return adb;
764
767
  }
@@ -1126,6 +1129,22 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1126
1129
  y: endY
1127
1130
  };
1128
1131
  }
1132
+ warnScrollDistanceClamped(direction, requestedDistance, appliedDistance) {
1133
+ if (requestedDistance <= appliedDistance) return;
1134
+ const scrollToSuggestion = {
1135
+ down: 'scrollToBottom',
1136
+ up: 'scrollToTop',
1137
+ left: 'scrollToLeft',
1138
+ right: 'scrollToRight'
1139
+ };
1140
+ const edgeLabel = {
1141
+ down: 'bottom',
1142
+ up: 'top',
1143
+ left: 'left edge',
1144
+ right: 'right edge'
1145
+ };
1146
+ warnDevice(`[midscene] Android ADB swipe coordinates must stay within the screen bounds. The requested scroll distance (${requestedDistance}px) exceeds the maximum single swipe distance (${appliedDistance}px) from the current start point, so it will be clamped. If you want to scroll to the ${edgeLabel[direction]}, use ${scrollToSuggestion[direction]} instead.`);
1147
+ }
1129
1148
  async screenshotBase64() {
1130
1149
  debugDevice('screenshotBase64 begin');
1131
1150
  const adapter = this.getScrcpyAdapter();
@@ -1310,58 +1329,66 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1310
1329
  async scrollUp(distance, startPoint) {
1311
1330
  const { height } = await this.size();
1312
1331
  const scrollDistance = Math.round(distance || height);
1332
+ const hasExplicitDistance = void 0 !== distance;
1313
1333
  if (startPoint) {
1314
1334
  const start = {
1315
1335
  x: Math.round(startPoint.left),
1316
1336
  y: Math.round(startPoint.top)
1317
1337
  };
1318
1338
  const end = this.calculateScrollEndPoint(start, 0, scrollDistance, 0, height);
1339
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('up', scrollDistance, Math.abs(end.y - start.y));
1319
1340
  await this.mouseDrag(start, end);
1320
1341
  return;
1321
1342
  }
1322
- await this.scroll(0, -scrollDistance);
1343
+ await this.scroll(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
1323
1344
  }
1324
1345
  async scrollDown(distance, startPoint) {
1325
1346
  const { height } = await this.size();
1326
1347
  const scrollDistance = Math.round(distance || height);
1348
+ const hasExplicitDistance = void 0 !== distance;
1327
1349
  if (startPoint) {
1328
1350
  const start = {
1329
1351
  x: Math.round(startPoint.left),
1330
1352
  y: Math.round(startPoint.top)
1331
1353
  };
1332
1354
  const end = this.calculateScrollEndPoint(start, 0, -scrollDistance, 0, height);
1355
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('down', scrollDistance, Math.abs(end.y - start.y));
1333
1356
  await this.mouseDrag(start, end);
1334
1357
  return;
1335
1358
  }
1336
- await this.scroll(0, scrollDistance);
1359
+ await this.scroll(0, scrollDistance, void 0, hasExplicitDistance, 'down');
1337
1360
  }
1338
1361
  async scrollLeft(distance, startPoint) {
1339
1362
  const { width } = await this.size();
1340
1363
  const scrollDistance = Math.round(distance || width);
1364
+ const hasExplicitDistance = void 0 !== distance;
1341
1365
  if (startPoint) {
1342
1366
  const start = {
1343
1367
  x: Math.round(startPoint.left),
1344
1368
  y: Math.round(startPoint.top)
1345
1369
  };
1346
1370
  const end = this.calculateScrollEndPoint(start, scrollDistance, 0, width, 0);
1371
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('left', scrollDistance, Math.abs(end.x - start.x));
1347
1372
  await this.mouseDrag(start, end);
1348
1373
  return;
1349
1374
  }
1350
- await this.scroll(-scrollDistance, 0);
1375
+ await this.scroll(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
1351
1376
  }
1352
1377
  async scrollRight(distance, startPoint) {
1353
1378
  const { width } = await this.size();
1354
1379
  const scrollDistance = Math.round(distance || width);
1380
+ const hasExplicitDistance = void 0 !== distance;
1355
1381
  if (startPoint) {
1356
1382
  const start = {
1357
1383
  x: Math.round(startPoint.left),
1358
1384
  y: Math.round(startPoint.top)
1359
1385
  };
1360
1386
  const end = this.calculateScrollEndPoint(start, -scrollDistance, 0, width, 0);
1387
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('right', scrollDistance, Math.abs(end.x - start.x));
1361
1388
  await this.mouseDrag(start, end);
1362
1389
  return;
1363
1390
  }
1364
- await this.scroll(scrollDistance, 0);
1391
+ await this.scroll(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
1365
1392
  }
1366
1393
  async ensureYadb() {
1367
1394
  if (!this.yadbPushed) {
@@ -1461,7 +1488,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1461
1488
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1462
1489
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1463
1490
  }
1464
- async scroll(deltaX, deltaY, duration) {
1491
+ async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
1465
1492
  if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
1466
1493
  const { width, height } = await this.size();
1467
1494
  const n = 4;
@@ -1471,8 +1498,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1471
1498
  const maxNegativeDeltaX = width - startX;
1472
1499
  const maxPositiveDeltaY = startY;
1473
1500
  const maxNegativeDeltaY = height - startY;
1501
+ const originalDeltaX = deltaX;
1502
+ const originalDeltaY = deltaY;
1474
1503
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1475
1504
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1505
+ if (warnOnClamp && direction && (deltaX !== originalDeltaX || deltaY !== originalDeltaY)) {
1506
+ const requestedDistance = 'left' === direction || 'right' === direction ? Math.abs(originalDeltaX) : Math.abs(originalDeltaY);
1507
+ const appliedDistance = 'left' === direction || 'right' === direction ? Math.abs(deltaX) : Math.abs(deltaY);
1508
+ this.warnScrollDistanceClamped(direction, requestedDistance, appliedDistance);
1509
+ }
1476
1510
  const endX = Math.round(startX - deltaX);
1477
1511
  const endY = Math.round(startY - deltaY);
1478
1512
  const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
@@ -1629,7 +1663,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1629
1663
  }
1630
1664
  debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1631
1665
  }
1632
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1666
+ warnDevice('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1633
1667
  return false;
1634
1668
  }
1635
1669
  constructor(deviceId, options){
@@ -675,6 +675,9 @@ const defaultNormalScrollDuration = 1000;
675
675
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
676
676
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
677
677
  const debugDevice = (0, logger_.getDebug)('android:device');
678
+ const warnDevice = (0, logger_.getDebug)('android:device', {
679
+ console: true
680
+ });
678
681
  function escapeForShell(text) {
679
682
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
680
683
  }
@@ -854,7 +857,7 @@ class AndroidDevice {
854
857
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
855
858
  } catch (error) {
856
859
  const msg = error instanceof Error ? error.message : String(error);
857
- console.warn(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
860
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
858
861
  }
859
862
  return adb;
860
863
  }
@@ -1222,6 +1225,22 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1222
1225
  y: endY
1223
1226
  };
1224
1227
  }
1228
+ warnScrollDistanceClamped(direction, requestedDistance, appliedDistance) {
1229
+ if (requestedDistance <= appliedDistance) return;
1230
+ const scrollToSuggestion = {
1231
+ down: 'scrollToBottom',
1232
+ up: 'scrollToTop',
1233
+ left: 'scrollToLeft',
1234
+ right: 'scrollToRight'
1235
+ };
1236
+ const edgeLabel = {
1237
+ down: 'bottom',
1238
+ up: 'top',
1239
+ left: 'left edge',
1240
+ right: 'right edge'
1241
+ };
1242
+ warnDevice(`[midscene] Android ADB swipe coordinates must stay within the screen bounds. The requested scroll distance (${requestedDistance}px) exceeds the maximum single swipe distance (${appliedDistance}px) from the current start point, so it will be clamped. If you want to scroll to the ${edgeLabel[direction]}, use ${scrollToSuggestion[direction]} instead.`);
1243
+ }
1225
1244
  async screenshotBase64() {
1226
1245
  debugDevice('screenshotBase64 begin');
1227
1246
  const adapter = this.getScrcpyAdapter();
@@ -1406,58 +1425,66 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1406
1425
  async scrollUp(distance, startPoint) {
1407
1426
  const { height } = await this.size();
1408
1427
  const scrollDistance = Math.round(distance || height);
1428
+ const hasExplicitDistance = void 0 !== distance;
1409
1429
  if (startPoint) {
1410
1430
  const start = {
1411
1431
  x: Math.round(startPoint.left),
1412
1432
  y: Math.round(startPoint.top)
1413
1433
  };
1414
1434
  const end = this.calculateScrollEndPoint(start, 0, scrollDistance, 0, height);
1435
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('up', scrollDistance, Math.abs(end.y - start.y));
1415
1436
  await this.mouseDrag(start, end);
1416
1437
  return;
1417
1438
  }
1418
- await this.scroll(0, -scrollDistance);
1439
+ await this.scroll(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
1419
1440
  }
1420
1441
  async scrollDown(distance, startPoint) {
1421
1442
  const { height } = await this.size();
1422
1443
  const scrollDistance = Math.round(distance || height);
1444
+ const hasExplicitDistance = void 0 !== distance;
1423
1445
  if (startPoint) {
1424
1446
  const start = {
1425
1447
  x: Math.round(startPoint.left),
1426
1448
  y: Math.round(startPoint.top)
1427
1449
  };
1428
1450
  const end = this.calculateScrollEndPoint(start, 0, -scrollDistance, 0, height);
1451
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('down', scrollDistance, Math.abs(end.y - start.y));
1429
1452
  await this.mouseDrag(start, end);
1430
1453
  return;
1431
1454
  }
1432
- await this.scroll(0, scrollDistance);
1455
+ await this.scroll(0, scrollDistance, void 0, hasExplicitDistance, 'down');
1433
1456
  }
1434
1457
  async scrollLeft(distance, startPoint) {
1435
1458
  const { width } = await this.size();
1436
1459
  const scrollDistance = Math.round(distance || width);
1460
+ const hasExplicitDistance = void 0 !== distance;
1437
1461
  if (startPoint) {
1438
1462
  const start = {
1439
1463
  x: Math.round(startPoint.left),
1440
1464
  y: Math.round(startPoint.top)
1441
1465
  };
1442
1466
  const end = this.calculateScrollEndPoint(start, scrollDistance, 0, width, 0);
1467
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('left', scrollDistance, Math.abs(end.x - start.x));
1443
1468
  await this.mouseDrag(start, end);
1444
1469
  return;
1445
1470
  }
1446
- await this.scroll(-scrollDistance, 0);
1471
+ await this.scroll(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
1447
1472
  }
1448
1473
  async scrollRight(distance, startPoint) {
1449
1474
  const { width } = await this.size();
1450
1475
  const scrollDistance = Math.round(distance || width);
1476
+ const hasExplicitDistance = void 0 !== distance;
1451
1477
  if (startPoint) {
1452
1478
  const start = {
1453
1479
  x: Math.round(startPoint.left),
1454
1480
  y: Math.round(startPoint.top)
1455
1481
  };
1456
1482
  const end = this.calculateScrollEndPoint(start, -scrollDistance, 0, width, 0);
1483
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('right', scrollDistance, Math.abs(end.x - start.x));
1457
1484
  await this.mouseDrag(start, end);
1458
1485
  return;
1459
1486
  }
1460
- await this.scroll(scrollDistance, 0);
1487
+ await this.scroll(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
1461
1488
  }
1462
1489
  async ensureYadb() {
1463
1490
  if (!this.yadbPushed) {
@@ -1557,7 +1584,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1557
1584
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1558
1585
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1559
1586
  }
1560
- async scroll(deltaX, deltaY, duration) {
1587
+ async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
1561
1588
  if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
1562
1589
  const { width, height } = await this.size();
1563
1590
  const n = 4;
@@ -1567,8 +1594,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1567
1594
  const maxNegativeDeltaX = width - startX;
1568
1595
  const maxPositiveDeltaY = startY;
1569
1596
  const maxNegativeDeltaY = height - startY;
1597
+ const originalDeltaX = deltaX;
1598
+ const originalDeltaY = deltaY;
1570
1599
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1571
1600
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1601
+ if (warnOnClamp && direction && (deltaX !== originalDeltaX || deltaY !== originalDeltaY)) {
1602
+ const requestedDistance = 'left' === direction || 'right' === direction ? Math.abs(originalDeltaX) : Math.abs(originalDeltaY);
1603
+ const appliedDistance = 'left' === direction || 'right' === direction ? Math.abs(deltaX) : Math.abs(deltaY);
1604
+ this.warnScrollDistanceClamped(direction, requestedDistance, appliedDistance);
1605
+ }
1572
1606
  const endX = Math.round(startX - deltaX);
1573
1607
  const endY = Math.round(startY - deltaY);
1574
1608
  const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
@@ -1725,7 +1759,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1725
1759
  }
1726
1760
  debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1727
1761
  }
1728
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1762
+ warnDevice('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1729
1763
  return false;
1730
1764
  }
1731
1765
  constructor(deviceId, options){
@@ -1955,7 +1989,7 @@ class AndroidMCPServer extends BaseMCPServer {
1955
1989
  constructor(toolsManager){
1956
1990
  super({
1957
1991
  name: '@midscene/android-mcp',
1958
- version: "1.7.4",
1992
+ version: "1.7.5-beta-20260420031652.0",
1959
1993
  description: 'Control the Android device using natural language commands'
1960
1994
  }, toolsManager);
1961
1995
  }
package/dist/lib/cli.js CHANGED
@@ -691,6 +691,9 @@ var __webpack_exports__ = {};
691
691
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
692
692
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
693
693
  const debugDevice = (0, logger_.getDebug)('android:device');
694
+ const warnDevice = (0, logger_.getDebug)('android:device', {
695
+ console: true
696
+ });
694
697
  function escapeForShell(text) {
695
698
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
696
699
  }
@@ -870,7 +873,7 @@ var __webpack_exports__ = {};
870
873
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
871
874
  } catch (error) {
872
875
  const msg = error instanceof Error ? error.message : String(error);
873
- console.warn(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
876
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
874
877
  }
875
878
  return adb;
876
879
  }
@@ -1238,6 +1241,22 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1238
1241
  y: endY
1239
1242
  };
1240
1243
  }
1244
+ warnScrollDistanceClamped(direction, requestedDistance, appliedDistance) {
1245
+ if (requestedDistance <= appliedDistance) return;
1246
+ const scrollToSuggestion = {
1247
+ down: 'scrollToBottom',
1248
+ up: 'scrollToTop',
1249
+ left: 'scrollToLeft',
1250
+ right: 'scrollToRight'
1251
+ };
1252
+ const edgeLabel = {
1253
+ down: 'bottom',
1254
+ up: 'top',
1255
+ left: 'left edge',
1256
+ right: 'right edge'
1257
+ };
1258
+ warnDevice(`[midscene] Android ADB swipe coordinates must stay within the screen bounds. The requested scroll distance (${requestedDistance}px) exceeds the maximum single swipe distance (${appliedDistance}px) from the current start point, so it will be clamped. If you want to scroll to the ${edgeLabel[direction]}, use ${scrollToSuggestion[direction]} instead.`);
1259
+ }
1241
1260
  async screenshotBase64() {
1242
1261
  debugDevice('screenshotBase64 begin');
1243
1262
  const adapter = this.getScrcpyAdapter();
@@ -1422,58 +1441,66 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1422
1441
  async scrollUp(distance, startPoint) {
1423
1442
  const { height } = await this.size();
1424
1443
  const scrollDistance = Math.round(distance || height);
1444
+ const hasExplicitDistance = void 0 !== distance;
1425
1445
  if (startPoint) {
1426
1446
  const start = {
1427
1447
  x: Math.round(startPoint.left),
1428
1448
  y: Math.round(startPoint.top)
1429
1449
  };
1430
1450
  const end = this.calculateScrollEndPoint(start, 0, scrollDistance, 0, height);
1451
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('up', scrollDistance, Math.abs(end.y - start.y));
1431
1452
  await this.mouseDrag(start, end);
1432
1453
  return;
1433
1454
  }
1434
- await this.scroll(0, -scrollDistance);
1455
+ await this.scroll(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
1435
1456
  }
1436
1457
  async scrollDown(distance, startPoint) {
1437
1458
  const { height } = await this.size();
1438
1459
  const scrollDistance = Math.round(distance || height);
1460
+ const hasExplicitDistance = void 0 !== distance;
1439
1461
  if (startPoint) {
1440
1462
  const start = {
1441
1463
  x: Math.round(startPoint.left),
1442
1464
  y: Math.round(startPoint.top)
1443
1465
  };
1444
1466
  const end = this.calculateScrollEndPoint(start, 0, -scrollDistance, 0, height);
1467
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('down', scrollDistance, Math.abs(end.y - start.y));
1445
1468
  await this.mouseDrag(start, end);
1446
1469
  return;
1447
1470
  }
1448
- await this.scroll(0, scrollDistance);
1471
+ await this.scroll(0, scrollDistance, void 0, hasExplicitDistance, 'down');
1449
1472
  }
1450
1473
  async scrollLeft(distance, startPoint) {
1451
1474
  const { width } = await this.size();
1452
1475
  const scrollDistance = Math.round(distance || width);
1476
+ const hasExplicitDistance = void 0 !== distance;
1453
1477
  if (startPoint) {
1454
1478
  const start = {
1455
1479
  x: Math.round(startPoint.left),
1456
1480
  y: Math.round(startPoint.top)
1457
1481
  };
1458
1482
  const end = this.calculateScrollEndPoint(start, scrollDistance, 0, width, 0);
1483
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('left', scrollDistance, Math.abs(end.x - start.x));
1459
1484
  await this.mouseDrag(start, end);
1460
1485
  return;
1461
1486
  }
1462
- await this.scroll(-scrollDistance, 0);
1487
+ await this.scroll(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
1463
1488
  }
1464
1489
  async scrollRight(distance, startPoint) {
1465
1490
  const { width } = await this.size();
1466
1491
  const scrollDistance = Math.round(distance || width);
1492
+ const hasExplicitDistance = void 0 !== distance;
1467
1493
  if (startPoint) {
1468
1494
  const start = {
1469
1495
  x: Math.round(startPoint.left),
1470
1496
  y: Math.round(startPoint.top)
1471
1497
  };
1472
1498
  const end = this.calculateScrollEndPoint(start, -scrollDistance, 0, width, 0);
1499
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('right', scrollDistance, Math.abs(end.x - start.x));
1473
1500
  await this.mouseDrag(start, end);
1474
1501
  return;
1475
1502
  }
1476
- await this.scroll(scrollDistance, 0);
1503
+ await this.scroll(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
1477
1504
  }
1478
1505
  async ensureYadb() {
1479
1506
  if (!this.yadbPushed) {
@@ -1573,7 +1600,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1573
1600
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1574
1601
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1575
1602
  }
1576
- async scroll(deltaX, deltaY, duration) {
1603
+ async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
1577
1604
  if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
1578
1605
  const { width, height } = await this.size();
1579
1606
  const n = 4;
@@ -1583,8 +1610,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1583
1610
  const maxNegativeDeltaX = width - startX;
1584
1611
  const maxPositiveDeltaY = startY;
1585
1612
  const maxNegativeDeltaY = height - startY;
1613
+ const originalDeltaX = deltaX;
1614
+ const originalDeltaY = deltaY;
1586
1615
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1587
1616
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1617
+ if (warnOnClamp && direction && (deltaX !== originalDeltaX || deltaY !== originalDeltaY)) {
1618
+ const requestedDistance = 'left' === direction || 'right' === direction ? Math.abs(originalDeltaX) : Math.abs(originalDeltaY);
1619
+ const appliedDistance = 'left' === direction || 'right' === direction ? Math.abs(deltaX) : Math.abs(deltaY);
1620
+ this.warnScrollDistanceClamped(direction, requestedDistance, appliedDistance);
1621
+ }
1588
1622
  const endX = Math.round(startX - deltaX);
1589
1623
  const endY = Math.round(startY - deltaY);
1590
1624
  const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
@@ -1741,7 +1775,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1741
1775
  }
1742
1776
  debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1743
1777
  }
1744
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1778
+ warnDevice('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1745
1779
  return false;
1746
1780
  }
1747
1781
  constructor(deviceId, options){
@@ -1967,7 +2001,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1967
2001
  const tools = new AndroidMidsceneTools();
1968
2002
  (0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-android', {
1969
2003
  stripPrefix: 'android_',
1970
- version: "1.7.4",
2004
+ version: "1.7.5-beta-20260420031652.0",
1971
2005
  extraCommands: (0, core_namespaceObject.createReportCliCommands)()
1972
2006
  }).catch((e)=>{
1973
2007
  if (!(e instanceof cli_namespaceObject.CLIError)) console.error(e);
package/dist/lib/index.js CHANGED
@@ -613,6 +613,9 @@ var __webpack_exports__ = {};
613
613
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
614
614
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
615
615
  const debugDevice = (0, logger_.getDebug)('android:device');
616
+ const warnDevice = (0, logger_.getDebug)('android:device', {
617
+ console: true
618
+ });
616
619
  function escapeForShell(text) {
617
620
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
618
621
  }
@@ -792,7 +795,7 @@ var __webpack_exports__ = {};
792
795
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
793
796
  } catch (error) {
794
797
  const msg = error instanceof Error ? error.message : String(error);
795
- console.warn(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
798
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
796
799
  }
797
800
  return adb;
798
801
  }
@@ -1160,6 +1163,22 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1160
1163
  y: endY
1161
1164
  };
1162
1165
  }
1166
+ warnScrollDistanceClamped(direction, requestedDistance, appliedDistance) {
1167
+ if (requestedDistance <= appliedDistance) return;
1168
+ const scrollToSuggestion = {
1169
+ down: 'scrollToBottom',
1170
+ up: 'scrollToTop',
1171
+ left: 'scrollToLeft',
1172
+ right: 'scrollToRight'
1173
+ };
1174
+ const edgeLabel = {
1175
+ down: 'bottom',
1176
+ up: 'top',
1177
+ left: 'left edge',
1178
+ right: 'right edge'
1179
+ };
1180
+ warnDevice(`[midscene] Android ADB swipe coordinates must stay within the screen bounds. The requested scroll distance (${requestedDistance}px) exceeds the maximum single swipe distance (${appliedDistance}px) from the current start point, so it will be clamped. If you want to scroll to the ${edgeLabel[direction]}, use ${scrollToSuggestion[direction]} instead.`);
1181
+ }
1163
1182
  async screenshotBase64() {
1164
1183
  debugDevice('screenshotBase64 begin');
1165
1184
  const adapter = this.getScrcpyAdapter();
@@ -1344,58 +1363,66 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1344
1363
  async scrollUp(distance, startPoint) {
1345
1364
  const { height } = await this.size();
1346
1365
  const scrollDistance = Math.round(distance || height);
1366
+ const hasExplicitDistance = void 0 !== distance;
1347
1367
  if (startPoint) {
1348
1368
  const start = {
1349
1369
  x: Math.round(startPoint.left),
1350
1370
  y: Math.round(startPoint.top)
1351
1371
  };
1352
1372
  const end = this.calculateScrollEndPoint(start, 0, scrollDistance, 0, height);
1373
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('up', scrollDistance, Math.abs(end.y - start.y));
1353
1374
  await this.mouseDrag(start, end);
1354
1375
  return;
1355
1376
  }
1356
- await this.scroll(0, -scrollDistance);
1377
+ await this.scroll(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
1357
1378
  }
1358
1379
  async scrollDown(distance, startPoint) {
1359
1380
  const { height } = await this.size();
1360
1381
  const scrollDistance = Math.round(distance || height);
1382
+ const hasExplicitDistance = void 0 !== distance;
1361
1383
  if (startPoint) {
1362
1384
  const start = {
1363
1385
  x: Math.round(startPoint.left),
1364
1386
  y: Math.round(startPoint.top)
1365
1387
  };
1366
1388
  const end = this.calculateScrollEndPoint(start, 0, -scrollDistance, 0, height);
1389
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('down', scrollDistance, Math.abs(end.y - start.y));
1367
1390
  await this.mouseDrag(start, end);
1368
1391
  return;
1369
1392
  }
1370
- await this.scroll(0, scrollDistance);
1393
+ await this.scroll(0, scrollDistance, void 0, hasExplicitDistance, 'down');
1371
1394
  }
1372
1395
  async scrollLeft(distance, startPoint) {
1373
1396
  const { width } = await this.size();
1374
1397
  const scrollDistance = Math.round(distance || width);
1398
+ const hasExplicitDistance = void 0 !== distance;
1375
1399
  if (startPoint) {
1376
1400
  const start = {
1377
1401
  x: Math.round(startPoint.left),
1378
1402
  y: Math.round(startPoint.top)
1379
1403
  };
1380
1404
  const end = this.calculateScrollEndPoint(start, scrollDistance, 0, width, 0);
1405
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('left', scrollDistance, Math.abs(end.x - start.x));
1381
1406
  await this.mouseDrag(start, end);
1382
1407
  return;
1383
1408
  }
1384
- await this.scroll(-scrollDistance, 0);
1409
+ await this.scroll(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
1385
1410
  }
1386
1411
  async scrollRight(distance, startPoint) {
1387
1412
  const { width } = await this.size();
1388
1413
  const scrollDistance = Math.round(distance || width);
1414
+ const hasExplicitDistance = void 0 !== distance;
1389
1415
  if (startPoint) {
1390
1416
  const start = {
1391
1417
  x: Math.round(startPoint.left),
1392
1418
  y: Math.round(startPoint.top)
1393
1419
  };
1394
1420
  const end = this.calculateScrollEndPoint(start, -scrollDistance, 0, width, 0);
1421
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('right', scrollDistance, Math.abs(end.x - start.x));
1395
1422
  await this.mouseDrag(start, end);
1396
1423
  return;
1397
1424
  }
1398
- await this.scroll(scrollDistance, 0);
1425
+ await this.scroll(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
1399
1426
  }
1400
1427
  async ensureYadb() {
1401
1428
  if (!this.yadbPushed) {
@@ -1495,7 +1522,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1495
1522
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1496
1523
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1497
1524
  }
1498
- async scroll(deltaX, deltaY, duration) {
1525
+ async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
1499
1526
  if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
1500
1527
  const { width, height } = await this.size();
1501
1528
  const n = 4;
@@ -1505,8 +1532,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1505
1532
  const maxNegativeDeltaX = width - startX;
1506
1533
  const maxPositiveDeltaY = startY;
1507
1534
  const maxNegativeDeltaY = height - startY;
1535
+ const originalDeltaX = deltaX;
1536
+ const originalDeltaY = deltaY;
1508
1537
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1509
1538
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1539
+ if (warnOnClamp && direction && (deltaX !== originalDeltaX || deltaY !== originalDeltaY)) {
1540
+ const requestedDistance = 'left' === direction || 'right' === direction ? Math.abs(originalDeltaX) : Math.abs(originalDeltaY);
1541
+ const appliedDistance = 'left' === direction || 'right' === direction ? Math.abs(deltaX) : Math.abs(deltaY);
1542
+ this.warnScrollDistanceClamped(direction, requestedDistance, appliedDistance);
1543
+ }
1510
1544
  const endX = Math.round(startX - deltaX);
1511
1545
  const endY = Math.round(startY - deltaY);
1512
1546
  const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
@@ -1663,7 +1697,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1663
1697
  }
1664
1698
  debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1665
1699
  }
1666
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1700
+ warnDevice('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1667
1701
  return false;
1668
1702
  }
1669
1703
  constructor(deviceId, options){
@@ -706,6 +706,9 @@ var __webpack_exports__ = {};
706
706
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
707
707
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
708
708
  const debugDevice = (0, logger_.getDebug)('android:device');
709
+ const warnDevice = (0, logger_.getDebug)('android:device', {
710
+ console: true
711
+ });
709
712
  function escapeForShell(text) {
710
713
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
711
714
  }
@@ -885,7 +888,7 @@ var __webpack_exports__ = {};
885
888
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
886
889
  } catch (error) {
887
890
  const msg = error instanceof Error ? error.message : String(error);
888
- console.warn(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
891
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
889
892
  }
890
893
  return adb;
891
894
  }
@@ -1253,6 +1256,22 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1253
1256
  y: endY
1254
1257
  };
1255
1258
  }
1259
+ warnScrollDistanceClamped(direction, requestedDistance, appliedDistance) {
1260
+ if (requestedDistance <= appliedDistance) return;
1261
+ const scrollToSuggestion = {
1262
+ down: 'scrollToBottom',
1263
+ up: 'scrollToTop',
1264
+ left: 'scrollToLeft',
1265
+ right: 'scrollToRight'
1266
+ };
1267
+ const edgeLabel = {
1268
+ down: 'bottom',
1269
+ up: 'top',
1270
+ left: 'left edge',
1271
+ right: 'right edge'
1272
+ };
1273
+ warnDevice(`[midscene] Android ADB swipe coordinates must stay within the screen bounds. The requested scroll distance (${requestedDistance}px) exceeds the maximum single swipe distance (${appliedDistance}px) from the current start point, so it will be clamped. If you want to scroll to the ${edgeLabel[direction]}, use ${scrollToSuggestion[direction]} instead.`);
1274
+ }
1256
1275
  async screenshotBase64() {
1257
1276
  debugDevice('screenshotBase64 begin');
1258
1277
  const adapter = this.getScrcpyAdapter();
@@ -1437,58 +1456,66 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1437
1456
  async scrollUp(distance, startPoint) {
1438
1457
  const { height } = await this.size();
1439
1458
  const scrollDistance = Math.round(distance || height);
1459
+ const hasExplicitDistance = void 0 !== distance;
1440
1460
  if (startPoint) {
1441
1461
  const start = {
1442
1462
  x: Math.round(startPoint.left),
1443
1463
  y: Math.round(startPoint.top)
1444
1464
  };
1445
1465
  const end = this.calculateScrollEndPoint(start, 0, scrollDistance, 0, height);
1466
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('up', scrollDistance, Math.abs(end.y - start.y));
1446
1467
  await this.mouseDrag(start, end);
1447
1468
  return;
1448
1469
  }
1449
- await this.scroll(0, -scrollDistance);
1470
+ await this.scroll(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
1450
1471
  }
1451
1472
  async scrollDown(distance, startPoint) {
1452
1473
  const { height } = await this.size();
1453
1474
  const scrollDistance = Math.round(distance || height);
1475
+ const hasExplicitDistance = void 0 !== distance;
1454
1476
  if (startPoint) {
1455
1477
  const start = {
1456
1478
  x: Math.round(startPoint.left),
1457
1479
  y: Math.round(startPoint.top)
1458
1480
  };
1459
1481
  const end = this.calculateScrollEndPoint(start, 0, -scrollDistance, 0, height);
1482
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('down', scrollDistance, Math.abs(end.y - start.y));
1460
1483
  await this.mouseDrag(start, end);
1461
1484
  return;
1462
1485
  }
1463
- await this.scroll(0, scrollDistance);
1486
+ await this.scroll(0, scrollDistance, void 0, hasExplicitDistance, 'down');
1464
1487
  }
1465
1488
  async scrollLeft(distance, startPoint) {
1466
1489
  const { width } = await this.size();
1467
1490
  const scrollDistance = Math.round(distance || width);
1491
+ const hasExplicitDistance = void 0 !== distance;
1468
1492
  if (startPoint) {
1469
1493
  const start = {
1470
1494
  x: Math.round(startPoint.left),
1471
1495
  y: Math.round(startPoint.top)
1472
1496
  };
1473
1497
  const end = this.calculateScrollEndPoint(start, scrollDistance, 0, width, 0);
1498
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('left', scrollDistance, Math.abs(end.x - start.x));
1474
1499
  await this.mouseDrag(start, end);
1475
1500
  return;
1476
1501
  }
1477
- await this.scroll(-scrollDistance, 0);
1502
+ await this.scroll(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
1478
1503
  }
1479
1504
  async scrollRight(distance, startPoint) {
1480
1505
  const { width } = await this.size();
1481
1506
  const scrollDistance = Math.round(distance || width);
1507
+ const hasExplicitDistance = void 0 !== distance;
1482
1508
  if (startPoint) {
1483
1509
  const start = {
1484
1510
  x: Math.round(startPoint.left),
1485
1511
  y: Math.round(startPoint.top)
1486
1512
  };
1487
1513
  const end = this.calculateScrollEndPoint(start, -scrollDistance, 0, width, 0);
1514
+ if (hasExplicitDistance) this.warnScrollDistanceClamped('right', scrollDistance, Math.abs(end.x - start.x));
1488
1515
  await this.mouseDrag(start, end);
1489
1516
  return;
1490
1517
  }
1491
- await this.scroll(scrollDistance, 0);
1518
+ await this.scroll(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
1492
1519
  }
1493
1520
  async ensureYadb() {
1494
1521
  if (!this.yadbPushed) {
@@ -1588,7 +1615,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1588
1615
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1589
1616
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1590
1617
  }
1591
- async scroll(deltaX, deltaY, duration) {
1618
+ async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
1592
1619
  if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
1593
1620
  const { width, height } = await this.size();
1594
1621
  const n = 4;
@@ -1598,8 +1625,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1598
1625
  const maxNegativeDeltaX = width - startX;
1599
1626
  const maxPositiveDeltaY = startY;
1600
1627
  const maxNegativeDeltaY = height - startY;
1628
+ const originalDeltaX = deltaX;
1629
+ const originalDeltaY = deltaY;
1601
1630
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1602
1631
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1632
+ if (warnOnClamp && direction && (deltaX !== originalDeltaX || deltaY !== originalDeltaY)) {
1633
+ const requestedDistance = 'left' === direction || 'right' === direction ? Math.abs(originalDeltaX) : Math.abs(originalDeltaY);
1634
+ const appliedDistance = 'left' === direction || 'right' === direction ? Math.abs(deltaX) : Math.abs(deltaY);
1635
+ this.warnScrollDistanceClamped(direction, requestedDistance, appliedDistance);
1636
+ }
1603
1637
  const endX = Math.round(startX - deltaX);
1604
1638
  const endY = Math.round(startY - deltaY);
1605
1639
  const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
@@ -1756,7 +1790,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1756
1790
  }
1757
1791
  debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1758
1792
  }
1759
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1793
+ warnDevice('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1760
1794
  return false;
1761
1795
  }
1762
1796
  constructor(deviceId, options){
@@ -1986,7 +2020,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1986
2020
  constructor(toolsManager){
1987
2021
  super({
1988
2022
  name: '@midscene/android-mcp',
1989
- version: "1.7.4",
2023
+ version: "1.7.5-beta-20260420031652.0",
1990
2024
  description: 'Control the Android device using natural language commands'
1991
2025
  }, toolsManager);
1992
2026
  }
@@ -168,6 +168,7 @@ export declare class AndroidDevice implements AbstractInterface {
168
168
  * @returns The calculated end point for the scroll gesture
169
169
  */
170
170
  private calculateScrollEndPoint;
171
+ private warnScrollDistanceClamped;
171
172
  screenshotBase64(): Promise<string>;
172
173
  clearInput(element?: ElementInfo): Promise<void>;
173
174
  forceScreenshot(path: string): Promise<void>;
@@ -206,7 +207,7 @@ export declare class AndroidDevice implements AbstractInterface {
206
207
  x: number;
207
208
  y: number;
208
209
  }, duration?: number): Promise<void>;
209
- scroll(deltaX: number, deltaY: number, duration?: number): Promise<void>;
210
+ scroll(deltaX: number, deltaY: number, duration?: number, warnOnClamp?: boolean, direction?: ScrollDirection): Promise<void>;
210
211
  destroy(): Promise<void>;
211
212
  /**
212
213
  * Get the current time from the Android device.
@@ -445,6 +446,8 @@ declare interface ScrcpyScreenshotOptions {
445
446
  idleTimeoutMs?: number;
446
447
  }
447
448
 
449
+ declare type ScrollDirection = 'up' | 'down' | 'left' | 'right';
450
+
448
451
  /**
449
452
  * Helper type to convert DeviceAction to wrapped method signature
450
453
  */
@@ -160,6 +160,7 @@ declare class AndroidDevice implements AbstractInterface {
160
160
  * @returns The calculated end point for the scroll gesture
161
161
  */
162
162
  private calculateScrollEndPoint;
163
+ private warnScrollDistanceClamped;
163
164
  screenshotBase64(): Promise<string>;
164
165
  clearInput(element?: ElementInfo): Promise<void>;
165
166
  forceScreenshot(path: string): Promise<void>;
@@ -198,7 +199,7 @@ declare class AndroidDevice implements AbstractInterface {
198
199
  x: number;
199
200
  y: number;
200
201
  }, duration?: number): Promise<void>;
201
- scroll(deltaX: number, deltaY: number, duration?: number): Promise<void>;
202
+ scroll(deltaX: number, deltaY: number, duration?: number, warnOnClamp?: boolean, direction?: ScrollDirection): Promise<void>;
202
203
  destroy(): Promise<void>;
203
204
  /**
204
205
  * Get the current time from the Android device.
@@ -270,6 +271,8 @@ export declare function mcpServerForAgent(agent: Agent | AndroidAgent): {
270
271
  launchHttp(options: LaunchMCPServerOptions): Promise<LaunchMCPServerResult>;
271
272
  };
272
273
 
274
+ declare type ScrollDirection = 'up' | 'down' | 'left' | 'right';
275
+
273
276
  /**
274
277
  * Helper type to convert DeviceAction to wrapped method signature
275
278
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/android",
3
- "version": "1.7.4",
3
+ "version": "1.7.5-beta-20260420031652.0",
4
4
  "description": "Android automation library for Midscene",
5
5
  "keywords": [
6
6
  "Android UI automation",
@@ -41,8 +41,8 @@
41
41
  "@yume-chan/stream-extra": "2.1.0",
42
42
  "appium-adb": "12.12.1",
43
43
  "sharp": "^0.34.3",
44
- "@midscene/shared": "1.7.4",
45
- "@midscene/core": "1.7.4"
44
+ "@midscene/core": "1.7.5-beta-20260420031652.0",
45
+ "@midscene/shared": "1.7.5-beta-20260420031652.0"
46
46
  },
47
47
  "optionalDependencies": {
48
48
  "@ffmpeg-installer/ffmpeg": "^1.1.0"
@@ -51,12 +51,12 @@
51
51
  "@rslib/core": "^0.18.3",
52
52
  "@types/node": "^18.0.0",
53
53
  "dotenv": "^16.4.5",
54
- "gh-release-fetch": "^4.0.3",
55
54
  "typescript": "^5.8.3",
56
55
  "tsx": "^4.19.2",
56
+ "undici": "^6.0.0",
57
57
  "vitest": "3.0.5",
58
58
  "zod": "^3.25.1",
59
- "@midscene/playground": "1.7.4"
59
+ "@midscene/playground": "1.7.5-beta-20260420031652.0"
60
60
  },
61
61
  "license": "MIT",
62
62
  "scripts": {