Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
101c053
test: expect right sidebar titlebar toggle
austinywang Jul 13, 2026
b2ac6c3
Add right sidebar titlebar toggle
austinywang Jul 13, 2026
7b3eb4f
Fix right sidebar titlebar control regressions
austinywang Jul 13, 2026
f919d99
Fix titlebar accessory ownership
austinywang Jul 13, 2026
a8bca8b
Fix right sidebar toggle in minimal mode
austinywang Jul 13, 2026
6ae9941
Keep right sidebar toggle state live
austinywang Jul 13, 2026
a0dad6f
Keep right sidebar toggle feedback live
austinywang Jul 13, 2026
c7b7f6d
Fix right sidebar toggle imports
austinywang Jul 14, 2026
3b8cba7
Split trailing titlebar controls
austinywang Jul 14, 2026
3c6c9f5
Inject settings into trailing titlebar controls
austinywang Jul 14, 2026
191f18e
Keep right sidebar toggle in native titlebar
austinywang Jul 14, 2026
481eef9
Measure trailing titlebar control width
austinywang Jul 14, 2026
74bf9ee
Scope trailing titlebar layout updates
austinywang Jul 14, 2026
db126f5
test: preserve right sidebar titlebar content width
austinywang Jul 14, 2026
6068fc0
Fix right sidebar titlebar toggle layout
austinywang Jul 14, 2026
55cb19b
Place right sidebar toggle in window chrome
austinywang Jul 14, 2026
e35571f
Merge origin/main into issue-7998-right-sidebar-toggle-icon
austinywang Jul 14, 2026
a49d009
test: keep right sidebar toggle position stable
austinywang Jul 14, 2026
85fee51
Keep right sidebar toggle position stable
austinywang Jul 14, 2026
d2e430f
test: replace visible right toggle with close button
austinywang Jul 14, 2026
f643678
Use close button for visible right sidebar
austinywang Jul 14, 2026
d1c5aeb
Give hidden sidebar toggle a collapsed edge
austinywang Jul 14, 2026
d8cca1b
test: cover right sidebar toggle chrome modes
austinywang Jul 14, 2026
faeb47a
test: exercise minimal sidebar toggle first
austinywang Jul 14, 2026
9b93287
Fix right sidebar titlebar control lane
austinywang Jul 14, 2026
913c65e
Merge remote-tracking branch 'origin/main' into issue-7998-right-side…
austinywang Jul 14, 2026
96523f5
test: keep minimal split controls beside sidebar toggle
austinywang Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 14 additions & 17 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2050,23 +2050,9 @@ struct ContentView: View {
.frame(maxWidth: .infinity)
.overlay(alignment: .topLeading) {
customTitlebar(appearance: appearance)
// The workspace titlebar band spans the full window width and sits at
// zIndex(100) over the content/sidebar layout. Its drag/double-click
// surface (`WindowDragHandleView` + `.contentShape(Rectangle())`) must
// not cover the right sidebar, whose mode bar (Files/Search/Feed/Vault)
// lives inside the titlebar-height strip — otherwise the band wins the
// hit-test and swallows every click/hover on those buttons (#5099).
// Confine the interactive titlebar surface to the area left of the
// right sidebar, matching the pre-#5017 "only over terminal content,
// not the sidebar" intent. The left sidebar's titlebar controls live in
// the AppKit titlebar accessory (above this band), so only the trailing
// (right-sidebar) edge needs to be ceded here.
//
// `rightSidebarWidth` is already `rightSidebarVisible ? fileExplorerWidth : 0`,
// so it collapses to 0 when the sidebar is hidden. The sidebar panel itself
// snaps without animation (`.transaction { $0.animation = nil }`), so we match
// that here — otherwise this inset could animate out of step with the panel on
// toggle and momentarily expose (or re-cover) the mode bar mid-transition.
// Keep the drag surface off the right-sidebar mode bar (#5099).
// The width collapses when hidden, and the sidebar itself snaps,
// so disable animation here to keep both hit regions in lockstep.
.padding(.trailing, rightSidebarWidth)
.animation(nil, value: rightSidebarWidth)
}
Expand Down Expand Up @@ -2424,6 +2410,17 @@ struct ContentView: View {
workspaceTitlebarBand(appearance: appearance)
.zIndex(100)
}

if !rightSidebarVisible {
RightSidebarTitlebarToggleButton(
foregroundColor: Color(nsColor: titlebarControlForegroundNSColor(opacity: 1))
) {
_ = AppDelegate.shared?.toggleRightSidebarInActiveMainWindow(preferredWindow: observedWindow)
}
.environment(\.colorScheme, appearance.chromeColorScheme)
.frame(maxWidth: .infinity, alignment: .trailing)
.zIndex(101)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.frame(minWidth: CGFloat(SessionPersistencePolicy.minimumWindowWidth), minHeight: CGFloat(SessionPersistencePolicy.minimumWindowHeight))
Expand Down
4 changes: 3 additions & 1 deletion Sources/MobileConnectTitlebarAccessory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ private struct TitlebarTrailingControls: View {
ProBadgeView()
MobileConnectTitlebarButton()
}
.padding(.trailing, 8)
// The full-size content view owns the right-sidebar glyph / X lane.
// Keep the native controls left of that lane in standard mode.
.padding(.trailing, RightSidebarChromeMetrics.headerControlSize + 12)
}
}

