From 2dd10e164a52a2c2ccce135269853a54c72778d4 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 5 Aug 2026 21:15:55 -0700 Subject: [PATCH] [eric] voice: one-time Globe-key conflict tip when macOS still binds fn to the emoji picker --- electron/preload.js | 6 ++++++ electron/voiceHotkey.js | 10 ++++++++++ .../src/shared/voice/VoiceDictationContext.tsx | 14 +++++++++++++- frontend/src/shared/voice/useVoiceDictation.ts | 7 ++++++- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/electron/preload.js b/electron/preload.js index b7b751cd..259d4dc1 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -116,6 +116,12 @@ contextBridge.exposeInMainWorld('openswarm', { ipcRenderer.on('voice:hold-up', up); return () => { ipcRenderer.removeListener('voice:hold-down', down); ipcRenderer.removeListener('voice:hold-up', up); }; }, + // Fires once at fn-watcher arm when macOS's own Globe-key action is still active (emoji picker on tap). + onVoiceGlobeConflict: (cb) => { + const h = () => cb(); + ipcRenderer.on('voice:globe-conflict', h); + return () => ipcRenderer.removeListener('voice:globe-conflict', h); + }, // Hands a vetted social platform's partition cookies to its session-backed MCP shim (allowlisted domains only, gated again in the main process). getPartitionCookies: (domain) => ipcRenderer.invoke('get-partition-cookies', domain), // Silently reads the user's own chatgpt.com/claude.ai history offscreen (no card) for onboarding personalization; main owns the injected script + gates the provider. diff --git a/electron/voiceHotkey.js b/electron/voiceHotkey.js index aa7016fb..8bfe282b 100644 --- a/electron/voiceHotkey.js +++ b/electron/voiceHotkey.js @@ -169,6 +169,16 @@ function installVoiceHotkey(getMainWindow) { }); app.on('will-quit', () => { try { fnProc && fnProc.kill('SIGKILL'); } catch (_) {} }); console.log('[voice] fn watcher armed (awaiting first event to prove Input Monitoring)'); + // macOS's own Globe-key action (emoji picker by default) fires on a quick fn tap alongside us; + // tell the renderer once so it can point the user at "Press Globe key to: Do Nothing". + require('child_process').exec('defaults read com.apple.HIToolbox AppleFnUsageType', (err, out) => { + const usage = err ? '2' : String(out).trim(); + if (usage !== '0') { + const win = getMainWindow(); + if (win && !win.isDestroyed()) win.webContents.send('voice:globe-conflict'); + console.log(`[voice] Globe key system action is active (AppleFnUsageType=${usage}); quick fn taps also trigger it`); + } + }); }; let tapKeycode; diff --git a/frontend/src/shared/voice/VoiceDictationContext.tsx b/frontend/src/shared/voice/VoiceDictationContext.tsx index 31b6c585..52b92062 100644 --- a/frontend/src/shared/voice/VoiceDictationContext.tsx +++ b/frontend/src/shared/voice/VoiceDictationContext.tsx @@ -10,7 +10,19 @@ import VoiceOverlay from './VoiceOverlay'; // out-of-sync state. Mounted once near the app root. export function VoiceDictationProvider({ children }: { children: React.ReactNode }): React.ReactElement { - const { state, lastText, error, pct, feedback, partial, toggle, start, stop, cancel, volumeRef } = useVoiceDictation(); + const { state, lastText, error, pct, feedback, partial, toggle, start, stop, cancel, notify, volumeRef } = useVoiceDictation(); + + // fn is the dictation key, but macOS may still have its own Globe action bound (emoji picker on a quick tap); say so once. + const globeWarnedRef = useRef(false); + useEffect(() => { + const bridge = window as unknown as { openswarm?: { onVoiceGlobeConflict?: (cb: () => void) => () => void } }; + const off = bridge.openswarm?.onVoiceGlobeConflict?.(() => { + if (globeWarnedRef.current) return; + globeWarnedRef.current = true; + notify('Tip: set System Settings > Keyboard > "Press Globe key to" to "Do Nothing" so fn only dictates.'); + }); + return () => { off?.(); }; + }, [notify]); const holdMode = useAppSelector((s) => s.settings.data.voice_hold_to_talk ?? true); const dictationShortcut = useAppSelector((s) => s.settings.data.dictation_shortcut ?? null); const dictationModel = useAppSelector((s) => s.settings.data.dictation_model ?? null); diff --git a/frontend/src/shared/voice/useVoiceDictation.ts b/frontend/src/shared/voice/useVoiceDictation.ts index 3f8fdbe7..dd780d00 100644 --- a/frontend/src/shared/voice/useVoiceDictation.ts +++ b/frontend/src/shared/voice/useVoiceDictation.ts @@ -294,5 +294,10 @@ export function useVoiceDictation() { // A dangling recorder (unmount mid-capture) must release the mic. useEffect(() => () => { teardown(); }, [teardown]); - return { state, lastText, error, pct, feedback, partial, toggle, start, stop, cancel, volumeRef }; + // One-line notices from outside the session flow (Globe-key conflict, permission hints) ride the same bubble. + const notify = useCallback((text: string): void => { + setFeedback({ tone: 'warn', icon: 'info', text, at: Date.now() }); + }, []); + + return { state, lastText, error, pct, feedback, partial, toggle, start, stop, cancel, notify, volumeRef }; }