Remove restore workspace

This commit is contained in:
Kaede Akino committed 2026-09-12 23:29:46 +08:00
1 parent c036f1a058
commit 8cc4419d1c
12 files changed
+14 -111

No files matched your search

@@ -11967,16 +11967,6 @@
}
}
},
"Restore Workspaces from Last Session" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "从上一个会话恢复工作区"
}
}
}
},
"Result" : {
"extractionState" : "stale",
"localizations" : {
+5 -6
View File
@@ -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.
-24
View File
@@ -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() }
}
-1
View File
@@ -130,7 +130,6 @@ struct MobileRootView: View {
openSameDirectory: scene.openSameDirectory,
closeWorkspace: scene.closeWorkspace
)
.onAppear { workspace.activateWhenOpened() }
}
}
.sheet(isPresented: $scene.isConnectionPickerPresented) {
+1 -2
View File
@@ -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)
+2 -10
View File
@@ -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()
@@ -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" }
+1 -42
View File
@@ -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)
+1 -4
View File
@@ -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()"))
}
+2 -4
View File
@@ -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)
}
}
-3
View File
@@ -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.
///
@@ -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.