Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
193 commits
Select commit Hold shift + click to select a range
f83cbac
feat(pro): autolink the pro native library via react-native.config.js
dishit-wednesday Jun 27, 2026
e4443b8
feat(locket): core whisper file-transcription support (chunkable opts…
dishit-wednesday Jun 27, 2026
dd7f01b
feat(locket): add 'whisper' remote-server provider type
dishit-wednesday Jun 27, 2026
5748d1c
feat(core): persistent + in-memory debug logging for export
dishit-wednesday Jun 27, 2026
0b28261
feat(ios): background-audio mode for locket recorder
dishit-wednesday Jun 27, 2026
59e4439
chore(ios): pod install for OffgridPro
dishit-wednesday Jun 27, 2026
eb9aea1
chore: bump pro submodule to feat/locket (locket feature)
dishit-wednesday Jun 27, 2026
e72ac68
feat(transcription): offset/duration chunking, live segments, CoreML …
dishit-wednesday Jun 28, 2026
52cdfdf
feat(nav): Memory tab (Pro recorder/paywall) + move Settings to Home …
dishit-wednesday Jun 28, 2026
db0a6ae
feat(chat): summarize oversized transcript/document attachments
dishit-wednesday Jun 28, 2026
4d7ab20
chore: bump pro submodule (transcription slicing + attach-to-chat)
dishit-wednesday Jun 28, 2026
3cbe0b8
feat(chat): stream transcript summaries live + bigger Summarize chip
dishit-wednesday Jun 29, 2026
f2e7716
chore: bump pro submodule (meeting intelligence + transcription work)
dishit-wednesday Jun 29, 2026
d050a81
fix(whisper): iOS streaming lifetime patch + PATCH-LIVE marker (whisp…
dishit-wednesday Jun 29, 2026
0009c8b
feat(rag): recordings knowledge base - chunk metadata, indexText, see…
dishit-wednesday Jun 29, 2026
ec76b64
perf(chat): keep tool/date prompt prefix cacheable, fixing TTFT
dishit-wednesday Jun 29, 2026
2687940
feat(whisper): CoreML asset guard, force English for .en models, tdrz…
dishit-wednesday Jun 29, 2026
e928e0b
chore: on-device memory-snapshot logging util
dishit-wednesday Jun 29, 2026
f121648
perf(summary): disable model thinking + cap map chunks so summaries s…
dishit-wednesday Jun 29, 2026
edf8325
feat(whisper): log memory footprint around model load
dishit-wednesday Jun 29, 2026
bb143cb
feat(nav): rename Memory tab to Recorder
dishit-wednesday Jun 29, 2026
ff0c194
test(whisper): assert loadModel called with options arg
dishit-wednesday Jun 29, 2026
cc7c0a4
polish(recorder): redesign the Recorder paywall
dishit-wednesday Jun 29, 2026
8f04e4e
chore: bump pro submodule (recorder + meeting intelligence + UI polish)
dishit-wednesday Jun 29, 2026
1cb80c3
test: fix stale assertions + add CoreML/RAG metadata coverage
dishit-wednesday Jul 1, 2026
f31a935
fix(whisper): free VAD PCM buffer to stop long-file OOM
dishit-wednesday Jul 1, 2026
2c2ab81
feat(pro): temporary reset-license testing controls
dishit-wednesday Jul 1, 2026
acab070
chore: bump pro submodule (recorder UI polish + cleanup classifier)
dishit-wednesday Jul 1, 2026
e94d86a
fix(chat): extract ChatInput action button to satisfy max-lines
dishit-wednesday Jul 1, 2026
bfd6c40
chore: point pro submodule at canonical off-grid-ai/mobile-pro
dishit-wednesday Jul 1, 2026
9ec2cfc
refactor(whisper): extract buildTranscribeOpts + split model catalogue
dishit-wednesday Jul 1, 2026
8c93f1f
test(locket): split-planning unit tests + whisper.rn VAD types
dishit-wednesday Jul 6, 2026
e8b7ba6
chore(locket): bump pro submodule (compression + recovery-hydration f…
dishit-wednesday Jul 6, 2026
cb979e3
feat: optional summary prompt overrides (systemPrompt/combinePrompt)
dishit-wednesday Jul 7, 2026
cf2752e
chore(deps): add @notifee/react-native for local scheduled notifications
dishit-wednesday Jul 7, 2026
5c6bc85
chore(locket): bump pro submodule to feat/locket-enhance (v2 enhancem…
dishit-wednesday Jul 7, 2026
aef5ee5
test(locket): review-fix coverage + bump pro submodule
dishit-wednesday Jul 7, 2026
3dd609a
test(locket): kill-safety recovery coverage + bump pro submodule
dishit-wednesday Jul 7, 2026
e2111eb
feat: multi-backend summary dispatch (LiteRT/remote/local)
dishit-wednesday Jul 7, 2026
f3e4ac4
chore: bump pro submodule (multi-backend summaries, meeting reminders…
dishit-wednesday Jul 7, 2026
4526e3d
feat: home.recorder slot + render RecorderHomeCard on Home
dishit-wednesday Jul 7, 2026
a3e2ad5
feat: replace recorder bottom tab with Settings tab
dishit-wednesday Jul 7, 2026
20a3583
chore: bump pro submodule (locket single-screen detail, auto-prune, h…
dishit-wednesday Jul 7, 2026
85c5103
chore(ios): onnxruntime-objc modular headers + bump pro (VAD-gated re…
dishit-wednesday Jul 8, 2026
34c009b
feat: home header cleanup + onnxruntime VAD pod
dishit-wednesday Jul 8, 2026
5b2fa29
chore: bump pro submodule (locket Today/Days/search, batch transcribe…
dishit-wednesday Jul 8, 2026
e412a39
chore: bump pro submodule (VAD gate tightening, VAD comparison tool, …
dishit-wednesday Jul 8, 2026
dc7afd1
chore: bump pro submodule (VAD inference-failure crash guard)
dishit-wednesday Jul 9, 2026
7fa7b22
Merge pull request #502 from off-grid-ai/feat/vad-gated-recorder
dishit-wednesday Jul 9, 2026
60e77cd
Merge pull request #469 from off-grid-ai/feat/locket-pro-enhance
dishit-wednesday Jul 9, 2026
a944066
Merge dev into feat/locket-pro (catch-up reconciliation, 451 commits …
dishit-wednesday Jul 9, 2026
1140016
chore: bump pro submodule (speech-only auto-trim tuning + 0:00 guard …
dishit-wednesday Jul 9, 2026
741e1ab
chore: bump pro submodule (remove diagnostic Check-speech buttons)
dishit-wednesday Jul 9, 2026
cf458ce
chore: bump pro submodule to feat/locket tip (trim + button removal +…
dishit-wednesday Jul 9, 2026
decceca
chore: gitignore dev insights sim harness + scratch screenshots
dishit-wednesday Jul 10, 2026
abe93b1
feat(summarizer/llm): grammar, repeat_penalty, streaming + abort for …
dishit-wednesday Jul 10, 2026
4e7ff47
feat(dev): GBNF grammar test harness across chat, tools, and LiteRT
dishit-wednesday Jul 10, 2026
2ad817c
feat(whisper): iOS CoreML encoder for ANE-accelerated encode
dishit-wednesday Jul 10, 2026
77185b8
chore(assets): update ggml-hexagon HTP libs + add silero VAD model
dishit-wednesday Jul 10, 2026
fb04c84
fix(llm): remove iOS Metal <4GB offload cap
dishit-wednesday Jul 10, 2026
92b9caf
chore: bump pro submodule (locket insights pipeline + hub/detail scre…
dishit-wednesday Jul 10, 2026
2a909a4
feat(summarizer): dynamic context sizing + token streaming + abort
dishit-wednesday Jul 12, 2026
9865b30
feat(litert): expose getContextTokens for backend-aware chunk sizing
dishit-wednesday Jul 12, 2026
0cfecfe
fix(whisper): fall back to another on-disk model when the active one …
dishit-wednesday Jul 12, 2026
1ed573d
chore(pro): bump submodule (DayStrip, day-scoped Insights, card schem…
dishit-wednesday Jul 12, 2026
52d4b86
chore: bump pro submodule to pushed feat/continuous-smart-recorder ti…
dishit-wednesday Jul 12, 2026
1fe0940
test(whisper): update store/service tests for model fallback + CoreML…
dishit-wednesday Jul 12, 2026
62754cd
fix(llm): reset streaming accumulators before ungrammared grammar-retry
dishit-wednesday Jul 12, 2026
4c73b24
chore(pro): bump submodule to review fixes (scheduler teardown + auto…
dishit-wednesday Jul 12, 2026
ce30b39
fix(whisper): drop deleted model from presentModelIds on active-model…
dishit-wednesday Jul 12, 2026
0feedca
chore(pro): bump submodule to off-thread checkpoint scan fix (audio-d…
dishit-wednesday Jul 12, 2026
f7d524f
chore(pro): bump submodule (permissions + player trim + wip) & add ha…
dishit-wednesday Jul 13, 2026
bac57f4
fix(whisper): pass maxContext=0 to stop repetition/hallucination on n…
dishit-wednesday Jul 13, 2026
156b819
Merge remote-tracking branch 'upstream/feat/locket-pro' into feat/vad…
dishit-wednesday Jul 13, 2026
31b9f34
feat(ui): add a global Toast component and mount it in App
dishit-wednesday Jul 13, 2026
15cdbde
feat(insights): route summaries to a remote provider when one is active
dishit-wednesday Jul 13, 2026
ff9a273
feat(whisper): accept an optional vocabulary hint as the initial prompt
dishit-wednesday Jul 13, 2026
244c7dc
fix(litert): drain fbjni finalizer at conversation teardown to avoid …
dishit-wednesday Jul 13, 2026
1a5d48f
chore(pro): bump submodule (insights transcribe, vocabulary, settings…
dishit-wednesday Jul 13, 2026
13b563e
feat(models): memory-aware text-model selector
dishit-wednesday Jul 13, 2026
6a24d79
feat(models): text-only load option to skip the vision mmproj clip
dishit-wednesday Jul 13, 2026
48a958e
chore(pro): bump submodule (memory-aware auto-load + battery-exemptio…
dishit-wednesday Jul 13, 2026
f9ef008
chore(pro): bump submodule to locket unified feed (+ residency helper…
dishit-wednesday Jul 14, 2026
a43a71b
chore: remove accidentally-committed scratch files (android/temp.txt,…
dishit-wednesday Jul 14, 2026
aa94f68
fix(summarizer): abort() also stops the active remote provider
dishit-wednesday Jul 14, 2026
18cd101
fix(litert): strip temporary DIAG-UAF logging from the conversation h…
dishit-wednesday Jul 14, 2026
98681a2
chore(pro): bump submodule to review fixes (brief-card tidy, battery …
dishit-wednesday Jul 14, 2026
e8fc497
fix(whisper): keep non-ASCII transcripts + guard overlapping file tra…
dishit-wednesday Jul 14, 2026
6b285fa
feat(services): export modelResidencyManager for a single-source RAM …
dishit-wednesday Jul 14, 2026
3133d64
chore(docs): remove pro/locket planning + handoff docs from open-core
dishit-wednesday Jul 15, 2026
79f0ee8
Merge pull request #532 from off-grid-ai/feat/vad-gated-recorder
dishit-wednesday Jul 15, 2026
bbd65ea
Merge offgrid/dev into locket-pro (core catch-up)
dishit-wednesday Jul 15, 2026
617f548
chore(locket): bump pro submodule to caught-up pin + mock its native …
dishit-wednesday Jul 15, 2026
d79d489
chore(locket): repoint pro pin to the LATEST recorder branch catch-up
dishit-wednesday Jul 15, 2026
3b76a7c
fix(whisper): restore fallback-recorder cancel in forceReset (dropped…
dishit-wednesday Jul 16, 2026
41765af
fix(whisper): scope forceReset's recorder-cancel to the realtime sess…
dishit-wednesday Jul 16, 2026
1bdc6c3
Merge pull request #559 from off-grid-ai/chore/locket-catchup-dev
dishit-wednesday Jul 16, 2026
f0c05eb
Merge remote-tracking branch 'offgrid/dev' into chore/locket-catchup-…
dishit-wednesday Jul 16, 2026
14b343d
fix(core): re-export stripMarkdownForSpeech for the pro audio submodule
dishit-wednesday Jul 16, 2026
197c210
chore(locket): bump pro pin to dev's current pro (ff0d8742) — audio s…
dishit-wednesday Jul 16, 2026
f842650
chore(core): drop the vestigial stripMarkdownForSpeech export (audit …
dishit-wednesday Jul 16, 2026
8888a46
Merge pull request #562 from off-grid-ai/chore/locket-catchup-dev-2
dishit-wednesday Jul 16, 2026
16e59b8
chore(locket): point pro pin at feat/locket (the → main base) after b…
dishit-wednesday Jul 16, 2026
1d00a31
fix(core): re-export AlertButton + Chunk for pro; bump pro pin to fea…
dishit-wednesday Jul 16, 2026
8453202
Merge pull request #566 from off-grid-ai/chore/locket-catchup-dev-2
dishit-wednesday Jul 16, 2026
ccda30f
chore(recorder): drop the Silero VAD model from public core (now bund…
dishit-wednesday Jul 16, 2026
89843be
Merge pull request #567 from off-grid-ai/chore/locket-catchup-dev-2
dishit-wednesday Jul 16, 2026
fa0df51
fix(whisper): clear file-transcription stop handle in forceReset
dishit-wednesday Jul 16, 2026
9f0d693
Merge pull request #570 from off-grid-ai/feat/rps-core-p0
dishit-wednesday Jul 17, 2026
b300b45
chore(locket): point pro submodule at merged feat/locket (RPS stack 6…
dishit-wednesday Jul 17, 2026
c1afb95
chore(ios): sync Podfile.lock with merged pro (add OffgridPro pod)
dishit-wednesday Jul 17, 2026
e749eff
fix(ios): stamp Podfile.lock with pinned CocoaPods 1.15.2 (match CI)
dishit-wednesday Jul 17, 2026
42013e6
chore(ci): TEMP diagnostic - print git status before uat.sh (find dir…
dishit-wednesday Jul 17, 2026
39b321b
fix(ci): ignore .bundle/config in uat.sh clean-tree check; drop diagn…
dishit-wednesday Jul 17, 2026
bd554df
fix(ci): let CI regenerate ios/Podfile.lock so it matches Manifest.lock
dishit-wednesday Jul 17, 2026
f49f323
test(locket): parameterize cross-screen sync over states + running-st…
dishit-wednesday Jul 17, 2026
d032056
chore(beta): 0.0.104-beta.1 (build 1784286784) [skip ci]
Jul 17, 2026
a1d1a33
test(locket): cross-screen sync + player smoke tests; scope dynamic-i…
dishit-wednesday Jul 17, 2026
2511047
chore(recorder): remove dead MemoryTabScreen (unwired)
dishit-wednesday Jul 17, 2026
d22ec8f
test(locket): rendered guards for queue-aware CTAs + green card
dishit-wednesday Jul 17, 2026
d4147b0
chore(pro): bump pointer to onboarding + queue-aware CTAs
dishit-wednesday Jul 17, 2026
edda8f9
Merge pull request #579 from off-grid-ai/fix/locket-recording-ctas
dishit-wednesday Jul 17, 2026
4743e63
Merge pull request #578 from off-grid-ai/refactor/locket-screens
dishit-wednesday Jul 17, 2026
ae962f8
feat(whisper): honest CoreML toggle + usage logs + hardened encoder d…
dishit-wednesday Jul 19, 2026
686096c
feat(whisper): per-model Neural Engine (ANE/CPU) status on the models…
dishit-wednesday Jul 19, 2026
a7a86c7
test(locket): rendered test for damaged-clip Repair banner + service
dishit-wednesday Jul 19, 2026
687cb7b
test(locket): guard speaker-turn rendering on the detail; drop dead p…
dishit-wednesday Jul 19, 2026
0b32074
feat(insights): expose hasRemoteBackend for the recorder model guard
dishit-wednesday Jul 20, 2026
2b99fe0
fix(ios): patch react-native-calendar-events for iOS 17+ Full Access
dishit-wednesday Jul 21, 2026
b90607a
feat(whisper): enforce GPU eligibility at the load site; drop flash-attn
dishit-wednesday Jul 21, 2026
85033b6
Merge pull request #581 from off-grid-ai/feat/core-round-1
dishit-wednesday Jul 22, 2026
179bbdc
fix(kb): render text-indexed docs in the preview instead of 'file not…
dishit-wednesday Jul 22, 2026
7c33831
feat(chat): scope a conversation to one recording via bounded per-tur…
dishit-wednesday Jul 23, 2026
bff2090
chore(llm): stop teeing llama.cpp native logs into the app log
dishit-wednesday Jul 23, 2026
7c71cd2
test(locket): rendered guards for pipeline %, setup focus/vocab, repa…
dishit-wednesday Jul 23, 2026
b720eb3
chore: bump pro submodule to the locket feed/detail + setup/settings …
dishit-wednesday Jul 23, 2026
b810055
chore(whisper): drop the experimental small.en-tdrz speaker-turns model
dishit-wednesday Jul 23, 2026
49a953d
fix(whisper): scroll the transcription model picker so all models are…
dishit-wednesday Jul 23, 2026
4d70880
chore: bump pro submodule to round-3 locket edits
dishit-wednesday Jul 24, 2026
6c0b44e
docs: update commit coordination
dishit-wednesday Jul 26, 2026
27ffa55
docs: record final commit plan for the transcript-visibility fix
dishit-wednesday Jul 26, 2026
02a15d5
docs: update commit coordination
dishit-wednesday Jul 26, 2026
b8ba620
docs: note branch rename and commit-1 status in coordination
dishit-wednesday Jul 26, 2026
2bbb251
docs: mark locket commits done and warn on shared-index commits
dishit-wednesday Jul 26, 2026
fdada9d
docs: log that 75c2ecc absorbed Agent A's copy-transcript hunk
dishit-wednesday Jul 26, 2026
8258aba
docs: record the coordinator sweep of remaining locket commits
dishit-wednesday Jul 26, 2026
c5a55ba
chore: bump pro submodule to round-4 locket edits
dishit-wednesday Jul 26, 2026
957cf0e
chore(deps): add cactus-react-native + react-native-nitro-modules
dishit-wednesday Jul 27, 2026
134e938
chore(android): arm64-only build (cactus ships arm64-only libs)
dishit-wednesday Jul 27, 2026
5536d71
chore: exclude poc-archive from tsc + gitignore bundled cactus models
dishit-wednesday Jul 27, 2026
cbd595d
chore: bump pro submodule to recorder-v2 STT engine commits
dishit-wednesday Jul 27, 2026
d2bb0af
chore: gitignore built app binaries
dishit-wednesday Jul 31, 2026
58b486a
chore(android): arm64-only packaging, and record why onnxruntime must…
dishit-wednesday Jul 31, 2026
922d2bf
feat(android): a releaseLocket build that installs alongside the norm…
dishit-wednesday Jul 31, 2026
5274f16
chore(android): bump to 0.0.104
dishit-wednesday Jul 31, 2026
a4b1a34
feat(ios): Live Activity and a recorder tile, as a widget extension
dishit-wednesday Jul 31, 2026
aeabfdb
chore(ios): make iOS ask for Local Network permission, and a JS refre…
dishit-wednesday Jul 31, 2026
7a79bc0
chore(ios): pod lockfile for cactus, nitro modules and ZIPFoundation
dishit-wednesday Jul 31, 2026
093badb
perf: O(1) debug-log buffer
dishit-wednesday Jul 31, 2026
6619399
test(infra): stub nitro/cactus so pro suites load, and fake the VAD/a…
dishit-wednesday Jul 31, 2026
aa778a3
feat: an STT model registry, so a pro model can be app-managed
dishit-wednesday Jul 31, 2026
bfd190c
feat: remoteOnly summarization - never fall back to on-device
dishit-wednesday Jul 31, 2026
8738b4f
fix: do not evict whisper out from under a running transcription
dishit-wednesday Jul 31, 2026
769f35e
feat: subscribable realtime transcription boundary
dishit-wednesday Jul 31, 2026
99f15a2
chore: agent definitions for the recorder work
dishit-wednesday Jul 31, 2026
9c20003
chore: bump pro submodule to the recorder-v2 work
dishit-wednesday Jul 31, 2026
dad48c8
docs(test): correct what happens to tests placed under pro/
dishit-wednesday Jul 31, 2026
029acc6
chore: bump pro submodule for the pro test runner
dishit-wednesday Jul 31, 2026
857b5b1
chore: bump pro submodule for the relocated pro tests
dishit-wednesday Jul 31, 2026
d4fe0d6
test(infra): let the locket STORES' dynamic imports run under jest too
dishit-wednesday Jul 31, 2026
7ed3148
chore: bump pro submodule for the un-parked rendered test
dishit-wednesday Jul 31, 2026
952970e
chore: bump pro submodule - pro's suite is green
dishit-wednesday Jul 31, 2026
47aae96
test(locket): repair the feed tests the unified-feed rework broke
dishit-wednesday Jul 31, 2026
caba441
chore: bump pro submodule - recorder tests moved out of core
dishit-wednesday Jul 31, 2026
c7edfe1
test: remove the locket suites that moved into pro
dishit-wednesday Aug 1, 2026
376a4c2
chore: bump pro submodule - the last locket tests moved out of core
dishit-wednesday Aug 1, 2026
51e3c60
Merge pull request #614 from off-grid-ai/fix/locket-round-3
dishit-wednesday Aug 1, 2026
d5c9f8f
Merge pull request #598 from off-grid-ai/fix/locket-round-2
dishit-wednesday Aug 1, 2026
6eb36e9
chore: bump pro submodule after the voiceprint fixture was purged
dishit-wednesday Aug 1, 2026
ef6ea21
Merge feat/locket-pro into feat/recorder-v2
dishit-wednesday Aug 1, 2026
d23ea0b
fix(download): key the staging file to the url so retries resume
dishit-wednesday Aug 3, 2026
b5e6f30
fix(downloads): keep aggregated model parts out of the manager list
dishit-wednesday Aug 3, 2026
2b1f822
feat(theme): a reading-text typography token
dishit-wednesday Aug 3, 2026
32aaf0b
chore(hooks): typecheck the pro submodule on pre-push
dishit-wednesday Aug 3, 2026
fb8e2a3
fix(rag): rename a document's name without re-embedding
dishit-wednesday Aug 3, 2026
5e431fe
fix(network): say WHY a server probe failed
dishit-wednesday Aug 3, 2026
15cfd96
fix(android): drop the redundant domain-config that forced strict TLS
dishit-wednesday Aug 3, 2026
783f446
test(models): declare the transcription veto in the residency cases
dishit-wednesday Aug 3, 2026
82f96e3
fix(litert): honor the requested token budget instead of re-clamping …
dishit-wednesday Aug 3, 2026
cceee7f
chore: bump pro to the recorder round-6 work
dishit-wednesday Aug 3, 2026
5d768f8
chore: bump pro for the speech-gating wiring
dishit-wednesday Aug 3, 2026
65ac615
Merge pull request #622 from off-grid-ai/feat/recorder-v3
dishit-wednesday Aug 3, 2026
40ab6e0
Merge pull request #619 from off-grid-ai/feat/recorder-v2
dishit-wednesday Aug 3, 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
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "pro"]
path = pro
url = https://github.com/wednesday-solutions/offgrid-pro.git
url = https://github.com/off-grid-ai/mobile-pro.git
8 changes: 4 additions & 4 deletions __tests__/rntl/navigation/AppNavigator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ describe('AppNavigator', () => {
expect(getAllByText('Chats').length).toBeGreaterThanOrEqual(1);
expect(getAllByText('Projects').length).toBeGreaterThanOrEqual(1);
expect(getAllByText('Models').length).toBeGreaterThanOrEqual(1);
expect(getAllByText('Settings').length).toBeGreaterThanOrEqual(1);
expect(getAllByText('Recorder').length).toBeGreaterThanOrEqual(1);
});

it('renders all tab buttons with testIDs', () => {
Expand All @@ -196,7 +196,7 @@ describe('AppNavigator', () => {
expect(getByTestId('chats-tab')).toBeTruthy();
expect(getByTestId('projects-tab')).toBeTruthy();
expect(getByTestId('models-tab')).toBeTruthy();
expect(getByTestId('settings-tab')).toBeTruthy();
expect(getByTestId('recorder-tab')).toBeTruthy();
});
});

Expand Down Expand Up @@ -279,14 +279,14 @@ describe('AppNavigator', () => {
expect(getAllByText('Chats').length).toBeGreaterThanOrEqual(1);
expect(getAllByText('Projects').length).toBeGreaterThanOrEqual(1);
expect(getAllByText('Models').length).toBeGreaterThanOrEqual(1);
expect(getAllByText('Settings').length).toBeGreaterThanOrEqual(1);
expect(getAllByText('Recorder').length).toBeGreaterThanOrEqual(1);

// All tab buttons should be pressable
expect(getByTestId('home-tab')).toBeTruthy();
expect(getByTestId('chats-tab')).toBeTruthy();
expect(getByTestId('projects-tab')).toBeTruthy();
expect(getByTestId('models-tab')).toBeTruthy();
expect(getByTestId('settings-tab')).toBeTruthy();
expect(getByTestId('recorder-tab')).toBeTruthy();
});
});
});
4 changes: 2 additions & 2 deletions __tests__/rntl/onboarding/HomeScreenSpotlight.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,15 +299,15 @@ describe('HomeScreen Spotlight Integration', () => {
// Flow 5: Explore Settings
// ========================================================================
describe('Flow 5: exploredSettings', () => {
it('queues pending spotlight 6, navigates to SettingsTab, fires goTo(5)', () => {
it('queues pending spotlight 6, navigates to Settings, fires goTo(5)', () => {
const { getByTestId } = renderHomeScreen();

act(() => {
fireEvent.press(getByTestId('step-exploredSettings'));
});

expect(peekPendingSpotlight()).toBe(6);
expect(mockNavigate).toHaveBeenCalledWith('SettingsTab');
expect(mockNavigate).toHaveBeenCalledWith('Settings');

act(() => { jest.advanceTimersByTime(800); });
expect(mockGoTo).toHaveBeenCalledWith(5);
Expand Down
4 changes: 2 additions & 2 deletions __tests__/unit/onboarding/handleStepPress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,9 @@ describe('handleStepPress', () => {
expect(peekPendingSpotlight()).toBe(6);
});

it('navigates to SettingsTab', () => {
it('navigates to Settings', () => {
simulateHandleStepPress('exploredSettings', callbacks());
expect(navigate).toHaveBeenCalledWith('SettingsTab');
expect(navigate).toHaveBeenCalledWith('Settings');
});

it('fires goTo(5) after delay', () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/unit/onboarding/onboardingFlows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('Onboarding Flows', () => {
downloadedModel: 'ModelsTab',
loadedModel: 'HomeTab',
sentMessage: 'ChatsTab',
exploredSettings: 'SettingsTab',
exploredSettings: 'Settings',
createdProject: 'ProjectsTab',
triedImageGen: 'ModelsTab',
});
Expand Down
22 changes: 16 additions & 6 deletions __tests__/unit/services/generationToolLoop.branches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,21 +354,31 @@ describe('runToolLoop — LiteRT loop branches', () => {
describe('runToolLoop — precise date/time context for calendar tools', () => {
beforeEach(resetMocks);

it('augments the system prompt with a precise timestamp when a calendar tool is enabled', async () => {
it('appends a precise time-of-day note to the latest user message when a calendar tool is enabled', async () => {
mockedGenerateResponseWithTools.mockResolvedValue({ fullResponse: 'ok', toolCalls: [] });

const ctx = createContext({
enabledToolIds: ['create_calendar_event'],
messages: [makeMessage({ role: 'system', content: 'You are helpful.' })],
messages: [
makeMessage({ role: 'system', content: 'You are helpful.' }),
makeMessage({ role: 'user', content: 'Schedule a sync' }),
],
});
await runToolLoop(ctx);

const sentMessages = mockedGenerateResponseWithTools.mock.calls[0][0];
// The STABLE date stays in the system prefix so the system+tools prefix
// remains cacheable turn-to-turn.
const sysContent = sentMessages.find((m: Message) => m.role === 'system')!.content as string;
// precise=true produces the YYYY-MM-DDTHH:MM:SS phrasing (not the date-only variant)
expect(sysContent).toContain('current date and time is');
expect(sysContent).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
expect(sysContent).toContain('in half an hour');
expect(sysContent).toContain('The current date is');
// The EXACT time-of-day (needed to resolve "in half an hour") is appended to
// the latest user message instead, keeping the big prefix cacheable.
const userContent = [...sentMessages]
.reverse()
.find((m: Message) => m.role === 'user')!.content as string;
expect(userContent).toContain('Current local date and time');
expect(userContent).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
expect(userContent).toContain('in half an hour');
});

it('uses the date-only context when no time-sensitive tool is enabled', async () => {
Expand Down
26 changes: 21 additions & 5 deletions __tests__/unit/services/rag/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ describe('RagDatabase', () => {
it('opens the database and creates tables', async () => {
await ragDatabase.ensureReady();
expect(open).toHaveBeenCalledWith({ name: 'rag.db' });
// rag_documents, rag_chunks, rag_embeddings = 3 tables
expect(mockExecuteSync).toHaveBeenCalledTimes(3);
// rag_documents, rag_chunks, ALTER rag_chunks (metadata migration), rag_embeddings
expect(mockExecuteSync).toHaveBeenCalledTimes(4);
expect(mockExecuteSync.mock.calls[0][0]).toContain('rag_documents');
expect(mockExecuteSync.mock.calls[1][0]).toContain('rag_chunks');
expect(mockExecuteSync.mock.calls[2][0]).toContain('rag_embeddings');
expect(mockExecuteSync.mock.calls[2][0]).toContain('ALTER TABLE rag_chunks');
expect(mockExecuteSync.mock.calls[2][0]).toContain('metadata');
expect(mockExecuteSync.mock.calls[3][0]).toContain('rag_embeddings');
});

it('does not re-initialize on second call', async () => {
Expand Down Expand Up @@ -86,8 +88,22 @@ describe('RagDatabase', () => {
(c: any[]) => typeof c[0] === 'string' && c[0].includes('INSERT INTO rag_chunks')
);
expect(chunkInserts).toHaveLength(2);
expect(chunkInserts[0][1]).toEqual(['chunk one', 42, 0]);
expect(chunkInserts[1][1]).toEqual(['chunk two', 42, 1]);
// 4th bind is metadata (null when the chunk carries none).
expect(chunkInserts[0][1]).toEqual(['chunk one', 42, 0, null]);
expect(chunkInserts[1][1]).toEqual(['chunk two', 42, 1, null]);
});

it('serializes chunk metadata to a JSON string on the way into the DB', async () => {
await ragDatabase.ensureReady();
mockExecuteSync.mockReturnValue({ insertId: 7, rowsAffected: 1, rows: [] });

const metadata = { recordingId: 'rec-1', startMs: 100, eventTitle: 'Standup' };
ragDatabase.insertChunks(42, [{ content: 'has meta', position: 0, metadata } as any]);

const chunkInsert = mockExecuteSync.mock.calls.find(
(c: any[]) => typeof c[0] === 'string' && c[0].includes('INSERT INTO rag_chunks'),
);
expect(chunkInsert![1]).toEqual(['has meta', 42, 0, JSON.stringify(metadata)]);
});
});

Expand Down
5 changes: 5 additions & 0 deletions __tests__/unit/services/whisperService.branches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ describe('WhisperService — branch coverage', () => {
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
// clearAllMocks() clears call history but NOT the mockResolvedValueOnce
// queue, so an unconsumed once from a prior test would shift these
// ordered exists()/stat() chains. Reset them for true per-test isolation.
mockedRNFS.exists.mockReset();
mockedRNFS.stat.mockReset();
resetService();
mockedBDS.isAvailable.mockReturnValue(true);
mockedBDS.cancelDownload.mockResolvedValue(undefined as any);
Expand Down
41 changes: 39 additions & 2 deletions __tests__/unit/services/whisperService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,47 @@ describe('WhisperService', () => {

await whisperService.loadModel('/path/to/model.bin');

expect(initWhisper).toHaveBeenCalledWith({ filePath: '/path/to/model.bin' });
expect(initWhisper).toHaveBeenCalledWith({
filePath: '/path/to/model.bin',
useGpu: false,
useFlashAttn: false,
useCoreMLIos: false,
});
expect(whisperService.isModelLoaded()).toBe(true);
expect(whisperService.getLoadedModelPath()).toBe('/path/to/model.bin');
});

it('falls back to CPU when CoreML requested but the encoder asset is missing', async () => {
// Valid model file, but the ggml-<model>-encoder.mlmodelc bundle is absent.
// Enabling CoreML without it makes whisper.rn crash at 0% on some iOS devices,
// so the guard must silently downgrade to CPU (useCoreMLIos: false).
mockedRNFS.stat.mockResolvedValue({ size: 75 * 1024 * 1024, isFile: () => true } as any);
mockedRNFS.exists.mockImplementation(async (p: string) =>
!p.endsWith('-encoder.mlmodelc'),
);
const mockContext = { id: 'ctx', release: jest.fn(), transcribeRealtime: jest.fn(), transcribe: jest.fn() };
mockedInitWhisper.mockResolvedValue(mockContext as any);

await whisperService.loadModel('/path/to/model.bin', { useCoreML: true });

expect(initWhisper).toHaveBeenCalledWith(
expect.objectContaining({ filePath: '/path/to/model.bin', useCoreMLIos: false }),
);
});

it('enables CoreML when the encoder asset is present', async () => {
mockedRNFS.stat.mockResolvedValue({ size: 75 * 1024 * 1024, isFile: () => true } as any);
mockedRNFS.exists.mockResolvedValue(true); // both the .bin and the .mlmodelc exist
const mockContext = { id: 'ctx', release: jest.fn(), transcribeRealtime: jest.fn(), transcribe: jest.fn() };
mockedInitWhisper.mockResolvedValue(mockContext as any);

await whisperService.loadModel('/path/to/model.bin', { useCoreML: true, useGpu: true, useFlashAttn: true });

expect(initWhisper).toHaveBeenCalledWith(
expect.objectContaining({ useCoreMLIos: true, useGpu: true, useFlashAttn: true }),
);
});

it('unloads different model before loading new one', async () => {
mockValidModelFile();
const mockContext1 = {
Expand Down Expand Up @@ -761,7 +797,8 @@ describe('WhisperService', () => {
})),
};
mockedInitWhisper.mockResolvedValueOnce(mockContext as any);
await whisperService.loadModel('/path/model.bin');
// English-only model (.en.bin) so transcribeFile forces language 'en'.
await whisperService.loadModel('/path/model.en.bin');

const result = await whisperService.transcribeFile('/audio.wav');

Expand Down
2 changes: 2 additions & 0 deletions __tests__/unit/stores/whisperStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ describe('whisperStore', () => {
expect(mockWhisperService.getModelPath).toHaveBeenCalledWith('ggml-tiny');
expect(mockWhisperService.loadModel).toHaveBeenCalledWith(
'/models/ggml-tiny',
undefined,
);
expect(getState().isModelLoaded).toBe(true);
});
Expand Down Expand Up @@ -252,6 +253,7 @@ describe('whisperStore', () => {
expect(mockWhisperService.getModelPath).toHaveBeenCalledWith('ggml-tiny');
expect(mockWhisperService.loadModel).toHaveBeenCalledWith(
'/models/ggml-tiny',
undefined,
);
expect(getState().isModelLoaded).toBe(true);
expect(getState().isModelLoading).toBe(false);
Expand Down
68 changes: 68 additions & 0 deletions __tests__/unit/utils/memorySnapshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* memorySnapshot unit tests
*
* logMemory() is a diagnostics probe used around whisper model load and each
* transcribe chunk to capture the app's footprint. On iOS it surfaces whether
* an apparent transcription "crash" was actually a jetsam low-memory kill.
*
* Guarantees under test:
* - formats used/total in MB and a percentage
* - never throws (a failing probe must not break the path it observes)
* - no divide-by-zero when total memory is reported as 0
*/

import DeviceInfo from 'react-native-device-info';
import logger from '../../../src/utils/logger';
import { logMemory } from '../../../src/utils/memorySnapshot';

const mockedDeviceInfo = DeviceInfo as jest.Mocked<typeof DeviceInfo>;

describe('logMemory', () => {
let logSpy: jest.SpyInstance;
let warnSpy: jest.SpyInstance;

beforeEach(() => {
jest.clearAllMocks();
logSpy = jest.spyOn(logger, 'log').mockImplementation(() => {});
warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
});

afterEach(() => {
logSpy.mockRestore();
warnSpy.mockRestore();
});

it('logs used/total in MB with a percentage, tagged with the call site', async () => {
mockedDeviceInfo.getUsedMemory.mockResolvedValue(1.4 * 1024 * 1024 * 1024);
mockedDeviceInfo.getTotalMemory.mockResolvedValue(4 * 1024 * 1024 * 1024);

await logMemory('whisper:beforeLoad');

expect(logSpy).toHaveBeenCalledTimes(1);
const msg = logSpy.mock.calls[0][0] as string;
expect(msg).toContain('[mem] whisper:beforeLoad');
expect(msg).toContain('used=1434MB');
expect(msg).toContain('total=4096MB');
expect(msg).toContain('(35%)');
});

it('never throws and warns when the probe fails', async () => {
mockedDeviceInfo.getUsedMemory.mockRejectedValue(new Error('boom'));
mockedDeviceInfo.getTotalMemory.mockResolvedValue(4 * 1024 * 1024 * 1024);

await expect(logMemory('transcribe:chunk@0s')).resolves.toBeUndefined();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('snapshot failed'));
expect(logSpy).not.toHaveBeenCalled();
});

it('does not divide by zero when total memory is unavailable', async () => {
mockedDeviceInfo.getUsedMemory.mockResolvedValue(100 * 1024 * 1024);
mockedDeviceInfo.getTotalMemory.mockResolvedValue(0);

await logMemory('zero');

const msg = logSpy.mock.calls[0][0] as string;
expect(msg).toContain('total=0MB');
expect(msg).toContain('(0%)');
});
});
24 changes: 14 additions & 10 deletions ios/OffgridMobile/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand All @@ -29,19 +33,15 @@
</array>
</dict>
</array>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>twitter</string>
<string>x</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
Expand All @@ -53,6 +53,10 @@
<string>_ollama._tcp</string>
<string>_lmstudio._tcp</string>
</array>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Used to read and create calendar events on your request.</string>
<key>NSCalendarsUsageDescription</key>
<string>Used to read and create calendar events on your request.</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to your camera to take photos and attach them to conversations.</string>
<key>NSFaceIDUsageDescription</key>
Expand All @@ -65,10 +69,6 @@
<string>This app needs permission to save generated images to your photo library.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to attach images to conversations.</string>
<key>NSCalendarsUsageDescription</key>
<string>Used to read and create calendar events on your request.</string>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Used to read and create calendar events on your request.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses on-device speech recognition to transcribe voice input.</string>
<key>RCTNewArchEnabled</key>
Expand All @@ -95,6 +95,10 @@
<string>SimpleLineIcons.ttf</string>
<string>FontAwesome6_Brands.ttf</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
Expand Down
Loading