Skip to content

Add floating bar custom notifications#5606

Merged
kodjima33 merged 1 commit intomainfrom
feat/floating-bar-custom-notifications
Mar 13, 2026
Merged

Add floating bar custom notifications#5606
kodjima33 merged 1 commit intomainfrom
feat/floating-bar-custom-notifications

Conversation

@kodjima33
Copy link
Collaborator

Summary

  • replace desktop system notifications with in-app notifications rendered below the floating bar
  • reuse floating bar styling and window management for queued notification presentation
  • update onboarding notifications step to demonstrate the new custom notification UI

Testing

  • xcrun swift build -c debug --package-path Desktop

@kodjima33 kodjima33 merged commit 5dd2d6e into main Mar 13, 2026
1 check passed
@kodjima33 kodjima33 deleted the feat/floating-bar-custom-notifications branch March 13, 2026 22:46
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR replaces macOS system (UNUserNotification) notifications with custom in-app notifications rendered directly below the floating bar, reusing the existing bar window and animation infrastructure. The approach is architecturally clean — FloatingBarNotification flows through FloatingControlBarStateFloatingControlBarWindowFloatingControlBarView, and FloatingControlBarManager handles queuing, auto-dismiss, and temporary window show/hide for disabled-bar users.

However, two behavioral regressions and one logic bug were found:

  • Screen capture reset broken: NotificationService.sendNotification now routes every notification through FloatingControlBarManager.showNotification, which only shows a floating bar card. The existing "Screen Recording Needs Reset" notification (sent from ProactiveAssistantsPlugin on both recovery paths) previously posted a UNNotificationRequest with a "Reset Now" UNNotificationAction. That entire code path (deliverNotification + screenCaptureResetCategoryId) is now dead code. Users with broken screen capture will see a floating bar message with no actionable button and no automatic reset.
  • Window not hidden after chained queued notifications: notificationWasTemporarilyShown is reset to false inside presentNotification when the window is already visible (chaining from the queue). This means when the bar is disabled and ≥2 notifications fire, the window is never hidden after the last notification is dismissed.
  • Default notification sound dropped: NotificationSound.default no longer plays any audio; the switch in showNotification only handles focusLost/focusRegained.

Confidence Score: 2/5

  • Not safe to merge — the screen capture reset flow is broken and the window-visibility bug will surface for disabled-bar users who receive multiple notifications.
  • Two concrete behavioral regressions: (1) the screen capture "Reset Now" action is silently dropped because deliverNotification is now dead code, leaving users unable to reset screen capture from the notification; (2) notificationWasTemporarilyShown is incorrectly cleared mid-chain, so the floating bar window remains visible after queued notifications when the bar is disabled. These are not edge-case issues — screen capture recovery is a critical repair path and queued notifications are expected in normal use.
  • NotificationService.swift (screen capture reset regression) and FloatingControlBarWindow.swift (notificationWasTemporarilyShown bug + missing default sound)

Important Files Changed

Filename Overview
desktop/Desktop/Sources/FloatingControlBar/FloatingControlBarWindow.swift Adds notification show/dismiss/queue management — contains a notificationWasTemporarilyShown bug that leaves the window visible after chained queued notifications when the bar is disabled, and silently drops the default notification sound.
desktop/Desktop/Sources/ProactiveAssistants/Services/NotificationService.swift Routes all notifications through FloatingControlBarManager, making deliverNotification dead code and breaking the screen capture reset "Reset Now" action button flow.
desktop/Desktop/Sources/FloatingControlBar/FloatingControlBarView.swift Adds notificationView component and refactors hover resize into the outer VStack body; the view logic is clean and the animation/transition approach is sensible.
desktop/Desktop/Sources/FloatingControlBar/FloatingControlBarState.swift Adds FloatingBarNotification struct and currentNotification published property; straightforward and correct.
desktop/Desktop/Sources/OnboardingNotificationStepView.swift Adds chatProvider dependency and wires up the floating bar during onboarding to demonstrate the new in-app notification; stale comment on line 130.
desktop/Desktop/Sources/OnboardingView.swift Passes chatProvider to OnboardingNotificationStepView; minimal, correct change.

Sequence Diagram

sequenceDiagram
    participant PA as ProactiveAssistantsPlugin
    participant NS as NotificationService
    participant FCM as FloatingControlBarManager
    participant FCW as FloatingControlBarWindow
    participant View as FloatingControlBarView

    PA->>NS: sendNotification(title, message)
    NS->>FCM: showNotification(title, message, assistantId, sound)
    FCM->>FCM: play custom sound (focusLost/focusRegained only)

    alt window hidden && bar disabled
        FCM->>FCW: orderFrontRegardless()
        FCM->>FCM: notificationWasTemporarilyShown = true
    end

    alt currentNotification == nil && !showingAIConversation
        FCM->>FCW: showNotification(notification)
        FCW->>View: state.currentNotification = notification
        FCW->>FCW: resizeAnchored(expandedWidth × notificationHeight)
        FCM->>FCM: schedule auto-dismiss after 6s
    else
        FCM->>FCM: pendingNotifications.append(notification)
    end

    alt User taps ✕
        View->>FCM: dismissCurrentNotification()
        FCM->>FCM: cancel auto-dismiss timer
    else Auto-dismiss fires
        FCM->>FCM: dismissNotificationAndAdvanceQueue()
    end

    FCM->>FCW: dismissNotification()
    FCW->>View: state.currentNotification = nil
    FCW->>FCW: resizeAnchored(minBarSize or expandedBarSize)

    alt pendingNotifications not empty
        FCM->>FCM: presentNotification(next) [⚠ resets notificationWasTemporarilyShown]
    else no more pending
        alt notificationWasTemporarilyShown && !isEnabled
            FCM->>FCW: orderOut(nil)
        end
        FCM->>FCM: notificationWasTemporarilyShown = false
    end
