Skip to content
Open
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
54 changes: 54 additions & 0 deletions Sources/CodexBar/CodexSessionsWatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Darwin
import Dispatch
import Foundation

final class CodexSessionsWatcher {
let watchedPaths: [String]
private(set) var isWatching: Bool = false

private let queue = DispatchQueue(label: "com.steipete.CodexBar.codexSessionsWatcher", qos: .utility)
private var descriptors: [CInt] = []
private var sources: [DispatchSourceFileSystemObject] = []
private let onChange: @Sendable () -> Void

init(urls: [URL], onChange: @escaping @Sendable () -> Void) {
self.watchedPaths = urls.map(\.path).sorted()
self.onChange = onChange
self.start(urls: urls)
self.isWatching = !self.sources.isEmpty
}

deinit {
self.stop()
}

private func start(urls: [URL]) {
for url in urls where FileManager.default.fileExists(atPath: url.path) {
let descriptor = Darwin.open(url.path, O_EVTONLY)
guard descriptor >= 0 else { continue }

let source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: descriptor,
eventMask: [.write, .extend, .delete, .rename, .attrib, .link, .revoke],
queue: self.queue)

source.setEventHandler { [onChange = self.onChange] in
onChange()
}
source.setCancelHandler {
Darwin.close(descriptor)
}
source.resume()

self.descriptors.append(descriptor)
self.sources.append(source)
}
}

private func stop() {
let activeSources = self.sources
self.sources.removeAll()
self.descriptors.removeAll()
activeSources.forEach { $0.cancel() }
}
}
8 changes: 8 additions & 0 deletions Sources/CodexBar/Providers/Codex/CodexSettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import CodexBarCore
import Foundation

