From 7d3320a0d733ab9331de03e830f16b5766e01199 Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Wed, 17 Jun 2026 16:54:16 -0700 Subject: [PATCH 1/3] Add focused-window workspace close shortcut regression --- .../CloseWorkspacesConfirmDialogUITests.swift | 149 +++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift b/cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift index 4e4945c9d740..a2208d517d44 100644 --- a/cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift +++ b/cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift @@ -178,6 +178,73 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase { clickCancelOnCloseWorkspacesAlert(app: app) } + func testCmdShiftWTargetsFocusedWindowWorkspaceWhenMultipleWindowsAreOpen() { + let app = XCUIApplication() + let recorderPath = "/tmp/cmux-ui-test-close-workspace-focused-window-\(UUID().uuidString).json" + try? FileManager.default.removeItem(atPath: recorderPath) + configureSocketLaunchEnvironment(app) + app.launchEnvironment["CMUX_UI_TEST_KEYEQUIV_PATH"] = recorderPath + app.launchEnvironment["CMUX_UI_TEST_FORCE_CONFIRM_CLOSE_WORKSPACE"] = "1" + app.launch() + XCTAssertTrue( + ensureForegroundAfterLaunch(app, timeout: 12.0), + "Expected app to launch for focused-window workspace close test. state=\(app.state.rawValue)" + ) + XCTAssertTrue( + waitForSocketPong(timeout: 12.0), + "Expected control socket to respond at \(socketPath). diagnostics=\(loadJSON(atPath: diagnosticsPath) ?? [:])" + ) + + let focusedWindowId = requireUUID(from: socketCommand("current_window"), context: "initial current_window") + let focusedWorkspaceId = requireUUID( + from: socketCommand("new_workspace focused-window-target"), + context: "new workspace in focused window" + ) + XCTAssertEqual(socketCommand("current_workspace"), focusedWorkspaceId) + + _ = requireUUID(from: socketCommand("new_window"), context: "new_window") + let otherWorkspaceId = requireUUID( + from: socketCommand("new_workspace other-window-target"), + context: "new workspace in other window" + ) + XCTAssertEqual(socketCommand("current_workspace"), otherWorkspaceId) + + XCTAssertEqual(socketCommand("focus_window \(focusedWindowId)"), "OK") + XCTAssertTrue( + waitForKeyWindow(focusedWindowId, timeout: 5.0), + "Expected focus_window to make the first cmux window key before Cmd+Shift+W. windows=\(socketCommand("list_windows") ?? "")" + ) + XCTAssertEqual(socketCommand("current_workspace"), focusedWorkspaceId) + + app.typeKey("w", modifierFlags: [.command, .shift]) + + let target = waitForJSONKey( + "closeConfirmationTargetWorkspaceId", + equals: focusedWorkspaceId, + atPath: recorderPath, + timeout: 5.0 + ) + XCTAssertEqual( + target?["closeConfirmationTargetWindowId"], + focusedWindowId, + "Cmd+Shift+W should target the selected workspace in the focused/key window, not the other cmux window. recorder=\(target ?? loadJSON(atPath: recorderPath) ?? [:])" + ) + + let presentation = waitForJSONKey( + "closeConfirmationHostWindowId", + equals: focusedWindowId, + atPath: recorderPath, + timeout: 5.0 + ) + XCTAssertEqual( + presentation?["closeConfirmationPresentation"], + "sheet", + "Close workspace confirmation should attach to the same focused window that owns the target workspace. recorder=\(presentation ?? loadJSON(atPath: recorderPath) ?? [:])" + ) + + clickCancelOnCloseWorkspaceAlert(app: app) + } + private func configureSocketLaunchEnvironment(_ app: XCUIApplication) { app.launchArguments += ["-socketControlMode", "allowAll"] app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] @@ -222,7 +289,22 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase { } ) if let resolvedPath { socketPath = resolvedPath } - return ready + if ready { + return true + } + if let diagnostics = loadJSON(atPath: diagnosticsPath), + controlSocketDiagnosticsReportReady(diagnostics) { + if let expectedPath = diagnostics["socketExpectedPath"], + !expectedPath.isEmpty, + FileManager.default.fileExists(atPath: expectedPath) { + socketPath = expectedPath + return true + } else if let readyCandidate = socketCandidates().first(where: { FileManager.default.fileExists(atPath: $0) }) { + socketPath = readyCandidate + return true + } + } + return false } private func socketCandidates() -> [String] { @@ -264,6 +346,46 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase { .count } + private func requireUUID( + from response: String?, + context: String, + file: StaticString = #filePath, + line: UInt = #line + ) -> String { + guard let response, + let uuid = response + .split(whereSeparator: { $0 == " " || $0 == "\n" || $0 == "\t" }) + .map(String.init) + .first(where: { UUID(uuidString: $0) != nil }) else { + XCTFail("Expected UUID in \(context) response. response=\(response ?? "")", file: file, line: line) + return "" + } + return uuid + } + + private func waitForKeyWindow(_ windowId: String, timeout: TimeInterval) -> Bool { + let expectation = XCTNSPredicateExpectation( + predicate: NSPredicate { _, _ in + self.keyWindowId() == windowId + }, + object: NSObject() + ) + return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed + } + + private func keyWindowId() -> String? { + guard let response = socketCommand("list_windows") else { return nil } + for line in response.split(separator: "\n") { + let parts = line + .trimmingCharacters(in: .whitespacesAndNewlines) + .split(separator: " ") + .map(String.init) + guard parts.first == "*", parts.count >= 3 else { continue } + return parts[2] + } + return nil + } + private func waitForJSONKey(_ key: String, equals expected: String, atPath path: String, timeout: TimeInterval) -> [String: String]? { var latest: [String: String]? let expectation = XCTNSPredicateExpectation( @@ -356,6 +478,23 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase { } } + private func clickCancelOnCloseWorkspaceAlert(app: XCUIApplication) { + let dialog = closeWorkspaceDialog(app: app) + if dialog.exists { + dialog.buttons["Cancel"].firstMatch.click() + return + } + let alert = closeWorkspaceAlert(app: app) + if alert.exists { + alert.buttons["Cancel"].firstMatch.click() + return + } + let anyDialog = app.dialogs.firstMatch + if anyDialog.exists, anyDialog.buttons["Cancel"].exists { + anyDialog.buttons["Cancel"].firstMatch.click() + } + } + private func closeWorkspacesDialog(app: XCUIApplication) -> XCUIElement { app.dialogs.containing(.staticText, identifier: "Close workspaces?").firstMatch } @@ -364,6 +503,14 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase { app.alerts.containing(.staticText, identifier: "Close workspaces?").firstMatch } + private func closeWorkspaceDialog(app: XCUIApplication) -> XCUIElement { + app.dialogs.containing(.staticText, identifier: "Close workspace?").firstMatch + } + + private func closeWorkspaceAlert(app: XCUIApplication) -> XCUIElement { + app.alerts.containing(.staticText, identifier: "Close workspace?").firstMatch + } + private final class ControlSocketClient { private let path: String private let responseTimeout: TimeInterval From 83a0167365ca5c42c164d9c397c4599dfd453c4f Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Wed, 17 Jun 2026 16:54:16 -0700 Subject: [PATCH 2/3] Target workspace close shortcut to event window --- ...legate+FocusedWorkspaceCloseShortcut.swift | 45 +++++ Sources/AppDelegate.swift | 8 +- ...abManager+CloseConfirmationTelemetry.swift | 41 +++++ Sources/TabManager.swift | 18 +- cmux.xcodeproj/project.pbxproj | 12 ++ ...rmDialogUITests+FocusedWindowHelpers.swift | 155 ++++++++++++++++++ .../CloseWorkspacesConfirmDialogUITests.swift | 153 +---------------- 7 files changed, 263 insertions(+), 169 deletions(-) create mode 100644 Sources/AppDelegate+FocusedWorkspaceCloseShortcut.swift create mode 100644 Sources/TabManager+CloseConfirmationTelemetry.swift create mode 100644 cmuxUITests/CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift diff --git a/Sources/AppDelegate+FocusedWorkspaceCloseShortcut.swift b/Sources/AppDelegate+FocusedWorkspaceCloseShortcut.swift new file mode 100644 index 000000000000..17d7b12fb2aa --- /dev/null +++ b/Sources/AppDelegate+FocusedWorkspaceCloseShortcut.swift @@ -0,0 +1,45 @@ +import AppKit + +extension AppDelegate { + func mainWindowContextForFocusedWorkspaceCloseShortcut(event: NSEvent) -> MainWindowContext? { + if let keyWindow = NSApp.keyWindow, + let context = contextForMainTerminalWindow(keyWindow) { + return context + } + + if let mainWindow = NSApp.mainWindow, + let context = contextForMainTerminalWindow(mainWindow) { + return context + } + + if let context = mainWindowContext(forShortcutEvent: event, debugSource: "shortcut.closeWorkspace") { + return context + } + + if shortcutEventHasAddressableWindow(event) { + if let eventWindow = resolvedShortcutEventWindow(event), + cmuxWindowShouldOwnCloseShortcut(eventWindow) { + return preferredMainWindowContextForShortcutRouting(event: event) + } + return nil + } + + return nil + } + + @discardableResult + func closeWorkspaceFromFocusedShortcut(event: NSEvent) -> Bool { + guard let context = mainWindowContextForFocusedWorkspaceCloseShortcut(event: event) else { + return false + } + + if let window = context.window ?? windowForMainWindowId(context.windowId) { + setActiveMainWindow(window) + } else { + activateMainWindowContext(context) + } + + context.tabManager.closeCurrentWorkspaceWithConfirmation() + return true + } +} diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index a64fd2485313..4093d5379565 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -6241,7 +6241,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return nil } - private func resolvedShortcutEventWindow(_ event: NSEvent) -> NSWindow? { + func resolvedShortcutEventWindow(_ event: NSEvent) -> NSWindow? { if let window = event.window { return window } @@ -8017,7 +8017,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return fallback } - private func shortcutEventHasAddressableWindow(_ event: NSEvent?) -> Bool { + func shortcutEventHasAddressableWindow(_ event: NSEvent?) -> Bool { guard let event else { return false } // NSEvent.windowNumber can be 0 for responder-chain events that are not // actually bound to an NSWindow (notably some WebKit key paths). @@ -13307,7 +13307,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } if matchConfiguredShortcut(event: event, action: .closeWorkspace) { - tabManagerForFocusedCloseShortcut(event: event)?.closeCurrentWorkspaceWithConfirmation() + _ = closeWorkspaceFromFocusedShortcut(event: event) return true } @@ -15675,7 +15675,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return nil } - private func activateMainWindowContext(_ context: MainWindowContext?) { + func activateMainWindowContext(_ context: MainWindowContext?) { guard let context else { tabManager = nil sidebarState = nil diff --git a/Sources/TabManager+CloseConfirmationTelemetry.swift b/Sources/TabManager+CloseConfirmationTelemetry.swift new file mode 100644 index 000000000000..4e675529f88d --- /dev/null +++ b/Sources/TabManager+CloseConfirmationTelemetry.swift @@ -0,0 +1,41 @@ +import AppKit +import Foundation + +extension TabManager { + func recordCloseConfirmationTarget(workspaceIds: [UUID]) { +#if DEBUG + let joinedWorkspaceIds = workspaceIds.map(\.uuidString).joined(separator: ",") + UITestRecorder.record([ + "closeConfirmationTargetWindowId": AppDelegate.shared?.windowId(for: self)?.uuidString ?? "", + "closeConfirmationTargetWorkspaceId": workspaceIds.first?.uuidString ?? "", + "closeConfirmationTargetWorkspaceIds": joinedWorkspaceIds, + ]) +#endif + } + + func recordCloseConfirmationSheetPresentation(hostWindow: NSWindow) { +#if DEBUG + // The sheet attaches after this hook returns, so read the attachment on the + // next runloop turn while the modal loop is running. + DispatchQueue.main.async { + var updates = [ + "closeConfirmationPresentation": "sheet", + "closeConfirmationAttachedSheet": hostWindow.attachedSheet == nil ? "0" : "1", + ] + if let hostWindowId = AppDelegate.shared?.mainWindowId(from: hostWindow)?.uuidString { + updates["closeConfirmationHostWindowId"] = hostWindowId + } + UITestRecorder.record(updates) + } +#endif + } + + func recordCloseConfirmationAppModalPresentation(hostWindowHadAttachedSheet: Bool) { +#if DEBUG + UITestRecorder.record([ + "closeConfirmationPresentation": "appModal", + "closeConfirmationAttachedSheet": hostWindowHadAttachedSheet ? "1" : "0", + ]) +#endif + } +} diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index f21ee16d6e4f..6586fd48f0e5 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -2205,6 +2205,7 @@ class TabManager: ObservableObject { @discardableResult func closeWorkspaceWithConfirmation(_ workspace: Workspace) -> Bool { + recordCloseConfirmationTarget(workspaceIds: [workspace.id]) if workspace.isPinned { guard confirmPinnedWorkspaceClose(source: .workspace) else { return false } closeWorkspaceIfRunningProcess(workspace, requiresConfirmation: false) @@ -2268,6 +2269,7 @@ class TabManager: ObservableObject { closeWorkspaceFromCloseTabGesture(workspaces[0]) return } + recordCloseConfirmationTarget(workspaceIds: workspaces.map(\.id)) let plan = closeWorkspacesPlan(for: workspaces) if shouldConfirmClose(requiresConfirmation: true, source: .tabClose) { @@ -2398,24 +2400,12 @@ class TabManager: ObservableObject { alert, presentingWindow: closeConfirmationPresentingWindow() ) { presentation in - #if DEBUG switch presentation { case .sheet(let hostWindow): - // The sheet attaches after this hook returns, so read the - // attachment on the next runloop turn (during the modal loop). - DispatchQueue.main.async { - UITestRecorder.record([ - "closeConfirmationPresentation": "sheet", - "closeConfirmationAttachedSheet": hostWindow.attachedSheet == nil ? "0" : "1", - ]) - } + self.recordCloseConfirmationSheetPresentation(hostWindow: hostWindow) case .appModal(let hostWindowHadAttachedSheet): - UITestRecorder.record([ - "closeConfirmationPresentation": "appModal", - "closeConfirmationAttachedSheet": hostWindowHadAttachedSheet ? "1" : "0", - ]) + self.recordCloseConfirmationAppModalPresentation(hostWindowHadAttachedSheet: hostWindowHadAttachedSheet) } - #endif } } diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 71a31a1a05b4..a80afef7b63f 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ C4160A030000000000000001 /* AppDelegate+ClosedItemHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */; }; C3677004000000000000001 /* AppDelegate+CmuxSSHURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3677004000000000000002 /* AppDelegate+CmuxSSHURL.swift */; }; E3309A01 /* AppDelegate+EqualizeSplitsShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3309A02 /* AppDelegate+EqualizeSplitsShortcut.swift */; }; + F8C0A0010000000000000001 /* AppDelegate+FocusedWorkspaceCloseShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C0A0010000000000000002 /* AppDelegate+FocusedWorkspaceCloseShortcut.swift */; }; C4160A040000000000000001 /* AppDelegate+FocusHistoryContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4160A040000000000000002 /* AppDelegate+FocusHistoryContextMenu.swift */; }; 3865A0063865A0063865A006 /* AppDelegate+GlobalSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3865B0063865B0063865B006 /* AppDelegate+GlobalSearch.swift */; }; D7AB00000000000000000001 /* AppDelegate+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000002 /* AppDelegate+MoveTabToNewWorkspace.swift */; }; @@ -162,6 +163,7 @@ B9000025A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000026A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift */; }; B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */; }; B900001AA1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */; }; + F8C0A0030000000000000001 /* CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C0A0030000000000000002 /* CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift */; }; B900001BA1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900001CA1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift */; }; C10D00010000000000000001 /* CloudVMActionLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10D00020000000000000002 /* CloudVMActionLauncher.swift */; }; B900000BA1B2C3D4E5F60719 /* cmux in Copy CLI */ = {isa = PBXBuildFile; fileRef = B9000004A1B2C3D4E5F60719 /* cmux */; }; @@ -776,6 +778,7 @@ D7AB00000000000000B041 /* SupersededPhoneDismissBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000B040 /* SupersededPhoneDismissBuffer.swift */; }; A5001303 /* SurfaceSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001301 /* SurfaceSearchOverlay.swift */; }; C9A5720BC9A5720BC9A5720B /* TabItemView+WorkspaceGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A5720CC9A5720CC9A5720C /* TabItemView+WorkspaceGroups.swift */; }; + F8C0A0020000000000000001 /* TabManager+CloseConfirmationTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C0A0020000000000000002 /* TabManager+CloseConfirmationTelemetry.swift */; }; D7AB00000000000000000013 /* TabManager+DetachedWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */; }; E3309A03 /* TabManager+EqualizeSplits.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3309A04 /* TabManager+EqualizeSplits.swift */; }; E3B7A400000000000000000B /* TabManager+FocusHistoryHosting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B7A400000000000000000C /* TabManager+FocusHistoryHosting.swift */; }; @@ -1081,6 +1084,7 @@ C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+ClosedItemHistory.swift"; sourceTree = ""; }; C3677004000000000000002 /* AppDelegate+CmuxSSHURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CmuxSSHURL.swift"; sourceTree = ""; }; E3309A02 /* AppDelegate+EqualizeSplitsShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+EqualizeSplitsShortcut.swift"; sourceTree = ""; }; + F8C0A0010000000000000002 /* AppDelegate+FocusedWorkspaceCloseShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+FocusedWorkspaceCloseShortcut.swift"; sourceTree = ""; }; C4160A040000000000000002 /* AppDelegate+FocusHistoryContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+FocusHistoryContextMenu.swift"; sourceTree = ""; }; 3865B0063865B0063865B006 /* AppDelegate+GlobalSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Search/AppDelegate+GlobalSearch.swift"; sourceTree = ""; }; D7AB00000000000000000002 /* AppDelegate+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MoveTabToNewWorkspace.swift"; sourceTree = ""; }; @@ -1188,6 +1192,7 @@ B9000026A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWindowConfirmDialogUITests.swift; sourceTree = ""; }; B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceCmdDUITests.swift; sourceTree = ""; }; B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceConfirmDialogUITests.swift; sourceTree = ""; }; + F8C0A0030000000000000002 /* CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift"; sourceTree = ""; }; B900001CA1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspacesConfirmDialogUITests.swift; sourceTree = ""; }; C10D00020000000000000002 /* CloudVMActionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudVMActionLauncher.swift; sourceTree = ""; }; B9000004A1B2C3D4E5F60719 /* cmux */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = cmux; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1715,6 +1720,7 @@ D7AB00000000000000B040 /* SupersededPhoneDismissBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupersededPhoneDismissBuffer.swift; sourceTree = ""; }; A5001301 /* SurfaceSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Find/SurfaceSearchOverlay.swift; sourceTree = ""; }; C9A5720CC9A5720CC9A5720C /* TabItemView+WorkspaceGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabItemView+WorkspaceGroups.swift"; sourceTree = ""; }; + F8C0A0020000000000000002 /* TabManager+CloseConfirmationTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+CloseConfirmationTelemetry.swift"; sourceTree = ""; }; D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+DetachedWorkspace.swift"; sourceTree = ""; }; E3309A04 /* TabManager+EqualizeSplits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+EqualizeSplits.swift"; sourceTree = ""; }; E3B7A400000000000000000C /* TabManager+FocusHistoryHosting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+FocusHistoryHosting.swift"; sourceTree = ""; }; @@ -2070,6 +2076,7 @@ B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */, B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */, B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */, + F8C0A0030000000000000002 /* CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift */, B900001CA1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift */, B9000026A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift */, B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */, @@ -2318,6 +2325,7 @@ C10D51700000000000000001 /* ClosedItemHistory.swift */, D1FFC0DE000000000000D001 /* DiffCommentSubmissionPool.swift */, D1FFC0DE000000000000A001 /* DiffCommentStore.swift */, + F8C0A0020000000000000002 /* TabManager+CloseConfirmationTelemetry.swift */, E3309A04 /* TabManager+EqualizeSplits.swift */, C5CC15CEAA80B779ADEE78AA /* WorkspaceActionDispatcher.swift */, D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */, @@ -2449,6 +2457,7 @@ C3677002000000000000002 /* CmuxSSHURLRequest.swift */, C3677004000000000000002 /* AppDelegate+CmuxSSHURL.swift */, E3309A02 /* AppDelegate+EqualizeSplitsShortcut.swift */, + F8C0A0010000000000000002 /* AppDelegate+FocusedWorkspaceCloseShortcut.swift */, D7AB00000000000000000002 /* AppDelegate+MoveTabToNewWorkspace.swift */, 2907A0022907A0022907A002 /* AppDelegate+RecoverableMainWindowRoutes.swift */, 4EDE10010000000000000002 /* AppDelegate+NotificationDeliverySeams.swift */, @@ -3499,6 +3508,7 @@ C4160A030000000000000001 /* AppDelegate+ClosedItemHistory.swift in Sources */, C3677004000000000000001 /* AppDelegate+CmuxSSHURL.swift in Sources */, E3309A01 /* AppDelegate+EqualizeSplitsShortcut.swift in Sources */, + F8C0A0010000000000000001 /* AppDelegate+FocusedWorkspaceCloseShortcut.swift in Sources */, C4160A040000000000000001 /* AppDelegate+FocusHistoryContextMenu.swift in Sources */, 3865A0063865A0063865A006 /* AppDelegate+GlobalSearch.swift in Sources */, D7AB00000000000000000001 /* AppDelegate+MoveTabToNewWorkspace.swift in Sources */, @@ -3866,6 +3876,7 @@ D7AB00000000000000B041 /* SupersededPhoneDismissBuffer.swift in Sources */, A5001303 /* SurfaceSearchOverlay.swift in Sources */, C9A5720BC9A5720BC9A5720B /* TabItemView+WorkspaceGroups.swift in Sources */, + F8C0A0020000000000000001 /* TabManager+CloseConfirmationTelemetry.swift in Sources */, D7AB00000000000000000013 /* TabManager+DetachedWorkspace.swift in Sources */, E3309A03 /* TabManager+EqualizeSplits.swift in Sources */, E3B7A400000000000000000B /* TabManager+FocusHistoryHosting.swift in Sources */, @@ -4071,6 +4082,7 @@ B9000025A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift in Sources */, B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */, B900001AA1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift in Sources */, + F8C0A0030000000000000001 /* CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift in Sources */, B900001BA1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift in Sources */, C0DE32470000000000000005 /* CommandPaletteIdentifierClipboardUITests.swift in Sources */, D10000000000000000B00000 /* ControlSocketReadinessUITestSupport.swift in Sources */, diff --git a/cmuxUITests/CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift b/cmuxUITests/CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift new file mode 100644 index 000000000000..f8d356efb2b1 --- /dev/null +++ b/cmuxUITests/CloseWorkspacesConfirmDialogUITests+FocusedWindowHelpers.swift @@ -0,0 +1,155 @@ +import Darwin +import Foundation +import XCTest + +extension CloseWorkspacesConfirmDialogUITests { + func requireUUID( + from response: String?, + context: String, + file: StaticString = #filePath, + line: UInt = #line + ) -> String { + guard let response, + let uuid = response + .split(whereSeparator: { $0 == " " || $0 == "\n" || $0 == "\t" }) + .map(String.init) + .first(where: { UUID(uuidString: $0) != nil }) else { + XCTFail("Expected UUID in \(context) response. response=\(response ?? "")", file: file, line: line) + return "" + } + return uuid + } + + func waitForKeyWindow(_ windowId: String, timeout: TimeInterval) -> Bool { + let expectation = XCTNSPredicateExpectation( + predicate: NSPredicate { _, _ in + self.keyWindowId() == windowId + }, + object: NSObject() + ) + return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed + } + + private func keyWindowId() -> String? { + guard let response = socketCommand("list_windows") else { return nil } + for line in response.split(separator: "\n") { + let parts = line + .trimmingCharacters(in: .whitespacesAndNewlines) + .split(separator: " ") + .map(String.init) + guard parts.first == "*", parts.count >= 3 else { continue } + return parts[2] + } + return nil + } +} + +final class CloseWorkspacesControlSocketClient { + private let path: String + private let responseTimeout: TimeInterval + + init(path: String, responseTimeout: TimeInterval = 2.0) { + self.path = path + self.responseTimeout = responseTimeout + } + + func sendLine(_ line: String) -> String? { + let fd = socket(AF_UNIX, SOCK_STREAM, 0) + guard fd >= 0 else { return nil } + defer { close(fd) } + + var socketTimeout = timeval( + tv_sec: Int(responseTimeout.rounded(.down)), + tv_usec: Int32(((responseTimeout - floor(responseTimeout)) * 1_000_000).rounded()) + ) + + var noSigPipe: Int32 = 1 + _ = withUnsafePointer(to: &noSigPipe) { ptr in + setsockopt( + fd, + SOL_SOCKET, + SO_NOSIGPIPE, + ptr, + socklen_t(MemoryLayout.size) + ) + } + _ = withUnsafePointer(to: &socketTimeout) { ptr in + setsockopt( + fd, + SOL_SOCKET, + SO_RCVTIMEO, + ptr, + socklen_t(MemoryLayout.size) + ) + } + _ = withUnsafePointer(to: &socketTimeout) { ptr in + setsockopt( + fd, + SOL_SOCKET, + SO_SNDTIMEO, + ptr, + socklen_t(MemoryLayout.size) + ) + } + + var addr = sockaddr_un() + memset(&addr, 0, MemoryLayout.size) + addr.sun_family = sa_family_t(AF_UNIX) + + let maxLen = MemoryLayout.size(ofValue: addr.sun_path) + let bytes = Array(path.utf8CString) + guard bytes.count <= maxLen else { return nil } + withUnsafeMutablePointer(to: &addr.sun_path) { p in + let raw = UnsafeMutableRawPointer(p).assumingMemoryBound(to: CChar.self) + memset(raw, 0, maxLen) + for i in 0...offset(of: \.sun_path) ?? 0 + let addrLen = socklen_t(pathOffset + bytes.count) + addr.sun_len = UInt8(min(Int(addrLen), 255)) + + let connected = withUnsafePointer(to: &addr) { ptr in + ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in + connect(fd, sa, addrLen) + } + } + guard connected == 0 else { return nil } + + let payload = line + "\n" + let wrote: Bool = payload.withCString { cstr in + var remaining = strlen(cstr) + var p = UnsafeRawPointer(cstr) + while remaining > 0 { + let n = write(fd, p, remaining) + if n <= 0 { return false } + remaining -= n + p = p.advanced(by: n) + } + return true + } + guard wrote else { return nil } + _ = shutdown(fd, SHUT_WR) + + var buf = [UInt8](repeating: 0, count: 4096) + var accum = "" + while true { + let n = read(fd, &buf, buf.count) + if n < 0 { + let code = errno + if code == EAGAIN || code == EWOULDBLOCK { + break + } + return nil + } + if n <= 0 { break } + if let chunk = String(bytes: buf[0.. String { - guard let response, - let uuid = response - .split(whereSeparator: { $0 == " " || $0 == "\n" || $0 == "\t" }) - .map(String.init) - .first(where: { UUID(uuidString: $0) != nil }) else { - XCTFail("Expected UUID in \(context) response. response=\(response ?? "")", file: file, line: line) - return "" - } - return uuid - } - - private func waitForKeyWindow(_ windowId: String, timeout: TimeInterval) -> Bool { - let expectation = XCTNSPredicateExpectation( - predicate: NSPredicate { _, _ in - self.keyWindowId() == windowId - }, - object: NSObject() - ) - return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed - } - - private func keyWindowId() -> String? { - guard let response = socketCommand("list_windows") else { return nil } - for line in response.split(separator: "\n") { - let parts = line - .trimmingCharacters(in: .whitespacesAndNewlines) - .split(separator: " ") - .map(String.init) - guard parts.first == "*", parts.count >= 3 else { continue } - return parts[2] - } - return nil - } - private func waitForJSONKey(_ key: String, equals expected: String, atPath path: String, timeout: TimeInterval) -> [String: String]? { var latest: [String: String]? let expectation = XCTNSPredicateExpectation( @@ -406,8 +366,8 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase { return json } - private func socketCommand(_ cmd: String, responseTimeout: TimeInterval = 2.0) -> String? { - if let response = ControlSocketClient(path: socketPath, responseTimeout: responseTimeout).sendLine(cmd) { + func socketCommand(_ cmd: String, responseTimeout: TimeInterval = 2.0) -> String? { + if let response = CloseWorkspacesControlSocketClient(path: socketPath, responseTimeout: responseTimeout).sendLine(cmd) { return response } return socketCommandViaNetcat(cmd, responseTimeout: responseTimeout) @@ -511,113 +471,4 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase { app.alerts.containing(.staticText, identifier: "Close workspace?").firstMatch } - private final class ControlSocketClient { - private let path: String - private let responseTimeout: TimeInterval - - init(path: String, responseTimeout: TimeInterval = 2.0) { - self.path = path - self.responseTimeout = responseTimeout - } - - func sendLine(_ line: String) -> String? { - let fd = socket(AF_UNIX, SOCK_STREAM, 0) - guard fd >= 0 else { return nil } - defer { close(fd) } - - var socketTimeout = timeval( - tv_sec: Int(responseTimeout.rounded(.down)), - tv_usec: Int32(((responseTimeout - floor(responseTimeout)) * 1_000_000).rounded()) - ) - - var noSigPipe: Int32 = 1 - _ = withUnsafePointer(to: &noSigPipe) { ptr in - setsockopt( - fd, - SOL_SOCKET, - SO_NOSIGPIPE, - ptr, - socklen_t(MemoryLayout.size) - ) - } - _ = withUnsafePointer(to: &socketTimeout) { ptr in - setsockopt( - fd, - SOL_SOCKET, - SO_RCVTIMEO, - ptr, - socklen_t(MemoryLayout.size) - ) - } - _ = withUnsafePointer(to: &socketTimeout) { ptr in - setsockopt( - fd, - SOL_SOCKET, - SO_SNDTIMEO, - ptr, - socklen_t(MemoryLayout.size) - ) - } - - var addr = sockaddr_un() - memset(&addr, 0, MemoryLayout.size) - addr.sun_family = sa_family_t(AF_UNIX) - - let maxLen = MemoryLayout.size(ofValue: addr.sun_path) - let bytes = Array(path.utf8CString) - guard bytes.count <= maxLen else { return nil } - withUnsafeMutablePointer(to: &addr.sun_path) { p in - let raw = UnsafeMutableRawPointer(p).assumingMemoryBound(to: CChar.self) - memset(raw, 0, maxLen) - for i in 0...offset(of: \.sun_path) ?? 0 - let addrLen = socklen_t(pathOffset + bytes.count) - addr.sun_len = UInt8(min(Int(addrLen), 255)) - - let connected = withUnsafePointer(to: &addr) { ptr in - ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in - connect(fd, sa, addrLen) - } - } - guard connected == 0 else { return nil } - - let payload = line + "\n" - let wrote: Bool = payload.withCString { cstr in - var remaining = strlen(cstr) - var p = UnsafeRawPointer(cstr) - while remaining > 0 { - let n = write(fd, p, remaining) - if n <= 0 { return false } - remaining -= n - p = p.advanced(by: n) - } - return true - } - guard wrote else { return nil } - _ = shutdown(fd, SHUT_WR) - - var buf = [UInt8](repeating: 0, count: 4096) - var accum = "" - while true { - let n = read(fd, &buf, buf.count) - if n < 0 { - let code = errno - if code == EAGAIN || code == EWOULDBLOCK { - break - } - return nil - } - if n <= 0 { break } - if let chunk = String(bytes: buf[0.. Date: Wed, 17 Jun 2026 17:47:11 -0700 Subject: [PATCH 3/3] Satisfy close confirmation telemetry policy --- ...TabManager+CloseConfirmationTelemetry.swift | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Sources/TabManager+CloseConfirmationTelemetry.swift b/Sources/TabManager+CloseConfirmationTelemetry.swift index 4e675529f88d..6d13367f6230 100644 --- a/Sources/TabManager+CloseConfirmationTelemetry.swift +++ b/Sources/TabManager+CloseConfirmationTelemetry.swift @@ -15,18 +15,14 @@ extension TabManager { func recordCloseConfirmationSheetPresentation(hostWindow: NSWindow) { #if DEBUG - // The sheet attaches after this hook returns, so read the attachment on the - // next runloop turn while the modal loop is running. - DispatchQueue.main.async { - var updates = [ - "closeConfirmationPresentation": "sheet", - "closeConfirmationAttachedSheet": hostWindow.attachedSheet == nil ? "0" : "1", - ] - if let hostWindowId = AppDelegate.shared?.mainWindowId(from: hostWindow)?.uuidString { - updates["closeConfirmationHostWindowId"] = hostWindowId - } - UITestRecorder.record(updates) + var updates = [ + "closeConfirmationPresentation": "sheet", + "closeConfirmationAttachedSheet": "1", + ] + if let hostWindowId = AppDelegate.shared?.mainWindowId(from: hostWindow)?.uuidString { + updates["closeConfirmationHostWindowId"] = hostWindowId } + UITestRecorder.record(updates) #endif }