Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
234 commits
Select commit Hold shift + click to select a range
767e0b9
feat: add TTS with Audio Mode and Chat Mode
alichherawalla Apr 7, 2026
1f9698c
fix: move TTS rendering to MessageRenderer, fix global audio-api mock
alichherawalla Apr 7, 2026
6d269de
feat: trigger TTS generation automatically after streaming in Audio Mode
alichherawalla Apr 7, 2026
567b9ee
fix: wire Audio Mode end-to-end — message audio fields, spinner logic…
alichherawalla Apr 7, 2026
78e40d5
feat: add Voice mode toggle to quick settings popover
alichherawalla Apr 7, 2026
ee07ec2
test: add useTTSStore mock to ChatInput test suite
alichherawalla Apr 7, 2026
e480690
fix: pre-testing bug sweep — 4 real issues
alichherawalla Apr 7, 2026
c602566
test: update getAudioCacheSizeMB test for readDir-based implementation
alichherawalla Apr 7, 2026
edd8043
Merge branch 'main' of github.com:alichherawalla/off-grid-mobile-ai i…
alichherawalla Apr 7, 2026
8ab6a50
feat: add NumericStepper component
alichherawalla Apr 7, 2026
ce920a4
feat: replace sliders with NumericStepper in all settings screens
alichherawalla Apr 7, 2026
ef9b973
feat: audio attachment type — duration, format, and recorder service
alichherawalla Apr 7, 2026
bb4ff1f
feat: Audio Mode full voice conversation — user audio bubbles + auto-…
alichherawalla Apr 7, 2026
51a2ba4
fix: stale closure bug — Audio Mode TTS trigger reads fresh store state
alichherawalla Apr 7, 2026
3d1eb33
feat: VoiceRecordButton — inline download prompt instead of navigation
alichherawalla Apr 7, 2026
6b337f3
feat: add TTS accordion to GenerationSettingsModal
alichherawalla Apr 7, 2026
9f5ad0d
feat: expand Whisper model catalogue and add downloadFromUrl
alichherawalla Apr 7, 2026
c072873
feat: VoiceSettingsScreen — HuggingFace model search
alichherawalla Apr 7, 2026
ff834e1
feat: multimodal audio input — pass WAV directly to audio-capable models
alichherawalla Apr 7, 2026
908af16
feat: TTS store, service, TTSButton, KokoroTTSManager, and App wiring
alichherawalla Apr 7, 2026
11f099b
feat: ChatMessage speak action, test fixes, and TTS plan update
alichherawalla Apr 7, 2026
b060674
fix: audio recording at 16 kHz and strip audio from non-audio LLM mes…
alichherawalla Apr 7, 2026
8bd96b3
fix: audio bubble playback and positioning
alichherawalla Apr 7, 2026
becde09
fix: audio attachments render as compact badge in Chat Mode
alichherawalla Apr 7, 2026
63aefb9
fix: smart audio mode flag — isAudioModeMessage persists per-message
alichherawalla Apr 7, 2026
3e54248
fix: audio bubble play, layout, voice cycling
alichherawalla Apr 7, 2026
339839c
fix: audio mode messages now render as audio bubbles + streaming TTS
alichherawalla Apr 7, 2026
ed5a0c4
docs: add cross-conversation RAG to personas plan
alichherawalla Apr 7, 2026
a4a00c1
fix: audio mode bubbles, waveform, chat-mode voice playback, input UI
alichherawalla Apr 7, 2026
e4cc785
fix: render all AI messages as audio bubbles in audio mode + voice label
alichherawalla Apr 7, 2026
63db18a
fix: live speed control, AI duration estimate, audio input layout
alichherawalla Apr 7, 2026
c56ce85
fix: flatten audio mode bar, dismiss popover on mode switch
alichherawalla Apr 8, 2026
78cc400
fix: remove TTS chunking, add pause/resume and amplitude state
alichherawalla Apr 8, 2026
0014dd1
fix: thinking block rendering in audio mode messages
alichherawalla Apr 8, 2026
d86d857
fix: Kokoro pause/resume, keepAlive, amplitude RMS + audio bubble UX
alichherawalla Apr 8, 2026
c310876
fix: strip think tags, XML tool calls, and markdown from TTS speech
alichherawalla Apr 8, 2026
df030c8
fix: streaming TTS, parallel transcription, popover positioning
alichherawalla Apr 8, 2026
f47bb3c
fix: thinking block width constraint + download manager UI updates
alichherawalla Apr 8, 2026
73aad91
fix: waveform animation, voice change crash, playback progress
alichherawalla Apr 8, 2026
c49f6ea
fix: stop TTS on app background and screen lock
alichherawalla Apr 8, 2026
6cec1ab
fix: move voice selector from audio bubbles to bottom bar
alichherawalla Apr 8, 2026
f856e8d
fix: tap-to-toggle recording in audio mode
alichherawalla Apr 8, 2026
dcd5102
fix: remove Transcribing text from audio mode mic button
alichherawalla Apr 8, 2026
0870fed
fix: thinking block blank bubble in audio mode
alichherawalla Apr 8, 2026
51bc18e
fix: waveform animation, voice picker popover, thinking block, playba…
alichherawalla Apr 8, 2026
662f210
fix: smooth progress bar, pause/resume on app switch, targeted store …
alichherawalla Apr 8, 2026
ac6de63
feat: audio mode system prompt for conversational voice responses
alichherawalla Apr 8, 2026
996b986
feat: rename voices to mood personas with pre-configured speeds
alichherawalla Apr 8, 2026
d8c9e00
feat: seekable progress bar — tap to jump to position in audio
alichherawalla Apr 8, 2026
a472a28
fix: speed chip syncs with persona default speed from store
alichherawalla Apr 8, 2026
29ebf34
fix: voice change crash, paused waveform, seekbar UX
alichherawalla Apr 8, 2026
dd97b03
fix: remove bottom hitSlop from play/speed buttons so seekbar receive…
alichherawalla Apr 8, 2026
6883ded
fix: seekbar now integrated into waveform area — tap waveform to seek
alichherawalla Apr 8, 2026
090bc80
fix: restore show transcript toggle on all audio bubbles
alichherawalla Apr 8, 2026
2542d56
fix: seekbar now uses onLayout width instead of measure()
alichherawalla Apr 8, 2026
6b7acf5
fix: full-width seekbar with debug logs, separate from waveform
alichherawalla Apr 8, 2026
6d529ce
fix: move seekbar below show transcript toggle
alichherawalla Apr 8, 2026
410d828
fix: always wait 300ms after kokoroRef.stop() before new speak()
alichherawalla Apr 8, 2026
b9bd6c7
fix: seekbar position preserved across stop/speak cycle
alichherawalla Apr 8, 2026
078e99b
fix: no flicker on seek, progress bar above show transcript
alichherawalla Apr 8, 2026
19de292
debug: add logs to blur/stop handlers for TTS navigation debugging
alichherawalla Apr 8, 2026
e60d245
fix: defer voice config change until Kokoro is idle — prevents native…
alichherawalla Apr 8, 2026
ea2222e
fix: center seekbar dot, stop TTS on back navigation
alichherawalla Apr 8, 2026
483d232
debug: add logs to handlePlayPause to trace wrong transcript issue
alichherawalla Apr 8, 2026
298fcee
fix: seekbar always visible on AI audio bubbles, not just during play…
alichherawalla Apr 8, 2026
a3c5891
feat: draggable seekbar — tap or drag to seek
alichherawalla Apr 8, 2026
8c3202e
fix: center seekbar thumb dot vertically on track
alichherawalla Apr 8, 2026
52422d6
refactor: fix all lint errors — extract components, reduce complexity
alichherawalla Apr 8, 2026
922594a
fix: remaining lint errors — unused var and param count
alichherawalla Apr 8, 2026
094aea0
fix: all tests passing — mock executorch, update test assertions
alichherawalla Apr 9, 2026
aa71157
refactor: replace custom PanResponder seekbar with native Slider
alichherawalla Apr 9, 2026
fbd7366
refactor: static waveform bars — remove all animation/amplitude tracking
alichherawalla Apr 9, 2026
097286e
feat: word highlighting in transcript, stop TTS on record, fix multi-…
alichherawalla Apr 9, 2026
47d44de
fix: voice change crash cooldown, single-word highlight with auto-scroll
alichherawalla Apr 9, 2026
372d40a
fix: remove inaccurate word highlighting, add playing state visual
alichherawalla Apr 9, 2026
a2b41ea
feat: WhatsApp-style waveform progress + increase voice change cooldown
alichherawalla Apr 9, 2026
00075da
fix: consistent bubble widths, fixed-width audio bubble
alichherawalla Apr 9, 2026
c26d7cc
feat: seekbar overlaid on waveform, visible on both user and AI bubbles
alichherawalla Apr 9, 2026
b3d7077
fix: hide seekbar thumb when not playing — no stray dot at position 0
alichherawalla Apr 9, 2026
43d972b
fix: seekbar thumb always visible, fix bar/thumb alignment
alichherawalla Apr 9, 2026
7410fe4
fix: WhatsApp-style layout — waveform full width, meta row below
alichherawalla Apr 9, 2026
e5b4816
fix: waveform bars span full width using space-between
alichherawalla Apr 9, 2026
d14dd2a
fix: tighter bars, bigger speed chip, 2s voice cooldown from stream end
alichherawalla Apr 9, 2026
5d99192
fix: waveform full bubble width, play button moved to meta row
alichherawalla Apr 9, 2026
e8bce31
fix: revert — play button back on left, reduce gap for wider waveform
alichherawalla Apr 9, 2026
33dd403
fix: add left margin to waveform for spacing from play button
alichherawalla Apr 9, 2026
e958dd4
fix: increase waveform left margin to SPACING.sm
alichherawalla Apr 9, 2026
64c6a2a
fix: waveform extends to bubble right edge, spacing from play button
alichherawalla Apr 9, 2026
ff738a0
fix: bars flex to fill full waveform width — no right gap
alichherawalla Apr 9, 2026
42e606c
fix: remove negative right margin — waveform stays within bubble
alichherawalla Apr 9, 2026
54f7a54
fix: audio playback state races, voice switch crash, chat scroll & UI
alichherawalla Apr 9, 2026
ea27099
chore: interim
alichherawalla Apr 9, 2026
0beba49
Merge branch 'main' of github.com:alichherawalla/off-grid-mobile-ai i…
alichherawalla Apr 9, 2026
a49e4a0
fix: tool-call audio rendering, transcript scroll, action menu, condi…
alichherawalla Apr 9, 2026
4cf1a10
fix: stop TTS on retry/resend to prevent orphaned audio playback
alichherawalla Apr 9, 2026
51c33c0
revert: keep KokoroTTSManager always mounted
alichherawalla Apr 9, 2026
6861c30
fix: drop streaming TTS chain, speak full response after streaming ends
alichherawalla Apr 9, 2026
609ddd5
feat: pluggable TTS engine interface with Kokoro + OuteTTS adapters
alichherawalla Apr 9, 2026
d300dbb
feat: pro feature registry, license seam, and submodule
dishit-wednesday Jun 19, 2026
26db7c0
feat: tool extension seam + MCP wiring into the generation loop
dishit-wednesday Jun 19, 2026
3d5613b
feat: RevenueCat pro license service + unlock modal
dishit-wednesday Jun 19, 2026
99718d6
feat: in-app debug log viewer
dishit-wednesday Jun 19, 2026
fcbc9c9
feat: pro-gated email/calendar tools via ToolExtension
dishit-wednesday Jun 19, 2026
66ff0c4
feat: add RevenueCat config key placeholders
dishit-wednesday Jun 19, 2026
18ee7f1
fix: map @offgrid/core/* in tsconfig so tsc resolves the pro extension
dishit-wednesday Jun 19, 2026
f2ca569
fix: make CI green without the private pro submodule
dishit-wednesday Jun 19, 2026
48bfd36
feat: pro unlock modal polish (dismiss, border, input-gated CTA)
dishit-wednesday Jun 19, 2026
d17f218
chore: bump pro submodule for MCP screen fixes
dishit-wednesday Jun 19, 2026
2e7de13
Merge audio mode (TTS engine interface) into pro-feature-registry base
Jun 19, 2026
0e12a06
refactor: move audio/TTS mode into the pro submodule behind a slot/ho…
Jun 19, 2026
42bd91f
fix(boot): isolate RevenueCat init so Pro features load when billing …
alichherawalla Jun 19, 2026
bce7187
test(tts): cover Kokoro on-disk install-status detection
alichherawalla Jun 19, 2026
1d16749
feat(chat-input): inline Chat/Audio toggle slot + fix clipped pill icons
alichherawalla Jun 19, 2026
feeae0f
fix(chat): stop stacking bottom safe-area below the chat input
alichherawalla Jun 19, 2026
4c3cc32
feat(models): add Voice Models tab with pro panel + non-pro upsell
alichherawalla Jun 19, 2026
e944b68
fix(downloads): collapse TTS/STT chips into one Voice Models filter
alichherawalla Jun 19, 2026
e9cdb00
feat(models): add Transcription Models (speech-to-text) tab
alichherawalla Jun 19, 2026
e1e2f98
feat(downloads): show voice + transcription models in Download Manager
alichherawalla Jun 19, 2026
8e09586
test(models): fix mock typing in TranscriptionModelsTab test
alichherawalla Jun 19, 2026
697e7a3
test(audio): cover GGUF download reroute + partial-file fix
alichherawalla Jun 19, 2026
39ef4a0
fix(models): reuse shared search styling in Transcription tab
alichherawalla Jun 19, 2026
238d406
docs: add reuse-before-building rule + model routing plan
alichherawalla Jun 19, 2026
157900d
docs(routing): pin classifier choice + gate routing on multiple models
alichherawalla Jun 19, 2026
ccd6bbe
feat(routing): add model residency manager (memory budget + eviction)
alichherawalla Jun 19, 2026
46b384c
feat(image): disable prompt enhancement when no text model is available
alichherawalla Jun 19, 2026
0bde2b9
feat(routing): make residency manager authoritative for model memory
alichherawalla Jun 19, 2026
8f9e643
refactor(models): remove the modelLoadingStrategy (fast/optimised) se…
alichherawalla Jun 19, 2026
2159ba3
feat(routing): remember last text model + preload it on chat open
alichherawalla Jun 19, 2026
7ce42ae
feat(routing): differentiate text vs image + load text model on demand
alichherawalla Jun 19, 2026
19130b4
feat(routing): use the SMOL LLM classifier for image-only routing
alichherawalla Jun 19, 2026
a328699
feat(routing): auto-provision the default SmolLM2 classifier
alichherawalla Jun 19, 2026
16b0445
test(pro): fix pro-boot entitlement gating tests under __DEV__
alichherawalla Jun 19, 2026
a0de93d
fix(chat): load models inline, no full-screen takeover
alichherawalla Jun 20, 2026
cf1605b
feat(routing): warm selected models at boot (text -> image -> TTS -> …
alichherawalla Jun 20, 2026
ed6b3a6
fix(routing): don't lose the message when the model selector opens
alichherawalla Jun 20, 2026
ca37086
test(routing): type preloader test mocks with rest params for tsc
alichherawalla Jun 20, 2026
ae5eeb9
feat(audio): wire streaming TTS + chat-mode select-text; bump pro
alichherawalla Jun 21, 2026
3047ba2
fix(audio): bump pro — cumulative seekbar position across streamed se…
alichherawalla Jun 21, 2026
66842eb
feat(home): collapsed Models control + per-type pickers; bump pro
alichherawalla Jun 21, 2026
2cd681e
feat(chat): Models selector in chat header → shared manager sheet
alichherawalla Jun 21, 2026
b4ab879
feat(settings): remove Voice Transcription + Text to Speech settings …
alichherawalla Jun 21, 2026
514202a
feat(transcription): multi-model — select/download/delete per model
alichherawalla Jun 21, 2026
6dd4383
chore(home): delete dead ActiveModelsSection; drop audioSummaryLabel …
alichherawalla Jun 21, 2026
02765fd
test(audio): cover streaming TTS, multi-model transcription, voice ro…
alichherawalla Jun 21, 2026
a5916b1
refactor(models): move shared model sheets to components + shadow the…
alichherawalla Jun 21, 2026
ab7b705
make litert model card ui similar to other cards
dishit-wednesday Jun 21, 2026
8053039
Hide pro banner once pro is registered
dishit-wednesday Jun 21, 2026
beabd32
fix supportemail type
dishit-wednesday Jun 21, 2026
52eeaa8
chore(knip): add tuned config and remove dead code it surfaced
alichherawalla Jun 21, 2026
501c6f3
gate debug logs and
dishit-wednesday Jun 21, 2026
27b0d21
test: update screen tests for the model-selector + voice redesign
alichherawalla Jun 21, 2026
6142e42
chore(pro): bump submodule to merged audio-mode tip (c00afad8)
alichherawalla Jun 21, 2026
4ae7031
Merge remote-tracking branch 'upstream/main' into feat/pro-feature-re…
dishit-wednesday Jun 21, 2026
f0cba52
chore(mcp): add native deps for OAuth sign-in
dishit-wednesday Jun 21, 2026
dab716b
feat(mcp): native OAuth adapters, boot wiring, and redirect scheme
dishit-wednesday Jun 21, 2026
b9b2981
test(mcp): unit tests for OAuth pkce, metadata, tokens, and client retry
dishit-wednesday Jun 21, 2026
e1b9e57
chore: remove orphaned website revenuecat-link test
dishit-wednesday Jun 21, 2026
34bec91
feat(settings): release-visible Debug Logs button + MCP OAuth browser…
dishit-wednesday Jun 22, 2026
c48f671
fix(tools): inject MCP hint once, and only when no native tool calling
dishit-wednesday Jun 22, 2026
c927452
test(mcp): cover schema trimming (pass-through, budget, required pres…
dishit-wednesday Jun 22, 2026
ab72335
chore(merge): resolve conflicts between feat/audio-mode-pro and feat/…
dishit-wednesday Jun 22, 2026
35f00e6
fix(ci): move pro-source tests into the pro submodule
dishit-wednesday Jun 22, 2026
7b43df5
fix(settings): keep Debug Logs dev-only; bump pro pointer
dishit-wednesday Jun 22, 2026
9ddf89f
test: fix tool-loop mock + cover tool-call parsing and native OAuth a…
dishit-wednesday Jun 22, 2026
57d5caf
fix(android-test): relax bytecode verification for unit tests (Amazon…
dishit-wednesday Jun 22, 2026
3d036cb
fix(pro): open unlock modal in verify mode from "Already paid?"
dishit-wednesday Jun 22, 2026
1db5d14
feat(home): animated load progress + rotating tips on the loading ove…
dishit-wednesday Jun 22, 2026
b5ea555
Merge remote-tracking branch 'upstream/main' into feat/pro-feature-re…
dishit-wednesday Jun 22, 2026
0a4aca2
fix(logging): re-wire in-app debug viewer dev-only after taking main'…
dishit-wednesday Jun 22, 2026
89cc17c
test: restore branch coverage above 80% after merging main
dishit-wednesday Jun 22, 2026
3e83836
feat(pro): reframe unlock copy around 'membership' / Get Pro
dishit-wednesday Jun 23, 2026
7d917e4
feat(litert): two-pass tool selection for large tool sets
dishit-wednesday Jun 23, 2026
9e20444
fix(litert): recover from native tool-call parse failures instead of …
dishit-wednesday Jun 23, 2026
61b435e
chore(deps): add react-compiler-runtime
dishit-wednesday Jun 23, 2026
6d00bf2
fix(chat): restore KeyboardAvoidingView keyboard avoidance
dishit-wednesday Jun 23, 2026
7c97849
fix(models): engine-aware loaded check so LiteRT models don't reload
dishit-wednesday Jun 23, 2026
aa0b31c
feat(audio): render the voice empty-state hero slot
dishit-wednesday Jun 23, 2026
39e2ddb
feat(chat-input): header Chat/Voice toggle, drop thinking bar icon, p…
dishit-wednesday Jun 23, 2026
ab319e3
feat(audio): larger voice mic + taller chat input
dishit-wednesday Jun 23, 2026
4c606f2
chore: bump pro submodule to audio-mode UI work
dishit-wednesday Jun 23, 2026
7a8c1f6
chore: bump pro submodule — Gemini review fixes
dishit-wednesday Jun 23, 2026
db231bf
feat(pro): Get Pro opens web pay page directly; modal is verify-only
dishit-wednesday Jun 23, 2026
244b3e8
Merge feat/pro-feature-registry into feat/audio-mode-pro
dishit-wednesday Jun 23, 2026
d460cd1
feat(litert): surface curated LiteRT models in onboarding
dishit-wednesday Jun 23, 2026
f9cceeb
chore: bump pro submodule to drop MCP Servers settings entry
dishit-wednesday Jun 23, 2026
db34070
Merge feat/pro-feature-registry (LiteRT onboarding) into feat/audio-m…
dishit-wednesday Jun 23, 2026
6128f70
chore: bump pro submodule to include 'drop MCP Servers settings entry'
dishit-wednesday Jun 23, 2026
3780e72
fix(ios): consolidate react-native-fs onto the @dr.pogodin fork
dishit-wednesday Jun 23, 2026
22e486b
feat(litert): direct audio input for LiteRT models on Android
dishit-wednesday Jun 23, 2026
319c348
feat(tools): two-pass tool routing for on-device models (iOS llama + …
dishit-wednesday Jun 23, 2026
60d0303
feat(audio): bump pro to audio-mode progress captions
dishit-wednesday Jun 23, 2026
a890f7e
fix(audio): bump pro for TTS low-RAM + react-native-fs shim
dishit-wednesday Jun 23, 2026
2431011
fix(ui): open the next sheet only after the Models manager fully closes
dishit-wednesday Jun 23, 2026
5c06870
test(chat-input): give useAppStore mock a getState for supportsAudioI…
dishit-wednesday Jun 23, 2026
af5e596
feat(mcp): wire MCP SDK OAuth — URL polyfill, deps, submodule bump
dishit-wednesday Jun 24, 2026
ae6f8b6
test: update ChatInputModeToggle test to the dropdown UX + bump pro s…
dishit-wednesday Jun 24, 2026
094dfef
fix(litert): send image and audio together in one turn
dishit-wednesday Jun 24, 2026
380d20b
fix(audio): clear attachments after a keyboard send in audio mode
dishit-wednesday Jun 24, 2026
c764027
chore: remove temporary audio/sheet debug logging
dishit-wednesday Jun 24, 2026
96ad679
fix(chat): update OpenCL warning message for clarity on GPU support o…
dishit-wednesday Jun 24, 2026
1fc39bf
fix(whisper): keep transcription model list in sync with disk
dishit-wednesday Jun 24, 2026
da75d34
test(whisper): cover per-model spinner and disk reconciliation
dishit-wednesday Jun 24, 2026
d7e2f9f
refactor(settings): extract shared SliderSetting component
dishit-wednesday Jun 24, 2026
b61bdc9
test(settings): cover SliderSetting and its adopters
dishit-wednesday Jun 24, 2026
3b13dd4
feat(components): add centered alert dialog
dishit-wednesday Jun 24, 2026
e52e839
feat(models): show Pro upsell in Home voice sheet for free builds
dishit-wednesday Jun 24, 2026
3b08ce2
fix(chat): honour the Tools 'N/A' gate during generation
dishit-wednesday Jun 24, 2026
846b4b0
test(chat): cover the Tools N/A generation gate
dishit-wednesday Jun 24, 2026
51b02f3
test(audio): cover voice panel disk-sync on focus
dishit-wednesday Jun 24, 2026
edd5dda
chore: bump pro submodule (audio + mcp UI fixes)
dishit-wednesday Jun 24, 2026
013ac5e
fix(lint): resolve pre-existing inline-style and complexity errors
dishit-wednesday Jun 24, 2026
3c14a66
test(home): drive deferred manager-sheet flow; match LoadingOverlay
dishit-wednesday Jun 24, 2026
94dc271
fix(audio): wire MCP sheet into the audio-mode quick settings
dishit-wednesday Jun 24, 2026
84db5bf
test(audio): update ChatInputModeToggle for alert-based no-model flow
dishit-wednesday Jun 24, 2026
2030484
chore(android): strip RevenueCat-injected Play Billing permission
dishit-wednesday Jun 24, 2026
47dd59c
fix(navigation): dedupe screen registration by name
dishit-wednesday Jun 24, 2026
d6ea2a1
chore(logs): strip verbose debug logging from services and LiteRT
dishit-wednesday Jun 24, 2026
ee96d76
chore: bump pro submodule (Kokoro voice-switch fixes + audio cleanup)
dishit-wednesday Jun 25, 2026
1acba96
fix(lint): drop unused loop var left by the log strip
dishit-wednesday Jun 25, 2026
8fdd8c8
chore(mcp): bump pro submodule — auth dropdown selected-state + form …
dishit-wednesday Jun 25, 2026
9dabc9a
Merge pull request #413 from off-grid-ai/feat/ios-build-rnfs-litert-a…
dishit-wednesday Jun 25, 2026
670465e
Merge pull request #409 from off-grid-ai/feat/audio-mode-pro
dishit-wednesday Jun 25, 2026
b54aa1b
fix(deps): pin react-native-zip-archive to 7.1.0 so patch-package app…
dishit-wednesday Jun 25, 2026
40d9092
test(ci): exclude pro-submodule suites from the public repo's CI
dishit-wednesday Jun 25, 2026
a83e465
fix(deps): pin react-native-zip-archive to 7.1.0 so patch-package app…
dishit-wednesday Jun 25, 2026
60fd37b
Merge pull request #415 from off-grid-ai/feat/ios-build-rnfs-litert-a…
dishit-wednesday Jun 25, 2026
cd7b947
fix(ios): activate the AVAudioSession before recording
dishit-wednesday Jun 25, 2026
aa13b25
fix(lint): use template literal in audioRecorderService (prefer-templ…
dishit-wednesday Jun 25, 2026
c9cfdc9
fix(deps): remove unused react-native-background-downloader
dishit-wednesday Jun 25, 2026
9b280c9
fix(lint): baseline NotificationPermission (unused audio-api playback…
dishit-wednesday Jun 25, 2026
4a6ae56
chore(pro): remove temporary 'Reset Pro identity' testing button
dishit-wednesday Jun 25, 2026
8ccb40d
test: raise Jest branch coverage to 82.69% to pass CI gate
dishit-wednesday Jun 25, 2026
8602aac
fix(downloads): keep Whisper download state in sync across surfaces
dishit-wednesday Jun 25, 2026
2b999b6
fix(deps): restore react-native-background-downloader (used at boot)
dishit-wednesday Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "pro"]
path = pro
url = https://github.com/wednesday-solutions/offgrid-pro.git
65 changes: 63 additions & 2 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,39 @@ import { useTheme } from './src/theme';
import { hardwareService, modelManager, authService, ragService, remoteServerManager } from './src/services';
import logger from './src/utils/logger';
import { useAppStore, useAuthStore, useRemoteServerStore } from './src/stores';
import { useDebugLogsStore } from './src/stores/debugLogsStore';
import { loadProFeatures } from './src/bootstrap/loadProFeatures';
import { preloadSelectedModels } from './src/services/modelPreloader';
import { configureRevenueCat, checkProStatus } from './src/services/proLicenseService';
import { hydrateDownloadStore } from './src/services/downloadHydration';
import { useDownloadListeners } from './src/hooks/useDownloads';
import { getSlot, SLOTS } from './src/bootstrap/slotRegistry';
import { LockScreen } from './src/screens';
import { useAppState } from './src/hooks/useAppState';
import { useDownloadStore } from './src/stores/downloadStore';

LogBox.ignoreAllLogs(); // Suppress all logs

// Dev-only: mirror logger output into the in-app Debug Logs viewer. The whole block
// is behind __DEV__, so release builds keep main's no-op logger (zero logging cost).
if (__DEV__) {
const fmt = (a: unknown): string => {
if (a instanceof Error) return `${a.name}: ${a.message}`;
if (typeof a === 'string') return a;
try { return JSON.stringify(a); } catch { return String(a); }
};
const base = { log: logger.log, warn: logger.warn, error: logger.error };
const tap = (level: 'log' | 'warn' | 'error') => (...args: unknown[]) => {
base[level](...args);
try {
useDebugLogsStore.getState().addLog({ timestamp: Date.now(), level, message: args.map(fmt).join(' ') });
} catch { /* never break logging */ }
};
logger.log = tap('log');
logger.warn = tap('warn');
logger.error = tap('error');
}

