Skip to content

Commit 10f2fc5

Browse files
committed
fix: list pages from lightweight targets
1 parent 46e12eb commit 10f2fc5

7 files changed

Lines changed: 290 additions & 61 deletions

File tree

src/McpContext.ts

Lines changed: 186 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ interface McpContextOptions {
5656
performanceCrux: boolean;
5757
}
5858

59+
export interface PageSummary {
60+
id: number;
61+
url: string;
62+
selected: boolean;
63+
isExtension: boolean;
64+
isolatedContextName?: string;
65+
}
66+
5967
const DEFAULT_TIMEOUT = 5_000;
6068
const NAVIGATION_TIMEOUT = 10_000;
6169

@@ -88,13 +96,18 @@ export class McpContext implements Context {
8896
#nextIsolatedContextId = 1;
8997

9098
#pages: Page[] = [];
99+
#pageSummaries: PageSummary[] = [];
91100
#extensionServiceWorkers: ExtensionServiceWorker[] = [];
92101

93102
#mcpPages = new Map<Page, McpPage>();
103+
#pageTargets = new Map<Target, number>();
104+
#targetsByPageId = new Map<number, Target>();
94105
#selectedPage?: McpPage;
106+
#selectedPageId?: number;
95107
#networkCollector: NetworkCollector;
96108
#consoleCollector: ConsoleCollector;
97109
#devtoolsUniverseManager: UniverseManager;
110+
#collectorsInitialized = false;
98111

99112
#isRunningTrace = false;
100113
#screenRecorderData: {recorder: ScreenRecorder; filePath: string} | null =
@@ -143,11 +156,14 @@ export class McpContext implements Context {
143156
}
144157

145158
async #init() {
146-
const pages = await this.createPagesSnapshot();
159+
this.createPageTargetSnapshot();
160+
await this.#ensureInitialSelectedPage();
161+
const pages = this.#selectedPage ? [this.#selectedPage.pptrPage] : [];
147162
await this.createExtensionServiceWorkersSnapshot();
148163
await this.#networkCollector.init(pages);
149164
await this.#consoleCollector.init(pages);
150165
await this.#devtoolsUniverseManager.init(pages);
166+
this.#collectorsInitialized = true;
151167
}
152168

