How FluidVoice SettingsStore Works: Complete Technical Guide
SettingsStore is the central, observable singleton that persists all user-configurable state in FluidVoice using UserDefaults, Keychain, and SwiftUI's ObservableObject protocol to provide reactive updates across the macOS app.
FluidVoice is an open-source macOS dictation application developed by altic-dev. At its core, the SettingsStore class manages everything from AI prompt configurations to launch-at-login preferences. This singleton observable object lives in Sources/Fluid/Persistence/SettingsStore.swift and serves as the single source of truth for all user settings, extended across modular files to maintain clean separation of concerns.
Singleton Architecture and Observable Pattern
The SettingsStore implements a strict singleton pattern to ensure consistency across the application lifecycle.
The Shared Instance
The store exposes a single shared instance via static let shared, preventing multiple initialization points that could cause state desynchronization. The class declaration in Sources/Fluid/Persistence/SettingsStore.swift conforms to ObservableObject, enabling SwiftUI views to react instantly to configuration changes:
final class SettingsStore: ObservableObject {
static let shared = SettingsStore()
private let defaults = UserDefaults.standard
private let keychain = KeychainService.shared
// ...
}
Initialization and Migration
The private initializer runs a cascade of migration helpers to maintain backward compatibility when users upgrade FluidVoice. According to the source code, this includes migrateTranscriptionStartSoundIfNeeded() and migrateProviderAPIKeysIfNeeded(), ensuring legacy data formats transition smoothly to current schemas.
The initialization also calls refreshLaunchAtStartupStatus(clearError: true, logMismatch: false) to synchronize the UI state with the actual macOS login item status at launch.
Data Persistence Strategy
UserDefaults Integration
All user-visible preferences read from and write to UserDefaults.standard. Each property uses well-named keys defined in enum extensions. For example, the launch-at-startup preference uses keys defined in Sources/Fluid/Persistence/SettingsStore+LaunchAtStartup.swift:
private enum LaunchAtStartupKeys {
static let preference = "LaunchAtStartup"
static let legacyCompatibilityItem = "LaunchAtStartupCompatibilityFallback"
}
Keychain for Sensitive Data
The store maintains a reference to KeychainService.shared for secure storage of API keys and other sensitive credentials, keeping them out of unsecured UserDefaults.
Prompt Profile Management System
FluidVoice supports named dictation prompt profiles that allow users to customize AI behavior for different contexts.
DictationPromptProfile Structure
Profiles are stored as an array of JSON-encoded objects representing the DictationPromptProfile struct. Each profile contains a name, prompt text, mode (dictate or edit), and context inclusion flag. The getter and setter in SettingsStore.swift handle the encoding/decoding automatically:
var dictationPromptProfiles: [DictationPromptProfile] {
get { /* decode from defaults */ }
set { /* encode & store */ }
}
Selection Enumeration
The DictationPromptSelection enum defines four states:
- .off – feature disabled
- .default – use built-in default
- .privateAI – use internal Private-AI prompt
- .profile(id) – use a user-created profile
The currently selected option persists under the key selectedDictationPromptID.
Prompt Routing Scope
As implemented in Sources/Fluid/Persistence/SettingsStore+PromptRouting.swift, the PromptRoutingScope determines whether the selected prompt applies globally or only to specific applications. This scope is stored per mode (dictate vs. edit) and evaluated during prompt resolution.
App-Specific Prompt Resolution
The Binding System
For per-application overrides, SettingsStore maintains an array of AppPromptBinding objects. Each binding records the target PromptMode, the app's bundle identifier, and an optional forced promptID. The upsertAppPromptBinding(for:appBundleID:appName:promptID:) method creates or updates these bindings:
func upsertAppPromptBinding(
for mode: PromptMode,
appBundleID: String,
appName: String,
promptID: String?
) { /* ... */ }
Resolution Hierarchy
When resolving the effective prompt for a specific mode and app, the store evaluates this priority chain:
- App binding (if present, uses specified profile or default)
- Prompt-routing scope (if "selected apps only" and app not in list, falls back to built-in)
- User-selected profile (from global selection)
- Built-in default (or user-provided override)
The core resolver promptResolution(for:appBundleID:) implements this logic in Sources/Fluid/Persistence/SettingsStore.swift (lines 443-511).
Default Prompt Overrides
Users may replace built-in defaults via defaultDictationPromptOverride and defaultEditPromptOverride keys. The store automatically strips any hidden base prompt (immutable instruction parts) before persisting overrides, ensuring the UI displays only the editable body.
macOS System Integration
Launch at Startup
The Sources/Fluid/Persistence/SettingsStore+LaunchAtStartup.swift extension bridges FluidVoice settings with macOS login items. The setLaunchAtStartup(_:) method registers or unregisters the service using SMAppService and updates the stored preference:
func setLaunchAtStartup(_ enabled: Bool) {
// Registers/unregisters SMAppService and syncs state
}
The refreshLaunchAtStartupStatus method reads the real login-item state via SMAppService.mainApp.status and forces the stored preference to match the system reality, preventing UI drift when users modify login items outside the app.
Error Handling
The store exposes launchAtStartupErrorMessage and launchAtStartupStatusMessage properties to communicate macOS permission issues (such as unsigned builds) to the UI in human-readable terms.
Nemotron Language Configuration
As defined in Sources/Fluid/Persistence/SettingsStore+NemotronLanguage.swift, the NemotronLanguage struct enumerates supported language identifiers for the Nemotron LLM integration. It provides displayName properties for UI presentation and handles legacy identifier mapping to ensure settings persist across model updates.
Practical Code Examples
Toggling Launch at Startup
import Fluid
let store = SettingsStore.shared
store.setLaunchAtStartup(true) // Enable
print(store.launchAtStartupEnabled) // → true (if macOS allowed it)
print(store.launchAtStartupStatusMessage)
Adding a Custom Dictation Profile
import Fluid
let store = SettingsStore.shared
let newProfile = SettingsStore.DictationPromptProfile(
name: "My Academic Prompt",
prompt: SettingsStore.baseDictationPromptText() + "\n\nPlease keep citations accurate.",
mode: .dictate,
includeContext: true
)
var profiles = store.dictationPromptProfiles
profiles.append(newProfile)
store.dictationPromptProfiles = profiles // Persists via UserDefaults
Resolving the Effective System Prompt
let mode = SettingsStore.PromptMode.dictate
let systemPrompt = SettingsStore.shared.effectiveSystemPrompt(for: mode)
let userMessage = SettingsStore.renderDictationUserMessage(
promptText: systemPrompt,
transcript: "send an email tomorrow"
)
// userMessage now contains the full prompt + transcript ready for the LLM
Summary
- SettingsStore is a singleton
ObservableObjectlocated inSources/Fluid/Persistence/SettingsStore.swiftthat serves as the single source of truth for FluidVoice configuration. - Persistence uses
UserDefaultsfor preferences andKeychainServicefor sensitive data, with automatic JSON encoding for complex types like prompt profiles. - Prompt resolution follows a hierarchical chain: app-specific bindings → routing scope → user selection → default overrides.
- macOS integration synchronizes launch-at-login status with
SMAppServiceand handles migration automatically during initialization. - Reactive updates via
objectWillChange.send()ensure SwiftUI views reflect changes immediately without manual notification management.
Frequently Asked Questions
Where is the SettingsStore located in the FluidVoice repository?
The core implementation resides in Sources/Fluid/Persistence/SettingsStore.swift. The class is extended across several modular files including SettingsStore+PromptRouting.swift, SettingsStore+LaunchAtStartup.swift, and SettingsStore+NemotronLanguage.swift to organize functionality by domain while maintaining the singleton pattern.
How does SettingsStore handle data migration between app versions?
During initialization, the private init() method executes migration helpers such as migrateTranscriptionStartSoundIfNeeded() and migrateProviderAPIKeysIfNeeded(). These methods check for legacy data formats and transform them to current schemas before the store becomes available to the rest of the application, ensuring seamless upgrades for existing users.
What is the difference between prompt profiles and prompt selections?
Prompt profiles are reusable templates (structs containing name, text, and settings) stored as JSON arrays. Prompt selections represent the current operational state—whether the feature is off, using the default, using the private AI option, or referencing a specific profile by ID. The selection determines which profile (if any) is active, while the profile defines the actual AI behavior.
How does SettingsStore sync with macOS login items?
The store calls refreshLaunchAtStartupStatus(clearError: true, logMismatch: false) during initialization to read the actual system state via SMAppService.mainApp.status. When users toggle the preference via setLaunchAtStartup(_:), the method registers or unregisters the login item and immediately syncs the stored boolean to match the result, handling errors like unsigned build restrictions by exposing them through launchAtStartupErrorMessage.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →