init claude-code
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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}`,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user