main 98cb208f2713 cached
633 files
59.8 MB
3.2M tokens
1574 symbols
1 requests
Copy disabled (too large) Download .txt
Showing preview only (12,962K chars total). Download the full file to get everything.
Repository: alichherawalla/off-grid-mobile
Branch: main
Commit: 98cb208f2713
Files: 633
Total size: 59.8 MB

Directory structure:
gitextract_vym5rnyp/

├── .bundle/
│   └── config
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   └── workflows/
│       ├── ci.yml
│       ├── pages.yml
│       ├── release-ios.yml
│       └── release.yml
├── .gitignore
├── .husky/
│   └── pre-push
├── .maestro/
│   ├── E2E_TESTING.md
│   ├── config.yaml
│   ├── flows/
│   │   ├── p0/
│   │   │   ├── 00-setup-model.yaml
│   │   │   ├── 01-app-launch.yaml
│   │   │   ├── 01a-onboarding-first-launch.yaml
│   │   │   ├── 01b-onboarding-skip.yaml
│   │   │   ├── 01c-model-download-first-time.yaml
│   │   │   ├── 01d-second-launch-no-onboarding.yaml
│   │   │   ├── 01e-tab-navigation.yaml
│   │   │   ├── 02-text-generation.yaml
│   │   │   ├── 03-stop-generation.yaml
│   │   │   └── 04-image-generation.yaml
│   │   ├── p1/
│   │   │   ├── 06a-document-attachment.yaml
│   │   │   ├── 06b-image-attachment.yaml
│   │   │   ├── 06c-text-generation-full.yaml
│   │   │   └── 06d-text-generation-retry.yaml
│   │   ├── p2/
│   │   │   ├── 05a-model-uninstall.yaml
│   │   │   ├── 05b-model-download.yaml
│   │   │   ├── 05b-model-selection.yaml
│   │   │   └── 05c-model-unload.yaml
│   │   └── p3/
│   │       ├── 07a-image-model-uninstall.yaml
│   │       ├── 07b-image-model-download.yaml
│   │       └── 07c-image-model-set-active.yaml
│   └── utils/
│       └── wait-for-app-ready.yaml
├── .prettierrc.js
├── .swiftlint.yml
├── .vscode/
│   └── settings.json
├── .watchmanconfig
├── AGENTS.md
├── App.tsx
├── CLAUDE.md
├── Gemfile
├── LICENSE
├── README.md
├── TODO.md
├── __tests__/
│   ├── App.test.tsx
│   ├── contracts/
│   │   ├── coreMLDiffusion.contract.test.ts
│   │   ├── iosDownloadManager.contract.test.ts
│   │   ├── llama.rn.test.ts
│   │   ├── llamaContext.contract.test.ts
│   │   ├── localDream.contract.test.ts
│   │   ├── ragEmbedding.contract.test.ts
│   │   ├── whisper.contract.test.ts
│   │   └── whisper.rn.test.ts
│   ├── helpers/
│   │   ├── mockCustomAlert.tsx
│   │   └── mockNetworkDeps.ts
│   ├── integration/
│   │   ├── generation/
│   │   │   ├── generationFlow.test.ts
│   │   │   ├── imageGenerationFlow.test.ts
│   │   │   ├── remoteProviderRouting.test.ts
│   │   │   ├── sharePromptFlow.test.ts
│   │   │   └── unifiedModelSelection.test.ts
│   │   ├── models/
│   │   │   └── activeModelService.test.ts
│   │   ├── onboarding/
│   │   │   └── spotlightFlowIntegration.test.ts
│   │   ├── rag/
│   │   │   ├── embeddingFlow.test.ts
│   │   │   └── ragFlow.test.ts
│   │   └── stores/
│   │       ├── chatStoreIntegration.test.ts
│   │       └── remoteServerDiscovery.test.ts
│   ├── rntl/
│   │   ├── components/
│   │   │   ├── AnimatedEntry.test.tsx
│   │   │   ├── AnimatedListItem.test.tsx
│   │   │   ├── AnimatedPressable.test.tsx
│   │   │   ├── AppSheet.test.tsx
│   │   │   ├── Card.test.tsx
│   │   │   ├── ChatInput.test.tsx
│   │   │   ├── ChatMessage.test.tsx
│   │   │   ├── ChatMessageTools.test.tsx
│   │   │   ├── CustomAlert.test.tsx
│   │   │   ├── DebugSheet.test.tsx
│   │   │   ├── GenerationSettingsModal.test.tsx
│   │   │   ├── ImageFilterBar.test.tsx
│   │   │   ├── MarkdownText.test.tsx
│   │   │   ├── ModelCard.test.tsx
│   │   │   ├── ModelPickerSheet.test.tsx
│   │   │   ├── ModelSelectorModal.test.tsx
│   │   │   ├── ProjectSelectorSheet.test.tsx
│   │   │   ├── RemoteServerModal.test.tsx
│   │   │   ├── SharePromptSheet.test.tsx
│   │   │   ├── ToolPickerSheet.test.tsx
│   │   │   └── VoiceRecordButton.test.tsx
│   │   ├── hooks/
│   │   │   └── useFocusTrigger.test.ts
│   │   ├── navigation/
│   │   │   └── AppNavigator.test.tsx
│   │   ├── onboarding/
│   │   │   ├── ChatScreenSpotlight.test.tsx
│   │   │   ├── ChatsListScreenSpotlight.test.tsx
│   │   │   ├── HomeScreenSpotlight.test.tsx
│   │   │   ├── ModelSettingsScreenSpotlight.test.tsx
│   │   │   └── ProjectEditScreenSpotlight.test.tsx
│   │   └── screens/
│   │       ├── ChatScreen.test.tsx
│   │       ├── ChatsListScreen.test.tsx
│   │       ├── DeviceInfoScreen.test.tsx
│   │       ├── DocumentPreviewScreen.test.tsx
│   │       ├── DownloadManagerScreen.test.tsx
│   │       ├── GalleryScreen.test.tsx
│   │       ├── HomeScreen.test.tsx
│   │       ├── KnowledgeBaseScreen.test.tsx
│   │       ├── LockScreen.test.tsx
│   │       ├── ModelDownloadHelpers.test.tsx
│   │       ├── ModelDownloadScreen.test.tsx
│   │       ├── ModelSettingsScreen.test.tsx
│   │       ├── ModelsScreen.test.tsx
│   │       ├── OnboardingScreen.test.tsx
│   │       ├── PassphraseSetupScreen.test.tsx
│   │       ├── ProjectChatsScreen.test.tsx
│   │       ├── ProjectDetailScreen.test.tsx
│   │       ├── ProjectEditScreen.test.tsx
│   │       ├── ProjectsScreen.test.tsx
│   │       ├── RemoteServersScreen.test.tsx
│   │       ├── SecuritySettingsScreen.test.tsx
│   │       ├── SettingsScreen.test.tsx
│   │       ├── StorageSettingsScreen.test.tsx
│   │       └── VoiceSettingsScreen.test.tsx
│   ├── specs/
│   │   ├── image-generation.yaml
│   │   ├── model-lifecycle.yaml
│   │   └── text-generation.yaml
│   ├── unit/
│   │   ├── components/
│   │   │   └── ChatMessage/
│   │   │       └── utils.test.ts
│   │   ├── constants/
│   │   │   └── constants.test.ts
│   │   ├── hooks/
│   │   │   ├── useAppState.test.ts
│   │   │   ├── useChatGenerationActions.test.ts
│   │   │   ├── useChatModelActions.test.ts
│   │   │   ├── useHomeScreen.test.ts
│   │   │   ├── useImageGenerationSettings.test.ts
│   │   │   ├── useKeyboardAwarePopover.test.ts
│   │   │   ├── useModelLoading.test.ts
│   │   │   ├── useTextGenerationAdvanced.test.ts
│   │   │   ├── useVoiceRecording.test.ts
│   │   │   └── useWhisperTranscription.test.ts
│   │   ├── onboarding/
│   │   │   ├── chatScreenSpotlight.test.ts
│   │   │   ├── checklistComponents.test.tsx
│   │   │   ├── handleStepPress.test.ts
│   │   │   ├── onboardingFlows.test.ts
│   │   │   ├── reactiveSpotlightConditions.test.ts
│   │   │   └── spotlightTooltips.test.ts
│   │   ├── screens/
│   │   │   ├── ChatScreen/
│   │   │   │   ├── toolUsage.test.ts
│   │   │   │   └── useSaveImage.test.ts
│   │   │   ├── DownloadManagerScreen/
│   │   │   │   └── items.test.tsx
│   │   │   └── ModelsScreen/
│   │   │       ├── imageDownloadActions.test.ts
│   │   │       ├── importHelpers.test.ts
│   │   │       ├── restoreImageDownloads.test.ts
│   │   │       ├── trendingSelection.test.ts
│   │   │       ├── useModelsScreen.test.ts
│   │   │       ├── useTextModels.handlers.test.ts
│   │   │       └── utils.test.ts
│   │   ├── services/
│   │   │   ├── authService.test.ts
│   │   │   ├── backgroundDownloadService.test.ts
│   │   │   ├── contextCompaction.test.ts
│   │   │   ├── coreMLModelBrowser.test.ts
│   │   │   ├── documentService.test.ts
│   │   │   ├── downloadHelpers.test.ts
│   │   │   ├── generationService.test.ts
│   │   │   ├── generationToolLoop.test.ts
│   │   │   ├── hardware.test.ts
│   │   │   ├── httpClient.test.ts
│   │   │   ├── huggingFaceModelBrowser.test.ts
│   │   │   ├── huggingface.test.ts
│   │   │   ├── imageGenerationHelpers.test.ts
│   │   │   ├── imageGenerator.test.ts
│   │   │   ├── imageModelRecommendation.test.ts
│   │   │   ├── intentClassifier.test.ts
│   │   │   ├── llm.test.ts
│   │   │   ├── llmHelpers.test.ts
│   │   │   ├── llmMessages.test.ts
│   │   │   ├── llmSafetyChecks.test.ts
│   │   │   ├── llmToolGeneration.test.ts
│   │   │   ├── localDreamGenerator.test.ts
│   │   │   ├── modelManager/
│   │   │   │   └── imageSync.test.ts
│   │   │   ├── modelManager.test.ts
│   │   │   ├── networkDiscovery.test.ts
│   │   │   ├── parallelMmproj.test.ts
│   │   │   ├── pdfExtractor.test.ts
│   │   │   ├── providers/
│   │   │   │   ├── localProvider.test.ts
│   │   │   │   ├── openAICompatibleProvider.test.ts
│   │   │   │   └── registry.test.ts
│   │   │   ├── rag/
│   │   │   │   ├── chunking.test.ts
│   │   │   │   ├── database.test.ts
│   │   │   │   ├── embedding.test.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── retrieval.test.ts
│   │   │   │   └── vectorMath.test.ts
│   │   │   ├── remoteServerManager.test.ts
│   │   │   ├── restore.test.ts
│   │   │   ├── toolHandlers.test.ts
│   │   │   ├── tools/
│   │   │   │   ├── handlers.test.ts
│   │   │   │   └── registry.test.ts
│   │   │   ├── voiceService.test.ts
│   │   │   └── whisperService.test.ts
│   │   ├── stores/
│   │   │   ├── appStore.test.ts
│   │   │   ├── appStoreSharePrompt.test.ts
│   │   │   ├── authStore.test.ts
│   │   │   ├── chatStore.test.ts
│   │   │   ├── projectStore.test.ts
│   │   │   ├── remoteServerStore.test.ts
│   │   │   └── whisperStore.test.ts
│   │   ├── theme/
│   │   │   └── palettes.test.ts
│   │   └── utils/
│   │       ├── coreMLModelUtils.test.ts
│   │       ├── downloadErrors.test.ts
│   │       ├── generateId.test.ts
│   │       ├── messageContent.test.ts
│   │       ├── network.test.ts
│   │       ├── pickerErrorUtils.test.ts
│   │       ├── resolvePickedFileUri.test.ts
│   │       └── sharePrompt.test.ts
│   └── utils/
│       ├── factories.ts
│       ├── spotlightMocks.tsx
│       └── testHelpers.ts
├── altstore-source.json
├── android/
│   ├── app/
│   │   ├── build.gradle
│   │   ├── debug.keystore
│   │   ├── lint-baseline.xml
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       ├── debug/
│   │       │   └── res/
│   │       │       └── values/
│   │       │           └── strings.xml
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── assets/
│   │       │   │   ├── index.android.bundle
│   │       │   │   └── models/
│   │       │   │       └── all-MiniLM-L6-v2-Q8_0.gguf
│   │       │   ├── java/
│   │       │   │   └── ai/
│   │       │   │       └── offgridmobile/
│   │       │   │           ├── MainActivity.kt
│   │       │   │           ├── MainApplication.kt
│   │       │   │           ├── SafePromise.kt
│   │       │   │           ├── download/
│   │       │   │           │   ├── DownloadCompleteBroadcastReceiver.kt
│   │       │   │           │   ├── DownloadDao.kt
│   │       │   │           │   ├── DownloadDatabase.kt
│   │       │   │           │   ├── DownloadEntity.kt
│   │       │   │           │   ├── DownloadEventBridge.kt
│   │       │   │           │   ├── DownloadManagerModule.kt
│   │       │   │           │   ├── DownloadManagerPackage.kt
│   │       │   │           │   ├── DownloadUiState.kt
│   │       │   │           │   ├── WorkerDownload.kt
│   │       │   │           │   └── WorkerDownloadStore.kt
│   │       │   │           ├── localdream/
│   │       │   │           │   ├── LocalDreamModule.kt
│   │       │   │           │   └── LocalDreamPackage.kt
│   │       │   │           └── pdf/
│   │       │   │               ├── PDFExtractorModule.kt
│   │       │   │               └── PDFExtractorPackage.kt
│   │       │   └── res/
│   │       │       ├── drawable/
│   │       │       │   ├── rn_edit_text_material.xml
│   │       │       │   └── splash_background.xml
│   │       │       ├── mipmap-anydpi-v26/
│   │       │       │   ├── ic_launcher.xml
│   │       │       │   └── ic_launcher_round.xml
│   │       │       ├── raw/
│   │       │       │   └── keep.xml
│   │       │       ├── values/
│   │       │       │   ├── ic_launcher_background.xml
│   │       │       │   ├── strings.xml
│   │       │       │   └── styles.xml
│   │       │       └── xml/
│   │       │           └── network_security_config.xml
│   │       └── test/
│   │           └── java/
│   │               └── ai/
│   │                   └── offgridmobile/
│   │                       ├── download/
│   │                       │   ├── DownloadCompleteBroadcastReceiverTest.kt
│   │                       │   └── DownloadManagerModuleTest.kt
│   │                       ├── localdream/
│   │                       │   └── LocalDreamModuleTest.kt
│   │                       └── rag/
│   │                           └── EmbeddingModelAssetTest.kt
│   ├── build.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   └── settings.gradle
├── app.json
├── babel.config.js
├── codecov.yml
├── docs/
│   ├── ARCHITECTURE.md
│   ├── PERFORMANCE_IMPROVEMENTS.md
│   ├── PERSONAS_IMPLEMENTATION_PLAN.md
│   ├── PRIVACY_POLICY.md
│   ├── TTS_IMPLEMENTATION_PLAN.md
│   ├── brand_tone_voice.md
│   ├── design/
│   │   ├── DESIGN_PHILOSOPHY_SYSTEM.md
│   │   └── VISUAL_HIERARCHY_STANDARD.md
│   ├── image-gen-without-text-model.md
│   ├── onboarding/
│   │   └── ONBOARDING_FLOWS.md
│   ├── standards/
│   │   └── CODEBASE_GUIDE.md
│   └── tests/
│       ├── QA_TEST_PLAN.md
│       └── QA_TEST_PLAN_TODO.md
├── e2e/
│   ├── maestro/
│   │   ├── import_vision_model.yaml
│   │   └── models_screen_navigation.yaml
│   └── scripts/
│       └── seedSimulatorFiles.js
├── index.js
├── ios/
│   ├── .Podfile.swp
│   ├── .xcode.env
│   ├── CoreMLDiffusionModule.m
│   ├── CoreMLDiffusionModule.swift
│   ├── DownloadManagerModule.m
│   ├── DownloadManagerModule.swift
│   ├── ExportOptions.plist
│   ├── OffgridMobile/
│   │   ├── AppDelegate.swift
│   │   ├── CoreMLDiffusion/
│   │   │   └── CoreMLDiffusionModule.m
│   │   ├── Download/
│   │   │   └── DownloadManagerModule.m
│   │   ├── Images.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   ├── Contents.json
│   │   │   └── Logo.imageset/
│   │   │       └── Contents.json
│   │   ├── Info.plist
│   │   ├── LaunchScreen.storyboard
│   │   ├── OffgridMobile-Bridging-Header.h
│   │   ├── OffgridMobile.entitlements
│   │   └── PrivacyInfo.xcprivacy
│   ├── OffgridMobile.xcodeproj/
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace/
│   │   │   ├── contents.xcworkspacedata
│   │   │   └── xcshareddata/
│   │   │       └── swiftpm/
│   │   │           └── Package.resolved
│   │   └── xcshareddata/
│   │       └── xcschemes/
│   │           └── OffgridMobile.xcscheme
│   ├── OffgridMobile.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       └── swiftpm/
│   │           └── Package.resolved
│   ├── OffgridMobileTests/
│   │   ├── EmbeddingModelBundleTests.swift
│   │   └── OffgridMobileTests.swift
│   ├── PDFExtractorModule.m
│   ├── PDFExtractorModule.swift
│   ├── Podfile
│   └── all-MiniLM-L6-v2-Q8_0.gguf
├── jest.config.js
├── jest.setup.ts
├── metro.config.js
├── package.json
├── patches/
│   ├── @react-native-voice+voice+3.2.4.patch
│   ├── react-native-device-info+15.0.1.patch
│   └── react-native-zip-archive+7.1.0.patch
├── scripts/
│   ├── release.sh
│   ├── run-sonar.sh
│   └── run-tests.sh
├── sonar-project.properties
├── src/
│   ├── components/
│   │   ├── AdvancedToggle.tsx
│   │   ├── AnimatedEntry.tsx
│   │   ├── AnimatedListItem.tsx
│   │   ├── AnimatedPressable.tsx
│   │   ├── AppSheet.styles.ts
│   │   ├── AppSheet.tsx
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   ├── ChatInput/
│   │   │   ├── Attachments.tsx
│   │   │   ├── Popovers.tsx
│   │   │   ├── Toolbar.tsx
│   │   │   ├── Voice.ts
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   └── useKeyboardAwarePopover.ts
│   │   ├── ChatMessage/
│   │   │   ├── components/
│   │   │   │   ├── ActionMenuSheet.tsx
│   │   │   │   ├── BlinkingCursor.tsx
│   │   │   │   ├── GenerationMeta.tsx
│   │   │   │   ├── MessageAttachments.tsx
│   │   │   │   ├── MessageContent.tsx
│   │   │   │   └── ThinkingBlock.tsx
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── CustomAlert.tsx
│   │   ├── DebugLogsScreen/
│   │   │   ├── index.tsx
│   │   │   └── styles.ts
│   │   ├── DebugSheet.tsx
│   │   ├── GenerationSettingsModal/
│   │   │   ├── ConversationActionsSection.tsx
│   │   │   ├── ImageGenerationSection.tsx
│   │   │   ├── ImageQualitySliders.tsx
│   │   │   ├── TextGenerationAdvanced.tsx
│   │   │   ├── TextGenerationSection.tsx
│   │   │   ├── index.tsx
│   │   │   └── styles.ts
│   │   ├── MadeWithLove.tsx
│   │   ├── MarkdownText.tsx
│   │   ├── ModelCard.styles.ts
│   │   ├── ModelCard.tsx
│   │   ├── ModelCardContent.tsx
│   │   ├── ModelSelectorModal/
│   │   │   ├── ImageTab.tsx
│   │   │   ├── TextTab.tsx
│   │   │   ├── index.tsx
│   │   │   ├── remoteStyles.ts
│   │   │   └── styles.ts
│   │   ├── ProjectSelectorSheet.tsx
│   │   ├── RemoteServerModal/
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   └── useRemoteServerForm.ts
│   │   ├── SharePromptSheet.tsx
│   │   ├── ThinkingIndicator.tsx
│   │   ├── ToolPickerSheet.tsx
│   │   ├── VoiceRecordButton/
│   │   │   ├── index.tsx
│   │   │   ├── states.tsx
│   │   │   └── styles.ts
│   │   ├── checklist/
│   │   │   ├── ProgressBar.tsx
│   │   │   ├── animations.ts
│   │   │   ├── index.ts
│   │   │   ├── types.ts
│   │   │   └── useOnboardingSteps.ts
│   │   ├── index.ts
│   │   └── onboarding/
│   │       ├── OnboardingSheet.tsx
│   │       ├── PulsatingIcon.tsx
│   │       ├── index.ts
│   │       ├── spotlightConfig.tsx
│   │       ├── spotlightState.ts
│   │       └── useOnboardingSheet.ts
│   ├── constants/
│   │   ├── index.ts
│   │   └── models.ts
│   ├── hooks/
│   │   ├── useActiveTextModel.ts
│   │   ├── useAppState.ts
│   │   ├── useFocusTrigger.ts
│   │   ├── useImageGenerationSettings.ts
│   │   ├── useTextGenerationAdvanced.ts
│   │   ├── useVoiceRecording.ts
│   │   └── useWhisperTranscription.ts
│   ├── navigation/
│   │   ├── AppNavigator.tsx
│   │   ├── index.ts
│   │   └── types.ts
│   ├── screens/
│   │   ├── ChatScreen/
│   │   │   ├── ChatMessageArea.tsx
│   │   │   ├── ChatModalSection.tsx
│   │   │   ├── ChatScreenComponents.tsx
│   │   │   ├── MessageRenderer.tsx
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   ├── stylesImage.ts
│   │   │   ├── toolUsage.ts
│   │   │   ├── types.ts
│   │   │   ├── useChatGenerationActions.ts
│   │   │   ├── useChatMessageHandlers.ts
│   │   │   ├── useChatModelActions.ts
│   │   │   ├── useChatScreen.ts
│   │   │   └── useSaveImage.ts
│   │   ├── ChatsListScreen.tsx
│   │   ├── DeviceInfoScreen.tsx
│   │   ├── DocumentPreviewScreen.tsx
│   │   ├── DownloadManagerScreen/
│   │   │   ├── index.tsx
│   │   │   ├── items.tsx
│   │   │   ├── styles.ts
│   │   │   └── useDownloadManager.ts
│   │   ├── GalleryScreen/
│   │   │   ├── FullscreenViewer.tsx
│   │   │   ├── GridItem.tsx
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   └── useGalleryActions.ts
│   │   ├── HomeScreen/
│   │   │   ├── components/
│   │   │   │   ├── ActiveModelsSection.tsx
│   │   │   │   ├── LoadingOverlay.tsx
│   │   │   │   ├── ModelPickerSheet.tsx
│   │   │   │   └── RecentConversations.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── useHomeScreen.ts
│   │   │   │   ├── useHomeScreenSpotlight.ts
│   │   │   │   ├── useLANDiscovery.ts
│   │   │   │   ├── useModelLoading.ts
│   │   │   │   └── useRemoteModelHandlers.ts
│   │   │   ├── index.tsx
│   │   │   └── styles.ts
│   │   ├── KnowledgeBaseScreen.styles.ts
│   │   ├── KnowledgeBaseScreen.tsx
│   │   ├── LockScreen.tsx
│   │   ├── ModelDownloadHelpers.tsx
│   │   ├── ModelDownloadScreen.tsx
│   │   ├── ModelSettingsScreen/
│   │   │   ├── ImageGenerationSection.tsx
│   │   │   ├── SystemPromptSection.tsx
│   │   │   ├── TextGenerationAdvanced.tsx
│   │   │   ├── TextGenerationSection.tsx
│   │   │   ├── index.tsx
│   │   │   └── styles.ts
│   │   ├── ModelsScreen/
│   │   │   ├── ImageFilterBar.tsx
│   │   │   ├── ImageModelsTab.tsx
│   │   │   ├── TextFiltersSection.tsx
│   │   │   ├── TextModelsTab.tsx
│   │   │   ├── constants.ts
│   │   │   ├── imageDownloadActions.ts
│   │   │   ├── imageStyles.ts
│   │   │   ├── importHelpers.ts
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   ├── types.ts
│   │   │   ├── useImageModels.ts
│   │   │   ├── useModelsScreen.ts
│   │   │   ├── useTextModels.ts
│   │   │   └── utils.ts
│   │   ├── OnboardingScreen.tsx
│   │   ├── OrphanedFilesSection.tsx
│   │   ├── PassphraseSetupScreen.tsx
│   │   ├── ProjectChatsScreen.tsx
│   │   ├── ProjectDetailKnowledgeBaseSection.tsx
│   │   ├── ProjectDetailScreen.styles.ts
│   │   ├── ProjectDetailScreen.tsx
│   │   ├── ProjectEditScreen.tsx
│   │   ├── ProjectsScreen.tsx
│   │   ├── RemoteServersScreen.styles.ts
│   │   ├── RemoteServersScreen.tsx
│   │   ├── SecuritySettingsScreen.tsx
│   │   ├── SettingsScreen.tsx
│   │   ├── StorageSettingsScreen.styles.ts
│   │   ├── StorageSettingsScreen.tsx
│   │   ├── VoiceSettingsScreen.tsx
│   │   └── index.ts
│   ├── services/
│   │   ├── activeModelService/
│   │   │   ├── index.ts
│   │   │   ├── loaders.ts
│   │   │   ├── memory.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── authService.ts
│   │   ├── backgroundDownloadService.ts
│   │   ├── backgroundDownloadTypes.ts
│   │   ├── contextCompaction.ts
│   │   ├── coreMLModelBrowser.ts
│   │   ├── documentService.ts
│   │   ├── generationService.ts
│   │   ├── generationServiceHelpers.ts
│   │   ├── generationToolLoop.ts
│   │   ├── hardware.ts
│   │   ├── httpClient.ts
│   │   ├── httpClientSSE.ts
│   │   ├── httpClientUtils.ts
│   │   ├── huggingFaceModelBrowser.ts
│   │   ├── huggingface.ts
│   │   ├── imageGenerationHelpers.ts
│   │   ├── imageGenerationService.ts
│   │   ├── imageGenerator.ts
│   │   ├── index.ts
│   │   ├── intentClassifier.ts
│   │   ├── llm.ts
│   │   ├── llmHelpers.ts
│   │   ├── llmMessages.ts
│   │   ├── llmSafetyChecks.ts
│   │   ├── llmToolGeneration.ts
│   │   ├── llmTypes.ts
│   │   ├── localDreamGenerator.ts
│   │   ├── modelManager/
│   │   │   ├── download.ts
│   │   │   ├── downloadHelpers.ts
│   │   │   ├── imageSync.ts
│   │   │   ├── index.ts
│   │   │   ├── restore.ts
│   │   │   ├── scan.ts
│   │   │   ├── storage.ts
│   │   │   └── types.ts
│   │   ├── networkDiscovery.ts
│   │   ├── pdfExtractor.ts
│   │   ├── providers/
│   │   │   ├── index.ts
│   │   │   ├── localProvider.ts
│   │   │   ├── openAICompatibleProvider.ts
│   │   │   ├── openAICompatibleStream.ts
│   │   │   ├── openAICompatibleTypes.ts
│   │   │   ├── openAIMessageBuilder.ts
│   │   │   ├── registry.ts
│   │   │   └── types.ts
│   │   ├── rag/
│   │   │   ├── chunking.ts
│   │   │   ├── database.ts
│   │   │   ├── embedding.ts
│   │   │   ├── index.ts
│   │   │   ├── retrieval.ts
│   │   │   └── vectorMath.ts
│   │   ├── remoteServerManager.ts
│   │   ├── remoteServerManagerUtils.ts
│   │   ├── tools/
│   │   │   ├── handlers.ts
│   │   │   ├── index.ts
│   │   │   ├── registry.ts
│   │   │   └── types.ts
│   │   ├── voiceService.ts
│   │   └── whisperService.ts
│   ├── stores/
│   │   ├── appStore.ts
│   │   ├── authStore.ts
│   │   ├── chatStore.ts
│   │   ├── debugLogsStore.ts
│   │   ├── index.ts
│   │   ├── projectStore.ts
│   │   ├── remoteModelCapabilities.ts
│   │   ├── remoteServerHelpers.ts
│   │   ├── remoteServerStore.ts
│   │   └── whisperStore.ts
│   ├── theme/
│   │   ├── index.ts
│   │   ├── palettes.ts
│   │   └── useThemedStyles.ts
│   ├── types/
│   │   ├── global.d.ts
│   │   ├── index.ts
│   │   ├── remoteServer.ts
│   │   └── whisper.rn.d.ts
│   └── utils/
│       ├── coreMLModelUtils.ts
│       ├── downloadErrors.ts
│       ├── generateId.ts
│       ├── haptics.ts
│       ├── logger.ts
│       ├── messageContent.ts
│       ├── network.ts
│       ├── pickerErrorUtils.ts
│       ├── resolvePickedFileUri.ts
│       ├── sharePrompt.ts
│       └── visionRepair.ts
├── tsconfig.json
└── website/
    ├── CNAME
    ├── Gemfile
    ├── _config.yml
    ├── _layouts/
    │   └── default.html
    ├── assets/
    │   └── css/
    │       └── main.css
    ├── early-access.md
    ├── ethos.md
    ├── guides/
    │   ├── android-setup.md
    │   ├── document-analysis.md
    │   ├── index.md
    │   ├── ios-setup.md
    │   ├── knowledge-base.md
    │   ├── lm-studio-android.md
    │   ├── ollama-android.md
    │   ├── remote-servers.md
    │   ├── run-llms-locally-android.md
    │   ├── run-llms-locally-iphone.md
    │   ├── stable-diffusion-android.md
    │   ├── stable-diffusion-iphone.md
    │   ├── tool-calling.md
    │   ├── vision-ai.md
    │   ├── voice-stt.md
    │   └── which-model.md
    ├── index.md
    ├── llms.txt
    ├── mission.md
    ├── quick-start.md
    ├── robots.txt
    ├── vision.md
    └── writing/
        ├── 200-year-secretary.md
        ├── 7-principles-personal-ai-os.md
        ├── a-day-with-personal-ai-os.md
        ├── architecture-of-trust.md
        ├── case-against-ai-subscriptions.md
        ├── context-gap.md
        ├── cross-device-sync-without-server.md
        ├── end-of-app-switching.md
        ├── how-personal-ai-should-act.md
        ├── index.md
        ├── intelligence-should-be-personal.md
        ├── next-virtual-assistant.md
        ├── one-person-two-devices.md
        ├── personal-ai-os-for-knowledge-workers.md
        ├── personal-ai-os-vs-assistant-vs-agent.md
        ├── phone-is-the-most-important-device.md
        ├── phone-laptop-know-nothing.md
        ├── platform-intelligence-doesnt-exist.md
        ├── privacy-is-not-a-feature.md
        ├── regulatory-case-for-on-device-ai.md
        ├── the-small-things.md
        ├── two-devices-zero-context.md
        ├── va-industry-disruption.md
        ├── walled-garden-problem.md
        ├── what-is-personal-ai-os.md
        ├── what-personal-ai-should-know.md
        ├── whatsapp-moment-for-ai.md
        ├── who-owns-your-ai-memory.md
        └── why-personal-ai-should-never-live-in-cloud.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .bundle/config
================================================
BUNDLE_PATH: "vendor/bundle"
BUNDLE_FORCE_RUBY_PLATFORM: 1


================================================
FILE: .eslintignore
================================================
# Generated build artifacts
android/app/build/
ios/build/
coverage/


================================================
FILE: .eslintrc.js
================================================
module.exports = {
  root: true,
  extends: '@react-native',
  plugins: [
    'react-native',
    'react',
    'react-hooks',
  ],
  env: {
    jest: true,
    browser: true,
    node: true,
    es6: true,
  },
  rules: {
    // TypeScript
    '@typescript-eslint/no-unused-vars': [
      'error',
      {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
        caughtErrorsIgnorePattern: '^_',
      },
    ],
    'no-shadow': 'off',
    '@typescript-eslint/no-shadow': 'error',

    // Code quality (built-in)
    'no-empty': 'error',
    'no-else-return': 'error',
    'prefer-template': 'error',
    complexity: ['error', 20],
    'max-lines-per-function': ['error', 350],
    'max-lines': ['error', 500],
    'max-params': ['error', 3],
    // React hooks
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',

    // React Native
    'react-native/no-unused-styles': 'error',
    'react-native/no-inline-styles': 'error',
    'react-native/no-color-literals': 'error',
    'react-native/no-raw-text': 'error',
    'react-native/no-single-element-style-arrays': 'error',
  },
  overrides: [
    {
      // Relax structural rules in test files — large test suites and helpers are acceptable
      files: ['__tests__/**/*', '*.test.ts', '*.test.tsx', 'jest.setup.ts'],
      rules: {
        'max-lines': 'off',
        'max-lines-per-function': 'off',
        'max-params': 'off',
        complexity: 'off',
        'react-native/no-inline-styles': 'off',
        'react-native/no-raw-text': 'off',
        'react-native/no-color-literals': 'off',
      },
    },
  ],
};


================================================
FILE: .gitattributes
================================================
releases/*.apk filter=lfs diff=lfs merge=lfs -text


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug Report
about: Report a bug or unexpected behavior
title: "[Bug] "
labels: bug
assignees: ''
---

## Description

<!-- A clear and concise description of what the bug is -->

## Steps to Reproduce

1.
2.
3.

## Expected Behavior

<!-- What you expected to happen -->

## Actual Behavior

<!-- What actually happened -->

## Screenshots / Screen Recordings

<!-- If applicable, add screenshots or recordings to help explain the problem -->

## Environment

- **Platform**: <!-- Android / iOS / Both -->
- **OS Version**: <!-- e.g. Android 14, iOS 17.2 -->
- **Device**: <!-- e.g. Pixel 8, iPhone 15 Pro, emulator -->
- **App Version**: <!-- e.g. 1.2.0 -->
- **Model in use**: <!-- e.g. llama-3.2-1b-instruct, or N/A -->

## Logs

<!-- Paste any relevant logs, error messages, or crash reports -->

```
(paste logs here)
```

## Additional Context

<!-- Any other context about the problem -->


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature Request
about: Suggest a new feature or improvement
title: "[Feature] "
labels: enhancement
assignees: ''
---

## Summary

<!-- A clear and concise description of the feature you'd like -->

## Problem / Motivation

<!-- What problem does this solve? Why is this feature needed? -->

## Proposed Solution

<!-- Describe the solution you'd like -->

## Alternatives Considered

<!-- Any alternative solutions or features you've considered -->

## Platform

- [ ] Android
- [ ] iOS
- [ ] Both

## Additional Context

<!-- Any mockups, references, or extra context -->


================================================
FILE: .github/pull_request_template.md
================================================
## Summary

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Refactor (code change that neither fixes a bug nor adds a feature)
- [ ] Chore (build process, CI, dependency updates, etc.)

## Screenshots / Screen Recordings

<!-- Mandatory for any UI change. Remove sections that don't apply. -->

### Android

| Before | After |
|--------|-------|
|        |       |

### iOS

| Before | After |
|--------|-------|
|        |       |

## Checklist

### General

- [ ] My code follows the project's coding style and conventions
- [ ] I have performed a self-review of my code
- [ ] I have added/updated comments where the logic isn't self-evident
- [ ] My changes generate no new warnings or errors

### Testing

- [ ] I have tested on **Android** (physical device or emulator)
- [ ] I have tested on **iOS** (physical device or simulator)
- [ ] I have tested in **light mode** and **dark mode**
- [ ] Existing tests pass locally (`npm test`)
- [ ] I have added tests that prove my fix is effective or my feature works

### React Native Specific

- [ ] No new native module without corresponding platform implementation (Android + iOS)
- [ ] New native modules are added to the Xcode project build target (`project.pbxproj`)
- [ ] No hardcoded pixel values — uses `SPACING` / `TYPOGRAPHY` constants from the theme
- [ ] Styles use `useThemedStyles` pattern (not inline or static `StyleSheet.create`)
- [ ] Animations/gestures work smoothly on both platforms
- [ ] Large lists use `FlatList` / `FlashList` (not `.map()` inside `ScrollView`)
- [ ] No unnecessary re-renders introduced (check with React DevTools Profiler if unsure)

### Performance & Models

- [ ] Downloads / long-running tasks report progress to the UI
- [ ] File paths are resolved correctly on both platforms (no hardcoded `/` vs `\\`)
- [ ] Large files (models, assets) are not committed to the repository

### Security

- [ ] No secrets, API keys, or credentials are included in the code
- [ ] User input is validated/sanitized where applicable

## Related Issues

<!-- Link any related issues: Fixes #123, Relates to #456 -->

## Additional Notes

<!-- Any context, trade-offs, or follow-up work worth mentioning -->


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  lint:
    runs-on: macos-latest

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

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

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Install dependencies
        run: npm ci

      - name: Install SwiftLint
        run: brew install swiftlint

      - name: Lint
        run: npm run lint

  typecheck:
    runs-on: ubuntu-latest

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

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

      - name: Install dependencies
        run: npm ci

      - name: Type check
        run: npx tsc --noEmit

  test:
    runs-on: macos-latest

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

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

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Install JS dependencies
        run: npm ci

      - name: Run Jest tests
        run: npx jest --coverage --forceExit

      - name: Run Android tests
        run: cd android && ./gradlew :app:testDebugUnitTest --rerun-tasks

      - name: Run iOS tests
        run: |
          cd ios && xcodebuild test \
            -workspace OffgridMobile.xcworkspace \
            -scheme OffgridMobile \
            -destination 'platform=iOS Simulator,name=iPhone 16' \
            -only-testing:OffgridMobileTests \
            2>&1 | (xcpretty 2>/dev/null || cat)

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info
          fail_ci_if_error: false

      - name: Upload iOS test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: ios-test-results
          path: ~/Library/Developer/Xcode/DerivedData/**/Logs/Test/*.xcresult
          if-no-files-found: ignore

      - name: Upload Android test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: android-test-results
          path: android/app/build/reports/tests/
          if-no-files-found: ignore

  android-build:
    runs-on: ubuntu-latest

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

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

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Install dependencies
        run: npm ci

      - name: Build Android Debug
        run: cd android && ./gradlew assembleDebug

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


================================================
FILE: .github/workflows/pages.yml
================================================
name: Deploy Off Grid Docs

on:
  push:
    branches: [main]
    paths: ['website/**']
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
          bundler-cache: true
          working-directory: website

      - name: Setup Pages
        uses: actions/configure-pages@v5

      - name: Build with Jekyll
        run: bundle exec jekyll build
        working-directory: website
        env:
          JEKYLL_ENV: production

      - name: Index with Pagefind
        run: npx pagefind --site website/_site

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: website/_site

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4


================================================
FILE: .github/workflows/release-ios.yml
================================================
name: Build and Release iOS

on:
  workflow_dispatch:  # Disabled - manual trigger only for now

permissions:
  contents: write

jobs:
  release-ios:
    runs-on: macos-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          ref: main
          fetch-depth: 0

      - name: Get version from package.json
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "VERSION=$VERSION" >> $GITHUB_ENV

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

      - name: Install dependencies
        run: npm ci

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true

      - name: Install CocoaPods
        run: |
          gem install cocoapods
          cd ios && pod install

      - name: Import signing certificate
        env:
          IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }}
          IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # Create temporary keychain
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
          security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

          # Import certificate
          CERT_PATH=$RUNNER_TEMP/certificate.p12
          printf '%s' "$IOS_CERTIFICATE_P12" | base64 --decode > "$CERT_PATH"
          # Convert password from UTF-8 to Latin-1 (£ char is 2 bytes in UTF-8 but security import expects 1 byte)
          CERT_PASS=$(printf '%s' "$IOS_CERTIFICATE_PASSWORD" | iconv -f UTF-8 -t ISO-8859-1)
          security import "$CERT_PATH" \
            -P "$CERT_PASS" \
            -A \
            -t cert \
            -f pkcs12 \
            -k "$KEYCHAIN_PATH"
          security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security list-keychain -d user -s "$KEYCHAIN_PATH"

      - name: Import provisioning profile
        env:
          IOS_PROVISION_PROFILE: ${{ secrets.IOS_PROVISION_PROFILE }}
        run: |
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          PP_PATH=~/Library/MobileDevice/Provisioning\ Profiles/e529cf17-07cc-43e0-94dd-3e2384d002ce.mobileprovision
          # Write base64 to temp file first to avoid shell interpretation issues
          printenv IOS_PROVISION_PROFILE > $RUNNER_TEMP/pp_b64.txt
          base64 --decode -i $RUNNER_TEMP/pp_b64.txt -o "$PP_PATH"
          # Verify — expected MD5: 445debe413481bd2085dddab92a78c14
          echo "Decoded profile size: $(wc -c < "$PP_PATH") bytes (expected: 14383)"
          echo "Decoded profile MD5: $(md5 -q "$PP_PATH")"

      - name: Sync version to Xcode project
        run: |
          VERSION_CODE=$(date +%s)
          sed -i '' "s/MARKETING_VERSION = .*/MARKETING_VERSION = ${{ env.VERSION }};/" \
            ios/OffgridMobile.xcodeproj/project.pbxproj
          sed -i '' "s/CURRENT_PROJECT_VERSION = .*/CURRENT_PROJECT_VERSION = $VERSION_CODE;/" \
            ios/OffgridMobile.xcodeproj/project.pbxproj

      - name: Build archive
        run: |
          xcodebuild archive \
            -workspace ios/OffgridMobile.xcworkspace \
            -scheme OffgridMobile \
            -configuration Release \
            -archivePath $RUNNER_TEMP/OffgridMobile.xcarchive \
            -destination "generic/platform=iOS" \
            CODE_SIGN_STYLE=Automatic \
            DEVELOPMENT_TEAM=84V6KCAC49 \
            -allowProvisioningUpdates

      - name: Export IPA
        run: |
          xcodebuild -exportArchive \
            -archivePath $RUNNER_TEMP/OffgridMobile.xcarchive \
            -exportOptionsPlist ios/ExportOptions.plist \
            -exportPath $RUNNER_TEMP/export

          # Rename IPA
          mv $RUNNER_TEMP/export/OffgridMobile.ipa \
             $RUNNER_TEMP/export/OffgridMobile-v${{ env.VERSION }}.ipa

      - name: Upload IPA to GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release upload v${{ env.VERSION }} \
            "$RUNNER_TEMP/export/OffgridMobile-v${{ env.VERSION }}.ipa" \
            --clobber

      - name: Update AltStore source JSON
        run: |
          IPA_SIZE=$(stat -f%z "$RUNNER_TEMP/export/OffgridMobile-v${{ env.VERSION }}.ipa")
          TODAY=$(date +%Y-%m-%d)
          DOWNLOAD_URL="https://github.com/alichherawalla/off-grid-mobile/releases/download/v${{ env.VERSION }}/OffgridMobile-v${{ env.VERSION }}.ipa"

          # Update altstore-source.json using node for reliable JSON manipulation
          node -e "
            const fs = require('fs');
            const source = JSON.parse(fs.readFileSync('altstore-source.json', 'utf8'));
            const app = source.apps[0];
            const newVersion = {
              version: '${{ env.VERSION }}',
              date: '${TODAY}',
              size: ${IPA_SIZE},
              downloadURL: '${DOWNLOAD_URL}',
              localizedDescription: 'Update to v${{ env.VERSION }}'
            };
            // Replace existing entry for this version or prepend
            const idx = app.versions.findIndex(v => v.version === '${{ env.VERSION }}');
            if (idx >= 0) {
              app.versions[idx] = newVersion;
            } else {
              app.versions.unshift(newVersion);
            }
            // Keep only the last 10 versions
            app.versions = app.versions.slice(0, 10);
            fs.writeFileSync('altstore-source.json', JSON.stringify(source, null, 2) + '\n');
          "

      - name: Commit updated AltStore source
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add altstore-source.json
          git diff --staged --quiet && echo "No changes to commit" && exit 0
          git commit -m "chore: update AltStore source for v${{ env.VERSION }} [skip ci]"
          git push

      - name: Cleanup keychain
        if: always()
        run: |
          security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true


================================================
FILE: .github/workflows/release.yml
================================================
name: Build and Release Android
# NOTE: The iOS workflow (release-ios.yml) triggers via workflow_run on this workflow.
# If you rename this workflow, update the workflow_run trigger in release-ios.yml.

on:
  workflow_dispatch:  # Disabled - manual trigger only for now

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          fetch-depth: 0  # Fetch all history for changelog

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

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Setup Android SDK
        uses: android-actions/setup-android@v3

      - name: Cache Gradle
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: gradle-

      - name: Install dependencies
        run: npm ci

      - name: Bump patch version
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          npm version patch --no-git-tag-version
          NEW_VERSION=$(node -p "require('./package.json').version")
          echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV

          # Update Android versionCode and versionName
          VERSION_CODE=$(date +%s)
          echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV

          # Update build.gradle
          sed -i "s/versionCode .*/versionCode $VERSION_CODE/" android/app/build.gradle
          sed -i "s/versionName .*/versionName \"$NEW_VERSION\"/" android/app/build.gradle

          git add package.json package-lock.json android/app/build.gradle
          git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
          git push

      - name: Generate release notes
        run: |
          # Get commits since last tag (or all commits if no tags)
          LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
          if [ -z "$LAST_TAG" ]; then
            COMMITS=$(git log --pretty=format:"- %s (%h)" --no-merges -20)
          else
            COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges)
          fi

          # Write release notes
          echo "## What's Changed" > release-notes.md
          echo "" >> release-notes.md
          echo "$COMMITS" >> release-notes.md
          echo "" >> release-notes.md
          echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${LAST_TAG:-v0.0.0}...v${{ env.NEW_VERSION }}" >> release-notes.md

          cat release-notes.md

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

      - name: Rename APK
        run: |
          mv android/app/build/outputs/apk/release/app-release.apk \
             android/app/build/outputs/apk/release/OffgridMobile-v${{ env.NEW_VERSION }}.apk

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release create v${{ env.NEW_VERSION }} \
            android/app/build/outputs/apk/release/OffgridMobile-v${{ env.NEW_VERSION }}.apk \
            --title "Off Grid v${{ env.NEW_VERSION }}" \
            --notes-file release-notes.md

      - name: Upload APK artifact
        uses: actions/upload-artifact@v4
        with:
          name: OffgridMobile-v${{ env.NEW_VERSION }}
          path: android/app/build/outputs/apk/standard/release/OffgridMobile-v${{ env.NEW_VERSION }}.apk
          if-no-files-found: error


================================================
FILE: .gitignore
================================================
# OSX
#
.DS_Store

# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
**/.xcode.env.local

# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
*.keystore
!debug.keystore
.kotlin/

# Environment variables
.env
.env.*

# node.js
#
node_modules/
npm-debug.log
yarn-error.log

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/

**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output

# Bundle artifact
*.jsbundle

# Ruby / CocoaPods
**/Pods/
/vendor/bundle/

# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*

# testing
/coverage

# Yarn
.yarn
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
docs/TRACTION_KNOWLEDGE_BASE.md

================================================
FILE: .husky/pre-push
================================================
#!/usr/bin/env sh

ZERO_OID="0000000000000000000000000000000000000000"

collect_changed_files() {
  changed_files=""

  if [ -t 0 ]; then
    git diff --name-only --diff-filter=ACMR @{upstream}..HEAD 2>/dev/null || true
    return
  fi

  while read -r local_ref local_oid remote_ref remote_oid; do
    [ -z "$local_oid" ] && continue
    [ "$local_oid" = "$ZERO_OID" ] && continue

    if [ "$remote_oid" = "$ZERO_OID" ]; then
      base=$(git merge-base "$local_oid" origin/main 2>/dev/null || true)
      if [ -n "$base" ]; then
        range="$base..$local_oid"
        changed=$(git diff --name-only --diff-filter=ACMR "$range")
      else
        changed=$(git diff-tree --no-commit-id --name-only -r --diff-filter=ACMR "$local_oid")
      fi
    else
      range="$remote_oid..$local_oid"
      changed=$(git diff --name-only --diff-filter=ACMR "$range")
    fi

    changed_files="${changed_files}
${changed}"
  done

  printf '%s\n' "$changed_files" | sed '/^$/d' | sort -u
}

CHANGED_FILES=$(collect_changed_files)

PUSHED_JS=$(printf '%s\n' "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
PUSHED_SWIFT=$(printf '%s\n' "$CHANGED_FILES" | grep '\.swift$' | grep -v 'Pods/' | grep -v 'build/' || true)
PUSHED_KOTLIN=$(printf '%s\n' "$CHANGED_FILES" | grep -E '\.(kt|kts)$' || true)

if [ -n "$PUSHED_JS" ]; then
  echo "▶ JS/TS lint (push range)..."
  echo "$PUSHED_JS" | tr '\n' '\0' | xargs -0 eslint --max-warnings=999

  echo "▶ TypeScript type check..."
  npx tsc --noEmit

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

if [ -n "$PUSHED_SWIFT" ]; then
  if command -v swiftlint >/dev/null 2>&1; then
    echo "▶ SwiftLint (push range)..."
    echo "$PUSHED_SWIFT" | tr '\n' '\0' | xargs -0 swiftlint lint --quiet
  else
    echo "⚠️  SwiftLint not installed — skipping Swift lint. Install: brew install swiftlint"
  fi

  echo "▶ iOS tests..."
  npm run test:ios
fi

if [ -n "$PUSHED_KOTLIN" ]; then
  echo "▶ Kotlin type check (compileDebugKotlin)..."
  (cd android && ./gradlew compileDebugKotlin --quiet)

  echo "▶ Android lint..."
  (cd android && ./gradlew :app:lintDebug --quiet)

  echo "▶ Android tests..."
  npm run test:android
fi

if [ -n "$PUSHED_JS$PUSHED_SWIFT$PUSHED_KOTLIN" ]; then
  echo "▶ Sonar scan..."
  npm run sonar
fi


================================================
FILE: .maestro/E2E_TESTING.md
================================================
# E2E Testing with Maestro

This directory contains end-to-end tests using [Maestro](https://maestro.mobile.dev/).

## Prerequisites

1. **Install Maestro CLI**
   ```bash
   curl -Ls "https://get.maestro.mobile.dev" | bash
   ```

2. **Android Setup**
   - Android device connected via USB with USB debugging enabled
   - OR Android emulator running
   - Verify with: `adb devices`

3. **iOS Setup** (macOS only)
   - iOS simulator running
   - OR physical device with developer mode enabled

4. **App Installed**
   - Build and install the app on your device:
     ```bash
     # Android
     npm run android

     # iOS
     npm run ios
     ```

## Running Tests

### Run All P0 Tests
```bash
maestro test .maestro/flows/p0/
```

### Run Single Test
```bash
maestro test .maestro/flows/p0/02-text-generation.yaml
```

### Run with Specific Device
```bash
# List devices
adb devices  # Android
xcrun simctl list devices  # iOS

# Run on specific device
maestro test --device <deviceId> .maestro/flows/p0/
```

### Run in CI Mode (no UI, headless)
```bash
maestro test --format junit .maestro/flows/p0/
```

## Test Structure

```
.maestro/
├── config.yaml           # Global configuration
├── E2E_TESTING.md        # This file
├── flows/
│   ├── p0/               # Critical path tests (run always)
│   │   ├── 01-app-launch.yaml
│   │   ├── 02-text-generation.yaml
│   │   ├── 03-stop-generation.yaml
│   │   ├── 04-model-loading.yaml
│   │   ├── 05-model-download.yaml
│   │   ├── 06-conversation-management.yaml
│   │   ├── 07-image-generation.yaml
│   │   └── 08-app-lifecycle.yaml
│   └── p1/               # Important tests (run on release)
└── utils/                # Reusable flow utilities
    └── wait-for-app-ready.yaml
```

## Test Priorities

- **P0 (Critical)**: App is unusable if broken. Run on every PR.
- **P1 (Important)**: Users notice if broken. Run on release builds.
- **P2 (Nice-to-have)**: Edge cases. Run weekly.

## Test IDs

These tests rely on `testID` props being set on React Native components.
Required test IDs:

### Core Navigation
- `home-screen`
- `chat-screen`
- `models-screen`
- `tab-bar`
- `new-chat-button`
- `models-tab`

### Chat Screen
- `chat-input`
- `send-button`
- `stop-button`
- `thinking-indicator`
- `streaming-message`
- `assistant-message`
- `model-selector`
- `model-loaded-indicator`

### Model Management
- `model-list`
- `model-item-{index}`
- `model-loading-indicator`
- `unload-model-button`
- `download-button`
- `download-progress`
- `download-complete`

### Conversation Management
- `conversation-list-button`
- `conversation-list`
- `conversation-item-{index}`

### Image Generation
- `image-model-loaded-indicator`
- `image-mode-toggle`
- `image-generation-progress`
- `generated-image`
- `image-message`
- `image-viewer`

## Writing New Tests

### Basic Structure
```yaml
appId: ai.offgridmobile
name: "Test Name"
tags:
  - p0
  - category
---

# Test steps
- launchApp
- assertVisible:
    id: "some-test-id"
- tapOn:
    id: "button-id"
```

### Common Patterns

**Wait for element**
```yaml
- assertVisible:
    id: "element-id"
    timeout: 10000
```

**Input text**
```yaml
- tapOn:
    id: "input-field"
- inputText: "Hello world"
```

**Conditional (optional) steps**
```yaml
- tapOn:
    id: "might-not-exist"
    optional: true
```

**Delays (use sparingly)**
```yaml
- delay: 2000
```

## Debugging

### Interactive Mode
```bash
maestro studio
```
Opens Maestro Studio for interactive test writing.

### View Logs
```bash
maestro test --debug .maestro/flows/p0/02-text-generation.yaml
```

### Screenshots
Screenshots are automatically saved on failure. Find them in:
```
~/.maestro/tests/<timestamp>/
```

## CI Integration

### GitHub Actions Example
```yaml
- name: Run E2E Tests
  run: |
    maestro test --format junit --output test-results.xml .maestro/flows/p0/

- name: Upload Results
  uses: actions/upload-artifact@v3
  with:
    name: e2e-results
    path: test-results.xml
```

## Required TestIDs to Add

The following testIDs need to be added to screen components for E2E tests to work:

### High Priority (P0 tests depend on these)

**HomeScreen.tsx**
```tsx
<View testID="home-screen">
<TouchableOpacity testID="new-chat-button">
```

**ChatScreen.tsx**
```tsx
<View testID="chat-screen">
<TouchableOpacity testID="model-selector">
<View testID="model-loaded-indicator">  // When model is loaded
<View testID="model-loading-indicator">  // During model load
<TouchableOpacity testID="conversation-list-button">
<View testID="assistant-message">  // On assistant message bubbles
<View testID="image-generation-progress">  // During image gen
<View testID="generated-image">  // On generated images
```

**ModelsScreen.tsx**
```tsx
<View testID="models-screen">
<TextInput testID="search-input">
<FlatList testID="models-list">
<TouchableOpacity testID="model-card-{index}">
<TouchableOpacity testID="download-button">
<View testID="download-progress">
<View testID="download-complete">
<TouchableOpacity testID="downloaded-tab">
```

**Navigation**
```tsx
<View testID="tab-bar">
<TouchableOpacity testID="models-tab">
```

**ConversationList (drawer or modal)**
```tsx
<View testID="conversation-list">
<TouchableOpacity testID="conversation-item-{index}">
```

### Existing TestIDs (Already in Place)

- `chat-input` - ChatInput component
- `send-button` - Send message button
- `stop-button` - Stop generation button
- `camera-button` - Camera/attachment button
- `image-mode-toggle` - Image generation toggle
- `thinking-indicator` - ThinkingIndicator component
- `streaming-cursor` - Cursor during streaming
- `message-text` - Message content
- `action-menu` - Message action menu

## Troubleshooting

### "No devices found"
- Ensure device/emulator is running
- Check `adb devices` output
- Restart ADB: `adb kill-server && adb start-server`

### "Element not found"
- Verify testID is set on the component
- Check spelling and case sensitivity
- Increase timeout value
- Use Maestro Studio to inspect element hierarchy

### "Timeout waiting for element"
- App might be slow on first launch
- Model loading takes time
- Increase timeout or add explicit delay


================================================
FILE: .maestro/config.yaml
================================================
# Maestro workspace config
#
# All flows use ${APP_ID} — pass it at runtime:
#
#   iOS:           maestro test -e APP_ID=ai.offgridmobile <flow>
#   Android debug: maestro test -e APP_ID=ai.offgridmobile.dev <flow>
#
# Or use the run script which auto-detects:
#   ./scripts/run-tests.sh [folder] [--ios | --android]


================================================
FILE: .maestro/flows/p0/00-setup-model.yaml
================================================
# P0 E2E: Setup - Ensure a text model is loaded and ready for chat
# This MUST run before all other tests
#
# Strategy: Simple and deterministic
# 1. Handle onboarding
# 2. If new-chat-button visible -> done
# 3. Otherwise navigate to Models tab directly and download if needed
# 4. Then select the model from picker

appId: ${APP_ID}
name: "P0: Setup Test Model"
tags:
  - p0
  - setup
---

# ==============================
# Launch app
# ==============================
- evalScript: "${console.log('SETUP - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000
- evalScript: "${console.log('SETUP - App ready')}"
- takeScreenshot: 01-launch

# ==============================
# Handle Onboarding
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('SETUP - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

# ==============================
# Handle Model Download Prompt
# ==============================
- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('SETUP - Skip download prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for Home
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('SETUP - On home')}"
- takeScreenshot: 02-home

# ==============================
# Check if model already loaded (early exit)
# ==============================
- runFlow:
    when:
      visible:
        id: "new-chat-button"
    commands:
      - evalScript: "${console.log('SETUP - Model already loaded!')}"
      - takeScreenshot: 03-done-early

# ==============================
# Need to setup model
# ==============================
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      - evalScript: "${console.log('SETUP - Need to load model')}"
      - takeScreenshot: 04-setup-card

      # First, check if there are any downloaded models
      # Open Text picker to see if models exist
      - evalScript: "${console.log('SETUP - Check for existing models')}"
      - tapOn:
          text: "Text"
      - extendedWaitUntil:
          visible:
            text: "Text Models"
          timeout: 5000
      - takeScreenshot: 05-picker

      # Check if "No models" message is visible
      - runFlow:
          when:
            visible:
              text: "No text models downloaded"
          commands:
            # No models exist, need to download one
            - evalScript: "${console.log('SETUP - No models, need to download')}"
            - takeScreenshot: 06-no-models

            # Close picker by tapping outside or back
            - tapOn:
                point: "50%,10%"

            # Go to Models tab
            - evalScript: "${console.log('SETUP - Go to Models tab')}"
            - tapOn:
                id: "models-tab"

            - extendedWaitUntil:
                visible:
                  id: "models-screen"
                timeout: 10000
            - evalScript: "${console.log('SETUP - On models screen')}"
            - takeScreenshot: 07-models-screen

            - extendedWaitUntil:
                visible:
                  id: "models-list"
                timeout: 15000

            # Search for SmolLM2 135M
            - evalScript: "${console.log('SETUP - Searching')}"
            - tapOn:
                id: "search-input"
            - inputText: "SmolLM2-135M-Instruct-GGUF unsloth"
            - tapOn:
                id: "search-button"
            - takeScreenshot: 08-search
            # Wait for result
            - extendedWaitUntil:
                visible:
                  text: "SmolLM2-135M-Instruct-GGUF"
                timeout: 30000
            - evalScript: "${console.log('SETUP - Found model')}"
            - takeScreenshot: 09-found
            - tapOn:
                text: "SmolLM2-135M-Instruct-GGUF"

            - extendedWaitUntil:
                visible:
                  text: "Available Files"
                timeout: 15000
            - takeScreenshot: 10-files

            # Download the model (tap the download icon on the first file card)
            - evalScript: "${console.log('SETUP - Downloading')}"
            - tapOn:
                id: "file-card-0-download"
            - extendedWaitUntil:
                visible:
                  text: "Success"
                timeout: 300000
            - evalScript: "${console.log('SETUP - Downloaded!')}"
            - takeScreenshot: 11-success
            - tapOn:
                text: "OK"

            # Go back to home
            - evalScript: "${console.log('SETUP - Back to home')}"
            - tapOn:
                id: "home-tab"
            - extendedWaitUntil:
                visible:
                  id: "home-screen"
                timeout: 10000
            - takeScreenshot: 12-home

            # Open Text picker again
            - evalScript: "${console.log('SETUP - Open picker again')}"
            - tapOn:
                text: "Text"
            - extendedWaitUntil:
                visible:
                  text: "Text Models"
                timeout: 5000
            - takeScreenshot: 13-picker

      # At this point, picker is open and there should be at least one model
      # Select the first available model
      - evalScript: "${console.log('SETUP - Select first model')}"
      - tapOn:
          index: 0
          id: "model-item"
      - evalScript: "${console.log('SETUP - Model tapped')}"
      - takeScreenshot: 14-tapped

      # Handle low memory warning
      - runFlow:
          when:
            visible:
              text: "Load Anyway"
          commands:
            - evalScript: "${console.log('SETUP - Low memory, load anyway')}"
            - tapOn:
                text: "Load Anyway"

      # Wait for model to load
      - evalScript: "${console.log('SETUP - Waiting for load')}"
      - extendedWaitUntil:
          visible:
            id: "new-chat-button"
          timeout: 120000
      - evalScript: "${console.log('SETUP - Model loaded!')}"
      - takeScreenshot: 15-loaded

# ==============================
# Final verification
# ==============================
- assertVisible:
    id: "new-chat-button"
- evalScript: "${console.log('SETUP - PASSED')}"
- takeScreenshot: 99-complete

================================================
FILE: .maestro/flows/p0/01-app-launch.yaml
================================================
# P0 E2E: App Launch
# Verifies the app launches successfully and shows home screen

appId: ${APP_ID}
name: "P0: App Launch"
tags:
  - p0
  - smoke
  - lifecycle
---

# Launch the app
- launchApp

# Wait for app to be ready (home screen visible)
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000

# Verify no crash occurred (app is still responsive)
- assertVisible:
    id: "home-screen"


================================================
FILE: .maestro/flows/p0/01a-onboarding-first-launch.yaml
================================================
# P0 E2E: 1.1 Onboarding appears on first launch
# Verifies onboarding shows on fresh install, slides work, and "Get Started" navigates to Model Download
# QA_TEST_PLAN §1.1

appId: ${APP_ID}
name: "P0: Onboarding First Launch"
tags:
  - p0
  - onboarding
  - first-launch
---

# ── Fresh install: wipe persisted state so hasCompletedOnboarding resets ──
- clearState
- launchApp

# Wait for JS bundle + app init + navigation (clearState resets persisted app state)
- extendedWaitUntil:
    visible:
      id: "onboarding-screen"
    timeout: 60000

# Must NOT show Home screen
- assertNotVisible:
    id: "home-screen"

# Wait for first slide keyword animation (500ms stagger)
- extendedWaitUntil:
    visible:
      text: "YOURS"
    timeout: 5000

# Verify Skip and Next buttons on first slide
- assertVisible:
    id: "onboarding-skip"
- assertVisible:
    text: "Next"

# ── Swipe through all slides ──

# Slide 1 → 2
- swipe:
    direction: LEFT
    duration: 400
- extendedWaitUntil:
    visible:
      text: "MAGIC"
    timeout: 5000

# Slide 2 → 3
- swipe:
    direction: LEFT
    duration: 400
- extendedWaitUntil:
    visible:
      text: "CREATE"
    timeout: 5000

# Skip still visible before last slide
- assertVisible:
    id: "onboarding-skip"

# Slide 3 → 4 (last)
- swipe:
    direction: LEFT
    duration: 400
- extendedWaitUntil:
    visible:
      text: "READY"
    timeout: 5000

# ── Last slide: "Get Started" shown, Skip hidden ──
- assertVisible:
    text: "Get Started"
- assertNotVisible:
    id: "onboarding-skip"

# ── Tap "Get Started" → Model Download screen ──
- tapOn:
    id: "onboarding-next"

- extendedWaitUntil:
    visible:
      id: "model-download-screen"
    timeout: 20000

- assertNotVisible:
    id: "onboarding-screen"


================================================
FILE: .maestro/flows/p0/01b-onboarding-skip.yaml
================================================
# P0 E2E: 1.2 Skip onboarding
# Verifies tapping "Skip" on any slide goes to Model Download screen
# QA_TEST_PLAN §1.2

appId: ${APP_ID}
name: "P0: Onboarding Skip"
tags:
  - p0
  - onboarding
  - skip
---

# Fresh install
- clearState
- launchApp

# Wait for onboarding
- extendedWaitUntil:
    visible:
      id: "onboarding-screen"
    timeout: 60000

# Tap Skip on the first slide
- tapOn:
    id: "onboarding-skip"

# Should go to Model Download screen
- extendedWaitUntil:
    visible:
      id: "model-download-screen"
    timeout: 20000

- assertNotVisible:
    id: "onboarding-screen"


================================================
FILE: .maestro/flows/p0/01c-model-download-first-time.yaml
================================================
# P0 E2E: 1.3 Model Download screen — first time
# Verifies Model Download screen shows device info and "Skip for Now" goes to Home
# QA_TEST_PLAN §1.3

appId: ${APP_ID}
name: "P0: Model Download First Time"
tags:
  - p0
  - onboarding
  - model-download
---

# Fresh install → skip onboarding to reach Model Download
- clearState
- launchApp

- extendedWaitUntil:
    visible:
      id: "onboarding-screen"
    timeout: 60000

- tapOn:
    id: "onboarding-skip"

# Wait for Model Download screen
- extendedWaitUntil:
    visible:
      id: "model-download-screen"
    timeout: 20000

# Verify screen content
- assertVisible:
    text: "Set Up Your AI"
- assertVisible:
    text: "Your Device"
- assertVisible:
    text: "Available Memory"

# Verify Skip for Now button exists
- assertVisible:
    id: "model-download-skip"

# Tap "Skip for Now" → Home screen
- tapOn:
    id: "model-download-skip"

- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000

# Verify setup card is shown (no model downloaded)
- assertVisible:
    id: "setup-card"


================================================
FILE: .maestro/flows/p0/01d-second-launch-no-onboarding.yaml
================================================
# P0 E2E: 1.5 Second launch — no onboarding
# Verifies relaunch skips onboarding entirely
# QA_TEST_PLAN §1.5
# Precondition: onboarding completed, no model downloaded (run after 01c)
# With no downloaded models, AppNavigator routes to ModelDownload, not Main

appId: ${APP_ID}
name: "P0: Second Launch No Onboarding"
tags:
  - p0
  - onboarding
  - relaunch
---

# Kill and relaunch (no clearState — keep persisted onboarding completion)
- stopApp
- launchApp

# No models downloaded → AppNavigator sends to ModelDownload (not Home)
- extendedWaitUntil:
    visible:
      id: "model-download-screen"
    timeout: 60000

# The key assertion: onboarding must NOT appear on second launch
- assertNotVisible:
    id: "onboarding-screen"


================================================
FILE: .maestro/flows/p0/01e-tab-navigation.yaml
================================================
# P0 E2E: 20.1 All 5 tabs
# Verifies all tab bar tabs are tappable and show correct screens
# QA_TEST_PLAN §20.1

appId: ${APP_ID}
name: "P0: Tab Navigation"
tags:
  - p0
  - navigation
  - tabs
---

- launchApp

# Handle whatever screen the app lands on — get to Home
- runFlow:
    when:
      visible:
        id: "onboarding-screen"
    commands:
      - tapOn:
          id: "onboarding-skip"
      - extendedWaitUntil:
          visible:
            id: "model-download-screen"
          timeout: 20000
      - tapOn:
          id: "model-download-skip"

- runFlow:
    when:
      visible:
        id: "model-download-screen"
    commands:
      - tapOn:
          id: "model-download-skip"

# 1. Home tab
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000

# 2. Chats tab
- tapOn:
    id: "chats-tab"
- extendedWaitUntil:
    visible:
      text: "Chats"
    timeout: 5000

# 3. Projects tab
- tapOn:
    id: "projects-tab"
- extendedWaitUntil:
    visible:
      text: "Projects"
    timeout: 5000

# 4. Models tab
- tapOn:
    id: "models-tab"
- extendedWaitUntil:
    visible:
      id: "models-screen"
    timeout: 5000

# 5. Settings tab
- tapOn:
    id: "settings-tab"
- extendedWaitUntil:
    visible:
      text: "Settings"
    timeout: 5000

# 6. Tap same tab repeatedly — no crash
- tapOn:
    id: "settings-tab"
- tapOn:
    id: "settings-tab"
- assertVisible:
    text: "Settings"

# Return to Home to confirm navigation still works
- tapOn:
    id: "home-tab"
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 5000


================================================
FILE: .maestro/flows/p0/02-text-generation.yaml
================================================
# P0 E2E: Text Generation Flow
# Tests the complete text generation cycle
# Prerequisites: Text model must be loaded

appId: ${APP_ID}
name: "P0: Text Generation"
tags:
  - p0
  - generation
  - text
---

- launchApp

# Wait for app initialization
- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# Handle onboarding if shown
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

# Handle download prompt if shown
- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - tapOn:
          text: "Skip for Now"

# Wait for home screen
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000

# Ensure model is loaded (run setup if needed)
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      - runFlow: 00-setup-model.yaml

# Wait for new-chat-button (model must be loaded)
- extendedWaitUntil:
    visible:
      id: "new-chat-button"
    timeout: 5000

# Navigate to chat screen
- tapOn:
    id: "new-chat-button"

# Wait for chat screen
- extendedWaitUntil:
    visible:
      id: "chat-screen"
    timeout: 10000

# Type a test message
- tapOn:
    id: "chat-input"
- inputText: "Hello, respond with one word: OK"

# Dismiss keyboard so send-button testID can be found
- hideKeyboard

# Send the message
- tapOn:
    id: "send-button"

# Wait for response (assistant message appears)
- extendedWaitUntil:
    visible:
      id: "assistant-message"
    timeout: 60000

# Verify input is ready for next message
- assertVisible:
    id: "chat-input"


================================================
FILE: .maestro/flows/p0/03-stop-generation.yaml
================================================
# P0 E2E: Stop Generation Flow
# Tests stopping an in-progress generation
# Prerequisites: Text model must be loaded

appId: ${APP_ID}
name: "P0: Stop Generation"
tags:
  - p0
  - generation
  - stop
---

- launchApp

# Wait for app initialization
- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# Handle onboarding if shown
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

# Handle download prompt if shown
- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - tapOn:
          text: "Skip for Now"

# Wait for home screen
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000

# Ensure model is loaded (run setup if needed)
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      - runFlow: 00-setup-model.yaml

# Wait for new-chat-button (model must be loaded)
- extendedWaitUntil:
    visible:
      id: "new-chat-button"
    timeout: 5000

# Navigate to chat screen
- tapOn:
    id: "new-chat-button"

- extendedWaitUntil:
    visible:
      id: "chat-screen"
    timeout: 10000

# Type a message that will generate a VERY long response
- tapOn:
    id: "chat-input"
- inputText: "Write a comprehensive 2000 word essay covering the complete history of artificial intelligence from the 1950s to today, including all major milestones, breakthroughs, key researchers, important papers, and technological developments in extreme detail"

# Dismiss keyboard before tapping send
- hideKeyboard

# Send the message
- tapOn:
    id: "send-button"

# Wait for stop button to appear (generation started)
- extendedWaitUntil:
    visible:
      id: "stop-button"
    timeout: 5000

# Tap stop immediately (small model generates fast)
- tapOn:
    id: "stop-button"

# Verify stop button disappears
- extendedWaitUntil:
    notVisible:
      id: "stop-button"
    timeout: 10000

# Dismiss voice input dialog if it appeared
- runFlow:
    when:
      visible:
        text: "Voice Input Unavailable"
    commands:
      - tapOn:
          text: "OK"

# Verify input is ready
- assertVisible:
    id: "chat-input"


================================================
FILE: .maestro/flows/p0/04-image-generation.yaml
================================================
# P0 E2E: Image Generation Flow
# Tests the complete image generation cycle including model download
# This test ensures image model is downloaded and tests image generation

appId: ${APP_ID}
name: "P0: Image Generation"
tags:
  - p0
  - generation
  - image
---

# ==============================
# Launch and setup
# ==============================
- evalScript: "${console.log('IMAGE_GEN - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# Handle onboarding
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('IMAGE_GEN - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

# Handle download prompt
- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('IMAGE_GEN - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# Wait for home screen
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('IMAGE_GEN - On home')}"

# Ensure text model is loaded
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      - evalScript: "${console.log('IMAGE_GEN - Load text model')}"
      - runFlow: 00-setup-model.yaml

# ==============================
# Ensure image model is active
# ==============================
# First check: Is image model already loaded on home screen?
- evalScript: "${console.log('IMAGE_GEN - Check image model status')}"

# Check if we need to select/download a model ("Tap to select" or "No models" visible)
- runFlow:
    when:
      visible:
        text: "Tap to select"
    commands:
      - evalScript: "${console.log('IMAGE_GEN - Model downloaded but not active, selecting...')}"
      - tapOn:
          id: "image-model-card"

      # Wait for picker to appear
      - extendedWaitUntil:
          visible:
            text: "Image Models"
          timeout: 5000

      # Select first model
      - tapOn:
          id: "model-item"
          index: 0

      # Wait for model to load
      - extendedWaitUntil:
          notVisible:
            text: "Tap to select"
          timeout: 30000

# If "No models" shown, need to download
- runFlow:
    when:
      visible:
        text: "No models"
    commands:
      - evalScript: "${console.log('IMAGE_GEN - No models, need to download')}"
      - tapOn:
          id: "image-model-card"

      # Wait for picker to appear
      - extendedWaitUntil:
          visible:
            text: "Image Models"
          timeout: 5000

      # If no models downloaded, go to Models screen
      - runFlow:
          when:
            visible:
              text: "No image models downloaded"
          commands:
            - evalScript: "${console.log('IMAGE_GEN - No models downloaded, need to download')}"

            # Close picker
            - tapOn:
                text: "Browse Models"

            # Wait for Models screen
            - extendedWaitUntil:
                visible:
                  id: "models-screen"
                timeout: 10000

            # Switch to Image Models tab
            - evalScript: "${console.log('IMAGE_GEN - Image Models tab')}"
            - tapOn:
                text: "Image Models"
            - takeScreenshot: 01-image-models-tab

            # Wait for models to load (no NPU filter, use CPU)
            - extendedWaitUntil:
                visible:
                  text: "Absolute Reality (CPU)"
                timeout: 5000

            # Tap download icon on 1st image model card
            - evalScript: "${console.log('IMAGE_GEN - Downloading CPU model')}"
            - tapOn:
                id: "image-model-card-0-download"

            # Wait for download to complete
            - evalScript: "${console.log('IMAGE_GEN - Waiting for download...')}"
            - extendedWaitUntil:
                visible:
                  text: "Success"
                timeout: 180000

            - evalScript: "${console.log('IMAGE_GEN - Download complete, auto-activated!')}"
            - takeScreenshot: 02-download-complete
            - tapOn:
                text: "OK"

            # Go back to home (model is auto-activated after download)
            - evalScript: "${console.log('IMAGE_GEN - Back to home')}"
            - tapOn:
                text: "Home"

            - extendedWaitUntil:
                visible:
                  id: "home-screen"
                timeout: 10000

# Ensure we're on home screen before continuing
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 10000

# ==============================
# Test image generation
# ==============================
- evalScript: "${console.log('IMAGE_GEN - Start chat')}"
- tapOn:
    id: "new-chat-button"

- extendedWaitUntil:
    visible:
      id: "chat-screen"
    timeout: 10000

# ==============================
# Configure auto-detect settings
# ==============================
- evalScript: "${console.log('IMAGE_GEN - Configure settings')}"
- tapOn:
    id: "chat-settings-icon"

# Expand the IMAGE GENERATION section (collapsed by default)
- extendedWaitUntil:
    visible:
      text: "IMAGE GENERATION"
    timeout: 5000
- tapOn:
    text: "IMAGE GENERATION"

- extendedWaitUntil:
    visible:
      text: "Auto-detect image requests"
    timeout: 5000

# Tap on Auto mode
- evalScript: "${console.log('IMAGE_GEN - Set Auto mode')}"
- tapOn:
    id: "image-gen-mode-auto"

# Tap on Pattern
- evalScript: "${console.log('IMAGE_GEN - Set Pattern')}"
- tapOn:
    id: "auto-detect-method-pattern"

# Close settings modal
- evalScript: "${console.log('IMAGE_GEN - Close settings')}"
- tapOn:
    text: "Done"

# Type an image generation prompt
- evalScript: "${console.log('IMAGE_GEN - Type prompt')}"
- tapOn:
    id: "chat-input"
- inputText: "Draw a picture of a cute cat"

# Dismiss keyboard
- hideKeyboard

# Send the message
- evalScript: "${console.log('IMAGE_GEN - Send')}"
- tapOn:
    id: "send-button"

# Wait for image generation to complete (3 min timeout)
- evalScript: "${console.log('IMAGE_GEN - Wait for image')}"
- extendedWaitUntil:
    visible:
      id: "generated-image"
    timeout: 180000

- evalScript: "${console.log('IMAGE_GEN - Image generated!')}"
- takeScreenshot: 02-image-generated

# Verify image can be tapped
- tapOn:
    id: "generated-image"
- takeScreenshot: 03-image-viewer

# Close image viewer
- back

- evalScript: "${console.log('IMAGE_GEN - PASSED')}"


================================================
FILE: .maestro/flows/p1/06a-document-attachment.yaml
================================================
# P0 E2E: Document Attachment Flow
# Tests attaching a document to a chat message and sending it
# Prerequisites: Text model must be loaded
#
# Strategy:
# 1. Open chat
# 2. Tap document picker button
# 3. Verify picker opens (native file picker)
# 4. Cancel picker (we can't select files in Maestro native pickers reliably)
# 5. Verify chat input is still usable after picker dismissal

appId: ${APP_ID}
name: "P0: Document Attachment"
tags:
  - p0
  - attachment
  - document
---

# ==============================
# Launch app
# ==============================
- evalScript: "${console.log('DOC_ATTACH - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# ==============================
# Handle onboarding
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('DOC_ATTACH - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('DOC_ATTACH - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for home screen
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('DOC_ATTACH - On home')}"

# ==============================
# Ensure model is loaded
# ==============================
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      - evalScript: "${console.log('DOC_ATTACH - Load model')}"
      - runFlow: 00-setup-model.yaml

- extendedWaitUntil:
    visible:
      id: "new-chat-button"
    timeout: 5000

# ==============================
# Open chat
# ==============================
- evalScript: "${console.log('DOC_ATTACH - Open chat')}"
- tapOn:
    id: "new-chat-button"

- extendedWaitUntil:
    visible:
      id: "chat-screen"
    timeout: 10000
- takeScreenshot: 01-chat-screen

# ==============================
# Test: Document picker button exists
# ==============================
- evalScript: "${console.log('DOC_ATTACH - Verify document picker button')}"
- assertVisible:
    id: "document-picker-button"
- takeScreenshot: 02-picker-button-visible

# ==============================
# Test: Tap document picker
# ==============================
- evalScript: "${console.log('DOC_ATTACH - Tap document picker')}"
- tapOn:
    id: "document-picker-button"

# Native file picker opens - wait briefly then dismiss
# On Android, back dismisses the picker; on iOS, cancel button
- evalScript: "${console.log('DOC_ATTACH - Dismiss native picker')}"
- back
- takeScreenshot: 03-after-picker-dismiss

# ==============================
# Verify chat is still functional after picker dismissal
# ==============================
- evalScript: "${console.log('DOC_ATTACH - Verify chat still functional')}"
- extendedWaitUntil:
    visible:
      id: "chat-input"
    timeout: 5000

# Type and send a message to verify chat works
- tapOn:
    id: "chat-input"
- inputText: "Hello"
- hideKeyboard

- assertVisible:
    id: "send-button"
- takeScreenshot: 04-chat-functional

# ==============================
# Verify send works after picker interaction
# ==============================
- evalScript: "${console.log('DOC_ATTACH - Send message')}"
- tapOn:
    id: "send-button"

- extendedWaitUntil:
    visible:
      id: "assistant-message"
    timeout: 60000

- evalScript: "${console.log('DOC_ATTACH - PASSED')}"
- takeScreenshot: 99-complete


================================================
FILE: .maestro/flows/p1/06b-image-attachment.yaml
================================================
# P0 E2E: Image Attachment Flow
# Tests the image attachment button and camera/library picker dialog
# Prerequisites: Text model must be loaded
#
# Strategy:
# 1. Open chat
# 2. Verify camera button visibility (depends on vision support)
# 3. Tap camera button
# 4. Verify source selection dialog appears (Camera / Photo Library)
# 5. Dismiss dialog
# 6. Verify chat remains functional

appId: ${APP_ID}
name: "P0: Image Attachment"
tags:
  - p0
  - attachment
  - image
---

# ==============================
# Launch app
# ==============================
- evalScript: "${console.log('IMG_ATTACH - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# ==============================
# Handle onboarding
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('IMG_ATTACH - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('IMG_ATTACH - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for home screen
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('IMG_ATTACH - On home')}"

# ==============================
# Ensure model is loaded
# ==============================
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      - evalScript: "${console.log('IMG_ATTACH - Load model')}"
      - runFlow: 00-setup-model.yaml

- extendedWaitUntil:
    visible:
      id: "new-chat-button"
    timeout: 5000

# ==============================
# Open chat
# ==============================
- evalScript: "${console.log('IMG_ATTACH - Open chat')}"
- tapOn:
    id: "new-chat-button"

- extendedWaitUntil:
    visible:
      id: "chat-screen"
    timeout: 10000
- takeScreenshot: 01-chat-screen

# ==============================
# Test: Camera button (vision-capable models only)
# ==============================
# Camera button only appears when model supports vision (mmProjPath)
# If camera button is visible, test the image picker flow
- runFlow:
    when:
      visible:
        id: "camera-button"
    commands:
      - evalScript: "${console.log('IMG_ATTACH - Camera button visible (vision model)')}"
      - takeScreenshot: 02-camera-button

      # Tap camera button to open source selection
      - tapOn:
          id: "camera-button"

      # Verify source selection dialog appears
      - extendedWaitUntil:
          visible:
            text: "Camera"
          timeout: 5000
      - evalScript: "${console.log('IMG_ATTACH - Source selection dialog shown')}"
      - takeScreenshot: 03-source-dialog

      # Verify both options exist
      - assertVisible:
          text: "Camera"
      - assertVisible:
          text: "Photo Library"

      # Dismiss dialog by tapping Cancel
      - tapOn:
          text: "Cancel"
      - evalScript: "${console.log('IMG_ATTACH - Dialog dismissed')}"
      - takeScreenshot: 04-dialog-dismissed

      # Verify vision indicator badge is shown
      - assertVisible:
          id: "vision-indicator"
      - evalScript: "${console.log('IMG_ATTACH - Vision indicator visible')}"

# If no camera button, model doesn't support vision - that's OK
- runFlow:
    when:
      notVisible:
        id: "camera-button"
    commands:
      - evalScript: "${console.log('IMG_ATTACH - No camera button (non-vision model), skipping image tests')}"
      - takeScreenshot: 02-no-camera-button

# ==============================
# Verify chat is still functional
# ==============================
- evalScript: "${console.log('IMG_ATTACH - Verify chat functional')}"
- tapOn:
    id: "chat-input"
- inputText: "Say OK"
- hideKeyboard

- tapOn:
    id: "send-button"

- extendedWaitUntil:
    visible:
      id: "assistant-message"
    timeout: 60000

- evalScript: "${console.log('IMG_ATTACH - PASSED')}"
- takeScreenshot: 99-complete


================================================
FILE: .maestro/flows/p1/06c-text-generation-full.yaml
================================================
# P0 E2E: Text Generation - Full Flow
# Tests the complete text generation lifecycle including:
# - Sending a message and receiving a response
# - Streaming state (thinking indicator, streaming cursor)
# - Generation metadata display (tokens/sec)
# - Message actions (copy, retry)
# - Multi-turn conversation
# - Chat input queue indicator
#
# Prerequisites: Text model must be loaded

appId: ${APP_ID}
name: "P0: Text Generation Full"
tags:
  - p0
  - generation
  - text
  - full
---

# ==============================
# Launch app
# ==============================
- evalScript: "${console.log('TEXT_GEN - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# ==============================
# Handle onboarding
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('TEXT_GEN - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('TEXT_GEN - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for home screen
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('TEXT_GEN - On home')}"

# ==============================
# Ensure model is loaded
# ==============================
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      - evalScript: "${console.log('TEXT_GEN - Load model')}"
      - runFlow: 00-setup-model.yaml

- extendedWaitUntil:
    visible:
      id: "new-chat-button"
    timeout: 5000

# ==============================
# Open new chat
# ==============================
- evalScript: "${console.log('TEXT_GEN - Open chat')}"
- tapOn:
    id: "new-chat-button"

- extendedWaitUntil:
    visible:
      id: "chat-screen"
    timeout: 10000
- takeScreenshot: 01-chat-screen

# ==============================
# Verify model selector is visible
# ==============================
- evalScript: "${console.log('TEXT_GEN - Verify model selector')}"
- assertVisible:
    id: "model-selector"
- assertVisible:
    id: "model-loaded-indicator"
- takeScreenshot: 02-model-info

# ==============================
# Test 1: Send first message and get response
# ==============================
- evalScript: "${console.log('TEXT_GEN - Send first message')}"
- tapOn:
    id: "chat-input"
- inputText: "Hello, respond with one word: OK"
- hideKeyboard

- tapOn:
    id: "send-button"

# Verify user message appears
- extendedWaitUntil:
    visible:
      id: "user-message"
    timeout: 5000
- evalScript: "${console.log('TEXT_GEN - User message shown')}"

# Wait for assistant response
- extendedWaitUntil:
    visible:
      id: "assistant-message"
    timeout: 60000
- evalScript: "${console.log('TEXT_GEN - Assistant responded')}"
- takeScreenshot: 03-first-response

# ==============================
# Verify generation metadata is shown
# ==============================
- evalScript: "${console.log('TEXT_GEN - Check generation meta')}"
- assertVisible:
    id: "generation-meta"
- takeScreenshot: 04-generation-meta

# ==============================
# Test 2: Message actions - long press to show action menu
# ==============================
- evalScript: "${console.log('TEXT_GEN - Test action menu')}"
- longPressOn:
    id: "assistant-message"

- extendedWaitUntil:
    visible:
      id: "action-menu"
    timeout: 5000
- takeScreenshot: 05-action-menu

# Verify copy action exists
- assertVisible:
    id: "action-copy"

# Verify retry action exists
- assertVisible:
    id: "action-retry"

# Dismiss action menu
- tapOn:
    id: "action-copy"
- evalScript: "${console.log('TEXT_GEN - Copied message')}"

# ==============================
# Test 3: Multi-turn conversation
# ==============================
- evalScript: "${console.log('TEXT_GEN - Send second message')}"
- tapOn:
    id: "chat-input"
- inputText: "Now say the word HELLO"
- hideKeyboard

- tapOn:
    id: "send-button"

# Wait for second response
- extendedWaitUntil:
    visible:
      id: "message-text"
    timeout: 60000
- evalScript: "${console.log('TEXT_GEN - Second response received')}"
- takeScreenshot: 06-multi-turn

# ==============================
# Test 4: Chat settings accessible
# ==============================
- evalScript: "${console.log('TEXT_GEN - Open settings')}"
- tapOn:
    id: "chat-settings-icon"

# Verify settings modal appears with generation options
- extendedWaitUntil:
    visible:
      text: "Temperature"
    timeout: 5000
- evalScript: "${console.log('TEXT_GEN - Settings modal shown')}"
- takeScreenshot: 07-settings

# Close settings
- tapOn:
    text: "Done"
- evalScript: "${console.log('TEXT_GEN - Settings closed')}"

# ==============================
# Verify input ready for next message
# ==============================
- assertVisible:
    id: "chat-input"
- assertVisible:
    id: "document-picker-button"

- evalScript: "${console.log('TEXT_GEN - PASSED')}"
- takeScreenshot: 99-complete


================================================
FILE: .maestro/flows/p1/06d-text-generation-retry.yaml
================================================
# P0 E2E: Text Generation - Retry Flow
# Tests retrying a generation from the message action menu
# Prerequisites: Text model must be loaded

appId: ${APP_ID}
name: "P0: Text Generation Retry"
tags:
  - p0
  - generation
  - text
  - retry
---

# ==============================
# Launch app
# ==============================
- evalScript: "${console.log('RETRY - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# ==============================
# Handle onboarding
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('RETRY - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('RETRY - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for home screen
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000

# ==============================
# Ensure model is loaded
# ==============================
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      - runFlow: 00-setup-model.yaml

- extendedWaitUntil:
    visible:
      id: "new-chat-button"
    timeout: 5000

# ==============================
# Open chat and send initial message
# ==============================
- evalScript: "${console.log('RETRY - Open chat')}"
- tapOn:
    id: "new-chat-button"

- extendedWaitUntil:
    visible:
      id: "chat-screen"
    timeout: 10000

- tapOn:
    id: "chat-input"
- inputText: "Say exactly: FIRST"
- hideKeyboard
- tapOn:
    id: "send-button"

# Wait for response
- extendedWaitUntil:
    visible:
      id: "assistant-message"
    timeout: 60000
- evalScript: "${console.log('RETRY - First response received')}"
- takeScreenshot: 01-first-response

# ==============================
# Test: Retry generation
# ==============================
- evalScript: "${console.log('RETRY - Long press for action menu')}"
- longPressOn:
    id: "assistant-message"

- extendedWaitUntil:
    visible:
      id: "action-menu"
    timeout: 5000

# Tap retry
- evalScript: "${console.log('RETRY - Tap retry')}"
- tapOn:
    id: "action-retry"

# Wait for new response (retry replaces the previous assistant message)
- extendedWaitUntil:
    visible:
      id: "assistant-message"
    timeout: 60000
- evalScript: "${console.log('RETRY - Retry response received')}"
- takeScreenshot: 02-retry-response

# Verify generation metadata on retried message
- assertVisible:
    id: "generation-meta"

# Verify chat input is ready
- assertVisible:
    id: "chat-input"

- evalScript: "${console.log('RETRY - PASSED')}"
- takeScreenshot: 99-complete


================================================
FILE: .maestro/flows/p2/05a-model-uninstall.yaml
================================================
# P0 E2E: Model Uninstall
# Tests deleting a downloaded model
#
# Precondition: At least one model downloaded
# Test: Go to Models tab → Delete model → Verify removed
# Postcondition: Model removed from downloaded list

appId: ${APP_ID}
name: "P0: Model Uninstall"
tags:
  - p0
  - model-uninstall
---

# ==============================
# Launch app
# ==============================
- evalScript: "${console.log('UNINSTALL - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000
- evalScript: "${console.log('UNINSTALL - App ready')}"

# ==============================
# Skip onboarding if needed
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('UNINSTALL - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('UNINSTALL - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for home screen
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('UNINSTALL - On home')}"
- takeScreenshot: 01-home

# ==============================
# Ensure at least one model exists (precondition)
# ==============================
# Just run setup to ensure a model is downloaded
- runFlow: 00-setup-model.yaml

# ==============================
# Navigate to Models screen
# ==============================
- evalScript: "${console.log('UNINSTALL - Go to Models')}"
- tapOn:
    text: "Models"

- extendedWaitUntil:
    visible:
      id: "models-screen"
    timeout: 10000
- takeScreenshot: 02-models-screen


# ==============================
# Tap downloads icon in top right
# ==============================
- evalScript: "${console.log('UNINSTALL - Tap downloads icon')}"
- tapOn:
    id: "downloads-icon"

# Wait for Download Manager screen
- extendedWaitUntil:
    visible:
      id: "downloaded-models-screen"
    timeout: 10000
- takeScreenshot: 03-downloads-screen

# ==============================
# Test: Delete first downloaded model
# ==============================
- evalScript: "${console.log('UNINSTALL - Tap delete icon')}"
- tapOn:
    index: 0
    id: "delete-model-button"
- takeScreenshot: 04-delete-confirm

# Confirm deletion
- extendedWaitUntil:
    visible:
      text: "Delete"
    timeout: 5000
- evalScript: "${console.log('UNINSTALL - Confirm delete')}"
- tapOn:
    text: "Delete"

# ==============================
# Verify deletion
# ==============================
- evalScript: "${console.log('UNINSTALL - Verify deleted')}"
- takeScreenshot: 09-after-delete

- evalScript: "${console.log('UNINSTALL - PASSED')}"
- takeScreenshot: 99-complete


================================================
FILE: .maestro/flows/p2/05b-model-download.yaml
================================================
# P0 E2E: Model Download
# Tests the full model download flow from search to completion
#
# Precondition: Clean app state (uses clearState)
# Test: Search for model → Download → Verify success
# Postcondition: Model downloaded and available

appId: ${APP_ID}
name: "P0: Model Download"
tags:
  - p0
  - model-download
---

# ==============================
# Launch app
# ==============================
- clearState
- evalScript: "${console.log('DOWNLOAD - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000
- evalScript: "${console.log('DOWNLOAD - App ready')}"

# ==============================
# Skip onboarding
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('DOWNLOAD - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

# Skip download prompt
- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('DOWNLOAD - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for home screen
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('DOWNLOAD - On home')}"
- takeScreenshot: 01-home

# ==============================
# Navigate to Models tab
# ==============================
- evalScript: "${console.log('DOWNLOAD - Go to Models')}"
- tapOn:
    id: "models-tab"

- extendedWaitUntil:
    visible:
      id: "models-screen"
    timeout: 10000
- takeScreenshot: 02-models-screen

- extendedWaitUntil:
    visible:
      id: "models-list"
    timeout: 15000

# ==============================
# Search for SmolLM2 135M
# ==============================
- evalScript: "${console.log('DOWNLOAD - Search for model')}"
- tapOn:
    id: "search-input"
- inputText: "SmolLM2-135M-Instruct-GGUF unsloth"
- tapOn:
    id: "search-button"
- takeScreenshot: 03-search

# Wait for results
- extendedWaitUntil:
    visible:
      text: "SmolLM2-135M-Instruct-GGUF"
    timeout: 30000
- evalScript: "${console.log('DOWNLOAD - Found model')}"
- takeScreenshot: 04-found

# ==============================
# Open model details
# ==============================
- tapOn:
    text: "SmolLM2-135M-Instruct-GGUF"

- extendedWaitUntil:
    visible:
      text: "Available Files"
    timeout: 15000
- takeScreenshot: 05-files

# ==============================
# Download the model
# ==============================
- evalScript: "${console.log('DOWNLOAD - Start download')}"
- tapOn:
    text: "Download"

# Wait for download to complete (5 min timeout)
- extendedWaitUntil:
    visible:
      text: "Success"
    timeout: 300000
- evalScript: "${console.log('DOWNLOAD - Complete!')}"
- takeScreenshot: 06-success

# ==============================
# Verify and close
# ==============================
- assertVisible:
    text: "Success"
- tapOn:
    text: "OK"

- evalScript: "${console.log('DOWNLOAD - PASSED')}"
- takeScreenshot: 99-complete


================================================
FILE: .maestro/flows/p2/05b-model-selection.yaml
================================================
# P0 E2E: Model Selection
# Tests selecting a model from multiple downloaded models
#
# Precondition: At least 2 models downloaded, none loaded
# Test: Open picker → Select model → Verify loaded
# Postcondition: Model loaded and ready

appId: ${APP_ID}
name: "P0: Model Selection"
tags:
  - p0
  - model-selection
---

# ==============================
# Launch app
# ==============================
- evalScript: "${console.log('SELECTION - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000
- evalScript: "${console.log('SELECTION - App ready')}"

# ==============================
# Skip onboarding if needed
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('SELECTION - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('SELECTION - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for home screen
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('SELECTION - On home')}"
- takeScreenshot: 01-home

# ==============================
# Ensure we have at least 2 models
# ==============================
# If no models, download 2
# If 1 model, download 1 more
# This ensures we can test selection between multiple models

- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      # No models loaded, check if any downloaded
      - evalScript: "${console.log('SELECTION - Check models')}"
      - tapOn:
          text: "Text"
      - extendedWaitUntil:
          visible:
            text: "Text Models"
          timeout: 5000

      # If no models exist, download one via setup
      - runFlow:
          when:
            visible:
              text: "No text models downloaded"
          commands:
            - evalScript: "${console.log('SELECTION - Need to download model')}"
            - tapOn:
                point: "50%,10%"
            # Run setup to get a model
            - runFlow: 00-setup-model.yaml

      # Close picker
      - evalScript: "${console.log('SELECTION - Close picker')}"
      - tapOn:
          point: "50%,10%"

# ==============================
# Unload any currently loaded model
# ==============================
- evalScript: "${console.log('SELECTION - Ensure no model loaded')}"
- tapOn:
    text: "Home"
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 10000

- runFlow:
    when:
      visible:
        id: "new-chat-button"
    commands:
      # Model is loaded, unload it
      - evalScript: "${console.log('SELECTION - Unload current model')}"
      - tapOn:
          text: "Text"
      - extendedWaitUntil:
          visible:
            text: "Text Models"
          timeout: 5000
      - tapOn:
          text: "Unload current model"
      - extendedWaitUntil:
          visible:
            id: "setup-card"
          timeout: 10000

- takeScreenshot: 02-ready-to-select

# ==============================
# Test: Select a model
# ==============================
- evalScript: "${console.log('SELECTION - Open picker')}"
- tapOn:
    text: "Text"

- extendedWaitUntil:
    visible:
      text: "Text Models"
    timeout: 5000
- takeScreenshot: 03-picker

# Select first model in list
- evalScript: "${console.log('SELECTION - Select model')}"
- tapOn:
    index: 0
    id: "model-item"
- takeScreenshot: 04-selected

# Handle low memory warning
- runFlow:
    when:
      visible:
        text: "Load Anyway"
    commands:
      - evalScript: "${console.log('SELECTION - Load anyway')}"
      - tapOn:
          text: "Load Anyway"

# ==============================
# Verify model loaded
# ==============================
- evalScript: "${console.log('SELECTION - Waiting for load')}"
- extendedWaitUntil:
    visible:
      id: "new-chat-button"
    timeout: 120000

- assertVisible:
    id: "new-chat-button"
- evalScript: "${console.log('SELECTION - PASSED')}"
- takeScreenshot: 99-complete


================================================
FILE: .maestro/flows/p2/05c-model-unload.yaml
================================================
# P0 E2E: Model Unload
# Tests unloading a currently loaded model
#
# Precondition: Model must be loaded
# Test: Open picker → Unload → Verify unloaded
# Postcondition: No model loaded, setup card visible

appId: ${APP_ID}
name: "P0: Model Unload"
tags:
  - p0
  - model-unload
---

# ==============================
# Launch app
# ==============================
- evalScript: "${console.log('UNLOAD - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000
- evalScript: "${console.log('UNLOAD - App ready')}"

# ==============================
# Skip onboarding if needed
# ==============================
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('UNLOAD - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('UNLOAD - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# ==============================
# Wait for home screen
# ==============================
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('UNLOAD - On home')}"
- takeScreenshot: 01-home

# ==============================
# Ensure a model is loaded (precondition)
# ==============================
- runFlow:
    when:
      visible:
        id: "setup-card"
    commands:
      # No model loaded, run setup to load one
      - evalScript: "${console.log('UNLOAD - Load model first')}"
      - runFlow: 00-setup-model.yaml

# Verify model is loaded
- assertVisible:
    id: "new-chat-button"
- takeScreenshot: 02-model-loaded

# ==============================
# Test: Unload the model
# ==============================
- evalScript: "${console.log('UNLOAD - Open picker')}"
- tapOn:
    text: "Text"

- extendedWaitUntil:
    visible:
      text: "Text Models"
    timeout: 5000
- takeScreenshot: 03-picker

# Tap unload button
- evalScript: "${console.log('UNLOAD - Tap unload')}"
- tapOn:
    text: "Unload current model"
- takeScreenshot: 04-unloading

# ==============================
# Verify model unloaded
# ==============================
- evalScript: "${console.log('UNLOAD - Verify unloaded')}"
- extendedWaitUntil:
    visible:
      id: "setup-card"
    timeout: 10000

- assertVisible:
    id: "setup-card"
- assertNotVisible:
    id: "new-chat-button"

- evalScript: "${console.log('UNLOAD - PASSED')}"
- takeScreenshot: 99-complete


================================================
FILE: .maestro/flows/p3/07a-image-model-uninstall.yaml
================================================
# P0 E2E: Image Model Uninstall
# Tests deleting a downloaded image model via Download Manager
# Assumes an image model is already downloaded

appId: ${APP_ID}
name: "P0: Image Model Uninstall"
tags:
  - p0
  - models
  - image
---

# ==============================
# Launch and setup
# ==============================
- evalScript: "${console.log('IMG_UNINSTALL - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# Handle onboarding
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('IMG_UNINSTALL - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

# Handle download prompt
- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('IMG_UNINSTALL - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# Wait for home screen
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('IMG_UNINSTALL - On home')}"

# ==============================
# Delete image model
# ==============================
- evalScript: "${console.log('IMG_UNINSTALL - Go to Models screen')}"
- tapOn:
    text: "Models"

- extendedWaitUntil:
    visible:
      id: "models-screen"
    timeout: 10000

# Switch to Image Models tab
- evalScript: "${console.log('IMG_UNINSTALL - Image Models tab')}"
- tapOn:
    text: "Image Models"

# Open Download Manager
- evalScript: "${console.log('IMG_UNINSTALL - Open Download Manager')}"
- tapOn:
    id: "downloads-icon"

- extendedWaitUntil:
    visible:
      id: "downloaded-models-screen"
    timeout: 10000

# Find the first image model and delete it
- evalScript: "${console.log('IMG_UNINSTALL - Delete first image model')}"
- tapOn:
    id: "delete-model-button"
    index: 0

# Confirm deletion
- extendedWaitUntil:
    visible:
      text: "Delete Image Model"
    timeout: 5000
- tapOn:
    text: "DELETE"

# Wait for deletion confirmation to disappear
- evalScript: "${console.log('IMG_UNINSTALL - Waiting for deletion...')}"
- extendedWaitUntil:
    notVisible:
      text: "Delete Image Model"
    timeout: 10000

- evalScript: "${console.log('IMG_UNINSTALL - PASSED')}"


================================================
FILE: .maestro/flows/p3/07b-image-model-download.yaml
================================================
# P0 E2E: Image Model Download
# Tests downloading an image model from the Models screen
# Assumes no image model is currently downloaded

appId: ${APP_ID}
name: "P0: Image Model Download"
tags:
  - p0
  - models
  - image
---

# ==============================
# Launch and setup
# ==============================
- evalScript: "${console.log('IMG_DOWNLOAD - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# Handle onboarding
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('IMG_DOWNLOAD - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

# Handle download prompt
- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('IMG_DOWNLOAD - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# Wait for home screen
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('IMG_DOWNLOAD - On home')}"

# ==============================
# Download image model
# ==============================
- evalScript: "${console.log('IMG_DOWNLOAD - Go to Models screen')}"
- tapOn:
    text: "Models"

- extendedWaitUntil:
    visible:
      id: "models-screen"
    timeout: 10000

# Switch to Image Models tab
- evalScript: "${console.log('IMG_DOWNLOAD - Image Models tab')}"
- tapOn:
    text: "Image Models"
- takeScreenshot: 01-image-models-tab

# Wait for models to load
- extendedWaitUntil:
    visible:
      text: "Absolute Reality (CPU)"
    timeout: 5000
# Tap download button (1st card = index 0)
- evalScript: "${console.log('IMG_DOWNLOAD - Tap Download')}"
- tapOn:
    text: "Download"
    index: 0

# Wait for download to complete
- evalScript: "${console.log('IMG_DOWNLOAD - Waiting for download...')}"
- extendedWaitUntil:
    visible:
      text: "Success"
    timeout: 180000

- evalScript: "${console.log('IMG_DOWNLOAD - Download complete!')}"
- takeScreenshot: 02-download-complete
- tapOn:
    text: "OK"

- evalScript: "${console.log('IMG_DOWNLOAD - PASSED')}"


================================================
FILE: .maestro/flows/p3/07c-image-model-set-active.yaml
================================================
# P0 E2E: Image Model Set Active
# Tests selecting a downloaded image model from the home screen picker
# Assumes an image model is already downloaded but not active

appId: ${APP_ID}
name: "P0: Image Model Set Active"
tags:
  - p0
  - models
  - image
---

# ==============================
# Launch and setup
# ==============================
- evalScript: "${console.log('IMG_SET_ACTIVE - Launch')}"
- launchApp

- extendedWaitUntil:
    notVisible:
      id: "app-loading"
    timeout: 30000

# Handle onboarding
- runFlow:
    when:
      visible:
        text: "Welcome to Off Grid"
    commands:
      - evalScript: "${console.log('IMG_SET_ACTIVE - Skip onboarding')}"
      - tapOn:
          text: "Skip"
      - extendedWaitUntil:
          visible:
            text: "Skip for Now"
          timeout: 30000
      - tapOn:
          text: "Skip for Now"

# Handle download prompt
- runFlow:
    when:
      visible:
        text: "Download Your First Model"
    commands:
      - evalScript: "${console.log('IMG_SET_ACTIVE - Skip prompt')}"
      - tapOn:
          text: "Skip for Now"

# Wait for home screen
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 15000
- evalScript: "${console.log('IMG_SET_ACTIVE - On home')}"

# ==============================
# Ensure model is unloaded first
# ==============================
# If a model is already loaded (not showing "Tap to select"), unload it
- runFlow:
    when:
      notVisible:
        text: "Tap to select"
    commands:
      - evalScript: "${console.log('IMG_SET_ACTIVE - Model already loaded, unloading...')}"
      - tapOn:
          id: "image-model-card"

      # Wait for picker
      - extendedWaitUntil:
          visible:
            text: "Image Models"
          timeout: 5000

      # Tap "Unload current model"
      - tapOn:
          text: "Unload current model"

      # Wait for model to unload
      - extendedWaitUntil:
          visible:
            text: "Tap to select"
          timeout: 30000

# ==============================
# Select image model
# ==============================
- evalScript: "${console.log('IMG_SET_ACTIVE - Verify Tap to select shown')}"
- extendedWaitUntil:
    visible:
      text: "Tap to select"
    timeout: 5000

# Tap on Image card to open picker
- evalScript: "${console.log('IMG_SET_ACTIVE - Open picker')}"
- tapOn:
    id: "image-model-card"

# Wait for picker to appear
- extendedWaitUntil:
    visible:
      text: "Image Models"
    timeout: 5000

# Select first model
- evalScript: "${console.log('IMG_SET_ACTIVE - Select model')}"
- tapOn:
    id: "model-item"
    index: 0

# Wait for model to load
- evalScript: "${console.log('IMG_SET_ACTIVE - Waiting for model to load...')}"
- extendedWaitUntil:
    notVisible:
      text: "Tap to select"
    timeout: 30000

# Verify model name is shown (don't check exact name as it could be any model)
- evalScript: "${console.log('IMG_SET_ACTIVE - Model loaded successfully')}"

- evalScript: "${console.log('IMG_SET_ACTIVE - PASSED')}"


================================================
FILE: .maestro/utils/wait-for-app-ready.yaml
================================================
# Utility: Wait for app to be ready
# Waits for the main UI to be visible

appId: ${APP_ID}
---

# Wait for home screen or chat screen to be visible
- extendedWaitUntil:
    visible:
      id: "home-screen"
    timeout: 10000


================================================
FILE: .prettierrc.js
================================================
module.exports = {
  arrowParens: 'avoid',
  singleQuote: true,
  trailingComma: 'all',
};


================================================
FILE: .swiftlint.yml
================================================
included:
  - ios

excluded:
  - ios/Pods
  - ios/build
  - ios/OffgridMobile.xcodeproj
  - ios/OffgridMobileTests

disabled_rules:
  - trailing_whitespace   # handled by editor
  - line_length           # RN bridge code has long lines

opt_in_rules:
  - force_unwrapping

force_unwrapping:
  severity: warning

function_body_length:
  warning: 100
  error: 200

type_body_length:
  warning: 400
  error: 1000


================================================
FILE: .vscode/settings.json
================================================
{
    "sonarlint.connectedMode.project": {
        "connectionId": "alichherawalla",
        "projectKey": "alichherawalla_off-grid-mobile"
    }
}

================================================
FILE: .watchmanconfig
================================================
{}


================================================
FILE: AGENTS.md
================================================
# Project Instructions

## Pre-Commit Quality Gates

All quality gates run automatically via Husky on every `git commit`, scoped to the file types you staged:

| Staged file type | Checks that run automatically |
|---|---|
| `.ts` / `.tsx` / `.js` / `.jsx` | eslint (staged only), `tsc --noEmit`, `npm test` |
| `.swift` | swiftlint (staged only), `npm run test:ios` |
| `.kt` / `.kts` | `compileDebugKotlin` (type check), `lintDebug`, `npm run test:android` |

**Requirements:**
- SwiftLint: `brew install swiftlint` (skipped with a warning if not installed)
- Android checks require the Gradle wrapper in `android/`

Before writing new code, ensure tests exist for your changes. If the hook fails, fix the issue and recommit — never skip with `--no-verify`.

## Testing Requirements

Always write **both** unit tests and integration tests for new features and significant changes:

- **Unit tests** (`__tests__/unit/`): Test individual functions, hooks, and store actions in isolation with mocked dependencies.
- **Integration tests** (`__tests__/integration/`): Test how multiple modules work together end-to-end (e.g., service A calls service B which writes to database C). Use mocked native modules but real logic across layers.

Do not consider a feature complete with only unit tests. Integration tests catch wiring bugs, incorrect data flow between layers, and lifecycle issues that unit tests miss.

## Push = Create PR + Address Review

When asked to push code, follow this full workflow:

0. ensure that you are on a branch that is specific to this change i.e feat/new-feature or fix/bug-fix or docs/update-readme or chore/update-dependencies, or test/new-test, etc
1. Push the branch to the remote (`git push -u origin <branch>`)
2. Create a PR using `gh pr create`. Ensure that you are adhering to the PR template. **Do NOT include "Generated with Codex" or any AI attribution in PR descriptions.**
3. Wait for Gemini to review the PR (poll with `gh pr checks` and `gh api repos/{owner}/{repo}/pulls/{number}/reviews` until a review appears)
4. Once a review exists, pull down the review comments: `gh api repos/{owner}/{repo}/pulls/{number}/comments` and `gh api repos/{owner}/{repo}/pulls/{number}/reviews`
5. Address every review comment — fix the code, re-run the quality gates (tests, lint, tsc).
6. Reply to **each** review comment individually on the PR using `gh api` (use `/pulls/comments/{id}/replies` endpoint). Every comment must get its own reply confirming what was done — do not post a single summary comment.
7. Push the fixes
8. Report what was changed in response to the review

## CI Review Loop

The repo has three automated reviewers on every PR. After pushing, loop until all are green:

| Reviewer | What it checks | How to address |
|---|---|---|
| **Gemini Bot** | Code quality, style, logic issues | Read comments via `gh api`, fix code or reply explaining why it's fine, then comment `/gemini review` to trigger a fresh pass |
| **Codecov** | Test coverage thresholds | Add missing tests, ensure new code is covered. Check the Codecov report for uncovered lines |
| **SonarCloud** | Security hotspots, code smells, duplications, bugs | Fix flagged issues — especially security hotspots and duplications. Resolve quality gate failures before merging |

**Workflow:**
1. Push code → wait for all three reviewers to report
2. Pull down Gemini comments, Codecov report, and SonarCloud findings
3. Fix issues: code changes for Gemini/SonarCloud, add tests for Codecov
4. Re-run local quality gates (`npm run lint && npm test && npx tsc --noEmit`)
5. Push fixes, comment `/gemini review` on the PR to re-trigger Gemini
6. Repeat until all three reviewers pass with no blocking issues


================================================
FILE: App.tsx
================================================
/**
 * Off Grid - On-Device AI Chat Application
 * Private AI assistant that runs entirely on your device
 */

import 'react-native-gesture-handler';
import React, { useEffect, useState, useCallback } from 'react';
import { StatusBar, ActivityIndicator, View, StyleSheet, LogBox } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
import { AppNavigator } from './src/navigation';
import { useTheme } from './src/theme';
import { hardwareService, modelManager, authService, ragService, remoteServerManager } from './src/services';
import logger from './src/utils/logger';
import { useAppStore, useAuthStore, useRemoteServerStore } from './src/stores';
import { LockScreen } from './src/screens';
import { useAppState } from './src/hooks/useAppState';

LogBox.ignoreAllLogs(); // Suppress all logs

const ensureRemoteServerStoreHydrated = async () => {
  const persistApi = useRemoteServerStore.persist;
  if (!persistApi?.hasHydrated || !persistApi.rehydrate) return;
  if (!persistApi.hasHydrated()) {
    await persistApi.rehydrate();
  }
};

function App() {
  const [isInitializing, setIsInitializing] = useState(true);
  const setDeviceInfo = useAppStore((s) => s.setDeviceInfo);
  const setModelRecommendation = useAppStore((s) => s.setModelRecommendation);
  const setDownloadedModels = useAppStore((s) => s.setDownloadedModels);
  const setDownloadedImageModels = useAppStore((s) => s.setDownloadedImageModels);
  const clearImageModelDownloading = useAppStore((s) => s.clearImageModelDownloading);

  const { colors, isDark } = useTheme();

  const {
    isEnabled: authEnabled,
    isLocked,
    setLocked,
    setLastBackgroundTime,
  } = useAuthStore();

  // Handle app state changes for auto-lock
  useAppState({
    onBackground: useCallback(() => {
      if (authEnabled) {
        setLastBackgroundTime(Date.now());
        setLocked(true);
      }
    }, [authEnabled, setLastBackgroundTime, setLocked]),
    onForeground: useCallback(() => {
      // Lock is already set when going to background
      // Nothing additional needed here
    }, []),
  });

  useEffect(() => {
    initializeApp();

  }, []);

  const ensureAppStoreHydrated = async () => {
    const persistApi = useAppStore.persist;
    if (!persistApi?.hasHydrated || !persistApi.rehydrate) return;
    if (!persistApi.hasHydrated()) {
      await persistApi.rehydrate();
    }
  };

  const initializeApp = async () => {
    try {
      // Ensure persisted download metadata is loaded before restore logic reads it.
      await ensureAppStoreHydrated();

      // Phase 1: Quick initialization - get app ready to show UI
      // Initialize hardware detection
      const deviceInfo = await hardwareService.getDeviceInfo();
      setDeviceInfo(deviceInfo);

      const recommendation = hardwareService.getModelRecommendation();
      setModelRecommendation(recommendation);

      // Initialize model manager and load downloaded models list
      await modelManager.initialize();

      // Clean up any mmproj files that were incorrectly added as standalone models
      await modelManager.cleanupMMProjEntries();

      // Wire up background download metadata persistence
      const {
        setBackgroundDownload,
        activeBackgroundDownloads,
        addDownloadedModel,
        setDownloadProgress,
      } = useAppStore.getState();
      modelManager.setBackgroundDownloadMetadataCallback((downloadId, info) => {
        setBackgroundDownload(downloadId, info);
      });

      // Recover any background downloads that completed while app was dead
      try {
        const recoveredModels = await modelManager.syncBackgroundDownloads(
          activeBackgroundDownloads,
          (downloadId) => setBackgroundDownload(downloadId, null)
        );
        for (const model of recoveredModels) {
          addDownloadedModel(model);
          logger.log('[App] Recovered background download:', model.name);
        }
      } catch (err) {
        logger.error('[App] Failed to sync background downloads:', err);
      }

      // Recover completed image downloads (zip unzip / multifile finalization)
      try {
        const recoveredImageModels = await modelManager.syncCompletedImageDownloads(
          activeBackgroundDownloads,
          (downloadId) => setBackgroundDownload(downloadId, null),
        );
        for (const model of recoveredImageModels) {
          logger.log('[App] Recovered image download:', model.name);
        }
      } catch (err) {
        logger.error('[App] Failed to sync completed image downloads:', err);
      }

      // Re-wire event listeners for downloads that were still running when the
      // app was killed (running/pending status in Android DownloadManager).
      try {
        const restoredDownloadIds = await modelManager.restoreInProgressDownloads(
          activeBackgroundDownloads,
          (progress) => {
            const key = `${progress.modelId}/${progress.fileName}`;
            setDownloadProgress(key, {
              progress: progress.progress,
              bytesDownloaded: progress.bytesDownloaded,
              totalBytes: progress.totalBytes,
            });
          },
        );
        for (const downloadId of restoredDownloadIds) {
          const metadata = activeBackgroundDownloads[downloadId];
          const progressKey = metadata ? `${metadata.modelId}/${metadata.fileName}` : null;
          modelManager.watchDownload(
            downloadId,
            (model) => {
              if (progressKey) setDownloadProgress(progressKey, null);
              addDownloadedModel(model);
              logger.log('[App] Restored in-progress download completed:', model.name);
            },
            (error) => {
              if (progressKey) setDownloadProgress(progressKey, null);
              logger.error('[App] Restored in-progress download failed:', error);
            },
          );
        }
      } catch (err) {
        logger.error('[App] Failed to restore in-progress downloads:', err);
      }

      // Clear any stale imageModelDownloading entries — if the app was killed
      // mid-download these would be persisted as "downloading" forever.
      clearImageModelDownloading();

      // Scan for any models that may have been downloaded externally or
      // when app was killed before JS callback fired
      const { textModels, imageModels } = await modelManager.refreshModelLists();
      setDownloadedModels(textModels);
      setDownloadedImageModels(imageModels);

      // Ensure remote server store is hydrated before initializing providers,
      // so getServers() / activeServerId reads see persisted data.
      await ensureRemoteServerStoreHydrated();

      // Initialize remote server providers in the background — don't block
      // the home screen while fetching models from potentially unreachable servers.
      remoteServerManager.initializeProviders().catch((err) => {
        logger.error('[App] Failed to initialize remote server providers:', err);
      });

      // Check if passphrase is set and lock app if needed
      const hasPassphrase = await authService.hasPassphrase();
      if (hasPassphrase && authEnabled) {
        setLocked(true);
      }

      // Initialize RAG database tables
      ragService.ensureReady().catch((err) => logger.error('Failed to initialize RAG service on startup', err));

      // Show the UI immediately
      setIsInitializing(false);

      // Models are loaded on-demand when the user opens a chat,
      // not eagerly on startup, to avoid freezing the UI.
    } catch (error) {
      logger.error('[App] Error initializing app:', error);
      setIsInitializing(false);
    }
  };

  const handleUnlock = useCallback(() => {
    setLocked(false);
  }, [setLocked]);

  if (isInitializing) {
    return (
      <GestureHandlerRootView style={styles.flex}>
        <SafeAreaProvider>
          <View style={[styles.loadingContainer, { backgroundColor: colors.background }]} testID="app-loading">
            <StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} backgroundColor={colors.background} />
            <ActivityIndicator size="large" color={colors.primary} />
          </View>
        </SafeAreaProvider>
      </GestureHandlerRootView>
    );
  }

  // Show lock screen if auth is enabled and app is locked
  if (authEnabled && isLocked) {
    return (
      <GestureHandlerRootView style={styles.flex} testID="app-locked">
        <SafeAreaProvider>
          <StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} backgroundColor={colors.background} />
          <LockScreen onUnlock={handleUnlock} />
        </SafeAreaProvider>
      </GestureHandlerRootView>
    );
  }

  return (
    <GestureHandlerRootView style={styles.flex}>
      <SafeAreaProvider>
        <StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} backgroundColor={colors.background} />
        <NavigationContainer
          theme={{
            dark: isDark,
            colors: {
              primary: colors.primary,
              background: colors.background,
              card: colors.surface,
              text: colors.text,
              border: colors.border,
              notification: colors.primary,
            },
            fonts: {
              regular: {
                fontFamily: 'System',
                fontWeight: '400',
              },
              medium: {
                fontFamily: 'System',
                fontWeight: '500',
              },
              bold: {
                fontFamily: 'System',
                fontWeight: '700',
              },
              heavy: {
                fontFamily: 'System',
                fontWeight: '900',
              },
            },
          }}
        >
          <AppNavigator />
        </NavigationContainer>
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  flex: {
    flex: 1,
  },
  loadingContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default App;


================================================
FILE: CLAUDE.md
================================================
# Project Instructions

## Branch Policy

**Never push directly to `main`.** All changes must go through a pull request:

0. Always create a branch specific to the change before committing: `feat/`, `fix/`, `docs/`, `chore/`, `test/`, etc.
1. Push the branch and open a PR — never `git push origin main`.
2. If you find yourself on `main`, create a branch first: `git checkout -b <branch-name>`.

## Copy & Content Standards

**Any change to website copy, essays, docs text, UI strings, or marketing content must follow the brand voice guide:**

- Read `docs/brand_tone_voice.md` before writing or editing any copy.
- The full quality checklist is at the bottom of that file — run every item before committing content changes.

Key rules that are easy to miss:

| Rule | Wrong | Right |
|---|---|---|
| Proof-first | "fast" | "15-30 tok/s on flagship devices" |
| Privacy as mechanism | "we value your privacy" | "the model runs in your phone's RAM, nothing is sent anywhere" |
| No exclamation marks | "It works!" | "It works." |
| No em dashes | "private — always" | "private - always" |
| No forbidden words | revolutionary, seamlessly, empower, leverage, robust, comprehensive, crucial, pivotal, delve, tapestry, testament, underscore, foster, cultivate, showcase, enhance | use specific, plain words instead |
| No AI slop phrases | "serves as", "stands as", "represents a", "marks a turning point", "it is worth noting" | just say "is" |
| No structural clichés | "Not just X, but Y" / "It's not X, it's Y" | state the thing directly |
| No curly quotes | "private" | "private" |

The emotional arc for all content: **Recognition -> Return -> Freedom**. Name what's been happening, show what's being given back, hand over the capability without condition.

---

## Design Standards

**Any change that touches UI (screens, components, styles) must comply with the design system:**

- Read `docs/design/VISUAL_HIERARCHY_STANDARD.md` before writing or modifying any UI code.
- Check `docs/design/` for any other relevant design documents.
- Use `TYPOGRAPHY` tokens — never hardcode font sizes or weights.
- Use `COLORS` tokens — never hardcode color values.
- Use `SPACING` tokens — never hardcode margin/padding values.
- Weights must stay ≤ 400 (no bold).
- Never use emojis or emoticons in UI text — always use `react-native-vector-icons` instead. Feather is the default; MaterialIcons is allowed only when Feather lacks a suitable icon (e.g. `whatshot` for trending).
- Never use `lucide-react` or any other icon library — only `react-native-vector-icons`.
- Follow the 5-category text hierarchy: TITLE → BODY → SUBTITLE/DESCRIPTION → META.

## Pre-Commit Quality Gates

All quality gates run automatically via Husky on every `git commit`, scoped to the file types you staged:

| Staged file type | Checks that run automatically |
|---|---|
| `.ts` / `.tsx` / `.js` / `.jsx` | eslint (staged only), `tsc --noEmit`, `npm test` |
| `.swift` | swiftlint (staged only), `npm run test:ios` |
| `.kt` / `.kts` | `compileDebugKotlin` (type check), `lintDebug`, `npm run test:android` |

**Requirements:**
- SwiftLint: `brew install swiftlint` (skipped with a warning if not installed)
- Android checks require the Gradle wrapper in `android/`

Before writing new code, ensure tests exist for your changes. If the hook fails, fix the issue and recommit — never skip with `--no-verify`.

## Testing Requirements

Always write **both** unit tests and integration tests for new features and significant changes:

- **Unit tests** (`__tests__/unit/`): Test individual functions, hooks, and store actions in isolation with mocked dependencies.
- **Integration tests** (`__tests__/integration/`): Test how multiple modules work together end-to-end (e.g., service A calls service B which writes to database C). Use mocked native modules but real logic across layers.

Do not consider a feature complete with only unit tests. Integration tests catch wiring bugs, incorrect data flow between layers, and lifecycle issues that unit tests miss.

## Push = Create PR + Address Review

When the user says "push" (or any equivalent like "ship it", "send it", "push this"), follow this full workflow:

### Before pushing
0. Write tests for any new or changed logic if they don't already exist.
1. Run `npm run lint && npx tsc --noEmit && npm test` — fix any failures before continuing.
2. Commit all staged changes with a descriptive message.
3. Ensure you are NOT on `main`. If you are, create an appropriately named branch first: `git checkout -b feat/...` or `fix/...` or `chore/...` etc.

### Pushing & PR
4. Push the branch: `git push -u origin <branch>`
5. If no PR exists for this branch, create one with `gh pr create`. **Do NOT include "Generated with Codex" or any AI attribution in PR descriptions.**
6. If a PR already exists, update its description to reflect **all commits in the PR** (not just the latest push). Read the full commit history with `git log main..HEAD` and write a coherent description that summarises the entire change set — what it does, why, and how.

### Review loop
7. Wait for Gemini to review the PR (poll with `gh pr checks` and `gh api repos/{owner}/{repo}/pulls/{number}/reviews` until a review appears).
8. Pull down review comments: `gh api repos/{owner}/{repo}/pulls/{number}/comments` and `gh api repos/{owner}/{repo}/pulls/{number}/reviews`.
9. Address every review comment — fix the code, re-run quality gates (lint, tsc, test).
10. Reply to **each** review comment individually using `gh api` (`/pulls/comments/{id}/replies`). Every comment gets its own reply — do not post a single summary comment.
11. Push fixes, update the PR description again to stay coherent across all commits.
12. Report what was changed in response to the review.

## CI Review Loop

The repo has three automated reviewers on every PR. After pushing, loop until all are green:

| Reviewer | What it checks | How to address |
|---|---|---|
| **Gemini Bot** | Code quality, style, logic issues | Read comments via `gh api`, fix code or reply explaining why it's fine, then comment `/gemini review` to trigger a fresh pass |
| **Codecov** | Test coverage thresholds | Add missing tests, ensure new code is covered. Check the Codecov report for uncovered lines |
| **SonarCloud** | Security hotspots, code smells, duplications, bugs | Fix flagged issues — especially security hotspots and duplications. Resolve quality gate failures before merging |

**Workflow:**
1. Push code → wait for all three reviewers to report
2. Pull down Gemini comments, Codecov report, and SonarCloud findings
3. Fix issues: code changes for Gemini/SonarCloud, add tests for Codecov
4. Re-run local quality gates (`npm run lint && npm test && npx tsc --noEmit`)
5. Push fixes, comment `/gemini review` on the PR to re-trigger Gemini
6. Repeat until all three reviewers pass with no blocking issues


================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'

# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"

# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'

# Ruby 3.4.0 has removed some libraries from the standard library.
gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2026 Mohammed Ali Chherawalla

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<div align="center">

<img src="src/assets/logo.png" alt="Off Grid Logo" width="120" />

# Off Grid

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

**Chat. Generate images. Use tools. See. Listen. All on your phone or Mac. All offline. Zero data leaves your device.**

[![GitHub stars](https://img.shields.io/github/stars/alichherawalla/off-grid-mobile?style=social)](https://github.com/alichherawalla/off-grid-mobile)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Google Play](https://img.shields.io/badge/Google%20Play-Download-brightgreen?logo=google-play)](https://play.google.com/store/apps/details?id=ai.offgridmobile)
[![App Store](https://img.shields.io/badge/App%20Store-Download-blue?logo=apple)](https://apps.apple.com/us/app/off-grid-local-ai/id6759299882)
[![Platform](https://img.shields.io/badge/Platform-Android%20%7C%20iOS%20%7C%20macOS-green.svg)](#install)
[![codecov](https://codecov.io/gh/alichherawalla/off-grid-mobile/graph/badge.svg)](https://codecov.io/gh/alichherawalla/off-grid-mobile)
[![Slack](https://img.shields.io/badge/Slack-Join%20Community-4A154B?logo=slack)](https://join.slack.com/t/off-grid-mobile/shared_invite/zt-3q7kj5gr6-rVzx5gl5LKPQh4mUE2CCvA)

</div>

---


## Not just another chat app

Most "local LLM" apps give you a text chatbot and call it a day. Off Grid is a **complete offline AI suite** — text generation, image generation, vision AI, voice transcription, tool calling, and document analysis, all running natively on your phone's or Mac's hardware.

---

## What can it do?

<div align="center">
<table>
  <tr>
    <td align="center"><img src="demo-gifs/onboarding.gif" width="200" /><br /><b>Onboarding</b></td>
    <td align="center"><img src="demo-gifs/text-gen.gif" width="200" /><br /><b>Text Generation</b></td>
    <td align="center"><img src="demo-gifs/image-gen.gif" width="200" /><br /><b>Image Generation</b></td>
  </tr>
  <tr>
    <td align="center"><img src="demo-gifs/vision.gif" width="200" /><br /><b>Vision AI</b></td>
    <td align="center"><img src="demo-gifs/attachments.gif" width="200" /><br /><b>Attachments</b></td>
    <td align="center"><img src="demo-gifs/tool-calling.gif" width="200" /><br /><b>Tool Calling</b></td>
</tr>
</table>
</div>

**Text Generation** — Run Qwen 3, Llama 3.2, Gemma 3, Phi-4, and any GGUF model. Streaming responses, thinking mode, markdown rendering, 15-30 tok/s on flagship devices. Bring your own `.gguf` files too.

**Remote LLM Servers** — Connect to any OpenAI-compatible server on your local network (Ollama, LM Studio, LocalAI). Discover models automatically, stream responses via SSE, store API keys securely in the system keychain. Switch seamlessly between local and remote models.

**Tool Calling** — Models that support function calling can use built-in tools: web search, calculator, date/time, device info, and knowledge base search. Automatic tool loop with runaway prevention. Clickable links in search results.

**Project Knowledge Base** — Upload PDFs and text documents to a project's knowledge base. Documents are chunked, embedded on-device with a bundled MiniLM model, and retrieved via cosine similarity — all stored locally in SQLite. The `search_knowledge_base` tool is automatically available in project conversations.

**Image Generation** — On-device Stable Diffusion with real-time preview. NPU-accelerated on Snapdragon (5-10s per image), Core ML on iOS. 20+ models including Absolute Reality, DreamShaper, Anything V5.

**Vision AI** — Point your camera at anything and ask questions. SmolVLM, Qwen3-VL, Gemma 3n — analyze documents, describe scenes, read receipts. ~7s on flagship devices.

**Voice Input** — On-device Whisper speech-to-text. Hold to record, auto-transcribe. No audio ever leaves your phone.

**Document Analysis** — Attach PDFs, code files, CSVs, and more to your conversations. Native PDF text extraction on both platforms.

**AI Prompt Enhancement** — Simple prompt in, detailed Stable Diffusion prompt out. Your text model automatically enhances image generation prompts.

---

<br />

<div align="center">

<sub>**FOUNDING SUPPORTER PRE-ORDERS · NOW OPEN**</sub>

# Off Grid Pro

**First 100 supporters lock in lifetime access for $10.**

</div>

<br />

The free OSS keeps shipping, MIT, forever — that's not changing. Pro is an optional, additive tier we're opening pre-orders for.

This is our little hope of keeping ambient AI on-device alive — and sustaining the open-source release that this project has been built on for the last two years. Not a subscription. Not VC. A small, finite group of people willing to fund the next 12 weeks of full-time work.

**$10 × 100 = $1,000. After that, lifetime Pro moves to $50.**

### What Pro adds

- **Custom personas** — system prompts, voice, persistent memory per assistant
- **End-to-end voice mode** — Whisper STT (already shipping) + Kokoro TTS, all on-device
- **Calendar + email + MCP servers** — Linear, Notion, GitHub, your own MCP. Drafts only; you approve every send.
- **Larger models** — full size range, including 7B on flagship phones, 13B on iPads / M-series Macs
- **Future Pro features** — included for the supported lifetime of the app

### The promise

Pro ships in **12 weeks** from your purchase, or full refund. No forms, no questions.

### Claim a Founding Supporter spot

Join the founders Slack and drop into **#pro-first-100**. We'll say hi and get you set up.

**[→ Join the Slack](https://join.slack.com/t/off-grid-mobile/shared_invite/zt-3q7kj5gr6-rVzx5gl5LKPQh4mUE2CCvA)**

## Performance

| Task | Flagship | Mid-range |
|------|----------|-----------|
| Text generation | 15-30 tok/s | 5-15 tok/s |
| Image gen (NPU) | 5-10s | — |
| Image gen (CPU) | ~15s | ~30s |
| Vision inference | ~7s | ~15s |
| Voice transcription | Real-time | Real-time |

Tested on Snapdragon 8 Gen 2/3, Apple A17 Pro. Results vary by model size and quantization.

---

<a name="install"></a>
## Install

<div align="center">
<table><tr>
<td align="center"><a href="https://apps.apple.com/us/app/off-grid-local-ai/id6759299882"><img src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg" alt="Download on the App Store" width="180" /></a></td>
<td align="center"><a href="https://play.google.com/store/apps/details?id=ai.offgridmobile"><img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" width="220" /></a></td>
</tr></table>
</div>

Or grab the latest APK from [**GitHub Releases**](https://github.com/alichherawalla/off-grid-mobile/releases/latest).

> **macOS**: The iOS App Store version runs natively on Apple Silicon Macs via Mac Catalyst / iPad compatibility.

### Build from source

```bash
git clone https://github.com/alichherawalla/off-grid-mobile.git
cd off-grid-mobile
npm install

# Android
cd android && ./gradlew clean && cd ..
npm run android

# iOS
cd ios && pod install && cd ..
npm run ios
```

> Requires Node.js 20+, JDK 17 / Android SDK 36 (Android), Xcode 15+ (iOS). See [full build guide](docs/ARCHITECTURE.md#building-from-source).

---

## Testing

[![CI](https://github.com/alichherawalla/off-grid-mobile/actions/workflows/ci.yml/badge.svg)](https://github.com/alichherawalla/off-grid-mobile/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/alichherawalla/off-grid-mobile/graph/badge.svg)](https://codecov.io/gh/alichherawalla/off-grid-mobile)

Tests run across three platforms on every PR:

| Platform | Framework | What's covered |
|----------|-----------|----------------|
| React Native | Jest + RNTL | Stores, services, components, screens, contracts |
| Android | JUnit | LocalDream, DownloadManager, BroadcastReceiver |
| iOS | XCTest | PDFExtractor, CoreMLDiffusion, DownloadManager |
| E2E | Maestro | Critical path flows (launch, chat, models, downloads) |

```bash
npm test              # Run all tests (Jest + Android + iOS)
npm run test:e2e      # Run Maestro E2E flows (requires running app)
```

This project is tested with BrowserStack.

---

## Documentation

| Document | Description |
|----------|-------------|
| [Architecture & Technical Reference](docs/ARCHITECTURE.md) | System architecture, design patterns, native modules, performance tuning |
| [Codebase Guide](docs/standards/CODEBASE_GUIDE.md) | Comprehensive code walkthrough |
| [Design System](docs/design/DESIGN_PHILOSOPHY_SYSTEM.md) | Brutalist design philosophy, theme system, tokens |
| [Visual Hierarchy Standard](docs/design/VISUAL_HIERARCHY_STANDARD.md) | Visual hierarchy and layout standards |

---

## Community

Join the conversation on [**Slack**](https://join.slack.com/t/off-grid-mobile/shared_invite/zt-3q7kj5gr6-rVzx5gl5LKPQh4mUE2CCvA) — ask questions, share feedback, and connect with other Off Grid users and contributors.

---

## Contributing

Contributions welcome! Fork, branch, PR. See [development guidelines](docs/ARCHITECTURE.md#contributing) for code style and the [codebase guide](docs/standards/CODEBASE_GUIDE.md) for patterns.

---

## Acknowledgments

Built on the shoulders of giants:
[llama.cpp](https://github.com/ggerganov/llama.cpp) | [whisper.cpp](https://github.com/ggerganov/whisper.cpp) | [llama.rn](https://github.com/mybigday/llama.rn) | [whisper.rn](https://github.com/mybigday/whisper.rn) | [local-dream](https://github.com/xororz/local-dream) | [ml-stable-diffusion](https://github.com/apple/ml-stable-diffusion) | [MNN](https://github.com/alibaba/MNN) | [Hugging Face](https://huggingface.co)

---


## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=alichherawalla/off-grid-mobile&type=date&legend=top-left)](https://www.star-history.com/#alichherawalla/off-grid-mobile&type=date&legend=top-left)

<div align="center">

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

*No cloud. No data harvesting. Just AI that works anywhere.*

[Join the Community on Slack](https://join.slack.com/t/off-grid-mobile/shared_invite/zt-3q7kj5gr6-rVzx5gl5LKPQh4mUE2CCvA)

</div>


================================================
FILE: TODO.md
================================================
# OffgridMobile - TODO

## Document Upload Support

- [ ] **Add Word/Office document support**
  - Research libraries for .docx, .xlsx parsing
  - May require server-side processing or heavy native dependencies

---

## Testing Improvements

- [ ] Add negative tests to intent classifier (patterns that should NOT match)
- [ ] Add integration tests for failure recovery scenarios



================================================
FILE: __tests__/App.test.tsx
================================================
/**
 * @format
 */

import React from 'react';
import ReactTestRenderer from 'react-test-renderer';
import App from '../App';

test('renders correctly', async () => {
  await ReactTestRenderer.act(() => {
    ReactTestRenderer.create(<App />);
  });
});


================================================
FILE: __tests__/contracts/coreMLDiffusion.contract.test.ts
================================================
/**
 * Contract Tests: CoreMLDiffusion Native Module (iOS Image Generation)
 *
 * These tests verify that the CoreMLDiffusion native module interface
 * maintains parity with the Android LocalDreamModule so the shared
 * TypeScript bridge (localDreamGenerator.ts) works on both platforms.
 */

// The CoreMLDiffusionModule must expose the same methods as LocalDreamModule
export interface CoreMLDiffusionModuleInterface {
  loadModel(params: {
    modelPath: string;
    threads?: number;
    backend?: string;
  }): Promise<boolean>;

  unloadModel(): Promise<boolean>;
  isModelLoaded(): Promise<boolean>;
  getLoadedModelPath(): Promise<string | null>;

  generateImage(params: {
    prompt: string;
    negativePrompt?: string;
    steps?: number;
    guidanceScale?: number;
    seed?: number;
    width?: number;
    height?: number;
    previewInterval?: number;
  }): Promise<{
    id: string;
    imagePath: string;
    width: number;
    height: number;
    seed: number;
  }>;

  cancelGeneration(): Promise<boolean>;
  isGenerating(): Promise<boolean>;
  isNpuSupported(): Promise<boolean>;

  getGeneratedImages(): Promise<Array<{
    id: string;
    prompt: string;
    imagePath: string;
    width: number;
    height: number;
    steps: number;
    seed: number;
    modelId: string;
    createdAt: string;
  }>>;

  deleteGeneratedImage(imageId: string): Promise<boolean>;
}

// Mock NativeModules
const mockCoreMLModule: CoreMLDiffusionModuleInterface = {
  loadModel: jest.fn(),
  unloadModel: jest.fn(),
  isModelLoaded: jest.fn(),
  getLoadedModelPath: jest.fn(),
  generateImage: jest.fn(),
  cancelGeneration: jest.fn(),
  isGenerating: jest.fn(),
  isNpuSupported: jest.fn(),
  getGeneratedImages: jest.fn(),
  deleteGeneratedImage: jest.fn(),
};

jest.mock('react-native', () => ({
  NativeModules: {
    CoreMLDiffusionModule: mockCoreMLModule,
  },
  NativeEventEmitter: jest.fn().mockImplementation(() => ({
    addListener: jest.fn().mockReturnValue({ remove: jest.fn() }),
    removeAllListeners: jest.fn(),
  })),
  Platform: { OS: 'ios' },
}));

describe('CoreMLDiffusion Contract (iOS parity with LocalDreamModule)', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('loadModel', () => {
    it('should accept modelPath parameter', async () => {
      (mockCoreMLModule.loadModel as jest.Mock).mockResolvedValue(true);

      const params = {
        modelPath: '/var/mobile/Containers/Data/Application/.../models/sd21',
      };

      const result = await mockCoreMLModule.loadModel(params);

      expect(mockCoreMLModule.loadModel).toHaveBeenCalledWith(
        expect.objectContaining({
          modelPath: expect.any(String),
        })
      );
      expect(typeof result).toBe('boolean');
    });
  });

  describe('unloadModel', () => {
    it('should return boolean success', async () => {
      (mockCoreMLModule.unloadModel as jest.Mock).mockResolvedValue(true);

      const result = await mockCoreMLModule.unloadModel();
      expect(typeof result).toBe('boolean');
    });
  });

  describe('isModelLoaded', () => {
    it('should return boolean state', async () => {
      (mockCoreMLModule.isModelLoaded as jest.Mock).mockResolvedValue(true);

      const result = await mockCoreMLModule.isModelLoaded();
      expect(typeof result).toBe('boolean');
    });
  });

  describe('getLoadedModelPath', () => {
    it('should return string path when model loaded', async () => {
      (mockCoreMLModule.getLoadedModelPath as jest.Mock).mockResolvedValue('/path/to/model');

      const result = await mockCoreMLModule.getLoadedModelPath();
      expect(typeof result).toBe('string');
    });

    it('should return null when no model loaded', async () => {
      (mockCoreMLModule.getLoadedModelPath as jest.Mock).mockResolvedValue(null);

      const result = await mockCoreMLModule.getLoadedModelPath();
      expect(result).toBeNull();
    });
  });

  describe('generateImage', () => {
    const validParams = {
      prompt: 'A beautiful sunset over mountains',
      negativePrompt: 'blurry, ugly',
      steps: 20,
      guidanceScale: 7.5,
      seed: 12345,
      width: 512,
      height: 512,
    };

    it('should accept valid generation params and return expected shape', async () => {
      const mockResult = {
        id: 'img-abc',
        imagePath: '/path/to/generated.png',
        width: 512,
        height: 512,
        seed: 12345,
      };
      (mockCoreMLModule.generateImage as jest.Mock).mockResolvedValue(mockResult);

      const result = await mockCoreMLModule.generateImage(validParams);

      expect(result).toHaveProperty('id');
      expect(result).toHaveProperty('imagePath');
      expect(result).toHaveProperty('width');
      expect(result).toHaveProperty('height');
      expect(result).toHaveProperty('seed');
      expect(typeof result.id).toBe('string');
      expect(typeof result.imagePath).toBe('string');
      expect(typeof result.width).toBe('number');
      expect(typeof result.height).toBe('number');
      expect(typeof result.seed).toBe('number');
    });

    it('should work with minimal params (prompt only)', async () => {
      const mockResult = {
        id: 'img-min',
        imagePath: '/path/to/img.png',
        width: 512,
        height: 512,
        seed: 99999,
      };
      (mockCoreMLModule.generateImage as jest.Mock).mockResolvedValue(mockResult);

      await mockCoreMLModule.generateImage({ prompt: 'A cat' });

      expect(mockCoreMLModule.generateImage).toHaveBeenCalledWith(
        expect.objectContaining({ prompt: 'A cat' })
      );
    });
  });

  describe('cancelGeneration', () => {
    it('should return boolean success', async () => {
      (mockCoreMLModule.cancelGeneration as jest.Mock).mockResolvedValue(true);

      const result = await mockCoreMLModule.cancelGeneration();
      expect(typeof result).toBe('boolean');
    });
  });

  describe('isGenerating', () => {
    it('should return boolean state', async () => {
      (mockCoreMLModule.isGenerating as jest.Mock).mockResolvedValue(false);

      const result = await mockCoreMLModule.isGenerating();
      expect(typeof result).toBe('boolean');
    });
  });

  describe('isNpuSupported', () => {
    it('should return true on iOS (Apple Neural Engine)', async () => {
      (mockCoreMLModule.isNpuSupported as jest.Mock).mockResolvedValue(true);

      const result = await mockCoreMLModule.isNpuSupported();
      expect(result).toBe(true);
    });
  });

  describe('getGeneratedImages', () => {
    it('should return array of generated images', async () => {
      const mockImages = [
        {
          id: 'img-1',
          prompt: 'A sunset',
          imagePath: '/path/to/img1.png',
          width: 512,
          height: 512,
          steps: 20,
          seed: 12345,
          modelId: 'sd21-coreml',
          createdAt: '2026-02-08T10:30:00Z',
        },
      ];
      (mockCoreMLModule.getGeneratedImages as jest.Mock).mockResolvedValue(mockImages);

      const result = await mockCoreMLModule.getGeneratedImages();

      expect(Array.isArray(result)).toBe(true);
      expect(result[0]).toHaveProperty('id');
      expect(result[0]).toHaveProperty('imagePath');
      expect(result[0]).toHaveProperty('createdAt');
    });

    it('should return empty array when no images', async () => {
      (mockCoreMLModule.getGeneratedImages as jest.Mock).mockResolvedValue([]);

      const result = await mockCoreMLModule.getGeneratedImages();
      expect(result).toEqual([]);
    });
  });

  describe('deleteGeneratedImage', () => {
    it('should accept image ID and return boolean', async () => {
      (mockCoreMLModule.deleteGeneratedImage as jest.Mock).mockResolvedValue(true);

      const result = await mockCoreMLModule.deleteGeneratedImage('img-abc');

      expect(mockCoreMLModule.deleteGeneratedImage).toHaveBeenCalledWith('img-abc');
      expect(typeof result).toBe('boolean');
    });
  });

  describe('Progress Events (same event names as Android)', () => {
    it('should emit LocalDreamProgress events', () => {
      const progressEvent = {
        step: 10,
        totalSteps: 20,
        progress: 0.5,
      };

      expect(progressEvent).toHaveProperty('step');
      expect(progressEvent).toHaveProperty('totalSteps');
      expect(progressEvent).toHaveProperty('progress');
      expect(progressEvent.progress).toBeGreaterThanOrEqual(0);
      expect(progressEvent.progress).toBeLessThanOrEqual(1);
    });

    it('should emit LocalDreamError events', () => {
      const errorEvent = {
        error: 'Core ML pipeline failed',
      };

      expect(errorEvent).toHaveProperty('error');
      expect(typeof errorEvent.error).toBe('string');
    });
  });

  describe('Interface parity with LocalDreamModule', () => {
    it('should expose all required methods', () => {
      const requiredMethods = [
        'loadModel',
        'unloadModel',
        'isModelLoaded',
        'getLoadedModelPath',
        'generateImage',
        'cancelGeneration',
        'isGenerating',
        'isNpuSupported',
        'getGeneratedImages',
        'deleteGeneratedImage',
      ];

      for (const method of requiredMethods) {
        expect(mockCoreMLModule).toHaveProperty(method);
        expect(typeof (mockCoreMLModule as any)[method]).toBe('function');
      }
    });
  });
});


================================================
FILE: __tests__/contracts/iosDownloadManager.contract.test.ts
================================================
/**
 * Contract Tests: iOS DownloadManagerModule (Background Downloads)
 *
 * Verifies that the iOS DownloadManagerModule (URLSession-based) exposes
 * the same interface as the Android DownloadManagerModule (DownloadManager-based).
 *
 * Both modules are registered under the same name "DownloadManagerModule"
 * so that backgroundDownloadService.ts works on both platforms unchanged.
 */


// The iOS module must match this interface (same as Android)
interface DownloadManagerModuleInterface {
  startDownload(params: {
    url: string;
    fileName: string;
    modelId: string;
    title?: string;
    description?: string;
    totalBytes?: number;
  }): Promise<{
    downloadId: number;
    fileName: string;
    modelId: string;
  }>;

  cancelDownload(downloadId: number): Promise<void>;

  getActiveDownloads(): Promise<Array<{
    downloadId: number;
    fileName: string;
    modelId: string;
    status: string;
    bytesDownloaded: number;
    totalBytes: number;
    startedAt: number;
    localUri?: string;
    failureReason?: string;
  }>>;

  getDownloadProgress(downloadId: number): Promise<{
    bytesDownloaded: number;
    totalBytes: number;
    status: string;
  }>;

  moveCompletedDownload(downloadId: number, targetPath: string): Promise<string>;

  // iOS no-ops for API compatibility with Android's polling model
  startProgressPolling(): void;
  stopProgressPolling(): void;
}

// Mock the iOS native module
const mockDownloadModule: DownloadManagerModuleInterface = {
  startDownload: jest.fn(),
  cancelDownload: jest.fn(),
  getActiveDownloads: jest.fn(),
  getDownloadProgress: jest.fn(),
  moveCompletedDownload: jest.fn(),
  startProgressPolling: jest.fn(),
  stopProgressPolling: jest.fn(),
};

jest.mock('react-native', () => ({
  NativeModules: {
    DownloadManagerModule: mockDownloadModule,
  },
  NativeEventEmitter: jest.fn().mockImplementation(() => ({
    addListener: jest.fn().mockReturnValue({ remove: jest.fn() }),
    removeAllListeners: jest.fn(),
  })),
  Platform: { OS: 'ios' },
}));

describe('iOS DownloadManagerModule Contract (parity with Android)', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  // ========================================================================
  // Interface parity
  // ========================================================================
  describe('Interface parity with Android', () => {
    it('exposes all required methods', () => {
      const requiredMethods = [
        'startDownload',
        'cancelDownload',
        'getActiveDownloads',
        'getDownloadProgress',
        'moveCompletedDownload',
        'startProgressPolling',
        'stopProgressPolling',
      ];

      for (const method of requiredMethods) {
        expect(mockDownloadModule).toHaveProperty(method);
        expect(typeof (mockDownloadModule as any)[method]).toBe('function');
      }
    });
  });

  // ========================================================================
  // startDownload
  // ========================================================================
  describe('startDownload', () => {
    it('accepts download params and returns downloadId + metadata', async () => {
      (mockDownloadModule.startDownload as jest.Mock).mockResolvedValue({
        downloadId: 1,
        fileName: 'sd21-coreml.zip',
        modelId: 'coreml_sd21',
      });

      const result = await mockDownloadModule.startDownload({
        url: 'https://huggingface.co/apple/coreml-stable-diffusion-2-1-base/resolve/main/model.zip',
        fileName: 'sd21-coreml.zip',
        modelId: 'coreml_sd21',
        title: 'Downloading SD 2.1 (Core ML)',
        description: 'Model download in progress...',
        totalBytes: 2_500_000_000,
      });

      expect(result).toHaveProperty('downloadId');
      expect(result).toHaveProperty('fileName');
      expect(result).toHaveProperty('modelId');
      expect(typeof result.downloadId).toBe('number');
      expect(typeof result.fileName).toBe('string');
    });

    it('works with minimal params (no title/description/totalBytes)', async () => {
      (mockDownloadModule.startDownload as jest.Mock).mockResolvedValue({
        downloadId: 2,
        fileName: 'model.gguf',
        modelId: 'test-model',
      });

      await mockDownloadModule.startDownload({
        url: 'https://example.com/model.gguf',
        fileName: 'model.gguf',
        modelId: 'test-model',
      });

      expect(mockDownloadModule.startDownload).toHaveBeenCalledWith(
        expect.objectContaining({
          url: expect.any(String),
          fileName: expect.any(String),
          modelId: expect.any(String),
        }),
      );
    });
  });

  // ========================================================================
  // cancelDownload
  // ========================================================================
  describe('cancelDownload', () => {
    it('accepts downloadId and returns void', async () => {
      (mockDownloadModule.cancelDownload as jest.Mock).mockResolvedValue(undefined);

      await mockDownloadModule.cancelDownload(42);

      expect(mockDownloadModule.cancelDownload).toHaveBeenCalledWith(42);
    });
  });

  // ========================================================================
  // getActiveDownloads
  // ========================================================================
  describe('getActiveDownloads', () => {
    it('returns array of download info objects', async () => {
      const mockDownloads = [
        {
          downloadId: 1,
          fileName: 'model.zip',
          modelId: 'coreml_sd21',
          status: 'running',
          bytesDownloaded: 500_000_000,
          totalBytes: 2_500_000_000,
          startedAt: Date.now(),
        },
      ];
      (mockDownloadModule.getActiveDownloads as jest.Mock).mockResolvedValue(mockDownloads);

      const result = await mockDownloadModule.getActiveDownloads();

      expect(Array.isArray(result)).toBe(true);
      expect(result[0]).toHaveProperty('downloadId');
      expect(result[0]).toHaveProperty('fileName');
      expect(result[0]).toHaveProperty('modelId');
      expect(result[0]).toHaveProperty('status');
      expect(result[0]).toHaveProperty('bytesDownloaded');
      expect(result[0]).toHaveProperty('totalBytes');
      expect(result[0]).toHaveProperty('startedAt');
    });

    it('returns empty array when no active downloads', async () => {
      (mockDownloadModule.getActiveDownloads as jest.Mock).mockResolvedValue([]);

      const result = await mockDownloadModule.getActiveDownloads();

      expect(result).toEqual([]);
    });

    it('includes completed downloads with localUri', async () => {
      const mockDownloads = [
        {
          downloadId: 1,
          fileName: 'model.zip',
          modelId: 'coreml_sd21',
          status: 'completed',
          bytesDownloaded: 2_500_000_000,
          totalBytes: 2_500_000_000,
          startedAt: Date.now() - 60000,
          localUri: '/var/mobile/.../Documents/downloads/model.zip',
        },
      ];
      (mockDownloadModule.getActiveDownloads as jest.Mock).mockResolvedValue(mockDownloads);

      const result = await mockDownloadModule.getActiveDownloads();

      expect(result[0].localUri).toBeDefined();
      expect(typeof result[0].localUri).toBe('string');
    });

    it('includes failed downloads with failureReason', async () => {
      const mockDownloads = [
        {
          downloadId: 2,
          fileName: 'model.zip',
          modelId: 'coreml_sd21',
          status: 'failed',
          bytesDownloaded: 100_000,
          totalBytes: 2_500_000_000,
          startedAt: Date.now() - 30000,
          failureReason: 'Network connection lost',
        },
      ];
      (mockDownloadModule.getActiveDownloads as jest.Mock).mockResolvedValue(mockDownloads);

      const result = await mockDownloadModule.getActiveDownloads();

      expect(result[0].status).toBe('failed');
      expect(result[0].failureReason).toBeDefined();
    });
  });

  // ========================================================================
  // getDownloadProgress
  // ========================================================================
  describe('getDownloadProgress', () => {
    it('returns progress for a specific download', async () => {
      (mockDownloadModule.getDownloadProgress as jest.Mock).mockResolvedValue({
        bytesDownloaded: 1_000_000_000,
        totalBytes: 2_500_000_000,
        status: 'running',
      });

      const result = await mockDownloadModule.getDownloadProgress(1);

      expect(result).toHaveProperty('bytesDownloaded');
      expect(result).toHaveProperty('totalBytes');
      expect(result).toHaveProperty('status');
      expect(typeof result.bytesDownloaded).toBe('number');
      expect(typeof result.totalBytes).toBe('number');
    });
  });

  // ========================================================================
  // moveCompletedDownload
  // ========================================================================
  describe('moveCompletedDownload', () => {
    it('moves file from temp location to target path', async () => {
      const targetPath = '/var/mobile/.../Documents/image_models/sd21/model.zip';
      (mockDownloadModule.moveCompletedDownload as jest.Mock).mockResolvedValue(targetPath);

      const result = await mockDownloadModule.moveCompletedDownload(1, targetPath);

      expect(mockDownloadModule.moveCompletedDownload).toHaveBeenCalledWith(1, targetPath);
      expect(typeof result).toBe('string');
      expect(result).toBe(targetPath);
    });
  });

  // ========================================================================
  // Polling compatibility stubs
  // ========================================================================
  describe('Polling compatibility (iOS no-ops)', () => {
    it('startProgressPolling exists but is a no-op on iOS', () => {
      // On iOS, progress comes via URLSessionDownloadDelegate (push-based),
      // so polling is unnecessary. These methods exist for API compatibility.
      mockDownloadModule.startProgressPolling();

      expect(mockDownloadModule.startProgressPolling).toHaveBeenCalled();
    });

    it('stopProgressPolling exists but is a no-op on iOS', () => {
      mockDownloadModule.stopProgressPolling();

      expect(mockDownloadModule.stopProgressPolling).toHaveBeenCalled();
    });
  });

  // ========================================================================
  // Event names and shapes (same as Android)
  // ========================================================================
  describe('Events (same names and shapes as Android)', () => {
    it('emits DownloadProgress with expected shape', () => {
      const progressEvent = {
        downloadId: 1,
        fileName: 'model.zip',
        modelId: 'coreml_sd21',
        bytesDownloaded: 500_000_000,
        totalBytes: 2_500_000_000,
        status: 'running',
      };

      expect(progressEvent).toHaveProperty('downloadId');
      expect(progressEvent).toHaveProperty('fileName');
      expect(progressEvent).toHaveProperty('modelId');
      expect(progressEvent).toHaveProperty('bytesDownloaded');
      expect(progressEvent).toHaveProperty('totalBytes');
      expect(progressEvent).toHaveProperty('status');
      expect(typeof progressEvent.downloadId).toBe('number');
      expect(typeof progressEvent.bytesDownloaded).toBe('number');
    });

    it('emits DownloadComplete with expected shape', () => {
      const completeEvent = {
        downloadId: 1,
        fileName: 'model.zip',
        modelId: 'coreml_sd21',
        bytesDownloaded: 2_500_000_000,
        totalBytes: 2_500_000_000,
        status: 'completed',
        localUri: '/var/mobile/.../Documents/downloads/model.zip',
      };

      expect(completeEvent).toHaveProperty('downloadId');
      expect(completeEvent).toHaveProperty('fileName');
      expect(completeEvent).toHaveProperty('modelId');
      expect(completeEvent).toHaveProperty('status', 'completed');
      expect(completeEvent).toHaveProperty('localUri');
      expect(typeof completeEvent.localUri).toBe('string');
    });

    it('emits DownloadError with expected shape', () => {
      const errorEvent = {
        downloadId: 1,
        fileName: 'model.zip',
        modelId: 'coreml_sd21',
        status: 'failed',
        reason: 'Network connection lost',
      };

      expect(errorEvent).toHaveProperty('downloadId');
      expect(errorEvent).toHaveProperty('fileName');
      expect(errorEvent).toHaveProperty('modelId');
      expect(errorEvent).toHaveProperty('status', 'failed');
      expect(errorEvent).toHaveProperty('reason');
      expect(typeof errorEvent.reason).toBe('string');
    });

    it('uses same event names as Android', () => {
      // These event names are hardcoded in backgroundDownloadService.ts
      // and must match on both platforms.
      const expectedEvents = [
        'DownloadProgress',
        'DownloadComplete',
        'DownloadError',
      ];

      // This is a documentation/contract test — the names are verified
      // against the TypeScript service that subscribes to them.
      expectedEvents.forEach(eventName => {
        expect(typeof eventName).toBe('string');
        expect(eventName.length).toBeGreaterThan(0);
      });
    });
  });

  // ========================================================================
  // iOS-specific behaviors
  // ========================================================================
  describe('iOS-specific download behaviors', () => {
    it('download status values match Android constants', () => {
      // Both platforms must use the same status strings
      const validStatuses = ['pending', 'running', 'paused', 'completed', 'failed'];

      validStatuses.forEach(status => {
        expect(typeof status).toBe('string');
      });
    });

    it('completed download includes localUri (moved from temp)', () => {
      // On iOS, URLSession downloads complete to a temporary file.
      // The native module must move it to Documents/ synchronously
      // and include the final path as localUri.
      const completedDownload = {
        downloadId: 1,
        fileName: 'model.zip',
        modelId: 'coreml_sd21',
        status: 'completed',
        bytesDownloaded: 2_500_000_000,
        totalBytes: 2_500_000_000,
        startedAt: Date.now() - 120000,
        localUri: '/var/mobile/Containers/Data/Application/.../Documents/downloads/model.zip',
      };

      expect(completedDownload.localUri).toBeDefined();
      expect(completedDownload.localUri).toContain('Documents');
    });
  });
});


================================================
FILE: __tests__/contracts/llama.rn.test.ts
================================================
/**
 * llama.rn Contract Tests
 *
 * These tests verify that our usage of llama.rn matches its expected interface.
 * They test the contract between our code and the native module.
 *
 * Note: These tests use mocks - they verify interface compatibility,
 * not actual native functionality (which requires a real device).
 */

/**
 * llama.rn Contract Tests
 *
 * These tests document and verify the expected interface of the llama.rn module.
 * They serve as living documentation for how we use the library.
 *
 * Note: These tests don't call the real native module - they verify our
 * understanding of the API contract through interface documentation.
 */

describe('llama.rn Contract', () => {
  // ============================================================================
  // initLlama Contract
  // ============================================================================
  describe('initLlama interface', () => {
    it('requires model path parameter', () => {
      // Document the required parameter
      const requiredParams = {
        model: '/path/to/model.gguf',
      };

      expect(requiredParams).toHaveProperty('model');
      expect(typeof requiredParams.model).toBe('string');
    });

    it('accepts context configuration options', () => {
      // Document optional configuration
      const configOptions = {
        model: '/path/to/model.gguf',
        n_ctx: 2048,       // Context length
        n_batch: 256,      // Batch size
        n_threads: 4,      // CPU threads
        n_gpu_layers: 6,   // GPU layers to offload
      };

      expect(configOptions.n_ctx).toBeGreaterThan(0);
      expect(configOptions.n_batch).toBeGreaterThan(0);
      expect(configOptions.n_threads).toBeGreaterThan(0);
      expect(configOptions.n_gpu_layers).toBeGreaterThanOrEqual(0);
    });

    it('accepts memory management options', () => {
      const memoryOptions = {
        use_mlock: false,  // Lock model in RAM
        use_mmap: true,    // Memory-map the model file
      };

      expect(typeof memoryOptions.use_mlock).toBe('boolean');
      expect(typeof memoryOptions.use_mmap).toBe('boolean');
    });

    it('accepts performance optimization options', () => {
      const perfOptions = {
        flash_attn: true,       // Flash attention
        cache_type_k: 'q8_0',   // KV cache quantization
        cache_type_v: 'q8_0',
      };

      expect(perfOptions.flash_attn).toBe(true);
      expect(['q8_0', 'f16', 'f32']).toContain(perfOptions.cache_type_k);
    });

    it('returns context with expected properties', () => {
      // Document expected return type
      const expectedContext = {
        id: 'context-id',
        gpu: false,
        model: { nParams: 1000000 },
        release: () => Promise.resolve(),
        completion: () => Promise.resolve({ text: '' }),
      };

      expect(expectedContext).toHaveProperty('id');
      expect(expectedContext).toHaveProperty('gpu');
      expect(expectedContext).toHaveProperty('release');
    });

    it('returns GPU status information', () => {
      // Document GPU-related return properties
      const gpuInfo = {
        gpu: true,
        reasonNoGPU: '',
        devices: ['Metal'],
      };

      expect(typeof gpuInfo.gpu).toBe('boolean');
    });
  });

  // ============================================================================
  // LlamaContext Contract
  // ============================================================================
  describe('LlamaContext interface', () => {
    it('context has release method', () => {
      const context = {
        release: jest.fn(() => Promise.resolve()),
      };

      expect(typeof context.release).toBe('function');
    });

    it('context has completion method', () => {
      const context = {
        completion: jest.fn(() => Promise.resolve({
          text: 'response',
          tokens_predicted: 10,
        })),
      };

      expect(typeof context.completion).toBe('function');
    });

    it('context supports multimodal initialization', () => {
      const context = {
        initMultimodal: jest.fn(() => Promise.resolve(true)),
        getMultimodalSupport: jest.fn(() => Promise.resolve({ vision: true, audio: false })),
      };

      expect(typeof context.initMultimodal).toBe('function');
    });
  });

  // ============================================================================
  // Message Format Contract
  // ============================================================================
  describe('Message Format', () => {
    it('accepts standard chat message format', () => {
      // Verify our message format matches llama.rn expectations
      const messages = [
        { role: 'system', content: 'You are a helpful assistant.' },
        { role: 'user', content: 'Hello!' },
        { role: 'assistant', content: 'Hi there!' },
      ];

      // Each message should have role and content
      messages.forEach(msg => {
        expect(msg).toHaveProperty('role');
        expect(msg).toHaveProperty('content');
        expect(['system', 'user', 'assistant']).toContain(msg.role);
        expect(typeof msg.content).toBe('string');
      });
    });

    it('supports multimodal message format', () => {
      // Multimodal messages can have content as array
      const multimodalMessage = {
        role: 'user',
        content: [
          { type: 'text', text: 'What is in this image?' },
          { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,...' } },
        ],
      };

      expect(multimodalMessage.role).toBe('user');
      expect(Array.isArray(multimodalMessage.content)).toBe(true);
      expect(multimodalMessage.content[0]).toHaveProperty('type');
    });
  });

  // ============================================================================
  // Completion Options Contract
  // ============================================================================
  describe('Completion Options', () => {
    it('supports temperature parameter', () => {
      const options = {
        temperature: 0.7,
      };

      expect(options.temperature).toBeGreaterThanOrEqual(0);
      expect(options.temperature).toBeLessThanOrEqual(2);
    });

    it('supports top_p parameter', () => {
      const options = {
        top_p: 0.9,
      };

      expect(options.top_p).toBeGreaterThanOrEqual(0);
      expect(options.top_p).toBeLessThanOrEqual(1);
    });

    it('supports max_tokens parameter', () => {
      const options = {
        n_predict: 1024, // llama.rn uses n_predict
      };

      expect(options.n_predict).toBeGreaterThan(0);
    });

    it('supports repeat_penalty parameter', () => {
      const options = {
        repeat_penalty: 1.1,
      };

      expect(options.repeat_penalty).toBeGreaterThanOrEqual(1);
    });

    it('supports stop sequences', () => {
      const options = {
        stop: ['</s>', '<|end|>', '\n\n'],
      };

      expect(Array.isArray(options.stop)).toBe(true);
      options.stop.forEach(seq => {
        expect(typeof seq).toBe('string');
      });
    });
  });

  // ============================================================================
  // Streaming Contract
  // ============================================================================
  describe('Streaming', () => {
    it('completion result includes token timing info', () => {
      // Expected structure of completion result
      const expectedResult = {
        text: 'Generated text',
        tokens_predicted: 10,
        tokens_evaluated: 5,
        timings: {
          predicted_per_token_ms: 50,
          predicted_per_second: 20,
        },
      };

      expect(expectedResult).toHaveProperty('text');
      expect(expectedResult).toHaveProperty('tokens_predicted');
      expect(expectedResult).toHaveProperty('timings');
      expect(expectedResult.timings).toHaveProperty('predicted_per_second');
    });
  });

  // ============================================================================
  // Error Handling Contract
  // ============================================================================
  describe('Error Handling', () => {
    it('documents expected error cases', () => {
      // Document the error cases we handle
      const expectedErrors = [
        'Model file not found',
        'Context creation failed',
        'Out of memory',
        'Invalid model format',
        'GPU initialization failed',
      ];

      // These are the error messages we should handle gracefully
      expectedErrors.forEach(error => {
        expect(typeof error).toBe('string');
      });
    });
  });
});


================================================
FILE: __tests__/contracts/llamaContext.contract.test.ts
================================================
/**
 * Contract Tests: llama.rn Native Module
 *
 * These tests verify that the llama.rn native module interface
 * matches our TypeScript expectations. They test the shape of
 * inputs/outputs without requiring actual model execution.
 */

import { initLlama, LlamaContext } from 'llama.rn';

// Mock the native module
jest.mock('llama.rn', () => ({
  initLlama: jest.fn(),
}));

const mockInitLlama = initLlama as jest.MockedFunction<typeof initLlama>;

describe('llama.rn Contract', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('initLlama', () => {
    const validInitParams = {
      model: '/path/to/model.gguf',
      use_mlock: false,
      n_batch: 512,
      n_threads: 4,
      use_mmap: true,
      vocab_only: false,
      flash_attn: true,
      cache_type_k: 'f16' as const,
      cache_type_v: 'f16' as const,
      n_ctx: 4096,
      n_gpu_layers: 99,
    };

    it('should accept valid initialization parameters', async () => {
      const mockContext: Partial<LlamaContext> = {
        gpu: true,
        reasonNoGPU: '',
        completion: jest.fn(),
        stopCompletion: jest.fn(),
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      await initLlama(validInitParams);

      expect(mockInitLlama).toHaveBeenCalledWith(
        expect.objectContaining({
          model: expect.any(String),
          n_ctx: expect.any(Number),
          n_gpu_layers: expect.any(Number),
          n_threads: expect.any(Number),
        })
      );
    });

    it('should return context with expected properties', async () => {
      const mockContext: Partial<LlamaContext> = {
        gpu: true,
        reasonNoGPU: '',
        devices: ['Apple M1'],
        model: { metadata: { 'general.name': 'test-model' } } as any,
        androidLib: undefined,
        systemInfo: 'Apple M1 Pro',
        completion: jest.fn(),
        tokenize: jest.fn(),
        stopCompletion: jest.fn(),
        release: jest.fn(),
        clearCache: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama(validInitParams);

      expect(context).toHaveProperty('gpu');
      expect(context).toHaveProperty('completion');
      expect(context).toHaveProperty('stopCompletion');
      expect(context).toHaveProperty('release');
    });

    it('should handle GPU unavailable reason', async () => {
      const mockContext: Partial<LlamaContext> = {
        gpu: false,
        reasonNoGPU: 'Metal not supported on this device',
        completion: jest.fn(),
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama(validInitParams);

      expect(context.gpu).toBe(false);
      expect(context.reasonNoGPU).toContain('Metal');
    });
  });

  describe('LlamaContext.completion', () => {
    it('should accept text-only completion params', async () => {
      const mockCompletion = jest.fn().mockResolvedValue({});
      const mockContext: Partial<LlamaContext> = {
        completion: mockCompletion,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({
        model: '/path/to/model.gguf',
        n_ctx: 4096,
        n_gpu_layers: 0,
      } as any);

      const completionParams = {
        prompt: 'Hello, how are you?',
        n_predict: 256,
        temperature: 0.7,
        top_k: 40,
        top_p: 0.95,
        penalty_repeat: 1.1,
        stop: ['</s>', '<|eot_id|>'],
      };

      const tokenCallback = jest.fn();
      await context.completion(completionParams, tokenCallback);

      expect(mockCompletion).toHaveBeenCalledWith(
        expect.objectContaining({
          prompt: expect.any(String),
          n_predict: expect.any(Number),
          temperature: expect.any(Number),
          stop: expect.any(Array),
        }),
        expect.any(Function)
      );
    });

    it('should accept chat messages format', async () => {
      const mockCompletion = jest.fn().mockResolvedValue({});
      const mockContext: Partial<LlamaContext> = {
        completion: mockCompletion,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);

      const completionParams = {
        messages: [
          { role: 'system', content: 'You are helpful.' },
          { role: 'user', content: 'Hello!' },
        ],
        n_predict: 256,
        temperature: 0.7,
        top_k: 40,
        top_p: 0.95,
        penalty_repeat: 1.1,
        stop: [],
      };

      await context.completion(completionParams, jest.fn());

      expect(mockCompletion).toHaveBeenCalledWith(
        expect.objectContaining({
          messages: expect.arrayContaining([
            expect.objectContaining({ role: 'system' }),
            expect.objectContaining({ role: 'user' }),
          ]),
        }),
        expect.any(Function)
      );
    });

    it('should accept multimodal messages with images', async () => {
      const mockCompletion = jest.fn().mockResolvedValue({});
      const mockContext: Partial<LlamaContext> = {
        completion: mockCompletion,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);

      const multimodalMessage = {
        role: 'user',
        content: [
          { type: 'text', text: 'What is in this image?' },
          { type: 'image_url', image_url: { url: 'file:///path/to/image.jpg' } },
        ],
      };

      const completionParams = {
        messages: [multimodalMessage],
        n_predict: 256,
        temperature: 0.7,
        top_k: 40,
        top_p: 0.95,
        penalty_repeat: 1.1,
        stop: [],
      };

      await context.completion(completionParams, jest.fn());

      expect(mockCompletion).toHaveBeenCalledWith(
        expect.objectContaining({
          messages: expect.arrayContaining([
            expect.objectContaining({
              content: expect.arrayContaining([
                expect.objectContaining({ type: 'text' }),
                expect.objectContaining({ type: 'image_url' }),
              ]),
            }),
          ]),
        }),
        expect.any(Function)
      );
    });

    it('should call token callback with expected shape', async () => {
      const tokenCallback = jest.fn();
      const mockCompletion = jest.fn().mockImplementation(async (params, callback) => {
        // Simulate token streaming
        callback({ token: 'Hello' });
        callback({ token: ' ' });
        callback({ token: 'world' });
        return {};
      });

      const mockContext: Partial<LlamaContext> = {
        completion: mockCompletion,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);
      await context.completion({ prompt: 'Hi', n_predict: 10 } as any, tokenCallback);

      expect(tokenCallback).toHaveBeenCalledWith(expect.objectContaining({ token: expect.any(String) }));
      expect(tokenCallback).toHaveBeenCalledTimes(3);
    });
  });

  describe('LlamaContext.tokenize', () => {
    it('should return token array', async () => {
      const mockTokenize = jest.fn().mockResolvedValue({ tokens: [1, 2, 3, 4, 5] });
      const mockContext: Partial<LlamaContext> = {
        tokenize: mockTokenize,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);
      const result = await context.tokenize!('Hello world');

      expect(result).toHaveProperty('tokens');
      expect(Array.isArray(result.tokens)).toBe(true);
      expect(result.tokens?.every(t => typeof t === 'number')).toBe(true);
    });
  });

  describe('LlamaContext.initMultimodal', () => {
    it('should accept mmproj path and GPU flag', async () => {
      const mockInitMultimodal = jest.fn().mockResolvedValue(true);
      const mockContext: Partial<LlamaContext> = {
        initMultimodal: mockInitMultimodal,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);
      const result = await context.initMultimodal!({
        path: '/path/to/mmproj.gguf',
        use_gpu: true,
      });

      expect(mockInitMultimodal).toHaveBeenCalledWith({
        path: expect.any(String),
        use_gpu: expect.any(Boolean),
      });
      expect(typeof result).toBe('boolean');
    });
  });

  describe('LlamaContext.getMultimodalSupport', () => {
    it('should return support flags', async () => {
      const mockGetMultimodalSupport = jest.fn().mockResolvedValue({
        vision: true,
        audio: false,
      });
      const mockContext: Partial<LlamaContext> = {
        getMultimodalSupport: mockGetMultimodalSupport,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);
      const support = await context.getMultimodalSupport!();

      expect(support).toHaveProperty('vision');
      expect(support).toHaveProperty('audio');
      expect(typeof support.vision).toBe('boolean');
    });
  });

  describe('LlamaContext.stopCompletion', () => {
    it('should be callable and return promise', async () => {
      const mockStopCompletion = jest.fn().mockResolvedValue(undefined);
      const mockContext: Partial<LlamaContext> = {
        stopCompletion: mockStopCompletion,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);
      await context.stopCompletion();

      expect(mockStopCompletion).toHaveBeenCalled();
    });
  });

  describe('LlamaContext.clearCache', () => {
    it('should accept optional clearData flag', async () => {
      const mockClearCache = jest.fn().mockResolvedValue(undefined);
      const mockContext: Partial<LlamaContext> = {
        clearCache: mockClearCache,
        release: jest.fn(),
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);

      // Without flag
      await context.clearCache!();
      expect(mockClearCache).toHaveBeenCalled();

      // With flag
      mockClearCache.mockClear();
      await context.clearCache!(true);
      expect(mockClearCache).toHaveBeenCalledWith(true);
    });
  });

  describe('LlamaContext.release', () => {
    it('should be callable for cleanup', async () => {
      const mockRelease = jest.fn().mockResolvedValue(undefined);
      const mockContext: Partial<LlamaContext> = {
        release: mockRelease,
      };
      mockInitLlama.mockResolvedValue(mockContext as LlamaContext);

      const context = await initLlama({ model: '/path/to/model.gguf' } as any);
      await context.release();

      expect(mockRelease).toHaveBeenCalled();
    });
  });

  describe('Error handling', () => {
    it('should reject on invalid model path', async () => {
      mockInitLlama.mockRejectedValue(new Error('Failed to load model: file not found'));

      await expect(initLlama({ model: '/invalid/path.gguf' } as any))
        .rejects.toThrow('Failed to load model');
    });

    it('should reject on out of memory', async () => {
      mockInitLlama.mockRejectedValue(new Error('Failed to allocate memory'));

      await expect(initLlama({ model: '/path/to/large-model.gguf' } as any))
        .rejects.toThrow('memory');
    });
  });
});


================================================
FILE: __tests__/contracts/localDream.contract.test.ts
================================================
/**
 * Contract Tests: LocalDream Native Module (Image Generation)
 *
 * These tests verify that the LocalDream native module interface
 * matches our TypeScript expectations for image generation.
 */

// Define the expected interface
export interface LocalDreamModuleInterface {
  loadModel(params: {
    modelPath: string;
    threads?: number;
    backend: 'mnn' | 'qnn' | 'auto';
  }): Promise<boolean>;

  unloadModel(): Promise<boolean>;
  isModelLoaded(): Promise<boolean>;
  getLoadedModelPath(): Promise<string | null>;
  getLoadedThreads(): number;

  generateImage(params: {
    prompt: string;
    negativePrompt?: string;
    steps?: number;
    guidanceScale?: number;
    seed?: number;
    width?: number;
    height?: number;
    previewInterval?: number;
  }): Promise<{
    id: string;
    imagePath: string;
    width: number;
    height: number;
    seed: number;
  }>;

  cancelGeneration(): Promise<boolean>;
  isGenerating(): Promise<boolean>;

  getGeneratedImages(): Promise<Array<{
    id: string;
    prompt: string;
    imagePath: string;
    width: number;
    height: number;
    steps: number;
    seed: number;
    modelId: string;
    createdAt: string;
  }>>;

  deleteGeneratedImage(imageId: string): Promise<boolean>;

  getConstants(): {
    DEFAULT_STEPS: number;
    DEFAULT_GUIDANCE_SCALE: number;
    DEFAULT_WIDTH: number;
    DEFAULT_HEIGHT: number;
    SUPPORTED_WIDTHS: number[];
    SUPPORTED_HEIGHTS: number[];
  };

  getServerPort(): Promise<number>;
  isNpuSupported(): Promise<boolean>;
}

// Mock NativeModules
const mockLocalDreamModule: LocalDreamModuleInterface = {
  loadModel: jest.fn(),
  unloadModel: jest.fn(),
  isModelLoaded: jest.fn(),
  getLoadedModelPath: jest.fn(),
  getLoadedThreads: jest.fn(),
  generateImage: jest.fn(),
  cancelGeneration: jest.fn(),
  isGenerating: jest.fn(),
  getGeneratedImages: jest.fn(),
  deleteGeneratedImage: jest.fn(),
  getConstants: jest.fn(),
  getServerPort: jest.fn(),
  isNpuSupported: jest.fn(),
};

jest.mock('react-native', () => ({
  NativeModules: {
    LocalDreamModule: mockLocalDreamModule,
  },
  NativeEventEmitter: jest.fn().mockImplementation(() => ({
    addListener: jest.fn().mockReturnValue({ remove: jest.fn() }),
    removeAllListeners: jest.fn(),
  })),
  Platform: { OS: 'android' },
}));

describe('LocalDream Contract', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('loadModel', () => {
    it('should accept valid model loading params', async () => {
      (mockLocalDreamModule.loadModel as jest.Mock).mockResolvedValue(true);

      const params = {
        modelPath: '/data/user/0/ai.offgridmobile/files/models/sdxl-turbo',
        threads: 4,
        backend: 'qnn' as const,
      };

      const result = await mockLocalDreamModule.loadModel(params);

      expect(mockLocalDreamModule.loadModel).toHaveBeenCalledWith(
        expect.objectContaining({
          modelPath: expect.any(String),
          threads: expect.any(Number),
          backend: expect.stringMatching(/^(mnn|qnn|auto)$/),
        })
      );
      expect(typeof result).toBe('boolean');
    });

    it('should work with optional threads param', async () => {
      (mockLocalDreamModule.loadModel as jest.Mock).mockResolvedValue(true);

      const params = {
        modelPath: '/path/to/model',
        backend: 'auto' as const,
      };

      await mockLocalDreamModule.loadModel(params);

      expect(mockLocalDreamModule.loadModel).toHaveBeenCalledWith(
        expect.objectContaining({
          modelPath: expect.any(String),
          backend: 'auto',
        })
      );
    });

    it('should accept mnn backend', async () => {
      (mockLocalDreamModule.loadModel as jest.Mock).mockResolvedValue(true);

      await mockLocalDreamModule.loadModel({
        modelPath: '/path/to/model',
        backend: 'mnn',
      });

      expect(mockLocalDreamModule.loadModel).toHaveBeenCalledWith(
        expect.objectContaining({ backend: 'mnn' })
      );
    });
  });

  describe('unloadModel', () => {
    it('should return boolean success', async () => {
      (mockLocalDreamModule.unloadModel as jest.Mock).mockResolvedValue(true);

      const result = await mockLocalDreamModule.unloadModel();

      expect(typeof result).toBe('boolean');
    });
  });

  describe('isModelLoaded', () => {
    it('should return boolean state', async () => {
      (mockLocalDreamModule.isModelLoaded as jest.Mock).mockResolvedValue(true);

      const result = await mockLocalDreamModule.isModelLoaded();

      expect(typeof result).toBe('boolean');
    });
  });

  describe('getLoadedModelPath', () => {
    it('should return string path when model loaded', async () => {
      (mockLocalDreamModule.getLoadedModelPath as jest.Mock).mockResolvedValue('/path/to/model');

      const result = await mockLocalDreamModule.getLoadedModelPath();

      expect(typeof result).toBe('string');
    });

    it('should return null when no model loaded', async () => {
      (mockLocalDreamModule.getLoadedModelPath as jest.Mock).mockResolvedValue(null);

      const result = await mockLocalDreamModule.getLoadedModelPath();

      expect(result).toBeNull();
    });
  });

  describe('generateImage', () => {
    const validGenerateParams = {
      prompt: 'A beautiful sunset over mountains',
      negativePrompt: 'blurry, ugly, distorted',
      steps: 20,
      guidanceScale: 7.5,
      seed: 12345,
      width: 512,
      height: 512,
      previewInterval: 5,
    };

    it('should accept valid generation params', async () => {
      const mockResult = {
        id: 'img-123',
        imagePath: '/data/user/0/ai.offgridmobile/files/generated/img-123.png',
        width: 512,
        height: 512,
        seed: 12345,
      };
      (mockLocalDreamModule.generateImage as jest.Mock).mockResolvedValue(mockResult);

      await mockLocalDreamModule.generateImage(validGenerateParams);

      expect(mockLocalDreamModule.generateImage).toHaveBeenCalledWith(
        expect.objectContaining({
          prompt: expect.any(String),
          steps: expect.any(Number),
          guidanceScale: expect.any(Number),
          width: expect.any(Number),
          height: expect.any(Number),
        })
      );
    });

    it('should return expected result shape', async () => {
      const mockResult = {
        id: 'img-123',
        imagePath: '/path/to/image.png',
        width: 512,
        height: 512,
        seed: 12345,
      };
      (mockLocalDreamModule.generateImage as jest.Mock).mockResolvedValue(mockResult);

      const result = await mockLocalDreamModule.generateImage(validGenerateParams);

      expect(result).toHaveProperty('id');
      expect(result).toHaveProperty('imagePath');
      expect(result).toHaveProperty('width');
      expect(result).toHaveProperty('height');
      expect(result).toHaveProperty('seed');
      expect(typeof result.id).toBe('string');
      expect(typeof result.imagePath).toBe('string');
      expect(typeof result.width).toBe('number');
      expect(typeof result.height).toBe('number');
      expect(typeof result.seed).toBe('number');
    });

    it('should work with minimal params (prompt only)', async () => {
      const mockResult = {
        id: 'img-456',
        imagePath: '/path/to/image.png',
        width: 512,
        height: 512,
        seed: 99999,
      };
      (mockLocalDreamModule.generateImage as jest.Mock).mockResolvedValue(mockResult);

      await mockLocalDreamModule.generateImage({ prompt: 'A cat' });

      expect(mockLocalDreamModule.generateImage).toHaveBeenCalledWith(
        expect.objectContaining({ prompt: 'A cat' })
      );
    });

    it('should generate random seed when not provided', async () => {
      const mockResult = {
        id: 'img-789',
        imagePath: '/path/to/image.png',
        width: 512,
        height: 512,
        seed: 987654321, // Random seed generated by native
      };
      (mockLocalDreamModule.generateImage as jest.Mock).mockResolvedValue(mockResult);

      const result = await mockLocalDreamModule.generateImage({
        prompt: 'A dog',
        // No seed provided
      });

      expect(result.seed).toBeDefined();
      expect(typeof result.seed).toBe('number');
    });
  });

  describe('cancelGeneration', () => {
    it('should return boolean success', async () => {
      (mockLocalDreamModule.cancelGeneration as jest.Mock).mockResolvedValue(true);

      const result = await mockLocalDreamModule.cancelGeneration();

      expect(typeof result).toBe('boolean');
    });
  });

  describe('isGenerating', () => {
    it('should return boolean state', async () => {
      (mockLocalDreamModule.isGenerating as jest.Mock).mockResolvedValue(false);

      const result = await mockLocalDreamModule.isGenerating();

      expect(typeof result).toBe('boolean');
    });
  });

  describe('getGeneratedImages', () => {
    it('should return array of generated images', async () => {
      const mockImages = [
        {
          id: 'img-1',
          prompt: 'A sunset',
          imagePath: '/path/to/img1.png',
          width: 512,
          height: 512,
          steps: 20,
          seed: 12345,
          modelId: 'sdxl-turbo',
          createdAt: '2024-01-15T10:30:00Z',
        },
        {
          id: 'img-2',
          prompt: 'A mountain',
          imagePath: '/path/to/img2.png',
          width: 768,
          height: 768,
          steps: 30,
          seed: 54321,
          modelId: 'sdxl-turbo',
          createdAt: '2024-01-15T11:00:00Z',
        },
      ];
      (mockLocalDreamModule.getGeneratedImages as jest.Mock).mockResolvedValue(mockImages);

      const result = await mockLocalDreamModule.getGeneratedImages();

      expect(Array.isArray(result)).toBe(true);
      expect(result.length).toBe(2);
      expect(result[0]).toHaveProperty('id');
      expect(result[0]).toHaveProperty('prompt');
      expect(result[0]).toHaveProperty('imagePath');
      expect(result[0]).toHaveProperty('createdAt');
    });

    it('should return empty array when no images', async () => {
      (mockLocalDreamModule.getGeneratedImages as jest.Mock).mockResolvedValue([]);

      const result = await mockLocalDreamModule.getGeneratedImages();

      expect(Array.isArray(result)).toBe(true);
      expect(result.length).toBe(0);
    });
  });

  describe('deleteGeneratedImage', () => {
    it('should accept image ID and return boolean', async () => {
      (mockLocalDreamModule.deleteGeneratedImage as jest.Mock).mockResolvedValue(true);

      const result = await mockLocalDreamModule.deleteGeneratedImage('img-123');

      expect(mockLocalDreamModule.deleteGeneratedImage).toHaveBeenCalledWith('img-123');
      expect(typeof result).toBe('boolean');
    });
  });

  describe('getConstants', () => {
    it('should return expected constants shape', () => {
      const mockConstants = {
        DEFAULT_STEPS: 20,
        DEFAULT_GUIDANCE_SCALE: 7.5,
        DEFAULT_WIDTH: 512,
        DEFAULT_HEIGHT: 512,
        SUPPORTED_WIDTHS: [512, 768, 1024],
        SUPPORTED_HEIGHTS: [512, 768, 1024],
      };
      (mockLocalDreamModule.getConstants as jest.Mock).mockReturnValue(mockConstants);

      const constants = mockLocalDreamModule.getConstants();

      expect(constants).toHaveProperty('DEFAULT_STEPS');
      expect(constants).toHaveProperty('DEFAULT_GUIDANCE_SCALE');
      expect(constants).toHaveProperty('DEFAULT_WIDTH');
      expect(constants).toHaveProperty('DEFAULT_HEIGHT');
      expect(constants).toHaveProperty('SUPPORTED_WIDTHS');
      expect(constants).toHaveProperty('SUPPORTED_HEIGHTS');
      expect(typeof constants.DEFAULT_STEPS).toBe('number');
      expect(Array.isArray(constants.SUPPORTED_WIDTHS)).toBe(true);
    });
  });

  describe('getServerPort', () => {
    it('should return port number', async () => {
      (mockLocalDreamModule.getServerPort as jest.Mock).mockResolvedValue(18081);

      const result = await mockLocalDreamModule.getServerPort();

      expect(typeof result).toBe('number');
      expect(result).toBeGreaterThan(0);
    });
  });

  describe('isNpuSupported', () => {
    it('should return boolean for NPU support', async () => {
      (mockLocalDreamModule.isNpuSupported as jest.Mock).mockResolvedValue(true);

      const result = await mockLocalDreamModule.isNpuSupported();

      expect(typeof result).toBe('boolean');
    });
  });

  describe('Progress Events', () => {
    it('should define expected progress event shape', () => {
      // Document the expected progress event interface
      const progressEvent = {
        step: 10,
        totalSteps: 20,
        progress: 0.5,
        previewPath: '/path/to/preview.png',
      };

      expect(progressEvent).toHaveProperty('step');
      expect(progressEvent).toHaveProperty('totalSteps');
      expect(progressEvent).toHaveProperty('progress');
      expect(typeof progressEvent.step).toBe('number');
      expect(typeof progressEvent.totalSteps).toBe('number');
      expect(typeof progressEvent.progress).toBe('number');
      expect(progressEvent.progress).toBeGreaterThanOrEqual(0);
      expect(progressEvent.progress).toBeLessThanOrEqual(1);
    });

    it('should define expected error event shape', () => {
      // Document the expected error event interface
      const errorEvent = {
        error: 'Out of memory during generation',
      };

      expect(errorEvent).toHaveProperty('error');
      expect(typeof errorEvent.error).toBe('string');
    });

    it('should support optional preview path in progress events', () => {
      const progressWithPreview = {
        step: 15,
        totalSteps: 20,
        progress: 0.75,
        previewPath: '/data/user/0/ai.offgridmobile/files/previews/step-15.png',
      };

      const progressWithoutPreview = {
        step: 5,
        totalSteps: 20,
        progress: 0.25,
      };

      expect(progressWithPreview.previewPath).toBeDefined();
      expect(progressWithoutPreview).not.toHaveProperty('previewPath');
    });
  });

  describe('Error handling', () => {
    it('should reject on model load failure', async () => {
      (mockLocalDreamModule.loadModel as jest.Mock).mockRejectedValue(
        new Error('Failed to load model: invalid format')
      );

      await expect(mockLocalDreamModule.loadModel({
        modelPath: '/invalid/model',
        backend: 'auto',
      })).rejects.toThrow('Failed to load model');
    });

    it('should reject on generation failure', async () => {
      (mockLocalDreamModule.generateImage as jest.Mock).mockRejectedValue(
        new Error('Generation failed: out of memory')
      );

      await expect(mockLocalDreamModule.generateImage({
        prompt: 'test',
      })).rejects.toThrow('Generation failed');
    });

    it('should handle server not running', async () => {
      (mockLocalDreamModule.generateImage as jest.Mock).mockRejectedValue(
        new Error('Server not running')
      );

      await expect(mockLocalDreamModule.generateImage({
        prompt: 'test',
      })).rejects.toThrow('Server not running');
    });
  });
});


================================================
FILE: __tests__/contracts/ragEmbedding.contract.test.ts
================================================
/**
 * RAG Embedding Contract Tests
 *
 * Documents and verifies the expected interface between our embedding service
 * and the llama.rn native module's embedding API. Also documents the vector
 * storage format and search contract.
 *
 * These tests use mocks — they verify interface compatibility and expected
 * data shapes, not actual native functionality.
 */

describe('RAG Embedding Contract', () => {
  // ============================================================================
  // initLlama Embedding Mode Contract
  // ============================================================================
  describe('initLlama embedding mode', () => {
    it('requires embedding: true to enable embedding mode', () => {
      const embeddingParams = {
        model: '/path/to/embedding-model.gguf',
        embedding: true,
        n_gpu_layers: 0,
        n_ctx: 512,
      };

      expect(embeddingParams.embedding).toBe(true);
      expect(embeddingParams.n_gpu_layers).toBe(0); // CPU-only to avoid GPU contention
    });

    it('uses small context size for embedding models', () => {
      const params = {
        model: '/path/to/model.gguf',
        embedding: true,
        n_ctx: 512,
        n_batch: 512,
        n_threads: 2,
      };

      // Embedding models need small context — input is one chunk at a time
      expect(params.n_ctx).toBeLessThanOrEqual(512);
      expect(params.n_batch).toBeLessThanOrEqual(512);
      // Use fewer threads than main LLM to reduce contention
      expect(params.n_threads).toBeLessThan(4);
    });

    it('runs on CPU only to avoid GPU contention with main LLM', () => {
      const params = { n_gpu_layers: 0 };
      expect(params.n_gpu_layers).toBe(0);
    });
  });

  // ============================================================================
  // Embedding API Contract
  // ============================================================================
  describe('context.embedding() interface', () => {
    it('accepts a string and returns embedding vector', () => {
      // Expected call signature
      const mockEmbedding = jest.fn().mockResolvedValue({
        embedding: new Array(384).fill(0.1),
      });

      const context = { embedding: mockEmbedding };

      expect(typeof context.embedding).toBe('function');
    });

    it('returns fixed-dimension vector for all-MiniLM-L6-v2', () => {
      // all-MiniLM-L6-v2 always produces 384-dimensional embeddings
      const expectedDimension = 384;
      const embedding = new Array(expectedDimension).fill(0);

      expect(embedding).toHaveLength(384);
    });

    it('embedding result has embedding property containing number array', () => {
      const result = {
        embedding: [0.1, -0.2, 0.3, 0.05],
      };

      expect(result).toHaveProperty('embedding');
      expect(Array.isArray(result.embedding)).toBe(true);
      result.embedding.forEach(val => {
        expect(typeof val).toBe('number');
        expect(Number.isFinite(val)).toBe(true);
      });
    });
  });

  // ============================================================================
  // Vector Storage Contract
  // ============================================================================
  describe('embedding storage format', () => {
    it('stores embeddings as Float32Array ArrayBuffer blobs', () => {
      const embedding = [0.1, 0.2, 0.3];
      const blob = new Float32Array(embedding).buffer;

      expect(blob.byteLength).toBe(embedding.length * 4); // 4 bytes per float32
    });

    it('can round-trip embeddings through Float32Array', () => {
      const original = [0.1, -0.5, 0.9, 0, -1];
      const blob = new Float32Array(original).buffer;
      const restored = Array.from(new Float32Array(blob));

      expect(restored).toHaveLength(original.length);
      original.forEach((val, i) => {
        expect(restored[i]).toBeCloseTo(val, 5);
      });
    });

    it('embedding blob for 384 dimensions is 1536 bytes', () => {
      const dimension = 384;
      const embedding = new Array(dimension).fill(0);
      const blob = new Float32Array(embedding).buffer;

      expect(blob.byteLength).toBe(1536); // 384 * 4 bytes
    });
  });

  // ============================================================================
  // Search Result Contract
  // ============================================================================
  describe('search result format', () => {
    it('RagSearchResult uses score instead of rank', () => {
      const result = {
        doc_id: 1,
        name: 'document.pdf',
        content: 'chunk text',
        position: 0,
        score: 0.85,
      };

      expect(result).toHaveProperty('score');
      expect(result.score).toBeGreaterThanOrEqual(-1);
      expect(result.score).toBeLessThanOrEqual(1);
    });

    it('cosine similarity score range is [-1, 1]', () => {
      // Identical vectors → 1.0
      // Orthogonal vectors → 0.0
      // Opposite vectors → -1.0
      const scores = [1, 0.85, 0.5, 0, -0.3, -1];
      scores.forEach(score => {
        expect(score).toBeGreaterThanOrEqual(-1);
        expect(score).toBeLessThanOrEqual(1);
      });
    });

    it('search results are sorted by descending score', () => {
      const results = [
        { score: 0.95 },
        { score: 0.8 },
        { score: 0.65 },
      ];

      for (let i = 1; i < results.length; i++) {
        expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
      }
    });
  });

  // ============================================================================
  // Model Asset Contract
  // ============================================================================
  describe('embedding model asset', () => {
    it('model filename follows expected convention', () => {
      const filename = 'all-MiniLM-L6-v2-Q8_0.gguf';

      expect(filename).toMatch(/\.gguf$/);
      expect(filename).toContain('MiniLM');
      expect(filename).toContain('Q8_0');
    });

    it('Android asset path follows models/ convention', () => {
      const assetPath = 'models/all-MiniLM-L6-v2-Q8_0.gguf';

      expect(assetPath).toMatch(/^models\//);
    });

    it('destination is DocumentDirectoryPath for both platforms', () => {
      // Both platforms copy to DocumentDirectoryPath at runtime
      const destPath = '/mock/documents/all-MiniLM-L6-v2-Q8_0.gguf';

      expect(destPath).toContain('all-MiniLM-L6-v2-Q8_0.gguf');
    });
  });

  // ============================================================================
  // IndexProgress Contract
  // ============================================================================
  describe('IndexProgress stages', () => {
    it('includes embedding stage in the pipeline', () => {
      const stages = ['extracting', 'chunking', 'indexing', 'embedding', 'done'];

      expect(stages).toContain('embedding');
      expect(stages.indexOf('embedding')).toBe(3);
      expect(stages.indexOf('done')).toBe(4);
    });

    it('embedding stage comes after indexing and before done', ()
Download .txt
gitextract_vym5rnyp/

├── .bundle/
│   └── config
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   └── workflows/
│       ├── ci.yml
│       ├── pages.yml
│       ├── release-ios.yml
│       └── release.yml
├── .gitignore
├── .husky/
│   └── pre-push
├── .maestro/
│   ├── E2E_TESTING.md
│   ├── config.yaml
│   ├── flows/
│   │   ├── p0/
│   │   │   ├── 00-setup-model.yaml
│   │   │   ├── 01-app-launch.yaml
│   │   │   ├── 01a-onboarding-first-launch.yaml
│   │   │   ├── 01b-onboarding-skip.yaml
│   │   │   ├── 01c-model-download-first-time.yaml
│   │   │   ├── 01d-second-launch-no-onboarding.yaml
│   │   │   ├── 01e-tab-navigation.yaml
│   │   │   ├── 02-text-generation.yaml
│   │   │   ├── 03-stop-generation.yaml
│   │   │   └── 04-image-generation.yaml
│   │   ├── p1/
│   │   │   ├── 06a-document-attachment.yaml
│   │   │   ├── 06b-image-attachment.yaml
│   │   │   ├── 06c-text-generation-full.yaml
│   │   │   └── 06d-text-generation-retry.yaml
│   │   ├── p2/
│   │   │   ├── 05a-model-uninstall.yaml
│   │   │   ├── 05b-model-download.yaml
│   │   │   ├── 05b-model-selection.yaml
│   │   │   └── 05c-model-unload.yaml
│   │   └── p3/
│   │       ├── 07a-image-model-uninstall.yaml
│   │       ├── 07b-image-model-download.yaml
│   │       └── 07c-image-model-set-active.yaml
│   └── utils/
│       └── wait-for-app-ready.yaml
├── .prettierrc.js
├── .swiftlint.yml
├── .vscode/
│   └── settings.json
├── .watchmanconfig
├── AGENTS.md
├── App.tsx
├── CLAUDE.md
├── Gemfile
├── LICENSE
├── README.md
├── TODO.md
├── __tests__/
│   ├── App.test.tsx
│   ├── contracts/
│   │   ├── coreMLDiffusion.contract.test.ts
│   │   ├── iosDownloadManager.contract.test.ts
│   │   ├── llama.rn.test.ts
│   │   ├── llamaContext.contract.test.ts
│   │   ├── localDream.contract.test.ts
│   │   ├── ragEmbedding.contract.test.ts
│   │   ├── whisper.contract.test.ts
│   │   └── whisper.rn.test.ts
│   ├── helpers/
│   │   ├── mockCustomAlert.tsx
│   │   └── mockNetworkDeps.ts
│   ├── integration/
│   │   ├── generation/
│   │   │   ├── generationFlow.test.ts
│   │   │   ├── imageGenerationFlow.test.ts
│   │   │   ├── remoteProviderRouting.test.ts
│   │   │   ├── sharePromptFlow.test.ts
│   │   │   └── unifiedModelSelection.test.ts
│   │   ├── models/
│   │   │   └── activeModelService.test.ts
│   │   ├── onboarding/
│   │   │   └── spotlightFlowIntegration.test.ts
│   │   ├── rag/
│   │   │   ├── embeddingFlow.test.ts
│   │   │   └── ragFlow.test.ts
│   │   └── stores/
│   │       ├── chatStoreIntegration.test.ts
│   │       └── remoteServerDiscovery.test.ts
│   ├── rntl/
│   │   ├── components/
│   │   │   ├── AnimatedEntry.test.tsx
│   │   │   ├── AnimatedListItem.test.tsx
│   │   │   ├── AnimatedPressable.test.tsx
│   │   │   ├── AppSheet.test.tsx
│   │   │   ├── Card.test.tsx
│   │   │   ├── ChatInput.test.tsx
│   │   │   ├── ChatMessage.test.tsx
│   │   │   ├── ChatMessageTools.test.tsx
│   │   │   ├── CustomAlert.test.tsx
│   │   │   ├── DebugSheet.test.tsx
│   │   │   ├── GenerationSettingsModal.test.tsx
│   │   │   ├── ImageFilterBar.test.tsx
│   │   │   ├── MarkdownText.test.tsx
│   │   │   ├── ModelCard.test.tsx
│   │   │   ├── ModelPickerSheet.test.tsx
│   │   │   ├── ModelSelectorModal.test.tsx
│   │   │   ├── ProjectSelectorSheet.test.tsx
│   │   │   ├── RemoteServerModal.test.tsx
│   │   │   ├── SharePromptSheet.test.tsx
│   │   │   ├── ToolPickerSheet.test.tsx
│   │   │   └── VoiceRecordButton.test.tsx
│   │   ├── hooks/
│   │   │   └── useFocusTrigger.test.ts
│   │   ├── navigation/
│   │   │   └── AppNavigator.test.tsx
│   │   ├── onboarding/
│   │   │   ├── ChatScreenSpotlight.test.tsx
│   │   │   ├── ChatsListScreenSpotlight.test.tsx
│   │   │   ├── HomeScreenSpotlight.test.tsx
│   │   │   ├── ModelSettingsScreenSpotlight.test.tsx
│   │   │   └── ProjectEditScreenSpotlight.test.tsx
│   │   └── screens/
│   │       ├── ChatScreen.test.tsx
│   │       ├── ChatsListScreen.test.tsx
│   │       ├── DeviceInfoScreen.test.tsx
│   │       ├── DocumentPreviewScreen.test.tsx
│   │       ├── DownloadManagerScreen.test.tsx
│   │       ├── GalleryScreen.test.tsx
│   │       ├── HomeScreen.test.tsx
│   │       ├── KnowledgeBaseScreen.test.tsx
│   │       ├── LockScreen.test.tsx
│   │       ├── ModelDownloadHelpers.test.tsx
│   │       ├── ModelDownloadScreen.test.tsx
│   │       ├── ModelSettingsScreen.test.tsx
│   │       ├── ModelsScreen.test.tsx
│   │       ├── OnboardingScreen.test.tsx
│   │       ├── PassphraseSetupScreen.test.tsx
│   │       ├── ProjectChatsScreen.test.tsx
│   │       ├── ProjectDetailScreen.test.tsx
│   │       ├── ProjectEditScreen.test.tsx
│   │       ├── ProjectsScreen.test.tsx
│   │       ├── RemoteServersScreen.test.tsx
│   │       ├── SecuritySettingsScreen.test.tsx
│   │       ├── SettingsScreen.test.tsx
│   │       ├── StorageSettingsScreen.test.tsx
│   │       └── VoiceSettingsScreen.test.tsx
│   ├── specs/
│   │   ├── image-generation.yaml
│   │   ├── model-lifecycle.yaml
│   │   └── text-generation.yaml
│   ├── unit/
│   │   ├── components/
│   │   │   └── ChatMessage/
│   │   │       └── utils.test.ts
│   │   ├── constants/
│   │   │   └── constants.test.ts
│   │   ├── hooks/
│   │   │   ├── useAppState.test.ts
│   │   │   ├── useChatGenerationActions.test.ts
│   │   │   ├── useChatModelActions.test.ts
│   │   │   ├── useHomeScreen.test.ts
│   │   │   ├── useImageGenerationSettings.test.ts
│   │   │   ├── useKeyboardAwarePopover.test.ts
│   │   │   ├── useModelLoading.test.ts
│   │   │   ├── useTextGenerationAdvanced.test.ts
│   │   │   ├── useVoiceRecording.test.ts
│   │   │   └── useWhisperTranscription.test.ts
│   │   ├── onboarding/
│   │   │   ├── chatScreenSpotlight.test.ts
│   │   │   ├── checklistComponents.test.tsx
│   │   │   ├── handleStepPress.test.ts
│   │   │   ├── onboardingFlows.test.ts
│   │   │   ├── reactiveSpotlightConditions.test.ts
│   │   │   └── spotlightTooltips.test.ts
│   │   ├── screens/
│   │   │   ├── ChatScreen/
│   │   │   │   ├── toolUsage.test.ts
│   │   │   │   └── useSaveImage.test.ts
│   │   │   ├── DownloadManagerScreen/
│   │   │   │   └── items.test.tsx
│   │   │   └── ModelsScreen/
│   │   │       ├── imageDownloadActions.test.ts
│   │   │       ├── importHelpers.test.ts
│   │   │       ├── restoreImageDownloads.test.ts
│   │   │       ├── trendingSelection.test.ts
│   │   │       ├── useModelsScreen.test.ts
│   │   │       ├── useTextModels.handlers.test.ts
│   │   │       └── utils.test.ts
│   │   ├── services/
│   │   │   ├── authService.test.ts
│   │   │   ├── backgroundDownloadService.test.ts
│   │   │   ├── contextCompaction.test.ts
│   │   │   ├── coreMLModelBrowser.test.ts
│   │   │   ├── documentService.test.ts
│   │   │   ├── downloadHelpers.test.ts
│   │   │   ├── generationService.test.ts
│   │   │   ├── generationToolLoop.test.ts
│   │   │   ├── hardware.test.ts
│   │   │   ├── httpClient.test.ts
│   │   │   ├── huggingFaceModelBrowser.test.ts
│   │   │   ├── huggingface.test.ts
│   │   │   ├── imageGenerationHelpers.test.ts
│   │   │   ├── imageGenerator.test.ts
│   │   │   ├── imageModelRecommendation.test.ts
│   │   │   ├── intentClassifier.test.ts
│   │   │   ├── llm.test.ts
│   │   │   ├── llmHelpers.test.ts
│   │   │   ├── llmMessages.test.ts
│   │   │   ├── llmSafetyChecks.test.ts
│   │   │   ├── llmToolGeneration.test.ts
│   │   │   ├── localDreamGenerator.test.ts
│   │   │   ├── modelManager/
│   │   │   │   └── imageSync.test.ts
│   │   │   ├── modelManager.test.ts
│   │   │   ├── networkDiscovery.test.ts
│   │   │   ├── parallelMmproj.test.ts
│   │   │   ├── pdfExtractor.test.ts
│   │   │   ├── providers/
│   │   │   │   ├── localProvider.test.ts
│   │   │   │   ├── openAICompatibleProvider.test.ts
│   │   │   │   └── registry.test.ts
│   │   │   ├── rag/
│   │   │   │   ├── chunking.test.ts
│   │   │   │   ├── database.test.ts
│   │   │   │   ├── embedding.test.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── retrieval.test.ts
│   │   │   │   └── vectorMath.test.ts
│   │   │   ├── remoteServerManager.test.ts
│   │   │   ├── restore.test.ts
│   │   │   ├── toolHandlers.test.ts
│   │   │   ├── tools/
│   │   │   │   ├── handlers.test.ts
│   │   │   │   └── registry.test.ts
│   │   │   ├── voiceService.test.ts
│   │   │   └── whisperService.test.ts
│   │   ├── stores/
│   │   │   ├── appStore.test.ts
│   │   │   ├── appStoreSharePrompt.test.ts
│   │   │   ├── authStore.test.ts
│   │   │   ├── chatStore.test.ts
│   │   │   ├── projectStore.test.ts
│   │   │   ├── remoteServerStore.test.ts
│   │   │   └── whisperStore.test.ts
│   │   ├── theme/
│   │   │   └── palettes.test.ts
│   │   └── utils/
│   │       ├── coreMLModelUtils.test.ts
│   │       ├── downloadErrors.test.ts
│   │       ├── generateId.test.ts
│   │       ├── messageContent.test.ts
│   │       ├── network.test.ts
│   │       ├── pickerErrorUtils.test.ts
│   │       ├── resolvePickedFileUri.test.ts
│   │       └── sharePrompt.test.ts
│   └── utils/
│       ├── factories.ts
│       ├── spotlightMocks.tsx
│       └── testHelpers.ts
├── altstore-source.json
├── android/
│   ├── app/
│   │   ├── build.gradle
│   │   ├── debug.keystore
│   │   ├── lint-baseline.xml
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       ├── debug/
│   │       │   └── res/
│   │       │       └── values/
│   │       │           └── strings.xml
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── assets/
│   │       │   │   ├── index.android.bundle
│   │       │   │   └── models/
│   │       │   │       └── all-MiniLM-L6-v2-Q8_0.gguf
│   │       │   ├── java/
│   │       │   │   └── ai/
│   │       │   │       └── offgridmobile/
│   │       │   │           ├── MainActivity.kt
│   │       │   │           ├── MainApplication.kt
│   │       │   │           ├── SafePromise.kt
│   │       │   │           ├── download/
│   │       │   │           │   ├── DownloadCompleteBroadcastReceiver.kt
│   │       │   │           │   ├── DownloadDao.kt
│   │       │   │           │   ├── DownloadDatabase.kt
│   │       │   │           │   ├── DownloadEntity.kt
│   │       │   │           │   ├── DownloadEventBridge.kt
│   │       │   │           │   ├── DownloadManagerModule.kt
│   │       │   │           │   ├── DownloadManagerPackage.kt
│   │       │   │           │   ├── DownloadUiState.kt
│   │       │   │           │   ├── WorkerDownload.kt
│   │       │   │           │   └── WorkerDownloadStore.kt
│   │       │   │           ├── localdream/
│   │       │   │           │   ├── LocalDreamModule.kt
│   │       │   │           │   └── LocalDreamPackage.kt
│   │       │   │           └── pdf/
│   │       │   │               ├── PDFExtractorModule.kt
│   │       │   │               └── PDFExtractorPackage.kt
│   │       │   └── res/
│   │       │       ├── drawable/
│   │       │       │   ├── rn_edit_text_material.xml
│   │       │       │   └── splash_background.xml
│   │       │       ├── mipmap-anydpi-v26/
│   │       │       │   ├── ic_launcher.xml
│   │       │       │   └── ic_launcher_round.xml
│   │       │       ├── raw/
│   │       │       │   └── keep.xml
│   │       │       ├── values/
│   │       │       │   ├── ic_launcher_background.xml
│   │       │       │   ├── strings.xml
│   │       │       │   └── styles.xml
│   │       │       └── xml/
│   │       │           └── network_security_config.xml
│   │       └── test/
│   │           └── java/
│   │               └── ai/
│   │                   └── offgridmobile/
│   │                       ├── download/
│   │                       │   ├── DownloadCompleteBroadcastReceiverTest.kt
│   │                       │   └── DownloadManagerModuleTest.kt
│   │                       ├── localdream/
│   │                       │   └── LocalDreamModuleTest.kt
│   │                       └── rag/
│   │                           └── EmbeddingModelAssetTest.kt
│   ├── build.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   └── settings.gradle
├── app.json
├── babel.config.js
├── codecov.yml
├── docs/
│   ├── ARCHITECTURE.md
│   ├── PERFORMANCE_IMPROVEMENTS.md
│   ├── PERSONAS_IMPLEMENTATION_PLAN.md
│   ├── PRIVACY_POLICY.md
│   ├── TTS_IMPLEMENTATION_PLAN.md
│   ├── brand_tone_voice.md
│   ├── design/
│   │   ├── DESIGN_PHILOSOPHY_SYSTEM.md
│   │   └── VISUAL_HIERARCHY_STANDARD.md
│   ├── image-gen-without-text-model.md
│   ├── onboarding/
│   │   └── ONBOARDING_FLOWS.md
│   ├── standards/
│   │   └── CODEBASE_GUIDE.md
│   └── tests/
│       ├── QA_TEST_PLAN.md
│       └── QA_TEST_PLAN_TODO.md
├── e2e/
│   ├── maestro/
│   │   ├── import_vision_model.yaml
│   │   └── models_screen_navigation.yaml
│   └── scripts/
│       └── seedSimulatorFiles.js
├── index.js
├── ios/
│   ├── .Podfile.swp
│   ├── .xcode.env
│   ├── CoreMLDiffusionModule.m
│   ├── CoreMLDiffusionModule.swift
│   ├── DownloadManagerModule.m
│   ├── DownloadManagerModule.swift
│   ├── ExportOptions.plist
│   ├── OffgridMobile/
│   │   ├── AppDelegate.swift
│   │   ├── CoreMLDiffusion/
│   │   │   └── CoreMLDiffusionModule.m
│   │   ├── Download/
│   │   │   └── DownloadManagerModule.m
│   │   ├── Images.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   ├── Contents.json
│   │   │   └── Logo.imageset/
│   │   │       └── Contents.json
│   │   ├── Info.plist
│   │   ├── LaunchScreen.storyboard
│   │   ├── OffgridMobile-Bridging-Header.h
│   │   ├── OffgridMobile.entitlements
│   │   └── PrivacyInfo.xcprivacy
│   ├── OffgridMobile.xcodeproj/
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace/
│   │   │   ├── contents.xcworkspacedata
│   │   │   └── xcshareddata/
│   │   │       └── swiftpm/
│   │   │           └── Package.resolved
│   │   └── xcshareddata/
│   │       └── xcschemes/
│   │           └── OffgridMobile.xcscheme
│   ├── OffgridMobile.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       └── swiftpm/
│   │           └── Package.resolved
│   ├── OffgridMobileTests/
│   │   ├── EmbeddingModelBundleTests.swift
│   │   └── OffgridMobileTests.swift
│   ├── PDFExtractorModule.m
│   ├── PDFExtractorModule.swift
│   ├── Podfile
│   └── all-MiniLM-L6-v2-Q8_0.gguf
├── jest.config.js
├── jest.setup.ts
├── metro.config.js
├── package.json
├── patches/
│   ├── @react-native-voice+voice+3.2.4.patch
│   ├── react-native-device-info+15.0.1.patch
│   └── react-native-zip-archive+7.1.0.patch
├── scripts/
│   ├── release.sh
│   ├── run-sonar.sh
│   └── run-tests.sh
├── sonar-project.properties
├── src/
│   ├── components/
│   │   ├── AdvancedToggle.tsx
│   │   ├── AnimatedEntry.tsx
│   │   ├── AnimatedListItem.tsx
│   │   ├── AnimatedPressable.tsx
│   │   ├── AppSheet.styles.ts
│   │   ├── AppSheet.tsx
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   ├── ChatInput/
│   │   │   ├── Attachments.tsx
│   │   │   ├── Popovers.tsx
│   │   │   ├── Toolbar.tsx
│   │   │   ├── Voice.ts
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   └── useKeyboardAwarePopover.ts
│   │   ├── ChatMessage/
│   │   │   ├── components/
│   │   │   │   ├── ActionMenuSheet.tsx
│   │   │   │   ├── BlinkingCursor.tsx
│   │   │   │   ├── GenerationMeta.tsx
│   │   │   │   ├── MessageAttachments.tsx
│   │   │   │   ├── MessageContent.tsx
│   │   │   │   └── ThinkingBlock.tsx
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── CustomAlert.tsx
│   │   ├── DebugLogsScreen/
│   │   │   ├── index.tsx
│   │   │   └── styles.ts
│   │   ├── DebugSheet.tsx
│   │   ├── GenerationSettingsModal/
│   │   │   ├── ConversationActionsSection.tsx
│   │   │   ├── ImageGenerationSection.tsx
│   │   │   ├── ImageQualitySliders.tsx
│   │   │   ├── TextGenerationAdvanced.tsx
│   │   │   ├── TextGenerationSection.tsx
│   │   │   ├── index.tsx
│   │   │   └── styles.ts
│   │   ├── MadeWithLove.tsx
│   │   ├── MarkdownText.tsx
│   │   ├── ModelCard.styles.ts
│   │   ├── ModelCard.tsx
│   │   ├── ModelCardContent.tsx
│   │   ├── ModelSelectorModal/
│   │   │   ├── ImageTab.tsx
│   │   │   ├── TextTab.tsx
│   │   │   ├── index.tsx
│   │   │   ├── remoteStyles.ts
│   │   │   └── styles.ts
│   │   ├── ProjectSelectorSheet.tsx
│   │   ├── RemoteServerModal/
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   └── useRemoteServerForm.ts
│   │   ├── SharePromptSheet.tsx
│   │   ├── ThinkingIndicator.tsx
│   │   ├── ToolPickerSheet.tsx
│   │   ├── VoiceRecordButton/
│   │   │   ├── index.tsx
│   │   │   ├── states.tsx
│   │   │   └── styles.ts
│   │   ├── checklist/
│   │   │   ├── ProgressBar.tsx
│   │   │   ├── animations.ts
│   │   │   ├── index.ts
│   │   │   ├── types.ts
│   │   │   └── useOnboardingSteps.ts
│   │   ├── index.ts
│   │   └── onboarding/
│   │       ├── OnboardingSheet.tsx
│   │       ├── PulsatingIcon.tsx
│   │       ├── index.ts
│   │       ├── spotlightConfig.tsx
│   │       ├── spotlightState.ts
│   │       └── useOnboardingSheet.ts
│   ├── constants/
│   │   ├── index.ts
│   │   └── models.ts
│   ├── hooks/
│   │   ├── useActiveTextModel.ts
│   │   ├── useAppState.ts
│   │   ├── useFocusTrigger.ts
│   │   ├── useImageGenerationSettings.ts
│   │   ├── useTextGenerationAdvanced.ts
│   │   ├── useVoiceRecording.ts
│   │   └── useWhisperTranscription.ts
│   ├── navigation/
│   │   ├── AppNavigator.tsx
│   │   ├── index.ts
│   │   └── types.ts
│   ├── screens/
│   │   ├── ChatScreen/
│   │   │   ├── ChatMessageArea.tsx
│   │   │   ├── ChatModalSection.tsx
│   │   │   ├── ChatScreenComponents.tsx
│   │   │   ├── MessageRenderer.tsx
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   ├── stylesImage.ts
│   │   │   ├── toolUsage.ts
│   │   │   ├── types.ts
│   │   │   ├── useChatGenerationActions.ts
│   │   │   ├── useChatMessageHandlers.ts
│   │   │   ├── useChatModelActions.ts
│   │   │   ├── useChatScreen.ts
│   │   │   └── useSaveImage.ts
│   │   ├── ChatsListScreen.tsx
│   │   ├── DeviceInfoScreen.tsx
│   │   ├── DocumentPreviewScreen.tsx
│   │   ├── DownloadManagerScreen/
│   │   │   ├── index.tsx
│   │   │   ├── items.tsx
│   │   │   ├── styles.ts
│   │   │   └── useDownloadManager.ts
│   │   ├── GalleryScreen/
│   │   │   ├── FullscreenViewer.tsx
│   │   │   ├── GridItem.tsx
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   └── useGalleryActions.ts
│   │   ├── HomeScreen/
│   │   │   ├── components/
│   │   │   │   ├── ActiveModelsSection.tsx
│   │   │   │   ├── LoadingOverlay.tsx
│   │   │   │   ├── ModelPickerSheet.tsx
│   │   │   │   └── RecentConversations.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── useHomeScreen.ts
│   │   │   │   ├── useHomeScreenSpotlight.ts
│   │   │   │   ├── useLANDiscovery.ts
│   │   │   │   ├── useModelLoading.ts
│   │   │   │   └── useRemoteModelHandlers.ts
│   │   │   ├── index.tsx
│   │   │   └── styles.ts
│   │   ├── KnowledgeBaseScreen.styles.ts
│   │   ├── KnowledgeBaseScreen.tsx
│   │   ├── LockScreen.tsx
│   │   ├── ModelDownloadHelpers.tsx
│   │   ├── ModelDownloadScreen.tsx
│   │   ├── ModelSettingsScreen/
│   │   │   ├── ImageGenerationSection.tsx
│   │   │   ├── SystemPromptSection.tsx
│   │   │   ├── TextGenerationAdvanced.tsx
│   │   │   ├── TextGenerationSection.tsx
│   │   │   ├── index.tsx
│   │   │   └── styles.ts
│   │   ├── ModelsScreen/
│   │   │   ├── ImageFilterBar.tsx
│   │   │   ├── ImageModelsTab.tsx
│   │   │   ├── TextFiltersSection.tsx
│   │   │   ├── TextModelsTab.tsx
│   │   │   ├── constants.ts
│   │   │   ├── imageDownloadActions.ts
│   │   │   ├── imageStyles.ts
│   │   │   ├── importHelpers.ts
│   │   │   ├── index.tsx
│   │   │   ├── styles.ts
│   │   │   ├── types.ts
│   │   │   ├── useImageModels.ts
│   │   │   ├── useModelsScreen.ts
│   │   │   ├── useTextModels.ts
│   │   │   └── utils.ts
│   │   ├── OnboardingScreen.tsx
│   │   ├── OrphanedFilesSection.tsx
│   │   ├── PassphraseSetupScreen.tsx
│   │   ├── ProjectChatsScreen.tsx
│   │   ├── ProjectDetailKnowledgeBaseSection.tsx
│   │   ├── ProjectDetailScreen.styles.ts
│   │   ├── ProjectDetailScreen.tsx
│   │   ├── ProjectEditScreen.tsx
│   │   ├── ProjectsScreen.tsx
│   │   ├── RemoteServersScreen.styles.ts
│   │   ├── RemoteServersScreen.tsx
│   │   ├── SecuritySettingsScreen.tsx
│   │   ├── SettingsScreen.tsx
│   │   ├── StorageSettingsScreen.styles.ts
│   │   ├── StorageSettingsScreen.tsx
│   │   ├── VoiceSettingsScreen.tsx
│   │   └── index.ts
│   ├── services/
│   │   ├── activeModelService/
│   │   │   ├── index.ts
│   │   │   ├── loaders.ts
│   │   │   ├── memory.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── authService.ts
│   │   ├── backgroundDownloadService.ts
│   │   ├── backgroundDownloadTypes.ts
│   │   ├── contextCompaction.ts
│   │   ├── coreMLModelBrowser.ts
│   │   ├── documentService.ts
│   │   ├── generationService.ts
│   │   ├── generationServiceHelpers.ts
│   │   ├── generationToolLoop.ts
│   │   ├── hardware.ts
│   │   ├── httpClient.ts
│   │   ├── httpClientSSE.ts
│   │   ├── httpClientUtils.ts
│   │   ├── huggingFaceModelBrowser.ts
│   │   ├── huggingface.ts
│   │   ├── imageGenerationHelpers.ts
│   │   ├── imageGenerationService.ts
│   │   ├── imageGenerator.ts
│   │   ├── index.ts
│   │   ├── intentClassifier.ts
│   │   ├── llm.ts
│   │   ├── llmHelpers.ts
│   │   ├── llmMessages.ts
│   │   ├── llmSafetyChecks.ts
│   │   ├── llmToolGeneration.ts
│   │   ├── llmTypes.ts
│   │   ├── localDreamGenerator.ts
│   │   ├── modelManager/
│   │   │   ├── download.ts
│   │   │   ├── downloadHelpers.ts
│   │   │   ├── imageSync.ts
│   │   │   ├── index.ts
│   │   │   ├── restore.ts
│   │   │   ├── scan.ts
│   │   │   ├── storage.ts
│   │   │   └── types.ts
│   │   ├── networkDiscovery.ts
│   │   ├── pdfExtractor.ts
│   │   ├── providers/
│   │   │   ├── index.ts
│   │   │   ├── localProvider.ts
│   │   │   ├── openAICompatibleProvider.ts
│   │   │   ├── openAICompatibleStream.ts
│   │   │   ├── openAICompatibleTypes.ts
│   │   │   ├── openAIMessageBuilder.ts
│   │   │   ├── registry.ts
│   │   │   └── types.ts
│   │   ├── rag/
│   │   │   ├── chunking.ts
│   │   │   ├── database.ts
│   │   │   ├── embedding.ts
│   │   │   ├── index.ts
│   │   │   ├── retrieval.ts
│   │   │   └── vectorMath.ts
│   │   ├── remoteServerManager.ts
│   │   ├── remoteServerManagerUtils.ts
│   │   ├── tools/
│   │   │   ├── handlers.ts
│   │   │   ├── index.ts
│   │   │   ├── registry.ts
│   │   │   └── types.ts
│   │   ├── voiceService.ts
│   │   └── whisperService.ts
│   ├── stores/
│   │   ├── appStore.ts
│   │   ├── authStore.ts
│   │   ├── chatStore.ts
│   │   ├── debugLogsStore.ts
│   │   ├── index.ts
│   │   ├── projectStore.ts
│   │   ├── remoteModelCapabilities.ts
│   │   ├── remoteServerHelpers.ts
│   │   ├── remoteServerStore.ts
│   │   └── whisperStore.ts
│   ├── theme/
│   │   ├── index.ts
│   │   ├── palettes.ts
│   │   └── useThemedStyles.ts
│   ├── types/
│   │   ├── global.d.ts
│   │   ├── index.ts
│   │   ├── remoteServer.ts
│   │   └── whisper.rn.d.ts
│   └── utils/
│       ├── coreMLModelUtils.ts
│       ├── downloadErrors.ts
│       ├── generateId.ts
│       ├── haptics.ts
│       ├── logger.ts
│       ├── messageContent.ts
│       ├── network.ts
│       ├── pickerErrorUtils.ts
│       ├── resolvePickedFileUri.ts
│       ├── sharePrompt.ts
│       └── visionRepair.ts
├── tsconfig.json
└── website/
    ├── CNAME
    ├── Gemfile
    ├── _config.yml
    ├── _layouts/
    │   └── default.html
    ├── assets/
    │   └── css/
    │       └── main.css
    ├── early-access.md
    ├── ethos.md
    ├── guides/
    │   ├── android-setup.md
    │   ├── document-analysis.md
    │   ├── index.md
    │   ├── ios-setup.md
    │   ├── knowledge-base.md
    │   ├── lm-studio-android.md
    │   ├── ollama-android.md
    │   ├── remote-servers.md
    │   ├── run-llms-locally-android.md
    │   ├── run-llms-locally-iphone.md
    │   ├── stable-diffusion-android.md
    │   ├── stable-diffusion-iphone.md
    │   ├── tool-calling.md
    │   ├── vision-ai.md
    │   ├── voice-stt.md
    │   └── which-model.md
    ├── index.md
    ├── llms.txt
    ├── mission.md
    ├── quick-start.md
    ├── robots.txt
    ├── vision.md
    └── writing/
        ├── 200-year-secretary.md
        ├── 7-principles-personal-ai-os.md
        ├── a-day-with-personal-ai-os.md
        ├── architecture-of-trust.md
        ├── case-against-ai-subscriptions.md
        ├── context-gap.md
        ├── cross-device-sync-without-server.md
        ├── end-of-app-switching.md
        ├── how-personal-ai-should-act.md
        ├── index.md
        ├── intelligence-should-be-personal.md
        ├── next-virtual-assistant.md
        ├── one-person-two-devices.md
        ├── personal-ai-os-for-knowledge-workers.md
        ├── personal-ai-os-vs-assistant-vs-agent.md
        ├── phone-is-the-most-important-device.md
        ├── phone-laptop-know-nothing.md
        ├── platform-intelligence-doesnt-exist.md
        ├── privacy-is-not-a-feature.md
        ├── regulatory-case-for-on-device-ai.md
        ├── the-small-things.md
        ├── two-devices-zero-context.md
        ├── va-industry-disruption.md
        ├── walled-garden-problem.md
        ├── what-is-personal-ai-os.md
        ├── what-personal-ai-should-know.md
        ├── whatsapp-moment-for-ai.md
        ├── who-owns-your-ai-memory.md
        └── why-personal-ai-should-never-live-in-cloud.md
Download .txt
SYMBOL INDEX (1574 symbols across 267 files)

FILE: App.tsx
  function App (line 30) | function App() {

FILE: __tests__/contracts/coreMLDiffusion.contract.test.ts
  type CoreMLDiffusionModuleInterface (line 10) | interface CoreMLDiffusionModuleInterface {

FILE: __tests__/contracts/iosDownloadManager.contract.test.ts
  type DownloadManagerModuleInterface (line 13) | interface DownloadManagerModuleInterface {

FILE: __tests__/contracts/localDream.contract.test.ts
  type LocalDreamModuleInterface (line 9) | interface LocalDreamModuleInterface {

FILE: __tests__/contracts/whisper.contract.test.ts
  type WhisperContextOptions (line 11) | interface WhisperContextOptions {
  type TranscribeOptions (line 16) | interface TranscribeOptions {
  type TranscribeRealtimeOptions (line 22) | interface TranscribeRealtimeOptions {
  type TranscribeResult (line 31) | interface TranscribeResult {
  type RealtimeTranscribeEvent (line 35) | interface RealtimeTranscribeEvent {
  type WhisperContext (line 42) | interface WhisperContext {

FILE: __tests__/integration/models/activeModelService.test.ts
  function expectLoadedSettings (line 34) | function expectLoadedSettings(expected: Record<string, unknown>) {
  function loadBothModelsWithSizes (line 353) | async function loadBothModelsWithSizes(textId: string, imageId: string) {
  function setupAndLoadBothModels (line 370) | async function setupAndLoadBothModels(textId = 'text-model', imageId = '...

FILE: __tests__/integration/stores/remoteServerDiscovery.test.ts
  function addServer (line 37) | function addServer(opts: {
  function jsonResponse (line 59) | function jsonResponse(body: unknown, ok = true, status = 200): Response {
  function rejectWith (line 68) | function rejectWith(msg: string): Promise<never> {

FILE: __tests__/rntl/components/AppSheet.test.tsx
  function getHandleContainer (line 645) | function getHandleContainer(getAllByType: (type: any) => any[]) {
  function makeTouchEvent (line 657) | function makeTouchEvent(pageY: number, previousY?: number, timestamp = D...

FILE: __tests__/rntl/components/ChatInput.test.tsx
  method pick (line 28) | get pick() { return mockPick; }
  method isErrorWithCode (line 29) | get isErrorWithCode() { return mockIsErrorWithCode; }
  method isSupported (line 46) | get isSupported() { return mockIsSupported; }
  method processDocumentFromPath (line 47) | get processDocumentFromPath() { return mockProcessDocument; }

FILE: __tests__/rntl/components/ChatMessageTools.test.tsx
  function renderToolResult (line 26) | function renderToolResult(toolName: string | undefined, content: string,...

FILE: __tests__/rntl/components/RemoteServerModal.test.tsx
  function createMockServer (line 102) | function createMockServer(overrides: Partial<any> = {}) {
  constant VALID_ENDPOINT (line 114) | const VALID_ENDPOINT = 'http://192.168.1.50:11434';
  function fillValidForm (line 243) | function fillValidForm(getByPlaceholderText: any) {
  function connectAndEnableSave (line 312) | async function connectAndEnableSave(getByText: any, getByPlaceholderText...
  function connectForEdit (line 360) | async function connectForEdit(getByText: any) {
  function setupPublicEndpointWithTest (line 408) | async function setupPublicEndpointWithTest(getByText: any, getByPlacehol...

FILE: __tests__/rntl/components/SharePromptSheet.test.tsx
  function renderSheet (line 17) | function renderSheet(onClose = jest.fn()) {

FILE: __tests__/rntl/onboarding/ChatScreenSpotlight.test.tsx
  method current (line 33) | get current() { return mockCurrent; }
  function renderChatScreen (line 169) | function renderChatScreen() {

FILE: __tests__/rntl/onboarding/ChatsListScreenSpotlight.test.tsx
  function renderScreen (line 57) | function renderScreen() {

FILE: __tests__/rntl/onboarding/HomeScreenSpotlight.test.tsx
  function renderHomeScreen (line 172) | function renderHomeScreen() {

FILE: __tests__/rntl/onboarding/ModelSettingsScreenSpotlight.test.tsx
  function renderScreen (line 39) | function renderScreen() {

FILE: __tests__/rntl/onboarding/ProjectEditScreenSpotlight.test.tsx
  function renderScreen (line 39) | function renderScreen() {

FILE: __tests__/rntl/screens/ChatScreen.test.tsx
  function setupTwoModelChat (line 1429) | function setupTwoModelChat() {
  function setupChatWithAssistantImage (line 1589) | function setupChatWithAssistantImage() {
  function setupRetryEditChat (line 1702) | function setupRetryEditChat(userMsgText: string, assistantMsgText: strin...
  function setupImageViewerChat (line 1771) | function setupImageViewerChat() {

FILE: __tests__/rntl/screens/ModelDownloadScreen.test.tsx
  constant MOCK_FILE (line 220) | const MOCK_FILE = {
  function flushPromises (line 235) | async function flushPromises(count = 10) {
  function setupDownloadCompletion (line 379) | async function setupDownloadCompletion() {

FILE: __tests__/rntl/screens/ModelsScreen.test.tsx
  constant VISION_PIPELINE_TAG (line 19) | const VISION_PIPELINE_TAG = 'image-text-to-text';
  constant CODE_FALLBACK_QUERY (line 20) | const CODE_FALLBACK_QUERY = 'coder';

FILE: __tests__/rntl/screens/RemoteServersScreen.test.tsx
  function createMockServer (line 105) | function createMockServer(overrides: Partial<any> = {}) {

FILE: __tests__/unit/hooks/useChatGenerationActions.test.ts
  function makeRef (line 183) | function makeRef<T>(value: T): React.MutableRefObject<T> {
  function makeGenerationDeps (line 190) | function makeGenerationDeps(overrides: Record<string, unknown> = {}): any {

FILE: __tests__/unit/hooks/useChatModelActions.test.ts
  function makeRef (line 82) | function makeRef<T>(value: T): React.MutableRefObject<T> {
  function makeDeps (line 86) | function makeDeps(overrides: Partial<any> = {}) {

FILE: __tests__/unit/hooks/useKeyboardAwarePopover.test.ts
  function showPopoverWithKeyboard (line 59) | function showPopoverWithKeyboard() {

FILE: __tests__/unit/hooks/useModelLoading.test.ts
  function makeTextModel (line 49) | function makeTextModel(overrides: Partial<any> = {}): any {
  function makeImageModel (line 53) | function makeImageModel(overrides: Partial<any> = {}): any {
  function makeSetters (line 57) | function makeSetters() {

FILE: __tests__/unit/onboarding/chatScreenSpotlight.test.ts
  class ChatScreenSpotlightSimulator (line 36) | class ChatScreenSpotlightSimulator {
    method goTo (line 43) | private goTo(step: number) {
    method simulateMount (line 48) | simulateMount() {
    method simulateTourStop (line 64) | simulateTourStop() {
    method simulateImageDrawCheck (line 84) | simulateImageDrawCheck(imageModelLoaded: boolean) {
    method simulateImageSettingsCheck (line 98) | simulateImageSettingsCheck() {
  function getAttachStepConfig (line 112) | function getAttachStepConfig(spotlight: number | null) {

FILE: __tests__/unit/onboarding/handleStepPress.test.ts
  type ImageState (line 33) | interface ImageState {
  constant DEFAULT_IMAGE_STATE (line 39) | const DEFAULT_IMAGE_STATE: ImageState = {
  constant PENDING_MAP (line 50) | const PENDING_MAP: Record<string, number> = {
  function simulateHandleStepPress (line 62) | function simulateHandleStepPress(

FILE: __tests__/unit/onboarding/reactiveSpotlightConditions.test.ts
  function shouldShowImageLoad (line 33) | function shouldShowImageLoad(): boolean {
  function shouldShowImageNewChat (line 80) | function shouldShowImageNewChat(): boolean {
  function shouldShowImageDraw (line 120) | function shouldShowImageDraw(imageModelLoaded: boolean): boolean {
  function shouldShowImageSettings (line 157) | function shouldShowImageSettings(): boolean {

FILE: __tests__/unit/screens/ModelsScreen/imageDownloadActions.test.ts
  function makeDeps (line 80) | function makeDeps(overrides: Partial<ImageDownloadDeps> = {}): ImageDown...
  function makeHFModelInfo (line 100) | function makeHFModelInfo(overrides: Partial<ImageModelDescriptor> = {}):...
  function makeZipModelInfo (line 118) | function makeZipModelInfo(overrides: Partial<ImageModelDescriptor> = {})...
  function makeCoreMLModelInfo (line 131) | function makeCoreMLModelInfo(overrides: Partial<ImageModelDescriptor> = ...

FILE: __tests__/unit/screens/ModelsScreen/restoreImageDownloads.test.ts
  function makeDownload (line 131) | function makeDownload(overrides: Partial<BackgroundDownloadInfo> = {}): ...
  function makeMetadata (line 144) | function makeMetadata(overrides: Partial<PersistedDownloadInfo> = {}): P...
  function renderUseImageModels (line 161) | function renderUseImageModels() {

FILE: __tests__/unit/services/contextCompaction.test.ts
  function mockTokenCounts (line 38) | function mockTokenCounts(nonSystemTokens = 500) {
  function compactWith (line 45) | function compactWith(messages: Message[], extra?: { previousSummary?: st...

FILE: __tests__/unit/services/coreMLModelBrowser.test.ts
  function setupSuccessfulFetch (line 81) | function setupSuccessfulFetch(_repo?: string) {
  function setupFailingFetch (line 114) | function setupFailingFetch() {

FILE: __tests__/unit/services/downloadHelpers.test.ts
  constant MODELS_DIR (line 18) | const MODELS_DIR = '/mock/documents/models';
  constant IMAGE_MODELS_DIR (line 19) | const IMAGE_MODELS_DIR = '/mock/documents/image_models';
  function makeDownloadedModel (line 25) | function makeDownloadedModel(overrides: Partial<DownloadedModel> = {}): ...
  function makeImageModel (line 39) | function makeImageModel(overrides: Partial<ONNXImageModel> = {}): ONNXIm...
  function makeRNFSFile (line 51) | function makeRNFSFile(name: string, path: string, size: number | string ...
  function makeRNFSDir (line 55) | function makeRNFSDir(name: string, path: string) {

FILE: __tests__/unit/services/generationToolLoop.test.ts
  function makeMessage (line 78) | function makeMessage(overrides: Partial<Message> = {}): Message {
  function makeToolCall (line 82) | function makeToolCall(overrides: Partial<ToolCall> = {}): ToolCall {
  function makeToolResult (line 91) | function makeToolResult(overrides: Partial<ToolResult> = {}): ToolResult {
  function createContext (line 101) | function createContext(overrides: Partial<ToolLoopContext> = {}): ToolLo...
  function createStreamingContext (line 977) | function createStreamingContext(overrides: Partial<ToolLoopContext> = {}...

FILE: __tests__/unit/services/httpClient.test.ts
  function parseSSEData (line 32) | async function parseSSEData(...chunks: string[]): Promise<{ events: any[...
  function mockFileReaderSuccess (line 481) | function mockFileReaderSuccess(result = 'data:image/png;base64,encoded') {
  function mockFileReaderError (line 498) | function mockFileReaderError() {
  function startStream (line 818) | function startStream(headers: Record<string, string> = {}): Promise<void> {
  function simulateProgress (line 823) | function simulateProgress(responseText: string) {
  function simulateComplete (line 831) | function simulateComplete(responseText: string) {

FILE: __tests__/unit/services/huggingFaceModelBrowser.test.ts
  function treeEntry (line 15) | function treeEntry(
  function mockFetchResponses (line 35) | function mockFetchResponses(...responses: { ok: boolean; body?: unknown ...

FILE: __tests__/unit/services/imageModelRecommendation.test.ts
  type TestImageModel (line 12) | interface TestImageModel {
  function isRecommendedModel (line 20) | function isRecommendedModel(model: TestImageModel, imageRec: ImageModelR...
  constant COREML_MODELS (line 36) | const COREML_MODELS: TestImageModel[] = [
  constant QNN_MODELS (line 70) | const QNN_MODELS: TestImageModel[] = [
  constant MNN_MODELS (line 77) | const MNN_MODELS: TestImageModel[] = [

FILE: __tests__/unit/services/llm.test.ts
  function setupScalingTest (line 23) | function setupScalingTest({
  method chatTemplates (line 2342) | get chatTemplates() { throw new Error('boom'); }

FILE: __tests__/unit/services/llmHelpers.test.ts
  method model (line 143) | get model() { throw new Error('boom'); }
  method model (line 172) | get model() { throw new Error('boom'); }
  function makeMsg (line 197) | function makeMsg(content: string): any {

FILE: __tests__/unit/services/llmToolGeneration.test.ts
  function createMockDeps (line 24) | function createMockDeps(overrides: Partial<ToolGenerationDeps> = {}): To...
  constant SAMPLE_TOOLS (line 50) | const SAMPLE_TOOLS = [

FILE: __tests__/unit/services/modelManager.test.ts
  constant MODELS_STORAGE_KEY (line 49) | const MODELS_STORAGE_KEY = '@local_llm/downloaded_models';

FILE: __tests__/unit/services/modelManager/imageSync.test.ts
  function makeOpts (line 52) | function makeOpts(overrides: Partial<typeof baseOpts> = {}) {

FILE: __tests__/unit/services/parallelMmproj.test.ts
  constant MODELS_DIR (line 54) | const MODELS_DIR = '/mock/documents/models';
  function visionFile (line 57) | function visionFile(mainSize = 4_000_000_000, mmProjSize = 500_000_000) {
  function stubStartDownload (line 69) | function stubStartDownload(ids: number[]) {
  function captureCompleteCallbacks (line 83) | function captureCompleteCallbacks(): Record<number, (event: any) => Prom...
  function captureErrorCallbacks (line 93) | function captureErrorCallbacks(): Record<number, (event: any) => void> {
  function captureProgressCallbacks (line 103) | function captureProgressCallbacks(): Record<number, (event: any) => void> {
  function setupVisionDownload (line 327) | async function setupVisionDownload() {

FILE: __tests__/unit/services/providers/registry.test.ts
  function makeProvider (line 16) | function makeProvider(id: string) {

FILE: __tests__/unit/services/rag/database.test.ts
  function expectDeleteCascade (line 23) | function expectDeleteCascade() {

FILE: __tests__/unit/services/restore.test.ts
  constant MODELS_DIR (line 25) | const MODELS_DIR = '/mock/documents/models';
  function makePersistedInfo (line 27) | function makePersistedInfo(overrides: Partial<PersistedDownloadInfo> = {...
  function makeActiveDownload (line 38) | function makeActiveDownload(overrides: Partial<{
  function callRestore (line 64) | function callRestore(overrides: {

FILE: __tests__/unit/services/tools/handlers.test.ts
  function makeToolCall (line 34) | function makeToolCall(name: string, args: Record<string, any> = {}): Too...
  function runTool (line 39) | async function runTool(name: string, args: Record<string, any> = {}) {
  function buildBraveSearchHTML (line 48) | function buildBraveSearchHTML(

FILE: __tests__/unit/stores/authStore.test.ts
  constant MAX_FAILED_ATTEMPTS (line 12) | const MAX_FAILED_ATTEMPTS = 5;
  constant LOCKOUT_DURATION (line 13) | const LOCKOUT_DURATION = 5 * 60 * 1000;

FILE: __tests__/unit/stores/remoteServerStore.test.ts
  function addTestServer (line 24) | function addTestServer(name = 'Test Server', endpoint = 'http://test:114...
  function addServerWithModel (line 36) | function addServerWithModel(modelId = 'model1', modelName = 'Model 1'): ...
  function discoverWithModels (line 806) | async function discoverWithModels(modelIds: string[]) {

FILE: __tests__/unit/utils/coreMLModelUtils.test.ts
  type MockReadDirItem (line 24) | interface MockReadDirItem {
  function makeFileItem (line 32) | function makeFileItem(name: string, parentPath: string, size = 1000): Mo...
  function makeDirItem (line 42) | function makeDirItem(name: string, parentPath: string): MockReadDirItem {

FILE: __tests__/utils/factories.ts
  type MessageFactoryOptions (line 43) | interface MessageFactoryOptions {
  type ConversationFactoryOptions (line 91) | interface ConversationFactoryOptions {
  type DownloadedModelFactoryOptions (line 130) | interface DownloadedModelFactoryOptions {
  type ModelFileFactoryOptions (line 176) | interface ModelFileFactoryOptions {
  type ModelInfoFactoryOptions (line 190) | interface ModelInfoFactoryOptions {
  type DeviceInfoFactoryOptions (line 220) | interface DeviceInfoFactoryOptions {
  type ModelRecommendationFactoryOptions (line 258) | interface ModelRecommendationFactoryOptions {
  type ONNXImageModelFactoryOptions (line 276) | interface ONNXImageModelFactoryOptions {
  type GeneratedImageFactoryOptions (line 304) | interface GeneratedImageFactoryOptions {
  type MediaAttachmentFactoryOptions (line 336) | interface MediaAttachmentFactoryOptions {
  type GenerationMetaFactoryOptions (line 377) | interface GenerationMetaFactoryOptions {
  type ProjectFactoryOptions (line 409) | interface ProjectFactoryOptions {

FILE: __tests__/utils/spotlightMocks.tsx
  function createSpotlightTourMock (line 24) | function createSpotlightTourMock() {
  function createNavigationMock (line 43) | function createNavigationMock(extras?: Record<string, any>) {
  function createCustomAlertMock (line 58) | function createCustomAlertMock() {
  function createAnimatedEntryMock (line 68) | function createAnimatedEntryMock() {
  function createAnimatedPressableMock (line 72) | function createAnimatedPressableMock() {
  function createAnimatedListItemMock (line 85) | function createAnimatedListItemMock() {
  function createHardwareServiceMock (line 99) | function createHardwareServiceMock() {
  function createModelManagerMock (line 112) | function createModelManagerMock() {
  function clearSpotlightMocks (line 124) | function clearSpotlightMocks() {

FILE: e2e/scripts/seedSimulatorFiles.js
  function makeMinimalGguf (line 82) | function makeMinimalGguf() {

FILE: jest.setup.ts
  function makeGroupAnimation (line 436) | function makeGroupAnimation(animations: any[]) {

FILE: src/components/AdvancedToggle.tsx
  type AdvancedToggleProps (line 8) | interface AdvancedToggleProps {

FILE: src/components/AnimatedEntry.tsx
  type AnimatedEntryProps (line 11) | interface AnimatedEntryProps {
  function AnimatedEntry (line 24) | function AnimatedEntry({

FILE: src/components/AnimatedListItem.tsx
  type AnimatedListItemProps (line 7) | interface AnimatedListItemProps {
  function AnimatedListItem (line 37) | function AnimatedListItem({

FILE: src/components/AnimatedPressable.tsx
  type AnimatedPressableProps (line 17) | interface AnimatedPressableProps {
  function AnimatedPressable (line 33) | function AnimatedPressable({

FILE: src/components/AppSheet.tsx
  type AppSheetProps (line 21) | interface AppSheetProps {
  function resolveSnapPoint (line 35) | function resolveSnapPoint(snap: string | number): number {
  function createSheetPanResponder (line 43) | function createSheetPanResponder({

FILE: src/components/Button.tsx
  type ButtonProps (line 13) | interface ButtonProps {

FILE: src/components/Card.tsx
  type CardProps (line 11) | interface CardProps {

FILE: src/components/ChatInput/Attachments.tsx
  function useAttachments (line 18) | function useAttachments(setAlertState: (state: AlertState) => void) {
  type AttachmentPreviewProps (line 121) | interface AttachmentPreviewProps {

FILE: src/components/ChatInput/Popovers.tsx
  constant SHADOW_COLOR (line 12) | const SHADOW_COLOR = '#000';
  type QuickSettingsPopoverProps (line 59) | interface QuickSettingsPopoverProps {
  function getImageModeBadge (line 73) | function getImageModeBadge(mode: ImageModeState, colors: any) {
  function getToolsStyle (line 79) | function getToolsStyle(supported: boolean, count: number, colors: any) {
  type AttachPickerPopoverProps (line 178) | interface AttachPickerPopoverProps {

FILE: src/components/ChatInput/Toolbar.tsx
  type QueueRowProps (line 7) | interface QueueRowProps {

FILE: src/components/ChatInput/Voice.ts
  type UseVoiceInputParams (line 5) | interface UseVoiceInputParams {
  function useVoiceInput (line 10) | function useVoiceInput({ conversationId, onTranscript }: UseVoiceInputPa...

FILE: src/components/ChatInput/index.tsx
  type ChatInputProps (line 17) | interface ChatInputProps {
  constant IMAGE_MODE_CYCLE (line 40) | const IMAGE_MODE_CYCLE: ImageModeState[] = ['auto', 'force', 'disabled'];

FILE: src/components/ChatInput/styles.ts
  constant PILL_ICON_SIZE (line 5) | const PILL_ICON_SIZE = 32;
  constant NUM_PILL_ICONS (line 6) | const NUM_PILL_ICONS = 2;
  constant PILL_ICONS_WIDTH (line 7) | const PILL_ICONS_WIDTH = PILL_ICON_SIZE * NUM_PILL_ICONS;
  constant ANIM_DURATION_IN (line 8) | const ANIM_DURATION_IN = 180;
  constant ANIM_DURATION_OUT (line 9) | const ANIM_DURATION_OUT = 200;

FILE: src/components/ChatInput/useKeyboardAwarePopover.ts
  function useKeyboardAwarePopover (line 10) | function useKeyboardAwarePopover(offsetX: number = SPACING.md) {

FILE: src/components/ChatMessage/components/ActionMenuSheet.tsx
  type ActionMenuSheetProps (line 8) | interface ActionMenuSheetProps {
  function ActionMenuSheet (line 22) | function ActionMenuSheet({
  type EditSheetProps (line 97) | interface EditSheetProps {
  function EditSheet (line 108) | function EditSheet({

FILE: src/components/ChatMessage/components/BlinkingCursor.tsx
  function BlinkingCursor (line 13) | function BlinkingCursor() {

FILE: src/components/ChatMessage/components/GenerationMeta.tsx
  type GenerationMetaProps (line 6) | interface GenerationMetaProps {
  type MetaItem (line 11) | type MetaItem = { key: string; label: string; maxLines?: number };
  function formatOptionalMeta (line 13) | function formatOptionalMeta(meta: NonNullable<Message['generationMeta']>...
  function buildMetaItems (line 30) | function buildMetaItems(
  function GenerationMeta (line 42) | function GenerationMeta({ generationMeta, styles }: Readonly<GenerationM...

FILE: src/components/ChatMessage/components/MessageAttachments.tsx
  type FadeInImageProps (line 19) | interface FadeInImageProps {
  function FadeInImage (line 27) | function FadeInImage({ uri, imageStyle, testID, wrapperTestID, onPress }...
  function formatFileSize (line 57) | function formatFileSize(bytes: number): string {
  type MessageAttachmentsProps (line 63) | interface MessageAttachmentsProps {
  function MessageAttachments (line 71) | function MessageAttachments({

FILE: src/components/ChatMessage/components/MessageContent.tsx
  type MessageContentProps (line 9) | interface MessageContentProps {
  function MessageContent (line 20) | function MessageContent({

FILE: src/components/ChatMessage/components/ThinkingBlock.tsx
  type ThinkingBlockProps (line 6) | interface ThinkingBlockProps {
  function ThinkingBlock (line 13) | function ThinkingBlock({

FILE: src/components/ChatMessage/index.tsx
  function getToolIcon (line 20) | function getToolIcon(toolName?: string): string {
  function getToolLabel (line 30) | function getToolLabel(toolName?: string, content?: string): string {
  type ToolResultBubbleProps (line 45) | type ToolResultBubbleProps = {
  type MetaRowProps (line 130) | type MetaRowProps = {

FILE: src/components/ChatMessage/types.ts
  type ChatMessageProps (line 3) | interface ChatMessageProps {
  type ParsedContent (line 17) | interface ParsedContent {

FILE: src/components/ChatMessage/utils.ts
  function parseThinkingContent (line 12) | function parseThinkingContent(content: string): ParsedContent {
  function formatTime (line 128) | function formatTime(timestamp: number): string {
  function formatDuration (line 133) | function formatDuration(ms: number): string {
  function buildMessageData (line 146) | function buildMessageData(message: Message): { displayContent: string; p...

FILE: src/components/CustomAlert.tsx
  type AlertButton (line 13) | interface AlertButton {
  type CustomAlertProps (line 19) | interface CustomAlertProps {
  type AlertState (line 85) | interface AlertState {

FILE: src/components/DebugLogsScreen/index.tsx
  type DebugLogsScreenProps (line 21) | interface DebugLogsScreenProps {

FILE: src/components/DebugLogsScreen/styles.ts
  function createStyles (line 3) | function createStyles(_colors: ThemeColors, _shadows: ThemeShadows) {

FILE: src/components/DebugSheet.tsx
  type DebugSheetProps (line 13) | interface DebugSheetProps {

FILE: src/components/GenerationSettingsModal/ConversationActionsSection.tsx
  type ConversationActionsSectionProps (line 7) | interface ConversationActionsSectionProps {

FILE: src/components/GenerationSettingsModal/TextGenerationAdvanced.tsx
  type BackendOption (line 20) | type BackendOption = { id: InferenceBackend; label: string; desc: string };
  constant IOS_BACKENDS (line 22) | const IOS_BACKENDS: BackendOption[] = [
  constant ANDROID_BASE_BACKENDS (line 27) | const ANDROID_BASE_BACKENDS: BackendOption[] = [
  constant HTP_BACKEND (line 32) | const HTP_BACKEND: BackendOption = {

FILE: src/components/GenerationSettingsModal/TextGenerationSection.tsx
  type SettingConfig (line 17) | interface SettingConfig {
  constant DEFAULT_SETTINGS (line 28) | const DEFAULT_SETTINGS: Record<string, number> = {
  constant FALLBACK_MAX_CONTEXT (line 36) | const FALLBACK_MAX_CONTEXT = 32768;
  constant HIGH_CONTEXT_THRESHOLD (line 37) | const HIGH_CONTEXT_THRESHOLD = 8192;
  constant BASIC_KEYS (line 44) | const BASIC_KEYS = ['temperature', 'maxTokens', 'contextLength'];
  type SettingSliderProps (line 95) | interface SettingSliderProps {

FILE: src/components/GenerationSettingsModal/index.tsx
  constant DEFAULT_SETTINGS (line 13) | const DEFAULT_SETTINGS = {
  type GenerationSettingsModalProps (line 23) | interface GenerationSettingsModalProps {

FILE: src/components/MadeWithLove.tsx
  constant WEDNESDAY_URL (line 5) | const WEDNESDAY_URL = 'https://www.wednesday.is/?utm_source=off-grid-mob...
  constant TEXT_COLOR (line 21) | const TEXT_COLOR = '#8C8C8C';
  constant HEART_COLOR (line 22) | const HEART_COLOR = '#FF0000';

FILE: src/components/MarkdownText.tsx
  function preprocessMarkdown (line 13) | function preprocessMarkdown(text: string): string {
  function createLinkRule (line 22) | function createLinkRule(onPress: (url: string) => void) {
  type MarkdownTextProps (line 35) | interface MarkdownTextProps {
  function MarkdownText (line 40) | function MarkdownText({ children, dimmed }: MarkdownTextProps) {
  function createMarkdownStyles (line 62) | function createMarkdownStyles(colors: ThemeColors, dimmed?: boolean) {

FILE: src/components/ModelCard.tsx
  type ModelCardProps (line 15) | interface ModelCardProps {
  function resolveQuantInfo (line 49) | function resolveQuantInfo(file?: ModelFile, downloadedModel?: Downloaded...
  function resolveFileSize (line 54) | function resolveFileSize(file?: ModelFile, downloadedModel?: DownloadedM...
  function resolveCredibility (line 60) | function resolveCredibility(
  function formatNumber (line 208) | function formatNumber(num: number): string {
  function formatBytes (line 214) | function formatBytes(bytes: number): string {

FILE: src/components/ModelCardContent.tsx
  type CredibilityInfo (line 12) | interface CredibilityInfo {
  type CompactModelCardContentProps (line 19) | interface CompactModelCardContentProps {
  function formatNumber (line 34) | function formatNumber(num: number): string {
  type ModelType (line 40) | type ModelType = 'text' | 'vision' | 'code';
  function modelTypeLabel (line 42) | function modelTypeLabel(modelType: ModelType): string {
  function modelTypeBadgeStyle (line 48) | function modelTypeBadgeStyle(
  function modelTypeTextStyle (line 57) | function modelTypeTextStyle(
  type StandardModelCardContentProps (line 135) | interface StandardModelCardContentProps {
  type ModelInfoBadgesProps (line 194) | interface ModelInfoBadgesProps {
  type ModelCardActionsProps (line 273) | interface ModelCardActionsProps {
  constant HIT_SLOP (line 287) | const HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 };
  function ActionButton (line 289) | function ActionButton({ icon, color, haptic, onPress, disabled, testID, ...
  function DownloadedActions (line 306) | function DownloadedActions({ isActive, testID, colors, styles, onSelect,...

FILE: src/components/ModelSelectorModal/ImageTab.tsx
  type ImageTabProps (line 9) | interface ImageTabProps {

FILE: src/components/ModelSelectorModal/TextTab.tsx
  type TextTabProps (line 9) | interface TextTabProps {

FILE: src/components/ModelSelectorModal/index.tsx
  type TabType (line 21) | type TabType = 'text' | 'image';
  type ModelSelectorModalProps (line 23) | interface ModelSelectorModalProps {

FILE: src/components/ProjectSelectorSheet.tsx
  type ProjectSelectorSheetProps (line 14) | interface ProjectSelectorSheetProps {

FILE: src/components/RemoteServerModal/index.tsx
  type RemoteServerModalProps (line 24) | interface RemoteServerModalProps {
  type TestResultSectionProps (line 31) | interface TestResultSectionProps {

FILE: src/components/RemoteServerModal/styles.ts
  function createStyles (line 3) | function createStyles(colors: ThemeColors, _shadows: ThemeShadows) {

FILE: src/components/RemoteServerModal/useRemoteServerForm.ts
  type FormOptions (line 8) | interface FormOptions {
  function useRemoteServerForm (line 15) | function useRemoteServerForm({ server, visible, onSave, onClose }: FormO...

FILE: src/components/SharePromptSheet.tsx
  type SharePromptSheetProps (line 11) | interface SharePromptSheetProps {

FILE: src/components/ThinkingIndicator.tsx
  type ThinkingIndicatorProps (line 5) | interface ThinkingIndicatorProps {

FILE: src/components/ToolPickerSheet.tsx
  type ToolPickerSheetProps (line 10) | interface ToolPickerSheetProps {

FILE: src/components/VoiceRecordButton/index.tsx
  type VoiceRecordButtonProps (line 28) | interface VoiceRecordButtonProps {
  constant CANCEL_DISTANCE (line 42) | const CANCEL_DISTANCE = 80;
  type CallbacksRef (line 44) | type CallbacksRef = { onStartRecording: () => void; onStopRecording: () ...
  function buildPanResponder (line 46) | function buildPanResponder({

FILE: src/components/VoiceRecordButton/states.tsx
  type LoadingStateProps (line 9) | interface LoadingStateProps {
  type TranscribingStateProps (line 31) | interface TranscribingStateProps {
  type UnavailableButtonProps (line 53) | interface UnavailableButtonProps {
  type ButtonIconProps (line 80) | interface ButtonIconProps {

FILE: src/components/checklist/ProgressBar.tsx
  type ProgressBarProps (line 6) | interface ProgressBarProps {
  function ProgressBar (line 12) | function ProgressBar({ completed, total, theme }: ProgressBarProps) {

FILE: src/components/checklist/animations.ts
  type SpringConfig (line 4) | interface SpringConfig {
  function springTo (line 9) | function springTo(
  function useStaggeredEntrance (line 23) | function useStaggeredEntrance(
  function useCheckmark (line 59) | function useCheckmark(completed: boolean, spring: SpringConfig) {
  function useStrikethrough (line 95) | function useStrikethrough(completed: boolean) {
  function useProgressAnimation (line 110) | function useProgressAnimation(progress: number) {

FILE: src/components/checklist/types.ts
  type OnboardingStep (line 1) | interface OnboardingStep {
  type ChecklistTheme (line 10) | interface ChecklistTheme {

FILE: src/components/checklist/useOnboardingSteps.ts
  function useOnboardingSteps (line 9) | function useOnboardingSteps() {
  function useChecklistTheme (line 35) | function useChecklistTheme(): ChecklistTheme {
  function useAutoDismiss (line 65) | function useAutoDismiss(completedCount: number, totalCount: number) {

FILE: src/components/onboarding/OnboardingSheet.tsx
  type OnboardingSheetProps (line 18) | interface OnboardingSheetProps {
  type ChecklistRowProps (line 24) | interface ChecklistRowProps {

FILE: src/components/onboarding/PulsatingIcon.tsx
  type PulsatingIconProps (line 6) | interface PulsatingIconProps {

FILE: src/components/onboarding/spotlightConfig.tsx
  type TooltipProps (line 8) | interface TooltipProps {
  constant STEP_INDEX_MAP (line 29) | const STEP_INDEX_MAP: Record<string, number> = {
  constant CHAT_INPUT_STEP_INDEX (line 39) | const CHAT_INPUT_STEP_INDEX = 3;
  constant MODEL_SETTINGS_STEP_INDEX (line 42) | const MODEL_SETTINGS_STEP_INDEX = 6;
  constant PROJECT_EDIT_STEP_INDEX (line 45) | const PROJECT_EDIT_STEP_INDEX = 8;
  constant DOWNLOAD_FILE_STEP_INDEX (line 48) | const DOWNLOAD_FILE_STEP_INDEX = 9;
  constant DOWNLOAD_MANAGER_STEP_INDEX (line 51) | const DOWNLOAD_MANAGER_STEP_INDEX = 10;
  constant MODEL_PICKER_STEP_INDEX (line 54) | const MODEL_PICKER_STEP_INDEX = 11;
  constant VOICE_HINT_STEP_INDEX (line 57) | const VOICE_HINT_STEP_INDEX = 12;
  constant IMAGE_LOAD_STEP_INDEX (line 60) | const IMAGE_LOAD_STEP_INDEX = 13;
  constant IMAGE_NEW_CHAT_STEP_INDEX (line 63) | const IMAGE_NEW_CHAT_STEP_INDEX = 14;
  constant IMAGE_DRAW_STEP_INDEX (line 66) | const IMAGE_DRAW_STEP_INDEX = 15;
  constant IMAGE_SETTINGS_STEP_INDEX (line 69) | const IMAGE_SETTINGS_STEP_INDEX = 16;
  constant IMAGE_DOWNLOAD_STEP_INDEX (line 72) | const IMAGE_DOWNLOAD_STEP_INDEX = 17;
  constant STEP_TAB_MAP (line 74) | const STEP_TAB_MAP: Record<string, string> = {
  function createSpotlightSteps (line 83) | function createSpotlightSteps(): TourStep[] {

FILE: src/components/onboarding/spotlightState.ts
  function setPendingSpotlight (line 6) | function setPendingSpotlight(stepIndex: number | null) {
  function consumePendingSpotlight (line 10) | function consumePendingSpotlight(): number | null {
  function peekPendingSpotlight (line 16) | function peekPendingSpotlight(): number | null {

FILE: src/components/onboarding/useOnboardingSheet.ts
  function useOnboardingSheet (line 5) | function useOnboardingSheet() {

FILE: src/constants/index.ts
  constant HF_API (line 4) | const HF_API = {
  constant LMSTUDIO_AUTHORS (line 18) | const LMSTUDIO_AUTHORS = [
  constant OFFICIAL_MODEL_AUTHORS (line 24) | const OFFICIAL_MODEL_AUTHORS: Record<string, string> = {
  constant VERIFIED_QUANTIZERS (line 49) | const VERIFIED_QUANTIZERS: Record<string, string> = {
  constant CREDIBILITY_LABELS (line 62) | const CREDIBILITY_LABELS = {
  constant APP_CONFIG (line 86) | const APP_CONFIG = {
  constant ONBOARDING_SLIDES (line 102) | const ONBOARDING_SLIDES = [
  constant FONTS (line 130) | const FONTS = {
  constant TYPOGRAPHY (line 135) | const TYPOGRAPHY = {
  constant SPACING (line 204) | const SPACING = {

FILE: src/constants/models.ts
  constant MODEL_RECOMMENDATIONS (line 2) | const MODEL_RECOMMENDATIONS = {
  constant RECOMMENDED_MODELS (line 16) | const RECOMMENDED_MODELS = [
  constant TRENDING_FAMILIES (line 128) | const TRENDING_FAMILIES: Record<string, string[]> = {
  constant TRENDING_MODEL_IDS (line 133) | const TRENDING_MODEL_IDS = Object.values(TRENDING_FAMILIES).flat();
  constant MODEL_ORGS (line 136) | const MODEL_ORGS = [
  constant QUANTIZATION_INFO (line 148) | const QUANTIZATION_INFO: Record<string, {

FILE: src/hooks/useActiveTextModel.ts
  type ActiveTextModelResult (line 5) | type ActiveTextModelResult = {
  function useActiveTextModel (line 20) | function useActiveTextModel(): ActiveTextModelResult {

FILE: src/hooks/useAppState.ts
  type UseAppStateCallbacks (line 4) | interface UseAppStateCallbacks {

FILE: src/hooks/useFocusTrigger.ts
  function useFocusTrigger (line 9) | function useFocusTrigger(): number {

FILE: src/hooks/useImageGenerationSettings.ts
  function useClearGpuCache (line 6) | function useClearGpuCache() {

FILE: src/hooks/useTextGenerationAdvanced.ts
  constant HTP_ENABLED (line 8) | const HTP_ENABLED = false;
  constant CACHE_TYPE_DESCRIPTIONS (line 10) | const CACHE_TYPE_DESCRIPTIONS: Record<CacheType, string> = {
  constant GPU_LAYERS_MAX (line 16) | const GPU_LAYERS_MAX = 99;
  constant CACHE_TYPE_OPTIONS (line 17) | const CACHE_TYPE_OPTIONS: CacheType[] = ['f16', 'q8_0', 'q4_0'];
  function useTextGenerationAdvanced (line 19) | function useTextGenerationAdvanced() {

FILE: src/hooks/useVoiceRecording.ts
  type UseVoiceRecordingResult (line 5) | interface UseVoiceRecordingResult {

FILE: src/hooks/useWhisperTranscription.ts
  type UseWhisperTranscriptionResult (line 14) | interface UseWhisperTranscriptionResult {

FILE: src/navigation/AppNavigator.tsx
  constant TAB_ICON_MAP (line 50) | const TAB_ICON_MAP: Record<string, string> = {

FILE: src/navigation/types.ts
  type RootStackParamList (line 3) | type RootStackParamList = {
  type MainTabParamList (line 28) | type MainTabParamList = {

FILE: src/screens/ChatScreen/ChatMessageArea.tsx
  type ChatMessageAreaProps (line 17) | type ChatMessageAreaProps = {

FILE: src/screens/ChatScreen/ChatModalSection.tsx
  type StylesType (line 11) | type StylesType = ReturnType<typeof createStyles>;
  type ColorsType (line 12) | type ColorsType = ReturnType<typeof useTheme>['colors'];
  type ChatModalSectionProps (line 14) | type ChatModalSectionProps = {

FILE: src/screens/ChatScreen/ChatScreenComponents.tsx
  type StylesType (line 19) | type StylesType = ReturnType<typeof createStyles>;
  type ColorsType (line 20) | type ColorsType = ReturnType<typeof useTheme>['colors'];

FILE: src/screens/ChatScreen/MessageRenderer.tsx
  type MessageRendererProps (line 6) | type MessageRendererProps = {

FILE: src/screens/ChatScreen/index.tsx
  function countConversationImages (line 20) | function countConversationImages(conv: Conversation | undefined): number {

FILE: src/screens/ChatScreen/toolUsage.ts
  constant SIMPLE_CALC_CHARS (line 1) | const SIMPLE_CALC_CHARS = new Set([' ', '+', '-', '*', '/', '^', '%', '....
  function looksLikeSimpleMathExpression (line 3) | function looksLikeSimpleMathExpression(text: string): boolean {
  constant TOOL_TRIGGER_PATTERNS (line 15) | const TOOL_TRIGGER_PATTERNS = {
  function shouldUseToolsForMessage (line 30) | function shouldUseToolsForMessage(messageText: string, enabledTools: str...

FILE: src/screens/ChatScreen/types.ts
  type ChatMessageItem (line 3) | type ChatMessageItem = {
  type StreamingState (line 13) | type StreamingState = {
  function getDisplayMessages (line 20) | function getDisplayMessages(
  type PlaceholderTextOptions (line 40) | type PlaceholderTextOptions = {
  function getPlaceholderText (line 47) | function getPlaceholderText({

FILE: src/screens/ChatScreen/useChatGenerationActions.ts
  type SetState (line 25) | type SetState<T> = Dispatch<SetStateAction<T>>;
  constant FALLBACK_RECENT_MESSAGE_COUNT (line 26) | const FALLBACK_RECENT_MESSAGE_COUNT = 2;
  type GenerationDeps (line 27) | type GenerationDeps = {
  function applyCompactionPrefix (line 70) | function applyCompactionPrefix(conversation: any, systemPrompt: string, ...
  function appendAttachmentText (line 80) | function appendAttachmentText(text: string, attachments?: MediaAttachmen...
  function buildMessagesForContext (line 85) | function buildMessagesForContext(conversationId: string, messageText: st...
  function shouldRouteToImageGenerationFn (line 93) | async function shouldRouteToImageGenerationFn(
  type ImageGenCall (line 131) | type ImageGenCall = {
  function handleImageGenerationFn (line 136) | async function handleImageGenerationFn(
  type StartGenerationCall (line 153) | type StartGenerationCall = { setDebugInfo: SetState<any>; targetConversa...
  function ensureModelReady (line 154) | async function ensureModelReady(deps: GenerationDeps): Promise<boolean> {
  function prepareContext (line 161) | async function prepareContext(setDebugInfo: SetState<any>, systemPrompt:...
  function generateWithCompactionRetry (line 172) | async function generateWithCompactionRetry(
  function injectRagContext (line 195) | async function injectRagContext(projectId: string | undefined, query: st...
  function resolveToolsAndPrompt (line 221) | function resolveToolsAndPrompt(deps: GenerationDeps, conversation: any, ...
  function startGenerationFn (line 249) | async function startGenerationFn(deps: GenerationDeps, call: StartGenera...
  type SendCall (line 295) | type SendCall = { text: string; attachments?: MediaAttachment[]; imageMo...
  function handleSendFn (line 296) | async function handleSendFn(deps: GenerationDeps, call: SendCall): Promi...
  function handleStopFn (line 322) | async function handleStopFn(deps: Pick<GenerationDeps, 'isGeneratingImag...
  function executeDeleteConversationFn (line 328) | async function executeDeleteConversationFn(
  type RegenerateCall (line 340) | type RegenerateCall = { setDebugInfo: SetState<any>; userMessage: Messag...
  function regenerateResponseFn (line 341) | async function regenerateResponseFn(deps: GenerationDeps, call: Regenera...
  type SelectProjectDeps (line 374) | type SelectProjectDeps = { activeConversationId: string | null | undefin...
  function handleSelectProjectFn (line 375) | function handleSelectProjectFn(deps: SelectProjectDeps, project: Project...

FILE: src/screens/ChatScreen/useChatMessageHandlers.ts
  type SetState (line 9) | type SetState<T> = Dispatch<SetStateAction<T>>;
  type RetryParams (line 11) | type RetryParams = {
  function handleRetryMessageFn (line 19) | async function handleRetryMessageFn(
  type EditParams (line 38) | type EditParams = {
  function handleEditMessageFn (line 48) | async function handleEditMessageFn(genDeps: GenerationDeps, p: EditParam...
  function handleDeleteConversationFn (line 55) | function handleDeleteConversationFn(
  function handleGenerateImageFromMsgFn (line 70) | async function handleGenerateImageFromMsgFn(

FILE: src/screens/ChatScreen/useChatModelActions.ts
  type SetState (line 11) | type SetState<T> = Dispatch<SetStateAction<T>>;
  type ActiveModelInfo (line 13) | type ActiveModelInfo = {
  type ModelActionDeps (line 20) | type ModelActionDeps = {
  function waitForRenderFrame (line 42) | function waitForRenderFrame(): Promise<void> {
  function addSystemMsg (line 48) | function addSystemMsg(
  function doLoadTextModel (line 60) | async function doLoadTextModel(deps: ModelActionDeps): Promise<void> {
  function initiateModelLoad (line 80) | async function initiateModelLoad(
  function ensureModelLoadedFn (line 135) | async function ensureModelLoadedFn(
  function proceedWithModelLoadFn (line 152) | async function proceedWithModelLoadFn(
  function handleModelSelectFn (line 182) | async function handleModelSelectFn(
  function handleUnloadModelFn (line 224) | async function handleUnloadModelFn(deps: ModelActionDeps): Promise<void> {
  type ImageModelEffectsDeps (line 248) | type ImageModelEffectsDeps = {
  function useChatImageModelEffects (line 254) | function useChatImageModelEffects(deps: ImageModelEffectsDeps): void {
  type ModelStateSyncDeps (line 289) | type ModelStateSyncDeps = {
  function useChatModelStateSync (line 301) | function useChatModelStateSync(deps: ModelStateSyncDeps): void {

FILE: src/screens/ChatScreen/useChatScreen.ts
  type ChatScreenRouteProp (line 22) | type ChatScreenRouteProp = RouteProp<RootStackParamList, 'Chat'>;
  type ActiveModelInfo (line 24) | type ActiveModelInfo = {

FILE: src/screens/ChatScreen/useSaveImage.ts
  function saveImageToGallery (line 7) | async function saveImageToGallery(

FILE: src/screens/ChatsListScreen.tsx
  type NavigationProp (line 25) | type NavigationProp = CompositeNavigationProp<

FILE: src/screens/DocumentPreviewScreen.tsx
  type NavigationProp (line 21) | type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
  type RouteProps (line 22) | type RouteProps = RouteProp<RootStackParamList, 'DocumentPreview'>;

FILE: src/screens/DownloadManagerScreen/items.tsx
  type DownloadItem (line 14) | type DownloadItem = {
  type DownloadItemsData (line 34) | interface DownloadItemsData {
  function formatBytes (line 44) | function formatBytes(bytes: number): string {
  function extractQuantization (line 52) | function extractQuantization(fileName: string): string {
  function getStatusText (line 64) | function getStatusText(status: string): string {
  function buildDownloadItems (line 75) | function buildDownloadItems(data: DownloadItemsData): DownloadItem[] {
  function getStatusLabel (line 180) | function getStatusLabel(item: DownloadItem): string {
  type ActiveDownloadCardProps (line 190) | interface ActiveDownloadCardProps {
  type CompletedDownloadCardProps (line 284) | interface CompletedDownloadCardProps {

FILE: src/screens/DownloadManagerScreen/useDownloadManager.ts
  type UseDownloadManagerResult (line 19) | interface UseDownloadManagerResult {
  function isNetworkRetryReason (line 33) | function isNetworkRetryReason(reason?: string, reasonCode?: string): boo...
  function isMmProjSidecar (line 43) | function isMmProjSidecar(metadata: { fileName: string; mmProjFileName?: ...
  function purgeStaleImageDownloads (line 49) | async function purgeStaleImageDownloads(downloads: BackgroundDownloadInf...
  function clearStaleTextProgressEntries (line 72) | function clearStaleTextProgressEntries(
  function shouldSyncSnapshot (line 95) | function shouldSyncSnapshot(
  function updateActiveDownloadStatus (line 110) | function updateActiveDownloadStatus(
  type RetryProgressContext (line 126) | type RetryProgressContext = {
  function handleRetryingProgressEvent (line 132) | function handleRetryingProgressEvent(
  function syncDownloadSnapshot (line 162) | function syncDownloadSnapshot(
  function useDownloadManager (line 198) | function useDownloadManager(): UseDownloadManagerResult {

FILE: src/screens/GalleryScreen/FullscreenViewer.tsx
  type FullscreenViewerProps (line 16) | interface FullscreenViewerProps {

FILE: src/screens/GalleryScreen/GridItem.tsx
  type GalleryGridItemProps (line 9) | interface GalleryGridItemProps {

FILE: src/screens/GalleryScreen/index.tsx
  type GalleryScreenRouteProp (line 15) | type GalleryScreenRouteProp = RouteProp<RootStackParamList, 'Gallery'>;

FILE: src/screens/GalleryScreen/styles.ts
  constant COLUMN_COUNT (line 6) | const COLUMN_COUNT = 3;
  constant GRID_SPACING (line 7) | const GRID_SPACING = 4;
  constant CELL_SIZE (line 8) | const CELL_SIZE = (screenWidth - GRID_SPACING * (COLUMN_COUNT + 1)) / CO...

FILE: src/screens/HomeScreen/components/ActiveModelsSection.tsx
  function ModelLoadingState (line 11) | function ModelLoadingState({ loadingState, styles }: { loadingState: Loa...
  type ActiveTextModel (line 23) | type ActiveTextModel = DownloadedModel | RemoteModel | undefined;
  type ActiveImageModel (line 24) | type ActiveImageModel = ONNXImageModel | RemoteModel | undefined;
  function isDownloadedModel (line 27) | function isDownloadedModel(model: ActiveTextModel): model is DownloadedM...
  function isRemoteModel (line 31) | function isRemoteModel(model: ActiveTextModel | ActiveImageModel): model...
  function isOnnxImageModel (line 35) | function isOnnxImageModel(model: ActiveImageModel): model is ONNXImageMo...
  type TextModelCardProps (line 39) | type TextModelCardProps = {
  type ImageModelCardProps (line 108) | type ImageModelCardProps = {
  type Props (line 182) | type Props = {

FILE: src/screens/HomeScreen/components/LoadingOverlay.tsx
  function getLoadingTitle (line 44) | function getLoadingTitle(state: LoadingState): string {
  type Props (line 50) | type Props = {

FILE: src/screens/HomeScreen/components/ModelPickerSheet.tsx
  type Props (line 15) | type Props = {
  type ImageTabColors (line 40) | type ImageTabColors = ReturnType<typeof useTheme>['colors'];
  type ImageTabStyles (line 41) | type ImageTabStyles = ReturnType<typeof createStyles>;
  type ImageTabProps (line 42) | type ImageTabProps = Pick<Props, 'downloadedImageModels' | 'activeImageM...
  constant TRANSPARENT (line 304) | const TRANSPARENT = 'transparent' as const;

FILE: src/screens/HomeScreen/components/RecentConversations.tsx
  function formatDate (line 10) | function formatDate(dateStr: string): string {
  type Props (line 27) | type Props = {

FILE: src/screens/HomeScreen/hooks/useHomeScreen.ts
  type HomeScreenNavigationProp (line 17) | type HomeScreenNavigationProp = CompositeNavigationProp<
  type ModelPickerType (line 22) | type ModelPickerType = 'text' | 'image' | null;
  type LoadingState (line 24) | type LoadingState = {
  function deleteConversationWithAlert (line 34) | function deleteConversationWithAlert(

FILE: src/screens/HomeScreen/hooks/useHomeScreenSpotlight.ts
  type SpotlightProps (line 8) | interface SpotlightProps {
  function useHomeScreenSpotlight (line 15) | function useHomeScreenSpotlight({ navigation, closeSheet, activeImageMod...

FILE: src/screens/HomeScreen/hooks/useLANDiscovery.ts
  type LANDiscoveryParams (line 14) | interface LANDiscoveryParams {
  function updateMovedServer (line 19) | async function updateMovedServer(
  function useLANDiscovery (line 34) | function useLANDiscovery({ navigation, setAlertState }: LANDiscoveryPara...

FILE: src/screens/HomeScreen/hooks/useModelLoading.ts
  type Setters (line 8) | type Setters = {

FILE: src/screens/HomeScreen/hooks/useRemoteModelHandlers.ts
  type RemoteModelHandlersParams (line 8) | interface RemoteModelHandlersParams {
  function useRemoteModelHandlers (line 15) | function useRemoteModelHandlers({ activeModelId, setPickerType, setLoadi...

FILE: src/screens/HomeScreen/index.tsx
  type HomeScreenProps (line 21) | type HomeScreenProps = {

FILE: src/screens/KnowledgeBaseScreen.tsx
  type NavigationProp (line 27) | type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
  type RouteProps (line 28) | type RouteProps = RouteProp<RootStackParamList, 'KnowledgeBase'>;

FILE: src/screens/LockScreen.tsx
  type LockScreenProps (line 25) | interface LockScreenProps {

FILE: src/screens/ModelDownloadHelpers.tsx
  function fetchModelFiles (line 19) | async function fetchModelFiles(

FILE: src/screens/ModelDownloadScreen.tsx
  type Props (line 26) | type Props = { navigation: NativeStackNavigationProp<RootStackParamList,...
  type RecommendedCardProps (line 28) | interface RecommendedCardProps {

FILE: src/screens/ModelSettingsScreen/TextGenerationAdvanced.tsx
  constant HTP_UI_ENABLED (line 18) | const HTP_UI_ENABLED = false;
  type BackendOption (line 22) | type BackendOption = { id: InferenceBackend; label: string };
  constant IOS_BACKENDS (line 24) | const IOS_BACKENDS: BackendOption[] = [
  constant ANDROID_BASE_BACKENDS (line 29) | const ANDROID_BASE_BACKENDS: BackendOption[] = [
  constant HTP_BACKEND (line 34) | const HTP_BACKEND: BackendOption = { id: INFERENCE_BACKENDS.HTP, label: ...

FILE: src/screens/ModelSettingsScreen/TextGenerationSection.tsx
  constant FALLBACK_MAX_CONTEXT (line 10) | const FALLBACK_MAX_CONTEXT = 32768;
  constant HIGH_CONTEXT_THRESHOLD (line 11) | const HIGH_CONTEXT_THRESHOLD = 8192;

FILE: src/screens/ModelsScreen/ImageFilterBar.tsx
  type Props (line 9) | interface Props {
  function getBackendLabel (line 23) | function getBackendLabel(filter: BackendFilter): string {
  function getSdLabel (line 30) | function getSdLabel(filter: string): string {
  function getStyleLabel (line 34) | function getStyleLabel(filter: string): string {
  type ExpandedSectionProps (line 38) | interface ExpandedSectionProps {

FILE: src/screens/ModelsScreen/ImageModelsTab.tsx
  type Props (line 17) | type Props = Pick<ModelsScreenViewModel,
  type ImageModelCardProps (line 36) | interface ImageModelCardProps {
  function shouldShowEmptyMessage (line 95) | function shouldShowEmptyMessage({ loading, error, filtered, available }:...
  type ScrollContentProps (line 99) | interface ScrollContentProps {

FILE: src/screens/ModelsScreen/TextFiltersSection.tsx
  type Props (line 9) | interface Props {

FILE: src/screens/ModelsScreen/TextModelsTab.tsx
  function hasNonSortFilters (line 21) | function hasNonSortFilters(fs: FilterState): boolean {
  function getEmptyText (line 25) | function getEmptyText(hasSearched: boolean, hasActiveFilters: boolean): ...
  type Props (line 31) | type Props = Pick<ModelsScreenViewModel,
  type DetailProps (line 55) | type DetailProps = Pick<Props,
  type ModelListItemProps (line 180) | interface ModelListItemProps {
  function applyBackNavigation (line 190) | function applyBackNavigation(setSelectedModel: (m: ModelInfo | null) => ...
  type SortPanelProps (line 197) | interface SortPanelProps {

FILE: src/screens/ModelsScreen/constants.ts
  constant VISION_PIPELINE_TAG (line 11) | const VISION_PIPELINE_TAG = 'image-text-to-text';
  constant CODE_FALLBACK_QUERY (line 12) | const CODE_FALLBACK_QUERY = 'coder';
  constant CREDIBILITY_OPTIONS (line 14) | const CREDIBILITY_OPTIONS: { key: CredibilityFilter; label: string; colo...
  constant MODEL_TYPE_OPTIONS (line 22) | const MODEL_TYPE_OPTIONS: { key: ModelTypeFilter; label: string }[] = [
  constant SIZE_OPTIONS (line 29) | const SIZE_OPTIONS: { key: SizeFilter; label: string; min: number; max: ...
  constant QUANT_OPTIONS (line 37) | const QUANT_OPTIONS = [
  constant STYLE_OPTIONS (line 46) | const STYLE_OPTIONS = [
  constant SD_VERSION_OPTIONS (line 52) | const SD_VERSION_OPTIONS = [
  constant BACKEND_OPTIONS (line 59) | const BACKEND_OPTIONS: { key: BackendFilter; label: string }[] = [
  constant SORT_OPTIONS (line 65) | const SORT_OPTIONS: { key: SortOption; label: string; icon: string }[] = [

FILE: src/screens/ModelsScreen/imageDownloadActions.ts
  function cleanupDownloadState (line 16) | function cleanupDownloadState(deps: ImageDownloadDeps, modelId: string, ...
  function registerAndNotify (line 34) | async function registerAndNotify(
  function wireDownloadListeners (line 50) | function wireDownloadListeners(
  type ImageDownloadDeps (line 73) | interface ImageDownloadDeps {
  function downloadHuggingFaceModel (line 99) | async function downloadHuggingFaceModel(
  function downloadCoreMLMultiFile (line 168) | async function downloadCoreMLMultiFile(
  function proceedWithDownload (line 245) | async function proceedWithDownload(
  function getQnnWarningMessage (line 352) | function getQnnWarningMessage(
  function showQnnWarningAlert (line 375) | function showQnnWarningAlert(
  function handleDownloadImageModel (line 392) | async function handleDownloadImageModel(

FILE: src/screens/ModelsScreen/importHelpers.ts
  type GgufFileRef (line 6) | type GgufFileRef = { uri: string; name: string; size: number };
  type GgufImportDeps (line 8) | type GgufImportDeps = {
  function isMmProj (line 14) | function isMmProj(name: string): boolean {
  function classifyGgufPair (line 23) | function classifyGgufPair(
  function getErrorMessage (line 37) | function getErrorMessage(error: unknown): string {
  function importGgufFiles (line 42) | async function importGgufFiles(

FILE: src/screens/ModelsScreen/types.ts
  type BackendFilter (line 7) | type BackendFilter = 'all' | 'mnn' | 'qnn' | 'coreml';
  type ImageModelDescriptor (line 9) | interface ImageModelDescriptor {
  type CredibilityFilter (line 28) | type CredibilityFilter = 'all' | ModelSource;
  type ModelTypeFilter (line 29) | type ModelTypeFilter = 'all' | 'text' | 'vision' | 'code' | 'image-gen';
  type SizeFilter (line 30) | type SizeFilter = 'all' | 'tiny' | 'small' | 'medium' | 'large';
  type SortOption (line 31) | type SortOption = 'recommended' | 'bestfit' | 'size' | 'downloads' | 're...
  type FilterDimension (line 32) | type FilterDimension = 'org' | 'type' | 'source' | 'size' | 'quant' | 's...
  type ImageFilterDimension (line 33) | type ImageFilterDimension = 'backend' | 'style' | 'sdVersion' | null;
  type ModelTab (line 34) | type ModelTab = 'text' | 'image';
  type FilterState (line 36) | interface FilterState {
  type NavigationProp (line 46) | type NavigationProp = CompositeNavigationProp<

FILE: src/screens/ModelsScreen/useImageModels.ts
  function handleCompletedImageDownload (line 24) | async function handleCompletedImageDownload(opts: {
  function useImageModels (line 85) | function useImageModels(setAlertState: (s: AlertState) => void) {

FILE: src/screens/ModelsScreen/useModelsScreen.ts
  type ZipImportDeps (line 21) | type ZipImportDeps = {
  function importImageModelZip (line 29) | async function importImageModelZip(sourceUri: string, fileName: string, ...
  function useModelsScreen (line 74) | function useModelsScreen() {
  type ModelsScreenViewModel (line 287) | type ModelsScreenViewModel = ReturnType<typeof useModelsScreen>;

FILE: src/screens/ModelsScreen/useTextModels.ts
  constant PARAM_COUNT_REGEX (line 15) | const PARAM_COUNT_REGEX = /\b(\d+[.]\d+|\d+)\s?[Bb]\b/;
  function parseParamCount (line 17) | function parseParamCount(model: ModelInfo): number | null {
  function bestFitScore (line 23) | function bestFitScore(model: ModelInfo, ramGB: number): number {
  function applySort (line 30) | function applySort<T extends ModelInfo>(models: T[], sort: SortOption, r...
  function matchesOrgFilter (line 42) | function matchesOrgFilter(model: ModelInfo, orgs: string[]): boolean {
  function mapCuratedModel (line 52) | function mapCuratedModel(m: typeof RECOMMENDED_MODELS[number], details: ...
  function fetchRecommendedModelDetails (line 59) | async function fetchRecommendedModelDetails(): Promise<Record<string, Mo...
  function computeFilteredResults (line 68) | function computeFilteredResults(
  function useTextModels (line 95) | function useTextModels(setAlertState: (s: AlertState) => void) {

FILE: src/screens/ModelsScreen/utils.ts
  function formatNumber (line 6) | function formatNumber(num: number): string {
  function formatBytes (line 12) | function formatBytes(bytes: number): string {
  function getDirectorySize (line 19) | async function getDirectorySize(dirPath: string): Promise<number> {
  function isImageGenModel (line 35) | function isImageGenModel(tags: string[], name: string, id: string): bool...
  function isVisionModel (line 43) | function isVisionModel(tags: string[], name: string, id: string): boolean {
  function isCodeModel (line 51) | function isCodeModel(tags: string[], name: string, id: string): boolean {
  function getModelType (line 59) | function getModelType(model: ModelInfo): ModelTypeFilter {
  function isPhiModel (line 71) | function isPhiModel(modelName: string, modelId: string): boolean {
  function getTextModelCompatibility (line 77) | function getTextModelCompatibility(
  function matchesSdVersionFilter (line 91) | function matchesSdVersionFilter(modelName: string, sdVersionFilter: stri...
  function getImageModelCompatibility (line 104) | function getImageModelCompatibility(
  function hfModelToDescriptor (line 141) | function hfModelToDescriptor(

FILE: src/screens/OnboardingScreen.tsx
  type OnboardingScreenProps (line 32) | type OnboardingScreenProps = {

FILE: src/screens/OrphanedFilesSection.tsx
  type OrphanedFile (line 17) | interface OrphanedFile {
  type Props (line 23) | interface Props {

FILE: src/screens/PassphraseSetupScreen.tsx
  type PassphraseSetupScreenProps (line 22) | interface PassphraseSetupScreenProps {

FILE: src/screens/ProjectChatsScreen.tsx
  type NavigationProp (line 23) | type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
  type RouteProps (line 24) | type RouteProps = RouteProp<RootStackParamList, 'ProjectChats'>;

FILE: src/screens/ProjectDetailKnowledgeBaseSection.tsx
  type KBSectionProps (line 18) | interface KBSectionProps {

FILE: src/screens/ProjectDetailScreen.tsx
  type NavigationProp (line 22) | type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
  type RouteProps (line 23) | type RouteProps = RouteProp<RootStackParamList, 'ProjectDetail'>;

FILE: src/screens/ProjectEditScreen.tsx
  type NavigationProp (line 24) | type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Pro...
  type RouteProps (line 25) | type RouteProps = RouteProp<RootStackParamList, 'ProjectEdit'>;

FILE: src/screens/ProjectsScreen.tsx
  type NavigationProp (line 28) | type NavigationProp = CompositeNavigationProp<

FILE: src/screens/RemoteServersScreen.styles.ts
  function createStyles (line 3) | function createStyles(colors: ThemeColors, _shadows: ThemeShadows) {

FILE: src/screens/RemoteServersScreen.tsx
  type NavigationProp (line 28) | type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Rem...

FILE: src/screens/SettingsScreen.tsx
  constant FEEDBACK_EMAIL (line 32) | const FEEDBACK_EMAIL = 'work@wednesday.is';
  type NavigationProp (line 34) | type NavigationProp = CompositeNavigationProp<

FILE: src/services/activeModelService/index.ts
  class ActiveModelService (line 31) | class ActiveModelService {
    method acquireTextMutex (line 41) | private acquireTextMutex(): { release: () => void; ready: Promise<void...
    method getActiveModels (line 47) | getActiveModels(): ActiveModelInfo {
    method hasAnyModelLoaded (line 68) | hasAnyModelLoaded(): boolean {
    method getLoadedModelIds (line 72) | getLoadedModelIds(): {
    method getPerformanceStats (line 81) | getPerformanceStats() {
    method loadTextModel (line 84) | async loadTextModel(
    method unloadTextModel (line 138) | async unloadTextModel(): Promise<void> {
    method checkImageModelCanLoad (line 160) | private async checkImageModelCanLoad(
    method loadImageModel (line 184) | async loadImageModel(
    method unloadImageModel (line 246) | async unloadImageModel(): Promise<void> {
    method unloadAllModels (line 273) | async unloadAllModels(): Promise<{ textUnloaded: boolean; imageUnloade...
    method getResourceUsage (line 300) | async getResourceUsage(): Promise<ResourceUsage> {
    method getIds (line 303) | private getIds() {
    method getLists (line 306) | private getLists() {
    method getCurrentlyLoadedMemoryGB (line 310) | private getCurrentlyLoadedMemoryGB(): number {
    method checkMemoryForModel (line 313) | async checkMemoryForModel(modelId: string, modelType: ModelType): Prom...
    method checkMemoryForDualModel (line 316) | async checkMemoryForDualModel(textModelId: string | null, imageModelId...
    method clearTextModelCache (line 319) | async clearTextModelCache(): Promise<void> {
    method syncWithNativeState (line 324) | async syncWithNativeState(): Promise<void> {
    method subscribe (line 339) | subscribe(listener: ModelChangeListener): () => void {
    method notifyListeners (line 343) | private notifyListeners(): void {

FILE: src/services/activeModelService/loaders.ts
  function isMMProjFile (line 19) | function isMMProjFile(fileName: string): boolean {
  function scanDirForMmProj (line 31) | async function scanDirForMmProj(modelFilePath: string): Promise<RNFS.Rea...
  function resolveMmProjPath (line 39) | async function resolveMmProjPath(
  type TextLoadContext (line 89) | interface TextLoadContext {
  function doLoadTextModel (line 100) | async function doLoadTextModel(ctx: TextLoadContext): Promise<void> {
  type ImageLoadContext (line 184) | interface ImageLoadContext {
  function doLoadImageModel (line 198) | async function doLoadImageModel(ctx: ImageLoadContext): Promise<void> {

FILE: src/services/activeModelService/memory.ts
  function estimateModelMemoryGB (line 39) | function estimateModelMemoryGB(
  type LoadedModelIds (line 53) | interface LoadedModelIds {
  type ModelLists (line 58) | interface ModelLists {
  function getCurrentlyLoadedMemoryGB (line 63) | function getCurrentlyLoadedMemoryGB(
  function getOtherLoadedMemoryGB (line 89) | function getOtherLoadedMemoryGB(
  type CheckMemoryParams (line 116) | interface CheckMemoryParams {
  function checkMemoryForModel (line 123) | async function checkMemoryForModel(
  type CheckDualMemoryParams (line 201) | interface CheckDualMemoryParams {
  function checkMemoryForDualModel (line 207) | async function checkMemoryForDualModel(

FILE: src/services/activeModelService/types.ts
  type ModelType (line 4) | type ModelType = 'text' | 'image';
  type MemoryCheckSeverity (line 6) | type MemoryCheckSeverity = 'safe' | 'warning' | 'critical' | 'blocked';
  type MemoryCheckResult (line 8) | interface MemoryCheckResult {
  type ActiveModelInfo (line 19) | interface ActiveModelInfo {
  type ResourceUsage (line 32) | interface ResourceUsage {
  type ModelChangeListener (line 41) | type ModelChangeListener = (info: ActiveModelInfo) => void;
  constant TEXT_MODEL_OVERHEAD_MULTIPLIER (line 51) | const TEXT_MODEL_OVERHEAD_MULTIPLIER = 1.5;
  constant IMAGE_MODEL_OVERHEAD_MULTIPLIER (line 53) | const IMAGE_MODEL_OVERHEAD_MULTIPLIER = Platform.OS === 'ios' ? 1.5 : 1.8;

FILE: src/services/activeModelService/utils.ts
  function getResourceUsage (line 11) | async function getResourceUsage(): Promise<ResourceUsage> {
  type SyncStateTarget (line 38) | interface SyncStateTarget {
  function syncWithNativeState (line 46) | async function syncWithNativeState(target: SyncStateTarget): Promise<voi...

FILE: src/services/authService.ts
  constant SERVICE_NAME (line 4) | const SERVICE_NAME = 'ai.offgridmobile.auth';
  constant PASSPHRASE_KEY (line 5) | const PASSPHRASE_KEY = 'passphrase_hash';
  class AuthService (line 7) | class AuthService {
    method hashPassphrase (line 8) | private hashPassphrase(passphrase: string): string {
    method setPassphrase (line 32) | async setPassphrase(passphrase: string): Promise<boolean> {
    method verifyPassphrase (line 46) | async verifyPassphrase(passphrase: string): Promise<boolean> {
    method hasPassphrase (line 64) | async hasPassphrase(): Promise<boolean> {
    method removePassphrase (line 76) | async removePassphrase(): Promise<boolean> {
    method changePassphrase (line 88) | async changePassphrase(oldPassphrase: string, newPassphrase: string): ...

FILE: src/services/backgroundDownloadService.ts
  class BackgroundDownloadService (line 11) | class BackgroundDownloadService {
    method constructor (line 20) | constructor() {
    method isAvailable (line 27) | isAvailable(): boolean {
    method startDownload (line 31) | async startDownload(params: DownloadParams): Promise<BackgroundDownloa...
    method startMultiFileDownload (line 55) | async startMultiFileDownload(params: MultiFileDownloadParams): Promise...
    method cancelDownload (line 79) | async cancelDownload(downloadId: number): Promise<void> {
    method getActiveDownloads (line 90) | async getActiveDownloads(): Promise<BackgroundDownloadInfo[]> {
    method getDownloadProgress (line 110) | async getDownloadProgress(downloadId: number): Promise<{
    method moveCompletedDownload (line 132) | async moveCompletedDownload(downloadId: number, targetPath: string): P...
    method registerListener (line 139) | private registerListener<T>(listeners: Map<string, T>, key: string, ca...
    method onProgress (line 144) | onProgress(downloadId: number, callback: DownloadProgressCallback): ()...
    method onComplete (line 147) | onComplete(downloadId: number, callback: DownloadCompleteCallback): ()...
    method onError (line 150) | onError(downloadId: number, callback: DownloadErrorCallback): () => vo...
    method onAnyProgress (line 153) | onAnyProgress(callback: DownloadProgressCallback): () => void {
    method onAnyComplete (line 156) | onAnyComplete(callback: DownloadCompleteCallback): () => void {
    method onAnyError (line 159) | onAnyError(callback: DownloadErrorCallback): () => void {
    method startProgressPolling (line 162) | startProgressPolling(): void {
    method stopProgressPolling (line 170) | stopProgressPolling(): void {
    method isBatteryOptimizationIgnored (line 179) | async isBatteryOptimizationIgnored(): Promise<boolean> {
    method requestBatteryOptimizationIgnore (line 189) | requestBatteryOptimizationIgnore(): void {
    method checkAndPromptBatteryOptimization (line 199) | async checkAndPromptBatteryOptimization(): Promise<void> {
    method downloadFileTo (line 227) | downloadFileTo(opts: {
    method markSilent (line 294) | markSilent(downloadId: number): void { this.silentDownloadIds.add(down...
    method unmarkSilent (line 295) | unmarkSilent(downloadId: number): void { this.silentDownloadIds.delete...
    method excludeFromBackup (line 297) | async excludeFromBackup(path: string): Promise<boolean> {
    method cleanup (line 302) | cleanup(): void {
    method dispatchToListeners (line 311) | private dispatchToListeners<T extends { downloadId: number }>(
    method setupEventListeners (line 322) | private setupEventListeners(): void {

FILE: src/services/backgroundDownloadTypes.ts
  type DownloadParams (line 3) | interface DownloadParams {
  type MultiFileDownloadParams (line 13) | interface MultiFileDownloadParams {
  type DownloadProgressEvent (line 21) | interface DownloadProgressEvent {
  type DownloadCompleteEvent (line 32) | interface DownloadCompleteEvent {
  type DownloadErrorEvent (line 42) | interface DownloadErrorEvent {
  type DownloadProgressCallback (line 51) | type DownloadProgressCallback = (event: DownloadProgressEvent) => void;
  type DownloadCompleteCallback (line 52) | type DownloadCompleteCallback = (event: DownloadCompleteEvent) => void;
  type DownloadErrorCallback (line 53) | type DownloadErrorCallback = (event: DownloadErrorEvent) => void;

FILE: src/services/contextCompaction.ts
  constant CONTEXT_FULL_PATTERNS (line 20) | const CONTEXT_FULL_PATTERNS = [
  constant PROMPT_BUDGET_RATIO (line 28) | const PROMPT_BUDGET_RATIO = 0.55;
  constant SUMMARY_BUDGET_RATIO (line 31) | const SUMMARY_BUDGET_RATIO = 0.12;
  constant CHARS_PER_TOKEN_ESTIMATE (line 34) | const CHARS_PER_TOKEN_ESTIMATE = 4;
  constant SUMMARIZER_INSTRUCTION_OVERHEAD_TOKENS (line 37) | const SUMMARIZER_INSTRUCTION_OVERHEAD_TOKENS = 100;
  constant SUMMARIZER_SYSTEM_PROMPT (line 40) | const SUMMARIZER_SYSTEM_PROMPT =
  class ContextCompactionService (line 43) | class ContextCompactionService {
    method isCompacting (line 47) | get isCompacting(): boolean { return this._isCompacting; }
    method subscribeCompacting (line 49) | subscribeCompacting(listener: (v: boolean) => void): () => void {
    method setCompacting (line 55) | private setCompacting(v: boolean): void {
    method isContextFullError (line 60) | isContextFullError(error: unknown): boolean {
    method countTokens (line 66) | private async countTokens(text: string): Promise<number> {
    method compact (line 84) | async compact(
    method summarizeMessages (line 171) | private async summarizeMessages(
    method clearSummary (line 214) | clearSummary(conversationId: string): void {

FILE: src/services/coreMLModelBrowser.ts
  type CoreMLModelFile (line 2) | interface CoreMLModelFile {
  type CoreMLImageModel (line 9) | interface CoreMLImageModel {
  type HFTreeEntry (line 24) | interface HFTreeEntry {
  type RepoEntry (line 35) | interface RepoEntry {
  constant REPOS (line 43) | const REPOS: RepoEntry[] = [
  constant CACHE_TTL (line 85) | const CACHE_TTL = 5 * 60 * 1000;
  function fetchRepoTree (line 87) | async function fetchRepoTree(repo: string, path = ''): Promise<HFTreeEnt...
  function findCompiledZip (line 102) | function findCompiledZip(entries: HFTreeEntry[], variant: 'split_einsum'...
  function fetchModelFromRepo (line 112) | async function fetchModelFromRepo(
  function fetchAvailableCoreMLModels (line 201) | async function fetchAvailableCoreMLModels(

FILE: src/services/documentService.ts
  constant TEXT_EXTENSIONS (line 14) | const TEXT_EXTENSIONS = ['.txt', '.md', '.csv', '.json', '.xml', '.html'...
  constant PDF_EXTENSION (line 17) | const PDF_EXTENSION = '.pdf';
  constant MAX_FILE_SIZE (line 20) | const MAX_FILE_SIZE = 5 * 1024 * 1024;
  constant ATTACHMENTS_DIR (line 23) | const ATTACHMENTS_DIR = `${RNFS.DocumentDirectoryPath}/attachments`;
  class DocumentService (line 25) | class DocumentService {
    method ensureAttachmentsDir (line 29) | private async ensureAttachmentsDir(): Promise<void> {
    method isSupported (line 38) | isSupported(fileName: string): boolean {
    method resolveContentUri (line 52) | private async resolveContentUri(uri: string, fileName: string): Promis...
    method validateFileType (line 109) | private validateFileType(extension: string, isPdf: boolean): void {
    method readContent (line 118) | private async readContent(resolvedPath: string, isPdf: boolean, maxCha...
    method savePersistentCopy (line 135) | private async savePersistentCopy(resolvedPath: string, originalPath: s...
    method processDocumentFromPath (line 153) | async processDocumentFromPath(filePath: string, fileName?: string, max...
    method createFromText (line 200) | async createFromText(text: string, fileName: string = 'pasted-text.txt...
    method formatForContext (line 234) | formatForContext(attachment: MediaAttachment): string {
    method getPreview (line 246) | getPreview(attachment: MediaAttachment, maxLength: number = 100): stri...
    method getSupportedExtensions (line 258) | getSupportedExtensions(): string[] {

FILE: src/services/generationService.ts
  constant SHARE_PROMPT_DELAY_MS (line 20) | const SHARE_PROMPT_DELAY_MS = 1500;
  type StreamChunk (line 21) | type StreamChunk = string | { content?: string; reasoningContent?: strin...
  type QueuedMessage (line 23) | interface QueuedMessage {
  type GenerationState (line 28) | interface GenerationState {
  type GenerationListener (line 37) | type GenerationListener = (state: GenerationState) => void;
  type QueueProcessor (line 38) | type QueueProcessor = (item: QueuedMessage) => Promise<void>;
  class GenerationService (line 40) | class GenerationService {
    method getCurrentProvider (line 60) | private getCurrentProvider() {
    method isUsingRemoteProvider (line 69) | private isUsingRemoteProvider(): boolean {
    method flushTokenBuffer (line 85) | private flushTokenBuffer(): void {
    method forceFlushTokens (line 98) | private forceFlushTokens(): void {
    method normalizeStreamChunk (line 106) | private normalizeStreamChunk(data: StreamChunk): { content?: string; r...
    method getState (line 110) | getState(): GenerationState { return { ...this.state }; }
    method isGeneratingFor (line 112) | isGeneratingFor(conversationId: string): boolean {
    method subscribe (line 116) | subscribe(listener: GenerationListener): () => void {
    method notifyListeners (line 120) | private notifyListeners(): void { this.listeners.forEach(l => l(this.g...
    method updateState (line 122) | private updateState(partial: Partial<GenerationState>): void {
    method checkSharePrompt (line 127) | private checkSharePrompt(delayMs = SHARE_PROMPT_DELAY_MS): void {
    method buildToolLoopHandlers (line 133) | private buildToolLoopHandlers() { return buildToolLoopHandlersImpl(thi...
    method buildGenerationMeta (line 134) | private buildGenerationMeta(): GenerationMeta { return buildGeneration...
    method prepareGeneration (line 135) | private async prepareGeneration(conversationId: string): Promise<boole...
    method generateResponse (line 140) | async generateResponse(
    method generateWithTools (line 153) | async generateWithTools(
    method stopGeneration (line 206) | async stopGeneration(): Promise<string> {
    method generateRemoteResponse (line 263) | async generateRemoteResponse(
    method generateRemoteWithTools (line 272) | async generateRemoteWithTools(
    method enqueueMessage (line 280) | enqueueMessage(entry: QueuedMessage): void {
    method removeFromQueue (line 285) | removeFromQueue(id: string): void {
    method clearQueue (line 290) | clearQueue(): void { this.state = { ...this.state, queuedMessages: [] ...
    method setQueueProcessor (line 292) | setQueueProcessor(processor: QueueProcessor | null): void { this.queue...
    method processNextInQueue (line 294) | private processNextInQueue(): void {
    method resetState (line 308) | private resetState(): void {

FILE: src/services/generationServiceHelpers.ts
  constant FLUSH_INTERVAL_MS (line 13) | const FLUSH_INTERVAL_MS = 50;
  type StreamChunk (line 14) | type StreamChunk = string | { content?: string; reasoningContent?: strin...
  type GenerationRequest (line 16) | interface GenerationRequest {
  type GenerationWithToolsRequest (line 22) | interface GenerationWithToolsRequest {
  function buildGenerationMetaImpl (line 34) | function buildGenerationMetaImpl(svc: any): GenerationMeta {
  function buildToolLoopHandlersImpl (line 69) | function buildToolLoopHandlersImpl(svc: any) {
  function prepareGenerationImpl (line 105) | async function prepareGenerationImpl(svc: any, conversationId: string): ...
  function generateResponseImpl (line 140) | async function generateResponseImpl(
  function generateRemoteResponseImpl (line 192) | async function generateRemoteResponseImpl(
  function generateRemoteWithToolsImpl (line 277) | async function generateRemoteWithToolsImpl(

FILE: src/services/generationToolLoop.ts
  constant MAX_TOOL_ITERATIONS (line 11) | const MAX_TOOL_ITERATIONS = 3;
  constant MAX_TOTAL_TOOL_CALLS (line 12) | const MAX_TOTAL_TOOL_CALLS = 5;
  type StreamChunk (line 13) | type StreamChunk = string | StreamToken;
  function parseXmlStyleToolCall (line 14) | function parseXmlStyleToolCall(body: string, idSuffix: number): ToolCall...
  function parseToolCallBody (line 25) | function parseToolCallBody(body: string, idSuffix: number): ToolCall | n...
  function parseToolCallsFromText (line 33) | function parseToolCallsFromText(text: string): { cleanText: string; tool...
  type ToolLoopCallbacks (line 61) | interface ToolLoopCallbacks {
  type ToolLoopContext (line 66) | interface ToolLoopContext {
  function normalizeStreamChunk (line 79) | function normalizeStreamChunk(data: StreamChunk): StreamToken {
  function getLastUserQuery (line 82) | function getLastUserQuery(messages: Message[]): string {
  function executeToolCalls (line 87) | async function executeToolCalls(ctx: ToolLoopContext, toolCalls: import(...
  constant MAX_LLM_RETRIES (line 114) | const MAX_LLM_RETRIES = 4;
  constant RETRY_BACKOFF_MS (line 115) | const RETRY_BACKOFF_MS = 1000;
  constant CONTEXT_RELEASE_PAUSE_MS (line 116) | const CONTEXT_RELEASE_PAUSE_MS = 500;
  function isNonRetryableError (line 117) | function isNonRetryableError(msg: string): boolean {
  function callRemoteLLMWithTools (line 121) | async function callRemoteLLMWithTools(
  function callLocalWithRetry (line 169) | async function callLocalWithRetry(
  type CallLLMOptions (line 190) | interface CallLLMOptions { onStream?: (data: StreamToken) => void; force...
  function callLLMWithRetry (line 193) | async function callLLMWithRetry(
  function resolveToolCalls (line 211) | function resolveToolCalls(fullResponse: string, toolCalls: ToolCall[]) {
  type ToolLoopState (line 222) | interface ToolLoopState {
  function buildStreamHandler (line 229) | function buildStreamHandler(ctx: ToolLoopContext, state: ToolLoopState):...
  function emitFinalResponse (line 246) | function emitFinalResponse(ctx: ToolLoopContext, state: ToolLoopState, d...
  function forceFinalTextResponse (line 260) | async function forceFinalTextResponse(ctx: ToolLoopContext, state: ToolL...
  function runToolLoop (line 276) | async function runToolLoop(ctx: ToolLoopContext): Promise<void> {

FILE: src/services/hardware.ts
  constant FLAGSHIP_8GEN2 (line 23) | const FLAGSHIP_8GEN2 = new Set([8550, 8650, 8735, 8750, 8845, 8850]);
  constant FLAGSHIP_8GEN1 (line 24) | const FLAGSHIP_8GEN1 = new Set([8450, 8475]);
  class HardwareService (line 25) | class HardwareService {
    method getDeviceInfo (line 30) | async getDeviceInfo(): Promise<DeviceInfoType> {
    method refreshMemoryInfo (line 60) | async refreshMemoryInfo(): Promise<DeviceInfoType> {
    method getAppMemoryUsage (line 80) | async getAppMemoryUsage(): Promise<{
    method getTotalMemoryGB (line 93) | getTotalMemoryGB(): number {
    method getAvailableMemoryGB (line 108) | getAvailableMemoryGB(): number {
    method getModelRecommendation (line 128) | getModelRecommendation(): ModelRecommendation {
    method canRunModel (line 153) | canRunModel(
    method estimateModelMemoryGB (line 166) | estimateModelMemoryGB(
    method getQuantizationBits (line 173) | private getQuantizationBits(quantization: string): number {
    method formatBytes (line 180) | formatBytes(bytes: number): string {
    method getModelTotalSize (line 186) | getModelTotalSize(model: { fileSize?: number; size?: number; mmProjFil...
    method formatModelSize (line 189) | formatModelSize(model: { fileSize?: number; size?: number; mmProjFileS...
    method estimateModelRam (line 192) | estimateModelRam(model: { fileSize?: number; size?: number; mmProjFile...
    method formatModelRam (line 195) | formatModelRam(model: { fileSize?: number; size?: number; mmProjFileSi...
    method detectAppleChip (line 198) | private detectAppleChip(deviceId: string): SoCInfo['appleChip'] {
    method getSoCInfo (line 209) | async getSoCInfo(): Promise<SoCInfo> {
    method getQnnVariantFromSoC (line 237) | private async getQnnVariantFromSoC(): Promise<
    method fetchSoCModel (line 244) | private async fetchSoCModel(): Promise<string> {
    method classifySmNumber (line 253) | private classifySmNumber(
    method getIosImageRec (line 266) | private getIosImageRec(chip: SoCInfo['appleChip'], ramGB: number): Ima...
    method getQualcommImageRec (line 276) | private getQualcommImageRec(socInfo: SoCInfo): ImageModelRecommendation {
    method getImageModelRecommendation (line 288) | async getImageModelRecommendation(): Promise<ImageModelRecommendation> {
    method getDeviceTier (line 318) | getDeviceTier(): 'low' | 'medium' | 'high' | 'flagship' {
    method getCpuCoreCount (line 325) | async getCpuCoreCount(): Promise<number> {
    method getRecommendedThreadCount (line 333) | async getRecommendedThreadCount(): Promise<number> {
    method getOpenCLCapability (line 337) | async getOpenCLCapability(): Promise<{ supported: boolean; reason?: st...

FILE: src/services/httpClient.ts
  type SSEEvent (line 15) | interface SSEEvent {
  type FetchOptions (line 25) | interface FetchOptions extends RequestInit {
  type StreamRequestOptions (line 35) | interface StreamRequestOptions {
  type StreamRequestConfig (line 42) | interface StreamRequestConfig extends StreamRequestOptions {
  type OpenAIStreamMessage (line 47) | interface OpenAIStreamMessage {
  type AnthropicStreamMessage (line 77) | interface AnthropicStreamMessage {
  constant DEFAULT_TIMEOUT (line 103) | const DEFAULT_TIMEOUT = 30000;
  constant DEFAULT_RETRIES (line 104) | const DEFAULT_RETRIES = 0;
  constant DEFAULT_RETRY_DELAY (line 105) | const DEFAULT_RETRY_DELAY = 1000;
  function fetchWithTimeout (line 110) | async function fetchWithTimeout<T = unknown>(
  function createStreamingRequest (line 170) | async function createStreamingRequest(
  function createNDJSONStreamingRequest (line 265) | async function createNDJSONStreamingRequest(

FILE: src/services/httpClientSSE.ts
  function yieldSSEvent (line 11) | function yieldSSEvent(currentEvent: Partial<SSEEvent>): SSEEvent {
  function parseSSELine (line 23) | function parseSSELine(
  function createSSELineProcessor (line 54) | function createSSELineProcessor(onEvent: (event: SSEEvent) => void) {
  function processSSELines (line 92) | function processSSELines(
  function parseOpenAIMessage (line 178) | function parseOpenAIMessage(event: SSEEvent): OpenAIStreamMessage | null {
  function parseAnthropicMessage (line 197) | function parseAnthropicMessage(event: SSEEvent): AnthropicStreamMessage ...

FILE: src/services/httpClientUtils.ts
  function mimeTypeFromExtension (line 5) | function mimeTypeFromExtension(ext: string | undefined): string {
  function fetchBlobAsBase64 (line 12) | async function fetchBlobAsBase64(uri: string): Promise<string> {
  function imageToBase64DataUrl (line 29) | async function imageToBase64DataUrl(uri: string): Promise<string> {
  function isPrivateNetworkEndpoint (line 65) | function isPrivateNetworkEndpoint(endpoint: string): boolean {
  function testEndpoint (line 120) | async function testEndpoint(
  function checkOllamaEndpoint (line 184) | async function checkOllamaEndpoint(
  function checkLmStudioEndpoint (line 205) | async function checkLmStudioEndpoint(
  function detectServerType (line 232) | async function detectServerType(

FILE: src/services/huggingFaceModelBrowser.ts
  type HFImageModel (line 1) | interface HFImageModel {
  type HFTreeEntry (line 13) | interface HFTreeEntry {
  constant REPOS (line 20) | const REPOS = {
  constant VARIANT_LABELS (line 25) | const VARIANT_LABELS: Record<string, string> = {
  constant CACHE_TTL (line 33) | const CACHE_TTL = 5 * 60 * 1000;
  function insertSpaces (line 35) | function insertSpaces(name: string): string {
  function parseFileName (line 41) | function parseFileName(fileName: string, backend: 'mnn' | 'qnn'): Omit<H...
  function fetchRepoFiles (line 72) | async function fetchRepoFiles(repo: string): Promise<HFTreeEntry[]> {
  function fetchAvailableModels (line 80) | async function fetchAvailableModels(forceRefresh = false, opts?: { skipQ...
  function getVariantLabel (line 128) | function getVariantLabel(variant?: string): string | undefined {
  function guessStyle (line 132) | function guessStyle(name: string): string {

FILE: src/services/huggingface.ts
  class HuggingFaceService (line 4) | class HuggingFaceService {
    method fetchJson (line 8) | private async fetchJson<T>(url: string): Promise<T> {
    method searchModels (line 14) | async searchModels(
    method getModelDetails (line 26) | async getModelDetails(modelId: string): Promise<ModelInfo> {
    method getModelFiles (line 31) | async getModelFiles(modelId: string): Promise<ModelFile[]> {
    method getModelFilesFromSiblings (line 53) | private async getModelFilesFromSiblings(modelId: string): Promise<Mode...
    method getDownloadUrl (line 65) | getDownloadUrl(modelId: string, fileName: string, revision: string = '...
    method determineCredibility (line 69) | private determineCredibility(author: string): ModelCredibility {
    method transformFileInfo (line 101) | private transformFileInfo(modelId: string, file: { rfilename: string; ...
    method extractQuantization (line 115) | private extractQuantization(fileName: string): string {
    method isMMProjFile (line 137) | private isMMProjFile(fileName: string): boolean {
    method findMatchingMMProj (line 144) | private findMatchingMMProj(
    method detectModelType (line 184) | private detectModelType(name: string, tags: string[]): string {
    method extractDescription (line 193) | private extractDescription(result: HFModelSearchResult): string {
    method formatFileSize (line 209) | formatFileSize(bytes: number): string {
    method getQuantizationInfo (line 218) | getQuantizationInfo(quantization: string) {

FILE: src/services/imageGenerationHelpers.ts
  type ActiveImageModel (line 5) | interface ActiveImageModel {
  function buildEnhancementMessages (line 12) | function buildEnhancementMessages(prompt: string, contextMessages: Messa...
  function getConversationContext (line 25) | function getConversationContext(conversationId: string): Message[] {
  function cleanEnhancedPrompt (line 34) | function cleanEnhancedPrompt(raw: string): string {
  function buildImageGenMeta (line 38) | function buildImageGenMeta(

FILE: src/services/imageGenerationService.ts
  constant SHARE_PROMPT_DELAY_MS (line 11) | const SHARE_PROMPT_DELAY_MS = 2000;
  type ImageGenerationState (line 13) | interface ImageGenerationState {
  type ImageGenerationListener (line 24) | type ImageGenerationListener = (state: ImageGenerationState) => void;
  type GenerateImageParams (line 26) | interface GenerateImageParams {
  type ActiveImageModel (line 36) | interface ActiveImageModel {
  type RunGenerationOptions (line 43) | interface RunGenerationOptions {
  type UpdateEnhancementOptions (line 54) | interface UpdateEnhancementOptions {
  class ImageGenerationService (line 65) | class ImageGenerationService {
    method getState (line 74) | getState(): ImageGenerationState { return { ...this.state }; }
    method isGeneratingFor (line 76) | isGeneratingFor(conversationId: string): boolean {
    method subscribe (line 80) | subscribe(listener: ImageGenerationListener): () => void {
    method notifyListeners (line 86) | private notifyListeners(): void {
    method updateState (line 91) | private updateState(partial: Partial<ImageGenerationState>): void {
    method _checkSharePrompt (line 101) | private _checkSharePrompt(): void {
    method _resetLlmAfterEnhancement (line 107) | private async _resetLlmAfterEnhancement(): Promise<void> {
    method _updateEnhancementMessage (line 118) | private async _updateEnhancementMessage(opts: UpdateEnhancementOptions...
    method _enhancePrompt (line 131) | private async _enhancePrompt(params: GenerateImageParams, steps: numbe...
    method _ensureImageModelLoaded (line 180) | private async _ensureImageModelLoaded(activeImageModelId: string | nul...
    method _saveResult (line 200) | private _saveResult(result: any, opts: { params: GenerateImageParams; ...
    method _runGenerationAndSave (line 221) | private async _runGenerationAndSave(opts: RunGenerationOptions): Promi...
    method generateImage (line 294) | async generateImage(params: GenerateImageParams): Promise<GeneratedIma...
    method cancelGeneration (line 329) | async cancelGeneration(): Promise<void> {
    method resetState (line 336) | private resetState(): void {

FILE: src/services/imageGenerator.ts
  type ProgressCallback (line 10) | type ProgressCallback = (progress: ImageGenerationProgress) => void;
  type CompleteCallback (line 11) | type CompleteCallback = (image: GeneratedImage) => void;
  class ImageGeneratorService (line 13) | class ImageGeneratorService {
    method constructor (line 18) | constructor() {
    method isAvailable (line 24) | isAvailable(): boolean {
    method isModelLoaded (line 28) | async isModelLoaded(): Promise<boolean> {
    method getLoadedModelPath (line 37) | async getLoadedModelPath(): Promise<string | null> {
    method loadModel (line 46) | async loadModel(modelPath: string): Promise<boolean> {
    method unloadModel (line 53) | async unloadModel(): Promise<boolean> {
    method attachEventListeners (line 63) | private attachEventListeners(onProgress?: ProgressCallback, onComplete...
    method generateImage (line 79) | async generateImage(
    method cancelGeneration (line 119) | async cancelGeneration(): Promise<boolean> {
    method isGenerating (line 125) | async isGenerating(): Promise<boolean> {
    method getGeneratedImages (line 130) | async getGeneratedImages(): Promise<GeneratedImage[]> {
    method deleteGeneratedImage (line 150) | async deleteGeneratedImage(imageId: string): Promise<boolean> {
    method getConstants (line 155) | getConstants() {
    method removeListeners (line 169) | private removeListeners() {

FILE: src/services/intentClassifier.ts
  type Intent (line 6) | type Intent = 'image' | 'text';
  type ClassifyOptions (line 8) | interface ClassifyOptions {
  constant CACHE_MAX_SIZE (line 19) | const CACHE_MAX_SIZE = 100;
  constant IMAGE_PATTERNS (line 22) | const IMAGE_PATTERNS = [
  constant TEXT_PATTERNS (line 80) | const TEXT_PATTERNS = [
  class IntentClassifier (line 150) | class IntentClassifier {
    method classifyIntent (line 157) | async classifyIntent(message: string, options: ClassifyOptions | boole...
    method classifyByPattern (line 198) | private classifyByPattern(message: string): Intent | null {
    method classifyWithLLM (line 231) | private async classifyWithLLM(message: string, opts: ClassifyOptions):...
    method cacheIntent (line 309) | private cacheIntent(key: string, intent: Intent): void {
    method clearCache (line 322) | clearCache(): void {
    method quickCheck (line 330) | quickCheck(message: string): Intent {
  constant TOOL_PATTERNS (line 344) | const TOOL_PATTERNS: Record<string, RegExp[]> = {
  constant COUPLED_TOOLS (line 388) | const COUPLED_TOOLS: string[][] = [['web_search', 'read_url']];
  function classifyToolsNeeded (line 398) | function classifyToolsNeeded(message: string): string[] {

FILE: src/services/llm.ts
  type StreamToken (line 22) | type StreamToken = { content?: string; reasoningContent?: string };
  type StreamCallback (line 23) | type StreamCallback = (data: StreamToken) => void;
  type CompleteCallback (line 24) | type CompleteCallback = (result: { content: string; reasoningContent: st...
  function resolveGpuBackend (line 25) | function resolveGpuBackend(enabled: boolean, devices: string[]): string {
  class LLMService (line 29) | class LLMService {
    method acquireContextMutex (line 47) | private acquireContextMutex(): { release: () => void; ready: Promise<v...
    method hashString (line 53) | private hashString(value: string): string { return hashString(value); }
    method ensureSessionCacheDir (line 54) | private ensureSessionCacheDir(): Promise<void> { return ensureSessionC...
    method getSessionPath (line 55) | private getSessionPath(promptHash: string): string { return getSession...
    method validateAndPrepareModel (line 56) | private async validateAndPrepareModel(modelPath: string): Promise<{ fi...
    method applyLoadedContext (line 76) | private async applyLoadedContext(opts: { context: LlamaContext; actual...
    method loadModel (line 91) | async loadModel(modelPath: string, mmProjPath?: string): Promise<void> {
    method initWithAutoContext (line 119) | private async initWithAutoContext(params: { baseParams: object; ctxLen...
    method initializeMultimodal (line 159) | async initializeMultimodal(mmProjPath: string): Promise<boolean> {
    method checkMultimodalSupport (line 173) | async checkMultimodalSupport(): Promise<MultimodalSupport> {
    method getMultimodalSupport (line 177) | getMultimodalSupport(): MultimodalSupport | null { return this.multimo...
    method supportsVision (line 178) | supportsVision(): boolean { return this.multimodalSupport?.vision || f...
    method supportsToolCalling (line 179) | supportsToolCalling(): boolean { return this.toolCallingSupported; }
    method supportsThinking (line 180) | supportsThinking(): boolean { return this.thinkingSupported; }
    method isThinkingEnabled (line 181) | isThinkingEnabled(): boolean { return this.thinkingSupported && useApp...
    method isGemma4Model (line 182) | isGemma4Model(): boolean {
    method shouldDisableCtxShift (line 187) | private shouldDisableCtxShift(): boolean { return Platform.OS === 'and...
    method detectToolCallingSupport (line 188) | private detectToolCallingSupport(): void {
    method detectThinkingSupport (line 200) | private detectThinkingSupport(): void {
    method doUnloadModel (line 204) | private async doUnloadModel(): Promise<void> {
    method unloadModel (line 215) | async unloadModel(): Promise<void> {
    method isModelLoaded (line 219) | isModelLoaded(): boolean { return this.context !== null; }
    method getLoadedModelPath (line 220) | getLoadedModelPath(): string | null { return this.currentModelPath; }
    method generateResponse (line 221) | async generateResponse(messages: Message[], onStream?: StreamCallback,...
    method generateResponseWithTools (line 263) | async generateResponseWithTools(messages: Message[], options: { tools:...
    method manageContextWindow (line 283) | private async manageContextWindow(messages: Message[], _extraReserve =...
    method generateWithMaxTokens (line 287) | async generateWithMaxTokens(messages: Message[], maxTokens: number): P...
    method stopGeneration (line 302) | async stopGeneration(): Promise<void> {
    method clearKVCache (line 307) | async clearKVCache(clearData: boolean = false): Promise<void> {
    method getEstimatedMemoryUsage (line 311) | getEstimatedMemoryUsage() {
    method getGpuInfo (line 315) | getGpuInfo() {
    method isCurrentlyGenerating (line 318) | isCurrentlyGenerating(): boolean { return this.isGenerating; }
    method formatMessages (line 319) | private formatMessages(messages: Message[]): string { return formatLla...
    method convertToOAIMessages (line 320) | private convertToOAIMessages(messages: Message[]): RNLlamaOAICompatibl...
    method getModelInfo (line 321) | async getModelInfo() { return this.context ? { contextLength: APP_CONF...
    method tokenize (line 322) | async tokenize(text: string) {
    method getTokenCount (line 326) | async getTokenCount(text: string) {
    method estimateContextUsage (line 330) | async estimateContextUsage(messages: Message[]) {
    method getFormattedPrompt (line 335) | getFormattedPrompt(messages: Message[]): string { return this.formatMe...
    method getContextDebugInfo (line 336) | async getContextDebugInfo(messages: Message[]) {
    method updatePerformanceSettings (line 350) | updatePerformanceSettings(settings: Partial<LLMPerformanceSettings>): ...
    method getPerformanceSettings (line 354) | getPerformanceSettings(): LLMPerformanceSettings { return { ...this.cu...
    method getPerformanceStats (line 355) | getPerformanceStats(): LLMPerformanceStats { return { ...this.performa...
    method reloadWithSettings (line 356) | async reloadWithSettings(modelPath: string, settings: LLMPerformanceSe...

FILE: src/services/llmHelpers.ts
  constant HTP_ENABLED (line 11) | const HTP_ENABLED = false;
  constant SYSTEM_PROMPT_RESERVE (line 13) | const SYSTEM_PROMPT_RESERVE = 256;
  constant RESPONSE_RESERVE (line 14) | const RESPONSE_RESERVE = 512;
  constant CONTEXT_SAFETY_MARGIN (line 15) | const CONTEXT_SAFETY_MARGIN = 0.85;
  constant DEFAULT_THREADS (line 16) | const DEFAULT_THREADS = 4;
  constant DEFAULT_BATCH (line 17) | const DEFAULT_BATCH = 512;
  constant DEFAULT_GPU_LAYERS (line 18) | const DEFAULT_GPU_LAYERS = Platform.OS === 'ios' ? 99 : 0;
  function getOptimalThreadCount (line 19) | function getOptimalThreadCount(): number { return DEFAULT_THREADS; }
  function getOptimalBatchSize (line 20) | function getOptimalBatchSize(): number { return DEFAULT_BATCH; }
  function logInferenceInit (line 21) | function logInferenceInit(level: 'log' | 'warn' | 'error', message: stri...
  constant REPACKABLE_QUANTS (line 24) | const REPACKABLE_QUANTS = ['q4_0', 'iq4_nl'];
  function shouldDisableMmap (line 26) | function shouldDisableMmap(modelPath: string): boolean {
  function hashString (line 30) | function hashString(str: string): string {
  function ensureSessionCacheDir (line 41) | async function ensureSessionCacheDir(cacheDir: string): Promise<void> {
  function getSessionPath (line 48) | function getSessionPath(cacheDir: string, promptHash: string): string {
  type ModelLoadParams (line 51) | interface ModelLoadParams {
  function buildModelParams (line 59) | function buildModelParams(
  type ContextInitResult (line 93) | interface ContextInitResult {
  constant GPU_INIT_TIMEOUT_MS (line 99) | const GPU_INIT_TIMEOUT_MS = 8000;
  constant HTP_INIT_TIMEOUT_MS (line 101) | const HTP_INIT_TIMEOUT_MS = 30000;
  function withTimeout (line 103) | function withTimeout<T>(promise: Promise<T>, ms: number, label: string):...
  function safeRelease (line 111) | async function safeRelease(ctx: LlamaContext | null): Promise<void> {
  function tryGpuInit (line 116) | async function tryGpuInit(promise: Promise<LlamaContext>, nGpuLayers: nu...
  function initContextWithFallback (line 126) | async function initContextWithFallback(
  type GpuInfo (line 190) | interface GpuInfo {
  function captureGpuInfo (line 197) | function captureGpuInfo(
  function supportsNativeThinking (line 209) | function supportsNativeThinking(context: LlamaContext | null): boolean {
  function buildThinkingCompletionParams (line 221) | function buildThinkingCompletionParams(enableThinking: boolean, isGemma4...
  function getStreamingDelta (line 226) | function getStreamingDelta(nextValue: string | undefined, previousValue:...
  function getModelMaxContext (line 233) | function getModelMaxContext(context: LlamaContext): number | null {
  function logContextMetadata (line 245) | function logContextMetadata(context: LlamaContext, contextLength: number...
  type MultimodalInitResult (line 251) | interface MultimodalInitResult {
  function initMultimodal (line 255) | async function initMultimodal(
  function checkContextMultimodal (line 281) | async function checkContextMultimodal(context: LlamaContext): Promise<Mu...
  function estimateTokens (line 293) | async function estimateTokens(context: LlamaContext, text: string): Prom...
  function fitMessagesInBudget (line 300) | async function fitMessagesInBudget(
  constant BYTES_PER_GB (line 328) | const BYTES_PER_GB = 1024 * 1024 * 1024;
  function getMaxContextForDevice (line 329) | function getMaxContextForDevice(totalMemoryBytes: number): number {
  constant ANDROID_GPU_LAYER_CAPS (line 336) | const ANDROID_GPU_LAYER_CAPS: { maxGB: number; layers: number }[] = [{ m...
  constant ANDROID_GPU_LAYERS_FALLBACK (line 337) | const ANDROID_GPU_LAYERS_FALLBACK = 24;
  function getGpuLayersForDevice (line 340) | function getGpuLayersForDevice(totalMemoryBytes: number, requestedLayers...
  constant STOP_TOKENS (line 353) | const STOP_TOKENS = ['</s>', '<|end|>', '<|eot_id|>'];
  function buildCompletionParams (line 354) | function buildCompletionParams(settings: {
  function recordGenerationStats (line 367) | function recordGenerationStats(

FILE: src/services/llmMessages.ts
  function formatLlamaMessages (line 4) | function formatLlamaMessages(messages: Message[], supportsVision: boolea...
  function extractImageUris (line 27) | function extractImageUris(messages: Message[]): string[] {
  function formatToolCallAsText (line 46) | function formatToolCallAsText(tc: { name: string; arguments: string }): ...
  function buildOAIMessages (line 51) | function buildOAIMessages(messages: Message[]): RNLlamaOAICompatibleMess...

FILE: src/services/llmSafetyChecks.ts
  constant GGUF_MAGIC (line 9) | const GGUF_MAGIC = 'GGUF';
  constant MIN_GGUF_FILE_SIZE (line 12) | const MIN_GGUF_FILE_SIZE = 1024;
  function validateModelFile (line 18) | async function validateModelFile(modelPath: string): Promise<{ valid: bo...
  function checkMemoryForModel (line 71) | async function checkMemoryForModel(
  function safeCompletion (line 109) | async function safeCompletion<T>(

FILE: src/services/llmToolGeneration.ts
  type ToolStreamCallback (line 13) | type ToolStreamCallback = (data: StreamToken) => void;
  type ToolCompleteCallback (line 14) | type ToolCompleteCallback = (fullResponse: string) => void;
  class ToolCallTokenFilter (line 22) | class ToolCallTokenFilter {
    method process (line 26) | process(token: string): string {
    method flush (line 31) | private flush(): string {
    method partialSuffix (line 70) | private partialSuffix(text: string, tag: string): number {
  function parseToolCall (line 78) | function parseToolCall(tc: any): ToolCall {
  type ToolGenerationDeps (line 87) | interface ToolGenerationDeps {
  function generateWithToolsImpl (line 99) | async function generateWithToolsImpl(

FILE: src/services/llmTypes.ts
  type MultimodalSupport (line 1) | interface MultimodalSupport {
  type LLMPerformanceSettings (line 6) | interface LLMPerformanceSettings {
  type LLMPerformanceStats (line 12) | interface LLMPerformanceStats {

FILE: src/services/localDreamGenerator.ts
  type ProgressCallback (line 19) | type ProgressCallback = (progress: ImageGenerationProgress) => void;
  type PreviewCallback (line 20) | type PreviewCallback = (preview: { previewPath: string; step: number; to...
  class LocalDreamGeneratorService (line 33) | class LocalDreamGeneratorService {
    method getEmitter (line 38) | private getEmitter(): NativeEventEmitter {
    method isAvailable (line 45) | isAvailable(): boolean {
    method isModelLoaded (line 49) | async isModelLoaded(): Promise<boolean> {
    method getLoadedModelPath (line 58) | async getLoadedModelPath(): Promise<string | null> {
    method loadModel (line 67) | async loadModel(modelPath: string, threads?: number, opts: { backend?:...
    method getLoadedThreads (line 92) | getLoadedThreads(): number | null {
    method unloadModel (line 96) | async unloadModel(): Promise<boolean> {
    method subscribeToProgress (line 109) | private subscribeToProgress(onProgress?: ProgressCallback, onPreview?:...
    method buildNativeParams (line 125) | private buildNativeParams(params: ImageGenerationParams & { previewInt...
    method buildResult (line 139) | private buildResult(params: ImageGenerationParams, result: any): Gener...
    method generateImage (line 154) | async generateImage(
    method cancelGeneration (line 191) | async cancelGeneration(): Promise<boolean> {
    method isGenerating (line 197) | async isGenerating(): Promise<boolean> {
    method getGeneratedImages (line 201) | async getGeneratedImages(): Promise<GeneratedImage[]> {
    method deleteGeneratedImage (line 221) | async deleteGeneratedImage(imageId: string): Promise<boolean> {
    method clearOpenCLCache (line 226) | async clearOpenCLCache(modelPath: string): Promise<number> {
    method hasKernelCache (line 231) | async hasKernelCache(modelPath: string): Promise<boolean> {
    method getConstants (line 236) | getConstants() {

FILE: src/services/modelManager/download.ts
  type PerformBackgroundDownloadOpts (line 23) | interface PerformBackgroundDownloadOpts {
  function performBackgroundDownload (line 32) | async function performBackgroundDownload(opts: PerformBackgroundDownload...
  function checkMmProjExists (line 52) | async function checkMmProjExists(path: string | null, expectedSize?: num...
  type AlreadyDownloadedOpts (line 71) | interface AlreadyDownloadedOpts {
  function handleAlreadyDownloaded (line 79) | async function handleAlreadyDownloaded(opts: AlreadyDownloadedOpts): Pro...
  type StartBgDownloadOpts (line 91) | interface StartBgDownloadOpts {
  function startBgDownload (line 103) | async function startBgDownload(opts: StartBgDownloadOpts): Promise<Backg...
  type WatchDownloadOpts (line 203) | interface WatchDownloadOpts {
  function watchBackgroundDownload (line 212) | function watchBackgroundDownload(opts: WatchDownloadOpts): void {

FILE: src/services/modelManager/downloadHelpers.ts
  function getOrphanedTextFiles (line 10) | async function getOrphanedTextFiles(
  function parseFileSize (line 40) | function parseFileSize(size: string | number): number {
  function calculateDirectorySize (line 44) | async function calculateDirectorySize(dirPath: string): Promise<number> {
  function isItemTracked (line 57) | function isItemTracked(itemPath: string, trackedPaths: string[]): boolean {
  function getOrphanedImageDirs (line 61) | async function getOrphanedImageDirs(
  type SyncDownloadsOpts (line 86) | interface SyncDownloadsOpts {
  function resolveMmProjPath (line 93) | async function resolveMmProjPath(metadata: PersistedDownloadInfo): Promi...
  function isMmProjStillRunning (line 108) | function isMmProjStillRunning(
  function processCompletedDownload (line 117) | async function processCompletedDownload(
  function handleFailedDownload (line 139) | function handleFailedDownload(
  function syncCompletedBackgroundDownloads (line 148) | async function syncCompletedBackgroundDownloads(opts: SyncDownloadsOpts)...

FILE: src/services/modelManager/imageSync.ts
  type SyncCompletedImageDownloadsOpts (line 7) | interface SyncCompletedImageDownloadsOpts {
  function isRecoverableImageDownload (line 15) | function isRecoverableImageDownload(metadata: PersistedDownloadInfo | un...
  function buildRecoveredImageModel (line 19) | function buildRecoveredImageModel(
  function recoverZipDownload (line 36) | async function recoverZipDownload(opts: {
  function recoverMultifileDownload (line 61) | async function recoverMultifileDownload(
  function syncCompletedImageDownloads (line 71) | async function syncCompletedImageDownloads(opts: SyncCompletedImageDownl...

FILE: src/services/modelManager/index.ts
  class ModelManager (line 45) | class ModelManager {
    method constructor (line 51) | constructor() {
    method resolveStoredPath (line 56) | private resolveStoredPath(p: string, d: string) { return resolveStored...
    method determineCredibility (line 57) | private determineCredibility(a: string) { return determineCredibility(...
    method isMMProjFile (line 58) | private isMMProjFile(f: string) { return isMMProjFile(f); }
    method initialize (line 60) | async initialize(): Promise<void> {
    method linkOrphanMmProj (line 68) | async linkOrphanMmProj(): Promise<void> {
    method getDownloadedModels (line 107) | async getDownloadedModels(): Promise<DownloadedModel[]> {
    method deleteModel (line 115) | async deleteModel(modelId: string): Promise<void> {
    method getModelPath (line 131) | async getModelPath(modelId: string): Promise<string | null> {
    method getStorageUsed (line 136) | async getStorageUsed(): Promise<number> {
    method getAvailableStorage (line 141) | async getAvailableStorage(): Promise<number> {
    method getOrphanedFiles (line 146) | async getOrphanedFiles(): Promise<Array<{ name: string; path: string; ...
    method deleteOrphanedFile (line 157) | async deleteOrphanedFile(filePath: string): Promise<void> {
    method setBackgroundDownloadMetadataCallback (line 161) | setBackgroundDownloadMetadataCallback(callback: BackgroundDownloadMeta...
    method isBackgroundDownloadSupported (line 165) | isBackgroundDownloadSupported(): boolean {
    method downloadModelBackground (line 169) | async downloadModelBackground(
    method watchDownload (line 188) | watchDownload(
    method cleanupCancelledTextArtifacts (line 203) | private async cleanupCancelledTextArtifacts(ctx: Extract<BackgroundDow...
    method cancelBackgroundDownload (line 218) | async cancelBackgroundDownload(downloadId: number): Promise<void> {
    method syncBackgroundDownloads (line 235) | async syncBackgroundDownloads(
    method syncCompletedImageDownloads (line 243) | async syncCompletedImageDownloads(
    method restoreInProgressDownloads (line 258) | async restoreInProgressDownloads(
    method getActiveBackgroundDownloads (line 273) | async getActiveBackgroundDownloads(): Promise<BackgroundDownloadInfo[]> {
    method startBackgroundDownloadPolling (line 277) | startBackgroundDownloadPolling(): void {
    method stopBackgroundDownloadPolling (line 281) | stopBackgroundDownloadPolling(): void {
    method repairMmProj (line 284) | async repairMmProj(
    method saveModelWithMmproj (line 330) | async saveModelWithMmproj(modelId: string, mmProjPath: string): Promis...
    method clearMmProjLink (line 344) | async clearMmProjLink(modelId: string): Promise<void> {
    method cleanupMMProjEntries (line 353) | async cleanupMMProjEntries(): Promise<number> {
    method importLocalModel (line 357) | async importLocalModel(opts: Omit<ImportLocalModelOpts, 'modelsDir'>):...
    method getDownloadedImageModels (line 362) | async getDownloadedImageModels(): Promise<ONNXImageModel[]> {
    method addDownloadedImageModel (line 370) | async addDownloadedImageModel(model: ONNXImageModel): Promise<void> {
    method deleteImageModel (line 378) | async deleteImageModel(modelId: string): Promise<void> {
    method getImageModelPath (line 390) | async getImageModelPath(modelId: string): Promise<string | null> {
    method getImageModelsStorageUsed (line 395) | async getImageModelsStorageUsed(): Promise<number> {
    method getImageModelsDirectory (line 400) | getImageModelsDirectory(): string {
    method scanForUntrackedImageModels (line 404) | async scanForUntrackedImageModels(): Promise<ONNXImageModel[]> {
    method scanForUntrackedTextModels (line 413) | async scanForUntrackedTextModels(): Promise<DownloadedModel[]> {
    method refreshModelLists (line 418) | async refreshModelLists(): Promise<{ textModels: DownloadedModel[]; im...

FILE: src/services/modelManager/restore.ts
  type RestoreDownloadsOpts (line 11) | interface RestoreDownloadsOpts {
  function isRestorable (line 20) | function isRestorable(download: BackgroundDownloadInfo): boolean {
  function resolveMmProjState (line 31) | async function resolveMmProjState(
  function buildFileInfo (line 60) | function buildFileInfo(metadata: PersistedDownloadInfo): ModelFile {
  type RestoreEntryOpts (line 74) | interface RestoreEntryOpts {
  function restoreDownloadEntry (line 84) | async function restoreDownloadEntry(opts: RestoreEntryOpts): Promise<voi...
  function restoreInProgressDownloads (line 158) | async function restoreInProgressDownloads(opts: RestoreDownloadsOpts): P...

FILE: src/services/modelManager/scan.ts
  function isMMProjFile (line 5) | function isMMProjFile(fileName: string): boolean {
  function parseSizeInt (line 12) | function parseSizeInt(size: string | number): number {
  function getDirSize (line 16) | async function getDirSize(dirPath: string): Promise<number> {
  function deleteOrphanedFile (line 25) | async function deleteOrphanedFile(filePath: string): Promise<void> {
  function looksLikeVisionModel (line 32) | function looksLikeVisionModel(model: DownloadedModel): boolean {
  function extractBaseName (line 39) | function extractBaseName(fileName: string): string {
  function findMatchingMmProj (line 44) | function findMatchingMmProj(
  function cleanupMMProjEntries (line 55) | async function cleanupMMProjEntries(modelsDir: string): Promise<number> {
  function detectBackend (line 88) | function detectBackend(dirName: string): 'mnn' | 'qnn' | 'coreml' {
  type ScanImageModelsOpts (line 94) | interface ScanImageModelsOpts {
  function scanForUntrackedImageModels (line 100) | async function scanForUntrackedImageModels(opts: ScanImageModelsOpts): P...
  function scanForUntrackedTextModels (line 134) | async function scanForUntrackedTextModels(
  function doScanForUntrackedTextModels (line 147) | async function doScanForUntrackedTextModels(
  type ImportLocalModelOpts (line 194) | interface ImportLocalModelOpts {
  function resolveUri (line 205) | function resolveUri(uri: string): string {
  function importLocalModel (line 215) | async function importLocalModel(opts: ImportLocalModelOpts): Promise<Dow...
  type CopyProgressOpts (line 275) | type CopyProgressOpts = { knownTotalBytes: number | null; onProgress?: (...
  function copyFileWithProgress (line 277) | async function copyFileWithProgress(

FILE: src/services/modelManager/storage.ts
  constant MODELS_STORAGE_KEY (line 6) | const MODELS_STORAGE_KEY = '@local_llm/downloaded_models';
  constant IMAGE_MODELS_STORAGE_KEY (line 7) | const IMAGE_MODELS_STORAGE_KEY = '@local_llm/downloaded_image_models';
  function determineCredibility (line 9) | function determineCredibility(author: string): ModelCredibility {
  function resolveStoredPath (line 44) | function resolveStoredPath(storedPath: string, currentBaseDir: string): ...
  function saveModelsList (line 57) | async function saveModelsList(models: DownloadedModel[]): Promise<void> {
  function saveImageModelsList (line 61) | async function saveImageModelsList(models: ONNXImageModel[]): Promise<vo...
  function tryResolveTextModelPath (line 65) | async function tryResolveTextModelPath(
  function tryResolveMmProjPath (line 79) | async function tryResolveMmProjPath(
  function loadDownloadedModels (line 96) | async function loadDownloadedModels(modelsDir: string): Promise<Download...
  function tryResolveImageModelPath (line 125) | async function tryResolveImageModelPath(
  function loadDownloadedImageModels (line 139) | async function loadDownloadedImageModels(imageModelsDir: string): Promis...
  type BuildModelOpts (line 166) | interface BuildModelOpts {
  function buildDownloadedModel (line 173) | async function buildDownloadedModel(opts: BuildModelOpts): Promise<Downl...
  function persistDownloadedModel (line 205) | async function persistDownloadedModel(

FILE: src/services/modelManager/types.ts
  type DownloadProgressCallback (line 3) | type DownloadProgressCallback = (progress: DownloadProgress) => void;
  type DownloadCompleteCallback (line 4) | type DownloadCompleteCallback = (model: DownloadedModel) => void;
  type DownloadErrorCallback (line 5) | type DownloadErrorCallback = (error: Error) => void;
  type BackgroundDownloadMetadataCallback (line 8) | type BackgroundDownloadMetadataCallback = (
  type BackgroundDownloadContext (line 24) | type BackgroundDownloadContext =

FILE: src/services/networkDiscovery.ts
  type DiscoveredServer (line 12) | interface DiscoveredServer {
  constant PROVIDERS (line 18) | const PROVIDERS = [
  constant TIMEOUT_MS (line 23) | const TIMEOUT_MS = 500;
  constant BATCH_SIZE (line 24) | const BATCH_SIZE = 50;
  constant BATCH_DELAY_MS (line 25) | const BATCH_DELAY_MS = 50;
  function probe (line 28) | async function probe(ip: string, port: number, path: string): Promise<bo...
  function runBatch (line 40) | async function runBatch<T>(tasks: (() => Promise<T>)[]): Promise<T[]> {
  function subnetBase (line 53) | function subnetBase(ip: string): string | null {
  constant FALLBACK_SUBNETS (line 65) | const FALLBACK_SUBNETS = ['192.168.1', '192.168.0'];
  function findReachableSubnet (line 72) | async function findReachableSubnet(subnets: string[]): Promise<string | ...
  function discoverLANServers (line 110) | async function discoverLANServers(): Promise<DiscoveredServer[]> {

FILE: src/services/pdfExtractor.ts
  class PDFExtractor (line 10) | class PDFExtractor {
    method isAvailable (line 14) | isAvailable(): boolean {
    method extractText (line 22) | async extractText(filePath: string, maxChars: number = 50000): Promise...

FILE: src/services/providers/localProvider.ts
  function getLocalCapabilities (line 20) | function getLocalCapabilities(): ProviderCapabilities {
  class LocalProvider (line 35) | class LocalProvider implements LLMProvider {
    method capabilities (line 41) | get capabilities(): ProviderCapabilities {
    method loadModel (line 45) | async loadModel(modelId: string): Promise<void> {
    method unloadModel (line 54) | async unloadModel(): Promise<void> {
    method isModelLoaded (line 60) | isModelLoaded(): boolean {
    method getLoadedModelId (line 64) | getLoadedModelId(): string | null {
    method generate (line 68) | async generate(
    method generateSimple (line 108) | private async generateSimple(
    method generateWithTools (line 142) | private async generateWithTools(
    method stopGeneration (line 185) | async stopGeneration(): Promise<void> {
    method getTokenCount (line 189) | async getTokenCount(text: string): Promise<number> {
    method isReady (line 197) | async isReady(): Promise<boolean> {
    method dispose (line 201) | async dispose(): Promise<void> {

FILE: src/services/providers/openAICompatibleProvider.ts
  function isOllamaEndpoint (line 29) | function isOllamaEndpoint(endpoint: string): boolean {
  function isLMStudioEndpoint (line 34) | function isLMStudioEndpoint(endpoint: string): boolean {
  class OpenAICompatibleProvider (line 41) | class OpenAICompatibleProvider implements LLMProvider {
    method constructor (line 48) | constructor(
    method capabilities (line 60) | get capabilities(): ProviderCapabilities {
    method updateConfig (line 64) | updateConfig(config: Partial<OpenAIConfig>): void {
    method loadModel (line 68) | async loadModel(modelId: string): Promise<void> {
    method updateCapabilities (line 76) | updateCapabilities(capabilities: Partial<ProviderCapabilities>): void {
    method unloadModel (line 80) | async unloadModel(): Promise<void> {
    method isModelLoaded (line 85) | isModelLoaded(): boolean { return !!this.config.modelId; }
    method getLoadedModelId (line 87) | getLoadedModelId(): string | null { return this.config.modelId || null; }
    method buildRequestBody (line 92) | private buildRequestBody(
    method generate (line 111) | async generate(
    method buildOpenAIMessages (line 226) | private buildOpenAIMessages(
    method stopGeneration (line 233) | async stopGeneration(): Promise<void> {
    method getTokenCount (line 240) | async getTokenCount(text: string): Promise<number> {
    method isReady (line 246) | async isReady(): Promise<boolean> {
    method dispose (line 250) | async dispose(): Promise<void> {
  function createOpenAIProvider (line 259) | function createOpenAIProvider(

FILE: src/services/providers/openAICompatibleStream.ts
  class ThinkTagParser (line 20) | class ThinkTagParser {
    method process (line 24) | process(content: string, onToken: (t: string) => void, onReasoning: (t...
    method handleOutsideThink (line 33) | private handleOutsideThink(openTag: string, onToken: (t: string) => vo...
    method handleInsideThink (line 56) | private handleInsideThink(closeTag: string, onReasoning: (t: string) =...
    method flush (line 75) | private flush(onToken: (t: string) => void, onReasoning: (t: string) =...
    method partialSuffix (line 87) | private partialSuffix(text: string, tag: string): number {
  type DeltaCtx (line 96) | interface DeltaCtx {
  type DeltaShape (line 102) | type DeltaShape = {
  function processToolCallChunk (line 113) | function processToolCallChunk(
  function processDelta (line 133) | function processDelta(
  function buildOllamaCompletion (line 169) | function buildOllamaCompletion(
  type OllamaStreamState (line 186) | interface OllamaStreamState {
  function handleOllamaChatLine (line 195) | function handleOllamaChatLine(
  function generateOllamaChatImpl (line 237) | async function generateOllamaChatImpl(

FILE: src/services/providers/openAICompatibleTypes.ts
  type OpenAIChatMessage (line 7) | interface OpenAIChatMessage {
  type OpenAIContentPart (line 16) | interface OpenAIContentPart {
  type OpenAIToolCall (line 26) | interface OpenAIToolCall {
  type OpenAIConfig (line 36) | interface OpenAIConfig {
  type OpenAIStreamState (line 43) | interface OpenAIStreamState {
  type OllamaChatRequest (line 53) | interface OllamaChatRequest {

FILE: src/services/providers/openAIMessageBuilder.ts
  function buildVisionContent (line 14) | async function buildVisionContent(
  function buildAssistantToolCallMessage (line 36) | function buildAssistantToolCallMessage(msg: Message): OpenAIChatMessage {
  function buildOpenAIMessagesImpl (line 52) | async function buildOpenAIMessagesImpl(

FILE: src/services/providers/registry.ts
  type ProviderChangeListener (line 12) | type ProviderChangeListener = (providerId: string | null) => void;
  class ProviderRegistry (line 14) | class ProviderRegistry {
    method constructor (line 19) | constructor() {
    method registerProvider (line 27) | registerProvider(id: string, provider: LLMProvider): void {
    method unregisterProvider (line 35) | unregisterProvider(id: string): void {
    method getProvider (line 54) | getProvider(id: string): LLMProvider | undefined {
    method getActiveProvider (line 63) | getActiveProvider(): LLMProvider {
    method getActiveProviderId (line 75) | getActiveProviderId(): string {
    method setActiveProvider (line 82) | setActiveProvider(id: string): boolean {
    method hasProvider (line 97) | hasProvider(id: string): boolean {
    method getProviderIds (line 104) | getProviderIds(): string[] {
    method subscribe (line 111) | subscribe(listener: ProviderChangeListener): () => void {
    method notifyListeners (line 119) | private notifyListeners(): void {
    method clear (line 127) | clear(): void {
  function getProviderForServer (line 148) | function getProviderForServer(serverId: string | null): LLMProvider {

FILE: src/services/providers/types.ts
  type ProviderType (line 11) | type ProviderType = 'local' | 'openai-compatible' | 'anthropic';
  type ProviderCapabilities (line 14) | interface ProviderCapabilities {
  type CompletionResult (line 28) | interface CompletionResult {
  type ToolCallResult (line 40) | interface ToolCallResult {
  type GenerationOptions (line 50) | interface GenerationOptions {
  type ToolDefinition (line 74) | interface ToolDefinition {
  type StreamCallbacks (line 89) | interface StreamCallbacks {
  type ModelLoadState (line 101) | interface ModelLoadState {
  type LLMProvider (line 118) | interface LLMProvider {
  type ProviderFactory (line 175) | type ProviderFactory = (config: ProviderConfig) => LLMProvider;
  type ProviderConfig (line 178) | interface ProviderConfig {

FILE: src/services/rag/chunking.ts
  type ChunkOptions (line 1) | interface ChunkOptions {
  type Chunk (line 7) | interface Chunk {
  constant DEFAULT_CHUNK_SIZE (line 12) | const DEFAULT_CHUNK_SIZE = 500;
  constant DEFAULT_OVERLAP (line 13) | const DEFAULT_OVERLAP = 100;
  constant DEFAULT_MIN_CHUNK_LENGTH (line 14) | const DEFAULT_MIN_CHUNK_LENGTH = 20;
  function slidingWindowChunks (line 16) | function slidingWindowChunks(
  function flushChunk (line 33) | function flushChunk(
  function chunkDocument (line 44) | function chunkDocument(text: string, options?: ChunkOptions): Chunk[] {

FILE: src/services/rag/database.ts
  type RagDocument (line 6) | interface RagDocument {
  type RagSearchResult (line 16) | interface RagSearchResult {
  type StoredEmbedding (line 24) | interface StoredEmbedding {
  class RagDatabase (line 33) | class RagDatabase {
    method ensureReady (line 37) | async ensureReady(): Promise<void> {
    method getDb (line 78) | private getDb(): DB {
    method insertDocument (line 83) | insertDocument(doc: { projectId: string; name: string; path: string; s...
    method insertChunks (line 93) | insertChunks(docId: number, chunks: Chunk[]): number[] {
    method embeddingToBlob (line 114) | private embeddingToBlob(embedding: number[]): ArrayBuffer {
    method blobToEmbedding (line 118) | private blobToEmbedding(blob: any): number[] {
    method insertEmbeddingsBatch (line 124) | insertEmbeddingsBatch(entries: { chunkRowid: number; docId: number; em...
    method getEmbeddingsByProject (line 141) | getEmbeddingsByProject(projectId: string): StoredEmbedding[] {
    method hasEmbeddingsForDocument (line 157) | hasEmbeddingsForDocument(docId: number): boolean {
    method getChunksByDocument (line 167) | getChunksByDocument(docId: number): { id: number; content: string; pos...
    method deleteDocument (line 176) | deleteDocument(docId: number): void {
    method getDocumentsByProject (line 183) | getDocumentsByProject(projectId: string): RagDocument[] {
    method toggleEnabled (line 192) | toggleEnabled(docId: number, enabled: boolean): void {
    method getChunksByProject (line 197) | getChunksByProject(projectId: string, topK: number = 5): RagSearchResu...
    method deleteDocumentsByProject (line 209) | deleteDocumentsByProject(projectId: string): void {

FILE: src/services/rag/embedding.ts
  constant EMBEDDING_MODEL_FILENAME (line 6) | const EMBEDDING_MODEL_FILENAME = 'all-MiniLM-L6-v2-Q8_0.gguf';
  constant EMBEDDING_DIMENSION (line 7) | const EMBEDDING_DIMENSION = 384;
  constant EMBEDDING_CTX_SIZE (line 8) | const EMBEDDING_CTX_SIZE = 512;
  class EmbeddingService (line 10) | class EmbeddingService {
    method load (line 14) | async load(): Promise<void> {
    method doLoad (line 26) | private async doLoad(): Promise<void> {
    method ensureModelCopied (line 42) | private async ensureModelCopied(): Promise<string> {
    method embed (line 57) | async embed(text: string): Promise<number[]> {
    method embedBatch (line 76) | async embedBatch(texts: string[]): Promise<number[][]> {
    method unload (line 84) | async unload(): Promise<void> {
    method isLoaded (line 96) | isLoaded(): boolean {
    method getDimension (line 100) | getDimension(): number {

FILE: src/services/rag/index.ts
  type IndexProgress (line 15) | interface IndexProgress {
  type IndexDocumentParams (line 20) | interface IndexDocumentParams {
  class RagService (line 28) | class RagService {
    method ensureReady (line 29) | async ensureReady(): Promise<void> {
    method indexDocument (line 33) | async indexDocument(params: IndexDocumentParams): Promise<number> {
    method backfillEmbeddings (line 82) | async backfillEmbeddings(projectId: string): Promise<number> {
    method deleteDocument (line 113) | async deleteDocument(docId: number): Promise<void> {
    method getDocumentsByProject (line 118) | async getDocumentsByProject(projectId: string) {
    method toggleDocument (line 123) | async toggleDocument(docId: number, enabled: boolean): Promise<void> {
    method searchProject (line 128) | async searchProject(projectId: string, query: string, contextLength?: ...
    method deleteProjectDocuments (line 136) | async deleteProjectDocuments(projectId: string): Promise<void> {

FILE: src/services/rag/retrieval.ts
  function stripAngleBracketTags (line 7) | function stripAngleBracketTags(text: string): string {
  type SearchResult (line 18) | interface SearchResult {
  class RetrievalService (line 23) | class RetrievalService {
    method search (line 24) | async search(projectId: string, query: string, topK: number = 5): Prom...
    method searchSemantic (line 29) | private async searchSemantic(projectId: string, query: string, topK: n...
    method formatForPrompt (line 68) | formatForPrompt(result: SearchResult): string {
    method estimateCharBudget (line 81) | estimateCharBudget(contextLengthTokens: number): number {
    method searchWithBudget (line 86) | async searchWithBudget(params: { projectId: string; query: string; con...

FILE: src/services/rag/vectorMath.ts
  function dotProduct (line 1) | function dotProduct(a: number[], b: number[]): number {
  function cosineSimilarity (line 9) | function cosineSimilarity(a: number[], b: number[]): number {
  type SimilarityResult (line 23) | interface SimilarityResult {
  function topKSimilar (line 28) | function topKSimilar(queryVec: number[], candidates: number[][], k: numb...

FILE: src/services/remoteServerManager.ts
  class RemoteServerManager (line 25) | class RemoteServerManager {
    method addServer (line 29) | async addServer(
    method updateServer (line 61) | async updateServer(
    method removeServer (line 96) | async removeServer(id: string): Promise<void> {
    method getServers (line 104) | getServers(): RemoteServer[] {
    method getServer (line 109) | getServer(id: string): RemoteServer | null {
    method getServerWithApiKey (line 114) | async getServerWithApiKey(id: string): Promise<(RemoteServer & { apiKe...
    method testConnection (line 124) | async testConnection(
    method testConnectionByEndpoint (line 132) | async testConnectionByEndpoint(
    method discoverModels (line 142) | async discoverModels(id: string): Promise<RemoteModel[]> {
    method setActiveServer (line 153) | setActiveServer(id: string | null): void {
    method setActiveRemoteTextModel (line 160) | async setActiveRemoteTextModel(serverId: string, modelId: string): Pro...
    method setActiveRemoteImageModel (line 165) | async setActiveRemoteImageModel(serverId: string, modelId: string): Pr...
    method clearActiveRemoteModel (line 172) | clearActiveRemoteModel(): void {
    method getActiveServer (line 182) | getActiveServer(): RemoteServer | null {
    method initializeProviders (line 191) | async initializeProviders(): Promise<void> {
    method clearAllServers (line 198) | async clearAllServers(): Promise<void> {
    method storeApiKey (line 210) | async storeApiKey(serverId: string, apiKey: string): Promise<void> {
    method getApiKey (line 214) | async getApiKey(serverId: string): Promise<string | null> {
    method removeApiKey (line 218) | private async removeApiKey(serverId: string): Promise<void> {

FILE: src/services/remoteServerManagerUtils.ts
  constant KEYCHAIN_SERVICE (line 12) | const KEYCHAIN_SERVICE = 'ai.offgridmobile.servers';
  function storeApiKeyImpl (line 18) | async function storeApiKeyImpl(serverId: string, apiKey: string): Promis...
  function getApiKeyImpl (line 35) | async function getApiKeyImpl(serverId: string): Promise<string | null> {
  function removeApiKeyImpl (line 47) | async function removeApiKeyImpl(serverId: string): Promise<void> {
  function detectVisionCapability (line 60) | function detectVisionCapability(modelId: string): boolean {
  function detectToolCallingCapability (line 71) | function detectToolCallingCapability(modelId: string): boolean {
  function createProviderForServerImpl (line 86) | async function createProviderForServerImpl(server: RemoteServer): Promis...
  function setActiveRemoteTextModelImpl (line 97) | async function setActiveRemoteTextModelImpl(
  function setActiveRemoteImageModelImpl (line 138) | async function setActiveRemoteImageModelImpl(
  function initializeProvidersImpl (line 169) | async function initializeProvidersImpl(

FILE: src/services/tools/handlers.ts
  function makeResult (line 6) | function makeResult(call: ToolCall, start: number, opts: { content: stri...
  function requireString (line 9) | function requireString(call: ToolCall, param: string): string | null {
  function executeToolCall (line 14) | async function executeToolCall(call: ToolCall): Promise<ToolResult> {
  function dispatchTool (line 25) | async function dispatchTool(call: ToolCall): Promise<string> {
  function handleWebSearch (line 53) | async function handleWebSearch(query: string): Promise<string> {
  type SearchResult (line 85) | type SearchResult = { title: string; snippet: string; url?: string };
  function stripHtmlTags (line 87) | function stripHtmlTags(html: string): string {
  function parseResultBlock (line 98) | function parseResultBlock(block: string): SearchResult | null {
  function parseBraveResults (line 116) | function parseBraveResults(html: string): SearchResult[] {
  function decodeHTMLEntities (line 140) | function decodeHTMLEntities(text: string): string {
  function evaluateExpression (line 160) | function evaluateExpression(expr: string): number {
  function handleCalculator (line 221) | function handleCalculator(expression: string): string {
  function handleGetDatetime (line 236) | function handleGetDatetime(timezone?: string): string {
  function collectDeviceSection (line 254) | async function collectDeviceSection(
  function handleGetDeviceInfo (line 260) | async function handleGetDeviceInfo(infoType = 'all'): Promise<string> {
  function isPrivateUrl (line 299) | function isPrivateUrl(url: string): boolean {
  function handleReadUrl (line 307) | async function handleReadUrl(rawUrl: string): Promise<string> {
  function handleSearchKnowledgeBase (line 336) | async function handleSearchKnowledgeBase(query: string, projectId?: stri...
  function formatBytes (line 346) | function formatBytes(bytes: number): string {

FILE: src/services/tools/registry.ts
  constant AVAILABLE_TOOLS (line 3) | const AVAILABLE_TOOLS: ToolDefinition[] = [
  function getToolsAsOpenAISchema (line 91) | function getToolsAsOpenAISchema(enabledToolIds: string[]) {
  function buildToolSystemPromptHint (line 119) | function buildToolSystemPromptHint(enabledToolIds: string[]): string {

FILE: src/services/tools/types.ts
  type ToolDefinition (line 1) | interface ToolDefinition {
  type ToolParameter (line 11) | interface ToolParameter {
  type ToolCall (line 18) | interface ToolCall {
  type ToolResult (line 25) | interface ToolResult {

FILE: src/services/voiceService.ts
  type VoiceEventCallbacks (line 10) | type VoiceEventCallbacks = {
  class VoiceService (line 18) | class VoiceService {
    method initialize (line 22) | async initialize(): Promise<boolean> {
    method requestPermissions (line 60) | async requestPermissions(): Promise<boolean> {
    method setCallbacks (line 82) | setCallbacks(callbacks: VoiceEventCallbacks) {
    method startListening (line 111) | async startListening(): Promise<void> {
    method stopListening (line 121) | async stopListening(): Promise<void> {
    method cancelListening (line 130) | async cancelListening(): Promise<void> {
    method destroy (line 139) | async destroy(): Promise<void> {
    method isRecognizing (line 148) | async isRecognizing(): Promise<boolean> {

FILE: src/services/whisperService.ts
  type TranscriptionResult (line 7) | interface TranscriptionResult {
  type TranscriptionCallback (line 13) | type TranscriptionCallback = (result: TranscriptionResult) => void;
  constant WHISPER_MODELS (line 15) | const WHISPER_MODELS = [
  class WhisperService (line 23) | class WhisperService {
    method getModelsDir (line 33) | getModelsDir(): string { return `${RNFS.DocumentDirectoryPath}/whisper...
    method ensureModelsDirExists (line 34) | async ensureModelsDirExists(): Promise<void> {
    method getModelPath (line 38) | getModelPath(modelId: string): string { return `${this.getModelsDir()}...
    method isModelDownloaded (line 39) | async isModelDownloaded(modelId: string): Promise<boolean> { return RN...
    method downloadModel (line 41) | async downloadModel(modelId: string, onProgress?: (progress: number) =...
    method deleteModel (line 85) | async deleteModel(modelId: string): Promise<void> {
    method validateModelFile (line 107) | async validateModelFile(modelPath: string): Promise<void> {
    method loadModel (line 131) | async loadModel(modelPath: string): Promise<void> {
    method unloadModel (line 156) | async unloadModel(): Promise<void> {
    method isModelLoaded (line 172) | isModelLoaded(): boolean { return this.context !== null; }
    method getLoadedModelPath (line 173) | getLoadedModelPath(): string | null { return this.currentModelPath; }
    method requestPermissions (line 175) | async requestPermissions(): Promise<boolean> {
    method startRealtimeTranscription (line 208) | async startRealtimeTranscription(
    method stopTranscription (line 307) | async stopTranscription(): Promise<void> {
    method forceReset (line 333) | forceReset(): void {
    method isCurrentlyTranscribing (line 345) | isCurrentlyTranscribing(): boolean { return this.isTranscribing; }
    method transcribeFile (line 348) | async transcribeFile(

FILE: src/stores/appStore.ts
  type DownloadProgressInfo (line 7) | type DownloadProgressInfo = {
  type OnboardingChecklist (line 17) | type OnboardingChecklist = {
  type AppSettings (line 22) | type AppSettings = {
  type ThemeMode (line 36) | type ThemeMode = 'system' | 'light' | 'dark';
  type AppState (line 38) | interface AppState {
  constant DEFAULT_CHECKLIST (line 108) | const DEFAULT_CHECKLIST: OnboardingChecklist = {
  constant DEFAULT_SETTINGS (line 113) | const DEFAULT_SETTINGS: AppSettings = {
  function migrateEnabledTools (line 143) | function migrateEnabledTools(merged: any): void {
  function migratePersistedState (line 148) | function migratePersistedState(persistedState: any, currentState: AppSta...

FILE: src/stores/authStore.ts
  type AuthState (line 5) | interface AuthState {
  constant MAX_FAILED_ATTEMPTS (line 22) | const MAX_FAILED_ATTEMPTS = 5;
  constant LOCKOUT_DURATION (line 23) | const LOCKOUT_DURATION = 5 * 60 * 1000;

FILE: src/stores/chatStore.ts
  function nextUpdatedAt (line 8) | function nextUpdatedAt(previousUpdatedAt?: string): string {
  function updateMessageInConv (line 17) | function updateMessageInConv(
  function sliceThinkingBlock (line 30) | function sliceThinkingBlock(
  function extractChannelThinking (line 47) | function extractChannelThinking(rawContent: string): { reasoningContent:...
  function deriveTitle (line 58) | function deriveTitle(currentTitle: string, role: string, content: string...
  function mapConversation (line 65) | function mapConversation(
  type ChatState (line 73) | interface ChatState {

FILE: src/stores/debugLogsStore.ts
  type DebugLogEntry (line 3) | interface DebugLogEntry {
  type DebugLogsState (line 9) | interface DebugLogsState {

FILE: src/stores/projectStore.ts
  type ProjectState (line 9) | interface ProjectState {
  constant DEFAULT_PROJECTS (line 21) | const DEFAULT_PROJECTS: Project[] = [

FILE: src/stores/remoteModelCapabilities.ts
  type RemoteModelInfo (line 11) | interface RemoteModelInfo {
  function parseModelInfoKeys (line 18) | function parseModelInfoKeys(modelInfo: Record<string, unknown>): { conte...
  function parseNumCtx (line 33) | function parseNumCtx(parameters: string): number {
  function extractOllamaCapabilities (line 42) | function extractOllamaCapabilities(data: Record<string, unknown>): Remot...
  function fetchRemoteModelInfo (line 75) | async function fetchRemoteModelInfo(
  function fetchLmStudioModelInfo (line 108) | async function fetchLmStudioModelInfo(
  function deltaHasThinking (line 177) | function deltaHasThinking(delta: Record<string, unknown>): boolean {
  function probeLmStudioThinking (line 185) | async function probeLmStudioThinking(endpoint: string, modelId: string):...
  function hasRealData (line 229) | function hasRealData(info: RemoteModelInfo): boolean {
  function fetchModelCapabilities (line 238) | async function fetchModelCapabilities(
  function isGenerativeModel (line 260) | function isGenerativeModel(modelId: string): boolean {

FILE: src/stores/remoteServerHelpers.ts
  constant DISCOVERY_FETCH_TIMEOUT_MS (line 22) | const DISCOVERY_FETCH_TIMEOUT_MS = 5000;
  function testServerConnection (line 24) | async function testServerConnection(server: RemoteServer): Promise<Serve...
  function testEndpointAndGetModels (line 59) | async function testEndpointAndGetModels(
  function fetchModelsFromServer (line 103) | async function fetchModelsFromServer(server: RemoteServer): Promise<Remo...

FILE: src/stores/remoteServerStore.ts
  type RemoteServerState (line 25) | interface RemoteServerState {

FILE: src/stores/whisperStore.ts
  type WhisperState (line 6) | interface WhisperState {

FILE: src/theme/index.ts
  type ThemeMode (line 16) | type ThemeMode = 'system' | 'light' | 'dark';
  type Theme (line 18) | interface Theme {
  function getTheme (line 26) | function getTheme(mode: 'light' | 'dark'): Theme {
  function useTheme (line 35) | function useTheme(): Theme {

FILE: src/theme/palettes.ts
  type ThemeColors (line 3) | type ThemeColors = typeof COLORS_LIGHT;
  type ShadowStyle (line 5) | interface ShadowStyle {
  type ThemeShadows (line 9) | type ThemeShadows = {
  constant COLORS_LIGHT (line 17) | const COLORS_LIGHT = {
  constant COLORS_DARK (line 54) | const COLORS_DARK = {
  constant SHADOWS_LIGHT (line 93) | const SHADOWS_LIGHT: ThemeShadows = {
  constant SHADOWS_DARK (line 109) | const SHADOWS_DARK: ThemeShadows = {
  function createElevation (line 125) | function createElevation(colors: ThemeColors) {

FILE: src/theme/useThemedStyles.ts
  function useThemedStyles (line 15) | function useThemedStyles<T extends StyleSheet.NamedStyles<T>>(

FILE: src/types/index.ts
  type ModelCategory (line 2) | type ModelCategory = 'text-generation' | 'image-generation' | 'vision' |...
  type ModelSource (line 4) | type ModelSource = 'lmstudio' | 'official' | 'verified-quantizer' | 'com...
  type ModelCredibility (line 6) | interface ModelCredibility {
  type ModelInfo (line 13) | interface ModelInfo {
  type ModelFile (line 29) | interface ModelFile {
  type DownloadedModel (line 44) | interface DownloadedModel {
  type PersistedDownloadInfo (line 61) | interface PersistedDownloadInfo {
  type DownloadProgress (line 82) | interface DownloadProgress {
  type SoCVendor (line 91) | type SoCVendor = 'qualcomm' | 'mediatek' | 'exynos' | 'tensor' | 'apple'...
  type SoCInfo (line 92) | interface SoCInfo {
  type ImageModelRecommendation (line 99) | interface ImageModelRecommendation {
  type DeviceInfo (line 110) | interface DeviceInfo {
  type ModelRecommendation (line 120) | interface ModelRecommendation {
  type MediaAttachment (line 128) | interface MediaAttachment {
  type GenerationMeta (line 143) | interface GenerationMeta {
  type Message (line 170) | interface Message {
  type Conversation (line 194) | interface Conversation {
  type OnboardingStep (line 207) | interface OnboardingStep {
  type HFModelSearchResult (line 215) | interface HFModelSearchResult {
  type HFModelFile (line 236) | interface HFModelFile {
  type ImageGenerationModel (line 248) | interface ImageGenerationModel {
  type ONNXImageModel (line 261) | interface ONNXImageModel {
  type ImageGenerationState (line 274) | interface ImageGenerationState {
  type ImageGenerationMode (line 282) | type ImageGenerationMode = 'auto' | 'manual';
  type AutoDetectMethod (line 283) | type AutoDetectMethod = 'pattern' | 'llm';
  type ModelLoadingStrategy (line 284) | type ModelLoadingStrategy = 'performance' | 'memory';
  type CacheType (line 285) | type CacheType = 'f16' | 'q8_0' | 'q4_0';
  type InferenceBackend (line 286) | type InferenceBackend = 'cpu' | 'opencl' | 'htp' | 'metal';
  constant INFERENCE_BACKENDS (line 287) | const INFERENCE_BACKENDS = {
  type ImageModeState (line 294) | type ImageModeState = 'auto' | 'force' | 'disabled';
  type GeneratedImage (line 296) | interface GeneratedImage {
  type ImageGenerationParams (line 310) | interface ImageGenerationParams {
  type ImageGenerationProgress (line 320) | interface ImageGenerationProgress {
  type Project (line 325) | interface Project {
  type BackgroundDownloadStatus (line 334) | type BackgroundDownloadStatus =
  type BackgroundDownloadReasonCode (line 344) | type BackgroundDownloadReasonCode =
  type BackgroundDownloadInfo (line 360) | interface BackgroundDownloadInfo {
  type DebugInfo (line 375) | interface DebugInfo {
  type AppScreen (line 383) | type AppScreen = 'onboarding' | 'home' | 'models' | 'chat' | 'settings' ...

FILE: src/types/remoteServer.ts
  type RemoteProviderType (line 9) | type RemoteProviderType = 'openai-compatible' | 'anthropic';
  type RemoteServer (line 12) | interface RemoteServer {
  type RemoteModel (line 34) | interface RemoteModel {
  type RemoteModelCapabilities (line 50) | interface RemoteModelCapabilities {
  type ServerTestResult (line 64) | interface ServerTestResult {
  type ServerInfo (line 78) | interface ServerInfo {
  type RemoteGenerationSettings (line 88) | interface RemoteGenerationSettings {
  constant DEFAULT_REMOTE_GENERATION_SETTINGS (line 100) | const DEFAULT_REMOTE_GENERATION_SETTINGS: RemoteGenerationSettings = {
  type SelectableModel (line 108) | interface SelectableModel {

FILE: src/types/whisper.rn.d.ts
  type WhisperContextOptions (line 2) | interface WhisperContextOptions {
  type TranscribeOptions (line 10) | interface TranscribeOptions {
  type TranscribeRealtimeOptions (line 16) | interface TranscribeRealtimeOptions {
  type TranscribeResult (line 25) | interface TranscribeResult {
  type RealtimeTranscribeEvent (line 29) | interface RealtimeTranscribeEvent {
  type WhisperContext (line 38) | interface WhisperContext {

FILE: src/utils/coreMLModelUtils.ts
  function resolveCoreMLModelDir (line 11) | async function resolveCoreMLModelDir(modelDir: string): Promise<string> {
  function downloadCoreMLTokenizerFiles (line 32) | async function downloadCoreMLTokenizerFiles(modelDir: string, repo: stri...

FILE: src/utils/downloadErrors.ts
  function getReasonMessageFromCode (line 3) | function getReasonMessageFromCode(reasonCode?: BackgroundDownloadReasonC...
  function getLegacyMessage (line 46) | function getLegacyMessage(reason?: string | null): string {
  function isRetryableError (line 98) | function isRetryableError(
  function getUserFacingDownloadMessage (line 121) | function getUserFacingDownloadMessage(
  constant NETWORK_LOST_LABEL (line 128) | const NETWORK_LOST_LABEL = 'Network connection lost - waiting to resume....
  constant SIMPLE_STATUS_LABELS (line 130) | const SIMPLE_STATUS_LABELS: Partial<Record<string, string>> = {
  function getPendingLabel (line 138) | function getPendingLabel(
  function getRetryingLabel (line 149) | function getRetryingLabel(
  function getDownloadStatusLabel (line 161) | function getDownloadStatusLabel(

FILE: src/utils/generateId.ts
  function generateId (line 6) | function generateId(): string {
  function generateRandomSeed (line 23) | function generateRandomSeed(): number {

FILE: src/utils/haptics.ts
  type HapticType (line 3) | type HapticType =
  function triggerHaptic (line 17) | function triggerHaptic(type: HapticType): void {

FILE: src/utils/logger.ts
  constant LOG_FILE_NAME (line 4) | const LOG_FILE_NAME = 'download-debug.log';
  constant MAX_LOG_FILE_BYTES (line 5) | const MAX_LOG_FILE_BYTES = 2 * 1024 * 1024;
  constant RETAINED_LOG_LINES (line 6) | const RETAINED_LOG_LINES = 4000;
  function getLogFilePath (line 10) | function getLogFilePath(): string {
  function formatArg (line 14) | function formatArg(arg: unknown): string {
  function appendPersistentLog (line 27) | function appendPersistentLog(level: 'log' | 'warn' | 'error', args: unkn...
  function capture (line 53) | function capture(level: 'log' | 'warn' | 'error', args: unknown[]): void {

FILE: src/utils/messageContent.ts
  constant CONTROL_TOKEN_PATTERNS (line 1) | const CONTROL_TOKEN_PATTERNS: RegExp[] = [
  constant CHANNEL_ANALYSIS_START (line 17) | const CHANNEL_ANALYSIS_START = /<\|channel\|>analysis<\|message\|>/gi;
  constant CHANNEL_FINAL_START (line 18) | const CHANNEL_FINAL_START = /<\|channel\|>final<\|message\|>/gi;
  constant GEMMA4_THINK_OPEN (line 21) | const GEMMA4_THINK_OPEN = /<\|channel>thought\n/gi;
  constant GEMMA4_THINK_CLOSE (line 22) | const GEMMA4_THINK_CLOSE = /<channel\|>/gi;
  function stripControlTokens (line 29) | function stripControlTokens(content: string): string {
  function stripStreamingControlTokens (line 44) | function stripStreamingControlTokens(content: string): string {

FILE: src/utils/network.ts
  function isPrivateIPv4 (line 4) | function isPrivateIPv4(ip: string): boolean {
  function isIPv6 (line 17) | function isIPv6(ip: string): boolean {
  function isOnLocalNetwork (line 25) | async function isOnLocalNetwork(): Promise<boolean> {

FILE: src/utils/pickerErrorUtils.ts
  function isPickerStuck (line 5) | function isPickerStuck(err: unknown): boolean {

FILE: src/utils/resolvePickedFileUri.ts
  function decodePickedUri (line 4) | function decodePickedUri(uri: string): string {

FILE: src/utils/sharePrompt.ts
  constant GITHUB_URL (line 1) | const GITHUB_URL = 'https://github.com/alichherawalla/off-grid-mobile-ai';
  constant SHARE_TEXT (line 3) | const SHARE_TEXT = `Just tried Off Grid - a completely free, open-source...
  constant SHARE_ON_X_URL (line 9) | const SHARE_ON_X_URL = `https://twitter.com/intent/tweet?text=${encodeUR...
  function shouldShowSharePrompt (line 14) | function shouldShowSharePrompt(count: number): boolean {
  type ShareVariant (line 20) | type ShareVariant = 'text' | 'image';
  type SharePromptListener (line 21) | type SharePromptListener = (variant: ShareVariant) => void;
  function subscribeSharePrompt (line 25) | function subscribeSharePrompt(
  function emitSharePrompt (line 32) | function emitSharePrompt(variant: ShareVariant): void {

FILE: src/utils/visionRepair.ts
  type VisionRepairCandidate (line 3) | interface VisionRepairCandidate {
  function needsVisionRepair (line 14) | function needsVisionRepair(
Copy disabled (too large) Download .json
Condensed preview — 633 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (13,295K chars).
[
  {
    "path": ".bundle/config",
    "chars": 59,
    "preview": "BUNDLE_PATH: \"vendor/bundle\"\nBUNDLE_FORCE_RUBY_PLATFORM: 1\n"
  },
  {
    "path": ".eslintignore",
    "chars": 68,
    "preview": "# Generated build artifacts\nandroid/app/build/\nios/build/\ncoverage/\n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 1626,
    "preview": "module.exports = {\n  root: true,\n  extends: '@react-native',\n  plugins: [\n    'react-native',\n    'react',\n    'react-ho"
  },
  {
    "path": ".gitattributes",
    "chars": 51,
    "preview": "releases/*.apk filter=lfs diff=lfs merge=lfs -text\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 905,
    "preview": "---\nname: Bug Report\nabout: Report a bug or unexpected behavior\ntitle: \"[Bug] \"\nlabels: bug\nassignees: ''\n---\n\n## Descri"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 584,
    "preview": "---\nname: Feature Request\nabout: Suggest a new feature or improvement\ntitle: \"[Feature] \"\nlabels: enhancement\nassignees:"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 2439,
    "preview": "## Summary\n\n<!-- Briefly describe what this PR does and why -->\n\n## Type of Change\n\n- [ ] Bug fix (non-breaking change t"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 3296,
    "preview": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  lint:\n    runs-on: "
  },
  {
    "path": ".github/workflows/pages.yml",
    "chars": 1194,
    "preview": "name: Deploy Off Grid Docs\n\non:\n  push:\n    branches: [main]\n    paths: ['website/**']\n  workflow_dispatch:\n\npermissions"
  },
  {
    "path": ".github/workflows/release-ios.yml",
    "chars": 6466,
    "preview": "name: Build and Release iOS\n\non:\n  workflow_dispatch:  # Disabled - manual trigger only for now\n\npermissions:\n  contents"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 3881,
    "preview": "name: Build and Release Android\n# NOTE: The iOS workflow (release-ios.yml) triggers via workflow_run on this workflow.\n#"
  },
  {
    "path": ".gitignore",
    "chars": 1151,
    "preview": "# OSX\n#\n.DS_Store\n\n# Xcode\n#\nbuild/\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.p"
  },
  {
    "path": ".husky/pre-push",
    "chars": 2368,
    "preview": "#!/usr/bin/env sh\n\nZERO_OID=\"0000000000000000000000000000000000000000\"\n\ncollect_changed_files() {\n  changed_files=\"\"\n\n  "
  },
  {
    "path": ".maestro/E2E_TESTING.md",
    "chars": 6160,
    "preview": "# E2E Testing with Maestro\n\nThis directory contains end-to-end tests using [Maestro](https://maestro.mobile.dev/).\n\n## P"
  },
  {
    "path": ".maestro/config.yaml",
    "chars": 317,
    "preview": "# Maestro workspace config\n#\n# All flows use ${APP_ID} — pass it at runtime:\n#\n#   iOS:           maestro test -e APP_ID"
  },
  {
    "path": ".maestro/flows/p0/00-setup-model.yaml",
    "chars": 6662,
    "preview": "# P0 E2E: Setup - Ensure a text model is loaded and ready for chat\n# This MUST run before all other tests\n#\n# Strategy: "
  },
  {
    "path": ".maestro/flows/p0/01-app-launch.yaml",
    "chars": 416,
    "preview": "# P0 E2E: App Launch\n# Verifies the app launches successfully and shows home screen\n\nappId: ${APP_ID}\nname: \"P0: App Lau"
  },
  {
    "path": ".maestro/flows/p0/01a-onboarding-first-launch.yaml",
    "chars": 1765,
    "preview": "# P0 E2E: 1.1 Onboarding appears on first launch\n# Verifies onboarding shows on fresh install, slides work, and \"Get Sta"
  },
  {
    "path": ".maestro/flows/p0/01b-onboarding-skip.yaml",
    "chars": 594,
    "preview": "# P0 E2E: 1.2 Skip onboarding\n# Verifies tapping \"Skip\" on any slide goes to Model Download screen\n# QA_TEST_PLAN §1.2\n\n"
  },
  {
    "path": ".maestro/flows/p0/01c-model-download-first-time.yaml",
    "chars": 1067,
    "preview": "# P0 E2E: 1.3 Model Download screen — first time\n# Verifies Model Download screen shows device info and \"Skip for Now\" g"
  },
  {
    "path": ".maestro/flows/p0/01d-second-launch-no-onboarding.yaml",
    "chars": 735,
    "preview": "# P0 E2E: 1.5 Second launch — no onboarding\n# Verifies relaunch skips onboarding entirely\n# QA_TEST_PLAN §1.5\n# Precondi"
  },
  {
    "path": ".maestro/flows/p0/01e-tab-navigation.yaml",
    "chars": 1583,
    "preview": "# P0 E2E: 20.1 All 5 tabs\n# Verifies all tab bar tabs are tappable and show correct screens\n# QA_TEST_PLAN §20.1\n\nappId:"
  },
  {
    "path": ".maestro/flows/p0/02-text-generation.yaml",
    "chars": 1794,
    "preview": "# P0 E2E: Text Generation Flow\n# Tests the complete text generation cycle\n# Prerequisites: Text model must be loaded\n\nap"
  },
  {
    "path": ".maestro/flows/p0/03-stop-generation.yaml",
    "chars": 2359,
    "preview": "# P0 E2E: Stop Generation Flow\n# Tests stopping an in-progress generation\n# Prerequisites: Text model must be loaded\n\nap"
  },
  {
    "path": ".maestro/flows/p0/04-image-generation.yaml",
    "chars": 6676,
    "preview": "# P0 E2E: Image Generation Flow\n# Tests the complete image generation cycle including model download\n# This test ensures"
  },
  {
    "path": ".maestro/flows/p1/06a-document-attachment.yaml",
    "chars": 3750,
    "preview": "# P0 E2E: Document Attachment Flow\n# Tests attaching a document to a chat message and sending it\n# Prerequisites: Text m"
  },
  {
    "path": ".maestro/flows/p1/06b-image-attachment.yaml",
    "chars": 4265,
    "preview": "# P0 E2E: Image Attachment Flow\n# Tests the image attachment button and camera/library picker dialog\n# Prerequisites: Te"
  },
  {
    "path": ".maestro/flows/p1/06c-text-generation-full.yaml",
    "chars": 5319,
    "preview": "# P0 E2E: Text Generation - Full Flow\n# Tests the complete text generation lifecycle including:\n# - Sending a message an"
  },
  {
    "path": ".maestro/flows/p1/06d-text-generation-retry.yaml",
    "chars": 3004,
    "preview": "# P0 E2E: Text Generation - Retry Flow\n# Tests retrying a generation from the message action menu\n# Prerequisites: Text "
  },
  {
    "path": ".maestro/flows/p2/05a-model-uninstall.yaml",
    "chars": 3031,
    "preview": "# P0 E2E: Model Uninstall\n# Tests deleting a downloaded model\n#\n# Precondition: At least one model downloaded\n# Test: Go"
  },
  {
    "path": ".maestro/flows/p2/05b-model-download.yaml",
    "chars": 3245,
    "preview": "# P0 E2E: Model Download\n# Tests the full model download flow from search to completion\n#\n# Precondition: Clean app stat"
  },
  {
    "path": ".maestro/flows/p2/05b-model-selection.yaml",
    "chars": 4374,
    "preview": "# P0 E2E: Model Selection\n# Tests selecting a model from multiple downloaded models\n#\n# Precondition: At least 2 models "
  },
  {
    "path": ".maestro/flows/p2/05c-model-unload.yaml",
    "chars": 2693,
    "preview": "# P0 E2E: Model Unload\n# Tests unloading a currently loaded model\n#\n# Precondition: Model must be loaded\n# Test: Open pi"
  },
  {
    "path": ".maestro/flows/p3/07a-image-model-uninstall.yaml",
    "chars": 2417,
    "preview": "# P0 E2E: Image Model Uninstall\n# Tests deleting a downloaded image model via Download Manager\n# Assumes an image model "
  },
  {
    "path": ".maestro/flows/p3/07b-image-model-download.yaml",
    "chars": 2287,
    "preview": "# P0 E2E: Image Model Download\n# Tests downloading an image model from the Models screen\n# Assumes no image model is cur"
  },
  {
    "path": ".maestro/flows/p3/07c-image-model-set-active.yaml",
    "chars": 3031,
    "preview": "# P0 E2E: Image Model Set Active\n# Tests selecting a downloaded image model from the home screen picker\n# Assumes an ima"
  },
  {
    "path": ".maestro/utils/wait-for-app-ready.yaml",
    "chars": 226,
    "preview": "# Utility: Wait for app to be ready\n# Waits for the main UI to be visible\n\nappId: ${APP_ID}\n---\n\n# Wait for home screen "
  },
  {
    "path": ".prettierrc.js",
    "chars": 91,
    "preview": "module.exports = {\n  arrowParens: 'avoid',\n  singleQuote: true,\n  trailingComma: 'all',\n};\n"
  },
  {
    "path": ".swiftlint.yml",
    "chars": 410,
    "preview": "included:\n  - ios\n\nexcluded:\n  - ios/Pods\n  - ios/build\n  - ios/OffgridMobile.xcodeproj\n  - ios/OffgridMobileTests\n\ndisa"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 147,
    "preview": "{\n    \"sonarlint.connectedMode.project\": {\n        \"connectionId\": \"alichherawalla\",\n        \"projectKey\": \"alichherawal"
  },
  {
    "path": ".watchmanconfig",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "AGENTS.md",
    "chars": 3718,
    "preview": "# Project Instructions\n\n## Pre-Commit Quality Gates\n\nAll quality gates run automatically via Husky on every `git commit`"
  },
  {
    "path": "App.tsx",
    "chars": 10225,
    "preview": "/**\n * Off Grid - On-Device AI Chat Application\n * Private AI assistant that runs entirely on your device\n */\n\nimport 'r"
  },
  {
    "path": "CLAUDE.md",
    "chars": 6879,
    "preview": "# Project Instructions\n\n## Branch Policy\n\n**Never push directly to `main`.** All changes must go through a pull request:"
  },
  {
    "path": "Gemfile",
    "chars": 510,
    "preview": "source 'https://rubygems.org'\n\n# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version\nruby \""
  },
  {
    "path": "LICENSE",
    "chars": 1081,
    "preview": "MIT License\n\nCopyright (c) 2026 Mohammed Ali Chherawalla\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "README.md",
    "chars": 10014,
    "preview": "<div align=\"center\">\n\n<img src=\"src/assets/logo.png\" alt=\"Off Grid Logo\" width=\"120\" />\n\n# Off Grid\n\n### The Swiss Army "
  },
  {
    "path": "TODO.md",
    "chars": 381,
    "preview": "# OffgridMobile - TODO\n\n## Document Upload Support\n\n- [ ] **Add Word/Office document support**\n  - Research libraries fo"
  },
  {
    "path": "__tests__/App.test.tsx",
    "chars": 254,
    "preview": "/**\n * @format\n */\n\nimport React from 'react';\nimport ReactTestRenderer from 'react-test-renderer';\nimport App from '../"
  },
  {
    "path": "__tests__/contracts/coreMLDiffusion.contract.test.ts",
    "chars": 9360,
    "preview": "/**\n * Contract Tests: CoreMLDiffusion Native Module (iOS Image Generation)\n *\n * These tests verify that the CoreMLDiff"
  },
  {
    "path": "__tests__/contracts/iosDownloadManager.contract.test.ts",
    "chars": 14678,
    "preview": "/**\n * Contract Tests: iOS DownloadManagerModule (Background Downloads)\n *\n * Verifies that the iOS DownloadManagerModul"
  },
  {
    "path": "__tests__/contracts/llama.rn.test.ts",
    "chars": 8602,
    "preview": "/**\n * llama.rn Contract Tests\n *\n * These tests verify that our usage of llama.rn matches its expected interface.\n * Th"
  },
  {
    "path": "__tests__/contracts/llamaContext.contract.test.ts",
    "chars": 12015,
    "preview": "/**\n * Contract Tests: llama.rn Native Module\n *\n * These tests verify that the llama.rn native module interface\n * matc"
  },
  {
    "path": "__tests__/contracts/localDream.contract.test.ts",
    "chars": 15127,
    "preview": "/**\n * Contract Tests: LocalDream Native Module (Image Generation)\n *\n * These tests verify that the LocalDream native m"
  },
  {
    "path": "__tests__/contracts/ragEmbedding.contract.test.ts",
    "chars": 7368,
    "preview": "/**\n * RAG Embedding Contract Tests\n *\n * Documents and verifies the expected interface between our embedding service\n *"
  },
  {
    "path": "__tests__/contracts/whisper.contract.test.ts",
    "chars": 15679,
    "preview": "/**\n * Contract Tests: whisper.rn Native Module (Speech-to-Text)\n *\n * These tests verify that the whisper.rn native mod"
  },
  {
    "path": "__tests__/contracts/whisper.rn.test.ts",
    "chars": 7429,
    "preview": "/**\n * whisper.rn Contract Tests\n *\n * These tests document and verify the expected interface of the whisper.rn module.\n"
  },
  {
    "path": "__tests__/helpers/mockCustomAlert.tsx",
    "chars": 1435,
    "preview": "/**\n * Shared CustomAlert mock for test files.\n *\n * Usage in test files:\n *   jest.mock('.../CustomAlert', () =>\n *    "
  },
  {
    "path": "__tests__/helpers/mockNetworkDeps.ts",
    "chars": 589,
    "preview": "/**\n * Shared mocks for react-native-device-info and logger,\n * used by network.test.ts and networkDiscovery.test.ts.\n *"
  },
  {
    "path": "__tests__/integration/generation/generationFlow.test.ts",
    "chars": 20221,
    "preview": "/**\n * Integration Tests: Generation Flow\n *\n * Tests the integration between:\n * - generationService ↔ llmService (toke"
  },
  {
    "path": "__tests__/integration/generation/imageGenerationFlow.test.ts",
    "chars": 56779,
    "preview": "/**\n * Integration Tests: Image Generation Flow\n *\n * Tests the integration between:\n * - imageGenerationService ↔ local"
  },
  {
    "path": "__tests__/integration/generation/remoteProviderRouting.test.ts",
    "chars": 10479,
    "preview": "/**\n * Generation Service Provider Routing Integration Tests\n *\n * Tests for routing between local and remote providers "
  },
  {
    "path": "__tests__/integration/generation/sharePromptFlow.test.ts",
    "chars": 11538,
    "preview": "/**\n * Integration Tests: Share Prompt Flow\n *\n * Tests the integration between:\n * - generationService → appStore (text"
  },
  {
    "path": "__tests__/integration/generation/unifiedModelSelection.test.ts",
    "chars": 8257,
    "preview": "/**\n * Unified Model Selection Integration Tests\n *\n * Tests the flow of selecting local vs remote models and ensuring\n "
  },
  {
    "path": "__tests__/integration/models/activeModelService.test.ts",
    "chars": 66430,
    "preview": "/**\n * Integration Tests: ActiveModelService\n *\n * Tests the integration between:\n * - activeModelService ↔ llmService ("
  },
  {
    "path": "__tests__/integration/onboarding/spotlightFlowIntegration.test.ts",
    "chars": 22744,
    "preview": "/**\n * Integration Tests: Onboarding Spotlight Flow Coordination\n *\n * Tests the full lifecycle of each onboarding flow "
  },
  {
    "path": "__tests__/integration/rag/embeddingFlow.test.ts",
    "chars": 8923,
    "preview": "/**\n * Integration Tests: Embedding Flow\n *\n * Tests the full embedding pipeline:\n * - Index document → generate embeddi"
  },
  {
    "path": "__tests__/integration/rag/ragFlow.test.ts",
    "chars": 15526,
    "preview": "/**\n * Integration Tests: RAG Flow\n *\n * Tests the integration between:\n * - ragService → ragDatabase (index, search, de"
  },
  {
    "path": "__tests__/integration/stores/chatStoreIntegration.test.ts",
    "chars": 13736,
    "preview": "/**\n * Integration Tests: ChatStore Streaming Integration\n *\n * Tests the chatStore's streaming functionality in isolati"
  },
  {
    "path": "__tests__/integration/stores/remoteServerDiscovery.test.ts",
    "chars": 18649,
    "preview": "/**\n * Integration Tests: Remote Server Model Discovery\n *\n * Tests the model discovery flow in remoteServerStore, speci"
  },
  {
    "path": "__tests__/rntl/components/AnimatedEntry.test.tsx",
    "chars": 3619,
    "preview": "/**\n * AnimatedEntry Component Tests\n *\n * Tests for the animated entry wrapper:\n * - Renders children when index < maxI"
  },
  {
    "path": "__tests__/rntl/components/AnimatedListItem.test.tsx",
    "chars": 3189,
    "preview": "/**\n * AnimatedListItem Component Tests\n *\n * Tests for the AnimatedListItem wrapper component covering:\n * - Basic rend"
  },
  {
    "path": "__tests__/rntl/components/AnimatedPressable.test.tsx",
    "chars": 6852,
    "preview": "/**\n * AnimatedPressable Component Tests\n *\n * Tests for the pressable component with scale animation and haptic feedbac"
  },
  {
    "path": "__tests__/rntl/components/AppSheet.test.tsx",
    "chars": 31112,
    "preview": "/**\n * AppSheet Component Tests\n *\n * Tests for the bottom sheet component using RN Modal + Animated:\n * - Returns null "
  },
  {
    "path": "__tests__/rntl/components/Card.test.tsx",
    "chars": 3547,
    "preview": "/**\n * Card Component Tests\n *\n * Tests for the Card component covering all branches:\n * - Container type (View vs Touch"
  },
  {
    "path": "__tests__/rntl/components/ChatInput.test.tsx",
    "chars": 59887,
    "preview": "/**\n * ChatInput Component Tests\n *\n * Tests for the message input component including:\n * - Text input and send\n * - At"
  },
  {
    "path": "__tests__/rntl/components/ChatMessage.test.tsx",
    "chars": 54103,
    "preview": "/**\n * ChatMessage Component Tests\n *\n * Tests for the message rendering component including:\n * - Message display by ro"
  },
  {
    "path": "__tests__/rntl/components/ChatMessageTools.test.tsx",
    "chars": 12239,
    "preview": "/**\n * ChatMessage Tool Rendering Tests\n *\n * Tests for tool-related message rendering:\n * - ToolResultMessage (role ==="
  },
  {
    "path": "__tests__/rntl/components/CustomAlert.test.tsx",
    "chars": 3826,
    "preview": "/**\n * CustomAlert Component Tests\n *\n * Tests for the custom alert dialog:\n * - Renders title and message\n * - Renders "
  },
  {
    "path": "__tests__/rntl/components/DebugSheet.test.tsx",
    "chars": 14249,
    "preview": "/**\n * DebugSheet Component Tests\n *\n * Tests for the debug info bottom sheet:\n * - Context stats display\n * - Message s"
  },
  {
    "path": "__tests__/rntl/components/GenerationSettingsModal.test.tsx",
    "chars": 40348,
    "preview": "/**\n * GenerationSettingsModal Component Tests\n *\n * Tests for the settings modal including:\n * - Visibility behavior\n *"
  },
  {
    "path": "__tests__/rntl/components/ImageFilterBar.test.tsx",
    "chars": 17153,
    "preview": "/**\n * ImageFilterBar Component Tests\n *\n * Tests for the image model filter bar including:\n * - Platform-specific rende"
  },
  {
    "path": "__tests__/rntl/components/MarkdownText.test.tsx",
    "chars": 4623,
    "preview": "/**\n * MarkdownText Component Tests\n *\n * Tests for the themed markdown renderer covering:\n * - Rendering markdown eleme"
  },
  {
    "path": "__tests__/rntl/components/ModelCard.test.tsx",
    "chars": 23010,
    "preview": "/**\n * ModelCard Component Tests\n *\n * Tests for the model card display component including:\n * - Basic rendering (full "
  },
  {
    "path": "__tests__/rntl/components/ModelPickerSheet.test.tsx",
    "chars": 25703,
    "preview": "/**\n * ModelPickerSheet Component Tests\n *\n * Tests for the HomeScreen bottom sheet showing model selection:\n * - Visibi"
  },
  {
    "path": "__tests__/rntl/components/ModelSelectorModal.test.tsx",
    "chars": 29834,
    "preview": "/**\n * ModelSelectorModal Component Tests\n *\n * Tests for the modal showing text and image model lists:\n * - Returns nul"
  },
  {
    "path": "__tests__/rntl/components/ProjectSelectorSheet.test.tsx",
    "chars": 5281,
    "preview": "/**\n * ProjectSelectorSheet Component Tests\n *\n * Tests for the project selection bottom sheet:\n * - Visibility toggling"
  },
  {
    "path": "__tests__/rntl/components/RemoteServerModal.test.tsx",
    "chars": 19637,
    "preview": "/**\n * RemoteServerModal Component Tests\n *\n * Tests for the remote server configuration modal including:\n * - Rendering"
  },
  {
    "path": "__tests__/rntl/components/SharePromptSheet.test.tsx",
    "chars": 2169,
    "preview": "/**\n * SharePromptSheet Component Tests\n *\n * Tests for the share/star prompt bottom sheet.\n * Priority: P1 (High)\n */\n\n"
  },
  {
    "path": "__tests__/rntl/components/ToolPickerSheet.test.tsx",
    "chars": 4156,
    "preview": "/**\n * ToolPickerSheet Tests\n *\n * Tests for the tool picker bottom sheet including:\n * - Visibility (renders nothing wh"
  },
  {
    "path": "__tests__/rntl/components/VoiceRecordButton.test.tsx",
    "chars": 15191,
    "preview": "/**\n * VoiceRecordButton Component Tests\n *\n * Tests for the voice recording button with animation, drag-to-cancel:\n * -"
  },
  {
    "path": "__tests__/rntl/hooks/useFocusTrigger.test.ts",
    "chars": 1097,
    "preview": "/**\n * useFocusTrigger Hook Tests\n *\n * Tests for the focus trigger hook:\n * - Returns 0 initially\n * - Increments when "
  },
  {
    "path": "__tests__/rntl/navigation/AppNavigator.test.tsx",
    "chars": 10346,
    "preview": "/**\n * AppNavigator Tests\n *\n * Tests for the main navigation setup including:\n * - Tab bar safe area inset handling\n * "
  },
  {
    "path": "__tests__/rntl/onboarding/ChatScreenSpotlight.test.tsx",
    "chars": 10643,
    "preview": "/**\n * ChatScreen Spotlight Integration Tests\n *\n * Renders the actual ChatScreen and verifies:\n * - Pending step 3 cons"
  },
  {
    "path": "__tests__/rntl/onboarding/ChatsListScreenSpotlight.test.tsx",
    "chars": 4831,
    "preview": "/**\n * ChatsListScreen Spotlight Integration Tests\n *\n * Renders the actual ChatsListScreen and verifies:\n * - Reactive "
  },
  {
    "path": "__tests__/rntl/onboarding/HomeScreenSpotlight.test.tsx",
    "chars": 14969,
    "preview": "/**\n * HomeScreen Spotlight Integration Tests\n *\n * Renders the actual HomeScreen component and verifies:\n * - handleSte"
  },
  {
    "path": "__tests__/rntl/onboarding/ModelSettingsScreenSpotlight.test.tsx",
    "chars": 2965,
    "preview": "/**\n * ModelSettingsScreen Spotlight Integration Tests\n *\n * Renders the actual ModelSettingsScreen and verifies:\n * - P"
  },
  {
    "path": "__tests__/rntl/onboarding/ProjectEditScreenSpotlight.test.tsx",
    "chars": 2763,
    "preview": "/**\n * ProjectEditScreen Spotlight Integration Tests\n *\n * Renders the actual ProjectEditScreen and verifies:\n * - Pendi"
  },
  {
    "path": "__tests__/rntl/screens/ChatScreen.test.tsx",
    "chars": 167874,
    "preview": "/**\n * ChatScreen Tests\n *\n * Tests for the main chat interface including:\n * - No model state / model loading state\n * "
  },
  {
    "path": "__tests__/rntl/screens/ChatsListScreen.test.tsx",
    "chars": 17988,
    "preview": "/**\n * ChatsListScreen Tests\n *\n * Tests for the conversation list screen including:\n * - Title and header rendering\n * "
  },
  {
    "path": "__tests__/rntl/screens/DeviceInfoScreen.test.tsx",
    "chars": 4223,
    "preview": "/**\n * DeviceInfoScreen Tests\n *\n * Tests for the device information screen including:\n * - Title display\n * - Device mo"
  },
  {
    "path": "__tests__/rntl/screens/DocumentPreviewScreen.test.tsx",
    "chars": 5911,
    "preview": "/**\n * DocumentPreviewScreen Tests\n */\n\nimport React from 'react';\nimport { render, act, fireEvent } from '@testing-libr"
  },
  {
    "path": "__tests__/rntl/screens/DownloadManagerScreen.test.tsx",
    "chars": 57223,
    "preview": "/**\n * DownloadManagerScreen Tests\n *\n * Tests for the download manager screen including:\n * - Title display\n * - Empty "
  },
  {
    "path": "__tests__/rntl/screens/GalleryScreen.test.tsx",
    "chars": 23379,
    "preview": "/**\n * GalleryScreen Tests\n *\n * Tests for the gallery screen including:\n * - Title rendering\n * - Empty state when no i"
  },
  {
    "path": "__tests__/rntl/screens/HomeScreen.test.tsx",
    "chars": 63486,
    "preview": "/**\n * HomeScreen Tests\n *\n * Tests for the home dashboard including:\n * - Model cards display\n * - Model selection and "
  },
  {
    "path": "__tests__/rntl/screens/KnowledgeBaseScreen.test.tsx",
    "chars": 8903,
    "preview": "/**\n * KnowledgeBaseScreen Tests\n */\n\nimport React from 'react';\nimport { render, fireEvent, act } from '@testing-librar"
  },
  {
    "path": "__tests__/rntl/screens/LockScreen.test.tsx",
    "chars": 16007,
    "preview": "/**\n * LockScreen Tests\n *\n * Tests for the lock screen including:\n * - Lock icon rendering\n * - Passphrase input\n * - U"
  },
  {
    "path": "__tests__/rntl/screens/ModelDownloadHelpers.test.tsx",
    "chars": 12830,
    "preview": "/**\n * ModelDownloadHelpers Tests\n *\n * Tests for helper components and functions used by the model download screen:\n * "
  },
  {
    "path": "__tests__/rntl/screens/ModelDownloadScreen.test.tsx",
    "chars": 21398,
    "preview": "/**\n * ModelDownloadScreen Tests\n *\n * Tests for the model download screen including:\n * - Screen rendering (loading sta"
  },
  {
    "path": "__tests__/rntl/screens/ModelSettingsScreen.test.tsx",
    "chars": 41476,
    "preview": "/**\n * ModelSettingsScreen Tests\n *\n * Tests for the model settings screen including:\n * - Section titles rendering\n * -"
  },
  {
    "path": "__tests__/rntl/screens/ModelsScreen.test.tsx",
    "chars": 86967,
    "preview": "/**\n * ModelsScreen Tests\n *\n * Tests for the model discovery and download screen including:\n * - Rendering the actual c"
  },
  {
    "path": "__tests__/rntl/screens/OnboardingScreen.test.tsx",
    "chars": 8907,
    "preview": "/**\n * OnboardingScreen Tests\n *\n * Tests for the onboarding screen including:\n * - First slide content rendering\n * - N"
  },
  {
    "path": "__tests__/rntl/screens/PassphraseSetupScreen.test.tsx",
    "chars": 14626,
    "preview": "/**\n * PassphraseSetupScreen Tests\n *\n * Tests for the passphrase setup/change screen including:\n * - Title display for "
  },
  {
    "path": "__tests__/rntl/screens/ProjectChatsScreen.test.tsx",
    "chars": 9783,
    "preview": "/**\n * ProjectChatsScreen Tests\n */\n\nimport React from 'react';\nimport { render, fireEvent, act } from '@testing-library"
  },
  {
    "path": "__tests__/rntl/screens/ProjectDetailScreen.test.tsx",
    "chars": 28347,
    "preview": "/**\n * ProjectDetailScreen Tests\n *\n * Tests for the project detail screen including:\n * - Project name and description "
  },
  {
    "path": "__tests__/rntl/screens/ProjectEditScreen.test.tsx",
    "chars": 13805,
    "preview": "/**\n * ProjectEditScreen Tests\n *\n * Tests for the project edit screen including:\n * - Edit screen title display\n * - Ne"
  },
  {
    "path": "__tests__/rntl/screens/ProjectsScreen.test.tsx",
    "chars": 9642,
    "preview": "/**\n * ProjectsScreen Tests\n *\n * Tests for the projects management screen including:\n * - Title and subtitle rendering\n"
  },
  {
    "path": "__tests__/rntl/screens/RemoteServersScreen.test.tsx",
    "chars": 20789,
    "preview": "/**\n * RemoteServersScreen Tests\n *\n * Tests for the remote servers settings screen including:\n * - Empty state renderin"
  },
  {
    "path": "__tests__/rntl/screens/SecuritySettingsScreen.test.tsx",
    "chars": 12156,
    "preview": "/**\n * SecuritySettingsScreen Tests\n *\n * Tests for the security settings screen including:\n * - Title display\n * - App "
  },
  {
    "path": "__tests__/rntl/screens/SettingsScreen.test.tsx",
    "chars": 5848,
    "preview": "/**\n * SettingsScreen Tests\n *\n * Tests for the settings screen including:\n * - Title and version display\n * - Navigatio"
  },
  {
    "path": "__tests__/rntl/screens/StorageSettingsScreen.test.tsx",
    "chars": 23761,
    "preview": "/**\n * StorageSettingsScreen Tests\n *\n * Tests for the storage settings screen including:\n * - Title display\n * - Back b"
  },
  {
    "path": "__tests__/rntl/screens/VoiceSettingsScreen.test.tsx",
    "chars": 12482,
    "preview": "/**\n * VoiceSettingsScreen Tests\n *\n * Tests for the voice settings screen including:\n * - Title display\n * - Descriptio"
  },
  {
    "path": "__tests__/specs/image-generation.yaml",
    "chars": 7438,
    "preview": "# Image Generation Flow Test Specification\n# Priority: P0 (Critical)\n# Image generation is a core feature of the app\n\nfl"
  },
  {
    "path": "__tests__/specs/model-lifecycle.yaml",
    "chars": 7035,
    "preview": "# Model Lifecycle Flow Test Specification\n# Priority: P0 (Critical)\n# Models must load/unload correctly for app to funct"
  },
  {
    "path": "__tests__/specs/text-generation.yaml",
    "chars": 8616,
    "preview": "# Text Generation Flow Test Specification\n# Priority: P0 (Critical)\n# This flow is core to the app - if broken, app is u"
  },
  {
    "path": "__tests__/unit/components/ChatMessage/utils.test.ts",
    "chars": 7072,
    "preview": "/**\n * ChatMessage/utils Tests\n *\n * Unit tests for parseThinkingContent, formatTime, formatDuration\n */\n\nimport { parse"
  },
  {
    "path": "__tests__/unit/constants/constants.test.ts",
    "chars": 7326,
    "preview": "/**\n * Constants Validation Tests\n *\n * Tests for model constants: RECOMMENDED_MODELS, MODEL_ORGS, VERIFIED_QUANTIZERS.\n"
  },
  {
    "path": "__tests__/unit/hooks/useAppState.test.ts",
    "chars": 4347,
    "preview": "/**\n * useAppState Hook Unit Tests\n *\n * Tests for the AppState listener hook that fires callbacks\n * on foreground/back"
  },
  {
    "path": "__tests__/unit/hooks/useChatGenerationActions.test.ts",
    "chars": 49129,
    "preview": "/**\n * Unit tests for useChatGenerationActions\n *\n * Covers uncovered branches:\n * - shouldRouteToImageGenerationFn: LLM"
  },
  {
    "path": "__tests__/unit/hooks/useChatModelActions.test.ts",
    "chars": 15512,
    "preview": "/**\n * Unit tests for useChatModelActions\n *\n * Tests the exported async functions directly, covering uncovered branches"
  },
  {
    "path": "__tests__/unit/hooks/useHomeScreen.test.ts",
    "chars": 20042,
    "preview": "/**\n * useHomeScreen Hook Unit Tests\n *\n * Tests for the HomeScreen orchestration hook covering:\n * - startNewChat / con"
  },
  {
    "path": "__tests__/unit/hooks/useImageGenerationSettings.test.ts",
    "chars": 3856,
    "preview": "/**\n * useImageGenerationSettings (useClearGpuCache) Unit Tests\n */\n\njest.mock('react-native', () => ({\n  Alert: { alert"
  },
  {
    "path": "__tests__/unit/hooks/useKeyboardAwarePopover.test.ts",
    "chars": 10668,
    "preview": "/**\n * useKeyboardAwarePopover Hook Unit Tests\n *\n * Tests for keyboard-aware popover positioning hook that handles\n * k"
  },
  {
    "path": "__tests__/unit/hooks/useModelLoading.test.ts",
    "chars": 11780,
    "preview": "/**\n * useModelLoading Hook Unit Tests\n *\n * Covers Load Anyway button callbacks and isLowMemDevice branches.\n */\n\nimpor"
  },
  {
    "path": "__tests__/unit/hooks/useTextGenerationAdvanced.test.ts",
    "chars": 1279,
    "preview": "import { renderHook, act } from '@testing-library/react-native';\nimport { resetStores } from '../../utils/testHelpers';\n"
  },
  {
    "path": "__tests__/unit/hooks/useVoiceRecording.test.ts",
    "chars": 13249,
    "preview": "/**\n * useVoiceRecording Hook Unit Tests\n *\n * Tests for the voice recording hook that wraps voiceService.\n */\n\nimport {"
  },
  {
    "path": "__tests__/unit/hooks/useWhisperTranscription.test.ts",
    "chars": 18651,
    "preview": "import { renderHook, act } from '@testing-library/react-native';\nimport { useWhisperTranscription } from '../../../src/h"
  },
  {
    "path": "__tests__/unit/onboarding/chatScreenSpotlight.test.ts",
    "chars": 12472,
    "preview": "/**\n * ChatScreen Spotlight Coordination Tests\n *\n * Tests the ChatScreen-specific spotlight logic in isolation:\n * - Co"
  },
  {
    "path": "__tests__/unit/onboarding/checklistComponents.test.tsx",
    "chars": 7306,
    "preview": "/**\n * Checklist component tests — covers ProgressBar, animations, useOnboardingSteps,\n * useChecklistTheme, useAutoDism"
  },
  {
    "path": "__tests__/unit/onboarding/handleStepPress.test.ts",
    "chars": 12821,
    "preview": "/**\n * handleStepPress Unit Tests\n *\n * Tests the HomeScreen handleStepPress logic in isolation.\n * This function is the"
  },
  {
    "path": "__tests__/unit/onboarding/onboardingFlows.test.ts",
    "chars": 16730,
    "preview": "/**\n * Onboarding Spotlight Flow Tests\n *\n * Tests that verify the onboarding checklist flows work correctly:\n * - Spotl"
  },
  {
    "path": "__tests__/unit/onboarding/reactiveSpotlightConditions.test.ts",
    "chars": 11621,
    "preview": "/**\n * Reactive Spotlight Condition Tests\n *\n * Tests the exact boolean conditions that each screen's useEffect checks\n "
  },
  {
    "path": "__tests__/unit/onboarding/spotlightTooltips.test.ts",
    "chars": 3344,
    "preview": "/**\n * Spotlight Tooltip Content Tests\n *\n * Verifies that every spotlight step renders a tooltip with the correct\n * ti"
  },
  {
    "path": "__tests__/unit/screens/ChatScreen/toolUsage.test.ts",
    "chars": 5303,
    "preview": "/**\n * Tool Usage Detection Unit Tests\n *\n * Tests for determining when tools should be automatically triggered.\n */\n\nim"
  },
  {
    "path": "__tests__/unit/screens/ChatScreen/useSaveImage.test.ts",
    "chars": 4073,
    "preview": "/**\n * useSaveImage Unit Tests\n */\n\njest.mock('react-native', () => ({\n  Platform: { OS: 'ios', select: (obj: any) => ob"
  },
  {
    "path": "__tests__/unit/screens/DownloadManagerScreen/items.test.tsx",
    "chars": 1232,
    "preview": "import { buildDownloadItems } from '../../../../src/screens/DownloadManagerScreen/items';\n\njest.mock('../../../../src/se"
  },
  {
    "path": "__tests__/unit/screens/ModelsScreen/imageDownloadActions.test.ts",
    "chars": 30534,
    "preview": "import { Platform } from 'react-native';\nimport {\n  downloadHuggingFaceModel,\n  downloadCoreMLMultiFile,\n  proceedWithDo"
  },
  {
    "path": "__tests__/unit/screens/ModelsScreen/importHelpers.test.ts",
    "chars": 9785,
    "preview": "/**\n * Unit tests for importHelpers.ts\n *\n * Tests pure helpers (isMmProj, classifyGgufPair, getErrorMessage) directly,\n"
  },
  {
    "path": "__tests__/unit/screens/ModelsScreen/restoreImageDownloads.test.ts",
    "chars": 14689,
    "preview": "/**\n * Tests for restoreActiveImageDownloads (via useImageModels hook mount).\n *\n * handleCompletedImageDownload is not "
  },
  {
    "path": "__tests__/unit/screens/ModelsScreen/trendingSelection.test.ts",
    "chars": 6142,
    "preview": "/**\n * trendingSelection.test.ts\n *\n * Tests for the trendingAsModelInfo logic in useTextModels.\n * Verifies that the be"
  },
  {
    "path": "__tests__/unit/screens/ModelsScreen/useModelsScreen.test.ts",
    "chars": 25138,
    "preview": "/**\n * useModelsScreen Hook Unit Tests\n *\n * Tests for the ModelsScreen orchestrator hook including:\n * - Tab switching\n"
  },
  {
    "path": "__tests__/unit/screens/ModelsScreen/useTextModels.handlers.test.ts",
    "chars": 8566,
    "preview": "/**\n * useTextModels.handlers.test.ts\n *\n * Unit tests for handler functions in useTextModels that are not covered by\n *"
  },
  {
    "path": "__tests__/unit/screens/ModelsScreen/utils.test.ts",
    "chars": 16096,
    "preview": "import { formatNumber, formatBytes, getDirectorySize, getModelType, matchesSdVersionFilter, getImageModelCompatibility, "
  },
  {
    "path": "__tests__/unit/services/authService.test.ts",
    "chars": 8247,
    "preview": "/**\n * AuthService Unit Tests\n *\n * Tests for passphrase management: set, verify, check, remove, and change.\n * Uses rea"
  },
  {
    "path": "__tests__/unit/services/backgroundDownloadService.test.ts",
    "chars": 43868,
    "preview": "/**\n * BackgroundDownloadService Unit Tests\n *\n * Tests for Android background download management via NativeModules.\n *"
  },
  {
    "path": "__tests__/unit/services/contextCompaction.test.ts",
    "chars": 10808,
    "preview": "/**\n * Context Compaction Service Unit Tests\n *\n * Tests for LLM-based summarization and token-aware message trimming\n *"
  },
  {
    "path": "__tests__/unit/services/coreMLModelBrowser.test.ts",
    "chars": 14962,
    "preview": "/**\n * CoreMLModelBrowser Unit Tests\n *\n * Tests the iOS-specific Core ML model discovery service that fetches\n * availa"
  },
  {
    "path": "__tests__/unit/services/documentService.test.ts",
    "chars": 29469,
    "preview": "/**\n * DocumentService Unit Tests\n *\n * Tests for document reading, parsing, and formatting.\n * Priority: P1 - Document "
  },
  {
    "path": "__tests__/unit/services/downloadHelpers.test.ts",
    "chars": 11194,
    "preview": "/**\n * Download Helpers Unit Tests\n *\n * Tests for the low-level helpers in modelManager/downloadHelpers.ts:\n * - getOrp"
  },
  {
    "path": "__tests__/unit/services/generationService.test.ts",
    "chars": 67208,
    "preview": "/**\n * Generation Service Unit Tests\n *\n * Tests for the LLM generation service state machine.\n * Priority: P0 (Critical"
  },
  {
    "path": "__tests__/unit/services/generationToolLoop.test.ts",
    "chars": 50212,
    "preview": "/**\n * Generation Tool Loop Unit Tests\n *\n * Tests for the tool-calling generation loop that orchestrates\n * LLM calls, "
  },
  {
    "path": "__tests__/unit/services/hardware.test.ts",
    "chars": 44372,
    "preview": "/**\n * HardwareService Unit Tests\n *\n * Tests for device info, memory calculations, model recommendations, and formattin"
  },
  {
    "path": "__tests__/unit/services/httpClient.test.ts",
    "chars": 37152,
    "preview": "/**\n * HTTP Client Unit Tests\n *\n * Tests for SSE parsing, timeout handling, base64 encoding,\n * and network utilities u"
  },
  {
    "path": "__tests__/unit/services/huggingFaceModelBrowser.test.ts",
    "chars": 11056,
    "preview": "import {\n  fetchAvailableModels,\n  getVariantLabel,\n  guessStyle,\n} from '../../../src/services/huggingFaceModelBrowser'"
  },
  {
    "path": "__tests__/unit/services/huggingface.test.ts",
    "chars": 20882,
    "preview": " \ndeclare const global: any;\n\n/**\n * HuggingFace Service Unit Tests\n *\n * Tests for model search, metadata parsing, quan"
  },
  {
    "path": "__tests__/unit/services/imageGenerationHelpers.test.ts",
    "chars": 7232,
    "preview": "/**\n * Image Generation Helpers Unit Tests\n *\n * Tests for pure helper functions used in image generation:\n * buildEnhan"
  },
  {
    "path": "__tests__/unit/services/imageGenerator.test.ts",
    "chars": 22488,
    "preview": "export {};\n\n/**\n * ImageGeneratorService Unit Tests\n *\n * Tests for the Android-only image generation service that wraps"
  },
  {
    "path": "__tests__/unit/services/imageModelRecommendation.test.ts",
    "chars": 10233,
    "preview": "/**\n * Image Model Recommendation Filter Tests\n *\n * Tests the matching logic used to determine if an image model is \"re"
  },
  {
    "path": "__tests__/unit/services/intentClassifier.test.ts",
    "chars": 41777,
    "preview": "/**\n * Intent Classifier Unit Tests\n *\n * Comprehensive tests for the pattern-based intent classification system.\n * Tes"
  },
  {
    "path": "__tests__/unit/services/llm.test.ts",
    "chars": 93985,
    "preview": "/**\n * LLMService Unit Tests\n *\n * Tests for the core LLM inference service (model loading, generation, context manageme"
  },
  {
    "path": "__tests__/unit/services/llmHelpers.test.ts",
    "chars": 16485,
    "preview": "import {\n  getMaxContextForDevice,\n  getGpuLayersForDevice,\n  BYTES_PER_GB,\n  supportsNativeThinking,\n  getModelMaxConte"
  },
  {
    "path": "__tests__/unit/services/llmMessages.test.ts",
    "chars": 9612,
    "preview": "/**\n * llmMessages Unit Tests\n *\n * Tests for message formatting helpers (OAI message building, llama prompt formatting)"
  },
  {
    "path": "__tests__/unit/services/llmSafetyChecks.test.ts",
    "chars": 3315,
    "preview": "import RNFS from 'react-native-fs';\nimport { validateModelFile, checkMemoryForModel } from '../../../src/services/llmSaf"
  },
  {
    "path": "__tests__/unit/services/llmToolGeneration.test.ts",
    "chars": 24772,
    "preview": "/**\n * llmToolGeneration Unit Tests\n *\n * Tests for the tool-aware LLM generation helper (tool calls parsing, streaming,"
  },
  {
    "path": "__tests__/unit/services/localDreamGenerator.test.ts",
    "chars": 27929,
    "preview": "export {};\n\n/**\n * LocalDreamGenerator Unit Tests - Cross-Platform Routing\n *\n * Tests that localDreamGenerator.ts corre"
  },
  {
    "path": "__tests__/unit/services/modelManager/imageSync.test.ts",
    "chars": 10889,
    "preview": "/**\n * imageSync Unit Tests\n *\n * Tests for syncCompletedImageDownloads and related helpers.\n */\n\njest.mock('react-nativ"
  },
  {
    "path": "__tests__/unit/services/modelManager.test.ts",
    "chars": 90289,
    "preview": "/**\n * ModelManager Unit Tests\n *\n * Tests for model download, storage, deletion, and background download management.\n *"
  },
  {
    "path": "__tests__/unit/services/networkDiscovery.test.ts",
    "chars": 6341,
    "preview": "/**\n * Network Discovery Unit Tests\n *\n * Tests for LAN LLM server discovery (Ollama, LM Studio).\n */\n\njest.mock('react-"
  },
  {
    "path": "__tests__/unit/services/parallelMmproj.test.ts",
    "chars": 29415,
    "preview": "/**\n * Parallel mmproj Download Tests\n *\n * Tests for downloading mmproj (vision projection) files in parallel with the\n"
  },
  {
    "path": "__tests__/unit/services/pdfExtractor.test.ts",
    "chars": 2545,
    "preview": "/**\n * PDFExtractor Unit Tests\n *\n * Tests for the TypeScript wrapper around native PDF extraction modules.\n */\n\nimport "
  },
  {
    "path": "__tests__/unit/services/providers/localProvider.test.ts",
    "chars": 12980,
    "preview": "/**\n * Local Provider Unit Tests\n *\n * Tests for the local LLM provider wrapper that delegates to llmService.\n */\n\nimpor"
  },
  {
    "path": "__tests__/unit/services/providers/openAICompatibleProvider.test.ts",
    "chars": 32074,
    "preview": "/**\n * OpenAI-Compatible Provider Unit Tests\n *\n * Tests for the OpenAI-compatible provider that communicates with\n * re"
  },
  {
    "path": "__tests__/unit/services/providers/registry.test.ts",
    "chars": 6391,
    "preview": "/**\n * ProviderRegistry Unit Tests\n */\n\njest.mock('../../../../src/utils/logger', () => ({\n  __esModule: true,\n  default"
  },
  {
    "path": "__tests__/unit/services/rag/chunking.test.ts",
    "chars": 5554,
    "preview": "import { chunkDocument } from '../../../../src/services/rag/chunking';\n\ndescribe('chunkDocument', () => {\n  it('returns "
  },
  {
    "path": "__tests__/unit/services/rag/database.test.ts",
    "chars": 9058,
    "preview": "import { open } from '@op-engineering/op-sqlite';\n\n// We need to get a reference to the mock DB to control its return va"
  },
  {
    "path": "__tests__/unit/services/rag/embedding.test.ts",
    "chars": 3833,
    "preview": "import { initLlama } from 'llama.rn';\nimport RNFS from 'react-native-fs';\n\njest.mock('../../../../src/utils/logger', () "
  },
  {
    "path": "__tests__/unit/services/rag/index.test.ts",
    "chars": 8270,
    "preview": "jest.mock('../../../../src/services/rag/database', () => ({\n  ragDatabase: {\n    ensureReady: jest.fn(() => Promise.reso"
  },
  {
    "path": "__tests__/unit/services/rag/retrieval.test.ts",
    "chars": 7485,
    "preview": "jest.mock('../../../../src/services/rag/database', () => ({\n  ragDatabase: {\n    getEmbeddingsByProject: jest.fn(() => ["
  },
  {
    "path": "__tests__/unit/services/rag/vectorMath.test.ts",
    "chars": 4875,
    "preview": "import { cosineSimilarity, dotProduct, topKSimilar } from '../../../../src/services/rag/vectorMath';\n\ndescribe('vectorMa"
  },
  {
    "path": "__tests__/unit/services/remoteServerManager.test.ts",
    "chars": 28031,
    "preview": "/**\n * Remote Server Manager Unit Tests\n *\n * Tests for managing remote LLM server connections and provider selection.\n "
  },
  {
    "path": "__tests__/unit/services/restore.test.ts",
    "chars": 12487,
    "preview": "/**\n * restoreInProgressDownloads Unit Tests\n *\n * Tests for the download-restoration logic that re-wires background dow"
  },
  {
    "path": "__tests__/unit/services/toolHandlers.test.ts",
    "chars": 8002,
    "preview": "/**\n * Tool Handlers Unit Tests\n *\n * Tests for the read_url and search_knowledge_base tool handlers.\n */\n\nimport { exec"
  },
  {
    "path": "__tests__/unit/services/tools/handlers.test.ts",
    "chars": 22764,
    "preview": "/**\n * Tool Handlers Unit Tests\n *\n * Tests for executeToolCall dispatcher, calculator, datetime, device info,\n * and we"
  },
  {
    "path": "__tests__/unit/services/tools/registry.test.ts",
    "chars": 5737,
    "preview": "/**\n * Tool Registry Unit Tests\n *\n * Tests for AVAILABLE_TOOLS, getToolsAsOpenAISchema(), and buildToolSystemPromptHint"
  },
  {
    "path": "__tests__/unit/services/voiceService.test.ts",
    "chars": 14781,
    "preview": "/**\n * VoiceService Unit Tests\n *\n * Tests for the Voice recognition service wrapper around @react-native-voice/voice.\n "
  },
  {
    "path": "__tests__/unit/services/whisperService.test.ts",
    "chars": 29865,
    "preview": "/**\n * WhisperService Unit Tests\n *\n * Tests for Whisper speech-to-text service.\n * Priority: P1 - Voice input support.\n"
  },
  {
    "path": "__tests__/unit/stores/appStore.test.ts",
    "chars": 55852,
    "preview": "/**\n * App Store Unit Tests\n *\n * Tests for app-wide state management including models, settings, and image generation.\n"
  },
  {
    "path": "__tests__/unit/stores/appStoreSharePrompt.test.ts",
    "chars": 2271,
    "preview": "/**\n * AppStore Share Prompt Unit Tests\n *\n * Tests for generation count tracking and persistence.\n * Priority: P1 (High"
  },
  {
    "path": "__tests__/unit/stores/authStore.test.ts",
    "chars": 10065,
    "preview": "/**\n * Auth Store Unit Tests\n *\n * Tests for authentication and lockout functionality.\n * Priority: P0 (Critical) - Secu"
  },
  {
    "path": "__tests__/unit/stores/chatStore.test.ts",
    "chars": 44039,
    "preview": "/**\n * Chat Store Unit Tests\n *\n * Tests for conversation and message management in the chat store.\n * Priority: P0 (Cri"
  }
]

// ... and 433 more files (download for full content)

About this extraction

This page contains the full source code of the alichherawalla/off-grid-mobile GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 633 files (59.8 MB), approximately 3.2M tokens, and a symbol index with 1574 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!