chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:33 +08:00
commit fc4fcbab58
1848 changed files with 472303 additions and 0 deletions
@@ -0,0 +1,87 @@
import Foundation
/// Canonical adaptive-refresh decision table shared by the app and offline replay tooling.
/// Platform adapters normalize their thermal signals before calling this type; thresholds and
/// delays live here only.
package struct AdaptiveRefreshPolicyCore: Sendable {
package struct Input: Sendable, Equatable {
package let now: Date
package let lastMenuOpenAt: Date?
package let lowPowerModeEnabled: Bool
package let thermalPressure: ThermalPressure
package init(
now: Date,
lastMenuOpenAt: Date?,
lowPowerModeEnabled: Bool,
thermalPressure: ThermalPressure)
{
self.now = now
self.lastMenuOpenAt = lastMenuOpenAt
self.lowPowerModeEnabled = lowPowerModeEnabled
self.thermalPressure = thermalPressure
}
}
package enum ThermalPressure: Sendable, Equatable {
case nominal
case constrained
}
package enum Reason: String, Sendable, Equatable {
case recentInteraction
case warm
case idle
case longIdle
case constrained
}
package struct Decision: Sendable, Equatable {
package let delay: Duration
package let reason: Reason
fileprivate init(delay: Duration, reason: Reason) {
self.delay = delay
self.reason = reason
}
}
private static let recentInteractionThreshold: TimeInterval = 5 * 60
private static let warmThreshold: TimeInterval = 60 * 60
private static let idleThreshold: TimeInterval = 4 * 60 * 60
private static let recentInteractionDelay: Duration = .seconds(2 * 60)
private static let warmDelay: Duration = .seconds(5 * 60)
private static let idleDelay: Duration = .seconds(15 * 60)
private static let longIdleDelay: Duration = .seconds(30 * 60)
private static let constrainedDelay: Duration = .seconds(30 * 60)
/// Representative cadence for consumers that need one interval but cannot access live state.
package static let nominalIntervalForHeuristics: TimeInterval = 5 * 60
package init() {}
package func nextDelay(for input: Input) -> Decision {
if input.lowPowerModeEnabled || input.thermalPressure == .constrained {
return Decision(delay: Self.constrainedDelay, reason: .constrained)
}
guard let lastMenuOpenAt = input.lastMenuOpenAt else {
return Decision(delay: Self.longIdleDelay, reason: .longIdle)
}
// A future or clock-adjusted timestamp yields a negative age, which reads as recent.
let age = input.now.timeIntervalSince(lastMenuOpenAt)
if age <= Self.recentInteractionThreshold {
return Decision(delay: Self.recentInteractionDelay, reason: .recentInteraction)
}
if age <= Self.warmThreshold {
return Decision(delay: Self.warmDelay, reason: .warm)
}
if age < Self.idleThreshold {
return Decision(delay: Self.idleDelay, reason: .idle)
}
return Decision(delay: Self.longIdleDelay, reason: .longIdle)
}
}
@@ -0,0 +1,98 @@
import AdaptiveReplayKit
import Foundation
enum ReplayPolicyName: String, CaseIterable, Sendable {
case adaptive
case adaptiveActivity = "adaptive-activity"
case fixed2Minutes = "fixed-2m"
case fixed5Minutes = "fixed-5m"
case fixed15Minutes = "fixed-15m"
case fixed30Minutes = "fixed-30m"
case manual
var policy: any ReplayPolicy {
switch self {
case .adaptive:
AdaptiveReplayPolicy()
case .adaptiveActivity:
CodingActivityAdaptivePolicy()
case .fixed2Minutes:
FixedIntervalPolicy(minutes: 2)
case .fixed5Minutes:
FixedIntervalPolicy(minutes: 5)
case .fixed15Minutes:
FixedIntervalPolicy(minutes: 15)
case .fixed30Minutes:
FixedIntervalPolicy(minutes: 30)
case .manual:
ManualPolicy()
}
}
static var expectedValues: String {
allCases.map(\.rawValue).joined(separator: ", ")
}
}
enum CLIArguments {
case run(
tracePath: String,
policyNames: [ReplayPolicyName],
jsonOutput: Bool,
gapGraceSeconds: TimeInterval?)
case help(exitCode: Int32)
case invalid(message: String)
static func parse(_ arguments: [String]) -> Self {
if arguments.contains("-h") || arguments.contains("--help") {
return .help(exitCode: EXIT_SUCCESS)
}
var tracePath: String?
var policyNames: [ReplayPolicyName] = []
var jsonOutput = false
var gapGraceSeconds: TimeInterval? = ReplayTraceSegmenter.defaultGraceSeconds
var index = 0
while index < arguments.count {
let argument = arguments[index]
switch argument {
case "--json":
jsonOutput = true
case "--raw-wall-clock":
gapGraceSeconds = nil
case "--gap-grace":
index += 1
guard index < arguments.count,
let seconds = TimeInterval(arguments[index]),
seconds >= 0,
seconds.isFinite
else { return .invalid(message: "--gap-grace requires non-negative finite seconds") }
gapGraceSeconds = seconds
case "--policy":
index += 1
guard index < arguments.count else { return .invalid(message: "--policy requires a value") }
let rawPolicyName = arguments[index]
guard let policyName = ReplayPolicyName(rawValue: rawPolicyName) else {
return .invalid(
message: "unknown policy '\(rawPolicyName)' (expected: \(ReplayPolicyName.expectedValues))")
}
policyNames.append(policyName)
default:
guard tracePath == nil else {
return .invalid(message: "unexpected argument '\(argument)'")
}
tracePath = argument
}
index += 1
}
guard let tracePath else {
return .help(exitCode: EXIT_FAILURE)
}
return .run(
tracePath: tracePath,
policyNames: policyNames.isEmpty ? ReplayPolicyName.allCases : policyNames,
jsonOutput: jsonOutput,
gapGraceSeconds: gapGraceSeconds)
}
}
+217
View File
@@ -0,0 +1,217 @@
import AdaptiveReplayKit
import Foundation
/// Thin CLI shell over `AdaptiveReplayKit`: parses a trace path and a policy name, runs the
/// replay, and prints the resulting `ReplayMetrics`. All parsing/replay/metrics logic lives in
/// the library this file only routes arguments to it and formats the result.
enum AdaptiveReplayCLI {
static func main() {
let arguments = CLIArguments.parse(Array(CommandLine.arguments.dropFirst()))
switch arguments {
case let .help(exitCode):
print(Self.helpText)
exit(exitCode)
case let .invalid(message):
FileHandle.standardError.write(Data("error: \(message)\n\n\(Self.helpText)\n".utf8))
exit(EXIT_FAILURE)
case let .run(tracePath, policyNames, jsonOutput, gapGraceSeconds):
Self.run(
tracePath: tracePath,
policyNames: policyNames,
jsonOutput: jsonOutput,
gapGraceSeconds: gapGraceSeconds)
}
}
private static func run(
tracePath: String,
policyNames: [ReplayPolicyName],
jsonOutput: Bool,
gapGraceSeconds: TimeInterval?)
{
let records: [AdaptiveRefreshTraceRecord]
do {
records = try AdaptiveRefreshTraceParser.parse(contentsOf: URL(fileURLWithPath: tracePath))
} catch {
FileHandle.standardError.write(Data("error: failed to parse trace: \(error)\n".utf8))
exit(EXIT_FAILURE)
}
let policies = policyNames.map(\.policy)
let results = policies.map { policy in
gapGraceSeconds.map {
ReplayEngine.runSegmented(trace: records, policy: policy, graceSeconds: $0)
} ?? ReplayEngine.run(trace: records, policy: policy)
}
let activityCoverage = ActivityCoverageStats.compute(from: records)
let recordedScheduleAudit = RecordedScheduleAuditor.audit(records)
if jsonOutput {
print(Self.renderJSON(
results,
activityCoverage: activityCoverage,
recordedScheduleAudit: recordedScheduleAudit,
gapGraceSeconds: gapGraceSeconds))
} else {
print(Self.renderTable(results))
print(Self.renderActivityCoverage(activityCoverage))
print(Self.renderRecordedScheduleAudit(recordedScheduleAudit))
if let gapGraceSeconds, let first = results.first {
print(String(
format: "segmentation: %d segments, %.2fh excluded (legacy heuristic, %.0fs grace)",
first.segmentCount,
first.excludedGapSeconds / 3600,
gapGraceSeconds))
} else {
print("segmentation: disabled (raw wall clock)")
}
}
}
private static func renderRecordedScheduleAudit(_ audit: RecordedScheduleAudit) -> String {
"recorded schedule: \(audit.recordedAdvanceCount) advances, "
+ "\(audit.acceptedEvaluationCount)/\(audit.evaluatedCount) evaluations accepted, "
+ "payload=\(audit.payloadMismatchCount) decision=\(audit.decisionMismatchCount) "
+ "menu-link=\(audit.menuLinkMismatchCount) mismatches, "
+ "ambiguous=\(audit.ambiguousComparisonCount)"
}
/// Reports coverage of optional activity observations already present in the input trace.
private static func renderActivityCoverage(_ stats: ActivityCoverageStats) -> String {
guard stats.decisionCount > 0 else {
return "activity telemetry: no decision events in trace"
}
let sampledSummary = String(
format: "%d/%d decisions sampled (%.0f%%)",
stats.sampledCount,
stats.decisionCount,
stats.sampledFraction * 100)
let activeSummary = String(
format: "%d/%d active coding at decision time (%.0f%%)",
stats.activeCount,
stats.sampledCount,
stats.activeFraction * 100)
return "activity telemetry: \(sampledSummary), \(activeSummary)"
}
private static func renderTable(_ results: [ReplayMetrics]) -> String {
var lines: [String] = []
let header = [
"policy", "refreshes", "per24h", "sim advances", "active >5m", "staleness p50",
"staleness p95", "constrained ok",
]
lines.append(header.joined(separator: "\t"))
for metrics in results {
let staleness = metrics.stalenessAtMenuOpen
lines.append([
metrics.policyName,
String(metrics.totalRefreshCount),
String(format: "%.2f", metrics.refreshCountPer24h),
String(metrics.interactionAdvanceCount),
"\(metrics.codingActiveDelayViolationCount)/\(metrics.codingActiveDecisionCount)",
staleness.map { String(format: "%.0fs", $0.median) } ?? "n/a",
staleness.map { String(format: "%.0fs", $0.p95) } ?? "n/a",
metrics.constrainedCompliance
.isCompliant ? "yes" : "NO (\(metrics.constrainedCompliance.violationCount))",
].joined(separator: "\t"))
}
return lines.joined(separator: "\n")
}
private static func renderJSON(
_ results: [ReplayMetrics],
activityCoverage: ActivityCoverageStats,
recordedScheduleAudit: RecordedScheduleAudit,
gapGraceSeconds: TimeInterval?) -> String
{
let policies = results.map { metrics -> [String: Any] in
var dict: [String: Any] = [
"policy": metrics.policyName,
"simulatedSpanSeconds": metrics.simulatedSpanSeconds,
"totalRefreshCount": metrics.totalRefreshCount,
"refreshCountPer24h": metrics.refreshCountPer24h,
"interactionAdvanceCount": metrics.interactionAdvanceCount,
"codingActiveDecisionCount": metrics.codingActiveDecisionCount,
"codingActiveDelayViolationCount": metrics.codingActiveDelayViolationCount,
"segmentCount": metrics.segmentCount,
"excludedGapSeconds": metrics.excludedGapSeconds,
"boundaryCensoredMenuOpenCount": metrics.boundaryCensoredMenuOpenCount,
"constrainedDecisionCount": metrics.constrainedCompliance.constrainedDecisionCount,
"constrainedViolationCount": metrics.constrainedCompliance.violationCount,
"constrainedCompliant": metrics.constrainedCompliance.isCompliant,
]
if let staleness = metrics.stalenessAtMenuOpen {
dict["stalenessMeanSeconds"] = staleness.mean
dict["stalenessMedianSeconds"] = staleness.median
dict["stalenessP95Seconds"] = staleness.p95
dict["stalenessSampleCount"] = staleness.sampleCount
}
return dict
}
let segmentation: [String: Any] = [
"mode": gapGraceSeconds == nil ? "rawWallClock" : "legacyGapHeuristic",
"gapGraceSeconds": gapGraceSeconds.map { $0 as Any } ?? NSNull(),
]
let payload: [String: Any] = [
"policies": policies,
"activityCoverage": [
"decisionCount": activityCoverage.decisionCount,
"sampledCount": activityCoverage.sampledCount,
"activeCount": activityCoverage.activeCount,
"sampledFraction": activityCoverage.sampledFraction,
"activeFraction": activityCoverage.activeFraction,
],
"recordedScheduleAudit": [
"recordedAdvanceCount": recordedScheduleAudit.recordedAdvanceCount,
"evaluatedCount": recordedScheduleAudit.evaluatedCount,
"acceptedEvaluationCount": recordedScheduleAudit.acceptedEvaluationCount,
"rejectedEvaluationCount": recordedScheduleAudit.rejectedEvaluationCount,
"payloadMismatchCount": recordedScheduleAudit.payloadMismatchCount,
"decisionMismatchCount": recordedScheduleAudit.decisionMismatchCount,
"menuLinkMismatchCount": recordedScheduleAudit.menuLinkMismatchCount,
"ambiguousComparisonCount": recordedScheduleAudit.ambiguousComparisonCount,
"isValid": recordedScheduleAudit.isValid,
],
"segmentation": segmentation,
]
guard let data = try? JSONSerialization.data(
withJSONObject: payload,
options: [.prettyPrinted, .sortedKeys]),
let text = String(data: data, encoding: .utf8)
else {
return "{}"
}
return text
}
private static let helpText = """
Usage: AdaptiveReplayCLI <trace.jsonl> [--policy <name>]... [--gap-grace <seconds>] [--raw-wall-clock] [--json]
Replays a JSONL adaptive-refresh trace against one or more refresh-timing policies and prints
per-policy metrics over automatically segmented observed time. Simulated advances are
counterfactual policy events; recorded live schedule evaluations are audited separately.
Policies:
adaptive The shared production adaptive policy. Advances on menu-open interactions,
same as UsageStore.noteMenuOpened(at:).
adaptive-activity Experimental adaptive table capped at 5m after observed coding activity.
fixed-2m Fixed 2 minute cadence. Unaffected by menu-open interactions.
fixed-5m Fixed 5 minute cadence.
fixed-15m Fixed 15 minute cadence.
fixed-30m Fixed 30 minute cadence.
manual Never refreshes (degenerate floor).
Defaults to comparing all seven policies when --policy is omitted.
Options:
--policy <name> Restrict to one listed policy; repeat to compare a specific subset.
--gap-grace <s> Split legacy gaps this many seconds after the last timer deadline (default 300).
--raw-wall-clock Disable gap segmentation; useful only for auditing the old behavior.
--json Print a machine-readable report including replay, activity, and audit data.
-h, --help Print this help text.
"""
}
AdaptiveReplayCLI.main()
@@ -0,0 +1,50 @@
import Foundation
/// Informational summary of optional coding-activity observations in a trace's `decision` events.
/// It reports how many decisions carried activity data and how many sampled decisions were below
/// `activeThresholdSeconds` for either CLI.
public struct ActivityCoverageStats: Sendable, Equatable {
public let decisionCount: Int
public let sampledCount: Int
public let activeCount: Int
public init(decisionCount: Int, sampledCount: Int, activeCount: Int) {
self.decisionCount = decisionCount
self.sampledCount = sampledCount
self.activeCount = activeCount
}
/// Fraction of `decision` events that carried at least one non-nil activity field.
public var sampledFraction: Double {
self.decisionCount == 0 ? 0 : Double(self.sampledCount) / Double(self.decisionCount)
}
/// Fraction of the *sampled* decisions (not all decisions) that looked like active coding.
public var activeFraction: Double {
self.sampledCount == 0 ? 0 : Double(self.activeCount) / Double(self.sampledCount)
}
/// - Parameter activeThresholdSeconds: below this many seconds since the newest transcript
/// write, a CLI counts as "active coding at decision time". Defaults to 5 minutes.
public static func compute(
from records: [AdaptiveRefreshTraceRecord],
activeThresholdSeconds: TimeInterval = 300) -> Self
{
var sampledCount = 0
var activeCount = 0
var decisionCount = 0
for record in records where record.kind == .decision {
decisionCount += 1
let codexSeconds = record.codexActivitySeconds
let claudeSeconds = record.claudeActivitySeconds
guard codexSeconds != nil || claudeSeconds != nil else { continue }
sampledCount += 1
let isActive = (codexSeconds ?? .infinity) < activeThresholdSeconds
|| (claudeSeconds ?? .infinity) < activeThresholdSeconds
if isActive {
activeCount += 1
}
}
return Self(decisionCount: decisionCount, sampledCount: sampledCount, activeCount: activeCount)
}
}
@@ -0,0 +1,201 @@
import Foundation
/// The event kinds a trace records. `decision` events capture a full policy tick (the
/// signals it saw plus what it chose). `menuOpen` and `refreshCompleted` capture the two
/// ground-truth events the replay engine anchors a simulation to, independent of any candidate
/// policy. `timerAdvanced` captures the one place live behavior *isn't* a plain tick loop: when
/// opening the menu makes `UsageStore.noteMenuOpened(at:)` pull the next adaptive refresh forward
/// (see `UsageStore.shouldAdvanceAdaptiveTimer(scheduledAt:candidate:)`). Recording it separately
/// from `decision` lets a trace answer "did an advance happen, and to when" without relying on
/// fragile inference from decision-timestamp gaps.
public enum AdaptiveRefreshTraceEventKind: String, Sendable, Codable {
case decision
case menuOpen
case refreshCompleted
case timerAdvanced
/// Every live advance comparison, including the cases correctly rejected because the current
/// timer was already earlier. This is distinct from counterfactual replay advances.
case timerAdvanceEvaluated
}
/// One line of a JSONL adaptive-refresh trace. Field presence depends on `kind`: `decision`
/// records populate `menuAgeSeconds`, `lowPowerModeEnabled`, `thermalState`, `reason`, and
/// `delaySeconds`, plus optional activity observations supplied by the input trace
/// (`codexActivitySeconds`/`claudeActivitySeconds`, the seconds-since-newest-transcript fields,
/// and the per-file intensity fields alongside them); timer advance records populate
/// `previousScheduledAt`, `candidateScheduledAt`, `reason`, and `delaySeconds`; `menuOpen` and
/// `refreshCompleted` carry only `kind` and `timestamp`.
public struct AdaptiveRefreshTraceRecord: Sendable, Codable, Equatable {
public let kind: AdaptiveRefreshTraceEventKind
public let timestamp: Date
public let menuAgeSeconds: TimeInterval?
public let lowPowerModeEnabled: Bool?
public let thermalState: ReplayThermalState?
public let reason: String?
public let delaySeconds: TimeInterval?
/// Timer advance records only: the adaptive timer's scheduled refresh time before the
/// comparison, or `nil` when no refresh had been scheduled yet (matches
/// `UsageStore.shouldAdvanceAdaptiveTimer`'s "always advance when nothing is scheduled" rule).
public let previousScheduledAt: Date?
/// Timer advance records only: the candidate refresh time, i.e. the menu-open timestamp plus
/// the freshly computed decision's delay.
public let candidateScheduledAt: Date?
/// `timerAdvanceEvaluated` only: whether the live schedule comparison accepted the candidate.
public let timerAdvanceAccepted: Bool?
/// `timerAdvanceEvaluated` only: `previousScheduledAt - candidateScheduledAt`, captured before
/// whole-second ISO-8601 serialization. Positive means the candidate was earlier. Optional for
/// compatibility with traces recorded before exact comparison deltas were added.
public let scheduleLeadSeconds: TimeInterval?
/// `timerAdvanceEvaluated` only: whether another refresh was in flight at comparison time.
public let refreshInFlight: Bool?
/// `decision` only: seconds since the newest observed Codex session transcript modification,
/// or `nil` when unavailable. Optional so old trace lines without this field keep decoding.
public let codexActivitySeconds: TimeInterval?
/// `decision` only: the Claude Code counterpart of `codexActivitySeconds`.
public let claudeActivitySeconds: TimeInterval?
/// `decision` only: how long the newest Codex transcript has been
/// growing (its mtime minus its creationDate), or `nil` when unavailable. Not a separate
/// session-age field age is `codexActivitySeconds` + `codexSessionDurationSeconds`.
public let codexSessionDurationSeconds: TimeInterval?
/// `decision` only: the Claude Code counterpart of `codexSessionDurationSeconds`.
public let claudeSessionDurationSeconds: TimeInterval?
/// `decision` only: size in bytes of the newest Codex transcript, as a stateless raw value.
public let codexTranscriptBytes: Int64?
/// `decision` only: the Claude Code counterpart of `codexTranscriptBytes`.
public let claudeTranscriptBytes: Int64?
/// `decision` only: count of Codex `.jsonl` transcripts modified in the observation window.
public let codexActiveTranscriptCount: Int?
/// `decision` only: the Claude Code counterpart of `codexActiveTranscriptCount`.
public let claudeActiveTranscriptCount: Int?
public init(
kind: AdaptiveRefreshTraceEventKind,
timestamp: Date,
menuAgeSeconds: TimeInterval? = nil,
lowPowerModeEnabled: Bool? = nil,
thermalState: ReplayThermalState? = nil,
reason: String? = nil,
delaySeconds: TimeInterval? = nil,
previousScheduledAt: Date? = nil,
candidateScheduledAt: Date? = nil,
timerAdvanceAccepted: Bool? = nil,
scheduleLeadSeconds: TimeInterval? = nil,
refreshInFlight: Bool? = nil,
codexActivitySeconds: TimeInterval? = nil,
claudeActivitySeconds: TimeInterval? = nil,
codexSessionDurationSeconds: TimeInterval? = nil,
claudeSessionDurationSeconds: TimeInterval? = nil,
codexTranscriptBytes: Int64? = nil,
claudeTranscriptBytes: Int64? = nil,
codexActiveTranscriptCount: Int? = nil,
claudeActiveTranscriptCount: Int? = nil)
{
self.kind = kind
self.timestamp = timestamp
self.menuAgeSeconds = menuAgeSeconds
self.lowPowerModeEnabled = lowPowerModeEnabled
self.thermalState = thermalState
self.reason = reason
self.delaySeconds = delaySeconds
self.previousScheduledAt = previousScheduledAt
self.candidateScheduledAt = candidateScheduledAt
self.timerAdvanceAccepted = timerAdvanceAccepted
self.scheduleLeadSeconds = scheduleLeadSeconds
self.refreshInFlight = refreshInFlight
self.codexActivitySeconds = codexActivitySeconds
self.claudeActivitySeconds = claudeActivitySeconds
self.codexSessionDurationSeconds = codexSessionDurationSeconds
self.claudeSessionDurationSeconds = claudeSessionDurationSeconds
self.codexTranscriptBytes = codexTranscriptBytes
self.claudeTranscriptBytes = claudeTranscriptBytes
self.codexActiveTranscriptCount = codexActiveTranscriptCount
self.claudeActiveTranscriptCount = claudeActiveTranscriptCount
}
// swiftlint:disable:next function_parameter_count
public static func decision(
timestamp: Date,
menuAgeSeconds: TimeInterval?,
lowPowerModeEnabled: Bool,
thermalState: ReplayThermalState,
reason: String,
delaySeconds: TimeInterval,
codexActivitySeconds: TimeInterval? = nil,
claudeActivitySeconds: TimeInterval? = nil,
codexSessionDurationSeconds: TimeInterval? = nil,
claudeSessionDurationSeconds: TimeInterval? = nil,
codexTranscriptBytes: Int64? = nil,
claudeTranscriptBytes: Int64? = nil,
codexActiveTranscriptCount: Int? = nil,
claudeActiveTranscriptCount: Int? = nil) -> Self
{
Self(
kind: .decision,
timestamp: timestamp,
menuAgeSeconds: menuAgeSeconds,
lowPowerModeEnabled: lowPowerModeEnabled,
thermalState: thermalState,
reason: reason,
delaySeconds: delaySeconds,
codexActivitySeconds: codexActivitySeconds,
claudeActivitySeconds: claudeActivitySeconds,
codexSessionDurationSeconds: codexSessionDurationSeconds,
claudeSessionDurationSeconds: claudeSessionDurationSeconds,
codexTranscriptBytes: codexTranscriptBytes,
claudeTranscriptBytes: claudeTranscriptBytes,
codexActiveTranscriptCount: codexActiveTranscriptCount,
claudeActiveTranscriptCount: claudeActiveTranscriptCount)
}
public static func menuOpen(timestamp: Date) -> Self {
Self(kind: .menuOpen, timestamp: timestamp)
}
public static func refreshCompleted(timestamp: Date) -> Self {
Self(kind: .refreshCompleted, timestamp: timestamp)
}
/// - Parameters:
/// - timestamp: When the menu open that triggered the advance occurred.
/// - previousScheduledAt: The timer's scheduled refresh time immediately before the advance.
/// - candidateScheduledAt: The refresh time the timer advanced to (`timestamp + delaySeconds`).
/// - reason: The freshly computed decision's reason (e.g. `"recentInteraction"`).
/// - delaySeconds: The freshly computed decision's delay.
public static func timerAdvanced(
timestamp: Date,
previousScheduledAt: Date?,
candidateScheduledAt: Date,
reason: String,
delaySeconds: TimeInterval) -> Self
{
Self(
kind: .timerAdvanced,
timestamp: timestamp,
reason: reason,
delaySeconds: delaySeconds,
previousScheduledAt: previousScheduledAt,
candidateScheduledAt: candidateScheduledAt)
}
// swiftlint:disable:next function_parameter_count
public static func timerAdvanceEvaluated(
timestamp: Date,
previousScheduledAt: Date?,
candidateScheduledAt: Date,
reason: String,
delaySeconds: TimeInterval,
accepted: Bool,
refreshInFlight: Bool) -> Self
{
Self(
kind: .timerAdvanceEvaluated,
timestamp: timestamp,
reason: reason,
delaySeconds: delaySeconds,
previousScheduledAt: previousScheduledAt,
candidateScheduledAt: candidateScheduledAt,
timerAdvanceAccepted: accepted,
scheduleLeadSeconds: previousScheduledAt.map { $0.timeIntervalSince(candidateScheduledAt) },
refreshInFlight: refreshInFlight)
}
}
@@ -0,0 +1,85 @@
import Foundation
/// A malformed trace line, with enough context to find and fix it.
public struct AdaptiveRefreshTraceParseError: Error, Sendable, Equatable, CustomStringConvertible {
public let lineNumber: Int
public let content: String
public let underlyingDescription: String
public init(lineNumber: Int, content: String, underlyingDescription: String) {
self.lineNumber = lineNumber
self.content = content
self.underlyingDescription = underlyingDescription
}
public var description: String {
"trace line \(self.lineNumber) is malformed: \(self.underlyingDescription) (content: \(self.content))"
}
}
/// Parses newline-delimited JSON adaptive-refresh traces.
///
/// Deliberate choice: a malformed line **fails the whole parse** rather than being silently
/// skipped. A trace is acceptance evidence if a line is corrupt (truncated write, disk-full
/// mid-append, hand-edited fixture with a typo), the honest answer is "this trace is untrustworthy
/// as a whole", not "here are metrics computed from however much of it happened to parse". A
/// silently-shortened trace would still produce a superficially plausible replay report, which is
/// worse than a loud failure: it hides exactly the kind of gap that would bias staleness/refresh
/// counts. Callers that genuinely want best-effort parsing can catch the error and fall back to
/// `AdaptiveRefreshTraceParser.parseTolerantly`, which skips bad lines and returns what parsed.
public enum AdaptiveRefreshTraceParser {
public static func parse(_ text: String) throws -> [AdaptiveRefreshTraceRecord] {
let decoder = Self.makeDecoder()
var records: [AdaptiveRefreshTraceRecord] = []
for (index, line) in text.split(
omittingEmptySubsequences: false,
whereSeparator: \.isNewline).enumerated()
{
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { continue }
guard let data = trimmed.data(using: .utf8) else {
throw AdaptiveRefreshTraceParseError(
lineNumber: index + 1,
content: trimmed,
underlyingDescription: "not valid UTF-8")
}
do {
try records.append(decoder.decode(AdaptiveRefreshTraceRecord.self, from: data))
} catch {
throw AdaptiveRefreshTraceParseError(
lineNumber: index + 1,
content: trimmed,
underlyingDescription: String(describing: error))
}
}
return records
}
public static func parse(contentsOf url: URL) throws -> [AdaptiveRefreshTraceRecord] {
let text = try String(contentsOf: url, encoding: .utf8)
return try self.parse(text)
}
/// Best-effort variant: skips lines that fail to parse instead of throwing. Not the default
/// see the type-level documentation for why silent skipping is the wrong default for
/// acceptance-evidence traces. Exists for callers (future exploratory tooling) that explicitly
/// want partial data over none.
public static func parseTolerantly(_ text: String) -> [AdaptiveRefreshTraceRecord] {
let decoder = Self.makeDecoder()
var records: [AdaptiveRefreshTraceRecord] = []
for line in text.split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) {
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let data = trimmed.data(using: .utf8) else { continue }
if let record = try? decoder.decode(AdaptiveRefreshTraceRecord.self, from: data) {
records.append(record)
}
}
return records
}
private static func makeDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}
}
@@ -0,0 +1,57 @@
import AdaptiveRefreshCore
import Foundation
/// Replay adapter for the same canonical policy core used by the CodexBar app.
public struct AdaptiveReplayPolicy: ReplayPolicy, Sendable {
public let name = "adaptive"
/// Matches `UsageStore.noteMenuOpened(at:)`'s adaptive-only advance guard: this is the one
/// baseline that actually models the interaction-advance path, so it is the only one that
/// overrides the protocol's `false` default.
public let advancesOnInteraction = true
public init() {}
public func decide(_ input: ReplayPolicyInput) -> ReplayPolicyDecision {
let decision = AdaptiveRefreshPolicyCore().nextDelay(for: AdaptiveRefreshPolicyCore.Input(
now: input.now,
lastMenuOpenAt: input.lastMenuOpenAt,
lowPowerModeEnabled: input.lowPowerModeEnabled,
thermalPressure: input.thermalState.isConstrained ? .constrained : .nominal))
return ReplayPolicyDecision(
delaySeconds: TimeInterval(decision.delay.components.seconds),
reason: decision.reason.rawValue)
}
}
/// A fixed-cadence baseline: always waits the same interval, regardless of signals. Used to
/// compare the adaptive policy against the flat refresh frequencies CodexBar also offers
/// (2/5/15/30 minutes). Never advances on interaction (`advancesOnInteraction` stays the protocol
/// default of `false`), matching the real app: fixed-cadence refresh frequencies never wire up
/// `noteMenuOpened`'s advance check.
public struct FixedIntervalPolicy: ReplayPolicy, Sendable {
public let name: String
private let intervalSeconds: TimeInterval
public init(minutes: Int) {
self.name = "fixed-\(minutes)m"
self.intervalSeconds = TimeInterval(minutes) * 60
}
public func decide(_: ReplayPolicyInput) -> ReplayPolicyDecision {
ReplayPolicyDecision(delaySeconds: self.intervalSeconds, reason: "fixed")
}
}
/// The degenerate floor: never schedules a refresh. A trace replayed against this policy always
/// reports zero refreshes, which is the point it establishes the worst-case staleness bound the
/// other policies are compared against.
public struct ManualPolicy: ReplayPolicy, Sendable {
public let name = "manual"
public init() {}
public func decide(_: ReplayPolicyInput) -> ReplayPolicyDecision {
ReplayPolicyDecision(delaySeconds: nil, reason: "manual")
}
}
@@ -0,0 +1,27 @@
import Foundation
/// Replay-only candidate used to test whether stat-only coding activity can close the accepted
/// "active work is never slower than five minutes" gap. It is not a production policy approval.
public struct CodingActivityAdaptivePolicy: ReplayPolicy, Sendable {
public let name = "adaptive-activity"
public let advancesOnInteraction = true
private let base = AdaptiveReplayPolicy()
private static let activeThreshold: TimeInterval = 5 * 60
private static let activeDelayCap: TimeInterval = 5 * 60
public init() {}
public func decide(_ input: ReplayPolicyInput) -> ReplayPolicyDecision {
let baseDecision = self.base.decide(input)
guard !input.isConstrained,
let activityAge = input.codingActivityAgeSeconds,
activityAge < Self.activeThreshold,
let baseDelay = baseDecision.delaySeconds,
baseDelay > Self.activeDelayCap
else {
return baseDecision
}
return ReplayPolicyDecision(delaySeconds: Self.activeDelayCap, reason: "codingActivity")
}
}
+39
View File
@@ -0,0 +1,39 @@
# AdaptiveReplayKit
`AdaptiveReplayKit` is an offline harness for comparing refresh-timing policies against an
explicit JSONL trace. `AdaptiveReplayCLI` is the command-line wrapper around the library.
## Scope
The replay targets do not import `CodexBar` or `CodexBarCore`; they share only the package-internal,
Foundation-only `AdaptiveRefreshCore` target with the app. They do not record app behavior, scan
Codex or Claude transcript directories, write trace files, call providers, or change the production
refresh policy. Trace capture and lifecycle management are deliberately outside this PR; callers
provide an existing trace path to the CLI.
Optional activity fields in the trace schema are inputs only. The replay kit never discovers or
collects them. Old records without those fields continue to decode.
## Components
- `AdaptiveRefreshTrace.swift` defines the version-tolerant trace schema.
- `AdaptiveRefreshTraceParser.swift` parses JSONL strictly by default. The tolerant entry point is
available for exploratory work that explicitly accepts skipped malformed records.
- `AdaptiveRefreshCore` owns the production decision table. `ReplayPolicy.swift`,
`BaselinePolicies.swift`, and `CandidatePolicies.swift` provide replay adapters, fixed/manual
baselines, and the replay-only activity candidate.
- `ReplayEngine.swift` and `ReplayMetrics.swift` calculate simulated refresh cadence, menu-open
staleness, interaction advances, and constrained-state compliance.
- `ReplayTraceSegmentation.swift` excludes legacy deadline-overrun gaps with an explicit heuristic
and reports the excluded duration.
- `RecordedScheduleAudit.swift` audits recorded timer-advance events independently from the replay
clock.
- `Sources/AdaptiveReplayCLI` formats table or JSON reports.
`interactionAdvanceCount` is counterfactual. Replay assumes a zero-duration refresh, while the
live app waits for provider work and may already have a refresh in flight. Recorded schedule events
therefore have a separate audit instead of a direct count comparison.
The legacy gap heuristic cannot distinguish sleep or reboot from a long refresh or event-loop
stall. Reports expose the segment count, grace interval, and excluded time rather than assigning a
cause.
@@ -0,0 +1,153 @@
import Foundation
public struct RecordedScheduleAudit: Sendable, Equatable {
public let recordedAdvanceCount: Int
public let evaluatedCount: Int
public let acceptedEvaluationCount: Int
public let rejectedEvaluationCount: Int
public let payloadMismatchCount: Int
public let decisionMismatchCount: Int
public let menuLinkMismatchCount: Int
public let ambiguousComparisonCount: Int
public var isValid: Bool {
self.payloadMismatchCount == 0
&& self.decisionMismatchCount == 0
&& self.menuLinkMismatchCount == 0
&& self.ambiguousComparisonCount == 0
}
}
/// Audits the live schedule records without equating them to ReplayEngine's counterfactual clock.
public enum RecordedScheduleAuditor {
public static func audit(
_ records: [AdaptiveRefreshTraceRecord],
timestampTolerance: TimeInterval = 1) -> RecordedScheduleAudit
{
let sorted = records.sorted { $0.timestamp < $1.timestamp }
let menuTimestamps = sorted.filter { $0.kind == .menuOpen }.map(\.timestamp)
let advances = sorted.filter { $0.kind == .timerAdvanced }
let evaluations = sorted.filter { $0.kind == .timerAdvanceEvaluated }
var payloadMismatchCount = advances.count(where: { !Self.payloadIsValid($0) })
let evaluationOutcomes = evaluations.map(Self.evaluationOutcome)
let decisionMismatchCount = evaluationOutcomes.count(where: { $0 == .mismatch })
let ambiguousComparisonCount = evaluationOutcomes.count(where: { $0 == .ambiguous })
// Every evaluation is caused by one menu open. Before evaluation records existed, an
// accepted advance was the only causal record, so retain those legacy advances as linkage
// events. Modern accepted advances are reconciled against evaluations below instead of
// consuming the same menu open twice.
let legacyAdvances = evaluations.first.map { firstEvaluation in
advances.filter { $0.timestamp < firstEvaluation.timestamp }
} ?? advances
let menuLinkMismatchCount = Self.unmatchedEventCount(
evaluations + legacyAdvances,
menuTimestamps: menuTimestamps,
timestampTolerance: timestampTolerance)
if let firstEvaluationAt = evaluations.first?.timestamp {
let accepted = evaluations.filter { $0.timerAdvanceAccepted == true }
let auditableAdvances = advances.filter { $0.timestamp >= firstEvaluationAt }
payloadMismatchCount += Self.scheduleMultiplicityDifference(accepted, auditableAdvances)
}
return RecordedScheduleAudit(
recordedAdvanceCount: advances.count,
evaluatedCount: evaluations.count,
acceptedEvaluationCount: evaluations.count(where: { $0.timerAdvanceAccepted == true }),
rejectedEvaluationCount: evaluations.count(where: { $0.timerAdvanceAccepted == false }),
payloadMismatchCount: payloadMismatchCount,
decisionMismatchCount: decisionMismatchCount,
menuLinkMismatchCount: menuLinkMismatchCount,
ambiguousComparisonCount: ambiguousComparisonCount)
}
private static func payloadIsValid(_ record: AdaptiveRefreshTraceRecord) -> Bool {
guard let candidate = record.candidateScheduledAt,
let delay = record.delaySeconds,
abs(candidate.timeIntervalSince(record.timestamp) - delay) < 0.001
else { return false }
// Whole-second legacy timestamps can collapse a sub-second accepted lead to equality.
return record.previousScheduledAt.map { candidate <= $0 } ?? true
}
private enum EvaluationOutcome: Equatable {
case valid
case mismatch
case ambiguous
}
private static func evaluationOutcome(_ record: AdaptiveRefreshTraceRecord) -> EvaluationOutcome {
guard let accepted = record.timerAdvanceAccepted,
let candidate = record.candidateScheduledAt,
let delay = record.delaySeconds,
abs(candidate.timeIntervalSince(record.timestamp) - delay) < 0.001
else { return .mismatch }
guard let previous = record.previousScheduledAt else { return accepted ? .valid : .mismatch }
if candidate != previous {
return accepted == (candidate < previous) ? .valid : .mismatch
}
guard let lead = record.scheduleLeadSeconds else { return .ambiguous }
return accepted == (lead > 0) ? .valid : .mismatch
}
private struct ScheduleKey: Hashable {
let timestamp: Date
let previousScheduledAt: Date?
let candidateScheduledAt: Date?
let reason: String?
let delaySeconds: TimeInterval?
}
private static func scheduleMultiplicityDifference(
_ lhs: [AdaptiveRefreshTraceRecord],
_ rhs: [AdaptiveRefreshTraceRecord]) -> Int
{
func counts(_ records: [AdaptiveRefreshTraceRecord]) -> [ScheduleKey: Int] {
Dictionary(grouping: records, by: scheduleKey).mapValues(\.count)
}
let lhsCounts = counts(lhs)
let rhsCounts = counts(rhs)
return Set(lhsCounts.keys).union(rhsCounts.keys).reduce(0) { difference, key in
difference + abs(lhsCounts[key, default: 0] - rhsCounts[key, default: 0])
}
}
/// Maximum one-to-one matching for sorted points with a symmetric tolerance window. Extra
/// menu opens are valid because fixed/manual modes do not emit schedule evaluations; only an
/// event without its own causal menu open is a mismatch.
private static func unmatchedEventCount(
_ records: [AdaptiveRefreshTraceRecord],
menuTimestamps: [Date],
timestampTolerance: TimeInterval) -> Int
{
let eventTimestamps = records.map(\.timestamp).sorted()
var eventIndex = 0
var menuIndex = 0
var unmatched = 0
while eventIndex < eventTimestamps.count, menuIndex < menuTimestamps.count {
let eventTimestamp = eventTimestamps[eventIndex]
let menuTimestamp = menuTimestamps[menuIndex]
if menuTimestamp < eventTimestamp.addingTimeInterval(-timestampTolerance) {
menuIndex += 1
} else if menuTimestamp > eventTimestamp.addingTimeInterval(timestampTolerance) {
unmatched += 1
eventIndex += 1
} else {
eventIndex += 1
menuIndex += 1
}
}
return unmatched + eventTimestamps.count - eventIndex
}
private static func scheduleKey(_ record: AdaptiveRefreshTraceRecord) -> ScheduleKey {
ScheduleKey(
timestamp: record.timestamp,
previousScheduledAt: record.previousScheduledAt,
candidateScheduledAt: record.candidateScheduledAt,
reason: record.reason,
delaySeconds: record.delaySeconds)
}
}
@@ -0,0 +1,303 @@
import Foundation
/// Simulates the live timer loop (`decide` sleep refresh `decide` ...) over a trace's
/// observed span for a given `ReplayPolicy`, pure and deterministic: the same trace and policy
/// always produce the same `ReplayMetrics`, since every input the policy sees comes from the
/// trace, never from a live clock.
///
/// Ground truth vs. reconstructed signal: `menuOpen` events are ground truth a menu either
/// opened at a timestamp or it didn't, independent of any policy. `lowPowerModeEnabled` and
/// `thermalState`, by contrast, are only *sampled* at the timestamps the trace's original
/// `decision` events happened to occur at (whatever policy produced the trace). When a candidate
/// policy's own tick times fall between those samples, the engine holds the most recent known
/// value (step function). This is the phase-1 approximation: without a continuous power/thermal
/// signal in the trace, "most recent sample" is the best available reconstruction. Before the
/// first known sample, the earliest available sample is used (hold-first).
///
/// Interaction advances: this is a *counterfactual* replay, not a literal replay of whatever the
/// recording policy happened to do each candidate policy gets its own tick schedule computed
/// fresh from `policy.decide(_:)`. To reproduce `UsageStore.noteMenuOpened(at:)`'s "pull the timer
/// forward" behavior (see `UsageStore.shouldAdvanceAdaptiveTimer(scheduledAt:candidate:)`) for
/// *any* candidate policy, every `menuOpen` event that falls inside a policy's current tick window
/// is independently re-evaluated: if `policy.advancesOnInteraction` and the decision computed as of
/// that menu open would land earlier than the already-scheduled next tick, the schedule advances to
/// that earlier time, exactly like `startTimer(preservingResetBoundaryRefresh: true)` replacing a
/// pending sleep with a shorter one. Recorded `timerAdvanced` events are audited separately: their
/// count is not expected to equal
/// this counterfactual schedule because live refresh work has non-zero duration and can coalesce.
public enum ReplayEngine {
/// Safety valve against a pathological policy (e.g. a zero-or-negative delay bug) turning a
/// long trace into an unbounded loop.
private static let maxIterations = 2_000_000
/// The trace-derived, replay-invariant inputs the simulation loop reads on every tick:
/// menu-open ground truth plus the sampled power/thermal signal, both precomputed and sorted
/// once per `run` so the per-tick lookups stay O(log n).
private struct TraceSignals {
let menuOpenTimestamps: [Date]
let signalSamples: [(timestamp: Date, lowPower: Bool, thermal: ReplayThermalState)]
let signalTimestamps: [Date]
let activitySamples: [ActivityObservation]
let activityTimestamps: [Date]
}
private struct ActivityObservation {
let timestamp: Date
let lastCodingActivityAt: Date?
}
public static func run(trace: [AdaptiveRefreshTraceRecord], policy: some ReplayPolicy) -> ReplayMetrics {
self.runDetailed(trace: trace, policy: policy).metrics
}
static func runDetailed(
trace: [AdaptiveRefreshTraceRecord],
policy: some ReplayPolicy,
stalenessStartAt: Date? = nil) -> ReplayRun
{
guard let start = trace.map(\.timestamp).min(), let end = trace.map(\.timestamp).max() else {
return ReplayRun(
metrics: ReplayMetrics(
policyName: policy.name,
simulatedSpanSeconds: 0,
totalRefreshCount: 0,
refreshCountPer24h: 0,
stalenessAtMenuOpen: nil,
constrainedCompliance: ConstrainedCompliance(constrainedDecisionCount: 0, violationCount: 0)),
stalenessSamples: [])
}
let menuOpenTimestamps = trace
.filter { $0.kind == .menuOpen }
.map(\.timestamp)
.sorted()
let signalSamples: [(timestamp: Date, lowPower: Bool, thermal: ReplayThermalState)] = trace
.filter { $0.kind == .decision }
.compactMap { record in
guard let lowPower = record.lowPowerModeEnabled, let thermal = record.thermalState else {
return nil
}
return (timestamp: record.timestamp, lowPower: lowPower, thermal: thermal)
}
.sorted { $0.timestamp < $1.timestamp }
let activitySamples = trace
.filter { $0.kind == .decision }
.map { record in
let activityDates = [record.codexActivitySeconds, record.claudeActivitySeconds]
.compactMap(\.self)
.map { record.timestamp.addingTimeInterval(-max(0, $0)) }
return ActivityObservation(
timestamp: record.timestamp,
lastCodingActivityAt: activityDates.max())
}
.sorted { $0.timestamp < $1.timestamp }
let signals = TraceSignals(
menuOpenTimestamps: menuOpenTimestamps,
signalSamples: signalSamples,
signalTimestamps: signalSamples.map(\.timestamp),
activitySamples: activitySamples,
activityTimestamps: activitySamples.map(\.timestamp))
var cursor = start
var refreshTimestamps: [Date] = []
var constrainedDecisionCount = 0
var violationCount = 0
var interactionAdvanceCount = 0
var codingActiveDecisionCount = 0
var codingActiveDelayViolationCount = 0
var iterations = 0
// Monotonic pointer into `menuOpenTimestamps`: the scan below considers each menu open for
// an advance at most once, in the single tick window (cursor, next] it falls into.
var menuOpenScanIndex = 0
while cursor <= end, iterations < self.maxIterations {
iterations += 1
let (lowPower, thermal) = self.signal(
signals.signalSamples,
timestamps: signals.signalTimestamps,
at: cursor)
let input = ReplayPolicyInput(
now: cursor,
lastMenuOpenAt: self.lastValue(menuOpenTimestamps, atOrBefore: cursor),
lastCodingActivityAt: self.lastActivity(
signals.activitySamples,
timestamps: signals.activityTimestamps,
at: cursor),
lowPowerModeEnabled: lowPower,
thermalState: thermal)
let decision = policy.decide(input)
if input.isConstrained {
constrainedDecisionCount += 1
if let delay = decision.delaySeconds, delay < 1800 {
violationCount += 1
}
}
if !input.isConstrained,
let activityAge = input.codingActivityAgeSeconds,
activityAge < 5 * 60
{
codingActiveDecisionCount += 1
if decision.delaySeconds.map({ $0 <= 0 || $0 > 5 * 60 }) ?? true {
codingActiveDelayViolationCount += 1
}
}
guard let delay = decision.delaySeconds, delay > 0 else { break }
var next = cursor.addingTimeInterval(delay)
if policy.advancesOnInteraction {
let advanced = self.applyInteractionAdvances(
policy: policy,
signals: signals,
scanIndex: &menuOpenScanIndex,
windowStart: cursor,
scheduledAt: next)
next = advanced.scheduledAt
interactionAdvanceCount += advanced.advanceCount
}
guard next <= end else { break }
refreshTimestamps.append(next)
cursor = next
}
let span = end.timeIntervalSince(start)
let refreshCountPer24h = span > 0 ? Double(refreshTimestamps.count) * 86400 / span : 0
let stalenessMenuTimestamps = stalenessStartAt.map { start in
menuOpenTimestamps.filter { $0 >= start }
} ?? menuOpenTimestamps
let stalenessSamples = stalenessMenuTimestamps.isEmpty ? [] : self.stalenessSamples(
menuOpenTimestamps: stalenessMenuTimestamps,
refreshTimestamps: refreshTimestamps,
initialFreshAt: stalenessStartAt ?? start)
return ReplayRun(
metrics: ReplayMetrics(
policyName: policy.name,
simulatedSpanSeconds: span,
totalRefreshCount: refreshTimestamps.count,
refreshCountPer24h: refreshCountPer24h,
stalenessAtMenuOpen: StalenessStats(samples: stalenessSamples),
constrainedCompliance: ConstrainedCompliance(
constrainedDecisionCount: constrainedDecisionCount,
violationCount: violationCount),
interactionAdvanceCount: interactionAdvanceCount,
codingActiveDecisionCount: codingActiveDecisionCount,
codingActiveDelayViolationCount: codingActiveDelayViolationCount),
stalenessSamples: stalenessSamples)
}
/// Re-evaluates every not-yet-scanned menu open that falls in `(windowStart, scheduledAt]`
/// against `policy`, mirroring `UsageStore.shouldAdvanceAdaptiveTimer(scheduledAt:candidate:)`:
/// a menu open at time `T` computes `policy.decide(now: T, lastMenuOpenAt: T, ...)` (age zero,
/// exactly as `noteMenuOpened(at:)` does with `self.lastMenuOpenAt = date` already applied), and
/// if the resulting candidate (`T + delay`) lands earlier than the currently scheduled refresh,
/// the schedule advances to that candidate. Later menu opens in the same window are then
/// compared against the *advanced* schedule, same as a real second interaction tightening an
/// already-shortened sleep. Returns the (possibly advanced) scheduled time plus how many
/// advances were taken in this window.
private static func applyInteractionAdvances(
policy: some ReplayPolicy,
signals: TraceSignals,
scanIndex: inout Int,
windowStart: Date,
scheduledAt: Date) -> (scheduledAt: Date, advanceCount: Int)
{
var next = scheduledAt
var advanceCount = 0
while scanIndex < signals.menuOpenTimestamps.count {
let menuOpenAt = signals.menuOpenTimestamps[scanIndex]
guard menuOpenAt > windowStart else {
scanIndex += 1
continue
}
guard menuOpenAt <= next else { break }
let (lowPower, thermal) = self.signal(
signals.signalSamples,
timestamps: signals.signalTimestamps,
at: menuOpenAt)
let advanceDecision = policy.decide(ReplayPolicyInput(
now: menuOpenAt,
lastMenuOpenAt: menuOpenAt,
lastCodingActivityAt: self.lastActivity(
signals.activitySamples,
timestamps: signals.activityTimestamps,
at: menuOpenAt),
lowPowerModeEnabled: lowPower,
thermalState: thermal))
scanIndex += 1
guard let advanceDelay = advanceDecision.delaySeconds, advanceDelay > 0 else { continue }
let candidate = menuOpenAt.addingTimeInterval(advanceDelay)
if candidate < next {
next = candidate
advanceCount += 1
}
}
return (next, advanceCount)
}
private static func stalenessSamples(
menuOpenTimestamps: [Date],
refreshTimestamps: [Date],
initialFreshAt: Date) -> [Double]
{
menuOpenTimestamps.map { menuOpenAt in
let simulatedRefresh = self.lastValue(refreshTimestamps, atOrBefore: menuOpenAt)
let freshestAt = simulatedRefresh.map { max($0, initialFreshAt) } ?? initialFreshAt
return menuOpenAt.timeIntervalSince(freshestAt)
}
}
private static func lastActivity(
_ samples: [ActivityObservation],
timestamps: [Date],
at time: Date) -> Date?
{
guard let index = self.lastIndex(timestamps, atOrBefore: time) else { return nil }
return samples[index].lastCodingActivityAt
}
/// Binds the most recent power/thermal sample at or before `time` (hold-last), falling back
/// to the earliest known sample when `time` precedes every sample (hold-first), and to
/// nominal/not-low-power when no samples exist at all.
private static func signal(
_ samples: [(timestamp: Date, lowPower: Bool, thermal: ReplayThermalState)],
timestamps: [Date],
at time: Date) -> (Bool, ReplayThermalState)
{
guard !samples.isEmpty else { return (false, .nominal) }
if let index = self.lastIndex(timestamps, atOrBefore: time) {
return (samples[index].lowPower, samples[index].thermal)
}
return (samples[0].lowPower, samples[0].thermal)
}
private static func lastValue(_ timestamps: [Date], atOrBefore time: Date) -> Date? {
guard let index = self.lastIndex(timestamps, atOrBefore: time) else { return nil }
return timestamps[index]
}
/// Binary search for the last index whose timestamp is `<= time`, assuming `timestamps` is
/// sorted ascending. O(log n) so a long trace (thousands of decisions) stays fast to replay.
private static func lastIndex(_ timestamps: [Date], atOrBefore time: Date) -> Int? {
var low = 0
var high = timestamps.count - 1
var result: Int?
while low <= high {
let mid = (low + high) / 2
if timestamps[mid] <= time {
result = mid
low = mid + 1
} else {
high = mid - 1
}
}
return result
}
}
@@ -0,0 +1,108 @@
import Foundation
/// Mean/median/p95 of staleness (seconds since the last simulated refresh) observed at each
/// historical menu-open event. `p95` uses nearest-rank: samples are sorted ascending and index
/// `ceil(0.95 * n) - 1` (clamped to the last index) is reported the same convention most
/// dashboards use for small-to-medium sample counts, and simple enough to hand-verify in tests.
public struct StalenessStats: Sendable, Equatable {
public let mean: Double
public let median: Double
public let p95: Double
public let sampleCount: Int
public init(mean: Double, median: Double, p95: Double, sampleCount: Int) {
self.mean = mean
self.median = median
self.p95 = p95
self.sampleCount = sampleCount
}
init?(samples: [Double]) {
guard !samples.isEmpty else { return nil }
let sorted = samples.sorted()
self.init(
mean: sorted.reduce(0, +) / Double(sorted.count),
median: Self.percentile(sorted, fraction: 0.5),
p95: Self.percentile(sorted, fraction: 0.95),
sampleCount: sorted.count)
}
private static func percentile(_ sorted: [Double], fraction: Double) -> Double {
let rank = Int((fraction * Double(sorted.count)).rounded(.up))
return sorted[max(0, min(sorted.count - 1, rank - 1))]
}
}
/// Whether a policy honored the "never refresh faster than 30 minutes while constrained (low
/// power or serious/critical thermal)" rule at every simulated decision point where the input was
/// constrained.
public struct ConstrainedCompliance: Sendable, Equatable {
public let constrainedDecisionCount: Int
public let violationCount: Int
public init(constrainedDecisionCount: Int, violationCount: Int) {
self.constrainedDecisionCount = constrainedDecisionCount
self.violationCount = violationCount
}
public var isCompliant: Bool {
self.violationCount == 0
}
}
public struct ReplayMetrics: Sendable, Equatable {
public let policyName: String
public let simulatedSpanSeconds: TimeInterval
public let totalRefreshCount: Int
public let refreshCountPer24h: Double
public let stalenessAtMenuOpen: StalenessStats?
public let constrainedCompliance: ConstrainedCompliance
/// How many of `totalRefreshCount` were pulled forward by a menu-open interaction rather than
/// firing on the policy's own previously scheduled cadence i.e. how many times
/// `ReplayEngine.run` took the `advancesOnInteraction` branch for this policy. Always `0` for
/// policies that report `advancesOnInteraction == false` (see `ReplayPolicy`).
public let interactionAdvanceCount: Int
/// Unconstrained replayed decisions with a known transcript-write observation under five minutes old.
public let codingActiveDecisionCount: Int
/// Unconstrained active decisions whose selected delay exceeded the five-minute acceptance cap.
public let codingActiveDelayViolationCount: Int
/// Number of independently simulated awake/run segments contributing to these metrics.
public let segmentCount: Int
/// Wall-clock time excluded after an expected timer deadline because the app was unobserved.
public let excludedGapSeconds: TimeInterval
/// Menu opens before a segment's first recorded refresh, excluded equally for every policy.
public let boundaryCensoredMenuOpenCount: Int
public init(
policyName: String,
simulatedSpanSeconds: TimeInterval,
totalRefreshCount: Int,
refreshCountPer24h: Double,
stalenessAtMenuOpen: StalenessStats?,
constrainedCompliance: ConstrainedCompliance,
interactionAdvanceCount: Int = 0,
codingActiveDecisionCount: Int = 0,
codingActiveDelayViolationCount: Int = 0,
segmentCount: Int = 1,
excludedGapSeconds: TimeInterval = 0,
boundaryCensoredMenuOpenCount: Int = 0)
{
self.policyName = policyName
self.simulatedSpanSeconds = simulatedSpanSeconds
self.totalRefreshCount = totalRefreshCount
self.refreshCountPer24h = refreshCountPer24h
self.stalenessAtMenuOpen = stalenessAtMenuOpen
self.constrainedCompliance = constrainedCompliance
self.interactionAdvanceCount = interactionAdvanceCount
self.codingActiveDecisionCount = codingActiveDecisionCount
self.codingActiveDelayViolationCount = codingActiveDelayViolationCount
self.segmentCount = segmentCount
self.excludedGapSeconds = excludedGapSeconds
self.boundaryCensoredMenuOpenCount = boundaryCensoredMenuOpenCount
}
}
struct ReplayRun: Sendable {
let metrics: ReplayMetrics
let stalenessSamples: [Double]
}
@@ -0,0 +1,90 @@
import Foundation
// Replay harness for the adaptive refresh policy shipped in the `CodexBar` app target. The app
// and replay adapter both call `AdaptiveRefreshPolicyCore`; these types only normalize replay
// inputs and report replay-friendly output.
/// Coarse thermal-pressure signal matching the two `ProcessInfo.ThermalState` cases the policy
/// distinguishes (`.serious`/`.critical` vs everything else), expressed independently so this
/// library never needs Darwin-only APIs and can build on any platform.
public enum ReplayThermalState: String, Sendable, Codable, CaseIterable {
case nominal
case fair
case serious
case critical
public var isConstrained: Bool {
self == .serious || self == .critical
}
}
/// The inputs a refresh-timing policy needs to decide how long to wait before the next refresh.
/// Replay-specific policy input. Platform-independent fields map into the shared policy core.
public struct ReplayPolicyInput: Sendable, Equatable {
public let now: Date
public let lastMenuOpenAt: Date?
/// Most recent transcript write reconstructed from the latest activity observation available
/// at or before `now`. This is nil when that observation could not see either CLI.
public let lastCodingActivityAt: Date?
public let lowPowerModeEnabled: Bool
public let thermalState: ReplayThermalState
public init(
now: Date,
lastMenuOpenAt: Date?,
lastCodingActivityAt: Date? = nil,
lowPowerModeEnabled: Bool,
thermalState: ReplayThermalState)
{
self.now = now
self.lastMenuOpenAt = lastMenuOpenAt
self.lastCodingActivityAt = lastCodingActivityAt
self.lowPowerModeEnabled = lowPowerModeEnabled
self.thermalState = thermalState
}
/// Whether this input represents a power/thermal-constrained moment, independent of which
/// policy is deciding. Used by the replay engine to score constrained-tier compliance without
/// depending on any single policy's own notion of "constrained".
public var isConstrained: Bool {
self.lowPowerModeEnabled || self.thermalState.isConstrained
}
public var codingActivityAgeSeconds: TimeInterval? {
self.lastCodingActivityAt.map { max(0, self.now.timeIntervalSince($0)) }
}
}
/// A policy's decision: how long to wait, and a short human-readable reason code for reporting.
/// `delaySeconds == nil` means "never schedule another refresh" the degenerate floor used by
/// `ManualPolicy`.
public struct ReplayPolicyDecision: Sendable, Equatable {
public let delaySeconds: TimeInterval?
public let reason: String
public init(delaySeconds: TimeInterval?, reason: String) {
self.delaySeconds = delaySeconds
self.reason = reason
}
}
/// A pure, deterministic function from `ReplayPolicyInput` to `ReplayPolicyDecision`.
public protocol ReplayPolicy: Sendable {
var name: String { get }
/// Whether opening the menu can pull this policy's next refresh forward, mirroring
/// `UsageStore.noteMenuOpened(at:)`'s guard on `settings.refreshFrequency == .adaptive`: in the
/// real app, only adaptive mode ever advances the timer from an interaction fixed-cadence and
/// manual modes just record `lastMenuOpenAt` and let the existing schedule run. Defaults to
/// `false` so baseline policies (`FixedIntervalPolicy`, `ManualPolicy`) need no override; only
/// policies that actually model the adaptive table set this to `true`.
var advancesOnInteraction: Bool { get }
func decide(_ input: ReplayPolicyInput) -> ReplayPolicyDecision
}
extension ReplayPolicy {
public var advancesOnInteraction: Bool {
false
}
}
@@ -0,0 +1,123 @@
import Foundation
public struct ReplayTraceSegment: Sendable, Equatable {
public let records: [AdaptiveRefreshTraceRecord]
public let start: Date
public let end: Date
var replayRecords: [AdaptiveRefreshTraceRecord] {
guard self.records.last?.timestamp != self.end else { return self.records }
return self.records + [.refreshCompleted(timestamp: self.end)]
}
}
public struct ReplaySegmentationReport: Sendable, Equatable {
public let segments: [ReplayTraceSegment]
public let excludedGapSeconds: TimeInterval
public let breakCount: Int
public let graceSeconds: TimeInterval
public var includedSpanSeconds: TimeInterval {
self.segments.reduce(0) { $0 + max(0, $1.end.timeIntervalSince($1.start)) }
}
}
/// Splits legacy traces only when observation resumes well after the last timer deadline. The
/// normal scheduled wait remains inside the preceding segment; only overdue wall time is excluded.
public enum ReplayTraceSegmenter {
public static let defaultGraceSeconds: TimeInterval = 5 * 60
public static func automatic(
_ records: [AdaptiveRefreshTraceRecord],
graceSeconds: TimeInterval = Self.defaultGraceSeconds) -> ReplaySegmentationReport
{
let sorted = records.sorted { $0.timestamp < $1.timestamp }
guard let first = sorted.first else {
return ReplaySegmentationReport(
segments: [], excludedGapSeconds: 0, breakCount: 0, graceSeconds: graceSeconds)
}
var segments: [ReplayTraceSegment] = []
var currentRecords: [AdaptiveRefreshTraceRecord] = []
var currentStart = first.timestamp
var expectedDeadline: Date?
var excludedGapSeconds: TimeInterval = 0
for record in sorted {
if let deadline = expectedDeadline,
record.timestamp.timeIntervalSince(deadline) > graceSeconds,
!currentRecords.isEmpty
{
let end = max(currentRecords.last!.timestamp, deadline)
segments.append(ReplayTraceSegment(records: currentRecords, start: currentStart, end: end))
excludedGapSeconds += max(0, record.timestamp.timeIntervalSince(end))
currentRecords = []
currentStart = record.timestamp
expectedDeadline = nil
}
currentRecords.append(record)
if record.kind == .decision, let delay = record.delaySeconds, delay > 0 {
expectedDeadline = record.timestamp.addingTimeInterval(delay)
} else if record.kind == .timerAdvanced, let candidate = record.candidateScheduledAt {
expectedDeadline = candidate
}
}
if let last = currentRecords.last {
segments.append(ReplayTraceSegment(records: currentRecords, start: currentStart, end: last.timestamp))
}
return ReplaySegmentationReport(
segments: segments,
excludedGapSeconds: excludedGapSeconds,
breakCount: max(0, segments.count - 1),
graceSeconds: graceSeconds)
}
}
extension ReplayEngine {
public static func runSegmented(
trace: [AdaptiveRefreshTraceRecord],
policy: some ReplayPolicy,
graceSeconds: TimeInterval = ReplayTraceSegmenter.defaultGraceSeconds) -> ReplayMetrics
{
let report = ReplayTraceSegmenter.automatic(trace, graceSeconds: graceSeconds)
let stalenessStarts = report.segments.map { segment in
segment.records.first(where: { $0.kind == .refreshCompleted })?.timestamp
}
let runs = zip(report.segments, stalenessStarts).map { segment, stalenessStart in
self.runDetailed(
trace: segment.replayRecords,
policy: policy,
stalenessStartAt: stalenessStart ?? .distantFuture)
}
let boundaryCensoredMenuOpenCount = zip(report.segments, stalenessStarts).reduce(0) { partial, pair in
let (segment, stalenessStart) = pair
return partial + segment.records.count(where: { record in
record.kind == .menuOpen && (stalenessStart.map { record.timestamp < $0 } ?? true)
})
}
let span = report.includedSpanSeconds
let refreshCount = runs.reduce(0) { $0 + $1.metrics.totalRefreshCount }
let stalenessSamples = runs.flatMap(\.stalenessSamples)
return ReplayMetrics(
policyName: policy.name,
simulatedSpanSeconds: span,
totalRefreshCount: refreshCount,
refreshCountPer24h: span > 0 ? Double(refreshCount) * 86400 / span : 0,
stalenessAtMenuOpen: StalenessStats(samples: stalenessSamples),
constrainedCompliance: ConstrainedCompliance(
constrainedDecisionCount: runs.reduce(0) {
$0 + $1.metrics.constrainedCompliance.constrainedDecisionCount
},
violationCount: runs.reduce(0) { $0 + $1.metrics.constrainedCompliance.violationCount }),
interactionAdvanceCount: runs.reduce(0) { $0 + $1.metrics.interactionAdvanceCount },
codingActiveDecisionCount: runs.reduce(0) { $0 + $1.metrics.codingActiveDecisionCount },
codingActiveDelayViolationCount: runs.reduce(0) {
$0 + $1.metrics.codingActiveDelayViolationCount
},
segmentCount: report.segments.count,
excludedGapSeconds: report.excludedGapSeconds,
boundaryCensoredMenuOpenCount: boundaryCensoredMenuOpenCount)
}
}
+5
View File
@@ -0,0 +1,5 @@
module CSQLite3 [system] {
header "shim.h"
link "sqlite3"
export *
}
+1
View File
@@ -0,0 +1 @@
#include <sqlite3.h>
+84
View File
@@ -0,0 +1,84 @@
import AppKit
@MainActor
func showAbout() {
NSApp.activate(ignoringOtherApps: true)
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? ""
let versionString = build.isEmpty ? version : "\(version) (\(build))"
let buildTimestamp = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String
let gitCommit = Bundle.main.object(forInfoDictionaryKey: "CodexGitCommit") as? String
let separator = NSAttributedString(string: " · ", attributes: [
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
])
func makeLink(_ title: String, urlString: String) -> NSAttributedString {
NSAttributedString(string: title, attributes: [
.link: URL(string: urlString) as Any,
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
])
}
let credits = NSMutableAttributedString(string: "Peter Steinberger — MIT License\n")
credits.append(makeLink("GitHub", urlString: "https://github.com/steipete/CodexBar"))
credits.append(separator)
credits.append(makeLink("Website", urlString: "https://codexbar.app"))
credits.append(separator)
credits.append(makeLink("Twitter", urlString: "https://twitter.com/steipete"))
credits.append(separator)
credits.append(makeLink("Email", urlString: "mailto:peter@steipete.me"))
if let buildTimestamp, let formatted = formattedBuildTimestamp(buildTimestamp) {
var builtLine = "Built \(formatted)"
if let gitCommit, !gitCommit.isEmpty, gitCommit != "unknown" {
builtLine += " (\(gitCommit)"
#if DEBUG
builtLine += " DEBUG BUILD"
#endif
builtLine += ")"
}
credits.append(NSAttributedString(string: "\n\(builtLine)", attributes: [
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
.foregroundColor: NSColor.secondaryLabelColor,
]))
}
let options: [NSApplication.AboutPanelOptionKey: Any] = [
.applicationName: "CodexBar",
.applicationVersion: versionString,
.version: versionString,
.credits: credits,
.applicationIcon: (NSApplication.shared.applicationIconImage ?? NSImage()) as Any,
]
NSApp.orderFrontStandardAboutPanel(options: options)
// Remove the focus ring around the app icon in the standard About panel for a cleaner look.
if let aboutPanel = NSApp.windows.first(where: { $0.className.contains("About") }) {
removeFocusRings(in: aboutPanel.contentView)
}
}
private func formattedBuildTimestamp(_ timestamp: String) -> String? {
let parser = ISO8601DateFormatter()
parser.formatOptions = [.withInternetDateTime]
guard let date = parser.date(from: timestamp) else { return timestamp }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = .current
return formatter.string(from: date)
}
@MainActor
private func removeFocusRings(in view: NSView?) {
guard let view else { return }
if let imageView = view as? NSImageView {
imageView.focusRingType = .none
}
for subview in view.subviews {
removeFocusRings(in: subview)
}
}
@@ -0,0 +1,35 @@
import AdaptiveRefreshCore
import Foundation
/// Decides how long to wait before the next automatic usage refresh.
/// Pure by construction: every signal arrives via `Input`, so the same
/// input always yields the same `Decision` with no clock or system reads.
struct AdaptiveRefreshPolicy: Sendable {
struct Input: Sendable, Equatable {
let now: Date
let lastMenuOpenAt: Date?
let lowPowerModeEnabled: Bool
let thermalState: ProcessInfo.ThermalState
}
typealias Reason = AdaptiveRefreshPolicyCore.Reason
typealias Decision = AdaptiveRefreshPolicyCore.Decision
/// Representative cadence for consumers that need a single interval but cannot reach live
/// signals (`ProviderRegistry` builds provider specs before a `UsageStore` exists). Matches
/// `warmDelay`: the steady-state cadence while the user is active, which is when
/// interval-derived heuristics such as the persistent-CLI-session idle window matter most.
static let nominalIntervalForHeuristics = AdaptiveRefreshPolicyCore.nominalIntervalForHeuristics
func nextDelay(for input: Input) -> Decision {
AdaptiveRefreshPolicyCore().nextDelay(for: AdaptiveRefreshPolicyCore.Input(
now: input.now,
lastMenuOpenAt: input.lastMenuOpenAt,
lowPowerModeEnabled: input.lowPowerModeEnabled,
thermalPressure: Self.isConstrained(input.thermalState) ? .constrained : .nominal))
}
private static func isConstrained(_ state: ProcessInfo.ThermalState) -> Bool {
state == .serious || state == .critical
}
}
+154
View File
@@ -0,0 +1,154 @@
import CodexBarCore
import Foundation
import Observation
struct AgentSessionRemoteRefreshGate {
private(set) var generation = 0
private(set) var isInFlight = false
private(set) var isPending = false
mutating func settingsDidChange() {
self.generation += 1
self.isPending = self.isInFlight
}
mutating func begin() -> Int? {
guard !self.isInFlight else {
return nil
}
self.isInFlight = true
self.isPending = false
return self.generation
}
mutating func finish(generation: Int) -> (shouldPublish: Bool, shouldRetry: Bool) {
self.isInFlight = false
let outcome = (generation == self.generation, self.isPending)
self.isPending = false
return outcome
}
}
@MainActor
@Observable
final class AgentSessionsStore {
private let settings: SettingsStore
private let localScanner: LocalAgentSessionScanner
private let remoteFetcher: RemoteSessionFetcher
@ObservationIgnored private var localRefreshTask: Task<Void, Never>?
@ObservationIgnored private var remoteRefreshTask: Task<Void, Never>?
@ObservationIgnored private var localRefreshInFlight = false
@ObservationIgnored private var remoteRefreshGate = AgentSessionRemoteRefreshGate()
@ObservationIgnored var onUpdate: (@MainActor () -> Void)?
private(set) var localSessions: [AgentSession] = []
private(set) var remoteHosts: [RemoteSessionHostResult] = []
private(set) var lastUpdatedAt: Date?
init(
settings: SettingsStore,
localScanner: LocalAgentSessionScanner = LocalAgentSessionScanner(),
remoteFetcher: RemoteSessionFetcher = RemoteSessionFetcher())
{
self.settings = settings
self.localScanner = localScanner
self.remoteFetcher = remoteFetcher
}
var totalCount: Int {
self.localSessions.count + self.remoteHosts.reduce(0) { $0 + $1.sessions.count }
}
func start() {
guard self.localRefreshTask == nil, self.remoteRefreshTask == nil else { return }
self.localRefreshTask = Task { [weak self] in
while !Task.isCancelled {
await self?.refreshLocal()
try? await Task.sleep(for: .seconds(30))
}
}
self.remoteRefreshTask = Task { [weak self] in
while !Task.isCancelled {
await self?.refreshRemote()
try? await Task.sleep(for: .seconds(60))
}
}
}
func stop() {
self.localRefreshTask?.cancel()
self.remoteRefreshTask?.cancel()
self.localRefreshTask = nil
self.remoteRefreshTask = nil
}
func settingsDidChange() {
self.remoteRefreshGate.settingsDidChange()
guard self.settings.agentSessionsEnabled else {
self.localSessions = []
self.remoteHosts = []
self.onUpdate?()
return
}
guard !SettingsStore.isRunningTests else { return }
Task { [weak self] in
await self?.refreshLocal()
await self?.refreshRemote()
}
}
func refreshOnMenuOpen() {
guard self.settings.agentSessionsEnabled, !SettingsStore.isRunningTests else { return }
Task { [weak self] in
await self?.refreshLocal()
await self?.refreshRemote()
}
}
func focus(_ session: AgentSession, remoteHost: String?) {
if let remoteHost {
Task {
await self.remoteFetcher.focus(sessionID: session.id, host: remoteHost)
}
} else {
_ = SessionWindowFocuser.focus(session)
}
}
private func refreshLocal() async {
guard self.settings.agentSessionsEnabled, !self.localRefreshInFlight else { return }
self.localRefreshInFlight = true
let sessions = await self.localScanner.scan()
self.localRefreshInFlight = false
guard !Task.isCancelled, self.settings.agentSessionsEnabled else { return }
self.localSessions = sessions
self.lastUpdatedAt = Date()
self.onUpdate?()
}
private func refreshRemote() async {
guard self.settings.agentSessionsEnabled else { return }
guard var generation = self.remoteRefreshGate.begin() else { return }
while self.settings.agentSessionsEnabled {
var hosts = self.manualHosts
await hosts.append(contentsOf: self.remoteFetcher.discoveredHosts())
let results = await self.remoteFetcher.fetch(hosts: hosts)
let outcome = self.remoteRefreshGate.finish(generation: generation)
guard !Task.isCancelled, self.settings.agentSessionsEnabled else { return }
if outcome.shouldPublish {
self.remoteHosts = results
self.lastUpdatedAt = Date()
self.onUpdate?()
}
guard outcome.shouldRetry, let nextGeneration = self.remoteRefreshGate.begin() else { return }
generation = nextGeneration
}
}
private var manualHosts: [String] {
self.settings.agentSessionsManualHosts
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
}
}
+115
View File
@@ -0,0 +1,115 @@
import CodexBarCore
import Foundation
@preconcurrency import UserNotifications
@MainActor
final class AppNotifications {
static let shared = AppNotifications()
private let centerProvider: @Sendable () -> UNUserNotificationCenter
private let logger = CodexBarLog.logger(LogCategories.notifications)
private var authorizationTask: Task<Bool, Never>?
init(centerProvider: @escaping @Sendable () -> UNUserNotificationCenter = { UNUserNotificationCenter.current() }) {
self.centerProvider = centerProvider
}
func requestAuthorizationOnStartup() {
guard !Self.isRunningUnderTests else { return }
_ = self.ensureAuthorizationTask()
}
func post(
idPrefix: String,
title: String,
body: String,
badge: NSNumber? = nil,
soundEnabled: Bool = true)
{
guard !Self.isRunningUnderTests else { return }
let center = self.centerProvider()
let logger = self.logger
Task { @MainActor in
let granted = await self.ensureAuthorized()
guard granted else {
logger.debug("not authorized; skipping post", metadata: ["prefix": idPrefix])
return
}
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = soundEnabled ? .default : nil
content.badge = badge
let request = UNNotificationRequest(
identifier: "codexbar-\(idPrefix)-\(UUID().uuidString)",
content: content,
trigger: nil)
logger.info("posting", metadata: ["prefix": idPrefix])
do {
try await center.add(request)
} catch {
let errorText = String(describing: error)
logger.error("failed to post", metadata: ["prefix": idPrefix, "error": errorText])
}
}
}
// MARK: - Private
private func ensureAuthorizationTask() -> Task<Bool, Never> {
if let authorizationTask { return authorizationTask }
let task = Task { @MainActor in
await self.requestAuthorization()
}
self.authorizationTask = task
return task
}
private func ensureAuthorized() async -> Bool {
await self.ensureAuthorizationTask().value
}
private func requestAuthorization() async -> Bool {
if let existing = await self.notificationAuthorizationStatus() {
if existing == .authorized || existing == .provisional {
return true
}
if existing == .denied {
return false
}
}
let center = self.centerProvider()
return await withCheckedContinuation { continuation in
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
continuation.resume(returning: granted)
}
}
}
private func notificationAuthorizationStatus() async -> UNAuthorizationStatus? {
let center = self.centerProvider()
return await withCheckedContinuation { continuation in
center.getNotificationSettings { settings in
continuation.resume(returning: settings.authorizationStatus)
}
}
}
private static var isRunningUnderTests: Bool {
// Swift Testing doesn't always set XCTest env vars, and removing XCTest imports from
// the test target can make NSClassFromString("XCTestCase") return nil. If we're not
// running inside an app bundle, treat it as "tests/headless" to avoid crashes when
// accessing UNUserNotificationCenter.
if Bundle.main.bundleURL.pathExtension != "app" { return true }
let env = ProcessInfo.processInfo.environment
if env["XCTestConfigurationFilePath"] != nil { return true }
if env["TESTING_LIBRARY_VERSION"] != nil { return true }
if env["SWIFT_TESTING"] != nil { return true }
return NSClassFromString("XCTestCase") != nil
}
}
@@ -0,0 +1,11 @@
import Foundation
enum ChartBarHoverSelection {
static func accepts(distanceFromBarCenter: CGFloat, barHalfWidth: CGFloat, selectableCount: Int) -> Bool {
selectableCount <= 1 || distanceFromBarCenter <= barHalfWidth
}
static func nextCalendarDay(after date: Date, calendar: Calendar = .current) -> Date {
calendar.date(byAdding: .day, value: 1, to: date) ?? date.addingTimeInterval(86400)
}
}
+123
View File
@@ -0,0 +1,123 @@
import CodexBarCore
import Darwin
import Foundation
struct ClaudeLoginRunner {
static let loginArguments = ["auth", "login", "--claudeai"]
private static let successMarkers = ["Successfully logged in", "Login successful", "Logged in successfully"]
enum Phase {
case requesting
case waitingBrowser
}
struct Result {
enum Outcome {
case success
case timedOut
case failed(status: Int32)
case missingBinary
case launchFailed(String)
}
let outcome: Outcome
let output: String
let authLink: String?
}
static func run(
timeout: TimeInterval = 120,
binary: String = "claude",
environment: [String: String]? = nil,
onPhaseChange: @escaping @Sendable (Phase) -> Void) async -> Result
{
await Task(priority: .userInitiated) {
onPhaseChange(.requesting)
do {
let runResult = try self.runPTY(
timeout: timeout,
binary: binary,
environment: environment,
onPhaseChange: onPhaseChange)
let link = self.firstLink(in: runResult.output)
switch runResult.completion {
case .processExited(status: 0):
return Result(outcome: .success, output: runResult.output, authLink: link)
case let .processExited(status):
return Result(outcome: .failed(status: status), output: runResult.output, authLink: link)
case .outputCondition where self.successMarkers.contains(where: runResult.output.contains):
return Result(outcome: .success, output: runResult.output, authLink: link)
case .outputCondition, .idleTimeout, .deadlineExceeded:
return Result(outcome: .timedOut, output: runResult.output, authLink: link)
}
} catch LoginError.binaryNotFound {
return Result(outcome: .missingBinary, output: "", authLink: nil)
} catch let LoginError.timedOut(text) {
return Result(outcome: .timedOut, output: text, authLink: self.firstLink(in: text))
} catch {
return Result(outcome: .launchFailed(error.localizedDescription), output: "", authLink: nil)
}
}.value
}
// MARK: - PTY runner
private enum LoginError: Error {
case binaryNotFound
case timedOut(text: String)
case launchFailed(String)
}
private struct PTYRunResult {
let output: String
let completion: TTYCommandRunner.Result.Completion
}
private static func runPTY(
timeout: TimeInterval,
binary: String,
environment: [String: String]?,
onPhaseChange: @escaping @Sendable (Phase) -> Void) throws -> PTYRunResult
{
let runner = TTYCommandRunner()
var options = TTYCommandRunner.Options(rows: 50, cols: 160, timeout: timeout)
options.extraArgs = self.loginArguments
options.baseEnvironment = environment
options.stopOnURL = false // keep running until CLI confirms
options.stopOnSubstrings = self.successMarkers
options.sendOnSubstrings = ["press ENTER to open in browser": "\r"]
options.settleAfterStop = 0.35
options.returnOnEmptyProcessExit = true
do {
let result = try runner.run(
binary: binary,
send: "",
options: options,
onURLDetected: { onPhaseChange(.waitingBrowser) })
return PTYRunResult(output: result.text, completion: result.completion)
} catch TTYCommandRunner.Error.binaryNotFound {
throw LoginError.binaryNotFound
} catch TTYCommandRunner.Error.timedOut {
throw LoginError.timedOut(text: "")
} catch let TTYCommandRunner.Error.launchFailed(msg) {
throw LoginError.launchFailed(msg)
} catch {
throw LoginError.launchFailed(error.localizedDescription)
}
}
private static func firstLink(in text: String) -> String? {
let pattern = #"https?://[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+"#
guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
let nsRange = NSRange(text.startIndex..<text.endIndex, in: text)
guard let match = regex.firstMatch(in: text, options: [], range: nsRange),
let range = Range(match.range, in: text) else { return nil }
var url = String(text[range])
while let last = url.unicodeScalars.last,
CharacterSet(charactersIn: ".,;:)]}>\"'").contains(last)
{
url.unicodeScalars.removeLast()
}
return url
}
}
+77
View File
@@ -0,0 +1,77 @@
import AppKit
import SwiftUI
@MainActor
enum MenuPasteboardCopy {
typealias DeferredAction = @MainActor @Sendable () -> Void
typealias Scheduler = @MainActor @Sendable (@escaping DeferredAction) -> Void
typealias Writer = @MainActor @Sendable (String) -> Void
static func perform(
_ text: String,
scheduler: Scheduler = Self.schedule,
writer: @escaping Writer = Self.write,
completion: @escaping DeferredAction = {})
{
scheduler {
writer(text)
completion()
}
}
private static func schedule(_ action: @escaping DeferredAction) {
DispatchQueue.main.async(execute: action)
}
private static func write(_ text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
}
struct ClickToCopyOverlay: NSViewRepresentable {
let copyText: String
func makeNSView(context: Context) -> ClickToCopyView {
ClickToCopyView(copyText: self.copyText)
}
func updateNSView(_ nsView: ClickToCopyView, context: Context) {
// Guard against no-op writes to avoid AppKit view invalidation on every
// parent card SwiftUI diff (each MenuCardView body re-eval runs through
// .overlay { ClickToCopyOverlay(...) }, which calls updateNSView even
// when copyText is unchanged).
guard nsView.copyText != self.copyText else { return }
nsView.copyText = self.copyText
}
}
final class ClickToCopyView: NSView {
var copyText: String
private let copyAction: (String) -> Void
init(
copyText: String,
copyAction: @escaping (String) -> Void = { MenuPasteboardCopy.perform($0) })
{
self.copyText = copyText
self.copyAction = copyAction
super.init(frame: .zero)
self.wantsLayer = false
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
true
}
override func mouseDown(with event: NSEvent) {
_ = event
self.copyAction(self.copyText)
}
}
@@ -0,0 +1,180 @@
import CodexBarCore
import Foundation
enum CodexAccountHealth: Equatable {
case ok
case needsReauth
case workspaceDeactivated
case missingAuth
case unavailable
var label: String? {
switch self {
case .ok:
nil
case .needsReauth:
"Needs re-auth"
case .workspaceDeactivated:
"Workspace deactivated"
case .missingAuth:
"Missing auth"
case .unavailable:
"Unavailable"
}
}
static func status(for account: CodexVisibleAccount, error: String?) -> CodexAccountHealth {
if let error {
return self.status(forError: error)
}
if account.authenticationHealthLabel != nil {
return .missingAuth
}
return .ok
}
static func status(forError error: String) -> CodexAccountHealth {
let normalized = error.lowercased()
if normalized.contains("deactivated") {
return .workspaceDeactivated
}
if normalized.contains("expired") ||
normalized.contains("revoked") ||
normalized.contains("unauthorized") ||
normalized.contains("401")
{
return .needsReauth
}
if normalized.contains("missing"), normalized.contains("auth") {
return .missingAuth
}
return .unavailable
}
}
enum CodexAccountPresentationOrdering {
static func orderedAccounts(
_ accounts: [CodexVisibleAccount],
snapshots: [CodexAccountUsageSnapshot],
activeVisibleAccountID: String?)
-> [CodexVisibleAccount]
{
guard accounts.count > 1 else { return accounts }
let snapshotByID = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.id, $0) })
let rankedAccounts = accounts.enumerated().map { index, account in
RankedAccount(
account: account,
rank: Rank(
account: account,
snapshot: snapshotByID[account.id],
activeVisibleAccountID: activeVisibleAccountID,
originalIndex: index))
}
let grouped = Dictionary(grouping: rankedAccounts, by: { Self.workspaceSortKey(for: $0.account) })
return grouped.values.sorted { lhs, rhs in
(lhs.map(\.rank).min() ?? .last) < (rhs.map(\.rank).min() ?? .last)
}.flatMap { group in
group.sorted { lhs, rhs in lhs.rank < rhs.rank }.map(\.account)
}
}
private struct RankedAccount {
let account: CodexVisibleAccount
let rank: Rank
}
private struct Rank: Comparable {
static let last = Rank(bucket: Int.max, availabilityScore: -.greatestFiniteMagnitude, originalIndex: Int.max)
let bucket: Int
let availabilityScore: Double
let displaySort: String
let originalIndex: Int
private init(bucket: Int, availabilityScore: Double, originalIndex: Int) {
self.bucket = bucket
self.availabilityScore = availabilityScore
self.displaySort = ""
self.originalIndex = originalIndex
}
init(
account: CodexVisibleAccount,
snapshot: CodexAccountUsageSnapshot?,
activeVisibleAccountID: String?,
originalIndex: Int)
{
self.originalIndex = originalIndex
self.displaySort = account.menuDisplayName.lowercased()
if account.id == activeVisibleAccountID {
self.bucket = 0
} else {
let health = CodexAccountHealth.status(for: account, error: snapshot?.error)
if health != .ok {
self.bucket = health == .missingAuth ? 4 : 3
} else if let availability = Self.availability(snapshot?.snapshot), availability <= 0 {
self.bucket = 2
} else {
self.bucket = 1
}
}
self.availabilityScore = Self.availability(snapshot?.snapshot) ?? -1
}
static func < (lhs: Rank, rhs: Rank) -> Bool {
if lhs.bucket != rhs.bucket { return lhs.bucket < rhs.bucket }
if lhs.availabilityScore != rhs.availabilityScore {
return lhs.availabilityScore > rhs.availabilityScore
}
if lhs.displaySort != rhs.displaySort { return lhs.displaySort < rhs.displaySort }
return lhs.originalIndex < rhs.originalIndex
}
private static func availability(_ snapshot: UsageSnapshot?) -> Double? {
guard let snapshot else { return nil }
let session = snapshot.primary?.remainingPercent
let weekly = snapshot.secondary?.remainingPercent
return switch (session, weekly) {
case let (.some(session), .some(weekly)):
min(session, weekly)
case let (.some(session), .none):
session
case let (.none, .some(weekly)):
weekly
case (.none, .none):
nil
}
}
}
private static func workspaceSortKey(for account: CodexVisibleAccount) -> String {
if let workspaceAccountID = account.workspaceAccountID, !workspaceAccountID.isEmpty {
return workspaceAccountID.lowercased()
}
return account.menuWorkspaceLabel?.lowercased() ?? "personal"
}
}
struct CodexAccountWorkspaceSection: Equatable {
let title: String
let accounts: [CodexVisibleAccount]
}
extension [CodexVisibleAccount] {
func codexWorkspaceSections() -> [CodexAccountWorkspaceSection] {
guard !self.isEmpty else { return [] }
var sections: [CodexAccountWorkspaceSection] = []
for account in self {
let title = account.menuWorkspaceLabel ?? "Personal"
if let index = sections.firstIndex(where: { $0.title == title }) {
var accounts = sections[index].accounts
accounts.append(account)
sections[index] = CodexAccountWorkspaceSection(title: title, accounts: accounts)
} else {
sections.append(CodexAccountWorkspaceSection(title: title, accounts: [account]))
}
}
return sections
}
}
@@ -0,0 +1,114 @@
import Foundation
import Observation
struct CodexSystemAccountPromotionUserFacingError: Error, Equatable {
let title: String
let message: String
}
@MainActor
@Observable
final class CodexAccountPromotionCoordinator {
let service: CodexAccountPromotionService
weak var managedAccountCoordinator: ManagedCodexAccountCoordinator?
private(set) var isAuthenticatingLiveAccount = false
private(set) var isPromotingSystemAccount = false
private(set) var userFacingError: CodexSystemAccountPromotionUserFacingError?
init(
service: CodexAccountPromotionService,
managedAccountCoordinator: ManagedCodexAccountCoordinator? = nil)
{
self.service = service
self.managedAccountCoordinator = managedAccountCoordinator
}
convenience init(
settingsStore: SettingsStore,
usageStore: UsageStore,
managedAccountCoordinator: ManagedCodexAccountCoordinator? = nil)
{
self.init(
service: CodexAccountPromotionService(settingsStore: settingsStore, usageStore: usageStore),
managedAccountCoordinator: managedAccountCoordinator)
}
func promote(managedAccountID: UUID)
async -> Result<CodexAccountPromotionResult, CodexSystemAccountPromotionUserFacingError>
{
self.userFacingError = nil
guard !self.isInteractionBlocked() else {
let error = Self.interactionBlockedError()
self.userFacingError = error
return .failure(error)
}
self.isPromotingSystemAccount = true
defer { self.isPromotingSystemAccount = false }
do {
let result = try await self.service.promoteManagedAccount(id: managedAccountID)
return .success(result)
} catch {
let mapped = Self.mapUserFacingError(error)
self.userFacingError = mapped
return .failure(mapped)
}
}
func clearError() {
self.userFacingError = nil
}
func setLiveReauthenticationInProgress(_ isInProgress: Bool) {
self.isAuthenticatingLiveAccount = isInProgress
}
func isInteractionBlocked() -> Bool {
self.isPromotingSystemAccount ||
self.isAuthenticatingLiveAccount ||
self.managedAccountCoordinator?.hasConflictingManagedAccountOperationInFlight == true
}
private static func interactionBlockedError() -> CodexSystemAccountPromotionUserFacingError {
CodexSystemAccountPromotionUserFacingError(
title: L("Could not switch system account"),
message: L("Finish the current managed account change before switching the system account."))
}
static func mapUserFacingError(_ error: Error) -> CodexSystemAccountPromotionUserFacingError {
let title = L("Could not switch system account")
if let error = error as? CodexAccountPromotionError {
let message = switch error {
case .targetManagedAccountNotFound:
L("That account is no longer available in CodexBar. Refresh the account list and try again.")
case .targetManagedAccountAuthMissing:
L("CodexBar could not find saved auth for that account. Re-authenticate it and try again.")
case .targetManagedAccountAuthUnreadable:
L("CodexBar could not read saved auth for that account. Re-authenticate it and try again.")
case .liveAccountUnreadable:
L("CodexBar could not read the current system account on this Mac.")
case .liveAccountMissingIdentityForPreservation:
L("CodexBar could not safely preserve the current system account before switching.")
case .liveAccountAPIKeyOnlyUnsupported:
L("CodexBar can't replace a system account that is signed in with an API key only setup.")
case .displacedLiveManagedAccountConflict:
L(
"CodexBar found another managed account that already uses the current system account. " +
"Resolve the duplicate account before switching.")
case .displacedLiveImportFailed:
L("CodexBar could not save the current system account before switching.")
case .managedStoreCommitFailed:
L("CodexBar could not update managed account storage.")
case .liveAuthSwapFailed:
L("CodexBar could not replace the live Codex auth on this Mac.")
}
return CodexSystemAccountPromotionUserFacingError(title: title, message: message)
}
return CodexSystemAccountPromotionUserFacingError(title: title, message: error.localizedDescription)
}
}
@@ -0,0 +1,277 @@
import CodexBarCore
import Foundation
private struct CodexPreparedImportedAccount {
let account: ManagedCodexAccount
let homeURL: URL
}
struct CodexDisplacedLivePreservationExecutionResult: Equatable {
let displacedLiveDisposition: CodexAccountPromotionResult.DisplacedLiveDisposition
}
@MainActor
struct CodexDisplacedLivePreservationExecutor {
private let store: any ManagedCodexAccountStoring
private let homeFactory: any ManagedCodexHomeProducing
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
homeFactory: any ManagedCodexHomeProducing,
fileManager: FileManager = .default)
{
self.store = store
self.homeFactory = homeFactory
self.fileManager = fileManager
}
func execute(
plan: CodexDisplacedLivePreservationPlan,
context: PreparedPromotionContext) throws
-> CodexDisplacedLivePreservationExecutionResult
{
/*
Safety contract:
- This executor never swaps live auth. The caller must do that only after success.
- Import cleanup is best-effort and leaves no orphaned managed home on failure.
- Refresh/repair may copy auth before store commit, matching current behavior.
*/
switch plan {
case .none:
return CodexDisplacedLivePreservationExecutionResult(displacedLiveDisposition: .none)
case let .reject(reason):
throw self.error(for: reason)
case .importNew:
let importedAccount = try self.importDisplacedLiveAccount(from: context)
return try self.commitImportedAccount(importedAccount)
case let .refreshExisting(destination, _),
let .repairExisting(destination, _):
guard destination.persisted.id != context.target.persisted.id else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let refreshed = try self.refreshExistingManagedAccount(destination, from: context)
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .alreadyManaged(managedAccountID: refreshed.id))
}
}
private func error(for reason: CodexDisplacedLivePreservationRejectReason) -> CodexAccountPromotionError {
switch reason {
case .liveUnreadable:
.liveAccountUnreadable
case .liveAPIKeyOnlyUnsupported:
.liveAccountAPIKeyOnlyUnsupported
case .liveIdentityMissingForPreservation:
.liveAccountMissingIdentityForPreservation
case .conflictingReadableManagedHome:
.displacedLiveManagedAccountConflict
}
}
private func importDisplacedLiveAccount(
from context: PreparedPromotionContext) throws
-> CodexPreparedImportedAccount
{
guard case let .readable(liveAuthMaterial) = context.live.homeState else {
throw CodexAccountPromotionError.displacedLiveImportFailed
}
let importedHomeURL = self.homeFactory.makeHomeURL()
let importedAccountID = Self.accountID(for: importedHomeURL)
do {
try self.fileManager.createDirectory(at: importedHomeURL, withIntermediateDirectories: true)
try self.writeManagedAuthData(liveAuthMaterial.rawData, to: importedHomeURL)
guard let liveAuthIdentity = context.live.authIdentity,
let email = liveAuthIdentity.email,
liveAuthIdentity.identity != .unresolved
else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
let now = Date().timeIntervalSince1970
return CodexPreparedImportedAccount(
account: ManagedCodexAccount(
id: importedAccountID,
email: email,
providerAccountID: liveAuthIdentity.providerAccountID,
workspaceLabel: liveAuthIdentity.workspaceLabel,
workspaceAccountID: liveAuthIdentity.workspaceAccountID,
authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData),
managedHomePath: importedHomeURL.path,
createdAt: now,
updatedAt: now,
lastAuthenticatedAt: now),
homeURL: importedHomeURL)
} catch let error as CodexAccountPromotionError {
try? self.removeManagedHomeIfSafe(importedHomeURL)
throw error
} catch {
try? self.removeManagedHomeIfSafe(importedHomeURL)
throw CodexAccountPromotionError.displacedLiveImportFailed
}
}
private func commitImportedAccount(_ importedAccount: CodexPreparedImportedAccount) throws
-> CodexDisplacedLivePreservationExecutionResult
{
do {
let latestManagedAccounts = try self.store.loadAccounts()
try self.store.storeAccounts(ManagedCodexAccountSet(
version: latestManagedAccounts.version,
accounts: latestManagedAccounts.accounts + [importedAccount.account]))
return try self.resolveImportedAccountAfterCommit(importedAccount)
} catch let error as CodexAccountPromotionError {
try? self.removeManagedHomeIfSafe(importedAccount.homeURL)
throw error
} catch {
try? self.removeManagedHomeIfSafe(importedAccount.homeURL)
throw CodexAccountPromotionError.managedStoreCommitFailed
}
}
private func resolveImportedAccountAfterCommit(_ importedAccount: CodexPreparedImportedAccount) throws
-> CodexDisplacedLivePreservationExecutionResult
{
let persistedManagedAccounts = try self.store.loadAccounts()
if persistedManagedAccounts.account(id: importedAccount.account.id) != nil {
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .imported(managedAccountID: importedAccount.account.id))
}
guard let existingManagedAccount = self.repairDestination(
in: persistedManagedAccounts,
for: importedAccount.account)
else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let repairedManagedAccount = ManagedCodexAccount(
id: existingManagedAccount.id,
email: importedAccount.account.email,
providerAccountID: importedAccount.account.providerAccountID,
workspaceLabel: importedAccount.account.workspaceLabel,
workspaceAccountID: importedAccount.account.workspaceAccountID,
authFingerprint: importedAccount.account.authFingerprint,
managedHomePath: importedAccount.homeURL.path,
createdAt: existingManagedAccount.createdAt,
updatedAt: importedAccount.account.updatedAt,
lastAuthenticatedAt: importedAccount.account.lastAuthenticatedAt)
try self.store.storeAccounts(ManagedCodexAccountSet(
version: persistedManagedAccounts.version,
accounts: persistedManagedAccounts.accounts.map { account in
guard account.id == existingManagedAccount.id else { return account }
return repairedManagedAccount
}))
if existingManagedAccount.managedHomePath != importedAccount.homeURL.path {
try? self.removeManagedHomeIfSafe(
URL(fileURLWithPath: existingManagedAccount.managedHomePath, isDirectory: true))
}
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .alreadyManaged(managedAccountID: existingManagedAccount.id))
}
private func repairDestination(
in persistedManagedAccounts: ManagedCodexAccountSet,
for importedAccount: ManagedCodexAccount) -> ManagedCodexAccount?
{
if let providerAccountID = importedAccount.providerAccountID {
return persistedManagedAccounts.account(
email: importedAccount.email,
providerAccountID: providerAccountID)
}
let normalizedEmail = importedAccount.email
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return persistedManagedAccounts.accounts.first {
$0.email == normalizedEmail && $0.providerAccountID == nil
}
}
private func refreshExistingManagedAccount(
_ destination: PreparedStoredManagedAccount,
from context: PreparedPromotionContext) throws
-> ManagedCodexAccount
{
guard case let .readable(liveAuthMaterial) = context.live.homeState else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
guard let liveAuthIdentity = context.live.authIdentity else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
do {
let latestManagedAccounts = try self.store.loadAccounts()
guard let persistedManagedAccount = latestManagedAccounts.account(id: destination.persisted.id) else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let email = liveAuthIdentity.email
?? (liveAuthIdentity.providerAccountID != nil ? persistedManagedAccount.email : nil)
guard let email, liveAuthIdentity.identity != .unresolved else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
let now = Date().timeIntervalSince1970
let refreshedManagedAccount = ManagedCodexAccount(
id: persistedManagedAccount.id,
email: email,
providerAccountID: liveAuthIdentity.providerAccountID ?? persistedManagedAccount.providerAccountID,
workspaceLabel: liveAuthIdentity.workspaceLabel ?? persistedManagedAccount.workspaceLabel,
workspaceAccountID: liveAuthIdentity.workspaceAccountID ?? persistedManagedAccount.workspaceAccountID,
authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData),
managedHomePath: persistedManagedAccount.managedHomePath,
createdAt: persistedManagedAccount.createdAt,
updatedAt: now,
lastAuthenticatedAt: now)
let refreshedHomeURL = URL(fileURLWithPath: persistedManagedAccount.managedHomePath, isDirectory: true)
do {
try self.homeFactory.validateManagedHomeForDeletion(refreshedHomeURL)
} catch {
throw CodexAccountPromotionError.displacedLiveImportFailed
}
try self.fileManager.createDirectory(at: refreshedHomeURL, withIntermediateDirectories: true)
try self.writeManagedAuthData(liveAuthMaterial.rawData, to: refreshedHomeURL)
try self.store.storeAccounts(ManagedCodexAccountSet(
version: latestManagedAccounts.version,
accounts: latestManagedAccounts.accounts.map { account in
guard account.id == persistedManagedAccount.id else { return account }
return refreshedManagedAccount
}))
return refreshedManagedAccount
} catch let error as CodexAccountPromotionError {
throw error
} catch {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
}
private func writeManagedAuthData(_ data: Data, to homeURL: URL) throws {
let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL)
try data.write(to: authFileURL, options: .atomic)
try self.fileManager.setAttributes(
[.posixPermissions: NSNumber(value: Int16(0o600))],
ofItemAtPath: authFileURL.path)
}
private func removeManagedHomeIfSafe(_ homeURL: URL) throws {
try self.homeFactory.validateManagedHomeForDeletion(homeURL)
if self.fileManager.fileExists(atPath: homeURL.path) {
try self.fileManager.removeItem(at: homeURL)
}
}
private static func accountID(for homeURL: URL) -> UUID {
UUID(uuidString: homeURL.lastPathComponent) ?? UUID()
}
}
@@ -0,0 +1,190 @@
import CodexBarCore
import Foundation
enum CodexDisplacedLivePreservationNoneReason: Equatable {
case liveMissing
case targetMatchesLiveAuthIdentity
}
enum CodexDisplacedLivePreservationRejectReason: Equatable {
case liveUnreadable
case liveAPIKeyOnlyUnsupported
case liveIdentityMissingForPreservation
case conflictingReadableManagedHome
}
enum CodexDisplacedLivePreservationImportReason: Equatable {
case noExistingManagedDestination
}
enum CodexDisplacedLivePreservationRefreshReason: Equatable {
case readableHomeIdentityMatch
case readableHomeIdentityMatchUsingPersistedEmailFallback
}
enum CodexDisplacedLivePreservationRepairReason: Equatable {
case persistedProviderMatchWithMissingHome
case persistedProviderMatchWithUnreadableHome
case persistedLegacyEmailMatch
}
enum CodexDisplacedLivePreservationPlan {
case none(reason: CodexDisplacedLivePreservationNoneReason)
case reject(reason: CodexDisplacedLivePreservationRejectReason)
case importNew(reason: CodexDisplacedLivePreservationImportReason)
case refreshExisting(
destination: PreparedStoredManagedAccount,
reason: CodexDisplacedLivePreservationRefreshReason)
case repairExisting(
destination: PreparedStoredManagedAccount,
reason: CodexDisplacedLivePreservationRepairReason)
}
struct CodexDisplacedLivePreservationPlanner {
func makePlan(context: PreparedPromotionContext) -> CodexDisplacedLivePreservationPlan {
switch context.live.homeState {
case .missing:
return .none(reason: .liveMissing)
case .unreadable:
return .reject(reason: .liveUnreadable)
case .apiKeyOnly:
return .reject(reason: .liveAPIKeyOnlyUnsupported)
case .readable:
break
}
guard let liveAuthIdentity = context.live.authIdentity else {
return .reject(reason: .liveIdentityMissingForPreservation)
}
if let targetAuthIdentity = context.target.authIdentity,
CodexIdentityMatcher.matches(
targetAuthIdentity.identity,
lhsEmail: targetAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
{
return .none(reason: .targetMatchesLiveAuthIdentity)
}
let candidates = context.storedManagedAccounts.filter { $0.persisted.id != context.target.persisted.id }
if let destination = self.findReadableHomeMatch(in: candidates, liveAuthIdentity: liveAuthIdentity) {
let reason: CodexDisplacedLivePreservationRefreshReason =
if liveAuthIdentity.email == nil {
.readableHomeIdentityMatchUsingPersistedEmailFallback
} else {
.readableHomeIdentityMatch
}
return .refreshExisting(destination: destination, reason: reason)
}
if self.hasConflictingReadableHome(in: candidates, liveAuthIdentity: liveAuthIdentity) {
return .reject(reason: .conflictingReadableManagedHome)
}
if let repaired = self.findPersistedRepairMatch(in: candidates, liveAuthIdentity: liveAuthIdentity) {
return .repairExisting(destination: repaired.destination, reason: repaired.reason)
}
guard liveAuthIdentity.identity != .unresolved, liveAuthIdentity.email != nil else {
return .reject(reason: .liveIdentityMissingForPreservation)
}
return .importNew(reason: .noExistingManagedDestination)
}
private func findReadableHomeMatch(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> PreparedStoredManagedAccount?
{
candidates.first { candidate in
guard let candidateAuthIdentity = candidate.authIdentity else { return false }
return CodexIdentityMatcher.matches(
candidateAuthIdentity.identity,
lhsEmail: candidateAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
}
}
private func findPersistedRepairMatch(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> (destination: PreparedStoredManagedAccount, reason: CodexDisplacedLivePreservationRepairReason)?
{
switch liveAuthIdentity.identity {
case let .providerAccount(id):
let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(id)
if let destination = candidates.first(where: {
guard $0.persisted.providerAccountID == providerAccountID else { return false }
guard let liveEmail = liveAuthIdentity.email else { return true }
return $0.persisted.email == liveEmail
}),
let reason = self.providerRepairReason(for: destination)
{
return (destination, reason)
}
if let liveEmail = liveAuthIdentity.email,
let destination = candidates.first(where: {
$0.persisted.providerAccountID == nil && $0.persisted.email == liveEmail
})
{
return (destination, .persistedLegacyEmailMatch)
}
return nil
case let .emailOnly(normalizedEmail):
guard let destination = candidates.first(where: {
$0.persisted.providerAccountID == nil && $0.persisted.email == normalizedEmail
}) else {
return nil
}
return (destination, .persistedLegacyEmailMatch)
case .unresolved:
return nil
}
}
private func hasConflictingReadableHome(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> Bool
{
guard case let .providerAccount(id) = liveAuthIdentity.identity else {
return false
}
let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(id)
return candidates.contains { candidate in
guard candidate.persisted.providerAccountID == providerAccountID else { return false }
if let liveEmail = liveAuthIdentity.email, candidate.persisted.email != liveEmail {
return false
}
guard case .readable = candidate.homeState else { return false }
guard let candidateAuthIdentity = candidate.authIdentity else { return false }
return !CodexIdentityMatcher.matches(
candidateAuthIdentity.identity,
lhsEmail: candidateAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
}
}
private func providerRepairReason(
for destination: PreparedStoredManagedAccount)
-> CodexDisplacedLivePreservationRepairReason?
{
switch destination.homeState {
case .missing:
.persistedProviderMatchWithMissingHome
case .unreadable:
.persistedProviderMatchWithUnreadableHome
case .readable:
nil
}
}
}
@@ -0,0 +1,346 @@
import CodexBarCore
import Foundation
struct PreparedIdentity: Equatable {
let email: String?
let identity: CodexIdentity
let providerAccountID: String?
let workspaceLabel: String?
let workspaceAccountID: String?
}
struct PreparedAuthMaterial {
let homeURL: URL
let rawData: Data
let credentials: CodexOAuthCredentials
let runtimeAccount: CodexAuthBackedAccount
let authIdentity: PreparedIdentity
}
enum PreparedManagedHomeState {
case readable(PreparedAuthMaterial)
case missing(homeURL: URL)
case unreadable(homeURL: URL)
}
struct PreparedStoredManagedAccount {
let persisted: ManagedCodexAccount
let persistedIdentity: PreparedIdentity
let homeState: PreparedManagedHomeState
var authIdentity: PreparedIdentity? {
switch self.homeState {
case let .readable(authMaterial):
authMaterial.authIdentity
case .missing, .unreadable:
nil
}
}
}
enum PreparedLiveHomeState {
case missing(homeURL: URL)
case unreadable(homeURL: URL)
case apiKeyOnly(PreparedAuthMaterial)
case readable(PreparedAuthMaterial)
}
struct PreparedLiveAccount {
let homeState: PreparedLiveHomeState
var homeURL: URL {
switch self.homeState {
case let .missing(homeURL), let .unreadable(homeURL):
homeURL
case let .apiKeyOnly(authMaterial), let .readable(authMaterial):
authMaterial.homeURL
}
}
var authIdentity: PreparedIdentity? {
switch self.homeState {
case let .apiKeyOnly(authMaterial), let .readable(authMaterial):
authMaterial.authIdentity
case .missing, .unreadable:
nil
}
}
}
struct PreparedPromotionContext {
let snapshot: CodexAccountReconciliationSnapshot
let managedAccounts: ManagedCodexAccountSet
let storedManagedAccounts: [PreparedStoredManagedAccount]
let target: PreparedStoredManagedAccount
let live: PreparedLiveAccount
}
@MainActor
struct PreparedPromotionContextBuilder {
private let store: any ManagedCodexAccountStoring
private let workspaceResolver: any ManagedCodexWorkspaceResolving
private let snapshotLoader: any CodexAccountReconciliationSnapshotLoading
private let authMaterialReader: any CodexAuthMaterialReading
private let baseEnvironment: [String: String]
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
workspaceResolver: any ManagedCodexWorkspaceResolving,
snapshotLoader: any CodexAccountReconciliationSnapshotLoading,
authMaterialReader: any CodexAuthMaterialReading,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default)
{
self.store = store
self.workspaceResolver = workspaceResolver
self.snapshotLoader = snapshotLoader
self.authMaterialReader = authMaterialReader
self.baseEnvironment = baseEnvironment
self.fileManager = fileManager
}
func build(targetID: UUID) async throws -> PreparedPromotionContext {
let snapshot = self.snapshotLoader.loadSnapshot()
let managedAccounts = try self.store.loadAccounts()
var preparedAccounts: [PreparedStoredManagedAccount] = []
preparedAccounts.reserveCapacity(managedAccounts.accounts.count)
for account in managedAccounts.accounts {
let preparedAccount = try await self.prepareStoredManagedAccount(account)
preparedAccounts.append(preparedAccount)
}
guard let target = preparedAccounts.first(where: { $0.persisted.id == targetID }) else {
throw CodexAccountPromotionError.targetManagedAccountNotFound
}
let live = await self.prepareLiveAccount()
return PreparedPromotionContext(
snapshot: snapshot,
managedAccounts: managedAccounts,
storedManagedAccounts: preparedAccounts,
target: target,
live: live)
}
private func prepareStoredManagedAccount(
_ account: ManagedCodexAccount) async throws
-> PreparedStoredManagedAccount
{
let homeURL = URL(fileURLWithPath: account.managedHomePath, isDirectory: true)
let persistedIdentity = Self.persistedIdentity(from: account)
let homeState = await self.prepareManagedHomeState(homeURL: homeURL)
return PreparedStoredManagedAccount(
persisted: account,
persistedIdentity: persistedIdentity,
homeState: homeState)
}
private func prepareManagedHomeState(homeURL: URL) async -> PreparedManagedHomeState {
let readResult = self.readAuthData(homeURL: homeURL)
switch readResult {
case .missing:
return .missing(homeURL: homeURL)
case .unreadable:
return .unreadable(homeURL: homeURL)
case let .readable(rawData):
guard let authMaterial = await self.inspectAuthMaterial(homeURL: homeURL, rawData: rawData) else {
return .unreadable(homeURL: homeURL)
}
return .readable(authMaterial)
}
}
private func prepareLiveAccount() async -> PreparedLiveAccount {
let liveHomeURL = self.liveHomeURL()
let readResult = self.readAuthData(homeURL: liveHomeURL)
switch readResult {
case .missing:
return PreparedLiveAccount(homeState: .missing(homeURL: liveHomeURL))
case .unreadable:
return PreparedLiveAccount(homeState: .unreadable(homeURL: liveHomeURL))
case let .readable(rawData):
guard let authMaterial = await self.inspectAuthMaterial(homeURL: liveHomeURL, rawData: rawData) else {
return PreparedLiveAccount(homeState: .unreadable(homeURL: liveHomeURL))
}
if Self.isAPIKeyOnly(credentials: authMaterial.credentials, rawData: authMaterial.rawData) {
return PreparedLiveAccount(homeState: .apiKeyOnly(authMaterial))
}
return PreparedLiveAccount(homeState: .readable(authMaterial))
}
}
private func inspectAuthMaterial(homeURL: URL, rawData: Data) async -> PreparedAuthMaterial? {
guard let credentials = try? CodexOAuthCredentialsStore.parse(data: rawData),
let runtimeAccount = try? Self.runtimeAccount(from: rawData)
else {
return nil
}
let authIdentity = await self.derivedIdentity(
homePath: homeURL.path,
runtimeAccount: runtimeAccount)
return PreparedAuthMaterial(
homeURL: homeURL,
rawData: rawData,
credentials: credentials,
runtimeAccount: runtimeAccount,
authIdentity: authIdentity)
}
private func derivedIdentity(homePath: String, runtimeAccount: CodexAuthBackedAccount) async -> PreparedIdentity {
let normalizedEmail = Self.normalizeEmail(runtimeAccount.email)
let normalizedIdentity = Self.normalizedIdentity(runtimeAccount.identity, email: normalizedEmail)
let providerAccountID: String? = switch normalizedIdentity {
case let .providerAccount(id):
ManagedCodexAccount.normalizeProviderAccountID(id)
case .emailOnly, .unresolved:
nil
}
let workspaceIdentity: CodexOpenAIWorkspaceIdentity? = if let providerAccountID {
await self.workspaceResolver.resolveWorkspaceIdentity(
homePath: homePath,
providerAccountID: providerAccountID)
} else {
nil
}
return PreparedIdentity(
email: normalizedEmail,
identity: normalizedIdentity,
providerAccountID: providerAccountID,
workspaceLabel: workspaceIdentity?.workspaceLabel,
workspaceAccountID: workspaceIdentity?.workspaceAccountID ?? providerAccountID)
}
private static func persistedIdentity(from account: ManagedCodexAccount) -> PreparedIdentity {
let normalizedEmail = Self.normalizeEmail(account.email)
let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(account.providerAccountID)
let identity = Self.normalizedIdentity(
CodexIdentityResolver.resolve(accountId: providerAccountID, email: normalizedEmail),
email: normalizedEmail)
return PreparedIdentity(
email: normalizedEmail,
identity: identity,
providerAccountID: providerAccountID,
workspaceLabel: account.workspaceLabel,
workspaceAccountID: account.workspaceAccountID)
}
private func liveHomeURL() -> URL {
CodexHomeScope.ambientHomeURL(env: self.baseEnvironment, fileManager: self.fileManager)
}
private func readAuthData(homeURL: URL) -> PreparedAuthReadState {
do {
let rawData = try self.authMaterialReader.readAuthData(homeURL: homeURL)
guard let rawData else {
return .missing
}
return .readable(rawData)
} catch {
return .unreadable
}
}
private static func runtimeAccount(from rawData: Data) throws -> CodexAuthBackedAccount {
guard let json = try JSONSerialization.jsonObject(with: rawData) as? [String: Any] else {
throw CodexOAuthCredentialsError.decodeFailed("Invalid JSON")
}
let tokens = json["tokens"] as? [String: Any]
let idToken = tokens.flatMap {
Self.nonEmptyString(in: $0, snakeCaseKey: "id_token", camelCaseKey: "idToken")
}
let payload = idToken.flatMap(UsageFetcher.parseJWT)
let authDict = payload?["https://api.openai.com/auth"] as? [String: Any]
let profileDict = payload?["https://api.openai.com/profile"] as? [String: Any]
let email = Self.normalizeEmail(
(payload?["email"] as? String) ?? (profileDict?["email"] as? String))
let plan = Self.normalizedField(
(authDict?["chatgpt_plan_type"] as? String) ?? (payload?["chatgpt_plan_type"] as? String))
let accountID = ManagedCodexAccount.normalizeProviderAccountID(
tokens.flatMap {
Self.nonEmptyString(in: $0, snakeCaseKey: "account_id", camelCaseKey: "accountId")
}
?? (authDict?["chatgpt_account_id"] as? String)
?? (payload?["chatgpt_account_id"] as? String))
let identity = Self.normalizedIdentity(
CodexIdentityResolver.resolve(accountId: accountID, email: email),
email: email)
return CodexAuthBackedAccount(identity: identity, email: email, plan: plan)
}
private static func normalizedField(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
return nil
}
return value
}
private static func normalizeEmail(_ email: String?) -> String? {
CodexIdentityResolver.normalizeEmail(email)
}
private static func normalizedIdentity(_ identity: CodexIdentity, email: String?) -> CodexIdentity {
guard let email else { return identity }
return CodexIdentityMatcher.normalized(identity, fallbackEmail: email)
}
private static func isAPIKeyOnly(credentials: CodexOAuthCredentials, rawData: Data) -> Bool {
guard self.hasUsableOAuthTokens(in: rawData) == false else {
return false
}
return credentials.refreshToken.isEmpty
&& credentials.idToken == nil
&& credentials.accountId == nil
&& credentials.lastRefresh == nil
}
private static func hasUsableOAuthTokens(in rawData: Data) -> Bool {
guard let json = try? JSONSerialization.jsonObject(with: rawData) as? [String: Any],
let tokens = json["tokens"] as? [String: Any]
else {
return false
}
let accessToken = self.nonEmptyString(
in: tokens,
snakeCaseKey: "access_token",
camelCaseKey: "accessToken")
let refreshToken = self.nonEmptyString(
in: tokens,
snakeCaseKey: "refresh_token",
camelCaseKey: "refreshToken")
return accessToken != nil && refreshToken != nil
}
private static func nonEmptyString(
in dictionary: [String: Any],
snakeCaseKey: String,
camelCaseKey: String)
-> String?
{
if let value = dictionary[snakeCaseKey] as? String,
value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
{
return value
}
if let value = dictionary[camelCaseKey] as? String,
value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
{
return value
}
return nil
}
}
private enum PreparedAuthReadState {
case missing
case unreadable
case readable(Data)
}
@@ -0,0 +1,311 @@
import CodexBarCore
import Darwin
import Foundation
@MainActor
protocol CodexAccountReconciliationSnapshotLoading {
func loadSnapshot() -> CodexAccountReconciliationSnapshot
}
protocol CodexAuthMaterialReading: Sendable {
func readAuthData(homeURL: URL) throws -> Data?
}
protocol CodexLiveAuthSwapping: Sendable {
func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws
}
@MainActor
protocol CodexActiveSourceWriting {
func writeCodexActiveSource(_ source: CodexActiveSource)
}
@MainActor
protocol CodexAccountScopedRefreshing {
func refreshCodexAccountScopedState(allowDisabled: Bool) async
}
@MainActor
struct SettingsStoreCodexAccountReconciliationSnapshotLoader: CodexAccountReconciliationSnapshotLoading {
private let settingsStore: SettingsStore
init(settingsStore: SettingsStore) {
self.settingsStore = settingsStore
}
func loadSnapshot() -> CodexAccountReconciliationSnapshot {
self.settingsStore.codexAccountReconciliationSnapshot
}
}
struct DefaultCodexAuthMaterialReader: CodexAuthMaterialReading {
func readAuthData(homeURL: URL) throws -> Data? {
let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL)
guard FileManager.default.fileExists(atPath: authFileURL.path) else {
return nil
}
return try Data(contentsOf: authFileURL)
}
}
struct DefaultCodexLiveAuthSwapper: CodexLiveAuthSwapping {
func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws {
try FileManager.default.createDirectory(at: liveHomeURL, withIntermediateDirectories: true)
let liveAuthURL = CodexAccountPromotionService.authFileURL(for: liveHomeURL)
let stagedAuthURL = liveHomeURL.appendingPathComponent(
"auth.json.codexbar-staged-\(UUID().uuidString)",
isDirectory: false)
do {
try data.write(to: stagedAuthURL)
try FileManager.default.setAttributes(
[.posixPermissions: NSNumber(value: Int16(0o600))],
ofItemAtPath: stagedAuthURL.path)
try self.renameItem(at: stagedAuthURL, to: liveAuthURL)
} catch {
try? FileManager.default.removeItem(at: stagedAuthURL)
throw error
}
}
private func renameItem(at sourceURL: URL, to destinationURL: URL) throws {
let sourcePath = sourceURL.path
let destinationPath = destinationURL.path
let result = sourcePath.withCString { sourceFS in
destinationPath.withCString { destinationFS in
rename(sourceFS, destinationFS)
}
}
guard result == 0 else {
throw NSError(
domain: NSPOSIXErrorDomain,
code: Int(errno),
userInfo: [NSFilePathErrorKey: destinationPath])
}
}
}
@MainActor
struct SettingsStoreCodexActiveSourceWriter: CodexActiveSourceWriting {
private let settingsStore: SettingsStore
init(settingsStore: SettingsStore) {
self.settingsStore = settingsStore
}
func writeCodexActiveSource(_ source: CodexActiveSource) {
self.settingsStore.codexActiveSource = source
}
}
@MainActor
struct UsageStoreCodexAccountScopedRefresher: CodexAccountScopedRefreshing {
private let usageStore: UsageStore
init(usageStore: UsageStore) {
self.usageStore = usageStore
}
func refreshCodexAccountScopedState(allowDisabled: Bool) async {
await self.usageStore.refreshCodexAccountScopedState(allowDisabled: allowDisabled)
}
}
struct CodexAccountPromotionResult: Equatable {
enum Outcome: Equatable {
case promoted
case convergedNoOp
}
enum DisplacedLiveDisposition: Equatable {
case none
case alreadyManaged(managedAccountID: UUID)
case imported(managedAccountID: UUID)
}
let targetManagedAccountID: UUID
let outcome: Outcome
let displacedLiveDisposition: DisplacedLiveDisposition
let didMutateLiveAuth: Bool
let resultingActiveSource: CodexActiveSource
}
enum CodexAccountPromotionError: Error, Equatable {
case targetManagedAccountNotFound
case targetManagedAccountAuthMissing
case targetManagedAccountAuthUnreadable
case liveAccountUnreadable
case liveAccountMissingIdentityForPreservation
case liveAccountAPIKeyOnlyUnsupported
case displacedLiveManagedAccountConflict
case displacedLiveImportFailed
case managedStoreCommitFailed
case liveAuthSwapFailed
}
@MainActor
final class CodexAccountPromotionService {
private let store: any ManagedCodexAccountStoring
private let homeFactory: any ManagedCodexHomeProducing
private let identityReader: any ManagedCodexIdentityReading
private let workspaceResolver: any ManagedCodexWorkspaceResolving
private let snapshotLoader: any CodexAccountReconciliationSnapshotLoading
private let authMaterialReader: any CodexAuthMaterialReading
private let liveAuthSwapper: any CodexLiveAuthSwapping
private let activeSourceWriter: any CodexActiveSourceWriting
private let accountScopedRefresher: any CodexAccountScopedRefreshing
private let baseEnvironment: [String: String]
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
homeFactory: any ManagedCodexHomeProducing,
identityReader: any ManagedCodexIdentityReading,
workspaceResolver: any ManagedCodexWorkspaceResolving = DefaultManagedCodexWorkspaceResolver(),
snapshotLoader: any CodexAccountReconciliationSnapshotLoading,
authMaterialReader: any CodexAuthMaterialReading,
liveAuthSwapper: any CodexLiveAuthSwapping,
activeSourceWriter: any CodexActiveSourceWriting,
accountScopedRefresher: any CodexAccountScopedRefreshing,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default)
{
self.store = store
self.homeFactory = homeFactory
self.identityReader = identityReader
self.workspaceResolver = workspaceResolver
self.snapshotLoader = snapshotLoader
self.authMaterialReader = authMaterialReader
self.liveAuthSwapper = liveAuthSwapper
self.activeSourceWriter = activeSourceWriter
self.accountScopedRefresher = accountScopedRefresher
self.baseEnvironment = baseEnvironment
self.fileManager = fileManager
}
convenience init(
settingsStore: SettingsStore,
usageStore: UsageStore,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default)
{
self.init(
store: FileManagedCodexAccountStore(fileManager: fileManager),
homeFactory: ManagedCodexHomeFactory(fileManager: fileManager),
identityReader: DefaultManagedCodexIdentityReader(),
workspaceResolver: DefaultManagedCodexWorkspaceResolver(),
snapshotLoader: SettingsStoreCodexAccountReconciliationSnapshotLoader(settingsStore: settingsStore),
authMaterialReader: DefaultCodexAuthMaterialReader(),
liveAuthSwapper: DefaultCodexLiveAuthSwapper(),
activeSourceWriter: SettingsStoreCodexActiveSourceWriter(settingsStore: settingsStore),
accountScopedRefresher: UsageStoreCodexAccountScopedRefresher(usageStore: usageStore),
baseEnvironment: baseEnvironment,
fileManager: fileManager)
}
func promoteManagedAccount(id: UUID) async throws -> CodexAccountPromotionResult {
let contextBuilder = PreparedPromotionContextBuilder(
store: self.store,
workspaceResolver: self.workspaceResolver,
snapshotLoader: self.snapshotLoader,
authMaterialReader: self.authMaterialReader,
baseEnvironment: self.baseEnvironment,
fileManager: self.fileManager)
let context = try await contextBuilder.build(targetID: id)
if let resultingActiveSource = self.convergedActiveSource(for: context) {
self.activeSourceWriter.writeCodexActiveSource(resultingActiveSource)
await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true)
return CodexAccountPromotionResult(
targetManagedAccountID: id,
outcome: .convergedNoOp,
displacedLiveDisposition: .none,
didMutateLiveAuth: false,
resultingActiveSource: resultingActiveSource)
}
let targetAuthMaterial = try self.requiredTargetAuthMaterial(from: context.target)
let preservationPlan = CodexDisplacedLivePreservationPlanner().makePlan(context: context)
let executionResult = try CodexDisplacedLivePreservationExecutor(
store: self.store,
homeFactory: self.homeFactory,
fileManager: self.fileManager)
.execute(plan: preservationPlan, context: context)
do {
try self.liveAuthSwapper.swapLiveAuthData(targetAuthMaterial.rawData, liveHomeURL: context.live.homeURL)
} catch {
throw CodexAccountPromotionError.liveAuthSwapFailed
}
self.activeSourceWriter.writeCodexActiveSource(.liveSystem)
await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true)
return CodexAccountPromotionResult(
targetManagedAccountID: id,
outcome: .promoted,
displacedLiveDisposition: executionResult.displacedLiveDisposition,
didMutateLiveAuth: true,
resultingActiveSource: .liveSystem)
}
nonisolated static func authFileURL(for homeURL: URL) -> URL {
homeURL.appendingPathComponent("auth.json", isDirectory: false)
}
private func convergedActiveSource(for context: PreparedPromotionContext) -> CodexActiveSource? {
if let liveAuthIdentity = context.live.authIdentity {
let targetIdentity = context.target.authIdentity ?? context.target.persistedIdentity
guard CodexIdentityMatcher.matches(
targetIdentity.identity,
lhsEmail: targetIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
else {
return nil
}
if liveAuthIdentity.email != nil {
return .liveSystem
}
if liveAuthIdentity.providerAccountID != nil {
return .managedAccount(id: context.target.persisted.id)
}
return nil
}
guard let liveSystemAccount = context.snapshot.liveSystemAccount else {
return nil
}
guard CodexIdentityMatcher.matches(
context.snapshot.runtimeIdentity(for: context.target.persisted),
lhsEmail: context.snapshot.runtimeEmail(for: context.target.persisted),
context.snapshot.runtimeIdentity(for: liveSystemAccount),
rhsEmail: liveSystemAccount.email)
else {
return nil
}
return .liveSystem
}
private func requiredTargetAuthMaterial(from target: PreparedStoredManagedAccount) throws -> PreparedAuthMaterial {
switch target.homeState {
case let .readable(authMaterial):
guard authMaterial.authIdentity.email != nil else {
throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable
}
return authMaterial
case .missing:
throw CodexAccountPromotionError.targetManagedAccountAuthMissing
case .unreadable:
throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable
}
}
}
@@ -0,0 +1,4 @@
import CodexBarCore
typealias CodexVisibleAccount = CodexBarCore.CodexVisibleAccount
typealias CodexVisibleAccountProjection = CodexBarCore.CodexVisibleAccountProjection
@@ -0,0 +1,133 @@
import CodexBarCore
import Foundation
protocol CodexAccountUsageSnapshotStoring: Sendable {
func load(for accounts: [CodexVisibleAccount]) -> [CodexAccountUsageSnapshot]
func store(_ snapshots: [CodexAccountUsageSnapshot])
}
struct FileCodexAccountUsageSnapshotStore: CodexAccountUsageSnapshotStoring, @unchecked Sendable {
private struct Payload: Codable {
let version: Int
let records: [Record]
}
private struct Record: Codable {
let id: String
let accountIdentity: AccountIdentity?
let snapshot: UsageSnapshot?
let error: String?
let sourceLabel: String?
}
private struct AccountIdentity: Codable, Equatable {
let normalizedEmail: String?
let workspaceAccountID: String?
let authFingerprint: String?
let storedAccountID: UUID?
let selectionSource: CodexActiveSource?
init(account: CodexVisibleAccount) {
self.normalizedEmail = CodexIdentityResolver.normalizeEmail(account.email)
self.workspaceAccountID = CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(
account.workspaceAccountID)
self.authFingerprint = CodexAuthFingerprint.normalize(account.authFingerprint)
self.storedAccountID = account.storedAccountID
self.selectionSource = account.selectionSource
}
func matches(_ account: CodexVisibleAccount) -> Bool {
guard let normalizedEmail = self.normalizedEmail,
normalizedEmail == CodexIdentityResolver.normalizeEmail(account.email),
let workspaceAccountID = self.workspaceAccountID,
workspaceAccountID == CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(
account.workspaceAccountID)
else {
return false
}
return true
}
}
private static let currentVersion = 1
private let fileURL: URL
private let fileManager: FileManager
init(fileURL: URL = Self.defaultURL(), fileManager: FileManager = .default) {
self.fileURL = fileURL
self.fileManager = fileManager
}
func load(for accounts: [CodexVisibleAccount]) -> [CodexAccountUsageSnapshot] {
guard self.fileManager.fileExists(atPath: self.fileURL.path),
let data = try? Data(contentsOf: self.fileURL),
let payload = try? JSONDecoder().decode(Payload.self, from: data),
payload.version == Self.currentVersion
else {
return []
}
let accountsByID = Dictionary(uniqueKeysWithValues: accounts.map { ($0.id, $0) })
return payload.records.compactMap { record in
guard let account = accountsByID[record.id] else { return nil }
guard record.accountIdentity?.matches(account) == true else { return nil }
return CodexAccountUsageSnapshot(
account: account,
snapshot: Self.relabelSnapshot(record.snapshot, for: account),
error: record.error,
sourceLabel: record.sourceLabel)
}
}
func store(_ snapshots: [CodexAccountUsageSnapshot]) {
let payload = Payload(
version: Self.currentVersion,
records: snapshots.compactMap { snapshot in
let identity = AccountIdentity(account: snapshot.account)
guard identity.normalizedEmail != nil, identity.workspaceAccountID != nil else { return nil }
return Record(
id: snapshot.id,
accountIdentity: identity,
snapshot: snapshot.snapshot,
error: snapshot.error,
sourceLabel: snapshot.sourceLabel)
})
let directory = self.fileURL.deletingLastPathComponent()
do {
if !self.fileManager.fileExists(atPath: directory.path) {
try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
try encoder.encode(payload).write(to: self.fileURL, options: [.atomic])
#if os(macOS)
try self.fileManager.setAttributes([
.posixPermissions: NSNumber(value: Int16(0o600)),
], ofItemAtPath: self.fileURL.path)
#endif
} catch {
// Snapshot hydration is best-effort; never make menu refresh fail because disk cache failed.
}
}
private static func relabelSnapshot(_ snapshot: UsageSnapshot?, for account: CodexVisibleAccount)
-> UsageSnapshot?
{
guard let snapshot else { return nil }
let identity = snapshot.identity(for: .codex)
return snapshot.withIdentity(ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: account.email,
accountOrganization: identity?.accountOrganization,
loginMethod: identity?.loginMethod ?? account.workspaceLabel))
}
static func defaultURL() -> URL {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? FileManager.default.homeDirectoryForCurrentUser
return base
.appendingPathComponent("CodexBar", isDirectory: true)
.appendingPathComponent("codex-account-snapshots.json", isDirectory: false)
}
}
@@ -0,0 +1,139 @@
import CodexBarCore
import CryptoKit
import Foundation
enum CodexHistoryPersistedOwner: Equatable {
case canonical(String)
case legacyEmailHash(String)
case legacyOpaqueScoped(String)
case legacyUnscoped
}
enum CodexHistoryOwnership {
private static let providerAccountPrefix = "codex:v1:provider-account:"
private static let emailHashPrefix = "codex:v1:email-hash:"
static func canonicalKey(for identity: CodexIdentity) -> String? {
switch identity {
case let .providerAccount(id):
guard let normalized = Self.normalizeScopedValue(id) else { return nil }
return "\(Self.providerAccountPrefix)\(normalized)"
case let .emailOnly(normalizedEmail):
guard let normalized = CodexIdentityResolver.normalizeEmail(normalizedEmail) else { return nil }
return self.canonicalEmailHashKey(for: normalized)
case .unresolved:
return nil
}
}
static func canonicalEmailHashKey(for normalizedEmail: String) -> String {
"\(self.emailHashPrefix)\(self.legacyEmailHash(normalizedEmail: normalizedEmail))"
}
static func legacyEmailHash(normalizedEmail: String) -> String {
guard let normalized = CodexIdentityResolver.normalizeEmail(normalizedEmail) else { return "" }
return self.sha256Hex(normalized)
}
static func classifyPersistedKey(
_ rawKey: String?,
legacyEmailHash: String? = nil) -> CodexHistoryPersistedOwner
{
guard let normalizedKey = normalizeScopedValue(rawKey) else {
return .legacyUnscoped
}
if self.isCanonicalKey(normalizedKey) {
return .canonical(normalizedKey)
}
if let legacyEmailHash, normalizedKey == legacyEmailHash {
return .legacyEmailHash(normalizedKey)
}
return .legacyOpaqueScoped(normalizedKey)
}
static func belongsToTargetContinuity(
_ owner: CodexHistoryPersistedOwner,
targetCanonicalKey: String,
canonicalEmailHashKey: String?) -> Bool
{
switch owner {
case let .canonical(key):
if key == targetCanonicalKey {
return true
}
guard let canonicalEmailHashKey, self.isCanonicalEmailHashKey(canonicalEmailHashKey) else {
return false
}
return key == canonicalEmailHashKey
case .legacyEmailHash:
guard let canonicalEmailHashKey, self.isCanonicalEmailHashKey(canonicalEmailHashKey) else {
return false
}
return canonicalEmailHashKey == targetCanonicalKey ||
targetCanonicalKey.hasPrefix(self.providerAccountPrefix)
case .legacyOpaqueScoped, .legacyUnscoped:
return false
}
}
static func hasStrictSingleAccountContinuity(
scopedRawKeys: [String],
targetCanonicalKey: String,
canonicalEmailHashKey: String?,
legacyEmailHash: String?,
hasAdjacentMultiAccountVeto: Bool) -> Bool
{
guard !hasAdjacentMultiAccountVeto else { return false }
let normalizedCandidates = Set(scopedRawKeys.compactMap { rawKey in
let owner = self.classifyPersistedKey(rawKey, legacyEmailHash: legacyEmailHash)
if self.belongsToTargetContinuity(
owner,
targetCanonicalKey: targetCanonicalKey,
canonicalEmailHashKey: canonicalEmailHashKey)
{
return targetCanonicalKey
}
switch owner {
case .legacyUnscoped:
return nil
case let .legacyOpaqueScoped(key):
return "legacy-opaque:\(key)"
case let .legacyEmailHash(hash):
return "legacy-email-hash:\(hash)"
case let .canonical(key):
return key
}
})
guard normalizedCandidates.count == 1, normalizedCandidates.first == targetCanonicalKey else {
return false
}
return true
}
private static func isCanonicalKey(_ rawKey: String) -> Bool {
self.isCanonicalProviderAccountKey(rawKey) || self.isCanonicalEmailHashKey(rawKey)
}
static func isCanonicalProviderAccountKey(_ rawKey: String) -> Bool {
rawKey.hasPrefix(self.providerAccountPrefix) && rawKey.count > self.providerAccountPrefix.count
}
private static func isCanonicalEmailHashKey(_ rawKey: String) -> Bool {
rawKey.hasPrefix(self.emailHashPrefix) && rawKey.count > self.emailHashPrefix.count
}
private static func normalizeScopedValue(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
return nil
}
return trimmed
}
private static func sha256Hex(_ input: String) -> String {
let digest = SHA256.hash(data: Data(input.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
}
@@ -0,0 +1,44 @@
import Foundation
struct CodexLoginAlertInfo: Equatable {
let title: String
let message: String
}
enum CodexLoginAlertPresentation {
static func alertInfo(for result: CodexLoginRunner.Result) -> CodexLoginAlertInfo? {
switch result.outcome {
case .success:
return nil
case .missingBinary:
return CodexLoginAlertInfo(
title: L("Codex CLI not found"),
message: L("Install the Codex CLI (npm i -g @openai/codex) and try again."))
case let .launchFailed(message):
return CodexLoginAlertInfo(title: L("Could not start codex login"), message: message)
case .timedOut:
return CodexLoginAlertInfo(
title: L("Codex login timed out"),
message: self.trimmedOutput(result.output))
case let .failed(status):
let statusLine = String(format: L("codex login exited with status %d."), status)
let message = self.trimmedOutput(result.output.isEmpty ? statusLine : result.output)
return CodexLoginAlertInfo(title: L("Codex login failed"), message: message)
}
}
static func managedLoginFailureMessage(for result: CodexLoginRunner.Result) -> String {
let baseMessage = L("managed_login_failed")
guard let info = self.alertInfo(for: result) else { return baseMessage }
return "\(baseMessage)\n\n\(L("codex_login_output"))\n\(info.message)"
}
private static func trimmedOutput(_ text: String) -> String {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
let limit = 600
if trimmed.isEmpty { return L("No output captured.") }
if trimmed.count <= limit { return trimmed }
let idx = trimmed.index(trimmed.startIndex, offsetBy: limit)
return "\(trimmed[..<idx])"
}
}
+193
View File
@@ -0,0 +1,193 @@
import CodexBarCore
import Darwin
import Foundation
struct CodexLoginRunner {
struct Result: Equatable {
enum Outcome: Equatable {
case success
case timedOut
case failed(status: Int32)
case missingBinary
case launchFailed(String)
}
let outcome: Outcome
let output: String
}
static func run(
homePath: String? = nil,
timeout: TimeInterval = 120,
outputDrainTimeout: TimeInterval = 3,
environment: [String: String] = ProcessInfo.processInfo.environment,
loginPATH: [String]? = LoginShellPathCache.shared.current) async -> Result
{
await Task(priority: .userInitiated) {
var env = environment
env["PATH"] = PathBuilder.effectivePATH(
purposes: [.rpc, .tty, .nodeTooling],
env: env,
loginPATH: loginPATH)
env = CodexHomeScope.scopedEnvironment(base: env, codexHome: homePath)
guard let executable = BinaryLocator.resolveCodexBinary(
env: env,
loginPATH: loginPATH)
else {
return Result(outcome: .missingBinary, output: "")
}
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = [executable, "login"]
process.environment = env
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
let stdoutCapture = ProcessPipeCapture(pipe: stdout)
let stderrCapture = ProcessPipeCapture(pipe: stderr)
let termination = ProcessTermination()
process.terminationHandler = { _ in
termination.resolve(timedOut: false)
}
var processGroup: pid_t?
do {
try process.run()
processGroup = self.attachProcessGroup(process)
} catch {
return Result(outcome: .launchFailed(error.localizedDescription), output: "")
}
stdoutCapture.start()
stderrCapture.start()
let timedOut = await self.wait(timeout: timeout, termination: termination)
if timedOut {
self.terminate(process, processGroup: processGroup)
}
let output = await self.combinedOutput(
stdout: stdoutCapture,
stderr: stderrCapture,
timeout: outputDrainTimeout)
if timedOut {
return Result(outcome: .timedOut, output: output)
}
let status = process.terminationStatus
if status == 0 {
return Result(outcome: .success, output: output)
}
return Result(outcome: .failed(status: status), output: output)
}.value
}
private final class ProcessTermination: @unchecked Sendable {
private let lock = NSLock()
private var timedOut: Bool?
private var continuation: CheckedContinuation<Bool, Never>?
func resolve(timedOut: Bool) {
let continuation: CheckedContinuation<Bool, Never>?
self.lock.lock()
guard self.timedOut == nil else {
self.lock.unlock()
return
}
self.timedOut = timedOut
continuation = self.continuation
self.continuation = nil
self.lock.unlock()
continuation?.resume(returning: timedOut)
}
func wait() async -> Bool {
await withCheckedContinuation { continuation in
let timedOut: Bool?
self.lock.lock()
timedOut = self.timedOut
if timedOut == nil {
self.continuation = continuation
}
self.lock.unlock()
if let timedOut {
continuation.resume(returning: timedOut)
}
}
}
}
private static func wait(timeout: TimeInterval, termination: ProcessTermination) async -> Bool {
let timeoutTask = Task.detached(priority: .userInitiated) {
try? await Task.sleep(nanoseconds: self.timeoutNanoseconds(timeout))
if Task.isCancelled == false {
termination.resolve(timedOut: true)
}
}
let timedOut = await termination.wait()
timeoutTask.cancel()
return timedOut
}
private static func timeoutNanoseconds(_ timeout: TimeInterval) -> UInt64 {
guard timeout.isFinite else { return UInt64.max }
let seconds = max(0, min(timeout, Double(UInt64.max) / 1_000_000_000))
return UInt64(seconds * 1_000_000_000)
}
private static func terminate(_ process: Process, processGroup: pid_t?) {
if let pgid = processGroup {
kill(-pgid, SIGTERM)
}
if process.isRunning {
process.terminate()
}
let deadline = Date().addingTimeInterval(2.0)
while process.isRunning, Date() < deadline {
usleep(100_000)
}
if process.isRunning {
if let pgid = processGroup {
kill(-pgid, SIGKILL)
}
kill(process.processIdentifier, SIGKILL)
}
}
private static func attachProcessGroup(_ process: Process) -> pid_t? {
let pid = process.processIdentifier
return setpgid(pid, pid) == 0 ? pid : nil
}
private static func combinedOutput(
stdout: ProcessPipeCapture,
stderr: ProcessPipeCapture,
timeout: TimeInterval) async -> String
{
let drainTimeout = Duration.seconds(max(0, timeout))
async let outData = stdout.finish(timeout: drainTimeout)
async let errData = stderr.finish(timeout: drainTimeout)
let out = await self.decode(outData)
let err = await self.decode(errData)
let merged: String = if !out.isEmpty, !err.isEmpty {
[out, err].joined(separator: "\n")
} else {
out + err
}
let trimmed = merged.trimmingCharacters(in: .whitespacesAndNewlines)
let limited = trimmed.prefix(4000)
return limited.isEmpty ? L("No output captured.") : String(limited)
}
private static func decode(_ data: Data) -> String {
ProcessPipeCapture.decodeUTF8(data)
}
}
@@ -0,0 +1,188 @@
import CodexBarCore
import CryptoKit
import Foundation
struct CodexOwnershipContext {
let canonicalKey: String?
let canonicalEmailHashKey: String?
let historicalLegacyEmailHash: String?
let planUtilizationLegacyEmailHash: String?
let currentWeeklyResetAt: Date?
let hasAdjacentMultiAccountVeto: Bool
let hasAdjacentEmailScopeAmbiguity: Bool
}
extension UsageStore {
func codexOwnershipContext(
preferredEmail: String? = nil,
snapshot: UsageSnapshot? = nil,
includeDashboardFallback: Bool = false) -> CodexOwnershipContext
{
let resolvedIdentity = self.currentCodexRuntimeIdentity(
source: self.settings.codexResolvedActiveSource,
preferCurrentSnapshot: true,
allowLastKnownLiveFallback: true)
let activeSourceEmail = self.codexAccountScopedRefreshEmail(
preferCurrentSnapshot: true,
allowLastKnownLiveFallback: true)
let normalizedEmail = CodexIdentityResolver.normalizeEmail(
preferredEmail ??
activeSourceEmail ??
snapshot?.accountEmail(for: .codex) ??
self.snapshots[.codex]?.accountEmail(for: .codex) ??
(includeDashboardFallback ? self.codexAccountEmailForOpenAIDashboard() : nil))
let canonicalIdentity: CodexIdentity = switch resolvedIdentity {
case .unresolved:
if let normalizedEmail {
.emailOnly(normalizedEmail: normalizedEmail)
} else {
.unresolved
}
default:
resolvedIdentity
}
let legacyEmailSource: String? = switch canonicalIdentity {
case let .emailOnly(normalizedEmail):
normalizedEmail
case .providerAccount, .unresolved:
normalizedEmail
}
let attachedDashboardSnapshot = includeDashboardFallback
? self.attachedOpenAIDashboardSnapshot
: nil
let normalizedDashboardSnapshot = attachedDashboardSnapshot?
.toUsageSnapshot(provider: .codex, accountEmail: normalizedEmail)
let currentWeeklyResetAt = snapshot?.secondary?.resetsAt
?? self.snapshots[.codex]?.secondary?.resetsAt
?? normalizedDashboardSnapshot?.secondary?.resetsAt
return CodexOwnershipContext(
canonicalKey: CodexHistoryOwnership.canonicalKey(for: canonicalIdentity),
canonicalEmailHashKey: normalizedEmail.map { CodexHistoryOwnership.canonicalEmailHashKey(for: $0) },
historicalLegacyEmailHash: legacyEmailSource.map {
CodexHistoryOwnership.legacyEmailHash(normalizedEmail: $0)
},
planUtilizationLegacyEmailHash: legacyEmailSource.map {
Self.codexLegacyPlanUtilizationEmailHashKey(for: $0)
},
currentWeeklyResetAt: currentWeeklyResetAt,
hasAdjacentMultiAccountVeto: self.codexHasAdjacentMultiAccountVeto(),
hasAdjacentEmailScopeAmbiguity: normalizedEmail.map {
self.codexHasAdjacentEmailScopeAmbiguity(normalizedEmail: $0) ||
self.codexVisibleAccountsHaveAdjacentEmailScopeAmbiguity(normalizedEmail: $0)
} ?? false)
}
func codexOwnershipContext(
forVisibleAccount account: CodexVisibleAccount,
currentWeeklyResetAt: Date? = nil) -> CodexOwnershipContext
{
let normalizedEmail = CodexIdentityResolver.normalizeEmail(account.email)
let workspaceAccountID = CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(account.workspaceAccountID)
let canonicalIdentity: CodexIdentity = if let workspaceAccountID {
.providerAccount(id: workspaceAccountID)
} else if let normalizedEmail {
.emailOnly(normalizedEmail: normalizedEmail)
} else {
.unresolved
}
return CodexOwnershipContext(
canonicalKey: CodexHistoryOwnership.canonicalKey(for: canonicalIdentity),
canonicalEmailHashKey: normalizedEmail.map { CodexHistoryOwnership.canonicalEmailHashKey(for: $0) },
historicalLegacyEmailHash: normalizedEmail.map {
CodexHistoryOwnership.legacyEmailHash(normalizedEmail: $0)
},
planUtilizationLegacyEmailHash: normalizedEmail.map {
Self.codexLegacyPlanUtilizationEmailHashKey(for: $0)
},
currentWeeklyResetAt: currentWeeklyResetAt,
hasAdjacentMultiAccountVeto: self.codexHasAdjacentMultiAccountVeto() ||
self.codexVisibleAccountsHaveAdjacentMultiAccountVeto(),
hasAdjacentEmailScopeAmbiguity: normalizedEmail.map {
self.codexHasAdjacentEmailScopeAmbiguity(normalizedEmail: $0) ||
self.codexVisibleAccountsHaveAdjacentEmailScopeAmbiguity(normalizedEmail: $0)
} ?? false)
}
func codexHasAdjacentMultiAccountVeto() -> Bool {
let snapshot = self.settings.codexAccountReconciliationSnapshot
var distinctAccounts: Set<String> = []
if let activeManagedAccount = self.settings.activeManagedCodexAccount {
distinctAccounts.insert(CodexIdentityMatcher.selectionKey(
for: snapshot.runtimeIdentity(for: activeManagedAccount),
fallbackEmail: snapshot.runtimeEmail(for: activeManagedAccount)))
}
if let liveSystemAccount = snapshot.liveSystemAccount {
distinctAccounts.insert(CodexIdentityMatcher.selectionKey(
for: snapshot.runtimeIdentity(for: liveSystemAccount),
fallbackEmail: liveSystemAccount.email))
}
return distinctAccounts.count > 1
}
private func codexHasAdjacentEmailScopeAmbiguity(normalizedEmail: String) -> Bool {
let snapshot = self.settings.codexAccountReconciliationSnapshot
var distinctAccounts: Set<String> = []
if let activeManagedAccount = self.settings.activeManagedCodexAccount,
CodexIdentityResolver.normalizeEmail(snapshot.runtimeEmail(for: activeManagedAccount)) == normalizedEmail
{
distinctAccounts.insert(CodexIdentityMatcher.selectionKey(
for: snapshot.runtimeIdentity(for: activeManagedAccount),
fallbackEmail: snapshot.runtimeEmail(for: activeManagedAccount)))
}
if let liveSystemAccount = snapshot.liveSystemAccount,
CodexIdentityResolver.normalizeEmail(liveSystemAccount.email) == normalizedEmail
{
distinctAccounts.insert(CodexIdentityMatcher.selectionKey(
for: snapshot.runtimeIdentity(for: liveSystemAccount),
fallbackEmail: liveSystemAccount.email))
}
return distinctAccounts.count > 1
}
private func codexVisibleAccountsHaveAdjacentMultiAccountVeto() -> Bool {
let accounts = self.settings.codexVisibleAccountProjection.visibleAccounts
var distinctAccounts: Set<String> = []
for account in accounts {
if let workspaceAccountID = CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(
account.workspaceAccountID)
{
distinctAccounts.insert("provider:\(workspaceAccountID)")
} else if let normalizedEmail = CodexIdentityResolver.normalizeEmail(account.email) {
distinctAccounts.insert("email:\(normalizedEmail)")
}
}
return distinctAccounts.count > 1
}
private func codexVisibleAccountsHaveAdjacentEmailScopeAmbiguity(normalizedEmail: String) -> Bool {
let accounts = self.settings.codexVisibleAccountProjection.visibleAccounts
var distinctAccounts: Set<String> = []
for account in accounts where CodexIdentityResolver.normalizeEmail(account.email) == normalizedEmail {
if let workspaceAccountID = CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(
account.workspaceAccountID)
{
distinctAccounts.insert("provider:\(workspaceAccountID)")
} else {
distinctAccounts.insert("email:\(normalizedEmail)")
}
}
return distinctAccounts.count > 1
}
nonisolated static func codexLegacyPlanUtilizationEmailHashKey(for normalizedEmail: String) -> String {
self.sha256Hex("\(UsageProvider.codex.rawValue):email:\(normalizedEmail)")
}
nonisolated static func sha256Hex(_ input: String) -> String {
let digest = SHA256.hash(data: Data(input.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
}
@@ -0,0 +1,63 @@
import CodexBarCore
import CryptoKit
import Foundation
@MainActor
struct CodexResetCreditExpiryNotifier {
static let expiryWindow: TimeInterval = 3 * 24 * 60 * 60
static let notificationPrefix = "codex-reset-credit-expiry"
static let summaryFingerprintsKey = "codexResetCreditExpirySummaryFingerprints"
static let maximumRememberedSummaries = 64
var userDefaults: UserDefaults = .standard
var notificationPoster: (String, String, String) -> Void = { prefix, title, body in
AppNotifications.shared.post(idPrefix: prefix, title: title, body: body)
}
func postExpiringCreditsIfNeeded(
snapshot: CodexRateLimitResetCreditsSnapshot,
resetStyle: ResetTimeDisplayStyle,
now: Date = Date())
{
let expiringCredits = snapshot.availableInventory(at: now).credits.filter { credit in
guard let expiresAt = credit.expiresAt else { return false }
return expiresAt.timeIntervalSince(now) <= Self.expiryWindow
}
guard !expiringCredits.isEmpty else { return }
let fingerprint = Self.summaryFingerprint(expiringCredits)
// Account-scoped refreshes can alternate inventories, so remember more than the latest summary.
var notifiedFingerprints = self.userDefaults.stringArray(forKey: Self.summaryFingerprintsKey) ?? []
guard !notifiedFingerprints.contains(fingerprint) else { return }
notifiedFingerprints.append(fingerprint)
if notifiedFingerprints.count > Self.maximumRememberedSummaries {
notifiedFingerprints.removeFirst(notifiedFingerprints.count - Self.maximumRememberedSummaries)
}
self.userDefaults.set(notifiedFingerprints, forKey: Self.summaryFingerprintsKey)
let expiringSnapshot = CodexRateLimitResetCreditsSnapshot(
credits: expiringCredits,
availableCount: expiringCredits.count,
updatedAt: now)
guard let presentation = CodexResetCreditsPresentation.make(
snapshot: expiringSnapshot,
resetStyle: resetStyle,
now: now)
else {
return
}
self.notificationPoster(
Self.notificationPrefix,
L("Limit Reset Credits"),
presentation.helpText)
}
private static func summaryFingerprint(_ credits: [CodexRateLimitResetCredit]) -> String {
let material = credits.map { credit in
"\(credit.id)\u{1f}\(credit.expiresAt?.timeIntervalSince1970 ?? 0)"
}.joined(separator: "\u{1e}")
return SHA256.hash(data: Data(material.utf8))
.map { String(format: "%02x", $0) }
.joined()
}
}
+596
View File
@@ -0,0 +1,596 @@
import AppKit
import CodexBarCore
import KeyboardShortcuts
import Observation
import QuartzCore
import Security
import SwiftUI
@main
struct CodexBarApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@State private var settings: SettingsStore
@State private var store: UsageStore
@State private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator
@State private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator
private let preferencesSelection: PreferencesSelection
private let account: AccountInfo
init() {
let env = ProcessInfo.processInfo.environment
let storedLevel = CodexBarLog.parseLevel(UserDefaults.standard.string(forKey: "debugLogLevel")) ?? .verbose
let level = CodexBarLog.parseLevel(env["CODEXBAR_LOG_LEVEL"]) ?? storedLevel
CodexBarLog.bootstrapIfNeeded(.init(
destination: .oslog(subsystem: "com.steipete.codexbar"),
level: level,
json: false))
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
let gitCommit = Bundle.main.object(forInfoDictionaryKey: "CodexGitCommit") as? String ?? "unknown"
let buildTimestamp = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String ?? "unknown"
CodexBarLog.logger(LogCategories.app).info(
"CodexBar starting",
metadata: [
"version": version,
"build": build,
"git": gitCommit,
"built": buildTimestamp,
])
KeychainAccessGate.isDisabled = UserDefaults.standard.bool(forKey: "debugDisableKeychainAccess")
KeychainPromptCoordinator.install()
if MainThreadHangWatchdog.isEnabledForCurrentProcess {
MainThreadHangWatchdog.shared.start()
}
let preferencesSelection = PreferencesSelection()
let settings = SettingsStore()
Self.applyLanguagePreference(from: settings)
configureUsageFormatterLocalizationProvider()
let managedCodexAccountCoordinator = ManagedCodexAccountCoordinator()
managedCodexAccountCoordinator.onManagedAccountsDidChange = {
_ = settings.refreshCodexAccountReconciliationAfterManagedAccountsDidChange()
}
_ = settings.persistResolvedCodexActiveSourceCorrectionIfNeeded()
let fetcher = UsageFetcher()
let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL)
let account = fetcher.loadAccountInfo()
let store = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: settings)
let codexAccountPromotionCoordinator = CodexAccountPromotionCoordinator(
settingsStore: settings,
usageStore: store,
managedAccountCoordinator: managedCodexAccountCoordinator)
self.preferencesSelection = preferencesSelection
_settings = State(wrappedValue: settings)
_store = State(wrappedValue: store)
_managedCodexAccountCoordinator = State(wrappedValue: managedCodexAccountCoordinator)
_codexAccountPromotionCoordinator = State(wrappedValue: codexAccountPromotionCoordinator)
self.account = account
CodexBarLog.setLogLevel(settings.debugLogLevel)
self.appDelegate.configure(.init(
store: store,
settings: settings,
account: account,
selection: preferencesSelection,
managedCodexAccountCoordinator: managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: codexAccountPromotionCoordinator))
}
@SceneBuilder
var body: some Scene {
// Hidden 1×1 window to keep SwiftUI's lifecycle alive so `Settings` scene
// shows the native toolbar tabs even though the UI is AppKit-based.
WindowGroup("CodexBarLifecycleKeepalive") {
HiddenWindowView()
}
.defaultSize(width: 20, height: 20)
.windowStyle(.hiddenTitleBar)
Settings {
PreferencesView(
settings: self.settings,
store: self.store,
updater: self.appDelegate.updaterController,
selection: self.preferencesSelection,
managedCodexAccountCoordinator: self.managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator,
runProviderLoginFlow: { provider in
await self.appDelegate.runProviderLoginFlow(provider)
})
}
.defaultSize(width: SettingsPane.windowWidth, height: SettingsPane.windowHeight)
.windowResizability(.contentMinSize)
}
private func openSettings(pane: SettingsPane) {
self.preferencesSelection.pane = pane
NSApp.activate(ignoringOtherApps: true)
_ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
private static func applyLanguagePreference(from settings: SettingsStore) {
AppLanguagePreferenceMigration.clearLegacyOverrideIfOwned(storedAppLanguage: settings.appLanguage)
resetCodexBarLocalizationCache()
}
}
// MARK: - Updater abstraction
@MainActor
protocol UpdaterProviding: AnyObject {
var automaticallyChecksForUpdates: Bool { get set }
var automaticallyDownloadsUpdates: Bool { get set }
var isAvailable: Bool { get }
var unavailableReason: String? { get }
var updateStatus: UpdateStatus { get }
func checkForUpdates(_ sender: Any?)
func installUpdate()
}
/// No-op updater used for debug builds and non-bundled runs to suppress Sparkle dialogs.
final class DisabledUpdaterController: UpdaterProviding {
var automaticallyChecksForUpdates: Bool = false
var automaticallyDownloadsUpdates: Bool = false
let isAvailable: Bool = false
let unavailableReason: String?
let updateStatus = UpdateStatus()
init(unavailableReason: String? = nil) {
self.unavailableReason = unavailableReason
}
func checkForUpdates(_ sender: Any?) {}
func installUpdate() {}
}
@MainActor
@Observable
final class UpdateStatus {
static let disabled = UpdateStatus()
var isUpdateReady: Bool
init(isUpdateReady: Bool = false) {
self.isUpdateReady = isUpdateReady
}
}
#if canImport(Sparkle) && ENABLE_SPARKLE
import Sparkle
@MainActor
final class SparkleUpdaterController: NSObject, UpdaterProviding, SPUUpdaterDelegate {
private final class ImmediateInstallHandler: @unchecked Sendable {
private let handler: () -> Void
init(_ handler: @escaping () -> Void) {
self.handler = handler
}
func install() {
self.handler()
}
}
private lazy var controller = SPUStandardUpdaterController(
startingUpdater: false,
updaterDelegate: self,
userDriverDelegate: nil)
let updateStatus = UpdateStatus()
let unavailableReason: String? = nil
private var immediateInstallHandler: ImmediateInstallHandler?
init(savedAutoUpdate: Bool) {
super.init()
let updater = self.controller.updater
updater.automaticallyChecksForUpdates = savedAutoUpdate
updater.automaticallyDownloadsUpdates = savedAutoUpdate
self.controller.startUpdater()
}
var automaticallyChecksForUpdates: Bool {
get { self.controller.updater.automaticallyChecksForUpdates }
set { self.controller.updater.automaticallyChecksForUpdates = newValue }
}
var automaticallyDownloadsUpdates: Bool {
get { self.controller.updater.automaticallyDownloadsUpdates }
set { self.controller.updater.automaticallyDownloadsUpdates = newValue }
}
var isAvailable: Bool {
true
}
func checkForUpdates(_ sender: Any?) {
self.controller.checkForUpdates(sender)
}
func installUpdate() {
guard let immediateInstallHandler else {
self.controller.checkForUpdates(nil)
return
}
immediateInstallHandler.install()
}
nonisolated func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
_ = updater
_ = item
}
nonisolated func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
_ = updater
_ = item
_ = error
Task { @MainActor in
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
}
}
nonisolated func userDidCancelDownload(_ updater: SPUUpdater) {
_ = updater
Task { @MainActor in
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
}
}
nonisolated func updater(
_ updater: SPUUpdater,
willInstallUpdateOnQuit item: SUAppcastItem,
immediateInstallationBlock immediateInstallHandler: @escaping () -> Void)
-> Bool
{
_ = updater
_ = item
let installHandler = ImmediateInstallHandler(immediateInstallHandler)
Task { @MainActor in
self.immediateInstallHandler = installHandler
self.updateStatus.isUpdateReady = true
}
return true
}
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
_ = updater
_ = error
Task { @MainActor in
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
}
}
nonisolated func updater(
_ updater: SPUUpdater,
userDidMake choice: SPUUserUpdateChoice,
forUpdate updateItem: SUAppcastItem,
state: SPUUserUpdateState)
{
let downloaded = state.stage == .downloaded
Task { @MainActor in
switch choice {
case .install, .skip:
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
case .dismiss:
self.updateStatus.isUpdateReady = downloaded
@unknown default:
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
}
}
}
nonisolated func allowedChannels(for updater: SPUUpdater) -> Set<String> {
UpdateChannel.current.allowedSparkleChannels
}
}
private func isDeveloperIDSigned(bundleURL: URL) -> Bool {
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess,
let code = staticCode else { return false }
var infoCF: CFDictionary?
guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &infoCF) == errSecSuccess,
let info = infoCF as? [String: Any],
let certs = info[kSecCodeInfoCertificates as String] as? [SecCertificate],
let leaf = certs.first else { return false }
if let summary = SecCertificateCopySubjectSummary(leaf) as String? {
return summary.hasPrefix("Developer ID Application:")
}
return false
}
@MainActor
private func makeUpdaterController() -> UpdaterProviding {
let bundleURL = Bundle.main.bundleURL
let isBundledApp = bundleURL.pathExtension == "app"
guard isBundledApp else {
return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.")
}
if InstallOrigin.isHomebrewCask(appBundleURL: bundleURL) {
return DisabledUpdaterController(
unavailableReason: "Updates managed by Homebrew. Run: brew upgrade --cask steipete/tap/codexbar")
}
guard isDeveloperIDSigned(bundleURL: bundleURL) else {
return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.")
}
let defaults = UserDefaults.standard
let autoUpdateKey = "autoUpdateEnabled"
// Default to true for first launch; fall back to saved preference thereafter.
let savedAutoUpdate = (defaults.object(forKey: autoUpdateKey) as? Bool) ?? true
return SparkleUpdaterController(savedAutoUpdate: savedAutoUpdate)
}
#else
private func makeUpdaterController() -> UpdaterProviding {
DisabledUpdaterController()
}
#endif
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
struct Dependencies {
let store: UsageStore
let settings: SettingsStore
let account: AccountInfo
let selection: PreferencesSelection
let managedCodexAccountCoordinator: ManagedCodexAccountCoordinator
let codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator
}
let updaterController: UpdaterProviding = makeUpdaterController()
private let confettiOverlayController = ScreenConfettiOverlayController()
private let confettiLogger = CodexBarLog.logger(LogCategories.confetti)
private lazy var memoryPressureMonitor = MemoryPressureMonitor(trimAppCaches: { [weak self] in
self?.trimRebuildableCachesForMemoryPressure() ?? MemoryPressureCacheTrimSummary()
})
private var statusController: StatusItemControlling?
private var store: UsageStore?
private var settings: SettingsStore?
private var account: AccountInfo?
private var preferencesSelection: PreferencesSelection?
private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator?
private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator?
private var hasInstalledLimitResetObservers = false
#if DEBUG
private var debugMemoryPressureObserver: NSObjectProtocol?
#endif
var terminateActiveProcessesForAppShutdown: () -> Void = {
TTYCommandRunner.terminateActiveProcessesForAppShutdown()
}
func configure(_ dependencies: Dependencies) {
self.store = dependencies.store
self.settings = dependencies.settings
self.account = dependencies.account
self.preferencesSelection = dependencies.selection
self.managedCodexAccountCoordinator = dependencies.managedCodexAccountCoordinator
self.codexAccountPromotionCoordinator = dependencies.codexAccountPromotionCoordinator
}
func applicationWillFinishLaunching(_ notification: Notification) {
self.configureAppIconForMacOSVersion()
}
func applicationDidFinishLaunching(_ notification: Notification) {
AppNotifications.shared.requestAuthorizationOnStartup()
self.memoryPressureMonitor.start()
#if DEBUG
self.installDebugMemoryPressureObserverIfNeeded()
#endif
self.ensureStatusController()
KeyboardShortcuts.onKeyUp(for: .openMenu) { [weak self] in
// KeyboardShortcuts dispatches both normal and menu-tracking hotkeys on the main event loop.
MainActor.assumeIsolated {
self?.statusController?.openMenuFromShortcut()
}
}
if !self.hasInstalledLimitResetObservers {
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleSessionLimitResetNotification(_:)),
name: .codexbarSessionLimitReset,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleWeeklyLimitResetNotification(_:)),
name: .codexbarWeeklyLimitReset,
object: nil)
self.hasInstalledLimitResetObservers = true
}
}
func applicationWillTerminate(_ notification: Notification) {
self.memoryPressureMonitor.stop()
#if DEBUG
self.removeDebugMemoryPressureObserver()
#endif
self.statusController?.prepareForAppShutdown()
self.confettiOverlayController.dismiss()
self.dismissAppKitWindowsForShutdown()
self.terminateActiveProcessesForAppShutdown()
}
func runProviderLoginFlow(_ provider: UsageProvider) async {
self.ensureStatusController()
guard let statusController else { return }
await statusController.runLoginFlowFromSettings(provider: provider)
}
@objc private func handleSessionLimitResetNotification(_ notification: Notification) {
guard let event = notification.object as? SessionLimitResetEvent else { return }
guard self.settings?.confettiOnSessionLimitResetsEnabled == true else { return }
self.playLimitResetConfetti(
provider: event.provider,
accountIdentifier: event.accountIdentifier,
resetKind: "session")
}
@objc private func handleWeeklyLimitResetNotification(_ notification: Notification) {
guard let event = notification.object as? WeeklyLimitResetEvent else { return }
guard self.settings?.confettiOnWeeklyLimitResetsEnabled == true else { return }
self.playLimitResetConfetti(
provider: event.provider,
accountIdentifier: event.accountIdentifier,
resetKind: "weekly")
}
private func playLimitResetConfetti(
provider: UsageProvider,
accountIdentifier: String,
resetKind: String)
{
let origin = self.statusController?.celebrationOriginPoint(for: provider)
self.confettiLogger.info(
"Triggering confetti",
metadata: [
"provider": provider.rawValue,
"accountIdentifier": accountIdentifier,
"resetKind": resetKind,
"originKnown": origin == nil ? "0" : "1",
])
self.confettiOverlayController.play(originInScreen: origin)
}
/// Use the classic (non-Liquid Glass) app icon on macOS versions before 26.
private func configureAppIconForMacOSVersion() {
if #unavailable(macOS 26) {
self.applyClassicAppIcon()
}
}
private func applyClassicAppIcon() {
guard let classicIcon = Self.loadClassicIcon() else { return }
NSApp.applicationIconImage = classicIcon
}
private static func loadClassicIcon() -> NSImage? {
guard let url = self.classicIconURL(),
let image = NSImage(contentsOf: url)
else {
return nil
}
return image
}
private static func classicIconURL() -> URL? {
Bundle.main.url(forResource: "Icon-classic", withExtension: "icns")
}
private func dismissAppKitWindowsForShutdown() {
guard let app = NSApp else { return }
for window in app.windows {
window.orderOut(nil)
}
}
private func ensureStatusController() {
if self.statusController != nil { return }
if let store,
let settings,
let account,
let selection = self.preferencesSelection,
let managedCodexAccountCoordinator,
let codexAccountPromotionCoordinator
{
self.statusController = StatusItemController.factory(
store,
settings,
account,
self.updaterController,
selection,
managedCodexAccountCoordinator,
codexAccountPromotionCoordinator)
return
}
// Defensive fallback: this should not be hit in normal app lifecycle.
CodexBarLog.logger(LogCategories.app)
.error("StatusItemController fallback path used; settings/store mismatch likely.")
assertionFailure("StatusItemController fallback path used; check app lifecycle wiring.")
let fallbackSettings = SettingsStore()
let fetcher = UsageFetcher()
let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL)
let fallbackAccount = fetcher.loadAccountInfo()
let fallbackStore = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: fallbackSettings)
let fallbackManagedCodexAccountCoordinator = ManagedCodexAccountCoordinator()
let fallbackCodexAccountPromotionCoordinator = CodexAccountPromotionCoordinator(
settingsStore: fallbackSettings,
usageStore: fallbackStore,
managedAccountCoordinator: fallbackManagedCodexAccountCoordinator)
self.statusController = StatusItemController.factory(
fallbackStore,
fallbackSettings,
fallbackAccount,
self.updaterController,
PreferencesSelection(),
fallbackManagedCodexAccountCoordinator,
fallbackCodexAccountPromotionCoordinator)
}
private func trimRebuildableCachesForMemoryPressure() -> MemoryPressureCacheTrimSummary {
var summary = MemoryPressureCacheTrimSummary()
let statusSummary = self.statusController?.trimRebuildableCachesForMemoryPressure()
?? MemoryPressureCacheTrimSummary()
let storeSummary = self.store?.trimRebuildableCachesForMemoryPressure()
?? MemoryPressureCacheTrimSummary()
summary.merge(statusSummary)
summary.merge(storeSummary)
return summary
}
#if DEBUG
private func installDebugMemoryPressureObserverIfNeeded() {
guard self.debugMemoryPressureObserver == nil else { return }
self.debugMemoryPressureObserver = DistributedNotificationCenter.default().addObserver(
forName: .codexbarDebugSimulateMemoryPressure,
object: nil,
queue: .main)
{ [weak self] notification in
let rawLevel = notification.userInfo?["level"] as? String
let shouldSeedCaches = notification.userInfo?["seedCaches"] as? String == "1"
MainActor.assumeIsolated {
self?.handleDebugMemoryPressureNotification(
rawLevel: rawLevel,
shouldSeedCaches: shouldSeedCaches)
}
}
}
private func removeDebugMemoryPressureObserver() {
guard let observer = self.debugMemoryPressureObserver else { return }
DistributedNotificationCenter.default().removeObserver(observer)
self.debugMemoryPressureObserver = nil
}
private func handleDebugMemoryPressureNotification(rawLevel: String?, shouldSeedCaches: Bool) {
let isCritical = rawLevel?.caseInsensitiveCompare("critical") == .orderedSame
if shouldSeedCaches {
OpenAIDashboardFetcher.seedCachedWebViewsForMemoryPressureProof()
self.statusController?.seedRebuildableCachesForMemoryPressureProof()
self.store?.seedRebuildableCachesForMemoryPressureProof()
}
CodexBarLog.logger(LogCategories.memoryPressure).info(
"Debug memory pressure notification received",
metadata: [
"level": isCritical ? "critical" : "warning",
"seedCaches": shouldSeedCaches ? "1" : "0",
])
self.memoryPressureMonitor.handleMemoryPressureForTesting(isWarning: !isCritical, isCritical: isCritical)
}
#endif
deinit {
NotificationCenter.default.removeObserver(self)
}
}
@@ -0,0 +1,373 @@
import CodexBarCore
import Foundation
struct CodexBarConfigMigrator {
struct LegacyStores {
let zaiTokenStore: any ZaiTokenStoring
let syntheticTokenStore: any SyntheticTokenStoring
let codexCookieStore: any CookieHeaderStoring
let claudeCookieStore: any CookieHeaderStoring
let cursorCookieStore: any CookieHeaderStoring
let opencodeCookieStore: any CookieHeaderStoring
let factoryCookieStore: any CookieHeaderStoring
let minimaxCookieStore: any MiniMaxCookieStoring
let minimaxAPITokenStore: any MiniMaxAPITokenStoring
let kimiTokenStore: any KimiTokenStoring
let kimiK2TokenStore: any KimiK2TokenStoring
let augmentCookieStore: any CookieHeaderStoring
let ampCookieStore: any CookieHeaderStoring
let copilotTokenStore: any CopilotTokenStoring
let tokenAccountStore: any ProviderTokenAccountStoring
}
private static let legacyMigrationCompletedKey = "codexbar.legacySecretsMigrationCompleted"
private struct MigrationState {
var didUpdate = false
var sawLegacySecrets = false
var sawLegacyAccounts = false
}
static func loadOrMigrate(
configStore: CodexBarConfigStore,
userDefaults: UserDefaults,
stores: LegacyStores) -> CodexBarConfig
{
let log = CodexBarLog.logger(LogCategories.configMigration)
let existing = try? configStore.load()
var config = (existing ?? CodexBarConfig.makeDefault()).normalized()
var state = MigrationState()
// applyLegacyCookieSources reads only UserDefaults cheap, runs unconditionally so
// newly-added cookie-source keys are picked up on every launch.
self.applyLegacyCookieSources(userDefaults: userDefaults, config: &config, state: &state)
let migrationCompleted = userDefaults.bool(forKey: Self.legacyMigrationCompletedKey)
if !migrationCompleted {
// Run once: migrate Keychain/file secrets then clear them. Using a completion flag rather
// than `existing == nil` ensures a crash between config-save and clearLegacyStores can
// finish cleanup on the next launch without re-doing the (already-saved) data migration.
if existing == nil {
self.applyLegacyOrderAndToggles(userDefaults: userDefaults, config: &config, state: &state)
}
self.migrateLegacySecrets(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
self.migrateLegacyAccounts(stores: stores, config: &config, state: &state)
}
var didPersistUpdates = true
if state.didUpdate {
do {
try configStore.save(config)
} catch {
didPersistUpdates = false
log.error("Failed to persist config: \(error)")
}
}
guard didPersistUpdates else {
return config.normalized()
}
if state.sawLegacySecrets || state.sawLegacyAccounts {
let cleared = self.clearLegacyStores(stores: stores, sawAccounts: state.sawLegacyAccounts, log: log)
if cleared {
userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey)
}
} else if !migrationCompleted {
userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey)
}
return config.normalized()
}
private static func applyLegacyOrderAndToggles(
userDefaults: UserDefaults,
config: inout CodexBarConfig,
state: inout MigrationState)
{
if let order = userDefaults.stringArray(forKey: "providerOrder"), !order.isEmpty {
config = self.applyProviderOrder(order, config: config)
state.didUpdate = true
}
let toggles = userDefaults.dictionary(forKey: "providerToggles") as? [String: Bool] ?? [:]
if !toggles.isEmpty {
config = self.applyProviderToggles(toggles, config: config)
state.didUpdate = true
}
}
private static func migrateLegacySecrets(
userDefaults: UserDefaults,
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
self.migrateTokenProviders(
[
(.zai, stores.zaiTokenStore.loadToken),
(.synthetic, stores.syntheticTokenStore.loadToken),
(.copilot, stores.copilotTokenStore.loadToken),
(.kimik2, stores.kimiK2TokenStore.loadToken),
],
config: &config,
state: &state)
self.migrateCookieProviders(
[
(.codex, stores.codexCookieStore.loadCookieHeader),
(.claude, stores.claudeCookieStore.loadCookieHeader),
(.cursor, stores.cursorCookieStore.loadCookieHeader),
(.factory, stores.factoryCookieStore.loadCookieHeader),
(.augment, stores.augmentCookieStore.loadCookieHeader),
(.amp, stores.ampCookieStore.loadCookieHeader),
],
config: &config,
state: &state)
self.migrateMiniMax(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
self.migrateKimi(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
self.migrateOpenCode(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
}
private static func applyLegacyCookieSources(
userDefaults: UserDefaults,
config: inout CodexBarConfig,
state: inout MigrationState)
{
let sources: [(UsageProvider, String)] = [
(.codex, "codexCookieSource"),
(.claude, "claudeCookieSource"),
(.cursor, "cursorCookieSource"),
(.opencode, "opencodeCookieSource"),
(.factory, "factoryCookieSource"),
(.minimax, "minimaxCookieSource"),
(.kimi, "kimiCookieSource"),
(.augment, "augmentCookieSource"),
(.amp, "ampCookieSource"),
]
for (provider, key) in sources {
guard let raw = userDefaults.string(forKey: key),
let source = ProviderCookieSource(rawValue: raw)
else { continue }
self.updateProvider(provider, config: &config, state: &state) { entry in
guard entry.cookieSource == nil else { return false }
entry.cookieSource = source
return true
}
}
if userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool == false {
self.updateProvider(.codex, config: &config, state: &state) { entry in
guard entry.cookieSource == nil else { return false }
entry.cookieSource = .off
return true
}
}
}
private static func migrateTokenProviders(
_ providers: [(UsageProvider, () throws -> String?)],
config: inout CodexBarConfig,
state: inout MigrationState)
{
for (provider, loader) in providers {
let token = try? loader()
if token != nil { state.sawLegacySecrets = true }
self.updateProvider(provider, config: &config, state: &state) { entry in
self.setIfEmpty(&entry.apiKey, token)
}
}
}
private static func migrateCookieProviders(
_ providers: [(UsageProvider, () throws -> String?)],
config: inout CodexBarConfig,
state: inout MigrationState)
{
for (provider, loader) in providers {
let header = try? loader()
if header != nil { state.sawLegacySecrets = true }
self.updateProvider(provider, config: &config, state: &state) { entry in
self.setIfEmpty(&entry.cookieHeader, header)
}
}
}
private static func migrateMiniMax(
userDefaults: UserDefaults,
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
let token = try? stores.minimaxAPITokenStore.loadToken()
let header = try? stores.minimaxCookieStore.loadCookieHeader()
if token != nil || header != nil {
state.sawLegacySecrets = true
}
let regionRaw = userDefaults.string(forKey: "minimaxAPIRegion")
self.updateProvider(.minimax, config: &config, state: &state) { entry in
var changed = false
changed = self.setIfEmpty(&entry.apiKey, token) || changed
if let regionRaw, !regionRaw.isEmpty, entry.region == nil {
entry.region = regionRaw
changed = true
}
changed = self.setIfEmpty(&entry.cookieHeader, header) || changed
return changed
}
}
private static func migrateKimi(
userDefaults: UserDefaults,
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
var token = try? stores.kimiTokenStore.loadToken()
if token?.isEmpty ?? true {
token = userDefaults.string(forKey: "kimiManualCookieHeader")
}
if token != nil { state.sawLegacySecrets = true }
self.updateProvider(.kimi, config: &config, state: &state) { entry in
self.setIfEmpty(&entry.cookieHeader, token)
}
}
private static func migrateOpenCode(
userDefaults: UserDefaults,
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
let header = try? stores.opencodeCookieStore.loadCookieHeader()
if header != nil { state.sawLegacySecrets = true }
let workspaceID = userDefaults.string(forKey: "opencodeWorkspaceID")
self.updateProvider(.opencode, config: &config, state: &state) { entry in
var changed = false
changed = self.setIfEmpty(&entry.cookieHeader, header) || changed
if let workspaceID, !workspaceID.isEmpty, entry.workspaceID == nil {
entry.workspaceID = workspaceID
changed = true
}
return changed
}
}
private static func migrateLegacyAccounts(
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
guard let accounts = try? stores.tokenAccountStore.loadAccounts(), !accounts.isEmpty else { return }
state.sawLegacyAccounts = true
for (provider, data) in accounts where !data.accounts.isEmpty {
self.updateProvider(provider, config: &config, state: &state) { entry in
guard entry.tokenAccounts == nil else { return false }
entry.tokenAccounts = data
return true
}
}
}
private static func updateProvider(
_ provider: UsageProvider,
config: inout CodexBarConfig,
state: inout MigrationState,
mutate: (inout ProviderConfig) -> Bool)
{
guard let index = config.providers.firstIndex(where: { $0.id == provider }) else { return }
var entry = config.providers[index]
let changed = mutate(&entry)
if changed {
config.providers[index] = entry
state.didUpdate = true
}
}
private static func setIfEmpty(_ value: inout String?, _ replacement: String?) -> Bool {
let cleaned = replacement?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let cleaned, !cleaned.isEmpty else { return false }
if value == nil || value?.isEmpty == true {
value = cleaned
return true
}
return false
}
@discardableResult
private static func clearLegacyStores(
stores: LegacyStores,
sawAccounts: Bool,
log: CodexBarLogger) -> Bool
{
var success = true
do {
try stores.zaiTokenStore.storeToken(nil)
try stores.syntheticTokenStore.storeToken(nil)
try stores.copilotTokenStore.storeToken(nil)
try stores.minimaxAPITokenStore.storeToken(nil)
try stores.kimiTokenStore.storeToken(nil)
try stores.kimiK2TokenStore.storeToken(nil)
try stores.codexCookieStore.storeCookieHeader(nil)
try stores.claudeCookieStore.storeCookieHeader(nil)
try stores.cursorCookieStore.storeCookieHeader(nil)
try stores.opencodeCookieStore.storeCookieHeader(nil)
try stores.factoryCookieStore.storeCookieHeader(nil)
try stores.minimaxCookieStore.storeCookieHeader(nil)
try stores.augmentCookieStore.storeCookieHeader(nil)
try stores.ampCookieStore.storeCookieHeader(nil)
} catch {
log.error("Failed to clear legacy secrets: \(error)")
success = false
}
if sawAccounts {
let legacyURL = FileTokenAccountStore.defaultURL()
if FileManager.default.fileExists(atPath: legacyURL.path) {
try? FileManager.default.removeItem(at: legacyURL)
}
}
return success
}
private static func applyProviderOrder(_ raw: [String], config: CodexBarConfig) -> CodexBarConfig {
let configsByID = Dictionary(uniqueKeysWithValues: config.providers.map { ($0.id, $0) })
var seen: Set<UsageProvider> = []
var ordered: [ProviderConfig] = []
ordered.reserveCapacity(config.providers.count)
for rawValue in raw {
guard let provider = UsageProvider(rawValue: rawValue),
let entry = configsByID[provider],
!seen.contains(provider)
else { continue }
seen.insert(provider)
ordered.append(entry)
}
for provider in UsageProvider.allCases where !seen.contains(provider) {
ordered.append(configsByID[provider] ?? ProviderConfig(id: provider))
}
var updated = config
updated.providers = ordered
return updated
}
private static func applyProviderToggles(
_ toggles: [String: Bool],
config: CodexBarConfig) -> CodexBarConfig
{
var updated = config
for index in updated.providers.indices {
let provider = updated.providers[index].id
let meta = ProviderDescriptorRegistry.descriptor(for: provider).metadata
if let value = toggles[meta.cliName] {
updated.providers[index].enabled = value
}
}
return updated
}
}
+190
View File
@@ -0,0 +1,190 @@
import CodexBarCore
import Foundation
import Security
protocol CookieHeaderStoring: Sendable {
func loadCookieHeader() throws -> String?
func storeCookieHeader(_ header: String?) throws
}
enum CookieHeaderStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainCookieHeaderStore: CookieHeaderStoring {
private static let log = CodexBarLog.logger(LogCategories.cookieHeaderStore)
private let service = "com.steipete.CodexBar"
private let account: String
private let promptKind: KeychainPromptContext.Kind
// Cache to reduce keychain access frequency
private nonisolated(unsafe) static var cache: [String: CachedValue] = [:]
private static let cacheLock = NSLock()
private static let cacheTTL: TimeInterval = 1800 // 30 minutes
private struct CachedValue {
let value: String?
let timestamp: Date
var isExpired: Bool {
Date().timeIntervalSince(self.timestamp) > KeychainCookieHeaderStore.cacheTTL
}
}
init(account: String, promptKind: KeychainPromptContext.Kind) {
self.account = account
self.promptKind = promptKind
}
func loadCookieHeader() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping cookie load")
return nil
}
// Check cache first
Self.cacheLock.lock()
if let cached = Self.cache[self.account], !cached.isExpired {
Self.cacheLock.unlock()
Self.log.debug("Using cached cookie header for \(self.account)")
return cached.value
}
Self.cacheLock.unlock()
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: self.promptKind,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
// Cache the nil result
Self.cacheLock.lock()
Self.cache[self.account] = CachedValue(value: nil, timestamp: Date())
Self.cacheLock.unlock()
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw CookieHeaderStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw CookieHeaderStoreError.invalidData
}
let header = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
let finalValue = (header?.isEmpty == false) ? header : nil
// Cache the result
Self.cacheLock.lock()
Self.cache[self.account] = CachedValue(value: finalValue, timestamp: Date())
Self.cacheLock.unlock()
return finalValue
}
func storeCookieHeader(_ header: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping cookie store")
return
}
guard let raw = header?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
try self.deleteIfPresent()
// Invalidate cache
Self.cacheLock.lock()
Self.cache.removeValue(forKey: self.account)
Self.cacheLock.unlock()
return
}
guard CookieHeaderNormalizer.normalize(raw) != nil else {
try self.deleteIfPresent()
// Invalidate cache
Self.cacheLock.lock()
Self.cache.removeValue(forKey: self.account)
Self.cacheLock.unlock()
return
}
let data = raw.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
// Update cache
Self.cacheLock.lock()
Self.cache[self.account] = CachedValue(value: raw, timestamp: Date())
Self.cacheLock.unlock()
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw CookieHeaderStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw CookieHeaderStoreError.keychainStatus(addStatus)
}
// Update cache
Self.cacheLock.lock()
Self.cache[self.account] = CachedValue(value: raw, timestamp: Date())
Self.cacheLock.unlock()
}
private func deleteIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
// Invalidate cache
Self.cacheLock.lock()
Self.cache.removeValue(forKey: self.account)
Self.cacheLock.unlock()
return
}
Self.log.error("Keychain delete failed: \(status)")
throw CookieHeaderStoreError.keychainStatus(status)
}
}
+128
View File
@@ -0,0 +1,128 @@
import CodexBarCore
import Foundation
import Security
protocol CopilotTokenStoring: Sendable {
func loadToken() throws -> String?
func storeToken(_ token: String?) throws
}
enum CopilotTokenStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainCopilotTokenStore: CopilotTokenStoring {
private static let log = CodexBarLog.logger(LogCategories.copilotTokenStore)
private let service = "com.steipete.CodexBar"
private let account = "copilot-api-token"
func loadToken() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .copilotToken,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw CopilotTokenStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw CopilotTokenStoreError.invalidData
}
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let token, !token.isEmpty {
return token
}
return nil
}
func storeToken(_ token: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token store")
return
}
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned == nil || cleaned?.isEmpty == true {
try self.deleteTokenIfPresent()
return
}
let data = cleaned!.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw CopilotTokenStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw CopilotTokenStoreError.keychainStatus(addStatus)
}
}
private func deleteTokenIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw CopilotTokenStoreError.keychainStatus(status)
}
}
@@ -0,0 +1,909 @@
import Charts
import CodexBarCore
import SwiftUI
@MainActor
struct CostHistoryChartMenuView: View {
typealias DailyEntry = CostUsageDailyReport.Entry
enum AxisLabelPlacement: Equatable {
case hidden
case centered
case edges
}
private struct Point: Identifiable {
let id: String
let date: Date
let costUSD: Double
let totalTokens: Int?
let requestCount: Int?
init(date: Date, costUSD: Double, totalTokens: Int?, requestCount: Int?) {
self.date = date
self.costUSD = costUSD
self.totalTokens = totalTokens
self.requestCount = requestCount
self.id = "\(Int(date.timeIntervalSince1970))-\(costUSD)"
}
}
private struct DetailRow: Identifiable {
let id: String
let title: String
let subtitle: String?
let modeSubtitle: String?
let accentColor: Color
}
private struct DetailContent {
let primary: String
let rows: [DetailRow]
}
private let provider: UsageProvider
private let daily: [DailyEntry]
private let totalCostUSD: Double?
private let currencyCode: String
private let historyDays: Int
private let windowLabel: String?
private let projects: [CostUsageProjectBreakdown]
private let width: CGFloat
@State private var selectedDateKey: String?
init(
provider: UsageProvider,
daily: [DailyEntry],
totalCostUSD: Double?,
currencyCode: String = "USD",
historyDays: Int = 30,
windowLabel: String? = nil,
projects: [CostUsageProjectBreakdown] = [],
width: CGFloat)
{
self.provider = provider
self.daily = daily
self.totalCostUSD = totalCostUSD
self.currencyCode = currencyCode
self.historyDays = max(1, min(365, historyDays))
self.windowLabel = windowLabel
self.projects = projects
self.width = width
}
var body: some View {
let model = Self.makeModel(provider: self.provider, daily: self.daily)
let selectedDateKey = self.selectedDateKey ?? Self.defaultSelectedDateKey(model: model)
VStack(alignment: .leading, spacing: Self.outerSpacing) {
if model.points.isEmpty {
Text(L("No cost history data."))
.font(.footnote)
.foregroundStyle(.secondary)
.accessibilityLabel(L("No cost history data."))
} else {
Chart {
ForEach(model.points) { point in
BarMark(
x: .value(L("Day"), point.date, unit: .day),
y: .value(L("Cost"), point.costUSD))
.foregroundStyle(model.barColor)
}
if let peak = Self.peakPoint(model: model) {
let capStart = max(peak.costUSD - Self.capHeight(maxValue: model.maxCostUSD), 0)
BarMark(
x: .value(L("Day"), peak.date, unit: .day),
yStart: .value(L("Cap start"), capStart),
yEnd: .value(L("Cap end"), peak.costUSD))
.foregroundStyle(Color(nsColor: .systemYellow))
}
}
.chartYAxis {
AxisMarks(position: .leading, values: Self.yAxisTickValues(maxCostUSD: model.maxCostUSD)) { value in
AxisGridLine().foregroundStyle(Color.clear)
AxisTick().foregroundStyle(Color.clear)
AxisValueLabel(centered: false) {
if let raw = value.as(Double.self) {
Text(Self.yAxisCostString(raw, currencyCode: self.currencyCode))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.padding(.leading, 4)
}
}
}
}
.chartXAxis {
AxisMarks(values: model.axisDates) { value in
AxisGridLine().foregroundStyle(Color.clear)
AxisTick().foregroundStyle(Color.clear)
if let date = value.as(Date.self) {
AxisValueLabel(anchor: Self.xAxisLabelAnchor(for: date, axisDates: model.axisDates)) {
Text(date, format: .dateTime.month(.abbreviated).day())
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
}
}
}
}
.chartLegend(.hidden)
.frame(height: Self.chartHeight)
.accessibilityLabel(L("Cost history chart"))
.accessibilityValue(
model.points.isEmpty
? L("No data")
: String(format: L("%d days of cost data"), model.points.count))
.chartOverlay { proxy in
GeometryReader { geo in
ZStack(alignment: .topLeading) {
if let rect = self.selectionBandRect(model: model, proxy: proxy, geo: geo) {
Rectangle()
.fill(Self.selectionBandColor)
.frame(width: rect.width, height: rect.height)
.position(x: rect.midX, y: rect.midY)
.allowsHitTesting(false)
}
MouseLocationReader { location in
self.updateSelection(location: location, model: model, proxy: proxy, geo: geo)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
}
let detail = self.detailContent(selectedDateKey: selectedDateKey, model: model)
VStack(alignment: .leading, spacing: Self.detailSpacing) {
Text(detail.primary)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)
if model.detailViewportRowCount > 0 {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: Self.detailSpacing) {
ForEach(detail.rows) { row in
HStack(alignment: .top, spacing: 8) {
Rectangle()
.fill(row.accentColor)
.frame(
width: 2,
height: Self.accentHeight(
for: row,
rowHeight: model.detailRowHeight))
.padding(.top, 1)
VStack(alignment: .leading, spacing: 1) {
Text(row.title)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailTitleLineHeight, alignment: .leading)
if let subtitle = row.subtitle {
Text(subtitle)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(
height: Self.detailSubtitleLineHeight,
alignment: .leading)
}
if let modeSubtitle = row.modeSubtitle {
Text(modeSubtitle)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(
height: Self.detailSubtitleLineHeight,
alignment: .leading)
}
}
}
.frame(height: model.detailRowHeight, alignment: .leading)
}
}
}
.scrollIndicators(
Self.detailRowsNeedScrolling(itemCount: detail.rows.count) ? .visible : .hidden)
.frame(
height: Self.detailRowsViewportHeight(
rowCount: model.detailViewportRowCount,
rowHeight: model.detailRowHeight),
alignment: .topLeading)
.id(selectedDateKey)
if model.hasDetailOverflow {
Text(Self.detailOverflowHint(itemCount: detail.rows.count) ?? " ")
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.frame(height: Self.detailHintHeight, alignment: .leading)
.accessibilityHidden(!Self.detailRowsNeedScrolling(itemCount: detail.rows.count))
}
}
}
.frame(
height: Self.detailBlockHeight(
rowCount: model.detailViewportRowCount,
hasOverflow: model.hasDetailOverflow,
rowHeight: model.detailRowHeight),
alignment: .topLeading)
}
if let total = self.totalCostUSD {
Text(String(
format: L("Est. total (%@): %@"),
self.windowLabel ?? Self.windowLabel(days: self.historyDays),
self.costString(total)))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.head)
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)
}
if !self.projects.isEmpty {
VStack(alignment: .leading, spacing: Self.projectRowSpacing) {
Text("Projects")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)
ForEach(Array(self.projects.prefix(Self.maxVisibleProjectRows)), id: \.projectRowID) { project in
let visibleSources = Self.visibleProjectSources(project)
VStack(alignment: .leading, spacing: Self.projectSourceSpacing) {
self.projectParentRow(project)
if !visibleSources.isEmpty {
ForEach(
Array(visibleSources.prefix(Self.maxVisibleProjectSourceRows)),
id: \.sourceRowID)
{ source in
self.projectSourceRow(source)
}
let hiddenSourceCount = visibleSources.count - Self.maxVisibleProjectSourceRows
if hiddenSourceCount > 0 {
Text("+ \(hiddenSourceCount) more")
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.padding(.leading, Self.projectSourceIndent)
.frame(height: Self.projectMoreRowHeight, alignment: .leading)
}
}
}
.frame(height: Self.projectEntryHeight(project), alignment: .topLeading)
}
}
.frame(height: Self.projectBlockHeight(projects: self.projects), alignment: .topLeading)
}
}
.padding(.horizontal, 16)
.padding(.vertical, Self.verticalPadding)
.frame(minWidth: self.width, maxWidth: .infinity, alignment: .top)
}
private struct Model {
let points: [Point]
let pointsByDateKey: [String: Point]
let entriesByDateKey: [String: DailyEntry]
let dateKeys: [(key: String, date: Date)]
let axisDates: [Date]
let barColor: Color
let peakKey: String?
let maxCostUSD: Double
let detailViewportRowCount: Int
let hasDetailOverflow: Bool
let detailRowHeight: CGFloat
}
private static let selectionBandColor = Color(nsColor: .labelColor).opacity(0.1)
static let maxVisibleDetailLines = 4
private static let detailPrimaryLineHeight: CGFloat = 16
private static let detailTitleLineHeight: CGFloat = 16
private static let detailSubtitleLineHeight: CGFloat = 13
private static let compactDetailRowHeight: CGFloat = 36
private static let expandedDetailRowHeight: CGFloat = 44
private static let detailSpacing: CGFloat = 6
private static let detailHintHeight: CGFloat = 13
private static let chartHeight: CGFloat = 130
private static let outerSpacing: CGFloat = 10
private static let projectRowHeight: CGFloat = 31
private static let projectRowSpacing: CGFloat = 5
private static let maxVisibleProjectRows = 5
private static let projectSourceRowHeight: CGFloat = 29
private static let projectSourceSpacing: CGFloat = 3
private static let projectSourceIndent: CGFloat = 10
private static let projectMoreRowHeight: CGFloat = 16
private static let maxVisibleProjectSourceRows = 2
static let verticalPadding: CGFloat = 10
static func windowLabel(days: Int) -> String {
if days == 1 {
return L("Today")
}
return String(format: L("Last %d days"), days)
}
private static func accentHeight(for row: DetailRow, rowHeight: CGFloat) -> CGFloat {
row.subtitle == nil && row.modeSubtitle == nil ? 14 : rowHeight
}
private static func capHeight(maxValue: Double) -> Double {
maxValue * 0.05
}
/// Y-axis tick values for the cost chart: 0, mid, max when the range is at
/// $1 or more; 0 and max for smaller ranges; empty for flat/no data so the
/// axis renders no labels.
private static func yAxisTickValues(maxCostUSD: Double) -> [Double] {
guard maxCostUSD > 0 else { return [] }
if maxCostUSD < 1.0 {
return [0, maxCostUSD]
}
return [0, maxCostUSD / 2, maxCostUSD]
}
private static func makeModel(provider: UsageProvider, daily: [DailyEntry]) -> Model {
let sorted = daily.sorted { lhs, rhs in lhs.date < rhs.date }
var points: [Point] = []
points.reserveCapacity(sorted.count)
var pointsByKey: [String: Point] = [:]
pointsByKey.reserveCapacity(sorted.count)
var entriesByKey: [String: DailyEntry] = [:]
entriesByKey.reserveCapacity(sorted.count)
var dateKeys: [(key: String, date: Date)] = []
dateKeys.reserveCapacity(sorted.count)
var peak: (key: String, costUSD: Double)?
var maxCostUSD: Double = 0
var maxDetailRows = 0
var hasModeDetails = false
for entry in sorted {
guard let (costUSD, date) = self.chartPointInput(for: entry) else { continue }
let point = Point(
date: date,
costUSD: costUSD,
totalTokens: entry.totalTokens,
requestCount: entry.requestCount)
points.append(point)
pointsByKey[entry.date] = point
entriesByKey[entry.date] = entry
dateKeys.append((entry.date, date))
let modelBreakdowns = entry.modelBreakdowns ?? []
maxDetailRows = max(maxDetailRows, modelBreakdowns.count)
hasModeDetails = hasModeDetails || modelBreakdowns.contains { Self.hasModeSubtitle($0) }
if let cur = peak {
if costUSD > cur.costUSD {
peak = (entry.date, costUSD)
}
} else {
peak = (entry.date, costUSD)
}
maxCostUSD = max(maxCostUSD, costUSD)
}
let axisDates: [Date] = {
guard let first = dateKeys.first?.date, let last = dateKeys.last?.date else { return [] }
if Calendar.current.isDate(first, inSameDayAs: last) {
return [first]
}
return [first, last]
}()
let barColor = Self.barColor(for: provider)
return Model(
points: points,
pointsByDateKey: pointsByKey,
entriesByDateKey: entriesByKey,
dateKeys: dateKeys,
axisDates: axisDates,
barColor: barColor,
peakKey: maxCostUSD > 0 ? peak?.key : nil,
maxCostUSD: maxCostUSD,
detailViewportRowCount: min(maxDetailRows, self.maxVisibleDetailLines),
hasDetailOverflow: maxDetailRows > self.maxVisibleDetailLines,
detailRowHeight: hasModeDetails ? self.expandedDetailRowHeight : self.compactDetailRowHeight)
}
private static func axisLabelPlacement(for dates: [Date]) -> AxisLabelPlacement {
switch dates.count {
case 0: .hidden
case 1: .centered
default: .edges
}
}
private static func xAxisLabelAnchor(for date: Date, axisDates: [Date]) -> UnitPoint {
switch self.axisLabelPlacement(for: axisDates) {
case .hidden, .centered:
.top
case .edges:
if let first = axisDates.first, Calendar.current.isDate(date, inSameDayAs: first) {
.topLeading
} else if let last = axisDates.last, Calendar.current.isDate(date, inSameDayAs: last) {
.topTrailing
} else {
.top
}
}
}
private static func barColor(for provider: UsageProvider) -> Color {
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
return Color(red: color.red, green: color.green, blue: color.blue)
}
private static func dateFromDayKey(_ key: String) -> Date? {
let parts = key.split(separator: "-")
guard parts.count == 3,
let year = Int(parts[0]),
let month = Int(parts[1]),
let day = Int(parts[2]) else { return nil }
var comps = DateComponents()
comps.calendar = Calendar.current
comps.timeZone = TimeZone.current
comps.year = year
comps.month = month
comps.day = day
comps.hour = 12
return comps.date
}
private static func chartPointInput(for entry: DailyEntry) -> (costUSD: Double, date: Date)? {
guard let costUSD = entry.costUSD, costUSD >= 0 else { return nil }
guard let date = self.dateFromDayKey(entry.date) else { return nil }
return (costUSD, date)
}
private static func peakPoint(model: Model) -> Point? {
guard let key = model.peakKey else { return nil }
return model.pointsByDateKey[key]
}
private static func hasModeSubtitle(_ item: CostUsageDailyReport.ModelBreakdown) -> Bool {
item.standardCostUSD != nil || item.priorityCostUSD != nil
}
private static func detailRowsViewportHeight(rowCount: Int, rowHeight: CGFloat) -> CGFloat {
guard rowCount > 0 else { return 0 }
return CGFloat(rowCount) * rowHeight + CGFloat(rowCount - 1) * self.detailSpacing
}
private static func detailBlockHeight(rowCount: Int, hasOverflow: Bool, rowHeight: CGFloat) -> CGFloat {
guard rowCount > 0 else { return self.detailPrimaryLineHeight }
var height = self.detailPrimaryLineHeight + self.detailSpacing
height += self.detailRowsViewportHeight(rowCount: rowCount, rowHeight: rowHeight)
if hasOverflow {
height += self.detailSpacing + self.detailHintHeight
}
return height
}
private static func projectBlockHeight(projects: [CostUsageProjectBreakdown]) -> CGFloat {
let visibleProjects = Array(projects.prefix(self.maxVisibleProjectRows))
guard !visibleProjects.isEmpty else { return 0 }
return self.detailPrimaryLineHeight
+ self.projectRowSpacing
+ visibleProjects.reduce(CGFloat(0)) { $0 + self.projectEntryHeight($1) }
+ CGFloat(max(visibleProjects.count - 1, 0)) * self.projectRowSpacing
}
private static func projectEntryHeight(_ project: CostUsageProjectBreakdown) -> CGFloat {
let sources = self.visibleProjectSources(project)
guard !sources.isEmpty else { return self.projectRowHeight }
let visibleSources = min(sources.count, self.maxVisibleProjectSourceRows)
let moreRows = sources.count > self.maxVisibleProjectSourceRows ? 1 : 0
return self.projectRowHeight
+ CGFloat(visibleSources) * (self.projectSourceRowHeight + self.projectSourceSpacing)
+ CGFloat(moreRows) * (self.projectMoreRowHeight + self.projectSourceSpacing)
}
static func visibleProjectSources(
_ project: CostUsageProjectBreakdown) -> [CostUsageProjectSourceBreakdown]
{
guard project.sources.count == 1 else { return project.sources }
guard let source = project.sources.first, source.path != project.path else { return [] }
return [source]
}
private static func defaultSelectedDateKey(model: Model) -> String? {
model.dateKeys.last?.key
}
private func selectionBandRect(model: Model, proxy: ChartProxy, geo: GeometryProxy) -> CGRect? {
guard let key = self.selectedDateKey else { return nil }
guard let plotAnchor = proxy.plotFrame else { return nil }
let plotFrame = geo[plotAnchor]
guard let index = model.dateKeys.firstIndex(where: { $0.key == key }) else { return nil }
let date = model.dateKeys[index].date
guard let x = proxy.position(forX: date) else { return nil }
// Use the calendar day slot width so the band stays the same size regardless of data gaps.
let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: date)) ?? (x + 20)
let slotWidth = abs(nextDayX - x)
let barHalfWidth = slotWidth * 0.25 + 2
let left = plotFrame.origin.x + x - barHalfWidth
let right = plotFrame.origin.x + x + barHalfWidth
return CGRect(x: left, y: plotFrame.origin.y, width: right - left, height: plotFrame.height)
}
private func updateSelection(
location: CGPoint?,
model: Model,
proxy: ChartProxy,
geo: GeometryProxy)
{
// Keep the last hovered day selected when the pointer leaves the chart so the adjacent
// model-breakdown scroller remains interactive. The selection resets with the menu view.
guard let location else { return }
guard let plotAnchor = proxy.plotFrame else { return }
let plotFrame = geo[plotAnchor]
guard plotFrame.contains(location) else { return }
let xInPlot = location.x - plotFrame.origin.x
guard let date: Date = proxy.value(atX: xInPlot) else { return }
guard let nearest = self.nearestDateKey(to: date, model: model) else { return }
// Stay on the last selected bar when cursor is in the gap between bars.
if let nearestEntry = model.dateKeys.first(where: { $0.key == nearest }),
let barX = proxy.position(forX: nearestEntry.date)
{
let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: nearestEntry.date)) ??
(barX + 20)
let slotWidth = abs(nextDayX - barX)
guard ChartBarHoverSelection.accepts(
distanceFromBarCenter: abs(location.x - (plotFrame.origin.x + barX)),
barHalfWidth: slotWidth * 0.25 + 2,
selectableCount: model.dateKeys.count)
else { return }
}
if self.selectedDateKey != nearest {
self.selectedDateKey = nearest
}
}
private func projectSummary(_ project: CostUsageProjectBreakdown) -> String {
let cost = project.totalCostUSD
.map { self.costString($0) } ?? ""
guard let totalTokens = project.totalTokens else { return cost }
return "\(cost) · \(L("%@ tokens", UsageFormatter.tokenCountString(totalTokens)))"
}
private func projectParentRow(_ project: CostUsageProjectBreakdown) -> some View {
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 8) {
Text(project.name)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 8)
Text(self.projectSummary(project))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.head)
}
if let path = project.path {
Text(path)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.middle)
}
}
.frame(height: Self.projectRowHeight, alignment: .leading)
}
private func projectSourceRow(_ source: CostUsageProjectSourceBreakdown) -> some View {
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 6) {
Text(source.name)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 6)
Text(self.projectSourceSummary(source))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.head)
}
if let path = source.path {
Text(path)
.font(.caption2)
.foregroundStyle(Color(nsColor: .quaternaryLabelColor))
.lineLimit(1)
.truncationMode(.middle)
}
}
.padding(.leading, Self.projectSourceIndent)
.frame(height: Self.projectSourceRowHeight, alignment: .leading)
}
private func projectSourceSummary(_ source: CostUsageProjectSourceBreakdown) -> String {
let cost = source.totalCostUSD
.map { self.costString($0) } ?? ""
guard let totalTokens = source.totalTokens else { return cost }
return "\(cost) · \(L("%@ tokens", UsageFormatter.tokenCountString(totalTokens)))"
}
private func nearestDateKey(to date: Date, model: Model) -> String? {
guard !model.dateKeys.isEmpty else { return nil }
var best: (key: String, distance: TimeInterval)?
for entry in model.dateKeys {
let dist = abs(entry.date.timeIntervalSince(date))
if let cur = best {
if dist < cur.distance {
best = (entry.key, dist)
}
} else {
best = (entry.key, dist)
}
}
return best?.key
}
private func detailContent(selectedDateKey: String?, model: Model) -> DetailContent {
guard let key = selectedDateKey,
let point = model.pointsByDateKey[key],
let date = Self.dateFromDayKey(key)
else {
return DetailContent(primary: L("Hover a bar for details"), rows: [])
}
let dayLabel = date.formatted(.dateTime.month(.abbreviated).day())
let cost = self.costString(point.costUSD)
var parts = [cost]
if let tokens = point.totalTokens {
parts.append("\(UsageFormatter.tokenCountString(tokens)) tokens")
}
if let requests = point.requestCount {
parts.append("\(UsageFormatter.tokenCountString(requests)) requests")
}
let primary = "\(dayLabel): \(parts.joined(separator: " · "))"
return DetailContent(primary: primary, rows: self.breakdownRows(key: key, model: model))
}
private func breakdownRows(key: String, model: Model) -> [DetailRow] {
guard let entry = model.entriesByDateKey[key] else { return [] }
guard let breakdown = entry.modelBreakdowns, !breakdown.isEmpty else { return [] }
return Self.orderedBreakdownItems(breakdown)
.enumerated()
.map { index, item in
DetailRow(
id: "\(item.modelName)-\(index)",
title: UsageFormatter.modelDisplayName(item.modelName),
subtitle: self.modelBreakdownTotalSubtitle(item),
modeSubtitle: self.modelBreakdownModeSubtitle(item),
accentColor: model.barColor.opacity(Self.breakdownAccentOpacity(for: index)))
}
}
static func orderedBreakdownItems(
_ breakdown: [CostUsageDailyReport.ModelBreakdown]) -> [CostUsageDailyReport.ModelBreakdown]
{
breakdown.sorted { lhs, rhs in
let lCost = lhs.costUSD ?? -1
let rCost = rhs.costUSD ?? -1
if lCost != rCost {
return lCost > rCost
}
let lTokens = lhs.totalTokens ?? -1
let rTokens = rhs.totalTokens ?? -1
if lTokens != rTokens {
return lTokens > rTokens
}
return lhs.modelName > rhs.modelName
}
}
static func detailViewportRowCount(itemCount: Int) -> Int {
min(max(itemCount, 0), self.maxVisibleDetailLines)
}
static func detailRowsNeedScrolling(itemCount: Int) -> Bool {
itemCount > self.maxVisibleDetailLines
}
static func detailOverflowHint(itemCount: Int) -> String? {
self.detailRowsNeedScrolling(itemCount: itemCount) ? L("Scroll to see more models") : nil
}
private func modelBreakdownTotalSubtitle(_ item: CostUsageDailyReport.ModelBreakdown) -> String? {
UsageFormatter.modelCostDetail(
item.modelName,
costUSD: item.costUSD,
totalTokens: item.totalTokens,
currencyCode: self.currencyCode)
}
private func modelBreakdownModeSubtitle(_ item: CostUsageDailyReport.ModelBreakdown) -> String? {
var parts: [String] = []
if let standardCost = item.standardCostUSD {
var standardPart = "Std \(self.costString(standardCost))"
if let standardTokens = item.standardTokens {
standardPart += " · \(UsageFormatter.tokenCountString(standardTokens))"
}
parts.append(standardPart)
}
if let priorityCost = item.priorityCostUSD {
var priorityPart = "Fast \(self.costString(priorityCost))"
if let priorityTokens = item.priorityTokens {
priorityPart += " · \(UsageFormatter.tokenCountString(priorityTokens))"
}
parts.append(priorityPart)
}
guard !parts.isEmpty else { return nil }
return parts.joined(separator: " / ")
}
private func costString(_ value: Double) -> String {
Self.costString(value, currencyCode: self.currencyCode)
}
private static func costString(_ value: Double, currencyCode: String) -> String {
UsageFormatter.currencyString(value, currencyCode: currencyCode)
}
private static func yAxisCostString(_ value: Double, currencyCode: String) -> String {
UsageFormatter.compactCurrencyString(value, currencyCode: currencyCode)
}
private static func breakdownAccentOpacity(for index: Int) -> Double {
let opacity = 0.75 - (Double(index) * 0.12)
return max(0.3, opacity)
}
}
extension CostHistoryChartMenuView {
struct RenderFingerprint: Equatable {
let currencyCode: String
let historyDays: Int
let windowLabel: String?
let totalCostBitPattern: UInt64?
let hasDailyEntries: Bool
let daily: [VisibleDailyFingerprint]
let projects: [VisibleProjectFingerprint]
}
struct VisibleDailyFingerprint: Equatable {
let date: String
let totalTokens: Int?
let requestCount: Int?
let costBitPattern: UInt64?
let modelBreakdowns: [VisibleModelBreakdownFingerprint]
}
struct VisibleModelBreakdownFingerprint: Equatable {
let modelName: String
let costBitPattern: UInt64?
let totalTokens: Int?
let standardCostBitPattern: UInt64?
let priorityCostBitPattern: UInt64?
let standardTokens: Int?
let priorityTokens: Int?
}
struct VisibleProjectFingerprint: Equatable {
let name: String
let path: String?
let totalTokens: Int?
let totalCostBitPattern: UInt64?
let visibleSourceCount: Int
let sources: [VisibleSourceFingerprint]
}
struct VisibleSourceFingerprint: Equatable {
let name: String
let path: String?
let totalTokens: Int?
let totalCostBitPattern: UInt64?
}
static func renderFingerprint(
from snapshot: CostUsageTokenSnapshot,
provider: UsageProvider) -> RenderFingerprint
{
let projects = provider == .codex ? snapshot.projects : []
return RenderFingerprint(
currencyCode: snapshot.currencyCode,
historyDays: snapshot.historyDays,
windowLabel: snapshot.historyLabel,
totalCostBitPattern: snapshot.last30DaysCostUSD.map(\.bitPattern),
hasDailyEntries: !snapshot.daily.isEmpty,
daily: snapshot.daily
.filter { self.chartPointInput(for: $0) != nil }
.sorted { $0.date < $1.date }
.map(self.visibleDailyFingerprint),
projects: Array(projects.prefix(self.maxVisibleProjectRows)).map { project in
let visibleSources = self.visibleProjectSources(project)
return VisibleProjectFingerprint(
name: project.name,
path: project.path,
totalTokens: project.totalTokens,
totalCostBitPattern: project.totalCostUSD.map(\.bitPattern),
visibleSourceCount: visibleSources.count,
sources: Array(visibleSources.prefix(self.maxVisibleProjectSourceRows)).map { source in
VisibleSourceFingerprint(
name: source.name,
path: source.path,
totalTokens: source.totalTokens,
totalCostBitPattern: source.totalCostUSD.map(\.bitPattern))
})
})
}
private static func visibleDailyFingerprint(_ entry: DailyEntry) -> VisibleDailyFingerprint {
VisibleDailyFingerprint(
date: entry.date,
totalTokens: entry.totalTokens,
requestCount: entry.requestCount,
costBitPattern: entry.costUSD.map(\.bitPattern),
modelBreakdowns: self.orderedBreakdownItems(entry.modelBreakdowns ?? []).map { item in
VisibleModelBreakdownFingerprint(
modelName: item.modelName,
costBitPattern: item.costUSD.map(\.bitPattern),
totalTokens: item.totalTokens,
standardCostBitPattern: item.standardCostUSD.map(\.bitPattern),
priorityCostBitPattern: item.priorityCostUSD.map(\.bitPattern),
standardTokens: item.standardCostUSD == nil ? nil : item.standardTokens,
priorityTokens: item.priorityCostUSD == nil ? nil : item.priorityTokens)
})
}
static func _defaultSelectedDateKeyForTesting(provider: UsageProvider, daily: [DailyEntry]) -> String? {
self.defaultSelectedDateKey(model: self.makeModel(provider: provider, daily: daily))
}
static func _axisDatesForTesting(provider: UsageProvider, daily: [DailyEntry]) -> [Date] {
self.makeModel(provider: provider, daily: daily).axisDates
}
static func _axisLabelPlacementForTesting(
provider: UsageProvider,
daily: [DailyEntry]) -> AxisLabelPlacement
{
self.axisLabelPlacement(for: self.makeModel(provider: provider, daily: daily).axisDates)
}
static func _yAxisTickValuesForTesting(maxCostUSD: Double) -> [Double] {
self.yAxisTickValues(maxCostUSD: maxCostUSD)
}
static func _yAxisCostStringForTesting(_ value: Double, currencyCode: String = "USD") -> String {
self.yAxisCostString(value, currencyCode: currencyCode)
}
static func _detailViewportConfigurationForTesting(
provider: UsageProvider,
daily: [DailyEntry]) -> (rowCount: Int, hasOverflow: Bool, rowHeight: CGFloat)
{
let model = self.makeModel(provider: provider, daily: daily)
return (model.detailViewportRowCount, model.hasDetailOverflow, model.detailRowHeight)
}
}
extension CostUsageProjectBreakdown {
fileprivate var projectRowID: String {
self.path ?? "unknown:\(self.name)"
}
}
extension CostUsageProjectSourceBreakdown {
fileprivate var sourceRowID: String {
self.path ?? "unknown:\(self.name)"
}
}
@@ -0,0 +1,334 @@
import Charts
import CodexBarCore
import SwiftUI
@MainActor
struct CreditsHistoryChartMenuView: View {
private struct Point: Identifiable {
let id: String
let date: Date
let creditsUsed: Double
init(date: Date, creditsUsed: Double) {
self.date = date
self.creditsUsed = creditsUsed
self.id = "\(Int(date.timeIntervalSince1970))-\(creditsUsed)"
}
}
private let breakdown: [OpenAIDashboardDailyBreakdown]
private let width: CGFloat
@State private var selectedDayKey: String?
init(breakdown: [OpenAIDashboardDailyBreakdown], width: CGFloat) {
self.breakdown = breakdown
self.width = width
}
var body: some View {
let model = Self.makeModel(from: self.breakdown)
VStack(alignment: .leading, spacing: 10) {
if model.points.isEmpty {
Text(L("No credits history data."))
.font(.footnote)
.foregroundStyle(.secondary)
.accessibilityLabel(L("No credits history data available."))
} else {
Chart {
ForEach(model.points) { point in
BarMark(
x: .value(L("Day"), point.date, unit: .day),
y: .value(L("Credits used"), point.creditsUsed))
.foregroundStyle(Self.barColor)
}
if let peak = Self.peakPoint(model: model) {
let capStart = max(peak.creditsUsed - Self.capHeight(maxValue: model.maxCreditsUsed), 0)
BarMark(
x: .value(L("Day"), peak.date, unit: .day),
yStart: .value(L("Cap start"), capStart),
yEnd: .value(L("Cap end"), peak.creditsUsed))
.foregroundStyle(Color(nsColor: .systemYellow))
}
}
.chartYAxis(.hidden)
.chartXAxis {
AxisMarks(values: model.axisDates) { _ in
AxisGridLine().foregroundStyle(Color.clear)
AxisTick().foregroundStyle(Color.clear)
AxisValueLabel(format: .dateTime.month(.abbreviated).day())
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
}
}
.chartLegend(.hidden)
.frame(height: 130)
.accessibilityLabel(L("Credits history chart"))
.accessibilityValue(
model.points.isEmpty
? L("No data")
: String(format: L("%d days of credits data"), model.points.count))
.chartOverlay { proxy in
GeometryReader { geo in
ZStack(alignment: .topLeading) {
if let rect = self.selectionBandRect(model: model, proxy: proxy, geo: geo) {
Rectangle()
.fill(Self.selectionBandColor)
.frame(width: rect.width, height: rect.height)
.position(x: rect.midX, y: rect.midY)
.allowsHitTesting(false)
}
MouseLocationReader { location in
self.updateSelection(location: location, model: model, proxy: proxy, geo: geo)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
}
let detail = self.detailLines(model: model)
VStack(alignment: .leading, spacing: 0) {
Text(detail.primary)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: 16, alignment: .leading)
Text(detail.secondary ?? " ")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: 16, alignment: .leading)
.opacity(detail.secondary == nil ? 0 : 1)
}
if let total = model.totalCreditsUsed {
Text(String(
format: L("Total (30d): %@ credits"),
total.formatted(.number.precision(.fractionLength(0...2)))))
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.frame(minWidth: self.width, maxWidth: .infinity, alignment: .leading)
}
private struct Model {
let points: [Point]
let breakdownByDayKey: [String: OpenAIDashboardDailyBreakdown]
let pointsByDayKey: [String: Point]
let dayDates: [(dayKey: String, date: Date)]
let selectableDayDates: [(dayKey: String, date: Date)]
let axisDates: [Date]
let peakKey: String?
let totalCreditsUsed: Double?
let maxCreditsUsed: Double
}
private static let barColor = Color(red: 73 / 255, green: 163 / 255, blue: 176 / 255)
private static let selectionBandColor = Color(nsColor: .labelColor).opacity(0.1)
private static func capHeight(maxValue: Double) -> Double {
maxValue * 0.05
}
private static func makeModel(from breakdown: [OpenAIDashboardDailyBreakdown]) -> Model {
let sorted = breakdown.sorted { lhs, rhs in lhs.day < rhs.day }
var points: [Point] = []
points.reserveCapacity(sorted.count)
var breakdownByDayKey: [String: OpenAIDashboardDailyBreakdown] = [:]
breakdownByDayKey.reserveCapacity(sorted.count)
var pointsByDayKey: [String: Point] = [:]
pointsByDayKey.reserveCapacity(sorted.count)
var dayDates: [(dayKey: String, date: Date)] = []
dayDates.reserveCapacity(sorted.count)
var selectableDayDates: [(dayKey: String, date: Date)] = []
selectableDayDates.reserveCapacity(sorted.count)
var totalCreditsUsed: Double = 0
var peak: (key: String, creditsUsed: Double)?
var maxCreditsUsed: Double = 0
for day in sorted {
guard let date = self.dateFromDayKey(day.day) else { continue }
breakdownByDayKey[day.day] = day
dayDates.append((dayKey: day.day, date: date))
totalCreditsUsed += day.totalCreditsUsed
if day.totalCreditsUsed > 0 {
let point = Point(date: date, creditsUsed: day.totalCreditsUsed)
points.append(point)
pointsByDayKey[day.day] = point
selectableDayDates.append((dayKey: day.day, date: date))
if let cur = peak {
if day.totalCreditsUsed > cur.creditsUsed { peak = (day.day, day.totalCreditsUsed) }
} else {
peak = (day.day, day.totalCreditsUsed)
}
maxCreditsUsed = max(maxCreditsUsed, day.totalCreditsUsed)
}
}
let axisDates: [Date] = {
guard let first = dayDates.first?.date, let last = dayDates.last?.date else { return [] }
if Calendar.current.isDate(first, inSameDayAs: last) { return [first] }
return [first, last]
}()
return Model(
points: points,
breakdownByDayKey: breakdownByDayKey,
pointsByDayKey: pointsByDayKey,
dayDates: dayDates,
selectableDayDates: selectableDayDates,
axisDates: axisDates,
peakKey: peak?.key,
totalCreditsUsed: totalCreditsUsed > 0 ? totalCreditsUsed : nil,
maxCreditsUsed: maxCreditsUsed)
}
private static func dateFromDayKey(_ key: String) -> Date? {
let parts = key.split(separator: "-")
guard parts.count == 3,
let year = Int(parts[0]),
let month = Int(parts[1]),
let day = Int(parts[2])
else {
return nil
}
var comps = DateComponents()
comps.calendar = Calendar.current
comps.timeZone = TimeZone.current
comps.year = year
comps.month = month
comps.day = day
comps.hour = 12
return comps.date
}
private static func peakPoint(model: Model) -> Point? {
guard let key = model.peakKey else { return nil }
return model.pointsByDayKey[key]
}
private func selectionBandRect(model: Model, proxy: ChartProxy, geo: GeometryProxy) -> CGRect? {
guard let key = self.selectedDayKey else { return nil }
guard let plotAnchor = proxy.plotFrame else { return nil }
let plotFrame = geo[plotAnchor]
guard let index = model.dayDates.firstIndex(where: { $0.dayKey == key }) else { return nil }
let date = model.dayDates[index].date
guard let x = proxy.position(forX: date) else { return nil }
if model.dayDates.count <= 1 {
return CGRect(
x: plotFrame.origin.x,
y: plotFrame.origin.y,
width: plotFrame.width,
height: plotFrame.height)
}
// Use the calendar day slot width (always 1 day on the time axis) so the band is the
// same size for every bar regardless of gaps in the data.
let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: date)) ?? (x + 20)
let slotWidth = abs(nextDayX - x)
let barHalfWidth = slotWidth * 0.25 + 2
let left = plotFrame.origin.x + x - barHalfWidth
let right = plotFrame.origin.x + x + barHalfWidth
return CGRect(x: left, y: plotFrame.origin.y, width: right - left, height: plotFrame.height)
}
private func updateSelection(
location: CGPoint?,
model: Model,
proxy: ChartProxy,
geo: GeometryProxy)
{
guard let location else {
if self.selectedDayKey != nil { self.selectedDayKey = nil }
return
}
guard let plotAnchor = proxy.plotFrame else { return }
let plotFrame = geo[plotAnchor]
guard plotFrame.contains(location) else { return }
let xInPlot = location.x - plotFrame.origin.x
guard let date: Date = proxy.value(atX: xInPlot) else { return }
guard let nearest = self.nearestDayKey(to: date, model: model) else { return }
// Stay on the last selected bar when cursor is in the gap between bars; only switch
// selection when the cursor is over the bar's own visual body.
// Skip this gate for single-day charts: no gap exists, and selectionBandRect
// already covers the full plot width in that case.
if model.selectableDayDates.count > 1,
let nearestEntry = model.selectableDayDates.first(where: { $0.dayKey == nearest }),
let barX = proxy.position(forX: nearestEntry.date)
{
let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: nearestEntry.date)) ??
(barX + 20)
let slotWidth = abs(nextDayX - barX)
guard ChartBarHoverSelection.accepts(
distanceFromBarCenter: abs(location.x - (plotFrame.origin.x + barX)),
barHalfWidth: slotWidth * 0.25 + 2,
selectableCount: model.selectableDayDates.count)
else { return }
}
if self.selectedDayKey != nearest {
self.selectedDayKey = nearest
}
}
private func nearestDayKey(to date: Date, model: Model) -> String? {
guard !model.selectableDayDates.isEmpty else { return nil }
var best: (key: String, distance: TimeInterval)?
for entry in model.selectableDayDates {
let dist = abs(entry.date.timeIntervalSince(date))
if let cur = best {
if dist < cur.distance { best = (entry.dayKey, dist) }
} else {
best = (entry.dayKey, dist)
}
}
return best?.key
}
private func detailLines(model: Model) -> (primary: String, secondary: String?) {
guard let key = self.selectedDayKey,
let day = model.breakdownByDayKey[key],
let date = Self.dateFromDayKey(key)
else {
return (L("Hover a bar for details"), nil)
}
let dayLabel = date.formatted(.dateTime.month(.abbreviated).day())
let total = day.totalCreditsUsed.formatted(.number.precision(.fractionLength(0...2)))
if day.services.isEmpty {
return (String(format: L("%@: %@ credits"), dayLabel, total), nil)
}
if day.services.count <= 1, let first = day.services.first {
let used = first.creditsUsed.formatted(.number.precision(.fractionLength(0...2)))
return (String(format: L("%@: %@ credits"), dayLabel, used), first.service)
}
let services = day.services
.sorted { lhs, rhs in
if lhs.creditsUsed == rhs.creditsUsed { return lhs.service < rhs.service }
return lhs.creditsUsed > rhs.creditsUsed
}
.prefix(3)
.map { "\($0.service) \($0.creditsUsed.formatted(.number.precision(.fractionLength(0...2))))" }
.joined(separator: " · ")
return (String(format: L("%@: %@ credits"), dayLabel, total), services)
}
}
+117
View File
@@ -0,0 +1,117 @@
import AppKit
import CodexBarCore
import Foundation
/// Opens Cursor in the user's browser and waits until the normal browser-cookie importer can read a session.
@MainActor
final class CursorLoginRunner {
enum Phase {
case loading
case waitingLogin
case success
case failed(String)
}
struct Result {
enum Outcome {
case success
case cancelled
case failed(String)
}
let outcome: Outcome
let email: String?
}
typealias SnapshotLoader = @Sendable () async throws -> CursorStatusSnapshot
typealias Sleeper = @Sendable (UInt64) async throws -> Void
typealias SessionCacheResetter = @Sendable () async -> Void
private let loadSnapshot: SnapshotLoader
private let openURL: @MainActor (URL) -> Bool
private let sleeper: Sleeper
private let resetSessionCache: SessionCacheResetter
private let timeout: TimeInterval
private let pollInterval: TimeInterval
private let logger = CodexBarLog.logger(LogCategories.cursorLogin)
static let authURL = URL(string: "https://authenticator.cursor.sh/")!
init(
browserDetection: BrowserDetection,
timeout: TimeInterval = 120,
pollInterval: TimeInterval = 2,
openURL: @escaping @MainActor (URL) -> Bool = { NSWorkspace.shared.open($0) },
loadSnapshot: SnapshotLoader? = nil,
sleeper: @escaping Sleeper = { try await Task.sleep(nanoseconds: $0) },
resetSessionCache: @escaping SessionCacheResetter = {
CookieHeaderCache.clear(provider: .cursor)
CursorSessionStore.shared.clearCookies()
})
{
self.timeout = timeout
self.pollInterval = pollInterval
self.openURL = openURL
self.sleeper = sleeper
self.resetSessionCache = resetSessionCache
self.loadSnapshot = loadSnapshot ?? {
let probe = CursorStatusProbe(browserDetection: browserDetection)
return try await probe.fetch(
allowCachedSessions: false,
allowAppAuthFallback: false)
}
}
func run(onPhaseChange: @escaping @MainActor (Phase) -> Void) async -> Result {
onPhaseChange(.loading)
self.logger.info("Cursor login started")
await self.resetSessionCache()
guard self.openURL(Self.authURL) else {
let message = L("Could not open Cursor login in your browser.")
onPhaseChange(.failed(message))
self.logger.error("Cursor login browser launch failed")
return Result(outcome: .failed(message), email: nil)
}
onPhaseChange(.waitingLogin)
let deadline = Date().addingTimeInterval(self.timeout)
var lastError: Error?
repeat {
if Task.isCancelled {
self.logger.info("Cursor login cancelled")
return Result(outcome: .cancelled, email: nil)
}
do {
let snapshot = try await self.loadSnapshot()
onPhaseChange(.success)
self.logger.info("Cursor login completed", metadata: ["outcome": "success"])
return Result(outcome: .success, email: snapshot.accountEmail)
} catch {
lastError = error
}
guard Date() < deadline else { break }
let delay = UInt64(max(0.1, self.pollInterval) * 1_000_000_000)
try? await self.sleeper(delay)
} while true
let message = Self.timeoutMessage(lastError: lastError)
onPhaseChange(.failed(message))
self.logger.warning("Cursor login timed out", metadata: ["error": message])
return Result(outcome: .failed(message), email: nil)
}
private static func timeoutMessage(lastError: Error?) -> String {
let hint = L("Sign in to cursor.com in your browser, then refresh Cursor in CodexBar.")
guard let lastError else {
return String(format: L("Timed out waiting for Cursor login. %@"), hint)
}
return String(
format: L("Timed out waiting for Cursor login. %@ Last error: %@"),
hint,
lastError.localizedDescription)
}
}
@@ -0,0 +1,23 @@
import Foundation
enum RelativeTimeFormatters {
@MainActor
static func full(locale: Locale) -> RelativeDateTimeFormatter {
let formatter = RelativeDateTimeFormatter()
formatter.locale = locale
formatter.unitsStyle = .full
return formatter
}
}
extension Date {
@MainActor
func relativeDescription(now: Date = .now) -> String {
let seconds = abs(now.timeIntervalSince(self))
if seconds < 15 {
return L("just now")
}
let locale = codexBarLocalizedLocale()
return RelativeTimeFormatters.full(locale: locale).localizedString(for: self, relativeTo: now)
}
}
+96
View File
@@ -0,0 +1,96 @@
import AppKit
import CoreVideo
import Observation
import QuartzCore
/// Minimal display link driver using NSScreen.displayLink on macOS 15+,
/// and CVDisplayLink on macOS 14.
/// Publishes ticks on the main thread at the requested frame rate.
@MainActor
@Observable
final class DisplayLinkDriver {
// Published counter used to drive SwiftUI updates.
var tick: Int = 0
private var displayLink: CADisplayLink?
private var cvDisplayLink: CVDisplayLink?
private var targetInterval: CFTimeInterval = 1.0 / 60.0
private var lastTickTimestamp: CFTimeInterval = 0
private let onTick: (() -> Void)?
init(onTick: (() -> Void)? = nil) {
self.onTick = onTick
}
func start(fps: Double = 12) {
guard self.displayLink == nil, self.cvDisplayLink == nil else { return }
let clampedFps = max(fps, 1)
self.targetInterval = 1.0 / clampedFps
self.lastTickTimestamp = 0
if #available(macOS 15, *), let screen = NSScreen.main {
// NSScreen.displayLink is macOS 15+ only.
let displayLink = screen.displayLink(target: self, selector: #selector(self.step))
let rate = Float(clampedFps)
displayLink.preferredFrameRateRange = CAFrameRateRange(
minimum: rate,
maximum: rate,
preferred: rate)
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
} else {
self.startCVDisplayLink()
}
}
func stop() {
self.displayLink?.invalidate()
self.displayLink = nil
if let cvDisplayLink = self.cvDisplayLink {
CVDisplayLinkStop(cvDisplayLink)
}
self.cvDisplayLink = nil
}
@objc private func step(_: AnyObject) {
self.handleTick()
}
private func handleTick() {
let now = CACurrentMediaTime()
if self.lastTickTimestamp > 0, now - self.lastTickTimestamp < self.targetInterval {
return
}
self.lastTickTimestamp = now
// Safe on main runloop; drives SwiftUI updates.
self.tick &+= 1
self.onTick?()
}
private func startCVDisplayLink() {
var link: CVDisplayLink?
if CVDisplayLinkCreateWithActiveCGDisplays(&link) != kCVReturnSuccess {
return
}
guard let link else { return }
let callback: CVDisplayLinkOutputCallback = { _, _, _, _, _, userInfo in
guard let userInfo else { return kCVReturnSuccess }
let driver = Unmanaged<DisplayLinkDriver>.fromOpaque(userInfo).takeUnretainedValue()
driver.scheduleTick()
return kCVReturnSuccess
}
CVDisplayLinkSetOutputCallback(link, callback, Unmanaged.passUnretained(self).toOpaque())
CVDisplayLinkStart(link)
self.cvDisplayLink = link
}
private nonisolated func scheduleTick() {
Task { @MainActor [weak self] in
self?.handleTick()
}
}
deinit {
Task { @MainActor [weak self] in
self?.stop()
}
}
}
+95
View File
@@ -0,0 +1,95 @@
import AppKit
import CodexBarCore
import Foundation
enum GeminiLoginRunner {
private static let geminiConfigDir = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".gemini")
private static let credentialsFile = "oauth_creds.json"
private static func clearCredentials() {
let fm = FileManager.default
let filesToDelete = [credentialsFile, "google_accounts.json"]
for file in filesToDelete {
let path = self.geminiConfigDir.appendingPathComponent(file)
try? fm.removeItem(at: path)
}
}
struct Result {
enum Outcome {
case success
case missingBinary
case launchFailed(String)
}
let outcome: Outcome
}
static func run(onCredentialsCreated: (@Sendable () -> Void)? = nil) async -> Result {
await Task(priority: .userInitiated) {
let env = ProcessInfo.processInfo.environment
guard let binary = BinaryLocator.resolveGeminiBinary(
env: env,
loginPATH: LoginShellPathCache.shared.current)
else {
return Result(outcome: .missingBinary)
}
// Clear existing credentials before auth (enables clean account switch)
Self.clearCredentials()
// Start watching for credentials file to be created
if let callback = onCredentialsCreated {
Self.watchForCredentials(callback: callback)
}
// Create a temporary shell script that runs gemini (auto-prompts for auth when no creds)
let scriptContent = """
#!/bin/bash
cd ~
"\(binary)"
"""
let tempDir = FileManager.default.temporaryDirectory
let scriptURL = tempDir.appendingPathComponent("gemini_login_\(UUID().uuidString).command")
do {
try scriptContent.write(to: scriptURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path)
let config = NSWorkspace.OpenConfiguration()
config.activates = true
try await NSWorkspace.shared.open(scriptURL, configuration: config)
// Clean up script after Terminal has time to read it
let scriptPath = scriptURL.path
DispatchQueue.global().asyncAfter(deadline: .now() + 10) {
try? FileManager.default.removeItem(atPath: scriptPath)
}
return Result(outcome: .success)
} catch {
return Result(outcome: .launchFailed(error.localizedDescription))
}
}.value
}
/// Watch for credentials file to be created, then call callback once
private static func watchForCredentials(callback: @escaping @Sendable () -> Void, timeout: TimeInterval = 300) {
let credsPath = self.geminiConfigDir.appendingPathComponent(self.credentialsFile).path
DispatchQueue.global(qos: .utility).async {
let startTime = Date()
while Date().timeIntervalSince(startTime) < timeout {
if FileManager.default.fileExists(atPath: credsPath) {
// Small delay to ensure file is fully written
Thread.sleep(forTimeInterval: 0.5)
callback()
return
}
Thread.sleep(forTimeInterval: 1.0)
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
import SwiftUI
struct HiddenWindowView: View {
@Environment(\.openSettings) private var openSettings
var body: some View {
Color.clear
.frame(width: 20, height: 20)
.onReceive(NotificationCenter.default.publisher(for: .codexbarOpenSettings)) { _ in
Task { @MainActor in
self.openSettings()
}
}
.task {
// Migrate keychain items to reduce permission prompts during development (runs off main thread)
await Task.detached(priority: .userInitiated) {
KeychainMigration.migrateIfNeeded()
}.value
}
.onAppear {
if let window = NSApp.windows.first(where: { $0.title == "CodexBarLifecycleKeepalive" }) {
// Make the keepalive window truly invisible and non-interactive.
window.styleMask = [.borderless]
window.collectionBehavior = [.auxiliary, .ignoresCycle, .transient, .canJoinAllSpaces]
window.isExcludedFromWindowsMenu = true
window.level = .floating
window.isOpaque = false
window.alphaValue = 0
window.backgroundColor = .clear
window.hasShadow = false
window.ignoresMouseEvents = true
window.canHide = false
window.setContentSize(NSSize(width: 1, height: 1))
window.setFrameOrigin(NSPoint(x: -5000, y: -5000))
}
}
}
}
+975
View File
@@ -0,0 +1,975 @@
import CodexBarCore
import Foundation
enum HistoricalUsageWindowKind: String, Codable {
case secondary
}
enum HistoricalUsageRecordSource: String, Codable {
case live
case backfill
}
struct HistoricalUsageRecord: Codable {
let v: Int
let provider: UsageProvider
let windowKind: HistoricalUsageWindowKind
let source: HistoricalUsageRecordSource
let accountKey: String?
let sampledAt: Date
let usedPercent: Double
let resetsAt: Date
let windowMinutes: Int
init(
v: Int,
provider: UsageProvider,
windowKind: HistoricalUsageWindowKind,
source: HistoricalUsageRecordSource,
accountKey: String?,
sampledAt: Date,
usedPercent: Double,
resetsAt: Date,
windowMinutes: Int)
{
self.v = v
self.provider = provider
self.windowKind = windowKind
self.source = source
self.accountKey = accountKey
self.sampledAt = sampledAt
self.usedPercent = usedPercent
self.resetsAt = resetsAt
self.windowMinutes = windowMinutes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.v = try container.decodeIfPresent(Int.self, forKey: .v) ?? 1
self.provider = try container.decode(UsageProvider.self, forKey: .provider)
self.windowKind = try container.decode(HistoricalUsageWindowKind.self, forKey: .windowKind)
self.source = try container.decodeIfPresent(HistoricalUsageRecordSource.self, forKey: .source) ?? .live
self.accountKey = try container.decodeIfPresent(String.self, forKey: .accountKey)
self.sampledAt = try container.decode(Date.self, forKey: .sampledAt)
self.usedPercent = try container.decode(Double.self, forKey: .usedPercent)
self.resetsAt = try container.decode(Date.self, forKey: .resetsAt)
self.windowMinutes = try container.decode(Int.self, forKey: .windowMinutes)
}
}
struct HistoricalWeekProfile {
let resetsAt: Date
let windowMinutes: Int
let curve: [Double]
}
struct CodexHistoricalDataset {
static let gridPointCount = 169
let weeks: [HistoricalWeekProfile]
}
actor HistoricalUsageHistoryStore {
private static let schemaVersion = 1
private static let writeInterval: TimeInterval = 30 * 60
private static let writeDeltaThreshold: Double = 1
private static let retentionDays: TimeInterval = 56 * 24 * 60 * 60
private static let minimumWeekSamples = 6
private static let boundaryCoverageWindow: TimeInterval = 24 * 60 * 60
private static let backfillWindowCapWeeks = 8
private static let backfillCalibrationMinimumUsedPercent = 1.0
private static let backfillCalibrationMinimumCredits = 0.001
private static let backfillSampleFractions: [Double] = (0...14).map { Double($0) / 14.0 }
private static let coverageTolerance: TimeInterval = 16 * 60 * 60
private static let resetBucketSeconds: TimeInterval = 5 * 60
private let fileURL: URL
private var records: [HistoricalUsageRecord] = []
private var loaded = false
init(fileURL: URL? = nil) {
self.fileURL = fileURL ?? HistoricalUsageHistoryStore.defaultFileURL()
}
func loadCodexDataset(accountKey: String?) -> CodexHistoricalDataset? {
self.ensureLoaded()
return self.buildDataset(accountKey: accountKey)
}
func loadCodexDataset(
canonicalAccountKey: String?,
canonicalEmailHashKey: String?,
legacyEmailHash: String?,
hasAdjacentMultiAccountVeto: Bool) -> CodexHistoricalDataset?
{
self.ensureLoaded()
return self.buildDataset(
canonicalAccountKey: canonicalAccountKey,
canonicalEmailHashKey: canonicalEmailHashKey,
legacyEmailHash: legacyEmailHash,
hasAdjacentMultiAccountVeto: hasAdjacentMultiAccountVeto)
}
func recordCodexWeekly(
window: RateWindow,
sampledAt: Date = .init(),
accountKey: String?) -> CodexHistoricalDataset?
{
guard let rawResetsAt = window.resetsAt else { return self.loadCodexDataset(accountKey: accountKey) }
guard let windowMinutes = window.windowMinutes, windowMinutes > 0 else {
return self.loadCodexDataset(accountKey: accountKey)
}
self.ensureLoaded()
let resetsAt = Self.normalizeReset(rawResetsAt)
let sample = HistoricalUsageRecord(
v: Self.schemaVersion,
provider: .codex,
windowKind: .secondary,
source: .live,
accountKey: accountKey,
sampledAt: sampledAt,
usedPercent: Self.clamp(window.usedPercent, lower: 0, upper: 100),
resetsAt: resetsAt,
windowMinutes: windowMinutes)
if !self.shouldAccept(sample) {
return self.buildDataset(accountKey: accountKey)
}
self.records.append(sample)
self.pruneOldRecords(now: sampledAt)
self.records.sort { lhs, rhs in
if lhs.sampledAt == rhs.sampledAt {
if lhs.resetsAt == rhs.resetsAt {
return lhs.usedPercent < rhs.usedPercent
}
return lhs.resetsAt < rhs.resetsAt
}
return lhs.sampledAt < rhs.sampledAt
}
self.persist()
return self.buildDataset(accountKey: accountKey)
}
func backfillCodexWeeklyFromUsageBreakdown(
_ breakdown: [OpenAIDashboardDailyBreakdown],
referenceWindow: RateWindow,
now: Date = .init(),
accountKey: String?) -> CodexHistoricalDataset?
{
self.ensureLoaded()
let existingDataset = self.buildDataset(accountKey: accountKey)
guard let rawResetsAt = referenceWindow.resetsAt else { return existingDataset }
guard let windowMinutes = referenceWindow.windowMinutes, windowMinutes > 0 else { return existingDataset }
let resetsAt = Self.normalizeReset(rawResetsAt)
let duration = TimeInterval(windowMinutes) * 60
guard duration > 0 else { return existingDataset }
let windowStart = resetsAt.addingTimeInterval(-duration)
let calibrationEnd = Self.clampDate(now, lower: windowStart, upper: resetsAt)
let dayUsages = Self.parseDayUsages(
from: breakdown,
asOf: calibrationEnd,
fillingFrom: windowStart)
guard !dayUsages.isEmpty else { return existingDataset }
guard let coverageStart = dayUsages.first?.start, let coverageEnd = dayUsages.last?.end else {
return existingDataset
}
guard coverageStart <= windowStart.addingTimeInterval(Self.coverageTolerance) else {
return existingDataset
}
guard coverageEnd >= calibrationEnd.addingTimeInterval(-Self.coverageTolerance) else {
return existingDataset
}
let currentUsedPercent = Self.clamp(referenceWindow.usedPercent, lower: 0, upper: 100)
guard currentUsedPercent >= Self.backfillCalibrationMinimumUsedPercent else { return existingDataset }
let currentCredits = Self.creditsUsed(
from: dayUsages,
between: windowStart,
and: calibrationEnd)
guard currentCredits > Self.backfillCalibrationMinimumCredits else { return existingDataset }
let estimatedCreditsAtLimit = currentCredits / (currentUsedPercent / 100)
guard estimatedCreditsAtLimit.isFinite, estimatedCreditsAtLimit > Self.backfillCalibrationMinimumCredits else {
return existingDataset
}
struct RecordKey: Hashable {
let resetsAt: Date
let sampledAt: Date
let windowMinutes: Int
let accountKey: String?
}
var synthesized: [HistoricalUsageRecord] = []
synthesized.reserveCapacity(Self.backfillWindowCapWeeks * Self.backfillSampleFractions.count)
for weeksBack in 1...Self.backfillWindowCapWeeks {
let reset = Self.normalizeReset(resetsAt.addingTimeInterval(-duration * Double(weeksBack)))
let start = reset.addingTimeInterval(-duration)
guard start >= coverageStart.addingTimeInterval(-Self.coverageTolerance),
reset <= coverageEnd.addingTimeInterval(Self.coverageTolerance)
else {
continue
}
let existingForWeek = self.records.filter {
$0.provider == .codex &&
$0.windowKind == .secondary &&
$0.windowMinutes == windowMinutes &&
$0.accountKey == accountKey &&
$0.resetsAt == reset
}
if Self.isCompleteWeek(samples: existingForWeek, windowStart: start, resetsAt: reset) {
continue
}
var existingRecordKeys = Set(existingForWeek.map {
RecordKey(
resetsAt: $0.resetsAt,
sampledAt: $0.sampledAt,
windowMinutes: $0.windowMinutes,
accountKey: $0.accountKey)
})
let weekCredits = Self.creditsUsed(from: dayUsages, between: start, and: reset)
guard weekCredits > Self.backfillCalibrationMinimumCredits else { continue }
for fraction in Self.backfillSampleFractions {
let sampledAt = start.addingTimeInterval(duration * fraction)
let recordKey = RecordKey(
resetsAt: reset,
sampledAt: sampledAt,
windowMinutes: windowMinutes,
accountKey: accountKey)
guard !existingRecordKeys.contains(recordKey) else { continue }
let cumulativeCredits = Self.creditsUsed(from: dayUsages, between: start, and: sampledAt)
let usedPercent = Self.clamp((cumulativeCredits / estimatedCreditsAtLimit) * 100, lower: 0, upper: 100)
synthesized.append(HistoricalUsageRecord(
v: Self.schemaVersion,
provider: .codex,
windowKind: .secondary,
source: .backfill,
accountKey: accountKey,
sampledAt: sampledAt,
usedPercent: usedPercent,
resetsAt: reset,
windowMinutes: windowMinutes))
existingRecordKeys.insert(recordKey)
}
}
guard !synthesized.isEmpty else { return existingDataset }
self.records.append(contentsOf: synthesized)
self.pruneOldRecords(now: now)
self.records.sort { lhs, rhs in
if lhs.sampledAt == rhs.sampledAt {
if lhs.resetsAt == rhs.resetsAt {
return lhs.usedPercent < rhs.usedPercent
}
return lhs.resetsAt < rhs.resetsAt
}
return lhs.sampledAt < rhs.sampledAt
}
self.persist()
return self.buildDataset(accountKey: accountKey)
}
private func shouldAccept(_ sample: HistoricalUsageRecord) -> Bool {
guard let prior = self.records
.last(where: {
$0.provider == sample.provider &&
$0.windowKind == sample.windowKind &&
$0.accountKey == sample.accountKey &&
$0.windowMinutes == sample.windowMinutes
})
else {
return true
}
if prior.resetsAt != sample.resetsAt { return true }
if sample.sampledAt.timeIntervalSince(prior.sampledAt) >= Self.writeInterval { return true }
if abs(sample.usedPercent - prior.usedPercent) >= Self.writeDeltaThreshold { return true }
return false
}
private func pruneOldRecords(now: Date) {
let cutoff = now.addingTimeInterval(-Self.retentionDays)
self.records.removeAll { $0.sampledAt < cutoff }
}
private func ensureLoaded() {
guard !self.loaded else { return }
self.loaded = true
self.records = self.readRecordsFromDisk()
self.pruneOldRecords(now: .init())
}
private func readRecordsFromDisk() -> [HistoricalUsageRecord] {
guard let data = try? Data(contentsOf: self.fileURL), !data.isEmpty else { return [] }
guard let text = String(data: data, encoding: .utf8) else { return [] }
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
var decoded: [HistoricalUsageRecord] = []
decoded.reserveCapacity(text.count / 80)
for rawLine in text.split(separator: "\n", omittingEmptySubsequences: true) {
let line = String(rawLine).trimmingCharacters(in: .whitespacesAndNewlines)
guard !line.isEmpty, let lineData = line.data(using: .utf8) else { continue }
guard var record = try? decoder.decode(HistoricalUsageRecord.self, from: lineData) else { continue }
record = HistoricalUsageRecord(
v: record.v,
provider: record.provider,
windowKind: record.windowKind,
source: record.source,
accountKey: record.accountKey?.isEmpty == false ? record.accountKey : nil,
sampledAt: record.sampledAt,
usedPercent: Self.clamp(record.usedPercent, lower: 0, upper: 100),
resetsAt: Self.normalizeReset(record.resetsAt),
windowMinutes: record.windowMinutes)
decoded.append(record)
}
return decoded
}
private func persist() {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.sortedKeys]
var lines: [String] = []
lines.reserveCapacity(self.records.count)
for record in self.records {
guard let data = try? encoder.encode(record),
let line = String(data: data, encoding: .utf8)
else {
continue
}
lines.append(line)
}
let payload = (lines.joined(separator: "\n") + "\n").data(using: .utf8) ?? Data()
let directory = self.fileURL.deletingLastPathComponent()
do {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
try payload.write(to: self.fileURL, options: [.atomic])
} catch {
// Best-effort cache file; ignore write failures.
}
}
private func buildDataset(accountKey: String?) -> CodexHistoricalDataset? {
let scoped = self.records.filter { record in
guard Self.isCodexSecondaryRecord(record) else { return false }
if let accountKey {
return record.accountKey == accountKey
}
return record.accountKey == nil
}
return self.buildDataset(from: scoped)
}
private func buildDataset(
canonicalAccountKey: String?,
canonicalEmailHashKey: String?,
legacyEmailHash: String?,
hasAdjacentMultiAccountVeto: Bool) -> CodexHistoricalDataset?
{
guard let canonicalAccountKey else {
return self.buildDataset(accountKey: nil)
}
let shouldIncludeUnscoped = CodexHistoryOwnership.hasStrictSingleAccountContinuity(
scopedRawKeys: Self.scopedRawKeysRelevantToCodexUnscopedHistory(self.records),
targetCanonicalKey: canonicalAccountKey,
canonicalEmailHashKey: canonicalEmailHashKey,
legacyEmailHash: legacyEmailHash,
hasAdjacentMultiAccountVeto: hasAdjacentMultiAccountVeto)
let scoped = self.records.filter { record in
guard Self.isCodexSecondaryRecord(record) else { return false }
guard let rawKey = record.accountKey else {
return shouldIncludeUnscoped
}
let owner = CodexHistoryOwnership.classifyPersistedKey(rawKey, legacyEmailHash: legacyEmailHash)
return CodexHistoryOwnership.belongsToTargetContinuity(
owner,
targetCanonicalKey: canonicalAccountKey,
canonicalEmailHashKey: canonicalEmailHashKey)
}
return self.buildDataset(from: scoped)
}
private func buildDataset(from scoped: [HistoricalUsageRecord]) -> CodexHistoricalDataset? {
struct WeekKey: Hashable {
let resetsAt: Date
let windowMinutes: Int
}
if scoped.isEmpty { return nil }
let grouped = Dictionary(grouping: scoped) {
WeekKey(resetsAt: $0.resetsAt, windowMinutes: $0.windowMinutes)
}
var weeks: [HistoricalWeekProfile] = []
weeks.reserveCapacity(grouped.count)
for (key, samples) in grouped {
let duration = TimeInterval(key.windowMinutes) * 60
guard duration > 0 else { continue }
let windowStart = key.resetsAt.addingTimeInterval(-duration)
guard Self.isCompleteWeek(samples: samples, windowStart: windowStart, resetsAt: key.resetsAt) else {
continue
}
guard let curve = Self.reconstructWeekCurve(
samples: samples,
windowStart: windowStart,
windowDuration: duration,
gridPointCount: CodexHistoricalDataset.gridPointCount)
else {
continue
}
weeks.append(HistoricalWeekProfile(
resetsAt: key.resetsAt,
windowMinutes: key.windowMinutes,
curve: curve))
}
weeks.sort { $0.resetsAt < $1.resetsAt }
if weeks.isEmpty { return nil }
return CodexHistoricalDataset(weeks: weeks)
}
private nonisolated static func isCodexSecondaryRecord(_ record: HistoricalUsageRecord) -> Bool {
record.provider == .codex && record.windowKind == .secondary && record.windowMinutes > 0
}
private static func reconstructWeekCurve(
samples: [HistoricalUsageRecord],
windowStart: Date,
windowDuration: TimeInterval,
gridPointCount: Int) -> [Double]?
{
guard gridPointCount >= 2 else { return nil }
var points = samples.map { sample -> (u: Double, value: Double) in
let offset = sample.sampledAt.timeIntervalSince(windowStart)
let u = Self.clamp(offset / windowDuration, lower: 0, upper: 1)
return (u: u, value: Self.clamp(sample.usedPercent, lower: 0, upper: 100))
}
points.sort { lhs, rhs in
if lhs.u == rhs.u {
return lhs.value < rhs.value
}
return lhs.u < rhs.u
}
guard !points.isEmpty else { return nil }
// Enforce monotonicity on observed samples before interpolation.
var monotonePoints: [(u: Double, value: Double)] = []
monotonePoints.reserveCapacity(points.count)
var runningMax = 0.0
for point in points {
runningMax = max(runningMax, point.value)
monotonePoints.append((u: point.u, value: runningMax))
}
// Anchor reconstructed curves to reset start and end-of-week plateau.
let endValue = monotonePoints.last?.value ?? 0
monotonePoints.append((u: 0, value: 0))
monotonePoints.append((u: 1, value: endValue))
monotonePoints.sort { lhs, rhs in
if lhs.u == rhs.u {
return lhs.value < rhs.value
}
return lhs.u < rhs.u
}
runningMax = 0
for index in monotonePoints.indices {
runningMax = max(runningMax, monotonePoints[index].value)
monotonePoints[index].value = runningMax
}
var curve = Array(repeating: 0.0, count: gridPointCount)
let first = monotonePoints[0]
let last = monotonePoints[monotonePoints.count - 1]
var upperIndex = 1
let denominator = Double(gridPointCount - 1)
for index in 0..<gridPointCount {
let u = Double(index) / denominator
if u <= first.u {
curve[index] = first.value
continue
}
if u >= last.u {
curve[index] = last.value
continue
}
while upperIndex < monotonePoints.count, monotonePoints[upperIndex].u < u {
upperIndex += 1
}
let hi = monotonePoints[min(upperIndex, monotonePoints.count - 1)]
let lo = monotonePoints[max(0, upperIndex - 1)]
if hi.u <= lo.u {
curve[index] = max(lo.value, hi.value)
continue
}
let ratio = Self.clamp((u - lo.u) / (hi.u - lo.u), lower: 0, upper: 1)
curve[index] = lo.value + (hi.value - lo.value) * ratio
}
// Re-enforce monotonicity on reconstructed grid.
var curveMax = 0.0
for index in curve.indices {
curve[index] = Self.clamp(curve[index], lower: 0, upper: 100)
curveMax = max(curveMax, curve[index])
curve[index] = curveMax
}
return curve
}
private static func isCompleteWeek(samples: [HistoricalUsageRecord], windowStart: Date, resetsAt: Date) -> Bool {
guard samples.count >= self.minimumWeekSamples else { return false }
let startBoundary = windowStart.addingTimeInterval(Self.boundaryCoverageWindow)
let endBoundary = resetsAt.addingTimeInterval(-Self.boundaryCoverageWindow)
let hasStartCoverage = samples.contains { sample in
sample.sampledAt >= windowStart && sample.sampledAt <= startBoundary
}
let hasEndCoverage = samples.contains { sample in
sample.sampledAt >= endBoundary && sample.sampledAt <= resetsAt
}
return hasStartCoverage && hasEndCoverage
}
private nonisolated static func scopedRawKeysRelevantToCodexUnscopedHistory(
_ records: [HistoricalUsageRecord]) -> [String]
{
let unscopedRecords = records.filter { record in
Self.isCodexSecondaryRecord(record) && record.accountKey == nil
}
guard let continuityWindow = self.historicalContinuityWindow(for: unscopedRecords) else {
return []
}
return records.compactMap { record in
guard Self.isCodexSecondaryRecord(record),
let accountKey = record.accountKey,
continuityWindow.contains(record.sampledAt)
else {
return nil
}
return accountKey
}
}
private nonisolated static func historicalContinuityWindow(
for records: [HistoricalUsageRecord]) -> ClosedRange<Date>?
{
let sampledDates = records.map(\.sampledAt)
guard let lowerBound = sampledDates.min(),
let upperBound = sampledDates.max()
else {
return nil
}
let expansion = TimeInterval(records.map(\.windowMinutes).max() ?? 0) * 60
return lowerBound.addingTimeInterval(-expansion)...upperBound.addingTimeInterval(expansion)
}
private struct DayUsage {
let start: Date
let end: Date
let creditsUsed: Double
}
private static func parseDayUsages(
from breakdown: [OpenAIDashboardDailyBreakdown],
asOf: Date,
fillingFrom expectedCoverageStart: Date? = nil) -> [DayUsage]
{
var creditsByStart: [Date: Double] = [:]
creditsByStart.reserveCapacity(breakdown.count)
for day in breakdown {
guard let dayStart = Self.dayStart(for: day.day) else { continue }
creditsByStart[dayStart, default: 0] += max(0, day.totalCreditsUsed)
}
let calendar = Self.gregorianCalendar()
var dayUsages: [DayUsage] = []
dayUsages.reserveCapacity(creditsByStart.count)
for (dayStart, credits) in creditsByStart {
guard let nominalEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { continue }
let effectiveEnd: Date = if dayStart <= asOf, asOf < nominalEnd {
asOf
} else {
nominalEnd
}
guard effectiveEnd > dayStart else { continue }
dayUsages.append(DayUsage(start: dayStart, end: effectiveEnd, creditsUsed: credits))
}
dayUsages.sort { lhs, rhs in lhs.start < rhs.start }
return Self.fillMissingZeroUsageDays(
in: dayUsages,
through: asOf,
fillingFrom: expectedCoverageStart)
}
private static func fillMissingZeroUsageDays(
in dayUsages: [DayUsage],
through asOf: Date,
fillingFrom expectedCoverageStart: Date? = nil) -> [DayUsage]
{
guard let firstStart = dayUsages.first?.start else { return [] }
let calendar = Self.gregorianCalendar()
let fillStart: Date = if let expectedCoverageStart {
min(firstStart, calendar.startOfDay(for: expectedCoverageStart))
} else {
firstStart
}
let finalDayStart = calendar.startOfDay(for: asOf)
guard fillStart <= finalDayStart else { return dayUsages }
let creditsByStart = Dictionary(uniqueKeysWithValues: dayUsages.map { ($0.start, $0.creditsUsed) })
let daySpan = max(0, calendar.dateComponents([.day], from: fillStart, to: finalDayStart).day ?? 0)
var filled: [DayUsage] = []
filled.reserveCapacity(daySpan + 1)
var cursor = fillStart
while cursor <= finalDayStart {
guard let nominalEnd = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }
let effectiveEnd: Date = if cursor <= asOf, asOf < nominalEnd {
asOf
} else {
nominalEnd
}
guard effectiveEnd > cursor else { break }
filled.append(DayUsage(
start: cursor,
end: effectiveEnd,
creditsUsed: creditsByStart[cursor] ?? 0))
guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }
cursor = next
}
return filled
}
private static func dayStart(for key: String) -> Date? {
let components = key.split(separator: "-", omittingEmptySubsequences: true)
guard components.count == 3,
let year = Int(components[0]),
let month = Int(components[1]),
let day = Int(components[2])
else {
return nil
}
let calendar = Self.gregorianCalendar()
var dateComponents = DateComponents()
dateComponents.calendar = calendar
dateComponents.timeZone = calendar.timeZone
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.hour = 0
dateComponents.minute = 0
dateComponents.second = 0
return dateComponents.date
}
private static func creditsUsed(from dayUsages: [DayUsage], between start: Date, and end: Date) -> Double {
guard end > start else { return 0 }
var total = 0.0
for day in dayUsages {
if day.end <= start { continue }
if day.start >= end { break }
let overlapStart = max(day.start, start)
let overlapEnd = min(day.end, end)
guard overlapEnd > overlapStart else { continue }
let dayDuration = day.end.timeIntervalSince(day.start)
guard dayDuration > 0 else { continue }
let overlap = overlapEnd.timeIntervalSince(overlapStart)
total += day.creditsUsed * (overlap / dayDuration)
}
return max(0, total)
}
nonisolated static func defaultFileURL() -> URL {
let root = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? FileManager.default.homeDirectoryForCurrentUser
return root
.appendingPathComponent("CodexBar", isDirectory: true)
.appendingPathComponent("usage-history.jsonl", isDirectory: false)
}
private nonisolated static func clamp(_ value: Double, lower: Double, upper: Double) -> Double {
min(upper, max(lower, value))
}
private nonisolated static func clampDate(_ value: Date, lower: Date, upper: Date) -> Date {
min(upper, max(lower, value))
}
private nonisolated static func normalizeReset(_ value: Date) -> Date {
let bucket = Self.resetBucketSeconds
guard bucket > 0 else { return value }
let rounded = (value.timeIntervalSinceReferenceDate / bucket).rounded() * bucket
return Date(timeIntervalSinceReferenceDate: rounded)
}
private nonisolated static func gregorianCalendar() -> Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone.current
return calendar
}
#if DEBUG
nonisolated static func _dayStartForTesting(_ key: String) -> Date? {
self.dayStart(for: key)
}
nonisolated static func _creditsUsedForTesting(
breakdown: [OpenAIDashboardDailyBreakdown],
asOf: Date,
start: Date,
end: Date) -> Double
{
let dayUsages = Self.parseDayUsages(from: breakdown, asOf: asOf)
return Self.creditsUsed(from: dayUsages, between: start, and: end)
}
#endif
}
enum CodexHistoricalPaceEvaluator {
static let minimumCompleteWeeksForHistorical = 3
static let minimumWeeksForRisk = 5
private static let recencyTauWeeks: Double = 3
private static let epsilon: Double = 1e-9
private static let resetBucketSeconds: TimeInterval = 5 * 60
static func evaluate(window: RateWindow, now: Date, dataset: CodexHistoricalDataset?) -> UsagePace? {
guard let dataset else { return nil }
guard let resetsAt = window.resetsAt else { return nil }
let minutes = window.windowMinutes ?? 10080
guard minutes > 0 else { return nil }
let duration = TimeInterval(minutes) * 60
guard duration > 0 else { return nil }
let timeUntilReset = resetsAt.timeIntervalSince(now)
guard timeUntilReset > 0, timeUntilReset <= duration else { return nil }
let normalizedResetsAt = Self.normalizeReset(resetsAt)
let elapsed = Self.clamp(duration - timeUntilReset, lower: 0, upper: duration)
let actual = Self.clamp(window.usedPercent, lower: 0, upper: 100)
if elapsed == 0, actual > 0 { return nil }
let uNow = Self.clamp(elapsed / duration, lower: 0, upper: 1)
let scopedWeeks = dataset.weeks.filter { week in
week.windowMinutes == minutes && week.resetsAt < normalizedResetsAt
}
guard scopedWeeks.count >= Self.minimumCompleteWeeksForHistorical else { return nil }
let weightedWeeks = scopedWeeks.map { week in
let ageWeeks = Self.clamp(
normalizedResetsAt.timeIntervalSince(week.resetsAt) / duration,
lower: 0,
upper: Double.greatestFiniteMagnitude)
let weight = exp(-ageWeeks / Self.recencyTauWeeks)
return (week: week, weight: weight)
}
let totalWeight = weightedWeeks.reduce(0.0) { $0 + $1.weight }
guard totalWeight > Self.epsilon else { return nil }
let totalWeightSquared = weightedWeeks.reduce(0.0) { $0 + ($1.weight * $1.weight) }
let nEff = totalWeightSquared > Self.epsilon ? (totalWeight * totalWeight) / totalWeightSquared : 0
let lambda = Self.clamp((nEff - 2) / 6, lower: 0, upper: 1)
let gridCount = CodexHistoricalDataset.gridPointCount
let denominator = Double(gridCount - 1)
var expectedCurve = Array(repeating: 0.0, count: gridCount)
for index in 0..<gridCount {
let u = Double(index) / denominator
let values = weightedWeeks.map { $0.week.curve[index] }
let weights = weightedWeeks.map(\.weight)
let historicalMedian = Self.weightedMedian(values: values, weights: weights)
let linearBaseline = 100 * u
// Historical demand can exceed a sustainable quota pace. Never call that excess a reserve.
expectedCurve[index] = Self.clamp(
(lambda * historicalMedian) + ((1 - lambda) * linearBaseline),
lower: 0,
upper: linearBaseline)
}
// Expected cumulative usage should be monotone.
var runningExpected = 0.0
for index in expectedCurve.indices {
runningExpected = max(runningExpected, expectedCurve[index])
expectedCurve[index] = runningExpected
}
let expectedNow = Self.interpolate(curve: expectedCurve, at: uNow)
var weightedRunOutMass = 0.0
var crossingCandidates: [(etaSeconds: TimeInterval, weight: Double)] = []
crossingCandidates.reserveCapacity(weightedWeeks.count)
for weighted in weightedWeeks {
var extendedCurve = weighted.week.curve
if let capIndex = extendedCurve.firstIndex(where: { $0 >= 100 - Self.epsilon }),
capIndex > 0, capIndex < extendedCurve.count - 1
{
let gridCount = CodexHistoricalDataset.gridPointCount
let uCap = Double(capIndex) / Double(gridCount - 1)
let valCap = extendedCurve[capIndex]
let slope: Double = valCap / uCap
for i in capIndex..<extendedCurve.count {
let u = Double(i) / Double(gridCount - 1)
extendedCurve[i] = slope * u
}
}
let weight = weighted.weight
let weekNow = Self.interpolate(curve: extendedCurve, at: uNow)
let shift = actual - weekNow
let shiftedEnd = (extendedCurve.last ?? 0) + shift
let runOut = shiftedEnd >= 100 - Self.epsilon
if runOut {
weightedRunOutMass += weight
if let crossingU = Self.firstCrossing(
after: uNow,
curve: extendedCurve,
shift: shift,
actualAtNow: actual)
{
let etaSeconds = max(0, (crossingU - uNow) * duration)
crossingCandidates.append((etaSeconds: etaSeconds, weight: weight))
}
}
}
let smoothedProbability = Self.clamp(
(weightedRunOutMass + 0.5) / (totalWeight + 1),
lower: 0,
upper: 1)
var runOutProbability: Double? = scopedWeeks.count >= Self.minimumWeeksForRisk ? smoothedProbability : nil
var willLastToReset = smoothedProbability < 0.5
var etaSeconds: TimeInterval?
if actual >= 100 {
willLastToReset = false
etaSeconds = 0
runOutProbability = 1
} else if !willLastToReset {
let values = crossingCandidates.map(\.etaSeconds)
let weights = crossingCandidates.map(\.weight)
if values.isEmpty {
willLastToReset = true
} else {
etaSeconds = max(0, Self.weightedMedian(values: values, weights: weights))
}
}
return UsagePace.historical(
expectedUsedPercent: expectedNow,
actualUsedPercent: actual,
etaSeconds: etaSeconds,
willLastToReset: willLastToReset,
runOutProbability: runOutProbability,
projectedRemainingUsage: max(0, (expectedCurve.last ?? expectedNow) - expectedNow))
}
private static func firstCrossing(
after uNow: Double,
curve: [Double],
shift: Double,
actualAtNow: Double) -> Double?
{
let gridCount = curve.count
guard gridCount >= 2 else { return nil }
let denominator = Double(gridCount - 1)
var previousU = uNow
var previousValue = actualAtNow
let startIndex = min(gridCount - 1, max(1, Int(floor(uNow * denominator)) + 1))
for index in startIndex..<gridCount {
let u = Double(index) / denominator
if u <= uNow + Self.epsilon { continue }
let value = Self.clamp(curve[index] + shift, lower: 0, upper: 100)
if previousValue < 100 - Self.epsilon, value >= 100 - Self.epsilon {
let delta = value - previousValue
if abs(delta) <= Self.epsilon { return u }
let ratio = Self.clamp((100 - previousValue) / delta, lower: 0, upper: 1)
return Self.clamp(previousU + ratio * (u - previousU), lower: uNow, upper: 1)
}
previousU = u
previousValue = value
}
return nil
}
private static func interpolate(curve: [Double], at u: Double) -> Double {
guard !curve.isEmpty else { return 0 }
if curve.count == 1 { return curve[0] }
let clipped = Self.clamp(u, lower: 0, upper: 1)
let scaled = clipped * Double(curve.count - 1)
let lower = Int(floor(scaled))
let upper = min(curve.count - 1, lower + 1)
if lower == upper { return curve[lower] }
let ratio = scaled - Double(lower)
return curve[lower] + ((curve[upper] - curve[lower]) * ratio)
}
private static func weightedMedian(values: [Double], weights: [Double]) -> Double {
guard values.count == weights.count, !values.isEmpty else { return 0 }
let pairs = zip(values, weights)
.map { (value: $0, weight: max(0, $1)) }
.sorted { lhs, rhs in lhs.value < rhs.value }
let totalWeight = pairs.reduce(0.0) { $0 + $1.weight }
if totalWeight <= Self.epsilon {
let sortedValues = values.sorted()
return sortedValues[sortedValues.count / 2]
}
let threshold = totalWeight / 2
var cumulative = 0.0
for pair in pairs {
cumulative += pair.weight
if cumulative >= threshold {
return pair.value
}
}
return pairs.last?.value ?? 0
}
private static func clamp(_ value: Double, lower: Double, upper: Double) -> Double {
min(upper, max(lower, value))
}
private static func normalizeReset(_ value: Date) -> Date {
let bucket = Self.resetBucketSeconds
guard bucket > 0 else { return value }
let rounded = (value.timeIntervalSinceReferenceDate / bucket).rounded() * bucket
return Date(timeIntervalSinceReferenceDate: rounded)
}
}
@@ -0,0 +1,152 @@
import CodexBarCore
import Foundation
enum IconRemainingResolver {
private static let visibleZeroPercent = 0.0001
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
// Antigravity quota summaries expose exact 5-hour session and weekly buckets for the compact icon.
private static let sessionWindowMinutes = 5 * 60
private static let weeklyWindowMinutes = 7 * 24 * 60
private static func codexProjection(snapshot: UsageSnapshot, now: Date) -> CodexConsumerProjection {
CodexConsumerProjection.make(
surface: .menuBar,
context: CodexConsumerProjection.Context(
snapshot: snapshot,
rawUsageError: nil,
liveCredits: nil,
rawCreditsError: nil,
liveDashboard: nil,
rawDashboardError: nil,
dashboardAttachmentAuthorized: false,
dashboardRequiresLogin: false,
now: now))
}
private static func codexVisibleWindows(snapshot: UsageSnapshot, now: Date) -> [RateWindow] {
let projection = self.codexProjection(snapshot: snapshot, now: now)
return projection.visibleRateLanes.compactMap { projection.menuBarSelectableRateWindow(for: $0) }
}
private static func antigravityQuotaSummaryWindows(
snapshot: UsageSnapshot)
-> (primary: RateWindow?, secondary: RateWindow?)?
{
let quotaSummaryWindows = snapshot.extraRateWindows?
.filter {
$0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix)
} ?? []
guard !quotaSummaryWindows.isEmpty else { return nil }
return self.antigravityQuotaSummaryPair(in: quotaSummaryWindows.filter(\.usageKnown))
}
private static func antigravityQuotaSummaryPair(
in windows: [NamedRateWindow])
-> (primary: RateWindow?, secondary: RateWindow?)?
{
let session = self.mostConstrainedWindow(in: windows, windowMinutes: Self.sessionWindowMinutes)
let weekly = self.mostConstrainedWindow(in: windows, windowMinutes: Self.weeklyWindowMinutes)
guard session != nil || weekly != nil else { return nil }
return (primary: session, secondary: weekly)
}
/// Returns the highest-usage window for an exact Antigravity compact-icon cadence.
private static func mostConstrainedWindow(in windows: [NamedRateWindow], windowMinutes: Int) -> RateWindow? {
windows
.filter { $0.window.windowMinutes == windowMinutes }
.max { lhs, rhs in
if lhs.window.usedPercent != rhs.window.usedPercent {
return lhs.window.usedPercent < rhs.window.usedPercent
}
// max(by:) keeps the right-hand element when this returns true; use `>` so the smallest id wins ties.
return lhs.id > rhs.id
}?
.window
}
static func resolvedWindows(
snapshot: UsageSnapshot,
style: IconStyle,
secondaryOverrideWindowID: String? = nil,
now: Date = Date())
-> (primary: RateWindow?, secondary: RateWindow?)
{
if style == .perplexity {
let windows = snapshot.orderedPerplexityDisplayWindows()
return (
primary: windows.first,
secondary: windows.dropFirst().first)
}
if style == .antigravity {
// Only current quota-summary buckets define the fixed session/weekly icon lanes.
return self.antigravityQuotaSummaryWindows(snapshot: snapshot)
?? (primary: nil, secondary: nil)
}
if style == .codex {
let windows = self.codexVisibleWindows(snapshot: snapshot, now: now)
return (
primary: windows.first,
secondary: windows.dropFirst().first)
}
if style == .copilot,
let secondaryOverrideWindowID,
let extraWindow = snapshot.extraRateWindows?.first(where: { $0.id == secondaryOverrideWindowID })?.window
{
return (
primary: snapshot.primary,
secondary: extraWindow)
}
return (
primary: snapshot.primary,
secondary: snapshot.secondary)
}
static func resolvedRemaining(
snapshot: UsageSnapshot,
style: IconStyle,
secondaryOverrideWindowID: String? = nil,
now: Date = Date())
-> (primary: Double?, secondary: Double?)
{
let windows = self.resolvedWindows(
snapshot: snapshot,
style: style,
secondaryOverrideWindowID: secondaryOverrideWindowID,
now: now)
return (
primary: windows.primary?.remainingPercent,
secondary: windows.secondary?.remainingPercent)
}
static func resolvedPercents(
snapshot: UsageSnapshot,
style: IconStyle,
showUsed: Bool,
renderingStyle: IconStyle? = nil,
secondaryOverrideWindowID: String? = nil,
now: Date = Date())
-> (primary: Double?, secondary: Double?)
{
let windows = Self.resolvedWindows(
snapshot: snapshot,
style: style,
secondaryOverrideWindowID: secondaryOverrideWindowID,
now: now)
var percents = (
primary: showUsed ? windows.primary?.usedPercent : windows.primary?.remainingPercent,
secondary: showUsed ? windows.secondary?.usedPercent : windows.secondary?.remainingPercent)
// Provider style chooses the usage lanes; rendering style controls renderer-specific layout sentinels.
// Merged icons still resolve Warp's lanes, but render as `.combined` and must keep the real percentage.
if showUsed, style == .warp, (renderingStyle ?? style) == .warp, let secondary = windows.secondary {
if secondary.remainingPercent <= 0 {
// Preserve Warp's exhausted/no-bonus layout even though used percent is 100.
percents.secondary = 0
} else if percents.secondary == 0 {
// A zero fill means "lane absent" to IconRenderer; keep an unused bonus lane visible.
percents.secondary = self.visibleZeroPercent
}
}
return percents
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,914 @@
import CodexBarCore
import SwiftUI
struct InlineUsageDashboardModel: Equatable {
struct KPI: Equatable {
let title: String
let value: String
let emphasis: Bool
}
struct Point: Equatable, Identifiable {
let id: String
let label: String
let value: Double
let accessibilityValue: String
}
enum ValueStyle: Equatable {
case currencyUSD
case currency(symbol: String)
case tokens
case points
}
let accessibilityLabel: String
let valueStyle: ValueStyle
let kpis: [KPI]
let points: [Point]
let detailLines: [String]
/// Provider branding color used to fill the mini usage bars. When nil the bars fall back to a
/// neutral palette derived from `valueStyle`.
var barColor: Color?
/// ISO 4217 currency code for cost dashboards. When non-nil, `MiniUsageBars` shows a max-cost scale label.
/// Nil for token/points dashboards.
var currencyCode: String?
}
extension UsageMenuCardView.Model {
static func apiProviderUsageNotes(input: Input) -> [String]? {
if input.provider == .openai,
let usage = input.snapshot?.openAIAPIUsage
{
return self.openAIAPIUsageNotes(usage)
}
if input.provider == .deepgram,
let usage = input.snapshot?.deepgramUsage
{
return usage.displayLines
}
if input.provider == .clawrouter,
let usage = input.snapshot?.clawRouterUsage
{
var notes = [
"\(UsageFormatter.tokenCountString(usage.requestCount)) \(L("requests")) · " +
"\(UsageFormatter.tokenCountString(usage.totalTokens)) \(L("tokens"))",
]
if usage.errorCount > 0 {
notes.append("\(usage.successCount) succeeded · \(usage.errorCount) failed")
}
if !usage.providers.isEmpty {
let mix = usage.providers.prefix(5)
.map { "\($0.provider): \(UsageFormatter.tokenCountString($0.requestCount))" }
.joined(separator: " · ")
notes.append("Routed providers: \(mix)")
}
return notes
}
if input.provider == .wayfinder,
let usage = input.snapshot?.wayfinderUsage
{
return usage.displayLines
}
if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
let billing = input.snapshot?.minimaxUsage?.billingSummary
{
return [
String(format: L("Today: %@ tokens"), UsageFormatter.tokenCountString(billing.todayTokens)),
String(
format: L("Last 30 days: %@ tokens"),
UsageFormatter.tokenCountString(billing.last30DaysTokens)),
]
}
if input.provider == .deepseek,
input.showOptionalCreditsAndExtraUsage,
let usage = input.snapshot?.deepseekUsage
{
let symbol = usage.currency == "CNY" ? "¥" : "$"
let todayCostStr = usage.todayCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? ""
return [
String(
format: L("Today: %@ · %@ tokens"),
todayCostStr,
UsageFormatter.tokenCountString(usage.todayTokens)),
String(format: L("This month: %@ tokens"), UsageFormatter.tokenCountString(usage.currentMonthTokens)),
]
}
if input.provider == .poe,
let usage = input.snapshot?.poeUsage
{
return self.poeUsageNotes(usage, now: input.now)
}
if input.provider == .ollama,
input.snapshot?.identity?.loginMethod == "API key"
{
return [L("API key verified. Ollama does not expose Cloud quota limits through the API.")]
}
return nil
}
static func openAIAPIUsageNotes(_ usage: OpenAIAPIUsageSnapshot) -> [String] {
let today = usage.currentDay
let seven = usage.last7Days
let thirty = usage.last30Days
let historyLabel = usage.historyWindowLabel
let todayNote = String(
format: L("Today: %@ · %@ tokens"),
UsageFormatter.usdString(today.costUSD),
UsageFormatter.tokenCountString(today.totalTokens))
let sevenDayNote = "7d: \(UsageFormatter.usdString(seven.costUSD)) · " +
"\(UsageFormatter.tokenCountString(seven.requests)) \(L("requests"))"
let thirtyDayNote =
"\(historyLabel): \(UsageFormatter.tokenCountString(thirty.totalTokens)) \(L("tokens")) · " +
"\(UsageFormatter.tokenCountString(thirty.requests)) \(L("requests"))"
var notes: [String] = [
todayNote,
sevenDayNote,
thirtyDayNote,
]
if let topModel = usage.topModels.first {
notes.append("\(L("Top model")): \(topModel.name)")
}
return notes
}
static func poeUsageNotes(
_ usage: PoeUsageHistorySnapshot,
now: Date = Date(),
calendar: Calendar = .current) -> [String]
{
let today = usage.currentDay(now: now, calendar: calendar)
let week = usage.last7Days
let month = usage.last30Days
let todayUSD = today.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let weekUSD = week.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let monthUSD = month.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let todayLine = "Today: \(Self.pointsSummary(today.points)) · " +
"\(UsageFormatter.tokenCountString(today.requests)) \(L("requests"))\(todayUSD)"
let weekLine = "7d: \(Self.pointsSummary(week.points)) · " +
"\(UsageFormatter.tokenCountString(week.requests)) \(L("requests"))\(weekUSD)"
let monthLine = "30d: \(Self.pointsSummary(month.points)) · " +
"\(UsageFormatter.tokenCountString(month.requests)) \(L("requests"))\(monthUSD)"
var notes = [
todayLine,
weekLine,
monthLine,
]
if let topModel = usage.topModels.first {
notes.append("\(L("Top model")): \(topModel.name) (\(Self.pointsSummary(topModel.points)))")
}
if !usage.topUsageTypes.isEmpty {
let mix = usage.topUsageTypes.prefix(2)
.map { "\($0.name): \(Self.pointsSummary($0.points))" }
.joined(separator: " · ")
notes.append("Usage mix: \(mix)")
}
return notes
}
static func inlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? {
guard var model = self.resolveInlineUsageDashboard(input: input) else { return nil }
model.barColor = Self.inlineDashboardBarColor(for: input.provider)
return model
}
/// Provider branding color for the inline usage bars, matching the provider's switcher tab and
/// detailed cost-history chart.
static func inlineDashboardBarColor(for provider: UsageProvider) -> Color {
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
return Color(red: color.red, green: color.green, blue: color.blue)
}
private static func resolveInlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? {
if self.usesProviderCostHistoryAsPrimaryDashboard(input.provider),
let tokenSnapshot = primaryCostHistorySnapshot(input: input),
!tokenSnapshot.daily.isEmpty
{
return self.costHistoryInlineDashboard(
provider: input.provider,
snapshot: tokenSnapshot,
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
}
if input.provider == .claude,
let usage = input.snapshot?.claudeAdminAPIUsage
{
return Self.claudeAdminAPIInlineDashboard(usage)
}
if input.provider == .openrouter,
let usage = input.snapshot?.openRouterUsage
{
return Self.openRouterInlineDashboard(usage)
}
if input.provider == .crossmodel,
let usage = input.snapshot?.crossModelUsage
{
return Self.crossModelInlineDashboard(usage)
}
if input.provider == .zai,
let modelUsage = input.snapshot?.zaiUsage?.modelUsage
{
return Self.zaiInlineDashboard(modelUsage: modelUsage, now: input.now)
}
if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
let billing = input.snapshot?.minimaxUsage?.billingSummary,
!billing.daily.isEmpty
{
return Self.minimaxInlineDashboard(billing)
}
if input.provider == .deepseek,
input.showOptionalCreditsAndExtraUsage,
let usage = input.snapshot?.deepseekUsage,
!usage.daily.isEmpty
{
return Self.deepseekInlineDashboard(usage)
}
if input.provider == .poe,
let usage = input.snapshot?.poeUsage,
!usage.daily.isEmpty
{
return Self.poeInlineDashboard(usage, now: input.now)
}
if [.codex, .claude, .vertexai, .bedrock].contains(input.provider),
input.tokenCostInlineDashboardEnabled,
let tokenSnapshot = input.tokenSnapshot,
!tokenSnapshot.daily.isEmpty
{
return Self.costHistoryInlineDashboard(
provider: input.provider,
snapshot: tokenSnapshot,
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
}
return nil
}
static func usesProviderCostHistoryAsPrimaryDashboard(_ provider: UsageProvider) -> Bool {
provider == .openai || provider == .mistral
}
static func primaryCostHistorySnapshot(input: Input) -> CostUsageTokenSnapshot? {
switch input.provider {
case .openai:
if let projected = input.snapshot?.openAIAPIUsage?.toCostUsageTokenSnapshot() {
return projected
}
return input.snapshot == nil ? input.tokenSnapshot : nil
case .mistral:
if let projected = input.snapshot?.mistralUsage?.toCostUsageTokenSnapshot() {
return projected
}
return input.snapshot == nil ? input.tokenSnapshot : nil
default:
return input.tokenSnapshot
}
}
static func poeInlineDashboard(
_ usage: PoeUsageHistorySnapshot,
now: Date = Date(),
calendar: Calendar = .current) -> InlineUsageDashboardModel
{
let today = usage.currentDay(now: now, calendar: calendar)
let week = usage.last7Days
let month = usage.last30Days
let points = usage.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.day,
label: Self.shortDayLabel($0.day),
value: $0.points,
accessibilityValue: "\($0.day): \(Self.pointsSummary($0.points))")
}
var details = ["30d requests: \(UsageFormatter.tokenCountString(month.requests))"]
if let topModel = usage.topModel {
details.append("\(L("Top model")): \(topModel)")
}
if !usage.topUsageTypes.isEmpty {
let mix = usage.topUsageTypes.prefix(3)
.map { "\($0.name): \(Self.pointsSummary($0.points))" }
.joined(separator: " · ")
details.append("Usage mix: \(mix)")
}
if let usd = today.costUSD, usd > 0 {
details.append("Today USD: \(UsageFormatter.usdString(usd))")
}
if let usd = week.costUSD, usd > 0 {
details.append("7d USD: \(UsageFormatter.usdString(usd))")
}
if let usd = month.costUSD, usd > 0 {
details.append("30d USD: \(UsageFormatter.usdString(usd))")
}
let recent = usage.recentEntries(limit: 2)
if !recent.isEmpty {
let text = recent.map { "\($0.model) \(Self.pointsSummary($0.points))" }.joined(separator: " · ")
details.append("Recent: \(text)")
}
return InlineUsageDashboardModel(
accessibilityLabel: "Poe points usage trend",
valueStyle: .points,
kpis: [
.init(title: L("Today"), value: Self.pointsSummary(today.points), emphasis: true),
.init(title: "7d", value: Self.pointsSummary(week.points), emphasis: false),
.init(title: "30d", value: Self.pointsSummary(month.points), emphasis: false),
.init(title: L("Requests"), value: UsageFormatter.tokenCountString(month.requests), emphasis: false),
],
points: points,
detailLines: details)
}
static func pointsSummary(_ value: Double) -> String {
let clamped = max(0, value)
if clamped.rounded() == clamped {
return "\(UsageFormatter.tokenCountString(Int(clamped))) points"
}
return "\(String(format: "%.1f", clamped)) points"
}
private static func costHistoryInlineDashboard(
provider: UsageProvider,
snapshot: CostUsageTokenSnapshot,
comparisonPeriodsEnabled: Bool) -> InlineUsageDashboardModel
{
let historyDays = max(1, min(365, snapshot.historyDays))
let historyTitle = snapshot.historyLabel
?? (historyDays == 1
? L("Today")
: historyDays == 30
? L("30d cost")
: "\(String(format: L("Last %d days"), historyDays)) \(L("Cost"))")
let tokenHistoryTitle = snapshot.historyLabel.map { "\($0) \(L("tokens"))" }
?? (historyDays == 1
? L("Today tokens")
: historyDays == 30
? L("30d tokens")
: String(format: L("%@ tokens"), String(format: L("Last %d days"), historyDays)))
let requestHistoryTitle = snapshot.historyLabel.map { "\($0) \(L("requests"))" }
?? (historyDays == 1
? L("Today requests")
: historyDays == 30
? L("30d requests")
: String(format: L("%@ requests"), String(format: L("Last %d days"), historyDays)))
let periodLabel = snapshot.historyLabel?.lowercased()
?? (historyDays == 1 ? "today" : "\(historyDays) day")
let points = snapshot.daily.suffix(historyDays).compactMap { entry -> InlineUsageDashboardModel.Point? in
guard let cost = entry.costUSD else { return nil }
return InlineUsageDashboardModel.Point(
id: entry.date,
label: Self.shortDayLabel(entry.date),
value: cost,
accessibilityValue: "\(entry.date): \(Self.costString(cost, currencyCode: snapshot.currencyCode))")
}
let latest = CostUsageTokenSnapshot.latestEntry(in: snapshot.daily)
let usesLatestPrimary = provider == .bedrock || provider == .mistral
let primaryCostUSD = usesLatestPrimary ? latest?.costUSD : snapshot.sessionCostUSD
var details: [String] = []
if comparisonPeriodsEnabled {
details.append(contentsOf: snapshot.comparisonSummaries().map {
Self.costWindowLine(summary: $0, currencyCode: snapshot.currencyCode)
})
}
if let topModel = Self.topCostModel(from: snapshot.daily) {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
}
if let requestCount = snapshot.last30DaysRequests {
details.append("\(requestHistoryTitle): \(UsageFormatter.tokenCountString(requestCount)) \(L("requests"))")
}
if let hint = Self.tokenUsageHint(provider: provider) {
details.append(hint)
} else {
details.append(L("cost_estimate_hint"))
}
let providerName = ProviderDefaults.metadata[provider]?.displayName ?? provider.rawValue
var model = InlineUsageDashboardModel(
accessibilityLabel: "\(providerName) \(periodLabel) cost trend",
valueStyle: Self.costValueStyle(currencyCode: snapshot.currencyCode),
kpis: [
.init(
title: usesLatestPrimary ? L("Latest") : L("Today"),
value: primaryCostUSD.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "",
emphasis: true),
.init(
title: historyTitle,
value: snapshot.last30DaysCostUSD
.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "",
emphasis: false),
.init(
title: tokenHistoryTitle,
value: snapshot.last30DaysTokens.map(UsageFormatter.tokenCountString) ?? "",
emphasis: false),
] + Self.costHistoryTrailingKPIs(snapshot: snapshot, latest: latest),
points: points,
detailLines: details)
model.currencyCode = snapshot.currencyCode
return model
}
private static func costHistoryTrailingKPIs(
snapshot: CostUsageTokenSnapshot,
latest: CostUsageDailyReport.Entry?)
-> [InlineUsageDashboardModel.KPI]
{
if let requests = snapshot.last30DaysRequests {
return [
.init(
title: L("Requests"),
value: UsageFormatter.tokenCountString(requests),
emphasis: false),
]
}
return [
.init(
title: L("Latest tokens"),
value: latest?.totalTokens.map(UsageFormatter.tokenCountString) ?? "",
emphasis: false),
]
}
fileprivate static func claudeAdminAPIInlineDashboard(_ usage: ClaudeAdminAPIUsageSnapshot)
-> InlineUsageDashboardModel
{
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
let points = usage.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.day,
label: Self.shortDayLabel($0.day),
value: $0.costUSD,
accessibilityValue: "\($0.day): \(UsageFormatter.usdString($0.costUSD))")
}
var details = [
"30d: \(UsageFormatter.tokenCountString(last30.totalTokens)) \(L("tokens"))",
"\(L("Cache read")): \(UsageFormatter.tokenCountString(last30.cacheReadInputTokens)) \(L("tokens"))",
]
if let topModel = usage.topModels.first {
details.append("\(L("Top model")): \(Self.shortModelName(topModel.name))")
}
var model = InlineUsageDashboardModel(
accessibilityLabel: L("Claude Admin API 30 day spend trend"),
valueStyle: .currencyUSD,
kpis: [
.init(title: L("Today"), value: UsageFormatter.usdString(today.costUSD), emphasis: true),
.init(title: L("7d spend"), value: UsageFormatter.usdString(last7.costUSD), emphasis: false),
.init(
title: L("30d spend"),
value: UsageFormatter.usdString(last30.costUSD),
emphasis: false),
.init(
title: L("Today tokens"),
value: UsageFormatter.tokenCountString(today.totalTokens),
emphasis: false),
],
points: points,
detailLines: details)
model.currencyCode = "USD"
return model
}
private static func crossModelInlineDashboard(_ usage: CrossModelUsageSnapshot) -> InlineUsageDashboardModel? {
let periodValues: [(String, String, Double?)] = [
("day", L("Today"), usage.daily?.cost),
("week", L("Week"), usage.weekly?.cost),
("month", L("Month"), usage.monthly?.cost),
]
let points = periodValues.compactMap { id, label, value -> InlineUsageDashboardModel.Point? in
guard let value else { return nil }
return InlineUsageDashboardModel.Point(
id: id,
label: label,
value: value,
accessibilityValue: "\(label): \(usage.currencyString(value))")
}
var model = InlineUsageDashboardModel(
accessibilityLabel: L("CrossModel API spend trend"),
valueStyle: Self.costValueStyle(currencyCode: usage.currency),
kpis: [
.init(title: L("Balance"), value: usage.balanceDisplay, emphasis: true),
.init(
title: L("Today"),
value: usage.daily.map { usage.currencyString($0.cost) } ?? "",
emphasis: false),
.init(
title: L("Week"),
value: usage.weekly.map { usage.currencyString($0.cost) } ?? "",
emphasis: false),
.init(
title: L("Month"),
value: usage.monthly.map { usage.currencyString($0.cost) } ?? "",
emphasis: false),
],
points: points,
detailLines: [])
model.currencyCode = usage.currency
return model
}
private static func openRouterInlineDashboard(_ usage: OpenRouterUsageSnapshot) -> InlineUsageDashboardModel? {
let periodValues: [(String, String, Double?)] = [
("day", L("Today"), usage.keyUsageDaily),
("week", L("Week"), usage.keyUsageWeekly),
("month", L("Month"), usage.keyUsageMonthly),
]
let points = periodValues.compactMap { id, label, value -> InlineUsageDashboardModel.Point? in
guard let value else { return nil }
let formattedValue = Self.openRouterCurrencyString(value)
return InlineUsageDashboardModel.Point(
id: id,
label: label,
value: value,
accessibilityValue: String(format: L("%@: %@"), label, formattedValue))
}
guard !points.isEmpty else { return nil }
var details: [String] = []
if let rate = usage.rateLimit {
details.append(String(format: L("Rate limit: %d / %@"), rate.requests, rate.interval))
}
switch usage.keyQuotaStatus {
case .available:
if let remaining = usage.keyRemaining {
details.append(String(
format: L("%@: %@"),
L("Key remaining"),
Self.openRouterCurrencyString(remaining)))
}
case .noLimitConfigured:
details.append(L("No limit set for the API key"))
case .unavailable:
details.append(L("API key limit unavailable right now"))
}
var model = InlineUsageDashboardModel(
accessibilityLabel: L("OpenRouter API key spend trend"),
valueStyle: .currencyUSD,
kpis: [
.init(title: L("Balance"), value: Self.openRouterCurrencyString(usage.balance), emphasis: true),
.init(
title: L("Today"),
value: usage.keyUsageDaily.map(Self.openRouterCurrencyString) ?? "",
emphasis: false),
.init(
title: L("Week"),
value: usage.keyUsageWeekly.map(Self.openRouterCurrencyString) ?? "",
emphasis: false),
.init(
title: L("Month"),
value: usage.keyUsageMonthly.map(Self.openRouterCurrencyString) ?? "",
emphasis: false),
],
points: points,
detailLines: details)
model.currencyCode = "USD"
return model
}
private static func zaiInlineDashboard(modelUsage: ZaiModelUsageData, now: Date) -> InlineUsageDashboardModel? {
let bars = ZaiHourlyBars.from(modelData: modelUsage, range: .last24h, now: now)
guard !bars.isEmpty else { return nil }
let total = bars.reduce(0) { $0 + $1.totalTokens }
let latest = bars.last
let peak = bars.max { $0.totalTokens < $1.totalTokens }
let points = bars.enumerated().map { index, bar in
InlineUsageDashboardModel.Point(
id: "\(index)-\(bar.label)",
label: bar.label,
value: Double(bar.totalTokens),
accessibilityValue: "\(bar.label): \(UsageFormatter.tokenCountString(bar.totalTokens)) \(L("tokens"))")
}
let topModel = Self.topZaiModel(from: bars)
return InlineUsageDashboardModel(
accessibilityLabel: L("z.ai hourly token trend"),
valueStyle: .tokens,
kpis: [
.init(title: L("24h tokens"), value: UsageFormatter.tokenCountString(total), emphasis: true),
.init(
title: L("Latest hour"),
value: latest.map { UsageFormatter.tokenCountString($0.totalTokens) } ?? "",
emphasis: false),
.init(
title: L("Peak hour"),
value: peak.map { UsageFormatter.tokenCountString($0.totalTokens) } ?? "",
emphasis: false),
.init(title: L("Models"), value: "\(modelUsage.modelNames.count)", emphasis: false),
],
points: points,
detailLines: topModel.map { ["\(L("Top model")): \(Self.shortModelName($0))"] } ?? [])
}
private static func minimaxInlineDashboard(_ billing: MiniMaxBillingSummary) -> InlineUsageDashboardModel {
let points = billing.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.day,
label: Self.shortDayLabel($0.day),
value: Double($0.tokens),
accessibilityValue: "\($0.day): \(UsageFormatter.tokenCountString($0.tokens)) \(L("tokens"))")
}
var details = [L("30d billing history from MiniMax web session")]
if let topModel = billing.topModels.first {
details.append("\(L("Top model")): \(Self.shortModelName(topModel.name))")
}
if let topMethod = billing.topMethods.first {
details.append("\(L("Top method")): \(Self.shortModelName(topMethod.name))")
}
if let cash = billing.last30DaysCash {
details.append("\(L("30d cash")): \(Self.minimaxCashString(cash))")
}
return InlineUsageDashboardModel(
accessibilityLabel: L("MiniMax 30 day token usage trend"),
valueStyle: .tokens,
kpis: [
.init(
title: L("Today"),
value: UsageFormatter.tokenCountString(billing.todayTokens),
emphasis: true),
.init(
title: L("30d tokens"),
value: UsageFormatter.tokenCountString(billing.last30DaysTokens),
emphasis: false),
.init(
title: L("Today cash"),
value: billing.todayCash.map(Self.minimaxCashString) ?? "",
emphasis: false),
.init(
title: L("Models"),
value: "\(billing.topModels.count)",
emphasis: false),
],
points: points,
detailLines: details)
}
private static func deepseekInlineDashboard(_ usage: DeepSeekUsageSummary) -> InlineUsageDashboardModel {
let symbol = usage.currency == "CNY" ? "¥" : "$"
let points = usage.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.date,
label: Self.shortDayLabel($0.date),
value: Double($0.totalTokens),
accessibilityValue: "\($0.date): \(UsageFormatter.tokenCountString($0.totalTokens)) \(L("tokens"))")
}
var details: [String] = []
if let topModel = usage.topModel {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
}
if let cacheHit = usage.categoryBreakdown.first(where: { $0.category == .promptCacheHitToken }) {
details.append("\(L("cache-hit input")): \(UsageFormatter.tokenCountString(cacheHit.tokens))")
}
if let cacheMiss = usage.categoryBreakdown.first(where: { $0.category == .promptCacheMissToken }) {
details.append("\(L("cache-miss input")): \(UsageFormatter.tokenCountString(cacheMiss.tokens))")
}
if let output = usage.categoryBreakdown.first(where: { $0.category == .responseToken }) {
details.append("\(L("output")): \(UsageFormatter.tokenCountString(output.tokens))")
}
details.append("\(L("requests")): \(usage.currentMonthRequestCount)")
let todayCostStr = usage.todayCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? ""
let monthCostStr = usage.currentMonthCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? ""
let monthTokensStr = UsageFormatter.tokenCountString(usage.currentMonthTokens)
return InlineUsageDashboardModel(
accessibilityLabel: L("DeepSeek 30 day token usage trend"),
valueStyle: .tokens,
kpis: [
.init(
title: L("Today"),
value: "\(todayCostStr) · \(UsageFormatter.tokenCountString(usage.todayTokens))",
emphasis: true),
.init(
title: L("This month"),
value: "\(monthCostStr) · \(monthTokensStr)",
emphasis: false),
.init(
title: L("Models"),
value: usage.topModel.map { Self.shortModelName($0) } ?? "",
emphasis: false),
.init(
title: L("Requests"),
value: "\(usage.currentMonthRequestCount)",
emphasis: false),
],
points: points,
detailLines: details)
}
private static func topMistralModel(from entries: [MistralDailyUsageBucket]) -> String? {
var tokens: [String: Int] = [:]
for entry in entries {
for model in entry.models {
tokens[model.name, default: 0] += model.totalTokens
}
}
return tokens.max {
if $0.value == $1.value { return $0.key > $1.key }
return $0.value < $1.value
}?.key
}
private static func topZaiModel(from bars: [ZaiHourlyBar]) -> String? {
var tokens: [String: Int] = [:]
for bar in bars {
for segment in bar.segments {
tokens[segment.model, default: 0] += segment.tokens
}
}
return tokens.max {
if $0.value == $1.value { return $0.key > $1.key }
return $0.value < $1.value
}?.key
}
private static func openRouterCurrencyString(_ value: Double) -> String {
String(format: "$%.2f", value)
}
private static func minimaxCashString(_ value: Double) -> String {
String(format: "%.2f", max(0, value))
}
private static func costString(_ value: Double, currencyCode: String) -> String {
UsageFormatter.currencyString(value, currencyCode: currencyCode)
}
private static func costValueStyle(currencyCode: String) -> InlineUsageDashboardModel.ValueStyle {
if currencyCode == "USD" { return .currencyUSD }
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currencyCode
formatter.locale = Locale(identifier: "en_US")
let symbol = formatter.currencySymbol ?? currencyCode
return .currency(symbol: symbol)
}
private static func shortDayLabel(_ day: String) -> String {
let pieces = day.split(separator: "-")
guard pieces.count == 3, let rawDay = Int(pieces[2]) else { return day }
return "\(rawDay)"
}
private static func shortModelName(_ name: String) -> String {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count > 26 else { return trimmed }
return String(trimmed.prefix(25)) + ""
}
private static func topCostModel(from entries: [CostUsageDailyReport.Entry]) -> String? {
var scores: [String: (cost: Double, tokens: Int)] = [:]
for entry in entries {
for model in entry.modelBreakdowns ?? [] {
var score = scores[model.modelName] ?? (0, 0)
score.cost += model.costUSD ?? 0
score.tokens += model.totalTokens ?? 0
scores[model.modelName] = score
}
}
return scores.max {
if $0.value.cost == $1.value.cost { return $0.value.tokens < $1.value.tokens }
return $0.value.cost < $1.value.cost
}?.key
}
}
struct InlineUsageDashboardContent: View {
private let model: InlineUsageDashboardModel
@Environment(\.menuItemHighlighted) private var isHighlighted
init(model: InlineUsageDashboardModel) {
self.model = model
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
self.kpis
if !self.model.points.isEmpty {
MiniUsageBars(model: self.model)
.frame(height: 58)
.accessibilityLabel(self.model.accessibilityLabel)
}
self.detailLines
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var kpis: some View {
LazyVGrid(
columns: [
GridItem(.flexible(minimum: 118), alignment: .leading),
GridItem(.flexible(minimum: 100), alignment: .leading),
],
alignment: .leading,
spacing: 6)
{
ForEach(Array(self.model.kpis.enumerated()), id: \.offset) { _, kpi in
KPIBlock(title: kpi.title, value: kpi.value, emphasis: kpi.emphasis)
}
}
}
private var detailLines: some View {
VStack(alignment: .leading, spacing: 3) {
ForEach(Array(self.model.detailLines.enumerated()), id: \.offset) { _, line in
Text(line)
.font(.caption)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
}
}
}
private struct KPIBlock: View {
let title: String
let value: String
let emphasis: Bool
@Environment(\.menuItemHighlighted) private var isHighlighted
var body: some View {
VStack(alignment: .leading, spacing: 1) {
Text(self.title)
.font(.caption2)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
Text(self.value)
.font(self.emphasis ? .headline : .subheadline)
.fontWeight(.semibold)
.foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))
.lineLimit(1)
.minimumScaleFactor(0.72)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private struct MiniUsageBars: View {
let model: InlineUsageDashboardModel
@Environment(\.menuItemHighlighted) private var isHighlighted
var body: some View {
let scale = UsageChartScale(values: self.model.points.map(\.value))
VStack(alignment: .trailing, spacing: 2) {
if let currencyCode = self.model.currencyCode, scale.maximum > 0 {
Text(UsageFormatter.compactCurrencyString(scale.maximum, currencyCode: currencyCode))
.font(.caption2)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.monospacedDigit()
.lineLimit(1)
.allowsTightening(true)
}
GeometryReader { geometry in
HStack(alignment: .bottom, spacing: 2) {
ForEach(self.model.points) { point in
RoundedRectangle(cornerRadius: 1.5, style: .continuous)
.fill(self.fill(for: point, scale: scale))
.frame(maxWidth: .infinity)
.frame(height: self.height(for: point, scale: scale, available: geometry.size.height))
.accessibilityLabel(point.accessibilityValue)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.overlay(alignment: .bottomLeading) {
Rectangle()
.fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(0.22))
.frame(height: 1)
}
}
}
}
private func height(
for point: InlineUsageDashboardModel.Point,
scale: UsageChartScale,
available: CGFloat) -> CGFloat
{
let ratio = scale.fraction(for: point.value)
guard ratio > 0 else { return 1 }
return max(3, CGFloat(ratio) * available)
}
private func fill(for point: InlineUsageDashboardModel.Point, scale: UsageChartScale) -> Color {
let ratio = max(0.18, scale.fraction(for: point.value))
if self.isHighlighted {
return Color.white.opacity(0.55 + ratio * 0.35)
}
return self.baseColor.opacity(0.42 + ratio * 0.58)
}
private var baseColor: Color {
if let barColor = self.model.barColor {
return barColor
}
switch self.model.valueStyle {
case .currencyUSD, .currency:
return Color(red: 0.81, green: 0.56, blue: 0.24)
case .tokens:
return Color(red: 0.48, green: 0.41, blue: 0.86)
case .points:
return Color(red: 0.16, green: 0.62, blue: 0.36)
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import Foundation
enum InstallOrigin {
static func isHomebrewCask(appBundleURL: URL) -> Bool {
let resolved = appBundleURL.resolvingSymlinksInPath()
let path = resolved.path
return path.contains("/Caskroom/") || path.contains("/Homebrew/Caskroom/")
}
}
@@ -0,0 +1,6 @@
import KeyboardShortcuts
@MainActor
extension KeyboardShortcuts.Name {
static let openMenu = Self("openMenu")
}
+154
View File
@@ -0,0 +1,154 @@
import CodexBarCore
import Foundation
import Security
/// Migrates keychain items to use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
/// to prevent permission prompts on every rebuild during development.
enum KeychainMigration {
private static let log = CodexBarLog.logger(LogCategories.keychainMigration)
private static let migrationKey = "KeychainMigrationV1Completed"
struct MigrationItem: Hashable {
let service: String
let account: String?
var label: String {
let accountLabel = self.account ?? "<any>"
return "\(self.service):\(accountLabel)"
}
}
static let itemsToMigrate: [MigrationItem] = [
MigrationItem(service: "com.steipete.CodexBar", account: "codex-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "claude-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "cursor-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "factory-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "minimax-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "minimax-api-token"),
MigrationItem(service: "com.steipete.CodexBar", account: "augment-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "copilot-api-token"),
MigrationItem(service: "com.steipete.CodexBar", account: "zai-api-token"),
MigrationItem(service: "com.steipete.CodexBar", account: "synthetic-api-key"),
]
/// Run migration once per installation
static func migrateIfNeeded() {
guard !KeychainAccessGate.isDisabled else {
self.log.info("Keychain access disabled; skipping migration")
return
}
if !UserDefaults.standard.bool(forKey: self.migrationKey) {
self.log.info("Starting keychain migration to reduce permission prompts")
var migratedCount = 0
var errorCount = 0
for item in self.itemsToMigrate {
do {
if try self.migrateItem(item) {
migratedCount += 1
}
} catch {
errorCount += 1
self.log.error("Failed to migrate \(item.label): \(String(describing: error))")
}
}
self.log.info("Keychain migration complete: \(migratedCount) migrated, \(errorCount) errors")
UserDefaults.standard.set(true, forKey: self.migrationKey)
if migratedCount > 0 {
self.log.info("✅ Future rebuilds will not prompt for keychain access")
}
} else {
self.log.debug("Keychain migration already completed, skipping")
}
}
/// Migrate a single keychain item to the new accessibility level
/// Returns true if item was migrated, false if item didn't exist
private static func migrateItem(_ item: MigrationItem) throws -> Bool {
// First, try to read the existing item
var result: CFTypeRef?
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: item.service,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
kSecReturnAttributes as String: true,
]
if let account = item.account {
query[kSecAttrAccount as String] = account
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
// Item doesn't exist, nothing to migrate
return false
}
guard status == errSecSuccess else {
throw KeychainMigrationError.readFailed(status)
}
guard let rawItem = result as? [String: Any],
let data = rawItem[kSecValueData as String] as? Data,
let accessible = rawItem[kSecAttrAccessible as String] as? String
else {
throw KeychainMigrationError.invalidItemFormat
}
// Check if already using the correct accessibility
if accessible == (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String) {
self.log.debug("\(item.label) already using correct accessibility")
return false
}
// Delete the old item
var deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: item.service,
]
if let account = item.account {
deleteQuery[kSecAttrAccount as String] = account
}
let deleteStatus = KeychainSecurity.delete(deleteQuery as CFDictionary)
guard deleteStatus == errSecSuccess else {
throw KeychainMigrationError.deleteFailed(deleteStatus)
}
// Add it back with the new accessibility
var addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: item.service,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
if let account = item.account {
addQuery[kSecAttrAccount as String] = account
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
throw KeychainMigrationError.addFailed(addStatus)
}
self.log.info("Migrated \(item.label) to new accessibility level")
return true
}
/// Reset migration flag (for testing)
static func resetMigrationFlag() {
UserDefaults.standard.removeObject(forKey: self.migrationKey)
}
}
enum KeychainMigrationError: Error {
case readFailed(OSStatus)
case deleteFailed(OSStatus)
case addFailed(OSStatus)
case invalidItemFormat
}
@@ -0,0 +1,222 @@
import AppKit
import CodexBarCore
import SweetCookieKit
private enum KeychainPromptMessage {
static let browserCookie =
"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies " +
"and authenticate your account. Click OK to continue."
static let claudeOAuth =
"CodexBar will ask macOS Keychain for the Claude Code OAuth token " +
"so it can fetch your Claude usage. Click OK to continue."
static let codexCookie =
"CodexBar will ask macOS Keychain for your OpenAI cookie header " +
"so it can fetch Codex dashboard extras. Click OK to continue."
static let claudeCookie =
"CodexBar will ask macOS Keychain for your Claude cookie header " +
"so it can fetch Claude web usage. Click OK to continue."
static let cursorCookie =
"CodexBar will ask macOS Keychain for your Cursor cookie header " +
"so it can fetch usage. Click OK to continue."
static let openCodeCookie =
"CodexBar will ask macOS Keychain for your OpenCode cookie header " +
"so it can fetch usage. Click OK to continue."
static let factoryCookie =
"CodexBar will ask macOS Keychain for your Factory cookie header " +
"so it can fetch usage. Click OK to continue."
static let zaiToken =
"CodexBar will ask macOS Keychain for your z.ai API token " +
"so it can fetch usage. Click OK to continue."
static let syntheticToken =
"CodexBar will ask macOS Keychain for your Synthetic API key " +
"so it can fetch usage. Click OK to continue."
static let copilotToken =
"CodexBar will ask macOS Keychain for your GitHub Copilot token " +
"so it can fetch usage. Click OK to continue."
static let kimiToken =
"CodexBar will ask macOS Keychain for your Kimi auth token " +
"so it can fetch usage. Click OK to continue."
static let kimiK2Token =
"CodexBar will ask macOS Keychain for your Kimi K2 API key " +
"so it can fetch usage. Click OK to continue."
static let minimaxCookie =
"CodexBar will ask macOS Keychain for your MiniMax cookie header " +
"so it can fetch usage. Click OK to continue."
static let minimaxToken =
"CodexBar will ask macOS Keychain for your MiniMax API token " +
"so it can fetch usage. Click OK to continue."
static let augmentCookie =
"CodexBar will ask macOS Keychain for your Augment cookie header " +
"so it can fetch usage. Click OK to continue."
static let ampCookie =
"CodexBar will ask macOS Keychain for your Amp cookie header " +
"so it can fetch usage. Click OK to continue."
}
struct KeychainPromptAlertModel: Equatable {
let title: String
let message: String
let primaryButtonTitle: String
let learnMoreButtonTitle: String
let documentationURL: String
}
@MainActor
private final class KeychainPromptLearnMoreTarget: NSObject {
private let documentationURL: String
init(documentationURL: String) {
self.documentationURL = documentationURL
}
@objc func openDocumentation() {
guard let url = URL(string: self.documentationURL) else { return }
NSWorkspace.shared.open(url)
}
}
enum KeychainPromptCoordinator {
private static let promptLock = NSLock()
private static let log = CodexBarLog.logger(LogCategories.keychainPrompt)
private static let documentationURL =
"https://github.com/steipete/CodexBar/blob/main/docs/keychain-prompts.md"
static func install() {
KeychainPromptHandler.handler = { context in
self.presentKeychainPrompt(context)
}
BrowserCookieKeychainPromptHandler.handler = { context in
self.presentBrowserCookiePrompt(context)
}
self.disableKeychainForUnbundledExecutableIfNeeded()
}
private static let unbundledExecutableCheckLock = NSLock()
private nonisolated(unsafe) static var didCheckUnbundledExecutable = false
static func disableKeychainForUnbundledExecutableIfNeeded() {
self.unbundledExecutableCheckLock.lock()
guard !self.didCheckUnbundledExecutable else {
self.unbundledExecutableCheckLock.unlock()
return
}
self.didCheckUnbundledExecutable = true
self.unbundledExecutableCheckLock.unlock()
let executablePath = Bundle.main.executableURL?.path ?? ""
guard Self.isUnbundledCodexBarExecutable(executablePath) else { return }
KeychainAccessGate.forceDisabledForProcess(reason: "unbundled-executable")
Self.log.warning(
"Unbundled CodexBar executable detected; disabling keychain access to avoid repeated prompts",
metadata: ["doc": "docs/DEVELOPMENT_SETUP.md"])
}
static func isUnbundledCodexBarExecutable(_ executablePath: String) -> Bool {
guard executablePath.hasPrefix("/") else { return false }
let executableURL = URL(fileURLWithPath: executablePath).standardizedFileURL
return executableURL.lastPathComponent == "CodexBar"
&& !executableURL.pathComponents.contains(where: { $0.hasSuffix(".app") })
}
private static func presentKeychainPrompt(_ context: KeychainPromptContext) {
let model = self.alertModel(for: context)
self.log.info("Keychain prompt requested", metadata: ["kind": "\(context.kind)"])
self.presentAlert(model)
}
private static func presentBrowserCookiePrompt(_ context: BrowserCookieKeychainPromptContext) {
let model = self.browserCookieAlertModel(label: context.label)
self.log.info("Browser cookie keychain prompt requested", metadata: ["label": context.label])
self.presentAlert(model)
}
static func alertModel(for context: KeychainPromptContext) -> KeychainPromptAlertModel {
let purpose = switch context.kind {
case .claudeOAuth:
L(KeychainPromptMessage.claudeOAuth)
case .codexCookie:
L(KeychainPromptMessage.codexCookie)
case .claudeCookie:
L(KeychainPromptMessage.claudeCookie)
case .cursorCookie:
L(KeychainPromptMessage.cursorCookie)
case .opencodeCookie:
L(KeychainPromptMessage.openCodeCookie)
case .factoryCookie:
L(KeychainPromptMessage.factoryCookie)
case .zaiToken:
L(KeychainPromptMessage.zaiToken)
case .syntheticToken:
L(KeychainPromptMessage.syntheticToken)
case .copilotToken:
L(KeychainPromptMessage.copilotToken)
case .kimiToken:
L(KeychainPromptMessage.kimiToken)
case .kimiK2Token:
L(KeychainPromptMessage.kimiK2Token)
case .minimaxCookie:
L(KeychainPromptMessage.minimaxCookie)
case .minimaxToken:
L(KeychainPromptMessage.minimaxToken)
case .augmentCookie:
L(KeychainPromptMessage.augmentCookie)
case .ampCookie:
L(KeychainPromptMessage.ampCookie)
}
return self.alertModel(purpose: purpose)
}
static func browserCookieAlertModel(label: String) -> KeychainPromptAlertModel {
self.alertModel(purpose: L(KeychainPromptMessage.browserCookie, label))
}
private static func alertModel(purpose: String) -> KeychainPromptAlertModel {
KeychainPromptAlertModel(
title: L("Keychain Access Required"),
message: "\(purpose)\n\n\(L("keychain_prompt_privacy_note"))",
primaryButtonTitle: L("OK"),
learnMoreButtonTitle: L("keychain_prompt_learn_more"),
documentationURL: self.documentationURL)
}
private static func presentAlert(_ model: KeychainPromptAlertModel) {
self.promptLock.lock()
defer { self.promptLock.unlock() }
if Thread.isMainThread {
MainActor.assumeIsolated {
self.showAlert(model)
}
return
}
DispatchQueue.main.sync {
MainActor.assumeIsolated {
self.showAlert(model)
}
}
}
@MainActor
private static func showAlert(_ model: KeychainPromptAlertModel) {
let alert = NSAlert()
alert.messageText = model.title
alert.informativeText = model.message
alert.addButton(withTitle: model.primaryButtonTitle)
let learnMoreTarget = KeychainPromptLearnMoreTarget(documentationURL: model.documentationURL)
let learnMoreButton = NSButton(
title: model.learnMoreButtonTitle,
target: learnMoreTarget,
action: #selector(KeychainPromptLearnMoreTarget.openDocumentation))
learnMoreButton.isBordered = false
learnMoreButton.contentTintColor = .linkColor
learnMoreButton.font = .systemFont(ofSize: NSFont.smallSystemFontSize)
learnMoreButton.sizeToFit()
alert.accessoryView = learnMoreButton
withExtendedLifetime(learnMoreTarget) {
_ = alert.runModal()
}
}
}
+128
View File
@@ -0,0 +1,128 @@
import CodexBarCore
import Foundation
import Security
protocol KimiK2TokenStoring: Sendable {
func loadToken() throws -> String?
func storeToken(_ token: String?) throws
}
enum KimiK2TokenStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainKimiK2TokenStore: KimiK2TokenStoring {
private static let log = CodexBarLog.logger(LogCategories.kimiK2TokenStore)
private let service = "com.steipete.CodexBar"
private let account = "kimi-k2-api-token"
func loadToken() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .kimiK2Token,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw KimiK2TokenStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw KimiK2TokenStoreError.invalidData
}
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let token, !token.isEmpty {
return token
}
return nil
}
func storeToken(_ token: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token store")
return
}
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned == nil || cleaned?.isEmpty == true {
try self.deleteTokenIfPresent()
return
}
let data = cleaned!.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw KimiK2TokenStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw KimiK2TokenStoreError.keychainStatus(addStatus)
}
}
private func deleteTokenIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw KimiK2TokenStoreError.keychainStatus(status)
}
}
+128
View File
@@ -0,0 +1,128 @@
import CodexBarCore
import Foundation
import Security
protocol KimiTokenStoring: Sendable {
func loadToken() throws -> String?
func storeToken(_ token: String?) throws
}
enum KimiTokenStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainKimiTokenStore: KimiTokenStoring {
private static let log = CodexBarLog.logger(LogCategories.kimiTokenStore)
private let service = "com.steipete.CodexBar"
private let account = "kimi-auth-token"
func loadToken() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .kimiToken,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw KimiTokenStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw KimiTokenStoreError.invalidData
}
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let token, !token.isEmpty {
return token
}
return nil
}
func storeToken(_ token: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token store")
return
}
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned == nil || cleaned?.isEmpty == true {
try self.deleteTokenIfPresent()
return
}
let data = cleaned!.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw KimiTokenStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw KimiTokenStoreError.keychainStatus(addStatus)
}
}
private func deleteTokenIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw KimiTokenStoreError.keychainStatus(status)
}
}
@@ -0,0 +1,56 @@
import CodexBarCore
import ServiceManagement
enum LaunchAtLoginManager {
typealias StatusProvider = () -> SMAppService.Status
typealias RegistrationAction = () throws -> Void
private static let isRunningTests: Bool = {
let env = ProcessInfo.processInfo.environment
if env["XCTestConfigurationFilePath"] != nil { return true }
if env["TESTING_LIBRARY_VERSION"] != nil { return true }
if env["SWIFT_TESTING"] != nil { return true }
return NSClassFromString("XCTestCase") != nil
}()
static func setEnabled(_ enabled: Bool) {
if self.isRunningTests { return }
let service = SMAppService.mainApp
self.setEnabled(
enabled,
status: { service.status },
register: { try service.register() },
unregister: { try service.unregister() })
}
static func setEnabled(
_ enabled: Bool,
status: StatusProvider,
register: RegistrationAction,
unregister: RegistrationAction)
{
do {
if enabled {
switch status() {
case .enabled, .requiresApproval:
return
case .notRegistered, .notFound:
try register()
@unknown default:
try register()
}
} else {
switch status() {
case .enabled, .requiresApproval:
try unregister()
case .notRegistered, .notFound:
return
@unknown default:
try unregister()
}
}
} catch {
CodexBarLog.logger(LogCategories.launchAtLogin).error("Failed to update login item: \(error)")
}
}
}
+62
View File
@@ -0,0 +1,62 @@
import Foundation
enum LoadingPattern: String, CaseIterable, Identifiable {
case knightRider
case cylon
case outsideIn
case race
case pulse
case unbraid
var id: String {
self.rawValue
}
var displayName: String {
switch self {
case .knightRider: "Knight Rider"
case .cylon: "Cylon"
case .outsideIn: "Outside-In"
case .race: "Race"
case .pulse: "Pulse"
case .unbraid: "Unbraid (logo → bars)"
}
}
/// Secondary offset so the lower bar moves differently.
var secondaryOffset: Double {
switch self {
case .knightRider: .pi
case .cylon: .pi / 2
case .outsideIn: .pi
case .race: .pi / 3
case .pulse: .pi / 2
case .unbraid: .pi / 2
}
}
func value(phase: Double) -> Double {
let v: Double
switch self {
case .knightRider:
v = 0.5 + 0.5 * sin(phase) // ping-pong
case .cylon:
let t = phase.truncatingRemainder(dividingBy: .pi * 2) / (.pi * 2)
v = t // sawtooth 01
case .outsideIn:
v = abs(cos(phase)) // high at edges, dip center
case .race:
let t = (phase * 1.2).truncatingRemainder(dividingBy: .pi * 2) / (.pi * 2)
v = t
case .pulse:
v = 0.4 + 0.6 * (0.5 + 0.5 * sin(phase)) // 40100%
case .unbraid:
v = 0.5 + 0.5 * sin(phase) // smooth 01 for morph
}
return max(0, min(v * 100, 100))
}
}
extension Notification.Name {
static let codexbarDebugReplayAllAnimations = Notification.Name("codexbarDebugReplayAllAnimations")
}
+264
View File
@@ -0,0 +1,264 @@
import CodexBarCore
import Foundation
enum CodexBarLocalizationOverride {
@TaskLocal static var appLanguage: String?
}
enum AppLanguagePreferenceMigration {
private static let appleLanguagesKey = "AppleLanguages"
static func clearLegacyOverrideIfOwned(
storedAppLanguage: String,
defaults: UserDefaults = .standard)
{
let language = storedAppLanguage.trimmingCharacters(in: .whitespacesAndNewlines)
guard !language.isEmpty,
defaults.stringArray(forKey: self.appleLanguagesKey) == [language]
else { return }
defaults.removeObject(forKey: self.appleLanguagesKey)
}
}
private func appLanguageDefaults() -> UserDefaults {
if Bundle.main.bundleIdentifier != nil {
return .standard
}
if UserDefaults.standard.object(forKey: "appLanguage") != nil {
return .standard
}
// Fallback for running outside a .app bundle (swift run / debug builds)
return UserDefaults(suiteName: "CodexBar") ?? .standard
}
private let isRunningTestsProcessAtStartup: Bool = {
let env = ProcessInfo.processInfo.environment
if env["XCTestConfigurationFilePath"] != nil { return true }
if env["TESTING_LIBRARY_VERSION"] != nil { return true }
if env["SWIFT_TESTING"] != nil { return true }
return NSClassFromString("XCTestCase") != nil
}()
private func isRunningTestsProcess() -> Bool {
isRunningTestsProcessAtStartup
}
private func resolvedAppLanguage() -> String {
if let override = CodexBarLocalizationOverride.appLanguage {
return override
}
if isRunningTestsProcess() {
return "en"
}
return appLanguageDefaults().string(forKey: "appLanguage") ?? ""
}
func codexBarLocalizationSignature() -> String {
resolvedAppLanguage()
}
/// Resolving the `.lproj`/resource bundles repeats `Bundle(url:)`/`Bundle(path:)` filesystem lookups,
/// which are surprisingly hot: every `L()` and `codexBarLocalizationSignature()` call runs them, and
/// menu row bodies (`MetricRow`, `ProviderCostContent`, `UsageMenuCardView.Model`) re-evaluate them on
/// every closed-menu rebuild tick on the main thread (#1347). The resolved bundles never change unless
/// the language changes, so cache them. A single lock with compute-happening-outside-the-lock keeps the
/// disk work off the critical section and avoids re-entrant deadlock when the localized-bundle compute
/// closure calls back into the resource-bundle accessor.
private enum LocalizationBundleCache {
private static let lock = NSLock()
private nonisolated(unsafe) static var resourceBundle: Bundle?
private nonisolated(unsafe) static var localizedBundlesByLanguage: [String: Bundle] = [:]
static func defaultResourceBundle(_ compute: () -> Bundle) -> Bundle {
self.lock.lock()
if let resourceBundle {
self.lock.unlock()
return resourceBundle
}
self.lock.unlock()
let computed = compute()
self.lock.lock()
resourceBundle = computed
self.lock.unlock()
return computed
}
static func localizedBundle(forLanguage language: String, _ compute: () -> Bundle) -> Bundle {
self.lock.lock()
if let cachedLocalizedBundle = self.localizedBundlesByLanguage[language] {
self.lock.unlock()
return cachedLocalizedBundle
}
self.lock.unlock()
let computed = compute()
self.lock.lock()
self.localizedBundlesByLanguage[language] = computed
self.lock.unlock()
return computed
}
static func reset() {
self.lock.lock()
self.resourceBundle = nil
self.localizedBundlesByLanguage = [:]
self.lock.unlock()
}
}
func codexBarLocalizationResourceBundle(
mainBundle: Bundle = .main,
bundleName: String = "CodexBar_CodexBar") -> Bundle
{
// Only the default (process `.main`) resolution is cached: it is constant for the lifetime of the
// process. Custom arguments (tests) keep resolving directly so they stay isolated from the cache.
guard mainBundle === Bundle.main, bundleName == "CodexBar_CodexBar" else {
return resolveLocalizationResourceBundle(mainBundle: mainBundle, bundleName: bundleName)
}
return LocalizationBundleCache.defaultResourceBundle {
resolveLocalizationResourceBundle(mainBundle: mainBundle, bundleName: bundleName)
}
}
private func resolveLocalizationResourceBundle(mainBundle: Bundle, bundleName: String) -> Bundle {
guard mainBundle.bundleURL.pathExtension == "app" else {
return Bundle.module
}
if let url = mainBundle.url(forResource: bundleName, withExtension: "bundle"),
let bundle = Bundle(url: url)
{
return bundle
}
if let resourceURL = mainBundle.resourceURL?.absoluteURL,
let bundle = Bundle(url: resourceURL.appendingPathComponent("\(bundleName).bundle"))
{
return bundle
}
return mainBundle
}
private func localizedBundle() -> Bundle {
// Keyed on the resolved language so a language switch (settings change or test override) transparently
// re-resolves; otherwise the cached bundle is returned without touching the filesystem.
let language = resolvedAppLanguage()
return localizedBundle(forLanguage: language)
}
private func localizedBundle(forLanguage language: String) -> Bundle {
LocalizationBundleCache.localizedBundle(forLanguage: language) {
resolveLocalizedBundle(forLanguage: language)
}
}
private func resolveLocalizedBundle(forLanguage language: String) -> Bundle {
let resourceBundle = codexBarLocalizationResourceBundle()
if !language.isEmpty {
if let bundle = lprojBundle(named: language, in: resourceBundle) {
return bundle
}
} else {
// System mode: follow macOS language preferences
let localizations = resourceBundle.localizations.filter { $0 != "Base" }
let preferred = Bundle.preferredLocalizations(
from: localizations,
forPreferences: Locale.preferredLanguages).first
if let preferred,
let bundle = lprojBundle(named: preferred, in: resourceBundle)
{
return bundle
}
}
// Fallback to en.lproj
if let path = resourceBundle.path(forResource: "en", ofType: "lproj"),
let bundle = Bundle(path: path)
{
return bundle
}
return resourceBundle
}
private func lprojBundle(named language: String, in resourceBundle: Bundle) -> Bundle? {
let candidates = [language, language.lowercased()]
for candidate in candidates where !candidate.isEmpty {
if let path = resourceBundle.path(forResource: candidate, ofType: "lproj"),
let bundle = Bundle(path: path)
{
return bundle
}
}
return nil
}
func L(_ key: String) -> String {
let resourceBundle = codexBarLocalizationResourceBundle()
return codexBarLocalizedString(key, bundle: localizedBundle(), resourceBundle: resourceBundle)
}
func L(_ key: String, _ arguments: CVarArg...) -> String {
String(format: L(key), arguments: arguments)
}
func L(_ key: String, language: String) -> String {
let resourceBundle = codexBarLocalizationResourceBundle()
let bundle = localizedBundle(forLanguage: language)
return codexBarLocalizedString(key, bundle: bundle, resourceBundle: resourceBundle)
}
func codexBarLocalizedLocale() -> Locale {
let language = resolvedAppLanguage()
guard !language.isEmpty else { return .current }
switch language.lowercased() {
case "zh-hans":
return Locale(identifier: "zh-Hans")
case "zh-hant":
return Locale(identifier: "zh-Hant")
case "pt-br":
return Locale(identifier: "pt-BR")
default:
return Locale(identifier: language)
}
}
func codexBarLocalizedString(_ key: String, bundle: Bundle, resourceBundle: Bundle) -> String {
let value = bundle.localizedString(forKey: key, value: nil, table: nil)
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty, value != key {
return value
}
guard bundle.bundleURL.lastPathComponent != "en.lproj",
let englishBundle = lprojBundle(named: "en", in: resourceBundle)
else {
return trimmed.isEmpty ? key : value
}
let fallback = englishBundle.localizedString(forKey: key, value: nil, table: nil)
return fallback.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? key : fallback
}
func resetCodexBarLocalizationCache() {
LocalizationBundleCache.reset()
}
#if DEBUG
func codexBarLocalizedBundleForTesting() -> Bundle {
localizedBundle()
}
func resetCodexBarLocalizationCacheForTesting() {
resetCodexBarLocalizationCache()
}
#endif
func configureUsageFormatterLocalizationProvider() {
UsageFormatter.setLocalizationProvider { key in
let resourceBundle = codexBarLocalizationResourceBundle()
return codexBarLocalizedString(key, bundle: localizedBundle(), resourceBundle: resourceBundle)
}
UsageFormatter.setLocaleProvider {
codexBarLocalizedLocale()
}
}
@@ -0,0 +1,311 @@
import CodexBarCore
import Foundation
/// Tracks what the main thread is currently doing so hang reports can name the
/// operation even when the stall happens in uninstrumented code.
enum MainThreadActivityBreadcrumb {
private final class State: @unchecked Sendable {
let lock = NSLock()
var stack: [String] = []
}
private static let state = State()
static var current: String? {
guard MainThreadHangWatchdog.isEnabledForCurrentProcess else { return nil }
return self.state.lock.withLock { self.state.stack.last }
}
static func push(_ label: String) {
guard MainThreadHangWatchdog.isEnabledForCurrentProcess else { return }
self.state.lock.withLock {
self.state.stack.append(label)
}
}
static func pop() {
guard MainThreadHangWatchdog.isEnabledForCurrentProcess else { return }
self.state.lock.withLock {
_ = self.state.stack.popLast()
}
}
}
/// Detects main-queue response delays and records breadcrumbs for the work that
/// occupied the main thread. Long hangs launch `/usr/bin/sample` asynchronously
/// so sampling cannot delay recovery detection or inflate the reported duration.
final class MainThreadHangWatchdog: @unchecked Sendable {
static let shared = MainThreadHangWatchdog()
static let isEnabledForCurrentProcess: Bool = {
#if DEBUG
true
#else
let environment = ProcessInfo.processInfo.environment
return environment["CODEXBAR_MAIN_THREAD_HANG_WATCHDOG"] == "1" ||
UserDefaults.standard.bool(forKey: "debugMainThreadHangWatchdog")
#endif
}()
private let logger = CodexBarLog.logger(LogCategories.app)
private let pingInterval: TimeInterval
private let hangThreshold: TimeInterval
private let sampleThreshold: TimeInterval
private let sampleCooldown: TimeInterval
private let sampleCaptureOverride: (@Sendable () -> String?)?
private let schedulePing: @Sendable (@escaping @Sendable () -> Void) -> Void
private let lock = NSLock()
private var isRunning = false
private var lastSampleAt: Date?
private var activeSampleProcesses: [ObjectIdentifier: Process] = [:]
var onHangForTesting: ((TimeInterval, [String]) -> Void)?
#if DEBUG
var onHangDetectionForTesting: (() -> Void)?
private var onSampleAttemptForTesting: (() -> Void)?
#endif
private enum SampleCaptureResult {
case coolingDown
case attempted(String?)
}
init(
pingInterval: TimeInterval = 0.025,
hangThreshold: TimeInterval = 0.15,
sampleThreshold: TimeInterval = 2.0,
sampleCooldown: TimeInterval = 300,
sampleCaptureOverride: (@Sendable () -> String?)? = nil,
schedulePing: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void = { response in
DispatchQueue.main.async(execute: response)
})
{
self.pingInterval = pingInterval
self.hangThreshold = hangThreshold
self.sampleThreshold = sampleThreshold
self.sampleCooldown = sampleCooldown
self.sampleCaptureOverride = sampleCaptureOverride
self.schedulePing = schedulePing
}
func start() {
self.lock.lock()
defer { self.lock.unlock() }
guard !self.isRunning else { return }
self.isRunning = true
let thread = Thread { [weak self] in self?.run() }
thread.name = "CodexBar.MainThreadHangWatchdog"
thread.qualityOfService = .utility
thread.start()
}
func stop() {
self.lock.withLock {
self.isRunning = false
}
}
private var shouldRun: Bool {
self.lock.withLock { self.isRunning }
}
private final class PingBox: @unchecked Sendable {
private let lock = NSLock()
private let semaphore = DispatchSemaphore(value: 0)
private var _respondedAt: DispatchTime?
func markResponded() {
self.lock.withLock {
self._respondedAt = .now()
}
self.semaphore.signal()
}
var respondedAt: DispatchTime? {
self.lock.withLock { self._respondedAt }
}
func waitForResponse(timeout: TimeInterval) -> Bool {
self.semaphore.wait(timeout: .now() + timeout) == .success
}
}
private func run() {
while self.shouldRun {
let box = PingBox()
let pingSentAt = DispatchTime.now()
self.schedulePing { box.markResponded() }
if !box.waitForResponse(timeout: self.hangThreshold) {
guard self.shouldRun else { return }
#if DEBUG
self.onHangDetectionForTesting?()
#endif
self.traceHang(box: box, pingSentAt: pingSentAt)
}
guard self.shouldRun else { return }
Thread.sleep(forTimeInterval: self.pingInterval)
}
}
private func traceHang(box: PingBox, pingSentAt: DispatchTime) {
// One delayed ping can span several main-thread operations, so retain each
// distinct breadcrumb observed until the queued ping finally executes.
var activities: [String] = []
func recordActivity() {
guard activities.count < 8,
let activity = MainThreadActivityBreadcrumb.current,
!activities.contains(activity)
else { return }
activities.append(activity)
}
recordActivity()
var sampleFile: String?
var didAttemptSample = false
while box.respondedAt == nil, self.shouldRun {
recordActivity()
if !didAttemptSample, self.elapsedSeconds(since: pingSentAt) >= self.sampleThreshold {
#if DEBUG
self.onSampleAttemptForTesting?()
#endif
switch self.captureSampleIfAllowed() {
case .coolingDown:
break
case let .attempted(file):
didAttemptSample = true
sampleFile = file
}
}
Thread.sleep(forTimeInterval: 0.025)
}
guard let respondedAt = box.respondedAt else { return }
let duration = self.elapsedSeconds(from: pingSentAt, to: respondedAt)
var metadata: [String: String] = [
"durationMs": String(format: "%.0f", duration * 1000),
"activity": activities.isEmpty ? "unknown" : activities.joined(separator: ","),
]
if let sampleFile {
metadata["sampleRequested"] = sampleFile
}
self.logger.warning("main thread hang", metadata: metadata)
self.onHangForTesting?(duration, activities)
}
private func elapsedSeconds(since start: DispatchTime) -> TimeInterval {
self.elapsedSeconds(from: start, to: .now())
}
private func elapsedSeconds(from start: DispatchTime, to end: DispatchTime) -> TimeInterval {
TimeInterval(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000
}
private func captureSampleIfAllowed() -> SampleCaptureResult {
let now = Date()
let shouldCapture = self.lock.withLock {
if self.lastSampleAt.map({ now.timeIntervalSince($0) < self.sampleCooldown }) ?? false {
return false
}
self.lastSampleAt = now
return true
}
guard shouldCapture else { return .coolingDown }
let file = if let sampleCaptureOverride {
sampleCaptureOverride()
} else {
self.launchSample()
}
return .attempted(file)
}
private func launchSample() -> String? {
let directory = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Logs/CodexBar", isDirectory: true)
do {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
} catch {
self.logger.warning(
"main thread hang sample failed",
metadata: ["error": "\(error)"])
return nil
}
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate, .withTime]
let stamp = formatter.string(from: Date()).replacingOccurrences(of: ":", with: "-")
let file = directory.appendingPathComponent("hang-sample-\(stamp).txt")
let process = Process()
let processID = ObjectIdentifier(process)
process.executableURL = URL(fileURLWithPath: "/usr/bin/sample")
process.arguments = ["\(ProcessInfo.processInfo.processIdentifier)", "3", "-file", file.path]
process.terminationHandler = { [weak self] completedProcess in
self?.sampleDidFinish(completedProcess, processID: processID, file: file)
}
self.lock.withLock {
self.activeSampleProcesses[processID] = process
}
do {
try process.run()
} catch {
_ = self.lock.withLock {
self.activeSampleProcesses.removeValue(forKey: processID)
}
self.logger.warning(
"main thread hang sample failed",
metadata: ["error": "\(error)"])
return nil
}
return file.path
}
private func sampleDidFinish(_ process: Process, processID: ObjectIdentifier, file: URL) {
_ = self.lock.withLock {
self.activeSampleProcesses.removeValue(forKey: processID)
}
guard process.terminationStatus == 0,
FileManager.default.fileExists(atPath: file.path)
else {
self.logger.warning(
"main thread hang sample failed",
metadata: ["status": "\(process.terminationStatus)"])
return
}
self.logger.info(
"main thread hang sample captured",
metadata: ["sample": file.path])
}
#if DEBUG
func traceHangForTesting(
responseDelay: TimeInterval,
waitForSampleAttempt: Bool = false,
responseBeforeTrace: Bool = false)
{
self.lock.withLock {
self.isRunning = true
}
defer {
self.lock.withLock {
self.isRunning = false
}
self.onSampleAttemptForTesting = nil
}
let box = PingBox()
let pingSentAt = DispatchTime.now()
let scheduleResponse = {
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + responseDelay) {
box.markResponded()
}
}
if responseBeforeTrace {
Thread.sleep(forTimeInterval: responseDelay)
box.markResponded()
} else if waitForSampleAttempt {
self.onSampleAttemptForTesting = scheduleResponse
} else {
scheduleResponse()
}
self.traceHang(box: box, pingSentAt: pingSentAt)
}
#endif
}
@@ -0,0 +1,61 @@
import CodexBarCore
import Foundation
import Observation
enum ManagedCodexAccountCoordinatorError: Error, Equatable {
case authenticationInProgress
}
@MainActor
@Observable
final class ManagedCodexAccountCoordinator {
let service: ManagedCodexAccountService
private(set) var isAuthenticatingManagedAccount: Bool = false
private(set) var authenticatingManagedAccountID: UUID?
private(set) var isRemovingManagedAccount: Bool = false
private(set) var removingManagedAccountID: UUID?
var onManagedAccountsDidChange: (@MainActor () -> Void)?
var hasConflictingManagedAccountOperationInFlight: Bool {
self.isAuthenticatingManagedAccount || self.isRemovingManagedAccount
}
init(service: ManagedCodexAccountService = ManagedCodexAccountService()) {
self.service = service
}
func authenticateManagedAccount(
existingAccountID: UUID? = nil,
timeout: TimeInterval = 120)
async throws -> ManagedCodexAccount
{
guard self.isAuthenticatingManagedAccount == false else {
throw ManagedCodexAccountCoordinatorError.authenticationInProgress
}
self.isAuthenticatingManagedAccount = true
self.authenticatingManagedAccountID = existingAccountID
defer {
self.isAuthenticatingManagedAccount = false
self.authenticatingManagedAccountID = nil
}
let account = try await self.service.authenticateManagedAccount(
existingAccountID: existingAccountID,
timeout: timeout)
self.onManagedAccountsDidChange?()
return account
}
func removeManagedAccount(id: UUID) async throws {
self.isRemovingManagedAccount = true
self.removingManagedAccountID = id
defer {
self.isRemovingManagedAccount = false
self.removingManagedAccountID = nil
}
try await self.service.removeManagedAccount(id: id)
self.onManagedAccountsDidChange?()
}
}
@@ -0,0 +1,497 @@
import AppKit
import CodexBarCore
import Foundation
protocol ManagedCodexHomeProducing: Sendable {
func makeHomeURL() -> URL
func validateManagedHomeForDeletion(_ url: URL) throws
}
protocol ManagedCodexLoginRunning: Sendable {
func run(homePath: String, timeout: TimeInterval) async -> CodexLoginRunner.Result
}
protocol ManagedCodexIdentityReading: Sendable {
func loadAccountIdentity(homePath: String) throws -> CodexAuthBackedAccount
}
protocol ManagedCodexWorkspaceResolving: Sendable {
func resolveWorkspaceIdentity(homePath: String, providerAccountID: String) async -> CodexOpenAIWorkspaceIdentity?
func availableWorkspaceIdentities(homePath: String) async -> [CodexOpenAIWorkspaceIdentity]
}
extension ManagedCodexWorkspaceResolving {
func availableWorkspaceIdentities(homePath _: String) async -> [CodexOpenAIWorkspaceIdentity] {
[]
}
}
protocol ManagedCodexWorkspaceSelecting: Sendable {
@MainActor
func selectWorkspace(
email: String,
currentWorkspaceID: String?,
workspaces: [CodexOpenAIWorkspaceIdentity]) async -> CodexOpenAIWorkspaceIdentity?
}
enum ManagedCodexAccountServiceError: Error, Equatable {
case loginFailed(CodexLoginRunner.Result)
case missingEmail
case workspaceSelectionCancelled
case unsafeManagedHome(String)
}
extension ManagedCodexAccountServiceError {
var userFacingMessage: String {
switch self {
case let .loginFailed(result):
CodexLoginAlertPresentation.managedLoginFailureMessage(for: result)
case .missingEmail:
L("managed_login_missing_email")
case .workspaceSelectionCancelled:
L("workspace_selection_cancelled")
case let .unsafeManagedHome(path):
String(format: L("unsafe_managed_home"), path)
}
}
}
struct ManagedCodexHomeFactory: ManagedCodexHomeProducing {
let root: URL
init(root: URL = Self.defaultRootURL(), fileManager: FileManager = .default) {
let standardizedRoot = root.standardizedFileURL
if standardizedRoot.path != root.path {
self.root = standardizedRoot
} else {
self.root = root
}
_ = fileManager
}
func makeHomeURL() -> URL {
self.root.appendingPathComponent(UUID().uuidString, isDirectory: true)
}
func validateManagedHomeForDeletion(_ url: URL) throws {
let rootPath = self.root.standardizedFileURL.path
let targetPath = url.standardizedFileURL.path
let rootPrefix = rootPath.hasSuffix("/") ? rootPath : rootPath + "/"
guard targetPath.hasPrefix(rootPrefix), targetPath != rootPath else {
throw ManagedCodexAccountServiceError.unsafeManagedHome(url.path)
}
}
static func defaultRootURL(fileManager: FileManager = .default) -> URL {
let base = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? fileManager.homeDirectoryForCurrentUser
return base
.appendingPathComponent("CodexBar", isDirectory: true)
.appendingPathComponent("managed-codex-homes", isDirectory: true)
}
}
struct DefaultManagedCodexLoginRunner: ManagedCodexLoginRunning {
func run(homePath: String, timeout: TimeInterval) async -> CodexLoginRunner.Result {
await CodexLoginRunner.run(homePath: homePath, timeout: timeout)
}
}
struct DefaultManagedCodexIdentityReader: ManagedCodexIdentityReading {
func loadAccountIdentity(homePath: String) throws -> CodexAuthBackedAccount {
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
return UsageFetcher(environment: env).loadAuthBackedCodexAccount()
}
}
struct DefaultManagedCodexWorkspaceResolver: ManagedCodexWorkspaceResolving {
private let workspaceCache: CodexOpenAIWorkspaceIdentityCache
init(
workspaceCache: CodexOpenAIWorkspaceIdentityCache = CodexOpenAIWorkspaceIdentityCache())
{
self.workspaceCache = workspaceCache
}
func resolveWorkspaceIdentity(homePath: String, providerAccountID: String) async -> CodexOpenAIWorkspaceIdentity? {
let normalizedProviderAccountID = ManagedCodexAccount.normalizeProviderAccountID(providerAccountID)
?? providerAccountID
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
if let credentials = try? CodexOAuthCredentialsStore.load(env: env),
let authoritativeIdentity = try? await CodexOpenAIWorkspaceResolver.resolve(credentials: credentials)
{
try? self.workspaceCache.store(authoritativeIdentity)
return authoritativeIdentity
}
let cachedLabel = self.workspaceCache.workspaceLabel(for: normalizedProviderAccountID)
return CodexOpenAIWorkspaceIdentity(
workspaceAccountID: normalizedProviderAccountID,
workspaceLabel: cachedLabel)
}
func availableWorkspaceIdentities(homePath: String) async -> [CodexOpenAIWorkspaceIdentity] {
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
guard let credentials = try? CodexOAuthCredentialsStore.load(env: env),
let identities = try? await CodexOpenAIWorkspaceResolver.listWorkspaces(credentials: credentials)
else {
return []
}
for identity in identities {
try? self.workspaceCache.store(identity)
}
return identities
}
}
struct CodexWorkspaceAlertSelector: ManagedCodexWorkspaceSelecting {
@MainActor
func selectWorkspace(
email: String,
currentWorkspaceID: String?,
workspaces: [CodexOpenAIWorkspaceIdentity]) async -> CodexOpenAIWorkspaceIdentity?
{
guard workspaces.count > 1 else { return workspaces.first }
let popup = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 360, height: 26), pullsDown: false)
let sortedWorkspaces = workspaces.sorted { lhs, rhs in
self.workspaceTitle(lhs) < self.workspaceTitle(rhs)
}
for workspace in sortedWorkspaces {
popup.addItem(withTitle: self.workspaceTitle(workspace))
popup.lastItem?.representedObject = workspace.workspaceAccountID
}
if let currentWorkspaceID,
let selectedIndex = sortedWorkspaces.firstIndex(where: { $0.workspaceAccountID == currentWorkspaceID })
{
popup.selectItem(at: selectedIndex)
}
let alert = NSAlert()
alert.messageText = L("Choose Codex workspace")
alert.informativeText = String(format: L("multiple_workspaces_found"), email)
alert.alertStyle = .informational
alert.accessoryView = popup
alert.addButton(withTitle: L("Add Workspace"))
alert.addButton(withTitle: L("Cancel"))
guard alert.runModal() == .alertFirstButtonReturn else {
return nil
}
let selectedWorkspaceID = popup.selectedItem?.representedObject as? String
return sortedWorkspaces.first { $0.workspaceAccountID == selectedWorkspaceID }
}
private func workspaceTitle(_ workspace: CodexOpenAIWorkspaceIdentity) -> String {
workspace.workspaceLabel ?? workspace.workspaceAccountID
}
}
@MainActor
final class ManagedCodexAccountService {
private let store: any ManagedCodexAccountStoring
private let homeFactory: any ManagedCodexHomeProducing
private let loginRunner: any ManagedCodexLoginRunning
private let identityReader: any ManagedCodexIdentityReading
private let workspaceResolver: any ManagedCodexWorkspaceResolving
private let workspaceSelector: any ManagedCodexWorkspaceSelecting
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
homeFactory: any ManagedCodexHomeProducing,
loginRunner: any ManagedCodexLoginRunning,
identityReader: any ManagedCodexIdentityReading,
workspaceResolver: any ManagedCodexWorkspaceResolving = DefaultManagedCodexWorkspaceResolver(),
workspaceSelector: any ManagedCodexWorkspaceSelecting = CodexWorkspaceAlertSelector(),
fileManager: FileManager = .default)
{
self.store = store
self.homeFactory = homeFactory
self.loginRunner = loginRunner
self.identityReader = identityReader
self.workspaceResolver = workspaceResolver
self.workspaceSelector = workspaceSelector
self.fileManager = fileManager
}
convenience init(fileManager: FileManager = .default) {
self.init(
store: FileManagedCodexAccountStore(fileManager: fileManager),
homeFactory: ManagedCodexHomeFactory(fileManager: fileManager),
loginRunner: DefaultManagedCodexLoginRunner(),
identityReader: DefaultManagedCodexIdentityReader(),
workspaceResolver: DefaultManagedCodexWorkspaceResolver(),
workspaceSelector: CodexWorkspaceAlertSelector(),
fileManager: fileManager)
}
func authenticateManagedAccount(
existingAccountID: UUID? = nil,
timeout: TimeInterval = 120)
async throws -> ManagedCodexAccount
{
let snapshot = try self.store.loadAccounts()
let homeURL = self.homeFactory.makeHomeURL()
try self.fileManager.createDirectory(at: homeURL, withIntermediateDirectories: true)
let account: ManagedCodexAccount
let existingHomePathsToDelete: [String]
do {
let result = await self.loginRunner.run(homePath: homeURL.path, timeout: timeout)
guard case .success = result.outcome else { throw ManagedCodexAccountServiceError.loginFailed(result) }
let identity = try self.identityReader.loadAccountIdentity(homePath: homeURL.path)
guard let rawEmail = identity.email?.trimmingCharacters(in: .whitespacesAndNewlines),
!rawEmail.isEmpty
else {
throw ManagedCodexAccountServiceError.missingEmail
}
let authenticatedProviderAccountID: String? = switch identity.identity {
case let .providerAccount(id):
ManagedCodexAccount.normalizeProviderAccountID(id)
case .emailOnly, .unresolved:
nil
}
let selectedWorkspace = try await self.selectedWorkspaceIdentity(
email: rawEmail,
homePath: homeURL.path,
authenticatedProviderAccountID: authenticatedProviderAccountID)
let providerAccountID = selectedWorkspace?.workspaceAccountID ?? authenticatedProviderAccountID
let workspaceIdentity: CodexOpenAIWorkspaceIdentity? = if let selectedWorkspace {
selectedWorkspace
} else {
await self.resolvedWorkspaceIdentity(
homePath: homeURL.path,
providerAccountID: providerAccountID)
}
let now = Date().timeIntervalSince1970
let existing = self.reconciledExistingAccount(
authenticatedEmail: rawEmail,
providerAccountID: providerAccountID,
existingAccountID: existingAccountID,
snapshot: snapshot)
let persistedMetadata = self.persistedProviderMetadata(
authenticatedProviderAccountID: providerAccountID,
resolvedWorkspaceIdentity: workspaceIdentity,
existingAccount: existing)
account = ManagedCodexAccount(
id: existing?.id ?? UUID(),
email: rawEmail,
providerAccountID: persistedMetadata.providerAccountID,
workspaceLabel: persistedMetadata.workspaceLabel,
workspaceAccountID: persistedMetadata.workspaceAccountID,
authFingerprint: CodexAuthFingerprint.fingerprint(
homePath: homeURL.path,
fileManager: self.fileManager),
managedHomePath: homeURL.path,
createdAt: existing?.createdAt ?? now,
updatedAt: now,
lastAuthenticatedAt: now)
let replacedAccountIDs = self.replacedAccountIDs(
authenticatedEmail: rawEmail,
providerAccountID: providerAccountID,
existingAccountID: existingAccountID,
matchedAccountID: existing?.id,
snapshot: snapshot)
existingHomePathsToDelete = snapshot.accounts
.filter { replacedAccountIDs.contains($0.id) }
.map(\.managedHomePath)
let updatedSnapshot = ManagedCodexAccountSet(
version: snapshot.version,
accounts: snapshot.accounts.filter { replacedAccountIDs.contains($0.id) == false } + [account])
try self.store.storeAccounts(updatedSnapshot)
} catch {
try? self.removeManagedHomeIfSafe(atPath: homeURL.path)
throw error
}
for existingHomePathToDelete in existingHomePathsToDelete where existingHomePathToDelete != homeURL.path {
try? self.removeManagedHomeIfSafe(atPath: existingHomePathToDelete)
}
return account
}
func removeManagedAccount(id: UUID) async throws {
let snapshot = try self.store.loadAccounts()
guard let account = snapshot.account(id: id) else { return }
let homeURL = URL(fileURLWithPath: account.managedHomePath, isDirectory: true)
let canDeleteHome = (try? self.homeFactory.validateManagedHomeForDeletion(homeURL)) != nil
let remaining = snapshot.accounts.filter { $0.id != id }
try self.store.storeAccounts(ManagedCodexAccountSet(
version: snapshot.version,
accounts: remaining))
if canDeleteHome, self.fileManager.fileExists(atPath: homeURL.path) {
try? self.fileManager.removeItem(at: homeURL)
}
}
private func removeManagedHomeIfSafe(atPath path: String) throws {
let homeURL = URL(fileURLWithPath: path, isDirectory: true)
try self.homeFactory.validateManagedHomeForDeletion(homeURL)
if self.fileManager.fileExists(atPath: homeURL.path) {
try self.fileManager.removeItem(at: homeURL)
}
}
private func selectedWorkspaceIdentity(
email: String,
homePath: String,
authenticatedProviderAccountID: String?) async throws -> CodexOpenAIWorkspaceIdentity?
{
let workspaces = await self.workspaceResolver.availableWorkspaceIdentities(homePath: homePath)
guard workspaces.count > 1 else {
return workspaces.first { $0.workspaceAccountID == authenticatedProviderAccountID }
}
guard let selected = await self.workspaceSelector.selectWorkspace(
email: email,
currentWorkspaceID: authenticatedProviderAccountID,
workspaces: workspaces)
else {
throw ManagedCodexAccountServiceError.workspaceSelectionCancelled
}
try self.persistSelectedWorkspaceID(selected.workspaceAccountID, homePath: homePath)
return selected
}
private func resolvedWorkspaceIdentity(
homePath: String,
providerAccountID: String?) async -> CodexOpenAIWorkspaceIdentity?
{
guard let providerAccountID else { return nil }
return await self.workspaceResolver.resolveWorkspaceIdentity(
homePath: homePath,
providerAccountID: providerAccountID)
}
private func persistSelectedWorkspaceID(_ workspaceID: String, homePath: String) throws {
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
let credentials = try CodexOAuthCredentialsStore.load(env: env)
try CodexOAuthCredentialsStore.save(
CodexOAuthCredentials(
accessToken: credentials.accessToken,
refreshToken: credentials.refreshToken,
idToken: credentials.idToken,
accountId: workspaceID,
lastRefresh: credentials.lastRefresh),
env: env)
}
private func reconciledExistingAccount(
authenticatedEmail: String,
providerAccountID: String?,
existingAccountID: UUID?,
snapshot: ManagedCodexAccountSet)
-> ManagedCodexAccount?
{
if let providerAccountID,
let existingByProviderAccountID = snapshot.account(
email: authenticatedEmail,
providerAccountID: providerAccountID)
{
return existingByProviderAccountID
}
if let existingAccountID,
let existingByID = snapshot.account(id: existingAccountID),
existingByID.email == Self.normalizeEmail(authenticatedEmail),
providerAccountID == nil || existingByID.providerAccountID == nil
{
return existingByID
}
guard providerAccountID == nil else {
return nil
}
// Email-only reconciliation is a legacy/hardening fallback. Once an auth payload carries a
// provider account ID, matching must stay on that ID so same-email workspaces can coexist.
return snapshot.account(email: authenticatedEmail)
}
private func replacedAccountIDs(
authenticatedEmail: String,
providerAccountID: String?,
existingAccountID: UUID?,
matchedAccountID: UUID?,
snapshot: ManagedCodexAccountSet) -> Set<UUID>
{
var ids: Set<UUID> = []
let normalizedEmail = Self.normalizeEmail(authenticatedEmail)
if let matchedAccountID {
ids.insert(matchedAccountID)
}
if providerAccountID != nil {
let legacySameEmailIDs = snapshot.accounts
.filter {
$0.id != matchedAccountID &&
$0.providerAccountID == nil &&
$0.email == normalizedEmail
}
.map(\.id)
ids.formUnion(legacySameEmailIDs)
}
guard let existingAccountID,
existingAccountID != matchedAccountID,
let existingByID = snapshot.account(id: existingAccountID)
else {
return ids
}
if existingByID.providerAccountID == nil,
existingByID.email == normalizedEmail,
providerAccountID != nil
{
ids.insert(existingAccountID)
}
return ids
}
private static func normalizeEmail(_ email: String) -> String {
email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
private func persistedProviderMetadata(
authenticatedProviderAccountID: String?,
resolvedWorkspaceIdentity: CodexOpenAIWorkspaceIdentity?,
existingAccount: ManagedCodexAccount?) -> (
providerAccountID: String?,
workspaceLabel: String?,
workspaceAccountID: String?)
{
if let authenticatedProviderAccountID {
let isExistingProviderMatch = existingAccount?.providerAccountID == authenticatedProviderAccountID
return (
providerAccountID: authenticatedProviderAccountID,
workspaceLabel: resolvedWorkspaceIdentity?.workspaceLabel
?? (isExistingProviderMatch ? existingAccount?.workspaceLabel : nil),
workspaceAccountID: resolvedWorkspaceIdentity?.workspaceAccountID ??
(isExistingProviderMatch ? existingAccount?.workspaceAccountID : nil) ??
authenticatedProviderAccountID)
}
guard let existingAccount, existingAccount.providerAccountID != nil else {
return (providerAccountID: nil, workspaceLabel: nil, workspaceAccountID: nil)
}
return (
providerAccountID: existingAccount.providerAccountID,
workspaceLabel: existingAccount.workspaceLabel,
workspaceAccountID: existingAccount.workspaceAccountID ?? existingAccount.providerAccountID)
}
}
@@ -0,0 +1,149 @@
import CodexBarCore
import Dispatch
import Foundation
@MainActor
struct MemoryPressureCacheTrimSummary: Equatable {
var menuCardHeights = 0
var menuWidths = 0
var mergedSwitcherSelections = 0
var recycledMenuCardViews = 0
var openAIWebDebugLines = 0
var total: Int {
self.menuCardHeights +
self.menuWidths +
self.mergedSwitcherSelections +
self.recycledMenuCardViews +
self.openAIWebDebugLines
}
var metadata: [String: String] {
[
"menuCardHeights": "\(self.menuCardHeights)",
"menuWidths": "\(self.menuWidths)",
"mergedSwitcherSelections": "\(self.mergedSwitcherSelections)",
"recycledMenuCardViews": "\(self.recycledMenuCardViews)",
"openAIWebDebugLines": "\(self.openAIWebDebugLines)",
"total": "\(self.total)",
]
}
mutating func merge(_ other: MemoryPressureCacheTrimSummary) {
self.menuCardHeights += other.menuCardHeights
self.menuWidths += other.menuWidths
self.mergedSwitcherSelections += other.mergedSwitcherSelections
self.recycledMenuCardViews += other.recycledMenuCardViews
self.openAIWebDebugLines += other.openAIWebDebugLines
}
}
@MainActor
final class MemoryPressureMonitor {
typealias CacheTrimHandler = @MainActor () -> MemoryPressureCacheTrimSummary
private let logger = CodexBarLog.logger(LogCategories.memoryPressure)
private let releaseFreeMallocPages: @Sendable () -> Void
private let trimAppCaches: CacheTrimHandler
private var source: DispatchSourceMemoryPressure?
init(
trimAppCaches: @escaping CacheTrimHandler = { MemoryPressureCacheTrimSummary() },
releaseFreeMallocPages: @escaping @Sendable () -> Void = {
MemoryPressureRelief.releaseFreeMallocPages()
})
{
self.trimAppCaches = trimAppCaches
self.releaseFreeMallocPages = releaseFreeMallocPages
}
func start() {
guard self.source == nil else { return }
let source = DispatchSource.makeMemoryPressureSource(
eventMask: [.warning, .critical],
queue: .global(qos: .utility))
source.setEventHandler(handler: Self.makeEventHandler(
source: source,
handle: { [weak self] isWarning, isCritical in
self?.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical)
}))
self.source = source
source.resume()
}
nonisolated static func makeEventHandler(
source: DispatchSourceMemoryPressure,
handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void)
-> @Sendable () -> Void
{
self.makeEventHandler(
eventReader: { [weak source] in source?.data ?? [] },
handle: handle)
}
nonisolated static func makeEventHandler(
eventReader: @escaping @Sendable () -> DispatchSource.MemoryPressureEvent,
handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void)
-> @Sendable () -> Void
{
// DispatchSource invokes this on the utility queue. Keep the handler
// nonisolated, then hop to MainActor for app-state cleanup.
{ @Sendable in
let event = eventReader()
let isWarning = event.contains(.warning)
let isCritical = event.contains(.critical)
Task { @MainActor in
handle(isWarning, isCritical)
}
}
}
func stop() {
self.source?.cancel()
self.source = nil
}
deinit {
self.source?.cancel()
}
#if DEBUG
func handleMemoryPressureForTesting(isWarning: Bool, isCritical: Bool) {
self.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical)
}
#endif
private func handleMemoryPressure(isWarning: Bool, isCritical: Bool) {
let level = if isCritical {
"critical"
} else if isWarning {
"warning"
} else {
"normal"
}
self.logger.warning("System memory pressure", metadata: ["level": level])
#if DEBUG
let cachedWebViewsBefore = OpenAIDashboardFetcher.cachedWebViewCountForTesting()
#endif
OpenAIDashboardFetcher.evictIdleCachedWebViews()
#if DEBUG
let cachedWebViewsAfter = OpenAIDashboardFetcher.cachedWebViewCountForTesting()
self.logger.info(
"Memory pressure OpenAI webview cache",
metadata: [
"before": "\(cachedWebViewsBefore)",
"after": "\(cachedWebViewsAfter)",
"evicted": "\(max(0, cachedWebViewsBefore - cachedWebViewsAfter))",
])
#endif
let trimSummary = self.trimAppCaches()
if trimSummary.total > 0 {
self.logger.info("Trimmed app caches for memory pressure", metadata: trimSummary.metadata)
}
let releaseFreeMallocPages = self.releaseFreeMallocPages
Task.detached(priority: .utility) {
releaseFreeMallocPages()
}
}
}
@@ -0,0 +1,7 @@
import Darwin
enum MemoryPressureRelief {
static func releaseFreeMallocPages() {
_ = malloc_zone_pressure_relief(nil, 0)
}
}
+31
View File
@@ -0,0 +1,31 @@
import Foundation
/// Controls what the menu bar displays when brand icon mode is enabled.
enum MenuBarDisplayMode: String, CaseIterable, Identifiable {
case percent
case pace
case both
case resetTime
var id: String {
self.rawValue
}
var label: String {
switch self {
case .percent: L("display_mode_percent")
case .pace: L("display_mode_pace")
case .both: L("display_mode_both")
case .resetTime: L("display_mode_reset_time")
}
}
var description: String {
switch self {
case .percent: L("display_mode_percent_desc")
case .pace: L("display_mode_pace_desc")
case .both: L("display_mode_both_desc")
case .resetTime: L("display_mode_reset_time_desc")
}
}
}
+182
View File
@@ -0,0 +1,182 @@
import CodexBarCore
import Foundation
enum MenuBarDisplayText {
static func percentText(window: RateWindow?, showUsed: Bool) -> String? {
guard let window else { return nil }
let percent = showUsed ? window.usedPercent : window.remainingPercent
return UsageFormatter.percentString(percent)
}
static func paceText(pace: UsagePace?) -> String? {
guard let pace else { return nil }
let deltaValue = Int(abs(pace.deltaPercent).rounded())
if deltaValue == 0 { return "0%" }
let sign = pace.deltaPercent >= 0 ? "+" : "-"
return "\(sign)\(deltaValue)%"
}
/// Combined "session · weekly" menu-bar text shared by providers that expose both a
/// session (5h) and weekly (7d) lane, e.g. Codex and Claude.
static func combinedSessionWeeklyPercentText(
sessionWindow: RateWindow?,
weeklyWindow: RateWindow?,
showUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle = .countdown,
showsResetTimeWhenExhausted: Bool = false,
now: Date = .init())
-> String?
{
var parts: [String] = []
if let sessionWindow,
let session = self.laneValueText(
window: sessionWindow,
showUsed: showUsed,
resetTimeDisplayStyle: resetTimeDisplayStyle,
showsResetTimeWhenExhausted: showsResetTimeWhenExhausted,
now: now)
{
parts.append("\(self.sessionWindowLabel(window: sessionWindow)) \(session)")
}
if let weeklyWindow,
let weekly = self.laneValueText(
window: weeklyWindow,
showUsed: showUsed,
resetTimeDisplayStyle: resetTimeDisplayStyle,
showsResetTimeWhenExhausted: showsResetTimeWhenExhausted,
now: now)
{
parts.append("W \(weekly)")
}
return parts.isEmpty ? nil : parts.joined(separator: " · ")
}
private static func laneValueText(
window: RateWindow,
showUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle,
showsResetTimeWhenExhausted: Bool,
now: Date) -> String?
{
if let resetText = self.exhaustedResetText(
window: window,
enabled: showsResetTimeWhenExhausted,
style: resetTimeDisplayStyle,
now: now)
{
return resetText
}
return self.percentText(window: window, showUsed: showUsed)
}
private static func sessionWindowLabel(window: RateWindow) -> String {
guard let minutes = window.windowMinutes, minutes > 0 else { return "S" }
guard minutes.isMultiple(of: 60) else { return "\(minutes)m" }
return "\(minutes / 60)h"
}
static func displayText(
mode: MenuBarDisplayMode,
percentWindow: RateWindow?,
pace: UsagePace? = nil,
showUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle = .countdown,
showsResetTimeWhenExhausted: Bool = false,
now: Date = .init()) -> String?
{
if mode != .resetTime,
showsResetTimeWhenExhausted,
let percentWindow,
percentWindow.remainingPercent <= 0
{
if let resetText = self.exhaustedResetText(
window: percentWindow,
enabled: true,
style: resetTimeDisplayStyle,
now: now)
{
return resetText
}
// Smart mode cannot replace an exhausted percentage unless the reset is concrete, future,
// and schedulable. Preserve the quota signal in pace/both modes too; a pace from another
// combined lane must not hide that this displayed lane is already exhausted.
return self.percentText(window: percentWindow, showUsed: showUsed)
}
switch mode {
case .percent:
return self.percentText(window: percentWindow, showUsed: showUsed)
case .pace:
// Pace can be temporarily unavailable near a reset or when a provider omits window metadata.
// Keep the selected quota visible instead of collapsing the status item to an icon-only state.
return self.paceText(pace: pace)
?? self.percentText(window: percentWindow, showUsed: showUsed)
case .both:
guard let percent = percentText(window: percentWindow, showUsed: showUsed) else { return nil }
// Fall back to percent-only when pace is unavailable (e.g. Copilot)
guard let paceText = Self.paceText(pace: pace) else { return percent }
return "\(percent) · \(paceText)"
case .resetTime:
guard let percentWindow else { return nil }
return self.resetTimeText(window: percentWindow, style: resetTimeDisplayStyle, now: now)
?? self.percentText(window: percentWindow, showUsed: showUsed)
}
}
/// " " reset text for a window, or nil when it carries no usable reset metadata.
static func resetTimeText(
window: RateWindow,
style: ResetTimeDisplayStyle,
now: Date) -> String?
{
if let resetsAt = window.resetsAt {
let description = switch style {
case .countdown:
UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
case .absolute:
UsageFormatter.resetDescription(from: resetsAt, now: now)
}
return "\(description)"
}
if let resetDescription = self.resetMetadataText(window.resetDescription) {
return "\(resetDescription)"
}
return nil
}
/// Smart-mode replacement: when enabled and the quota is exhausted (0% remaining, regardless of
/// whether the display shows used or remaining), surface the reset time instead of a dead percent.
///
/// Requires a concrete, still-future `resetsAt`. The smart option only replaces the percent when it
/// has a reset time it can both render as a live countdown/clock AND hand to the refresh scheduler,
/// so the lane keeps ticking and flips back to the percentage once the reset passes. Windows with
/// only textual reset metadata (`resetDescription`, no `resetsAt`) or an already-elapsed reset can't
/// be scheduled, so they keep showing the percent instead of freezing on stale reset text.
private static func exhaustedResetText(
window: RateWindow?,
enabled: Bool,
style: ResetTimeDisplayStyle,
now: Date) -> String?
{
guard enabled, let window, window.remainingPercent <= 0 else { return nil }
guard let resetsAt = window.resetsAt, resetsAt > now else { return nil }
return self.resetTimeText(window: window, style: style, now: now)
}
private static func resetMetadataText(_ description: String?) -> String? {
guard let description else { return nil }
let trimmed = description.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
// RateWindow.resetDescription predates provider-specific detail fields and is also used for
// request/token summaries. Only trust phrases that explicitly describe reset timing.
let normalized = trimmed.lowercased()
let resetPrefixes = [
"reset ", "resets ", "in ", "today ", "today,", "tomorrow ", "tomorrow,", "next ",
"expire ", "expires ", "refill ", "refills ",
]
let exactResetDescriptions = ["today", "tomorrow", "expired", "now", "soon"]
return exactResetDescriptions.contains(normalized) || resetPrefixes.contains(where: normalized.hasPrefix)
? trimmed
: nil
}
}
@@ -0,0 +1,283 @@
import CodexBarCore
import Foundation
enum MenuBarMetricWindowResolver {
private enum Lane {
case primary
case secondary
case tertiary
}
static func rateWindow(
preference: MenuBarMetricPreference,
provider: UsageProvider,
snapshot: UsageSnapshot?,
supportsAverage: Bool)
-> RateWindow?
{
guard let snapshot else { return nil }
switch preference {
case .monthlyPlan:
return snapshot.extraRateWindows?.first { $0.id == "mistral-monthly-plan" }?.window
case .extraUsage:
return Self.extraUsageWindow(snapshot: snapshot)
case .tertiary:
return Self.requestedWindow(
provider: provider,
snapshot: snapshot,
lanes: Self.tertiaryOrder(for: provider))
case .primary:
return Self.requestedWindow(
provider: provider,
snapshot: snapshot,
lanes: Self.primaryOrder(for: provider))
case .secondary:
return Self.requestedWindow(
provider: provider,
snapshot: snapshot,
lanes: Self.secondaryOrder(for: provider))
case .primaryAndSecondary:
// Claude accounts that only expose an enterprise/extra-usage spend limit have no real
// session/weekly lanes; surface the spend limit (as `.automatic` does) instead of an empty
// or 0% placeholder lane.
if provider == .claude, let spendLimit = Self.claudeSpendLimitWindow(snapshot: snapshot) {
return spendLimit
}
return Self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
tertiary: nil)
case .average:
return Self.averageWindow(provider: provider, snapshot: snapshot, supportsAverage: supportsAverage)
case .automatic:
return Self.automaticWindow(provider: provider, snapshot: snapshot)
}
}
private static func tertiaryOrder(for provider: UsageProvider) -> [Lane] {
if provider == .zai {
return [.tertiary, .primary, .secondary]
}
if provider == .perplexity || provider == .cursor || provider == .antigravity {
return [.tertiary, .secondary, .primary]
}
return [.primary, .secondary]
}
private static func primaryOrder(for provider: UsageProvider) -> [Lane] {
if provider == .zai {
return [.primary, .tertiary, .secondary]
}
if provider == .perplexity || provider == .antigravity {
return [.primary, .secondary, .tertiary]
}
return [.primary, .secondary]
}
private static func secondaryOrder(for provider: UsageProvider) -> [Lane] {
if provider == .zai || provider == .antigravity {
return [.secondary, .primary, .tertiary]
}
if provider == .perplexity {
return [.secondary, .tertiary, .primary]
}
return [.secondary, .primary]
}
private static func averageWindow(
provider: UsageProvider,
snapshot: UsageSnapshot,
supportsAverage: Bool)
-> RateWindow?
{
guard supportsAverage,
let primary = snapshot.primary,
let secondary = snapshot.secondary
else {
if provider == .antigravity {
return self.window(in: snapshot, following: [.primary, .secondary, .tertiary])
}
return snapshot.primary ?? snapshot.secondary
}
let usedPercent = (primary.usedPercent + secondary.usedPercent) / 2
return RateWindow(usedPercent: usedPercent, windowMinutes: nil, resetsAt: nil, resetDescription: nil)
}
private static func automaticWindow(provider: UsageProvider, snapshot: UsageSnapshot) -> RateWindow? {
if provider == .antigravity {
if let window = mostConstrainedAntigravityQuotaSummaryWindow(snapshot: snapshot) {
return window
}
return self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
tertiary: snapshot.tertiary)
?? self.mostConstrainedAntigravityLegacyExtraWindow(snapshot: snapshot)
}
if provider == .perplexity {
return snapshot.automaticPerplexityWindow()
}
if provider == .zai {
return self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.tertiary,
tertiary: nil) ?? snapshot.secondary
}
if provider == .factory || provider == .kimi {
return snapshot.secondary ?? snapshot.primary
}
if provider == .litellm {
return snapshot.secondary ?? snapshot.primary
}
if provider == .copilot,
let primary = snapshot.primary,
let secondary = snapshot.secondary
{
return primary.usedPercent >= secondary.usedPercent ? primary : secondary
}
if provider == .cursor {
return Self.mostConstrainedCursorWindow(
total: snapshot.primary,
auto: snapshot.secondary,
api: snapshot.tertiary)
}
if provider == .minimax {
return Self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
tertiary: snapshot.tertiary)
}
if provider == .claude, let spendLimit = Self.claudeSpendLimitWindow(snapshot: snapshot) {
return spendLimit
}
return snapshot.primary ?? snapshot.secondary
}
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private static let antigravityCompactFallbackWindowIDPrefix = "antigravity-compact-fallback-"
private static func mostConstrainedAntigravityQuotaSummaryWindow(snapshot: UsageSnapshot) -> RateWindow? {
let windows = snapshot.extraRateWindows?
.filter { $0.usageKnown && $0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix) }
.map(\.window) ?? []
guard !windows.isEmpty else { return nil }
let usableWindows = windows.filter { $0.usedPercent < 100 }
if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) {
return maxUsable
}
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}
private static func mostConstrainedAntigravityLegacyExtraWindow(snapshot: UsageSnapshot) -> RateWindow? {
let windows = snapshot.extraRateWindows?
.filter {
$0.usageKnown && $0.id.hasPrefix(Self.antigravityCompactFallbackWindowIDPrefix)
}
.map(\.window) ?? []
guard !windows.isEmpty else { return nil }
let usableWindows = windows.filter { $0.usedPercent < 100 }
if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) {
return maxUsable
}
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}
private static func requestedWindow(
provider: UsageProvider,
snapshot: UsageSnapshot,
lanes: [Lane]) -> RateWindow?
{
self.window(in: snapshot, following: lanes)
?? (provider == .antigravity
? self.mostConstrainedAntigravityLegacyExtraWindow(snapshot: snapshot)
: nil)
}
private static func window(in snapshot: UsageSnapshot, following lanes: [Lane]) -> RateWindow? {
for lane in lanes {
if let window = self.window(in: snapshot, lane: lane) {
return window
}
}
return nil
}
private static func window(in snapshot: UsageSnapshot, lane: Lane) -> RateWindow? {
switch lane {
case .primary:
snapshot.primary
case .secondary:
snapshot.secondary
case .tertiary:
snapshot.tertiary
}
}
private static func mostConstrainedWindow(
primary: RateWindow?,
secondary: RateWindow?,
tertiary: RateWindow?)
-> RateWindow?
{
let windows = [primary, secondary, tertiary].compactMap(\.self)
guard !windows.isEmpty else { return nil }
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}
private static func mostConstrainedCursorWindow(
total: RateWindow?,
auto: RateWindow?,
api: RateWindow?)
-> RateWindow?
{
if let total, total.usedPercent >= 100 {
return total
}
let subquotaWindows = [auto, api].compactMap(\.self)
let usableSubquotaWindows = subquotaWindows.filter { $0.usedPercent < 100 }
if !subquotaWindows.isEmpty, usableSubquotaWindows.isEmpty {
return subquotaWindows.max(by: { $0.usedPercent < $1.usedPercent })
}
return ([total].compactMap(\.self) + usableSubquotaWindows)
.max(by: { $0.usedPercent < $1.usedPercent })
}
/// The Claude spend-limit window when the account only exposes an enterprise/extra-usage spend limit
/// and has no real session/weekly quota lanes (`primary` nil, a `.spendLimit` window, or an explicitly
/// marked placeholder). Lets the automatic and combined metrics surface the spend limit instead of an empty
/// or 0% placeholder lane. Returns nil for accounts that expose genuine quota lanes.
static func claudeSpendLimitWindow(snapshot: UsageSnapshot) -> RateWindow? {
guard self.shouldUseClaudeSpendLimit(providerCost: snapshot.providerCost, snapshot: snapshot) else {
return nil
}
return self.extraUsageWindow(snapshot: snapshot)
}
private static func shouldUseClaudeSpendLimit(
providerCost: ProviderCostSnapshot?,
snapshot: UsageSnapshot)
-> Bool
{
guard providerCost?.limit ?? 0 > 0,
snapshot.secondary == nil,
snapshot.tertiary == nil
else { return false }
guard let primary = snapshot.primary else { return true }
return primary.isSyntheticPlaceholder
}
private static func extraUsageWindow(snapshot: UsageSnapshot?) -> RateWindow? {
guard let cost = snapshot?.providerCost, cost.limit > 0 else { return nil }
let usedPercent = max(0, min(100, (cost.used / cost.limit) * 100))
return RateWindow(
usedPercent: usedPercent,
windowMinutes: nil,
resetsAt: cost.resetsAt,
resetDescription: nil)
}
}
@@ -0,0 +1,54 @@
import Foundation
enum MenuBarStatusItemDefaultsRepair {
static let didRepairKey = "hasRepairedHiddenStatusItemVisibilityDefaults"
private static let visibilityPrefix = "NSStatusItem VisibleCC "
private static let legacyAutosavePrefix = "codexbar-"
static func repairHiddenVisibilityDefaultsIfNeeded(defaults: UserDefaults) -> [String] {
guard !defaults.bool(forKey: self.didRepairKey) else { return [] }
let repairedKeys = defaults.dictionaryRepresentation().keys
.filter { key in
self.shouldRepair(key: key, value: defaults.object(forKey: key))
}
.sorted()
for key in repairedKeys {
defaults.removeObject(forKey: key)
}
defaults.set(true, forKey: self.didRepairKey)
return repairedKeys
}
static func shouldRepair(key: String, value: Any?) -> Bool {
guard key.hasPrefix(self.visibilityPrefix), self.isFalse(value) else { return false }
let itemName = String(key.dropFirst(self.visibilityPrefix.count))
return itemName.hasPrefix(self.legacyAutosavePrefix) || self.isDefaultStatusItemName(itemName)
}
static func visibilityDefault(defaults: UserDefaults, autosaveName: String) -> Bool? {
guard !autosaveName.isEmpty else { return nil }
return self.boolValue(defaults.object(forKey: self.visibilityPrefix + autosaveName))
}
private static func isDefaultStatusItemName(_ itemName: String) -> Bool {
guard itemName.hasPrefix("Item-") else { return false }
return itemName.dropFirst("Item-".count).allSatisfy(\.isNumber)
}
private static func isFalse(_ value: Any?) -> Bool {
self.boolValue(value) == false
}
private static func boolValue(_ value: Any?) -> Bool? {
switch value {
case let number as NSNumber:
number.boolValue
case let bool as Bool:
bool
default:
nil
}
}
}
@@ -0,0 +1,61 @@
import AppKit
@MainActor
enum MenuBarStatusItemPlacementPreflight {
static let preferredPositionPrefix = "NSStatusItem Preferred Position "
static let suspiciousPreferredPositionPadding: Double = 512
static func preferredPositionKey(autosaveName: String) -> String {
"\(self.preferredPositionPrefix)\(autosaveName)"
}
@discardableResult
static func prepare(
defaults: UserDefaults,
autosaveName: String,
legacyDefaultItemIndex: Int? = nil,
maximumPreferredPosition: Double? = currentMaximumPreferredPosition())
-> Bool
{
let key = self.preferredPositionKey(autosaveName: autosaveName)
var repaired = self.clearPreferredPositionIfNeeded(
defaults: defaults,
key: key,
maximumPreferredPosition: maximumPreferredPosition)
if let legacyDefaultItemIndex {
let legacyKey = self.preferredPositionKey(autosaveName: "Item-\(legacyDefaultItemIndex)")
repaired = self.clearPreferredPositionIfNeeded(
defaults: defaults,
key: legacyKey,
maximumPreferredPosition: maximumPreferredPosition) || repaired
}
return repaired
}
static func shouldClearPreferredPosition(_ value: Any, maximumPreferredPosition: Double?) -> Bool {
guard let number = value as? NSNumber else { return true }
let position = number.doubleValue
if position <= 0 {
return true
}
guard let maximumPreferredPosition else { return false }
return position > maximumPreferredPosition + self.suspiciousPreferredPositionPadding
}
private static func clearPreferredPositionIfNeeded(
defaults: UserDefaults,
key: String,
maximumPreferredPosition: Double?)
-> Bool
{
guard let value = defaults.object(forKey: key),
self.shouldClearPreferredPosition(value, maximumPreferredPosition: maximumPreferredPosition)
else { return false }
defaults.removeObject(forKey: key)
return true
}
private static func currentMaximumPreferredPosition() -> Double? {
NSScreen.screens.map { Double($0.frame.maxX) }.max()
}
}
@@ -0,0 +1,110 @@
import AppKit
import CoreGraphics
import Foundation
struct MenuBarStatusItemWindowSnapshot: Equatable, CustomStringConvertible {
let name: String
let ownerName: String
let bounds: CGRect
let isOnscreen: Bool
let displayBounds: CGRect?
var isWithinDisplayBounds: Bool {
guard let displayBounds else { return false }
return displayBounds.contains(self.bounds)
}
var isTahoeBlockedProxy: Bool {
self.ownerName == "Control Center"
&& self.isOnscreen
&& abs(self.bounds.minX) <= 1
&& self.bounds.maxY <= 0
&& self.bounds.width > 0
&& self.bounds.height > 0
&& !self.isWithinDisplayBounds
}
var description: String {
let display = self.displayBounds.map {
"display=\(Int($0.minX)),\(Int($0.minY)) \(Int($0.width))x\(Int($0.height))"
} ?? "display=nil"
return "name=\(self.name),owner=\(self.ownerName),x=\(Int(self.bounds.minX)),"
+ "w=\(Int(self.bounds.width)),onscreen=\(self.isOnscreen),"
+ "withinDisplay=\(self.isWithinDisplayBounds),\(display)"
}
}
enum MenuBarStatusItemWindowProbe {
static func snapshots(matching names: Set<String>) -> [MenuBarStatusItemWindowSnapshot] {
self.snapshots(
matching: names,
windowInfo: self.windowInfo(),
displayBounds: NSScreen.screens.map(\.frame))
}
static func snapshots(
matching names: Set<String>,
windowInfo: [[String: Any]],
displayBounds: [CGRect])
-> [MenuBarStatusItemWindowSnapshot]
{
guard !names.isEmpty else { return [] }
return windowInfo.compactMap { record in
self.snapshot(record: record, matching: names, displayBounds: displayBounds)
}
}
private static func windowInfo() -> [[String: Any]] {
guard let windows = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as? [[String: Any]] else {
return []
}
return windows
}
private static func snapshot(
record: [String: Any],
matching names: Set<String>,
displayBounds: [CGRect])
-> MenuBarStatusItemWindowSnapshot?
{
guard let name = record[kCGWindowName as String] as? String,
names.contains(name),
let bounds = self.bounds(record[kCGWindowBounds as String])
else { return nil }
let ownerName = record[kCGWindowOwnerName as String] as? String ?? "unknown"
let isOnscreen = (record[kCGWindowIsOnscreen as String] as? NSNumber)?.boolValue
?? record[kCGWindowIsOnscreen as String] as? Bool
?? false
return MenuBarStatusItemWindowSnapshot(
name: name,
ownerName: ownerName,
bounds: bounds,
isOnscreen: isOnscreen,
displayBounds: displayBounds.first { $0.intersects(bounds) })
}
private static func bounds(_ value: Any?) -> CGRect? {
guard let dictionary = value as? [String: Any],
let x = self.double(dictionary["X"]),
let y = self.double(dictionary["Y"]),
let width = self.double(dictionary["Width"]),
let height = self.double(dictionary["Height"])
else { return nil }
return CGRect(x: x, y: y, width: width, height: height)
}
private static func double(_ value: Any?) -> Double? {
switch value {
case let number as NSNumber:
number.doubleValue
case let double as Double:
double
case let int as Int:
Double(int)
case let cgFloat as CGFloat:
Double(cgFloat)
default:
nil
}
}
}
@@ -0,0 +1,464 @@
import AppKit
import Foundation
struct StatusItemVisibilitySnapshot: Equatable {
let isVisible: Bool
let hasButton: Bool
let hasWindow: Bool
let hasScreen: Bool
let isOnCurrentScreen: Bool
let buttonWidth: CGFloat
init(
isVisible: Bool,
hasButton: Bool,
hasWindow: Bool,
hasScreen: Bool,
isOnCurrentScreen: Bool = true,
buttonWidth: CGFloat)
{
self.isVisible = isVisible
self.hasButton = hasButton
self.hasWindow = hasWindow
self.hasScreen = hasScreen
self.isOnCurrentScreen = isOnCurrentScreen
self.buttonWidth = buttonWidth
}
}
extension StatusItemVisibilitySnapshot: CustomStringConvertible {
var description: String {
"visible=\(self.isVisible),button=\(self.hasButton),window=\(self.hasWindow),"
+ "screen=\(self.hasScreen),currentScreen=\(self.isOnCurrentScreen),"
+ "width=\(String(format: "%.1f", Double(self.buttonWidth)))"
}
}
struct StatusItemStartupVisibilityEvidence: Equatable, CustomStringConvertible {
let autosaveName: String
let expectsVisibility: Bool
let visibilityDefault: Bool?
let snapshot: StatusItemVisibilitySnapshot
var description: String {
"name=\(self.autosaveName),expected=\(self.expectsVisibility),"
+ "default=\(self.visibilityDefault.map(String.init) ?? "unset"),\(self.snapshot)"
}
}
@MainActor
func isStatusItemBlocked(_ item: NSStatusItem) -> Bool {
MenuBarVisibilityWatcher.isBlockedSnapshot(snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
}
enum MenuBarVisibilityWatcher {
static let guidanceShownKey = "hasShownTahoeAllowListGuidance"
static let guidanceLastShownAtKey = "tahoeAllowListGuidanceLastShownAt"
static let guidanceRepeatInterval: TimeInterval = 24 * 60 * 60
static let startupFreshnessInterval: TimeInterval = 10
static let startupCheckDelay: TimeInterval = 2
static let screenChangeCheckDelay: Duration = .milliseconds(750)
static let screenChangeFollowUpDelay: Duration = .seconds(2)
static let settingsURL = URL(string: "x-apple.systempreferences:com.apple.MenuBarSettings")!
@MainActor
static func visibilitySnapshot(_ item: NSStatusItem) -> StatusItemVisibilitySnapshot {
let screen = item.button?.window?.screen
return StatusItemVisibilitySnapshot(
isVisible: item.isVisible,
hasButton: item.button != nil,
hasWindow: item.button?.window != nil,
hasScreen: screen != nil,
isOnCurrentScreen: screen.map(self.isCurrentScreen) ?? false,
buttonWidth: item.button?.frame.size.width ?? 0)
}
@MainActor
private static func isCurrentScreen(_ screen: NSScreen) -> Bool {
let screenNumber = self.screenNumber(screen)
return NSScreen.screens.contains { candidate in
if let screenNumber, let candidateNumber = self.screenNumber(candidate) {
return candidateNumber == screenNumber
}
return candidate === screen
}
}
private static func screenNumber(_ screen: NSScreen) -> NSNumber? {
screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber
}
static func isBlockedSnapshot(snapshot: StatusItemVisibilitySnapshot) -> Bool {
guard snapshot.isVisible else { return false }
guard snapshot.hasButton else { return true }
// Menu bar managers can park status-item windows off the current screen while preserving the
// underlying NSStatusItem. Recreating in that state makes those managers see a new item.
return !snapshot.hasWindow || snapshot.buttonWidth <= 0
}
static func isDisplacedSnapshot(snapshot: StatusItemVisibilitySnapshot) -> Bool {
guard snapshot.isVisible, snapshot.hasButton, snapshot.hasWindow, snapshot.buttonWidth > 0 else {
return false
}
return !snapshot.hasScreen || !snapshot.isOnCurrentScreen
}
static func hasBlockedVisibleSnapshots(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
let visibleItems = snapshots.filter(\.isVisible)
guard !visibleItems.isEmpty else { return false }
return visibleItems.allSatisfy { snapshot in
self.isBlockedSnapshot(snapshot: snapshot)
}
}
static func hasAnyBlockedVisibleSnapshot(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
snapshots.contains { snapshot in
snapshot.isVisible && self.isBlockedSnapshot(snapshot: snapshot)
}
}
static func hasAnyDisplacedVisibleSnapshot(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
snapshots.contains { snapshot in
self.isDisplacedSnapshot(snapshot: snapshot)
}
}
static func hasAnyStartupRecoveryCandidate(
snapshots: [StatusItemVisibilitySnapshot],
evidence: [StatusItemStartupVisibilityEvidence] = [],
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
detectTahoeBlockedStatusItem: Bool = false)
-> Bool
{
if self.hasAnyBlockedVisibleSnapshot(snapshots) {
return true
}
if detectTahoeBlockedStatusItem,
self.hasAnyTahoeHiddenNoProxyCandidate(evidence: evidence, windowSnapshots: windowSnapshots)
{
return true
}
guard detectTahoeBlockedStatusItem,
self.hasAnyDisplacedVisibleSnapshot(snapshots),
windowSnapshots.contains(where: \.isTahoeBlockedProxy)
else {
return false
}
return true
}
static func hasAnyTahoeHiddenNoProxyCandidate(
evidence: [StatusItemStartupVisibilityEvidence],
windowSnapshots: [MenuBarStatusItemWindowSnapshot])
-> Bool
{
evidence.contains { item in
// Tahoe can destroy the Control Center scene while leaving its enabled default behind.
// Requiring both app intent and that default avoids treating ordinary hidden items as blocked.
item.expectsVisibility
&& item.visibilityDefault == true
&& !item.snapshot.isVisible
&& !item.snapshot.hasWindow
&& !windowSnapshots.contains {
$0.name == item.autosaveName && $0.isOnscreen && $0.isWithinDisplayBounds
}
}
}
@MainActor
static func visibilitySnapshots(_ items: [NSStatusItem]) -> [StatusItemVisibilitySnapshot] {
items.map { item in
self.visibilitySnapshot(item)
}
}
@MainActor
static func hasBlockedVisibleStatusItems(_ items: [NSStatusItem]) -> Bool {
self.hasBlockedVisibleSnapshots(self.visibilitySnapshots(items))
}
static func shouldAttemptStartupRecovery(
appLaunchedAt: Date,
now: Date = Date(),
snapshots: [StatusItemVisibilitySnapshot],
evidence: [StatusItemStartupVisibilityEvidence] = [],
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
detectTahoeBlockedStatusItem: Bool = false)
-> Bool
{
guard now.timeIntervalSince(appLaunchedAt) <= self.startupFreshnessInterval else { return false }
return self.hasAnyStartupRecoveryCandidate(
snapshots: snapshots,
evidence: evidence,
windowSnapshots: windowSnapshots,
detectTahoeBlockedStatusItem: detectTahoeBlockedStatusItem)
}
static func shouldRefreshScreenChangePlacement(
previousScreenCount _: Int,
currentScreenCount _: Int,
snapshots: [StatusItemVisibilitySnapshot])
-> Bool
{
self.hasAnyDisplacedVisibleSnapshot(snapshots)
}
static func shouldAttemptScreenChangeRecovery(snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
self.hasAnyBlockedVisibleSnapshot(snapshots)
}
static func shouldShowGuidance(defaults: UserDefaults, now: Date = Date()) -> Bool {
guard defaults.bool(forKey: self.guidanceShownKey) else { return true }
let lastShownAt = defaults.double(forKey: self.guidanceLastShownAtKey)
guard lastShownAt > 0 else { return false }
return now.timeIntervalSince1970 - lastShownAt >= self.guidanceRepeatInterval
}
static func markGuidanceShown(defaults: UserDefaults, now: Date = Date()) {
defaults.set(true, forKey: self.guidanceShownKey)
defaults.set(now.timeIntervalSince1970, forKey: self.guidanceLastShownAtKey)
}
@MainActor
static func presentGuidance(
defaults: UserDefaults,
now: Date = Date(),
openURL: (URL) -> Void = { NSWorkspace.shared.open($0) })
{
self.markGuidanceShown(defaults: defaults, now: now)
let alert = NSAlert()
alert.messageText = L("CodexBar can't show its menu bar icon")
alert.informativeText = L(
"macOS Tahoe can block menu bar apps in System Settings → Menu Bar → Allow in the Menu Bar. "
+ "CodexBar is running, but macOS may be hiding its icon. Open Menu Bar settings and turn CodexBar on.")
alert.alertStyle = .warning
alert.addButton(withTitle: L("Open Menu Bar Settings"))
alert.addButton(withTitle: L("Dismiss"))
if alert.runModal() == .alertFirstButtonReturn {
openURL(self.settingsURL)
}
}
}
extension StatusItemController {
func scheduleStartupStatusItemVisibilityCheck(appLaunchedAt: Date = Date()) {
guard !SettingsStore.isRunningTests else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + MenuBarVisibilityWatcher.startupCheckDelay) { [weak self] in
Task { @MainActor [weak self] in
self?.checkStartupStatusItemVisibility(appLaunchedAt: appLaunchedAt)
}
}
}
private func checkStartupStatusItemVisibility(appLaunchedAt: Date, now: Date = Date()) {
let evidence = self.startupStatusItemVisibilityEvidence()
let snapshots = evidence.map(\.snapshot)
let windowSnapshots = self.statusItemWindowSnapshots()
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: appLaunchedAt,
now: now,
snapshots: snapshots,
evidence: evidence,
windowSnapshots: windowSnapshots,
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
else {
return
}
self.menuLogger.error(
"Status item failed to materialize or remained detached; recreating status items",
metadata: [
"snapshots": snapshots.map(\.description).joined(separator: " | "),
"evidence": evidence.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(windowSnapshots),
])
self.recreateStatusItemsForVisibilityRecovery()
let recoveredEvidence = self.startupStatusItemVisibilityEvidence()
let recoveredSnapshots = recoveredEvidence.map(\.snapshot)
let recoveredWindowSnapshots = self.statusItemWindowSnapshots()
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: appLaunchedAt,
now: now,
snapshots: recoveredSnapshots,
evidence: recoveredEvidence,
windowSnapshots: recoveredWindowSnapshots,
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
else {
self.menuLogger.info(
"Status item materialized after recreation",
metadata: ["snapshots": recoveredSnapshots.map(\.description).joined(separator: " | ")])
return
}
self.menuLogger.error(
"Status item still unavailable after recreation",
metadata: [
"snapshots": recoveredSnapshots.map(\.description).joined(separator: " | "),
"evidence": recoveredEvidence.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(recoveredWindowSnapshots),
])
guard #available(macOS 26.0, *),
MenuBarVisibilityWatcher.shouldShowGuidance(defaults: self.settings.userDefaults, now: now)
else {
return
}
MenuBarVisibilityWatcher.presentGuidance(defaults: self.settings.userDefaults, now: now)
}
@objc func handleScreenParametersDidChange(_: Notification) {
let previousScreenCount = max(
self.pendingScreenChangePreviousCount ?? self.lastKnownScreenCount,
self.lastKnownScreenCount)
let currentScreenCount = NSScreen.screens.count
self.pendingScreenChangePreviousCount = previousScreenCount
self.lastKnownScreenCount = currentScreenCount
self.scheduleScreenChangeStatusItemVisibilityCheck(
previousScreenCount: previousScreenCount,
currentScreenCount: currentScreenCount)
}
private func scheduleScreenChangeStatusItemVisibilityCheck(
previousScreenCount: Int,
currentScreenCount: Int)
{
guard !SettingsStore.isRunningTests else { return }
self.screenChangeVisibilityTask?.cancel()
self.screenChangeVisibilityTask = Task { @MainActor [weak self] in
do {
try await Task.sleep(for: MenuBarVisibilityWatcher.screenChangeCheckDelay)
} catch {
return
}
self?.checkScreenChangeStatusItemVisibility(
previousScreenCount: previousScreenCount,
currentScreenCount: currentScreenCount)
}
}
private func checkScreenChangeStatusItemVisibility(previousScreenCount: Int, currentScreenCount: Int) {
self.pendingScreenChangePreviousCount = nil
let settledCurrentScreenCount = NSScreen.screens.count
self.lastKnownScreenCount = settledCurrentScreenCount
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
if MenuBarVisibilityWatcher.shouldAttemptScreenChangeRecovery(snapshots: snapshots) {
self.menuLogger.error(
"Display configuration changed; recreating status items",
metadata: [
"previousScreenCount": "\(previousScreenCount)",
"currentScreenCount": "\(settledCurrentScreenCount)",
"capturedScreenCount": "\(currentScreenCount)",
"snapshots": snapshots.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(),
])
self.recreateStatusItemsForVisibilityRecovery()
self.schedulePostScreenChangeRecoveryVerification(attempt: 1)
return
}
guard MenuBarVisibilityWatcher.shouldRefreshScreenChangePlacement(
previousScreenCount: previousScreenCount,
currentScreenCount: settledCurrentScreenCount,
snapshots: snapshots)
else {
return
}
self.menuLogger.info(
"Display configuration changed; refreshing existing status items",
metadata: [
"previousScreenCount": "\(previousScreenCount)",
"currentScreenCount": "\(settledCurrentScreenCount)",
"capturedScreenCount": "\(currentScreenCount)",
"snapshots": snapshots.map(\.description).joined(separator: " | "),
])
self.refreshExistingStatusItemsForVisibilityRecovery()
}
private func schedulePostScreenChangeRecoveryVerification(attempt: Int) {
self.screenChangeVisibilityTask = Task { @MainActor [weak self] in
do {
try await Task.sleep(for: MenuBarVisibilityWatcher.screenChangeFollowUpDelay)
} catch {
return
}
self?.verifyScreenChangeRecoveryIfNeeded(attempt: attempt)
}
}
private func verifyScreenChangeRecoveryIfNeeded(attempt: Int) {
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
guard MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(snapshots) else {
self.menuLogger.info(
"Status item recovered after display-change recovery",
metadata: ["attempt": "\(attempt)", "snapshots": snapshots.map(\.description).joined(separator: " | ")])
return
}
self.menuLogger.error(
"Status item still blocked after display-change recovery; recreating status items again",
metadata: [
"attempt": "\(attempt)",
"snapshots": snapshots.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(),
])
self.recreateStatusItemsForVisibilityRecovery()
// No further async retries: a menu bar manager may park the newly recreated item in a state
// that still looks blocked, causing repeated NSStatusItem destruction that corrupts Control Center.
// Instead, do one synchronous re-check to surface guidance if macOS itself is blocking the item.
let finalSnapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
guard MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(finalSnapshots) else { return }
self.menuLogger.error(
"Status item still blocked after display-change recovery recreation",
metadata: [
"snapshots": finalSnapshots.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(),
])
guard #available(macOS 26.0, *),
MenuBarVisibilityWatcher.shouldShowGuidance(defaults: self.settings.userDefaults)
else { return }
MenuBarVisibilityWatcher.presentGuidance(defaults: self.settings.userDefaults)
}
private var startupVisibilityStatusItems: [NSStatusItem] {
[self.statusItem] + Array(self.statusItems.values)
}
private func startupStatusItemVisibilityEvidence() -> [StatusItemStartupVisibilityEvidence] {
self.startupVisibilityStatusItems.map { item in
let autosaveName = item.autosaveName ?? ""
return StatusItemStartupVisibilityEvidence(
autosaveName: autosaveName,
expectsVisibility: self.expectedVisibleStatusItemAutosaveNames.contains(autosaveName),
visibilityDefault: MenuBarStatusItemDefaultsRepair.visibilityDefault(
defaults: self.settings.userDefaults,
autosaveName: autosaveName),
snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
}
}
private var canDetectTahoeBlockedStatusItem: Bool {
if #available(macOS 26.0, *) {
return true
}
return false
}
private func statusItemWindowSnapshots() -> [MenuBarStatusItemWindowSnapshot] {
let names = Set(self.startupVisibilityStatusItems.compactMap { item in
item.autosaveName.isEmpty ? nil : item.autosaveName
})
return MenuBarStatusItemWindowProbe.snapshots(matching: names)
}
private func statusItemWindowDiagnosticsDescription(
_ snapshots: [MenuBarStatusItemWindowSnapshot]? = nil)
-> String
{
let snapshots = snapshots ?? self.statusItemWindowSnapshots()
guard !snapshots.isEmpty else { return "none" }
return snapshots.map(\.description).joined(separator: " | ")
}
}
@@ -0,0 +1,310 @@
import AppKit
import SwiftUI
/// Hosts a menu-card SwiftUI row whose selection highlight is rendered entirely by AppKit/Core
/// Animation instead of SwiftUI, so moving the highlight while scrolling costs no SwiftUI body
/// re-evaluation or content re-rasterization.
///
/// The reported Overview scroll stutter comes from driving the native selection look through SwiftUI:
/// each scroll step flips `menuItemHighlighted`, which re-renders the entire rich row subtree
/// (header, usage bars, storage line). A headless benchmark measured ~310 ms per toggle with
/// spikes past one 120 Hz frame, matching the dropped frames in the bug report.
///
/// This view keeps the SwiftUI content pinned to its normal (unselected) appearance and recreates
/// the selected look in two GPU-composited steps that never touch the SwiftUI graph:
/// 1. an `NSVisualEffectView` with the native `.selection` material drawn behind the content, and
/// 2. a `CIColorMatrix` content filter that maps the row's pixels to the selected text color
/// this matches the existing design, where every element already becomes
/// `selectedMenuItemTextColor` when highlighted.
/// Toggling selection then costs a layer property change (~0.05 ms) rather than a SwiftUI pass.
@MainActor
final class GPUSelectionHostingView<Content: View>: NSView, MenuCardHighlighting, MenuCardMeasuring {
private let hosting: NSHostingView<MenuCardSectionContainerView<Content>>
private let selectionView = NSVisualEffectView()
private var tintFilter: CIFilter?
private var isRowHighlighted = false
private var onClick: (() -> Void)?
private let containsInteractiveControls: Bool
private let interactiveRegionStore: MenuCardInteractiveRegionStore?
private(set) var allowsMenuHighlight: Bool
/// Selection inset/radius mirror the SwiftUI `MenuCardSectionContainerView` highlight
/// (`.padding(.horizontal, 6).padding(.vertical, 2)` with a 6 pt corner radius) so the AppKit
/// background lands in the same place the SwiftUI one used to.
private static var selectionHorizontalInset: CGFloat {
6
}
private static var selectionVerticalInset: CGFloat {
2
}
private static var selectionCornerRadius: CGFloat {
6
}
/// Short enough that a fast flick still looks crisp, long enough to read as a glide rather than
/// a hard cut. Tunable from real-device recordings.
private static var selectionFadeDuration: CFTimeInterval {
0.06
}
init(
rootView: MenuCardSectionContainerView<Content>,
allowsMenuHighlight: Bool,
containsInteractiveControls: Bool = false,
interactiveRegionStore: MenuCardInteractiveRegionStore? = nil,
onClick: (() -> Void)?)
{
self.hosting = NSHostingView(rootView: rootView)
self.allowsMenuHighlight = allowsMenuHighlight
self.containsInteractiveControls = containsInteractiveControls
self.interactiveRegionStore = interactiveRegionStore
self.onClick = onClick
self.tintFilter = nil
super.init(frame: .zero)
self.wantsLayer = true
self.refreshTintFilter()
self.setupSelectionView()
self.setupHosting()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var allowsVibrancy: Bool {
true
}
override var intrinsicContentSize: NSSize {
NSSize(width: self.frame.width, height: self.hosting.intrinsicContentSize.height)
}
override func acceptsFirstMouse(for _: NSEvent?) -> Bool {
true
}
override func viewDidChangeEffectiveAppearance() {
super.viewDidChangeEffectiveAppearance()
self.refreshTintFilter()
}
/// Forward accessibility activation to the click handler, mirroring `MenuCardItemHostingView`.
override func accessibilityRole() -> NSAccessibility.Role? {
self.onClick == nil ? super.accessibilityRole() : .button
}
override func accessibilityPerformPress() -> Bool {
guard let onClick = self.onClick else {
return super.accessibilityPerformPress()
}
onClick()
return true
}
override func hitTest(_ point: NSPoint) -> NSView? {
let descendant = super.hitTest(point)
if let descendant {
var current: NSView? = descendant
while let view = current, view !== self {
if view is NSButton || view is NSControl {
return descendant
}
current = view.superview
}
if self.hitsHostedInteractiveControl(at: point) {
return descendant
}
if descendant !== self, self.onClick != nil {
return self
}
}
return descendant
}
private func hitsHostedInteractiveControl(at point: NSPoint) -> Bool {
guard self.containsInteractiveControls else { return false }
let hostedPoint = self.hosting.convert(point, from: self)
return self.interactiveRegionStore?.contains(
hostedPoint,
hostingBounds: self.hosting.bounds,
fittedSize: self.hosting.fittingSize) == true
}
private func locationInView(for event: NSEvent) -> NSPoint {
guard self.window != nil else {
return event.locationInWindow
}
return self.convert(event.locationInWindow, from: nil)
}
override func mouseDown(with event: NSEvent) {
guard event.type == .leftMouseDown, self.onClick != nil else {
super.mouseDown(with: event)
return
}
guard self.bounds.contains(self.locationInView(for: event)), let window = self.window else { return }
// A submenu-backed NSMenuItem consumes mouseUp in its nested tracking loop before a custom
// view receives it. Track the drag/up sequence directly so release-inside cancellation stays
// native while the menu never gets a chance to close before the row action runs.
var shouldInvoke = false
window.trackEvents(
matching: [.leftMouseDragged, .leftMouseUp],
timeout: NSEvent.foreverDuration,
mode: .eventTracking)
{ [weak self] trackedEvent, stop in
guard let self, let trackedEvent else {
stop.pointee = true
return
}
if self.primaryPressShouldYieldToMenu(for: trackedEvent) {
// We dequeued this drag from the window; put it back so NSMenu's tracking loop can
// continue native drag-to-submenu selection from the same event.
window.postEvent(trackedEvent, atStart: true)
stop.pointee = true
return
}
guard let decision = self.primaryPressDecision(for: trackedEvent) else { return }
shouldInvoke = decision
stop.pointee = true
}
if shouldInvoke {
self.onClick?()
}
}
private func primaryPressDecision(for event: NSEvent) -> Bool? {
guard event.type == .leftMouseUp else { return nil }
return self.bounds.contains(self.locationInView(for: event))
}
private func primaryPressShouldYieldToMenu(for event: NSEvent) -> Bool {
event.type == .leftMouseDragged && !self.bounds.contains(self.locationInView(for: event))
}
override func layout() {
super.layout()
self.selectionView.frame = self.bounds.insetBy(
dx: Self.selectionHorizontalInset,
dy: Self.selectionVerticalInset)
self.selectionView.layer?.cornerRadius = Self.selectionCornerRadius
self.hosting.frame = self.bounds
}
func setHighlighted(_ highlighted: Bool) {
guard self.isRowHighlighted != highlighted else { return }
self.isRowHighlighted = highlighted
// Tint the content to the selected text color via a GPU color matrix; clearing the
// filter returns it to its normal palette. No SwiftUI invalidation happens here.
if let tintFilter {
self.hosting.layer?.filters = highlighted ? [tintFilter] : []
}
// Crossfade the selection background instead of hard-cutting it. As the wheel moves the
// highlight, the leaving row fades out while the arriving row fades in, which reads as the
// selection gliding between rows rather than teleporting. The fade is short so fast flicks
// still resolve crisply. Runs entirely on the GPU via Core Animation.
let layer = self.selectionView.layer
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = layer?.presentation()?.opacity ?? (highlighted ? 0 : 1)
fade.toValue = highlighted ? 1 : 0
fade.duration = Self.selectionFadeDuration
fade.timingFunction = CAMediaTimingFunction(name: .easeOut)
layer?.add(fade, forKey: "selectionFade")
layer?.opacity = highlighted ? 1 : 0
}
func measuredHeight(width: CGFloat) -> CGFloat {
self.hosting.frame = NSRect(origin: self.hosting.frame.origin, size: NSSize(width: width, height: 1))
self.hosting.layoutSubtreeIfNeeded()
return self.hosting.fittingSize.height
}
#if DEBUG
/// True once the menu marks this row highlighted via `setHighlighted`.
var isHighlightedForTesting: Bool {
self.isRowHighlighted
}
/// The hosted SwiftUI highlight state, which must stay `false` for GPU-selected rows proving
/// selection never re-invalidates the SwiftUI graph while scrolling.
var swiftUIHighlightStateIsHighlightedForTesting: Bool {
self.hosting.rootView.highlightState.isHighlighted
}
#endif
private func setupSelectionView() {
self.selectionView.material = .selection
self.selectionView.blendingMode = .withinWindow
self.selectionView.state = .active
self.selectionView.isEmphasized = true
self.selectionView.wantsLayer = true
self.selectionView.layer?.masksToBounds = true
// Visibility is driven by layer opacity (crossfaded in `setHighlighted`) rather than
// `isHidden`, so the selection can glide in and out instead of hard-cutting.
self.selectionView.layer?.opacity = 0
self.selectionView.autoresizingMask = [.width, .height]
self.addSubview(self.selectionView)
}
private func setupHosting() {
self.hosting.wantsLayer = true
self.hosting.autoresizingMask = [.width, .height]
self.addSubview(self.hosting)
}
/// Maps every pixel's RGB to the system selected-menu-item text color while preserving alpha,
/// reproducing the appearance the SwiftUI rows already adopt when highlighted. The bias is read
/// from `NSColor.selectedMenuItemTextColor` rather than hard-coded to white so graphite/
/// high-contrast/accessibility appearances tint correctly. Core Image runs this on the GPU
/// (Metal), so it composites for free per frame.
private func refreshTintFilter() {
self.tintFilter = Self.makeSelectedTextTintFilter(appearance: self.effectiveAppearance)
if self.isRowHighlighted {
self.hosting.layer?.filters = self.tintFilter.map { [$0] } ?? []
}
}
private static func makeSelectedTextTintFilter(appearance: NSAppearance) -> CIFilter? {
guard let filter = CIFilter(name: "CIColorMatrix") else { return nil }
var tint: NSColor = .white
appearance.performAsCurrentDrawingAppearance {
tint = NSColor.selectedMenuItemTextColor.usingColorSpace(.deviceRGB) ?? .white
}
filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputRVector")
filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputGVector")
filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBVector")
filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")
filter.setValue(
CIVector(x: tint.redComponent, y: tint.greenComponent, z: tint.blueComponent, w: 0),
forKey: "inputBiasVector")
return filter
}
}
#if DEBUG
extension GPUSelectionHostingView {
func _test_hitsHostedInteractiveControl(at point: NSPoint) -> Bool {
self.hitsHostedInteractiveControl(at: point)
}
func _test_simulateRuntimeClick(at point: NSPoint? = nil) -> Bool {
let clickPoint = point ?? NSPoint(x: self.bounds.midX, y: self.bounds.midY)
guard let onClick = self.onClick, self.hitTest(clickPoint) === self else { return false }
guard self.bounds.contains(clickPoint) else { return false }
onClick()
return true
}
func _test_primaryPressDecision(for event: NSEvent) -> Bool? {
self.primaryPressDecision(for: event)
}
func _test_primaryPressShouldYieldToMenu(for event: NSEvent) -> Bool {
self.primaryPressShouldYieldToMenu(for: event)
}
}
#endif
@@ -0,0 +1,177 @@
import Foundation
extension UsageMenuCardView.Model {
func heightFingerprint(section: String, additional: [String] = []) -> String {
let notesFingerprint = MenuCardHeightFingerprint.join(self.usageNotes.map {
MenuCardHeightFingerprint.field("note", $0)
})
return MenuCardHeightFingerprint.join([
"section=\(section)",
"provider=\(self.provider.rawValue)",
"localization=\(codexBarLocalizationSignature())",
MenuCardHeightFingerprint.field("name", self.providerName),
MenuCardHeightFingerprint.field("email", self.email),
MenuCardHeightFingerprint.field("subtitle", self.subtitleText),
"subtitleStyle=\(self.subtitleStyle.heightFingerprint)",
MenuCardHeightFingerprint.field("plan", self.planText),
MenuCardHeightFingerprint.field("placeholder", self.placeholder),
MenuCardHeightFingerprint.field("credits", self.creditsText),
"creditsRemaining=\(self.creditsRemaining.map(String.init(describing:)) ?? "nil")",
MenuCardHeightFingerprint.field("creditsHint", self.creditsHintText),
MenuCardHeightFingerprint.field("creditsCopy", self.creditsHintCopyText),
"codexResetCredits=\(self.codexResetCredits?.heightFingerprint ?? "")",
"metrics=\(MenuCardHeightFingerprint.join(self.metrics.map(\.heightFingerprint)))",
"notes=\(notesFingerprint)",
"dashboard=\(self.inlineUsageDashboard?.heightFingerprint ?? "")",
"providerCost=\(self.providerCost?.heightFingerprint ?? "")",
"tokenUsage=\(self.tokenUsage?.heightFingerprint ?? "")",
"openaiAPI=\(self.openAIAPIUsage == nil ? "0" : "1")",
] + additional)
}
static func heightFingerprintField(_ name: String, _ value: String?) -> String {
MenuCardHeightFingerprint.field(name, value)
}
}
private enum MenuCardHeightFingerprint {
private static let hashSalt = UUID()
static func join(_ values: [String]) -> String {
values.map { "\($0.count):\($0)" }.joined(separator: "|")
}
static func field(_ name: String, _ value: String?) -> String {
guard let value else {
return "\(name)=nil"
}
return "\(name)=\(Self.stringShape(value))"
}
private static func stringShape(_ value: String) -> String {
var hasher = Hasher()
hasher.combine(Self.hashSalt)
hasher.combine(value)
let digest = String(UInt(bitPattern: hasher.finalize()), radix: 16)
return "chars:\(value.count),utf8:\(value.utf8.count),lines:\(Self.lineCount(value)),hash:\(digest)"
}
private static func lineCount(_ value: String) -> Int {
guard !value.isEmpty else { return 0 }
return value.utf8.reduce(1) { count, byte in
byte == 10 ? count + 1 : count
}
}
}
extension UsageMenuCardView.Model.SubtitleStyle {
fileprivate var heightFingerprint: String {
switch self {
case .info: "info"
case .loading: "loading"
case .error: "error"
}
}
}
extension UsageMenuCardView.Model.Metric {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
self.id,
MenuCardHeightFingerprint.field("title", self.title),
"percent=\(Int(self.percent.rounded()))",
"percentStyle=\(self.percentStyle.rawValue)",
MenuCardHeightFingerprint.field("status", self.statusText),
MenuCardHeightFingerprint.field("reset", self.resetText),
MenuCardHeightFingerprint.field("detail", self.detailText),
MenuCardHeightFingerprint.field("detailLeft", self.detailLeftText),
MenuCardHeightFingerprint.field("detailRight", self.detailRightText),
self.pacePercent == nil ? "pace=0" : "pace=1",
self.paceOnTop ? "paceTop=1" : "paceTop=0",
self.cardStyle ? "card=1" : "card=0",
"warningMarkers=\(self.warningMarkerPercents.count)",
"workdayMarkers=\(self.workdayMarkerPercents.count)",
])
}
}
extension UsageMenuCardView.Model.ProviderCostSection {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("title", self.title),
MenuCardHeightFingerprint.field("spend", self.spendLine),
MenuCardHeightFingerprint.field("percentLine", self.percentLine),
MenuCardHeightFingerprint.field("personalSpend", self.personalSpendLine),
self.percentUsed == nil ? "percent=0" : "percent=1",
])
}
}
extension UsageMenuCardView.Model.TokenUsageSection {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("session", self.sessionLine),
MenuCardHeightFingerprint.field("month", self.monthLine),
MenuCardHeightFingerprint.field("comparisons", self.comparisonLines.joined(separator: "|")),
MenuCardHeightFingerprint.field("hint", self.hintLine),
MenuCardHeightFingerprint.field("error", self.errorLine),
MenuCardHeightFingerprint.field("errorCopy", self.errorCopyText),
])
}
}
extension CodexResetCreditsPresentation {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("text", self.text),
MenuCardHeightFingerprint.field("expirySummary", self.expirySummaryText),
])
}
}
extension InlineUsageDashboardModel {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("accessibility", self.accessibilityLabel),
self.valueStyle.heightFingerprint,
MenuCardHeightFingerprint.join(self.kpis.map(\.heightFingerprint)),
MenuCardHeightFingerprint.join(self.points.map(\.heightFingerprint)),
MenuCardHeightFingerprint.join(self.detailLines.map { MenuCardHeightFingerprint.field("detail", $0) }),
])
}
}
extension InlineUsageDashboardModel.KPI {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("title", self.title),
MenuCardHeightFingerprint.field("value", self.value),
self.emphasis ? "1" : "0",
])
}
}
extension InlineUsageDashboardModel.Point {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
self.id,
MenuCardHeightFingerprint.field("label", self.label),
MenuCardHeightFingerprint.field("accessibilityValue", self.accessibilityValue),
])
}
}
extension InlineUsageDashboardModel.ValueStyle {
fileprivate var heightFingerprint: String {
switch self {
case .currencyUSD:
"currencyUSD"
case let .currency(symbol):
"currency:\(symbol)"
case .tokens:
"tokens"
case .points:
"points"
}
}
}
@@ -0,0 +1,30 @@
import CodexBarCore
extension CodexConsumerProjection.RateLane {
var quotaWarningWindow: QuotaWarningWindow {
switch self {
case .session:
.session
case .weekly:
.weekly
}
}
}
extension UsageMenuCardView.Model {
static func warningMarkerPercents(thresholds: [Int]?, showUsed: Bool) -> [Double] {
guard let thresholds, !thresholds.isEmpty else { return [] }
return QuotaWarningThresholds.active(thresholds)
.map { showUsed ? 100 - Double($0) : Double($0) }
.filter { $0 > 0 && $0 < 100 }
}
}
/// Returns boundary percentages for work day markers on a weekly progress bar.
/// Only valid when windowMinutes == 10080 (standard 7-day week).
/// nil workDays means feature is disabled.
func workDayMarkerPercents(workDays: Int?, windowMinutes: Int?) -> [Double] {
guard workDays != nil, windowMinutes == 10080 else { return [] }
guard let wd = workDays, wd >= 2, wd <= 7 else { return [] }
return (1..<wd).map { Double($0) * 100.0 / Double(wd) }
}
@@ -0,0 +1,111 @@
import CodexBarCore
import Observation
struct MenuCardLiveSubtitle {
let text: String
let style: UsageMenuCardView.Model.SubtitleStyle
}
/// Updates values in an already-hosted card without rebuilding its tracked NSMenu.
@MainActor
@Observable
final class MenuCardRefreshMonitor {
typealias ModelResolver = @MainActor (UsageProvider) -> UsageMenuCardView.Model?
typealias ProviderRefreshStateResolver = @MainActor (UsageProvider) -> Bool
private let resolveModel: ModelResolver
private let isProviderRefreshActive: ProviderRefreshStateResolver
/// Set while an all-providers refresh is running; individual cards freeze only while their
/// provider has active refresh work.
private var globalManualRefreshInFlight = false
/// Providers with an individual manual refresh in flight. Concurrent entries are allowed so
/// refreshing one provider does not stall or unfreeze another.
private var manualRefreshProviders: Set<UsageProvider> = []
private var frozenManualRefreshModels: [UsageProvider: UsageMenuCardView.Model] = [:]
/// True while any manual refresh (global or per-provider) is running.
var isManualRefreshInFlight: Bool {
self.globalManualRefreshInFlight || !self.manualRefreshProviders.isEmpty
}
init(
resolveModel: @escaping ModelResolver,
isProviderRefreshActive: @escaping ProviderRefreshStateResolver)
{
self.resolveModel = resolveModel
self.isProviderRefreshActive = isProviderRefreshActive
}
func beginManualRefresh(
frozenModels: [UsageProvider: UsageMenuCardView.Model],
provider: UsageProvider? = nil)
{
if let provider {
self.frozenManualRefreshModels[provider] = frozenModels[provider]
self.manualRefreshProviders.insert(provider)
} else {
self.frozenManualRefreshModels = frozenModels
self.globalManualRefreshInFlight = true
}
}
/// Balances a `beginManualRefresh` with the same `provider` argument (nil ends the global refresh).
func endManualRefresh(for provider: UsageProvider? = nil) {
if let provider {
self.manualRefreshProviders.remove(provider)
self.frozenManualRefreshModels[provider] = nil
} else {
self.globalManualRefreshInFlight = false
self.frozenManualRefreshModels.removeAll(keepingCapacity: true)
}
}
func resetManualRefresh() {
self.globalManualRefreshInFlight = false
self.manualRefreshProviders.removeAll(keepingCapacity: true)
self.frozenManualRefreshModels.removeAll(keepingCapacity: true)
}
func isManualRefreshInFlight(for provider: UsageProvider) -> Bool {
self.manualRefreshProviders.contains(provider) ||
(self.globalManualRefreshInFlight && self.isProviderRefreshActive(provider))
}
func model(
for provider: UsageProvider,
fallback: UsageMenuCardView.Model) -> UsageMenuCardView.Model
{
guard !self.isManualRefreshInFlight(for: provider) else {
guard let frozen = self.frozenManualRefreshModels[provider] else {
return fallback
}
if fallback.hasCompatibleTrackedLayout(with: frozen) {
return frozen
}
// A rebuilding menu may temporarily lose some metric rows, but retained rows and other sections
// must still match the frozen layout.
if fallback.hasCompatibleTrackedMetricSubset(of: frozen) {
return frozen
}
return fallback
}
guard let resolved = self.resolveModel(provider),
fallback.hasCompatibleTrackedLayout(with: resolved)
else {
return fallback
}
return resolved
}
func subtitle(
for provider: UsageProvider,
fallback: MenuCardLiveSubtitle) -> MenuCardLiveSubtitle
{
if self.isManualRefreshInFlight(for: provider) {
return MenuCardLiveSubtitle(text: "\(L("Refreshing"))", style: .loading)
}
guard let model = self.resolveModel(provider) else { return fallback }
return MenuCardLiveSubtitle(text: model.subtitleText, style: model.subtitleStyle)
}
}
@@ -0,0 +1,132 @@
import CodexBarCore
import SwiftUI
struct CodexResetCreditPresentationItem: Equatable {
let expiryText: String
let compactExpiryText: String
}
struct CodexResetCreditsPresentation: Equatable {
let text: String
let items: [CodexResetCreditPresentationItem]
var expirySummaryText: String {
let visibleItems = self.items.prefix(4).map(\.compactExpiryText)
let hiddenCount = self.items.count - visibleItems.count
let suffix = hiddenCount > 0 ? ["+\(hiddenCount)"] : []
return (visibleItems + suffix).joined(separator: " · ")
}
var helpText: String {
self.items.enumerated().map { index, item in
"\(index + 1). \(item.expiryText)"
}.joined(separator: "\n")
}
var accessibilityLabel: String {
[L("Limit Reset Credits"), self.text, self.helpText]
.filter { !$0.isEmpty }
.joined(separator: ", ")
}
static func make(
snapshot: CodexRateLimitResetCreditsSnapshot,
resetStyle: ResetTimeDisplayStyle,
now: Date) -> CodexResetCreditsPresentation?
{
let inventory = snapshot.availableInventory(at: now)
guard !inventory.credits.isEmpty else { return nil }
let items = inventory.credits.map { credit in
Self.presentationItem(for: credit, resetStyle: resetStyle, now: now)
}
return CodexResetCreditsPresentation(
text: Self.availableText(count: inventory.count),
items: items)
}
private static func availableText(count: Int) -> String {
count == 1 ? L("1 available") : String(format: L("%d available"), count)
}
private static func presentationItem(
for credit: CodexRateLimitResetCredit,
resetStyle: ResetTimeDisplayStyle,
now: Date) -> CodexResetCreditPresentationItem
{
guard let expiresAt = credit.expiresAt else {
return CodexResetCreditPresentationItem(expiryText: L("No expiry"), compactExpiryText: L("No expiry"))
}
let formattedTime = Self.formattedTime(expiresAt, resetStyle: resetStyle, now: now)
let compactExpiryText = resetStyle == .countdown && formattedTime.hasPrefix("in ")
? String(formattedTime.dropFirst(3))
: formattedTime
return CodexResetCreditPresentationItem(
expiryText: String(format: L("Expires %@"), formattedTime),
compactExpiryText: compactExpiryText)
}
private static func formattedTime(
_ expiresAt: Date,
resetStyle: ResetTimeDisplayStyle,
now: Date) -> String
{
switch resetStyle {
case .absolute:
return UsageFormatter.resetDescription(from: expiresAt, now: now)
case .countdown:
let countdown = UsageFormatter.resetCountdownDescription(from: expiresAt, now: now)
return countdown == "now" ? L("now") : countdown
}
}
}
struct CodexResetCreditsContent: View {
let presentation: CodexResetCreditsPresentation
@Environment(\.menuItemHighlighted) private var isHighlighted
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(L("Limit Reset Credits"))
.font(.body)
.fontWeight(.medium)
.lineLimit(1)
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.presentation.text)
.font(.footnote.weight(.semibold))
.foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))
.lineLimit(1)
.layoutPriority(1)
Spacer(minLength: 8)
HStack(alignment: .firstTextBaseline, spacing: 4) {
Image(systemName: "clock")
.font(.caption2)
Text(self.presentation.expirySummaryText)
.font(.caption)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.accessibilityHidden(true)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.help(self.presentation.helpText)
.accessibilityElement(children: .combine)
.accessibilityLabel(self.presentation.accessibilityLabel)
}
}
extension UsageMenuCardView.Model {
static func codexResetCredits(input: Input) -> CodexResetCreditsPresentation? {
guard input.provider == .codex,
let resetCredits = input.snapshot?.codexResetCredits
else {
return nil
}
return CodexResetCreditsPresentation.make(
snapshot: resetCredits,
resetStyle: input.resetTimeDisplayStyle,
now: input.now)
}
}
+393
View File
@@ -0,0 +1,393 @@
import CodexBarCore
import Foundation
extension UsageMenuCardView.Model.ProviderCostSection {
init(
title: String,
percentUsed: Double?,
spendLine: String,
percentLine: String?)
{
self.init(
title: title,
percentUsed: percentUsed,
spendLine: spendLine,
percentLine: percentLine,
personalSpendLine: nil)
}
}
extension UsageMenuCardView.Model {
static func sakanaPayAsYouGoSection(_ usage: SakanaPayAsYouGoSnapshot?) -> ProviderCostSection? {
guard let usage else { return nil }
return ProviderCostSection(
title: L("Extra usage"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(usage.balanceDetail)",
percentLine: usage.periodUsageTotal.map { "\(L("Usage")): \(UsageFormatter.usdString($0))" })
}
static func isRequiredOpenCodeZenBalance(_ snapshot: UsageSnapshot?) -> Bool {
snapshot?.primary == nil &&
snapshot?.secondary == nil &&
snapshot?.providerCost?.period == "Zen balance"
}
static func tokenUsageSnapshot(input: Input) -> CostUsageTokenSnapshot? {
if usesProviderCostHistoryAsPrimaryDashboard(input.provider), input.snapshot != nil {
return primaryCostHistorySnapshot(input: input)
}
return input.tokenSnapshot
}
static func creditsLine(
metadata: ProviderMetadata,
snapshot: UsageSnapshot?,
credits: CreditsSnapshot?,
error: String?) -> String?
{
guard metadata.supportsCredits else { return nil }
if metadata.id == .codex, credits == nil, error == nil { return nil }
if metadata.id == .amp,
let ampUsage = snapshot?.ampUsage,
let ampCredits = self.ampCreditsLine(ampUsage)
{
return ampCredits
}
if let credits {
if let creditLimit = credits.codexCreditLimit {
return UsageFormatter.creditsString(from: creditLimit.remaining)
}
return UsageFormatter.creditsString(from: credits.remaining)
}
if let error, !error.isEmpty {
return error.trimmingCharacters(in: .whitespacesAndNewlines)
}
return L(metadata.creditsHint)
}
static func creditsProgressPercent(credits: CreditsSnapshot?) -> Double? {
credits?.codexCreditLimit?.remainingPercent
}
static func creditsScaleText(credits: CreditsSnapshot?) -> String? {
guard let limit = credits?.codexCreditLimit else { return nil }
return L("of %@", UsageFormatter.creditsNumberString(from: limit.limit))
}
static func codexCreditLimitDetail(credits: CreditsSnapshot?, now: Date) -> String? {
guard let limit = credits?.codexCreditLimit else { return nil }
var parts = [
L("%@ used", UsageFormatter.creditsNumberString(from: limit.used)),
]
if let resetsAt = limit.resetsAt {
parts.append(L("resets %@", UsageFormatter.resetDescription(from: resetsAt, now: now)))
}
return parts.joined(separator: " · ")
}
private static func ampCreditsLine(_ usage: AmpUsageDetails) -> String? {
var lines: [String] = []
if let individualCredits = usage.individualCredits {
lines.append(
"\(L("Individual credits")): \(UsageFormatter.currencyString(individualCredits, currencyCode: "USD"))")
}
lines.append(contentsOf: usage.workspaceBalances.map { workspace in
"\(L("Workspace")) \(workspace.name): " +
UsageFormatter.currencyString(workspace.remaining, currencyCode: "USD")
})
return lines.isEmpty ? nil : lines.joined(separator: "\n")
}
static func tokenUsageSection(
provider: UsageProvider,
enabled: Bool,
comparisonPeriodsEnabled: Bool,
snapshot: CostUsageTokenSnapshot?,
error: String?) -> TokenUsageSection?
{
guard ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.supportsTokenCost else {
return nil
}
guard enabled else { return nil }
guard let snapshot else { return nil }
let sessionCost = snapshot.sessionCostUSD.map {
UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode)
} ?? ""
let sessionTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) }
let sessionLabel = if provider == .bedrock || provider == .mistral {
Self.latestBillingDayLabel(from: snapshot)
} else {
L("Today")
}
let sessionLine: String = {
if let sessionTokens {
return String(format: L("%@: %@ · %@ tokens"), sessionLabel, sessionCost, sessionTokens)
}
return "\(sessionLabel): \(sessionCost)"
}()
let monthCost = snapshot.last30DaysCostUSD.map {
UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode)
} ?? ""
let fallbackTokens = snapshot.daily.compactMap(\.totalTokens).reduce(0, +)
let monthTokensValue = snapshot.last30DaysTokens ?? (fallbackTokens > 0 ? fallbackTokens : nil)
let monthTokens = monthTokensValue.map { UsageFormatter.tokenCountString($0) }
let windowLabel = snapshot.historyLabel ?? Self.costHistoryWindowLabel(days: snapshot.historyDays)
let monthLine: String = {
if let monthTokens {
return String(format: L("%@: %@ · %@ tokens"), windowLabel, monthCost, monthTokens)
}
return "\(windowLabel): \(monthCost)"
}()
let err = (error?.isEmpty ?? true) ? nil : error
return TokenUsageSection(
sessionLine: sessionLine,
monthLine: monthLine,
comparisonLines: comparisonPeriodsEnabled
? snapshot.comparisonSummaries().map {
Self.costWindowLine(summary: $0, currencyCode: snapshot.currencyCode)
}
: [],
hintLine: Self.tokenUsageHint(provider: provider),
errorLine: err,
errorCopyText: (error?.isEmpty ?? true) ? nil : error)
}
static func costWindowLine(summary: CostUsageWindowSummary, currencyCode: String) -> String {
let label = Self.costHistoryWindowLabel(days: summary.days)
let cost = summary.totalCostUSD.map {
UsageFormatter.currencyString($0, currencyCode: currencyCode)
} ?? ""
guard let totalTokens = summary.totalTokens else { return "\(label): \(cost)" }
return String(
format: L("%@: %@ · %@ tokens"),
label,
cost,
UsageFormatter.tokenCountString(totalTokens))
}
static func tokenUsageHint(provider: UsageProvider) -> String? {
switch provider {
case .codex:
L("Estimated from local Codex logs for the selected account.")
case .claude:
UsageFormatter.costEstimateHint(provider: provider)
case .vertexai:
L("cost_estimate_hint")
case .bedrock:
L("AWS Cost Explorer billing can lag.")
case .openai:
L("Reported by OpenAI Admin API organization usage.")
case .mistral:
L("Reported by Mistral billing usage.")
default:
nil
}
}
static func costHistoryWindowLabel(days: Int) -> String {
days == 1 ? L("Today") : String(format: L("Last %d days"), days)
}
private static func latestBillingDayLabel(from snapshot: CostUsageTokenSnapshot) -> String {
guard let entry = bedrockLatestBillingDay(from: snapshot.daily),
let displayDate = bedrockDisplayDate(from: entry.date)
else { return L("Latest billing day") }
return String(format: L("Latest billing day (%@)"), displayDate)
}
private static func bedrockLatestBillingDay(from entries: [CostUsageDailyReport.Entry])
-> CostUsageDailyReport.Entry?
{
entries.compactMap { entry -> (entry: CostUsageDailyReport.Entry, dayKey: String)? in
guard let dayKey = bedrockBillingDayKey(from: entry.date) else { return nil }
return (entry, dayKey)
}
.max { lhs, rhs in
if lhs.dayKey != rhs.dayKey { return lhs.dayKey < rhs.dayKey }
let lCost = lhs.entry.costUSD ?? -1
let rCost = rhs.entry.costUSD ?? -1
if lCost != rCost { return lCost < rCost }
let lTokens = lhs.entry.totalTokens ?? -1
let rTokens = rhs.entry.totalTokens ?? -1
if lTokens != rTokens { return lTokens < rTokens }
return lhs.entry.date < rhs.entry.date
}?.entry
}
private static func bedrockDisplayDate(from text: String) -> String? {
guard let dayKey = bedrockBillingDayKey(from: text) else { return nil }
let monthStart = dayKey.index(dayKey.startIndex, offsetBy: 5)
let monthEnd = dayKey.index(monthStart, offsetBy: 2)
let dayStart = dayKey.index(dayKey.startIndex, offsetBy: 8)
guard
let month = Int(dayKey[monthStart..<monthEnd]),
let day = Int(dayKey[dayStart...]),
(1...Self.bedrockMonthAbbreviations.count).contains(month),
(1...31).contains(day)
else { return nil }
return "\(Self.bedrockMonthAbbreviations[month - 1]) \(day)"
}
private static let bedrockMonthAbbreviations = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]
private static func bedrockBillingDayKey(from text: String) -> String? {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count == 10 else { return nil }
for (offset, character) in trimmed.enumerated() {
switch offset {
case 4, 7:
guard character == "-" else { return nil }
default:
guard character.isNumber else { return nil }
}
}
let monthStart = trimmed.index(trimmed.startIndex, offsetBy: 5)
let monthEnd = trimmed.index(monthStart, offsetBy: 2)
let dayStart = trimmed.index(trimmed.startIndex, offsetBy: 8)
let yearEnd = trimmed.index(trimmed.startIndex, offsetBy: 4)
guard
let year = Int(trimmed[..<yearEnd]),
let month = Int(trimmed[monthStart..<monthEnd]),
let day = Int(trimmed[dayStart...]),
(1...Self.bedrockMonthAbbreviations.count).contains(month),
(1...Self.daysInBedrockBillingMonth(month, year: year)).contains(day)
else { return nil }
return trimmed
}
private static func daysInBedrockBillingMonth(_ month: Int, year: Int) -> Int {
switch month {
case 2:
if year.isMultiple(of: 400) { return 29 }
if year.isMultiple(of: 100) { return 28 }
return year.isMultiple(of: 4) ? 29 : 28
case 4, 6, 9, 11:
return 30
default:
return 31
}
}
static func providerCostSection(
provider: UsageProvider,
cost: ProviderCostSnapshot?) -> ProviderCostSection?
{
if provider == .manus {
return nil
}
guard let cost else { return nil }
guard provider != .synthetic else { return nil }
if provider == .factory || provider == .devin, cost.period == "Extra usage balance" {
let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
return ProviderCostSection(
title: L("Extra usage"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
}
if provider == .opencodego, cost.period == "Zen balance" {
let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
return ProviderCostSection(
title: L("Zen balance"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
}
if provider == .minimax, cost.period == "MiniMax points balance" {
let balance = String(format: "%.0f", cost.used)
return ProviderCostSection(
title: L("Credits"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
}
if provider == .openai || provider == .claude || provider == .litellm, cost.limit <= 0 {
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "Last 30 days")
return ProviderCostSection(
title: L("API spend"),
percentUsed: nil,
spendLine: "\(periodLabel): \(spend)",
percentLine: nil)
}
if provider == .litellm {
return nil
}
if provider == .clawrouter, cost.limit <= 0 {
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
return ProviderCostSection(
title: "ClawRouter spend",
percentUsed: nil,
spendLine: "\(L("This month")): \(spend)",
percentLine: nil)
}
guard cost.limit > 0 else { return nil }
let used: String
let limit: String
let title: String
if provider == .clawrouter {
title = "Monthly budget"
used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
limit = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode)
} else if cost.currencyCode == "Quota" {
title = L("Quota usage")
used = String(format: "%.0f", cost.used)
limit = String(format: "%.0f", cost.limit)
} else {
title = L("Extra usage")
used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
limit = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode)
}
let percentUsed = Self.clamped((cost.used / cost.limit) * 100)
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "This month")
// When the headline budget is a shared pool (e.g. Cursor team on-demand), show the
// account's own contribution underneath it.
let personalSpendLine: String? = cost.personalUsed.flatMap { personal in
personal > 0
? "\(L("Your spend")): \(UsageFormatter.currencyString(personal, currencyCode: cost.currencyCode))"
: nil
}
return ProviderCostSection(
title: title,
percentUsed: percentUsed,
spendLine: "\(periodLabel): \(used) / \(limit)",
percentLine: String(format: L("%.0f%% used"), min(100, max(0, percentUsed))),
personalSpendLine: personalSpendLine)
}
private static func localizedPeriodLabel(_ label: String) -> String {
let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines)
switch trimmed.lowercased() {
case "last 30 days":
return L("Last 30 days")
case "this month":
return L("This month")
case "today":
return L("Today")
default:
return L(trimmed)
}
}
static func clamped(_ value: Double) -> Double {
min(100, max(0, value))
}
}
+43
View File
@@ -0,0 +1,43 @@
import CodexBarCore
import Foundation
extension UsageMenuCardView.Model {
static func kiroUsageNotes(input: Input) -> [String] {
var notes: [String] = []
if let authMethod = input.snapshot?.loginMethod(for: .kiro)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!authMethod.isEmpty
{
notes.append("\(L("Auth")): \(authMethod)")
}
if let overages = input.snapshot?.kiroUsage?.overagesStatus?
.trimmingCharacters(in: .whitespacesAndNewlines),
!overages.isEmpty
{
notes.append("\(L("Overages")): \(overages)")
}
let overagesEnabled = input.snapshot?.kiroUsage?.overagesStatus?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.hasPrefix("enabled") == true
if overagesEnabled,
let overageCreditsUsed = input.snapshot?.kiroUsage?.overageCreditsUsed
{
notes.append(
"\(L("Overage usage")): \(UsageFormatter.kiroCreditNumber(overageCreditsUsed)) \(L("credits"))")
}
if overagesEnabled,
let estimatedOverageCostUSD = input.snapshot?.kiroUsage?.estimatedOverageCostUSD
{
notes.append("\(L("Overage cost")): \(UsageFormatter.usdString(estimatedOverageCostUSD))")
}
return notes
}
static func kiroPlan(snapshot: UsageSnapshot?) -> String? {
guard let plan = snapshot?.kiroUsage?.displayPlanName,
!plan.isEmpty
else { return nil }
return plan
}
}
+142
View File
@@ -0,0 +1,142 @@
import CodexBarCore
import Foundation
extension UsageMenuCardView.Model {
static func minimaxMetrics(services: [MiniMaxServiceUsage], input: Input) -> [Metric] {
let percentStyle: PercentStyle = .used
let displayNameCounts = Dictionary(grouping: services.map(\.displayName), by: { $0 }).mapValues(\.count)
return services.enumerated().map { index, service in
let used = service.usage
let displayPercent = min(100, max(0, service.percent))
let usageLabel = if service.isUnlimited {
nil as String?
} else {
String(
format: L("minimax_usage_amount_format"),
used.formatted(),
service.limit.formatted())
}
let localizedName = Self.localizedMiniMaxServiceName(service.displayName)
let title = if (displayNameCounts[service.displayName] ?? 0) > 1 {
"\(localizedName) · \(Self.displayWindowBadge(for: service.windowType))"
} else {
localizedName
}
return Metric(
id: "minimax-service-\(index)",
title: title,
percent: displayPercent,
percentStyle: percentStyle,
statusText: service.isUnlimited ? "∞ Unlimited" : nil,
resetText: Self.localizedMiniMaxResetDescription(service.resetDescription),
detailText: nil,
detailLeftText: usageLabel,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true,
warningMarkerPercents: service.isUnlimited
? []
: Self.miniMaxWarningMarkerPercents(service: service, input: input),
workdayMarkerPercents: service.isUnlimited
? []
: Self.miniMaxWorkdayMarkerPercents(service: service, input: input),
cardStyle: false)
}
}
private static func miniMaxWarningMarkerPercents(service: MiniMaxServiceUsage, input: Input) -> [Double] {
switch self.miniMaxQuotaWarningWindow(for: service) {
case .session:
warningMarkerPercents(
thresholds: input.quotaWarningThresholds[.session],
showUsed: true)
case .weekly:
warningMarkerPercents(
thresholds: input.quotaWarningThresholds[.weekly],
showUsed: true)
}
}
private static func miniMaxWorkdayMarkerPercents(service: MiniMaxServiceUsage, input: Input) -> [Double] {
guard self.miniMaxQuotaWarningWindow(for: service) == .weekly else { return [] }
return workDayMarkerPercents(
workDays: input.workDaysPerWeek,
windowMinutes: self.miniMaxWindowMinutes(for: service.windowType))
}
private static func miniMaxQuotaWarningWindow(for service: MiniMaxServiceUsage) -> QuotaWarningWindow {
service.windowType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "weekly" ? .weekly : .session
}
private static func miniMaxWindowMinutes(for windowType: String) -> Int? {
let normalized = windowType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if normalized == "weekly" {
return 7 * 24 * 60
}
if normalized == "today" || normalized == "daily" {
return 24 * 60
}
if normalized == "5h" {
return 5 * 60
}
let pieces = normalized.split(separator: " ")
guard pieces.count >= 2, let value = Int(pieces[0]) else { return nil }
switch pieces[1] {
case "hour", "hours", "hr", "hrs":
return value * 60
case "minute", "minutes", "min", "mins":
return value
default:
return nil
}
}
private static func displayWindowBadge(for windowType: String) -> String {
let trimmed = windowType.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = trimmed.lowercased()
if normalized == "weekly" {
return L("Weekly")
}
if normalized == "5 hours" || normalized == "5 hour" || normalized == "5h" {
return "5h"
}
if normalized == "today" {
return L("Today")
}
if normalized == "daily" {
return L("Daily")
}
return trimmed.isEmpty ? windowType : trimmed
}
private static func localizedMiniMaxResetDescription(_ text: String) -> String {
let prefix = "Resets in "
guard text.hasPrefix(prefix) else { return text }
let rest = String(text.dropFirst(prefix.count))
return L("Resets in %@", rest)
}
private static func localizedMiniMaxServiceName(_ raw: String) -> String {
switch raw {
case "Text Generation", "text_generation":
L("minimax_service_text_generation")
case "Text to Speech", "text_to_speech":
L("minimax_service_text_to_speech")
case "Music Generation", "music_generation":
L("minimax_service_music_generation")
case "Image Generation", "image_generation":
L("minimax_service_image_generation")
case "lyrics_generation":
L("minimax_service_lyrics_generation")
case "coding-plan-vlm":
L("minimax_service_coding_plan_vlm")
case "coding-plan-search":
L("minimax_service_coding_plan_search")
default:
raw
}
}
}
@@ -0,0 +1,933 @@
import CodexBarCore
import SwiftUI
extension UsageMenuCardView.Model {
struct PaceDetail {
let leftLabel: String
let rightLabel: String?
let pacePercent: Double?
let paceOnTop: Bool
}
static func redactedMetricDetail(_ detail: String?, provider: UsageProvider, metricID: String) -> String? {
guard let detail else { return nil }
guard provider == .litellm,
metricID == "secondary",
detail.hasPrefix("Team "),
let separator = detail.range(of: ": ", options: .backwards)
else {
return PersonalInfoRedactor.redactEmails(in: detail, isEnabled: true)
}
return PersonalInfoRedactor.redactEmails(in: "Team\(detail[separator.lowerBound...])", isEnabled: true)
}
static func redactedMetrics(
_ metrics: [Metric],
provider: UsageProvider,
hidePersonalInfo: Bool) -> [Metric]
{
guard hidePersonalInfo else { return metrics }
return metrics.map { metric in
Metric(
id: metric.id,
title: PersonalInfoRedactor.redactEmails(in: metric.title, isEnabled: true) ?? metric.title,
percent: metric.percent,
percentStyle: metric.percentStyle,
statusText: PersonalInfoRedactor.redactEmails(in: metric.statusText, isEnabled: true),
resetText: PersonalInfoRedactor.redactEmails(in: metric.resetText, isEnabled: true),
detailText: Self.redactedMetricDetail(
metric.detailText,
provider: provider,
metricID: metric.id),
detailLeftText: PersonalInfoRedactor.redactEmails(in: metric.detailLeftText, isEnabled: true),
detailRightText: PersonalInfoRedactor.redactEmails(in: metric.detailRightText, isEnabled: true),
pacePercent: metric.pacePercent,
paceOnTop: metric.paceOnTop,
warningMarkerPercents: metric.warningMarkerPercents,
workdayMarkerPercents: metric.workdayMarkerPercents,
cardStyle: metric.cardStyle)
}
}
static func usageNotes(input: Input) -> [String] {
let subscriptionNotes = self.subscriptionMetadataNotes(snapshot: input.snapshot, provider: input.provider)
if input.provider == .sub2api {
return self.sub2APIUsageNotes(input.snapshot?.sub2APIUsage) + subscriptionNotes
}
if input.provider == .kiro {
return self.kiroUsageNotes(input: input) + subscriptionNotes
}
if input.provider == .kilo {
var notes = Self.kiloLoginDetails(snapshot: input.snapshot)
let resolvedSource = input.sourceLabel?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
if input.kiloAutoMode,
resolvedSource == "cli",
!notes.contains(where: { $0.caseInsensitiveCompare("Using CLI fallback") == .orderedSame })
{
notes.append(L("Using CLI fallback"))
}
return notes + subscriptionNotes
}
if input.provider == .mimo, input.snapshot != nil {
return Self.mimoUsageNotes(input: input, subscriptionNotes: subscriptionNotes)
}
if let notes = self.apiProviderUsageNotes(input: input) {
return notes + subscriptionNotes
}
if input.provider == .crossmodel, let crossModel = input.snapshot?.crossModelUsage {
return Self.crossModelSpendNotes(crossModel) + subscriptionNotes
}
guard input.provider == .openrouter,
let openRouter = input.snapshot?.openRouterUsage
else {
return subscriptionNotes
}
var notes = Self.openRouterSpendNotes(openRouter)
switch openRouter.keyQuotaStatus {
case .available:
break
case .noLimitConfigured:
notes.append(L("No limit set for the API key"))
case .unavailable:
notes.append(L("API key limit unavailable right now"))
}
return notes + subscriptionNotes
}
var isOverviewErrorOnly: Bool {
self.subtitleStyle == .error &&
self.metrics.isEmpty &&
self.usageNotes.isEmpty &&
self.openAIAPIUsage == nil &&
self.inlineUsageDashboard == nil &&
self.creditsRemaining == nil &&
self.providerCost == nil &&
self.tokenUsage == nil &&
self.placeholder == nil
}
var hasUsageContent: Bool {
!self.metrics.isEmpty ||
!self.usageNotes.isEmpty ||
self.openAIAPIUsage != nil ||
self.inlineUsageDashboard != nil ||
self.codexResetCredits != nil ||
self.placeholder != nil
}
var usesStackedDetailLayout: Bool {
!self.metrics.isEmpty ||
self.creditsText != nil ||
self.codexResetCredits != nil ||
self.providerCost != nil ||
self.tokenUsage != nil
}
func hasCompatibleTrackedLayout(with candidate: Self) -> Bool {
self.hasCompatibleTrackedLayout(with: candidate, includeMetrics: true)
}
func hasCompatibleTrackedLayoutIgnoringMetrics(with candidate: Self) -> Bool {
self.hasCompatibleTrackedLayout(with: candidate, includeMetrics: false)
}
func hasCompatibleTrackedMetricSubset(of candidate: Self) -> Bool {
guard self.metrics.count < candidate.metrics.count,
self.hasCompatibleTrackedLayoutIgnoringMetrics(with: candidate)
else {
return false
}
return self.metrics.allSatisfy { metric in
candidate.metrics.contains { Self.hasCompatibleMetricLayout(metric, $0) }
}
}
private func hasCompatibleTrackedLayout(with candidate: Self, includeMetrics: Bool) -> Bool {
guard self.provider == candidate.provider,
!includeMetrics || self.metrics.count == candidate.metrics.count,
self.usageNotes == candidate.usageNotes,
(self.openAIAPIUsage == nil) == (candidate.openAIAPIUsage == nil),
Self.hasCompatibleCreditsLayout(
currentText: self.creditsText,
currentRemaining: self.creditsRemaining,
candidateText: candidate.creditsText,
candidateRemaining: candidate.creditsRemaining),
self.creditsHintText == candidate.creditsHintText,
self.codexResetCredits == candidate.codexResetCredits,
self.placeholder == candidate.placeholder,
Self.hasCompatibleDashboardLayout(self.inlineUsageDashboard, candidate.inlineUsageDashboard),
Self.hasCompatibleProviderCostLayout(self.providerCost, candidate.providerCost),
Self.hasCompatibleTokenUsageLayout(self.tokenUsage, candidate.tokenUsage)
else {
return false
}
guard includeMetrics else { return true }
return zip(self.metrics, candidate.metrics).allSatisfy(Self.hasCompatibleMetricLayout)
}
private static func hasCompatibleMetricLayout(_ current: Metric, _ candidate: Metric) -> Bool {
current.id == candidate.id &&
current.title == candidate.title &&
current.percentStyle == candidate.percentStyle &&
(current.statusText == nil) == (candidate.statusText == nil) &&
(current.resetText == nil) == (candidate.resetText == nil) &&
(current.detailText == nil) == (candidate.detailText == nil) &&
(current.detailLeftText == nil) == (candidate.detailLeftText == nil) &&
(current.detailRightText == nil) == (candidate.detailRightText == nil) &&
current.cardStyle == candidate.cardStyle
}
private static func hasCompatibleCreditsLayout(
currentText: String?,
currentRemaining: Double?,
candidateText: String?,
candidateRemaining: Double?) -> Bool
{
switch (currentText, candidateText) {
case (nil, nil):
return true
case let (currentText?, candidateText?):
guard (currentRemaining == nil) == (candidateRemaining == nil) else { return false }
// Numeric balances render as a fixed single line beside the full-scale label.
// Multiline workspace balances retain their measured text until the menu reopens.
return currentRemaining != nil || currentText == candidateText
default:
return false
}
}
private static func hasCompatibleDashboardLayout(
_ current: InlineUsageDashboardModel?,
_ candidate: InlineUsageDashboardModel?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.valueStyle == candidate.valueStyle &&
current.kpis.count == candidate.kpis.count &&
current.points.count == candidate.points.count &&
current.detailLines.count == candidate.detailLines.count &&
zip(current.kpis, candidate.kpis).allSatisfy {
$0.title == $1.title && $0.emphasis == $1.emphasis
} &&
zip(current.points, candidate.points).allSatisfy {
$0.id == $1.id && $0.label == $1.label
}
default:
false
}
}
private static func hasCompatibleProviderCostLayout(
_ current: ProviderCostSection?,
_ candidate: ProviderCostSection?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.title == candidate.title &&
(current.percentUsed == nil) == (candidate.percentUsed == nil) &&
(current.percentLine == nil) == (candidate.percentLine == nil) &&
(current.personalSpendLine == nil) == (candidate.personalSpendLine == nil)
default:
false
}
}
private static func hasCompatibleTokenUsageLayout(
_ current: TokenUsageSection?,
_ candidate: TokenUsageSection?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.hintLine == candidate.hintLine &&
current.errorLine == candidate.errorLine &&
current.comparisonLines.count == candidate.comparisonLines.count
default:
false
}
}
static func progressColor(for provider: UsageProvider) -> Color {
if provider == .elevenlabs {
return Color(nsColor: .labelColor)
}
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
return Color(red: color.red, green: color.green, blue: color.blue)
}
static func rateWindowLabels(
input: Input,
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
{
if input.provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
}
// Legacy request-based Cursor plans track a request quota, not the token-based "Total" pool.
let primaryLabel = if input.provider == .cursor, snapshot.cursorRequests != nil {
"Requests"
} else if input.provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .sub2api {
Sub2APIProviderDescriptor.primaryLabel(details: snapshot.sub2APIUsage) ?? input.metadata.sessionLabel
} else {
input.metadata.sessionLabel
}
return (
L(primaryLabel),
L(input.metadata.weeklyLabel),
input.metadata.opusLabel.map(L) ?? L("Sonnet"),
input.metadata.supportsOpus)
}
static func sub2APIUsageNotes(_ usage: Sub2APIUsageDetails?) -> [String] {
guard let usage else { return [] }
var notes: [String] = []
if let balance = usage.balance {
notes.append("\(L("Balance")): \(UsageFormatter.currencyString(balance, currencyCode: usage.unit))")
}
if let today = usage.today {
notes.append("\(L("Today")): \(self.sub2APITotalsText(today, unit: usage.unit))")
}
if let total = usage.total {
notes.append("\(L("Total")): \(self.sub2APITotalsText(total, unit: usage.unit))")
}
return notes
}
private static func sub2APITotalsText(_ totals: Sub2APIUsageDetails.Totals, unit: String) -> String {
"\(UsageFormatter.tokenCountString(totals.requests)) \(L("requests")) · " +
"\(UsageFormatter.tokenCountString(totals.totalTokens)) \(L("tokens")) · " +
UsageFormatter.currencyString(totals.actualCostUSD, currencyCode: unit)
}
static func resetText(
for window: RateWindow,
style: ResetTimeDisplayStyle,
now: Date) -> String?
{
UsageFormatter.resetLine(for: window, style: style, now: now)
}
static func placeholder(input: Input) -> String? {
if self.shouldShowRateLimitsUnavailablePlaceholder(input: input) {
return L("Limits not available")
}
if input.snapshot == nil, !input.isRefreshing, input.lastError == nil {
return self.hasLocalCodexTokenUsage(input) ? nil : L("No usage yet")
}
return nil
}
static func lastError(input: Input) -> String? {
guard let lastError = input.lastError?.trimmingCharacters(in: .whitespacesAndNewlines),
!lastError.isEmpty
else {
return nil
}
if self.shouldShowRateLimitsUnavailablePlaceholder(input: input, lastError: lastError) {
return nil
}
return lastError
}
static func dashboardHint(error: String?) -> String? {
guard let error, !error.isEmpty else { return nil }
return error
}
static func mimoUsageNotes(input: Input, subscriptionNotes: [String]) -> [String] {
let source = input.sourceLabel?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
guard source != "local" else { return [] }
return [
L("Balance updates in near-real time (up to 5 min lag)"),
L("Daily billing data finalizes at 07:00 UTC"),
] + subscriptionNotes
}
static func subscriptionMetadataNotes(snapshot: UsageSnapshot?, provider: UsageProvider) -> [String] {
guard let snapshot else { return [] }
if let renewsAt = snapshot.subscriptionRenewsAt {
return [String(format: L("Renews: %@"), self.subscriptionDateString(renewsAt, provider: provider))]
}
if let expiresAt = snapshot.subscriptionExpiresAt {
return [String(format: L("Plan expires: %@"), self.subscriptionDateString(expiresAt, provider: provider))]
}
return []
}
private static func subscriptionDateString(_ date: Date, provider: UsageProvider) -> String {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.timeZone = self.subscriptionDateTimeZone(provider: provider)
formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy")
return formatter.string(from: date)
}
private static func subscriptionDateTimeZone(provider: UsageProvider) -> TimeZone {
switch provider {
case .minimax:
TimeZone(identifier: "Asia/Shanghai") ?? .current
default:
.current
}
}
static func poeBalanceDetailText(input: Input) -> String? {
guard input.provider == .poe else { return nil }
return StatusItemController.poeBalanceDisplayText(snapshot: input.snapshot)
}
private static func hasLocalCodexTokenUsage(_ input: Input) -> Bool {
input.provider == .codex &&
input.tokenCostUsageEnabled &&
self.tokenUsageSnapshot(input: input) != nil
}
private static func shouldShowRateLimitsUnavailablePlaceholder(input: Input, lastError: String? = nil) -> Bool {
let currentError = lastError ?? input.lastError
if let currentError = currentError?.trimmingCharacters(in: .whitespacesAndNewlines),
!currentError.isEmpty,
!UsageError.isNoRateLimitsFoundDescription(currentError),
!ClaudeStatusProbe.isSubscriptionQuotaUnavailableDescription(currentError)
{
return false
}
if input.limitsAvailability?.isUnavailable == true {
return true
}
return self.rateLimitsUnavailable(input: input, lastError: currentError)
}
private static func rateLimitsUnavailable(input: Input, lastError: String? = nil) -> Bool {
UsageLimitsAvailability.resolve(
provider: input.provider,
snapshot: input.snapshot,
account: input.account,
lastErrorDescription: lastError ?? input.lastError)
.isUnavailable
}
static func sessionPaceDetail(
provider: UsageProvider,
window: RateWindow,
now: Date,
showUsed: Bool) -> PaceDetail?
{
guard let detail = UsagePaceText.sessionDetail(provider: provider, window: window, now: now) else { return nil }
let expectedUsed = detail.expectedUsedPercent
let actualUsed = window.usedPercent
let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
let actualPercent = showUsed ? actualUsed : (100 - actualUsed)
if expectedPercent.isFinite == false || actualPercent.isFinite == false {
return nil
}
let paceOnTop = actualUsed <= expectedUsed
let pacePercent: Double? = if detail.stage == .onTrack {
nil
} else {
expectedPercent
}
return PaceDetail(
leftLabel: detail.leftLabel,
rightLabel: detail.rightLabel,
pacePercent: pacePercent,
paceOnTop: paceOnTop)
}
static func weeklyPaceDetail(
provider: UsageProvider,
window: RateWindow,
now: Date,
pace: UsagePace?,
showUsed: Bool) -> PaceDetail?
{
guard let pace else { return nil }
let detail = UsagePaceText.weeklyDetail(provider: provider, pace: pace, now: now)
let expectedUsed = detail.expectedUsedPercent
let actualUsed = window.usedPercent
let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
let actualPercent = showUsed ? actualUsed : (100 - actualUsed)
if expectedPercent.isFinite == false || actualPercent.isFinite == false {
return nil
}
let paceOnTop = actualUsed <= expectedUsed
let pacePercent: Double? = if detail.stage == .onTrack {
nil
} else {
expectedPercent
}
return PaceDetail(
leftLabel: detail.leftLabel,
rightLabel: detail.rightLabel,
pacePercent: pacePercent,
paceOnTop: paceOnTop)
}
static func standardWeeklyPace(input: Input, window: RateWindow) -> UsagePace? {
if let weeklyPace = input.weeklyPace {
return weeklyPace
}
return Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
}
private static func displayableWeeklyPace(_ pace: UsagePace?) -> UsagePace? {
guard let pace else { return nil }
return pace.expectedUsedPercent >= 3 || pace.etaSeconds == 0 ? pace : nil
}
static func resetWindowPaceDetail(
window: RateWindow,
input: Input,
pace: UsagePace? = nil) -> PaceDetail?
{
guard self.supportsResetWindowPace(provider: input.provider, window: window),
window.remainingPercent > 0
else { return nil }
let paceWindow = Self.resetWindowForPace(provider: input.provider, window: window)
let resolved = pace ?? UsagePace.weekly(
window: paceWindow,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek)
guard let resolved = Self.displayableWeeklyPace(resolved) else { return nil }
return Self.weeklyPaceDetail(
provider: input.provider,
window: paceWindow,
now: input.now,
pace: resolved,
showUsed: input.usageBarsShowUsed)
}
private static let monthlyWindowSentinelMinutes = 30 * 24 * 60
private static func supportsResetWindowPace(provider: UsageProvider, window: RateWindow) -> Bool {
switch provider {
case .cursor:
window.windowMinutes != nil
case .alibaba, .alibabatokenplan, .doubao, .opencodego:
window.windowMinutes == self.monthlyWindowSentinelMinutes
default:
false
}
}
private static func resetWindowForPace(provider: UsageProvider, window: RateWindow) -> RateWindow {
// Provider snapshots use 30 days as a monthly sentinel; use the reset date for the real calendar-cycle length.
guard self.usesInferredMonthlyDuration(provider: provider, window: window),
let resetsAt = window.resetsAt,
let minutes = self.inferredMonthlyWindowMinutes(endingAt: resetsAt)
else { return window }
return RateWindow(
usedPercent: window.usedPercent,
windowMinutes: minutes,
resetsAt: window.resetsAt,
resetDescription: window.resetDescription,
nextRegenPercent: window.nextRegenPercent,
isSyntheticPlaceholder: window.isSyntheticPlaceholder)
}
private static func usesInferredMonthlyDuration(provider: UsageProvider, window: RateWindow) -> Bool {
guard window.windowMinutes == self.monthlyWindowSentinelMinutes else { return false }
switch provider {
case .alibaba, .alibabatokenplan, .doubao, .opencodego:
return true
default:
return false
}
}
private static func inferredMonthlyWindowMinutes(endingAt resetsAt: Date) -> Int? {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? calendar.timeZone
guard let startsAt = calendar.date(byAdding: .month, value: -1, to: resetsAt) else { return nil }
let minutes = resetsAt.timeIntervalSince(startsAt) / 60
guard minutes.isFinite, minutes > 0 else { return nil }
return Int(minutes.rounded())
}
static func antigravityMetrics(input: Input, snapshot: UsageSnapshot) -> [Metric] {
let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left
if Self.hasAntigravityQuotaSummaryWindows(snapshot) {
return Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
percentStyle: percentStyle)
}
var metrics: [Metric] = []
if let primary = snapshot.primary {
metrics.append(Self.antigravityMetric(
id: "primary",
title: L(input.metadata.sessionLabel),
window: primary,
input: input,
percentStyle: percentStyle))
}
if let secondary = snapshot.secondary {
metrics.append(Self.antigravityMetric(
id: "secondary",
title: L(input.metadata.weeklyLabel),
window: secondary,
input: input,
percentStyle: percentStyle))
}
if input.metadata.supportsOpus, let tertiary = snapshot.tertiary {
metrics.append(Self.antigravityMetric(
id: "tertiary",
title: input.metadata.opusLabel.map(L) ?? L("Gemini Flash"),
window: tertiary,
input: input,
percentStyle: percentStyle))
}
metrics.append(contentsOf: Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
percentStyle: percentStyle))
return metrics
}
static func extraRateWindowMetrics(
snapshot: UsageSnapshot,
input: Input,
percentStyle: PercentStyle) -> [Metric]
{
guard let extraRateWindows = snapshot.extraRateWindows else { return [] }
// Codex additional limits (e.g. Codex Spark) are optional extra usage and follow the
// "optional credits and extra usage" setting. Other providers' extra windows (Antigravity
// per-model quotas, Factory core windows, etc.) are core data and must always render.
if input.provider == .codex, !input.showOptionalCreditsAndExtraUsage {
return []
}
if input.provider == .copilot, !input.copilotBudgetExtrasEnabled {
return []
}
let visibleRateWindows = if input.provider == .codex, !input.codexSparkUsageVisible {
extraRateWindows.filter { !Self.isCodexSparkRateWindow($0) }
} else {
extraRateWindows
}
return visibleRateWindows.map { namedWindow in
let paceDetail = Self.extraRateWindowPaceDetail(
provider: input.provider,
window: namedWindow.window,
input: input)
let usageKnown = namedWindow.usageKnown
let resolvedResetText = Self.extraRateWindowResetText(
namedWindow: namedWindow,
input: input)
let resetText = input.provider == .sub2api && namedWindow.window.resetsAt == nil
? nil
: resolvedResetText
let detailText = input.provider == .sub2api
? namedWindow.window.resetDescription
: nil
let statusText: String? = if usageKnown {
nil
} else if let resetText {
"\(L("Unavailable")) - \(resetText)"
} else {
L("Unavailable")
}
return Metric(
id: namedWindow.id,
title: namedWindow.title,
percent: Self.clamped(
input.usageBarsShowUsed
? namedWindow.window.usedPercent
: namedWindow.window.remainingPercent),
percentStyle: percentStyle,
statusText: statusText,
resetText: usageKnown ? resetText : nil,
detailText: usageKnown ? detailText : nil,
detailLeftText: usageKnown ? paceDetail?.leftLabel : nil,
detailRightText: usageKnown ? paceDetail?.rightLabel : nil,
pacePercent: usageKnown ? paceDetail?.pacePercent : nil,
paceOnTop: paceDetail?.paceOnTop ?? true)
}
}
private static func isCodexSparkRateWindow(_ namedWindow: NamedRateWindow) -> Bool {
namedWindow.id == CodexAdditionalRateLimitMapper.sparkWindowID ||
namedWindow.id == CodexAdditionalRateLimitMapper.sparkWeeklyWindowID
}
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private static func hasAntigravityQuotaSummaryWindows(_ snapshot: UsageSnapshot) -> Bool {
snapshot.extraRateWindows?.contains(where: self.isAntigravityQuotaSummaryWindow) == true
}
private static func isAntigravityQuotaSummaryWindow(_ namedWindow: NamedRateWindow) -> Bool {
namedWindow.id.hasPrefix(self.antigravityQuotaSummaryWindowIDPrefix)
}
private static func extraRateWindowResetText(
namedWindow: NamedRateWindow,
input: Input) -> String?
{
if namedWindow.window.resetsAt != nil {
return self.resetText(
for: namedWindow.window,
style: input.resetTimeDisplayStyle,
now: input.now)
}
if input.provider == .antigravity,
self.isAntigravityQuotaSummaryWindow(namedWindow)
{
return self.antigravityQuotaSummaryResetText(namedWindow.window.resetDescription)
}
return self.resetText(
for: namedWindow.window,
style: input.resetTimeDisplayStyle,
now: input.now)
}
private static func antigravityQuotaSummaryResetText(_ description: String?) -> String? {
guard let description = description?.trimmingCharacters(in: .whitespacesAndNewlines),
!description.isEmpty
else { return nil }
if let range = description.range(of: "fully refresh in ", options: .caseInsensitive) {
var suffix = String(description[range.upperBound...])
.trimmingCharacters(in: .whitespacesAndNewlines)
while suffix.last == "." {
suffix.removeLast()
}
guard !suffix.isEmpty else { return description }
return String(format: L("Resets in %@"), suffix)
}
return description
}
private static func extraRateWindowPaceDetail(
provider: UsageProvider,
window: RateWindow,
input: Input) -> PaceDetail?
{
guard provider == .codex || provider == .antigravity else { return nil }
switch window.windowMinutes {
case 300:
return self.sessionPaceDetail(
provider: provider,
window: window,
now: input.now,
showUsed: input.usageBarsShowUsed)
case 10080:
let pace = Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
return Self.weeklyPaceDetail(
provider: provider,
window: window,
now: input.now,
pace: pace,
showUsed: input.usageBarsShowUsed)
default:
return nil
}
}
private static func antigravityMetricPaceDetail(
window: RateWindow,
input: Input) -> PaceDetail?
{
guard input.provider == .antigravity else { return nil }
switch window.windowMinutes {
case nil, 300:
return self.sessionPaceDetail(
provider: input.provider,
window: window,
now: input.now,
showUsed: input.usageBarsShowUsed)
case 10080:
let pace = Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
return Self.weeklyPaceDetail(
provider: input.provider,
window: window,
now: input.now,
pace: pace,
showUsed: input.usageBarsShowUsed)
default:
return nil
}
}
static func antigravityMetric(
id: String,
title: String,
window: RateWindow?,
input: Input,
percentStyle: PercentStyle) -> Metric
{
guard let window else {
let placeholderPercent = input.usageBarsShowUsed ? 100.0 : 0.0
return Metric(
id: id,
title: title,
percent: placeholderPercent,
percentStyle: percentStyle,
statusText: nil,
resetText: nil,
detailText: nil,
detailLeftText: nil,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true)
}
let percent = input.usageBarsShowUsed ? window.usedPercent : window.remainingPercent
let paceDetail = Self.antigravityMetricPaceDetail(window: window, input: input)
return Metric(
id: id,
title: title,
percent: Self.clamped(percent),
percentStyle: percentStyle,
resetText: Self.resetText(for: window, style: input.resetTimeDisplayStyle, now: input.now),
detailText: nil,
detailLeftText: paceDetail?.leftLabel,
detailRightText: paceDetail?.rightLabel,
pacePercent: paceDetail?.pacePercent,
paceOnTop: paceDetail?.paceOnTop ?? true)
}
static func zaiLimitDetailText(limit: ZaiLimitEntry?) -> String? {
guard let limit else { return nil }
if let currentValue = limit.currentValue,
let usage = limit.usage,
let remaining = limit.remaining
{
let currentStr = UsageFormatter.tokenCountString(currentValue)
let usageStr = UsageFormatter.tokenCountString(usage)
let remainingStr = UsageFormatter.tokenCountString(remaining)
return String(format: L("%@ / %@ (%@ remaining)"), currentStr, usageStr, remainingStr)
}
return nil
}
static func crossModelSpendNotes(_ usage: CrossModelUsageSnapshot) -> [String] {
let candidates: [(String, Double?)] = [
(L("Today"), usage.daily?.cost),
(L("This week"), usage.weekly?.cost),
(L("This month"), usage.monthly?.cost),
]
let rendered = candidates.compactMap { candidate -> String? in
guard let value = candidate.1 else { return nil }
return "\(candidate.0): \(usage.currencyString(value))"
}
guard !rendered.isEmpty else { return [] }
return [rendered.joined(separator: " · ")]
}
static func openRouterQuotaDetail(provider: UsageProvider, snapshot: UsageSnapshot) -> String? {
guard provider == .openrouter,
let usage = snapshot.openRouterUsage,
usage.hasValidKeyQuota,
let keyRemaining = usage.keyRemaining,
let keyLimit = usage.keyLimit
else {
return nil
}
let remaining = UsageFormatter.usdString(keyRemaining)
let limit = UsageFormatter.usdString(keyLimit)
return String(format: L("%@/%@ left"), remaining, limit)
}
static func syntheticRegenDetail(
weekly: RateWindow,
cost: ProviderCostSnapshot?,
now: Date,
showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
{
guard let cost,
cost.limit > 0,
let nextRegenAmount = cost.nextRegenAmount,
nextRegenAmount > 0,
let resetsAt = weekly.resetsAt
else { return nil }
let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
let resetText = String(format: L("Regenerates %@"), countdown)
let nextRegenPercent = (nextRegenAmount / cost.limit) * 100
let afterNextRegenRemaining = min(100, weekly.remainingPercent + nextRegenPercent)
let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
let suffix = showUsed ? L("used after next regen") : L("after next regen")
let ticksToFull = max(0, cost.used) / nextRegenAmount
let left = String(format: "%.0f%% %@", afterNextRegen, suffix)
let right = if ticksToFull <= 0.1 {
L("Near full")
} else if ticksToFull < 1.5 {
L("Full in ~1 regen")
} else {
String(format: L("Full in ~%.0f regens"), ceil(ticksToFull))
}
return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true))
}
static func syntheticRollingRegenDetail(
window: RateWindow,
now: Date,
showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
{
guard let resetsAt = window.resetsAt,
let nextRegenPercent = window.nextRegenPercent,
nextRegenPercent > 0
else { return nil }
let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
let resetText = String(format: L("Regenerates %@"), countdown)
let afterNextRegenRemaining = min(100, window.remainingPercent + nextRegenPercent)
let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
let suffix = showUsed ? L("used after next regen") : L("after next regen")
let left = String(format: "%.0f%% %@", afterNextRegen, suffix)
let missingPercent = max(0, window.usedPercent)
let ticksToFull = missingPercent / nextRegenPercent
let right = if ticksToFull <= 0.1 {
L("Near full")
} else if ticksToFull < 1.5 {
L("Full in ~1 regen")
} else {
String(format: L("Full in ~%.0f regens"), ceil(ticksToFull))
}
return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true))
}
}
@@ -0,0 +1,110 @@
import CodexBarCore
import Foundation
extension UsageMenuCardView.Model {
struct Input {
let provider: UsageProvider
let metadata: ProviderMetadata
let snapshot: UsageSnapshot?
let codexProjection: CodexConsumerProjection?
let credits: CreditsSnapshot?
let creditsError: String?
let dashboard: OpenAIDashboardSnapshot?
let dashboardError: String?
let tokenSnapshot: CostUsageTokenSnapshot?
let tokenError: String?
let account: AccountInfo
let accountIsAuthoritative: Bool
let planOverride: String?
let isRefreshing: Bool
let lastError: String?
let limitsAvailability: UsageLimitsAvailability?
let usageBarsShowUsed: Bool
let resetTimeDisplayStyle: ResetTimeDisplayStyle
let tokenCostUsageEnabled: Bool
let tokenCostInlineDashboardEnabled: Bool
let tokenCostMenuSectionEnabled: Bool
let costComparisonPeriodsEnabled: Bool
let showOptionalCreditsAndExtraUsage: Bool
let codexSparkUsageVisible: Bool
let copilotBudgetExtrasEnabled: Bool
let sourceLabel: String?
let kiloAutoMode: Bool
let hidePersonalInfo: Bool
let weeklyPace: UsagePace?
let quotaWarningThresholds: [QuotaWarningWindow: [Int]]
let workDaysPerWeek: Int?
let usesLiveSubtitle: Bool
let now: Date
init(
provider: UsageProvider,
metadata: ProviderMetadata,
snapshot: UsageSnapshot?,
codexProjection: CodexConsumerProjection? = nil,
credits: CreditsSnapshot?,
creditsError: String?,
dashboard: OpenAIDashboardSnapshot?,
dashboardError: String?,
tokenSnapshot: CostUsageTokenSnapshot?,
tokenError: String?,
account: AccountInfo,
accountIsAuthoritative: Bool = false,
planOverride: String? = nil,
isRefreshing: Bool,
lastError: String?,
limitsAvailability: UsageLimitsAvailability? = nil,
usageBarsShowUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle,
tokenCostUsageEnabled: Bool,
tokenCostInlineDashboardEnabled: Bool? = nil,
tokenCostMenuSectionEnabled: Bool? = nil,
costComparisonPeriodsEnabled: Bool = false,
showOptionalCreditsAndExtraUsage: Bool,
codexSparkUsageVisible: Bool = true,
copilotBudgetExtrasEnabled: Bool = false,
sourceLabel: String? = nil,
kiloAutoMode: Bool = false,
hidePersonalInfo: Bool,
weeklyPace: UsagePace? = nil,
quotaWarningThresholds: [QuotaWarningWindow: [Int]] = [:],
workDaysPerWeek: Int? = nil,
usesLiveSubtitle: Bool = false,
now: Date)
{
self.provider = provider
self.metadata = metadata
self.snapshot = snapshot
self.codexProjection = codexProjection
self.credits = credits
self.creditsError = creditsError
self.dashboard = dashboard
self.dashboardError = dashboardError
self.tokenSnapshot = tokenSnapshot
self.tokenError = tokenError
self.account = account
self.accountIsAuthoritative = accountIsAuthoritative
self.planOverride = planOverride
self.isRefreshing = isRefreshing
self.lastError = lastError
self.limitsAvailability = limitsAvailability
self.usageBarsShowUsed = usageBarsShowUsed
self.resetTimeDisplayStyle = resetTimeDisplayStyle
self.tokenCostUsageEnabled = tokenCostUsageEnabled
self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled ?? tokenCostUsageEnabled
self.tokenCostMenuSectionEnabled = tokenCostMenuSectionEnabled ?? tokenCostUsageEnabled
self.costComparisonPeriodsEnabled = costComparisonPeriodsEnabled
self.showOptionalCreditsAndExtraUsage = showOptionalCreditsAndExtraUsage
self.codexSparkUsageVisible = codexSparkUsageVisible
self.copilotBudgetExtrasEnabled = copilotBudgetExtrasEnabled
self.sourceLabel = sourceLabel
self.kiloAutoMode = kiloAutoMode
self.hidePersonalInfo = hidePersonalInfo
self.weeklyPace = weeklyPace
self.quotaWarningThresholds = quotaWarningThresholds
self.workDaysPerWeek = workDaysPerWeek
self.usesLiveSubtitle = usesLiveSubtitle
self.now = now
}
}
}
File diff suppressed because it is too large Load Diff
+267
View File
@@ -0,0 +1,267 @@
import AppKit
import CodexBarCore
import SwiftUI
@MainActor
struct MenuContent: View {
@Bindable var store: UsageStore
@Bindable var settings: SettingsStore
let account: AccountInfo
let updater: UpdaterProviding
let provider: UsageProvider?
let actions: MenuActions
var body: some View {
let descriptor = MenuDescriptor.build(
provider: self.provider,
store: self.store,
settings: self.settings,
account: self.account,
updateReady: self.updater.updateStatus.isUpdateReady)
VStack(alignment: .leading, spacing: 8) {
ForEach(Array(descriptor.sections.enumerated()), id: \.offset) { index, section in
VStack(alignment: .leading, spacing: 4) {
ForEach(Array(section.entries.enumerated()), id: \.offset) { _, entry in
self.row(for: entry)
}
}
if index < descriptor.sections.count - 1 {
Divider()
}
}
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.frame(minWidth: 260, alignment: .leading)
}
@ViewBuilder
private func row(for entry: MenuDescriptor.Entry) -> some View {
switch entry {
case let .text(text, style):
switch style {
case .headline:
Text(text).font(.headline)
.accessibilityLabel(text)
case .primary:
Text(text)
.accessibilityLabel(text)
case .secondary:
Text(text).foregroundStyle(.secondary).font(.footnote)
.accessibilityLabel(text)
}
case let .action(title, action):
Button {
self.perform(action)
} label: {
if let icon = self.iconName(for: action) {
HStack(spacing: 8) {
Image(systemName: icon)
.imageScale(.medium)
.frame(width: 18, alignment: .center)
Text(title)
}
.foregroundStyle(.primary)
.accessibilityElement(children: .combine)
.accessibilityLabel(title)
} else {
Text(title)
.accessibilityLabel(title)
}
}
.buttonStyle(.plain)
case let .unavailable(title, tooltip):
Text(title)
.foregroundStyle(.secondary)
.help(tooltip ?? "")
case let .submenu(title, systemImageName, submenuItems):
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
if let systemImageName {
Image(systemName: systemImageName)
}
Text(title).font(.headline)
}
.accessibilityElement(children: .combine)
.accessibilityLabel(title)
ForEach(Array(submenuItems.enumerated()), id: \.offset) { _, submenuItem in
HStack(spacing: 8) {
if submenuItem.isChecked {
Image(systemName: "checkmark")
.imageScale(.small)
.frame(width: 18, alignment: .center)
} else {
Spacer().frame(width: 18)
}
Text(submenuItem.title)
.foregroundStyle(submenuItem.isEnabled ? .primary : .secondary)
}
.accessibilityElement(children: .combine)
.accessibilityLabel(submenuItem.title)
}
}
case .divider:
Divider()
}
}
private func iconName(for action: MenuDescriptor.MenuAction) -> String? {
action.systemImageName
}
private func perform(_ action: MenuDescriptor.MenuAction) {
switch action {
case .refresh:
self.actions.refresh()
case .refreshAugmentSession:
self.actions.refreshAugmentSession()
case .installUpdate:
self.actions.installUpdate()
case .dashboard:
self.actions.openDashboard()
case .statusPage:
self.actions.openStatusPage()
case .changelog:
self.actions.openChangelog()
case .addCodexAccount:
self.actions.addCodexAccount()
case .requestCodexSystemPromotion:
return
case let .addProviderAccount(provider):
self.actions.switchAccount(provider)
case let .switchAccount(provider):
self.actions.switchAccount(provider)
case let .openTerminal(command):
self.actions.openTerminal(command)
case let .loginToProvider(url):
if let urlObj = URL(string: url) {
NSWorkspace.shared.open(urlObj)
}
case .settings:
self.actions.openSettings()
case .about:
self.actions.openAbout()
case .quit:
self.actions.quit()
case let .copyError(message):
self.actions.copyError(message)
case .focusAgentSession:
return
}
}
}
struct MenuActions {
let installUpdate: () -> Void
let refresh: () -> Void
let refreshAugmentSession: () -> Void
let openDashboard: () -> Void
let openStatusPage: () -> Void
let openChangelog: () -> Void
let addCodexAccount: () -> Void
let switchAccount: (UsageProvider) -> Void
let openTerminal: (String) -> Void
let openSettings: () -> Void
let openAbout: () -> Void
let quit: () -> Void
let copyError: (String) -> Void
}
struct PersistentRefreshRowMetrics: Equatable {
static let defaults = Self(
rowHeight: 24,
selectionHorizontalInset: 5,
selectionVerticalInset: 0,
selectionCornerRadius: 7,
// Align the custom row's image/title frames with native NSMenuItem columns.
leadingPadding: 15,
trailingPadding: 8,
iconWidth: 16,
iconSymbolPointSize: 16,
iconSymbolWeight: .regular,
iconTitleSpacing: 4.5,
shortcutFontSize: 13,
shortcutXOffset: -9.5,
shortcutYOffset: 0)
let rowHeight: CGFloat
let selectionHorizontalInset: CGFloat
let selectionVerticalInset: CGFloat
let selectionCornerRadius: CGFloat
let leadingPadding: CGFloat
let trailingPadding: CGFloat
let iconWidth: CGFloat
let iconSymbolPointSize: CGFloat
let iconSymbolWeight: NSFont.Weight
let iconTitleSpacing: CGFloat
let shortcutFontSize: CGFloat
let shortcutXOffset: CGFloat
let shortcutYOffset: CGFloat
}
@MainActor
struct StatusIconView: View {
@Bindable var store: UsageStore
let provider: UsageProvider
var body: some View {
Image(nsImage: self.icon)
.renderingMode(.template)
.interpolation(.none)
.accessibilityLabel(self.accessibilityLabel)
.accessibilityValue(self.accessibilityValue)
}
private var accessibilityLabel: String {
let descriptor = ProviderDescriptorRegistry.descriptor(for: self.provider)
return descriptor.metadata.displayName
}
private var accessibilityValue: String {
let snapshot = self.store.snapshot(for: self.provider)
guard let snap = snapshot else {
return L("No data")
}
let remaining = IconRemainingResolver.resolvedRemaining(
snapshot: snap,
style: self.store.style(for: self.provider))
let primary = remaining.primary
let percent = primary.map(Self.accessibilityPercentRemaining) ?? L("Unknown")
let stale = self.store.isStale(provider: self.provider)
return stale ? "\(percent), \(L("stale data"))" : percent
}
static func accessibilityPercentRemaining(_ remaining: Double) -> String {
String(format: L("%d percent remaining"), Int(remaining.rounded()))
}
private var icon: NSImage {
let now = Date()
let snapshot = self.store.snapshot(for: self.provider)
let remaining = snapshot.map {
IconRemainingResolver.resolvedRemaining(
snapshot: $0,
style: self.store.style(for: self.provider),
now: now)
}
let creditsProjection = self.store.codexConsumerProjectionIfNeeded(
for: self.provider,
surface: .menuBar,
snapshotOverride: snapshot,
now: now)
let creditsRemaining = creditsProjection?.menuBarFallback == .creditsBalance
? self.store.codexMenuBarCreditsRemaining(
snapshotOverride: snapshot,
now: now)
: nil
return IconRenderer.makeIcon(
primaryRemaining: remaining?.primary,
weeklyRemaining: remaining?.secondary,
creditsRemaining: creditsRemaining,
stale: self.store.isStale(provider: self.provider),
style: self.store.style(for: self.provider),
statusIndicator: self.store.statusIndicator(for: self.provider),
hideCritters: self.store.settings.menuBarHidesCritters)
}
}
@@ -0,0 +1,169 @@
import CodexBarCore
import Foundation
extension MenuDescriptor {
static func appendOpenAIAPIUsageSummary(
entries: inout [Entry],
usage: OpenAIAPIUsageSnapshot)
{
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
let historyLabel = usage.historyWindowLabel
entries.append(.text(
"\(L("Today")): \(UsageFormatter.usdString(today.costUSD)) · " +
"\(UsageFormatter.tokenCountString(today.totalTokens)) \(L("tokens"))",
.secondary))
entries.append(.text(
"7d: \(UsageFormatter.usdString(last7.costUSD)) · " +
"\(UsageFormatter.tokenCountString(last7.requests)) \(L("requests"))",
.secondary))
entries.append(.text(
"\(historyLabel): \(UsageFormatter.usdString(last30.costUSD)) · " +
"\(UsageFormatter.tokenCountString(last30.requests)) \(L("requests"))",
.secondary))
if let topModel = usage.topModels.first?.name {
entries.append(.text("\(L("Top model")): \(topModel)", .secondary))
}
}
static func appendClaudeAdminAPIUsageSummary(
entries: inout [Entry],
usage: ClaudeAdminAPIUsageSnapshot)
{
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
entries.append(.text(
"\(L("Today")): \(UsageFormatter.usdString(today.costUSD)) · " +
"\(UsageFormatter.tokenCountString(today.totalTokens)) \(L("tokens"))",
.secondary))
entries.append(.text(
"7d: \(UsageFormatter.usdString(last7.costUSD)) · " +
"\(UsageFormatter.tokenCountString(last7.totalTokens)) \(L("tokens"))",
.secondary))
entries.append(.text(
"30d: \(UsageFormatter.usdString(last30.costUSD)) · " +
"\(UsageFormatter.tokenCountString(last30.totalTokens)) \(L("tokens"))",
.secondary))
if let topModel = usage.topModels.first?.name {
entries.append(.text("\(L("Top model")): \(topModel)", .secondary))
}
}
static func appendOpenRouterUsageSummary(
entries: inout [Entry],
usage: OpenRouterUsageSnapshot)
{
if let daily = usage.keyUsageDaily {
entries.append(.text("\(L("Today")): \(UsageFormatter.usdString(daily))", .secondary))
}
if let weekly = usage.keyUsageWeekly {
entries.append(.text("\(L("Week")): \(UsageFormatter.usdString(weekly))", .secondary))
}
if let monthly = usage.keyUsageMonthly {
entries.append(.text("\(L("Month")): \(UsageFormatter.usdString(monthly))", .secondary))
}
}
static func appendMistralUsageSummary(
entries: inout [Entry],
usage: MistralUsageSnapshot)
{
let latest = usage.daily.last
if let latest {
entries.append(.text(
"\(L("Latest")): \(usage.currencySymbol)\(String(format: "%.4f", max(0, latest.cost))) · " +
"\(UsageFormatter.tokenCountString(latest.totalTokens)) \(L("tokens"))",
.secondary))
}
let totalTokens = usage.totalInputTokens + usage.totalCachedTokens + usage.totalOutputTokens
entries.append(.text(
"\(L("Month")): \(usage.currencySymbol)\(String(format: "%.4f", max(0, usage.totalCost))) · " +
"\(UsageFormatter.tokenCountString(totalTokens)) \(L("tokens"))",
.secondary))
if let top = Self.topMistralModel(from: usage.daily) {
entries.append(.text("\(L("Top model")): \(top)", .secondary))
}
}
static func appendPoeUsageSummary(
entries: inout [Entry],
usage: PoeUsageHistorySnapshot)
{
let today = usage.currentDay()
let week = usage.last7Days
let month = usage.last30Days
let todayCostSuffix = today.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let weekCostSuffix = week.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let monthCostSuffix = month.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
entries.append(.text(
"\(L("Today")): \(Self.pointsString(today.points)) · " +
"\(UsageFormatter.tokenCountString(today.requests)) \(L("requests"))\(todayCostSuffix)",
.secondary))
entries.append(.text(
"7d: \(Self.pointsString(week.points)) · " +
"\(UsageFormatter.tokenCountString(week.requests)) \(L("requests"))\(weekCostSuffix)",
.secondary))
entries.append(.text(
"30d: \(Self.pointsString(month.points)) · " +
"\(UsageFormatter.tokenCountString(month.requests)) \(L("requests"))\(monthCostSuffix)",
.secondary))
if let topModel = usage.topModels.first {
entries.append(
.text(
"\(L("Top model")): \(topModel.name) (\(Self.pointsString(topModel.points)))",
.secondary))
}
if !usage.topUsageTypes.isEmpty {
let summary = usage.topUsageTypes
.prefix(2)
.map { "\($0.name): \(Self.pointsString($0.points))" }
.joined(separator: " · ")
entries.append(.text("Usage mix: \(summary)", .secondary))
}
let recent = usage.recentEntries(limit: 3)
if !recent.isEmpty {
entries.append(.text("Recent activity:", .secondary))
for entry in recent {
let stamp = Self.poeTimeString(entry.createdAt)
entries.append(.text(
"\(stamp) · \(entry.model) · \(Self.pointsString(entry.points))",
.secondary))
}
}
}
private static func topMistralModel(from entries: [MistralDailyUsageBucket]) -> String? {
var tokens: [String: Int] = [:]
for entry in entries {
for model in entry.models {
tokens[model.name, default: 0] += model.totalTokens
}
}
return tokens.max {
if $0.value == $1.value {
return $0.key > $1.key
}
return $0.value < $1.value
}?.key
}
private static func pointsString(_ points: Double) -> String {
let value = max(0, points)
if value.rounded() == value {
return "\(UsageFormatter.tokenCountString(Int(value))) points"
}
return "\(String(format: "%.1f", value)) points"
}
private static func poeTimeString(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = .current
formatter.dateFormat = "MM-dd HH:mm"
return formatter.string(from: date)
}
}
@@ -0,0 +1,12 @@
import CodexBarCore
extension MenuDescriptor {
static func appendWayfinderUsageSummary(
entries: inout [Entry],
usage: WayfinderUsageSnapshot)
{
for line in usage.displayLines {
entries.append(.text(line, .secondary))
}
}
}
+752
View File
@@ -0,0 +1,752 @@
import CodexBarCore
import Foundation
@MainActor
struct MenuDescriptor {
struct SubmenuItem: Equatable {
let title: String
let action: MenuAction?
let isEnabled: Bool
let isChecked: Bool
init(title: String, action: MenuAction?, isEnabled: Bool = true, isChecked: Bool = false) {
self.title = title
self.action = action
self.isEnabled = isEnabled
self.isChecked = isChecked
}
}
struct Section {
var entries: [Entry]
}
enum Entry {
case text(String, TextStyle)
case action(String, MenuAction)
case unavailable(String, String?)
case submenu(String, String?, [SubmenuItem])
case divider
var isActionable: Bool {
switch self {
case .action, .submenu, .unavailable: true
case .text, .divider: false
}
}
}
enum MenuActionSystemImage: String {
case installUpdate = "arrow.down.circle"
case refresh = "arrow.clockwise"
case dashboard = "chart.xyaxis.line"
case statusPage = "waveform.path.ecg"
case changelog = "list.bullet.rectangle"
case addAccount = "plus"
case systemAccount = "person.crop.circle"
case switchAccount = "key"
case openTerminal = "terminal"
case loginToProvider = "arrow.right.square"
case settings = "gearshape"
case about = "info.circle"
case quit = "xmark.rectangle"
case copyError = "doc.on.doc"
}
enum TextStyle {
case headline
case primary
case secondary
}
enum MenuAction: Equatable {
case installUpdate
case refresh
case refreshAugmentSession
case dashboard
case statusPage
case changelog
case addCodexAccount
case requestCodexSystemPromotion(UUID)
case addProviderAccount(UsageProvider)
case switchAccount(UsageProvider)
case openTerminal(command: String)
case loginToProvider(url: String)
case settings
case about
case quit
case copyError(String)
case focusAgentSession(AgentSession, remoteHost: String?)
}
var sections: [Section]
static func build(
provider: UsageProvider?,
store: UsageStore,
settings: SettingsStore,
account: AccountInfo,
managedCodexAccountCoordinator: ManagedCodexAccountCoordinator? = nil,
codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator? = nil,
updateReady: Bool,
includeContextualActions: Bool = true,
agentSessionsEnabled: Bool = false,
localAgentSessions: [AgentSession] = [],
remoteAgentHosts: [RemoteSessionHostResult] = [],
now: Date = Date()) -> MenuDescriptor
{
var sections: [Section] = []
if let provider {
let fallbackAccount = store.accountInfo(for: provider)
sections.append(Self.usageSection(for: provider, store: store, settings: settings))
if let accountSection = Self.accountSection(
for: provider,
store: store,
settings: settings,
account: fallbackAccount)
{
sections.append(accountSection)
}
} else {
var addedUsage = false
for enabledProvider in store.enabledProviders() {
sections.append(Self.usageSection(for: enabledProvider, store: store, settings: settings))
addedUsage = true
}
if addedUsage {
if let accountProvider = Self.accountProviderForCombined(store: store),
let fallbackAccount = Optional(store.accountInfo(for: accountProvider)),
let accountSection = Self.accountSection(
for: accountProvider,
store: store,
settings: settings,
account: fallbackAccount)
{
sections.append(accountSection)
}
} else {
sections.append(Section(entries: [.text(L("No usage configured."), .secondary)]))
}
}
if includeContextualActions {
let actions = Self.actionsSection(
for: provider,
store: store,
account: account,
managedCodexAccountCoordinator: managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: codexAccountPromotionCoordinator)
if !actions.entries.isEmpty {
sections.append(actions)
}
}
if agentSessionsEnabled {
sections.append(Self.agentSessionsSection(
localSessions: localAgentSessions,
remoteHosts: remoteAgentHosts,
now: now))
}
sections.append(Self.metaSection(updateReady: updateReady))
return MenuDescriptor(sections: sections)
}
static func agentSessionsSection(
localSessions: [AgentSession],
remoteHosts: [RemoteSessionHostResult],
now: Date = Date()) -> Section
{
let totalCount = localSessions.count + remoteHosts.reduce(0) { $0 + $1.sessions.count }
var entries: [Entry] = [.text("Agent Sessions (\(totalCount))", .headline)]
for session in localSessions {
entries.append(.action(
self.agentSessionRowTitle(session, now: now),
.focusAgentSession(session, remoteHost: nil)))
}
for remoteHost in remoteHosts {
if let error = remoteHost.error {
entries.append(.unavailable("\(remoteHost.host) — unreachable", error))
continue
}
entries.append(.text("\(remoteHost.host)\(remoteHost.sessions.count)", .secondary))
for session in remoteHost.sessions {
entries.append(.action(
self.agentSessionRowTitle(session, now: now),
.focusAgentSession(session, remoteHost: remoteHost.host)))
}
}
if totalCount == 0 {
entries.append(.unavailable("No agent sessions found", nil))
}
return Section(entries: entries)
}
private static func agentSessionRowTitle(_ session: AgentSession, now: Date) -> String {
let state = session.state == .active ? "" : ""
let providerGlyph = session.provider == .codex ? "" : ""
let project = session.projectName ?? "Unknown project"
return "\(state) \(providerGlyph) \(project)\(session.provider.rawValue) · " +
"\(session.source.rawValue) · \(self.agentSessionAge(session, now: now))"
}
private static func agentSessionAge(_ session: AgentSession, now: Date) -> String {
guard let activity = session.lastActivityAt ?? session.startedAt else { return "now" }
let seconds = max(0, Int(now.timeIntervalSince(activity)))
if seconds < 60 {
return "\(seconds)s"
}
if seconds < 3600 {
return "\(seconds / 60)m"
}
if seconds < 86400 {
return "\(seconds / 3600)h"
}
return "\(seconds / 86400)d"
}
private static func usageSection(
for provider: UsageProvider,
store: UsageStore,
settings: SettingsStore) -> Section
{
let meta = store.metadata(for: provider)
var entries: [Entry] = []
let headlineText: String = {
if let ver = Self.versionNumber(for: provider, store: store) {
return "\(meta.displayName) \(ver)"
}
return meta.displayName
}()
entries.append(.text(headlineText, .headline))
if let snap = store.snapshot(for: provider) {
let resetStyle = settings.resetTimeDisplayStyle
let labels = Self.rateWindowLabels(provider: provider, metadata: meta, snapshot: snap)
if let primary = snap.primary {
let primaryDetail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
let primaryDescriptionIsDetail = provider == .warp || provider == .kilo || provider == .abacus ||
provider == .deepseek || provider == .azureopenai || provider == .mimo || provider == .qoder ||
provider == .sub2api
let primaryWindow = if primaryDescriptionIsDetail {
// Some providers use resetDescription for non-reset detail
// (e.g., "Unlimited", "X/Y credits"). Avoid rendering it as a "Resets ..." line.
RateWindow(
usedPercent: primary.usedPercent,
windowMinutes: primary.windowMinutes,
resetsAt: primary.resetsAt,
resetDescription: nil)
} else {
primary
}
Self.appendRateWindow(
entries: &entries,
title: labels.primary,
window: primaryWindow,
resetStyle: resetStyle,
showUsed: settings.usageBarsShowUsed)
if primaryDescriptionIsDetail,
let primaryDetail,
!primaryDetail.isEmpty
{
entries.append(.text(primaryDetail, .secondary))
}
if provider == .crof,
primary.resetsAt != nil,
let detail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
!detail.isEmpty
{
entries.append(.text(detail, .secondary))
}
if provider == .abacus,
let pace = store.weeklyPace(provider: provider, window: primary)
{
let paceSummary = UsagePaceText.weeklySummary(provider: provider, pace: pace)
entries.append(.text(paceSummary, .secondary))
}
if let paceSummary = UsagePaceText.sessionSummary(provider: provider, window: primary) {
entries.append(.text(paceSummary, .secondary))
}
}
if let weekly = snap.secondary {
let weeklyResetOverride: String? = {
guard provider == .warp || provider == .kilo || provider == .perplexity || provider == .crof ||
provider == .sub2api
else { return nil }
let detail = weekly.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let detail, !detail.isEmpty else { return nil }
if provider == .kilo, weekly.resetsAt != nil {
return nil
}
return detail
}()
Self.appendRateWindow(
entries: &entries,
title: labels.secondary,
window: weekly,
resetStyle: resetStyle,
showUsed: settings.usageBarsShowUsed,
resetOverride: weeklyResetOverride)
if provider == .kilo,
weekly.resetsAt != nil,
let detail = weekly.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
!detail.isEmpty
{
entries.append(.text(detail, .secondary))
}
if let pace = store.weeklyPace(provider: provider, window: weekly) {
let paceSummary = UsagePaceText.weeklySummary(provider: provider, pace: pace)
entries.append(.text(paceSummary, .secondary))
}
}
if labels.showsTertiary, let opus = snap.tertiary {
// Perplexity purchased credits don't reset; show the balance as plain text.
let opusResetOverride: String? = provider == .perplexity || provider == .sub2api
? opus.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
: nil
Self.appendRateWindow(
entries: &entries,
title: labels.tertiary,
window: opus,
resetStyle: resetStyle,
showUsed: settings.usageBarsShowUsed,
resetOverride: opusResetOverride)
}
Self.appendProviderUsageSummaries(
entries: &entries,
snapshot: snap,
showOptionalUsage: settings.showOptionalCreditsAndExtraUsage)
if snap.rateLimitsUnavailable(for: provider) {
entries.append(.text(L("Limits not available"), .secondary))
}
} else if !store.isStale(provider: provider),
store.knownLimitsAvailability(for: provider)?.isUnavailable == true
{
entries.append(.text(L("Limits not available"), .secondary))
} else {
entries.append(.text(L("No usage yet"), .secondary))
}
let usageContext = ProviderMenuUsageContext(
provider: provider,
store: store,
settings: settings,
metadata: meta,
snapshot: store.snapshot(for: provider))
ProviderCatalog.implementation(for: provider)?
.appendUsageMenuEntries(context: usageContext, entries: &entries)
return Section(entries: entries)
}
private static func appendProviderUsageSummaries(
entries: inout [Entry],
snapshot: UsageSnapshot,
showOptionalUsage: Bool)
{
if let cost = snapshot.providerCost {
if cost.currencyCode == "Quota" {
let used = String(format: "%.0f", cost.used)
let limit = String(format: "%.0f", cost.limit)
entries.append(.text("\(L("Quota")): \(used) / \(limit)", .primary))
}
}
if let openAIAPIUsage = snapshot.openAIAPIUsage {
Self.appendOpenAIAPIUsageSummary(entries: &entries, usage: openAIAPIUsage)
}
if let claudeAdminAPIUsage = snapshot.claudeAdminAPIUsage {
Self.appendClaudeAdminAPIUsageSummary(entries: &entries, usage: claudeAdminAPIUsage)
}
if let openRouterUsage = snapshot.openRouterUsage {
Self.appendOpenRouterUsageSummary(entries: &entries, usage: openRouterUsage)
}
if let clawRouterUsage = snapshot.clawRouterUsage {
entries.append(.text(
"\(UsageFormatter.tokenCountString(clawRouterUsage.requestCount)) \(L("requests")) · " +
"\(UsageFormatter.tokenCountString(clawRouterUsage.totalTokens)) \(L("tokens"))",
.secondary))
if !clawRouterUsage.providers.isEmpty {
let mix = clawRouterUsage.providers.prefix(5)
.map { "\($0.provider): \(UsageFormatter.tokenCountString($0.requestCount))" }
.joined(separator: " · ")
entries.append(.text("Routed providers: \(mix)", .secondary))
}
}
if let wayfinderUsage = snapshot.wayfinderUsage {
Self.appendWayfinderUsageSummary(entries: &entries, usage: wayfinderUsage)
}
if let poeUsage = snapshot.poeUsage, !poeUsage.daily.isEmpty {
Self.appendPoeUsageSummary(entries: &entries, usage: poeUsage)
}
if let mistralUsage = snapshot.mistralUsage, !mistralUsage.daily.isEmpty {
Self.appendMistralUsageSummary(entries: &entries, usage: mistralUsage)
}
if let mimoUsage = snapshot.mimoUsage {
entries.append(.text("\(L("Balance")): \(mimoUsage.balanceDetail)", .primary))
}
// Sakana pay-as-you-go is optional data gated by "Show optional credits and extra usage".
// Gate the render on the setting too, not just the fetch: toggling the setting off only
// rebuilds the menu, it does not immediately refetch, so a previously-populated
// sakanaPayAsYouGo would otherwise linger in the cached snapshot until the next refresh.
if showOptionalUsage, let sakanaPayAsYouGo = snapshot.sakanaPayAsYouGo {
entries.append(.text("\(L("Balance")): \(sakanaPayAsYouGo.balanceDetail)", .primary))
if let periodUsageTotal = sakanaPayAsYouGo.periodUsageTotal {
entries.append(.text(
"\(L("Usage")): \(UsageFormatter.usdString(periodUsageTotal))",
.secondary))
}
}
}
private static func accountSection(
for provider: UsageProvider,
store: UsageStore,
settings: SettingsStore,
account: AccountInfo) -> Section?
{
let snapshot = store.snapshot(for: provider)
let metadata = store.metadata(for: provider)
let entries = Self.accountEntries(
provider: provider,
snapshot: snapshot,
metadata: metadata,
fallback: account,
hidePersonalInfo: settings.hidePersonalInfo)
guard !entries.isEmpty else { return nil }
return Section(entries: entries)
}
private static func accountEntries(
provider: UsageProvider,
snapshot: UsageSnapshot?,
metadata: ProviderMetadata,
fallback: AccountInfo,
hidePersonalInfo: Bool) -> [Entry]
{
var entries: [Entry] = []
let emailText = snapshot?.accountEmail(for: provider)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let loginMethodText = snapshot?.loginMethod(for: provider)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let redactedEmail = PersonalInfoRedactor.redactEmail(emailText, isEnabled: hidePersonalInfo)
if let emailText, !emailText.isEmpty, !redactedEmail.isEmpty {
entries.append(.text("\(L("Account")): \(redactedEmail)", .secondary))
}
if provider == .kiro {
if let plan = snapshot?.kiroUsage?.displayPlanName,
!plan.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
entries.append(.text("\(L("Plan")): \(plan)", .secondary))
}
if let loginMethodText, !loginMethodText.isEmpty {
entries.append(.text("\(L("Auth")): \(loginMethodText)", .secondary))
}
if let overages = snapshot?.kiroUsage?.overagesStatus,
!overages.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
entries.append(.text("\(L("Overages")): \(overages)", .secondary))
}
} else if provider == .kilo {
let kiloLogin = self.kiloLoginParts(loginMethod: loginMethodText)
if let pass = kiloLogin.pass {
entries.append(.text("\(L("Plan")): \(AccountFormatter.plan(pass, provider: provider))", .secondary))
}
for detail in kiloLogin.details {
entries.append(.text("\(L("Activity")): \(detail)", .secondary))
}
} else if provider == .crossmodel {
if let loginMethodText, !loginMethodText.isEmpty {
entries.append(.text("\(L("Auth")): \(loginMethodText)", .secondary))
}
} else if let loginMethodText, !loginMethodText.isEmpty {
if provider == .openrouter || provider == .mimo || provider == .poe,
loginMethodText.localizedCaseInsensitiveContains("balance:")
{
let balanceValue = loginMethodText
.replacingOccurrences(
of: #"(?i)^\s*balance:\s*"#,
with: "",
options: [.regularExpression])
.trimmingCharacters(in: .whitespacesAndNewlines)
let value = balanceValue.isEmpty ? loginMethodText : balanceValue
entries.append(
.text("\(L("Balance")): \(AccountFormatter.plan(value, provider: provider))", .secondary))
} else {
entries.append(
.text(
"\(L("Plan")): \(AccountFormatter.plan(loginMethodText, provider: provider))",
.secondary))
}
}
if metadata.usesAccountFallback {
if emailText?.isEmpty ?? true, let fallbackEmail = fallback.email, !fallbackEmail.isEmpty {
let redacted = PersonalInfoRedactor.redactEmail(fallbackEmail, isEnabled: hidePersonalInfo)
if !redacted.isEmpty {
entries.append(.text("\(L("Account")): \(redacted)", .secondary))
}
}
if loginMethodText?.isEmpty ?? true, let fallbackPlan = fallback.plan, !fallbackPlan.isEmpty {
entries.append(
.text(
"\(L("Plan")): \(AccountFormatter.plan(fallbackPlan, provider: provider))",
.secondary))
}
}
return entries
}
private static func kiloLoginParts(loginMethod: String?) -> (pass: String?, details: [String]) {
guard let loginMethod else {
return (nil, [])
}
let parts = loginMethod
.components(separatedBy: "·")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
guard !parts.isEmpty else {
return (nil, [])
}
let first = parts[0]
if self.isKiloActivitySegment(first) {
return (nil, parts)
}
return (first, Array(parts.dropFirst()))
}
private static func isKiloActivitySegment(_ text: String) -> Bool {
let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return normalized.hasPrefix("auto top-up:")
}
private static func accountProviderForCombined(store: UsageStore) -> UsageProvider? {
for provider in store.enabledProviders() {
let metadata = store.metadata(for: provider)
if store.snapshot(for: provider)?.identity(for: provider) != nil {
return provider
}
if metadata.usesAccountFallback {
return provider
}
}
return nil
}
private static func actionsSection(
for provider: UsageProvider?,
store: UsageStore,
account: AccountInfo,
managedCodexAccountCoordinator: ManagedCodexAccountCoordinator?,
codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator?) -> Section
{
var entries: [Entry] = []
let targetProvider = provider ?? store.enabledProviders().first
let metadata = targetProvider.map { store.metadata(for: $0) }
let fallbackAccount = targetProvider.map { store.accountInfo(for: $0) } ?? account
let loginContext = targetProvider.map {
ProviderMenuLoginContext(
provider: $0,
store: store,
settings: store.settings,
account: fallbackAccount)
}
// Show "Add Account" if no account, "Switch Account" if logged in
if let targetProvider,
let implementation = ProviderCatalog.implementation(for: targetProvider),
implementation.supportsLoginFlow
{
if let loginContext,
let override = implementation.loginMenuAction(context: loginContext)
{
entries.append(.action(override.label, override.action))
} else {
let loginAction = self.switchAccountTarget(for: provider, store: store)
let hasAccount = self.hasAccount(for: provider, store: store, account: fallbackAccount)
let accountLabel = hasAccount ? L("Switch Account...") : L("Add Account...")
entries.append(.action(accountLabel, loginAction))
}
}
if let targetProvider {
let actionContext = ProviderMenuActionContext(
provider: targetProvider,
store: store,
settings: store.settings,
account: fallbackAccount,
managedCodexAccountCoordinator: managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: codexAccountPromotionCoordinator)
ProviderCatalog.implementation(for: targetProvider)?
.appendActionMenuEntries(context: actionContext, entries: &entries)
}
if metadata?.dashboardURL != nil {
entries.append(.action(L("Usage Dashboard"), .dashboard))
}
if metadata?.statusPageURL != nil || metadata?.statusLinkURL != nil {
entries.append(.action(L("Status Page"), .statusPage))
}
if store.settings.providerChangelogLinksEnabled, metadata?.changelogURL != nil {
entries.append(.action(L("Changelog"), .changelog))
}
if let statusLine = self.statusLine(for: provider, store: store) {
entries.append(.text(statusLine, .secondary))
}
return Section(entries: entries)
}
private static func metaSection(updateReady: Bool) -> Section {
var entries: [Entry] = []
if updateReady {
entries.append(.action(L("Update ready, restart now?"), .installUpdate))
}
entries.append(contentsOf: [
.action(L("Refresh"), .refresh),
.action(L("Settings..."), .settings),
.action(L("About CodexBar"), .about),
.action(L("Quit"), .quit),
])
return Section(entries: entries)
}
private static func statusLine(for provider: UsageProvider?, store: UsageStore) -> String? {
let target = provider ?? store.enabledProviders().first
guard let target,
let status = store.status(for: target),
status.indicator != .none else { return nil }
let description = status.description?.trimmingCharacters(in: .whitespacesAndNewlines)
let label = description?.isEmpty == false ? description! : status.indicator.label
if let updated = status.updatedAt {
let freshness = UsageFormatter.updatedString(from: updated)
return "\(label)\(freshness)"
}
return label
}
private static func switchAccountTarget(for provider: UsageProvider?, store: UsageStore) -> MenuAction {
if let provider {
return .switchAccount(provider)
}
if let enabled = store.enabledProviders().first {
return .switchAccount(enabled)
}
return .switchAccount(.codex)
}
private static func hasAccount(for provider: UsageProvider?, store: UsageStore, account: AccountInfo) -> Bool {
let target = provider ?? store.enabledProviders().first ?? .codex
if let email = store.snapshot(for: target)?.accountEmail(for: target),
!email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
return true
}
let metadata = store.metadata(for: target)
if metadata.usesAccountFallback,
let fallback = account.email?.trimmingCharacters(in: .whitespacesAndNewlines),
!fallback.isEmpty
{
return true
}
return false
}
private static func rateWindowLabels(
provider: UsageProvider,
metadata: ProviderMetadata,
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
{
if provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
}
let primaryLabel = if provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
} else if provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
} else if provider == .sub2api {
Sub2APIProviderDescriptor.primaryLabel(details: snapshot.sub2APIUsage) ?? metadata.sessionLabel
} else {
metadata.sessionLabel
}
return (
L(primaryLabel),
L(metadata.weeklyLabel),
metadata.opusLabel.map(L) ?? L("Sonnet"),
metadata.supportsOpus)
}
private static func appendRateWindow(
entries: inout [Entry],
title: String,
window: RateWindow,
resetStyle: ResetTimeDisplayStyle,
showUsed: Bool,
resetOverride: String? = nil)
{
let line = UsageFormatter
.usageLine(remaining: window.remainingPercent, used: window.usedPercent, showUsed: showUsed)
entries.append(.text("\(title): \(line)", .primary))
if let resetOverride {
entries.append(.text(resetOverride, .secondary))
} else if let reset = UsageFormatter.resetLine(for: window, style: resetStyle) {
entries.append(.text(reset, .secondary))
}
}
private static func versionNumber(for provider: UsageProvider, store: UsageStore) -> String? {
guard let raw = store.version(for: provider) else { return nil }
let pattern = #"[0-9]+(?:\.[0-9]+)*"#
guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
let range = NSRange(raw.startIndex..<raw.endIndex, in: raw)
guard let match = regex.firstMatch(in: raw, options: [], range: range),
let r = Range(match.range, in: raw) else { return nil }
return String(raw[r])
}
}
private enum AccountFormatter {
static func plan(_ text: String, provider: UsageProvider) -> String {
let cleaned = if provider == .codex {
CodexPlanFormatting.displayName(text) ?? UsageFormatter.cleanPlanName(text)
} else {
UsageFormatter.cleanPlanName(text)
}
return cleaned.isEmpty ? text : cleaned
}
static func email(_ text: String) -> String {
text
}
}
extension MenuDescriptor.MenuAction {
var systemImageName: String? {
switch self {
case .installUpdate: MenuDescriptor.MenuActionSystemImage.installUpdate.rawValue
case .settings: MenuDescriptor.MenuActionSystemImage.settings.rawValue
case .about: MenuDescriptor.MenuActionSystemImage.about.rawValue
case .quit: MenuDescriptor.MenuActionSystemImage.quit.rawValue
case .refresh: MenuDescriptor.MenuActionSystemImage.refresh.rawValue
case .refreshAugmentSession: MenuDescriptor.MenuActionSystemImage.refresh.rawValue
case .dashboard: MenuDescriptor.MenuActionSystemImage.dashboard.rawValue
case .statusPage: MenuDescriptor.MenuActionSystemImage.statusPage.rawValue
case .changelog: MenuDescriptor.MenuActionSystemImage.changelog.rawValue
case .addCodexAccount, .addProviderAccount: MenuDescriptor.MenuActionSystemImage.addAccount.rawValue
case .requestCodexSystemPromotion:
nil
case .switchAccount: MenuDescriptor.MenuActionSystemImage.switchAccount.rawValue
case .openTerminal: MenuDescriptor.MenuActionSystemImage.openTerminal.rawValue
case .loginToProvider: MenuDescriptor.MenuActionSystemImage.loginToProvider.rawValue
case .copyError: MenuDescriptor.MenuActionSystemImage.copyError.rawValue
case .focusAgentSession:
nil
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import SwiftUI
extension EnvironmentValues {
@Entry var menuItemHighlighted: Bool = false
/// Optional live-refresh monitor injected into menu card views so the provider card
/// subtitle can reflect the in-flight "Refreshing" state in place while the NSMenu
/// stays open, without rebuilding the menu during AppKit tracking.
@Entry var menuCardRefreshMonitor: MenuCardRefreshMonitor?
}
enum MenuHighlightStyle {
static let selectionText = Color(nsColor: .selectedMenuItemTextColor)
static let normalPrimaryText = Color(nsColor: .controlTextColor)
static let normalSecondaryText = Color(nsColor: .secondaryLabelColor)
static func primary(_ highlighted: Bool) -> Color {
highlighted ? self.selectionText : self.normalPrimaryText
}
static func secondary(_ highlighted: Bool) -> Color {
highlighted ? self.selectionText : self.normalSecondaryText
}
static func error(_ highlighted: Bool) -> Color {
highlighted ? self.selectionText : Color(nsColor: .systemRed)
}
static func progressTrack(_ highlighted: Bool) -> Color {
highlighted ? self.selectionText.opacity(0.22) : Color(nsColor: .tertiaryLabelColor).opacity(0.22)
}
static func progressTint(_ highlighted: Bool, fallback: Color) -> Color {
highlighted ? self.selectionText : fallback
}
static func selectionBackground(_ highlighted: Bool) -> Color {
highlighted ? Color(nsColor: .selectedContentBackgroundColor) : .clear
}
}
@@ -0,0 +1,41 @@
import CodexBarCore
struct MenuOpenRefreshPlan: Equatable {
struct Inputs {
let refreshAllOnOpen: Bool
let enabledProviders: [UsageProvider]
let visibleProviders: [UsageProvider]
let refreshingProviders: Set<UsageProvider>
let staleProviders: Set<UsageProvider>
let missingProviders: Set<UsageProvider>
}
enum Scheduling: Equatable {
case sequential
case concurrent
}
let providers: [UsageProvider]
let scheduling: Scheduling
let refreshCodexDashboard: Bool
static func resolve(_ inputs: Inputs) -> Self {
if inputs.refreshAllOnOpen {
return Self(
providers: inputs.enabledProviders,
scheduling: .concurrent,
refreshCodexDashboard: inputs.enabledProviders.contains(.codex))
}
let enabled = Set(inputs.enabledProviders)
let providers = inputs.visibleProviders.filter {
enabled.contains($0) &&
(inputs.refreshingProviders.contains($0) || inputs.staleProviders.contains($0) ||
inputs.missingProviders.contains($0))
}
return Self(
providers: providers,
scheduling: .sequential,
refreshCodexDashboard: false)
}
}
@@ -0,0 +1,211 @@
struct MenuSessionCoordinator<MenuID: Hashable> {
enum ClosedPreparationPlan: Equatable {
case none
case nonDeferred
case required(version: Int)
}
private(set) var contentVersion = 0
private(set) var latestRequiredRebuildVersion = 0
private(set) var latestDataOnlyContentVersion = 0
private(set) var latestStructuralContentVersion = 0
private(set) var renderedVersions: [MenuID: Int] = [:]
private(set) var deferredUntilNextOpen: Set<MenuID> = []
private(set) var parentRebuildsDeferredDuringTracking: Set<MenuID> = []
private var nextMenuInteractionGeneration = 0
private(set) var menuInteractionGenerations: [MenuID: Int] = [:]
private var nextViewportRestoreGeneration = 0
private(set) var pendingViewportRestores: [MenuID: Int] = [:]
@discardableResult
mutating func invalidate(
allowsStaleContent: Bool,
requiresRebuild: Bool)
-> Int
{
self.contentVersion &+= 1
if allowsStaleContent {
self.latestDataOnlyContentVersion = self.contentVersion
} else {
self.latestStructuralContentVersion = self.contentVersion
if requiresRebuild {
self.latestRequiredRebuildVersion = self.contentVersion
}
}
return self.contentVersion
}
func needsRefresh(_ menuID: MenuID) -> Bool {
self.renderedVersions[menuID] != self.contentVersion
}
mutating func markFresh(_ menuID: MenuID) {
self.renderedVersions[menuID] = self.contentVersion
}
func renderedVersion(for menuID: MenuID) -> Int? {
self.renderedVersions[menuID]
}
func canPreserveStaleContent(for menuID: MenuID) -> Bool {
guard let renderedVersion = self.renderedVersions[menuID] else { return false }
return self.contentVersion == self.latestDataOnlyContentVersion &&
renderedVersion >= self.latestStructuralContentVersion
}
func hasRequiredClosedPreparation(for menuIDs: some Sequence<MenuID>) -> Bool {
guard self.latestRequiredRebuildVersion > 0 else { return false }
return menuIDs.contains { self.isRenderedVersion($0, olderThan: self.latestRequiredRebuildVersion) }
}
func closedPreparationPlan(for menuIDs: some Sequence<MenuID>) -> ClosedPreparationPlan {
if self.hasRequiredClosedPreparation(for: menuIDs) {
return .required(version: self.latestRequiredRebuildVersion)
}
if self.contentVersion > self.latestRequiredRebuildVersion {
return .none
}
return .nonDeferred
}
func isRenderedVersion(_ menuID: MenuID, olderThan version: Int) -> Bool {
(self.renderedVersions[menuID] ?? -1) < version
}
mutating func deferUntilNextOpen(_ menuID: MenuID) {
self.deferredUntilNextOpen.insert(menuID)
}
mutating func clearNextOpenDeferral(_ menuID: MenuID) {
self.deferredUntilNextOpen.remove(menuID)
}
func isDeferredUntilNextOpen(_ menuID: MenuID) -> Bool {
self.deferredUntilNextOpen.contains(menuID)
}
mutating func deferParentRebuild(_ menuID: MenuID) {
self.parentRebuildsDeferredDuringTracking.insert(menuID)
}
mutating func clearParentRebuildDeferral(_ menuID: MenuID) {
self.parentRebuildsDeferredDuringTracking.remove(menuID)
}
func isParentRebuildDeferred(_ menuID: MenuID) -> Bool {
self.parentRebuildsDeferredDuringTracking.contains(menuID)
}
/// Identifies one concrete open/close lifetime even when AppKit reuses the same menu object.
@discardableResult
mutating func beginTrackingSession(_ menuID: MenuID) -> Int {
self.replaceMenuInteractionGeneration(for: menuID)
}
func menuInteractionGeneration(for menuID: MenuID) -> Int? {
self.menuInteractionGenerations[menuID]
}
func isCurrentMenuInteraction(_ generation: Int, for menuID: MenuID) -> Bool {
self.menuInteractionGenerations[menuID] == generation
}
@discardableResult
mutating func advanceMenuInteraction(for menuID: MenuID) -> Int? {
guard self.menuInteractionGenerations[menuID] != nil else { return nil }
return self.replaceMenuInteractionGeneration(for: menuID)
}
private mutating func replaceMenuInteractionGeneration(for menuID: MenuID) -> Int {
self.nextMenuInteractionGeneration &+= 1
self.menuInteractionGenerations[menuID] = self.nextMenuInteractionGeneration
return self.nextMenuInteractionGeneration
}
mutating func endTrackingSession(_ menuID: MenuID) {
self.menuInteractionGenerations.removeValue(forKey: menuID)
}
/// One-shot viewport restore tied to the menu-tracking session that started a manual refresh.
@discardableResult
mutating func armViewportRestore(_ menuID: MenuID) -> Int {
self.nextViewportRestoreGeneration &+= 1
self.pendingViewportRestores[menuID] = self.nextViewportRestoreGeneration
return self.nextViewportRestoreGeneration
}
func isCurrentViewportRestore(_ generation: Int, for menuID: MenuID) -> Bool {
self.pendingViewportRestores[menuID] == generation
}
@discardableResult
mutating func consumeViewportRestore(_ menuID: MenuID, generation: Int) -> Bool {
guard self.isCurrentViewportRestore(generation, for: menuID) else { return false }
self.pendingViewportRestores.removeValue(forKey: menuID)
return true
}
mutating func cancelViewportRestore(_ menuID: MenuID) {
self.pendingViewportRestores.removeValue(forKey: menuID)
}
mutating func removeMenu(_ menuID: MenuID) {
self.renderedVersions.removeValue(forKey: menuID)
self.deferredUntilNextOpen.remove(menuID)
self.parentRebuildsDeferredDuringTracking.remove(menuID)
self.endTrackingSession(menuID)
self.cancelViewportRestore(menuID)
}
mutating func clearMenuTracking() {
self.renderedVersions.removeAll(keepingCapacity: false)
self.deferredUntilNextOpen.removeAll(keepingCapacity: false)
self.parentRebuildsDeferredDuringTracking.removeAll(keepingCapacity: false)
self.menuInteractionGenerations.removeAll(keepingCapacity: false)
self.pendingViewportRestores.removeAll(keepingCapacity: false)
}
#if DEBUG
mutating func replaceContentVersionForTesting(_ version: Int) {
self.contentVersion = version
}
mutating func replaceRenderedVersionsForTesting(_ versions: [MenuID: Int]) {
self.renderedVersions = versions
}
mutating func replaceDeferredMenusForTesting(_ menuIDs: Set<MenuID>) {
self.deferredUntilNextOpen = menuIDs
}
#endif
}
struct MenuRebuildRequestRegistry<MenuID: Hashable> {
private var nextToken = 0
private(set) var tokens: [MenuID: Int] = [:]
mutating func replaceRequest(for menuID: MenuID) -> Int {
self.nextToken &+= 1
self.tokens[menuID] = self.nextToken
return self.nextToken
}
func isCurrent(_ token: Int, for menuID: MenuID) -> Bool {
self.tokens[menuID] == token
}
@discardableResult
mutating func finish(_ token: Int, for menuID: MenuID) -> Bool {
guard self.isCurrent(token, for: menuID) else { return false }
self.tokens.removeValue(forKey: menuID)
return true
}
mutating func cancel(for menuID: MenuID) {
self.tokens.removeValue(forKey: menuID)
}
mutating func cancelAll() {
self.tokens.removeAll(keepingCapacity: false)
}
}
+128
View File
@@ -0,0 +1,128 @@
import CodexBarCore
import Foundation
import Security
protocol MiniMaxAPITokenStoring: Sendable {
func loadToken() throws -> String?
func storeToken(_ token: String?) throws
}
enum MiniMaxAPITokenStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainMiniMaxAPITokenStore: MiniMaxAPITokenStoring {
private static let log = CodexBarLog.logger(LogCategories.minimaxAPITokenStore)
private let service = "com.steipete.CodexBar"
private let account = "minimax-api-token"
func loadToken() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .minimaxToken,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw MiniMaxAPITokenStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw MiniMaxAPITokenStoreError.invalidData
}
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let token, !token.isEmpty {
return token
}
return nil
}
func storeToken(_ token: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token store")
return
}
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned == nil || cleaned?.isEmpty == true {
try self.deleteIfPresent()
return
}
let data = cleaned!.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw MiniMaxAPITokenStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw MiniMaxAPITokenStoreError.keychainStatus(addStatus)
}
}
private func deleteIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw MiniMaxAPITokenStoreError.keychainStatus(status)
}
}
+133
View File
@@ -0,0 +1,133 @@
import CodexBarCore
import Foundation
import Security
protocol MiniMaxCookieStoring: Sendable {
func loadCookieHeader() throws -> String?
func storeCookieHeader(_ header: String?) throws
}
enum MiniMaxCookieStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainMiniMaxCookieStore: MiniMaxCookieStoring {
private static let log = CodexBarLog.logger(LogCategories.minimaxCookieStore)
private let service = "com.steipete.CodexBar"
private let account = "minimax-cookie"
func loadCookieHeader() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping cookie load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .minimaxCookie,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw MiniMaxCookieStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw MiniMaxCookieStoreError.invalidData
}
let header = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let header, !header.isEmpty {
return header
}
return nil
}
func storeCookieHeader(_ header: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping cookie store")
return
}
guard let raw = header?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
try self.deleteIfPresent()
return
}
guard MiniMaxCookieHeader.normalized(from: raw) != nil else {
try self.deleteIfPresent()
return
}
let data = raw.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw MiniMaxCookieStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw MiniMaxCookieStoreError.keychainStatus(addStatus)
}
}
private func deleteIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw MiniMaxCookieStoreError.keychainStatus(status)
}
}
@@ -0,0 +1,69 @@
import AppKit
import SwiftUI
/// Lightweight NSView-based mouse tracking with local coordinates.
///
/// Why: SwiftUI's `onHover` doesn't provide location, but we want "hover a bar to see values" on macOS.
@MainActor
struct MouseLocationReader: NSViewRepresentable {
let onMoved: (CGPoint?) -> Void
func makeNSView(context: Context) -> TrackingView {
let view = TrackingView()
view.onMoved = self.onMoved
return view
}
func updateNSView(_ nsView: TrackingView, context: Context) {
nsView.onMoved = self.onMoved
}
final class TrackingView: NSView {
var onMoved: ((CGPoint?) -> Void)?
private var trackingArea: NSTrackingArea?
override var isFlipped: Bool {
true
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
self.window?.acceptsMouseMovedEvents = true
self.updateTrackingAreas()
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [
// NSMenu popups aren't "key windows", so `.activeInKeyWindow` would drop events and cause hover
// state to flicker. `.activeAlways` keeps tracking stable while the menu is open.
.activeAlways,
.inVisibleRect,
.mouseEnteredAndExited,
.mouseMoved,
]
let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil)
self.addTrackingArea(area)
self.trackingArea = area
}
override func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
self.onMoved?(self.convert(event.locationInWindow, from: nil))
}
override func mouseMoved(with event: NSEvent) {
super.mouseMoved(with: event)
self.onMoved?(self.convert(event.locationInWindow, from: nil))
}
override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
self.onMoved?(nil)
}
}
}
@@ -0,0 +1,60 @@
import CodexBarCore
import Foundation
extension Notification.Name {
static let codexbarOpenSettings = Notification.Name("codexbarOpenSettings")
static let codexbarDebugBlinkNow = Notification.Name("codexbarDebugBlinkNow")
#if DEBUG
static let codexbarDebugSimulateMemoryPressure =
Notification.Name("com.steipete.codexbar.debug.simulateMemoryPressure")
#endif
static let codexbarSessionLimitReset = Notification.Name("codexbarSessionLimitReset")
static let codexbarWeeklyLimitReset = Notification.Name("codexbarWeeklyLimitReset")
static let codexbarProviderConfigDidChange = Notification.Name("codexbarProviderConfigDidChange")
static let codexbarQuotaWarningDidPost = Notification.Name("codexbarQuotaWarningDidPost")
}
@MainActor
final class SessionLimitResetEvent: NSObject {
let provider: UsageProvider
let accountIdentifier: String
let accountLabel: String?
let usedPercent: Double
init(provider: UsageProvider, accountIdentifier: String, accountLabel: String?, usedPercent: Double) {
self.provider = provider
self.accountIdentifier = accountIdentifier
self.accountLabel = accountLabel
self.usedPercent = usedPercent
}
}
@MainActor
final class WeeklyLimitResetEvent: NSObject {
let provider: UsageProvider
let accountIdentifier: String
let accountLabel: String?
let usedPercent: Double
init(provider: UsageProvider, accountIdentifier: String, accountLabel: String?, usedPercent: Double) {
self.provider = provider
self.accountIdentifier = accountIdentifier
self.accountLabel = accountLabel
self.usedPercent = usedPercent
}
}
@MainActor
final class QuotaWarningPostedEvent: NSObject {
let provider: UsageProvider
let window: QuotaWarningWindow
let threshold: Int
let postedAt: Date
init(provider: UsageProvider, window: QuotaWarningWindow, threshold: Int, postedAt: Date) {
self.provider = provider
self.window = window
self.threshold = threshold
self.postedAt = postedAt
}
}
@@ -0,0 +1,548 @@
import AppKit
import CodexBarCore
import WebKit
@MainActor
final class OpenAICreditsPurchaseWindowController: NSWindowController, WKNavigationDelegate, WKScriptMessageHandler {
private static let defaultSize = NSSize(width: 980, height: 760)
private static let logHandlerName = "codexbarLog"
private static let debugLogURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("codexbar-buy-credits.log")
private static let autoStartScript = """
(() => {
if (window.__codexbarAutoBuyCreditsStarted) return 'already';
const log = (...args) => {
try {
window.webkit?.messageHandlers?.codexbarLog?.postMessage(args);
} catch {}
};
const buttonSelector = 'button, a, [role="button"], input[type="button"], input[type="submit"]';
const isVisible = (el) => {
if (!el || !el.getBoundingClientRect) return false;
const rect = el.getBoundingClientRect();
if (rect.width < 2 || rect.height < 2) return false;
const style = window.getComputedStyle ? window.getComputedStyle(el) : null;
if (style) {
if (style.display === 'none' || style.visibility === 'hidden') return false;
if (parseFloat(style.opacity || '1') === 0) return false;
}
return true;
};
const textOf = el => {
const raw = el && (el.innerText || el.textContent) ? String(el.innerText || el.textContent) : '';
return raw.trim();
};
const matches = text => {
const lower = String(text || '').toLowerCase();
if (!lower.includes('credit')) return false;
return (
lower.includes('buy') ||
lower.includes('add') ||
lower.includes('purchase') ||
lower.includes('top up') ||
lower.includes('top-up')
);
};
const matchesAddMore = text => {
const lower = String(text || '').toLowerCase();
return lower.includes('add more');
};
const labelFor = el => {
if (!el) return '';
return textOf(el) || el.getAttribute('aria-label') || el.getAttribute('title') || el.value || '';
};
const summarize = el => {
if (!el) return null;
return {
tag: el.tagName,
type: el.getAttribute('type'),
role: el.getAttribute('role'),
label: labelFor(el),
aria: el.getAttribute('aria-label'),
disabled: isDisabled(el),
href: el.getAttribute('href'),
testId: el.getAttribute('data-testid'),
className: (el.className && String(el.className).slice(0, 120)) || ''
};
};
const collectButtons = () => {
const results = new Set();
const addAll = (root) => {
if (!root || !root.querySelectorAll) return;
root.querySelectorAll(buttonSelector).forEach(el => results.add(el));
};
addAll(document);
document.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) addAll(el.shadowRoot);
});
document.querySelectorAll('iframe').forEach(frame => {
try {
const doc = frame.contentDocument;
if (!doc) return;
addAll(doc);
doc.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) addAll(el.shadowRoot);
});
} catch {}
});
return Array.from(results);
};
const findDialogNextButton = () => {
const dialog = document.querySelector('[role=\"dialog\"], dialog, [aria-modal=\"true\"]');
if (!dialog) return null;
const buttons = Array.from(dialog.querySelectorAll(buttonSelector));
const labeled = buttons.filter(btn => labelFor(btn).toLowerCase().startsWith('next'));
const visible = labeled.find(isVisible);
return visible || labeled[0] || null;
};
const clickButton = (el) => {
if (!el) return false;
try {
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
} catch {
try {
el.click();
} catch {
return false;
}
}
return true;
};
const triggerPointerClick = (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
if (!rect) return false;
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const events = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'];
for (const type of events) {
try {
el.dispatchEvent(new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
}));
} catch {
return false;
}
}
return true;
};
const pickLikelyButton = (buttons) => {
if (!buttons || buttons.length === 0) return null;
const labeled = buttons.find(btn => {
const label = labelFor(btn);
if (matches(label) || matchesAddMore(label)) return true;
const aria = String(btn.getAttribute('aria-label') || '').toLowerCase();
return aria.includes('credit') || aria.includes('buy') || aria.includes('add');
});
return labeled || buttons[0];
};
const findAddMoreButton = () => {
const buttons = collectButtons();
return buttons.find(btn => matchesAddMore(labelFor(btn))) || null;
};
const findNextButton = () => {
const dialogNext = findDialogNextButton();
if (dialogNext) return dialogNext;
const buttons = collectButtons();
const labeled = buttons.filter(btn => {
const label = labelFor(btn).toLowerCase();
return label === 'next' || label.startsWith('next ');
});
const visible = labeled.find(isVisible);
if (visible) return visible;
const submit = buttons.find(btn => btn.type && String(btn.type).toLowerCase() === 'submit' && isVisible(btn));
return submit || labeled[0] || null;
};
const isDisabled = (el) => {
if (!el) return true;
if (el.disabled) return true;
const ariaDisabled = String(el.getAttribute('aria-disabled') || '').toLowerCase();
if (ariaDisabled === 'true') return true;
if (el.classList && (el.classList.contains('disabled') || el.classList.contains('is-disabled'))) {
return true;
}
return false;
};
const forceClickElement = (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
if (rect) {
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const target = document.elementFromPoint(x, y);
if (target) {
try {
target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
return true;
} catch {
return false;
}
}
}
return false;
};
const requestSubmit = (el) => {
if (!el || !el.closest) return false;
const form = el.closest('form');
if (!form) return false;
if (typeof form.requestSubmit === 'function') {
form.requestSubmit(el);
return true;
}
if (typeof form.submit === 'function') {
form.submit();
return true;
}
return false;
};
const clickNextIfReady = (attempts) => {
const nextButton = findNextButton();
if (!nextButton) {
if (attempts && attempts % 5 === 0) log('next_missing', { attempts });
return false;
}
if (isDisabled(nextButton)) {
if (attempts && attempts % 5 === 0) log('next_disabled', summarize(nextButton));
return false;
}
if (!isVisible(nextButton)) {
if (attempts && attempts % 5 === 0) log('next_hidden', summarize(nextButton));
return false;
}
nextButton.focus?.();
if (requestSubmit(nextButton)) {
log('next_submit', summarize(nextButton));
return true;
}
if (triggerPointerClick(nextButton)) {
log('next_pointer', summarize(nextButton));
return true;
}
if (clickButton(nextButton)) {
log('next_click', summarize(nextButton));
return true;
}
return forceClickElement(nextButton);
};
const startNextPolling = (initialDelay = 500, interval = 500, maxAttempts = 90) => {
if (window.__codexbarNextPolling) return;
window.__codexbarNextPolling = true;
log('start_next_poll', { initialDelay, interval, maxAttempts });
setTimeout(() => {
let attempts = 0;
const nextTimer = setInterval(() => {
attempts += 1;
if (attempts % 5 === 0) {
const nextButton = findNextButton();
log('next_poll', {
attempts,
found: Boolean(nextButton),
summary: summarize(nextButton)
});
}
if (clickNextIfReady(attempts) || attempts >= maxAttempts) {
clearInterval(nextTimer);
}
}, interval);
}, initialDelay);
};
const observeNextButton = () => {
if (window.__codexbarNextObserver || !window.MutationObserver) return;
const observer = new MutationObserver(() => {
if (clickNextIfReady(1)) {
observer.disconnect();
window.__codexbarNextObserver = null;
}
});
observer.observe(document.body, { subtree: true, childList: true, attributes: true });
window.__codexbarNextObserver = observer;
};
const findCreditsCardButton = () => {
const nodes = Array.from(document.querySelectorAll('h1,h2,h3,div,span,p'));
const labelMatch = nodes.find(node => {
const lower = textOf(node).toLowerCase();
return lower === 'credits remaining' || (lower.includes('credits') && lower.includes('remaining'));
});
if (!labelMatch) return null;
let cur = labelMatch;
for (let i = 0; i < 6 && cur; i++) {
const buttons = Array.from(cur.querySelectorAll(buttonSelector));
const picked = pickLikelyButton(buttons);
if (picked) return picked;
cur = cur.parentElement;
}
return null;
};
const findAndClick = () => {
const addMoreButton = findAddMoreButton();
if (addMoreButton) {
log('add_more_click', summarize(addMoreButton));
clickButton(addMoreButton);
return true;
}
const cardButton = findCreditsCardButton();
if (!cardButton) return false;
log('credits_card_click', summarize(cardButton));
return clickButton(cardButton);
};
const logDialogButtons = () => {
const dialog = document.querySelector('[role=\"dialog\"], dialog, [aria-modal=\"true\"]');
if (dialog) {
const buttons = Array.from(dialog.querySelectorAll(buttonSelector)).map(summarize).filter(Boolean);
if (buttons.length) {
log('dialog_buttons', { count: buttons.length, buttons: buttons.slice(0, 6) });
}
const nextButton = findDialogNextButton();
if (nextButton) {
log('dialog_next', summarize(nextButton));
setTimeout(() => clickNextIfReady(1), 100);
}
return;
}
const candidates = collectButtons()
.map(summarize)
.filter(Boolean)
.filter(entry => {
const label = (entry.label || '').toLowerCase();
return label.includes('next')
|| label.includes('continue')
|| label.includes('confirm')
|| label.includes('buy');
});
if (candidates.length) {
log('button_candidates', { count: candidates.length, buttons: candidates.slice(0, 8) });
}
};
log('auto_start', { href: location.href, ready: document.readyState });
const iframeSources = Array.from(document.querySelectorAll('iframe'))
.map(frame => frame.getAttribute('src') || '')
.filter(Boolean)
.slice(0, 6);
if (iframeSources.length) {
log('iframes', iframeSources);
}
const shadowHostCount = Array.from(document.querySelectorAll('*')).filter(el => el.shadowRoot).length;
if (shadowHostCount > 0) {
log('shadow_roots', { count: shadowHostCount });
}
if (findAndClick()) {
window.__codexbarAutoBuyCreditsStarted = true;
startNextPolling();
observeNextButton();
logDialogButtons();
return 'clicked';
}
startNextPolling(500);
observeNextButton();
logDialogButtons();
let attempts = 0;
const maxAttempts = 14;
const timer = setInterval(() => {
attempts += 1;
if (findAndClick()) {
logDialogButtons();
startNextPolling();
clearInterval(timer);
return;
}
if (attempts >= maxAttempts) {
clearInterval(timer);
}
}, 500);
window.__codexbarAutoBuyCreditsStarted = true;
return 'scheduled';
})();
"""
private let logger = CodexBarLog.logger(LogCategories.creditsPurchase)
private var webView: WKWebView?
private var accountEmail: String?
private var cacheScope: CookieHeaderCache.Scope?
private var pendingAutoStart = false
private let logHandler = WeakScriptMessageHandler()
init() {
super.init(window: nil)
self.logHandler.delegate = self
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func show(
purchaseURL: URL,
accountEmail: String?,
cacheScope: CookieHeaderCache.Scope?,
autoStartPurchase: Bool)
{
guard Self.canOpenPurchaseWindow(accountEmail: accountEmail, cacheScope: cacheScope) else {
self.close()
self.accountEmail = nil
self.cacheScope = nil
self.logger.error("Buy credits blocked: scoped account email unavailable")
return
}
let normalizedEmail = Self.normalizeEmail(accountEmail)
if self.window == nil || normalizedEmail != self.accountEmail || cacheScope != self.cacheScope {
self.accountEmail = normalizedEmail
self.cacheScope = cacheScope
self.buildWindow()
}
Self.resetDebugLog()
let accountValue = normalizedEmail == nil ? "none" : "set"
let sanitizedURL = Self.sanitizedURLString(purchaseURL)
Self.appendDebugLog(
"show autoStart=\(autoStartPurchase) url=\(sanitizedURL) account=\(accountValue)")
self.logger.info("Buy credits window opened")
self.logger.debug("Auto-start purchase", metadata: ["enabled": autoStartPurchase ? "1" : "0"])
self.logger.debug("Purchase URL", metadata: ["url": sanitizedURL])
self.logger.debug("Account email", metadata: ["state": accountValue])
self.pendingAutoStart = autoStartPurchase
self.load(url: purchaseURL)
self.window?.center()
self.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
private func buildWindow() {
let config = WKWebViewConfiguration()
config.userContentController.add(self.logHandler, name: Self.logHandlerName)
config.websiteDataStore = OpenAIDashboardWebsiteDataStore.store(
forAccountEmail: self.accountEmail,
scope: self.cacheScope)
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = self
webView.allowsBackForwardNavigationGestures = true
webView.translatesAutoresizingMaskIntoConstraints = false
let container = NSView(frame: .zero)
container.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
webView.topAnchor.constraint(equalTo: container.topAnchor),
webView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
let window = NSWindow(
contentRect: Self.defaultFrame(),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false)
window.title = "Buy Credits"
window.isReleasedWhenClosed = false
window.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary]
window.contentView = container
window.center()
window.delegate = self
self.window = window
self.webView = webView
}
private func load(url: URL) {
guard let webView else { return }
let request = URLRequest(url: url)
webView.load(request)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard self.pendingAutoStart else { return }
self.pendingAutoStart = false
let currentURL = webView.url?.absoluteString ?? "unknown"
Self.appendDebugLog("didFinish url=\(currentURL)")
self.logger.debug("Buy credits navigation finished", metadata: ["url": currentURL])
webView.evaluateJavaScript(Self.autoStartScript) { [logger] result, error in
if let error {
Self.appendDebugLog("autoStart error=\(error.localizedDescription)")
logger.error("Auto-start purchase failed", metadata: ["error": error.localizedDescription])
return
}
if let result {
Self.appendDebugLog("autoStart result=\(String(describing: result))")
logger.debug("Auto-start purchase result", metadata: ["result": String(describing: result)])
}
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == Self.logHandlerName else { return }
let payload = String(describing: message.body)
Self.appendDebugLog("js \(payload)")
self.logger.debug("Auto-buy log", metadata: ["payload": payload])
}
private static func normalizeEmail(_ email: String?) -> String? {
guard let raw = email?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return nil }
return raw.lowercased()
}
static func canOpenPurchaseWindow(accountEmail: String?, cacheScope: CookieHeaderCache.Scope?) -> Bool {
cacheScope == nil || self.normalizeEmail(accountEmail) != nil
}
private static func defaultFrame() -> NSRect {
let visible = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 900)
let width = min(Self.defaultSize.width, visible.width * 0.92)
let height = min(Self.defaultSize.height, visible.height * 0.88)
let origin = NSPoint(x: visible.midX - width / 2, y: visible.midY - height / 2)
return NSRect(origin: origin, size: NSSize(width: width, height: height))
}
private static func appendDebugLog(_ message: String) {
let timestamp = ISO8601DateFormatter().string(from: Date())
let line = "[\(timestamp)] \(LogRedactor.redact(message))\n"
guard let data = line.data(using: .utf8) else { return }
if FileManager.default.fileExists(atPath: Self.debugLogURL.path) {
if let handle = try? FileHandle(forWritingTo: Self.debugLogURL) {
handle.seekToEndOfFile()
handle.write(data)
try? handle.close()
}
} else {
try? data.write(to: Self.debugLogURL, options: .atomic)
}
}
private static func resetDebugLog() {
try? FileManager.default.removeItem(at: self.debugLogURL)
}
private static func sanitizedURLString(_ url: URL) -> String {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return url.absoluteString
}
components.query = nil
components.fragment = nil
return components.string ?? url.absoluteString
}
}
private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
weak var delegate: WKScriptMessageHandler?
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
self.delegate?.userContentController(userContentController, didReceive: message)
}
}
// MARK: - NSWindowDelegate
extension OpenAICreditsPurchaseWindowController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
guard let window = self.window else { return }
let webView = self.webView
self.pendingAutoStart = false
self.webView = nil
self.window = nil
self.logger.info("Buy credits window closing")
WebKitTeardown.scheduleCleanup(owner: window, window: window, webView: webView)
}
}
@@ -0,0 +1,33 @@
import Foundation
enum PersonalInfoRedactor {
static let emailPlaceholder = ""
private static let emailRegex: NSRegularExpression? = {
let pattern = #"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}"#
return try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
}()
static func redactEmail(_ email: String?, isEnabled: Bool) -> String {
guard let email, !email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return "" }
guard isEnabled else { return email }
return Self.emailPlaceholder
}
static func redactEmails(in text: String?, isEnabled: Bool) -> String? {
guard let text else { return nil }
guard isEnabled else { return text }
guard let regex = Self.emailRegex else { return text }
let range = NSRange(text.startIndex..<text.endIndex, in: text)
guard regex.firstMatch(in: text, options: [], range: range) != nil else { return text }
let redacted = regex.stringByReplacingMatches(
in: text,
options: [],
range: range,
withTemplate: Self.emailPlaceholder)
return redacted
.replacingOccurrences(of: #"\s+([:.,;])"#, with: "$1", options: .regularExpression)
.replacingOccurrences(of: #"\s{2,}"#, with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
@@ -0,0 +1,853 @@
import Charts
import CodexBarCore
import SwiftUI
@MainActor
struct PlanUtilizationHistoryChartMenuView: View {
private enum Layout {
static let chartHeight: CGFloat = 130
static let detailHeight: CGFloat = 16
static let emptyStateHeight: CGFloat = chartHeight + detailHeight
static let maxPoints = 30
static let maxAxisLabels = 4
static let barWidth: CGFloat = 6
}
private struct SeriesSelection: Hashable {
let name: PlanUtilizationSeriesName
let windowMinutes: Int
var id: String {
"\(self.name.rawValue):\(self.windowMinutes)"
}
}
private struct VisibleSeries: Identifiable, Equatable {
let selection: SeriesSelection
let title: String
let history: PlanUtilizationSeriesHistory
var id: String {
self.selection.id
}
}
private struct EntryPointAccumulator {
let effectiveBoundaryDate: Date
let displayBoundaryDate: Date
let observedAt: Date
let usedPercent: Double
let hasObservedResetBoundary: Bool
}
private struct ResetBoundaryLattice {
let referenceBoundaryDate: Date
let windowInterval: TimeInterval
}
private struct Point: Identifiable {
let id: Date
let index: Int
let date: Date
let usedPercent: Double
let isObserved: Bool
}
private struct Model {
let points: [Point]
let axisIndexes: [Double]
let xDomain: ClosedRange<Double>?
let pointsByID: [Date: Point]
let pointsByIndex: [Int: Point]
let barColor: Color
let trackColor: Color
}
private let provider: UsageProvider
private let visibleSeries: [VisibleSeries]
private let modelsBySeriesID: [String: Model]
private let emptyModel: Model
private let width: CGFloat
@State private var selectedSeriesID: String?
@State private var selectedPointID: Date?
init(
provider: UsageProvider,
histories: [PlanUtilizationSeriesHistory],
snapshot: UsageSnapshot? = nil,
width: CGFloat)
{
self.provider = provider
let visibleSeries = Self.visibleSeries(
histories: histories,
provider: provider,
snapshot: snapshot)
let referenceDate = Date()
self.visibleSeries = visibleSeries
self.modelsBySeriesID = Dictionary(uniqueKeysWithValues: visibleSeries.map {
($0.id, Self.makeModel(history: $0.history, provider: provider, referenceDate: referenceDate))
})
self.emptyModel = Self.emptyModel(provider: provider)
self.width = width
}
var body: some View {
let effectiveSelectedSeries = self.visibleSeries.first(where: { $0.id == self.selectedSeriesID })
?? self.visibleSeries.first
let model = effectiveSelectedSeries.flatMap { self.modelsBySeriesID[$0.id] } ?? self.emptyModel
VStack(alignment: .leading, spacing: 10) {
if self.visibleSeries.count > 1 {
Picker(selection: Binding(
get: { effectiveSelectedSeries?.id ?? "" },
set: { newValue in
self.selectedSeriesID = newValue
self.selectedPointID = nil
})) {
ForEach(self.visibleSeries) { series in
Text(series.title).tag(series.id)
}
} label: {
EmptyView()
}
.labelsHidden()
.pickerStyle(.segmented)
}
if model.points.isEmpty {
ZStack {
Text(Self.emptyStateText(title: effectiveSelectedSeries?.title))
.font(.footnote)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.frame(height: Layout.emptyStateHeight)
} else {
self.utilizationChart(model: model)
.chartYAxis(.hidden)
.chartYScale(domain: 0...100)
.chartXAxis {
AxisMarks(values: model.axisIndexes) { value in
AxisGridLine().foregroundStyle(Color.clear)
AxisTick().foregroundStyle(Color.clear)
AxisValueLabel {
if let raw = value.as(Double.self) {
let index = Int(raw.rounded())
if let point = model.pointsByIndex[index] {
let isTrailingFullChartLabel = index == model.points.last?.index
&& model.points.count == Layout.maxPoints
Self.axisLabel(
for: point,
windowMinutes: effectiveSelectedSeries?.history.windowMinutes ?? 0,
isTrailingFullChartLabel: isTrailingFullChartLabel)
}
}
}
}
}
.chartLegend(.hidden)
.frame(height: Layout.chartHeight)
.accessibilityLabel(L("Plan utilization chart"))
.accessibilityValue(
model.points.isEmpty
? L("No data")
: String(format: L("%d utilization samples"), model.points.count))
.chartOverlay { proxy in
GeometryReader { geo in
MouseLocationReader { location in
self.updateSelection(location: location, model: model, proxy: proxy, geo: geo)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
Text(self.detailLine(model: model, windowMinutes: effectiveSelectedSeries?.history.windowMinutes ?? 0))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Layout.detailHeight, alignment: .leading)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.frame(minWidth: self.width, maxWidth: .infinity, alignment: .topLeading)
.task(id: self.visibleSeries.map(\.id).joined(separator: ",")) {
guard let firstVisibleSeries = self.visibleSeries.first else { return }
guard !self.visibleSeries.contains(where: { $0.id == self.selectedSeriesID }) else { return }
self.selectedSeriesID = firstVisibleSeries.id
self.selectedPointID = nil
}
}
private nonisolated static func visibleSeries(
histories: [PlanUtilizationSeriesHistory],
provider: UsageProvider,
snapshot: UsageSnapshot?) -> [VisibleSeries]
{
let metadata = ProviderDescriptorRegistry.metadata[provider]
let allowedNames = self.visibleSeriesNames(provider: provider, snapshot: snapshot)
var historiesBySelection: [SeriesSelection: PlanUtilizationSeriesHistory] = [:]
for history in histories {
guard !history.entries.isEmpty else { continue }
guard history.windowMinutes > 0 else { continue }
guard allowedNames?.contains(history.name) ?? true else { continue }
let canonicalWindowMinutes = history.name.canonicalWindowMinutes(history.windowMinutes)
let selection = SeriesSelection(name: history.name, windowMinutes: canonicalWindowMinutes)
if let existingHistory = historiesBySelection[selection] {
historiesBySelection[selection] = PlanUtilizationSeriesHistory(
name: history.name,
windowMinutes: canonicalWindowMinutes,
entries: Self.mergedEntries(existingHistory.entries + history.entries))
} else {
historiesBySelection[selection] = PlanUtilizationSeriesHistory(
name: history.name,
windowMinutes: canonicalWindowMinutes,
entries: history.entries)
}
}
return historiesBySelection.values
.sorted { lhs, rhs in
let lhsOrder = self.seriesSortOrder(lhs.name)
let rhsOrder = self.seriesSortOrder(rhs.name)
if lhsOrder != rhsOrder {
return lhsOrder < rhsOrder
}
if lhs.windowMinutes != rhs.windowMinutes {
return lhs.windowMinutes < rhs.windowMinutes
}
return lhs.name.rawValue < rhs.name.rawValue
}
.map { history in
VisibleSeries(
selection: SeriesSelection(name: history.name, windowMinutes: history.windowMinutes),
title: self.seriesTitle(name: history.name, metadata: metadata),
history: history)
}
}
nonisolated static func mergedEntries(
_ entries: [PlanUtilizationHistoryEntry]) -> [PlanUtilizationHistoryEntry]
{
var seen: Set<PlanUtilizationHistoryEntry> = []
return entries.filter { entry in
seen.insert(entry).inserted
}
}
private nonisolated static func visibleSeriesNames(
provider: UsageProvider,
snapshot: UsageSnapshot?) -> Set<PlanUtilizationSeriesName>?
{
guard let snapshot else { return nil }
var names: Set<PlanUtilizationSeriesName> = []
switch provider {
case .codex:
if snapshot.primary != nil { names.insert(.session) }
if snapshot.secondary != nil { names.insert(.weekly) }
case .claude:
if snapshot.primary != nil { names.insert(.session) }
if snapshot.secondary != nil { names.insert(.weekly) }
if snapshot.tertiary != nil,
ProviderDescriptorRegistry.metadata[provider]?.supportsOpus == true
{
names.insert(.opus)
}
default:
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
+ (snapshot.extraRateWindows?.filter(\.usageKnown).map(\.window) ?? [])
guard windows.contains(where: { $0.windowMinutes == 7 * 24 * 60 }) else { return nil }
names.insert(.weekly)
}
return names
}
private nonisolated static func makeModel(
history: PlanUtilizationSeriesHistory?,
provider: UsageProvider,
referenceDate: Date) -> Model
{
guard let history else {
return self.emptyModel(provider: provider)
}
var points = self.seriesPoints(history: history, referenceDate: referenceDate)
if points.count > Layout.maxPoints {
points = Array(points.suffix(Layout.maxPoints))
}
points = points.enumerated().map { offset, point in
Point(
id: point.id,
index: offset,
date: point.date,
usedPercent: point.usedPercent,
isObserved: point.isObserved)
}
let pointsByID = Dictionary(uniqueKeysWithValues: points.map { ($0.id, $0) })
let pointsByIndex = Dictionary(uniqueKeysWithValues: points.map { ($0.index, $0) })
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
let barColor = Color(red: color.red, green: color.green, blue: color.blue)
let trackColor = MenuHighlightStyle.progressTrack(false)
return Model(
points: points,
axisIndexes: self.axisIndexes(points: points, windowMinutes: history.windowMinutes),
xDomain: self.xDomain(points: points),
pointsByID: pointsByID,
pointsByIndex: pointsByIndex,
barColor: barColor,
trackColor: trackColor)
}
private nonisolated static func emptyModel(provider: UsageProvider) -> Model {
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
let barColor = Color(red: color.red, green: color.green, blue: color.blue)
let trackColor = MenuHighlightStyle.progressTrack(false)
return Model(
points: [],
axisIndexes: [],
xDomain: nil,
pointsByID: [:],
pointsByIndex: [:],
barColor: barColor,
trackColor: trackColor)
}
private nonisolated static func seriesPoints(
history: PlanUtilizationSeriesHistory,
referenceDate: Date) -> [Point]
{
guard history.windowMinutes > 0 else { return [] }
let windowInterval = Double(history.windowMinutes) * 60
let resetBoundaryLattice = self.resetBoundaryLattice(
entries: history.entries,
windowMinutes: history.windowMinutes)
var strongestObservedPointByPeriod: [Date: EntryPointAccumulator] = [:]
for entry in history.entries {
let candidate = self.observedPointCandidate(
for: entry,
windowMinutes: history.windowMinutes,
resetBoundaryLattice: resetBoundaryLattice)
if let existing = strongestObservedPointByPeriod[candidate.effectiveBoundaryDate],
!self.shouldPreferObservedPoint(candidate, over: existing)
{
continue
}
strongestObservedPointByPeriod[candidate.effectiveBoundaryDate] = candidate
}
guard !strongestObservedPointByPeriod.isEmpty else { return [] }
let sortedPeriodBoundaryDates = strongestObservedPointByPeriod.keys.sorted()
var points: [Point] = []
var previousPeriodBoundaryDate: Date?
for periodBoundaryDate in sortedPeriodBoundaryDates {
if let previousPeriodBoundaryDate {
var cursor = previousPeriodBoundaryDate.addingTimeInterval(windowInterval)
while cursor < periodBoundaryDate {
points.append(Point(
id: cursor,
index: 0,
date: cursor,
usedPercent: 0,
isObserved: false))
cursor = cursor.addingTimeInterval(windowInterval)
}
}
if let bucket = strongestObservedPointByPeriod[periodBoundaryDate] {
points.append(Point(
id: bucket.effectiveBoundaryDate,
index: 0,
date: bucket.displayBoundaryDate,
usedPercent: bucket.usedPercent,
isObserved: true))
}
previousPeriodBoundaryDate = periodBoundaryDate
}
if let lastObservedPeriodBoundaryDate = sortedPeriodBoundaryDates.last {
let currentPeriodBoundaryDate = self.currentPeriodBoundaryDate(
for: referenceDate,
windowMinutes: history.windowMinutes,
resetBoundaryLattice: resetBoundaryLattice)
if currentPeriodBoundaryDate > lastObservedPeriodBoundaryDate {
var cursor = lastObservedPeriodBoundaryDate.addingTimeInterval(windowInterval)
while cursor <= currentPeriodBoundaryDate {
points.append(Point(
id: cursor,
index: 0,
date: cursor,
usedPercent: 0,
isObserved: false))
cursor = cursor.addingTimeInterval(windowInterval)
}
}
}
return points
}
private nonisolated static func observedPointCandidate(
for entry: PlanUtilizationHistoryEntry,
windowMinutes: Int,
resetBoundaryLattice: ResetBoundaryLattice?) -> EntryPointAccumulator
{
let rawResetBoundaryDate = entry.resetsAt.map(self.normalizedBoundaryDate)
let effectiveBoundaryDate = self.effectivePeriodBoundaryDate(
for: entry,
windowMinutes: windowMinutes,
rawResetBoundaryDate: rawResetBoundaryDate,
resetBoundaryLattice: resetBoundaryLattice)
return EntryPointAccumulator(
effectiveBoundaryDate: effectiveBoundaryDate,
displayBoundaryDate: rawResetBoundaryDate ?? effectiveBoundaryDate,
observedAt: entry.capturedAt,
usedPercent: max(0, min(100, entry.usedPercent)),
hasObservedResetBoundary: rawResetBoundaryDate != nil)
}
private nonisolated static func resetBoundaryLattice(
entries: [PlanUtilizationHistoryEntry],
windowMinutes: Int) -> ResetBoundaryLattice?
{
guard let latestObservedResetBoundaryDate = entries
.compactMap(\.resetsAt)
.map(self.normalizedBoundaryDate)
.max()
else {
return nil
}
return ResetBoundaryLattice(
referenceBoundaryDate: latestObservedResetBoundaryDate,
windowInterval: Double(windowMinutes) * 60)
}
private nonisolated static func normalizedBoundaryDate(_ date: Date) -> Date {
Date(timeIntervalSince1970: floor(date.timeIntervalSince1970))
}
private nonisolated static func effectivePeriodBoundaryDate(
for entry: PlanUtilizationHistoryEntry,
windowMinutes: Int,
rawResetBoundaryDate: Date?,
resetBoundaryLattice: ResetBoundaryLattice?) -> Date
{
if let rawResetBoundaryDate {
if let resetBoundaryLattice {
return self.closestPeriodBoundaryDate(
to: rawResetBoundaryDate,
resetBoundaryLattice: resetBoundaryLattice)
}
return rawResetBoundaryDate
}
if let resetBoundaryLattice {
return self.periodBoundaryDate(
containing: entry.capturedAt,
resetBoundaryLattice: resetBoundaryLattice)
}
return self.syntheticBoundaryDate(for: entry.capturedAt, windowMinutes: windowMinutes)
}
private nonisolated static func shouldPreferObservedPoint(
_ candidate: EntryPointAccumulator,
over existing: EntryPointAccumulator) -> Bool
{
if candidate.usedPercent != existing.usedPercent {
return candidate.usedPercent > existing.usedPercent
}
if candidate.hasObservedResetBoundary != existing.hasObservedResetBoundary {
return candidate.hasObservedResetBoundary
}
if candidate.displayBoundaryDate != existing.displayBoundaryDate {
return candidate.displayBoundaryDate > existing.displayBoundaryDate
}
return candidate.observedAt >= existing.observedAt
}
private nonisolated static func currentPeriodBoundaryDate(
for referenceDate: Date,
windowMinutes: Int,
resetBoundaryLattice: ResetBoundaryLattice?) -> Date
{
if let resetBoundaryLattice {
return self.periodBoundaryDate(
containing: referenceDate,
resetBoundaryLattice: resetBoundaryLattice)
}
return self.syntheticBoundaryDate(for: referenceDate, windowMinutes: windowMinutes)
}
private nonisolated static func closestPeriodBoundaryDate(
to rawBoundaryDate: Date,
resetBoundaryLattice: ResetBoundaryLattice) -> Date
{
let offset = rawBoundaryDate.timeIntervalSince(resetBoundaryLattice.referenceBoundaryDate)
let periodOffset = (offset / resetBoundaryLattice.windowInterval).rounded()
return resetBoundaryLattice.referenceBoundaryDate
.addingTimeInterval(periodOffset * resetBoundaryLattice.windowInterval)
}
private nonisolated static func periodBoundaryDate(
containing capturedAt: Date,
resetBoundaryLattice: ResetBoundaryLattice) -> Date
{
let offset = capturedAt.timeIntervalSince(resetBoundaryLattice.referenceBoundaryDate)
let periodOffset = ceil(offset / resetBoundaryLattice.windowInterval)
return resetBoundaryLattice.referenceBoundaryDate
.addingTimeInterval(periodOffset * resetBoundaryLattice.windowInterval)
}
private nonisolated static func syntheticBoundaryDate(for date: Date, windowMinutes: Int) -> Date {
let bucketSeconds = Double(windowMinutes) * 60
let bucketIndex = floor(date.timeIntervalSince1970 / bucketSeconds)
return Date(timeIntervalSince1970: (bucketIndex + 1) * bucketSeconds)
}
private nonisolated static func xDomain(points: [Point]) -> ClosedRange<Double>? {
guard !points.isEmpty else { return nil }
return -0.5...(Double(Layout.maxPoints) - 0.5)
}
private nonisolated static func axisIndexes(points: [Point], windowMinutes: Int) -> [Double] {
let candidateIndexes = self.axisCandidateIndexes(points: points, windowMinutes: windowMinutes)
return self.proportionalAxisIndexes(points: points, candidateIndexes: candidateIndexes)
}
private nonisolated static func axisCandidateIndexes(points: [Point], windowMinutes: Int) -> [Int] {
if windowMinutes <= 300 {
return self.sessionAxisCandidateIndexes(points: points)
}
return points.map(\.index)
}
private nonisolated static func sessionAxisCandidateIndexes(points: [Point]) -> [Int] {
guard let firstPoint = points.first else { return [] }
let calendar = Calendar.current
var previousPoint = firstPoint
var rawIndexes: [Int] = [firstPoint.index]
for point in points.dropFirst() {
if !calendar.isDate(point.date, inSameDayAs: previousPoint.date) {
rawIndexes.append(point.index)
}
previousPoint = point
}
return rawIndexes
}
private nonisolated static func proportionalAxisIndexes(points: [Point], candidateIndexes: [Int]) -> [Double] {
guard !points.isEmpty, !candidateIndexes.isEmpty else { return [] }
let occupiedFraction = Double(points.count) / Double(Layout.maxPoints)
let proportionalBudget = Int(ceil(Double(Layout.maxAxisLabels) * occupiedFraction))
let labelBudget = max(1, min(Layout.maxAxisLabels, proportionalBudget, candidateIndexes.count))
if labelBudget == 1 {
return [Double(candidateIndexes[0])]
}
let step = Double(candidateIndexes.count - 1) / Double(labelBudget - 1)
var selectedIndexes = (0..<labelBudget).map { position in
let candidateOffset = Int((Double(position) * step).rounded())
return candidateIndexes[candidateOffset]
}
selectedIndexes = Array(NSOrderedSet(array: selectedIndexes)) as? [Int] ?? selectedIndexes
let trailingLabelCutoff = points.first!.index + Int(floor(Double(points.count) * 0.8))
if selectedIndexes.count > 1,
let lastSelectedIndex = selectedIndexes.last,
lastSelectedIndex >= trailingLabelCutoff
{
selectedIndexes.removeLast()
}
if points.count == Layout.maxPoints,
let lastVisibleIndex = points.last?.index,
!selectedIndexes.contains(lastVisibleIndex)
{
selectedIndexes.append(lastVisibleIndex)
}
let deduplicated = Array(NSOrderedSet(array: selectedIndexes)) as? [Int] ?? selectedIndexes
return deduplicated.map(Double.init)
}
@ViewBuilder
private static func axisLabel(
for point: Point,
windowMinutes: Int,
isTrailingFullChartLabel: Bool) -> some View
{
let label = Text(point.date.formatted(self.axisFormat(windowMinutes: windowMinutes)))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
if isTrailingFullChartLabel {
label
.frame(width: 48, alignment: .trailing)
.offset(x: -24)
} else {
label
}
}
private nonisolated static func axisFormat(windowMinutes: Int) -> Date.FormatStyle {
if windowMinutes <= 300 {
return .dateTime.month(.abbreviated).day()
}
return .dateTime.month(.abbreviated).day()
}
private nonisolated static func seriesTitle(
name: PlanUtilizationSeriesName,
metadata: ProviderMetadata?) -> String
{
switch name {
case .session:
L(metadata?.sessionLabel ?? "Session")
case .weekly:
L(metadata?.weeklyLabel ?? "Weekly")
case .opus:
metadata?.opusLabel ?? "Opus"
default:
self.fallbackTitle(for: name.rawValue)
}
}
private nonisolated static func fallbackTitle(for rawValue: String) -> String {
let words = rawValue
.replacingOccurrences(of: "([a-z0-9])([A-Z])", with: "$1 $2", options: .regularExpression)
.split(separator: " ")
return words.map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined(separator: " ")
}
private nonisolated static func seriesSortOrder(_ name: PlanUtilizationSeriesName) -> Int {
switch name {
case .session:
0
case .weekly:
1
case .opus:
2
default:
100
}
}
private nonisolated static func emptyStateText(title: String?) -> String {
if let title {
return String(format: L("No %@ utilization data yet."), title.lowercased())
}
return L("No utilization data yet.")
}
#if DEBUG
struct ModelSnapshot: Equatable {
let pointCount: Int
let axisIndexes: [Double]
let xDomain: ClosedRange<Double>?
let selectedSeries: String?
let visibleSeries: [String]
let usedPercents: [Double]
let pointDates: [String]
}
nonisolated static func _modelSnapshotForTesting(
selectedSeriesRawValue: String? = nil,
histories: [PlanUtilizationSeriesHistory],
provider: UsageProvider,
snapshot: UsageSnapshot? = nil,
referenceDate: Date? = nil) -> ModelSnapshot
{
let visibleSeries = self.visibleSeries(histories: histories, provider: provider, snapshot: snapshot)
let selectedSeries = visibleSeries.first(where: { $0.id == selectedSeriesRawValue }) ?? visibleSeries.first
let model = self.makeModel(
history: selectedSeries?.history,
provider: provider,
referenceDate: referenceDate ?? histories.flatMap(\.entries).map(\.capturedAt).max() ?? Date())
return ModelSnapshot(
pointCount: model.points.count,
axisIndexes: model.axisIndexes,
xDomain: model.xDomain,
selectedSeries: selectedSeries?.id,
visibleSeries: visibleSeries.map(\.id),
usedPercents: model.points.map(\.usedPercent),
pointDates: model.points.map { point in
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone.current
formatter.dateFormat = "yyyy-MM-dd HH:mm"
return formatter.string(from: point.date)
})
}
nonisolated static func _detailLineForTesting(
selectedSeriesRawValue: String? = nil,
histories: [PlanUtilizationSeriesHistory],
provider: UsageProvider,
snapshot: UsageSnapshot? = nil,
referenceDate: Date? = nil) -> String
{
let visibleSeries = self.visibleSeries(histories: histories, provider: provider, snapshot: snapshot)
let selectedSeries = visibleSeries.first(where: { $0.id == selectedSeriesRawValue }) ?? visibleSeries.first
let model = self.makeModel(
history: selectedSeries?.history,
provider: provider,
referenceDate: referenceDate ?? histories.flatMap(\.entries).map(\.capturedAt).max() ?? Date())
return self.detailLine(point: model.points.last, windowMinutes: selectedSeries?.history.windowMinutes ?? 0)
}
nonisolated static func _emptyStateTextForTesting(title: String?) -> String {
self.emptyStateText(title: title)
}
#endif
private func xValue(for index: Int) -> PlottableValue<Double> {
.value(L("Series"), Double(index))
}
@ViewBuilder
private func utilizationChart(model: Model) -> some View {
if let xDomain = model.xDomain {
Chart {
self.utilizationChartContent(model: model)
}
.chartXScale(domain: xDomain)
} else {
Chart {
self.utilizationChartContent(model: model)
}
}
}
@ChartContentBuilder
private func utilizationChartContent(model: Model) -> some ChartContent {
ForEach(model.points) { point in
BarMark(
x: self.xValue(for: point.index),
yStart: .value(L("Capacity Start"), 0),
yEnd: .value(L("Capacity End"), 100),
width: .fixed(Layout.barWidth))
.foregroundStyle(model.trackColor)
BarMark(
x: self.xValue(for: point.index),
yStart: .value(L("Utilization Start"), 0),
yEnd: .value(L("Utilization End"), point.usedPercent),
width: .fixed(Layout.barWidth))
.foregroundStyle(model.barColor)
}
if let selected = self.selectedPoint(model: model) {
RuleMark(x: self.xValue(for: selected.index))
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3]))
}
}
private func selectedPoint(model: Model) -> Point? {
guard let selectedPointID else { return nil }
return model.pointsByID[selectedPointID]
}
private func detailLine(model: Model, windowMinutes: Int) -> String {
let activePoint = self.selectedPoint(model: model) ?? model.points.last
return Self.detailLine(point: activePoint, windowMinutes: windowMinutes)
}
private func updateSelection(
location: CGPoint?,
model: Model,
proxy: ChartProxy,
geo: GeometryProxy)
{
guard let location else {
if self.selectedPointID != nil { self.selectedPointID = nil }
return
}
guard let plotAnchor = proxy.plotFrame else { return }
let plotFrame = geo[plotAnchor]
guard plotFrame.contains(location) else {
if self.selectedPointID != nil { self.selectedPointID = nil }
return
}
let xInPlot = location.x - plotFrame.origin.x
guard let xValue: Double = proxy.value(atX: xInPlot) else { return }
var best: (id: Date, distance: Double)?
for point in model.points {
let distance = abs(Double(point.index) - xValue)
if let current = best {
if distance < current.distance {
best = (point.id, distance)
}
} else {
best = (point.id, distance)
}
}
// Stay on the last selected bar when cursor is in the gap between bars; only switch
// selection when the cursor is over the bar's own visual body.
if let best, let bestPoint = model.pointsByID[best.id],
let barX = proxy.position(forX: Double(bestPoint.index))
{
guard ChartBarHoverSelection.accepts(
distanceFromBarCenter: abs(location.x - (plotFrame.origin.x + barX)),
barHalfWidth: Layout.barWidth / 2,
selectableCount: model.points.count)
else { return }
}
if self.selectedPointID != best?.id {
self.selectedPointID = best?.id
}
}
}
extension PlanUtilizationHistoryChartMenuView {
private nonisolated static func detailLine(point: Point?, windowMinutes: Int) -> String {
guard let point else {
return "-"
}
let dateLabel = self.detailDateLabel(for: point.date, windowMinutes: windowMinutes)
let used = max(0, min(100, point.usedPercent))
if !point.isObserved {
return "\(dateLabel): -"
}
let usedText = used.formatted(.number.precision(.fractionLength(0...1)))
return L("%@: %@%% used", dateLabel, usedText)
}
private nonisolated static func detailDateLabel(for date: Date, windowMinutes: Int) -> String {
let formatter = DateFormatter()
formatter.locale = codexBarLocalizedLocale()
formatter.timeZone = TimeZone.current
formatter.setLocalizedDateFormatFromTemplate("MMM d, h:mm a")
var rendered = formatter.string(from: date).replacingOccurrences(of: "\u{202F}", with: " ")
let amSymbol = formatter.amSymbol ?? ""
let pmSymbol = formatter.pmSymbol ?? ""
if !amSymbol.isEmpty {
rendered = rendered.replacingOccurrences(of: amSymbol, with: amSymbol.lowercased())
}
if !pmSymbol.isEmpty {
rendered = rendered.replacingOccurrences(of: pmSymbol, with: pmSymbol.lowercased())
}
return rendered
}
}
@@ -0,0 +1,361 @@
import CodexBarCore
import Foundation
struct PlanUtilizationSeriesName: RawRepresentable, Hashable, Codable, ExpressibleByStringLiteral, Sendable {
let rawValue: String
init(rawValue: String) {
self.rawValue = rawValue
}
init(stringLiteral value: StringLiteralType) {
self.rawValue = value
}
static let session: Self = "session"
static let weekly: Self = "weekly"
static let opus: Self = "opus"
func canonicalWindowMinutes(_ windowMinutes: Int) -> Int {
switch self {
case .session where (295...305).contains(windowMinutes):
300
case .weekly where (10070...10090).contains(windowMinutes):
10080
default:
windowMinutes
}
}
}
struct PlanUtilizationHistoryEntry: Codable, Equatable, Hashable, Sendable {
let capturedAt: Date
let usedPercent: Double
let resetsAt: Date?
}
struct PlanUtilizationSeriesHistory: Codable, Equatable, Sendable {
let name: PlanUtilizationSeriesName
let windowMinutes: Int
let entries: [PlanUtilizationHistoryEntry]
init(name: PlanUtilizationSeriesName, windowMinutes: Int, entries: [PlanUtilizationHistoryEntry]) {
self.name = name
self.windowMinutes = windowMinutes
self.entries = entries.sorted { lhs, rhs in
if lhs.capturedAt != rhs.capturedAt {
return lhs.capturedAt < rhs.capturedAt
}
if lhs.usedPercent != rhs.usedPercent {
return lhs.usedPercent < rhs.usedPercent
}
let lhsReset = lhs.resetsAt?.timeIntervalSince1970 ?? Date.distantPast.timeIntervalSince1970
let rhsReset = rhs.resetsAt?.timeIntervalSince1970 ?? Date.distantPast.timeIntervalSince1970
return lhsReset < rhsReset
}
}
var latestCapturedAt: Date? {
self.entries.last?.capturedAt
}
}
struct PlanUtilizationHistoryBuckets: Equatable, Sendable {
var preferredAccountKey: String?
var unscoped: [PlanUtilizationSeriesHistory] = []
var accounts: [String: [PlanUtilizationSeriesHistory]] = [:]
func histories(for accountKey: String?) -> [PlanUtilizationSeriesHistory] {
guard let accountKey, !accountKey.isEmpty else { return self.unscoped }
return self.accounts[accountKey] ?? []
}
mutating func setHistories(_ histories: [PlanUtilizationSeriesHistory], for accountKey: String?) {
let sorted = Self.sortedHistories(histories)
guard let accountKey, !accountKey.isEmpty else {
self.unscoped = sorted
return
}
if sorted.isEmpty {
self.accounts.removeValue(forKey: accountKey)
} else {
self.accounts[accountKey] = sorted
}
}
var isEmpty: Bool {
self.unscoped.isEmpty && self.accounts.values.allSatisfy(\.isEmpty)
}
private static func sortedHistories(_ histories: [PlanUtilizationSeriesHistory]) -> [PlanUtilizationSeriesHistory] {
histories.sorted { lhs, rhs in
if lhs.windowMinutes != rhs.windowMinutes {
return lhs.windowMinutes < rhs.windowMinutes
}
return lhs.name.rawValue < rhs.name.rawValue
}
}
}
private struct ProviderHistoryFile: Codable, Sendable {
let preferredAccountKey: String?
let unscoped: [PlanUtilizationSeriesHistory]
let accounts: [String: [PlanUtilizationSeriesHistory]]
}
private struct ProviderHistoryDocument: Codable, Sendable {
let version: Int
let preferredAccountKey: String?
let unscoped: [PlanUtilizationSeriesHistory]
let accounts: [String: [PlanUtilizationSeriesHistory]]
}
struct PlanUtilizationHistoryStore: Sendable {
fileprivate static let providerSchemaVersion = 1
let directoryURL: URL?
init(directoryURL: URL? = Self.defaultDirectoryURL()) {
self.directoryURL = directoryURL
}
static func defaultAppSupport() -> Self {
Self()
}
func load() -> [UsageProvider: PlanUtilizationHistoryBuckets] {
self.loadProviderFiles()
}
/// Loads the persisted histories on a utility-priority detached task.
///
/// The on-disk decode is synchronous I/O + JSON parsing that can take
/// ~150 ms for mature two-year histories and must not run on the app
/// startup main thread. The returned dictionary is safe to apply on the
/// main actor once decoding completes.
func loadAsync() async -> [UsageProvider: PlanUtilizationHistoryBuckets] {
await Task.detached(priority: .utility) { self.load() }.value
}
func save(_ providers: [UsageProvider: PlanUtilizationHistoryBuckets]) {
guard let directoryURL = self.directoryURL else { return }
do {
try FileManager.default.createDirectory(
at: directoryURL,
withIntermediateDirectories: true)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.sortedKeys]
for provider in UsageProvider.allCases {
let fileURL = self.providerFileURL(for: provider)
let buckets = providers[provider] ?? PlanUtilizationHistoryBuckets()
let unscoped = Self.sortedHistories(buckets.unscoped)
let accounts = Self.sortedAccounts(buckets.accounts)
guard !unscoped.isEmpty || !accounts.isEmpty else {
try? FileManager.default.removeItem(at: fileURL)
continue
}
let payload = ProviderHistoryDocument(
version: Self.providerSchemaVersion,
preferredAccountKey: buckets.preferredAccountKey,
unscoped: unscoped,
accounts: accounts)
let data = try encoder.encode(payload)
try data.write(to: fileURL, options: Data.WritingOptions.atomic)
}
} catch {
// Best-effort persistence only.
}
}
private func loadProviderFiles() -> [UsageProvider: PlanUtilizationHistoryBuckets] {
guard self.directoryURL != nil else { return [:] }
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
var output: [UsageProvider: PlanUtilizationHistoryBuckets] = [:]
for provider in UsageProvider.allCases {
let fileURL = self.providerFileURL(for: provider)
guard FileManager.default.fileExists(atPath: fileURL.path) else { continue }
guard let data = try? Data(contentsOf: fileURL),
let decoded = try? decoder.decode(ProviderHistoryDocument.self, from: data)
else {
continue
}
let history = ProviderHistoryFile(
preferredAccountKey: decoded.preferredAccountKey,
unscoped: decoded.unscoped,
accounts: decoded.accounts)
output[provider] = Self.decodeProvider(history)
}
return output
}
private static func decodeProviders(
_ providers: [String: ProviderHistoryFile]) -> [UsageProvider: PlanUtilizationHistoryBuckets]
{
var output: [UsageProvider: PlanUtilizationHistoryBuckets] = [:]
for (rawProvider, providerHistory) in providers {
guard let provider = UsageProvider(rawValue: rawProvider) else { continue }
output[provider] = Self.decodeProvider(providerHistory)
}
return output
}
private static func decodeProvider(_ providerHistory: ProviderHistoryFile) -> PlanUtilizationHistoryBuckets {
PlanUtilizationHistoryBuckets(
preferredAccountKey: providerHistory.preferredAccountKey,
unscoped: self.sortedHistories(providerHistory.unscoped),
accounts: Dictionary(
uniqueKeysWithValues: providerHistory.accounts.compactMap { accountKey, histories in
let sorted = Self.sortedHistories(histories)
guard !sorted.isEmpty else { return nil }
return (accountKey, sorted)
}))
}
private static func sortedAccounts(
_ accounts: [String: [PlanUtilizationSeriesHistory]]) -> [String: [PlanUtilizationSeriesHistory]]
{
Dictionary(
uniqueKeysWithValues: accounts.compactMap { accountKey, histories in
let sorted = Self.sortedHistories(histories)
guard !sorted.isEmpty else { return nil }
return (accountKey, sorted)
})
}
private static func sortedHistories(_ histories: [PlanUtilizationSeriesHistory]) -> [PlanUtilizationSeriesHistory] {
self.sanitizedHistories(histories).sorted { lhs, rhs in
if lhs.windowMinutes != rhs.windowMinutes {
return lhs.windowMinutes < rhs.windowMinutes
}
return lhs.name.rawValue < rhs.name.rawValue
}
}
private static func sanitizedHistories(_ histories: [PlanUtilizationSeriesHistory])
-> [PlanUtilizationSeriesHistory] {
histories.filter { history in
history.windowMinutes > 0 && !history.entries.isEmpty
}
}
private static func defaultDirectoryURL() -> URL? {
guard let root = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
return nil
}
let dir = root.appendingPathComponent("com.steipete.codexbar", isDirectory: true)
return dir.appendingPathComponent("history", isDirectory: true)
}
private func providerFileURL(for provider: UsageProvider) -> URL {
let directoryURL = self.directoryURL ?? URL(fileURLWithPath: "/dev/null", isDirectory: true)
return directoryURL.appendingPathComponent("\(provider.rawValue).json", isDirectory: false)
}
}
extension ProviderHistoryDocument {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(Int.self, forKey: .version)
guard version == PlanUtilizationHistoryStore.providerSchemaVersion else {
throw DecodingError.dataCorruptedError(
forKey: .version,
in: container,
debugDescription: "Unsupported provider history schema version \(version)")
}
self.version = version
self.preferredAccountKey = try container.decodeIfPresent(String.self, forKey: .preferredAccountKey)
self.unscoped = try container.decode([PlanUtilizationSeriesHistory].self, forKey: .unscoped)
self.accounts = try container.decode([String: [PlanUtilizationSeriesHistory]].self, forKey: .accounts)
}
}
/// One-shot synchronization primitive used by `UsageStore.init` to defer the
/// utility-priority plan-utilization history load until a test chooses to
/// release it. The default `nil` gate is open and the load proceeds immediately.
///
/// Used to verify that `UsageStore.init` returns before disk I/O completes and
/// that the history is applied exactly once after the gate opens.
final class PlanUtilizationHistoryLoadGate: @unchecked Sendable {
private enum State {
case closed
case open
case cancelled
}
private let lock = NSLock()
private var continuations: [CheckedContinuation<Bool, Never>] = []
private var state: State = .closed
init() {}
var isOpen: Bool {
self.lock.lock()
defer { self.lock.unlock() }
return self.state == .open
}
var isCancelled: Bool {
self.lock.lock()
defer { self.lock.unlock() }
return self.state == .cancelled
}
func wait() async -> Bool {
await withCheckedContinuation { continuation in
self.lock.lock()
switch self.state {
case .open:
self.lock.unlock()
continuation.resume(returning: true)
case .cancelled:
self.lock.unlock()
continuation.resume(returning: false)
case .closed:
self.continuations.append(continuation)
self.lock.unlock()
}
}
}
func open() {
self.lock.lock()
guard self.state == .closed else {
self.lock.unlock()
return
}
self.state = .open
let pending = self.continuations
self.continuations.removeAll()
self.lock.unlock()
for continuation in pending {
continuation.resume(returning: true)
}
}
/// Cancels this one-shot gate and resumes pending or future waiters with
/// `false`. Cancellation is sticky so it cannot race ahead of `wait()` and
/// lose the wakeup that drains the load task.
func cancel() {
self.lock.lock()
guard self.state == .closed else {
self.lock.unlock()
return
}
self.state = .cancelled
let pending = self.continuations
self.continuations.removeAll()
self.lock.unlock()
for continuation in pending {
continuation.resume(returning: false)
}
}
}
@@ -0,0 +1,284 @@
import CodexBarCore
import Foundation
struct PredictivePaceWarningStateKey: Hashable {
let provider: UsageProvider
let accountDiscriminator: String
let window: QuotaWarningWindow
let resetWindow: PredictivePaceWarningResetWindow
}
struct PredictivePaceWarningResetWindow: Hashable {
let windowMinutes: Int?
let resetsAt: Date
func belongsToSameCycle(as other: Self) -> Bool {
guard self.windowMinutes == other.windowMinutes else { return false }
let tolerance = self.windowMinutes.map { max(TimeInterval($0 * 60) / 2, 300) } ?? 300
return abs(self.resetsAt.timeIntervalSince(other.resetsAt)) < tolerance
}
}
struct PredictivePaceWarningEvent: Equatable {
let window: QuotaWarningWindow
let etaSeconds: TimeInterval
let accountDisplayName: String?
}
enum PredictivePaceWarningNotificationLogic {
static func notificationIDPrefix(provider: UsageProvider, event: PredictivePaceWarningEvent) -> String {
"predictive-pace-warning-\(provider.rawValue)-\(event.window.rawValue)"
}
static func notificationCopy(
providerName: String,
event: PredictivePaceWarningEvent,
now: Date = .init()) -> (title: String, body: String)
{
let windowLabel = event.window.localizedNotificationDisplayName
let title = L("predictive_pace_warning_notification_title", providerName, windowLabel)
let durationText = Self.durationText(seconds: event.etaSeconds, now: now)
let body = if let accountDisplayName = event.accountDisplayName {
L("predictive_pace_warning_notification_body_with_account", accountDisplayName, durationText)
} else {
L("predictive_pace_warning_notification_body", durationText)
}
return (title, body)
}
static func shouldNotify(pace: UsagePace) -> Bool {
guard !pace.willLastToReset else { return false }
guard let etaSeconds = pace.etaSeconds, etaSeconds > 0 else { return false }
guard (pace.runOutProbability ?? 1) >= 0.5 else { return false }
return true
}
static func recordObservation(
key: PredictivePaceWarningStateKey,
pace: UsagePace,
notifiedKeys: inout Set<PredictivePaceWarningStateKey>) -> Bool
{
if pace.willLastToReset {
notifiedKeys.remove(key)
return false
}
guard self.shouldNotify(pace: pace) else { return false }
guard !notifiedKeys.contains(key) else { return false }
notifiedKeys.insert(key)
return true
}
static func reconcileSiblingWindowKeys(
activeKey: PredictivePaceWarningStateKey,
notifiedKeys: inout Set<PredictivePaceWarningStateKey>)
{
let siblingKeys = notifiedKeys.filter { key in
key.provider == activeKey.provider &&
key.accountDiscriminator == activeKey.accountDiscriminator &&
key.window == activeKey.window
}
guard !siblingKeys.isEmpty else { return }
let alreadyWarnedThisCycle = siblingKeys.contains { key in
key.resetWindow.belongsToSameCycle(as: activeKey.resetWindow)
}
notifiedKeys.subtract(siblingKeys)
if alreadyWarnedThisCycle {
// Follow small provider reset-time corrections without re-alerting. Replacing the key
// lets successive relative-TTL observations move together instead of accumulating drift.
notifiedKeys.insert(activeKey)
}
}
private static func durationText(seconds: TimeInterval, now: Date) -> String {
let countdown = UsageFormatter.resetCountdownDescription(from: now.addingTimeInterval(seconds), now: now)
if countdown.hasPrefix("in ") {
return String(countdown.dropFirst(3))
}
return countdown
}
}
@MainActor
extension UsageStore {
func handlePredictivePaceWarningTransitions(
provider: UsageProvider,
snapshot: UsageSnapshot,
accountDiscriminatorOverride: String? = nil)
{
guard self.settings.predictivePaceWarningNotificationsEnabled else {
self.predictivePaceWarningNotifiedKeys = Set(
self.predictivePaceWarningNotifiedKeys.filter { $0.provider != provider })
return
}
guard provider == .codex || provider == .claude else { return }
guard let accountDiscriminator = self.predictivePaceWarningAccountDiscriminator(
provider: provider,
snapshot: snapshot,
accountDiscriminatorOverride: accountDiscriminatorOverride)
else { return }
let candidates = self.predictivePaceWarningCandidates(provider: provider, snapshot: snapshot)
for candidate in candidates {
guard let resetWindow = Self.predictivePaceWarningResetWindow(for: candidate.rateWindow) else {
continue
}
let key = PredictivePaceWarningStateKey(
provider: provider,
accountDiscriminator: accountDiscriminator,
window: candidate.window,
resetWindow: resetWindow)
PredictivePaceWarningNotificationLogic.reconcileSiblingWindowKeys(
activeKey: key,
notifiedKeys: &self.predictivePaceWarningNotifiedKeys)
guard PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: candidate.pace,
notifiedKeys: &self.predictivePaceWarningNotifiedKeys)
else { continue }
self.postPredictivePaceWarning(
PredictivePaceWarningEvent(
window: candidate.window,
etaSeconds: candidate.pace.etaSeconds ?? 0,
accountDisplayName: self.predictivePaceWarningAccountDisplayName(
provider: provider,
snapshot: snapshot)),
provider: provider,
now: snapshot.updatedAt)
}
}
private func predictivePaceWarningCandidates(
provider: UsageProvider,
snapshot: UsageSnapshot) -> [(window: QuotaWarningWindow, rateWindow: RateWindow, pace: UsagePace)]
{
var candidates: [(window: QuotaWarningWindow, rateWindow: RateWindow, pace: UsagePace)] = []
let now = snapshot.updatedAt
if let sessionWindow = self.predictivePaceWarningSessionWindow(provider: provider, snapshot: snapshot),
!sessionWindow.isSyntheticPlaceholder,
let sessionPace = UsagePaceText.sessionPace(provider: provider, window: sessionWindow, now: now)
{
candidates.append((window: .session, rateWindow: sessionWindow, pace: sessionPace))
}
if let weeklyWindow = self.predictivePaceWarningWeeklyWindow(provider: provider, snapshot: snapshot),
let weeklyPace = self.weeklyPace(provider: provider, window: weeklyWindow, now: now)
{
candidates.append((window: .weekly, rateWindow: weeklyWindow, pace: weeklyPace))
}
return candidates
}
private func predictivePaceWarningSessionWindow(provider: UsageProvider, snapshot: UsageSnapshot) -> RateWindow? {
if provider == .codex {
return self.codexConsumerProjection(
surface: .liveCard,
snapshotOverride: snapshot,
now: snapshot.updatedAt)
.sourceRateWindow(for: .session)
}
return self.sessionQuotaWindow(provider: provider, snapshot: snapshot)?.window
}
private func predictivePaceWarningWeeklyWindow(provider: UsageProvider, snapshot: UsageSnapshot) -> RateWindow? {
if provider == .codex {
return self.codexConsumerProjection(
surface: .liveCard,
snapshotOverride: snapshot,
now: snapshot.updatedAt)
.sourceRateWindow(for: .weekly)
}
return snapshot.secondary
}
private func predictivePaceWarningAccountDiscriminator(
provider: UsageProvider,
snapshot: UsageSnapshot,
accountDiscriminatorOverride: String? = nil) -> String?
{
if provider == .codex {
return self.codexOwnershipContext(
preferredEmail: snapshot.accountEmail(for: .codex),
snapshot: snapshot)
.canonicalKey
}
if let accountDiscriminatorOverride = accountDiscriminatorOverride?
.trimmingCharacters(in: .whitespacesAndNewlines),
!accountDiscriminatorOverride.isEmpty
{
return accountDiscriminatorOverride
}
guard let account = snapshot.accountEmail(for: provider)?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!account.isEmpty
else { return nil }
return "email:\(account)"
}
static func predictivePaceWarningClaudeAccountDiscriminator(
strategyKind: ProviderFetchKind,
observation: ClaudeOAuthActiveAccountObservation,
oauthHistoryOwnerIdentifier: String? = nil) -> String?
{
switch strategyKind {
case .cli:
return self.predictivePaceWarningClaudeActiveAccountDiscriminator(observation: observation)
case .oauth:
if let activeAccount = self.predictivePaceWarningClaudeActiveAccountDiscriminator(
observation: observation)
{
return activeAccount
}
guard let owner = oauthHistoryOwnerIdentifier?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!owner.isEmpty
else { return nil }
// OAuth usage has no email. Keep a credential-scoped fallback so predictive warnings still work
// when Claude's active-account metadata is unavailable, without merging unrelated accounts.
return "claude-oauth-owner:\(owner)"
case .apiToken, .localProbe, .web, .webDashboard:
return nil
}
}
private static func predictivePaceWarningClaudeActiveAccountDiscriminator(
observation: ClaudeOAuthActiveAccountObservation) -> String?
{
guard case let .stable(identity) = observation,
let identity = identity?.trimmingCharacters(in: .whitespacesAndNewlines),
!identity.isEmpty
else { return nil }
return "claude-account:\(identity)"
}
static func predictivePaceWarningTokenAccountDiscriminator(_ account: ProviderTokenAccount?) -> String? {
guard let account else { return nil }
return "token-account:\(account.id.uuidString.lowercased())"
}
private func predictivePaceWarningAccountDisplayName(provider: UsageProvider, snapshot: UsageSnapshot) -> String? {
guard !self.settings.hidePersonalInfo else { return nil }
let account = snapshot.accountEmail(for: provider)?
.trimmingCharacters(in: .whitespacesAndNewlines)
guard let account, !account.isEmpty else { return nil }
return account
}
private static func predictivePaceWarningResetWindow(for window: RateWindow)
-> PredictivePaceWarningResetWindow?
{
guard let resetsAt = window.resetsAt else { return nil }
return PredictivePaceWarningResetWindow(
windowMinutes: window.windowMinutes,
resetsAt: resetsAt)
}
}
+181
View File
@@ -0,0 +1,181 @@
import AppKit
import SwiftUI
@MainActor
struct AboutPane: View {
let updater: UpdaterProviding
@State private var iconHover = false
@AppStorage("autoUpdateEnabled") private var autoUpdateEnabled: Bool = true
@AppStorage(UpdateChannel.userDefaultsKey)
private var updateChannelRaw: String = UpdateChannel.defaultChannel.rawValue
@State private var didLoadUpdaterState = false
private var versionString: String {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
return build.map { "\(version) (\($0))" } ?? version
}
private var buildTimestamp: String? {
guard let raw = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String else { return nil }
let parser = ISO8601DateFormatter()
parser.formatOptions = [.withInternetDateTime]
guard let date = parser.date(from: raw) else { return raw }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = .current
return formatter.string(from: date)
}
var body: some View {
Form {
Section {
self.hero
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)
}
if self.updater.isAvailable {
Section {
Toggle(L("check_updates_auto"), isOn: self.$autoUpdateEnabled)
Picker(selection: self.updateChannelBinding) {
ForEach(UpdateChannel.allCases) { channel in
Text(channel.displayName).tag(channel)
}
} label: {
SettingsRowLabel(L("update_channel"), subtitle: self.updateChannel.description)
}
LabeledContent(String(format: L("version_format"), self.versionString)) {
Button(L("check_for_updates")) { self.updater.checkForUpdates(nil) }
}
} header: {
Text(L("section_updates"))
}
} else {
Section {
Text(self.updater.unavailableReason ?? L("updates_unavailable"))
.foregroundStyle(.secondary)
}
}
Section {
AboutLinkRow(
icon: "chevron.left.slash.chevron.right",
title: L("link_github"),
url: "https://github.com/steipete/CodexBar")
AboutLinkRow(icon: "globe", title: L("link_website"), url: "https://steipete.me")
AboutLinkRow(icon: "bird", title: L("link_twitter"), url: "https://twitter.com/steipete")
AboutLinkRow(icon: "envelope", title: L("link_email"), url: "mailto:peter@steipete.me")
} header: {
Text(L("section_links"))
} footer: {
Text(L("copyright"))
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
}
}
.formStyle(.grouped)
.toggleStyle(.switch)
.scrollContentBackground(.hidden)
.onAppear {
guard !self.didLoadUpdaterState else { return }
// Align Sparkle's flag with the persisted preference on first load.
self.updater.automaticallyChecksForUpdates = self.autoUpdateEnabled
self.updater.automaticallyDownloadsUpdates = self.autoUpdateEnabled
self.didLoadUpdaterState = true
}
.onChange(of: self.autoUpdateEnabled) { _, newValue in
self.updater.automaticallyChecksForUpdates = newValue
self.updater.automaticallyDownloadsUpdates = newValue
}
}
private var hero: some View {
VStack(spacing: 10) {
if let image = NSApplication.shared.applicationIconImage {
Button(action: self.openProjectHome) {
Image(nsImage: image)
.resizable()
.frame(width: 92, height: 92)
.cornerRadius(16)
.scaleEffect(self.iconHover ? 1.05 : 1.0)
.shadow(color: self.iconHover ? .accentColor.opacity(0.25) : .clear, radius: 6)
}
.buttonStyle(.plain)
.focusEffectDisabled()
.onHover { hovering in
withAnimation(.spring(response: 0.32, dampingFraction: 0.78)) {
self.iconHover = hovering
}
}
}
VStack(spacing: 2) {
Text("CodexBar")
.font(.title3).bold()
Text(String(format: L("version_format"), self.versionString))
.foregroundStyle(.secondary)
if let buildTimestamp {
Text(String(format: L("built_format"), buildTimestamp))
.font(.footnote)
.foregroundStyle(.secondary)
}
Text(L("about_tagline"))
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 6)
}
private var updateChannel: UpdateChannel {
UpdateChannel(rawValue: self.updateChannelRaw) ?? .stable
}
private var updateChannelBinding: Binding<UpdateChannel> {
Binding(
get: { self.updateChannel },
set: { newValue in
self.updateChannelRaw = newValue.rawValue
self.updater.checkForUpdates(nil)
})
}
private func openProjectHome() {
guard let url = URL(string: "https://github.com/steipete/CodexBar") else { return }
NSWorkspace.shared.open(url)
}
}
@MainActor
struct AboutLinkRow: View {
let icon: String
let title: String
let url: String
@State private var hovering = false
var body: some View {
Button {
if let url = URL(string: self.url) { NSWorkspace.shared.open(url) }
} label: {
HStack(spacing: 8) {
Image(systemName: self.icon)
.frame(width: 18)
.foregroundStyle(.secondary)
Text(self.title)
.foregroundStyle(.primary)
Spacer()
Image(systemName: "arrow.up.right")
.font(.caption)
.foregroundStyle(self.hovering ? Color.accentColor : Color.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.onHover { self.hovering = $0 }
}
}

Some files were not shown because too many files have changed in this diff Show More