Skip to content

Commit 5531c75

Browse files
authored
feat: add global toggle to disable all AI features (#496)
* feat: add global toggle to disable all AI features * fix: address review feedback for AI toggle
1 parent f331ab1 commit 5531c75

10 files changed

Lines changed: 129 additions & 45 deletions

File tree

CHANGELOG.md

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

1010
### Added
1111

12+
- Global toggle to disable all AI features (Settings > AI)
1213
- Drag to reorder columns in the Structure tab (MySQL/MariaDB)
1314
- Nested hierarchical groups for connection list (up to 3 levels deep)
1415
- Confirmation dialogs for deep link queries, connection imports, and pre-connect scripts

TablePro/Core/AI/InlineSuggestionManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ final class InlineSuggestionManager {
130130

131131
private func isEnabled() -> Bool {
132132
let settings = AppSettingsManager.shared.ai
133+
guard settings.enabled else { return false }
133134
guard settings.inlineSuggestEnabled else { return false }
134135
guard let controller else { return false }
135136
guard let textView = controller.textView else { return false }

TablePro/Core/AI/OllamaDetector.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ enum OllamaDetector {
1616
@MainActor
1717
static func detectAndRegister() async {
1818
let settings = AppSettingsManager.shared.ai
19+
guard settings.enabled else { return }
1920

2021
// Skip if an Ollama provider already exists
2122
if settings.providers.contains(where: { $0.type == .ollama }) {

TablePro/Models/AI/AIModels.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ enum AIConnectionPolicy: String, Codable, CaseIterable, Identifiable {
132132

133133
/// Global AI feature settings
134134
struct AISettings: Codable, Equatable {
135+
var enabled: Bool
135136
var providers: [AIProviderConfig]
136137
var featureRouting: [String: AIFeatureRoute]
137138
var includeSchema: Bool
@@ -142,6 +143,7 @@ struct AISettings: Codable, Equatable {
142143
var inlineSuggestEnabled: Bool
143144

144145
static let `default` = AISettings(
146+
enabled: true,
145147
providers: [],
146148
featureRouting: [:],
147149
includeSchema: true,
@@ -153,6 +155,7 @@ struct AISettings: Codable, Equatable {
153155
)
154156

155157
init(
158+
enabled: Bool = true,
156159
providers: [AIProviderConfig] = [],
157160
featureRouting: [String: AIFeatureRoute] = [:],
158161
includeSchema: Bool = true,
@@ -162,6 +165,7 @@ struct AISettings: Codable, Equatable {
162165
defaultConnectionPolicy: AIConnectionPolicy = .askEachTime,
163166
inlineSuggestEnabled: Bool = false
164167
) {
168+
self.enabled = enabled
165169
self.providers = providers
166170
self.featureRouting = featureRouting
167171
self.includeSchema = includeSchema
@@ -174,6 +178,7 @@ struct AISettings: Codable, Equatable {
174178

175179
init(from decoder: Decoder) throws {
176180
let container = try decoder.container(keyedBy: CodingKeys.self)
181+
enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
177182
providers = try container.decodeIfPresent([AIProviderConfig].self, forKey: .providers) ?? []
178183
featureRouting = try container.decodeIfPresent([String: AIFeatureRoute].self, forKey: .featureRouting) ?? [:]
179184
includeSchema = try container.decodeIfPresent(Bool.self, forKey: .includeSchema) ?? true

TablePro/Resources/Localizable.xcstrings

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11307,7 +11307,26 @@
1130711307
}
1130811308
},
1130911309
"Enable AI Features" : {
11310-
11310+
"localizations" : {
11311+
"tr" : {
11312+
"stringUnit" : {
11313+
"state" : "translated",
11314+
"value" : "AI Özelliklerini Etkinleştir"
11315+
}
11316+
},
11317+
"vi" : {
11318+
"stringUnit" : {
11319+
"state" : "translated",
11320+
"value" : "Bật tính năng AI"
11321+
}
11322+
},
11323+
"zh-Hans" : {
11324+
"stringUnit" : {
11325+
"state" : "translated",
11326+
"value" : "启用 AI 功能"
11327+
}
11328+
}
11329+
}
1131111330
},
1131211331
"Enable inline suggestions" : {
1131311332
"localizations" : {

TablePro/Views/Editor/AIEditorContextMenu.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ final class AIEditorContextMenu: NSMenu, NSMenuDelegate {
6060
menu.addItem(saveAsFavItem)
6161

6262
// AI items — only when text is selected
63-
guard hasSelection?() == true else { return }
63+
guard AppSettingsManager.shared.ai.enabled, hasSelection?() == true else { return }
6464

6565
menu.addItem(.separator())
6666

TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -379,18 +379,26 @@ extension MainContentCoordinator {
379379
errorMessage: error.localizedDescription
380380
)
381381

382-
// Show error alert with AI fix option
382+
// Show error alert (with AI fix option when AI is enabled)
383383
let errorMessage = error.localizedDescription
384384
let queryCopy = sql
385385
Task { @MainActor in
386-
let wantsAIFix = await AlertHelper.showQueryErrorWithAIOption(
387-
title: String(localized: "Query Execution Failed"),
388-
message: errorMessage,
389-
window: NSApp.keyWindow
390-
)
391-
if wantsAIFix {
392-
showAIChatPanel()
393-
aiViewModel?.handleFixError(query: queryCopy, error: errorMessage)
386+
if AppSettingsManager.shared.ai.enabled {
387+
let wantsAIFix = await AlertHelper.showQueryErrorWithAIOption(
388+
title: String(localized: "Query Execution Failed"),
389+
message: errorMessage,
390+
window: NSApp.keyWindow
391+
)
392+
if wantsAIFix {
393+
showAIChatPanel()
394+
aiViewModel?.handleFixError(query: queryCopy, error: errorMessage)
395+
}
396+
} else {
397+
AlertHelper.showErrorSheet(
398+
title: String(localized: "Query Execution Failed"),
399+
message: errorMessage,
400+
window: NSApp.keyWindow
401+
)
394402
}
395403
}
396404
}

TablePro/Views/RightSidebar/UnifiedRightPanelView.swift

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,51 @@ struct UnifiedRightPanelView: View {
1414
let connection: DatabaseConnection
1515
let tables: [TableInfo]
1616

17+
private var detailsView: some View {
18+
RightSidebarView(
19+
tableName: inspectorContext.tableName,
20+
tableMetadata: inspectorContext.tableMetadata,
21+
selectedRowData: inspectorContext.selectedRowData,
22+
isEditable: inspectorContext.isEditable,
23+
isRowDeleted: inspectorContext.isRowDeleted,
24+
onSave: { state.onSave?() },
25+
editState: state.editState
26+
)
27+
}
28+
1729
var body: some View {
1830
VStack(spacing: 0) {
19-
// Tab switcher
20-
Picker("", selection: $state.activeTab) {
21-
ForEach(RightPanelTab.allCases, id: \.self) { tab in
22-
Label(tab.localizedTitle, systemImage: tab.systemImage)
23-
.tag(tab)
31+
if AppSettingsManager.shared.ai.enabled {
32+
Picker("", selection: $state.activeTab) {
33+
ForEach(RightPanelTab.allCases, id: \.self) { tab in
34+
Label(tab.localizedTitle, systemImage: tab.systemImage)
35+
.tag(tab)
36+
}
2437
}
25-
}
26-
.pickerStyle(.segmented)
27-
.labelsHidden()
28-
.padding(.horizontal, 12)
29-
.padding(.vertical, 8)
38+
.pickerStyle(.segmented)
39+
.labelsHidden()
40+
.padding(.horizontal, 12)
41+
.padding(.vertical, 8)
3042

31-
switch state.activeTab {
32-
case .details:
33-
RightSidebarView(
34-
tableName: inspectorContext.tableName,
35-
tableMetadata: inspectorContext.tableMetadata,
36-
selectedRowData: inspectorContext.selectedRowData,
37-
isEditable: inspectorContext.isEditable,
38-
isRowDeleted: inspectorContext.isRowDeleted,
39-
onSave: { state.onSave?() },
40-
editState: state.editState
41-
)
42-
case .aiChat:
43-
AIChatPanelView(
44-
connection: connection,
45-
tables: tables,
46-
currentQuery: inspectorContext.currentQuery,
47-
queryResults: inspectorContext.queryResults,
48-
viewModel: state.aiViewModel
49-
)
43+
switch state.activeTab {
44+
case .details:
45+
detailsView
46+
case .aiChat:
47+
AIChatPanelView(
48+
connection: connection,
49+
tables: tables,
50+
currentQuery: inspectorContext.currentQuery,
51+
queryResults: inspectorContext.queryResults,
52+
viewModel: state.aiViewModel
53+
)
54+
}
55+
} else {
56+
detailsView
57+
}
58+
}
59+
.onChange(of: AppSettingsManager.shared.ai.enabled) {
60+
if !AppSettingsManager.shared.ai.enabled {
61+
state.activeTab = .details
5062
}
5163
}
5264
}

TablePro/Views/Settings/AISettingsView.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ struct AISettingsView: View {
1818

1919
var body: some View {
2020
Form {
21-
providersSection
22-
featureRoutingSection
23-
contextSection
24-
inlineSuggestionsSection
25-
privacySection
21+
Section {
22+
Toggle(String(localized: "Enable AI Features"), isOn: $settings.enabled)
23+
}
24+
if settings.enabled {
25+
providersSection
26+
featureRoutingSection
27+
contextSection
28+
inlineSuggestionsSection
29+
privacySection
30+
}
2631
}
2732
.formStyle(.grouped)
2833
.sheet(item: $editingProvider) { provider in
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// AISettingsTests.swift
3+
// TableProTests
4+
//
5+
6+
import Foundation
7+
@testable import TablePro
8+
import Testing
9+
10+
@Suite("AISettings")
11+
struct AISettingsTests {
12+
@Test("default has enabled true")
13+
func defaultEnabledIsTrue() {
14+
#expect(AISettings.default.enabled == true)
15+
}
16+
17+
@Test("decoding without enabled key defaults to true")
18+
func decodingWithoutEnabledDefaultsToTrue() throws {
19+
let json = "{}"
20+
let data = json.data(using: .utf8)!
21+
let settings = try JSONDecoder().decode(AISettings.self, from: data)
22+
#expect(settings.enabled == true)
23+
}
24+
25+
@Test("decoding with enabled false sets it correctly")
26+
func decodingWithEnabledFalse() throws {
27+
let json = "{\"enabled\": false}"
28+
let data = json.data(using: .utf8)!
29+
let settings = try JSONDecoder().decode(AISettings.self, from: data)
30+
#expect(settings.enabled == false)
31+
}
32+
}

0 commit comments

Comments
 (0)