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
8 changes: 4 additions & 4 deletions mac/Sources/CodeBurnMenubar/Data/MenubarPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ extension CurrentBlock {
oneShotRate = try c.decodeIfPresent(Double.self, forKey: .oneShotRate)
inputTokens = try c.decode(Int.self, forKey: .inputTokens)
outputTokens = try c.decode(Int.self, forKey: .outputTokens)
cacheHitPercent = try c.decode(Double.self, forKey: .cacheHitPercent)
topActivities = try c.decode([ActivityEntry].self, forKey: .topActivities)
topModels = try c.decode([ModelEntry].self, forKey: .topModels)
providers = try c.decode([String: Double].self, forKey: .providers)
cacheHitPercent = try c.decodeIfPresent(Double.self, forKey: .cacheHitPercent) ?? 0
topActivities = try c.decodeIfPresent([ActivityEntry].self, forKey: .topActivities) ?? []
topModels = try c.decodeIfPresent([ModelEntry].self, forKey: .topModels) ?? []
providers = try c.decodeIfPresent([String: Double].self, forKey: .providers) ?? [:]
topProjects = try c.decodeIfPresent([ProjectEntry].self, forKey: .topProjects) ?? []
modelEfficiency = try c.decodeIfPresent([ModelEfficiencyEntry].self, forKey: .modelEfficiency) ?? []
topSessions = try c.decodeIfPresent([TopSessionEntry].self, forKey: .topSessions) ?? []
Expand Down
2 changes: 1 addition & 1 deletion mac/Sources/CodeBurnMenubar/Views/ActivitySection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct ActivitySection: View {
}
) {
VStack(alignment: .leading, spacing: 7) {
let maxCost = store.payload.current.topActivities.map(\.cost).max() ?? 1
let maxCost = max(store.payload.current.topActivities.map(\.cost).max() ?? 1, 0.01)
ForEach(store.payload.current.topActivities, id: \.name) { activity in
ActivityRow(activity: activity, maxCost: maxCost)
}
Expand Down
2 changes: 1 addition & 1 deletion mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ private struct BarTooltipCard: View {

if !bar.topModels.isEmpty {
VStack(alignment: .leading, spacing: 3) {
ForEach(Array(bar.topModels.prefix(4).enumerated()), id: \.element.name) { idx, m in
ForEach(Array(bar.topModels.prefix(4).enumerated()), id: \.offset) { idx, m in
HStack(spacing: 6) {
RoundedRectangle(cornerRadius: 1)
.fill(Theme.brandAccent.opacity(0.75 - Double(idx) * 0.12))
Expand Down
6 changes: 4 additions & 2 deletions mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,10 @@ struct FooterBar: View {
}

let fresh = await FXRateCache.shared.rate(for: code)
store.currency = code
CurrencyState.shared.apply(code: code, rate: fresh ?? cached, symbol: symbol)
if let rate = fresh ?? cached {
store.currency = code
CurrencyState.shared.apply(code: code, rate: rate, symbol: symbol)
}
}

CLICurrencyConfig.persist(code: code)
Expand Down
2 changes: 1 addition & 1 deletion mac/Sources/CodeBurnMenubar/Views/ModelsSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct ModelsSection: View {
}
) {
VStack(alignment: .leading, spacing: 7) {
let maxCost = store.payload.current.topModels.map(\.cost).max() ?? 1
let maxCost = max(store.payload.current.topModels.map(\.cost).max() ?? 1, 0.01)
ForEach(store.payload.current.topModels, id: \.name) { model in
ModelRow(model: model, maxCost: maxCost)
}
Expand Down
95 changes: 23 additions & 72 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,6 @@ program
let scanRange: DateRange
let cache: DailyCache
let todayProviderData: PeriodData | null = null
let usedPerProviderCachePath = false

if (isAllProviders) {
cache = await hydrateCache()
Expand All @@ -487,32 +486,11 @@ program
}
} else {
cache = await loadDailyCache()
const cacheIsCurrent = cache.lastComputedDate !== null
&& cache.lastComputedDate >= yesterdayStr
if (cacheIsCurrent && rangeStartStr < todayStr) {
const todayProviderProjects = fp(await parseAllSessions(todayRange, pf))
todayProviderData = buildPeriodData(periodInfo.label, todayProviderProjects)
const historicalDays = getDaysInRange(cache, rangeStartStr, yesterdayStr)
let histCost = 0, histCalls = 0
for (const d of historicalDays) {
const prov = d.providers[pf]
if (prov) { histCost += prov.cost; histCalls += prov.calls }
}
currentData = {
...todayProviderData,
cost: todayProviderData.cost + histCost,
calls: todayProviderData.calls + histCalls,
}
scanProjects = todayProviderProjects
scanRange = todayRange
usedPerProviderCachePath = true
} else {
const fullProjects = fp(await parseAllSessions(periodInfo.range, pf))
todayProviderData = buildPeriodData(periodInfo.label, fullProjects)
currentData = todayProviderData
scanProjects = fullProjects
scanRange = periodInfo.range
}
const fullProjects = fp(await parseAllSessions(periodInfo.range, pf))
todayProviderData = buildPeriodData(periodInfo.label, fullProjects)
currentData = todayProviderData
scanProjects = fullProjects
scanRange = periodInfo.range
}

// PROVIDERS
Expand Down Expand Up @@ -579,7 +557,8 @@ program
topModels,
}
})
} else if (usedPerProviderCachePath) {
} else {
const emptyModels = [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[]
const historyFromCache = allCacheDays.map(d => {
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
return {
Expand All @@ -590,53 +569,25 @@ program
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
topModels: emptyModels,
}
})
const todayCost = todayProviderData!.cost
const todayCalls = todayProviderData!.calls
if (todayCost > 0 || todayCalls > 0) {
historyFromCache.push({
date: todayStr,
cost: todayCost,
calls: todayCalls,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
topModels: [],
const todayFromParse = aggregateProjectsIntoDays(scanProjects)
.filter(d => d.date === todayStr)
.map(d => {
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
return {
date: d.date,
cost: prov.cost,
calls: prov.calls,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
topModels: emptyModels,
}
})
}
dailyHistory = historyFromCache
} else {
const histFromCache = allCacheDays.map(d => {
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
return {
date: d.date,
cost: prov.cost,
calls: prov.calls,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
}
})
const fallbackDays = aggregateProjectsIntoDays(scanProjects)
const liveDays = fallbackDays.map(d => {
const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
return {
date: d.date,
cost: prov.cost,
calls: prov.calls,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
topModels: [] as { name: string; cost: number; calls: number; inputTokens: number; outputTokens: number }[],
}
})
dailyHistory = [...histFromCache, ...liveDays]
dailyHistory = [...historyFromCache, ...todayFromParse]
}

const home = homedir()
Expand Down
2 changes: 1 addition & 1 deletion src/providers/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
.filter(c => c.type === 'input_text')
.map(c => c.text ?? '')
.filter(Boolean)
if (texts.length > 0) pendingUserMessage = texts.join(' ')
if (texts.length > 0) pendingUserMessage = texts.join(' ').slice(0, 500)
continue
}

Expand Down
11 changes: 4 additions & 7 deletions src/providers/gemini.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { readdir, readFile, stat } from 'fs/promises'
import { readdir, stat } from 'fs/promises'
import { join } from 'path'
import { homedir } from 'os'

import { readSessionFile } from '../fs-utils.js'
import { calculateCost } from '../models.js'
import { extractBashCommands } from '../bash-utils.js'
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
Expand Down Expand Up @@ -185,12 +186,8 @@ function parseJsonl(raw: string): GeminiSession | null {
function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
return {
async *parse(): AsyncGenerator<ParsedProviderCall> {
let raw: string
try {
raw = await readFile(source.path, 'utf-8')
} catch {
return
}
const raw = await readSessionFile(source.path)
if (raw === null) return

let data: GeminiSession | null = null

Expand Down
Loading