Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
1bc9f25
ios: keep tab navigation chrome mounted
azooz2003-bit Sep 29, 2026
54ae5dc
test(ios): cover notification tab toolbar switching
azooz2003-bit Sep 29, 2026
4feb145
fix(ios): keep compact toolbar mounted across tabs
azooz2003-bit Sep 29, 2026
5a23cd4
Merge main (7d177547949b) into feat-ios-tab-toolbar-flash
teamleaderleo Sep 30, 2026
335ff5d
Merge main (3ed1d77ebb02) into pin2-15712
teamleaderleo Sep 30, 2026
1f56e1b
fix(ios): unmount inactive primary tab stacks
azooz2003-bit Sep 30, 2026
602eb9b
fix(ios): centralize notification confirmation and tab hits
azooz2003-bit Sep 30, 2026
e495a1e
Merge commit '152174b6d8f47065e562ba5ac731dca01d2f0880' into feat-ios…
azooz2003-bit Sep 30, 2026
751e1ce
Merge main (75bb022b5749) into feat-ios-tab-toolbar-flash
azooz2003-bit Sep 30, 2026
a81dd26
fix(ios): hide nested compact navigation bars
azooz2003-bit Sep 30, 2026
ae333b9
fix(ios): preserve compact tab state across switches
azooz2003-bit Sep 30, 2026
6ff89ad
fix(ios): refresh active notification feed copies
azooz2003-bit Sep 30, 2026
fafde81
fix(ios): gate inactive notification lifecycle
azooz2003-bit Sep 30, 2026
3dd5242
fix(ios): refresh agent feed on activation
azooz2003-bit Sep 30, 2026
2178698
fix(ios): restore feed lifecycle on navigation
azooz2003-bit Sep 30, 2026
c048f94
fix(ios): scope tab content to active selection
azooz2003-bit Sep 30, 2026
49e0f44
fix(ios): retain compact task composer
azooz2003-bit Sep 30, 2026
68cd2bd
fix(ios): close inactive search lifecycle
azooz2003-bit Sep 30, 2026
13eb0c2
fix(ios): cancel inactive notification opens
azooz2003-bit Sep 30, 2026
1decc2f
fix(ios): gate inactive tab work
azooz2003-bit Sep 30, 2026
1333880
fix(ios): bound feed activation refreshes
azooz2003-bit Sep 30, 2026
ab5e65d
fix(ios): bound feed refresh recovery
azooz2003-bit Sep 30, 2026
7755b0d
fix(ios): rely on bounded feed requests
azooz2003-bit Sep 30, 2026
1c8bbb3
fix(ios): keep tab bar hit region clear
azooz2003-bit Sep 30, 2026
00cba5a
fix(ios): preserve tab bar hit testing
azooz2003-bit Sep 30, 2026
577bd6f
fix(ios): restart notification preview on selection
azooz2003-bit Sep 30, 2026
88b7da7
fix(ios): keep search host in native search tab
azooz2003-bit Sep 30, 2026
f8c324c
fix(ios): keep tab controls above content layer
azooz2003-bit Sep 30, 2026
d26320b
Merge main (71bb553867ca) into feat-ios-tab-toolbar-flash
azooz2003-bit Sep 30, 2026
2b45d5d
fix(ios): keep primary tabs in native navigation
azooz2003-bit Sep 30, 2026
eb8417a
Merge main (02b80fecc984) into feat-ios-tab-toolbar-flash
azooz2003-bit Sep 30, 2026
831f1da
fix(ios): scope tab switch animation suppression
azooz2003-bit Sep 30, 2026
02a497d
Merge main (b520727d0c19) into feat-ios-tab-toolbar-flash
azooz2003-bit Sep 30, 2026
deb45a2
test(ios): probe rendered link position
azooz2003-bit Sep 30, 2026
a9aeaac
Merge main (4e9d77988827) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
7943e3b
fix(ios): preserve search and notification refresh paths
azooz2003-bit Oct 1, 2026
58ce74c
fix(ios): close tab review gaps
azooz2003-bit Oct 1, 2026
3dd39a1
test: restore configurable main queue drain helper
azooz2003-bit Oct 1, 2026
4120832
fix: bound agent feed refresh requests
azooz2003-bit Oct 1, 2026
f729dfc
test: keep configurable queue drain in close-tab suite
azooz2003-bit Oct 1, 2026
d1dfa05
fix: hide nested search navigation chrome
azooz2003-bit Oct 1, 2026
02f68fd
test: restore pane resize controller binding
azooz2003-bit Oct 1, 2026
99acc5c
Merge main (22f7c10093f0) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
397a7af
test: keep last-surface queue drain bounded
azooz2003-bit Oct 1, 2026
ff8b2cb
Merge main (612389b3bdf9) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
33feb1a
fix: restore updater accent environment
azooz2003-bit Oct 1, 2026
c6102f9
Merge main (70c83fd8423f) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
9c46f00
Merge main (6c26fc34b1b0) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
43b8b06
Merge main (974d0a0c9412) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
f5a58f3
Merge main (64bb5e9f3fb) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
6b21ac4
Merge main (f2526b0fc42b) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
763c567
fix: keep pane resize controller binding after merge
azooz2003-bit Oct 1, 2026
2d4cdd3
Merge main (8395a5b4301c) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
a124f6e
fix: remove duplicate updater accent binding
azooz2003-bit Oct 1, 2026
3d67943
Merge main (a20ed74ca17b) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
6872b5a
fix: resolve merge compile and search compatibility
azooz2003-bit Oct 1, 2026
671bf1d
fix: resolve notification accent color
azooz2003-bit Oct 1, 2026
78ec4be
fix: keep pre-iOS search in one navigation bar
azooz2003-bit Oct 1, 2026
2ad1aac
fix: restore test window geometry reset
azooz2003-bit Oct 1, 2026
cca9d50
fix: unwrap browser restoration URL
azooz2003-bit Oct 1, 2026
ac07000
test: sample toolbar during repeated tab switches
azooz2003-bit Oct 1, 2026
370abca
fix: enable repeated switch fixture in toolbar test
azooz2003-bit Oct 1, 2026
679edde
fix: repair exact-head verification and base compile drift
azooz2003-bit Oct 1, 2026
1cbedf2
Merge main (6aa634361c91) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
1a670c9
fix: keep notification search controls on legacy iOS
azooz2003-bit Oct 1, 2026
6010a94
fix: preserve legacy search root controls
azooz2003-bit Oct 1, 2026
bb805fc
test: synchronize toolbar sampling with tab selection
azooz2003-bit Oct 1, 2026
311fb01
test: let tab switch fixture reach the active test
azooz2003-bit Oct 1, 2026
b18130d
test: use the existing hidden anchor lookup
azooz2003-bit Oct 1, 2026
e2a4fe5
test: repair cmux test helper call sites
azooz2003-bit Oct 1, 2026
86e3442
refactor: isolate shared iOS toolbar types
azooz2003-bit Oct 1, 2026
9480fa6
test: bind sidebar indicator query before expect
azooz2003-bit Oct 1, 2026
35b4c10
test: include CLIError in CLI test target
azooz2003-bit Oct 1, 2026
2c4672c
Revert "test: include CLIError in CLI test target"
azooz2003-bit Oct 1, 2026
bae1810
fix: make tmux parser errors target neutral
azooz2003-bit Oct 1, 2026
5317de2
fix: keep tmux parser validation target neutral
azooz2003-bit Oct 1, 2026
95115c4
Merge main (43ea79cc3d09) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
9dc9995
test: repair hosted macOS CI fixtures
azooz2003-bit Oct 1, 2026
6f36d19
test: align notification tab switch timing
azooz2003-bit Oct 1, 2026
8efae4c
fix(cli): preserve typed tmux argument errors
azooz2003-bit Oct 1, 2026
eda9650
fix(cli): keep tmux argument error type target neutral
azooz2003-bit Oct 1, 2026
e1b7b04
fix(cli): isolate tmux argument error type
azooz2003-bit Oct 1, 2026
f69a82a
fix(cli): keep parser error type with parser
azooz2003-bit Oct 1, 2026
04c4a4b
Merge origin/main into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
840da0b
fix(cli): use shared CLI error contract
azooz2003-bit Oct 1, 2026
d31cb74
fix(ci): preserve runtime source root in CLI hooks
azooz2003-bit Oct 1, 2026
c6d3a12
fix(tests): remove duplicate Codex environment helper
azooz2003-bit Oct 1, 2026
5466e35
fix(tests): keep XCTest JSON assertions
azooz2003-bit Oct 1, 2026
375824d
fix(tests): keep Codex fixture helper in XCTest suite
azooz2003-bit Oct 1, 2026
29d34c7
Merge main (874ed61e2218) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
2232206
fix(tests): mark unused Codex session id
azooz2003-bit Oct 1, 2026
305aef3
Merge main into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
c282eb4
fix(ios): retain cloud routes with shared tab shell
azooz2003-bit Oct 1, 2026
a9db8f4
fix: restore cloud workspace routing
azooz2003-bit Oct 1, 2026
779d6b2
Merge remote-tracking branch 'origin/main' into feat-ios-tab-toolbar-…
azooz2003-bit Oct 1, 2026
0519f3e
fix(cli): keep auto-naming parser helper static
azooz2003-bit Oct 1, 2026
0d7c43e
fix(cli): bind cloud transfer retry error
azooz2003-bit Oct 1, 2026
b1f0abb
test: retain multi-message inbox coverage
azooz2003-bit Oct 1, 2026
d226835
Merge green main into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
c84cb40
Merge main (337861cfd05e) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
3eaef72
fix: handle cloud tab in notification preview
azooz2003-bit Oct 1, 2026
8ed060d
fix: restore workspace destination return
azooz2003-bit Oct 1, 2026
2ed9205
Merge main (984179e9c187) into feat-ios-tab-toolbar-flash
azooz2003-bit Oct 1, 2026
319fe4c
fix(ios): use deployment-safe toolbar visibility
azooz2003-bit Oct 1, 2026
584e73c
test: drop the uncompilable CLI half of the Cloud link-failure copy test
teamleaderleo Oct 1, 2026
2259c1c
fix: preserve cloud and recovery toolbar behavior
azooz2003-bit Oct 1, 2026
2b7821a
chore: drop unrelated XCTest fixture edits
azooz2003-bit Oct 1, 2026
370d99f
test: exercise unavailable Cloud link copy
azooz2003-bit Oct 1, 2026
7086bfa
Merge remote-tracking branch 'origin/main' into feat-ios-tab-toolbar-…
azooz2003-bit Oct 1, 2026
2aed0e6
fix: preserve public Foundation import after main merge
azooz2003-bit Oct 1, 2026
8e6edd7
fix: compile sidebar usage owner selection
azooz2003-bit Oct 1, 2026
81f2755
fix: use local SSH shell quoting helper
azooz2003-bit Oct 1, 2026
c589cbf
Merge remote-tracking branch 'origin/main' into feat-ios-tab-toolbar-…
azooz2003-bit Oct 1, 2026
b21a41a
fix: restore custom sidebar preview assets
azooz2003-bit Oct 1, 2026
8e13af3
fix: restore custom upload drop path
azooz2003-bit Oct 1, 2026
d35b2ad
Merge remote-tracking branch 'origin/main' into feat-ios-tab-toolbar-…
azooz2003-bit Oct 1, 2026
d064649
test: isolate CLI routing fixture home
azooz2003-bit Oct 2, 2026
964cefa
Merge current main into toolbar fix
azooz2003-bit Oct 2, 2026
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
7 changes: 7 additions & 0 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40689,6 +40689,13 @@ 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")
)
}
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"] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -311,7 +312,11 @@ extension MobileShellComposite {
method: "feed.list",
params: [:]
)
let data = try await client.sendRequest(request)
let data = try await client.sendRequest(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
request,
timeoutNanoseconds: runtime?.rpcRequestTimeoutNanoseconds
?? mobileShellAgentFeedRequestTimeoutNanoseconds
)
let response = try MobileAgentFeedListResponse.decode(data)
guard !Task.isCancelled,
agentFeedClient(for: macDeviceID) === client else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ struct AgentFeedStoreView: View {
let status: MobileNotificationFeedStatus
let pendingReplyRequestIDs: Set<String>
let pendingTerminalReplyItemIDs: Set<MobileAgentFeedItemID>
var isActive = true

@State private var showsNavigationFailure = false
@State private var isFeedVisible = false
@Bindable var searchCoordinator: MobilePrimarySearchCoordinator

var body: some View {
Expand All @@ -24,14 +26,18 @@ struct AgentFeedStoreView: View {
pendingTerminalReplyItemIDs: pendingTerminalReplyItemIDs,
failedTerminalReplies: store.agentFeedFailedTerminalReplies,
refreshesOnAppear: true,
isActive: isActive,
actions: actions,
searchText: searchCoordinator.searchDestinationText(for: .feed)
)
.onAppear {
store.recordAppEvent(.agentFeedOpened, count: items.count)
updateFeedVisibility(isActive)
}
.onChange(of: isActive) { _, active in
updateFeedVisibility(active)
}
.onDisappear {
store.recordAppEvent(.agentFeedClosed)
updateFeedVisibility(false)
}
.alert(String(localized: "mobile.agentFeed.openFailed.title", defaultValue: "Couldn’t open event", bundle: .module),
isPresented: $showsNavigationFailure) {
Expand Down Expand Up @@ -95,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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct AgentFeedView: View {
let pendingTerminalReplyItemIDs: Set<MobileAgentFeedItemID>
var failedTerminalReplies: [MobileAgentFeedItemID: MobileAgentFeedFailedReply] = [:]
let refreshesOnAppear: Bool
var isActive = true
let actions: AgentFeedActions
var searchText: String = ""
@Environment(MobileDisplaySettings.self) private var displaySettings
Expand All @@ -34,6 +35,7 @@ struct AgentFeedView: View {
pendingTerminalReplyItemIDs: Set<MobileAgentFeedItemID>,
failedTerminalReplies: [MobileAgentFeedItemID: MobileAgentFeedFailedReply] = [:],
refreshesOnAppear: Bool,
isActive: Bool = true,
actions: AgentFeedActions,
searchText: String = ""
) {
Expand All @@ -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))
Expand Down Expand Up @@ -129,8 +132,14 @@ struct AgentFeedView: View {
}
.onAppear {
now = Date()
guard refreshesOnAppear else { return }
Task { await actions.refresh() }
}
.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()
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
.onChange(of: items) { _, newItems in
preparedRows = newItems.map(AgentFeedRowModel.init)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,34 @@ struct MobilePrimarySearchNavigationStack<Root: View, Destination: View>: View {
@Binding var path: [MobileWorkspacePreview.ID]
@Binding var selection: MobilePrimaryTab
@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()
.mobileToolbarVisibility(rootNavigationBarVisibility, for: .navigationBar)
.modifier(MobilePrimarySearchLifecycleModifier(
scope: searchCoordinator.scope,
update: searchCoordinator.updateLifecycle
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) {
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<Bool> {
Expand All @@ -38,6 +49,13 @@ struct MobilePrimarySearchNavigationStack<Root: View, Destination: View>: View {
)
}

private var rootNavigationBarVisibility: Visibility {
if #available(iOS 26.0, *), hidesRootNavigationBar {
return .hidden
}
return .automatic
}

private var searchText: Binding<String> {
let scope = searchCoordinator.scope
let generation = searchCoordinator.activationGeneration
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<Content: View, Toolbar: ToolbarContent>: 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
}
.mobileToolbarVisibility(toolbarVisibility, for: .navigationBar)
.mobileToolbarVisibility(tabBarVisibility, for: .tabBar)
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -66,28 +66,20 @@ struct MobilePrimaryTabScaffold<
}
}
.tabViewSearchActivation(.searchTabSelection)
.tabViewStyle(.tabBarOnly)
.accessibilityIdentifier("MobilePrimaryTabs")
.animation(nil, value: selection)
.onChange(of: selection, initial: true) { _, selection in
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 {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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) {
Expand Down Expand Up @@ -127,6 +119,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 tabSelection: Binding<MobilePrimaryTab> {
Binding(
get: { selection },
Expand All @@ -141,7 +153,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
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading