Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- AI Chat: Copilot Agent mode now persists across follow-up turns. The chat-mode flag was only set on `conversationCreate`, so the first message in a conversation ran with tools but the second and later messages reverted to plain chat (the model would say "I don't have access to query execution tools"). `CopilotConversationTurnParams` now carries `chatMode`, `customChatModeId`, and `needToolCallConfirmation`, and `streamChat` sends them on every turn
- iOS: data browser no longer flashes the "No Data" empty state mid-reload when the search field is cleared, filters change, sort changes, or page size changes. `DataBrowserViewModel.load()` now sets `isPageLoading` when rows already exist (so the in-list overlay shows during the reload), and the view renders a progress spinner instead of the empty state while a reload is in flight (reported via TestFlight, iPhone 17 Pro / iOS 26.4.2)
- AI Chat: streaming responses no longer hang the process at 100% CPU, and Agent-mode assistant turns that emit text before a tool call now show that text above the tool card (it previously appeared below). The previous design rebuilt the assistant turn's text block on every 150 ms flush, recomputed `plainText` from blocks on every observer tick, and re-fed the result to `Markdown(text)` keyed by `\.offset`, making MarkdownUI reparse the full message every flush. The fix is structural: an assistant turn is now a single ordered list of `ChatContentBlock` reference instances (`@Observable` classes with stable UUIDs), tokens flow directly into the trailing streaming block, and tool blocks are appended after a `finishStreamingTextBlock()` call so text emitted before any tool call lands first. Per-property Observation scopes re-renders to the one streaming text view; siblings are not invalidated. Markdown rendering moved off the third-party `swift-markdown-ui` dependency to a native renderer built on Foundation's `AttributedString(markdown:)` with SwiftUI block layout (paragraphs, headers, lists, blockquotes, fenced code, GFM tables). Schema prefetch and turn resolution run on `Task.detached` so the main thread stays free between flushes. Providers and persistence now operate on a separate `ChatTurnWire` value type, so live message instances never cross actor boundaries (#1205)
- MySQL/MariaDB: `BINARY(N)` and `VARBINARY(N)` columns are no longer classified as text. The plugin's type switch returned `"CHAR"` for type code 254 and `"VARCHAR"` for type code 253 without consulting the binary flag, so binary columns came through as `.text` instead of `.blob`. The data grid then allowed inline edit on those cells, the editor opened with an empty field (because `PluginCellValue.bytes(_).asText` returns `nil`), and focus-out marked the cell as modified-to-empty. A single accidental click was enough to schedule destruction of the original bytes on Save. Case 253 now returns `"VARBINARY"` and case 254 returns `"BINARY"` when the binary flag is set, so these cells route through the blob editor (chevron-triggered) instead of inline text edit
- Data grid: dropdown / boolean / date / JSON / blob cells now show or hide their chevron accessory when the cell's editability changes (refresh, safe-mode toggle, view-to-table switch). The performance pass in #1212 made `configure` skip `needsDisplay = true` when nothing visible changed, but the editability flag was updated unconditionally without flagging a redraw, so chevrons could stick in their stale state (visible in `audit_log.payload` JSON cells after save / refresh / reopen)
- MySQL/MariaDB: JSON column detection no longer flickers across refreshes. The plugin reads `mariadb_field_attr(MARIADB_FIELD_ATTR_FORMAT_NAME)` to recognize JSON-stored-as-LONGTEXT. The returned `MARIADB_CONST_STRING` is a length-prefixed buffer (not null-terminated), but we were reading it with `String(cString:)`, which scans bytes until the next `\0` and so reads past the buffer into adjacent memory. When that memory happened to contain a null byte the comparison passed and we tagged the column `JSON`; when it contained garbage the comparison failed and we fell through to `LONGTEXT`. Read exactly `attr.length` bytes via `String(data: Data(bytes:count:), encoding: .utf8)`
Expand Down
17 changes: 0 additions & 17 deletions TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
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 */; };
5ACE00012F4F00000000000D /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F00000000000C /* MarkdownUI */; };
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 Expand Up @@ -674,7 +673,6 @@
5A32BBFB2F9D5EAB00BAEB5F /* X509 in Frameworks */,
5ACE00012F4F000000000006 /* CodeEditTextView in Frameworks */,
5ACE00012F4F00000000000A /* Sparkle in Frameworks */,
5ACE00012F4F00000000000D /* MarkdownUI in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1049,7 +1047,6 @@
5ACE00012F4F000000000003 /* CodeEditLanguages */,
5ACE00012F4F000000000007 /* CodeEditTextView */,
5ACE00012F4F000000000009 /* Sparkle */,
5ACE00012F4F00000000000C /* MarkdownUI */,
5A3A69B72F976F38000AC5B2 /* GhosttyTerminal */,
5A3A69B92F976F38000AC5B2 /* GhosttyTheme */,
5ACE00012F4F000000000010 /* TableProAnalytics */,
Expand Down Expand Up @@ -1640,7 +1637,6 @@
packageReferences = (
5A0000012F4F000000000101 /* XCLocalSwiftPackageReference "LocalPackages/CodeEditSourceEditor" */,
5ACE00012F4F000000000008 /* XCRemoteSwiftPackageReference "Sparkle" */,
5ACE00012F4F00000000000B /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
5A0000012F4F000000000100 /* XCLocalSwiftPackageReference "LocalPackages/CodeEditLanguages" */,
5ACE00012F4F00000000000E /* XCRemoteSwiftPackageReference "oracle-nio" */,
5A3A69B62F976F38000AC5B2 /* XCRemoteSwiftPackageReference "libghostty-spm" */,
Expand Down Expand Up @@ -4054,14 +4050,6 @@
minimumVersion = 2.0.0;
};
};
5ACE00012F4F00000000000B /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.0.0;
};
};
5ACE00012F4F00000000000E /* XCRemoteSwiftPackageReference "oracle-nio" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/TableProApp/oracle-nio";
Expand Down Expand Up @@ -4105,11 +4093,6 @@
package = 5ACE00012F4F000000000008 /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
5ACE00012F4F00000000000C /* MarkdownUI */ = {
isa = XCSwiftPackageProductDependency;
package = 5ACE00012F4F00000000000B /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
productName = MarkdownUI;
};
5ACE00012F4F00000000000F /* OracleNIO */ = {
isa = XCSwiftPackageProductDependency;
package = 5ACE00012F4F00000000000E /* XCRemoteSwiftPackageReference "oracle-nio" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions TablePro/Core/AI/AnthropicProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class AnthropicProvider: ChatTransport {
}

func streamChat(
turns: [ChatTurn],
turns: [ChatTurnWire],
options: ChatTransportOptions
) -> AsyncThrowingStream<ChatStreamEvent, Error> {
AsyncThrowingStream { continuation in
Expand Down Expand Up @@ -111,7 +111,7 @@ final class AnthropicProvider: ChatTransport {

func testConnection() async throws -> Bool {
let testModel = model.isEmpty ? (Self.knownModels.first ?? "") : model
let testTurn = ChatTurn(role: .user, blocks: [.text("Hi")])
let testTurn = ChatTurnWire(role: .user, blocks: [.text("Hi")])
let testOptions = ChatTransportOptions(model: testModel, maxOutputTokens: 1)
let request = try buildMessagesRequest(turns: [testTurn], options: testOptions, stream: false)

Expand All @@ -136,7 +136,7 @@ final class AnthropicProvider: ChatTransport {
}

private func buildMessagesRequest(
turns: [ChatTurn],
turns: [ChatTurnWire],
options: ChatTransportOptions,
stream: Bool = true
) throws -> URLRequest {
Expand Down Expand Up @@ -256,10 +256,10 @@ final class AnthropicProvider: ChatTransport {
]
}

static func encodeTurn(_ turn: ChatTurn) throws -> [String: Any]? {
static func encodeTurn(_ turn: ChatTurnWire) throws -> [String: Any]? {
let blocks = turn.blocks
let needsTypedBlocks = blocks.contains { block in
switch block {
switch block.kind {
case .toolUse, .toolResult:
return true
case .text, .attachment:
Expand All @@ -278,8 +278,8 @@ final class AnthropicProvider: ChatTransport {
return ["role": turn.role.rawValue, "content": text]
}

static func encodeBlock(_ block: ChatContentBlock) throws -> [String: Any]? {
switch block {
static func encodeBlock(_ block: ChatContentBlockWire) throws -> [String: Any]? {
switch block.kind {
case .text(let text):
guard !text.isEmpty else { return nil }
return ["type": "text", "text": text]
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/AI/Chat/ChatTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation

protocol ChatTransport: AnyObject, Sendable {
func streamChat(
turns: [ChatTurn],
turns: [ChatTurnWire],
options: ChatTransportOptions
) -> AsyncThrowingStream<ChatStreamEvent, Error>

Expand Down
Loading
Loading