voice.ts
commands/voice/voice.ts
151
Lines
5264
Bytes
1
Exports
9
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file lives in the command layer. It likely turns a user action into concrete program behavior.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with commands. It contains 151 lines, 9 detected imports, and 1 detected exports.
Important relationships
Detected exports
call
Keywords
voicesettingstextrecordingmodecheckguidanceservicesutilshint
Detected imports
../../hooks/useVoice.js../../keybindings/shortcutFormat.js../../services/analytics/index.js../../types/command.js../../utils/auth.js../../utils/config.js../../utils/settings/changeDetector.js../../utils/settings/settings.js../../voice/voiceModeEnabled.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { normalizeLanguageForSTT } from '../../hooks/useVoice.js'
import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js'
import { logEvent } from '../../services/analytics/index.js'
import type { LocalCommandCall } from '../../types/command.js'
import { isAnthropicAuthEnabled } from '../../utils/auth.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { settingsChangeDetector } from '../../utils/settings/changeDetector.js'
import {
getInitialSettings,
updateSettingsForSource,
} from '../../utils/settings/settings.js'
import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js'
const LANG_HINT_MAX_SHOWS = 2
export const call: LocalCommandCall = async () => {
// Check auth and kill-switch before allowing voice mode
if (!isVoiceModeEnabled()) {
// Differentiate: OAuth-less users get an auth hint, everyone else
// gets nothing (command shouldn't be reachable when the kill-switch is on).
if (!isAnthropicAuthEnabled()) {
return {
type: 'text' as const,
value:
'Voice mode requires a Claude.ai account. Please run /login to sign in.',
}
}
return {
type: 'text' as const,
value: 'Voice mode is not available.',
}
}
const currentSettings = getInitialSettings()
const isCurrentlyEnabled = currentSettings.voiceEnabled === true
// Toggle OFF — no checks needed
if (isCurrentlyEnabled) {
const result = updateSettingsForSource('userSettings', {
voiceEnabled: false,
})
if (result.error) {
return {
type: 'text' as const,
value:
'Failed to update settings. Check your settings file for syntax errors.',
}
}
settingsChangeDetector.notifyChange('userSettings')
logEvent('tengu_voice_toggled', { enabled: false })
return {
type: 'text' as const,
value: 'Voice mode disabled.',
}
}
// Toggle ON — run pre-flight checks first
const { isVoiceStreamAvailable } = await import(
'../../services/voiceStreamSTT.js'
)
const { checkRecordingAvailability } = await import('../../services/voice.js')
// Check recording availability (microphone access)
const recording = await checkRecordingAvailability()
if (!recording.available) {
return {
type: 'text' as const,
value:
recording.reason ?? 'Voice mode is not available in this environment.',
}
}
// Check for API key
if (!isVoiceStreamAvailable()) {
return {
type: 'text' as const,
value:
'Voice mode requires a Claude.ai account. Please run /login to sign in.',
}
}
// Check for recording tools
const { checkVoiceDependencies, requestMicrophonePermission } = await import(
'../../services/voice.js'
)
const deps = await checkVoiceDependencies()
if (!deps.available) {
const hint = deps.installCommand
? `\nInstall audio recording tools? Run: ${deps.installCommand}`
: '\nInstall SoX manually for audio recording.'
return {
type: 'text' as const,
value: `No audio recording tool found.${hint}`,
}
}
// Probe mic access so the OS permission dialog fires now rather than
// on the user's first hold-to-talk activation.
if (!(await requestMicrophonePermission())) {
let guidance: string
if (process.platform === 'win32') {
guidance = 'Settings \u2192 Privacy \u2192 Microphone'
} else if (process.platform === 'linux') {
guidance = "your system's audio settings"
} else {
guidance = 'System Settings \u2192 Privacy & Security \u2192 Microphone'
}
return {
type: 'text' as const,
value: `Microphone access is denied. To enable it, go to ${guidance}, then run /voice again.`,
}
}
// All checks passed — enable voice
const result = updateSettingsForSource('userSettings', { voiceEnabled: true })
if (result.error) {
return {
type: 'text' as const,
value:
'Failed to update settings. Check your settings file for syntax errors.',
}
}
settingsChangeDetector.notifyChange('userSettings')
logEvent('tengu_voice_toggled', { enabled: true })
const key = getShortcutDisplay('voice:pushToTalk', 'Chat', 'Space')
const stt = normalizeLanguageForSTT(currentSettings.language)
const cfg = getGlobalConfig()
// Reset the hint counter whenever the resolved STT language changes
// (including first-ever enable, where lastLanguage is undefined).
const langChanged = cfg.voiceLangHintLastLanguage !== stt.code
const priorCount = langChanged ? 0 : (cfg.voiceLangHintShownCount ?? 0)
const showHint = !stt.fellBackFrom && priorCount < LANG_HINT_MAX_SHOWS
let langNote = ''
if (stt.fellBackFrom) {
langNote = ` Note: "${stt.fellBackFrom}" is not a supported dictation language; using English. Change it via /config.`
} else if (showHint) {
langNote = ` Dictation language: ${stt.code} (/config to change).`
}
if (langChanged || showHint) {
saveGlobalConfig(prev => ({
...prev,
voiceLangHintShownCount: priorCount + (showHint ? 1 : 0),
voiceLangHintLastLanguage: stt.code,
}))
}
return {
type: 'text' as const,
value: `Voice mode enabled. Hold ${key} to record.${langNote}`,
}
}