153169
dispose() {
@@ -275,22 +291,24 @@ export class McpContext implements Context {
275291
} else {
276292
page = await this.browser.newPage({background});
277293
}
278-
await this.createPagesSnapshot();
279-
this.selectPage(this.#getMcpPage(page));
280-
this.#networkCollector.addPage(page);
281-
this.#consoleCollector.addPage(page);
282-
return this.#getMcpPage(page);
294+
this.createPageTargetSnapshot();
295+
const target = page.target();
296+
const pageId = this.#getOrCreatePageIdForTarget(target);
297+
const mcpPage = this.#registerPage(page, pageId);
298+
mcpPage.isolatedContextName = isolatedContextName;
299+
this.selectPage(mcpPage);
300+
return mcpPage;
283301
}
284302
async closePage(pageId: number): Promise<void> {
285-
if (this.#pages.length === 1) {
303+
this.createPageTargetSnapshot();
304+
if (this.#pageSummaries.length === 1) {
286305
throw new Error(CLOSE_PAGE_ERROR);
287306
}
288-
const page = this.getPageById(pageId);
289-
if (page) {
290-
page.dispose();
291-
this.#mcpPages.delete(page.pptrPage);
292-
}
307+
const page = await this.ensurePageById(pageId);
308+
page.dispose();
309+
this.#mcpPages.delete(page.pptrPage);
293310
await page.pptrPage.close({runBeforeUnload: false});
311+
this.createPageTargetSnapshot();
294312
}
295313

296314
getNetworkRequestById(page: McpPage, reqid: number): HTTPRequest {
@@ -443,6 +461,33 @@ export class McpContext implements Context {
443461
return page;
444462
}
445463

464+
async ensurePageById(pageId: number): Promise<McpPage> {
465+
const existingPage = this.#mcpPages
466+
.values()
467+
.find(mcpPage => mcpPage.id === pageId);
468+
if (existingPage && !existingPage.pptrPage.isClosed()) {
469+
return existingPage;
470+
}
471+
472+
this.createPageTargetSnapshot();
473+
const target = this.#targetsByPageId.get(pageId);
474+
if (!target) {
475+
throw new Error('No page found');
476+
}
477+
478+
let page = await this.#resolveTargetPage(target);
479+
if (!page && target.url().startsWith('chrome-extension://')) {
480+
page = await this.#resolveTargetPage(target, true);
481+
}
482+
if (!page) {
483+
throw new Error(
484+
`Page ${pageId} is not responding. Call ${listPages().name} to see open pages.`,
485+
);
486+
}
487+
488+
return this.#registerPage(page, pageId);
489+
}
490+
446491
getPageId(page: Page): number | undefined {
447492
return this.#mcpPages.get(page)?.id;
448493
}
@@ -465,6 +510,7 @@ export class McpContext implements Context {
465510

466511
selectPage(newPage: McpPage): void {
467512
this.#selectedPage = newPage;
513+
this.#selectedPageId = newPage.id;
468514
this.#updateSelectedPageTimeouts();
469515
}
470516

@@ -533,19 +579,76 @@ export class McpContext implements Context {
533579
return this.#extensionServiceWorkers;
534580
}
535581

582+
createPageTargetSnapshot(): PageSummary[] {
583+
const contextToName = this.#getContextToName();
584+
const targets = this.browser.targets().filter(target => {
585+
return (
586+
this.#isPageTarget(target) &&
587+
(this.#options.experimentalDevToolsDebugging ||
588+
!target.url().startsWith('devtools://'))
589+
);
590+
});
591+
const currentTargets = new Set(targets);
592+
593+
for (const target of targets) {
594+
this.#getOrCreatePageIdForTarget(target);
595+
}
596+
597+
for (const [target, id] of this.#pageTargets) {
598+
if (!currentTargets.has(target)) {
599+
this.#pageTargets.delete(target);
600+
this.#targetsByPageId.delete(id);
601+
}
602+
}
603+
604+
for (const [page, mcpPage] of this.#mcpPages) {
605+
if (page.isClosed()) {
606+
mcpPage.dispose();
607+
this.#mcpPages.delete(page);
608+
}
609+
}
610+
611+
if (this.#selectedPage && this.#selectedPage.pptrPage.isClosed()) {
612+
this.#selectedPage = undefined;
613+
}
614+
615+
if (
616+
this.#selectedPageId === undefined ||
617+
!targets.some(target => {
618+
return this.#pageTargets.get(target) === this.#selectedPageId;
619+
})
620+
) {
621+
this.#selectedPageId = targets[0]
622+
? this.#getOrCreatePageIdForTarget(targets[0])
623+
: undefined;
624+
this.#selectedPage = undefined;
625+
}
626+
627+
this.#pageSummaries = targets.map(target => {
628+
const id = this.#getOrCreatePageIdForTarget(target);
629+
const isolatedContextName = contextToName.get(target.browserContext());
630+
const summary: PageSummary = {
631+
id,
632+
url: target.url(),
633+
selected: id === this.#selectedPageId,
634+
isExtension: target.url().startsWith('chrome-extension://'),
635+
};
636+
if (isolatedContextName) {
637+
summary.isolatedContextName = isolatedContextName;
638+
}
639+
return summary;
640+
});
641+
642+
return this.#pageSummaries;
643+
}
644+
536645
async createPagesSnapshot(): Promise<Page[]> {
537646
const {pages: allPages, isolatedContextNames} = await this.#getAllPages();
538647

539648
for (const page of allPages) {
540-
let mcpPage = this.#mcpPages.get(page);
541-
if (!mcpPage) {
542-
mcpPage = new McpPage(page, this.#nextPageId++);
543-
this.#mcpPages.set(page, mcpPage);
544-
// We emulate a focused page for all pages to support multi-agent workflows.
545-
void page.emulateFocusedPage(true).catch(error => {
546-
this.logger('Error turning on focused page emulation', error);
547-
});
548-
}
649+
const target = page.target();
650+
const pageId = this.#getOrCreatePageIdForTarget(target);
651+
const mcpPage = this.#registerPage(page, pageId);
549652
mcpPage.isolatedContextName = isolatedContextNames.get(page);
550653
}
551654

@@ -573,6 +676,7 @@ export class McpContext implements Context {
573676
this.selectPage(this.#getMcpPage(this.#pages[0]));
574677
}
575678

679+
this.createPageTargetSnapshot();
576680
await this.detectOpenDevToolsWindows();
577681

578682
return this.#pages;
@@ -582,7 +686,6 @@ export class McpContext implements Context {
582686
pages: Page[];
583687
isolatedContextNames: Map<Page, string>;
584688
}> {
585-
const defaultCtx = this.browser.defaultBrowserContext();
586689
const pagePromises: Array<Promise<Page | null>> = [];
587690
for (const target of this.browser.targets()) {
588691
if (!this.#isPageTarget(target)) {
@@ -626,7 +729,22 @@ export class McpContext implements Context {
626729
}
627730
}
628731

629-
// Build a reverse lookup from BrowserContext instance → name.
732+
const contextToName = this.#getContextToName();
733+
// Map each page to its isolated context name (if any).
734+
const isolatedContextNames = new Map<Page, string>();
735+
for (const page of allPages) {
736+
const ctx = page.browserContext();
737+
const name = contextToName.get(ctx);
738+
if (name) {
739+
isolatedContextNames.set(page, name);
740+
}
741+
}
742+
743+
return {pages: allPages, isolatedContextNames};
744+
}
745+
746+
#getContextToName(): Map<BrowserContext, string> {
747+
const defaultCtx = this.browser.defaultBrowserContext();
630748
const contextToName = new Map<BrowserContext, string>();
631749
for (const [name, ctx] of this.#isolatedContexts) {
632750
contextToName.set(ctx, name);
@@ -642,18 +760,48 @@ export class McpContext implements Context {
642760
contextToName.set(ctx, name);
643761
}
644762
}
763+
return contextToName;
764+
}
645765

646-
// Map each page to its isolated context name (if any).
647-
const isolatedContextNames = new Map<Page, string>();
648-
for (const page of allPages) {
649-
const ctx = page.browserContext();
650-
const name = contextToName.get(ctx);
651-
if (name) {
652-
isolatedContextNames.set(page, name);
766+
#getOrCreatePageIdForTarget(target: Target): number {
767+
const existingId = this.#pageTargets.get(target);
768+
if (existingId) {
769+
return existingId;
770+
}
771+
const id = this.#nextPageId++;
772+
this.#pageTargets.set(target, id);
773+
this.#targetsByPageId.set(id, target);
774+
return id;
775+
}
776+
777+
#registerPage(page: Page, pageId: number): McpPage {
778+
let mcpPage = this.#mcpPages.get(page);
779+
if (!mcpPage) {
780+
mcpPage = new McpPage(page, pageId);
781+
this.#mcpPages.set(page, mcpPage);
782+
// We emulate a focused page for all pages to support multi-agent workflows.
783+
void page.emulateFocusedPage(true).catch(error => {
784+
this.logger('Error turning on focused page emulation', error);
785+
});
786+
if (this.#collectorsInitialized) {
787+
this.#networkCollector.addPage(page);
788+
this.#consoleCollector.addPage(page);
653789
}
654790
}
791+
return mcpPage;
792+
}
655793

656-
return {pages: allPages, isolatedContextNames};
794+
async #ensureInitialSelectedPage(): Promise<void> {
795+
const pageId = this.#selectedPageId;
796+
if (pageId === undefined) {
797+
return;
798+
}
799+
try {
800+
const page = await this.ensurePageById(pageId);
801+
this.selectPage(page);
802+
} catch (error) {
803+
this.logger(`Failed to load initial selected page ${pageId}`, error);
804+
}
657805
}
658806

659807
#isPageTarget(target: Target): boolean {
@@ -694,7 +842,9 @@ export class McpContext implements Context {
694842

695843
async detectOpenDevToolsWindows() {
696844
this.logger('Detecting open DevTools windows');
697-
const {pages} = await this.#getAllPages();
845+
const pages = [...this.#mcpPages.keys()].filter(page => {
846+
return !page.isClosed();
847+
});
698848

699849
await Promise.all(
700850
pages.map(async page => {
@@ -733,6 +883,10 @@ export class McpContext implements Context {
733883
return this.#pages;
734884
}
735885

886+
getPageSummaries(): PageSummary[] {
887+
return this.#pageSummaries;
888+
}
889+
736890
getIsolatedContextName(page: Page): string | undefined {
737891
return this.#mcpPages.get(page)?.isolatedContextName;
738892
}

0 commit comments

Comments
 (0)