From 93bc39724e793bbca252b6eb1a8a4b458ae79dc3 Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Sun, 3 Aug 2025 17:14:09 -0700 Subject: [PATCH 01/13] add my comments --- .../BuiltinExtensionWorkspacePlugin.swift | 2 ++ .../GitHubCopilotExtension.swift | 1 + .../LanguageServer/GitHubCopilotService.swift | 1 + Tool/Sources/XcodeInspector/XcodeInspector.swift | 13 +++++++++++++ 4 files changed, 17 insertions(+) diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtensionWorkspacePlugin.swift b/Tool/Sources/BuiltinExtension/BuiltinExtensionWorkspacePlugin.swift index a03c34d1..c3c6ff5d 100644 --- a/Tool/Sources/BuiltinExtension/BuiltinExtensionWorkspacePlugin.swift +++ b/Tool/Sources/BuiltinExtension/BuiltinExtensionWorkspacePlugin.swift @@ -9,6 +9,7 @@ public final class BuiltinExtensionWorkspacePlugin: WorkspacePlugin { super.init(workspace: workspace) } + // this calls Copilot notifyOpenTextDocument override public func didOpenFilespace(_ filespace: Filespace) { notifyOpenFile(filespace: filespace) } @@ -32,6 +33,7 @@ public final class BuiltinExtensionWorkspacePlugin: WorkspacePlugin { } } + // this calls Copilot notifyOpenTextDocument public func notifyOpenFile(filespace: Filespace) { Task { guard filespace.isTextReadable else { return } diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift index 119278ee..29f90b61 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift @@ -42,6 +42,7 @@ public final class GitHubCopilotExtension: BuiltinExtension { public func workspaceDidClose(_: WorkspaceInfo) {} + // this calls Copilot notifyOpenTextDocument public func workspace(_ workspace: WorkspaceInfo, didOpenDocumentAt documentURL: URL) { guard isLanguageServerInUse else { return } // check if file size is larger than 15MB, if so, return immediately diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 4ea5de5c..7c46f1c0 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -36,6 +36,7 @@ public protocol GitHubCopilotSuggestionServiceType { func notifyShown(_ completion: CodeSuggestion) async func notifyAccepted(_ completion: CodeSuggestion, acceptedLength: Int?) async func notifyRejected(_ completions: [CodeSuggestion]) async + // tells Copilot LSP that a file has been opened func notifyOpenTextDocument(fileURL: URL, content: String) async throws func notifyChangeTextDocument(fileURL: URL, content: String, version: Int) async throws func notifyCloseTextDocument(fileURL: URL) async throws diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 2b2ea1e8..f513b932 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -140,9 +140,11 @@ public final class XcodeInspector: ObservableObject { latestNonRootWorkspaceURL = nil } + // finds running Xcode instances let runningApplications = NSWorkspace.shared.runningApplications xcodes = runningApplications .filter { $0.isXcode } + // creates a XcodeAppInstanceInspector for each Xcode instance .map(XcodeAppInstanceInspector.init(runningApplication:)) let activeXcode = xcodes.first(where: \.isActive) latestActiveXcode = activeXcode ?? xcodes.first @@ -160,6 +162,7 @@ public final class XcodeInspector: ObservableObject { } await withThrowingTaskGroup(of: Void.self) { [weak self] group in + // activation group.addTask { [weak self] in // Did activate app let sequence = NSWorkspace.shared.notificationCenter .notifications(named: NSWorkspace.didActivateApplicationNotification) @@ -193,6 +196,7 @@ public final class XcodeInspector: ObservableObject { } } + // termination group.addTask { [weak self] in // Did terminate app let sequence = NSWorkspace.shared.notificationCenter .notifications(named: NSWorkspace.didTerminateApplicationNotification) @@ -242,6 +246,7 @@ public final class XcodeInspector: ObservableObject { } } + // accessibility API issues group.addTask { [weak self] in // malfunctioning let sequence = NotificationCenter.default .notifications(named: .accessibilityAPIMalfunctioning) @@ -267,6 +272,10 @@ public final class XcodeInspector: ObservableObject { } } + // Get the focused UI element + // Determine if it's a source editor + // Monitor for focus changes + @XcodeInspectorActor private func setActiveXcode(_ xcode: XcodeAppInstanceInspector) { previousActiveApplication = activeApplication @@ -279,9 +288,12 @@ public final class XcodeInspector: ObservableObject { activeXcode = xcode latestActiveXcode = xcode + // active document activeDocumentURL = xcode.documentURL + // focused window focusedWindow = xcode.focusedWindow completionPanel = xcode.completionPanel + // project root activeProjectRootURL = xcode.projectRootURL activeWorkspaceURL = xcode.workspaceURL focusedWindow = xcode.focusedWindow @@ -304,6 +316,7 @@ public final class XcodeInspector: ObservableObject { } focusedElement = getFocusedElementAndRecordStatus(xcode.appElement) + // focused editor if let editorElement = focusedElement, editorElement.isSourceEditor { focusedEditor = .init( runningApplication: xcode.runningApplication, From b9fedb9c3726deee3293c673cab255bdf478e59c Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Mon, 4 Aug 2025 15:36:26 -0700 Subject: [PATCH 02/13] add CLAUDE.md --- CLAUDE.md | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..6ffadcce --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,108 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +GitHub Copilot for Xcode is a macOS application that provides AI-powered code completion and chat assistance for Xcode. It integrates GitHub Copilot's capabilities directly into the Xcode development environment through Xcode source editor extensions. + +## Build Commands + +### Local Development Build +```bash +cd ./Script +sh ./uninstall-app.sh # Remove any previous installation +del ../build # Clean the build directory (use del instead of rm) +sh ./localbuild-app.sh # Build a fresh copy of the app +``` + +### Server Components (Node.js/TypeScript) +```bash +cd Server +npm install # Install dependencies +npm run build # Build with webpack +``` + +### Testing +```bash +# Run all unit tests (from Xcode or command line) +xcodebuild test -scheme "Copilot for Xcode" -workspace "Copilot for Xcode.xcworkspace" + +# Tests are configured in TestPlan.xctestplan +# All new tests should be added to this test plan +``` + +### Code Formatting +- Uses SwiftFormat for Swift code formatting +- Follows Ray Wenderlich Style Guide with 4 spaces for indentation + +## Architecture Overview + +The project is organized into several key targets and packages: + +### Main Targets +- **Copilot for Xcode**: Host app containing XPCService and editor extension, provides settings UI +- **EditorExtension**: Xcode source editor extension that forwards editor content to XPCService +- **ExtensionService**: Background service where all core features are implemented +- **CommunicationBridge**: Maintains communication between host app/editor extension and ExtensionService + +### Swift Packages +- **Core**: Contains main application logic organized by feature areas + - `Service`: ExtensionService implementation + - `HostApp`: Host application implementation + - `SuggestionWidget`: UI components for code suggestions + - `ConversationTab`: Chat interface components + - `ChatService`: Chat functionality and context management + - `SuggestionService`: Code completion service + - `PromptToCodeService`: Code generation from prompts + +- **Tool**: Shared utilities and lower-level services + - `GitHubCopilotService`: Core integration with GitHub Copilot Language Server + - `Workspace`: File system and project management + - `XcodeInspector`: Xcode app monitoring and interaction + - `SuggestionBasic`: Core suggestion data types + - `Preferences`: Configuration management + - `Logger`: Logging infrastructure + +- **Server**: Node.js/TypeScript components + - Monaco editor integration for diff views + - Terminal/xterm integration + - Webpack-based build system + +### Key Service Architecture +- Uses actor-based concurrency with `@WorkspaceActor` and `@ServiceActor` +- Dependency injection via swift-dependencies +- Composable Architecture (TCA) for UI state management +- XPC communication between sandboxed and non-sandboxed components + +### Data Flow +1. Editor extension captures Xcode content via source editor APIs +2. Content forwarded to ExtensionService via CommunicationBridge XPC +3. ExtensionService processes requests through GitHubCopilotService +4. Language Server Protocol (LSP) communication with GitHub Copilot backend +5. Results returned through same XPC chain back to editor + +## Prerequisites + +- macOS 12+ +- Xcode 8+ +- Node.js and npm (symlinked to /usr/local/bin for Xcode run scripts) +- GitHub Copilot subscription + +## Key Files to Understand + +- `Core/Sources/Service/Service.swift`: Main service entry point +- `Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift`: Core Copilot integration +- `Core/Package.swift` & `Tool/Package.swift`: Swift package configurations +- `TestPlan.xctestplan`: Centralized test configuration +- `Version.xcconfig`: Version control for all targets +- `DEVELOPMENT.md`: Detailed development setup and architecture notes + +## Common Development Patterns + +- All async operations use Swift concurrency (async/await) +- UI built with SwiftUI using Composable Architecture patterns +- Preference storage via UserDefaults with type-safe property wrappers +- Logging via centralized Logger package with different log levels +- File watching and workspace management through dedicated services +- XPC communication follows request-response patterns with proper error handling \ No newline at end of file From 2c6f61d018d1e671a9442f278b270335955a6007 Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Sun, 3 Aug 2025 19:11:39 -0700 Subject: [PATCH 03/13] add docs/XCODE_STATUS_MONITORING.md --- Docs/XCODE_STATUS_MONITORING.md | 189 ++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 Docs/XCODE_STATUS_MONITORING.md diff --git a/Docs/XCODE_STATUS_MONITORING.md b/Docs/XCODE_STATUS_MONITORING.md new file mode 100644 index 00000000..98032a62 --- /dev/null +++ b/Docs/XCODE_STATUS_MONITORING.md @@ -0,0 +1,189 @@ +# Copilot for Xcode: Complete Xcode Status Reading Architecture Map + +Based on a thorough analysis of the codebase, here's the comprehensive map of how this project reads Xcode status: + +## 🏗️ **Core Architecture Overview** + +The project uses a **multi-layered approach** combining: +1. **macOS Accessibility API** (primary method) +2. **Xcode Editor Extensions** (direct integration) +3. **XPC Inter-Process Communication** (service coordination) +4. **NSWorkspace monitoring** (application state tracking) + +--- + +## 📊 **Data Flow & Components** + +### **1. Application State Monitoring** +**Location**: `Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift` +- **Purpose**: Tracks which applications are active/running +- **Key Data Captured**: + - Active application status (`isActive`, `isXcode`) + - Process identifiers + - Application lifecycle events (launch/terminate) +- **Real-time Updates**: Uses `NSWorkspace.didActivateApplicationNotification` + +### **2. Accessibility-Based Xcode Monitoring** +**Primary Components**: + +**A. AXUIElement Extensions** (`Tool/Sources/AXExtension/AXUIElement.swift:1-330`) +- **UI Element Properties**: + - `selectedTextRange` (current cursor/selection) + - `value` (editor content) + - `isFocused`, `isSourceEditor` (element state) + - `document`, `title`, `role` (element metadata) +- **Element Hierarchy Navigation**: + - `parent`, `children`, `focusedElement` + - `window`, `focusedWindow` access +- **Xcode-Specific Detection**: + - `isXcodeWorkspaceWindow` + - `isEditorArea`, `isSourceEditor` + +**B. AX Notification Streaming** (`Tool/Sources/AXNotificationStream/AXNotificationStream.swift:8-170`) +- **Real-time Event Monitoring**: + - `kAXFocusedUIElementChangedNotification` + - `kAXTitleChangedNotification` + - `kAXWindowMovedNotification`/`kAXWindowResizedNotification` + - `kAXUIElementDestroyedNotification` +- **Performance Features**: + - Configurable run loop modes + - Automatic retry with backoff + - Accessibility permission detection + +**C. AX Helper Utilities** (`Tool/Sources/AXHelper/AXHelper.swift:5-70`) +- **Code Injection**: Direct content manipulation via accessibility API +- **Cursor Management**: Selection range preservation/restoration +- **Scroll Position**: Viewport state maintenance + +### **3. Centralized Xcode Inspector** +**Location**: `Tool/Sources/XcodeInspector/XcodeInspector.swift:23-432` + +**A. Published State Properties**: +```swift +@Published public var activeProjectRootURL: URL? +@Published public var activeDocumentURL: URL? +@Published public var activeWorkspaceURL: URL? +@Published public var focusedWindow: XcodeWindowInspector? +@Published public var focusedEditor: SourceEditor? +@Published public var focusedElement: AXUIElement? +@Published public var completionPanel: AXUIElement? +``` + +**B. Real-time vs Cached Data**: +- **Cached**: `activeDocumentURL`, `activeWorkspaceURL` +- **Real-time**: `realtimeActiveDocumentURL`, `realtimeActiveWorkspaceURL` +- **Source**: Real-time data extracted directly from window titles/elements + +**C. Self-Healing Mechanisms**: +- **Malfunction Detection**: Monitors for accessibility API corruption +- **Auto-Recovery**: Automatic restart when inconsistencies detected +- **Debounced Validation**: Prevents excessive restart attempts + +### **4. Xcode App Instance Monitoring** +**Location**: `Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift:8-200+` + +**A. Window-Level Inspection**: +- **Workspace Window Detection**: `identifier == "Xcode.WorkspaceWindow"` +- **Document URL Extraction**: From window title parsing +- **Project Structure Analysis**: Workspace vs project distinction + +**B. Real-time Data Extraction**: +```swift +public var realtimeDocumentURL: URL? // From focused window +public var realtimeWorkspaceURL: URL? // From window title +public var realtimeProjectURL: URL? // Derived from workspace/document +``` + +**C. Notification Handling**: +- **AX Event Processing**: 13 different notification types +- **Async Stream**: `AsyncPassthroughSubject` +- **Window Focus Tracking**: Automatic inspector switching + +### **5. Editor Extension Integration** +**Location**: `EditorExtension/SourceEditorExtension.swift:11-91` + +**A. Direct Xcode Integration**: +- **XCSourceEditorExtension**: Official Xcode extension API +- **Command Registration**: Built-in commands for suggestions/chat +- **XPC Service Wake-up**: Automatic service initialization + +**B. Available Commands**: +- Suggestion controls (accept/reject/navigate) +- Settings access +- Chat integration +- Real-time suggestion toggling + +### **6. XPC Communication Layer** +**Location**: `ExtensionService/XPCController.swift:5-87` + +**A. Service Coordination**: +- **Anonymous XPC Listener**: Cross-process communication +- **Bridge Management**: Connection lifecycle handling +- **Ping Mechanism**: Service health monitoring + +**B. Data Exposure**: +- **Inspector Data API**: `getXcodeInspectorData()` +- **State Serialization**: JSON encoding of current Xcode state +- **Background Permission Handling**: Automated permission requests + +--- + +## 🔄 **Real-time Data Extraction Flow** + +``` +1. NSWorkspace → Application Launch/Focus Detection +2. AXNotificationStream → UI Change Events +3. XcodeInspector → State Aggregation & Publishing +4. XcodeAppInstanceInspector → Window-Specific Analysis +5. AXUIElement Extensions → Element Property Reading +6. XPC Service → Data Exposure to Extensions +``` + +## 📍 **Key Data Points Tracked** + +| Data Point | Source | Frequency | Method | +|------------|--------|-----------|---------| +| **Active File** | Window title parsing | Real-time | `realtimeDocumentURL` | +| **Workspace** | Window title/AX tree | Real-time | `realtimeWorkspaceURL` | +| **Cursor Position** | Focused element | Real-time | `selectedTextRange` | +| **Editor Content** | AX value attribute | On-demand | `focusedEditor.getContent()` | +| **UI State** | AX notifications | Event-driven | `focusedElement`, `completionPanel` | +| **Project Root** | Workspace analysis | Cached | `activeProjectRootURL` | + +## 🛡️ **Robustness Features** + +- **Permission Monitoring**: Continuous accessibility permission validation +- **Malfunction Detection**: Element consistency checking +- **Auto-Recovery**: Intelligent restart mechanisms +- **Backoff Strategies**: Exponential retry delays +- **Thread Safety**: Global actor isolation (`@XcodeInspectorActor`) + +## 🔧 **Key Implementation Details** + +### Accessibility API Usage +The project heavily relies on macOS Accessibility APIs to monitor Xcode's UI state: +- Uses `AXUIElementCreateApplication()` to get app-level access +- Monitors specific notification types for real-time updates +- Implements robust error handling for permission issues +- Provides fallback mechanisms when accessibility API fails + +### Window Title Parsing +Real-time file and workspace detection happens through: +- Parsing Xcode workspace window titles +- Extracting file paths from window identifiers +- Distinguishing between workspace and project contexts +- Handling edge cases like unsaved files and temporary documents + +### Performance Optimizations +- **Debounced Updates**: Prevents excessive state changes +- **Selective Monitoring**: Only tracks relevant UI elements +- **Async Processing**: Non-blocking state updates +- **Memory Management**: Proper cleanup of observers and tasks + +### Error Recovery +- **Automatic Restart**: When accessibility API becomes corrupted +- **Exponential Backoff**: For failed connection attempts +- **State Validation**: Continuous consistency checking +- **Graceful Degradation**: Fallback to cached data when real-time fails + +This comprehensive architecture enables the Copilot for Xcode extension to maintain accurate, real-time awareness of Xcode's state while providing robust error handling and performance optimization. \ No newline at end of file From e45a0ff3ed9d549aeb5f6087f281a13f3c0956c0 Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Mon, 4 Aug 2025 20:25:00 -0700 Subject: [PATCH 04/13] add docs/XCODE_INTEGRATION_COMPONENTS.md --- Docs/XCODE_INTEGRATION_COMPONENTS.md | 163 +++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 Docs/XCODE_INTEGRATION_COMPONENTS.md diff --git a/Docs/XCODE_INTEGRATION_COMPONENTS.md b/Docs/XCODE_INTEGRATION_COMPONENTS.md new file mode 100644 index 00000000..f62ccb4a --- /dev/null +++ b/Docs/XCODE_INTEGRATION_COMPONENTS.md @@ -0,0 +1,163 @@ +# Xcode Integration Components Guide + +This guide breaks down which components in the CopilotForXcode project are needed for Xcode interaction versus Copilot-specific features. Use this when building a parallel app that needs to monitor and communicate with Xcode without Copilot functionality. + +## **Essential Xcode Monitoring Components** + +### Core Xcode State Monitoring +These components provide the fundamental ability to monitor Xcode's state in real-time: + +#### **`Tool/Sources/XcodeInspector/`** - Central Coordinator +- **`XcodeInspector.swift`** - Main coordinator that publishes Xcode state +- **`Apps/XcodeAppInstanceInspector.swift`** - Xcode-specific window and document tracking +- **`XcodeWindowInspector.swift`** - Window-level inspection +- **`SourceEditor.swift`** - Editor content and cursor management + +**Published State Properties:** +```swift +@Published public var activeDocumentURL: URL? +@Published public var activeWorkspaceURL: URL? +@Published public var focusedWindow: XcodeWindowInspector? +@Published public var focusedEditor: SourceEditor? +@Published public var focusedElement: AXUIElement? +``` + +#### **`Tool/Sources/ActiveApplicationMonitor/`** - App State Tracking +- **`ActiveApplicationMonitor.swift`** - Tracks which applications are active/running +- Uses `NSWorkspace.didActivateApplicationNotification` for real-time updates +- Provides: `isActive`, `isXcode`, process identifiers, lifecycle events + +#### **`Tool/Sources/AXExtension/`** - Accessibility API Interface +- **`AXUIElement.swift`** - Core accessibility API extensions +- Provides UI element access: `selectedTextRange`, `value`, `isFocused`, `isSourceEditor` +- Element navigation: `parent`, `children`, `focusedElement`, `window` +- Xcode detection: `isXcodeWorkspaceWindow`, `isEditorArea` + +#### **`Tool/Sources/AXNotificationStream/`** - Real-time Events +- **`AXNotificationStream.swift`** - Real-time accessibility event monitoring +- Monitors: Focus changes, title changes, window moves/resizes, element destruction +- Features: Configurable run loops, retry with backoff, permission detection + +#### **`Tool/Sources/AXHelper/`** - Accessibility Utilities +- **`AXHelper.swift`** - Higher-level accessibility operations +- Code injection, cursor management, scroll position handling + +### Communication Infrastructure (if needed) + +#### **`Tool/Sources/XPCShared/`** - XPC Communication +- **`XPCServiceProtocol.swift`** - Service protocols +- **`XcodeInspectorData.swift`** - Xcode state data types +- **`Models.swift`** - Shared data models + +#### **`ExtensionService/XPCController.swift`** - Service Coordination +- Anonymous XPC listener for cross-process communication +- Bridge management and connection lifecycle +- Data exposure API: `getXcodeInspectorData()` + +#### **`EditorExtension/SourceEditorExtension.swift`** - Xcode Menu Integration +- Official Xcode source editor extension +- Command registration for custom menu items +- XPC service wake-up and communication + +### Supporting Infrastructure + +#### **`Tool/Sources/Workspace/`** - File System Management +- **`Workspace.swift`** - Project and workspace management +- **`WorkspacePool.swift`** - Multiple workspace handling +- **File watching components** - Monitor file system changes + +#### **`Tool/Sources/Preferences/`** - Configuration +- **`AppStorage.swift`** - UserDefaults with property wrappers +- **`Keys.swift`** - Preference key definitions +- **`UserDefaults.swift`** - Type-safe preference access + +#### **`Tool/Sources/Logger/`** - Logging +- **`Logger.swift`** - Centralized logging infrastructure +- **`FileLogger.swift`** - File-based logging + +#### **`Tool/Sources/UserDefaultsObserver/`** - Settings Observation +- **`UserDefaultsObserver.swift`** - Real-time settings changes + +## **Copilot-Specific Components (Not Needed)** + +### GitHub Copilot Integration +- **`Tool/Sources/GitHubCopilotService/`** - All Language Server integration +- **`Tool/Sources/BuiltinExtension/`** - Copilot extension provider +- **`Tool/Sources/SuggestionProvider/`** - Code suggestion services +- **`Tool/Sources/SuggestionBasic/`** - Suggestion data types +- **`Tool/Sources/ConversationServiceProvider/`** - Chat conversation handling +- **`Tool/Sources/TelemetryService/`** - Usage telemetry + +### UI Components for Copilot Features +- **`Core/Sources/SuggestionWidget/`** - Code suggestion UI +- **`Core/Sources/ConversationTab/`** - Chat interface +- **`Core/Sources/ChatService/`** - Chat functionality +- **`Core/Sources/SuggestionService/`** - Suggestion management +- **`Core/Sources/PromptToCodeService/`** - Code generation +- **`Tool/Sources/ChatAPIService/`** - Chat API integration +- **`Tool/Sources/ChatTab/`** - Chat tab management + +### Server Components +- **`Server/`** - All Node.js/TypeScript web components +- Monaco editor integration, terminal integration, webpack build + +## **Minimal Architecture for Xcode Monitoring** + +### Recommended Component Structure +``` +Tool/Sources/ +├── ActiveApplicationMonitor/ # App state tracking +├── AXExtension/ # Accessibility API +├── AXNotificationStream/ # Real-time events +├── AXHelper/ # AX utilities +├── XcodeInspector/ # Central coordinator +├── XPCShared/ # Communication (optional) +├── Logger/ # Logging +├── Preferences/ # Configuration +├── UserDefaultsObserver/ # Settings +└── Workspace/ # File management +``` + +### Core Data Flow Pattern +``` +1. ActiveApplicationMonitor → Detect Xcode launch/focus +2. AXNotificationStream → Real-time UI change events +3. XcodeInspector → Aggregate and publish state +4. XcodeAppInstanceInspector → Extract specific data +5. AXUIElement extensions → Low-level element access +6. XPC Service → Expose data to extensions (optional) +``` + +### Key Published Data +The `XcodeInspector` provides real-time awareness of: +- **Active File**: `activeDocumentURL: URL?` +- **Workspace**: `activeWorkspaceURL: URL?` +- **Editor State**: `focusedEditor: SourceEditor?` +- **Cursor Position**: Available through `SourceEditor.selectedTextRange` +- **Editor Content**: Available through `SourceEditor.getContent()` + +## **Implementation Notes** + +### Accessibility Permissions +- Requires macOS Accessibility permission +- Automatic permission detection and retry mechanisms +- Robust error handling for permission issues + +### Performance Optimizations +- **Debounced Updates**: Prevents excessive state changes +- **Selective Monitoring**: Only tracks relevant UI elements +- **Async Processing**: Non-blocking state updates +- **Memory Management**: Proper cleanup of observers + +### Error Recovery +- **Automatic Restart**: When accessibility API corrupts +- **Exponential Backoff**: For failed connections +- **State Validation**: Consistency checking +- **Graceful Degradation**: Fallback to cached data + +### Thread Safety +- Uses global actors (`@XcodeInspectorActor`) for thread safety +- All state updates happen on appropriate actors +- Proper async/await patterns throughout + +This architecture provides comprehensive Xcode monitoring without any Copilot dependencies, giving you real-time awareness of files, workspaces, cursor position, and editor content. \ No newline at end of file From 1df8caa36b7d57c973d40a48ac1103a5d9afd462 Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Fri, 26 Sep 2025 17:20:08 -0700 Subject: [PATCH 05/13] AS1 --- AS1/AS1.xcodeproj/project.pbxproj | 326 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + AS1/AS1/AS1.entitlements | 10 + AS1/AS1/AS1App.swift | 10 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 ++++ AS1/AS1/Assets.xcassets/Contents.json | 6 + AS1/AS1/ContentView.swift | 17 + 8 files changed, 445 insertions(+) create mode 100644 AS1/AS1.xcodeproj/project.pbxproj create mode 100644 AS1/AS1.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 AS1/AS1/AS1.entitlements create mode 100644 AS1/AS1/AS1App.swift create mode 100644 AS1/AS1/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 AS1/AS1/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 AS1/AS1/Assets.xcassets/Contents.json create mode 100644 AS1/AS1/ContentView.swift diff --git a/AS1/AS1.xcodeproj/project.pbxproj b/AS1/AS1.xcodeproj/project.pbxproj new file mode 100644 index 00000000..75c85182 --- /dev/null +++ b/AS1/AS1.xcodeproj/project.pbxproj @@ -0,0 +1,326 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + 63C4A66B2E41AB5C00467C2C /* AS1.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AS1.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 63C4A66D2E41AB5C00467C2C /* AS1 */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AS1; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 63C4A6682E41AB5C00467C2C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 63C4A6622E41AB5B00467C2C = { + isa = PBXGroup; + children = ( + 63C4A66D2E41AB5C00467C2C /* AS1 */, + 63C4A66C2E41AB5C00467C2C /* Products */, + ); + sourceTree = ""; + }; + 63C4A66C2E41AB5C00467C2C /* Products */ = { + isa = PBXGroup; + children = ( + 63C4A66B2E41AB5C00467C2C /* AS1.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 63C4A66A2E41AB5C00467C2C /* AS1 */ = { + isa = PBXNativeTarget; + buildConfigurationList = 63C4A6772E41AB5D00467C2C /* Build configuration list for PBXNativeTarget "AS1" */; + buildPhases = ( + 63C4A6672E41AB5C00467C2C /* Sources */, + 63C4A6682E41AB5C00467C2C /* Frameworks */, + 63C4A6692E41AB5C00467C2C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 63C4A66D2E41AB5C00467C2C /* AS1 */, + ); + name = AS1; + packageProductDependencies = ( + ); + productName = AS1; + productReference = 63C4A66B2E41AB5C00467C2C /* AS1.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 63C4A6632E41AB5B00467C2C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 63C4A66A2E41AB5C00467C2C = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = 63C4A6662E41AB5B00467C2C /* Build configuration list for PBXProject "AS1" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 63C4A6622E41AB5B00467C2C; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 63C4A66C2E41AB5C00467C2C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 63C4A66A2E41AB5C00467C2C /* AS1 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 63C4A6692E41AB5C00467C2C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 63C4A6672E41AB5C00467C2C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 63C4A6752E41AB5D00467C2C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 8WK25CU37Q; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 63C4A6762E41AB5D00467C2C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 8WK25CU37Q; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 63C4A6782E41AB5D00467C2C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AS1/AS1.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8WK25CU37Q; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.rzsoftware.AS1; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 63C4A6792E41AB5D00467C2C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AS1/AS1.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8WK25CU37Q; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.rzsoftware.AS1; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 63C4A6662E41AB5B00467C2C /* Build configuration list for PBXProject "AS1" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 63C4A6752E41AB5D00467C2C /* Debug */, + 63C4A6762E41AB5D00467C2C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 63C4A6772E41AB5D00467C2C /* Build configuration list for PBXNativeTarget "AS1" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 63C4A6782E41AB5D00467C2C /* Debug */, + 63C4A6792E41AB5D00467C2C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 63C4A6632E41AB5B00467C2C /* Project object */; +} diff --git a/AS1/AS1.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AS1/AS1.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/AS1/AS1.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/AS1/AS1/AS1.entitlements b/AS1/AS1/AS1.entitlements new file mode 100644 index 00000000..18aff0ce --- /dev/null +++ b/AS1/AS1/AS1.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/AS1/AS1/AS1App.swift b/AS1/AS1/AS1App.swift new file mode 100644 index 00000000..04760027 --- /dev/null +++ b/AS1/AS1/AS1App.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct AS1App: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/AS1/AS1/Assets.xcassets/AccentColor.colorset/Contents.json b/AS1/AS1/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/AS1/AS1/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AS1/AS1/Assets.xcassets/AppIcon.appiconset/Contents.json b/AS1/AS1/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..3f00db43 --- /dev/null +++ b/AS1/AS1/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AS1/AS1/Assets.xcassets/Contents.json b/AS1/AS1/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/AS1/AS1/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AS1/AS1/ContentView.swift b/AS1/AS1/ContentView.swift new file mode 100644 index 00000000..b000a7e4 --- /dev/null +++ b/AS1/AS1/ContentView.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} From 978913e5995092fc0fc4e7af4510f3d76948952a Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Sun, 31 Aug 2025 17:29:11 -0700 Subject: [PATCH 06/13] AS2 initial commit --- AS2/AS2.xcodeproj/project.pbxproj | 326 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + AS2/AS2/AS2.entitlements | 10 + AS2/AS2/AS2App.swift | 17 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 ++++ AS2/AS2/Assets.xcassets/Contents.json | 6 + AS2/AS2/ContentView.swift | 17 + 8 files changed, 452 insertions(+) create mode 100644 AS2/AS2.xcodeproj/project.pbxproj create mode 100644 AS2/AS2.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 AS2/AS2/AS2.entitlements create mode 100644 AS2/AS2/AS2App.swift create mode 100644 AS2/AS2/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 AS2/AS2/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 AS2/AS2/Assets.xcassets/Contents.json create mode 100644 AS2/AS2/ContentView.swift diff --git a/AS2/AS2.xcodeproj/project.pbxproj b/AS2/AS2.xcodeproj/project.pbxproj new file mode 100644 index 00000000..1a018415 --- /dev/null +++ b/AS2/AS2.xcodeproj/project.pbxproj @@ -0,0 +1,326 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + 63652E162E651D9000A5256C /* AS2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AS2.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 63652E182E651D9000A5256C /* AS2 */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AS2; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 63652E132E651D9000A5256C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 63652E0D2E651D9000A5256C = { + isa = PBXGroup; + children = ( + 63652E182E651D9000A5256C /* AS2 */, + 63652E172E651D9000A5256C /* Products */, + ); + sourceTree = ""; + }; + 63652E172E651D9000A5256C /* Products */ = { + isa = PBXGroup; + children = ( + 63652E162E651D9000A5256C /* AS2.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 63652E152E651D9000A5256C /* AS2 */ = { + isa = PBXNativeTarget; + buildConfigurationList = 63652E222E651D9200A5256C /* Build configuration list for PBXNativeTarget "AS2" */; + buildPhases = ( + 63652E122E651D9000A5256C /* Sources */, + 63652E132E651D9000A5256C /* Frameworks */, + 63652E142E651D9000A5256C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 63652E182E651D9000A5256C /* AS2 */, + ); + name = AS2; + packageProductDependencies = ( + ); + productName = AS2; + productReference = 63652E162E651D9000A5256C /* AS2.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 63652E0E2E651D9000A5256C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 63652E152E651D9000A5256C = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = 63652E112E651D9000A5256C /* Build configuration list for PBXProject "AS2" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 63652E0D2E651D9000A5256C; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 63652E172E651D9000A5256C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 63652E152E651D9000A5256C /* AS2 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 63652E142E651D9000A5256C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 63652E122E651D9000A5256C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 63652E202E651D9200A5256C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 8WK25CU37Q; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 63652E212E651D9200A5256C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 8WK25CU37Q; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 63652E232E651D9200A5256C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AS2/AS2.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8WK25CU37Q; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.rzsoftware.AS2; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 63652E242E651D9200A5256C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AS2/AS2.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8WK25CU37Q; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.rzsoftware.AS2; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 63652E112E651D9000A5256C /* Build configuration list for PBXProject "AS2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 63652E202E651D9200A5256C /* Debug */, + 63652E212E651D9200A5256C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 63652E222E651D9200A5256C /* Build configuration list for PBXNativeTarget "AS2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 63652E232E651D9200A5256C /* Debug */, + 63652E242E651D9200A5256C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 63652E0E2E651D9000A5256C /* Project object */; +} diff --git a/AS2/AS2.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AS2/AS2.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/AS2/AS2.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/AS2/AS2/AS2.entitlements b/AS2/AS2/AS2.entitlements new file mode 100644 index 00000000..18aff0ce --- /dev/null +++ b/AS2/AS2/AS2.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/AS2/AS2/AS2App.swift b/AS2/AS2/AS2App.swift new file mode 100644 index 00000000..8071d1a7 --- /dev/null +++ b/AS2/AS2/AS2App.swift @@ -0,0 +1,17 @@ +// +// AS2App.swift +// AS2 +// +// Created by Chad Parker on 8/31/25. +// + +import SwiftUI + +@main +struct AS2App: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/AS2/AS2/Assets.xcassets/AccentColor.colorset/Contents.json b/AS2/AS2/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/AS2/AS2/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AS2/AS2/Assets.xcassets/AppIcon.appiconset/Contents.json b/AS2/AS2/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..3f00db43 --- /dev/null +++ b/AS2/AS2/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AS2/AS2/Assets.xcassets/Contents.json b/AS2/AS2/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/AS2/AS2/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AS2/AS2/ContentView.swift b/AS2/AS2/ContentView.swift new file mode 100644 index 00000000..b000a7e4 --- /dev/null +++ b/AS2/AS2/ContentView.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} From 55c6a87ef39c4b3c360b25adb8481367484c7265 Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Sun, 31 Aug 2025 20:05:24 -0700 Subject: [PATCH 07/13] Claude's plan --- AS2/docs/XCODE_MONITORING_PLAN.md | 202 ++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 AS2/docs/XCODE_MONITORING_PLAN.md diff --git a/AS2/docs/XCODE_MONITORING_PLAN.md b/AS2/docs/XCODE_MONITORING_PLAN.md new file mode 100644 index 00000000..72452b06 --- /dev/null +++ b/AS2/docs/XCODE_MONITORING_PLAN.md @@ -0,0 +1,202 @@ +# AS2 Xcode Monitoring Implementation Plan + +Following the proven architecture of CopilotForXcode, this document outlines the phased approach to implementing comprehensive Xcode monitoring capabilities. + +## 🏗️ **Overall Architecture** + +**Based on CopilotForXcode's successful pattern:** +- **Pure Accessibility API approach** (no AppleScript) +- **Unsandboxed application** for full system access +- **Real-time AX notification streams** for instant updates +- **Incremental implementation** - build up capabilities gradually + +--- + +## 📋 **Implementation Phases** + +### **Phase 1: Foundation & Real-time Monitoring** ⭐ *Next* +**Goal**: Remove sandbox, add accessibility foundation, improve Xcode detection + +**Tasks**: +1. **Remove App Sandbox** + - Update `AS2.entitlements`: `com.apple.security.app-sandbox = false` + - Test basic functionality still works + +2. **Add Accessibility Foundation** + - Import required frameworks (`ApplicationServices`, `Cocoa`) + - Add basic AXUIElement creation for Xcode apps + - Handle accessibility permission requests + +3. **Improve Xcode Detection** + - Fix active/background state detection + - Add better real-time app state updates + - Show Xcode version information + +4. **Basic AX Health Check** + - Test AXUIElement creation for detected Xcode instances + - Display basic accessibility status in UI + - Handle permission denied gracefully + +**Success Criteria**: +- ✅ App runs without sandbox restrictions +- ✅ Detects Xcode instances accurately +- ✅ Shows accessibility permission status +- ✅ Creates AXUIElements for Xcode apps + +--- + +### **Phase 2: Window & File Path Detection** +**Goal**: Monitor Xcode windows and extract file paths + +**Tasks**: +1. **Window Monitoring** + - Detect focused Xcode windows + - Identify workspace vs other window types + - Track window changes in real-time + +2. **File Path Extraction** (following CopilotForXcode patterns) + - `extractDocumentURL`: Get current file from `windowElement.document` + - `extractWorkspaceURL`: Parse workspace path from window children + - `extractProjectURL`: Derive project root from workspace/document + +3. **Real-time Updates** + - AX notification streams for window focus changes + - Update UI when active document changes + - Handle multiple Xcode windows + +**Success Criteria**: +- ✅ Shows current active file path +- ✅ Shows workspace path +- ✅ Updates in real-time when switching files +- ✅ Handles multiple Xcode instances + +--- + +### **Phase 3: Advanced Window Analysis** +**Goal**: Deep window inspection and UI element traversal + +**Tasks**: +1. **Window Element Hierarchy** + - Navigate AX element tree structure + - Identify editor areas vs other UI elements + - Find source editor elements specifically + +2. **Multiple Window Support** + - Track all open Xcode windows per instance + - Handle split editors and multiple tabs + - Project context awareness + +3. **Robustness Features** + - Accessibility API malfunction detection + - Auto-recovery when AX elements become stale + - Error handling for permission changes + +**Success Criteria**: +- ✅ Identifies source editor elements +- ✅ Handles complex window layouts +- ✅ Robust error recovery + +--- + +### **Phase 4: Editor Content Access** +**Goal**: Read editor content, cursor position, selections + +**Tasks**: +1. **Editor Content Reading** + - Get full text content via `kAXValueAttribute` + - Read selected text ranges + - Parse content into lines + +2. **Cursor & Selection Tracking** + - Real-time cursor position updates + - Selection range detection + - Convert between different coordinate systems + +3. **Performance Optimization** + - Implement content caching (like CopilotForXcode's `Cache` class) + - Efficient line-based range conversions + - Debounced updates + +**Success Criteria**: +- ✅ Displays current editor content +- ✅ Shows cursor position and selections +- ✅ Real-time updates without performance issues + +--- + +### **Phase 5: Advanced Features** +**Goal**: Full feature parity with monitoring capabilities + +**Tasks**: +1. **Comprehensive State Tracking** + - Completion panel detection + - Line annotations and error markers + - Scroll position tracking + +2. **Multi-Workspace Management** + - Track all open workspaces per Xcode instance + - Workspace-specific context + - Tab enumeration and tracking + +3. **Integration Ready** + - Clean APIs for external tools + - Event streaming for other components + - Extensible architecture + +--- + +## 🔧 **Technical Implementation Notes** + +### **Key CopilotForXcode Patterns to Follow**: + +1. **AXUIElement Management**: + ```swift + let app = AXUIElementCreateApplication(processIdentifier) + app.setMessagingTimeout(2) + ``` + +2. **Window Identification**: + ```swift + window.identifier == "Xcode.WorkspaceWindow" + ``` + +3. **File Path Extraction**: + ```swift + let path = windowElement.document // for current file + // Parse children descriptions for workspace path + ``` + +4. **Real-time Updates**: + ```swift + AXNotificationStream( + app: runningApplication, + notificationNames: kAXFocusedUIElementChangedNotification, + kAXTitleChangedNotification, ... + ) + ``` + +5. **Error Recovery**: + - Global actor isolation (`@XcodeInspectorActor`) + - Malfunction detection and auto-restart + - Graceful degradation when AX fails + +### **Required Entitlements**: +```xml +com.apple.security.app-sandbox + + +``` + +### **User Permission Required**: +- **System Preferences > Security & Privacy > Accessibility** +- App must be explicitly granted permission by user +- Handle permission denied gracefully + +--- + +## 🎯 **Current Status**: Phase 1 Preparation + +**Completed**: Basic NSWorkspace monitoring +**Next**: Remove sandbox and implement AX foundation + +This plan ensures we build robust, maintainable Xcode monitoring following proven patterns while allowing for incremental development and testing. \ No newline at end of file From 8255f9a96624fcaba35e9b04a5d38af584e365be Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Sun, 31 Aug 2025 21:27:56 -0700 Subject: [PATCH 08/13] remove sandbox --- AS2/AS2/AS2.entitlements | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AS2/AS2/AS2.entitlements b/AS2/AS2/AS2.entitlements index 18aff0ce..e89b7f32 100644 --- a/AS2/AS2/AS2.entitlements +++ b/AS2/AS2/AS2.entitlements @@ -3,8 +3,6 @@ com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + From f3337453ef1f64065202f70491d7b12a507fb016 Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Sun, 31 Aug 2025 21:29:27 -0700 Subject: [PATCH 09/13] phase 1, tracking active Xcode and other app PIDs --- AS2/AS2/ContentView.swift | 54 ++++++++- AS2/AS2/XcodeMonitor.swift | 187 ++++++++++++++++++++++++++++++ AS2/docs/XCODE_MONITORING_PLAN.md | 21 +++- 3 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 AS2/AS2/XcodeMonitor.swift diff --git a/AS2/AS2/ContentView.swift b/AS2/AS2/ContentView.swift index b000a7e4..0a765e16 100644 --- a/AS2/AS2/ContentView.swift +++ b/AS2/AS2/ContentView.swift @@ -1,12 +1,56 @@ import SwiftUI struct ContentView: View { + @StateObject private var xcodeMonitor = XcodeMonitor() + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + VStack(spacing: 20) { + Text("🔍 Xcode Monitor") + .font(.title) + + Group { + Text("Xcode Instances: \(xcodeMonitor.xcodeInstances.count)") + + HStack { + Text("Accessibility:") + Text(xcodeMonitor.accessibilityPermissionGranted ? "✅ Granted" : "❌ Not Granted") + .foregroundColor(xcodeMonitor.accessibilityPermissionGranted ? .green : .red) + } + + if let activeXcode = xcodeMonitor.activeXcode { + Text("✅ Active Xcode: PID \(activeXcode.processIdentifier)") + .foregroundColor(.green) + } else if let lastActive = xcodeMonitor.lastActiveXcode { + Text("🔄 Last Active: PID \(lastActive.processIdentifier)") + .foregroundColor(.orange) + } else { + Text("❌ No Xcode Found") + .foregroundColor(.red) + } + + Button("🔄 Refresh State") { + xcodeMonitor.refreshActiveState() + } + .buttonStyle(.bordered) + } + .font(.headline) + + if !xcodeMonitor.xcodeInstances.isEmpty { + Text("All Xcode Instances:") + .font(.subheadline) + + ForEach(xcodeMonitor.xcodeInstances, id: \.processIdentifier) { xcode in + HStack { + Text("PID: \(xcode.processIdentifier)") + if xcodeMonitor.activeXcode?.processIdentifier == xcode.processIdentifier { + Text("🟢 Active") + } else { + Text("⚪ Background") + } + } + .font(.caption) + } + } } .padding() } diff --git a/AS2/AS2/XcodeMonitor.swift b/AS2/AS2/XcodeMonitor.swift new file mode 100644 index 00000000..6b8f86eb --- /dev/null +++ b/AS2/AS2/XcodeMonitor.swift @@ -0,0 +1,187 @@ +import Cocoa +import Foundation +import ApplicationServices + +/// Minimal Xcode monitoring - starts with basic application detection +class XcodeMonitor: ObservableObject { + @Published var xcodeInstances: [NSRunningApplication] = [] + @Published var activeXcode: NSRunningApplication? + @Published var lastActiveXcode: NSRunningApplication? + @Published var accessibilityPermissionGranted: Bool = false + + private let workspace = NSWorkspace.shared + + init() { + checkAccessibilityPermission() + setupMonitoring() + findExistingXcodeInstances() + startPeriodicCheck() + } + + private func startPeriodicCheck() { + // Check every 2 seconds to ensure state is accurate + Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in + Task { @MainActor in + self?.refreshActiveState() + } + } + } + + @MainActor + public func refreshActiveState() { + let frontmostApp = workspace.frontmostApplication + let currentlyActivePID = frontmostApp?.processIdentifier ?? -1 + + print("🔄 Periodic check - Frontmost: \(frontmostApp?.localizedName ?? "None") (PID: \(currentlyActivePID))") + + let shouldBeActiveXcode = xcodeInstances.first { xcode in + xcode.processIdentifier == currentlyActivePID + } + + if shouldBeActiveXcode?.processIdentifier != activeXcode?.processIdentifier { + print("🔄 State correction needed!") + print(" - Should be active: \(shouldBeActiveXcode?.processIdentifier ?? -1)") + print(" - Currently tracked as active: \(activeXcode?.processIdentifier ?? -1)") + + activeXcode = shouldBeActiveXcode + if let newActive = shouldBeActiveXcode { + lastActiveXcode = newActive + } + } + } + + private func setupMonitoring() { + // Monitor for app activation + NotificationCenter.default.addObserver( + forName: NSWorkspace.didActivateApplicationNotification, + object: nil, + queue: .main + ) { [weak self] notification in + self?.handleApplicationActivated(notification) + } + + // Monitor for app termination + NotificationCenter.default.addObserver( + forName: NSWorkspace.didTerminateApplicationNotification, + object: nil, + queue: .main + ) { [weak self] notification in + self?.handleApplicationTerminated(notification) + } + } + + private func checkAccessibilityPermission() { + accessibilityPermissionGranted = AXIsProcessTrusted() + print("🔐 Accessibility Permission: \(accessibilityPermissionGranted ? "✅ Granted" : "❌ Not Granted")") + + if !accessibilityPermissionGranted { + print("💡 Request accessibility permission...") + requestAccessibilityPermission() + } + } + + private func requestAccessibilityPermission() { + let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] + AXIsProcessTrustedWithOptions(options as CFDictionary) + } + + private func findExistingXcodeInstances() { + xcodeInstances = workspace.runningApplications.filter { $0.isXcode } + + // Check what's actually frontmost right now + let frontmostApp = workspace.frontmostApplication + print("🔍 Current frontmost app: \(frontmostApp?.localizedName ?? "Unknown") (PID: \(frontmostApp?.processIdentifier ?? -1))") + + // Find active Xcode - only if Xcode is actually frontmost + activeXcode = xcodeInstances.first { xcode in + xcode.processIdentifier == frontmostApp?.processIdentifier + } + + // If no active Xcode but we have instances, remember the first one as "last active" + if activeXcode == nil && !xcodeInstances.isEmpty { + lastActiveXcode = lastActiveXcode ?? xcodeInstances.first + } else if let active = activeXcode { + lastActiveXcode = active + } + + print("📱 Found \(xcodeInstances.count) Xcode instances") + if let active = activeXcode { + print("✅ Active Xcode: PID \(active.processIdentifier)") + } else if let lastActive = lastActiveXcode { + print("🔄 Last Active Xcode: PID \(lastActive.processIdentifier)") + } + + // Test AX access for first Xcode if we have permission + if accessibilityPermissionGranted && !xcodeInstances.isEmpty { + testAccessibilityAccess() + } + } + + private func testAccessibilityAccess() { + guard let xcode = xcodeInstances.first else { return } + + let axApp = AXUIElementCreateApplication(xcode.processIdentifier) + + // Test basic AX access + var focusedElement: AnyObject? + let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement) + + switch result { + case .success: + print("🎯 AX Access Test: ✅ Success - can read focused element") + case .apiDisabled: + print("🚫 AX Access Test: API Disabled") + case .notImplemented: + print("⚠️ AX Access Test: Not Implemented") + default: + print("❓ AX Access Test: Other error - \(result.rawValue)") + } + } + + private func handleApplicationActivated(_ notification: Notification) { + guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } + + print("📱 App activated: \(app.localizedName ?? "Unknown") (PID: \(app.processIdentifier))") + + // Ensure UI updates happen on main thread + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + if app.isXcode { + print("🔄 Xcode activated: PID \(app.processIdentifier)") + self.activeXcode = app + self.lastActiveXcode = app + + if !self.xcodeInstances.contains(where: { $0.processIdentifier == app.processIdentifier }) { + self.xcodeInstances.append(app) + print("📱 Added new Xcode instance: PID \(app.processIdentifier)") + } + } else { + // When another app becomes active, Xcode is no longer active + if let previousActive = self.activeXcode { + print("📱 \(app.localizedName ?? "Unknown app") became active, Xcode (PID: \(previousActive.processIdentifier)) backgrounded") + self.activeXcode = nil + } + } + } + } + + private func handleApplicationTerminated(_ notification: Notification) { + guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } + + if app.isXcode { + print("❌ Xcode terminated: PID \(app.processIdentifier)") + xcodeInstances.removeAll { $0.processIdentifier == app.processIdentifier } + + if activeXcode?.processIdentifier == app.processIdentifier { + activeXcode = xcodeInstances.first(where: \.isActive) + } + } + } +} + +extension NSRunningApplication { + var isXcode: Bool { + bundleIdentifier == "com.apple.dt.Xcode" + } +} \ No newline at end of file diff --git a/AS2/docs/XCODE_MONITORING_PLAN.md b/AS2/docs/XCODE_MONITORING_PLAN.md index 72452b06..b5dbbf82 100644 --- a/AS2/docs/XCODE_MONITORING_PLAN.md +++ b/AS2/docs/XCODE_MONITORING_PLAN.md @@ -194,9 +194,22 @@ Following the proven architecture of CopilotForXcode, this document outlines the --- -## 🎯 **Current Status**: Phase 1 Preparation - -**Completed**: Basic NSWorkspace monitoring -**Next**: Remove sandbox and implement AX foundation +## 🎯 **Current Status**: Phase 1 Complete → Starting Phase 2 + +### **✅ Phase 1 COMPLETED**: +- ✅ Removed App Sandbox (`com.apple.security.app-sandbox = false`) +- ✅ Added Accessibility Foundation (ApplicationServices framework) +- ✅ Implemented accessibility permission handling with auto-prompt +- ✅ Fixed Xcode detection using `workspace.frontmostApplication` +- ✅ Added basic AXUIElement creation and testing +- ✅ Robust active/inactive state tracking with periodic correction +- ✅ Thread-safe UI updates with proper MainActor usage + +### **🚀 Phase 2 NEXT**: Window & File Path Detection +**Ready to implement**: +1. Monitor focused Xcode windows using AX notifications +2. Extract file paths from window elements (`windowElement.document`) +3. Extract workspace paths from window children +4. Real-time updates when switching files/projects This plan ensures we build robust, maintainable Xcode monitoring following proven patterns while allowing for incremental development and testing. \ No newline at end of file From a2fc5b57940f1007c2a190a2c6e5be69f642fddc Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Mon, 1 Sep 2025 20:24:45 -0700 Subject: [PATCH 10/13] phase 2, track current file and workspace info Add XcodeWindowInspector to extract document, workspace, and project URLs from focused Xcode windows using accessibility APIs. Update ContentView to display current file/workspace info and XcodeMonitor to refresh window data. --- AS2/AS2/ContentView.swift | 50 ++++++++++ AS2/AS2/XcodeMonitor.swift | 70 ++++++++++++++ AS2/AS2/XcodeWindowInspector.swift | 146 +++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 AS2/AS2/XcodeWindowInspector.swift diff --git a/AS2/AS2/ContentView.swift b/AS2/AS2/ContentView.swift index 0a765e16..51cc3ba0 100644 --- a/AS2/AS2/ContentView.swift +++ b/AS2/AS2/ContentView.swift @@ -35,6 +35,56 @@ struct ContentView: View { } .font(.headline) + // Phase 2: File and workspace info + if xcodeMonitor.accessibilityPermissionGranted { + Divider() + + VStack(spacing: 10) { + Text("📄 Current File & Workspace") + .font(.title2) + .bold() + + Group { + if let docURL = xcodeMonitor.currentDocumentURL { + HStack { + Text("📄 Document:") + Text(docURL.lastPathComponent) + .foregroundColor(.blue) + .font(.monospaced(.caption)()) + } + } else { + Text("📄 No document detected") + .foregroundColor(.secondary) + } + + if let workspaceURL = xcodeMonitor.currentWorkspaceURL { + HStack { + Text("📦 Workspace:") + Text(workspaceURL.lastPathComponent) + .foregroundColor(.green) + .font(.monospaced(.caption)()) + } + } else { + Text("📦 No workspace detected") + .foregroundColor(.secondary) + } + + if let projectURL = xcodeMonitor.currentProjectURL { + HStack { + Text("🏗️ Project:") + Text(projectURL.lastPathComponent) + .foregroundColor(.purple) + .font(.monospaced(.caption)()) + } + } else { + Text("🏗️ No project detected") + .foregroundColor(.secondary) + } + } + .font(.caption) + } + } + if !xcodeMonitor.xcodeInstances.isEmpty { Text("All Xcode Instances:") .font(.subheadline) diff --git a/AS2/AS2/XcodeMonitor.swift b/AS2/AS2/XcodeMonitor.swift index 6b8f86eb..b74c0204 100644 --- a/AS2/AS2/XcodeMonitor.swift +++ b/AS2/AS2/XcodeMonitor.swift @@ -9,6 +9,12 @@ class XcodeMonitor: ObservableObject { @Published var lastActiveXcode: NSRunningApplication? @Published var accessibilityPermissionGranted: Bool = false + // Phase 2: Window and file monitoring + @Published var focusedWindow: XcodeWindowInspector? + @Published var currentDocumentURL: URL? + @Published var currentWorkspaceURL: URL? + @Published var currentProjectURL: URL? + private let workspace = NSWorkspace.shared init() { @@ -46,7 +52,15 @@ class XcodeMonitor: ObservableObject { activeXcode = shouldBeActiveXcode if let newActive = shouldBeActiveXcode { lastActiveXcode = newActive + + // Update window monitoring for new active Xcode + if accessibilityPermissionGranted { + monitorXcodeWindows(for: newActive) + } } + } else if let currentActive = shouldBeActiveXcode, accessibilityPermissionGranted { + // Even if active Xcode didn't change, refresh window info + monitorXcodeWindows(for: currentActive) } } @@ -114,6 +128,62 @@ class XcodeMonitor: ObservableObject { // Test AX access for first Xcode if we have permission if accessibilityPermissionGranted && !xcodeInstances.isEmpty { testAccessibilityAccess() + + // Start window monitoring if we have an active Xcode + if let activeXcode = activeXcode { + Task { @MainActor in + monitorXcodeWindows(for: activeXcode) + } + } + } + } + + // MARK: - Phase 2: Window Monitoring + + @MainActor + private func monitorXcodeWindows(for xcodeApp: NSRunningApplication) { + guard accessibilityPermissionGranted else { + print("❌ Cannot monitor windows - no accessibility permission") + return + } + + let axApp = AXUIElementCreateApplication(xcodeApp.processIdentifier) + + // Get focused window + var focusedWindowElement: AnyObject? + let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedWindowAttribute as CFString, &focusedWindowElement) + + guard result == .success, let windowElement = focusedWindowElement else { + print("❌ Could not get focused window from Xcode") + return + } + + guard CFGetTypeID(windowElement) == AXUIElementGetTypeID() else { + print("❌ Focused window element is not an AXUIElement") + return + } + + let axWindowElement = windowElement as! AXUIElement + + print("🪟 Found focused window, creating inspector...") + + // Create window inspector + let windowInspector = XcodeWindowInspector( + processIdentifier: xcodeApp.processIdentifier, + windowElement: axWindowElement + ) + + // Check if it's a workspace window + if windowInspector.isWorkspaceWindow { + print("✅ Found Xcode workspace window") + focusedWindow = windowInspector + + // Update current file/workspace info + currentDocumentURL = windowInspector.documentURL + currentWorkspaceURL = windowInspector.workspaceURL + currentProjectURL = windowInspector.projectRootURL + } else { + print("📝 Found other Xcode window (not workspace)") } } diff --git a/AS2/AS2/XcodeWindowInspector.swift b/AS2/AS2/XcodeWindowInspector.swift new file mode 100644 index 00000000..023453ae --- /dev/null +++ b/AS2/AS2/XcodeWindowInspector.swift @@ -0,0 +1,146 @@ +import ApplicationServices +import Foundation + +/// Window inspector for individual Xcode windows - follows CopilotForXcode patterns +class XcodeWindowInspector { + let processIdentifier: pid_t + let windowElement: AXUIElement + + @Published var documentURL: URL? + @Published var workspaceURL: URL? + @Published var projectRootURL: URL? + + init(processIdentifier: pid_t, windowElement: AXUIElement) { + self.processIdentifier = processIdentifier + self.windowElement = windowElement + + // Initial extraction + refresh() + } + + /// Refresh all URLs from the window + func refresh() { + documentURL = Self.extractDocumentURL(from: windowElement) + workspaceURL = Self.extractWorkspaceURL(from: windowElement) + projectRootURL = Self.extractProjectURL(workspaceURL: workspaceURL, documentURL: documentURL) + + print("📄 Window refresh:") + print(" 📁 Document: \(documentURL?.lastPathComponent ?? "None")") + print(" 📦 Workspace: \(workspaceURL?.lastPathComponent ?? "None")") + print(" 🏗️ Project: \(projectRootURL?.lastPathComponent ?? "None")") + } + + // MARK: - URL Extraction (following CopilotForXcode patterns) + + /// Extract document URL from window element - mirrors CopilotForXcode's extractDocumentURL + static func extractDocumentURL(from windowElement: AXUIElement) -> URL? { + // Fetch file path of the frontmost window of Xcode through Accessibility API + var documentValue: AnyObject? + let result = AXUIElementCopyAttributeValue(windowElement, kAXDocumentAttribute as CFString, &documentValue) + + guard result == .success, let path = documentValue as? String else { + return nil + } + + // Remove percent encoding and clean up path + guard let cleanPath = path.removingPercentEncoding else { return nil } + + let url = URL(fileURLWithPath: cleanPath.replacingOccurrences(of: "file://", with: "")) + return adjustFileURL(url) + } + + /// Extract workspace URL from window children - mirrors CopilotForXcode's extractWorkspaceURL + static func extractWorkspaceURL(from windowElement: AXUIElement) -> URL? { + var children: AnyObject? + let result = AXUIElementCopyAttributeValue(windowElement, kAXChildrenAttribute as CFString, &children) + + guard result == .success, let childrenArray = children as? [AXUIElement] else { + return nil + } + + // Look through children for path descriptions + for child in childrenArray { + var description: AnyObject? + let descResult = AXUIElementCopyAttributeValue(child, kAXDescriptionAttribute as CFString, &description) + + guard descResult == .success, let desc = description as? String else { continue } + + // Check if this looks like a path (starts with "/" and is longer than 1 character) + if desc.starts(with: "/"), desc.count > 1 { + let trimmedPath = desc.trimmingCharacters(in: .newlines) + return URL(fileURLWithPath: trimmedPath) + } + } + + return nil + } + + /// Extract project root URL - mirrors CopilotForXcode's extractProjectURL + static func extractProjectURL(workspaceURL: URL?, documentURL: URL?) -> URL? { + guard var currentURL = workspaceURL ?? documentURL else { return nil } + + var firstDirectoryURL: URL? + var lastGitDirectoryURL: URL? + + while currentURL.pathComponents.count > 1 { + defer { currentURL.deleteLastPathComponent() } + + guard FileManager.default.fileExists(atPath: currentURL.path) else { continue } + + var isDirectory: ObjCBool = false + FileManager.default.fileExists(atPath: currentURL.path, isDirectory: &isDirectory) + guard isDirectory.boolValue else { continue } + + // Skip Xcode-specific directories + guard currentURL.pathExtension != "xcodeproj", + currentURL.pathExtension != "xcworkspace", + currentURL.pathExtension != "playground" else { continue } + + if firstDirectoryURL == nil { + firstDirectoryURL = currentURL + } + + // Check for .git directory + let gitURL = currentURL.appendingPathComponent(".git") + var gitIsDirectory: ObjCBool = false + + if FileManager.default.fileExists(atPath: gitURL.path, isDirectory: &gitIsDirectory) { + if gitIsDirectory.boolValue { + lastGitDirectoryURL = currentURL + } else if let gitContent = try? String(contentsOf: gitURL) { + // Check for git worktree + if !gitContent.hasPrefix("gitdir: ../") && gitContent.contains("/.git/worktrees/") { + lastGitDirectoryURL = currentURL + } + } + } + } + + return lastGitDirectoryURL ?? firstDirectoryURL ?? workspaceURL + } + + /// Adjust file URL for special cases - mirrors CopilotForXcode's adjustFileURL + static func adjustFileURL(_ url: URL) -> URL { + if url.pathExtension == "playground", + FileManager.default.fileExists(atPath: url.path) { + var isDirectory: ObjCBool = false + FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + if isDirectory.boolValue { + return url.appendingPathComponent("Contents.swift") + } + } + return url + } + + /// Check if this is a workspace window + var isWorkspaceWindow: Bool { + var identifier: AnyObject? + let result = AXUIElementCopyAttributeValue(windowElement, kAXIdentifierAttribute as CFString, &identifier) + + if result == .success, let id = identifier as? String { + return id == "Xcode.WorkspaceWindow" + } + + return false + } +} \ No newline at end of file From ad8196c245c54ca1f7d5ecef265460b19ee67a9f Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Mon, 1 Sep 2025 20:49:04 -0700 Subject: [PATCH 11/13] refactor: implement strategy pattern for multi-editor support - Extract generic AppMonitor with EditorStrategy protocol - XcodeMonitor now inherits from AppMonitor for specialized features - Consolidate window inspection into XcodeStrategy - Reduce XcodeMonitor complexity from 252 to 58 lines - Maintain UI compatibility while enabling future multi-editor support --- AS2/AS2/AppMonitor.swift | 242 ++++++++++++++++ AS2/AS2/ContentView.swift | 3 + AS2/AS2/XcodeMonitor.swift | 269 +++--------------- ...dowInspector.swift => XcodeStrategy.swift} | 64 ++--- AS2/docs/XCODE_MONITORING_PLAN.md | 31 +- 5 files changed, 338 insertions(+), 271 deletions(-) create mode 100644 AS2/AS2/AppMonitor.swift rename AS2/AS2/{XcodeWindowInspector.swift => XcodeStrategy.swift} (76%) diff --git a/AS2/AS2/AppMonitor.swift b/AS2/AS2/AppMonitor.swift new file mode 100644 index 00000000..49aa57f9 --- /dev/null +++ b/AS2/AS2/AppMonitor.swift @@ -0,0 +1,242 @@ +import Cocoa +import Foundation +import ApplicationServices + +protocol WindowInspector: AnyObject { + var documentURL: URL? { get } + var workspaceURL: URL? { get } + var projectURL: URL? { get } + var isMainWorkWindow: Bool { get } + + func refresh() +} + +protocol EditorStrategy { + var displayName: String { get } + var bundleIdentifier: String { get } + + func shouldMonitor(_ app: NSRunningApplication) -> Bool + func createWindowInspector(processId: pid_t, windowElement: AXUIElement) -> WindowInspector? +} + +struct AppInstance { + let app: NSRunningApplication + let strategy: EditorStrategy + var windowInspector: WindowInspector? + + var displayName: String { + strategy.displayName + } + + var processId: pid_t { + app.processIdentifier + } +} + +@MainActor +class AppMonitor: ObservableObject { + @Published var monitoredApps: [String: AppInstance] = [:] + @Published var activeApp: AppInstance? + @Published var accessibilityPermissionGranted: Bool = false + + private let strategies: [String: EditorStrategy] + private let workspace = NSWorkspace.shared + + init(strategies: [EditorStrategy]) { + self.strategies = Dictionary(uniqueKeysWithValues: strategies.map { ($0.bundleIdentifier, $0) }) + + checkAccessibilityPermission() + setupMonitoring() + findExistingApps() + startPeriodicCheck() + } + + private func startPeriodicCheck() { + Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in + Task { @MainActor in + self?.refreshActiveState() + } + } + } + + @MainActor + public func refreshActiveState() { + let frontmostApp = workspace.frontmostApplication + let currentlyActivePID = frontmostApp?.processIdentifier ?? -1 + + print("🔄 Periodic check - Frontmost: \(frontmostApp?.localizedName ?? "None") (PID: \(currentlyActivePID))") + + let shouldBeActive = monitoredApps.values.first { instance in + instance.processId == currentlyActivePID + } + + if shouldBeActive?.processId != activeApp?.processId { + print("🔄 State correction needed!") + print(" - Should be active: \(shouldBeActive?.processId ?? -1)") + print(" - Currently tracked as active: \(activeApp?.processId ?? -1)") + + activeApp = shouldBeActive + + if let newActive = shouldBeActive, accessibilityPermissionGranted { + monitorAppWindows(for: newActive) + } + } else if let currentActive = shouldBeActive, accessibilityPermissionGranted { + monitorAppWindows(for: currentActive) + } + } + + private func setupMonitoring() { + NotificationCenter.default.addObserver( + forName: NSWorkspace.didActivateApplicationNotification, + object: nil, + queue: .main + ) { [weak self] notification in + self?.handleApplicationActivated(notification) + } + + NotificationCenter.default.addObserver( + forName: NSWorkspace.didTerminateApplicationNotification, + object: nil, + queue: .main + ) { [weak self] notification in + self?.handleApplicationTerminated(notification) + } + } + + private func checkAccessibilityPermission() { + accessibilityPermissionGranted = AXIsProcessTrusted() + print("🔐 Accessibility Permission: \(accessibilityPermissionGranted ? "✅ Granted" : "❌ Not Granted")") + + if !accessibilityPermissionGranted { + print("💡 Request accessibility permission...") + requestAccessibilityPermission() + } + } + + private func requestAccessibilityPermission() { + let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] + AXIsProcessTrustedWithOptions(options as CFDictionary) + } + + private func findExistingApps() { + let runningApps = workspace.runningApplications + + for app in runningApps { + if let strategy = findStrategy(for: app) { + let instance = AppInstance(app: app, strategy: strategy, windowInspector: nil) + monitoredApps[makeKey(for: app)] = instance + print("📱 Found \(strategy.displayName): PID \(app.processIdentifier)") + } + } + + let frontmostApp = workspace.frontmostApplication + print("🔍 Current frontmost app: \(frontmostApp?.localizedName ?? "Unknown") (PID: \(frontmostApp?.processIdentifier ?? -1))") + + activeApp = monitoredApps.values.first { instance in + instance.processId == frontmostApp?.processIdentifier + } + + if let active = activeApp { + print("✅ Active app: \(active.displayName) PID \(active.processId)") + } + + if accessibilityPermissionGranted, let activeApp = activeApp { + Task { @MainActor in + monitorAppWindows(for: activeApp) + } + } + } + + @MainActor + private func monitorAppWindows(for appInstance: AppInstance) { + guard accessibilityPermissionGranted else { + print("❌ Cannot monitor windows - no accessibility permission") + return + } + + let axApp = AXUIElementCreateApplication(appInstance.processId) + + var focusedWindowElement: AnyObject? + let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedWindowAttribute as CFString, &focusedWindowElement) + + guard result == .success, let windowElement = focusedWindowElement else { + print("❌ Could not get focused window from \(appInstance.displayName)") + return + } + + guard CFGetTypeID(windowElement) == AXUIElementGetTypeID() else { + print("❌ Focused window element is not an AXUIElement") + return + } + + let axWindowElement = windowElement as! AXUIElement + + print("🪟 Found focused window for \(appInstance.displayName), creating inspector...") + + if let windowInspector = appInstance.strategy.createWindowInspector( + processId: appInstance.processId, + windowElement: axWindowElement + ) { + if windowInspector.isMainWorkWindow { + print("✅ Found main work window for \(appInstance.displayName)") + var updatedInstance = appInstance + updatedInstance.windowInspector = windowInspector + monitoredApps[makeKey(for: appInstance.app)] = updatedInstance + + if activeApp?.processId == appInstance.processId { + activeApp = updatedInstance + } + } else { + print("📝 Found other window for \(appInstance.displayName) (not main work window)") + } + } + } + + private func findStrategy(for app: NSRunningApplication) -> EditorStrategy? { + return strategies.values.first { strategy in + strategy.shouldMonitor(app) + } + } + + private func makeKey(for app: NSRunningApplication) -> String { + return "\(app.bundleIdentifier ?? "unknown")_\(app.processIdentifier)" + } + + private func handleApplicationActivated(_ notification: Notification) { + guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } + + print("📱 App activated: \(app.localizedName ?? "Unknown") (PID: \(app.processIdentifier))") + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + if let strategy = self.findStrategy(for: app) { + print("🔄 \(strategy.displayName) activated: PID \(app.processIdentifier)") + + let key = self.makeKey(for: app) + let instance = AppInstance(app: app, strategy: strategy, windowInspector: nil) + self.monitoredApps[key] = instance + self.activeApp = instance + } else { + if let previousActive = self.activeApp { + print("📱 \(app.localizedName ?? "Unknown app") became active, \(previousActive.displayName) (PID: \(previousActive.processId)) backgrounded") + self.activeApp = nil + } + } + } + } + + private func handleApplicationTerminated(_ notification: Notification) { + guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } + + if findStrategy(for: app) != nil { + let key = makeKey(for: app) + print("❌ App terminated: \(app.localizedName ?? "Unknown") PID \(app.processIdentifier)") + monitoredApps.removeValue(forKey: key) + + if activeApp?.processId == app.processIdentifier { + activeApp = monitoredApps.values.first { $0.app.isActive } + } + } + } +} \ No newline at end of file diff --git a/AS2/AS2/ContentView.swift b/AS2/AS2/ContentView.swift index 51cc3ba0..4a99a8b1 100644 --- a/AS2/AS2/ContentView.swift +++ b/AS2/AS2/ContentView.swift @@ -3,6 +3,9 @@ import SwiftUI struct ContentView: View { @StateObject private var xcodeMonitor = XcodeMonitor() + // Generic app monitor for multi-editor support (future use) + // @StateObject private var appMonitor = AppMonitor(strategies: [XcodeStrategy(), VSCodeStrategy()]) + var body: some View { VStack(spacing: 20) { Text("🔍 Xcode Monitor") diff --git a/AS2/AS2/XcodeMonitor.swift b/AS2/AS2/XcodeMonitor.swift index b74c0204..f8277be7 100644 --- a/AS2/AS2/XcodeMonitor.swift +++ b/AS2/AS2/XcodeMonitor.swift @@ -2,252 +2,59 @@ import Cocoa import Foundation import ApplicationServices -/// Minimal Xcode monitoring - starts with basic application detection -class XcodeMonitor: ObservableObject { +/// Specialized Xcode monitor with enhanced Xcode-specific features +class XcodeMonitor: AppMonitor { @Published var xcodeInstances: [NSRunningApplication] = [] @Published var activeXcode: NSRunningApplication? @Published var lastActiveXcode: NSRunningApplication? - @Published var accessibilityPermissionGranted: Bool = false - - // Phase 2: Window and file monitoring - @Published var focusedWindow: XcodeWindowInspector? + @Published var focusedWindow: WindowInspector? @Published var currentDocumentURL: URL? @Published var currentWorkspaceURL: URL? @Published var currentProjectURL: URL? private let workspace = NSWorkspace.shared - init() { - checkAccessibilityPermission() - setupMonitoring() - findExistingXcodeInstances() - startPeriodicCheck() + override init(strategies: [EditorStrategy]) { + super.init(strategies: strategies) + setupXcodeSpecificMonitoring() } - private func startPeriodicCheck() { - // Check every 2 seconds to ensure state is accurate - Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in - Task { @MainActor in - self?.refreshActiveState() - } - } + convenience init() { + self.init(strategies: [XcodeStrategy()]) } - @MainActor - public func refreshActiveState() { - let frontmostApp = workspace.frontmostApplication - let currentlyActivePID = frontmostApp?.processIdentifier ?? -1 - - print("🔄 Periodic check - Frontmost: \(frontmostApp?.localizedName ?? "None") (PID: \(currentlyActivePID))") - - let shouldBeActiveXcode = xcodeInstances.first { xcode in - xcode.processIdentifier == currentlyActivePID - } - - if shouldBeActiveXcode?.processIdentifier != activeXcode?.processIdentifier { - print("🔄 State correction needed!") - print(" - Should be active: \(shouldBeActiveXcode?.processIdentifier ?? -1)") - print(" - Currently tracked as active: \(activeXcode?.processIdentifier ?? -1)") - - activeXcode = shouldBeActiveXcode - if let newActive = shouldBeActiveXcode { - lastActiveXcode = newActive - - // Update window monitoring for new active Xcode - if accessibilityPermissionGranted { - monitorXcodeWindows(for: newActive) - } - } - } else if let currentActive = shouldBeActiveXcode, accessibilityPermissionGranted { - // Even if active Xcode didn't change, refresh window info - monitorXcodeWindows(for: currentActive) - } + private func setupXcodeSpecificMonitoring() { + // Subscribe to parent's state changes and maintain Xcode-specific properties + $monitoredApps + .map { apps in apps.values.compactMap { $0.app.isXcode ? $0.app : nil } } + .assign(to: &$xcodeInstances) + + $activeApp + .map { $0?.app.isXcode == true ? $0?.app : nil } + .assign(to: &$activeXcode) + + $activeApp + .compactMap { $0?.windowInspector } + .assign(to: &$focusedWindow) + + $activeApp + .compactMap { $0?.windowInspector?.documentURL } + .assign(to: &$currentDocumentURL) + + $activeApp + .compactMap { $0?.windowInspector?.workspaceURL } + .assign(to: &$currentWorkspaceURL) + + $activeApp + .compactMap { $0?.windowInspector?.projectURL } + .assign(to: &$currentProjectURL) + + // Track last active Xcode + $activeXcode + .compactMap { $0 } + .assign(to: &$lastActiveXcode) } - private func setupMonitoring() { - // Monitor for app activation - NotificationCenter.default.addObserver( - forName: NSWorkspace.didActivateApplicationNotification, - object: nil, - queue: .main - ) { [weak self] notification in - self?.handleApplicationActivated(notification) - } - - // Monitor for app termination - NotificationCenter.default.addObserver( - forName: NSWorkspace.didTerminateApplicationNotification, - object: nil, - queue: .main - ) { [weak self] notification in - self?.handleApplicationTerminated(notification) - } - } - - private func checkAccessibilityPermission() { - accessibilityPermissionGranted = AXIsProcessTrusted() - print("🔐 Accessibility Permission: \(accessibilityPermissionGranted ? "✅ Granted" : "❌ Not Granted")") - - if !accessibilityPermissionGranted { - print("💡 Request accessibility permission...") - requestAccessibilityPermission() - } - } - - private func requestAccessibilityPermission() { - let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] - AXIsProcessTrustedWithOptions(options as CFDictionary) - } - - private func findExistingXcodeInstances() { - xcodeInstances = workspace.runningApplications.filter { $0.isXcode } - - // Check what's actually frontmost right now - let frontmostApp = workspace.frontmostApplication - print("🔍 Current frontmost app: \(frontmostApp?.localizedName ?? "Unknown") (PID: \(frontmostApp?.processIdentifier ?? -1))") - - // Find active Xcode - only if Xcode is actually frontmost - activeXcode = xcodeInstances.first { xcode in - xcode.processIdentifier == frontmostApp?.processIdentifier - } - - // If no active Xcode but we have instances, remember the first one as "last active" - if activeXcode == nil && !xcodeInstances.isEmpty { - lastActiveXcode = lastActiveXcode ?? xcodeInstances.first - } else if let active = activeXcode { - lastActiveXcode = active - } - - print("📱 Found \(xcodeInstances.count) Xcode instances") - if let active = activeXcode { - print("✅ Active Xcode: PID \(active.processIdentifier)") - } else if let lastActive = lastActiveXcode { - print("🔄 Last Active Xcode: PID \(lastActive.processIdentifier)") - } - - // Test AX access for first Xcode if we have permission - if accessibilityPermissionGranted && !xcodeInstances.isEmpty { - testAccessibilityAccess() - - // Start window monitoring if we have an active Xcode - if let activeXcode = activeXcode { - Task { @MainActor in - monitorXcodeWindows(for: activeXcode) - } - } - } - } - - // MARK: - Phase 2: Window Monitoring - - @MainActor - private func monitorXcodeWindows(for xcodeApp: NSRunningApplication) { - guard accessibilityPermissionGranted else { - print("❌ Cannot monitor windows - no accessibility permission") - return - } - - let axApp = AXUIElementCreateApplication(xcodeApp.processIdentifier) - - // Get focused window - var focusedWindowElement: AnyObject? - let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedWindowAttribute as CFString, &focusedWindowElement) - - guard result == .success, let windowElement = focusedWindowElement else { - print("❌ Could not get focused window from Xcode") - return - } - - guard CFGetTypeID(windowElement) == AXUIElementGetTypeID() else { - print("❌ Focused window element is not an AXUIElement") - return - } - - let axWindowElement = windowElement as! AXUIElement - - print("🪟 Found focused window, creating inspector...") - - // Create window inspector - let windowInspector = XcodeWindowInspector( - processIdentifier: xcodeApp.processIdentifier, - windowElement: axWindowElement - ) - - // Check if it's a workspace window - if windowInspector.isWorkspaceWindow { - print("✅ Found Xcode workspace window") - focusedWindow = windowInspector - - // Update current file/workspace info - currentDocumentURL = windowInspector.documentURL - currentWorkspaceURL = windowInspector.workspaceURL - currentProjectURL = windowInspector.projectRootURL - } else { - print("📝 Found other Xcode window (not workspace)") - } - } - - private func testAccessibilityAccess() { - guard let xcode = xcodeInstances.first else { return } - - let axApp = AXUIElementCreateApplication(xcode.processIdentifier) - - // Test basic AX access - var focusedElement: AnyObject? - let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement) - - switch result { - case .success: - print("🎯 AX Access Test: ✅ Success - can read focused element") - case .apiDisabled: - print("🚫 AX Access Test: API Disabled") - case .notImplemented: - print("⚠️ AX Access Test: Not Implemented") - default: - print("❓ AX Access Test: Other error - \(result.rawValue)") - } - } - - private func handleApplicationActivated(_ notification: Notification) { - guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } - - print("📱 App activated: \(app.localizedName ?? "Unknown") (PID: \(app.processIdentifier))") - - // Ensure UI updates happen on main thread - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - if app.isXcode { - print("🔄 Xcode activated: PID \(app.processIdentifier)") - self.activeXcode = app - self.lastActiveXcode = app - - if !self.xcodeInstances.contains(where: { $0.processIdentifier == app.processIdentifier }) { - self.xcodeInstances.append(app) - print("📱 Added new Xcode instance: PID \(app.processIdentifier)") - } - } else { - // When another app becomes active, Xcode is no longer active - if let previousActive = self.activeXcode { - print("📱 \(app.localizedName ?? "Unknown app") became active, Xcode (PID: \(previousActive.processIdentifier)) backgrounded") - self.activeXcode = nil - } - } - } - } - - private func handleApplicationTerminated(_ notification: Notification) { - guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } - - if app.isXcode { - print("❌ Xcode terminated: PID \(app.processIdentifier)") - xcodeInstances.removeAll { $0.processIdentifier == app.processIdentifier } - - if activeXcode?.processIdentifier == app.processIdentifier { - activeXcode = xcodeInstances.first(where: \.isActive) - } - } - } } extension NSRunningApplication { diff --git a/AS2/AS2/XcodeWindowInspector.swift b/AS2/AS2/XcodeStrategy.swift similarity index 76% rename from AS2/AS2/XcodeWindowInspector.swift rename to AS2/AS2/XcodeStrategy.swift index 023453ae..d86ddb4f 100644 --- a/AS2/AS2/XcodeWindowInspector.swift +++ b/AS2/AS2/XcodeStrategy.swift @@ -1,40 +1,44 @@ import ApplicationServices +import AppKit import Foundation -/// Window inspector for individual Xcode windows - follows CopilotForXcode patterns -class XcodeWindowInspector { +class XcodeWindowInspector: WindowInspector { let processIdentifier: pid_t let windowElement: AXUIElement - @Published var documentURL: URL? - @Published var workspaceURL: URL? - @Published var projectRootURL: URL? + private(set) var documentURL: URL? + private(set) var workspaceURL: URL? + private(set) var projectURL: URL? init(processIdentifier: pid_t, windowElement: AXUIElement) { self.processIdentifier = processIdentifier self.windowElement = windowElement - - // Initial extraction refresh() } - /// Refresh all URLs from the window func refresh() { documentURL = Self.extractDocumentURL(from: windowElement) workspaceURL = Self.extractWorkspaceURL(from: windowElement) - projectRootURL = Self.extractProjectURL(workspaceURL: workspaceURL, documentURL: documentURL) + projectURL = Self.extractProjectURL(workspaceURL: workspaceURL, documentURL: documentURL) - print("📄 Window refresh:") + print("📄 Xcode window refresh:") print(" 📁 Document: \(documentURL?.lastPathComponent ?? "None")") print(" 📦 Workspace: \(workspaceURL?.lastPathComponent ?? "None")") - print(" 🏗️ Project: \(projectRootURL?.lastPathComponent ?? "None")") + print(" 🏗️ Project: \(projectURL?.lastPathComponent ?? "None")") } - // MARK: - URL Extraction (following CopilotForXcode patterns) + var isMainWorkWindow: Bool { + var identifier: AnyObject? + let result = AXUIElementCopyAttributeValue(windowElement, kAXIdentifierAttribute as CFString, &identifier) + + if result == .success, let id = identifier as? String { + return id == "Xcode.WorkspaceWindow" + } + + return false + } - /// Extract document URL from window element - mirrors CopilotForXcode's extractDocumentURL static func extractDocumentURL(from windowElement: AXUIElement) -> URL? { - // Fetch file path of the frontmost window of Xcode through Accessibility API var documentValue: AnyObject? let result = AXUIElementCopyAttributeValue(windowElement, kAXDocumentAttribute as CFString, &documentValue) @@ -42,14 +46,12 @@ class XcodeWindowInspector { return nil } - // Remove percent encoding and clean up path guard let cleanPath = path.removingPercentEncoding else { return nil } let url = URL(fileURLWithPath: cleanPath.replacingOccurrences(of: "file://", with: "")) return adjustFileURL(url) } - /// Extract workspace URL from window children - mirrors CopilotForXcode's extractWorkspaceURL static func extractWorkspaceURL(from windowElement: AXUIElement) -> URL? { var children: AnyObject? let result = AXUIElementCopyAttributeValue(windowElement, kAXChildrenAttribute as CFString, &children) @@ -58,14 +60,12 @@ class XcodeWindowInspector { return nil } - // Look through children for path descriptions for child in childrenArray { var description: AnyObject? let descResult = AXUIElementCopyAttributeValue(child, kAXDescriptionAttribute as CFString, &description) guard descResult == .success, let desc = description as? String else { continue } - // Check if this looks like a path (starts with "/" and is longer than 1 character) if desc.starts(with: "/"), desc.count > 1 { let trimmedPath = desc.trimmingCharacters(in: .newlines) return URL(fileURLWithPath: trimmedPath) @@ -75,7 +75,6 @@ class XcodeWindowInspector { return nil } - /// Extract project root URL - mirrors CopilotForXcode's extractProjectURL static func extractProjectURL(workspaceURL: URL?, documentURL: URL?) -> URL? { guard var currentURL = workspaceURL ?? documentURL else { return nil } @@ -91,7 +90,6 @@ class XcodeWindowInspector { FileManager.default.fileExists(atPath: currentURL.path, isDirectory: &isDirectory) guard isDirectory.boolValue else { continue } - // Skip Xcode-specific directories guard currentURL.pathExtension != "xcodeproj", currentURL.pathExtension != "xcworkspace", currentURL.pathExtension != "playground" else { continue } @@ -100,7 +98,6 @@ class XcodeWindowInspector { firstDirectoryURL = currentURL } - // Check for .git directory let gitURL = currentURL.appendingPathComponent(".git") var gitIsDirectory: ObjCBool = false @@ -108,7 +105,6 @@ class XcodeWindowInspector { if gitIsDirectory.boolValue { lastGitDirectoryURL = currentURL } else if let gitContent = try? String(contentsOf: gitURL) { - // Check for git worktree if !gitContent.hasPrefix("gitdir: ../") && gitContent.contains("/.git/worktrees/") { lastGitDirectoryURL = currentURL } @@ -119,7 +115,6 @@ class XcodeWindowInspector { return lastGitDirectoryURL ?? firstDirectoryURL ?? workspaceURL } - /// Adjust file URL for special cases - mirrors CopilotForXcode's adjustFileURL static func adjustFileURL(_ url: URL) -> URL { if url.pathExtension == "playground", FileManager.default.fileExists(atPath: url.path) { @@ -131,16 +126,17 @@ class XcodeWindowInspector { } return url } +} + +struct XcodeStrategy: EditorStrategy { + let displayName = "Xcode" + let bundleIdentifier = "com.apple.dt.Xcode" - /// Check if this is a workspace window - var isWorkspaceWindow: Bool { - var identifier: AnyObject? - let result = AXUIElementCopyAttributeValue(windowElement, kAXIdentifierAttribute as CFString, &identifier) - - if result == .success, let id = identifier as? String { - return id == "Xcode.WorkspaceWindow" - } - - return false + func shouldMonitor(_ app: NSRunningApplication) -> Bool { + return app.bundleIdentifier == bundleIdentifier + } + + func createWindowInspector(processId: pid_t, windowElement: AXUIElement) -> WindowInspector? { + return XcodeWindowInspector(processIdentifier: processId, windowElement: windowElement) } -} \ No newline at end of file +} diff --git a/AS2/docs/XCODE_MONITORING_PLAN.md b/AS2/docs/XCODE_MONITORING_PLAN.md index b5dbbf82..232e3a6e 100644 --- a/AS2/docs/XCODE_MONITORING_PLAN.md +++ b/AS2/docs/XCODE_MONITORING_PLAN.md @@ -194,7 +194,7 @@ Following the proven architecture of CopilotForXcode, this document outlines the --- -## 🎯 **Current Status**: Phase 1 Complete → Starting Phase 2 +## 🎯 **Current Status**: Phase 2 Complete + Architecture Refactor → Phase 3 Ready ### **✅ Phase 1 COMPLETED**: - ✅ Removed App Sandbox (`com.apple.security.app-sandbox = false`) @@ -205,11 +205,30 @@ Following the proven architecture of CopilotForXcode, this document outlines the - ✅ Robust active/inactive state tracking with periodic correction - ✅ Thread-safe UI updates with proper MainActor usage -### **🚀 Phase 2 NEXT**: Window & File Path Detection +### **✅ Phase 2 COMPLETED**: +- ✅ Window monitoring for focused Xcode windows +- ✅ File path extraction using CopilotForXcode patterns: + - `extractDocumentURL` from `windowElement.document` + - `extractWorkspaceURL` from window children descriptions + - `extractProjectURL` with git repository detection +- ✅ Real-time UI updates showing current file, workspace, and project +- ✅ Multiple Xcode instance handling +- ✅ Workspace window identification using `Xcode.WorkspaceWindow` identifier + +### **🔄 ADDITIONAL REFACTOR COMPLETED**: +**Strategy Pattern Architecture for Multi-Editor Support** +- ✅ Created generic `AppMonitor` framework with `EditorStrategy` protocol +- ✅ Implemented `XcodeStrategy` following strategy pattern +- ✅ Refactored `XcodeMonitor` to inherit from `AppMonitor` (252→58 lines) +- ✅ Added `WindowInspector` protocol for extensible window analysis +- ✅ Maintained backward compatibility with existing UI +- ✅ Foundation ready for future VSCode, Sublime Text, etc. strategies + +### **🚀 Phase 3 NEXT**: Advanced Window Analysis **Ready to implement**: -1. Monitor focused Xcode windows using AX notifications -2. Extract file paths from window elements (`windowElement.document`) -3. Extract workspace paths from window children -4. Real-time updates when switching files/projects +1. Navigate AX element tree structure for editor areas +2. Identify source editor elements specifically +3. Handle split editors and multiple tabs +4. Robust error recovery and AX element staleness detection This plan ensures we build robust, maintainable Xcode monitoring following proven patterns while allowing for incremental development and testing. \ No newline at end of file From f68f3928df52d454184fe567ff34517087377282 Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Thu, 18 Sep 2025 11:26:09 -0700 Subject: [PATCH 12/13] reorg files & dirs --- AS2/AS2/AS2App.swift | 7 ---- AS2/AS2/Core/AppInstance.swift | 16 ++++++++ AS2/AS2/{ => Core}/AppMonitor.swift | 39 +++---------------- AS2/AS2/Core/Protocols.swift | 20 ++++++++++ .../{ => Editors/Xcode}/XcodeMonitor.swift | 10 ----- AS2/AS2/Editors/Xcode/XcodeStrategy.swift | 16 ++++++++ .../Xcode/XcodeWindowInspector.swift} | 17 +------- .../NSRunningApplication+Extensions.swift | 7 ++++ AS2/docs/XCODE_MONITORING_PLAN.md | 17 ++++++++ 9 files changed, 84 insertions(+), 65 deletions(-) create mode 100644 AS2/AS2/Core/AppInstance.swift rename AS2/AS2/{ => Core}/AppMonitor.swift (90%) create mode 100644 AS2/AS2/Core/Protocols.swift rename AS2/AS2/{ => Editors/Xcode}/XcodeMonitor.swift (84%) create mode 100644 AS2/AS2/Editors/Xcode/XcodeStrategy.swift rename AS2/AS2/{XcodeStrategy.swift => Editors/Xcode/XcodeWindowInspector.swift} (91%) create mode 100644 AS2/AS2/Extensions/NSRunningApplication+Extensions.swift diff --git a/AS2/AS2/AS2App.swift b/AS2/AS2/AS2App.swift index 8071d1a7..c6e875f4 100644 --- a/AS2/AS2/AS2App.swift +++ b/AS2/AS2/AS2App.swift @@ -1,10 +1,3 @@ -// -// AS2App.swift -// AS2 -// -// Created by Chad Parker on 8/31/25. -// - import SwiftUI @main diff --git a/AS2/AS2/Core/AppInstance.swift b/AS2/AS2/Core/AppInstance.swift new file mode 100644 index 00000000..d5f134fe --- /dev/null +++ b/AS2/AS2/Core/AppInstance.swift @@ -0,0 +1,16 @@ +import Cocoa +import Foundation + +struct AppInstance { + let app: NSRunningApplication + let strategy: EditorStrategy + var windowInspector: WindowInspector? + + var displayName: String { + strategy.displayName + } + + var processId: pid_t { + app.processIdentifier + } +} \ No newline at end of file diff --git a/AS2/AS2/AppMonitor.swift b/AS2/AS2/Core/AppMonitor.swift similarity index 90% rename from AS2/AS2/AppMonitor.swift rename to AS2/AS2/Core/AppMonitor.swift index 49aa57f9..b795bfeb 100644 --- a/AS2/AS2/AppMonitor.swift +++ b/AS2/AS2/Core/AppMonitor.swift @@ -2,37 +2,6 @@ import Cocoa import Foundation import ApplicationServices -protocol WindowInspector: AnyObject { - var documentURL: URL? { get } - var workspaceURL: URL? { get } - var projectURL: URL? { get } - var isMainWorkWindow: Bool { get } - - func refresh() -} - -protocol EditorStrategy { - var displayName: String { get } - var bundleIdentifier: String { get } - - func shouldMonitor(_ app: NSRunningApplication) -> Bool - func createWindowInspector(processId: pid_t, windowElement: AXUIElement) -> WindowInspector? -} - -struct AppInstance { - let app: NSRunningApplication - let strategy: EditorStrategy - var windowInspector: WindowInspector? - - var displayName: String { - strategy.displayName - } - - var processId: pid_t { - app.processIdentifier - } -} - @MainActor class AppMonitor: ObservableObject { @Published var monitoredApps: [String: AppInstance] = [:] @@ -91,7 +60,9 @@ class AppMonitor: ObservableObject { object: nil, queue: .main ) { [weak self] notification in - self?.handleApplicationActivated(notification) + Task { @MainActor in + self?.handleApplicationActivated(notification) + } } NotificationCenter.default.addObserver( @@ -99,7 +70,9 @@ class AppMonitor: ObservableObject { object: nil, queue: .main ) { [weak self] notification in - self?.handleApplicationTerminated(notification) + Task { @MainActor in + self?.handleApplicationTerminated(notification) + } } } diff --git a/AS2/AS2/Core/Protocols.swift b/AS2/AS2/Core/Protocols.swift new file mode 100644 index 00000000..d19530b6 --- /dev/null +++ b/AS2/AS2/Core/Protocols.swift @@ -0,0 +1,20 @@ +import Cocoa +import Foundation +import ApplicationServices + +protocol WindowInspector: AnyObject { + var documentURL: URL? { get } + var workspaceURL: URL? { get } + var projectURL: URL? { get } + var isMainWorkWindow: Bool { get } + + func refresh() +} + +protocol EditorStrategy { + var displayName: String { get } + var bundleIdentifier: String { get } + + func shouldMonitor(_ app: NSRunningApplication) -> Bool + func createWindowInspector(processId: pid_t, windowElement: AXUIElement) -> WindowInspector? +} \ No newline at end of file diff --git a/AS2/AS2/XcodeMonitor.swift b/AS2/AS2/Editors/Xcode/XcodeMonitor.swift similarity index 84% rename from AS2/AS2/XcodeMonitor.swift rename to AS2/AS2/Editors/Xcode/XcodeMonitor.swift index f8277be7..7dbea2a5 100644 --- a/AS2/AS2/XcodeMonitor.swift +++ b/AS2/AS2/Editors/Xcode/XcodeMonitor.swift @@ -2,7 +2,6 @@ import Cocoa import Foundation import ApplicationServices -/// Specialized Xcode monitor with enhanced Xcode-specific features class XcodeMonitor: AppMonitor { @Published var xcodeInstances: [NSRunningApplication] = [] @Published var activeXcode: NSRunningApplication? @@ -24,7 +23,6 @@ class XcodeMonitor: AppMonitor { } private func setupXcodeSpecificMonitoring() { - // Subscribe to parent's state changes and maintain Xcode-specific properties $monitoredApps .map { apps in apps.values.compactMap { $0.app.isXcode ? $0.app : nil } } .assign(to: &$xcodeInstances) @@ -49,16 +47,8 @@ class XcodeMonitor: AppMonitor { .compactMap { $0?.windowInspector?.projectURL } .assign(to: &$currentProjectURL) - // Track last active Xcode $activeXcode .compactMap { $0 } .assign(to: &$lastActiveXcode) } - -} - -extension NSRunningApplication { - var isXcode: Bool { - bundleIdentifier == "com.apple.dt.Xcode" - } } \ No newline at end of file diff --git a/AS2/AS2/Editors/Xcode/XcodeStrategy.swift b/AS2/AS2/Editors/Xcode/XcodeStrategy.swift new file mode 100644 index 00000000..ac36813e --- /dev/null +++ b/AS2/AS2/Editors/Xcode/XcodeStrategy.swift @@ -0,0 +1,16 @@ +import ApplicationServices +import AppKit +import Foundation + +struct XcodeStrategy: EditorStrategy { + let displayName = "Xcode" + let bundleIdentifier = "com.apple.dt.Xcode" + + func shouldMonitor(_ app: NSRunningApplication) -> Bool { + return app.bundleIdentifier == bundleIdentifier + } + + func createWindowInspector(processId: pid_t, windowElement: AXUIElement) -> WindowInspector? { + return XcodeWindowInspector(processIdentifier: processId, windowElement: windowElement) + } +} \ No newline at end of file diff --git a/AS2/AS2/XcodeStrategy.swift b/AS2/AS2/Editors/Xcode/XcodeWindowInspector.swift similarity index 91% rename from AS2/AS2/XcodeStrategy.swift rename to AS2/AS2/Editors/Xcode/XcodeWindowInspector.swift index d86ddb4f..5d75641e 100644 --- a/AS2/AS2/XcodeStrategy.swift +++ b/AS2/AS2/Editors/Xcode/XcodeWindowInspector.swift @@ -104,7 +104,7 @@ class XcodeWindowInspector: WindowInspector { if FileManager.default.fileExists(atPath: gitURL.path, isDirectory: &gitIsDirectory) { if gitIsDirectory.boolValue { lastGitDirectoryURL = currentURL - } else if let gitContent = try? String(contentsOf: gitURL) { + } else if let gitContent = try? String(contentsOf: gitURL, encoding: .utf8) { if !gitContent.hasPrefix("gitdir: ../") && gitContent.contains("/.git/worktrees/") { lastGitDirectoryURL = currentURL } @@ -126,17 +126,4 @@ class XcodeWindowInspector: WindowInspector { } return url } -} - -struct XcodeStrategy: EditorStrategy { - let displayName = "Xcode" - let bundleIdentifier = "com.apple.dt.Xcode" - - func shouldMonitor(_ app: NSRunningApplication) -> Bool { - return app.bundleIdentifier == bundleIdentifier - } - - func createWindowInspector(processId: pid_t, windowElement: AXUIElement) -> WindowInspector? { - return XcodeWindowInspector(processIdentifier: processId, windowElement: windowElement) - } -} +} \ No newline at end of file diff --git a/AS2/AS2/Extensions/NSRunningApplication+Extensions.swift b/AS2/AS2/Extensions/NSRunningApplication+Extensions.swift new file mode 100644 index 00000000..67291315 --- /dev/null +++ b/AS2/AS2/Extensions/NSRunningApplication+Extensions.swift @@ -0,0 +1,7 @@ +import Cocoa + +extension NSRunningApplication { + var isXcode: Bool { + bundleIdentifier == "com.apple.dt.Xcode" + } +} \ No newline at end of file diff --git a/AS2/docs/XCODE_MONITORING_PLAN.md b/AS2/docs/XCODE_MONITORING_PLAN.md index 232e3a6e..6d9e5531 100644 --- a/AS2/docs/XCODE_MONITORING_PLAN.md +++ b/AS2/docs/XCODE_MONITORING_PLAN.md @@ -224,6 +224,23 @@ Following the proven architecture of CopilotForXcode, this document outlines the - ✅ Maintained backward compatibility with existing UI - ✅ Foundation ready for future VSCode, Sublime Text, etc. strategies +### **🏗️ ARCHITECTURE RESTRUCTURE COMPLETED**: +**Improved Project Organization for Better Maintainability** +- ✅ **Core/**: Framework abstractions separated from implementations + - `Protocols.swift`: EditorStrategy, WindowInspector protocols + - `AppInstance.swift`: Shared data structures + - `AppMonitor.swift`: Generic monitoring engine (clean, focused) +- ✅ **Editors/Xcode/**: Complete Xcode implementation namespace + - `XcodeStrategy.swift`: Clean 13-line strategy implementation + - `XcodeWindowInspector.swift`: 129 lines of focused window analysis + - `XcodeMonitor.swift`: Specialized monitor with Xcode-specific features +- ✅ **Extensions/**: Shared utilities and extensions + - `NSRunningApplication+Extensions.swift`: isXcode property +- ✅ **Single Responsibility**: Each file has one clear, focused purpose +- ✅ **Future-Proof**: Adding new editors (VS Code, JetBrains) is now trivial +- ✅ **Zero Build Warnings**: Clean compilation with proper async/await patterns +- ✅ **Better Testability**: Clear boundaries for unit testing each component + ### **🚀 Phase 3 NEXT**: Advanced Window Analysis **Ready to implement**: 1. Navigate AX element tree structure for editor areas From 84ee3b0651696e05a49f937e53f9f85cdb7cfffe Mon Sep 17 00:00:00 2001 From: Chad Parker Date: Fri, 26 Sep 2025 17:21:52 -0700 Subject: [PATCH 13/13] temp delete plan (to avoid poisoning evaluations) --- AS2/docs/XCODE_MONITORING_PLAN.md | 251 ------------------------------ 1 file changed, 251 deletions(-) delete mode 100644 AS2/docs/XCODE_MONITORING_PLAN.md diff --git a/AS2/docs/XCODE_MONITORING_PLAN.md b/AS2/docs/XCODE_MONITORING_PLAN.md deleted file mode 100644 index 6d9e5531..00000000 --- a/AS2/docs/XCODE_MONITORING_PLAN.md +++ /dev/null @@ -1,251 +0,0 @@ -# AS2 Xcode Monitoring Implementation Plan - -Following the proven architecture of CopilotForXcode, this document outlines the phased approach to implementing comprehensive Xcode monitoring capabilities. - -## 🏗️ **Overall Architecture** - -**Based on CopilotForXcode's successful pattern:** -- **Pure Accessibility API approach** (no AppleScript) -- **Unsandboxed application** for full system access -- **Real-time AX notification streams** for instant updates -- **Incremental implementation** - build up capabilities gradually - ---- - -## 📋 **Implementation Phases** - -### **Phase 1: Foundation & Real-time Monitoring** ⭐ *Next* -**Goal**: Remove sandbox, add accessibility foundation, improve Xcode detection - -**Tasks**: -1. **Remove App Sandbox** - - Update `AS2.entitlements`: `com.apple.security.app-sandbox = false` - - Test basic functionality still works - -2. **Add Accessibility Foundation** - - Import required frameworks (`ApplicationServices`, `Cocoa`) - - Add basic AXUIElement creation for Xcode apps - - Handle accessibility permission requests - -3. **Improve Xcode Detection** - - Fix active/background state detection - - Add better real-time app state updates - - Show Xcode version information - -4. **Basic AX Health Check** - - Test AXUIElement creation for detected Xcode instances - - Display basic accessibility status in UI - - Handle permission denied gracefully - -**Success Criteria**: -- ✅ App runs without sandbox restrictions -- ✅ Detects Xcode instances accurately -- ✅ Shows accessibility permission status -- ✅ Creates AXUIElements for Xcode apps - ---- - -### **Phase 2: Window & File Path Detection** -**Goal**: Monitor Xcode windows and extract file paths - -**Tasks**: -1. **Window Monitoring** - - Detect focused Xcode windows - - Identify workspace vs other window types - - Track window changes in real-time - -2. **File Path Extraction** (following CopilotForXcode patterns) - - `extractDocumentURL`: Get current file from `windowElement.document` - - `extractWorkspaceURL`: Parse workspace path from window children - - `extractProjectURL`: Derive project root from workspace/document - -3. **Real-time Updates** - - AX notification streams for window focus changes - - Update UI when active document changes - - Handle multiple Xcode windows - -**Success Criteria**: -- ✅ Shows current active file path -- ✅ Shows workspace path -- ✅ Updates in real-time when switching files -- ✅ Handles multiple Xcode instances - ---- - -### **Phase 3: Advanced Window Analysis** -**Goal**: Deep window inspection and UI element traversal - -**Tasks**: -1. **Window Element Hierarchy** - - Navigate AX element tree structure - - Identify editor areas vs other UI elements - - Find source editor elements specifically - -2. **Multiple Window Support** - - Track all open Xcode windows per instance - - Handle split editors and multiple tabs - - Project context awareness - -3. **Robustness Features** - - Accessibility API malfunction detection - - Auto-recovery when AX elements become stale - - Error handling for permission changes - -**Success Criteria**: -- ✅ Identifies source editor elements -- ✅ Handles complex window layouts -- ✅ Robust error recovery - ---- - -### **Phase 4: Editor Content Access** -**Goal**: Read editor content, cursor position, selections - -**Tasks**: -1. **Editor Content Reading** - - Get full text content via `kAXValueAttribute` - - Read selected text ranges - - Parse content into lines - -2. **Cursor & Selection Tracking** - - Real-time cursor position updates - - Selection range detection - - Convert between different coordinate systems - -3. **Performance Optimization** - - Implement content caching (like CopilotForXcode's `Cache` class) - - Efficient line-based range conversions - - Debounced updates - -**Success Criteria**: -- ✅ Displays current editor content -- ✅ Shows cursor position and selections -- ✅ Real-time updates without performance issues - ---- - -### **Phase 5: Advanced Features** -**Goal**: Full feature parity with monitoring capabilities - -**Tasks**: -1. **Comprehensive State Tracking** - - Completion panel detection - - Line annotations and error markers - - Scroll position tracking - -2. **Multi-Workspace Management** - - Track all open workspaces per Xcode instance - - Workspace-specific context - - Tab enumeration and tracking - -3. **Integration Ready** - - Clean APIs for external tools - - Event streaming for other components - - Extensible architecture - ---- - -## 🔧 **Technical Implementation Notes** - -### **Key CopilotForXcode Patterns to Follow**: - -1. **AXUIElement Management**: - ```swift - let app = AXUIElementCreateApplication(processIdentifier) - app.setMessagingTimeout(2) - ``` - -2. **Window Identification**: - ```swift - window.identifier == "Xcode.WorkspaceWindow" - ``` - -3. **File Path Extraction**: - ```swift - let path = windowElement.document // for current file - // Parse children descriptions for workspace path - ``` - -4. **Real-time Updates**: - ```swift - AXNotificationStream( - app: runningApplication, - notificationNames: kAXFocusedUIElementChangedNotification, - kAXTitleChangedNotification, ... - ) - ``` - -5. **Error Recovery**: - - Global actor isolation (`@XcodeInspectorActor`) - - Malfunction detection and auto-restart - - Graceful degradation when AX fails - -### **Required Entitlements**: -```xml -com.apple.security.app-sandbox - - -``` - -### **User Permission Required**: -- **System Preferences > Security & Privacy > Accessibility** -- App must be explicitly granted permission by user -- Handle permission denied gracefully - ---- - -## 🎯 **Current Status**: Phase 2 Complete + Architecture Refactor → Phase 3 Ready - -### **✅ Phase 1 COMPLETED**: -- ✅ Removed App Sandbox (`com.apple.security.app-sandbox = false`) -- ✅ Added Accessibility Foundation (ApplicationServices framework) -- ✅ Implemented accessibility permission handling with auto-prompt -- ✅ Fixed Xcode detection using `workspace.frontmostApplication` -- ✅ Added basic AXUIElement creation and testing -- ✅ Robust active/inactive state tracking with periodic correction -- ✅ Thread-safe UI updates with proper MainActor usage - -### **✅ Phase 2 COMPLETED**: -- ✅ Window monitoring for focused Xcode windows -- ✅ File path extraction using CopilotForXcode patterns: - - `extractDocumentURL` from `windowElement.document` - - `extractWorkspaceURL` from window children descriptions - - `extractProjectURL` with git repository detection -- ✅ Real-time UI updates showing current file, workspace, and project -- ✅ Multiple Xcode instance handling -- ✅ Workspace window identification using `Xcode.WorkspaceWindow` identifier - -### **🔄 ADDITIONAL REFACTOR COMPLETED**: -**Strategy Pattern Architecture for Multi-Editor Support** -- ✅ Created generic `AppMonitor` framework with `EditorStrategy` protocol -- ✅ Implemented `XcodeStrategy` following strategy pattern -- ✅ Refactored `XcodeMonitor` to inherit from `AppMonitor` (252→58 lines) -- ✅ Added `WindowInspector` protocol for extensible window analysis -- ✅ Maintained backward compatibility with existing UI -- ✅ Foundation ready for future VSCode, Sublime Text, etc. strategies - -### **🏗️ ARCHITECTURE RESTRUCTURE COMPLETED**: -**Improved Project Organization for Better Maintainability** -- ✅ **Core/**: Framework abstractions separated from implementations - - `Protocols.swift`: EditorStrategy, WindowInspector protocols - - `AppInstance.swift`: Shared data structures - - `AppMonitor.swift`: Generic monitoring engine (clean, focused) -- ✅ **Editors/Xcode/**: Complete Xcode implementation namespace - - `XcodeStrategy.swift`: Clean 13-line strategy implementation - - `XcodeWindowInspector.swift`: 129 lines of focused window analysis - - `XcodeMonitor.swift`: Specialized monitor with Xcode-specific features -- ✅ **Extensions/**: Shared utilities and extensions - - `NSRunningApplication+Extensions.swift`: isXcode property -- ✅ **Single Responsibility**: Each file has one clear, focused purpose -- ✅ **Future-Proof**: Adding new editors (VS Code, JetBrains) is now trivial -- ✅ **Zero Build Warnings**: Clean compilation with proper async/await patterns -- ✅ **Better Testability**: Clear boundaries for unit testing each component - -### **🚀 Phase 3 NEXT**: Advanced Window Analysis -**Ready to implement**: -1. Navigate AX element tree structure for editor areas -2. Identify source editor elements specifically -3. Handle split editors and multiple tabs -4. Robust error recovery and AX element staleness detection - -This plan ensures we build robust, maintainable Xcode monitoring following proven patterns while allowing for incremental development and testing. \ No newline at end of file