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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.15</string>
<string>0.1.16</string>
<key>CFBundleVersion</key>
<string>15</string>
<string>16</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexToolbar/App/CodexToolbarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ enum AppVersion {
return version
}

return "0.1.15"
return "0.1.16"
}()

private static func developmentVersionFromSourceInfoPlist() -> String? {
Expand Down
63 changes: 62 additions & 1 deletion Sources/CodexToolbar/Models/CodexRateLimits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ enum RateLimitProgressState: Equatable, Sendable {
}
}

enum RateLimitProjectionGraphElement: Equatable, Sendable {
case pace
case current
case currentAndProjectedEmpty
case reset
case projectedEmpty
}

struct RateLimitProjectionViewData: Equatable, Sendable {
enum State: Equatable, Sendable {
case resetFirst
Expand All @@ -213,6 +221,10 @@ struct RateLimitProjectionViewData: Equatable, Sendable {
let projectedEmptyPosition: Double
let summaryText: String
let detailText: String
let paceTooltipText: String
let currentTooltipText: String
let resetTooltipText: String
let projectedEmptyTooltipText: String
let accessibilityLabel: String

init?(
Expand Down Expand Up @@ -278,6 +290,18 @@ struct RateLimitProjectionViewData: Equatable, Sendable {
locale: locale,
timeZone: timeZone
)
let resetText = RateLimitFormatter.absoluteResetText(
for: resetDate,
now: now,
calendar: calendar,
locale: locale,
timeZone: timeZone
)
let paceTooltipText = Self.paceTooltipText(
dailyUsePercent: dailyUsePercent,
windowDurationMins: windowDurationMins,
locale: locale
)
let summaryText = runsOutBeforeReset ? "Projected empty before reset" : "Reset comes first"
let detailText = runsOutBeforeReset
? "Empty \(projectedText) at pace"
Expand All @@ -295,7 +319,26 @@ struct RateLimitProjectionViewData: Equatable, Sendable {
projectedEmptyPosition = Self.clampedPosition(for: projectedEmptyDate, start: windowStartDate, durationSeconds: chartDurationSeconds)
self.summaryText = summaryText
self.detailText = detailText
accessibilityLabel = "\(windowLabel) pace. \(summaryText). \(detailText). \(remainingPercent)% remaining."
self.paceTooltipText = paceTooltipText
currentTooltipText = "Now · \(remainingPercent)% remaining"
resetTooltipText = "Reset · \(resetText)"
projectedEmptyTooltipText = "Projected empty · \(projectedText) at current pace"
accessibilityLabel = "\(windowLabel) pace. \(paceTooltipText). \(summaryText). \(detailText). \(remainingPercent)% remaining."
}

func tooltipText(for element: RateLimitProjectionGraphElement) -> String {
switch element {
case .pace:
return paceTooltipText
case .current:
return currentTooltipText
case .currentAndProjectedEmpty:
return "Now · \(currentRemainingPercent)% remaining · projected empty"
case .reset:
return resetTooltipText
case .projectedEmpty:
return projectedEmptyTooltipText
}
}

private static func clampedPosition(for date: Date, start: Date, durationSeconds: TimeInterval) -> Double {
Expand All @@ -305,6 +348,23 @@ struct RateLimitProjectionViewData: Equatable, Sendable {

return min(max(date.timeIntervalSince(start) / durationSeconds, 0), 1)
}

private static func paceTooltipText(
dailyUsePercent: Double,
windowDurationMins: Int,
locale: Locale
) -> String {
let usesHourlyPace = windowDurationMins < 1_440
let value = usesHourlyPace ? dailyUsePercent / 24 : dailyUsePercent
let formatter = NumberFormatter()
formatter.locale = locale
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = value < 1 ? 2 : 1
let formattedValue = formatter.string(from: NSNumber(value: value)) ?? String(Int(value.rounded()))
let unit = usesHourlyPace ? "hour" : "day"
return "Current pace · \(formattedValue)% used per \(unit)"
}
}

struct RateLimitCardViewData: Equatable, Sendable {
Expand Down Expand Up @@ -338,6 +398,7 @@ struct RateLimitCardViewData: Equatable, Sendable {
}

if let projection {
parts.append(projection.paceTooltipText)
parts.append(projection.summaryText)
parts.append(projection.detailText)
}
Expand Down
23 changes: 14 additions & 9 deletions Sources/CodexToolbar/Networking/CodexAppServerClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -381,26 +381,31 @@ actor CodexAppServerClient: CodexRateLimitClient {
}

nonisolated static func codexPathCandidates(environmentPath: String, homeDirectory: String) -> [String] {
let preferredAppCandidates = [
"/Applications/ChatGPT.app/Contents/Resources/codex",
URL(fileURLWithPath: homeDirectory).appendingPathComponent("Applications/ChatGPT.app/Contents/Resources/codex").path,
"/Applications/Codex.app/Contents/Resources/codex",
URL(fileURLWithPath: homeDirectory).appendingPathComponent("Applications/Codex.app/Contents/Resources/codex").path
]

let pathCandidates = environmentPath.split(separator: ":").map { pathComponent in
URL(fileURLWithPath: String(pathComponent)).appendingPathComponent("codex").path
}

let fallbackCandidates = [
let installedCLICandidates = [
URL(fileURLWithPath: homeDirectory).appendingPathComponent(".local/bin/codex").path,
"/opt/homebrew/bin/codex",
"/usr/local/bin/codex",
"/usr/bin/codex"
]

// Prefer a user-installed CLI. Executables inside another app's bundle are
// private implementation details and may be replaced or quarantined when
// that app updates, so only use them when no standalone CLI is available.
let appBundleCandidates = [
"/Applications/ChatGPT.app/Contents/Resources/codex",
URL(fileURLWithPath: homeDirectory).appendingPathComponent("Applications/ChatGPT.app/Contents/Resources/codex").path,
"/Applications/Codex.app/Contents/Resources/codex",
URL(fileURLWithPath: homeDirectory).appendingPathComponent("Applications/Codex.app/Contents/Resources/codex").path
]
let appBundleCandidateSet = Set(appBundleCandidates)
let standalonePathCandidates = pathCandidates.filter { !appBundleCandidateSet.contains($0) }

var seen = Set<String>()
let orderedCandidates = preferredAppCandidates + pathCandidates + fallbackCandidates
let orderedCandidates = standalonePathCandidates + installedCLICandidates + appBundleCandidates

return orderedCandidates.filter { candidate in
seen.insert(candidate).inserted
Expand Down
10 changes: 7 additions & 3 deletions Sources/CodexToolbar/Support/RateLimitStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,8 @@ final class RateLimitStore {
window: entry.window,
displayLabelOverride: displayLabelOverride(
role: entry.role,
limitId: entry.limitId
limitId: entry.limitId,
windowDurationMins: entry.window.windowDurationMins
),
familyId: entry.limitId,
categoryLabel: entry.categoryLabel,
Expand Down Expand Up @@ -577,9 +578,12 @@ final class RateLimitStore {

private static func displayLabelOverride(
role: WindowRole,
limitId: String?
limitId: String?,
windowDurationMins: Int?
) -> String? {
guard limitId == GetAccountRateLimitsResponse.codexLimitId else {
guard limitId == GetAccountRateLimitsResponse.codexLimitId,
windowDurationMins == nil
else {
return nil
}

Expand Down
Loading