Skip to content

Commit 416c234

Browse files
authored
Merge pull request #265 from altic-dev/B/1.5.13-optimized-notch-design
UX changes for notch presentation and DynamicNotchKit pin update
2 parents eedfd1b + 8d66139 commit 416c234

10 files changed

Lines changed: 976 additions & 306 deletions

File tree

Fluid.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/Fluid/ContentView.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,27 +314,34 @@ struct ContentView: View {
314314

315315
// Set up notch click callback for expanding command conversation
316316
NotchOverlayManager.shared.onNotchClicked = {
317+
guard NotchOverlayManager.shared.canHandleNotchCommandTap else { return }
317318
// When notch is clicked in command mode, show expanded conversation
318-
if !NotchContentState.shared.commandConversationHistory.isEmpty {
319+
if NotchOverlayManager.shared.canShowExpandedCommandOutput,
320+
!NotchContentState.shared.commandConversationHistory.isEmpty
321+
{
319322
NotchOverlayManager.shared.showExpandedCommandOutput()
320323
}
321324
}
322325

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

328332
// Chat management callbacks
329333
NotchOverlayManager.shared.onNewChat = { [weak commandModeService] in
334+
guard NotchOverlayManager.shared.allowsCommandNotchActions else { return }
330335
commandModeService?.createNewChat()
331336
}
332337

333338
NotchOverlayManager.shared.onSwitchChat = { [weak commandModeService] chatID in
339+
guard NotchOverlayManager.shared.allowsCommandNotchActions else { return }
334340
commandModeService?.switchToChat(id: chatID)
335341
}
336342

337343
NotchOverlayManager.shared.onClearChat = { [weak commandModeService] in
344+
guard NotchOverlayManager.shared.allowsCommandNotchActions else { return }
338345
commandModeService?.deleteCurrentChat()
339346
}
340347

@@ -1777,12 +1784,12 @@ struct ContentView: View {
17771784

17781785
self.clearActiveRecordingMode()
17791786

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

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

@@ -1877,7 +1885,7 @@ struct ContentView: View {
18771885
let postProcessingStart = Date()
18781886

18791887
// Update overlay text to show we're now refining (processing already true)
1880-
NotchOverlayManager.shared.updateTranscriptionText("Refining...")
1888+
NotchOverlayManager.shared.updateTranscriptionText("Refining")
18811889

18821890
// Ensure the status label becomes visible immediately.
18831891
await Task.yield()
@@ -2040,6 +2048,10 @@ struct ContentView: View {
20402048
]
20412049
)
20422050
}
2051+
2052+
if !didTypeExternally {
2053+
NotchOverlayManager.shared.hide()
2054+
}
20432055
}
20442056

20452057
private func currentDictationOutputRouteForHotkeyStop() -> DictationOutputRoute {

Sources/Fluid/Persistence/SettingsStore.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,22 @@ final class SettingsStore: ObservableObject {
13501350
}
13511351
}
13521352

1353+
/// Internal presentation modes for the top notch overlay.
1354+
/// This is intentionally separate from bottom overlay sizing.
1355+
enum NotchPresentationMode: String, CaseIterable, Codable {
1356+
case standard
1357+
case minimal
1358+
1359+
var displayName: String {
1360+
switch self {
1361+
case .standard:
1362+
return "Standard Notch"
1363+
case .minimal:
1364+
return "Compact"
1365+
}
1366+
}
1367+
}
1368+
13531369
/// Where the recording overlay appears (default: bottom)
13541370
var overlayPosition: OverlayPosition {
13551371
get {
@@ -1366,6 +1382,22 @@ final class SettingsStore: ObservableObject {
13661382
}
13671383
}
13681384

1385+
/// Internal-only top notch presentation mode. No public settings UI yet.
1386+
var notchPresentationMode: NotchPresentationMode {
1387+
get {
1388+
guard let raw = self.defaults.string(forKey: Keys.notchPresentationMode),
1389+
let mode = NotchPresentationMode(rawValue: raw)
1390+
else {
1391+
return .standard
1392+
}
1393+
return mode
1394+
}
1395+
set {
1396+
objectWillChange.send()
1397+
self.defaults.set(newValue.rawValue, forKey: Keys.notchPresentationMode)
1398+
}
1399+
}
1400+
13691401
/// Vertical offset for the bottom overlay (distance from bottom of screen/dock)
13701402
var overlayBottomOffset: Double {
13711403
get {
@@ -3625,6 +3657,7 @@ private extension SettingsStore {
36253657

36263658
// Overlay Position
36273659
static let overlayPosition = "OverlayPosition"
3660+
static let notchPresentationMode = "NotchPresentationMode"
36283661
static let overlayBottomOffset = "OverlayBottomOffset"
36293662
static let overlayBottomOffsetMigratedTo50 = "OverlayBottomOffsetMigratedTo50"
36303663
static let overlaySize = "OverlaySize"

Sources/Fluid/Services/CommandModeService.swift

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ final class CommandModeService: ObservableObject {
3333
self.loadCurrentChatFromStore()
3434
}
3535

36+
private var shouldSyncCommandNotchState: Bool {
37+
self.enableNotchOutput && NotchOverlayManager.shared.shouldSyncCommandConversationToNotch
38+
}
39+
3640
private func loadCurrentChatFromStore() {
3741
if let session = chatStore.currentSession {
3842
self.currentChatID = session.id
@@ -278,6 +282,10 @@ final class CommandModeService: ObservableObject {
278282

279283
/// Sync conversation history to NotchContentState
280284
private func syncToNotchState() {
285+
guard self.shouldSyncCommandNotchState else {
286+
return
287+
}
288+
281289
NotchContentState.shared.clearCommandOutput()
282290

283291
for msg in self.conversationHistory {
@@ -308,7 +316,7 @@ final class CommandModeService: ObservableObject {
308316
self.saveCurrentChat()
309317

310318
// Push to notch
311-
if self.enableNotchOutput {
319+
if self.shouldSyncCommandNotchState {
312320
NotchContentState.shared.addCommandMessage(role: .user, content: text)
313321
NotchContentState.shared.setCommandProcessing(true)
314322
}
@@ -322,14 +330,18 @@ final class CommandModeService: ObservableObject {
322330

323331
// Add to both histories
324332
self.conversationHistory.append(Message(role: .user, content: text))
325-
NotchContentState.shared.addCommandMessage(role: .user, content: text)
333+
if self.shouldSyncCommandNotchState {
334+
NotchContentState.shared.addCommandMessage(role: .user, content: text)
335+
}
326336

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

330340
self.isProcessing = true
331341
self.didRequireConfirmationThisRun = false
332-
NotchContentState.shared.setCommandProcessing(true)
342+
if self.shouldSyncCommandNotchState {
343+
NotchContentState.shared.setCommandProcessing(true)
344+
}
333345

334346
await self.processNextTurn()
335347
}
@@ -374,7 +386,7 @@ final class CommandModeService: ObservableObject {
374386
self.captureCommandRunCompleted(success: false)
375387

376388
// Push to notch
377-
if self.enableNotchOutput {
389+
if self.shouldSyncCommandNotchState {
378390
NotchContentState.shared.addCommandMessage(role: .assistant, content: errorMsg)
379391
NotchContentState.shared.setCommandProcessing(false)
380392
self.showExpandedNotchIfNeeded()
@@ -386,7 +398,7 @@ final class CommandModeService: ObservableObject {
386398
self.currentStep = .thinking("Analyzing...")
387399

388400
// Push status to notch
389-
if self.enableNotchOutput {
401+
if self.shouldSyncCommandNotchState {
390402
NotchContentState.shared.addCommandMessage(role: .status, content: "Thinking...")
391403
}
392404

@@ -414,7 +426,7 @@ final class CommandModeService: ObservableObject {
414426
))
415427

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

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

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

490502
// Push error to notch
491-
if self.enableNotchOutput {
503+
if self.shouldSyncCommandNotchState {
492504
NotchContentState.shared.addCommandMessage(role: .assistant, content: errorMsg)
493505
NotchContentState.shared.setCommandProcessing(false)
494506
self.showExpandedNotchIfNeeded()
@@ -530,7 +542,8 @@ final class CommandModeService: ObservableObject {
530542

531543
/// Show expanded notch output if there's content to display
532544
private func showExpandedNotchIfNeeded() {
533-
guard self.enableNotchOutput else { return }
545+
guard self.shouldSyncCommandNotchState else { return }
546+
guard NotchOverlayManager.shared.canShowExpandedCommandOutput else { return }
534547
guard !NotchContentState.shared.commandConversationHistory.isEmpty else { return }
535548

536549
// Show the expanded notch
@@ -913,7 +926,7 @@ final class CommandModeService: ObservableObject {
913926
self.streamingText = fullContent
914927

915928
// Push to notch for real-time display
916-
if self.enableNotchOutput {
929+
if self.shouldSyncCommandNotchState {
917930
NotchContentState.shared.updateCommandStreamingText(fullContent)
918931
}
919932
}
@@ -929,7 +942,7 @@ final class CommandModeService: ObservableObject {
929942
let fullContent = self.streamingBuffer.joined()
930943
if !fullContent.isEmpty {
931944
self.streamingText = fullContent
932-
if self.enableNotchOutput {
945+
if self.shouldSyncCommandNotchState {
933946
NotchContentState.shared.updateCommandStreamingText(fullContent)
934947
}
935948
}
@@ -946,7 +959,7 @@ final class CommandModeService: ObservableObject {
946959
self.thinkingBuffer = [] // Clear thinking buffer
947960

948961
// Clear notch streaming text as well
949-
if self.enableNotchOutput {
962+
if self.shouldSyncCommandNotchState {
950963
NotchContentState.shared.updateCommandStreamingText("")
951964
}
952965

Sources/Fluid/Services/MenuBarManager.swift

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
4444
// Track pending overlay operations to prevent spam
4545
private var pendingShowOperation: DispatchWorkItem?
4646
private var pendingHideOperation: DispatchWorkItem?
47+
private var pendingProcessingShowOperation: DispatchWorkItem?
48+
private let processingVisualDelay: DispatchTimeInterval = .milliseconds(100)
4749

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

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

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

144143
// Double-check expanded notch isn't showing (could have changed during delay)
145144
// But only block if we're in command mode
146-
if NotchOverlayManager.shared.isCommandOutputExpanded && self.currentOverlayMode == .command {
145+
if NotchOverlayManager.shared.isCommandOutputExpanded,
146+
self.currentOverlayMode == .command,
147+
NotchOverlayManager.shared.supportsCommandNotchUI
148+
{
147149
self.pendingShowOperation = nil
148150
return
149151
}
@@ -157,7 +159,7 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
157159
self.pendingShowOperation = nil
158160
}
159161
self.pendingShowOperation = showItem
160-
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: showItem)
162+
DispatchQueue.main.async(execute: showItem)
161163
} else {
162164
// Cancel any pending show operation
163165
self.pendingShowOperation?.cancel()
@@ -191,7 +193,7 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
191193
self.pendingHideOperation = nil
192194
}
193195
self.pendingHideOperation = hideItem
194-
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: hideItem)
196+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(30), execute: hideItem)
195197
}
196198
}
197199

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

213215
if processing {
216+
self.pendingProcessingShowOperation?.cancel()
214217
// Cancel any pending hide - we want to keep the overlay visible for AI processing
215218
self.pendingHideOperation?.cancel()
216219
self.pendingHideOperation = nil
217220
self.overlayVisible = true
221+
222+
let showItem = DispatchWorkItem { [weak self] in
223+
guard let self = self, self.isProcessingActive else { return }
224+
NotchOverlayManager.shared.setProcessing(true)
225+
self.pendingProcessingShowOperation = nil
226+
}
227+
self.pendingProcessingShowOperation = showItem
228+
DispatchQueue.main.asyncAfter(deadline: .now() + self.processingVisualDelay, execute: showItem)
218229
} else {
230+
self.pendingProcessingShowOperation?.cancel()
231+
self.pendingProcessingShowOperation = nil
219232
// When processing ends, schedule the hide (unless expanded output is showing)
220233
self.overlayVisible = false
221234

@@ -240,8 +253,9 @@ final class MenuBarManager: NSObject, ObservableObject, NSMenuDelegate {
240253
}
241254
self.pendingHideOperation = hideItem
242255
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: hideItem)
256+
NotchOverlayManager.shared.setProcessing(false)
257+
return
243258
}
244-
NotchOverlayManager.shared.setProcessing(processing)
245259
}
246260

247261
private func setupMenuBarSafely() {

0 commit comments

Comments
 (0)