Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e3249d8
feat(tabs): add EditorTabStripView and tab manager mutation helpers
datlechin May 14, 2026
955f8df
feat(tabs): add EditorTabContainerViewController for in-window tabs
datlechin May 14, 2026
24e42e1
feat(infra): add ConnectionWindowRestoration and WindowManager open stub
datlechin May 14, 2026
cf56a8a
feat(tabs): add ConnectionSplitContainerView for detail-pane assembly
datlechin May 14, 2026
7147cde
refactor(tabs)!: replace per-tab NSWindow with per-connection window …
datlechin May 14, 2026
e7de002
feat(tabs): migrate window-layout defaults to per-connection keys
datlechin May 14, 2026
0fa0131
refactor(tabs): consolidate window-title resolution into ConnectionWi…
datlechin May 14, 2026
c27acea
fix(tabs): close window via NSWindow.close to avoid performClose recu…
datlechin May 14, 2026
c2b0de4
fix(tabs): commit cell edit and dismiss FK popover on tab switch
datlechin May 14, 2026
9a6aa4f
style(tabs): remove explanatory comments, extract commitOutgoingTabGr…
datlechin May 14, 2026
7cf551d
test(tabs): cover tab manager mutations, tab intents, and layout migr…
datlechin May 14, 2026
29d4ca0
docs(tabs): update tabs, shortcuts, CLAUDE.md, CHANGELOG for in-windo…
datlechin May 14, 2026
22f3043
fix(tabs): close background tab without pre-selecting it; drop dead r…
datlechin May 14, 2026
2f80c32
fix(tabs): use per-connection window frame autosave name
datlechin May 14, 2026
e246d47
refactor(tabs): drop dead window-lookup helpers and explanatory comments
datlechin May 14, 2026
8760c7d
test(tabs): update editor-load tests for in-window tab model
datlechin May 14, 2026
8b1ed2a
style(tabs): drop remaining doc comments per no-comments rule
datlechin May 14, 2026
cbb1229
Merge branch 'main' into feat/1220-in-window-tabs
datlechin May 15, 2026
32158ea
fix(tabs): keep window alive when session not yet in activeSessions
datlechin May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Editor tabs now live inside one window per connection, with an in-window tab bar. The sidebar and inspector are shared across all tabs of a connection instead of duplicated per window. (#1220)
- Add competitive tracking docs sourced from top TablePlus issues.

### Fixed
Expand Down
11 changes: 4 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ When adding a new method to the driver protocol: add to `PluginDatabaseDriver` (
- **`SQLEditorTheme`** — single source of truth for editor colors/fonts
- **`TableProEditorTheme`** — adapter to CodeEdit's `EditorTheme` protocol
- **`CompletionEngine`** — framework-agnostic; **`SQLCompletionAdapter`** bridges to CodeEdit's `CodeSuggestionDelegate`
- Editor tabs use native NSWindow tabs (`NSWindow.tabbingMode = .preferred` in `TabWindowController`); there is no custom tab bar.
- Editor tabs are in-window tabs: one `ConnectionWindowController` window per connection, with `EditorTabStripView` (the in-window tab bar) driving `QueryTabManager.selectedTabId`. `MainContentView` renders the selected tab. `ConnectionWindow.tabbingMode = .automatic`. There are no per-tab NSWindows.
- Cursor model: `cursorPositions: [CursorPosition]` (multi-cursor via CodeEditSourceEditor)

### Change Tracking Flow
Expand All @@ -135,12 +135,9 @@ These have caused real bugs when violated:

**WelcomeViewModel tree rebuild**: The welcome screen renders `treeItems` (grouped/filtered), not `connections` directly. Every mutation to `connections` must call `rebuildTree()` afterward, or the UI won't update.

**Tab replacement guard**: `openTableTab` checks for active work (unsaved edits, applied filters, sorting) before replacing the current tab. Tabs with active work open a new native window tab instead. This check runs before the preview tab branch.
**Tab replacement guard**: `openTableTab` checks for active work (unsaved edits, applied filters, sorting) before replacing the current tab. Tabs with active work open a new in-window tab instead. This check runs before the preview tab branch.

**Window tab titles**: Resolved in TWO places that must stay in sync:
1. `ContentView.init` (title resolution chain) — initial title from payload
2. `MainContentView+Setup.swift` `updateWindowTitleAndFileState()` — ongoing title updates
Missing a case produces a wrong "{Language} Query" title on the first frame.
**Window title resolution**: `ConnectionWindowController.refreshWindowTitle()` is the single source of truth for the window title, proxy icon, and dirty dot. It is called from `windowDidBecomeKey`, the controller's `init`, and the selected-tab-change hook in `MainContentView`. Do not resolve the title anywhere else.

**Schema loading**: `SQLSchemaProvider` (actor) stores an in-flight `loadTask: Task<Void, Never>?`. Concurrent callers `await` the same Task instead of firing duplicate `fetchTables()` queries. Never use a boolean `isLoading` guard that returns without data — callers need to await the result.

Expand All @@ -150,7 +147,7 @@ Missing a case produces a wrong "{Language} Query" title on the first frame.

### Window Close (Cmd+W)

`EditorWindow` (NSWindow subclass in `TabWindowController.swift`) overrides `performClose:` to route Cmd+W through `closeTab()`. SwiftUI's `.commands { Button(...).keyboardShortcut("w") }` does NOT replace AppKit's built-in "File > Close"both fire, and AppKit's wins. The NSWindow subclass is the correct native pattern.
`ConnectionWindow` (NSWindow subclass in `ConnectionWindowController.swift`) overrides `performClose:` to route Cmd+W through `commandActions.closeTab()`, which closes the selected in-window tab (or the window when it is the last tab). SwiftUI's `.commands { Button(...).keyboardShortcut("w") }` does NOT replace AppKit's built-in "File > Close": both fire, and AppKit's wins. The NSWindow subclass is the correct native pattern. `closeCurrentTab` closes the window with `NSWindow.close()`, not `performClose(_:)`, to avoid re-entering the override.

### Storage Patterns

Expand Down
2 changes: 1 addition & 1 deletion TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
objects = {

/* Begin PBXBuildFile section */
5AD1D8C12FB5000000000001 /* TableProMSSQLCore in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD1D8C12FB5000000000002 /* TableProMSSQLCore */; };
5A32BBFB2F9D5EAB00BAEB5F /* X509 in Frameworks */ = {isa = PBXBuildFile; productRef = 5A32BBFA2F9D5EAB00BAEB5F /* X509 */; };
5A32BC0B2F9D659100BAEB5F /* tablepro-mcp in Copy Files */ = {isa = PBXBuildFile; fileRef = 5A32BC002F9D5F1300BAEB5F /* tablepro-mcp */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5A3A69B82F976F38000AC5B2 /* GhosttyTerminal in Frameworks */ = {isa = PBXBuildFile; productRef = 5A3A69B72F976F38000AC5B2 /* GhosttyTerminal */; };
Expand Down Expand Up @@ -55,6 +54,7 @@
5ACE00012F4F000000000005 /* CodeEditLanguages in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000003 /* CodeEditLanguages */; };
5ACE00012F4F000000000006 /* CodeEditTextView in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000007 /* CodeEditTextView */; };
5ACE00012F4F00000000000A /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000009 /* Sparkle */; };
5AD1D8C12FB5000000000001 /* TableProMSSQLCore in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD1D8C12FB5000000000002 /* TableProMSSQLCore */; };
5ADDB00100000000000000A1 /* DynamoDBConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADDB00200000000000000A1 /* DynamoDBConnection.swift */; };
5ADDB00100000000000000A2 /* DynamoDBItemFlattener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADDB00200000000000000A2 /* DynamoDBItemFlattener.swift */; };
5ADDB00100000000000000A3 /* DynamoDBPartiQLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADDB00200000000000000A3 /* DynamoDBPartiQLParser.swift */; };
Expand Down
5 changes: 3 additions & 2 deletions TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
.first(where: { $0.connectionId == connectionId })?.commandActions {
actions.newTab()
} else {
WindowManager.shared.openTab(
payload: EditorTabPayload(connectionId: connectionId, intent: .newEmptyTab)
WindowManager.shared.openConnectionWindow(
for: connectionId,
intent: EditorTabPayload(connectionId: connectionId, intent: .newEmptyTab)
)
}
}
Expand Down
13 changes: 4 additions & 9 deletions TablePro/Core/MCP/Protocol/Tools/OpenConnectionWindowTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,16 @@ public struct OpenConnectionWindowTool: MCPToolImplementation {

Self.logger.debug("open_connection_window invoked for connection \(connectionId.uuidString, privacy: .public)")

let windowId = await MainActor.run { () -> UUID in
let payload = EditorTabPayload(
connectionId: connectionId,
tabType: .query,
intent: .restoreOrDefault
)
WindowManager.shared.openTab(payload: payload)
let windowId = await MainActor.run { () -> UUID? in
let id = WindowManager.shared.openConnectionWindow(for: connectionId)
NSApp.activate(ignoringOtherApps: true)
return payload.id
return id
}

let result: JsonValue = .object([
"status": .string("opened"),
"connection_id": .string(connectionId.uuidString),
"window_id": .string(windowId.uuidString)
"window_id": .string(windowId?.uuidString ?? "")
])
return .structured(result)
}
Expand Down
8 changes: 4 additions & 4 deletions TablePro/Core/MCP/Protocol/Tools/OpenTableTabTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public struct OpenTableTabTool: MCPToolImplementation {

Self.logger.debug("open_table_tab invoked for connection \(connectionId.uuidString, privacy: .public)")

let windowId = await MainActor.run { () -> UUID in
let windowId = await MainActor.run { () -> UUID? in
let payload = EditorTabPayload(
connectionId: connectionId,
tabType: .table,
Expand All @@ -65,16 +65,16 @@ public struct OpenTableTabTool: MCPToolImplementation {
schemaName: schemaName,
intent: .openContent
)
WindowManager.shared.openTab(payload: payload)
let id = WindowManager.shared.openConnectionWindow(for: connectionId, intent: payload)
NSApp.activate(ignoringOtherApps: true)
return payload.id
return id
}

let result: JsonValue = .object([
"status": .string("opened"),
"connection_id": .string(connectionId.uuidString),
"table_name": .string(tableName),
"window_id": .string(windowId.uuidString)
"window_id": .string(windowId?.uuidString ?? "")
])
return .structured(result)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal final class AppLaunchCoordinator {

internal func didFinishLaunching() {
hasFinishedLaunching = true
WindowLayoutMigration.runIfNeeded()
let deadline = Date().addingTimeInterval(0.150)
phase = .collectingIntents(deadline: deadline)
deadlineTask = Task { [weak self] in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// (toolbar items + main content) is its own SwiftUI scene context, and
// focus-scene-value propagation breaks once a toolbar Button takes scene
// focus. The registry is updated on `windowDidBecomeKey` from
// `TabWindowController`, then read by `AppMenuCommands` as a fallback when
// `ConnectionWindowController`, then read by `AppMenuCommands` as a fallback when
// `@FocusedValue` returns nil — so menu shortcuts (Cmd+T, Cmd+1...9, etc.)
// stay live regardless of which sub-NSHostingController holds focus.
//
Expand Down
Loading
Loading