← Back to PRs

#23135: feat(macos): menubar UI consistency overhaul — sections, hover, icons, toggles

by apethree open 2026-02-22 02:09 View on GitHub →
app: macos size: XL
## Summary Complete redesign of the macOS menubar menu for visual consistency and stability. Every row across every section now uses the same custom `NSMenuItem.view` rendering pipeline, eliminating the mix of native AppKit rendering and custom SwiftUI views that caused misaligned indents, inconsistent row heights, and blue selection highlights in some sections but not others. ## What changed ### Selection / hover colour - **Before:** mixed — some rows showed AppKit's bold blue selection highlight, others nothing - **After:** all selectable rows use a soft grey rounded rect (`unemphasizedSelectedContentBackgroundColor`), matching macOS system menus (Spotlight, Control Centre, etc.) - Toggle rows (OpenClaw on/off header, all Quick Settings toggles) show **no** selection background — clicking fires the action without any visual highlight ### Sections & labels - **Before:** no section labels for Actions or App; sections had inconsistent leading indents - **After:** four clearly labelled sections — **Status**, **Activity**, **Quick Settings**, **Actions**, **App** — all with the same leading indent and 11 pt semibold grey headers - Separator between Context and Usage subsections removed (they belong to the same Activity section) ### Row layout & padding - **Before:** Quick Settings rows were ~22 pt tall (3 pt vertical padding); node/session rows were ~28–30 pt; native Actions rows used AppKit's state-column indent (~38 px from left edge) - **After:** all single-line rows are uniform height (5 pt vertical padding); multi-line rows keep their own sizing; every row starts at the same 12 pt leading indent ### Icons - **Before:** Quick Settings and Connected Devices icons were oversized (16 pt), creating visual inconsistency against the 13 pt icons in Actions; Connected Devices used `laptopcomputer.and.iphone` which rendered very large - **After:** all icons normalised to 13 pt inside 16×16 frames; Connected Devices uses `network` ### Section headers (Context / Usage) - **Before:** plain greyed-out text labels with no icon - **After:** leading SF Symbol icon (`list.bullet` / `bolt`) + primary-colour label + secondary count subtitle; wrapped in `HighlightedMenuItemHostView` so they get the same grey hover as other rows ### Exec Approvals dropdown - **Before:** native NSMenuItem items with AppKit checkmarks and blue selection - **After:** each option wrapped in `HighlightedMenuItemHostView` + `MenuPickerRow` (leading checkmark, grey hover, consistent indent) ### Active header (OpenClaw toggle) - **Before:** native SwiftUI `Toggle` with AppKit checkmark rendering - **After:** custom `MenuActiveHeaderView` with a proper capsule toggle switch (macOS-style); `ClickableMenuItemHostView` intercepts `mouseDown` to fire the action since `NSMenuItem.target/action` is ignored for custom views ### Cost usage submenu - **Before:** used `NSHostingController` + separate `NSHostingView` for the same view (two independent render trees causing blank/wrong chart) - **After:** single `NSHostingView` sized via `fittingSize` ### New Swift files - `MenuActiveHeaderView.swift` — `MenuActiveHeaderView` (capsule toggle header) + `ClickableMenuItemHostView` subclass - `MenuToggleViews.swift` — `CapsuleToggle`, `QuickSettingsRow`, `MenuSubMenuRow`, `MenuNativeItemRow`, `MenuPickerRow`, `MenuSectionLabelView` ### gitignore - Added `.vscode/` and `.claude/` to `.gitignore` ## After <img width="667" height="785" alt="after" src="https://github.com/user-attachments/assets/9d726bbf-9b3d-464d-9f2b-3e841177a899" /> ## Before <img width="339" height="701" alt="before" src="https://github.com/user-attachments/assets/d11aa1d1-1b54-4db7-af62-c7cffe1471e9" /> AI-assisted: - [x] AI-assisted change - [x] Tested locally (targeted) <h3>Greptile Summary</h3> Confidence Score: 5/5 This PR is safe to merge with minimal risk only UI changes. Replaces the mixed native-AppKit/SwiftUI rendering pipeline in the macOS menubar with a unified NSMenuItem.view-based system across all sections. Core infrastructure (MenuHighlightedHostView.swift, MenuActiveHeaderView.swift): - HighlightedMenuItemHostView changed from final to open class; adds showsHighlight: Bool flag to gate hover/selection drawing - Selection color changed from selectedContentBackgroundColor (blue) to unemphasizedSelectedContentBackgroundColor (soft grey) with tighter inset - New ClickableMenuItemHostView subclass intercepts mouseDown to fire actions (required because NSMenuItem.target/action is ignored for custom views); sets showsHighlight = false for toggle rows New views (MenuToggleViews.swift): - CapsuleToggle — animated macOS-style switch glyph - QuickSettingsRow / MenuSubMenuRow — toggle and picker rows with consistent 13 pt icons, 5 pt vertical padding, 12 pt leading indent - MenuNativeItemRow — wraps native SwiftUI Button/Menu items so Actions and App sections share the same indent and grey hover - MenuPickerRow — selection list row with leading checkmark for Exec Approvals submenu - MenuSectionLabelView — 11 pt semibold grey section headers Injection pipeline (MenuSessionsInjector.swift): - Adds injectActiveHeader, injectQuickSettings, injectActionsLabel, injectAppLabel, injectNativeItemViews — each idempotent via numeric tags (9_415_559 through 9_415_564) - injectNativeItemViews walks items after the Actions label, wraps submenu items with HighlightedMenuItemHostView and button items with ClickableMenuItemHostView + NSApp.sendAction forwarding - Voice Wake moved above Exec Approvals; separator between Context/Usage removed - buildExecApprovalsSubmenu replaced with HighlightedMenuItemHostView-wrapped MenuPickerRow items - buildCostUsageSubmenu simplified to single NSHostingView (was double-allocating via NSHostingController + NSHostingView) View cleanup (MenuContentView.swift, session/usage label views): - Removed Toggle, settingToggleRow, and related bindings from MenuContent.body — all now owned by the injector - Removed @Environment(\.menuItemHighlighted) color-inversion logic from SessionMenuLabelView, UsageMenuLabelView, SessionMenuPreviewView, NodesMenu views — all text now uses static .primary/.secondary

Most Similar PRs