Skip to content
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 107 additions & 49 deletions Sources/Fluid/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ struct ContentView: View {
.onReceive(NotificationCenter.default.publisher(for: .settingsBackupDidRestore)) { _ in
self.reloadSettingsStateAfterBackupRestore()
}
.onChange(of: self.settings.copyTranscriptionToClipboard) { _, newValue in
// Keep Settings toggle in sync when onboarding skip (or other writers)
// update SettingsStore directly.
self.copyToClipboard = newValue
}
.toolbar {
if !self.settings.shouldShowOnboarding {
ToolbarItemGroup(placement: .primaryAction) {
Expand Down Expand Up @@ -619,6 +624,15 @@ struct ContentView: View {
self.audioObserver.startObserving()
self.asr.initialize()
self.menuBarManager.configure(asrService: self.appServices.asr)
self.menuBarManager.configureDictationControls(
start: { [self] in
self.startRecording()
},
stop: { [self] in
let route = self.currentDictationOutputRouteForHotkeyStop()
await self.stopAndProcessTranscription(route: route)
}
)
self.refreshDevices()

if self.selectedInputUID.isEmpty, let defIn = AudioDevice.getDefaultInputDevice()?.uid {
Expand Down Expand Up @@ -1330,6 +1344,10 @@ struct ContentView: View {
openAccessibilitySettings: self.openAccessibilitySettings,
restartApp: self.restartApp,
menuBarManager: self.menuBarManager,
stopAndProcessTranscription: {
await self.stopAndProcessTranscription(route: .onboardingSandbox)
},
startRecording: self.startRecording,
activeShortcutRecordingTarget: self.$activeShortcutRecordingTarget,
shortcutRecordingMessage: self.$shortcutRecordingMessage,
theme: self.theme
Expand Down Expand Up @@ -2028,6 +2046,11 @@ struct ContentView: View {
DebugLogger.shared.info("Output route selected: \(route.rawValue)", source: "ContentView")
self.appBench("stop_path_enter route=\(route.rawValue)")

// Disable menu Start/Stop for the full stop window — including hotkey and
// in-app stops, where isRunning flips false before transcription finishes.
self.menuBarManager.beginDictationStopProcessing()
defer { self.menuBarManager.endDictationStopProcessing() }

// Check if we're in rewrite or command mode
let modeAtStop = self.activeRecordingMode
let wasRewriteMode = modeAtStop == .edit || self.isRecordingForRewrite
Expand Down Expand Up @@ -2363,28 +2386,41 @@ struct ContentView: View {
model: transcriptionModelInfo.model
)
}
// When FluidVoice itself is frontmost, the bound editor already receives `finalText`.
// Avoid re-inserting or overwriting the clipboard in that self-target case.
let shouldCopyToClipboard = shouldPersistOutputs &&
SettingsStore.shared.copyTranscriptionToClipboard &&
!isFluidFrontmost
// When FluidVoice itself is frontmost and Accessibility works, the bound
// editor already receives `finalText` — skip clipboard overwrite there.
// In clipboard-only mode (!Accessibility), still copy even when Fluid is
// frontmost so in-app / menu-bar recordings are not dropped.
let accessibilityTrusted = AXIsProcessTrusted()
let shouldForceClipboardFallback = shouldPersistOutputs && !accessibilityTrusted
let shouldCopyToClipboard = shouldPersistOutputs && (
shouldForceClipboardFallback ||
(!isFluidFrontmost && SettingsStore.shared.copyTranscriptionToClipboard)
)

var didCopyToClipboard = false
if shouldCopyToClipboard {
ClipboardService.copyToClipboard(finalText)
AnalyticsService.shared.capture(
.outputDelivered,
properties: [
"mode": AnalyticsMode.dictation.rawValue,
"method": AnalyticsOutputMethod.clipboard.rawValue,
]
)
didCopyToClipboard = ClipboardService.copyToClipboard(finalText)
if didCopyToClipboard {
AnalyticsService.shared.capture(
.outputDelivered,
properties: [
"mode": AnalyticsMode.dictation.rawValue,
"method": AnalyticsOutputMethod.clipboard.rawValue,
"clipboard_only_output": shouldForceClipboardFallback,
]
)
}
}

if shouldForceClipboardFallback, didCopyToClipboard {
NotificationService.showClipboardOnlyOutput()
}
Comment thread
uzairansaruzi marked this conversation as resolved.

var didTypeExternally = false
let shouldTypeExternally = shouldPersistOutputs && !isFluidFrontmost
let shouldTypeExternally = shouldPersistOutputs && !isFluidFrontmost && accessibilityTrusted

DebugLogger.shared.debug(
"Typing decision → frontmost: \(frontmostName), fluidFrontmost: \(isFluidFrontmost), editorFocused: \(self.isTranscriptionFocused), willTypeExternally: \(shouldTypeExternally)",
"Typing decision → frontmost: \(frontmostName), fluidFrontmost: \(isFluidFrontmost), editorFocused: \(self.isTranscriptionFocused), accessibilityTrusted: \(accessibilityTrusted), willTypeExternally: \(shouldTypeExternally)",
source: "ContentView"
)

Expand Down Expand Up @@ -2434,7 +2470,7 @@ struct ContentView: View {
aiProvider: modelInfo.provider
)
} else if shouldPersistOutputs,
SettingsStore.shared.copyTranscriptionToClipboard == false,
!didCopyToClipboard,
SettingsStore.shared.saveTranscriptionHistory
{
AnalyticsService.shared.capture(
Expand Down Expand Up @@ -2735,16 +2771,25 @@ struct ContentView: View {

let frontmostApp = NSWorkspace.shared.frontmostApplication
let isFluidFrontmost = frontmostApp?.bundleIdentifier == Bundle.main.bundleIdentifier
let accessibilityTrusted = AXIsProcessTrusted()
let shouldForceClipboardFallback = !accessibilityTrusted
let shouldCopyToClipboard = shouldForceClipboardFallback ||
(!isFluidFrontmost && SettingsStore.shared.copyTranscriptionToClipboard)

var didCopyToClipboard = false
if shouldCopyToClipboard {
didCopyToClipboard = ClipboardService.copyToClipboard(finalText)
}

if SettingsStore.shared.copyTranscriptionToClipboard, !isFluidFrontmost {
ClipboardService.copyToClipboard(finalText)
if shouldForceClipboardFallback, didCopyToClipboard {
NotificationService.showClipboardOnlyOutput()
}

let focusedPID = TypingService.captureSystemFocusedPID()
?? NSWorkspace.shared.frontmostApplication?.processIdentifier
NotchContentState.shared.recordingTargetPID = focusedPID

let shouldTypeExternally = !isFluidFrontmost
let shouldTypeExternally = !isFluidFrontmost && accessibilityTrusted
if shouldTypeExternally {
let typingTarget = self.resolveTypingTargetPID()
if typingTarget.shouldRestoreOriginalFocus {
Expand Down Expand Up @@ -2850,8 +2895,9 @@ struct ContentView: View {
self.pendingAIReprocessText = nil
}

var didCopyToClipboard = false
if SettingsStore.shared.copyTranscriptionToClipboard {
ClipboardService.copyToClipboard(finalText)
didCopyToClipboard = ClipboardService.copyToClipboard(finalText)
}

let focusedPID = TypingService.captureSystemFocusedPID()
Expand All @@ -2860,7 +2906,17 @@ struct ContentView: View {

let frontmostApp = NSWorkspace.shared.frontmostApplication
let isFluidFrontmost = frontmostApp?.bundleIdentifier?.contains("fluid") == true
let shouldTypeExternally = !isFluidFrontmost || self.isTranscriptionFocused == false
let accessibilityTrusted = AXIsProcessTrusted()
let shouldForceClipboardFallback = !accessibilityTrusted
if shouldForceClipboardFallback {
if !didCopyToClipboard {
didCopyToClipboard = ClipboardService.copyToClipboard(finalText)
}
if didCopyToClipboard {
NotificationService.showClipboardOnlyOutput()
}
}
let shouldTypeExternally = (!isFluidFrontmost || self.isTranscriptionFocused == false) && accessibilityTrusted
if shouldTypeExternally {
let typingTarget = self.resolveTypingTargetPID()
if typingTarget.shouldRestoreOriginalFocus {
Expand Down Expand Up @@ -2901,35 +2957,44 @@ struct ContentView: View {
if !self.rewriteModeService.rewrittenText.isEmpty {
DebugLogger.shared.info("Rewrite successful, typing result (chars: \(self.rewriteModeService.rewrittenText.count))", source: "ContentView")

// Copy to clipboard as backup
if SettingsStore.shared.copyTranscriptionToClipboard {
ClipboardService.copyToClipboard(self.rewriteModeService.rewrittenText)
// Copy to clipboard as backup (or as primary delivery without Accessibility)
let accessibilityTrusted = AXIsProcessTrusted()
var didCopyToClipboard = false
if SettingsStore.shared.copyTranscriptionToClipboard || !accessibilityTrusted {
didCopyToClipboard = ClipboardService.copyToClipboard(self.rewriteModeService.rewrittenText)
if didCopyToClipboard {
AnalyticsService.shared.capture(
.outputDelivered,
properties: [
"mode": AnalyticsMode.rewrite.rawValue,
"method": AnalyticsOutputMethod.clipboard.rawValue,
"clipboard_only_output": !accessibilityTrusted,
]
)
}
}

// Type the rewritten text when Accessibility is available
if accessibilityTrusted {
let typingTarget = self.resolveTypingTargetPID()
if typingTarget.shouldRestoreOriginalFocus {
await self.restoreFocusToRecordingTarget()
}
self.asr.typeTextToActiveField(
self.rewriteModeService.rewrittenText,
preferredTargetPID: typingTarget.pid
)
AnalyticsService.shared.capture(
.outputDelivered,
properties: [
"mode": AnalyticsMode.rewrite.rawValue,
"method": AnalyticsOutputMethod.clipboard.rawValue,
"method": AnalyticsOutputMethod.typed.rawValue,
]
)
} else if didCopyToClipboard {
NotificationService.showClipboardOnlyOutput()
}

// Type the rewritten text
let typingTarget = self.resolveTypingTargetPID()
if typingTarget.shouldRestoreOriginalFocus {
await self.restoreFocusToRecordingTarget()
}
self.asr.typeTextToActiveField(
self.rewriteModeService.rewrittenText,
preferredTargetPID: typingTarget.pid
)
AnalyticsService.shared.capture(
.outputDelivered,
properties: [
"mode": AnalyticsMode.rewrite.rawValue,
"method": AnalyticsOutputMethod.typed.rawValue,
]
)

// Clear the rewrite service state for next use
self.rewriteModeService.clearState()
self.hideOverlayAfterOutput()
Expand Down Expand Up @@ -3760,10 +3825,6 @@ extension ContentView {
self.asr.micStatus == .authorized
}

private var onboardingAccessibilityReady: Bool {
self.accessibilityEnabled
}

private var onboardingAIReady: Bool {
self.settings.onboardingAISkipped || DictationAIPostProcessingGate.isProviderConfigured()
}
Expand Down Expand Up @@ -3822,9 +3883,6 @@ extension ContentView {
if !self.onboardingMicrophoneReady {
missing.append("microphone access")
}
if !self.onboardingAccessibilityReady {
missing.append("Accessibility access")
}
if !allowsAIConfiguration, !self.onboardingAIReady {
missing.append("AI choice")
}
Expand Down
18 changes: 17 additions & 1 deletion Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1772,7 +1772,10 @@ final class SettingsStore: ObservableObject {

var copyTranscriptionToClipboard: Bool {
get { self.defaults.bool(forKey: Keys.copyTranscriptionToClipboard) }
set { self.defaults.set(newValue, forKey: Keys.copyTranscriptionToClipboard) }
set {
objectWillChange.send()
self.defaults.set(newValue, forKey: Keys.copyTranscriptionToClipboard)
}
}

var preferredInputDeviceUID: String? {
Expand Down Expand Up @@ -2336,6 +2339,14 @@ final class SettingsStore: ObservableObject {
}
}

var onboardingAccessibilitySkipped: Bool {
get { self.defaults.bool(forKey: Keys.onboardingAccessibilitySkipped) }
set {
objectWillChange.send()
self.defaults.set(newValue, forKey: Keys.onboardingAccessibilitySkipped)
}
}

var onboardingSelectedLanguageID: String {
get {
let stored = self.defaults.string(forKey: Keys.onboardingSelectedLanguageID)?
Expand Down Expand Up @@ -2396,13 +2407,15 @@ final class SettingsStore: ObservableObject {
self.defaults.set(false, forKey: Keys.onboardingAISkipped)
self.defaults.set(false, forKey: Keys.onboardingPlaygroundValidated)
self.defaults.set(false, forKey: Keys.onboardingPlaygroundSkipped)
self.defaults.set(false, forKey: Keys.onboardingAccessibilitySkipped)
self.defaults.set("en", forKey: Keys.onboardingSelectedLanguageID)
} else {
self.defaults.set(true, forKey: Keys.onboardingCompleted)
self.defaults.set(0, forKey: Keys.onboardingCurrentStep)
self.defaults.set(false, forKey: Keys.onboardingAISkipped)
self.defaults.set(false, forKey: Keys.onboardingPlaygroundValidated)
self.defaults.set(false, forKey: Keys.onboardingPlaygroundSkipped)
self.defaults.set(false, forKey: Keys.onboardingAccessibilitySkipped)
self.defaults.set("en", forKey: Keys.onboardingSelectedLanguageID)
}
}
Expand All @@ -2416,6 +2429,7 @@ final class SettingsStore: ObservableObject {
self.defaults.set(false, forKey: Keys.onboardingAISkipped)
self.defaults.set(false, forKey: Keys.onboardingPlaygroundValidated)
self.defaults.set(false, forKey: Keys.onboardingPlaygroundSkipped)
self.defaults.set(false, forKey: Keys.onboardingAccessibilitySkipped)
self.defaults.set("en", forKey: Keys.onboardingSelectedLanguageID)
self.defaults.set(false, forKey: Keys.playgroundUsed)
}
Expand Down Expand Up @@ -2443,6 +2457,7 @@ final class SettingsStore: ObservableObject {
self.defaults.set(false, forKey: Keys.onboardingAISkipped)
self.defaults.set(false, forKey: Keys.onboardingPlaygroundValidated)
self.defaults.set(false, forKey: Keys.onboardingPlaygroundSkipped)
self.defaults.set(false, forKey: Keys.onboardingAccessibilitySkipped)
}

private func hasLegacyUsageSignals() -> Bool {
Expand Down Expand Up @@ -4857,6 +4872,7 @@ private extension SettingsStore {
static let onboardingAISkipped = "OnboardingAISkipped"
static let onboardingPlaygroundValidated = "OnboardingPlaygroundValidated"
static let onboardingPlaygroundSkipped = "OnboardingPlaygroundSkipped"
static let onboardingAccessibilitySkipped = "OnboardingAccessibilitySkipped"
static let onboardingSelectedLanguageID = "OnboardingSelectedLanguageID"

// Command Mode Keys
Expand Down
Loading
Loading