@@ -24,29 +24,55 @@ function jsonBytes(value: unknown): number {
2424 return encoder . encode ( JSON . stringify ( value ) ) . length
2525}
2626
27+ type SnapshotNode = ComputerUseSnapshot [ 'nodes' ] [ number ]
28+
29+ /** Layout wrappers that carry no text, state or action; their children are shown under the nearest meaningful ancestor. */
30+ const WRAPPER_ROLES = new Set ( [
31+ 'AXGroup' ,
32+ 'AXSplitGroup' ,
33+ 'AXLayoutArea' ,
34+ 'AXLayoutItem' ,
35+ 'AXUnknown' ,
36+ ] )
37+
38+ function isWrapper ( node : SnapshotNode ) : boolean {
39+ return (
40+ WRAPPER_ROLES . has ( node . role ) &&
41+ ! node . label &&
42+ ! node . value &&
43+ ! node . placeholder &&
44+ ! node . focused &&
45+ ! node . editable &&
46+ node . actions . length === 0
47+ )
48+ }
49+
2750function nodeRow (
28- node : ComputerUseSnapshot [ 'nodes' ] [ number ] ,
51+ node : SnapshotNode ,
52+ parentId : string | undefined ,
53+ selectedWindowId : string ,
2954 selected ?: ReadonlySet < string >
3055) : string {
3156 const fields = [ node . elementId , node . role ]
32- if ( node . parentId && ( ! selected || selected . has ( node . parentId ) ) )
33- fields . push ( `parent=${ node . parentId } ` )
57+ if ( parentId && ( ! selected || selected . has ( parentId ) ) ) fields . push ( `parent=${ parentId } ` )
3458 if ( node . label ) fields . push ( `label=${ JSON . stringify ( truncateAtCodePoint ( node . label , 512 , '' ) ) } ` )
3559 if ( node . value ) fields . push ( `value=${ JSON . stringify ( truncateAtCodePoint ( node . value , 512 , '' ) ) } ` )
3660 if ( node . placeholder )
3761 fields . push ( `placeholder=${ JSON . stringify ( truncateAtCodePoint ( node . placeholder , 256 , '' ) ) } ` )
3862 if ( node . focused ) fields . push ( 'focused' )
3963 if ( node . editable ) fields . push ( 'editable' )
4064 if ( node . enabled === false ) fields . push ( 'disabled' )
41- if ( node . actions . length ) fields . push ( `actions=${ JSON . stringify ( node . actions . slice ( 0 , 16 ) ) } ` )
42- if ( node . windowId ) fields . push ( `window=${ node . windowId } ` )
65+ if ( node . actions . length ) fields . push ( `actions=${ node . actions . slice ( 0 , 16 ) . join ( ',' ) } ` )
66+ if ( node . windowId && node . windowId !== selectedWindowId ) fields . push ( `window=${ node . windowId } ` )
4367 if (
4468 node . x !== undefined &&
4569 node . y !== undefined &&
4670 node . width !== undefined &&
4771 node . height !== undefined
4872 )
49- fields . push ( `screenRect=${ JSON . stringify ( [ node . x , node . y , node . width , node . height ] ) } ` )
73+ fields . push (
74+ `screenRect=${ [ node . x , node . y , node . width , node . height ] . map ( ( value ) => Math . round ( value ) ) . join ( ',' ) } `
75+ )
5076 return fields . join ( ' ' )
5177}
5278
@@ -70,8 +96,10 @@ function snapshotForModel(result: ComputerUseSnapshot, envelopeBytes = 0) {
7096 : { } ) ,
7197 windows,
7298 omittedWindowCount : allWindows . length - windows . length ,
99+ treeFormat :
100+ 'Rows: elementId role [parent=id] [label|value|placeholder] [focused] [editable] [disabled] [actions=...] [window=id, omitted for windowId] [screenRect=x,y,w,h in global points]. Empty layout groups are folded into their parent.' ,
73101 accessibilityTree : '' ,
74- omittedNodeCount : nodes . length ,
102+ omittedNodeCount : nodes . filter ( ( node ) => ! isWrapper ( node ) ) . length ,
75103 nodeTextLimit : 512 ,
76104 ...( image
77105 ? {
@@ -87,8 +115,25 @@ function snapshotForModel(result: ComputerUseSnapshot, envelopeBytes = 0) {
87115 model . omittedWindowCount += 1
88116 }
89117 const byId = new Map ( nodes . map ( ( node ) => [ node . elementId , node ] ) )
90- const rows = new Map ( nodes . map ( ( node ) => [ node . elementId , nodeRow ( node ) ] ) )
91- const priority = ( node : ComputerUseSnapshot [ 'nodes' ] [ number ] ) => {
118+ const shown = nodes . filter ( ( node ) => ! isWrapper ( node ) )
119+ /** Each shown node's nearest shown ancestor, skipping wrappers (and stopping on a parent cycle). */
120+ const shownParent = new Map < string , string | undefined > ( )
121+ for ( const node of shown ) {
122+ const seen = new Set ( [ node . elementId ] )
123+ let parent = node . parentId ? byId . get ( node . parentId ) : undefined
124+ while ( parent && isWrapper ( parent ) && ! seen . has ( parent . elementId ) ) {
125+ seen . add ( parent . elementId )
126+ parent = parent . parentId ? byId . get ( parent . parentId ) : undefined
127+ }
128+ shownParent . set ( node . elementId , parent && ! isWrapper ( parent ) ? parent . elementId : undefined )
129+ }
130+ const rows = new Map (
131+ shown . map ( ( node ) => [
132+ node . elementId ,
133+ nodeRow ( node , shownParent . get ( node . elementId ) , state . windowId ) ,
134+ ] )
135+ )
136+ const priority = ( node : SnapshotNode ) => {
92137 const visible =
93138 selectedWindow &&
94139 node . windowId === selectedWindow . windowId &&
@@ -133,14 +178,14 @@ function snapshotForModel(result: ComputerUseSnapshot, envelopeBytes = 0) {
133178 bytes += cost
134179 return true
135180 }
136- for ( const node of [ ...nodes ] . sort ( ( left , right ) => priority ( right ) - priority ( left ) ) ) {
181+ for ( const node of [ ...shown ] . sort ( ( left , right ) => priority ( right ) - priority ( left ) ) ) {
137182 const ancestry = [ node . elementId ]
138183 const seen = new Set ( ancestry )
139- let parent = node . parentId
140- while ( parent && byId . has ( parent ) && ! seen . has ( parent ) ) {
184+ let parent = shownParent . get ( node . elementId )
185+ while ( parent && ! seen . has ( parent ) ) {
141186 ancestry . push ( parent )
142187 seen . add ( parent )
143- parent = byId . get ( parent ) ?. parentId
188+ parent = shownParent . get ( parent )
144189 }
145190 /** Keep the editor itself even if an unusually deep ancestry cannot fit. */
146191 if ( ! add ( ancestry . reverse ( ) ) ) add ( [ node . elementId ] )
@@ -155,14 +200,14 @@ function snapshotForModel(result: ComputerUseSnapshot, envelopeBytes = 0) {
155200 ) || allWindows . some ( ( window ) => window . title . length > 256 )
156201 return {
157202 ...model ,
158- accessibilityTree : nodes
203+ accessibilityTree : shown
159204 . filter ( ( node ) => selected . has ( node . elementId ) )
160- . map ( ( node ) => nodeRow ( node , selected ) )
205+ . map ( ( node ) => nodeRow ( node , shownParent . get ( node . elementId ) , state . windowId , selected ) )
161206 . join ( '\n' ) ,
162- omittedNodeCount : nodes . length - selected . size ,
207+ omittedNodeCount : shown . length - selected . size ,
163208 truncated :
164209 state . truncated ||
165- selected . size < nodes . length ||
210+ selected . size < shown . length ||
166211 windows . length < allWindows . length ||
167212 clipped ||
168213 screenshotOmitted ,
0 commit comments