diff --git a/supacode/App/ContentView.swift b/supacode/App/ContentView.swift index f25d26d7..fd87b11f 100644 --- a/supacode/App/ContentView.swift +++ b/supacode/App/ContentView.swift @@ -100,12 +100,6 @@ struct ContentView: View { ) } .background(WindowTabbingDisabler()) - .navigationTitle( - WindowTitle.compute( - repositories: store.repositories, - terminalManager: terminalManager - ) - ) } private func toggleLeftSidebar() { diff --git a/supacode/App/WindowTabbingDisabler.swift b/supacode/App/WindowTabbingDisabler.swift index 4ab75d33..358f7308 100644 --- a/supacode/App/WindowTabbingDisabler.swift +++ b/supacode/App/WindowTabbingDisabler.swift @@ -20,7 +20,8 @@ final class WindowTabbingView: NSView, NSWindowDelegate { func disallowTabbing() { guard let window else { return } window.tabbingMode = .disallowed - window.identifier = NSUserInterfaceItemIdentifier("main") + window.identifier = NSUserInterfaceItemIdentifier(WindowID.main) + window.isExcludedFromWindowsMenu = true if window.delegate !== self { window.delegate = self } diff --git a/supacode/App/WindowTitle.swift b/supacode/App/WindowTitle.swift index f891903e..e228d028 100644 --- a/supacode/App/WindowTitle.swift +++ b/supacode/App/WindowTitle.swift @@ -59,9 +59,13 @@ enum WindowTitle { else { return appName } + // No active tab → drop worktree context so the title doesn't outlive the last tab. + guard let tab = selectedTabTitle(in: terminalState(worktreeID)) else { + return appName + } return format( repository: repositoryDisplayTitle(repositoryID: repositoryID, repositories: repositories), - tab: selectedTabTitle(in: terminalState(worktreeID)) + tab: tab ) } diff --git a/supacode/App/supacodeApp.swift b/supacode/App/supacodeApp.swift index 971cc3b9..9ee203d4 100644 --- a/supacode/App/supacodeApp.swift +++ b/supacode/App/supacodeApp.swift @@ -598,8 +598,11 @@ struct SupacodeApp: App { SidebarCommands(store: store) TerminalCommands(ghosttyShortcuts: ghosttyShortcuts) WindowCommands( + store: store, + terminalManager: terminalManager, ghosttyShortcuts: ghosttyShortcuts, - resolvedKeybindings: store.resolvedKeybindings + resolvedKeybindings: store.resolvedKeybindings, + settingsWindowManager: SettingsWindowManager.shared ) } CommandGroup(after: .textEditing) { diff --git a/supacode/Commands/WindowCommands.swift b/supacode/Commands/WindowCommands.swift index 8b9f766f..067b7a90 100644 --- a/supacode/Commands/WindowCommands.swift +++ b/supacode/Commands/WindowCommands.swift @@ -1,8 +1,13 @@ +import ComposableArchitecture import SwiftUI struct WindowCommands: Commands { + @Bindable var store: StoreOf + let terminalManager: WorktreeTerminalManager let ghosttyShortcuts: GhosttyShortcutManager let resolvedKeybindings: ResolvedKeybindingMap + @Bindable var settingsWindowManager: SettingsWindowManager + @Dependency(SettingsWindowClient.self) private var settingsWindowClient @FocusedValue(\.closeTabAction) private var closeTabAction @FocusedValue(\.closeSurfaceAction) private var closeSurfaceAction @FocusedValue(\.selectPreviousTerminalTabAction) private var selectPreviousTerminalTabAction @@ -34,13 +39,13 @@ struct WindowCommands: Commands { ) } - CommandGroup(replacing: .windowArrangement) { - Button("Prowl") { - NSApp.surfaceMainWindow() - } - .help("Show main window") - Divider() + let mainWindowTitle = WindowTitle.compute( + repositories: store.repositories, + terminalManager: terminalManager + ) + let isSettingsOpen = settingsWindowManager.isOpen + CommandGroup(replacing: .windowArrangement) { Button("Select Previous Tab") { selectPreviousTerminalTabAction?() } @@ -124,6 +129,20 @@ struct WindowCommands: Commands { ) .disabled(selectTerminalPaneRightAction == nil) } + + Divider() + + Button(mainWindowTitle) { + NSApp.surfaceMainWindow() + } + .help("Show main window") + + if isSettingsOpen { + Button("Settings") { + settingsWindowClient.show() + } + .help("Show Settings window") + } } } } diff --git a/supacode/Features/Repositories/Views/WorktreeDetailView.swift b/supacode/Features/Repositories/Views/WorktreeDetailView.swift index 985d6a65..d1a6ca09 100644 --- a/supacode/Features/Repositories/Views/WorktreeDetailView.swift +++ b/supacode/Features/Repositories/Views/WorktreeDetailView.swift @@ -62,7 +62,7 @@ struct WorktreeDetailView: View { selectedTerminalWorktree: selectedTerminalWorktree, selectedWorktreeSummaries: selectedWorktreeSummaries ) - .navigationTitle(repositories.isShowingCanvas ? "Canvas" : "") + .navigationTitle(WindowTitle.compute(repositories: repositories, terminalManager: terminalManager)) .toolbar(removing: repositories.isShowingCanvas ? nil : .title) .toolbar { if repositories.isShowingCanvas { diff --git a/supacode/Features/Settings/BusinessLogic/SettingsWindowManager.swift b/supacode/Features/Settings/BusinessLogic/SettingsWindowManager.swift index 3043ea7d..2096e55a 100644 --- a/supacode/Features/Settings/BusinessLogic/SettingsWindowManager.swift +++ b/supacode/Features/Settings/BusinessLogic/SettingsWindowManager.swift @@ -3,13 +3,17 @@ import ComposableArchitecture import SwiftUI @MainActor +@Observable final class SettingsWindowManager { - static let shared = SettingsWindowManager() + @ObservationIgnored static let shared = SettingsWindowManager() - private var settingsWindow: NSWindow? - private var store: StoreOf? - private var ghosttyShortcuts: GhosttyShortcutManager? - private var commandKeyObserver: CommandKeyObserver? + private(set) var isOpen: Bool = false + + @ObservationIgnored private var settingsWindow: NSWindow? + @ObservationIgnored private var store: StoreOf? + @ObservationIgnored private var ghosttyShortcuts: GhosttyShortcutManager? + @ObservationIgnored private var commandKeyObserver: CommandKeyObserver? + @ObservationIgnored private var willCloseObserver: NSObjectProtocol? private init() {} @@ -29,6 +33,7 @@ final class SettingsWindowManager { existingWindow.deminiaturize(nil) } existingWindow.makeKeyAndOrderFront(nil) + isOpen = true return } @@ -41,9 +46,9 @@ final class SettingsWindowManager { let hostingController = NSHostingController(rootView: settingsView) let window = NSWindow(contentViewController: hostingController) - window.title = "" + window.title = "Settings" window.titleVisibility = .hidden - window.identifier = NSUserInterfaceItemIdentifier("settings") + window.identifier = NSUserInterfaceItemIdentifier(WindowID.settings) window.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView] window.tabbingMode = .disallowed window.titlebarAppearsTransparent = true @@ -53,12 +58,24 @@ final class SettingsWindowManager { window.toolbar?.showsBaselineSeparator = false } window.isReleasedWhenClosed = false + window.isExcludedFromWindowsMenu = true window.setContentSize(NSSize(width: 800, height: 600)) window.minSize = NSSize(width: 750, height: 500) + willCloseObserver = NotificationCenter.default.addObserver( + forName: NSWindow.willCloseNotification, + object: window, + queue: .main + ) { [weak self] _ in + MainActor.assumeIsolated { + self?.isOpen = false + } + } + window.center() window.makeKeyAndOrderFront(nil) settingsWindow = window + isOpen = true } } diff --git a/supacodeTests/WindowTitleTests.swift b/supacodeTests/WindowTitleTests.swift index a845ff05..e280c790 100644 --- a/supacodeTests/WindowTitleTests.swift +++ b/supacodeTests/WindowTitleTests.swift @@ -37,6 +37,37 @@ struct WindowTitleTests { #expect(WindowTitle.compute(repositories: state, terminalManager: manager) == "Archived Worktrees") } + @Test func computeFallsBackToAppNameWhenWorktreeHasNoActiveTab() { + let rootURL = URL(fileURLWithPath: "/tmp/repo") + let worktree = makeWorktree(rootURL: rootURL) + let repository = Repository( + id: rootURL.path(percentEncoded: false), + rootURL: rootURL, + name: "repo", + worktrees: IdentifiedArray(uniqueElements: [worktree]) + ) + var state = RepositoriesFeature.State(repositories: [repository]) + state.selection = .worktree(worktree.id) + + let terminalState = WorktreeTerminalState(runtime: GhosttyRuntime(), worktree: worktree) + let tabId = terminalState.tabManager.createTab(title: "codex", icon: nil) + terminalState.tabManager.closeTab(tabId) + + #expect( + WindowTitle.compute( + repositories: state, + terminalState: { id in id == worktree.id ? terminalState : nil } + ) == "Prowl" + ) + + #expect( + WindowTitle.compute( + repositories: state, + terminalState: { _ in nil } + ) == "Prowl" + ) + } + @Test func computeUsesCustomRepositoryTitleAndSelectedTabTitle() { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = makeWorktree(rootURL: rootURL)