import Chartsimport CodexBarCoreimport SwiftUI @MainActorstruct 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 sessions: [CostUsageSessionBreakdown] 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] = [], sessions: [CostUsageSessionBreakdown] = [], 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.sessions = sessions 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 { VStack(alignment: .leading, spacing: 2) { 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 let disclaimer = Self.estimateDisclaimer(provider: self.provider) { Text(disclaimer) .font(.caption2) .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) .lineLimit(1) .truncationMode(.tail) } } } 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) } if !self.sessions.isEmpty { self.sessionsBlock } } .padding(.horizontal, 16) .padding(.vertical, Self.verticalPadding) .frame(minWidth: self.width, maxWidth: .infinity, alignment: .top) } static func estimateDisclaimer(provider: UsageProvider) -> String? { provider == .codex ? L("codex_api_estimate_hint") : nil } 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 private static let sessionRowHeight: CGFloat = 44 private static let sessionRowSpacing: CGFloat = 5 private static let maxVisibleSessionRows = 5 static let verticalPadding: CGFloat = 10 private var sessionsBlock: some View { let visibleCount = min(self.sessions.count, Self.maxVisibleSessionRows) return VStack(alignment: .leading, spacing: Self.sessionRowSpacing) { HStack { Text("Conversations (\(self.windowLabel ?? Self.windowLabel(days: self.historyDays)))") .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) Spacer() Text("\(self.sessions.count)") .font(.caption2) .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) } .frame(height: Self.detailPrimaryLineHeight, alignment: .leading) ScrollView(.vertical) { LazyVStack(alignment: .leading, spacing: Self.sessionRowSpacing) { ForEach(self.sessions) { session in self.sessionRow(session) } } } .scrollIndicators(self.sessions.count > visibleCount ? .visible : .hidden) .frame( height: CGFloat(visibleCount) * Self.sessionRowHeight + CGFloat(max(visibleCount - 1, 0)) * Self.sessionRowSpacing, alignment: .topLeading) } .frame( height: Self.detailPrimaryLineHeight + Self.sessionRowSpacing + CGFloat(visibleCount) * Self.sessionRowHeight + CGFloat(max(visibleCount - 1, 0)) * Self.sessionRowSpacing, alignment: .topLeading) } private func sessionRow(_ session: CostUsageSessionBreakdown) -> some View { HStack(alignment: .top, spacing: 8) { VStack(alignment: .leading, spacing: 1) { Text("Session \(Self.shortSessionID(session.sessionID))") .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) Text(Self.sessionUsageLine(session)) .font(.caption2) .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) .lineLimit(1) .truncationMode(.tail) Text(session.lastActivity, format: .dateTime.month(.abbreviated).day().hour().minute()) .font(.caption2) .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) .lineLimit(1) } Spacer(minLength: 8) Text(session.costUSD.map(self.costString) ?? "—") .font(.caption) .monospacedDigit() .foregroundStyle(.secondary) .lineLimit(1) } .frame(height: Self.sessionRowHeight, alignment: .topLeading) .accessibilityElement(children: .combine) } static func shortSessionID(_ sessionID: String) -> String { let trimmed = sessionID.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.count > 12 else { return trimmed } return "\(trimmed.prefix(4))...\(trimmed.suffix(8))" } private static func sessionUsageLine(_ session: CostUsageSessionBreakdown) -> String { let models = session.modelBreakdowns.map(\.modelName) let modelLabel = if models.isEmpty { "Unknown model" } else if models.count == 1 { models[0] } else { "\(models[0]) +\(models.count - 1)" } let input = session.inputTokens.map(UsageFormatter.tokenCountString) ?? "—" let cached = session.cachedInputTokens.map(UsageFormatter.tokenCountString) ?? "—" let output = session.outputTokens.map(UsageFormatter.tokenCountString) ?? "—" return "\(modelLabel) · \(input) input · \(cached) cached · \(output) output" } 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] }Lines 1–500 of 1049. Load more
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 sessions: [CostUsageSessionBreakdown] 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] = [], sessions: [CostUsageSessionBreakdown] = [], 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.sessions = sessions 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 { VStack(alignment: .leading, spacing: 2) { 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 let disclaimer = Self.estimateDisclaimer(provider: self.provider) { Text(disclaimer) .font(.caption2) .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) .lineLimit(1) .truncationMode(.tail) } } } 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) } if !self.sessions.isEmpty { self.sessionsBlock } } .padding(.horizontal, 16) .padding(.vertical, Self.verticalPadding) .frame(minWidth: self.width, maxWidth: .infinity, alignment: .top) } static func estimateDisclaimer(provider: UsageProvider) -> String? { provider == .codex ? L("codex_api_estimate_hint") : nil } 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 private static let sessionRowHeight: CGFloat = 44 private static let sessionRowSpacing: CGFloat = 5 private static let maxVisibleSessionRows = 5 static let verticalPadding: CGFloat = 10 private var sessionsBlock: some View { let visibleCount = min(self.sessions.count, Self.maxVisibleSessionRows) return VStack(alignment: .leading, spacing: Self.sessionRowSpacing) { HStack { Text("Conversations (\(self.windowLabel ?? Self.windowLabel(days: self.historyDays)))") .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) Spacer() Text("\(self.sessions.count)") .font(.caption2) .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) } .frame(height: Self.detailPrimaryLineHeight, alignment: .leading) ScrollView(.vertical) { LazyVStack(alignment: .leading, spacing: Self.sessionRowSpacing) { ForEach(self.sessions) { session in self.sessionRow(session) } } } .scrollIndicators(self.sessions.count > visibleCount ? .visible : .hidden) .frame( height: CGFloat(visibleCount) * Self.sessionRowHeight + CGFloat(max(visibleCount - 1, 0)) * Self.sessionRowSpacing, alignment: .topLeading) } .frame( height: Self.detailPrimaryLineHeight + Self.sessionRowSpacing + CGFloat(visibleCount) * Self.sessionRowHeight + CGFloat(max(visibleCount - 1, 0)) * Self.sessionRowSpacing, alignment: .topLeading) } private func sessionRow(_ session: CostUsageSessionBreakdown) -> some View { HStack(alignment: .top, spacing: 8) { VStack(alignment: .leading, spacing: 1) { Text("Session \(Self.shortSessionID(session.sessionID))") .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) Text(Self.sessionUsageLine(session)) .font(.caption2) .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) .lineLimit(1) .truncationMode(.tail) Text(session.lastActivity, format: .dateTime.month(.abbreviated).day().hour().minute()) .font(.caption2) .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) .lineLimit(1) } Spacer(minLength: 8) Text(session.costUSD.map(self.costString) ?? "—") .font(.caption) .monospacedDigit() .foregroundStyle(.secondary) .lineLimit(1) } .frame(height: Self.sessionRowHeight, alignment: .topLeading) .accessibilityElement(children: .combine) } static func shortSessionID(_ sessionID: String) -> String { let trimmed = sessionID.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.count > 12 else { return trimmed } return "\(trimmed.prefix(4))...\(trimmed.suffix(8))" } private static func sessionUsageLine(_ session: CostUsageSessionBreakdown) -> String { let models = session.modelBreakdowns.map(\.modelName) let modelLabel = if models.isEmpty { "Unknown model" } else if models.count == 1 { models[0] } else { "\(models[0]) +\(models.count - 1)" } let input = session.inputTokens.map(UsageFormatter.tokenCountString) ?? "—" let cached = session.cachedInputTokens.map(UsageFormatter.tokenCountString) ?? "—" let output = session.outputTokens.map(UsageFormatter.tokenCountString) ?? "—" return "\(modelLabel) · \(input) input · \(cached) cached · \(output) output" } 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] }