diff --git a/Sources/AppDelegate+CloudTeamPicker.swift b/Sources/AppDelegate+CloudTeamPicker.swift new file mode 100644 index 000000000000..c5d1b1aaad92 --- /dev/null +++ b/Sources/AppDelegate+CloudTeamPicker.swift @@ -0,0 +1,55 @@ +import AppKit + +extension AppDelegate { + @MainActor + func handleCloudTeamPickerShortcut(_ event: NSEvent) -> Bool { + guard matchConfiguredShortcut(event: event, action: .openTeamPicker) else { return false } + _ = openCloudTeamPicker( + preferredWindow: event.window ?? shortcutRoutingActiveWindow, + debugSource: "shortcut.openTeamPicker" + ) + return true + } + + /// Reveals the active window's Cloud panel and requests its team menu. + @MainActor + @discardableResult + func openCloudTeamPicker( + preferredWindow: NSWindow? = nil, + debugSource: String + ) -> Bool { + guard CloudMachinesFeature.isEnabled, + RightSidebarMode.machines.isAvailable() else { + let alert = NSAlert() + alert.messageText = String(localized: "command.auth.teamPicker.title", defaultValue: "Open Team Picker") + alert.informativeText = CloudMachinesFeature.disabledMessage + alert.alertStyle = .informational + alert.addButton(withTitle: String(localized: "common.ok", defaultValue: "OK")) + if let window = preferredWindow ?? NSApp.keyWindow ?? NSApp.mainWindow { + alert.beginSheetModal(for: window) + } else { + alert.runModal() + } + return false + } + + guard let context = preferredRegisteredMainWindowContext(preferredWindow: preferredWindow), + let state = context.fileExplorerState else { + NSSound.beep() + return false + } + + state.mode = .machines + state.setVisible(true) + _ = focusRightSidebarInActiveMainWindow( + mode: .machines, + focusFirstItem: false, + preferredWindow: context.window ?? preferredWindow + ) + state.cloudTeamPickerPresentation.isPresented = auth?.accountFlow.isAuthenticated == true +#if DEBUG + cmuxDebugLog("cloud.teamPicker.open source=\(debugSource)") +#endif + return true + } +} diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index f7d28c7f8d96..66d71385b8fc 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -14971,10 +14971,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent openPreferencesWindow(debugSource: "shortcut.openSettings") return true } - if matchConfiguredShortcut(event: event, action: .openTeamPicker) { - NotificationCenter.default.post(name: .cmuxTeamPickerShortcutRequested, object: self) - return true - } + if handleCloudTeamPickerShortcut(event) { return true } if matchConfiguredShortcut(event: event, action: .reloadConfiguration) { reloadConfiguration(source: "shortcut.reloadConfiguration") return true diff --git a/Sources/SidebarAccountTeamPicker.swift b/Sources/Cloud/CloudTeamPicker.swift similarity index 67% rename from Sources/SidebarAccountTeamPicker.swift rename to Sources/Cloud/CloudTeamPicker.swift index 4b92d2af0cb5..f8801735a8c8 100644 --- a/Sources/SidebarAccountTeamPicker.swift +++ b/Sources/Cloud/CloudTeamPicker.swift @@ -1,8 +1,8 @@ import CmuxSettingsUI import SwiftUI -/// The nested team menu. Selection and creation keep both menu levels open. -struct SidebarAccountTeamPicker: View { +/// The Cloud team menu. Selection and creation use the shared account flow. +struct CloudTeamPicker: View { let accountFlow: HostAccountFlow @State private var isCreatingTeam = false @State private var newTeamName = "" @@ -12,29 +12,37 @@ struct SidebarAccountTeamPicker: View { var body: some View { teamPickerContent - .buttonStyle(SidebarAccountMenuButtonStyle()) - .disabled(isSubmitting || accountFlow.isWorkingOnAuth) - .padding(12) - .frame(width: 220, alignment: .leading) + .buttonStyle(SidebarAccountMenuButtonStyle()) + .disabled(isSubmitting || accountFlow.isWorkingOnAuth) + .padding(12) + .frame(width: 220, alignment: .leading) } @ViewBuilder private var teamPickerContent: some View { + let teams = accountFlow.availableTeams + let selectedTeamID = accountFlow.selectedTeamID + let pendingTeamID = accountFlow.pendingTeamSelection?.teamID VStack(alignment: .leading, spacing: 0) { - if accountFlow.availableTeams.isEmpty { + if teams.isEmpty { Text(String(localized: "sidebar.account.loadingTeams", defaultValue: "Loading teams…")) .cmuxFont(size: 12) .foregroundStyle(.secondary) .frame(minHeight: SidebarAccountMenuButtonStyle.rowHeight, alignment: .leading) } else { - ForEach(accountFlow.availableTeams) { team in - teamRow(team) + ForEach(teams) { team in + CloudTeamPickerTeamRow( + team: team, + isSelected: team.id == selectedTeamID, + isPending: team.id == pendingTeamID, + onSelect: { selectTeam(team) } + ) } } if isCreatingTeam { createTeamEditor } else { - accountMenuRow( + teamMenuRow( title: String(localized: "sidebar.account.createTeam", defaultValue: "Create team…"), systemImage: "plus" ) { @@ -42,7 +50,7 @@ struct SidebarAccountTeamPicker: View { newTeamName = "" isCreatingTeam = true } - .accessibilityIdentifier("SidebarAccountCreateTeamButton") + .accessibilityIdentifier("CloudTeamPickerCreateTeamButton") } if let errorMessage { Text(errorMessage) @@ -55,43 +63,19 @@ struct SidebarAccountTeamPicker: View { } } - private func teamRow(_ team: AccountTeamSummary) -> some View { - let activeTeamID = accountFlow.selectedTeamID - let isSelected = team.id == activeTeamID - let isPending = team.id == accountFlow.pendingTeamSelection?.teamID - return Button { - guard !isSelected, !accountFlow.isSelectingTeam else { return } - errorMessage = nil - Task { @MainActor in - do { - try await accountFlow.selectTeam(id: team.id) - } catch { - errorMessage = String( - localized: "sidebar.account.switchTeamFailed", - defaultValue: "Could not switch teams. Try again." - ) - } - } - } label: { - HStack(spacing: 8) { - Label(team.displayName, systemImage: "person.2") - .lineLimit(1) - Spacer(minLength: 8) - if isPending { - ProgressView().controlSize(.mini) - } else if isSelected { - Image(systemName: "checkmark") - .foregroundStyle(.secondary) - } + private func selectTeam(_ team: AccountTeamSummary) { + guard team.id != accountFlow.selectedTeamID, !accountFlow.isSelectingTeam else { return } + errorMessage = nil + Task { @MainActor in + do { + try await accountFlow.selectTeam(id: team.id) + } catch { + errorMessage = String( + localized: "sidebar.account.switchTeamFailed", + defaultValue: "Could not switch teams. Try again." + ) } - .frame(maxWidth: .infinity, alignment: .leading) } - .accessibilityLabel(String( - format: String(localized: "sidebar.account.teamRowLabel", defaultValue: "%1$@%2$@"), - team.displayName, - isSelected ? String(localized: "sidebar.account.activeSuffix", defaultValue: ", active") : "" - )) - .accessibilityIdentifier("SidebarAccountTeam_\(team.id)") } private var createTeamEditor: some View { @@ -139,7 +123,7 @@ struct SidebarAccountTeamPicker: View { .padding(.vertical, 2) .frame(minHeight: SidebarAccountMenuButtonStyle.rowHeight, alignment: .leading) .onAppear { isCreateFieldFocused = true } - .accessibilityIdentifier("SidebarAccountCreateTeamEditor") + .accessibilityIdentifier("CloudTeamPickerCreateTeamEditor") } private func submitCreateTeam() { @@ -163,7 +147,7 @@ struct SidebarAccountTeamPicker: View { } } - private func accountMenuRow( + private func teamMenuRow( title: String, systemImage: String, action: @escaping () -> Void diff --git a/Sources/Cloud/CloudTeamPickerHeader.swift b/Sources/Cloud/CloudTeamPickerHeader.swift new file mode 100644 index 000000000000..932d47a1ceac --- /dev/null +++ b/Sources/Cloud/CloudTeamPickerHeader.swift @@ -0,0 +1,53 @@ +import AppKit +import SwiftUI + +/// Team scope and machine actions share the Cloud header. Fleet status keeps its +/// own row so it cannot squeeze the active team's name out of a narrow sidebar. +struct CloudTeamPickerHeader: View { + let accountFlow: HostAccountFlow? + let presentation: CloudTeamPickerPresentation? + let chromeBackgroundColor: NSColor + let isRefreshing: Bool + let onRefresh: () -> Void + let onNewMachine: () -> Void + @ViewBuilder let agentMenu: () -> AgentMenu + @ViewBuilder let status: () -> Status + @State private var panePresentation = CloudTeamPickerPresentation() + + var body: some View { + @Bindable var picker = presentation ?? panePresentation + VStack(spacing: 0) { + HStack(spacing: 6) { + if let accountFlow { + CloudTeamPickerRow(accountFlow: accountFlow, isPresented: $picker.isPresented) + .disabled(accountFlow.isWorkingOnAuth) + } + Spacer(minLength: 0) + agentMenu() + MachinesChromeIconButton( + symbolName: "arrow.clockwise", + accessibilityLabel: String(localized: "machines.refresh", defaultValue: "Refresh Machines"), + isBusy: isRefreshing, + action: onRefresh + ) + MachinesChromeIconButton( + symbolName: "plus", + accessibilityLabel: String(localized: "machines.new", defaultValue: "New Machine"), + isBusy: false, + action: onNewMachine + ) + } + .rightSidebarChromeBar() + .rightSidebarChromeBottomBorder(backgroundColor: chromeBackgroundColor) + .accessibilityElement(children: .contain) + .accessibilityIdentifier("CloudMachinesSectionHeader") + HStack(spacing: 6) { + status() + Spacer(minLength: 0) + } + .padding(.horizontal, 10) + .padding(.vertical, 4) + } + .onDisappear { picker.isPresented = false } + } +} diff --git a/Sources/Cloud/CloudTeamPickerPresentation.swift b/Sources/Cloud/CloudTeamPickerPresentation.swift new file mode 100644 index 000000000000..3fb4437d27da --- /dev/null +++ b/Sources/Cloud/CloudTeamPickerPresentation.swift @@ -0,0 +1,8 @@ +import Observation + +/// Transient presentation owned by one Cloud surface, separate from team selection. +@MainActor +@Observable +final class CloudTeamPickerPresentation { + var isPresented = false +} diff --git a/Sources/Cloud/CloudTeamPickerRow.swift b/Sources/Cloud/CloudTeamPickerRow.swift new file mode 100644 index 000000000000..23752312c490 --- /dev/null +++ b/Sources/Cloud/CloudTeamPickerRow.swift @@ -0,0 +1,54 @@ +import CmuxSettingsUI +import SwiftUI + +/// Shows the active team in the Cloud header and opens its team menu. +struct CloudTeamPickerRow: View { + let accountFlow: HostAccountFlow + @Binding var isPresented: Bool + + private var currentTeam: AccountTeamSummary? { + accountFlow.availableTeams.first { $0.id == accountFlow.selectedTeamID } + } + + var body: some View { + Button { + isPresented = true + } label: { + HStack(spacing: 5) { + Image(systemName: "person.2") + .font(.system(size: 10, weight: .semibold)) + Text(currentTeam?.displayName ?? String( + localized: "sidebar.account.noTeam", + defaultValue: "No team" + )) + .cmuxFont(size: 11, weight: .medium) + .lineLimit(1) + Image(systemName: "chevron.down") + .font(.system(size: 9, weight: .semibold)) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 7) + .frame(height: 22) + .contentShape(Capsule(style: .continuous)) + } + .buttonStyle(.plain) + .popover(isPresented: $isPresented, arrowEdge: .top) { + CloudTeamPicker(accountFlow: accountFlow) + } + .safeHelp(String(localized: "settings.account.activeTeam", defaultValue: "Active Team")) + .accessibilityLabel(teamPickerAccessibilityLabel) + .accessibilityIdentifier("CloudTeamPickerButton") + } + + private var teamPickerAccessibilityLabel: String { + let name = currentTeam?.displayName ?? String( + localized: "sidebar.account.noTeam", + defaultValue: "No team" + ) + return String( + format: String(localized: "sidebar.account.teamRowLabel", defaultValue: "%1$@%2$@"), + name, + String(localized: "sidebar.account.activeSuffix", defaultValue: ", active") + ) + } +} diff --git a/Sources/Cloud/CloudTeamPickerTeamRow.swift b/Sources/Cloud/CloudTeamPickerTeamRow.swift new file mode 100644 index 000000000000..106c89d4ce7e --- /dev/null +++ b/Sources/Cloud/CloudTeamPickerTeamRow.swift @@ -0,0 +1,33 @@ +import CmuxSettingsUI +import SwiftUI + +/// A value snapshot keeps the team's observable owner above the list boundary. +struct CloudTeamPickerTeamRow: View { + let team: AccountTeamSummary + let isSelected: Bool + let isPending: Bool + let onSelect: () -> Void + + var body: some View { + Button(action: onSelect) { + HStack(spacing: 8) { + Label(team.displayName, systemImage: "person.2") + .lineLimit(1) + Spacer(minLength: 8) + if isPending { + ProgressView().controlSize(.mini) + } else if isSelected { + Image(systemName: "checkmark") + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .accessibilityLabel(String( + format: String(localized: "sidebar.account.teamRowLabel", defaultValue: "%1$@%2$@"), + team.displayName, + isSelected ? String(localized: "sidebar.account.activeSuffix", defaultValue: ", active") : "" + )) + .accessibilityIdentifier("CloudTeamPickerTeam_\(team.id)") + } +} diff --git a/Sources/Cloud/MachinesPanelView.swift b/Sources/Cloud/MachinesPanelView.swift index 51d140e66175..ba493a74ce4d 100644 --- a/Sources/Cloud/MachinesPanelView.swift +++ b/Sources/Cloud/MachinesPanelView.swift @@ -28,15 +28,18 @@ struct MachinesPanelView: View { @AppStorage(CloudTreeStyleStore.defaultsKey) private var cloudTreeStyleID: String = CloudTreeStyle.defaultStyle.id let chromeBackgroundColor: NSColor var tabManager: TabManager? = nil + let teamPickerPresentation: CloudTeamPickerPresentation? init( chromeBackgroundColor: NSColor, machinePinStore: CloudMachinePinStore? = nil, devicesModel: DevicesPanelViewModel? = nil, - tabManager: TabManager? = nil + tabManager: TabManager? = nil, + teamPickerPresentation: CloudTeamPickerPresentation? = nil ) { self.chromeBackgroundColor = chromeBackgroundColor self.tabManager = tabManager + self.teamPickerPresentation = teamPickerPresentation _viewModel = StateObject(wrappedValue: MachinesPanelViewModel( machinePinStore: machinePinStore, localWorkspacesProvider: { [weak tabManager] in @@ -201,29 +204,16 @@ struct MachinesPanelView: View { } private var controlBar: some View { - HStack(spacing: 6) { - cloudStatus - .padding(.leading, 4) - Spacer(minLength: 4) - cloudAgentMenu - MachinesChromeIconButton( - symbolName: "arrow.clockwise", - accessibilityLabel: String(localized: "machines.refresh", defaultValue: "Refresh Machines"), - isBusy: viewModel.isLoading || devicesModel.isRefreshing - ) { - refreshMachines() - } - MachinesChromeIconButton( - symbolName: "plus", - accessibilityLabel: String(localized: "machines.new", defaultValue: "New Machine"), - isBusy: false - ) { - requestNewMachine() - } - } - .rightSidebarChromeBar() - .rightSidebarChromeBottomBorder(backgroundColor: chromeBackgroundColor) - .accessibilityIdentifier("CloudMachinesSectionHeader") + CloudTeamPickerHeader( + accountFlow: accountFlow, + presentation: teamPickerPresentation, + chromeBackgroundColor: chromeBackgroundColor, + isRefreshing: viewModel.isLoading || devicesModel.isRefreshing, + onRefresh: refreshMachines, + onNewMachine: requestNewMachine, + agentMenu: { cloudAgentMenu }, + status: { cloudStatus } + ) } @ViewBuilder diff --git a/Sources/ContentView+AuthCommandPalette.swift b/Sources/ContentView+AuthCommandPalette.swift index 803b3f1f72eb..cedc0631c393 100644 --- a/Sources/ContentView+AuthCommandPalette.swift +++ b/Sources/ContentView+AuthCommandPalette.swift @@ -36,7 +36,7 @@ extension ContentView { CommandPaletteCommandContribution( commandId: commandPaletteAuthTeamPickerCommandId, title: constant(String(localized: "command.auth.teamPicker.title", defaultValue: "Open Team Picker")), - subtitle: constant(String(localized: "command.auth.subtitle", defaultValue: "Account")), + subtitle: constant(String(localized: "command.cloudVM.subtitle", defaultValue: "Cloud")), keywords: ["account", "auth", "team", "teams", "switch", "create"], when: { context in context.bool(CommandPaletteContextKeys.authSignedIn) @@ -70,7 +70,10 @@ extension ContentView { } } registry.register(commandId: Self.commandPaletteAuthTeamPickerCommandId) { - NotificationCenter.default.post(name: .cmuxTeamPickerShortcutRequested, object: self) + _ = AppDelegate.shared?.openCloudTeamPicker( + preferredWindow: tabManager.window, + debugSource: "palette.auth.teamPicker" + ) } } } diff --git a/Sources/ContentView+RightSidebarCommandPalette.swift b/Sources/ContentView+RightSidebarCommandPalette.swift index 4cb269e39396..3a7121fc5d20 100644 --- a/Sources/ContentView+RightSidebarCommandPalette.swift +++ b/Sources/ContentView+RightSidebarCommandPalette.swift @@ -13,6 +13,8 @@ extension ContentView { } switch commandId { + case Self.commandPaletteAuthTeamPickerCommandId: + return .openTeamPicker case "palette.newWorkspace": return .newTab case "palette.newBrowserWorkspace": diff --git a/Sources/FileExplorerState.swift b/Sources/FileExplorerState.swift index 9b18275f39a0..4fb200eaf82d 100644 --- a/Sources/FileExplorerState.swift +++ b/Sources/FileExplorerState.swift @@ -36,6 +36,9 @@ final class FileExplorerState: ObservableObject { /// persisted). @Published var rightSidebarOwnsInputFocus: Bool = false + /// The right-sidebar Cloud picker belongs to this window, even before it mounts. + @MainActor lazy var cloudTeamPickerPresentation = CloudTeamPickerPresentation() + /// Active mode for the right sidebar (file tree, search, sessions, or enabled beta modes). var mode: RightSidebarMode { get { storedMode } diff --git a/Sources/RightSidebarPanelView.swift b/Sources/RightSidebarPanelView.swift index 1d4c23780f8b..4c339febe4c8 100644 --- a/Sources/RightSidebarPanelView.swift +++ b/Sources/RightSidebarPanelView.swift @@ -208,6 +208,7 @@ struct RightSidebarPanelView: View { } .onChange(of: fileExplorerState.isVisible) { _, visible in if visible { hasMountedRightSidebarContent = true } + else { fileExplorerState.cloudTeamPickerPresentation.isPresented = false } } .onChange(of: feedEnabled) { _, _ in refreshModeAvailabilityAndFocusIfNeeded() } .onChange(of: dockEnabled) { _, _ in refreshModeAvailabilityAndFocusIfNeeded() } @@ -451,7 +452,7 @@ struct RightSidebarPanelView: View { } ) .onAppear { - sessionIndexStore.setCurrentDirectoryIfChanged(sessionIndexDirectory) + sessionIndexStore.setCurrentDirectoryIfChanged(sessionIndexStore.currentDirectory) } case .feed: FeedPanelView( @@ -464,7 +465,8 @@ struct RightSidebarPanelView: View { chromeBackgroundColor: windowAppearance.resolvedChromeBackgroundColor, machinePinStore: AppDelegate.shared?.cloudMachinePinStore, devicesModel: devicesModel, - tabManager: tabManager + tabManager: tabManager, + teamPickerPresentation: fileExplorerState.cloudTeamPickerPresentation ) case .customSidebar: customSidebarPanel @@ -524,10 +526,6 @@ struct RightSidebarPanelView: View { Task { await client.shutdown() } } - private var sessionIndexDirectory: String? { - sessionIndexStore.currentDirectory - } - /// Renders this window's own Dock (created lazily on first show); no /// window ever defers to a Dock rendered elsewhere. @ViewBuilder @@ -551,7 +549,7 @@ struct RightSidebarPanelView: View { private func selectMode(_ mode: RightSidebarMode) { fileExplorerState.mode = mode if fileExplorerState.mode == .sessions { - sessionIndexStore.setCurrentDirectoryIfChanged(sessionIndexDirectory) + sessionIndexStore.setCurrentDirectoryIfChanged(sessionIndexStore.currentDirectory) if sessionIndexStore.entries.isEmpty { sessionIndexStore.reload() } diff --git a/Sources/SidebarAccountMenuButtonStyle.swift b/Sources/SidebarAccountMenuButtonStyle.swift index 9385950ce5d0..56efe1dad328 100644 --- a/Sources/SidebarAccountMenuButtonStyle.swift +++ b/Sources/SidebarAccountMenuButtonStyle.swift @@ -1,6 +1,6 @@ import SwiftUI -/// Gives both levels of the account menu the same compact row rhythm. +/// Gives account actions and the Cloud team menu the same compact row rhythm. struct SidebarAccountMenuButtonStyle: ButtonStyle { static let rowHeight: CGFloat = 26 diff --git a/Sources/SidebarAccountTeamPickerRow.swift b/Sources/SidebarAccountTeamPickerRow.swift deleted file mode 100644 index e12330d5293d..000000000000 --- a/Sources/SidebarAccountTeamPickerRow.swift +++ /dev/null @@ -1,69 +0,0 @@ -import CmuxAppKitSupportUI -import CmuxSettingsUI -import SwiftUI - -/// Opens the team submenu on hover, click, or keyboard activation. -struct SidebarAccountTeamPickerRow: View { - let accountFlow: HostAccountFlow - @Binding var isPresented: Bool - let popoverGroup: CmuxPopoverGroup - - private var currentTeam: AccountTeamSummary? { - accountFlow.availableTeams.first { $0.id == accountFlow.selectedTeamID } - } - - var body: some View { - Button { - isPresented = true - } label: { - HStack(spacing: 8) { - Label(currentTeam?.displayName ?? String( - localized: "sidebar.account.noTeam", - defaultValue: "No team" - ), systemImage: "person.2") - .lineLimit(1) - Spacer(minLength: 8) - Image(systemName: "chevron.right") - .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(.secondary) - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .buttonStyle(SidebarAccountMenuButtonStyle()) - .contentShape(Rectangle()) - .onHover { hovering in - if hovering { isPresented = true } - } - .overlay(alignment: .trailing) { - ArrowlessPopoverAnchor( - isPresented: $isPresented, - preferredEdge: .maxX, - detachedGap: 4, - presentationAnimation: .enabled, - group: popoverGroup - ) { - SidebarAccountTeamPicker(accountFlow: accountFlow) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .allowsHitTesting(false) - } - .accessibilityLabel(teamPickerAccessibilityLabel) - .accessibilityHint(String( - localized: "settings.account.activeTeam", - defaultValue: "Active Team" - )) - .accessibilityIdentifier("SidebarAccountTeamPickerButton") - } - - private var teamPickerAccessibilityLabel: String { - let name = currentTeam?.displayName ?? String( - localized: "sidebar.account.noTeam", - defaultValue: "No team" - ) - return String( - format: String(localized: "sidebar.account.teamRowLabel", defaultValue: "%1$@%2$@"), - name, - String(localized: "sidebar.account.activeSuffix", defaultValue: ", active") - ) - } -} diff --git a/Sources/SidebarAccountTeamPopover.swift b/Sources/SidebarAccountTeamPopover.swift index aceb28921a7f..289a8a73277b 100644 --- a/Sources/SidebarAccountTeamPopover.swift +++ b/Sources/SidebarAccountTeamPopover.swift @@ -3,12 +3,7 @@ import CmuxAppKitSupportUI import CmuxSettingsUI import SwiftUI -extension Notification.Name { - static let cmuxTeamPickerShortcutRequested = Notification.Name("cmux.teamPicker.shortcutRequested") -} - -/// The existing compact sidebar account button with team management added to -/// its account popover. +/// The compact sidebar account button for account-level actions. struct SidebarAccountMenuButton: View { @EnvironmentObject private var tabManager: TabManager private var accountFlow: HostAccountFlow? { AppDelegate.shared?.auth?.accountFlow } @@ -16,7 +11,6 @@ struct SidebarAccountMenuButton: View { private let signInTitle = String(localized: "settings.account.signIn", defaultValue: "Sign In…") private let buttonSize = SidebarFooterButtonMetrics.buttonSize @State private var isPopoverPresented = false - @State private var isShowingTeamPicker = false @State private var popoverGroup = CmuxPopoverGroup() #if DEBUG @AppStorage(SidebarFooterProfileIconDebugSettings.sizeKey) @@ -100,33 +94,18 @@ struct SidebarAccountMenuButton: View { ) { SidebarAccountPopover( accountFlow: accountFlow, - dismiss: { popoverGroup.dismissAll() }, - isShowingTeamPicker: $isShowingTeamPicker, - popoverGroup: popoverGroup + dismiss: { popoverGroup.dismissAll() } ) }) .safeHelp(buttonTitle) .accessibilityLabel(buttonTitle) .accessibilityIdentifier("SidebarAccountMenuButton") - .task { - for await _ in NotificationCenter.default.notifications(named: .cmuxTeamPickerShortcutRequested) { - guard !Task.isCancelled else { return } - isPopoverPresented = true - } - } - .onChange(of: isPopoverPresented) { _, presented in - if !presented { - isShowingTeamPicker = false - } - } } } private struct SidebarAccountPopover: View { let accountFlow: HostAccountFlow? let dismiss: () -> Void - @Binding var isShowingTeamPicker: Bool - let popoverGroup: CmuxPopoverGroup @State private var shortcutObserver = KeyboardShortcutSettingsObserver.shared private var settingsShortcutHint: String { @@ -157,15 +136,6 @@ private struct SidebarAccountPopover: View { } } } - Divider() - .padding(.vertical, 4) - if let accountFlow { - SidebarAccountTeamPickerRow( - accountFlow: accountFlow, - isPresented: $isShowingTeamPicker, - popoverGroup: popoverGroup - ) - } settingsRow } else { Text(String(localized: "settings.account.signedOut.title", defaultValue: "Not signed in")) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 0e832f4f7c36..dd7ac9232976 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -223,6 +223,7 @@ C4A570010000000000000001 /* AppDelegate+CanvasShortcutRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A570010000000000000002 /* AppDelegate+CanvasShortcutRouting.swift */; }; C4160A030000000000000001 /* AppDelegate+ClosedItemHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */; }; AC329BBA70590E415B129F2F /* AppDelegate+CloudDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D0A6FA47518B9D3AF658084 /* AppDelegate+CloudDiagnostics.swift */; }; + 9FC7BD6263A65F7A691B01A2 /* AppDelegate+CloudTeamPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27422F10498F0E21AFC4E789 /* AppDelegate+CloudTeamPicker.swift */; }; C72B971AA11641AD9D61CDA3 /* AppDelegate+CloudTerminalNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33981D3B415C488D8406A09E /* AppDelegate+CloudTerminalNavigation.swift */; }; 7A0CE1000000000000000045 /* AppDelegate+CloudTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0CE1000000000000000019 /* AppDelegate+CloudTunnel.swift */; }; C54860050000000000000001 /* AppDelegate+CmuxNavigationDeepLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54860050000000000000002 /* AppDelegate+CmuxNavigationDeepLinks.swift */; }; @@ -888,6 +889,12 @@ 09FBC463786149D49E73B297 /* CloudSurfaceDropGateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23E881DEAE40649BC8FFCF /* CloudSurfaceDropGateView.swift */; }; 5CB4AC1D92064236B5C2DDF1 /* CloudSurfaceMoveOwnershipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF212BA811D54BB986A5E27F /* CloudSurfaceMoveOwnershipTests.swift */; }; 6D2669CD5E4944C2B8C0F49C /* CloudSurfaceOwnershipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DAE66C6AF74046B5545B47 /* CloudSurfaceOwnershipTests.swift */; }; + 13019A000000000000000004 /* CloudTeamPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13019A000000000000000003 /* CloudTeamPicker.swift */; }; + A14327000000000000000004 /* CloudTeamPickerHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14327000000000000000003 /* CloudTeamPickerHeader.swift */; }; + A14327000000000000000006 /* CloudTeamPickerPlacementUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14327000000000000000005 /* CloudTeamPickerPlacementUITests.swift */; }; + A14327000000000000000002 /* CloudTeamPickerPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14327000000000000000001 /* CloudTeamPickerPresentation.swift */; }; + 13019A000000000000000006 /* CloudTeamPickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13019A000000000000000005 /* CloudTeamPickerRow.swift */; }; + 2F646430024CB401EEC6AC0D /* CloudTeamPickerTeamRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA268D4CE6EFBDFFB69F9B7E /* CloudTeamPickerTeamRow.swift */; }; 13019C000000000000000002 /* CloudTeamScopeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13019C000000000000000001 /* CloudTeamScopeObserver.swift */; }; CCBC23BCB453E9A38F16DB2D /* CloudTelemetryClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF35B9235AD817EE95281459 /* CloudTelemetryClient.swift */; }; A38052AB06B7E0B46A215689 /* CloudTelemetrySending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EBCFB7E8CD8163F74525C53 /* CloudTelemetrySending.swift */; }; @@ -1606,6 +1613,7 @@ 7520C0DF0000000000000009 /* CodexTurnLifecycleCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7520C0DF000000000000000A /* CodexTurnLifecycleCoordinator.swift */; }; D3AD00000000000000000005 /* CodexTurnRestoreIntentPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3AD00000000000000000004 /* CodexTurnRestoreIntentPolicy.swift */; }; C4041001000000000000001B /* CommandClickFileOpenRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4041001000000000000001A /* CommandClickFileOpenRouter.swift */; }; + 1322F13D9B823BB4630B8989 /* CommandPaletteAuthCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25DB476025F922037411EA98 /* CommandPaletteAuthCommandTests.swift */; }; C0DE86060000000000000001 /* CommandPaletteFocusRestoreCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE86060000000000000002 /* CommandPaletteFocusRestoreCoordinator.swift */; }; C0DE32470000000000000005 /* CommandPaletteIdentifierClipboardUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE32470000000000000006 /* CommandPaletteIdentifierClipboardUITests.swift */; }; C0DEC0DE000000000000F202 /* CommandPaletteNucleoFFILibrarySupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEC0DE000000000000F201 /* CommandPaletteNucleoFFILibrarySupport.swift */; }; @@ -3003,8 +3011,6 @@ 5C3E0454B6C24B02A2F091A8 /* ShortcutRoutingSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42A82C6AA614E74873D9A5F /* ShortcutRoutingSupport.swift */; }; F6001000A1B2C3D4E5F60718 /* ShortcutUnbindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6001001A1B2C3D4E5F60718 /* ShortcutUnbindingTests.swift */; }; 173E29C3FB4A4A0F867894B0 /* SidebarAccountMenuButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1E153B939944FC893F1EC2 /* SidebarAccountMenuButtonStyle.swift */; }; - 13019A000000000000000004 /* SidebarAccountTeamPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13019A000000000000000003 /* SidebarAccountTeamPicker.swift */; }; - 13019A000000000000000006 /* SidebarAccountTeamPickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13019A000000000000000005 /* SidebarAccountTeamPickerRow.swift */; }; 13019A000000000000000002 /* SidebarAccountTeamPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13019A000000000000000001 /* SidebarAccountTeamPopover.swift */; }; A6AC71020000000000000001 /* SidebarAgentActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC71030000000000000001 /* SidebarAgentActivityIndicator.swift */; }; A6AC71040000000000000001 /* SidebarAgentActivitySummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC71050000000000000001 /* SidebarAgentActivitySummary.swift */; }; @@ -4497,6 +4503,7 @@ C4A570010000000000000002 /* AppDelegate+CanvasShortcutRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CanvasShortcutRouting.swift"; sourceTree = ""; }; C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+ClosedItemHistory.swift"; sourceTree = ""; }; 0D0A6FA47518B9D3AF658084 /* AppDelegate+CloudDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CloudDiagnostics.swift"; sourceTree = ""; }; + 27422F10498F0E21AFC4E789 /* AppDelegate+CloudTeamPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CloudTeamPicker.swift"; sourceTree = ""; }; 33981D3B415C488D8406A09E /* AppDelegate+CloudTerminalNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CloudTerminalNavigation.swift"; sourceTree = ""; }; 7A0CE1000000000000000019 /* AppDelegate+CloudTunnel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CloudTunnel.swift"; sourceTree = ""; }; C54860050000000000000002 /* AppDelegate+CmuxNavigationDeepLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CmuxNavigationDeepLinks.swift"; sourceTree = ""; }; @@ -5142,6 +5149,12 @@ 3A23E881DEAE40649BC8FFCF /* CloudSurfaceDropGateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudSurfaceDropGateView.swift; sourceTree = ""; }; DF212BA811D54BB986A5E27F /* CloudSurfaceMoveOwnershipTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudSurfaceMoveOwnershipTests.swift; sourceTree = ""; }; D4DAE66C6AF74046B5545B47 /* CloudSurfaceOwnershipTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudSurfaceOwnershipTests.swift; sourceTree = ""; }; + 13019A000000000000000003 /* CloudTeamPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTeamPicker.swift; sourceTree = ""; }; + A14327000000000000000003 /* CloudTeamPickerHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTeamPickerHeader.swift; sourceTree = ""; }; + A14327000000000000000005 /* CloudTeamPickerPlacementUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTeamPickerPlacementUITests.swift; sourceTree = ""; }; + A14327000000000000000001 /* CloudTeamPickerPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTeamPickerPresentation.swift; sourceTree = ""; }; + 13019A000000000000000005 /* CloudTeamPickerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTeamPickerRow.swift; sourceTree = ""; }; + CA268D4CE6EFBDFFB69F9B7E /* CloudTeamPickerTeamRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudTeamPickerTeamRow.swift"; sourceTree = ""; }; 13019C000000000000000001 /* CloudTeamScopeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudTeamScopeObserver.swift"; sourceTree = ""; }; FF35B9235AD817EE95281459 /* CloudTelemetryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudTelemetryClient.swift"; sourceTree = ""; }; 3EBCFB7E8CD8163F74525C53 /* CloudTelemetrySending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudTelemetrySending.swift"; sourceTree = ""; }; @@ -5737,6 +5750,7 @@ 7520C0DF000000000000000A /* CodexTurnLifecycleCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTurnLifecycleCoordinator.swift; sourceTree = ""; }; D3AD00000000000000000004 /* CodexTurnRestoreIntentPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTurnRestoreIntentPolicy.swift; sourceTree = ""; }; C4041001000000000000001A /* CommandClickFileOpenRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandClickFileOpenRouter.swift; sourceTree = ""; }; + 25DB476025F922037411EA98 /* CommandPaletteAuthCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandPaletteAuthCommandTests.swift"; sourceTree = ""; }; C0DE86060000000000000002 /* CommandPaletteFocusRestoreCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteFocusRestoreCoordinator.swift; sourceTree = ""; }; C0DE32470000000000000006 /* CommandPaletteIdentifierClipboardUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIdentifierClipboardUITests.swift; sourceTree = ""; }; C0DEC0DE000000000000F201 /* CommandPaletteNucleoFFILibrarySupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteNucleoFFILibrarySupport.swift; sourceTree = ""; }; @@ -7118,8 +7132,6 @@ B42A82C6AA614E74873D9A5F /* ShortcutRoutingSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/ShortcutRoutingSupport.swift; sourceTree = ""; }; F6001001A1B2C3D4E5F60718 /* ShortcutUnbindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutUnbindingTests.swift; sourceTree = ""; }; EC1E153B939944FC893F1EC2 /* SidebarAccountMenuButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SidebarAccountMenuButtonStyle.swift"; sourceTree = ""; }; - 13019A000000000000000003 /* SidebarAccountTeamPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SidebarAccountTeamPicker.swift"; sourceTree = ""; }; - 13019A000000000000000005 /* SidebarAccountTeamPickerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SidebarAccountTeamPickerRow.swift"; sourceTree = ""; }; 13019A000000000000000001 /* SidebarAccountTeamPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SidebarAccountTeamPopover.swift"; sourceTree = ""; }; A6AC71030000000000000001 /* SidebarAgentActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarAgentActivityIndicator.swift; sourceTree = ""; }; A6AC71050000000000000001 /* SidebarAgentActivitySummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarAgentActivitySummary.swift; sourceTree = ""; }; @@ -8657,6 +8669,7 @@ 2D7997450F5175FDC84FC976 /* RemoteTmuxSizingUITests+Oracle.swift */, 2D7997460F5175FDC84FC977 /* RemoteTmuxSizingUITests+Content.swift */, E93214F8A3E0C68058E9EEF6 /* RemoteTmuxSizingUITests+Socket.swift */, + A14327000000000000000005 /* CloudTeamPickerPlacementUITests.swift */, AA1B2C3D4E5F60721 /* RightSidebarChromeHeightUITests.swift */, 340AACC8DD8772A000EA1971 /* VaultRefreshUITests.swift */, ); @@ -9156,6 +9169,11 @@ 1652962DDCD1CB279278F8D4 /* CloudMachineCache.swift */, 352B2228561CD8C4B042302A /* CloudActivationPolicy.swift */, 7A0CE1000000000000000725 /* CloudTreeNodeActions+Prompts.swift */, + A14327000000000000000003 /* CloudTeamPickerHeader.swift */, + CA268D4CE6EFBDFFB69F9B7E /* CloudTeamPickerTeamRow.swift */, + 13019A000000000000000003 /* CloudTeamPicker.swift */, + 13019A000000000000000005 /* CloudTeamPickerRow.swift */, + A14327000000000000000001 /* CloudTeamPickerPresentation.swift */, 13019C000000000000000001 /* CloudTeamScopeObserver.swift */, ); name = Cloud; @@ -9268,9 +9286,8 @@ C0DE86060000000000000002 /* CommandPaletteFocusRestoreCoordinator.swift */, C0DE86060000000000000004 /* CommandPaletteRestoreFocusTarget.swift */, F7216001000000000000002 /* VerticalTabsSidebar+EmptyAreasAndFooter.swift */, + 27422F10498F0E21AFC4E789 /* AppDelegate+CloudTeamPicker.swift */, 13019A000000000000000001 /* SidebarAccountTeamPopover.swift */, - 13019A000000000000000003 /* SidebarAccountTeamPicker.swift */, - 13019A000000000000000005 /* SidebarAccountTeamPickerRow.swift */, EC1E153B939944FC893F1EC2 /* SidebarAccountMenuButtonStyle.swift */, @@ -12516,6 +12533,7 @@ 511EAB11A81C099E4F5A8ECF /* DeferredAgentResumeAdmissionOwnerTests.swift */, C7E3FFB9B84F6DE45250D5D5 /* CodexSessionStartDeadTurnTests.swift */, 0B0073F9140EA86D5ADEE233 /* CodexStaleTurnRestoreIntentTests.swift */, + 25DB476025F922037411EA98 /* CommandPaletteAuthCommandTests.swift */, F0677DB157F30F725FECA0EC /* CMUXCLICodexUnavailableAdmissionTests.swift */, ); path = cmuxTests; @@ -13490,6 +13508,7 @@ C4A570010000000000000001 /* AppDelegate+CanvasShortcutRouting.swift in Sources */, C4160A030000000000000001 /* AppDelegate+ClosedItemHistory.swift in Sources */, AC329BBA70590E415B129F2F /* AppDelegate+CloudDiagnostics.swift in Sources */, + 9FC7BD6263A65F7A691B01A2 /* AppDelegate+CloudTeamPicker.swift in Sources */, C72B971AA11641AD9D61CDA3 /* AppDelegate+CloudTerminalNavigation.swift in Sources */, 7A0CE1000000000000000045 /* AppDelegate+CloudTunnel.swift in Sources */, C54860050000000000000001 /* AppDelegate+CmuxNavigationDeepLinks.swift in Sources */, @@ -13846,6 +13865,11 @@ 375E0D1FDA464DC4A05359E1 /* CloudSidebarRowSnapshot.swift in Sources */, D318853B007D48AD9532F83F /* CloudSurfaceDropGate.swift in Sources */, 09FBC463786149D49E73B297 /* CloudSurfaceDropGateView.swift in Sources */, + 13019A000000000000000004 /* CloudTeamPicker.swift in Sources */, + A14327000000000000000004 /* CloudTeamPickerHeader.swift in Sources */, + A14327000000000000000002 /* CloudTeamPickerPresentation.swift in Sources */, + 13019A000000000000000006 /* CloudTeamPickerRow.swift in Sources */, + 2F646430024CB401EEC6AC0D /* CloudTeamPickerTeamRow.swift in Sources */, 13019C000000000000000002 /* CloudTeamScopeObserver.swift in Sources */, CCBC23BCB453E9A38F16DB2D /* CloudTelemetryClient.swift in Sources */, A38052AB06B7E0B46A215689 /* CloudTelemetrySending.swift in Sources */, @@ -15065,8 +15089,6 @@ D35A00000000000000000010 /* ShortcutRecorderRejectedAttempt.swift in Sources */, 5C3E0454B6C24B02A2F091A8 /* ShortcutRoutingSupport.swift in Sources */, 173E29C3FB4A4A0F867894B0 /* SidebarAccountMenuButtonStyle.swift in Sources */, - 13019A000000000000000004 /* SidebarAccountTeamPicker.swift in Sources */, - 13019A000000000000000006 /* SidebarAccountTeamPickerRow.swift in Sources */, 13019A000000000000000002 /* SidebarAccountTeamPopover.swift in Sources */, A6AC71020000000000000001 /* SidebarAgentActivityIndicator.swift in Sources */, A6AC71040000000000000001 /* SidebarAgentActivitySummary.swift in Sources */, @@ -16201,6 +16223,7 @@ B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */, B900001AA1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift in Sources */, B900001BA1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift in Sources */, + A14327000000000000000006 /* CloudTeamPickerPlacementUITests.swift in Sources */, C0DE32470000000000000005 /* CommandPaletteIdentifierClipboardUITests.swift in Sources */, D10000000000000000B00000 /* ControlSocketReadinessUITestSupport.swift in Sources */, B8F266266A1A3D9A45BD840F /* DisplayResolutionRegressionUITests.swift in Sources */, @@ -16683,6 +16706,7 @@ C0DEBACC0000000000000001 /* CodexTeamsAppServerFixture.swift in Sources */, C0DEBACC0000000000000002 /* CodexTeamsSocketFixture.swift in Sources */, 7520C0DF0000000000000001 /* CodexTurnCompletionOwnershipTests.swift in Sources */, + 1322F13D9B823BB4630B8989 /* CommandPaletteAuthCommandTests.swift in Sources */, C0DEC0DE000000000000F202 /* CommandPaletteNucleoFFILibrarySupport.swift in Sources */, C0DEC0DE000000000000F102 /* CommandPaletteNucleoFFITests.swift in Sources */, C0DEC0DE000000000000F204 /* CommandPaletteNucleoFixtures.swift in Sources */, diff --git a/cmuxTests/CommandPaletteAuthCommandTests.swift b/cmuxTests/CommandPaletteAuthCommandTests.swift new file mode 100644 index 000000000000..4944a11c1a5c --- /dev/null +++ b/cmuxTests/CommandPaletteAuthCommandTests.swift @@ -0,0 +1,54 @@ +import CmuxCommandPalette +import Foundation +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +/// Auth command visibility and the Cloud team's existing customizable command. +@MainActor +struct CommandPaletteAuthCommandTests { + @Test func signedOutContextShowsSignInCommandOnly() { + var context = CommandPaletteContextSnapshot() + context.setBool(CommandPaletteContextKeys.authSignedIn, false) + context.setBool(CommandPaletteContextKeys.authWorking, false) + #expect(visibleAuthCommandIDs(context) == [ContentView.commandPaletteAuthSignInCommandId]) + } + + @Test func signedInContextShowsSignOutAndTeamPicker() { + var context = CommandPaletteContextSnapshot() + context.setBool(CommandPaletteContextKeys.authSignedIn, true) + context.setBool(CommandPaletteContextKeys.authWorking, false) + #expect(visibleAuthCommandIDs(context) == [ + ContentView.commandPaletteAuthSignOutCommandId, + ContentView.commandPaletteAuthTeamPickerCommandId + ]) + } + + @Test(arguments: [false, true]) + func workingAuthContextHidesAccountCommands(signedIn: Bool) { + var context = CommandPaletteContextSnapshot() + context.setBool(CommandPaletteContextKeys.authSignedIn, signedIn) + context.setBool(CommandPaletteContextKeys.authWorking, true) + #expect(visibleAuthCommandIDs(context).isEmpty) + } + + @Test func teamPickerIsACloudCommandUsingTheCustomizableShortcut() throws { + let command = try #require(ContentView.commandPaletteAuthCommandContributions().first { + $0.commandId == ContentView.commandPaletteAuthTeamPickerCommandId + }) + #expect(command.subtitle(CommandPaletteContextSnapshot()) == String( + localized: "command.cloudVM.subtitle", defaultValue: "Cloud" + )) + #expect(ContentView.commandPaletteShortcutAction(forCommandID: command.commandId) == .openTeamPicker) + } + + private func visibleAuthCommandIDs(_ context: CommandPaletteContextSnapshot) -> [String] { + ContentView.commandPaletteAuthCommandContributions() + .filter { $0.when(context) } + .map(\.commandId) + } +} diff --git a/cmuxTests/ShortcutAndCommandPaletteTests.swift b/cmuxTests/ShortcutAndCommandPaletteTests.swift index 5bb5cf8083dc..6c5cb7727a98 100644 --- a/cmuxTests/ShortcutAndCommandPaletteTests.swift +++ b/cmuxTests/ShortcutAndCommandPaletteTests.swift @@ -842,47 +842,6 @@ final class CommandPaletteRenameSelectionSettingsTests: XCTestCase { } } -final class CommandPaletteAuthCommandTests: XCTestCase { - func testSignedOutContextShowsSignInCommandOnly() { - var context = CommandPaletteContextSnapshot() - context.setBool(CommandPaletteContextKeys.authSignedIn, false) - context.setBool(CommandPaletteContextKeys.authWorking, false) - - let visibleCommandIds = visibleAuthCommandIds(context) - - XCTAssertEqual(visibleCommandIds, [ContentView.commandPaletteAuthSignInCommandId]) - } - - func testSignedInContextShowsSignOutCommandOnly() { - var context = CommandPaletteContextSnapshot() - context.setBool(CommandPaletteContextKeys.authSignedIn, true) - context.setBool(CommandPaletteContextKeys.authWorking, false) - - let visibleCommandIds = visibleAuthCommandIds(context) - - XCTAssertEqual(visibleCommandIds, [ - ContentView.commandPaletteAuthSignOutCommandId, - ContentView.commandPaletteAuthTeamPickerCommandId, - ]) - } - - func testWorkingAuthContextHidesSignInAndSignOutCommands() { - for signedIn in [false, true] { - var context = CommandPaletteContextSnapshot() - context.setBool(CommandPaletteContextKeys.authSignedIn, signedIn) - context.setBool(CommandPaletteContextKeys.authWorking, true) - - XCTAssertTrue(visibleAuthCommandIds(context).isEmpty) - } - } - - private func visibleAuthCommandIds(_ context: CommandPaletteContextSnapshot) -> [String] { - ContentView.commandPaletteAuthCommandContributions() - .filter { $0.when(context) } - .map(\.commandId) - } -} - final class CommandPaletteCloudCommandTests: XCTestCase { @MainActor func testCloudCommandPaletteIncludesCloudWorkspaceActions() { diff --git a/cmuxUITests/CloudTeamPickerPlacementUITests.swift b/cmuxUITests/CloudTeamPickerPlacementUITests.swift new file mode 100644 index 000000000000..5c62f302cad9 --- /dev/null +++ b/cmuxUITests/CloudTeamPickerPlacementUITests.swift @@ -0,0 +1,162 @@ +import XCTest + +final class CloudTeamPickerPlacementUITests: XCTestCase { + private let flagKeys = [ + "cmux.flags.override.cloud-machines-enabled-release", + "cmux.flags.override.sidebar-account-button-enabled-release", + ] + private let teamPickerShortcutKey = "shortcut.openTeamPicker" + private var savedTeamPickerShortcut: Any? + private var savedFlags: [String: Any] = [:] + private var fixtureDefaults: UserDefaults? + + override func setUpWithError() throws { + try super.setUpWithError() + let defaults = try XCTUnwrap(UserDefaults(suiteName: "com.cmuxterm.app.debug")) + fixtureDefaults = defaults + savedTeamPickerShortcut = defaults.object(forKey: teamPickerShortcutKey) + defaults.set(Self.defaultTeamPickerShortcutData, forKey: teamPickerShortcutKey) + // The flag reader accepts typed Booleans. Launch arguments such as YES + // are strings and do not force the effective flag on. + for key in flagKeys { + savedFlags[key] = defaults.object(forKey: key) + defaults.set(true, forKey: key) + } + defaults.synchronize() + } + + + private static let defaultTeamPickerShortcutData = Data(#"{"key":"t","command":true,"shift":true,"option":true,"control":false}"#.utf8) + override func tearDown() { + for key in flagKeys { + fixtureDefaults?.set(savedFlags[key], forKey: key) + } + if let savedTeamPickerShortcut { + fixtureDefaults?.set(savedTeamPickerShortcut, forKey: teamPickerShortcutKey) + } else { + fixtureDefaults?.removeObject(forKey: teamPickerShortcutKey) + } + fixtureDefaults?.synchronize() + fixtureDefaults = nil + savedFlags.removeAll() + super.tearDown() + } + + func testAccountFooterDoesNotOfferTeamPickerAndCloudHeaderDoes() { + let app = launchSignedInApp() + defer { app.terminate() } + let accountButton = app.buttons.matching(NSPredicate( + format: "identifier == %@ OR label == %@", "SidebarAccountMenuButton", "Account" + )).firstMatch + XCTAssertTrue(accountButton.waitForExistence(timeout: 10)) + accountButton.click() + XCTAssertTrue(app.buttons["SidebarAccountSignOutButton"].waitForExistence(timeout: 5)) + capture("account-popover") + XCTAssertFalse( + app.buttons["SidebarAccountTeamPickerButton"].waitForExistence(timeout: 2), + "The local account popover must not offer team switching." + ) + XCTAssertFalse(app.buttons["SidebarAccountCreateTeamButton"].exists) + + let cloudMode = app.buttons["RightSidebarModeButton.machines"] + XCTAssertTrue(cloudMode.waitForExistence(timeout: 10)) + cloudMode.click() + capture("cloud-header") + XCTAssertTrue( + app.buttons["CloudTeamPickerButton"].waitForExistence(timeout: 10), + "The signed-in Cloud header must offer team scope." + ) + } + + func testShortcutAndPaletteRevealCloudAndOpenItsPicker() { + let app = launchSignedInApp(sidebarVisible: false) + defer { app.terminate() } + app.typeKey("t", modifierFlags: [.command, .option, .shift]) + let create = app.buttons["CloudTeamPickerCreateTeamButton"] + XCTAssertTrue(create.waitForExistence(timeout: 10)) + XCTAssertTrue(app.buttons["CloudTeamPickerButton"].exists) + XCTAssertFalse(app.buttons["SidebarAccountSignOutButton"].exists) + capture("shortcut-opens-cloud-picker") + + create.click() + let name = app.textFields["Team name"] + XCTAssertTrue(name.waitForExistence(timeout: 5)) + name.typeText("Draft team") + app.buttons["Cancel"].click() + XCTAssertTrue(create.waitForExistence(timeout: 5)) + capture("cloud-create-team-editor-cancelled") + app.typeKey(.escape, modifierFlags: []) + XCTAssertTrue(create.waitForNonExistence(timeout: 5)) + app.buttons["RightSidebar.closeButton"].click() + + invokePickerFromPalette(app) + XCTAssertTrue(create.waitForExistence(timeout: 10)) + XCTAssertTrue(app.buttons["CloudTeamPickerButton"].exists) + capture("palette-opens-cloud-picker") + } + + func testCloudGateExplainsWhyPickerCannotOpen() { + let app = launchSignedInApp(cloudEnabled: false, sidebarVisible: false) + defer { app.terminate() } + app.typeKey("t", modifierFlags: [.command, .option, .shift]) + let unavailable = app.staticTexts["Cloud Machines are temporarily unavailable."] + XCTAssertTrue(unavailable.waitForExistence(timeout: 10)) + XCTAssertFalse(app.buttons["CloudTeamPickerButton"].exists) + capture("shortcut-cloud-unavailable") + app.buttons["OK"].click() + XCTAssertTrue(unavailable.waitForNonExistence(timeout: 5)) + + invokePickerFromPalette(app) + XCTAssertTrue(unavailable.waitForExistence(timeout: 10)) + XCTAssertFalse(app.buttons["CloudTeamPickerButton"].exists) + capture("palette-cloud-unavailable") + } + + private func invokePickerFromPalette(_ app: XCUIApplication) { + app.typeKey("p", modifierFlags: [.command, .shift]) + let search = app.textFields["CommandPaletteSearchField"] + XCTAssertTrue(search.waitForExistence(timeout: 5)) + search.click() + search.typeText("open team picker") + let command = app.descendants(matching: .any).matching(NSPredicate( + format: "identifier BEGINSWITH %@ AND value == %@", + "CommandPaletteResultRow.", "palette.auth.teamPicker" + )).firstMatch + XCTAssertTrue(command.waitForExistence(timeout: 5)) + command.click() + } + + private func launchSignedInApp( + cloudEnabled: Bool = true, + sidebarVisible: Bool = true + ) -> XCUIApplication { + let app = XCUIApplication.cmuxTestApplication() + app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" + app.launchEnvironment["CMUX_UITEST_AUTH_FIXTURE"] = "1" + app.launchEnvironment["CMUX_UITEST_AUTH_USER_ID"] = "team-picker-fixture" + app.launchEnvironment["CMUX_UITEST_AUTH_NAME"] = "Team Picker Fixture" + if sidebarVisible { + app.launchEnvironment["CMUX_UI_TEST_BONSPLIT_SHOW_RIGHT_SIDEBAR"] = "1" + } + app.launchArguments += [ + "-workspacePresentationMode", "standard", + "-cloud.beta.machines.enabled", cloudEnabled ? "YES" : "NO", + "-fileExplorer.isVisible", sidebarVisible ? "YES" : "NO", + "-rightSidebar.mode", "files", + "-menuBarOnly", "false", + "-AppleLanguages", "(en)", + "-AppleLocale", "en_US", + ] + app.launch() + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 20)) + return app + } + + private func capture(_ name: String) { + let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) + screenshot.name = name + screenshot.lifetime = .keepAlways + add(screenshot) + } +}