Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions Cotabby/App/Coordinators/SuggestionCoordinator+Input.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

func handleFocusSnapshotChange(_ snapshot: FocusSnapshot) {
CotabbyLogger.suggestion.trace(
"Focus snapshot changed: app=\(snapshot.applicationName) capability=\(snapshot.capability.shortLabel) \(focusDiagnostics(for: snapshot))"

Check warning on line 28 in Cotabby/App/Coordinators/SuggestionCoordinator+Input.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line should be 140 characters or less; currently it has 149 characters (line_length)
)
// Start capturing visual context for a newly focused input even when predictions are
// temporarily disabled by transient field states (e.g., "text is selected" or "secure
Expand Down Expand Up @@ -132,15 +132,31 @@
}

if event.shouldSchedulePrediction {
// Capture AX state immediately at keystroke time so the debounce window
// works with the freshest possible snapshot, not whenever the poll timer last fired.
focusModel.refreshNow()
schedulePrediction()
scheduleDeferredFocusRefreshAndPrediction()
}

return false
}

/// Refreshes the AX focus snapshot and schedules a prediction *off* the synchronous
/// event-tap callback.
///
/// `FocusTracker.refreshNow()` performs a blocking Accessibility tree walk, and the keyboard
/// monitor is an active session event tap that the system routes the live input stream through
/// and waits on. Doing the AX read inline stalls every keystroke behind it, and against an
/// unresponsive app it can exceed the tap deadline, at which point macOS disables the tap and
/// drops the in-flight events queued behind it. Lost key-ups strand held keys (movement keys in
/// games keep firing) and desync modifier shortcuts. Hopping to the next main-actor turn lets
/// the keystroke flow through untouched; the debounced generation re-reads focus anyway
/// (`generateFromCurrentFocus`), so nothing downstream depends on this running inline.
func scheduleDeferredFocusRefreshAndPrediction() {
Task { @MainActor [weak self] in
guard let self else { return }
self.focusModel.refreshNow()
self.schedulePrediction()
}
}
Comment on lines +152 to +158
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Method should be private

scheduleDeferredFocusRefreshAndPrediction is only called from within this file, yet it sits in the non-private extension and is therefore visible at module scope. All three call sites are in the same file, and the existing internal-only formatting helpers (focusDiagnostics, formatRect) already live in the private extension block below. Moving this method there (or adding an explicit private modifier) keeps the public surface of SuggestionCoordinator consistent with the existing convention and avoids accidental callers in other coordinator files.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code


func handleSuppressedSyntheticInput() {
logStage(
"suppressed-synthetic-input",
Expand All @@ -165,8 +181,7 @@
clearDiagnostics: false
)
if event.shouldSchedulePrediction {
focusModel.refreshNow()
schedulePrediction()
scheduleDeferredFocusRefreshAndPrediction()
}
return false

Expand All @@ -176,8 +191,7 @@
clearDiagnostics: false
)
if event.shouldSchedulePrediction {
focusModel.refreshNow()
schedulePrediction()
scheduleDeferredFocusRefreshAndPrediction()
}
return false

Expand Down
Loading