@@ -25,19 +25,21 @@ import type {
2525import { listPages } from './tools/pages.js' ;
2626import { takeSnapshot } from './tools/snapshot.js' ;
2727import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js' ;
28- import type { Context } from './tools/ToolDefinition.js' ;
28+ import type { Context , DevToolsData } from './tools/ToolDefinition.js' ;
2929import type { TraceResult } from './trace-processing/parse.js' ;
3030import { WaitForHelper } from './WaitForHelper.js' ;
3131
3232export interface TextSnapshotNode extends SerializedAXNode {
3333 id : string ;
34+ backendNodeId ?: number ;
3435 children : TextSnapshotNode [ ] ;
3536}
3637
3738export interface TextSnapshot {
3839 root : TextSnapshotNode ;
3940 idToNode : Map < string , TextSnapshotNode > ;
4041 snapshotId : string ;
42+ selectedElementUid ?: string ;
4143}
4244
4345interface McpContextOptions {
@@ -151,6 +153,42 @@ export class McpContext implements Context {
151153 return context ;
152154 }
153155
156+ resolveCdpRequestId ( cdpRequestId : string ) : number | undefined {
157+ const selectedPage = this . getSelectedPage ( ) ;
158+ if ( ! cdpRequestId ) {
159+ this . logger ( 'no network request' ) ;
160+ return ;
161+ }
162+ const request = this . #networkCollector. find ( selectedPage , request => {
163+ // @ts -expect-error id is internal.
164+ return request . id === cdpRequestId ;
165+ } ) ;
166+ if ( ! request ) {
167+ this . logger ( 'no network request for ' + cdpRequestId ) ;
168+ return ;
169+ }
170+ return this . #networkCollector. getIdForResource ( request ) ;
171+ }
172+
173+ resolveCdpElementId ( cdpBackendNodeId : number ) : string | undefined {
174+ if ( ! cdpBackendNodeId ) {
175+ this . logger ( 'no cdpBackendNodeId' ) ;
176+ return ;
177+ }
178+ // TODO: index by backendNodeId instead.
179+ const queue = [ this . #textSnapshot?. root ] ;
180+ while ( queue . length ) {
181+ const current = queue . pop ( ) ! ;
182+ if ( current . backendNodeId === cdpBackendNodeId ) {
183+ return current . id ;
184+ }
185+ for ( const child of current . children ) {
186+ queue . push ( child ) ;
187+ }
188+ }
189+ return ;
190+ }
191+
154192 getNetworkRequests ( includePreservedRequests ?: boolean ) : HTTPRequest [ ] {
155193 const page = this . getSelectedPage ( ) ;
156194 return this . #networkCollector. getData ( page , includePreservedRequests ) ;
@@ -380,49 +418,47 @@ export class McpContext implements Context {
380418 return this . #pageToDevToolsPage. get ( page ) ;
381419 }
382420
383- async getDevToolsData ( ) : Promise < undefined | { requestId ?: number } > {
421+ async getDevToolsData ( ) : Promise < DevToolsData > {
384422 try {
423+ this . logger ( 'Getting DevTools UI data' ) ;
385424 const selectedPage = this . getSelectedPage ( ) ;
386425 const devtoolsPage = this . getDevToolsPage ( selectedPage ) ;
387- if ( devtoolsPage ) {
388- const cdpRequestId = await devtoolsPage . evaluate ( async ( ) => {
426+ if ( ! devtoolsPage ) {
427+ this . logger ( 'No DevTools page detected' ) ;
428+ return { } ;
429+ }
430+ const { cdpRequestId, cdpBackendNodeId} = await devtoolsPage . evaluate (
431+ async ( ) => {
389432 // @ts -expect-error no types
390433 const UI = await import ( '/bundled/ui/legacy/legacy.js' ) ;
391434 // @ts -expect-error no types
392435 const SDK = await import ( '/bundled/core/sdk/sdk.js' ) ;
393436 const request = UI . Context . Context . instance ( ) . flavor (
394437 SDK . NetworkRequest . NetworkRequest ,
395438 ) ;
396- return request ?. requestId ( ) ;
397- } ) ;
398- if ( ! cdpRequestId ) {
399- this . logger ( 'no context request' ) ;
400- return ;
401- }
402- const request = this . #networkCollector. find ( selectedPage , request => {
403- // @ts -expect-error id is internal.
404- return request . id === cdpRequestId ;
405- } ) ;
406- if ( ! request ) {
407- this . logger ( 'no collected request for ' + cdpRequestId ) ;
408- return ;
409- }
410- return {
411- requestId : this . #networkCollector. getIdForResource ( request ) ,
412- } ;
413- } else {
414- this . logger ( 'no devtools page deteched' ) ;
415- }
439+ const node = UI . Context . Context . instance ( ) . flavor (
440+ SDK . DOMModel . DOMNode ,
441+ ) ;
442+ return {
443+ cdpRequestId : request ?. requestId ( ) ,
444+ cdpBackendNodeId : node ?. backendNodeId ( ) ,
445+ } ;
446+ } ,
447+ ) ;
448+ return { cdpBackendNodeId, cdpRequestId} ;
416449 } catch ( err ) {
417450 this . logger ( 'error getting devtools data' , err ) ;
418451 }
419- return ;
452+ return { } ;
420453 }
421454
422455 /**
423456 * Creates a text snapshot of a page.
424457 */
425- async createTextSnapshot ( verbose = false ) : Promise < void > {
458+ async createTextSnapshot (
459+ verbose = false ,
460+ devtoolsData : DevToolsData | undefined = undefined ,
461+ ) : Promise < void > {
426462 const page = this . getSelectedPage ( ) ;
427463 const rootNode = await page . accessibility . snapshot ( {
428464 includeIframes : true ,
@@ -465,6 +501,12 @@ export class McpContext implements Context {
465501 snapshotId : String ( snapshotId ) ,
466502 idToNode,
467503 } ;
504+ const data = devtoolsData ?? ( await this . getDevToolsData ( ) ) ;
505+ if ( data ?. cdpBackendNodeId ) {
506+ this . #textSnapshot. selectedElementUid = this . resolveCdpElementId (
507+ data ?. cdpBackendNodeId ,
508+ ) ;
509+ }
468510 }
469511
470512 getTextSnapshot ( ) : TextSnapshot | null {
0 commit comments