Skip to content

refactor: replace AIProvider with ChatTransport content-block protocol#1057

Merged
datlechin merged 2 commits into
mainfrom
refactor/chat-transport-foundation
May 7, 2026
Merged

refactor: replace AIProvider with ChatTransport content-block protocol#1057
datlechin merged 2 commits into
mainfrom
refactor/chat-transport-foundation

Conversation

@datlechin

Copy link
Copy Markdown
Member

First PR against #1047 (AI Chat full rewrite). This is the foundation: replaces the flat text-only AIProvider with a content-block transport protocol that can carry text, tool-use, tool-result, and attachment blocks. No UI changes; no behavior change for end users.

What changed

New abstractions in TablePro/Core/AI/Chat/

  • ChatTurn — replaces AIChatMessage. Carries [ChatContentBlock] instead of a flat content: String. Includes per-turn modelId and providerId slots for the model picker UI in the next phase. Backwards-compatible decoder reads either the new blocks field or the legacy content field, so persisted conversations decode without a migration step.
  • ChatContentBlock — discriminated enum: .text, .toolUse(ToolUseBlock), .toolResult(ToolResultBlock), .attachment(ContextItem). Manual Codable with a kind discriminator (Swift's auto-synthesized Codable can't handle enums with named-case associated values).
  • ChatTransport — replaces AIProvider. streamChat(turns:options:) returns AsyncThrowingStream<ChatStreamEvent, Error>.
  • ChatStreamEvent — replaces AIStreamEvent. Cases: .textDelta, .toolUseStart, .toolUseDelta, .toolUseEnd, .usage. Tool events are wired into the type but no transport emits them yet (Phase 4 territory).
  • JSONValue — strict Swift discriminated-union for arbitrary JSON. Used by ToolUseBlock.input and ChatToolSpec.inputSchema. Round-trips losslessly across providers and through Codable storage. Tools decode to their typed Input: Codable via JSONValue.decoded(as:).
  • ContextItem — placeholder for @-mention attachments (Phase 3): .schema, .table, .currentQuery, .queryResult, .savedQuery, .file. Defined now so ChatContentBlock.attachment(ContextItem) is closed; UI does not emit any yet.

Migrated to ChatTransport

  • AnthropicProvider
  • OpenAICompatibleProvider (covers OpenAI, OpenRouter, Ollama, Custom)
  • GeminiProvider
  • CopilotChatProvider
  • AIChatInlineSource (consumer)

All providers map their request format from [ChatTurn] (text-only for now via turn.plainText) and yield .textDelta / .usage events. Tool blocks emitted by future versions are accepted by the type system and ignored by current providers.

Deleted

  • AIProvider protocol
  • AIChatMessage struct
  • AIChatRole enum
  • AIStreamEvent enum

AIProviderError stays (it's the user-facing error type used across providers and UI). collectErrorBody(from:) moved to a ChatTransport extension. AITokenUsage stays — it's the user-facing token count display type, no need to rename.

Touched

  • AIProviderFactoryResolvedProvider.provider is now ChatTransport.
  • AIProviderDescriptor.makeProvider — returns ChatTransport.
  • AIChatViewModelmessages: [ChatTurn]. .content += mutations replaced with appendText(_:) helper on ChatTurn. Stream switch handles new tool events with explicit break.
  • AIChatMessageView — reads message.plainText instead of message.content.
  • AIChatPanelView — same.
  • AIConversationmessages: [ChatTurn].

Why the design is what it is

JSONValue over [String: Any] shim or raw String JSON: tools need to round-trip arbitrary structured input across the wire and through Codable storage. [String: Any] requires an AnyCodable shim. Raw String JSON loses structural validation across the type system. JSONValue is a closed enum that mirrors what JSONSerialization produces, is Codable/Sendable/Equatable/Hashable natively, and exposes typed decode helpers for tool handlers (value.decoded(as: MyToolInput.self)).

Manual Codable for ChatContentBlock: Swift's auto-synthesized Codable flattens enum cases with associated values into a single key, which breaks round-trip across providers and across versions. Manual init(from:) / encode(to:) with a kind discriminator gives explicit, stable wire format that we can extend without breaking persisted conversations.

Backwards-compatible decoder on ChatTurn: persisted conversations from before this rewrite have content: String. The new init(from: Decoder) reads either blocks: [...] (new) or wraps content into [.text(content)] (legacy). Writes always use the new shape. No separate migration script needed.

What's deferred

This PR ships the protocol foundation. The remaining umbrella items become separate PRs:

  • Inline model picker + per-turn provider attributionChatTurn.modelId / .providerId are wired but no UI consumes them yet.
  • @-mention attachments + slash commandsContextItem exists but no picker UI emits one. ChatContentBlock.attachment(ContextItem) is reachable through Codable but never produced.
  • Tool calling via MCPChatToolSpec exists; ChatTransportOptions.tools: [ChatToolSpec] is wired through; no transport actually sends tools yet, no ChatTool protocol exists yet, no MCP bridging.
  • Apple Intelligence transport — issue AI Chat: Apple Intelligence transport via Foundation Models #1048, depends on this PR plus tool calling.

CHANGELOG

No entry. Internal architectural refactor with no user-visible behavior change. Persisted conversations decode under the new types via the backwards-compatible decoder.

Test plan

  • Build the app.
  • Open AI Chat with each configured provider; verify text streams as before.
  • Send a message that triggers token usage; verify usage badge appears.
  • Open a saved conversation from before this PR; verify it decodes and renders correctly.
  • Cancel an in-flight stream; verify partial assistant message is removed if empty.
  • Inline AI suggestion (Copilot/Claude/etc.) still streams.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant