Skip to content

Commit cc1d735

Browse files
sodiumsunclaude
andcommitted
fix: Accessibility never prompted in onboarding + hotkey dead until restart
Two real bugs behind 'Right ⌘ shows no HUD': 1. Onboarding never asked for Accessibility. The AX prompt was wired (_act_grant_accessibility) but nothing fired it — enabling Whisper requested only Microphone. So users reached the 'hold Right ⌘' demo without the permission that makes Right ⌘ work. Now enable_whisper also requests AX, and the Whisper step gates the read-aloud demo behind an 'Enable Accessibility' step when it's missing. 2. Granting Accessibility mid-session didn't help — _start_hotkey checks trust only at launch, and a global event monitor created while untrusted stays deaf even after the grant (it must be re-created). The 10s watch loop now re-arms the hotkey once trust flips true, so the HUD starts working without an app restart. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GTBZjBGurqsHUHsjaU1gA
1 parent 1dcbae8 commit cc1d735

3 files changed

Lines changed: 40 additions & 1 deletion

File tree

heard/daemon.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,14 @@ def _start_hotkey(self, prompt_for_accessibility: bool = False) -> None:
945945
# daemon-spawn path passes prompt_for_accessibility=False so the
946946
# system dialog doesn't fire alongside the onboarding card.
947947
trusted = accessibility.ensure_trusted(prompt=prompt_for_accessibility)
948+
# Remember whether we armed the hotkey WHILE trusted. A global event
949+
# monitor created before Accessibility is granted receives no key events,
950+
# and granting it later does NOT wake the existing monitor — it must be
951+
# RE-created. The watch loop (_start_voice_service_watch) reads this and
952+
# re-arms once trust flips true, so PTT starts working without an app
953+
# restart. (Before this, granting Accessibility mid-session left the HUD
954+
# dead until relaunch.)
955+
self._hotkey_trusted = trusted
948956
if not trusted:
949957
print(
950958
"heard: Accessibility permission pending — hotkeys will start "
@@ -1026,6 +1034,19 @@ def _watch() -> None:
10261034
_log("voice_service_gate_changed", want=want, trying=trying)
10271035
self.cfg = cfg # adopt the config we decided on
10281036
self._sync_voice_service()
1037+
# Accessibility granted AFTER launch: the hotkey monitor was
1038+
# armed while untrusted (deaf) and needs re-creating. Re-arm
1039+
# once trust flips true so the Right ⌘ HUD starts without a
1040+
# restart. Only when we know we armed untrusted, and only if
1041+
# the hotkey is enabled, so we don't thrash.
1042+
if (
1043+
getattr(self, "_hotkey_trusted", True) is False
1044+
and cfg.get("hotkey_enabled", True)
1045+
and accessibility.is_trusted()
1046+
):
1047+
_log("accessibility_granted_rearming_hotkey")
1048+
self.cfg = cfg
1049+
self._start_hotkey()
10291050
except Exception as e:
10301051
_log("voice_service_watch_error", err=str(e))
10311052

heard/home_window.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,19 @@ def _act_enable_whisper(self, body):
875875
# voice-mode radio, which sets both.
876876
config.set_value("push_to_talk", True)
877877
_reload_daemon()
878+
# PTT needs TWO permissions: Microphone (below) AND Accessibility
879+
# (the global Right ⌘ monitor can't see the key without it). The
880+
# AX prompt was wired (_act_grant_accessibility) but nothing ever
881+
# fired it during onboarding, so users enabled Whisper, got the
882+
# "hold Right ⌘" demo, and no HUD ever appeared. Fire it here so
883+
# enabling Whisper asks for both. The daemon's watch re-arms the
884+
# hotkey once the grant lands (no restart needed).
885+
try:
886+
from heard import accessibility
887+
888+
accessibility.ensure_trusted(prompt=True)
889+
except Exception as e:
890+
_log_bridge_error("enable_whisper_ax", e)
878891
# CRITICAL: the serve is a subprocess with no UI, so it can't
879892
# show the mic TCC prompt — macOS silently KILLS it the moment it
880893
# opens the mic (a native crash, no traceback → the serve

heard/onboarding.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,12 @@ <h1 class="hero" style="font-size:56px">Choose your voice.</h1>
683683
<div class="eyebrow">Heard Whisper</div>
684684
<h1 class="hero" style="font-size:44px">Speak, don’t type.</h1>
685685
${on
686-
? `<p class="lede" style="max-width:500px;margin-top:12px">Whisper’s on. Read the line below to try it.</p>${test}`
686+
? (S.axGranted
687+
? `<p class="lede" style="max-width:500px;margin-top:12px">Whisper’s on. Read the line below to try it.</p>${test}`
688+
: `<p class="lede" style="max-width:500px;margin-top:12px">Whisper’s on — one last permission.</p>
689+
<div style="max-width:500px;margin-top:12px;font:400 14px/1.6 var(--sans);color:var(--ink-2)">Heard needs <strong>Accessibility</strong> access to catch the <span class="kbd">Right ⌘</span> key anywhere you type. Without it, push-to-talk can’t work.</div>
690+
<div class="row" style="margin-top:16px"><button class="btn btn-primary" onclick="bridge('grant_accessibility')">Enable Accessibility</button></div>
691+
<p style="margin-top:12px;font:400 12px var(--sans);color:var(--ink-3)">Toggle <strong>Heard</strong> on in System Settings → Privacy & Security → Accessibility. This screen updates once it’s granted.</p>`)
687692
: `<p class="lede" style="max-width:500px;margin-top:12px">Heard’s voice-to-text. Two ways to dictate to your agents instead of typing:</p>
688693
<div style="max-width:540px;margin-top:18px;display:flex;flex-direction:column;gap:12px">${rows}</div>
689694
<p style="margin-top:20px;font:400 13px var(--sans);color:var(--ink-2)">Press below to enable Heard Whisper.</p>`}

0 commit comments

Comments
 (0)