mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-19 16:13:56 +00:00
* Add CodeBurn Pro Mac App Store app SwiftUI MenuBarExtra with litellm-snapshot pricing, Claude/Codex/Copilot parsers, session discovery, auto-refresh timer, and dashboard UI matching the real menubar design. * Add appstore/ to .gitignore Private Mac App Store build, not for the public repo. * Add Optimize tab, token display modes, daily budget alerts, and project drill-down - New Optimize insight tab with Retry Tax and Routing Waste computations (pure math from session data, no LLM required) - Retry tax: shows money wasted on failed edit retries, per-model breakdown - Routing waste: counterfactual savings vs cheapest reliable model, per-model - Token display modes: Cost ($), Tokens (up/down split), Total Tokens - Daily budget alert: configurable threshold, flame turns yellow when exceeded - Project drill-down: click project rows to see per-session cost, tokens, models - Period-aware top sessions: 30-day view now shows 30-day costliest sessions - Friendly project names: show directory name instead of full path, Home for ~ - Session cache TTL bumped to 180s to prevent re-parsing on non-today periods - Audit fixes: array mutation, percentage rounding, ForEach ID collision, baseline minimum threshold, editTurns scoping, first-of-month edge case * Fix model badge ForEach ID collision and budget warning provider filter - SessionDetailsList model badges used id: \.name which silently drops duplicate model entries. Switched to enumerated offset-based ID. - Hero budget warning compared against provider-filtered payload instead of todayPayload (all providers). Now matches the menubar flame tint.
106 lines
4.4 KiB
Swift
106 lines
4.4 KiB
Swift
import SwiftUI
|
|
|
|
struct HeroSection: View {
|
|
@Environment(AppStore.self) private var store
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
SectionCaption(text: caption)
|
|
|
|
HStack(alignment: .firstTextBaseline) {
|
|
Text(heroText)
|
|
.font(.system(size: 32, weight: .semibold, design: .rounded))
|
|
.monospacedDigit()
|
|
.tracking(-1)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Theme.brandAccent, Theme.brandAccentDeep],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
|
|
Spacer()
|
|
|
|
VStack(alignment: .trailing, spacing: 2) {
|
|
if store.displayMetric == .tokens {
|
|
HStack(spacing: 2) {
|
|
Image(systemName: "arrow.up")
|
|
.font(.system(size: 9, weight: .semibold))
|
|
Text(formatTokens(Double(store.payload.current.outputTokens)))
|
|
}
|
|
.font(.system(size: 11))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.secondary)
|
|
HStack(spacing: 2) {
|
|
Image(systemName: "arrow.down")
|
|
.font(.system(size: 9, weight: .semibold))
|
|
Text(formatTokens(Double(store.payload.current.inputTokens)))
|
|
}
|
|
.font(.system(size: 10.5))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.tertiary)
|
|
} else {
|
|
Text("\(store.payload.current.calls.asThousandsSeparated()) calls")
|
|
.font(.system(size: 11))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.secondary)
|
|
Text("\(store.payload.current.sessions) sessions")
|
|
.font(.system(size: 10.5))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
|
|
if store.selectedPeriod == .today,
|
|
store.dailyBudget > 0,
|
|
let todayCost = store.todayPayload?.current.cost,
|
|
todayCost >= store.dailyBudget {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.font(.system(size: 10))
|
|
Text("Daily budget of \(store.dailyBudget.asCurrency()) exceeded")
|
|
.font(.system(size: 11, weight: .medium))
|
|
}
|
|
.foregroundStyle(.orange)
|
|
.padding(.top, 2)
|
|
}
|
|
}
|
|
.padding(.horizontal, 14)
|
|
.padding(.top, 10)
|
|
.padding(.bottom, 12)
|
|
}
|
|
|
|
private var heroText: String {
|
|
if store.displayMetric == .tokens || store.displayMetric == .totalTokens {
|
|
let total = Double(store.payload.current.inputTokens + store.payload.current.outputTokens)
|
|
if total >= 1_000_000_000 { return String(format: "%.2fB tok", total / 1_000_000_000) }
|
|
if total >= 1_000_000 { return String(format: "%.1fM tok", total / 1_000_000) }
|
|
if total >= 1_000 { return String(format: "%.0fK tok", total / 1_000) }
|
|
return String(format: "%.0f tok", total)
|
|
}
|
|
return store.payload.current.cost.asCurrency()
|
|
}
|
|
|
|
private func formatTokens(_ n: Double) -> String {
|
|
if n >= 1_000_000_000 { return String(format: "%.1fB", n / 1_000_000_000) }
|
|
if n >= 1_000_000 { return String(format: "%.1fM", n / 1_000_000) }
|
|
if n >= 1_000 { return String(format: "%.0fK", n / 1_000) }
|
|
return String(format: "%.0f", n)
|
|
}
|
|
|
|
private var caption: String {
|
|
let label = store.payload.current.label.isEmpty ? store.selectedPeriod.rawValue : store.payload.current.label
|
|
if store.selectedPeriod == .today {
|
|
return "\(label) · \(todayDate)"
|
|
}
|
|
return label
|
|
}
|
|
|
|
private var todayDate: String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "EEE MMM d"
|
|
return formatter.string(from: Date())
|
|
}
|
|
}
|