M7 #14 (pass-2): keep ACP session alive across tab switches

Pass-2 observation: "when a user switches away from chat and comes
back, there is a loading time — should we keep it open so there
isn't a reload needed?"

Removed the .onDisappear { controller.stop() } hook. TabView unmounts
tab content on switch (disappear fires), but @State keeps the
ChatController alive — so dropping the SSH exec channel + re-
opening on next appear was costing a ~1-2s reconnect every time
the user bounced Dashboard → Chat → Memory → Chat.

Cleanup still happens correctly because ChatController's lifetime
is tied to ChatView's parent (ScarfGoTabRoot). When the user
Disconnects/Forgets from the More tab, RootModel flips out of
.connected, the whole tab root unmounts, and the controller + its
ACPClient tear down via .deinit. Background termination is handled
by iOS naturally.

A comment in the file documents why we no longer tear down on
.onDisappear — easy to re-add if a future iPad / multi-window
variant wants explicit idle-pause behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-24 14:31:20 +02:00
parent 723ef6743d
commit f3c4bc56e9
+16 -3
View File
@@ -93,9 +93,22 @@ struct ChatView: View {
coordinator?.pendingResumeSessionID = nil
Task { await controller.startResuming(sessionID: sessionID) }
}
.onDisappear {
Task { await controller.stop() }
}
// Deliberately NOT tearing down the ACP session on .onDisappear.
// `TabView` unmounts tab content when the user switches tabs
// (disappear fires), but `@State var controller` keeps the
// ChatController alive across those switches, so dropping the
// SSH exec channel + re-opening on next appear would cost the
// user a ~1-2s reconnect every time they hop to Dashboard
// and back. The ACPClient stays open; the controller cleans up
// properly when:
// - the user Disconnects / Forgets the server (RootModel
// flips out of .connected, whole tab root unmounts, and
// ChatController.deinit + transport teardown runs),
// - or the app goes to background (iOS will terminate the
// socket eventually if memory pressure hits anyway).
// If a future iPad / multi-window variant wants to explicitly
// pause idle connections, add a coordinator-driven stop() on
// app-lifecycle phase changes instead.
.overlay {
if case .failed(let msg) = controller.state {
errorOverlay(msg)