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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ concurrency:
jobs:
test:
name: Swift tests
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 10
steps:
- name: Check out repository
Expand Down
12 changes: 12 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,15 @@ _Avoid_: Worst case, guaranteed minimum
**Burn-down chart**:
A chart for the current usage window with time and remaining-budget axes. It shows the observed usage curve, a straight path from 100% to the safety buffer, the current projection, the historical projection, and the current point; earlier windows inform the historical projection but are not drawn individually.
_Avoid_: Activity chart, task history

**Usage sample**:
A timestamped observation of the main limit's remaining budget and window reset. Its observation time is recorded locally; its remaining budget and window reset come from Codex.
_Avoid_: Token bucket, activity event

**Shared usage history**:
The combined usage samples recorded by Macs connected through the same optional sync folder. It excludes preferences, launch settings, credentials, and raw Codex responses.
_Avoid_: Shared app state, cloud backup

**Sync folder**:
An optional user-selected folder that connects the shared usage history of Macs signed in to the same Codex account. One sync folder represents one Codex account; the app does not identify or verify that account.
_Avoid_: Account store, settings sync
14 changes: 7 additions & 7 deletions Sources/CodexLimits/ForecastEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ enum ForecastEngine {
) -> Forecast {
let daysLeft = max(window.resetsAt.timeIntervalSince(now) / 86_400, 0)
let currentSamples = samples
.filter { $0.resetsAt == window.resetsAt && $0.date <= now }
.sorted { $0.date < $1.date }
.filter { $0.resetsAt == window.resetsAt && $0.observedAt <= now }
.sorted { $0.observedAt < $1.observedAt }
let elapsedDays = max(now.timeIntervalSince(window.startsAt) / 86_400, 1 / 24)
let windowRate = max((100 - window.remainingPercent) / elapsedDays, 0)
let recentRate: Double

if let first = currentSamples.first,
let last = currentSamples.last,
last.date > first.date {
let days = last.date.timeIntervalSince(first.date) / 86_400
last.observedAt > first.observedAt {
let days = last.observedAt.timeIntervalSince(first.observedAt) / 86_400
recentRate = max((first.remainingPercent - last.remainingPercent) / days, 0)
} else {
recentRate = windowRate
Expand All @@ -32,11 +32,11 @@ enum ForecastEngine {
let historicalRates = Dictionary(grouping: samples.filter { $0.resetsAt != window.resetsAt }) {
$0.resetsAt
}.values.compactMap { windowSamples -> Double? in
let ordered = windowSamples.sorted { $0.date < $1.date }
let ordered = windowSamples.sorted { $0.observedAt < $1.observedAt }
guard let first = ordered.first,
let last = ordered.last,
last.date > first.date else { return nil }
let days = last.date.timeIntervalSince(first.date) / 86_400
last.observedAt > first.observedAt else { return nil }
let days = last.observedAt.timeIntervalSince(first.observedAt) / 86_400
return max((first.remainingPercent - last.remainingPercent) / days, 0)
}
let historicalRate: Double
Expand Down
54 changes: 50 additions & 4 deletions Sources/CodexLimits/MenuContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SwiftUI
struct MenuContentView: View {
@ObservedObject var monitor: UsageMonitor
@AppStorage(UsageMonitor.safetyBufferKey) private var safetyBuffer = 3.0
@Environment(\.openSettings) private var openSettings

var body: some View {
Group {
Expand Down Expand Up @@ -111,7 +112,14 @@ struct MenuContentView: View {
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
SettingsLink {
Button {
openSettings()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NSApp.windows.first {
$0.isVisible && $0.styleMask.contains(.titled)
}?.orderFrontRegardless()
}
} label: {
Image(systemName: "gearshape")
}
.buttonStyle(.borderless)
Expand Down Expand Up @@ -227,8 +235,8 @@ private struct BurnDownChart: View {
private var observed: [BurnPoint] {
let current = BurnPoint(date: fetchedAt, remaining: window.remainingPercent)
let local = samples
.filter { $0.date > window.startsAt && $0.date < fetchedAt }
.map { BurnPoint(date: $0.date, remaining: $0.remainingPercent) }
.filter { $0.observedAt > window.startsAt && $0.observedAt < fetchedAt }
.map { BurnPoint(date: $0.observedAt, remaining: $0.remainingPercent) }
.sorted { $0.date < $1.date }
let firstKnown = local.first ?? current
let buckets = tokenHistory
Expand Down Expand Up @@ -492,10 +500,37 @@ struct SettingsView: View {
.font(.caption)
.foregroundStyle(.secondary)
}

Section("History sync") {
Text("Keep usage history in a folder available on your other Macs.")
.foregroundStyle(.secondary)

if let folderName = monitor.syncFolderName {
LabeledContent("Folder", value: folderName)
Button("Stop Syncing") {
Task { await monitor.stopHistorySync() }
}
} else {
Button("Choose Folder…", action: chooseHistoryFolder)
}

Text("Use this folder only on Macs signed in to the same Codex account.")
.font(.caption)
.foregroundStyle(.secondary)
Text("Choose a private folder that isn’t shared with other people.")
.font(.caption)
.foregroundStyle(.secondary)

if let syncErrorMessage = monitor.syncErrorMessage {
Label(syncErrorMessage, systemImage: "exclamationmark.triangle")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.formStyle(.grouped)
.padding()
.frame(width: 320)
.frame(width: 380)
}

private func updateLaunchAtLogin(_ enabled: Bool) {
Expand All @@ -512,6 +547,17 @@ struct SettingsView: View {
loginItemError = "Couldn’t update the login setting."
}
}

private func chooseHistoryFolder() {
let panel = NSOpenPanel()
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.allowsMultipleSelection = false
panel.canCreateDirectories = true
panel.prompt = "Choose"
guard panel.runModal() == .OK, let directory = panel.url else { return }
Task { await monitor.connectHistoryFolder(directory) }
}
}

enum LoginItem {
Expand Down
Loading