@@ -728,6 +728,9 @@ export class McpContext implements Context {
728728 page : McpPage ,
729729 verbose = false ,
730730 devtoolsData : DevToolsData | undefined = undefined ,
731+ options : {
732+ diff ?: boolean ;
733+ } = { } ,
731734 ) : Promise < void > {
732735 const rootNode = await page . pptrPage . accessibility . snapshot ( {
733736 includeIframes : true ,
@@ -745,10 +748,20 @@ export class McpContext implements Context {
745748 let idCounter = 0 ;
746749 const idToNode = new Map < string , TextSnapshotNode > ( ) ;
747750 const seenUniqueIds = new Set < string > ( ) ;
748- const assignIds = ( node : SerializedAXNode ) : TextSnapshotNode => {
751+ const assignIds = (
752+ node : SerializedAXNode ,
753+ parentId = 'root' ,
754+ index = 0 ,
755+ ) : TextSnapshotNode => {
749756 let id = '' ;
750- // @ts -expect-error untyped loaderId & backendNodeId.
751- const uniqueBackendId = `${ node . loaderId } _${ node . backendNodeId } ` ;
757+ const nodeAny = node as any ;
758+ // StaticText nodes often have unstable backendNodeIds in some contexts,
759+ // or we might want to group them by their parent.
760+ const uniqueBackendId =
761+ nodeAny . backendNodeId && node . role !== 'StaticText'
762+ ? `${ nodeAny . loaderId } _${ nodeAny . backendNodeId } `
763+ : `${ nodeAny . loaderId } _${ nodeAny . role } _${ parentId } _${ index } ` ;
764+
752765 if ( uniqueBackendNodeIdToMcpId . has ( uniqueBackendId ) ) {
753766 // Re-use MCP exposed ID if the uniqueId is the same.
754767 id = uniqueBackendNodeIdToMcpId . get ( uniqueBackendId ) ! ;
@@ -763,7 +776,7 @@ export class McpContext implements Context {
763776 ...node ,
764777 id,
765778 children : node . children
766- ? node . children . map ( child => assignIds ( child ) )
779+ ? node . children . map ( ( child , i ) => assignIds ( child , id , i ) )
767780 : [ ] ,
768781 } ;
769782
@@ -788,7 +801,38 @@ export class McpContext implements Context {
788801 hasSelectedElement : false ,
789802 verbose,
790803 } ;
804+
805+ if ( options . diff && page . lastSnapshot ) {
806+ const lastIdToNode = page . lastSnapshot . idToNode ;
807+ const added : string [ ] = [ ] ;
808+ const changed : string [ ] = [ ] ;
809+ const removed : string [ ] = [ ] ;
810+
811+ for ( const [ id , node ] of idToNode ) {
812+ const lastNode = lastIdToNode . get ( id ) ;
813+ if ( ! lastNode ) {
814+ added . push ( id ) ;
815+ } else if (
816+ node . name !== lastNode . name ||
817+ node . value !== lastNode . value ||
818+ node . description !== lastNode . description ||
819+ node . role !== lastNode . role
820+ ) {
821+ changed . push ( id ) ;
822+ }
823+ }
824+
825+ for ( const id of lastIdToNode . keys ( ) ) {
826+ if ( ! idToNode . has ( id ) ) {
827+ removed . push ( id ) ;
828+ }
829+ }
830+
831+ snapshot . diff = { added, changed, removed} ;
832+ }
833+
791834 page . textSnapshot = snapshot ;
835+ page . lastSnapshot = snapshot ;
792836 const data = devtoolsData ?? ( await this . getDevToolsData ( page ) ) ;
793837 if ( data ?. cdpBackendNodeId ) {
794838 snapshot . hasSelectedElement = true ;
0 commit comments