mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-19 10:13:21 +00:00
#1657 put the ~ mark on estimated rows, but the headline total stayed bare on the desktop hero, the mac hero, the Windows hero and tray text, and GNOME. Each now applies the same 1% rule to the provider split's estimated portion (the desktop helper moves from the VS Code summary into lib/format). Combined device totals carry no estimated split and stay unmarked.
165 lines
7.7 KiB
TypeScript
165 lines
7.7 KiB
TypeScript
import { t } from '../i18n'
|
|
import { estimatedPortion, formatCount, formatResetTime, formatUsd, isEstimatedCost, setActiveCurrency, type ActiveCurrency } from '../lib/format'
|
|
import { PROVIDER_NAMES } from '../lib/providers'
|
|
import type { MenubarPayload, ProviderName, QuotaProvider } from '../lib/types'
|
|
|
|
export type Figure = { cost: number; estimated: boolean }
|
|
export type Ranked = { name: string; cost: number; estimated: boolean }
|
|
export type QuotaLine = { provider: string; plan: string | null; label: string; percent: number; resetsAt: string | null }
|
|
|
|
/** Everything the status bar, its tooltip, the sidebar and Copy summary show.
|
|
* Small and plain so the last one is cached and painted at the next startup. */
|
|
export type Summary = {
|
|
at: number
|
|
currency: ActiveCurrency
|
|
today: Figure
|
|
week: Figure | null
|
|
month: Figure | null
|
|
topModels: Ranked[]
|
|
topProjects: Ranked[]
|
|
workspace: { label: string; today: Figure; week: Figure | null } | null
|
|
quota: QuotaLine[]
|
|
optimize: { findingCount: number; savingsUSD: number } | null
|
|
}
|
|
|
|
const USD: ActiveCurrency = { code: 'USD', symbol: '$', rate: 1 }
|
|
|
|
/** The #1657 rule, as every other surface applies it. Needs the payload's currency active. */
|
|
export function figureOf(payload: MenubarPayload): Figure {
|
|
return { cost: payload.current.cost, estimated: isEstimatedCost(payload.current.cost, estimatedPortion(payload.current)) }
|
|
}
|
|
|
|
export function buildSummary(input: {
|
|
today: MenubarPayload
|
|
week: MenubarPayload | null
|
|
month: MenubarPayload | null
|
|
workspace: { label: string; today: MenubarPayload; week: MenubarPayload | null } | null
|
|
quota: QuotaProvider[] | null
|
|
optimize: { findingCount: number; savingsUSD: number } | null
|
|
now?: number
|
|
}): Summary {
|
|
const currency = input.today.currency ?? USD
|
|
setActiveCurrency(currency)
|
|
const ranked = input.week ?? input.today
|
|
return {
|
|
at: input.now ?? Date.now(),
|
|
currency,
|
|
today: figureOf(input.today),
|
|
week: input.week ? figureOf(input.week) : null,
|
|
month: input.month ? figureOf(input.month) : null,
|
|
topModels: (ranked.current.topModels ?? []).slice(0, 5).map(row => ({ name: row.name, cost: row.cost, estimated: isEstimatedCost(row.cost, row.estimatedCostUSD) })),
|
|
topProjects: (ranked.current.topProjects ?? []).slice(0, 5).map(row => ({ name: row.name, cost: row.cost, estimated: false })),
|
|
workspace: input.workspace
|
|
? { label: input.workspace.label, today: figureOf(input.workspace.today), week: input.workspace.week ? figureOf(input.workspace.week) : null }
|
|
: null,
|
|
quota: quotaLines(input.quota ?? []),
|
|
optimize: input.optimize,
|
|
}
|
|
}
|
|
|
|
/** One line per connected provider: its most-used window, the one that runs out first. */
|
|
export function quotaLines(quota: QuotaProvider[]): QuotaLine[] {
|
|
const lines: QuotaLine[] = []
|
|
for (const provider of quota) {
|
|
if (provider.connection !== 'connected' && provider.connection !== 'stale') continue
|
|
const windows = [provider.primary, ...provider.details].filter((w): w is NonNullable<typeof w> => w !== null)
|
|
const top = windows.reduce<typeof windows[number] | null>((best, w) => (best === null || w.percent > best.percent ? w : best), null)
|
|
if (!top) continue
|
|
lines.push({ provider: provider.provider, plan: provider.planLabel, label: top.label, percent: top.percent, resetsAt: top.resetsAt })
|
|
}
|
|
return lines
|
|
}
|
|
|
|
export function money(figure: Figure): string {
|
|
return `${figure.estimated ? '~' : ''}${formatUsd(figure.cost)}`
|
|
}
|
|
|
|
function percent(value: number): string {
|
|
return `${Math.round(value * 100)}%`
|
|
}
|
|
|
|
export type StatusBarFormat = 'cost' | 'costAndQuota' | 'workspace' | 'hidden'
|
|
|
|
export function statusText(summary: Summary | null, format: StatusBarFormat): string {
|
|
if (!summary) return '$(flame)'
|
|
setActiveCurrency(summary.currency)
|
|
const today = money(summary.today)
|
|
if (format === 'costAndQuota' && summary.quota.length > 0) {
|
|
const top = Math.max(...summary.quota.map(line => line.percent))
|
|
return `$(flame) ${today} · ${percent(top)}`
|
|
}
|
|
if (format === 'workspace' && summary.workspace) return `$(flame) ${money(summary.workspace.today)} / ${today}`
|
|
return `$(flame) ${today}`
|
|
}
|
|
|
|
function providerName(line: QuotaLine): string {
|
|
const name = PROVIDER_NAMES[line.provider as ProviderName] ?? line.provider
|
|
return line.plan ? `${name} ${line.plan}` : name
|
|
}
|
|
|
|
export function quotaText(line: QuotaLine): string {
|
|
const reset = formatResetTime(line.resetsAt)
|
|
const used = reset
|
|
? t('plans.quota.usedWithReset', { percent: Math.round(line.percent * 100), reset })
|
|
: t('plans.quota.used', { percent: Math.round(line.percent * 100) })
|
|
return `${providerName(line)} · ${line.label} · ${used}`
|
|
}
|
|
|
|
/** Names reach the tooltip from repositories and logs: keep them literal text. */
|
|
function cell(text: string): string {
|
|
return text.replace(/[\\`*_[\]()|<>!$#~]/g, '\\$&')
|
|
}
|
|
|
|
/** The status bar tooltip: plain markdown, one command link, no HTML. */
|
|
export function tooltipMarkdown(summary: Summary): string {
|
|
setActiveCurrency(summary.currency)
|
|
const rows: Array<[string, string]> = [[t('common.period.today'), money(summary.today)]]
|
|
if (summary.week) rows.push([t('common.period.week'), money(summary.week)])
|
|
if (summary.month) rows.push([t('common.period.month'), money(summary.month)])
|
|
if (summary.workspace) rows.push([t('ide.status.workspaceToday', { name: summary.workspace.label }), money(summary.workspace.today)])
|
|
const out = ['**CodeBurn**', '', '| | |', '|:--|--:|', ...rows.map(([label, value]) => `| ${cell(label)} | ${value} |`), '']
|
|
const top = summary.topModels[0]
|
|
if (top) out.push(`${t('ide.status.topModel')}: ${cell(top.name)} · ${money(top)}`, '')
|
|
if (summary.quota.length > 0) {
|
|
out.push(`**${t('shell.nav.plans')}**`, '')
|
|
for (const line of summary.quota) out.push(`- ${quotaText(line)}`)
|
|
out.push('')
|
|
}
|
|
const estimated = [summary.today, summary.week, summary.month, summary.workspace?.today, top].some(figure => figure?.estimated)
|
|
if (estimated) out.push(`_${t('shared.usd.estimated')}_`, '')
|
|
out.push(`[${t('ide.status.open')}](command:codeburn.openDashboard)`)
|
|
return out.join('\n')
|
|
}
|
|
|
|
/** Copy summary: plain text a person pastes into a chat or a standup note. */
|
|
export function summaryText(summary: Summary): string {
|
|
setActiveCurrency(summary.currency)
|
|
const lines = [`CodeBurn · ${new Date(summary.at).toLocaleDateString()}`]
|
|
lines.push(`${t('common.period.today')}: ${money(summary.today)}`)
|
|
if (summary.week) lines.push(`${t('common.period.week')}: ${money(summary.week)}`)
|
|
if (summary.month) lines.push(`${t('common.period.month')}: ${money(summary.month)}`)
|
|
if (summary.workspace) {
|
|
lines.push(`${summary.workspace.label}: ${money(summary.workspace.today)}${summary.workspace.week ? ` · ${t('common.period.week')} ${money(summary.workspace.week)}` : ''}`)
|
|
}
|
|
if (summary.topModels.length > 0) {
|
|
lines.push(`${t('ide.sidebar.topModels')}: ${summary.topModels.slice(0, 3).map(row => `${row.name} ${money(row)}`).join(', ')}`)
|
|
}
|
|
for (const line of summary.quota) lines.push(quotaText(line))
|
|
if (summary.optimize && summary.optimize.findingCount > 0) {
|
|
lines.push(`${t('shell.nav.optimize')}: ${formatCount(summary.optimize.findingCount, 'finding')} · ${t('ide.sidebar.savings', { amount: formatUsd(summary.optimize.savingsUSD) })}`)
|
|
}
|
|
return lines.join('\n')
|
|
}
|
|
|
|
/** What the activity bar view renders. */
|
|
export type SidebarState = {
|
|
status: 'loading' | 'ok' | 'error'
|
|
error: { kind: string; message: string } | null
|
|
summary: Summary | null
|
|
/** The workspace's name, or null with no folder open. */
|
|
workspaceLabel: string | null
|
|
/** Set when the CLI runs on the editor's own, too-old Node. */
|
|
runtimeNote: string | null
|
|
/** The star line at the bottom, once the extension has seen real use. */
|
|
star?: boolean
|
|
}
|