From 9f3de0e431389ce323d6cbeb0143f05013e56617 Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Wed, 19 Aug 2026 16:59:28 +0530 Subject: [PATCH 01/82] test(mcp): cover the "Connect desktop tools" row + bump pro pointer - McpAddServerSheet: a paired Off Grid desktop renders a row that fires onAddDesktop, and an already-added companion shows the connected state. - McpServersScreen: the remote-server store mock now provides `servers` (the real store always has it), which the new desktop-detection reads. - Bumps the pro submodule to the companion-desktop feature commits. Leaves package-lock.json / Podfile.lock (local install churn) out of this change. --- .../components/McpAddServerSheet.test.tsx | 27 +++++++++++++++++++ .../rntl/components/McpServersScreen.test.tsx | 2 +- pro | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/__tests__/rntl/components/McpAddServerSheet.test.tsx b/__tests__/rntl/components/McpAddServerSheet.test.tsx index de0d59295..a63994689 100644 --- a/__tests__/rntl/components/McpAddServerSheet.test.tsx +++ b/__tests__/rntl/components/McpAddServerSheet.test.tsx @@ -124,4 +124,31 @@ maybe('McpAddServerSheet', () => { expect(useMcpStore.getState().servers).toHaveLength(1); expect(props.onAddedPreset).not.toHaveBeenCalled(); }); + + it('lists a paired Off Grid desktop and calls onAddDesktop when tapped', () => { + const props = baseProps(); + const onAddDesktop = jest.fn(); + const desktops = [ + { id: 'pc-1', name: 'My Mac', endpoint: 'http://192.168.1.18:7878' }, + ] as any; + const { getByTestId } = render( + , + ); + fireEvent.press(getByTestId('mcp-desktop-pc-1')); + expect(onAddDesktop).toHaveBeenCalledWith(desktops[0]); + }); + + it('marks a desktop whose /mcp companion is already added as connected', () => { + useMcpStore.setState({ + servers: [{ id: 'c', name: 'My Mac tools', url: 'http://192.168.1.18:7878/mcp', authMode: 'header' }], + }); + const desktops = [ + { id: 'pc-1', name: 'My Mac', endpoint: 'http://192.168.1.18:7878' }, + ] as any; + const { getByText } = render( + , + ); + // Feather icons render as their name via the mock; the connected row shows check-circle. + expect(getByText('check-circle')).toBeTruthy(); + }); }); diff --git a/__tests__/rntl/components/McpServersScreen.test.tsx b/__tests__/rntl/components/McpServersScreen.test.tsx index e936b0a30..ecc3fd90e 100644 --- a/__tests__/rntl/components/McpServersScreen.test.tsx +++ b/__tests__/rntl/components/McpServersScreen.test.tsx @@ -45,7 +45,7 @@ jest.mock('@react-navigation/native', () => { jest.mock('../../../src/services/tools/extensions', () => ({ getToolExtensions: () => [] })); const mockAppState = { settings: { enabledTools: [] as string[] }, updateSettings: jest.fn(), activeModelId: undefined, downloadedModels: [] as any[] }; -const mockRemoteState = { activeRemoteTextModelId: 'remote-1' }; +const mockRemoteState = { activeRemoteTextModelId: 'remote-1', servers: [] as any[] }; jest.mock('../../../src/stores', () => ({ useAppStore: (selector?: any) => (selector ? selector(mockAppState) : mockAppState), useRemoteServerStore: (selector?: any) => (selector ? selector(mockRemoteState) : mockRemoteState), diff --git a/pro b/pro index 20ff55a1d..7a7196902 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 20ff55a1da292e247db80bbf48655b6cf94f2345 +Subproject commit 7a719690245e174a0c81fe3397093a1a43a39d7a From d48de0f3bbbb638c0538354303091dea5fe2bcc8 Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Thu, 20 Aug 2026 12:13:35 +0530 Subject: [PATCH 02/82] feat(mcp): QR pairing scanner deps + tests + pro bump - Adds react-native-vision-camera (v4) for the desktop-QR scanner; Podfile.lock updated (VisionCamera 4.7.3). Camera usage string was already in Info.plist. - jest: stub react-native-vision-camera (native module) so screens importing the scanner render in tests; cover the new "Scan a desktop QR" button. - Bumps the pro submodule to the QR pairing commits (scanner, parser, panel, empty-token guard). Leaves the local personal-team signing tweaks (pbxproj DEVELOPMENT_TEAM, entitlements) out of this change. --- .../components/McpAddServerSheet.test.tsx | 14 ++++ ios/Podfile.lock | 16 +++- jest.setup.ts | 9 +++ package-lock.json | 73 +++++++------------ package.json | 1 + pro | 2 +- 6 files changed, 63 insertions(+), 52 deletions(-) diff --git a/__tests__/rntl/components/McpAddServerSheet.test.tsx b/__tests__/rntl/components/McpAddServerSheet.test.tsx index a63994689..e27f7aa10 100644 --- a/__tests__/rntl/components/McpAddServerSheet.test.tsx +++ b/__tests__/rntl/components/McpAddServerSheet.test.tsx @@ -78,6 +78,20 @@ maybe('McpAddServerSheet', () => { expect(props.onAddCustom).toHaveBeenCalledTimes(1); }); + it('shows a "Scan a desktop QR" button that calls onScan (only when provided)', () => { + const withScan = render(); + expect(withScan.queryByTestId('scan-desktop-qr')).toBeTruthy(); + + const onScan = jest.fn(); + const r = render(); + fireEvent.press(r.getByTestId('scan-desktop-qr')); + expect(onScan).toHaveBeenCalledTimes(1); + + // No scan handler -> no scan button (older callers / manual-only). + const noScan = render(); + expect(noScan.queryByTestId('scan-desktop-qr')).toBeNull(); + }); + it('lists the preset rows', () => { const props = baseProps(); const { getByTestId } = render(); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2fb5a5cd1..39c621d9a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3459,6 +3459,12 @@ PODS: - SSZipArchive (~> 2.5.5) - SocketRocket (0.7.1) - SSZipArchive (2.5.5) + - VisionCamera (4.7.3): + - VisionCamera/Core (= 4.7.3) + - VisionCamera/React (= 4.7.3) + - VisionCamera/Core (4.7.3) + - VisionCamera/React (4.7.3): + - React-Core - whisper-rn (0.5.5): - boost - DoubleConversion @@ -3599,6 +3605,7 @@ DEPENDENCIES: - RNWorklets (from `../node_modules/react-native-worklets`) - RNZipArchive (from `../node_modules/react-native-zip-archive`) - SocketRocket (~> 0.7.1) + - VisionCamera (from `../node_modules/react-native-vision-camera`) - whisper-rn (from `../node_modules/whisper.rn`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -3823,6 +3830,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-worklets" RNZipArchive: :path: "../node_modules/react-native-zip-archive" + VisionCamera: + :path: "../node_modules/react-native-vision-camera" whisper-rn: :path: "../node_modules/whisper.rn" Yoga: @@ -3836,7 +3845,7 @@ SPEC CHECKSUMS: FBLazyVector: 309703e71d3f2f1ed7dc7889d58309c9d77a95a4 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 3de70ea2100f1780402cf146bb8110a0cdb2f34e + hermes-engine: 34c7ed219c0c2601856fc539a2b11284f30b7735 llama-rn: 21f400cc2cf8ae1785f0fdd38b674db646d9cd22 MMKV: 86859fdfa2b0b21db1fd6e48788474a6416a2c77 MMKVCore: 3d16ce9f7d411e135020915fde98a056859a1efa @@ -3881,7 +3890,7 @@ SPEC CHECKSUMS: react-native-background-downloader: b02d12c3961322ce1c85fa0f8b3e4adb5b652106 react-native-document-picker: dc2d83366e47e89e7c51e8a41eab99c1d54e941c react-native-document-viewer: 8c6ed07e7e27352743fa98e8dd6d288ad925b884 - react-native-executorch: 9a44ee2b18773cbe5ad2e6d7376eb76f347e2935 + react-native-executorch: 9b76d1e297ae1ea4c3c93d58c6ecec693423206a react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-image-picker: 0314366753615115fa55c3cc937ac44cb7e75702 react-native-keyboard-controller: 7534b5a39d1e8b2b79f86e8e998ed71c7154f69f @@ -3940,9 +3949,10 @@ SPEC CHECKSUMS: RNZipArchive: f2806ba80e24cf1984d6a7cb361d8a07d734997d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SSZipArchive: c69881e8ac5521f0e622291387add5f60f30f3c4 + VisionCamera: 7187b3dac1ff3071234ead959ce311875748e14f whisper-rn: 7566faf9b7d78e39ab9fc634cb90fdee81177793 Yoga: 5456bb010373068fc92221140921b09d126b116e PODFILE CHECKSUM: f66f810a788ead15881075527443239e49b50db1 -COCOAPODS: 1.16.2 +COCOAPODS: 1.17.0 diff --git a/jest.setup.ts b/jest.setup.ts index 17436bc3b..d95d3e98f 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -41,6 +41,15 @@ jest.mock('react-native-edge-to-edge', () => ({ NavigationBar: () => null, })); +// react-native-vision-camera is a native module (NitroModules); stub it so any +// screen that imports the QR scanner can render in jest without the native view. +jest.mock('react-native-vision-camera', () => ({ + Camera: () => null, + useCameraDevice: () => undefined, + useCameraPermission: () => ({ hasPermission: false, requestPermission: jest.fn() }), + useCodeScanner: (config: unknown) => config, +})); + // ============================================================================ // AsyncStorage Mock // ============================================================================ diff --git a/package-lock.json b/package-lock.json index 6156fcd4c..9b280e0d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "react-native-tcp-socket": "^6.4.1", "react-native-url-polyfill": "^3.0.0", "react-native-vector-icons": "^10.3.0", + "react-native-vision-camera": "^4.7.3", "react-native-worklets": "^0.7.3", "react-native-zeroconf": "^0.14.0", "react-native-zip-archive": "7.1.0", @@ -4595,9 +4596,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4615,9 +4613,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4635,9 +4630,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4655,9 +4647,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4675,9 +4664,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4695,9 +4681,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4715,9 +4698,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4735,9 +4715,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4950,9 +4927,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4967,9 +4941,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4984,9 +4955,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5001,9 +4969,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5018,9 +4983,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5035,9 +4997,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5052,9 +5011,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5069,9 +5025,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -15107,6 +15060,30 @@ "node": ">=10" } }, + "node_modules/react-native-vision-camera": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-4.7.3.tgz", + "integrity": "sha512-g1/neOyjSqn1kaAa2FxI/qp5KzNvPcF0bnQw6NntfbxH6tm0+8WFZszlgb5OV+iYlB6lFUztCbDtyz5IpL47OA==", + "license": "MIT", + "peerDependencies": { + "@shopify/react-native-skia": "*", + "react": "*", + "react-native": "*", + "react-native-reanimated": "*", + "react-native-worklets-core": "*" + }, + "peerDependenciesMeta": { + "@shopify/react-native-skia": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + }, + "react-native-worklets-core": { + "optional": true + } + } + }, "node_modules/react-native-worklets": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.3.tgz", diff --git a/package.json b/package.json index 899fdf9c1..ad273ed50 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "react-native-tcp-socket": "^6.4.1", "react-native-url-polyfill": "^3.0.0", "react-native-vector-icons": "^10.3.0", + "react-native-vision-camera": "^4.7.3", "react-native-worklets": "^0.7.3", "react-native-zeroconf": "^0.14.0", "react-native-zip-archive": "7.1.0", diff --git a/pro b/pro index 7a7196902..8812431d1 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 7a719690245e174a0c81fe3397093a1a43a39d7a +Subproject commit 8812431d1cd58ba8181fedc380bdb0efa04a862d From 4b1c450cfcd2441c258525f037d95b92c676344b Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Thu, 20 Aug 2026 15:13:50 +0530 Subject: [PATCH 03/82] test(mcp): drop the vision-camera scanner mock + scan-button test The desktop token-QR scanner is gone (tools now arrive over the sync mesh), so remove its jest mock and replace the scan-button test with a guard that the button stays absent. Bumps the mobile-pro submodule to bfdb9eb. --- .../rntl/components/McpAddServerSheet.test.tsx | 17 +++++------------ jest.setup.ts | 9 --------- pro | 2 +- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/__tests__/rntl/components/McpAddServerSheet.test.tsx b/__tests__/rntl/components/McpAddServerSheet.test.tsx index e27f7aa10..c9e177841 100644 --- a/__tests__/rntl/components/McpAddServerSheet.test.tsx +++ b/__tests__/rntl/components/McpAddServerSheet.test.tsx @@ -78,18 +78,11 @@ maybe('McpAddServerSheet', () => { expect(props.onAddCustom).toHaveBeenCalledTimes(1); }); - it('shows a "Scan a desktop QR" button that calls onScan (only when provided)', () => { - const withScan = render(); - expect(withScan.queryByTestId('scan-desktop-qr')).toBeTruthy(); - - const onScan = jest.fn(); - const r = render(); - fireEvent.press(r.getByTestId('scan-desktop-qr')); - expect(onScan).toHaveBeenCalledTimes(1); - - // No scan handler -> no scan button (older callers / manual-only). - const noScan = render(); - expect(noScan.queryByTestId('scan-desktop-qr')).toBeNull(); + it('has no "Scan a desktop QR" button - a paired desktop grants tools over the mesh', () => { + // The QR-scan pairing was removed: a paired desktop now hands its tools over + // the sync mesh, so there is nothing to scan from the add sheet. + const { queryByTestId } = render(); + expect(queryByTestId('scan-desktop-qr')).toBeNull(); }); it('lists the preset rows', () => { diff --git a/jest.setup.ts b/jest.setup.ts index d95d3e98f..17436bc3b 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -41,15 +41,6 @@ jest.mock('react-native-edge-to-edge', () => ({ NavigationBar: () => null, })); -// react-native-vision-camera is a native module (NitroModules); stub it so any -// screen that imports the QR scanner can render in jest without the native view. -jest.mock('react-native-vision-camera', () => ({ - Camera: () => null, - useCameraDevice: () => undefined, - useCameraPermission: () => ({ hasPermission: false, requestPermission: jest.fn() }), - useCodeScanner: (config: unknown) => config, -})); - // ============================================================================ // AsyncStorage Mock // ============================================================================ diff --git a/pro b/pro index 8812431d1..bfdb9ebc5 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 8812431d1cd58ba8181fedc380bdb0efa04a862d +Subproject commit bfdb9ebc5b621e8ba6e9782189f40859f8e0d65d From c256d0f8ac5ff54b21b8ed9a696c1db83479b612 Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Thu, 20 Aug 2026 15:43:11 +0530 Subject: [PATCH 04/82] test(sync): cover scan-to-pair + restore the vision-camera mock A decoded QR carrying a valid pairing code pairs via the same onPair as typing; a non-pairing QR is ignored. Restores the vision-camera jest stub for the new QrScanner and bumps the mobile-pro submodule. --- .../rntl/components/pairingCodeSheet.test.tsx | 110 ++++++++++++++++++ jest.setup.ts | 9 ++ pro | 2 +- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 __tests__/rntl/components/pairingCodeSheet.test.tsx diff --git a/__tests__/rntl/components/pairingCodeSheet.test.tsx b/__tests__/rntl/components/pairingCodeSheet.test.tsx new file mode 100644 index 000000000..ec2c70811 --- /dev/null +++ b/__tests__/rntl/components/pairingCodeSheet.test.tsx @@ -0,0 +1,110 @@ +/** + * Integration (RNTL): PairingCodeSheet scan-to-pair. + * + * Guards the approved behavior change: a paired-code sheet can be filled by scanning + * the other device's QR, not just by typing. A decoded QR carrying a valid pairing + * code lands on the SAME onPair (syncService.pair) as the typed path, and a QR that + * is not a pairing code is ignored so the scanner keeps looking. + * + * Lives in the private pro/ submodule, loaded via a computed path so the suite skips + * in open-core CI where pro/ is absent. + */ + +import React from 'react'; +import { render, fireEvent, act } from '@testing-library/react-native'; + +jest.mock('react-native-vector-icons/Feather', () => { + const { Text } = require('react-native'); + return ({ name, ...props }: any) => {name}; +}); + +// The sheet is a modal wrapper; render its children inline so the test can drive the +// content without a navigation/provider host. +jest.mock('@offgrid/core/components/AppSheet', () => ({ + AppSheet: ({ children }: { children: React.ReactNode }) => children, +})); + +jest.mock('../../../src/theme', () => { + const colors = { + text: '#000', textMuted: '#999', primary: '#1DB954', error: '#F00', + background: '#FFF', surface: '#F5F5F5', border: '#E0E0E0', + }; + const shadows = { small: {}, medium: {}, large: {} }; + return { + useTheme: () => ({ colors, shadows, isDark: false }), + useThemedStyles: (fn: any) => fn(colors, shadows), + }; +}); + +// vision-camera is globally stubbed in jest.setup; capture the scan config here so +// the test can simulate a decoded QR frame. +const visionCamera = require('react-native-vision-camera'); +let scanConfig: { onCodeScanned: (codes: { value?: string }[]) => void } | null = null; + +type SheetModule = typeof import('../../../pro/ui/SyncScreen/PairingCodeSheet'); + +function load(): SheetModule | null { + try { + return require(['..', '..', '..', 'pro', 'ui', 'SyncScreen', 'PairingCodeSheet'].join('/')); + } catch { + return null; + } +} + +const mod = load(); +const maybe = mod ? describe : describe.skip; + +// A valid code: every character is in the pairing alphabet. +const VALID_QR = 'ABCD2345'; + +maybe('PairingCodeSheet scan-to-pair', () => { + const { PairingCodeSheet } = mod!; + + const baseProps = () => ({ + visible: true, + deviceName: 'Studio Mac', + confirmLabel: 'Pair', + testIDPrefix: 'sync-test', + onClose: jest.fn(), + onPair: jest.fn().mockResolvedValue(undefined), + }); + + beforeEach(() => { + scanConfig = null; + jest.spyOn(visionCamera, 'useCodeScanner').mockImplementation((cfg: any) => { + scanConfig = cfg; + return cfg; + }); + }); + + it('offers a Scan button that opens the camera scanner', () => { + const { getByTestId, queryByText, getByText } = render( + , + ); + expect(queryByText('Camera access needed')).toBeNull(); + fireEvent.press(getByTestId('sync-test-scan')); + // Global vision-camera mock reports no permission, so the scanner asks for it - + // proof the scanner surface mounted. + expect(getByText('Camera access needed')).toBeTruthy(); + }); + + it('pairs from a scanned QR via the same onPair as typing', async () => { + const props = baseProps(); + const { getByTestId } = render(); + fireEvent.press(getByTestId('sync-test-scan')); + await act(async () => { + scanConfig!.onCodeScanned([{ value: VALID_QR }]); + }); + expect(props.onPair).toHaveBeenCalledWith(VALID_QR); + }); + + it('ignores a QR that is not a pairing code', async () => { + const props = baseProps(); + const { getByTestId } = render(); + fireEvent.press(getByTestId('sync-test-scan')); + await act(async () => { + scanConfig!.onCodeScanned([{ value: 'https://example.com/not-a-code' }]); + }); + expect(props.onPair).not.toHaveBeenCalled(); + }); +}); diff --git a/jest.setup.ts b/jest.setup.ts index 17436bc3b..a2bc142f2 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -41,6 +41,15 @@ jest.mock('react-native-edge-to-edge', () => ({ NavigationBar: () => null, })); +// react-native-vision-camera is a native module (NitroModules); stub it so the +// QR scanner (mesh Scan-to-pair) can render in jest without the native view. +jest.mock('react-native-vision-camera', () => ({ + Camera: () => null, + useCameraDevice: () => undefined, + useCameraPermission: () => ({ hasPermission: false, requestPermission: jest.fn() }), + useCodeScanner: (config: unknown) => config, +})); + // ============================================================================ // AsyncStorage Mock // ============================================================================ diff --git a/pro b/pro index bfdb9ebc5..ee1ea526e 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit bfdb9ebc5b621e8ba6e9782189f40859f8e0d65d +Subproject commit ee1ea526eeb30c4b91af93ebe90dffb10b2fdf22 From ce0b1ee97f2363c3279e3aa81ffa08317abeb726 Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Thu, 20 Aug 2026 16:00:09 +0530 Subject: [PATCH 05/82] test(sync): guard that opening the scanner hides the pairing sheet Regression for the iOS one-modal-at-a-time fix: pressing Scan hides the sheet body so the scanner can present. Bumps mobile-pro. --- .../rntl/components/pairingCodeSheet.test.tsx | 16 +++++++++++++--- pro | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/__tests__/rntl/components/pairingCodeSheet.test.tsx b/__tests__/rntl/components/pairingCodeSheet.test.tsx index ec2c70811..1ce4dbcf9 100644 --- a/__tests__/rntl/components/pairingCodeSheet.test.tsx +++ b/__tests__/rntl/components/pairingCodeSheet.test.tsx @@ -18,10 +18,12 @@ jest.mock('react-native-vector-icons/Feather', () => { return ({ name, ...props }: any) => {name}; }); -// The sheet is a modal wrapper; render its children inline so the test can drive the -// content without a navigation/provider host. +// The sheet is a modal wrapper; render its children inline (respecting `visible`, as +// the real one does) so the test can drive the content and observe it hiding while +// the scanner is open. jest.mock('@offgrid/core/components/AppSheet', () => ({ - AppSheet: ({ children }: { children: React.ReactNode }) => children, + AppSheet: ({ visible, children }: { visible: boolean; children: React.ReactNode }) => + visible ? children : null, })); jest.mock('../../../src/theme', () => { @@ -88,6 +90,14 @@ maybe('PairingCodeSheet scan-to-pair', () => { expect(getByText('Camera access needed')).toBeTruthy(); }); + it('hides the pairing sheet while the scanner is open (one modal at a time)', () => { + // iOS presents one modal at a time; the sheet must yield so the scanner can show. + const { getByTestId, queryByTestId } = render(); + expect(queryByTestId('sync-test-input')).toBeTruthy(); + fireEvent.press(getByTestId('sync-test-scan')); + expect(queryByTestId('sync-test-input')).toBeNull(); + }); + it('pairs from a scanned QR via the same onPair as typing', async () => { const props = baseProps(); const { getByTestId } = render(); diff --git a/pro b/pro index ee1ea526e..01a838a05 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit ee1ea526eeb30c4b91af93ebe90dffb10b2fdf22 +Subproject commit 01a838a05a440ba9efc62a7607dbbf05f49f6aff From 81da65833e915a3e6698143c8dc42949b11c1c27 Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Thu, 20 Aug 2026 16:08:11 +0530 Subject: [PATCH 06/82] chore(pro): bump mobile-pro - QR scanner overlay fix --- pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pro b/pro index 01a838a05..7847444c7 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 01a838a05a440ba9efc62a7607dbbf05f49f6aff +Subproject commit 7847444c7fa2a3999fe98c4961a6b58aa714cd18 From 0445d6f2f93af98d535809b7f80b3cd34514a33e Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Thu, 20 Aug 2026 16:31:38 +0530 Subject: [PATCH 07/82] test(mcp): cover the Pro tools desktop-tools grant + bump mobile-pro CompanionToolsSection lists only desktop peers, reflects grantedByDeviceId, and toggles via requestTools. Bumps mobile-pro to the single-home refactor. --- .../components/companionToolsSection.test.tsx | 84 +++++++++++++++++++ pro | 2 +- 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 __tests__/rntl/components/companionToolsSection.test.tsx diff --git a/__tests__/rntl/components/companionToolsSection.test.tsx b/__tests__/rntl/components/companionToolsSection.test.tsx new file mode 100644 index 000000000..6a083d199 --- /dev/null +++ b/__tests__/rntl/components/companionToolsSection.test.tsx @@ -0,0 +1,84 @@ +/** + * Integration (RNTL): CompanionToolsSection - the single home for desktop tools. + * + * Proves the moved grant: paired desktops (only desktops) appear in Pro tools with a + * switch that reflects whether their tools are connected here (grantedByDeviceId), and + * flipping one calls requestTools(deviceId, next) - the same mesh request the old + * Devices-row toggle sent. Loaded via a computed path so it skips where pro/ is absent. + */ + +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; + +jest.mock('react-native-vector-icons/Feather', () => { + const { Text } = require('react-native'); + return ({ name, ...props }: any) => {name}; +}); + +jest.mock('../../../src/theme', () => ({ + useTheme: () => ({ + colors: { + text: '#000', textMuted: '#999', primary: '#1DB954', surface: '#F5F5F5', border: '#E0E0E0', + }, + }), +})); + +const mockState: { knownDevices: unknown[]; servers: unknown[] } = { knownDevices: [], servers: [] }; +const mockRequestTools = jest.fn(); + +jest.mock('../../../pro/sync/syncStore', () => ({ + useSyncStore: (selector: (s: unknown) => unknown) => + selector({ knownDevices: mockState.knownDevices }), +})); +jest.mock('../../../pro/mcp/mcpStore', () => ({ + useMcpStore: (selector: (s: unknown) => unknown) => selector({ servers: mockState.servers }), +})); +jest.mock('../../../pro/mcp/mcpToolGrantService', () => ({ + requestTools: (...args: unknown[]) => mockRequestTools(...args), +})); + +type Mod = typeof import('../../../pro/ui/CompanionToolsSection'); +function load(): Mod | null { + try { + return require(['..', '..', '..', 'pro', 'ui', 'CompanionToolsSection'].join('/')); + } catch { + return null; + } +} + +const mod = load(); +const maybe = mod ? describe : describe.skip; + +maybe('CompanionToolsSection', () => { + const { CompanionToolsSection } = mod!; + + beforeEach(() => { + mockState.knownDevices = []; + mockState.servers = []; + mockRequestTools.mockClear(); + }); + + it('lists only desktop peers, reflects the grant, and toggles via requestTools', () => { + mockState.knownDevices = [ + { id: 'mac1', name: 'My Mac', platform: 'macos' }, + { id: 'phone1', name: 'My Phone', platform: 'ios' }, + ]; + mockState.servers = [{ id: 's1', grantedByDeviceId: 'mac1' }]; + + const { getByTestId, queryByTestId } = render(); + // Desktop shows; a phone peer (serves no tools) is filtered out. + expect(getByTestId('companion-tools-mac1')).toBeTruthy(); + expect(queryByTestId('companion-tools-phone1')).toBeNull(); + + const sw = getByTestId('companion-tools-switch-mac1'); + expect(sw.props.value).toBe(true); // granted -> on + fireEvent(sw, 'valueChange', false); + expect(mockRequestTools).toHaveBeenCalledWith('mac1', false); + }); + + it('renders nothing when there are no paired desktops', () => { + mockState.knownDevices = [{ id: 'phone1', name: 'Phone', platform: 'android' }]; + const { toJSON } = render(); + expect(toJSON()).toBeNull(); + }); +}); diff --git a/pro b/pro index 7847444c7..e04376ccb 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 7847444c7fa2a3999fe98c4961a6b58aa714cd18 +Subproject commit e04376ccbce06db2f2bb7554c6cd2d1fedb24d5f From c61a8e8cdfef5853987c404b50f1a47e23fe961e Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Thu, 20 Aug 2026 22:01:24 +0530 Subject: [PATCH 08/82] chore(pro): bump mobile-pro - offline-forget fix --- pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pro b/pro index e04376ccb..aeb861e58 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit e04376ccbce06db2f2bb7554c6cd2d1fedb24d5f +Subproject commit aeb861e5851fc56fbc95d81b74dac177a5ab03b1 From b1fb265147de0a87d41d452c9a58318955a70d6f Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Fri, 21 Aug 2026 10:57:04 +0530 Subject: [PATCH 09/82] test(mcp): drop the desktop-section sheet tests + bump mobile-pro The "Paired Off Grid desktops" add-sheet section is gone (mesh grant replaces it); remove its two render tests. Bumps mobile-pro to the removal. --- .../components/McpAddServerSheet.test.tsx | 27 ------------------- pro | 2 +- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/__tests__/rntl/components/McpAddServerSheet.test.tsx b/__tests__/rntl/components/McpAddServerSheet.test.tsx index c9e177841..ce29aca23 100644 --- a/__tests__/rntl/components/McpAddServerSheet.test.tsx +++ b/__tests__/rntl/components/McpAddServerSheet.test.tsx @@ -131,31 +131,4 @@ maybe('McpAddServerSheet', () => { expect(useMcpStore.getState().servers).toHaveLength(1); expect(props.onAddedPreset).not.toHaveBeenCalled(); }); - - it('lists a paired Off Grid desktop and calls onAddDesktop when tapped', () => { - const props = baseProps(); - const onAddDesktop = jest.fn(); - const desktops = [ - { id: 'pc-1', name: 'My Mac', endpoint: 'http://192.168.1.18:7878' }, - ] as any; - const { getByTestId } = render( - , - ); - fireEvent.press(getByTestId('mcp-desktop-pc-1')); - expect(onAddDesktop).toHaveBeenCalledWith(desktops[0]); - }); - - it('marks a desktop whose /mcp companion is already added as connected', () => { - useMcpStore.setState({ - servers: [{ id: 'c', name: 'My Mac tools', url: 'http://192.168.1.18:7878/mcp', authMode: 'header' }], - }); - const desktops = [ - { id: 'pc-1', name: 'My Mac', endpoint: 'http://192.168.1.18:7878' }, - ] as any; - const { getByText } = render( - , - ); - // Feather icons render as their name via the mock; the connected row shows check-circle. - expect(getByText('check-circle')).toBeTruthy(); - }); }); diff --git a/pro b/pro index aeb861e58..c235e6fa9 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit aeb861e5851fc56fbc95d81b74dac177a5ab03b1 +Subproject commit c235e6fa98e4c16bfec9049c90b52d531e2716cc From ca2b0e205ed2f7c70141c2f3200e5dd993a528e8 Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Fri, 21 Aug 2026 11:41:02 +0530 Subject: [PATCH 10/82] chore(pro): bump mobile-pro - revert offline-forget (conflicts with intended behavior) --- pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pro b/pro index c235e6fa9..aab1e6798 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit c235e6fa98e4c16bfec9049c90b52d531e2716cc +Subproject commit aab1e6798661b7cf241d2b046cdd645868584372 From 61f6a2879193373bf78dc3c0a7ea1a9f07bc0bcd Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Fri, 21 Aug 2026 11:55:18 +0530 Subject: [PATCH 11/82] test(mcp): stub CompanionToolsSection in the McpServersScreen suite The paired-desktops tools section pulls the sync store + grant service (and thus syncService, which doesn't load under jest) into McpServersScreen's module, so the suite could no longer require the screen. Stub it to null - this suite covers the MCP server cards, not the companion section. --- __tests__/rntl/components/McpServersScreen.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/__tests__/rntl/components/McpServersScreen.test.tsx b/__tests__/rntl/components/McpServersScreen.test.tsx index ecc3fd90e..7252655b5 100644 --- a/__tests__/rntl/components/McpServersScreen.test.tsx +++ b/__tests__/rntl/components/McpServersScreen.test.tsx @@ -55,6 +55,10 @@ jest.mock('../../../pro/mcp/mcpService', () => ({ connectServer: jest.fn(), disconnectServer: jest.fn(), signOutServer: jest.fn(), })); +// The paired-desktops tools section pulls in the sync store + grant service (→ syncService, +// which does not load under jest). This suite is about the MCP server cards, so stub it out. +jest.mock('../../../pro/ui/CompanionToolsSection', () => ({ CompanionToolsSection: () => null })); + type ScreenModule = typeof import('../../../pro/ui/McpServersScreen'); type StoreModule = typeof import('../../../pro/mcp/mcpStore'); From 87adccb2f3c5485b4309e1564f373f0da381c352 Mon Sep 17 00:00:00 2001 From: siddWednesday Date: Fri, 21 Aug 2026 17:05:58 +0530 Subject: [PATCH 12/82] feat(chat): chatOverlay slot for the pending computer-use approval card Add a chat.overlay slot rendered below the header, above the messages, and wire ChatScreen to render whatever pro registers into it. Pro fills it with the computer-use approval card (a paired desktop's forwarded task); free builds register nothing and the slot is inert. src never imports pro - the slot seam keeps that boundary. Co-Authored-By: Claude Opus 4.8 --- src/bootstrap/slotRegistry.ts | 4 ++++ src/screens/ChatScreen/index.tsx | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/bootstrap/slotRegistry.ts b/src/bootstrap/slotRegistry.ts index fa90db726..2da6e9b79 100644 --- a/src/bootstrap/slotRegistry.ts +++ b/src/bootstrap/slotRegistry.ts @@ -85,4 +85,8 @@ export const SLOTS = { * download/management). The tab itself only appears when this is * registered, so free builds show just Text/Image. */ modelsScreenVoiceTab: 'modelsScreen.voiceTab', + /** Full-width overlay pinned above the chat messages (below the header) - e.g. a + * pending computer-use approval forwarded from a paired desktop, answered here. + * Renders nothing (and takes no space) when there is nothing pending. */ + chatOverlay: 'chat.overlay', } as const; diff --git a/src/screens/ChatScreen/index.tsx b/src/screens/ChatScreen/index.tsx index 6cebc0eac..e51e9ba6f 100644 --- a/src/screens/ChatScreen/index.tsx +++ b/src/screens/ChatScreen/index.tsx @@ -27,6 +27,7 @@ import { WhisperPickerSheet } from '../../components/models/WhisperPickerSheet'; import { VoiceModelsSheet } from '../../components/models/VoiceModelsSheet'; import { useWhisperStore } from '../../stores/whisperStore'; import { WHISPER_MODELS } from '../../services'; +import { getSlot, SLOTS } from '../../bootstrap/slotRegistry'; function countConversationImages(conv: Conversation | undefined): number { return (conv?.messages || []).reduce((n: number, m: Message) => @@ -215,6 +216,12 @@ export const ChatScreen: React.FC = () => { setShowProjectSelector={chat.setShowProjectSelector} isRemote={chat.activeModelInfo?.isRemote} /> + {/* Pro-registered overlay pinned above the messages: a paired desktop's pending + computer-use approval, answered here. Self-hides when nothing is pending. */} + {(() => { + const ChatOverlay = getSlot(SLOTS.chatOverlay); + return ChatOverlay ? : null; + })()} setModelsManagerOpen(false)} From 8bd93e5af4d464845f3501f623a4d13835ce88df Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Sat, 22 Aug 2026 08:12:00 +0530 Subject: [PATCH 13/82] test(sync): send Whisper from mobile to Mac --- .../sync/modelTransfer.integration.test.tsx | 46 ++++++++++++++++--- pro | 2 +- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/__tests__/pro/sync/modelTransfer.integration.test.tsx b/__tests__/pro/sync/modelTransfer.integration.test.tsx index d143aa0ce..ab95e11fc 100644 --- a/__tests__/pro/sync/modelTransfer.integration.test.tsx +++ b/__tests__/pro/sync/modelTransfer.integration.test.tsx @@ -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( @@ -402,6 +409,24 @@ 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(), + { timeout: 5000 }, + ); + expect(returnedFileName).toBe('ggml-base.bin'); + expect(returnedModel).toEqual(whisperBytes); // Not asserted: the sheet's "Sent " 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. @@ -409,7 +434,7 @@ describe('Pro mobile model transfer journey', () => { // 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', @@ -447,10 +472,14 @@ 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, @@ -458,7 +487,7 @@ describe('Pro mobile model transfer journey', () => { ui = render( - {}} /> + {}} /> , ); @@ -468,6 +497,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(); }); diff --git a/pro b/pro index 879094707..cbe05dcb1 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 879094707d0007e251cf0c9209ddbf8aaf35a83d +Subproject commit cbe05dcb11e9ad84b1d75e78bda74f5cd2f0534a From 4df548598e51c6c327470756ea53393f3bea51ad Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Sat, 22 Aug 2026 08:13:14 +0530 Subject: [PATCH 14/82] test(chat): render clean mobile previews --- __tests__/rntl/screens/HomeScreen.test.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/__tests__/rntl/screens/HomeScreen.test.tsx b/__tests__/rntl/screens/HomeScreen.test.tsx index 3bc2073e0..287ffe006 100644 --- a/__tests__/rntl/screens/HomeScreen.test.tsx +++ b/__tests__/rntl/screens/HomeScreen.test.tsx @@ -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: + '__LABEL:Enhanced prompt__\nA sleek black dog in soft morning light.', + }), + ], + }); + useChatStore.setState({ conversations: [conv] }); + + const { getByText, queryByText } = renderHomeScreen(); + expect( + getByText('A sleek black dog in soft morning light.'), + ).toBeTruthy(); + expect(queryByText(/|__LABEL:/)).toBeNull(); + }); + it('shows "You: " prefix for last user message', () => { const conv = createConversation({ title: 'User Preview Test', From 37bd5d718d5820f588f481ca57a468978bf2f34d Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Sat, 22 Aug 2026 08:19:26 +0530 Subject: [PATCH 15/82] test(sync): allow real Whisper transfer to settle --- __tests__/pro/sync/modelTransfer.integration.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__tests__/pro/sync/modelTransfer.integration.test.tsx b/__tests__/pro/sync/modelTransfer.integration.test.tsx index ab95e11fc..310657e73 100644 --- a/__tests__/pro/sync/modelTransfer.integration.test.tsx +++ b/__tests__/pro/sync/modelTransfer.integration.test.tsx @@ -423,7 +423,9 @@ describe('Pro mobile model transfer journey', () => { expect( ui!.getByText(`Whisper Base is available on ${remoteDevice.name}.`), ).toBeTruthy(), - { timeout: 5000 }, + // 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); From 9b90bb3e7b1b737238a6d69906f68d150c7423df Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Sat, 22 Aug 2026 08:39:27 +0530 Subject: [PATCH 16/82] fix(pro): remove device licence shortcut --- __tests__/rntl/screens/ProDetailScreen.test.tsx | 3 ++- src/screens/ProDetailScreen/index.tsx | 10 ---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/__tests__/rntl/screens/ProDetailScreen.test.tsx b/__tests__/rntl/screens/ProDetailScreen.test.tsx index dd2ed6e01..115492464 100644 --- a/__tests__/rntl/screens/ProDetailScreen.test.tsx +++ b/__tests__/rntl/screens/ProDetailScreen.test.tsx @@ -76,8 +76,9 @@ describe('ProDetailScreen', () => { }); it('renders the Get Pro call-to-action when the user is not Pro', () => { - const { queryAllByText } = render(); + const { queryAllByText, queryByText } = render(); 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', () => { diff --git a/src/screens/ProDetailScreen/index.tsx b/src/screens/ProDetailScreen/index.tsx index acf2d6b9e..833f17db1 100644 --- a/src/screens/ProDetailScreen/index.tsx +++ b/src/screens/ProDetailScreen/index.tsx @@ -29,7 +29,6 @@ import { ScreenHeader } from '../../components/ScreenHeader'; import { ProManageSection } from './ProManageSection'; import { ProIncludedSection } from './ProIncludedSection'; import { ProUnlockModal } from './ProUnlockModal'; -import { useHasRegisteredScreen } from '../../navigation/screenRegistry'; import type { RootStackParamList } from '../../navigation/types'; // Off Grid AI Pro is the ambient intelligence layer across desktop + phone, not a @@ -70,7 +69,6 @@ export const ProDetailScreen: React.FC = () => { const hasSavedProCredential = useAppStore(s => s.hasSavedProCredential); const isProActive = useAppStore(s => s.isProActive); const hasProAccess = useAppStore(selectHasProAccess); - const hasSyncBootstrap = useHasRegisteredScreen('Sync'); const [verifyModalVisible, setVerifyModalVisible] = useState(false); const pricing = getPricingCopy(); const isDevelopmentAccess = __DEV__ && isProActive && !hasSavedProCredential; @@ -217,14 +215,6 @@ export const ProDetailScreen: React.FC = () => { onPress={openVerifyModal} style={styles.verifyButton} /> - {hasSyncBootstrap ? ( -