mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
fix: report when the selected page was auto-replaced by the fallback (#2308)
Follow-up to #2304 (the "note" part discussed there). When the selected page disappears from the page list, `createPagesSnapshot()` silently re-selects the first page. The agent gets no signal: if its next call is `list_pages` (which is what the closed-page error message recommends), the listing already shows the new selection and every subsequent tool call runs against a page the agent never picked. This PR records the automatic fallback and surfaces it as a one-line note in the pages section of the same response: ``` ## Pages Note: the previously selected page was closed. Page 1 is now selected. 1: about:blank [selected] ``` For a selected page that is missing from the list without being closed, the note reads "is no longer listed" instead. If the expectation from #2304 holds (a page stays listed as long as it is not closed), that wording never renders; if the transient case discussed there does occur in the wild, the note will make it visible. The fallback behavior itself is unchanged (as discussed in #2304, closed tabs keep the browser-like auto-selection). No note is emitted on first connect, when nothing was selected before. Tests: two unit tests for the fallback bookkeeping (closed page, regular selection), one for the missing-but-open case via a stubbed page list, and the one affected snapshot updated (`close_page` now includes the note). Refs: #2304
This commit is contained in:
@@ -88,6 +88,7 @@ export class McpContext implements Context {
|
||||
|
||||
#mcpPages = new Map<Page, McpPage>();
|
||||
#selectedPage?: McpPage;
|
||||
#selectedPageFallback?: {wasClosed: boolean};
|
||||
#networkCollector: NetworkCollector;
|
||||
#consoleCollector: ConsoleCollector;
|
||||
#devtoolsUniverseManager: UniverseManager;
|
||||
@@ -571,6 +572,16 @@ export class McpContext implements Context {
|
||||
this.#updateSelectedPageTimeouts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns details about the last page snapshot automatically replacing the
|
||||
* selection because the selected page disappeared from the page list, or
|
||||
* `undefined` if the snapshot left the selection intact. Recomputed on every
|
||||
* createPagesSnapshot() call.
|
||||
*/
|
||||
getSelectedPageFallback(): {wasClosed: boolean} | undefined {
|
||||
return this.#selectedPageFallback;
|
||||
}
|
||||
|
||||
#updateSelectedPageTimeouts() {
|
||||
const page = this.#getSelectedMcpPage();
|
||||
// For waiters 5sec timeout should be sufficient.
|
||||
@@ -674,11 +685,19 @@ export class McpContext implements Context {
|
||||
);
|
||||
});
|
||||
|
||||
this.#selectedPageFallback = undefined;
|
||||
if (
|
||||
(!this.#selectedPage ||
|
||||
this.#pages.indexOf(this.#selectedPage.pptrPage) === -1) &&
|
||||
this.#pages[0]
|
||||
) {
|
||||
// Record the automatic change so the response can surface it. Skipped on
|
||||
// first connect, when there was no prior selection to replace.
|
||||
if (this.#selectedPage) {
|
||||
this.#selectedPageFallback = {
|
||||
wasClosed: this.#selectedPage.pptrPage.isClosed(),
|
||||
};
|
||||
}
|
||||
this.selectPage(this.#getMcpPage(this.#pages[0]));
|
||||
}
|
||||
|
||||
|
||||
@@ -1006,6 +1006,18 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
{regularPages: [], extensionPages: []},
|
||||
);
|
||||
|
||||
const selectionFallback = context.getSelectedPageFallback();
|
||||
if (selectionFallback) {
|
||||
let selectedPageId: number | undefined;
|
||||
try {
|
||||
selectedPageId = context.getSelectedMcpPage().id;
|
||||
} catch {
|
||||
selectedPageId = undefined;
|
||||
}
|
||||
response.push(
|
||||
`Note: the previously selected page ${selectionFallback.wasClosed ? 'was closed' : 'is no longer listed'}.${selectedPageId !== undefined ? ` Page ${selectedPageId} is now selected.` : ''}`,
|
||||
);
|
||||
}
|
||||
if (regularPages.length) {
|
||||
const parts = [`## Pages`];
|
||||
const structuredPages = [];
|
||||
|
||||
@@ -135,6 +135,68 @@ describe('McpContext', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reports the fallback when the selected page is closed', async () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
const page = await context.newPage();
|
||||
assert.ok(context.isPageSelected(page.pptrPage));
|
||||
|
||||
await page.pptrPage.close();
|
||||
await context.createPagesSnapshot();
|
||||
|
||||
const [firstPage] = context.getPages();
|
||||
assert.ok(firstPage);
|
||||
assert.ok(context.isPageSelected(firstPage));
|
||||
|
||||
const fallback = context.getSelectedPageFallback();
|
||||
assert.ok(fallback, 'fallback should be reported');
|
||||
assert.strictEqual(fallback.wasClosed, true);
|
||||
});
|
||||
});
|
||||
|
||||
it('clears the fallback on the next snapshot with a valid selection', async () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
const page = await context.newPage();
|
||||
await page.pptrPage.close();
|
||||
await context.createPagesSnapshot();
|
||||
assert.ok(context.getSelectedPageFallback());
|
||||
|
||||
// A later snapshot keeps a valid selection (e.g. the one taken before the
|
||||
// next response, or after an explicit select), so the note is not repeated.
|
||||
await context.createPagesSnapshot();
|
||||
assert.strictEqual(context.getSelectedPageFallback(), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not report a fallback for a regular selection', async () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
await context.newPage();
|
||||
await context.createPagesSnapshot();
|
||||
assert.strictEqual(context.getSelectedPageFallback(), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('reports the fallback when the selected page is missing from the list', async () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
const page = await context.newPage();
|
||||
assert.ok(context.isPageSelected(page.pptrPage));
|
||||
|
||||
// A live page that is temporarily missing from the pages list.
|
||||
const pages = await context.browser.pages();
|
||||
const stub = sinon
|
||||
.stub(context.browser, 'pages')
|
||||
.resolves(pages.filter(otherPage => otherPage !== page.pptrPage));
|
||||
try {
|
||||
await context.createPagesSnapshot();
|
||||
} finally {
|
||||
stub.restore();
|
||||
}
|
||||
|
||||
const fallback = context.getSelectedPageFallback();
|
||||
assert.ok(fallback, 'fallback should be reported');
|
||||
assert.strictEqual(fallback.wasClosed, false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include network requests in structured content', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const mockRequest = getMockRequest({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
exports[`pages > close_page > when dialog is open 1`] = `
|
||||
{"content":[{"type":"text","text":"## Pages\\n1: about:blank [selected]"}],"structuredContent":{"pages":[{"id":1,"url":"about:blank","title":"","selected":true}]}}
|
||||
{"content":[{"type":"text","text":"Note: the previously selected page was closed. Page 1 is now selected.\\n## Pages\\n1: about:blank [selected]"}],"structuredContent":{"pages":[{"id":1,"url":"about:blank","title":"","selected":true}]}}
|
||||
`;
|
||||
|
||||
exports[`pages > list_pages > list pages for extension pages with --category-extensions 1`] = `
|
||||
|
||||
Reference in New Issue
Block a user