Skip to content

Commit 8edf6e8

Browse files
committed
fix: per-page dialog tracking and per-context focus for parallel agents
Replace singleton dialog handler with per-page Map so parallel agents can handle dialogs on their own pages independently. Dialog handlers are attached per-page via #setupPage() when pages are first discovered. Replace #selectedPage-based defocus with per-context focus tracking via #focusedPagePerContext Map. This prevents interleaved selectPage() calls from parallel agents defocusing the wrong page or skipping defocus entirely. Both getDialog() and clearDialog() accept an optional page parameter, defaulting to the selected page for backward compatibility. handle_dialog now resolves page via pageIdSchema. Entries are cleaned up on page close.
1 parent 166c3ab commit 8edf6e8

5 files changed

Lines changed: 293 additions & 21 deletions

File tree

docs/tool-reference.md

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

3-
# Chrome DevTools MCP Tool Reference (~7267 cl100k_base tokens)
3+
# Chrome DevTools MCP Tool Reference (~7294 cl100k_base tokens)
44

55
- **[Input automation](#input-automation)** (8 tools)
66
- [`click`](#click)
@@ -94,6 +94,7 @@
9494
**Parameters:**
9595

9696
- **action** (enum: "accept", "dismiss") **(required)**: Whether to dismiss or accept the dialog
97+
- **pageId** (number) _(optional)_: Targets a specific page by ID.
9798
- **promptText** (string) _(optional)_: Optional prompt text to enter into the dialog.
9899

99100
---
@@ -320,12 +321,12 @@ so returned values have to be JSON-serializable.
320321
**Parameters:**
321322

322323
- **function** (string) **(required)**: A JavaScript function declaration to be executed by the tool in the currently selected page.
323-
Example without arguments: `() => {
324+
Example without arguments: `() => {
324325
return document.title
325326
}` or `async () => {
326327
return await fetch("example.com")
327328
}`.
328-
Example with arguments: `(el) => {
329+
Example with arguments: `(el) => {
329330
return el.innerText;
330331
}`
331332

src/McpContext.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ export class McpContext implements Context {
141141
#screenRecorderData: {recorder: ScreenRecorder; filePath: string} | null =
142142
null;
143143
#emulationSettingsMap = new WeakMap<Page, EmulationSettings>();
144-
#dialog?: Dialog;
144+
#pageDialogs = new Map<Page, Dialog>();
145+
#pageDialogHandlers = new WeakMap<Page, (dialog: Dialog) => void>();
146+
#focusedPagePerContext = new Map<BrowserContext, Page>();
145147

146148
#pageIdMap = new WeakMap<Page, number>();
147149
#nextPageId = 1;
@@ -307,6 +309,11 @@ export class McpContext implements Context {
307309
throw new Error(CLOSE_PAGE_ERROR);
308310
}
309311
const page = this.getPageById(pageId);
312+
this.#pageDialogs.delete(page);
313+
const ctx = page.browserContext();
314+
if (this.#focusedPagePerContext.get(ctx) === page) {
315+
this.#focusedPagePerContext.delete(ctx);
316+
}
310317
await page.close({runBeforeUnload: false});
311318
this.#pageToIsolatedContextName.delete(page);
312319
}
@@ -482,12 +489,19 @@ export class McpContext implements Context {
482489
return this.#options.performanceCrux;
483490
}
484491

485-
getDialog(): Dialog | undefined {
486-
return this.#dialog;
492+
getDialog(page?: Page): Dialog | undefined {
493+
const targetPage = page ?? this.#selectedPage;
494+
if (!targetPage) {
495+
return undefined;
496+
}
497+
return this.#pageDialogs.get(targetPage);
487498
}
488499

489-
clearDialog(): void {
490-
this.#dialog = undefined;
500+
clearDialog(page?: Page): void {
501+
const targetPage = page ?? this.#selectedPage;
502+
if (targetPage) {
503+
this.#pageDialogs.delete(targetPage);
504+
}
491505
}
492506

493507
getSelectedPage(): Page {
@@ -522,24 +536,34 @@ export class McpContext implements Context {
522536
return this.#pageIdMap.get(page);
523537
}
524538

525-
#dialogHandler = (dialog: Dialog): void => {
526-
this.#dialog = dialog;
527-
};
539+
/**
540+
* Attaches a per-page dialog handler.
541+
* Called once per page when it is first discovered.
542+
*/
543+
#setupPage(page: Page): void {
544+
if (!this.#pageDialogHandlers.has(page)) {
545+
const handler = (dialog: Dialog): void => {
546+
this.#pageDialogs.set(page, dialog);
547+
};
548+
page.on('dialog', handler);
549+
this.#pageDialogHandlers.set(page, handler);
550+
}
551+
}
528552

529553
isPageSelected(page: Page): boolean {
530554
return this.#selectedPage === page;
531555
}
532556

533557
selectPage(newPage: Page): void {
534-
const oldPage = this.#selectedPage;
535-
if (oldPage) {
536-
oldPage.off('dialog', this.#dialogHandler);
537-
void oldPage.emulateFocusedPage(false).catch(error => {
558+
const ctx = newPage.browserContext();
559+
const oldFocused = this.#focusedPagePerContext.get(ctx);
560+
if (oldFocused && oldFocused !== newPage && !oldFocused.isClosed()) {
561+
void oldFocused.emulateFocusedPage(false).catch(error => {
538562
this.logger('Error turning off focused page emulation', error);
539563
});
540564
}
565+
this.#focusedPagePerContext.set(ctx, newPage);
541566
this.#selectedPage = newPage;
542-
newPage.on('dialog', this.#dialogHandler);
543567
this.#updateSelectedPageTimeouts();
544568
void newPage.emulateFocusedPage(true).catch(error => {
545569
this.logger('Error turning on focused page emulation', error);
@@ -600,6 +624,7 @@ export class McpContext implements Context {
600624
for (const page of allPages) {
601625
if (!this.#pageIdMap.has(page)) {
602626
this.#pageIdMap.set(page, this.#nextPageId++);
627+
this.#setupPage(page);
603628
}
604629
}
605630

src/tools/ToolDefinition.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ export type Context = Readonly<{
109109
storeTraceRecording(result: TraceResult): void;
110110
getSelectedPage(): Page;
111111
resolvePageById(pageId?: number): Page;
112-
getDialog(): Dialog | undefined;
113-
clearDialog(): void;
112+
getDialog(page?: Page): Dialog | undefined;
113+
clearDialog(page?: Page): void;
114114
getPageById(pageId: number): Page;
115115
getPageId(page: Page): number | undefined;
116116
isPageSelected(page: Page): boolean;

src/tools/pages.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export const navigatePage = defineTool({
186186
void dialog.dismiss();
187187
}
188188
// We are not going to report the dialog like regular dialogs.
189-
context.clearDialog();
189+
context.clearDialog(page);
190190
}
191191
};
192192

@@ -325,6 +325,7 @@ export const handleDialog = defineTool({
325325
readOnlyHint: false,
326326
},
327327
schema: {
328+
...pageIdSchema,
328329
action: zod
329330
.enum(['accept', 'dismiss'])
330331
.describe('Whether to dismiss or accept the dialog'),
@@ -334,7 +335,8 @@ export const handleDialog = defineTool({
334335
.describe('Optional prompt text to enter into the dialog.'),
335336
},
336337
handler: async (request, response, context) => {
337-
const dialog = context.getDialog();
338+
const page = context.resolvePageById(request.params.pageId);
339+
const dialog = context.getDialog(page);
338340
if (!dialog) {
339341
throw new Error('No open dialog found');
340342
}
@@ -362,7 +364,7 @@ export const handleDialog = defineTool({
362364
}
363365
}
364366

365-
context.clearDialog();
367+
context.clearDialog(page);
366368
response.setIncludePages(true);
367369
},
368370
});

0 commit comments

Comments
 (0)