Skip to content

Adopt macOS 26 Liquid Glass design#2647

Open
rodchristiansen wants to merge 5 commits intomanaflow-ai:mainfrom
rodchristiansen:liquid-glass-design
Open

Adopt macOS 26 Liquid Glass design#2647
rodchristiansen wants to merge 5 commits intomanaflow-ai:mainfrom
rodchristiansen:liquid-glass-design

Conversation

@rodchristiansen
Copy link
Copy Markdown

@rodchristiansen rodchristiansen commented Apr 6, 2026

Summary

  • NavigationSplitView with native glass sidebar and system titlebar on macOS 26
  • SwiftUI .toolbar for bell and new-tab buttons with .accessoryBarAction style
  • 8pt external padding around terminal detail content for Liquid Glass spacing
  • 6pt internal terminal content inset keeps text within safe zone
  • Leading corners (top-left, bottom-left) rounded at AppKit portal level when sidebar visible
  • Bonsplit tab bar hidden for single-tab panes on macOS 26
  • Old titlebar accessory controllers and custom titlebar overlay skipped on macOS 26
  • App icon auto-set skipped on macOS 26 (system handles dark variants)

Known limitations

  • Terminal corner rounding (right side): CAMetalLayer ignores Core Animation clipping (cornerRadius, masksToBounds, layer.mask on parent views). Left corners work because the sidebar glass provides contrast. Full corner rounding deferred to follow-up.
  • Light mode sidebar tint: NavigationSplitView's glass material adds a subtle gray tint in light mode. This matches native apps (Finder, Mail) and is the expected system behavior.

