Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,12 @@ public struct WorkspaceListLayoutPreviewView: View {
} else {
let workspaceListStack = NavigationStack {
MobilePrimaryWorkspaceSearchHost(
searchCoordinator: primarySearchCoordinator
searchCoordinator: primarySearchCoordinator,
// The live shell puts New Task in the bottom toolbar
// next to the system search pill; the tab-scaffold
// preview must render both so their shared bottom-bar
// layout can be exercised without Mac pairing.
taskComposerAction: showsTabScaffold ? {} : nil
) { searchText in
workspaceListFixture(searchText: searchText)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,17 @@ struct WorkspaceListSearchHost<Content: View>: View {
@ViewBuilder
private var iOSContent: some View {
if #available(iOS 26.0, *) {
// A `.bottomBar` toolbar item cannot host New Task here: the
// TabView's search-role tab renders its pill in the same
// bottom-trailing slot and the two stack on top of each other.
// Mount the shared button in the bottom safe-area bar instead,
// above the tab-bar chrome the system owns.
content(searchText)
.toolbar {
.safeAreaBar(edge: .bottom, alignment: .trailing, spacing: 0) {
if let taskComposerAction {
ToolbarSpacer(.flexible, placement: .bottomBar)
ToolbarItem(placement: .bottomBar) {
Button(action: taskComposerAction) {
Image(systemName: "sparkles")
}
.accessibilityLabel(
L10n.string(
"mobile.taskComposer.button.accessibilityLabel",
defaultValue: "New Task"
)
)
.accessibilityHint(
L10n.string(
"mobile.taskComposer.button.accessibilityHint",
defaultValue: "Opens the task composer."
)
)
.accessibilityIdentifier("MobileTaskComposerButton")
}
TaskComposerButton(action: taskComposerAction)
.padding(.trailing, 20)
.padding(.bottom, 6)
}
}
} else {
Expand All @@ -65,6 +54,13 @@ struct WorkspaceListSearchHost<Content: View>: View {
placement: .navigationBarDrawer(displayMode: .always)
)
.searchFocused($searchIsFocused)
.overlay(alignment: .bottomTrailing) {
if let taskComposerAction {
TaskComposerButton(action: taskComposerAction)
.padding(.trailing, 20)
.padding(.bottom, 6)
}
}
}
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -465,11 +465,6 @@ struct WorkspaceShellView: View {
rootToolbarContent
}
}
#if os(iOS)
.overlay(alignment: .bottomTrailing) {
taskComposerButtonOverlay
}
#endif
.navigationDestination(for: MobileWorkspacePreview.ID.self) { workspaceID in
workspaceDestination(
for: workspaceID,
Expand Down Expand Up @@ -562,11 +557,6 @@ struct WorkspaceShellView: View {
.toolbar {
rootToolbarContent
}
#if os(iOS)
.overlay(alignment: .bottomTrailing) {
taskComposerButtonOverlay
}
#endif
.navigationSplitViewColumnWidth(min: 320, ideal: 380, max: 440)
} detail: {
workspaceDestination(
Expand Down Expand Up @@ -804,22 +794,6 @@ struct WorkspaceShellView: View {
}
}

private var showsTaskComposerButtonOverlay: Bool {
guard displaySettings.taskComposerEnabled else { return false }
if #available(iOS 26.0, *) {
return false
}
return true
}

@ViewBuilder
private var taskComposerButtonOverlay: some View {
if showsTaskComposerButtonOverlay {
TaskComposerButton(action: openTaskComposer)
.padding(.trailing, 20)
.padding(.bottom, 6)
}
}
#endif

/// Apply (and clear) a pending deep-link navigation intent. On the compact
Expand Down
43 changes: 43 additions & 0 deletions ios/cmuxUITests/cmuxUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,49 @@ final class cmuxUITests: XCTestCase {
XCTAssertTrue(restoredMinimizedSearchMatches.firstMatch.waitForExistence(timeout: 3))
}

/// Regression: on iOS 26 the workspace list's New Task control rendered in
/// the same bottom-trailing slot as the system search tab pill, so the two
/// stacked and New Task was occluded. The shared list host must lay the
/// button out clear of the search pill and keep both tappable.
@MainActor
func testWorkspaceListNewTaskButtonClearsSearchPill() throws {
guard #available(iOS 26.0, *) else {
throw XCTSkip("The detached workspace search pill requires iOS 26.")
}
let app = launchApp(mockData: false, environment: [
"CMUX_UITEST_WORKSPACE_LIST_PREVIEW": "1",
"CMUX_UITEST_WORKSPACE_LIST_PREVIEW_TABS": "1",
])
defer { app.terminate() }

let composer = app.buttons["MobileTaskComposerButton"]
XCTAssertTrue(composer.waitForExistence(timeout: 8))
guard let composerFrame = waitForUsableFrame(of: composer, timeout: 3) else {
XCTFail("New Task button had no usable frame")
return
}
let searchPill = app.tabBars.buttons
.matching(NSPredicate(format: "label == %@", "Search"))
.firstMatch
XCTAssertTrue(searchPill.waitForExistence(timeout: 3))
guard let searchPillFrame = waitForUsableFrame(of: searchPill, timeout: 3) else {
XCTFail("Search pill had no usable frame")
return
}
XCTAssertFalse(
composerFrame.intersects(searchPillFrame),
"New Task \(composerFrame) must not overlap the search pill \(searchPillFrame)"
)
XCTAssertTrue(
waitForHittable(composer, timeout: 3),
"New Task must be tappable next to the search pill"
)
XCTAssertTrue(
waitForHittable(searchPill, timeout: 3),
"The search pill must stay tappable next to New Task"
)
}

@MainActor
func testWorkspaceSearchClearUpdatesResults() throws {
guard #available(iOS 26.0, *) else {
Expand Down