()
| 257 | } |
| 258 | |
| 259 | export async function checkRecordingAvailability(): Promise<RecordingAvailability> { |
| 260 | // Remote environments have no local microphone |
| 261 | if (isRunningOnHomespace() || isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) { |
| 262 | return { |
| 263 | available: false, |
| 264 | reason: |
| 265 | 'Voice mode requires microphone access, but no audio device is available in this environment.\n\nTo use voice mode, run Claude Code locally instead.', |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | // Native audio module (cpal) handles everything on macOS, Linux, and Windows |
| 270 | const napi = await loadAudioNapi() |
| 271 | if (napi.isNativeAudioAvailable()) { |
| 272 | return { available: true, reason: null } |
| 273 | } |
| 274 | |
| 275 | // Windows has no supported fallback |
| 276 | if (process.platform === 'win32') { |
| 277 | return { |
| 278 | available: false, |
| 279 | reason: |
| 280 | 'Voice recording requires the native audio module, which could not be loaded.', |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | const wslNoAudioReason = |
| 285 | 'Voice mode could not access an audio device in WSL.\n\nWSL2 with WSLg (Windows 11) provides audio via PulseAudio — if you are on Windows 10 or WSL1, run Claude Code in native Windows instead.' |
| 286 | |
| 287 | // On Linux (including WSL), probe arecord. hasCommand() is insufficient: |
| 288 | // the binary can exist while the device open() fails (WSL1, Win10-WSL2, |
| 289 | // headless Linux). WSL2+WSLg (Win11 default) works via PulseAudio RDP |
| 290 | // pipes — cpal fails (no /proc/asound/cards) but arecord succeeds. |
| 291 | if (process.platform === 'linux' && hasCommand('arecord')) { |
| 292 | const probe = await probeArecord() |
| 293 | if (probe.ok) { |
| 294 | return { available: true, reason: null } |
| 295 | } |
| 296 | if (getPlatform() === 'wsl') { |
| 297 | return { available: false, reason: wslNoAudioReason } |
| 298 | } |
| 299 | logForDebugging(`[voice] arecord probe failed: ${probe.stderr}`) |
| 300 | // fall through to SoX |
| 301 | } |
| 302 | |
| 303 | // Fallback: check for SoX |
| 304 | if (!hasCommand('rec')) { |
| 305 | // WSL without arecord AND without SoX: the generic "install SoX" |
| 306 | // hint below is misleading on WSL1/Win10 (no audio devices at all), |
| 307 | // but correct on WSL2+WSLg (SoX works via PulseAudio). Since we can't |
| 308 | // distinguish WSLg-vs-not without a backend to probe, show the WSLg |
| 309 | // guidance — it points WSL1 users at native Windows AND tells WSLg |
| 310 | // users their setup should work (they can install sox or alsa-utils). |
| 311 | // Known gap: WSL with SoX but NO arecord skips both this branch and |
| 312 | // the probe above — hasCommand('rec') lies the same way. We optimistically |
| 313 | // trust it (WSLg+SoX would work) rather than probeSox() for a near-zero |
| 314 | // population (WSL1 × minimal distro × SoX-but-not-alsa-utils). |
| 315 | if (getPlatform() === 'wsl') { |
| 316 | return { available: false, reason: wslNoAudioReason } |
no test coverage detected