Feat/pro feature registry#386
Open
dishit-wednesday wants to merge 234 commits into
Open
Conversation
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>
…nto feat/tts-implementation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)@offgrid/prosubmodule and the registry seam (screenRegistry,sectionRegistry,loadProFeatures) so pro features register themselves at boot through anactivate(registry)bag, with a no-op stub when the submodule is absent.Tool extension seam + MCP wiring (
26db7c0d)ToolExtension(with optionalgetToolDefinitions()) 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)proLicenseServiceconfigures RevenueCat with Trusted Entitlements (INFORMATIONALverification,FAILEDsignatures blocked), caches the license in the keychain, and revalidates online at launch so access can be revoked from the RC dashboard.ProUnlockModalcollects 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)revenueCatKeys.local.tsand are never committed.In-app debug log viewer (
99718d62)DebugLogsScreen+debugLogsStorefor on-device log inspection.Pro-gated email/calendar tools (
fcbc9c99)send_email,create_calendar_event,read_calendar_eventsnow live in the pro submodule as aToolExtension. The core picker renders core tools plus extension-provided ones, deduped.handlers.ts/registry.tsmatchmainexactly — the handler code exists only in the pro package.tsconfig path mapping (
18ee7f1d)@offgrid/core/*→src/*for tsc, mirroring the existing jestmoduleNameMapper, so tsc resolves the pro extension's core type imports.Verification
tsc --noEmitclean.handleSendEmail,handleCreateCalendarEvent,ensureCalendarPermission,RNCalendarEvents) in any source file across the branch history.handlers.ts/registry.tsare byte-identical tomain.Notes
ed9b93b.