Remove restore workspace
This commit is contained in:
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" : {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -130,7 +130,6 @@ struct MobileRootView: View {
|
||||
openSameDirectory: scene.openSameDirectory,
|
||||
closeWorkspace: scene.closeWorkspace
|
||||
)
|
||||
.onAppear { workspace.activateWhenOpened() }
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $scene.isConnectionPickerPresented) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()"))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user