1010import time
1111from typing import Literal
1212
13- Detail = Literal ["standard" , "minimal " , "full" ]
13+ Detail = Literal ["compact " , "full" ]
1414
1515
1616# ---------------------------------------------------------------------------
@@ -122,6 +122,110 @@ def _count_nodes(nodes: list[dict]) -> int:
122122
123123_CHROME_ROLES = frozenset ({"scrollbar" , "separator" , "titlebar" , "tooltip" , "status" })
124124
125+ # ---------------------------------------------------------------------------
126+ # Vocabulary short codes — compact aliases for roles, states, and actions.
127+ # These reduce per-node token cost by ~50% on role/state/action strings.
128+ # ---------------------------------------------------------------------------
129+
130+ ROLE_CODES : dict [str , str ] = {
131+ "alert" : "alrt" ,
132+ "alertdialog" : "adlg" ,
133+ "application" : "app" ,
134+ "banner" : "bnr" ,
135+ "button" : "btn" ,
136+ "cell" : "cel" ,
137+ "checkbox" : "chk" ,
138+ "columnheader" : "colh" ,
139+ "combobox" : "cmb" ,
140+ "complementary" : "cmp" ,
141+ "contentinfo" : "ci" ,
142+ "dialog" : "dlg" ,
143+ "document" : "doc" ,
144+ "form" : "frm" ,
145+ "generic" : "gen" ,
146+ "grid" : "grd" ,
147+ "group" : "grp" ,
148+ "heading" : "hdg" ,
149+ "img" : "img" ,
150+ "link" : "lnk" ,
151+ "list" : "lst" ,
152+ "listitem" : "li" ,
153+ "log" : "log" ,
154+ "main" : "main" ,
155+ "marquee" : "mrq" ,
156+ "menu" : "mnu" ,
157+ "menubar" : "mnub" ,
158+ "menuitem" : "mi" ,
159+ "menuitemcheckbox" : "mic" ,
160+ "menuitemradio" : "mir" ,
161+ "navigation" : "nav" ,
162+ "none" : "none" ,
163+ "option" : "opt" ,
164+ "progressbar" : "pbar" ,
165+ "radio" : "rad" ,
166+ "region" : "rgn" ,
167+ "row" : "row" ,
168+ "rowheader" : "rowh" ,
169+ "scrollbar" : "sb" ,
170+ "search" : "srch" ,
171+ "searchbox" : "sbx" ,
172+ "separator" : "sep" ,
173+ "slider" : "sld" ,
174+ "spinbutton" : "spn" ,
175+ "status" : "sts" ,
176+ "switch" : "sw" ,
177+ "tab" : "tab" ,
178+ "table" : "tbl" ,
179+ "tablist" : "tabs" ,
180+ "tabpanel" : "tpnl" ,
181+ "text" : "txt" ,
182+ "textbox" : "tbx" ,
183+ "timer" : "tmr" ,
184+ "titlebar" : "ttlb" ,
185+ "toolbar" : "tlbr" ,
186+ "tooltip" : "ttp" ,
187+ "tree" : "tre" ,
188+ "treeitem" : "ti" ,
189+ "window" : "win" ,
190+ }
191+
192+ STATE_CODES : dict [str , str ] = {
193+ "busy" : "bsy" ,
194+ "checked" : "chk" ,
195+ "collapsed" : "col" ,
196+ "disabled" : "dis" ,
197+ "editable" : "edt" ,
198+ "expanded" : "exp" ,
199+ "focused" : "foc" ,
200+ "hidden" : "hid" ,
201+ "mixed" : "mix" ,
202+ "modal" : "mod" ,
203+ "multiselectable" : "msel" ,
204+ "offscreen" : "off" ,
205+ "pressed" : "prs" ,
206+ "readonly" : "ro" ,
207+ "required" : "req" ,
208+ "selected" : "sel" ,
209+ }
210+
211+ ACTION_CODES : dict [str , str ] = {
212+ "click" : "clk" ,
213+ "collapse" : "col" ,
214+ "decrement" : "dec" ,
215+ "dismiss" : "dsm" ,
216+ "doubleclick" : "dbl" ,
217+ "expand" : "exp" ,
218+ "focus" : "foc" ,
219+ "increment" : "inc" ,
220+ "longpress" : "lp" ,
221+ "rightclick" : "rclk" ,
222+ "scroll" : "scr" ,
223+ "select" : "sel" ,
224+ "setvalue" : "sv" ,
225+ "toggle" : "tog" ,
226+ "type" : "typ" ,
227+ }
228+
125229
126230def _should_skip (node : dict , parent : dict | None , siblings : int ) -> bool :
127231 """Decide if a node should be pruned (entire subtree is dropped)."""
@@ -346,64 +450,28 @@ def _has_meaningful_actions(node: dict) -> bool:
346450 return any (a != "focus" for a in actions )
347451
348452
349- def _prune_minimal_node (node : dict ) -> dict | None :
350- """Minimal pruning: keep only nodes with meaningful actions + ancestors.
351-
352- Returns a pruned copy of the node if it or any descendant has meaningful
353- actions, or None if the entire subtree can be dropped.
354- """
355- children = node .get ("children" , [])
356-
357- # Recursively prune children first
358- kept_children = []
359- for child in children :
360- pruned_child = _prune_minimal_node (child )
361- if pruned_child is not None :
362- kept_children .append (pruned_child )
363-
364- # Keep this node if it has meaningful actions OR if any child was kept
365- if _has_meaningful_actions (node ) or kept_children :
366- pruned = {k : v for k , v in node .items () if k != "children" }
367- if kept_children :
368- pruned ["children" ] = kept_children
369- return pruned
370-
371- return None
372-
373-
374453def prune_tree (
375454 tree : list [dict ],
376455 * ,
377- detail : Detail = "standard " ,
456+ detail : Detail = "compact " ,
378457 screen : dict | None = None ,
379458) -> list [dict ]:
380459 """Apply pruning to a CUP tree, returning a new pruned tree.
381460
382461 Args:
383462 tree: List of root CUP node dicts.
384463 detail: Pruning level:
385- "standard" — Remove unnamed generics, decorative images, empty
386- text, offscreen noise, etc. (default)
387- "minimal" — Keep only nodes with meaningful actions (not just
388- focus) and their ancestors. Dramatically reduces
389- token count.
390- "full" — No pruning; return every node from the raw tree.
464+ "compact" — Remove unnamed generics, decorative images, empty
465+ text, offscreen noise, etc. (default)
466+ "full" — No pruning; return every node from the raw tree.
391467 screen: Screen dimensions dict with "w" and "h" keys. When provided,
392468 elements entirely outside the screen bounds are clipped even
393469 if no scrollable ancestor is present.
394470 """
395471 if detail == "full" :
396472 return copy .deepcopy (tree )
397473
398- if detail == "minimal" :
399- result = []
400- for root in tree :
401- pruned = _prune_minimal_node (root )
402- if pruned is not None :
403- result .append (pruned )
404- return result
405-
406- # "standard" — use screen as baseline viewport so elements far offscreen
474+ # "compact" — use screen as baseline viewport so elements far offscreen
407475 # (e.g. in web-based apps with virtual scroll) are clipped even when no
408476 # ancestor exposes the "scroll" action.
409477 screen_viewport = None
@@ -417,7 +485,8 @@ def prune_tree(
417485
418486def _format_line (node : dict ) -> str :
419487 """Format a single CUP node as a compact one-liner."""
420- parts = [f"[{ node ['id' ]} ]" , node ["role" ]]
488+ role = node ["role" ]
489+ parts = [f"[{ node ['id' ]} ]" , ROLE_CODES .get (role , role )]
421490
422491 name = node .get ("name" , "" )
423492 if name :
@@ -426,22 +495,26 @@ def _format_line(node: dict) -> str:
426495 truncated = truncated .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' ).replace ("\n " , " " )
427496 parts .append (f'"{ truncated } "' )
428497
498+ # Actions (drop "focus" -- it's noise)
499+ actions = [a for a in node .get ("actions" , []) if a != "focus" ]
500+
501+ # Only include bounds for interactable nodes (nodes with meaningful actions).
502+ # Non-interactable nodes are context-only — agents reference them by ID, not
503+ # by coordinates, so spatial info adds tokens without value.
429504 bounds = node .get ("bounds" )
430- if bounds :
431- parts .append (f"@ { bounds ['x' ]} ,{ bounds ['y' ]} { bounds ['w' ]} x{ bounds ['h' ]} " )
505+ if bounds and actions :
506+ parts .append (f"{ bounds ['x' ]} ,{ bounds ['y' ]} { bounds ['w' ]} x{ bounds ['h' ]} " )
432507
433508 states = node .get ("states" , [])
434509 if states :
435- parts .append ("{" + "," .join (states ) + "}" )
510+ parts .append ("{" + "," .join (STATE_CODES . get ( s , s ) for s in states ) + "}" )
436511
437- # Actions (drop "focus" -- it's noise)
438- actions = [a for a in node .get ("actions" , []) if a != "focus" ]
439512 if actions :
440- parts .append ("[" + "," .join (actions ) + "]" )
513+ parts .append ("[" + "," .join (ACTION_CODES . get ( a , a ) for a in actions ) + "]" )
441514
442515 # Value for input-type elements
443516 value = node .get ("value" , "" )
444- if value and node [ " role" ] in ("textbox" , "searchbox" , "combobox" , "spinbutton" , "slider" ):
517+ if value and role in ("textbox" , "searchbox" , "combobox" , "spinbutton" , "slider" ):
445518 truncated_val = value [:120 ] + ("..." if len (value ) > 120 else "" )
446519 truncated_val = truncated_val .replace ('"' , '\\ "' ).replace ("\n " , " " )
447520 parts .append (f'val="{ truncated_val } "' )
@@ -513,7 +586,7 @@ def serialize_compact(
513586 envelope : dict ,
514587 * ,
515588 window_list : list [dict ] | None = None ,
516- detail : Detail = "standard " ,
589+ detail : Detail = "compact " ,
517590 max_chars : int = MAX_OUTPUT_CHARS ,
518591) -> str :
519592 """Serialize a CUP envelope to compact LLM-friendly text.
@@ -526,7 +599,7 @@ def serialize_compact(
526599 envelope: CUP envelope dict with tree data.
527600 window_list: Optional list of open windows to include in header
528601 for situational awareness (used by foreground scope).
529- detail: Pruning level ("standard", "minimal", or "full").
602+ detail: Pruning level ("compact" or "full").
530603 max_chars: Hard character limit for output. When exceeded, the
531604 output is truncated with a diagnostic message.
532605 """
0 commit comments