Refresh the look and layout of the model-usage plugin

This commit is contained in:
David Heinemeier Hansson
2026-07-28 09:18:24 -07:00
parent 234ac7b3b6
commit 9b693cca63
11 changed files with 1153 additions and 1293 deletions
+43 -21
View File
@@ -26,23 +26,22 @@ Item {
}
property var providers: [claudeProvider, codexProvider]
// A subscription earns a place in the panel by being both switched on in
// settings and actually present on this machine — nobody wants a Codex tab
// full of zeroes on a box that has never run Codex.
property var enabledProviders: {
var rev = syncRevision
var running = syncRunning
var result = []
if (claudeProvider.enabled) result.push(displayProvider(claudeProvider))
if (codexProvider.enabled) result.push(displayProvider(codexProvider))
if (claudeProvider.enabled && claudeProvider.installed) result.push(displayProvider(claudeProvider))
if (codexProvider.enabled && codexProvider.installed) result.push(displayProvider(codexProvider))
return result
}
property int activeIndex: 0
property var activeProvider: enabledProviders.length > 0 ? enabledProviders[Math.min(activeIndex, enabledProviders.length - 1)] : null
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 string barDisplayMode: setting("barDisplayMode", "active")
property int barCycleIntervalSec: Math.max(1, Number(setting("barCycleIntervalSec", 5)))
property string barMetric: setting("barMetric", "prompts")
property int refreshIntervalSec: Math.max(30, Number(setting("refreshIntervalSec", 900)))
property var syncModeSetting: setting("syncMode", setting("syncEnabled", false))
@@ -75,13 +74,6 @@ Item {
return value === undefined || value === null ? fallback : value
}
Timer {
interval: root.barCycleIntervalSec * 1000
running: root.barDisplayMode === "cycle" && root.enabledProviders.length > 1
repeat: true
onTriggered: root.activeIndex = (root.activeIndex + 1) % root.enabledProviders.length
}
Timer {
interval: root.refreshIntervalSec * 1000
running: true
@@ -147,10 +139,6 @@ Item {
onLoaded: root.detectedHostname = String(text() || "").trim()
}
onEnabledProvidersChanged: {
if (enabledProviders.length === 0 || activeIndex >= enabledProviders.length) activeIndex = 0
}
function providerEnabled(id) {
if (!settings || !settings.providers || !settings.providers[id]) return id === "claude" || id === "codex"
return settings.providers[id].enabled !== false
@@ -355,6 +343,8 @@ Item {
recentByDay: recentByDay,
totalPrompts: 0,
totalSessions: 0,
activeDays: 0,
activeDates: ({}),
modelUsage: ({}),
devices: ({})
}
@@ -378,6 +368,12 @@ Item {
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 : []
@@ -418,6 +414,7 @@ Item {
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
@@ -447,6 +444,8 @@ Item {
recentDays: cloneValue(provider.recentDays, []),
totalPrompts: numberValue(provider.totalPrompts),
totalSessions: numberValue(provider.totalSessions),
activeDays: numberValue(provider.activeDays),
activeDates: cloneValue(provider.activeDates, []),
modelUsage: cloneValue(provider.modelUsage, ({}))
}
}
@@ -502,6 +501,7 @@ Item {
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,
@@ -531,12 +531,34 @@ Item {
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("-")
if (parts.length >= 3) return parts[0].charAt(0).toUpperCase() + parts[0].slice(1) + " " + parts[1] + "." + parts[2]
if (parts.length === 2) return parts[0].charAt(0).toUpperCase() + parts[0].slice(1) + " " + parts[1]
return name.charAt(0).toUpperCase() + name.slice(1)
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"
}
}