const ensureRemoteServerStoreHydrated = async () => {
const persistApi = useRemoteServerStore.persist;
if (!persistApi?.hasHydrated || !persistApi.rehydrate) return;
Expand Down Expand Up @@ -166,11 +191,46 @@ function App() {
// Initialize RAG database tables
ragService.ensureReady().catch((err) => logger.error('Failed to initialize RAG service on startup', err));

// Configure RevenueCat and read the cached entitlement before Pro features load.
// configureRevenueCat is sync; checkProStatus reads the keychain cache immediately
// and fires a background RC network sync so the next launch stays fresh.
//
// Pro is optional: a failure here (missing native module, keychain locked,
// bad RC config) must never abort app init or hang the splash screen, so the
// whole block is isolated and only logs on error.
// RevenueCat is isolated in its own try: a failure here (no billing on a
// simulator, bad RC config, no network) must NOT prevent pro features from
// loading — otherwise the dev unlock below would never run.
let isPro = false;
try {
configureRevenueCat();
isPro = await checkProStatus();
} catch (rcError) {
logger.error('[App] RevenueCat init failed, continuing without entitlement:', rcError);
}

try {
// Load pro features — only activates if the keychain entitlement is set
// (or in dev, where loadProFeatures force-unlocks).
await loadProFeatures(isPro);

// DEV ONLY: treat dev builds as Pro so the upsell banner hides and pro
// UI is unlocked for local testing. Never runs in release (__DEV__ false).
if (__DEV__) {
useAppStore.getState().setHasRegisteredPro(true);
}
} catch (proError) {
logger.error('[App] Pro feature load failed, continuing without Pro:', proError);
}

// Show the UI immediately
setIsInitializing(false);

// Models are loaded on-demand when the user opens a chat,
// not eagerly on startup, to avoid freezing the UI.
// Warm the selected models in the background (text → image → TTS → STT,
// budget-gated, sequential) so the common paths have no cold-start wait.
// Fire-and-forget after the UI is up; loads run one at a time off the JS
// thread so they don't freeze the screen.
preloadSelectedModels();
} catch (error) {
logger.error('[App] Error initializing app:', error);
setIsInitializing(false);
Expand Down Expand Up @@ -223,6 +283,7 @@ function App() {
<GestureHandlerRootView style={styles.flex}>
<SafeAreaProvider>
<StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} backgroundColor={colors.background} />
{(() => { const AppRoot = getSlot(SLOTS.appRoot); return AppRoot ? <AppRoot /> : null; })()}
<NavigationContainer
theme={{
dark: isDark,
Expand Down
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ The emotional arc for all content: **Recognition -> Return -> Freedom**. Name wh
- Never use `lucide-react` or any other icon library — only `react-native-vector-icons`.
- Follow the 5-category text hierarchy: TITLE → BODY → SUBTITLE/DESCRIPTION → META.

## Reuse Before Building

**Before writing any new component, style, hook, or service, search for an existing one and reuse it.** Building a parallel version of something that already exists creates visual and behavioural drift (e.g. a search box that looks different from every other search box).

- For UI: grep `src/components/` and the relevant screen folder for an existing component or shared style (e.g. `ModelCard`, `Card`, `Button`, shared `searchContainer`/`searchInput` styles) before creating your own. Two screens that show the same kind of thing must use the same component.
- For logic: check for an existing hook/service/store action (`grep -rn`) before adding a new one.
- If an existing component is close but not exact, extend it with a prop rather than forking a copy.
- Only build new when nothing fits — and say so in the PR description.

## Architecture & Abstractions (SOLID)

**Design to abstractions, not concrete implementations.** When there are multiple interchangeable implementations of a thing (TTS engines, model backends, providers, storage), the rest of the app must depend on a single interface/service layer — never branch on a concrete type.

- **No leaking implementation details upward.** UI and stores must not do `instanceof SpecificEngine`, check `engineId === 'kokoro'`, or branch on capabilities to decide *how* to do something. Push that decision behind the abstraction (the engine/provider implements it; or a service layer dispatches once). If you find yourself writing `if (engine X) … else …` in a component, the abstraction is wrong.
- **Single uniform entry point.** Prefer one polymorphic method (e.g. `engine.play(text, opts)`) that every implementation satisfies over several mechanism-specific methods (`speak` vs `playFromFile`) that callers must choose between.
- **Service layer between UI and implementations.** Implementations (engines/adapters) are swappable; a service abstracts them and exposes a normalized API + state. Adding a new implementation must require zero changes to UI/store.
- **Dependency Inversion / Liskov:** any implementation must be substitutable through the interface without callers knowing which one is active. Normalize gaps (e.g. an engine that can't report playback position) inside the service, not in the UI.
- Apply the rest of SOLID: single responsibility per module, open for extension (add an implementation) / closed for modification (don't touch callers), segregated interfaces (don't force implementations to stub methods they can't support — model that with the abstraction).

## Pre-Commit Quality Gates

All quality gates run automatically via Husky on every `git commit`, scoped to the file types you staged:
Expand Down
2 changes: 1 addition & 1 deletion __tests__/integration/generation/generationFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ describe('Generation Flow Integration', () => {
onComplete: expect.any(Function),
onError: expect.any(Function),
}),
['file:///one.png', 'file:///two.png'],
{ imageUris: ['file:///one.png', 'file:///two.png'], audioUris: [] },
);

const [, callbacks] = mockLiteRTService.sendMessage.mock.calls[0];
Expand Down
178 changes: 178 additions & 0 deletions __tests__/integration/generation/toolExtensionLoop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* Integration test: registered ToolExtension flows through runToolLoop.
*
* Verifies:
* 1. Extension system-prompt hint is appended to the system message.
* 2. Extension tool calls parsed from LLM text are collected.
* 3. Extension executor is called instead of the built-in executeToolCall.
* 4. Extension tool calls result in a tool-result message in the chat store.
* 5. Free path (no extensions): behaviour is identical to today.
*/

import { runToolLoop, ToolLoopContext } from '../../../src/services/generationToolLoop';
import {
registerToolExtension,
_clearExtensionsForTesting,
ToolExtension,
} from '../../../src/services/tools/extensions';
import type { ToolCall } from '../../../src/services/tools/types';
import { useChatStore } from '../../../src/stores';
import { resetStores } from '../../utils/testHelpers';

// Mock the LLM so we control what it "says"
jest.mock('../../../src/services/llm');
jest.mock('../../../src/services/litert');
jest.mock('../../../src/services/activeModelService');

const { llmService } = require('../../../src/services/llm');
const { liteRTService } = require('../../../src/services/litert');

// Mock executeToolCall so built-in tools don't actually run
jest.mock('../../../src/services/tools', () => ({
getToolsAsOpenAISchema: jest.fn(() => []),
executeToolCall: jest.fn().mockResolvedValue({ name: 'builtin', content: 'builtin-result', durationMs: 1 }),
}));

// Mock the stores index: pull in the REAL chat + app stores (the loop reads/writes
// them and the test asserts on stored messages), stub the remote server store so we
// stay on the local path. Requiring the submodules directly avoids loading the full
// stores index (auth/whisper/project), which fails to initialise in this test env.
jest.mock('../../../src/stores', () => {
const { useChatStore: realChatStore } = jest.requireActual('../../../src/stores/chatStore');
const { useAppStore: realAppStore } = jest.requireActual('../../../src/stores/appStore');
return {
useChatStore: realChatStore,
useAppStore: realAppStore,
useRemoteServerStore: {
getState: () => ({ activeServerId: null, activeRemoteTextModelId: null }),
},
};
});

// Fake extension that parses <mcp_call>tool_name</mcp_call> tags
const MCP_TOOL_NAME = 'mcp_fake_tool';
const MCP_HINT = '\n\nMCP tools available:\n- mcp_fake_tool: A fake MCP tool';
const MCP_RESULT = 'mcp-result-content';

function makeFakeExtension(executorMock: jest.Mock): ToolExtension {
return {
id: 'mcp',
getSystemPromptHint: () => MCP_HINT,
parseToolCalls: (text: string): ToolCall[] => {
const match = /<mcp_call>([\s\S]*?)<\/mcp_call>/.exec(text);
if (!match) return [];
return [{ id: 'mcp-tc-1', name: match[1].trim(), arguments: {} }];
},
stripFromVisibleText: (text: string) => text.replace(/<mcp_call>[\s\S]*?<\/mcp_call>/g, '').trim(),
canHandle: (name: string) => name === MCP_TOOL_NAME,
execute: executorMock,
enabledToolCount: () => 1,
};
}

function makeCtx(overrides: Partial<ToolLoopContext> = {}): ToolLoopContext {
// createConversation takes a modelId and returns the generated conversation UUID
const conversationId = useChatStore.getState().createConversation('test-model');
return {
conversationId,
messages: [
{ id: 'sys', role: 'system', content: 'You are helpful.', timestamp: 0 },
{ id: 'u1', role: 'user', content: 'Run the MCP tool.', timestamp: 1 },
],
enabledToolIds: [],
isAborted: () => false,
onThinkingDone: jest.fn(),
onFinalResponse: jest.fn(),
...overrides,
};
}

describe('tool extension loop integration', () => {
beforeEach(() => {
resetStores();
_clearExtensionsForTesting();
jest.clearAllMocks();
liteRTService.isModelLoaded.mockReturnValue(false);
llmService.isModelLoaded.mockReturnValue(true);
llmService.stopGeneration.mockResolvedValue(undefined);
});

describe('free path — no extensions registered', () => {
it('calls onFinalResponse with the LLM text', async () => {
llmService.generateResponseWithTools.mockResolvedValue({
fullResponse: 'Hello world',
toolCalls: [],
});
const ctx = makeCtx();
await runToolLoop(ctx);
expect(ctx.onFinalResponse).toHaveBeenCalledWith('Hello world');
});
});

function setupProExtension(
firstResponse = `<mcp_call>${MCP_TOOL_NAME}</mcp_call>`,
secondResponse = 'Done.',
): jest.Mock {
const executorMock = jest.fn().mockResolvedValue({
name: MCP_TOOL_NAME, content: MCP_RESULT, durationMs: 5,
});
registerToolExtension(makeFakeExtension(executorMock));
llmService.generateResponseWithTools
.mockResolvedValueOnce({ fullResponse: firstResponse, toolCalls: [] })
.mockResolvedValueOnce({ fullResponse: secondResponse, toolCalls: [] });
return executorMock;
}

describe('pro path — extension registered', () => {
it('appends extension hint to the system prompt sent to LLM', async () => {
setupProExtension();

const ctx = makeCtx();
await runToolLoop(ctx);

// System prompt in the first LLM call should contain the extension hint
const firstCallMessages = llmService.generateResponseWithTools.mock.calls[0][0] as any[];
const sysMsg = firstCallMessages.find((m: any) => m.role === 'system');
expect(sysMsg.content).toContain(MCP_HINT);
});

it('routes execution to the extension executor, not built-in executeToolCall', async () => {
const executorMock = setupProExtension();

const ctx = makeCtx();
await runToolLoop(ctx);

expect(executorMock).toHaveBeenCalledTimes(1);
expect(executorMock).toHaveBeenCalledWith(
expect.objectContaining({ name: MCP_TOOL_NAME }),
);

const { executeToolCall } = require('../../../src/services/tools');
expect(executeToolCall).not.toHaveBeenCalled();
});

it('stores tool result in chat store', async () => {
setupProExtension();

const ctx = makeCtx();
await runToolLoop(ctx);

const messages = useChatStore.getState().conversations.find(c => c.id === ctx.conversationId)?.messages ?? [];
const toolResultMsg = messages.find(m => m.role === 'tool' && m.toolName === MCP_TOOL_NAME);
expect(toolResultMsg).toBeDefined();
expect(toolResultMsg?.content).toBe(MCP_RESULT);
});

it('strips extension syntax from visible text', async () => {
setupProExtension(`Thinking...<mcp_call>${MCP_TOOL_NAME}</mcp_call>`, 'Final answer.');

const ctx = makeCtx();
await runToolLoop(ctx);

// The assistant message stored for the tool-call turn must not contain the raw tag
const messages = useChatStore.getState().conversations.find(c => c.id === ctx.conversationId)?.messages ?? [];
const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0);
expect(assistantMsg?.content).not.toContain('<mcp_call>');
});
});
});
Loading
Loading