Skip to content

Commit 43fa7dd

Browse files
authored
Merge pull request #324 from altic-dev/B/321-320-prompt-routing-ui
Fix prompt routing controls
2 parents 1eb28ca + 21df6c7 commit 43fa7dd

14 files changed

Lines changed: 411 additions & 201 deletions

Sources/Fluid/ContentView.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,7 @@ struct ContentView: View {
959959
.listRowBackground(self.sidebarRowBackground(for: .voiceEngine))
960960

961961
NavigationLink(value: SidebarItem.aiEnhancements) {
962-
Label("AI Enhancements", systemImage: "brain")
962+
Label("AI Enhancement", systemImage: "brain")
963963
.font(.system(size: 15, weight: .medium))
964964
.padding(.leading, 18)
965965
}
@@ -1578,7 +1578,7 @@ struct ContentView: View {
15781578
return self.buildSystemPrompt(appInfo: appInfo, dictationSlot: dictationSlot)
15791579
}()
15801580

1581-
// Dictation cleanup folds the prompt + transcript into a single user
1581+
// Dictation enhancement folds the prompt + transcript into a single user
15821582
// turn (substituting `${transcript}` when present, otherwise appending
15831583
// the transcript after a blank line). Non-dictation callers — the AI
15841584
// chat tab specifically — keep the legacy two-message layout where
@@ -1631,7 +1631,7 @@ struct ContentView: View {
16311631
}
16321632
self.logDictationPromptTrace("Selected context text", value: "<none (dictation mode)>")
16331633
}
1634-
DebugLogger.shared.debug("Using Apple Intelligence for transcription cleanup", source: "ContentView")
1634+
DebugLogger.shared.debug("Using Apple Intelligence for transcription enhancement", source: "ContentView")
16351635
let output = try await provider.process(systemPrompt: systemPrompt, userText: userMessageContent)
16361636
if self.shouldTracePromptProcessing {
16371637
self.logDictationPromptTrace("Model answer (A)", value: output)
@@ -1710,7 +1710,7 @@ struct ContentView: View {
17101710
)
17111711
}
17121712

