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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- AI Chat: panel layout redesign. The right inspector now has a Details / AI Chat segmented picker at the top. The chat tab is composer-focused: empty state is a small icon and one-line title, and all chat actions live in a single-row composer footer (mention, slash commands, mode picker, model picker, history, new conversation, send). The mode picker (Ask / Edit / Agent) is saved to settings but does not yet change provider behavior.
- AI Chat: inline model picker in the composer with per-turn model attribution. Switch between configured providers and any of their available models without leaving the chat. The model that produced each assistant turn is shown in the message footer.
- AI Chat: slash commands `/explain`, `/optimize`, `/fix`, and `/help`. Type the command in the composer or pick from the slash menu next to the model picker. `/explain`, `/optimize`, and `/fix` operate on the current query in the active editor. `/help` lists the commands inline.
- AI Chat: attach context to a message via the `@` menu next to the slash menu, or by typing `@` directly in the composer.
Expand Down
19 changes: 4 additions & 15 deletions TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// items to — NSToolbar must be constructed directly on NSWindow.
//
// Each item's content is still authored in SwiftUI (`NSHostingView(rootView:)`)
// so existing subviews (ConnectionStatusView, SafeModeBadgeView, popovers,
// so existing subviews (ConnectionStatusView, SafeModeBadgeView, popovers,x
// etc.) are reused verbatim.
//

Expand All @@ -35,7 +35,7 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {
/// Retain the hosting controllers — without this, NSHostingController
/// deallocs immediately and its view becomes orphaned, producing zero-size
/// items that get pushed right by flexibleSpace.
private var hostingControllers: [NSToolbarItem.Identifier: NSHostingController<AnyView>] = [:]
internal var hostingControllers: [NSToolbarItem.Identifier: NSHostingController<AnyView>] = [:]
private var sidebarButtons: [NSButton] = []
private var sidebarObservationTask: Task<Void, Never>?
private var splitViewObserver: NSObjectProtocol?
Expand Down Expand Up @@ -218,7 +218,7 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {

// MARK: - Helpers

private func hostingItem<Content: View>(
internal func hostingItem<Content: View>(
id: NSToolbarItem.Identifier,
label: String,
content: Content
Expand Down Expand Up @@ -353,18 +353,7 @@ private struct NewTabToolbarButton: View {
var body: some View {
let state = coordinator.toolbarState
Button {
coordinator.commandActions?.newTab()
// Defensive: a new window will become key. Restore its first
// responder so AppKit's responder chain — which SwiftUI uses to
// resolve `@FocusedValue` — points back at MainContentView.
// Belt-and-suspenders for the `.focusable(false)` fix in
// `hostingItem`; covers any path where SwiftUI might still
// briefly retain scene focus on the toolbar's hosting controller.
DispatchQueue.main.async {
if let key = NSApp.keyWindow {
key.makeFirstResponder(key.contentView)
}
}
NSApp.sendAction(#selector(NSWindow.newWindowForTab(_:)), to: nil, from: nil)
} label: {
Label("New Tab", systemImage: "plus.rectangle")
}
Expand Down
35 changes: 33 additions & 2 deletions TablePro/Models/AI/AIModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,32 @@ enum AIConnectionPolicy: String, Codable, CaseIterable, Identifiable, Sendable {
}
}

// MARK: - AI Chat Mode

enum AIChatMode: String, Codable, CaseIterable, Identifiable, Sendable {
case ask
case edit
case agent

var id: String { rawValue }

var displayName: String {
switch self {
case .ask: return String(localized: "Ask")
case .edit: return String(localized: "Edit")
case .agent: return String(localized: "Agent")
}
}

var symbolName: String {
switch self {
case .ask: return "questionmark.bubble"
case .edit: return "pencil.and.outline"
case .agent: return "infinity"
}
}
}

// MARK: - AI Settings

struct AISettings: Codable, Equatable, Sendable {
Expand All @@ -143,6 +169,7 @@ struct AISettings: Codable, Equatable, Sendable {
var includeQueryResults: Bool
var maxSchemaTables: Int
var defaultConnectionPolicy: AIConnectionPolicy
var chatMode: AIChatMode

static let `default` = AISettings(
enabled: true,
Expand All @@ -153,7 +180,8 @@ struct AISettings: Codable, Equatable, Sendable {
includeCurrentQuery: false,
includeQueryResults: false,
maxSchemaTables: 20,
defaultConnectionPolicy: .askEachTime
defaultConnectionPolicy: .askEachTime,
chatMode: .ask
)

init(
Expand All @@ -165,7 +193,8 @@ struct AISettings: Codable, Equatable, Sendable {
includeCurrentQuery: Bool = false,
includeQueryResults: Bool = false,
maxSchemaTables: Int = 20,
defaultConnectionPolicy: AIConnectionPolicy = .askEachTime
defaultConnectionPolicy: AIConnectionPolicy = .askEachTime,
chatMode: AIChatMode = .ask
) {
self.enabled = enabled
self.providers = providers
Expand All @@ -176,6 +205,7 @@ struct AISettings: Codable, Equatable, Sendable {
self.includeQueryResults = includeQueryResults
self.maxSchemaTables = maxSchemaTables
self.defaultConnectionPolicy = defaultConnectionPolicy
self.chatMode = chatMode
}

init(from decoder: Decoder) throws {
Expand All @@ -191,6 +221,7 @@ struct AISettings: Codable, Equatable, Sendable {
defaultConnectionPolicy = try container.decodeIfPresent(
AIConnectionPolicy.self, forKey: .defaultConnectionPolicy
) ?? .askEachTime
chatMode = try container.decodeIfPresent(AIChatMode.self, forKey: .chatMode) ?? .ask
}

var activeProvider: AIProviderConfig? {
Expand Down
Loading
Loading