Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Localizations: add Spanish and Catalan language packs and fill missing localization keys (#1041). Thanks @seifreed!

### Fixed
- Menu: restore full-width provider switcher quota bars and refresh them while the menu stays open (#1094). Thanks @bcharleson!
- Codex: accept the first click in the account switcher inside menu popovers (#1079). Thanks @ptstory!
- Codex/Claude: terminate PTY child process trees during probe cleanup so wrapper-launched CLI descendants do not linger after sessions finish (#1085). Thanks @mickobizzle!
- MiniMax: exclude explicitly failed billing-history records from token charts and model/method totals (#1089). Thanks @Yuxin-Qiao!
Expand Down
49 changes: 44 additions & 5 deletions Sources/CodexBar/ProviderSwitcherButtons.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import AppKit

final class PaddedToggleButton: NSButton {
private var quotaBarReservedHeight: CGFloat = 0

var contentPadding = NSEdgeInsets(top: 4, left: 7, bottom: 4, right: 7) {
didSet {
if oldValue.top != self.contentPadding.top ||
Expand All @@ -17,24 +19,39 @@ final class PaddedToggleButton: NSButton {
let size = super.intrinsicContentSize
return NSSize(
width: size.width + self.contentPadding.left + self.contentPadding.right,
height: size.height + self.contentPadding.top + self.contentPadding.bottom)
height: size.height + self.contentPadding.top + self.contentPadding.bottom + self.quotaBarReservedHeight)
}

func setQuotaBarReservedHeight(_ height: CGFloat) {
guard self.quotaBarReservedHeight != height else { return }
self.quotaBarReservedHeight = height
self.invalidateIntrinsicContentSize()
}
}

@MainActor
protocol ProviderSwitcherToggleButton: AnyObject {
func setQuotaBarReservedHeight(_ height: CGFloat)
}

extension PaddedToggleButton: ProviderSwitcherToggleButton {}

final class InlineIconToggleButton: NSButton {
private let iconView = NSImageView()
private let titleField = NSTextField(labelWithString: "")
private let stack = NSStackView()
private var paddingConstraints: [NSLayoutConstraint] = []
private var iconSizeConstraints: [NSLayoutConstraint] = []
private var quotaBarReservedHeight: CGFloat = 0
private var isConfiguring = false // Batch invalidation during setup

var contentPadding = NSEdgeInsets(top: 4, left: 7, bottom: 4, right: 7) {
didSet {
self.paddingConstraints.first { $0.firstAttribute == .top }?.constant = self.contentPadding.top
self.paddingConstraints.first { $0.firstAttribute == .leading }?.constant = self.contentPadding.left
self.paddingConstraints.first { $0.firstAttribute == .trailing }?.constant = -self.contentPadding.right
self.paddingConstraints.first { $0.firstAttribute == .bottom }?.constant = -self.contentPadding.bottom
self.paddingConstraints.first { $0.firstAttribute == .bottom }?.constant =
-(self.contentPadding.bottom + self.quotaBarReservedHeight)
if !self.isConfiguring { self.invalidateIntrinsicContentSize() }
}
}
Expand Down Expand Up @@ -71,6 +88,14 @@ final class InlineIconToggleButton: NSButton {
self.titleField.font = NSFont.systemFont(ofSize: size)
}

func setQuotaBarReservedHeight(_ height: CGFloat) {
guard self.quotaBarReservedHeight != height else { return }
self.quotaBarReservedHeight = height
self.paddingConstraints.first { $0.firstAttribute == .bottom }?.constant =
-(self.contentPadding.bottom + height)
if !self.isConfiguring { self.invalidateIntrinsicContentSize() }
}

func setAllowsTwoLineTitle(_ allow: Bool) {
let hasWhitespace = self.titleField.stringValue.rangeOfCharacter(from: .whitespacesAndNewlines) != nil
let shouldWrap = allow && hasWhitespace
Expand All @@ -83,7 +108,7 @@ final class InlineIconToggleButton: NSButton {
let size = self.stack.fittingSize
return NSSize(
width: size.width + self.contentPadding.left + self.contentPadding.right,
height: size.height + self.contentPadding.top + self.contentPadding.bottom)
height: size.height + self.contentPadding.top + self.contentPadding.bottom + self.quotaBarReservedHeight)
}

init(title: String, image: NSImage, target: AnyObject?, action: Selector?) {
Expand Down Expand Up @@ -152,20 +177,24 @@ final class InlineIconToggleButton: NSButton {
}
}

extension InlineIconToggleButton: ProviderSwitcherToggleButton {}

final class StackedToggleButton: NSButton {
private let iconView = NSImageView()
private let titleField = NSTextField(labelWithString: "")
private let stack = NSStackView()
private var paddingConstraints: [NSLayoutConstraint] = []
private var iconSizeConstraints: [NSLayoutConstraint] = []
private var quotaBarReservedHeight: CGFloat = 0
private var isConfiguring = false // Batch invalidation during setup

var contentPadding = NSEdgeInsets(top: 2, left: 4, bottom: 2, right: 4) {
didSet {
self.paddingConstraints.first { $0.firstAttribute == .top }?.constant = self.contentPadding.top
self.paddingConstraints.first { $0.firstAttribute == .leading }?.constant = self.contentPadding.left
self.paddingConstraints.first { $0.firstAttribute == .trailing }?.constant = -self.contentPadding.right
self.paddingConstraints.first { $0.firstAttribute == .bottom }?.constant = -self.contentPadding.bottom
self.paddingConstraints.first { $0.firstAttribute == .bottom }?.constant =
-(self.contentPadding.bottom + self.quotaBarReservedHeight)
if !self.isConfiguring { self.invalidateIntrinsicContentSize() }
}
}
Expand Down Expand Up @@ -202,6 +231,14 @@ final class StackedToggleButton: NSButton {
self.titleField.font = NSFont.systemFont(ofSize: size)
}

func setQuotaBarReservedHeight(_ height: CGFloat) {
guard self.quotaBarReservedHeight != height else { return }
self.quotaBarReservedHeight = height
self.paddingConstraints.first { $0.firstAttribute == .bottom }?.constant =
-(self.contentPadding.bottom + height)
if !self.isConfiguring { self.invalidateIntrinsicContentSize() }
}

func setAllowsTwoLineTitle(_ allow: Bool) {
let hasWhitespace = self.titleField.stringValue.rangeOfCharacter(from: .whitespacesAndNewlines) != nil
let shouldWrap = allow && hasWhitespace
Expand All @@ -214,7 +251,7 @@ final class StackedToggleButton: NSButton {
let size = self.stack.fittingSize
return NSSize(
width: size.width + self.contentPadding.left + self.contentPadding.right,
height: size.height + self.contentPadding.top + self.contentPadding.bottom)
height: size.height + self.contentPadding.top + self.contentPadding.bottom + self.quotaBarReservedHeight)
}

init(title: String, image: NSImage, target: AnyObject?, action: Selector?) {
Expand Down Expand Up @@ -282,3 +319,5 @@ final class StackedToggleButton: NSButton {
NSLayoutConstraint.activate(self.paddingConstraints + self.iconSizeConstraints)
}
}

extension StackedToggleButton: ProviderSwitcherToggleButton {}
5 changes: 4 additions & 1 deletion Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ extension StatusItemController {
{
self.performMenuMutationWithoutAnimation {
let contentStartIndex = menu.items.first?.view is ProviderSwitcherView ? 2 : 0
(menu.items.first?.view as? ProviderSwitcherView)?.updateSelection(context.switcherSelection)
if let switcherView = menu.items.first?.view as? ProviderSwitcherView {
switcherView.updateSelection(context.switcherSelection)
switcherView.updateQuotaIndicators()
}
while menu.items.count > contentStartIndex {
menu.removeItem(at: contentStartIndex)
}
Expand Down
Loading