Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
227 commits
Select commit Hold shift + click to select a range
454bccb
build(mobile): add native video playback dependency [skip ci]
alichherawalla Sep 28, 2026
50c5046
feat(media): store synced videos and update Pro integration [skip ci]
alichherawalla Sep 28, 2026
d42bbb1
feat(gallery): play save and delete videos on mobile [skip ci]
alichherawalla Sep 28, 2026
6e936cd
test(sync): cover received videos and chat attachment cleanup
alichherawalla Sep 28, 2026
3b80a82
feat(video): add shared native lifecycle and iOS Metal generation [sk…
alichherawalla Sep 28, 2026
d46a539
feat(video): add Android Vulkan generation and scoped media export [s…
alichherawalla Sep 28, 2026
2b2c7f3
feat(models): manage complete video packs and resumable downloads [sk…
alichherawalla Sep 28, 2026
0f42fb9
feat(remote): discover OGAD video models and resume remote jobs [skip…
alichherawalla Sep 28, 2026
26bf6fd
feat(models): add video search selection settings and storage control…
alichherawalla Sep 28, 2026
3adb2ba
feat(video): own generation recovery gallery and media export [skip ci]
alichherawalla Sep 28, 2026
421c95d
feat(chat): route video turns and tools through shared generation sta…
alichherawalla Sep 28, 2026
80ec42e
build(video): prepare pinned runtimes through CocoaPods and Gradle [s…
alichherawalla Sep 28, 2026
397a438
chore(ios): refresh pod checksums for physical device build [skip ci]
alichherawalla Sep 29, 2026
7b13e9a
fix(models): match video browsing to the text model flow [skip ci]
alichherawalla Sep 29, 2026
4777c92
fix(downloads): preserve video pack identity and validation progress …
alichherawalla Sep 29, 2026
9adcd03
fix(chat): allow local and remote video-only chat entry [skip ci]
alichherawalla Sep 29, 2026
f05fd82
fix(models): share text detail hierarchy with video browsing [skip ci]
alichherawalla Sep 29, 2026
a8c5fc9
feat(settings): allow hiding the dev sync status bar [skip ci]
alichherawalla Sep 29, 2026
b0e52ba
fix(video): use shared memory failure and override flow [skip ci]
alichherawalla Sep 29, 2026
519674a
fix(video): show generation progress inline in chat [skip ci]
alichherawalla Sep 29, 2026
49ad751
fix(settings): keep typed slider values visible [skip ci]
alichherawalla Sep 29, 2026
a806a96
fix(navigation): return from Pro page to Home [skip ci]
alichherawalla Sep 29, 2026
8d2d149
feat(video): load LTX and Hunyuan packs on mobile [skip ci]
alichherawalla Sep 29, 2026
2dcffb7
fix(video): use CPU conditioning on Android [skip ci]
alichherawalla Sep 29, 2026
e87d2fa
fix(video): show prompt processing and elapsed time [skip ci]
alichherawalla Sep 29, 2026
42256cf
perf(video): optimize Android debug engine kernels [skip ci]
alichherawalla Sep 29, 2026
b7312ff
fix(text): try GPU after NPU load failure [skip ci]
alichherawalla Sep 29, 2026
62b1df7
build(video): bundle Qualcomm DSP runtimes [skip ci]
alichherawalla Sep 29, 2026
9e65d09
fix(android): prepare DSP assets before packaging [skip ci]
alichherawalla Sep 29, 2026
b26d6cc
feat(video): select and report Android NPU execution [skip ci]
alichherawalla Sep 29, 2026
d22d20e
fix(video): respect buffer ownership across NPU fallback [skip ci]
alichherawalla Sep 29, 2026
995d324
fix(video): separate outputs across convolution fallback [skip ci]
alichherawalla Sep 29, 2026
cb7e96a
fix(video): keep NPU memory cap off GPU fallback [skip ci]
alichherawalla Sep 29, 2026
a4847ed
fix(video): show sampling when step zero starts [skip ci]
alichherawalla Sep 29, 2026
9a2576d
fix(video): stream NPU weights to reduce Android memory [skip ci]
alichherawalla Sep 29, 2026
30d7946
fix(image): validate pinned model packs before registration [skip ci]
alichherawalla Sep 29, 2026
d653f2f
feat(image): expose the mobile SD runtime with hardware fallback [ski…
alichherawalla Sep 29, 2026
75b5488
feat(image): connect Qwen packs to mobile model and generation flows …
alichherawalla Sep 29, 2026
5e764cd
feat(sync): describe portable mobile SD image packs [skip ci]
alichherawalla Sep 29, 2026
af12fc3
fix(video): name the frame decoding stage [skip ci]
alichherawalla Sep 29, 2026
e33aae2
fix(image): preserve active model residency and shared image identity…
alichherawalla Sep 29, 2026
2643722
fix(image): reuse verified pack parts and update transfer integration…
alichherawalla Sep 29, 2026
c277ec1
fix(video): bound Android GPU decode graphs [skip ci]
alichherawalla Sep 29, 2026
8d85823
fix(image): recover verified SD packs after interrupted registration …
alichherawalla Sep 29, 2026
1986b23
style(image): normalize integrity helper whitespace [skip ci]
alichherawalla Sep 29, 2026
c261d94
fix(video): avoid Adreno crash inside device-fault diagnostics [skip ci]
alichherawalla Sep 29, 2026
d4d5807
feat(image): search and download compatible SD packs from Hugging Fac…
alichherawalla Sep 29, 2026
986e046
fix(video): show OGAD stages on mobile clients [skip ci]
alichherawalla Sep 29, 2026
7b6b36a
chore(video): record Adreno shader compile details [skip ci]
alichherawalla Sep 29, 2026
c8323ea
fix(video): route unsafe Adreno float matvec to fallback backend [ski…
alichherawalla Sep 29, 2026
9564462
fix(sync): include video retry completion correction [skip ci]
alichherawalla Sep 29, 2026
6b5858b
fix(video): use CPU Wan decode on affected Adreno devices [skip ci]
alichherawalla Sep 29, 2026
d9c81bc
fix(build): use macOS host compiler for Vulkan shader generator [skip…
alichherawalla Sep 29, 2026
6810e0c
fix(video): report iOS background interruption [skip ci]
alichherawalla Sep 29, 2026
5d489af
fix(video): distinguish generation failures from load errors [skip ci]
alichherawalla Sep 29, 2026
c7850b0
fix(ios): stream model checksums during startup [skip ci]
alichherawalla Sep 29, 2026
a97958b
chore(ios): refresh installed pod spec checksums [skip ci]
alichherawalla Sep 29, 2026
f4883a4
feat(video): show reused frame previews and decode progress [skip ci]
alichherawalla Sep 29, 2026
48481c9
feat(video): report decode sections and reuse final frame preview [sk…
alichherawalla Sep 29, 2026
340b824
fix(video): honor full precision attention on Hexagon [skip ci]
alichherawalla Sep 29, 2026
a101a2e
feat(mobile): use themed video player controls and picture in picture…
alichherawalla Sep 29, 2026
8003ebf
fix(ios): rebuild video preview runtime for physical devices [skip ci]
alichherawalla Sep 29, 2026
e59e738
fix(mobile): fit video controls and picture in picture in one bar [sk…
alichherawalla Sep 29, 2026
d42d193
fix(mobile): show the first video frame before playback [skip ci]
alichherawalla Sep 29, 2026
0182a30
fix(chat): remove extra frame and caption from generated videos [skip…
alichherawalla Sep 29, 2026
2e4e47a
fix(video): use active app theme for player controls [skip ci]
alichherawalla Sep 29, 2026
cdf5737
fix(video): keep Android attention buffers safe across devices [skip ci]
alichherawalla Sep 29, 2026
67cd2db
fix(ios): keep local generation awake and log background admission [s…
alichherawalla Sep 29, 2026
b0b252f
fix(video): route unsafe Adreno attention through fallback [skip ci]
alichherawalla Sep 29, 2026
53e4d5d
fix(video): use catalog names in mobile model labels [skip ci]
alichherawalla Sep 29, 2026
62bc123
fix(video): show iOS background interruption while engine stops [skip…
alichherawalla Sep 29, 2026
ed7a6f4
fix(video): respect download cancellation and count pack storage [ski…
alichherawalla Sep 29, 2026
755cd74
feat(video): include compatible packs in mobile auto setup [skip ci]
alichherawalla Sep 29, 2026
6dc25d5
chore(pro): update video transfer display names [skip ci]
alichherawalla Sep 29, 2026
a7ef3ac
fix(video): bound Adreno attention work per dispatch [skip ci]
alichherawalla Sep 29, 2026
ca25ae3
perf(video): keep Adreno attention chunks on the GPU [skip ci]
alichherawalla Sep 29, 2026
4acdb1b
chore(video): log native progress changes for device checks [skip ci]
alichherawalla Sep 29, 2026
879acf4
fix(android): bound Adreno video attention dispatches [skip ci]
alichherawalla Sep 29, 2026
e572ae4
perf(video): share GPU query storage across attention chunks [skip ci]
alichherawalla Sep 29, 2026
885abdb
fix(android): keep video generation screen awake [skip ci]
alichherawalla Sep 29, 2026
0cd8fbb
fix(video): exclude model cache from iOS backups [skip ci]
alichherawalla Sep 29, 2026
54750d6
docs(video): refresh mobile integration audit and limits [skip ci]
alichherawalla Sep 29, 2026
3cc57c9
fix(image): keep cancellation from starting overlapping SD work [skip…
alichherawalla Sep 29, 2026
3b08e42
fix(litert): report requested accelerator and fallback [skip ci]
alichherawalla Sep 29, 2026
3b75e36
fix(image): use model-aware resolution controls [skip ci]
alichherawalla Sep 29, 2026
2c3a965
fix(ios): bound Wan decode graphs and report chunk progress [skip ci]
alichherawalla Sep 29, 2026
39f97ab
fix(image): retain residency lock through late SD load [skip ci]
alichherawalla Sep 29, 2026
2fd3175
fix(image): verify SD file sizes before pack registration [skip ci]
alichherawalla Sep 29, 2026
1df1f0d
fix(image): account for failed SD load cleanup [skip ci]
alichherawalla Sep 29, 2026
490814b
chore(pro): update video transfer integrity checks [skip ci]
alichherawalla Sep 29, 2026
4f0a96a
fix(image): retain admission until cancelled request settles [skip ci]
alichherawalla Sep 29, 2026
b4faf2f
fix(video): log Android latent and decode numeric summaries [skip ci]
alichherawalla Sep 29, 2026
f874fd4
Update Pro pricing copy for monthly plan
alichherawalla Sep 29, 2026
21b0440
feat(settings): move shared generation controls to General
alichherawalla Sep 29, 2026
d1b8bd0
test(settings): cover General controls on both surfaces
alichherawalla Sep 29, 2026
dca21cd
fix: show saved video generation details in chat [skip ci]
alichherawalla Sep 29, 2026
6e19d08
feat(settings): show default system prompt in chat Text settings
alichherawalla Sep 29, 2026
f9dae27
fix: recover active Android video after React reload [skip ci]
alichherawalla Sep 29, 2026
bb537c1
fix: retain video completion after bridge disposal [skip ci]
alichherawalla Sep 29, 2026
eaeeec9
feat(video): show auto setup pack files [skip ci]
alichherawalla Sep 29, 2026
93220a4
fix(onboarding): read device RAM before auto setup [skip ci]
alichherawalla Sep 30, 2026
dc67ffa
feat(onboarding): choose auto setup model downloads [skip ci]
alichherawalla Sep 30, 2026
340e440
fix(onboarding): explain video auto setup exclusion [skip ci]
alichherawalla Sep 30, 2026
2c295f5
fix(onboarding): offer video on 12 GB devices [skip ci]
alichherawalla Sep 30, 2026
52ee407
fix(onboarding): trust bundled video engine [skip ci]
alichherawalla Sep 30, 2026
ff55f7f
fix(video): preserve sampling state across reload [skip ci]
alichherawalla Sep 30, 2026
b85a21c
feat: stream lightweight SD image previews on mobile [skip ci]
alichherawalla Sep 30, 2026
96fcdce
Add selectable model downloads to Mobile Auto Setup
alichherawalla Sep 30, 2026
c688eb8
feat: load SD checkpoints and preserve RGBA image output [skip ci]
alichherawalla Sep 30, 2026
00b89f1
feat(image): support complete SD checkpoints on mobile [skip ci]
alichherawalla Sep 30, 2026
96f9cd5
fix(video): show pending stop until native work ends [skip ci]
alichherawalla Sep 30, 2026
8f9d6f4
feat(video): add short Android backend diagnostics [skip ci]
alichherawalla Sep 30, 2026
69fc04c
fix: retain iOS image backend and failure diagnostics [skip ci]
alichherawalla Sep 30, 2026
66f8111
Make Mobile Auto Setup choices compact
alichherawalla Sep 30, 2026
0a9571a
fix: keep video loading counts out of sampling progress [skip ci]
alichherawalla Sep 30, 2026
0a2e6fa
Tighten Mobile Auto Setup layout
alichherawalla Sep 30, 2026
1e98df1
feat: add mobile design partner details and email draft
alichherawalla Sep 30, 2026
448e8c8
feat: show design partner offer below Pro pricing
alichherawalla Sep 30, 2026
2a54562
feat: link Settings Pro card to design partner offer
alichherawalla Sep 30, 2026
0ec6a8b
copy: update small business design partner eligibility
alichherawalla Sep 30, 2026
e1bf5d2
fix(mobile): simplify included Auto Setup models
alichherawalla Sep 30, 2026
8c9e818
fix: report SD image runtime without inferred hardware [skip ci]
alichherawalla Sep 30, 2026
6a250c3
fix: handle resend while native video is stopping [skip ci]
alichherawalla Sep 30, 2026
113224d
copy: use dedicated design partner contact address
alichherawalla Sep 30, 2026
92df184
fix: keep Pro offers compact and make actions visible
alichherawalla Sep 30, 2026
95eac44
fix: use compact navigation rows for Pro secondary actions
alichherawalla Sep 30, 2026
ef87de9
copy: describe Pro memory Sync and approved actions
alichherawalla Sep 30, 2026
cab26f8
fix: state text hardware and global settings scope [skip ci]
alichherawalla Sep 30, 2026
fe4248f
fix(mobile): give Auto Setup actions accessible touch areas
alichherawalla Sep 30, 2026
1a54363
fix(models): resolve SD quant packs from canonical repository
alichherawalla Sep 30, 2026
427abb8
docs(design): define mobile UI quality and density rules
alichherawalla Oct 1, 2026
74ad70b
feat(models): compact embedding picker and clear retry error
alichherawalla Oct 1, 2026
1a29a7f
fix(auto-setup): select fitting video and speech models
alichherawalla Oct 1, 2026
7bb85dd
fix(downloads): retain partial files and reconcile completed models
alichherawalla Oct 1, 2026
d30343a
fix(settings): group image quality controls consistently
alichherawalla Oct 1, 2026
dd337f9
fix(chat): align video quick setting and require a model
alichherawalla Oct 1, 2026
47feee9
Merge origin/main into feature/local-video-generation
alichherawalla Oct 13, 2026
69bb7bd
Run local and remote video through the shared engine, job and polling…
alichherawalla Oct 13, 2026
9f3d0c6
Choose image weights, companions and samplers through the shared imag…
alichherawalla Oct 13, 2026
c602826
List and auto-set-up only the video models this phone can run
alichherawalla Oct 13, 2026
d4dd4ec
Get the mobile video branch green: tests follow the branch's settings…
alichherawalla Oct 13, 2026
13e2039
Log partial video and image files that cannot be removed instead of i…
alichherawalla Oct 13, 2026
22c7af8
The app can send opt-in analytics through the PostHog React Native SDK
alichherawalla Oct 23, 2026
bacf608
Analytics stays off until the person opts in, and opting out resets t…
alichherawalla Oct 23, 2026
e873c4f
Opted-in sessions report app opens, backgrounding and screens, and a …
alichherawalla Oct 23, 2026
4649789
Auto Setup offers an unchecked share-analytics row above every way ou…
alichherawalla Oct 23, 2026
c50c674
Report a problem shows the debug log, then shares it with support
alichherawalla Oct 23, 2026
b43a9fd
Settings has an Analytics section, and Reset All Settings turns analy…
alichherawalla Oct 23, 2026
9dde0a3
Opted-in sessions report how each text, vision, image and voice gener…
alichherawalla Oct 23, 2026
8f4214b
Opted-in sessions report model loads and load failures with the devic…
alichherawalla Oct 23, 2026
13fded2
Opted-in sessions report model download starts, finishes and failures
alichherawalla Oct 23, 2026
b08eea4
Opted-in sessions count chats, messages, regenerations, projects, doc…
alichherawalla Oct 23, 2026
d0be4a7
Remote server, image and voice setup report their funnel steps
alichherawalla Oct 23, 2026
6d797b5
The Pro upgrade funnel reports gates, the Pro page and activation
alichherawalla Oct 23, 2026
8fc1213
Release builds send opt-in analytics to the Off Grid AI PostHog EU pr…
alichherawalla Oct 23, 2026
9aeae0d
The debug log no longer holds prompts, replies, reasoning, transcript…
alichherawalla Oct 23, 2026
543e1c8
Privacy copy says chats and files never leave the device, and analyti…
alichherawalla Oct 23, 2026
65192b4
The iOS privacy manifest declares the opt-in analytics data, not link…
alichherawalla Oct 23, 2026
003c54e
Store listings and docs describe opt-in analytics instead of claiming…
alichherawalla Oct 23, 2026
936b66a
Message routing and stream parse logs keep lengths, never the typed t…
alichherawalla Oct 23, 2026
8e13832
Analytics persists through the app's AsyncStorage, and every event ca…
alichherawalla Oct 23, 2026
b56073c
Closing the app sends what analytics has queued and stops its timers
alichherawalla Oct 23, 2026
93b1e8e
Analytics journeys are proven on the real App: default sends nothing,…
alichherawalla Oct 23, 2026
05ce60c
Analytics drops failed boundary reads in one place, and a corrupt cra…
alichherawalla Oct 23, 2026
781823b
Every new analytics module is covered end to end and held to 100% cov…
alichherawalla Oct 23, 2026
7fc15bf
Downloads are not measured at all before opt-in, and a same-milliseco…
alichherawalla Oct 23, 2026
22959d0
Test settings fixtures carry the analytics consent fields, and the ST…
alichherawalla Oct 23, 2026
5f300d0
Core uses the Pro commits that report Pro funnels and keep tool conte…
alichherawalla Oct 23, 2026
bad3d80
A crash report keeps a file name only for the app's own code; a perso…
alichherawalla Oct 23, 2026
5b680e2
The SDK's own lifecycle events stay off, so no deep link URL reaches …
alichherawalla Oct 23, 2026
764dc36
Store builds keep the size-capped debug log, so Report a problem carr…
alichherawalla Oct 23, 2026
ba4a773
The analytics spec matches OGAD's review: document file types, and ho…
alichherawalla Oct 23, 2026
97e6b68
llama.cpp's per-token log lines stay out of the debug log, and quoted…
alichherawalla Oct 23, 2026
c2770c7
llama.cpp's per-node graph lines stay out of the debug log, so Report…
alichherawalla Oct 23, 2026
30b8f78
PR screenshots: every screen opt-in analytics changes, before and aft…
alichherawalla Oct 23, 2026
93ec4e8
Crash scrubbing checks file types with sets, not long regular express…
alichherawalla Oct 23, 2026
d3cfeb3
A consent change never rejects, and every screen that makes one handl…
alichherawalla Oct 23, 2026
8232294
The fallback loop keeps its name; the turn-level analytics wrapper ge…
alichherawalla Oct 23, 2026
a8f7197
Small readability fixes SonarCloud flagged in the analytics feature s…
alichherawalla Oct 23, 2026
bbbe5bb
The remote server funnel starts only when the editor opens, not when …
alichherawalla Oct 23, 2026
e9c7556
A regenerate is counted once, where it really runs
alichherawalla Oct 23, 2026
aeb2130
The PDF log line keeps sizes only, never the path that names the docu…
alichherawalla Oct 23, 2026
a4a5abc
A refused microphone ends voice input as stopped, not failed, and ask…
alichherawalla Oct 23, 2026
2120e37
Image model downloads report their catalog ID, not custom
alichherawalla Oct 23, 2026
5cdf181
Sending the debug log waits for a flush already writing
alichherawalla Oct 23, 2026
5113b01
Report a problem hides Clear, which would empty the view but not the …
alichherawalla Oct 23, 2026
b8a61a7
The offline claim names the features that use the network after download
alichherawalla Oct 23, 2026
afe1b38
Only a real trip to the background counts as leaving the app, so a co…
alichherawalla Oct 23, 2026
e8a5ee9
App open and background handling is internal to analytics; tests driv…
alichherawalla Oct 23, 2026
ef2d714
Merge remote-tracking branch 'origin/feature/opt-in-analytics' into r…
alichherawalla Oct 23, 2026
cb40117
Merge local video generation into the analytics and video release
alichherawalla Oct 23, 2026
2fb4ba8
A model that asked for the NPU but ran on the CPU reports zero offloa…
alichherawalla Oct 23, 2026
234bb88
Video joins opt-in analytics: generation, downloads, setup flow and f…
alichherawalla Oct 23, 2026
bb4ef39
A Desktop video made from a chat sends a video generation_finished an…
alichherawalla Oct 23, 2026
a57406e
The search model changer keeps its types private
alichherawalla Oct 23, 2026
9776579
Queue tests follow main's rule again: queued messages are sent togeth…
alichherawalla Oct 23, 2026
b349b8a
The features snapshot test covers video generation; SD packs report e…
alichherawalla Oct 23, 2026
d1a0522
Settings and Auto Setup tests reach the merged screens: backend in Ge…
alichherawalla Oct 23, 2026
fcb04c4
Analytics spec matches OGAD's copy word for word
alichherawalla Oct 23, 2026
8331583
A release build bundles the JS after the video runtime is prepared, s…
alichherawalla Oct 23, 2026
48fc231
Pro points at its release head: video stream reporting within the lin…
alichherawalla Oct 23, 2026
03d89f3
Screenshots of the screens this release changes, iPhone and Android, …
alichherawalla Oct 23, 2026
9b652c8
Merge remote-tracking branch 'origin/main' into release/analytics-video
alichherawalla Oct 23, 2026
2022f19
Analytics tests cover the video setup funnel and video model facts
alichherawalla Oct 24, 2026
ec86708
Scrub test covers a person's file name followed by punctuation
alichherawalla Oct 24, 2026
15d5994
Analytics spec: when each app reports a video model load (OGAM sends …
alichherawalla Oct 24, 2026
f3a8e6f
A queued video prompt is always its own turn; queued text still goes …
alichherawalla Oct 24, 2026
d51e111
Video is Pro: the public app keeps only hooks, slots and the saved-vi…
alichherawalla Oct 26, 2026
d99aa13
Seams for Video Studio: actions under video messages, Animate in the …
alichherawalla Oct 26, 2026
824c261
Video card has the chat card shadow; Pro's film module links only wit…
alichherawalla Oct 26, 2026
dd96a04
Analytics spec for video matches Off Grid Desktop word for word
alichherawalla Oct 26, 2026
70443f0
Phones start a clip from a photo, and Android upscales film shots
alichherawalla Oct 26, 2026
71f5ff2
Take back the noise session's unfinished Wan backend edits from the s…
alichherawalla Oct 26, 2026
a3f6bd8
make_film joins generate_video as a Pro chat tool; film cards keep th…
alichherawalla Oct 26, 2026
ceda310
Android Wan video diffusion runs on CPU, because Hexagon returns a co…
alichherawalla Oct 26, 2026
8bc6073
The search model shows everywhere the other models do
alichherawalla Oct 26, 2026
4c2da3a
Pro: start pictures carry real consent
alichherawalla Oct 26, 2026
47934e3
Fastest settings places every modality, not only text
alichherawalla Oct 26, 2026
ffab408
Fastest settings log line passes lint
alichherawalla Oct 26, 2026
bbb7a7c
Use the shared contract for film cards and Fastest settings results
alichherawalla Oct 26, 2026
4d40c9f
iOS upscales film shots with sd.cpp's ESRGAN
alichherawalla Oct 26, 2026
98be140
Extreme offers the largest models the phone can run, on every phone
alichherawalla Oct 26, 2026
79859b7
Speech models can be found on Hugging Face
alichherawalla Oct 26, 2026
9061db4
feat(models): recommend sd.cpp image models that fit the phone's memory
alichherawalla Oct 26, 2026
b7995df
fix(models): show an empty image search, not an error, when no checkp…
alichherawalla Oct 26, 2026
ac9574d
feat(video): Pro draws the chat clip card; Gallery's Videos opens the…
alichherawalla Oct 26, 2026
0b44ed8
Remove the design partner offer: Off Grid AI is a consumer product
alichherawalla Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions .dependency-cruiser.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ module.exports = {
from: { path: '^src/components/' },
to: { path: '^src/(screens|navigation)/' },
},
{
name: 'core-not-to-pro-code',
severity: 'error',
comment: 'Video, voice, sync and tools are Pro and live in the private pro/ submodule. Core reaches them only through bootstrap/hookRegistry, slotRegistry and screenRegistry, so the open-source build compiles and runs with pro/ absent. Add a hook or slot; never import pro/ (or @offgrid/pro/*) from src/.',
from: { path: '^src/' },
to: { path: '(^pro/|^@offgrid/pro/)' },
},
// ── Dead code ─────────────────────────────────────────────────────────────
{
name: 'no-orphans',
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,7 @@ docs/wire-captures/

# Scratch renders derived from scripts/e2e/fixtures (regenerable, not versioned)
/tmp/

# Generated native video runtime
.video-build/
native/video/*.xcframework/
28 changes: 23 additions & 5 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,21 @@
import { ErrorBoundary } from './src/components/ErrorBoundary';
import { DebugLogsScreen } from './src/components/DebugLogsScreen';
import { LoadingDots } from './src/components/LoadingDots';
import { AnalyticsConsentPrompt } from './src/components/AnalyticsConsentPrompt';
import {
startAnalytics,
trackScreen,
} from './src/services/analytics/analytics';
import { featureSnapshot } from './src/services/analytics/featureSnapshot';

LogBox.ignoreAllLogs(); // Suppress all logs

let stopStartupProbe: (() => void) | null = null;
// 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__) {
let stopAnalytics: (() => void) | null = null;
// Every build mirrors logger output into the in-app Debug Logs viewer and the size-capped log
// file, so Report a problem carries a trace from a store build too. Log lines hold metadata only,
// never chat content (docs/ANALYTICS.md, Report a problem). The console stays dev-only (logger.ts).
{
const fmt = (a: unknown): string => {
if (a instanceof Error) return `${a.name}: ${a.message}`;
if (typeof a === 'string') return a;
Expand All @@ -68,7 +76,7 @@
initDebugLogFile();
// Immediately after the sink exists, so the first sample lands before anything heavy runs. The app
// was being killed by iOS at launch with the log going silent half a second in; this says where it
// stops and what memory was doing when it did.
// stops and what memory was doing when it did. Dev builds only (the probe is a no-op elsewhere).
stopStartupProbe = startStartupMemoryProbe();
}

Expand Down Expand Up @@ -129,7 +137,7 @@
});
}

function App() {

Check warning on line 140 in App.tsx

View workflow job for this annotation

GitHub Actions / ci

Function 'App' has too many lines (366). Maximum allowed is 350
useEffect(() => () => {
stopStartupProbe?.();
stopStartupProbe = null;
Expand Down Expand Up @@ -373,6 +381,9 @@
logger.log('[BOOT] startup complete');
setIsInitializing(false);

// Settings are hydrated: analytics starts only now, and only if the person opted in.
stopAnalytics = startAnalytics(featureSnapshot);

// Reconcile downloaded Whisper models against disk at startup. presentModelIds
// isn't persisted (the filesystem is the source of truth), so it rehydrates
// empty — without this scan a freshly launched app shows an already-installed
Expand Down Expand Up @@ -406,6 +417,8 @@
return () => {
startupGeneration.current += 1;
stopNetworkReconnectWatcher();
stopAnalytics?.();
stopAnalytics = null;
};
}, [initializeApp]);

Expand Down Expand Up @@ -447,7 +460,11 @@
{AppRoot ? <AppRoot /> : null}
<NavigationContainer
ref={appNavigationRef}
onReady={applyPendingProRedirect}
onReady={() => {
applyPendingProRedirect();
trackScreen(appNavigationRef.getCurrentRoute()?.name);
}}
onStateChange={() => trackScreen(appNavigationRef.getCurrentRoute()?.name)}
theme={{
dark: isDark,
colors: {
Expand Down Expand Up @@ -480,6 +497,7 @@
>
<AppNavigator />
</NavigationContainer>
<AnalyticsConsentPrompt />
{__DEV__ && showSyncStatusBar ? <DevSyncStrip /> : null}
</SafeAreaProvider>
</GestureHandlerRootView>
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

### The Swiss Army Knife of On-Device AI

**Chat. Generate images. Use tools. See. Listen. All on your phone or Mac. All offline. Zero data leaves your device.**
**Chat. Generate images. Use tools. See. Listen. All on your phone or Mac. All offline. Your chats and files never leave your device.**

Anonymous usage data and crash reports only if you turn them on. See [what we share](docs/ANALYTICS.md).

[![GitHub stars](https://img.shields.io/github/stars/off-grid-ai/OGAM?style=social)](https://github.com/off-grid-ai/OGAM)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
Expand Down Expand Up @@ -223,7 +225,7 @@ Built on the shoulders of giants:

**Off Grid AI** — Your AI, your device, your data.

*No cloud. No data harvesting. Just AI that works anywhere.*
*No cloud. Your chats and files never leave your device. Just AI that works anywhere.*

[Join the Community on Slack](https://join.slack.com/t/off-grid-mobile/shared_invite/zt-494gbuv5c-kj9tT7U0AS8Kq8UUO63_jw)

Expand Down
13 changes: 8 additions & 5 deletions __tests__/hardening/batch9-diagnostics-debuglog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* (getDeviceTier thresholds + formatBytes are already COVERED-REAL in
* hardware.test.ts and are NOT duplicated here.)
*
* 2. debugLogFile.ts — the dev file sink's size-cap/rotation logic (flush appends, checks
* 2. debugLogFile.ts — the file sink's size-cap/rotation logic (flush appends, checks
* stat, and truncates to the newest half once over MAX_BYTES), the flush-at-50-lines
* trigger, the disabled no-op, and __DEV__/idempotent init. No existing test covers it.
* trigger, the disabled no-op, idempotent init, and the dev-only wire file. No existing test covers it.
* We stand up an in-memory RNFS so the REAL flush()/rotation runs end to end.
*/

Expand Down Expand Up @@ -109,7 +109,7 @@ describe('BATCH 9 — debugLogFile size-cap / rotation (real flush logic)', () =
: '';

beforeEach(() => {
(global as any).__DEV__ = true; // the sink is __DEV__-gated
(global as any).__DEV__ = true; // the wire file is __DEV__-gated
jest.resetModules(); // fresh module-level `enabled`/`buffer` state per test
fileSystem = require('../harness/nativeFileSystem').defaultNativeFileSystemBoundary;
fileSystem.reset();
Expand Down Expand Up @@ -182,13 +182,16 @@ describe('BATCH 9 — debugLogFile size-cap / rotation (real flush logic)', () =
expect(mod.getDebugLogPath()).toBe('/mock/documents/offgrid-debug.log');
});

it('initDebugLogFile is a no-op outside __DEV__', async () => {
it('a store build writes the log for Report a problem, but never the unbounded wire file', async () => {
(global as any).__DEV__ = false;
jest.resetModules();
fileSystem = require('../harness/nativeFileSystem').defaultNativeFileSystemBoundary;
const prodMod = require('../../src/utils/debugLogFile');
prodMod.initDebugLogFile();
prodMod.appendDebugLine('info', 'prod line');
prodMod.appendDebugLine('log', '[WIRE-STT] {"textLength":3}');
await jest.runOnlyPendingTimersAsync();
expect(await readLog()).toBe('');
expect(await readLog()).toContain('prod line');
expect(await fileSystem.exists(prodMod.getWireLogPath())).toBe(false);
});
});
139 changes: 139 additions & 0 deletions __tests__/harness/analyticsApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* The real App on the production navigation stack, for opt-in analytics journeys.
*
* Device boundaries only: the llama.rn engine, an in-memory disk holding one downloaded model, the
* RAM sensor, the OS app-state events, and the PostHog endpoint at `fetch`. A fresh install opens on
* Onboarding, so every journey passes Auto Setup the way a new user does.
*/
import { GB, installNativeBoundary, requireRTL, type NativeBoundary } from './nativeBoundary';
import { installPostHogEndpoint, type PostHogEndpoint } from './postHogEndpoint';
import { createDownloadedModel } from '../utils/factories';

type DevGlobal = { __DEV__?: boolean };

export interface AnalyticsApp {
boundary: NativeBoundary;
endpoint: PostHogEndpoint;
rtl: ReturnType<typeof requireRTL>;
view: ReturnType<ReturnType<typeof requireRTL>['render']>;
}

const originalDev = (global as DevGlobal).__DEV__;

/** Put `__DEV__` back after a test that ran as a release build. */
export function restoreBuildFlavor(): void {
(global as DevGlobal).__DEV__ = originalDev;
}

/**
* Launch the App. `release: true` runs it as a store build (`__DEV__` false), the only build that
* may start the analytics SDK. `relaunch: true` keeps the disk and storage of the previous launch.
*/
export async function launchApp(options: {
release: boolean;
relaunch?: boolean;
/** Installed before this version: onboarding was finished on an earlier launch. */
returningUser?: boolean;
platform?: 'ios' | 'android';
}): Promise<AnalyticsApp> {
(global as DevGlobal).__DEV__ = !options.release;
const boundary = installNativeBoundary({
llama: true,
fs: true,
ram: { platform: options.platform ?? 'android', totalBytes: 12 * GB, availBytes: 8 * GB },
});
const endpoint = installPostHogEndpoint();
const docs = boundary.fs!.DocumentDirectoryPath;
const model = {
...createDownloadedModel({
id: 'm',
name: 'Test Model',
fileName: 'small.gguf',
filePath: `${docs}/models/small.gguf`,
engine: 'llama',
fileSize: 500 * 1024 * 1024,
}),
// Where a Hugging Face download records it came from (a public repo).
origin: { repoId: 'unsloth/Qwen3-0.6B-GGUF', revision: 'main', path: 'small.gguf' },
};
// The disk a download leaves behind: the file and its record. Nothing else is pre-placed.
boundary.fs!.seedFile(model.filePath!, model.fileSize);
const storage = require('@react-native-async-storage/async-storage');
if (!options.relaunch) {
await storage.clear();
await storage.setItem('@local_llm/downloaded_models', JSON.stringify([model]));
if (options.returningUser) {
// What an earlier version left on disk: onboarding done, the setup tour dismissed.
await storage.setItem(
'local-llm-app-storage',
JSON.stringify({ state: { hasCompletedOnboarding: true, checklistDismissed: true }, version: 0 }),
);
}
}
const g = globalThis as unknown as { window?: Record<string, unknown> };
if (!g.window) g.window = { dispatchEvent: () => true, addEventListener: () => {}, removeEventListener: () => {} };
const React = require('react');
const rtl = requireRTL();
const App = require('../../App').default;
const view = rtl.render(React.createElement(App));
await rtl.waitFor(() => expect(view.queryByTestId('app-loading')).toBeNull(), { timeout: 10_000 });
return { boundary, endpoint, rtl, view };
}

/** A new user: Onboarding → Auto Setup (ticking the share row or not) → Skip for Now → Home. */
export async function passOnboarding(app: AnalyticsApp, share: boolean): Promise<void> {
const { view, rtl } = app;
rtl.fireEvent.press(await view.findByTestId('onboarding-skip'));
const skip = await view.findByTestId('auto-setup-skip', {}, { timeout: 10_000 });
const row = view.getByTestId('auto-setup-share-analytics');
expect(row.props.accessibilityState).toEqual({ checked: false });
if (share) {
rtl.fireEvent.press(row);
await rtl.waitFor(() =>
expect(view.getByTestId('auto-setup-share-analytics').props.accessibilityState).toEqual({ checked: true }),
);
}
rtl.fireEvent.press(skip);
await view.findByTestId('home-screen');
}

export async function openNewChat(app: AnalyticsApp): Promise<void> {
const { view, rtl } = app;
rtl.fireEvent.press(view.getByTestId('browse-models-button'));
rtl.fireEvent.press(await view.findByTestId('text-model-row-m'));
await rtl.waitFor(() => expect(view.queryByTestId('text-model-row-m')).toBeNull());
rtl.fireEvent.press(view.getByTestId('new-chat-button'));
await view.findByTestId('chat-input');
}

/** Type and send a message; the model's reply (scripted at the engine leaf) renders. */
export async function sendMessage(app: AnalyticsApp, prompt: string, reply: string): Promise<void> {
const { view, rtl, boundary } = app;
boundary.llama!.scriptCompletion({ text: reply });
rtl.fireEvent.changeText(view.getByTestId('chat-input'), prompt);
rtl.fireEvent.press(view.getByTestId('send-button'));
await view.findByText(reply, {}, { timeout: 10_000 });
await rtl.waitFor(() => expect(view.queryByTestId('stop-button')).toBeNull());
}

/** The person leaves the app: the OS moves it to the background. */
export async function sendToBackground(app: AnalyticsApp): Promise<void> {
await app.rtl.act(async () => {
app.boundary.emitAppStateChange('background');
});
}

export async function bringToForeground(app: AnalyticsApp): Promise<void> {
await app.rtl.act(async () => {
app.boundary.emitAppStateChange('active');
});
}

/** From a chat: back to the tabs, then the Settings tab. */
export async function openSettings(app: AnalyticsApp): Promise<void> {
const { view, rtl } = app;
const back = view.queryAllByLabelText('Back');
if (back.length > 0) rtl.fireEvent.press(back[0]);
rtl.fireEvent.press(await view.findByTestId('settings-tab'));
await view.findByTestId('settings-analytics');
}
95 changes: 95 additions & 0 deletions __tests__/harness/desktopVideoServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Off Grid AI Desktop as the video server, at the network boundary (fetch + the RNFS download).
*
* Wire shapes are Desktop's own (desktop/src/main/model-server.ts, startGatewayVideoJob and
* gatewayVideoJob): POST /v1/videos answers { request_id, status, poll_url }; GET /v1/videos/{id}
* is the poll { request_id, status, stage, progress, result, error }; a completed result is the
* shared VideoGenerationResultContract plus `url: /v1/videos/{id}/content`, which the phone
* downloads. The model switcher is /v1/models/catalog, /installed, /active and /activate.
*/
import type { NativeFileSystemBoundary } from './nativeFileSystem';

export const DESKTOP_VIDEO_ENDPOINT = 'http://192.168.1.62:7878'; // NOSONAR - private LAN test fixture
export const DESKTOP_VIDEO_MODEL = { id: 'wan2.1-1.3b', name: 'Wan 2.1 1.3B' };

export interface DesktopVideoJobs {
/** Every video submission Desktop received: the JSON body the phone sent. */
submissions: Array<Record<string, unknown>>;
restore: () => void;
}

export function serveDesktopVideoJobs(fs: NativeFileSystemBoundary): DesktopVideoJobs {
const original = globalThis.fetch;
const jobs = new Map<string, { body: Record<string, unknown>; polls: number }>();
let activeVideo: string | null = null;
const state: DesktopVideoJobs = {
submissions: [],
restore: () => {
globalThis.fetch = original;
},
};
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });

globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const href = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
const url = new URL(href);
if (url.origin !== DESKTOP_VIDEO_ENDPOINT) return original(input, init);
const method = (init?.method ?? 'GET').toUpperCase();
const route = `${method} ${url.pathname}`;
if (route === 'GET /v1/models') return json({ object: 'list', data: [] });
if (route === 'GET /v1/models/catalog') {
return json({
kinds: ['text', 'vision', 'image', 'video', 'speech', 'transcription'],
models: [{ ...DESKTOP_VIDEO_MODEL, kind: 'video', org: 'Wan-AI', tags: [], files: [] }],
});
}
if (route === 'GET /v1/models/installed') return json({ installed: [DESKTOP_VIDEO_MODEL.id] });
if (route === 'GET /v1/models/active') {
return json({ text: null, image: null, video: activeVideo, speech: null, transcription: null });
}
if (route === 'POST /v1/models/activate') {
const body = JSON.parse(String(init?.body ?? '{}')) as { id?: string; kind?: string };
const known = body.kind === 'video' && body.id === DESKTOP_VIDEO_MODEL.id;
if (known) activeVideo = DESKTOP_VIDEO_MODEL.id;
return json({ success: known });
}
if (route === 'POST /v1/videos') {
const body = JSON.parse(String(init?.body ?? '{}')) as Record<string, unknown>;
state.submissions.push(body);
const id = String(body.client_job_id);
jobs.set(id, { body, polls: 0 });
return json({ request_id: id, object: 'request', kind: 'video', status: 'queued', poll_url: `/v1/videos/${id}` }, 202);
}
const poll = url.pathname.match(/^\/v1\/videos\/([^/]+)$/);
if (poll && method === 'GET') {
const id = decodeURIComponent(poll[1]);
const job = jobs.get(id);
if (!job) return json({ error: { message: 'Video job was not found.', type: 'not_found' } }, 404);
job.polls += 1;
const base = { request_id: id, poll_url: `/v1/videos/${id}` };
if (job.polls === 1) {
return json({ ...base, status: 'running', stage: 'generating', progress: { step: 1, total: 4 } });
}
const contentUrl = `${DESKTOP_VIDEO_ENDPOINT}/v1/videos/${id}/content`;
fs.serveRemoteFile(contentUrl, { body: 'mp4-bytes' });
// Desktop returns its job output without the local path (VideoGenerationOutputContract).
const { prompt, negativePrompt, width, height, frames, fps, steps, guidance, seed } = job.body;
return json({
...base,
status: 'completed',
stage: 'encoding',
result: {
prompt, negativePrompt, width, height, frames, fps, steps, guidance, seed,
model: DESKTOP_VIDEO_MODEL.id,
durationSeconds: Number(frames) / Number(fps),
syncId: `desktop-video-${id}`,
durationMs: 4200,
url: `/v1/videos/${id}/content`,
},
});
}
return json({ error: { message: 'Unknown endpoint', type: 'not_found' } }, 404);
}) as typeof globalThis.fetch;
return state;
}
Loading
Loading