Skip to content

fix(mobile): correct the privacy sheet's voice transcription and gateway claims - #6196

Merged
iscekic merged 1 commit into
mainfrom
kwf/privacy-sheet-details-283c
Sep 18, 2026
Merged

iscekic merged 1 commit into
mainfrom
kwf/privacy-sheet-details-283c

Conversation

@iscekic

@iscekic iscekic commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Changelog for users

  • The Voice transcription row now names the Kilo gateway beside your device and Apple or Google.
  • With gateway transcription on, that row reads "Via the Kilo gateway" instead of "On device".
  • While a setting is still loading, the row shows one loading placeholder and no destination claim.
  • When the account read fails, the row shows "Could not load your transcription setting." with a Retry that refetches.
  • The Kilo Gateway section now says it carries the prompts and voice recordings the app sends through it.

Changelog for maintainers

  • Voice transcription "Who" copy: removed the "On-device only" claim; it now names your device, the Kilo gateway with your chosen transcription model, or Apple or Google for online recognition (voiceTranscriptionWho).
  • Kilo Gateway "What" copy: now names the prompts and voice recordings the app sends through the gateway (gatewayWhat); it previously named only account ID, request metadata and token usage.
  • New viaGateway key ("Via the Kilo gateway") fills the voice footer status row when gateway transcription is on; the engine is chosen in Preferences, so that row carries no switch.
  • Gateway transcription owns every dictation while the preference is on, so the row no longer prints "On device" on a device that also supports on-device recognition.
  • useCurrentUserId exposes an additive isFetchError: a failed fetch with a cached identity still reports the failure, while isError keeps the signed-in state.
  • Voice footer order: loading, retryable account error, gateway, on-device, sign-in/consent. Start review at the branch order in consent-details.tsx; the account read gates every claim.
  • All shipped catalogs carry the new key and the two reworded strings; English copy stays in en.json.
  • Mounted tests cover the gateway row, the loading placeholder, and the retry row for a failed initial read and a failed refetch over a warm cache.

E2E proof

[p1] Privacy sheet states the corrected claims (happy: signed in, gateway transcription off) — e2e-mobile-app/p1-gateway.png

[p1] Privacy sheet states the corrected claims (happy: signed in, gateway transcription off) — e2e-mobile-app/p1.png

[p5] ux-check: gateway transcription on, privacy sheet voice footer — e2e-mobile-app/p5-sheet-final.png

[e5] ux-check: Gateway transcription off, on-device-capable device: the Voice transcription footer shows 'On device' and no online-transcription switch. — prior/e5.png

ux-check: Cold start then open the privacy sheet: the Voice transcription footer shows at most one loading indicator, then settles into the correct row without blanking or jumping. — e3.png

