Files
Panama/config/dot/quickshell/services/AgentUsage.qml
T
Gabriel Brown 8b96d907a1 Show how much of the subscription is gone, without risking the session
The last of Section F, and the only thing in Panama that reads an
authentication token, so most of the design is about that rather than
about the number.

It never refreshes the token and never writes to the credentials file.
That token expires roughly hourly and Claude Code refreshes it on
demand; if this refreshed it too, two processes would be rotating one
credential, and a rotation invalidates the other holder's copy. The
failure mode is being silently signed out of Claude Code by a status
widget, which no bar indicator is worth. So it reads the token, uses it
while valid, and reports "waiting for Claude Code to refresh" when not
-- which covers the case that matters, because while you are using
Claude Code the token is fresh, and while you are not there is nothing
to watch.

The token never reaches argv either: curl takes the Authorization
header on stdin through --config, because a header passed as an
argument sits in /proc/<pid>/cmdline for the length of the request.
Same rule the password and MOK paths already follow. And it never
reaches the output: the record carries percentages and timestamps and
nothing else. Both are pinned, and both were checked by sabotaging the
collector to pass -H and watching the contract name it.

Off by default. It is a coding-tool readout, not something a
general-purpose desktop shows without being asked, and it hides unless
the collector has real numbers rather than displaying "unknown".
2026-08-22 08:33:39 -04:00

93 lines
3.6 KiB
QML

pragma Singleton
// ─────────────────────────────────────────────────────────────────────────────
// How much of the Claude subscription this account has used.
//
// The collector writes one display-ready record and this only ever reads it.
// That split is the point: adding a second agent later is a collector, not a
// change here or in the widget, and nothing in QML ever sees a credential.
//
// The record carries its own status, so this can tell the three cases apart:
// the collector has never run, it ran and the session was stale, or it has
// real numbers. The widget hides for the first two, which is right -- a bar
// indicator that says "unknown" is worse than an empty space.
// ─────────────────────────────────────────────────────────────────────────────
import Quickshell
import Quickshell.Io
import QtQuick
import qs.config
Singleton {
id: root
// "ok" | "stale" | "unavailable" | "" (never collected)
property string status: ""
property string detail: ""
// Percentages, 0-100, or -1 when the endpoint did not report one.
property int fiveHourUsed: -1
property int weekUsed: -1
property string weekResetsAt: ""
property string tier: ""
readonly property bool available: root.status === "ok"
&& (root.fiveHourUsed >= 0 || root.weekUsed >= 0)
// The number worth showing when there is only room for one: whichever
// window is closer to its limit is the one about to interrupt you.
readonly property int headline: Math.max(root.fiveHourUsed, root.weekUsed)
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-agent-usage"
readonly property string statePath:
(Quickshell.env("XDG_STATE_HOME") || `${Quickshell.env("HOME")}/.local/state`)
+ "/panama/agent-usage.json"
// Minutes, never a repaint. Usage moves slowly and the collector makes a
// network call; anything faster would be spending someone's battery to
// watch a number that changes a few times an hour.
Timer {
interval: 5 * 60 * 1000
running: Settings.showAgentUsage
repeat: true
triggeredOnStart: true
onTriggered: collect.running = true
}
Process {
id: collect
command: [root.helperPath]
onExited: record.reload()
}
FileView {
id: record
path: root.statePath
printErrors: false
watchChanges: true
onFileChanged: this.reload()
onLoaded: {
try {
const parsed = JSON.parse(this.text());
root.status = String(parsed.status ?? "");
root.detail = String(parsed.detail ?? "");
const usage = parsed.usage;
if (usage) {
root.fiveHourUsed = Number.isFinite(usage.fiveHour?.used)
? usage.fiveHour.used : -1;
root.weekUsed = Number.isFinite(usage.week?.used)
? usage.week.used : -1;
root.weekResetsAt = String(usage.week?.resetsAt ?? "");
root.tier = String(usage.tier ?? "");
} else {
root.fiveHourUsed = -1;
root.weekUsed = -1;
}
} catch (error) {
root.status = "";
}
}
onLoadFailed: root.status = "";
}
}