Files
omarchycn/shell/plugins/model-usage/Main.qml
T
David Heinemeier HanssonandClaude Opus 5 24dc914d28 Recover model-usage limits from a boot-time probe failure
The shell starts before DHCP finishes, so Claude's first probe of the
OAuth usage endpoint came back as a transport failure and pinned "Claude
limits unavailable" for a full refresh interval. Opening the panel could
not clear it either: the open path refreshed unforced and hit the same
throttle it was trying to escape.

A transport failure now arms a short retry that goes straight to the
probe, since a retry answering to the throttle never gets off the
ground. Any real answer disarms it — a server that replied, 429
included, is one to stop pestering. Probes are single-flight, a fresh
token skips the throttle its predecessor set, and opening the panel asks
for the wire without re-walking every transcript on disk.

The module also earns its place in the bar now instead of sitting there
dimmed with nothing to say: it collapses out entirely until a provider
has actually recorded usage, here or on a synced machine. That made both
presence probes redundant, so they are gone. Selection follows the
provider rather than its slot, so one appearing while the panel is open
no longer swaps out what you were reading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:00:43 -07:00

590 lines
21 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Io
import "providers"
Item {
id: root
visible: false
property var settings: ({})
Claude {
id: claudeProvider
enabled: root.providerEnabled("claude")
providerSettings: root.settings && root.settings.providers && root.settings.providers.claude ? root.settings.providers.claude : ({})
onLastRefreshedAtMsChanged: root.scheduleSync()
onReadyChanged: root.scheduleSync()
}
Codex {
id: codexProvider
enabled: root.providerEnabled("codex")
providerSettings: root.settings && root.settings.providers && root.settings.providers.codex ? root.settings.providers.codex : ({})
onLastRefreshedAtMsChanged: root.scheduleSync()
onReadyChanged: root.scheduleSync()
}
property var providers: [claudeProvider, codexProvider]
// A subscription earns a place in the bar and the panel by being switched on
// in settings and having actually produced numbers — locally or on a synced
// device. Presence on disk is not enough: a box that installed Codex and
// never ran it would get a tab full of zeroes. With nothing to show, the
// whole module collapses out of the bar rather than sitting there dimmed.
property var enabledProviders: {
var rev = syncRevision
var running = syncRunning
var result = []
if (claudeProvider.enabled) {
var claude = displayProvider(claudeProvider)
if (providerHasData(claude)) result.push(claude)
}
if (codexProvider.enabled) {
var codex = displayProvider(codexProvider)
if (providerHasData(codex)) result.push(codex)
}
return result
}
// All-time, not today: a quiet day is not the same as an absent provider.
function providerHasData(p) {
return numberValue(p.totalPrompts) > 0 || numberValue(p.totalSessions) > 0
|| numberValue(p.activeDays) > 0 || Number(p.rateLimitPercent) >= 0
|| Number(p.secondaryRateLimitPercent) >= 0
}
property bool refreshing: claudeProvider.refreshing || codexProvider.refreshing || syncRunning
property double aggregateUpdatedAtMs: aggregateData && aggregateData.updatedAtMs ? Number(aggregateData.updatedAtMs) : 0
property double lastRefreshedAtMs: Math.max(aggregateUpdatedAtMs, claudeProvider.lastRefreshedAtMs || 0, codexProvider.lastRefreshedAtMs || 0)
property int refreshIntervalSec: Math.max(30, Number(setting("refreshIntervalSec", 900)))
property var syncModeSetting: setting("syncMode", setting("syncEnabled", false))
property bool syncEnabled: parseSyncEnabled(syncModeSetting)
property string syncDir: String(setting("syncDir", ""))
property string syncFileName: String(setting("syncFileName", ""))
property string syncDeviceId: String(setting("syncDeviceId", ""))
readonly property string home: Quickshell.env("HOME") || ""
property string detectedHostname: ""
readonly property string syncEffectiveDir: expandPath(syncDir)
readonly property string syncEffectiveFileName: safeSnapshotFileName(syncFileName, syncDeviceId)
readonly property string syncEffectiveDeviceId: safeDeviceId(syncDeviceId || syncEffectiveFileName.replace(/\.json$/i, ""))
readonly property string syncSnapshotPath: syncConfigured() ? syncEffectiveDir + "/" + syncEffectiveFileName : home + "/.cache/omarchy/model-usage-disabled.json"
property var aggregateData: ({})
property int syncRevision: 0
property bool syncRunning: false
property bool syncRequestedWhileRunning: false
property string syncStatusText: ""
property int syncDeviceCount: syncConfigured() && aggregateData && aggregateData.deviceCount ? Number(aggregateData.deviceCount) : 0
onSyncEnabledChanged: syncSettingsChanged()
onSyncDirChanged: syncSettingsChanged()
onSyncFileNameChanged: if (syncConfigured()) scheduleSync()
onSyncDeviceIdChanged: if (syncConfigured()) scheduleSync()
Component.onCompleted: if (syncConfigured()) scheduleSync()
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
Timer {
interval: root.refreshIntervalSec * 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshAll()
}
Timer {
id: syncDebounce
interval: 1000
repeat: false
onTriggered: root.runSync()
}
Process {
id: syncMkdirProcess
running: false
onRunningChanged: root.updateSyncRunning()
onExited: function(exitCode) {
if (exitCode !== 0) {
if (root.syncConfigured()) root.syncStatusText = "Usage sync mkdir failed"
root.finishSyncRun()
return
}
root.writeSyncSnapshot()
}
}
Process {
id: syncScanProcess
running: false
onRunningChanged: root.updateSyncRunning()
onExited: function(exitCode) {
if (exitCode !== 0 && root.syncConfigured()) root.syncStatusText = "Usage sync scan failed"
root.finishSyncRun()
}
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.parseSyncScanOutput(text)
}
stderr: StdioCollector {
waitForEnd: true
onStreamFinished: if (text.trim() !== "") console.warn("model-usage/sync", text.trim())
}
}
FileView {
id: syncSnapshotFile
path: root.syncSnapshotPath
watchChanges: false
atomicWrites: true
printErrors: false
}
FileView {
id: hostnameFile
path: "/etc/hostname"
watchChanges: false
printErrors: false
onLoaded: root.detectedHostname = String(text() || "").trim()
}
function providerEnabled(id) {
if (!settings || !settings.providers || !settings.providers[id]) return id === "claude" || id === "codex"
return settings.providers[id].enabled !== false
}
function parseSyncEnabled(value) {
if (value === true) return true
var text = String(value || "").trim().toLowerCase()
return text === "on" || text === "enabled" || text === "true" || text === "yes" || text === "1"
}
function syncConfigured() {
return root.syncEnabled === true && String(root.syncDir || "").trim() !== ""
}
function syncSettingsChanged() {
if (syncConfigured()) {
scheduleSync()
} else {
syncDebounce.stop()
syncRequestedWhileRunning = false
aggregateData = ({})
syncStatusText = ""
syncRevision++
}
}
function updateSyncRunning() {
root.syncRunning = syncMkdirProcess.running || syncScanProcess.running
}
function scheduleSync() {
if (!syncConfigured()) return
syncDebounce.restart()
}
function runSync() {
if (!syncConfigured()) return
if (root.syncRunning) {
syncRequestedWhileRunning = true
return
}
syncRequestedWhileRunning = false
syncStatusText = ""
syncMkdirProcess.command = ["mkdir", "-p", root.syncEffectiveDir]
syncMkdirProcess.running = true
}
function writeSyncSnapshot() {
if (!syncConfigured()) {
finishSyncRun()
return
}
syncSnapshotFile.setText(JSON.stringify(localSnapshot(), null, 2) + "\n")
Qt.callLater(root.startSyncScan)
}
function startSyncScan() {
if (!syncConfigured()) {
finishSyncRun()
return
}
var script = "dir=$0; [[ -d \"$dir\" ]] || exit 0; shopt -s nullglob; for f in \"$dir\"/*.json; do [[ -f \"$f\" ]] || continue; printf '===%s===\\n' \"$f\"; cat \"$f\"; printf '\\n=== EOM ===\\n'; done"
syncScanProcess.command = ["bash", "-c", script, root.syncEffectiveDir]
syncScanProcess.running = true
}
function finishSyncRun() {
if (syncRequestedWhileRunning && syncConfigured()) {
syncRequestedWhileRunning = false
scheduleSync()
}
}
function expandPath(path) {
var value = String(path || "").trim()
if (value === "") return ""
if (value === "~") return home
if (value.indexOf("~/") === 0) return home + value.substring(1)
if (value.indexOf("$HOME/") === 0) return home + value.substring(5)
if (value.charAt(0) !== "/") return home + "/" + value
return value
}
function safeDeviceId(raw) {
var value = String(raw || "").trim()
if (value === "") value = Quickshell.env("HOSTNAME") || root.detectedHostname || Quickshell.env("HOST") || Quickshell.env("USER") || "device"
value = value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "")
if (value === "") value = "device"
return value.length > 80 ? value.substring(0, 80) : value
}
function safeSnapshotFileName(rawFileName, rawDeviceId) {
var value = String(rawFileName || "").trim()
if (value === "") value = safeDeviceId(rawDeviceId) + ".json"
value = value.split("/").pop().replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "")
if (value === "") value = safeDeviceId(rawDeviceId) + ".json"
if (!/\.json$/i.test(value)) value += ".json"
return value.length > 100 ? value.substring(0, 95) + ".json" : value
}
function parseSyncScanOutput(output) {
var lines = String(output || "").split("\n")
var snapshots = []
var currentPath = ""
var currentJson = []
function flush() {
if (currentPath === "") return
var raw = currentJson.join("\n").trim()
try {
var parsed = JSON.parse(raw)
if (parsed && parsed.providers) snapshots.push(parsed)
} catch (e) {
console.warn("model-usage/sync", "Ignoring bad snapshot", currentPath, e)
}
currentPath = ""
currentJson = []
}
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
var start = line.match(/^===(.+)===$/)
if (start && line !== "=== EOM ===") {
flush()
currentPath = start[1]
currentJson = []
continue
}
if (line === "=== EOM ===") {
flush()
continue
}
if (currentPath !== "") currentJson.push(line)
}
flush()
aggregateData = aggregateSnapshots(snapshots)
syncStatusText = ""
syncRevision++
}
function cloneValue(value, fallback) {
if (value === undefined || value === null) return fallback
try {
return JSON.parse(JSON.stringify(value))
} catch (e) {
return fallback
}
}
function numberValue(value) {
var n = Number(value || 0)
return isFinite(n) ? Math.round(n) : 0
}
function dateString(date) {
var y = date.getFullYear()
var m = String(date.getMonth() + 1).padStart(2, "0")
var d = String(date.getDate()).padStart(2, "0")
return y + "-" + m + "-" + d
}
function recentDateStrings() {
var result = []
for (var offset = 6; offset >= 0; offset--) {
var date = new Date()
date.setDate(date.getDate() - offset)
result.push(dateString(date))
}
return result
}
function emptyTokenBucket() {
return { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, cacheCreationInputTokens: 0 }
}
function addObjectNumbers(target, source) {
if (!source) return
for (var key in source) target[key] = numberValue(target[key]) + numberValue(source[key])
}
function aggregateSnapshots(snapshots) {
var dates = recentDateStrings()
var devices = {}
var providers = {}
function providerAcc(id) {
if (providers[id]) return providers[id]
var recentByDay = {}
for (var d = 0; d < dates.length; d++) recentByDay[dates[d]] = 0
providers[id] = {
providerId: id,
providerName: "",
ready: false,
hasLocalStats: false,
todayPrompts: 0,
todaySessions: 0,
todayTotalTokens: 0,
todayTokensByModel: ({}),
recentByDay: recentByDay,
totalPrompts: 0,
totalSessions: 0,
activeDays: 0,
activeDates: ({}),
modelUsage: ({}),
devices: ({})
}
return providers[id]
}
for (var i = 0; i < snapshots.length; i++) {
var snapshot = snapshots[i]
var device = safeDeviceId(snapshot.deviceId || "device")
devices[device] = true
var snapshotProviders = snapshot.providers || {}
for (var providerId in snapshotProviders) {
var stats = snapshotProviders[providerId] || {}
var acc = providerAcc(String(providerId))
acc.devices[device] = true
if (stats.providerName && acc.providerName === "") acc.providerName = String(stats.providerName)
acc.ready = acc.ready || stats.ready === true
acc.hasLocalStats = acc.hasLocalStats || stats.hasLocalStats !== false
acc.todayPrompts += numberValue(stats.todayPrompts)
acc.todaySessions += numberValue(stats.todaySessions)
acc.todayTotalTokens += numberValue(stats.todayTotalTokens)
acc.totalPrompts += numberValue(stats.totalPrompts)
acc.totalSessions += numberValue(stats.totalSessions)
// Active days overlap between machines, so union the dates rather than
// summing counts. Snapshots written before activeDates existed only
// carry a count; the widest one stands in for them.
var activeDates = Array.isArray(stats.activeDates) ? stats.activeDates : []
for (var ad = 0; ad < activeDates.length; ad++) acc.activeDates[String(activeDates[ad])] = true
acc.activeDays = Math.max(acc.activeDays, numberValue(stats.activeDays))
addObjectNumbers(acc.todayTokensByModel, stats.todayTokensByModel || {})
var recent = Array.isArray(stats.recentDays) ? stats.recentDays : []
for (var r = 0; r < recent.length; r++) {
var day = recent[r] || {}
var date = String(day.date || "")
if (acc.recentByDay[date] !== undefined) acc.recentByDay[date] += numberValue(day.messageCount)
}
var usage = stats.modelUsage || {}
for (var modelId in usage) {
var bucket = acc.modelUsage[modelId]
if (!bucket) bucket = acc.modelUsage[modelId] = emptyTokenBucket()
var source = usage[modelId] || {}
bucket.inputTokens += numberValue(source.inputTokens)
bucket.outputTokens += numberValue(source.outputTokens)
bucket.cacheReadInputTokens += numberValue(source.cacheReadInputTokens)
bucket.cacheCreationInputTokens += numberValue(source.cacheCreationInputTokens)
}
}
}
var outProviders = {}
for (var id in providers) {
var acc = providers[id]
var recentDays = []
for (var di = 0; di < dates.length; di++) recentDays.push({ date: dates[di], messageCount: acc.recentByDay[dates[di]] || 0 })
var providerDevices = Object.keys(acc.devices).sort()
outProviders[id] = {
providerId: acc.providerId,
providerName: acc.providerName,
ready: acc.ready || providerDevices.length > 0,
hasLocalStats: acc.hasLocalStats,
todayPrompts: acc.todayPrompts,
todaySessions: acc.todaySessions,
todayTotalTokens: acc.todayTotalTokens,
todayTokensByModel: acc.todayTokensByModel,
recentDays: recentDays,
totalPrompts: acc.totalPrompts,
totalSessions: acc.totalSessions,
activeDays: Math.max(acc.activeDays, Object.keys(acc.activeDates).length),
modelUsage: acc.modelUsage,
deviceCount: providerDevices.length,
devices: providerDevices
}
}
return {
schemaVersion: 1,
updatedAt: new Date().toISOString(),
updatedAtMs: Date.now(),
deviceCount: Object.keys(devices).length,
devices: Object.keys(devices).sort(),
providers: outProviders
}
}
function providerSnapshot(provider) {
return {
providerId: provider.providerId,
providerName: provider.providerName,
ready: provider.ready === true,
hasLocalStats: provider.hasLocalStats !== false,
todayPrompts: numberValue(provider.todayPrompts),
todaySessions: numberValue(provider.todaySessions),
todayTotalTokens: numberValue(provider.todayTotalTokens),
todayTokensByModel: cloneValue(provider.todayTokensByModel, ({})),
recentDays: cloneValue(provider.recentDays, []),
totalPrompts: numberValue(provider.totalPrompts),
totalSessions: numberValue(provider.totalSessions),
activeDays: numberValue(provider.activeDays),
activeDates: cloneValue(provider.activeDates, []),
modelUsage: cloneValue(provider.modelUsage, ({}))
}
}
function localSnapshot() {
var providerMap = {}
for (var i = 0; i < providers.length; i++) {
var provider = providers[i]
if (provider.enabled) providerMap[provider.providerId] = providerSnapshot(provider)
}
return {
schemaVersion: 1,
deviceId: syncEffectiveDeviceId,
updatedAt: new Date().toISOString(),
providers: providerMap
}
}
function syncedStatsFor(providerId) {
var rev = syncRevision
if (!syncConfigured() || !aggregateData || !aggregateData.providers) return null
return aggregateData.providers[providerId] || null
}
function displayProvider(provider) {
var stats = syncedStatsFor(provider.providerId)
var synced = !!stats
var deviceCount = synced ? Number(stats.deviceCount || aggregateData.deviceCount || 0) : 0
return {
providerId: provider.providerId,
providerName: provider.providerName,
providerIcon: provider.providerIcon,
enabled: provider.enabled,
ready: provider.ready || synced,
refreshing: provider.refreshing || root.syncRunning,
lastRefreshedAtMs: Math.max(provider.lastRefreshedAtMs || 0, root.aggregateUpdatedAtMs || 0),
usageStatusText: provider.usageStatusText,
authHelpText: provider.authHelpText,
rateLimitPercent: provider.rateLimitPercent,
rateLimitLabel: provider.rateLimitLabel,
rateLimitResetAt: provider.rateLimitResetAt,
secondaryRateLimitPercent: provider.secondaryRateLimitPercent,
secondaryRateLimitLabel: provider.secondaryRateLimitLabel,
secondaryRateLimitResetAt: provider.secondaryRateLimitResetAt,
tierLabel: provider.tierLabel,
todayPrompts: synced ? numberValue(stats.todayPrompts) : provider.todayPrompts,
todaySessions: synced ? numberValue(stats.todaySessions) : provider.todaySessions,
todayTotalTokens: synced ? numberValue(stats.todayTotalTokens) : provider.todayTotalTokens,
todayTokensByModel: synced ? (stats.todayTokensByModel || ({})) : provider.todayTokensByModel,
recentDays: synced ? (stats.recentDays || []) : provider.recentDays,
totalPrompts: synced ? numberValue(stats.totalPrompts) : provider.totalPrompts,
totalSessions: synced ? numberValue(stats.totalSessions) : provider.totalSessions,
activeDays: synced ? numberValue(stats.activeDays) : provider.activeDays,
modelUsage: synced ? (stats.modelUsage || ({})) : provider.modelUsage,
hasLocalStats: synced ? (stats.hasLocalStats !== false) : provider.hasLocalStats,
syncEnabled: synced,
syncDeviceCount: deviceCount,
syncUpdatedAt: aggregateData && aggregateData.updatedAt ? aggregateData.updatedAt : "",
formatResetTime: function(isoTimestamp) { return provider.formatResetTime(isoTimestamp) }
}
}
function refresh() { refreshAll(true) }
function refreshAll(force) {
for (var i = 0; i < providers.length; i++) {
var p = providers[i]
if (p.enabled && typeof p.refresh === "function") p.refresh(force === true)
}
scheduleSync()
}
// Opening the panel wants the numbers that go stale on the wire, not another
// walk over every transcript on disk. Forcing a whole refresh would do both,
// and re-opening the panel would then rescan the lot each time.
function refreshLimits() {
for (var i = 0; i < providers.length; i++) {
var p = providers[i]
if (p.enabled && typeof p.refreshLimits === "function") p.refreshLimits()
}
}
function formatTokenCount(n) {
if (n === undefined || n === null) return "0"
if (n >= 1e9) return (n / 1e9).toFixed(1) + "B"
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"
return String(n)
}
function modelWordCase(word) {
if (word === "gpt") return "GPT"
return word.charAt(0).toUpperCase() + word.slice(1)
}
// Model ids arrive hyphenated with the version split across segments
// (`claude-opus-4-8`, `gpt-5.6-sol`). Rejoin the numeric run into one
// version and title-case the words around it.
function friendlyModelName(id) {
if (!id) return "Unknown"
var name = String(id).replace(/^claude-/, "").replace(/-\d{8}$/, "")
var parts = name.split("-")
var words = []
var version = []
for (var i = 0; i < parts.length; i++) {
var part = parts[i]
if (part === "") continue
if (/^\d/.test(part)) {
version.push(part)
continue
}
if (version.length > 0) {
words.push(version.join("."))
version = []
}
words.push(modelWordCase(part))
}
if (version.length > 0) words.push(version.join("."))
return words.length > 0 ? words.join(" ") : "Unknown"
}
}