extension SettingsStore {
static var codexSessionAnalyticsWindowPresets: [Int] {
[10, 20, 50, 100]
}

static func normalizedCodexSessionAnalyticsWindowSize(_ value: Int) -> Int {
self.codexSessionAnalyticsWindowPresets.contains(value) ? value : 20
}

private var codexPersistedActiveSource: CodexActiveSource {
if let persistedSource = self.providerConfig(for: .codex)?.codexActiveSource {
return persistedSource
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ extension SettingsStore {
}
}

var codexSessionAnalyticsWindowSize: Int {
get { self.defaultsState.codexSessionAnalyticsWindowSize }
set {
let normalized = Self.normalizedCodexSessionAnalyticsWindowSize(newValue)
self.defaultsState.codexSessionAnalyticsWindowSize = normalized
self.userDefaults.set(normalized, forKey: "codexSessionAnalyticsWindowSize")
}
}

var costUsageEnabled: Bool {
get { self.defaultsState.costUsageEnabled }
set {
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStore+MenuObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ extension SettingsStore {
_ = self.historicalTrackingEnabled
_ = self.showAllTokenAccountsInMenu
_ = self.menuBarMetricPreferencesRaw
_ = self.codexSessionAnalyticsWindowSize
_ = self.costUsageEnabled
_ = self.hidePersonalInfo
_ = self.randomBlinkEnabled
Expand Down
9 changes: 8 additions & 1 deletion Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,12 @@ extension SettingsStore {
forKey: "mergedOverviewSelectedProviders") as? [String] ?? []
let selectedMenuProviderRaw = userDefaults.string(forKey: "selectedMenuProvider")
let providerDetectionCompleted = userDefaults.object(forKey: "providerDetectionCompleted") as? Bool ?? false
let storedAnalyticsWindowSize = userDefaults.object(forKey: "codexSessionAnalyticsWindowSize") as? Int
let codexSessionAnalyticsWindowSize = Self.normalizedCodexSessionAnalyticsWindowSize(
storedAnalyticsWindowSize ?? 20)
if storedAnalyticsWindowSize != codexSessionAnalyticsWindowSize {
userDefaults.set(codexSessionAnalyticsWindowSize, forKey: "codexSessionAnalyticsWindowSize")
}

return SettingsDefaultsState(
refreshFrequency: refreshFrequency,
Expand Down Expand Up @@ -264,7 +270,8 @@ extension SettingsStore {
mergedMenuLastSelectedWasOverview: mergedMenuLastSelectedWasOverview,
mergedOverviewSelectedProvidersRaw: mergedOverviewSelectedProvidersRaw,
selectedMenuProviderRaw: selectedMenuProviderRaw,
providerDetectionCompleted: providerDetectionCompleted)
providerDetectionCompleted: providerDetectionCompleted,
codexSessionAnalyticsWindowSize: codexSessionAnalyticsWindowSize)
}
}

Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ struct SettingsDefaultsState {
var mergedOverviewSelectedProvidersRaw: [String]
var selectedMenuProviderRaw: String?
var providerDetectionCompleted: Bool
var codexSessionAnalyticsWindowSize: Int
}
36 changes: 36 additions & 0 deletions Sources/CodexBar/StatusItemController+HostedSubviewMenus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import AppKit

extension StatusItemController {
func isHostedSubviewMenu(_ menu: NSMenu) -> Bool {
let ids: Set = [
"usageBreakdownChart",
"creditsHistoryChart",
"costHistoryChart",
"usageHistoryChart",
"sessionAnalyticsContent",
"sessionAnalyticsEmptyState",
]
return menu.items.contains { item in
guard let id = item.representedObject as? String else { return false }
return ids.contains(id)
}
}

func isSessionAnalyticsSubviewMenu(_ menu: NSMenu) -> Bool {
menu.items.contains { item in
guard let id = item.representedObject as? String else { return false }
return id.hasPrefix("sessionAnalytics")
}
}

func isOpenAIWebSubviewMenu(_ menu: NSMenu) -> Bool {
let ids: Set = [
"usageBreakdownChart",
"creditsHistoryChart",
]
return menu.items.contains { item in
guard let id = item.representedObject as? String else { return false }
return ids.contains(id)
}
}
}
48 changes: 21 additions & 27 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ extension StatusItemController {
}

func menuWillOpen(_ menu: NSMenu) {
if self.isSessionAnalyticsRootMenu(menu) {
self.populateSessionAnalyticsSubmenu(menu)
self.refreshHostedSubviewHeights(in: menu)
self.openMenus[ObjectIdentifier(menu)] = menu
return
}

if self.isHostedSubviewMenu(menu) {
self.refreshHostedSubviewHeights(in: menu)
if Self.menuRefreshEnabled, self.isOpenAIWebSubviewMenu(menu) {
Expand Down Expand Up @@ -252,7 +259,9 @@ extension StatusItemController {
currentProvider: currentProvider,
context: openAIContext,
addedOpenAIWebItems: addedOpenAIWebItems)
if self.addUsageHistoryMenuItemIfNeeded(to: menu, provider: currentProvider) {
let addedUsageHistory = self.addUsageHistoryMenuItemIfNeeded(to: menu, provider: currentProvider)
let addedSessionAnalytics = self.addSessionAnalyticsMenuItemIfNeeded(to: menu, provider: currentProvider)
if addedUsageHistory || addedSessionAnalytics {
menu.addItem(.separator())
}
}
Expand Down Expand Up @@ -304,7 +313,9 @@ extension StatusItemController {
currentProvider: currentProvider,
context: openAIContext,
addedOpenAIWebItems: addedOpenAIWebItems)
if self.addUsageHistoryMenuItemIfNeeded(to: menu, provider: currentProvider) {
let addedUsageHistory = self.addUsageHistoryMenuItemIfNeeded(to: menu, provider: currentProvider)
let addedSessionAnalytics = self.addSessionAnalyticsMenuItemIfNeeded(to: menu, provider: currentProvider)
if addedUsageHistory || addedSessionAnalytics {
menu.addItem(.separator())
}
self.addActionableSections(descriptor.sections, to: menu, width: menuWidth)
Expand Down Expand Up @@ -799,6 +810,12 @@ extension StatusItemController {
continue
}

if self.isSessionAnalyticsRootMenu(menu) {
self.populateSessionAnalyticsSubmenu(menu)
self.refreshHostedSubviewHeights(in: menu)
continue
}

if self.isHostedSubviewMenu(menu) {
self.refreshHostedSubviewHeights(in: menu)
continue
Expand Down Expand Up @@ -925,6 +942,7 @@ extension StatusItemController {
id: String,
width: CGFloat,
submenu: NSMenu? = nil,
showsSubmenuIndicator: Bool = true,
submenuIndicatorAlignment: Alignment = .topTrailing,
submenuIndicatorTopPadding: CGFloat = 8,
onClick: (() -> Void)? = nil) -> NSMenuItem
Expand All @@ -944,7 +962,7 @@ extension StatusItemController {
let highlightState = MenuCardHighlightState()
let wrapped = MenuCardSectionContainerView(
highlightState: highlightState,
showsSubmenuIndicator: submenu != nil,
showsSubmenuIndicator: submenu != nil && showsSubmenuIndicator,
submenuIndicatorAlignment: submenuIndicatorAlignment,
submenuIndicatorTopPadding: submenuIndicatorTopPadding)
{
Expand Down Expand Up @@ -1484,30 +1502,6 @@ extension StatusItemController {
return submenu
}

private func isHostedSubviewMenu(_ menu: NSMenu) -> Bool {
let ids: Set = [
"usageBreakdownChart",
"creditsHistoryChart",
"costHistoryChart",
"usageHistoryChart",
]
return menu.items.contains { item in
guard let id = item.representedObject as? String else { return false }
return ids.contains(id)
}
}

private func isOpenAIWebSubviewMenu(_ menu: NSMenu) -> Bool {
let ids: Set = [
"usageBreakdownChart",
"creditsHistoryChart",
]
return menu.items.contains { item in
guard let id = item.representedObject as? String else { return false }
return ids.contains(id)
}
}

private func refreshHostedSubviewHeights(in menu: NSMenu) {
let enabledProviders = self.store.enabledProvidersForDisplay()
let width = self.menuCardWidth(for: enabledProviders, menu: menu)
Expand Down
Loading