Skip to content

fix(voice): start iPhone dictation from a tap, and stop asking for the mic on On in a home-screen app (#2343) - #2370

Merged
devswha merged 3 commits into
mainfrom
fix/ios-pwa-voice-crash
Oct 29, 2026
Merged

devswha merged 3 commits into
mainfrom
fix/ios-pwa-voice-crash

Conversation

@devswha

@devswha devswha commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Closes #2343. Thanks to @osamahbeig for the report and the diagnosis.

What was wrong

  • Safari tab, mic allowed, "permission denied" (browser speech engine, no OpenAI key). MicButton started a take on pointerdown. iOS WebKit lets SpeechRecognition.start() run only from a user activation, and a touch's pointerdown is not one. On the iOS 27 Simulator, a tap logs navigator.userActivation.isActive as false on pointerdown/touchstart and true on pointerup/touchend/click. A 900 ms hold is false throughout and fires no click. So every touch take ended in not-allowed, which we showed as "Microphone permission was denied". The click fallback never ran for a touch, because it skips clicks within 500 ms of a pointerup.
  • Home-screen app closes or reloads on Settings > Voice input > On. Choosing On called getUserMedia right away. Nothing else in that path differs from a Safari tab.

Change

  • MicButton: with the browser engine and a non-mouse pointer (touch or pen), the tap's click starts or finishes the take. Which of the two is decided at pointerdown, so a take that ends on its own before the click is not started again. A mouse keeps press-and-hold and tap-to-toggle. The server engine (MediaRecorder) keeps press-and-hold on touch too: its getUserMedia and AudioContext resume work from pointerdown, and the Simulator shows the AudioContext reaching running. Only the pointer that pressed may release a hold, so a right-click's pointerup no longer finishes a take. The browser engine's mic is labelled "Dictate" instead of "Dictate (hold)".
  • Settings: in an iPhone home-screen app (navigator.standalone), On no longer asks for the microphone at once; the first tap of the mic asks.
  • service-not-allowed (the device's recognizer is off, i.e. Siri & Dictation, or has no model for the language) now says "Speech recognition is off or unavailable on this device" instead of blaming the permission. Translated into ko/ja/zh.
  • iOS app: NSSpeechRecognitionUsageDescription in Info.plist. WKWebView's speech recognition asks SFSpeechRecognizer for access from the app process, and an app without that key is terminated when it asks. Mirrored in app/store/ios.md.
  • DESIGN.md's Voice input section records the tap/hold rules.

Design reference: the Claude and ChatGPT iOS apps, and Safari's own dictation fields, start dictation with a tap and stop it with a second tap; a hold is not their gesture. A mouse keeps push-to-talk here because desktop browsers allow it.

Verification

  • Fail-first: src/components/VoiceInput.test.tsx failed on main's MicButton (press on touch pointerdown). Its stop-tap and right-click cases failed on the first fix commit. All pass now. src/lib/voice.test.ts pins service-not-allowed -> speech_unavailable.
  • bun run check fast is green on b5776e6 (rebased on ddc74b0): 8666 unit tests pass, 0 fail; typecheck, build and generated types are clean.
  • iOS 27 Simulator (eamac, own device, WebDriverAgent taps), the demo build with an event hook:
    • main, Safari tab: Settings On calls getUserMedia. On the probe page, a touch pointerdown start runs with isActive=false.
    • fix, Safari tab: a mic tap logs pointerdown (isActive=false) -> pointerup -> click -> SpeechRecognition.start isActive=true. The pill shows "Ready…" and the keyboard stays closed.
    • fix, home-screen app: Auto -> On calls no getUserMedia, and the app does not reload. A mic tap then starts recognition from the click (isActive=true).

Known limits

  • The Simulator's WebKit site-permission sheet ignores synthesized taps, so a whole recording through to text was not observed. The home-screen reload itself did not reproduce in the Simulator either: a bare getUserMedia kept its sheet up for 6 minutes with no reload. The Settings change removes the only request that path made. Whether the first mic tap in a home-screen app now triggers the same reload on a real iPhone is unconfirmed, and needs a check on the device (the reporter offered to test a build).
  • With the browser engine, holding the mic on a touch screen now does nothing (Android Chrome included), where Chrome used to treat it as push-to-talk. A tap works everywhere.
  • In a home-screen app the Settings page can no longer show "Microphone permission was denied". A denial shows in the input's note on the first tap instead.

Reviews

  • omo-native-code-reviewer: APPROVE. Its MEDIUM (the changelog claimed the reload was fixed) is resolved: the fragment now says the request was removed, not that the reload is confirmed fixed. Its LOW about the label is fixed; the other LOWs are listed above.
  • Codex Sol 6.1 Fast xhigh, round 1: REQUEST_CHANGES with two MEDIUMs, a stop tap restarting a take and a right-click releasing a touch-started take, plus the label. All three are fixed, each with a test that failed before.
  • Codex final round, on the delta only: it confirmed those fixes and found two new MEDIUMs.
    • A touch hold on the stop button (which fires no click) left a stale "finish" that swallowed the next Enter or Space. Fixed: a tap's gesture now counts only for a click within 500 ms of its pointerup. A test covers it and failed before.
    • With the server engine, two overlapping touches whose first click arrives while the second is still down can start a second take. It is left as a known limit under the lane's final-round rule. It needs a multi-touch on the mic, which Codex itself could not deliver natively (browsers may suppress that click), and the server engine's pointerdown path is the one main already had.

…e mic on On in a home-screen app (#2343)

iOS WebKit starts SpeechRecognition only from a user activation, and a
touch's pointerdown is none, so every touch take on the browser engine
ended in "not-allowed", shown as "Microphone permission was denied".
A touch on the browser engine now starts and finishes the take from the
tap's click; a mouse and the server engine keep press-and-hold.

Choosing On in Settings asked getUserMedia at once, which closed or
reloaded an iPhone home-screen app; there the first mic tap asks now.
"service-not-allowed" (the device's recognizer is off or lacks the
language) gets its own message, and the iOS app declares
NSSpeechRecognitionUsageDescription for WKWebView's speech recognition.
…nter releases a hold (#2343)

Review round (Codex Sol): a touch tap now picks start or finish at its
pointerdown, so a take that ended before the click is not started again,
and a pointerup from another pointer (a right-click) no longer finishes a
hold. The browser engine's mic is labelled "Dictate", since a touch cannot
hold it. The changelog no longer claims the home-screen reload is
confirmed fixed.
…Enter or Space (#2343)

Codex Sol final round: a touch hold on the stop button ends with no click,
so its "finish" stayed pending and a later keyboard activation finished an
idle take instead of starting one. A tap's gesture now counts only for a
click within 500 ms of its pointerup.
@coderabbitai

coderabbitai Bot commented Oct 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 11 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between ddc74b0 and b5776e6.

Files selected for processing: 12
  • DESIGN.md
  • app/ios/App/App/Info.plist
  • app/store/ios.md
  • changelog.d/ios-voice-input-tap.md
  • src/components/SettingsDialog.tsx
  • src/components/VoiceInput.test.tsx
  • src/components/VoiceInput.tsx
  • src/lib/i18n/composer.ja.ts
  • src/lib/i18n/composer.ko.ts
  • src/lib/i18n/composer.zh.ts
  • src/lib/voice.test.ts
  • src/lib/voice.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devswha

devswha commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

CodeRabbit hit its review limit on this PR, so no CodeRabbit review ran. Two reviews ran in its place, both adversarial and read-only. Their findings and how each was resolved:

omo-native-code-reviewer (on 87da4206): APPROVE

  • MEDIUM: no one has confirmed what made the home-screen app reload; the Simulator does not reproduce it. Skipping the request on Settings > On may only move it to the first mic tap, so the changelog should not claim the reload is fixed. Resolved: the fragment and the PR body now say the request was removed, not that the reload is confirmed fixed. A check on a real iPhone is still needed; the reporter offered to test a build.
  • LOW: the mic is still labelled "Dictate (hold)". Fixed in 9d41878: the browser engine's mic is labelled "Dictate", translated into ko/ja/zh.
  • LOW: holding the mic with the browser engine now does nothing on a touch screen (Android Chrome included). Intended, since browser speech can only start from a click; recorded in DESIGN.md and the PR's known limits.
  • LOW: in a home-screen app, the Settings page can no longer show a denied permission. Accepted: a denial still shows in the input's note on the first tap.
  • LOW: thin coverage in the new test. Addressed by the three cases added in the later rounds.

Codex Sol 6.1 Fast xhigh, round 1 (on 87da4206): REQUEST_CHANGES

  • MEDIUM: if recognition ended between a stop tap's pointerdown and its click, the click started a new take. Fixed in 9d41878: a tap decides at pointerdown whether it starts or finishes the take. Test: "finishes from a stop tap even when the take ended before its click" (failed on 87da4206).
  • MEDIUM: a take started by touch kept a stale hold time, so a later right-click's pointerup finished it. Fixed in 9d41878: only the pointer that pressed can release a hold. Test: "lets only the pointer that pressed release a hold" (failed on 87da4206).
  • LOW: the "Dictate (hold)" label. Fixed, as above.

Codex final round, on the delta only (on 84153973): REQUEST_CHANGES

  • It confirmed the three round-1 fixes.
  • MEDIUM: a touch hold on the stop button fires no click, so its pending "finish" made the next Enter or Space finish an idle take instead of starting one. Fixed in b5776e6: a tap's gesture counts only for a click within 500 ms of its pointerup. Test: "drops a touch hold's gesture that got no click" (failed on 84153973).
  • MEDIUM, left as a known limit: with the server engine (OpenAI key) still starting a take, two overlapping touches whose first click arrives while the second finger is down can start a second take. This needs a multi-touch on the mic. Codex could not get a real browser to send that click sequence (browsers may suppress clicks during multi-touch), and the server engine's pointerdown path is the one main already had. Per the lane's final-round rule it is not fixed here; it is listed in the PR body.

bun run check fast is green on the final head b5776e6: 8666 unit tests pass, 0 fail.

@devswha

devswha commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

Merging b5776e6 (closes #2343, thanks @osamahbeig for the report). iOS starts speech recognition only from a user activation, and a touch's pointerdown is none, so with the browser engine a touch now starts or finishes a take from the tap's click (a mouse, and the server engine, keep hold-to-talk); only the pointer that pressed may release a hold. In an iPhone home-screen app, Settings > Voice input On no longer calls getUserMedia (the request that closed or reloaded it); the first mic tap asks instead. service-not-allowed gets its own note, and the iOS app gains NSSpeechRecognitionUsageDescription (takes effect with the next iOS build). Reviewed by the merge pass: the change is confined to MicButton's pointer handlers, the Settings request and one new error key (translated in ja/ko/zh); 4 fail-first tests. Known limits (from the PR body): a whole recording through to text and the home-screen reload were not observed in the Simulator and need a real-iPhone check; with the browser engine a touch hold no longer does push-to-talk (Android Chrome included), a tap works everywhere; the home-screen Settings page no longer shows the denied note (the first tap does). Unreproduced, below HIGH: with the server engine, two overlapping touches whose first click lands while the second is still down can start a second take; that path is unchanged from main.

@devswha
devswha merged commit cbe3436 into main Oct 29, 2026
11 checks passed
@devswha
devswha deleted the fix/ios-pwa-voice-crash branch October 29, 2026 18:22
@devswha devswha mentioned this pull request Oct 30, 2026
devswha added a commit that referenced this pull request Oct 30, 2026
## Release 0.4.21

The release PR, prepared by step 1 of
[docs/development.md#releasing](https://github.com/devswha/ovis/blob/main/docs/development.md#releasing),
now re-folded on main `1ef6ea329108a0985b823e8235b03daddcd0a5f4` after
the owner held 0.4.21 for more work. The merge pass reported the 0.4.21
queue empty.

**Do not merge yet:** the owner judges the final build before release.
No tag and no Release run from this PR's author.

- **Version:** `version` is 0.4.21 in `package.json` and
`herdr-plugin.toml`.
- **Changelog:** `bun scripts/changelog-fold.ts 0.4.21 2026-10-30`
folded 63 fragments into the 2026-10-30 section of `CHANGELOG.md` (71
entries), linked their PRs and deleted the fragments; `changelog.d`
holds only `README.md`.
- `by @login` on every outside contributor's PR link: #2361 @eddieparc,
#2333 @IdrisGit, #2355 @imurfutur3, #2341 @sulthonzh, #2359 @YuuuXie,
#2377 @IdrisGit, #2269 @CharleoYang, #2405 @jpcarranza94, #2414
@homebodify, #2417 @cheol-95, #2418 @eddieparc, #2435 @cheol-95.
- Two outside PRs had no fragment and got an entry by hand: #2341
(OpenCode background commands, Added) and #2359 (installer test timing,
Maintenance).
- Issue reporters are credited in their entries: @blaker3115-Agri
(#1488), @Hyd3-14 (#2026), @osamahbeig (#2343), @Shunamxiao (#2329),
@matiemer (#2362), @kilhyeonjun (#2344), @eddieparc (#2360), @Ploppy3
(#2236), @sulthonzh (#2340), @WOULDU-pres (#2056), @odfalik (#2038),
@aNNdii (#511), @Noobear (#2281, fixed by #2424); #2435 fixes
@cheol-95's own #2434.
- Two fragments ran past the changelog's width (#2418, #2269); their
entries are re-wrapped, words unchanged. #2382's entry reads "OpenCode's
installer puts its folder first" (reviewed earlier on this PR).
- Maintainer PRs without a fragment and without a user-facing change
have no entry: #2353 (check tool), #2371 (guide docs), #2396 and #2397
(tests), #2403 and #2433 (server and client transport seams, no
behaviour change), #2437 (test-only fix for #2435's check). #2438's own
entry (the sidebar selection marked by its fill again) is dropped: the
stroke #2352 added never shipped, so users see no change; #2352's entry
keeps only the Windows high-contrast Highlight bar.
- **Release summaries:** en, ko, ja and zh each have 5 new, 5 improved
and 5 fixed lines. Since the first review: New lists the in-browser
Claude Code / Codex sign-in (#2428) in place of the tab/workspace
shortcut line; Improved lists Settings → Agents (#2427) in place of the
sidebar fold line; Fixed lists the Codex wrong-session fix (#2426,
#2430) in place of the phone screenshot paste line. The Artifacts and
Claude status-line lines keep their reviewed wording (turned on in
Settings). Not in 0.4.21: #2420 (Codex chat behind Kiro), #2065 (Portal
sign-in) and #2436 (relay foundations) stay open for a later release.
- **Remote bundle:** `REMOTE_BUNDLE_VERSION` rises from 39 to 40 because
the bundle inputs (`server/`, `shared/`, `patches/`) changed since
remote-v39. After merge: push `remote-v40` on the merged commit and wait
for the Remote PC bundles workflow before running Release.
- **iOS:** `CURRENT_PROJECT_VERSION` stays 3 (#2370 changed
`Info.plist`; build 3 is not known to be uploaded yet).

## Validation

- `bun scripts/release-guards.ts --base v0.4.20`: "Release 0.4.21:
remote bundle, telemetry Worker and release metadata are in order."
- `dori heavy … bun run check fast` on this branch: workflow syntax,
generated types and themes, typecheck, build, unit tests all ok. Unit:
9166 pass, 0 fail, 36 skip in 591 files.
- Every PR in `v0.4.20..main` was pre-run for the owner's hands-on guide
on isolated copies (own herdr session, fresh state, never the owner's
live session). Items that need a real device (iPhone tap dictation
#2370, dictation keyboards #2386, the installed desktop app's title-bar
overlay #2381) are left for the owner's final check.

This is a release-metadata PR: it changes no code path. Per the release
steps, merge it after CI and the owner's go, then push `remote-v40`,
wait for its bundles, and run Release with `0.4.21`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iPhone: voice input crashes the home-screen app, and dictation says "permission denied" in Safari

1 participant