refactor(history): remove redundant closed workspace state
This commit is contained in:
1 parent
231be54e08
commit
59fbd11030
18 files changed
+126
-147
No files matched your search
@@ -19,7 +19,6 @@ struct ConnectionHistoryMenuTests {
|
||||
let local = UUID()
|
||||
let repository = InMemoryConnectionHistoryRepository()
|
||||
let history = ConnectionHistoryModel(repository: repository, sourceDeviceID: local)
|
||||
let devices = DeviceDirectoryModel(currentDeviceID: local)
|
||||
let library = HostLibraryModel(hosts: [host])
|
||||
let first = try #require(history.record(kind: .ssh, for: host.id, at: now))
|
||||
let second = try #require(history.record(kind: .sftp, for: host.id, at: now.addingTimeInterval(1)))
|
||||
@@ -29,13 +28,13 @@ struct ConnectionHistoryMenuTests {
|
||||
history.reload()
|
||||
|
||||
let menu = ConnectionHistoryMenuSnapshot(
|
||||
history: history, library: library, devices: devices, now: now, calendar: calendar)
|
||||
history: history, library: library, now: now, calendar: calendar)
|
||||
#expect(menu.recentToday.map(\.id) == [synced.id, second.id, first.id])
|
||||
#expect(menu.days.isEmpty)
|
||||
|
||||
let restarted = ConnectionHistoryModel(repository: repository, sourceDeviceID: local)
|
||||
let afterRestart = ConnectionHistoryMenuSnapshot(
|
||||
history: restarted, library: library, devices: devices, now: now, calendar: calendar)
|
||||
history: restarted, library: library, now: now, calendar: calendar)
|
||||
#expect(afterRestart.recentToday.map(\.id) == [synced.id, second.id, first.id])
|
||||
#expect(afterRestart.days.isEmpty)
|
||||
}
|
||||
@@ -49,7 +48,7 @@ struct ConnectionHistoryMenuTests {
|
||||
try #require(history.record(kind: .ssh, for: host.id, at: today.addingTimeInterval(Double(index + 1))))
|
||||
}
|
||||
let menu = ConnectionHistoryMenuSnapshot(
|
||||
history: history, library: HostLibraryModel(hosts: [host]), devices: DeviceDirectoryModel(),
|
||||
history: history, library: HostLibraryModel(hosts: [host]),
|
||||
now: today.addingTimeInterval(3600), calendar: calendar)
|
||||
#expect(menu.recentToday.map(\.id) == entries.suffix(15).reversed().map(\.id))
|
||||
#expect(menu.days.map(\.isToday) == [true, false])
|
||||
@@ -68,10 +67,9 @@ struct ConnectionHistoryMenuTests {
|
||||
let missing = ConnectionHistoryEntry(
|
||||
hostID: UUID(), kind: .ssh, connectedAt: today.addingTimeInterval(-200_000))
|
||||
let history = ConnectionHistoryModel(entries: [earlier, missing, recent])
|
||||
let devices = DeviceDirectoryModel()
|
||||
let library = HostLibraryModel(hosts: [host])
|
||||
let menu = ConnectionHistoryMenuSnapshot(
|
||||
history: history, library: library, devices: devices, now: now, calendar: calendar)
|
||||
history: history, library: library, now: now, calendar: calendar)
|
||||
#expect(menu.recentToday.map(\.id) == [recent.id])
|
||||
#expect(menu.days.map(\.isToday) == [false])
|
||||
#expect(menu.days.flatMap(\.rows).map(\.id) == [earlier.id])
|
||||
@@ -79,11 +77,11 @@ struct ConnectionHistoryMenuTests {
|
||||
var shifted = calendar
|
||||
shifted.timeZone = TimeZone(secondsFromGMT: 8 * 3600)!
|
||||
let localMenu = ConnectionHistoryMenuSnapshot(
|
||||
history: history, library: library, devices: devices, now: today.addingTimeInterval(120), calendar: shifted)
|
||||
history: history, library: library, now: today.addingTimeInterval(120), calendar: shifted)
|
||||
#expect(localMenu.days.isEmpty)
|
||||
#expect(localMenu.recentToday.map(\.id) == [recent.id, earlier.id])
|
||||
let tomorrow = ConnectionHistoryMenuSnapshot(
|
||||
history: history, library: library, devices: devices,
|
||||
history: history, library: library,
|
||||
now: today.addingTimeInterval(86_400), calendar: calendar)
|
||||
#expect(tomorrow.recentToday.isEmpty)
|
||||
#expect(tomorrow.days.allSatisfy { !$0.isToday })
|
||||
@@ -96,15 +94,14 @@ struct ConnectionHistoryMenuTests {
|
||||
hostID: host.id, kind: .sftp, connectedAt: calendar.startOfDay(for: now).addingTimeInterval(-60))
|
||||
let history = ConnectionHistoryModel(entries: [old])
|
||||
let library = HostLibraryModel(hosts: [host])
|
||||
let devices = DeviceDirectoryModel()
|
||||
_ = try #require(history.record(kind: .ssh, for: host.id, at: now))
|
||||
let before = ConnectionHistoryMenuSnapshot(
|
||||
history: history, library: library, devices: devices, now: now, calendar: calendar)
|
||||
history: history, library: library, now: now, calendar: calendar)
|
||||
#expect(before.recentToday.first?.canOpen == false)
|
||||
#expect(before.days.first?.rows.first?.canOpen == false)
|
||||
history.clearAll()
|
||||
let after = ConnectionHistoryMenuSnapshot(
|
||||
history: history, library: library, devices: devices, now: now, calendar: calendar)
|
||||
history: history, library: library, now: now, calendar: calendar)
|
||||
#expect(after.recentToday.isEmpty && after.days.isEmpty)
|
||||
#expect(library.host(host.id) != nil)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import struct TermindDomain.Host
|
||||
public struct ConnectionHistoryMenuRow: Identifiable {
|
||||
public let entry: ConnectionHistoryEntry
|
||||
public let host: Host
|
||||
public let deviceName: String?
|
||||
public var id: UUID { entry.id }
|
||||
public var canOpen: Bool {
|
||||
switch entry.kind {
|
||||
@@ -30,7 +29,7 @@ public struct ConnectionHistoryMenuSnapshot {
|
||||
|
||||
@MainActor
|
||||
public init(
|
||||
history: ConnectionHistoryModel, library: HostLibraryModel, devices: DeviceDirectoryModel,
|
||||
history: ConnectionHistoryModel, library: HostLibraryModel,
|
||||
now: Date? = nil, calendar: Calendar = .current
|
||||
) {
|
||||
let rows = history.entries().sorted {
|
||||
@@ -38,9 +37,7 @@ public struct ConnectionHistoryMenuSnapshot {
|
||||
return $0.id.uuidString < $1.id.uuidString
|
||||
}.compactMap { entry -> ConnectionHistoryMenuRow? in
|
||||
guard let host = library.host(entry.hostID) else { return nil }
|
||||
return ConnectionHistoryMenuRow(
|
||||
entry: entry, host: host,
|
||||
deviceName: history.isLocal(entry) ? nil : devices.name(for: entry.sourceDeviceID))
|
||||
return ConnectionHistoryMenuRow(entry: entry, host: host)
|
||||
}
|
||||
let referenceDate = now ?? history.menuReferenceDate
|
||||
recentToday = Array(
|
||||
|
||||
@@ -83,7 +83,7 @@ public struct HistoryMenuRecords: View {
|
||||
open(row)
|
||||
} label: {
|
||||
Label {
|
||||
Text(verbatim: title(row))
|
||||
Text(verbatim: row.host.name)
|
||||
} icon: {
|
||||
image(row.entry.kind.symbol)
|
||||
}
|
||||
@@ -91,6 +91,4 @@ public struct HistoryMenuRecords: View {
|
||||
.disabled(!row.canOpen)
|
||||
}
|
||||
}
|
||||
|
||||
private func title(_ row: ConnectionHistoryMenuRow) -> String { row.host.name }
|
||||
}
|
||||
@@ -468,8 +468,8 @@ final class MobileAppModel {
|
||||
}
|
||||
|
||||
private func closeWorkspaces(for hostID: UUID) {
|
||||
forgetClosedWorkspaces(for: hostID)
|
||||
for session in workspaceSessions {
|
||||
session.forgetClosedWorkspaces(for: hostID)
|
||||
for item in session.orderedWorkspaceItems where item.descriptor.hostID == hostID {
|
||||
session.close(item.id, rememberForReopen: false)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ final class MobileSceneModel {
|
||||
var confirmsClearConnectionHistory = false
|
||||
|
||||
var historyMenuSnapshot: ConnectionHistoryMenuSnapshot {
|
||||
ConnectionHistoryMenuSnapshot(history: app.connectionHistory, library: app.library, devices: app.devices)
|
||||
ConnectionHistoryMenuSnapshot(history: app.connectionHistory, library: app.library)
|
||||
}
|
||||
var hasConnectionHistory: Bool { !app.connectionHistory.entries().isEmpty }
|
||||
|
||||
|
||||
@@ -55,10 +55,6 @@ final class MobileWorkspaceSessionModel {
|
||||
var canReopen: Bool { !isDiscarded && !app.closedWorkspaceRecords.isEmpty }
|
||||
var reopensWindow: Bool { app.closedWorkspaceRecords.first?.isWindow == true }
|
||||
var recentlyClosedRecords: [RecentlyClosedRecord<MobileWorkspaceDescriptor>] { app.closedWorkspaceRecords }
|
||||
var recentlyClosedWorkspaces: [MobileWorkspaceDescriptor] {
|
||||
guard !isDiscarded else { return [] }
|
||||
return app.closedWorkspaceRecords.filter { !$0.isWindow }.flatMap(\.items)
|
||||
}
|
||||
private(set) var isDiscarded = false
|
||||
@ObservationIgnored private unowned let app: MobileAppModel
|
||||
|
||||
@@ -301,10 +297,6 @@ final class MobileWorkspaceSessionModel {
|
||||
}
|
||||
}
|
||||
|
||||
func forgetClosedWorkspaces(for hostID: Host.ID) {
|
||||
app.forgetClosedWorkspaces(for: hostID)
|
||||
}
|
||||
|
||||
func reopenLastClosed() -> MobileWorkspace.ID? {
|
||||
guard !isDiscarded, let record = app.closedWorkspaceRecords.first else { return nil }
|
||||
if record.isWindow {
|
||||
|
||||
@@ -26,14 +26,14 @@ struct MobileWorkspaceSessionTests {
|
||||
let nextApp = try fixture.makeApp()
|
||||
let window = MobileSceneModel(app: nextApp, sessionID: "second")
|
||||
let other = MobileSceneModel(app: nextApp, sessionID: "other")
|
||||
#expect(window.workspaceSession.recentlyClosedWorkspaces.map(\.id) == [closed.id])
|
||||
#expect(window.workspaceSession.recentlyClosedRecords.flatMap(\.items).map(\.id) == [closed.id])
|
||||
window.open(hostID: fixture.host.id, kind: .ssh)
|
||||
window.activatePendingWorkspace()
|
||||
let current = try #require(window.activeWorkspace)
|
||||
window.closeActiveWorkspace()
|
||||
#expect(window.workspaceSession.recentlyClosedWorkspaces.map(\.id) == [current.id, closed.id])
|
||||
#expect(window.workspaceSession.recentlyClosedRecords.flatMap(\.items).map(\.id) == [current.id, closed.id])
|
||||
window.reopenLastClosedWorkspace()
|
||||
#expect(window.workspaceSession.recentlyClosedWorkspaces.map(\.id) == [closed.id])
|
||||
#expect(window.workspaceSession.recentlyClosedRecords.flatMap(\.items).map(\.id) == [closed.id])
|
||||
window.reopenLastClosedWorkspace()
|
||||
window.activatePendingWorkspace()
|
||||
#expect(window.activeWorkspace?.descriptor.kind == .sftp)
|
||||
@@ -102,9 +102,9 @@ struct MobileWorkspaceSessionTests {
|
||||
let app = try fixture.makeApp()
|
||||
let session = app.workspaceSession(for: "owner")
|
||||
session.closeAll()
|
||||
#expect(session.recentlyClosedWorkspaces.map(\.id) == records.reversed().map(\.id))
|
||||
#expect(session.recentlyClosedRecords.flatMap(\.items).map(\.id) == records.reversed().map(\.id))
|
||||
#expect(session.reopenClosedWorkspace(records[0].id) == records[0].id)
|
||||
#expect(session.recentlyClosedWorkspaces.map(\.id) == [records[2].id, records[1].id])
|
||||
#expect(session.recentlyClosedRecords.flatMap(\.items).map(\.id) == [records[2].id, records[1].id])
|
||||
#expect(session.reopenClosedWorkspace(records[0].id) == nil)
|
||||
#expect(session.reopenLastClosed() == records[2].id)
|
||||
}
|
||||
@@ -231,6 +231,22 @@ struct MobileWorkspaceSessionTests {
|
||||
#expect(session.workspaces.isEmpty)
|
||||
}
|
||||
|
||||
@Test func deletingAHostBeforeBindingAWindowRemovesPersistedClosedWorkspaces() throws {
|
||||
let fixture = try Fixture()
|
||||
defer { fixture.cleanUp() }
|
||||
let firstApp = try fixture.makeApp()
|
||||
let session = firstApp.workspaceSession(for: "owner")
|
||||
let id = try #require(session.open(hostID: fixture.host.id, kind: .ssh))
|
||||
session.close(id)
|
||||
|
||||
let nextApp = try fixture.makeApp()
|
||||
#expect(nextApp.recentlyClosedStore.entries.flatMap(\.items).map(\.id) == [id])
|
||||
#expect(nextApp.deleteHost(fixture.host.id))
|
||||
#expect(nextApp.recentlyClosedStore.entries.isEmpty)
|
||||
let thirdApp = try fixture.makeApp()
|
||||
#expect(thirdApp.recentlyClosedStore.entries.isEmpty)
|
||||
}
|
||||
|
||||
@Test func agentForwardingStaysWithTheConversationScene() async throws {
|
||||
let fixture = try Fixture()
|
||||
defer { fixture.cleanUp() }
|
||||
|
||||
@@ -709,12 +709,6 @@ final class AppModel {
|
||||
AssistantExecutionController.titleContext(for: conversation)
|
||||
}
|
||||
|
||||
func forgetClosedWorkspaces(_ ids: Set<UUID>) {
|
||||
for window in windows.values.compactMap(\.value) {
|
||||
for id in ids { window.forgetClosedWorkspace(id) }
|
||||
}
|
||||
}
|
||||
|
||||
func unregisterWindow(_ window: WindowModel) {
|
||||
windows[ObjectIdentifier(window)] = nil
|
||||
onboarding.windowClosed(window)
|
||||
@@ -725,12 +719,10 @@ final class AppModel {
|
||||
configureConnectionHistory(in: window)
|
||||
window.connectionCoordinator = connections
|
||||
window.hostStore = hosts
|
||||
window.closedHistory = { [weak self] in self?.closedWorkspaceRecords ?? [] }
|
||||
window.rememberClosedWorkspace = { [weak self] in self?.rememberClosedWorkspaces([$0]) }
|
||||
window.rememberClosedWindow = { [weak self] workspaces, selected in
|
||||
self?.rememberClosedWorkspaces(workspaces, isWindow: true, selectedID: selected)
|
||||
}
|
||||
window.restoreClosedRecord = { [weak self, weak window] id in self?.reopenClosedRecord(id, in: window) }
|
||||
window.metricsModel = serverMetrics
|
||||
windows = windows.filter { $0.value.value != nil }
|
||||
windows[ObjectIdentifier(window)] = WeakWindow(window)
|
||||
|
||||
@@ -49,7 +49,6 @@ extension AppModel {
|
||||
window.select(restored[selected].id)
|
||||
}
|
||||
let removed = Set(items.map(\.id))
|
||||
forgetClosedWorkspaces(removed)
|
||||
record.items.removeAll { removed.contains($0.id) }
|
||||
closedWorkspaceInstances[id]?.removeAll { removed.contains($0.id) }
|
||||
if record.items.isEmpty {
|
||||
|
||||
@@ -297,7 +297,7 @@ struct TermindCommands: Commands {
|
||||
|
||||
HistoryMenuRecords(
|
||||
snapshot: ConnectionHistoryMenuSnapshot(
|
||||
history: app.connectionHistory, library: app.hostLibrary, devices: app.devices),
|
||||
history: app.connectionHistory, library: app.hostLibrary),
|
||||
image: { Image(nsImage: MenuImageVisibilityController.markedImage(systemName: $0)) }
|
||||
) { row in
|
||||
guard let window, let host = app.hostLibrary.host(row.host.id) else { return }
|
||||
|
||||
@@ -277,15 +277,6 @@ final class WindowModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Closed workspaces and individually closed panes, most recent last. A closed
|
||||
/// pane is kept as a single-pane Workspace. ⇧⌘T pops from here. The Workspace retains
|
||||
/// its layout, names, and conversation, while `prepareForReopen` replaces
|
||||
/// terminal models because a dead SSH session cannot be revived.
|
||||
private var reopenStack: [Workspace] {
|
||||
get { workspaceDeck.reopenStack }
|
||||
set { workspaceDeck.reopenStack = newValue }
|
||||
}
|
||||
|
||||
var commandFocusChanged: (() -> Void)?
|
||||
var configureCommandNotifications: ((Pane) -> Void)?
|
||||
|
||||
@@ -545,17 +536,8 @@ final class WindowModel {
|
||||
}
|
||||
}
|
||||
|
||||
var closedHistory: () -> [RecentlyClosedRecord<ClosedWorkspaceRecord>] = { [] }
|
||||
var rememberClosedWorkspace: ((Workspace) -> Void)?
|
||||
var rememberClosedWindow: (([Workspace], UUID?) -> Void)?
|
||||
var restoreClosedRecord: ((UUID) -> Void)?
|
||||
var canReopen: Bool {
|
||||
!isClosed && (restoreClosedRecord == nil ? !reopenStack.isEmpty : !closedHistory().isEmpty)
|
||||
}
|
||||
var reopensWindow: Bool { closedHistory().first?.isWindow == true }
|
||||
var recentlyClosedWorkspaces: [Workspace] { Array(reopenStack.reversed()) }
|
||||
|
||||
func forgetClosedWorkspace(_ id: UUID) { reopenStack.removeAll { $0.id == id } }
|
||||
var confirmsClearConnectionHistory = false
|
||||
var historyOperationError: String?
|
||||
|
||||
@@ -1397,7 +1379,6 @@ final class WindowModel {
|
||||
workspace.lifecycleRouter = nil
|
||||
}
|
||||
workspaces.removeAll()
|
||||
reopenStack.removeAll()
|
||||
dragging = nil
|
||||
presentations.route = nil
|
||||
transcriptCache.removeAll()
|
||||
@@ -1412,7 +1393,7 @@ final class WindowModel {
|
||||
guard let index = workspaces.firstIndex(where: { $0.id == id }) else { return }
|
||||
let removed = workspaces.remove(at: index)
|
||||
removed.disconnectConnections()
|
||||
rememberForReopen(removed)
|
||||
rememberClosedWorkspace?(removed)
|
||||
|
||||
if selectedWorkspaceID == id {
|
||||
// Select the neighbour that visually takes its place.
|
||||
@@ -1436,7 +1417,7 @@ final class WindowModel {
|
||||
func closeOthers(than id: Workspace.ID) {
|
||||
for workspace in workspaces where workspace.id != id {
|
||||
workspace.disconnectConnections()
|
||||
rememberForReopen(workspace)
|
||||
rememberClosedWorkspace?(workspace)
|
||||
}
|
||||
workspaces.removeAll { $0.id != id }
|
||||
selectedWorkspaceID = id
|
||||
@@ -1445,7 +1426,7 @@ final class WindowModel {
|
||||
func closeAll() {
|
||||
for workspace in workspaces {
|
||||
workspace.disconnectConnections()
|
||||
rememberForReopen(workspace)
|
||||
rememberClosedWorkspace?(workspace)
|
||||
}
|
||||
workspaces.removeAll()
|
||||
promoteVisibleSidebarConversationOrSelectVault()
|
||||
@@ -1487,13 +1468,7 @@ final class WindowModel {
|
||||
// session. prepareForReopen restores routing when it becomes live again.
|
||||
pane.sshSession?.inputRouter = nil
|
||||
pane.sftpChannel?.navigationRouter = nil
|
||||
rememberForReopen(closedWorkspace)
|
||||
}
|
||||
|
||||
private func rememberForReopen(_ workspace: Workspace) {
|
||||
reopenStack.append(workspace)
|
||||
rememberClosedWorkspace?(workspace)
|
||||
if reopenStack.count > 15 { reopenStack.removeFirst(reopenStack.count - 15) }
|
||||
rememberClosedWorkspace?(closedWorkspace)
|
||||
}
|
||||
|
||||
func closeWorkspaces(for hostID: Host.ID) {
|
||||
@@ -1509,23 +1484,6 @@ final class WindowModel {
|
||||
}
|
||||
}
|
||||
|
||||
func reopenLastClosed() {
|
||||
let candidate = restoreClosedRecord == nil ? reopenStack.last?.id : closedHistory().first?.id
|
||||
guard let id = candidate else { return }
|
||||
reopenClosedWorkspace(id)
|
||||
}
|
||||
|
||||
func reopenClosedWorkspace(_ id: Workspace.ID) {
|
||||
if let restoreClosedRecord {
|
||||
restoreClosedRecord(id)
|
||||
return
|
||||
}
|
||||
guard let index = reopenStack.firstIndex(where: { $0.id == id }) else { return }
|
||||
let workspace = reopenStack.remove(at: index)
|
||||
workspace.prepareForReopen()
|
||||
open(workspace)
|
||||
}
|
||||
|
||||
// MARK: - Reordering
|
||||
|
||||
/// Drag-reorder in the Navigator: puts `id` at `index` in the Workspace
|
||||
|
||||
@@ -3,13 +3,11 @@ import TermindApplication
|
||||
import TermindDomain
|
||||
import TermindPresentation
|
||||
|
||||
/// Window-scoped ownership for the dynamic workspace collection and its
|
||||
/// recoverable close history. Navigation remains in `WindowModel`; workspace
|
||||
/// lifetime no longer shares storage with Assistant and Vault state.
|
||||
/// Window-scoped ownership for the dynamic workspace collection.
|
||||
/// Navigation remains in `WindowModel`; recoverable close history belongs to the app.
|
||||
@Observable
|
||||
final class WorkspaceDeckModel {
|
||||
var workspaces: [Workspace]
|
||||
var reopenStack: [Workspace] = []
|
||||
|
||||
init(workspaces: [Workspace] = []) {
|
||||
self.workspaces = workspaces
|
||||
|
||||
@@ -11,6 +11,29 @@ import struct TermindDomain.Host
|
||||
|
||||
@Suite @MainActor
|
||||
struct ClosedWorkspaceRestoreTests {
|
||||
@Test func evictedHistoryReleasesWorkspacesAcrossWindows() {
|
||||
let app = AppModel(hosts: HostLibraryModel())
|
||||
let firstWindow = WindowModel()
|
||||
let secondWindow = WindowModel()
|
||||
app.registerWindow(firstWindow)
|
||||
app.registerWindow(secondWindow)
|
||||
weak var evicted: Workspace?
|
||||
do {
|
||||
let workspace = Workspace(pane: Pane(.empty))
|
||||
evicted = workspace
|
||||
firstWindow.open(workspace)
|
||||
firstWindow.close(workspace.id)
|
||||
}
|
||||
#expect(evicted != nil)
|
||||
for _ in 0..<15 {
|
||||
let workspace = Workspace(pane: Pane(.empty))
|
||||
secondWindow.open(workspace)
|
||||
secondWindow.close(workspace.id)
|
||||
}
|
||||
#expect(app.closedWorkspaceRecords.count == 15)
|
||||
#expect(evicted == nil)
|
||||
}
|
||||
|
||||
@Test func restartKeepsClosedSplitsInOneRecentList() throws {
|
||||
let name = "ClosedWorkspace.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
@@ -34,27 +57,24 @@ struct ClosedWorkspaceRestoreTests {
|
||||
let nextApp = AppModel(hosts: hosts, dependencies: dependencies)
|
||||
let window = WindowModel()
|
||||
nextApp.registerWindow(window)
|
||||
#expect(window.recentlyClosedWorkspaces.isEmpty)
|
||||
#expect(window.closedHistory().flatMap(\.items).map(\.title) == ["Operations"])
|
||||
#expect(window.canReopen)
|
||||
#expect(nextApp.closedWorkspaceRecords.flatMap(\.items).map(\.title) == ["Operations"])
|
||||
#expect(!nextApp.closedWorkspaceRecords.isEmpty)
|
||||
let current = Workspace.sshSession(host: host)
|
||||
window.open(current)
|
||||
window.close(current.id)
|
||||
#expect(window.closedHistory().count == 2)
|
||||
window.reopenLastClosed()
|
||||
#expect(nextApp.closedWorkspaceRecords.count == 2)
|
||||
nextApp.reopenClosedRecord(try #require(nextApp.closedWorkspaceRecords.first?.id), in: window)
|
||||
#expect(window.selectedWorkspace === current)
|
||||
window.reopenLastClosed()
|
||||
nextApp.reopenClosedRecord(try #require(nextApp.closedWorkspaceRecords.first?.id), in: window)
|
||||
let restored = try #require(window.selectedWorkspace)
|
||||
#expect(restored !== workspace)
|
||||
#expect(restored.panes.map(\.kind) == [.sshSession, .sftpChannel])
|
||||
#expect(restored.panes.map(\.customTitle) == ["Shell", "Files"])
|
||||
#expect(restored.customTitle == "Operations")
|
||||
#expect(restored.focusedPane.kind == .sftpChannel)
|
||||
#expect(!window.canReopen)
|
||||
#expect(nextApp.closedWorkspaceRecords.isEmpty)
|
||||
let thirdApp = AppModel(hosts: hosts, dependencies: dependencies)
|
||||
let thirdWindow = WindowModel()
|
||||
thirdApp.registerWindow(thirdWindow)
|
||||
#expect(!thirdWindow.canReopen)
|
||||
#expect(thirdApp.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test func closingAWindowCreatesOneGroupAndReopenWaitsForANewWindow() throws {
|
||||
@@ -73,11 +93,11 @@ struct ClosedWorkspaceRestoreTests {
|
||||
#expect(app.closedWorkspaceRecords.count == 1)
|
||||
let current = WindowModel()
|
||||
app.registerWindow(current)
|
||||
#expect(current.reopensWindow)
|
||||
#expect(app.closedWorkspaceRecords.first?.isWindow == true)
|
||||
var requests: [UUID] = []
|
||||
app.mainWindows.createScene = { requests.append($0) }
|
||||
current.reopenLastClosed()
|
||||
current.reopenLastClosed()
|
||||
app.reopenClosedRecord(record.id, in: current)
|
||||
app.reopenClosedRecord(record.id, in: current)
|
||||
#expect(requests.count == 1)
|
||||
#expect(current.workspaces.isEmpty)
|
||||
#expect(app.closedWorkspaceRecords.count == 1)
|
||||
@@ -107,7 +127,7 @@ struct ClosedWorkspaceRestoreTests {
|
||||
app.restoreClosedRecord(id, into: destination, itemID: first.id)
|
||||
#expect(destination.selectedWorkspace === first)
|
||||
#expect(app.closedWorkspaceRecords.first?.items.map(\.id) == [second.id])
|
||||
#expect(destination.reopensWindow)
|
||||
#expect(app.closedWorkspaceRecords.first?.isWindow == true)
|
||||
}
|
||||
|
||||
@Test func missingHostsAreNotOfferedAfterRestart() throws {
|
||||
@@ -117,8 +137,8 @@ struct ClosedWorkspaceRestoreTests {
|
||||
let window = WindowModel()
|
||||
app.registerWindow(window)
|
||||
app.recentlyClosedStore.record(RecentlyClosedRecord(items: [record]))
|
||||
#expect(!window.canReopen)
|
||||
window.reopenClosedWorkspace(record.id)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
app.reopenClosedRecord(record.id, in: window)
|
||||
#expect(window.workspaces.isEmpty)
|
||||
}
|
||||
}
|
||||
@@ -206,7 +206,7 @@ struct CommandNotificationTests {
|
||||
other.close(moved.id)
|
||||
#expect(app.commandNotificationTarget(firstID) == nil)
|
||||
#expect(app.selectCommandNotificationTarget(firstID) == nil)
|
||||
other.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: other)
|
||||
#expect(app.commandNotificationTarget(firstID) == nil)
|
||||
#expect(first.commandSessionID != firstID)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ struct HistoryMenuRecordTests {
|
||||
let history = ConnectionHistoryModel(entries: [earlier, yesterday] + recent)
|
||||
let current = try #require(history.record(kind: .ssh, for: host.id, at: now))
|
||||
let snapshot = ConnectionHistoryMenuSnapshot(
|
||||
history: history, library: HostLibraryModel(hosts: [host]), devices: DeviceDirectoryModel(), now: now)
|
||||
history: history, library: HostLibraryModel(hosts: [host]), now: now)
|
||||
var opened: [UUID] = []
|
||||
let menu = NSHostingMenu(
|
||||
rootView: HistoryMenuRecords(
|
||||
|
||||
@@ -42,7 +42,7 @@ struct MultiWindowTests {
|
||||
|
||||
app.mainWindows.register(targetNative, model: target, requestID: request)
|
||||
#expect(source.workspaces.isEmpty)
|
||||
#expect(!source.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
#expect(!source.isClosed)
|
||||
#expect(source.vaultDestination == .welcome)
|
||||
#expect(target.selectedWorkspace === workspace)
|
||||
@@ -122,7 +122,7 @@ struct MultiWindowTests {
|
||||
#expect(session.status == .disconnected)
|
||||
app.mainWindows.register(second, model: target, requestID: request)
|
||||
#expect(target.workspaces.isEmpty)
|
||||
#expect(!source.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.first?.isWindow == true)
|
||||
source.closeForWindow()
|
||||
session.status = .connecting
|
||||
let phase = session.connectionJourney.phase
|
||||
@@ -276,14 +276,14 @@ struct MultiWindowTests {
|
||||
app.mainWindows.closeSelectionOrWindow(in: model)
|
||||
|
||||
#expect(model.workspaces.isEmpty)
|
||||
#expect(model.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.count == 2)
|
||||
#expect(firstSession.status == .disconnected)
|
||||
#expect(!model.isClosed)
|
||||
|
||||
app.mainWindows.closeSelectionOrWindow(in: model)
|
||||
|
||||
#expect(model.isClosed)
|
||||
#expect(!model.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.count == 2)
|
||||
}
|
||||
|
||||
@Test(arguments: [WindowDestination.vault, .settings, .assistant, .metrics])
|
||||
@@ -310,7 +310,7 @@ struct MultiWindowTests {
|
||||
#expect(model.selectedDestination == destination)
|
||||
#expect(model.workspaces.first === workspace)
|
||||
#expect(session.status == .connected)
|
||||
#expect(!model.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true])
|
||||
|
||||
@@ -33,17 +33,19 @@ struct WindowStateTests {
|
||||
let first = Workspace(pane: Pane(.empty))
|
||||
let second = Workspace(pane: Pane(.empty))
|
||||
let window = WindowModel(workspaces: [first, second])
|
||||
let app = AppModel(hosts: HostLibraryModel())
|
||||
app.registerWindow(window)
|
||||
window.close(first.id)
|
||||
window.close(second.id)
|
||||
#expect(window.recentlyClosedWorkspaces.map(\.id) == [second.id, first.id])
|
||||
window.reopenClosedWorkspace(first.id)
|
||||
#expect(app.closedWorkspaceRecords.flatMap(\.items).map(\.id) == [second.id, first.id])
|
||||
app.reopenClosedRecord(first.id, in: window)
|
||||
#expect(window.selectedWorkspace === first)
|
||||
#expect(window.recentlyClosedWorkspaces.map(\.id) == [second.id])
|
||||
window.reopenClosedWorkspace(first.id)
|
||||
#expect(app.closedWorkspaceRecords.flatMap(\.items).map(\.id) == [second.id])
|
||||
app.reopenClosedRecord(first.id, in: window)
|
||||
#expect(window.workspaces.count == 1)
|
||||
window.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
|
||||
#expect(window.selectedWorkspace === second)
|
||||
#expect(!window.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test(arguments: [WindowDestination.vault, .metrics, .settings, .assistant])
|
||||
@@ -1277,12 +1279,14 @@ struct WindowStateTests {
|
||||
pane.customTitle = "Logs"
|
||||
let closedSession = try #require(pane.sshSession)
|
||||
let window = WindowModel(workspaces: [workspace])
|
||||
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
|
||||
app.registerWindow(window)
|
||||
|
||||
window.close(workspace.id)
|
||||
#expect(closedSession.status == .disconnected)
|
||||
#expect(window.canReopen)
|
||||
#expect(!app.closedWorkspaceRecords.isEmpty)
|
||||
|
||||
window.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
|
||||
|
||||
let reopenedPane = try #require(window.selectedWorkspace?.focusedPane)
|
||||
let reopenedSession = try #require(reopenedPane.sshSession)
|
||||
@@ -1291,7 +1295,7 @@ struct WindowStateTests {
|
||||
#expect(reopenedSession.status == .connecting)
|
||||
#expect(reopenedPane.customTitle == "Logs")
|
||||
#expect(window.selectedWorkspace === workspace)
|
||||
#expect(!window.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true])
|
||||
@@ -1308,6 +1312,8 @@ struct WindowStateTests {
|
||||
workspace.splitFocusedPane(axis: .vertical, with: closedPane)
|
||||
workspace.broadcastsInput = true
|
||||
let window = WindowModel(workspaces: [workspace])
|
||||
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
|
||||
app.registerWindow(window)
|
||||
|
||||
if closeSelected {
|
||||
window.closeSelected()
|
||||
@@ -1315,11 +1321,11 @@ struct WindowStateTests {
|
||||
window.closePane(closedPane.id)
|
||||
}
|
||||
|
||||
#expect(window.canReopen)
|
||||
#expect(!app.closedWorkspaceRecords.isEmpty)
|
||||
#expect(closedSession.status == .disconnected)
|
||||
#expect(workspace.panes.map(\.id) == [survivor.id])
|
||||
|
||||
window.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
|
||||
|
||||
let reopened = try #require(window.selectedWorkspace)
|
||||
let reopenedSession = try #require(reopened.focusedPane.sshSession)
|
||||
@@ -1335,7 +1341,7 @@ struct WindowStateTests {
|
||||
#expect(survivorSession.status == .connected)
|
||||
#expect(survivorSession.inputRouter === workspace)
|
||||
#expect(!workspace.broadcastsInput && !reopened.broadcastsInput)
|
||||
#expect(!window.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1347,30 +1353,34 @@ struct WindowStateTests {
|
||||
workspace.splitFocusedPane(axis: .horizontal, with: second)
|
||||
let other = Workspace.sshSession(host: host)
|
||||
let window = WindowModel(workspaces: [workspace, other])
|
||||
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
|
||||
app.registerWindow(window)
|
||||
|
||||
window.closePane(second.id)
|
||||
window.close(other.id)
|
||||
window.closePane(first.id)
|
||||
#expect(window.workspaces.isEmpty)
|
||||
|
||||
window.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
|
||||
#expect(window.selectedWorkspace === workspace)
|
||||
#expect(workspace.panes.map(\.id) == [first.id])
|
||||
window.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
|
||||
#expect(window.selectedWorkspace === other)
|
||||
window.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
|
||||
let reopened = try #require(window.selectedWorkspace)
|
||||
#expect(reopened.panes.map(\.id) == [second.id])
|
||||
#expect(reopened.focusedPane.sftpChannel?.path == "/var/log")
|
||||
#expect(window.workspaces.count == 3)
|
||||
#expect(!window.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func individuallyClosedPanesShareTheFifteenItemReopenLimit() {
|
||||
func individuallyClosedPanesShareTheFifteenItemReopenLimit() throws {
|
||||
let survivor = Pane(.empty)
|
||||
let workspace = Workspace(pane: survivor)
|
||||
let window = WindowModel(workspaces: [workspace])
|
||||
let app = AppModel(hosts: HostLibraryModel())
|
||||
app.registerWindow(window)
|
||||
let closedPanes = (0..<16).map { _ in Pane(.empty) }
|
||||
for pane in closedPanes {
|
||||
workspace.splitFocusedPane(axis: .horizontal, with: pane)
|
||||
@@ -1378,17 +1388,17 @@ struct WindowStateTests {
|
||||
}
|
||||
|
||||
var reopenedIDs: [Pane.ID] = []
|
||||
while window.canReopen {
|
||||
window.reopenLastClosed()
|
||||
while !app.closedWorkspaceRecords.isEmpty {
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
|
||||
if let pane = window.selectedWorkspace?.focusedPane { reopenedIDs.append(pane.id) }
|
||||
}
|
||||
#expect(reopenedIDs == closedPanes.suffix(15).reversed().map(\.id))
|
||||
#expect(workspace.panes.map(\.id) == [survivor.id])
|
||||
|
||||
window.closePane(reopenedIDs[0])
|
||||
#expect(window.canReopen)
|
||||
#expect(!app.closedWorkspaceRecords.isEmpty)
|
||||
window.closeForWindow()
|
||||
#expect(!window.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.first?.isWindow == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1480,7 +1490,7 @@ struct WindowStateTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func normalRemoteExitClosesOnlyItsSplitPane() {
|
||||
func normalRemoteExitClosesOnlyItsSplitPane() throws {
|
||||
let firstHost = Host(name: "First", host: "first.example")
|
||||
let secondHost = Host(name: "Second", host: "second.example")
|
||||
let workspace = Workspace.sshSession(host: firstHost)
|
||||
@@ -1491,6 +1501,8 @@ struct WindowStateTests {
|
||||
workspace.focus(firstPane.id)
|
||||
workspace.broadcastsInput = true
|
||||
let window = WindowModel(workspaces: [workspace])
|
||||
let app = AppModel(hosts: HostLibraryModel(hosts: [firstHost, secondHost]))
|
||||
app.registerWindow(window)
|
||||
|
||||
firstSSHSession.handleRemoteExit(.success(0))
|
||||
|
||||
@@ -1500,13 +1512,13 @@ struct WindowStateTests {
|
||||
#expect(workspace.focusedPaneID == secondPane.id)
|
||||
#expect(!workspace.broadcastsInput)
|
||||
#expect(firstSSHSession.inputRouter == nil)
|
||||
#expect(window.canReopen)
|
||||
#expect(!app.closedWorkspaceRecords.isEmpty)
|
||||
|
||||
window.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
|
||||
|
||||
#expect(window.selectedWorkspace?.focusedPane.id == firstPane.id)
|
||||
#expect(window.selectedWorkspace?.focusedPane.sshSession !== firstSSHSession)
|
||||
#expect(!window.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -55,7 +55,7 @@ struct WorkspaceWindowMoveTests {
|
||||
#expect(workspace.lifecycleRouter === source)
|
||||
#expect(source.sidebar.sessionContext.workingDirectory == "/srv")
|
||||
#expect(target.sidebar.sessionContext.workingDirectory == nil)
|
||||
#expect(!source.canReopen && !target.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
#expect(ssh.status == .connected && sftp.status == .connected)
|
||||
#expect(sftp.path == "/srv")
|
||||
#expect(Set(history.entries().map(\.id)) == historyIDs)
|
||||
@@ -100,7 +100,7 @@ struct WorkspaceWindowMoveTests {
|
||||
#expect(source.workspaces.isEmpty)
|
||||
#expect(source.selectedDestination == .vault)
|
||||
#expect(source.vaultDestination == .welcome)
|
||||
#expect(!source.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true])
|
||||
@@ -134,10 +134,10 @@ struct WorkspaceWindowMoveTests {
|
||||
#expect(session.status == .disconnected)
|
||||
#expect(target.workspaces.isEmpty)
|
||||
#expect(source.pane(moved.id) == nil)
|
||||
#expect(source.canReopen == !closesWindow)
|
||||
#expect(app.closedWorkspaceRecords.first?.isWindow == closesWindow)
|
||||
#expect(app.mainWindows.movePane(moved.id, from: source) == nil)
|
||||
if !closesWindow {
|
||||
source.reopenLastClosed()
|
||||
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: source)
|
||||
#expect(source.selectedWorkspace?.focusedPane.id == moved.id)
|
||||
#expect(source.selectedWorkspace?.focusedPane.sshSession !== session)
|
||||
#expect(target.workspaces.isEmpty)
|
||||
@@ -197,7 +197,7 @@ struct WorkspaceWindowMoveTests {
|
||||
#expect(first.selectedWorkspace?.focusedPane === firstPane)
|
||||
#expect(second.selectedWorkspace?.focusedPane === secondPane)
|
||||
#expect(source.workspaces.isEmpty)
|
||||
#expect(!source.canReopen)
|
||||
#expect(app.closedWorkspaceRecords.isEmpty)
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true])
|
||||
|
||||
Reference in new issue
Block a user