feat(settings): power-source profiles, guided permissions, and searchable settings index#641
Conversation
Build on power-based switching (#630) so each power state selects a full profile (engine + model), not just a local model file: - Apple Intelligence can be chosen per power state (for example, on battery), so users can save power on battery and use a larger local model while charging. The option is offered and applied only when Apple Intelligence is available. - PowerSourceMonitor now detects live charger plug/unplug via an IOKit IOPSNotificationCreateRunLoopSource, not just at launch and on wake. - The switcher applies the active profile on power change, on enable, on profile edits, and once the model list loads; switching the engine is near-free and reloads the local model only when it actually changes. - Settings gains an engine-level "Power" section with one profile picker per state. Persists batteryEngine/pluggedInEngine alongside the existing filenames; both default to Open Source so the model-only setup from #630 keeps working. Refs #619.
… index Bundles the per-power-source engine and model profiles work (PR #637) with the settings redesign so they ship together: a searchable settings index with broad keyword coverage, in-app guided permission prompts replacing cold System Settings hand-offs, directly editable word-count steppers, and refreshed menu-bar branding assets. Renames the Settings pane's permission row to SettingsPermissionRow to avoid colliding with the menu bar's PermissionRow.
| case .llama(let filename): | ||
| suggestionSettings.selectEngine(.llamaOpenSource) | ||
|
|
||
| guard !filename.isEmpty, | ||
| runtimeModel.availableModels.contains(where: { $0.filename == filename }), | ||
| runtimeModel.selectedModelFilename != filename else { | ||
| return | ||
| } | ||
| .store(in: &cancellables) | ||
|
|
||
| Task { | ||
| await runtimeModel.selectModel(filename) | ||
| } |
There was a problem hiding this comment.
Engine switch happens before model validation
In the .llama case, selectEngine(.llamaOpenSource) is called unconditionally, before the guards that verify the filename is non-empty and the model is actually installed. If the profile's model has been deleted after being configured, availableModels.contains fails and the function returns early — but the engine has already been switched to llamaOpenSource with no model loaded, silently breaking suggestions. Moving the selectEngine call inside the guarded block (after all validation passes) would prevent this stranded state.
| deinit { | ||
| if let observer { | ||
| NSWorkspace.shared.notificationCenter.removeObserver(observer) | ||
| if let wakeObserver { | ||
| NSWorkspace.shared.notificationCenter.removeObserver(wakeObserver) | ||
| } | ||
|
|
||
| if let runLoopSource { | ||
| CFRunLoopRemoveSource(CFRunLoopGetMain(), runLoopSource, .defaultMode) | ||
| } | ||
| } |
There was a problem hiding this comment.
@MainActor deinit not marked nonisolated — macOS 26 back-deployment concern
SuggestionSettingsModel includes a nonisolated deinit {} specifically to avoid a StopLookupScope bug in the back-deployment main-actor executor shim on macOS 26. PowerSourceMonitor is also a @MainActor class with a non-trivial deinit (NSWorkspace observer removal + CFRunLoopRemoveSource), and is equally susceptible to that bug. If the shim misfires and the deinit never executes, the run loop source stays live; because Unmanaged.passUnretained(self) was used, any subsequent IOKit callback would access freed memory. Since PowerSourceMonitor lives for the app's lifetime in practice, the observable risk is low, but the same defensive pattern applied in SuggestionSettingsModel should be considered here too.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Bundles the per-power-source engine + model profiles feature (originally PR #637) with the settings redesign so they ship together. Adds a searchable settings index, routes permission prompts through the in-app guided flow instead of cold System Settings hand-offs, makes the custom word-count limits directly editable, and refreshes the menu-bar branding assets.
Validation
Linked issues
Refs #637 (this PR supersedes it; the power-source work is included here, so #637 is being closed).
Risk / rollout notes
SuggestionSettingsStore. Defaults keep current behavior (feature off).IOPSNotificationCreateRunLoopSource) so charger plug/unplug switches profiles live, not only at launch/wake.PermissionGuidanceController.requestAccess(...)(guided overlay / native TCC prompt) instead of opening System Settings directly.PermissionRowtoSettingsPermissionRowto avoid colliding with the menu bar'sPermissionRow.apple_intelligenceandllamaimagesets; updatedCotabbyLogo.Greptile Summary
This PR bundles per-power-source engine+model profiles with a settings redesign: it adds a
PowerProfileenum, IOKit live plug/unplug detection, guided permission prompts (replacing cold System Settings hand-offs), editable word-count TextFields, and a greatly expanded searchable settings index.batteryEngine/pluggedInEnginepersisted fields, aPowerProfileenum carrying engine + model in a single picker tag, and aMergeManyCombine pipeline that applies the right profile whenever power state, the toggle, any profile field, or the installed-model list changes. Upgrade path preserves legacy model-only behavior by defaulting both engine fields to.llamaOpenSource.PowerSourceMonitorgains anIOPSNotificationCreateRunLoopSourcerun-loop source so charger plug/unplug is detected during an active session rather than only at launch/wake; the wake observer is retained as a safety net.PermissionsPaneViewpermission rows now callPermissionGuidanceController.requestAccess(for:sourceFrameInScreen:), and theSettingsIndexgains 9 new searchable items with expanded synonym lists.Confidence Score: 3/5
Safe to merge with one fix: after a model is deleted, the plugged-in or battery profile can switch the engine to the local Open Source engine with no model loaded, silently disabling suggestions for that power state.
The engine selection in
applyPowerProfileruns before the filename and model-availability guards, so a.llama(filename)profile whose model has been deleted will switch the active engine away from its current state without loading any model. ThePowerSourceMonitorIOKit run-loop integration is otherwise clean, and the settings persistence, upgrade path, and permission-guidance plumbing all look correct.Cotabby/App/Core/CotabbyAppEnvironment.swift — the
applyPowerProfilestatic method's.llamabranch needs the engine switch moved inside the validation guards.Important Files Changed
selectEngine(.llamaOpenSource)fires before filename/availability guards inapplyPowerProfile.Unmanaged.passUnretainedsafely in practice, butdeinitlacks thenonisolatedguard found in sibling@MainActorclasses.batteryEngine/pluggedInEnginepublished properties and profile convenience setters; logic is sound and backwards-compatible with legacy model-only persisted state..llamaOpenSource) that preserve pre-upgrade behavior; round-trip tested and covered by new store tests.powerProfilePickerfor both power states; display fallback for empty filename is correct.openSettingscallbacks withPermissionGuidanceController.requestAccessand renames the private type to avoid collisions.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["MergeMany triggers\n(isPluggedIn, toggle, engines,\nfilenames, availableModels)"] --> B["observePowerSourceProfileSwitching\nsink"] B --> C{isPowerBased\nSwitchingEnabled?} C -- No --> D[return — no-op] C -- Yes --> E["resolve profile\n(batteryProfile or pluggedInProfile)"] E --> F{profile} F -- ".appleIntelligence" --> G{isAvailable?} G -- No --> D G -- Yes --> H["selectEngine(.appleIntelligence)"] F -- ".llama(filename)" --> I["selectEngine(.llamaOpenSource)\n⚠️ unconditional"] I --> J{filename valid\n& model installed?} J -- No --> D J -- Yes --> K["Task: runtimeModel.selectModel(filename)"]Reviews (1): Last reviewed commit: "feat(settings): power-source profiles, g..." | Re-trigger Greptile