@jobshimo/browser-link 0.23.6 → 0.23.8
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/agent-instructions/content.js +17 -7
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/cdp/flow.d.ts +74 -10
- package/dist/cdp/flow.js +56 -19
- package/dist/cdp/flow.js.map +1 -1
- package/dist/cdp/inpage/builders.d.ts +17 -2
- package/dist/cdp/inpage/builders.js +13 -8
- package/dist/cdp/inpage/builders.js.map +1 -1
- package/dist/cdp/inpage/deep-query.d.ts +1 -1
- package/dist/cdp/inpage/deep-query.js +27 -1
- package/dist/cdp/inpage/deep-query.js.map +1 -1
- package/dist/cdp/inpage/dom-helpers.d.ts +1 -1
- package/dist/cdp/inpage/dom-helpers.js +16 -0
- package/dist/cdp/inpage/dom-helpers.js.map +1 -1
- package/dist/cdp/transport.js +7 -0
- package/dist/cdp/transport.js.map +1 -1
- package/dist/commands/about.js +2 -2
- package/dist/commands/about.js.map +1 -1
- package/dist/extension/background.js +297 -276
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/flow.d.ts +74 -10
- package/dist/extension/flow.js +56 -19
- package/dist/extension/flow.js.map +1 -1
- package/dist/extension/inpage/builders.d.ts +17 -2
- package/dist/extension/inpage/builders.js +13 -8
- package/dist/extension/inpage/builders.js.map +1 -1
- package/dist/extension/inpage/deep-query.d.ts +1 -1
- package/dist/extension/inpage/deep-query.js +27 -1
- package/dist/extension/inpage/deep-query.js.map +1 -1
- package/dist/extension/inpage/dom-helpers.d.ts +1 -1
- package/dist/extension/inpage/dom-helpers.js +16 -0
- package/dist/extension/inpage/dom-helpers.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/map/tools.js +2 -2
- package/dist/map/tools.js.map +1 -1
- package/dist/permissions.js +1 -1
- package/dist/permissions.js.map +1 -1
- package/dist/tools/browser-definitions.js +36 -8
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.js +62 -2
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.js +10 -4
- package/dist/tools/server-instructions.js.map +1 -1
- package/package.json +1 -1
|
@@ -631,8 +631,9 @@ async function waitForLoad(tabId, timeoutMs = 20_000) {
|
|
|
631
631
|
}
|
|
632
632
|
/**
|
|
633
633
|
* `performFind` / `performClick` / `performType` / `performPress` /
|
|
634
|
-
* `performWaitFor` — the extracted bodies of the
|
|
635
|
-
* `click` / `type` / `press` / `wait_for`
|
|
634
|
+
* `performWaitFor` / `performDrag` — the extracted bodies of the
|
|
635
|
+
* standalone `find` / `click` / `type` / `press` / `wait_for` / `drag`
|
|
636
|
+
* cases in `handleTool` below.
|
|
636
637
|
*
|
|
637
638
|
* Both the standalone `case` handlers AND `browser.flow`'s step executor
|
|
638
639
|
* (`runFlow` in `./flow.js`) call these exact functions — there is one
|
|
@@ -706,6 +707,8 @@ async function performClick(tabId, params) {
|
|
|
706
707
|
});
|
|
707
708
|
const settleResult = await runSettle(tabId, settle);
|
|
708
709
|
const result = { clicked: selector, tag: resolved.tag };
|
|
710
|
+
if (resolved.hit_element)
|
|
711
|
+
result.hit_element = resolved.hit_element;
|
|
709
712
|
if (settleResult)
|
|
710
713
|
result.settle = settleResult;
|
|
711
714
|
return { ok: true, result };
|
|
@@ -862,6 +865,279 @@ async function performWaitFor(tabId, state, params) {
|
|
|
862
865
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
863
866
|
}
|
|
864
867
|
}
|
|
868
|
+
async function performDrag(tabId, params) {
|
|
869
|
+
try {
|
|
870
|
+
// Same numeric guard the wire boundary used before this body was
|
|
871
|
+
// extracted out of `handleDrag`: anything non-finite or negative is
|
|
872
|
+
// treated as absent, so flow drag steps get identical protection.
|
|
873
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
|
|
874
|
+
const clamp = (v, max) => Math.min(v, max);
|
|
875
|
+
const fromSelector = params.from_selector;
|
|
876
|
+
const toSelector = params.to_selector;
|
|
877
|
+
const fromXRaw = num(params.from_x);
|
|
878
|
+
const fromYRaw = num(params.from_y);
|
|
879
|
+
const toXRaw = num(params.to_x);
|
|
880
|
+
const toYRaw = num(params.to_y);
|
|
881
|
+
// Cap every duration that ends up in a setTimeout to keep CodeQL's
|
|
882
|
+
// "user-controlled timer duration" check happy and to prevent a
|
|
883
|
+
// misbehaving agent from parking the bridge for hours on a typo.
|
|
884
|
+
const durationMs = clamp(num(params.duration_ms) ?? 1500, MAX_DRAG_DURATION_MS);
|
|
885
|
+
const holdBeforeMoveMs = clamp(num(params.hold_before_move_ms) ?? 0, MAX_DRAG_HOLD_MS);
|
|
886
|
+
const holdBeforeReleaseMs = clamp(num(params.hold_before_release_ms) ?? 0, MAX_DRAG_HOLD_MS);
|
|
887
|
+
const hasFromCoords = fromXRaw !== undefined && fromYRaw !== undefined;
|
|
888
|
+
const hasToCoords = toXRaw !== undefined && toYRaw !== undefined;
|
|
889
|
+
if (!fromSelector && !hasFromCoords) {
|
|
890
|
+
return { ok: false, error: 'drag: provide from_selector or both from_x and from_y' };
|
|
891
|
+
}
|
|
892
|
+
if (!toSelector && !hasToCoords) {
|
|
893
|
+
return { ok: false, error: 'drag: provide to_selector or both to_x and to_y' };
|
|
894
|
+
}
|
|
895
|
+
const probeExpr = buildDragProbeJs({
|
|
896
|
+
from_selector: fromSelector,
|
|
897
|
+
to_selector: toSelector,
|
|
898
|
+
from_x: fromXRaw,
|
|
899
|
+
from_y: fromYRaw,
|
|
900
|
+
to_x: toXRaw,
|
|
901
|
+
to_y: toYRaw,
|
|
902
|
+
});
|
|
903
|
+
const probe = await evaluateInTab(tabId, probeExpr);
|
|
904
|
+
if (probe.err) {
|
|
905
|
+
return { ok: false, error: `drag: ${probe.err}` };
|
|
906
|
+
}
|
|
907
|
+
if (!probe.from || !probe.to) {
|
|
908
|
+
return { ok: false, error: 'drag: could not resolve coordinates' };
|
|
909
|
+
}
|
|
910
|
+
if (!probe.from.in_viewport) {
|
|
911
|
+
return {
|
|
912
|
+
ok: false,
|
|
913
|
+
error: 'drag: source point is offscreen — scroll first or pass viewport coords',
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
if (!probe.to.in_viewport) {
|
|
917
|
+
return {
|
|
918
|
+
ok: false,
|
|
919
|
+
error: 'drag: destination point is offscreen — scroll first or pass viewport coords',
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
const fromX = probe.from.x;
|
|
923
|
+
const fromY = probe.from.y;
|
|
924
|
+
const toX = probe.to.x;
|
|
925
|
+
const toY = probe.to.y;
|
|
926
|
+
const isDraggable = !!probe.draggable;
|
|
927
|
+
// ~30fps interpolation; minimum 2 steps so the path actually has a midpoint.
|
|
928
|
+
const steps = durationMs > 0 ? Math.max(2, Math.round(durationMs / 33)) : 1;
|
|
929
|
+
const stepDelayMs = steps > 0 ? durationMs / steps : 0;
|
|
930
|
+
const eventsFired = [];
|
|
931
|
+
// Every branch below reassigns `dragMode` before the final `return`
|
|
932
|
+
// reads it. (The old `case`-block extraction needed a
|
|
933
|
+
// no-useless-assignment disable here; inside this try/catch scope the
|
|
934
|
+
// linter no longer flags the initializer, so the directive is gone.)
|
|
935
|
+
let dragMode = 'pointer';
|
|
936
|
+
let interceptReceived = false;
|
|
937
|
+
const dragStart = Date.now();
|
|
938
|
+
let interceptionEnabled = false;
|
|
939
|
+
const interpolate = (t) => ({
|
|
940
|
+
x: fromX + (toX - fromX) * t,
|
|
941
|
+
y: fromY + (toY - fromY) * t,
|
|
942
|
+
});
|
|
943
|
+
// Mouse move WITH the left button held down. Chrome's HTML5 drag
|
|
944
|
+
// system only treats movement as a drag when the button state is
|
|
945
|
+
// signalled on every move after mousePressed; omitting it makes
|
|
946
|
+
// Blink ignore the move for drag-detection purposes.
|
|
947
|
+
const moveHeld = (x, y) => cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
948
|
+
type: 'mouseMoved',
|
|
949
|
+
x,
|
|
950
|
+
y,
|
|
951
|
+
button: 'left',
|
|
952
|
+
buttons: 1,
|
|
953
|
+
});
|
|
954
|
+
// Wiggle distance has to comfortably clear `kDragThreshold` (~5px on
|
|
955
|
+
// Mac, 4px on Linux/Windows). 5px landed *right* at the boundary and
|
|
956
|
+
// Chrome did not start the drag — 20px crosses it on every platform.
|
|
957
|
+
const WIGGLE_PX = 20;
|
|
958
|
+
// Time to wait for Input.dragIntercepted after the wiggle. 120ms was
|
|
959
|
+
// enough on local-only synthetic tests but flaky in real Chrome —
|
|
960
|
+
// 250ms is comfortable without dragging out the overall latency.
|
|
961
|
+
const INTERCEPT_TIMEOUT_MS = 250;
|
|
962
|
+
try {
|
|
963
|
+
if (isDraggable) {
|
|
964
|
+
try {
|
|
965
|
+
await cdp(tabId, 'Input.setInterceptDrags', { enabled: true });
|
|
966
|
+
interceptionEnabled = true;
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
// setInterceptDrags is experimental — fall back to pointer mode silently.
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (interceptionEnabled) {
|
|
973
|
+
// Arm the listener BEFORE the press+wiggle that may trigger it.
|
|
974
|
+
const interceptPromise = waitForCdpEvent(tabId, 'Input.dragIntercepted', INTERCEPT_TIMEOUT_MS);
|
|
975
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
976
|
+
type: 'mouseMoved',
|
|
977
|
+
x: fromX,
|
|
978
|
+
y: fromY,
|
|
979
|
+
});
|
|
980
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
981
|
+
type: 'mousePressed',
|
|
982
|
+
x: fromX,
|
|
983
|
+
y: fromY,
|
|
984
|
+
button: 'left',
|
|
985
|
+
clickCount: 1,
|
|
986
|
+
});
|
|
987
|
+
if (holdBeforeMoveMs > 0)
|
|
988
|
+
await sleep(holdBeforeMoveMs);
|
|
989
|
+
// Wiggle toward the destination so Chrome's native drag system
|
|
990
|
+
// crosses its activation threshold. Direction matters for libs
|
|
991
|
+
// that have direction-sensitive activation constraints.
|
|
992
|
+
const dx = toX - fromX;
|
|
993
|
+
const dy = toY - fromY;
|
|
994
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
995
|
+
const wx = fromX + (dx / len) * WIGGLE_PX;
|
|
996
|
+
const wy = fromY + (dy / len) * WIGGLE_PX;
|
|
997
|
+
await moveHeld(wx, wy);
|
|
998
|
+
const intercepted = await interceptPromise;
|
|
999
|
+
if (intercepted) {
|
|
1000
|
+
dragMode = 'html5';
|
|
1001
|
+
interceptReceived = true;
|
|
1002
|
+
eventsFired.push('Input.dragIntercepted');
|
|
1003
|
+
const dragData = intercepted.data;
|
|
1004
|
+
for (let i = 1; i <= steps; i++) {
|
|
1005
|
+
const t = i / steps;
|
|
1006
|
+
const { x, y } = interpolate(t);
|
|
1007
|
+
await moveHeld(x, y);
|
|
1008
|
+
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
1009
|
+
type: 'dragOver',
|
|
1010
|
+
x,
|
|
1011
|
+
y,
|
|
1012
|
+
data: dragData,
|
|
1013
|
+
});
|
|
1014
|
+
if (i === 1)
|
|
1015
|
+
eventsFired.push('dragOver');
|
|
1016
|
+
if (stepDelayMs > 0)
|
|
1017
|
+
await sleep(stepDelayMs);
|
|
1018
|
+
}
|
|
1019
|
+
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
1020
|
+
type: 'dragEnter',
|
|
1021
|
+
x: toX,
|
|
1022
|
+
y: toY,
|
|
1023
|
+
data: dragData,
|
|
1024
|
+
});
|
|
1025
|
+
eventsFired.push('dragEnter');
|
|
1026
|
+
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
1027
|
+
type: 'dragOver',
|
|
1028
|
+
x: toX,
|
|
1029
|
+
y: toY,
|
|
1030
|
+
data: dragData,
|
|
1031
|
+
});
|
|
1032
|
+
if (holdBeforeReleaseMs > 0)
|
|
1033
|
+
await sleep(holdBeforeReleaseMs);
|
|
1034
|
+
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
1035
|
+
type: 'drop',
|
|
1036
|
+
x: toX,
|
|
1037
|
+
y: toY,
|
|
1038
|
+
data: dragData,
|
|
1039
|
+
});
|
|
1040
|
+
eventsFired.push('drop');
|
|
1041
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1042
|
+
type: 'mouseReleased',
|
|
1043
|
+
x: toX,
|
|
1044
|
+
y: toY,
|
|
1045
|
+
button: 'left',
|
|
1046
|
+
clickCount: 1,
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
// Element was tagged draggable but no native drag fired — the page
|
|
1051
|
+
// either preventDefault'd dragstart or wires its own pointer logic.
|
|
1052
|
+
// Continue with pointer-only events from where we already pressed.
|
|
1053
|
+
dragMode = 'pointer';
|
|
1054
|
+
for (let i = 1; i <= steps; i++) {
|
|
1055
|
+
const t = i / steps;
|
|
1056
|
+
const { x, y } = interpolate(t);
|
|
1057
|
+
await moveHeld(x, y);
|
|
1058
|
+
if (stepDelayMs > 0)
|
|
1059
|
+
await sleep(stepDelayMs);
|
|
1060
|
+
}
|
|
1061
|
+
if (holdBeforeReleaseMs > 0)
|
|
1062
|
+
await sleep(holdBeforeReleaseMs);
|
|
1063
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1064
|
+
type: 'mouseReleased',
|
|
1065
|
+
x: toX,
|
|
1066
|
+
y: toY,
|
|
1067
|
+
button: 'left',
|
|
1068
|
+
clickCount: 1,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
// Pointer-only branch: no native drag involvement at all.
|
|
1074
|
+
dragMode = 'pointer';
|
|
1075
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1076
|
+
type: 'mouseMoved',
|
|
1077
|
+
x: fromX,
|
|
1078
|
+
y: fromY,
|
|
1079
|
+
});
|
|
1080
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1081
|
+
type: 'mousePressed',
|
|
1082
|
+
x: fromX,
|
|
1083
|
+
y: fromY,
|
|
1084
|
+
button: 'left',
|
|
1085
|
+
clickCount: 1,
|
|
1086
|
+
});
|
|
1087
|
+
if (holdBeforeMoveMs > 0)
|
|
1088
|
+
await sleep(holdBeforeMoveMs);
|
|
1089
|
+
for (let i = 1; i <= steps; i++) {
|
|
1090
|
+
const t = i / steps;
|
|
1091
|
+
const { x, y } = interpolate(t);
|
|
1092
|
+
await moveHeld(x, y);
|
|
1093
|
+
if (stepDelayMs > 0)
|
|
1094
|
+
await sleep(stepDelayMs);
|
|
1095
|
+
}
|
|
1096
|
+
if (holdBeforeReleaseMs > 0)
|
|
1097
|
+
await sleep(holdBeforeReleaseMs);
|
|
1098
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1099
|
+
type: 'mouseReleased',
|
|
1100
|
+
x: toX,
|
|
1101
|
+
y: toY,
|
|
1102
|
+
button: 'left',
|
|
1103
|
+
clickCount: 1,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
finally {
|
|
1108
|
+
// Critical: leaving interception on would block the human user from
|
|
1109
|
+
// dragging anything in this tab. Best-effort cleanup, swallow errors.
|
|
1110
|
+
if (interceptionEnabled) {
|
|
1111
|
+
try {
|
|
1112
|
+
await cdp(tabId, 'Input.setInterceptDrags', { enabled: false });
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
// ignore
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
ok: true,
|
|
1121
|
+
result: {
|
|
1122
|
+
from: { x: fromX, y: fromY, selector: fromSelector ?? null },
|
|
1123
|
+
to: { x: toX, y: toY, selector: toSelector ?? null },
|
|
1124
|
+
duration_ms_actual: Date.now() - dragStart,
|
|
1125
|
+
drag_mode: dragMode,
|
|
1126
|
+
interception_attempted: interceptionEnabled,
|
|
1127
|
+
intercept_received: interceptReceived,
|
|
1128
|
+
events_fired: eventsFired,
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
// The pre-extraction `case 'drag'` let thrown errors bubble to
|
|
1134
|
+
// `handleTool`'s outer catch, which produced this exact same
|
|
1135
|
+
// `err.message` string — converting here instead keeps the standalone
|
|
1136
|
+
// wire behavior identical while giving `runFlow` the `ActionOutcome`
|
|
1137
|
+
// shape it expects from every other perform* function.
|
|
1138
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
865
1141
|
/**
|
|
866
1142
|
* `handlePing` through `handleSetPermission` — the extracted bodies of the
|
|
867
1143
|
* remaining standalone tool cases in `handleTool` below that never got the
|
|
@@ -958,6 +1234,7 @@ async function handleFlow(tabId, state, msg, p) {
|
|
|
958
1234
|
performType: (params) => performType(tabId, params),
|
|
959
1235
|
performPress: (params) => performPress(tabId, params),
|
|
960
1236
|
performWaitFor: (params) => performWaitFor(tabId, state, params),
|
|
1237
|
+
performDrag: (params) => performDrag(tabId, params),
|
|
961
1238
|
buildRecoverySnapshot: () => evaluateInTab(tabId, buildSnapshotJs({ only_interactive: true, max_interactive: 40 })),
|
|
962
1239
|
});
|
|
963
1240
|
// The wire-level response is ok:true whenever the flow RAN (even a
|
|
@@ -967,285 +1244,29 @@ async function handleFlow(tabId, state, msg, p) {
|
|
|
967
1244
|
return { kind: 'tool.response', id: msg.id, ok: true, result: flowResult };
|
|
968
1245
|
}
|
|
969
1246
|
async function handleDrag(tabId, msg, p, optStr) {
|
|
1247
|
+
// Thin wire wrapper over performDrag — the same pattern the standalone
|
|
1248
|
+
// click/type/press/wait_for cases use over their perform* functions.
|
|
1249
|
+
// Numbers pass through with a plain typeof check; performDrag applies
|
|
1250
|
+
// the full finite/non-negative guard and the duration/hold clamps.
|
|
970
1251
|
const optNum = (key) => {
|
|
971
1252
|
const v = p[key];
|
|
972
|
-
return typeof v === 'number'
|
|
1253
|
+
return typeof v === 'number' ? v : undefined;
|
|
973
1254
|
};
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const durationMs = clamp(optNum('duration_ms') ?? 1500, MAX_DRAG_DURATION_MS);
|
|
985
|
-
const holdBeforeMoveMs = clamp(optNum('hold_before_move_ms') ?? 0, MAX_DRAG_HOLD_MS);
|
|
986
|
-
const holdBeforeReleaseMs = clamp(optNum('hold_before_release_ms') ?? 0, MAX_DRAG_HOLD_MS);
|
|
987
|
-
const hasFromCoords = fromXRaw !== undefined && fromYRaw !== undefined;
|
|
988
|
-
const hasToCoords = toXRaw !== undefined && toYRaw !== undefined;
|
|
989
|
-
if (!fromSelector && !hasFromCoords) {
|
|
990
|
-
return {
|
|
991
|
-
kind: 'tool.response',
|
|
992
|
-
id: msg.id,
|
|
993
|
-
ok: false,
|
|
994
|
-
error: 'drag: provide from_selector or both from_x and from_y',
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
if (!toSelector && !hasToCoords) {
|
|
998
|
-
return {
|
|
999
|
-
kind: 'tool.response',
|
|
1000
|
-
id: msg.id,
|
|
1001
|
-
ok: false,
|
|
1002
|
-
error: 'drag: provide to_selector or both to_x and to_y',
|
|
1003
|
-
};
|
|
1004
|
-
}
|
|
1005
|
-
const probeExpr = buildDragProbeJs({
|
|
1006
|
-
from_selector: fromSelector,
|
|
1007
|
-
to_selector: toSelector,
|
|
1008
|
-
from_x: fromXRaw,
|
|
1009
|
-
from_y: fromYRaw,
|
|
1010
|
-
to_x: toXRaw,
|
|
1011
|
-
to_y: toYRaw,
|
|
1255
|
+
const outcome = await performDrag(tabId, {
|
|
1256
|
+
from_selector: optStr('from_selector'),
|
|
1257
|
+
from_x: optNum('from_x'),
|
|
1258
|
+
from_y: optNum('from_y'),
|
|
1259
|
+
to_selector: optStr('to_selector'),
|
|
1260
|
+
to_x: optNum('to_x'),
|
|
1261
|
+
to_y: optNum('to_y'),
|
|
1262
|
+
duration_ms: optNum('duration_ms'),
|
|
1263
|
+
hold_before_move_ms: optNum('hold_before_move_ms'),
|
|
1264
|
+
hold_before_release_ms: optNum('hold_before_release_ms'),
|
|
1012
1265
|
});
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
return { kind: 'tool.response', id: msg.id, ok: false, error: `drag: ${probe.err}` };
|
|
1266
|
+
if (!outcome.ok) {
|
|
1267
|
+
return { kind: 'tool.response', id: msg.id, ok: false, error: outcome.error };
|
|
1016
1268
|
}
|
|
1017
|
-
|
|
1018
|
-
return {
|
|
1019
|
-
kind: 'tool.response',
|
|
1020
|
-
id: msg.id,
|
|
1021
|
-
ok: false,
|
|
1022
|
-
error: 'drag: could not resolve coordinates',
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
if (!probe.from.in_viewport) {
|
|
1026
|
-
return {
|
|
1027
|
-
kind: 'tool.response',
|
|
1028
|
-
id: msg.id,
|
|
1029
|
-
ok: false,
|
|
1030
|
-
error: 'drag: source point is offscreen — scroll first or pass viewport coords',
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
if (!probe.to.in_viewport) {
|
|
1034
|
-
return {
|
|
1035
|
-
kind: 'tool.response',
|
|
1036
|
-
id: msg.id,
|
|
1037
|
-
ok: false,
|
|
1038
|
-
error: 'drag: destination point is offscreen — scroll first or pass viewport coords',
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
const fromX = probe.from.x;
|
|
1042
|
-
const fromY = probe.from.y;
|
|
1043
|
-
const toX = probe.to.x;
|
|
1044
|
-
const toY = probe.to.y;
|
|
1045
|
-
const isDraggable = !!probe.draggable;
|
|
1046
|
-
// ~30fps interpolation; minimum 2 steps so the path actually has a midpoint.
|
|
1047
|
-
const steps = durationMs > 0 ? Math.max(2, Math.round(durationMs / 33)) : 1;
|
|
1048
|
-
const stepDelayMs = steps > 0 ? durationMs / steps : 0;
|
|
1049
|
-
const eventsFired = [];
|
|
1050
|
-
// Every branch below reassigns `dragMode` before the final `return` reads it.
|
|
1051
|
-
let dragMode = 'pointer'; // eslint-disable-line no-useless-assignment -- see comment above
|
|
1052
|
-
let interceptReceived = false;
|
|
1053
|
-
const dragStart = Date.now();
|
|
1054
|
-
let interceptionEnabled = false;
|
|
1055
|
-
const interpolate = (t) => ({
|
|
1056
|
-
x: fromX + (toX - fromX) * t,
|
|
1057
|
-
y: fromY + (toY - fromY) * t,
|
|
1058
|
-
});
|
|
1059
|
-
// Mouse move WITH the left button held down. Chrome's HTML5 drag
|
|
1060
|
-
// system only treats movement as a drag when the button state is
|
|
1061
|
-
// signalled on every move after mousePressed; omitting it makes
|
|
1062
|
-
// Blink ignore the move for drag-detection purposes.
|
|
1063
|
-
const moveHeld = (x, y) => cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1064
|
-
type: 'mouseMoved',
|
|
1065
|
-
x,
|
|
1066
|
-
y,
|
|
1067
|
-
button: 'left',
|
|
1068
|
-
buttons: 1,
|
|
1069
|
-
});
|
|
1070
|
-
// Wiggle distance has to comfortably clear `kDragThreshold` (~5px on
|
|
1071
|
-
// Mac, 4px on Linux/Windows). 5px landed *right* at the boundary and
|
|
1072
|
-
// Chrome did not start the drag — 20px crosses it on every platform.
|
|
1073
|
-
const WIGGLE_PX = 20;
|
|
1074
|
-
// Time to wait for Input.dragIntercepted after the wiggle. 120ms was
|
|
1075
|
-
// enough on local-only synthetic tests but flaky in real Chrome —
|
|
1076
|
-
// 250ms is comfortable without dragging out the overall latency.
|
|
1077
|
-
const INTERCEPT_TIMEOUT_MS = 250;
|
|
1078
|
-
try {
|
|
1079
|
-
if (isDraggable) {
|
|
1080
|
-
try {
|
|
1081
|
-
await cdp(tabId, 'Input.setInterceptDrags', { enabled: true });
|
|
1082
|
-
interceptionEnabled = true;
|
|
1083
|
-
}
|
|
1084
|
-
catch {
|
|
1085
|
-
// setInterceptDrags is experimental — fall back to pointer mode silently.
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
if (interceptionEnabled) {
|
|
1089
|
-
// Arm the listener BEFORE the press+wiggle that may trigger it.
|
|
1090
|
-
const interceptPromise = waitForCdpEvent(tabId, 'Input.dragIntercepted', INTERCEPT_TIMEOUT_MS);
|
|
1091
|
-
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1092
|
-
type: 'mouseMoved',
|
|
1093
|
-
x: fromX,
|
|
1094
|
-
y: fromY,
|
|
1095
|
-
});
|
|
1096
|
-
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1097
|
-
type: 'mousePressed',
|
|
1098
|
-
x: fromX,
|
|
1099
|
-
y: fromY,
|
|
1100
|
-
button: 'left',
|
|
1101
|
-
clickCount: 1,
|
|
1102
|
-
});
|
|
1103
|
-
if (holdBeforeMoveMs > 0)
|
|
1104
|
-
await sleep(holdBeforeMoveMs);
|
|
1105
|
-
// Wiggle toward the destination so Chrome's native drag system
|
|
1106
|
-
// crosses its activation threshold. Direction matters for libs
|
|
1107
|
-
// that have direction-sensitive activation constraints.
|
|
1108
|
-
const dx = toX - fromX;
|
|
1109
|
-
const dy = toY - fromY;
|
|
1110
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
1111
|
-
const wx = fromX + (dx / len) * WIGGLE_PX;
|
|
1112
|
-
const wy = fromY + (dy / len) * WIGGLE_PX;
|
|
1113
|
-
await moveHeld(wx, wy);
|
|
1114
|
-
const intercepted = await interceptPromise;
|
|
1115
|
-
if (intercepted) {
|
|
1116
|
-
dragMode = 'html5';
|
|
1117
|
-
interceptReceived = true;
|
|
1118
|
-
eventsFired.push('Input.dragIntercepted');
|
|
1119
|
-
const dragData = intercepted.data;
|
|
1120
|
-
for (let i = 1; i <= steps; i++) {
|
|
1121
|
-
const t = i / steps;
|
|
1122
|
-
const { x, y } = interpolate(t);
|
|
1123
|
-
await moveHeld(x, y);
|
|
1124
|
-
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
1125
|
-
type: 'dragOver',
|
|
1126
|
-
x,
|
|
1127
|
-
y,
|
|
1128
|
-
data: dragData,
|
|
1129
|
-
});
|
|
1130
|
-
if (i === 1)
|
|
1131
|
-
eventsFired.push('dragOver');
|
|
1132
|
-
if (stepDelayMs > 0)
|
|
1133
|
-
await sleep(stepDelayMs);
|
|
1134
|
-
}
|
|
1135
|
-
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
1136
|
-
type: 'dragEnter',
|
|
1137
|
-
x: toX,
|
|
1138
|
-
y: toY,
|
|
1139
|
-
data: dragData,
|
|
1140
|
-
});
|
|
1141
|
-
eventsFired.push('dragEnter');
|
|
1142
|
-
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
1143
|
-
type: 'dragOver',
|
|
1144
|
-
x: toX,
|
|
1145
|
-
y: toY,
|
|
1146
|
-
data: dragData,
|
|
1147
|
-
});
|
|
1148
|
-
if (holdBeforeReleaseMs > 0)
|
|
1149
|
-
await sleep(holdBeforeReleaseMs);
|
|
1150
|
-
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
1151
|
-
type: 'drop',
|
|
1152
|
-
x: toX,
|
|
1153
|
-
y: toY,
|
|
1154
|
-
data: dragData,
|
|
1155
|
-
});
|
|
1156
|
-
eventsFired.push('drop');
|
|
1157
|
-
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1158
|
-
type: 'mouseReleased',
|
|
1159
|
-
x: toX,
|
|
1160
|
-
y: toY,
|
|
1161
|
-
button: 'left',
|
|
1162
|
-
clickCount: 1,
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
else {
|
|
1166
|
-
// Element was tagged draggable but no native drag fired — the page
|
|
1167
|
-
// either preventDefault'd dragstart or wires its own pointer logic.
|
|
1168
|
-
// Continue with pointer-only events from where we already pressed.
|
|
1169
|
-
dragMode = 'pointer';
|
|
1170
|
-
for (let i = 1; i <= steps; i++) {
|
|
1171
|
-
const t = i / steps;
|
|
1172
|
-
const { x, y } = interpolate(t);
|
|
1173
|
-
await moveHeld(x, y);
|
|
1174
|
-
if (stepDelayMs > 0)
|
|
1175
|
-
await sleep(stepDelayMs);
|
|
1176
|
-
}
|
|
1177
|
-
if (holdBeforeReleaseMs > 0)
|
|
1178
|
-
await sleep(holdBeforeReleaseMs);
|
|
1179
|
-
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1180
|
-
type: 'mouseReleased',
|
|
1181
|
-
x: toX,
|
|
1182
|
-
y: toY,
|
|
1183
|
-
button: 'left',
|
|
1184
|
-
clickCount: 1,
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
else {
|
|
1189
|
-
// Pointer-only branch: no native drag involvement at all.
|
|
1190
|
-
dragMode = 'pointer';
|
|
1191
|
-
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1192
|
-
type: 'mouseMoved',
|
|
1193
|
-
x: fromX,
|
|
1194
|
-
y: fromY,
|
|
1195
|
-
});
|
|
1196
|
-
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1197
|
-
type: 'mousePressed',
|
|
1198
|
-
x: fromX,
|
|
1199
|
-
y: fromY,
|
|
1200
|
-
button: 'left',
|
|
1201
|
-
clickCount: 1,
|
|
1202
|
-
});
|
|
1203
|
-
if (holdBeforeMoveMs > 0)
|
|
1204
|
-
await sleep(holdBeforeMoveMs);
|
|
1205
|
-
for (let i = 1; i <= steps; i++) {
|
|
1206
|
-
const t = i / steps;
|
|
1207
|
-
const { x, y } = interpolate(t);
|
|
1208
|
-
await moveHeld(x, y);
|
|
1209
|
-
if (stepDelayMs > 0)
|
|
1210
|
-
await sleep(stepDelayMs);
|
|
1211
|
-
}
|
|
1212
|
-
if (holdBeforeReleaseMs > 0)
|
|
1213
|
-
await sleep(holdBeforeReleaseMs);
|
|
1214
|
-
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
1215
|
-
type: 'mouseReleased',
|
|
1216
|
-
x: toX,
|
|
1217
|
-
y: toY,
|
|
1218
|
-
button: 'left',
|
|
1219
|
-
clickCount: 1,
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
finally {
|
|
1224
|
-
// Critical: leaving interception on would block the human user from
|
|
1225
|
-
// dragging anything in this tab. Best-effort cleanup, swallow errors.
|
|
1226
|
-
if (interceptionEnabled) {
|
|
1227
|
-
try {
|
|
1228
|
-
await cdp(tabId, 'Input.setInterceptDrags', { enabled: false });
|
|
1229
|
-
}
|
|
1230
|
-
catch {
|
|
1231
|
-
// ignore
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return {
|
|
1236
|
-
kind: 'tool.response',
|
|
1237
|
-
id: msg.id,
|
|
1238
|
-
ok: true,
|
|
1239
|
-
result: {
|
|
1240
|
-
from: { x: fromX, y: fromY, selector: fromSelector ?? null },
|
|
1241
|
-
to: { x: toX, y: toY, selector: toSelector ?? null },
|
|
1242
|
-
duration_ms_actual: Date.now() - dragStart,
|
|
1243
|
-
drag_mode: dragMode,
|
|
1244
|
-
interception_attempted: interceptionEnabled,
|
|
1245
|
-
intercept_received: interceptReceived,
|
|
1246
|
-
events_fired: eventsFired,
|
|
1247
|
-
},
|
|
1248
|
-
};
|
|
1269
|
+
return { kind: 'tool.response', id: msg.id, ok: true, result: outcome.result };
|
|
1249
1270
|
}
|
|
1250
1271
|
async function handleDialogRespond(tabId, msg, p, optStr) {
|
|
1251
1272
|
const accept = p.accept === true;
|