fix(dictation): say when another app already owns the shortcut

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
This commit is contained in:
Palash Debnath
2026-09-09 21:20:32 -07:00
co-authored by Claude Opus 5
parent d0ab15c376
commit 647ddc842b
25 changed files with 108 additions and 4 deletions
+1
View File
@@ -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)
+14 -1
View File
@@ -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");
}
}
}
+26 -3
View File
@@ -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 }) {
<CheckCircle2 size={12} /> {t('demo.dictation_status_ok')}
</span>
);
case 'unregistered':
return (
<span
className={`${STATUS_BASE} border-transparent bg-[rgba(204,36,29,0.12)] text-[#fb4934]`}
>
<AlertTriangle size={12} />{' '}
{t('demo.dictation_status_taken', {
defaultValue:
'Another app already uses this shortcut — pick a different one in Settings.',
})}{' '}
<code className="font-mono text-[10px] px-[4px] py-[1px] bg-[rgba(0,0,0,0.3)] rounded-[3px]">
{shortcut.display}
</code>
</span>
);
case 'registered':
return (
<span
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "اقرأ واحدة منها بصوت عالٍ بعد الضغط على مفتاح التشغيل السريع، أو اضغط على \"إعادة التشغيل\" لإرسال العينة المجمعة عبر الناسخ حتى تتمكن من رؤية الإملاء يعمل من البداية إلى النهاية دون التحدث.",
"dictation_status_ok": "تم التحقق - مفتاح التشغيل السريع يعمل على هذا الجهاز",
"dictation_status_pending": "اضغط على الاختصار الخاص بك في أي مكان للاختبار",
"dictation_status_taken": "يستخدم تطبيق آخر هذا الاختصار بالفعل — اختر اختصارًا مختلفًا من الإعدادات.",
"dictation_status_warn": "لم يتم تسجيل أي مفتاح تشغيل سريع - قم بتعيين واحد أدناه",
"dictation_hear": "اسمع",
"dictation_stop": "توقف",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Lesen Sie eines davon vor, nachdem Sie Ihren Hotkey gedrückt haben, oder klicken Sie auf „Wiederholen“, um die gebündelte Probe durch den Transkriptor zu senden, sodass Sie die Diktatarbeit von Anfang bis Ende verfolgen können, ohne zu sprechen.",
"dictation_status_ok": "Verifiziert Hotkey funktioniert auf diesem Computer",
"dictation_status_pending": "Drücken Sie zum Testen an einer beliebigen Stelle Ihre Tastenkombination",
"dictation_status_taken": "Eine andere App belegt dieses Tastenkürzel bereits — wähle in den Einstellungen ein anderes.",
"dictation_status_warn": "Kein Hotkey registriert legen Sie unten einen fest",
"dictation_hear": "Hören",
"dictation_stop": "Stopp",
+1
View File
@@ -2007,6 +2007,7 @@
"dictation_lede": "Read one of these aloud after pressing your hotkey, or hit Replay to send the bundled sample through the transcriber so you can see dictation work end-to-end without speaking.",
"dictation_status_ok": "Verified — hotkey works on this machine",
"dictation_status_pending": "Press your shortcut anywhere to test",
"dictation_status_taken": "Another app already uses this shortcut — pick a different one in Settings.",
"dictation_status_warn": "No hotkey registered — set one below",
"dictation_hear": "Hear",
"dictation_stop": "Stop",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Lea uno de estos en voz alta después de presionar la tecla de acceso rápido, o presione Reproducir para enviar la muestra incluida a través del transcriptor para que pueda ver el dictado de un extremo a otro sin hablar.",
"dictation_status_ok": "Verificado: la tecla de acceso rápido funciona en esta máquina",
"dictation_status_pending": "Presione su acceso directo en cualquier lugar para probar",
"dictation_status_taken": "Otra aplicación ya usa este atajo: elige otro en Ajustes.",
"dictation_status_warn": "No hay ninguna tecla de acceso rápido registrada: configure una a continuación",
"dictation_hear": "escuchar",
"dictation_stop": "Detener",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Lisez-en un à haute voix après avoir appuyé sur votre touche de raccourci, ou appuyez sur Replay pour envoyer l'échantillon fourni via le transcripteur afin que vous puissiez voir le travail de dictée de bout en bout sans parler.",
"dictation_status_ok": "Vérifié  le raccourci clavier fonctionne sur cette machine",
"dictation_status_pending": "Appuyez n'importe où sur votre raccourci pour tester",
"dictation_status_taken": "Une autre application utilise déjà ce raccourci — choisissez-en un autre dans les Réglages.",
"dictation_status_warn": "Aucun raccourci clavier enregistré  définissez-en un ci-dessous",
"dictation_hear": "Écoutez",
"dictation_stop": "Arrêter",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "अपनी हॉटकी दबाने के बाद इनमें से किसी एक को जोर से पढ़ें, या बंडल किए गए नमूने को ट्रांसक्राइबर के माध्यम से भेजने के लिए रीप्ले दबाएं ताकि आप बिना बोले श्रुतलेख कार्य को शुरू से अंत तक देख सकें।",
"dictation_status_ok": "सत्यापित - हॉटकी इस मशीन पर काम करती है",
"dictation_status_pending": "परीक्षण करने के लिए अपना शॉर्टकट कहीं भी दबाएँ",
"dictation_status_taken": "यह शॉर्टकट पहले से किसी अन्य ऐप के पास है — सेटिंग्स में दूसरा चुनें।",
"dictation_status_warn": "कोई हॉटकी पंजीकृत नहीं - नीचे एक सेट करें",
"dictation_hear": "सुनो",
"dictation_stop": "रुकें",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Bacalah salah satunya dengan lantang setelah menekan tombol pintas Anda, atau tekan Putar Ulang untuk mengirim sampel yang dibundel melalui transcriber sehingga Anda dapat melihat dikte berfungsi secara menyeluruh tanpa harus berbicara.",
"dictation_status_ok": "Terverifikasi — tombol pintas berfungsi pada mesin ini",
"dictation_status_pending": "Tekan pintasan Anda di mana saja untuk menguji",
"dictation_status_taken": "Aplikasi lain sudah memakai pintasan ini — pilih yang lain di Pengaturan.",
"dictation_status_warn": "Tidak ada hotkey yang terdaftar — atur satu di bawah",
"dictation_hear": "Dengar",
"dictation_stop": "Berhenti",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Leggi uno di questi ad alta voce dopo aver premuto il tasto di scelta rapida oppure premi Riproduci per inviare il campione in bundle al trascrittore in modo da poter vedere il lavoro di dettatura end-to-end senza parlare.",
"dictation_status_ok": "Verificato: il tasto di scelta rapida funziona su questa macchina",
"dictation_status_pending": "Premi la scorciatoia ovunque per testare",
"dictation_status_taken": "Unaltra app usa già questa scorciatoia: scegline unaltra nelle Impostazioni.",
"dictation_status_warn": "Nessun tasto di scelta rapida registrato: impostane uno di seguito",
"dictation_hear": "Ascolta",
"dictation_stop": "Fermati",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "ホットキーを押した後、これらのいずれかを声に出して読み上げます。または、再生を押して、バンドルされたサンプルをトランスクライバー経由で送信し、話さずにエンドツーエンドのディクテーション作業を確認できます。",
"dictation_status_ok": "確認済み — このマシンではホットキーが機能します",
"dictation_status_pending": "テストするには任意の場所のショートカットを押してください",
"dictation_status_taken": "このショートカットは他のアプリが使用中です。設定で別のものを選んでください。",
"dictation_status_warn": "ホットキーが登録されていません — 以下のホットキーを設定してください",
"dictation_hear": "聞く",
"dictation_stop": "停止",
+1
View File
@@ -2037,6 +2037,7 @@
"dictation_lede": "단축키를 누른 후 이 중 하나를 큰 소리로 읽거나 재생을 눌러 번들 샘플을 전사기를 통해 전송하여 말하지 않고도 받아쓰기 작업을 끝까지 볼 수 있습니다.",
"dictation_status_ok": "확인됨 — 이 컴퓨터에서 단축키가 작동합니다",
"dictation_status_pending": "테스트하려면 어디에서나 바로가기를 누르세요.",
"dictation_status_taken": "다른 앱이 이미 이 단축키를 사용 중입니다. 설정에서 다른 것을 선택하세요.",
"dictation_status_warn": "등록된 단축키가 없습니다. 아래에서 단축키를 설정하세요.",
"dictation_hear": "듣기",
"dictation_stop": "중지",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Lees een van deze hardop nadat u op uw sneltoets hebt gedrukt, of druk op Opnieuw afspelen om het gebundelde voorbeeld door de transcriber te sturen, zodat u het dicteerwerk van begin tot eind kunt zien zonder te spreken.",
"dictation_status_ok": "Geverifieerd - sneltoets werkt op deze machine",
"dictation_status_pending": "Druk ergens op uw snelkoppeling om te testen",
"dictation_status_taken": "Een andere app gebruikt deze sneltoets al — kies een andere in Instellingen.",
"dictation_status_warn": "Geen sneltoets geregistreerd - stel er hieronder een in",
"dictation_hear": "Hoor",
"dictation_stop": "Stop",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Przeczytaj jeden z nich na głos po naciśnięciu klawisza skrótu lub naciśnij przycisk Odtwórz, aby wysłać dołączoną próbkę do narzędzia dokonującego transkrypcji i zobaczyć, jak dyktowanie działa od początku do końca, bez mówienia.",
"dictation_status_ok": "Zweryfikowano — klawisz skrótu działa na tym komputerze",
"dictation_status_pending": "Naciśnij skrót w dowolnym miejscu, aby przetestować",
"dictation_status_taken": "Inna aplikacja już używa tego skrótu — wybierz inny w Ustawieniach.",
"dictation_status_warn": "Nie zarejestrowano żadnego klawisza skrótu — ustaw go poniżej",
"dictation_hear": "Usłysz",
"dictation_stop": "Zatrzymaj się",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Leia um deles em voz alta depois de pressionar a tecla de atalho ou pressione Replay para enviar a amostra agrupada através do transcritor para que você possa ver o trabalho do ditado de ponta a ponta sem falar.",
"dictation_status_ok": "Verificado a tecla de atalho funciona nesta máquina",
"dictation_status_pending": "Pressione seu atalho em qualquer lugar para testar",
"dictation_status_taken": "Outra aplicação já usa este atalho — escolha outro nas Definições.",
"dictation_status_warn": "Nenhuma tecla de atalho registrada defina uma abaixo",
"dictation_hear": "Ouça",
"dictation_stop": "Pare",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Прочтите один из них вслух после нажатия горячей клавиши или нажмите «Воспроизвести», чтобы отправить прилагаемый образец через транскрибатор, чтобы вы могли видеть работу диктовки от начала до конца, не говоря ни слова.",
"dictation_status_ok": "Проверено — горячая клавиша работает на этом компьютере.",
"dictation_status_pending": "Нажмите ярлык в любом месте, чтобы проверить",
"dictation_status_taken": "Это сочетание клавиш уже занято другим приложением — выберите другое в настройках.",
"dictation_status_warn": "Горячая клавиша не зарегистрирована — установите ее ниже",
"dictation_hear": "Слышать",
"dictation_stop": "Останавливаться",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Läs en av dessa högt efter att du har tryckt på din snabbtangent, eller tryck på Replay för att skicka det medföljande provet genom transkriberaren så att du kan se dikteringsarbetet från början till slut utan att tala.",
"dictation_status_ok": "Verifierad snabbtangent fungerar på den här maskinen",
"dictation_status_pending": "Tryck på din genväg var som helst för att testa",
"dictation_status_taken": "En annan app använder redan detta kortkommando — välj ett annat i Inställningar.",
"dictation_status_warn": "Ingen snabbtangent registrerad — ställ in en nedan",
"dictation_hear": "Hör",
"dictation_stop": "Sluta",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "อ่านออกเสียงข้อความเหล่านี้หลังจากกดปุ่มลัด หรือกดเล่นซ้ำเพื่อส่งตัวอย่างที่รวมกลุ่มผ่านตัวถอดเสียง เพื่อให้คุณสามารถเห็นการเขียนตามคำบอกตั้งแต่ต้นจนจบโดยไม่ต้องพูด",
"dictation_status_ok": "ตรวจสอบแล้ว — ปุ่มลัดใช้งานได้กับเครื่องนี้",
"dictation_status_pending": "กดทางลัดของคุณได้ทุกที่เพื่อทดสอบ",
"dictation_status_taken": "แอปอื่นใช้ทางลัดนี้อยู่แล้ว — เลือกปุ่มอื่นในการตั้งค่า",
"dictation_status_warn": "ไม่มีการลงทะเบียนปุ่มลัด — ตั้งค่าไว้ด้านล่าง",
"dictation_hear": "ได้ยิน",
"dictation_stop": "หยุด",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Kısayol tuşuna bastıktan sonra bunlardan birini yüksek sesle okuyun veya paketlenmiş örneği transcriber aracılığıyla göndermek için Tekrar Oynat'a basın, böylece dikte çalışmasını konuşmadan uçtan uca görebilirsiniz.",
"dictation_status_ok": "Doğrulandı — kısayol tuşu bu makinede çalışıyor",
"dictation_status_pending": "Test etmek için herhangi bir yerde kısayolunuza basın",
"dictation_status_taken": "Bu kısayolu başka bir uygulama kullanıyor — Ayarlardan farklı bir tane seçin.",
"dictation_status_warn": "Kayıtlı kısayol tuşu yok — aşağıdan bir tane ayarlayın",
"dictation_hear": "Duy",
"dictation_stop": "Durdur",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Прочитайте одну з них вголос після натискання гарячої клавіші або натисніть «Повторити», щоб надіслати зразок у комплекті через транскрибатор, щоб ви могли бачити роботу диктанту від кінця до кінця, не розмовляючи.",
"dictation_status_ok": "Перевірено — гаряча клавіша працює на цій машині",
"dictation_status_pending": "Щоб перевірити, натисніть ярлик будь-де",
"dictation_status_taken": "Це сполучення клавіш уже зайняте іншою програмою — виберіть інше в налаштуваннях.",
"dictation_status_warn": "Немає зареєстрованих гарячих клавіш — установіть одну нижче",
"dictation_hear": "Почуйте",
"dictation_stop": "Стоп",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "Đọc to một trong những nội dung này sau khi nhấn phím nóng hoặc nhấn Phát lại để gửi mẫu đi kèm qua trình chuyển mã để bạn có thể xem chính tả hoạt động từ đầu đến cuối mà không cần nói.",
"dictation_status_ok": "Đã xác minh - phím nóng hoạt động trên máy này",
"dictation_status_pending": "Nhấn phím tắt của bạn ở bất cứ đâu để kiểm tra",
"dictation_status_taken": "Một ứng dụng khác đã dùng phím tắt này — hãy chọn phím khác trong Cài đặt.",
"dictation_status_warn": "Không có phím nóng nào được đăng ký - đặt một phím nóng bên dưới",
"dictation_hear": "Nghe",
"dictation_stop": "Dừng lại",
+1
View File
@@ -1989,6 +1989,7 @@
"dictation_lede": "按下快捷键后朗读其中一句,或点击“重放”将内置示例送入转录器,无需开口即可看到听写的完整流程。",
"dictation_status_ok": "已验证 — 快捷键在本机可用",
"dictation_status_pending": "在任意位置按下快捷键以测试",
"dictation_status_taken": "该快捷键已被其他应用占用,请在设置中另选一个。",
"dictation_status_warn": "未注册快捷键 — 请在下方设置",
"dictation_hear": "试听",
"dictation_stop": "停止",
+1
View File
@@ -1722,6 +1722,7 @@
"dictation_lede": "按下熱鍵後大聲朗讀其中一篇,或點擊重播透過轉錄器發送捆綁的樣本,這樣您就可以在不說話的情況下看到端到端的聽寫工作。",
"dictation_status_ok": "已驗證 — 熱鍵可在本機上使用",
"dictation_status_pending": "在任何位置按快捷鍵進行測試",
"dictation_status_taken": "此快速鍵已被其他應用程式占用,請在設定中另選一個。",
"dictation_status_warn": "未註冊熱鍵 — 在下方設定一個",
"dictation_hear": "聽到",
"dictation_stop": "停止",
+46
View File
@@ -14,6 +14,12 @@ const { readiness, apiJson } = vi.hoisted(() => ({
},
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(<DictationDemo />));
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(<DictationDemo />));
expect(screen.getByText('⌘⇧Space')).toBeInTheDocument();
});
it('leaves a successfully registered shortcut alone', () => {
shortcutInfo.backend = 'native';
render(withI18n(<DictationDemo />));
expect(screen.queryByText(/Another app already uses this shortcut/i)).not.toBeInTheDocument();
});
});