Skip to content

Feat/pro feature registry#386

Open
dishit-wednesday wants to merge 234 commits into
mainfrom
feat/pro-feature-registry
Open

Feat/pro feature registry#386
dishit-wednesday wants to merge 234 commits into
mainfrom
feat/pro-feature-registry

Conversation

@dishit-wednesday

@dishit-wednesday dishit-wednesday commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

What this does

Introduces the Pro feature layer for Off Grid and moves the email/calendar tools out of the open core into the private pro submodule, behind the Pro entitlement.

The branch history was rebuilt into 7 focused commits so that no commit in the open repo ever contains the email/calendar handler implementation or any RevenueCat API keys.

Change set

Pro feature registry + license seam (d300dbb0)

  • Adds the @offgrid/pro submodule and the registry seam (screenRegistry, sectionRegistry, loadProFeatures) so pro features register themselves at boot through an activate(registry) bag, with a no-op stub when the submodule is absent.

Tool extension seam + MCP wiring (26db7c0d)

  • Adds ToolExtension (with optional getToolDefinitions()) and wires extension-provided tools into the generation tool loop, the chat input popovers, and the chat screen.

RevenueCat pro license service + unlock modal (3d5613bf)

  • Web-billing flow: email is both the payment identity and the unlock key. proLicenseService configures RevenueCat with Trusted Entitlements (INFORMATIONAL verification, FAILED signatures blocked), caches the license in the keychain, and revalidates online at launch so access can be revoked from the RC dashboard.
  • ProUnlockModal collects the email, opens the RC web checkout, and verifies the purchase. Replaces the auto-restart library with a manual "close and reopen" prompt.

RevenueCat config placeholders (66ff0c44)

  • Public placeholder config so the app compiles. Real iOS/Android keys stay in gitignored revenueCatKeys.local.ts and are never committed.

In-app debug log viewer (99718d62)

  • DebugLogsScreen + debugLogsStore for on-device log inspection.

Pro-gated email/calendar tools (fcbc9c99)

  • send_email, create_calendar_event, read_calendar_events now live in the pro submodule as a ToolExtension. The core picker renders core tools plus extension-provided ones, deduped. handlers.ts/registry.ts match main exactly — the handler code exists only in the pro package.

