import { Gtk } from "ags/gtk3" import { createPoll } from "ags/time" import { For, createState } from "ags" import { shell } from "../lib/utils" type Limit = { label: string percent: number fraction: number resetsIn: string } type DayUsage = { key: string label: string tokens: string fraction: number isToday: boolean detail: string } type ModelUsage = { key: string label: string tokens: string fraction: number detail: string } type AgentUsage = { ready: boolean name: string plan: string statusText: string helpText: string limits: Limit[] days: DayUsage[] models: ModelUsage[] } const EMPTY_USAGE: AgentUsage = { ready: false, name: "Claude Code", plan: "", statusText: "", helpText: "", limits: [], days: [], models: [], } const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] const MARK_PATH = "/usr/share/omarchy/shell/plugins/agents/assets/claude.svg" function formatTokens(tokens: number) { if (tokens >= 1000000000) return `${(tokens / 1000000000).toFixed(1)}B` if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M` if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K` return `${Math.round(tokens)}` } function localDateKey(date: Date) { const month = `${date.getMonth() + 1}`.padStart(2, "0") const day = `${date.getDate()}`.padStart(2, "0") return `${date.getFullYear()}-${month}-${day}` } function formatResetsIn(resetsAt: string) { const resetTime = Date.parse(resetsAt) if (!resetTime) return "" const minutesLeft = Math.round((resetTime - Date.now()) / 60000) if (minutesLeft <= 0) return "resetting" const days = Math.floor(minutesLeft / 1440) const hours = Math.floor((minutesLeft % 1440) / 60) const minutes = minutesLeft % 60 if (days > 0) return `Resets in ${days}d ${hours}h` if (hours > 0) return `Resets in ${hours}h ${minutes}m` return `Resets in ${minutes}m` } function formatModelName(model: string) { return model .replace(/^claude-/, "") .replace(/-/g, " ") .replace(/(^| )(\w)/g, (_match, space, letter) => `${space}${letter.toUpperCase()}`) } function formatPercent(usedFraction: number) { if (usedFraction > 0 && usedFraction < 0.01) return "<1%" return `${Math.round(usedFraction * 100)}%` } function parseLimits(record: any): Limit[] { if (!Array.isArray(record.limits)) return [] return record.limits.map((limit: any) => { const usedFraction = Number(limit.percent) || 0 return { label: String(limit.label ?? "").replace(/\s*\(.*\)$/, ""), percent: usedFraction, fraction: Math.min(1, Math.max(0, usedFraction)), resetsIn: formatResetsIn(String(limit.resetsAt ?? "")), } }) } function parseDays(record: any): DayUsage[] { if (!Array.isArray(record.recentDays)) return [] const todayKey = localDateKey(new Date()) const dayTokens = record.recentDays.map((day: any) => Number(day.messageCount) || 0) const busiestDay = Math.max(...dayTokens, 1) return record.recentDays.map((day: any, index: number) => { const date = String(day.date ?? "") const tokens = dayTokens[index] const isToday = date === todayKey const parsedDate = new Date(`${date}T00:00:00`) const weekday = WEEKDAYS[parsedDate.getDay()] ?? date const shortDate = `${parsedDate.getMonth() + 1}/${parsedDate.getDate()}` const detail = `${weekday} ${shortDate} - ${formatTokens(tokens)} tokens` return { key: date, label: isToday ? "Today" : weekday, tokens: formatTokens(tokens), fraction: tokens / busiestDay, isToday, detail: isToday ? `${detail} - ${record.todayPrompts ?? 0} prompts, ${record.todaySessions ?? 0} sessions` : detail, } }) } function parseModels(record: any): ModelUsage[] { const usageByModel = record.modelUsage if (!usageByModel || typeof usageByModel !== "object") return [] const models = Object.keys(usageByModel).map((model) => { const usage = usageByModel[model] ?? {} const input = Number(usage.inputTokens) || 0 const output = Number(usage.outputTokens) || 0 const cacheCreation = Number(usage.cacheCreationInputTokens) || 0 const cacheRead = Number(usage.cacheReadInputTokens) || 0 return { key: model, label: formatModelName(model), total: input + output + cacheCreation + cacheRead, detail: `in ${formatTokens(input)} - out ${formatTokens(output)} - ` + `cache write ${formatTokens(cacheCreation)} - cache read ${formatTokens(cacheRead)}`, } }) const heaviestModel = Math.max(...models.map((model) => model.total), 1) return models .sort((left, right) => right.total - left.total) .map((model) => ({ key: model.key, label: model.label, tokens: formatTokens(model.total), fraction: model.total / heaviestModel, detail: model.detail, })) } function parseUsage(stdout: string): AgentUsage { try { const record = JSON.parse(stdout.trim()) if (!record.ready) return EMPTY_USAGE return { ready: true, name: String(record.name ?? "Claude Code"), plan: String(record.tierLabel ?? ""), statusText: String(record.usageStatusText ?? ""), helpText: String(record.authHelpText ?? ""), limits: parseLimits(record), days: parseDays(record), models: parseModels(record), } } catch { return EMPTY_USAGE } } const usage = createPoll( EMPTY_USAGE, 60000, shell("bash $HOME/.config/ags/lib/claude-usage.sh"), (stdout) => parseUsage(stdout), ) const [limitsExpanded, setLimitsExpanded] = createState(true) const [daysExpanded, setDaysExpanded] = createState(true) const [modelsExpanded, setModelsExpanded] = createState(true) function Section({ title, expanded, onToggle, content }: { title: string expanded: any onToggle: () => void content: any }) { return ( {content} ) } function LimitRow({ limit }: { limit: Limit }) { return ( ) } function DayRow({ day }: { day: DayUsage }) { return ( ) } function ModelRow({ model }: { model: ModelUsage }) { const fillPercent = Math.round(model.fraction * 100) const fill = `linear-gradient(to right, alpha(@foreground, 0.18) ${fillPercent}%, alpha(@foreground, 0.06) ${fillPercent}%)` return ( ) } export function ClaudeUsageCard() { return (