Skip to content

feat(mobile): real-time gateway dictation, voice language setting, and test field (part 1/1) - #6099

Merged
iscekic merged 1 commit into
mainfrom
kwf/surface-the-mobile-app-apps-mobile-ecfe-l1
Sep 14, 2026
Merged

feat(mobile): real-time gateway dictation, voice language setting, and test field (part 1/1)#6099
iscekic merged 1 commit into
mainfrom
kwf/surface-the-mobile-app-apps-mobile-ecfe-l1

Conversation

@iscekic

@iscekic iscekic commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Changelog for users

  • In gateway mode, voice input shows transcribed text while you speak, in short segments, instead of only after you stop.
  • Voice settings add a Language row that shows Automatic or the chosen language.
  • Device mode's picker lists the languages the device supports, and gateway mode's picker lists the languages the app supports; both include an Automatic row and search that matches names without accents.
  • Voice settings add a "Test voice input" text area that appends recognized speech live, with a microphone button and a Clear action that empties the field.
  • A failed gateway upload mid-dictation shows the error beside the field and keeps the text already transcribed.
  • Searching the language picker now finds names on devices set to Turkish or Azerbaijani.
  • Gateway dictation now stops with an error when it cannot read the saved transcription model, instead of leaving the session stuck.
  • Picker and sheet headers now use the same top-spacing rule on iOS and Android, so iPhone sheets no longer show an empty band above Cancel, title, and Done.

Changelog for maintainers

  • The gateway engine records three-second segments and uploads each finished segment, emitting one final result per segment while the session stays listening; an upload chain applies segment results in recording order, rotation prepares the successor before stopping the current recorder, stop and abort clear the rotation timer and finalize only after the chain drains, and no-speech is reported only when the session produced no text.
  • The chosen voice-input language persists as a BCP-47 tag in SecureStore; null or empty means Automatic, and start reconciles it against the active mode's list; the new voice-language-picker formSheet renders device or gateway options, and useVoiceRecognitionLanguages memoizes the native locale fetch and adds a cache-invalidating retry.
  • Voice settings adds the Language row and the test field; the field stays uncontrolled, mirrors toast failures inline for assistive tech, and Clear aborts and clears; new copy keys voiceLanguage.* and voiceInput.test* ship in every catalog, foldForSearch is shared with the app language picker, and Android cmn* tags normalize to zh*.
  • SheetHeader now uses one top-inset rule for iOS and Android: it drops the Platform branch and reads StatusBar.currentHeight ?? 0 (an Android-only API that is undefined on iOS); bottom-form-sheet drops the top clearance unconditionally, while always stays a no-op on iOS because the synchronous fallback is 0; the doc comment records why Android caps its detents below the inset and the iOS sheet clears the top edge with its grabber.
  • The instance-picker and app-unlock react-native test mocks now provide StatusBar.currentHeight, and sheet-header.mounted.test.tsx runs the fallback, resolved-inset, bottom-form-sheet, landscape-side-inset, and PickerSheet pass-through cases under both ios and android, plus a regression asserting both platforms produce the same bottom-form-sheet style; start the review at the inset computation in sheet-header.tsx.
  • apps/mobile/src/lib/voice-input/gateway/gateway-voice-input-engine.ts:347 — accepted: the unguarded model read could reject and poison the upload chain; it now fails the session with a client error, so later segments still upload and clean up.
  • apps/mobile/src/i18n/fold-for-search.ts:10 — accepted: a Turkish or Azerbaijani locale lowered I to ı; the fold now uses toLowerCase() so language search keeps matching.
  • apps/mobile/src/components/sheet-header.mounted.test.tsx:539 — accepted: the helper returned PickerSheet's shell, so the topInset assertion was vacuous; it now derives the inset wrapper from the header row and the test asserts the Done button's ancestor. Regression tests cover the rejected model read and the locale-independent fold; start the review at the guarded read, then the fold helper.

E2E proof

Superseded and replaced: the earlier gateway-unreachable fault capture is replaced by the fault-injected dictation start below; the earlier single-query search check is replaced by the Turkish-locale and espanol/turkce checks; the earlier Android header-inset measurement is replaced by the repaired stack-tip opened-picker measurements in portrait and landscape below. The platform captures were recorded on Android; the repair removes the platform branch, so iOS has no separate inset path.

