Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
A5001094 /* NotificationsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001091 /* NotificationsPage.swift */; };
A5001095 /* TerminalNotificationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001092 /* TerminalNotificationStore.swift */; };
A5001100 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5001101 /* Assets.xcassets */; };
IC000003 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = IC000002 /* AppIcon.icon */; };
A5001201 /* UpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001211 /* UpdateController.swift */; };
A5001202 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001212 /* UpdateDelegate.swift */; };
A5001203 /* UpdateDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001213 /* UpdateDriver.swift */; };
Expand Down Expand Up @@ -387,7 +388,7 @@
FE003001 /* SessionIndexStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIndexStore.swift; sourceTree = "<group>"; };
FE003002 /* SessionIndexView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIndexView.swift; sourceTree = "<group>"; };
FE003003 /* RightSidebarPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSidebarPanelView.swift; sourceTree = "<group>"; };
IC000002 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = AppIcon.icon; sourceTree = "<group>"; };
IC000002 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -824,6 +825,7 @@
buildActionMask = 2147483647;
files = (
A5001100 /* Assets.xcassets in Resources */,
IC000003 /* AppIcon.icon in Resources */,
84E00D47E4584162AE53BC8D /* xterm-ghostty in Resources */,
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */,
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */,
Expand Down
84 changes: 71 additions & 13 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,37 @@ func cmuxJavaScriptStringLiteral(_ value: String?) -> String? {

final class MainWindowHostingView<Content: View>: NSHostingView<Content> {
private let zeroSafeAreaLayoutGuide = NSLayoutGuide()
private let usesSystemSafeArea: Bool

override var safeAreaInsets: NSEdgeInsets { NSEdgeInsetsZero }
override var safeAreaRect: NSRect { bounds }
override var safeAreaLayoutGuide: NSLayoutGuide { zeroSafeAreaLayoutGuide }
override var safeAreaInsets: NSEdgeInsets {
usesSystemSafeArea ? super.safeAreaInsets : NSEdgeInsetsZero
}
override var safeAreaRect: NSRect {
usesSystemSafeArea ? super.safeAreaRect : bounds
}
override var safeAreaLayoutGuide: NSLayoutGuide {
usesSystemSafeArea ? super.safeAreaLayoutGuide : zeroSafeAreaLayoutGuide
}

required init(rootView: Content) {
if #available(macOS 26.0, *) {
// On macOS 26, use system safe area so:
// - Sidebar (.ignoresSafeArea) extends under the glass titlebar
// - Terminal content respects the titlebar and stays below it
self.usesSystemSafeArea = true
} else {
self.usesSystemSafeArea = false
}
super.init(rootView: rootView)
addLayoutGuide(zeroSafeAreaLayoutGuide)
NSLayoutConstraint.activate([
zeroSafeAreaLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor),
zeroSafeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor),
zeroSafeAreaLayoutGuide.topAnchor.constraint(equalTo: topAnchor),
zeroSafeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor),
])
if !usesSystemSafeArea {
addLayoutGuide(zeroSafeAreaLayoutGuide)
NSLayoutConstraint.activate([
zeroSafeAreaLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor),
zeroSafeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor),
zeroSafeAreaLayoutGuide.topAnchor.constraint(equalTo: topAnchor),
zeroSafeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}

@available(*, unavailable)
Expand Down Expand Up @@ -1002,6 +1019,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
syncMenuBarExtraVisibility()
updateController.startUpdaterIfNeeded()
}
// Start the titlebar accessory controller on all versions so the
// notifications popover infrastructure is available. On macOS 26
// the visual titlebar items come from SwiftUI .toolbar, but the
// popover is still managed by the accessory controller.
titlebarAccessoryController.start()
windowDecorationsController.start()
installMainWindowKeyObserver()
Expand Down Expand Up @@ -5552,10 +5573,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
window.collectionBehavior.insert(.fullScreenDisallowsTiling)
}
window.title = ""
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
if #available(macOS 26.0, *) {
// On macOS 26+, let the system render the native glass titlebar
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = false
} else {
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
}
window.isMovableByWindowBackground = false
window.isMovable = false
if #available(macOS 26.0, *) {
window.isMovable = true
} else {
window.isMovable = false
}
let restoredFrame = resolvedWindowFrame(from: sessionWindowSnapshot)
if let restoredFrame {
window.setFrame(restoredFrame, display: false)
Expand Down Expand Up @@ -8588,6 +8619,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
#endif

func attachUpdateAccessory(to window: NSWindow) {
if #available(macOS 26.0, *) {
// On macOS 26, toolbar buttons are native SwiftUI .toolbar items
// in the NavigationSplitView. Skip attaching the old titlebar
// accessory views, but the controller is already started (for
// notifications popover support).
return
}
titlebarAccessoryController.start()
titlebarAccessoryController.attach(to: window)
}
Comment on lines 8621 to 8631
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Bell button is a no-op on macOS 26

attachUpdateAccessory(to:) returns early without calling titlebarAccessoryController.start() or .attach(to:), and applicationDidFinishLaunching also skips .start() on macOS 26 (~line 2601). As a result controlsControllers.allObjects is always empty. When the SwiftUI toolbar bell calls AppDelegate.shared?.toggleNotificationsPopover(animated: true), UpdateTitlebarAccessoryController.toggleNotificationsPopover bails immediately on guard !controllers.isEmpty else { return } — clicking the bell silently does nothing.