Expand Down
105 changes: 105 additions & 0 deletions Sources/RightSidebarHeaderCloseButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import CmuxFoundation
import CmuxSettings
import CmuxSettingsUI
import SwiftUI

/// Closes the right sidebar from its header while presenting the configured shortcut hint.
struct RightSidebarHeaderCloseButton: View {
let action: () -> Void

@ObservedObject private var keyboardShortcutSettingsObserver = KeyboardShortcutSettingsObserver.shared
@State private var shortcutHintMonitor = WindowScopedShortcutHintModifierMonitor(activation: .commandOnly)
@LiveSetting(\.shortcuts.showModifierHoldHints) private var showModifierHoldHints
private let alwaysShowShortcutHints = ShortcutHintDebugSettings().alwaysShowHints
private let shortcutHintXOffset = ShortcutHintDebugSettings.defaultRightSidebarCloseHintX
private let shortcutHintYOffset = ShortcutHintDebugSettings.defaultRightSidebarCloseHintY

private func startShortcutHintMonitorIfNeeded() {
if showModifierHoldHints {
shortcutHintMonitor.start()
} else {
shortcutHintMonitor.stop()
}
}

var body: some View {
let _ = keyboardShortcutSettingsObserver.revision
let shortcut = KeyboardShortcutSettings.shortcut(for: .toggleRightSidebar)
let showsShortcutHint = ShortcutHintTitlebarPolicy.shouldShow(
shortcut: shortcut,
alwaysShowShortcutHints: alwaysShowShortcutHints,
modifierPressed: shortcutHintMonitor.isModifierPressed,
modifierHoldHintsEnabled: showModifierHoldHints
)

ZStack {
Button(action: action) {
HeaderChromeIconStyle.symbol("xmark")
}
.buttonStyle(
RightSidebarHeaderIconButtonStyle(
iconGeometryKeyPrefix: "rightSidebarHeaderCloseIcon"
)
)
.frame(
width: RightSidebarChromeMetrics.headerControlSize,
height: RightSidebarChromeMetrics.headerControlSize
)
.reportRightSidebarChromeNamedGeometryForBonsplitUITest(
keyPrefix: "rightSidebarHeaderClose",
isVisible: true
)
.safeHelp(
KeyboardShortcutSettings.Action.toggleRightSidebar.tooltip(
String(
localized: "rightSidebar.toggle.tooltip",
defaultValue: "Toggle right sidebar"
)
)
)
.accessibilityLabel(
String(
localized: "rightSidebar.close.accessibilityLabel",
defaultValue: "Close Right Sidebar"
)
)
.accessibilityIdentifier("RightSidebar.closeButton")
}
.frame(
width: RightSidebarChromeMetrics.headerControlSize,
height: RightSidebarChromeMetrics.headerControlSize
)
.background(
WindowAccessor(refreshID: showModifierHoldHints) { window in
shortcutHintMonitor.setHostWindow(showModifierHoldHints ? window : nil)
}
.frame(width: 0, height: 0)
)
.overlay(alignment: .top) {
if showsShortcutHint {
ShortcutHintPill(shortcut: shortcut, fontSize: 9, emphasis: 1.05)
.fixedSize(horizontal: true, vertical: false)
.offset(
x: CGFloat(ShortcutHintDebugSettings.clamped(shortcutHintXOffset)),
y: CGFloat(ShortcutHintDebugSettings.clamped(shortcutHintYOffset))
)
.shortcutHintTransition()
.accessibilityIdentifier("rightSidebarCloseShortcutHint")
.allowsHitTesting(false)
.zIndex(10)
}
}
.rightSidebarHeaderControlAlignment()
.shortcutHintVisibilityAnimation(value: showsShortcutHint)
.titlebarInteractiveControl()
.onAppear {
startShortcutHintMonitorIfNeeded()
}
.onDisappear {
shortcutHintMonitor.stop()
}
.onChange(of: showModifierHoldHints) { _, _ in
startShortcutHintMonitorIfNeeded()
}
}
}
61 changes: 1 addition & 60 deletions Sources/RightSidebarPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,9 @@ struct RightSidebarPanelView: View {
return AppDelegate.shared?.isRightSidebarFocusResponder(responder, in: window) == true
}
@State private var focusShortcutHintMonitor = WindowScopedShortcutHintModifierMonitor(activation: .commandOnly)
@State private var closeShortcutHintMonitor = WindowScopedShortcutHintModifierMonitor(activation: .commandOnly)
@State private var hasMountedRightSidebarContent = false
@ObservedObject private var keyboardShortcutSettingsObserver = KeyboardShortcutSettingsObserver.shared
private let alwaysShowShortcutHints = ShortcutHintDebugSettings().alwaysShowHints
private let closeShortcutHintXOffset = ShortcutHintDebugSettings.defaultRightSidebarCloseHintX
private let closeShortcutHintYOffset = ShortcutHintDebugSettings.defaultRightSidebarCloseHintY
private let focusShortcutHintXOffset = ShortcutHintDebugSettings.defaultRightSidebarFocusHintX
private let focusShortcutHintYOffset = ShortcutHintDebugSettings.defaultRightSidebarFocusHintY
@LiveSetting(\.shortcuts.showModifierHoldHints) private var showModifierHoldHints
Expand Down Expand Up @@ -163,13 +160,11 @@ struct RightSidebarPanelView: View {
}
modeShortcutHintMonitor.start()
focusShortcutHintMonitor.start()
closeShortcutHintMonitor.start()
}

