From 647ddc842b132db19e38450a08930fc99de08d7e Mon Sep 17 00:00:00 2001 From: Palash Debnath Date: Wed, 9 Sep 2026 21:20:32 -0700 Subject: [PATCH] fix(dictation): say when another app already owns the shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1858. Whichever app registers a global shortcut first wins, and the default collides with 1Password Quick Access on macOS — so for a large share of installs the hotkey the onboarding screen advertises silently does nothing. Registration failure was a Rust-side log line and nothing else. There was no publish on the error path, so the frontend kept reporting whatever accelerator had been REQUESTED, with no way for any screen to know the OS had refused it. The failure is published now, carrying the outcome in `backend` and still naming the accelerator so the UI can say WHICH combination is taken. Surfaced as its own state rather than folding into the existing "no hotkey registered" badge. That one means "not checked yet"; this means "this exact combination belongs to another app, pick a different one" — different situations needing different actions. Detection rather than a new default, deliberately. Any default can collide with something, so changing the value would move the problem rather than remove it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S --- CHANGELOG.md | 1 + frontend/src-tauri/src/dictation_shortcut.rs | 15 ++++++- frontend/src/components/DictationDemo.jsx | 29 ++++++++++-- frontend/src/i18n/locales/ar.json | 1 + frontend/src/i18n/locales/de.json | 1 + frontend/src/i18n/locales/en.json | 1 + frontend/src/i18n/locales/es.json | 1 + frontend/src/i18n/locales/fr.json | 1 + frontend/src/i18n/locales/hi.json | 1 + frontend/src/i18n/locales/id.json | 1 + frontend/src/i18n/locales/it.json | 1 + frontend/src/i18n/locales/ja.json | 1 + frontend/src/i18n/locales/ko.json | 1 + frontend/src/i18n/locales/nl.json | 1 + frontend/src/i18n/locales/pl.json | 1 + frontend/src/i18n/locales/pt.json | 1 + frontend/src/i18n/locales/ru.json | 1 + frontend/src/i18n/locales/sv.json | 1 + frontend/src/i18n/locales/th.json | 1 + frontend/src/i18n/locales/tr.json | 1 + frontend/src/i18n/locales/uk.json | 1 + frontend/src/i18n/locales/vi.json | 1 + frontend/src/i18n/locales/zh-CN.json | 1 + frontend/src/i18n/locales/zh-TW.json | 1 + frontend/src/test/DictationDemo.test.jsx | 46 ++++++++++++++++++++ 25 files changed, 108 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72583807..a4b20a13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ the frozen-backend fallback mirror it for their toolchains. ## [Unreleased] **Highlights** +- A dictation shortcut another app already owns now says so, instead of silently doing nothing (#1858) - Generating from a one-character input now says the input was too short, instead of quoting a convolution error (#1826) - First run asks about text size before the install, not after it (#1849) - Cloning without a reference clip now says so, instead of naming library parameters you cannot set (#1879) diff --git a/frontend/src-tauri/src/dictation_shortcut.rs b/frontend/src-tauri/src/dictation_shortcut.rs index 94d18394..5ac9c750 100644 --- a/frontend/src-tauri/src/dictation_shortcut.rs +++ b/frontend/src-tauri/src/dictation_shortcut.rs @@ -64,7 +64,20 @@ impl DictationShortcutManager { Ok(()) => { manager.publish(&app, accelerator, None, "native"); } - Err(error) => log::warn!("Failed to register global shortcut: {error}"), + Err(error) => { + // Publish the failure instead of only logging it. Whichever + // app registers a global shortcut first wins, and the default + // collides with 1Password Quick Access on macOS — so for a lot + // of installs the hotkey the onboarding screen advertises + // silently does nothing. With no publish on this path the + // frontend kept reporting whatever accelerator was REQUESTED, + // with no way to know the OS never granted it (#1858). + // + // The accelerator is still published so the UI can name the + // shortcut that failed; `backend` carries the outcome. + log::warn!("Failed to register global shortcut: {error}"); + manager.publish(&app, accelerator, None, "unregistered"); + } } } diff --git a/frontend/src/components/DictationDemo.jsx b/frontend/src/components/DictationDemo.jsx index 0be2cec2..b781a29b 100644 --- a/frontend/src/components/DictationDemo.jsx +++ b/frontend/src/components/DictationDemo.jsx @@ -101,9 +101,17 @@ export default function DictationDemo({ embedded = false }) { useEffect(() => { if (!desktop) return; - setHotkeyState((current) => - current === 'verified' ? current : shortcut.backend === 'focused' ? 'unknown' : 'registered', - ); + // `unregistered` is its own state, not a flavour of `unknown`: the OS + // refused the accelerator, usually because another app already holds it + // (the default collides with 1Password Quick Access on macOS). Saying + // "no hotkey registered" there would read as "we have not checked yet", + // when what the user needs to know is that this specific combination is + // taken and they should pick another (#1858). + setHotkeyState((current) => { + if (shortcut.backend === 'unregistered') return 'unregistered'; + if (current === 'verified') return current; + return shortcut.backend === 'focused' ? 'unknown' : 'registered'; + }); }, [desktop, shortcut.backend]); // Subscribe to dictation events: the moment the user presses their @@ -203,6 +211,21 @@ export default function DictationDemo({ embedded = false }) { {t('demo.dictation_status_ok')} ); + case 'unregistered': + return ( + + {' '} + {t('demo.dictation_status_taken', { + defaultValue: + 'Another app already uses this shortcut — pick a different one in Settings.', + })}{' '} + + {shortcut.display} + + + ); case 'registered': return ( ({ }, apiJson: vi.fn(), })); +const { shortcutInfo } = vi.hoisted(() => ({ + shortcutInfo: { accelerator: 'CmdOrCtrl+Shift+Space', display: '⌘⇧Space', backend: 'native' }, +})); +vi.mock('../hooks/useEffectiveDictationShortcut', () => ({ + useEffectiveDictationShortcut: () => ({ info: shortcutInfo }), +})); vi.mock('../hooks/useDictationReadiness', () => ({ useDictationReadiness: () => readiness, })); @@ -150,3 +156,43 @@ describe('DictationDemo', () => { expect(screen.queryByTestId('asr-model-chooser')).not.toBeInTheDocument(); }); }); + +// #1858: whichever app registers a global shortcut first wins, and the default +// collides with 1Password Quick Access on macOS. Registration failure used to +// be a Rust-side log line and nothing else — the frontend kept reporting the +// accelerator that had been REQUESTED, so the onboarding screen advertised a +// hotkey the OS had refused, with no way for the user to find out. +describe('DictationDemo — hotkey registration failure', () => { + beforeEach(() => { + window.__TAURI_INTERNALS__ = {}; + }); + afterEach(() => { + delete window.__TAURI_INTERNALS__; + }); + + it('says the shortcut is taken rather than merely unregistered', () => { + shortcutInfo.backend = 'unregistered'; + shortcutInfo.display = '⌘⇧Space'; + render(withI18n()); + + expect(screen.getByText(/Another app already uses this shortcut/i)).toBeInTheDocument(); + // "No hotkey registered" means "not checked yet" — a different situation. + expect(screen.queryByText(/No hotkey registered/i)).not.toBeInTheDocument(); + }); + + it('still names the shortcut that failed', () => { + // The user has to know WHICH combination is taken to pick another. + shortcutInfo.backend = 'unregistered'; + shortcutInfo.display = '⌘⇧Space'; + render(withI18n()); + + expect(screen.getByText('⌘⇧Space')).toBeInTheDocument(); + }); + + it('leaves a successfully registered shortcut alone', () => { + shortcutInfo.backend = 'native'; + render(withI18n()); + + expect(screen.queryByText(/Another app already uses this shortcut/i)).not.toBeInTheDocument(); + }); +});