Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,14 @@ final class SettingsStore: ObservableObject {
}
}

var sendCustomPromptOnly: Bool {
get { self.defaults.bool(forKey: Keys.sendCustomPromptOnly) }
set {
objectWillChange.send()
self.defaults.set(newValue, forKey: Keys.sendCustomPromptOnly)
Comment on lines +342 to +346

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve custom-only prompt setting in backups

This new persisted setting is not included in SettingsBackupPayload/settingsBackupPayload() and restore(from:) never reads it back, so exporting and importing a backup silently resets the toggle to the default false. For users who rely on custom-only Dictate prompts, a restored machine will start prepending the built-in base prompt again even though their prompt profiles and selections were restored.

Useful? React with 👍 / 👎.

}
}

var isEditPromptOff: Bool {
get { self.defaults.bool(forKey: Keys.editPromptOff) }
set {
Expand Down Expand Up @@ -1034,7 +1042,7 @@ final class SettingsStore: ObservableObject {
profile: profile,
appBinding: binding,
promptBody: body,
systemPrompt: Self.combineBasePrompt(for: normalizedMode, with: body)
systemPrompt: self.systemPrompt(forCustomProfileBody: body, mode: normalizedMode)
)
}
}
Expand Down Expand Up @@ -1063,7 +1071,7 @@ final class SettingsStore: ObservableObject {
profile: profile,
appBinding: nil,
promptBody: body,
systemPrompt: Self.combineBasePrompt(for: normalizedMode, with: body)
systemPrompt: self.systemPrompt(forCustomProfileBody: body, mode: normalizedMode)
)
}
}
Expand Down Expand Up @@ -1113,7 +1121,7 @@ final class SettingsStore: ObservableObject {
}
let body = Self.stripBasePrompt(for: .dictate, from: profile.prompt)
if !body.isEmpty {
return Self.combineBasePrompt(for: .dictate, with: body)
return self.systemPrompt(forCustomProfileBody: body, mode: .dictate)
}
return self.effectiveSystemPrompt(for: .dictate, appBundleID: appBundleID)
}
Expand Down Expand Up @@ -1188,6 +1196,15 @@ final class SettingsStore: ObservableObject {
)
}

private func systemPrompt(forCustomProfileBody body: String, mode: PromptMode) -> String {
let normalizedMode = mode.normalized
let trimmedBody = body.trimmingCharacters(in: .whitespacesAndNewlines)
if normalizedMode == .dictate, self.sendCustomPromptOnly {
return trimmedBody
Comment on lines +1202 to +1203

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor custom-only prompts on the recording path

When a user enables this toggle and selects a custom Dictate prompt for the primary or secondary shortcut, the live recording path never reaches this new helper: applyDictationShortcutSelectionContext builds promptModeOverrideText with SettingsStore.combineBasePrompt(...) (ContentView.swift:3399-3402), and processTextWithAI prefers that non-empty overrideSystemPrompt before calling buildSystemPrompt (ContentView.swift:1819-1822). In that common selected-profile scenario, the built-in base prompt is still prepended even though sendCustomPromptOnly is true, so the advertised feature only works for paths that resolve through SettingsStore directly, such as app-bound profiles.

Useful? React with 👍 / 👎.

}
return Self.combineBasePrompt(for: normalizedMode, with: trimmedBody)
}

// MARK: - Model Reasoning Configuration