[p9] Gateway-mode test field appends text segment by segment before Stop (needs:recording): expected — android emulator-5554: p9-gateway-realtime.log shows the test EditText at 0 segments with 'Stop voice input'/'Listening...' (p9-listen-1), then 'Gateway transcription online Gateway transcription online' with 'Stop voice input'/'Listening...' (p9-listen-2), then 4 segments with 'Stop voice input'/'Listening...' (p9-listen-3), then 6 segments and 'Start voice input' after Stop (p9-stop), proving segment-by-segment appending before Stop; recording p9-gateway-realtime.mp4 captured for the visual reviewer.

p9-gateway-realtime.mp4.trim.mp4

[e1] Test field with no speech shows the no-speech toast and leaves the text area unchanged (needs:recording): expected — Android device mode (gateway transcription OFF) on emulator-5554: scene e1 OK shows 'No speech detected. Tap the microphone to try again.' while the field reads 'Tap the microphone and start speaking.' and the mic is back to 'Start voice input' — no UX-DEFECT (e1-behavior.log; recording e1-voice-test-nospeech.mp4).

e1-voice-test-nospeech.mp4.trim.mp4

[p9] Gateway-mode test field appends text segment by segment before Stop (needs:recording): expected — android emulator-5554: gateway transcription ON with Fake Transcribe, tapping Start voice input appended 'Gateway transcription online' segment by segment before Stop ('== t+3s 21:22:28' 3 copies, '== t+12s 21:22:49' 10 copies, 13 at Stop) and retained them after Stop (p9-final.png); recording p9-gateway-append.mp4, replay p9.replay.json re-ran as SCENE p9 OK; UX audit of the Voice input screen found no UX-DEFECT. e2e-index gap: no voice-transcription switch/test-id/fault hook is documented, only the Fake Transcribe model.

p9-gateway-append.mp4.trim.mp4

[p8] Device-mode test field appends the transcript to the text area as the user speaks (needs:recording): expected — android emulator-5606, device mode (Gateway transcription switch checked=false, Language Deutsch); a device-level fake RecognitionService (com.kilo.fakerec, installed + Settings.Secure voice_recognition_service) streamed canned partials through the app's real expo-speech-recognition path because the emulator is -no-audio. p8-run.log 'SCENE p8-listen OK' shows 'android.widget.EditText the quick brown fox jumps over tappable [82,992][998,1213]' beside 'android.widget.TextView Listening...' and 'android.widget.Button Stop voice input', and 'SCENE p8 OK' shows 'android.widget.EditText the quick…

p8-device-transcript.mp4.trim.mp4

[p9] Gateway-mode test field appends text segment by segment before Stop (needs:recording): expected — e2e-mobile-app/p9-final.png

[e1] Gateway-mode voice test field: start dictation when the stored transcription model cannot be read (fault-injected) — an error toast plus the inline status appear and the mic returns to 'Start voice… — android emulator-5554, nextjs down (fault.sh down nextjs) with gateway transcription on and 'Earlier transcript kept' in the test field: tapping Start voice input left the mic at 'Start voice input' (e1-fault.log "ASSERT HIT 'Start voice input' matches=1"), showed the inline error beside it (digest "android.widget.TextView Couldn't reach the Kilo gateway. Check your connection and try again. tappable [193,1187][792,1261]") and kept the transcript (e1-fault.log "ASSERT HIT 'Earlier transcript kept' matches=1"); recording e1-fault-model-unreadable.mp4 and screenshot e1-error-inline.png captured.

e1-fault-model-unreadable.mp4.trim.mp4

[e1] Gateway-mode voice test field: start dictation when the stored transcription model cannot be read (fault-injected) — an error toast plus the inline status appear and the mic returns to 'Start voice… — e2e-mobile-app/e1-error-inline.png

[e2] Voice language picker with the device locale set to Turkish (tr-TR): typing 'indonesian' lists Indonesian and typing 'italian' lists Italian. — Device locale tr-TR proved in out/e2-locale-evidence.log ('tr-TR' twice); out/e2-search-tr.log shows the picker search EditText 'android.widget.EditText text="indonesian"' beside result 'text="Bahasa Indonesia"' and 'text="id-ID"', and 'android.widget.EditText text="italian"' beside 'text="Italiano"' and 'text="it-IT"', with cross-checks 'absent: Italiano (correct)' and 'absent: Bahasa Indonesia (correct)'. The scripted scene aborted on a harness WebDriver stale element (out/e2e-run.log 'SCENE e2 MISS error WebDriverError: Cached elements ... do not exist in DOM anymore'), so the queries were completed via adb input + uiautomator dumps; captures e2-indonesian.png and e2-italian.png are left for the visual reviewer.

