Appearance
Architecture Overview
MVVM layers, dependency injection, and actor isolation in LucidPal.
Layer Diagram
┌─────────────────────────────────────┐
│ SwiftUI Views │ ← Layout only, zero business logic
├─────────────────────────────────────┤
│ ViewModels │ ← @MainActor ObservableObject
│ ChatViewModel SessionListViewModel│
│ ModelDownloadViewModel Settings │
│ AgentViewModel TranscriptionViewModel
├─────────────────────────────────────┤
│ Services (Protocols) │ ← Injected as any XProtocol
│ LLMOrchestrator (cloud+local) │
│ LLMService (llama.cpp actor) │
│ CloudLLMService (Gemini API) │
│ CalendarService SessionManager │
│ SpeechService WhisperSpeechService│
│ ContactsService HabitStore │
│ NotesStore NoteEnrichmentService │
│ SystemPromptBuilder ProactiveAgent│
│ ImmichService HealthService │
│ LiveActivityService PremiumManager │
│ WidgetCloudService SyncManager │
│ WebSearchService TTSService │
│ AirPodsVoiceCoordinator │
│ LiveTrialManager EmailTrialManager │
│ AnalyticsConsentManager │
│ DeviceIdentityService │
├─────────────────────────────────────┤
│ Models / Domain Types │ ← Pure data, no UIKit/SwiftUI
│ ChatMessage ChatSession │
│ CalendarEventPreview ModelInfo │
│ HabitModels NoteItem Ability │
└─────────────────────────────────────┘For how the Services layer assembles the AI system prompt, see System Prompt Builder.
Dependency Injection
LucidPal uses constructor injection throughout. All service dependencies are declared as protocol existentials (any XProtocol), never concrete types.
swift
// ✅ Correct — protocol existential
final class ChatViewModel: ObservableObject {
let llmService: any LLMServiceProtocol
let calendarService: any CalendarServiceProtocol
let settings: any AppSettingsProtocol
}
// ❌ Wrong — concrete type (untestable, breaks DI)
let llmService: LLMServiceLucidPalApp is the sole composition root — the only place concrete services are instantiated:
swift
@main struct LucidPalApp: App {
private let llmService = LLMService()
private let calendarService = CalendarService()
private let hapticService = HapticService()
private let contactsService = ContactsService()
private let habitStore = HabitStore()
private let noteEnrichmentService = NoteEnrichmentService()
private let premiumManager = PremiumManager()
private let liveTrialManager = LiveTrialManager()
private let emailTrialManager = EmailTrialManager()
private let analyticsConsentManager = AnalyticsConsentManager()
// all trial managers + analyticsConsentManager injected as .environmentObject()
// emailTrialManager also passed into PremiumManager to gate canUseGmail + canUseExchange + canUseCloudAI
}LucidPalApp also observes UIApplication.willEnterForegroundNotification to refresh AppSettings.notificationsEnabled on every foreground transition, keeping the settings UI in sync with the system permission state.
Actor Isolation
| Actor | Purpose |
|---|---|
@MainActor | All ViewModels and ObservableObjects — guarantees UI updates on main thread |
LlamaActor | Serial actor wrapping llama.cpp C FFI — serializes inference, safe for async |
swift
actor LlamaActor {
// All calls serialized — no data races on C pointers
func generate(prompt: String) async throws -> String { ... }
}See LLM Inference for full LlamaActor internals, model loading, and streaming token generation.
Protocol Inventory
| Protocol | Conforming Type | Mock |
|---|---|---|
LLMServiceProtocol | LLMService | MockLLMService |
CloudLLMServiceProtocol | CloudLLMService | MockCloudLLMService |
CalendarServiceProtocol | CalendarService | MockCalendarService |
DocumentProcessorProtocol | DocumentProcessor | — |
SessionManagerProtocol | SessionManager | MockSessionManager |
SpeechServiceProtocol | SpeechService | MockSpeechService |
HapticServiceProtocol | HapticService | MockHapticService |
ChatHistoryManagerProtocol | ChatHistoryManager / NoOpChatHistoryManager | — |
ModelDownloaderProtocol | ModelDownloader | MockModelDownloader |
AppSettingsProtocol | AppSettings | MockAppSettings |
PinnedPromptsStoreProtocol | PinnedPromptsStore | — |
NotificationServiceProtocol | NotificationService | — |
LiveActivityServiceProtocol | LiveActivityService | — |
NotesStoreProtocol | NotesStore | — |
ContactsServiceProtocol | ContactsService | — |
HabitStoreProtocol | HabitStore | — |
ContextServiceProtocol | ContextService | — |
SuggestedPromptsProviderProtocol | SuggestedPromptsProvider | — |
PremiumStatusProvider | PremiumManager | — |
Note:
NoteEnrichmentServiceandPremiumManagerare concrete services (no protocol) —NoteEnrichmentServiceis injected intoNotesListViewModelfor async LLM-driven note enrichment;PremiumManageris instantiated at the composition root and manages StoreKit subscription state.
Deep-dive pages for key protocols:
SessionManagerProtocol→ SessionsHabitStoreProtocol→ Habit StoreNotesStoreProtocol→ Notes StoreNoteEnrichmentService→ Note EnrichmentContextServiceProtocol/SuggestedPromptsProviderProtocol→ Chat ViewModelDeepgramTranscriptionService/TranscriptionViewModel→ Voice TranscriptionLiveTrialManager/DeviceIdentityService→ Voice Transcription — Free TrialAnalyticsConsentManager/AnalyticsConsentSheet→ Analytics & TelemetryAppTabBar/Tabenum → Navigation & Tab Bar
Model Download Pipeline
The download pipeline involves two services working in sequence:
| Service | Role |
|---|---|
ModelDownloader | Downloads the GGUF file via an iOS background URLSession. Uses resume data to avoid restarting interrupted transfers. Verifies the file with a SHA-256 checksum after each download. |
ModelPageCacheWarmer | After a successful download, prefetches model pages into RAM using mlock-style reads. This reduces the cold-start latency the first time LLMService loads the model. |
ModelDownloader uses session identifier app.lucidpal.model-download, which lets iOS reconnect to an in-progress transfer across app launches. AppDelegate stores the system's completion handler in ModelDownloader.backgroundSessionCompletion so the OS is notified once all background events are processed.
For the full download state machine, background session handling, and cache warming details, see Model Download.
Testing and CI/CD
- Unit and integration test patterns → Testing
- Woodpecker CI pipelines →
.woodpecker/directory (NOT GitHub Actions)
File Structure
Sources/
├── App/
│ ├── LucidPalApp.swift ← @main, composition root
│ ├── ContentView.swift ← Opacity tab switcher + root overlays (see navigation.md)
│ ├── AppTabBar.swift ← Custom tab bar (no UITabBarController)
│ ├── AppDelegate.swift ← UIApplicationDelegate (background tasks)
│ ├── APIEnvironment.swift ← API base URLs and endpoints
│ └── BuildEnvironment.swift ← Build-time flags and feature gates
├── Models/
│ ├── CalendarActionModels.swift ← Payload and result types
│ ├── ChatMessage.swift ← Message struct, CalendarEventPreview
│ ├── ChatSession.swift ← Session and SessionMeta types
│ ├── ContextItem.swift ← Attached context items (documents, images)
│ ├── ConversationTemplate.swift ← Template definitions for system prompts
│ ├── HabitModels.swift ← Habit and habit-log domain types
│ ├── LucidPalActivityAttributes.swift ← Live Activity attributes
│ ├── ModelInfo.swift ← GGUF model metadata
│ ├── NoteItem.swift ← Note model with all AI metadata fields
│ ├── PinnedPrompt.swift ← Pinned prompt data model
│ ├── ReminderPreview.swift ← Reminder preview for display
│ └── SpeakerSegment.swift ← Single speaker-diarized transcript segment
├── Intents/
│ ├── LucidPalShortcuts.swift ← AppShortcutsProvider (iOS 16.4+)
│ ├── AskLucidPalIntent.swift ← "Ask LucidPal [question]"
│ ├── StartVoiceIntent.swift ← "Talk to LucidPal"
│ ├── CheckCalendarIntent.swift ← "Check my LucidPal calendar"
│ ├── AddCalendarEventIntent.swift ← "Add [event] to LucidPal"
│ ├── FindFreeTimeShortcutIntent.swift ← "Find free time in LucidPal"
│ ├── DeleteCalendarEventIntent.swift ← background delete
│ ├── UndoLastDeletionIntent.swift ← undo last calendar action
│ ├── AgentTaskIntent.swift ← "Ask LucidPal Agent [task]"
│ ├── SaveNoteIntent.swift ← background note save
│ ├── FindContactIntent.swift ← background contact lookup
│ ├── LogHabitIntent.swift ← background habit log
│ ├── SetReminderIntent.swift ← background reminder set
│ ├── SiriContextStore.swift ← Last-action store for undo
│ └── SiriCalendarBridge.swift ← Siri ↔ EventKit bridge
├── Services/
│ ├── AirPodsVoiceCoordinator.swift ← AirPods mic routing coordinator
│ ├── AudioRouteMonitor.swift ← AVAudioSession route-change observer
│ ├── CalendarActionController.swift ← LLM JSON → calendar action
│ ├── CalendarActionController+Helpers.swift ← Action controller utilities
│ ├── CalendarError.swift ← Calendar error types
│ ├── CalendarFreeSlotEngine.swift ← Pure slot-finding algorithm
│ ├── CalendarPromptSection.swift ← Calendar section of system prompt
│ ├── CalendarService.swift ← EventKit abstraction
│ ├── CalendarServiceProtocol.swift ← Protocol for calendar access
│ ├── ChatHistoryManager.swift ← Message history persistence
│ ├── CloudLLMService.swift ← Gemini API client
│ ├── CloudLLMServiceProtocol.swift ← Protocol for cloud LLM
│ ├── ContactsActionController.swift ← LLM JSON → contacts action
│ ├── ContactsPromptSection.swift ← Contacts section of system prompt
│ ├── ContactsService.swift ← Contacts framework abstraction
│ ├── ContactsServiceProtocol.swift ← Protocol for contacts access
│ ├── ContextService.swift ← Attached context item management
│ ├── ContextServiceProtocol.swift ← Protocol for context service
│ ├── DebugLogStore.swift ← In-memory debug log storage
│ ├── DocumentProcessor.swift ← PDF/document text extraction
│ ├── DocumentProcessorProtocol.swift← Protocol for document processing
│ ├── GmailService.swift ← Gmail API integration
│ ├── GmailServiceProtocol.swift ← Protocol for Gmail
│ ├── GmailPromptSection.swift ← Gmail section of system prompt
│ ├── HabitActionController.swift ← LLM JSON → habit action
│ ├── HabitPromptSection.swift ← Habit section of system prompt
│ ├── HabitStore.swift ← Habit log persistence (ObservableObject)
│ ├── HabitStoreProtocol.swift ← Protocol for habit store
│ ├── HapticService.swift ← UIImpactFeedbackGenerator wrapper
│ ├── HealthService.swift ← HealthKit abstraction
│ ├── ImmichService.swift ← Photo server (Immich) integration
│ ├── LiveActivityService.swift ← Live Activity start/update/end
│ ├── LlamaActor.swift ← llama.cpp serial actor (base)
│ ├── LlamaActor+Generate.swift ← Token generation extension
│ ├── LlamaActor+KVCache.swift ← KV-cache profiling extension
│ ├── LlamaActor+Tokenize.swift ← Tokenization extension
│ ├── LLMOrchestrator.swift ← Cloud/local routing decision
│ ├── LLMService.swift ← Model load/unload, streaming
│ ├── LLMServiceProtocol.swift ← Protocol for LLM service
│ ├── LocationService.swift ← CoreLocation geocoding wrapper
│ ├── ModelDownloader.swift ← GGUF download + checksum verification
│ ├── ModelPageCacheWarmer.swift ← Prefetch model pages into RAM
│ ├── NoteActionController.swift ← LLM JSON → note action
│ ├── NoteEnrichmentService.swift ← Async LLM enrichment for notes
│ ├── NotesPromptSection.swift ← Notes section of system prompt
│ ├── NotesStore.swift ← Notes persistence (ObservableObject)
│ ├── NotesStoreProtocol.swift ← Protocol for notes store
│ ├── NotificationService.swift ← UNUserNotificationCenter wrapper
│ ├── PinnedPromptsStore.swift ← Pinned prompts persistence
│ ├── ProactiveAgentService.swift ← BGTaskScheduler + morning briefing
│ ├── PromptSection.swift ← Base protocol for prompt sections
│ ├── ReminderActionController.swift ← LLM JSON → reminder action
│ ├── ReminderPromptSection.swift ← Reminder section of system prompt
│ ├── SessionManager.swift ← Multi-session persistence
│ ├── SpeechService.swift ← AVFoundation speech recognition
│ ├── SpeechServiceProtocol.swift ← Protocol for speech service
│ ├── SuggestedPromptsProvider.swift ← Context-aware prompt suggestions
│ ├── SyncManager.swift ← Offline-first sync orchestration
│ ├── SystemPromptBuilder.swift ← Assembles full system prompt
│ ├── TTSService.swift ← Text-to-speech service
│ ├── VisionImageProcessor.swift ← Image resize + base64 for vision models
│ ├── WebSearchService.swift ← Web search integration
│ ├── WidgetCloudService.swift ← Widget data refresh via R2
│ ├── AnalyticsConsentManager.swift ← Consent state + one-shot telemetry send
│ ├── DeviceIdentityService.swift ← Keychain-backed stable device UUID
│ ├── LiveTrialManager.swift ← Free trial balance cache (30 min cap)
│ ├── EmailTrialManager.swift ← 30-day unified email trial (Gmail + Exchange), hint scheduling, query counter
│ ├── DeepgramTranscriptionService.swift ← Live Deepgram WebSocket + diarization
│ └── WhisperSpeechService.swift ← On-device Whisper transcription
├── ViewModels/
│ ├── AgentViewModel.swift ← Agent mode orchestration + tool execution
│ ├── AgentViewModelDependencies.swift ← Dependency container for AgentViewModel
│ ├── AppSettings.swift ← @AppStorage preferences
│ ├── AppSettingsProtocol.swift ← Protocol for app settings
│ ├── ChatConstants.swift ← Shared chat constants (token limits etc.)
│ ├── ChatViewModel.swift ← Core message/stream logic
│ ├── ChatViewModel+CalendarConfirmation.swift ← Confirm/cancel/undo
│ ├── ChatViewModel+MessageHandling.swift ← Send/stream/live-activity
│ ├── ChatViewModel+Persistence.swift ← Save/load message history
│ ├── ChatViewModel+Publishers.swift ← Combine subscriptions
│ ├── ChatViewModel+Speech.swift ← Voice recording + haptics
│ ├── ChatViewModelDependencies.swift ← Dependency container for ChatViewModel
│ ├── ModelDownloadViewModel.swift ← Model download progress and state
│ ├── SessionListViewModel.swift ← Session CRUD + Siri routing
│ ├── SessionListViewModelDependencies.swift ← Dependency container for SessionListViewModel
│ ├── SettingsViewModel.swift ← Settings form logic
│ ├── TranscriptionViewModel.swift ← Phase state machine + summarize API call
│ └── UserDefaultsKeys.swift ← UserDefaults key constants
└── Views/
├── AgentView.swift ← Agent mode UI (orb, abilities drawer)
├── BulkDeletionBar.swift ← Multi-select delete toolbar
├── CalendarActionPill.swift ← Inline calendar action confirmation
├── CalendarEventCard.swift ← Event preview card
├── CalendarEventCard+Pending.swift← Pending-confirmation card state
├── CalendarEventCard+Subviews.swift← Event card subview builders
├── CalendarEventListCard.swift ← List of calendar events card
├── CalendarQueryResultCard.swift ← Calendar query result display
├── ChatInputBar.swift ← Text input bar component
├── ChatSessionContainer.swift ← Session lifecycle wrapper
├── ChatView.swift ← Message list + toolbar
├── ChatView+Banners.swift ← Template pill banners
├── ChatView+InputBar.swift ← Pinned prompt chips + input
├── ChatView+Subviews.swift ← Shared subview builders
├── ConflictDetailSheet.swift ← Scheduling conflict detail sheet
├── ContactResultCard.swift ← Contact lookup result card
├── CreateEventSheet.swift ← Manual event creation form
├── DebugLogView.swift ← In-app debug log viewer
├── DesignConstants.swift ← Shared design tokens
├── DocumentAttachmentPill.swift ← Document attachment chip
├── DocumentPickerButton.swift ← Document picker trigger button
├── EmailAuthView.swift ← Gmail OAuth sign-in
├── ETACard.swift ← ETA result card
├── ExchangeEmailCard.swift ← Exchange email result card
├── GmailEmailCard.swift ← Gmail result card
├── GmailSignInView.swift ← Gmail sign-in UI
├── HabitCard.swift ← Habit summary card
├── HabitCreationSheet.swift ← New habit creation form
├── HabitDashboardView.swift ← Habit overview dashboard
├── HabitDetailView.swift ← Single habit detail and log
├── HabitLogSheet.swift ← Log a habit entry sheet
├── HabitStreakCard.swift ← Streak display card
├── HealthInsightsView.swift ← Health AI insights view
├── HealthSummaryCard.swift ← Health summary card
├── IntegrationsSettingsView.swift ← Third-party integrations settings
├── MailComposeView.swift ← Email compose view
├── MemoryView.swift ← AI memory browser
├── MessageBubbleView.swift ← Per-message bubble + long-press
├── MessageBubbleView+ImageViewer.swift ← Full-screen image viewer
├── ModelCatalogSheet.swift ← Model selection sheet
├── ModelDownloadView.swift ← Model download progress screen
├── ModelLoadingOverlay.swift ← Model load in-progress overlay
├── NoteCard.swift ← Note summary card
├── NoteEditorView.swift ← Note editing view
├── NotesListView.swift ← Notes list browser
├── OfflineFallbackSheet.swift ← Offline fallback UI
├── OnboardingCarouselView.swift ← First-launch onboarding carousel
├── PlansCardView.swift ← Subscription plans card
├── PremiumOverlayModifier.swift ← Premium gate modifier
├── ProactiveAISettingsView.swift ← Proactive AI settings
├── ReminderCard.swift ← Reminder result card
├── RichMarkdownEditor.swift ← Markdown editor for notes
├── SessionDetailView.swift ← Voice session detail view
├── SessionListView.swift ← Session browser
├── SessionListView+Subviews.swift ← Search bar + row subviews
├── SessionRecordView.swift ← Voice recording session view
├── SessionSetupSheet.swift ← Session setup form
├── SettingsView.swift ← Settings form
├── SettingsView+APIAccess.swift ← API access settings
├── SettingsView+DataExport.swift ← Data export settings
├── SettingsView+Exchange.swift ← Exchange account settings
├── SettingsView+Immich.swift ← Immich integration settings
├── SettingsView+Memory.swift ← AI memory settings
├── SettingsView+Shortcuts.swift ← Siri shortcuts settings section
├── SettingsView+Sync.swift ← Cloud sync settings
├── SettingsView+VisionSection.swift← Vision model settings section
├── SettingsView+Widgets.swift ← Widget settings
├── SFSymbolPicker.swift ← Emoji/symbol picker
├── SiriEventCard.swift ← Siri-triggered event card
├── SuggestedPromptsView.swift ← Contextual prompt suggestions UI
├── AnalyticsConsentSheet.swift ← Analytics opt-in consent sheet (see analytics.md)
├── LiveNotesTrialModal.swift ← First-session free trial info modal
├── EmailTrialOnboardingSheet.swift ← One-time onboarding after Gmail/Exchange connect (context-aware prompts + AI badge)
├── EmailTrialWarningModal.swift ← Day 25/28/exhausted loss-framed upgrade prompts (context-aware copy)
├── EmailHintCard.swift ← Day-paced contextual prompt hints in chat (day 1/3/7, context-aware)
├── ThinkingDisclosure.swift ← Expandable thinking block
├── ToastView.swift ← Ephemeral toast notification
├── TranscriptionView.swift ← Full-screen voice transcription UI
├── UnsupportedDeviceView.swift ← Low-RAM unsupported device screen
├── UpgradeView.swift ← Subscription upgrade view
├── View+PremiumShadow.swift ← Premium shadow view modifier
├── VoiceRecordingOverlay.swift ← Voice recording in-progress overlay
├── WeatherCard.swift ← Weather result card
├── WebSearchSettingsView.swift ← Web search settings section
└── WidgetsSettingsView.swift ← Widget settings