Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
142 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
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
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/
4 changes: 4 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { videoGenerationService } from './src/services/videoGenerationService';
import { videoProvider } from './src/services/modelDownloadService/providers/videoProvider';
/**
* Off Grid - On-Device AI Chat Application
* Private AI assistant that runs entirely on your device
Expand Down Expand Up @@ -121,7 +123,7 @@
);
}

function App() {

Check warning on line 126 in App.tsx

View workflow job for this annotation

GitHub Actions / ci

Function 'App' has too many lines (352). Maximum allowed is 350
useEffect(() => () => {
stopStartupProbe?.();
stopStartupProbe = null;
Expand Down Expand Up @@ -235,6 +237,8 @@
// becomes the SINGLE owner only once the Download Manager consumes the service
// and the old recovery paths are folded into the providers.
registerCoreDownloadProviders();
await videoProvider.reconcile?.().catch(error => logger.error('[Video] Download recovery failed:', error));
await videoGenerationService.restore().catch(error => logger.error('[Video] Job recovery failed:', error));

// Re-surface QUEUED downloads that never started before an app kill. A queued item (waiting for
// one of the 3 concurrency slots) has no native row, so hydrateDownloadStore can't recover it —
Expand Down
1 change: 1 addition & 0 deletions __tests__/hardening/batch7-ragProjectScoping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jest.mock('../../src/services/rag/database', () => ({
// Embedding native boundary: deterministic, direction-preserving unit vectors.
jest.mock('../../src/services/rag/embedding', () => ({
embeddingService: {
runExclusive: <T,>(operation: () => Promise<T>) => operation(),
isLoaded: jest.fn(() => true),
load: jest.fn(() => Promise.resolve()),
embed: jest.fn((text: string) => {
Expand Down
11 changes: 10 additions & 1 deletion __tests__/hardening/batch9-kb-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ function mockDeterministicVector(text: string): number[] {
}
jest.mock('../../src/services/rag/embedding', () => ({
embeddingService: {
runExclusive: <T,>(operation: () => Promise<T>) => operation(),
load: jest.fn(() => Promise.resolve()),
embed: jest.fn((text: string) => Promise.resolve(mockDeterministicVector(text))),
embedBatch: jest.fn((texts: string[]) => Promise.resolve(texts.map(mockDeterministicVector))),
Expand Down Expand Up @@ -199,7 +200,15 @@ describe('BATCH 9 — KB add → indexed → searchable round-trip (real sqlite
expect(mockMemDb._rows('rag_documents')[0]).toMatchObject({ id: docId, project_id: PROJECT, name: 'solar.txt', size: 4200, enabled: 1 });
expect(mockMemDb._rows('rag_chunks').length).toBeGreaterThan(0);
expect(mockMemDb._rows('rag_embeddings').length).toBe(mockMemDb._rows('rag_chunks').length);
expect(stages).toEqual(['extracting', 'chunking', 'indexing', 'embedding', 'done']);
// Embedding announces itself, then reports once per chunk.
const chunkCount = mockMemDb._rows('rag_chunks').length;
expect(stages).toEqual([
'extracting',
'chunking',
'indexing',
...Array.from({ length: chunkCount + 1 }, () => 'embedding'),
'done',
]);

// Case 15/16: the real filename + size come back from the list query.
const docs = await ragService.getDocumentsByProject(PROJECT);
Expand Down
4 changes: 2 additions & 2 deletions __tests__/integration/happy/imageBackends.happy.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* HAPPY-PATH (UI, BEHAVIORAL) — image generation across compute backends via the REAL ChatScreen: the user
* turns on the image-mode toggle (ON/force) and sends; the generated image's details show the correct
* backend label: NPU→"QNN (NPU)" · MNN(GPU)→"MNN (GPU)" · Metal(iOS)→"Core ML (ANE)".
* backend label: NPU→"QNN (NPU)" · MNN(GPU)→"MNN (GPU)" · Metal(iOS)→"Core ML".
*
* Only the native diffusion + LiteRT leaves are faked. The image model is DOWNLOADED (boundary) and ACTIVATED
* by the real toggle gesture (not setState). Generation details are turned on via the real toggle.
Expand All @@ -21,7 +21,7 @@ jest.mock('@react-navigation/native', () => ({
type Cfg = { label: string; backend: 'mnn' | 'qnn'; platform: 'ios' | 'android'; expected: string };
const CONFIGS: Cfg[] = [
{ label: 'MNN GPU (Android)', backend: 'mnn', platform: 'android', expected: 'MNN (GPU)' },
{ label: 'Metal (Core ML, iOS)', backend: 'mnn', platform: 'ios', expected: 'Core ML (ANE)' },
{ label: 'Metal (Core ML, iOS)', backend: 'mnn', platform: 'ios', expected: 'Core ML' },
];

describe('happy — image generation shows the correct backend label (heavy entry point)', () => {
Expand Down
12 changes: 11 additions & 1 deletion __tests__/integration/image/imageParameterPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,24 @@ describe('shared image parameter policy at the Mobile generation boundary', () =

it('uses current settings for both local and remote callers', () => {
expect(
resolveMobileImageParameters(model, {
resolveMobileImageParameters({ id: 'juggernaut-xl', name: 'Juggernaut XL', backend: 'sd' }, {
imageSteps: 42,
imageGuidanceScale: 5.5,
imageWidth: 768,
}),
).toEqual({ steps: 42, guidanceScale: 5.5, size: 768 });
});

it('keeps a native image pack within its fixed 512 size', () => {
expect(
resolveMobileImageParameters({ ...model, backend: 'mnn' }, {
imageSteps: 42,
imageGuidanceScale: 5.5,
imageWidth: 768,
}),
).toEqual({ steps: 42, guidanceScale: 5.5, size: 512 });
});

it('lets an explicit request win and falls back from damaged persisted values', () => {
expect(
resolveMobileImageParameters(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { handleModelSelectFn } from '../../../src/screens/ChatScreen/useChatMode
jest.mock('../../../src/services/llm');
jest.mock('../../../src/services/localDreamGenerator');
jest.mock('../../../src/services/hardware');
jest.mock('../../../src/utils/imageModelIntegrity', () => ({
jest.mock('../../../src/services/imageModelIntegrity', () => ({
validateImageModelDir: jest.fn(async () => ({ complete: true, missing: [] })),
ensureImageExtractionComplete: jest.fn(async () => {}),
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function ModelsSheet() {
labels={{
text: textModel ?? 'Local text',
image: imageModel ?? 'Local image',
video: 'Local video',
voice: mediaServers.voice ? 'Cloud voice' : 'Local voice',
speech: mediaServers.transcription ? 'Cloud speech' : 'Local speech',
}}
Expand Down
2 changes: 1 addition & 1 deletion __tests__/integration/models/imageDownloadRecovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const mockModelManager = {

// Integrity is a boundary for this recovery test (it exercises the resume/move
// orchestration, not the completeness rule — that has its own dedicated tests).
jest.mock('../../../src/utils/imageModelIntegrity', () => ({
jest.mock('../../../src/services/imageModelIntegrity', () => ({
validateImageModelDir: jest.fn(async () => ({ complete: true, missing: [] })),
ensureImageExtractionComplete: jest.fn(async () => {}),
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ const catalog: AutoSetupCatalogBoundaries = {
],
};

/** Open a plan's details; the device's own plan starts open, and its heading toggles. */
function openPlan(ui: ReturnType<typeof render>, tier: string): void {
const heading = ui.getByTestId(`auto-setup-plan-heading-${tier}`);
if (!heading.props.accessibilityState?.expanded) fireEvent.press(heading);
}

