chore: introduce response type (#738)
This commit is contained in:
@@ -28,13 +28,10 @@ const close = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async context => {
|
||||
handle: async (context, params, response) => {
|
||||
await context.close();
|
||||
return {
|
||||
code: [`await page.close()`],
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
response.setIncludeTabs();
|
||||
response.addCode(`await page.close()`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,22 +48,13 @@ const resize = defineTabTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Resize browser window to ${params.width}x${params.height}`,
|
||||
`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`
|
||||
];
|
||||
handle: async (tab, params, response) => {
|
||||
response.addCode(`// Resize browser window to ${params.width}x${params.height}`);
|
||||
response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
|
||||
|
||||
const action = async () => {
|
||||
await tab.run(async () => {
|
||||
await tab.page.setViewportSize({ width: params.width, height: params.height });
|
||||
};
|
||||
|
||||
return {
|
||||
code,
|
||||
action,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: true
|
||||
};
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -26,19 +26,8 @@ const console = defineTabTool({
|
||||
inputSchema: z.object({}),
|
||||
type: 'readOnly',
|
||||
},
|
||||
handle: async tab => {
|
||||
const messages = tab.consoleMessages();
|
||||
const log = messages.map(message => message.toString()).join('\n');
|
||||
return {
|
||||
code: [`// <internal code to get console messages>`],
|
||||
action: async () => {
|
||||
return {
|
||||
content: [{ type: 'text', text: log }]
|
||||
};
|
||||
},
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
handle: async (tab, params, response) => {
|
||||
tab.consoleMessages().map(message => response.addResult(message.toString()));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -31,27 +31,20 @@ const handleDialog = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
const dialogState = tab.modalStates().find(state => state.type === 'dialog');
|
||||
if (!dialogState)
|
||||
throw new Error('No dialog visible');
|
||||
|
||||
if (params.accept)
|
||||
await dialogState.dialog.accept(params.promptText);
|
||||
else
|
||||
await dialogState.dialog.dismiss();
|
||||
|
||||
tab.clearModalState(dialogState);
|
||||
|
||||
const code = [
|
||||
`// <internal code to handle "${dialogState.dialog.type()}" dialog>`,
|
||||
];
|
||||
|
||||
return {
|
||||
code,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
await tab.run(async () => {
|
||||
if (params.accept)
|
||||
await dialogState.dialog.accept(params.promptText);
|
||||
else
|
||||
await dialogState.dialog.dismiss();
|
||||
}, response);
|
||||
},
|
||||
|
||||
clearsModalState: 'dialog',
|
||||
|
||||
@@ -38,29 +38,22 @@ const evaluate = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code: string[] = [];
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
let locator: playwright.Locator | undefined;
|
||||
if (params.ref && params.element) {
|
||||
locator = await tab.refLocator({ ref: params.ref, element: params.element });
|
||||
code.push(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
|
||||
response.addCode(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
|
||||
} else {
|
||||
code.push(`await page.evaluate(${javascript.quote(params.function)});`);
|
||||
response.addCode(`await page.evaluate(${javascript.quote(params.function)});`);
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
action: async () => {
|
||||
const receiver = locator ?? tab.page as any;
|
||||
const result = await receiver._evaluateFunction(params.function);
|
||||
return {
|
||||
content: [{ type: 'text', text: '- Result: ' + (JSON.stringify(result, null, 2) || 'undefined') }],
|
||||
};
|
||||
},
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
await tab.run(async () => {
|
||||
const receiver = locator ?? tab.page as any;
|
||||
const result = await receiver._evaluateFunction(params.function);
|
||||
response.addResult(JSON.stringify(result, null, 2) || 'undefined');
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -30,26 +30,20 @@ const uploadFile = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
const modalState = tab.modalStates().find(state => state.type === 'fileChooser');
|
||||
if (!modalState)
|
||||
throw new Error('No file chooser visible');
|
||||
|
||||
const code = [
|
||||
`// <internal code to chose files ${params.paths.join(', ')}`,
|
||||
];
|
||||
response.addCode(`// Select files for upload`);
|
||||
response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`);
|
||||
|
||||
const action = async () => {
|
||||
await tab.run(async () => {
|
||||
await modalState.fileChooser.setFiles(params.paths);
|
||||
tab.clearModalState(modalState);
|
||||
};
|
||||
|
||||
return {
|
||||
code,
|
||||
action,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: true,
|
||||
};
|
||||
}, response);
|
||||
},
|
||||
clearsModalState: 'fileChooser',
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ const install = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async context => {
|
||||
handle: async (context, params, response) => {
|
||||
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
|
||||
const cliUrl = import.meta.resolve('playwright/package.json');
|
||||
const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
|
||||
@@ -49,11 +49,7 @@ const install = defineTool({
|
||||
reject(new Error(`Failed to install browser: ${output.join('')}`));
|
||||
});
|
||||
});
|
||||
return {
|
||||
code: [`// Browser ${channel} installed`],
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
response.setIncludeTabs();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -34,20 +34,14 @@ const pressKey = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Press ${params.key}`,
|
||||
`await page.keyboard.press('${params.key}');`,
|
||||
];
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
response.addCode(`// Press ${params.key}`);
|
||||
response.addCode(`await page.keyboard.press('${params.key}');`);
|
||||
|
||||
const action = () => tab.page.keyboard.press(params.key);
|
||||
|
||||
return {
|
||||
code,
|
||||
action,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: true
|
||||
};
|
||||
await tab.run(async () => {
|
||||
await tab.page.keyboard.press(params.key);
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,34 +61,27 @@ const type = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
const locator = await tab.refLocator(params);
|
||||
|
||||
const code: string[] = [];
|
||||
const steps: (() => Promise<void>)[] = [];
|
||||
await tab.run(async () => {
|
||||
if (params.slowly) {
|
||||
response.addCode(`// Press "${params.text}" sequentially into "${params.element}"`);
|
||||
response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
|
||||
await locator.pressSequentially(params.text);
|
||||
} else {
|
||||
response.addCode(`// Fill "${params.text}" into "${params.element}"`);
|
||||
response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
|
||||
await locator.fill(params.text);
|
||||
}
|
||||
|
||||
if (params.slowly) {
|
||||
code.push(`// Press "${params.text}" sequentially into "${params.element}"`);
|
||||
code.push(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
|
||||
steps.push(() => locator.pressSequentially(params.text));
|
||||
} else {
|
||||
code.push(`// Fill "${params.text}" into "${params.element}"`);
|
||||
code.push(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
|
||||
steps.push(() => locator.fill(params.text));
|
||||
}
|
||||
|
||||
if (params.submit) {
|
||||
code.push(`// Submit text`);
|
||||
code.push(`await page.${await generateLocator(locator)}.press('Enter');`);
|
||||
steps.push(() => locator.press('Enter'));
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
action: () => steps.reduce((acc, step) => acc.then(step), Promise.resolve()),
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: true,
|
||||
};
|
||||
if (params.submit) {
|
||||
response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`);
|
||||
await locator.press('Enter');
|
||||
}
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -34,18 +34,13 @@ const mouseMove = defineTabTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Move mouse to (${params.x}, ${params.y})`,
|
||||
`await page.mouse.move(${params.x}, ${params.y});`,
|
||||
];
|
||||
const action = () => tab.page.mouse.move(params.x, params.y);
|
||||
return {
|
||||
code,
|
||||
action,
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false
|
||||
};
|
||||
handle: async (tab, params, response) => {
|
||||
response.addCode(`// Move mouse to (${params.x}, ${params.y})`);
|
||||
response.addCode(`await page.mouse.move(${params.x}, ${params.y});`);
|
||||
|
||||
await tab.run(async () => {
|
||||
await tab.page.mouse.move(params.x, params.y);
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -62,24 +57,19 @@ const mouseClick = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Click mouse at coordinates (${params.x}, ${params.y})`,
|
||||
`await page.mouse.move(${params.x}, ${params.y});`,
|
||||
`await page.mouse.down();`,
|
||||
`await page.mouse.up();`,
|
||||
];
|
||||
const action = async () => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
response.addCode(`// Click mouse at coordinates (${params.x}, ${params.y})`);
|
||||
response.addCode(`await page.mouse.move(${params.x}, ${params.y});`);
|
||||
response.addCode(`await page.mouse.down();`);
|
||||
response.addCode(`await page.mouse.up();`);
|
||||
|
||||
await tab.run(async () => {
|
||||
await tab.page.mouse.move(params.x, params.y);
|
||||
await tab.page.mouse.down();
|
||||
await tab.page.mouse.up();
|
||||
};
|
||||
return {
|
||||
code,
|
||||
action,
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: true,
|
||||
};
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -98,28 +88,21 @@ const mouseDrag = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`,
|
||||
`await page.mouse.move(${params.startX}, ${params.startY});`,
|
||||
`await page.mouse.down();`,
|
||||
`await page.mouse.move(${params.endX}, ${params.endY});`,
|
||||
`await page.mouse.up();`,
|
||||
];
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
const action = async () => {
|
||||
response.addCode(`// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`);
|
||||
response.addCode(`await page.mouse.move(${params.startX}, ${params.startY});`);
|
||||
response.addCode(`await page.mouse.down();`);
|
||||
response.addCode(`await page.mouse.move(${params.endX}, ${params.endY});`);
|
||||
response.addCode(`await page.mouse.up();`);
|
||||
|
||||
await tab.run(async () => {
|
||||
await tab.page.mouse.move(params.startX, params.startY);
|
||||
await tab.page.mouse.down();
|
||||
await tab.page.mouse.move(params.endX, params.endY);
|
||||
await tab.page.mouse.up();
|
||||
};
|
||||
|
||||
return {
|
||||
code,
|
||||
action,
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: true,
|
||||
};
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -30,20 +30,13 @@ const navigate = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
handle: async (context, params, response) => {
|
||||
const tab = await context.ensureTab();
|
||||
await tab.navigate(params.url);
|
||||
|
||||
const code = [
|
||||
`// Navigate to ${params.url}`,
|
||||
`await page.goto('${params.url}');`,
|
||||
];
|
||||
|
||||
return {
|
||||
code,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
response.addCode(`// Navigate to ${params.url}`);
|
||||
response.addCode(`await page.goto('${params.url}');`);
|
||||
response.addSnapshot(await tab.captureSnapshot());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,18 +50,13 @@ const goBack = defineTabTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async tab => {
|
||||
await tab.page.goBack();
|
||||
const code = [
|
||||
`// Navigate back`,
|
||||
`await page.goBack();`,
|
||||
];
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
return {
|
||||
code,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
await tab.page.goBack();
|
||||
response.addCode(`// Navigate back`);
|
||||
response.addCode(`await page.goBack();`);
|
||||
response.addSnapshot(await tab.captureSnapshot());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -81,17 +69,13 @@ const goForward = defineTabTool({
|
||||
inputSchema: z.object({}),
|
||||
type: 'readOnly',
|
||||
},
|
||||
handle: async tab => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
await tab.page.goForward();
|
||||
const code = [
|
||||
`// Navigate forward`,
|
||||
`await page.goForward();`,
|
||||
];
|
||||
return {
|
||||
code,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
response.addCode(`// Navigate forward`);
|
||||
response.addCode(`await page.goForward();`);
|
||||
response.addSnapshot(await tab.captureSnapshot());
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -30,19 +30,9 @@ const requests = defineTabTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async tab => {
|
||||
handle: async (tab, params, response) => {
|
||||
const requests = tab.requests();
|
||||
const log = [...requests.entries()].map(([request, response]) => renderRequest(request, response)).join('\n');
|
||||
return {
|
||||
code: [`// <internal code to list network requests>`],
|
||||
action: async () => {
|
||||
return {
|
||||
content: [{ type: 'text', text: log }]
|
||||
};
|
||||
},
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
[...requests.entries()].forEach(([req, res]) => response.addResult(renderRequest(req, res)));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -35,20 +35,12 @@ const pdf = defineTabTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
|
||||
|
||||
const code = [
|
||||
`// Save page as ${fileName}`,
|
||||
`await page.pdf(${javascript.formatObject({ path: fileName })});`,
|
||||
];
|
||||
|
||||
return {
|
||||
code,
|
||||
action: async () => tab.page.pdf({ path: fileName }).then(() => {}),
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
response.addCode(`// Save page as ${fileName}`);
|
||||
response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
|
||||
response.addResult(`Saved page as ${fileName}`);
|
||||
await tab.page.pdf({ path: fileName });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const screenshot = defineTabTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
const fileType = params.raw ? 'png' : 'jpeg';
|
||||
const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
||||
const options: playwright.PageScreenshotOptions = {
|
||||
@@ -64,36 +64,22 @@ const screenshot = defineTabTool({
|
||||
const isElementScreenshot = params.element && params.ref;
|
||||
|
||||
const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport');
|
||||
const code = [
|
||||
`// Screenshot ${screenshotTarget} and save it as ${fileName}`,
|
||||
];
|
||||
response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
|
||||
|
||||
// Only get snapshot when element screenshot is needed
|
||||
const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null;
|
||||
|
||||
if (locator)
|
||||
code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
||||
response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
||||
else
|
||||
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
||||
response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
|
||||
|
||||
const includeBase64 = tab.context.config.imageResponses !== 'omit';
|
||||
const action = async () => {
|
||||
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
||||
return {
|
||||
content: includeBase64 ? [{
|
||||
type: 'image' as 'image',
|
||||
data: screenshot.toString('base64'),
|
||||
mimeType: fileType === 'png' ? 'image/png' : 'image/jpeg',
|
||||
}] : []
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
code,
|
||||
action,
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
||||
response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
|
||||
response.addImage({
|
||||
contentType: fileType === 'png' ? 'image/png' : 'image/jpeg',
|
||||
data: buffer
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -30,14 +30,9 @@ const snapshot = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async context => {
|
||||
await context.ensureTab();
|
||||
|
||||
return {
|
||||
code: [`// <internal code to capture accessibility snapshot>`],
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
handle: async (context, params, response) => {
|
||||
const tab = await context.ensureTab();
|
||||
response.addSnapshot(await tab.captureSnapshot());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,26 +56,27 @@ const click = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
const locator = await tab.refLocator(params);
|
||||
const button = params.button;
|
||||
const buttonAttr = button ? `{ button: '${button}' }` : '';
|
||||
|
||||
const code: string[] = [];
|
||||
if (params.doubleClick) {
|
||||
code.push(`// Double click ${params.element}`);
|
||||
code.push(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`);
|
||||
response.addCode(`// Double click ${params.element}`);
|
||||
response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`);
|
||||
} else {
|
||||
code.push(`// Click ${params.element}`);
|
||||
code.push(`await page.${await generateLocator(locator)}.click(${buttonAttr});`);
|
||||
response.addCode(`// Click ${params.element}`);
|
||||
response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`);
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
action: () => params.doubleClick ? locator.dblclick({ button }) : locator.click({ button }),
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: true,
|
||||
};
|
||||
await tab.run(async () => {
|
||||
if (params.doubleClick)
|
||||
await locator.dblclick({ button });
|
||||
else
|
||||
await locator.click({ button });
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -99,23 +95,19 @@ const drag = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
const [startLocator, endLocator] = await tab.refLocators([
|
||||
{ ref: params.startRef, element: params.startElement },
|
||||
{ ref: params.endRef, element: params.endElement },
|
||||
]);
|
||||
|
||||
const code = [
|
||||
`// Drag ${params.startElement} to ${params.endElement}`,
|
||||
`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`
|
||||
];
|
||||
await tab.run(async () => {
|
||||
await startLocator.dragTo(endLocator);
|
||||
}, response);
|
||||
|
||||
return {
|
||||
code,
|
||||
action: () => startLocator.dragTo(endLocator),
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: true,
|
||||
};
|
||||
response.addCode(`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -129,20 +121,15 @@ const hover = defineTabTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
const locator = await tab.refLocator(params);
|
||||
response.addCode(`await page.${await generateLocator(locator)}.hover();`);
|
||||
|
||||
const code = [
|
||||
`// Hover over ${params.element}`,
|
||||
`await page.${await generateLocator(locator)}.hover();`
|
||||
];
|
||||
|
||||
return {
|
||||
code,
|
||||
action: () => locator.hover(),
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: true,
|
||||
};
|
||||
await tab.run(async () => {
|
||||
await locator.hover();
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,20 +147,16 @@ const selectOption = defineTabTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (tab, params) => {
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
const locator = await tab.refLocator(params);
|
||||
response.addCode(`// Select options [${params.values.join(', ')}] in ${params.element}`);
|
||||
response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`);
|
||||
|
||||
const code = [
|
||||
`// Select options [${params.values.join(', ')}] in ${params.element}`,
|
||||
`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`
|
||||
];
|
||||
|
||||
return {
|
||||
code,
|
||||
action: () => locator.selectOption(params.values).then(() => {}),
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: true,
|
||||
};
|
||||
await tab.run(async () => {
|
||||
await locator.selectOption(params.values);
|
||||
}, response);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -28,19 +28,9 @@ const listTabs = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async context => {
|
||||
handle: async (context, params, response) => {
|
||||
await context.ensureTab();
|
||||
return {
|
||||
code: [`// <internal code to list tabs>`],
|
||||
captureSnapshot: false,
|
||||
waitForNetwork: false,
|
||||
resultOverride: {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: (await context.listTabsMarkdown()).join('\n'),
|
||||
}],
|
||||
},
|
||||
};
|
||||
response.setIncludeTabs();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,17 +47,10 @@ const selectTab = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
await context.selectTab(params.index);
|
||||
const code = [
|
||||
`// <internal code to select tab ${params.index}>`,
|
||||
];
|
||||
|
||||
return {
|
||||
code,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false
|
||||
};
|
||||
handle: async (context, params, response) => {
|
||||
const tab = await context.selectTab(params.index);
|
||||
response.setIncludeSnapshot();
|
||||
response.addSnapshot(await tab.captureSnapshot());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -84,19 +67,13 @@ const newTab = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
handle: async (context, params, response) => {
|
||||
const tab = await context.newTab();
|
||||
if (params.url)
|
||||
await tab.navigate(params.url);
|
||||
|
||||
const code = [
|
||||
`// <internal code to open a new tab>`,
|
||||
];
|
||||
return {
|
||||
code,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false
|
||||
};
|
||||
response.setIncludeSnapshot();
|
||||
response.addSnapshot(await tab.captureSnapshot());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -113,16 +90,12 @@ const closeTab = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
handle: async (context, params, response) => {
|
||||
await context.closeTab(params.index);
|
||||
const code = [
|
||||
`// <internal code to close tab ${params.index}>`,
|
||||
];
|
||||
return {
|
||||
code,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false
|
||||
};
|
||||
response.setIncludeTabs();
|
||||
response.addCode(`await myPage.close();`);
|
||||
if (context.tabs().length)
|
||||
response.addSnapshot(await context.currentTabOrDie().captureSnapshot());
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
|
||||
import type { z } from 'zod';
|
||||
import type { Context } from '../context.js';
|
||||
import type * as playwright from 'playwright';
|
||||
import type { ToolCapability } from '../../config.js';
|
||||
import type { Tab } from '../tab.js';
|
||||
import type { Response } from '../response.js';
|
||||
|
||||
export type ToolSchema<Input extends InputType> = {
|
||||
name: string;
|
||||
@@ -45,21 +45,30 @@ export type DialogModalState = {
|
||||
|
||||
export type ModalState = FileUploadModalState | DialogModalState;
|
||||
|
||||
export type ToolActionResult = { content?: (ImageContent | TextContent)[] } | undefined | void;
|
||||
export type SnapshotContent = {
|
||||
type: 'snapshot';
|
||||
snapshot: string;
|
||||
};
|
||||
|
||||
export type ToolResult = {
|
||||
export type TextContent = {
|
||||
type: 'text';
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type ImageContent = {
|
||||
type: 'image';
|
||||
image: string;
|
||||
};
|
||||
|
||||
export type CodeContent = {
|
||||
type: 'code';
|
||||
code: string[];
|
||||
action?: () => Promise<ToolActionResult>;
|
||||
captureSnapshot: boolean;
|
||||
waitForNetwork: boolean;
|
||||
resultOverride?: ToolActionResult;
|
||||
};
|
||||
|
||||
export type Tool<Input extends InputType = InputType> = {
|
||||
capability: ToolCapability;
|
||||
schema: ToolSchema<Input>;
|
||||
clearsModalState?: ModalState['type'];
|
||||
handle: (context: Context, params: z.output<Input>) => Promise<ToolResult>;
|
||||
handle: (context: Context, params: z.output<Input>, response: Response) => Promise<void>;
|
||||
};
|
||||
|
||||
export function defineTool<Input extends InputType>(tool: Tool<Input>): Tool<Input> {
|
||||
@@ -70,20 +79,20 @@ export type TabTool<Input extends InputType = InputType> = {
|
||||
capability: ToolCapability;
|
||||
schema: ToolSchema<Input>;
|
||||
clearsModalState?: ModalState['type'];
|
||||
handle: (tab: Tab, params: z.output<Input>) => Promise<ToolResult>;
|
||||
handle: (tab: Tab, params: z.output<Input>, response: Response) => Promise<void>;
|
||||
};
|
||||
|
||||
export function defineTabTool<Input extends InputType>(tool: TabTool<Input>): Tool<Input> {
|
||||
return {
|
||||
...tool,
|
||||
handle: async (context, params) => {
|
||||
handle: async (context, params, response) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
const modalStates = tab.modalStates().map(state => state.type);
|
||||
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
|
||||
throw new Error(`The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
|
||||
if (!tool.clearsModalState && modalStates.length)
|
||||
throw new Error(`Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
|
||||
return tool.handle(tab, params);
|
||||
return tool.handle(tab, params, response);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ const wait = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
handle: async (context, params, response) => {
|
||||
if (!params.text && !params.textGone && !params.time)
|
||||
throw new Error('Either time, text or textGone must be provided');
|
||||
|
||||
@@ -57,11 +57,8 @@ const wait = defineTool({
|
||||
await locator.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
captureSnapshot: true,
|
||||
waitForNetwork: false,
|
||||
};
|
||||
response.addResult(`Waited for ${params.text || params.textGone || params.time}`);
|
||||
response.addSnapshot(await tab.captureSnapshot());
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user