diff --git a/Sources/RemoteTmuxController+Attach.swift b/Sources/RemoteTmuxController+Attach.swift index fea8a29e215e..d47779e096d0 100644 --- a/Sources/RemoteTmuxController+Attach.swift +++ b/Sources/RemoteTmuxController+Attach.swift @@ -198,8 +198,10 @@ extension RemoteTmuxController { let workspaces = sourceManager.tabs.filter { hostWorkspaceIds.contains($0.id) } // A window holding nothing but this host's mirrors has no reason to stay once they // leave. Emptied, it recovers by opening a fresh local shell, which leaves a blank - // window on screen that nobody asked for. + // window on screen that nobody asked for. Its Dock panels belong to no workspace + // and close with the window, so a window with any of those stays. let holdsOnlyTheseMirrors = workspaces.count == sourceManager.tabs.count + && (AppDelegate.shared?.existingWindowDock(for: sourceManager)?.panels.isEmpty ?? true) for workspace in workspaces { guard let detached = sourceManager.detachWorkspace(tabId: workspace.id) else { continue } targetManager.attachWorkspace(detached, select: false) diff --git a/cmuxTests/RemoteTmuxMirrorCloseDetachTests.swift b/cmuxTests/RemoteTmuxMirrorCloseDetachTests.swift index 2ad352e8330a..46a286987956 100644 --- a/cmuxTests/RemoteTmuxMirrorCloseDetachTests.swift +++ b/cmuxTests/RemoteTmuxMirrorCloseDetachTests.swift @@ -232,6 +232,45 @@ import Testing ) } + /// The window's Dock holds terminals and browsers that belong to no workspace. Closing the + /// window closes them, so a window whose workspaces were all mirrors stays open while its + /// Dock has panels. + @Test func consolidatingMirrorsKeepsAWindowWhoseDockHasPanels() throws { + // The cleanup detach runs ssh to close its master; `true` stands in for it. + let previousSSH = environmentValue(for: sshOverrideKey) + setenv(sshOverrideKey, "/usr/bin/true", 1) + defer { restoreEnvironment(sshOverrideKey, previousValue: previousSSH) } + let source = try Harness() + defer { source.tearDown() } + let target = try Harness() + defer { target.tearDown() } + let host = RemoteTmuxHost(destination: "consolidate-\(UUID().uuidString)@example.test") + let controller = source.controller + controller.cacheConnection(RemoteTmuxControlConnection(host: host, sessionName: "dev")) + defer { + if controller.sessionMirror(host: host, sessionName: "dev") != nil { + controller.detach(host: host, sessionName: "dev") + } + } + #expect(try controller.mirrorSession(host: host, sessionName: "dev", into: source.manager)) + let mirrorWorkspace = try #require(source.manager.tabs.first(where: { $0.isRemoteTmuxMirror })) + source.manager.closeWorkspace(source.workspace, recordHistory: false) + #expect(source.manager.tabs.map(\.id) == [mirrorWorkspace.id]) + let dock = source.appDelegate.windowDock(forWindowId: source.windowId) + let dockPanel = TerminalPanel(workspaceId: dock.workspaceId, runtimeSpawnPolicy: .pacedSessionRestore) + dock.panels[dockPanel.id] = dockPanel + + controller.moveExistingMirrors(for: host, into: target.manager) + + #expect(target.manager.tabs.contains { $0.id == mirrorWorkspace.id }) + #expect( + source.appDelegate.listMainWindowSummaries().contains { $0.windowId == source.windowId }, + "the window closed while its Dock still held a terminal" + ) + #expect(!dock.isRetired) + #expect(dock.panels[dockPanel.id] != nil) + } + /// A window with workspaces of its own keeps them, and gains nothing, when the host's /// mirrors move out. @Test func consolidatingMirrorsLeavesAWindowWithOtherWorkspacesAlone() throws {