Skip to content
This repository was archived by the owner on Nov 17, 2025. It is now read-only.

Commit dbcffe1

Browse files
steipeteclaude
andcommitted
Replace deprecated CGWindowListCreateImage with modern ScreenCaptureKit API
- Use SCShareableContent and SCScreenshotManager for window capture - Remove deprecated CGWindowListCreateImage methods - Maintain same OCR functionality with modern API - Simplify window detection using SCWindow properties - Fix deprecation warnings for macOS 14+ - Set Claude monitoring interval to 5s to match other monitors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 687c878 commit dbcffe1

1 file changed

Lines changed: 78 additions & 128 deletions

File tree

Features/Monitoring/Domain/Services/ClaudeMonitorService.swift

Lines changed: 78 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Diagnostics
33
import Defaults
44
import AppKit
55
import Vision
6-
import ScreenCaptureKit
6+
@preconcurrency import ScreenCaptureKit
77
import Darwin
88
import CoreImage
99

@@ -68,7 +68,7 @@ public final class ClaudeMonitorService: ObservableObject, Sendable {
6868
monitoringTask = Task { [weak self] in
6969
while !Task.isCancelled {
7070
await self?.scanForClaudeInstances()
71-
try? await Task.sleep(for: .seconds(10)) // Reduced from 3s to 10s to save CPU
71+
try? await Task.sleep(for: .seconds(5)) // Match other monitoring intervals
7272
}
7373
}
7474
}
@@ -627,165 +627,115 @@ public final class ClaudeMonitorService: ObservableObject, Sendable {
627627
private nonisolated func getTerminalContentViaImprovedOCRForInstance(_ instance: ClaudeInstance) async -> String? {
628628
logger.info("Attempting improved OCR for Claude instance PID \(instance.pid) in \(instance.folderName)")
629629

630-
// Check if we have screen recording permission
631-
let hasPermission = CGPreflightScreenCaptureAccess()
632-
if !hasPermission {
633-
logger.debug("No screen recording permission for OCR - requesting access")
634-
_ = CGRequestScreenCaptureAccess()
635-
return nil
636-
}
637-
638-
// Get list of all windows
639-
guard let windowList = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] else {
640-
logger.debug("Failed to get window list for screen capture")
641-
return nil
642-
}
643-
644-
// Look for terminal windows that match our specific instance
645-
for window in windowList {
646-
if let windowName = window[kCGWindowName as String] as? String,
647-
let ownerName = window[kCGWindowOwnerName as String] as? String,
648-
let bounds = window[kCGWindowBounds as String] as? [String: Any],
649-
let windowID = window[kCGWindowNumber as String] as? CGWindowID {
630+
do {
631+
// Use ScreenCaptureKit to get available windows
632+
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
633+
634+
// Look for terminal windows that match our specific instance
635+
for window in content.windows {
636+
guard let windowTitle = window.title,
637+
let appName = window.owningApplication?.applicationName else { continue }
650638

651639
// Check if this looks like a terminal window
652-
let isTerminal = ownerName.lowercased().contains("ghostty") ||
653-
ownerName.lowercased().contains("terminal") ||
654-
ownerName.lowercased().contains("iterm") ||
655-
ownerName.lowercased().contains("warp")
640+
let isTerminal = appName.lowercased().contains("ghostty") ||
641+
appName.lowercased().contains("terminal") ||
642+
appName.lowercased().contains("iterm") ||
643+
appName.lowercased().contains("warp")
656644

657645
// Check if this window might contain our specific Claude instance
658646
// Look for folder name or working directory in the window title
659-
let matchesInstance = windowName.contains(instance.folderName) ||
660-
windowName.contains(instance.workingDirectory) ||
661-
windowName.contains("Claude") // Generic fallback
647+
let matchesInstance = windowTitle.contains(instance.folderName) ||
648+
windowTitle.contains(instance.workingDirectory) ||
649+
windowTitle.contains("Claude") // Generic fallback
662650

663651
if isTerminal && matchesInstance {
664-
logger.info("Found matching terminal window: '\(windowName)' (\(ownerName)) for \(instance.folderName)")
652+
logger.info("Found matching terminal window: '\(windowTitle)' (\(appName)) for \(instance.folderName)")
665653

666-
// Move heavy processing to background thread
667-
if let status = await captureWindowAndExtractImprovedTextAsync(windowID: windowID, bounds: bounds) {
654+
// Capture window using modern ScreenCaptureKit API
655+
if let status = await captureWindowAndExtractTextModern(window: window) {
668656
return status
669657
}
670658
}
671659
}
660+
661+
logger.debug("No matching terminal windows found for instance \(instance.folderName)")
662+
return nil
663+
} catch {
664+
logger.error("Failed to get window content via ScreenCaptureKit: \(error)")
665+
return nil
672666
}
673-
674-
logger.debug("No matching terminal windows found for instance \(instance.folderName)")
675-
return nil
676667
}
677668

678669
private nonisolated func getTerminalContentViaImprovedOCR(pid: Int32) async -> String? {
679670
logger.info("Attempting improved OCR for Claude PID \(pid)")
680671

681-
// Check if we have screen recording permission
682-
let hasPermission = CGPreflightScreenCaptureAccess()
683-
if !hasPermission {
684-
logger.debug("No screen recording permission for OCR - requesting access")
685-
_ = CGRequestScreenCaptureAccess()
686-
return nil
687-
}
688-
689-
// Get list of all windows
690-
guard let windowList = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] else {
691-
logger.debug("Failed to get window list for screen capture")
692-
return nil
693-
}
694-
695-
// Look for terminal windows
696-
for window in windowList {
697-
if let windowName = window[kCGWindowName as String] as? String,
698-
let ownerName = window[kCGWindowOwnerName as String] as? String,
699-
let bounds = window[kCGWindowBounds as String] as? [String: Any],
700-
let windowID = window[kCGWindowNumber as String] as? CGWindowID {
672+
do {
673+
// Use ScreenCaptureKit to get available windows
674+
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
675+
676+
// Look for terminal windows
677+
for window in content.windows {
678+
guard let windowTitle = window.title,
679+
let appName = window.owningApplication?.applicationName else { continue }
701680

702681
// Check if this looks like a terminal window
703-
let isTerminal = ownerName.lowercased().contains("ghostty") ||
704-
ownerName.lowercased().contains("terminal") ||
705-
ownerName.lowercased().contains("iterm") ||
706-
ownerName.lowercased().contains("warp")
682+
let isTerminal = appName.lowercased().contains("ghostty") ||
683+
appName.lowercased().contains("terminal") ||
684+
appName.lowercased().contains("iterm") ||
685+
appName.lowercased().contains("warp")
707686

708687
if isTerminal {
709-
logger.info("Found terminal window: '\(windowName)' (\(ownerName))")
688+
logger.info("Found terminal window: '\(windowTitle)' (\(appName))")
710689

711-
// Move heavy processing to background thread
712-
if let status = await captureWindowAndExtractImprovedTextAsync(windowID: windowID, bounds: bounds) {
690+
// Capture window using modern ScreenCaptureKit API
691+
if let status = await captureWindowAndExtractTextModern(window: window) {
713692
return status
714693
}
715694
}
716695
}
717-
}
718-
719-
logger.debug("No terminal windows found for screen capture")
720-
return nil
721-
}
722-
723-
private nonisolated func captureWindowAndExtractImprovedText(windowID: CGWindowID, bounds: [String: Any]) -> String? {
724-
// Capture the window
725-
guard let image = CGWindowListCreateImage(
726-
CGRect.null,
727-
.optionIncludingWindow,
728-
windowID,
729-
[]
730-
) else {
731-
logger.debug("Failed to capture window \(windowID)")
696+
697+
logger.debug("No terminal windows found for screen capture")
732698
return nil
733-
}
734-
735-
logger.debug("Captured window \(windowID), size: \(image.width)x\(image.height)")
736-
737-
// Preprocess the image for better OCR results
738-
guard let preprocessedImage = preprocessImageForOCR(image) else {
739-
logger.debug("Failed to preprocess image")
699+
} catch {
700+
logger.error("Failed to get window content via ScreenCaptureKit: \(error)")
740701
return nil
741702
}
742-
743-
// Convert to NSImage for Vision framework
744-
let nsImage = NSImage(cgImage: preprocessedImage, size: NSSize(width: preprocessedImage.width, height: preprocessedImage.height))
745-
746-
// Use Vision framework with improved settings
747-
return extractTextFromImageWithImprovedSettings(nsImage)
748703
}
749704

750-
private nonisolated func captureWindowAndExtractImprovedTextAsync(windowID: CGWindowID, bounds: [String: Any]) async -> String? {
751-
// Move the heavy image processing to a background queue
752-
return await withCheckedContinuation { (continuation: CheckedContinuation<String?, Never>) in
753-
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
754-
guard let self = self else {
755-
continuation.resume(returning: nil)
756-
return
757-
}
758-
759-
// Capture the window (this is fast)
760-
guard let image = CGWindowListCreateImage(
761-
CGRect.null,
762-
.optionIncludingWindow,
763-
windowID,
764-
[]
765-
) else {
766-
logger.debug("Failed to capture window \(windowID)")
767-
continuation.resume(returning: nil)
768-
return
769-
}
770-
771-
logger.debug("Captured window \(windowID) on background thread, size: \(image.width)x\(image.height)")
772-
773-
// Preprocess the image for better OCR results (CPU intensive)
774-
guard let preprocessedImage = self.preprocessImageForOCR(image) else {
775-
logger.debug("Failed to preprocess image")
776-
continuation.resume(returning: nil)
777-
return
778-
}
779-
780-
// Convert to NSImage for Vision framework
781-
let nsImage = NSImage(cgImage: preprocessedImage, size: NSSize(width: preprocessedImage.width, height: preprocessedImage.height))
782-
783-
// Use Vision framework with improved settings (CPU intensive)
784-
let result = self.extractTextFromImageWithImprovedSettings(nsImage)
785-
786-
logger.debug("OCR processing completed on background thread")
787-
continuation.resume(returning: result)
705+
private nonisolated func captureWindowAndExtractTextModern(window: SCWindow) async -> String? {
706+
do {
707+
// Configure capture settings
708+
let configuration = SCStreamConfiguration()
709+
configuration.width = Int(window.frame.width)
710+
configuration.height = Int(window.frame.height)
711+
configuration.scalesToFit = true
712+
configuration.showsCursor = false
713+
714+
// Create content filter for the specific window
715+
let filter = SCContentFilter(desktopIndependentWindow: window)
716+
717+
// Capture the window using modern ScreenCaptureKit API
718+
let image = try await SCScreenshotManager.captureImage(
719+
contentFilter: filter,
720+
configuration: configuration
721+
)
722+
723+
logger.debug("Captured window '\(window.title ?? "unknown")' using ScreenCaptureKit, size: \(image.width)x\(image.height)")
724+
725+
// Preprocess the image for better OCR results
726+
guard let preprocessedImage = preprocessImageForOCR(image) else {
727+
logger.debug("Failed to preprocess image")
728+
return nil
788729
}
730+
731+
// Convert to NSImage for Vision framework
732+
let nsImage = NSImage(cgImage: preprocessedImage, size: NSSize(width: preprocessedImage.width, height: preprocessedImage.height))
733+
734+
// Use Vision framework with improved settings
735+
return extractTextFromImageWithImprovedSettings(nsImage)
736+
} catch {
737+
logger.error("Failed to capture window using ScreenCaptureKit: \(error)")
738+
return nil
789739
}
790740
}
791741

0 commit comments

Comments
 (0)