fix(ios): preserve active chat across foreground reconnects (#108241)

* fix(ios): preserve chat focus across reconnects

* chore(ios): sync native i18n inventory

* test(ios): strengthen reconnect ownership coverage

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>

---------

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
NianJiu
2026-07-16 01:38:45 -07:00
committed by GitHub
co-authored by NianJiuZst Peter Steinberger
parent 48183146f5
commit 727f9e0a7e
3 changed files with 50 additions and 18 deletions
+14 -14
View File
@@ -22867,7 +22867,7 @@
},
{
"kind": "ui-localized-call",
"line": 3813,
"line": 3819,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Approval needed",
"surface": "apple",
@@ -22875,7 +22875,7 @@
},
{
"kind": "ui-localized-call",
"line": 3814,
"line": 3820,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Action required",
"surface": "apple",
@@ -22883,7 +22883,7 @@
},
{
"kind": "ui-localized-call",
"line": 4491,
"line": 4497,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Connecting...",
"surface": "apple",
@@ -22891,7 +22891,7 @@
},
{
"kind": "ui-localized-call",
"line": 4492,
"line": 4498,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Reconnecting...",
"surface": "apple",
@@ -22899,7 +22899,7 @@
},
{
"kind": "conditional-branch",
"line": 4836,
"line": 4842,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Connected",
"surface": "apple",
@@ -22907,7 +22907,7 @@
},
{
"kind": "conditional-branch",
"line": 4836,
"line": 4842,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Offline",
"surface": "apple",
@@ -22915,7 +22915,7 @@
},
{
"kind": "conditional-branch",
"line": 5978,
"line": 5984,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "No chat messages yet",
"surface": "apple",
@@ -22923,7 +22923,7 @@
},
{
"kind": "conditional-branch",
"line": 6177,
"line": 6183,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Connecting…",
"surface": "apple",
@@ -22931,7 +22931,7 @@
},
{
"kind": "conditional-branch",
"line": 6177,
"line": 6183,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Reconnecting…",
"surface": "apple",
@@ -22939,7 +22939,7 @@
},
{
"kind": "conditional-branch",
"line": 8462,
"line": 8468,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Approval",
"surface": "apple",
@@ -22947,7 +22947,7 @@
},
{
"kind": "conditional-branch",
"line": 8462,
"line": 8468,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "This approval was already",
"surface": "apple",
@@ -22955,7 +22955,7 @@
},
{
"kind": "conditional-branch",
"line": 8468,
"line": 8474,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "This approval was already set to Always Allow.",
"surface": "apple",
@@ -22963,7 +22963,7 @@
},
{
"kind": "conditional-branch",
"line": 8469,
"line": 8475,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "Approval set to Always Allow.",
"surface": "apple",
@@ -22971,7 +22971,7 @@
},
{
"kind": "conditional-branch",
"line": 9773,
"line": 9779,
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
"source": "\\(urlText.prefix(500))…",
"surface": "apple",
+10 -4
View File
@@ -3412,7 +3412,7 @@ extension NodeAppModel {
nodeOptions: connectOptions)
let previousGatewayStableID = self.activeGatewayConnectConfig?.effectiveStableID
?? self.connectedGatewayID
let preserveGatewayProblem = previousGatewayStableID.map {
let isSameGatewayTarget = previousGatewayStableID.map {
!$0.isEmpty && GatewayStableIdentifier.matches($0, effectiveStableID)
} ?? false
let targetChanged = previousGatewayStableID.map {
@@ -3449,7 +3449,8 @@ extension NodeAppModel {
self.activeGatewayConnectConfig = nextConfig
prepareForGatewayConnect(
stableID: effectiveStableID,
preservingGatewayProblem: preserveGatewayProblem)
preservingGatewayProblem: isSameGatewayTarget,
preservingFocusedChatSession: isSameGatewayTarget)
if operatorLoopRequired {
startOperatorGatewayLoop(
url: url,
@@ -3693,7 +3694,8 @@ extension NodeAppModel {
private func prepareForGatewayConnect(
stableID: String,
preservingGatewayProblem: Bool = false)
preservingGatewayProblem: Bool = false,
preservingFocusedChatSession: Bool = false)
{
self.invalidateNodePushToTalkRoute()
self.operatorTalkConnectionGeneration &+= 1
@@ -3730,7 +3732,11 @@ extension NodeAppModel {
self.gatewayDefaultAgentId = nil
self.gatewayAgents = []
self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID)
self.focusedChatSessionKey = nil
// Session keys are gateway-owned: transport reconnects keep the active chat,
// while initial connects and target changes must not inherit another route.
if !preservingFocusedChatSession {
self.focusedChatSessionKey = nil
}
self.synchronizeTalkSessionKey()
self.homeCanvasRevision &+= 1
self.apnsLastRegisteredTokenHex = nil
@@ -500,6 +500,29 @@ private func waitForActiveGateway(stableID: String, appModel: NodeAppModel) asyn
#expect(appModel.gatewayPairingRequestId == nil)
}
@Test @MainActor func `stable gateway owner preserves focused chat across route changes`() throws {
let appModel = NodeAppModel()
defer { appModel.disconnectGateway() }
let currentConfig = Self.makeGatewayConnectConfig()
let focusedSessionKey = "agent:main:ios-focused"
appModel.applyGatewayConnectConfig(currentConfig)
appModel.focusChatSession(focusedSessionKey)
let replacementURL = try #require(URL(string: "wss://127.0.0.1:2"))
let refreshedRoute = Self.makeGatewayConnectConfig(
url: replacementURL,
stableID: currentConfig.stableID)
appModel.applyGatewayConnectConfig(refreshedRoute, forceReconnect: true)
#expect(appModel.chatSessionKey == focusedSessionKey)
let replacementConfig = Self.makeGatewayConnectConfig(
url: replacementURL,
stableID: "manual|replacement.example.com|443")
appModel.applyGatewayConnectConfig(replacementConfig, forceReconnect: true)
#expect(appModel.chatSessionKey != focusedSessionKey)
}
@Test func `gateway connect config matches equivalent inputs`() {
let lhs = Self.makeGatewayConnectConfig()
let rhs = GatewayConnectConfig(
@@ -2537,11 +2560,14 @@ private func waitForActiveGateway(stableID: String, appModel: NodeAppModel) asyn
defer { appModel.disconnectGateway() }
let config = Self.makeGatewayConnectConfig()
let focusedSessionKey = "agent:main:ios-foreground-focused"
appModel.applyGatewayConnectConfig(config)
appModel.focusChatSession(focusedSessionKey)
await appModel._test_restartGatewaySessionsAfterForegroundStaleConnection()
#expect(appModel.gatewayStatusText == "Reconnecting…")
#expect(appModel.activeGatewayConnectConfig?.hasSameConnectionInputs(as: config) == true)
#expect(appModel.chatSessionKey == focusedSessionKey)
#expect(appModel._test_hasGatewayLoopTasks().node)
#expect(appModel._test_hasGatewayLoopTasks().operator)
}