Skip to content
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
527 commits
Select commit Hold shift + click to select a range
ce679cd
docs(checklist): mark T001/T022/T023 automated (✅) with their red tes…
alichherawalla Jul 11, 2026
b7b2edb
docs(checklist): T024 honest coverage split — budgetRedflow (arithmet…
alichherawalla Jul 11, 2026
0df8306
test(harness): add driveable whisper.rn STT fake (realtime + file) to…
alichherawalla Jul 11, 2026
9ee2eb3
test(harness): STT chat-mode support — whisper option, setupWhisperMo…
alichherawalla Jul 11, 2026
9bf9ca0
test(T080/T075): red — chat-mode STT never transcribes to text (DEV-B…
alichherawalla Jul 11, 2026
5f86b38
docs(checklist): mark T075/T080 automated (chatModeSttArchitecture red)
alichherawalla Jul 11, 2026
4b47f8f
docs(checklist): reconcile count to 52 automated (incl. T075/T080 STT)
alichherawalla Jul 11, 2026
c8f6c9f
docs(checklist): T031 marked n/a (device-stress observation, ignore p…
alichherawalla Jul 11, 2026
f0aae67
docs(checklist): T062 fixed (resend replays recordedTurnKind=image); …
alichherawalla Jul 11, 2026
5e4d539
test(T062): green guard — text-mode resend of 'draw a dog' re-draws (…
alichherawalla Jul 11, 2026
8137ef0
test(harness): reusable voice-mode/TTS harness + faithful executorch/…
alichherawalla Jul 12, 2026
cde4f24
test(T062): voice + enhancement resend re-draws — the exact DEV-B33 d…
alichherawalla Jul 12, 2026
4ec47c6
docs(checklist): T062 FIXED+GUARDED across text, voice, and voice+enh…
alichherawalla Jul 12, 2026
efb6d97
test(T071): RED — prompt enhancement sends enable_thinking=true (DEV-…
alichherawalla Jul 12, 2026
bd97d1b
test(T018): RED — litert CPU invoke error (Status 13) surfaces, no an…
alichherawalla Jul 12, 2026
84131ad
docs(checklist): T018 automated (litertCpuInvokeError red, DEV-B23)
alichherawalla Jul 12, 2026
17df5fa
test(T088): GREEN guard — voice-mode mic becomes STOP during generati…
alichherawalla Jul 12, 2026
1ebcbd9
docs(checklist): T088 automated (voice-mode stop-button green guard, …
alichherawalla Jul 12, 2026
c9e2bbc
test(T056): GREEN guard — generation error clears the spinner + shows…
alichherawalla Jul 12, 2026
c0adb3c
docs(checklist): T056 local error-clears-spinner green guard (DEV-B13…
alichherawalla Jul 12, 2026
98ac905
revert(T056): back out errorClearsSpinner — could not falsify it (unp…
alichherawalla Jul 12, 2026
95690b2
revert(T056): remove errorClearsSpinner test file (unproven green — s…
alichherawalla Jul 12, 2026
fde0f74
test(T056): PROVEN green guard — generation error clears the spinner …
alichherawalla Jul 12, 2026
75e00af
docs(checklist): T056 PROVEN green guard (DEV-B13 local, falsified)
alichherawalla Jul 12, 2026
f1c62f7
test(T056): RED — llama generation failure shows NO error + spinner s…
alichherawalla Jul 12, 2026
f4baabd
docs(checklist): T056 is a RED reproducing B13 on the llama path (was…
alichherawalla Jul 12, 2026
cf1526a
test(T062-llama): PROVEN guard — B33 resend re-draws on the LLAMA eng…
alichherawalla Jul 12, 2026
6dce189
test(T049): RED — remote (LM Studio) reasoning is dropped, not shown …
alichherawalla Jul 12, 2026
92a0a01
docs(checklist): T049 automated (remote reasoning dropped RED, DEV-B1…
alichherawalla Jul 12, 2026
d915c7f
test(T051): GREEN guard — remote Ollama reasoning RENDERS (contrast t…
alichherawalla Jul 12, 2026
6ef7d77
docs(checklist): T051 Ollama green guard; T050 folded into T049 (reas…
alichherawalla Jul 12, 2026
67e4171
test(T033): PROVEN green guard — <think> reasoning renders in the thi…
alichherawalla Jul 12, 2026
ada4620
docs(checklist): T033 automated (mid-stream thinking-block green guar…
alichherawalla Jul 12, 2026
c62e13f
test(N3): fix the vacuous noImageModelDrawSend guard — assert on the …
alichherawalla Jul 12, 2026
0881cd9
test(residencySwap): fix false green — assert residency ACCOUNTING, n…
alichherawalla Jul 12, 2026
bfe13d2
test(T023b): RED — Eject All leaves ALL sidecars resident, not just w…
alichherawalla Jul 12, 2026
2e4efa2
docs(checklist): add T023b — ejectAll leaves all sidecars (DEV-B1 gen…
alichherawalla Jul 12, 2026
b61abf4
feat(residency): model selector shows the loaded model + its RAM (rem…
alichherawalla Jul 12, 2026
5ee8af7
feat(residency): image tab Currently Loaded also shows RAM consumed (…
alichherawalla Jul 12, 2026
08dd527
fix(test): add formatModelRam + estimateImageModelRam to ModelSelecto…
alichherawalla Jul 12, 2026
10cfeca
feat(settings): add 'Stay in the loop' card (Follow @alichherawalla o…
alichherawalla Jul 12, 2026
48fa40c
feat(about): add Follow @alichherawalla on X + Join Slack links to th…
alichherawalla Jul 12, 2026
fc93dcc
refactor: FOLLOW_X_URL + SLACK_INVITE_URL as shared constants (not in…
alichherawalla Jul 12, 2026
c8f7e42
feat(residency): per-model eject in the model selector (In Memory sec…
alichherawalla Jul 12, 2026
40d8b1c
test: lazy-reload after per-model eject — ejected text model reloads …
alichherawalla Jul 12, 2026
d47ed91
fix(residency): Eject All frees every resident incl. sidecars (DEV-B1)
alichherawalla Jul 12, 2026
fcdef1c
chore: bump pro — remove Pro Tools entry from Settings
alichherawalla Jul 12, 2026
d7e78c9
fix(residency): do not load whisper resident on download (DEV-B1 #1)
alichherawalla Jul 12, 2026
57eb57a
test: behavioral UI guards for the Follow/Slack cards + Pro Tools rem…
alichherawalla Jul 12, 2026
d95c044
test(residency): STT reclaimed on send (tight device) + debuggable lo…
alichherawalla Jul 12, 2026
cfb98f8
test(residency): validate STT reclaim via the model selector In Memor…
alichherawalla Jul 12, 2026
d8f91e9
docs(checklist): add residency matrix T111-T120 (co-residency/auto-ev…
alichherawalla Jul 12, 2026
fdcdeb5
test(T116): text + whisper allowed co-residence via In Memory UI
alichherawalla Jul 12, 2026
2cea43a
docs(checklist): T116 co-residence → automated (textWhisperCoresident)
alichherawalla Jul 12, 2026
2c214b8
test(T117): OS memory warning reclaims idle sidecars via In Memory UI
alichherawalla Jul 12, 2026
ce37ad2
docs(checklist): T117 auto-eviction → automated (memoryWarningEvictsS…
alichherawalla Jul 12, 2026
aa7e3d2
test(T115): voice-note send reclaims idle STT on a tight device (In M…
alichherawalla Jul 12, 2026
f7cc8bc
docs(checklist): T115 voice reclaim → automated (voiceNoteReclaimsStt)
alichherawalla Jul 12, 2026
390be1c
docs: log T119 (whisper blocked-retry) as OPEN/deferred — harness gap…
alichherawalla Jul 12, 2026
b69b780
test(T120): TTS co-resides during a voice turn via In Memory UI
alichherawalla Jul 12, 2026
29ce692
docs(checklist): T120 automated; T118/T119 deferred with honest reasons
alichherawalla Jul 12, 2026
a6c42a4
fix(test): T023 arrival — load whisper via real loadModel, not the (n…
alichherawalla Jul 12, 2026
0e218b4
test(T044): parallel calculator tools render two result bubbles (full…
alichherawalla Jul 12, 2026
3cf3829
test(T038): thinking + tool-result + answer all render (full UI, upgr…
alichherawalla Jul 12, 2026
c0aa70d
docs(checklist): T038 + T044 upgraded to full-UI (partial→automated)
alichherawalla Jul 12, 2026
d1d05d2
docs(checklist): T071 already automated (enhancementNoThinking) — fix…
alichherawalla Jul 12, 2026
df6168c
test(T038): flip Thinking on via the real quick-settings toggle, not …
alichherawalla Jul 12, 2026
7c4987d
test(T048): remote parallel tool_calls render as bubbles + reply (ful…
alichherawalla Jul 12, 2026
722b8e6
docs(checklist): T048 upgraded to full-UI (remoteParallelTools)
alichherawalla Jul 12, 2026
20ed9e7
test(T035): thinking-box header reads DONE while reasoning still stre…
alichherawalla Jul 12, 2026
e41d0ae
test(T046): add a remote server → it connects (full UI, upgrade from …
alichherawalla Jul 12, 2026
851e86d
docs(checklist): partial campaign complete — 6/13 upgraded to full-UI…
alichherawalla Jul 12, 2026
2c95bdb
test(T085): voice-mode calculator journey — STT → tool → answer (full…
alichherawalla Jul 12, 2026
e20c2ea
test(T084): voice-mode image journey — STT 'draw a dog' → route → ima…
alichherawalla Jul 12, 2026
aa7d8ec
docs(checklist): T084 + T085 voice journeys automated
alichherawalla Jul 12, 2026
95ea469
test(T014): GPU/OpenCL backend → GenerationMeta shows offloaded layer…
alichherawalla Jul 12, 2026
e22fc66
test(T020): LiteRT select is lazy (no eager warm), loads on first sen…
alichherawalla Jul 12, 2026
d527163
test(T072): enhancement reasoning must not become the image prompt (D…
alichherawalla Jul 12, 2026
a31d0fc
test(T077): chat-mode mic leaks the realtime session on navigate-away…
alichherawalla Jul 12, 2026
60137f3
test(T072): validate enhancement-reasoning red at the UI layer, not t…
alichherawalla Jul 12, 2026
556c469
docs(T015): defer NPU-gibberish as native-only (no JS surface to assert)
alichherawalla Jul 12, 2026
db1603b
test(T047): empty LAN scan shows alert AND adds no phantom server (DE…
alichherawalla Jul 12, 2026
271edba
test(T053): remote model is marked in the selector (Remote badge + se…
alichherawalla Jul 12, 2026
7c8499c
test(T057): tapping a pre-send image thumbnail opens no preview (DEV-…
alichherawalla Jul 12, 2026
b2a043e
test(T095): server+model configured → tap Continue skips onboarding i…
alichherawalla Jul 12, 2026
d992221
test(T096): support-share sheet dismisses after Share on X, does not …
alichherawalla Jul 12, 2026
7b52051
test(T097): Home Text count with a remote model active is truthful, n…
alichherawalla Jul 12, 2026
1e17504
test(T012): ModelsScreen renders N downloaded-indicator marks for N d…
alichherawalla Jul 12, 2026
ee9f5d7
test(T078): chat-mode mic double-tap opens a second realtime session …
alichherawalla Jul 12, 2026
79f4456
test(T073): enhancement step shows no streaming/progress (DEV-B30b, red)
alichherawalla Jul 12, 2026
5aafed0
test(T058,T086,T103): vision-affordance parity + voice thinking width…
alichherawalla Jul 12, 2026
0b69a3c
test(T034): max-predict cutoff is silent — no cut-off/continue indica…
alichherawalla Jul 12, 2026
3336b1f
test(T087): voice mode renders a stray empty '#' bubble after a tool …
alichherawalla Jul 12, 2026
e3c04f9
docs: defer T021 (mmproj estimate — wrong surface + harness gap) and …
alichherawalla Jul 12, 2026
39956d5
docs: mark T010/T011 deferred (need the RAG doc-attach UI harness, pe…
alichherawalla Jul 12, 2026
75d3f42
test(T083): deleting the TTS model mid-playback breaks playback (V5-g…
alichherawalla Jul 12, 2026
27f4c8d
test(T010,T011): mounted KB doc-attach — scanned-PDF message (red) + …
alichherawalla Jul 12, 2026
a6d52ca
test(T118): embedding model co-resides as a sidecar after a KB index …
alichherawalla Jul 12, 2026
044469c
test(T016): GPU/OpenCL init timeout falls back to CPU gracefully (DEV…
alichherawalla Jul 12, 2026
d17e4fd
test(T119): voice note transcribes via whisper blocked->free->retry o…
alichherawalla Jul 12, 2026
1c23eaa
docs(T021): re-scope deferral — blocked on a product decision (mmproj…
alichherawalla Jul 12, 2026
81e2414
fix(whisper): deleteModel only cancels the in-flight download when it…
alichherawalla Jul 12, 2026
82a7f3d
chore(pro): bump pointer for V4 tts residency-release fix
alichherawalla Jul 12, 2026
592bc45
fix(speech): single source of truth for speech-ready text; chat Speak…
alichherawalla Jul 12, 2026
2d9539d
chore(pro): bump pointer for Q19 speech-text DRY migration
alichherawalla Jul 12, 2026
0b99b4a
fix(whisper): don't list a truncated/sub-threshold file as a download…
alichherawalla Jul 12, 2026
e2f797a
fix(tools): single lenient tool-arg parse; recover unquoted keys + st…
alichherawalla Jul 12, 2026
019ab18
fix(tools): LiteRT surfaces tool result when the model's final turn i…
alichherawalla Jul 12, 2026
93ce7bb
fix(tools): tool router honors an explicit 'none' decline over a name…
alichherawalla Jul 12, 2026
741b327
fix(tools): LiteRT tool-loop routes media through the modelMedia seam…
alichherawalla Jul 12, 2026
2fa5edd
fix(voice): chat-mode direct-audio voice note transcribes before disp…
alichherawalla Jul 12, 2026
557a2dc
fix(stt): chat-mode mic lifecycle — stop on unmount (B11) + absorb do…
alichherawalla Jul 12, 2026
fae483c
fix(downloads): Download Manager 'downloading' count uses isDownloadi…
alichherawalla Jul 12, 2026
2c57469
fix(projects): cascade project delete, gate KB tool on project existe…
alichherawalla Jul 12, 2026
6c4828b
fix(enhance): image-prompt enhancement forces thinking OFF, streams, …
alichherawalla Jul 12, 2026
27b4112
fix(enhance): stream the partial rewrite into the enhancement message…
alichherawalla Jul 12, 2026
231a63f
fix(thinking): show remote dedicated-reasoning + 'Thinking...' header…
alichherawalla Jul 12, 2026
fa1e8cb
chore(pro): bump pointer for B27 voice thinking-block width
alichherawalla Jul 12, 2026
e326f98
chore(pro): bump pointer for B32 stray-bubble fix
alichherawalla Jul 12, 2026
b7d7635
fix(litert): re-apply sampler mid-conversation when it changes (Q18)
alichherawalla Jul 12, 2026
ef0a38d
fix(generation): surface a runtime engine failure durably inline + ke…
alichherawalla Jul 12, 2026
4ca5aa1
fix(generation): surface a silent max-predict cutoff with a cut-off i…
alichherawalla Jul 12, 2026
cd6ae22
fix(rag): roll back a failed index + name the scanned-PDF cause (KB)
alichherawalla Jul 12, 2026
b5b0de8
fix(rag): invalidate a stale-dimension tool embedding on model swap (…
alichherawalla Jul 12, 2026
2505508
fix(image): mount hydration must not orphan the active image model (i…
alichherawalla Jul 12, 2026
5b295d3
fix(settings): Reset to Defaults also resets the image params (Q12)
alichherawalla Jul 12, 2026
0749803
feat(memory): 3-mode model-loading selector (conservative/balanced/ag…
alichherawalla Jul 12, 2026
3546a1a
test(memory): 3-mode loading spec (conservative/balanced/aggressive +…
alichherawalla Jul 12, 2026
27ac4e0
fix(promo): support-open-source sheet shows at most once per session
alichherawalla Jul 12, 2026
6032147
feat(memory): implement 3-mode behavior + Load-Anyway-always + 2 pre-…
alichherawalla Jul 12, 2026
ca259f5
feat(memory): dirty-vs-clean real-RAM gate — clean residents page, di…
alichherawalla Jul 12, 2026
cd5c143
fix(memory): loadPolicySync projects the 3-mode setting, not just the…
alichherawalla Jul 12, 2026
d1de8ff
test(memory): prove no dead-end refusal (Load Anyway always offered) …
alichherawalla Jul 12, 2026
5171353
test(memory): balanced SWAP + failed-unload are dirty-vs-dirty (clean…
alichherawalla Jul 12, 2026
9ac7164
test(memory): re-pin single-model switching to conservative; 2.5x ima…
alichherawalla Jul 12, 2026
18f9ac5
fix(memory): revert the dirty-gate regression — restore Android recla…
alichherawalla Jul 13, 2026
1ba65b3
test(memory): residency matrix — co-reside/swap across all 3 modes (s…
alichherawalla Jul 13, 2026
a11e6ac
fix(memory): dirty physical ceiling refuses an oversized dirty model …
alichherawalla Jul 13, 2026
068b912
test(memory+cleanup): align override/loadPolicySync tests to ratified…
alichherawalla Jul 13, 2026
2a17bfe
fix(device-bugs): downloads recovery+retry, KB embed-fail abort, imag…
alichherawalla Jul 13, 2026
4f02171
fix(device-bugs): stuck spinner (B13), remote prompt-enhance (B30), a…
alichherawalla Jul 13, 2026
9d3bbd8
fix(image): Q7 guidance-scale default 7.5 (DRY); fix Q20 STT test; dr…
alichherawalla Jul 13, 2026
1788eb0
test(remote): align reasoning_content spec to B16/B17 — dedicated fie…
alichherawalla Jul 13, 2026
2afad7f
refactor(hygiene): extract to bring whisperService/modelResidency/ima…
alichherawalla Jul 13, 2026
298f1f9
chore(hygiene): clear all pre-push eslint errors (behavior-neutral)
alichherawalla Jul 13, 2026
5d16202
chore(hygiene): clear last 6 eslint errors in test harness (unused va…
alichherawalla Jul 13, 2026
5b35ac2
test(chat): dictation inserts into composer, not auto-send (B26 — ali…
alichherawalla Jul 13, 2026
e99ae3a
test: align 2 stale specs to ratified behavior (whisper size floor, T…
alichherawalla Jul 13, 2026
c84872e
chore: gitignore docs/wire-captures (raw device logs kept local, not …
alichherawalla Jul 13, 2026
0391730
fix(ci): unbreak the CI pipeline (secrets-in-if startup failure) + cl…
alichherawalla Jul 13, 2026
48702d0
chore(ci): local Sonar scan skips (not fails) when project uses serve…
alichherawalla Jul 13, 2026
a70781a
fix(settings): move 'Stay in the loop' card below Off Grid AI PRO, ab…
alichherawalla Jul 13, 2026
c2e874c
docs(test): comprehensive release checklist — 45 -> 185 rows (gap-aud…
alichherawalla Jul 13, 2026
b42b8d6
docs(test): move permission-prompt rows to first-use (mic->Voice, pho…
alichherawalla Jul 13, 2026
043c8e3
test(spec): UI-behavior spec-tests for onboarding-desktop-link, scan-…
alichherawalla Jul 13, 2026
a377838
fix(models,onboarding): make 3 device-reported bugs pass their spec-t…
alichherawalla Jul 13, 2026
59977b7
Merge pull request #541 from off-grid-ai/fix/onboarding-scan-litert-d…
alichherawalla Jul 13, 2026
8f36e23
fix(downloads): queued downloads survive an app kill/restart
alichherawalla Jul 13, 2026
7e65286
refactor(models): one owned device-aware litert download-warning deci…
alichherawalla Jul 13, 2026
bb180e1
fix(downloads): restore dispatches queued items without awaiting slot…
alichherawalla Jul 13, 2026
ebbc585
docs(test): checklist rows for queued-kill survival, device-aware lit…
alichherawalla Jul 13, 2026
b4bfeac
fix(generation): stop mid-turn no longer wedges the chat ('LLM servic…
alichherawalla Jul 13, 2026
65a802b
fix(capabilities): a selected-but-not-loaded GGUF shows its real Tool…
alichherawalla Jul 13, 2026
6364644
feat(models): remote selection in the Models sheet carries a cloud ma…
alichherawalla Jul 13, 2026
291f2b6
fix(boot): download-state recovery no longer blocks first paint
alichherawalla Jul 13, 2026
5bffd1c
fix(generation): stopped turn is never mislabeled 'No response / inco…
alichherawalla Jul 13, 2026
046e5ad
docs(gaps): GPU-selected-but-CPU report, manager-sheet residency desi…
alichherawalla Jul 13, 2026
fdf29b5
docs(gaps): reload is a drifted second load path (drops thinking+GPU+…
alichherawalla Jul 13, 2026
074751a
docs(gaps): mic loader during background STT download misreads as app…
alichherawalla Jul 13, 2026
1ffdc06
test(generation): RED — a send racing a settings reload silently lose…
alichherawalla Jul 13, 2026
3baa327
test(generation): RED — GPU-selected load that lands on CPU is a sile…
alichherawalla Jul 13, 2026
396bea2
fix(models): loaded-state publishes atomically after capability detec…
alichherawalla Jul 13, 2026
e34cdcc
test: delete reloadWithSettings (dead drifted second load path) and t…
alichherawalla Jul 13, 2026
0cd5755
test(models): all-branch unit layer under describeGpuFallback + captu…
alichherawalla Jul 13, 2026
d706636
docs: close the reload-drift gaps entry (root cause corrected: readin…
alichherawalla Jul 13, 2026
2e7b238
docs(claude): integration tests are the standard; failing mockist tes…
alichherawalla Jul 13, 2026
dcda950
test(models): RED — manager sheet is the residency surface (RAM chip …
alichherawalla Jul 13, 2026
d918320
test(generation): RED rendered journey — stale text failure card must…
alichherawalla Jul 13, 2026
8ab8f97
fix(generation): clear the stale text failure card at the dispatch se…
alichherawalla Jul 13, 2026
b0fe0e6
docs: checklist row 193 (stale failure card cleared on new attempt) +…
alichherawalla Jul 13, 2026
077dc91
feat(models): manager sheet is the residency surface — per-row RAM ch…
alichherawalla Jul 13, 2026
53c83b3
Merge stale-failure-card fix: card clears at generation dispatch (age…
alichherawalla Jul 13, 2026
e3f53bd
test(models): RED — Home picker tags a 2.89GB model 'may not fit' on …
alichherawalla Jul 13, 2026
12581f0
fix(models): picker fit hint uses the ONE owned device budget (fileEx…
alichherawalla Jul 13, 2026
935e9f5
test(voice): RED — background STT download must not render the mic as…
alichherawalla Jul 13, 2026
f9354fa
fix(models): Android GPU init timeout 8s -> 25s (Adreno 735 first-loa…
alichherawalla Jul 13, 2026
5f33937
test: advance fake timers past the raised 25s Android GPU init timeout
alichherawalla Jul 13, 2026
4b767c6
fix(voice): background STT download renders a determinate download ri…
alichherawalla Jul 13, 2026
89d6fa7
docs: checklist row 192 (mic download ring, not loader) + close the I…
alichherawalla Jul 13, 2026
4468910
Merge mic-loader fix: background STT download renders a download ring…
alichherawalla Jul 13, 2026
6b0c5ac
fix(generation): a stopped turn is never labeled 'Reply cut off at th…
alichherawalla Jul 13, 2026
264664c
fix(tools): never auto-inject search_knowledge_base — only user-toggl…
alichherawalla Jul 13, 2026
2888929
fix(generation): tool path displays the runtime's reasoning instead o…
alichherawalla Jul 13, 2026
acb0f54
fix(settings): Model Loading label 'Conservative' -> 'Lean' so the 3-…
alichherawalla Jul 13, 2026
45604e3
test(memory): migrate the residency suite off the removed picker In-M…
alichherawalla Jul 14, 2026
95a1d08
test(projects): guard that a real project never auto-injects search_k…
alichherawalla Jul 14, 2026
e1ea199
fix(models): reload waits for the old context's memory to be reclaime…
alichherawalla Jul 14, 2026
75439fd
fix(models): show the load spinner ON the selected model row, not a b…
alichherawalla Jul 14, 2026
4f1a936
fix(models): read the model's real trained context (arch-prefixed GGU…
alichherawalla Jul 14, 2026
12db7dc
fix(models): load spinner shows on the just-tapped row, not the previ…
alichherawalla Jul 14, 2026
8530521
fix(memory): GPU-aware text RAM estimate so Aggressive doesn't overco…
alichherawalla Jul 14, 2026
a2dc2c9
fix(models): selection highlight follows the just-tapped model during…
alichherawalla Jul 14, 2026
f540bf7
fix(models): one owner for activeModelId (select intent) + clear it o…
alichherawalla Jul 14, 2026
41128bb
fix(generation): thinking toggle applies to the current turn (no off-…
alichherawalla Jul 14, 2026
f6ec772
fix(models): show the load spinner on the active row during a no-tap …
alichherawalla Jul 14, 2026
68d2e0c
fix(vision): pair the mmproj to its model, not the first one in the dir
alichherawalla Jul 14, 2026
dc67edc
fix(vision): match mmproj on the quant-stripped model stem (quant-ind…
alichherawalla Jul 14, 2026
f42105c
fix(vision): don't trust a persisted mismatched mmproj — self-heal th…
alichherawalla Jul 14, 2026
d7d059e
fix(vision): refuse a non-belonging mmproj instead of pairing the clo…
alichherawalla Jul 14, 2026
a8ccee5
fix(downloads): show a wrench (repair), not an eye, for a vision mode…
alichherawalla Jul 14, 2026
347fc42
feat(chat): surface 'vision file missing → repair' instead of a bare …
alichherawalla Jul 14, 2026
ad01759
fix(models): wrench (not eye) for the repair action on the model cata…
alichherawalla Jul 14, 2026
dbcdf1d
fix(vision): keep the repair signal on dead-link cleanup + gate image…
alichherawalla Jul 14, 2026
0d20107
test(vision): pin the image-send gate — only a working vision model a…
alichherawalla Jul 14, 2026
c815752
refactor(vision): one strict model<->projector matching rule (SOLID/DRY)
alichherawalla Jul 14, 2026
5cb2f3e
fix(downloads): one shared projector per model across quants (no re-d…
alichherawalla Jul 14, 2026
cd924b0
fix(chat): one shared vision gate for BOTH send and resend
alichherawalla Jul 14, 2026
91ae918
fix(vision): heal the durable vision flag from the catalog → wrench s…
alichherawalla Jul 14, 2026
615f010
feat(settings): add Transcription (STT) + Text to Speech (TTS) to Mod…
alichherawalla Jul 14, 2026
8ba4504
fix(models): one reactive source for 'currently loaded' (loaded/unloa…
alichherawalla Jul 14, 2026
7f87e97
fix(generation): Stop keeps the shown partial (both engines) + stop t…
alichherawalla Jul 14, 2026
38cee0e
chore(diag): [STOP-SM] trace the keep-partial decision (convId + show…
alichherawalla Jul 14, 2026
3c31df3
fix(generation): Stop keeps a reasoning-only partial (stopped mid-THI…
alichherawalla Jul 14, 2026
1e5e2b7
fix(downloads): repair-in-progress uses the theme color, not warning …
alichherawalla Jul 14, 2026
e04d12b
fix(generation): never discard shown output on ERROR either (llama/li…
alichherawalla Jul 14, 2026
d57c986
fix(voice): chat-mode STT is dictation-to-the-input-box on every engi…
alichherawalla Jul 14, 2026
2d3ad38
fix(reasoning): strip bare <|channel>thought opener (empty thought → …
alichherawalla Jul 14, 2026
b3d9406
fix(image): read steps/cfg/guidance fresh from store, not a stale cal…
alichherawalla Jul 14, 2026
a6c0777
fix(thinking): derive Thinking capability from chat_template, not Jin…
alichherawalla Jul 14, 2026
5674d2f
chore(lint): clear pre-push blockers across the branch
alichherawalla Jul 14, 2026
d57a3fe
chore(lint): alias inner useAppStore require in HomeScreen.test to cl…
alichherawalla Jul 14, 2026
c221b08
test: right-size harness model + drop mockists of superseded behavior
alichherawalla Jul 14, 2026
ec97a55
chore(lint): drop symbols orphaned by the deleted useModelLoading sel…
alichherawalla Jul 14, 2026
6933e0a
test(memory): eviction-premise tests declare the heavy 4GB model expl…
alichherawalla Jul 14, 2026
f500ec1
test(gpu): widen reload waits — reload now includes the device-critic…
alichherawalla Jul 14, 2026
cc1be68
refactor(models): break the ModelsManagerSheet<->useResidentRows impo…
alichherawalla Jul 14, 2026
eab99f6
fix(memory): changing the load mode ejects every resident (Lean now t…
alichherawalla Jul 14, 2026
6ca8c9f
fix(generation): remote tool turn resets generating state on error (s…
alichherawalla Jul 14, 2026
7201365
fix(chat): stack message attachments vertically (voice note above ima…
alichherawalla Jul 14, 2026
314e3f7
chore(lint): trim desync-fix comment to keep generationServiceHelpers…
alichherawalla Jul 14, 2026
e57b7aa
chore(lint): condense two historical comments to keep generationServi…
alichherawalla Jul 14, 2026
3a6d745
chore(lint): condense catch comment — generationServiceHelpers now at…
alichherawalla Jul 14, 2026
930140c
ci+chore: fix CI gates — Node 22, pro submodule in typecheck/architec…
alichherawalla Jul 14, 2026
b18e65f
fix(memory)+ci: remove unwired survival-floor cluster; run CI jest se…
alichherawalla Jul 14, 2026
f3c5ca1
test(audio): assert voice thinking-block width on the constraining wr…
alichherawalla Jul 14, 2026
118eef1
fix(tools): cap tool result fed to the model (~24k chars) so a bad to…
alichherawalla Jul 14, 2026
a4c10e1
chore(pro): bump submodule to voice-mode fixes (PR #22: image render,…
alichherawalla Jul 14, 2026
c0c1570
ci: run tests on Node 24 (node:sqlite) + drop mockist textDownloadPro…
alichherawalla Jul 14, 2026
e0b0a26
ci: run tests on Node 26, not 24 (node:sqlite teardown segfault)
alichherawalla Jul 14, 2026
b3f6808
test(hardening): make project-chat ordering test deterministic (contr…
alichherawalla Jul 14, 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
121 changes: 121 additions & 0 deletions .dependency-cruiser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* dependency-cruiser — the STANDING GATE for the architectural boundaries we keep
* re-establishing by hand in review (layering, engine DIP, dead code, cycles, dep hygiene).
*
* AGGRESSIVE by design: rules are strict and at `error`. It sees the IMPORT GRAPH, not values —
* so it enforces "a screen may not import a concrete engine service" (the SO2/SO4 class, at the
* bad import) and "utils/services may not import UI" (the DR1 backward-layering class), but it
* does NOT catch the `engine === 'litert'` VALUE branch (an ESLint no-restricted-syntax rule
* guards that) or DRY drift / logic bugs. Complements the hygiene standard; does not replace it.
*
* The tree is CLEAN — zero violations, no baseline file. Every rule is fully enforced with no
* exceptions; any new violation fails `npm run depcruise` (CI + pre-push). If you ever must adopt
* a new strict rule onto legacy debt, baseline it (`depcruise --output-type baseline >
* .dependency-cruiser-known-violations.json` + run the gate with `--ignore-known`) and burn it
* down — never regenerate a baseline to hide a fresh violation.
*/
module.exports = {
forbidden: [
// ── Architecture: layering + DIP ──────────────────────────────────────────
{
name: 'no-circular',
severity: 'error',
comment: 'Import cycles make load order undefined and desync-prone. Break the cycle (import the concrete module, not the barrel; extract shared state down a layer).',
from: {},
to: { circular: true },
},
{
name: 'no-backward-layering-core',
severity: 'error',
comment: 'Core (utils/services/stores/types/constants/config) must not import UI (screens/components/navigation). If a screen owns logic the core needs, move the logic DOWN (see DR1: parseModelOutput → utils).',
from: { path: '^src/(utils|services|stores|types|constants|config)/' },
to: { path: '^src/(screens|components|navigation)/' },
},
{
name: 'utils-stay-pure',
severity: 'error',
comment: 'src/utils is the zero-IO pure layer — it must not depend on services or stores. Pure logic here is unit-testable without mocking I/O (hygiene §A).',
from: { path: '^src/utils/' },
to: { path: '^src/(services|stores)/', pathNot: '^src/utils/' },
},
{
name: 'engine-dip-no-concrete-in-ui',
severity: 'error',
comment: 'UI (screens/components) must depend on the engine ABSTRACTION (services/engines), never a concrete engine service (services/litert, services/llm). Branching on a concrete engine in a caller is the DIP violation we keep fixing (SO2/SO4). Route through services/engines.',
from: { path: '^src/(screens|components)/' },
to: { path: '^src/services/(litert|llm)(/|\\.|$)' },
},
{
name: 'components-are-leaf-ui',
severity: 'error',
comment: 'Reusable components must not import screens or navigation — a leaf UI piece depending on a whole screen inverts the dependency and creates cycles. Lift shared bits into components/ or pass them in as props.',
from: { path: '^src/components/' },
to: { path: '^src/(screens|navigation)/' },
},
// ── Dead code ─────────────────────────────────────────────────────────────
{
name: 'no-orphans',
severity: 'error',
comment: 'Orphan module (no importers, imports nothing relevant) — dead code. Confirm with grep, then delete (the standing dead-code gate that retires the manual recon).',
from: {
orphan: true,
pathNot: [
'\\.(d\\.ts)$',
'(^|/)index\\.(ts|tsx)$', // barrel/entry files
'^src/types/', // type barrels are legitimately import-only
'^src/(bootstrap|shims|config)/', // wiring/shim/config shells reached outside the graph
],
},
to: {},
},
// ── Test / build hygiene ──────────────────────────────────────────────────
{
name: 'not-to-test-from-prod',
severity: 'error',
comment: 'Production code (src) must not import test files or test utilities — that ships test-only code (and its mocks) into the app bundle.',
from: { path: '^src/', pathNot: '\\.(test|spec)\\.[jt]sx?$' },
to: { path: '(\\.(test|spec)\\.[jt]sx?$|^(__tests__|__mocks__)/)' },
},
{
name: 'not-to-dev-dep',
severity: 'error',
comment: 'Production code (src) must not import a devDependency — it will be missing in the release bundle. Move the package to dependencies, or the import out of src.',
from: { path: '^src/', pathNot: '\\.(test|spec)\\.[jt]sx?$' },
to: { dependencyTypes: ['npm-dev'], pathNot: '(@types/|typescript$)' },
},
{
name: 'no-phantom-deps',
severity: 'error',
comment: 'Imports a package that is not declared in package.json (a phantom/transitive dependency) — it breaks the moment the transitive graph changes. Declare it explicitly.',
from: {},
to: {
dependencyTypes: ['unknown', 'undetermined', 'npm-no-pkg', 'npm-unknown'],
// Known non-resolvable-by-cruiser but LEGITIMATE: whisper.rn IS declared (^0.5.5) but the
// `.rn` name defeats cruiser's resolver; @offgrid/pro is the private open-core submodule
// wired via metro haste through the ONE bootstrap loader (loadProFeatures) — intentional,
// not a phantom dep. Neither is real debt, so exclude rather than baseline.
pathNot: '(^whisper\\.rn(/|$)|^@offgrid/pro(/|$))',
},
},
{
name: 'no-deprecated-core',
severity: 'warn',
comment: 'Depends on a deprecated Node core module (e.g. punycode) — will break on a future runtime. Replace it.',
from: {},
to: { dependencyTypes: ['core'], path: '^(punycode|domain|constants|sys|_linklist|_stream_wrap)$' },
},
],
options: {
doNotFollow: { path: 'node_modules' },
tsPreCompilationDeps: true,
tsConfig: { fileName: 'tsconfig.json' },
enhancedResolveOptions: {
exportsFields: ['exports'],
conditionNames: ['import', 'require', 'node', 'default', 'types'],
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
},
reporterOptions: {
text: { highlightFocused: true },
},
},
};
29 changes: 28 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
module.exports = {
root: true,
extends: '@react-native',
extends: [
'@react-native',
// SonarJS: Sonar-grade bug/smell detection in normal lint — free, local, and it covers
// PRO too (pro has no cloud Sonar project; a private cloud project is paid-by-LOC). Most
// rules stay at `error` (recommended default) as a forward guard; the handful already
// tripped on legacy code are `warn` below with a logged burn-down (GAPS_BACKLOG).
'plugin:sonarjs/recommended-legacy',
],
plugins: [
'react-native',
'react',
'react-hooks',
'sonarjs',
],
env: {
jest: true,
Expand Down Expand Up @@ -43,6 +51,20 @@ module.exports = {
'react-native/no-color-literals': 'error',
'react-native/no-raw-text': 'error',
'react-native/no-single-element-style-arrays': 'error',

// SonarJS — every rule stays at the recommended `error` (a real forward guard on new code)
// EXCEPT the two handled here:
// - no-duplicate-string OFF: it fights RN styling — 'space-between'/'center'/'row' and color
// literals repeat by design across StyleSheet objects; a constant per style value is noise,
// not clarity. The one low-value SonarJS rule for this codebase.
// - the rest are `warn` (already tripped on legacy core; burn-down in docs/GAPS_BACKLOG.md,
// ratchet each back to `error` as its count hits zero).
'sonarjs/no-duplicate-string': 'off',
'sonarjs/prefer-single-boolean-return': 'warn',
'sonarjs/no-nested-template-literals': 'warn',
'sonarjs/no-collapsible-if': 'warn',
'sonarjs/prefer-immediate-return': 'warn',
'sonarjs/no-duplicated-branches': 'warn',
},
overrides: [
{
Expand All @@ -56,6 +78,11 @@ module.exports = {
'react-native/no-inline-styles': 'off',
'react-native/no-raw-text': 'off',
'react-native/no-color-literals': 'off',
// Duplicate test bodies (identical arrange/act across cases) are acceptable and clearer
// than over-DRYing tests; the real-bug SonarJS rules (mischeck, unused-collection, etc.)
// stay ON for tests — they caught a tautology assertion + a dead collection here.
'sonarjs/no-identical-functions': 'off',
'sonarjs/cognitive-complexity': 'off',
},
},
],
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ jobs:
- name: Type check
run: npx tsc --noEmit

architecture:
# Dependency-cruiser standing gate: fails on any NEW architectural violation beyond the
# baselined debt (layering, engine DIP, import cycles, orphans). See .dependency-cruiser.js.
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Dependency-cruiser (architecture gate)
run: npm run depcruise

test:
runs-on: macos-latest

Expand Down Expand Up @@ -173,3 +194,8 @@ jobs:

- name: Build Android Release
run: cd android && ./gradlew assembleRelease

# NOTE: SonarCloud runs via Automatic Analysis (SonarCloud-side, on the public core project) —
# no CI job. A CI scan would only add coverage import, which Codecov (in the `test` job) already
# does, so it'd be redundant complexity. Pro is scanned locally by eslint-plugin-sonarjs (free,
# private, no leak). See docs/GAPS_BACKLOG.md.
3 changes: 3 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ if [ -n "$PUSHED_JS" ]; then

echo "▶ JS/TS tests (related to changed files)..."
echo "$PUSHED_JS" | tr '\n' '\0' | xargs -0 npx jest --findRelatedTests --passWithNoTests

echo "▶ Architecture gate (dependency-cruiser)..."
npm run depcruise
fi

if [ -n "$PUSHED_SWIFT" ]; then
Expand Down
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ Then `grep`/read `/tmp/offgrid-debug.log`. The file appends a `===== session sta

**Merge strategy: ALWAYS a merge commit. NEVER squash (and never rebase-merge).** When merging a PR, use `gh pr merge --merge` (or the "Create a merge commit" button) so the full commit history is preserved on `main`. Do not squash under any circumstances - the small, meaningful per-concern commits are the record and must survive the merge. This applies to both the core repo and the `pro` submodule.

### Commit early, commit often - never lose progress (agents especially)

**A long task is a chain of small, GREEN, committed steps - not one giant uncommitted diff.** Agents run against context/session limits; anything uncommitted is lost when the session ends. So:

- **Commit each cohesive step as soon as it is green** (typecheck + the relevant tests pass), with a real per-concern message. A refactor done in slices commits after each slice, not at the end.
- **Never leave a large uncommitted working tree across a risky/long operation.** If you are about to start something big, commit what already works first so there is a clean restore point.
- **Every commit is a safe restore point:** it must be behavior-neutral-or-better and pass the gates for the files it touches. Do not commit a knowingly-broken tree; if mid-refactor is unavoidably broken, finish to green before committing (or stash), never push broken.
- **Prefer many small commits over few large ones.** They survive a merge (we never squash), make review tractable, and mean a lost session costs one step, not the whole task.
- This is not optional polish - it is how work is not lost. Treat "a lot of uncommitted changes" as a bug to fix immediately by landing them as small commits.

## Copy & Content Standards

**Any change to website copy, essays, docs text, UI strings, or marketing content must follow the brand voice guide:**
Expand Down
2 changes: 0 additions & 2 deletions __tests__/integration/audio/streamingStateMachine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ jest.mock('@offgrid/core/utils/logger', () => ({
type SpeakMode = 'resolve' | 'hang' | 'throw';
let speakMode: SpeakMode = 'resolve';
let enginePhase = 'ready';
const releasers: Array<() => void> = [];
const mockEngine = {
speak: jest.fn(() => {
if (speakMode === 'throw') return Promise.reject(new Error('std::exception'));
Expand Down Expand Up @@ -68,7 +67,6 @@ beforeEach(async () => {
speakMode = 'resolve';
enginePhase = 'ready';
mockCanLoad.mockReturnValue(false);
releasers.length = 0;
// clearAllMocks resets call history but NOT implementations — restore the
// speakMode-driven impl so a prior test's mockImplementation can't leak in.
mockEngine.speak.mockImplementation(() => {
Expand Down
123 changes: 123 additions & 0 deletions __tests__/integration/chat/reasoningPipeline.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* INTEGRATION — the reasoning pipeline through its REAL seams, end to end.
*
* This is the test that proves the parse-once refactor actually works for a user, not that a
* mock returned what it was told. It drives the REAL chatStore (streaming → finalize) and the
* REAL ChatMessage render, with NOTHING mocked in the parse path, and asserts the observable
* outcome: reasoning is captured, the visible answer is clean, and raw model markup NEVER
* reaches the screen. It reproduces the two bugs this refactor closes:
* - DR1 — remote channel reasoning leaking into the answer (streamed-split flow)
* - OD14 / tool-call leak — raw <tool_call>/<function=…> markup shown as text
* for every format in the single-source grammar. A wall of green unit tests could not catch a
* wiring bug between stream → store → render; this can.
*/
import React from 'react';
import { render } from '@testing-library/react-native';
import { useChatStore } from '../../../src/stores/chatStore';
import { ChatMessage } from '../../../src/components/ChatMessage';
import { REASONING_DELIMITERS } from '../../../src/utils/messageContent';
import { ThinkTagParser } from '../../../src/services/providers/openAICompatibleStream';
import { resetStores, getChatState } from '../../utils/testHelpers';
import type { Message } from '../../../src/types';

const REASONING = 'let me weigh the options carefully';
const ANSWER = 'Here is the clean answer.';

/** Read the single assistant message the store just finalized into the active conversation. */
function finalizedMessage(convId: string): Message {
const msg = getChatState()
.conversations.find(c => c.id === convId)
?.messages.at(-1);
if (!msg) throw new Error('no finalized message');
return msg;
}

describe('reasoning pipeline — stream → finalize → render, real seams', () => {
beforeEach(() => resetStores());

describe.each(REASONING_DELIMITERS)(
'format opened by %j',
({ open, close }) => {
const raw = `${open}${REASONING}${close}${ANSWER}`;

it('LOCAL flow: inline-tagged content is split into reasoning + clean answer, and renders both', () => {
const store = useChatStore.getState();
const convId = store.createConversation('local-model');
store.startStreaming(convId);
// Local (llama.rn) path: raw tokens accumulate in streamingMessage; reasoning is extracted
// at finalize by the ONE shared parser.
store.appendToStreamingMessage(raw);
store.finalizeStreamingMessage(convId);

const msg = finalizedMessage(convId);
expect(msg.reasoningContent).toBe(REASONING);
expect(msg.content).toBe(ANSWER);
// The stored answer carries none of the opener/closer markup.
expect(msg.content).not.toContain(open.trim());
expect(msg.content).not.toContain(close.trim());

const { getByText, queryByText } = render(
<ChatMessage message={msg} />,
);
expect(getByText(ANSWER)).toBeTruthy(); // clean answer is visible
expect(getByText(new RegExp(REASONING.slice(0, 20)))).toBeTruthy(); // reasoning survived into the block
expect(
queryByText(
new RegExp(open.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
),
).toBeNull(); // no raw markup on screen
});

it('REMOTE flow: ThinkTagParser split → store → finalize keeps reasoning out of the answer (DR1)', () => {
const store = useChatStore.getState();
const convId = store.createConversation('remote-model');
store.startStreaming(convId);
// Remote (OpenAI-compatible) path: the streaming parser routes reasoning vs answer live,
// feeding the two store channels. Same grammar, different seam.
const parser = new ThinkTagParser();
parser.process(
raw,
t => useChatStore.getState().appendToStreamingMessage(t),
r => useChatStore.getState().appendToStreamingReasoningContent(r),
);
store.finalizeStreamingMessage(convId);

const msg = finalizedMessage(convId);
expect(msg.reasoningContent).toBe(REASONING);
expect(msg.content).toBe(ANSWER);

const { getByText, queryByText } = render(
<ChatMessage message={msg} />,
);
expect(getByText(ANSWER)).toBeTruthy();
expect(
queryByText(
new RegExp(open.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
),
).toBeNull();
});
},
);

it('tool-call markup never reaches the stored answer or the screen (OD14 / leak class)', () => {
const store = useChatStore.getState();
const convId = store.createConversation('local-model');
store.startStreaming(convId);
// A model that emits its answer wrapped around raw tool-call markup. After finalize the
// stored content — and everything rendered — must be free of that markup.
store.appendToStreamingMessage(
'Sure, checking that.<tool_call><function=get_weather><parameter=city>NYC</parameter></function></tool_call>',
);
store.finalizeStreamingMessage(convId);

const msg = finalizedMessage(convId);
expect(msg.content).not.toContain('<tool_call>');
expect(msg.content).not.toContain('<function=');
expect(msg.content).not.toContain('<parameter=');

const { queryByText } = render(<ChatMessage message={msg} />);
expect(queryByText(/<tool_call>/)).toBeNull();
expect(queryByText(/<function=/)).toBeNull();
expect(queryByText(/<parameter=/)).toBeNull();
});
});
Loading