Skip to content

Commit 8a91f31

Browse files
committed
Merge branch 'main' into feat/libsql-turso-driver
2 parents 9abece1 + fa72ea3 commit 8a91f31

53 files changed

Lines changed: 914 additions & 266 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- libSQL / Turso database support via downloadable plugin
13+
- JSON viewer with Text/Tree toggle for query results — tree view with expand/collapse, search, copy key path
1314
- MCP server: built-in Model Context Protocol server lets AI tools (Claude Desktop, Claude Code, Cursor) browse schemas, run queries, and export data through TablePro's connections
1415
- MCP server: connected clients list in Settings and status menu item showing server state
1516
- Import connections from TablePlus, Sequel Ace, and DBeaver with one-click migration

TablePro/AppDelegate+ConnectionHandler.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ extension AppDelegate {
8484
connectingURLConnectionIds.insert(connection.id)
8585
connectingURLParamKeys.insert(paramKey)
8686

87-
Task { @MainActor in
87+
Task {
8888
defer {
8989
self.connectingURLConnectionIds.remove(connection.id)
9090
self.connectingURLParamKeys.remove(paramKey)
@@ -136,7 +136,7 @@ extension AppDelegate {
136136
guard !connectingFilePaths.contains(filePath) else { return }
137137
connectingFilePaths.insert(filePath)
138138

139-
Task { @MainActor in
139+
Task {
140140
defer {
141141
self.connectingFilePaths.remove(filePath)
142142
}
@@ -186,7 +186,7 @@ extension AppDelegate {
186186
guard !connectingFilePaths.contains(filePath) else { return }
187187
connectingFilePaths.insert(filePath)
188188

189-
Task { @MainActor in
189+
Task {
190190
defer {
191191
self.connectingFilePaths.remove(filePath)
192192
}
@@ -236,7 +236,7 @@ extension AppDelegate {
236236
guard !connectingFilePaths.contains(filePath) else { return }
237237
connectingFilePaths.insert(filePath)
238238

239-
Task { @MainActor in
239+
Task {
240240
defer {
241241
self.connectingFilePaths.remove(filePath)
242242
}
@@ -311,7 +311,7 @@ extension AppDelegate {
311311
}
312312

313313
private func postSQLFilesWhenReady(urls: [URL]) {
314-
Task { @MainActor in
314+
Task {
315315
await waitForConnection(timeout: .seconds(3))
316316
NotificationCenter.default.post(name: .openSQLFiles, object: urls)
317317
}
@@ -331,7 +331,7 @@ extension AppDelegate {
331331
// MARK: - Post-Connect Actions
332332

333333
private func handlePostConnectionActions(_ parsed: ParsedConnectionURL, connectionId: UUID) {
334-
Task { @MainActor in
334+
Task {
335335
await waitForConnection(timeout: .seconds(5))
336336

337337
if let schema = parsed.schema {
@@ -409,7 +409,7 @@ extension AppDelegate {
409409
continuation.resume()
410410
}
411411

412-
let timeoutTask = Task { @MainActor in
412+
let timeoutTask = Task {
413413
try? await Task.sleep(for: timeout)
414414
resumeOnce()
415415
}
@@ -438,7 +438,7 @@ extension AppDelegate {
438438
continuation.resume()
439439
}
440440

441-
let timeoutTask = Task { @MainActor in
441+
let timeoutTask = Task {
442442
try? await Task.sleep(for: timeout)
443443
resumeOnce()
444444
}

TablePro/AppDelegate+FileOpen.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ extension AppDelegate {
4848
let initialPayload = EditorTabPayload(connectionId: connectionId)
4949
WindowManager.shared.openTab(payload: initialPayload)
5050

51-
Task { @MainActor in
51+
Task {
5252
do {
5353
try await DatabaseManager.shared.connectToSession(connection)
5454
for window in NSApp.windows where self.isWelcomeWindow(window) {
@@ -88,22 +88,22 @@ extension AppDelegate {
8888
func handleOpenURLs(_ urls: [URL]) {
8989
let deeplinks = urls.filter { $0.scheme == "tablepro" }
9090
if !deeplinks.isEmpty {
91-
Task { @MainActor in
91+
Task {
9292
for url in deeplinks { await self.handleDeeplink(url) }
9393
}
9494
}
9595

9696
let plugins = urls.filter { $0.pathExtension == "tableplugin" }
9797
if !plugins.isEmpty {
98-
Task { @MainActor in
98+
Task {
9999
for url in plugins { await self.handlePluginInstall(url) }
100100
}
101101
}
102102

103103
let databaseURLs = urls.filter { isDatabaseURL($0) }
104104
if !databaseURLs.isEmpty {
105105
suppressWelcomeWindow()
106-
Task { @MainActor in
106+
Task {
107107
for url in databaseURLs { self.handleDatabaseURL(url) }
108108
// endFileOpenSuppression is called here to match suppressWelcomeWindow above.
109109
// Individual handlers no longer manage this flag.
@@ -114,7 +114,7 @@ extension AppDelegate {
114114
let databaseFiles = urls.filter { isDatabaseFile($0) }
115115
if !databaseFiles.isEmpty {
116116
suppressWelcomeWindow()
117-
Task { @MainActor in
117+
Task {
118118
for url in databaseFiles {
119119
guard let dbType = self.databaseTypeForFile(url) else { continue }
120120
switch dbType {
@@ -245,7 +245,7 @@ extension AppDelegate {
245245
let deeplinkPayload = EditorTabPayload(connectionId: connection.id)
246246
WindowManager.shared.openTab(payload: deeplinkPayload)
247247

248-
Task { @MainActor in
248+
Task {
249249
do {
250250
// Confirm pre-connect script if present (deep links are external, so always confirm)
251251
if let script = connection.preConnectScript,

TablePro/AppDelegate+WindowConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ extension AppDelegate {
8686
let payload = EditorTabPayload(connectionId: connection.id, intent: .restoreOrDefault)
8787
WindowManager.shared.openTab(payload: payload)
8888

89-
Task { @MainActor in
89+
Task {
9090
do {
9191
try await DatabaseManager.shared.connectToSession(connection)
9292

TablePro/AppDelegate.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
9999
PluginManager.shared.loadPlugins()
100100
ConnectionStorage.shared.migratePluginSecureFieldsIfNeeded()
101101

102-
Task { @MainActor in
102+
Task {
103103
LicenseManager.shared.startPeriodicValidation()
104104
}
105105

@@ -176,7 +176,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
176176
guard let rejected = notification.object as? [(name: String, reason: String)],
177177
!rejected.isEmpty else { return }
178178
let details = rejected.map { "\($0.name): \($0.reason)" }.joined(separator: "\n")
179-
Task { @MainActor in
179+
Task {
180180
let alert = NSAlert()
181181
alert.messageText = String(
182182
format: String(localized: "%d plugin(s) could not be loaded"),

TablePro/Core/AI/InlineSuggestionManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ final class InlineSuggestionManager {
191191
generationID &+= 1
192192
let myGeneration = generationID
193193

194-
currentTask = Task { [weak self] in
194+
currentTask = Task { @MainActor [weak self] in
195195
guard let self else { return }
196196
self.suggestionOffset = cursorOffset
197197

TablePro/Core/SSH/Auth/PromptPassphraseProvider.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,7 @@ internal final class PromptPassphraseProvider: @unchecked Sendable {
2626
if Thread.isMainThread {
2727
return showAlert()
2828
}
29-
30-
let semaphore = DispatchSemaphore(value: 0)
31-
var result: PassphrasePromptResult?
32-
DispatchQueue.main.async {
33-
result = self.showAlert()
34-
semaphore.signal()
35-
}
36-
let waitResult = semaphore.wait(timeout: .now() + 120)
37-
guard waitResult == .success else { return nil }
38-
return result
29+
return DispatchQueue.main.sync { showAlert() }
3930
}
4031

4132
private func showAlert() -> PassphrasePromptResult? {

TablePro/Core/SSH/Auth/PromptTOTPProvider.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,7 @@ internal final class PromptTOTPProvider: TOTPProvider, @unchecked Sendable {
1515
if Thread.isMainThread {
1616
return try handleResult(showAlert())
1717
}
18-
19-
let semaphore = DispatchSemaphore(value: 0)
20-
var code: String?
21-
DispatchQueue.main.async {
22-
code = self.showAlert()
23-
semaphore.signal()
24-
}
25-
let result = semaphore.wait(timeout: .now() + 120)
26-
guard result == .success else {
27-
throw SSHTunnelError.connectionTimeout
28-
}
29-
return try handleResult(code)
18+
return try handleResult(DispatchQueue.main.sync { showAlert() })
3019
}
3120

3221
// Note: runModal() is intentional here. This method runs on the main thread

TablePro/Core/Services/Infrastructure/MainSplitViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
411411
},
412412
set: { [weak self] newValue in
413413
guard let sessionId = self?.payload?.connectionId ?? self?.currentSession?.id else { return }
414-
Task { @MainActor in
414+
Task {
415415
DatabaseManager.shared.updateSession(sessionId) { session in
416416
set(&session, newValue)
417417
}

TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {
8181

8282
// MARK: - Identifiers
8383

84-
private static let connection = NSToolbarItem.Identifier("com.TablePro.toolbar.connection")
85-
private static let database = NSToolbarItem.Identifier("com.TablePro.toolbar.database")
84+
private static let connectionGroup = NSToolbarItem.Identifier("com.TablePro.toolbar.connectionGroup")
8685
private static let refresh = NSToolbarItem.Identifier("com.TablePro.toolbar.refresh")
8786
private static let saveChanges = NSToolbarItem.Identifier("com.TablePro.toolbar.saveChanges")
8887
private static let principal = NSToolbarItem.Identifier("com.TablePro.toolbar.principal")
@@ -106,8 +105,7 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {
106105
[
107106
Self.sidebarToggle,
108107
.sidebarTrackingSeparator,
109-
Self.connection,
110-
Self.database,
108+
Self.connectionGroup,
111109
Self.refreshSaveGroup,
112110
.flexibleSpace,
113111
Self.principal,
@@ -150,12 +148,12 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {
150148
switch itemIdentifier {
151149
case Self.sidebarToggle:
152150
return makeSidebarToggleItem(coordinator: coordinator)
153-
case Self.connection:
151+
case Self.connectionGroup:
154152
return hostingItem(id: itemIdentifier, label: String(localized: "Connection"),
155-
content: ConnectionToolbarButton(coordinator: coordinator))
156-
case Self.database:
157-
return hostingItem(id: itemIdentifier, label: String(localized: "Database"),
158-
content: DatabaseToolbarButton(coordinator: coordinator))
153+
content: HStack(spacing: 4) {
154+
ConnectionToolbarButton(coordinator: coordinator)
155+
DatabaseToolbarButton(coordinator: coordinator)
156+
})
159157
case Self.refresh:
160158
return hostingItem(id: itemIdentifier, label: String(localized: "Refresh"),
161159
content: RefreshToolbarButton(coordinator: coordinator))
@@ -283,19 +281,18 @@ private struct DatabaseToolbarButton: View {
283281
var body: some View {
284282
let state = coordinator.toolbarState
285283
let supportsSwitch = PluginManager.shared.supportsDatabaseSwitching(for: state.databaseType)
286-
Button {
287-
coordinator.commandActions?.openDatabaseSwitcher()
288-
} label: {
289-
Label("Database", systemImage: "cylinder")
284+
if supportsSwitch {
285+
Button {
286+
coordinator.commandActions?.openDatabaseSwitcher()
287+
} label: {
288+
Label("Database", systemImage: "cylinder")
289+
}
290+
.help(String(localized: "Open Database (⌘K)"))
291+
.disabled(
292+
state.connectionState != .connected
293+
|| PluginManager.shared.connectionMode(for: state.databaseType) == .fileBased
294+
)
290295
}
291-
.help(String(localized: "Open Database (⌘K)"))
292-
.disabled(
293-
!supportsSwitch
294-
|| state.connectionState != .connected
295-
|| PluginManager.shared.connectionMode(for: state.databaseType) == .fileBased
296-
)
297-
.opacity(supportsSwitch ? 1 : 0)
298-
.allowsHitTesting(supportsSwitch)
299296
}
300297
}
301298

@@ -415,20 +412,20 @@ private struct ResultsToolbarButton: View {
415412

416413
var body: some View {
417414
let state = coordinator.toolbarState
418-
Button {
419-
coordinator.commandActions?.toggleResults()
420-
} label: {
421-
Label(
422-
"Results",
423-
systemImage: state.isResultsCollapsed
424-
? "rectangle.bottomhalf.inset.filled"
425-
: "rectangle.inset.filled"
426-
)
415+
if !state.isTableTab {
416+
Button {
417+
coordinator.commandActions?.toggleResults()
418+
} label: {
419+
Label(
420+
"Results",
421+
systemImage: state.isResultsCollapsed
422+
? "rectangle.bottomhalf.inset.filled"
423+
: "rectangle.inset.filled"
424+
)
425+
}
426+
.help(String(localized: "Toggle Results (⌘⌥R)"))
427+
.disabled(state.connectionState != .connected)
427428
}
428-
.help(String(localized: "Toggle Results (⌘⌥R)"))
429-
.disabled(state.connectionState != .connected)
430-
.opacity(state.isTableTab ? 0 : 1)
431-
.allowsHitTesting(!state.isTableTab)
432429
}
433430
}
434431

@@ -496,20 +493,18 @@ private struct ImportToolbarButton: View {
496493

497494
var body: some View {
498495
let state = coordinator.toolbarState
499-
let supportsImport = PluginManager.shared.supportsImport(for: state.databaseType)
500-
Button {
501-
coordinator.commandActions?.importTables()
502-
} label: {
503-
Label("Import", systemImage: "square.and.arrow.down")
496+
if PluginManager.shared.supportsImport(for: state.databaseType) {
497+
Button {
498+
coordinator.commandActions?.importTables()
499+
} label: {
500+
Label("Import", systemImage: "square.and.arrow.down")
501+
}
502+
.help(String(localized: "Import Data (⌘⇧I)"))
503+
.disabled(
504+
state.connectionState != .connected
505+
|| state.safeModeLevel.blocksAllWrites
506+
)
504507
}
505-
.help(String(localized: "Import Data (⌘⇧I)"))
506-
.disabled(
507-
state.connectionState != .connected
508-
|| state.safeModeLevel.blocksAllWrites
509-
|| !supportsImport
510-
)
511-
.opacity(supportsImport ? 1 : 0)
512-
.allowsHitTesting(supportsImport)
513508
}
514509
}
515510

0 commit comments

Comments
 (0)