Fix: either call titlebarAccessoryController.start() (without attach) so the popover infrastructure is initialized, or route the bell action through a macOS-26-specific popover path that does not depend on controlsControllers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

titlebarAccessoryController.start() is already called unconditionally at app launch (AppDelegate.swift:2605), so controlsControllers is populated on macOS 26 too. On 26, the SwiftUI toolbar's bell button toggles local @State directly, and menu/shortcut paths go through toggleNotificationsPopover → NotificationCenter broadcast. The broadcast is now window-scoped in afb1af9.

Expand All @@ -8596,7 +8634,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
windowDecorationsController.apply(to: window)
}

/// Notification posted on macOS 26 to toggle the SwiftUI notifications popover.
static let toggleNotificationsPopoverNotification = Notification.Name("cmux.toggleNotificationsPopover")

func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
if #available(macOS 26.0, *) {
// Scope the broadcast to the owning window so a single bell-button
// tap or menu command only toggles the popover in the intended
// window — not in every cmux window at once.
let targetWindow = anchorView?.window ?? NSApp.keyWindow
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
let targetWindowId = targetWindow.flatMap { mainWindowContexts[ObjectIdentifier($0)]?.windowId }
var userInfo: [AnyHashable: Any] = [:]
if let targetWindowId {
userInfo["windowId"] = targetWindowId
}
NotificationCenter.default.post(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
name: Self.toggleNotificationsPopoverNotification,
object: targetWindow,
userInfo: userInfo.isEmpty ? nil : userInfo
)
return
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.
titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
Comment on lines +8637 to 8676
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope and complete the macOS 26 notifications presenter.

This bridge only emits a global toggle and drops the originating window. The SwiftUI side therefore has no deterministic target window, and the explicit show/dismiss/state entry points in this file (showNotificationsPopoverFromMenuBar, dismissNotificationsPopoverIfShown, isNotificationsPopoverShown) still talk to titlebarAccessoryController. On macOS 26 that can desync menu-bar/Escape behavior from the popover that is actually visible.

🛠️ Suggested direction
     /// Notification posted on macOS 26 to toggle the SwiftUI notifications popover.
     static let toggleNotificationsPopoverNotification = Notification.Name("cmux.toggleNotificationsPopover")
+    static let showNotificationsPopoverNotification = Notification.Name("cmux.showNotificationsPopover")
+    static let dismissNotificationsPopoverNotification = Notification.Name("cmux.dismissNotificationsPopover")
+    static let notificationsPopoverWindowIdUserInfoKey = "windowId"

     func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
         if `#available`(macOS 26.0, *) {
-            NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: nil)
+            guard
+                let window = anchorView?.window ?? NSApp.keyWindow ?? NSApp.mainWindow,
+                let windowId = mainWindowId(for: window)
+            else { return }
+            NotificationCenter.default.post(
+                name: Self.toggleNotificationsPopoverNotification,
+                object: window,
+                userInfo: [Self.notificationsPopoverWindowIdUserInfoKey: windowId]
+            )
             return
         }
         titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
     }

Then route showNotificationsPopoverFromMenuBar(), dismissNotificationsPopoverIfShown(), and isNotificationsPopoverShown() through the same macOS 26 presenter/observer path instead of the legacy accessory controller.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 10168 - 10176, The macOS 26 bridge
only posts a global toggle and drops the originating window, causing desync with
titlebarAccessoryController; change the NotificationCenter bridge used by
toggleNotificationsPopover(animated:anchorView:) to include identifying info
(e.g., the originating NSWindow or a unique window identifier in userInfo) so
the SwiftUI presenter has a deterministic target, and then route
showNotificationsPopoverFromMenuBar(), dismissNotificationsPopoverIfShown(), and
isNotificationsPopoverShown() to use the same macOS 26 notification/presenter
path instead of calling titlebarAccessoryController directly (keep the legacy
controller path for <macOS26). Locate and update the symbols
toggleNotificationsPopover(animated:anchorView:),
showNotificationsPopoverFromMenuBar(), dismissNotificationsPopoverIfShown(), and
isNotificationsPopoverShown() to post/observe and query the unified presenter
notification (using userInfo keys) so all entry points target the same presenter
on macOS 26.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Partially addressed in afb1af9 — the toggle broadcast now scopes to the owning window via userInfo["windowId"] and ContentView filters on that. The dismiss / isShown bridges still route through the legacy controller on 26, which currently reports 'nothing shown' because the SwiftUI @State owns the popover. Tracking for a follow-up that wires the SwiftUI state back through AppDelegate.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@rodchristiansen Thanks — I see the toggle is now correctly window-scoped via userInfo["windowId"] and ContentView filters on it. I can complete the bridge so all AppDelegate entry points use the SwiftUI presenter on macOS 26 and stop reporting “nothing shown”.

Plan:

  • Add show/dismiss notifications + a visibilityDidChange notification with userInfo { windowId, visible }.
  • Track visibility per-window in AppDelegate for isShown()/dismiss() return values.
  • Route showNotificationsPopoverFromMenuBar(), dismissNotificationsPopoverIfShown(), and isNotificationsPopoverShown() through the macOS 26 presenter path; keep legacy accessory for < macOS 26.
  • Have ContentView observe show/dismiss and publish visibilityDidChange whenever its @State changes.

Proposed changes

