How FluidVoice Manages Per-App Configuration and Different Prompt Sets

FluidVoice uses a hierarchical resolution system where SettingsStore.promptResolution(for:appBundleID:) checks for app-specific bindings first, then falls back to global defaults, supporting distinct prompt sets for Dictate and Edit modes across different applications.

FluidVoice is an open-source AI dictation tool for macOS that enables context-aware transcription through customizable prompt configurations. The repository implements a sophisticated per-app configuration system that allows users to assign distinct prompts to individual applications while maintaining global defaults for seamless fallback.

Understanding the Configuration Architecture

The SettingsStore Model

At the core of FluidVoice's configuration system lies the SettingsStore class, defined in Sources/Fluid/UI/AISettingsView+AIConfiguration.swift. This model maintains the relationship between applications and their assigned prompts through two key properties: appPromptBindings and selectedPromptID(for:).

The appPromptBindings collection stores tuples of (appBundleID, promptID?), where a nil value indicates the app should use the global default. The selectedPromptID(for:) method retrieves the globally chosen prompt for a given transcription mode.

PromptMode Enumeration

FluidVoice distinguishes between two transcription contexts using the SettingsStore.PromptMode enum:

  • .dictate — For standard dictation sessions
  • .edit — For editing existing text

This enumeration allows the system to maintain separate global defaults for each mode while supporting per-app overrides for both contexts.

How Prompt Resolution Works

The resolution logic centers on the promptResolution(for:appBundleID:) method in SettingsStore. This method implements a fallback hierarchy:

  1. Check if an app-specific binding exists for the provided bundle ID
  2. If found, return the bound promptID
  3. If no binding exists (or if promptID is nil), return the global selectedPromptID(for:) for the current mode
// Example from DictationE2ETests.swift (line 438)
let mailResolution = settings.promptResolution(for: .dictate, appBundleID: "com.apple.mail")

This ensures every transcription context always has a valid prompt, whether customized or default.

Runtime Flow and UI Integration

The live prompt resolution occurs in Sources/Fluid/Views/NotchContentViews.swift. When a user initiates dictation, the view computes the promptResolutionBundleID property to determine the current application's bundle identifier.

// NotchContentViews.swift (line 483, excerpt)
private var promptResolutionBundleID: String? { /* … */ }

// Resolution call
let activePrompt = settings.promptResolution(
    for: self.activePromptMode ?? .dictate,
    appBundleID: self.promptResolutionBundleID
)

The UI overlay (triggered by ⌃⌥P by default) displays the global list of profiles while highlighting any app-specific selection. When users choose a profile, the system updates appPromptBindings for the active app via setAppPromptBinding(bundleID:promptID:).

Configuring App-Specific Prompts

To establish or modify per-app configurations, FluidVoice exposes the setAppPromptBinding method. Passing a nil promptID removes the app-specific override and reverts to global defaults.

// Update an app‑specific binding from the UI
settings.setAppPromptBinding(
    bundleID: currentAppBundleID,
    promptID: selectedProfile.id   // nil → use global default
)

This design allows seamless switching between global and per-app prompt sets without requiring complex configuration workflows.

Summary

  • Hierarchical resolution: SettingsStore.promptResolution(for:appBundleID:) prioritizes app-specific bindings over global defaults
  • Dual context support: Separate prompt sets for .dictate and .edit modes via PromptMode enum
  • Dynamic UI integration: NotchContentViews.swift handles live resolution using promptResolutionBundleID
  • Flexible binding management: appPromptBindings stores tuples with optional promptID values, where nil indicates global fallback
  • Key implementation files: AISettingsView+AIConfiguration.swift contains the model logic, while NotchContentViews.swift handles the runtime resolution

Frequently Asked Questions

How does FluidVoice determine which prompt to use for a specific application?

FluidVoice determines the active prompt by calling SettingsStore.promptResolution(for:appBundleID:) with the current PromptMode and the active application's bundle identifier. According to the altic-dev/FluidVoice source code, the method first checks appPromptBindings for a specific assignment; if none exists or the binding is nil, it returns the global prompt selected for that mode via selectedPromptID(for:).

Can I set different prompts for dictation versus editing in the same app?

Yes. FluidVoice maintains separate global defaults for each mode through selectedPromptID(for:), which accepts either .dictate or .edit. When setting app-specific bindings via setAppPromptBinding, these respect the currently active mode, allowing distinct prompts for dictation and editing contexts within the same application.

What happens if I haven't configured a per-app prompt for a specific application?

If no app-specific binding exists in appPromptBindings, or if the binding contains a nil promptID, FluidVoice automatically falls back to the global default prompt for the current mode. This fallback mechanism ensures uninterrupted transcription functionality without requiring manual configuration for every application.

Where is the per-app configuration data stored in the codebase?

The configuration logic resides primarily in Sources/Fluid/UI/AISettingsView+AIConfiguration.swift, which defines the SettingsStore class, PromptMode enum, and the promptResolution method. The runtime application of these settings occurs in Sources/Fluid/Views/NotchContentViews.swift, specifically within the promptResolutionBundleID computed property and related UI state management. Integration tests demonstrating this behavior are available in Tests/FluidDictationIntegrationTests/DictationE2ETests.swift.

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 →