diff --git a/Frameworks/Resources/Localizable.xcstrings b/Frameworks/Resources/Localizable.xcstrings index 8bd92149..c1330d8c 100644 --- a/Frameworks/Resources/Localizable.xcstrings +++ b/Frameworks/Resources/Localizable.xcstrings @@ -11967,16 +11967,6 @@ } } }, - "Restore Workspaces from Last Session" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "从上一个会话恢复工作区" - } - } - } - }, "Result" : { "extractionState" : "stale", "localizations" : { diff --git a/Memory/ARCHITECTURE.md b/Memory/ARCHITECTURE.md index 527d38a2..124b5cc8 100644 --- a/Memory/ARCHITECTURE.md +++ b/Memory/ARCHITECTURE.md @@ -309,10 +309,9 @@ live path exists. Explicit close never retries. Initial connection failure and recovery stay in Connection Journey. Background-task expiration closes transports but retains intent; foreground resumes existing -workspaces, while cold restoration connects only when opened. Restoration stores only -descriptor ID, host reference, title, and kind—never commands, paths, credentials, or -actions. Host deletion hides pending records before restoration, runs the shared -outbox saga, and cancels sessions, transfers, forwarding, and local history. +workspaces. Workspaces exist only for the current app process and are never restored +after relaunch. Host deletion runs the shared outbox saga and cancels sessions, +transfers, forwarding, and local history. One serialized Live Activity represents all Workspaces with counts and up to six distribution icons. Validated links open a Workspace or list and carry no secrets or @@ -332,8 +331,8 @@ Each mobile scene owns one navigation/command model and publishes it as a focuse scene value, so hardware-keyboard commands never operate another iPad window. Mobile uses the macOS shortcuts for capabilities that exist on both platforms: navigation, connection/creation, terminal Find and Command Generator, Workspace close/switch, -and Assistant send/stop. Workspace switching activates lazy restoration but never -claims terminal focus. The UIKit terminal surface owns F3/Shift-F3 and replaces +and Assistant send/stop. Workspace switching never reconnects the session or claims +terminal focus. The UIKit terminal surface owns F3/Shift-F3 and replaces Ghostty's bare/Shift-Escape commands only while Find is presented; otherwise Escape continues to the terminal unchanged. diff --git a/TFI/App/MobileAppModel.swift b/TFI/App/MobileAppModel.swift index 8c3f1407..d3e1e0dc 100644 --- a/TFI/App/MobileAppModel.swift +++ b/TFI/App/MobileAppModel.swift @@ -195,20 +195,6 @@ final class MobileAppModel { metricsSync?.didApplyRemote = { [weak self] snapshot, _ in self?.metrics.core.applyRemoteSnapshot(snapshot) } - if let data = self.defaults?.data(forKey: "mobile.workspaces.v1") { - let descriptors = try JSONDecoder().decode([MobileWorkspaceDescriptor].self, from: data) - workspaces = descriptors.filter { library.host($0.hostID) != nil }.map { - MobileWorkspace( - descriptor: $0, hosts: library, transfers: transfers, terminalSettings: terminalSettings, - commandHistory: commandHistory, sftpHistory: sftpHistory, - connectionServices: workspaceConnectionServices, - connectsWhenOpened: true) - } - workspaceActivitySessions = workspaces.map(\.activitySession) - for workspace in workspaces { - observeActivityState(of: workspace) - } - } } func open( @@ -244,7 +230,6 @@ final class MobileAppModel { guard let workspace = workspaces.first(where: { $0.id == id }), workspace.rename(to: title) else { return false } - persistWorkspaces() return true } @@ -256,7 +241,6 @@ final class MobileAppModel { workspaces.removeAll { $0.id == id } workspaceActivitySessions.removeAll { $0.id == id } liveActivities.update(workspaceActivitySessions) - persistWorkspaces() } func closeAll() { @@ -270,7 +254,6 @@ final class MobileAppModel { workspace.disconnect() } liveActivities.update(workspaceActivitySessions) - persistWorkspaces() } func deleteHost(_ id: UUID) { @@ -387,12 +370,6 @@ final class MobileAppModel { liveActivities.focus(id, in: workspaceActivitySessions) } - private func persistWorkspaces() { - do { - defaults?.set(try JSONEncoder().encode(workspaces.map(\.descriptor)), forKey: "mobile.workspaces.v1") - } catch { self.error = error.localizedDescription } - } - private func makeWorkspace( host: Host, kind: MobileWorkspaceDescriptor.Kind, startup: MobileWorkspaceStartup = .none ) -> MobileWorkspace { @@ -433,7 +410,6 @@ final class MobileAppModel { for workspace in added { observeActivityState(of: workspace) } workspaces.append(contentsOf: added) workspaceActivitySessions.append(contentsOf: added.map(\.activitySession)) - persistWorkspaces() liveActivities.start(workspaceActivitySessions) for workspace in added { workspace.connect() } } diff --git a/TFI/App/MobileRootView.swift b/TFI/App/MobileRootView.swift index 3d84fd42..668e799e 100644 --- a/TFI/App/MobileRootView.swift +++ b/TFI/App/MobileRootView.swift @@ -130,7 +130,6 @@ struct MobileRootView: View { openSameDirectory: scene.openSameDirectory, closeWorkspace: scene.closeWorkspace ) - .onAppear { workspace.activateWhenOpened() } } } .sheet(isPresented: $scene.isConnectionPickerPresented) { diff --git a/TFI/App/MobileSceneModel.swift b/TFI/App/MobileSceneModel.swift index a868c607..041e7aad 100644 --- a/TFI/App/MobileSceneModel.swift +++ b/TFI/App/MobileSceneModel.swift @@ -92,8 +92,7 @@ final class MobileSceneModel { } func selectWorkspace(_ id: MobileWorkspace.ID) { - guard let workspace = app.workspaces.first(where: { $0.id == id }) else { return } - workspace.activateWhenOpened() + guard app.workspaces.contains(where: { $0.id == id }) else { return } selectedDestination = .workspaces workspaceID = id app.focusWorkspaceActivity(id) diff --git a/TFI/Features/Workspaces/MobileWorkspace.swift b/TFI/Features/Workspaces/MobileWorkspace.swift index 75d202d4..7b2d55d1 100644 --- a/TFI/Features/Workspaces/MobileWorkspace.swift +++ b/TFI/Features/Workspaces/MobileWorkspace.swift @@ -13,7 +13,6 @@ import struct TermindDomain.Host private enum MobileWorkspaceConnectionResumeIntent: Equatable { case none - case whenOpened case foreground } @@ -202,8 +201,7 @@ final class MobileWorkspace: Identifiable { reconnectController: SessionReconnectController = SessionReconnectController(), initialCommand: String? = nil, recordsInitialCommandInHistory: Bool = true, - initialSFTPPath: String? = nil, - connectsWhenOpened: Bool = false + initialSFTPPath: String? = nil ) { self.terminalSettings = terminalSettings self.commandHistory = commandHistory @@ -225,7 +223,7 @@ final class MobileWorkspace: Identifiable { } self.connectionServices = connectionServices connection = RemoteSessionConnectionController(reconnect: reconnectController) - connectionResumeIntent = connectsWhenOpened ? .whenOpened : .none + connectionResumeIntent = .none let relay = MobileTerminalRelay() backend = InMemoryTerminalSession( write: { data in @@ -734,12 +732,6 @@ final class MobileWorkspace: Identifiable { } } - func activateWhenOpened() { - guard connectionResumeIntent == .whenOpened else { return } - connectionResumeIntent = .none - connect() - } - func suspendForBackgroundExpiration() { guard connectionResumeIntent != .foreground, hasActiveConnectionFlow else { return } cancelPendingUpload() diff --git a/TFI/Features/Workspaces/MobileWorkspaceDescriptor.swift b/TFI/Features/Workspaces/MobileWorkspaceDescriptor.swift index 60338529..b733ec46 100644 --- a/TFI/Features/Workspaces/MobileWorkspaceDescriptor.swift +++ b/TFI/Features/Workspaces/MobileWorkspaceDescriptor.swift @@ -1,7 +1,7 @@ import Foundation -nonisolated struct MobileWorkspaceDescriptor: Codable, Hashable, Identifiable, Sendable { - enum Kind: String, Codable, CaseIterable, Sendable { +nonisolated struct MobileWorkspaceDescriptor: Hashable, Identifiable, Sendable { + enum Kind: String, CaseIterable, Sendable { case ssh, sftp var title: String { self == .ssh ? "SSH" : "SFTP" } var symbol: String { self == .ssh ? "terminal" : "folder" } diff --git a/TFITests/MobileAppTests.swift b/TFITests/MobileAppTests.swift index baa4b841..5a3f1539 100644 --- a/TFITests/MobileAppTests.swift +++ b/TFITests/MobileAppTests.swift @@ -49,14 +49,6 @@ struct MobileAppTests { @Suite @MainActor struct MobileWorkspaceTests { - @Test func descriptorRoundTripContainsOnlyRestorationMetadata() throws { - let descriptor = MobileWorkspaceDescriptor(hostID: UUID(), title: "Production", kind: .ssh) - let data = try JSONEncoder().encode(descriptor) - #expect(try JSONDecoder().decode(MobileWorkspaceDescriptor.self, from: data) == descriptor) - let keys = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) - #expect(Set(keys.keys) == ["id", "hostID", "title", "kind"]) - } - @Test func liveActivitiesUseStableDistinctWorkspaceDestinations() { let first = MobileWorkspaceDescriptor(hostID: UUID(), title: "Server", kind: .ssh) var renamed = first @@ -66,7 +58,7 @@ struct MobileWorkspaceTests { #expect(first.destinationURL != second.destinationURL) } - @Test func renamingUpdatesRestorationAndActivityStateWithoutChangingIdentity() throws { + @Test func renamingUpdatesActivityStateWithoutChangingIdentity() throws { let descriptor = MobileWorkspaceDescriptor(hostID: UUID(), title: "Production", kind: .ssh) let workspace = MobileWorkspace(descriptor: descriptor, hosts: HostLibraryModel()) var activityTitle: String? @@ -79,11 +71,6 @@ struct MobileWorkspaceTests { #expect(workspace.activitySession.title == "Release Shell") #expect(activityTitle == "Release Shell") #expect(!workspace.rename(to: "Release Shell")) - - let data = try JSONEncoder().encode(workspace.descriptor) - let restored = try JSONDecoder().decode(MobileWorkspaceDescriptor.self, from: data) - #expect(restored.title == "Release Shell") - #expect(restored.id == descriptor.id) } @Test func closeAllDisconnectsAndRemovesEveryWorkspaceAndIsRepeatable() throws { @@ -270,33 +257,6 @@ struct MobileWorkspaceConnectionRecoveryTests { #expect(workspace.operationError == nil) } - @Test func restoredWorkspaceConnectsOnlyWhenFirstOpened() async throws { - let (host, library) = try connectionFixture() - let connections = MobileSFTPConnectionBox(clients: [MobileTestSFTPClient(), MobileTestSFTPClient()]) - let first = MobileWorkspace( - descriptor: MobileWorkspaceDescriptor(hostID: host.id, title: "First", kind: .sftp), - hosts: library, connectionServices: connections.services, connectsWhenOpened: true) - let second = MobileWorkspace( - descriptor: MobileWorkspaceDescriptor(hostID: host.id, title: "Second", kind: .sftp), - hosts: library, connectionServices: connections.services, connectsWhenOpened: true) - defer { - first.disconnect() - second.disconnect() - } - - #expect(connections.connectCount == 0) - #expect(!first.journey.isPresented && !second.journey.isPresented) - - first.activateWhenOpened() - - #expect(first.journey.isPresented) - #expect(!second.journey.isPresented) - #expect(await eventually { first.connected }) - #expect(connections.connectCount == 1) - first.activateWhenOpened() - #expect(connections.connectCount == 1) - } - @Test func explicitDisconnectDoesNotCreateAResumeIntent() async throws { let (host, library) = try connectionFixture() let connections = MobileSFTPConnectionBox(clients: [MobileTestSFTPClient(), MobileTestSFTPClient()]) @@ -308,7 +268,6 @@ struct MobileWorkspaceConnectionRecoveryTests { workspace.disconnect() workspace.resumeAfterBackground() - workspace.activateWhenOpened() #expect(!workspace.connected) #expect(!workspace.journey.isPresented) diff --git a/TFITests/MobileNavigationParityTests.swift b/TFITests/MobileNavigationParityTests.swift index 0b018f8b..4876311b 100644 --- a/TFITests/MobileNavigationParityTests.swift +++ b/TFITests/MobileNavigationParityTests.swift @@ -415,12 +415,9 @@ struct MobileNavigationParityTests { #expect(metrics.contains("app.metrics.core.setEnabled(false, for: server)")) #expect(metrics.contains("dismiss()")) - let rootView = try String( - contentsOf: root.appending(path: "TFI/App/MobileRootView.swift"), encoding: .utf8) - #expect(rootView.contains("workspace.activateWhenOpened()")) let appModel = try String( contentsOf: root.appending(path: "TFI/App/MobileAppModel.swift"), encoding: .utf8) - #expect(appModel.contains("connectsWhenOpened: true")) + #expect(!appModel.contains("mobile.workspaces.v1")) #expect(appModel.contains("workspace.resumeAfterBackground()")) } diff --git a/TFITests/MobileSnippetTests.swift b/TFITests/MobileSnippetTests.swift index cca548d5..b59797fa 100644 --- a/TFITests/MobileSnippetTests.swift +++ b/TFITests/MobileSnippetTests.swift @@ -162,9 +162,7 @@ struct MobileSnippetTests { #expect(workspace.consumePendingInitialCommand() == "uname -a") #expect(workspace.consumePendingInitialCommand() == nil) - let restoredDescriptor = try JSONDecoder().decode( - MobileWorkspaceDescriptor.self, from: JSONEncoder().encode(descriptor)) - let restored = MobileWorkspace(descriptor: restoredDescriptor, hosts: HostLibraryModel()) - #expect(restored.pendingInitialCommand == nil) + let separateWorkspace = MobileWorkspace(descriptor: descriptor, hosts: HostLibraryModel()) + #expect(separateWorkspace.pendingInitialCommand == nil) } } diff --git a/TFM/App/DesktopSettingsKey.swift b/TFM/App/DesktopSettingsKey.swift index 4b804bdd..b0815f6b 100644 --- a/TFM/App/DesktopSettingsKey.swift +++ b/TFM/App/DesktopSettingsKey.swift @@ -41,9 +41,6 @@ nonisolated enum DesktopSettingsKey { static let onboardingQuickOpenCompleted = "onboarding.quickOpen.completed" static let onboardingStatusBarPathCompleted = "onboarding.statusBarPath.completed" static let onboardingCommandGeneratorCompleted = "onboarding.commandGenerator.completed" - /// Restore the previous window's Workspaces on launch. Keep the persisted - /// key stable so this UI rename does not discard the user's preference. - static let restoreWorkspaces = "window.restoreTabs" /// Keep Termind behind whatever the user is doing when macOS opens it at /// login. Only the launch is quiet; the window is built as usual. /// diff --git a/TFM/Features/Settings/SettingsPageView.swift b/TFM/Features/Settings/SettingsPageView.swift index a1fdbc51..18097d9b 100644 --- a/TFM/Features/Settings/SettingsPageView.swift +++ b/TFM/Features/Settings/SettingsPageView.swift @@ -525,7 +525,6 @@ private struct StartupSettings: View { @State private var loginItem = LoginItemController() @AppStorage(DesktopSettingsKey.openAtLoginQuietly) private var opensQuietly = DesktopSettingsKey.defaultOpenAtLoginQuietly - @AppStorage(DesktopSettingsKey.restoreWorkspaces) private var restoreWorkspaces = true var body: some View { Section("Startup") { @@ -567,8 +566,6 @@ private struct StartupSettings: View { if let message = loginItem.errorMessage { Text(message).font(.caption).foregroundStyle(.orange) } - - Toggle("Restore Workspaces from Last Session", isOn: $restoreWorkspaces) } // The registration lives in System Settings, where it can be revoked // without telling us. Re-read it whenever this window is looked at.