From aef63c8ac8eb2981ac3705c03f6d51aabdc15b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Wed, 1 Apr 2026 14:13:45 +0700 Subject: [PATCH 1/2] feat: add option to group all connection tabs in one window --- CHANGELOG.md | 4 ++ TablePro/AppDelegate+ConnectionHandler.swift | 2 +- TablePro/AppDelegate+FileOpen.swift | 2 +- TablePro/AppDelegate+WindowConfig.swift | 40 +++++++++++++++---- .../Infrastructure/WindowOpener.swift | 10 ++++- TablePro/Models/Settings/AppSettings.swift | 5 ++- TablePro/Resources/Localizable.xcstrings | 6 +++ TablePro/ViewModels/WelcomeViewModel.swift | 21 ++++++---- .../Views/Connection/ConnectionFormView.swift | 10 ++++- ...ainContentCoordinator+MultiStatement.swift | 14 +++++-- .../MainContentCoordinator+Navigation.swift | 29 ++++++++++++-- .../Views/Main/MainContentCoordinator.swift | 5 +++ TablePro/Views/Main/MainContentView.swift | 4 +- .../Views/Settings/GeneralSettingsView.swift | 6 +++ .../Toolbar/ConnectionSwitcherPopover.swift | 26 +++++++----- .../Services/WindowTabGroupingTests.swift | 30 ++++++++++++++ docs/customization/settings.mdx | 8 ++++ 17 files changed, 185 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54064cc04..8889b7ea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Option to group all connection tabs in one window instead of separate windows per connection + ## [0.27.1] - 2026-04-01 ### Fixed diff --git a/TablePro/AppDelegate+ConnectionHandler.swift b/TablePro/AppDelegate+ConnectionHandler.swift index d3123e33a..ad0af0051 100644 --- a/TablePro/AppDelegate+ConnectionHandler.swift +++ b/TablePro/AppDelegate+ConnectionHandler.swift @@ -316,7 +316,7 @@ extension AppDelegate { private func openNewConnectionWindow(for connection: DatabaseConnection) { let hadExistingMain = NSApp.windows.contains { isMainWindow($0) && $0.isVisible } - if hadExistingMain { + if hadExistingMain && !AppSettingsManager.shared.tabs.groupAllConnectionTabs { NSWindow.allowsAutomaticWindowTabbing = false } let payload = EditorTabPayload(connectionId: connection.id) diff --git a/TablePro/AppDelegate+FileOpen.swift b/TablePro/AppDelegate+FileOpen.swift index 3e0748fcb..b0fcf3c52 100644 --- a/TablePro/AppDelegate+FileOpen.swift +++ b/TablePro/AppDelegate+FileOpen.swift @@ -176,7 +176,7 @@ extension AppDelegate { } let hadExistingMain = NSApp.windows.contains { isMainWindow($0) && $0.isVisible } - if hadExistingMain { + if hadExistingMain && !AppSettingsManager.shared.tabs.groupAllConnectionTabs { NSWindow.allowsAutomaticWindowTabbing = false } diff --git a/TablePro/AppDelegate+WindowConfig.swift b/TablePro/AppDelegate+WindowConfig.swift index ad1a83515..36a6c1d92 100644 --- a/TablePro/AppDelegate+WindowConfig.swift +++ b/TablePro/AppDelegate+WindowConfig.swift @@ -80,10 +80,12 @@ extension AppDelegate { } catch { windowLogger.error("Dock connection failed for '\(connection.name)': \(error.localizedDescription)") - for window in NSApp.windows where self.isMainWindow(window) { + for window in WindowLifecycleMonitor.shared.windows(for: connection.id) { window.close() } - self.openWelcomeWindow() + if !NSApp.windows.contains(where: { self.isMainWindow($0) && $0.isVisible }) { + self.openWelcomeWindow() + } } } } @@ -251,8 +253,15 @@ extension AppDelegate { // If no code opened this window (pendingId is nil), this is a // SwiftUI WindowGroup state restoration — not a window we created. // Hide it (orderOut, not close) to break the close→restore loop. + // Exception: if the window is already part of a tab group, it was + // attached by our addTabbedWindow call — not a restoration orphan. + // Ordering it out would crash NSWindowStackController. if pendingId == nil && !isAutoReconnecting { configuredWindows.insert(windowId) + if let tabbedWindows = window.tabbedWindows, tabbedWindows.count > 1 { + // Already in a tab group — leave it alone + return + } window.orderOut(nil) return } @@ -260,9 +269,11 @@ extension AppDelegate { let existingIdentifier = NSApp.windows .first { $0 !== window && isMainWindow($0) && $0.isVisible }? .tabbingIdentifier + let groupAll = MainActor.assumeIsolated { AppSettingsManager.shared.tabs.groupAllConnectionTabs } let resolvedIdentifier = TabbingIdentifierResolver.resolve( pendingConnectionId: pendingId, - existingIdentifier: existingIdentifier + existingIdentifier: existingIdentifier, + groupAllConnections: groupAll ) window.tabbingIdentifier = resolvedIdentifier configuredWindows.insert(windowId) @@ -273,10 +284,25 @@ extension AppDelegate { // Explicitly attach to existing tab group — automatic tabbing // doesn't work when tabbingIdentifier is set after window creation. - if let existingWindow = NSApp.windows.first(where: { - $0 !== window && isMainWindow($0) && $0.isVisible - && $0.tabbingIdentifier == resolvedIdentifier - }) { + let matchingWindow: NSWindow? + if groupAll { + // When grouping all connections, attach to any visible main window + // and normalize all existing windows' tabbingIdentifiers so future + // windows also match (not just the first one found). + let existingMainWindows = NSApp.windows.filter { + $0 !== window && isMainWindow($0) && $0.isVisible + } + for existing in existingMainWindows { + existing.tabbingIdentifier = resolvedIdentifier + } + matchingWindow = existingMainWindows.first + } else { + matchingWindow = NSApp.windows.first { + $0 !== window && isMainWindow($0) && $0.isVisible + && $0.tabbingIdentifier == resolvedIdentifier + } + } + if let existingWindow = matchingWindow { let targetWindow = existingWindow.tabbedWindows?.last ?? existingWindow targetWindow.addTabbedWindow(window, ordered: .above) window.makeKeyAndOrderFront(nil) diff --git a/TablePro/Core/Services/Infrastructure/WindowOpener.swift b/TablePro/Core/Services/Infrastructure/WindowOpener.swift index 4df64c4cc..926aa83c9 100644 --- a/TablePro/Core/Services/Infrastructure/WindowOpener.swift +++ b/TablePro/Core/Services/Infrastructure/WindowOpener.swift @@ -48,8 +48,16 @@ internal enum TabbingIdentifierResolver { /// - Parameters: /// - pendingConnectionId: The connectionId from WindowOpener (if a tab was just opened) /// - existingIdentifier: The tabbingIdentifier from an existing visible main window (if any) + /// - groupAllConnections: When true, all windows share one tab group regardless of connection /// - Returns: The tabbingIdentifier to assign to the new window - internal static func resolve(pendingConnectionId: UUID?, existingIdentifier: String?) -> String { + internal static func resolve( + pendingConnectionId: UUID?, + existingIdentifier: String?, + groupAllConnections: Bool = false + ) -> String { + if groupAllConnections { + return "com.TablePro.main" + } if let connectionId = pendingConnectionId { return "com.TablePro.main.\(connectionId.uuidString)" } diff --git a/TablePro/Models/Settings/AppSettings.swift b/TablePro/Models/Settings/AppSettings.swift index fee2f4d71..aa0722871 100644 --- a/TablePro/Models/Settings/AppSettings.swift +++ b/TablePro/Models/Settings/AppSettings.swift @@ -451,14 +451,17 @@ struct HistorySettings: Codable, Equatable { /// Tab behavior settings struct TabSettings: Codable, Equatable { var enablePreviewTabs: Bool = true + var groupAllConnectionTabs: Bool = false static let `default` = TabSettings() - init(enablePreviewTabs: Bool = true) { + init(enablePreviewTabs: Bool = true, groupAllConnectionTabs: Bool = false) { self.enablePreviewTabs = enablePreviewTabs + self.groupAllConnectionTabs = groupAllConnectionTabs } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) enablePreviewTabs = try container.decodeIfPresent(Bool.self, forKey: .enablePreviewTabs) ?? true + groupAllConnectionTabs = try container.decodeIfPresent(Bool.self, forKey: .groupAllConnectionTabs) ?? false } } diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 4fd0f3f22..99f5ad0b4 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -14626,6 +14626,9 @@ } } } + }, + "Group all connections in one window" : { + }, "Group name" : { "localizations" : { @@ -33971,6 +33974,9 @@ } } } + }, + "When enabled, tabs from different connections share the same window instead of opening separate windows." : { + }, "When enabled, this favorite is visible in all connections" : { "localizations" : { diff --git a/TablePro/ViewModels/WelcomeViewModel.swift b/TablePro/ViewModels/WelcomeViewModel.swift index a264523a6..ec7ef7573 100644 --- a/TablePro/ViewModels/WelcomeViewModel.swift +++ b/TablePro/ViewModels/WelcomeViewModel.swift @@ -217,7 +217,7 @@ final class WelcomeViewModel { try await dbManager.connectToSession(connection) } catch is CancellationError { // User cancelled password prompt — return to welcome - NSApplication.shared.closeWindows(withId: "main") + closeConnectionWindows(for: connection.id) self.openWindow?(id: "welcome") } catch { if case PluginError.pluginNotInstalled = error { @@ -226,7 +226,7 @@ final class WelcomeViewModel { } else { Self.logger.error( "Failed to connect: \(error.localizedDescription, privacy: .public)") - handleConnectionFailure(error: error) + handleConnectionFailure(error: error, connectionId: connection.id) } } } @@ -242,12 +242,12 @@ final class WelcomeViewModel { do { try await dbManager.connectToSession(connection) } catch is CancellationError { - NSApplication.shared.closeWindows(withId: "main") + closeConnectionWindows(for: connection.id) self.openWindow?(id: "welcome") } catch { Self.logger.error( "Failed to connect after plugin install: \(error.localizedDescription, privacy: .public)") - handleConnectionFailure(error: error) + handleConnectionFailure(error: error, connectionId: connection.id) } } } @@ -511,9 +511,9 @@ final class WelcomeViewModel { // MARK: - Private Helpers - private func handleConnectionFailure(error: Error) { + private func handleConnectionFailure(error: Error, connectionId: UUID) { guard let openWindow else { return } - NSApplication.shared.closeWindows(withId: "main") + closeConnectionWindows(for: connectionId) openWindow(id: "welcome") AlertHelper.showErrorSheet( @@ -525,8 +525,15 @@ final class WelcomeViewModel { private func handleMissingPlugin(connection: DatabaseConnection) { guard let openWindow else { return } - NSApplication.shared.closeWindows(withId: "main") + closeConnectionWindows(for: connection.id) openWindow(id: "welcome") pluginInstallConnection = connection } + + /// Close windows for a specific connection only, preserving other connections' windows. + private func closeConnectionWindows(for connectionId: UUID) { + for window in WindowLifecycleMonitor.shared.windows(for: connectionId) { + window.close() + } + } } diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index f80a0dfad..5b44a9156 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -966,7 +966,7 @@ struct ConnectionFormView: View { handleMissingPlugin(connection: connection) return } - NSApplication.shared.closeWindows(withId: "main") + closeConnectionWindows(for: connection.id) openWindow(id: "welcome") guard !(error is CancellationError) else { return } Self.logger.error("Failed to connect: \(error.localizedDescription, privacy: .public)") @@ -977,11 +977,17 @@ struct ConnectionFormView: View { } private func handleMissingPlugin(connection: DatabaseConnection) { - NSApplication.shared.closeWindows(withId: "main") + closeConnectionWindows(for: connection.id) openWindow(id: "welcome") pluginInstallConnection = connection } + private func closeConnectionWindows(for connectionId: UUID) { + for window in WindowLifecycleMonitor.shared.windows(for: connectionId) { + window.close() + } + } + private func connectAfterInstall(_ connection: DatabaseConnection) { WindowOpener.shared.pendingConnectionId = connection.id openWindow(id: "main", value: EditorTabPayload(connectionId: connection.id)) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift index cbea6b140..865f34bff 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift @@ -202,8 +202,13 @@ extension MainContentCoordinator { let safeRows = selectResult.rows.map { row in row.map { $0.map { String($0) } } } - let tableName = lastSelectSQL.flatMap { - extractTableName(from: $0) + // For table tabs, preserve existing tableName instead of re-extracting + // from SQL — extractTableName can fail on schema-qualified/quoted names + let tableName: String? + if updatedTab.tabType == .table, let existing = updatedTab.tableName { + tableName = existing + } else { + tableName = lastSelectSQL.flatMap { extractTableName(from: $0) } } updatedTab.resultColumns = safeColumns @@ -215,7 +220,10 @@ extension MainContentCoordinator { updatedTab.resultColumns = [] updatedTab.columnTypes = [] updatedTab.resultRows = [] - updatedTab.tableName = nil + // Preserve tableName for table tabs even when no SELECT result + if updatedTab.tabType != .table { + updatedTab.tableName = nil + } updatedTab.isEditable = false } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index 7cde0c142..8ff889011 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -62,8 +62,10 @@ extension MainContentCoordinator { // Check if another native window tab already has this table open — switch to it if let keyWindow = NSApp.keyWindow { + let ownWindows = Set(WindowLifecycleMonitor.shared.windows(for: connectionId).map { ObjectIdentifier($0) }) let tabbedWindows = keyWindow.tabbedWindows ?? [keyWindow] - for window in tabbedWindows where window.title == tableName { + for window in tabbedWindows + where window.title == tableName && ownWindows.contains(ObjectIdentifier(window)) { window.makeKeyAndOrderFront(nil) return } @@ -219,8 +221,25 @@ extension MainContentCoordinator { } } - // No preview window exists but current tab is already a preview: replace in-place - if let selectedTab = tabManager.selectedTab, selectedTab.isPreview { + // No preview window exists but current tab can be reused: replace in-place. + // This covers: preview tabs, non-preview table tabs with no active work, + // and empty/default query tabs (no user-entered content). + let isReusableTab: Bool = { + guard let tab = tabManager.selectedTab else { return false } + if tab.isPreview { return true } + // Table tab with no active work + if tab.tabType == .table && !changeManager.hasChanges + && !filterStateManager.hasAppliedFilters && !tab.sortState.isSorting { + return true + } + // Empty/default query tab (no user content, no results, never executed) + if tab.tabType == .query && tab.lastExecutedAt == nil + && tab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true + } + return false + }() + if let selectedTab = tabManager.selectedTab, isReusableTab { // Skip if already showing this table if selectedTab.tableName == tableName, selectedTab.databaseName == databaseName { return @@ -346,7 +365,11 @@ extension MainContentCoordinator { private func closeSiblingNativeWindows() { guard let keyWindow = NSApp.keyWindow else { return } let siblings = keyWindow.tabbedWindows ?? [] + let ownWindows = Set(WindowLifecycleMonitor.shared.windows(for: connectionId).map { ObjectIdentifier($0) }) for sibling in siblings where sibling !== keyWindow { + // Only close windows belonging to this connection to avoid + // destroying tabs from other connections when groupAllConnectionTabs is ON + guard ownWindows.contains(ObjectIdentifier(sibling)) else { continue } sibling.close() } } diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index a1b96f659..d7276c9eb 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -845,6 +845,11 @@ final class MainContentCoordinator { if usesNoSQLBrowsing { tableName = tabManager.selectedTab?.tableName isEditable = tableName != nil + } else if tab.tabType == .table, let existingName = tab.tableName { + // Table tabs already know their table name — don't re-extract from SQL + // which can fail for schema-qualified or quoted identifiers + tableName = existingName + isEditable = true } else { tableName = extractTableName(from: effectiveSQL) isEditable = tableName != nil diff --git a/TablePro/Views/Main/MainContentView.swift b/TablePro/Views/Main/MainContentView.swift index 23fade2e5..5543ff3d2 100644 --- a/TablePro/Views/Main/MainContentView.swift +++ b/TablePro/Views/Main/MainContentView.swift @@ -623,7 +623,9 @@ struct MainContentView: View { } else { window.subtitle = connection.name } - window.tabbingIdentifier = "com.TablePro.main.\(connection.id.uuidString)" + window.tabbingIdentifier = AppSettingsManager.shared.tabs.groupAllConnectionTabs + ? "com.TablePro.main" + : "com.TablePro.main.\(connection.id.uuidString)" window.tabbingMode = .preferred coordinator.windowId = windowId diff --git a/TablePro/Views/Settings/GeneralSettingsView.swift b/TablePro/Views/Settings/GeneralSettingsView.swift index 46d50ed56..3fa448859 100644 --- a/TablePro/Views/Settings/GeneralSettingsView.swift +++ b/TablePro/Views/Settings/GeneralSettingsView.swift @@ -82,6 +82,12 @@ struct GeneralSettingsView: View { Text("Single-clicking a table opens a temporary tab that gets replaced on next click.") .font(.caption) .foregroundStyle(.secondary) + + Toggle("Group all connections in one window", isOn: $settingsManager.tabs.groupAllConnectionTabs) + + Text("When enabled, tabs from different connections share the same window instead of opening separate windows.") + .font(.caption) + .foregroundStyle(.secondary) } Section { diff --git a/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift b/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift index c0ab8a619..4d7a71a24 100644 --- a/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift +++ b/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift @@ -340,17 +340,23 @@ struct ConnectionSwitcherPopover: View { } /// Open a new window for a different connection, ensuring it doesn't - /// merge as a tab with the current connection's window group. + /// merge as a tab with the current connection's window group + /// (unless the user opted to group all connections in one window). private func openWindowForDifferentConnection(_ payload: EditorTabPayload) { - // Temporarily disable tab merging so the new window opens independently - let currentWindow = NSApp.keyWindow - let previousMode = currentWindow?.tabbingMode ?? .preferred - currentWindow?.tabbingMode = .disallowed - WindowOpener.shared.pendingConnectionId = payload.connectionId - openWindow(id: "main", value: payload) - // Restore after the next run loop to let window creation complete - DispatchQueue.main.async { - currentWindow?.tabbingMode = previousMode + if AppSettingsManager.shared.tabs.groupAllConnectionTabs { + // Let the window merge into the existing tab group + WindowOpener.shared.openNativeTab(payload) + } else { + // Temporarily disable tab merging so the new window opens independently + let currentWindow = NSApp.keyWindow + let previousMode = currentWindow?.tabbingMode ?? .preferred + currentWindow?.tabbingMode = .disallowed + WindowOpener.shared.pendingConnectionId = payload.connectionId + openWindow(id: "main", value: payload) + // Restore after the next run loop to let window creation complete + DispatchQueue.main.async { + currentWindow?.tabbingMode = previousMode + } } } } diff --git a/TableProTests/Core/Services/WindowTabGroupingTests.swift b/TableProTests/Core/Services/WindowTabGroupingTests.swift index 955068525..09a7e2cc4 100644 --- a/TableProTests/Core/Services/WindowTabGroupingTests.swift +++ b/TableProTests/Core/Services/WindowTabGroupingTests.swift @@ -141,4 +141,34 @@ struct WindowTabGroupingTests { #expect(result == "com.TablePro.main.\(connectionB.uuidString)") #expect(result != existingWindowIdentifier) } + + // MARK: - groupAllConnections + + @Test("groupAllConnections returns shared identifier regardless of connectionId") + func groupAllConnectionsReturnsSharedIdentifier() { + let connectionA = UUID() + let connectionB = UUID() + + let idA = TabbingIdentifierResolver.resolve( + pendingConnectionId: connectionA, existingIdentifier: nil, groupAllConnections: true + ) + let idB = TabbingIdentifierResolver.resolve( + pendingConnectionId: connectionB, existingIdentifier: nil, groupAllConnections: true + ) + + #expect(idA == "com.TablePro.main") + #expect(idB == "com.TablePro.main") + #expect(idA == idB) + } + + @Test("groupAllConnections ignores existingIdentifier") + func groupAllConnectionsIgnoresExistingIdentifier() { + let existing = "com.TablePro.main.SOME-UUID" + + let result = TabbingIdentifierResolver.resolve( + pendingConnectionId: nil, existingIdentifier: existing, groupAllConnections: true + ) + + #expect(result == "com.TablePro.main") + } } diff --git a/docs/customization/settings.mdx b/docs/customization/settings.mdx index 456d22849..e0f661c81 100644 --- a/docs/customization/settings.mdx +++ b/docs/customization/settings.mdx @@ -295,6 +295,14 @@ Single-clicking a table opens a preview tab that gets replaced on next click. Do Preview tabs show "Preview" in the subtitle and are not persisted across restarts. +### Tab Grouping + +| Setting | Default | Description | +|---------|---------|-------------| +| **Group all connections in one window** | Off | Tabs from different connections share the same window instead of opening separate windows | + +By default, each database connection opens its own window — tabs for the same connection are grouped together, while different connections get separate windows. Enable this setting to group all tabs into a single window regardless of which connection they belong to. + ## Keyboard Settings Customize keyboard shortcuts for menu actions. See [Keyboard Shortcuts](/features/keyboard-shortcuts#customizing-shortcuts) for full details. From 943545c3e49ddf5a2ddfe005d8f926a23760230e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Wed, 1 Apr 2026 14:24:09 +0700 Subject: [PATCH 2/2] fix: scope auto-reconnect failure cleanup to connection and document runtime toggle --- TablePro/AppDelegate+WindowConfig.swift | 13 ++++++++----- docs/customization/settings.mdx | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/TablePro/AppDelegate+WindowConfig.swift b/TablePro/AppDelegate+WindowConfig.swift index 36a6c1d92..c39d8210b 100644 --- a/TablePro/AppDelegate+WindowConfig.swift +++ b/TablePro/AppDelegate+WindowConfig.swift @@ -365,18 +365,21 @@ extension AppDelegate { window.close() } } catch is CancellationError { - for window in NSApp.windows where self.isMainWindow(window) { + for window in WindowLifecycleMonitor.shared.windows(for: connection.id) { window.close() } - self.openWelcomeWindow() + if !NSApp.windows.contains(where: { self.isMainWindow($0) && $0.isVisible }) { + self.openWelcomeWindow() + } } catch { windowLogger.error("Auto-reconnect failed for '\(connection.name)': \(error.localizedDescription)") - for window in NSApp.windows where self.isMainWindow(window) { + for window in WindowLifecycleMonitor.shared.windows(for: connection.id) { window.close() } - - self.openWelcomeWindow() + if !NSApp.windows.contains(where: { self.isMainWindow($0) && $0.isVisible }) { + self.openWelcomeWindow() + } } } } diff --git a/docs/customization/settings.mdx b/docs/customization/settings.mdx index e0f661c81..49cf53b00 100644 --- a/docs/customization/settings.mdx +++ b/docs/customization/settings.mdx @@ -303,6 +303,8 @@ Preview tabs show "Preview" in the subtitle and are not persisted across restart By default, each database connection opens its own window — tabs for the same connection are grouped together, while different connections get separate windows. Enable this setting to group all tabs into a single window regardless of which connection they belong to. +The setting takes effect for newly opened connections. Existing windows keep their current grouping until closed. + ## Keyboard Settings Customize keyboard shortcuts for menu actions. See [Keyboard Shortcuts](/features/keyboard-shortcuts#customizing-shortcuts) for full details.