[e2] Voice language picker with the device locale set to Turkish (tr-TR): typing 'indonesian' lists Indonesian and typing 'italian' lists Italian. — e2e-mobile-app/e2-indonesian.png

[e2] Voice language picker with the device locale set to Turkish (tr-TR): typing 'indonesian' lists Indonesian and typing 'italian' lists Italian. — e2e-mobile-app/e2-italian.png

[e3] Voice language picker search regression: typing 'espanol' lists Español and 'turkce' lists Türkçe. — android emulator-5604 (en-US): e3-search-espanol.log lines 30-35 show only 'Español, es-ES'/'Español, es-US' after typing 'espanol'; e3-search.log lines 1,30-32 show 'SCENE e3-search OK' and only 'Türkçe, tr-TR' after typing 'turkce'; replay e3.replay.json; no UX-DEFECT observed.

[e1] [p-header] Android picker header has no dead band: on the built app open Voice settings -> Language (bottom formSheet), and confirm the header row (Cancel/title/Done) sits flush at the sheet's top… — e2e-mobile-app/e1.png

[e2] [p-header-landscape] Rotate the open picker to landscape: the header stays clear of the side cutout via left/right insets while still reserving no top gap. — e2e-mobile-app/e2.png

Owner request for the stack

Surface: the mobile app (apps/mobile).

Three related changes to voice transcription, delivered together as one PR.

  1. If possible, gateway voice transcription should be "real time": transcribe as the user talks, instead of waiting for a stop before transcription.

  2. New setting in voice: chosen language. In device mode, the user can pick any device-supported language. In gateway mode, the user can pick any language we support in the app.

  3. New "testing field" (text area) in voice transcription settings: users can test their voice settings as they are setting them up.

Comment thread apps/mobile/src/lib/voice-input/gateway/gateway-voice-input-engine.ts Outdated
Comment thread apps/mobile/src/i18n/fold-for-search.ts Outdated
Comment thread apps/mobile/src/components/sheet-header.mounted.test.tsx Outdated
@kilo-code-bot

