Skip to main content

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 │
├─────────────────────────────────────┤
│ Services (Protocols) │ ← Injected as any XProtocol
│ LLMService CalendarService │
│ SessionManager SpeechService │
│ HapticService ModelDownloader │
│ ContactsService HabitStore │
├─────────────────────────────────────┤
│ Models / Domain Types │ ← Pure data, no UIKit/SwiftUI
│ ChatMessage ChatSession │
│ CalendarEventPreview ModelInfo │
└─────────────────────────────────────┘

Dependency Injection

LucidPal uses constructor injection throughout. All service dependencies are declared as protocol existentials (any XProtocol), never concrete types.

// ✅ 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: LLMService

LucidPalApp is the sole composition root — the only place concrete services are instantiated:

@main struct LucidPalApp: App {
private let llmService = LLMService()
private let calendarService = CalendarService()
private let hapticService = HapticService()
private let contactsService = ContactsService()
private let habitStore = HabitStore()
// ...injected into SessionListViewModel
}

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

ActorPurpose
@MainActorAll ViewModels and ObservableObjects — guarantees UI updates on main thread
LlamaActorSerial actor wrapping llama.cpp C FFI — serializes inference, safe for async
actor LlamaActor {
// All calls serialized — no data races on C pointers
func generate(prompt: String) async throws -> String { ... }
}

Protocol Inventory

ProtocolConforming TypeMock
LLMServiceProtocolLLMServiceMockLLMService
CalendarServiceProtocolCalendarServiceMockCalendarService
CalendarActionControllerProtocolCalendarActionControllerMockCalendarActionController
SessionManagerProtocolSessionManagerMockSessionManager
SpeechServiceProtocolSpeechServiceMockSpeechService
HapticServiceProtocolHapticServiceMockHapticService
ChatHistoryManagerProtocolChatHistoryManager / NoOpChatHistoryManager
ModelDownloaderProtocolModelDownloaderMockModelDownloader
AppSettingsProtocolAppSettingsMockAppSettings
PinnedPromptsStoreProtocolPinnedPromptsStore
NotificationServiceProtocolNotificationService
LiveActivityServiceProtocolLiveActivityService
NotesStoreProtocolNotesStore
ContactsServiceProtocolContactsService
HabitStoreProtocolHabitStore

File Structure

Sources/
├── App/
│ ├── LucidPalApp.swift ← @main, composition root
│ ├── ContentView.swift ← Root navigation (onboarding → sessions)
│ └── AppDelegate.swift ← UIApplicationDelegate (background tasks)
├── Models/
│ ├── ChatMessage.swift ← Message struct, CalendarEventPreview
│ ├── ChatSession.swift ← Session and SessionMeta types
│ ├── CalendarActionModels.swift← Payload and result types
│ ├── ConversationTemplate.swift← Template definitions for system prompts
│ ├── PinnedPrompt.swift ← Pinned prompt data model
│ ├── LucidPalActivityAttributes.swift ← Live Activity attributes
│ └── ModelInfo.swift ← GGUF model metadata
├── Services/
│ ├── LLMService.swift ← Model load/unload, streaming
│ ├── LlamaActor.swift ← llama.cpp serial actor
│ ├── CalendarService.swift ← EventKit abstraction
│ ├── CalendarActionController.swift ← LLM JSON → calendar action
│ ├── CalendarFreeSlotEngine.swift ← Pure slot-finding algorithm
│ ├── SessionManager.swift ← Multi-session persistence
│ ├── HapticService.swift ← UIImpactFeedbackGenerator wrapper
│ ├── LiveActivityService.swift ← Live Activity start/update/end
│ ├── NotificationService.swift ← UNUserNotificationCenter wrapper
│ ├── PinnedPromptsStore.swift ← Pinned prompts persistence
│ ├── ContactsService.swift ← Contacts framework abstraction
│ ├── ContactsServiceProtocol.swift ← Protocol for contacts access
│ ├── HabitStore.swift ← Habit log persistence (ObservableObject)
│ └── HabitStoreProtocol.swift ← Protocol for habit store
├── ViewModels/
│ ├── ChatViewModel.swift ← Core message/stream logic
│ ├── ChatViewModel+CalendarConfirmation.swift ← Confirm/cancel/undo
│ ├── ChatViewModel+MessageHandling.swift ← Send/stream/live-activity
│ ├── ChatViewModel+Speech.swift ← Voice recording + haptics
│ ├── ChatViewModel+Persistence.swift ← Save/load message history
│ ├── ChatViewModel+Publishers.swift ← Combine subscriptions
│ ├── SessionListViewModel.swift ← Session CRUD + Siri routing
│ ├── SettingsViewModel.swift ← Settings form logic
│ └── AppSettings.swift ← @AppStorage preferences
└── Views/
├── ChatView.swift ← Message list + toolbar
├── ChatView+Banners.swift ← Template pill banners
├── ChatView+InputBar.swift ← Pinned prompt chips + input
├── ChatView+Subviews.swift ← Shared subview builders
├── ChatSessionContainer.swift← Session lifecycle wrapper
├── MessageBubbleView.swift ← Per-message bubble + long-press
├── SessionListView.swift ← Session browser
├── SessionListView+Subviews.swift ← Search bar + row subviews
├── CalendarEventCard.swift ← Event preview card
├── ThinkingDisclosure.swift ← Expandable thinking block
└── SettingsView.swift ← Settings form