private func stopShortcutHintMonitors() {
modeShortcutHintMonitor.stop()
focusShortcutHintMonitor.stop()
closeShortcutHintMonitor.stop()
}

var body: some View {
Expand All @@ -190,7 +185,6 @@ struct RightSidebarPanelView: View {
let hintWindow = showModifierHoldHints ? window : nil
modeShortcutHintMonitor.setHostWindow(hintWindow)
focusShortcutHintMonitor.setHostWindow(hintWindow)
closeShortcutHintMonitor.setHostWindow(hintWindow)
}
.frame(width: 0, height: 0)
)
Expand Down Expand Up @@ -249,7 +243,7 @@ struct RightSidebarPanelView: View {
if fileExplorerState.mode.canOpenAsPane {
openAsPaneButton(mode: fileExplorerState.mode)
}
closeButton
RightSidebarHeaderCloseButton(action: onClose)
}
}
.rightSidebarChromeBar(leadingPadding: 4, trailingPadding: 6, height: titlebarHeight)
Expand Down Expand Up @@ -292,59 +286,6 @@ struct RightSidebarPanelView: View {
.titlebarInteractiveControl()
}

private var closeButton: some View {
let _ = keyboardShortcutSettingsObserver.revision
let shortcut = KeyboardShortcutSettings.shortcut(for: .toggleRightSidebar)
let showsShortcutHint = ShortcutHintTitlebarPolicy.shouldShow(
shortcut: shortcut,
alwaysShowShortcutHints: alwaysShowShortcutHints,
modifierPressed: closeShortcutHintMonitor.isModifierPressed,
modifierHoldHintsEnabled: showModifierHoldHints
)
return ZStack {
Button(action: onClose) {
HeaderChromeIconStyle.symbol("xmark")
}
.buttonStyle(RightSidebarHeaderIconButtonStyle(iconGeometryKeyPrefix: "rightSidebarHeaderCloseIcon"))
.frame(
width: RightSidebarChromeMetrics.headerControlSize,
height: RightSidebarChromeMetrics.headerControlSize
)
.reportRightSidebarChromeNamedGeometryForBonsplitUITest(
keyPrefix: "rightSidebarHeaderClose",
isVisible: true
)
.safeHelp(
KeyboardShortcutSettings.Action.toggleRightSidebar.tooltip(
String(localized: "rightSidebar.toggle.tooltip", defaultValue: "Toggle right sidebar")
)
)
.accessibilityLabel(String(localized: "rightSidebar.close.accessibilityLabel", defaultValue: "Close Right Sidebar"))
.accessibilityIdentifier("RightSidebar.closeButton")
}
.frame(
width: RightSidebarChromeMetrics.headerControlSize,
height: RightSidebarChromeMetrics.headerControlSize
)
.overlay(alignment: .top) {
if showsShortcutHint {
ShortcutHintPill(shortcut: shortcut, fontSize: 9, emphasis: 1.05)
.fixedSize(horizontal: true, vertical: false)
.offset(
x: CGFloat(ShortcutHintDebugSettings.clamped(closeShortcutHintXOffset)),
y: CGFloat(ShortcutHintDebugSettings.clamped(closeShortcutHintYOffset))
)
.shortcutHintTransition()
.accessibilityIdentifier("rightSidebarCloseShortcutHint")
.allowsHitTesting(false)
.zIndex(10)
}
}
.rightSidebarHeaderControlAlignment()
.shortcutHintVisibilityAnimation(value: showsShortcutHint)
.titlebarInteractiveControl()
}