1713-
// Build messages array. For dictation cleanup the whole prompt +
1713+
// Build messages array. For dictation enhancement the whole prompt +
17141714
// transcript is folded into a single user message, so we omit the
17151715
// (empty) system role. Non-dictation callers keep the legacy
17161716
// system + user shape.
@@ -1722,7 +1722,7 @@ struct ContentView: View {
17221722

17231723
// NOTE: Transcription doesn't need streaming - the full result appears at once
17241724
// Streaming is only useful for Command/Rewrite modes where real-time display helps
1725-
// Using non-streaming is simpler and more reliable for transcription cleanup
1725+
// Using non-streaming is simpler and more reliable for transcription enhancement
17261726
let enableStreaming = false // Hardcoded off for transcription
17271727

17281728
// Build LLMClient configuration
@@ -1881,9 +1881,11 @@ struct ContentView: View {
18811881

18821882
var finalText: String
18831883
var aiFallbackReason: String?
1884+
let appInfo = self.recordingAppInfo ?? self.getCurrentAppInfo()
18841885

1885-
let shouldUseAI = activeDictationSlot.map { DictationAIPostProcessingGate.isConfigured(for: $0) } ??
1886-
DictationAIPostProcessingGate.isConfigured()
1886+
let shouldUseAI = activeDictationSlot.map {
1887+
DictationAIPostProcessingGate.isConfigured(for: $0, appBundleID: appInfo.bundleId)
1888+
} ?? DictationAIPostProcessingGate.isConfigured(for: .primary, appBundleID: appInfo.bundleId)
18871889
let transcriptionModelInfo = self.currentTranscriptionModelInfo()
18881890

18891891
if shouldUseAI {
@@ -1973,7 +1975,6 @@ struct ContentView: View {
19731975

19741976
// Save to transcription history (transcription mode only, if enabled)
19751977
if shouldPersistOutputs, SettingsStore.shared.saveTranscriptionHistory {
1976-
let appInfo = self.recordingAppInfo ?? self.getCurrentAppInfo()
19771978
TranscriptionHistoryStore.shared.addEntry(
19781979
rawText: transcribedText,
19791980
processedText: finalText,
@@ -2194,7 +2195,8 @@ struct ContentView: View {
21942195

21952196
var finalText = transcribedText
21962197
var aiFallbackReason: String?
2197-
let shouldUseAI = DictationAIPostProcessingGate.isConfigured()
2198+
let appInfo = self.getCurrentAppInfo()
2199+
let shouldUseAI = DictationAIPostProcessingGate.isConfigured(for: .primary, appBundleID: appInfo.bundleId)
21982200
if shouldUseAI {
21992201
do {
22002202
finalText = try await self.processTextWithAI(transcribedText)
@@ -2213,7 +2215,6 @@ struct ContentView: View {
22132215
self.menuBarManager.setProcessing(false)
22142216

22152217
finalText = ASRService.applyGAAVFormatting(finalText)
2216-
let appInfo = self.getCurrentAppInfo()
22172218

22182219
if SettingsStore.shared.saveTranscriptionHistory {
22192220
TranscriptionHistoryStore.shared.addEntry(

Sources/Fluid/Networking/AppleIntelligenceProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ enum AppleIntelligenceService {
4343
#if canImport(FoundationModels)
4444
@available(macOS 26.0, *)
4545
final class AppleIntelligenceProvider {
46-
/// Process text with a system prompt (for transcription cleanup)
46+
/// Process text with a system prompt (for transcription enhancement)
4747
func process(systemPrompt: String, userText: String) async throws -> String {
4848
let session = LanguageModelSession()
4949

Sources/Fluid/Persistence/BackupService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ struct SettingsBackupPayload: Codable, Equatable {
6565
let customDictionaryEntries: [SettingsStore.CustomDictionaryEntry]
6666
let selectedDictationPromptID: String?
6767
let dictationPromptOff: Bool?
68+
let dictationPromptRoutingScope: SettingsStore.PromptRoutingScope?
6869
let selectedEditPromptID: String?
70+
let editPromptRoutingScope: SettingsStore.PromptRoutingScope?
6971
let defaultDictationPromptOverride: String?
7072
let defaultEditPromptOverride: String?
7173
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Combine
2+
import Foundation
3+
4+
extension SettingsStore {
5+
enum PromptRoutingScope: String, Codable, CaseIterable, Identifiable {
6+
case allApps
7+
case selectedAppsOnly
8+
9+
var id: String { self.rawValue }
10+
}
11+
12+
var dictationPromptRoutingScope: PromptRoutingScope {
13+
get {
14+
guard let rawValue = UserDefaults.standard.string(forKey: PromptRoutingKeys.dictation),
15+
let scope = PromptRoutingScope(rawValue: rawValue)
16+
else {
17+
return .allApps
18+
}
19+
return scope
20+
}
21+
set {
22+
objectWillChange.send()
23+
UserDefaults.standard.set(newValue.rawValue, forKey: PromptRoutingKeys.dictation)
24+
}
25+
}
26+
27+
var editPromptRoutingScope: PromptRoutingScope {
28+
get {
29+
guard let rawValue = UserDefaults.standard.string(forKey: PromptRoutingKeys.edit),
30+
let scope = PromptRoutingScope(rawValue: rawValue)
31+
else {
32+
return .allApps
33+
}
34+
return scope
35+
}
36+
set {
37+
objectWillChange.send()
38+
UserDefaults.standard.set(newValue.rawValue, forKey: PromptRoutingKeys.edit)
39+
}
40+
}
41+
42+
func promptRoutingScope(for mode: PromptMode) -> PromptRoutingScope {
43+
switch mode.normalized {
44+
case .dictate:
45+
return self.dictationPromptRoutingScope
46+
case .edit, .write, .rewrite:
47+
return self.editPromptRoutingScope
48+
}
49+
}
50+
51+
func setPromptRoutingScope(_ scope: PromptRoutingScope, for mode: PromptMode) {
52+
switch mode.normalized {
53+
case .dictate:
54+
self.dictationPromptRoutingScope = scope
55+
case .edit, .write, .rewrite:
56+
self.editPromptRoutingScope = scope
57+
}
58+
}
59+
}
60+
61+
private enum PromptRoutingKeys {
62+
static let dictation = "DictationPromptRoutingScope"
63+
static let edit = "EditPromptRoutingScope"
64+
}

Sources/Fluid/Persistence/SettingsStore.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ final class SettingsStore: ObservableObject {
235235
let systemPrompt: String
236236
}
237237

238-
/// User-defined dictation prompt profiles (named system prompts for dictation cleanup).
238+
/// User-defined dictation prompt profiles (named system prompts for dictation enhancement).
239239
/// The built-in default prompt is not stored here.
240240
var dictationPromptProfiles: [DictationPromptProfile] {
241241
get {
@@ -886,6 +886,15 @@ final class SettingsStore: ObservableObject {
886886
)
887887
}
888888

889+
if self.promptRoutingScope(for: normalizedMode) == .selectedAppsOnly {
890+
return self.defaultPromptResolution(
891+
for: normalizedMode,
892+
source: .builtInDefault,
893+
appBinding: nil,
894+
allowDefaultOverride: false
895+
)
896+
}
897+
889898
if let profile = self.selectedPromptProfile(for: normalizedMode) {
890899
let body = Self.stripBasePrompt(for: normalizedMode, from: profile.prompt)
891900
if !body.isEmpty {
@@ -907,6 +916,11 @@ final class SettingsStore: ObservableObject {
907916
}
908917

909918
func effectiveDictationPromptBody(for slot: DictationShortcutSlot, appBundleID: String? = nil) -> String {
919+
if self.promptRoutingScope(for: .dictate) == .selectedAppsOnly {
920+
guard self.dictationPromptSelection(for: slot) != .off else { return "" }
921+
return self.effectivePromptBody(for: .dictate, appBundleID: appBundleID)
922+
}
923+
910924
switch self.dictationPromptSelection(for: slot) {
911925
case .off:
912926
return ""
@@ -925,6 +939,11 @@ final class SettingsStore: ObservableObject {
925939
}
926940

927941
func effectiveDictationSystemPrompt(for slot: DictationShortcutSlot, appBundleID: String? = nil) -> String {
942+
if self.promptRoutingScope(for: .dictate) == .selectedAppsOnly {
943+
guard self.dictationPromptSelection(for: slot) != .off else { return "" }
944+
return self.effectiveSystemPrompt(for: .dictate, appBundleID: appBundleID)
945+
}
946+
928947
switch self.dictationPromptSelection(for: slot) {
929948
case .off, .default:
930949
return self.effectiveSystemPrompt(for: .dictate, appBundleID: appBundleID)
@@ -953,10 +972,10 @@ final class SettingsStore: ObservableObject {
953972
}
954973

955974
/// Literal placeholder that gets substituted with the raw transcription
956-
/// when composing the user message for a dictation cleanup call.
975+
/// when composing the user message for a dictation enhancement call.
957976
static let transcriptPlaceholder = "${transcript}"
958977

959-
/// Compose the user-turn string for a dictation cleanup call by folding
978+
/// Compose the user-turn string for a dictation enhancement call by folding
960979
/// the transcript into the prompt template. If the template contains the
961980
/// `${transcript}` placeholder, the placeholder is replaced; otherwise
962981
/// the transcript is appended after a blank line, matching the pre-PR
@@ -973,9 +992,10 @@ final class SettingsStore: ObservableObject {
973992
private func defaultPromptResolution(
974993
for mode: PromptMode,
975994
source: PromptResolutionSource,
976-
appBinding: AppPromptBinding?
995+
appBinding: AppPromptBinding?,
996+
allowDefaultOverride: Bool = true
977997
) -> PromptResolution {
978-
if let override = self.defaultPromptOverride(for: mode) {
998+
if allowDefaultOverride, let override = self.defaultPromptOverride(for: mode) {
979999
let trimmedOverride = override.trimmingCharacters(in: .whitespacesAndNewlines)
9801000
if trimmedOverride.isEmpty {
9811001
return PromptResolution(
@@ -2265,7 +2285,9 @@ final class SettingsStore: ObservableObject {
22652285
customDictionaryEntries: self.customDictionaryEntries,
22662286
selectedDictationPromptID: self.selectedDictationPromptID,
22672287
dictationPromptOff: self.isDictationPromptOff,
2288+
dictationPromptRoutingScope: self.dictationPromptRoutingScope,
22682289
selectedEditPromptID: self.selectedEditPromptID,
2290+
editPromptRoutingScope: self.editPromptRoutingScope,
22692291
defaultDictationPromptOverride: self.defaultDictationPromptOverride,
22702292
defaultEditPromptOverride: self.defaultEditPromptOverride
22712293
)
@@ -2340,6 +2362,8 @@ final class SettingsStore: ObservableObject {
23402362
self.appPromptBindings = appPromptBindings
23412363
self.selectedDictationPromptID = payload.selectedDictationPromptID
23422364
self.isDictationPromptOff = payload.dictationPromptOff ?? self.isDictationPromptOff
2365+
self.dictationPromptRoutingScope = payload.dictationPromptRoutingScope ?? .allApps
2366+
self.editPromptRoutingScope = payload.editPromptRoutingScope ?? .allApps
23432367
self.selectedEditPromptID = payload.selectedEditPromptID
23442368
self.defaultDictationPromptOverride = payload.defaultDictationPromptOverride
23452369
self.defaultEditPromptOverride = payload.defaultEditPromptOverride

Sources/Fluid/Services/DictationAIPostProcessingGate.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ enum DictationAIPostProcessingGate {
77
/// - Requires dictation prompt selection to not be `Off`
88
/// - Requires the selected provider connection to still be verified
99
static func isConfigured() -> Bool {
10-
self.isConfigured(for: .primary)
10+
self.isConfigured(for: .primary, appBundleID: nil)
1111
}
1212

13-
static func isConfigured(for slot: SettingsStore.DictationShortcutSlot) -> Bool {
13+
static func isConfigured(for slot: SettingsStore.DictationShortcutSlot, appBundleID: String? = nil) -> Bool {
1414
let settings = SettingsStore.shared
1515
guard settings.dictationPromptSelection(for: slot) != .off else { return false }
16+
if let appBundleID,
17+
settings.promptRoutingScope(for: .dictate) == .selectedAppsOnly,
18+
!settings.hasAppPromptBinding(for: .dictate, appBundleID: appBundleID)
19+
{
20+
return false
21+
}
1622

1723
return self.isProviderConfigured()
1824
}

Sources/Fluid/Services/NotificationService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ enum NotificationService {
4242

4343
private static func deliverAIProcessingFallback(error: String, using center: UNUserNotificationCenter) {
4444
let content = UNMutableNotificationContent()
45-
content.title = "AI cleanup failed"
45+
content.title = "AI Enhancement failed"
4646
content.body = "Typed raw transcription instead."
4747
content.subtitle = error
4848
content.sound = nil

Sources/Fluid/UI/AISettings/AIEnhancementSettingsView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ struct AIEnhancementSettingsView: View {
1414
@State var selectedPromptMode: SettingsStore.PromptMode = .dictate
1515
@State var hoveredPromptModeKey: String? = nil
1616
@State var hoveredCleanupControlKey: String? = nil
17+
@State var hoveredPromptScopeKey: String? = nil
1718

1819
var body: some View {
1920
self.aiConfigurationCard

Sources/Fluid/UI/AISettings/AIEnhancementSettingsViewModel.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,7 +1429,12 @@ final class AIEnhancementSettingsViewModel: ObservableObject {
14291429
let trimmedName = appName.trimmingCharacters(in: .whitespacesAndNewlines)
14301430
let resolvedName = trimmedName.isEmpty ? normalizedBundleID : trimmedName
14311431
let existingPromptID = self.settings.appPromptBinding(for: mode, appBundleID: normalizedBundleID)?.promptID
1432-
let resolvedPromptID = existingPromptID ?? self.selectedPromptID(for: mode)
1432+
let resolvedPromptID: String?
1433+
if self.settings.promptRoutingScope(for: mode) == .selectedAppsOnly {
1434+
resolvedPromptID = existingPromptID
1435+
} else {
1436+
resolvedPromptID = existingPromptID ?? self.selectedPromptID(for: mode)
1437+
}
14331438

14341439
self.appPromptBindingErrorMessage = ""
14351440
self.settings.upsertAppPromptBinding(
@@ -1497,13 +1502,24 @@ final class AIEnhancementSettingsViewModel: ObservableObject {
14971502
$0.mode.normalized == mode.normalized
14981503
})
14991504
else {
1500-
return "Default"
1505+
return "Built-in Default"
15011506
}
15021507

15031508
let trimmed = profile.name.trimmingCharacters(in: .whitespacesAndNewlines)
15041509
return trimmed.isEmpty ? "Untitled Prompt" : trimmed
15051510
}
15061511

1512+
func promptRoutingScope(for mode: SettingsStore.PromptMode) -> SettingsStore.PromptRoutingScope {
1513+
self.settings.promptRoutingScope(for: mode)
1514+
}
1515+
1516+
func setPromptRoutingScope(_ scope: SettingsStore.PromptRoutingScope, for mode: SettingsStore.PromptMode) {
1517+
self.settings.setPromptRoutingScope(scope, for: mode)
1518+
self.selectedDictationPromptID = self.settings.selectedDictationPromptID
1519+
self.selectedEditPromptID = self.settings.selectedEditPromptID
1520+
self.isDictationPromptOff = self.settings.isDictationPromptOff
1521+
}
1522+
15071523
func isPrimaryDictationPromptSelectionOff() -> Bool {
15081524
self.settings.isDictationPromptOff
15091525
}

Sources/Fluid/UI/AISettingsView+AIConfiguration.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ extension AIEnhancementSettingsView {
126126
.frame(width: 34, height: 34)
127127

128128
VStack(alignment: .leading, spacing: 2) {
129-
Text("AI Enhancements")
129+
Text("AI Enhancement")
130130
.font(.title3)
131131
.fontWeight(.semibold)
132132
.foregroundStyle(self.theme.palette.primaryText)
133-
Text("Choose the model used for AI Cleanup.")
133+
Text("Choose the model used for AI Enhancement.")
134134
.font(.caption)
135135
.foregroundStyle(self.theme.palette.secondaryText)
136136
}
@@ -146,13 +146,13 @@ extension AIEnhancementSettingsView {
146146
self.aiSetupSummaryDivider
147147
self.aiSetupSummaryItem(icon: "cloud", text: "Cloud models use provider APIs")
148148
self.aiSetupSummaryDivider
149-
self.aiSetupSummaryItem(icon: "slider.horizontal.3", text: "AI Cleanup enables dictation prompts")
149+
self.aiSetupSummaryItem(icon: "slider.horizontal.3", text: "AI Enhancement enables dictation prompts")
150150
}
151151

152152
VStack(alignment: .leading, spacing: 7) {
153153
self.aiSetupSummaryItem(icon: "cpu", text: "Local models run on Mac")
154154
self.aiSetupSummaryItem(icon: "cloud", text: "Cloud models use provider APIs")
155-
self.aiSetupSummaryItem(icon: "slider.horizontal.3", text: "AI Cleanup enables dictation prompts")
155+
self.aiSetupSummaryItem(icon: "slider.horizontal.3", text: "AI Enhancement enables dictation prompts")
156156
}
157157
}
158158
.padding(.horizontal, 2)

0 commit comments

Comments
 (0)