AppDelegate.swift

  • Notifications and keys, visibility tracking, and unified routing
@@
     /// Notification posted on macOS 26 to toggle the SwiftUI notifications popover.
     static let toggleNotificationsPopoverNotification = Notification.Name("cmux.toggleNotificationsPopover")
+    /// Explicit show on macOS 26 (used by menu bar path).
+    static let showNotificationsPopoverNotification = Notification.Name("cmux.showNotificationsPopover")
+    /// Explicit dismiss on macOS 26 (used by Escape/menu bar and imperative callers).
+    static let dismissNotificationsPopoverNotification = Notification.Name("cmux.dismissNotificationsPopover")
+    /// SwiftUI presenter reports state back to AppDelegate on any present/dismiss.
+    static let notificationsPopoverVisibilityDidChangeNotification = Notification.Name("cmux.notificationsPopoverVisibilityDidChange")
+    /// userInfo keys shared across all of the above.
+    static let notificationsPopoverWindowIdUserInfoKey = "windowId"
+    static let notificationsPopoverVisibleUserInfoKey = "visible"
+
+    // Track popover visibility by window for isShown()/dismiss return value on macOS 26.
+    private var notificationsPopoverVisibleByWindowId: [UUID: Bool] = [:]
+    private var notificationsPopoverVisibilityObserver: NSObjectProtocol?
@@
     func applicationDidFinishLaunching(_ notification: Notification) {
         …
         titlebarAccessoryController.start()
         windowDecorationsController.start()
+        // Track SwiftUI-presented popover visibility on macOS 26.
+        installNotificationsPopoverVisibilityObserver()
         …
     }
@@
+    private func installNotificationsPopoverVisibilityObserver() {
+        guard notificationsPopoverVisibilityObserver == nil else { return }
+        notificationsPopoverVisibilityObserver = NotificationCenter.default.addObserver(
+            forName: Self.notificationsPopoverVisibilityDidChangeNotification,
+            object: nil,
+            queue: .main
+        ) { [weak self] note in
+            guard let self else { return }
+            guard let userInfo = note.userInfo,
+                  let windowId = userInfo[Self.notificationsPopoverWindowIdUserInfoKey] as? UUID,
+                  let visible = userInfo[Self.notificationsPopoverVisibleUserInfoKey] as? Bool else {
+                return
+            }
+            self.notificationsPopoverVisibleByWindowId[windowId] = visible
+        }
+    }
@@
-    func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
-        if `#available`(macOS 26.0, *) {
-            // Scope the broadcast to the owning window so a single bell-button
-            // tap or menu command only toggles the popover in the intended
-            // window — not in every cmux window at once.
-            let targetWindow = anchorView?.window ?? NSApp.keyWindow
-            let targetWindowId = targetWindow.flatMap { mainWindowContexts[ObjectIdentifier($0)]?.windowId }
-            var userInfo: [AnyHashable: Any] = [:]
-            if let targetWindowId {
-                userInfo["windowId"] = targetWindowId
-            }
-            NotificationCenter.default.post(
-                name: Self.toggleNotificationsPopoverNotification,
-                object: targetWindow,
-                userInfo: userInfo.isEmpty ? nil : userInfo
-            )
-            return
-        }
-        titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
+    func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
+        if `#available`(macOS 26.0, *) {
+            let targetWindow = anchorView?.window ?? NSApp.keyWindow ?? NSApp.mainWindow
+            guard let window = targetWindow, let windowId = mainWindowId(for: window) else { return }
+            NotificationCenter.default.post(
+                name: Self.toggleNotificationsPopoverNotification,
+                object: window,
+                userInfo: [Self.notificationsPopoverWindowIdUserInfoKey: windowId]
+            )
+            return
+        }
+        titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
     }
@@
-    func showNotificationsPopoverFromMenuBar() {
+    func showNotificationsPopoverFromMenuBar() {
         let context: MainWindowContext? = {
             if let keyWindow = NSApp.keyWindow,
                let keyContext = contextForMainTerminalWindow(keyWindow) {
                 return keyContext
             }
             if let first = mainWindowContexts.values.first {
                 return first
             }
             let windowId = createMainWindow()
             return mainWindowContexts.values.first(where: { $0.windowId == windowId })
         }()
 
-        if let context,
-           let window = context.window ?? windowForMainWindowId(context.windowId) {
-            setActiveMainWindow(window)
-            bringToFront(window)
-        }
-
-        DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
-            self?.titlebarAccessoryController.showNotificationsPopover(animated: false)
-        }
+        if let context, let window = context.window ?? windowForMainWindowId(context.windowId) {
+            setActiveMainWindow(window)
+            bringToFront(window)
+            if `#available`(macOS 26.0, *) {
+                NotificationCenter.default.post(
+                    name: Self.showNotificationsPopoverNotification,
+                    object: window,
+                    userInfo: [Self.notificationsPopoverWindowIdUserInfoKey: context.windowId]
+                )
+                return
+            }
+        }
+        // Legacy path (< macOS 26)
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
+            self?.titlebarAccessoryController.showNotificationsPopover(animated: false)
+        }
     }
