# How FluidVoice Implements Per-App Prompt Configuration for Dictation and Editing Contexts

> Discover how FluidVoice enables per app prompt configuration for dictation and editing. Learn its fallback logic using SettingsStore for dictation and editing.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: deep-dive
- Published: 2026-07-01

---

**FluidVoice resolves the active prompt for each transcription session by first checking for an app-specific binding in `SettingsStore`, then falling back to the globally selected prompt for the current mode (Dictate or Edit).**

FluidVoice provides granular control over AI transcription behavior by allowing users to assign distinct prompts to individual applications and contexts. The system supports two primary transcription modes—**Dictate** and **Edit**—and maintains separate prompt configurations for each app bundle ID via a centralized `SettingsStore`.

## The SettingsStore Architecture

The prompt configuration system centers on the `SettingsStore` model defined in `Sources/Fluid/UI/AISettingsView+AIConfiguration.swift`. This store manages the relationship between applications, transcription contexts, and their assigned AI prompts.

### PromptMode Enumeration

At the core of the system lies the `PromptMode` enum, which strictly types the two transcription contexts:

```swift
// AISettingsView+AIConfiguration.swift
enum PromptMode {
    case dictate
    case edit
}

```

These modes ensure that Mail might use a formal rewriting prompt during editing while using a quick-capture prompt during dictation, with both configurations stored independently.

### App-Specific Bindings vs. Global Defaults

The `SettingsStore` maintains prompt assignments through two parallel mechanisms:

- **`appPromptBindings`**: A collection of `(appBundleID, promptID?)` tuples that maps specific applications to custom prompts. A `nil` `promptID` indicates the app should use the global default.
- **`selectedPromptID(for:)`**: Returns the globally selected prompt ID for a given `PromptMode`, serving as the fallback when no app-specific binding exists.

The resolution logic resides in `promptResolution(for:appBundleID:)`:

```swift
// AISettingsView+AIConfiguration.swift
func promptResolution(for mode: PromptMode, appBundleID: String?) -> Prompt {
    // Check for app-specific binding first
    if let binding = appPromptBindings.first(where: { $0.bundleID == appBundleID }),
       let promptID = binding.promptID {
        return prompts.first { $0.id == promptID }!
    }
    // Fall back to global selection for this mode
    return prompts.first { $0.id == selectedPromptID(for: mode) }!
}

```

## Runtime Prompt Resolution Flow

When a user initiates transcription, FluidVoice determines the appropriate prompt through a three-phase resolution process.

### 1. Capture Active Context

[`NotchContentViews.swift`](https://github.com/altic-dev/FluidVoice/blob/main/NotchContentViews.swift) computes the current application context through the `promptResolutionBundleID` property:

```swift
// NotchContentViews.swift (line 483)
private var promptResolutionBundleID: String? {
    // Returns bundle ID of frontmost application
    NSWorkspace.shared.frontmostApplication?.bundleIdentifier
}

```

### 2. Resolve the Prompt

The view layer calls `promptResolution` with the active mode and bundle ID:

```swift
// Example from DictationE2ETests.swift (line 438)
let activePrompt = settings.promptResolution(
    for: .dictate, 
    appBundleID: "com.apple.mail"
)

```

### 3. Apply Configuration

The resolution method checks `appPromptBindings` for an entry matching the current bundle ID. If found, it returns the associated prompt; otherwise, it retrieves the global default via `selectedPromptID(for:)`. This guarantees every transcription context always resolves to a valid prompt configuration.

## Configuring Prompts via the UI

Users interact with the prompt system through a dedicated picker overlay, accessible via the default shortcut **⌃⌥P**.

### The Prompt Picker Overlay

The overlay UI, implemented in [`Sources/Fluid/UI/AISettingsView.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/UI/AISettingsView.swift), displays available prompt profiles and indicates whether the current selection is global or app-specific. When a user selects a profile, the system updates the binding:

```swift
// AISettingsView.swift
settings.setAppPromptBinding(
    bundleID: currentAppBundleID,
    promptID: selectedProfile.id  // Pass nil to revert to global default
)

```

The UI highlights "Default" when showing the global prompt, or the specific profile name when an app-specific override is active, providing clear visual feedback about the current configuration state.

## Practical Implementation Examples

Here are common patterns for working with the per-app prompt configuration system:

**Resolve the active prompt for the current session:**

```swift
let activePrompt = settings.promptResolution(
    for: self.activePromptMode ?? .dictate,
    appBundleID: self.promptResolutionBundleID
)

```

**Check if an app has a custom binding:**

```swift
let hasCustomBinding = settings.appPromptBindings.contains { 
    $0.bundleID == currentBundleID && $0.promptID != nil 
}

```

**Programmatically set a global default:**

```swift
settings.setSelectedPromptID(
    myPrompt.id, 
    for: .edit
)

```

## Summary

- FluidVoice uses `SettingsStore.PromptMode` to distinguish between `.dictate` and `.edit` transcription contexts.
- The `promptResolution(for:appBundleID:)` method implements a priority system: app-specific bindings override global defaults.
- `appPromptBindings` stores tuples of bundle IDs and optional prompt IDs, allowing selective overrides while maintaining fallback behavior.
- [`NotchContentViews.swift`](https://github.com/altic-dev/FluidVoice/blob/main/NotchContentViews.swift) captures the active application context via `promptResolutionBundleID` at runtime.
- Users manage configurations through a picker overlay that updates bindings via `setAppPromptBinding(bundleID:promptID:)`.

## Frequently Asked Questions

### How does FluidVoice decide which prompt to use when I start dictating?

FluidVoice calls `settings.promptResolution(for:appBundleID:)` with the current mode (Dictate or Edit) and the frontmost application's bundle ID. According to the source code in `AISettingsView+AIConfiguration.swift`, it first checks `appPromptBindings` for a matching bundle ID with a non-nil prompt ID. If found, it uses that specific prompt; otherwise, it falls back to the global prompt selected for that mode.

### Can I set different prompts for the same app in Dictate mode versus Edit mode?

Yes. The `appPromptBindings` collection associates prompt IDs with specific applications, while the `PromptMode` enum (`.dictate` or `.edit`) determines which global prompt serves as the fallback. Since the resolution method accepts the mode as a parameter, you can maintain separate app-specific configurations for each context by setting bindings while in different modes.

### What happens if I delete a prompt that is assigned to a specific app?

If a prompt ID referenced in `appPromptBindings` no longer exists in the available prompts collection, the `promptResolution` method would fail to find a matching prompt and fall back to the global default for that mode. The system ensures continuity by always having a valid fallback via `selectedPromptID(for:)`.

### Where is the per-app prompt data stored?

The prompt binding data persists within the `SettingsStore` model as part of `appPromptBindings`. This store manages the collection of `(appBundleID, promptID?)` tuples alongside the global `selectedPromptID` preferences, ensuring that both app-specific and global configurations persist across application launches.