Skip to content

Commit e96fc9a

Browse files
committed
feat: report newly opened pages in the action response
When a click (or another input action) opens a new page, e.g. via a link with target=_blank or window.open(), the response now includes a note and the list of open pages so that clients can perceive the new page without calling list_pages. The page list is only included when a new page was actually opened during the action. Fixes #367
1 parent 8028bfe commit e96fc9a

3 files changed

Lines changed: 62 additions & 1 deletion

File tree

src/McpResponse.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import type {
3636
Extension,
3737
HTTPRequest,
3838
} from './third_party/index.js';
39-
import {handleDialog, listPages} from './tools/pages.js';
39+
import {handleDialog, listPages, selectPage} from './tools/pages.js';
4040
import type {ToolGroups} from './tools/thirdPartyDeveloper.js';
4141
import type {
4242
DevToolsData,
@@ -317,6 +317,12 @@ export class McpResponse implements Response {
317317

318318
attachWaitForResult(result: WaitForEventsResult): void {
319319
this.#attachedWaitForResult = result;
320+
if (result.newPagesOpened) {
321+
// The action opened a new page (e.g., a click on a link with
322+
// target=_blank). Include the page list so that the client can
323+
// perceive the new page without calling list_pages.
324+
this.setIncludePages(true);
325+
}
320326
}
321327

322328
setHeapSnapshotAggregates(
@@ -792,6 +798,7 @@ export class McpResponse implements Response {
792798
extensionPages?: object[];
793799
errorMessage?: string;
794800
navigatedToUrl?: string;
801+
newPagesOpened?: boolean;
795802
geolocation?: {latitude: number; longitude: number};
796803
} = {};
797804

@@ -841,6 +848,12 @@ export class McpResponse implements Response {
841848
structuredContent.navigatedToUrl =
842849
this.#attachedWaitForResult.navigatedToUrl;
843850
}
851+
if (this.#attachedWaitForResult.newPagesOpened) {
852+
response.push(
853+
`The action opened a new page. See the list of pages below and call ${selectPage.name} to switch to a page.`,
854+
);
855+
structuredContent.newPagesOpened = true;
856+
}
844857
}
845858

846859
const networkConditions = this.#page?.networkConditions;

src/WaitForHelper.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export class WaitForHelper {
2121
#dialogHandled = false;
2222
/** Track all dialogs as they pause the renderer. */
2323
#dialogDetected = false;
24+
/** Whether the action opened one or more new pages. */
25+
#newPagesOpened = false;
2426
#initialUrl: string;
2527

2628
constructor(
@@ -185,6 +187,16 @@ export class WaitForHelper {
185187
this.#page.off('dialog', dialogHandler);
186188
});
187189

190+
// Detect pages opened by the action (e.g., a click on a link with
191+
// target=_blank or a window.open() call) so the response can report them.
192+
const popupHandler = () => {
193+
this.#newPagesOpened = true;
194+
};
195+
this.#page.on('popup', popupHandler);
196+
this.#abortController.signal.addEventListener('abort', () => {
197+
this.#page.off('popup', popupHandler);
198+
});
199+
188200
const navigationFinished = this.waitForNavigationStarted()
189201
.then(navigationStated => {
190202
if (navigationStated) {
@@ -230,6 +242,7 @@ export class WaitForHelper {
230242
...(urlAfterAction !== this.#initialUrl
231243
? {navigatedToUrl: urlAfterAction}
232244
: {}),
245+
...(this.#newPagesOpened ? {newPagesOpened: true} : {}),
233246
dialogHandled: this.#dialogHandled,
234247
};
235248
}
@@ -241,6 +254,11 @@ export interface WaitForEventsResult {
241254
* occurred.
242255
*/
243256
navigatedToUrl?: string;
257+
/**
258+
* Whether the action opened one or more new pages (e.g., a click on a
259+
* link with target=_blank or a window.open() call).
260+
*/
261+
newPagesOpened?: boolean;
244262
/**
245263
* Whether a dialog was automatically handled during the action.
246264
*/

tests/tools/input.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,36 @@ describe('input', () => {
9090
assert.ok(await page.$('text/dblclicked'));
9191
});
9292
});
93+
it('reports newly opened pages', async () => {
94+
await withMcpContext(async (response, context) => {
95+
const page = context.getSelectedMcpPage().pptrPage;
96+
await page.setContent(
97+
html`<button onclick="window.open('about:blank');">open</button>`,
98+
);
99+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
100+
context.getSelectedMcpPage(),
101+
);
102+
await click.handler(
103+
{
104+
params: {
105+
uid: '1_1',
106+
},
107+
page: context.getSelectedMcpPage(),
108+
},
109+
response,
110+
context,
111+
);
112+
assert.strictEqual(
113+
response.responseLines[0],
114+
'Successfully clicked on the element',
115+
);
116+
assert.ok(response.includePages);
117+
const result = await response.handle(context);
118+
const text = getTextContent(result.content[0]);
119+
assert.ok(text.includes('The action opened a new page'));
120+
assert.ok(text.includes('## Pages'));
121+
});
122+
});
93123
it('waits for navigation', async () => {
94124
const resolveNavigation = Promise.withResolvers<void>();
95125
server.addHtmlRoute(

0 commit comments

Comments
 (0)