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:

  1. App binding (if present, uses specified profile or default)
  2. Prompt-routing scope (if "selected apps only" and app not in list, falls back to built-in)
  3. User-selected profile (from global selection)
  4. 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 ObservableObject located in Sources/Fluid/Persistence/SettingsStore.swift that serves as the single source of truth for FluidVoice configuration.
  • Persistence uses UserDefaults for preferences and KeychainService for 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 SMAppService and 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →