@skyramp/mcp 0.2.2 → 0.2.3
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/build/playwright/registerPlaywrightTools.js +1 -0
- package/node_modules/playwright/lib/mcp/skyramp/common/mouseActions.js +123 -0
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +46 -0
- package/node_modules/playwright/lib/mcp/skyramp/mouseActionTool.js +131 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +92 -4
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var mouseActions_exports = {};
|
|
20
|
+
__export(mouseActions_exports, {
|
|
21
|
+
DEFAULT_DRAG_STEPS: () => DEFAULT_DRAG_STEPS,
|
|
22
|
+
decodeModifierKeys: () => decodeModifierKeys,
|
|
23
|
+
decomposeDrag: () => decomposeDrag,
|
|
24
|
+
mouseActionToJsonl: () => mouseActionToJsonl,
|
|
25
|
+
mouseJsonlToCode: () => mouseJsonlToCode
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(mouseActions_exports);
|
|
28
|
+
const DEFAULT_DRAG_STEPS = 10;
|
|
29
|
+
function decodeModifierKeys(mask, platform = typeof process !== "undefined" ? process.platform : "linux") {
|
|
30
|
+
if (!mask)
|
|
31
|
+
return [];
|
|
32
|
+
const keys = [];
|
|
33
|
+
if (mask & 1) keys.push("Alt");
|
|
34
|
+
if ((mask & 6) === 6) {
|
|
35
|
+
keys.push(platform === "darwin" ? "Meta" : "Control");
|
|
36
|
+
} else {
|
|
37
|
+
if (mask & 2) keys.push("Control");
|
|
38
|
+
if (mask & 4) keys.push("Meta");
|
|
39
|
+
}
|
|
40
|
+
if (mask & 8) keys.push("Shift");
|
|
41
|
+
return keys;
|
|
42
|
+
}
|
|
43
|
+
function decomposeDrag(start, end, steps = DEFAULT_DRAG_STEPS) {
|
|
44
|
+
return [
|
|
45
|
+
{ name: "mouse.move", position: start },
|
|
46
|
+
{ name: "mouse.down" },
|
|
47
|
+
{ name: "mouse.move", position: end, steps },
|
|
48
|
+
{ name: "mouse.up" }
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
function mouseActionToJsonl(params) {
|
|
52
|
+
const button = params.button ?? "left";
|
|
53
|
+
const modifiers = params.modifiers ?? 0;
|
|
54
|
+
switch (params.action) {
|
|
55
|
+
case "move": {
|
|
56
|
+
if (params.x === void 0 || params.y === void 0)
|
|
57
|
+
return { error: 'mouse action "move" requires x and y.' };
|
|
58
|
+
const move = params.steps !== void 0 ? { name: "mouse.move", position: { x: params.x, y: params.y }, steps: params.steps } : { name: "mouse.move", position: { x: params.x, y: params.y } };
|
|
59
|
+
return { actions: [move] };
|
|
60
|
+
}
|
|
61
|
+
case "down":
|
|
62
|
+
return { actions: [button === "left" ? { name: "mouse.down" } : { name: "mouse.down", button }] };
|
|
63
|
+
case "up":
|
|
64
|
+
return { actions: [button === "left" ? { name: "mouse.up" } : { name: "mouse.up", button }] };
|
|
65
|
+
case "wheel": {
|
|
66
|
+
if (params.deltaX === void 0 && params.deltaY === void 0)
|
|
67
|
+
return { error: 'mouse action "wheel" requires deltaX and/or deltaY.' };
|
|
68
|
+
if (params.x === void 0 || params.y === void 0)
|
|
69
|
+
return { error: 'mouse action "wheel" requires x and y (the scroll target) so the pointer is moved there before scrolling.' };
|
|
70
|
+
const position = { x: params.x, y: params.y };
|
|
71
|
+
return { actions: [
|
|
72
|
+
{ name: "mouse.move", position },
|
|
73
|
+
{ name: "mouse.wheel", position, deltaX: params.deltaX ?? 0, deltaY: params.deltaY ?? 0, modifiers }
|
|
74
|
+
] };
|
|
75
|
+
}
|
|
76
|
+
case "click": {
|
|
77
|
+
if (params.x === void 0 || params.y === void 0)
|
|
78
|
+
return { error: 'mouse action "click" requires x and y.' };
|
|
79
|
+
return { actions: [{ name: "click", selector: "body", position: { x: params.x, y: params.y }, button, modifiers, clickCount: params.clickCount ?? 1 }] };
|
|
80
|
+
}
|
|
81
|
+
case "drag": {
|
|
82
|
+
if (params.x === void 0 || params.y === void 0 || params.endX === void 0 || params.endY === void 0)
|
|
83
|
+
return { error: 'mouse action "drag" requires x, y, endX and endY.' };
|
|
84
|
+
return { actions: decomposeDrag({ x: params.x, y: params.y }, { x: params.endX, y: params.endY }, params.steps) };
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
return { error: `Unknown mouse action: ${params.action}` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function mouseJsonlToCode(a) {
|
|
91
|
+
switch (a.name) {
|
|
92
|
+
case "mouse.move":
|
|
93
|
+
return [a.steps !== void 0 ? `await page.mouse.move(${a.position.x}, ${a.position.y}, { steps: ${a.steps} });` : `await page.mouse.move(${a.position.x}, ${a.position.y});`];
|
|
94
|
+
case "mouse.down":
|
|
95
|
+
return [a.button && a.button !== "left" ? `await page.mouse.down({ button: '${a.button}' });` : `await page.mouse.down();`];
|
|
96
|
+
case "mouse.up":
|
|
97
|
+
return [a.button && a.button !== "left" ? `await page.mouse.up({ button: '${a.button}' });` : `await page.mouse.up();`];
|
|
98
|
+
case "mouse.wheel":
|
|
99
|
+
return withModifierLines(a.modifiers, `await page.mouse.wheel(${a.deltaX}, ${a.deltaY});`);
|
|
100
|
+
case "click": {
|
|
101
|
+
const opts = a.button !== "left" || a.clickCount !== 1 ? `, { button: '${a.button}', clickCount: ${a.clickCount} }` : "";
|
|
102
|
+
return withModifierLines(a.modifiers, `await page.mouse.click(${a.position.x}, ${a.position.y}${opts});`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function withModifierLines(mask, line) {
|
|
107
|
+
const keys = decodeModifierKeys(mask);
|
|
108
|
+
if (!keys.length)
|
|
109
|
+
return [line];
|
|
110
|
+
return [
|
|
111
|
+
...keys.map((k) => `await page.keyboard.down('${k}');`),
|
|
112
|
+
line,
|
|
113
|
+
...[...keys].reverse().map((k) => `await page.keyboard.up('${k}');`)
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
117
|
+
0 && (module.exports = {
|
|
118
|
+
DEFAULT_DRAG_STEPS,
|
|
119
|
+
decodeModifierKeys,
|
|
120
|
+
decomposeDrag,
|
|
121
|
+
mouseActionToJsonl,
|
|
122
|
+
mouseJsonlToCode
|
|
123
|
+
});
|
|
@@ -31,6 +31,7 @@ module.exports = __toCommonJS(loadTraceTool_exports);
|
|
|
31
31
|
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
32
32
|
var import_utils = require("playwright-core/lib/utils");
|
|
33
33
|
var import_tool = require("../sdk/tool");
|
|
34
|
+
var import_mouseActions = require("./common/mouseActions");
|
|
34
35
|
const loadTraceSchema = {
|
|
35
36
|
name: "skyramp_load_trace",
|
|
36
37
|
title: "Load and replay a Skyramp trace",
|
|
@@ -183,6 +184,17 @@ function urlMatchesPattern(currentUrl, pattern) {
|
|
|
183
184
|
return (0, import_utils.urlMatches)(void 0, currentUrl, pattern);
|
|
184
185
|
return currentUrl.toLowerCase().includes(pattern.toLowerCase());
|
|
185
186
|
}
|
|
187
|
+
async function withMouseModifiers(page, mask, fn) {
|
|
188
|
+
const keys = (0, import_mouseActions.decodeModifierKeys)(mask);
|
|
189
|
+
for (const k of keys)
|
|
190
|
+
await page.keyboard.down(k);
|
|
191
|
+
try {
|
|
192
|
+
await fn();
|
|
193
|
+
} finally {
|
|
194
|
+
for (const k of [...keys].reverse())
|
|
195
|
+
await page.keyboard.up(k);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
186
198
|
const DEFAULT_ACTION_TIMEOUT = 15e3;
|
|
187
199
|
async function performClientAction(page, actionInContext) {
|
|
188
200
|
const action = actionInContext.action;
|
|
@@ -215,6 +227,40 @@ async function performClientAction(page, actionInContext) {
|
|
|
215
227
|
await page.keyboard.press(shortcut);
|
|
216
228
|
return;
|
|
217
229
|
}
|
|
230
|
+
if (action.name === "mouse.move") {
|
|
231
|
+
if (!action.position)
|
|
232
|
+
return;
|
|
233
|
+
if (action.steps !== void 0)
|
|
234
|
+
await page.mouse.move(action.position.x, action.position.y, { steps: action.steps });
|
|
235
|
+
else
|
|
236
|
+
await page.mouse.move(action.position.x, action.position.y);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (action.name === "mouse.down") {
|
|
240
|
+
if (action.button && action.button !== "left")
|
|
241
|
+
await page.mouse.down({ button: action.button });
|
|
242
|
+
else
|
|
243
|
+
await page.mouse.down();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (action.name === "mouse.up") {
|
|
247
|
+
if (action.button && action.button !== "left")
|
|
248
|
+
await page.mouse.up({ button: action.button });
|
|
249
|
+
else
|
|
250
|
+
await page.mouse.up();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (action.name === "mouse.wheel") {
|
|
254
|
+
await withMouseModifiers(page, action.modifiers, () => page.mouse.wheel(action.deltaX ?? 0, action.deltaY ?? 0));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (action.name === "click" && action.position && (!action.selector || action.selector === "body")) {
|
|
258
|
+
const button = action.button ?? "left";
|
|
259
|
+
const clickCount = action.clickCount ?? 1;
|
|
260
|
+
const needsOpts = button !== "left" || clickCount !== 1;
|
|
261
|
+
await withMouseModifiers(page, action.modifiers, () => needsOpts ? page.mouse.click(action.position.x, action.position.y, { button, clickCount }) : page.mouse.click(action.position.x, action.position.y));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
218
264
|
if (!action.selector)
|
|
219
265
|
return;
|
|
220
266
|
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var mouseActionTool_exports = {};
|
|
20
|
+
__export(mouseActionTool_exports, {
|
|
21
|
+
_schema: () => mouseActionSchema,
|
|
22
|
+
executeMouseAction: () => executeMouseAction,
|
|
23
|
+
mouseActionMcpTool: () => mouseActionMcpTool,
|
|
24
|
+
mouseActionSchema: () => mouseActionSchema
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(mouseActionTool_exports);
|
|
27
|
+
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
28
|
+
var import_tool = require("../sdk/tool");
|
|
29
|
+
var import_mouseActions = require("./common/mouseActions");
|
|
30
|
+
const mouseActionSchema = {
|
|
31
|
+
name: "browser_mouse_action",
|
|
32
|
+
title: "Mouse action (coordinate-based)",
|
|
33
|
+
description: [
|
|
34
|
+
"Perform coordinate-based mouse interactions: drag-and-drop, slider adjustment, canvas/diagram manipulation, scrolling, and other gestures that operate on viewport pixel positions rather than element references.",
|
|
35
|
+
"Primary uses: drag an item between positions (drag), move a slider thumb (drag), pan/draw on a canvas (move/down/up), and scroll a region (wheel).",
|
|
36
|
+
"The `action` parameter selects the gesture:",
|
|
37
|
+
"- drag: press at (x, y), move to (endX, endY), release. Decomposed into low-level mouse events (move\u2192down\u2192move\u2192up) so it works for sliders, canvas, and custom drag-handles where high-level element drag fails.",
|
|
38
|
+
"- move / down / up: individual pointer steps for composing custom gestures.",
|
|
39
|
+
"- wheel: scroll by deltaX / deltaY at the pointer position.",
|
|
40
|
+
'- click: a single position click at (x, y). Use for canvas hotspots, or as a fallback for an interactive element that has no usable accessibility ref or is occluded by an overlay (where browser_click reports "intercepts pointer events"). For ordinary elements, prefer browser_click.',
|
|
41
|
+
"Coordinates are viewport pixels, obtained from a snapshot or screenshot."
|
|
42
|
+
].join(" "),
|
|
43
|
+
inputSchema: import_mcpBundle.z.object({
|
|
44
|
+
action: import_mcpBundle.z.enum(["move", "down", "up", "click", "wheel", "drag"]).describe("The mouse action to perform."),
|
|
45
|
+
x: import_mcpBundle.z.number().optional().describe("X coordinate (viewport px). Required for move/click/wheel; the start X for drag."),
|
|
46
|
+
y: import_mcpBundle.z.number().optional().describe("Y coordinate (viewport px). Required for move/click/wheel; the start Y for drag."),
|
|
47
|
+
endX: import_mcpBundle.z.number().optional().describe("End X coordinate for drag."),
|
|
48
|
+
endY: import_mcpBundle.z.number().optional().describe("End Y coordinate for drag."),
|
|
49
|
+
button: import_mcpBundle.z.enum(["left", "right", "middle"]).optional().describe("Mouse button for down/up/click. Defaults to left."),
|
|
50
|
+
clickCount: import_mcpBundle.z.number().int().positive().optional().describe("Number of clicks for click (e.g. 2 for double-click). Defaults to 1."),
|
|
51
|
+
deltaX: import_mcpBundle.z.number().optional().describe("Horizontal scroll delta for wheel."),
|
|
52
|
+
deltaY: import_mcpBundle.z.number().optional().describe("Vertical scroll delta for wheel."),
|
|
53
|
+
steps: import_mcpBundle.z.number().int().positive().optional().describe("Intermediate move steps for move/drag (smoother movement)."),
|
|
54
|
+
modifiers: import_mcpBundle.z.number().int().min(0).max(15).optional().describe("Modifier bitmask (Alt=1, Control=2, Meta=4, Shift=8) for click/wheel. Range 0-15.")
|
|
55
|
+
}),
|
|
56
|
+
type: "action"
|
|
57
|
+
};
|
|
58
|
+
function mouseActionMcpTool() {
|
|
59
|
+
return (0, import_tool.toMcpTool)(mouseActionSchema);
|
|
60
|
+
}
|
|
61
|
+
async function executeMouseAction(page, params) {
|
|
62
|
+
const built = (0, import_mouseActions.mouseActionToJsonl)(params);
|
|
63
|
+
if ("error" in built)
|
|
64
|
+
return { result: { content: [{ type: "text", text: `### Error
|
|
65
|
+
${built.error}` }], isError: true }, actions: [] };
|
|
66
|
+
if (!page)
|
|
67
|
+
return { result: { content: [{ type: "text", text: "### Error\nNo open page. Call browser_navigate first." }], isError: true }, actions: [] };
|
|
68
|
+
const code = [];
|
|
69
|
+
try {
|
|
70
|
+
for (const a of built.actions) {
|
|
71
|
+
code.push(...(0, import_mouseActions.mouseJsonlToCode)(a));
|
|
72
|
+
await runOne(page, a);
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return { result: { content: [{ type: "text", text: `### Error
|
|
76
|
+
Mouse action failed: ${e.message}` }], isError: true }, actions: [] };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
result: { content: [{ type: "text", text: `### Ran Playwright code
|
|
80
|
+
${code.join("\n")}` }] },
|
|
81
|
+
actions: built.actions
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function runOne(page, a) {
|
|
85
|
+
switch (a.name) {
|
|
86
|
+
case "mouse.move":
|
|
87
|
+
if (a.steps !== void 0)
|
|
88
|
+
await page.mouse.move(a.position.x, a.position.y, { steps: a.steps });
|
|
89
|
+
else
|
|
90
|
+
await page.mouse.move(a.position.x, a.position.y);
|
|
91
|
+
return;
|
|
92
|
+
case "mouse.down":
|
|
93
|
+
if (a.button && a.button !== "left")
|
|
94
|
+
await page.mouse.down({ button: a.button });
|
|
95
|
+
else
|
|
96
|
+
await page.mouse.down();
|
|
97
|
+
return;
|
|
98
|
+
case "mouse.up":
|
|
99
|
+
if (a.button && a.button !== "left")
|
|
100
|
+
await page.mouse.up({ button: a.button });
|
|
101
|
+
else
|
|
102
|
+
await page.mouse.up();
|
|
103
|
+
return;
|
|
104
|
+
case "mouse.wheel":
|
|
105
|
+
await withModifiers(page, a.modifiers, () => page.mouse.wheel(a.deltaX, a.deltaY));
|
|
106
|
+
return;
|
|
107
|
+
case "click": {
|
|
108
|
+
const needsOpts = a.button !== "left" || a.clickCount !== 1;
|
|
109
|
+
await withModifiers(page, a.modifiers, () => needsOpts ? page.mouse.click(a.position.x, a.position.y, { button: a.button, clickCount: a.clickCount }) : page.mouse.click(a.position.x, a.position.y));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function withModifiers(page, mask, fn) {
|
|
115
|
+
const keys = (0, import_mouseActions.decodeModifierKeys)(mask);
|
|
116
|
+
for (const k of keys)
|
|
117
|
+
await page.keyboard.down(k);
|
|
118
|
+
try {
|
|
119
|
+
await fn();
|
|
120
|
+
} finally {
|
|
121
|
+
for (const k of [...keys].reverse())
|
|
122
|
+
await page.keyboard.up(k);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
126
|
+
0 && (module.exports = {
|
|
127
|
+
_schema,
|
|
128
|
+
executeMouseAction,
|
|
129
|
+
mouseActionMcpTool,
|
|
130
|
+
mouseActionSchema
|
|
131
|
+
});
|
|
@@ -46,6 +46,7 @@ var import_assertTool = require("./assertTool");
|
|
|
46
46
|
var import_assertApiRequestTool = require("./assertApiRequestTool");
|
|
47
47
|
var import_loadTraceTool = require("./loadTraceTool");
|
|
48
48
|
var import_skyRampImport = require("./skyRampImport");
|
|
49
|
+
var import_mouseActionTool = require("./mouseActionTool");
|
|
49
50
|
var import_utils = require("playwright-core/lib/utils");
|
|
50
51
|
var import_types = require("./types");
|
|
51
52
|
const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
|
|
@@ -119,7 +120,7 @@ class TraceRecordingBackend {
|
|
|
119
120
|
}
|
|
120
121
|
async listTools() {
|
|
121
122
|
const browserTools = await this._browserBackend.listTools();
|
|
122
|
-
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)()];
|
|
123
|
+
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)(), (0, import_mouseActionTool.mouseActionMcpTool)()];
|
|
123
124
|
}
|
|
124
125
|
async callTool(name, args, progress) {
|
|
125
126
|
if (!this._initialized)
|
|
@@ -146,6 +147,10 @@ class TraceRecordingBackend {
|
|
|
146
147
|
const parsed = import_loadTraceTool.loadTraceSchema.inputSchema.parse(args || {});
|
|
147
148
|
return this._handleLoadTrace(parsed);
|
|
148
149
|
}
|
|
150
|
+
if (name === import_mouseActionTool.mouseActionSchema.name) {
|
|
151
|
+
const parsed = import_mouseActionTool.mouseActionSchema.inputSchema.parse(args || {});
|
|
152
|
+
return this._handleMouseAction(parsed);
|
|
153
|
+
}
|
|
149
154
|
if (name === import_assertTool.assertToolSchema.name) {
|
|
150
155
|
const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
|
|
151
156
|
return this._handleAssert(parsed);
|
|
@@ -320,6 +325,33 @@ Reloaded current page: ${currentUrl}
|
|
|
320
325
|
serverClosed() {
|
|
321
326
|
void this._autoExportAndClose().catch(import_log.logUnhandledError);
|
|
322
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Execute a coordinate-based mouse action and track each decomposed sub-action
|
|
330
|
+
* (mouse.move/down/up/wheel or a position click) into _trackedActions so it is
|
|
331
|
+
* exported into the combined JSONL trace. The JSONL field shapes come straight
|
|
332
|
+
* from common/mouseActions, which is the single source of truth shared with
|
|
333
|
+
* the export and seeding paths.
|
|
334
|
+
*/
|
|
335
|
+
async _handleMouseAction(params) {
|
|
336
|
+
const page = this._browserBackend.context?.currentTab()?.page;
|
|
337
|
+
const timestamp = Date.now();
|
|
338
|
+
const pageAlias = this._currentPageAlias;
|
|
339
|
+
const { result, actions } = await (0, import_mouseActionTool.executeMouseAction)(page, params);
|
|
340
|
+
if (result.isError)
|
|
341
|
+
return result;
|
|
342
|
+
actions.forEach((a, i) => {
|
|
343
|
+
this._trackedActions.push({
|
|
344
|
+
toolName: "browser_mouse_action",
|
|
345
|
+
args: a,
|
|
346
|
+
code: "",
|
|
347
|
+
// Stagger timestamps so a decomposed drag keeps its move<down<move<up order.
|
|
348
|
+
timestamp: timestamp + i,
|
|
349
|
+
pageAlias
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
traceDebug(`Tracked ${actions.length} mouse sub-action(s) for "${params.action}" on ${pageAlias}`);
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
323
355
|
/**
|
|
324
356
|
* Load a prior Skyramp trace and replay it against the live browser, honoring
|
|
325
357
|
* an optional stop point, then seed _trackedActions with the replayed actions
|
|
@@ -455,6 +487,14 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
|
|
|
455
487
|
*/
|
|
456
488
|
_seedTrackedActionFields(a, locatorExpr) {
|
|
457
489
|
const sq = (s) => `'${String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}'`;
|
|
490
|
+
if (a.name === "mouse.move" || a.name === "mouse.down" || a.name === "mouse.up" || a.name === "mouse.wheel") {
|
|
491
|
+
const args = this._seedMouseActionArgs(a);
|
|
492
|
+
if (!args)
|
|
493
|
+
return null;
|
|
494
|
+
return { toolName: "browser_mouse_action", code: "", args };
|
|
495
|
+
}
|
|
496
|
+
if (a.name === "click" && a.position && (!a.selector || a.selector === "body"))
|
|
497
|
+
return { toolName: "browser_mouse_action", code: "", args: { name: "click", selector: "body", position: a.position, button: a.button ?? "left", modifiers: a.modifiers ?? 0, clickCount: a.clickCount ?? 1 } };
|
|
458
498
|
const SELECTOR_REQUIRED = /* @__PURE__ */ new Set(["click", "hover", "fill", "pressSequentially", "check", "uncheck", "select"]);
|
|
459
499
|
if (SELECTOR_REQUIRED.has(a.name) && !locatorExpr)
|
|
460
500
|
return null;
|
|
@@ -530,6 +570,35 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
|
|
|
530
570
|
return null;
|
|
531
571
|
}
|
|
532
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Build the normalized JSONL args for a re-seeded coordinate mouse action
|
|
575
|
+
* (mouse.move/down/up/wheel), following the MouseJsonlAction contract in
|
|
576
|
+
* common/mouseActions.ts. Only known fields are emitted (so stray fields from
|
|
577
|
+
* the loaded trace line are not forwarded), wheel deltas/modifiers default to
|
|
578
|
+
* 0, and a move/wheel missing its position is rejected (returns null) so the
|
|
579
|
+
* caller skips it rather than exporting an invalid shape for the Go consumer.
|
|
580
|
+
*/
|
|
581
|
+
_seedMouseActionArgs(a) {
|
|
582
|
+
switch (a.name) {
|
|
583
|
+
case "mouse.move": {
|
|
584
|
+
if (!a.position)
|
|
585
|
+
return null;
|
|
586
|
+
const args = { name: "mouse.move", position: a.position };
|
|
587
|
+
if (a.steps !== void 0)
|
|
588
|
+
args.steps = a.steps;
|
|
589
|
+
return args;
|
|
590
|
+
}
|
|
591
|
+
case "mouse.down":
|
|
592
|
+
case "mouse.up":
|
|
593
|
+
return a.button && a.button !== "left" ? { name: a.name, button: a.button } : { name: a.name };
|
|
594
|
+
case "mouse.wheel":
|
|
595
|
+
if (!a.position)
|
|
596
|
+
return null;
|
|
597
|
+
return { name: "mouse.wheel", position: a.position, deltaX: a.deltaX ?? 0, deltaY: a.deltaY ?? 0, modifiers: a.modifiers ?? 0 };
|
|
598
|
+
default:
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
533
602
|
/**
|
|
534
603
|
* Handle browser_select_option with automatic fallback for custom dropdowns.
|
|
535
604
|
* 1. Try native selectOption first.
|
|
@@ -654,14 +723,26 @@ Could not extract selector from hover result.` }], isError: true };
|
|
|
654
723
|
}
|
|
655
724
|
const locatorExpr = locatorMatch[1].trim();
|
|
656
725
|
let parsed = this._codeToLocator(locatorExpr);
|
|
657
|
-
|
|
726
|
+
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
727
|
+
if (snapResult.isError) {
|
|
728
|
+
const errText = snapResult.content?.[0]?.type === "text" ? snapResult.content[0].text : "";
|
|
658
729
|
return { content: [{ type: "text", text: `### Assertion Failed
|
|
659
|
-
Could not
|
|
730
|
+
Could not capture page snapshot to verify the assertion. ${errText}` }], isError: true };
|
|
660
731
|
}
|
|
661
|
-
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
662
732
|
const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
|
|
663
733
|
const snapLines = snapText.split("\n");
|
|
664
734
|
const refLine = snapLines.find((l) => l.includes(`[ref=${params.ref}]`)) || "";
|
|
735
|
+
if (!parsed || parsed.locator.kind === "css") {
|
|
736
|
+
const fromSnapshot = this._extractLocatorForRef(refLine);
|
|
737
|
+
if (fromSnapshot) {
|
|
738
|
+
traceDebug(`Resolved assert selector from snapshot accessible name (hover code was ${parsed ? "brittle CSS" : "unparseable"}: ${locatorExpr})`);
|
|
739
|
+
parsed = fromSnapshot;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (!parsed) {
|
|
743
|
+
return { content: [{ type: "text", text: `### Assertion Failed
|
|
744
|
+
Could not parse locator: ${locatorExpr}` }], isError: true };
|
|
745
|
+
}
|
|
665
746
|
const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
|
|
666
747
|
const elementText = textMatch?.[1] || "";
|
|
667
748
|
const valueMatch = refLine.match(/\]:\s*(.+)$/);
|
|
@@ -739,6 +820,13 @@ ${details}` }]
|
|
|
739
820
|
if (ariaRefMatch) {
|
|
740
821
|
return null;
|
|
741
822
|
}
|
|
823
|
+
const cssMatch = expr.match(/locator\(\s*['"]([#.][^'"]+)['"]\s*\)/);
|
|
824
|
+
if (cssMatch) {
|
|
825
|
+
return {
|
|
826
|
+
selector: cssMatch[1],
|
|
827
|
+
locator: { kind: "css", body: cssMatch[1], options: {} }
|
|
828
|
+
};
|
|
829
|
+
}
|
|
742
830
|
return null;
|
|
743
831
|
}
|
|
744
832
|
/**
|
|
@@ -227,6 +227,11 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
|
|
|
227
227
|
}
|
|
228
228
|
if (toolName === "browser_press_key")
|
|
229
229
|
return JSON.stringify({ name: "press", key: args.key, modifiers: modifiersArrayToMask(args.modifiers), selector: "", ...base });
|
|
230
|
+
if (toolName === "browser_mouse_action") {
|
|
231
|
+
if (!args.name)
|
|
232
|
+
return null;
|
|
233
|
+
return JSON.stringify({ ...args, ...base });
|
|
234
|
+
}
|
|
230
235
|
if (!code)
|
|
231
236
|
return null;
|
|
232
237
|
const extracted = extractLocatorFromCode(code);
|