Test plan

  • Verify glass sidebar renders correctly in light and dark mode
  • Verify toolbar buttons (bell, +) appear and function
  • Verify 8pt padding around terminal on all sides
  • Verify terminal text has internal padding (doesn't touch edges)
  • Verify left corners are rounded when sidebar is visible
  • Toggle sidebar visibility — padding and corners adapt
  • Test fullscreen transitions
  • Test split panes and workspace switching
  • Test drag/drop reordering in sidebar

🤖 Generated with Claude Code


Summary by cubic

Adopts macOS 26 Liquid Glass with NavigationSplitView, a native system titlebar, and a SwiftUI toolbar for pane actions, notifications (with unread badge), and new tab. Refines terminal spacing/corners, hides split dividers, fills sidebar row width, and removes legacy titlebar/sidebar UI.

  • New Features

    • Use NavigationSplitView on macOS 26 for a native glass sidebar and safe-area spacing; persist user-resized width; make sidebar rows fill full width; remove the custom titlebar overlay/separator and the system sidebar chevron.
    • Add a SwiftUI toolbar: pane actions (new terminal, new browser, split right/down), notifications via a SwiftUI .popover with an unread badge and a NotificationCenter toggle, and new tab; localized labels and accessibility IDs; start the accessory controller on all versions for popover support, but skip attaching legacy titlebar accessories on macOS 26.
    • Terminal: 8pt outer padding and a 6pt internal inset on leading/top edges; 16pt leading corner rounding when the sidebar is visible; hide NSSplitView dividers and prevent corner-radius flicker.
    • Visual/behavior: subtle Liquid Glass on sidebar footer hover and the active tab; enable native window dragging on macOS 26; hide bonsplit tab bar and split buttons for single-tab panes; hide legacy sidebar controls on macOS 26; let the system handle app icon variants.
  • Dependencies

    • Update bonsplit submodule.

Written for commit eba5d8c. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added toolbar controls for sidebar, notifications, and creating a new workspace.
    • Notifications now open in a dedicated popover.
  • Improvements

    • Modern split-view sidebar/navigation with constrained column widths and hidden dividers on newer macOS.
    • Updated titlebar/window appearance and movability on newer macOS.
    • Reduced terminal top padding, refined corner rounding, divider visuals, and scroll insets.
    • Subtle hover glass effects for tabs and footer buttons.

Note

Medium Risk
Medium risk due to widespread window/layout changes gated on macOS 26 (safe area, titlebar, sidebar/terminal geometry) plus a new private-selector hack to hide NSSplitView dividers that could regress on OS updates.

Overview
Updates macOS 26 behavior to adopt native Liquid Glass: switches the main layout to NavigationSplitView, relies on system safe areas/titlebar rendering, and replaces legacy titlebar overlay/accessory UI with SwiftUI .toolbar controls (including a SwiftUI notifications .popover toggled via NotificationCenter).

Adjusts window/terminal presentation on macOS 26 (movability, transparent titlebar handling, disables custom NSGlassEffectView insertion, adds outer/inner padding and leading-corner rounding when the sidebar is present, and hides split dividers via an NSSplitView patch), while keeping pre-26 code paths using the existing custom glass/titlebar implementations. Also adds subtle glass hover/active effects in the sidebar, hides the bonsplit tab bar for single-tab panes on macOS 26, and skips custom app icon appearance overrides on macOS 26.

Reviewed by Cursor Bugbot for commit eba5d8c. Bugbot is set up for automated code reviews on this repo. Configure here.

NavigationSplitView with native glass sidebar, system titlebar, and
SwiftUI toolbar on macOS 26. Terminal detail content gets 8pt external
padding, 6pt internal content inset, and leading corner rounding (16pt)
when the sidebar is visible.

Key changes:
- NavigationSplitView wraps sidebar + detail for native glass treatment
- System titlebar (titlebarAppearsTransparent = false) with SwiftUI
  .toolbar for bell and new-tab buttons
- SplitViewDividerHider clears NSSplitView divider color
- Terminal content inset (6pt) keeps text within safe zone
- Leading corners rounded at AppKit portal level when sidebar present
- Bonsplit tab bar hidden for single-tab panes on macOS 26
- App icon auto-set skipped on macOS 26 (system handles dark variants)
- Old titlebar accessory controllers skipped on macOS 26
Copilot AI review requested due to automatic review settings April 6, 2026 21:14
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 6, 2026

@rodchristiansen is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 6, 2026

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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

macOS 26+ gated UI changes: system safe-area integration, NavigationSplitView for content/sidebar, toolbar/ titlebar behavior and accessory/popover flow updated, terminal hosting/scroll geometry and layer corner rounding adjusted, bonsplit submodule bumped.

Changes

Cohort / File(s) Summary
Window & Titlebar
Sources/AppDelegate.swift, Sources/WindowToolbarController.swift
Gated window/titlebar properties for macOS 26+ (titlebarAppearsTransparent, separator style, isMovable), toolbar style changed to .unified and three new toolbar items (sidebar, notifications, new workspace) added with handlers; accessory controller start unconditional, legacy attach skipped on macOS 26+.
Content layout & Sidebar
Sources/ContentView.swift
macOS 26+ switches to NavigationSplitView (bound columnVisibility, constrained split widths, new toolbar items), removes custom titlebar overlay/scrim/backdrop on macOS 26+, adds split-divider hider and hover glass modifiers; sidebar/tab/backdrop behavior version-gated.
Terminal hosting & Scrolling
Sources/GhosttyTerminalView.swift, Sources/TerminalWindowPortal.swift
Scroll view frame calc now applies a 6‑point inset on leading/top for macOS 26+; portal geometry update sets hosted layer cornerRadius and maskedCorners when sidebar is left-present (rounded leading corners).
App lifecycle & Appearance
Sources/cmuxApp.swift, Sources/Workspace.swift
Extracted mainWindowContent view, macOS 26+ guard to skip app icon overrides, bonsplit appearance now can hide tab bar / split buttons on macOS 26+.
Accessory / Popover & Visibility State
Sources/Update/UpdateTitlebarAccessory.swift, Sources/ContentView.swift
NotificationsPopoverView elevated to module scope; ContentView adds isNotificationsPopoverPresented and macOS 26+ popover flow (AppDelegate posts toggle notification and ContentView presents popover) while legacy accessory toggle remains for older macOS.
Vendored dependency
vendor/bonsplit
Updated submodule pointer to a new commit SHA (vendor version bump).

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 received
        Content->>Content: set isNotificationsPopoverPresented = true
        Content->>User: present popover
    else macOS < 26
        AppDelegate->>Accessory: titlebarAccessoryController.toggleNotificationsPopover(animated:)
        Accessory->>User: toggle accessory popover
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I hopped through code with careful paws,
Safe edges set and split-view laws,
Toolbars, popovers, corners bright,
A tiny hop for UI delight,
macOS 26 — springtime claws ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Adopt macOS 26 Liquid Glass design' clearly and concisely summarizes the main change—migrating to macOS 26's Liquid Glass design system. It directly reflects the core objective of the PR.
Description check ✅ Passed PR description is comprehensive with clear summary, test plan, known limitations, and AI-generated summaries, though test plan items lack completion checkmarks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 6, 2026

Greptile Summary

This PR adopts macOS 26 Liquid Glass by switching to NavigationSplitView, adding native SwiftUI .toolbar buttons, rounding terminal leading corners, and hiding the bonsplit tab bar — but two P1 regressions need fixing before merge.

  • Bell button broken on macOS 26: attachUpdateAccessory(to:) returns early, so UpdateTitlebarAccessoryController never starts; toggleNotificationsPopover guards on !controllers.isEmpty and silently returns. Clicking the bell does nothing.
  • Tab bar unconditionally hidden: tabBarHeight = 0 is set for all panes regardless of tab count, not just single-tab panes as described, removing in-pane tab switching in split workspaces.

Confidence Score: 4/5

Not 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

Filename Overview
Sources/AppDelegate.swift Skips titlebarAccessoryController.start() on macOS 26, making the toolbar bell button a silent no-op via empty controlsControllers
Sources/Workspace.swift Unconditionally sets tabBarHeight=0 on macOS 26 for all panes, breaking multi-tab pane navigation in split workspaces
Sources/ContentView.swift Adopts NavigationSplitView for glass sidebar; sidebar width changes from native divider drag not persisted back to sidebarState
Sources/GhosttyTerminalView.swift Adds 6pt symmetric inset on macOS 26 but only leading corners are rounded, unnecessarily shrinking right/bottom terminal area
Sources/TerminalWindowPortal.swift Rounds leading corners when targetFrame.origin.x > 20; magic-number heuristic for sidebar presence is fragile
Sources/WindowToolbarController.swift Adds sidebar toggle, notifications, and new-tab toolbar items on macOS 26 with unified toolbar style; looks correct
Sources/cmuxApp.swift Skips custom app icon on macOS 26 and uses system safe area in MainWindowHostingView; no issues

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]
Loading