tsconfig path mapping (18ee7f1d)

  • Maps @offgrid/core/*src/* for tsc, mirroring the existing jest moduleNameMapper, so tsc resolves the pro extension's core type imports.

Verification

  • tsc --noEmit clean.
  • EmailCalendarExtension unit tests pass (16/16).
  • No email/calendar handler code (handleSendEmail, handleCreateCalendarEvent, ensureCalendarPermission, RNCalendarEvents) in any source file across the branch history.
  • No RevenueCat API keys in history.
  • handlers.ts / registry.ts are byte-identical to main.

Notes

  • Branch coverage is ~79.7% (gate 80%); coverage top-up is a tracked follow-up.
  • The pro submodule changes ship via offgrid-pro#2; the gitlink points at ed9b93b.

alichherawalla and others added 30 commits April 7, 2026 16:41
Implements on-device text-to-speech using OuteTTS 0.3 (454 MB) +
WavTokenizer (73 MB) via llama.rn, with react-native-audio-api for playback.

Two interface modes (user-switchable from Settings):
- Chat Mode: play/stop TTSButton on each assistant message bubble
- Audio Mode: waveform bubbles with auto-TTS after streaming, transcript expand,
  speed cycling, and PCM audio persisted to disk per message for repeat playback

New files:
- src/constants/ttsModels.ts — model URLs, RAM thresholds, cache config
- src/services/ttsService.ts — download, load, generate, persist, play
- src/stores/ttsStore.ts — Zustand store with Chat + Audio Mode actions
- src/hooks/useTTS.ts — convenience hook with RAM gate and weighted progress
- src/components/TTSButton/index.tsx — Chat Mode play/stop per message
- src/components/AudioMessageBubble/index.tsx — waveform bubble component
- src/screens/TTSSettingsScreen/index.tsx — download, mode, speed, cache

Modified:
- Message type: audioPath, waveformData, audioDurationSeconds, isGeneratingAudio
- ChatMessage: Audio Mode branch + TTSButton in meta row
- SettingsScreen: Text to Speech nav row
- Navigation: TTSSettings route
- stores/index.ts, services/index.ts: exports

Tests: 42 unit + integration tests covering service, store, and full flows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert ChatMessage to main (avoids pre-existing complexity lint failure
  when the file enters the push-range diff)
- Add Audio Mode + TTSButton to MessageRenderer instead — clean, under limit
- Move audioPath/waveformData/audioDurationSeconds/isGeneratingAudio fields
  from types/index.ts to types/tts.ts via module augmentation (keeps index.ts
  under the 350-line max)
- Add react-native-audio-api global mock to jest.setup.ts so all test suites
  that transitively import ttsService can resolve the native module

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In finalizeStreamingMessage, after addMessage() saves the assistant reply,
check if Audio Mode is active and model is loaded — if so, fire
useTTSStore.generateAndSave() in the background so the waveform bubble
auto-generates instead of spinning indefinitely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, TTSButton placement

Critical fixes for TTS Audio Mode:

- Add updateMessageAudio() to chatStore — writes audioPath, waveformData,
  audioDurationSeconds, isGeneratingAudio back to the conversation message
  (without this, the waveform bubble spun forever after generation)

- Wire auto-TTS trigger in useChatScreen via useEffect on isStreamingForThisConversation:
  detects streaming → stopped, checks Audio Mode + model loaded, calls
  triggerAudioModeGeneration() which sets isGeneratingAudio:true, fires
  generateAndSave, then writes audio fields or clears the flag on error

- Fix isGenerating logic: show spinner only when isGeneratingAudio===true,
  not for every assistant message missing audioPath (which made all old
  messages spin forever in Audio Mode)

- Fix TTSButton placement: add metaExtra prop to ChatMessage/MessageMetaRow
  so TTSButton renders inline in the timestamp row rather than below the bubble

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Voice row (volume icon + Chat/Audio/N/A badge) to the quick
settings popover in the chat input. Tapping it:
- Toggles between Chat and Audio mode when models are downloaded
- Auto-loads/unloads the TTS model on switch
- Navigates to TTSSettings when models are not yet downloaded

This makes Audio Mode accessible without leaving the chat screen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ChatInput test mock for src/stores was missing useTTSStore, causing
Popovers.tsx (which now uses useTTSStore) to throw on render.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. checkDownloadStatus() never called on TTSSettingsScreen mount
   → store always showed models as not downloaded after fresh app start

2. speak() race condition: stop() during generation didn't prevent playback
   → set isSpeakingFlag=true before generate(), check it after, use finally

3. RNFS.stat() on directory reports block size (~0), not total file size
   → replaced with readDir() recursive sum of individual .pcm file sizes

4. Historical messages without audio showed broken play button in Audio Mode
   → AudioMessageBubble only rendered when msg.audioPath || msg.isGeneratingAudio

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced stat() mock with readDir() mocks matching the new recursive
file-size summation approach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces slider controls with a [–] value [+] stepper row for
precise numeric input in settings screens. Supports min/max/step,
optional decimal formatting, and testID for E2E automation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes @react-native-community/slider from GenerationSettingsModal,
ModelSettingsScreen, and TTSSettingsScreen. Every numeric control
(temperature, top-p, GPU layers, speed, etc.) now uses the stepper
for touch-friendly precise adjustment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MediaAttachment gains audioFormat and audioDurationSeconds fields
- audioRecorderService.stopRecording() now returns { path, durationSeconds }
  instead of just the path, enabling accurate audio bubble scrubbing
- ChatInput/Attachments.addAudioAttachment stores the duration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…send

In Audio Mode, user voice recordings now appear as right-aligned audio
bubbles instead of text messages, making both sides of the conversation
audio-native.

- Voice.ts: adds file-based transcription path (audioRecorderService +
  whisperService.transcribeFile) and onAutoSend callback for atomic send
  with audio attachment. Multimodal models skip transcription entirely.
- ChatInput: passes onAutoSend in Audio Mode; builds MediaAttachment
  inline to avoid async state-update race; uses attachmentsRef for sync reads.
- AudioMessageBubble: adds isUser prop for right-aligned primary-tinted style.
- MessageRenderer: renders user audio attachments as AudioMessageBubble
  before the normal message path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The streaming-complete useEffect only listed isStreamingForThisConversation
in its deps, so activeConversation was captured stale. When streaming ended,
the last message was always the old value — TTS generation was never triggered.

Fix: read conversation and last message directly from useChatStore.getState()
inside the effect instead of relying on the closed-over activeConversation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When no Whisper model is installed and the user taps the mic, show a
CustomAlert offering to download Whisper Small (466 MB) immediately,
rather than navigating away to VoiceSettings.

UnavailableButton also now shows a download icon + percentage while
the model is being fetched, so feedback is in-place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a TEXT TO SPEECH section alongside IMAGE GENERATION and TEXT
GENERATION in the chat settings modal. Shows mode toggle (chat/audio),
enable switch, speed stepper, and auto-play toggle. Deep-links to
TTSSettingsScreen for full configuration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WHISPER_MODELS grows from 5 to 10 entries covering English-only and
Multilingual variants for tiny/base/small/medium, plus Large v3 Turbo
and Large v3.

whisperService.downloadFromUrl(url, modelId) downloads any ggml .bin
file from an arbitrary URL — enables installing community models from
HuggingFace. whisperStore exposes it as downloadFromUrl action.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewrites the voice settings screen with three sections:
- Active model card with inline download progress and remove action
- Curated models grouped by English-only / Multilingual (all sizes,
  tiny → large-v3)
- Live HuggingFace search bar (500 ms debounce) that queries ASR repos;
  tap a repo to expand and browse its ggml .bin files; tap a file to
  confirm and download via downloadFromUrl

huggingFaceService gains searchWhisperRepos() and getWhisperFiles()
to power the HF search without coupling to the LLM model browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
llmMessages builds an input_audio content block from audio attachments
when the active model reports audio support, bypassing Whisper entirely.
llm.ts exposes getMultimodalSupport() so the voice layer can detect this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ttsStore: adds interfaceMode, speed, autoPlay, enabled settings;
  generateAndSave flow for Audio Mode; updateMessageAudio
- ttsService: OuteTTS generate+save path for AI audio bubbles
- TTSButton: play/stop per-message with generation spinner
- KokoroTTSManager + kokoroModels: scaffold for Tier 1 Kokoro TTS
  (not yet wired to react-native-executorch, marked not started)
- App.tsx: mounts KokoroTTSManager near root
- packages: react-native-executorch, background-downloader, dr.pogodin/react-native-fs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChatMessage: long-press action sheet gains Speak option (delegates to ttsStore)
- ModelSettingsScreen: suppress pre-existing exhaustive-deps lint warning
- Tests: update GenerationSettingsModal and ModelSettingsScreen tests for
  NumericStepper (gpu-layers-stepper-increment) replacing slider testIDs
- TTS_IMPLEMENTATION_PLAN: rewritten to reflect Audio Mode bidirectional
  voice conversation, stale closure fix, and implementation status

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sages

Two bugs causing broken Audio Mode:

1. AudioRecorder was recording at the system default rate (~44.1 kHz),
   producing WAV that Whisper interprets as static ('TV static' / [SOUND]).
   Fix: pass a preset with sampleRate:16000, BitDepth.Bit16 so the file
   is Whisper-compatible 16 kHz mono int16 PCM from the start.

2. buildOAIMessages was always including audio attachments as input_audio
   content blocks, even for models that don't support audio input (e.g.
   remote Qwen 3.5 2B / Gemma 42B). Those models replied 'I cannot hear
   audio'. Fix: buildOAIMessages now accepts supportsAudio flag (default
   false) and only emits input_audio parts when the model declares audio
   support. llm.ts passes multimodalSupport.audio when calling it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
playFromFile was treating WAV bytes as raw Float32 PCM — designed for
OuteTTS output only. WAV files have a 44-byte RIFF header plus int16
samples; reinterpreting them as Float32 produces pure static.

Fix: use AudioContext.decodeAudioData(filePath) which properly parses
the WAV header and decodes samples. The file:// prefix is added if
missing.

MessageRenderer now wraps user and assistant audio bubbles in a
container View with paddingHorizontal:16 and marginVertical:8,
matching the ChatMessage container layout so bubbles align correctly
with the chat edges instead of touching screen borders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audio type attachments were falling through to the FadeInImage branch,
causing Image to try to load the WAV file path — resulting in a broken
image placeholder that stretched the user bubble very wide (the 'super
long' bubble issue).

Audio attachments now render as a compact mic icon + 'Voice message'
badge (matching the document badge style), keeping the bubble compact.
In Audio Mode they never reach this code — they render as AudioMessageBubble.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add isAudioModeMessage to Message type and updateMessageAudio signature.
Set flag in triggerAudioModeGeneration so mode switches don't reformat
old text messages. MessageRenderer now checks msg.isAudioModeMessage
instead of global ttsMode for assistant audio bubbles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 2: handlePlayPause calls speak() for AI bubbles (empty audioPath)
instead of playMessage with empty string. Remove isGenerating spinner.
Bug 3: WaveformBars gets flex:1 + overflow:hidden, WAVEFORM_BARS 40→28,
bubble overflow:hidden, maxWidth 80%→88%.
Bug 4: user bubble flips play row order (speed+duration left, play right).
Bug 5: voice cycling chip on AI bubbles reads/writes kokoroVoiceId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix guard: was checking isModelLoaded (OuteTTS, always false) instead
  of kokoroReady — so isAudioModeMessage was never stamped and all AI
  messages rendered as text in audio mode
- Add sentence-level streaming TTS: Kokoro now starts speaking each
  sentence as soon as LLM finishes generating it, instead of waiting
  for the full response
- Fix waveform invisible in idle state: min bar height 3→6px and
  empty waveform now renders a sine-wave placeholder instead of
  nearly-invisible flat bars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds memory-rag capability and conversationRagService spec so Jarvis
can retrieve relevant context from past conversations and inject it
into the system prompt — giving it cross-chat intelligence without
requiring the user to repeat themselves.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Stamp isAudioModeMessage BEFORE checking TTS engine readiness — so
  AI messages always render as audio bubbles even when Kokoro hasn't
  downloaded yet
- Add minWidth: 220 to audio bubble so flex:1 waveform container has
  space to expand (previously collapsed to 0 since bubble shrinks to
  content in flex-end alignment)
- Audio mode input: hide text pill, show centered VoiceRecordButton
  with 'Hold to speak' / 'Release to send' hint — clearly communicates
  the interface mode
- User voice recordings now render as AudioMessageBubble in BOTH chat
  and audio mode — tap play to hear your recording back regardless of
  which interface is active

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MessageRenderer now renders ALL assistant messages as audio bubbles
  when interfaceMode=audio (not just isAudioModeMessage-stamped ones),
  fixing old messages showing as text after enabling audio mode
- Removed voiceChip from play row; added dedicated voice row below
  controls with mic icon + voice name + chevron-right to cycle voices
- AudioMessageBubble: streaming-only messages (no audioPath) correctly
  fall through to speak(transcript) for on-demand playback
- ChatInput audio mode: added +/settings buttons back on left side so
  users can attach photos and configure tools while in audio mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dishit-wednesday and others added 30 commits June 24, 2026 19:26
A centered fade-Modal alert (vs CustomAlert's bottom sheet). Less prone to
the iOS present-while-dismiss conflict, so it suits confirms triggered from
inside another modal/menu. Shares the AlertButton/showAlert API.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Reuse VoiceModelsUpsell (the Models Voice tab's surface) instead of a
dead-end 'not available in this build' line, with a Get Pro CTA to ProDetail.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
When the UI shows Tools as N/A (picker locked), generation must not inject
tools the user can't disable. Thread supportsToolCalling through and respect it.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Move the chat mode-toggle wrapper style into the stylesheet, and extract
VoiceRecordButton's chat-mode style builder to module scope to drop the
component back under the complexity limit.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The manager-sheet pickers and eject alert now run via onClosed
(closeManagerThen), so the AppSheet mock must fire onClosed when it goes
invisible. Also assert the full-screen LoadingOverlay shows during load,
which is the shipped behaviour (the prior 'no overlay' assertion was stale).

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The audio-mode quick-settings popover's MCP entry did nothing because ChatInput
never forwarded onMcpPress/mcpToolCount to the audio slot (the chat-mode path
already passes them). Pass both through so the MCP servers sheet opens from
audio mode too.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Choosing Voice with no model now shows a prompt (then routes to the nested
Main > ModelsTab > Voice on 'Get voice model'), instead of navigating
directly. Drive that flow on the Android path (iOS defers the alert to the
dropdown's onDismiss, which jest can't fire).

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Pro uses web billing only (no in-app Play purchases), so the
com.android.vending.BILLING permission pulled in transitively via
com.android.billingclient is unnecessary. Strip it from the merged
manifest with tools:node="remove".

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
registerScreen now ignores a repeated same-name registration (first wins),
mirroring sectionRegistry. Prevents duplicate routes if the registration path
runs more than once (dev Fast Refresh, or a future re-activate-on-purchase),
which crashed the navigator with a duplicate screen.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Remove branch-added logger.log/info/debug (JS) and Log.i (Kotlin) noise
from the litert / tool-loop / model lifecycle / pro-license / whisper /
classifier paths. Keeps all warn/error logging, logs that already
existed on main, and control-flow logs (model-residency unload catches,
LiteRT first-message marker). Drops now-unused logger imports and locals
that only fed those logs. No behaviour change.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…polish

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…udio-tools

fix(ios): RNFS build consolidation + LiteRT audio, two-pass tools, sheet fix
feat: Audio (Voice) mode + model routing & selection redesign
…lies

package.json used ^7.1.0, which let the lockfile drift to 7.1.1. The
NPE-crash patch (patches/react-native-zip-archive+7.1.0.patch) only
applies to 7.1.0, so patch-package failed during npm ci, breaking the
install step and thus every CI job (lint/test/typecheck/android-build).
Pin to exact 7.1.0 to keep the patch applicable. 7.1.1 offered nothing
needed (its only relevant change, the (int) compressionLevel cast, is
already in the patch).

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The audio/TTS test suites import directly from the private pro/ submodule
(pro/audio/*) via relative paths. The public repo's CI checkout does not
(and must not) include that private submodule, so those modules can't
resolve there, failing both typecheck and the jest run.

Exclude the 10 pro-dependent suites from jest (testPathIgnorePatterns) and
tsconfig (exclude). They continue to run in the pro repo's own CI, where
both core and pro are present. No source or behavior change.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…lies

Same drift fix as on feat/pro-feature-registry: ^7.1.0 let the lock
move to 7.1.1, which the +7.1.0 patch can't apply to, failing npm ci.
Pin exact 7.1.0. Needed here so the pro repo's CI (which pairs against
this branch) can install cleanly.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…udio-tools

fix(deps): pin react-native-zip-archive to 7.1.0 so patch-package app…
On iOS the recorder needs an active AVAudioSession in a record-capable
category. Nothing configured it, so the native start() returned "Audio session
is not active", no WAV was written, and stop() failed with "Recording failed to
save" — so voice/audio mode produced no input. Configure playAndRecord (so TTS
playback can share the session) + defaultToSpeaker and activate it before
recording (iOS only; Android is unchanged). Also surface a failed native
start() instead of silently continuing to a doomed stop().

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…ate)

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The @kesha-antonov/react-native-background-downloader dep (com.eko) was
added in 908af16 for the original in-core TTS work, which has since
moved to the pro submodule and uses a different downloader. Nothing in
core or pro imports it, and no patch depends on it — it only autolinks a
ResumableDownloadService (notification foreground service) that trips the
NotificationPermission Android lint error (POST_NOTIFICATIONS was removed
in 13e6369). Removing it kills that lint error at the source.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
… notification)

react-native-audio-api declares a PlaybackNotification capability in its
manifest, which lint flags as needing POST_NOTIFICATIONS. The app only
uses AudioContext for playback (and AudioManager for the recording audio
session) — it never uses the now-playing/media-session API, so it never
posts that notification. POST_NOTIFICATIONS was intentionally dropped in
13e6369. Baseline the finding (a known, unused library capability)
rather than re-add a permission for a notification we never show.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Ungated-in-release dev button (flagged TEMPORARY) that called
resetProIdentityForTesting to clear the RevenueCat identity. Removed the
button, its now-unused Alert / resetProIdentityForTesting imports, and
the test that exercised it. Kept the restoreButton/restoreText styles —
shared with the real 'Already a member? Verify with email' button.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Add targeted unit/RNTL tests covering previously-uncovered branches
(error paths, ??/|| fallbacks, try/catch, guard clauses, early returns)
across services, stores, hooks, and components. No source or config
changes; coverage thresholds and collectCoverageFrom are untouched.

Global branch coverage 78.2% -> 82.69% (statements/functions/lines all
remain >= 80%). Files covered: litert, generationToolLoop,
generationServiceHelpers, modelManager/scan, huggingface,
downloadHydration, activeModelService/loaders, whisperService,
tools/handlers, llmHelpers, hardware, audioRecorderService,
NumericStepper, downloadStore, useDownloadManager, useImageModels.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Whisper (STT) state lived in two places: whisperStore (Home banner + Models
screen) and a direct disk read in the Download Manager. So deleting a Whisper
model in the Download Manager went straight through whisperService and never
updated whisperStore — leaving the Home banner and Models screen still showing
it as present/active. Downloads finishing elsewhere also didn't appear in the
Download Manager within the same session.

- Download Manager STT delete now routes through whisperStore.deleteModelById
  (unloads if active, deletes the file, updates presentModelIds/downloadedModelId),
  so all three surfaces update live.
- useVoiceDownloadItems subscribes to presentModelIds and refreshes when it
  changes, so a Whisper model downloaded from Home/Models appears in the Download
  Manager in the same session (on completion).

The deleted file is unchanged (same single ggml-<id>.bin by id); the only added
behaviour is the in-memory unload + store update. Live in-progress Whisper rows
in the Download Manager remain out of scope (the simpler approach).

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Reverts the earlier removal. It is NOT unused: the voice-model disk scan
reads its directories.documents at boot (KokoroEngine refreshDiskStatus
-> listDownloadedModels -> listDownloadedFiles), so removing the dep left
the bundled JS calling an unlinked native module and crashed the app on
launch with 'react-native-background-downloader doesn't seem to be linked'.
Re-baseline its NotificationPermission lint (the dep's ResumableDownloadService).

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
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.

2 participants