Adopt macOS 26 Liquid Glass design#2647
Adopt macOS 26 Liquid Glass design#2647rodchristiansen wants to merge 10 commits intomanaflow-ai:mainfrom
Conversation
|
@rodchristiansen is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
This review could not be run because your cubic account has exceeded the monthly review limit. If you need help restoring access, please contact contact@cubic.dev. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughmacOS 26+ gated UI and window behavior changes: safe-area forwarding, NavigationSplitView sidebar, new toolbar items and notification popover path, titlebar accessory attachment gated, terminal scroll/layout and hosted-layer corner rounding adjusted, bonsplit submodule bumped, and AppIcon added to project resources. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Toolbar as NSToolbar
participant AppDelegate
participant Content as ContentView
participant Accessory as TitlebarAccessory
User->>Toolbar: Click "Notifications" item
Toolbar->>AppDelegate: toolbar action toggleNotificationsPopover
alt macOS >= 26
AppDelegate->>AppDelegate: post Notification.Name("cmux.toggleNotificationsPopover")
AppDelegate-->>Content: Notification delivered
Content->>Content: set isNotificationsPopoverPresented = true
Content->>User: present SwiftUI popover
else macOS < 26
AppDelegate->>Accessory: titlebarAccessoryController.toggleNotificationsPopover(animated:)
Accessory->>User: toggle accessory popover
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adopts macOS 26 Liquid Glass by switching to
Confidence Score: 4/5Not safe to merge — two P1 regressions ship broken functionality to macOS 26 users. The bell-button no-op and unconditional tab-bar hiding are both present defects on the changed code path requiring fixes before merge. Sources/AppDelegate.swift (broken notifications popover) and Sources/Workspace.swift (unconditional tab bar hide) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[macOS 26 toolbar bell click] --> B[AppDelegate.toggleNotificationsPopover]
B --> C[titlebarAccessoryController.toggleNotificationsPopover]
C --> D{controlsControllers.isEmpty?}
D -- yes macOS 26 --> E[return NO-OP]
D -- no pre-26 --> F[show popover]
G[App launch macOS 26] --> H[skip titlebarAccessoryController.start]
H --> J[controlsControllers stays empty]
K[Window created macOS 26] --> L[attachUpdateAccessory]
L --> M{macOS 26?}
M -- yes --> N[return early no start/attach]
M -- no --> O[start + attach controller]
Reviews (1): Last reviewed commit: "Adopt macOS 26 Liquid Glass design" | Re-trigger Greptile |
| func attachUpdateAccessory(to window: NSWindow) { | ||
| if #available(macOS 26.0, *) { | ||
| // On macOS 26, toolbar buttons are native SwiftUI .toolbar items | ||
| // in the NavigationSplitView. Skip the old titlebar accessory controllers. | ||
| return | ||
| } | ||
| titlebarAccessoryController.start() | ||
| titlebarAccessoryController.attach(to: window) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| let hideTabBar: Bool | ||
| if #available(macOS 26.0, *) { | ||
| hideTabBar = true | ||
| } else { | ||
| hideTabBar = false | ||
| } | ||
| return BonsplitConfiguration.Appearance( | ||
| tabBarHeight: hideTabBar ? 0 : 33, |
There was a problem hiding this comment.
Tab bar unconditionally hidden for all panes, not just single-tab panes
Setting hideTabBar = true on macOS 26 sets tabBarHeight = 0 for every pane, regardless of how many tabs it contains. The PR description says "single-tab panes," but no tab count is checked here. In a split workspace with multiple tabs per pane, all in-pane tab switching UI disappears on macOS 26.
If the goal is to hide the bar only for single-tab panes, the appearance must be updated reactively based on bonsplitController.tabs(inPane:).count rather than as a static launch-time configuration.
There was a problem hiding this comment.
Resolved upstream by companion PR manaflow-ai/bonsplit#95 ("Hide tab bar when tabBarHeight is 0, show only when 2+ tabs"). Once that merges and the bonsplit submodule pointer bumps, tabBarHeight = 0 only hides the chrome on single-tab panes; multi-tab panes still show the bar.
| NavigationSplitView(columnVisibility: Binding( | ||
| get: { sidebarState.isVisible ? .all : .detailOnly }, | ||
| set: { newValue in | ||
| let shouldShow = (newValue != .detailOnly) | ||
| if shouldShow != sidebarState.isVisible { | ||
| _ = sidebarState.toggle() | ||
| } | ||
| } | ||
| )) { | ||
| sidebarView | ||
| .navigationSplitViewColumnWidth(min: 120, ideal: sidebarWidth, max: 400) | ||
| } detail: { | ||
| terminalContentWithSidebarDropOverlay | ||
| .padding(8) | ||
| } | ||
| .navigationSplitViewStyle(.prominentDetail) | ||
| .background(SplitViewDividerHider()) | ||
| .toolbar { | ||
| ToolbarItemGroup(placement: .primaryAction) { | ||
| Button { | ||
| _ = AppDelegate.shared?.toggleNotificationsPopover(animated: true) | ||
| } label: { | ||
| Image(systemName: "bell") | ||
| } | ||
| .buttonStyle(.accessoryBarAction) | ||
| .accessibilityIdentifier("toolbar.notifications") | ||
|
|
||
| Button { | ||
| if let appDelegate = AppDelegate.shared { | ||
| if appDelegate.addWorkspaceInPreferredMainWindow(debugSource: "toolbar.newTab") == nil { | ||
| appDelegate.openNewMainWindow(nil) | ||
| } | ||
| } | ||
| } label: { | ||
| Image(systemName: "plus") | ||
| } | ||
| .buttonStyle(.accessoryBarAction) | ||
| .accessibilityIdentifier("toolbar.newTab") | ||
| } | ||
| } | ||
| ) |
There was a problem hiding this comment.
Sidebar width not persisted when user drags the NavigationSplitView divider
navigationSplitViewColumnWidth(min: 120, ideal: sidebarWidth, max: 400) provides an initial hint, but drag events from the native divider are not fed back into sidebarWidth or sidebarState.persistedWidth. Every sidebar toggle or app restart snaps back to the last value written by the old custom-resizer path, ignoring any width the user set via the system divider on macOS 26.
There was a problem hiding this comment.
Already addressed — ContentView.swift:2900-2909 wraps the sidebar column in a GeometryReader that writes back to both sidebarWidth and sidebarState.persistedWidth whenever the rendered width diverges by more than 1pt, so native divider drags persist.
| let contentInset: CGFloat | ||
| if #available(macOS 26.0, *) { | ||
| contentInset = 6 | ||
| } else { | ||
| contentInset = 0 | ||
| } | ||
| let scrollFrame = contentInset > 0 ? bounds.insetBy(dx: contentInset, dy: contentInset) : bounds | ||
| _ = setFrameIfNeeded(scrollView, to: scrollFrame) |
There was a problem hiding this comment.
Symmetric inset shrinks right/bottom unnecessarily
bounds.insetBy(dx: contentInset, dy: contentInset) applies the 6 pt padding on all four sides. Only the leading corners (top-left, bottom-left) are rounded; the right and bottom edges are flat. The right-side and bottom 6 pt waste terminal real estate with no visual justification. Consider applying inset only on the leading and top edges where rounding occurs.
There was a problem hiding this comment.
Not symmetric — the code uses x: origin.x + inset, y: origin.y, width: W - inset, height: H - inset, which insets the leading edge (x+6) and the top edge (height-6 with unchanged y in AppKit's bottom-left origin). Right and bottom are unchanged. Comment and behavior match.
| if #available(macOS 26.0, *) { | ||
| let hasSidebarToLeft = targetFrame.origin.x > 20 | ||
| let desiredRadius: CGFloat = hasSidebarToLeft ? 16 : 0 | ||
| if hostedView.layer?.cornerRadius != desiredRadius { | ||
| hostedView.layer?.cornerRadius = desiredRadius | ||
| hostedView.layer?.maskedCorners = hasSidebarToLeft | ||
| ? [.layerMinXMinYCorner, .layerMinXMaxYCorner] | ||
| : [] | ||
| } | ||
| } |
There was a problem hiding this comment.
Magic-number heuristic for sidebar detection is fragile
let hasSidebarToLeft = targetFrame.origin.x > 20 infers sidebar presence from the terminal frame's x-origin. A window positioned near x = 0 with no sidebar, or a sidebar narrower than 20 pt during animation, would produce incorrect rounding. Deriving this directly from sidebarState.isVisible would be more robust and self-documenting.
There was a problem hiding this comment.
Deferring — the inline comment captures the tradeoff: this AppKit view can't reach SidebarState directly, and sidebar minimum width is 120pt so x > 20 is robust for the current layout. If we refactor to bridge SidebarState into the portal layer we'll tighten this.
There was a problem hiding this comment.
Pull request overview
Updates the macOS UI path to align with macOS 26 “Liquid Glass” conventions by switching to system-native sidebar/titlebar behaviors and adjusting terminal spacing/clipping to match the new visual language.
Changes:
- Adds a macOS 26-specific
NavigationSplitViewlayout with SwiftUI toolbar actions and updated window/titlebar configuration. - Adjusts terminal presentation (external padding, internal content inset, leading-corner rounding) for Liquid Glass spacing/shape.
- Skips legacy titlebar accessory / custom glass paths and icon override logic on macOS 26; hides Bonsplit tab bar on macOS 26.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| Sources/Workspace.swift | macOS 26-specific Bonsplit appearance change to hide tab bar. |
| Sources/WindowToolbarController.swift | Adds macOS 26 toolbar style + additional toolbar items/actions. |
| Sources/TerminalWindowPortal.swift | Adds macOS 26 leading-corner rounding logic for hosted terminal views. |
| Sources/GhosttyTerminalView.swift | Insets terminal scroll view on macOS 26 to create internal safe padding. |
| Sources/ContentView.swift | Introduces macOS 26 NavigationSplitView path, toolbar buttons, window/titlebar behavior changes, sidebar background changes, and new glass-effect modifiers/utilities. |
| Sources/cmuxApp.swift | Refactors main window content builder; skips custom app icon override behavior on macOS 26. |
| Sources/AppDelegate.swift | Enables system safe area on macOS 26 and skips legacy titlebar accessory startup/attachment on macOS 26. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .padding(.top, effectiveTitlebarPadding) | ||
| .padding(.top, { | ||
| if #available(macOS 26.0, *) { | ||
| return 0 // Native glass titlebar handles spacing via safe area |
There was a problem hiding this comment.
On macOS 26 this sets terminalContent top padding to 0 assuming the system safe area will keep content below the titlebar, but ContentView later applies .ignoresSafeArea() to the root view. With safe areas ignored, the terminal content can end up under the titlebar and reintroduce the drag/interaction issues this padding previously avoided. Consider making the safe-area ignoring conditional (e.g., only for the sidebar/background) or keep an appropriate top inset for the detail content on macOS 26.
| return 0 // Native glass titlebar handles spacing via safe area | |
| // Keep the terminal content below the titlebar even if an ancestor view | |
| // ignores safe areas; relying on the system safe area here can place | |
| // interactive content under the titlebar. | |
| return effectiveTitlebarPadding |
There was a problem hiding this comment.
On macOS 26 the sidebar sits inside NavigationSplitView, which supplies safe-area insets to its columns; the root-level .ignoresSafeArea() only applies to the custom-titlebar overlay branch, not to the NavigationSplitView path. Not reproducing text under the titlebar during 26 testing, but will keep an eye on it.
| Image(systemName: "bell") | ||
| } | ||
| .buttonStyle(.accessoryBarAction) | ||
| .accessibilityIdentifier("toolbar.notifications") | ||
|
|
||
| Button { | ||
| if let appDelegate = AppDelegate.shared { | ||
| if appDelegate.addWorkspaceInPreferredMainWindow(debugSource: "toolbar.newTab") == nil { | ||
| appDelegate.openNewMainWindow(nil) | ||
| } | ||
| } | ||
| } label: { | ||
| Image(systemName: "plus") | ||
| } | ||
| .buttonStyle(.accessoryBarAction) | ||
| .accessibilityIdentifier("toolbar.newTab") |
There was a problem hiding this comment.
.buttonStyle(.accessoryBarAction) is a macOS 26 SwiftUI API; wrapping it only in if #available(macOS 26.0, *) is not sufficient for builds using older SDKs/toolchains (this repo’s Package.swift is swift-tools-version 5.9). To preserve backward build compatibility, gate these calls with a compiler/SDK availability check (similar to the #if compiler(>=6.2) guards used elsewhere) or provide a fallback button style.
There was a problem hiding this comment.
Deferring — cmux's Xcode toolchain targets the macOS 26 SDK, so #available(macOS 26.0, *) is sufficient at both runtime and build time. #if compiler(>=6.2) would only matter if we supported builds on older Xcodes, which we don't currently.
| // Override the divider color to clear | ||
| splitView.setValue(NSColor.clear, forKey: "dividerColor") |
There was a problem hiding this comment.
splitView.setValue(NSColor.clear, forKey: "dividerColor") relies on KVC to mutate an AppKit property and can raise an Objective‑C exception at runtime if the key isn’t settable on some OS versions. Prefer a supported API (e.g., setting a public dividerColor if writable) or guard the KVC path with a selector/responds(to:) check and fail safely.
| // Override the divider color to clear | |
| splitView.setValue(NSColor.clear, forKey: "dividerColor") | |
| // Override the divider color to clear when the Objective-C setter exists. | |
| let setDividerColorSelector = NSSelectorFromString("setDividerColor:") | |
| if splitView.responds(to: setDividerColorSelector) { | |
| splitView.perform(setDividerColorSelector, with: NSColor.clear) | |
| } |
There was a problem hiding this comment.
Already addressed — the call uses responds(to:) guarded perform(_:with:), not setValue(_:forKey:) (ContentView.swift:15607-15610). No ObjC exception possible if the selector isn't there.
| /// Finds NSSplitView(s) inside NavigationSplitView and hides dividers | ||
| /// by installing a custom delegate that draws nothing. | ||
| @available(macOS 26.0, *) | ||
| private struct SplitViewDividerHider: NSViewRepresentable { | ||
| func makeNSView(context: Context) -> SplitViewDividerHiderView { | ||
| SplitViewDividerHiderView() | ||
| } | ||
|
|
||
| func updateNSView(_ nsView: SplitViewDividerHiderView, context: Context) { | ||
| nsView.scheduleHide() | ||
| } | ||
| } | ||
|
|
||
| @available(macOS 26.0, *) | ||
| final class SplitViewDividerHiderView: NSView { | ||
| private var installed = false | ||
|
|
There was a problem hiding this comment.
The SplitViewDividerHider docs say it hides dividers “by installing a custom delegate”, but the implementation only walks subviews and tweaks properties (no delegate is installed). Also installed is declared but unused, suggesting an intended one-time install that isn’t implemented. Please update the comment to match behavior and either remove installed or use it to prevent repeated recursive patching on every SwiftUI update.
There was a problem hiding this comment.
Fair — will tighten the docstring in a follow-up. The walk-and-patch approach is the one in use; the installed flag and delegate-install language are stale from an earlier iteration.
| if #available(macOS 26.0, *) { | ||
| // On macOS 26 without fullSizeContentView, the system titlebar | ||
| // handles drag natively. | ||
| window.isMovable = true | ||
| window.isMovableByWindowBackground = false | ||
| } else { | ||
| // Keep window immovable; the sidebar's WindowDragHandleView handles | ||
| // drag-to-move via performDrag with temporary movable override. | ||
| // isMovableByWindowBackground=true breaks tab reordering, and | ||
| // isMovable=true blocks clicks on sidebar buttons in minimal mode. | ||
| window.isMovableByWindowBackground = false | ||
| window.isMovable = false | ||
| } | ||
| window.styleMask.insert(.fullSizeContentView) | ||
|
|
There was a problem hiding this comment.
This comment says “On macOS 26 without fullSizeContentView, the system titlebar handles drag natively”, but the code unconditionally inserts .fullSizeContentView right after. Either make the style-mask change conditional for macOS 26 or update the comment, since it currently contradicts the actual behavior and makes debugging titlebar/safe-area interactions harder.
There was a problem hiding this comment.
Will clean up the misleading comment in a follow-up. The code path is correct: .fullSizeContentView stays inserted so macOS 26's native glass titlebar draws in-window, and drag behavior works because we keep titlebarAppearsTransparent aligned with the style.
| /// Fills the background with the terminal's current background color, | ||
| /// staying in sync with theme changes. Used behind NavigationSplitView | ||
| /// so the sidebar's rounded corners blend seamlessly with the window. | ||
| @available(macOS 26.0, *) | ||
| private struct TerminalBackgroundFill: View { | ||
| @State private var bgColor = Color(nsColor: GhosttyBackgroundTheme.currentColor()) | ||
|
|
||
| var body: some View { | ||
| bgColor | ||
| .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in | ||
| bgColor = Color(nsColor: GhosttyBackgroundTheme.currentColor()) | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
TerminalBackgroundFill is documented as “Used behind NavigationSplitView”, but it isn’t referenced anywhere in ContentView (search only finds its definition). If it’s no longer needed, consider removing it; if it is needed to blend the sidebar corners, wire it into the macOS 26 NavigationSplitView branch so the code matches the comment.
| /// Fills the background with the terminal's current background color, | |
| /// staying in sync with theme changes. Used behind NavigationSplitView | |
| /// so the sidebar's rounded corners blend seamlessly with the window. | |
| @available(macOS 26.0, *) | |
| private struct TerminalBackgroundFill: View { | |
| @State private var bgColor = Color(nsColor: GhosttyBackgroundTheme.currentColor()) | |
| var body: some View { | |
| bgColor | |
| .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in | |
| bgColor = Color(nsColor: GhosttyBackgroundTheme.currentColor()) | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Already addressed — TerminalBackgroundFill is removed from the file.
| let hideTabBar: Bool | ||
| if #available(macOS 26.0, *) { | ||
| hideTabBar = true | ||
| } else { | ||
| hideTabBar = false | ||
| } | ||
| return BonsplitConfiguration.Appearance( | ||
| tabBarHeight: hideTabBar ? 0 : 33, | ||
| splitButtonTooltips: Self.currentSplitButtonTooltips(), |
There was a problem hiding this comment.
On macOS 26 hideTabBar is always set to true, which forces tabBarHeight to 0 for all panes. The PR description mentions hiding the tab bar only for single-tab panes; if multi-tab panes should still show a tab bar, this needs conditional logic based on actual tab count/state rather than a blanket OS check.
There was a problem hiding this comment.
Same resolution as greptile's P1 — companion bonsplit PR manaflow-ai/bonsplit#95 makes the tab bar appear only on panes with 2+ tabs.
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (2)
Sources/TerminalWindowPortal.swift (1)
1504-1516: Corner radius updates may animate unexpectedly during geometry sync.The surrounding frame/bounds updates are wrapped in a
CATransactionwith disabled actions (lines 1489–1499), but the corner radius changes are applied after the transaction commits. This could cause implicit Core Animation transitions when toggling sidebar visibility, potentially resulting in visual flicker during rapid split/resize operations.Consider wrapping the corner radius update in its own transaction with disabled actions, or moving it inside the existing transaction block:
🔧 Suggested fix
CATransaction.commit() if geometryChanged { hostedView.reconcileGeometryNow() hostedView.refreshSurfaceNow(reason: "portal.frameChange") } // On macOS 26, round the terminal's leading corners when a sidebar // is visible to its left, matching the NavigationSplitView glass shape. if `#available`(macOS 26.0, *) { let hasSidebarToLeft = targetFrame.origin.x > 20 let desiredRadius: CGFloat = hasSidebarToLeft ? 16 : 0 if hostedView.layer?.cornerRadius != desiredRadius { + CATransaction.begin() + CATransaction.setDisableActions(true) hostedView.layer?.cornerRadius = desiredRadius hostedView.layer?.maskedCorners = hasSidebarToLeft ? [.layerMinXMinYCorner, .layerMinXMaxYCorner] : [] + CATransaction.commit() } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/TerminalWindowPortal.swift` around lines 1504 - 1516, The corner radius and maskedCorners updates on hostedView.layer (the block checking hasSidebarToLeft and setting hostedView.layer?.cornerRadius / hostedView.layer?.maskedCorners) must be performed inside a CATransaction with actions disabled (or moved into the existing CATransaction that updates frame/bounds) to prevent implicit animations during geometry sync; wrap the radius/maskedCorners assignment in a CATransaction.begin()/setDisableActions(true)/commit() pair (or relocate those lines into the existing disabled-action transaction) so changes to cornerRadius and maskedCorners aren’t implicitly animated.Sources/WindowToolbarController.swift (1)
230-236: Remove unnecessary_ =assignments.Both
toggle()andtoggleNotificationsPopover(animated:)returnVoid, so the discard assignments are unnecessary.✨ Suggested cleanup
`@objc` private func toggleSidebarAction() { - _ = AppDelegate.shared?.sidebarState?.toggle() + AppDelegate.shared?.sidebarState?.toggle() } `@objc` private func toggleNotificationsAction() { - _ = AppDelegate.shared?.toggleNotificationsPopover(animated: true) + AppDelegate.shared?.toggleNotificationsPopover(animated: true) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/WindowToolbarController.swift` around lines 230 - 236, The discard assignments are unnecessary: remove the `_ =` when calling AppDelegate.shared?.sidebarState?.toggle() inside toggleSidebarAction() and when calling AppDelegate.shared?.toggleNotificationsPopover(animated: true) inside toggleNotificationsAction() so each method is invoked directly (i.e., call sidebarState?.toggle() and toggleNotificationsPopover(animated: true) without assigning their Void results).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 2601-2605: The notifications popover is never initialized on macOS
26 because titlebarAccessoryController.start() is skipped; call
titlebarAccessoryController.start() for macOS 26 so
UpdateTitlebarAccessoryController populates controlsControllers and the popover
helpers (toggleNotificationsPopover / showNotificationsPopover /
dismissNotificationsPopoverIfShown) work; specifically, remove or modify the
availability check around titlebarAccessoryController.start() in AppDelegate so
start() always runs (or add a macOS26-specific branch that calls start()), and
ensure Start/UpdateTitlebarAccessoryController safely no-ops if
platform-specific internals differ so
WindowToolbarController.toggleNotificationsAction ->
AppDelegate.toggleNotificationsPopover() works on macOS 26.
In `@Sources/ContentView.swift`:
- Around line 2901-2922: The toolbar's image-only Buttons (the bell and plus in
the ToolbarItemGroup) lack localized action names for VoiceOver; replace each
Button's label Image block with a localized Label using String(localized:
"toolbar.notifications", defaultValue: "Notifications") and String(localized:
"toolbar.newTab", defaultValue: "New Tab") (or similar keys), then apply
.labelStyle(.iconOnly) to keep the icon-only appearance; ensure the same
accessibilityIdentifier values ("toolbar.notifications" and "toolbar.newTab")
remain and add corresponding keys to Resources/Localizable.xcstrings with
English and Japanese translations.
- Around line 2884-2895: The native NavigationSplitView configuration only
writes the preferred width into sidebarWidth (via
navigationSplitViewColumnWidth) but never updates
sidebarWidth/sidebarState.persistedWidth when the user drags the native divider,
causing the custom resizer monitor (which reads
sidebarWidth/sidebarState.persistedWidth) to drift; fix this by observing the
actual rendered sidebar width (e.g., with a GeometryReader or .background
preference) inside the sidebarView and, when its width changes beyond a small
threshold, write the new value back into sidebarWidth and
sidebarState.persistedWidth (and persist via sidebarState.save/persist method if
one exists), ensuring updates are debounced/throttled to avoid tight update
loops and only performed when shouldShow/sidebarState.isVisible matches the
split state.
- Around line 15458-15472: The current approach in
hideDividers()/patchSplitViews(in:) uses KVC to set a read-only NSSplitView
property and indiscriminately mutates every split view in the window; instead,
create a focused custom NSSplitView subclass (e.g., TransparentDividerSplitView)
that overrides dividerColor (or drawDivider(in:)) to return/paint clear, then
replace only the specific NavigationSplitView's split view instance with this
subclass (or ensure the NavigationSplitView uses it) rather than recursing from
window.contentView; update hideDividers to target the navigation split view or
its container and remove the setValue(_:forKey:) KVC call and broad traversal.
- Around line 3487-3506: The code sets
window.styleMask.insert(.fullSizeContentView) unconditionally, which contradicts
the macOS 26+ branch that expects the system titlebar to handle dragging without
fullSizeContentView; either make the insertion conditional (only insert
.fullSizeContentView in the else branch for pre-macOS 26) or update the comment
above the macOS 26+ branch to explicitly explain why fullSizeContentView is
still required there; locate the availability checks and the unconditional call
to window.styleMask.insert(.fullSizeContentView) and change it to conditional
insertion tied to the macOS 26 availability or revise the comment near the macOS
26 block to describe the actual layout/drag implications when
.fullSizeContentView is present.
In `@Sources/WindowToolbarController.swift`:
- Around line 193-222: Replace the hard-coded UI strings in the toolbar item
construction with localized strings using String(localized:defaultValue:), e.g.
for items identified by sidebarToggleIdentifier, notificationsIdentifier, and
newTabIdentifier update item.label, item.toolTip, and NSImage(...
accessibilityDescription:) to use String(localized: "TOOLBAR_SIDEBAR_LABEL",
defaultValue: "Sidebar") (and similar keys for tooltip and accessibility text
for each item); ensure the code changes occur in the toolbar item creation block
(the method that builds NSToolbarItem instances and sets item.target/action like
toggleSidebarAction, toggleNotificationsAction, newTabAction) and add the
corresponding keys and translations into Resources/Localizable.xcstrings for
English and Japanese.
In `@Sources/Workspace.swift`:
- Around line 6781-6789: The code unconditionally sets tabBarHeight to 0 on
macOS 26 which hides tabs for all panes; change the helper so tabBarHeight is
computed per-pane (or accept a parameter like tabCount or isSingleTab) and only
return 0 when the pane has a single tab on macOS 26. Update the
BonsplitConfiguration.Appearance creation logic (the call site that currently
uses Self.currentSplitButtonTooltips()) to pass the pane-specific flag or count,
and alter the helper that currently sets hideTabBar to instead check both OS
version and whether the pane is single-tab before returning tabBarHeight 0; keep
the default 33 for multi-tab panes.
---
Nitpick comments:
In `@Sources/TerminalWindowPortal.swift`:
- Around line 1504-1516: The corner radius and maskedCorners updates on
hostedView.layer (the block checking hasSidebarToLeft and setting
hostedView.layer?.cornerRadius / hostedView.layer?.maskedCorners) must be
performed inside a CATransaction with actions disabled (or moved into the
existing CATransaction that updates frame/bounds) to prevent implicit animations
during geometry sync; wrap the radius/maskedCorners assignment in a
CATransaction.begin()/setDisableActions(true)/commit() pair (or relocate those
lines into the existing disabled-action transaction) so changes to cornerRadius
and maskedCorners aren’t implicitly animated.
In `@Sources/WindowToolbarController.swift`:
- Around line 230-236: The discard assignments are unnecessary: remove the `_ =`
when calling AppDelegate.shared?.sidebarState?.toggle() inside
toggleSidebarAction() and when calling
AppDelegate.shared?.toggleNotificationsPopover(animated: true) inside
toggleNotificationsAction() so each method is invoked directly (i.e., call
sidebarState?.toggle() and toggleNotificationsPopover(animated: true) without
assigning their Void results).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b13684ed-2261-47a7-9ab5-3bfe6eab8317
📒 Files selected for processing (8)
Sources/AppDelegate.swiftSources/ContentView.swiftSources/GhosttyTerminalView.swiftSources/TerminalWindowPortal.swiftSources/WindowToolbarController.swiftSources/Workspace.swiftSources/cmuxApp.swiftvendor/bonsplit
| .toolbar { | ||
| ToolbarItemGroup(placement: .primaryAction) { | ||
| Button { | ||
| _ = AppDelegate.shared?.toggleNotificationsPopover(animated: true) | ||
| } label: { | ||
| Image(systemName: "bell") | ||
| } | ||
| .buttonStyle(.accessoryBarAction) | ||
| .accessibilityIdentifier("toolbar.notifications") | ||
|
|
||
| Button { | ||
| if let appDelegate = AppDelegate.shared { | ||
| if appDelegate.addWorkspaceInPreferredMainWindow(debugSource: "toolbar.newTab") == nil { | ||
| appDelegate.openNewMainWindow(nil) | ||
| } | ||
| } | ||
| } label: { | ||
| Image(systemName: "plus") | ||
| } | ||
| .buttonStyle(.accessoryBarAction) | ||
| .accessibilityIdentifier("toolbar.newTab") | ||
| } |
There was a problem hiding this comment.
Give the new toolbar icons localized action names.
These are image-only buttons, so VoiceOver/help text falls back to the SF Symbol name (or no useful action name). Please switch them to localized Labels and keep the icon-only appearance with .labelStyle(.iconOnly).
Possible fix
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button {
_ = AppDelegate.shared?.toggleNotificationsPopover(animated: true)
} label: {
- Image(systemName: "bell")
+ Label(
+ String(localized: "toolbar.notifications.button", defaultValue: "Notifications"),
+ systemImage: "bell"
+ )
}
+ .labelStyle(.iconOnly)
+ .help(String(localized: "toolbar.notifications.button", defaultValue: "Notifications"))
.buttonStyle(.accessoryBarAction)
.accessibilityIdentifier("toolbar.notifications")
Button {
if let appDelegate = AppDelegate.shared {
if appDelegate.addWorkspaceInPreferredMainWindow(debugSource: "toolbar.newTab") == nil {
appDelegate.openNewMainWindow(nil)
}
}
} label: {
- Image(systemName: "plus")
+ Label(
+ String(localized: "toolbar.newWorkspace.button", defaultValue: "New Workspace"),
+ systemImage: "plus"
+ )
}
+ .labelStyle(.iconOnly)
+ .help(String(localized: "toolbar.newWorkspace.button", defaultValue: "New Workspace"))
.buttonStyle(.accessoryBarAction)
.accessibilityIdentifier("toolbar.newTab")
}
}As per coding guidelines All user-facing strings must be localized using String(localized: "key.name", defaultValue: "English text") for every string shown in the UI. Keys must go in Resources/Localizable.xcstrings with translations for all supported languages (English and Japanese).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/ContentView.swift` around lines 2901 - 2922, The toolbar's image-only
Buttons (the bell and plus in the ToolbarItemGroup) lack localized action names
for VoiceOver; replace each Button's label Image block with a localized Label
using String(localized: "toolbar.notifications", defaultValue: "Notifications")
and String(localized: "toolbar.newTab", defaultValue: "New Tab") (or similar
keys), then apply .labelStyle(.iconOnly) to keep the icon-only appearance;
ensure the same accessibilityIdentifier values ("toolbar.notifications" and
"toolbar.newTab") remain and add corresponding keys to
Resources/Localizable.xcstrings with English and Japanese translations.
There was a problem hiding this comment.
The SwiftUI toolbar buttons carry .accessibilityLabel(String(localized:...)) and .accessibilityIdentifier(...); the AppKit NSToolbarItems use String(localized:) for label, toolTip, and accessibilityDescription (WindowToolbarController.swift:196-219). VoiceOver picks up the localized names.
There was a problem hiding this comment.
@rodchristiansen Thanks for the clarification — agreed.
I checked the current macOS 26+ SwiftUI toolbar in Sources/ContentView.swift:
- Notifications button: has .accessibilityIdentifier("toolbar.notifications") and a localized .accessibilityLabel("toolbar.notifications.label", defaultValue: "Notifications").
- New Tab button: has .accessibilityIdentifier("toolbar.newTab") and a localized .accessibilityLabel("toolbar.newTab.label", defaultValue: "New Tab").
- The rest of the primary actions also carry localized .accessibilityLabel.
- The navigation sidebar toggle additionally defines .help, and WindowToolbarController.swift sets localized label/toolTip/accessibilityDescription for its NSToolbarItems.
Given that, VoiceOver already announces proper, localized names. No change is required, and I’m withdrawing the earlier suggestion.
Optional polish (not required): you could add .help to the two SwiftUI buttons for consistent tooltips in the SwiftUI path:
- Notifications: .help(String(localized: "toolbar.notifications.label", defaultValue: "Notifications"))
- New Tab: .help(String(localized: "toolbar.newTab.label", defaultValue: "New Tab"))
✏️ Learnings added
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.
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: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T02:19:07.331Z
Learning: Applies to Sources/**/GhosttyTerminalView.swift : Mount `SurfaceSearchOverlay` from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`, to maintain proper layering during split/workspace churn
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-25T04:48:00.216Z
Learning: Applies to **/GhosttyTerminalView.swift : `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers. Portal-hosted terminal views can sit above SwiftUI during split/workspace churn.
Learnt from: wonbywondev
Repo: manaflow-ai/cmux PR: 1805
File: Sources/ContentView.swift:662-672
Timestamp: 2026-03-19T10:48:34.765Z
Learning: Repo manaflow-ai/cmux — Sources/ContentView.swift: FileDropOverlayView is anchored to the window’s contentView (theme frame). Within its bounds, the only non-terminal/non-browser area is the Sidebar, so performDragOperation’s folder-drop fallback (calling onDrop when no terminal/web view is under the cursor) is effectively sidebar-only and does not require additional hit-testing for the titlebar or other regions.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-05T03:20:48.079Z
Learning: Applies to **/*TerminalView.swift : Terminal find layering contract: SurfaceSearchOverlay must be mounted from GhosttySurfaceScrollView in Sources/GhosttyTerminalView.swift (AppKit portal layer), not from SwiftUI panel containers
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: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-04T01:01:43.511Z
Learning: Applies to {**/GhosttyTerminalView.swift,**/TerminalPanelView.swift} : `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-14T19:57:19.057Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — SidebarTabDropDelegate.performDrop(info:) funnels all sidebar drag-and-drop through a section-aware path: (1) if the drop target is inside a section, compute the insertion index from the destination section’s workspaceIds after removing the dragged id, then call TabManager.moveWorkspaceToSection(tabId:sectionId:atIndex:); (2) if the target is ungrouped, first remove the workspace from its source section via TabManager.removeWorkspaceFromSection(tabId:) and then call TabManager.reorderWorkspace(tabId:toIndex:). Same-index same-section drops intentionally no-op. This ensures cross-section moves update membership and same-section downward moves don’t overshoot.
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/ContentView.swift:9956-9963
Timestamp: 2026-04-14T19:57:01.062Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — TabItemView.index must always reflect the flat order from TabManager.tabs (not the sidebar section/display order). VerticalTabsSidebar builds a tabIndexById from tabs.enumerated() and passes index: tabIndexById[tab.id] to all TabItemView call sites (pinned, ungrouped, and sectioned). Future changes should preserve this contract so shift-selection and moveBy(_:) continue to work correctly.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-05T00:05:51.436Z
Learning: Applies to **/GhosttyTerminalView.swift : `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-25T04:48:00.216Z
Learning: Applies to **/ContentView.swift : In `TabItemView` in `ContentView.swift`: uses `Equatable` conformance + `.equatable()` to skip body re-evaluation during typing. Do not add `EnvironmentObject`, `ObservedObject` (besides `tab`), or `Binding` properties without updating the `==` function. Do not remove `.equatable()` from the ForEach call site. Do not read `tabManager` or `notificationStore` in the body; use precomputed `let` parameters instead.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-25T04:48:00.216Z
Learning: Applies to **/*.swift : All user-facing strings must be localized using `String(localized: "key.name", defaultValue: "English text")` for every string shown in the UI (labels, buttons, menus, dialogs, tooltips, error messages). Keys must go in `Resources/Localizable.xcstrings` with translations for all supported languages (English and Japanese). Never use bare string literals in SwiftUI `Text()`, `Button()`, alert titles, etc.
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 : All user-facing strings must be localized. Use `String(localized: "key.name", defaultValue: "English text")` for every string shown in the UI (labels, buttons, menus, dialogs, tooltips, error messages). Keys go in `Resources/Localizable.xcstrings` with translations for all supported languages (currently English and Japanese)
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: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T02:19:07.331Z
Learning: Applies to **/*.swift : Never use bare string literals in SwiftUI `Text()`, `Button()`, alert titles, or other UI components; use localized strings instead
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: Znboston
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-26T19:00:12.434Z
Learning: Repo: manaflow-ai/cmux — Sources/GroupHeaderView.swift and Sources/ContentView.swift — Palette color entry labels (`Text(entry.name)`) use dynamic palette data loaded at runtime and cannot be statically keyed with `String(localized:)`. This matches the existing workspace color picker pattern in ContentView.swift. Palette name localization would require restructuring the color palette system and is intentionally deferred; do not flag `entry.name` as a missing localization in this codebase.
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 318
File: Sources/Panels/BrowserPanel.swift:5511-5521
Timestamp: 2026-03-17T05:34:52.853Z
Learning: In manaflow-ai/cmux, import-flow UI strings in Sources/Panels/BrowserPanel.swift must use String(localized:..., defaultValue:...) with browser.import.* keys, while brand/engine names (e.g., Google, DuckDuckGo) remain unlocalized per project convention.
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: 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: ariel-wiz
Repo: manaflow-ai/cmux PR: 543
File: CLI/cmux.swift:2006-2007
Timestamp: 2026-03-11T09:27:14.201Z
Learning: In manaflow-ai/cmux (CLI/cmux.swift), all CLI error messages and help text use plain Swift string literals — there is no `String(localized:...)` usage anywhere in the repository, and no `Localizable.xcstrings` file exists. Do not flag CLI error/help strings as missing localization for this project.
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: 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: 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: 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: 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: 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: 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: 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: lawrencecchen
Repo: manaflow-ai/cmux PR: 2876
File: Sources/cmuxApp.swift:3812-3821
Timestamp: 2026-04-14T04:50:37.548Z
Learning: Repo: manaflow-ai/cmux — App icon contract: In Sources/cmuxApp.swift, AppIconSettings.automatic uses the bundle .icon only when AppearanceSettings is .system; when the app appearance is forced (.light/.dark), it switches to a runtime light/dark image. cmuxApp.applyAppearance() re-applies AppIconSettings.applyIcon(.automatic) on appearance changes to keep the icon in sync across hosts (Icon Composer supported and fallback).
Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 2505
File: Sources/GhosttyTerminalView.swift:1694-1737
Timestamp: 2026-04-06T07:16:45.980Z
Learning: Repo: manaflow-ai/cmux — Background rendering contract
- In Sources/GhosttyTerminalView.swift, GhosttyApp.loadDefaultConfigFilesWithLegacyFallback(_:) sets usesHostLayerBackground = false when a Ghostty background-image is configured (injects "macos-background-from-layer = false"); otherwise it enables the host-layer path.
- GhosttyNSView.applySurfaceBackground() reads GhosttyApp.shared.usesHostLayerBackground and sets GhosttySurfaceScrollView’s background to .clear when false, avoiding a second solid-color fill under Ghostty’s image compositor.
- After config reload, terminal panels reapply the correct host-layer/background mode to stay consistent with the current setting.
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: 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: debgotwired
Repo: manaflow-ai/cmux PR: 1149
File: Sources/ContentView.swift:3977-3978
Timestamp: 2026-03-10T09:33:29.814Z
Learning: In manaflow-ai/cmux, for Sources/ContentView.swift, do not modify the sync-seeding behavior of scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:). It is shared infrastructure across all submenus. Treat brief initial flashes as consistent with existing submenus. Any UX improvement (for example making seeding synchronous on a forced corpus refresh) should not be implemented in feature-scoped PRs; instead implement and test such UX changes globally in a dedicated follow-up PR.
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: 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.
| if #available(macOS 26.0, *) { | ||
| window.titlebarAppearsTransparent = false | ||
| window.titlebarSeparatorStyle = .none | ||
| } else { | ||
| window.titlebarAppearsTransparent = true | ||
| } | ||
| if #available(macOS 26.0, *) { | ||
| // On macOS 26 without fullSizeContentView, the system titlebar | ||
| // handles drag natively. | ||
| window.isMovable = true | ||
| window.isMovableByWindowBackground = false | ||
| } else { | ||
| // Keep window immovable; the sidebar's WindowDragHandleView handles | ||
| // drag-to-move via performDrag with temporary movable override. | ||
| // isMovableByWindowBackground=true breaks tab reordering, and | ||
| // isMovable=true blocks clicks on sidebar buttons in minimal mode. | ||
| window.isMovableByWindowBackground = false | ||
| window.isMovable = false | ||
| } | ||
| window.styleMask.insert(.fullSizeContentView) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
On macOS, what layout effect does NSWindow.StyleMask.fullSizeContentViewhave whentitlebarAppearsTransparent is false, and does it still extend content into the titlebar area?
💡 Result:
NSWindow.StyleMask.fullSizeContentView makes the window’s contentView take the entire window size (including the region normally reserved for the titlebar/toolbar), and you’re expected to use contentLayoutRect / contentLayoutGuide if you want your UI to start below the titlebar/toolbar area. [1]
If titlebarAppearsTransparent is false (the default), the titlebar/toolbar is still drawn on top of that extended content area (typically blurring/obscuring what’s underneath), so your content is still extended into the titlebar region, but it won’t appear “behind” the titlebar as unobscured content. [1][2]
🏁 Script executed:
# Check lines around 2679-2684 to understand the "zero-top-padding macOS 26 path"
sed -n '2670,2700p' Sources/ContentView.swiftRepository: manaflow-ai/cmux
Length of output: 1428
🏁 Script executed:
# Check the full context of the window configuration section (3487-3506 and surrounding)
sed -n '3480,3520p' Sources/ContentView.swiftRepository: manaflow-ai/cmux
Length of output: 2128
🏁 Script executed:
# Search for all fullSizeContentView references to understand how it's used conditionally
rg 'fullSizeContentView' Sources/ContentView.swift -B 2 -A 2Repository: manaflow-ai/cmux
Length of output: 866
Remove fullSizeContentView from the macOS 26+ path, or update the comment to reflect actual behavior.
The comment at line 3494 says "On macOS 26 without fullSizeContentView, the system titlebar handles drag natively," but line 3506 unconditionally inserts fullSizeContentView for all OS versions. This contradiction is inconsistent with the zero-top-padding design at lines 2679-2684 (which relies on the native glass titlebar to handle safe-area spacing). When fullSizeContentView is active, the contentView extends into the titlebar region, and the layout depends entirely on SwiftUI respecting contentLayoutRect—a fragile dependency that conflicts with the stated intent of letting the macOS 26 native titlebar handle the layout natively.
Either conditionally skip .fullSizeContentView for macOS 26+, or clarify the comment to explain why the native titlebar path still requires extended content bounds.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/ContentView.swift` around lines 3487 - 3506, The code sets
window.styleMask.insert(.fullSizeContentView) unconditionally, which contradicts
the macOS 26+ branch that expects the system titlebar to handle dragging without
fullSizeContentView; either make the insertion conditional (only insert
.fullSizeContentView in the else branch for pre-macOS 26) or update the comment
above the macOS 26+ branch to explicitly explain why fullSizeContentView is
still required there; locate the availability checks and the unconditional call
to window.styleMask.insert(.fullSizeContentView) and change it to conditional
insertion tied to the macOS 26 availability or revise the comment near the macOS
26 block to describe the actual layout/drag implications when
.fullSizeContentView is present.
There was a problem hiding this comment.
Will tighten the contradictory comment in a follow-up. The code behavior — unconditional .fullSizeContentView — is correct for macOS 26's native titlebar; the comment wording predates the decision.
There was a problem hiding this comment.
@rodchristiansen Got it—thanks for clarifying. I’ll skip changes here.
If helpful, here’s a ready-to-paste comment tweak for that block to avoid future confusion:
swift
// macOS 26+: We intentionally keep .fullSizeContentView set.
// The system titlebar draws above the extended content and handles window dragging.
// We keep titlebarAppearsTransparent = false and isMovable = true (with
// isMovableByWindowBackground = false). Geometry/padding is derived from
// contentLayoutRect where needed.
Want me to open a small follow-up issue to track this comment cleanup and link back to this PR and discussion? I can file it and assign it to you.
✏️ Learnings added
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.
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: 1805
File: Sources/ContentView.swift:662-672
Timestamp: 2026-03-19T10:48:34.765Z
Learning: Repo manaflow-ai/cmux — Sources/ContentView.swift: FileDropOverlayView is anchored to the window’s contentView (theme frame). Within its bounds, the only non-terminal/non-browser area is the Sidebar, so performDragOperation’s folder-drop fallback (calling onDrop when no terminal/web view is under the cursor) is effectively sidebar-only and does not require additional hit-testing for the titlebar or other regions.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T02:19:07.331Z
Learning: Applies to Sources/**/GhosttyTerminalView.swift : Mount `SurfaceSearchOverlay` from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`, to maintain proper layering during split/workspace churn
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: 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: 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: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-25T04:48:00.216Z
Learning: Applies to **/GhosttyTerminalView.swift : `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers. Portal-hosted terminal views can sit above SwiftUI during split/workspace churn.
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: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-14T19:57:19.057Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — SidebarTabDropDelegate.performDrop(info:) funnels all sidebar drag-and-drop through a section-aware path: (1) if the drop target is inside a section, compute the insertion index from the destination section’s workspaceIds after removing the dragged id, then call TabManager.moveWorkspaceToSection(tabId:sectionId:atIndex:); (2) if the target is ungrouped, first remove the workspace from its source section via TabManager.removeWorkspaceFromSection(tabId:) and then call TabManager.reorderWorkspace(tabId:toIndex:). Same-index same-section drops intentionally no-op. This ensures cross-section moves update membership and same-section downward moves don’t overshoot.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-05T03:20:48.079Z
Learning: Applies to **/*TerminalView.swift : Terminal find layering contract: SurfaceSearchOverlay must be mounted from GhosttySurfaceScrollView in Sources/GhosttyTerminalView.swift (AppKit portal layer), not from SwiftUI panel containers
Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 2505
File: Sources/GhosttyTerminalView.swift:1694-1737
Timestamp: 2026-04-06T07:16:45.980Z
Learning: Repo: manaflow-ai/cmux — Background rendering contract
- In Sources/GhosttyTerminalView.swift, GhosttyApp.loadDefaultConfigFilesWithLegacyFallback(_:) sets usesHostLayerBackground = false when a Ghostty background-image is configured (injects "macos-background-from-layer = false"); otherwise it enables the host-layer path.
- GhosttyNSView.applySurfaceBackground() reads GhosttyApp.shared.usesHostLayerBackground and sets GhosttySurfaceScrollView’s background to .clear when false, avoiding a second solid-color fill under Ghostty’s image compositor.
- After config reload, terminal panels reapply the correct host-layer/background mode to stay consistent with the current setting.
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: 2475
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-03T07:19:36.497Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, the command‑palette rename flow uses a single-line SwiftUI TextField via commandPaletteEditorField(style: .singleLine(...)); the multiline NSTextView editor is only used for the workspace description input. Do not flag newline persistence for rename.
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/CmuxConfigExecutor.swift:107-118
Timestamp: 2026-04-14T19:56:39.598Z
Learning: Repo: manaflow-ai/cmux — In Sources/CmuxConfigExecutor.swift, executeWorkspaceCommand for wsDef.target == .current intentionally uses closePanel(panelId, force: true) before calling applyCustomLayout. This is by design: soft-closing (force: false) leaves detached panes that applyCustomLayout cannot reconcile. If a non-destructive autoApply variant is needed in the future, it should be opted into explicitly via a separate flag rather than changing the default force: true behavior.
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: 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: 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: 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: 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: 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: 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: lawrencecchen
Repo: manaflow-ai/cmux PR: 2876
File: Sources/cmuxApp.swift:3812-3821
Timestamp: 2026-04-14T04:50:37.548Z
Learning: Repo: manaflow-ai/cmux — App icon contract: In Sources/cmuxApp.swift, AppIconSettings.automatic uses the bundle .icon only when AppearanceSettings is .system; when the app appearance is forced (.light/.dark), it switches to a runtime light/dark image. cmuxApp.applyAppearance() re-applies AppIconSettings.applyIcon(.automatic) on appearance changes to keep the icon in sync across hosts (Icon Composer supported and fallback).
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: 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: debgotwired
Repo: manaflow-ai/cmux PR: 1149
File: Sources/ContentView.swift:3977-3978
Timestamp: 2026-03-10T09:33:29.814Z
Learning: In manaflow-ai/cmux, for Sources/ContentView.swift, do not modify the sync-seeding behavior of scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:). It is shared infrastructure across all submenus. Treat brief initial flashes as consistent with existing submenus. Any UX improvement (for example making seeding synchronous on a forced corpus refresh) should not be implemented in feature-scoped PRs; instead implement and test such UX changes globally in a dedicated follow-up PR.
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: 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.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
Sources/ContentView.swift (1)
15475-15520:⚠️ Potential issue | 🟠 MajorAvoid private selector dispatch on
NSSplitView.Line 15511 uses
setDividerColor:via runtime selector dispatch. This is not a public API—the macOS SDK documents only adividerColorgetter onNSSplitViewand does not expose a public setter. Using undocumented selectors is brittle and can break across SDK/runtime updates.Instead, keep only public APIs (like
dividerStyle). If visual hiding is still needed, mask with a SwiftUI/AppKit overlay as a safer fallback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/ContentView.swift` around lines 15475 - 15520, The code uses a private ObjC selector NSSelectorFromString("setDividerColor:") inside patchSplitViews(in:) to call setDividerColor: on NSSplitView; remove that selector dispatch and keep only the public API change (splitView.dividerStyle = .thin) in SplitViewDividerHiderView.patchSplitViews(in:). If you still need to visually hide dividers, implement a public-API fallback: add a safe masking helper (e.g., addDividerMask(to:) called from patchSplitViews) that inserts an NSView overlay or adjusts the split view’s layer (wantsLayer = true and layer properties) to cover the divider area, and remove the NSSelectorFromString/perform(...) block entirely.
🧹 Nitpick comments (3)
Sources/GhosttyTerminalView.swift (1)
9118-9134: Consider tying the leading inset to the same sidebar-presence condition used for corner rounding.
Sources/TerminalWindowPortal.swiftonly rounds leading corners when a sidebar is detected; keeping this inset always-on may leave unnecessary gutter when the sidebar is hidden. A shared condition (or propagated flag) would keep geometry behavior aligned.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/GhosttyTerminalView.swift` around lines 9118 - 9134, The leading inset is always applied in GhosttyTerminalView despite corner rounding only happening when the sidebar is visible; update the inset calculation to apply the inset only when the same "sidebar visible" condition used in TerminalWindowPortal.swift is true (propagate or query that flag into GhosttyTerminalView), e.g. compute inset = (isSidebarVisible ? 6 : 0) before building scrollFrame, and then call setFrameIfNeeded(scrollView, to: scrollFrame) as before so geometry and corner rounding remain in sync; refer to the scrollFrame/inset calculation and the call to setFrameIfNeeded(scrollView, to:) when locating the change.Sources/WindowToolbarController.swift (1)
230-232: Minor: Unnecessary discard assignment.
SidebarState.toggle()returnsVoid, so the_ =prefix is superfluous.♻️ Suggested simplification
`@objc` private func toggleSidebarAction() { - _ = AppDelegate.shared?.sidebarState?.toggle() + AppDelegate.shared?.sidebarState?.toggle() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/WindowToolbarController.swift` around lines 230 - 232, The toggleSidebarAction method contains an unnecessary discard assignment; remove the "_ =" so the call simply invokes AppDelegate.shared?.sidebarState?.toggle() because SidebarState.toggle() returns Void. Update the toggleSidebarAction function to call SidebarState.toggle() without assigning its result (targeting the invocation in toggleSidebarAction and the SidebarState.toggle call).Sources/AppDelegate.swift (1)
2601-2605: Decouple the macOS 26 notifications bridge from legacy accessory startup.
titlebarAccessoryController.start()now runs even thoughattachUpdateAccessory(to:)never attaches on macOS 26. That still keeps the legacy controller’s observer/startup lifecycle alive after the UI moved to the SwiftUI toolbar/popover path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/AppDelegate.swift` around lines 2601 - 2605, The legacy accessory controller is being started unconditionally via titlebarAccessoryController.start() even on macOS 26 where attachUpdateAccessory(to:) is not used; stop starting the legacy controller in that case to avoid keeping its observers/lifecycle alive. Update the startup logic so titlebarAccessoryController.start() is only called when the legacy accessory path will be attached (e.g., guard the call behind the same OS-version or feature check used by attachUpdateAccessory(to:) — or add a predicate like shouldUseLegacyAccessory() — and call start() only when that predicate is true), leaving the SwiftUI toolbar/popover path on macOS 26 untouched. Ensure references to titlebarAccessoryController and attachUpdateAccessory(to:) are adjusted consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 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.
In `@Sources/ContentView.swift`:
- Around line 2967-2969: The global notification handler in ContentView
(.onReceive(NotificationCenter.default.publisher(for:
AppDelegate.toggleNotificationsPopoverNotification))) unconditionally toggles
isNotificationsPopoverPresented causing all windows to change; change the
onReceive closure to first cast notification.object to an NSWindow and only
toggle when that window matches this view’s hosting window (i.e., compare
notification.object as? NSWindow to this ContentView’s window/hosting NSWindow),
and update AppDelegate.toggleNotificationsPopoverNotification posts to include
the NSWindow as the notification object so the subscriber can perform the match.
In `@Sources/GhosttyTerminalView.swift`:
- Around line 9123-9130: The inset calculation in GhosttyTerminalView (inside
the macOS 26.0 availability branch where scrollFrame is computed) can produce
negative width/height when bounds are smaller than the hardcoded inset; fix this
by clamping computed width and height to non-negative values (e.g., use
max(bounds.width - inset, 0) and max(bounds.height - inset, 0)) and adjust the
x/y inset application to not exceed the bounds (e.g., compute effectiveInset =
min(inset, bounds.width) for x and min(inset, bounds.height) for y) before
assigning scrollFrame so the resulting CGRect never has negative dimensions.
---
Duplicate comments:
In `@Sources/ContentView.swift`:
- Around line 15475-15520: The code uses a private ObjC selector
NSSelectorFromString("setDividerColor:") inside patchSplitViews(in:) to call
setDividerColor: on NSSplitView; remove that selector dispatch and keep only the
public API change (splitView.dividerStyle = .thin) in
SplitViewDividerHiderView.patchSplitViews(in:). If you still need to visually
hide dividers, implement a public-API fallback: add a safe masking helper (e.g.,
addDividerMask(to:) called from patchSplitViews) that inserts an NSView overlay
or adjusts the split view’s layer (wantsLayer = true and layer properties) to
cover the divider area, and remove the NSSelectorFromString/perform(...) block
entirely.
---
Nitpick comments:
In `@Sources/AppDelegate.swift`:
- Around line 2601-2605: The legacy accessory controller is being started
unconditionally via titlebarAccessoryController.start() even on macOS 26 where
attachUpdateAccessory(to:) is not used; stop starting the legacy controller in
that case to avoid keeping its observers/lifecycle alive. Update the startup
logic so titlebarAccessoryController.start() is only called when the legacy
accessory path will be attached (e.g., guard the call behind the same OS-version
or feature check used by attachUpdateAccessory(to:) — or add a predicate like
shouldUseLegacyAccessory() — and call start() only when that predicate is true),
leaving the SwiftUI toolbar/popover path on macOS 26 untouched. Ensure
references to titlebarAccessoryController and attachUpdateAccessory(to:) are
adjusted consistently.
In `@Sources/GhosttyTerminalView.swift`:
- Around line 9118-9134: The leading inset is always applied in
GhosttyTerminalView despite corner rounding only happening when the sidebar is
visible; update the inset calculation to apply the inset only when the same
"sidebar visible" condition used in TerminalWindowPortal.swift is true
(propagate or query that flag into GhosttyTerminalView), e.g. compute inset =
(isSidebarVisible ? 6 : 0) before building scrollFrame, and then call
setFrameIfNeeded(scrollView, to: scrollFrame) as before so geometry and corner
rounding remain in sync; refer to the scrollFrame/inset calculation and the call
to setFrameIfNeeded(scrollView, to:) when locating the change.
In `@Sources/WindowToolbarController.swift`:
- Around line 230-232: The toggleSidebarAction method contains an unnecessary
discard assignment; remove the "_ =" so the call simply invokes
AppDelegate.shared?.sidebarState?.toggle() because SidebarState.toggle() returns
Void. Update the toggleSidebarAction function to call SidebarState.toggle()
without assigning its result (targeting the invocation in toggleSidebarAction
and the SidebarState.toggle call).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 04b9fbdf-fc7b-405a-a08d-2f294a14cb52
📒 Files selected for processing (7)
Sources/AppDelegate.swiftSources/ContentView.swiftSources/GhosttyTerminalView.swiftSources/TerminalWindowPortal.swiftSources/Update/UpdateTitlebarAccessory.swiftSources/WindowToolbarController.swiftSources/Workspace.swift
✅ Files skipped from review due to trivial changes (1)
- Sources/Update/UpdateTitlebarAccessory.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- Sources/TerminalWindowPortal.swift
| /// 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, *) { | ||
| NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: nil) | ||
| return | ||
| } | ||
| titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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
@Statechanges.
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
@Statevar 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.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
Sources/ContentView.swift (1)
2991-2999:⚠️ Potential issue | 🟠 MajorScope popover toggle to the owning window.
The notifications popover broadcast is received by all
ContentViewinstances without window filtering. In multi-window sessions, toggling the popover in one window will unintentionally open/close it in unrelated windows. Pass the window to the notification (similar tocommandPaletteRequested) and filter subscriptions by comparing againstwindowIdentifier.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/ContentView.swift` around lines 2991 - 2999, The notifications popover toggle is being broadcast to all ContentView instances; modify the publisher subscription in ContentView so it only reacts for its owning window: change the notification payload (AppDelegate.toggleNotificationsPopoverNotification) to include the source window identifier (similar to commandPaletteRequested), and in ContentView's .onReceive for NotificationCenter.default.publisher(for: AppDelegate.toggleNotificationsPopoverNotification) unpack the notification, compare the payload's windowIdentifier to this view's windowIdentifier, and only then toggle isNotificationsPopoverPresented (used with NotificationsPopoverView and its onDismiss). Ensure the sender sets the windowIdentifier when posting the notification and keep the filtering logic in ContentView so other windows ignore it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/ContentView.swift`:
- Around line 2918-2932: The custom toolbar toggle is only created when
sidebarState.isVisible is false while SystemSidebarToggleStripper removes the
system toggle unconditionally, leaving no collapse affordance; update the
toolbar code so the Button in the ToolbarItem (placement: .navigation) that
calls sidebarState.toggle() is rendered regardless of sidebarState.isVisible
(remove the if !sidebarState.isVisible guard) and/or change
SystemSidebarToggleStripper to only strip the system-injected toggle when a
custom toggle exists; keep the same Button action (withAnimation and
sidebarState.toggle()), image ("sidebar.left"), accessibilityIdentifier
("toolbar.toggleSidebar") and accessibility/help strings so the toggle remains
available to collapse the open sidebar.
- Around line 15598-15616: The current patchSplitViews(in:) walks every
NSSplitView in the window and clears dividers globally; instead limit the change
to the outer NavigationSplitView container: update hideDividers() to locate only
the top-level split view that represents the navigation container (search
window.contentView?.subviews for the NSSplitView that is the NavigationSplitView
host, e.g. the direct child corresponding to the app's sidebar split) and call a
new patch function only on that single split view; keep patchSplitViews(in:) but
rename/use it to operate on a single NSSplitView (or add a
patchNavigationSplitView(in:)) so nested split-based content is not modified.
Ensure you reference hideDividers(), patchSplitViews(in:), NavigationSplitView
and window.contentView when making the change.
- Around line 2911-2914: The outer .padding(8) applied to the detail container
creates a dead file-drop margin on macOS 26 because FileDropOverlayView only
forwards drops when the cursor is over a GhosttyNSView or WKWebView; remove the
.padding(8) from the detail: closure and instead apply equivalent spacing inside
terminalContentWithSidebarDropOverlay (i.e., inside the terminal/browser
content) so the visual inset is part of the terminal/browser view rather than
the detail container and drops still hit GhosttyNSView/WKWebView.
---
Duplicate comments:
In `@Sources/ContentView.swift`:
- Around line 2991-2999: The notifications popover toggle is being broadcast to
all ContentView instances; modify the publisher subscription in ContentView so
it only reacts for its owning window: change the notification payload
(AppDelegate.toggleNotificationsPopoverNotification) to include the source
window identifier (similar to commandPaletteRequested), and in ContentView's
.onReceive for NotificationCenter.default.publisher(for:
AppDelegate.toggleNotificationsPopoverNotification) unpack the notification,
compare the payload's windowIdentifier to this view's windowIdentifier, and only
then toggle isNotificationsPopoverPresented (used with NotificationsPopoverView
and its onDismiss). Ensure the sender sets the windowIdentifier when posting the
notification and keep the filtering logic in ContentView so other windows ignore
it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8a317528-e622-4d94-b23e-17eb20516479
📒 Files selected for processing (2)
Sources/ContentView.swiftSources/KeyboardShortcutSettings.swift
| .toolbar { | ||
| if !sidebarState.isVisible { | ||
| ToolbarItem(placement: .navigation) { | ||
| Button { | ||
| withAnimation(.easeInOut(duration: 0.2)) { | ||
| _ = sidebarState.toggle() | ||
| } | ||
| } label: { | ||
| Image(systemName: "sidebar.left") | ||
| } | ||
| .accessibilityIdentifier("toolbar.toggleSidebar") | ||
| .accessibilityLabel(String(localized: "toolbar.sidebar.accessibilityLabel", defaultValue: "Toggle Sidebar")) | ||
| .help(String(localized: "toolbar.sidebar.tooltip", defaultValue: "Toggle Sidebar")) | ||
| } | ||
| } |
There was a problem hiding this comment.
Keep a toolbar toggle visible while the sidebar is open.
The custom button only renders when the sidebar is already hidden, and SystemSidebarToggleStripper removes the system-injected toggle unconditionally. That leaves no clickable toolbar affordance to collapse an open sidebar.
♻️ Suggested change
- if !sidebarState.isVisible {
- ToolbarItem(placement: .navigation) {
- Button {
- withAnimation(.easeInOut(duration: 0.2)) {
- _ = sidebarState.toggle()
- }
- } label: {
- Image(systemName: "sidebar.left")
- }
- .accessibilityIdentifier("toolbar.toggleSidebar")
- .accessibilityLabel(String(localized: "toolbar.sidebar.accessibilityLabel", defaultValue: "Toggle Sidebar"))
- .help(String(localized: "toolbar.sidebar.tooltip", defaultValue: "Toggle Sidebar"))
- }
- }
+ ToolbarItem(placement: .navigation) {
+ Button {
+ withAnimation(.easeInOut(duration: 0.2)) {
+ sidebarState.toggle()
+ }
+ } label: {
+ Image(systemName: "sidebar.left")
+ }
+ .accessibilityIdentifier("toolbar.toggleSidebar")
+ .accessibilityLabel(String(localized: "toolbar.sidebar.accessibilityLabel", defaultValue: "Toggle Sidebar"))
+ .help(String(localized: "toolbar.sidebar.tooltip", defaultValue: "Toggle Sidebar"))
+ }Also applies to: 15535-15552
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/ContentView.swift` around lines 2918 - 2932, The custom toolbar
toggle is only created when sidebarState.isVisible is false while
SystemSidebarToggleStripper removes the system toggle unconditionally, leaving
no collapse affordance; update the toolbar code so the Button in the ToolbarItem
(placement: .navigation) that calls sidebarState.toggle() is rendered regardless
of sidebarState.isVisible (remove the if !sidebarState.isVisible guard) and/or
change SystemSidebarToggleStripper to only strip the system-injected toggle when
a custom toggle exists; keep the same Button action (withAnimation and
sidebarState.toggle()), image ("sidebar.left"), accessibilityIdentifier
("toolbar.toggleSidebar") and accessibility/help strings so the toggle remains
available to collapse the open sidebar.
There was a problem hiding this comment.
Intentional — when the sidebar is visible, NavigationSplitView owns the native collapse chevron in the column header. The custom button is only needed to reopen the sidebar once it's been hidden (the chevron goes with the column). Keeping both visible would duplicate the toggle.
There was a problem hiding this comment.
@rodchristiansen Got it — thanks for clarifying. Given that on macOS 26 the NavigationSplitView shows its own collapse chevron in the sidebar’s column header when the sidebar is visible, keeping our toolbar toggle only for the “reopen when hidden” case (and stripping the system-injected toolbar toggle) avoids duplicate controls. No change needed here; I’ve captured this as a learning so we don’t re-flag it in future reviews.
✏️ Learnings added
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-2932
Timestamp: 2026-04-14T20:05:24.226Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift (PR `#2647`): On macOS 26, NavigationSplitView provides a native collapse chevron in the sidebar column header when the sidebar is visible. The code intentionally: (1) shows a custom toolbar toggle only when the sidebar is hidden to reopen it, and (2) uses SystemSidebarToggleStripper to remove the system-injected toolbar toggle to prevent duplication. Do not flag “no collapse affordance” while the sidebar is open.
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: 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: 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: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T02:19:07.331Z
Learning: Applies to Sources/**/GhosttyTerminalView.swift : Mount `SurfaceSearchOverlay` from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`, to maintain proper layering during split/workspace churn
Learnt from: wonbywondev
Repo: manaflow-ai/cmux PR: 1805
File: Sources/ContentView.swift:662-672
Timestamp: 2026-03-19T10:48:34.765Z
Learning: Repo manaflow-ai/cmux — Sources/ContentView.swift: FileDropOverlayView is anchored to the window’s contentView (theme frame). Within its bounds, the only non-terminal/non-browser area is the Sidebar, so performDragOperation’s folder-drop fallback (calling onDrop when no terminal/web view is under the cursor) is effectively sidebar-only and does not require additional hit-testing for the titlebar or other regions.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-25T04:48:00.216Z
Learning: Applies to **/GhosttyTerminalView.swift : `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers. Portal-hosted terminal views can sit above SwiftUI during split/workspace churn.
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-04-14T20:01:09.378Z
Learning: Repo: manaflow-ai/cmux — macOS 26 UI: NavigationSplitView with SwiftUI .toolbar owns window chrome; WindowToolbarController is not used on macOS 26 and remains as an AppKit fallback. Notifications popover on macOS 26 is driven via AppDelegate.toggleNotificationsPopover posting cmux.toggleNotificationsPopover, which ContentView observes to present a SwiftUI popover.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-05T03:20:48.079Z
Learning: Applies to **/*TerminalView.swift : Terminal find layering contract: SurfaceSearchOverlay must be mounted from GhosttySurfaceScrollView in Sources/GhosttyTerminalView.swift (AppKit portal layer), not from SwiftUI panel containers
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: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-04T01:01:43.511Z
Learning: Applies to {**/GhosttyTerminalView.swift,**/TerminalPanelView.swift} : `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/ContentView.swift:9956-9963
Timestamp: 2026-04-14T19:57:01.062Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — TabItemView.index must always reflect the flat order from TabManager.tabs (not the sidebar section/display order). VerticalTabsSidebar builds a tabIndexById from tabs.enumerated() and passes index: tabIndexById[tab.id] to all TabItemView call sites (pinned, ungrouped, and sectioned). Future changes should preserve this contract so shift-selection and moveBy(_:) continue to work correctly.
Learnt from: Znboston
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-26T21:24:07.678Z
Learning: Repo: manaflow-ai/cmux — Sources/SidebarSelectionState.swift — In `SidebarInteractionController.registerClick(on:location:...)`, call sites that use `onTapGesture` cannot supply real coordinates (SwiftUI's `onTapGesture` does not expose the tap location), so they pass `location: nil`. The `CGPoint?`-nil-gated distance check in `registerClick` is intentionally designed to skip spatial filtering for these callers. Target identity discrimination (`.groupHeader(groupId:)`, `.workspace(workspaceId:parentGroupId:)`, `.emptyArea`) prevents false double-click triggers across distinct targets. Do not flag missing coordinates from `onTapGesture` callsites as a defect; fixing it would require switching to a different gesture recognizer system.
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: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/ContentView.swift:8917-8921
Timestamp: 2026-04-03T03:35:56.499Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — ShortcutHintModifierPolicy.shouldShowHints(for:) reveals sidebar/titlebar shortcut hints when the current modifier flags exactly equal KeyboardShortcutSettings.shortcut(for: .selectWorkspaceByNumber).modifierFlags, and returns false for chorded-number mappings. This follows the configured workspace-number modifier (not strictly Command-only). TabItemView’s close-button suppression via showsModifierShortcutHints therefore tracks the configured modifier. The debug toggle showHintsOnCommandHold predates customizable shortcuts and conceptually means “show hints on holding the configured workspace-number modifier(s).”
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}.
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: 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: 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: 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: 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: 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: 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: 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: 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: debgotwired
Repo: manaflow-ai/cmux PR: 1149
File: Sources/ContentView.swift:3977-3978
Timestamp: 2026-03-10T09:33:29.814Z
Learning: In manaflow-ai/cmux, for Sources/ContentView.swift, do not modify the sync-seeding behavior of scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:). It is shared infrastructure across all submenus. Treat brief initial flashes as consistent with existing submenus. Any UX improvement (for example making seeding synchronous on a forced corpus refresh) should not be implemented in feature-scoped PRs; instead implement and test such UX changes globally in a dedicated follow-up PR.
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: 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: 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: 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.
| private func hideDividers() { | ||
| guard let window else { return } | ||
| patchSplitViews(in: window.contentView) | ||
| } | ||
|
|
||
| private func patchSplitViews(in view: NSView?) { | ||
| guard let view else { return } | ||
| if let splitView = view as? NSSplitView { | ||
| splitView.dividerStyle = .thin | ||
| // Clear the divider color via ObjC messaging (private API). | ||
| let selector = NSSelectorFromString("setDividerColor:") | ||
| if splitView.responds(to: selector) { | ||
| splitView.perform(selector, with: NSColor.clear) | ||
| } | ||
| } | ||
| for subview in view.subviews { | ||
| patchSplitViews(in: subview) | ||
| } | ||
| } |
There was a problem hiding this comment.
Limit divider hiding to the outer navigation split view.
patchSplitViews(in: window.contentView) walks every NSSplitView in the window, not just the NavigationSplitView container. That will also clear dividers in nested split-based content, which is a much broader UI change than hiding the single sidebar divider.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/ContentView.swift` around lines 15598 - 15616, The current
patchSplitViews(in:) walks every NSSplitView in the window and clears dividers
globally; instead limit the change to the outer NavigationSplitView container:
update hideDividers() to locate only the top-level split view that represents
the navigation container (search window.contentView?.subviews for the
NSSplitView that is the NavigationSplitView host, e.g. the direct child
corresponding to the app's sidebar split) and call a new patch function only on
that single split view; keep patchSplitViews(in:) but rename/use it to operate
on a single NSSplitView (or add a patchNavigationSplitView(in:)) so nested
split-based content is not modified. Ensure you reference hideDividers(),
patchSplitViews(in:), NavigationSplitView and window.contentView when making the
change.
There was a problem hiding this comment.
Noted — in practice the only NSSplitView in the window tree on macOS 26 is the NavigationSplitView's, so patchSplitViews has one target by construction. If we add another split view later we'll scope the walk to the NavigationSplitView ancestor.
There was a problem hiding this comment.
@rodchristiansen Acknowledged — thanks for the context. Given macOS 26 only installs the NavigationSplitView’s NSSplitView in this window, the current recursive walk is effectively scoped to that single split and is fine as-is. No change requested. If helpful down the road, a tiny DEBUG-only check that logs when more than one NSSplitView is found could catch future regressions early, but we can leave this as-is for now.
✏️ Learnings added
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:15597-15615
Timestamp: 2026-04-14T20:05:50.303Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — On macOS 26 in this app, the only NSSplitView in the window’s view tree is the one owned by NavigationSplitView. Therefore SplitViewDividerHiderView.hideDividers()/patchSplitViews(in:) effectively scope to that single split. Do not flag “global divider clearing” for this path unless additional split views are introduced later.
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: 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: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-04-14T20:01:09.378Z
Learning: Repo: manaflow-ai/cmux — macOS 26 UI: NavigationSplitView with SwiftUI .toolbar owns window chrome; WindowToolbarController is not used on macOS 26 and remains as an AppKit fallback. Notifications popover on macOS 26 is driven via AppDelegate.toggleNotificationsPopover posting cmux.toggleNotificationsPopover, which ContentView observes to present a SwiftUI popover.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T02:19:07.331Z
Learning: Applies to Sources/**/GhosttyTerminalView.swift : Mount `SurfaceSearchOverlay` from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`, to maintain proper layering during split/workspace churn
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: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-25T04:48:00.216Z
Learning: Applies to **/GhosttyTerminalView.swift : `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers. Portal-hosted terminal views can sit above SwiftUI during split/workspace churn.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T02:19:07.331Z
Learning: Applies to Sources/**/ContentView.swift : In `TabItemView` in `ContentView.swift`, use `Equatable` conformance + `.equatable()` to skip body re-evaluation during typing. Do not add `EnvironmentObject`, `ObservedObject` (besides `tab`), or `Binding` properties without updating the `==` function. Do not remove `.equatable()` from the ForEach call site. Do not read `tabManager` or `notificationStore` in the body; use precomputed `let` parameters instead
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-25T04:48:00.216Z
Learning: Applies to **/ContentView.swift : In `TabItemView` in `ContentView.swift`: uses `Equatable` conformance + `.equatable()` to skip body re-evaluation during typing. Do not add `EnvironmentObject`, `ObservedObject` (besides `tab`), or `Binding` properties without updating the `==` function. Do not remove `.equatable()` from the ForEach call site. Do not read `tabManager` or `notificationStore` in the body; use precomputed `let` parameters instead.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-04T00:58:31.755Z
Learning: Applies to **/ContentView.swift : In `TabItemView` in `ContentView.swift`: uses `Equatable` conformance + `.equatable()` to skip body re-evaluation during typing. Do not add `EnvironmentObject`, `ObservedObject` (besides `tab`), or `Binding` properties without updating the `==` function. Do not remove `.equatable()` from the ForEach call site. Do not read `tabManager` or `notificationStore` in the body; use precomputed `let` parameters instead.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-05T03:20:48.079Z
Learning: Applies to **/*TerminalView.swift : Terminal find layering contract: SurfaceSearchOverlay must be mounted from GhosttySurfaceScrollView in Sources/GhosttyTerminalView.swift (AppKit portal layer), not from SwiftUI panel containers
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: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T02:19:07.331Z
Learning: Applies to Sources/**/TerminalWindowPortal.swift : In `WindowTerminalHostView.hitTest()` in `TerminalWindowPortal.swift`, keep all divider/sidebar/drag routing gated to pointer events only. Do not add work outside the `isPointerEvent` guard to avoid typing latency
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-25T04:48:00.216Z
Learning: Applies to **/TerminalWindowPortal.swift : In `WindowTerminalHostView.hitTest()` in `TerminalWindowPortal.swift`: all divider/sidebar/drag routing must be gated to pointer events only. Do not add work outside the `isPointerEvent` guard as this path is called on every event including keyboard input.
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: Znboston
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-26T19:00:12.434Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — In `moveWorkspaceOutOfGroup(_:toSidebarIndex:)`, do NOT call `syncUngroupedSidebarOrder()` after inserting into `sidebarOrder`. That helper derives order from the `tabs` array insertion index, which is unrelated to the user's drag-drop target position. `sidebarOrder` is the source of truth for sidebar rendering; the raw insertion at the resolved sidebar index is correct and sufficient. Calling `syncUngroupedSidebarOrder()` would overwrite the intended drop position with stale `tabs` order, causing a snap-back regression.
Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 2505
File: Sources/AppDelegate.swift:4910-4918
Timestamp: 2026-04-06T02:02:45.662Z
Learning: Repo: manaflow-ai/cmux — File: Sources/AppDelegate.swift — Keyboard repair pattern: In AppDelegate.repairFocusedTerminalKeyboardRoutingIfNeeded(window:event:), determine whether to repair focus by asking the focused terminal’s hosted view if the current first responder already matches that panel’s preferred keyboard target. Implemented via hostedView.responderMatchesPreferredKeyboardFocus(responder) inside responderNeedsFocusedTerminalKeyRepair(_:in:hostedView:). This covers same-window drift to a different Ghostty surface without comparing workspace/panel IDs. Verified by test cmuxTests/AppDelegateShortcutRoutingTests.swift::testWindowSendEventRepairsVisibleSameWindowResponderDriftForFocusedTerminalTyping.
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: 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}.
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: 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: 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: 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: 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: 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: 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: 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: 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: debgotwired
Repo: manaflow-ai/cmux PR: 1149
File: Sources/ContentView.swift:3977-3978
Timestamp: 2026-03-10T09:33:29.814Z
Learning: In manaflow-ai/cmux, for Sources/ContentView.swift, do not modify the sync-seeding behavior of scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:). It is shared infrastructure across all submenus. Treat brief initial flashes as consistent with existing submenus. Any UX improvement (for example making seeding synchronous on a forced corpus refresh) should not be implemented in feature-scoped PRs; instead implement and test such UX changes globally in a dedicated follow-up PR.
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: 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.
Rebased onto current upstream main (resolves PR manaflow-ai#2647 merge conflicts). Summary of the Liquid Glass pass, all gated on #available(macOS 26.0, *): - NavigationSplitView with native glass sidebar and system titlebar - SwiftUI .toolbar with .accessoryBarAction style for bell + new-tab; pane actions (terminal/browser/split) as a ControlGroup - Notifications popover via SwiftUI .popover, scoped to the owning window via a windowId-tagged NotificationCenter broadcast - Unread notification badge on the toolbar bell button - 8pt external padding around terminal detail content; 6pt internal scroll inset so text stays within the safe zone - Leading corners rounded at AppKit portal level when sidebar is visible - SplitViewDividerHider clears NSSplitView divider color - Tab bar collapses to 0 height on macOS 26 (hides for single-tab panes once the matching bonsplit change lands) - System handles app icon appearance on macOS 26 (AppIcon.icon now wired into Resources with folder.iconcomposer.icon type so actool emits Aqua/Dark/Tintable variants) - Legacy titlebar accessory controllers and custom titlebar overlay skipped on macOS 26 - Toggle Sidebar default shortcut changed to ⌃⌘S (system standard) - System sidebar chevron + injected tracking separator stripped Rebase notes for reviewers: - Squashed the original 10-commit PR into one commit while rebasing onto current manaflow-ai/main. No behavior change vs prior tip. - Upstream refactor "Split AppDelegate support code" reconciled by taking the new file layout; Liquid Glass edits move with it. - Submodule vendor/bonsplit pinned at upstream SHA. The single-tab tab bar hiding needs a bonsplit-side change; tracked separately. Known limitations (unchanged from prior tip): - Terminal right-side corner rounding: CAMetalLayer ignores Core Animation clipping; deferred to follow-up. - Light mode sidebar has a subtle glass tint from NavigationSplitView, matching Finder/Mail.
68af6e0 to
850d29c
Compare
|
Rebased onto current Reconciliation notes:
Local compile ( |
Two linked fixes for the macOS 26 NavigationSplitView toolbar:
1) Keep our own sidebar-toggle button in the leading toolbar position
unconditionally (both states) instead of only when the sidebar is
hidden. The system-injected toggle was the only affordance to hide
the sidebar when it was visible, and we want to strip that everywhere.
2) Strengthen SystemSidebarToggleStripperView so the system toggle does
not leak through when the window is narrow enough to push toolbar
items into the overflow popover:
- Also match items by user-facing label ("Hide Sidebar" / "Show
Sidebar"), since NavigationSplitView re-injects the toggle into
the overflow with an opaque identifier that does not contain
"toggleSidebar".
- Observe NSToolbar.willAddItemNotification and re-strip on every
item insertion, so SwiftUI cannot silently re-add the toggle
after our initial strip.
- Observe NSWindow.didResizeNotification since window resize is
what triggers the re-layout that shoves items into overflow.
Before: narrowing the window revealed a `»` overflow chevron whose
popover contained a system "Hide Sidebar" button even though we had
stripped the same item from the main toolbar at launch.
After: the overflow popover no longer contains any sidebar toggle in
either state, and the leading sidebar button remains available at all
window widths.
There was a problem hiding this comment.
6 issues found across 10 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Sources/GhosttyTerminalView.swift">
<violation number="1" location="Sources/GhosttyTerminalView.swift:9599">
P2: Fixed 6pt inset is not clamped to current bounds, so transient tiny host sizes can produce zero/negative scroll/surface dimensions.</violation>
</file>
<file name="Sources/AppDelegate.swift">
<violation number="1" location="Sources/AppDelegate.swift:8645">
P2: macOS 26 notification toggle can still broadcast globally when no target window is resolved, causing multiple windows to toggle instead of only the intended one.</violation>
<violation number="2" location="Sources/AppDelegate.swift:8651">
P2: macOS 26 notifications popover toggle is SwiftUI-notification driven, but dismiss/isShown still query only the legacy titlebar accessory controller, causing stale state and potential no-op dismiss behavior.</violation>
</file>
<file name="Sources/ContentView.swift">
<violation number="1" location="Sources/ContentView.swift:14750">
P2: Sidebar-toggle stripping depends on hardcoded English toolbar labels, which can fail on localized macOS and leave duplicate sidebar controls.</violation>
<violation number="2" location="Sources/ContentView.swift:14825">
P2: Divider-hiding logic patches every NSSplitView in the window, not just NavigationSplitView, risking unintended UI regressions in unrelated split-based views.</violation>
</file>
<file name="Sources/TerminalWindowPortal.swift">
<violation number="1" location="Sources/TerminalWindowPortal.swift:1507">
P2: Sidebar detection via `targetFrame.origin.x > 20` is not specific to sidebar visibility and can incorrectly round split panes when sidebar is hidden.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
The prior overflow fix matched system-injected toggle items by label
("Hide Sidebar" / "Show Sidebar"), which breaks on localized macOS
builds. Replace the label check with an action-selector check against
toggleSidebar: — the same selector AppKit uses internally for both
NSSplitViewController.toggleSidebar and NavigationSplitView's injected
item, regardless of locale.
Identifier-based matching (items whose itemIdentifier contains
"toggleSidebar" or "splitViewSeparator") remains as the primary path
for items that carry a stable identifier.
Flagged by cursor[bot] and cubic-dev-ai[bot] on PR manaflow-ai#2647.
Seven follow-ups flagged by cursor[bot] and cubic-dev-ai[bot] on 4/22–4/23:
1. (HIGH) Restore right sidebar panel on macOS 26 — wrap NavigationSplitView
detail with terminalContentWithRightSidebarPanel so File Explorer and
Session Index remain reachable.
2. Bump vendor/bonsplit to cfff8a9 ("Hide tab bar when tabBarHeight is 0")
so the macOS 26 single-tab pane actually collapses the bar instead of
rendering at zero height; switch the submodule URL to rodchristiansen/
bonsplit (same change already on main) since cfff8a9 isn't on a tracked
branch of manaflow-ai/bonsplit. Drop tabTitleFontSize references from
Workspace.swift to match the new bonsplit Appearance API.
3. Clamp the macOS 26 6pt scroll-view inset to max(0, bounds-inset) and
skip it entirely for sub-16pt host bounds so transient tiny layouts
can't produce zero/negative scroll/surface dimensions.
4. AppDelegate.toggleNotificationsPopover early-returns when no
targetWindowId resolves (was broadcasting globally to every observer).
5. dismissNotificationsPopoverIfShown / isNotificationsPopoverShown route
through a per-window state set on macOS 26 so they don't return stale
results from the legacy titlebar accessory controller.
6. SplitViewDividerHider walks up to the nearest enclosing NSSplitView
instead of recursing the whole window.contentView, so unrelated
split-based UI can't be silently mutated.
7. TerminalWindowPortal corner-radius logic asks the enclosing NSSplitView
whether its sidebar subview is collapsed instead of using the
origin.x > 20 frame heuristic; falls back to the heuristic when no
ancestor exists.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c89a423. Configure here.
| [submodule "vendor/bonsplit"] | ||
| path = vendor/bonsplit | ||
| url = https://github.com/manaflow-ai/bonsplit.git | ||
| url = https://github.com/rodchristiansen/bonsplit.git |
There was a problem hiding this comment.
Bonsplit submodule URL changed to personal fork
Medium Severity
The bonsplit submodule URL was changed from the organization repo (manaflow-ai/bonsplit.git) to a personal fork (rodchristiansen/bonsplit.git). Merging this to main would make the project depend on a personal fork rather than the canonical organization repository, creating a bus-factor risk and potential access issues for other contributors.
Reviewed by Cursor Bugbot for commit c89a423. Configure here.
There was a problem hiding this comment.
2 issues found across 7 files (changes from recent commits).
You’re at about 92% of the monthly review limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Sources/Workspace.swift">
<violation number="1" location="Sources/Workspace.swift:6756">
P2: Removing the `surfaceTabBarFontSize` plumbing makes the tab-title font-size setting a no-op for new workspaces and config/theme refreshes.</violation>
</file>
<file name=".gitmodules">
<violation number="1" location=".gitmodules:10">
P2: Point this submodule back to the organization-owned bonsplit repository to avoid relying on a personal fork that can disappear or drift unexpectedly.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| @@ -6752,8 +6752,7 @@ final class Workspace: Identifiable, ObservableObject { | |||
| private static func bonsplitAppearance(from config: GhosttyConfig) -> BonsplitConfiguration.Appearance { | |||
There was a problem hiding this comment.
P2: Removing the surfaceTabBarFontSize plumbing makes the tab-title font-size setting a no-op for new workspaces and config/theme refreshes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Workspace.swift, line 6756:
<comment>Removing the `surfaceTabBarFontSize` plumbing makes the tab-title font-size setting a no-op for new workspaces and config/theme refreshes.</comment>
<file context>
@@ -6752,8 +6752,7 @@ final class Workspace: Identifiable, ObservableObject {
- backgroundOpacity: config.backgroundOpacity,
- tabTitleFontSize: config.surfaceTabBarFontSize
+ backgroundOpacity: config.backgroundOpacity
)
}
</file context>
| [submodule "vendor/bonsplit"] | ||
| path = vendor/bonsplit | ||
| url = https://github.com/manaflow-ai/bonsplit.git | ||
| url = https://github.com/rodchristiansen/bonsplit.git |
There was a problem hiding this comment.
P2: Point this submodule back to the organization-owned bonsplit repository to avoid relying on a personal fork that can disappear or drift unexpectedly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .gitmodules, line 10:
<comment>Point this submodule back to the organization-owned bonsplit repository to avoid relying on a personal fork that can disappear or drift unexpectedly.</comment>
<file context>
@@ -7,4 +7,4 @@
[submodule "vendor/bonsplit"]
path = vendor/bonsplit
- url = https://github.com/manaflow-ai/bonsplit.git
+ url = https://github.com/rodchristiansen/bonsplit.git
</file context>
| url = https://github.com/rodchristiansen/bonsplit.git | |
| url = https://github.com/manaflow-ai/bonsplit.git |
Two follow-ups after on-device verification of c89a423: 1. Vertical line at the sidebar boundary returned because the prior ancestor walk in SplitViewDividerHider doesn't find the NavigationSplitView's NSSplitView — the hider is mounted as a `.background`, which sits as a sibling in SwiftUI's hosting hierarchy rather than as a descendant. Walk down from window.contentView and patch only the topmost (first) NSSplitView found, which is reliably the NavigationSplitView. Still respects cubic[bot]'s concern (no recursive patching of every split view in the window). 2. Sidebar-toggle button placement now follows Apple HIG: when the sidebar is shown, the toggle sits inside the sidebar column's leading edge (matching Mail/Notes); when collapsed, it appears in the leading toolbar position. Achieved by removing the custom ToolbarItem(placement: .navigation) toggle and the SystemSidebarToggleStripper — NavigationSplitView's native toggle provides the correct placement automatically. The columnVisibility binding wires the native toggle into our SidebarState.
The previous attempt to rely entirely on NavigationSplitView's auto-injected sidebar toggle backfired: on macOS 26 the system toggle gets pushed into the toolbar overflow popover when the sidebar is collapsed, leaving no visible button on the leading edge. Reinstate the custom ToolbarItem(placement: .navigation) sidebar toggle so there's always a visible leading-edge button, plus SystemSidebarToggleStripper to remove the system duplicate. The stripper's match rules (action == toggleSidebar: or itemIdentifier contains "toggleSidebar"/"splitViewSeparator") only match AppKit-native items; our SwiftUI button uses neither, so it survives.
Mail/Notes pattern (Apple HIG): when the sidebar is shown, the toggle button sits at the top of the sidebar column itself; when the sidebar is hidden, the toggle appears on the leading edge of the toolbar so the user can re-open it. Implementation: - Add a borderless sidebar.left button at the top of the sidebar VStack in the macOS 26 NavigationSplitView. It's only visible when the sidebar is shown (because the whole column is collapsed otherwise). - Make the ToolbarItem(placement: .navigation) toggle conditional on sidebarState.isVisible == false. So exactly one toggle is visible in every state, in the right place, and the bottom-of-stack SystemSidebarToggleStripper continues to drop any system duplicate.
Putting .toolbar on the sidebar view (rather than the NavigationSplitView root) places the leading-navigation item at title-bar y-level *inside* the sidebar column — Apple HIG / Mail / Notes pattern. NavigationSplitView auto-relocates the same item to the leading edge of the detail toolbar when the sidebar is collapsed, so a single ToolbarItem covers both states. The SystemSidebarToggleStripper continues to drop any auto-injected duplicate.
User-observed behavior on macOS 26: the system-injected toggle DOES place itself correctly (inside the sidebar column at title-bar y-level when expanded; leading edge of the detail toolbar when collapsed). The previous SystemSidebarToggleStripper was killing the *good* placement during sidebar drag, leaving only our custom (wrong-position) toggle. Drop the stripper and the custom ToolbarItem entirely; let NavigationSplitView handle sidebar-toggle placement on macOS 26. The columnVisibility binding still routes the system toggle through SidebarState. Reverts both 9db1c3a (system-toggle stripper + custom leading toggle) and 894ec52 (re-introduction of the stripper after the prior trial of this approach), now that the sidebar-internal trial of dcffaf9/58cbc7b8 showed the system toggle was already placing itself in the right spot. If the overflow regression returns when the window is narrowed, we'll solve it with toolbar-item priority instead of stripping.
…e when hidden When the sidebar is shown, NavigationSplitView's auto-injected toggle places itself correctly inside the sidebar column at title-bar y-level. When the sidebar is hidden, the same toggle gets shoved into the toolbar overflow popover instead of the leading edge. Gate both the SystemSidebarToggleStripper and the custom ToolbarItem(placement: .navigation) on `!sidebarState.isVisible`. Result: * Sidebar shown: only the system in-sidebar toggle is visible, in the right place. * Sidebar hidden: stripper kills the overflow duplicate, custom leading toolbar item is the user-visible affordance. Local-only commit (not pushing to PR while iterating).


Summary
.toolbarfor bell and new-tab buttons with.accessoryBarActionstyleKnown limitations
Test plan
🤖 Generated with Claude Code
Summary by cubic
Adopts macOS 26 Liquid Glass with a native
NavigationSplitViewsidebar, system titlebar, and a SwiftUI toolbar. Restores the right sidebar and uses the system sidebar toggle when visible, with a custom leading toolbar toggle when hidden to avoid toolbar-overflow issues.New Features
.popoverwith an unread badge and per-window scoping, and new tab; start the accessory controller on all versions for popover support but skip attaching legacy accessories on macOS 26.bonsplittab bar on macOS 26; change Toggle Sidebar to ⌃⌘S; subtle glass hovers on active tabs and sidebar footer buttons; includeAppIcon.iconin Resources and skip custom app‑icon switching on macOS 26.Bug Fixes
NSSplitViewdivider; base corner rounding on actual sidebar visibility viaNSSplitView; clamp terminal insets to avoid invalid frames during animated resizes.Written for commit cdf6eb0. Summary will update on new commits.
Summary by CodeRabbit
New Features
Improvements
Other
Note
Medium Risk
Medium risk because it changes core window/layout behavior and notification popover handling with macOS-version branching (including some AppKit toolbar/view hierarchy manipulation) that could regress multi-window UI interactions.
Overview
On macOS 26+, the main window layout switches to
NavigationSplitViewto get native Liquid Glass sidebar/titlebar behavior, adds an 8pt padded detail area, disables custom titlebar overlays/accessories, and relies on system safe areas/window dragging instead of the previous "immovable window" approach.Notifications UI is reworked for macOS 26: the bell button becomes a SwiftUI toolbar item with a per-window SwiftUI popover, plus new AppDelegate notification broadcasts/state tracking so menu/command invocations toggle/dismiss the correct window’s popover.
Additional macOS 26 polish includes Liquid Glass hover/active effects in the sidebar, hiding bonsplit’s single-tab tab bar, insetting terminal scroll content for rounded-corner safe zones, rounding terminal leading corners based on actual split-view sidebar visibility, and avoiding manual
WindowGlassEffectinsertion so it doesn’t fight system glass. Separately, the default Toggle Sidebar shortcut changes and project wiring/resources are updated (including bundlingAppIcon.iconand updating thevendor/bonsplitsubmodule URL).Reviewed by Cursor Bugbot for commit cdf6eb0. Bugbot is set up for automated code reviews on this repo. Configure here.