mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 09:47:44 +02:00
[eric] dictation: fn dead-key deadlock broken; a no-prompt IOHIDCheckAccess boot probe arms fn on granted machines and intent arming covers the rest (Haik, 99% of devices)
This commit is contained in:
@@ -3,6 +3,16 @@
|
||||
// tap: needs the same Input Monitoring grant the app already requests, never swallows anything.
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import IOKit.hid
|
||||
|
||||
// --no-prompt: the boot-time probe must never raise the Input Monitoring TCC prompt (ENG-341);
|
||||
// IOHIDCheckAccess answers silently, so granted machines arm and everyone else exits clean.
|
||||
if CommandLine.arguments.contains("--no-prompt")
|
||||
&& IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) != kIOHIDAccessTypeGranted {
|
||||
print("e no-permission")
|
||||
fflush(stdout)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
var fnDown = false
|
||||
var tapRef: CFMachPort?
|
||||
|
||||
+17
-4
@@ -139,12 +139,12 @@ 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 = () => {
|
||||
const startFnWatcher = (noPrompt) => {
|
||||
if (process.platform !== 'darwin' || combo.special !== 'fn' || fnProc) return;
|
||||
resolveFnWatcherBinary((bin) => {
|
||||
if (!bin) { console.log('[voice] no fn watcher binary, legacy hotkey stays primary'); return; }
|
||||
if (combo.special !== 'fn' || fnProc) return; // rebound or raced while compiling
|
||||
startFnWatcherWith(bin);
|
||||
startFnWatcherWith(bin, noPrompt === true);
|
||||
});
|
||||
};
|
||||
// Kill fn-watchers left by a previous OpenSwarm that died badly. will-quit is the ONLY thing that
|
||||
@@ -170,11 +170,11 @@ function installVoiceHotkey(getMainWindow) {
|
||||
lastFnPokeMs = now;
|
||||
try { fnProc.stdin.write('r\n'); } catch (_) {}
|
||||
};
|
||||
const startFnWatcherWith = (bin) => {
|
||||
const startFnWatcherWith = (bin, noPrompt) => {
|
||||
sweepStrayFnWatchers(bin);
|
||||
try {
|
||||
// stdin stays open on purpose: "r\n" re-arms the tap, and EOF tells an orphaned watcher to die.
|
||||
fnProc = spawn(bin, [], { stdio: ['pipe', 'pipe', 'ignore'] });
|
||||
fnProc = spawn(bin, noPrompt ? ['--no-prompt'] : [], { stdio: ['pipe', 'pipe', 'ignore'] });
|
||||
} catch (e) {
|
||||
console.log('[voice] fn watcher spawn failed:', e && e.message);
|
||||
fnProc = null;
|
||||
@@ -304,6 +304,10 @@ function installVoiceHotkey(getMainWindow) {
|
||||
};
|
||||
try {
|
||||
if (fs.existsSync(path.join(app.getPath('userData'), 'dictation-used'))) armNativeTiers();
|
||||
// No marker yet: fn is the DEFAULT hotkey, and the watcher is the only thing that can see it,
|
||||
// so probe it in --no-prompt mode. Machines with Input Monitoring already granted get a working
|
||||
// fn immediately; ungranted machines exit silently and never see a boot-time TCC prompt (ENG-341).
|
||||
else if (combo.special === 'fn') startFnWatcher(true);
|
||||
} catch (_) {}
|
||||
app.on('browser-window-focus', () => { unregisterFallbackShortcut(); pokeFnWatcher(); });
|
||||
app.on('browser-window-blur', registerVoiceShortcut);
|
||||
@@ -326,6 +330,14 @@ function installVoiceHotkey(getMainWindow) {
|
||||
const installVoiceHoldRelay = (contents) => {
|
||||
contents.on('before-input-event', (event, input) => {
|
||||
if (input.type !== 'keyDown' || input.isAutoRepeat) return;
|
||||
// A bare Fn keydown Chromium happens to deliver while focused is both dictation intent
|
||||
// (full-arm the tiers, prompt lands with context) and a press that must WORK right now.
|
||||
if (combo.special === 'fn' && !primaryProven() && (input.key === 'Fn' || input.code === 'Fn')) {
|
||||
armNativeTiers();
|
||||
sendFallbackToggle();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (inputMatchesCombo(input)) {
|
||||
if (!primaryProven()) sendFallbackToggle();
|
||||
event.preventDefault();
|
||||
@@ -347,6 +359,7 @@ function installVoiceHotkey(getMainWindow) {
|
||||
fallbackCombo = combo.special ? parseCombo(LEGACY_COMBO) : combo;
|
||||
if (UiohookKeyRef && !combo.special) tapKeycode = uiohookKeycodeFor(combo.key, UiohookKeyRef);
|
||||
if (tiersArmed) startFnWatcher();
|
||||
else if (combo.special === 'fn') startFnWatcher(true);
|
||||
unregisterFallbackShortcut();
|
||||
registerVoiceShortcut();
|
||||
console.log('[voice] hotkey set to', combo.accel);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// The fn dead-key deadlock (Haik, exp.16, ~99% of devices): the dictation-used marker was only
|
||||
// written by a successful transcription, the fn watcher only started once the marker existed, and
|
||||
// fn was the default hotkey, so a never-dictated install could never dictate. The fix is a boot
|
||||
// probe that arms fn WITHOUT ever prompting (IOHIDCheckAccess preflight) plus intent arming.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const swiftSrc = fs.readFileSync(path.join(__dirname, 'native', 'fn-watcher.swift'), 'utf8');
|
||||
const hotkeySrc = fs.readFileSync(path.join(__dirname, 'voiceHotkey.js'), 'utf8');
|
||||
|
||||
test('the swift preflight refuses to prompt in --no-prompt mode', () => {
|
||||
const preflight = swiftSrc.indexOf('--no-prompt');
|
||||
const firstTap = swiftSrc.indexOf('func armTap');
|
||||
assert.ok(preflight > -1, 'preflight missing');
|
||||
assert.ok(preflight < firstTap, 'preflight must run before any tap can raise the TCC prompt');
|
||||
assert.match(swiftSrc, /IOHIDCheckAccess\(kIOHIDRequestTypeListenEvent\)/);
|
||||
});
|
||||
|
||||
test('boot probes fn silently when the marker is absent', () => {
|
||||
const bootBlock = hotkeySrc.split("fs.existsSync(path.join(app.getPath('userData'), 'dictation-used'))")[1].slice(0, 400);
|
||||
assert.match(bootBlock, /combo\.special === 'fn'.*startFnWatcher\(true\)/s,
|
||||
'a fresh install with the default fn hotkey must get the no-prompt probe at boot');
|
||||
});
|
||||
|
||||
test('rebinding to fn before tiers are armed also probes', () => {
|
||||
assert.match(hotkeySrc, /else if \(combo\.special === 'fn'\) startFnWatcher\(true\);\n unregisterFallbackShortcut/);
|
||||
});
|
||||
|
||||
test('a focused bare Fn keydown arms the tiers and toggles', () => {
|
||||
const relay = hotkeySrc.split('installVoiceHoldRelay')[1].slice(0, 900);
|
||||
assert.match(relay, /input\.key === 'Fn'/);
|
||||
assert.match(relay, /armNativeTiers\(\)/);
|
||||
assert.match(relay, /sendFallbackToggle\(\)/);
|
||||
});
|
||||
|
||||
test('the probe flag reaches the spawn argv', () => {
|
||||
assert.match(hotkeySrc, /spawn\(bin, noPrompt \? \['--no-prompt'\] : \[\]/);
|
||||
});
|
||||
Reference in New Issue
Block a user