1+ import Cocoa
2+ import Foundation
3+ import ApplicationServices
4+
5+ /// Minimal Xcode monitoring - starts with basic application detection
6+ class XcodeMonitor : ObservableObject {
7+ @Published var xcodeInstances : [ NSRunningApplication ] = [ ]
8+ @Published var activeXcode : NSRunningApplication ?
9+ @Published var lastActiveXcode : NSRunningApplication ?
10+ @Published var accessibilityPermissionGranted : Bool = false
11+
12+ private let workspace = NSWorkspace . shared
13+
14+ init ( ) {
15+ checkAccessibilityPermission ( )
16+ setupMonitoring ( )
17+ findExistingXcodeInstances ( )
18+ startPeriodicCheck ( )
19+ }
20+
21+ private func startPeriodicCheck( ) {
22+ // Check every 2 seconds to ensure state is accurate
23+ Timer . scheduledTimer ( withTimeInterval: 2.0 , repeats: true ) { [ weak self] _ in
24+ Task { @MainActor in
25+ self ? . refreshActiveState ( )
26+ }
27+ }
28+ }
29+
30+ @MainActor
31+ public func refreshActiveState( ) {
32+ let frontmostApp = workspace. frontmostApplication
33+ let currentlyActivePID = frontmostApp? . processIdentifier ?? - 1
34+
35+ print ( " 🔄 Periodic check - Frontmost: \( frontmostApp? . localizedName ?? " None " ) (PID: \( currentlyActivePID) ) " )
36+
37+ let shouldBeActiveXcode = xcodeInstances. first { xcode in
38+ xcode. processIdentifier == currentlyActivePID
39+ }
40+
41+ if shouldBeActiveXcode? . processIdentifier != activeXcode? . processIdentifier {
42+ print ( " 🔄 State correction needed! " )
43+ print ( " - Should be active: \( shouldBeActiveXcode? . processIdentifier ?? - 1 ) " )
44+ print ( " - Currently tracked as active: \( activeXcode? . processIdentifier ?? - 1 ) " )
45+
46+ activeXcode = shouldBeActiveXcode
47+ if let newActive = shouldBeActiveXcode {
48+ lastActiveXcode = newActive
49+ }
50+ }
51+ }
52+
53+ private func setupMonitoring( ) {
54+ // Monitor for app activation
55+ NotificationCenter . default. addObserver (
56+ forName: NSWorkspace . didActivateApplicationNotification,
57+ object: nil ,
58+ queue: . main
59+ ) { [ weak self] notification in
60+ self ? . handleApplicationActivated ( notification)
61+ }
62+
63+ // Monitor for app termination
64+ NotificationCenter . default. addObserver (
65+ forName: NSWorkspace . didTerminateApplicationNotification,
66+ object: nil ,
67+ queue: . main
68+ ) { [ weak self] notification in
69+ self ? . handleApplicationTerminated ( notification)
70+ }
71+ }
72+
73+ private func checkAccessibilityPermission( ) {
74+ accessibilityPermissionGranted = AXIsProcessTrusted ( )
75+ print ( " 🔐 Accessibility Permission: \( accessibilityPermissionGranted ? " ✅ Granted " : " ❌ Not Granted " ) " )
76+
77+ if !accessibilityPermissionGranted {
78+ print ( " 💡 Request accessibility permission... " )
79+ requestAccessibilityPermission ( )
80+ }
81+ }
82+
83+ private func requestAccessibilityPermission( ) {
84+ let options = [ kAXTrustedCheckOptionPrompt. takeUnretainedValue ( ) : true ]
85+ AXIsProcessTrustedWithOptions ( options as CFDictionary )
86+ }
87+
88+ private func findExistingXcodeInstances( ) {
89+ xcodeInstances = workspace. runningApplications. filter { $0. isXcode }
90+
91+ // Check what's actually frontmost right now
92+ let frontmostApp = workspace. frontmostApplication
93+ print ( " 🔍 Current frontmost app: \( frontmostApp? . localizedName ?? " Unknown " ) (PID: \( frontmostApp? . processIdentifier ?? - 1 ) ) " )
94+
95+ // Find active Xcode - only if Xcode is actually frontmost
96+ activeXcode = xcodeInstances. first { xcode in
97+ xcode. processIdentifier == frontmostApp? . processIdentifier
98+ }
99+
100+ // If no active Xcode but we have instances, remember the first one as "last active"
101+ if activeXcode == nil && !xcodeInstances. isEmpty {
102+ lastActiveXcode = lastActiveXcode ?? xcodeInstances. first
103+ } else if let active = activeXcode {
104+ lastActiveXcode = active
105+ }
106+
107+ print ( " 📱 Found \( xcodeInstances. count) Xcode instances " )
108+ if let active = activeXcode {
109+ print ( " ✅ Active Xcode: PID \( active. processIdentifier) " )
110+ } else if let lastActive = lastActiveXcode {
111+ print ( " 🔄 Last Active Xcode: PID \( lastActive. processIdentifier) " )
112+ }
113+
114+ // Test AX access for first Xcode if we have permission
115+ if accessibilityPermissionGranted && !xcodeInstances. isEmpty {
116+ testAccessibilityAccess ( )
117+ }
118+ }
119+
120+ private func testAccessibilityAccess( ) {
121+ guard let xcode = xcodeInstances. first else { return }
122+
123+ let axApp = AXUIElementCreateApplication ( xcode. processIdentifier)
124+
125+ // Test basic AX access
126+ var focusedElement : AnyObject ?
127+ let result = AXUIElementCopyAttributeValue ( axApp, kAXFocusedUIElementAttribute as CFString , & focusedElement)
128+
129+ switch result {
130+ case . success:
131+ print ( " 🎯 AX Access Test: ✅ Success - can read focused element " )
132+ case . apiDisabled:
133+ print ( " 🚫 AX Access Test: API Disabled " )
134+ case . notImplemented:
135+ print ( " ⚠️ AX Access Test: Not Implemented " )
136+ default :
137+ print ( " ❓ AX Access Test: Other error - \( result. rawValue) " )
138+ }
139+ }
140+
141+ private func handleApplicationActivated( _ notification: Notification ) {
142+ guard let app = notification. userInfo ? [ NSWorkspace . applicationUserInfoKey] as? NSRunningApplication else { return }
143+
144+ print ( " 📱 App activated: \( app. localizedName ?? " Unknown " ) (PID: \( app. processIdentifier) ) " )
145+
146+ // Ensure UI updates happen on main thread
147+ DispatchQueue . main. async { [ weak self] in
148+ guard let self = self else { return }
149+
150+ if app. isXcode {
151+ print ( " 🔄 Xcode activated: PID \( app. processIdentifier) " )
152+ self . activeXcode = app
153+ self . lastActiveXcode = app
154+
155+ if !self . xcodeInstances. contains ( where: { $0. processIdentifier == app. processIdentifier } ) {
156+ self . xcodeInstances. append ( app)
157+ print ( " 📱 Added new Xcode instance: PID \( app. processIdentifier) " )
158+ }
159+ } else {
160+ // When another app becomes active, Xcode is no longer active
161+ if let previousActive = self . activeXcode {
162+ print ( " 📱 \( app. localizedName ?? " Unknown app " ) became active, Xcode (PID: \( previousActive. processIdentifier) ) backgrounded " )
163+ self . activeXcode = nil
164+ }
165+ }
166+ }
167+ }
168+
169+ private func handleApplicationTerminated( _ notification: Notification ) {
170+ guard let app = notification. userInfo ? [ NSWorkspace . applicationUserInfoKey] as? NSRunningApplication else { return }
171+
172+ if app. isXcode {
173+ print ( " ❌ Xcode terminated: PID \( app. processIdentifier) " )
174+ xcodeInstances. removeAll { $0. processIdentifier == app. processIdentifier }
175+
176+ if activeXcode? . processIdentifier == app. processIdentifier {
177+ activeXcode = xcodeInstances. first ( where: \. isActive)
178+ }
179+ }
180+ }
181+ }
182+
183+ extension NSRunningApplication {
184+ var isXcode : Bool {
185+ bundleIdentifier == " com.apple.dt.Xcode "
186+ }
187+ }
0 commit comments