Skip to content

Commit 1581e27

Browse files
committed
fix: remove stale split status items
1 parent 63e473f commit 1581e27

6 files changed

Lines changed: 216 additions & 227 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66
- Menu: keep the status menu open when manually refreshing usage from the menu (#845). Thanks @OlimjonovOtabek!
7+
- Menu bar: remove stale split provider status items instead of hiding them, avoiding leftover second-icon slots on macOS 26.4.
78
- Augment: report the real 1-minute keepalive check/min-refresh intervals in startup logs and docs (#434). Thanks @guglielmofonda!
89

910
## 0.24 — 2026-05-06
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import CodexBarCore
2+
3+
enum IconRemainingResolver {
4+
private static func codexProjection(snapshot: UsageSnapshot) -> CodexConsumerProjection {
5+
CodexConsumerProjection.make(
6+
surface: .menuBar,
7+
context: CodexConsumerProjection.Context(
8+
snapshot: snapshot,
9+
rawUsageError: nil,
10+
liveCredits: nil,
11+
rawCreditsError: nil,
12+
liveDashboard: nil,
13+
rawDashboardError: nil,
14+
dashboardAttachmentAuthorized: false,
15+
dashboardRequiresLogin: false,
16+
now: snapshot.updatedAt))
17+
}
18+
19+
private static func codexVisibleWindows(snapshot: UsageSnapshot) -> [RateWindow] {
20+
let projection = self.codexProjection(snapshot: snapshot)
21+
return projection.visibleRateLanes.compactMap { projection.rateWindow(for: $0) }
22+
}
23+
24+
static func resolvedWindows(
25+
snapshot: UsageSnapshot,
26+
style: IconStyle)
27+
-> (primary: RateWindow?, secondary: RateWindow?)
28+
{
29+
if style == .perplexity {
30+
let windows = snapshot.orderedPerplexityDisplayWindows()
31+
return (
32+
primary: windows.first,
33+
secondary: windows.dropFirst().first)
34+
}
35+
if style == .antigravity {
36+
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
37+
return (
38+
primary: windows.first,
39+
secondary: windows.dropFirst().first)
40+
}
41+
if style == .codex {
42+
let windows = self.codexVisibleWindows(snapshot: snapshot)
43+
return (
44+
primary: windows.first,
45+
secondary: windows.dropFirst().first)
46+
}
47+
return (
48+
primary: snapshot.primary,
49+
secondary: snapshot.secondary)
50+
}
51+
52+
static func resolvedRemaining(
53+
snapshot: UsageSnapshot,
54+
style: IconStyle)
55+
-> (primary: Double?, secondary: Double?)
56+
{
57+
if style == .perplexity {
58+
let windows = snapshot.orderedPerplexityDisplayWindows()
59+
return (
60+
primary: windows.first?.remainingPercent,
61+
secondary: windows.dropFirst().first?.remainingPercent)
62+
}
63+
if style == .antigravity {
64+
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
65+
return (
66+
primary: windows.first?.remainingPercent,
67+
secondary: windows.dropFirst().first?.remainingPercent)
68+
}
69+
if style == .codex {
70+
let windows = self.codexVisibleWindows(snapshot: snapshot)
71+
return (
72+
primary: windows.first?.remainingPercent,
73+
secondary: windows.dropFirst().first?.remainingPercent)
74+
}
75+
return (
76+
primary: snapshot.primary?.remainingPercent,
77+
secondary: snapshot.secondary?.remainingPercent)
78+
}
79+
80+
static func resolvedPercents(
81+
snapshot: UsageSnapshot,
82+
style: IconStyle,
83+
showUsed: Bool)
84+
-> (primary: Double?, secondary: Double?)
85+
{
86+
let windows = Self.resolvedWindows(snapshot: snapshot, style: style)
87+
return (
88+
primary: showUsed ? windows.primary?.usedPercent : windows.primary?.remainingPercent,
89+
secondary: showUsed ? windows.secondary?.usedPercent : windows.secondary?.remainingPercent)
90+
}
91+
}

Sources/CodexBar/IconView.swift

Lines changed: 0 additions & 213 deletions
This file was deleted.

Sources/CodexBar/StatusItemController.swift

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import AppKit
22
import CodexBarCore
33
import Observation
44
import QuartzCore
5-
import SwiftUI
65

76
// MARK: - Status item controller (AppKit-hosted icons, SwiftUI popovers)
87

@@ -531,8 +530,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
531530
let mergeIcons = self.shouldMergeIcons
532531
if mergeIcons {
533532
self.statusItem.isVisible = anyEnabled || force
534-
for item in self.statusItems.values {
535-
item.isVisible = false
533+
for provider in Array(self.statusItems.keys) {
534+
self.removeProviderStatusItem(for: provider)
536535
}
537536
self.attachMenus()
538537
} else {
@@ -544,8 +543,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
544543
if shouldBeVisible {
545544
let item = self.lazyStatusItem(for: provider)
546545
item.isVisible = true
547-
} else if let item = self.statusItems[provider] {
548-
item.isVisible = false
546+
} else {
547+
self.removeProviderStatusItem(for: provider)
549548
}
550549
}
551550
self.attachMenus(fallback: fallback)
@@ -610,10 +609,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
610609
}
611610
}
612611
} else if let item = self.statusItems[provider] {
613-
// Item exists but is no longer needed - clear its menu
614-
if item.menu != nil {
615-
item.menu = nil
616-
}
612+
item.menu = nil
617613
}
618614
}
619615
}
@@ -625,16 +621,31 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
625621
let ordered = self.settings.orderedProviders()
626622
let desired = Set(ordered)
627623
for provider in Array(self.statusItems.keys) where !desired.contains(provider) {
628-
if let item = self.statusItems.removeValue(forKey: provider) {
629-
self.statusBar.removeStatusItem(item)
630-
}
624+
self.removeProviderStatusItem(for: provider)
631625
}
632626

633-
for provider in ordered {
627+
guard !self.shouldMergeIcons else { return }
628+
let fallback = self.fallbackProvider
629+
let force = self.store.debugForceAnimation
630+
for provider in ordered where self.isEnabled(provider) || fallback == provider || force {
634631
_ = self.lazyStatusItem(for: provider)
635632
}
636633
}
637634

635+
private func removeProviderStatusItem(for provider: UsageProvider) {
636+
if let menu = self.providerMenus.removeValue(forKey: provider) {
637+
let menuID = ObjectIdentifier(menu)
638+
self.menuProviders.removeValue(forKey: menuID)
639+
self.menuVersions.removeValue(forKey: menuID)
640+
self.openMenus.removeValue(forKey: menuID)
641+
self.menuRefreshTasks.removeValue(forKey: menuID)?.cancel()
642+
}
643+
644+
guard let item = self.statusItems.removeValue(forKey: provider) else { return }
645+
item.menu = nil
646+
self.statusBar.removeStatusItem(item)
647+
}
648+
638649
func isVisible(_ provider: UsageProvider) -> Bool {
639650
self.store.debugForceAnimation || self.isEnabled(provider) || self.fallbackProvider == provider
640651
}

0 commit comments

Comments
 (0)