mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 04:07:44 +02:00
[eric] dictation: the Accessibility check waits on the Input Monitoring request by callback, not line order (rdar://7381305)
This commit is contained in:
+23
-10
@@ -168,16 +168,21 @@ function installVoiceHotkey(getMainWindow) {
|
||||
// ---- fn/Globe primary tier (macOS): the native watcher, since no JS tap can see keycode 63 ----
|
||||
let fnProc = null;
|
||||
let quitReaperWired = false;
|
||||
const startFnWatcher = (noPrompt) => {
|
||||
if (process.platform !== 'darwin' || combo.special !== 'fn' || fnProc) return;
|
||||
// `asked` runs once the Input Monitoring request is actually in flight (or provably never will
|
||||
// be). Anything that touches Accessibility must wait for it; see rdar://7381305.
|
||||
const startFnWatcher = (noPrompt, asked) => {
|
||||
const done = () => { if (typeof asked === 'function') asked(); };
|
||||
if (process.platform !== 'darwin' || combo.special !== 'fn' || fnProc) { done(); return; }
|
||||
resolveFnWatcherBinary((bin) => {
|
||||
if (!bin) {
|
||||
console.log('[voice] no fn watcher binary, legacy hotkey stays primary');
|
||||
notifyPrimaryUnusable('no-watcher-binary');
|
||||
done();
|
||||
return;
|
||||
}
|
||||
if (combo.special !== 'fn' || fnProc) return; // rebound or raced while compiling
|
||||
if (combo.special !== 'fn' || fnProc) { done(); return; }
|
||||
startFnWatcherWith(bin, noPrompt === true);
|
||||
done();
|
||||
});
|
||||
};
|
||||
// Kill fn-watchers left by a previous OpenSwarm that died badly. will-quit is the ONLY thing that
|
||||
@@ -361,13 +366,21 @@ function installVoiceHotkey(getMainWindow) {
|
||||
// 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');
|
||||
registerVoiceShortcut();
|
||||
// It has to be a CALLBACK: the dev path compiles the watcher first, so a plain statement order
|
||||
// still let the Accessibility check win on wall-clock and the prompt stayed dead.
|
||||
const armTapTier = () => {
|
||||
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');
|
||||
registerVoiceShortcut();
|
||||
};
|
||||
let tapArmed = false;
|
||||
const armTapOnce = () => { if (!tapArmed) { tapArmed = true; armTapTier(); } };
|
||||
startFnWatcher(false, armTapOnce);
|
||||
// A wedged swiftc must never cost the user their chord tier.
|
||||
setTimeout(armTapOnce, 8000);
|
||||
};
|
||||
try {
|
||||
if (fs.existsSync(path.join(app.getPath('userData'), 'dictation-used'))) armNativeTiers();
|
||||
|
||||
@@ -127,11 +127,21 @@ test('the intent path spawns WITHOUT --no-prompt, or nothing is ever asked', ()
|
||||
// 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');
|
||||
test('the Accessibility check waits for the Input Monitoring request, by callback not by line order', () => {
|
||||
// Statement order is NOT enough: the dev path compiles the watcher asynchronously, so a plain
|
||||
// "fn first" still let the AX check win on wall-clock and the prompt stayed dead (seen live).
|
||||
const arm = hotkeySrc.split('const armNativeTiers = () => {')[1].split('\n };')[0];
|
||||
assert.match(arm, /startFnWatcher\(false, armTapOnce\)/,
|
||||
'the tap tier must be handed to the watcher as a completion callback');
|
||||
const tier = arm.split('const armTapTier = () => {')[1].split(' };')[0];
|
||||
assert.match(tier, /tryStartNativeTap\(\)/,
|
||||
'the AX-touching call belongs inside the deferred tier, never inline ahead of the request');
|
||||
assert.match(arm, /setTimeout\(armTapOnce, \d+\)/, 'a wedged compile must not cost the chord tier');
|
||||
});
|
||||
|
||||
test('the watcher signals when the request is in flight', () => {
|
||||
assert.match(hotkeySrc, /const startFnWatcher = \(noPrompt, asked\) =>/);
|
||||
const body = hotkeySrc.split('const startFnWatcher = (noPrompt, asked) =>')[1].slice(0, 900);
|
||||
assert.ok((body.match(/done\(\)/g) || []).length >= 4,
|
||||
'every exit path must signal, or a failure strands the Accessibility tier forever');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user