Skip to content

Commit 587ea1d

Browse files
grokifyclaude
andcommitted
fix(desktop): add @mainactor thread safety to InputMonitor
Add proper thread safety to InputMonitor by using @mainactor isolation instead of @unchecked Sendable with unprotected mutable dictionaries. Changes: - InputMonitor is now @mainactor isolated - detectors array is explicitly nonisolated (immutable, safe) - init is nonisolated for use as default parameter - TerminalViewRepresentable.checkForInputPrompts() is @mainactor - Timer callback wraps call in Task { @mainactor in ... } - Add content hash caching to skip redundant input detection This ensures compile-time guarantees that InputMonitor state is only accessed from the main thread, preventing potential data races. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f987c68 commit 587ea1d

2 files changed

Lines changed: 30 additions & 9 deletions

File tree

apps/desktop/Sources/PlexusOneDesktop/Services/InputMonitor.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ import UserNotifications
66

77
/// Monitors terminal content for input prompts from AI assistants.
88
/// Uses AssistantKit's pattern detectors to identify when the user needs to respond.
9+
///
10+
/// Thread Safety: This class is `@MainActor` isolated since it:
11+
/// - Updates UI-observable state (`activeAlerts`, `activeSuggestedActions`)
12+
/// - Posts NotificationCenter notifications
13+
/// - Interacts with AppKit (NSSound) and UserNotifications
14+
@MainActor
915
@Observable
10-
final class InputMonitor: @unchecked Sendable {
16+
final class InputMonitor {
1117
/// Active alerts by session ID
1218
private(set) var activeAlerts: [UUID: DetectionResult] = [:]
1319

1420
/// Suggested actions for active alerts
1521
private(set) var activeSuggestedActions: [UUID: [SuggestedAction]] = [:]
1622

1723
/// Detectors to use for scanning terminal content
18-
private let detectors: [any InputDetector]
24+
private nonisolated let detectors: [any InputDetector]
1925

2026
/// Whether to show macOS notifications
2127
var enableNotifications: Bool = false
@@ -26,7 +32,9 @@ final class InputMonitor: @unchecked Sendable {
2632
/// Minimum confidence threshold for alerts
2733
var confidenceThreshold: Double = 0.7
2834

29-
init(requestNotifications: Bool = true) {
35+
/// Initialize InputMonitor.
36+
/// - Parameter requestNotifications: Whether to request notification permissions (set to false in tests)
37+
nonisolated init(requestNotifications: Bool = true) {
3038
self.detectors = [
3139
ClaudeDetector(),
3240
KiroDetector(),
@@ -35,7 +43,9 @@ final class InputMonitor: @unchecked Sendable {
3543

3644
// Request notification permission if needed (skip in test environments)
3745
if requestNotifications {
38-
requestNotificationPermission()
46+
Task { @MainActor in
47+
self.requestNotificationPermission()
48+
}
3949
}
4050
}
4151

apps/desktop/Sources/PlexusOneDesktop/Views/TerminalViewRepresentable.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ struct AppTerminalViewRepresentable: NSViewRepresentable {
178178
var scrollMonitor: Any?
179179
var inputDetectionTimer: Timer?
180180

181+
/// Cache last content hash to skip redundant input detection
182+
private var lastContentHash: Int = 0
183+
181184
init(_ parent: AppTerminalViewRepresentable) {
182185
self.parent = parent
183186
}
@@ -192,12 +195,17 @@ struct AppTerminalViewRepresentable: NSViewRepresentable {
192195
/// Start periodic input detection and focus checking
193196
func startInputDetection() {
194197
// Poll for input prompts and focus state every 500ms
198+
// Timer fires on main thread, checkForInputPrompts is @MainActor
195199
inputDetectionTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
196-
self?.checkForInputPrompts()
197-
self?.containerView?.updateFocusState()
200+
guard let self = self else { return }
201+
Task { @MainActor in
202+
self.checkForInputPrompts()
203+
}
204+
self.containerView?.updateFocusState()
198205
}
199206
}
200207

208+
@MainActor
201209
private func checkForInputPrompts() {
202210
guard let terminalView = terminalView,
203211
let sessionId = terminalView.attachedSessionId() else {
@@ -208,6 +216,11 @@ struct AppTerminalViewRepresentable: NSViewRepresentable {
208216
let content = extractRecentContent(from: terminalView, lineCount: 15)
209217
guard !content.isEmpty else { return }
210218

219+
// Skip processing if content hasn't changed (performance optimization)
220+
let contentHash = content.hashValue
221+
guard contentHash != lastContentHash else { return }
222+
lastContentHash = contentHash
223+
211224
// Get cursor position
212225
guard let terminal = terminalView.terminal else { return }
213226
let cursor = terminal.getCursorLocation()
@@ -222,9 +235,7 @@ struct AppTerminalViewRepresentable: NSViewRepresentable {
222235

223236
// Notify parent if input detected
224237
if let result = parent.inputMonitor.alert(for: sessionId) {
225-
DispatchQueue.main.async { [weak self] in
226-
self?.parent.onInputDetected?(result)
227-
}
238+
parent.onInputDetected?(result)
228239
}
229240
}
230241

0 commit comments

Comments
 (0)