Reviews (1): Last reviewed commit: "Adopt macOS 26 Liquid Glass design" | Re-trigger Greptile

Comment on lines 10152 to 10160
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)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Bell button is a no-op on macOS 26

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

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

Comment on lines +6781 to +6788
let hideTabBar: Bool
if #available(macOS 26.0, *) {
hideTabBar = true
} else {
hideTabBar = false
}
return BonsplitConfiguration.Appearance(
tabBarHeight: hideTabBar ? 0 : 33,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Comment on lines +2884 to +2924
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")
}
}
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Comment on lines +9120 to +9127
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Comment on lines +1507 to +1516
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]
: []
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 NavigationSplitView layout 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
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +2906 to +2921
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")
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

.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.

Copilot uses AI. Check for mistakes.
Comment on lines +15467 to +15468
// Override the divider color to clear
splitView.setValue(NSColor.clear, forKey: "dividerColor")
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// 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)
}

Copilot uses AI. Check for mistakes.
Comment on lines +15430 to +15446
/// 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

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +3493 to 3507
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)

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +15415 to +15429
/// 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())
}
}
}

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
/// 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())
}
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +6781 to 6789
let hideTabBar: Bool
if #available(macOS 26.0, *) {
hideTabBar = true
} else {
hideTabBar = false
}
return BonsplitConfiguration.Appearance(
tabBarHeight: hideTabBar ? 0 : 33,
splitButtonTooltips: Self.currentSplitButtonTooltips(),
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 CATransaction with 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() and toggleNotificationsPopover(animated:) return Void, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 179b16c and 10f2d7a.

📒 Files selected for processing (8)
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/WindowToolbarController.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • vendor/bonsplit

Comment on lines +2901 to +2922
.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")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +3487 to 3506
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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.swift

Repository: 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.swift

Repository: 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 2

Repository: 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.

Comment on lines +15458 to +15472
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
// Override the divider color to clear
splitView.setValue(NSColor.clear, forKey: "dividerColor")
}
for subview in view.subviews {
patchSplitViews(in: subview)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does AppKit's NSSplitViewpublicly expose adividerColorproperty orsetDividerColor:selector on macOS, and issetValue(_:forKey: "dividerColor") a supported customization path?

💡 Result:

  • Public API: On macOS, NSSplitView publicly exposes dividerColor only as a getter (-dividerColor / var dividerColor: NSColor { get }). Apple’s docs explicitly describe it as something you override to change divider drawing, and the header is shown as readonly. [1], [2]
  • setDividerColor: selector: Apple’s public NSSplitView API does not document or declare a setDividerColor: setter (i.e., dividerColor is not a public settable property in AppKit). [1], [2]
  • setValue(_:forKey: "dividerColor"): Using KVC to force-set "dividerColor" is not a supported customization path (it’s not part of the documented API contract, can break across macOS releases, and may fail depending on implementation details). The supported approaches are overriding dividerColor and/or drawDivider(in:). [1], [3]

Sources:
[1] Apple “NSSplitView Class Reference” PDF (Leopard ADC mirror)
[2] Header excerpt showing @property (readonly, copy) NSColor *dividerColor (blog quoting NSSplitView.h)
[3] Stack Overflow discussions demonstrating the supported pattern of overriding dividerColor / drawDivider(in:) rather than setting it directly


Don't use KVC to set dividerColor; override it instead. Also, scope is too broad.

Line 15468's setValue(_:forKey: "dividerColor") attempts to set a read-only property via KVC. NSSplitView.dividerColor is documented as readonly in AppKit and has no public setter. This unsupported customization path will break across macOS versions and may silently fail or throw at runtime.

Additionally, line 15460 starts from window.contentView, so this recursively patches every NSSplitView in the window, not just the NavigationSplitView shell dividers.

Fix: Create a custom NSSplitView subclass and override dividerColor (to return .clear) or drawDivider(in:) (to skip drawing). Apply it only to the navigation split view that needs the change, not the entire window hierarchy.

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

In `@Sources/ContentView.swift` around lines 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.

Comment on lines +193 to +222
if #available(macOS 26.0, *) {
if itemIdentifier == sidebarToggleIdentifier {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.image = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: "Toggle Sidebar")
item.label = "Sidebar"
item.toolTip = "Toggle Sidebar"
item.target = self
item.action = #selector(toggleSidebarAction)
return item
}