Loading

Comments Outside Diff (1)

  1. desktop/Desktop/Sources/OnboardingNotificationStepView.swift, line 130 (link)

    Stale code comment

    The comment "Send a real macOS notification" no longer reflects what this call does — it now triggers an in-app floating bar notification, not a system UNUserNotification.

Last reviewed commit: e089fc5

Comment on lines +1044 to +1050
private func presentNotification(_ notification: FloatingBarNotification, in window: FloatingControlBarWindow) {
if !window.isVisible {
notificationWasTemporarilyShown = true
window.orderFrontRegardless()
} else {
notificationWasTemporarilyShown = false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

notificationWasTemporarilyShown incorrectly reset when chaining queued notifications

When the bar is disabled (!isEnabled) and two notifications arrive in quick succession:

  1. First notification: window is hidden → notificationWasTemporarilyShown = true, window shown temporarily.
  2. Second notification: queued in pendingNotifications.
  3. First dismissed → dismissNotificationAndAdvanceQueue calls presentNotification(#2, window) and returns early, skipping the orderOut + reset block.
  4. In presentNotification(#2), the window is now visible so notificationWasTemporarilyShown = false is set unconditionally.
  5. Second notification dismissed → notificationWasTemporarilyShown == false, so window.orderOut(nil) is never called.

Result: the floating bar window remains visible even when the user has the bar disabled, as long as at least two notifications fired.

The fix is to not reset notificationWasTemporarilyShown to false when window is already visible during a queued-notification chain:

private func presentNotification(_ notification: FloatingBarNotification, in window: FloatingControlBarWindow) {
    if !window.isVisible {
        notificationWasTemporarilyShown = true
        window.orderFrontRegardless()
    }
    // Do NOT set notificationWasTemporarilyShown = false here;
    // it was set true by the first notification and should persist through the chain.
    // ...
}

Comment on lines 200 to 207
func sendNotification(title: String, message: String, assistantId: String = "default", sound: NotificationSound = .default) {
// Check permission before attempting delivery to avoid UNErrorDomain code 1 errors
UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in
Task { @MainActor in
guard settings.authorizationStatus == .authorized else {
log("Notification skipped (auth=\(settings.authorizationStatus.rawValue)): \(title)")

// If auth reverted to notDetermined (not explicitly denied), trigger repair
// Debounce: at most once per 10 minutes to avoid hammering lsregister
if settings.authorizationStatus == .notDetermined {
let now = Date()
if self?.lastRepairAttempt == nil || now.timeIntervalSince(self?.lastRepairAttempt ?? .distantPast) > 600 {
self?.lastRepairAttempt = now
log("Notification auth is notDetermined at send time — triggering repair")
AnalyticsManager.shared.notificationRepairTriggered(
reason: "send_time_not_determined",
previousStatus: "unknown",
currentStatus: "notDetermined"
)
ProactiveAssistantsPlugin.repairNotificationRegistration()
}
}

return
}

self?.deliverNotification(title: title, message: message, assistantId: assistantId, sound: sound)
}
}
FloatingControlBarManager.shared.showNotification(
title: title,
message: message,
assistantId: assistantId,
sound: sound
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Screen capture reset "Reset Now" action button silently dropped

sendNotification is now the only public entry point and it routes all notifications through FloatingControlBarManager.showNotification. However, the screen capture reset notification (NotificationService.screenCaptureResetTitle) is sent via NotificationService.shared.sendNotification(...) from ProactiveAssistantsPlugin (lines 1342–1345 and 1355–1358 in ProactiveAssistantsPlugin.swift).

Previously, deliverNotification detected this title and assigned screenCaptureResetCategoryId, which added a "Reset Now" UNNotificationAction. The UNUserNotificationCenter delegate then called handleScreenCaptureResetAction when the user tapped it, triggering ScreenCaptureService.resetScreenCapturePermissionAndRestart().

With this change:

  • The notification appears only as a floating bar notification with no action button.
  • deliverNotification (and all its screenCaptureResetCategoryId logic) is now unreachable dead code.
  • Users who encounter a broken screen recording session will see a message saying "Click to open Settings" but no actionable button and no automatic reset mechanism.

The screen capture reset notification likely needs to keep using a system UNNotification with its action button, or a dedicated action button needs to be added to the floating bar notification view for this specific case.

Comment on lines +880 to +885
switch sound {
case .focusLost, .focusRegained:
sound.playCustomSound()
case .default, .none:
break
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Default notification sound is silently dropped

In showNotification, the switch only plays sounds for .focusLost and .focusRegained. For NotificationSound.default (the case used by most proactive assistant notifications), no sound is played at all.

Previously, deliverNotification passed sound.unSound (which returns UNNotificationSound.default for the .default case) to UNMutableNotificationContent.sound, so the system played the default notification chime. That feedback is now absent for all standard notifications.

If silent in-app notifications are intentional, the NotificationSound.default case in NotificationSound.unSound and the comment on line 130 of OnboardingNotificationStepView should be updated to reflect this. If not, a sound should be played here for .default — for example:

case .default:
    NSSound.beep()

or load and play a bundled sound similar to the custom cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant