Fix iOS selected agent chat routing

This commit is contained in:
joshavant
2026-06-05 19:31:38 -05:00
parent af50a5959d
commit 5c07f7ccf0
4 changed files with 80 additions and 4 deletions

View File

@@ -6,6 +6,7 @@ struct ChatProTab: View {
@Environment(NodeAppModel.self) private var appModel
@Environment(\.colorScheme) private var colorScheme
@State private var viewModel: OpenClawChatViewModel?
@State private var programmaticSessionSwitchCounts: [String: Int] = [:]
var body: some View {
NavigationStack {
@@ -104,6 +105,14 @@ struct ChatProTab: View {
sessionKey: sessionKey,
transport: IOSGatewayChatTransport(gateway: self.appModel.operatorSession),
onSessionChanged: { sessionKey in
if self.consumeProgrammaticSessionSwitch(sessionKey) {
// Programmatic switches complete asynchronously; stale completions
// must repair back to the current app-model session, not become focus.
if sessionKey != self.appModel.chatSessionKey {
self.syncChatViewModel()
}
return
}
self.appModel.focusChatSession(sessionKey)
},
diagnosticsLog: { message in
@@ -112,9 +121,26 @@ struct ChatProTab: View {
return
}
guard viewModel.sessionKey != sessionKey else { return }
self.recordProgrammaticSessionSwitch(sessionKey)
viewModel.switchSession(to: sessionKey)
}
private func recordProgrammaticSessionSwitch(_ sessionKey: String) {
self.programmaticSessionSwitchCounts[sessionKey, default: 0] += 1
}
private func consumeProgrammaticSessionSwitch(_ sessionKey: String) -> Bool {
guard let count = self.programmaticSessionSwitchCounts[sessionKey] else {
return false
}
if count <= 1 {
self.programmaticSessionSwitchCounts[sessionKey] = nil
} else {
self.programmaticSessionSwitchCounts[sessionKey] = count - 1
}
return true
}
private var talkControl: OpenClawChatTalkControl {
OpenClawChatTalkControl(
isEnabled: self.appModel.talkMode.isEnabled,

View File

@@ -502,7 +502,7 @@ struct CommandCenterTab: View {
trailing: "session",
color: self.gatewayConnected ? OpenClawBrand.ok : .secondary,
progress: nil,
route: .chat(self.appModel.chatSessionKey)),
route: .chat(self.appModel.defaultChatSessionKey)),
WorkItem(
id: "talk-mode",
icon: "waveform",

View File

@@ -762,6 +762,7 @@ final class NodeAppModel {
let selected = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if !selected.isEmpty, !decoded.agents.contains(where: { $0.id == selected }) {
self.selectedAgentId = nil
self.focusedChatSessionKey = nil
}
self.talkMode.updateMainSessionKey(self.mainSessionKey)
self.homeCanvasRevision &+= 1
@@ -779,13 +780,19 @@ final class NodeAppModel {
func setSelectedAgentId(_ agentId: String?) {
let trimmed = (agentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let nextSelectedAgentId = trimmed.isEmpty ? nil : trimmed
let currentSelectedAgentId = self.selectedAgentId?.trimmingCharacters(in: .whitespacesAndNewlines)
let selectedAgentChanged = currentSelectedAgentId != nextSelectedAgentId
let stableID = (self.connectedGatewayID ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if stableID.isEmpty {
self.selectedAgentId = trimmed.isEmpty ? nil : trimmed
self.selectedAgentId = nextSelectedAgentId
} else {
self.selectedAgentId = trimmed.isEmpty ? nil : trimmed
self.selectedAgentId = nextSelectedAgentId
GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: stableID, agentId: self.selectedAgentId)
}
if selectedAgentChanged {
self.focusedChatSessionKey = nil
}
self.talkMode.updateMainSessionKey(self.mainSessionKey)
self.homeCanvasRevision &+= 1
if let relay = ShareGatewayRelaySettings.loadConfig() {
@@ -1837,9 +1844,13 @@ extension NodeAppModel {
{
return focused
}
return self.defaultChatSessionKey
}
var defaultChatSessionKey: String {
// Keep chat aligned with the gateway's resolved main session key.
// A hardcoded "ios" base creates synthetic placeholder sessions in the chat UI.
return self.mainSessionKey
self.mainSessionKey
}
func openChat(sessionKey: String?) {
@@ -2005,6 +2016,7 @@ extension NodeAppModel {
self.gatewayDefaultAgentId = nil
self.gatewayAgents = []
self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID)
self.focusedChatSessionKey = nil
self.homeCanvasRevision &+= 1
self.apnsLastRegisteredTokenHex = nil
}

View File

@@ -214,6 +214,44 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
#expect(appModel.mainSessionKey == "agent:agent-123:main")
}
@Test @MainActor func selectingAgentClearsExplicitChatFocus() {
let appModel = NodeAppModel()
appModel.gatewayDefaultAgentId = "main"
let rustSessionKey = SessionKey.makeAgentSessionKey(agentId: "rust-claw", baseKey: "main")
appModel.setSelectedAgentId("rust-claw")
#expect(appModel.chatSessionKey == rustSessionKey)
appModel.focusChatSession(rustSessionKey)
appModel.setSelectedAgentId("main")
#expect(appModel.defaultChatSessionKey == "main")
#expect(appModel.mainSessionKey == "main")
#expect(appModel.chatSessionKey == "main")
}
@Test @MainActor func sameSelectedAgentKeepsExplicitChatFocus() {
let appModel = NodeAppModel()
appModel.gatewayDefaultAgentId = "main"
appModel.setSelectedAgentId("main")
appModel.openChat(sessionKey: "incident-42")
appModel.setSelectedAgentId("main")
#expect(appModel.defaultChatSessionKey == "main")
#expect(appModel.chatSessionKey == "incident-42")
}
@Test @MainActor func defaultChatSessionKeyIgnoresExplicitChatFocus() {
let appModel = NodeAppModel()
appModel.gatewayDefaultAgentId = "main"
appModel.setSelectedAgentId("rust-claw")
appModel.openChat(sessionKey: "incident-42")
#expect(appModel.defaultChatSessionKey == SessionKey.makeAgentSessionKey(
agentId: "rust-claw",
baseKey: "main"))
#expect(appModel.chatSessionKey == "incident-42")
}
@Test @MainActor func execApprovalPromptPresentationTracksLatestNotificationTap() throws {
let appModel = NodeAppModel()
try appModel._test_presentExecApprovalPrompt(