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 @@ -13,9 +13,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.2</string>
<string>0.2.4</string>
<key>CFBundleVersion</key>
<string>4</string>
<string>5</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
Expand Down
26 changes: 23 additions & 3 deletions Sources/CodexLimits/ActiveTimeAvailability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ struct ActiveTimeAvailableEstimate: Equatable, Sendable {

struct ActiveTimeAvailabilitySnapshot: Equatable, Sendable {
let activeTimeThisWeek: TimeInterval
let maximumConcurrency: Int
let waitingTime: TimeInterval?
let pollingTime: TimeInterval?
let activityBreakdownReason: String?
let activeTimeCoverage: CoverageLevel
let activeTimeReason: String?
let observedInterval: DateInterval?
Expand All @@ -54,15 +58,16 @@ enum ActiveTimeWeekEvidenceBuilder {
usage: [WeeklyUsageEvidence],
facts: [LocalActivityFact],
projections: [ThreadProjection],
observation: LocalActivityObservation
observation: LocalActivityObservation,
factIndex: LocalActivityFactIndex? = nil
) -> ActiveTimeHistorySelection {
guard let currentUsage else {
return ActiveTimeHistorySelection(
evidence: [],
unavailableReason: nil
)
}
let factIndex = LocalActivityFactIndex(facts)
let factIndex = factIndex ?? LocalActivityFactIndex(facts)
var result: [ActiveTimeWeekEvidence] = []
var workloadMismatchCount = 0
for candidate in usage.sorted(
Expand All @@ -80,7 +85,10 @@ enum ActiveTimeWeekEvidenceBuilder {
continue
}
let timeline = ActivityTimelineAggregator.evaluate(
facts: factIndex.activityFacts(in: candidate.interval),
facts: factIndex.activityFacts(
in: candidate.interval,
from: facts
),
projections: projections,
interval: candidate.interval,
observation: scopedObservation(
Expand Down Expand Up @@ -168,6 +176,10 @@ enum ActiveTimeAvailabilityEngine {
static func evaluate(
currentUsage: WeeklyUsageEvidence?,
activeTimeThisWeek: TimeInterval,
maximumConcurrency: Int = 0,
waitingTime: TimeInterval? = nil,
pollingTime: TimeInterval? = nil,
activityBreakdownReason: String? = nil,
activeTimeCoverage: CoverageLevel,
activeTimeReason: String?,
history: [ActiveTimeWeekEvidence],
Expand All @@ -177,6 +189,10 @@ enum ActiveTimeAvailabilityEngine {
let unavailable: (String) -> ActiveTimeAvailabilitySnapshot = {
ActiveTimeAvailabilitySnapshot(
activeTimeThisWeek: activeTimeThisWeek,
maximumConcurrency: maximumConcurrency,
waitingTime: waitingTime,
pollingTime: pollingTime,
activityBreakdownReason: activityBreakdownReason,
activeTimeCoverage: activeTimeCoverage,
activeTimeReason: activeTimeReason,
observedInterval: currentUsage?.interval,
Expand Down Expand Up @@ -272,6 +288,10 @@ enum ActiveTimeAvailabilityEngine {
let confidence: ConfidenceLevel = caveats.isEmpty ? .high : .medium
return ActiveTimeAvailabilitySnapshot(
activeTimeThisWeek: activeTimeThisWeek,
maximumConcurrency: maximumConcurrency,
waitingTime: waitingTime,
pollingTime: pollingTime,
activityBreakdownReason: activityBreakdownReason,
activeTimeCoverage: activeTimeCoverage,
activeTimeReason: activeTimeReason,
observedInterval: currentUsage.interval,
Expand Down
50 changes: 33 additions & 17 deletions Sources/CodexLimits/ActivityTimeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ struct ActivityTimelineSnapshot: Equatable, Sendable {
fileprivate let observation: LocalActivityObservation
let interval: DateInterval

func updating(
interval: DateInterval,
observation: LocalActivityObservation
) -> ActivityTimelineSnapshot {
ActivityTimelineSnapshot(
turns: turns,
boundaryIssues: boundaryIssues,
observation: observation,
interval: interval
)
}

func slice(
in selectedInterval: DateInterval,
filters: WorkspaceFilters
Expand Down Expand Up @@ -304,27 +316,31 @@ enum ActivityTimelineAggregator {
start: start,
end: end
)
pendingIssues.append(
(
turnKey,
ActivityTimelineSnapshot.BoundaryIssue(
date: issueDate,
affectedStart: affectedBounds.start,
affectedEnd: affectedBounds.end,
reason: root == nil && taskID != nil
? "Task Tree metadata is missing"
: "Some Active Turn boundaries are missing or invalid",
rootTaskID: root,
projectLabel: root.flatMap {
projectionsByTask[$0]?.projectLabel
},
context: context
)
)
let issue = ActivityTimelineSnapshot.BoundaryIssue(
date: issueDate,
affectedStart: affectedBounds.start,
affectedEnd: affectedBounds.end,
reason: root == nil && taskID != nil
? "Task Tree metadata is missing"
: "Some Active Turn boundaries are missing or invalid",
rootTaskID: root,
projectLabel: root.flatMap {
projectionsByTask[$0]?.projectLabel
},
context: context
)
if issue.affects(
DateInterval(
start: interval.start,
end: .distantFuture
)
) {
pendingIssues.append((turnKey, issue))
}
}
continue
}
guard end > interval.start else { continue }
turns.append(
ActivityTimelineSnapshot.TurnInterval(
start: start,
Expand Down
144 changes: 139 additions & 5 deletions Sources/CodexLimits/AnalyticsWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,19 @@ enum AnalyticsGraph: String, CaseIterable, Codable, Identifiable, Sendable {

var id: String { rawValue }

static let coreCases: [AnalyticsGraph] = [
.usageRemaining,
.tokenActivity
]

var usesAccountScope: Bool {
self == .usageRemaining || self == .usagePerToken
self == .usageRemaining
|| self == .tokenActivity
|| self == .usagePerToken
}

var usesLocalAnalytics: Bool {
self == .usagePerToken || self == .concurrency
}
}

Expand Down Expand Up @@ -62,6 +73,57 @@ enum AnalyticsTimeRange: String, CaseIterable, Codable, Identifiable, Sendable {
}
}

struct AccountTokenActivityRange: Equatable, Sendable {
let days: [TokenDay]
let completeDayCount: Int
let completeTokens: Int64?

init(days: [TokenDay], interval: DateInterval) {
let day: TimeInterval = 86_400
self.days = days
.filter {
$0.date < interval.end
&& $0.date.addingTimeInterval(day) > interval.start
}
.sorted { $0.date < $1.date }

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)
?? calendar.timeZone
let startOfDay = calendar.startOfDay(for: interval.start)
var expected = startOfDay < interval.start
? startOfDay.addingTimeInterval(day)
: startOfDay
var expectedDates: [Date] = []
while expected.addingTimeInterval(day) <= interval.end {
expectedDates.append(expected)
expected = expected.addingTimeInterval(day)
}
completeDayCount = expectedDates.count

guard !expectedDates.isEmpty else {
completeTokens = nil
return
}
var total: Int64 = 0
for date in expectedDates {
guard let bucket = self.days.first(where: { $0.date == date }),
bucket.completeness == .complete,
bucket.tokens >= 0 else {
completeTokens = nil
return
}
let result = total.addingReportingOverflow(bucket.tokens)
guard !result.overflow else {
completeTokens = nil
return
}
total = result.partialValue
}
completeTokens = total
}
}

struct WorkspaceFilters: Codable, Equatable, Sendable {
var projectID: String?
var taskTreeID: String?
Expand Down Expand Up @@ -101,6 +163,10 @@ struct AnalyticsExplorationState: Codable, Equatable, Sendable {
pinnedUsageBaselineID: nil,
pinnedUsageBaselineAccountPartitionID: nil
)

var usesLocalAnalytics: Bool {
section == .graphs && graph.usesLocalAnalytics
}
}

@MainActor
Expand Down Expand Up @@ -133,6 +199,12 @@ final class AnalyticsWorkspaceStore: ObservableObject {
) else {
return .initial
}
guard AnalyticsGraph.coreCases.contains(restored.graph) else {
var core = restored
core.graph = .usageRemaining
core.filters = .all
return core
}
return restored
}

Expand Down Expand Up @@ -353,10 +425,48 @@ struct UsageChartSelection: Equatable, Sendable {
in chart: UsageChartSnapshot,
within visibleRange: DateInterval? = nil
) -> UsageChartSelection? {
candidates(in: chart)
.filter {
visibleRange?.contains($0.point.date) ?? true
}.min {
[
nearestCandidate(
in: chart.allObserved,
series: .observed,
priority: 0,
source: .account,
to: date,
within: visibleRange
),
nearestCandidate(
in: chart.currentProjection,
series: .currentEstimate,
priority: 1,
source: .derivedEstimate,
to: date,
within: visibleRange
),
nearestCandidate(
in: chart.historicalProjection,
series: .pastEstimate,
priority: 2,
source: .accountHistory,
to: date,
within: visibleRange
),
nearestCandidate(
in: chart.estimatedBackfill,
series: .estimatedBackfill,
priority: 3,
source: .tokenEstimate,
to: date,
within: visibleRange
),
nearestCandidate(
in: chart.target,
series: .target,
priority: 4,
source: .weeklyTarget,
to: date,
within: visibleRange
)
].compactMap { $0 }.min {
let leftDistance = abs($0.point.date.timeIntervalSince(date))
let rightDistance = abs($1.point.date.timeIntervalSince(date))
if leftDistance == rightDistance {
Expand All @@ -373,6 +483,30 @@ struct UsageChartSelection: Equatable, Sendable {
}
}

private static func nearestCandidate(
in points: [UsageChartPoint],
series: UsageChartSeries,
priority: Int,
source: UsageChartPointSource,
to date: Date,
within visibleRange: DateInterval?
) -> Candidate? {
guard let point = nearestPoint(
in: points,
to: date,
date: \.date,
within: visibleRange
) else {
return nil
}
return Candidate(
series: series,
point: point,
priority: priority,
source: source
)
}

private static func candidates(
in chart: UsageChartSnapshot
) -> [Candidate] {
Expand Down
Loading