native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add setting to auto-hide tab bar when tmux is active (#218)

Detects tmux via Ghostty shell integration title changes and hides
the tab bar when it is the only running tab. Controlled by a new
"Hide Tab Bar for tmux" toggle in General > Advanced (off by default).

authored by

Stefano Bertagno and committed by
GitHub
dc8eb02e 3ad7b1cc

+91 -23
+1
supacode/Clients/Terminal/TerminalClient.swift
··· 23 23 case prune(Set<Worktree.ID>) 24 24 case setNotificationsEnabled(Bool) 25 25 case setSelectedWorktreeID(Worktree.ID?) 26 + case refreshTmuxTabBarVisibility 26 27 } 27 28 28 29 enum Event: Equatable {
+3
supacode/Features/App/Reducer/AppFeature.swift
··· 307 307 await terminalClient.send(.setNotificationsEnabled(settings.inAppNotificationsEnabled)) 308 308 }, 309 309 .run { _ in 310 + await terminalClient.send(.refreshTmuxTabBarVisibility) 311 + }, 312 + .run { _ in 310 313 await worktreeInfoWatcher.send( 311 314 .setPullRequestTrackingEnabled(settings.githubIntegrationEnabled) 312 315 )
+7
supacode/Features/Settings/Models/GlobalSettings.swift
··· 50 50 var pullRequestMergeStrategy: PullRequestMergeStrategy 51 51 var terminalThemeSyncEnabled: Bool 52 52 var restoreTerminalLayoutEnabled: Bool 53 + var hideTmuxTabBar: Bool 53 54 var autoDeleteArchivedWorktreesAfterDays: AutoDeletePeriod? 54 55 var shortcutOverrides: [AppShortcutID: AppShortcutOverride] 55 56 ··· 76 77 pullRequestMergeStrategy: .merge, 77 78 terminalThemeSyncEnabled: false, 78 79 restoreTerminalLayoutEnabled: false, 80 + hideTmuxTabBar: false, 79 81 defaultWorktreeBaseDirectoryPath: nil, 80 82 autoDeleteArchivedWorktreesAfterDays: nil, 81 83 shortcutOverrides: [:] ··· 104 106 pullRequestMergeStrategy: PullRequestMergeStrategy = .merge, 105 107 terminalThemeSyncEnabled: Bool = false, 106 108 restoreTerminalLayoutEnabled: Bool = false, 109 + hideTmuxTabBar: Bool = false, 107 110 defaultWorktreeBaseDirectoryPath: String? = nil, 108 111 autoDeleteArchivedWorktreesAfterDays: AutoDeletePeriod? = nil, 109 112 shortcutOverrides: [AppShortcutID: AppShortcutOverride] = [:] ··· 130 133 self.pullRequestMergeStrategy = pullRequestMergeStrategy 131 134 self.terminalThemeSyncEnabled = terminalThemeSyncEnabled 132 135 self.restoreTerminalLayoutEnabled = restoreTerminalLayoutEnabled 136 + self.hideTmuxTabBar = hideTmuxTabBar 133 137 self.defaultWorktreeBaseDirectoryPath = defaultWorktreeBaseDirectoryPath 134 138 self.autoDeleteArchivedWorktreesAfterDays = autoDeleteArchivedWorktreesAfterDays 135 139 self.shortcutOverrides = shortcutOverrides ··· 218 222 restoreTerminalLayoutEnabled = 219 223 try container.decodeIfPresent(Bool.self, forKey: .restoreTerminalLayoutEnabled) 220 224 ?? Self.default.restoreTerminalLayoutEnabled 225 + hideTmuxTabBar = 226 + try container.decodeIfPresent(Bool.self, forKey: .hideTmuxTabBar) 227 + ?? Self.default.hideTmuxTabBar 221 228 defaultWorktreeBaseDirectoryPath = 222 229 try container.decodeIfPresent(String.self, forKey: .defaultWorktreeBaseDirectoryPath) 223 230 ?? Self.default.defaultWorktreeBaseDirectoryPath
+4
supacode/Features/Settings/Reducer/SettingsFeature.swift
··· 28 28 var pullRequestMergeStrategy: PullRequestMergeStrategy 29 29 var terminalThemeSyncEnabled: Bool 30 30 var restoreTerminalLayoutEnabled: Bool 31 + var hideTmuxTabBar: Bool 31 32 var defaultWorktreeBaseDirectoryPath: String 32 33 var autoDeleteArchivedWorktreesAfterDays: AutoDeletePeriod? 33 34 var shortcutOverrides: [AppShortcutID: AppShortcutOverride] ··· 62 63 pullRequestMergeStrategy = settings.pullRequestMergeStrategy 63 64 terminalThemeSyncEnabled = settings.terminalThemeSyncEnabled 64 65 restoreTerminalLayoutEnabled = settings.restoreTerminalLayoutEnabled 66 + hideTmuxTabBar = settings.hideTmuxTabBar 65 67 autoDeleteArchivedWorktreesAfterDays = settings.autoDeleteArchivedWorktreesAfterDays 66 68 shortcutOverrides = settings.shortcutOverrides 67 69 defaultWorktreeBaseDirectoryPath = ··· 92 94 pullRequestMergeStrategy: pullRequestMergeStrategy, 93 95 terminalThemeSyncEnabled: terminalThemeSyncEnabled, 94 96 restoreTerminalLayoutEnabled: restoreTerminalLayoutEnabled, 97 + hideTmuxTabBar: hideTmuxTabBar, 95 98 defaultWorktreeBaseDirectoryPath: SupacodePaths.normalizedWorktreeBaseDirectoryPath( 96 99 defaultWorktreeBaseDirectoryPath 97 100 ), ··· 182 185 state.pullRequestMergeStrategy = normalizedSettings.pullRequestMergeStrategy 183 186 state.terminalThemeSyncEnabled = normalizedSettings.terminalThemeSyncEnabled 184 187 state.restoreTerminalLayoutEnabled = normalizedSettings.restoreTerminalLayoutEnabled 188 + state.hideTmuxTabBar = normalizedSettings.hideTmuxTabBar 185 189 state.autoDeleteArchivedWorktreesAfterDays = normalizedSettings.autoDeleteArchivedWorktreesAfterDays 186 190 state.shortcutOverrides = normalizedSettings.shortcutOverrides 187 191 state.defaultWorktreeBaseDirectoryPath = normalizedSettings.defaultWorktreeBaseDirectoryPath ?? ""
+6
supacode/Features/Settings/Views/AppearanceSettingsView.swift
··· 76 76 } footer: { 77 77 Text("Changes to Analytics require Supacode to restart before they take effect.") 78 78 } 79 + Section("Advanced") { 80 + Toggle(isOn: $store.hideTmuxTabBar) { 81 + Text("Hide Tab Bar for `tmux`") 82 + Text("Applies when `tmux` is the only running tab.") 83 + } 84 + } 79 85 } 80 86 .formStyle(.grouped) 81 87 .padding(.top, -20)
+4
supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift
··· 94 94 prune(keeping: ids) 95 95 case .setNotificationsEnabled(let enabled): 96 96 setNotificationsEnabled(enabled) 97 + case .refreshTmuxTabBarVisibility: 98 + for state in states.values { 99 + state.refreshTmuxTabBarVisibility() 100 + } 97 101 case .setSelectedWorktreeID(let id): 98 102 guard id != selectedWorktreeID else { return } 99 103 if let previousID = selectedWorktreeID, let previousState = states[previousID] {
+39
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 25 25 private var surfaces: [UUID: GhosttySurfaceView] = [:] 26 26 private var focusedSurfaceIdByTab: [TerminalTabID: UUID] = [:] 27 27 var tabIsRunningById: [TerminalTabID: Bool] = [:] 28 + private var tmuxActiveTabIds: Set<TerminalTabID> = [] 29 + private(set) var shouldHideTabBar = false 28 30 private var blockingScripts: [TerminalTabID: BlockingScriptKind] = [:] 29 31 private var blockingScriptLaunchDirectories: [TerminalTabID: URL] = [:] 30 32 private var lastBlockingScriptTabByKind: [BlockingScriptKind: TerminalTabID] = [:] ··· 70 72 blockingScripts.values.contains(kind) 71 73 } 72 74 75 + private func setTmuxActive(_ active: Bool, for tabId: TerminalTabID) { 76 + if active { 77 + tmuxActiveTabIds.insert(tabId) 78 + } else { 79 + tmuxActiveTabIds.remove(tabId) 80 + } 81 + updateShouldHideTabBar() 82 + } 83 + 84 + private func updateShouldHideTabBar() { 85 + @Shared(.settingsFile) var settingsFile 86 + shouldHideTabBar = 87 + settingsFile.global.hideTmuxTabBar 88 + && tabManager.tabs.count == 1 89 + && (tabManager.tabs.first.map { tmuxActiveTabIds.contains($0.id) } ?? false) 90 + } 91 + 92 + func refreshTmuxTabBarVisibility() { 93 + updateShouldHideTabBar() 94 + } 95 + 96 + private func titleIndicatesTmux(_ title: String) -> Bool { 97 + let trimmed = title.trimmingCharacters(in: .whitespaces) 98 + return trimmed == "tmux" || trimmed.hasPrefix("tmux ") 99 + } 100 + 73 101 func ensureInitialTab(focusing: Bool) { 74 102 guard tabManager.tabs.isEmpty else { return } 75 103 guard !isEnsuringInitialTab else { return } ··· 224 252 context: creation.context 225 253 ) 226 254 tabIsRunningById[tabId] = false 255 + updateShouldHideTabBar() 227 256 if creation.focusing, let surface = tree.root?.leftmostLeaf() { 228 257 focusSurface(surface, in: tabId) 229 258 } ··· 360 389 func closeTab(_ tabId: TerminalTabID) { 361 390 let closedBlockingKind = blockingScripts.removeValue(forKey: tabId) 362 391 cleanupBlockingScriptLaunchDirectory(for: tabId) 392 + tmuxActiveTabIds.remove(tabId) 363 393 // Clear lingering tab tracking for completed or non-blocking tabs. 364 394 for (kind, tracked) in lastBlockingScriptTabByKind where tracked == tabId { 365 395 lastBlockingScriptTabByKind.removeValue(forKey: kind) 366 396 } 367 397 removeTree(for: tabId) 368 398 tabManager.closeTab(tabId) 399 + updateShouldHideTabBar() 369 400 if let selected = tabManager.selectedTabId { 370 401 focusSurface(in: selected) 371 402 } else { ··· 903 934 if self.focusedSurfaceIdByTab[tabId] == view.id { 904 935 self.tabManager.updateTitle(tabId, title: title) 905 936 } 937 + if self.titleIndicatesTmux(title) { 938 + self.setTmuxActive(true, for: tabId) 939 + } 906 940 } 907 941 view.bridge.onSplitAction = { [weak self, weak view] action in 908 942 guard let self, let view else { return false } ··· 932 966 } 933 967 view.bridge.onCommandFinished = { [weak self] exitCode in 934 968 guard let self else { return } 969 + if self.tmuxActiveTabIds.contains(tabId) { 970 + self.setTmuxActive(false, for: tabId) 971 + } 935 972 self.handleBlockingScriptCommandFinished(tabId: tabId, exitCode: exitCode) 936 973 } 937 974 view.bridge.onChildExited = { [weak self] exitCode in ··· 1190 1227 trees.removeValue(forKey: tabId) 1191 1228 focusedSurfaceIdByTab.removeValue(forKey: tabId) 1192 1229 cleanupBlockingScriptLaunchDirectory(for: tabId) 1230 + tmuxActiveTabIds.remove(tabId) 1193 1231 tabManager.closeTab(tabId) 1232 + updateShouldHideTabBar() 1194 1233 if let kind = blockingScripts.removeValue(forKey: tabId) { 1195 1234 lastBlockingScriptTabByKind.removeValue(forKey: kind) 1196 1235
+27 -23
supacode/Features/Terminal/Views/WorktreeTerminalTabsView.swift
··· 12 12 var body: some View { 13 13 let state = manager.state(for: worktree) { shouldRunSetupScript } 14 14 VStack(spacing: 0) { 15 - TerminalTabBarView( 16 - manager: state.tabManager, 17 - createTab: createTab, 18 - splitHorizontally: { 19 - _ = state.performBindingActionOnFocusedSurface("new_split:down") 20 - }, 21 - splitVertically: { 22 - _ = state.performBindingActionOnFocusedSurface("new_split:right") 23 - }, 24 - canSplit: state.tabManager.selectedTabId != nil, 25 - closeTab: { tabId in 26 - state.closeTab(tabId) 27 - }, 28 - closeOthers: { tabId in 29 - state.closeOtherTabs(keeping: tabId) 30 - }, 31 - closeToRight: { tabId in 32 - state.closeTabsToRight(of: tabId) 33 - }, 34 - closeAll: { 35 - state.closeAllTabs() 36 - } 37 - ) 15 + if !state.shouldHideTabBar { 16 + TerminalTabBarView( 17 + manager: state.tabManager, 18 + createTab: createTab, 19 + splitHorizontally: { 20 + _ = state.performBindingActionOnFocusedSurface("new_split:down") 21 + }, 22 + splitVertically: { 23 + _ = state.performBindingActionOnFocusedSurface("new_split:right") 24 + }, 25 + canSplit: state.tabManager.selectedTabId != nil, 26 + closeTab: { tabId in 27 + state.closeTab(tabId) 28 + }, 29 + closeOthers: { tabId in 30 + state.closeOtherTabs(keeping: tabId) 31 + }, 32 + closeToRight: { tabId in 33 + state.closeTabsToRight(of: tabId) 34 + }, 35 + closeAll: { 36 + state.closeAllTabs() 37 + } 38 + ) 39 + .transition(.move(edge: .top).combined(with: .opacity)) 40 + } 38 41 if let selectedId = state.tabManager.selectedTabId { 39 42 TerminalTabContentStack(tabs: state.tabManager.tabs, selectedTabId: selectedId) { tabId in 40 43 TerminalSplitTreeAXContainer(tree: state.splitTree(for: tabId)) { operation in ··· 45 48 EmptyTerminalPaneView(message: "No terminals open") 46 49 } 47 50 } 51 + .animation(.easeInOut(duration: 0.2), value: state.shouldHideTabBar) 48 52 .background( 49 53 WindowFocusObserverView { activity in 50 54 windowActivity = activity