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>
144 lines
4.3 KiB
QML
144 lines
4.3 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
|
|
Item {
|
|
id: root
|
|
visible: false
|
|
|
|
property string providerId: "codex"
|
|
property string providerName: "Codex"
|
|
property string providerIcon: "ai"
|
|
property bool enabled: false
|
|
property bool ready: false
|
|
property bool refreshing: false
|
|
property double lastRefreshedAtMs: 0
|
|
|
|
property real rateLimitPercent: -1
|
|
property string rateLimitLabel: ""
|
|
property string rateLimitResetAt: ""
|
|
property real secondaryRateLimitPercent: -1
|
|
property string secondaryRateLimitLabel: ""
|
|
property string secondaryRateLimitResetAt: ""
|
|
|
|
property int todayPrompts: 0
|
|
property int todaySessions: 0
|
|
property real todayTotalTokens: 0
|
|
property var todayTokensByModel: ({})
|
|
|
|
property var recentDays: []
|
|
property int totalPrompts: 0
|
|
property int totalSessions: 0
|
|
property int activeDays: 0
|
|
property var activeDates: []
|
|
property var modelUsage: ({})
|
|
|
|
property string tierLabel: ""
|
|
property string usageStatusText: ""
|
|
property string authHelpText: "Run `codex login` to authenticate."
|
|
property bool hasLocalStats: true
|
|
|
|
property string configModel: ""
|
|
property var providerSettings: ({})
|
|
|
|
readonly property string scannerPath: String(Qt.resolvedUrl("../scripts/codex_usage_scanner.py")).replace("file://", "")
|
|
|
|
Process {
|
|
id: usageScanner
|
|
command: ["python3", root.scannerPath]
|
|
running: false
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.parseScannerOutput(text)
|
|
}
|
|
|
|
onExited: root.finishRefresh()
|
|
|
|
stderr: StdioCollector {
|
|
onStreamFinished: if (text.trim() !== "") console.warn("model-usage/codex", text.trim())
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: 5 * 60 * 1000
|
|
running: root.enabled
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: root.refresh()
|
|
}
|
|
|
|
onEnabledChanged: if (enabled) refresh()
|
|
|
|
function finishRefresh() {
|
|
root.refreshing = false
|
|
root.lastRefreshedAtMs = Date.now()
|
|
}
|
|
|
|
function refresh(force) {
|
|
if (usageScanner.running)
|
|
return
|
|
root.refreshing = true
|
|
usageScanner.running = true
|
|
}
|
|
|
|
// Codex reports limits and local stats from the same scanner run, and that
|
|
// run has no expensive mode to skip, so there is nothing cheaper to do.
|
|
function refreshLimits() { refresh() }
|
|
|
|
function parseScannerOutput(output) {
|
|
const raw = String(output || "").trim()
|
|
if (raw === "")
|
|
return
|
|
|
|
try {
|
|
const data = JSON.parse(raw.split("\n").pop())
|
|
root.ready = !!data.ready
|
|
root.hasLocalStats = data.hasLocalStats !== false
|
|
|
|
root.todayPrompts = data.todayPrompts || 0
|
|
root.todaySessions = data.todaySessions || 0
|
|
root.todayTotalTokens = data.todayTotalTokens || 0
|
|
root.todayTokensByModel = data.todayTokensByModel || ({})
|
|
root.recentDays = data.recentDays || []
|
|
root.totalPrompts = data.totalPrompts || 0
|
|
root.totalSessions = data.totalSessions || 0
|
|
root.activeDays = data.activeDays || 0
|
|
root.activeDates = data.activeDates || []
|
|
root.modelUsage = data.modelUsage || ({})
|
|
|
|
root.rateLimitPercent = data.rateLimitPercent ?? -1
|
|
root.rateLimitLabel = data.rateLimitLabel || ""
|
|
root.rateLimitResetAt = data.rateLimitResetAt || ""
|
|
root.secondaryRateLimitPercent = data.secondaryRateLimitPercent ?? -1
|
|
root.secondaryRateLimitLabel = data.secondaryRateLimitLabel || ""
|
|
root.secondaryRateLimitResetAt = data.secondaryRateLimitResetAt || ""
|
|
|
|
root.tierLabel = data.tierLabel || ""
|
|
root.usageStatusText = data.usageStatusText || ""
|
|
root.authHelpText = data.authHelpText || "Run `codex login` to authenticate."
|
|
} catch (e) {
|
|
console.error("model-usage/codex", "Failed to parse scanner output:", e, raw)
|
|
root.usageStatusText = "Codex scan failed"
|
|
root.authHelpText = String(e)
|
|
root.ready = true
|
|
}
|
|
}
|
|
|
|
function formatResetTime(isoTimestamp) {
|
|
if (!isoTimestamp)
|
|
return ""
|
|
const reset = new Date(isoTimestamp)
|
|
const now = new Date()
|
|
const diffMs = reset.getTime() - now.getTime()
|
|
if (diffMs <= 0)
|
|
return "now"
|
|
const hours = Math.floor(diffMs / 3600000)
|
|
const mins = Math.floor((diffMs % 3600000) / 60000)
|
|
if (hours > 24)
|
|
return Math.floor(hours / 24) + "d " + (hours % 24) + "h"
|
|
if (hours > 0)
|
|
return hours + "h " + mins + "m"
|
|
return mins + "m"
|
|
}
|
|
}
|