if itemIdentifier == notificationsIdentifier {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.image = NSImage(systemSymbolName: "bell", accessibilityDescription: "Notifications")
item.label = "Notifications"
item.toolTip = "Show Notifications"
item.target = self
item.action = #selector(toggleNotificationsAction)
return item
}

if itemIdentifier == newTabIdentifier {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.image = NSImage(systemSymbolName: "plus", accessibilityDescription: "New Workspace")
item.label = "New Workspace"
item.toolTip = "New Workspace"
item.target = self
item.action = #selector(newTabAction)
return item
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

User-facing strings must be localized.

The toolbar item labels, tooltips, and accessibility descriptions are bare string literals. As per coding guidelines, all UI strings must use String(localized:defaultValue:).

🌐 Proposed fix for localization
 if itemIdentifier == sidebarToggleIdentifier {
     let item = NSToolbarItem(itemIdentifier: itemIdentifier)
-    item.image = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: "Toggle Sidebar")
-    item.label = "Sidebar"
-    item.toolTip = "Toggle Sidebar"
+    item.image = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: String(localized: "toolbar.sidebar.accessibilityDescription", defaultValue: "Toggle Sidebar"))
+    item.label = String(localized: "toolbar.sidebar.label", defaultValue: "Sidebar")
+    item.toolTip = String(localized: "toolbar.sidebar.tooltip", defaultValue: "Toggle Sidebar")
     item.target = self
     item.action = `#selector`(toggleSidebarAction)
     return item
 }

 if itemIdentifier == notificationsIdentifier {
     let item = NSToolbarItem(itemIdentifier: itemIdentifier)
-    item.image = NSImage(systemSymbolName: "bell", accessibilityDescription: "Notifications")
-    item.label = "Notifications"
-    item.toolTip = "Show Notifications"
+    item.image = NSImage(systemSymbolName: "bell", accessibilityDescription: String(localized: "toolbar.notifications.accessibilityDescription", defaultValue: "Notifications"))
+    item.label = String(localized: "toolbar.notifications.label", defaultValue: "Notifications")
+    item.toolTip = String(localized: "toolbar.notifications.tooltip", defaultValue: "Show Notifications")
     item.target = self
     item.action = `#selector`(toggleNotificationsAction)
     return item
 }

 if itemIdentifier == newTabIdentifier {
     let item = NSToolbarItem(itemIdentifier: itemIdentifier)
-    item.image = NSImage(systemSymbolName: "plus", accessibilityDescription: "New Workspace")
-    item.label = "New Workspace"
-    item.toolTip = "New Workspace"
+    item.image = NSImage(systemSymbolName: "plus", accessibilityDescription: String(localized: "toolbar.newWorkspace.accessibilityDescription", defaultValue: "New Workspace"))
+    item.label = String(localized: "toolbar.newWorkspace.label", defaultValue: "New Workspace")
+    item.toolTip = String(localized: "toolbar.newWorkspace.tooltip", defaultValue: "New Workspace")
     item.target = self
     item.action = `#selector`(newTabAction)
     return item
 }

