feat: auto handle dialogs during script evaluation (#1839)

Provide a argument to LLM to handle dialogs that come up during code
execution.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Nikolay Vitkov
2026-04-20 08:42:05 +02:00
committed by GitHub
parent 0ed086ec19
commit da33cb5b95
8 changed files with 120 additions and 12 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run gen' to update-->
# Chrome DevTools MCP Tool Reference (~6962 cl100k_base tokens)
# Chrome DevTools MCP Tool Reference (~7005 cl100k_base tokens)
- **[Input automation](#input-automation)** (9 tools)
- [`click`](#click)
@@ -333,6 +333,7 @@ so returned values have to be JSON-serializable.
}`
- **args** (array) _(optional)_: An optional list of arguments to pass to the function.
- **dialogAction** (string) _(optional)_: Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.
---
+1 -1
View File
@@ -117,7 +117,7 @@ export class McpPage implements ContextPage {
waitForEventsAfterAction(
action: () => Promise<unknown>,
options?: {timeout?: number},
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
): Promise<void> {
const helper = this.createWaitForHelper(
this.cpuThrottlingRate,
+18 -2
View File
@@ -5,7 +5,7 @@
*/
import {logger} from './logger.js';
import type {Page, Protocol, CdpPage} from './third_party/index.js';
import type {Page, Protocol, CdpPage, Dialog} from './third_party/index.js';
import type {PredefinedNetworkConditions} from './third_party/index.js';
export class WaitForHelper {
@@ -126,8 +126,24 @@ export class WaitForHelper {
async waitForEventsAfterAction(
action: () => Promise<unknown>,
options?: {timeout?: number},
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
): Promise<void> {
if (options?.handleDialog) {
const dialogHandler = (dialog: Pick<Dialog, 'accept' | 'dismiss'>) => {
if (options.handleDialog === 'dismiss') {
void dialog.dismiss();
} else if (options.handleDialog === 'accept') {
void dialog.accept();
} else {
void dialog.accept(options.handleDialog);
}
};
this.#page.on('dialog', dialogHandler);
this.#abortController.signal.addEventListener('abort', () => {
this.#page.off('dialog', dialogHandler);
});
}
const navigationFinished = this.waitForNavigationStarted()
.then(navigationStated => {
if (navigationStated) {
+7
View File
@@ -155,6 +155,13 @@ export const commands: Commands = {
description: 'An optional list of arguments to pass to the function.',
required: false,
},
dialogAction: {
name: 'dialogAction',
type: 'string',
description:
'Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.',
required: false,
},
},
},
fill: {
+4
View File
@@ -107,6 +107,10 @@
{
"name": "args_count",
"argType": "number"
},
{
"name": "dialog_action_length",
"argType": "number"
}
]
},
+1 -1
View File
@@ -247,7 +247,7 @@ export type ContextPage = Readonly<{
clearDialog(): void;
waitForEventsAfterAction(
action: () => Promise<unknown>,
options?: {timeout?: number},
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
): Promise<void>;
getInPageTools(): ToolGroup<InPageToolDefinition> | undefined;
}>;
+18 -7
View File
@@ -46,6 +46,12 @@ Example with arguments: \`(el) => {
)
.optional()
.describe(`An optional list of arguments to pass to the function.`),
dialogAction: zod
.string()
.optional()
.describe(
'Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.',
),
...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
...(cliArgs?.categoryExtensions
? {
@@ -64,6 +70,7 @@ Example with arguments: \`(el) => {
args: uidArgs,
function: fnString,
pageId,
dialogAction,
} = request.params;
if (cliArgs?.categoryExtensions && serviceWorkerId) {
@@ -77,11 +84,12 @@ Example with arguments: \`(el) => {
}
const worker = await getWebWorker(context, serviceWorkerId);
await context
.getSelectedMcpPage()
.waitForEventsAfterAction(async () => {
await context.getSelectedMcpPage().waitForEventsAfterAction(
async () => {
await performEvaluation(worker, fnString, [], response);
});
},
{handleDialog: dialogAction ?? 'accept'},
);
return;
}
@@ -101,9 +109,12 @@ Example with arguments: \`(el) => {
const evaluatable = await getPageOrFrame(page, frames);
await mcpPage.waitForEventsAfterAction(async () => {
await performEvaluation(evaluatable, fnString, args, response);
});
await mcpPage.waitForEventsAfterAction(
async () => {
await performEvaluation(evaluatable, fnString, args, response);
},
{handleDialog: dialogAction ?? 'accept'},
);
} finally {
void Promise.allSettled(args.map(arg => arg.dispose()));
}
+69
View File
@@ -98,6 +98,75 @@ describe('script', () => {
});
});
it('work for scripts that trigger dialogs', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.setContent(html`<button id="test">test</button>`);
await evaluateScript().handler(
{
params: {
function: String(() => {
alert('hello');
return 'Works';
}),
},
},
response,
context,
);
const lineEvaluation = response.responseLines.at(2)!;
assert.strictEqual(JSON.parse(lineEvaluation), 'Works');
});
});
it('work for scripts that trigger dialogs and dismiss them', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.setContent(html`<button id="test">test</button>`);
await evaluateScript().handler(
{
params: {
function: String(() => {
return confirm('hello');
}),
dialogAction: 'dismiss',
},
},
response,
context,
);
const lineEvaluation = response.responseLines.at(2)!;
assert.strictEqual(JSON.parse(lineEvaluation), false);
});
});
it('work for scripts that trigger prompts and fill them', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.setContent(html`<button id="test">test</button>`);
await evaluateScript().handler(
{
params: {
function: String(() => {
return prompt('Enter your name:');
}),
dialogAction: 'John Doe',
},
},
response,
context,
);
const lineEvaluation = response.responseLines.at(2)!;
assert.strictEqual(JSON.parse(lineEvaluation), 'John Doe');
});
});
it('work for async functions', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();