@@ -26,6 +26,9 @@ struct RootTabs: View {
2626 @State private var selectedSidebarDestination : SidebarDestination = Self . initialSidebarDestination
2727 @State private var selectedSettingsRoute : SettingsRoute ? = Self . initialSidebarDestination. settingsRoute
2828 @State private var selectedSettingsRouteRequestID : Int = 0
29+ @State private var phoneControlNavigationRequest : PhoneControlNavigationRequest ?
30+ @State private var phoneChatReturn : PhoneChatReturn ?
31+ @State private var phoneChatSettingsResetRequestID : Int = 0
2932 // Embedded Settings rows push onto the sidebar stack; clear it before
3033 // changing sidebar roots so stale settings detail screens cannot survive.
3134 @State private var sidebarNavigationPath : [ SettingsRoute ] = [ ]
@@ -158,9 +161,10 @@ struct RootTabs: View {
158161 }
159162
160163 private var phoneTabContent : some View {
161- TabView ( selection: self . $selectedTab ) {
162- PhoneTabSettingsHost { openSettingsRoute in
164+ TabView ( selection: self . phoneTabSelection ) {
165+ PhoneTabSettingsHost ( resetRequestID : self . phoneChatSettingsResetRequestID ) { openSettingsRoute in
163166 ChatProTab (
167+ headerLeadingAction: self . phoneChatReturnAction,
164168 ownsNavigationStack: false ,
165169 openSettings: { openSettingsRoute ( . gateway) } )
166170 }
@@ -183,7 +187,9 @@ struct RootTabs: View {
183187 RootTabsPhoneControlHub (
184188 groups: Self . phoneControlGroups,
185189 initialDestination: Self . requestedInitialSidebarDestination,
186- openRootDestination: { self . selectSidebarDestination ( $0) } )
190+ navigationRequest: self . phoneControlNavigationRequest,
191+ openRootDestination: { self . selectSidebarDestination ( $0) } ,
192+ openChatFromControlDetail: { self . openChatFromControlDetail ( $0) } )
187193 . tabItem { Label ( " Control " , systemImage: " square.grid.2x2 " ) }
188194 . badge ( self . appModel. pendingExecApprovalPrompt == nil ? 0 : 1 )
189195 . tag ( AppTab . control)
@@ -580,6 +586,23 @@ struct RootTabs: View {
580586 action: { self . showSidebar ( ) } )
581587 }
582588
589+ private var phoneChatReturnAction : OpenClawSidebarHeaderAction ? {
590+ guard !self . usesSidebarTabs, let phoneChatReturn else { return nil }
591+ return OpenClawSidebarHeaderAction (
592+ systemName: " chevron.left " ,
593+ accessibilityLabel: " Back to \( phoneChatReturn. destination. title) " ,
594+ accessibilityIdentifier: " OpenClawChatBackToControlDetailButton " ,
595+ action: { self . openPhoneControlDetail ( phoneChatReturn. destination) } )
596+ }
597+
598+ /// TabView writes through this binding; internal routing writes selectedTab directly.
599+ /// That distinction keeps only a user-selected Control tab responsible for resetting its child stack.
600+ private var phoneTabSelection : Binding < AppTab > {
601+ Binding (
602+ get: { self . selectedTab } ,
603+ set: { self . handlePhoneTabSelection ( $0) } )
604+ }
605+
583606 private var sidebarHideButton : some View {
584607 Button {
585608 self . hideSidebar ( )
@@ -835,8 +858,8 @@ struct RootTabs: View {
835858 guard !newValue else { return }
836859 self . maybeRequestLocalNetworkAccess ( reason: " onboarding_dismissed " )
837860 }
838- . onChange ( of: self . appModel. openChatRequestID) { _, _ in
839- self . selectSidebarDestination ( . chat )
861+ . onChange ( of: self . appModel. openChatRequestID) { _, newValue in
862+ self . handleOpenChatRequest ( newValue )
840863 }
841864 . onChange ( of: self . appModel. gatewaySetupRequestID) { _, _ in
842865 self . maybeOpenSettingsForGatewaySetup ( )
@@ -1026,21 +1049,78 @@ struct RootTabs: View {
10261049}
10271050
10281051extension RootTabs {
1029- private func selectSidebarDestination( _ destination: SidebarDestination ) {
1052+ private func selectSidebarDestination(
1053+ _ destination: SidebarDestination ,
1054+ preservingChatReturn: Bool = false )
1055+ {
1056+ if destination != . chat || !preservingChatReturn {
1057+ self . phoneChatReturn = nil
1058+ }
10301059 self . sidebarNavigationPath. removeAll ( )
10311060 if destination. settingsRoute != . notifications {
10321061 self . suppressedExecApprovalPromptIDForNotificationSettings = nil
10331062 }
10341063 self . selectedSidebarDestination = destination
10351064 self . selectedSettingsRoute = destination. settingsRoute
10361065 self . selectedTab = destination. appTab
1066+ self . requestPhoneControlDestinationIfNeeded ( destination)
10371067 guard self . usesSidebarTabs, self . shouldCollapseSidebarAfterSelection else { return }
10381068 withAnimation ( . easeInOut( duration: 0.22 ) ) {
10391069 self . setSidebarVisible ( false )
10401070 }
10411071 }
10421072
1073+ private func openChatFromControlDetail( _ returnDestination: SidebarDestination ) {
1074+ // Detail screens focus a session before invoking this route callback. Remember that
1075+ // synchronous request so its later observation cannot erase the contextual return.
1076+ self . phoneChatReturn = PhoneChatReturn (
1077+ destination: returnDestination,
1078+ openChatRequestID: self . appModel. openChatRequestID)
1079+ // Chat owns an embedded Settings stack. Pop it before routing so the requested
1080+ // session and contextual return action cannot remain hidden behind Settings.
1081+ self . phoneChatSettingsResetRequestID &+= 1
1082+ self . selectSidebarDestination ( . chat, preservingChatReturn: true )
1083+ }
1084+
1085+ private func handleOpenChatRequest( _ requestID: Int ) {
1086+ guard requestID != self . phoneChatReturn? . openChatRequestID else { return }
1087+ self . selectSidebarDestination ( . chat)
1088+ }
1089+
1090+ private func openPhoneControlDetail( _ destination: SidebarDestination ) {
1091+ self . selectSidebarDestination ( destination)
1092+ if destination == . overview {
1093+ self . requestPhoneControlDestinationIfNeeded ( destination, force: true )
1094+ }
1095+ }
1096+
1097+ private func handlePhoneTabSelection( _ selectedTab: AppTab ) {
1098+ if selectedTab != . chat {
1099+ self . phoneChatReturn = nil
1100+ }
1101+ if selectedTab == . control {
1102+ self . requestPhoneControlNavigation ( . root)
1103+ }
1104+ self . selectedTab = selectedTab
1105+ }
1106+
1107+ private func requestPhoneControlDestinationIfNeeded(
1108+ _ destination: SidebarDestination ,
1109+ force: Bool = false )
1110+ {
1111+ guard !self . usesSidebarTabs else { return }
1112+ guard destination. appTab == . control else { return }
1113+ guard force || destination != . overview else { return }
1114+ self . requestPhoneControlNavigation ( . detail( destination) )
1115+ }
1116+
1117+ private func requestPhoneControlNavigation( _ target: PhoneControlNavigationRequest . Target ) {
1118+ let requestID = ( self . phoneControlNavigationRequest? . id ?? 0 ) &+ 1
1119+ self . phoneControlNavigationRequest = PhoneControlNavigationRequest ( id: requestID, target: target)
1120+ }
1121+
10431122 private func selectSettingsRoute( _ route: SettingsRoute ) {
1123+ self . phoneChatReturn = nil
10441124 self . sidebarNavigationPath. removeAll ( )
10451125 if route != . notifications {
10461126 self . suppressedExecApprovalPromptIDForNotificationSettings = nil
@@ -1263,9 +1343,14 @@ extension RootTabs {
12631343/// (deep links, onboarding, problem banner) jump to the canonical Settings tab.
12641344private struct PhoneTabSettingsHost < Content: View > : View {
12651345 @State private var settingsPath : [ SettingsRoute ] = [ ]
1346+ private let resetRequestID : Int
12661347 private let content : ( _ openSettingsRoute: @escaping ( SettingsRoute ) -> Void ) -> Content
12671348
1268- init ( @ViewBuilder content: @escaping ( _ openSettingsRoute: @escaping ( SettingsRoute ) -> Void ) -> Content ) {
1349+ init (
1350+ resetRequestID: Int = 0 ,
1351+ @ViewBuilder content: @escaping ( _ openSettingsRoute: @escaping ( SettingsRoute ) -> Void ) -> Content )
1352+ {
1353+ self . resetRequestID = resetRequestID
12691354 self . content = content
12701355 }
12711356
@@ -1278,6 +1363,9 @@ private struct PhoneTabSettingsHost<Content: View>: View {
12781363 SettingsProTab ( directRoute: route)
12791364 }
12801365 }
1366+ . onChange ( of: self . resetRequestID) { _, _ in
1367+ self . settingsPath. removeAll ( )
1368+ }
12811369 }
12821370}
12831371
0 commit comments