Simplify model usage panel metrics

This commit is contained in:
David Heinemeier Hansson
2026-07-29 19:26:55 -07:00
parent 19a6eedb11
commit 3304b79c92
2 changed files with 33 additions and 198 deletions
+28 -190
View File
@@ -32,7 +32,7 @@ Panel {
property bool cursorActive: false
// Countdowns, pace, and "updated" all read this instead of Date.now() so the
// Countdowns and "updated" read this instead of Date.now() so the
// panel keeps telling the truth while it sits open.
property double nowMs: Date.now()
@@ -58,7 +58,7 @@ Panel {
//
// Both providers report the same two shapes: a short rolling session window
// and a long weekly one. Everything below normalizes them into one record so
// the meters, the pace math, and the hero all speak a single language.
// the meters and the hero speak a single language.
// Claude spells its windows out ("Session (5-hour)"), Codex abbreviates
// them ("5h window", "30m window"). Both have to land on the same record.
@@ -87,19 +87,9 @@ Panel {
return plain === "" ? "Limit" : plain
}
function windowSpanLabel(spanMs) {
if (spanMs <= 0) return ""
if (spanMs >= 24 * 3600 * 1000) return Math.round(spanMs / (24 * 3600 * 1000)) + "d"
if (spanMs >= 3600 * 1000) return Math.round(spanMs / (3600 * 1000)) + "h"
return Math.round(spanMs / 60000) + "m"
}
function limitWindow(label, percent, resetAt) {
var spanMs = windowSpanMs(label)
return {
title: windowTitle(label),
span: windowSpanLabel(spanMs),
spanMs: spanMs,
percent: Number(percent),
resetAt: String(resetAt || "")
}
@@ -130,48 +120,6 @@ Panel {
return isFinite(ms) ? ms - root.nowMs : -1
}
// Where the clock says you should be, versus where you actually are.
function paceFor(w) {
if (!w || w.percent < 0 || w.spanMs <= 0) return null
var remaining = resetMsFor(w)
if (remaining <= 0 || remaining > w.spanMs) return null
var elapsed = w.spanMs - remaining
var expected = clamp(elapsed / w.spanMs, 0, 1)
var used = clamp(w.percent, 0, 1)
var projected = expected > 0 ? used / expected : -1
var runsOutMs = (used > 0 && projected > 1) ? (elapsed / used) * (1 - used) : -1
return {
expected: expected,
diff: used - expected,
projected: projected,
runsOutMs: runsOutMs < remaining ? runsOutMs : -1,
remaining: remaining
}
}
function paceText(w) {
var pace = paceFor(w)
if (!pace) return ""
if (pace.runsOutMs >= 0) return "Runs out in " + formatDuration(pace.runsOutMs)
if (Math.abs(pace.diff) <= 0.02) return "On pace"
if (pace.diff > 0) return Math.round(pace.diff * 100) + "% ahead of pace"
return Math.round(-pace.diff * 100) + "% in reserve"
}
function paceIsUrgent(w) {
var pace = paceFor(w)
return !!pace && pace.runsOutMs >= 0
}
function paceDetail(w) {
var pace = paceFor(w)
if (!pace) return ""
var lines = "Expected " + Math.round(pace.expected * 100) + "% used by now"
if (pace.projected >= 0) lines += " · tracking to " + Math.round(pace.projected * 100) + "% by reset"
return lines
}
function formatDuration(ms) {
if (!(ms > 0)) return "now"
var minutes = Math.floor(ms / 60000)
@@ -182,14 +130,10 @@ Panel {
return Math.max(1, minutes) + "m"
}
function percentText(value) {
return value < 0 ? "—" : Math.round(value * 100) + "%"
}
// ---------------------------------------------------------------- content
// The plan you pay for, under the name of the tool it pays for. Limits and
// pace live in their own section; the hero just says what this is.
// The plan you pay for, under the name of the tool it pays for. Limits live
// in their own section; the hero just says what this is.
function heroMeta(p) {
if (!p) return ""
if (String(p.usageStatusText || "") !== "") return p.usageStatusText
@@ -233,13 +177,6 @@ Panel {
return text
}
function weekTotal(p) {
var days = p ? (p.recentDays || []) : []
var total = 0
for (var i = 0; i < days.length; i++) total += Number(days[i].messageCount || 0)
return total
}
function weekPeak(p) {
var days = p ? (p.recentDays || []) : []
var peak = 0
@@ -269,18 +206,6 @@ Panel {
return rows.slice(0, 4)
}
// Every model, not just the rows that fit — the section header sums the lot.
function modelTotalTokens(p) {
var usageByModel = p ? (p.modelUsage || {}) : {}
var total = 0
for (var id in usageByModel) {
var bucket = usageByModel[id] || {}
total += Number(bucket.inputTokens || 0) + Number(bucket.outputTokens || 0)
+ Number(bucket.cacheReadInputTokens || 0) + Number(bucket.cacheCreationInputTokens || 0)
}
return total
}
function modelTooltip(row) {
if (!row) return ""
return "In " + usage.formatTokenCount(row.input)
@@ -385,7 +310,7 @@ Panel {
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(380))
// Taller than the control panels on purpose: this one is a dashboard, and
// the whole point is reading limits, pace, and history without scrolling.
// the whole point is reading limits and history without scrolling.
contentHeight: panel.fittedContentHeight(column.implicitHeight, Style.space(640))
PanelKeyCatcher {
@@ -560,29 +485,11 @@ Panel {
readonly property var days: root.provider ? (root.provider.recentDays || []) : []
readonly property real peak: Math.max(1, root.weekPeak(root.provider))
Item {
PanelSectionHeader {
width: parent.width
implicitHeight: Math.max(usageHeader.implicitHeight, usageTotalText.implicitHeight)
PanelSectionHeader {
id: usageHeader
text: "USAGE THIS WEEK"
foreground: root.foreground
fontFamily: root.fontFamily
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: usageTotalText
text: usage.formatTokenCount(root.weekTotal(root.provider)) + " tokens · 7 days"
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
text: "TOKENS BY DAY"
foreground: root.foreground
fontFamily: root.fontFamily
}
Repeater {
@@ -614,34 +521,11 @@ Panel {
width: parent.width
spacing: Style.spacing.md
Item {
PanelSectionHeader {
width: parent.width
implicitHeight: Math.max(modelHeader.implicitHeight, modelTotals.implicitHeight)
PanelSectionHeader {
id: modelHeader
text: "USAGE BY MODEL"
foreground: root.foreground
fontFamily: root.fontFamily
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: modelTotals
text: {
if (!root.provider) return ""
var days = Number(root.provider.activeDays || 0)
return usage.formatTokenCount(root.modelTotalTokens(root.provider)) + " tokens"
+ (days > 0 ? " · " + days + " day" + (days === 1 ? "" : "s") : "")
}
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
text: "TOKENS BY MODEL"
foreground: root.foreground
fontFamily: root.fontFamily
}
Repeater {
@@ -674,12 +558,11 @@ Panel {
}
}
// A limit window: label, percent, meter with the pace notch, reset and pace.
// A limit window: label and percentage, meter, and reset countdown.
component LimitRow: Column {
id: limitRow
property var window: null
readonly property var pace: root.paceFor(window)
readonly property bool alarming: window && window.percent >= 0.9
spacing: Style.space(6)
@@ -690,9 +573,7 @@ Panel {
Text {
id: limitLabel
text: limitRow.window
? limitRow.window.title + (limitRow.window.span !== "" ? " · " + limitRow.window.span : "")
: ""
text: limitRow.window ? limitRow.window.title : ""
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
@@ -702,11 +583,12 @@ Panel {
Text {
id: limitValue
text: root.percentText(limitRow.window ? limitRow.window.percent : -1)
text: limitRow.window && limitRow.window.percent >= 0
? Math.round(limitRow.window.percent * 100) + "%"
: "—"
color: limitRow.alarming ? root.urgent : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.subtitle
font.bold: true
font.pixelSize: Style.font.caption
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
@@ -715,49 +597,27 @@ Panel {
Meter {
width: parent.width
value: limitRow.window ? limitRow.window.percent : -1
marker: limitRow.pace ? limitRow.pace.expected : -1
alarming: limitRow.alarming
tooltipText: root.paceDetail(limitRow.window)
}
Item {
Text {
id: resetText
width: parent.width
implicitHeight: Math.max(resetText.implicitHeight, paceText.implicitHeight)
Text {
id: resetText
text: {
var remaining = root.resetMsFor(limitRow.window)
return remaining > 0 ? "Resets in " + root.formatDuration(remaining) : ""
}
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: paceText
text: root.paceText(limitRow.window)
color: root.paceIsUrgent(limitRow.window) ? root.urgent : root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: {
var remainingMs = root.resetMsFor(limitRow.window)
return remainingMs > 0 ? "Resets in " + root.formatDuration(remainingMs) : ""
}
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
}
}
// Rounded track with a fill, plus an optional notch marking where an evenly
// paced week would have you right now.
// Rounded track showing the percentage of the allowance used.
component Meter: Item {
id: meter
property real value: -1
property real marker: -1
property bool alarming: false
property string tooltipText: ""
property real thickness: Math.max(Style.space(4), Math.round(Style.spacing.controlHeight * 0.14))
implicitHeight: thickness
@@ -782,28 +642,6 @@ Panel {
}
}
Rectangle {
visible: meter.marker >= 0 && meter.marker <= 1
width: Math.max(1, Style.space(2))
height: meterTrack.height + Style.space(4)
radius: width / 2
anchors.verticalCenter: meterTrack.verticalCenter
x: root.clamp(meterTrack.width * meter.marker - width / 2, 0, Math.max(0, meterTrack.width - width))
color: meter.marker <= meter.value ? root.surface : root.alpha(root.foreground, 0.5)
}
MouseArea {
id: meterHover
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
PanelToolTip {
visible: meter.tooltipText !== "" && meterHover.containsMouse
text: meter.tooltipText
fontFamily: root.fontFamily
}
}
// One row per day: label, bar, tokens. Today is picked out in full
+5 -8
View File
@@ -11,15 +11,12 @@ adapter per subscription.
Auth and endpoint problems replace the plan line and repeat in a card.
- **Subscription switch** — one chip per enabled provider (`h`/`l` or click).
It appears only when more than one provider is enabled.
- **Limits** — a meter per window (session, weekly). The notch on the meter
marks where an evenly paced window would have you right now, so the gap
between the fill and the notch is the whole story: fill behind the notch
means budget in reserve, fill past it means you are burning faster than
the clock and the row says when it runs out.
- **Usage this week** — one row per day for the last week: day, bar, tokens, with today
- **Limits** — the percentage of each allowance used, a matching meter, and
the time until the session or weekly window resets.
- **Tokens by day** — one row per day for the last week: day, bar, tokens, with today
bolded at the bottom. Hover today for its prompt and session count.
- **Usage by model** — total tokens and active days in the header, then
tokens per model with the bar behind each row scaled to the heaviest model,
- **Tokens by model** — tokens per model with the bar behind each row scaled
to the heaviest model,
the same way the weekly chart scales to its busiest day. Hover for the
input / output / cache split.