Skip to content

fix: hide dictation overlay from VoiceOver and fast-path system shortcuts (#401)#402

Open
postoso wants to merge 3 commits into
altic-dev:mainfrom
postoso:fix/401-overlay-accessibility-shortcut-latency
Open

fix: hide dictation overlay from VoiceOver and fast-path system shortcuts (#401)#402
postoso wants to merge 3 commits into
altic-dev:mainfrom
postoso:fix/401-overlay-accessibility-shortcut-latency

Conversation

@postoso

@postoso postoso commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #401.

Two FluidVoice-side fixes, working from @Alshekhi's patch in the issue.

Overlay focus / VoiceOver

The recording overlay panels (main pill + the prompt/mode/actions submenus) and the overlay's SwiftUI content weren't hidden from the accessibility tree, so VoiceOver announced them and could shift focus away from the user's active app. Each panel now sets setAccessibilityElement(false) + setAccessibilityRole(.unknown), and the SwiftUI body uses .accessibilityHidden(true).

Cmd+Tab / system-shortcut latency

The session event tap runs the full modifier-tracking + shortcut-matching pass on every key event system-wide, which adds perceptible latency to Cmd+Tab / Cmd+Space / Cmd+` (worse when VoiceOver is also intercepting). Added a fast path that passes these reserved system shortcuts straight through before any matching work. None of them can be a FluidVoice hotkey, so it's safe.

The fast path still cancels a pending modifier-only hold on key-down before passing through, so a Command-based modifier-only FluidVoice shortcut held during a Cmd+Tab isn't mistaken for a clean modifier hold.

Out of scope

The issue's third root cause (DynamicNotchPanel.canBecomeKey/canBecomeMain) lives in the external altic-dev/DynamicNotchKit package, not this repo, so that's a separate change there.

Testing

  • xcodebuild build: succeeds
  • 5 new unit tests covering the fast-path passthrough logic: pass
  • swiftlint --strict: 0 violations

…cuts

Two accessibility/perf fixes for the bottom dictation overlay (issue altic-dev#401):

1. Accessibility: the four floating NSPanels (overlay + prompt/mode/actions
   selectors) and the BottomOverlayView SwiftUI body are now hidden from the
   accessibility tree (setAccessibilityElement(false) + setAccessibilityRole(.unknown),
   and .accessibilityHidden(true)). VoiceOver no longer announces the overlay or
   shifts focus away from the user's active app while dictating.

2. Latency: GlobalHotkeyManager's CGEvent-tap callback now fast-paths Command-modified
   system switching shortcuts (Cmd+Tab, Cmd+Shift+Tab, Cmd+Space, Cmd+`), passing them
   straight through before the modifier-tracking/shortcut-matching work. The tap fires
   for every key event system-wide, so this removes perceptible latency from app
   switching / Spotlight, which compounds when VoiceOver also processes the events.
   The fast path still records the combo and cancels any pending modifier-only hold
   start, so a Command-based modifier-only FluidVoice shortcut is not mis-triggered.

Adds GlobalHotkeyManagerSystemShortcutTests covering the passthrough decision.

The issue's third root cause (DynamicNotchKit's DynamicNotchPanel canBecomeKey/
canBecomeMain) lives in the external DynamicNotchKit package and is out of scope here.

Implementation adapted from the patch proposed by @Alshekhi in the issue report.

