[eric] dictation: using dictation asks for the fn grant every time, not once per launch (ENG-360)

This commit is contained in:
ciregenz
2026-08-21 14:30:31 -07:00
parent 76ef467a6e
commit 1947238dec
2 changed files with 41 additions and 0 deletions
+16
View File
@@ -482,15 +482,31 @@ function installVoiceHotkey(getMainWindow) {
// but a running tap makes macOS list the app in that pane for the user to flip.
ipcMain.handle('voice:request-hold-permission', () => {
armNativeTiers();
askForFnPermission();
if (process.platform === 'darwin' && !tapProven) {
try { systemPreferences.isTrustedAccessibilityClient(true); } catch (_) {}
}
return tapProven;
});
// Deliberately re-asked on every dictation attempt, not once per launch: armNativeTiers is
// one-shot, so a user who was denied (or dismissed the prompt) could never be asked again, which
// is how a permission you are willing to grant turns into a key that just never works.
let lastFnAskMs = 0;
const askForFnPermission = () => {
if (process.platform !== 'darwin' || combo.special !== 'fn' || fnProc) return;
const now = Date.now();
if (now - lastFnAskMs < 10_000) return;
lastFnAskMs = now;
unusableNotified = false;
startFnWatcher();
};
// Fires the real TCC mic prompt BEFORE the first capture: with the entitlement present but no
// prior grant, getUserMedia would still fail once and burn the user's first dictation attempt.
ipcMain.handle('voice:request-mic-access', async () => {
armNativeTiers();
// Using dictation IS the intent that earns the fn prompt, every time, not just the first.
askForFnPermission();
if (process.platform !== 'darwin') return true;
try {
if (systemPreferences.getMediaAccessStatus('microphone') === 'granted') return true;
+25
View File
@@ -92,3 +92,28 @@ test('a key that starts working retracts its own warning', () => {
assert.match(provenBlock, /lastHotkeyIssue = null/);
assert.match(provenBlock, /voice:primary-usable/);
});
// ---- Asking is part of the feature, not a one-shot side effect ----
// Eric: "if the user clicks it, they should still ask for microphone permission and fn permissions".
// armNativeTiers is one-shot, so a denied (or dismissed) grant could never be re-requested and the
// key stayed dead for the life of the install even though the user was willing to grant it.
test('using dictation re-asks for the fn grant, not just the first time', () => {
assert.match(hotkeySrc, /const askForFnPermission = \(\) => \{/);
const micHandler = hotkeySrc.split("ipcMain.handle('voice:request-mic-access'")[1].slice(0, 300);
assert.match(micHandler, /askForFnPermission\(\)/,
'the mic prompt and the fn prompt are the same moment of intent');
const holdHandler = hotkeySrc.split("ipcMain.handle('voice:request-hold-permission'")[1].slice(0, 300);
assert.match(holdHandler, /askForFnPermission\(\)/);
});
test('the re-ask is throttled so it cannot spawn a watcher per keystroke', () => {
const fn = hotkeySrc.split('const askForFnPermission')[1].slice(0, 400);
assert.match(fn, /lastFnAskMs/);
assert.match(fn, /return;/);
});
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');
});