2026-10-17 22:43:38 -07:00
import AppKit
import ApplicationServices
@ _silgen_name ( "_AXUIElementGetWindow" )
func _AXUIElementGetWindow ( _ element : AXUIElement , _ id : UnsafeMutablePointer < CGWindowID >) -> AXError
struct RefSource {
let pid : pid_t
let window : Int ?
}
2026-10-17 23:03:33 -07:00
struct RefTable {
let elements : [ String : AXUIElement ]
let source : RefSource
}
var latestRefs = RefTable ( elements : [:], source : RefSource ( pid : 0 , window : nil ))
var refCount = 0
let refsLock = NSLock ()
func currentRefs () -> RefTable {
return refsLock . withLock { latestRefs }
}
func newRef () -> String {
return refsLock . withLock {
refCount += 1
return "e \( refCount ) "
}
}
2026-10-17 22:43:38 -07:00
let shownActions : [ String : String ] = [
kAXPressAction : "press" ,
kAXConfirmAction : "confirm" ,
kAXIncrementAction : "increment" ,
kAXDecrementAction : "decrement" ,
kAXPickAction : "pick" ,
kAXCancelAction : "cancel" ,
]
let maxVisits = 30000
2026-10-18 08:43:52 -07:00
let notAnswering = ToolError ( "the app isn't answering accessibility requests; it may be busy or have a menu open (capture it to see, and escape closes a menu)" )
func attr ( _ e : AXUIElement , _ name : String ) throws -> CFTypeRef ? {
2026-10-17 22:43:38 -07:00
var v : CFTypeRef ?
2026-10-18 08:43:52 -07:00
let err = AXUIElementCopyAttributeValue ( e , name as CFString , & v )
if err == . cannotComplete {
throw notAnswering
}
if err != . success {
2026-10-17 22:43:38 -07:00
return nil
}
return v
}
2026-10-18 08:43:52 -07:00
func text ( _ e : AXUIElement , _ name : String ) throws -> String {
guard let v = try attr ( e , name ) else {
2026-10-17 22:43:38 -07:00
return ""
}
if let s = v as ? String {
return s
}
if CFGetTypeID ( v ) == CFBooleanGetTypeID () {
return ""
}
if let n = v as ? NSNumber {
return n . stringValue
}
return ""
}
2026-10-18 08:43:52 -07:00
func children ( _ e : AXUIElement ) throws -> [ AXUIElement ] {
return ( try attr ( e , kAXChildrenAttribute ) as ? [ AXUIElement ]) ?? []
2026-10-17 22:43:38 -07:00
}
func appElement ( _ pid : pid_t ) -> AXUIElement {
2026-10-17 23:03:33 -07:00
return AXUIElementCreateApplication ( pid )
}
func requireTrust () throws {
if ! AXIsProcessTrusted () {
throw ToolError ( "byre lacks Accessibility, so no app's tree can be read or acted on; the human must switch byre on in System Settings → Privacy & Security → Accessibility" )
}
}
func axRole ( _ e : AXUIElement ) throws -> String {
2026-10-18 08:43:52 -07:00
return try attr ( e , kAXRoleAttribute ) as ? String ?? ""
2026-10-17 22:43:38 -07:00
}
func axWindow ( _ id : Int ) throws -> ( WindowInfo , AXUIElement ) {
2026-10-17 23:03:33 -07:00
try requireTrust ()
2026-10-17 22:43:38 -07:00
let info = try window ( id )
2026-10-18 08:43:52 -07:00
for w in ( try attr ( appElement ( info . pid ), kAXWindowsAttribute ) as ? [ AXUIElement ]) ?? [] {
2026-10-17 22:43:38 -07:00
var wid : CGWindowID = 0
if _AXUIElementGetWindow ( w , & wid ) == . success && Int ( wid ) == id {
return ( info , w )
}
}
throw ToolError ( "window \( id ) has no accessibility element; the app may not expose it" )
}
2026-10-17 23:03:33 -07:00
func element ( _ ref : String , in info : WindowInfo , _ table : RefTable ) throws -> AXUIElement {
let s = table . source
guard let e = table . elements [ ref ] else {
throw ToolError ( "unknown ref \( ref ) ; refs come from the latest ax_tree (or a tree returned by act), and each new tree replaces them with new numbers" )
2026-10-17 22:43:38 -07:00
}
if s . pid != info . pid || ( s . window != nil && s . window != Int ( info . id )) {
let from = s . window . map { "window \( $0 ) " } ?? "the menu bar of process \( s . pid ) "
throw ToolError ( " \( ref ) is from \( from ) , not window \( info . id ) ; read window \( info . id ) 's tree (or its app's menu bar) first" )
}
return e
}
2026-10-18 08:43:52 -07:00
func frame ( _ e : AXUIElement ) throws -> CGRect ? {
guard let p = try attr ( e , kAXPositionAttribute ), let s = try attr ( e , kAXSizeAttribute ) else {
2026-10-17 22:43:38 -07:00
return nil
}
var pt = CGPoint . zero
var sz = CGSize . zero
AXValueGetValue ( p as ! AXValue , . cgPoint , & pt )
AXValueGetValue ( s as ! AXValue , . cgSize , & sz )
return CGRect ( origin : pt , size : sz )
}
func clip ( _ s : String , _ n : Int ) -> String {
let flat = s . replacingOccurrences ( of : " \n " , with : " " )
if flat . count <= n {
return flat
}
return String ( flat . prefix ( n )) + "…"
}
struct TreeOptions {
var query : String ?
var depth = 80
var maxLines = 400
var frames = false
var origin = CGPoint . zero
}
2026-10-17 23:03:33 -07:00
func axTree ( _ root : AXUIElement , _ source : RefSource , _ opts : TreeOptions ) throws -> String {
var found : [ String : AXUIElement ] = [:]
2026-10-17 22:43:38 -07:00
var lines : [ String ] = []
var visits = 0
var truncated = false
let q = opts . query ?. lowercased ()
2026-10-17 23:03:33 -07:00
func walk ( _ e : AXUIElement , _ depth : Int , _ indent : Int , _ parentParts : [ String ]) throws {
2026-10-17 22:43:38 -07:00
visits += 1
if visits > maxVisits || lines . count >= opts . maxLines {
truncated = true
return
}
2026-10-17 23:03:33 -07:00
let role = try axRole ( e )
2026-10-17 22:43:38 -07:00
var parts : [ String ] = []
for name in [ kAXTitleAttribute , kAXDescriptionAttribute , kAXValueAttribute , kAXPlaceholderValueAttribute ] {
2026-10-18 08:43:52 -07:00
let s = try text ( e , name )
2026-10-17 22:43:38 -07:00
if ! s . isEmpty && ! parts . contains ( s ) {
parts . append ( s )
}
}
var names : CFArray ?
2026-10-18 08:43:52 -07:00
if AXUIElementCopyActionNames ( e , & names ) == . cannotComplete {
throw notAnswering
}
2026-10-17 22:43:38 -07:00
var acts = (( names as ? [ String ]) ?? []). compactMap { shownActions [ $0 ] }
var settable : DarwinBoolean = false
2026-10-18 08:43:52 -07:00
if AXUIElementIsAttributeSettable ( e , kAXValueAttribute as CFString , & settable ) == . cannotComplete {
throw notAnswering
}
2026-10-17 22:43:38 -07:00
if settable . boolValue {
acts . append ( "set" )
}
2026-10-18 08:43:52 -07:00
if ( try attr ( e , kAXFocusedAttribute ) as ? Bool ) == true {
2026-10-17 22:43:38 -07:00
acts . append ( "focused" )
}
2026-10-18 08:43:52 -07:00
if ( try attr ( e , kAXEnabledAttribute ) as ? Bool ) == false {
2026-10-17 22:43:38 -07:00
acts . append ( "disabled" )
}
let echo = role == kAXStaticTextRole && acts . isEmpty && parts . allSatisfy { p in parentParts . contains { $0 . contains ( p ) } }
let shown = ( ! parts . isEmpty || ! acts . isEmpty ) && ! echo
let matches = q == nil || parts . contains { $0 . lowercased (). contains ( q !) }
var next = indent
if shown && matches {
2026-10-17 23:03:33 -07:00
let ref = newRef ()
found [ ref ] = e
2026-10-17 22:43:38 -07:00
var line = String ( repeating : " " , count : q == nil ? indent : 0 ) + ref + " " + role . replacingOccurrences ( of : "AX" , with : "" ). lowercased ()
for p in parts {
line += " " + clip ( p , 200 ). debugDescription
}
if ! acts . isEmpty {
line += " [" + acts . joined ( separator : " " ) + "]"
}
2026-10-18 08:43:52 -07:00
if opts . frames , let f = try frame ( e ) {
2026-10-17 22:43:38 -07:00
line += " @ \( Int ( f . minX - opts . origin . x )) , \( Int ( f . minY - opts . origin . y )) \( Int ( f . width )) x \( Int ( f . height )) "
}
lines . append ( line )
next = indent + 1
}
if depth >= opts . depth {
return
}
2026-10-18 08:43:52 -07:00
for c in try children ( e ) {
2026-10-17 23:03:33 -07:00
try walk ( c , depth + 1 , next , parts . isEmpty ? parentParts : parts )
2026-10-17 22:43:38 -07:00
}
}
2026-10-17 23:03:33 -07:00
try walk ( root , 0 , 0 , [])
refsLock . withLock {
latestRefs = RefTable ( elements : found , source : source )
}
2026-10-17 22:43:38 -07:00
if truncated {
lines . append ( "… truncated at \( lines . count ) lines; narrow it with query or depth, or raise max_lines" )
}
return lines . joined ( separator : " \n " )
}
func treeOptions ( _ args : [ String : Any ], origin : CGPoint ) -> TreeOptions {
var o = TreeOptions ()
o . query = args . string ( "query" )
o . depth = args . int ( "depth" ) ?? o . depth
o . maxLines = args . int ( "max_lines" ) ?? o . maxLines
o . frames = args . bool ( "frames" )
o . origin = origin
return o
}
func axTreeTool ( _ args : [ String : Any ]) async throws -> [ Content ] {
if let pid = args . int ( "menu_bar_pid" ) {
2026-10-17 23:03:33 -07:00
try requireTrust ()
guard let p = pid_t ( exactly : pid ), p > 0 else {
throw ToolError ( "process \( pid ) has no menu bar" )
}
let app = appElement ( p )
_ = try axRole ( app )
2026-10-18 08:43:52 -07:00
guard let bar = try attr ( app , kAXMenuBarAttribute ) else {
2026-10-17 22:43:38 -07:00
throw ToolError ( "process \( pid ) has no menu bar" )
}
2026-10-17 23:03:33 -07:00
return [. text ( try axTree ( bar as ! AXUIElement , RefSource ( pid : p , window : nil ), treeOptions ( args , origin : . zero )))]
2026-10-17 22:43:38 -07:00
}
let id = try args . require ( "window" , args . int )
let ( info , w ) = try axWindow ( id )
2026-10-17 23:03:33 -07:00
return [. text ( try axTree ( w , RefSource ( pid : info . pid , window : id ), treeOptions ( args , origin : info . frame . origin )))]
2026-10-17 22:43:38 -07:00
}
2026-10-17 23:11:31 -07:00
func windowMatches ( _ root : AXUIElement , _ needle : String ) throws -> Int {
2026-10-17 22:43:38 -07:00
let n = needle . lowercased ()
var visits = 0
2026-10-17 23:11:31 -07:00
var count = 0
func walk ( _ e : AXUIElement ) throws {
2026-10-17 22:43:38 -07:00
visits += 1
if visits > maxVisits {
2026-10-17 23:11:31 -07:00
return
2026-10-17 22:43:38 -07:00
}
2026-10-17 23:03:33 -07:00
_ = try axRole ( e )
2026-10-18 08:43:52 -07:00
if try [ kAXTitleAttribute , kAXDescriptionAttribute , kAXValueAttribute ]. contains ( where : { try text ( e , $0 ). lowercased (). contains ( n ) }) {
2026-10-17 23:11:31 -07:00
count += 1
2026-10-17 22:43:38 -07:00
}
2026-10-18 08:43:52 -07:00
for c in try children ( e ) {
2026-10-17 23:11:31 -07:00
try walk ( c )
2026-10-17 23:03:33 -07:00
}
2026-10-17 22:43:38 -07:00
}
2026-10-17 23:11:31 -07:00
try walk ( root )
return count
2026-10-17 22:43:38 -07:00
}