@@ -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 ) ;
@@ -378,49 +416,47 @@ export class McpContext implements Context {
378416 return this . #pageToDevToolsPage. get ( page ) ;
379417 }
380418
381- async getDevToolsData ( ) : Promise < undefined | { requestId ?: number } > {
419+ async getDevToolsData ( ) : Promise < DevToolsData > {
382420 try {
421+ this . logger ( 'Getting DevTools UI data' ) ;
383422 const selectedPage = this . getSelectedPage ( ) ;
384423 const devtoolsPage = this . getDevToolsPage ( selectedPage ) ;
385- if ( devtoolsPage ) {
386- const cdpRequestId = await devtoolsPage . evaluate ( async ( ) => {
424+ if ( ! devtoolsPage ) {
425+ this . logger ( 'No DevTools page detected' ) ;
426+ return { } ;
427+ }
428+ const { cdpRequestId, cdpBackendNodeId} = await devtoolsPage . evaluate (
429+ async ( ) => {
387430 // @ts -expect-error no types
388431 const UI = await import ( '/bundled/ui/legacy/legacy.js' ) ;
389432 // @ts -expect-error no types
390433 const SDK = await import ( '/bundled/core/sdk/sdk.js' ) ;
391434 const request = UI . Context . Context . instance ( ) . flavor (
392435 SDK . NetworkRequest . NetworkRequest ,
393436 ) ;
394- return request ?. requestId ( ) ;
395- } ) ;
396- if ( ! cdpRequestId ) {
397- this . logger ( 'no context request' ) ;
398- return ;
399- }
400- const request = this . #networkCollector. find ( selectedPage , request => {
401- // @ts -expect-error id is internal.
402- return request . id === cdpRequestId ;
403- } ) ;
404- if ( ! request ) {
405- this . logger ( 'no collected request for ' + cdpRequestId ) ;
406- return ;
407- }
408- return {
409- requestId : this . #networkCollector. getIdForResource ( request ) ,
410- } ;
411- } else {
412- this . logger ( 'no devtools page deteched' ) ;
413- }
437+ const node = UI . Context . Context . instance ( ) . flavor (
438+ SDK . DOMModel . DOMNode ,
439+ ) ;
440+ return {
441+ cdpRequestId : request ?. requestId ( ) ,
442+ cdpBackendNodeId : node ?. backendNodeId ( ) ,
443+ } ;
444+ } ,
445+ ) ;
446+ return { cdpBackendNodeId, cdpRequestId} ;
414447 } catch ( err ) {
415448 this . logger ( 'error getting devtools data' , err ) ;
416449 }
417- return ;
450+ return { } ;
418451 }
419452
420453 /**
421454 * Creates a text snapshot of a page.
422455 */
423- async createTextSnapshot ( verbose = false ) : Promise < void > {
456+ async createTextSnapshot (
457+ verbose = false ,
458+ devtoolsData : DevToolsData | undefined = undefined ,
459+ ) : Promise < void > {
424460 const page = this . getSelectedPage ( ) ;
425461 const rootNode = await page . accessibility . snapshot ( {
426462 includeIframes : true ,
@@ -463,6 +499,12 @@ export class McpContext implements Context {
463499 snapshotId : String ( snapshotId ) ,
464500 idToNode,
465501 } ;
502+ const data = devtoolsData ?? ( await this . getDevToolsData ( ) ) ;
503+ if ( data ?. cdpBackendNodeId ) {
504+ this . #textSnapshot. selectedElementUid = this . resolveCdpElementId (
505+ data ?. cdpBackendNodeId ,
506+ ) ;
507+ }
466508 }
467509
468510 getTextSnapshot ( ) : TextSnapshot | null {
0 commit comments