How FluidVoice Implements Per-App Prompt Configuration for Dictation and Editing Contexts
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:
// 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. AnilpromptIDindicates the app should use the global default.selectedPromptID(for:): Returns the globally selected prompt ID for a givenPromptMode, serving as the fallback when no app-specific binding exists.
The resolution logic resides in promptResolution(for:appBundleID:):
// 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 computes the current application context through the promptResolutionBundleID property:
// 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:
// 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, 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:
// 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:
let activePrompt = settings.promptResolution(
for: self.activePromptMode ?? .dictate,
appBundleID: self.promptResolutionBundleID
)
Check if an app has a custom binding:
let hasCustomBinding = settings.appPromptBindings.contains {
$0.bundleID == currentBundleID && $0.promptID != nil
}
Programmatically set a global default:
settings.setSelectedPromptID(
myPrompt.id,
for: .edit
)
Summary
- FluidVoice uses
SettingsStore.PromptModeto distinguish between.dictateand.edittranscription contexts. - The
promptResolution(for:appBundleID:)method implements a priority system: app-specific bindings override global defaults. appPromptBindingsstores tuples of bundle IDs and optional prompt IDs, allowing selective overrides while maintaining fallback behavior.NotchContentViews.swiftcaptures the active application context viapromptResolutionBundleIDat 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.
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 →