From 9c2e9279ccb24d0590521aedbba70e651b903e8d Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Fri, 24 Apr 2026 13:56:57 +0200 Subject: [PATCH] M9 #3: flush UserHomeCache on soft disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full ConnectedServerRegistry was scoped out of this phase — SwiftUI view lifecycle already tears down transports via .onDisappear when ScarfGoTabRoot unmounts on state transition to .serverList. Adding a formal registry that tracks every active transport per ServerID is complexity without proven UX payoff right now (can revisit post pass-2 if users hit stale-connection bugs). One real cleanup we should always do on soft disconnect: invalidate the shared UserHomeCache entry for the server we're leaving. The cache lives forever otherwise, and a hypothetical scenario where the remote user's home directory changes between sessions would surface as SFTP paths resolving to the wrong directory. Rare, but free to fix. `RootModel.softDisconnect()` now calls the new static `ServerContext.invalidateCachedHome(forServerID:)` before flipping state to `.serverList`. Static form is a convenience for callers that have the ServerID in hand but not a full ServerContext (avoids forcing a round-trip through config store just to rebuild the context we're already discarding). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ScarfCore/Models/ServerContext.swift | 7 +++++++ scarf/Scarf iOS/App/ScarfIOSApp.swift | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/ServerContext.swift b/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/ServerContext.swift index ae6e0f2..bdfdbf6 100644 --- a/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/ServerContext.swift +++ b/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/ServerContext.swift @@ -219,6 +219,13 @@ extension ServerContext { public static func invalidateCaches(for contextID: ServerID) async { await UserHomeCache.shared.invalidate(contextID: contextID) } + + /// Static convenience for callers that have the ServerID but not + /// a full ServerContext (e.g. RootModel.softDisconnect). Mirrors + /// the instance method above. + public static func invalidateCachedHome(forServerID id: ServerID) async { + await UserHomeCache.shared.invalidate(contextID: id) + } } // MARK: - Convenience file I/O via the right transport diff --git a/scarf/Scarf iOS/App/ScarfIOSApp.swift b/scarf/Scarf iOS/App/ScarfIOSApp.swift index 12ddcee..a7a1eb2 100644 --- a/scarf/Scarf iOS/App/ScarfIOSApp.swift +++ b/scarf/Scarf iOS/App/ScarfIOSApp.swift @@ -182,14 +182,19 @@ final class RootModel { state = .connected(id, config, key) } - /// Soft disconnect: close any live transport but keep stored - /// credentials. Returns to the server list so the user can tap - /// another server (or the same one again). + /// Soft disconnect: return to the server list without wiping + /// credentials. Per-view controllers (ChatController, + /// IOSDashboardViewModel, etc.) tear down their transports via + /// SwiftUI `.onDisappear` when ScarfGoTabRoot unmounts; on next + /// connect we get fresh transports. We also flush the shared + /// UserHomeCache entry for the server we're leaving so a future + /// reconnect doesn't reuse a stale `$HOME` probe (minor, but + /// matters if the remote user's home directory changed — rare + /// but possible on shared hosts). func softDisconnect() async { - // Transport teardown is owned by ConnectedServerRegistry - // (added in 3.3); for now the per-view controllers own their - // own lifecycles via .onDisappear, so this is mostly a state - // change. The registry commit will thread through here. + if case .connected(let id, _, _) = state { + await ServerContext.invalidateCachedHome(forServerID: id) + } state = .serverList }