@@
-    `@discardableResult`
-    func dismissNotificationsPopoverIfShown() -> Bool {
-        titlebarAccessoryController.dismissNotificationsPopoverIfShown()
-    }
+    `@discardableResult`
+    func dismissNotificationsPopoverIfShown() -> Bool {
+        if `#available`(macOS 26.0, *) {
+            let window = NSApp.keyWindow ?? NSApp.mainWindow
+            guard let w = window, let windowId = mainWindowId(for: w) else { return false }
+            let wasVisible = notificationsPopoverVisibleByWindowId[windowId] == true
+            NotificationCenter.default.post(
+                name: Self.dismissNotificationsPopoverNotification,
+                object: w,
+                userInfo: [Self.notificationsPopoverWindowIdUserInfoKey: windowId]
+            )
+            // Optimistically flip to false; ContentView will confirm via visibilityDidChange.
+            notificationsPopoverVisibleByWindowId[windowId] = false
+            return wasVisible
+        }
+        return titlebarAccessoryController.dismissNotificationsPopoverIfShown()
+    }
@@
-    func isNotificationsPopoverShown() -> Bool {
-        titlebarAccessoryController.isNotificationsPopoverShown()
-    }
+    func isNotificationsPopoverShown() -> Bool {
+        if `#available`(macOS 26.0, *) {
+            let window = NSApp.keyWindow ?? NSApp.mainWindow
+            guard let w = window, let windowId = mainWindowId(for: w) else { return false }
+            return notificationsPopoverVisibleByWindowId[windowId] == true
+        }
+        return titlebarAccessoryController.isNotificationsPopoverShown()
+    }
@@
     private func unregisterMainWindow(_ window: NSWindow) {
         …
-        commandPaletteSnapshotByWindowId.removeValue(forKey: removed.windowId)
+        commandPaletteSnapshotByWindowId.removeValue(forKey: removed.windowId)
+        notificationsPopoverVisibleByWindowId.removeValue(forKey: removed.windowId)
         …
     }

ContentView.swift (SwiftUI presenter)

  • Observe the new show/dismiss notifications and publish visibility changes back to AppDelegate. Assuming ContentView already has windowId: UUID and @State var isNotificationsPopoverPresented.
// In onAppear/onReceive setup for notifications presenter on macOS 26:
if `#available`(macOS 26.0, *) {
    let center = NotificationCenter.default
    notificationObservers.append(
        center.addObserver(forName: AppDelegate.toggleNotificationsPopoverNotification, object: nil, queue: .main) { note in
            guard let targetId = (note.userInfo?[AppDelegate.notificationsPopoverWindowIdUserInfoKey] as? UUID),
                  targetId == self.windowId else { return }
            isNotificationsPopoverPresented.toggle()
        }
    )
    notificationObservers.append(
        center.addObserver(forName: AppDelegate.showNotificationsPopoverNotification, object: nil, queue: .main) { note in
            guard let targetId = (note.userInfo?[AppDelegate.notificationsPopoverWindowIdUserInfoKey] as? UUID),
                  targetId == self.windowId else { return }
            isNotificationsPopoverPresented = true
        }
    )
    notificationObservers.append(
        center.addObserver(forName: AppDelegate.dismissNotificationsPopoverNotification, object: nil, queue: .main) { note in
            guard let targetId = (note.userInfo?[AppDelegate.notificationsPopoverWindowIdUserInfoKey] as? UUID),
                  targetId == self.windowId else { return }
            isNotificationsPopoverPresented = false
        }
    )
}

// Whenever `@State` changes (e.g., .popover presenter or onChange):
.onChange(of: isNotificationsPopoverPresented) { newValue in
    if `#available`(macOS 26.0, *) {
        NotificationCenter.default.post(
            name: AppDelegate.notificationsPopoverVisibilityDidChangeNotification,
            object: nil,
            userInfo: [
                AppDelegate.notificationsPopoverWindowIdUserInfoKey: windowId,
                AppDelegate.notificationsPopoverVisibleUserInfoKey: newValue
            ]
        )
    }
}

Result

  • All four entry points (toggle, show-from-menubar, dismiss, isShown) consistently target the active window’s SwiftUI-presented popover on macOS 26; the legacy titlebar accessory remains in use only for < macOS 26.
  • AppDelegate can now reliably dismiss and report visibility without depending on the accessory controller.

Would you like me to push a follow-up commit on this PR with the above changes, or open a tracking issue and land it separately?


