107 lines · 4.4 KB · Swift
RawDownload
#if os(macOS)import AppKitimport ApplicationServicesimport Foundation public enum SessionFocusResult: Equatable, Sendable {    case focused    case activatedApplicationOnly    case failed} @MainActorpublic enum SessionWindowFocuser {    private static let knownBundleIdentifiers: Set<String> = [        "com.mitchellh.ghostty",        "com.googlecode.iterm2",        "com.apple.Terminal",        "dev.warp.Warp-Stable",        "com.github.wez.wezterm",        "net.kovidgoyal.kitty",        "org.alacritty",        "com.microsoft.VSCode",        "com.todesktop.230313mzl4w4u92",        "dev.zed.Zed",        "com.anthropic.claudefordesktop",    ]     @discardableResult    public static func focus(_ session: AgentSession, promptForAccessibility: Bool = true) -> SessionFocusResult {        guard let application = self.application(for: session) else { return .failed }        guard application.activate() else { return .failed }         let trusted = AXIsProcessTrustedWithOptions(            ["AXTrustedCheckOptionPrompt": promptForAccessibility] as CFDictionary)        guard trusted else { return .activatedApplicationOnly }         let appElement = AXUIElementCreateApplication(application.processIdentifier)        var windowsValue: CFTypeRef?        guard AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsValue) == .success,              let windows = windowsValue as? [AXUIElement],              let window = self.preferredWindow(windows, session: session) ?? windows.first        else { return .activatedApplicationOnly }        AXUIElementPerformAction(window, kAXRaiseAction as CFString)        return .focused    }     private static func application(for session: AgentSession) -> NSRunningApplication? {        if let pid = session.pid {            var currentPID = pid            var fallback: NSRunningApplication?            var visited = Set<Int32>()            while currentPID > 0, visited.insert(currentPID).inserted {                if let application = NSRunningApplication(processIdentifier: currentPID) {                    fallback = fallback ?? application                    if let bundleIdentifier = application.bundleIdentifier,                       self.knownBundleIdentifiers.contains(bundleIdentifier)                    {                        return application                    }                }                guard let parent = self.parentPID(of: currentPID), parent != currentPID else { break }                currentPID = parent            }            if let fallback {                return fallback            }        }         let bundleIdentifier: String? = switch (session.provider, session.source) {        case (.claude, .desktopApp): "com.anthropic.claudefordesktop"        case (.codex, .desktopApp): "com.openai.codex"        default: nil        }        guard let bundleIdentifier else { return nil }        return NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).first    }     private static func preferredWindow(_ windows: [AXUIElement], session: AgentSession) -> AXUIElement? {        let candidates = [session.projectName, session.cwd.map { URL(fileURLWithPath: $0).lastPathComponent }]            .compactMap { $0?.lowercased() }            .filter { !$0.isEmpty }        guard !candidates.isEmpty else { return nil }        return windows.first { window in            var titleValue: CFTypeRef?            guard AXUIElementCopyAttributeValue(window, kAXTitleAttribute as CFString, &titleValue) == .success,                  let title = titleValue as? String            else { return false }            let lowercasedTitle = title.lowercased()            return candidates.contains { lowercasedTitle.contains($0) }        }    }     private static func parentPID(of pid: Int32) -> Int32? {        let process = Process()        process.executableURL = URL(fileURLWithPath: "/bin/ps")        process.arguments = ["-o", "ppid=", "-p", String(pid)]        let pipe = Pipe()        process.standardOutput = pipe        process.standardError = FileHandle.nullDevice        guard (try? process.run()) != nil else { return nil }        process.waitUntilExit()        let data = pipe.fileHandleForReading.readDataToEndOfFile()        guard let output = String(data: data, encoding: .utf8) else { return nil }        return Int32(output.trimmingCharacters(in: .whitespacesAndNewlines))    }}#endif