Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
156 commits
Select commit Hold shift + click to select a range
9f3de0e
test(mcp): cover the "Connect desktop tools" row + bump pro pointer
siddWednesday Aug 19, 2026
d48de0f
feat(mcp): QR pairing scanner deps + tests + pro bump
siddWednesday Aug 20, 2026
4b1c450
test(mcp): drop the vision-camera scanner mock + scan-button test
siddWednesday Aug 20, 2026
c256d0f
test(sync): cover scan-to-pair + restore the vision-camera mock
siddWednesday Aug 20, 2026
ce0b1ee
test(sync): guard that opening the scanner hides the pairing sheet
siddWednesday Aug 20, 2026
81da658
chore(pro): bump mobile-pro - QR scanner overlay fix
siddWednesday Aug 20, 2026
0445d6f
test(mcp): cover the Pro tools desktop-tools grant + bump mobile-pro
siddWednesday Aug 20, 2026
c61a8e8
chore(pro): bump mobile-pro - offline-forget fix
siddWednesday Aug 20, 2026
b1fb265
test(mcp): drop the desktop-section sheet tests + bump mobile-pro
siddWednesday Aug 21, 2026
ca2b0e2
chore(pro): bump mobile-pro - revert offline-forget (conflicts with i…
siddWednesday Aug 21, 2026
61f6a28
test(mcp): stub CompanionToolsSection in the McpServersScreen suite
siddWednesday Aug 21, 2026
87adccb
feat(chat): chatOverlay slot for the pending computer-use approval card
siddWednesday Aug 21, 2026
8bd93e5
test(sync): send Whisper from mobile to Mac
alichherawalla Aug 22, 2026
4df5485
test(chat): render clean mobile previews
alichherawalla Aug 22, 2026
37bd5d7
test(sync): allow real Whisper transfer to settle
alichherawalla Aug 22, 2026
9b90bb3
fix(pro): remove device licence shortcut
alichherawalla Aug 22, 2026
f294ca6
test(pro): prove Home to Pro journey
alichherawalla Aug 22, 2026
6d04b00
test(sync): prove licence startup timer cleanup
alichherawalla Aug 22, 2026
b822c75
fix(app): release startup diagnostics on teardown
alichherawalla Aug 22, 2026
098cb48
fix(app): keep probe stop private
alichherawalla Aug 22, 2026
668fe3c
fix(home): retry cancelled LAN discovery
alichherawalla Aug 22, 2026
921489f
ci(test): allow the full Jest suite heap
alichherawalla Aug 22, 2026
b6110f8
test(sync): allow the full transfer journey
alichherawalla Aug 22, 2026
c543467
test(ci): serialize related rendered journeys
alichherawalla Aug 22, 2026
e261317
fix(chat): show stored messages immediately
alichherawalla Aug 22, 2026
229394f
Merge remote-tracking branch 'origin/main' into feat/companion-deskto…
siddWednesday Aug 23, 2026
85290b0
feat(speech): add selectable multilingual voices
alichherawalla Aug 24, 2026
6de04bc
fix(chat): open links and enable back gestures
alichherawalla Aug 24, 2026
ab062a5
test(sync): cover unified model transfer progress
alichherawalla Aug 24, 2026
ca9cce9
test(audio): await recording cleanup
alichherawalla Aug 24, 2026
c255b9b
chore(ui): keep settings selector private
alichherawalla Aug 24, 2026
b3e5a97
fix(ci): finish passing related test gate
alichherawalla Aug 24, 2026
540e568
fix(audio): await realtime reset teardown
alichherawalla Aug 24, 2026
49d6ae9
test(chat): prove immediate stored message display
alichherawalla Aug 24, 2026
3edb596
fix(markdown): validate every rendered link
alichherawalla Aug 24, 2026
942d521
fix(home): wait for remote settings hydration
alichherawalla Aug 24, 2026
c4a081e
refactor(voice): default decode language at boundary
alichherawalla Aug 24, 2026
2791203
fix(audio): keep fallback teardown in reset barrier
alichherawalla Aug 24, 2026
102ce93
feat(sync): add private mesh runtime controls
alichherawalla Aug 24, 2026
d92caee
fix(stt): align capture with the selected model
alichherawalla Aug 24, 2026
5613980
feat(chat): support tap or hold mic recording
alichherawalla Aug 24, 2026
439c5e4
fix(remote): allow private Tailscale endpoints
alichherawalla Aug 24, 2026
8aa9178
refactor(chat): place voice mode with header actions
alichherawalla Aug 24, 2026
e132688
refactor(pro): use the shared activation sheet
alichherawalla Aug 24, 2026
5082298
chore(pro): update voice and private mesh features
alichherawalla Aug 24, 2026
dc96757
chore: bump mobile-pro (desktop tools controlled entirely by device s…
siddWednesday Aug 24, 2026
bdcac8d
refactor(mobile): clear release quality findings
alichherawalla Aug 24, 2026
0646bb3
test(voice): align release expectations
alichherawalla Aug 24, 2026
f3cea4d
test(chat): await generation cleanup
alichherawalla Aug 24, 2026
d917a9b
test(chat): budget heavy coverage journeys
alichherawalla Aug 24, 2026
9b01076
test(chat): centralize the heavy journey budget
alichherawalla Aug 24, 2026
b1da03a
fix(ios): separate proximity advertising lifecycle
alichherawalla Aug 24, 2026
e2c4a5e
fix(sync): honor hidden state on iOS
alichherawalla Aug 24, 2026
39382ea
test(integration): budget native UI journeys
alichherawalla Aug 24, 2026
5f0abfd
docs: explain Personal Mesh connection controls
alichherawalla Aug 24, 2026
b4c2857
chore: update Pro CI policy pointer
alichherawalla Aug 24, 2026
a25e116
feat(audio): surface voice downloads
alichherawalla Aug 24, 2026
c7a681d
test(audio): verify runtime voice selection
alichherawalla Aug 24, 2026
d5402b9
test(audio): verify cached language reuse
alichherawalla Aug 24, 2026
abba61a
Merge remote-tracking branch 'origin/feat/companion-desktop-mcp-tools…
alichherawalla Aug 25, 2026
31a3c5d
fix(sync): fall back when native upload stalls
alichherawalla Aug 25, 2026
c54ee6a
chore(pro): include transfer cancellation state
alichherawalla Aug 25, 2026
dfbae6c
fix(remote): auto-recover the remote connection on network change
Anurag-Wednesday Aug 25, 2026
38293d4
fix(chat): honor new chat model and context
alichherawalla Aug 25, 2026
5c711e2
feat(sync): receive action approvals
alichherawalla Aug 25, 2026
d060279
refactor(actions): use one approval sync path
alichherawalla Aug 25, 2026
d628c3a
test(actions): prove chat-only remote approval
alichherawalla Aug 25, 2026
e7b0e09
feat(downloads): measure canonical mobile progress
alichherawalla Aug 25, 2026
6c88515
feat(downloads): show progress across mobile flows
alichherawalla Aug 25, 2026
51d6e16
fix(pro): revoke mobile runtime on expiry
alichherawalla Aug 26, 2026
055bd61
fix(runtime): harden mobile pro service boundaries
alichherawalla Aug 26, 2026
b14d71c
test(sync): cover mobile pro device journeys
alichherawalla Aug 26, 2026
80c63a9
build(pro): update mobile pro dependency
alichherawalla Aug 26, 2026
396f601
test(models): keep explicit remote selection authoritative
alichherawalla Aug 26, 2026
96f076d
test(pro): cover live entitlement teardown
alichherawalla Aug 26, 2026
f7a2ce1
build(pro): update mobile pro dependency
alichherawalla Aug 26, 2026
8a44159
test(pro): seed authoritative access state
alichherawalla Aug 26, 2026
bbc0d14
test(sync): balance uploader deadline signal
alichherawalla Aug 26, 2026
d32f67b
ci(shared): build mobile ui dependency
alichherawalla Aug 26, 2026
b2cc88c
test(pro): seed email calendar entitlement
alichherawalla Aug 26, 2026
0574297
ci(pro): build shared ui in private verification
alichherawalla Aug 26, 2026
4ee42a2
ci: allow manual verification runs
alichherawalla Aug 26, 2026
783e12d
fix(sync): use connected-device mesh service
alichherawalla Aug 26, 2026
bf0bde0
test(sync): verify compacted mobile startup
alichherawalla Aug 26, 2026
234de43
fix(quality): address mobile analysis findings
alichherawalla Aug 26, 2026
075da9f
feat(settings): let you cap what the model spends on thinking
alichherawalla Aug 27, 2026
8580dcf
feat(community): ask for a store rating instead of an X share
alichherawalla Aug 28, 2026
6cac76a
docs(release): check the store rating rows instead of the X share
alichherawalla Aug 28, 2026
904e2c8
fix(tests): update the share sheet for rating, unblock two ambiguous …
alichherawalla Aug 28, 2026
32b7687
chore: keep the store URLs private
alichherawalla Aug 28, 2026
cedc7de
ci(shared): build mobile model contracts
alichherawalla Aug 28, 2026
393a3fd
build(pro): update mobile pro verification
alichherawalla Aug 28, 2026
6df0b89
fix(sync): stop mesh service on Android timeout
alichherawalla Aug 28, 2026
b856b02
fix(sync): prevent unowned mesh service restarts
alichherawalla Aug 28, 2026
f6c407d
feat(chat): bind remote tools to their conversation
alichherawalla Aug 28, 2026
be237e3
feat(onboarding): add device-fit auto setup
alichherawalla Aug 28, 2026
a8b7173
refactor(onboarding): enforce setup ownership
alichherawalla Aug 28, 2026
a9d2910
test(tasks): prove remote task chat journey
alichherawalla Aug 28, 2026
7bc326b
test(onboarding): prove Voice follows Pro activation
alichherawalla Aug 28, 2026
1f2fd77
build(pro): update task card architecture
alichherawalla Aug 28, 2026
9011cc5
refactor(models): move download owners to services
alichherawalla Aug 28, 2026
981adcd
test(onboarding): prove auto setup journey
alichherawalla Aug 28, 2026
b142a2a
fix(onboarding): compare every auto setup plan
alichherawalla Aug 28, 2026
01a64a1
fix(onboarding): expand the selected setup plan
alichherawalla Aug 28, 2026
c99f857
feat(onboarding): scale setup models by tier
alichherawalla Aug 28, 2026
cba8868
test(tasks): prove remote control and desktop routing
alichherawalla Aug 28, 2026
bae1c34
docs(tasks): record mobile task gate evidence
alichherawalla Aug 28, 2026
bdfe9e4
test(models): fake the Hugging Face boundary
alichherawalla Aug 28, 2026
8dad946
test(models): refresh curated vision fixtures
alichherawalla Aug 28, 2026
4477e23
test(onboarding): follow auto setup journey
alichherawalla Aug 28, 2026
d2750c7
fix(models): remove duplicate type import
alichherawalla Aug 28, 2026
fa4a3e7
test(tasks): prove synced session playback
alichherawalla Aug 28, 2026
2b5fa13
test(tasks): keep task views in chat tools
alichherawalla Aug 28, 2026
12fce08
fix(sync): retry Android mesh residency safely
alichherawalla Aug 28, 2026
b5970a5
test(tasks): verify mobile sync readiness and replay
alichherawalla Aug 28, 2026
14aa4f1
test(tasks): verify one mobile replay surface
alichherawalla Aug 28, 2026
13a46f5
fix(models): keep download alerts below UI layer
alichherawalla Aug 29, 2026
fd7d4f2
chore(mobile): keep release exports in use
alichherawalla Aug 29, 2026
3de418c
refactor(chat): remove unused approval surface
alichherawalla Aug 29, 2026
c6e7987
test(tasks): verify mobile routing and early sync
alichherawalla Aug 29, 2026
562f9b3
fix(chat): remove retired approval import
alichherawalla Aug 29, 2026
99a6d1f
refactor(onboarding): expose auto setup boundaries
alichherawalla Aug 29, 2026
e49f66d
test(release): verify setup and replay journeys
alichherawalla Aug 29, 2026
57c0a5c
fix(onboarding): own cancellable auto setup sessions
alichherawalla Aug 29, 2026
29ecccd
fix(chat): preflight remote models before tool turns
alichherawalla Aug 29, 2026
5e09d4d
fix(sync): report Android residency degradation
alichherawalla Aug 29, 2026
313293d
test(tasks): prove secure mobile task journeys
alichherawalla Aug 29, 2026
ee59fbc
chore(pro): update secure task sync
alichherawalla Aug 29, 2026
f37df57
chore(pro): update bounded sync shutdown
alichherawalla Aug 29, 2026
fdcc6d3
test(tasks): install companion routing boundaries
alichherawalla Aug 29, 2026
db1c655
test(tasks): install MCP task sync boundaries
alichherawalla Aug 29, 2026
43143af
refactor(mobile): keep setup and residency types private
alichherawalla Aug 29, 2026
a1e3c9e
fix(tasks): render synced preview loading feedback
alichherawalla Aug 29, 2026
8741abb
build(mobile): install shared automation contracts
alichherawalla Aug 29, 2026
ec87372
chore(mobile): update Pro task controls
alichherawalla Aug 29, 2026
b77df49
test(tasks): cover full-screen session playback
alichherawalla Aug 29, 2026
bc12900
fix(metro): resolve shared automation package
alichherawalla Aug 29, 2026
061a2dd
fix(sync): settle shared files after reconnect
alichherawalla Aug 30, 2026
7b5523d
feat(sync): add resilient mobile QR pairing and activation
alichherawalla Aug 30, 2026
0455029
feat(tasks): add mobile companion controls and guidance
alichherawalla Aug 30, 2026
adc44c8
feat(models): add mobile remote media and server parity
alichherawalla Aug 30, 2026
e9abcc6
feat(pro): refresh capability and sharing prompts
alichherawalla Aug 30, 2026
db2f7a9
docs(release): update Mobile Release 107 QA
alichherawalla Aug 30, 2026
3ce9a85
fix(models): clear discovery request timers
alichherawalla Aug 30, 2026
dc45bd5
test: align mobile release journeys
alichherawalla Aug 30, 2026
712322b
chore(pro): update release test boundary
alichherawalla Aug 30, 2026
f5c4cec
Merge remote-tracking branch 'origin/feat/remote-reconnect' into rele…
alichherawalla Aug 30, 2026
0d447ae
fix(chat): remove duplicate tool aliases
alichherawalla Aug 30, 2026
1263ac0
chore(mobile): remove redundant public surface
alichherawalla Aug 30, 2026
d73d162
fix(reconnect): close mobile recovery races
alichherawalla Aug 30, 2026
908044b
fix(release): close mobile review blockers
alichherawalla Aug 30, 2026
8ce188b
test(ci): budget heavy rendered journeys
alichherawalla Aug 30, 2026
0381f91
refactor(release): close sonar maintainability findings
alichherawalla Aug 30, 2026
fc186f9
fix(release): close final mobile review findings
alichherawalla Aug 30, 2026
261da4e
test(ci): keep boot proof stable under coverage
alichherawalla Aug 30, 2026
2076e18
refactor(release): simplify whisper cancellation owner
alichherawalla Aug 30, 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
18 changes: 15 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { hardwareService, modelManager, authService, ragService, remoteServerMan
import logger from './src/utils/logger';
import { useAppStore, useAuthStore, useRemoteServerStore, useWhisperStore } from './src/stores';
import { useDebugLogsStore } from './src/stores/debugLogsStore';
import { initDebugLogFile, appendDebugLine } from './src/utils/debugLogFile';
import { initDebugLogFile, appendDebugLine, stopDebugLogFile } from './src/utils/debugLogFile';
import { startStartupMemoryProbe } from './src/services/startupMemoryProbe';
import { loadProFeatures } from './src/bootstrap/loadProFeatures';
import { hydrateDownloadStore } from './src/services/downloadHydration';
Expand All @@ -34,6 +34,8 @@ import { ErrorBoundary } from './src/components/ErrorBoundary';

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__) {
Expand All @@ -60,7 +62,7 @@ if (__DEV__) {
// 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.
startStartupMemoryProbe();
stopStartupProbe = startStartupMemoryProbe();
}

const ensureRemoteServerStoreHydrated = async () => {
Expand All @@ -72,6 +74,12 @@ const ensureRemoteServerStoreHydrated = async () => {
};

function App() {
useEffect(() => () => {
stopStartupProbe?.();
stopStartupProbe = null;
stopDebugLogFile();
}, []);

useDownloadListeners();
// Reactive: when Pro is activated at runtime (license key → loadProFeatures),
// the appRoot slot (TTS engine bridge) registers and this re-renders to mount
Expand Down Expand Up @@ -205,7 +213,11 @@ function App() {
})().catch((error) => {
logger.error('[App] Download-state recovery failed:', error);
});
}, [setDownloadedModels, setDownloadedImageModels]);
}, [
reattachTextDownloadRecovery,
setDownloadedModels,
setDownloadedImageModels,
]);

