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
16 changes: 11 additions & 5 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,9 +132,13 @@
}

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()
// Deliberately do NOT refresh focus here. `handleInputEvent` runs inside the synchronous
// CGEvent tap callback, and macOS withholds the keystroke from the focused app until the
// callback returns (see `InputMonitor.handleTap`). A full AX resolve on this path can take
// tens of milliseconds in complex browser trees — worst when the caret is mid-text and the
// resolver falls back to the deep-tree caret walk — which the user feels directly as typing
// lag. The debounced `generateFromCurrentFocus` re-reads focus at fire time, and the 80ms
// poll keeps the snapshot warm, so scheduling alone is enough and stays off the hot path.
schedulePrediction()
}

Expand Down Expand Up @@ -165,7 +169,9 @@
clearDiagnostics: false
)
if event.shouldSchedulePrediction {
focusModel.refreshNow()
// No synchronous focus refresh here: this runs inside the event tap callback and
// would stall the keystroke. `generateFromCurrentFocus` re-reads focus after the
// debounce. See the matching note in `handleInputEvent(_:)`.
schedulePrediction()
}
return false
Expand All @@ -176,7 +182,7 @@
clearDiagnostics: false
)
if event.shouldSchedulePrediction {
focusModel.refreshNow()
// See the note above: keep the AX resolve off the synchronous event-tap path.
schedulePrediction()
}
return false
Expand Down
Loading