Merge pull request #1479 from manaflow-ai/feat-hidden-titlebar-minimalism-reset

Hide workspace titlebar chrome in minimal mode
This commit is contained in:
Lawrence Chen 2026-03-18 06:07:29 -07:00 committed by GitHub
commit e4c3d93e6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1946 additions and 29 deletions

View file

@ -14349,6 +14349,40 @@
}
}
},
"command.enableMinimalMode.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Enable Minimal Mode"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ミニマルモードを有効にする"
}
}
}
},
"command.disableMinimalMode.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Disable Minimal Mode"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ミニマルモードを無効にする"
}
}
}
},
"command.installCLI.subtitle": {
"extractionState": "manual",
"localizations": {
@ -45454,6 +45488,159 @@
}
}
},
"settings.app.showWorkspaceTitlebar": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Show Workspace Title Bar"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペースのタイトルバーを表示"
}
}
}
},
"settings.app.showWorkspaceTitlebar.subtitleOff": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Hide the folder and active title above pane tabs."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ペインタブの上にあるフォルダ名と現在のタイトルを隠します。"
}
}
}
},
"settings.app.showWorkspaceTitlebar.subtitleOn": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Show the folder and active title above pane tabs."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ペインタブの上にフォルダ名と現在のタイトルを表示します。"
}
}
}
},
"settings.app.fadeButtons": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Fade Buttons"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ボタンをフェード表示"
}
}
}
},
"settings.app.fadeButtons.subtitleOff": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Keep action buttons always visible."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "操作ボタンを常に表示します。"
}
}
}
},
"settings.app.fadeButtons.subtitleOn": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Show action buttons only on hover."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "操作ボタンはホバー時のみ表示します。"
}
}
}
},
"settings.app.minimalMode": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Minimal Mode"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ミニマルモード"
}
}
}
},
"settings.app.minimalMode.subtitleOff": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Use the standard workspace title bar and controls."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "標準のワークスペースタイトルバーと操作を使います。"
}
}
}
},
"settings.app.minimalMode.subtitleOn": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Hide the workspace title bar and move workspace controls into the sidebar."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペースのタイトルバーを隠し、ワークスペース操作をサイドバーに移動します。"
}
}
}
},
"settings.app.showPullRequests": {
"extractionState": "manual",
"localizations": {

View file

@ -9,6 +9,30 @@ import Combine
import ObjectiveC.runtime
import Darwin
final class MainWindowHostingView<Content: View>: NSHostingView<Content> {
private let zeroSafeAreaLayoutGuide = NSLayoutGuide()
override var safeAreaInsets: NSEdgeInsets { NSEdgeInsetsZero }
override var safeAreaRect: NSRect { bounds }
override var safeAreaLayoutGuide: NSLayoutGuide { zeroSafeAreaLayoutGuide }
required init(rootView: Content) {
super.init(rootView: rootView)
addLayoutGuide(zeroSafeAreaLayoutGuide)
NSLayoutConstraint.activate([
zeroSafeAreaLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor),
zeroSafeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor),
zeroSafeAreaLayoutGuide.topAnchor.constraint(equalTo: topAnchor),
zeroSafeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private enum CmuxThemeNotifications {
static let reloadConfig = Notification.Name("com.cmuxterm.themes.reload-config")
}
@ -2041,6 +2065,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private var jumpUnreadFocusExpectation: (tabId: UUID, surfaceId: UUID)?
private var jumpUnreadFocusObserver: NSObjectProtocol?
private var didSetupGotoSplitUITest = false
private var didSetupBonsplitTabDragUITest = false
private var bonsplitTabDragUITestRecorder: DispatchSourceTimer?
private var gotoSplitUITestObservers: [NSObjectProtocol] = []
private var didSetupMultiWindowNotificationsUITest = false
private var didSetupDisplayResolutionUITestDiagnostics = false
@ -2644,6 +2670,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
#if DEBUG
setupJumpUnreadUITestIfNeeded()
setupGotoSplitUITestIfNeeded()
setupBonsplitTabDragUITestIfNeeded()
setupMultiWindowNotificationsUITestIfNeeded()
setupDisplayResolutionUITestDiagnosticsIfNeeded()
@ -5773,7 +5800,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
} else {
window.center()
}
window.contentView = NSHostingView(rootView: root)
window.contentView = MainWindowHostingView(rootView: root)
// Apply shared window styling.
attachUpdateAccessory(to: window)
@ -6966,6 +6993,176 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}
private func setupBonsplitTabDragUITestIfNeeded() {
guard !didSetupBonsplitTabDragUITest else { return }
didSetupBonsplitTabDragUITest = true
let env = ProcessInfo.processInfo.environment
guard env["CMUX_UI_TEST_BONSPLIT_TAB_DRAG_SETUP"] == "1" else { return }
guard tabManager != nil else { return }
let startWithHiddenSidebar = env["CMUX_UI_TEST_BONSPLIT_START_WITH_HIDDEN_SIDEBAR"] == "1"
let deadline = Date().addingTimeInterval(20.0)
func hasMainTerminalWindow() -> Bool {
NSApp.windows.contains { window in
guard let raw = window.identifier?.rawValue else { return false }
return raw == "cmux.main" || raw.hasPrefix("cmux.main.")
}
}
func runSetupWhenWindowReady() {
guard Date() < deadline else {
writeBonsplitTabDragUITestData(["setupError": "Timed out waiting for main window"])
return
}
guard hasMainTerminalWindow() else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
runSetupWhenWindowReady()
}
return
}
if let mainWindow = NSApp.windows.first(where: { window in
guard let raw = window.identifier?.rawValue else { return false }
return raw == "cmux.main" || raw.hasPrefix("cmux.main.")
}) {
let screenFrame = mainWindow.screen?.visibleFrame ?? NSScreen.main?.visibleFrame
if let screenFrame {
let targetSize = NSSize(width: min(960, screenFrame.width - 80), height: min(720, screenFrame.height - 80))
let targetOrigin = NSPoint(
x: screenFrame.minX + 40,
y: screenFrame.maxY - 40 - targetSize.height
)
let targetFrame = NSRect(origin: targetOrigin, size: targetSize)
if !mainWindow.frame.equalTo(targetFrame) {
mainWindow.setFrame(targetFrame, display: true)
}
}
}
guard let tabManager = self.tabManager,
let workspace = tabManager.selectedWorkspace ?? tabManager.tabs.first,
let alphaPanelId = workspace.focusedPanelId else {
self.writeBonsplitTabDragUITestData(["setupError": "Missing initial workspace or panel"])
return
}
let workspaceTitle = "UITest Workspace"
let alphaTitle = "UITest Alpha"
let betaTitle = "UITest Beta"
tabManager.setCustomTitle(tabId: workspace.id, title: workspaceTitle)
workspace.setPanelCustomTitle(panelId: alphaPanelId, title: alphaTitle)
tabManager.newSurface()
guard let betaPanelId = workspace.focusedPanelId, betaPanelId != alphaPanelId else {
self.writeBonsplitTabDragUITestData(["setupError": "Failed to create second surface"])
return
}
workspace.setPanelCustomTitle(panelId: betaPanelId, title: betaTitle)
if startWithHiddenSidebar {
self.sidebarState?.isVisible = false
}
self.writeBonsplitTabDragUITestData([
"ready": "1",
"sidebarVisible": startWithHiddenSidebar ? "0" : "1",
"workspaceId": workspace.id.uuidString,
"workspaceTitle": workspaceTitle,
"alphaTitle": alphaTitle,
"betaTitle": betaTitle,
"alphaPanelId": alphaPanelId.uuidString,
"betaPanelId": betaPanelId.uuidString,
])
self.startBonsplitTabDragUITestRecorder(
workspaceId: workspace.id,
alphaPanelId: alphaPanelId,
betaPanelId: betaPanelId
)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
guard self != nil else { return }
runSetupWhenWindowReady()
}
}
private func bonsplitTabDragUITestDataPath() -> String? {
let env = ProcessInfo.processInfo.environment
guard env["CMUX_UI_TEST_BONSPLIT_TAB_DRAG_SETUP"] == "1",
let path = env["CMUX_UI_TEST_BONSPLIT_TAB_DRAG_PATH"],
!path.isEmpty else {
return nil
}
return path
}
private func startBonsplitTabDragUITestRecorder(
workspaceId: UUID,
alphaPanelId: UUID,
betaPanelId: UUID
) {
bonsplitTabDragUITestRecorder?.cancel()
bonsplitTabDragUITestRecorder = nil
let timer = DispatchSource.makeTimerSource(queue: .main)
timer.schedule(deadline: .now(), repeating: .milliseconds(100))
timer.setEventHandler { [weak self] in
self?.recordBonsplitTabDragUITestState(
workspaceId: workspaceId,
alphaPanelId: alphaPanelId,
betaPanelId: betaPanelId
)
}
bonsplitTabDragUITestRecorder = timer
timer.resume()
}
private func recordBonsplitTabDragUITestState(
workspaceId: UUID,
alphaPanelId: UUID,
betaPanelId: UUID
) {
guard let tabManager else { return }
guard let workspace = (tabManager.tabs.first { $0.id == workspaceId } ?? tabManager.selectedWorkspace ?? tabManager.tabs.first) else {
return
}
let trackedPaneId = workspace.paneId(forPanelId: alphaPanelId)
?? workspace.paneId(forPanelId: betaPanelId)
?? workspace.bonsplitController.focusedPaneId
?? workspace.bonsplitController.allPaneIds.first
guard let trackedPaneId else { return }
let titles: [String] = workspace.bonsplitController.tabs(inPane: trackedPaneId).compactMap { tab in
guard let panelId = workspace.panelIdFromSurfaceId(tab.id) else { return nil }
return workspace.panelTitle(panelId: panelId)
}
let selectedTitle = workspace.bonsplitController.selectedTab(inPane: trackedPaneId)
.flatMap { workspace.panelIdFromSurfaceId($0.id) }
.flatMap { workspace.panelTitle(panelId: $0) } ?? ""
writeBonsplitTabDragUITestData([
"trackedPaneId": trackedPaneId.description,
"trackedPaneTabTitles": titles.joined(separator: "|"),
"trackedPaneTabCount": String(titles.count),
"trackedPaneSelectedTitle": selectedTitle,
])
}
private func writeBonsplitTabDragUITestData(_ updates: [String: String]) {
guard let path = bonsplitTabDragUITestDataPath() else { return }
var payload = loadBonsplitTabDragUITestData(at: path)
for (key, value) in updates {
payload[key] = value
}
guard let data = try? JSONSerialization.data(withJSONObject: payload) else { return }
try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
}
private func loadBonsplitTabDragUITestData(at path: String) -> [String: String] {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
return [:]
}
return object
}
private func isGotoSplitUITestRecordingEnabled() -> Bool {
let env = ProcessInfo.processInfo.environment
return env["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] == "1" || env["CMUX_UI_TEST_GOTO_SPLIT_RECORD_ONLY"] == "1"
@ -8349,6 +8546,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
titlebarAccessoryController.dismissNotificationsPopoverIfShown()
}
func isNotificationsPopoverShown() -> Bool {
titlebarAccessoryController.isNotificationsPopoverShown()
}
func jumpToLatestUnread() {
guard let notificationStore else { return }
#if DEBUG
@ -8468,7 +8669,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private func installShortcutDefaultsObserver() {
guard shortcutDefaultsObserver == nil else { return }
shortcutDefaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
forName: KeyboardShortcutSettings.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in

View file

@ -1609,6 +1609,7 @@ struct ContentView: View {
static let hasWorkspace = "workspace.hasSelection"
static let workspaceName = "workspace.name"
static let workspaceHasCustomName = "workspace.hasCustomName"
static let workspaceMinimalModeEnabled = "workspace.minimalModeEnabled"
static let workspaceShouldPin = "workspace.shouldPin"
static let workspaceHasPullRequests = "workspace.hasPullRequests"
static let workspaceHasSplits = "workspace.hasSplits"
@ -2035,6 +2036,16 @@ struct ContentView: View {
/// Space at top of content area for the titlebar. This must be at least the actual titlebar
/// height; otherwise controls like Bonsplit tab dragging can be interpreted as window drags.
@State private var titlebarPadding: CGFloat = 32
@AppStorage(WorkspacePresentationModeSettings.modeKey)
private var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue
private var isMinimalMode: Bool {
WorkspacePresentationModeSettings.mode(for: workspacePresentationMode) == .minimal
}
private var effectiveTitlebarPadding: CGFloat {
isMinimalMode ? 0 : titlebarPadding
}
private var terminalContent: some View {
let mountedWorkspaceIdSet = Set(mountedWorkspaceIds)
@ -2092,10 +2103,12 @@ struct ContentView: View {
.allowsHitTesting(sidebarSelectionState.selection == .notifications)
.accessibilityHidden(sidebarSelectionState.selection != .notifications)
}
.padding(.top, titlebarPadding)
.padding(.top, effectiveTitlebarPadding)
.overlay(alignment: .top) {
// Titlebar overlay is only over terminal content, not the sidebar.
customTitlebar
if !isMinimalMode {
// Titlebar overlay is only over terminal content, not the sidebar.
customTitlebar
}
}
}
@ -2134,7 +2147,8 @@ struct ContentView: View {
anchorView: fullscreenControlsViewModel.notificationsAnchorView
)
},
onNewTab: { tabManager.addTab() }
onNewTab: { tabManager.addTab() },
visibilityMode: .alwaysVisible
)
}
@ -2312,7 +2326,7 @@ struct ContentView: View {
contentAndSidebarLayout
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.overlay(alignment: .topLeading) {
if isFullScreen && sidebarState.isVisible {
if isFullScreen && sidebarState.isVisible && !isMinimalMode {
fullscreenControls
.padding(.leading, 10)
.padding(.top, 4)
@ -4875,6 +4889,7 @@ struct ContentView: View {
terminalOpenTargets: Set<TerminalDirectoryOpenTarget>? = nil
) -> CommandPaletteContextSnapshot {
var snapshot = CommandPaletteContextSnapshot()
snapshot.setBool(CommandPaletteContextKeys.workspaceMinimalModeEnabled, isMinimalMode)
if let workspace = tabManager.selectedWorkspace {
snapshot.setBool(CommandPaletteContextKeys.hasWorkspace, true)
@ -5079,6 +5094,24 @@ struct ContentView: View {
keywords: ["toggle", "sidebar", "layout"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.enableMinimalMode",
title: constant(String(localized: "command.enableMinimalMode.title", defaultValue: "Enable Minimal Mode")),
subtitle: constant(String(localized: "command.toggleSidebar.subtitle", defaultValue: "Layout")),
keywords: ["minimal", "mode", "titlebar", "sidebar", "layout"],
when: { !$0.bool(CommandPaletteContextKeys.workspaceMinimalModeEnabled) }
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.disableMinimalMode",
title: constant(String(localized: "command.disableMinimalMode.title", defaultValue: "Disable Minimal Mode")),
subtitle: constant(String(localized: "command.toggleSidebar.subtitle", defaultValue: "Layout")),
keywords: ["minimal", "mode", "titlebar", "sidebar", "layout"],
when: { $0.bool(CommandPaletteContextKeys.workspaceMinimalModeEnabled) }
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.triggerFlash",
@ -5698,6 +5731,12 @@ struct ContentView: View {
registry.register(commandId: "palette.toggleSidebar") {
sidebarState.toggle()
}
registry.register(commandId: "palette.enableMinimalMode") {
workspacePresentationMode = WorkspacePresentationModeSettings.Mode.minimal.rawValue
}
registry.register(commandId: "palette.disableMinimalMode") {
workspacePresentationMode = WorkspacePresentationModeSettings.Mode.standard.rawValue
}
registry.register(commandId: "palette.triggerFlash") {
tabManager.triggerFocusFlash()
}
@ -8093,10 +8132,17 @@ struct VerticalTabsSidebar: View {
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
@AppStorage(SidebarWorkspaceDetailSettings.showNotificationMessageKey)
private var sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
@AppStorage(WorkspacePresentationModeSettings.modeKey)
private var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue
/// Space at top of sidebar for traffic light buttons
private let trafficLightPadding: CGFloat = 28
private let tabRowSpacing: CGFloat = 2
private let hiddenTitlebarControlsLeadingInset: CGFloat = 72
private var isMinimalMode: Bool {
WorkspacePresentationModeSettings.mode(for: workspacePresentationMode) == .minimal
}
private var showsSidebarNotificationMessage: Bool {
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
@ -8194,6 +8240,13 @@ struct VerticalTabsSidebar: View {
WindowDragHandleView()
.frame(height: trafficLightPadding)
}
.overlay(alignment: .topLeading) {
if isMinimalMode {
HiddenTitlebarSidebarControlsView(notificationStore: notificationStore)
.padding(.leading, hiddenTitlebarControlsLeadingInset)
.padding(.top, 2)
}
}
.background(Color.clear)
.modifier(ClearScrollBackground())
}

View file

@ -6093,9 +6093,22 @@ func shouldAllowEnsureFocusWindowActivation(
activeTabManager: TabManager?,
targetTabManager: TabManager,
keyWindow: NSWindow?,
mainWindow: NSWindow?
mainWindow: NSWindow?,
targetWindow: NSWindow
) -> Bool {
activeTabManager === targetTabManager || (keyWindow == nil && mainWindow == nil)
guard activeTabManager === targetTabManager || (keyWindow == nil && mainWindow == nil) else {
return false
}
if let keyWindow {
return keyWindow === targetWindow
}
if let mainWindow {
return mainWindow === targetWindow
}
return true
}
final class GhosttySurfaceScrollView: NSView {
@ -7652,7 +7665,8 @@ final class GhosttySurfaceScrollView: NSView {
activeTabManager: delegate.tabManager,
targetTabManager: tabManager,
keyWindow: NSApp.keyWindow,
mainWindow: NSApp.mainWindow
mainWindow: NSApp.mainWindow,
targetWindow: window
) else {
return
}

View file

@ -3,6 +3,9 @@ import SwiftUI
/// Stores customizable keyboard shortcuts (definitions + persistence).
enum KeyboardShortcutSettings {
static let didChangeNotification = Notification.Name("cmux.keyboardShortcutSettingsDidChange")
static let actionUserInfoKey = "action"
enum Action: String, CaseIterable, Identifiable {
// Titlebar / primary UI
case toggleSidebar
@ -198,16 +201,34 @@ enum KeyboardShortcutSettings {
if let data = try? JSONEncoder().encode(shortcut) {
UserDefaults.standard.set(data, forKey: action.defaultsKey)
}
postDidChangeNotification(action: action)
}
static func resetShortcut(for action: Action) {
UserDefaults.standard.removeObject(forKey: action.defaultsKey)
postDidChangeNotification(action: action)
}
static func resetAll() {
for action in Action.allCases {
resetShortcut(for: action)
UserDefaults.standard.removeObject(forKey: action.defaultsKey)
}
postDidChangeNotification()
}
private static func postDidChangeNotification(
action: Action? = nil,
center: NotificationCenter = .default
) {
var userInfo: [AnyHashable: Any] = [:]
if let action {
userInfo[actionUserInfoKey] = action.rawValue
}
center.post(
name: didChangeNotification,
object: nil,
userInfo: userInfo.isEmpty ? nil : userInfo
)
}
// MARK: - Backwards-Compatible API (call-sites can migrate gradually)

View file

@ -119,6 +119,22 @@ final class TitlebarControlsViewModel: ObservableObject {
weak var notificationsAnchorView: NSView?
}
extension Notification.Name {
static let cmuxNotificationsPopoverVisibilityDidChange = Notification.Name("cmux.notificationsPopoverVisibilityDidChange")
}
private enum NotificationsPopoverVisibilityUserInfoKey {
static let isShown = "isShown"
}
private func postNotificationsPopoverVisibilityDidChange(isShown: Bool) {
NotificationCenter.default.post(
name: .cmuxNotificationsPopoverVisibilityDidChange,
object: nil,
userInfo: [NotificationsPopoverVisibilityUserInfoKey.isShown: isShown]
)
}
struct NotificationsAnchorView: NSViewRepresentable {
let onResolve: (NSView) -> Void
@ -240,11 +256,14 @@ struct TitlebarControlsView: View {
let onToggleSidebar: () -> Void
let onToggleNotifications: () -> Void
let onNewTab: () -> Void
let visibilityMode: TitlebarControlsVisibilityMode
@AppStorage("titlebarControlsStyle") private var styleRawValue = TitlebarControlsStyle.classic.rawValue
@AppStorage(ShortcutHintDebugSettings.titlebarHintXKey) private var titlebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultTitlebarHintX
@AppStorage(ShortcutHintDebugSettings.titlebarHintYKey) private var titlebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultTitlebarHintY
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
@State private var shortcutRefreshTick = 0
@State private var isHoveringControls = false
@State private var isNotificationsPopoverShown = false
@StateObject private var modifierKeyMonitor = TitlebarShortcutHintModifierMonitor()
private let titlebarHintRightSafetyShift: CGFloat = 10
private let titlebarHintBaseXShift: CGFloat = -10
@ -279,6 +298,13 @@ struct TitlebarControlsView: View {
alwaysShowShortcutHints || modifierKeyMonitor.isModifierPressed
}
private var shouldShowControls: Bool {
if visibilityMode == .alwaysVisible {
return true
}
return isHoveringControls || isNotificationsPopoverShown || shouldShowTitlebarShortcutHints
}
var body: some View {
// Force the `.safeHelp(...)` tooltips to re-evaluate when shortcuts are changed in settings.
// (The titlebar controls don't otherwise re-render on UserDefaults changes.)
@ -288,15 +314,28 @@ struct TitlebarControlsView: View {
controlsGroup(config: config)
.padding(.leading, 4)
.padding(.trailing, titlebarHintTrailingInset)
.contentShape(Rectangle())
.opacity(shouldShowControls ? 1 : 0)
.allowsHitTesting(shouldShowControls)
.animation(.easeInOut(duration: 0.14), value: shouldShowControls)
.background(
WindowAccessor { window in
modifierKeyMonitor.setHostWindow(window)
}
.frame(width: 0, height: 0)
)
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
.onHover { hovering in
isHoveringControls = hovering
}
.onReceive(NotificationCenter.default.publisher(for: KeyboardShortcutSettings.didChangeNotification)) { _ in
shortcutRefreshTick &+= 1
}
.onAppear {
isNotificationsPopoverShown = AppDelegate.shared?.isNotificationsPopoverShown() ?? false
}
.onReceive(NotificationCenter.default.publisher(for: .cmuxNotificationsPopoverVisibilityDidChange)) { notification in
isNotificationsPopoverShown = (notification.userInfo?[NotificationsPopoverVisibilityUserInfoKey.isShown] as? Bool) ?? false
}
.onAppear {
modifierKeyMonitor.start()
}
@ -508,6 +547,36 @@ struct TitlebarControlsView: View {
}
}
struct HiddenTitlebarSidebarControlsView: View {
@ObservedObject var notificationStore: TerminalNotificationStore
@StateObject private var viewModel = TitlebarControlsViewModel()
private let hostWidth: CGFloat = 124
private let hostHeight: CGFloat = 28
var body: some View {
TitlebarControlsView(
notificationStore: notificationStore,
viewModel: viewModel,
onToggleSidebar: { _ = AppDelegate.shared?.sidebarState?.toggle() },
onToggleNotifications: { [viewModel] in
AppDelegate.shared?.toggleNotificationsPopover(
animated: true,
anchorView: viewModel.notificationsAnchorView
)
},
onNewTab: { _ = AppDelegate.shared?.tabManager?.addTab() },
visibilityMode: .onHover
)
.frame(width: hostWidth, height: hostHeight, alignment: .leading)
}
}
enum TitlebarControlsVisibilityMode {
case alwaysVisible
case onHover
}
@MainActor
private final class TitlebarShortcutHintModifierMonitor: ObservableObject {
@Published private(set) var isModifierPressed = false
@ -714,6 +783,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
private let viewModel = TitlebarControlsViewModel()
private var userDefaultsObserver: NSObjectProtocol?
var popoverIsShownForTesting: Bool { notificationsPopover.isShown }
private var showsWorkspaceTitlebar: Bool { !WorkspacePresentationModeSettings.isMinimal() }
init(notificationStore: TerminalNotificationStore) {
self.notificationStore = notificationStore
@ -727,7 +797,8 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
viewModel: viewModel,
onToggleSidebar: toggleSidebar,
onToggleNotifications: toggleNotifications,
onNewTab: newTab
onNewTab: newTab,
visibilityMode: .alwaysVisible
)
)
@ -749,9 +820,11 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
object: nil,
queue: .main
) { [weak self] _ in
self?.applyWorkspaceTitlebarVisibility()
self?.scheduleSizeUpdate(invalidateFittingSize: true)
}
applyWorkspaceTitlebarVisibility()
scheduleSizeUpdate(invalidateFittingSize: true)
}
@ -796,6 +869,8 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
}
private func updateSize() {
applyWorkspaceTitlebarVisibility()
guard showsWorkspaceTitlebar else { return }
let contentSize: NSSize
if fittingSizeNeedsRefresh || cachedFittingSize == nil {
hostingView.invalidateIntrinsicContentSize()
@ -828,6 +903,16 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
hostingView.frame = NSRect(x: 0, y: yOffset, width: contentSize.width, height: contentSize.height)
}
private func applyWorkspaceTitlebarVisibility() {
let shouldShow = showsWorkspaceTitlebar
view.isHidden = !shouldShow
if !shouldShow {
preferredContentSize = .zero
containerView.frame = .zero
hostingView.frame = .zero
}
}
func toggleNotificationsPopover(animated: Bool = true, externalAnchor: NSView? = nil) {
if notificationsPopover.isShown {
notificationsPopover.performClose(nil)
@ -861,6 +946,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
if !anchorRect.isEmpty {
notificationsPopover.animates = animated
notificationsPopover.show(relativeTo: anchorRect, of: contentView, preferredEdge: .maxY)
postNotificationsPopoverVisibilityDidChange(isShown: true)
return
}
}
@ -871,6 +957,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
if !anchorRect.isEmpty {
notificationsPopover.animates = animated
notificationsPopover.show(relativeTo: anchorRect, of: contentView, preferredEdge: .maxY)
postNotificationsPopoverVisibilityDidChange(isShown: true)
return
}
}
@ -880,6 +967,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
let anchorRect = NSRect(x: 12, y: bounds.maxY - 8, width: 1, height: 1)
notificationsPopover.animates = animated
notificationsPopover.show(relativeTo: anchorRect, of: contentView, preferredEdge: .maxY)
postNotificationsPopoverVisibilityDidChange(isShown: true)
}
func dismissNotificationsPopover() {
@ -902,6 +990,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
func popoverDidClose(_ notification: Notification) {
// Clear the content view controller to stop SwiftUI observers when popover is hidden
notificationsPopover.contentViewController = nil
postNotificationsPopoverVisibilityDidChange(isShown: false)
}
}
@ -1090,6 +1179,7 @@ private struct NotificationPopoverRow: View {
}
}
@MainActor
final class UpdateTitlebarAccessoryController {
private weak var updateViewModel: UpdateViewModel?
private var didStart = false
@ -1130,7 +1220,9 @@ final class UpdateTitlebarAccessoryController {
queue: .main
) { [weak self] notification in
guard let window = notification.object as? NSWindow else { return }
self?.attachIfNeeded(to: window)
Task { @MainActor [weak self] in
self?.attachIfNeeded(to: window)
}
})
observers.append(center.addObserver(
@ -1139,7 +1231,9 @@ final class UpdateTitlebarAccessoryController {
queue: .main
) { [weak self] notification in
guard let window = notification.object as? NSWindow else { return }
self?.attachIfNeeded(to: window)
Task { @MainActor [weak self] in
self?.attachIfNeeded(to: window)
}
})
// We intentionally do not rely on "window became visible" notifications here:
@ -1159,7 +1253,9 @@ final class UpdateTitlebarAccessoryController {
let delays: [TimeInterval] = [0.05, 0.15, 0.3, 0.6, 1.0, 2.0, 3.0]
for delay in delays {
let item = DispatchWorkItem { [weak self] in
self?.attachToExistingWindows()
Task { @MainActor [weak self] in
self?.attachToExistingWindows()
}
#if DEBUG
let env = ProcessInfo.processInfo.environment
if env["CMUX_UI_TEST_MODE"] == "1" {
@ -1175,7 +1271,6 @@ final class UpdateTitlebarAccessoryController {
}
private func attachIfNeeded(to window: NSWindow) {
guard !attachedWindows.contains(window) else { return }
guard !isSettingsWindow(window) else { return }
// Window identifiers are assigned by SwiftUI via WindowAccessor, which can run
@ -1187,8 +1282,10 @@ final class UpdateTitlebarAccessoryController {
if attempts < 40 {
pendingAttachRetries[key] = attempts + 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self, weak window] in
guard let self, let window else { return }
self.attachIfNeeded(to: window)
Task { @MainActor [weak self, weak window] in
guard let self, let window else { return }
self.attachIfNeeded(to: window)
}
}
} else {
pendingAttachRetries.removeValue(forKey: key)
@ -1198,6 +1295,13 @@ final class UpdateTitlebarAccessoryController {
pendingAttachRetries.removeValue(forKey: ObjectIdentifier(window))
guard !WorkspacePresentationModeSettings.isMinimal() else {
removeAccessoryIfPresent(from: window)
return
}
guard !attachedWindows.contains(window) else { return }
if !window.titlebarAccessoryViewControllers.contains(where: { $0.view.identifier == controlsIdentifier }) {
let controls = TitlebarControlsAccessoryViewController(
notificationStore: TerminalNotificationStore.shared
@ -1219,6 +1323,40 @@ final class UpdateTitlebarAccessoryController {
#endif
}
private func removeAccessoryIfPresent(from window: NSWindow) {
let matchingIndices = window.titlebarAccessoryViewControllers.indices.reversed().filter { index in
window.titlebarAccessoryViewControllers[index].view.identifier == controlsIdentifier
}
guard !matchingIndices.isEmpty || attachedWindows.contains(window) else { return }
for index in matchingIndices {
let accessory = window.titlebarAccessoryViewControllers[index]
if let controls = accessory as? TitlebarControlsAccessoryViewController {
controls.dismissNotificationsPopover()
}
window.removeTitlebarAccessoryViewController(at: index)
}
attachedWindows.remove(window)
pendingAttachRetries.removeValue(forKey: ObjectIdentifier(window))
DispatchQueue.main.async { [weak window] in
guard let window else { return }
window.contentView?.needsLayout = true
window.contentView?.superview?.needsLayout = true
window.contentView?.layoutSubtreeIfNeeded()
window.contentView?.superview?.layoutSubtreeIfNeeded()
window.invalidateShadow()
}
#if DEBUG
let env = ProcessInfo.processInfo.environment
if env["CMUX_UI_TEST_MODE"] == "1" {
let ident = window.identifier?.rawValue ?? "<nil>"
UpdateLogStore.shared.append("removed titlebar accessories from window id=\(ident)")
}
#endif
}
private func isSettingsWindow(_ window: NSWindow) -> Bool {
if window.identifier?.rawValue == "cmux.settings" {
return true

View file

@ -16,9 +16,15 @@ struct WorkspaceContentView: View {
_ notificationPayloadHex: String?
) -> Void)?
@State private var config = WorkspaceContentView.resolveGhosttyAppearanceConfig(reason: "stateInit")
@AppStorage(WorkspacePresentationModeSettings.modeKey)
private var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue
@Environment(\.colorScheme) private var colorScheme
@EnvironmentObject var notificationStore: TerminalNotificationStore
private var isMinimalMode: Bool {
WorkspacePresentationModeSettings.mode(for: workspacePresentationMode) == .minimal
}
static func panelVisibleInUI(
isWorkspaceVisible: Bool,
isSelectedInPane: Bool,
@ -52,7 +58,7 @@ struct WorkspaceContentView: View {
}
}()
BonsplitView(controller: workspace.bonsplitController) { tab, paneId in
let bonsplitView = BonsplitView(controller: workspace.bonsplitController) { tab, paneId in
// Content for each tab in bonsplit
let _ = Self.debugPanelLookup(tab: tab, workspace: workspace)
if let panel = workspace.panel(for: tab.id) {
@ -147,6 +153,15 @@ struct WorkspaceContentView: View {
notificationPayloadHex: payloadHex
)
}
Group {
if isMinimalMode {
bonsplitView
.ignoresSafeArea(.container, edges: .top)
} else {
bonsplitView
}
}
}
private func syncBonsplitNotificationBadges() {

View file

@ -4,6 +4,89 @@ import Darwin
import Bonsplit
import UniformTypeIdentifiers
enum WorkspaceTitlebarSettings {
static let showTitlebarKey = "workspaceTitlebarVisible"
static let defaultShowTitlebar = true
static func isVisible(defaults: UserDefaults = .standard) -> Bool {
if defaults.object(forKey: showTitlebarKey) == nil {
return defaultShowTitlebar
}
return defaults.bool(forKey: showTitlebarKey)
}
}
enum WorkspacePresentationModeSettings {
static let modeKey = "workspacePresentationMode"
enum Mode: String {
case standard
case minimal
}
static let defaultMode: Mode = .standard
static func mode(for rawValue: String?) -> Mode {
Mode(rawValue: rawValue ?? "") ?? defaultMode
}
static func mode(defaults: UserDefaults = .standard) -> Mode {
mode(for: defaults.string(forKey: modeKey))
}
static func isMinimal(defaults: UserDefaults = .standard) -> Bool {
mode(defaults: defaults) == .minimal
}
}
enum WorkspaceButtonFadeSettings {
static let modeKey = "workspaceButtonsFadeMode"
static let legacyTitlebarControlsVisibilityModeKey = "titlebarControlsVisibilityMode"
static let legacyPaneTabBarControlsVisibilityModeKey = "paneTabBarControlsVisibilityMode"
enum Mode: String {
case enabled
case disabled
}
static let defaultMode: Mode = .disabled
static func mode(for rawValue: String?) -> Mode {
Mode(rawValue: rawValue ?? "") ?? defaultMode
}
static func isEnabled(defaults: UserDefaults = .standard) -> Bool {
mode(for: defaults.string(forKey: modeKey)) == .enabled
}
static func initializeStoredModeIfNeeded(defaults: UserDefaults = .standard) {
guard defaults.string(forKey: modeKey) == nil else { return }
if let migratedMode = migratedLegacyMode(defaults: defaults) {
defaults.set(migratedMode.rawValue, forKey: modeKey)
return
}
let initialMode: Mode = WorkspaceTitlebarSettings.isVisible(defaults: defaults) ? .disabled : .enabled
defaults.set(initialMode.rawValue, forKey: modeKey)
}
private static func migratedLegacyMode(defaults: UserDefaults) -> Mode? {
let legacyValues = [
defaults.string(forKey: legacyTitlebarControlsVisibilityModeKey),
defaults.string(forKey: legacyPaneTabBarControlsVisibilityModeKey),
]
if legacyValues.contains(where: { $0 == "onHover" || $0 == "hover" || $0 == "enabled" }) {
return .enabled
}
if legacyValues.contains(where: { $0 == "always" || $0 == "disabled" }) {
return .disabled
}
return nil
}
}
enum UITestLaunchManifest {
static let argumentName = "-cmuxUITestLaunchManifest"
@ -2492,6 +2575,8 @@ private struct AcknowledgmentsView: View {
final class SettingsWindowController: NSWindowController, NSWindowDelegate {
static let shared = SettingsWindowController()
private var pendingFocusRestoreWorkItems: [DispatchWorkItem] = []
private var focusRestoreGeneration = 0
private init() {
let window = NSWindow(
@ -2534,6 +2619,93 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
dlog("settings.window.show completed isVisible=\(window.isVisible ? 1 : 0) isKey=\(window.isKeyWindow ? 1 : 0)")
#endif
}
func preserveFocusAfterPreferenceMutation() {
guard let window, window.isVisible else { return }
cancelPendingFocusRestore()
focusRestoreGeneration += 1
let generation = focusRestoreGeneration
writeFocusDiagnosticsIfNeeded(stage: "requested")
scheduleFocusRestore(
for: window,
generation: generation,
delays: [0, 0.04, 0.12, 0.24, 0.4, 0.7]
)
}
func windowWillClose(_ notification: Notification) {
cancelPendingFocusRestore()
writeFocusDiagnosticsIfNeeded(stage: "windowWillClose")
}
func windowDidBecomeKey(_ notification: Notification) {
writeFocusDiagnosticsIfNeeded(stage: "didBecomeKey")
}
func windowDidResignKey(_ notification: Notification) {
guard let window else { return }
writeFocusDiagnosticsIfNeeded(stage: "didResignKey")
guard focusRestoreGeneration > 0 else { return }
scheduleFocusRestore(
for: window,
generation: focusRestoreGeneration,
delays: [0, 0.03, 0.1]
)
}
private func scheduleFocusRestore(
for window: NSWindow,
generation: Int,
delays: [TimeInterval]
) {
for (index, delay) in delays.enumerated() {
let isLastAttempt = index == delays.count - 1
let workItem = DispatchWorkItem { [weak self, weak window] in
guard let self, let window, window.isVisible else { return }
guard self.focusRestoreGeneration == generation else { return }
self.writeFocusDiagnosticsIfNeeded(stage: "restoreAttempt.\(index)")
if !window.isKeyWindow {
NSApp.activate(ignoringOtherApps: true)
window.orderFrontRegardless()
window.makeKeyAndOrderFront(nil)
self.writeFocusDiagnosticsIfNeeded(stage: "restoreApplied.\(index)")
}
if isLastAttempt, self.focusRestoreGeneration == generation {
self.focusRestoreGeneration = 0
}
}
pendingFocusRestoreWorkItems.append(workItem)
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
private func cancelPendingFocusRestore() {
pendingFocusRestoreWorkItems.forEach { $0.cancel() }
pendingFocusRestoreWorkItems.removeAll()
focusRestoreGeneration = 0
}
private func writeFocusDiagnosticsIfNeeded(stage: String) {
let env = ProcessInfo.processInfo.environment
guard let path = env["CMUX_UI_TEST_DIAGNOSTICS_PATH"], !path.isEmpty else { return }
var payload = loadFocusDiagnostics(at: path)
payload["focusStage"] = stage
payload["keyWindowIdentifier"] = NSApp.keyWindow?.identifier?.rawValue ?? ""
payload["mainWindowIdentifier"] = NSApp.mainWindow?.identifier?.rawValue ?? ""
payload["settingsWindowIsKey"] = (window?.isKeyWindow ?? false) ? "1" : "0"
guard let data = try? JSONSerialization.data(withJSONObject: payload) else { return }
try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
}
private func loadFocusDiagnostics(at path: String) -> [String: String] {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
return [:]
}
return object
}
}
enum SettingsNavigationTarget: String {
@ -3597,6 +3769,8 @@ struct SettingsView: View {
@AppStorage(LanguageSettings.languageKey) private var appLanguage = LanguageSettings.defaultLanguage.rawValue
@AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue
@AppStorage(AppIconSettings.modeKey) private var appIconMode = AppIconSettings.defaultMode.rawValue
@AppStorage(WorkspacePresentationModeSettings.modeKey)
private var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue
@AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue
@AppStorage(ClaudeCodeIntegrationSettings.hooksEnabledKey)
private var claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled
@ -3687,6 +3861,23 @@ struct SettingsView: View {
NewWorkspacePlacement(rawValue: newWorkspacePlacement) ?? WorkspacePlacementSettings.defaultPlacement
}
private var minimalModeEnabled: Bool {
WorkspacePresentationModeSettings.mode(for: workspacePresentationMode) == .minimal
}
private var minimalModeSubtitle: String {
if minimalModeEnabled {
return String(
localized: "settings.app.minimalMode.subtitleOn",
defaultValue: "Hide the workspace title bar and move workspace controls into the sidebar."
)
}
return String(
localized: "settings.app.minimalMode.subtitleOff",
defaultValue: "Use the standard workspace title bar and controls."
)
}
private var keepWorkspaceOpenOnLastSurfaceShortcut: Bool {
!closeWorkspaceOnLastSurfaceShortcut
}
@ -3782,6 +3973,18 @@ struct SettingsView: View {
)
}
private var minimalModeBinding: Binding<Bool> {
Binding(
get: { minimalModeEnabled },
set: { newValue in
workspacePresentationMode = newValue
? WorkspacePresentationModeSettings.Mode.minimal.rawValue
: WorkspacePresentationModeSettings.Mode.standard.rawValue
SettingsWindowController.shared.preserveFocusAfterPreferenceMutation()
}
)
}
private var settingsSidebarTintLightBinding: Binding<Color> {
Binding(
get: {
@ -4142,6 +4345,21 @@ struct SettingsView: View {
SettingsCardDivider()
SettingsCardRow(
String(localized: "settings.app.minimalMode", defaultValue: "Minimal Mode"),
subtitle: minimalModeSubtitle
) {
Toggle("", isOn: minimalModeBinding)
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsMinimalModeToggle")
.accessibilityLabel(
String(localized: "settings.app.minimalMode", defaultValue: "Minimal Mode")
)
}
SettingsCardDivider()
SettingsCardRow(
String(localized: "settings.app.closeWorkspaceOnLastSurfaceShortcut", defaultValue: "Keep Workspace Open When Closing Last Surface"),
subtitle: closeWorkspaceOnLastSurfaceShortcutSubtitle
@ -5305,6 +5523,12 @@ struct SettingsView: View {
ShortcutHintDebugSettings.resetVisibilityDefaults()
alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue
let defaults = UserDefaults.standard
defaults.removeObject(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.modeKey)
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
closeWorkspaceOnLastSurfaceShortcut = LastSurfaceCloseShortcutSettings.defaultValue
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
@ -5870,7 +6094,7 @@ private struct ShortcutSettingRow: View {
.onChange(of: shortcut) { newValue in
KeyboardShortcutSettings.setShortcut(newValue, for: action)
}
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
.onReceive(NotificationCenter.default.publisher(for: KeyboardShortcutSettings.didChangeNotification)) { _ in
let latest = KeyboardShortcutSettings.shortcut(for: action)
if latest != shortcut {
shortcut = latest

View file

@ -875,6 +875,224 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
}
}
func testMinimalModeUsesZeroTopSafeAreaForMainWindowContentView() {
guard let appDelegate = AppDelegate.shared else {
XCTFail("Expected AppDelegate.shared")
return
}
let defaults = UserDefaults.standard
let savedMode = defaults.object(forKey: WorkspacePresentationModeSettings.modeKey)
let savedLegacyTitlebar = defaults.object(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
defaults.set(WorkspacePresentationModeSettings.Mode.minimal.rawValue, forKey: WorkspacePresentationModeSettings.modeKey)
defaults.removeObject(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
defer {
restoreDefaultsValue(savedMode, forKey: WorkspacePresentationModeSettings.modeKey, defaults: defaults)
restoreDefaultsValue(savedLegacyTitlebar, forKey: WorkspaceTitlebarSettings.showTitlebarKey, defaults: defaults)
}
let windowId = appDelegate.createMainWindow()
defer { closeWindow(withId: windowId) }
guard let window = window(withId: windowId),
let contentView = window.contentView else {
XCTFail("Expected main window content view")
return
}
contentView.layoutSubtreeIfNeeded()
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05))
XCTAssertEqual(
contentView.safeAreaInsets.top,
0,
accuracy: 0.5,
"Minimal mode should not leave a top safe-area inset in the main window content view"
)
}
func testAttachUpdateAccessoryRemovesTitlebarAccessoryWhenMinimalModeEnabled() {
guard let appDelegate = AppDelegate.shared else {
XCTFail("Expected AppDelegate.shared")
return
}
let defaults = UserDefaults.standard
let savedMode = defaults.object(forKey: WorkspacePresentationModeSettings.modeKey)
let savedLegacyTitlebar = defaults.object(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
defaults.set(WorkspacePresentationModeSettings.Mode.standard.rawValue, forKey: WorkspacePresentationModeSettings.modeKey)
defaults.removeObject(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
defer {
restoreDefaultsValue(savedMode, forKey: WorkspacePresentationModeSettings.modeKey, defaults: defaults)
restoreDefaultsValue(savedLegacyTitlebar, forKey: WorkspaceTitlebarSettings.showTitlebarKey, defaults: defaults)
}
let windowId = appDelegate.createMainWindow()
defer { closeWindow(withId: windowId) }
guard let window = window(withId: windowId) else {
XCTFail("Expected main window")
return
}
let hasTitlebarAccessory: () -> Bool = {
window.titlebarAccessoryViewControllers.contains {
$0.view.identifier?.rawValue == "cmux.titlebarControls"
}
}
XCTAssertTrue(hasTitlebarAccessory(), "Expected visible-titlebar mode to attach the titlebar accessory")
defaults.set(WorkspacePresentationModeSettings.Mode.minimal.rawValue, forKey: WorkspacePresentationModeSettings.modeKey)
appDelegate.attachUpdateAccessory(to: window)
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05))
XCTAssertFalse(
hasTitlebarAccessory(),
"Minimal mode should remove the titlebar accessory instead of keeping a hidden controller attached"
)
}
func testWorkspaceButtonFadeModeDefaultsOffWhenTitlebarVisible() {
let defaults = UserDefaults.standard
let savedMode = defaults.object(forKey: WorkspaceButtonFadeSettings.modeKey)
let savedTitlebarVisibility = defaults.object(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
let savedLegacyTitlebarMode = defaults.object(forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
let savedLegacyPaneMode = defaults.object(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
defer {
restoreDefaultsValue(savedMode, forKey: WorkspaceButtonFadeSettings.modeKey, defaults: defaults)
restoreDefaultsValue(savedTitlebarVisibility, forKey: WorkspaceTitlebarSettings.showTitlebarKey, defaults: defaults)
restoreDefaultsValue(savedLegacyTitlebarMode, forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey, defaults: defaults)
restoreDefaultsValue(savedLegacyPaneMode, forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey, defaults: defaults)
}
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.modeKey)
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
defaults.set(true, forKey: WorkspaceTitlebarSettings.showTitlebarKey)
WorkspaceButtonFadeSettings.initializeStoredModeIfNeeded(defaults: defaults)
XCTAssertEqual(
defaults.string(forKey: WorkspaceButtonFadeSettings.modeKey),
WorkspaceButtonFadeSettings.Mode.disabled.rawValue
)
}
func testWorkspaceButtonFadeModeDefaultsOnWhenTitlebarHidden() {
let defaults = UserDefaults.standard
let savedMode = defaults.object(forKey: WorkspaceButtonFadeSettings.modeKey)
let savedTitlebarVisibility = defaults.object(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
let savedLegacyTitlebarMode = defaults.object(forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
let savedLegacyPaneMode = defaults.object(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
defer {
restoreDefaultsValue(savedMode, forKey: WorkspaceButtonFadeSettings.modeKey, defaults: defaults)
restoreDefaultsValue(savedTitlebarVisibility, forKey: WorkspaceTitlebarSettings.showTitlebarKey, defaults: defaults)
restoreDefaultsValue(savedLegacyTitlebarMode, forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey, defaults: defaults)
restoreDefaultsValue(savedLegacyPaneMode, forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey, defaults: defaults)
}
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.modeKey)
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
defaults.set(false, forKey: WorkspaceTitlebarSettings.showTitlebarKey)
WorkspaceButtonFadeSettings.initializeStoredModeIfNeeded(defaults: defaults)
XCTAssertEqual(
defaults.string(forKey: WorkspaceButtonFadeSettings.modeKey),
WorkspaceButtonFadeSettings.Mode.enabled.rawValue
)
}
func testWorkspaceButtonFadeModeMigratesLegacyHoverVisibilityPreference() {
let defaults = UserDefaults.standard
let savedMode = defaults.object(forKey: WorkspaceButtonFadeSettings.modeKey)
let savedTitlebarVisibility = defaults.object(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
let savedLegacyTitlebarMode = defaults.object(forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
let savedLegacyPaneMode = defaults.object(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
defer {
restoreDefaultsValue(savedMode, forKey: WorkspaceButtonFadeSettings.modeKey, defaults: defaults)
restoreDefaultsValue(savedTitlebarVisibility, forKey: WorkspaceTitlebarSettings.showTitlebarKey, defaults: defaults)
restoreDefaultsValue(savedLegacyTitlebarMode, forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey, defaults: defaults)
restoreDefaultsValue(savedLegacyPaneMode, forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey, defaults: defaults)
}
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.modeKey)
defaults.set(true, forKey: WorkspaceTitlebarSettings.showTitlebarKey)
defaults.set("always", forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
defaults.set("onHover", forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
WorkspaceButtonFadeSettings.initializeStoredModeIfNeeded(defaults: defaults)
XCTAssertEqual(
defaults.string(forKey: WorkspaceButtonFadeSettings.modeKey),
WorkspaceButtonFadeSettings.Mode.enabled.rawValue
)
}
func testWorkspaceButtonFadeModePreservesExistingStoredMode() {
let defaults = UserDefaults.standard
let savedMode = defaults.object(forKey: WorkspaceButtonFadeSettings.modeKey)
let savedTitlebarVisibility = defaults.object(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
let savedLegacyTitlebarMode = defaults.object(forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
let savedLegacyPaneMode = defaults.object(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
defer {
restoreDefaultsValue(savedMode, forKey: WorkspaceButtonFadeSettings.modeKey, defaults: defaults)
restoreDefaultsValue(savedTitlebarVisibility, forKey: WorkspaceTitlebarSettings.showTitlebarKey, defaults: defaults)
restoreDefaultsValue(savedLegacyTitlebarMode, forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey, defaults: defaults)
restoreDefaultsValue(savedLegacyPaneMode, forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey, defaults: defaults)
}
defaults.set(WorkspaceButtonFadeSettings.Mode.disabled.rawValue, forKey: WorkspaceButtonFadeSettings.modeKey)
defaults.set(false, forKey: WorkspaceTitlebarSettings.showTitlebarKey)
defaults.set("onHover", forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
defaults.set("onHover", forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
WorkspaceButtonFadeSettings.initializeStoredModeIfNeeded(defaults: defaults)
XCTAssertEqual(
defaults.string(forKey: WorkspaceButtonFadeSettings.modeKey),
WorkspaceButtonFadeSettings.Mode.disabled.rawValue
)
}
func testWorkspaceMinimalModeDefaultsToStandardPresentation() {
let defaults = UserDefaults.standard
let savedMode = defaults.object(forKey: WorkspacePresentationModeSettings.modeKey)
let savedLegacyTitlebar = defaults.object(forKey: WorkspaceTitlebarSettings.showTitlebarKey)
let savedLegacyFade = defaults.object(forKey: WorkspaceButtonFadeSettings.modeKey)
defer {
restoreDefaultsValue(savedMode, forKey: WorkspacePresentationModeSettings.modeKey, defaults: defaults)
restoreDefaultsValue(savedLegacyTitlebar, forKey: WorkspaceTitlebarSettings.showTitlebarKey, defaults: defaults)
restoreDefaultsValue(savedLegacyFade, forKey: WorkspaceButtonFadeSettings.modeKey, defaults: defaults)
}
defaults.removeObject(forKey: WorkspacePresentationModeSettings.modeKey)
defaults.set(false, forKey: WorkspaceTitlebarSettings.showTitlebarKey)
defaults.set(WorkspaceButtonFadeSettings.Mode.enabled.rawValue, forKey: WorkspaceButtonFadeSettings.modeKey)
XCTAssertEqual(
WorkspacePresentationModeSettings.mode(defaults: defaults),
.standard
)
}
func testKeyboardShortcutSettingsSetShortcutPostsSpecificChangeNotification() {
let notificationName = Notification.Name("cmux.keyboardShortcutSettingsDidChange")
let expectedAction = KeyboardShortcutSettings.Action.toggleSidebar.rawValue
let expectation = expectation(forNotification: notificationName, object: nil) { notification in
notification.userInfo?["action"] as? String == expectedAction
}
KeyboardShortcutSettings.setShortcut(
StoredShortcut(key: "s", command: true, shift: false, option: false, control: true),
for: .toggleSidebar
)
wait(for: [expectation], timeout: 0.2)
}
func testCmdPhysicalPWithDvorakCharactersDoesNotTriggerCommandPaletteSwitcher() {
guard let appDelegate = AppDelegate.shared else {
XCTFail("Expected AppDelegate.shared")
@ -2920,6 +3138,14 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
window.performClose(nil)
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05))
}
private func restoreDefaultsValue(_ value: Any?, forKey key: String, defaults: UserDefaults) {
if let value {
defaults.set(value, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
}
}
private final class CommandPaletteMarkedTextFieldEditor: NSTextView {

View file

@ -12,34 +12,40 @@ final class GhosttyEnsureFocusWindowActivationTests: XCTestCase {
func testAllowsActivationForActiveManager() {
let activeManager = TabManager()
let otherManager = TabManager()
let targetWindow = NSWindow()
let otherWindow = NSWindow()
XCTAssertTrue(
shouldAllowEnsureFocusWindowActivation(
activeTabManager: activeManager,
targetTabManager: activeManager,
keyWindow: NSWindow(),
mainWindow: NSWindow()
keyWindow: targetWindow,
mainWindow: targetWindow,
targetWindow: targetWindow
)
)
XCTAssertFalse(
shouldAllowEnsureFocusWindowActivation(
activeTabManager: activeManager,
targetTabManager: otherManager,
keyWindow: NSWindow(),
mainWindow: NSWindow()
keyWindow: otherWindow,
mainWindow: otherWindow,
targetWindow: targetWindow
)
)
}
func testAllowsActivationWhenAppHasNoKeyAndNoMainWindow() {
let targetManager = TabManager()
let targetWindow = NSWindow()
XCTAssertTrue(
shouldAllowEnsureFocusWindowActivation(
activeTabManager: nil,
targetTabManager: targetManager,
keyWindow: nil,
mainWindow: nil
mainWindow: nil,
targetWindow: targetWindow
)
)
XCTAssertFalse(
@ -47,7 +53,8 @@ final class GhosttyEnsureFocusWindowActivationTests: XCTestCase {
activeTabManager: nil,
targetTabManager: targetManager,
keyWindow: NSWindow(),
mainWindow: nil
mainWindow: nil,
targetWindow: targetWindow
)
)
XCTAssertFalse(
@ -55,7 +62,8 @@ final class GhosttyEnsureFocusWindowActivationTests: XCTestCase {
activeTabManager: nil,
targetTabManager: targetManager,
keyWindow: nil,
mainWindow: NSWindow()
mainWindow: NSWindow(),
targetWindow: targetWindow
)
)
}

View file

@ -0,0 +1,603 @@
import XCTest
import Foundation
import AppKit
import CoreGraphics
final class BonsplitTabDragUITests: XCTestCase {
private let launchTimeout: TimeInterval = 20.0
private let setupTimeout: TimeInterval = 25.0
override func setUp() {
super.setUp()
continueAfterFailure = false
let cleanup = XCUIApplication()
cleanup.terminate()
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
}
func testMinimalModeKeepsTabReorderWorking() {
let (app, dataPath) = launchConfiguredApp()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: launchTimeout),
"Expected app to launch for minimal-mode Bonsplit tab drag UI test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: setupTimeout), "Expected tab-drag setup data at \(dataPath)")
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: setupTimeout) else {
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
return
}
if let setupError = ready["setupError"], !setupError.isEmpty {
XCTFail("Setup failed: \(setupError)")
return
}
let alphaTitle = ready["alphaTitle"] ?? "UITest Alpha"
let betaTitle = ready["betaTitle"] ?? "UITest Beta"
let window = app.windows.element(boundBy: 0)
let alphaTab = app.buttons[alphaTitle]
let betaTab = app.buttons[betaTitle]
let dropIndicator = app.descendants(matching: .any).matching(identifier: "paneTabBar.dropIndicator").firstMatch
let initialOrder = "\(alphaTitle)|\(betaTitle)"
let reorderedOrder = "\(betaTitle)|\(alphaTitle)"
XCTAssertTrue(window.waitForExistence(timeout: 5.0), "Expected main window to exist")
XCTAssertTrue(alphaTab.waitForExistence(timeout: 5.0), "Expected alpha tab to exist")
XCTAssertTrue(betaTab.waitForExistence(timeout: 5.0), "Expected beta tab to exist")
XCTAssertTrue(
waitForJSONKey("trackedPaneTabTitles", equals: initialOrder, atPath: dataPath, timeout: 5.0) != nil,
"Expected initial tracked tab order to be \(initialOrder). data=\(loadJSON(atPath: dataPath) ?? [:])"
)
XCTAssertLessThan(alphaTab.frame.minX, betaTab.frame.minX, "Expected beta tab to start to the right of alpha")
let windowFrameBeforeDrag = window.frame
let start = CGPoint(x: betaTab.frame.midX, y: betaTab.frame.midY)
let destination = CGPoint(x: alphaTab.frame.midX - 14, y: alphaTab.frame.midY)
guard let dragSession = beginMouseDrag(
fromAccessibilityPoint: start,
holdDuration: 0.20
) else {
XCTFail("Expected raw mouse drag session to start")
return
}
continueMouseDrag(
dragSession,
toAccessibilityPoint: destination,
steps: 28,
dragDuration: 0.45
)
XCTAssertTrue(
waitForCondition(timeout: 2.0) { dropIndicator.exists },
"Expected dragging beta onto alpha to reveal the Bonsplit drop indicator."
)
endMouseDrag(dragSession, atAccessibilityPoint: destination)
XCTAssertTrue(
waitForJSONKey("trackedPaneTabTitles", equals: reorderedOrder, atPath: dataPath, timeout: 5.0) != nil,
"Expected tracked tab order to become \(reorderedOrder). data=\(loadJSON(atPath: dataPath) ?? [:])"
)
XCTAssertTrue(
waitForCondition(timeout: 5.0) { betaTab.frame.minX < alphaTab.frame.minX },
"Expected dragging beta onto alpha to reorder tab frames. alpha=\(alphaTab.frame) beta=\(betaTab.frame)"
)
XCTAssertEqual(window.frame.origin.x, windowFrameBeforeDrag.origin.x, accuracy: 2.0, "Expected tab drag not to move the window horizontally")
XCTAssertEqual(window.frame.origin.y, windowFrameBeforeDrag.origin.y, accuracy: 2.0, "Expected tab drag not to move the window vertically")
}
func testMinimalModePlacesPaneTabBarAtTopEdge() {
let (app, dataPath) = launchConfiguredApp()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: launchTimeout),
"Expected app to launch for minimal-mode top-gap UI test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: setupTimeout), "Expected tab-drag setup data at \(dataPath)")
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: setupTimeout) else {
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
return
}
if let setupError = ready["setupError"], !setupError.isEmpty {
XCTFail("Setup failed: \(setupError)")
return
}
let window = app.windows.element(boundBy: 0)
XCTAssertTrue(window.waitForExistence(timeout: 5.0), "Expected main window to exist")
let alphaTitle = ready["alphaTitle"] ?? "UITest Alpha"
let alphaTab = app.buttons[alphaTitle]
XCTAssertTrue(alphaTab.waitForExistence(timeout: 5.0), "Expected alpha tab to exist")
let gapIfOriginIsBottomLeft = abs(window.frame.maxY - alphaTab.frame.maxY)
let gapIfOriginIsTopLeft = abs(alphaTab.frame.minY - window.frame.minY)
let topGap = min(gapIfOriginIsBottomLeft, gapIfOriginIsTopLeft)
XCTAssertLessThanOrEqual(
topGap,
8,
"Expected the selected pane tab to reach the top edge in minimal mode. window=\(window.frame) alphaTab=\(alphaTab.frame) gap.bottomLeft=\(gapIfOriginIsBottomLeft) gap.topLeft=\(gapIfOriginIsTopLeft)"
)
}
func testMinimalModeKeepsSidebarRowsBelowTrafficLights() {
let (app, dataPath) = launchConfiguredApp()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: launchTimeout),
"Expected app to launch for minimal-mode sidebar inset UI test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: setupTimeout), "Expected tab-drag setup data at \(dataPath)")
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: setupTimeout) else {
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
return
}
if let setupError = ready["setupError"], !setupError.isEmpty {
XCTFail("Setup failed: \(setupError)")
return
}
let window = app.windows.element(boundBy: 0)
XCTAssertTrue(window.waitForExistence(timeout: 5.0), "Expected main window to exist")
let workspaceId = ready["workspaceId"] ?? ""
let workspaceRowIdentifier = "sidebarWorkspace.\(workspaceId)"
let workspaceRow = app.descendants(matching: .any).matching(identifier: workspaceRowIdentifier).firstMatch
XCTAssertTrue(workspaceRow.waitForExistence(timeout: 5.0), "Expected workspace row to exist")
let topInset = distanceToTopEdge(of: workspaceRow, in: window)
XCTAssertEqual(
topInset,
36,
accuracy: 4,
"Expected minimal mode to keep the sidebar workspace row offset unchanged while reserving the existing traffic-light strip. window=\(window.frame) workspaceRow=\(workspaceRow.frame) topInset=\(topInset)"
)
}
func testStandardModeKeepsWorkspaceControlsOutOfSidebar() {
let (app, dataPath) = launchConfiguredApp(presentationMode: .standard)
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: launchTimeout),
"Expected app to launch for standard-mode sidebar control placement UI test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: setupTimeout), "Expected tab-drag setup data at \(dataPath)")
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: setupTimeout) else {
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
return
}
if let setupError = ready["setupError"], !setupError.isEmpty {
XCTFail("Setup failed: \(setupError)")
return
}
let window = app.windows.element(boundBy: 0)
XCTAssertTrue(window.waitForExistence(timeout: 5.0), "Expected main window to exist")
let sidebar = app.descendants(matching: .any).matching(identifier: "Sidebar").firstMatch
XCTAssertTrue(sidebar.waitForExistence(timeout: 5.0), "Expected sidebar to exist")
let toggleSidebarButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.toggleSidebar").firstMatch
let notificationsButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.showNotifications").firstMatch
let newWorkspaceButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.newTab").firstMatch
XCTAssertTrue(
waitForCondition(timeout: 2.0) {
toggleSidebarButton.exists && toggleSidebarButton.isHittable &&
notificationsButton.exists && notificationsButton.isHittable &&
newWorkspaceButton.exists && newWorkspaceButton.isHittable
},
"Expected standard mode to keep workspace controls visible in the titlebar."
)
let leadingControlX = min(
toggleSidebarButton.frame.minX,
notificationsButton.frame.minX,
newWorkspaceButton.frame.minX
)
XCTAssertGreaterThanOrEqual(
leadingControlX,
sidebar.frame.maxX - 4,
"Expected standard mode workspace controls to stay outside the sidebar header. sidebar=\(sidebar.frame) toggle=\(toggleSidebarButton.frame) notifications=\(notificationsButton.frame) new=\(newWorkspaceButton.frame)"
)
}
func testMinimalModeSidebarControlsRevealOnlyFromSidebarHover() {
let (app, dataPath) = launchConfiguredApp()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: launchTimeout),
"Expected app to launch for minimal-mode sidebar hover UI test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: setupTimeout), "Expected tab-drag setup data at \(dataPath)")
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: setupTimeout) else {
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
return
}
if let setupError = ready["setupError"], !setupError.isEmpty {
XCTFail("Setup failed: \(setupError)")
return
}
let window = app.windows.element(boundBy: 0)
XCTAssertTrue(window.waitForExistence(timeout: 5.0), "Expected main window to exist")
let sidebar = app.descendants(matching: .any).matching(identifier: "Sidebar").firstMatch
XCTAssertTrue(sidebar.waitForExistence(timeout: 5.0), "Expected sidebar to exist")
let toggleSidebarButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.toggleSidebar").firstMatch
let notificationsButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.showNotifications").firstMatch
let newWorkspaceButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.newTab").firstMatch
let alphaTitle = ready["alphaTitle"] ?? "UITest Alpha"
let alphaTab = app.buttons[alphaTitle]
XCTAssertTrue(alphaTab.waitForExistence(timeout: 5.0), "Expected alpha tab to exist")
let paneLeadingGap = alphaTab.frame.minX - sidebar.frame.maxX
XCTAssertLessThan(
paneLeadingGap,
28,
"Expected visible-sidebar minimal mode to keep pane tabs tight to the sidebar edge while the traffic lights sit over the sidebar. window=\(window.frame) sidebar=\(sidebar.frame) alphaTab=\(alphaTab.frame) paneLeadingGap=\(paneLeadingGap)"
)
window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8)).hover()
XCTAssertTrue(
waitForCondition(timeout: 2.0) {
!toggleSidebarButton.isHittable && !notificationsButton.isHittable && !newWorkspaceButton.isHittable
},
"Expected minimal-mode sidebar controls to stay hidden away from the sidebar hover zone."
)
hover(in: window, at: CGPoint(x: window.frame.maxX - 48, y: window.frame.minY + 18))
XCTAssertTrue(
waitForCondition(timeout: 2.0) {
!toggleSidebarButton.isHittable && !notificationsButton.isHittable && !newWorkspaceButton.isHittable
},
"Expected the removed titlebar area to stop revealing minimal-mode controls."
)
hover(
in: window,
at: CGPoint(
x: min(sidebar.frame.maxX - 36, sidebar.frame.minX + 116),
y: window.frame.minY + 18
)
)
XCTAssertTrue(
waitForCondition(timeout: 2.0) {
toggleSidebarButton.exists && toggleSidebarButton.isHittable &&
notificationsButton.exists && notificationsButton.isHittable &&
newWorkspaceButton.exists && newWorkspaceButton.isHittable
},
"Expected minimal-mode sidebar controls to reveal when hovering the sidebar chrome area."
)
}
func testMinimalModeCollapsedSidebarKeepsWorkspaceControlsSuppressed() {
let (app, dataPath) = launchConfiguredApp(startWithHiddenSidebar: true)
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: launchTimeout),
"Expected app to launch for collapsed-sidebar minimal-mode controls UI test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: setupTimeout), "Expected tab-drag setup data at \(dataPath)")
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: setupTimeout) else {
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
return
}
if let setupError = ready["setupError"], !setupError.isEmpty {
XCTFail("Setup failed: \(setupError)")
return
}
XCTAssertEqual(ready["sidebarVisible"], "0", "Expected hidden-sidebar UI test setup to collapse the sidebar. data=\(ready)")
let window = app.windows.element(boundBy: 0)
XCTAssertTrue(window.waitForExistence(timeout: 5.0), "Expected main window to exist")
let alphaTitle = ready["alphaTitle"] ?? "UITest Alpha"
let alphaTab = app.buttons[alphaTitle]
XCTAssertTrue(alphaTab.waitForExistence(timeout: 5.0), "Expected alpha tab to exist")
let toggleSidebarButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.toggleSidebar").firstMatch
let notificationsButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.showNotifications").firstMatch
let newWorkspaceButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.newTab").firstMatch
hover(in: window, at: CGPoint(x: window.frame.maxX - 48, y: window.frame.minY + 18))
XCTAssertTrue(
waitForCondition(timeout: 2.0) {
(!toggleSidebarButton.exists || !toggleSidebarButton.isHittable) &&
(!notificationsButton.exists || !notificationsButton.isHittable) &&
(!newWorkspaceButton.exists || !newWorkspaceButton.isHittable)
},
"Expected collapsed-sidebar minimal mode to keep workspace controls suppressed. toggle=\(toggleSidebarButton.debugDescription) notifications=\(notificationsButton.debugDescription) new=\(newWorkspaceButton.debugDescription)"
)
let leadingInset = alphaTab.frame.minX - window.frame.minX
XCTAssertLessThan(
leadingInset,
96,
"Expected pane tabs to stay near the leading edge when collapsed-sidebar minimal mode removes the titlebar accessory lane. window=\(window.frame) alphaTab=\(alphaTab.frame) leadingInset=\(leadingInset)"
)
}
func testMinimalModeSidebarControlsRemainVisibleWhileNotificationsPopoverIsShown() {
let (app, dataPath) = launchConfiguredApp()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: launchTimeout),
"Expected app to launch for minimal-mode notifications-popover pinning UI test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: setupTimeout), "Expected tab-drag setup data at \(dataPath)")
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: setupTimeout) else {
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
return
}
if let setupError = ready["setupError"], !setupError.isEmpty {
XCTFail("Setup failed: \(setupError)")
return
}
let window = app.windows.element(boundBy: 0)
XCTAssertTrue(window.waitForExistence(timeout: 5.0), "Expected main window to exist")
let toggleSidebarButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.toggleSidebar").firstMatch
let notificationsButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.showNotifications").firstMatch
let newWorkspaceButton = app.descendants(matching: .any).matching(identifier: "titlebarControl.newTab").firstMatch
window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8)).hover()
XCTAssertTrue(
waitForCondition(timeout: 2.0) {
!toggleSidebarButton.isHittable && !notificationsButton.isHittable && !newWorkspaceButton.isHittable
},
"Expected minimal-mode sidebar controls to start hidden away from hover."
)
app.typeKey("i", modifierFlags: [.command])
XCTAssertTrue(
app.buttons["notificationsPopover.jumpToLatest"].waitForExistence(timeout: 6.0)
|| app.staticTexts["No notifications yet"].waitForExistence(timeout: 6.0),
"Expected notifications popover to open."
)
window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8)).hover()
XCTAssertTrue(
waitForCondition(timeout: 2.0) {
toggleSidebarButton.exists && toggleSidebarButton.isHittable &&
notificationsButton.exists && notificationsButton.isHittable &&
newWorkspaceButton.exists && newWorkspaceButton.isHittable
},
"Expected minimal-mode sidebar controls to remain visible while the notifications popover is open."
)
}
func testMinimalModeCollapsedSidebarStillRevealsPaneTabBarControlsOnHover() {
let (app, dataPath) = launchConfiguredApp(startWithHiddenSidebar: true)
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: launchTimeout),
"Expected app to launch for collapsed-sidebar minimal-mode Bonsplit controls hover UI test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: setupTimeout), "Expected tab-drag setup data at \(dataPath)")
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: setupTimeout) else {
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
return
}
if let setupError = ready["setupError"], !setupError.isEmpty {
XCTFail("Setup failed: \(setupError)")
return
}
let window = app.windows.element(boundBy: 0)
XCTAssertTrue(window.waitForExistence(timeout: 5.0), "Expected main window to exist")
let alphaTitle = ready["alphaTitle"] ?? "UITest Alpha"
let betaTitle = ready["betaTitle"] ?? "UITest Beta"
let alphaTab = app.buttons[alphaTitle]
XCTAssertTrue(alphaTab.waitForExistence(timeout: 5.0), "Expected alpha tab to exist")
let betaTab = app.buttons[betaTitle]
XCTAssertTrue(betaTab.waitForExistence(timeout: 5.0), "Expected beta tab to exist")
let newTerminalButton = app.descendants(matching: .any).matching(identifier: "paneTabBarControl.newTerminal").firstMatch
window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8)).hover()
XCTAssertTrue(
waitForCondition(timeout: 2.0) { !newTerminalButton.exists || !newTerminalButton.isHittable },
"Expected pane tab bar controls to hide away from the pane tab bar in minimal mode. button=\(newTerminalButton.debugDescription)"
)
hover(
in: window,
at: CGPoint(
x: min(window.frame.maxX - 140, betaTab.frame.maxX + 80),
y: alphaTab.frame.midY
)
)
XCTAssertTrue(
waitForCondition(timeout: 2.0) { newTerminalButton.exists && newTerminalButton.isHittable },
"Expected pane tab bar controls to reveal when hovering inside empty pane-tab-bar space in collapsed-sidebar minimal mode. window=\(window.frame) alphaTab=\(alphaTab.frame) betaTab=\(betaTab.frame) button=\(newTerminalButton.debugDescription)"
)
window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8)).hover()
XCTAssertTrue(
waitForCondition(timeout: 2.0) { !newTerminalButton.exists || !newTerminalButton.isHittable },
"Expected pane tab bar controls to hide again after leaving the pane tab bar in minimal mode. button=\(newTerminalButton.debugDescription)"
)
}
private enum WorkspacePresentationMode: String {
case standard
case minimal
}
private func launchConfiguredApp(
startWithHiddenSidebar: Bool = false,
presentationMode: WorkspacePresentationMode = .minimal
) -> (XCUIApplication, String) {
let app = XCUIApplication()
let dataPath = "/tmp/cmux-ui-test-bonsplit-tab-drag-\(UUID().uuidString).json"
try? FileManager.default.removeItem(atPath: dataPath)
app.launchEnvironment["CMUX_UI_TEST_BONSPLIT_TAB_DRAG_SETUP"] = "1"
app.launchEnvironment["CMUX_UI_TEST_BONSPLIT_TAB_DRAG_PATH"] = dataPath
if startWithHiddenSidebar {
app.launchEnvironment["CMUX_UI_TEST_BONSPLIT_START_WITH_HIDDEN_SIDEBAR"] = "1"
}
app.launchArguments += ["-workspacePresentationMode", presentationMode.rawValue]
app.launch()
app.activate()
return (app, dataPath)
}
private func ensureForegroundAfterLaunch(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
if app.wait(for: .runningForeground, timeout: timeout) {
return true
}
if app.state == .runningBackground {
app.activate()
return app.wait(for: .runningForeground, timeout: 6.0)
}
return false
}
private func waitForAnyJSON(atPath path: String, timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if loadJSON(atPath: path) != nil { return true }
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return loadJSON(atPath: path) != nil
}
private func waitForJSONKey(_ key: String, equals expected: String, atPath path: String, timeout: TimeInterval) -> [String: String]? {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if let data = loadJSON(atPath: path), data[key] == expected {
return data
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
if let data = loadJSON(atPath: path), data[key] == expected {
return data
}
return nil
}
private func loadJSON(atPath path: String) -> [String: String]? {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
return nil
}
return object
}
private func waitForCondition(timeout: TimeInterval, _ condition: () -> Bool) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if condition() { return true }
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return condition()
}
private func hover(in window: XCUIElement, at point: CGPoint) {
let origin = window.coordinate(withNormalizedOffset: .zero)
origin.withOffset(
CGVector(
dx: point.x - window.frame.minX,
dy: point.y - window.frame.minY
)
).hover()
}
private func distanceToTopEdge(of element: XCUIElement, in window: XCUIElement) -> CGFloat {
let gapIfOriginIsBottomLeft = abs(window.frame.maxY - element.frame.maxY)
let gapIfOriginIsTopLeft = abs(element.frame.minY - window.frame.minY)
return min(gapIfOriginIsBottomLeft, gapIfOriginIsTopLeft)
}
private struct RawMouseDragSession {
let source: CGEventSource
}
private func beginMouseDrag(
fromAccessibilityPoint start: CGPoint,
holdDuration: TimeInterval = 0.15
) -> RawMouseDragSession? {
let source = CGEventSource(stateID: .hidSystemState)
XCTAssertNotNil(source, "Expected CGEventSource for raw mouse drag")
guard let source else { return nil }
let quartzStart = quartzPoint(fromAccessibilityPoint: start)
postMouseEvent(type: .mouseMoved, at: quartzStart, source: source)
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
postMouseEvent(type: .leftMouseDown, at: quartzStart, source: source)
RunLoop.current.run(until: Date().addingTimeInterval(holdDuration))
return RawMouseDragSession(source: source)
}
private func continueMouseDrag(
_ session: RawMouseDragSession,
toAccessibilityPoint end: CGPoint,
steps: Int = 20,
dragDuration: TimeInterval = 0.30
) {
let currentLocation = NSEvent.mouseLocation
let quartzEnd = quartzPoint(fromAccessibilityPoint: end)
let clampedSteps = max(2, steps)
for step in 1...clampedSteps {
let progress = CGFloat(step) / CGFloat(clampedSteps)
let point = CGPoint(
x: currentLocation.x + ((quartzEnd.x - currentLocation.x) * progress),
y: currentLocation.y + ((quartzEnd.y - currentLocation.y) * progress)
)
postMouseEvent(type: .leftMouseDragged, at: point, source: session.source)
RunLoop.current.run(until: Date().addingTimeInterval(dragDuration / Double(clampedSteps)))
}
}
private func endMouseDrag(
_ session: RawMouseDragSession,
atAccessibilityPoint end: CGPoint
) {
let quartzEnd = quartzPoint(fromAccessibilityPoint: end)
postMouseEvent(type: .leftMouseUp, at: quartzEnd, source: session.source)
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
}
private func postMouseEvent(
type: CGEventType,
at point: CGPoint,
source: CGEventSource
) {
guard let event = CGEvent(
mouseEventSource: source,
mouseType: type,
mouseCursorPosition: point,
mouseButton: .left
) else {
XCTFail("Expected CGEvent for mouse type \(type.rawValue) at \(point)")
return
}
event.setIntegerValueField(.mouseEventClickState, value: 1)
event.post(tap: .cghidEventTap)
}
private func quartzPoint(fromAccessibilityPoint point: CGPoint) -> CGPoint {
let desktopBounds = NSScreen.screens.reduce(CGRect.null) { partialResult, screen in
partialResult.union(screen.frame)
}
XCTAssertFalse(desktopBounds.isNull, "Expected at least one screen when converting raw mouse coordinates")
guard !desktopBounds.isNull else { return point }
return CGPoint(x: point.x, y: desktopBounds.maxY - point.y)
}
}

View file

@ -519,6 +519,164 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
)
}
func testMinimalModeToggleKeepsSettingsWindowFocused() throws {
let app = XCUIApplication()
let diagnosticsPath = "/tmp/cmux-ui-test-settings-focus-\(UUID().uuidString).json"
try? FileManager.default.removeItem(atPath: diagnosticsPath)
app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"]
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
app.launchEnvironment["CMUX_UI_TEST_SHOW_SETTINGS"] = "1"
app.launchEnvironment["CMUX_UI_TEST_DIAGNOSTICS_PATH"] = diagnosticsPath
launchAndActivate(app)
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 8.0) {
app.windows.count >= 2
},
"Expected the main window and Settings window to be visible"
)
focusSettingsWindow(app: app)
let toggle = try requireMinimalModeToggle(app: app)
let initialState = toggleIsOn(toggle)
toggle.click()
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 3.0) {
toggle.exists && toggleIsOn(toggle) != initialState
},
"Expected the minimal mode setting to toggle"
)
let diagnostics = waitForDiagnostics(
at: diagnosticsPath,
timeout: 3.0
) { data in
data["keyWindowIdentifier"] == "cmux.settings" && data["settingsWindowIsKey"] == "1"
}
XCTAssertEqual(
diagnostics?["keyWindowIdentifier"],
"cmux.settings",
"Expected the Settings window to remain key after toggling minimal mode. diagnostics=\(diagnostics ?? [:])"
)
XCTAssertEqual(
diagnostics?["settingsWindowIsKey"],
"1",
"Expected the Settings window to report itself as key after toggling minimal mode. diagnostics=\(diagnostics ?? [:])"
)
XCTAssertTrue(
diagnosticsRemainStable(
at: diagnosticsPath,
duration: 0.8
) { data in
data["keyWindowIdentifier"] == "cmux.settings" && data["settingsWindowIsKey"] == "1"
},
"Expected the Settings window to stay key after toggling minimal mode. diagnostics=\(loadDiagnostics(at: diagnosticsPath) ?? [:])"
)
app.typeKey("w", modifierFlags: [.command])
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 3.0) {
app.windows.count == 1 && !toggle.exists
},
"Expected Cmd+W after toggling minimal mode to close the focused Settings window instead of defocusing back to the workspace window"
)
}
func testCommandPaletteCanEnableAndDisableMinimalMode() throws {
let app = XCUIApplication()
configureSocketControlledLaunch(app, showSettingsWindow: true)
app.launchArguments += ["-workspacePresentationMode", "standard"]
launchAndActivate(app)
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 8.0) {
app.windows.count >= 2
},
"Expected the main window and Settings window to be visible"
)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)")
let mainWindowId = try XCTUnwrap(
socketCommand("current_window")?.trimmingCharacters(in: .whitespacesAndNewlines)
)
focusSettingsWindow(app: app)
let toggle = try requireMinimalModeToggle(app: app)
if toggleIsOn(toggle) {
toggle.click()
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 3.0) {
toggle.exists && !toggleIsOn(toggle)
},
"Expected the minimal mode setting to start from off for this test"
)
}
XCTAssertEqual(socketCommand("focus_window \(mainWindowId)"), "OK")
openCommandPaletteCommands(app: app)
let searchField = app.textFields["CommandPaletteSearchField"]
searchField.typeText("minimal")
let enableSnapshot = try XCTUnwrap(
waitForCommandPaletteSnapshot(windowId: mainWindowId, mode: "commands", query: "minimal", timeout: 5.0) { snapshot in
self.commandPaletteResultRows(from: snapshot).contains { row in
(row["command_id"] as? String) == "palette.enableMinimalMode"
}
},
"Expected the command palette to show Enable Minimal Mode while standard mode is active"
)
XCTAssertFalse(
commandPaletteResultRows(from: enableSnapshot).contains { row in
(row["command_id"] as? String) == "palette.disableMinimalMode"
},
"Expected Disable Minimal Mode to stay hidden while standard mode is active. snapshot=\(enableSnapshot)"
)
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
focusSettingsWindow(app: app)
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 3.0) {
toggle.exists && toggleIsOn(toggle)
},
"Expected running the command palette action to enable minimal mode"
)
XCTAssertEqual(socketCommand("focus_window \(mainWindowId)"), "OK")
openCommandPaletteCommands(app: app)
let disableSearchField = app.textFields["CommandPaletteSearchField"]
disableSearchField.typeText("minimal")
let disableSnapshot = try XCTUnwrap(
waitForCommandPaletteSnapshot(windowId: mainWindowId, mode: "commands", query: "minimal", timeout: 5.0) { snapshot in
self.commandPaletteResultRows(from: snapshot).contains { row in
(row["command_id"] as? String) == "palette.disableMinimalMode"
}
},
"Expected the command palette to show Disable Minimal Mode while minimal mode is active"
)
XCTAssertFalse(
commandPaletteResultRows(from: disableSnapshot).contains { row in
(row["command_id"] as? String) == "palette.enableMinimalMode"
},
"Expected Enable Minimal Mode to stay hidden while minimal mode is active. snapshot=\(disableSnapshot)"
)
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
focusSettingsWindow(app: app)
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 3.0) {
toggle.exists && !toggleIsOn(toggle)
},
"Expected running the command palette action to disable minimal mode"
)
}
func testSwitcherEmptyStateDoesNotBlinkWhileRefiningNoMatchQuery() throws {
let app = XCUIApplication()
configureSocketControlledLaunch(app)
@ -672,6 +830,31 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
throw XCTSkip("Could not find the command palette all-surfaces toggle")
}
private func requireMinimalModeToggle(app: XCUIApplication) throws -> XCUIElement {
let scrollView = app.scrollViews.firstMatch
let candidates = [
app.switches["SettingsMinimalModeToggle"],
app.checkBoxes["SettingsMinimalModeToggle"],
app.buttons["SettingsMinimalModeToggle"],
app.otherElements["SettingsMinimalModeToggle"],
app.switches["Minimal Mode"],
app.checkBoxes["Minimal Mode"],
app.buttons["Minimal Mode"],
app.otherElements["Minimal Mode"],
]
for _ in 0..<8 {
if let element = firstExistingElement(candidates: candidates, timeout: 0.4), element.isHittable {
return element
}
if scrollView.exists {
scrollView.swipeUp()
}
}
throw XCTSkip("Could not find the minimal mode toggle")
}
private func toggleIsOn(_ element: XCUIElement) -> Bool {
let value = String(describing: element.value ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return value == "1" || value == "true" || value == "on"
@ -833,6 +1016,50 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
return ControlSocketClient(path: socketPath, responseTimeout: 2.0).sendJSON(request)
}
private func waitForDiagnostics(
at path: String,
timeout: TimeInterval,
condition: ([String: String]) -> Bool
) -> [String: String]? {
let deadline = Date().addingTimeInterval(timeout)
var last: [String: String]?
while Date() < deadline {
if let data = loadDiagnostics(at: path) {
last = data
if condition(data) {
return data
}
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return last
}
private func diagnosticsRemainStable(
at path: String,
duration: TimeInterval,
condition: ([String: String]) -> Bool
) -> Bool {
let deadline = Date().addingTimeInterval(duration)
while Date() < deadline {
guard let data = loadDiagnostics(at: path), condition(data) else {
return false
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return true
}
private func loadDiagnostics(at path: String) -> [String: String]? {
guard let raw = try? Data(contentsOf: URL(fileURLWithPath: path)),
let object = try? JSONSerialization.jsonObject(with: raw) as? [String: String] else {
return nil
}
return object
}
private final class ControlSocketClient {
private let path: String
private let responseTimeout: TimeInterval

2
vendor/bonsplit vendored

@ -1 +1 @@
Subproject commit 02fa188ccd244b1e6efc037e4ed631e966144795
Subproject commit 31c3810a3411d792da6f60e5f5da3deca0b637e5