feat: add plugin settings system with options persistence#261
feat: add plugin settings system with options persistence#261
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughAdds per-plugin persistent settings storage, makes export plugin option models Codable, tracks plugin instances in PluginManager with namespaced disabledPlugins migration, removes four PluginCapability cases, and adds tests for storage and migration. Changes
Sequence DiagramsequenceDiagram
participant Plugin as Export Plugin
participant Storage as PluginSettingsStorage
participant Defaults as UserDefaults
Note over Plugin,Storage: Plugin Initialization
Plugin->>Storage: init(pluginId)
Storage->>Defaults: retrieve key com.TablePro.plugin.<id>.settings
Defaults-->>Storage: return JSON data
Storage-->>Plugin: decoded options (if present)
Note over Plugin,Defaults: User updates options
Plugin->>Plugin: options = newValue
Plugin->>Storage: save(options)
Storage->>Defaults: encode & persist JSON under namespaced key
Defaults-->>Storage: ✓ saved
Storage-->>Plugin: confirmation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
Plugins/SQLExportPlugin/SQLExportPlugin.swift (1)
38-42: Reuse the existingstorageproperty instead of creating a new instance.Line 39 creates a new
PluginSettingsStorageinstance rather than usingself.storage. While functionally correct (samepluginIdaccesses the same keys), reusing the existing instance is more consistent and avoids redundant allocations.♻️ Proposed fix
required init() { - if let saved = PluginSettingsStorage(pluginId: "sql").load(SQLExportOptions.self) { + if let saved = storage.load(SQLExportOptions.self) { options = saved } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/SQLExportPlugin/SQLExportPlugin.swift` around lines 38 - 42, The initializer is creating a new PluginSettingsStorage("sql") instead of reusing the existing storage property; update required init() to load SQLExportOptions from the existing storage property (storage.load(SQLExportOptions.self)) and assign it to options so you avoid the redundant PluginSettingsStorage allocation and remain consistent with the storage field.Plugins/CSVExportPlugin/CSVExportPlugin.swift (1)
29-33: Reuse the existingstorageproperty instead of creating a new instance.Same as in
SQLExportPlugin.swift: Line 30 creates a newPluginSettingsStorageinstance rather than usingself.storage. Reusing the existing instance is more consistent.♻️ Proposed fix
required init() { - if let saved = PluginSettingsStorage(pluginId: "csv").load(CSVExportOptions.self) { + if let saved = storage.load(CSVExportOptions.self) { options = saved } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/CSVExportPlugin/CSVExportPlugin.swift` around lines 29 - 33, The initializer creates a new PluginSettingsStorage instead of reusing the existing storage property; update required init() to call self.storage.load(CSVExportOptions.self) and assign the result to options (i.e., replace PluginSettingsStorage(pluginId: "csv").load(...) with self.storage.load(...)) so CSVExportOptions are loaded from the existing storage instance (keep using options and CSVExportOptions as before).Plugins/TableProPluginKit/PluginSettingsStorage.swift (1)
22-25: Consider logging encoding failures for debugging.The
savemethod silently discards encoding errors. While this prevents crashes, it could make debugging difficult when settings fail to persist. Consider logging the failure usingOSLogas per coding guidelines.💡 Optional: Add error logging
+import os + public final class PluginSettingsStorage { + private static let logger = Logger(subsystem: "com.TablePro", category: "PluginSettingsStorage") private let pluginId: String ... public func save<T: Encodable>(_ value: T, forKey optionKey: String = "settings") { - guard let data = try? encoder.encode(value) else { return } + do { + let data = try encoder.encode(value) + defaults.set(data, forKey: key(for: optionKey)) + } catch { + Self.logger.error("Failed to encode settings for plugin \(self.pluginId): \(error)") + } - defaults.set(data, forKey: key(for: optionKey)) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/TableProPluginKit/PluginSettingsStorage.swift` around lines 22 - 25, The save(_:forKey:) method currently swallows encoding failures; modify it to catch and log encoding errors using OSLog (or the project's logger) so failures are discoverable. Change the guard let try? encoder.encode(value) to a do/catch around encoder.encode(value) inside save(_:forKey:), and in the catch block call OSLog.default.error or the project's logger with a clear message including the optionKey and error description; keep using defaults.set(data, forKey: key(for: optionKey)) on success and return after logging on failure.Plugins/XLSXExportPlugin/XLSXExportPlugin.swift (1)
26-30: Use the existingstorageproperty instead of creating a new instance.Same issue as in the other export plugins: Line 27 creates a new
PluginSettingsStorageinstance rather than reusingself.storage.♻️ Proposed fix
required init() { - if let saved = PluginSettingsStorage(pluginId: "xlsx").load(XLSXExportOptions.self) { + if let saved = storage.load(XLSXExportOptions.self) { options = saved } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/XLSXExportPlugin/XLSXExportPlugin.swift` around lines 26 - 30, In required init() for XLSXExportPlugin, don't instantiate a new PluginSettingsStorage; use the existing storage property instead—replace the PluginSettingsStorage(pluginId: "xlsx").load(...) call with storage.load(XLSXExportOptions.self) and assign the returned value to options (inside required init), ensuring you reuse the plugin's storage instance rather than creating a new one.Plugins/MQLExportPlugin/MQLExportPlugin.swift (1)
33-37: Use the existingstorageproperty instead of creating a new instance.Line 34 creates a new
PluginSettingsStorageinstance rather than using the already-declaredself.storageproperty. Additionally, assigning tooptionstriggers thedidSetobserver, which saves the just-loaded value back to storage—a redundant write.♻️ Proposed fix
required init() { - if let saved = PluginSettingsStorage(pluginId: "mql").load(MQLExportOptions.self) { - options = saved + if let saved = storage.load(MQLExportOptions.self) { + _options = saved } }Note: If
@Observablemacro doesn't expose_optionsas the backing storage, an alternative is to use a flag or restructure to avoid the redundant save:+ private var isInitializing = true + required init() { - if let saved = PluginSettingsStorage(pluginId: "mql").load(MQLExportOptions.self) { + if let saved = storage.load(MQLExportOptions.self) { options = saved } + isInitializing = false } var options = MQLExportOptions() { - didSet { storage.save(options) } + didSet { + if !isInitializing { storage.save(options) } + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/MQLExportPlugin/MQLExportPlugin.swift` around lines 33 - 37, The initializer currently creates a new PluginSettingsStorage("mql") and assigns to options which triggers options.didSet and causes a redundant save; change init() to use the existing storage property (self.storage) to load MQLExportOptions and assign the loaded value to the options backing storage (e.g. _options) or use a temporary flag to bypass didSet so the loaded value is not immediately saved back; reference the required init(), storage property, options (and its didSet observer), PluginSettingsStorage, and MQLExportOptions when making this change.TablePro/Core/Plugins/PluginManager.swift (1)
50-56: Consider adding a log entry when migration occurs.The migration is silent. Adding a log entry when the legacy key is found and migrated would aid debugging and provide visibility into one-time migrations.
📝 Proposed enhancement
private func migrateDisabledPluginsKey() { let defaults = UserDefaults.standard if let legacy = defaults.stringArray(forKey: Self.legacyDisabledPluginsKey) { defaults.set(legacy, forKey: Self.disabledPluginsKey) defaults.removeObject(forKey: Self.legacyDisabledPluginsKey) + Self.logger.info("Migrated disabledPlugins key to namespaced key") } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TablePro/Core/Plugins/PluginManager.swift` around lines 50 - 56, The migration in migrateDisabledPluginsKey currently runs silently; update migrateDisabledPluginsKey to emit a log entry when a legacy value is found and migrated (reference Self.legacyDisabledPluginsKey and Self.disabledPluginsKey), e.g. use your app logger (os_log / Logger or your existing logging utility) to record that a migration occurred and include useful context such as the number of migrated entries or the actual list from UserDefaults.standard so you can trace one-time migrations.Plugins/JSONExportPlugin/JSONExportPlugin.swift (1)
26-30: Use the existingstorageproperty instead of creating a new instance.Same issue as in
MQLExportPlugin: Line 27 creates a newPluginSettingsStorageinstance rather than reusingself.storage, and the assignment triggers a redundant write viadidSet.♻️ Proposed fix
required init() { - if let saved = PluginSettingsStorage(pluginId: "json").load(JSONExportOptions.self) { + if let saved = storage.load(JSONExportOptions.self) { options = saved } }Consider the same pattern suggested for
MQLExportPluginto avoid the redundant save on initial load.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/JSONExportPlugin/JSONExportPlugin.swift` around lines 26 - 30, In required init() of JSONExportPlugin, stop creating a new PluginSettingsStorage and instead call the existing storage property to load JSONExportOptions (use storage.load(JSONExportOptions.self)); then assign the loaded value directly to the underlying backing storage for options (avoid triggering options' didSet) — e.g., set the private backing variable used by options or initialize options before didSet is active — so you reuse storage and avoid the redundant save caused by options' didSet.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@Plugins/CSVExportPlugin/CSVExportPlugin.swift`:
- Around line 29-33: The initializer creates a new PluginSettingsStorage instead
of reusing the existing storage property; update required init() to call
self.storage.load(CSVExportOptions.self) and assign the result to options (i.e.,
replace PluginSettingsStorage(pluginId: "csv").load(...) with
self.storage.load(...)) so CSVExportOptions are loaded from the existing storage
instance (keep using options and CSVExportOptions as before).
In `@Plugins/JSONExportPlugin/JSONExportPlugin.swift`:
- Around line 26-30: In required init() of JSONExportPlugin, stop creating a new
PluginSettingsStorage and instead call the existing storage property to load
JSONExportOptions (use storage.load(JSONExportOptions.self)); then assign the
loaded value directly to the underlying backing storage for options (avoid
triggering options' didSet) — e.g., set the private backing variable used by
options or initialize options before didSet is active — so you reuse storage and
avoid the redundant save caused by options' didSet.
In `@Plugins/MQLExportPlugin/MQLExportPlugin.swift`:
- Around line 33-37: The initializer currently creates a new
PluginSettingsStorage("mql") and assigns to options which triggers
options.didSet and causes a redundant save; change init() to use the existing
storage property (self.storage) to load MQLExportOptions and assign the loaded
value to the options backing storage (e.g. _options) or use a temporary flag to
bypass didSet so the loaded value is not immediately saved back; reference the
required init(), storage property, options (and its didSet observer),
PluginSettingsStorage, and MQLExportOptions when making this change.
In `@Plugins/SQLExportPlugin/SQLExportPlugin.swift`:
- Around line 38-42: The initializer is creating a new
PluginSettingsStorage("sql") instead of reusing the existing storage property;
update required init() to load SQLExportOptions from the existing storage
property (storage.load(SQLExportOptions.self)) and assign it to options so you
avoid the redundant PluginSettingsStorage allocation and remain consistent with
the storage field.
In `@Plugins/TableProPluginKit/PluginSettingsStorage.swift`:
- Around line 22-25: The save(_:forKey:) method currently swallows encoding
failures; modify it to catch and log encoding errors using OSLog (or the
project's logger) so failures are discoverable. Change the guard let try?
encoder.encode(value) to a do/catch around encoder.encode(value) inside
save(_:forKey:), and in the catch block call OSLog.default.error or the
project's logger with a clear message including the optionKey and error
description; keep using defaults.set(data, forKey: key(for: optionKey)) on
success and return after logging on failure.
In `@Plugins/XLSXExportPlugin/XLSXExportPlugin.swift`:
- Around line 26-30: In required init() for XLSXExportPlugin, don't instantiate
a new PluginSettingsStorage; use the existing storage property instead—replace
the PluginSettingsStorage(pluginId: "xlsx").load(...) call with
storage.load(XLSXExportOptions.self) and assign the returned value to options
(inside required init), ensuring you reuse the plugin's storage instance rather
than creating a new one.
In `@TablePro/Core/Plugins/PluginManager.swift`:
- Around line 50-56: The migration in migrateDisabledPluginsKey currently runs
silently; update migrateDisabledPluginsKey to emit a log entry when a legacy
value is found and migrated (reference Self.legacyDisabledPluginsKey and
Self.disabledPluginsKey), e.g. use your app logger (os_log / Logger or your
existing logging utility) to record that a migration occurred and include useful
context such as the number of migrated entries or the actual list from
UserDefaults.standard so you can trace one-time migrations.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1c38fd9e-8b94-4b9c-a2f9-554ae93eb8e7
📒 Files selected for processing (17)
CHANGELOG.mdPlugins/CSVExportPlugin/CSVExportModels.swiftPlugins/CSVExportPlugin/CSVExportPlugin.swiftPlugins/JSONExportPlugin/JSONExportModels.swiftPlugins/JSONExportPlugin/JSONExportPlugin.swiftPlugins/MQLExportPlugin/MQLExportModels.swiftPlugins/MQLExportPlugin/MQLExportPlugin.swiftPlugins/SQLExportPlugin/SQLExportModels.swiftPlugins/SQLExportPlugin/SQLExportPlugin.swiftPlugins/TableProPluginKit/PluginCapability.swiftPlugins/TableProPluginKit/PluginSettingsStorage.swiftPlugins/TableProPluginKit/TableProPlugin.swiftPlugins/XLSXExportPlugin/XLSXExportModels.swiftPlugins/XLSXExportPlugin/XLSXExportPlugin.swiftTablePro/Core/Plugins/PluginManager.swiftTablePro/Views/Settings/Plugins/BrowsePluginsView.swiftTablePro/Views/Settings/Plugins/InstalledPluginsView.swift
💤 Files with no reviewable changes (1)
- Plugins/TableProPluginKit/PluginCapability.swift
… crash Adding settingsView() as a protocol requirement was ABI-breaking for dynamically loaded plugin bundles, causing witness table mismatch and SIGILL at protocol descriptor for PluginDatabaseDriver.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
Plugins/JSONExportPlugin/JSONExportPlugin.swift (2)
22-24: Add explicit access control tooptionsproperty.The property lacks an explicit access modifier. As per coding guidelines, "Always specify access control explicitly (private, internal, public) on both extensions and individual members."
♻️ Proposed fix
- var options = JSONExportOptions() { + internal var options = JSONExportOptions() { didSet { storage.save(options) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/JSONExportPlugin/JSONExportPlugin.swift` around lines 22 - 24, The options property (var options = JSONExportOptions() { didSet { storage.save(options) } }) lacks an explicit access control modifier; update its declaration to include the correct access level (for example private, private(set), internal, or public as appropriate for the plugin API) while preserving the didSet that calls storage.save(options) so behavior is unchanged. Ensure the chosen modifier matches other members' visibility and coding guidelines for JSONExportPlugin and JSONExportOptions.
26-30: Reuse the existingstorageproperty instead of creating a duplicate instance.Line 27 creates a new
PluginSettingsStorageinstance instead of using thestorageproperty defined on line 20. This is redundant and inconsistent with howstorageis used in thedidSetobserver.♻️ Proposed fix
required init() { - if let saved = PluginSettingsStorage(pluginId: "json").load(JSONExportOptions.self) { + if let saved = storage.load(JSONExportOptions.self) { options = saved } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/JSONExportPlugin/JSONExportPlugin.swift` around lines 26 - 30, The initializer creates a new PluginSettingsStorage instead of reusing the existing storage property; update required init() to call storage.load(JSONExportOptions.self) and assign to options (keeping the same behavior as the didSet observer) so you don't instantiate a duplicate PluginSettingsStorage and remain consistent with the existing storage property and options handling.Plugins/XLSXExportPlugin/XLSXExportPlugin.swift (2)
20-30: Use a single source of truth for the storage key.Line 20 and Line 27 both hardcode
"xlsx"even thoughXLSXExportPlugin.formatIdalready exists. That makes the persistence key easy to desync on a future rename and can orphan saved settings. Reuse the existingstorageinstance ininit()as well.♻️ Proposed cleanup
- private let storage = PluginSettingsStorage(pluginId: "xlsx") + private let storage = PluginSettingsStorage(pluginId: XLSXExportPlugin.formatId) @@ required init() { - if let saved = PluginSettingsStorage(pluginId: "xlsx").load(XLSXExportOptions.self) { + if let saved = storage.load(XLSXExportOptions.self) { options = saved } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/XLSXExportPlugin/XLSXExportPlugin.swift` around lines 20 - 30, The plugin uses the literal "xlsx" twice for settings storage; replace those literals by reusing the existing storage instance and the canonical identifier XLSXExportPlugin.formatId: in the initializer (required init()) call storage.load(XLSXExportOptions.self) instead of creating a new PluginSettingsStorage(pluginId: "xlsx"), and ensure the storage property was initialized using PluginSettingsStorage(pluginId: XLSXExportPlugin.formatId) so the single source of truth (storage and XLSXExportPlugin.formatId) is used for saving and loading options.
22-30: Add explicit access control to the new members.
optionsandrequired init()currently rely on defaultinternalvisibility. Please spell that out explicitly to match the Swift repo rules.🔧 Suggested change
- var options = XLSXExportOptions() { + internal var options = XLSXExportOptions() { didSet { storage.save(options) } } - required init() { + internal required init() { if let saved = PluginSettingsStorage(pluginId: "xlsx").load(XLSXExportOptions.self) { options = saved } }As per coding guidelines, "Always specify access control explicitly (private, internal, public) on both extensions and individual members".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/XLSXExportPlugin/XLSXExportPlugin.swift` around lines 22 - 30, The members are using implicit internal visibility; explicitly mark them as internal: change the property declaration for options to "internal var options = XLSXExportOptions() { didSet { storage.save(options) } }" and mark the initializer as "internal required init()" so the access control for both the options property and the required initializer is spelled out (referencing options and required init in XLSXExportPlugin).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Plugins/SQLExportPlugin/SQLExportPlugin.swift`:
- Around line 28-42: The plugin is persisting settings under the hard-coded
"sql" namespace and creating a new PluginSettingsStorage in init(), which can
cause mismatched keys; update the storage instantiation to use the canonical
plugin identifier (the same static id used by your plugin entry, e.g.,
PluginEntry.id) and reuse the existing storage property in required init() when
loading saved SQLExportOptions so you don't create a second
PluginSettingsStorage; adjust references to PluginSettingsStorage(pluginId:
"sql") to use PluginEntry.id and replace the new storage creation in init() with
the existing storage variable when calling load(SQLExportOptions.self).
In `@TableProTests/Core/Plugins/PluginSettingsTests.swift`:
- Around line 136-140: The test onlyThreeCases currently checks the length of a
manual array instead of the enum itself; update it to use
PluginCapability.allCases (or Array(PluginCapability.allCases)) and assert that
its count == 3, and optionally assert that the set of PluginCapability.allCases
equals the expected cases (.databaseDriver, .exportFormat, .importFormat) to
ensure no extra cases exist. Locate the onlyThreeCases test and replace the
manual allCases array with PluginCapability.allCases and adjust the assertion
accordingly.
- Around line 168-238: The tests reimplement the migration instead of exercising
the real migration; remove the duplicated key-move code and invoke the
production migration entry point (call migrateDisabledPluginsKey() or the
PluginManager method that performs this migration) from both migrationMovesKey
and migrationNoOpWhenAbsent, keeping the same setup/teardown of UserDefaults and
assertions; if migrateDisabledPluginsKey is not publicly accessible, expose a
test-only internal entry (or extract a shared helper used by both production and
tests) and call that instead so the tests fail if the real migration is removed.
---
Nitpick comments:
In `@Plugins/JSONExportPlugin/JSONExportPlugin.swift`:
- Around line 22-24: The options property (var options = JSONExportOptions() {
didSet { storage.save(options) } }) lacks an explicit access control modifier;
update its declaration to include the correct access level (for example private,
private(set), internal, or public as appropriate for the plugin API) while
preserving the didSet that calls storage.save(options) so behavior is unchanged.
Ensure the chosen modifier matches other members' visibility and coding
guidelines for JSONExportPlugin and JSONExportOptions.
- Around line 26-30: The initializer creates a new PluginSettingsStorage instead
of reusing the existing storage property; update required init() to call
storage.load(JSONExportOptions.self) and assign to options (keeping the same
behavior as the didSet observer) so you don't instantiate a duplicate
PluginSettingsStorage and remain consistent with the existing storage property
and options handling.
In `@Plugins/XLSXExportPlugin/XLSXExportPlugin.swift`:
- Around line 20-30: The plugin uses the literal "xlsx" twice for settings
storage; replace those literals by reusing the existing storage instance and the
canonical identifier XLSXExportPlugin.formatId: in the initializer (required
init()) call storage.load(XLSXExportOptions.self) instead of creating a new
PluginSettingsStorage(pluginId: "xlsx"), and ensure the storage property was
initialized using PluginSettingsStorage(pluginId: XLSXExportPlugin.formatId) so
the single source of truth (storage and XLSXExportPlugin.formatId) is used for
saving and loading options.
- Around line 22-30: The members are using implicit internal visibility;
explicitly mark them as internal: change the property declaration for options to
"internal var options = XLSXExportOptions() { didSet { storage.save(options) }
}" and mark the initializer as "internal required init()" so the access control
for both the options property and the required initializer is spelled out
(referencing options and required init in XLSXExportPlugin).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cfbea017-d2ec-463d-8f06-a0f4f7830fd5
📒 Files selected for processing (7)
Plugins/CSVExportPlugin/CSVExportPlugin.swiftPlugins/JSONExportPlugin/JSONExportPlugin.swiftPlugins/MQLExportPlugin/MQLExportPlugin.swiftPlugins/SQLExportPlugin/SQLExportPlugin.swiftPlugins/XLSXExportPlugin/XLSXExportPlugin.swiftTablePro/Views/Settings/Plugins/InstalledPluginsView.swiftTableProTests/Core/Plugins/PluginSettingsTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
- Plugins/MQLExportPlugin/MQLExportPlugin.swift
- Plugins/CSVExportPlugin/CSVExportPlugin.swift
… against data loss
Summary
PluginSettingsStoragein UserDefaultssettingsView()rendered inline in Settings > Plugins detail sectiondisabledPluginskey tocom.TablePro.disabledPluginswith automatic one-time migrationPluginCapabilitycases (sqlDialect,aiProvider,cellRenderer,sidebarPanel)PluginInstallTracker.markInstalling()when download completesPluginSettingsStorage.removeAll()Changes
SDK (
TableProPluginKit)PluginSettingsStorageclass for plugins to persistCodableoptionssettingsView() -> AnyView?added toTableProPluginprotocol (defaultnil)PluginCapabilityreduced from 7 to 3 casesExport plugins (CSV, JSON, XLSX, SQL, MQL)
CodableconformancedidSetand loads oninit()settingsView()to return itsoptionsView()PluginManagerpluginInstancesdictionary tracks live plugin instances by bundle IDdisabledPluginskey namespaced with migrationUI
InstalledPluginsViewrenders plugin settings inline in detail sectionBrowsePluginsViewcallsmarkInstalling()at download completionTest plan
com.TablePro.disabledPlugins, no baredisabledPluginskeyPluginCapabilityhas exactly 3 cases, no compiler warningsSummary by CodeRabbit
New Features
Improvements