镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(inbox): the voice mic never stays stuck, and no tap does nothing - #2519

Merged
backnotprop merged 5 commits into
mainfrom
fix/inbox-voice-mic-stuck
Nov 2, 2026
Merged

backnotprop merged 5 commits into
mainfrom
fix/inbox-voice-mic-stuck

Conversation

@backnotprop

@backnotprop backnotprop commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

The owner reported that on iPhone Safari (0.28.45, New message) the mic sometimes did nothing when tapped: "I tapped, and nothing happened, so visually it was an issue." This PR makes every tap show at once, ends every stuck path at rest, and adds voice lines to the opt-in debug log so a repeat can be diagnosed.

What changes

Case main this PR
A tap while the mic is opening (it can take a while on a phone) the button lit, but no words for 1 s; nothing else moved at once: the button is lit with the spinner. "Opening the mic…" (or "Asking for the mic…") and Cancel follow after 150 ms
A second tap while opening ignored, with nothing on screen to say why still ignored, but the spinner and line stay, and the take listens once the mic opens. Cancel is the way out. (An earlier commit here made it cancel; the owner turned that down: unseen, it is worse.)
A mic that never opens waits forever (Cancel shown after 1 s) gives up after 10 s (30 s while the browser is asking) with the "microphone didn't start" note, so the next tap works
new AudioContext() throws as the mic opens stuck in opening for good: lit, no line, no Cancel, Send off, every tap ignored micfailed note, Send free, the next tap records
recorder.stop() rejects spinner for good badclip note, microphone released

Every other real-path flow I tried already returned to rest in Chromium (fake capture) and WebKit (iPhone viewport, a tone): quick re-taps, a tap while transcribing, double taps, compose closed mid-take and reopened, a suspended AudioContext, a track that ends, and a worklet that fails to load once.

A take that ends before the mic opens lets go of it

VoiceDeps.open(signal). Cancel, the box closing and the give-up abort the open. openVoiceRecorder then closes its AudioContext at once, not when getUserMedia settles (which may be never). A stream that arrives later has its tracks stopped before the worklet loads. The slow line and both give-up timers are cleared when the mic opens or the take ends, so an old take's give-up can't reach a newer take. Space or Enter while opening acts as a tap: busy, nothing cancelled. 10 s is the best case: a browser that can't say whether it will ask (often iOS) waits 30 s.

Debug log (opt-in, off by default, no UI)

