Skip to content

Commit da33cb5

Browse files
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>
1 parent 0ed086e commit da33cb5

8 files changed

Lines changed: 120 additions & 12 deletions

File tree

docs/tool-reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run gen' to update-->
22

3-
# Chrome DevTools MCP Tool Reference (~6962 cl100k_base tokens)
3+
# Chrome DevTools MCP Tool Reference (~7005 cl100k_base tokens)
44

55
- **[Input automation](#input-automation)** (9 tools)
66
- [`click`](#click)
@@ -333,6 +333,7 @@ so returned values have to be JSON-serializable.
333333
}`
334334

335335
- **args** (array) _(optional)_: An optional list of arguments to pass to the function.
336+
- **dialogAction** (string) _(optional)_: Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.
336337

337338
---
338339

src/McpPage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export class McpPage implements ContextPage {
117117

118118
waitForEventsAfterAction(
119119
action: () => Promise<unknown>,
120-
options?: {timeout?: number},
120+
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
121121
): Promise<void> {
122122
const helper = this.createWaitForHelper(
123123
this.cpuThrottlingRate,

src/WaitForHelper.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import {logger} from './logger.js';
8-
import type {Page, Protocol, CdpPage} from './third_party/index.js';
8+
import type {Page, Protocol, CdpPage, Dialog} from './third_party/index.js';
99
import type {PredefinedNetworkConditions} from './third_party/index.js';
1010

1111
export class WaitForHelper {
@@ -126,8 +126,24 @@ export class WaitForHelper {
126126

127127
async waitForEventsAfterAction(
128128
action: () => Promise<unknown>,
129-
options?: {timeout?: number},
129+
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
130130
): Promise<void> {
131+
if (options?.handleDialog) {
132+
const dialogHandler = (dialog: Pick<Dialog, 'accept' | 'dismiss'>) => {
133+
if (options.handleDialog === 'dismiss') {
134+
void dialog.dismiss();
135+
} else if (options.handleDialog === 'accept') {
136+
void dialog.accept();
137+
} else {
138+
void dialog.accept(options.handleDialog);
139+
}
140+
};
141+
this.#page.on('dialog', dialogHandler);
142+
this.#abortController.signal.addEventListener('abort', () => {
143+
this.#page.off('dialog', dialogHandler);
144+
});
145+
}
146+
131147
const navigationFinished = this.waitForNavigationStarted()
132148
.then(navigationStated => {
133149
if (navigationStated) {

src/bin/cliDefinitions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ export const commands: Commands = {
155155
description: 'An optional list of arguments to pass to the function.',
156156
required: false,
157157
},
158+
dialogAction: {
159+
name: 'dialogAction',
160+
type: 'string',
161+
description:
162+
'Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.',
163+
required: false,
164+
},
158165
},
159166
},
160167
fill: {

src/telemetry/tool_call_metrics.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@
107107
{
108108
"name": "args_count",
109109
"argType": "number"
110+
},
111+
{
112+
"name": "dialog_action_length",
113+
"argType": "number"
110114
}
111115
]
112116
},

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export type ContextPage = Readonly<{
247247
clearDialog(): void;
248248
waitForEventsAfterAction(
249249
action: () => Promise<unknown>,
250-
options?: {timeout?: number},
250+
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
251251
): Promise<void>;
252252
getInPageTools(): ToolGroup<InPageToolDefinition> | undefined;
253253
}>;

src/tools/script.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ Example with arguments: \`(el) => {
4646
)
4747
.optional()
4848
.describe(`An optional list of arguments to pass to the function.`),
49+
dialogAction: zod
50+
.string()
51+
.optional()
52+
.describe(
53+
'Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.',
54+
),
4955
...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
5056
...(cliArgs?.categoryExtensions
5157
? {
@@ -64,6 +70,7 @@ Example with arguments: \`(el) => {
6470
args: uidArgs,
6571
function: fnString,
6672
pageId,
73+
dialogAction,
6774
} = request.params;
6875

6976
if (cliArgs?.categoryExtensions && serviceWorkerId) {
@@ -77,11 +84,12 @@ Example with arguments: \`(el) => {
7784
}
7885

7986
const worker = await getWebWorker(context, serviceWorkerId);
80-
await context
81-
.getSelectedMcpPage()
82-
.waitForEventsAfterAction(async () => {
87+
await context.getSelectedMcpPage().waitForEventsAfterAction(
88+
async () => {
8389
await performEvaluation(worker, fnString, [], response);
84-
});
90+
},
91+
{handleDialog: dialogAction ?? 'accept'},
92+
);
8593
return;
8694
}
8795

@@ -101,9 +109,12 @@ Example with arguments: \`(el) => {
101109

102110
const evaluatable = await getPageOrFrame(page, frames);
103111

104-
await mcpPage.waitForEventsAfterAction(async () => {
105-
await performEvaluation(evaluatable, fnString, args, response);
106-
});
112+
await mcpPage.waitForEventsAfterAction(
113+
async () => {
114+
await performEvaluation(evaluatable, fnString, args, response);
115+
},
116+
{handleDialog: dialogAction ?? 'accept'},
117+
);
107118
} finally {
108119
void Promise.allSettled(args.map(arg => arg.dispose()));
109120
}

tests/tools/script.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,75 @@ describe('script', () => {
9898
});
9999
});
100100

101+
it('work for scripts that trigger dialogs', async () => {
102+
await withMcpContext(async (response, context) => {
103+
const page = context.getSelectedPptrPage();
104+
105+
await page.setContent(html`<button id="test">test</button>`);
106+
107+
await evaluateScript().handler(
108+
{
109+
params: {
110+
function: String(() => {
111+
alert('hello');
112+
return 'Works';
113+
}),
114+
},
115+
},
116+
response,
117+
context,
118+
);
119+
const lineEvaluation = response.responseLines.at(2)!;
120+
assert.strictEqual(JSON.parse(lineEvaluation), 'Works');
121+
});
122+
});
123+
124+
it('work for scripts that trigger dialogs and dismiss them', async () => {
125+
await withMcpContext(async (response, context) => {
126+
const page = context.getSelectedPptrPage();
127+
128+
await page.setContent(html`<button id="test">test</button>`);
129+
130+
await evaluateScript().handler(
131+
{
132+
params: {
133+
function: String(() => {
134+
return confirm('hello');
135+
}),
136+
dialogAction: 'dismiss',
137+
},
138+
},
139+
response,
140+
context,
141+
);
142+
const lineEvaluation = response.responseLines.at(2)!;
143+
assert.strictEqual(JSON.parse(lineEvaluation), false);
144+
});
145+
});
146+
147+
it('work for scripts that trigger prompts and fill them', async () => {
148+
await withMcpContext(async (response, context) => {
149+
const page = context.getSelectedPptrPage();
150+
151+
await page.setContent(html`<button id="test">test</button>`);
152+
153+
await evaluateScript().handler(
154+
{
155+
params: {
156+
function: String(() => {
157+
return prompt('Enter your name:');
158+
}),
159+
dialogAction: 'John Doe',
160+
},
161+
},
162+
response,
163+
context,
164+
);
165+
const lineEvaluation = response.responseLines.at(2)!;
166+
assert.strictEqual(JSON.parse(lineEvaluation), 'John Doe');
167+
});
168+
});
169+
101170
it('work for async functions', async () => {
102171
await withMcpContext(async (response, context) => {
103172
const page = context.getSelectedPptrPage();

0 commit comments

Comments
 (0)