const initializeApp = useCallback(async () => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,61 @@
* artifact: the boot loader ('app-loading') CLEARS anyway. RED on HEAD: the loader stays
* forever because initializeApp awaits hydrateDownloadStore/reattach before first paint.
*/
import { installNativeBoundary, requireRTL } from '../../harness/nativeBoundary';
import {
installNativeBoundary,
requireRTL,
} from '../../harness/nativeBoundary';

jest.mock('react-native-bootsplash', () => ({ hide: jest.fn(async () => {}) }), { virtual: true });
jest.mock(
'react-native-bootsplash',
() => ({ hide: jest.fn(async () => {}) }),
{ virtual: true },
);

describe('app boot is not blocked by the download DB (rendered)', () => {
it('clears the boot loader while getActiveDownloads never resolves (wedged download DB)', async () => {
const boundary = installNativeBoundary();
it('clears the boot loader while getActiveDownloads remains wedged', async () => {
installNativeBoundary();


const React = require('react');
const rtl = requireRTL();
const { NativeModules } = require('react-native');
// WEDGE the download DB: the native read never resolves (the device's 9-writer contention,
// taken to the limit). Everything else on the boundary behaves normally.
const AsyncStorage = require('@react-native-async-storage/async-storage');
await AsyncStorage.clear();
// WEDGE the download DB through first paint. The boundary is released only after the visible
// boot outcome is proved, so the test remains faithful without leaking an immortal Promise.
let releaseDownloadDb!: (rows: unknown[]) => void;
const pendingDownloadDb = new Promise<unknown[]>(resolve => {
releaseDownloadDb = resolve;
});
NativeModules.DownloadManagerModule = {
...NativeModules.DownloadManagerModule,
getActiveDownloads: jest.fn(() => new Promise(() => {})),
getActiveDownloads: jest.fn(() => pendingDownloadDb),
};
const App = require('../../../App').default;


const view = rtl.render(React.createElement(App));

// Precondition (anti-false-green): the boot loader genuinely renders first.
expect(view.queryByTestId('app-loading')).not.toBeNull();

// Terminal artifact: the loader clears even though the download DB never answered.
await rtl.waitFor(() => { expect(view.queryByTestId('app-loading')).toBeNull(); }, { timeout: 8000 });
await rtl.waitFor(
() => {
expect(view.queryByTestId('app-loading')).toBeNull();
},
{ timeout: 8000 },
);

await rtl.act(async () => {
releaseDownloadDb([]);
await pendingDownloadDb;
});
await rtl.waitFor(() => {
expect(
NativeModules.DownloadManagerModule.getActiveDownloads,
).toHaveBeenCalledTimes(2);
});

view.unmount();
void boundary;
}, 20000);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* The Pro pitch has one purchase path and one key-entry path. A second action that offered to use
* another device's licence duplicated the Sync journey and made the purchase screen ambiguous.
*
* This test enters through the real Home screen and real app navigation. Native rendering remains
* supplied by the Jest environment, but every Off Grid screen, store action, and route is real.
*/
import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { AppNavigator } from '../../../src/navigation/AppNavigator';
import { useAppStore } from '../../../src/stores/appStore';
import { createDeviceInfo, createDownloadedModel } from '../../utils/factories';

describe('Pro entry from Home', () => {
beforeEach(() => {
const model = createDownloadedModel();
const app = useAppStore.getState();

// Use the production store actions to restore a returning user's local app state. The behavior
// under test starts with their real Home-screen gesture below.
app.setOnboardingComplete(true);
app.setDeviceInfo(createDeviceInfo());
app.setDownloadedModels([model]);
app.setActiveModelId(model.id);
});

afterEach(() => {
const app = useAppStore.getState();
app.setActiveModelId(null);
app.setDownloadedModels([]);
app.setOnboardingComplete(false);
});

it('opens the Pro screen with its two valid actions and no device-licence shortcut', async () => {
const ui = render(
<NavigationContainer>
<AppNavigator />
</NavigationContainer>,
);

fireEvent.press(await ui.findByLabelText('Open Off Grid AI Pro'));

await waitFor(() => expect(ui.getByText('Off Grid AI Pro')).toBeTruthy());
expect(ui.getAllByText('Get Pro').length).toBeGreaterThan(0);
expect(ui.getByText('I have a license key')).toBeTruthy();
expect(ui.queryByText('Use Pro from another device')).toBeNull();

ui.unmount();
});
});
48 changes: 42 additions & 6 deletions __tests__/pro/sync/modelTransfer.integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,13 @@ describe('Pro mobile model transfer journey', () => {
).resolves.toBe(false);

fireEvent.press(ui.getByLabelText('Back'));
const whisperPath = `${modelTransferFsBoundary.DocumentDirectoryPath}/whisper-models/ggml-base.bin`;
const whisperBytes = Buffer.alloc(11 * 1024 * 1024);
await modelTransferFsBoundary.module.writeFile(
whisperPath,
whisperBytes.toString('base64'),
'base64',
);
fireEvent.press(ui.getByTestId(`sync-send-model-${remoteDevice.id}`));
await waitFor(() =>
expect(
Expand Down Expand Up @@ -402,14 +409,34 @@ describe('Pro mobile model transfer journey', () => {
),
).toBeTruthy(),
);
// The same aggregated picker resolves Whisper from its own disk registry and sends the real
// package to a Mac. This is the Android/iOS -> macOS route that was absent when transfer queried
// only the text-model registry.
returnedModel = undefined;
returnedFileName = undefined;
fireEvent.press(
ui.getByTestId('transfer-model-ggerganov/whisper.cpp/base'),
);
fireEvent.press(ui.getByTestId('send-selected-model'));
await waitFor(
() =>
expect(
ui!.getByText(`Whisper Base is available on ${remoteDevice.name}.`),
).toBeTruthy(),
// This moves a real 11 MB model through the transport. The full pre-push suite runs other
// integration tests at the same time, so allow the transfer to finish under that load.
{ timeout: 60000 },
);
expect(returnedFileName).toBe('ggml-base.bin');
expect(returnedModel).toEqual(whisperBytes);
// Not asserted: the sheet's "Sent <file>" progress line. The completion state replaces it, so
// matching it means catching a moment that has already passed - and the outcome is covered twice
// over, by the sentence the user reads and by the peer holding the exact bytes.
});

// A phone whose every model is vision-capable used to be told it had nothing to send: the send side
// refused any model with an mmproj, while the receiving side had installed those packages all along.
it('offers a vision package to a paired device and withholds a runtime that device cannot run', async () => {
it('offers vision and Whisper packages to a Mac and withholds a runtime it cannot run', async () => {
const vision = createVisionModel({
id: 'google/gemma-4-E2B/gemma-4-E2B-it-Q4_K_M.gguf',
name: 'Gemma 4 E2B',
Expand Down Expand Up @@ -447,18 +474,22 @@ describe('Pro mobile model transfer journey', () => {
{ ...liteRT, filePath: `${modelsDir}/${liteRT.fileName}` },
]),
);
const iPhone: DeviceInfo = {
id: 'paired-iphone',
name: 'iPhone',
platform: 'ios',
modelTransferFsBoundary.seedFile(
`${modelTransferFsBoundary.DocumentDirectoryPath}/whisper-models/ggml-base.bin`,
142 * 1024 * 1024,
);
const mac: DeviceInfo = {
id: 'paired-mac',
name: 'Mac',
platform: 'macos',
version: '1.0.0',
host: '192.168.1.20',
port: 51000,
};

ui = render(
<NavigationContainer>
<ModelTransferSheet target={iPhone} onClose={() => {}} />
<ModelTransferSheet target={mac} onClose={() => {}} />
</NavigationContainer>,
);

Expand All @@ -468,6 +499,11 @@ describe('Pro mobile model transfer journey', () => {
);
// LiteRT exists only on Android, so an iPhone is never offered one.
expect(ui.queryByTestId(`transfer-model-${liteRT.id}`)).toBeNull();
// Download Manager and model transfer both discover Whisper from its real disk registry.
expect(
ui.getByTestId('transfer-model-ggerganov/whisper.cpp/base'),
).toBeTruthy();
expect(ui.getByText('Whisper Base')).toBeTruthy();
// Its size is the whole package, not just the primary file.
expect(ui.getByText(/4\.5 GB|4\.49 GB/)).toBeTruthy();
});
Expand Down
21 changes: 21 additions & 0 deletions __tests__/rntl/screens/HomeScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,27 @@ describe('HomeScreen', () => {
expect(getByText(/Hi there, how can I help/)).toBeTruthy();
});

it('shows a clean enhanced-prompt preview without model protocol', () => {
const conv = createConversation({
title: 'Draw a dog',
messages: [
createMessage({ role: 'user', content: 'Draw a dog' }),
createMessage({
role: 'assistant',
content:
'<think>__LABEL:Enhanced prompt__\nA sleek black dog in soft morning light.</think>',
}),
],
});
useChatStore.setState({ conversations: [conv] });

const { getByText, queryByText } = renderHomeScreen();
expect(
getByText('A sleek black dog in soft morning light.'),
).toBeTruthy();
expect(queryByText(/<think>|__LABEL:/)).toBeNull();
});

it('shows "You: " prefix for last user message', () => {
const conv = createConversation({
title: 'User Preview Test',
Expand Down
3 changes: 2 additions & 1 deletion __tests__/rntl/screens/ProDetailScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ describe('ProDetailScreen', () => {
});

it('renders the Get Pro call-to-action when the user is not Pro', () => {
const { queryAllByText } = render(<ProDetailScreen />);
const { queryAllByText, queryByText } = render(<ProDetailScreen />);
expect(queryAllByText('Get Pro').length).toBeGreaterThan(0);
expect(queryByText('Use Pro from another device')).toBeNull();
});

it('Get Pro opens the web pay page directly without a modal', () => {
Expand Down
26 changes: 26 additions & 0 deletions __tests__/unit/sync/licenceRevalidationBudget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { settleWithinBudget } from '../../../pro/sync/licenceRevalidationBudget';

describe('licence revalidation startup budget', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('releases the budget timer when revalidation finishes first', async () => {
await settleWithinBudget(Promise.resolve(), 3_000);

expect(jest.getTimerCount()).toBe(0);
});

it('continues when the budget finishes before revalidation', async () => {
const pendingOperation = new Promise<void>(() => undefined);
const result = settleWithinBudget(pendingOperation, 3_000);

await jest.advanceTimersByTimeAsync(3_000);
await expect(result).resolves.toBeUndefined();
expect(jest.getTimerCount()).toBe(0);
});
});
2 changes: 1 addition & 1 deletion pro
Submodule pro updated from 879094 to e6e435
8 changes: 6 additions & 2 deletions src/screens/HomeScreen/hooks/useHomeScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export const useHomeScreen = (navigation: HomeScreenNavigationProp) => {
} = useRemoteModelHandlers({ activeModelId, setPickerType, setLoadingState, setAlertState });

useEffect(() => {
let lanDiscoveryTimer: ReturnType<typeof setTimeout> | null = null;
const task = InteractionManager.runAfterInteractions(() => {
loadData();
if (!hasInitializedNativeSync) {
Expand All @@ -146,11 +147,14 @@ export const useHomeScreen = (navigation: HomeScreenNavigationProp) => {
if (!persistApi?.hasHydrated || persistApi.hasHydrated()) migrateAutoDiscover();
else persistApi.onFinishHydration?.(migrateAutoDiscover);
// Delay LAN scan so the home screen is fully rendered and interactive first
setTimeout(runLANDiscovery, 3000);
lanDiscoveryTimer = setTimeout(runLANDiscovery, 3000);
}
});
isFirstMount.current = false;
return () => task.cancel();
return () => {
task.cancel();
if (lanDiscoveryTimer !== null) clearTimeout(lanDiscoveryTimer);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

}, []);

Expand Down
2 changes: 2 additions & 0 deletions src/screens/HomeScreen/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ navigation }) => {
onPress={() => navigation.navigate('ProDetail')}
hitSlop={8}
style={styles.crownButton}
accessibilityRole="button"
accessibilityLabel="Open Off Grid AI Pro"
>
<IconMC name="crown" size={16} color={colors.primary} />
</TouchableOpacity>
Expand Down
Loading
Loading