voice_press (each input with the phase and mode it met), voice_phase (each change), and voice_open (each step of opening the mic, with ms, the AudioContext's state and an error name). They hold fixed words only, never audio or text. They are documented in docs/agent-guides/inbox-window.md.

What this does not prove

The owner's exact trigger is unknown. In their debug log, the taps that did nothing made no request, which fits a mic slow to open (before this PR: no visible change for 1 s, and a second tap ignored) or a mic that never opened. The voice lines will show which next time.

Tests

  • packages/inbox/voiceSession.test.ts: no cancel on a second tap, by pointer or keyboard; the line within 150 ms; both give-ups; the open aborted by Cancel, close and give-up; a cancelled take's give-up never reaching the next take; the real recorder with a mic answering after the give-up (context closed at once, tracks stopped); the throw while opening and the failed stop.
  • packages/inbox/voiceComposeRenders.test.tsx (DOM): the spinner on the first tap with no wait, kept on a second tap, gone once listening. It fails on the previous head.
  • packages/inbox/voiceDebugLog.test.ts (DOM, now in tests/dom-test-list.txt; every trouble code survives the filter) and packages/shared/inbox/debug-log.test.ts.
  • tests/e2e/inbox-voice.spec.ts: the phone test, updated.

No new keyboard shortcuts.

- A throw from `deps.open()` (`new AudioContext()` refusing) left the take in
  `opening` for good: no status line, no Cancel, Send off, every tap ignored.
  It is now a mic that did not start (`micfailed`), and the next tap opens again.
- A second tap while the mic is still opening was dropped silently. It now
  cancels (nothing is recorded yet), which also frees a mic that never opens.
- A recorder that rejects on stop left the spinner up for good. It now ends at
  rest with `badclip`, the microphone released.

Regression tests: voiceSession.test.ts (3 fail on main) and a phone New
message e2e in inbox-voice.spec.ts (fails on main).
voice_press (each press, release, lost or toggle with the phase and mode it
met), voice_phase (each change of phase, mode, prompting, slow or trouble) and
voice_open (each step of opening the microphone: context, stream, worklet,
ready, with ok, ms, the AudioContext's state and an error name). Written
through debugNote, so nothing while debug is off. The Inbox's filter keeps
only their fixed words: never audio, the words or a status line.

Tests: voiceDebugLog.test.ts (DOM: off writes nothing, on writes the lines
with no words, every field survives the filter) and debug-log.test.ts.
@backnotprop

Copy link
Copy Markdown
Owner Author

Added in d56bd55f7: voice lines in the opt-in debug log, so the next "the mic did nothing" can be told apart.

  • voice_press: each press, release, lost or toggle, with the phase and mode it met.
  • voice_phase: each change of phase, mode, prompting, slow or trouble.
  • voice_open: each step of opening the microphone (context, stream, worklet, ready), with ok, ms, the AudioContext's state, and an error name when it failed.

They are written through debugNote, so nothing is written while debug is off. The Inbox's filter (cleanWindowRecord) keeps only their fixed words: never audio, the words or a status line. How to read them is in docs/agent-guides/inbox-window.md.

Tests: packages/inbox/voiceDebugLog.test.ts (DOM: off writes nothing; on writes the lines with no words, and every field survives the filter) and packages/shared/inbox/debug-log.test.ts.

The owner's report was a tap that showed nothing. Their call on the earlier fix
(a second tap while opening cancels): unseen, that is worse.

- From the press, the mic button is lit with the spinner while the mic opens.
  Its words ("Opening the mic…", "Asking for the mic…") and Cancel follow
  after 150 ms (was 1 s), so a mic that opens at once doesn't flash a line.
- A second tap while opening is ignored again, but the busy look stays and the
  take listens once the mic opens. Cancel on the bar is the way out.
- A mic that never opens gives up after 10 s (30 s while the browser is
  asking) with the "microphone didn't start" note, so the next tap works.
- Fixes 2 and 3 stay: a throw while opening is micfailed, a failed stop is
  badclip.

Tests: voiceSession.test.ts (no cancel, line within 150 ms, both give-ups),
voiceComposeRenders.test.tsx (DOM: the spinner on the first tap with no wait,
kept on a second tap, gone once listening), inbox-voice.spec.ts updated.
Review of #2519:
- voiceDebugLog.test.ts joins the DOM test list (the allowlist guard).
- VoiceDeps.open takes an AbortSignal. Cancel, the box closing and the
  10 s / 30 s give-up abort it. openVoiceRecorder then closes its
  AudioContext at once, not when getUserMedia settles (maybe never), and a
  stream that arrives later has its tracks stopped before the worklet loads.
  A test drives the real recorder on a stand-in AudioContext and
  getUserMedia: a mic answering after the give-up holds nothing.
- Space or Enter while the mic opens is a tap: busy, nothing cancelled.
- Every session trouble code survives the debug log's filter
  (VOICE_TROUBLE_CODES, the type derived from it).
- inbox-compose.md: the open's abort, the keyboard, and the one press that
  still starts nothing (a Send on its way).
The slow line and the 10 s / 30 s give-ups are cleared when the mic opens
or the take ends, so an old take's give-up can't reach a newer take that is
still opening, even without the run check. The test fake's canceller now
acts as clearTimeout. inbox-compose.md: 10 s is the best case; a browser
that can't say whether it will ask (often iOS) waits 30 s.
@backnotprop backnotprop added the ci:full Run the heavy CI tier on this PR (added by the merge script) label Nov 2, 2026
@backnotprop
backnotprop merged commit a072729 into main Nov 2, 2026
91 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full Run the heavy CI tier on this PR (added by the merge script)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant