fix: restore menu bar icon on macOS 26.4 (#805)#849
Conversation
Two changes addressing the invisible status item icon on macOS 26.4: 1. Replace compositingGroup + blendMode(.destinationOut) in UsageProgressBar with a single Canvas that draws track, fill, and pace-tip punch-out entirely inside Core Graphics. SwiftUI compositing modifiers (.compositingGroup, .blendMode as a view modifier) trigger Metal/RenderBox shader compilation; on macOS 26.4 that compilation can fail with a precondition error, making the NSStatusItem window invisible. Moving the blend to the GraphicsContext API inside the Canvas avoids the Metal path entirely. 2. Gate migrateLegacySecrets and migrateLegacyAccounts behind existing == nil in CodexBarConfigMigrator.loadOrMigrate(). These ran unconditionally on every launch, performing ~28 SecItemCopyMatching calls on the main thread. After the first launch the config is the source of truth and legacy stores are cleared, so subsequent calls all hit errSecItemNotFound — but macOS 26.4 Performance Diagnostics still emits a fault per call, adding startup jank. applyLegacyCookieSources (cheap UserDefaults reads) remains unconditional. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the `existing == nil` gate added in the previous commit with a UserDefaults boolean flag (`codexbar.legacySecretsMigrationCompleted`). The `existing == nil` gate had a blind spot: if the app crashed after configStore.save() but before clearLegacyStores(), the config would exist on the next launch (`existing != nil`) so migration would be skipped and orphaned Keychain items would never be cleaned up. The flag approach is safe for all cases: - Fresh install: flag absent → migration runs → items found → clear → set flag - Normal re-launch: flag present → skip (0 Keychain calls) - Crash-interrupted first migration: flag absent → migration re-runs → items still present → clear → set flag (data already in config via setIfEmpty) - No legacy data ever existed: flag absent → migration runs → finds nothing → set flag → never scans again Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 86e286d8b3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| self.clearLegacyStores(stores: stores, sawAccounts: state.sawLegacyAccounts, log: log) | ||
| userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey) |
There was a problem hiding this comment.
Only mark migration complete after legacy cleanup succeeds
loadOrMigrate now sets codexbar.legacySecretsMigrationCompleted immediately after calling clearLegacyStores, but clearLegacyStores catches and logs failures instead of reporting success/failure. In environments where keychain writes fail transiently (for example, a storeToken(nil) call throws), cleanup can be partial or skipped, yet the completion flag is still persisted; subsequent launches then skip migration/cleanup entirely. This creates a one-shot failure mode where legacy secrets/accounts can remain indefinitely because retries are permanently disabled.
Useful? React with 👍 / 👎.
clearLegacyStores catches keychain write failures internally and returns normally, so the completion flag was being set even on partial cleanup. A transient keychain error would permanently suppress retry on next launch. - Make clearLegacyStores return Bool (true = all deletes succeeded) - Gate legacyMigrationCompletedKey on that return value - Remove comment block above the key constant (SwiftFormat docComments lint) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1160719b05
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else if !migrationCompleted { | ||
| userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey) |
There was a problem hiding this comment.
Avoid finalizing migration after transient keychain read errors
Setting legacySecretsMigrationCompleted in this else branch permanently disables future migration attempts even when the current run failed to read legacy data. migrateLegacySecrets/migrateLegacyAccounts use try?, so transient keychain/file errors (for example, locked keychain returning errSecInteractionNotAllowed) are treated the same as “no legacy data,” leaving sawLegacy* false and triggering this path. In that case, users can lose one-time migration of existing credentials/accounts because subsequent launches skip migration entirely.
Useful? React with 👍 / 👎.
Summary
Fixes the invisible menu bar icon on macOS 26.4 reported in #805. Two changes, one PR since they address the same symptom.
Change 1 —
UsageProgressBar.swiftPR #677 removed
.drawingGroup()and.regularMaterial, but two more RenderBox-triggering SwiftUI compositing modifiers remained:.compositingGroup()(two branches inbody).blendMode(.destinationOut)applied as a view modifier on an innerCanvasOn macOS 26.4, these trigger Metal/RenderBox shader compilation. When that compilation fails with
[com.apple.renderbox:error] precondition failure, the NSStatusItem window becomes invisible (isVisible = 0, windowWidth = 16). The menu is built at startup (before the user opens it), so the failure fires on every launch.Fix: rewrite the entire view as a single
Canvas. The punch-out effect (pace-tip stripe) is now done withcontext.blendMode = .destinationOutset on theGraphicsContextproperty inside the canvas closure — this stays in Core Graphics and never touches Metal/RenderBox. Visual output is identical.Change 2 —
CodexBarConfigMigrator.swiftmigrateLegacySecrets()andmigrateLegacyAccounts()ran unconditionally on every launch fromSettingsStore.init()on the main thread, making ~28SecItemCopyMatchingcalls that all returnerrSecItemNotFoundafter the first launch. macOS 26.4 Performance Diagnostics emits afaultper call.Fix: guard the heavy migration behind a
UserDefaultscompletion flag (codexbar.legacySecretsMigrationCompleted), set afterclearLegacyStores()completes. Using a flag (rather thanexisting == nil) means a crash betweenconfigStore.save()andclearLegacyStores()can still finish cleanup on the next launch.applyLegacyCookieSources()(cheap UserDefaults reads) stays unconditional.