@@ -40,6 +40,7 @@ import {WaitForHelper} from './WaitForHelper.js';
4040export interface TextSnapshotNode extends SerializedAXNode {
4141 id : string ;
4242 backendNodeId ?: number ;
43+ loaderId ?: string ;
4344 children : TextSnapshotNode [ ] ;
4445}
4546
@@ -129,6 +130,8 @@ export class McpContext implements Context {
129130 #locatorClass: typeof Locator ;
130131 #options: McpContextOptions ;
131132
133+ #uniqueBackendNodeIdToMcpId = new Map < string , string > ( ) ;
134+
132135 private constructor (
133136 browser : Browser ,
134137 logger : Debugger ,
@@ -440,14 +443,6 @@ export class McpContext implements Context {
440443 `No snapshot found. Use ${ takeSnapshot . name } to capture one.` ,
441444 ) ;
442445 }
443- const [ snapshotId ] = uid . split ( '_' ) ;
444-
445- if ( this . #textSnapshot. snapshotId !== snapshotId ) {
446- throw new Error (
447- 'This uid is coming from a stale snapshot. Call take_snapshot to get a fresh snapshot.' ,
448- ) ;
449- }
450-
451446 const node = this . #textSnapshot?. idToNode . get ( uid ) ;
452447 if ( ! node ) {
453448 throw new Error ( 'No such element found in the snapshot' ) ;
@@ -589,15 +584,27 @@ export class McpContext implements Context {
589584 // will be used for the tree serialization and mapping ids back to nodes.
590585 let idCounter = 0 ;
591586 const idToNode = new Map < string , TextSnapshotNode > ( ) ;
587+ const seenUniqueIds = new Set < string > ( ) ;
592588 const assignIds = ( node : SerializedAXNode ) : TextSnapshotNode => {
593589 const nodeWithId : TextSnapshotNode = {
594590 ...node ,
595- id : ` ${ snapshotId } _ ${ idCounter ++ } ` ,
591+ id : '' , // placeholder to be set below.
596592 children : node . children
597593 ? node . children . map ( child => assignIds ( child ) )
598594 : [ ] ,
599595 } ;
600596
597+ const uniqueBackendId = `${ nodeWithId . loaderId } _${ nodeWithId . backendNodeId } ` ;
598+ if ( this . #uniqueBackendNodeIdToMcpId. has ( uniqueBackendId ) ) {
599+ // Re-use MCP exposed ID if the uniqueId is the same.
600+ nodeWithId . id = this . #uniqueBackendNodeIdToMcpId. get ( uniqueBackendId ) ! ;
601+ } else {
602+ // Only generate a new ID if we have not seen the node before.
603+ nodeWithId . id = `${ snapshotId } _${ idCounter ++ } ` ;
604+ this . #uniqueBackendNodeIdToMcpId. set ( uniqueBackendId , nodeWithId . id ) ;
605+ }
606+ seenUniqueIds . add ( uniqueBackendId ) ;
607+
601608 // The AXNode for an option doesn't contain its `value`.
602609 // Therefore, set text content of the option as value.
603610 if ( node . role === 'option' ) {
@@ -626,6 +633,13 @@ export class McpContext implements Context {
626633 data ?. cdpBackendNodeId ,
627634 ) ;
628635 }
636+
637+ // Clean up unique IDs that we did not see anymore.
638+ for ( const key of this . #uniqueBackendNodeIdToMcpId. keys ( ) ) {
639+ if ( ! seenUniqueIds . has ( key ) ) {
640+ this . #uniqueBackendNodeIdToMcpId. delete ( key ) ;
641+ }
642+ }
629643 }
630644
631645 getTextSnapshot ( ) : TextSnapshot | null {
0 commit comments