From aec0ff421377fc7a51d5611edfebdad9ab14ffff Mon Sep 17 00:00:00 2001 From: arzafran Date: Wed, 30 Sep 2026 14:28:02 -0300 Subject: [PATCH] fix: free the launch window's tab manager when it closes --- Sources/ContentView+SidebarResizer.swift | 7 +++++- Sources/ProgramaApp.swift | 32 +++++++++++++----------- Sources/ProgramaConfig.swift | 14 ++++++----- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Sources/ContentView+SidebarResizer.swift b/Sources/ContentView+SidebarResizer.swift index 6f446c67..a64534d7 100644 --- a/Sources/ContentView+SidebarResizer.swift +++ b/Sources/ContentView+SidebarResizer.swift @@ -206,7 +206,12 @@ extension ContentView { if isResizerBandActive { isResizerBandActive = false } isSidebarResizerCursorActive = false stopSidebarResizerCursorStabilizer() - scheduleSidebarResizerCursorRelease(force: true) + // Release synchronously. A deferred work item held in @State captures this view, and + // once the view has left the hierarchy the state write that clears it is dropped, so a + // pending item would pin the window's TabManager. + sidebarResizerCursorReleaseWorkItem?.cancel() + sidebarResizerCursorReleaseWorkItem = nil + releaseSidebarResizerCursorIfNeeded(force: true) } private func sidebarResizerHandleOverlay( diff --git a/Sources/ProgramaApp.swift b/Sources/ProgramaApp.swift index ab3527e2..3e480b88 100644 --- a/Sources/ProgramaApp.swift +++ b/Sources/ProgramaApp.swift @@ -966,13 +966,17 @@ struct programaApp: App { let workspace = manager.selectedWorkspace let workspaceIndex = workspace.flatMap { selectedWorkspaceIndex(in: manager, workspaceId: $0.id) } let windowMoveTargets = selectedWorkspaceWindowMoveTargets(in: manager) + let hasWorkspace = workspace != nil + + // Action closures resolve the active manager when invoked. Capturing `manager` or + // `workspace` would pin the window's TabManager in the main menu after that window closes. Button( workspace?.isPinned == true ? String(localized: "contextMenu.unpinWorkspace", defaultValue: "Unpin Workspace") : String(localized: "contextMenu.pinWorkspace", defaultValue: "Pin Workspace") ) { - toggleSelectedWorkspacePinned(in: manager) + toggleSelectedWorkspacePinned(in: activeTabManager) } .disabled(workspace == nil) @@ -988,30 +992,30 @@ struct programaApp: App { if workspace?.hasCustomTitle == true { Button(String(localized: "contextMenu.removeCustomWorkspaceName", defaultValue: "Remove Custom Workspace Name")) { - clearSelectedWorkspaceCustomName(in: manager) + clearSelectedWorkspaceCustomName(in: activeTabManager) } } Divider() Button(String(localized: "contextMenu.moveUp", defaultValue: "Move Up")) { - moveSelectedWorkspace(in: manager, by: -1) + moveSelectedWorkspace(in: activeTabManager, by: -1) } .disabled(workspaceIndex == nil || workspaceIndex == 0) Button(String(localized: "contextMenu.moveDown", defaultValue: "Move Down")) { - moveSelectedWorkspace(in: manager, by: 1) + moveSelectedWorkspace(in: activeTabManager, by: 1) } .disabled(workspaceIndex == nil || workspaceIndex == manager.tabs.count - 1) Button(String(localized: "contextMenu.moveToTop", defaultValue: "Move to Top")) { - moveSelectedWorkspaceToTop(in: manager) + moveSelectedWorkspaceToTop(in: activeTabManager) } .disabled(workspace == nil || workspaceIndex == 0) Menu(String(localized: "contextMenu.moveWorkspaceToWindow", defaultValue: "Move Workspace to Window")) { Button(String(localized: "contextMenu.newWindow", defaultValue: "New Window")) { - moveSelectedWorkspaceToNewWindow(in: manager) + moveSelectedWorkspaceToNewWindow(in: activeTabManager) } .disabled(workspace == nil) @@ -1021,9 +1025,9 @@ struct programaApp: App { ForEach(windowMoveTargets) { target in Button(target.label) { - moveSelectedWorkspace(in: manager, toWindow: target.windowId) + moveSelectedWorkspace(in: activeTabManager, toWindow: target.windowId) } - .disabled(target.isCurrentWindow || workspace == nil) + .disabled(target.isCurrentWindow || !hasWorkspace) } } .disabled(workspace == nil) @@ -1031,34 +1035,34 @@ struct programaApp: App { Divider() Button(String(localized: "menu.file.closeWorkspace", defaultValue: "Close Workspace")) { - manager.closeCurrentWorkspaceWithConfirmation() + activeTabManager.closeCurrentWorkspaceWithConfirmation() } .disabled(workspace == nil) Button(String(localized: "contextMenu.closeOtherWorkspaces", defaultValue: "Close Other Workspaces")) { - closeOtherSelectedWorkspacePeers(in: manager) + closeOtherSelectedWorkspacePeers(in: activeTabManager) } .disabled(workspace == nil || manager.tabs.count <= 1) Button(String(localized: "contextMenu.closeWorkspacesBelow", defaultValue: "Close Workspaces Below")) { - closeSelectedWorkspacesBelow(in: manager) + closeSelectedWorkspacesBelow(in: activeTabManager) } .disabled(workspaceIndex == nil || workspaceIndex == manager.tabs.count - 1) Button(String(localized: "contextMenu.closeWorkspacesAbove", defaultValue: "Close Workspaces Above")) { - closeSelectedWorkspacesAbove(in: manager) + closeSelectedWorkspacesAbove(in: activeTabManager) } .disabled(workspaceIndex == nil || workspaceIndex == 0) Divider() Button(String(localized: "contextMenu.markWorkspaceRead", defaultValue: "Mark Workspace as Read")) { - markSelectedWorkspaceRead(in: manager) + markSelectedWorkspaceRead(in: activeTabManager) } .disabled(!selectedWorkspaceHasUnreadNotifications(in: manager)) Button(String(localized: "contextMenu.markWorkspaceUnread", defaultValue: "Mark Workspace as Unread")) { - markSelectedWorkspaceUnread(in: manager) + markSelectedWorkspaceUnread(in: activeTabManager) } .disabled(!selectedWorkspaceHasReadNotifications(in: manager)) } diff --git a/Sources/ProgramaConfig.swift b/Sources/ProgramaConfig.swift index ec42aaec..cfb83077 100644 --- a/Sources/ProgramaConfig.swift +++ b/Sources/ProgramaConfig.swift @@ -387,15 +387,17 @@ final class ProgramaConfigStore: ObservableObject { func wireDirectoryTracking(tabManager: TabManager) { cancellables.removeAll() + // The pipeline keeps only the workspace id and its directory publisher. Operators such as + // removeDuplicates retain their last value, and holding the Workspace itself would pin a + // closed window's workspace until the next wiring. tabManager.$selectedTabId - .compactMap { [weak tabManager] tabId -> Workspace? in - guard let tabId, let tabManager else { return nil } - return tabManager.tabs.first(where: { $0.id == tabId }) + .compactMap { [weak tabManager] tabId -> (id: UUID, directory: AnyPublisher)? in + guard let tabId, + let workspace = tabManager?.tabs.first(where: { $0.id == tabId }) else { return nil } + return (workspace.id, workspace.$currentDirectory.eraseToAnyPublisher()) } .removeDuplicates(by: { $0.id == $1.id }) - .map { workspace -> AnyPublisher in - workspace.$currentDirectory.eraseToAnyPublisher() - } + .map(\.directory) .switchToLatest() .removeDuplicates() .receive(on: DispatchQueue.main)