chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
public enum SessionFocusResult: Equatable, Sendable {
|
||||
case focused
|
||||
case activatedApplicationOnly
|
||||
case failed
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public 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
|
||||
Reference in New Issue
Block a user