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".
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user