test('Auto Setup shows Absolute Reality when the device catalog would choose NAI Anime', async () => {
const navigation = { push() {}, replace() {} } as never;
const ui = render(
Expand All @@ -74,7 +80,7 @@ test('Auto Setup shows Absolute Reality when the device catalog would choose NAI
expect(ui.getByTestId('auto-setup-plan-lean')).toBeTruthy(),
);
for (const tier of ['lean', 'balanced', 'extreme']) {
fireEvent.press(ui.getByTestId(`auto-setup-plan-${tier}`));
openPlan(ui, tier);
expect(ui.getByText('Absolute Reality (NPU 8gen2)')).toBeTruthy();
expect(ui.queryByText('Absolute Reality (GPU)')).toBeNull();
expect(ui.queryByText('NAI Anime (NPU 8gen2)')).toBeNull();
Expand Down Expand Up @@ -104,7 +110,7 @@ test('Auto Setup offers the GPU model when the device recommends GPU', async ()
await waitFor(() =>
expect(ui.getByTestId('auto-setup-plan-balanced')).toBeTruthy(),
);
fireEvent.press(ui.getByTestId('auto-setup-plan-balanced'));
openPlan(ui, 'balanced');
expect(ui.getByText('Absolute Reality (GPU)')).toBeTruthy();
expect(ui.queryByText('Absolute Reality (NPU 8gen2)')).toBeNull();
});
Expand Down Expand Up @@ -143,7 +149,7 @@ test('Android Auto Setup presents complementary image models across its three pl
['balanced', 'Absolute Reality (NPU 8gen2)'],
['extreme', 'Dream Shaper V8 (NPU 8gen2)'],
]) {
fireEvent.press(ui.getByTestId(`auto-setup-plan-${tier}`));
openPlan(ui, tier);
expect(ui.getByText(name)).toBeTruthy();
}
});
23 changes: 18 additions & 5 deletions __tests__/integration/onboarding/autoSetupJourney.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class NativeDownloadBoundary implements DownloadProvider {
if (request.modelType === 'image') {
return this.begin(request.model.id, request.model.name, request.model.size);
}
if (request.modelType === 'video') {
const size = request.model.files.reduce((sum, file) => sum + (file.sizeBytes ?? 0), 0);
return this.begin(request.model.id, request.model.name, size);
}
return this.begin(request.modelId, request.modelId, 1 * MB);
}

