From eaf7cd1b2e775531266602b65547982919656d6b Mon Sep 17 00:00:00 2001 From: lonewolfyx Date: Mon, 13 Jul 2026 16:35:56 +0800 Subject: [PATCH] refactor: simplify menu bar to weekly-only quota and drop Codex Radar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce the menu bar to a single weekly quota window and remove the Codex Radar (model/reasoning-level) data source, shrinking the status item footprint and popover height. - QuotaSnapshot: merge primary/secondary windows into a single weekly window; remove ModelReasonEntry/ModelReasonSnapshot models - CodexRateLimitService: parse only the 10080-min (1w) window, drop the 5h short window and selectMenuWindows; switch error to missingWeeklyQuotaWindow - Remove CodexRadarService and the radar refresh timer/scheduling in QuotaStore - MenuBarQuotaView: drop the 5h row, separator, and model/reason rows; switch to the system menu bar font; statusItem width 168 -> 76 - QuotaPopoverView / TokenUsageSectionView: tighten popover and chart heights (popover 660/624 -> 520, chart 72 -> 56) - I18n: remove currentSession; rename resetCreditDefaultTitle to "Rate-limit reset" / "额度重置" - Package.swift: pin Sparkle to exact: "2.9.4"; sync Package.resolved --- Package.resolved | 4 +- Package.swift | 2 +- Sources/AgentBar/App/AppDelegate.swift | 12 +- Sources/AgentBar/Localization/I18n.swift | 15 +- Sources/AgentBar/Models/QuotaModels.swift | 27 +- .../AgentBar/Services/CodexRadarService.swift | 284 ------------------ .../Services/CodexRateLimitService.swift | 25 +- Sources/AgentBar/Store/QuotaStore.swift | 79 ----- Sources/AgentBar/UI/MenuBarQuotaView.swift | 164 +--------- Sources/AgentBar/UI/QuotaPopoverView.swift | 19 +- .../AgentBar/UI/TokenUsageSectionView.swift | 4 +- 11 files changed, 31 insertions(+), 604 deletions(-) delete mode 100644 Sources/AgentBar/Services/CodexRadarService.swift diff --git a/Package.resolved b/Package.resolved index cacc02f..45714d6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,12 @@ { - "originHash" : "e448248da6b7bc13aaee13f593daa9f669a5a581416de85ec0101194d86e2cd8", + "originHash" : "97cf1912cb6223cdafa20933980bebf75234b26888421b50b6646400bece1d4c", "pins" : [ { "identity" : "sparkle", "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7", + "revision" : "1d5611a1250a3c835cba39cf33cff1cc229eb50c", "version" : "2.9.4" } } diff --git a/Package.swift b/Package.swift index 037a92b..041dd5e 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( .executable(name: "AgentBar", targets: ["AgentBar"]), ], dependencies: [ - .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.4"), + .package(url: "https://github.com/sparkle-project/Sparkle", exact: "2.9.4"), ], targets: [ .executableTarget( diff --git a/Sources/AgentBar/App/AppDelegate.swift b/Sources/AgentBar/App/AppDelegate.swift index b30c8de..3c3dfc1 100644 --- a/Sources/AgentBar/App/AppDelegate.swift +++ b/Sources/AgentBar/App/AppDelegate.swift @@ -5,7 +5,7 @@ import SwiftUI @MainActor final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { - private static let statusItemWidth: CGFloat = 168 + private static let statusItemWidth: CGFloat = 76 private let store = QuotaStore() private var statusItem: NSStatusItem? @@ -72,7 +72,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { let popover = NSPopover() popover.behavior = .transient popover.delegate = self - popover.contentSize = NSSize(width: 320, height: 660) + popover.contentSize = NSSize(width: 320, height: 520) popover.contentViewController = VisualEffectHostingController( rootView: QuotaPopoverView( store: store, @@ -106,13 +106,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { } .store(in: &cancellables) - store.$modelReasonSnapshot - .receive(on: RunLoop.main) - .sink { [weak self] _ in - self?.updateMenuBarTitle() - } - .store(in: &cancellables) - store.$cliUpgradeAlertMessage .receive(on: RunLoop.main) .sink { [weak self] message in @@ -124,7 +117,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { private func updateMenuBarTitle() { menuBarView?.update( snapshot: store.snapshot, - modelReasonSnapshot: store.modelReasonSnapshot, statusMessage: store.statusMessage ) } diff --git a/Sources/AgentBar/Localization/I18n.swift b/Sources/AgentBar/Localization/I18n.swift index d5c9911..411a48c 100644 --- a/Sources/AgentBar/Localization/I18n.swift +++ b/Sources/AgentBar/Localization/I18n.swift @@ -17,7 +17,6 @@ enum I18n { let refreshNow: String let aboutUs: String let quit: String - let currentSession: String let recentWeek: String let resetTimeUnavailable: String let resetSoon: String @@ -37,7 +36,7 @@ enum I18n { let missingAccountResult: String let missingRateLimitResult: String let missingTokenUsageResult: String - let expectedQuotaWindows: String + let missingWeeklyQuotaWindow: String let availableResetCredits: String let resetCreditDefaultTitle: String let resetCreditNeverExpires: String @@ -169,8 +168,6 @@ enum I18n { func durationTitle(minutes: Int) -> String { switch minutes { - case 300: - return currentSession case 10080: return recentWeek case ..<1440: @@ -235,7 +232,6 @@ enum I18n { refreshNow: "Refresh now", aboutUs: "About us", quit: "Quit", - currentSession: "Current session", recentWeek: "Past 1 week", resetTimeUnavailable: "Reset time unavailable", resetSoon: "Reset soon", @@ -255,9 +251,9 @@ enum I18n { missingAccountResult: "Missing account result.", missingRateLimitResult: "Missing rate limit result.", missingTokenUsageResult: "Missing token usage result.", - expectedQuotaWindows: "Expected primary and secondary quota windows.", + missingWeeklyQuotaWindow: "The weekly quota window was not returned.", availableResetCredits: "Available resets:", - resetCreditDefaultTitle: "Full reset (Weekly + 5 hr)", + resetCreditDefaultTitle: "Rate-limit reset", resetCreditNeverExpires: "Does not expire", resetCreditDetailsUnavailable: "Details unavailable", noAvailableResetCredits: "No reset credits available", @@ -281,7 +277,6 @@ enum I18n { refreshNow: "立即刷新", aboutUs: "关于我们", quit: "退出", - currentSession: "当前会话", recentWeek: "近 1 周", resetTimeUnavailable: "刷新时间不可用", resetSoon: "即将刷新", @@ -301,9 +296,9 @@ enum I18n { missingAccountResult: "缺少账户结果。", missingRateLimitResult: "缺少额度结果。", missingTokenUsageResult: "缺少 token 消耗结果。", - expectedQuotaWindows: "需要 primary 和 secondary 两个额度窗口。", + missingWeeklyQuotaWindow: "未返回周额度窗口。", availableResetCredits: "可用重置次数:", - resetCreditDefaultTitle: "完整重置(周 + 5 小时)", + resetCreditDefaultTitle: "额度重置", resetCreditNeverExpires: "不会到期", resetCreditDetailsUnavailable: "暂无重置详情", noAvailableResetCredits: "暂无可用重置次数", diff --git a/Sources/AgentBar/Models/QuotaModels.swift b/Sources/AgentBar/Models/QuotaModels.swift index a3856ad..11e0ed9 100644 --- a/Sources/AgentBar/Models/QuotaModels.swift +++ b/Sources/AgentBar/Models/QuotaModels.swift @@ -21,8 +21,7 @@ struct RateLimitResetCredit: Identifiable, Sendable { } struct QuotaSnapshot: Sendable { - var primary: QuotaWindow - var secondary: QuotaWindow + var weekly: QuotaWindow var availableResetCredits: Int? var resetCredits: [RateLimitResetCredit]? var lastUpdated: Date @@ -62,30 +61,6 @@ struct TokenUsageSnapshot: Sendable { let lastUpdated: Date } -struct ModelReasonEntry: Sendable { - let model: String - let reasoningEffort: String - let score: Double - let date: String? - - var displayModel: String { - model - .replacingOccurrences(of: "gpt-", with: "GPT ") - .replacingOccurrences(of: "GPT-", with: "GPT ") - } - - var displayReasoningEffort: String { - reasoningEffort.lowercased() - } -} - -struct ModelReasonSnapshot: Sendable { - let selected: ModelReasonEntry - let entries: [ModelReasonEntry] - let sourceURL: URL - let lastUpdated: Date -} - struct CodexCLIVersion: Comparable, Sendable { let major: Int let minor: Int diff --git a/Sources/AgentBar/Services/CodexRadarService.swift b/Sources/AgentBar/Services/CodexRadarService.swift deleted file mode 100644 index 20138f6..0000000 --- a/Sources/AgentBar/Services/CodexRadarService.swift +++ /dev/null @@ -1,284 +0,0 @@ -import Foundation - -struct CodexRadarService { - private static let currentURL = URL(string: "https://codexradar.com/current.json")! - private static let htmlURL = URL(string: "https://codexradar.com/")! - - func readModelReason(completion: @escaping @Sendable (Result) -> Void) { - fetch(url: Self.currentURL) { result in - switch result { - case .success(let data): - do { - completion(.success(try parseCurrentJSON(data))) - } catch { - readModelReasonFromHTML(completion: completion) - } - case .failure: - readModelReasonFromHTML(completion: completion) - } - } - } - - private func readModelReasonFromHTML(completion: @escaping @Sendable (Result) -> Void) { - fetch(url: Self.htmlURL) { result in - switch result { - case .success(let data): - do { - completion(.success(try parseHTML(data))) - } catch { - completion(.failure(error)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - private func fetch(url: URL, completion: @escaping @Sendable (Result) -> Void) { - var request = URLRequest(url: url) - request.cachePolicy = .reloadIgnoringLocalCacheData - request.timeoutInterval = 15 - request.setValue("application/json,text/html;q=0.9,*/*;q=0.8", forHTTPHeaderField: "Accept") - request.setValue("codex-agent-bar/1.0", forHTTPHeaderField: "User-Agent") - - URLSession.shared.dataTask(with: request) { data, response, error in - if let error { - completion(.failure(error)) - return - } - - if - let response = response as? HTTPURLResponse, - !(200..<300).contains(response.statusCode) { - completion(.failure(CodexRadarError.requestFailed(response.statusCode))) - return - } - - guard let data, !data.isEmpty else { - completion(.failure(CodexRadarError.emptyResponse)) - return - } - - completion(.success(data)) - }.resume() - } -} - -private func parseCurrentJSON(_ data: Data) throws -> ModelReasonSnapshot { - guard - let root = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let modelIQ = root["model_iq"] as? [String: Any] - else { - throw CodexRadarError.parsingFailed("Missing model_iq") - } - - var entries: [ModelReasonEntry] = [] - if let latest = modelIQ["latest"] as? [String: Any], - let entry = parseJSONEntry(latest) { - entries.append(entry) - } - - if let comparisons = modelIQ["comparisons"] as? [String: Any] { - for value in comparisons.values { - guard let comparison = value as? [String: Any] else { - continue - } - - let latest = comparison["latest"] as? [String: Any] - let source = latest ?? comparison - if let entry = parseJSONEntry( - source, - fallbackModel: comparison["model"] as? String, - fallbackReasoningEffort: comparison["reasoning_effort"] as? String - ) { - entries.append(entry) - } - } - } - - return try makeSnapshot(entries: entries, sourceURL: URL(string: "https://codexradar.com/current.json")!) -} - -private func parseJSONEntry( - _ source: [String: Any], - fallbackModel: String? = nil, - fallbackReasoningEffort: String? = nil -) -> ModelReasonEntry? { - guard let score = doubleValue(source["score"]) else { - return nil - } - - let model = normalizedModel((source["model"] as? String) ?? fallbackModel) - let reasoningEffort = normalizedReasoningEffort((source["reasoning_effort"] as? String) ?? fallbackReasoningEffort) - - guard let model, let reasoningEffort else { - return nil - } - - return ModelReasonEntry( - model: model, - reasoningEffort: reasoningEffort, - score: score, - date: source["date"] as? String - ) -} - -private func parseHTML(_ data: Data) throws -> ModelReasonSnapshot { - guard let html = String(data: data, encoding: .utf8) else { - throw CodexRadarError.parsingFailed("HTML is not UTF-8") - } - - let pattern = #"]*class="[^"]*\bmodel-iq-score-chip\b[^"]*"[^>]*>.*?([^<]+)\s*([\d.]+)"# - let regex = try NSRegularExpression(pattern: pattern, options: [.dotMatchesLineSeparators]) - let matches = regex.matches(in: html, range: NSRange(html.startIndex..., in: html)) - let entries = matches.compactMap { match -> ModelReasonEntry? in - guard - let labelRange = Range(match.range(at: 1), in: html), - let scoreRange = Range(match.range(at: 2), in: html), - let score = Double(html[scoreRange]) - else { - return nil - } - - return parseHTMLEntry(label: String(html[labelRange]), score: score) - } - - return try makeSnapshot(entries: entries, sourceURL: URL(string: "https://codexradar.com/")!) -} - -private func parseHTMLEntry(label: String, score: Double) -> ModelReasonEntry? { - let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines) - let components = trimmed.replacingOccurrences(of: " ", with: "-").split(separator: "-") - - guard components.count >= 3, let effort = components.last else { - return nil - } - - let reasoningEffort = normalizedReasoningEffort(String(effort)) - let model = normalizedModel(components.dropLast().joined(separator: "-")) - - guard let model, let reasoningEffort else { - return nil - } - - return ModelReasonEntry(model: model, reasoningEffort: reasoningEffort, score: score, date: nil) -} - -private func makeSnapshot(entries: [ModelReasonEntry], sourceURL: URL) throws -> ModelReasonSnapshot { - let uniqueEntries = uniqueModelReasonEntries(entries) - guard let selected = sortedModelReasonEntries(uniqueEntries).first else { - throw CodexRadarError.parsingFailed("No model reason scores found") - } - - return ModelReasonSnapshot( - selected: selected, - entries: sortedModelReasonEntries(uniqueEntries), - sourceURL: sourceURL, - lastUpdated: Date() - ) -} - -private func uniqueModelReasonEntries(_ entries: [ModelReasonEntry]) -> [ModelReasonEntry] { - var seen = Set() - return entries.filter { entry in - let key = "\(entry.model.lowercased())-\(entry.reasoningEffort.lowercased())" - guard !seen.contains(key) else { - return false - } - - seen.insert(key) - return true - } -} - -private func sortedModelReasonEntries(_ entries: [ModelReasonEntry]) -> [ModelReasonEntry] { - entries.sorted { lhs, rhs in - if lhs.score != rhs.score { - return lhs.score > rhs.score - } - - let lhsReasonRank = reasoningRank(lhs.reasoningEffort) - let rhsReasonRank = reasoningRank(rhs.reasoningEffort) - if lhsReasonRank != rhsReasonRank { - return lhsReasonRank > rhsReasonRank - } - - let lhsModelRank = modelRank(lhs.model) - let rhsModelRank = modelRank(rhs.model) - if lhsModelRank != rhsModelRank { - return rhsModelRank.lexicographicallyPrecedes(lhsModelRank) - } - - return lhs.model.localizedStandardCompare(rhs.model) == .orderedAscending - } -} - -private func reasoningRank(_ value: String) -> Int { - switch value.lowercased() { - case "xhigh": - return 3 - case "high": - return 2 - case "medium": - return 1 - default: - return 0 - } -} - -private func modelRank(_ value: String) -> [Int] { - value - .split { !$0.isNumber } - .compactMap { Int($0) } -} - -private func normalizedModel(_ value: String?) -> String? { - guard let value else { - return nil - } - - let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmed.isEmpty ? nil : trimmed.lowercased() -} - -private func normalizedReasoningEffort(_ value: String?) -> String? { - guard let value else { - return nil - } - - let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - return trimmed.isEmpty ? nil : trimmed -} - -private func doubleValue(_ value: Any?) -> Double? { - if let value = value as? Double { - return value - } - - if let value = value as? Int { - return Double(value) - } - - if let value = value as? String { - return Double(value) - } - - return nil -} - -private enum CodexRadarError: LocalizedError { - case emptyResponse - case requestFailed(Int) - case parsingFailed(String) - - var errorDescription: String? { - switch self { - case .emptyResponse: - return "Codex radar returned an empty response." - case .requestFailed(let statusCode): - return "Codex radar request failed with HTTP \(statusCode)." - case .parsingFailed(let message): - return "Codex radar parsing failed: \(message)." - } - } -} diff --git a/Sources/AgentBar/Services/CodexRateLimitService.swift b/Sources/AgentBar/Services/CodexRateLimitService.swift index 281c6d9..5d3dc5f 100644 --- a/Sources/AgentBar/Services/CodexRateLimitService.swift +++ b/Sources/AgentBar/Services/CodexRateLimitService.swift @@ -35,15 +35,13 @@ struct CodexRateLimitService { (lhs.windowDurationMins ?? Int.max) < (rhs.windowDurationMins ?? Int.max) } - guard windows.count >= 2 else { - throw QuotaError.parsingFailed(I18n.current.expectedQuotaWindows) + guard let weekly = windows.first(where: { $0.windowDurationMins == 10080 }) else { + throw QuotaError.parsingFailed(I18n.current.missingWeeklyQuotaWindow) } - let selected = selectMenuWindows(from: windows) let resetCredits = resetCreditsSummary(from: result) let snapshot = QuotaSnapshot( - primary: selected.primary, - secondary: selected.secondary, + weekly: weekly, availableResetCredits: resetCredits.availableCount, resetCredits: resetCredits.credits, lastUpdated: Date() @@ -96,16 +94,6 @@ struct CodexRateLimitService { } } - private func selectMenuWindows(from windows: [QuotaWindow]) -> (primary: QuotaWindow, secondary: QuotaWindow) { - let fiveHour = windows.first { $0.windowDurationMins == 300 } - let oneWeek = windows.first { $0.windowDurationMins == 10080 } - - return ( - fiveHour ?? windows[0], - oneWeek ?? windows[1] - ) - } - private func intValue(_ value: Any?) -> Int? { if let value = value as? Int { return value @@ -210,8 +198,6 @@ struct CodexRateLimitService { } switch durationMins { - case 300: - return "5h" case 10080: return "1w" case ..<1440: @@ -227,8 +213,6 @@ struct CodexRateLimitService { } switch durationMins { - case 300: - return "5h" case 10080: return "1w" case ..<1440: @@ -241,8 +225,7 @@ struct CodexRateLimitService { private func printParsedQuota(_ snapshot: QuotaSnapshot) { let formatter = ISO8601DateFormatter() let payload: [String: Any] = [ - "primary": printableWindow(snapshot.primary, formatter: formatter), - "secondary": printableWindow(snapshot.secondary, formatter: formatter), + "weekly": printableWindow(snapshot.weekly, formatter: formatter), "availableResetCredits": snapshot.availableResetCredits ?? NSNull(), "resetCredits": snapshot.resetCredits?.map { printableResetCredit($0, formatter: formatter) } ?? NSNull(), "lastUpdated": formatter.string(from: snapshot.lastUpdated), diff --git a/Sources/AgentBar/Store/QuotaStore.swift b/Sources/AgentBar/Store/QuotaStore.swift index 3018d04..7a21cf3 100644 --- a/Sources/AgentBar/Store/QuotaStore.swift +++ b/Sources/AgentBar/Store/QuotaStore.swift @@ -10,16 +10,13 @@ final class QuotaStore: ObservableObject { @Published var isLoading = false @Published var cliUpgradeMessage: String? @Published var cliUpgradeAlertMessage: String? - @Published var modelReasonSnapshot: ModelReasonSnapshot? @Published private(set) var currentAccount: CodexAccount? private let client = CodexAppServerClient() private let accountService = CodexAccountService() private let rateLimitService = CodexRateLimitService() private let tokenUsageService = CodexTokenUsageService() - private let radarService = CodexRadarService() private var refreshTimer: Timer? - private var radarRefreshTimer: Timer? private var hasStarted = false init() { @@ -43,8 +40,6 @@ final class QuotaStore: ObservableObject { hasStarted = true isLoading = true statusMessage = I18n.current.loadingQuota - refreshModelReason() - scheduleNextModelReasonRefresh() checkCodexCLIVersionThenStart() } @@ -75,8 +70,6 @@ final class QuotaStore: ObservableObject { func stop() { refreshTimer?.invalidate() refreshTimer = nil - radarRefreshTimer?.invalidate() - radarRefreshTimer = nil client.stop() } @@ -85,81 +78,9 @@ final class QuotaStore: ObservableObject { return } - refreshModelReason() refreshAccountAndRateLimits() } - private func refreshModelReason() { - radarService.readModelReason { [weak self] result in - Task { @MainActor in - guard let self else { - return - } - - switch result { - case .success(let snapshot): - self.modelReasonSnapshot = snapshot - self.printParsedModelReason(snapshot) - case .failure(let error): - print("[AgentBar] Codex radar refresh failed: \(error.localizedDescription)") - } - } - } - } - - private func scheduleNextModelReasonRefresh() { - radarRefreshTimer?.invalidate() - - let nextRefreshDate = nextModelReasonRefreshDate(after: Date()) - radarRefreshTimer = Timer(fire: nextRefreshDate, interval: 0, repeats: false) { [weak self] _ in - Task { @MainActor in - guard let self else { - return - } - - self.refreshModelReason() - self.scheduleNextModelReasonRefresh() - } - } - - if let radarRefreshTimer { - RunLoop.main.add(radarRefreshTimer, forMode: .common) - } - } - - private func nextModelReasonRefreshDate(after date: Date) -> Date { - let calendar = Calendar.current - let refreshTimes = [(hour: 8, minute: 30), (hour: 14, minute: 30)] - - for refreshTime in refreshTimes { - guard - let candidate = calendar.nextDate( - after: calendar.startOfDay(for: date), - matching: DateComponents(hour: refreshTime.hour, minute: refreshTime.minute), - matchingPolicy: .nextTime - ), - candidate > date - else { - continue - } - - return candidate - } - - return calendar.nextDate( - after: date, - matching: DateComponents(hour: refreshTimes[0].hour, minute: refreshTimes[0].minute), - matchingPolicy: .nextTime - ) ?? date.addingTimeInterval(18 * 60 * 60) - } - - private func printParsedModelReason(_ snapshot: ModelReasonSnapshot) { - let selected = snapshot.selected - print( - "[AgentBar] Parsed Codex radar model reason: \(selected.displayModel) \(selected.displayReasoningEffort) \(selected.score)" - ) - } - private func refreshAccountAndRateLimits() { isLoading = true cliUpgradeMessage = nil diff --git a/Sources/AgentBar/UI/MenuBarQuotaView.swift b/Sources/AgentBar/UI/MenuBarQuotaView.swift index 4cecd7b..c750c89 100644 --- a/Sources/AgentBar/UI/MenuBarQuotaView.swift +++ b/Sources/AgentBar/UI/MenuBarQuotaView.swift @@ -2,17 +2,8 @@ import AppKit final class MenuBarQuotaView: NSControl { private let iconView = NSImageView() - private let fiveHourPrefixLabel = NSTextField(labelWithString: "5h") - private let fiveHourPercentLabel = NSTextField(labelWithString: "--") private let weekPrefixLabel = NSTextField(labelWithString: "1w") private let weekPercentLabel = NSTextField(labelWithString: "--") - private let separatorView = MenuBarSeparatorView() - private let reasoningModelPrefixLabel = NSTextField(labelWithString: "model") - private let reasoningModelLabel = NSTextField(labelWithString: "--") - private let reasoningLevelPrefixLabel = NSTextField(labelWithString: "reason") - private let reasoningLevelLabel = NSTextField(labelWithString: "--") - private let reasoningStack = NSStackView() - private let textStack = NSStackView() override init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -24,27 +15,19 @@ final class MenuBarQuotaView: NSControl { setup() } - func update(snapshot: QuotaSnapshot?, modelReasonSnapshot: ModelReasonSnapshot?, statusMessage: String) { - toolTip = tooltip(statusMessage: statusMessage, modelReasonSnapshot: modelReasonSnapshot) - updateModelReason(modelReasonSnapshot) + func update(snapshot: QuotaSnapshot?, statusMessage: String) { + toolTip = statusMessage guard let snapshot else { - fiveHourPrefixLabel.stringValue = "5h" - fiveHourPercentLabel.stringValue = "--" - fiveHourPercentLabel.textColor = .secondaryLabelColor weekPrefixLabel.stringValue = "1w" weekPercentLabel.stringValue = "--" weekPercentLabel.textColor = .secondaryLabelColor return } - fiveHourPrefixLabel.stringValue = snapshot.primary.shortTitle - fiveHourPercentLabel.stringValue = "\(Int(snapshot.primary.remainingPercent.rounded()))%" - fiveHourPercentLabel.textColor = quotaColor(forRemainingPercent: snapshot.primary.remainingPercent) - - weekPrefixLabel.stringValue = snapshot.secondary.shortTitle - weekPercentLabel.stringValue = "\(Int(snapshot.secondary.remainingPercent.rounded()))%" - weekPercentLabel.textColor = quotaColor(forRemainingPercent: snapshot.secondary.remainingPercent) + weekPrefixLabel.stringValue = snapshot.weekly.shortTitle + weekPercentLabel.stringValue = "\(Int(snapshot.weekly.remainingPercent.rounded()))%" + weekPercentLabel.textColor = quotaColor(forRemainingPercent: snapshot.weekly.remainingPercent) } override func mouseDown(with event: NSEvent) { @@ -54,6 +37,7 @@ final class MenuBarQuotaView: NSControl { private func setup() { wantsLayer = true toolTip = I18n.current.codexQuota + let defaultMenuBarFont = NSFont.menuBarFont(ofSize: 0) if let image = AppIcon.menuBarImage() { iconView.image = image @@ -66,65 +50,21 @@ final class MenuBarQuotaView: NSControl { iconView.imageScaling = .scaleProportionallyUpOrDown iconView.translatesAutoresizingMaskIntoConstraints = false - [fiveHourPrefixLabel, fiveHourPercentLabel, weekPrefixLabel, weekPercentLabel].forEach { label in - label.font = .monospacedDigitSystemFont(ofSize: 10, weight: .bold) - label.alignment = .left - label.lineBreakMode = .byClipping - label.setContentCompressionResistancePriority(.required, for: .horizontal) - label.translatesAutoresizingMaskIntoConstraints = false - } - - [reasoningModelPrefixLabel, reasoningLevelPrefixLabel].forEach { label in - label.font = .systemFont(ofSize: 8.5, weight: .medium) + [weekPrefixLabel, weekPercentLabel].forEach { label in + label.font = defaultMenuBarFont label.alignment = .left label.lineBreakMode = .byClipping - label.textColor = .labelColor label.setContentCompressionResistancePriority(.required, for: .horizontal) label.translatesAutoresizingMaskIntoConstraints = false } - [reasoningModelLabel, reasoningLevelLabel].forEach { label in - label.font = .systemFont(ofSize: 9.5, weight: .semibold) - label.alignment = .left - label.lineBreakMode = .byClipping - label.textColor = .labelColor - label.setContentCompressionResistancePriority(.required, for: .horizontal) - label.translatesAutoresizingMaskIntoConstraints = false - } - - separatorView.translatesAutoresizingMaskIntoConstraints = false - - fiveHourPrefixLabel.textColor = .labelColor weekPrefixLabel.textColor = .labelColor - fiveHourPercentLabel.textColor = .secondaryLabelColor weekPercentLabel.textColor = .secondaryLabelColor - let fiveHourRow = makeTextRow(prefix: fiveHourPrefixLabel, percent: fiveHourPercentLabel) let weekRow = makeTextRow(prefix: weekPrefixLabel, percent: weekPercentLabel) - textStack.orientation = .vertical - textStack.alignment = .leading - textStack.distribution = .fillEqually - textStack.spacing = -1 - textStack.translatesAutoresizingMaskIntoConstraints = false - textStack.addArrangedSubview(fiveHourRow) - textStack.addArrangedSubview(weekRow) - - let modelRow = makeReasoningRow(prefix: reasoningModelPrefixLabel, value: reasoningModelLabel) - let levelRow = makeReasoningRow(prefix: reasoningLevelPrefixLabel, value: reasoningLevelLabel) - - reasoningStack.orientation = .vertical - reasoningStack.alignment = .leading - reasoningStack.distribution = .fillEqually - reasoningStack.spacing = -1 - reasoningStack.translatesAutoresizingMaskIntoConstraints = false - reasoningStack.addArrangedSubview(modelRow) - reasoningStack.addArrangedSubview(levelRow) - addSubview(iconView) - addSubview(textStack) - addSubview(separatorView) - addSubview(reasoningStack) + addSubview(weekRow) NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: NSStatusBar.system.thickness), @@ -134,21 +74,9 @@ final class MenuBarQuotaView: NSControl { iconView.widthAnchor.constraint(equalToConstant: 18), iconView.heightAnchor.constraint(equalToConstant: 18), - textStack.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 5), - textStack.centerYAnchor.constraint(equalTo: centerYAnchor), - textStack.widthAnchor.constraint(equalToConstant: 50), - textStack.heightAnchor.constraint(equalToConstant: 24), - - separatorView.leadingAnchor.constraint(equalTo: textStack.trailingAnchor, constant: 5), - separatorView.centerYAnchor.constraint(equalTo: centerYAnchor), - separatorView.widthAnchor.constraint(equalToConstant: 1), - separatorView.heightAnchor.constraint(equalToConstant: 16), - - reasoningStack.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor, constant: 6), - reasoningStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6), - reasoningStack.centerYAnchor.constraint(equalTo: centerYAnchor), - reasoningStack.widthAnchor.constraint(equalToConstant: 70), - reasoningStack.heightAnchor.constraint(equalToConstant: 24), + weekRow.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 5), + weekRow.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6), + weekRow.centerYAnchor.constraint(equalTo: centerYAnchor), ]) } @@ -160,54 +88,9 @@ final class MenuBarQuotaView: NSControl { row.spacing = 5 row.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - prefix.widthAnchor.constraint(equalToConstant: 17), - percent.widthAnchor.constraint(equalToConstant: 28), - ]) - - return row - } - - private func makeReasoningRow(prefix: NSTextField, value: NSTextField) -> NSStackView { - let row = NSStackView(views: [prefix, value]) - row.orientation = .horizontal - row.alignment = .centerY - row.distribution = .gravityAreas - row.spacing = 3 - row.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - prefix.widthAnchor.constraint(equalToConstant: 29), - value.widthAnchor.constraint(equalToConstant: 38), - ]) - return row } - private func updateModelReason(_ snapshot: ModelReasonSnapshot?) { - guard let snapshot else { - reasoningModelLabel.stringValue = "--" - reasoningLevelLabel.stringValue = "--" - reasoningModelLabel.textColor = .secondaryLabelColor - reasoningLevelLabel.textColor = .secondaryLabelColor - return - } - - reasoningModelLabel.stringValue = snapshot.selected.displayModel - reasoningLevelLabel.stringValue = snapshot.selected.displayReasoningEffort - reasoningModelLabel.textColor = .labelColor - reasoningLevelLabel.textColor = .labelColor - } - - private func tooltip(statusMessage: String, modelReasonSnapshot: ModelReasonSnapshot?) -> String { - guard let modelReasonSnapshot else { - return statusMessage - } - - let selected = modelReasonSnapshot.selected - return "\(statusMessage)\nmodel reason: \(selected.displayModel) \(selected.displayReasoningEffort) \(selected.score)\nData: codexradar.com" - } - private func quotaColor(forRemainingPercent percent: Double) -> NSColor { switch percent { case ...20: @@ -219,26 +102,3 @@ final class MenuBarQuotaView: NSControl { } } } - -private final class MenuBarSeparatorView: NSView { - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - wantsLayer = true - updateSeparatorColor() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - wantsLayer = true - updateSeparatorColor() - } - - override func viewDidChangeEffectiveAppearance() { - super.viewDidChangeEffectiveAppearance() - updateSeparatorColor() - } - - private func updateSeparatorColor() { - layer?.backgroundColor = NSColor(calibratedWhite: 0.82, alpha: 1).cgColor - } -} diff --git a/Sources/AgentBar/UI/QuotaPopoverView.swift b/Sources/AgentBar/UI/QuotaPopoverView.swift index d65fc88..586e2fc 100644 --- a/Sources/AgentBar/UI/QuotaPopoverView.swift +++ b/Sources/AgentBar/UI/QuotaPopoverView.swift @@ -50,9 +50,7 @@ struct QuotaPopoverView: View { .padding(.bottom, 10) VStack(spacing: 14) { - QuotaRow(window: snapshot.primary) - Divider() - QuotaRow(window: snapshot.secondary) + QuotaRow(window: snapshot.weekly) } Divider() @@ -127,7 +125,7 @@ struct QuotaPopoverView: View { } private enum QuotaPopoverLayout { - static let popoverHeight: CGFloat = 624 + static let popoverHeight: CGFloat = 520 static let topPadding: CGFloat = 22 static let horizontalPadding: CGFloat = 18 static let menuHighlightMargin: CGFloat = 5 @@ -868,8 +866,6 @@ struct QuotaRow: View { private var displayTitle: String { switch window.windowDurationMins { - case 300: - return I18n.current.currentSession case 10080: return I18n.current.recentWeek case let duration?: @@ -899,10 +895,6 @@ struct QuotaRow: View { return I18n.current.resetTimeUnavailable } - if window.windowDurationMins == 300 { - return I18n.current.refreshAt(Self.timeFormatter().string(from: resetsAt)) - } - if window.windowDurationMins == 10080 { return I18n.current.refreshAt(Self.dateFormatter().string(from: resetsAt)) } @@ -927,13 +919,6 @@ struct QuotaRow: View { String(format: "%02d", value) } - private static func timeFormatter() -> DateFormatter { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: I18n.current.dateLocaleIdentifier) - formatter.dateFormat = "HH:mm:ss" - return formatter - } - private static func dateFormatter() -> DateFormatter { let formatter = DateFormatter() formatter.locale = Locale(identifier: I18n.current.dateLocaleIdentifier) diff --git a/Sources/AgentBar/UI/TokenUsageSectionView.swift b/Sources/AgentBar/UI/TokenUsageSectionView.swift index d38a614..9e311c3 100644 --- a/Sources/AgentBar/UI/TokenUsageSectionView.swift +++ b/Sources/AgentBar/UI/TokenUsageSectionView.swift @@ -107,11 +107,11 @@ private struct TokenUsageChartView: View { } } } - .frame(height: 72) + .frame(height: 56) .padding(.horizontal, 2) } else { TokenUsagePlaceholderView(text: I18n.current.tokenUsageUnavailable) - .frame(height: 72) + .frame(height: 56) } } .padding(.top, 2)