diff --git a/electron/voiceHotkey.js b/electron/voiceHotkey.js index 01b6e47b..d51fa872 100644 --- a/electron/voiceHotkey.js +++ b/electron/voiceHotkey.js @@ -357,12 +357,16 @@ function installVoiceHotkey(getMainWindow) { const armNativeTiers = () => { if (tiersArmed) return; tiersArmed = true; + // ORDER IS LOAD-BEARING (rdar://7381305 / FB7381305): once AXIsProcessTrustedWithOptions has + // been called, IOHIDRequestAccess stops raising the Input Monitoring dialog entirely. Electron's + // isTrustedAccessibilityClient IS that call, and tryStartNativeTap makes it, so asking for the + // tap first is what silently ate the fn prompt. Ask for Input Monitoring FIRST, always. + startFnWatcher(); const tapOk = tryStartNativeTap(); // Ctrl+Win is the Windows fn-equivalent and ONLY this tap can see it, so a tap that never loads // is the same dead key as a deaf fn watcher. Off macOS there is no TCC to blame, which is // exactly why it would otherwise fail with nothing said at all. if (tapOk === false && combo.special === 'ctrlmeta') notifyPrimaryUnusable('native-tap-unavailable'); - startFnWatcher(); registerVoiceShortcut(); }; try { diff --git a/electron/voiceHotkeyProbe.test.js b/electron/voiceHotkeyProbe.test.js index e2a47f02..10762279 100644 --- a/electron/voiceHotkeyProbe.test.js +++ b/electron/voiceHotkeyProbe.test.js @@ -121,3 +121,17 @@ test('the intent path spawns WITHOUT --no-prompt, or nothing is ever asked', () const fn = hotkeySrc.split('const askForFnPermission')[1].slice(0, 400); assert.match(fn, /startFnWatcher\(\);/, 'a --no-prompt spawn here would silently never prompt'); }); + +// ---- rdar://7381305: the AX check poisons the Input Monitoring prompt ---- +// Once AXIsProcessTrustedWithOptions has run, IOHIDRequestAccess never raises its dialog again. +// Electron's isTrustedAccessibilityClient IS that call, so arming the uiohook tap before the fn +// watcher silently ate the one prompt that can fix a dead fn key. Order is the whole fix. + +test('Input Monitoring is requested BEFORE any Accessibility check', () => { + const arm = hotkeySrc.split('const armNativeTiers = () => {')[1].split('};')[0]; + const fnAt = arm.indexOf('startFnWatcher()'); + const axAt = arm.indexOf('tryStartNativeTap()'); + assert.ok(fnAt > -1 && axAt > -1, 'both tiers must still be armed'); + assert.ok(fnAt < axAt, + 'rdar://7381305: an AX check first kills the Input Monitoring dialog, so fn must ask first'); +});