[eric] dictation: ask for Input Monitoring before any Accessibility check, or macOS never shows the prompt (rdar://7381305)

This commit is contained in:
ciregenz
2026-08-21 15:50:14 -07:00
parent 89f7621140
commit 07566a77e5
2 changed files with 19 additions and 1 deletions
+5 -1
View File
@@ -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 {
+14
View File
@@ -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');
});