@ViewBuilder
private var focusShortcutHintOverlay: some View {
let _ = keyboardShortcutSettingsObserver.revision
Expand Down
60 changes: 60 additions & 0 deletions Sources/RightSidebarTitlebarToggleButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import CmuxAppKitSupportUI
import CmuxFoundation
import CmuxSettings
import SwiftUI

/// The built-in title-bar control for showing the hidden right sidebar.
struct RightSidebarTitlebarToggleButton: View {
let foregroundColor: Color
let action: () -> Void
@AppStorage(TitlebarControlsStyle.storageKey)
private var styleRawValue = TitlebarControlsStyle.defaultRawValue
@ObservedObject private var keyboardShortcutSettingsObserver = KeyboardShortcutSettingsObserver.shared

var body: some View {
let _ = keyboardShortcutSettingsObserver.revision
let config = TitlebarControlsStyle.stored(rawValue: styleRawValue).config
TitlebarControlButton(
config: config,
foregroundColor: foregroundColor,
accessibilityIdentifier: "titlebarControl.toggleRightSidebar",
accessibilityLabel: String(
localized: "shortcut.toggleRightSidebar.label",
defaultValue: "Toggle Right Sidebar"
),
action: action
) {
TitlebarSidebarGlyph(edge: .trailing, iconSize: config.iconSize)
.frame(
width: HeaderChromeIconStyle.iconFrameSize(forIconSize: config.iconSize),
height: HeaderChromeIconStyle.iconFrameSize(forIconSize: config.iconSize)
)
.background(
TitlebarChromeGeometryReporter(
keyPrefix: "titlebarControl_toggleRightSidebarIcon"
)
)
}
.safeHelp(
KeyboardShortcutSettings.Action.toggleRightSidebar.tooltip(
String(
localized: "rightSidebar.toggle.tooltip",
defaultValue: "Toggle right sidebar"
)
)
)
.frame(
width: RightSidebarChromeMetrics.headerControlSize + 12,
height: WindowChromeMetrics.appTitlebarHeight
)
.overlay(alignment: .leading) {
WindowChromeBorder(
orientation: .vertical,
ignoresSafeArea: false,
refreshNotificationName: .ghosttyDefaultBackgroundDidChange,
backgroundColorProvider: { GhosttyBackgroundTheme.currentColor() }
)
.allowsHitTesting(false)
}
}
}
43 changes: 43 additions & 0 deletions Sources/TitlebarSidebarGlyph.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import SwiftUI

/// Draws the sidebar outline used by the built-in leading and trailing title-bar toggles.
struct TitlebarSidebarGlyph: View {
enum Edge: Equatable {
case leading
case trailing
}

let edge: Edge
let iconSize: CGFloat

var body: some View {
GlyphShape(edge: edge)
.stroke(
style: StrokeStyle(
lineWidth: HeaderChromeIconStyle.sidebarGlyphStrokeWidth,
lineCap: .round,
lineJoin: .round
)
)
.frame(width: max(13, iconSize + 2), height: max(11, iconSize - 1))
}

private struct GlyphShape: Shape {
let edge: Edge

func path(in rect: CGRect) -> Path {
var path = Path()
let insetRect = rect.insetBy(dx: 0.5, dy: 0.5)
path.addRoundedRect(
in: insetRect,
cornerSize: CGSize(width: 2, height: 2)
)

let dividerFraction = edge == .leading ? 0.36 : 0.64
let dividerX = insetRect.minX + insetRect.width * dividerFraction
path.move(to: CGPoint(x: dividerX, y: insetRect.minY + 1.5))
path.addLine(to: CGPoint(x: dividerX, y: insetRect.maxY - 1.5))
return path
}
}
}
Loading
Loading