Co-authored-by: Mansour Alshekhi <Alshekhi@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5fcd88aec0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if type == .keyDown {
self.cancelPendingHoldModeForComboKeyPress()
}
return Unmanaged.passUnretained(event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid bypassing saved Command shortcuts

When a user already has a FluidVoice shortcut such as Cmd+Space or Cmd+saved, this early return runs before the configured shortcut checks and passes the event through, so the dictation/cancel/prompt shortcut silently stops firing even though the recorder accepts arbitrary keyDown combinations inhandleShortcutKeyDownEvent` and the matching code below would otherwise handle them. Either reject these combinations when recording/restoring settings or only fast-path them when no configured shortcut matches.

Useful? React with 👍 / 👎.

}
// The overlay is a non-interactive dictation HUD; hide its SwiftUI content from
// VoiceOver so it is not announced and cannot steal focus from the active app.
.accessibilityHidden(true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not hide overlay controls from VoiceOver

This modifier is applied to the entire overlay view, but the same view contains interactive controls, including the mode/prompt/actions/settings tap targets and the AI failure Retry/Dismiss buttons. With VoiceOver enabled, accessibilityHidden(true) removes those descendants from the accessibility tree, so users can no longer reach those controls; hide only the passive HUD content or re-expose the controls that remain interactive.

Useful? React with 👍 / 👎.

…in over fast-path (altic-dev#401)

Addresses two over-reach issues found in code review on the altic-dev#401 fixes:

1. GlobalHotkeyManager: the system-shortcut fast path (Cmd+Tab, Cmd+Space,
   Cmd+`) ran before the configured-shortcut match and passed the event
   through unconditionally. The shortcut recorder accepts arbitrary keyDown
   combinations, so a user can bind one of these combos as a FluidVoice
   shortcut, which the fast path then silently shadowed. Add
   shouldFastPathSystemShortcut, which only fast-paths a candidate combo when
   it does not collide with an enabled configured shortcut (dictation, cancel,
   prompt assignments, prompt/command/rewrite mode). The latency win is kept
   for the common non-conflicting case.

2. BottomOverlayView: accessibilityHidden(true) was applied to the entire
   overlay view and the four floating panels, which removed the interactive
   controls (mode/prompt/actions/settings chips and the AI-failure
   Retry/Dismiss buttons) from the accessibility tree. Scope the hiding to
   only the passive HUD content (transcription preview and waveform/mode row)
   and drop the panel-level accessibility suppression, so VoiceOver users can
   still reach the controls while the passive status is not announced. Focus
   stealing remains prevented by the nonactivating panel style mask.

Extends GlobalHotkeyManagerSystemShortcutTests with conflict-aware coverage.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c24b43fcfe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let modifiers = Self.eventModifierFlags(from: flags)
return !configuredShortcuts.contains { configured in
configured.isEnabled &&
configured.shortcut.matches(keyCode: keyCode, modifiers: modifiers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not fast-path releases for saved shortcuts

When the saved shortcut is a Command system key with an extra modifier (for example Cmd+Shift+Tab or Cmd+Option+Space), keyDown correctly suppresses the fast path and starts the hold/automatic shortcut, but keyUp can arrive after the user has released the extra modifier while Command is still held. In that case this exact matches check no longer sees the configured shortcut, so the keyUp is passed through before the keyCode-only release handlers below can clear is*KeyPressed and stop/finish the recording.

Useful? React with 👍 / 👎.

…ltic-dev#401)

Follow-on to the conflict-aware fast path. The fast path used the event's
current modifier flags to decide whether a system switching combo collides
with a configured FluidVoice shortcut. That is correct on keyDown, but a keyUp
can arrive with fewer modifiers than its keyDown: if a saved shortcut is a
Command system key plus an extra modifier (e.g. Cmd+Shift+Tab or
Cmd+Option+Space) and the user releases the extra modifier first while Command
is still held, the keyUp matches as Cmd-only. The flag-based check then no
longer sees the configured shortcut, so the release was fast-pathed and the
keyUp handlers below never cleared is*KeyPressed or stopped the recording,
stranding the recording state.

Add hasActivePressNeedingReleaseHandling, which mirrors the keyCode-only,
modifier-independent conditions of the keyUp handlers, and a
shouldFastPathSystemShortcut overload that suppresses the fast path for any
keyUp whose key code has an in-flight press. Releases that need handling now
always reach the keyUp handlers regardless of current modifier flags, while
genuinely unconfigured system shortcuts keep the latency win.

Chose the in-flight-state gate over a modifier-independent keyCode match
against configured shortcuts because it ties suppression to the exact invariant
being protected (a release the handlers below must finish) and so does not
over-suppress the latency win for configured-but-not-currently-held keys.

Extends GlobalHotkeyManagerSystemShortcutTests with release-case coverage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VoiceOver: dictation overlay steals focus and Cmd+Tab becomes sluggish

1 participant