Expand Down Expand Up @@ -218,16 +222,21 @@ describe('Auto Setup release journey', () => {
expect(ui.getByTestId('auto-setup-plan-balanced')).toBeTruthy(),
);

expect(ui.getAllByText('INCLUDES')).toHaveLength(1);
// Only the chosen plan is expanded.
expect(ui.getAllByTestId('auto-setup-selected-plan')).toHaveLength(1);
expect(ui.getByText('Gemma 4 E4B')).toBeTruthy();
fireEvent.press(ui.getByTestId('auto-setup-plan-lean'));
fireEvent.press(ui.getByTestId('auto-setup-plan-heading-lean'));
expect(ui.getByText('Qwen 3.5 2B')).toBeTruthy();
expect(ui.queryByText('SmolVLM 2B')).toBeNull();
fireEvent.press(ui.getByTestId('auto-setup-plan-extreme'));
fireEvent.press(ui.getByTestId('auto-setup-plan-heading-extreme'));
expect(ui.getByText('Qwen 3.5 9B')).toBeTruthy();
expect(ui.queryByText('Gemma 4 E4B')).toBeNull();
expect(useAppStore.getState().settings.modelLoadingMode).toBe('aggressive');
// A tier only previews downloads; it never changes the live loading policy.
expect(useAppStore.getState().settings.modelLoadingMode).toBe('balanced');

// Text is preselected; image and speech-to-text are opt-in.
fireEvent.press(ui.getByTestId('auto-setup-choice-image'));
fireEvent.press(ui.getByTestId('auto-setup-choice-stt'));
fireEvent.press(ui.getByTestId('auto-setup-download'));
await waitFor(() =>
expect(ui.getByTestId('auto-setup-continue')).toBeTruthy(),
Expand Down Expand Up @@ -285,6 +294,8 @@ describe('Auto Setup release journey', () => {
expect(ui.getByTestId('auto-setup-download')).toBeTruthy(),
);

fireEvent.press(ui.getByTestId('auto-setup-choice-image'));
fireEvent.press(ui.getByTestId('auto-setup-choice-stt'));
fireEvent.press(ui.getByTestId('auto-setup-download'));

await waitFor(() =>
Expand Down Expand Up @@ -314,6 +325,8 @@ describe('Auto Setup release journey', () => {
await waitFor(() =>
expect(ui.getByTestId('auto-setup-download')).toBeTruthy(),
);
fireEvent.press(ui.getByTestId('auto-setup-choice-image'));
fireEvent.press(ui.getByTestId('auto-setup-choice-stt'));
fireEvent.press(ui.getByTestId('auto-setup-download'));
await waitFor(() =>
expect(ui.getAllByText(/STARTING|0%/).length).toBeGreaterThan(0),
Expand Down Expand Up @@ -364,7 +377,7 @@ describe('Auto Setup release journey', () => {
await waitFor(() =>
expect(ui.getAllByText(/STARTING|0%/).length).toBeGreaterThan(0),
);
fireEvent.press(ui.getByTestId('auto-setup-plan-extreme'));
fireEvent.press(ui.getByTestId('auto-setup-plan-heading-extreme'));

expect(ui.getByText('Gemma 4 E4B')).toBeTruthy();
expect(ui.queryByText('Qwen 3.5 9B')).toBeNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ describe('the live Pro expiry redirect', () => {

expect(resetRoot).toHaveBeenCalledTimes(1);
expect(resetRoot).toHaveBeenCalledWith({
index: 0,
routes: [{ name: 'ProDetail' }],
index: 1,
routes: [{ name: 'Main', params: { screen: 'HomeTab' } }, { name: 'ProDetail' }],
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ describe('Pro pricing reached from Settings (rendered, real App)', () => {
expect(view.queryByText(/\$119/)).toBeNull();
expect(view.queryByText('Intelligence, democratized.')).toBeNull();

// The Settings upsell banner (its LIVE SYNC feature grid) carries its own "Get Pro" button.
// The Settings upsell banner carries its own "Get Pro" button.
// Home stays mounted behind the tabs with a "Get Pro" of its own, so scope to the banner.
let banner = view.getByText('LIVE SYNC').parent;
let banner = view.getByText('Keep your work in context with memory, Sync, and actions you approve.').parent;
while (banner && rtl.within(banner).queryAllByText('Get Pro').length === 0) {
banner = banner.parent;
}
Expand Down
4 changes: 3 additions & 1 deletion __tests__/integration/rag/embeddingFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const deterministicEmbed = (text: string): number[] => {

jest.mock('../../../src/services/rag/embedding', () => ({
embeddingService: {
runExclusive: <T,>(operation: () => Promise<T>) => operation(),
load: jest.fn(() => Promise.resolve()),
embed: jest.fn((text: string) => Promise.resolve(deterministicEmbed(text))),
embedBatch: jest.fn((texts: string[]) => Promise.resolve(texts.map(deterministicEmbed))),
Expand Down Expand Up @@ -90,7 +91,8 @@ describe('Embedding Flow Integration', () => {

// Verify embedding service was called
expect(embeddingService.load).toHaveBeenCalled();
expect(embeddingService.embedBatch).toHaveBeenCalled();
// Each chunk is embedded on its own so indexing can report per-chunk progress.
expect(embeddingService.embed).toHaveBeenCalled();

// Verify embeddings were inserted into the database
const embInserts = mockExecuteSync.mock.calls.filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('Ollama over HTTPS with a saved key', () => {

// Back on Home, the picker lists the Ollama's model.
fireEvent.press(ui.getByLabelText('Back'));
await waitFor(() => expect(ui.getByText('Select a text or image model to start')).toBeTruthy());
await waitFor(() => expect(ui.getByText('Select a text, image, or video model to start')).toBeTruthy());
fireEvent.press(ui.getByTestId('browse-models-button'));
await waitFor(() => expect(ui.getByText(MODEL)).toBeTruthy());
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,20 @@ it('offers only CPU and GPU in Chat and Model Settings even with a saved NPU pre
const { useAppStore } = require('../../../src/stores');
await useAppStore.persist.rehydrate();
const { GenerationSettingsModal } = require('../../../src/components/GenerationSettingsModal');
const { TextGenerationSection } = require('../../../src/screens/ModelSettingsScreen/TextGenerationSection');
const { GeneralGenerationControls } = require('../../../src/components/settings/GeneralGenerationControls');
const model = createDownloadedModel({ id: 'gemma-litert', engine: 'litert', filePath: '/models/gemma.litertlm' });
useAppStore.getState().setDownloadedModels([model]);
useAppStore.getState().setActiveModelId(model.id);
useAppStore.getState().updateSettings({ liteRTBackend: 'npu' });
const chat = render(React.createElement(GenerationSettingsModal, { visible: true, onClose: () => {} }));
fireEvent.press(chat.getByText('TEXT GENERATION'));
fireEvent.press(chat.getByTestId('modal-text-advanced-toggle'));
// Acceleration is a General control on both surfaces.
fireEvent.press(chat.getByTestId('modal-general-accordion'));
expect(chat.queryByText('NPU (Beta)')).toBeNull();
expect(chat.getByText(/Run on GPU via OpenCL/)).toBeTruthy();
fireEvent.press(chat.getByText('CPU'));
expect(useAppStore.getState().settings.liteRTBackend).toBe('cpu');
chat.unmount();
const settings = render(React.createElement(TextGenerationSection));
fireEvent.press(settings.getByTestId('text-advanced-toggle'));
const settings = render(React.createElement(GeneralGenerationControls));
expect(settings.queryByText('NPU (Beta)')).toBeNull();
fireEvent.press(settings.getByText('GPU'));
expect(useAppStore.getState().settings.liteRTBackend).toBe('gpu');
Expand Down
34 changes: 34 additions & 0 deletions __tests__/integration/settings/modelSettingsSurfaceParity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,40 @@ describe('model settings surface parity', () => {
_clearSlotsForTesting();
});

it('keeps shared controls in General on both surfaces and preserves saved values', () => {
useAppStore.getState().updateSettings({
modelLoadingMode: 'aggressive',
showGenerationDetails: true,
});
const chatSettings = render(<GenerationSettingsModal visible onClose={() => {}} />);
fireEvent.press(chatSettings.getByText('TEXT GENERATION'));
expect(chatSettings.queryByText('Model Loading')).toBeNull();
expect(chatSettings.queryByText('Show Generation Details')).toBeNull();
fireEvent.press(chatSettings.getByTestId('modal-general-accordion'));
expect(chatSettings.getByText('Text Model Backend')).toBeTruthy();
expect(chatSettings.getByTestId('model-loading-mode-aggressive-button')).toBeTruthy();
expect(chatSettings.getByTestId('show-gen-details-on-button')).toBeTruthy();
chatSettings.unmount();

const modelSettings = renderModelSettings();
fireEvent.press(modelSettings.getByTestId('text-generation-accordion'));
expect(modelSettings.queryByText('Model Loading')).toBeNull();
fireEvent.press(modelSettings.getByTestId('general-generation-accordion'));
expect(modelSettings.getByText('Text Model Backend')).toBeTruthy();
expect(modelSettings.getByTestId('model-loading-mode-aggressive-button')).toBeTruthy();
expect(modelSettings.getByTestId('show-gen-details-on-button')).toBeTruthy();
});

it('shows and edits the saved default system prompt in chat Text settings', () => {
useAppStore.getState().updateSettings({ systemPrompt: 'Saved instructions' });
const chatSettings = render(<GenerationSettingsModal visible onClose={() => {}} />);
fireEvent.press(chatSettings.getByText('TEXT GENERATION'));
const input = chatSettings.getByTestId('default-system-prompt-input');
expect(input.props.value).toBe('Saved instructions');
fireEvent.changeText(input, 'New instructions');
expect(useAppStore.getState().settings.systemPrompt).toBe('New instructions');
});

it('caps output by context on both surfaces and writes one shared setting state', () => {
useAppStore.getState().setModelMaxContext(262144);
// A context wide enough for the output this test chooses. Max tokens is capped BY the context,
Expand Down
7 changes: 5 additions & 2 deletions __tests__/pro/sync/stateSync.integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,12 @@ describe('Pro mobile state sync journey', () => {
created_at: createdAt,
},
);
// Twenty real edits: a put that repeats the current value is not a new op, so each revision
// carries its own value and the Desktop's final 0.55 is genuinely later than the phone's
// own first-run settings.
for (let revision = 0; revision < 20; revision += 1) {
remoteLog.record(CORE_SYNC_ENTITIES.modelSetting, 'temperature', 'put', {
value_json: revision === 19 ? '0.55' : '0.5',
value_json: revision === 19 ? '0.55' : (0.3 + revision * 0.01).toFixed(2),
});
}

Expand All @@ -318,7 +321,7 @@ describe('Pro mobile state sync journey', () => {
);
fireEvent.press(ui.getByTestId('settings-tab'));
fireEvent.press(ui.getByText('Model Settings'));
fireEvent.press(ui.getByTestId('text-generation-accordion'));
fireEvent.press(ui.getByTestId('general-generation-accordion'));
fireEvent.press(ui.getByTestId('show-gen-details-on-button'));
fireEvent.press(ui.getByLabelText('Back'));
fireEvent.press(await waitFor(() => ui!.getByTestId('open-sync-settings')));
Expand Down
Loading
Loading