[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable — android emulator-5554, STATE HIT signed-in-home; with nextjs killed (e1-fault-down.log 'fault.sh: nextjs killed (port 7300 refuses; pids 208836 )') a freshly mounted privacy sheet's voice row showed 'android.widget.TextView Could not load your transcription setting. tappable [55,1782][1025,1828]' beside 'android.widget.Button Retry tappable [55,1846][1025,1962]' with zero 'On device' lines (e1-fault-mount-digest.log, e1-fault-row.xml); 'ASSERT HIT Could not load your transcription setting. matches=1'/'ASSERT HIT Retry matches=2' (e1-fault-mount.log); after nextjs up and Retry, 'ASSERT HIT On…

[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable — e1-fault-row.png

[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable

[e1] Retryable unhappy (needs:fault) — after Retry with the API reachable the row recovers to 'On device' — e1-recovered.png

/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-2275bf7-336e/e2e-backend/backend.log
    '    "code": "invalid_format",\n' +
    '    "format": "datetime",\n' +
    '    "pattern": "/^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:
    '    "path": [\n' +
    '      "needsInputSince"\n' +
    '    ],\n' +
    '    "message": "Invalid ISO datetime"\n' +
    '  }\n' +
    ']'
}
 ✓ src/lib/glanceable-delivery.test.ts (81 tests) 1638ms
 Test Files  16 passed (16)
      Tests  312 passed (312)
   Start at  17:50:38
   Duration  18.51s (transform 44.88s, setup 989ms, import 133.57s, tests 4.46s, environment 4ms)
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
No projects matched the filters in "/home/igor_kilocode_ai/.local/share/kwf/wt/kwf-fix-proof-2275bf7-336e"
[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load you -> pass :: android emulator-5554, STATE HIT signed-in-home; with nextjs killed (e1-fault-down.log '<internal>(port 7300 refuses; pids 208836 )') a freshly mounted privacy sheet's voice row showed 'android.widget.TextView Could not load your transcription setting. tappable [55,1782][1025,1828]' beside 'android.widget.Button Retry tappable [55,1846][1025,1962]' with zero 'On device' lines (e1-fault-mount-digest.log, e1-fault-row.xml); 'ASSERT HIT Could not load your transcription setting. matches=1'/'ASSERT HIT Retry matches=2' (e1-fault-mount.log); after nextjs up and Retry, 'ASSERT HIT On device matches=1' (e1-recovery.log) and e1-recovered-row.xml shows text="On device" bounds="[878,1916
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-2275bf7-336e/e2e-mobile-app/e1-fault-down.log
<internal>(port 7300 refuses; pids 208836 )
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-2275bf7-336e/e2e-mobile-app/e1-fault-mount-digest.log
android.widget.TextView Performance profiling tappable [55,688][1025,744]
android.widget.TextView What: CPU profiles from a small sample of sessions, including function names and call stacks. tappable [55,753][1025,845]
android.widget.TextView Why: Find and fix the code that makes the app slow. tappable [55,854][1025,900]
android.widget.TextView Who: Sentry. tappable [55,909][1025,955]
android.widget.TextView Profiles capture function names and call stacks, never your prompts or conversation content. tappable [82,1011][998,1085]
android.widget.TextView Install attribution tappable [55,1188][1025,1244]
android.widget.TextView What: Install source and campaign identifiers. tappable [55,1253][1025,1299]
android.widget.TextView Why: Understand which channels bring new users. tappable [55,1308][1025,1354]
android.widget.TextView Who: AppsFlyer. tappable [55,1363][1025,1409]
android.widget.TextView Voice transcription tappable [55,1486][1025,1542]
android.widget.TextView What: Your recording while you dictate. tappable [55,1551][1025,1597]
android.widget.TextView Why: To turn speech into text. tappable [55,1606][1025,1652]
android.widget.TextView Who: Your device, the Kilo gateway with your chosen transcription model, or Apple or Google for online recognition. tappable [55,1661][1025,1753]
android.widget.TextView Could not load your transcription setting. tappable [55,1782][1025,1828]
android.widget.Button Retry tappable [55,1846][1025,1962]
android.widget.TextView Retry tappable [500,1880][579,1926]
android.widget.TextView Full retention periods, your rights, and contact information are in the Kilo privacy policy. tappable [55,2052][1025,2126]
android.widget.Button Kilo privacy policy tappable [911,2052][1025,2089]
android.widget.Button Back to consent tappable [55,2200][1025,2316]
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-2275bf7-336e/e2e-mobile-app/e1-fault-mount.log
### nextjs down?
nextjs port 7300 down
### tap Go back
TAP OK 'Go back' matches=1
rc=0
### tap See full details
TAP OK 'See full details' matches=2
rc=0
### swipe to voice row
rc=0
### assert error line
ASSERT HIT 'Could not load your transcription setting.' matches=1
rc=0
### assert Retry
ASSERT HIT 'Retry' matches=2
rc=0
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-2275bf7-336e/e2e-mobile-app/e1-recovery.log
### tap Retry (recovery)
TAP OK 'Retry' matches=2
rc=0
### assert recovered row
ASSERT HIT 'On device' matches=1
rc=0
### hierarchy
hierarchy: /home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-2275bf7-336e/e2e-mobile-app/e1-recovered-row.xml (92 elements)
rc=0
Owner request

kwf-fix: proof-2275bf7

kwf-fix-pr: #6196

Prove the behaviour of PR #6196 with a live end-to-end run, and make no code change.
The pull request description carries no evidence: the '## E2E proof' section names a scenario it could not capture: - [e2] Retryable unhappy (needs:fault) - sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable: reported not_applicable, so nothing proves it (On this emulator on-device recognition is supported, so with nextjs stopped a freshly mounted sheet still rendered the on-device branch ('android.widget.TextVie) — reported not_applicable: the plan names a branch the product does not contain.

Run the pull request's own scenarios on a device or a simulator, for every platform its diff touches. Capture the decisive log lines always, and a screenshot as well for a user-visible change. Never a recording: they are gone (owner, 2026-09-16).
Write the evidence under a '## E2E proof' heading in your own pull request description. Never run gh: the driver publishes.
If a scenario fails, name it and say why, and still change no product code: this section proves what the branch already carries.

The pull request description as it stands now

This is the live body; you never need to fetch it, and you must not edit it. The driver merges what your final summary says into it.

## Changelog for users

- The Voice transcription row now names the Kilo gateway beside your device and Apple or Google.
- With gateway transcription on, that row reads "Via the Kilo gateway" instead of "On device".
- While a setting is still loading, the row shows one loading placeholder and no destination claim.
- When the account read fails, the row shows "Could not load your transcription setting." with a Retry that refetches.
- The Kilo Gateway section now says it carries the prompts and voice recordings the app sends through it.

## Changelog for maintainers

- Voice transcription "Who" copy: removed the "On-device only" claim; it now names your device, the Kilo gateway with your chosen transcription model, or Apple or Google for online recognition (`voiceTranscriptionWho`).
- Kilo Gateway "What" copy: now names the prompts and voice recordings the app sends through the gateway (`gatewayWhat`); it previously named only account ID, request metadata and token usage.
- New `viaGateway` key ("Via the Kilo gateway") fills the voice footer status row when gateway transcription is on; the engine is chosen in Preferences, so that row carries no switch.
- Gateway transcription owns every dictation while the preference is on, so the row no longer prints "On device" on a device that also supports on-device recognition.
- `useCurrentUserId` exposes an additive `isFetchError`: a failed fetch with a cached identity still reports the failure, while `isError` keeps the signed-in state.
- Voice footer order: loading, retryable account error, gateway, on-device, sign-in/consent. Start review at the branch order in `consent-details.tsx`; the account read gates every claim.
- All shipped catalogs carry the new key and the two reworded strings; English copy stays in `en.json`.
- Mounted tests cover the gateway row, the loading placeholder, and the retry row for a failed initial read and a failed refetch over a warm cache.

## E2E proof

![[p1] Privacy sheet states the corrected claims (happy: signed in, gateway transcription off) — e2e-mobile-app/p1-gateway.png](https://github.com/user-attachments/assets/4659d762-6711-4426-adcb-5e55bb865389)

![[p1] Privacy sheet states the corrected claims (happy: signed in, gateway transcription off) — e2e-mobile-app/p1.png](https://github.com/user-attachments/assets/fc39cc71-cccb-45f6-8021-336cec75af66)

![[p5] ux-check: gateway transcription on, privacy sheet voice footer — e2e-mobile-app/p5-sheet-final.png](https://github.com/user-attachments/assets/9ca119bf-8a7e-490b-8e30-711f81bd4c5b)

![[e5] ux-check: Gateway transcription off, on-device-capable device: the Voice transcription footer shows 'On device' and no online-transcription switch. — prior/e5.png](https://github.com/user-attachments/assets/b704b293-618b-4c26-8ef2-3d29d2899fd1)

![ux-check: Cold start then open the privacy sheet: the Voice transcription footer shows at most one loading indicator, then settles into the correct row without blanking or jumping. — e3.png](https://github.com/user-attachments/assets/3dbb99db-ef2e-4a11-952a-6aefdc3179fb)

**[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable** — android emulator-5554: STATE HIT signed-in-home (e1-state.log), 'fault.sh: nextjs killed (port 4400 refuses; pids 343783' (e1-fault-down2.log), then 'SCENE e1 OK' with the sheet digest 'android.widget.TextView Voice transcription tappable [55,1486][1025,1542]' + 'android.widget.TextView Could not load your transcription setting. tappable [55,1782][1025,1828]' + 'android.widget.Button Retry tappable [55,1846][1025,1962]' (e1-fault-round2b.log), screenshot e1.png left to the visual reviewer, no UX-DEFECT in the digest; the two post-recovery Retry taps I made ('SCENE e1retry2 OK', e1-retry2.log)…

![[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable — e2e-mobile-app/e1.png](https://github.com/user-attachments/assets/72211118-5bd6-4d38-b751-d450696160f0)

**[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable** — android emulator-5554; STATE HIT signed-in-home, then nextjs killed (e1-fault-down.log 'fault.sh: nextjs killed (port 4400 refuses'); e1-fault.log digest after the fault shows 'android.widget.TextView Could not load your transcription setting. tappable [55,2347][1025,2393]' and zero 'On device' lines, so the error gates the destination claim on this on-device-capable emulator; the run's MISS is only my extra tap on a below-the-fold control ('SCENE e1 MISS tap 'Retry''), the expected-line assert passed, and the Retry control (e1-retry-control2.xml 'Button | Retry | [55,1846][1025,1962]'), its…

![[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable — prior/e1-error.png](https://github.com/user-attachments/assets/f8c17533-3f5c-4759-b22e-4cec3b2e5b5e)

[e1] Retryable unhappy (needs:fault) — sheet voice row shows 'Could not load you -> pass :: android emulator-5554 with nextjs down, (e1-error.log) SCENE e1 OK and 'android.widget.TextView Could not load your transcription setting. tappable [55,1782][1025,1828]' beside 'android.widget.Button Retry tappable [55,1846][1025,1962]'; after nextjs up and Retry, (e1-recovered.log) actual state is 'android.widget.TextView On device tappable [878,1916][1024,1962]'.


> ```

</details>
## Follow-ups (not changed here)
- not proved live: - [e2] Retryable unhappy (needs:fault) - sheet voice row shows 'Could not load your transcription setting.' with Retry when the API is unreachable: reported not_applicable, so nothing proves it (On this emulator on-device recognition is supported, so with nextjs stopped a freshly mounted sheet still rendered the on-device branch ('android.widget.TextVie) — reporte
## Follow-ups (not changed here)

Open findings (not fixed here)

  • the '## E2E proof' section links a file path, which 404s on GitHub
p6-coldstart.mp4

p8v

e1

e1-error

e1-recovered

e1-fault-row

@iscekic
iscekic marked this pull request as draft September 16, 2026 03:01
@kilo-code-bot

kilo-code-bot Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Full re-review of the rebased privacy-sheet copy change at 2275bf7 (the previous review SHA is not an ancestor, so the whole diff was re-analyzed; the PR-authored patch is unchanged) found no bugs, security problems, or regressions on the changed lines.

Files Reviewed (91 files)
  • apps/mobile/src/components/consent/consent-details.tsx - branch order (loading, isFetchError, gateway, on-device, sign-in/consent) re-verified against useGatewayTranscriptionPreference (hasLoaded resolves in the store's finally, so the row cannot stick on the placeholder); every referenced key (common.loading, common.retry, consent.couldNotLoadTranscription, consent.viaGateway, consent.voiceTranscriptionTitle) exists in all catalogs; the added hook call registers only useSyncExternalStore subscriptions React removes on unmount, so no leak
  • apps/mobile/src/lib/hooks/use-current-user-id.ts - isFetchError (isError || (isLoading && isFetched)) differs from isError only by dropping the data === undefined guard, so the error branch fires under exactly the old conditions when no identity is cached and additionally on a failed refetch over a warm cache; isError is unchanged for other consumers
  • apps/mobile/src/components/consent/consent-details.mounted.test.tsx - gateway, loading-placeholder, and retry cases match the implemented branch order
  • apps/mobile/src/lib/hooks/use-current-user-id.test.ts - beforeEach resets data/isLoading/isError/isFetched, so the new isFetchError cases cannot leak state
  • apps/mobile/src/i18n/locales/*.json (87 catalogs) - every file changed identically (3 insertions, 2 deletions), viaGateway present exactly once and non-empty in all 87, gatewayWhat/voiceTranscriptionWho reworded in all, no English-identical viaGateway, and git diff --check reports no whitespace errors
Previous Review Summaries (3 snapshots, latest commit 0e70956)

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

Previous review (commit 0e70956)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Full re-review of the rebased privacy-sheet change (the previous review SHA is no longer an ancestor, so the whole PR diff was re-analyzed; the PR-authored patch itself is unchanged) found no bugs, security problems, or regressions on the changed lines.

Files Reviewed (91 files)
  • apps/mobile/src/components/consent/consent-details.tsx - branch reorder (loading, isFetchError, gateway, on-device, sign-in/consent) verified against useGatewayTranscriptionPreference and voiceInputController; every referenced key (common.loading, common.retry, consent.couldNotLoadTranscription, consent.viaGateway, consent.voiceTranscriptionTitle) exists in all catalogs
  • apps/mobile/src/lib/hooks/use-current-user-id.ts - additive isFetchError (isError || (isLoading && isFetched)) verified true for a failed refetch over a cached identity and false on initial load/success
  • apps/mobile/src/components/consent/consent-details.mounted.test.tsx - gateway, loading-placeholder, retry, and cached-refetch-failure cases match the implemented branch order
  • apps/mobile/src/lib/hooks/use-current-user-id.test.ts - new isFetchError cases verified; beforeEach resets shared query state, so no cross-test leakage
  • apps/mobile/src/i18n/locales/*.json (87 catalogs) - viaGateway present and non-empty in all 87, all files valid JSON, gatewayWhat/voiceTranscriptionWho reworded consistently, and no English-fallback values

Previous review (commit 755fc9f)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (91 files)
  • apps/mobile/src/components/consent/consent-details.tsx
  • apps/mobile/src/components/consent/consent-details.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-current-user-id.ts
  • apps/mobile/src/lib/hooks/use-current-user-id.test.ts
  • apps/mobile/src/i18n/locales/*.json - 87 catalogs (viaGateway, gatewayWhat, voiceTranscriptionWho)

Incremental re-review at 755fc9fd0: the branch was rebased onto newer main, so the PR-authored patch is byte-identical to the previously reviewed b8f2f8bc. Every incremental locale line is main's checksCount_* plural keys, not this PR. Both untrusted rebase sides were checked: no conflicting or duplicated keys, and the array of voice/gateway copy keys is intact in all 87 catalogs.

Previous review (commit b8f2f8b)

Status: No Issues Found | Recommendation: Merge

Reviewed the privacy-sheet copy correction and the new voice-status precedence end to end. The new isFetchError derivation is sound: isError || (isLoading && isFetched) is only true on a real failed attempt, so it cannot false-positive on the initial load (isFetched stays false until a fetch settles) and cannot false-negative on a failed refetch over a warm cache (raw isError is set while data is retained), and the exported isError is unchanged for all other consumers. The reordered branches in consent-details.tsx match the real engine selection (use-voice-input-actions.ts checks gateway mode before the OS recogniser), the loading gate covers both reads without stacking indicators, and Retry has a path back through refetch(). All 91 locale files parse, every catalog carries exactly one consent.viaGateway, and the three touched keys have no duplicate-copy, placeholder, whitespace, or direction-mark violations. The added useGatewayTranscriptionPreference() call runs before every early return and registers only useSyncExternalStore subscriptions that React removes on unmount, so no subscription, timer, or async-continuation leak is introduced. I could not execute pnpm check:i18n, pnpm typecheck, or the mounted tests, so verification is static.

Files Reviewed (91 files)
  • apps/mobile/src/components/consent/consent-details.tsx
  • apps/mobile/src/components/consent/consent-details.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-current-user-id.ts
  • apps/mobile/src/lib/hooks/use-current-user-id.test.ts
  • apps/mobile/src/i18n/locales/en.json
  • apps/mobile/src/i18n/locales/*.json - 86 non-English catalogs (mechanical viaGateway addition plus gatewayWhat / voiceTranscriptionWho rewrites)

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 ready for review September 16, 2026 03:20
@iscekic
iscekic force-pushed the kwf/privacy-sheet-details-283c branch from b8f2f8b to 755fc9f Compare September 16, 2026 18:45
@iscekic
iscekic force-pushed the kwf/privacy-sheet-details-283c branch from 755fc9f to 0e70956 Compare September 17, 2026 14:36
@iscekic
iscekic marked this pull request as draft September 17, 2026 14:46
@iscekic
iscekic force-pushed the kwf/privacy-sheet-details-283c branch from b482a07 to 2275bf7 Compare September 17, 2026 16:16
@iscekic
iscekic marked this pull request as ready for review September 17, 2026 16:46
@iscekic iscekic added the human-ready The PR is ready for human review. label Sep 17, 2026
@iscekic iscekic self-assigned this Sep 17, 2026
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