kilo-code-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (97 files)
  • apps/mobile/src/lib/voice-input/gateway/gateway-voice-input-engine.ts
  • apps/mobile/src/lib/voice-input/gateway/gateway-voice-input-engine.test.ts
  • apps/mobile/src/i18n/fold-for-search.ts
  • apps/mobile/src/i18n/fold-for-search.test.ts
  • apps/mobile/src/components/sheet-header.tsx
  • apps/mobile/src/components/sheet-header.mounted.test.tsx
  • apps/mobile/src/components/agents/instance-picker.mounted.test.tsx
  • apps/mobile/src/components/app-unlock-screen.test-helpers.tsx
  • apps/mobile/src/app/(app)/(tabs)/(1_kiloclaw)/_layout.tsx
  • apps/mobile/app.config.ts
  • apps/mobile/src/i18n/locales/*.json (87 catalogs - key reordering only, no copy changes)

Prior Findings Resolved

  • gateway-voice-input-engine.ts:347 - the model read is now wrapped in try/catch and terminalizes with client (verified against the new rejection test).
  • fold-for-search.ts:10 - now uses toLowerCase(), with a locale-spy regression test.
  • sheet-header.mounted.test.tsx:539 - findSafeAreaWrapper now derives the wrapper from the header row and the PickerSheet test asserts the Done button's ancestor, so the assertion is no longer vacuous.
Previous Review Summary (commit 34d25a4)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 34d25a4)

Status: 3 Issues Found | Recommendation: Address before merge

Executive Summary

The real-time gateway engine leaves one credential read unguarded, so a single rejection can poison the segment upload chain, leak files, and hang the session; two lower-risk items cover locale-sensitive search folding and a vacuous new test.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/voice-input/gateway/gateway-voice-input-engine.ts 347 Unguarded await deps.readModelId() can reject, poisoning uploadChain (later segments never upload/delete their files) and suppressing the session end.

SUGGESTION

File Line Issue
apps/mobile/src/i18n/fold-for-search.ts 10 Locale-sensitive toLocaleLowerCase() breaks search on tr/az devices; use toLowerCase().
apps/mobile/src/components/sheet-header.mounted.test.tsx 539 findSafeAreaWrapper returns PickerSheet's shell View, so the new topInset forwarding test is vacuous.
Files Reviewed (105 files)
  • apps/mobile/src/lib/voice-input/gateway/gateway-voice-input-engine.ts - 1 issue
  • apps/mobile/src/lib/voice-input/gateway/gateway-voice-input-engine.test.ts
  • apps/mobile/src/lib/voice-input/gateway/native-gateway-voice-input.test.ts
  • apps/mobile/src/lib/voice-input/use-voice-input.ts
  • apps/mobile/src/lib/voice-input/use-voice-input-actions.ts
  • apps/mobile/src/lib/voice-input/use-voice-recognition-languages.ts
  • apps/mobile/src/lib/voice-input/voice-input-language.ts
  • apps/mobile/src/lib/voice-input/voice-input-language-preference.ts
  • apps/mobile/src/lib/voice-input/voice-input-language*.test.ts
  • apps/mobile/src/lib/voice-input/use-voice-input.test.ts
  • apps/mobile/src/lib/storage-keys.ts
  • apps/mobile/src/components/voice-input-settings-screen.tsx
  • apps/mobile/src/components/voice-language-picker-sheet.tsx
  • apps/mobile/src/components/voice-language-picker-sheet*.test.tsx
  • apps/mobile/src/components/voice-language-picker-sheet.test-helpers.tsx
  • apps/mobile/src/components/voice-test-field.tsx
  • apps/mobile/src/components/voice-test-field.mounted.test.tsx
  • apps/mobile/src/components/voice-input-settings-screen*.test.tsx
  • apps/mobile/src/components/picker-sheet.tsx
  • apps/mobile/src/components/sheet-header.tsx
  • apps/mobile/src/components/sheet-header*.test.tsx
  • apps/mobile/src/components/agents/instance-picker.mounted.test.tsx
  • apps/mobile/src/components/code-reviewer/review-detail-screen.mounted.test.tsx
  • apps/mobile/src/i18n/fold-for-search.ts - 1 issue
  • apps/mobile/src/i18n/language-rows.ts
  • apps/mobile/src/i18n/resolve-language.ts / .test.ts
  • apps/mobile/src/i18n/voice-copy.test.ts
  • apps/mobile/src/app/(app)/_layout.tsx, (app)/voice-language-picker.tsx, (1_kiloclaw)/_layout.tsx
  • apps/mobile/src/i18n/locales/*.json (87 catalogs - key/placeholder parity verified, no issues)

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0

Review guidance: REVIEW.md from base branch main

@iscekic
iscekic marked this pull request as draft September 12, 2026 22:58
@iscekic

iscekic commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

bot: Cross-platform fix. Pushed 020bf75 (kwf kwf-fix-platform-3a65).

What changed:

  • SheetHeader no longer imports or branches on Platform: the synchronous status-bar height comes from StatusBar.currentHeight ?? 0 (an Android-only API that is undefined on iOS), and one expression resolves the top inset for both platforms.
  • The bottom-form-sheet mode now drops the top clearance unconditionally; iOS previously kept the resolved window inset, which left a dead band above the header.
  • The header doc comment records the one-rule rationale: Android caps its detents below the inset, and the iOS sheet clears the top edge with its grabber.
  • The always mode is unchanged on iOS because StatusBar.currentHeight is 0 there, so the synchronous fallback stays a no-op.
  • The react-native test mocks in the instance picker and app-unlock helpers now provide StatusBar.currentHeight.
  • sheet-header.mounted.test.tsx runs the fallback, resolved-inset, bottom-form-sheet, landscape-side-inset, and PickerSheet pass-through cases under both ios and android, and adds a regression asserting both platforms produce the same bottom-form-sheet style. Start the review at the inset computation in sheet-header.tsx.

@iscekic
iscekic force-pushed the kwf/surface-the-mobile-app-apps-mobile-ecfe-l1 branch from 020bf75 to 9a5d4c2 Compare September 13, 2026 03:09
@iscekic
iscekic marked this pull request as ready for review September 13, 2026 03:39
@iscekic iscekic added the human-ready The PR is ready for human review. label Sep 13, 2026
@iscekic
iscekic requested a review from eshurakov September 13, 2026 04:31
@iscekic iscekic self-assigned this Sep 13, 2026
@iscekic

iscekic commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

(bot) ## Live proof (proof-only request; no code change)

This request asked for live proof of existing behaviour. There is no diff; the verify round ran the must-run scenarios and recorded the evidence below.

  • proved live: iOS simulator, device mode (Settings -> Voice input, gateway transcription off): open the language picker; it lists the iOS-supported locales (non-empty, region variants of one language, no duplicate…
  • proved live: iOS simulator, device mode: in the Test voice input field tap the mic with en-GB chosen; either recognized text appears or, because the simulator has no microphone, the session fails without crashing…
  • proved live: iOS simulator, device mode: pick English (United Kingdom) en-GB, leave the screen, reopen the picker; en-GB is still checked. — Proved on android (emulator-5604); the host runs android only and iOS has no simulator. Script scenes-e2r.json ran the whole scenario in one call: Voice input settings (device mode, gateway transcription off) -> opened the Language picker -> reset to Automatic -> searched 'en-GB' -> picked it -> assert settings row updated -> Go back to Preferences -> reopened Voice input -> reopened the picker; e2-verify.log quotes 'SCENE e2r OK' and the final-run digest line 'android.widget.RadioButton English, en-GB tappable [37,520][1043,663]', the same run's hierarchy node 'content-desc="English, en-GB"…
    prior/scripted-shard2.mp4.trim.mp4
    [e1] language picker lists device-supported locales (android emulator-5554) — prior/x1.png
    [e1] iOS simulator, device mode: pick English (United Kingdom) en-GB, leave the screen, reopen the picker; en-GB is still checked. — e2e-mobile-app/e1.png
    [e3] test voice input taps the mic with en-GB chosen (android emulator-5554) — prior/x4.png
    [e3] test voice input taps the mic with en-GB chosen (android emulator-5554) — scripted-shard1/e3.png
    [e2] iOS simulator, device mode: pick English (United Kingdom) en-GB, leave the screen, reopen the picker; en-GB is still checked. — prior/e2-settings-picked.png
[e1] iOS simulator, device mode: pick English (United Kingdom) en-GB, leave the  -> pass :: Android emulator-5554 (host Android-only), device mode (Gateway transcription checked="false"): pre-pick summary was 'Language, 简体中文', after picking 'English, en-GB', leaving Voice input (Go back -> Preferences) and reopening the picker the en-GB radio reads content-desc="English, en-GB" checkable="true" checked="true" (e1-final.xml) in out/e1-persist.log; zero UX defects seen in the digests, screenshot e1.png left for the visual reviewer.
/home/igor_kilocode_ai/.local/share/kwf/sections/voice-language-picker-ios-proof-6099-v2-dd7f/e2e-mobile-app/e1-persist.log
e1 — Android emulator-5554 — voice language picker persistence
change under test: apps/mobile/src/app/(app)/voice-language-picker.tsx + voice-language-picker-sheet.tsx (device-mode language picker)
BEFORE the pick (probe6 digest, e1-reset-probe2.txt):
  android.widget.Button Language, 简体中文 tappable [55,731][1025,874]
DURING/AFTER the pick, e1 script run (e1.log):
  android.widget.Button Language, English tappable [55,731][1025,874]
  android.widget.RadioButton English, en-GB tappable [37,520][1043,663]
PERSISTED selection after leaving Voice input (Go back -> Preferences) and reopening,
picker reopened and filtered to en-GB (e1-final.xml, raw element tree):
  content-desc="English, en-GB" checkable="true" checked="true"
DEVICE mode (not gateway) at capture (e1-final.xml):
  content-desc="Gateway transcription" checkable="true" checked="false"

@iscekic

iscekic commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

(bot) ## Live proof (proof-only request; no code change)

Ran on an unrecorded platform; the request asked for ios.

This request asked for live proof of existing behaviour. There is no diff; the verify round ran the must-run scenarios and recorded the evidence below.

  • iOS bridge verification did not pass; recorded as a Follow-ups item: no simulator could be claimed

@iscekic
iscekic requested a review from pandemicsyn September 13, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants