@@ -77,6 +77,7 @@ import {
7777 type WebMCPTool ,
7878 type Protocol ,
7979 type Page ,
80+ type Frame ,
8081 type ConsoleMessage ,
8182 type HTTPRequest ,
8283 type DevTools ,
@@ -103,6 +104,7 @@ import {
103104 type WaitForEventsResult ,
104105 type DialogAction ,
105106} from './WaitForHelper.js' ;
107+ type DisposableStackInstance = InstanceType < typeof DisposableStack > ;
106108
107109/**
108110 * Per-page state wrapper. Consolidates dialog, snapshot, emulation,
@@ -120,6 +122,9 @@ export class McpPage implements ContextPage {
120122 textSnapshot : TextSnapshot | null = null ;
121123 uniqueBackendNodeIdToMcpId = new Map < string , string > ( ) ;
122124 extraHandles : ElementHandle [ ] = [ ] ;
125+ #extraHandleResources = new DisposableStack ( ) ;
126+ #extraHandleGeneration = 0 ;
127+ #disposed = false ;
123128
124129 // Emulation
125130 emulationSettings : EmulationSettings = { } ;
@@ -131,6 +136,7 @@ export class McpPage implements ContextPage {
131136 // Dialog
132137 #dialog?: Dialog ;
133138 #dialogHandler: ( dialog : Dialog ) => void ;
139+ #frameNavigatedHandler: ( frame : Frame ) => void ;
134140
135141 thirdPartyDeveloperTools : ToolGroups = [ ] ;
136142
@@ -157,7 +163,13 @@ export class McpPage implements ContextPage {
157163 this . #dialogHandler = ( dialog : Dialog ) : void => {
158164 this . #dialog = dialog ;
159165 } ;
166+ this . #frameNavigatedHandler = ( frame : Frame ) : void => {
167+ if ( frame === page . mainFrame ( ) ) {
168+ this . #invalidateExtraHandles( ) ;
169+ }
170+ } ;
160171 page . on ( 'dialog' , this . #dialogHandler) ;
172+ page . on ( 'framenavigated' , this . #frameNavigatedHandler) ;
161173
162174 this . networkCollector = new NetworkCollector ( page ) ;
163175 this . consoleCollector = new ConsoleCollector ( page , collect => {
@@ -428,20 +440,70 @@ export class McpPage implements ContextPage {
428440 }
429441
430442 dispose ( ) : void {
443+ if ( this . #disposed) {
444+ return ;
445+ }
446+ this . #disposed = true ;
431447 this . pptrPage . off ( 'dialog' , this . #dialogHandler) ;
448+ this . pptrPage . off ( 'framenavigated' , this . #frameNavigatedHandler) ;
432449 this . networkCollector . dispose ( ) ;
433450 this . consoleCollector . dispose ( ) ;
451+ this . #invalidateExtraHandles( ) ;
452+ }
453+
454+ #invalidateExtraHandles( ) : void {
455+ this . #extraHandleGeneration++ ;
456+ this . #clearExtraHandles( ) ;
457+ }
458+
459+ #clearExtraHandles( ) : void {
460+ this . #extraHandleResources. dispose ( ) ;
461+ this . #extraHandleResources = new DisposableStack ( ) ;
462+ this . extraHandles = [ ] ;
463+ }
464+
465+ #replaceExtraHandles(
466+ handles : ElementHandle [ ] ,
467+ resources : DisposableStackInstance ,
468+ generation : number ,
469+ ) : void {
470+ if ( this . #disposed) {
471+ resources . dispose ( ) ;
472+ throw new Error (
473+ `Page ${ this . id } was disposed before retaining element handles.` ,
474+ ) ;
475+ }
476+ if ( generation !== this . #extraHandleGeneration) {
477+ resources . dispose ( ) ;
478+ throw new Error (
479+ `Page ${ this . id } changed before retaining element handles.` ,
480+ ) ;
481+ }
482+ const previousResources = this . #extraHandleResources;
483+ this . #extraHandleResources = resources ;
484+ this . extraHandles = handles ;
485+ previousResources . dispose ( ) ;
486+ }
487+
488+ async #clearStashedElements( ) : Promise < void > {
489+ await this . pptrPage . evaluate ( ( ) => {
490+ if ( window . __dtmcp ) {
491+ window . __dtmcp . stashedElements = [ ] ;
492+ }
493+ } ) ;
434494 }
435495
436496 async executeThirdPartyDeveloperTool (
437497 toolName : string ,
438498 params : Record < string , unknown > ,
439499 response : Response ,
440500 ) : Promise < void > {
501+ const extraHandleGeneration = this . #extraHandleGeneration;
441502 // Creates array of ElementHandles from the UIDs in the params.
442503 // We do not replace the uids with the ElementsHandles yet, because
443504 // the `evaluate` function only turns them into DOM elements if they
444505 // are passed as non-nested arguments.
506+ using inputHandleResources = new DisposableStack ( ) ;
445507 const handles : ElementHandle [ ] = [ ] ;
446508 for ( const value of Object . values ( params ) ) {
447509 if (
@@ -450,11 +512,13 @@ export class McpPage implements ContextPage {
450512 typeof value . uid === 'string' &&
451513 Object . keys ( value ) . length === 1
452514 ) {
453- handles . push ( await this . getElementByUid ( value . uid ) ) ;
515+ handles . push (
516+ inputHandleResources . use ( await this . getElementByUid ( value . uid ) ) ,
517+ ) ;
454518 }
455519 }
456520
457- const result = await this . pptrPage . evaluate (
521+ const toolExecution = this . pptrPage . evaluate (
458522 async ( name , args , ...elements ) => {
459523 // Replace the UIDs with DOM elements.
460524 for ( const [ key , value ] of Object . entries ( args ) ) {
@@ -471,6 +535,7 @@ export class McpPage implements ContextPage {
471535 if ( ! window . __dtmcp ?. executeTool ) {
472536 throw new Error ( 'No tools found on the page' ) ;
473537 }
538+ window . __dtmcp . stashedElements = [ ] ;
474539 const toolResult = await window . __dtmcp . executeTool ( name , args ) ;
475540
476541 const stashDOMElement = ( el : Element ) => {
@@ -548,27 +613,51 @@ export class McpPage implements ContextPage {
548613 params ,
549614 ...handles ,
550615 ) ;
616+ let result : Awaited < typeof toolExecution > ;
617+ try {
618+ result = await toolExecution ;
619+ } catch ( error ) {
620+ try {
621+ await this . #clearStashedElements( ) ;
622+ } catch ( clearError ) {
623+ logger ?.( 'Failed to clear stashed elements' , clearError ) ;
624+ }
625+ throw error ;
626+ }
551627
628+ using extraHandleResources = new DisposableStack ( ) ;
552629 const elementHandles : ElementHandle [ ] = [ ] ;
553- for ( let i = 0 ; i < ( result . stashed ?? 0 ) ; i ++ ) {
554- const elementHandle = await this . pptrPage . evaluateHandle ( index => {
555- const el = window . __dtmcp ?. stashedElements ?. [ index ] ;
556- if ( ! el ) {
557- throw new Error ( `Stashed element at index ${ index } not found` ) ;
558- }
559- return el ;
560- } , i ) ;
561- elementHandles . push ( elementHandle ) ;
630+ try {
631+ for ( let i = 0 ; i < ( result . stashed ?? 0 ) ; i ++ ) {
632+ const elementHandle = await this . pptrPage . evaluateHandle ( index => {
633+ const el = window . __dtmcp ?. stashedElements ?. [ index ] ;
634+ if ( ! el ) {
635+ throw new Error ( `Stashed element at index ${ index } not found` ) ;
636+ }
637+ return el ;
638+ } , i ) ;
639+ elementHandles . push ( extraHandleResources . use ( elementHandle ) ) ;
640+ }
641+ } catch ( error ) {
642+ try {
643+ await this . #clearStashedElements( ) ;
644+ } catch ( clearError ) {
645+ logger ?.( 'Failed to clear stashed elements' , clearError ) ;
646+ }
647+ throw error ;
562648 }
649+ await this . #clearStashedElements( ) ;
563650
564651 if ( elementHandles . length ) {
565- using stack = new DisposableStack ( ) ;
566- for ( const handle of elementHandles ) {
567- stack . use ( handle ) ;
568- }
569- this . textSnapshot = await TextSnapshot . create ( this , {
652+ const textSnapshot = await TextSnapshot . create ( this , {
570653 extraHandles : elementHandles ,
571654 } ) ;
655+ this . #replaceExtraHandles(
656+ elementHandles ,
657+ extraHandleResources . move ( ) ,
658+ extraHandleGeneration ,
659+ ) ;
660+ this . textSnapshot = textSnapshot ;
572661 response . includeSnapshot ( ) ;
573662 }
574663
0 commit comments