✏️ Learnings added
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/AppDelegate.swift:10168-10189
Timestamp: 2026-04-14T20:02:38.217Z
Learning: Repo manaflow-ai/cmux — On macOS 26, the notifications popover is owned by ContentView (State isNotificationsPopoverPresented). AppDelegate APIs (toggle/show/dismiss/isShown) must route via NotificationCenter with a window-scoped userInfo["windowId"] and track visibility per-window, not call UpdateTitlebarAccessoryController. ContentView should observe show/dismiss/toggle and post a visibilityDidChange notification {windowId, visible}.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: wonbywondev
Repo: manaflow-ai/cmux PR: 1795
File: Sources/ContentView.swift:1392-1395
Timestamp: 2026-03-19T09:21:24.369Z
Learning: Repo manaflow-ai/cmux — Notifications UI: Production notifications are shown via NSPopover (toggleNotificationsPopover in Sources/Update/UpdateTitlebarAccessory.swift). The SidebarSelectionState.selection == .notifications path in Sources/ContentView.swift is test/scaffolding-only (set in AppDelegate for tests) and isn’t used by user actions; .tabs remains active in normal flows. Therefore, folder-drop handling doesn’t need to flip selection.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2735
File: Sources/AppDelegate.swift:476-483
Timestamp: 2026-04-09T00:29:20.810Z
Learning: Repo: manaflow-ai/cmux — File: Sources/AppDelegate.swift — Deprecated API handling pattern: wrap NSWorkspace.shared.fullPath(forApplication:) in a private helper (_legacyFullPath(forApplication:)) to centralize usage and intentionally accept the single deprecation warning at that call site, since there is no non-deprecated name-based replacement. Avoid marking the wrapper itself as deprecated to prevent propagating warnings to every wrapper use.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2735
File: Sources/AppDelegate.swift:476-483
Timestamp: 2026-04-09T00:29:06.142Z
Learning: Repo: manaflow-ai/cmux — File: Sources/AppDelegate.swift — The helper `_legacyFullPath(forApplication:)` intentionally remains annotated `available(macOS, deprecated: 11.0)` and calls `NSWorkspace.shared.fullPath(forApplication:)`. Team preference: keep a single, localized deprecation warning at its (single) call site since no non‑deprecated name-based replacement exists. Do not “fix” this by removing the deprecation or silencing the warning in future PRs.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:3586-3604
Timestamp: 2026-04-14T20:00:18.091Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — On macOS 26+, the app intentionally keeps NSWindow.StyleMask.fullSizeContentView enabled even with the native (non-transparent) system titlebar. The titlebar handles dragging natively; isMovable=true and isMovableByWindowBackground=false are used. Any comment suggesting “without fullSizeContentView” is outdated.

