@@ -3,7 +3,7 @@ import Diagnostics
33import Defaults
44import AppKit
55import Vision
6- import ScreenCaptureKit
6+ @ preconcurrency import ScreenCaptureKit
77import Darwin
88import 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