[BUG] Voice push-to-talk regression in v2.1.83: root cause analysis + patch (useEffect dep + EventEmitter stopImmediatePropagation)

Resolved 💬 6 comments Opened Mar 25, 2026 by jsk3342 Closed May 25, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

Voice push-to-talk broke after updating to v2.1.83. Holding Space sends literal space characters into the chat input instead of activating recording. This is a regression — working fine on v2.1.81.

After debugging the minified cli.js, I identified two root cause bugs in the voice hold-detection system (useVoiceKeybindingHandler) and produced a working patch.

---

Root Cause 1 — useEffect dependency array resets hold-detection counter every render

The effect that resets key-repeat counters when leaving "recording" state includes the setState function (Y) in its dependency array. Since its reference can change across renders, the effect re-fires continuously — resetting the repeat counter (M.current) to 0 before it reaches the threshold. The hold gesture is never detected.

// Buggy — Y (setState) in deps triggers on every render
useEffect(() => { if (state !== "recording") M.current = 0; ... }, [state, Y])

// Fixed — remove Y from deps
useEffect(() => { if (state !== "recording") M.current = 0; ... }, [state])

Root Cause 2 — stopImmediatePropagation() is a no-op in Ink's EventEmitter

During recording, the voice handler calls event.stopImmediatePropagation() to prevent space from reaching the text input handler. But Ink uses Node's EventEmitter, not the DOM — stopImmediatePropagation() does nothing. Space keystrokes pass through to the chat input even while recording.

Fix: Replace with an event-consumed flag pattern + prependListener for handler priority:

// Voice handler — register with prependListener so it runs first
useInput((key, mods, event) => {
  if (handleVoiceKey(event.keypress)) event._voiceConsumed = true;
}, { isActive: active, priority: "high" })

// In useInput (GD9) — check flag before processing
if (event._voiceConsumed) return;

Supplementary tuning (not root cause, but helps edge cases)

  • Hold detection window: 120ms → 800ms
  • Repeat count threshold: 5 → 2

Verification

Both patches applied to npm package cli.js (v2.1.83), running via node cli.js on macOS ARM64. Push-to-talk fully functional — hold detection works, space no longer floods the input during recording.

Related Issues

  • #31341 — original report (3 weeks ago, same symptom)
  • #38577 — same regression report (v2.1.83)
  • #35156 — Windows variant (marked duplicate)

Steps to Reproduce

  1. Update to v2.1.83
  2. /voice to enable voice mode
  3. Hold Space to record

Expected Behavior

Space is captured exclusively by voice mode; microphone records; speech is transcribed on release.

Actual Behavior

  • "Listening" indicator appears
  • Space simultaneously passes through to chat input
  • On release: "no voice detected"

Claude Model

Opus

Is this a regression?

Yes

Last Working Version

2.1.81

Claude Code Version

2.1.83

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

zsh

View original on GitHub ↗

This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