source dump of claude code
at main 150 lines 5.3 kB view raw
1import { normalizeLanguageForSTT } from '../../hooks/useVoice.js' 2import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js' 3import { logEvent } from '../../services/analytics/index.js' 4import type { LocalCommandCall } from '../../types/command.js' 5import { isAnthropicAuthEnabled } from '../../utils/auth.js' 6import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' 7import { settingsChangeDetector } from '../../utils/settings/changeDetector.js' 8import { 9 getInitialSettings, 10 updateSettingsForSource, 11} from '../../utils/settings/settings.js' 12import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js' 13 14const LANG_HINT_MAX_SHOWS = 2 15 16export const call: LocalCommandCall = async () => { 17 // Check auth and kill-switch before allowing voice mode 18 if (!isVoiceModeEnabled()) { 19 // Differentiate: OAuth-less users get an auth hint, everyone else 20 // gets nothing (command shouldn't be reachable when the kill-switch is on). 21 if (!isAnthropicAuthEnabled()) { 22 return { 23 type: 'text' as const, 24 value: 25 'Voice mode requires a Claude.ai account. Please run /login to sign in.', 26 } 27 } 28 return { 29 type: 'text' as const, 30 value: 'Voice mode is not available.', 31 } 32 } 33 34 const currentSettings = getInitialSettings() 35 const isCurrentlyEnabled = currentSettings.voiceEnabled === true 36 37 // Toggle OFF — no checks needed 38 if (isCurrentlyEnabled) { 39 const result = updateSettingsForSource('userSettings', { 40 voiceEnabled: false, 41 }) 42 if (result.error) { 43 return { 44 type: 'text' as const, 45 value: 46 'Failed to update settings. Check your settings file for syntax errors.', 47 } 48 } 49 settingsChangeDetector.notifyChange('userSettings') 50 logEvent('tengu_voice_toggled', { enabled: false }) 51 return { 52 type: 'text' as const, 53 value: 'Voice mode disabled.', 54 } 55 } 56 57 // Toggle ON — run pre-flight checks first 58 const { isVoiceStreamAvailable } = await import( 59 '../../services/voiceStreamSTT.js' 60 ) 61 const { checkRecordingAvailability } = await import('../../services/voice.js') 62 63 // Check recording availability (microphone access) 64 const recording = await checkRecordingAvailability() 65 if (!recording.available) { 66 return { 67 type: 'text' as const, 68 value: 69 recording.reason ?? 'Voice mode is not available in this environment.', 70 } 71 } 72 73 // Check for API key 74 if (!isVoiceStreamAvailable()) { 75 return { 76 type: 'text' as const, 77 value: 78 'Voice mode requires a Claude.ai account. Please run /login to sign in.', 79 } 80 } 81 82 // Check for recording tools 83 const { checkVoiceDependencies, requestMicrophonePermission } = await import( 84 '../../services/voice.js' 85 ) 86 const deps = await checkVoiceDependencies() 87 if (!deps.available) { 88 const hint = deps.installCommand 89 ? `\nInstall audio recording tools? Run: ${deps.installCommand}` 90 : '\nInstall SoX manually for audio recording.' 91 return { 92 type: 'text' as const, 93 value: `No audio recording tool found.${hint}`, 94 } 95 } 96 97 // Probe mic access so the OS permission dialog fires now rather than 98 // on the user's first hold-to-talk activation. 99 if (!(await requestMicrophonePermission())) { 100 let guidance: string 101 if (process.platform === 'win32') { 102 guidance = 'Settings \u2192 Privacy \u2192 Microphone' 103 } else if (process.platform === 'linux') { 104 guidance = "your system's audio settings" 105 } else { 106 guidance = 'System Settings \u2192 Privacy & Security \u2192 Microphone' 107 } 108 return { 109 type: 'text' as const, 110 value: `Microphone access is denied. To enable it, go to ${guidance}, then run /voice again.`, 111 } 112 } 113 114 // All checks passed — enable voice 115 const result = updateSettingsForSource('userSettings', { voiceEnabled: true }) 116 if (result.error) { 117 return { 118 type: 'text' as const, 119 value: 120 'Failed to update settings. Check your settings file for syntax errors.', 121 } 122 } 123 settingsChangeDetector.notifyChange('userSettings') 124 logEvent('tengu_voice_toggled', { enabled: true }) 125 const key = getShortcutDisplay('voice:pushToTalk', 'Chat', 'Space') 126 const stt = normalizeLanguageForSTT(currentSettings.language) 127 const cfg = getGlobalConfig() 128 // Reset the hint counter whenever the resolved STT language changes 129 // (including first-ever enable, where lastLanguage is undefined). 130 const langChanged = cfg.voiceLangHintLastLanguage !== stt.code 131 const priorCount = langChanged ? 0 : (cfg.voiceLangHintShownCount ?? 0) 132 const showHint = !stt.fellBackFrom && priorCount < LANG_HINT_MAX_SHOWS 133 let langNote = '' 134 if (stt.fellBackFrom) { 135 langNote = ` Note: "${stt.fellBackFrom}" is not a supported dictation language; using English. Change it via /config.` 136 } else if (showHint) { 137 langNote = ` Dictation language: ${stt.code} (/config to change).` 138 } 139 if (langChanged || showHint) { 140 saveGlobalConfig(prev => ({ 141 ...prev, 142 voiceLangHintShownCount: priorCount + (showHint ? 1 : 0), 143 voiceLangHintLastLanguage: stt.code, 144 })) 145 } 146 return { 147 type: 'text' as const, 148 value: `Voice mode enabled. Hold ${key} to record.${langNote}`, 149 } 150}