Learnt from: gaelic-ghost
Repo: manaflow-ai/cmux PR: 1771
File: Sources/AppDelegate.swift:9155-9171
Timestamp: 2026-03-23T03:31:05.252Z
Learning: Repo: manaflow-ai/cmux — Sources/AppDelegate.swift — In AppDelegate.handleCustomShortcut(event:), if hasEventWindowContext is true and synchronizeShortcutRoutingContext(event:) fails, only allow auxiliary windows to bypass routing for close-window shortcuts. Use shouldAllowAuxiliaryWindowShortcutWithoutMainRoute(event:eventTargetWindow:) (which calls isAuxiliaryWindowCloseShortcut) to permit Cmd+W/close-window only; for all other shortcuts, return false to avoid mutating the wrong main-window tabManager.

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T02:19:07.331Z
Learning: Applies to **/*.swift : Use the Debug menu (macOS menu bar, DEBUG builds only) for visual iteration. To add a debug toggle or visual option: create an `NSWindowController` subclass with a `shared` singleton, add it to the 'Debug Windows' menu in `Sources/cmuxApp.swift`, and add a SwiftUI view with `AppStorage` bindings for live changes

Learnt from: Horacehxw
Repo: manaflow-ai/cmux PR: 1980
File: Sources/AppDelegate.swift:2454-2458
Timestamp: 2026-04-06T09:33:40.789Z
Learning: Repo: manaflow-ai/cmux — File: Sources/AppDelegate.swift — In activateUITestAppIfNeeded() for macOS 14+, prefer NSRunningApplication.current.activate(options: [.activateAllWindows]) without .activateIgnoringOtherApps; the deprecated option is only used on older macOS. A future follow-up may migrate to the modern activation API.

Learnt from: tayl0r
Repo: manaflow-ai/cmux PR: 1909
File: Sources/AppDelegate.swift:1955-1970
Timestamp: 2026-03-21T07:13:41.796Z
Learning: Repo: manaflow-ai/cmux — AppDelegate.registerMainWindow ownership pattern: the primary window registers from ContentView.onAppear and passes the SwiftUI-owned FileBrowserDrawerState; secondary windows created via AppDelegate.createMainWindow construct and pass their own FileBrowserDrawerState. This mirrors SidebarState and avoids re-registration mismatches.

Learnt from: Horacehxw
Repo: manaflow-ai/cmux PR: 2043
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-04-06T09:32:26.967Z
Learning: Repo: manaflow-ai/cmux — File: Sources/AppDelegate.swift — UI test fallback window pattern: during XCTest launch stabilization, the app may force-create a fallback main window when NSApp.windows is empty. The window is tracked via `private weak var fallbackUITestWindow: NSWindow?` and, upon the first real WindowGroup registration in `registerMainWindow(...)`, if a second main window exists and the registering window differs, the fallback is closed and the reference cleared to avoid two main windows during tests.

Learnt from: EtanHey
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-17T15:14:58.452Z
Learning: Repo: manaflow-ai/cmux — Sources/TerminalNotificationStore.swift — The `asyncAfter` protection for `Published` `authorizationState` mutations is only required for init-time and passive query callbacks (e.g., `getNotificationSettings` callback in `refreshAuthorizationStatus`) that can fire during the window setup constraint pass and trigger an NSGenericException. User-action-triggered mutation sites (`ensureAuthorization` ~line 1084 and `requestAuthorization` ~line 1140) are safe to use plain `DispatchQueue.main.async` because AppKit window constraints are fully converged by the time any user interaction begins.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/AppDelegate.swift:2196-2200
Timestamp: 2026-04-03T03:36:45.112Z
Learning: Repo: manaflow-ai/cmux — In Sources/AppDelegate.swift, when KeyboardShortcutSettings.didChangeNotification fires, AppDelegate must clear configured-chord caches (pendingConfiguredShortcutChord and activeConfiguredShortcutChordPrefixForCurrentEvent) via clearConfiguredShortcutChordState() before refreshing tooltips/UI. Also clear chord state on applicationWillResignActive to avoid cross-activity leakage. Verified by cmuxTests/AppDelegateShortcutRoutingTests.swift::testShortcutChangeClearsPendingConfiguredChord.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/Update/UpdateTitlebarAccessory.swift:924-926
Timestamp: 2026-03-04T14:04:40.577Z
Learning: In manaflow-ai/cmux (Swift/SwiftUI macOS app), the notifications empty-state localization keys are intentionally different across two views:
- `NotificationsPage.swift` uses key `"notifications.empty.description"` with text "Desktop notifications will appear here for quick review." (full-page view).
- `UpdateTitlebarAccessory.swift` uses key `"notifications.empty.subtitle"` with text "Desktop notifications will appear here." (space-constrained titlebar popover).
Both keys have correct Japanese translations in the .xcstrings catalog. The difference in key names and text is by design, not an inconsistency.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/ContentView.swift:3194-3196
Timestamp: 2026-03-04T14:05:48.668Z
Learning: In manaflow-ai/cmux (PR `#819`), Sources/ContentView.swift: The command palette’s external window labels intentionally use the global window index from the full orderedSummaries (index + 1), matching the Window menu in AppDelegate. Do not reindex after filtering out the current window to avoid mismatches (“Window 2” for an external window is expected).

Learnt from: MaTriXy
Repo: manaflow-ai/cmux PR: 1460
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-03-16T08:02:06.824Z
Learning: In Swift sources, for any panel_id-only route handling in v2PanelMarkBackground(params:) and v2PanelMarkForeground(params:), first attempt v2ResolveTabManager(params:). Use the manager only if it actually owns the panelId; otherwise fall back to AppDelegate.shared?.locateSurface(surfaceId:) to locate the correct TabManager across windows. Apply this pattern to all panel_id-only routes to avoid active-window bias.

Learnt from: tayl0r
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-08T03:36:30.160Z
Learning: Repo: manaflow-ai/cmux — AppDelegate.FileBrowserDrawerState threading pattern (PR `#1909`, commit e0e57809): FileBrowserDrawerState must be threaded through AppDelegate.configure() as a weak stored property (matching the sidebarState pattern), passed through both configure() call sites, with registerMainWindow parameter made non-optional. The fallback `?? FileBrowserDrawerState()` must NOT be used as it creates detached instances that are not properly owned by the window context.

Learnt from: SuperManfred
Repo: manaflow-ai/cmux PR: 1150
File: Sources/AppDelegate.swift:7394-7401
Timestamp: 2026-03-10T10:24:14.017Z
Learning: Repo: manaflow-ai/cmux — For Cmd+W behavior with browser popups: BrowserPopupPanel.performKeyEquivalent is the primary handler to close popups. AppDelegate.handleCustomShortcut(_:), in its Cmd+W fallback, must check both NSApp.keyWindow and event.window for identifier "cmux.browser-popup" and close it if found, before routing to workspace/settings close logic.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2889-2911
Timestamp: 2026-04-14T19:59:52.026Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — On macOS 26, the NavigationSplitView sidebar width is synchronized back to model state via a GeometryReader that updates sidebarWidth and SidebarState.persistedWidth on width changes. Do not flag “write-only sidebar width” drift for this path going forward.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 2505
File: Sources/AppDelegate.swift:5636-5642
Timestamp: 2026-04-06T07:18:38.685Z
Learning: In the AppDelegate.swift keyDown focus-repair path (and related surface keyboard-focus matching like GhosttySurfaceScrollView.swift), do not dereference NSTextView.delegate when it is unsafe-unretained. Instead, resolve the field-editor ownership via cmuxFieldEditorOwnerView(_). For keyboard-focus matching, prefer superview/nextResponder traversal or hostedView.responderMatchesPreferredKeyboardFocus(...), and ensure any matches are performed using the owned responder chain rather than reading an unsafe delegate pointer.

Learnt from: johnhanks1
Repo: manaflow-ai/cmux PR: 1556
File: Sources/AppDelegate.swift:7878-7888
Timestamp: 2026-03-17T01:11:15.590Z
Learning: Repo: manaflow-ai/cmux — Sources/AppDelegate.swift — In AppDelegate.handleCustomShortcut(event:), when detecting an external keyboard input-source change (via KeyboardLayout.id), the skip branch must not update lastKeyboardInputSourceId; it should only log and return false. Update lastKeyboardInputSourceId only after passing the guard into normal shortcut handling. Rationale: prevents same-event re-entry (e.g., via handleBrowserSurfaceKeyEquivalent from NSWindow.performKeyEquivalent) from bypassing the skip and inadvertently processing app shortcuts.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2124
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-03-25T08:05:26.034Z
Learning: Repo: manaflow-ai/cmux — File: Sources/AppDelegate.swift — Pattern for “New Window” geometry seed:
In AppDelegate.createMainWindow(initialWorkingDirectory:sessionWindowSnapshot:), compute the existingFrame using preferredMainWindowContextForWorkspaceCreation(debugSource: "createMainWindow.initialGeometry") and then resolvedWindow(for:) rather than relying on NSApp.keyWindow or the first registered mainWindowContext. Rationale: ensures the new window inherits size from the intended main-terminal window even when an auxiliary window is key, and keeps behavior consistent with showOpenFolderPanel().

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2884
File: Sources/AppIconDockTilePlugin.swift:16-19
Timestamp: 2026-04-14T08:50:27.729Z
Learning: Repo: manaflow-ai/cmux — Sources/AppIconDockTilePlugin.swift — Two-layer icon persistence contract (PR `#2884`):
- App process (Sources/cmuxApp.swift): AppIconMode.automatic returns nil for imageName to avoid writing custom icons to the bundle from the app side (runtime-only swaps).
- Dock plugin (Sources/AppIconDockTilePlugin.swift): DockTileAppIconMode.automatic intentionally returns a concrete NSImage.Name (AppIconDark or AppIconLight) based on the current effective appearance, so the plugin persists an appearance-matched icon to the bundle. This ensures the Dock shows the correct icon after force-kill/quit instead of falling back to the static bundle asset.
- Do NOT flag DockTileAppIconMode.automatic returning a concrete image name as a bug; it is the intentional Dock-side persistence path. Only when the user explicitly selects light or dark mode is the plugin's behavior identical to cmuxApp.swift. The .automatic branch must NOT return nil from the plugin.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-3021
Timestamp: 2026-04-14T20:00:12.468Z
Learning: Repo: manaflow-ai/cmux — Accessibility convention: For toolbar actions in macOS 26+ SwiftUI .toolbar (Sources/ContentView.swift), use localized .accessibilityLabel (and identifiers) for VoiceOver; AppKit NSToolbarItems in Sources/WindowToolbarController.swift set localized label/toolTip/accessibilityDescription. Label(...).labelStyle(.iconOnly) is not required here as VO picks up the localized accessibilityLabel.

Learnt from: lucasward
Repo: manaflow-ai/cmux PR: 1903
File: Sources/ContentView.swift:13069-13113
Timestamp: 2026-03-21T06:23:38.764Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, DraggableFolderNSView.updateIcon() intentionally applies the "sidebarMonochromeIcons" setting only on view creation (no live observer). Do not add defaults observers/AppStorage for live refresh in feature-scoped PRs; a live-refresh can be considered in a separate follow-up.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2525
File: Sources/GhosttyTerminalView.swift:481-513
Timestamp: 2026-04-02T10:13:39.235Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, terminal file-link resolution trims trailing unmatched closing delimiters “) ] } >” only when they are dangling (more closers than openers), preserving wrapped tokens like “(file:///tmp/a.png)”. Implemented via terminalFileLinkTrailingClosingDelimiters and count comparison inside trimTrailingTerminalFileLinkPunctuation(_:) and exercised by a regression test (PR `#2525`, commit 3f5c5b6d).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2575
File: Sources/cmuxApp.swift:0-0
Timestamp: 2026-04-06T09:01:51.979Z
Learning: Repo: manaflow-ai/cmux — File: Sources/cmuxApp.swift — SettingsView now uses SwiftUI `.searchable(text:placement:prompt:)` for the sidebar search, which provides a native (accessible) clear button. Do not flag missing accessibility on a custom clear button in this view going forward.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-03-04T14:05:42.574Z
Learning: Guideline: In Swift files (cmux project), when handling pluralized strings, prefer using localization keys with the ICU-style plural forms .one and .other. For example, use keys like statusMenu.unreadCount.one for the singular case (1) and statusMenu.unreadCount.other for all other counts, and similarly for statusMenu.tooltip.unread.one/other. Rationale: ensures correct pluralization across locales and makes localization keys explicit. Review code to ensure any unread count strings and related tooltips follow this .one/.other key pattern and verify the correct value is chosen based on the count.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/AppDelegate.swift:3491-3493
Timestamp: 2026-03-04T14:06:12.296Z
Learning: In PRs affecting this repository, limit the scope of localization (i18n) changes to Japanese translations for the file Sources/AppDelegate.swift. Do not include UX enhancements (e.g., preferring workspace.customTitle in workspaceDisplayName() or altering move-target labels) in this PR. Open a separate follow-up issue to address any UX-related changes to avoid scope creep and keep localization review focused.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 954
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-03-05T22:04:34.712Z
Learning: Adopt the convention: for health/telemetry tri-state values in Swift, prefer Optionals (Bool?) over sentinel booleans. In TerminalController.swift, socketConnectable is Bool? and only set when socketProbePerformed is true; downstream logic must treat nil as 'not probed'. Ensure downstream code checks for nil before using a value and uses explicit non-nil checks to determine state, improving clarity and avoiding misinterpretation of default false.

Learnt from: moyashin63
Repo: manaflow-ai/cmux PR: 1074
File: Sources/AppDelegate.swift:7523-7545
Timestamp: 2026-03-09T01:38:24.337Z
Learning: When the command palette is visible (as in manaflow-ai/cmux Sources/AppDelegate.swift), ensure the shortcut handling consumes most Command shortcuts to protect the palette's text input. Specifically, do not allow UI zoom shortcuts (Cmd+Shift+= / Cmd+Shift+− / Cmd+Shift+0) to trigger while the palette is open. Do not reorder shortcut handlers (e.g., uiZoomShortcutAction(...)) to bypass this guard; users must close the palette before performing zoom actions. This guideline should apply to Swift source files handling global shortcuts within the app.

Learnt from: zlatkoc
Repo: manaflow-ai/cmux PR: 1368
File: Sources/Panels/BrowserPanel.swift:69-69
Timestamp: 2026-03-13T13:46:01.733Z
Learning: Do not wrap engine/brand name literals (e.g., displayName values such as Google, DuckDuckGo, Bing, Kagi, Startpage) in String(localized: ...). These are brand/product names that are not translatable UI text. Localization should apply to generic UI strings (labels, buttons, error messages, etc.). Apply this guideline across Swift source files under Sources/ (notably in BrowserPanel.swift and similar UI/engine-related strings) and flag only brand-name strings that are part of user-facing UI text appropriately for translation scope.

Learnt from: jt-hsiao
Repo: manaflow-ai/cmux PR: 1423
File: Sources/AppDelegate.swift:11220-11226
Timestamp: 2026-03-14T07:05:52.379Z
Learning: In Sources/AppDelegate.swift (within the manaflow-ai/cmux repo), ensure that NSWindow.cmux_performKeyEquivalent forwards to firstResponderWebView.performKeyEquivalent and returns its Bool unconditionally. This prevents re-entry into SwiftUI’s performKeyEquivalent path that can swallow keys when WKWebView has focus, and aligns with the Command-key routing chain implemented in Sources/Panels/CmuxWebView.swift: (1) route to NSApp.mainMenu.performKeyEquivalent when allowed, (2) fall back to AppDelegate.shared?.handleBrowserSurfaceKeyEquivalent(event) for non-menu shortcuts, (3) fall back to super.performKeyEquivalent. Non-Command keys should still call super directly.

Learnt from: jt-hsiao
Repo: manaflow-ai/cmux PR: 1423
File: Sources/AppDelegate.swift:11220-11226
Timestamp: 2026-03-14T07:05:52.379Z
Learning: In Sources/AppDelegate.swift for the manaflow-ai/cmux repository, Cmd+` (command-backtick) should not be routed directly to the main menu. Do not implement a window/main-menu bypass for this key. Tests will assert this behavior, so ensure routing logic respects this exclusion and that there is no path that bypasses the main menu for Cmd+`. If similar key-command bypass considerations exist in other files, apply the same explicit exclusion only where applicable.

Learnt from: kjb0787
Repo: manaflow-ai/cmux PR: 1461
File: Sources/GhosttyTerminalView.swift:5904-5905
Timestamp: 2026-03-15T19:22:32.330Z
Learning: In Swift files under the Sources directory that manage terminal/scroll behavior, ensure the following: when preserving scroll across workspace switches, save savedScrollRow only if the scrollbar offset is greater than 0 (indicating the user has scrolled up). On restore, call scroll_to_row only if savedScrollRow is non-nil; if it is nil, rely on synchronizeScrollView() to keep bottom-pinned sessions following new output. This pattern should be applied wherever GhosttyTerminalView-like views implement setVisibleInUI(_:) to maintain consistent user scroll state across workspace switches.

Learnt from: pratikpakhale
Repo: manaflow-ai/cmux PR: 2011
File: Resources/Localizable.xcstrings:15256-15368
Timestamp: 2026-03-23T21:39:50.795Z
Learning: When reviewing this repo’s Swift localization usage, do not flag missing `String.localizedStringWithFormat` for calls that use the modern overload `String(localized: "key", defaultValue: "...\(variable)")` (where `defaultValue` is a `String.LocalizationValue` built with `\(…)`). That overload natively supports interpolation and the xcstrings/runtime substitution handles the resulting placeholders automatically. Only require `String.localizedStringWithFormat` when using the older `String(localized:)` overload that takes a plain `String` (i.e., where format arguments must be passed separately), such as for keys like `clipboard.sshError.single`.

Learnt from: thunter009
Repo: manaflow-ai/cmux PR: 1825
File: Sources/TerminalController.swift:3620-3622
Timestamp: 2026-03-25T00:32:54.735Z
Learning: When validating or reporting workspace/tab colors in this repo, only accept and use 6-digit hex colors in the form `#RRGGBB` (no alpha, i.e., do not allow `#RRGGBBAA`). Ensure validation logic matches the existing behavior (e.g., WorkspaceTabColorSettings.normalizedHex(...) and TabManager.setTabColor(tabId:color:) as well as CLI/cmux.swift). Update any error/help text for workspace color to reference only `#RRGGBB` (not `#RRGGBBAA`).

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/GhosttyTerminalView.swift:3891-3903
Timestamp: 2026-04-02T21:37:21.463Z
Learning: In Swift source files like Sources/GhosttyTerminalView.swift, avoid logging raw startup commands or initialInput even in DEBUG (to prevent leaking sensitive paths/tokens and multiline content). If you need to diagnose startup/input, log only non-sensitive metadata such as (1) presence flags (e.g., hasStartupCommand/hasInitialInput), (2) byte counts, and (3) the relevant surface id (so issues can be correlated without exposing the underlying strings).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/cmuxApp.swift:6439-6444
Timestamp: 2026-04-03T03:35:54.082Z
Learning: In this repo’s keyboard shortcut implementation, ensure `KeyboardShortcutSettings.setShortcut(...)` does nothing (no-op) when `KeyboardShortcutSettings.isManagedBySettingsFile(action)` returns `true` (i.e., the shortcut is managed via `settings.json`). This prevents writing back overrides into `UserDefaults` and keeps `settings.json` as the source of truth.

}

Expand Down
Loading