/// Configuration for model-specific reasoning/thinking parameters
Expand Down Expand Up @@ -4444,6 +4461,7 @@ private extension SettingsStore {
static let dictationPromptProfiles = "DictationPromptProfiles"
static let appPromptBindings = "AppPromptBindings"
static let selectedDictationPromptID = "SelectedDictationPromptID"
static let sendCustomPromptOnly = "SendCustomPromptOnly"
static let editPromptOff = "EditPromptOff"
static let selectedEditPromptID = "SelectedEditPromptID"
static let selectedWritePromptID = "SelectedWritePromptID" // legacy fallback key
Expand Down
43 changes: 42 additions & 1 deletion Sources/Fluid/UI/AISettings/AIEnhancementSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ enum AIEnhancementConfigurationSection: String, CaseIterable, Identifiable {
case providers
case advancedPrompts

var id: String { self.rawValue }
var id: String {
self.rawValue
}

var title: String {
switch self {
Expand Down Expand Up @@ -142,4 +144,43 @@ struct AIEnhancementSettingsView: View {
Text(self.viewModel.appPromptBindingErrorMessage)
}
}

var customPromptOnlyToggleRow: some View {
HStack(alignment: .center, spacing: 12) {
Image(systemName: "text.quote")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(self.theme.palette.accent)
.frame(width: 24, height: 24)

VStack(alignment: .leading, spacing: 3) {
Text("Send Custom Prompt Only")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(self.theme.palette.primaryText)
Text("For custom Dictate prompts, send your prompt without prepending the built-in dictation prompt.")
.font(.caption2)
.foregroundStyle(self.theme.palette.secondaryText)
.fixedSize(horizontal: false, vertical: true)
}

Spacer(minLength: 12)

Toggle("", isOn: Binding(
get: { self.viewModel.sendCustomPromptOnly },
set: { self.viewModel.setSendCustomPromptOnly($0) }
))
.toggleStyle(.switch)
.labelsHidden()
.help("Send custom Dictate prompts without prepending the built-in dictation prompt.")
}
.padding(.horizontal, 13)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(self.theme.palette.cardBackground.opacity(0.72))
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(self.theme.palette.cardBorder.opacity(0.32), lineWidth: 1)
)
)
}
}
16 changes: 13 additions & 3 deletions Sources/Fluid/UI/AISettings/AIEnhancementSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ final class AIEnhancementSettingsViewModel: ObservableObject {
@Published var showKeychainPermissionAlert: Bool = false
@Published var keychainPermissionMessage: String = ""

// Reasoning config change tracker (triggers view updates)
/// Reasoning config change tracker (triggers view updates)
@Published var reasoningConfigVersion: Int = 0

// MARK: - Cached Provider Items (for scroll performance)

// These are cached to avoid recomputing on every view body evaluation
/// These are cached to avoid recomputing on every view body evaluation
struct ProviderItemData: Identifiable, Hashable {
let id: String
let name: String
Expand All @@ -97,7 +97,9 @@ final class AIEnhancementSettingsViewModel: ObservableObject {
let bundleID: String
let name: String

var id: String { self.bundleID }
var id: String {
self.bundleID
}
}

@Published var cachedProviderItems: [ProviderItemData] = []
Expand All @@ -110,6 +112,7 @@ final class AIEnhancementSettingsViewModel: ObservableObject {
@Published var appPromptBindingErrorMessage: String = ""
@Published var selectedDictationPromptID: String? = nil
@Published var selectedEditPromptID: String? = nil
@Published var sendCustomPromptOnly: Bool = false
@Published var promptEditorMode: PromptEditorMode? = nil
@Published var draftPromptName: String = ""
@Published var draftPromptText: String = ""
Expand Down Expand Up @@ -165,6 +168,7 @@ final class AIEnhancementSettingsViewModel: ObservableObject {
self.appPromptBindings = self.settings.appPromptBindings
self.selectedDictationPromptID = self.settings.selectedDictationPromptID
self.selectedEditPromptID = self.settings.selectedEditPromptID
self.sendCustomPromptOnly = self.settings.sendCustomPromptOnly
self.isDictationPromptOff = self.settings.isDictationPromptOff
self.isEditPromptOff = self.settings.isEditPromptOff

Expand Down Expand Up @@ -1876,6 +1880,11 @@ final class AIEnhancementSettingsViewModel: ObservableObject {
self.isEditPromptOff = self.settings.isEditPromptOff
}

func setSendCustomPromptOnly(_ sendOnly: Bool) {
self.settings.sendCustomPromptOnly = sendOnly
self.sendCustomPromptOnly = self.settings.sendCustomPromptOnly
}

func isPrimaryDictationPromptSelectionOff() -> Bool {
return self.settings.isDictationPromptOff
}
Expand Down Expand Up @@ -1984,6 +1993,7 @@ final class AIEnhancementSettingsViewModel: ObservableObject {
private func refreshPromptSelectionState() {
self.selectedDictationPromptID = self.settings.selectedDictationPromptID
self.selectedEditPromptID = self.settings.selectedEditPromptID
self.sendCustomPromptOnly = self.settings.sendCustomPromptOnly
self.isDictationPromptOff = self.settings.isDictationPromptOff
self.isEditPromptOff = self.settings.isEditPromptOff
}
Expand Down
34 changes: 34 additions & 0 deletions Sources/Fluid/UI/AISettingsView+AdvancedSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,10 @@ extension AIEnhancementSettingsView {
} else {
self.promptRoutingScopeRow(mode: mode)

if mode.normalized == .dictate {
self.customPromptOnlyToggleRow
}

Text(
isSelectedAppsOnly
? "Custom prompts only run in apps listed in App Overrides."
Expand Down Expand Up @@ -1692,6 +1696,10 @@ extension AIEnhancementSettingsView {
self.promptTest.updateDraftPromptText(combined)
}
}

if self.viewModel.draftPromptMode == .dictate {
self.baseDictationPromptReference
}
}

if self.viewModel.draftPromptMode != .dictate {
Expand Down Expand Up @@ -1910,6 +1918,32 @@ extension AIEnhancementSettingsView {
}
}

private var baseDictationPromptReference: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Built-in Base Prompt")
.font(.caption)
.foregroundStyle(.secondary)
Text("Reference only. Copy any parts you want into a custom prompt.")
.font(.caption2)
.foregroundStyle(self.theme.palette.secondaryText)

PromptTextView(
text: .constant(SettingsStore.baseDictationPromptText()),
isEditable: false,
font: NSFont.monospacedSystemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular)
)
.frame(height: 110)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(self.theme.palette.contentBackground)
.overlay(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.stroke(self.theme.palette.cardBorder, lineWidth: 1)
)
)
}
}

private func autoDisablePromptTestIfNeeded() {
guard self.promptTest.isActive else { return }
if !self.viewModel.isAIPostProcessingConfiguredForDictation() {
Expand Down
Loading