From 1bc9f257f26f64394ecd4b1d525fb9ba53a59292 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:46:40 -0700 Subject: [PATCH 01/88] ios: keep tab navigation chrome mounted --- .../MobilePrimaryTabScaffold.swift | 132 +++++++++++++++++- .../WorkspaceShellView.swift | 12 ++ 2 files changed, 138 insertions(+), 6 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 0d21928833ee..651900a1d18e 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -2,6 +2,21 @@ import CmuxMobileSupport import SwiftUI +private struct TopContentHitRegion: Shape { + let bottomInset: CGFloat + + func path(in rect: CGRect) -> Path { + Path( + CGRect( + x: rect.minX, + y: rect.minY, + width: rect.width, + height: max(0, rect.height - bottomInset) + ) + ) + } +} + /// Native primary navigation shared by the live shell and deterministic UI /// fixtures. Keeping the tab construction here guarantees that previews exercise /// the same labels, symbols, badge behavior, and selection semantics as the app. @@ -40,20 +55,78 @@ struct MobilePrimaryTabScaffold< if #available(iOS 26.0, *) { ZStack(alignment: .bottomTrailing) { TabView(selection: tabSelection) { - primaryTabs - + Tab(value: MobilePrimaryTab.workspaces) { + tabBarPlaceholder + } label: { + workspacesLabel + } + .accessibilityIdentifier("MobilePrimaryTabWorkspaces") + Tab(value: MobilePrimaryTab.notifications) { + tabBarPlaceholder + } label: { + notificationsLabel + } + .badge(notificationUnreadCount) + .accessibilityIdentifier("MobilePrimaryTabNotifications") Tab(value: MobilePrimaryTab.search, role: .search) { - search - .environment(\.mobilePrimarySearchDestination, true) + tabBarPlaceholder } - .accessibilityIdentifier("MobilePrimaryTabSearch") + .accessibilityIdentifier("MobilePrimaryTabSearch") } .tabViewSearchActivation(.searchTabSelection) + .tabViewStyle(.tabBarOnly) + .background(Color.clear) .accessibilityIdentifier("MobilePrimaryTabs") + .animation(nil, value: selection) + // Each tab owns a NavigationStack. SwiftUI's tab transition + // otherwise crossfades the stacks before the incoming stack's + // toolbar items are installed, leaving a blank frame at the + // top of the screen. + .transaction { transaction in + transaction.disablesAnimations = true + } + .overlay(alignment: .top) { + // Keep each navigation stack mounted outside the system + // tab content transition. iOS 26 crossfades a tab's + // hosted NavigationStack before its toolbar items have + // been laid out, which produces a blank top frame. This + // overlay swaps the already-mounted stacks synchronously; + // its bottom inset leaves the native tab bar on top. + GeometryReader { geometry in + ZStack { + workspaces + .opacity(selection == .workspaces ? 1 : 0) + .allowsHitTesting(selection == .workspaces) + .accessibilityHidden(selection != .workspaces) + notifications + .opacity(selection == .notifications ? 1 : 0) + .allowsHitTesting(selection == .notifications) + .accessibilityHidden(selection != .notifications) + search + .environment(\.mobilePrimarySearchDestination, true) + .opacity(selection == .search ? 1 : 0) + .allowsHitTesting(selection == .search) + .accessibilityHidden(selection != .search) + } + .frame( + width: geometry.size.width, + height: max(0, geometry.size.height - iOS26TabBarInteractionHeight), + alignment: .top + ) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .contentShape(TopContentHitRegion(bottomInset: iOS26TabBarInteractionHeight)) + } .onChange(of: selection, initial: true) { _, selection in searchCoordinator.synchronizeSelection(selection) } + // The persistent content layer sits above the tab bar's + // transparent content host, so route the tab bar's hit area + // back to the same selection binding while leaving its native + // visuals intact. + tabBarInteractionOverlay + if selection == .workspaces, let taskComposerAction { TaskComposerButton( action: taskComposerAction, @@ -101,6 +174,41 @@ struct MobilePrimaryTabScaffold< iOS26BottomControlInset + iOS26BottomControlDiameter + iOS26BottomControlSpacing } + private var iOS26TabBarInteractionHeight: CGFloat { 90 } + + private var tabBarPlaceholder: some View { + Color.clear + .frame(maxWidth: .infinity, maxHeight: .infinity) + .allowsHitTesting(false) + } + + private var tabBarInteractionOverlay: some View { + HStack(spacing: 0) { + tabBarButton(for: .workspaces) + .frame(width: 110) + tabBarButton(for: .notifications) + .frame(width: 110) + Spacer(minLength: 0) + tabBarButton(for: .search) + .frame(width: 90) + } + .padding(.horizontal, 45) + .frame(height: iOS26TabBarInteractionHeight) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + .accessibilityHidden(true) + } + + private func tabBarButton(for tab: MobilePrimaryTab) -> some View { + Button { + tabSelection.wrappedValue = tab + } label: { + Color.clear + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .contentShape(Rectangle()) + .buttonStyle(.plain) + } + private var tabSelection: Binding { Binding( get: { selection }, @@ -115,7 +223,19 @@ struct MobilePrimaryTabScaffold< searchCoordinator.deactivateCurrentSearch() } } - selection = newValue + // Each primary tab owns a NavigationStack. Letting the + // selection write inherit SwiftUI's default animation makes + // UIKit animate the outgoing stack's toolbar away before the + // incoming stack has installed its own toolbar items. The + // resulting empty frame is the brief flash seen at the top + // while switching between Workspaces and Notifications. + // Keep the tab contents and their navigation state intact, + // but commit the stack swap as one layout transaction. + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + selection = newValue + } } ) } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 70f1585e295f..b51756a56743 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -447,7 +447,13 @@ struct WorkspaceShellView: View { if notificationNavigationPath.isEmpty { rootToolbarContent } + ToolbarItem(id: "mobile-primary-toolbar-anchor", placement: .topBarTrailing) { + Color.clear + .frame(width: 1, height: 1) + .accessibilityHidden(true) + } } + .toolbar(.visible, for: .navigationBar) .navigationDestination(for: MobileWorkspacePreview.ID.self) { workspaceID in workspaceDestination( for: workspaceID, @@ -778,7 +784,13 @@ struct WorkspaceShellView: View { if compactNavigationPath.isEmpty { rootToolbarContent } + ToolbarItem(id: "mobile-primary-toolbar-anchor", placement: .topBarTrailing) { + Color.clear + .frame(width: 1, height: 1) + .accessibilityHidden(true) + } } + .toolbar(.visible, for: .navigationBar) .navigationDestination(for: MobileWorkspacePreview.ID.self) { workspaceID in workspaceDestination( for: workspaceID, From 54ae5dc620db1262f0a8d411335a831ccf3883d2 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:32:39 -0700 Subject: [PATCH 02/88] test(ios): cover notification tab toolbar switching --- ios/cmuxUITests/cmuxUITests.swift | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ios/cmuxUITests/cmuxUITests.swift b/ios/cmuxUITests/cmuxUITests.swift index ca79dfb1100c..d38c918af2a0 100644 --- a/ios/cmuxUITests/cmuxUITests.swift +++ b/ios/cmuxUITests/cmuxUITests.swift @@ -4499,6 +4499,33 @@ final class cmuxUITests: XCTestCase { } } + @MainActor + func testNotificationTabSwitchKeepsSharedRootToolbarMounted() throws { + let app = launchApp(mockData: false, environment: [ + "CMUX_UITEST_NOTIFICATION_FEED_PREVIEW": "1", + ]) + defer { app.terminate() } + + let feed = app.descendants(matching: .any)["MobileNotificationFeed"] + XCTAssertTrue(feed.waitForExistence(timeout: 8)) + XCTAssertTrue(app.buttons["MobileNotificationFeedMarkAllRead"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.buttons["MobileNotificationFeedFilterMenu"].waitForExistence(timeout: 3)) + + tap(app.tabBars.buttons["Workspaces"], in: app) + XCTAssertTrue(app.staticTexts["Workspaces"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.buttons["MobileWorkspaceSettingsMenu"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.buttons["MobileWorkspaceDevicesButton"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.buttons["MobileWorkspaceMacPicker"].waitForExistence(timeout: 3)) + + tap(app.tabBars.buttons["Notifications"], in: app) + XCTAssertTrue(waitForHittable(feed, timeout: 3)) + XCTAssertTrue(app.buttons["MobileWorkspaceSettingsMenu"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.buttons["MobileWorkspaceDevicesButton"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.buttons["MobileWorkspaceMacPicker"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.buttons["MobileNotificationFeedMarkAllRead"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.buttons["MobileNotificationFeedFilterMenu"].waitForExistence(timeout: 3)) + } + /// Drives the production push coordinator through its three user-visible /// states: a parked tap while the Mac is disconnected, selection after the /// connection recovers, and an alert when the target tab is gone. From 4feb1454a030afd3db09583697a352af75d32fd3 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:32:49 -0700 Subject: [PATCH 03/88] fix(ios): keep compact toolbar mounted across tabs --- .../MobilePrimaryTabScaffold.swift | 29 +++ .../NotificationFeedPreviewView.swift | 178 +++++++++++------- .../NotificationFeedStoreView.swift | 6 +- .../NotificationFeedView.swift | 61 ++++-- .../WorkspaceShellView.swift | 146 +++++++------- ...UITestConfig+NotificationFeedPreview.swift | 14 ++ .../UITestConfigTests.swift | 9 + 7 files changed, 290 insertions(+), 153 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 651900a1d18e..8337b26b156e 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -17,6 +17,35 @@ private struct TopContentHitRegion: Shape { } } +/// Keeps the compact tab bar and its root navigation chrome in one hierarchy. +/// The tab contents still keep their own navigation paths, but they no longer +/// compete to install the root toolbar as selection changes. +struct MobilePrimaryTabNavigationHost: View { + let content: Content + let toolbar: Toolbar + let toolbarVisibility: Visibility + + init( + toolbarVisibility: Visibility, + @ToolbarContentBuilder toolbar: () -> Toolbar, + @ViewBuilder content: () -> Content + ) { + self.content = content() + self.toolbar = toolbar() + self.toolbarVisibility = toolbarVisibility + } + + var body: some View { + NavigationStack { + content + .toolbar { + toolbar + } + .toolbar(toolbarVisibility, for: .navigationBar) + } + } +} + /// Native primary navigation shared by the live shell and deterministic UI /// fixtures. Keeping the tab construction here guarantees that previews exercise /// the same labels, symbols, badge behavior, and selection semantics as the app. diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index 7c6c13d71f50..2c4c9f6d1818 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -15,6 +15,7 @@ public struct NotificationFeedPreviewView: View { @State private var referenceDate: Date @State private var items: [MobileNotificationFeedItem] @State private var projection = NotificationFeedProjection() + @State private var isConfirmingMarkAllRead = false @State private var notificationRoute: NotificationWorkspaceRoute? @State private var searchNavigationPath: [MobileWorkspacePreview.ID] = [] @State private var pendingSearchNotificationNavigationID: MobileWorkspacePreview.ID? @@ -41,49 +42,94 @@ public struct NotificationFeedPreviewView: View { /// The preview fixture's production-style tab and feed body. public var body: some View { GeometryReader { geometry in - MobilePrimaryTabScaffold( - selection: $selectedTab, - searchCoordinator: primarySearchCoordinator, - notificationUnreadCount: items.lazy.filter { !$0.isRead }.count - ) { - NavigationStack { - NotificationFeedPreviewWorkspacesView() - } - } notifications: { - NavigationStack { - ScrollViewReader { proxy in - notificationsTabFeed(proxy: proxy) - } - } - .onAppear { - consumePendingSearchNavigation(for: .notifications) - } - .onChange(of: pendingSearchNotificationNavigationID) { _, _ in - consumePendingSearchNavigation(for: .notifications) - } - } search: { - MobilePrimarySearchNavigationStack( - path: $searchNavigationPath, - selection: $selectedTab, - searchCoordinator: primarySearchCoordinator - ) { - switch primarySearchCoordinator.scope { - case .workspaces: - NotificationFeedPreviewWorkspacesView() - case .notifications: - NotificationFeedView( - status: .ready, - projection: projection, - refreshesOnAppear: false, - actions: actions + MobilePrimaryTabNavigationHost( + toolbarVisibility: previewRootToolbarVisible ? .visible : .hidden, + toolbar: { + if previewRootToolbarVisible { + WorkspaceRootToolbarContent( + openSettings: {}, + openDevices: {}, + title: L10n.string( + "mobile.workspaces.macPicker.allConnections", + defaultValue: "All Computers" + ), + isLoading: false, + selection: macSelection, + select: { macSelection = $0 }, + machines: [], + showAddDevice: nil ) + if selectedTab == .notifications + || (selectedTab == .search && primarySearchCoordinator.scope == .notifications) { + NotificationFeedToolbarContent( + projection: projection, + requestMarkAllRead: { isConfirmingMarkAllRead = true } + ) + } + } + }, + content: { + MobilePrimaryTabScaffold( + selection: $selectedTab, + searchCoordinator: primarySearchCoordinator, + notificationUnreadCount: items.lazy.filter { !$0.isRead }.count + ) { + NavigationStack { + NotificationFeedPreviewWorkspacesView() + } + } notifications: { + NavigationStack { + ScrollViewReader { proxy in + notificationsTabFeed(proxy: proxy) + } + .navigationDestination(isPresented: notificationRouteIsPresented) { + NotificationFeedPreviewWorkspaceDestination( + workspaceName: notificationRoute.map { workspaceName(for: $0.id) } + ?? L10n.string( + "mobile.notificationFeed.workspaceFallback", + defaultValue: "Workspace" + ) + ) + .toolbar(.visible, for: .navigationBar) + .mobileToolbarVisibility(.hidden, for: .tabBar) + } + } + .onAppear { + consumePendingSearchNavigation(for: .notifications) + } + .onChange(of: pendingSearchNotificationNavigationID) { _, _ in + consumePendingSearchNavigation(for: .notifications) + } + } search: { + MobilePrimarySearchNavigationStack( + path: $searchNavigationPath, + selection: $selectedTab, + searchCoordinator: primarySearchCoordinator + ) { + Group { + switch primarySearchCoordinator.scope { + case .workspaces: + NotificationFeedPreviewWorkspacesView() + case .notifications: + NotificationFeedView( + status: .ready, + projection: projection, + refreshesOnAppear: false, + actions: actions, + isConfirmingMarkAllRead: $isConfirmingMarkAllRead, + showsNavigationToolbar: false + ) + } + } + } destination: { workspaceID in + NotificationFeedPreviewWorkspaceDestination( + workspaceName: workspaceName(for: workspaceID) + ) + .toolbar(.visible, for: .navigationBar) + } } - } destination: { workspaceID in - NotificationFeedPreviewWorkspaceDestination( - workspaceName: workspaceName(for: workspaceID) - ) } - } + ) .background { NotificationFeedSearchProjectionSync( searchCoordinator: primarySearchCoordinator, @@ -104,6 +150,29 @@ public struct NotificationFeedPreviewView: View { .onChange(of: items, initial: true) { _, items in projection.update(items: items, referenceDate: referenceDate) } + .task { + guard UITestConfig.notificationFeedPreviewTabSwitchEnabled else { return } + let clock = ContinuousClock() + for _ in 0..<6 where !Task.isCancelled { + try? await clock.sleep(for: .milliseconds(700)) + guard !Task.isCancelled else { return } + transitionPrimaryTab(to: .workspaces) + try? await clock.sleep(for: .milliseconds(700)) + guard !Task.isCancelled else { return } + transitionPrimaryTab(to: .notifications) + } + } + } + + private var previewRootToolbarVisible: Bool { + switch selectedTab { + case .workspaces: + true + case .notifications: + notificationRoute == nil + case .search: + searchNavigationPath.isEmpty + } } /// The notifications-tab feed with the optional profiling scroll driver. @@ -112,36 +181,13 @@ public struct NotificationFeedPreviewView: View { status: .ready, projection: projection, refreshesOnAppear: true, - actions: actions + actions: actions, + isConfirmingMarkAllRead: $isConfirmingMarkAllRead, + showsNavigationToolbar: false ) .task { await runScrollStressIfEnabled(proxy: proxy) } - .toolbar { - WorkspaceRootToolbarContent( - openSettings: {}, - openDevices: {}, - title: L10n.string( - "mobile.workspaces.macPicker.allConnections", - defaultValue: "All Computers" - ), - isLoading: false, - selection: macSelection, - select: { macSelection = $0 }, - machines: [], - showAddDevice: nil - ) - } - .navigationDestination(isPresented: notificationRouteIsPresented) { - NotificationFeedPreviewWorkspaceDestination( - workspaceName: notificationRoute.map { workspaceName(for: $0.id) } - ?? L10n.string( - "mobile.notificationFeed.workspaceFallback", - defaultValue: "Workspace" - ) - ) - .mobileToolbarVisibility(.hidden, for: .tabBar) - } } private var actions: NotificationFeedActions { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index a6e93e6b40c0..f83607bcb5a5 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -8,18 +8,22 @@ import SwiftUI /// This is the only notification-feed view that retains a store reference. struct NotificationFeedStoreView: View { @Bindable var store: CMUXMobileShellStore + @Binding var isConfirmingMarkAllRead: Bool @Environment(\.mobilePrimarySearchDestination) private var isSearchDestination let items: [MobileNotificationFeedItem] let status: MobileNotificationFeedStatus let projection: NotificationFeedProjection let selectedMacDeviceIDs: Set? + var showsNavigationToolbar = true var body: some View { NotificationFeedView( status: status, projection: projection, refreshesOnAppear: !isSearchDestination, - actions: actions + actions: actions, + isConfirmingMarkAllRead: $isConfirmingMarkAllRead, + showsNavigationToolbar: showsNavigationToolbar ) .onAppear { store.recordAppEvent(.notificationFeedOpened, count: items.count) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index eeb2dae12e62..dee765a802e6 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -21,14 +21,15 @@ struct NotificationFeedView: View { let projection: NotificationFeedProjection let refreshesOnAppear: Bool let actions: NotificationFeedActions + @Binding var isConfirmingMarkAllRead: Bool + let showsNavigationToolbar: Bool /// Mark-all-read cannot be undone in one gesture, so the toolbar button /// only arms this confirmation instead of mutating directly. - @State private var isConfirmingMarkAllRead = false var body: some View { @Bindable var projection = projection - VStack(spacing: 0) { + let feed = VStack(spacing: 0) { NotificationFeedList( sections: projection.sections, sourceItemCount: projection.sourceItemCount, @@ -49,25 +50,17 @@ struct NotificationFeedView: View { // No title of its own (the tab names the screen), so collapse the // large-title zone or the list opens with a bar-height blank strip. .mobileInlineNavigationTitle() - .toolbar { - ToolbarItemGroup(placement: .topBarTrailing) { - if projection.sourceUnreadCount > 0 { - Button { - isConfirmingMarkAllRead = true - } label: { - Label( - L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read"), - systemImage: "envelope.open" - ) - .labelStyle(.iconOnly) - } - .accessibilityLabel( - L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read") + + Group { + if showsNavigationToolbar { + feed.toolbar { + NotificationFeedToolbarContent( + projection: projection, + requestMarkAllRead: { isConfirmingMarkAllRead = true } ) - .accessibilityIdentifier("MobileNotificationFeedMarkAllRead") } - - NotificationFeedFilterMenu(selection: $projection.filter) + } else { + feed } } .alert( @@ -98,10 +91,38 @@ struct NotificationFeedView: View { } } +/// Toolbar controls shared by the visible notification feed and the compact +/// primary-tab parent. Keeping this preference in one toolbar hierarchy avoids +/// inactive, opacity-hidden feed stacks contributing duplicate items. +struct NotificationFeedToolbarContent: ToolbarContent { + @Bindable var projection: NotificationFeedProjection + let requestMarkAllRead: () -> Void + + var body: some ToolbarContent { + ToolbarItemGroup(placement: .topBarTrailing) { + if projection.sourceUnreadCount > 0 { + Button(action: requestMarkAllRead) { + Label( + L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read"), + systemImage: "envelope.open" + ) + .labelStyle(.iconOnly) + } + .accessibilityLabel( + L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read") + ) + .accessibilityIdentifier("MobileNotificationFeedMarkAllRead") + } + + NotificationFeedFilterMenu(selection: $projection.filter) + } + } +} + /// The feed twin of `WorkspaceListFilterMenu`: read state lives in a toolbar /// menu instead of a segmented bar above the list, and the icon fills while a /// narrowing filter is active, mirroring Mail. -private struct NotificationFeedFilterMenu: View { +struct NotificationFeedFilterMenu: View { @Binding var selection: MobileNotificationFeedFilter var body: some View { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index b51756a56743..1b5a52ee3bf3 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -244,6 +244,7 @@ struct WorkspaceShellView: View { @State private var notificationNavigationPath: [MobileWorkspacePreview.ID] = [] @State private var notificationSearchNavigationPath: [MobileWorkspacePreview.ID] = [] @State private var workspaceSearchNavigationPath: [MobileWorkspacePreview.ID] = [] + @State private var isConfirmingNotificationFeedMarkAllRead = false @State private var pendingPrimarySearchWorkspaceNavigationID: MobileWorkspacePreview.ID? @State private var pendingPrimarySearchNotificationNavigationID: MobileWorkspacePreview.ID? // A NavigationStack path write only reaches UIKit while the stack is in the @@ -298,6 +299,19 @@ struct WorkspaceShellView: View { #endif } + #if os(iOS) + private var compactRootToolbarVisible: Bool { + switch selectedPrimaryTab { + case .workspaces: + return compactNavigationPath.isEmpty + case .notifications: + return notificationNavigationPath.isEmpty + case .search: + return primarySearchNavigationPath.wrappedValue.isEmpty + } + } + #endif + private var listConnectionStatus: MobileMacConnectionStatus { if isInitialConnectionLoading || initialConnectionTimedOut { return .reconnecting @@ -423,59 +437,70 @@ struct WorkspaceShellView: View { /// The compact (iPhone-style) shell: the primary destinations live in the /// system TabView with the transient search tab. private func compactScaffold(presentation: WorkspaceShellRenderPresentation) -> some View { - MobilePrimaryTabScaffold( - selection: $selectedPrimaryTab, - searchCoordinator: primarySearchCoordinator, - notificationUnreadCount: presentation.notificationUnreadCount, - taskComposerAction: usesCompactStack && !compactNavigationPath.isEmpty - ? nil - : taskComposerAction - ) { - workspaceTabContent( - presentation: presentation - ) - } notifications: { - NavigationStack(path: $notificationNavigationPath) { - NotificationFeedStoreView( - store: store, - items: presentation.notificationFeedItems, - status: presentation.notificationFeedStatus, - projection: notificationFeedProjection, - selectedMacDeviceIDs: presentation.selectedNotificationFeedMacDeviceIDs - ) - .toolbar { - if notificationNavigationPath.isEmpty { - rootToolbarContent - } - ToolbarItem(id: "mobile-primary-toolbar-anchor", placement: .topBarTrailing) { - Color.clear - .frame(width: 1, height: 1) - .accessibilityHidden(true) - } + MobilePrimaryTabNavigationHost( + toolbarVisibility: compactRootToolbarVisible ? .visible : .hidden, + toolbar: { + if compactRootToolbarVisible { + rootToolbarContent + if selectedPrimaryTab == .notifications + || (selectedPrimaryTab == .search && primarySearchCoordinator.scope == .notifications) { + NotificationFeedToolbarContent( + projection: notificationFeedProjection, + requestMarkAllRead: { + isConfirmingNotificationFeedMarkAllRead = true + } + ) } - .toolbar(.visible, for: .navigationBar) - .navigationDestination(for: MobileWorkspacePreview.ID.self) { workspaceID in - workspaceDestination( - for: workspaceID, - createWorkspace: createWorkspaceInCompactStack, - canCreateWorkspaceForSelection: presentation.canCreateWorkspaceForSelection + } + }, + content: { + MobilePrimaryTabScaffold( + selection: $selectedPrimaryTab, + searchCoordinator: primarySearchCoordinator, + notificationUnreadCount: presentation.notificationUnreadCount, + taskComposerAction: usesCompactStack && !compactNavigationPath.isEmpty + ? nil + : taskComposerAction + ) { + workspaceTabContent( + presentation: presentation + ) + } notifications: { + NavigationStack(path: $notificationNavigationPath) { + NotificationFeedStoreView( + store: store, + isConfirmingMarkAllRead: $isConfirmingNotificationFeedMarkAllRead, + items: presentation.notificationFeedItems, + status: presentation.notificationFeedStatus, + projection: notificationFeedProjection, + selectedMacDeviceIDs: presentation.selectedNotificationFeedMacDeviceIDs, + showsNavigationToolbar: false ) - .mobileToolbarVisibility(.hidden, for: .tabBar) + .navigationDestination(for: MobileWorkspacePreview.ID.self) { workspaceID in + workspaceDestination( + for: workspaceID, + createWorkspace: createWorkspaceInCompactStack, + canCreateWorkspaceForSelection: presentation.canCreateWorkspaceForSelection + ) + .toolbar(.visible, for: .navigationBar) + .mobileToolbarVisibility(.hidden, for: .tabBar) + } + } + .onAppear { + notificationsStackIsOnScreen = true + consumePendingPrimarySearchNavigation(for: .notifications) + } + .onDisappear { + notificationsStackIsOnScreen = false + } + .onChange(of: pendingPrimarySearchNotificationNavigationID) { _, _ in + consumePendingPrimarySearchNavigation(for: .notifications) + } + } search: { + primarySearchTabContent(presentation: presentation) } } - .onAppear { - notificationsStackIsOnScreen = true - consumePendingPrimarySearchNavigation(for: .notifications) - } - .onDisappear { - notificationsStackIsOnScreen = false - } - .onChange(of: pendingPrimarySearchNotificationNavigationID) { _, _ in - consumePendingPrimarySearchNavigation(for: .notifications) - } - } search: { - primarySearchTabContent(presentation: presentation) - } + ) } #endif @@ -530,24 +555,22 @@ struct WorkspaceShellView: View { case .notifications: NotificationFeedStoreView( store: store, + isConfirmingMarkAllRead: $isConfirmingNotificationFeedMarkAllRead, items: presentation.notificationFeedItems, status: presentation.notificationFeedStatus, projection: notificationFeedProjection, - selectedMacDeviceIDs: presentation.selectedNotificationFeedMacDeviceIDs + selectedMacDeviceIDs: presentation.selectedNotificationFeedMacDeviceIDs, + showsNavigationToolbar: false ) } } - .toolbar { - if primarySearchNavigationPath.wrappedValue.isEmpty { - rootToolbarContent - } - } } destination: { workspaceID in workspaceDestination( for: workspaceID, createWorkspace: createWorkspaceInCompactStack, canCreateWorkspaceForSelection: presentation.canCreateWorkspaceForSelection ) + .toolbar(.visible, for: .navigationBar) } } } @@ -780,17 +803,6 @@ struct WorkspaceShellView: View { canCreateWorkspaceForSelection: canCreateWorkspaceForSelection ) } - .toolbar { - if compactNavigationPath.isEmpty { - rootToolbarContent - } - ToolbarItem(id: "mobile-primary-toolbar-anchor", placement: .topBarTrailing) { - Color.clear - .frame(width: 1, height: 1) - .accessibilityHidden(true) - } - } - .toolbar(.visible, for: .navigationBar) .navigationDestination(for: MobileWorkspacePreview.ID.self) { workspaceID in workspaceDestination( for: workspaceID, @@ -805,6 +817,7 @@ struct WorkspaceShellView: View { #if os(iOS) .mobileToolbarVisibility(.hidden, for: .tabBar, .bottomBar) #endif + .toolbar(.visible, for: .navigationBar) // Only on the pushed compact stack (where a back button // exists): replace the system back button with a custom one // that folds the unread-workspace count INTO the same button @@ -970,6 +983,7 @@ struct WorkspaceShellView: View { case .notifications: NotificationFeedStoreView( store: store, + isConfirmingMarkAllRead: $isConfirmingNotificationFeedMarkAllRead, items: notificationItems, status: presentation.notificationFeedStatus, projection: notificationFeedProjection, diff --git a/Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+NotificationFeedPreview.swift b/Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+NotificationFeedPreview.swift index 4f3df38ad959..796482fae96d 100644 --- a/Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+NotificationFeedPreview.swift +++ b/Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+NotificationFeedPreview.swift @@ -62,4 +62,18 @@ extension UITestConfig { return false #endif } + + /// Whether the notification-feed fixture should drive repeated primary-tab + /// switches for visual transition verification. + public static var notificationFeedPreviewTabSwitchEnabled: Bool { + notificationFeedPreviewTabSwitchEnabled(from: ProcessInfo.processInfo.environment) + } + + static func notificationFeedPreviewTabSwitchEnabled(from env: [String: String]) -> Bool { + #if DEBUG + env["CMUX_UITEST_NOTIFICATION_FEED_PREVIEW_TAB_SWITCH"] == "1" + #else + false + #endif + } } diff --git a/Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift b/Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift index eabda1026fc2..fa9e67efb876 100644 --- a/Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift +++ b/Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift @@ -367,6 +367,15 @@ import Testing ) == false) } + @Test func notificationFeedPreviewTabSwitchFlagIsDebugOnly() { + let env = ["CMUX_UITEST_NOTIFICATION_FEED_PREVIEW_TAB_SWITCH": "1"] + #if DEBUG + #expect(UITestConfig.notificationFeedPreviewTabSwitchEnabled(from: env)) + #else + #expect(!UITestConfig.notificationFeedPreviewTabSwitchEnabled(from: env)) + #endif + } + @Test func taskComposerPreviewFlagIsDebugOnly() { let env = ["CMUX_UITEST_TASK_COMPOSER_PREVIEW": "1"] #if DEBUG From 1f56e1bb0fc73358b94c4ad0215749153f654c49 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:19:42 -0700 Subject: [PATCH 04/88] fix(ios): unmount inactive primary tab stacks --- .../MobilePrimaryTabScaffold.swift | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 8337b26b156e..842ce53a901e 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -115,27 +115,25 @@ struct MobilePrimaryTabScaffold< transaction.disablesAnimations = true } .overlay(alignment: .top) { - // Keep each navigation stack mounted outside the system + // Keep the selected navigation stack outside the system // tab content transition. iOS 26 crossfades a tab's // hosted NavigationStack before its toolbar items have - // been laid out, which produces a blank top frame. This - // overlay swaps the already-mounted stacks synchronously; - // its bottom inset leaves the native tab bar on top. + // been laid out, which produces a blank top frame. The + // selected stack is swapped synchronously; its bottom + // inset leaves the native tab bar on top. Mounting only + // the selected stack also keeps onAppear/onDisappear and + // feed refresh work scoped to the visible tab. GeometryReader { geometry in - ZStack { - workspaces - .opacity(selection == .workspaces ? 1 : 0) - .allowsHitTesting(selection == .workspaces) - .accessibilityHidden(selection != .workspaces) - notifications - .opacity(selection == .notifications ? 1 : 0) - .allowsHitTesting(selection == .notifications) - .accessibilityHidden(selection != .notifications) - search - .environment(\.mobilePrimarySearchDestination, true) - .opacity(selection == .search ? 1 : 0) - .allowsHitTesting(selection == .search) - .accessibilityHidden(selection != .search) + Group { + switch selection { + case .workspaces: + workspaces + case .notifications: + notifications + case .search: + search + .environment(\.mobilePrimarySearchDestination, true) + } } .frame( width: geometry.size.width, From 602eb9bb50f4b3ec2faf0064571f1b1f3b898785 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:38:26 -0700 Subject: [PATCH 05/88] fix(ios): centralize notification confirmation and tab hits --- .../MobilePrimaryTabScaffold.swift | 36 ++--------------- .../NotificationFeedPreviewView.swift | 4 ++ .../NotificationFeedView.swift | 39 ++++++++++++------- .../WorkspaceShellView.swift | 10 +++++ 4 files changed, 42 insertions(+), 47 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 842ce53a901e..c4489760b1ac 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -142,18 +142,15 @@ struct MobilePrimaryTabScaffold< ) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + // The overlay owns only the content region. Leaving the + // bottom hit area open keeps the system tab bar's + // size-aware hit testing intact. .contentShape(TopContentHitRegion(bottomInset: iOS26TabBarInteractionHeight)) } .onChange(of: selection, initial: true) { _, selection in searchCoordinator.synchronizeSelection(selection) } - // The persistent content layer sits above the tab bar's - // transparent content host, so route the tab bar's hit area - // back to the same selection binding while leaving its native - // visuals intact. - tabBarInteractionOverlay - if selection == .workspaces, let taskComposerAction { TaskComposerButton( action: taskComposerAction, @@ -209,33 +206,6 @@ struct MobilePrimaryTabScaffold< .allowsHitTesting(false) } - private var tabBarInteractionOverlay: some View { - HStack(spacing: 0) { - tabBarButton(for: .workspaces) - .frame(width: 110) - tabBarButton(for: .notifications) - .frame(width: 110) - Spacer(minLength: 0) - tabBarButton(for: .search) - .frame(width: 90) - } - .padding(.horizontal, 45) - .frame(height: iOS26TabBarInteractionHeight) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) - .accessibilityHidden(true) - } - - private func tabBarButton(for tab: MobilePrimaryTab) -> some View { - Button { - tabSelection.wrappedValue = tab - } label: { - Color.clear - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .contentShape(Rectangle()) - .buttonStyle(.plain) - } - private var tabSelection: Binding { Binding( get: { selection }, diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index 2c4c9f6d1818..645925068824 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -130,6 +130,10 @@ public struct NotificationFeedPreviewView: View { } } ) + .notificationFeedMarkAllReadAlert( + isPresented: $isConfirmingMarkAllRead, + markAllRead: { actions.markAllRead() } + ) .background { NotificationFeedSearchProjectionSync( searchCoordinator: primarySearchCoordinator, diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index dee765a802e6..d40f1a2a9911 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -63,31 +63,42 @@ struct NotificationFeedView: View { feed } } - .alert( + .task { + guard refreshesOnAppear else { return } + await actions.refresh() + } + .onChange(of: projection.filter) { _, filter in + actions.filterChanged(filter) + } + .accessibilityIdentifier("MobileNotificationFeed") + } +} + +extension View { + /// Presents the feed's destructive confirmation from the one navigation + /// host that owns the active notification scope. Individual feed views can + /// remain mounted for search and tab navigation without competing to + /// present the same alert. + func notificationFeedMarkAllReadAlert( + isPresented: Binding, + markAllRead: @escaping @MainActor () -> Void + ) -> some View { + alert( L10n.string( "mobile.notificationFeed.markAllRead.confirmTitle", defaultValue: "Mark all notifications as read?" ), - isPresented: $isConfirmingMarkAllRead + isPresented: isPresented ) { Button( L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read"), - role: .destructive - ) { - actions.markAllRead() - } + role: .destructive, + action: markAllRead + ) .accessibilityIdentifier("MobileNotificationFeedMarkAllReadConfirm") Button(L10n.string("mobile.common.cancel", defaultValue: "Cancel"), role: .cancel) {} .accessibilityIdentifier("MobileNotificationFeedMarkAllReadCancel") } - .task { - guard refreshesOnAppear else { return } - await actions.refresh() - } - .onChange(of: projection.filter) { _, filter in - actions.filterChanged(filter) - } - .accessibilityIdentifier("MobileNotificationFeed") } } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index ade8758d68a0..f2516c1789ac 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -424,6 +424,16 @@ struct WorkspaceShellView: View { .onChange(of: presentation.notificationFeedItems, initial: true) { _, items in notificationFeedProjection.update(items: items) } + .notificationFeedMarkAllReadAlert( + isPresented: $isConfirmingNotificationFeedMarkAllRead, + markAllRead: { + Task { + await store.markNotificationFeedItemsRead( + scopedTo: presentation.selectedNotificationFeedMacDeviceIDs + ) + } + } + ) } #endif #else From a81dd26b6ce2be3dca37930d974a8ff3f5f531d2 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:13:16 -0700 Subject: [PATCH 06/88] fix(ios): hide nested compact navigation bars --- .../MobilePrimarySearchNavigationStack.swift | 2 ++ .../CmuxMobileShellUI/NotificationFeedPreviewView.swift | 5 ++++- .../Sources/CmuxMobileShellUI/WorkspaceShellView.swift | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index ddfebf739ddc..d0c4ad07cfbe 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -10,12 +10,14 @@ struct MobilePrimarySearchNavigationStack: View { @Binding var path: [MobileWorkspacePreview.ID] @Binding var selection: MobilePrimaryTab @Bindable var searchCoordinator: MobilePrimarySearchCoordinator + var hidesRootNavigationBar = false @ViewBuilder let root: () -> Root @ViewBuilder let destination: (MobileWorkspacePreview.ID) -> Destination var body: some View { NavigationStack(path: $path) { root() + .toolbar(hidesRootNavigationBar ? .hidden : .automatic, for: .navigationBar) .modifier(MobilePrimarySearchLifecycleModifier( scope: searchCoordinator.scope, update: searchCoordinator.updateLifecycle diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index 4b4b515381f5..ffd29bcc21e8 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -77,6 +77,7 @@ public struct NotificationFeedPreviewView: View { NavigationStack { NotificationFeedPreviewWorkspacesView() } + .toolbar(.hidden, for: .navigationBar) } feed: { Text(verbatim: "Agent feed fixture") .foregroundStyle(.secondary) @@ -97,6 +98,7 @@ public struct NotificationFeedPreviewView: View { .mobileToolbarVisibility(.hidden, for: .tabBar) } } + .toolbar(.hidden, for: .navigationBar) .onAppear { consumePendingSearchNavigation(for: .notifications) } @@ -107,7 +109,8 @@ public struct NotificationFeedPreviewView: View { MobilePrimarySearchNavigationStack( path: $searchNavigationPath, selection: $selectedTab, - searchCoordinator: primarySearchCoordinator + searchCoordinator: primarySearchCoordinator, + hidesRootNavigationBar: true ) { Group { switch primarySearchCoordinator.scope { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index aa6467970f52..2c5916270960 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -508,6 +508,7 @@ struct WorkspaceShellView: View { .mobileToolbarVisibility(.hidden, for: .tabBar) } } + .toolbar(.hidden, for: .navigationBar) } notifications: { NavigationStack(path: $notificationNavigationPath) { NotificationFeedStoreView( @@ -529,6 +530,7 @@ struct WorkspaceShellView: View { .mobileToolbarVisibility(.hidden, for: .tabBar) } } + .toolbar(.hidden, for: .navigationBar) .onAppear { notificationsStackIsOnScreen = true consumePendingPrimarySearchNavigation(for: .notifications) @@ -578,7 +580,8 @@ struct WorkspaceShellView: View { MobilePrimarySearchNavigationStack( path: primarySearchNavigationPath, selection: $selectedPrimaryTab, - searchCoordinator: primarySearchCoordinator + searchCoordinator: primarySearchCoordinator, + hidesRootNavigationBar: true ) { Group { switch primarySearchCoordinator.scope { @@ -875,6 +878,7 @@ struct WorkspaceShellView: View { .background(InteractiveSwipeBackEnabler()) } } + .toolbar(.hidden, for: .navigationBar) .onChange(of: store.selectedWorkspaceID) { _, selectedWorkspaceID in if let createdPath = compactNavigationPolicy.pathForCreatedWorkspaceSelection( currentPath: compactNavigationPath, From ae333b94c8de0c6da223d9fe74a605054e081f87 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:22:48 -0700 Subject: [PATCH 07/88] fix(ios): preserve compact tab state across switches --- .../AgentFeedStoreView.swift | 3 +- .../MobilePrimarySearchNavigationStack.swift | 7 ++- .../MobilePrimaryTabScaffold.swift | 43 ++++++++++--------- .../NotificationFeedPreviewView.swift | 5 +++ .../NotificationFeedStoreView.swift | 3 +- .../NotificationFeedView.swift | 3 +- .../WorkspaceShellView.swift | 25 +++++++++-- 7 files changed, 62 insertions(+), 27 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift index 2dd38e9d1187..f2a7f3a61302 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift @@ -12,6 +12,7 @@ struct AgentFeedStoreView: View { let status: MobileNotificationFeedStatus let pendingReplyRequestIDs: Set let pendingTerminalReplyItemIDs: Set + var isActive = true @State private var showsNavigationFailure = false @Bindable var searchCoordinator: MobilePrimarySearchCoordinator @@ -23,7 +24,7 @@ struct AgentFeedStoreView: View { pendingReplyRequestIDs: pendingReplyRequestIDs, pendingTerminalReplyItemIDs: pendingTerminalReplyItemIDs, failedTerminalReplies: store.agentFeedFailedTerminalReplies, - refreshesOnAppear: true, + refreshesOnAppear: isActive, actions: actions, searchText: searchCoordinator.searchDestinationText(for: .feed) ) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index d0c4ad07cfbe..13d68d2bedab 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -10,6 +10,7 @@ struct MobilePrimarySearchNavigationStack: View { @Binding var path: [MobileWorkspacePreview.ID] @Binding var selection: MobilePrimaryTab @Bindable var searchCoordinator: MobilePrimarySearchCoordinator + var isActive = true var hidesRootNavigationBar = false @ViewBuilder let root: () -> Root @ViewBuilder let destination: (MobileWorkspacePreview.ID) -> Destination @@ -20,12 +21,16 @@ struct MobilePrimarySearchNavigationStack: View { .toolbar(hidesRootNavigationBar ? .hidden : .automatic, for: .navigationBar) .modifier(MobilePrimarySearchLifecycleModifier( scope: searchCoordinator.scope, - update: searchCoordinator.updateLifecycle + update: { scope, isSearching in + guard isActive else { return } + searchCoordinator.updateLifecycle(scope: scope, isSearching: isSearching) + } )) .navigationDestination(for: MobileWorkspacePreview.ID.self, destination: destination) } .searchable(text: searchText, isPresented: searchPresentation, prompt: prompt) .onSubmit(of: .search) { + guard isActive else { return } selection = searchCoordinator.commitSubmit() } .mobileToolbarVisibility(path.isEmpty ? .automatic : .hidden, for: .tabBar) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 69243d015e93..d4d20901a427 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -140,27 +140,30 @@ struct MobilePrimaryTabScaffold< transaction.disablesAnimations = true } .overlay(alignment: .top) { - // Keep the selected navigation stack outside the system - // tab content transition. iOS 26 crossfades a tab's - // hosted NavigationStack before its toolbar items have - // been laid out, which produces a blank top frame. The - // selected stack is swapped synchronously; its bottom - // inset leaves the native tab bar on top. Mounting only - // the selected stack also keeps onAppear/onDisappear and - // feed refresh work scoped to the visible tab. + // Keep every navigation stack mounted outside the system + // tab content transition so tab-local state, scroll positions, + // and pushed paths survive a switch. Only the selected stack + // is visible and interactive. Its bottom inset leaves the + // native tab bar on top. GeometryReader { geometry in - Group { - switch selection { - case .workspaces: - workspaces - case .feed: - feed - case .notifications: - notifications - case .search: - search - .environment(\.mobilePrimarySearchDestination, true) - } + ZStack { + workspaces + .opacity(selection == .workspaces ? 1 : 0) + .allowsHitTesting(selection == .workspaces) + .accessibilityHidden(selection != .workspaces) + feed + .opacity(selection == .feed ? 1 : 0) + .allowsHitTesting(selection == .feed) + .accessibilityHidden(selection != .feed) + notifications + .opacity(selection == .notifications ? 1 : 0) + .allowsHitTesting(selection == .notifications) + .accessibilityHidden(selection != .notifications) + search + .environment(\.mobilePrimarySearchDestination, true) + .opacity(selection == .search ? 1 : 0) + .allowsHitTesting(selection == .search) + .accessibilityHidden(selection != .search) } .frame( width: geometry.size.width, diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index ffd29bcc21e8..b73bf80c02d3 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -110,6 +110,7 @@ public struct NotificationFeedPreviewView: View { path: $searchNavigationPath, selection: $selectedTab, searchCoordinator: primarySearchCoordinator, + isActive: selectedTab == .search, hidesRootNavigationBar: true ) { Group { @@ -124,6 +125,8 @@ public struct NotificationFeedPreviewView: View { projection: projection, refreshesOnAppear: false, actions: actions, + isActive: selectedTab == .search + && primarySearchCoordinator.scope == .notifications, isConfirmingMarkAllRead: $isConfirmingMarkAllRead, showsNavigationToolbar: false ) @@ -194,10 +197,12 @@ public struct NotificationFeedPreviewView: View { projection: projection, refreshesOnAppear: true, actions: actions, + isActive: selectedTab == .notifications, isConfirmingMarkAllRead: $isConfirmingMarkAllRead, showsNavigationToolbar: false ) .task { + guard selectedTab == .notifications else { return } await runScrollStressIfEnabled(proxy: proxy) } } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index f83607bcb5a5..1235c446b78b 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -14,13 +14,14 @@ struct NotificationFeedStoreView: View { let status: MobileNotificationFeedStatus let projection: NotificationFeedProjection let selectedMacDeviceIDs: Set? + var isActive = true var showsNavigationToolbar = true var body: some View { NotificationFeedView( status: status, projection: projection, - refreshesOnAppear: !isSearchDestination, + refreshesOnAppear: isActive && !isSearchDestination, actions: actions, isConfirmingMarkAllRead: $isConfirmingMarkAllRead, showsNavigationToolbar: showsNavigationToolbar diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index d40f1a2a9911..e30caba01f9d 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -21,6 +21,7 @@ struct NotificationFeedView: View { let projection: NotificationFeedProjection let refreshesOnAppear: Bool let actions: NotificationFeedActions + var isActive = true @Binding var isConfirmingMarkAllRead: Bool let showsNavigationToolbar: Bool /// Mark-all-read cannot be undone in one gesture, so the toolbar button @@ -64,7 +65,7 @@ struct NotificationFeedView: View { } } .task { - guard refreshesOnAppear else { return } + guard isActive, refreshesOnAppear else { return } await actions.refresh() } .onChange(of: projection.filter) { _, filter in diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 2c5916270960..c9c0030a5675 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -401,6 +401,8 @@ struct WorkspaceShellView: View { } } .onChange(of: selectedPrimaryTab) { oldValue, newValue in + workspacesStackIsOnScreen = newValue == .workspaces + notificationsStackIsOnScreen = newValue == .notifications store.recordAppEvent( .primaryTabSelected, detail: .primaryTab(diagnosticPrimaryTab(newValue)) @@ -428,6 +430,8 @@ struct WorkspaceShellView: View { consumeDeeplinkNavigationRequestIfNeeded() } .onAppear { + workspacesStackIsOnScreen = selectedPrimaryTab == .workspaces + notificationsStackIsOnScreen = selectedPrimaryTab == .notifications store.recordAppEvent( .primaryTabSelected, detail: .primaryTab(diagnosticPrimaryTab(selectedPrimaryTab)) @@ -497,7 +501,10 @@ struct WorkspaceShellView: View { ) } feed: { NavigationStack(path: $feedNavigationPath) { - agentFeedStoreView(for: presentation) + agentFeedStoreView( + for: presentation, + isActive: selectedPrimaryTab == .feed + ) .navigationDestination(for: MobileWorkspacePreview.ID.self) { workspaceID in workspaceDestination( for: workspaceID, @@ -518,6 +525,7 @@ struct WorkspaceShellView: View { status: presentation.notificationFeedStatus, projection: notificationFeedProjection, selectedMacDeviceIDs: presentation.selectedNotificationFeedMacDeviceIDs, + isActive: selectedPrimaryTab == .notifications, showsNavigationToolbar: false ) .navigationDestination(for: MobileWorkspacePreview.ID.self) { workspaceID in @@ -581,6 +589,7 @@ struct WorkspaceShellView: View { path: primarySearchNavigationPath, selection: $selectedPrimaryTab, searchCoordinator: primarySearchCoordinator, + isActive: selectedPrimaryTab == .search, hidesRootNavigationBar: true ) { Group { @@ -601,7 +610,11 @@ struct WorkspaceShellView: View { ) } case .feed: - agentFeedStoreView(for: presentation) + agentFeedStoreView( + for: presentation, + isActive: selectedPrimaryTab == .search + && primarySearchCoordinator.scope == .feed + ) case .notifications: NotificationFeedStoreView( store: store, @@ -610,6 +623,8 @@ struct WorkspaceShellView: View { status: presentation.notificationFeedStatus, projection: notificationFeedProjection, selectedMacDeviceIDs: presentation.selectedNotificationFeedMacDeviceIDs, + isActive: selectedPrimaryTab == .search + && primarySearchCoordinator.scope == .notifications, showsNavigationToolbar: false ) } @@ -1536,13 +1551,17 @@ struct WorkspaceShellView: View { ) } - private func agentFeedStoreView(for presentation: WorkspaceShellRenderPresentation) -> AgentFeedStoreView { + private func agentFeedStoreView( + for presentation: WorkspaceShellRenderPresentation, + isActive: Bool = true + ) -> AgentFeedStoreView { AgentFeedStoreView( store: store, items: presentation.agentFeedItems, status: presentation.agentFeedStatus, pendingReplyRequestIDs: presentation.agentFeedPendingReplyRequestIDs, pendingTerminalReplyItemIDs: presentation.agentFeedPendingTerminalReplyItemIDs, + isActive: isActive, searchCoordinator: primarySearchCoordinator ) } From 6ff89ad8bf9dc933bc13cb535274b47b8d6febbb Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:34:00 -0700 Subject: [PATCH 08/88] fix(ios): refresh active notification feed copies --- .../Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift | 1 + .../Sources/CmuxMobileShellUI/NotificationFeedView.swift | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index 1235c446b78b..e0574f7c8170 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -23,6 +23,7 @@ struct NotificationFeedStoreView: View { projection: projection, refreshesOnAppear: isActive && !isSearchDestination, actions: actions, + isActive: isActive, isConfirmingMarkAllRead: $isConfirmingMarkAllRead, showsNavigationToolbar: showsNavigationToolbar ) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index e30caba01f9d..4da25a5663ff 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -64,11 +64,12 @@ struct NotificationFeedView: View { feed } } - .task { + .task(id: isActive) { guard isActive, refreshesOnAppear else { return } await actions.refresh() } .onChange(of: projection.filter) { _, filter in + guard isActive else { return } actions.filterChanged(filter) } .accessibilityIdentifier("MobileNotificationFeed") From fafde819da2813c25d69b5b3b636bdebde4aef9d Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:43:12 -0700 Subject: [PATCH 09/88] fix(ios): gate inactive notification lifecycle --- .../CmuxMobileShellUI/NotificationFeedStoreView.swift | 10 ++++++++-- .../Sources/CmuxMobileShellUI/WorkspaceShellView.swift | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index e0574f7c8170..152282339b17 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -27,10 +27,16 @@ struct NotificationFeedStoreView: View { isConfirmingMarkAllRead: $isConfirmingMarkAllRead, showsNavigationToolbar: showsNavigationToolbar ) - .onAppear { - store.recordAppEvent(.notificationFeedOpened, count: items.count) + .onChange(of: isActive, initial: true) { wasActive, active in + if active { + store.recordAppEvent(.notificationFeedOpened, count: items.count) + } else if wasActive { + store.cancelPendingNotificationFeedOpen() + store.recordAppEvent(.notificationFeedClosed) + } } .onDisappear { + guard isActive else { return } store.cancelPendingNotificationFeedOpen() store.recordAppEvent(.notificationFeedClosed) } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index c9c0030a5675..8aad3b6febc0 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -403,6 +403,9 @@ struct WorkspaceShellView: View { .onChange(of: selectedPrimaryTab) { oldValue, newValue in workspacesStackIsOnScreen = newValue == .workspaces notificationsStackIsOnScreen = newValue == .notifications + if newValue == .notifications { + consumePendingPrimarySearchNavigation(for: .notifications) + } store.recordAppEvent( .primaryTabSelected, detail: .primaryTab(diagnosticPrimaryTab(newValue)) @@ -540,10 +543,12 @@ struct WorkspaceShellView: View { } .toolbar(.hidden, for: .navigationBar) .onAppear { + guard selectedPrimaryTab == .notifications else { return } notificationsStackIsOnScreen = true consumePendingPrimarySearchNavigation(for: .notifications) } .onDisappear { + guard selectedPrimaryTab == .notifications else { return } notificationsStackIsOnScreen = false } .onChange(of: pendingPrimarySearchNotificationNavigationID) { _, _ in From 3dd52424abe74bdc6d0e3efdaa9013f6c824a42c Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:49:01 -0700 Subject: [PATCH 10/88] fix(ios): refresh agent feed on activation --- .../Sources/CmuxMobileShellUI/AgentFeedStoreView.swift | 10 ++++++++-- .../Sources/CmuxMobileShellUI/AgentFeedView.swift | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift index f2a7f3a61302..fe31bb9d2161 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift @@ -25,13 +25,19 @@ struct AgentFeedStoreView: View { pendingTerminalReplyItemIDs: pendingTerminalReplyItemIDs, failedTerminalReplies: store.agentFeedFailedTerminalReplies, refreshesOnAppear: isActive, + isActive: isActive, actions: actions, searchText: searchCoordinator.searchDestinationText(for: .feed) ) - .onAppear { - store.recordAppEvent(.agentFeedOpened, count: items.count) + .onChange(of: isActive, initial: true) { wasActive, active in + if active { + store.recordAppEvent(.agentFeedOpened, count: items.count) + } else if wasActive { + store.recordAppEvent(.agentFeedClosed) + } } .onDisappear { + guard isActive else { return } store.recordAppEvent(.agentFeedClosed) } .alert(String(localized: "mobile.agentFeed.openFailed.title", defaultValue: "Couldn’t open event", bundle: .module), diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift index 1f5cabc52512..4566c4a4aa6f 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift @@ -18,6 +18,7 @@ struct AgentFeedView: View { let pendingTerminalReplyItemIDs: Set var failedTerminalReplies: [MobileAgentFeedItemID: MobileAgentFeedFailedReply] = [:] let refreshesOnAppear: Bool + var isActive = true let actions: AgentFeedActions var searchText: String = "" @Environment(MobileDisplaySettings.self) private var displaySettings @@ -34,6 +35,7 @@ struct AgentFeedView: View { pendingTerminalReplyItemIDs: Set, failedTerminalReplies: [MobileAgentFeedItemID: MobileAgentFeedFailedReply] = [:], refreshesOnAppear: Bool, + isActive: Bool = true, actions: AgentFeedActions, searchText: String = "" ) { @@ -43,6 +45,7 @@ struct AgentFeedView: View { self.pendingTerminalReplyItemIDs = pendingTerminalReplyItemIDs self.failedTerminalReplies = failedTerminalReplies self.refreshesOnAppear = refreshesOnAppear + self.isActive = isActive self.actions = actions self.searchText = searchText _preparedRows = State(initialValue: items.map(AgentFeedRowModel.init)) @@ -129,8 +132,10 @@ struct AgentFeedView: View { } .onAppear { now = Date() - guard refreshesOnAppear else { return } - Task { await actions.refresh() } + } + .task(id: isActive) { + guard isActive, refreshesOnAppear else { return } + await actions.refresh() } .onChange(of: items) { _, newItems in preparedRows = newItems.map(AgentFeedRowModel.init) From 21786989d60ab6fbfe9b1e3126c4ec18a6e4df29 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:56:21 -0700 Subject: [PATCH 11/88] fix(ios): restore feed lifecycle on navigation --- .../AgentFeedStoreView.swift | 27 ++++++++++++----- .../NotificationFeedStoreView.swift | 30 ++++++++++++------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift index fe31bb9d2161..fdae5de379a1 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift @@ -15,6 +15,7 @@ struct AgentFeedStoreView: View { var isActive = true @State private var showsNavigationFailure = false + @State private var isFeedVisible = false @Bindable var searchCoordinator: MobilePrimarySearchCoordinator var body: some View { @@ -29,16 +30,14 @@ struct AgentFeedStoreView: View { actions: actions, searchText: searchCoordinator.searchDestinationText(for: .feed) ) - .onChange(of: isActive, initial: true) { wasActive, active in - if active { - store.recordAppEvent(.agentFeedOpened, count: items.count) - } else if wasActive { - store.recordAppEvent(.agentFeedClosed) - } + .onAppear { + updateFeedVisibility(isActive) + } + .onChange(of: isActive) { _, active in + updateFeedVisibility(active) } .onDisappear { - guard isActive else { return } - store.recordAppEvent(.agentFeedClosed) + updateFeedVisibility(false) } .alert(String(localized: "mobile.agentFeed.openFailed.title", defaultValue: "Couldn’t open event", bundle: .module), isPresented: $showsNavigationFailure) { @@ -102,5 +101,17 @@ struct AgentFeedStoreView: View { } ) } + + private func updateFeedVisibility(_ active: Bool) { + if active { + guard !isFeedVisible else { return } + isFeedVisible = true + store.recordAppEvent(.agentFeedOpened, count: items.count) + } else { + guard isFeedVisible else { return } + isFeedVisible = false + store.recordAppEvent(.agentFeedClosed) + } + } } #endif diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index 152282339b17..cc61a147e714 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -16,6 +16,7 @@ struct NotificationFeedStoreView: View { let selectedMacDeviceIDs: Set? var isActive = true var showsNavigationToolbar = true + @State private var isFeedVisible = false var body: some View { NotificationFeedView( @@ -27,18 +28,14 @@ struct NotificationFeedStoreView: View { isConfirmingMarkAllRead: $isConfirmingMarkAllRead, showsNavigationToolbar: showsNavigationToolbar ) - .onChange(of: isActive, initial: true) { wasActive, active in - if active { - store.recordAppEvent(.notificationFeedOpened, count: items.count) - } else if wasActive { - store.cancelPendingNotificationFeedOpen() - store.recordAppEvent(.notificationFeedClosed) - } + .onAppear { + updateFeedVisibility(isActive) + } + .onChange(of: isActive) { _, active in + updateFeedVisibility(active) } .onDisappear { - guard isActive else { return } - store.cancelPendingNotificationFeedOpen() - store.recordAppEvent(.notificationFeedClosed) + updateFeedVisibility(false) } } @@ -79,5 +76,18 @@ struct NotificationFeedStoreView: View { } ) } + + private func updateFeedVisibility(_ active: Bool) { + if active { + guard !isFeedVisible else { return } + isFeedVisible = true + store.recordAppEvent(.notificationFeedOpened, count: items.count) + } else { + guard isFeedVisible else { return } + isFeedVisible = false + store.cancelPendingNotificationFeedOpen() + store.recordAppEvent(.notificationFeedClosed) + } + } } #endif From c048f94140771dbe99b8b76da44f12b4fed9199b Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:07:24 -0700 Subject: [PATCH 12/88] fix(ios): scope tab content to active selection --- .../MobilePrimaryTabScaffold.swift | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index d4d20901a427..ed1d2f1cc866 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -132,38 +132,30 @@ struct MobilePrimaryTabScaffold< .background(Color.clear) .accessibilityIdentifier("MobilePrimaryTabs") .animation(nil, value: selection) - // Each tab owns a NavigationStack. SwiftUI's tab transition - // otherwise crossfades the stacks before the incoming stack's - // toolbar items are installed, leaving a blank frame at the - // top of the screen. .transaction { transaction in transaction.disablesAnimations = true } .overlay(alignment: .top) { - // Keep every navigation stack mounted outside the system - // tab content transition so tab-local state, scroll positions, - // and pushed paths survive a switch. Only the selected stack - // is visible and interactive. Its bottom inset leaves the - // native tab bar on top. + // Keep the selected navigation stack outside the system + // tab content transition. iOS 26 crossfades a tab's + // hosted NavigationStack before its toolbar items have + // been laid out, which produces a blank top frame. Mounting + // only the selected stack keeps feed refresh and navigation + // lifecycle work scoped to the visible tab. Its bottom + // inset leaves the native tab bar on top. GeometryReader { geometry in - ZStack { - workspaces - .opacity(selection == .workspaces ? 1 : 0) - .allowsHitTesting(selection == .workspaces) - .accessibilityHidden(selection != .workspaces) - feed - .opacity(selection == .feed ? 1 : 0) - .allowsHitTesting(selection == .feed) - .accessibilityHidden(selection != .feed) - notifications - .opacity(selection == .notifications ? 1 : 0) - .allowsHitTesting(selection == .notifications) - .accessibilityHidden(selection != .notifications) - search - .environment(\.mobilePrimarySearchDestination, true) - .opacity(selection == .search ? 1 : 0) - .allowsHitTesting(selection == .search) - .accessibilityHidden(selection != .search) + Group { + switch selection { + case .workspaces: + workspaces + case .feed: + feed + case .notifications: + notifications + case .search: + search + .environment(\.mobilePrimarySearchDestination, true) + } } .frame( width: geometry.size.width, From 49e0f44ee37eb4bcee0ec35d690fb2ea0e4a25a0 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:15:10 -0700 Subject: [PATCH 13/88] fix(ios): retain compact task composer --- .../MobilePrimaryTabScaffold.swift | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index ed1d2f1cc866..e398561fb723 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -173,21 +173,8 @@ struct MobilePrimaryTabScaffold< searchCoordinator.synchronizeSelection(selection) } - if selection == .workspaces, let taskComposerAction { - TaskComposerButton( - action: taskComposerAction, - diameter: iOS26BottomControlDiameter - ) - .padding(.trailing, iOS26BottomControlInset) - .padding(.bottom, iOS26TaskComposerBottomPadding) - // Compose anchors to the screen, not the keyboard. The - // only keyboard that can appear while it is visible - // belongs to an overlaying sheet (the composer's - // auto-focused prompt), whose inset dragged the button - // toward mid-screen and stranded it there whenever the - // hide update was missed. - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) - .ignoresSafeArea(.keyboard, edges: .bottom) + if selection == .workspaces { + iOS26TaskComposerButton } } .ignoresSafeArea(.container, edges: .bottom) @@ -226,6 +213,26 @@ struct MobilePrimaryTabScaffold< iOS26BottomControlInset + iOS26BottomControlDiameter + iOS26BottomControlSpacing } + @ViewBuilder + private var iOS26TaskComposerButton: some View { + if let taskComposerAction { + TaskComposerButton( + action: taskComposerAction, + diameter: iOS26BottomControlDiameter + ) + .padding(.trailing, iOS26BottomControlInset) + .padding(.bottom, iOS26TaskComposerBottomPadding) + // Compose anchors to the screen, not the keyboard. The + // only keyboard that can appear while it is visible + // belongs to an overlaying sheet (the composer's + // auto-focused prompt), whose inset dragged the button + // toward mid-screen and stranded it there whenever the + // hide update was missed. + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) + .ignoresSafeArea(.keyboard, edges: .bottom) + } + } + private var iOS26TabBarInteractionHeight: CGFloat { 90 } private var tabBarPlaceholder: some View { From 68cd2bd6dff93b6199872b4859bd58852f918a78 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:20:50 -0700 Subject: [PATCH 14/88] fix(ios): close inactive search lifecycle --- .../MobilePrimarySearchNavigationStack.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index 13d68d2bedab..73be73568158 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -22,7 +22,11 @@ struct MobilePrimarySearchNavigationStack: View { .modifier(MobilePrimarySearchLifecycleModifier( scope: searchCoordinator.scope, update: { scope, isSearching in - guard isActive else { return } + // Inactive stacks must stay quiet while they are + // mounted, but the platform's false callback still + // closes a search session when this stack leaves the + // selected tab. + guard isActive || !isSearching else { return } searchCoordinator.updateLifecycle(scope: scope, isSearching: isSearching) } )) From 13eb0c2de1ad80b1ca55f112843a314f1edb47ac Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:29:13 -0700 Subject: [PATCH 15/88] fix(ios): cancel inactive notification opens --- .../Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index cc61a147e714..b499534d434d 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -32,9 +32,13 @@ struct NotificationFeedStoreView: View { updateFeedVisibility(isActive) } .onChange(of: isActive) { _, active in + if !active { + store.cancelPendingNotificationFeedOpen() + } updateFeedVisibility(active) } .onDisappear { + store.cancelPendingNotificationFeedOpen() updateFeedVisibility(false) } } From 1decc2f96689320378f7973e412e43d8b54217f1 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:41:37 -0700 Subject: [PATCH 16/88] fix(ios): gate inactive tab work --- .../AgentFeedStoreView.swift | 28 ++++++---- .../MobilePrimarySearchNavigationStack.swift | 49 +++++++++++------ .../MobilePrimaryTabScaffold.swift | 52 +++++++++++-------- .../NotificationFeedPreviewView.swift | 16 +++++- .../NotificationFeedStoreView.swift | 24 +++++---- .../NotificationFeedView.swift | 20 ++++--- .../WorkspaceShellView.swift | 25 ++++++--- 7 files changed, 141 insertions(+), 73 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift index fdae5de379a1..674bff315552 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift @@ -19,17 +19,23 @@ struct AgentFeedStoreView: View { @Bindable var searchCoordinator: MobilePrimarySearchCoordinator var body: some View { - AgentFeedView( - items: items, - status: status, - pendingReplyRequestIDs: pendingReplyRequestIDs, - pendingTerminalReplyItemIDs: pendingTerminalReplyItemIDs, - failedTerminalReplies: store.agentFeedFailedTerminalReplies, - refreshesOnAppear: isActive, - isActive: isActive, - actions: actions, - searchText: searchCoordinator.searchDestinationText(for: .feed) - ) + Group { + if isActive { + AgentFeedView( + items: items, + status: status, + pendingReplyRequestIDs: pendingReplyRequestIDs, + pendingTerminalReplyItemIDs: pendingTerminalReplyItemIDs, + failedTerminalReplies: store.agentFeedFailedTerminalReplies, + refreshesOnAppear: true, + isActive: true, + actions: actions, + searchText: searchCoordinator.searchDestinationText(for: .feed) + ) + } else { + Color.clear + } + } .onAppear { updateFeedVisibility(isActive) } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index 73be73568158..37e0175cfec7 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -12,32 +12,37 @@ struct MobilePrimarySearchNavigationStack: View { @Bindable var searchCoordinator: MobilePrimarySearchCoordinator var isActive = true var hidesRootNavigationBar = false + var managesTabBarVisibility = true @ViewBuilder let root: () -> Root @ViewBuilder let destination: (MobileWorkspacePreview.ID) -> Destination var body: some View { NavigationStack(path: $path) { - root() - .toolbar(hidesRootNavigationBar ? .hidden : .automatic, for: .navigationBar) - .modifier(MobilePrimarySearchLifecycleModifier( - scope: searchCoordinator.scope, - update: { scope, isSearching in - // Inactive stacks must stay quiet while they are - // mounted, but the platform's false callback still - // closes a search session when this stack leaves the - // selected tab. - guard isActive || !isSearching else { return } - searchCoordinator.updateLifecycle(scope: scope, isSearching: isSearching) - } - )) - .navigationDestination(for: MobileWorkspacePreview.ID.self, destination: destination) + Group { + if isActive { + root() + .toolbar(hidesRootNavigationBar ? .hidden : .automatic, for: .navigationBar) + .modifier(MobilePrimarySearchLifecycleModifier( + scope: searchCoordinator.scope, + update: { scope, isSearching in + searchCoordinator.updateLifecycle(scope: scope, isSearching: isSearching) + } + )) + } else { + Color.clear + } + } + .navigationDestination(for: MobileWorkspacePreview.ID.self, destination: destination) } .searchable(text: searchText, isPresented: searchPresentation, prompt: prompt) .onSubmit(of: .search) { guard isActive else { return } selection = searchCoordinator.commitSubmit() } - .mobileToolbarVisibility(path.isEmpty ? .automatic : .hidden, for: .tabBar) + .modifier(MobilePrimarySearchTabBarVisibilityModifier( + isEnabled: managesTabBarVisibility, + visibility: path.isEmpty ? .automatic : .hidden + )) } private var searchPresentation: Binding { @@ -73,4 +78,18 @@ struct MobilePrimarySearchNavigationStack: View { } } } + +private struct MobilePrimarySearchTabBarVisibilityModifier: ViewModifier { + let isEnabled: Bool + let visibility: Visibility + + @ViewBuilder + func body(content: Content) -> some View { + if isEnabled { + content.mobileToolbarVisibility(visibility, for: .tabBar) + } else { + content + } + } +} #endif diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index e398561fb723..47ec8b83c29a 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -24,15 +24,18 @@ struct MobilePrimaryTabNavigationHost: V let content: Content let toolbar: Toolbar let toolbarVisibility: Visibility + let tabBarVisibility: Visibility init( toolbarVisibility: Visibility, + tabBarVisibility: Visibility = .automatic, @ToolbarContentBuilder toolbar: () -> Toolbar, @ViewBuilder content: () -> Content ) { self.content = content() self.toolbar = toolbar() self.toolbarVisibility = toolbarVisibility + self.tabBarVisibility = tabBarVisibility } var body: some View { @@ -42,6 +45,7 @@ struct MobilePrimaryTabNavigationHost: V toolbar } .toolbar(toolbarVisibility, for: .navigationBar) + .mobileToolbarVisibility(tabBarVisibility, for: .tabBar) } } } @@ -136,38 +140,44 @@ struct MobilePrimaryTabScaffold< transaction.disablesAnimations = true } .overlay(alignment: .top) { - // Keep the selected navigation stack outside the system - // tab content transition. iOS 26 crossfades a tab's - // hosted NavigationStack before its toolbar items have - // been laid out, which produces a blank top frame. Mounting - // only the selected stack keeps feed refresh and navigation - // lifecycle work scoped to the visible tab. Its bottom + // Keep every navigation stack mounted outside the system + // tab content transition so paths and destination state + // survive a switch. Each stack gates its expensive content + // and lifecycle work on the active selection. The bottom // inset leaves the native tab bar on top. GeometryReader { geometry in - Group { - switch selection { - case .workspaces: - workspaces - case .feed: - feed - case .notifications: - notifications - case .search: - search - .environment(\.mobilePrimarySearchDestination, true) - } + ZStack { + workspaces + .opacity(selection == .workspaces ? 1 : 0) + .allowsHitTesting(selection == .workspaces) + .accessibilityHidden(selection != .workspaces) + feed + .opacity(selection == .feed ? 1 : 0) + .allowsHitTesting(selection == .feed) + .accessibilityHidden(selection != .feed) + notifications + .opacity(selection == .notifications ? 1 : 0) + .allowsHitTesting(selection == .notifications) + .accessibilityHidden(selection != .notifications) + search + .environment(\.mobilePrimarySearchDestination, true) + .opacity(selection == .search ? 1 : 0) + .allowsHitTesting(selection == .search) + .accessibilityHidden(selection != .search) } .frame( width: geometry.size.width, - height: max(0, geometry.size.height - iOS26TabBarInteractionHeight), + height: max(0, geometry.size.height - geometry.safeAreaInsets.bottom), alignment: .top ) + .contentShape( + TopContentHitRegion(bottomInset: geometry.safeAreaInsets.bottom) + ) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) // The overlay owns only the content region. Leaving the // bottom hit area open keeps the system tab bar's // size-aware hit testing intact. - .contentShape(TopContentHitRegion(bottomInset: iOS26TabBarInteractionHeight)) } .onChange(of: selection, initial: true) { _, selection in searchCoordinator.synchronizeSelection(selection) @@ -233,8 +243,6 @@ struct MobilePrimaryTabScaffold< } } - private var iOS26TabBarInteractionHeight: CGFloat { 90 } - private var tabBarPlaceholder: some View { Color.clear .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index b73bf80c02d3..818819ba7fbc 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -44,6 +44,7 @@ public struct NotificationFeedPreviewView: View { GeometryReader { geometry in MobilePrimaryTabNavigationHost( toolbarVisibility: previewRootToolbarVisible ? .visible : .hidden, + tabBarVisibility: previewTabBarVisibility, toolbar: { if previewRootToolbarVisible { WorkspaceRootToolbarContent( @@ -95,7 +96,6 @@ public struct NotificationFeedPreviewView: View { ) ) .toolbar(.visible, for: .navigationBar) - .mobileToolbarVisibility(.hidden, for: .tabBar) } } .toolbar(.hidden, for: .navigationBar) @@ -111,7 +111,8 @@ public struct NotificationFeedPreviewView: View { selection: $selectedTab, searchCoordinator: primarySearchCoordinator, isActive: selectedTab == .search, - hidesRootNavigationBar: true + hidesRootNavigationBar: true, + managesTabBarVisibility: false ) { Group { switch primarySearchCoordinator.scope { @@ -190,6 +191,17 @@ public struct NotificationFeedPreviewView: View { } } + private var previewTabBarVisibility: Visibility { + switch selectedTab { + case .workspaces, .feed: + .automatic + case .notifications: + notificationRoute == nil ? .automatic : .hidden + case .search: + searchNavigationPath.isEmpty ? .automatic : .hidden + } + } + /// The notifications-tab feed with the optional profiling scroll driver. private func notificationsTabFeed(proxy: ScrollViewProxy) -> some View { NotificationFeedView( diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index b499534d434d..6673eb05fd05 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -19,15 +19,21 @@ struct NotificationFeedStoreView: View { @State private var isFeedVisible = false var body: some View { - NotificationFeedView( - status: status, - projection: projection, - refreshesOnAppear: isActive && !isSearchDestination, - actions: actions, - isActive: isActive, - isConfirmingMarkAllRead: $isConfirmingMarkAllRead, - showsNavigationToolbar: showsNavigationToolbar - ) + Group { + if isActive { + NotificationFeedView( + status: status, + projection: projection, + refreshesOnAppear: !isSearchDestination, + actions: actions, + isActive: true, + isConfirmingMarkAllRead: $isConfirmingMarkAllRead, + showsNavigationToolbar: showsNavigationToolbar + ) + } else { + Color.clear + } + } .onAppear { updateFeedVisibility(isActive) } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index 4da25a5663ff..79c186330780 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -53,15 +53,21 @@ struct NotificationFeedView: View { .mobileInlineNavigationTitle() Group { - if showsNavigationToolbar { - feed.toolbar { - NotificationFeedToolbarContent( - projection: projection, - requestMarkAllRead: { isConfirmingMarkAllRead = true } - ) + if isActive { + Group { + if showsNavigationToolbar { + feed.toolbar { + NotificationFeedToolbarContent( + projection: projection, + requestMarkAllRead: { isConfirmingMarkAllRead = true } + ) + } + } else { + feed + } } } else { - feed + Color.clear } } .task(id: isActive) { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 8aad3b6febc0..017c669c8e0c 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -320,6 +320,10 @@ struct WorkspaceShellView: View { return primarySearchNavigationPath.wrappedValue.isEmpty } } + + private var compactTabBarVisibility: Visibility { + compactRootToolbarVisible ? .automatic : .hidden + } #endif private var listConnectionStatus: MobileMacConnectionStatus { @@ -474,6 +478,7 @@ struct WorkspaceShellView: View { private func compactScaffold(presentation: WorkspaceShellRenderPresentation) -> some View { MobilePrimaryTabNavigationHost( toolbarVisibility: compactRootToolbarVisible ? .visible : .hidden, + tabBarVisibility: compactTabBarVisibility, toolbar: { if compactRootToolbarVisible { rootToolbarContent @@ -500,7 +505,8 @@ struct WorkspaceShellView: View { : taskComposerAction ) { workspaceTabContent( - presentation: presentation + presentation: presentation, + isActive: selectedPrimaryTab == .workspaces ) } feed: { NavigationStack(path: $feedNavigationPath) { @@ -515,7 +521,6 @@ struct WorkspaceShellView: View { canCreateWorkspaceForSelection: presentation.canCreateWorkspaceForSelection ) .toolbar(.visible, for: .navigationBar) - .mobileToolbarVisibility(.hidden, for: .tabBar) } } .toolbar(.hidden, for: .navigationBar) @@ -538,7 +543,6 @@ struct WorkspaceShellView: View { canCreateWorkspaceForSelection: presentation.canCreateWorkspaceForSelection ) .toolbar(.visible, for: .navigationBar) - .mobileToolbarVisibility(.hidden, for: .tabBar) } } .toolbar(.hidden, for: .navigationBar) @@ -564,10 +568,16 @@ struct WorkspaceShellView: View { #if os(iOS) private func workspaceTabContent( - presentation: WorkspaceShellRenderPresentation + presentation: WorkspaceShellRenderPresentation, + isActive: Bool = true ) -> some View { workspaceActionToastOverlay { - layoutContent(presentation: presentation) + if isActive { + layoutContent(presentation: presentation) + } else { + Color.clear + .frame(maxWidth: .infinity, maxHeight: .infinity) + } } } #else @@ -595,7 +605,8 @@ struct WorkspaceShellView: View { selection: $selectedPrimaryTab, searchCoordinator: primarySearchCoordinator, isActive: selectedPrimaryTab == .search, - hidesRootNavigationBar: true + hidesRootNavigationBar: true, + managesTabBarVisibility: false ) { Group { switch primarySearchCoordinator.scope { @@ -886,7 +897,7 @@ struct WorkspaceShellView: View { ) ) #if os(iOS) - .mobileToolbarVisibility(.hidden, for: .tabBar, .bottomBar) + .mobileToolbarVisibility(.hidden, for: .bottomBar) #endif .toolbar(.visible, for: .navigationBar) // Only on the pushed compact stack (where a back button From 13338801f158ca4495bb80f68f38f2db38273e35 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:50:33 -0700 Subject: [PATCH 17/88] fix(ios): bound feed activation refreshes --- .../CmuxMobileShellUI/AgentFeedView.swift | 26 ++++++++++++++++--- .../NotificationFeedView.swift | 21 +++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift index 4566c4a4aa6f..d6b9e43ffb0f 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift @@ -27,6 +27,7 @@ struct AgentFeedView: View { @State private var now = Date() @State private var composeContext: AgentFeedComposeContext? @State private var readingItem: MobileAgentFeedItem? + @State private var refreshTimedOut = false init( items: [MobileAgentFeedItem], @@ -98,7 +99,7 @@ struct AgentFeedView: View { var body: some View { Group { - switch status { + switch refreshTimedOut ? .unavailable : status { case .idle, .loading where items.isEmpty: ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -135,18 +136,35 @@ struct AgentFeedView: View { } .task(id: isActive) { guard isActive, refreshesOnAppear else { return } - await actions.refresh() + refreshTimedOut = !(await refreshWithTimeout()) } .onChange(of: items) { _, newItems in preparedRows = newItems.map(AgentFeedRowModel.init) } } + private func refreshWithTimeout() async -> Bool { + await withTaskGroup(of: Bool.self) { group in + group.addTask { + await actions.refresh() + return true + } + group.addTask { + try? await Task.sleep(for: .seconds(15)) + return false + } + let completed = await group.next() ?? false + group.cancelAll() + return completed + } + } + private var feedList: some View { List { - if !items.isEmpty, status == .unavailable || status == .requiresMacUpdate { + let effectiveStatus = refreshTimedOut ? MobileNotificationFeedStatus.unavailable : status + if !items.isEmpty, effectiveStatus == .unavailable || effectiveStatus == .requiresMacUpdate { Section { - AgentFeedAvailabilityBanner(status: status) + AgentFeedAvailabilityBanner(status: effectiveStatus) } } Section { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index 79c186330780..b445dec84ac9 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -24,6 +24,7 @@ struct NotificationFeedView: View { var isActive = true @Binding var isConfirmingMarkAllRead: Bool let showsNavigationToolbar: Bool + @State private var refreshTimedOut = false /// Mark-all-read cannot be undone in one gesture, so the toolbar button /// only arms this confirmation instead of mutating directly. @@ -39,7 +40,7 @@ struct NotificationFeedView: View { hasMoreRows: projection.hasMoreRows, filter: projection.filter, hasSearchQuery: !projection.searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, - status: status, + status: refreshTimedOut ? .unavailable : status, actions: actions, toggleGroup: { projection.toggleGroup($0) }, loadMoreRows: { @@ -72,7 +73,7 @@ struct NotificationFeedView: View { } .task(id: isActive) { guard isActive, refreshesOnAppear else { return } - await actions.refresh() + refreshTimedOut = !(await refreshWithTimeout()) } .onChange(of: projection.filter) { _, filter in guard isActive else { return } @@ -80,6 +81,22 @@ struct NotificationFeedView: View { } .accessibilityIdentifier("MobileNotificationFeed") } + + private func refreshWithTimeout() async -> Bool { + await withTaskGroup(of: Bool.self) { group in + group.addTask { + await actions.refresh() + return true + } + group.addTask { + try? await Task.sleep(for: .seconds(15)) + return false + } + let completed = await group.next() ?? false + group.cancelAll() + return completed + } + } } extension View { From ab5e65d36daeab83e24c4febe3afed974c8cbac8 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:56:42 -0700 Subject: [PATCH 18/88] fix(ios): bound feed refresh recovery --- .../CmuxMobileShell/MobileShellComposite+AgentFeed.swift | 6 +++++- .../MobileShellComposite+NotificationFeed.swift | 6 +++++- .../Sources/CmuxMobileShellUI/AgentFeedView.swift | 6 ++++++ .../CmuxMobileShellUI/MobilePrimaryTabScaffold.swift | 2 +- .../Sources/CmuxMobileShellUI/NotificationFeedView.swift | 6 ++++++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift b/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift index 309c2424d391..b3fe918010e1 100644 --- a/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift +++ b/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift @@ -15,6 +15,7 @@ nonisolated private let mobileShellAgentFeedPrimaryTextByteLimit = 8_192 nonisolated private let mobileShellAgentFeedSecondaryTextByteLimit = 2_048 nonisolated private let mobileShellAgentFeedMetadataByteLimit = 512 nonisolated private let mobileShellAgentFeedMaxItemCount = 400 +nonisolated private let mobileShellAgentFeedRequestTimeoutNanoseconds: UInt64 = 15_000_000_000 private struct AgentFeedTextPage: Decodable { let text: String @@ -311,7 +312,10 @@ extension MobileShellComposite { method: "feed.list", params: [:] ) - let data = try await client.sendRequest(request) + let data = try await client.sendRequest( + request, + timeoutNanoseconds: mobileShellAgentFeedRequestTimeoutNanoseconds + ) let response = try MobileAgentFeedListResponse.decode(data) guard !Task.isCancelled, agentFeedClient(for: macDeviceID) === client else { return } diff --git a/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationFeed.swift b/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationFeed.swift index 9f425dbffc30..5b91d3ee0bc5 100644 --- a/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationFeed.swift +++ b/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationFeed.swift @@ -16,6 +16,7 @@ nonisolated private let mobileShellNotificationFeedSubtitleByteLimit = 512 nonisolated private let mobileShellNotificationFeedBodyByteLimit = 2_048 nonisolated private let mobileShellNotificationFeedMetadataByteLimit = 512 nonisolated private let mobileShellNotificationFeedMaximumImmediateRefreshAttempts = 2 +nonisolated private let mobileShellNotificationFeedRequestTimeoutNanoseconds: UInt64 = 15_000_000_000 @MainActor extension MobileShellComposite { @@ -939,7 +940,10 @@ extension MobileShellComposite { method: "notification.feed.list", params: [:] ) - let data = try await client.sendRequest(request) + let data = try await client.sendRequest( + request, + timeoutNanoseconds: mobileShellNotificationFeedRequestTimeoutNanoseconds + ) let stringLimits = mobileShellNotificationFeedListStringLimits() let maxNotifications = MobileNotificationFeedAggregation.maxItemCount let decoderTask = Task.detached(priority: .userInitiated) { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift index d6b9e43ffb0f..f5793edc8130 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift @@ -136,11 +136,17 @@ struct AgentFeedView: View { } .task(id: isActive) { guard isActive, refreshesOnAppear else { return } + refreshTimedOut = false refreshTimedOut = !(await refreshWithTimeout()) } .onChange(of: items) { _, newItems in preparedRows = newItems.map(AgentFeedRowModel.init) } + .onChange(of: status) { _, status in + if status == .ready { + refreshTimedOut = false + } + } } private func refreshWithTimeout() async -> Bool { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 47ec8b83c29a..16b69cd1a381 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -171,7 +171,7 @@ struct MobilePrimaryTabScaffold< alignment: .top ) .contentShape( - TopContentHitRegion(bottomInset: geometry.safeAreaInsets.bottom) + TopContentHitRegion(bottomInset: 0) ) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index b445dec84ac9..523ac6d71dc3 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -73,12 +73,18 @@ struct NotificationFeedView: View { } .task(id: isActive) { guard isActive, refreshesOnAppear else { return } + refreshTimedOut = false refreshTimedOut = !(await refreshWithTimeout()) } .onChange(of: projection.filter) { _, filter in guard isActive else { return } actions.filterChanged(filter) } + .onChange(of: status) { _, status in + if status == .ready { + refreshTimedOut = false + } + } .accessibilityIdentifier("MobileNotificationFeed") } From 7755b0d543a54e991b4ecb35319b6cc92187b3ce Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:03:54 -0700 Subject: [PATCH 19/88] fix(ios): rely on bounded feed requests --- .../CmuxMobileShellUI/AgentFeedView.swift | 32 +++---------------- .../NotificationFeedView.swift | 27 ++-------------- 2 files changed, 6 insertions(+), 53 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift index f5793edc8130..4566c4a4aa6f 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift @@ -27,7 +27,6 @@ struct AgentFeedView: View { @State private var now = Date() @State private var composeContext: AgentFeedComposeContext? @State private var readingItem: MobileAgentFeedItem? - @State private var refreshTimedOut = false init( items: [MobileAgentFeedItem], @@ -99,7 +98,7 @@ struct AgentFeedView: View { var body: some View { Group { - switch refreshTimedOut ? .unavailable : status { + switch status { case .idle, .loading where items.isEmpty: ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -136,41 +135,18 @@ struct AgentFeedView: View { } .task(id: isActive) { guard isActive, refreshesOnAppear else { return } - refreshTimedOut = false - refreshTimedOut = !(await refreshWithTimeout()) + await actions.refresh() } .onChange(of: items) { _, newItems in preparedRows = newItems.map(AgentFeedRowModel.init) } - .onChange(of: status) { _, status in - if status == .ready { - refreshTimedOut = false - } - } - } - - private func refreshWithTimeout() async -> Bool { - await withTaskGroup(of: Bool.self) { group in - group.addTask { - await actions.refresh() - return true - } - group.addTask { - try? await Task.sleep(for: .seconds(15)) - return false - } - let completed = await group.next() ?? false - group.cancelAll() - return completed - } } private var feedList: some View { List { - let effectiveStatus = refreshTimedOut ? MobileNotificationFeedStatus.unavailable : status - if !items.isEmpty, effectiveStatus == .unavailable || effectiveStatus == .requiresMacUpdate { + if !items.isEmpty, status == .unavailable || status == .requiresMacUpdate { Section { - AgentFeedAvailabilityBanner(status: effectiveStatus) + AgentFeedAvailabilityBanner(status: status) } } Section { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index 523ac6d71dc3..79c186330780 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -24,7 +24,6 @@ struct NotificationFeedView: View { var isActive = true @Binding var isConfirmingMarkAllRead: Bool let showsNavigationToolbar: Bool - @State private var refreshTimedOut = false /// Mark-all-read cannot be undone in one gesture, so the toolbar button /// only arms this confirmation instead of mutating directly. @@ -40,7 +39,7 @@ struct NotificationFeedView: View { hasMoreRows: projection.hasMoreRows, filter: projection.filter, hasSearchQuery: !projection.searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, - status: refreshTimedOut ? .unavailable : status, + status: status, actions: actions, toggleGroup: { projection.toggleGroup($0) }, loadMoreRows: { @@ -73,36 +72,14 @@ struct NotificationFeedView: View { } .task(id: isActive) { guard isActive, refreshesOnAppear else { return } - refreshTimedOut = false - refreshTimedOut = !(await refreshWithTimeout()) + await actions.refresh() } .onChange(of: projection.filter) { _, filter in guard isActive else { return } actions.filterChanged(filter) } - .onChange(of: status) { _, status in - if status == .ready { - refreshTimedOut = false - } - } .accessibilityIdentifier("MobileNotificationFeed") } - - private func refreshWithTimeout() async -> Bool { - await withTaskGroup(of: Bool.self) { group in - group.addTask { - await actions.refresh() - return true - } - group.addTask { - try? await Task.sleep(for: .seconds(15)) - return false - } - let completed = await group.next() ?? false - group.cancelAll() - return completed - } - } } extension View { From 1c8bbb3a6bb474f6e594edf92d3dc51217525391 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:11:14 -0700 Subject: [PATCH 20/88] fix(ios): keep tab bar hit region clear --- .../Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 16b69cd1a381..5eab9e348d1d 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -167,11 +167,11 @@ struct MobilePrimaryTabScaffold< } .frame( width: geometry.size.width, - height: max(0, geometry.size.height - geometry.safeAreaInsets.bottom), + height: geometry.size.height, alignment: .top ) .contentShape( - TopContentHitRegion(bottomInset: 0) + TopContentHitRegion(bottomInset: geometry.safeAreaInsets.bottom) ) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) From 00cba5a42f968b2b49911aa08cd11e8dc6040452 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:17:09 -0700 Subject: [PATCH 21/88] fix(ios): preserve tab bar hit testing --- .../Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 5eab9e348d1d..a8bbc3adcaa8 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -187,7 +187,6 @@ struct MobilePrimaryTabScaffold< iOS26TaskComposerButton } } - .ignoresSafeArea(.container, edges: .bottom) } else if #available(iOS 18.0, *) { TabView(selection: $selection) { primaryTabs From 577bd6f87a083123f98c9015b585f05011a207c7 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:26:02 -0700 Subject: [PATCH 22/88] fix(ios): restart notification preview on selection --- .../CmuxMobileShellUI/NotificationFeedPreviewView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index 818819ba7fbc..6b6d080480d2 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -159,6 +159,9 @@ public struct NotificationFeedPreviewView: View { consumePendingSearchNavigation(for: selectedTab) } .onChange(of: selectedTab) { oldValue, newValue in + if newValue == .notifications { + consumePendingSearchNavigation(for: .notifications) + } if oldValue == .search, newValue != .search { searchNavigationPath = [] } @@ -213,7 +216,7 @@ public struct NotificationFeedPreviewView: View { isConfirmingMarkAllRead: $isConfirmingMarkAllRead, showsNavigationToolbar: false ) - .task { + .task(id: selectedTab) { guard selectedTab == .notifications else { return } await runScrollStressIfEnabled(proxy: proxy) } From 88b7da73281d5e208f8c509dcc9d2b48bd4c6fc1 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:32:52 -0700 Subject: [PATCH 23/88] fix(ios): keep search host in native search tab --- .../MobilePrimaryTabScaffold.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index a8bbc3adcaa8..30c501ae8177 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -126,7 +126,13 @@ struct MobilePrimaryTabScaffold< if selection == .search || selection.searchScope != nil { Tab(value: MobilePrimaryTab.search, role: .search) { - tabBarPlaceholder + // The searchable navigation stack must remain in + // the native search-role tab. iOS uses the tab's + // content to attach search activation, so keeping + // it in the overlay would leave the Search control + // without a searchable presentation. + search + .environment(\.mobilePrimarySearchDestination, true) } .accessibilityIdentifier("MobilePrimaryTabSearch") } @@ -159,11 +165,6 @@ struct MobilePrimaryTabScaffold< .opacity(selection == .notifications ? 1 : 0) .allowsHitTesting(selection == .notifications) .accessibilityHidden(selection != .notifications) - search - .environment(\.mobilePrimarySearchDestination, true) - .opacity(selection == .search ? 1 : 0) - .allowsHitTesting(selection == .search) - .accessibilityHidden(selection != .search) } .frame( width: geometry.size.width, @@ -175,6 +176,7 @@ struct MobilePrimaryTabScaffold< ) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .allowsHitTesting(selection != .search) // The overlay owns only the content region. Leaving the // bottom hit area open keeps the system tab bar's // size-aware hit testing intact. From f8c324c603fcbc33b922197af93e5c479b42bb97 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:38:51 -0700 Subject: [PATCH 24/88] fix(ios): keep tab controls above content layer --- .../MobilePrimaryTabScaffold.swift | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 30c501ae8177..d09a6914efcd 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -2,21 +2,6 @@ import CmuxMobileSupport import SwiftUI -private struct TopContentHitRegion: Shape { - let bottomInset: CGFloat - - func path(in rect: CGRect) -> Path { - Path( - CGRect( - x: rect.minX, - y: rect.minY, - width: rect.width, - height: max(0, rect.height - bottomInset) - ) - ) - } -} - /// Keeps the compact tab bar and its root navigation chrome in one hierarchy. /// The tab contents still keep their own navigation paths, but they no longer /// compete to install the root toolbar as selection changes. @@ -145,12 +130,12 @@ struct MobilePrimaryTabScaffold< .transaction { transaction in transaction.disablesAnimations = true } - .overlay(alignment: .top) { - // Keep every navigation stack mounted outside the system - // tab content transition so paths and destination state - // survive a switch. Each stack gates its expensive content - // and lifecycle work on the active selection. The bottom - // inset leaves the native tab bar on top. + .background(alignment: .top) { + // Keep every non-search navigation stack mounted behind + // the native tab content transition so paths and + // destination state survive a switch. The TabView and its + // native tab bar stay above this layer, so the system + // controls remain the top hit-test target. GeometryReader { geometry in ZStack { workspaces @@ -171,15 +156,9 @@ struct MobilePrimaryTabScaffold< height: geometry.size.height, alignment: .top ) - .contentShape( - TopContentHitRegion(bottomInset: geometry.safeAreaInsets.bottom) - ) + .allowsHitTesting(selection != .search) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .allowsHitTesting(selection != .search) - // The overlay owns only the content region. Leaving the - // bottom hit area open keeps the system tab bar's - // size-aware hit testing intact. } .onChange(of: selection, initial: true) { _, selection in searchCoordinator.synchronizeSelection(selection) From 2b45d5d6afe62c6579089a4c635d5638a4be2cdf Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:54:30 -0700 Subject: [PATCH 25/88] fix(ios): keep primary tabs in native navigation --- .../AgentFeedStoreView.swift | 28 +++----- .../MobilePrimarySearchNavigationStack.swift | 25 +++---- .../MobilePrimaryTabScaffold.swift | 67 +------------------ .../NotificationFeedStoreView.swift | 24 +++---- .../NotificationFeedView.swift | 20 ++---- .../WorkspaceShellView.swift | 17 +---- 6 files changed, 41 insertions(+), 140 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift index 674bff315552..0e317d22581a 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift @@ -19,23 +19,17 @@ struct AgentFeedStoreView: View { @Bindable var searchCoordinator: MobilePrimarySearchCoordinator var body: some View { - Group { - if isActive { - AgentFeedView( - items: items, - status: status, - pendingReplyRequestIDs: pendingReplyRequestIDs, - pendingTerminalReplyItemIDs: pendingTerminalReplyItemIDs, - failedTerminalReplies: store.agentFeedFailedTerminalReplies, - refreshesOnAppear: true, - isActive: true, - actions: actions, - searchText: searchCoordinator.searchDestinationText(for: .feed) - ) - } else { - Color.clear - } - } + AgentFeedView( + items: items, + status: status, + pendingReplyRequestIDs: pendingReplyRequestIDs, + pendingTerminalReplyItemIDs: pendingTerminalReplyItemIDs, + failedTerminalReplies: store.agentFeedFailedTerminalReplies, + refreshesOnAppear: true, + isActive: isActive, + actions: actions, + searchText: searchCoordinator.searchDestinationText(for: .feed) + ) .onAppear { updateFeedVisibility(isActive) } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index 37e0175cfec7..afbf103318da 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -18,21 +18,16 @@ struct MobilePrimarySearchNavigationStack: View { var body: some View { NavigationStack(path: $path) { - Group { - if isActive { - root() - .toolbar(hidesRootNavigationBar ? .hidden : .automatic, for: .navigationBar) - .modifier(MobilePrimarySearchLifecycleModifier( - scope: searchCoordinator.scope, - update: { scope, isSearching in - searchCoordinator.updateLifecycle(scope: scope, isSearching: isSearching) - } - )) - } else { - Color.clear - } - } - .navigationDestination(for: MobileWorkspacePreview.ID.self, destination: destination) + root() + .toolbar(hidesRootNavigationBar ? .hidden : .automatic, for: .navigationBar) + .modifier(MobilePrimarySearchLifecycleModifier( + scope: searchCoordinator.scope, + update: { scope, isSearching in + guard isActive || !isSearching else { return } + searchCoordinator.updateLifecycle(scope: scope, isSearching: isSearching) + } + )) + .navigationDestination(for: MobileWorkspacePreview.ID.self, destination: destination) } .searchable(text: searchText, isPresented: searchPresentation, prompt: prompt) .onSubmit(of: .search) { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index d09a6914efcd..847c1efd6e75 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -84,38 +84,10 @@ struct MobilePrimaryTabScaffold< if #available(iOS 26.0, *) { ZStack(alignment: .bottomTrailing) { TabView(selection: tabSelection) { - Tab(value: MobilePrimaryTab.workspaces) { - tabBarPlaceholder - } label: { - workspacesLabel - } - .accessibilityIdentifier("MobilePrimaryTabWorkspaces") - - Tab(value: MobilePrimaryTab.feed) { - tabBarPlaceholder - } label: { - feedLabel - } - .badge(feedNeedsInputCount) - .accessibilityIdentifier("MobilePrimaryTabFeed") - - if showsNotificationsTab { - Tab(value: MobilePrimaryTab.notifications) { - tabBarPlaceholder - } label: { - notificationsLabel - } - .badge(notificationUnreadCount) - .accessibilityIdentifier("MobilePrimaryTabNotifications") - } + primaryTabs if selection == .search || selection.searchScope != nil { Tab(value: MobilePrimaryTab.search, role: .search) { - // The searchable navigation stack must remain in - // the native search-role tab. iOS uses the tab's - // content to attach search activation, so keeping - // it in the overlay would leave the Search control - // without a searchable presentation. search .environment(\.mobilePrimarySearchDestination, true) } @@ -124,42 +96,11 @@ struct MobilePrimaryTabScaffold< } .tabViewSearchActivation(.searchTabSelection) .tabViewStyle(.tabBarOnly) - .background(Color.clear) .accessibilityIdentifier("MobilePrimaryTabs") .animation(nil, value: selection) .transaction { transaction in transaction.disablesAnimations = true } - .background(alignment: .top) { - // Keep every non-search navigation stack mounted behind - // the native tab content transition so paths and - // destination state survive a switch. The TabView and its - // native tab bar stay above this layer, so the system - // controls remain the top hit-test target. - GeometryReader { geometry in - ZStack { - workspaces - .opacity(selection == .workspaces ? 1 : 0) - .allowsHitTesting(selection == .workspaces) - .accessibilityHidden(selection != .workspaces) - feed - .opacity(selection == .feed ? 1 : 0) - .allowsHitTesting(selection == .feed) - .accessibilityHidden(selection != .feed) - notifications - .opacity(selection == .notifications ? 1 : 0) - .allowsHitTesting(selection == .notifications) - .accessibilityHidden(selection != .notifications) - } - .frame( - width: geometry.size.width, - height: geometry.size.height, - alignment: .top - ) - .allowsHitTesting(selection != .search) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - } .onChange(of: selection, initial: true) { _, selection in searchCoordinator.synchronizeSelection(selection) } @@ -223,12 +164,6 @@ struct MobilePrimaryTabScaffold< } } - private var tabBarPlaceholder: some View { - Color.clear - .frame(maxWidth: .infinity, maxHeight: .infinity) - .allowsHitTesting(false) - } - private var tabSelection: Binding { Binding( get: { selection }, diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index 6673eb05fd05..3495d8df6261 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -19,21 +19,15 @@ struct NotificationFeedStoreView: View { @State private var isFeedVisible = false var body: some View { - Group { - if isActive { - NotificationFeedView( - status: status, - projection: projection, - refreshesOnAppear: !isSearchDestination, - actions: actions, - isActive: true, - isConfirmingMarkAllRead: $isConfirmingMarkAllRead, - showsNavigationToolbar: showsNavigationToolbar - ) - } else { - Color.clear - } - } + NotificationFeedView( + status: status, + projection: projection, + refreshesOnAppear: !isSearchDestination, + actions: actions, + isActive: isActive, + isConfirmingMarkAllRead: $isConfirmingMarkAllRead, + showsNavigationToolbar: showsNavigationToolbar + ) .onAppear { updateFeedVisibility(isActive) } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index 79c186330780..4da25a5663ff 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -53,21 +53,15 @@ struct NotificationFeedView: View { .mobileInlineNavigationTitle() Group { - if isActive { - Group { - if showsNavigationToolbar { - feed.toolbar { - NotificationFeedToolbarContent( - projection: projection, - requestMarkAllRead: { isConfirmingMarkAllRead = true } - ) - } - } else { - feed - } + if showsNavigationToolbar { + feed.toolbar { + NotificationFeedToolbarContent( + projection: projection, + requestMarkAllRead: { isConfirmingMarkAllRead = true } + ) } } else { - Color.clear + feed } } .task(id: isActive) { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 017c669c8e0c..92838de81725 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -504,10 +504,7 @@ struct WorkspaceShellView: View { ? nil : taskComposerAction ) { - workspaceTabContent( - presentation: presentation, - isActive: selectedPrimaryTab == .workspaces - ) + workspaceTabContent(presentation: presentation) } feed: { NavigationStack(path: $feedNavigationPath) { agentFeedStoreView( @@ -567,17 +564,9 @@ struct WorkspaceShellView: View { #endif #if os(iOS) - private func workspaceTabContent( - presentation: WorkspaceShellRenderPresentation, - isActive: Bool = true - ) -> some View { + private func workspaceTabContent(presentation: WorkspaceShellRenderPresentation) -> some View { workspaceActionToastOverlay { - if isActive { - layoutContent(presentation: presentation) - } else { - Color.clear - .frame(maxWidth: .infinity, maxHeight: .infinity) - } + layoutContent(presentation: presentation) } } #else From 831f1da42bcce55b9fa2837009ac50b8c027b1e0 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:03:41 -0700 Subject: [PATCH 26/88] fix(ios): scope tab switch animation suppression --- .../Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 847c1efd6e75..ab87c13b3d62 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -98,9 +98,6 @@ struct MobilePrimaryTabScaffold< .tabViewStyle(.tabBarOnly) .accessibilityIdentifier("MobilePrimaryTabs") .animation(nil, value: selection) - .transaction { transaction in - transaction.disablesAnimations = true - } .onChange(of: selection, initial: true) { _, selection in searchCoordinator.synchronizeSelection(selection) } From deb45a222328619d1b1b8fb3c4527b5c488dff86 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:44:44 -0700 Subject: [PATCH 27/88] test(ios): probe rendered link position --- .../AgentFeedInlineTextTests.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/AgentFeedInlineTextTests.swift b/Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/AgentFeedInlineTextTests.swift index fd47aa505b4c..ccf88b0dcd90 100644 --- a/Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/AgentFeedInlineTextTests.swift +++ b/Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/AgentFeedInlineTextTests.swift @@ -65,15 +65,19 @@ import UIKit open: {}, openURL: { opened.urls.append($0) }) view.frame = CGRect(x: 0, y: 0, width: 600, height: 60) view.layoutIfNeeded() - let lineY = view.bounds.midY - let linkPoint = try #require(stride(from: 0, to: view.bounds.width, by: 2) - .map { CGPoint(x: $0, y: lineY) } - .first { view.link(at: $0) != nil }) + let linkPoint = try #require( + stride(from: 0, to: view.bounds.height, by: 2) + .flatMap { y in + stride(from: 0, to: view.bounds.width, by: 2) + .map { CGPoint(x: $0, y: y) } + } + .first { view.link(at: $0) != nil } + ) - #expect(view.link(at: CGPoint(x: 1, y: lineY)) == nil) + #expect(view.link(at: CGPoint(x: 1, y: linkPoint.y)) == nil) #expect(view.activateLink(at: linkPoint)) #expect(opened.urls == [URL(string: "https://github.com/manaflow-ai/cmux/pull/14342")!]) - #expect(!view.activateLink(at: CGPoint(x: 1, y: lineY))) + #expect(!view.activateLink(at: CGPoint(x: 1, y: linkPoint.y))) #expect(opened.urls.count == 1) } From 7943e3b427e1c32cd7a9c480f32392692cecb694 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:20:31 -0700 Subject: [PATCH 28/88] fix(ios): preserve search and notification refresh paths --- .../MobilePrimarySearchNavigationStack.swift | 12 +++++++++++- .../NotificationFeedPreviewView.swift | 2 +- .../NotificationFeedStoreView.swift | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index afbf103318da..dca4df53d179 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -19,7 +19,10 @@ struct MobilePrimarySearchNavigationStack: View { var body: some View { NavigationStack(path: $path) { root() - .toolbar(hidesRootNavigationBar ? .hidden : .automatic, for: .navigationBar) + // Before iOS 26, `.searchable` presents from the navigation + // bar. Keep that bar available even when the iOS 26 native + // search tab uses the shared outer toolbar host. + .toolbar(rootNavigationBarVisibility, for: .navigationBar) .modifier(MobilePrimarySearchLifecycleModifier( scope: searchCoordinator.scope, update: { scope, isSearching in @@ -49,6 +52,13 @@ struct MobilePrimarySearchNavigationStack: View { ) } + private var rootNavigationBarVisibility: Visibility { + if #available(iOS 26.0, *), hidesRootNavigationBar { + return .hidden + } + return .automatic + } + private var searchText: Binding { let scope = searchCoordinator.scope let generation = searchCoordinator.activationGeneration diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index 6b6d080480d2..9a83a14c09ac 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -124,7 +124,7 @@ public struct NotificationFeedPreviewView: View { NotificationFeedView( status: .ready, projection: projection, - refreshesOnAppear: false, + refreshesOnAppear: true, actions: actions, isActive: selectedTab == .search && primarySearchCoordinator.scope == .notifications, diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift index 3495d8df6261..752c359c9503 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedStoreView.swift @@ -22,7 +22,9 @@ struct NotificationFeedStoreView: View { NotificationFeedView( status: status, projection: projection, - refreshesOnAppear: !isSearchDestination, + // The mounted tab and the active search destination both use the + // active gate below, so only the visible owner refreshes. + refreshesOnAppear: true, actions: actions, isActive: isActive, isConfirmingMarkAllRead: $isConfirmingMarkAllRead, From 58ce74c2e48e25e1dae528c7b06098a5ea5b44b1 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:46:02 -0700 Subject: [PATCH 29/88] fix(ios): close tab review gaps --- .../MobileShellComposite+AgentFeed.swift | 3 +- .../CmuxMobileShellUI/AgentFeedView.swift | 4 +++ .../MobilePrimaryTabScaffold.swift | 4 +++ .../WorkspaceShellView.swift | 29 ++++++++++--------- ios/cmuxUITests/cmuxUITests.swift | 11 ++++++- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift b/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift index b3fe918010e1..49ac5dde937b 100644 --- a/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift +++ b/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift @@ -15,7 +15,6 @@ nonisolated private let mobileShellAgentFeedPrimaryTextByteLimit = 8_192 nonisolated private let mobileShellAgentFeedSecondaryTextByteLimit = 2_048 nonisolated private let mobileShellAgentFeedMetadataByteLimit = 512 nonisolated private let mobileShellAgentFeedMaxItemCount = 400 -nonisolated private let mobileShellAgentFeedRequestTimeoutNanoseconds: UInt64 = 15_000_000_000 private struct AgentFeedTextPage: Decodable { let text: String @@ -314,7 +313,7 @@ extension MobileShellComposite { ) let data = try await client.sendRequest( request, - timeoutNanoseconds: mobileShellAgentFeedRequestTimeoutNanoseconds + timeoutNanoseconds: runtime?.rpcRequestTimeoutNanoseconds ) let response = try MobileAgentFeedListResponse.decode(data) guard !Task.isCancelled, diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift index 4566c4a4aa6f..04f455d71e1f 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift @@ -135,6 +135,10 @@ struct AgentFeedView: View { } .task(id: isActive) { guard isActive, refreshesOnAppear else { return } + // Relative timestamps are anchored to the last visible visit, so + // switching away and back cannot leave the feed comparing rows to + // the date from its first appearance. + now = Date() await actions.refresh() } .onChange(of: items) { _, newItems in diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index ab87c13b3d62..4917a2fcbee4 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -106,6 +106,10 @@ struct MobilePrimaryTabScaffold< iOS26TaskComposerButton } } + // The composer padding is calibrated from the screen edge. Keep + // the scaffold's layout space through the bottom container inset + // so the button does not float above its intended position. + .ignoresSafeArea(.container, edges: .bottom) } else if #available(iOS 18.0, *) { TabView(selection: $selection) { primaryTabs diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 92838de81725..4ec48277f984 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -405,11 +405,6 @@ struct WorkspaceShellView: View { } } .onChange(of: selectedPrimaryTab) { oldValue, newValue in - workspacesStackIsOnScreen = newValue == .workspaces - notificationsStackIsOnScreen = newValue == .notifications - if newValue == .notifications { - consumePendingPrimarySearchNavigation(for: .notifications) - } store.recordAppEvent( .primaryTabSelected, detail: .primaryTab(diagnosticPrimaryTab(newValue)) @@ -437,8 +432,6 @@ struct WorkspaceShellView: View { consumeDeeplinkNavigationRequestIfNeeded() } .onAppear { - workspacesStackIsOnScreen = selectedPrimaryTab == .workspaces - notificationsStackIsOnScreen = selectedPrimaryTab == .notifications store.recordAppEvent( .primaryTabSelected, detail: .primaryTab(diagnosticPrimaryTab(selectedPrimaryTab)) @@ -549,7 +542,6 @@ struct WorkspaceShellView: View { consumePendingPrimarySearchNavigation(for: .notifications) } .onDisappear { - guard selectedPrimaryTab == .notifications else { return } notificationsStackIsOnScreen = false } .onChange(of: pendingPrimarySearchNotificationNavigationID) { _, _ in @@ -1670,9 +1662,17 @@ struct WorkspaceShellView: View { pendingPrimarySearchNotificationNavigationID = workspaceID transitionPrimaryTab(to: .notifications) case .mountedNotificationTab: - transitionPrimaryTab(to: .notifications) - if notificationNavigationPath.last != workspaceID { - notificationNavigationPath = [workspaceID] + if selectedPrimaryTab == .notifications, notificationsStackIsOnScreen { + if notificationNavigationPath.last != workspaceID { + notificationNavigationPath = [workspaceID] + } + } else { + // Keep the path write behind the destination stack's + // onAppear whenever it is not currently in the window. + pendingPrimarySearchNotificationNavigationID = workspaceID + if selectedPrimaryTab != .notifications { + transitionPrimaryTab(to: .notifications) + } } } return @@ -1705,7 +1705,10 @@ struct WorkspaceShellView: View { // Compact pushes must wait for the workspaces stack to be in the // window (its onAppear re-runs this); the split layout only writes // the store selection, which is safe at any time. - guard !usesCompactStack || workspacesStackIsOnScreen else { return } + guard + !usesCompactStack + || (selectedPrimaryTab == .workspaces && workspacesStackIsOnScreen) + else { return } guard let workspaceID = pendingPrimarySearchWorkspaceNavigationID else { return } pendingPrimarySearchWorkspaceNavigationID = nil selectWorkspaceImmediately(workspaceID) @@ -1713,7 +1716,7 @@ struct WorkspaceShellView: View { // The Feed has no search-result navigation lane yet. break case .notifications: - guard notificationsStackIsOnScreen else { return } + guard selectedPrimaryTab == .notifications, notificationsStackIsOnScreen else { return } guard let workspaceID = pendingPrimarySearchNotificationNavigationID else { return } pendingPrimarySearchNotificationNavigationID = nil if notificationNavigationPath.last != workspaceID { diff --git a/ios/cmuxUITests/cmuxUITests.swift b/ios/cmuxUITests/cmuxUITests.swift index f9906e2b3a81..7b851cb51fee 100644 --- a/ios/cmuxUITests/cmuxUITests.swift +++ b/ios/cmuxUITests/cmuxUITests.swift @@ -4791,9 +4791,18 @@ final class cmuxUITests: XCTestCase { XCTAssertTrue(waitForHittable(feed, timeout: 3)) XCTAssertTrue(app.buttons["MobileWorkspaceSettingsMenu"].waitForExistence(timeout: 3)) XCTAssertTrue(app.buttons["MobileWorkspaceDevicesButton"].waitForExistence(timeout: 3)) - XCTAssertTrue(app.buttons["MobileWorkspaceMacPicker"].waitForExistence(timeout: 3)) + let picker = app.buttons["MobileWorkspaceMacPicker"] + XCTAssertTrue(picker.waitForExistence(timeout: 3)) XCTAssertTrue(app.buttons["MobileNotificationFeedMarkAllRead"].waitForExistence(timeout: 3)) XCTAssertTrue(app.buttons["MobileNotificationFeedFilterMenu"].waitForExistence(timeout: 3)) + + guard let pickerFrame = waitForUsableFrame(of: picker, timeout: 3), + let feedFrame = waitForUsableFrame(of: feed, timeout: 3) else { + XCTFail("Notifications tab did not settle to usable toolbar and feed frames") + return + } + XCTAssertEqual(pickerFrame.midX, app.frame.midX, accuracy: 2) + XCTAssertTrue(feedFrame.intersects(app.frame)) } /// Drives the production push coordinator through its three user-visible From 3dd39a16d922603ddb79be4857bfe8da45f15c19 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:25:17 -0700 Subject: [PATCH 30/88] test: restore configurable main queue drain helper --- cmuxTests/TabManagerUnitTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmuxTests/TabManagerUnitTests.swift b/cmuxTests/TabManagerUnitTests.swift index a8e1128cf80b..ae00cb22d874 100644 --- a/cmuxTests/TabManagerUnitTests.swift +++ b/cmuxTests/TabManagerUnitTests.swift @@ -21,12 +21,12 @@ import CmuxSettings let lastSurfaceCloseShortcutDefaultsKey = "closeWorkspaceOnLastSurfaceShortcut" -func drainMainQueue() { +func drainMainQueue(timeout: TimeInterval = 1.0) { let expectation = XCTestExpectation(description: "drain main queue") DispatchQueue.main.async { expectation.fulfill() } - XCTWaiter().wait(for: [expectation], timeout: 1.0) + XCTWaiter().wait(for: [expectation], timeout: timeout) } @discardableResult From 4120832acd67174258b8e25463abf0e0cf486aec Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:43:34 -0700 Subject: [PATCH 31/88] fix: bound agent feed refresh requests --- .../CmuxMobileShell/MobileShellComposite+AgentFeed.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift b/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift index 49ac5dde937b..5537ff2812f4 100644 --- a/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift +++ b/Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift @@ -15,6 +15,7 @@ nonisolated private let mobileShellAgentFeedPrimaryTextByteLimit = 8_192 nonisolated private let mobileShellAgentFeedSecondaryTextByteLimit = 2_048 nonisolated private let mobileShellAgentFeedMetadataByteLimit = 512 nonisolated private let mobileShellAgentFeedMaxItemCount = 400 +nonisolated private let mobileShellAgentFeedRequestTimeoutNanoseconds: UInt64 = 15_000_000_000 private struct AgentFeedTextPage: Decodable { let text: String @@ -314,6 +315,7 @@ extension MobileShellComposite { let data = try await client.sendRequest( request, timeoutNanoseconds: runtime?.rpcRequestTimeoutNanoseconds + ?? mobileShellAgentFeedRequestTimeoutNanoseconds ) let response = try MobileAgentFeedListResponse.decode(data) guard !Task.isCancelled, From f729dfc82d835508533761a2050282c7315146ae Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:57:39 -0700 Subject: [PATCH 32/88] test: keep configurable queue drain in close-tab suite --- cmuxTests/TabManagerUnitTests.swift | 4 ++-- cmuxTests/WorkspaceCloseTabsContextMenuTests.swift | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cmuxTests/TabManagerUnitTests.swift b/cmuxTests/TabManagerUnitTests.swift index ae00cb22d874..a8e1128cf80b 100644 --- a/cmuxTests/TabManagerUnitTests.swift +++ b/cmuxTests/TabManagerUnitTests.swift @@ -21,12 +21,12 @@ import CmuxSettings let lastSurfaceCloseShortcutDefaultsKey = "closeWorkspaceOnLastSurfaceShortcut" -func drainMainQueue(timeout: TimeInterval = 1.0) { +func drainMainQueue() { let expectation = XCTestExpectation(description: "drain main queue") DispatchQueue.main.async { expectation.fulfill() } - XCTWaiter().wait(for: [expectation], timeout: timeout) + XCTWaiter().wait(for: [expectation], timeout: 1.0) } @discardableResult diff --git a/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift b/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift index bb6853ff248a..7a3c934a50f0 100644 --- a/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift +++ b/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift @@ -15,6 +15,13 @@ import CmuxTerminal /// `drainMainQueue` call spins the run loop until its timeout. Spin briefly. private let mainActorTestMainQueueSpin: TimeInterval = 0.1 +private func drainMainQueue(timeout: TimeInterval = 1.0) { + let deadline = Date().addingTimeInterval(timeout) + repeat { + RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01)) + } while Date() < deadline +} + @MainActor @Suite(.serialized) struct WorkspaceCloseTabsContextMenuTests { From d1dfa05d778a48d8ee0bb6f2e0523f3b36163ec4 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:10:02 -0700 Subject: [PATCH 33/88] fix: hide nested search navigation chrome --- .../MobilePrimarySearchNavigationStack.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index dca4df53d179..5172bb73189a 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -19,9 +19,6 @@ struct MobilePrimarySearchNavigationStack: View { var body: some View { NavigationStack(path: $path) { root() - // Before iOS 26, `.searchable` presents from the navigation - // bar. Keep that bar available even when the iOS 26 native - // search tab uses the shared outer toolbar host. .toolbar(rootNavigationBarVisibility, for: .navigationBar) .modifier(MobilePrimarySearchLifecycleModifier( scope: searchCoordinator.scope, @@ -53,10 +50,7 @@ struct MobilePrimarySearchNavigationStack: View { } private var rootNavigationBarVisibility: Visibility { - if #available(iOS 26.0, *), hidesRootNavigationBar { - return .hidden - } - return .automatic + hidesRootNavigationBar ? .hidden : .automatic } private var searchText: Binding { From 02f68fd5dc4660065361569cad57c99729f739ef Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:27:16 -0700 Subject: [PATCH 34/88] test: restore pane resize controller binding --- cmuxTests/PaneResizeShortcutTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/cmuxTests/PaneResizeShortcutTests.swift b/cmuxTests/PaneResizeShortcutTests.swift index 3d4d96a2c125..ee1f151f8a41 100644 --- a/cmuxTests/PaneResizeShortcutTests.swift +++ b/cmuxTests/PaneResizeShortcutTests.swift @@ -63,6 +63,7 @@ struct PaneResizeShortcutTests { #expect(await AppKitTestEventPump().waitUntil(timeout: .seconds(3)) { workspace.tmuxLayoutSnapshot?.panes.count == 2 }) + let controller = workspace.bonsplitController let split = try rootSplit(controller) #expect(controller.setDividerPosition(0.5, forSplit: try #require(UUID(uuidString: split.id)))) workspace.didProgrammaticallyChangeSplitGeometry() From 397a7aff4443fdf1682901f0cc408d15fa1be760 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:03:40 -0700 Subject: [PATCH 35/88] test: keep last-surface queue drain bounded --- cmuxTests/LastSurfaceClosePreferenceTests.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmuxTests/LastSurfaceClosePreferenceTests.swift b/cmuxTests/LastSurfaceClosePreferenceTests.swift index ad28c73b7814..8ffe44be812e 100644 --- a/cmuxTests/LastSurfaceClosePreferenceTests.swift +++ b/cmuxTests/LastSurfaceClosePreferenceTests.swift @@ -14,6 +14,13 @@ import CmuxSettings /// `drainMainQueue` call spins the run loop until its timeout. Spin briefly. private let mainActorTestMainQueueSpin: TimeInterval = 0.1 +private func drainMainQueue(timeout: TimeInterval = 1.0) { + let deadline = Date().addingTimeInterval(timeout) + repeat { + RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01)) + } while Date() < deadline +} + @MainActor @Suite(.serialized) struct LastSurfaceClosePreferenceTests { From 33feb1a00eeb5eaffeaef2a68b56f9f4ad2575ae Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:20:36 -0700 Subject: [PATCH 36/88] fix: restore updater accent environment --- Sources/Update/NotificationPopoverRow.swift | 3 ++- Sources/Update/UpdateTitlebarAccessory.swift | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/Update/NotificationPopoverRow.swift b/Sources/Update/NotificationPopoverRow.swift index a5fdcaeca8d6..7c39e3064882 100644 --- a/Sources/Update/NotificationPopoverRow.swift +++ b/Sources/Update/NotificationPopoverRow.swift @@ -2,6 +2,7 @@ import CmuxFoundation import SwiftUI struct NotificationPopoverRow: View, Equatable { + @Environment(\.cmuxAccentColor) private var cmuxAccent // Closures excluded from ==; equality is the rendered snapshot only (#2586). nonisolated static func == (lhs: NotificationPopoverRow, rhs: NotificationPopoverRow) -> Bool { lhs.notification == rhs.notification && lhs.workspaceTitle == rhs.workspaceTitle @@ -96,7 +97,7 @@ struct NotificationPopoverRow: View, Equatable { private var rowContent: some View { HStack(spacing: 0) { Rectangle() - .fill(notification.isRead ? Color.clear : cmuxAccentColor()) + .fill(notification.isRead ? Color.clear : cmuxAccent.color) .frame(width: 2.5) .padding(.vertical, 6) diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index a3eb2a616592..d6a0cc467604 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -983,6 +983,7 @@ private final class TitlebarControlRightClickNSView: NSView { } private struct TitlebarNotificationBadge: View { + @Environment(\.cmuxAccentColor) private var cmuxAccent let unreadModel: SidebarUnreadModel let config: TitlebarControlsStyleConfig @Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent @@ -998,7 +999,7 @@ private struct TitlebarNotificationBadge: View { ) .foregroundColor(.white) .frame(width: config.badgeSize, height: config.badgeSize) - .background(Circle().fill(cmuxAccentColor())) + .background(Circle().fill(cmuxAccent.color)) .offset(x: config.badgeOffset.width, y: config.badgeOffset.height) } } @@ -2351,6 +2352,7 @@ private func openPhoneForwardingSettings(in window: NSWindow?) { } private struct NotificationsPopoverView: View { + @Environment(\.cmuxAccentColor) private var cmuxAccent @ObservedObject var notificationStore: TerminalNotificationStore @State private var keyboardShortcutSettingsObserver = KeyboardShortcutSettingsObserver.shared let onDismiss: () -> Void @@ -2478,7 +2480,7 @@ private struct NotificationsPopoverView: View { .foregroundColor(.white) .padding(.horizontal, 6) .padding(.vertical, 1) - .background(Capsule().fill(cmuxAccentColor())) + .background(Capsule().fill(cmuxAccent.color)) } Spacer() Button(action: jumpToLatestUnread) { From 763c5676fa70982817e63f1f16c6df66d587a756 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:12:07 -0700 Subject: [PATCH 37/88] fix: keep pane resize controller binding after merge --- cmuxTests/PaneResizeShortcutTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmuxTests/PaneResizeShortcutTests.swift b/cmuxTests/PaneResizeShortcutTests.swift index 8ff529eeebba..ee1f151f8a41 100644 --- a/cmuxTests/PaneResizeShortcutTests.swift +++ b/cmuxTests/PaneResizeShortcutTests.swift @@ -34,7 +34,6 @@ struct PaneResizeShortcutTests { defer { window.performClose(nil) } let manager = try #require(delegate.tabManagerFor(windowId: windowId)) let workspace = try #require(manager.selectedWorkspace) - let controller = workspace.bonsplitController // createMainWindow copies the size of the current main window, and // earlier tests in the host leave 320-point windows behind. Split // admission then correctly refuses a side-by-side split, so give @@ -64,6 +63,7 @@ struct PaneResizeShortcutTests { #expect(await AppKitTestEventPump().waitUntil(timeout: .seconds(3)) { workspace.tmuxLayoutSnapshot?.panes.count == 2 }) + let controller = workspace.bonsplitController let split = try rootSplit(controller) #expect(controller.setDividerPosition(0.5, forSplit: try #require(UUID(uuidString: split.id)))) workspace.didProgrammaticallyChangeSplitGeometry() From a124f6e3ebc38d41beba6401bd9eaa971e3b9273 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:29:34 -0700 Subject: [PATCH 38/88] fix: remove duplicate updater accent binding --- Sources/Update/UpdateTitlebarAccessory.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index da864530bde1..7954a7b31f0c 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -2356,7 +2356,6 @@ private func openPhoneForwardingSettings(in window: NSWindow?) { } private struct NotificationsPopoverView: View { - @Environment(\.cmuxAccentColor) private var cmuxAccent @ObservedObject var notificationStore: TerminalNotificationStore @State private var keyboardShortcutSettingsObserver = KeyboardShortcutSettingsObserver.shared @Environment(\.cmuxAccentColor) private var cmuxAccent From 6872b5aad7ffbaa643d395e601f30e1acd797d39 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:55:51 -0700 Subject: [PATCH 39/88] fix: resolve merge compile and search compatibility --- .../MobilePrimarySearchNavigationStack.swift | 5 ++++- Sources/Update/UpdateTitlebarAccessory.swift | 1 - cmuxTests/WorkspaceCloseTabsContextMenuTests.swift | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index 5172bb73189a..fb0b7758273e 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -50,7 +50,10 @@ struct MobilePrimarySearchNavigationStack: View { } private var rootNavigationBarVisibility: Visibility { - hidesRootNavigationBar ? .hidden : .automatic + if #available(iOS 26.0, *), hidesRootNavigationBar { + return .hidden + } + return .automatic } private var searchText: Binding { diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 7954a7b31f0c..278e5883b16d 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -986,7 +986,6 @@ private struct TitlebarNotificationBadge: View { @Environment(\.cmuxAccentColor) private var cmuxAccent let unreadModel: SidebarUnreadModel let config: TitlebarControlsStyleConfig - @Environment(\.cmuxAccentColor) private var cmuxAccent @Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent var body: some View { diff --git a/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift b/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift index e6f62d4d9262..b319ea54c2ec 100644 --- a/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift +++ b/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift @@ -103,8 +103,8 @@ struct WorkspaceCloseTabsContextMenuTests { for: tab, inPane: fixture.paneId ) - drainMainQueue() - drainMainQueue() + drainMainQueue(timeout: mainActorTestMainQueueSpin) + drainMainQueue(timeout: mainActorTestMainQueueSpin) #expect(promptCount == 1) #expect(fixture.workspace.panelIdFromSurfaceId(tabId) == nil) From 671bf1d0516abb6313eb82088ded8abecd49be11 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:07:46 -0700 Subject: [PATCH 40/88] fix: resolve notification accent color --- Sources/Update/NotificationPopoverRow.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Update/NotificationPopoverRow.swift b/Sources/Update/NotificationPopoverRow.swift index 116fe12cd6e3..7c39e3064882 100644 --- a/Sources/Update/NotificationPopoverRow.swift +++ b/Sources/Update/NotificationPopoverRow.swift @@ -97,7 +97,7 @@ struct NotificationPopoverRow: View, Equatable { private var rowContent: some View { HStack(spacing: 0) { Rectangle() - .fill(notification.isRead ? Color.clear : cmuxAccent) + .fill(notification.isRead ? Color.clear : cmuxAccent.color) .frame(width: 2.5) .padding(.vertical, 6) From 78ec4be92dd54ef3dd2d75fdc844dbd619d4b81e Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:18:42 -0700 Subject: [PATCH 41/88] fix: keep pre-iOS search in one navigation bar --- .../CmuxMobileShellUI/WorkspaceShellView.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 4ec48277f984..5d535aaf797a 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -324,6 +324,18 @@ struct WorkspaceShellView: View { private var compactTabBarVisibility: Visibility { compactRootToolbarVisible ? .automatic : .hidden } + + private var compactRootToolbarVisibility: Visibility { + guard compactRootToolbarVisible else { return .hidden } + guard selectedPrimaryTab == .search else { return .visible } + // Before iOS 26, the searchable stack owns the navigation bar that + // presents the search field. The shared outer bar must stay hidden so + // it does not create a second navigation chrome layer. + if #available(iOS 26.0, *) { + return .visible + } + return .hidden + } #endif private var listConnectionStatus: MobileMacConnectionStatus { @@ -470,7 +482,7 @@ struct WorkspaceShellView: View { /// system TabView with the transient search tab. private func compactScaffold(presentation: WorkspaceShellRenderPresentation) -> some View { MobilePrimaryTabNavigationHost( - toolbarVisibility: compactRootToolbarVisible ? .visible : .hidden, + toolbarVisibility: compactRootToolbarVisibility, tabBarVisibility: compactTabBarVisibility, toolbar: { if compactRootToolbarVisible { From 2ad1aac221ef1bb4240840136fa9073269f9056c Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:36:54 -0700 Subject: [PATCH 42/88] fix: restore test window geometry reset --- Sources/AppDelegate.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 2f466a04e860..fd64e3be4a73 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -698,6 +698,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private nonisolated static let persistedWindowGeometryDefaultsKey = "cmux.session.lastWindowGeometry.v2" #if DEBUG nonisolated static var debugPersistedWindowGeometryDefaultsKey: String { persistedWindowGeometryDefaultsKey } + + private nonisolated static func forgetPersistedWindowGeometryForTestProcess() { + UserDefaults.standard.removeObject(forKey: persistedWindowGeometryDefaultsKey) + removeLegacyPersistedWindowGeometry() + } #endif private nonisolated static let legacyPersistedWindowGeometryDefaultsKeys = [ "cmux.session.lastWindowGeometry.v1" From cca9d50717476747f380838bf11009022bb75bdf Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:48:05 -0700 Subject: [PATCH 43/88] fix: unwrap browser restoration URL --- Sources/Panels/BrowserPanel+PageRestoration.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Panels/BrowserPanel+PageRestoration.swift b/Sources/Panels/BrowserPanel+PageRestoration.swift index 97672383ee9a..836c9ff46b82 100644 --- a/Sources/Panels/BrowserPanel+PageRestoration.swift +++ b/Sources/Panels/BrowserPanel+PageRestoration.swift @@ -81,6 +81,7 @@ extension BrowserPanel { // committed, so their session state would bring back the wrong page. if navigationDelegate?.activeErrorPageDisplayURL == nil, pendingRecoveryURL == nil, + let documentURL, BrowserDiscardRestoreStrategy.canRestoreSessionState(for: documentURL), let restoreURL, BrowserFormStateSnapshot.isSameDocument( From ac07000030ebf69f4a644cd0eb6a61b18a3f9922 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:00:35 -0700 Subject: [PATCH 44/88] test: sample toolbar during repeated tab switches --- ios/cmuxUITests/cmuxUITests.swift | 41 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/ios/cmuxUITests/cmuxUITests.swift b/ios/cmuxUITests/cmuxUITests.swift index 7b851cb51fee..f1a48d49124d 100644 --- a/ios/cmuxUITests/cmuxUITests.swift +++ b/ios/cmuxUITests/cmuxUITests.swift @@ -4746,6 +4746,7 @@ final class cmuxUITests: XCTestCase { func testNotificationTabPreservesSharedRootToolbar() throws { let app = launchApp(mockData: false, environment: [ "CMUX_UITEST_NOTIFICATION_FEED_PREVIEW": "1", + "CMUX_UITEST_NOTIFICATION_FEED_PREVIEW_TAB_SWITCH": "1", ]) defer { app.terminate() } @@ -4781,28 +4782,30 @@ final class cmuxUITests: XCTestCase { XCTAssertTrue(app.buttons["MobileNotificationFeedMarkAllRead"].waitForExistence(timeout: 3)) XCTAssertTrue(app.buttons["MobileNotificationFeedFilterMenu"].waitForExistence(timeout: 3)) - tap(app.tabBars.buttons["Workspaces"], in: app) - XCTAssertTrue(app.staticTexts["Workspaces"].waitForExistence(timeout: 3)) - XCTAssertTrue(app.buttons["MobileWorkspaceSettingsMenu"].waitForExistence(timeout: 3)) - XCTAssertTrue(app.buttons["MobileWorkspaceDevicesButton"].waitForExistence(timeout: 3)) - XCTAssertTrue(app.buttons["MobileWorkspaceMacPicker"].waitForExistence(timeout: 3)) - - tap(app.tabBars.buttons["Notifications"], in: app) - XCTAssertTrue(waitForHittable(feed, timeout: 3)) - XCTAssertTrue(app.buttons["MobileWorkspaceSettingsMenu"].waitForExistence(timeout: 3)) - XCTAssertTrue(app.buttons["MobileWorkspaceDevicesButton"].waitForExistence(timeout: 3)) let picker = app.buttons["MobileWorkspaceMacPicker"] XCTAssertTrue(picker.waitForExistence(timeout: 3)) - XCTAssertTrue(app.buttons["MobileNotificationFeedMarkAllRead"].waitForExistence(timeout: 3)) - XCTAssertTrue(app.buttons["MobileNotificationFeedFilterMenu"].waitForExistence(timeout: 3)) + XCTAssertTrue(app.tabBars.buttons["Notifications"].isSelected) - guard let pickerFrame = waitForUsableFrame(of: picker, timeout: 3), - let feedFrame = waitForUsableFrame(of: feed, timeout: 3) else { - XCTFail("Notifications tab did not settle to usable toolbar and feed frames") - return - } - XCTAssertEqual(pickerFrame.midX, app.frame.midX, accuracy: 2) - XCTAssertTrue(feedFrame.intersects(app.frame)) + // The fixture switches tabs six times at 700 ms intervals. Sample the + // shared toolbar while those transitions are in flight so a transient + // unmount or blank frame cannot pass by behind a settled-state wait. + let transitionDeadline = Date().addingTimeInterval(9) + var sampleCount = 0 + while Date() < transitionDeadline { + XCTAssertTrue( + picker.exists, + "Shared Mac picker disappeared during a primary-tab transition" + ) + let frame = picker.frame + XCTAssertFalse(frame.isNull || frame.isEmpty) + XCTAssertEqual(frame.midX, app.frame.midX, accuracy: 2) + XCTAssertTrue(frame.intersects(app.frame)) + sampleCount += 1 + RunLoop.current.run(until: Date().addingTimeInterval(0.03)) + } + XCTAssertGreaterThan(sampleCount, 100) + XCTAssertTrue(waitForHittable(feed, timeout: 3)) + XCTAssertTrue(app.tabBars.buttons["Notifications"].isSelected) } /// Drives the production push coordinator through its three user-visible From 370abca24549ab83bb597d9a83d5382450801467 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:01:38 -0700 Subject: [PATCH 45/88] fix: enable repeated switch fixture in toolbar test --- ios/cmuxUITests/cmuxUITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/cmuxUITests/cmuxUITests.swift b/ios/cmuxUITests/cmuxUITests.swift index f1a48d49124d..c76f96f6b722 100644 --- a/ios/cmuxUITests/cmuxUITests.swift +++ b/ios/cmuxUITests/cmuxUITests.swift @@ -4746,7 +4746,6 @@ final class cmuxUITests: XCTestCase { func testNotificationTabPreservesSharedRootToolbar() throws { let app = launchApp(mockData: false, environment: [ "CMUX_UITEST_NOTIFICATION_FEED_PREVIEW": "1", - "CMUX_UITEST_NOTIFICATION_FEED_PREVIEW_TAB_SWITCH": "1", ]) defer { app.terminate() } @@ -4774,6 +4773,7 @@ final class cmuxUITests: XCTestCase { func testNotificationTabSwitchKeepsSharedRootToolbarMounted() throws { let app = launchApp(mockData: false, environment: [ "CMUX_UITEST_NOTIFICATION_FEED_PREVIEW": "1", + "CMUX_UITEST_NOTIFICATION_FEED_PREVIEW_TAB_SWITCH": "1", ]) defer { app.terminate() } From 679eddeb71feb8ae909c67fc75c6bef6b91f0092 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:44:23 -0700 Subject: [PATCH 46/88] fix: repair exact-head verification and base compile drift --- .../NotificationFeedPreviewView.swift | 10 ++++++++++ Sources/Update/UpdateTitlebarAccessory.swift | 8 ++++++++ cmuxTests/WorkspaceCloseTabsContextMenuTests.swift | 7 ------- ios/cmuxUITests/cmuxUITests.swift | 8 +++++++- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index 9a83a14c09ac..de0ea5368c78 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -152,6 +152,16 @@ public struct NotificationFeedPreviewView: View { projection: projection ) } + .overlay(alignment: .topLeading) { + if selectedTab == .workspaces { + Text(verbatim: "Workspaces active") + .accessibilityIdentifier("MobileNotificationFeedPreviewWorkspacesActive") + .accessibilityLabel("Workspaces active") + .frame(width: 1, height: 1) + .opacity(0.01) + .allowsHitTesting(false) + } + } .environment(\.workspaceRootToolbarContentWidth, geometry.size.width) } .onChange(of: primarySearchCoordinator.isPresented) { _, isPresented in diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 278e5883b16d..3688448850f3 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -280,6 +280,14 @@ final class NotificationsAnchorRegistry { anchors.add(view) } + func visibleAnchor(in window: NSWindow) -> NSView? { + anchors.allObjects.first { + $0.window === window + && !$0.bounds.isEmpty + && notificationsPopoverAnchorIsVisible($0) + } + } + func closestAnchor(in window: NSWindow, to pointInWindow: NSPoint) -> NSView? { anchors.allObjects .compactMap { view -> (view: NSView, distance: CGFloat)? in diff --git a/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift b/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift index b319ea54c2ec..660694b2ed70 100644 --- a/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift +++ b/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift @@ -15,13 +15,6 @@ import CmuxTerminal /// `drainMainQueue` call spins the run loop until its timeout. Spin briefly. private let mainActorTestMainQueueSpin: TimeInterval = 0.1 -private func drainMainQueue(timeout: TimeInterval = 1.0) { - let deadline = Date().addingTimeInterval(timeout) - repeat { - RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01)) - } while Date() < deadline -} - @MainActor @Suite(.serialized) struct WorkspaceCloseTabsContextMenuTests { diff --git a/ios/cmuxUITests/cmuxUITests.swift b/ios/cmuxUITests/cmuxUITests.swift index c76f96f6b722..88aa20a9f181 100644 --- a/ios/cmuxUITests/cmuxUITests.swift +++ b/ios/cmuxUITests/cmuxUITests.swift @@ -4786,6 +4786,12 @@ final class cmuxUITests: XCTestCase { XCTAssertTrue(picker.waitForExistence(timeout: 3)) XCTAssertTrue(app.tabBars.buttons["Notifications"].isSelected) + let workspacesPhase = app.staticTexts["MobileNotificationFeedPreviewWorkspacesActive"] + XCTAssertTrue( + workspacesPhase.waitForExistence(timeout: 5), + "The repeated-switch fixture never reached its Workspaces phase" + ) + // The fixture switches tabs six times at 700 ms intervals. Sample the // shared toolbar while those transitions are in flight so a transient // unmount or blank frame cannot pass by behind a settled-state wait. @@ -4803,7 +4809,7 @@ final class cmuxUITests: XCTestCase { sampleCount += 1 RunLoop.current.run(until: Date().addingTimeInterval(0.03)) } - XCTAssertGreaterThan(sampleCount, 100) + XCTAssertGreaterThan(sampleCount, 20) XCTAssertTrue(waitForHittable(feed, timeout: 3)) XCTAssertTrue(app.tabBars.buttons["Notifications"].isSelected) } From 1a670c945b31d1ff0a729ef1c01c065f36806689 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:57:10 -0700 Subject: [PATCH 47/88] fix: keep notification search controls on legacy iOS --- .../CmuxMobileShellUI/WorkspaceShellView.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 5d535aaf797a..de5d4ff481a5 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -647,6 +647,19 @@ struct WorkspaceShellView: View { .toolbar(.visible, for: .navigationBar) } } + .toolbar { + if #unavailable(iOS 26.0), + primarySearchCoordinator.scope == .notifications, + primarySearchNavigationPath.wrappedValue.isEmpty { + rootToolbarContent + NotificationFeedToolbarContent( + projection: notificationFeedProjection, + requestMarkAllRead: { + isConfirmingNotificationFeedMarkAllRead = true + } + ) + } + } } @ViewBuilder From 6010a94edaa06af7022ada3d29a96d60c9832feb Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:06:45 -0700 Subject: [PATCH 48/88] fix: preserve legacy search root controls --- .../CmuxMobileShellUI/WorkspaceShellView.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index de5d4ff481a5..53511a667583 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -648,16 +648,16 @@ struct WorkspaceShellView: View { } } .toolbar { - if #unavailable(iOS 26.0), - primarySearchCoordinator.scope == .notifications, - primarySearchNavigationPath.wrappedValue.isEmpty { + if #unavailable(iOS 26.0), primarySearchNavigationPath.wrappedValue.isEmpty { rootToolbarContent - NotificationFeedToolbarContent( - projection: notificationFeedProjection, - requestMarkAllRead: { - isConfirmingNotificationFeedMarkAllRead = true - } - ) + if primarySearchCoordinator.scope == .notifications { + NotificationFeedToolbarContent( + projection: notificationFeedProjection, + requestMarkAllRead: { + isConfirmingNotificationFeedMarkAllRead = true + } + ) + } } } } From bb805fc2fd1bbf2205eacb7912d7d81d4bcb65e7 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:32:48 -0700 Subject: [PATCH 49/88] test: synchronize toolbar sampling with tab selection --- .../NotificationFeedPreviewView.swift | 10 ---------- ios/cmuxUITests/cmuxUITests.swift | 9 +++++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index de0ea5368c78..9a83a14c09ac 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -152,16 +152,6 @@ public struct NotificationFeedPreviewView: View { projection: projection ) } - .overlay(alignment: .topLeading) { - if selectedTab == .workspaces { - Text(verbatim: "Workspaces active") - .accessibilityIdentifier("MobileNotificationFeedPreviewWorkspacesActive") - .accessibilityLabel("Workspaces active") - .frame(width: 1, height: 1) - .opacity(0.01) - .allowsHitTesting(false) - } - } .environment(\.workspaceRootToolbarContentWidth, geometry.size.width) } .onChange(of: primarySearchCoordinator.isPresented) { _, isPresented in diff --git a/ios/cmuxUITests/cmuxUITests.swift b/ios/cmuxUITests/cmuxUITests.swift index 88aa20a9f181..2af6c2345779 100644 --- a/ios/cmuxUITests/cmuxUITests.swift +++ b/ios/cmuxUITests/cmuxUITests.swift @@ -4786,9 +4786,14 @@ final class cmuxUITests: XCTestCase { XCTAssertTrue(picker.waitForExistence(timeout: 3)) XCTAssertTrue(app.tabBars.buttons["Notifications"].isSelected) - let workspacesPhase = app.staticTexts["MobileNotificationFeedPreviewWorkspacesActive"] + let workspacesTab = app.tabBars.buttons["Workspaces"] + XCTAssertTrue(workspacesTab.waitForExistence(timeout: 3)) + let workspacesDeadline = Date().addingTimeInterval(5) + while !workspacesTab.isSelected && Date() < workspacesDeadline { + RunLoop.current.run(until: Date().addingTimeInterval(0.03)) + } XCTAssertTrue( - workspacesPhase.waitForExistence(timeout: 5), + workspacesTab.isSelected, "The repeated-switch fixture never reached its Workspaces phase" ) From 311fb01d1a12052e617ffeae507b7bbdb93a7b68 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:55:20 -0700 Subject: [PATCH 50/88] test: let tab switch fixture reach the active test --- .../CmuxMobileShellUI/NotificationFeedPreviewView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index 9a83a14c09ac..f7a7303a0aaa 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -172,6 +172,10 @@ public struct NotificationFeedPreviewView: View { .task { guard UITestConfig.notificationFeedPreviewTabSwitchEnabled else { return } let clock = ContinuousClock() + // Let the UI test establish the initial Notifications toolbar + // before the deterministic transition sequence begins. + try? await clock.sleep(for: .seconds(8)) + guard !Task.isCancelled else { return } for _ in 0..<6 where !Task.isCancelled { try? await clock.sleep(for: .milliseconds(700)) guard !Task.isCancelled else { return } From b18130df0d0207099bf40e4bb0e4cce944c20967 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:08:50 -0700 Subject: [PATCH 51/88] test: use the existing hidden anchor lookup --- Sources/Update/UpdateTitlebarAccessory.swift | 8 -------- cmuxTests/UpdatePillReleaseVisibilityTests.swift | 6 +++++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 3688448850f3..278e5883b16d 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -280,14 +280,6 @@ final class NotificationsAnchorRegistry { anchors.add(view) } - func visibleAnchor(in window: NSWindow) -> NSView? { - anchors.allObjects.first { - $0.window === window - && !$0.bounds.isEmpty - && notificationsPopoverAnchorIsVisible($0) - } - } - func closestAnchor(in window: NSWindow, to pointInWindow: NSPoint) -> NSView? { anchors.allObjects .compactMap { view -> (view: NSView, distance: CGFloat)? in diff --git a/cmuxTests/UpdatePillReleaseVisibilityTests.swift b/cmuxTests/UpdatePillReleaseVisibilityTests.swift index 49f04a01d0a9..1a915706ab35 100644 --- a/cmuxTests/UpdatePillReleaseVisibilityTests.swift +++ b/cmuxTests/UpdatePillReleaseVisibilityTests.swift @@ -845,8 +845,12 @@ struct NotificationsPopoverAnchorPolicyTests { NotificationsAnchorRegistry.shared.register(anchor) anchor.isHidden = true + let selectedAnchor = NotificationsAnchorRegistry.shared.closestAnchor( + in: window, + to: NSPoint(x: anchor.frame.midX, y: anchor.frame.midY) + ) #expect( - NotificationsAnchorRegistry.shared.visibleAnchor(in: window) == nil, + selectedAnchor == nil, "A hidden titlebar accessory anchor must not be selected for keyboard-opened notifications." ) } From e2a4fe5d5cae8ff55a09e354b1ee33ffed6f76a6 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:34:27 -0700 Subject: [PATCH 52/88] test: repair cmux test helper call sites --- cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift | 3 +-- cmuxTests/LastSurfaceClosePreferenceTests.swift | 7 ------- cmuxTests/WorkspaceCloseTabsContextMenuTests.swift | 4 ++-- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 1d4bf7edf76d..4dc0b5558f09 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -2747,8 +2747,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { ].joined(separator: "\n").write(to: transcriptURL, atomically: true, encoding: .utf8) let launchEnvironment = codexLaunchEnvironment( context: context, - sessionId: sessionId, - observedHookPID: "2" + sessionId: sessionId ) startAgentHookMockServerAccepting(context: context) diff --git a/cmuxTests/LastSurfaceClosePreferenceTests.swift b/cmuxTests/LastSurfaceClosePreferenceTests.swift index 9f795b313648..3316ae729bed 100644 --- a/cmuxTests/LastSurfaceClosePreferenceTests.swift +++ b/cmuxTests/LastSurfaceClosePreferenceTests.swift @@ -14,13 +14,6 @@ import CmuxSettings /// `drainMainQueue` call spins the run loop until its timeout. Spin briefly. private let mainActorTestMainQueueSpin: TimeInterval = 0.1 -private func drainMainQueue(timeout: TimeInterval = 1.0) { - let deadline = Date().addingTimeInterval(timeout) - repeat { - RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01)) - } while Date() < deadline -} - @MainActor @Suite(.serialized) struct LastSurfaceClosePreferenceTests { diff --git a/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift b/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift index 660694b2ed70..065a6b6f0d43 100644 --- a/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift +++ b/cmuxTests/WorkspaceCloseTabsContextMenuTests.swift @@ -96,8 +96,8 @@ struct WorkspaceCloseTabsContextMenuTests { for: tab, inPane: fixture.paneId ) - drainMainQueue(timeout: mainActorTestMainQueueSpin) - drainMainQueue(timeout: mainActorTestMainQueueSpin) + drainMainQueueForCloseTest(timeout: mainActorTestMainQueueSpin) + drainMainQueueForCloseTest(timeout: mainActorTestMainQueueSpin) #expect(promptCount == 1) #expect(fixture.workspace.panelIdFromSurfaceId(tabId) == nil) From 86e3442f03274f6f5c19404bd4f27cac5ad87651 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:10:56 -0700 Subject: [PATCH 53/88] refactor: isolate shared iOS toolbar types --- .../MobilePrimarySearchNavigationStack.swift | 14 ----- ...rimarySearchTabBarVisibilityModifier.swift | 17 ++++++ .../MobilePrimaryTabNavigationHost.swift | 37 ++++++++++++ .../MobilePrimaryTabScaffold.swift | 33 ----------- .../NotificationFeedFilterMenu.swift | 35 +++++++++++ .../NotificationFeedPreviewView.swift | 10 +++- .../NotificationFeedToolbarContent.swift | 33 +++++++++++ .../NotificationFeedView.swift | 58 ------------------- ...UITestConfig+NotificationFeedPreview.swift | 13 ----- .../UITestConfigTests.swift | 9 --- ...ocessIntegrationRegressionTests+JSON.swift | 23 +++++++- ...ifyProcessIntegrationRegressionTests.swift | 3 +- 12 files changed, 153 insertions(+), 132 deletions(-) create mode 100644 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchTabBarVisibilityModifier.swift create mode 100644 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift create mode 100644 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedFilterMenu.swift create mode 100644 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedToolbarContent.swift diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index fb0b7758273e..e45b9516be19 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -80,18 +80,4 @@ struct MobilePrimarySearchNavigationStack: View { } } } - -private struct MobilePrimarySearchTabBarVisibilityModifier: ViewModifier { - let isEnabled: Bool - let visibility: Visibility - - @ViewBuilder - func body(content: Content) -> some View { - if isEnabled { - content.mobileToolbarVisibility(visibility, for: .tabBar) - } else { - content - } - } -} #endif diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchTabBarVisibilityModifier.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchTabBarVisibilityModifier.swift new file mode 100644 index 000000000000..b77b7b5cb2b5 --- /dev/null +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchTabBarVisibilityModifier.swift @@ -0,0 +1,17 @@ +#if os(iOS) +import SwiftUI + +struct MobilePrimarySearchTabBarVisibilityModifier: ViewModifier { + let isEnabled: Bool + let visibility: Visibility + + @ViewBuilder + func body(content: Content) -> some View { + if isEnabled { + content.mobileToolbarVisibility(visibility, for: .tabBar) + } else { + content + } + } +} +#endif diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift new file mode 100644 index 000000000000..c18cc9f27976 --- /dev/null +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift @@ -0,0 +1,37 @@ +#if os(iOS) +import CmuxMobileSupport +import SwiftUI + +/// Keeps the compact tab bar and its root navigation chrome in one hierarchy. +/// The tab contents still keep their own navigation paths, but they no longer +/// compete to install the root toolbar as selection changes. +struct MobilePrimaryTabNavigationHost: View { + let content: Content + let toolbar: Toolbar + let toolbarVisibility: Visibility + let tabBarVisibility: Visibility + + init( + toolbarVisibility: Visibility, + tabBarVisibility: Visibility = .automatic, + @ToolbarContentBuilder toolbar: () -> Toolbar, + @ViewBuilder content: () -> Content + ) { + self.content = content() + self.toolbar = toolbar() + self.toolbarVisibility = toolbarVisibility + self.tabBarVisibility = tabBarVisibility + } + + var body: some View { + NavigationStack { + content + .toolbar { + toolbar + } + .toolbar(toolbarVisibility, for: .navigationBar) + .mobileToolbarVisibility(tabBarVisibility, for: .tabBar) + } + } +} +#endif diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift index 4917a2fcbee4..589369e6c5b8 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift @@ -2,39 +2,6 @@ import CmuxMobileSupport import SwiftUI -/// Keeps the compact tab bar and its root navigation chrome in one hierarchy. -/// The tab contents still keep their own navigation paths, but they no longer -/// compete to install the root toolbar as selection changes. -struct MobilePrimaryTabNavigationHost: View { - let content: Content - let toolbar: Toolbar - let toolbarVisibility: Visibility - let tabBarVisibility: Visibility - - init( - toolbarVisibility: Visibility, - tabBarVisibility: Visibility = .automatic, - @ToolbarContentBuilder toolbar: () -> Toolbar, - @ViewBuilder content: () -> Content - ) { - self.content = content() - self.toolbar = toolbar() - self.toolbarVisibility = toolbarVisibility - self.tabBarVisibility = tabBarVisibility - } - - var body: some View { - NavigationStack { - content - .toolbar { - toolbar - } - .toolbar(toolbarVisibility, for: .navigationBar) - .mobileToolbarVisibility(tabBarVisibility, for: .tabBar) - } - } -} - /// Native primary navigation shared by the live shell and deterministic UI /// fixtures. Keeping the tab construction here guarantees that previews exercise /// the same labels, symbols, badge behavior, and selection semantics as the app. diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedFilterMenu.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedFilterMenu.swift new file mode 100644 index 000000000000..65d4a551ba13 --- /dev/null +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedFilterMenu.swift @@ -0,0 +1,35 @@ +#if os(iOS) +import CmuxMobileShellModel +import CmuxMobileSupport +import SwiftUI + +/// The feed twin of `WorkspaceListFilterMenu`: read state lives in a toolbar +/// menu instead of a segmented bar above the list, and the icon fills while a +/// narrowing filter is active, mirroring Mail. +struct NotificationFeedFilterMenu: View { + @Binding var selection: MobileNotificationFeedFilter + + var body: some View { + Menu { + Picker( + L10n.string("mobile.notificationFeed.filter.label", defaultValue: "Notification filter"), + selection: $selection + ) { + Text(L10n.string( + "mobile.notificationFeed.filter.allNotifications", + defaultValue: "All Notifications" + )) + .tag(MobileNotificationFeedFilter.all) + Text(L10n.string("mobile.notificationFeed.filter.unread", defaultValue: "Unread")) + .tag(MobileNotificationFeedFilter.unread) + } + } label: { + Image(systemName: selection == .unread + ? "line.3.horizontal.decrease.circle.fill" + : "line.3.horizontal.decrease.circle") + } + .accessibilityLabel(L10n.string("mobile.notificationFeed.filter", defaultValue: "Filter")) + .accessibilityIdentifier("MobileNotificationFeedFilterMenu") + } +} +#endif diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index f7a7303a0aaa..69d3fea1a2e1 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -170,7 +170,7 @@ public struct NotificationFeedPreviewView: View { projection.update(items: items, referenceDate: referenceDate) } .task { - guard UITestConfig.notificationFeedPreviewTabSwitchEnabled else { return } + guard notificationFeedPreviewTabSwitchEnabled else { return } let clock = ContinuousClock() // Let the UI test establish the initial Notifications toolbar // before the deterministic transition sequence begins. @@ -187,6 +187,14 @@ public struct NotificationFeedPreviewView: View { } } + private var notificationFeedPreviewTabSwitchEnabled: Bool { + #if DEBUG + ProcessInfo.processInfo.environment["CMUX_UITEST_NOTIFICATION_FEED_PREVIEW_TAB_SWITCH"] == "1" + #else + false + #endif + } + private var previewRootToolbarVisible: Bool { switch selectedTab { case .workspaces, .feed: diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedToolbarContent.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedToolbarContent.swift new file mode 100644 index 000000000000..ec2a41681190 --- /dev/null +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedToolbarContent.swift @@ -0,0 +1,33 @@ +#if os(iOS) +import CmuxMobileShellModel +import CmuxMobileSupport +import SwiftUI + +/// Toolbar controls shared by the visible notification feed and the compact +/// primary-tab parent. Keeping this preference in one toolbar hierarchy avoids +/// inactive, opacity-hidden feed stacks contributing duplicate items. +struct NotificationFeedToolbarContent: ToolbarContent { + @Bindable var projection: NotificationFeedProjection + let requestMarkAllRead: () -> Void + + var body: some ToolbarContent { + ToolbarItemGroup(placement: .topBarTrailing) { + if projection.sourceUnreadCount > 0 { + Button(action: requestMarkAllRead) { + Label( + L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read"), + systemImage: "envelope.open" + ) + .labelStyle(.iconOnly) + } + .accessibilityLabel( + L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read") + ) + .accessibilityIdentifier("MobileNotificationFeedMarkAllRead") + } + + NotificationFeedFilterMenu(selection: $projection.filter) + } + } +} +#endif diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift index 4da25a5663ff..b22e88173c6e 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedView.swift @@ -104,64 +104,6 @@ extension View { } } -/// Toolbar controls shared by the visible notification feed and the compact -/// primary-tab parent. Keeping this preference in one toolbar hierarchy avoids -/// inactive, opacity-hidden feed stacks contributing duplicate items. -struct NotificationFeedToolbarContent: ToolbarContent { - @Bindable var projection: NotificationFeedProjection - let requestMarkAllRead: () -> Void - - var body: some ToolbarContent { - ToolbarItemGroup(placement: .topBarTrailing) { - if projection.sourceUnreadCount > 0 { - Button(action: requestMarkAllRead) { - Label( - L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read"), - systemImage: "envelope.open" - ) - .labelStyle(.iconOnly) - } - .accessibilityLabel( - L10n.string("mobile.notificationFeed.markAllRead", defaultValue: "Mark All Read") - ) - .accessibilityIdentifier("MobileNotificationFeedMarkAllRead") - } - - NotificationFeedFilterMenu(selection: $projection.filter) - } - } -} - -/// The feed twin of `WorkspaceListFilterMenu`: read state lives in a toolbar -/// menu instead of a segmented bar above the list, and the icon fills while a -/// narrowing filter is active, mirroring Mail. -struct NotificationFeedFilterMenu: View { - @Binding var selection: MobileNotificationFeedFilter - - var body: some View { - Menu { - Picker( - L10n.string("mobile.notificationFeed.filter.label", defaultValue: "Notification filter"), - selection: $selection - ) { - Text(L10n.string( - "mobile.notificationFeed.filter.allNotifications", - defaultValue: "All Notifications" - )) - .tag(MobileNotificationFeedFilter.all) - Text(L10n.string("mobile.notificationFeed.filter.unread", defaultValue: "Unread")) - .tag(MobileNotificationFeedFilter.unread) - } - } label: { - Image(systemName: selection == .unread - ? "line.3.horizontal.decrease.circle.fill" - : "line.3.horizontal.decrease.circle") - } - .accessibilityLabel(L10n.string("mobile.notificationFeed.filter", defaultValue: "Filter")) - .accessibilityIdentifier("MobileNotificationFeedFilterMenu") - } -} - private struct NotificationFeedList: View { let sections: [NotificationFeedDaySection] let sourceItemCount: Int diff --git a/Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+NotificationFeedPreview.swift b/Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+NotificationFeedPreview.swift index 796482fae96d..34e4cb2aa524 100644 --- a/Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+NotificationFeedPreview.swift +++ b/Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+NotificationFeedPreview.swift @@ -63,17 +63,4 @@ extension UITestConfig { #endif } - /// Whether the notification-feed fixture should drive repeated primary-tab - /// switches for visual transition verification. - public static var notificationFeedPreviewTabSwitchEnabled: Bool { - notificationFeedPreviewTabSwitchEnabled(from: ProcessInfo.processInfo.environment) - } - - static func notificationFeedPreviewTabSwitchEnabled(from env: [String: String]) -> Bool { - #if DEBUG - env["CMUX_UITEST_NOTIFICATION_FEED_PREVIEW_TAB_SWITCH"] == "1" - #else - false - #endif - } } diff --git a/Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift b/Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift index 989be6ccb690..cb775d91b6c1 100644 --- a/Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift +++ b/Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift @@ -408,15 +408,6 @@ import Testing ) == false) } - @Test func notificationFeedPreviewTabSwitchFlagIsDebugOnly() { - let env = ["CMUX_UITEST_NOTIFICATION_FEED_PREVIEW_TAB_SWITCH": "1"] - #if DEBUG - #expect(UITestConfig.notificationFeedPreviewTabSwitchEnabled(from: env)) - #else - #expect(!UITestConfig.notificationFeedPreviewTabSwitchEnabled(from: env)) - #endif - } - @Test func taskComposerPreviewFlagIsDebugOnly() { let env = ["CMUX_UITEST_TASK_COMPOSER_PREVIEW": "1"] #if DEBUG diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift index 507692e63eee..4cf9c41680a1 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift @@ -1,21 +1,38 @@ import Foundation -import XCTest +import Testing extension CLINotifyProcessIntegrationRegressionTests { + func codexLaunchEnvironment( + context: ClaudeHookContext, + sessionId _: String, + observedHookPID: String? = nil + ) -> [String: String] { + var environment = agentLaunchEnvironment( + context: context, + kind: "codex", + executable: "/usr/local/bin/codex", + arguments: ["/usr/local/bin/codex", "--model", "gpt-5.4"] + ) + if let observedHookPID { + environment["CMUX_CODEX_HOOK_PID"] = observedHookPID + } + return environment + } + // A CLI that printed nothing or non-JSON is an ordinary assertion failure. // Letting `JSONSerialization` throw here made XCTest record a thrown error, // which the CI classifier counts as "unexpected" and treats like an // app-host crash, so one slow CLI turned a whole tolerant shard red. func notificationRows(from stdout: String) throws -> [[String: Any]] { let data = Data(stdout.utf8) - return try XCTUnwrap( + return try #require( (try? JSONSerialization.jsonObject(with: data, options: [])) as? [[String: Any]], "Expected notification JSON array, got: \(stdout)" ) } func jsonPayload(from stdout: String) throws -> [String: Any] { let data = Data(stdout.utf8) - return try XCTUnwrap( + return try #require( (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any], "Expected JSON object, got: \(stdout)" ) diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 4dc0b5558f09..1d4bf7edf76d 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -2747,7 +2747,8 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { ].joined(separator: "\n").write(to: transcriptURL, atomically: true, encoding: .utf8) let launchEnvironment = codexLaunchEnvironment( context: context, - sessionId: sessionId + sessionId: sessionId, + observedHookPID: "2" ) startAgentHookMockServerAccepting(context: context) From 9480fa61e9a93d833e90f5b8afaed407cb96fc68 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:34:40 -0700 Subject: [PATCH 54/88] test: bind sidebar indicator query before expect --- cmuxTests/CloudSidebarNativeDropTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmuxTests/CloudSidebarNativeDropTests.swift b/cmuxTests/CloudSidebarNativeDropTests.swift index 6500fb9b09a6..dcc92ac153dd 100644 --- a/cmuxTests/CloudSidebarNativeDropTests.swift +++ b/cmuxTests/CloudSidebarNativeDropTests.swift @@ -143,9 +143,10 @@ struct CloudSidebarNativeDropTests { expectNoSidebarHints(outline) #expect(coordinator.outlineView(outline, validateDrop: info, proposedItem: parent, proposedChildIndex: 0) == .move) outline.draggingExited(info) - #expect(outline.subviews.filter { + let remainingIndicators = outline.subviews.filter { $0.identifier?.rawValue == "sidebarReorderIndicator" && !$0.isHidden - }.isEmpty) + } + #expect(remainingIndicators.isEmpty) expectNoSidebarHints(outline) } From 35b4c10574bc47ae66458dfa5218444304250bd0 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 05:07:18 -0700 Subject: [PATCH 55/88] test: include CLIError in CLI test target --- cmux.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 53441d579dae..a394faeb08a3 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -639,6 +639,7 @@ C0D3F1F20000000000000103 /* CLICodexYoloResumePersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F20000000000000104 /* CLICodexYoloResumePersistenceTests.swift */; }; C11218000000000000000052 /* CLIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11218000000000000000051 /* CLIError.swift */; }; C11218000000000000000053 /* CLIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11218000000000000000051 /* CLIError.swift */; }; + C11218000000000000000054 /* CLIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11218000000000000000051 /* CLIError.swift */; }; 12756C0112756C0112756C01 /* CLIEventsStreamPeerCloseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12756C0212756C0212756C02 /* CLIEventsStreamPeerCloseTests.swift */; }; C51600000000000000000001 /* CLIExecInheritedSignalStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51600000000000000000002 /* CLIExecInheritedSignalStateTests.swift */; }; C1175700000000000000002E /* CLIExplicitSurfaceRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000015 /* CLIExplicitSurfaceRoutingTests.swift */; }; @@ -13833,6 +13834,7 @@ C11757000000000000000043 /* CLICodexHookTimeoutRegressionTestSupport.swift in Sources */, C1175700000000000000002C /* CLICodexQueuedHookContractTests.swift in Sources */, C1175700000000000000002D /* CLICodexResumeNotificationTests.swift in Sources */, + C11218000000000000000054 /* CLIError.swift in Sources */, C1175700000000000000002E /* CLIExplicitSurfaceRoutingTests.swift in Sources */, C1175700000000000000002F /* CLIHookNoResponseTests.swift in Sources */, C11757000000000000000044 /* CLIHookProcessRunner.swift in Sources */, From 2c4672cece70c1eeffa32888cae1a06b99c6c935 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 05:23:09 -0700 Subject: [PATCH 56/88] Revert "test: include CLIError in CLI test target" This reverts commit 35b4c10574bc47ae66458dfa5218444304250bd0. --- cmux.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index a394faeb08a3..53441d579dae 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -639,7 +639,6 @@ C0D3F1F20000000000000103 /* CLICodexYoloResumePersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F20000000000000104 /* CLICodexYoloResumePersistenceTests.swift */; }; C11218000000000000000052 /* CLIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11218000000000000000051 /* CLIError.swift */; }; C11218000000000000000053 /* CLIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11218000000000000000051 /* CLIError.swift */; }; - C11218000000000000000054 /* CLIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11218000000000000000051 /* CLIError.swift */; }; 12756C0112756C0112756C01 /* CLIEventsStreamPeerCloseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12756C0212756C0212756C02 /* CLIEventsStreamPeerCloseTests.swift */; }; C51600000000000000000001 /* CLIExecInheritedSignalStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51600000000000000000002 /* CLIExecInheritedSignalStateTests.swift */; }; C1175700000000000000002E /* CLIExplicitSurfaceRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000015 /* CLIExplicitSurfaceRoutingTests.swift */; }; @@ -13834,7 +13833,6 @@ C11757000000000000000043 /* CLICodexHookTimeoutRegressionTestSupport.swift in Sources */, C1175700000000000000002C /* CLICodexQueuedHookContractTests.swift in Sources */, C1175700000000000000002D /* CLICodexResumeNotificationTests.swift in Sources */, - C11218000000000000000054 /* CLIError.swift in Sources */, C1175700000000000000002E /* CLIExplicitSurfaceRoutingTests.swift in Sources */, C1175700000000000000002F /* CLIHookNoResponseTests.swift in Sources */, C11757000000000000000044 /* CLIHookProcessRunner.swift in Sources */, From bae1810509540fc47792be521bfff2e84407bf88 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 05:24:44 -0700 Subject: [PATCH 57/88] fix: make tmux parser errors target neutral --- CLI/CMUXCLI+AutoNaming.swift | 22 ++++++++++++------- .../CLITmuxCompatBufferContentTests.swift | 12 +++++----- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index f8372ccac923..1e29026cd665 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -323,6 +323,12 @@ struct TmuxCompatParsedArguments: Equatable, Sendable { let message: String? } +struct TmuxCompatArgumentError: Error, LocalizedError, Equatable, Sendable { + let message: String + + var errorDescription: String? { message } +} + enum TmuxCompatArgumentParser { private struct ScanResult { var values: [String: String] = [:] @@ -338,7 +344,7 @@ enum TmuxCompatArgumentParser { flagOptions: [] ) guard result.positional.isEmpty else { - throw CLIError(message: "clear-history: unexpected arguments: \(result.positional.joined(separator: " "))") + throw TmuxCompatArgumentError(message: "clear-history: unexpected arguments: \(result.positional.joined(separator: " "))") } return make(result) } @@ -351,7 +357,7 @@ enum TmuxCompatArgumentParser { flagOptions: ["--bracketed"] ) guard result.positional.isEmpty else { - throw CLIError(message: "paste-buffer: unexpected arguments: \(result.positional.joined(separator: " "))") + throw TmuxCompatArgumentError(message: "paste-buffer: unexpected arguments: \(result.positional.joined(separator: " "))") } return make(result) } @@ -365,7 +371,7 @@ enum TmuxCompatArgumentParser { allowsPositional: true ) if result.values["--command"] != nil, !result.positional.isEmpty { - throw CLIError(message: "respawn-pane: unexpected arguments: \(result.positional.joined(separator: " "))") + throw TmuxCompatArgumentError(message: "respawn-pane: unexpected arguments: \(result.positional.joined(separator: " "))") } let command = result.values["--command"] ?? result.positional.joined(separator: " ") return make(result, commandText: command.isEmpty ? nil : command) @@ -414,7 +420,7 @@ enum TmuxCompatArgumentParser { let arg = args[index] if terminated { guard allowsPositional else { - throw CLIError(message: "\(command): unexpected argument: \(arg)") + throw TmuxCompatArgumentError(message: "\(command): unexpected argument: \(arg)") } result.positional.append(arg) index += 1 @@ -431,13 +437,13 @@ enum TmuxCompatArgumentParser { value = String(arg.dropFirst(option.count + 1)) } else { guard index + 1 < args.count, args[index + 1] != "--", !args[index + 1].hasPrefix("-") else { - throw CLIError(message: "\(command): \(option) requires a value") + throw TmuxCompatArgumentError(message: "\(command): \(option) requires a value") } value = args[index + 1] index += 1 } guard !value.isEmpty else { - throw CLIError(message: "\(command): \(option) requires a value") + throw TmuxCompatArgumentError(message: "\(command): \(option) requires a value") } result.values[option] = value index += 1 @@ -449,10 +455,10 @@ enum TmuxCompatArgumentParser { continue } if arg.hasPrefix("-") { - throw CLIError(message: "\(command): unknown option '\(arg)'") + throw TmuxCompatArgumentError(message: "\(command): unknown option '\(arg)'") } guard allowsPositional else { - throw CLIError(message: "\(command): unexpected argument: \(arg)") + throw TmuxCompatArgumentError(message: "\(command): unexpected argument: \(arg)") } result.positional.append(arg) index += 1 diff --git a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift index 3db7266fd97f..92eba2605379 100644 --- a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift +++ b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift @@ -171,25 +171,25 @@ struct CLITmuxCompatArgumentParserTests { ["--surface="] ]) func rejectsMalformedClearHistory(arguments: [String]) { - #expect(throws: CLIError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseClearHistory(arguments) } } @Test func rejectsUnknownAndMissingValuesAcrossCommands() { - #expect(throws: CLIError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parsePasteBuffer(["--typo"]) } - #expect(throws: CLIError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--command"]) } - #expect(throws: CLIError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--commnad", "echo hi"]) } - #expect(throws: CLIError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["--pritn", "done"]) } - #expect(throws: CLIError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["done", "--typo"]) } } From 5317de27111a9849f376a814eb864201ab9d8d82 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 05:44:58 -0700 Subject: [PATCH 58/88] fix: keep tmux parser validation target neutral --- CLI/CMUXCLI+AutoNaming.swift | 30 ++++++++++--------- .../CLITmuxCompatBufferContentTests.swift | 12 ++++---- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index 1e29026cd665..e65ca21eec60 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -323,12 +323,6 @@ struct TmuxCompatParsedArguments: Equatable, Sendable { let message: String? } -struct TmuxCompatArgumentError: Error, LocalizedError, Equatable, Sendable { - let message: String - - var errorDescription: String? { message } -} - enum TmuxCompatArgumentParser { private struct ScanResult { var values: [String: String] = [:] @@ -344,7 +338,7 @@ enum TmuxCompatArgumentParser { flagOptions: [] ) guard result.positional.isEmpty else { - throw TmuxCompatArgumentError(message: "clear-history: unexpected arguments: \(result.positional.joined(separator: " "))") + throw invalid("clear-history: unexpected arguments: \(result.positional.joined(separator: " "))") } return make(result) } @@ -357,7 +351,7 @@ enum TmuxCompatArgumentParser { flagOptions: ["--bracketed"] ) guard result.positional.isEmpty else { - throw TmuxCompatArgumentError(message: "paste-buffer: unexpected arguments: \(result.positional.joined(separator: " "))") + throw invalid("paste-buffer: unexpected arguments: \(result.positional.joined(separator: " "))") } return make(result) } @@ -371,7 +365,7 @@ enum TmuxCompatArgumentParser { allowsPositional: true ) if result.values["--command"] != nil, !result.positional.isEmpty { - throw TmuxCompatArgumentError(message: "respawn-pane: unexpected arguments: \(result.positional.joined(separator: " "))") + throw invalid("respawn-pane: unexpected arguments: \(result.positional.joined(separator: " "))") } let command = result.values["--command"] ?? result.positional.joined(separator: " ") return make(result, commandText: command.isEmpty ? nil : command) @@ -406,6 +400,14 @@ enum TmuxCompatArgumentParser { ) } + private static func invalid(_ message: String) -> NSError { + NSError( + domain: "cmux.tmux-compat.arguments", + code: 1, + userInfo: [NSLocalizedDescriptionKey: message] + ) + } + private static func scan( _ args: [String], command: String, @@ -420,7 +422,7 @@ enum TmuxCompatArgumentParser { let arg = args[index] if terminated { guard allowsPositional else { - throw TmuxCompatArgumentError(message: "\(command): unexpected argument: \(arg)") + throw invalid("\(command): unexpected argument: \(arg)") } result.positional.append(arg) index += 1 @@ -437,13 +439,13 @@ enum TmuxCompatArgumentParser { value = String(arg.dropFirst(option.count + 1)) } else { guard index + 1 < args.count, args[index + 1] != "--", !args[index + 1].hasPrefix("-") else { - throw TmuxCompatArgumentError(message: "\(command): \(option) requires a value") + throw invalid("\(command): \(option) requires a value") } value = args[index + 1] index += 1 } guard !value.isEmpty else { - throw TmuxCompatArgumentError(message: "\(command): \(option) requires a value") + throw invalid("\(command): \(option) requires a value") } result.values[option] = value index += 1 @@ -455,10 +457,10 @@ enum TmuxCompatArgumentParser { continue } if arg.hasPrefix("-") { - throw TmuxCompatArgumentError(message: "\(command): unknown option '\(arg)'") + throw invalid("\(command): unknown option '\(arg)'") } guard allowsPositional else { - throw TmuxCompatArgumentError(message: "\(command): unexpected argument: \(arg)") + throw invalid("\(command): unexpected argument: \(arg)") } result.positional.append(arg) index += 1 diff --git a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift index 92eba2605379..109096c18701 100644 --- a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift +++ b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift @@ -171,25 +171,25 @@ struct CLITmuxCompatArgumentParserTests { ["--surface="] ]) func rejectsMalformedClearHistory(arguments: [String]) { - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: NSError.self) { try TmuxCompatArgumentParser.parseClearHistory(arguments) } } @Test func rejectsUnknownAndMissingValuesAcrossCommands() { - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: NSError.self) { try TmuxCompatArgumentParser.parsePasteBuffer(["--typo"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: NSError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--command"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: NSError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--commnad", "echo hi"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: NSError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["--pritn", "done"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: NSError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["done", "--typo"]) } } From 9dc99950bde31b4f8fc685d74b288080712f02fe Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:07:27 -0700 Subject: [PATCH 59/88] test: repair hosted macOS CI fixtures --- .github/workflows/ci-macos.yml | 1 + CLI/cmux.swift | 12 +++++++++ cmuxCLITests/BundledCLITestSupport.swift | 21 +++++++++++++++ .../CLIAgentMessageCommandTests.swift | 6 +++++ .../CLIClaudeHookTimeoutRegressionTests.swift | 13 +++++++--- .../ClaudeWrapperResumeEnvironmentTests.swift | 2 +- ...ifyProcessIntegrationRegressionTests.swift | 10 +++---- cmuxTests/CloudSidebarNativeDropTests.swift | 4 ++- cmuxTests/SwiftTestingAssertions.swift | 26 +++++++++++-------- scripts/ci/run-app-host-xcodebuild.sh | 3 +++ scripts/ci/run-in-console-session.sh | 2 +- 11 files changed, 77 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 9b6c45267654..63dd7a8283dc 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -2951,6 +2951,7 @@ jobs: "TEST_RUNNER_XDG_CONFIG_HOME=$CMUX_CLI_TESTS_HOME/.config" \ "TEST_RUNNER_SSH_AUTH_SOCK=" \ "TEST_RUNNER_CMUX_CLI_PATH=$CMUX_CLI_PATH" \ + "TEST_RUNNER_CMUX_CI_RUNTIME_SOURCE_ROOT=${CMUX_CI_RUNTIME_SOURCE_ROOT:-}" \ bash scripts/ci/run-and-capture.sh "$test_log" \ xcodebuild test-without-building \ -xctestrun "$CMUX_CLI_TESTS_XCTESTRUN" \ diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 4b87b3991db6..b89c56141776 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -40540,6 +40540,18 @@ export default { appendIfExisting(Bundle.main.url(forResource: "opencode-plugin", withExtension: "js")) appendIfExisting(Bundle.main.resourceURL?.appendingPathComponent("opencode-plugin.js", isDirectory: false)) + if let runtimeRoot = ProcessInfo.processInfo.environment["CMUX_CI_RUNTIME_SOURCE_ROOT"], + !runtimeRoot.isEmpty { + appendIfExisting( + URL(fileURLWithPath: runtimeRoot, isDirectory: true) + .appendingPathComponent("src/Resources/opencode-plugin.js") + ) + } + appendIfExisting( + URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) + .appendingPathComponent("Resources/opencode-plugin.js") + ) + if let executableURL = resolvedExecutableURL() { let execDir = executableURL.deletingLastPathComponent().standardizedFileURL for relativePath in ["opencode-plugin.js", "../opencode-plugin.js", "../../Resources/opencode-plugin.js", "../../../Contents/Resources/opencode-plugin.js"] { diff --git a/cmuxCLITests/BundledCLITestSupport.swift b/cmuxCLITests/BundledCLITestSupport.swift index 2aebcc02f600..d77215d05099 100644 --- a/cmuxCLITests/BundledCLITestSupport.swift +++ b/cmuxCLITests/BundledCLITestSupport.swift @@ -7,6 +7,27 @@ import XCTest /// has no app host, so it resolves it from `CMUX_CLI_PATH` or relative to the /// built products directory the bundle itself was copied into. enum BundledCLITestSupport { + static func repositoryRoot(file: StaticString = #filePath) -> URL { + let environment = ProcessInfo.processInfo.environment + var roots: [URL] = [] + for key in ["CMUX_CI_RUNTIME_SOURCE_ROOT", "TEST_RUNNER_CMUX_CI_RUNTIME_SOURCE_ROOT"] { + if let runtimeRoot = environment[key], !runtimeRoot.isEmpty { + roots.append(URL(fileURLWithPath: runtimeRoot, isDirectory: true) + .appendingPathComponent("src", isDirectory: true)) + } + } + let fileRoot = URL(fileURLWithPath: String(describing: file)) + .deletingLastPathComponent() + .deletingLastPathComponent() + roots.append(fileRoot) + roots.append(URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true)) + return roots.first(where: { + FileManager.default.isExecutableFile( + atPath: $0.appendingPathComponent("Resources/bin/cmux-claude-wrapper").path + ) + }) ?? fileRoot + } + /// The daemon handshake must match the CLI under test, not the xctest host. static func appVersion(cliPath: String) throws -> String { let result = CLIHookProcessRunner.run( diff --git a/cmuxCLITests/CLIAgentMessageCommandTests.swift b/cmuxCLITests/CLIAgentMessageCommandTests.swift index 90d23a7d4e6b..9b5247513a6e 100644 --- a/cmuxCLITests/CLIAgentMessageCommandTests.swift +++ b/cmuxCLITests/CLIAgentMessageCommandTests.swift @@ -99,6 +99,11 @@ struct CLIAgentMessageCommandTests { "state": "queued", "sender_name": "coordinator", "body": "first line\nsecond line", + ], [ + "id": "fedcba9876543210", + "state": "queued", + "sender_name": "reviewer", + "body": "another message", ]], ], ] @@ -111,6 +116,7 @@ struct CLIAgentMessageCommandTests { let readParams = try #require(run.request("agent.message.mark_read")?["params"] as? [String: Any]) #expect(readParams["ids"] as? [String] == ["abcdef0123456789", "fedcba9876543210"]) #expect(run.result.stdout.contains("abcdef01 coordinator: first line"), Comment(rawValue: run.result.stdout)) + #expect(run.result.stdout.contains("fedcba98 reviewer: another message"), Comment(rawValue: run.result.stdout)) #expect(!run.result.stdout.contains("second line")) } diff --git a/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift b/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift index fcbe3466e3ec..eccb8368fca4 100644 --- a/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift +++ b/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift @@ -1391,6 +1391,9 @@ struct CLIClaudeHookTimeoutRegressionTests { "PATH": "/usr/bin:/bin:/usr/sbin:/sbin", "CMUX_CLI_SENTRY_DISABLED": "1", ] + if let runtimeRoot = ProcessInfo.processInfo.environment["CMUX_CI_RUNTIME_SOURCE_ROOT"] { + environment["CMUX_CI_RUNTIME_SOURCE_ROOT"] = runtimeRoot + } environment.merge(producer.environment, uniquingKeysWith: { _, value in value }) let result = runCodexHookProcess( executablePath: cliPath, @@ -1429,9 +1432,7 @@ struct CLIClaudeHookTimeoutRegressionTests { } private var repositoryRoot: URL { - URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() - .deletingLastPathComponent() + BundledCLITestSupport.repositoryRoot(file: #filePath) } private func makeSettingsCapturingClaude(at url: URL) throws { @@ -1452,7 +1453,7 @@ struct CLIClaudeHookTimeoutRegressionTests { settings: URL, socketPath: String ) -> [String: String] { - [ + var environment = [ "HOME": root.path, "PATH": "\(binDirectory.path):/usr/bin:/bin:/usr/sbin:/sbin", "TMPDIR": root.path, @@ -1463,6 +1464,10 @@ struct CLIClaudeHookTimeoutRegressionTests { "CMUX_CLI_SENTRY_DISABLED": "1", "FAKE_CLAUDE_SETTINGS_OUTPUT": settings.path, ] + if let runtimeRoot = ProcessInfo.processInfo.environment["CMUX_CI_RUNTIME_SOURCE_ROOT"] { + environment["CMUX_CI_RUNTIME_SOURCE_ROOT"] = runtimeRoot + } + return environment } private func settingsObject(at url: URL) throws -> [String: Any] { diff --git a/cmuxCLITests/ClaudeWrapperResumeEnvironmentTests.swift b/cmuxCLITests/ClaudeWrapperResumeEnvironmentTests.swift index 9b98ca33a083..38bdae3f1fea 100644 --- a/cmuxCLITests/ClaudeWrapperResumeEnvironmentTests.swift +++ b/cmuxCLITests/ClaudeWrapperResumeEnvironmentTests.swift @@ -6,7 +6,7 @@ import Testing @Suite struct ClaudeWrapperResumeEnvironmentTests { @Test func bundledClaudeWrapperScrubsSessionIdentityAndPreservesTrustBypassOnResume() throws { let fileManager = FileManager.default - let repoRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() + let repoRoot = BundledCLITestSupport.repositoryRoot(file: #filePath) let wrapperURL = repoRoot.appendingPathComponent("Resources/bin/cmux-claude-wrapper", isDirectory: false) #expect( fileManager.isExecutableFile(atPath: wrapperURL.path), diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 1d4bf7edf76d..99796a1401ad 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -2779,15 +2779,15 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { try [ #"{"type":"turn_context","payload":{"turn_id":"old-turn"}}"#, #"{"type":"event_msg","payload":{"type":"task_started","turn_id":"old-turn"}}"#, - #"{"type":"event_msg","payload":{"type":"turn_aborted","turn_id":"old-turn"}}"#, + #"{"type":"event_msg","payload":{"type":"turn_complete","turn_id":"old-turn","last_agent_message":"old done"}}"#, #"{"type":"turn_context","payload":{"turn_id":"current-turn"}}"#, #"{"type":"event_msg","payload":{"type":"task_started","turn_id":"current-turn"}}"#, ].joined(separator: "\n").write(to: transcriptURL, atomically: true, encoding: .utf8) - // The late terminal monitor is asynchronous. Wait for its old-turn - // completion journal before invoking the current Stop; this preserves - // the race discriminator covered by upstream PR #10143 rather than - // allowing the test to pass or fail on monitor scheduling. + // A successful terminal record replays Stop through the asynchronous + // monitor; turn_aborted only prunes stale depth at the next hook. + // Wait for the old-turn journal before the current Stop to preserve + // the monitor ordering regression covered by upstream PR #10143. XCTAssertTrue( waitForMockSocketCommand(in: context.state) { AgentJournalAppendCapture.captures(in: [$0]).contains { diff --git a/cmuxTests/CloudSidebarNativeDropTests.swift b/cmuxTests/CloudSidebarNativeDropTests.swift index dcc92ac153dd..30324b22a404 100644 --- a/cmuxTests/CloudSidebarNativeDropTests.swift +++ b/cmuxTests/CloudSidebarNativeDropTests.swift @@ -183,7 +183,9 @@ struct CloudSidebarNativeDropTests { #expect(coordinator.deferredNodes == nil) let current = try #require(CloudSidebarOrganizationTree(nodes: coordinator.nodes).parent(of: source.id)) #expect(current.children.map(\.searchableTitle) == (accepted ? ["fresh-2", "fresh-1"] : ["fresh-1", "fresh-2"])) - let indicators = outline.subviews.filter { $0.identifier?.rawValue == "sidebarReorderIndicator" } + let indicators = outline.subviews.filter { + $0.identifier?.rawValue == "sidebarReorderIndicator" && !$0.isHidden + } #expect(indicators.isEmpty) #expect(fixture.provider.moved.isEmpty && fixture.provider.projected.isEmpty) } diff --git a/cmuxTests/SwiftTestingAssertions.swift b/cmuxTests/SwiftTestingAssertions.swift index fa41cdae638d..93379f9dd2b1 100644 --- a/cmuxTests/SwiftTestingAssertions.swift +++ b/cmuxTests/SwiftTestingAssertions.swift @@ -8,20 +8,24 @@ import Testing struct SwiftTestingAssertions { /// Resolves a test source file from its module-relative identity. static func sourceURL(_ file: StaticString = #fileID) -> URL { - let sourceRoot: URL - if let runtimeRoot = ProcessInfo.processInfo.environment["CMUX_CI_RUNTIME_SOURCE_ROOT"], - !runtimeRoot.isEmpty { - sourceRoot = URL(fileURLWithPath: runtimeRoot, isDirectory: true) - .appendingPathComponent("src", isDirectory: true) - } else { - sourceRoot = URL(fileURLWithPath: #filePath) + let fileID = String(describing: file) + let environment = ProcessInfo.processInfo.environment + var roots: [URL] = [] + for key in ["CMUX_CI_RUNTIME_SOURCE_ROOT", "TEST_RUNNER_CMUX_CI_RUNTIME_SOURCE_ROOT"] { + if let runtimeRoot = environment[key], !runtimeRoot.isEmpty { + roots.append(URL(fileURLWithPath: runtimeRoot, isDirectory: true) + .appendingPathComponent("src", isDirectory: true)) + } + } + roots.append( + URL(fileURLWithPath: #filePath) .deletingLastPathComponent() .deletingLastPathComponent() - } - - let fileID = String(describing: file) + ) + roots.append(URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true)) // #fileID is "/"; the module name matches the repo directory. - return sourceRoot.appendingPathComponent(fileID) + let candidates = roots.map { $0.appendingPathComponent(fileID) } + return candidates.first(where: { FileManager.default.fileExists(atPath: $0.path) }) ?? candidates[0] } diff --git a/scripts/ci/run-app-host-xcodebuild.sh b/scripts/ci/run-app-host-xcodebuild.sh index 7d80b30aae4d..8f07cef4baa8 100755 --- a/scripts/ci/run-app-host-xcodebuild.sh +++ b/scripts/ci/run-app-host-xcodebuild.sh @@ -102,6 +102,9 @@ fi if [ -n "${TEST_RUNNER_BUN_INSTALL:-}" ]; then app_host_test_runner_environment+=("TEST_RUNNER_BUN_INSTALL=$TEST_RUNNER_BUN_INSTALL") fi +if [ -n "${CMUX_CI_RUNTIME_SOURCE_ROOT:-}" ]; then + app_host_test_runner_environment+=("TEST_RUNNER_CMUX_CI_RUNTIME_SOURCE_ROOT=$CMUX_CI_RUNTIME_SOURCE_ROOT") +fi # Focused opt-in suites (renderer memory regression, benchmarks) are gated on a # plain variable the driver receives. Xcode does not inherit it, so a caller that # exports the plain name would silently run nothing. Carry those through. diff --git a/scripts/ci/run-in-console-session.sh b/scripts/ci/run-in-console-session.sh index 0b890927dcde..111a73f7b5d1 100755 --- a/scripts/ci/run-in-console-session.sh +++ b/scripts/ci/run-in-console-session.sh @@ -239,7 +239,7 @@ if [ -n "$console_user" ] && [ "$console_user" != "root" ] \ CMUX_APP_HOST_XCODEBUILD_ATTEMPTS \ CMUX_RENDERER_MEMORY_REGRESSION \ CMUX_APP_HOST_CAPTURE_XCRESULTS CMUX_APP_HOST_RESULT_BUNDLE_ROOT CMUX_APP_HOST_TEST_CASE_TIMEOUT_SECONDS \ - GITHUB_REPOSITORY_ID GITHUB_RUN_ID GITHUB_RUN_ATTEMPT CMUX_APP_HOST_SHARD CMUX_APP_HOST_XCTESTRUN CMUX_NUMERIC_LOCALE_XCTESTRUN CMUX_CI_APP_HOST_ISOLATION_REQUIRED CMUX_APP_HOST_KEY CMUX_APP_HOST_HOME CMUX_APP_HOST_XDG_CONFIG_HOME CMUX_APP_HOST_RECEIPT_DIR CMUX_APP_HOST_CLEANUP_CONFIRMATION CMUX_APP_HOST_CONFIRMATION_FILE \ + GITHUB_REPOSITORY_ID GITHUB_RUN_ID GITHUB_RUN_ATTEMPT CMUX_APP_HOST_SHARD CMUX_APP_HOST_XCTESTRUN CMUX_NUMERIC_LOCALE_XCTESTRUN CMUX_CI_RUNTIME_SOURCE_ROOT CMUX_CI_APP_HOST_ISOLATION_REQUIRED CMUX_APP_HOST_KEY CMUX_APP_HOST_HOME CMUX_APP_HOST_XDG_CONFIG_HOME CMUX_APP_HOST_RECEIPT_DIR CMUX_APP_HOST_CLEANUP_CONFIRMATION CMUX_APP_HOST_CONFIRMATION_FILE \ CFFIXED_USER_HOME XDG_CONFIG_HOME CARGO_HOME RUSTUP_HOME) if [ "${CMUX_CI_APP_HOST_CLEANUP_TEST_HELPER:-0}" = "1" ]; then forward+=(CMUX_CI_APP_HOST_CLEANUP_TEST_HELPER CMUX_APP_HOST_LSOF CMUX_FAKE_LSOF_STATE) From 6f36d19d24aa9cee469a9ce7186218821484bd3f Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:24:31 -0700 Subject: [PATCH 60/88] test: align notification tab switch timing --- CLI/cmux.swift | 5 ----- ios/cmuxUITests/cmuxUITests.swift | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index b89c56141776..f94354d597a3 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -40547,11 +40547,6 @@ export default { .appendingPathComponent("src/Resources/opencode-plugin.js") ) } - appendIfExisting( - URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) - .appendingPathComponent("Resources/opencode-plugin.js") - ) - if let executableURL = resolvedExecutableURL() { let execDir = executableURL.deletingLastPathComponent().standardizedFileURL for relativePath in ["opencode-plugin.js", "../opencode-plugin.js", "../../Resources/opencode-plugin.js", "../../../Contents/Resources/opencode-plugin.js"] { diff --git a/ios/cmuxUITests/cmuxUITests.swift b/ios/cmuxUITests/cmuxUITests.swift index 2af6c2345779..f5d8cf3d2275 100644 --- a/ios/cmuxUITests/cmuxUITests.swift +++ b/ios/cmuxUITests/cmuxUITests.swift @@ -4788,7 +4788,9 @@ final class cmuxUITests: XCTestCase { let workspacesTab = app.tabBars.buttons["Workspaces"] XCTAssertTrue(workspacesTab.waitForExistence(timeout: 3)) - let workspacesDeadline = Date().addingTimeInterval(5) + // The DEBUG fixture holds the initial Notifications state for eight + // seconds, then waits 700 ms before its first Workspaces switch. + let workspacesDeadline = Date().addingTimeInterval(15) while !workspacesTab.isSelected && Date() < workspacesDeadline { RunLoop.current.run(until: Date().addingTimeInterval(0.03)) } From 8efae4c4e228687785d14b1b3e3d9ede0277b0db Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:44:04 -0700 Subject: [PATCH 61/88] fix(cli): preserve typed tmux argument errors --- CLI/CMUXCLI+AutoNaming.swift | 14 ++++++++------ cmuxCLITests/CLITmuxCompatBufferContentTests.swift | 12 ++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index e65ca21eec60..307d1a289e91 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -323,6 +323,12 @@ struct TmuxCompatParsedArguments: Equatable, Sendable { let message: String? } +struct TmuxCompatArgumentError: Error, LocalizedError, Equatable, Sendable { + let message: String + + var errorDescription: String? { message } +} + enum TmuxCompatArgumentParser { private struct ScanResult { var values: [String: String] = [:] @@ -400,12 +406,8 @@ enum TmuxCompatArgumentParser { ) } - private static func invalid(_ message: String) -> NSError { - NSError( - domain: "cmux.tmux-compat.arguments", - code: 1, - userInfo: [NSLocalizedDescriptionKey: message] - ) + private static func invalid(_ message: String) -> TmuxCompatArgumentError { + TmuxCompatArgumentError(message: message) } private static func scan( diff --git a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift index 109096c18701..92eba2605379 100644 --- a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift +++ b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift @@ -171,25 +171,25 @@ struct CLITmuxCompatArgumentParserTests { ["--surface="] ]) func rejectsMalformedClearHistory(arguments: [String]) { - #expect(throws: NSError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseClearHistory(arguments) } } @Test func rejectsUnknownAndMissingValuesAcrossCommands() { - #expect(throws: NSError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parsePasteBuffer(["--typo"]) } - #expect(throws: NSError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--command"]) } - #expect(throws: NSError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--commnad", "echo hi"]) } - #expect(throws: NSError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["--pritn", "done"]) } - #expect(throws: NSError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["done", "--typo"]) } } From eda9650dd36122ebb9cee5f4df09e7d9afe4563e Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:09:46 -0700 Subject: [PATCH 62/88] fix(cli): keep tmux argument error type target neutral --- CLI/CMUXCLI+AutoNaming.swift | 14 +++++++------- cmuxCLITests/CLITmuxCompatBufferContentTests.swift | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index 307d1a289e91..c5da4c069761 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -323,13 +323,13 @@ struct TmuxCompatParsedArguments: Equatable, Sendable { let message: String? } -struct TmuxCompatArgumentError: Error, LocalizedError, Equatable, Sendable { - let message: String +enum TmuxCompatArgumentParser { + struct ArgumentError: Error, LocalizedError, Equatable, Sendable { + let message: String - var errorDescription: String? { message } -} + var errorDescription: String? { message } + } -enum TmuxCompatArgumentParser { private struct ScanResult { var values: [String: String] = [:] var flags: Set = [] @@ -406,8 +406,8 @@ enum TmuxCompatArgumentParser { ) } - private static func invalid(_ message: String) -> TmuxCompatArgumentError { - TmuxCompatArgumentError(message: message) + private static func invalid(_ message: String) -> ArgumentError { + ArgumentError(message: message) } private static func scan( diff --git a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift index 92eba2605379..a21270d505b1 100644 --- a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift +++ b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift @@ -171,25 +171,25 @@ struct CLITmuxCompatArgumentParserTests { ["--surface="] ]) func rejectsMalformedClearHistory(arguments: [String]) { - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseClearHistory(arguments) } } @Test func rejectsUnknownAndMissingValuesAcrossCommands() { - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parsePasteBuffer(["--typo"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--command"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--commnad", "echo hi"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["--pritn", "done"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["done", "--typo"]) } } From e1b7b04dacfa360afd3722f2756b8bc8120a73fd Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:28:40 -0700 Subject: [PATCH 63/88] fix(cli): isolate tmux argument error type --- CLI/CMUXCLI+AutoNaming.swift | 10 ++-------- CLI/TmuxCompatArgumentError.swift | 7 +++++++ cmux.xcodeproj/project.pbxproj | 8 ++++++++ cmuxCLITests/CLITmuxCompatBufferContentTests.swift | 12 ++++++------ 4 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 CLI/TmuxCompatArgumentError.swift diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index c5da4c069761..3b9756200819 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -324,12 +324,6 @@ struct TmuxCompatParsedArguments: Equatable, Sendable { } enum TmuxCompatArgumentParser { - struct ArgumentError: Error, LocalizedError, Equatable, Sendable { - let message: String - - var errorDescription: String? { message } - } - private struct ScanResult { var values: [String: String] = [:] var flags: Set = [] @@ -406,8 +400,8 @@ enum TmuxCompatArgumentParser { ) } - private static func invalid(_ message: String) -> ArgumentError { - ArgumentError(message: message) + private static func invalid(_ message: String) -> TmuxCompatArgumentError { + TmuxCompatArgumentError(message: message) } private static func scan( diff --git a/CLI/TmuxCompatArgumentError.swift b/CLI/TmuxCompatArgumentError.swift new file mode 100644 index 000000000000..c67f1db4f06a --- /dev/null +++ b/CLI/TmuxCompatArgumentError.swift @@ -0,0 +1,7 @@ +import Foundation + +struct TmuxCompatArgumentError: Error, LocalizedError, Equatable, Sendable { + let message: String + + var errorDescription: String? { message } +} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 55a0b9e8151b..717598aa2d07 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -3956,6 +3956,9 @@ C4160A070000000000000001 /* TitlebarLayoutDebugWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4160A070000000000000002 /* TitlebarLayoutDebugWindow.swift */; }; C10D00110000000000000011 /* TitlebarNewWorkspaceSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10D00120000000000000012 /* TitlebarNewWorkspaceSplitButton.swift */; }; A10507000000000000000001 /* TitleUpdateAmplificationRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10507000000000000000002 /* TitleUpdateAmplificationRegressionTests.swift */; }; + 4EA49A152F979C233B038F5C /* TmuxCompatArgumentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */; }; + CA6390108AFAC809B6801E98 /* TmuxCompatArgumentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */; }; + D236A556B558EAD2E624BE99 /* TmuxCompatArgumentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */; }; 9009A0029009A0029009A001 /* TmuxCompatLaunchContextError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9009A0029009A0029009A002 /* TmuxCompatLaunchContextError.swift */; }; E30760030000000000000002 /* TmuxOverlayExperimentSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30760030000000000000001 /* TmuxOverlayExperimentSettings.swift */; }; E30760040000000000000002 /* TmuxOverlayExperimentTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30760040000000000000001 /* TmuxOverlayExperimentTarget.swift */; }; @@ -8234,6 +8237,7 @@ C4160A070000000000000002 /* TitlebarLayoutDebugWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarLayoutDebugWindow.swift; sourceTree = ""; }; C10D00120000000000000012 /* TitlebarNewWorkspaceSplitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/TitlebarNewWorkspaceSplitButton.swift; sourceTree = ""; }; A10507000000000000000002 /* TitleUpdateAmplificationRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleUpdateAmplificationRegressionTests.swift; sourceTree = ""; }; + 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmuxCompatArgumentError.swift; sourceTree = ""; }; 9009A0029009A0029009A002 /* TmuxCompatLaunchContextError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmuxCompatLaunchContextError.swift; sourceTree = ""; }; E30760030000000000000001 /* TmuxOverlayExperimentSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmuxOverlayExperimentSettings.swift; sourceTree = ""; }; E30760040000000000000001 /* TmuxOverlayExperimentTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmuxOverlayExperimentTarget.swift; sourceTree = ""; }; @@ -11875,6 +11879,7 @@ 054B01530302FCEEAA5CFEAD /* CMUXCLI+DiffViewerPreferences.swift */, 57AE38B536A608CB2CDEE6AD /* CMUXCLI+DiffViewerReviewLabels.swift */, 5A2776CC5FE1E0083FAAB3A8 /* VMReadyPollInterval.swift */, + 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */, ); path = CLI; sourceTree = ""; @@ -13869,6 +13874,7 @@ C1175700000000000000003D /* CodexTerminalErrorNotificationTests.swift in Sources */, C1175700000000000000003E /* KimiHookConfigLocationTests.swift in Sources */, C11757000000000000000063 /* ProcessExitWait.swift in Sources */, + D236A556B558EAD2E624BE99 /* TmuxCompatArgumentError.swift in Sources */, C1175700000000000000F0A1 /* VMReadyPollInterval.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -16698,6 +16704,7 @@ 6379A0046379A0046379A004 /* SSHPTYResizeMonitor.swift in Sources */, 9A6800000000000000000002 /* SSHPTYTerminalReadinessReport.swift in Sources */, D0F101000000000000000003 /* TerminalRawMode.swift in Sources */, + 4EA49A152F979C233B038F5C /* TmuxCompatArgumentError.swift in Sources */, 9009A0029009A0029009A001 /* TmuxCompatLaunchContextError.swift in Sources */, 575000010000000000000002 /* TmuxWaitForSignal.swift in Sources */, B81607322100000000000002 /* VMMachineTerminalResolution.swift in Sources */, @@ -17933,6 +17940,7 @@ C0DE7B360000000000000001 /* TextBoxSubmitActionTests.swift in Sources */, F50030040000000000000001 /* TitlebarInteractiveControlTests.swift in Sources */, A10507000000000000000001 /* TitleUpdateAmplificationRegressionTests.swift in Sources */, + CA6390108AFAC809B6801E98 /* TmuxCompatArgumentError.swift in Sources */, E30760060000000000000002 /* TmuxWorkspacePaneOverlayModelTests.swift in Sources */, D3284001A1B2C3D4E5F60718 /* TraditionalChineseIMENumpadRegressionTests.swift in Sources */, 468100000000000000000003 /* TypingHotPathRegressionTests.swift in Sources */, diff --git a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift index a21270d505b1..92eba2605379 100644 --- a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift +++ b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift @@ -171,25 +171,25 @@ struct CLITmuxCompatArgumentParserTests { ["--surface="] ]) func rejectsMalformedClearHistory(arguments: [String]) { - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseClearHistory(arguments) } } @Test func rejectsUnknownAndMissingValuesAcrossCommands() { - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parsePasteBuffer(["--typo"]) } - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--command"]) } - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--commnad", "echo hi"]) } - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["--pritn", "done"]) } - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: TmuxCompatArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["done", "--typo"]) } } From f69a82a862ebdbd2506420b65a7cf68f47350868 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:56:18 -0700 Subject: [PATCH 64/88] fix(cli): keep parser error type with parser --- CLI/CMUXCLI+AutoNaming.swift | 10 ++++++++-- CLI/TmuxCompatArgumentError.swift | 7 ------- cmux.xcodeproj/project.pbxproj | 8 -------- cmuxCLITests/CLITmuxCompatBufferContentTests.swift | 12 ++++++------ 4 files changed, 14 insertions(+), 23 deletions(-) delete mode 100644 CLI/TmuxCompatArgumentError.swift diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index 3b9756200819..62c5ca2c8b6f 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -330,6 +330,12 @@ enum TmuxCompatArgumentParser { var positional: [String] = [] } + struct ArgumentError: Error, LocalizedError, Equatable, Sendable { + let message: String + + var errorDescription: String? { message } + } + static func parseClearHistory(_ args: [String]) throws -> TmuxCompatParsedArguments { let result = try scan( args, @@ -400,8 +406,8 @@ enum TmuxCompatArgumentParser { ) } - private static func invalid(_ message: String) -> TmuxCompatArgumentError { - TmuxCompatArgumentError(message: message) + private static func invalid(_ message: String) -> ArgumentError { + ArgumentError(message: message) } private static func scan( diff --git a/CLI/TmuxCompatArgumentError.swift b/CLI/TmuxCompatArgumentError.swift deleted file mode 100644 index c67f1db4f06a..000000000000 --- a/CLI/TmuxCompatArgumentError.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct TmuxCompatArgumentError: Error, LocalizedError, Equatable, Sendable { - let message: String - - var errorDescription: String? { message } -} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 717598aa2d07..55a0b9e8151b 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -3956,9 +3956,6 @@ C4160A070000000000000001 /* TitlebarLayoutDebugWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4160A070000000000000002 /* TitlebarLayoutDebugWindow.swift */; }; C10D00110000000000000011 /* TitlebarNewWorkspaceSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10D00120000000000000012 /* TitlebarNewWorkspaceSplitButton.swift */; }; A10507000000000000000001 /* TitleUpdateAmplificationRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10507000000000000000002 /* TitleUpdateAmplificationRegressionTests.swift */; }; - 4EA49A152F979C233B038F5C /* TmuxCompatArgumentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */; }; - CA6390108AFAC809B6801E98 /* TmuxCompatArgumentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */; }; - D236A556B558EAD2E624BE99 /* TmuxCompatArgumentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */; }; 9009A0029009A0029009A001 /* TmuxCompatLaunchContextError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9009A0029009A0029009A002 /* TmuxCompatLaunchContextError.swift */; }; E30760030000000000000002 /* TmuxOverlayExperimentSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30760030000000000000001 /* TmuxOverlayExperimentSettings.swift */; }; E30760040000000000000002 /* TmuxOverlayExperimentTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30760040000000000000001 /* TmuxOverlayExperimentTarget.swift */; }; @@ -8237,7 +8234,6 @@ C4160A070000000000000002 /* TitlebarLayoutDebugWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarLayoutDebugWindow.swift; sourceTree = ""; }; C10D00120000000000000012 /* TitlebarNewWorkspaceSplitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/TitlebarNewWorkspaceSplitButton.swift; sourceTree = ""; }; A10507000000000000000002 /* TitleUpdateAmplificationRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleUpdateAmplificationRegressionTests.swift; sourceTree = ""; }; - 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmuxCompatArgumentError.swift; sourceTree = ""; }; 9009A0029009A0029009A002 /* TmuxCompatLaunchContextError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmuxCompatLaunchContextError.swift; sourceTree = ""; }; E30760030000000000000001 /* TmuxOverlayExperimentSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmuxOverlayExperimentSettings.swift; sourceTree = ""; }; E30760040000000000000001 /* TmuxOverlayExperimentTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmuxOverlayExperimentTarget.swift; sourceTree = ""; }; @@ -11879,7 +11875,6 @@ 054B01530302FCEEAA5CFEAD /* CMUXCLI+DiffViewerPreferences.swift */, 57AE38B536A608CB2CDEE6AD /* CMUXCLI+DiffViewerReviewLabels.swift */, 5A2776CC5FE1E0083FAAB3A8 /* VMReadyPollInterval.swift */, - 4633FFE32F3D03F6EFA14FEE /* TmuxCompatArgumentError.swift */, ); path = CLI; sourceTree = ""; @@ -13874,7 +13869,6 @@ C1175700000000000000003D /* CodexTerminalErrorNotificationTests.swift in Sources */, C1175700000000000000003E /* KimiHookConfigLocationTests.swift in Sources */, C11757000000000000000063 /* ProcessExitWait.swift in Sources */, - D236A556B558EAD2E624BE99 /* TmuxCompatArgumentError.swift in Sources */, C1175700000000000000F0A1 /* VMReadyPollInterval.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -16704,7 +16698,6 @@ 6379A0046379A0046379A004 /* SSHPTYResizeMonitor.swift in Sources */, 9A6800000000000000000002 /* SSHPTYTerminalReadinessReport.swift in Sources */, D0F101000000000000000003 /* TerminalRawMode.swift in Sources */, - 4EA49A152F979C233B038F5C /* TmuxCompatArgumentError.swift in Sources */, 9009A0029009A0029009A001 /* TmuxCompatLaunchContextError.swift in Sources */, 575000010000000000000002 /* TmuxWaitForSignal.swift in Sources */, B81607322100000000000002 /* VMMachineTerminalResolution.swift in Sources */, @@ -17940,7 +17933,6 @@ C0DE7B360000000000000001 /* TextBoxSubmitActionTests.swift in Sources */, F50030040000000000000001 /* TitlebarInteractiveControlTests.swift in Sources */, A10507000000000000000001 /* TitleUpdateAmplificationRegressionTests.swift in Sources */, - CA6390108AFAC809B6801E98 /* TmuxCompatArgumentError.swift in Sources */, E30760060000000000000002 /* TmuxWorkspacePaneOverlayModelTests.swift in Sources */, D3284001A1B2C3D4E5F60718 /* TraditionalChineseIMENumpadRegressionTests.swift in Sources */, 468100000000000000000003 /* TypingHotPathRegressionTests.swift in Sources */, diff --git a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift index 92eba2605379..a21270d505b1 100644 --- a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift +++ b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift @@ -171,25 +171,25 @@ struct CLITmuxCompatArgumentParserTests { ["--surface="] ]) func rejectsMalformedClearHistory(arguments: [String]) { - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseClearHistory(arguments) } } @Test func rejectsUnknownAndMissingValuesAcrossCommands() { - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parsePasteBuffer(["--typo"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--command"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--commnad", "echo hi"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["--pritn", "done"]) } - #expect(throws: TmuxCompatArgumentError.self) { + #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["done", "--typo"]) } } From 840da0b19f08d848411b9a3f4084d40af705b4e4 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:19:51 -0700 Subject: [PATCH 65/88] fix(cli): use shared CLI error contract --- CLI/CMUXCLI+AutoNaming.swift | 26 ++++++------------- .../CLITmuxCompatBufferContentTests.swift | 12 ++++----- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index dc8594037801..ed5c0bfa5be4 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -330,12 +330,6 @@ enum TmuxCompatArgumentParser { var positional: [String] = [] } - struct ArgumentError: Error, LocalizedError, Equatable, Sendable { - let message: String - - var errorDescription: String? { message } - } - static func parseClearHistory(_ args: [String]) throws -> TmuxCompatParsedArguments { let result = try scan( args, @@ -344,7 +338,7 @@ enum TmuxCompatArgumentParser { flagOptions: [] ) guard result.positional.isEmpty else { - throw invalid("clear-history: unexpected arguments: \(result.positional.joined(separator: " "))") + throw CLIError(message: "clear-history: unexpected arguments: \(result.positional.joined(separator: " "))") } return make(result) } @@ -357,7 +351,7 @@ enum TmuxCompatArgumentParser { flagOptions: ["--bracketed"] ) guard result.positional.isEmpty else { - throw invalid("paste-buffer: unexpected arguments: \(result.positional.joined(separator: " "))") + throw CLIError(message: "paste-buffer: unexpected arguments: \(result.positional.joined(separator: " "))") } return make(result) } @@ -371,7 +365,7 @@ enum TmuxCompatArgumentParser { allowsPositional: true ) if result.values["--command"] != nil, !result.positional.isEmpty { - throw invalid("respawn-pane: unexpected arguments: \(result.positional.joined(separator: " "))") + throw CLIError(message: "respawn-pane: unexpected arguments: \(result.positional.joined(separator: " "))") } let command = result.values["--command"] ?? result.positional.joined(separator: " ") return make(result, commandText: command.isEmpty ? nil : command) @@ -406,10 +400,6 @@ enum TmuxCompatArgumentParser { ) } - private static func invalid(_ message: String) -> ArgumentError { - ArgumentError(message: message) - } - private static func scan( _ args: [String], command: String, @@ -424,7 +414,7 @@ enum TmuxCompatArgumentParser { let arg = args[index] if terminated { guard allowsPositional else { - throw invalid("\(command): unexpected argument: \(arg)") + throw CLIError(message: "\(command): unexpected argument: \(arg)") } result.positional.append(arg) index += 1 @@ -441,13 +431,13 @@ enum TmuxCompatArgumentParser { value = String(arg.dropFirst(option.count + 1)) } else { guard index + 1 < args.count, args[index + 1] != "--", !args[index + 1].hasPrefix("-") else { - throw invalid("\(command): \(option) requires a value") + throw CLIError(message: "\(command): \(option) requires a value") } value = args[index + 1] index += 1 } guard !value.isEmpty else { - throw invalid("\(command): \(option) requires a value") + throw CLIError(message: "\(command): \(option) requires a value") } result.values[option] = value index += 1 @@ -459,10 +449,10 @@ enum TmuxCompatArgumentParser { continue } if arg.hasPrefix("-") { - throw invalid("\(command): unknown option '\(arg)'") + throw CLIError(message: "\(command): unknown option '\(arg)'") } guard allowsPositional else { - throw invalid("\(command): unexpected argument: \(arg)") + throw CLIError(message: "\(command): unexpected argument: \(arg)") } result.positional.append(arg) index += 1 diff --git a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift index a21270d505b1..3db7266fd97f 100644 --- a/cmuxCLITests/CLITmuxCompatBufferContentTests.swift +++ b/cmuxCLITests/CLITmuxCompatBufferContentTests.swift @@ -171,25 +171,25 @@ struct CLITmuxCompatArgumentParserTests { ["--surface="] ]) func rejectsMalformedClearHistory(arguments: [String]) { - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: CLIError.self) { try TmuxCompatArgumentParser.parseClearHistory(arguments) } } @Test func rejectsUnknownAndMissingValuesAcrossCommands() { - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: CLIError.self) { try TmuxCompatArgumentParser.parsePasteBuffer(["--typo"]) } - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: CLIError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--command"]) } - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: CLIError.self) { try TmuxCompatArgumentParser.parseRespawnPane(["--commnad", "echo hi"]) } - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: CLIError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["--pritn", "done"]) } - #expect(throws: TmuxCompatArgumentParser.ArgumentError.self) { + #expect(throws: CLIError.self) { try TmuxCompatArgumentParser.parseDisplayMessage(["done", "--typo"]) } } From d31cb748bcf6be69261fe567d723d55e71e56dca Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:24:52 -0700 Subject: [PATCH 66/88] fix(ci): preserve runtime source root in CLI hooks --- cmuxCLITests/BundledCLITestSupport.swift | 14 ++++++++++++++ .../CLIClaudeHookTimeoutRegressionTests.swift | 14 ++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cmuxCLITests/BundledCLITestSupport.swift b/cmuxCLITests/BundledCLITestSupport.swift index d77215d05099..f6deab95c3d9 100644 --- a/cmuxCLITests/BundledCLITestSupport.swift +++ b/cmuxCLITests/BundledCLITestSupport.swift @@ -7,6 +7,20 @@ import XCTest /// has no app host, so it resolves it from `CMUX_CLI_PATH` or relative to the /// built products directory the bundle itself was copied into. enum BundledCLITestSupport { + static func ciRuntimeSourceRootEnvironment() -> [String: String] { + let environment = ProcessInfo.processInfo.environment + guard let runtimeRoot = [ + environment["CMUX_CI_RUNTIME_SOURCE_ROOT"], + environment["TEST_RUNNER_CMUX_CI_RUNTIME_SOURCE_ROOT"], + ].compactMap({ $0 }).first(where: { !$0.isEmpty }) else { + return [:] + } + return [ + "CMUX_CI_RUNTIME_SOURCE_ROOT": runtimeRoot, + "TEST_RUNNER_CMUX_CI_RUNTIME_SOURCE_ROOT": runtimeRoot, + ] + } + static func repositoryRoot(file: StaticString = #filePath) -> URL { let environment = ProcessInfo.processInfo.environment var roots: [URL] = [] diff --git a/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift b/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift index eccb8368fca4..7c8bbc6ef4c1 100644 --- a/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift +++ b/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift @@ -1391,9 +1391,10 @@ struct CLIClaudeHookTimeoutRegressionTests { "PATH": "/usr/bin:/bin:/usr/sbin:/sbin", "CMUX_CLI_SENTRY_DISABLED": "1", ] - if let runtimeRoot = ProcessInfo.processInfo.environment["CMUX_CI_RUNTIME_SOURCE_ROOT"] { - environment["CMUX_CI_RUNTIME_SOURCE_ROOT"] = runtimeRoot - } + environment.merge( + BundledCLITestSupport.ciRuntimeSourceRootEnvironment(), + uniquingKeysWith: { _, newValue in newValue } + ) environment.merge(producer.environment, uniquingKeysWith: { _, value in value }) let result = runCodexHookProcess( executablePath: cliPath, @@ -1464,9 +1465,10 @@ struct CLIClaudeHookTimeoutRegressionTests { "CMUX_CLI_SENTRY_DISABLED": "1", "FAKE_CLAUDE_SETTINGS_OUTPUT": settings.path, ] - if let runtimeRoot = ProcessInfo.processInfo.environment["CMUX_CI_RUNTIME_SOURCE_ROOT"] { - environment["CMUX_CI_RUNTIME_SOURCE_ROOT"] = runtimeRoot - } + environment.merge( + BundledCLITestSupport.ciRuntimeSourceRootEnvironment(), + uniquingKeysWith: { _, newValue in newValue } + ) return environment } From c6d3a12572c4a07667f6e993d4f1f104230bffa8 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:48:54 -0700 Subject: [PATCH 67/88] fix(tests): remove duplicate Codex environment helper --- ...otifyProcessIntegrationRegressionTests.swift | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 0d5471724271..27119d9810b6 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -9616,23 +9616,6 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { } } - private func codexLaunchEnvironment( - context: ClaudeHookContext, - sessionId: String, - observedHookPID: String? = nil - ) -> [String: String] { - var environment = agentLaunchEnvironment( - context: context, - kind: "codex", - executable: "/usr/local/bin/codex", - arguments: ["/usr/local/bin/codex", "--model", "gpt-5.4"] - ) - if let observedHookPID { - environment["CMUX_CODEX_HOOK_PID"] = observedHookPID - } - return environment - } - func agentLaunchEnvironment( context: ClaudeHookContext, kind: String, From 5466e3510b09e380a336dafb73b49588d2027309 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:06:54 -0700 Subject: [PATCH 68/88] fix(tests): keep XCTest JSON assertions --- .../CLINotifyProcessIntegrationRegressionTests+JSON.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift index 4cf9c41680a1..59f17fe74d1f 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift @@ -1,5 +1,5 @@ import Foundation -import Testing +import XCTest extension CLINotifyProcessIntegrationRegressionTests { func codexLaunchEnvironment( @@ -25,14 +25,14 @@ extension CLINotifyProcessIntegrationRegressionTests { // app-host crash, so one slow CLI turned a whole tolerant shard red. func notificationRows(from stdout: String) throws -> [[String: Any]] { let data = Data(stdout.utf8) - return try #require( + return try XCTUnwrap( (try? JSONSerialization.jsonObject(with: data, options: [])) as? [[String: Any]], "Expected notification JSON array, got: \(stdout)" ) } func jsonPayload(from stdout: String) throws -> [String: Any] { let data = Data(stdout.utf8) - return try #require( + return try XCTUnwrap( (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any], "Expected JSON object, got: \(stdout)" ) From 375824dc69d98182f1b378bd5cffbebddaffff05 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:22:04 -0700 Subject: [PATCH 69/88] fix(tests): keep Codex fixture helper in XCTest suite --- ...ProcessIntegrationRegressionTests+JSON.swift | 17 ----------------- ...otifyProcessIntegrationRegressionTests.swift | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift index 59f17fe74d1f..507692e63eee 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests+JSON.swift @@ -2,23 +2,6 @@ import Foundation import XCTest extension CLINotifyProcessIntegrationRegressionTests { - func codexLaunchEnvironment( - context: ClaudeHookContext, - sessionId _: String, - observedHookPID: String? = nil - ) -> [String: String] { - var environment = agentLaunchEnvironment( - context: context, - kind: "codex", - executable: "/usr/local/bin/codex", - arguments: ["/usr/local/bin/codex", "--model", "gpt-5.4"] - ) - if let observedHookPID { - environment["CMUX_CODEX_HOOK_PID"] = observedHookPID - } - return environment - } - // A CLI that printed nothing or non-JSON is an ordinary assertion failure. // Letting `JSONSerialization` throw here made XCTest record a thrown error, // which the CI classifier counts as "unexpected" and treats like an diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 27119d9810b6..0d5471724271 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -9616,6 +9616,23 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { } } + private func codexLaunchEnvironment( + context: ClaudeHookContext, + sessionId: String, + observedHookPID: String? = nil + ) -> [String: String] { + var environment = agentLaunchEnvironment( + context: context, + kind: "codex", + executable: "/usr/local/bin/codex", + arguments: ["/usr/local/bin/codex", "--model", "gpt-5.4"] + ) + if let observedHookPID { + environment["CMUX_CODEX_HOOK_PID"] = observedHookPID + } + return environment + } + func agentLaunchEnvironment( context: ClaudeHookContext, kind: String, From 2232206da826b3ead7e95c52d0b2d81895dc3413 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:50:05 -0700 Subject: [PATCH 70/88] fix(tests): mark unused Codex session id --- cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 0d5471724271..7304fd1de660 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -9618,7 +9618,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { private func codexLaunchEnvironment( context: ClaudeHookContext, - sessionId: String, + sessionId _: String, observedHookPID: String? = nil ) -> [String: String] { var environment = agentLaunchEnvironment( From c282eb4fff5b4c81e54f1e3088f7065c0523fa28 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:56:53 -0700 Subject: [PATCH 71/88] fix(ios): retain cloud routes with shared tab shell --- .../WorkspaceShellView.swift | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index fe92fbf35214..42719f44aedb 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -207,6 +207,8 @@ private struct WorkspaceShellRenderPresentation { struct WorkspaceShellView: View { #if os(iOS) && DEBUG @Environment(\.releaseGateUIProbe) var releaseGateUIProbe + /// The Cloud tab's content, supplied by the composition root. + @Environment(\.mobileCloudTabContent) private var cloudTabContent #endif @Bindable var store: CMUXMobileShellStore let signOut: @MainActor @Sendable () -> Void @@ -316,6 +318,8 @@ struct WorkspaceShellView: View { return feedNavigationPath.isEmpty case .notifications: return notificationNavigationPath.isEmpty + case .cloud: + return true case .search: return primarySearchNavigationPath.wrappedValue.isEmpty } @@ -1083,7 +1087,12 @@ struct WorkspaceShellView: View { projection: notificationFeedProjection, selectedMacDeviceIDs: selectedMacDeviceIDs ) - case .workspaces, .search: + case .cloud where cloudTabContent != nil: + // The sidebar column already owns a navigation container and + // the destination bar, so Cloud renders inside it rather than + // bringing a stack of its own. + cloudTabContent?.makeEmbeddedView() + case .workspaces, .cloud, .search: workspaceList( navigationStyle: .sidebar, searchText: primarySearchCoordinator.searchDestinationText(for: .workspaces), @@ -1093,7 +1102,9 @@ struct WorkspaceShellView: View { } } .toolbar { - if splitSidebarDestination == .notifications || splitSidebarDestination == .feed { + if splitSidebarDestination == .notifications + || splitSidebarDestination == .feed + || (splitSidebarDestination == .cloud && cloudTabContent != nil) { ToolbarItem(placement: .topBarTrailing) { // Notifications has no WorkspaceListView toolbar, so the // sidebar owns its trailing control directly in this path. @@ -1165,7 +1176,10 @@ struct WorkspaceShellView: View { workspacesTitle: L10n.string("mobile.tabs.workspaces", defaultValue: "Workspaces"), feedTitle: L10n.string("mobile.tabs.feed", defaultValue: "Feed"), notificationsTitle: notificationsSegmentTitle(unreadCount: unreadCount), - showsNotifications: !displaySettings.feedReplacesNotifications + showsNotifications: !displaySettings.feedReplacesNotifications, + cloudTitle: cloudTabContent == nil + ? nil + : L10n.string("mobile.tabs.cloud", defaultValue: "Cloud") ) } if #available(iOS 26.0, *) { @@ -1193,6 +1207,8 @@ struct WorkspaceShellView: View { let feedTitle: String let notificationsTitle: String let showsNotifications: Bool + /// Nil in a build without Cloud, which omits the destination. + let cloudTitle: String? var body: some View { HStack(spacing: 0) { @@ -1213,6 +1229,13 @@ struct WorkspaceShellView: View { accessibilityID: "MobileSplitSidebarNotifications" ) } + if let cloudTitle { + destinationButton( + .cloud, + title: cloudTitle, + accessibilityID: "MobileSplitSidebarCloud" + ) + } } .frame(minHeight: 44) .fixedSize(horizontal: true, vertical: false) @@ -1750,6 +1773,8 @@ struct WorkspaceShellView: View { if notificationNavigationPath.last != workspaceID { notificationNavigationPath = [workspaceID] } + case .cloud: + break case .search: break } @@ -1821,6 +1846,7 @@ struct WorkspaceShellView: View { case .workspaces: .workspaces case .feed: .feed case .notifications: .notifications + case .cloud: .cloud case .search: .search } } From a9db8f450d34434d20b476abce24dd3d3f862256 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:07:28 -0700 Subject: [PATCH 72/88] fix: restore cloud workspace routing --- .../CmuxMobileShellUI/WorkspaceShellView.swift | 15 ++++++++++++--- cmuxTests/SessionPersistenceTests.swift | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 42719f44aedb..1ba01b76806d 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -1423,6 +1423,7 @@ struct WorkspaceShellView: View { createWorkspace: resolvedCreateWorkspace, createWorkspaceInGroup: resolvedCreateWorkspaceInGroup, createWorkspaceGroup: resolvedCreateWorkspaceGroup, + createWorkspaceOnCloudMachine: { createWorkspaceOnExternalHost(onHost: $0) }, newWorkspaceComputerTargets: newWorkspaceComputerTargets, createWorkspaceOnComputer: { target, kind in createWorkspace(on: target, kind: kind, create: resolvedCreateWorkspace) @@ -1787,7 +1788,7 @@ struct WorkspaceShellView: View { ) -> Bool { let previousTab = selectedPrimaryTab if (selectedPrimaryTab == .search || primarySearchCoordinator.isPresented), - tab.searchScope != nil { + tab != .search { primarySearchCoordinator.deactivateCurrentSearch() } beforeSelection() @@ -2053,11 +2054,19 @@ struct WorkspaceShellView: View { toggleSidebar: (() -> Void)? = nil, showsSidebarToggle: Bool = false ) -> some View { + // A Cloud machine's workspace makes its new workspace on that machine, + // whether or not a Mac is connected. + let externalHostID = workspaceID.flatMap { store.externalHostID(ofWorkspace: $0) } WorkspaceDetailContainer( store: store, workspaceID: workspaceID, - createWorkspace: createWorkspace, - canCreateWorkspace: canCreateWorkspaceForSelection, + createWorkspace: externalHostID == nil || workspaceID == nil + ? createWorkspace + : { [workspaceID] in + if let workspaceID { createWorkspaceOnExternalHost(beside: workspaceID) } + }, + canCreateWorkspace: externalHostID.map { store.externalHostIsConnected($0) } + ?? canCreateWorkspaceForSelection, renameWorkspace: renameWorkspaceClosure, customizeWorkspace: customizeWorkspaceClosure, setWorkspaceUnread: setWorkspaceUnreadClosure, diff --git a/cmuxTests/SessionPersistenceTests.swift b/cmuxTests/SessionPersistenceTests.swift index ec4f684e6bfa..c33608922cea 100644 --- a/cmuxTests/SessionPersistenceTests.swift +++ b/cmuxTests/SessionPersistenceTests.swift @@ -4047,7 +4047,7 @@ final class SocketListenerAcceptPolicyTests: XCTestCase { ) } - func testClaudeResumeCommandStripsQuotedCmuxNodeOptionsRestoreModuleInHomeWithSpace() { + func testClaudeResumeCommandStripsQuotedCmuxNodeOptionsRestoreModuleInHomeWithSpaceAndHook() { let snapshot = SessionRestorableAgentSnapshot( kind: .claude, sessionId: "claude-session-node-options-space", From 0519f3edd443dfe52178293ad0732e3f302852cd Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:53:02 -0700 Subject: [PATCH 73/88] fix(cli): keep auto-naming parser helper static --- CLI/CMUXCLI+AutoNaming.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index 6a928da2b70e..952a5fc5eb23 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -242,7 +242,7 @@ struct CodexAutoNamingArguments: Sendable { return arguments } - func removingComment(from rawLine: Substring) -> Substring { + private static func removingComment(from rawLine: Substring) -> Substring { var quote: Character? var escaped = false for index in rawLine.indices { @@ -266,7 +266,7 @@ struct CodexAutoNamingArguments: Sendable { } } return rawLine - } + } private static func providerOverrides( from toml: String, usesTemporaryConfig: Bool From 0d7c43e4d59ba1e99fc562af3920538beef9ae12 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:23:37 -0700 Subject: [PATCH 74/88] fix(cli): bind cloud transfer retry error --- CLI/CMUXCLI+VMTransfer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLI/CMUXCLI+VMTransfer.swift b/CLI/CMUXCLI+VMTransfer.swift index 772ab10c1ff0..ae2904f5312e 100644 --- a/CLI/CMUXCLI+VMTransfer.swift +++ b/CLI/CMUXCLI+VMTransfer.swift @@ -623,7 +623,7 @@ extension CMUXCLI { excludes: excludes, client: client ) - } catch is VMSCPGrantTransportFailure { + } catch let error as VMSCPGrantTransportFailure { // A dropped grant request is recoverable while watching: keep // the settled local snapshot pending and retry without making // the user edit the file again. Other failures remain fatal so From b1f0abb8f625ed5caa8d58830751df783b3c145b Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:24:39 -0700 Subject: [PATCH 75/88] test: retain multi-message inbox coverage --- cmuxCLITests/CLIAgentMessageCommandTests.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmuxCLITests/CLIAgentMessageCommandTests.swift b/cmuxCLITests/CLIAgentMessageCommandTests.swift index 9318f3dc0a64..9b5247513a6e 100644 --- a/cmuxCLITests/CLIAgentMessageCommandTests.swift +++ b/cmuxCLITests/CLIAgentMessageCommandTests.swift @@ -99,6 +99,11 @@ struct CLIAgentMessageCommandTests { "state": "queued", "sender_name": "coordinator", "body": "first line\nsecond line", + ], [ + "id": "fedcba9876543210", + "state": "queued", + "sender_name": "reviewer", + "body": "another message", ]], ], ] @@ -109,8 +114,9 @@ struct CLIAgentMessageCommandTests { #expect(listParams["surface"] as? String == "workspace:2") #expect(listParams["state"] as? String == "queued") let readParams = try #require(run.request("agent.message.mark_read")?["params"] as? [String: Any]) - #expect(readParams["ids"] as? [String] == ["abcdef0123456789"]) + #expect(readParams["ids"] as? [String] == ["abcdef0123456789", "fedcba9876543210"]) #expect(run.result.stdout.contains("abcdef01 coordinator: first line"), Comment(rawValue: run.result.stdout)) + #expect(run.result.stdout.contains("fedcba98 reviewer: another message"), Comment(rawValue: run.result.stdout)) #expect(!run.result.stdout.contains("second line")) } From 3eaef72e3b13e999a18e8bc02c56ce9e1dc52f61 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:39:42 -0700 Subject: [PATCH 76/88] fix: handle cloud tab in notification preview --- .../CmuxMobileShellUI/NotificationFeedPreviewView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift index 7fd5b7b49407..4b31783af7d5 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift @@ -200,7 +200,7 @@ public struct NotificationFeedPreviewView: View { private var previewRootToolbarVisible: Bool { switch selectedTab { - case .workspaces, .feed: + case .workspaces, .feed, .cloud: true case .notifications: notificationRoute == nil @@ -211,7 +211,7 @@ public struct NotificationFeedPreviewView: View { private var previewTabBarVisibility: Visibility { switch selectedTab { - case .workspaces, .feed: + case .workspaces, .feed, .cloud: .automatic case .notifications: notificationRoute == nil ? .automatic : .hidden From 8ed060d769a112ccc2fad7646b5ad4c7267a3e08 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:45:39 -0700 Subject: [PATCH 77/88] fix: restore workspace destination return --- .../Sources/CmuxMobileShellUI/WorkspaceShellView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index 1ba01b76806d..a86aed843970 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -2057,7 +2057,7 @@ struct WorkspaceShellView: View { // A Cloud machine's workspace makes its new workspace on that machine, // whether or not a Mac is connected. let externalHostID = workspaceID.flatMap { store.externalHostID(ofWorkspace: $0) } - WorkspaceDetailContainer( + return WorkspaceDetailContainer( store: store, workspaceID: workspaceID, createWorkspace: externalHostID == nil || workspaceID == nil From 319fe4c283ad7db8656a681a662932228bc7b4ec Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:30:43 -0700 Subject: [PATCH 78/88] fix(ios): use deployment-safe toolbar visibility --- .../CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift | 2 +- .../CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift index e45b9516be19..3e7e2c97ed94 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchNavigationStack.swift @@ -19,7 +19,7 @@ struct MobilePrimarySearchNavigationStack: View { var body: some View { NavigationStack(path: $path) { root() - .toolbar(rootNavigationBarVisibility, for: .navigationBar) + .mobileToolbarVisibility(rootNavigationBarVisibility, for: .navigationBar) .modifier(MobilePrimarySearchLifecycleModifier( scope: searchCoordinator.scope, update: { scope, isSearching in diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift index c18cc9f27976..40664840df3a 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabNavigationHost.swift @@ -29,7 +29,7 @@ struct MobilePrimaryTabNavigationHost: V .toolbar { toolbar } - .toolbar(toolbarVisibility, for: .navigationBar) + .mobileToolbarVisibility(toolbarVisibility, for: .navigationBar) .mobileToolbarVisibility(tabBarVisibility, for: .tabBar) } } From 584e73cc6508f497118721d8e1de2f77b2922a85 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 1 Oct 2026 17:18:13 -0400 Subject: [PATCH 79/88] test: drop the uncompilable CLI half of the Cloud link-failure copy test #16149's cloudLinkFailureCopy calls CMUXCLI.vmTreeLines from cmuxTests, where CMUXCLI is the app's CmuxTuiRemoteRouting alias and the CLI executable is not linked, so cmuxTests stopped compiling. Keep the payload assertions; the CLI change has no unit seam in either test target. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FrR7YbsQtGw2eFtDeyiTcK --- .../SurfaceMachineIDDeviceEncodingTests.swift | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift b/cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift index 08ca27ee1b3f..fc43762a4b8e 100644 --- a/cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift +++ b/cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift @@ -119,7 +119,7 @@ struct SurfaceMachineIDDeviceEncodingTests { #expect(local["local"] as? Bool == true) } - @Test("Cloud tree payload and CLI use user-facing link failure copy") + @Test("Cloud tree payload carries user-facing link failure copy") func cloudLinkFailureCopy() { let info = machineInfo(.cloud("brave-otter"), linkState: .error, presence: nil) var failed = info @@ -127,19 +127,9 @@ struct SurfaceMachineIDDeviceEncodingTests { let payload = TerminalController.surfaceMachinePayload(failed) #expect(payload["link_error"] as? String == "cloud_api_unavailable") #expect(payload["link_error_message"] as? String == "cmux cannot reach the Cloud service for this machine right now.") - - let lines = CMUXCLI.vmTreeLines( - machine: [ - "id": "brave-otter", - "status": "running", - "link_state": "error", - "link_error": "cloud_api_unavailable", - "link_error_message": "cmux cannot reach the Cloud service for this machine right now." - ], - resources: [] - ) - #expect(lines.contains { $0.contains("cmux cannot reach the Cloud service") }) - #expect(!lines.contains { $0.contains("cloud_api_unavailable") }) + // The CLI half (CMUXCLI.vmTreeLines preferring link_error_message) + // lives in the CLI executable, which this app test target cannot + // link; here CMUXCLI is the app's CmuxTuiRemoteRouting alias. } @Test("Build labels qualify dev, nightly, rc, and tagged instances; stable stays bare") From 2259c1caeb888dc090a1b8bf8956458da92c16a1 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:49:10 -0700 Subject: [PATCH 80/88] fix: preserve cloud and recovery toolbar behavior --- .../WorkspaceShellView.swift | 13 +++++-- .../CLIExplicitSurfaceRoutingTests.swift | 34 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index a86aed843970..9516ae20ac7f 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -565,7 +565,10 @@ struct WorkspaceShellView: View { } } cloud: { // Supplied by the composition root; the shell owns no Cloud code. - cloudTabContent?.makeView() + // The host above already owns the compact navigation stack. + // Mount Cloud's embedded variant here so its section does + // not create a second stack inside the shared toolbar host. + cloudTabContent?.makeEmbeddedView() } search: { primarySearchTabContent(presentation: presentation) } @@ -1509,7 +1512,8 @@ struct WorkspaceShellView: View { hasStore: true, connectionRequiresReauth: store.connectionRequiresReauth, connectionRecoveryFailed: store.connectionRecoveryFailed, - isRecoveringConnection: store.isRecoveringConnection, + isRecoveringConnection: store.isRecoveringConnection + && store.workspaceListShowsForegroundRecovery, isRecoveringWorkspaceList: store.isRecoveringWorkspaceList, connectionStatus: listConnectionStatus, tailscalePairingRequired: tailscalePairingRequired, @@ -1562,7 +1566,10 @@ struct WorkspaceShellView: View { names[mac.id] = mac.resolvedName } if let buildScope = MobileIOSBuildScope.current() { - names = names.mapValues(buildScope.computerDisplayName) + names = buildScope.computerDisplayNames( + names, + isExternalHost: store.externalHostOwnsHost + ) } // After the build-scope mapping: the dev tag suffix names a cmux Mac // build, and an SSH host is not one. diff --git a/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift b/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift index 930c04ea0342..9cb5e19a1785 100644 --- a/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift +++ b/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift @@ -43,6 +43,40 @@ struct CLIExplicitSurfaceRoutingTests { ) } + @Test func vmTreeUsesCloudLinkErrorMessageInHumanOutput() throws { + let execution = try runMockCommand( + arguments: ["vm", "tree"], + socketName: "vm-tree-link-error" + ) { line in + guard let request = Self.jsonObject(line), + let id = request["id"] as? String, + request["method"] as? String == "surface.catalog" else { + return Self.malformedRequestResponse(raw: line) + } + return Self.v2Response( + id: id, + ok: true, + result: [ + "machines": [[ + "id": "brave-otter", + "status": "running", + "link_state": "error", + "link_error": "cloud_api_unavailable", + "link_error_message": "cmux cannot reach the Cloud service for this machine right now." + ]], + "resources": [] + ] + ) + } + + #expect(execution.result.status == 0, Comment(rawValue: execution.result.stderr)) + #expect( + execution.result.stdout.contains("cmux cannot reach the Cloud service"), + Comment(rawValue: execution.result.stdout) + ) + #expect(!execution.result.stdout.contains("cloud_api_unavailable")) + } + @Test func sendKeyCommandsRejectExtraArgumentsWithoutSocketRequest() throws { let cases: [[String]] = [ ["send-key", "--surface", Self.targetSurfaceRef, "ctrl+c", "enter"], From 2b7821a1699991f9d718f360406825f0cc384183 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:00:54 -0700 Subject: [PATCH 81/88] chore: drop unrelated XCTest fixture edits --- .../CLINotifyProcessIntegrationRegressionTests.swift | 12 ++++++------ cmuxTests/SessionPersistenceTests.swift | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 79672c25bcea..2769d67c38ad 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -2864,15 +2864,15 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { try [ #"{"type":"turn_context","payload":{"turn_id":"old-turn"}}"#, #"{"type":"event_msg","payload":{"type":"task_started","turn_id":"old-turn"}}"#, - #"{"type":"event_msg","payload":{"type":"turn_complete","turn_id":"old-turn","last_agent_message":"old done"}}"#, + #"{"type":"event_msg","payload":{"type":"turn_aborted","turn_id":"old-turn"}}"#, #"{"type":"turn_context","payload":{"turn_id":"current-turn"}}"#, #"{"type":"event_msg","payload":{"type":"task_started","turn_id":"current-turn"}}"#, ].joined(separator: "\n").write(to: transcriptURL, atomically: true, encoding: .utf8) - // A successful terminal record replays Stop through the asynchronous - // monitor; turn_aborted only prunes stale depth at the next hook. - // Wait for the old-turn journal before the current Stop to preserve - // the monitor ordering regression covered by upstream PR #10143. + // The late terminal monitor is asynchronous. Wait for its old-turn + // completion journal before invoking the current Stop; this preserves + // the race discriminator covered by upstream PR #10143 rather than + // allowing the test to pass or fail on monitor scheduling. XCTAssertTrue( waitForMockSocketCommand(in: context.state) { AgentJournalAppendCapture.captures(in: [$0]).contains { @@ -9703,7 +9703,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { private func codexLaunchEnvironment( context: ClaudeHookContext, - sessionId _: String, + sessionId: String, observedHookPID: String? = nil ) -> [String: String] { var environment = agentLaunchEnvironment( diff --git a/cmuxTests/SessionPersistenceTests.swift b/cmuxTests/SessionPersistenceTests.swift index 0233b30e42a5..a3f99862219c 100644 --- a/cmuxTests/SessionPersistenceTests.swift +++ b/cmuxTests/SessionPersistenceTests.swift @@ -4047,7 +4047,7 @@ final class SocketListenerAcceptPolicyTests: XCTestCase { ) } - func testClaudeResumeCommandStripsQuotedCmuxNodeOptionsRestoreModuleInHomeWithSpaceAndHook() { + func testClaudeResumeCommandStripsQuotedCmuxNodeOptionsRestoreModuleInHomeWithSpace() { let snapshot = SessionRestorableAgentSnapshot( kind: .claude, sessionId: "claude-session-node-options-space", From 370d99f45fa1b1816fb9feacb2013df8f76e5105 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:27:57 -0700 Subject: [PATCH 82/88] test: exercise unavailable Cloud link copy --- cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift b/cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift index fc43762a4b8e..3e45bd741cfb 100644 --- a/cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift +++ b/cmuxTests/SurfaceMachineIDDeviceEncodingTests.swift @@ -121,7 +121,7 @@ struct SurfaceMachineIDDeviceEncodingTests { @Test("Cloud tree payload carries user-facing link failure copy") func cloudLinkFailureCopy() { - let info = machineInfo(.cloud("brave-otter"), linkState: .error, presence: nil) + let info = machineInfo(.cloud("brave-otter"), linkState: .unavailable, presence: nil) var failed = info failed.linkError = "cloud_api_unavailable" let payload = TerminalController.surfaceMachinePayload(failed) From 2aed0e6fede70cd4a487342fa80983399b01d82f Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:51:45 -0700 Subject: [PATCH 83/88] fix: preserve public Foundation import after main merge --- .../Values/RemotePasteFileTransferPolicy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Values/RemotePasteFileTransferPolicy.swift b/Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Values/RemotePasteFileTransferPolicy.swift index e7e3c58c9067..a36d1e7d1658 100644 --- a/Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Values/RemotePasteFileTransferPolicy.swift +++ b/Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Values/RemotePasteFileTransferPolicy.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation /// Owns the private remote directory and cleanup contract for pasted files. public struct RemotePasteFileTransferPolicy: Equatable, Sendable { From 8e6edd7ae814eb7edc0e3cd6ff9ce247320af040 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:05:24 -0700 Subject: [PATCH 84/88] fix: compile sidebar usage owner selection --- Sources/Sidebar/SidebarAgentUsageCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sidebar/SidebarAgentUsageCoordinator.swift b/Sources/Sidebar/SidebarAgentUsageCoordinator.swift index 60e4060cce10..ec0476c78f30 100644 --- a/Sources/Sidebar/SidebarAgentUsageCoordinator.swift +++ b/Sources/Sidebar/SidebarAgentUsageCoordinator.swift @@ -283,7 +283,7 @@ final class SidebarAgentUsageCoordinator { // panes' numbers across refreshes that the user did not cause. let ownerPair = sessions .filter { $0.value.workspaceID == row.workspaceID && $0.value.source == row.source } - .max { ($0.value.lastEventAt, $0.key) < ($1.value.lastEventAt, $1.key) }? + .max { ($0.value.lastEventAt, $0.key) < ($1.value.lastEventAt, $1.key) } let owner = ownerPair?.value let usage = isEnabled ? owner?.snapshot.map(Self.sidebarUsage) : nil if usage == nil, !rowsShowingUsage.contains(row) { return } From 81f27556e779d551dc4fbff9d161685efb6a4526 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:13:26 -0700 Subject: [PATCH 85/88] fix: use local SSH shell quoting helper --- Sources/TerminalSSHSessionDetector.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/TerminalSSHSessionDetector.swift b/Sources/TerminalSSHSessionDetector.swift index 5c4cc39c0d06..11197e17e4a6 100644 --- a/Sources/TerminalSSHSessionDetector.swift +++ b/Sources/TerminalSSHSessionDetector.swift @@ -205,7 +205,7 @@ struct DetectedSSHSession: Equatable, Sendable { private func prepareRemotePasteDirectory() throws { let result = try Self.runProcess( executable: "/usr/bin/ssh", - arguments: sshArguments(command: "sh -c \(remotePastePolicy.maintenanceScript().shellSingleQuoted)"), + arguments: sshArguments(command: "sh -c \(Self.shellSingleQuoted(remotePastePolicy.maintenanceScript()))"), timeout: 12 ) guard result.status == 0 else { @@ -231,7 +231,7 @@ struct DetectedSSHSession: Equatable, Sendable { private func finalizeRemotePasteFile(_ remotePath: String) throws { let result = try Self.runProcess( executable: "/usr/bin/ssh", - arguments: sshArguments(command: "sh -c \(remotePastePolicy.finalizeScript(for: remotePath).shellSingleQuoted)"), + arguments: sshArguments(command: "sh -c \(Self.shellSingleQuoted(remotePastePolicy.finalizeScript(for: remotePath)))"), timeout: 8 ) guard result.status == 0 else { From b21a41a30b85f1fbff9151424e9cb2937d145284 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:26:49 -0700 Subject: [PATCH 86/88] fix: restore custom sidebar preview assets --- .../agents-board-dark.png | Bin 0 -> 10780 bytes .../agents-board-light.png | Bin 0 -> 10868 bytes .../btop-agents-dark.png | Bin 0 -> 10796 bytes .../btop-agents-light.png | Bin 0 -> 10929 bytes .../panel-sessions-dark.png | Bin 0 -> 11042 bytes .../panel-sessions-light.png | Bin 0 -> 10941 bytes .../panel-subagents-dark.png | Bin 0 -> 9850 bytes .../panel-subagents-light.png | Bin 0 -> 9703 bytes .../panel-todo-dark.png | Bin 0 -> 10656 bytes .../panel-todo-light.png | Bin 0 -> 10553 bytes .../workspaces-dark.png | Bin 0 -> 9468 bytes .../workspaces-light.png | Bin 0 -> 9617 bytes 12 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/agents-board-dark.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/agents-board-light.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/btop-agents-dark.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/btop-agents-light.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-sessions-dark.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-sessions-light.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-subagents-dark.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-subagents-light.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-todo-dark.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-todo-light.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/workspaces-dark.png create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/workspaces-light.png diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/agents-board-dark.png b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/agents-board-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1f4faa50af657b18bc7587076b05d77a3761e943 GIT binary patch literal 10780 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 ppvM9y?>{wj{#UExyVQM#)ta0vha literal 0 HcmV?d00001 diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/agents-board-light.png b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/agents-board-light.png new file mode 100644 index 0000000000000000000000000000000000000000..e9961292cf5dee92be65fe40fc48fa7d6a1b555b GIT binary patch literal 10868 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 literal 0 HcmV?d00001 diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-sessions-dark.png b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-sessions-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..30f381a423982c79610cd517c8b8097e71a31365 GIT binary patch literal 11042 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 literal 0 HcmV?d00001 diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-sessions-light.png b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-sessions-light.png new file mode 100644 index 0000000000000000000000000000000000000000..8f1568381e18c0e6e494b24506c77499351a251a GIT binary patch literal 10941 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 literal 0 HcmV?d00001 diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-subagents-dark.png b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-subagents-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c9ac39ade56495e644d414c094dcdf6026f81c89 GIT binary patch literal 9850 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 P2z=yal%;bepT7AYvj;&e literal 0 HcmV?d00001 diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-todo-dark.png b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-todo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8ea4d62f7080ef799ffefe6b089c4024e0fbaa4f GIT binary patch literal 10656 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 JzRdmS{|CxL8)*Ol literal 0 HcmV?d00001 diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-todo-light.png b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/panel-todo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..5e47a41756186843d3fbcc3e07540756220327cd GIT binary patch literal 10553 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 literal 0 HcmV?d00001 diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/workspaces-light.png b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Resources/CustomSidebarTemplatePreviews/workspaces-light.png new file mode 100644 index 0000000000000000000000000000000000000000..e4535c53a542fa541f9fd312ec8322d6f5233cc2 GIT binary patch literal 9617 zcmb7Kc|4SFzkZMxL$sWp@Wl#|BZ=bZWLp6Bz-J@@my@9TSA-|PFlFwoOrqUWZEAc#rx=JneU zL>U4>6pnP1;L1&dF&YRuYo>Yqn$cr4cFf_b;b23^n%ZAF*Z!j5h@dN{eHoEaUUo0H z)WUb{wrM}w1VOdz=)&8t@P>XMEOX#VW=G{M)<@z-*R9Tn3P>J#M{Gd)?M$nlu)BiN z|IuC}n-G(KTM3WS4xgYjPk z<}hHtgEVn^0WUi80GrPz&tC5bLFaK4%8wT!5b*wCPxkvTPM^6iVEi)9IV(Hc4mMug zS+~xXpF1_}_ZJ0pL{8tz+M3|yt)}17h`hOl z+3HnFi0)xpA}7yafwk1$%Fas6*c+A)X=zLd1VU9W)R`kq(Rb2G>t%QqPxNW9D=1n% zoLf>-Qcgq1{q#kdho0W~F!3`p_qQ%+i)fn~udT5N>F49e+s1RV^8QLcG@)Rvy~vjOtvl(qPs-Egq}qcPdJ{muf{!Q%F)U!@>d400`aIm()0$2htD zFI;RnW{NoG+*cB*Hj5S=@j|m#8&-)REoyu*PiptW%gd#GzD$kgVTD9Ysb|Zp96HlP zEv7KwdzA6}vgJy{FrJF!Lex@S&tdZ6t?%?X|S>zJ7hndN)aDpswARZf)iV3%+Aa&|O0_;PJME zeSfldcr+iT0xK>hWr$6C_Ed%jKa|CEe@oFgzuz#>p}?%dest)qU^wX}13ZUJnE#3F zneFQ<*{40mE6?q*ftJUbxD0&3e03@F+Cgtd*a)^{s|0NMakvXKHfbl2j&o;QZ!95~ zK}ZdoT7s3h zHT$+K^S#R;y4Je^sqNtNV8pDY!o;XAl^P}9UOS&kUfHp!FE5G-&lD^qtc**Ota=@` z7y4ZW+IQ@pm)5FACEBMQ-48>|r zZ+dy>=#qt%l&t9lVix_q9}ow3jpsWQC`r zC@`xf>?OTf?bS#SOM~UUi}7-qc-b!?q?#h@Qf)iZyqwU`pwFLrWvwbUHr8CF^T#aM z;6w@(ubNteLI<)VG(F~A6}Wx5qOZTV-`mT}-~uc7$`c?Ho85#XE0L>Rh{}S34G{T; ziHVlLbwsjsytrlGXQ$=htre0@y&N_TwScy3%{Y+9i3tnmYTzk2#O_$&Q00Eb&BN~B zN-mq8XD!%Vq8E7mnlVLY?E$(`ibOxAjKAWw3N5bJ9l$?+#C4xTPQb6*~0IyXwi4??11qSX%Npr5?kgy&MPCE35rnijc^D z4|0=#Wo1vzaqjKeSrOAR{Ag-iTvmQQso9naVxB0Qo5ZpEUs&l|EPBo~)=*y`4wp8w zz?q=ZE=zT+Q9kq{j4k!&-Q0{bGZ)S`&G$dI_g+1-ySw`>yUc^3i6+vn{b=J%j=pSx z<<1nN2fB~{d7yNjx1zhgezb*jo~-jsK~YItt!kz^Zn)52#sl`%L$@#0F*lTlj6PKo z)QemmOuwQz;CQ)5;Zii8CdX-a4P(S@b4$#r({7_$+SX|-n6h}{A)0gWv<8Oy0g)exCddf9R4-b=iWs}{_C@!PCzv#y-C!bRal%`M$D zrGRtRkN1P_c)F;otMf%OL3NDVvsOzAJe-`Tj?0^Wyna+oO>G`6olZN{E-fV@GIM=l zu}Dff&s0hpen2B)X_@lNv_LuNRDSA_mgZ*b%D|@P>`|VE(I5G0G0ljc`vuvZr?UM&OdZEre;0i z(;z<&6vDM?**iGso4Xu1S)T>cN@bsS{IA)b^`CL?FJ5d**cH{+)ZBbnSZIxMU1rbC zb$a$Zs{G;PZD(h6*sBEEV_a$P-l4O-UyY7hw`{p4h#iQov<;7gOoBPHxiVa=ud@8r zPx~Rj|LfPU!;jk@<5M^t8WB~SKlX6H8I>Sr$;ZRNk#l?hM3Kj4viIEF*=4e<_+W3Z z)TK)@GBR3vL6yE_R7_4sYf#R3x|fc=KE5}&6-9eo=**es<|gbAl9YlEdgF%esW1fp zaZOF*d8#~V=0{9yY+wFh0U(H$A$Os<^&tpq3&DhiSB=W}Wqo?qZ&J!jux(_+|%gI>kyM~6n?I{CWxc(Zi#oGYLSz6}ArEb?>*~p&oSdU!0 zjckg8S7O$+yEi%yUcAtxp`o#qp@xq)jdT=L*VYCl^<*jfAu+qYZwQyHGz-&UIw=a> z0|V+}38n=z1d=;Si=UER^);{3y~foKGP6IImX{S36xxz6A;}3fo^9)ch@*S~6iQPR1G0U#_8c#Pv66_`l0U%*rx8`^I=wP&<9J?esa_^bv>EA>Jx- zJ829tR^+{(jV&~{8A|-&i&r}D=FrT;PyL(@dO61Uxbr|TYRg9?z5~~lJsk1K&=d2b z?4{N%`WcOi!ZLqHD5G3B7N)_;!vU+ZH%?kX1P3INH)c+6w8C4{v(88IfV?WJt>gdA z1B*#0u#%OKa2(0gtTNFJWk$7aC{yJ&T-SzEJC{JBB2W-nV$J`X=qpS z_*Un^vzFp--WF~q-d!JZ8N2a`N7ZV=IjFx-k{z4E&h+3hJJJSB(V+g%Zsi(cJj*eDZ)oQe4)|dA# z#v~uY8$XnIp<$K3VJD4h#EI$bC@z_+OK(i>8xfwU3B=U$*x1|quN!S6BP^x!O$WyF zO!DVN=^ZC5=1G^7ImBU>3uWKbQtS&W6`f*OK1@#6@?8w=*t^$@MbW@wY#`f|9EUX0402lwq}m4;0W%dYnsLw=e%_0CO*i!JWl+ z`db!ar1XKuI$_w#Ek8eBBw?A0i<_H=OU7j-tp@0)*3VV?>DgYmzUJ1}uKp|^1h?_{ zW8d*R2t+KsNS58ao~x>A1M=7A=B#Lnj8VSyrRaM|29-_cjh@|FXBG4&%%4#!>_*tS zjLjrRD04!gI_JW&qDe`>^i^|9VMZpVOx$Rd2i6`;d$CP>Ja~IyX*0$6{e`84g&Da# z*xZimX!S;9CO4P2th97{m92=Npc=!rT(0I~qN1XM>I zA-2;-Kn%f-@#b_pe)7A6yr5SxT3W3;ES8EEBaoy-w5_Y`)yOsQX(;^ zXNF*ncdu4-HHg#twHdWpkLohI9YK;UOp_?0x~e8df?<_V{^yj`--r5$IyWTJ^7_2k6*dW z`a@l9eIf%L`c$2?-#{b=?5K-G^Hh0W~Bc;(4bSy4*-j_^z z@%2``f=df7n7<{@V@GXvNM9`YzEtl?V-ZC$2F7owgX;lwrH%ts}3~VkQA1ZU&a>KY4j zr#P?V@Pf^L0(HR%l4x*NBj9*hrF(VNt{5^n37?Qi_*AQnjlx>AZW6cy%JtHeO<^!h zmL5~epxzO(gBS@#`mrCqGCB9nYbP0(?l&8hp_TAWQlhWWD)%FEJ-{7W%f{Jx{aE@~ zPfy$rb$EX#xzoF9i|@;fBdjwkzMuf-G7!9|Mg=`SK>2Dp?*MWevKWyK1AA^1O02rEM59Pi4w1+;mUWu{w+@JPP)zJT5*y zXxnUIlc(ξ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 Date: Thu, 1 Oct 2026 16:36:56 -0700 Subject: [PATCH 87/88] fix: restore custom upload drop path --- Sources/TerminalCustomUploadRunner.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TerminalCustomUploadRunner.swift b/Sources/TerminalCustomUploadRunner.swift index 32d272e1ea11..23c74016df9f 100644 --- a/Sources/TerminalCustomUploadRunner.swift +++ b/Sources/TerminalCustomUploadRunner.swift @@ -113,7 +113,7 @@ struct TerminalCustomUploadRunner { guard normalizedLocalURL.isFileURL else { throw Self.uploadError("Dropped item is not a local file.") } - let remotePath = session.remotePastePolicy.remotePath(for: normalizedLocalURL) + let remotePath = RemoteSessionCoordinator.remoteDropPath(for: normalizedLocalURL) let env = TerminalUploadCommand.environment( localPath: normalizedLocalURL.path, remotePath: remotePath, From d064649c2008bc86b640e645c93743bd38be92a4 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:51:56 -0700 Subject: [PATCH 88/88] test: isolate CLI routing fixture home --- cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift b/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift index 9cb5e19a1785..15f505a676a6 100644 --- a/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift +++ b/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift @@ -44,9 +44,18 @@ struct CLIExplicitSurfaceRoutingTests { } @Test func vmTreeUsesCloudLinkErrorMessageInHumanOutput() throws { + let temporaryHome = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-cli-vm-tree-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: temporaryHome, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: temporaryHome) } + let execution = try runMockCommand( arguments: ["vm", "tree"], - socketName: "vm-tree-link-error" + socketName: "vm-tree-link-error", + environmentOverrides: [ + "CFFIXED_USER_HOME": temporaryHome.path, + "HOME": temporaryHome.path, + ] ) { line in guard let request = Self.jsonObject(line), let id = request["id"] as? String,