Skip to content
Merged

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions Sources/Fluid/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,27 +314,34 @@ struct ContentView: View {

// Set up notch click callback for expanding command conversation
NotchOverlayManager.shared.onNotchClicked = {
guard NotchOverlayManager.shared.canHandleNotchCommandTap else { return }
// When notch is clicked in command mode, show expanded conversation
if !NotchContentState.shared.commandConversationHistory.isEmpty {
if NotchOverlayManager.shared.canShowExpandedCommandOutput,
!NotchContentState.shared.commandConversationHistory.isEmpty
{
NotchOverlayManager.shared.showExpandedCommandOutput()
}
}

// Set up command mode callbacks for notch
NotchOverlayManager.shared.onCommandFollowUp = { [weak commandModeService] text in
guard NotchOverlayManager.shared.allowsCommandNotchActions else { return }
await commandModeService?.processFollowUpCommand(text)
}

// Chat management callbacks
NotchOverlayManager.shared.onNewChat = { [weak commandModeService] in
guard NotchOverlayManager.shared.allowsCommandNotchActions else { return }
commandModeService?.createNewChat()
}

NotchOverlayManager.shared.onSwitchChat = { [weak commandModeService] chatID in
guard NotchOverlayManager.shared.allowsCommandNotchActions else { return }
commandModeService?.switchToChat(id: chatID)
}

NotchOverlayManager.shared.onClearChat = { [weak commandModeService] in
guard NotchOverlayManager.shared.allowsCommandNotchActions else { return }
commandModeService?.deleteCurrentChat()
}

Expand Down Expand Up @@ -1777,12 +1784,12 @@ struct ContentView: View {

self.clearActiveRecordingMode()

// Show "Transcribing..." state before calling stop() to keep overlay visible.
// Show "Transcribing" state before calling stop() to keep overlay visible.
// The asr.stop() call performs the final transcription which can take a moment
// (especially for slower models like Whisper Medium/Large).
DebugLogger.shared.debug("Showing transcription processing state", source: "ContentView")
self.menuBarManager.setProcessing(true)
NotchOverlayManager.shared.updateTranscriptionText("Transcribing...")
NotchOverlayManager.shared.updateTranscriptionText("Transcribing")

// Give SwiftUI a chance to render the processing state before we do heavier work
// (ASR finalization + optional AI post-processing).
Expand All @@ -1799,6 +1806,7 @@ struct ContentView: View {
DebugLogger.shared.debug("Transcription returned empty text", source: "ContentView")
// Hide processing state when returning early
self.menuBarManager.setProcessing(false)
NotchOverlayManager.shared.hide()
return
}

Expand Down Expand Up @@ -1877,7 +1885,7 @@ struct ContentView: View {
let postProcessingStart = Date()

// Update overlay text to show we're now refining (processing already true)
NotchOverlayManager.shared.updateTranscriptionText("Refining...")
NotchOverlayManager.shared.updateTranscriptionText("Refining")

// Ensure the status label becomes visible immediately.
await Task.yield()
Expand Down Expand Up @@ -2040,6 +2048,10 @@ struct ContentView: View {
]
)
}

if !didTypeExternally {
NotchOverlayManager.shared.hide()
}
}

private func currentDictationOutputRouteForHotkeyStop() -> DictationOutputRoute {
Expand Down
33 changes: 33 additions & 0 deletions Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,22 @@ final class SettingsStore: ObservableObject {
}
}

/// Internal presentation modes for the top notch overlay.
/// This is intentionally separate from bottom overlay sizing.
enum NotchPresentationMode: String, CaseIterable, Codable {
case standard
case minimal

var displayName: String {
switch self {
case .standard:
return "Standard Notch"
case .minimal:
return "Compact"
}
}
}

/// Where the recording overlay appears (default: bottom)
var overlayPosition: OverlayPosition {
get {
Expand All @@ -1366,6 +1382,22 @@ final class SettingsStore: ObservableObject {
}
}

/// Internal-only top notch presentation mode. No public settings UI yet.
var notchPresentationMode: NotchPresentationMode {
get {
guard let raw = self.defaults.string(forKey: Keys.notchPresentationMode),
let mode = NotchPresentationMode(rawValue: raw)
else {
return .standard
}
return mode
}
set {
objectWillChange.send()
self.defaults.set(newValue.rawValue, forKey: Keys.notchPresentationMode)
}
}

/// Vertical offset for the bottom overlay (distance from bottom of screen/dock)
var overlayBottomOffset: Double {
get {
Expand Down Expand Up @@ -3625,6 +3657,7 @@ private extension SettingsStore {

// Overlay Position
static let overlayPosition = "OverlayPosition"
static let notchPresentationMode = "NotchPresentationMode"
static let overlayBottomOffset = "OverlayBottomOffset"
static let overlayBottomOffsetMigratedTo50 = "OverlayBottomOffsetMigratedTo50"
static let overlaySize = "OverlaySize"
Expand Down
39 changes: 26 additions & 13 deletions Sources/Fluid/Services/CommandModeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ final class CommandModeService: ObservableObject {
self.loadCurrentChatFromStore()
}

private var shouldSyncCommandNotchState: Bool {
self.enableNotchOutput && NotchOverlayManager.shared.shouldSyncCommandConversationToNotch
}

private func loadCurrentChatFromStore() {
if let session = chatStore.currentSession {
self.currentChatID = session.id
Expand Down Expand Up @@ -278,6 +282,10 @@ final class CommandModeService: ObservableObject {

/// Sync conversation history to NotchContentState
private func syncToNotchState() {
guard self.shouldSyncCommandNotchState else {
return
Comment on lines +285 to +286
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 command history when notch sync is temporarily disabled

When sync is disabled by presentation policy, this branch clears all notch command output state. If the user later re-enables standard notch mode, there is no automatic settings-change resync path, so expanded command output remains empty (and cannot open from existing history) until a new message or chat reload occurs. This makes mode toggling appear to lose the current conversation in notch UI.

Useful? React with 👍 / 👎.

}

NotchContentState.shared.clearCommandOutput()

for msg in self.conversationHistory {
Expand Down Expand Up @@ -308,7 +316,7 @@ final class CommandModeService: ObservableObject {
self.saveCurrentChat()

// Push to notch
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.addCommandMessage(role: .user, content: text)
NotchContentState.shared.setCommandProcessing(true)
}
Expand All @@ -322,14 +330,18 @@ final class CommandModeService: ObservableObject {

// Add to both histories
self.conversationHistory.append(Message(role: .user, content: text))
NotchContentState.shared.addCommandMessage(role: .user, content: text)
if self.shouldSyncCommandNotchState {
NotchContentState.shared.addCommandMessage(role: .user, content: text)
}

// Auto-save after adding user message
self.saveCurrentChat()

self.isProcessing = true
self.didRequireConfirmationThisRun = false
NotchContentState.shared.setCommandProcessing(true)
if self.shouldSyncCommandNotchState {
NotchContentState.shared.setCommandProcessing(true)
}

await self.processNextTurn()
}
Expand Down Expand Up @@ -374,7 +386,7 @@ final class CommandModeService: ObservableObject {
self.captureCommandRunCompleted(success: false)

// Push to notch
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.addCommandMessage(role: .assistant, content: errorMsg)
NotchContentState.shared.setCommandProcessing(false)
self.showExpandedNotchIfNeeded()
Expand All @@ -386,7 +398,7 @@ final class CommandModeService: ObservableObject {
self.currentStep = .thinking("Analyzing...")

// Push status to notch
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.addCommandMessage(role: .status, content: "Thinking...")
}

Expand Down Expand Up @@ -414,7 +426,7 @@ final class CommandModeService: ObservableObject {
))

// Push step to notch
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
let statusText = tc.purpose ?? self.stepDescription(for: stepType)
NotchContentState.shared.addCommandMessage(role: .status, content: statusText)
}
Expand All @@ -432,7 +444,7 @@ final class CommandModeService: ObservableObject {
self.currentStep = nil

// Push confirmation needed to notch
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.addCommandMessage(role: .status, content: "⚠️ Confirmation needed in Command Mode window")
NotchContentState.shared.setCommandProcessing(false)
}
Expand Down Expand Up @@ -464,7 +476,7 @@ final class CommandModeService: ObservableObject {
self.captureCommandRunCompleted(success: isFinal)

// Push final response to notch and show expanded view
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.updateCommandStreamingText("") // Clear streaming
NotchContentState.shared.addCommandMessage(role: .assistant, content: response.content)
NotchContentState.shared.setCommandProcessing(false)
Expand All @@ -488,7 +500,7 @@ final class CommandModeService: ObservableObject {
self.captureCommandRunCompleted(success: false)

// Push error to notch
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.addCommandMessage(role: .assistant, content: errorMsg)
NotchContentState.shared.setCommandProcessing(false)
self.showExpandedNotchIfNeeded()
Expand Down Expand Up @@ -530,7 +542,8 @@ final class CommandModeService: ObservableObject {

/// Show expanded notch output if there's content to display
private func showExpandedNotchIfNeeded() {
guard self.enableNotchOutput else { return }
guard self.shouldSyncCommandNotchState else { return }
guard NotchOverlayManager.shared.canShowExpandedCommandOutput else { return }
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 Allow bottom overlay to open expanded command output

Fresh evidence in this revision is the new showExpandedNotchIfNeeded guard on canShowExpandedCommandOutput: when a user previously selected Compact notch style and then switches to bottom overlay, this guard prevents expanded command output from opening even though bottom mode still syncs command messages. Because bottom overlay still has no in-place command history surface, command responses become inaccessible until the style is changed back or a different path re-syncs UI state.

Useful? React with 👍 / 👎.

guard !NotchContentState.shared.commandConversationHistory.isEmpty else { return }

// Show the expanded notch
Expand Down Expand Up @@ -913,7 +926,7 @@ final class CommandModeService: ObservableObject {
self.streamingText = fullContent

// Push to notch for real-time display
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.updateCommandStreamingText(fullContent)
}
}
Expand All @@ -929,7 +942,7 @@ final class CommandModeService: ObservableObject {
let fullContent = self.streamingBuffer.joined()
if !fullContent.isEmpty {
self.streamingText = fullContent
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.updateCommandStreamingText(fullContent)
}
}
Expand All @@ -946,7 +959,7 @@ final class CommandModeService: ObservableObject {
self.thinkingBuffer = [] // Clear thinking buffer

// Clear notch streaming text as well
if self.enableNotchOutput {
if self.shouldSyncCommandNotchState {
NotchContentState.shared.updateCommandStreamingText("")
}

Expand Down
32 changes: 23 additions & 9 deletions Sources/Fluid/Services/MenuBarManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
// Track pending overlay operations to prevent spam
private var pendingShowOperation: DispatchWorkItem?
private var pendingHideOperation: DispatchWorkItem?
private var pendingProcessingShowOperation: DispatchWorkItem?
private let processingVisualDelay: DispatchTimeInterval = .milliseconds(100)

// Subscription for forwarding audio levels to expanded command notch
private var expandedModeAudioSubscription: AnyCancellable?
Expand Down Expand Up @@ -87,9 +89,7 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
.receive(on: DispatchQueue.main)
.sink { [weak self] newText in
guard self != nil else { return }
// CRITICAL FIX: Check if streaming preview is enabled before updating notch
// The "Show Live Preview" toggle in Preferences should control this behavior
if SettingsStore.shared.enableStreamingPreview {
if NotchOverlayManager.shared.shouldShowOrTrackLivePreviewText {
NotchOverlayManager.shared.updateTranscriptionText(newText)
}
}
Expand All @@ -107,7 +107,6 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
// Prevent rapid state changes that could cause cycles
guard self.overlayVisible != isRunning else { return }

let delay: DispatchTimeInterval = .milliseconds(30)
if isRunning {
// Cancel any pending hide operation
self.pendingHideOperation?.cancel()
Expand All @@ -119,7 +118,7 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
if NotchOverlayManager.shared.isCommandOutputExpanded {
// Only keep expanded notch if this is a command mode recording (follow-up)
// For other modes (dictation, rewrite), close it and show regular notch
if self.currentOverlayMode == .command {
if self.currentOverlayMode == .command, NotchOverlayManager.shared.supportsCommandNotchUI {
// Enable recording visualization in the expanded notch
NotchContentState.shared.setRecordingInExpandedMode(true)

Expand All @@ -143,7 +142,10 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {

// Double-check expanded notch isn't showing (could have changed during delay)
// But only block if we're in command mode
if NotchOverlayManager.shared.isCommandOutputExpanded && self.currentOverlayMode == .command {
if NotchOverlayManager.shared.isCommandOutputExpanded,
self.currentOverlayMode == .command,
NotchOverlayManager.shared.supportsCommandNotchUI
{
self.pendingShowOperation = nil
return
}
Expand All @@ -157,7 +159,7 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
self.pendingShowOperation = nil
}
self.pendingShowOperation = showItem
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: showItem)
DispatchQueue.main.async(execute: showItem)
} else {
// Cancel any pending show operation
self.pendingShowOperation?.cancel()
Expand Down Expand Up @@ -191,7 +193,7 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
self.pendingHideOperation = nil
}
self.pendingHideOperation = hideItem
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: hideItem)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(30), execute: hideItem)
}
}

Expand All @@ -211,11 +213,22 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
self.isProcessingActive = processing

if processing {
self.pendingProcessingShowOperation?.cancel()
// Cancel any pending hide - we want to keep the overlay visible for AI processing
self.pendingHideOperation?.cancel()
self.pendingHideOperation = nil
self.overlayVisible = true

let showItem = DispatchWorkItem { [weak self] in
guard let self = self, self.isProcessingActive else { return }
NotchOverlayManager.shared.setProcessing(true)
self.pendingProcessingShowOperation = nil
}
self.pendingProcessingShowOperation = showItem
DispatchQueue.main.asyncAfter(deadline: .now() + self.processingVisualDelay, execute: showItem)
} else {
self.pendingProcessingShowOperation?.cancel()
self.pendingProcessingShowOperation = nil
// When processing ends, schedule the hide (unless expanded output is showing)
self.overlayVisible = false

Expand All @@ -240,8 +253,9 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
}
self.pendingHideOperation = hideItem
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: hideItem)
NotchOverlayManager.shared.setProcessing(false)
return
}
NotchOverlayManager.shared.setProcessing(processing)
}

private func setupMenuBarSafely() {
Expand Down
Loading
Loading