mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 09:05:17 -07:00
fix(captures): allow dictation without paste permission
This commit is contained in:
@@ -567,7 +567,7 @@ export function CapturesTab() {
|
|||||||
{t('captures.actions.configure')}
|
{t('captures.actions.configure')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
{readiness.allReady && (
|
{readiness.canRecord && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleUploadClick}
|
onClick={handleUploadClick}
|
||||||
@@ -583,11 +583,11 @@ export function CapturesTab() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* Hide Dictate when readiness fails so the user can't kick off
|
{/* Hide Dictate when recording readiness fails so the user can't kick off
|
||||||
a capture that has nowhere to land. Stop stays visible if a
|
a capture that has nowhere to land. Stop stays visible if a
|
||||||
recording is somehow already in flight (e.g. a model was
|
recording is somehow already in flight (e.g. a model was
|
||||||
uninstalled mid-record) so the user can always cancel. */}
|
uninstalled mid-record) so the user can always cancel. */}
|
||||||
{(readiness.allReady || session.isRecording) && (
|
{(readiness.canRecord || session.isRecording) && (
|
||||||
<Button
|
<Button
|
||||||
onClick={session.toggleRecording}
|
onClick={session.toggleRecording}
|
||||||
disabled={session.isUploading && !session.isRecording}
|
disabled={session.isUploading && !session.isRecording}
|
||||||
@@ -842,7 +842,7 @@ export function CapturesTab() {
|
|||||||
<Captions className="h-10 w-10 mx-auto opacity-40" />
|
<Captions className="h-10 w-10 mx-auto opacity-40" />
|
||||||
<p className="text-sm">{t('captures.empty.pickOne')}</p>
|
<p className="text-sm">{t('captures.empty.pickOne')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : hotkeyEnabled && !readiness.allReady ? (
|
) : hotkeyEnabled && !readiness.canRecord ? (
|
||||||
<DictationReadinessChecklist readiness={readiness} />
|
<DictationReadinessChecklist readiness={readiness} />
|
||||||
) : hotkeyEnabled && (pushToTalkKeys.length || toggleToTalkKeys.length) ? (
|
) : hotkeyEnabled && (pushToTalkKeys.length || toggleToTalkKeys.length) ? (
|
||||||
<div className="max-w-sm mx-auto text-center space-y-5">
|
<div className="max-w-sm mx-auto text-center space-y-5">
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ import { usePlatform } from '@/platform/PlatformContext';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Spawn (or quiet) the global hotkey monitor based on the saved
|
* Spawn (or quiet) the global hotkey monitor based on the saved
|
||||||
* `capture_settings.hotkey_enabled` flag AND the dictation readiness gates,
|
* `capture_settings.hotkey_enabled` flag and the recording readiness gates,
|
||||||
* and keep its bindings in sync with the user's chord choices.
|
* and keep its bindings in sync with the user's chord choices.
|
||||||
*
|
*
|
||||||
* Boot sequence:
|
* Boot sequence:
|
||||||
* - hotkey_enabled = false OR any readiness gate missing → call
|
* - hotkey_enabled = false OR a recording gate is missing → call
|
||||||
* `disable_hotkey` (no-op if monitor was never spawned). Crucially, we do
|
* `disable_hotkey` (no-op if monitor was never spawned). Crucially, we do
|
||||||
* *not* call `enable_hotkey` in this state, so the macOS Input Monitoring
|
* *not* call `enable_hotkey` in this state, so the macOS Input Monitoring
|
||||||
* TCC prompt is never triggered for users who haven't opted in, AND the
|
* TCC prompt is never triggered for users who haven't opted in, AND the
|
||||||
* chord physically can't fire when models aren't downloaded — preventing
|
* chord physically can't fire when models aren't downloaded — preventing
|
||||||
* the "stuck pill" failure mode where dictation triggers but has nowhere
|
* the "stuck pill" failure mode where dictation triggers but has nowhere
|
||||||
* to land.
|
* to land.
|
||||||
* - hotkey_enabled = true AND all gates green → call `enable_hotkey` with
|
* - hotkey_enabled = true AND recording gates green → call `enable_hotkey` with
|
||||||
* the saved chords. This creates the CGEventTap and triggers the TCC
|
* the saved chords. This creates the CGEventTap and triggers the TCC
|
||||||
* prompt on first opt-in. Re-runs whenever a gate flips green (e.g. the
|
* prompt on first opt-in. Re-runs whenever a gate flips green (e.g. the
|
||||||
* user finishes downloading Whisper in another tab) so the chord
|
* user finishes downloading Whisper in another tab) so the chord
|
||||||
@@ -28,7 +28,7 @@ import { usePlatform } from '@/platform/PlatformContext';
|
|||||||
export function useChordSync() {
|
export function useChordSync() {
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
const { settings } = useCaptureSettings();
|
const { settings } = useCaptureSettings();
|
||||||
const { allReady } = useDictationReadiness();
|
const { canRecord } = useDictationReadiness();
|
||||||
const enabled = settings?.hotkey_enabled;
|
const enabled = settings?.hotkey_enabled;
|
||||||
const pushKeys = settings?.chord_push_to_talk_keys;
|
const pushKeys = settings?.chord_push_to_talk_keys;
|
||||||
const toggleKeys = settings?.chord_toggle_to_talk_keys;
|
const toggleKeys = settings?.chord_toggle_to_talk_keys;
|
||||||
@@ -36,7 +36,7 @@ export function useChordSync() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!platform.metadata.isTauri) return;
|
if (!platform.metadata.isTauri) return;
|
||||||
if (enabled === undefined || !pushKeys || !toggleKeys) return;
|
if (enabled === undefined || !pushKeys || !toggleKeys) return;
|
||||||
const shouldArm = enabled && allReady;
|
const shouldArm = enabled && canRecord;
|
||||||
const command = shouldArm ? 'enable_hotkey' : 'disable_hotkey';
|
const command = shouldArm ? 'enable_hotkey' : 'disable_hotkey';
|
||||||
const args = shouldArm ? { pushToTalk: pushKeys, toggleToTalk: toggleKeys } : {};
|
const args = shouldArm ? { pushToTalk: pushKeys, toggleToTalk: toggleKeys } : {};
|
||||||
invoke(command, args).catch((err) => {
|
invoke(command, args).catch((err) => {
|
||||||
@@ -45,7 +45,7 @@ export function useChordSync() {
|
|||||||
}, [
|
}, [
|
||||||
platform.metadata.isTauri,
|
platform.metadata.isTauri,
|
||||||
enabled,
|
enabled,
|
||||||
allReady,
|
canRecord,
|
||||||
// Stringify so a referentially-new array with the same content
|
// Stringify so a referentially-new array with the same content
|
||||||
// doesn't fire a redundant invoke on every settings refetch.
|
// doesn't fire a redundant invoke on every settings refetch.
|
||||||
pushKeys?.join(','),
|
pushKeys?.join(','),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type ReadinessGate = 'stt' | 'llm' | 'input_monitoring' | 'accessibility'
|
|||||||
|
|
||||||
export interface DictationReadiness {
|
export interface DictationReadiness {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
canRecord: boolean;
|
||||||
allReady: boolean;
|
allReady: boolean;
|
||||||
/** Subset of gates that are NOT yet satisfied — what the checklist renders. */
|
/** Subset of gates that are NOT yet satisfied — what the checklist renders. */
|
||||||
missing: ReadinessGate[];
|
missing: ReadinessGate[];
|
||||||
@@ -26,12 +27,11 @@ export interface DictationReadiness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single source of truth for "can the user trigger dictation right now?"
|
* Single source of truth for dictation readiness.
|
||||||
*
|
*
|
||||||
* Combines four gates into one struct so the chord-sync hook can refuse to
|
* ``canRecord`` covers the gates that must be green before the chord can
|
||||||
* arm the global hotkey unless every gate is green — the "stuck pill" we
|
* start recording. ``allReady`` also includes Accessibility, which only gates
|
||||||
* used to get on missing models is solved by never letting the chord fire
|
* synthetic paste — dictation still records and lands in Captures without it.
|
||||||
* in the first place.
|
|
||||||
*
|
*
|
||||||
* Gates:
|
* Gates:
|
||||||
* - stt / llm: backend ``/capture/readiness`` (polled, since downloads
|
* - stt / llm: backend ``/capture/readiness`` (polled, since downloads
|
||||||
@@ -87,9 +87,11 @@ export function useDictationReadiness(): DictationReadiness {
|
|||||||
if (!llmReady) missing.push('llm');
|
if (!llmReady) missing.push('llm');
|
||||||
if (!inputMonitoring) missing.push('input_monitoring');
|
if (!inputMonitoring) missing.push('input_monitoring');
|
||||||
if (!accessibility) missing.push('accessibility');
|
if (!accessibility) missing.push('accessibility');
|
||||||
|
const canRecord = sttReady && llmReady && inputMonitoring;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading,
|
isLoading,
|
||||||
|
canRecord,
|
||||||
allReady: missing.length === 0,
|
allReady: missing.length === 0,
|
||||||
missing,
|
missing,
|
||||||
stt: data?.stt,
|
stt: data?.stt,
|
||||||
|
|||||||
Reference in New Issue
Block a user