Remember to add the new keys to 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/WindowToolbarController.swift` around lines 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.

Comment on lines +6781 to 6789
let hideTabBar: Bool
if #available(macOS 26.0, *) {
hideTabBar = true
} else {
hideTabBar = false
}
return BonsplitConfiguration.Appearance(
tabBarHeight: hideTabBar ? 0 : 33,
splitButtonTooltips: Self.currentSplitButtonTooltips(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid globally hiding the tab bar on macOS 26.

Line 6788 sets tabBarHeight to 0 for all panes on macOS 26, which hides tabs even when a pane has multiple tabs. The PR objective calls for hiding only single-tab panes, so this can regress multi-tab navigation.

💡 Suggested adjustment in this helper
-        let hideTabBar: Bool
-        if `#available`(macOS 26.0, *) {
-            hideTabBar = true
-        } else {
-            hideTabBar = false
-        }
+        // Keep global appearance stable here. Single-tab hiding should be applied
+        // at the pane/tab-rendering layer where tab counts are available.
+        let hideTabBar = false
         return BonsplitConfiguration.Appearance(
             tabBarHeight: hideTabBar ? 0 : 33,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 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.

- Fix bell button no-op: start titlebarAccessoryController on macOS 26
  so notifications popover infrastructure is available
- Add tab bar design comment: bonsplit shows tabs when pane.tabs > 1
- Replace dividerColor KVC with safe responds(to:)/perform selector
- Add accessibility labels to toolbar buttons (bell, new tab)
- Localize all WindowToolbarController toolbar item strings
- Add comment about NavigationSplitView width persistence limitation
- Move corner radius update inside CATransaction to prevent flicker
- Replace magic number 20 with named sidebarDetectionThreshold constant
- Remove unused TerminalBackgroundFill struct
- Fix SplitViewDividerHider doc comment and remove unused property
- Add comment explaining uniform 6pt content inset design
…macOS 26

- Fix bell button no-op: use SwiftUI .popover on macOS 26 instead of the
  broken AppKit titlebar accessory path; bridge keyboard shortcut via
  NotificationCenter
- Move pane action buttons (terminal, browser, split right/down) from
  bonsplit tab bar to native toolbar as a ControlGroup on macOS 26
- Hide bonsplit split buttons when tab bar is hidden (macOS 26)
- Hide old HiddenTitlebarSidebarControlsView on macOS 26
- Remove system sidebar toggle chevron (.toolbar(removing: .sidebarToggle))
- Fix asymmetric inset: only inset leading/top edges where corners are
  rounded, preserving terminal real estate on right/bottom
- Fix misleading comment about fullSizeContentView on macOS 26
- Update SplitViewDividerHider doc to match actual implementation
- Clarify hideTabBar comment: PaneContainerView conditionally shows tab
  bar for multi-tab panes
- Improve sidebar detection heuristic comment explaining why frame-based
  approach is used (AppKit view cannot access SwiftUI SidebarState)
- Add GeometryReader to persist sidebar width on macOS 26 when user
  drags the native NavigationSplitView divider
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
Sources/ContentView.swift (1)

15475-15520: ⚠️ Potential issue | 🟠 Major

Avoid private selector dispatch on NSSplitView.

Line 15511 uses setDividerColor: via runtime selector dispatch. This is not a public API—the macOS SDK documents only a dividerColor getter on NSSplitView and 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.swift only 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() returns Void, 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 though attachUpdateAccessory(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

📥 Commits

Reviewing files that changed from the base of the PR and between 10f2d7a and 65db12c.

📒 Files selected for processing (7)
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/Update/UpdateTitlebarAccessory.swift
  • Sources/WindowToolbarController.swift
  • Sources/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

Comment on lines +10168 to 10176
/// 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope and complete the macOS 26 notifications presenter.

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

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

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

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

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

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

Comment on lines +2967 to +2969
.onReceive(NotificationCenter.default.publisher(for: AppDelegate.toggleNotificationsPopoverNotification)) { _ in
isNotificationsPopoverPresented.toggle()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether toggleNotificationsPopoverNotification posts are window-scoped.
rg -n --type=swift -C3 'toggleNotificationsPopoverNotification|post\(\s*name:\s*AppDelegate\.toggleNotificationsPopoverNotification'

Repository: manaflow-ai/cmux

Length of output: 1580


Scope notifications popover toggles to the intended window.

Line 2967 subscribes to a global notification and unconditionally toggles local state. In multi-window sessions, posting one notification toggles all ContentView instances' popovers simultaneously.

Suggested fix
-                        .onReceive(NotificationCenter.default.publisher(for: AppDelegate.toggleNotificationsPopoverNotification)) { _ in
-                            isNotificationsPopoverPresented.toggle()
-                        }
+                        .onReceive(NotificationCenter.default.publisher(for: AppDelegate.toggleNotificationsPopoverNotification)) { notification in
+                            if let targetWindow = notification.object as? NSWindow {
+                                guard targetWindow === observedWindow else { return }
+                            }
+                            isNotificationsPopoverPresented.toggle()
+                        }

Also update AppDelegate.swift line 10173 to post the notification with the window object:

-            NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: nil)
+            NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: NSApp.keyWindow)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 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.

Comment on lines +9123 to +9130
if #available(macOS 26.0, *) {
let inset: CGFloat = 6
scrollFrame = CGRect(
x: bounds.origin.x + inset,
y: bounds.origin.y,
width: bounds.width - inset,
height: bounds.height - inset
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clamp inset geometry to avoid negative frame sizes during transient layout.

When bounds are smaller than the 6pt inset (e.g., during attach/detach or live transitions), this can produce negative width/height and unstable scroll geometry.

Proposed fix
         if `#available`(macOS 26.0, *) {
             let inset: CGFloat = 6
+            let xInset = min(inset, max(0, bounds.width))
+            let topInset = min(inset, max(0, bounds.height))
             scrollFrame = CGRect(
-                x: bounds.origin.x + inset,
+                x: bounds.origin.x + xInset,
                 y: bounds.origin.y,
-                width: bounds.width - inset,
-                height: bounds.height - inset
+                width: max(0, bounds.width - xInset),
+                height: max(0, bounds.height - topInset)
             )
         } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if #available(macOS 26.0, *) {
let inset: CGFloat = 6
scrollFrame = CGRect(
x: bounds.origin.x + inset,
y: bounds.origin.y,
width: bounds.width - inset,
height: bounds.height - inset
)
if `#available`(macOS 26.0, *) {
let inset: CGFloat = 6
let xInset = min(inset, max(0, bounds.width))
let topInset = min(inset, max(0, bounds.height))
scrollFrame = CGRect(
x: bounds.origin.x + xInset,
y: bounds.origin.y,
width: max(0, bounds.width - xInset),
height: max(0, bounds.height - topInset)
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 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.

…s on macOS 26

- Use flexible frame for sidebar in NavigationSplitView so rows extend full width
  instead of floating centered with gaps
- Skip attaching legacy TitlebarControlsAccessoryViewController on macOS 26 since
  NavigationSplitView toolbar provides its own controls
- Add unread notification badge to the new toolbar bell button
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit eba5d8c. Configure here.

if #available(macOS 26.0, *) {
NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: nil)
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Notifications popover state queries broken on macOS 26

High Severity

On macOS 26, dismissNotificationsPopoverIfShown() and isNotificationsPopoverShown() incorrectly delegate to titlebarAccessoryController. As the notifications popover is SwiftUI-managed on this OS, these functions don't reflect its actual state, breaking Escape key dismissal and allowing typing to leak into the terminal.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eba5d8c. Configure here.

appDelegate.openNewMainWindow(nil)
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WindowToolbarController is never instantiated — dead code

Low Severity

The WindowToolbarController is never instantiated, making its new macOS 26 toolbar items and their associated action methods dead code. The application's macOS 26 toolbar functionality is already provided by SwiftUI's .toolbar in ContentView.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eba5d8c. Configure here.

func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
if #available(macOS 26.0, *) {
NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: nil)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Notification popover broadcast toggles all windows simultaneously

Medium Severity

On macOS 26, toggleNotificationsPopover() posts toggleNotificationsPopoverNotification to NotificationCenter.default with no window-scoping information. Every ContentView instance subscribes via .onReceive and toggles its own isNotificationsPopoverPresented state. In a multi-window setup, triggering the notification shortcut or fullscreen controls bell button causes ALL windows' popovers to toggle simultaneously instead of only the active window's.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eba5d8c. Configure here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants