mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 07:05:14 -07:00
feat(capture): gate global hotkey on dictation readiness checklist
Stops the "stuck pill" failure where pressing the chord with missing STT/LLM models triggers a recording that has nowhere to land. The hotkey now stays disarmed until every gate (models downloaded, Input Monitoring + Accessibility granted) is green; the empty-state checklist in CapturesTab surfaces each unmet gate with a one-click action and auto-arms the chord once everything turns green. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
868a40fb7e
commit
85a3e1363f
@@ -0,0 +1,97 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAccessibilityPermission } from '@/components/AccessibilityGate/AccessibilityGate';
|
||||
import { useInputMonitoringPermission } from '@/components/InputMonitoringGate/InputMonitoringGate';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import type { ModelReadiness } from '@/lib/api/types';
|
||||
import { usePlatform } from '@/platform/PlatformContext';
|
||||
|
||||
const READINESS_POLL_INTERVAL_MS = 5_000;
|
||||
|
||||
export type ReadinessGate = 'stt' | 'llm' | 'input_monitoring' | 'accessibility';
|
||||
|
||||
export interface DictationReadiness {
|
||||
isLoading: boolean;
|
||||
allReady: boolean;
|
||||
/** Subset of gates that are NOT yet satisfied — what the checklist renders. */
|
||||
missing: ReadinessGate[];
|
||||
stt: ModelReadiness | undefined;
|
||||
llm: ModelReadiness | undefined;
|
||||
inputMonitoring: boolean;
|
||||
accessibility: boolean;
|
||||
refetch: () => void;
|
||||
openInputMonitoringSettings: () => Promise<void>;
|
||||
openAccessibilitySettings: () => Promise<void>;
|
||||
recheckInputMonitoring: () => Promise<boolean>;
|
||||
recheckAccessibility: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single source of truth for "can the user trigger dictation right now?"
|
||||
*
|
||||
* Combines four gates into one struct so the chord-sync hook can refuse to
|
||||
* arm the global hotkey unless every gate is green — the "stuck pill" we
|
||||
* used to get on missing models is solved by never letting the chord fire
|
||||
* in the first place.
|
||||
*
|
||||
* Gates:
|
||||
* - stt / llm: backend ``/capture/readiness`` (polled, since downloads
|
||||
* finish out-of-band — e.g. user kicks off a download in another tab and
|
||||
* expects the toggle to auto-unlock when it lands)
|
||||
* - input_monitoring / accessibility: macOS TCC checks via Tauri commands
|
||||
* (rechecked on window focus by the underlying hooks)
|
||||
*
|
||||
* Hotkey-enabled is the user's intent toggle and is intentionally *not*
|
||||
* a gate here — that's `useChordSync`'s concern.
|
||||
*/
|
||||
export function useDictationReadiness(): DictationReadiness {
|
||||
const platform = usePlatform();
|
||||
const isTauri = platform.metadata.isTauri;
|
||||
|
||||
const {
|
||||
needsPermission: inputMonNeeds,
|
||||
recheck: recheckInputMon,
|
||||
openSettings: openInputMon,
|
||||
} = useInputMonitoringPermission();
|
||||
const {
|
||||
needsPermission: a11yNeeds,
|
||||
recheck: recheckA11y,
|
||||
openSettings: openA11y,
|
||||
} = useAccessibilityPermission();
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['capture-readiness'],
|
||||
queryFn: () => apiClient.getCaptureReadiness(),
|
||||
refetchInterval: READINESS_POLL_INTERVAL_MS,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
|
||||
// On the web build there's no TCC layer — treat both as granted so the
|
||||
// checklist doesn't block users who can't even open System Settings.
|
||||
const inputMonitoring = isTauri ? !inputMonNeeds : true;
|
||||
const accessibility = isTauri ? !a11yNeeds : true;
|
||||
const sttReady = data?.stt.ready ?? false;
|
||||
const llmReady = data?.llm.ready ?? false;
|
||||
|
||||
const missing: ReadinessGate[] = [];
|
||||
if (!sttReady) missing.push('stt');
|
||||
if (!llmReady) missing.push('llm');
|
||||
if (!inputMonitoring) missing.push('input_monitoring');
|
||||
if (!accessibility) missing.push('accessibility');
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
allReady: missing.length === 0,
|
||||
missing,
|
||||
stt: data?.stt,
|
||||
llm: data?.llm,
|
||||
inputMonitoring,
|
||||
accessibility,
|
||||
refetch: () => {
|
||||
refetch();
|
||||
},
|
||||
openInputMonitoringSettings: openInputMon,
|
||||
openAccessibilitySettings: openA11y,
|
||||
recheckInputMonitoring: recheckInputMon,
|
||||
recheckAccessibility: recheckA11y,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user