Skip to content

Commit f333745

Browse files
committed
phase 1, tracking active Xcode and other app PIDs
1 parent 8255f9a commit f333745

3 files changed

Lines changed: 253 additions & 9 deletions

File tree

AS2/AS2/ContentView.swift

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,56 @@
11
import SwiftUI
22

33
struct ContentView: View {
4+
@StateObject private var xcodeMonitor = XcodeMonitor()
5+
46
var body: some View {
5-
VStack {
6-
Image(systemName: "globe")
7-
.imageScale(.large)
8-
.foregroundStyle(.tint)
9-
Text("Hello, world!")
7+
VStack(spacing: 20) {
8+
Text("🔍 Xcode Monitor")
9+
.font(.title)
10+
11+
Group {
12+
Text("Xcode Instances: \(xcodeMonitor.xcodeInstances.count)")
13+
14+
HStack {
15+
Text("Accessibility:")
16+
Text(xcodeMonitor.accessibilityPermissionGranted ? "✅ Granted" : "❌ Not Granted")
17+
.foregroundColor(xcodeMonitor.accessibilityPermissionGranted ? .green : .red)
18+
}
19+
20+
if let activeXcode = xcodeMonitor.activeXcode {
21+
Text("✅ Active Xcode: PID \(activeXcode.processIdentifier)")
22+
.foregroundColor(.green)
23+
} else if let lastActive = xcodeMonitor.lastActiveXcode {
24+
Text("🔄 Last Active: PID \(lastActive.processIdentifier)")
25+
.foregroundColor(.orange)
26+
} else {
27+
Text("❌ No Xcode Found")
28+
.foregroundColor(.red)
29+
}
30+
31+
Button("🔄 Refresh State") {
32+
xcodeMonitor.refreshActiveState()
33+
}
34+
.buttonStyle(.bordered)
35+
}
36+
.font(.headline)
37+
38+
if !xcodeMonitor.xcodeInstances.isEmpty {
39+
Text("All Xcode Instances:")
40+
.font(.subheadline)
41+
42+
ForEach(xcodeMonitor.xcodeInstances, id: \.processIdentifier) { xcode in
43+
HStack {
44+
Text("PID: \(xcode.processIdentifier)")
45+
if xcodeMonitor.activeXcode?.processIdentifier == xcode.processIdentifier {
46+
Text("🟢 Active")
47+
} else {
48+
Text("⚪ Background")
49+
}
50+
}
51+
.font(.caption)
52+
}
53+
}
1054
}
1155
.padding()
1256
}

AS2/AS2/XcodeMonitor.swift

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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+
}

AS2/docs/XCODE_MONITORING_PLAN.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,22 @@ Following the proven architecture of CopilotForXcode, this document outlines the
194194

195195
---
196196

197-
## 🎯 **Current Status**: Phase 1 Preparation
198-
199-
**Completed**: Basic NSWorkspace monitoring
200-
**Next**: Remove sandbox and implement AX foundation
197+
## 🎯 **Current Status**: Phase 1 Complete → Starting Phase 2
198+
199+
### **✅ Phase 1 COMPLETED**:
200+
- ✅ Removed App Sandbox (`com.apple.security.app-sandbox = false`)
201+
- ✅ Added Accessibility Foundation (ApplicationServices framework)
202+
- ✅ Implemented accessibility permission handling with auto-prompt
203+
- ✅ Fixed Xcode detection using `workspace.frontmostApplication`
204+
- ✅ Added basic AXUIElement creation and testing
205+
- ✅ Robust active/inactive state tracking with periodic correction
206+
- ✅ Thread-safe UI updates with proper MainActor usage
207+
208+
### **🚀 Phase 2 NEXT**: Window & File Path Detection
209+
**Ready to implement**:
210+
1. Monitor focused Xcode windows using AX notifications
211+
2. Extract file paths from window elements (`windowElement.document`)
212+
3. Extract workspace paths from window children
213+
4. Real-time updates when switching files/projects
201214

202215
This plan ensures we build robust, maintainable Xcode monitoring following proven patterns while allowing for incremental development and testing.

0 commit comments

Comments
 (0)