Skip to content

fix(mobile): pick a cloud dictation model from the phone - #31221

Merged
nwparker merged 4 commits into
mainfrom
nwparker/mobile-dictation-cloud-model
Oct 20, 2026
Merged

nwparker merged 4 commits into
mainfrom
nwparker/mobile-dictation-cloud-model

Conversation

@nwparker

@nwparker nwparker commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​130 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​130
Prod 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​88 $\color{#cf222e}{\Huge{\mathbf{−}}}$​51 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​37

ELI5

On the phone, the voice dictation setup lists speech models. Cloud models (OpenAI) were shown as plain text you couldn't tap. That's fine when the computer also has on-device models to pick. But some hosts now offer only cloud models: Windows orca serve and managed SSH or WSL servers. On those, there was nothing to choose, so dictation could never be set up from the phone. Now a ready cloud model has a Use button, just like a downloaded model.

What Changed

Before:

  • In both the dictation setup sheet and Settings → Voice, an OpenAI row said "API key set" or "Set up on desktop" and could not be selected.
  • On a cloud-only host, the sheet had no selectable rows. Every mic tap reopened it.

After:

  • A ready OpenAI row shows Use, or In use once it is selected. Selecting it sends the same speech.dictation.setup { enabled, modelId } write a downloaded model uses.
  • An OpenAI row that isn't ready says "Add an OpenAI API key on this computer". The old text said "desktop", which doesn't fit a headless host.
  • OpenAI rows never show a delete button, because there is nothing on the computer to delete.

How it works: one small function, dictationModelRowAction in mobile/src/dictation/mobile-dictation-setup.ts, decides what each row offers: in use, use, download, downloading, or "needs API key". Both MobileDictationSetupSheet.tsx and VoiceModelList.tsx render from it, so the two lists can't drift apart again.

Why

  • It works with older hosts. They already accept an OpenAI model id in speech.dictation.setup: configure() only checks that the id is in the catalog. They also report an OpenAI model as ready when a key is stored. So this needs no protocol change, and a new phone can select a cloud model on any host version.
  • One rule instead of two copies. I put the decision in a single function instead of patching each list separately, because the two lists had already copied the same read-only branch.
  • Setting the key from the phone is out of scope. Entering an API key on the phone would mean sending a secret to the host. That's a separate piece of work, and this PR doesn't depend on it.

Linked Issue

Refs #31174. Thanks to @delorean2020 for the report that led here.

This is a follow-up to #31217 (speech on headless servers), which makes these cloud-only hosts list their cloud models honestly.

Visual Proof

No screenshots. The change is the button and text on existing rows, using the existing Use button, In use tag and state text. It's covered by a render test that checks the button, the tag and the text. No new styles or layout.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated

Tests:

  • mobile/src/components/voice-model-list-cloud-models.test.tsx:
    • a ready cloud row offers Use and calls the same select handler
    • once selected it shows In use
    • with no key it shows the new text, and no delete button in either case
  • dictationModelRowAction cases in mobile-dictation-setup.test.ts.

Checks run locally:

  • mobile vitest for src/dictation, src/settings, src/components and the session sheets
  • mobile tsc
  • the full static job: lint, format, code quality, React Doctor, pnpm tc and the ratchets

Not checked on a device: I haven't run this on a real phone against a cloud-only host.

Review

  • Codex (gpt-6.1-sol, high): no findings.
  • Independent Opus review: requested separately.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Mobile and mixed versions: no change to what goes over the wire.
  • Desktop app: unaffected.
  • SSH and Windows hosts: these are the cloud-only hosts this PR exists for.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

The dictation setup sheet and Voice settings showed OpenAI models as
read-only text, so a host that lists only cloud models (Windows orca
serve, managed SSH or WSL servers) left the phone with nothing to pick.
A ready cloud row is now selectable through the same speech.dictation.setup
modelId write as a downloaded model; a cloud row without a key says to add
it on the paired computer instead of "Set up on desktop".

Refs #31174
Reported-by: delorean2020 (@delorean2020)
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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 8 minutes.

Check out review usage here.

View limit details

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: eae497d0-6692-4b6f-9fdd-76be81174c51
📥 Commits

Reviewing files that changed from the base of the PR and between 2ca412f and 987e109.

📒 Files selected for processing: 4
  • mobile/src/components/MobileDictationSetupSheet.tsx
  • mobile/src/components/VoiceModelList.tsx
  • mobile/src/components/voice-model-list-cloud-models.test.tsx
  • mobile/src/dictation/mobile-dictation-setup.ts
📝 Walkthrough

The change adds a shared classifier for model-row actions. Cloud models that are not ready require an API key. Ready models show “Use” or “In use.” On-device models show download or downloading states. The mobile setup sheet and voice model list use the classifier. Tests cover classifier results and cloud-model UI states.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 2ca41

The change is mergeable; adding direct setup-sheet tests is recommended follow-up coverage rather than a current behavior failure.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: enabling users to select a cloud dictation model from a mobile device.
Description check Passed The description is complete and focused. It explains the user impact, implementation, rationale, linked issue, visual-proof decision, automated testing, review status, compatibility considerations, an…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is complete and focused. It explains the user impact, implementation, rationale, linked issue, visual-proof decision, automated testing, review status, compatibility considerations, and checklist results. It also clearly states that device testing was not performed.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments: 1
mobile/src/components/MobileDictationSetupSheet.tsx: 1

152-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add interface tests for MobileDictationSetupSheet.

The current tests render only VoiceModelList. They do not render MobileDictationSetupSheet or invoke its ready-row handler. The helper and RPC-wrapper tests also cannot detect a setup-sheet regression that changes the Use/In use/API-key UI or the handler arguments.

Add a focused setup-sheet test that covers the three cloud-row states and asserts that pressing Use passes { enabled: true, modelId } to setDictationConfig. The existing RPC-wrapper test can continue to verify the speech.dictation.setup method.

Suggested fix
+++ b/mobile/src/components/MobileDictationSetupSheet.test.tsx
@@
+describe('MobileDictationSetupSheet cloud models', () => {
+  it('renders Use and sends the selected model payload', async () => {
+    const ready = cloud('openai-gpt-4o-mini-transcribe', 'ready')
+    const { root } = await renderSheet(setup({ models: [ready] }))
+
+    expect(texts(root)).toContain('Use')
+    await act(async () => root.findAll(isPressable)[0].props.onPress())
+
+    expect(setDictationConfig).toHaveBeenCalledWith(client, {
+      enabled: true,
+      modelId: ready.id
+    })
+  })
+
+  it('renders In use for the selected cloud model', async () => {
+    const ready = cloud('openai-gpt-4o-mini-transcribe', 'ready')
+    const { root } = await renderSheet(setup({ models: [ready], selectedModelId: ready.id }))
+
+    expect(texts(root)).toContain('In use')
+    expect(root.findAll(isPressable)).toHaveLength(0)
+  })
+
+  it('renders the API-key message without an action', async () => {
+    const { root } = await renderSheet(
+      setup({ models: [cloud('openai-gpt-4o-transcribe', 'not-downloaded')] })
+    )
+
+    expect(texts(root)).toContain(CLOUD_MODEL_NEEDS_KEY)
+    expect(root.findAll(isPressable)).toHaveLength(0)
+  })
+})

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 11fd4f48-edf0-4df1-a574-8ea456c109e9
📥 Commits

Reviewing files that changed from the base of the PR and between 10107dd and 2ca412f.

📒 Files selected for processing: 1
  • mobile/src/components/voice-model-list-cloud-models.test.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

@nwparker
nwparker merged commit 749aadc into main Oct 20, 2026
25 checks passed
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.

1 participant