Created
March 12, 2026 01:01
-
-
Save cozuya/44c9ebcd5be2ef61461c8e29acafd478 to your computer and use it in GitHub Desktop.
Claude code usage page tampermonkey script to show more details/do some math
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Claude Usage Budget Helper | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2026-03-12 | |
| // @description Adds weekly usage pace, history, and a compact summary to the Claude usage page. | |
| // @author You | |
| // @match https://claude.ai/settings/usage* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=claude.ai | |
| // @grant none | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| var BADGE_ID = "tm-claude-usage-budget-badge"; | |
| var STYLE_ID = "tm-claude-usage-budget-style"; | |
| var SUMMARY_ID = "tm-claude-usage-summary"; | |
| var HISTORY_STORAGE_KEY = "tm-claude-usage-all-models-history"; | |
| var MAX_HISTORY_ENTRIES = 60; | |
| var HISTORY_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; | |
| var DAY_MS = 24 * 60 * 60 * 1000; | |
| var WEEK_MS = 7 * DAY_MS; | |
| var WEEKDAYS = { | |
| Sun: 0, | |
| Mon: 1, | |
| Tue: 2, | |
| Wed: 3, | |
| Thu: 4, | |
| Fri: 5, | |
| Sat: 6, | |
| }; | |
| function injectStyles() { | |
| if (document.getElementById(STYLE_ID)) { | |
| return; | |
| } | |
| var style = document.createElement("style"); | |
| style.id = STYLE_ID; | |
| style.textContent = [ | |
| "#" + BADGE_ID + "{", | |
| "display:inline-flex;", | |
| "align-items:center;", | |
| "padding:2px 8px;", | |
| "border-radius:999px;", | |
| "font-size:12px;", | |
| "line-height:1.5;", | |
| "font-weight:600;", | |
| "white-space:nowrap;", | |
| "background:rgba(16,163,127,0.12);", | |
| "color:rgb(16,124,93);", | |
| "border:1px solid rgba(16,163,127,0.25);", | |
| "}", | |
| "#" + BADGE_ID + '[data-tone="warn"]{', | |
| "background:rgba(245,158,11,0.12);", | |
| "color:rgb(180,83,9);", | |
| "border-color:rgba(245,158,11,0.25);", | |
| "}", | |
| "#" + BADGE_ID + '[data-tone="danger"]{', | |
| "background:rgba(239,68,68,0.12);", | |
| "color:rgb(185,28,28);", | |
| "border-color:rgba(239,68,68,0.25);", | |
| "}", | |
| "#" + SUMMARY_ID + "{", | |
| "position:fixed;", | |
| "top:16px;", | |
| "left:50%;", | |
| "transform:translateX(-50%);", | |
| "z-index:9999;", | |
| "display:flex;", | |
| "align-items:center;", | |
| "justify-content:center;", | |
| "flex-wrap:wrap;", | |
| "gap:8px;", | |
| "width:min(980px, calc(100vw - 32px));", | |
| "padding:10px 12px;", | |
| "border-radius:999px;", | |
| "background:rgba(255,255,255,0.92);", | |
| "backdrop-filter:blur(10px);", | |
| "color:rgb(15,23,42);", | |
| "border:1px solid rgba(15,23,42,0.08);", | |
| "box-shadow:0 16px 32px -20px rgba(15,23,42,0.35);", | |
| "font-family:ui-sans-serif, system-ui, sans-serif;", | |
| "}", | |
| "@media (prefers-color-scheme: dark){", | |
| "#" + SUMMARY_ID + "{", | |
| "background:rgba(17,24,39,0.88);", | |
| "color:rgb(241,245,249);", | |
| "border-color:rgba(255,255,255,0.08);", | |
| "box-shadow:0 16px 32px -20px rgba(0,0,0,0.55);", | |
| "}", | |
| "}", | |
| "#" + SUMMARY_ID + " .tm-summary-title{", | |
| "font-size:11px;", | |
| "font-weight:700;", | |
| "letter-spacing:0.08em;", | |
| "text-transform:uppercase;", | |
| "opacity:0.7;", | |
| "margin-right:4px;", | |
| "}", | |
| "#" + SUMMARY_ID + " .tm-summary-chip{", | |
| "display:inline-flex;", | |
| "align-items:center;", | |
| "padding:4px 10px;", | |
| "border-radius:999px;", | |
| "background:rgba(15,23,42,0.05);", | |
| "font-size:12px;", | |
| "font-weight:600;", | |
| "line-height:1.4;", | |
| "}", | |
| "@media (prefers-color-scheme: dark){", | |
| "#" + SUMMARY_ID + " .tm-summary-chip{", | |
| "background:rgba(255,255,255,0.08);", | |
| "}", | |
| "}", | |
| "#" + SUMMARY_ID + " .tm-good{", | |
| "color:rgb(22,163,74);", | |
| "}", | |
| "#" + SUMMARY_ID + " .tm-warn{", | |
| "color:rgb(217,119,6);", | |
| "}", | |
| "#" + SUMMARY_ID + " .tm-bad{", | |
| "color:rgb(220,38,38);", | |
| "}", | |
| ].join(""); | |
| document.head.appendChild(style); | |
| } | |
| function normalizeText(text) { | |
| return (text || "").replace(/\s+/g, " ").trim(); | |
| } | |
| function clamp(value, minimum, maximum) { | |
| return Math.min(maximum, Math.max(minimum, value)); | |
| } | |
| function roundToTenths(value) { | |
| return Math.round(value * 10) / 10; | |
| } | |
| function getTone(perDayBudget) { | |
| if (perDayBudget >= 10) { | |
| return "ok"; | |
| } | |
| if (perDayBudget >= 5) { | |
| return "warn"; | |
| } | |
| return "danger"; | |
| } | |
| function getSummaryTone(metrics) { | |
| if (metrics.paceDelta >= 5) { | |
| return "tm-good"; | |
| } | |
| if (metrics.paceDelta >= 0) { | |
| return "tm-warn"; | |
| } | |
| return "tm-bad"; | |
| } | |
| function formatSignedPercent(value) { | |
| var roundedValue = roundToTenths(Math.abs(value)); | |
| var prefix = value >= 0 ? "+" : "-"; | |
| return prefix + roundedValue.toFixed(1) + "%"; | |
| } | |
| function formatDuration(milliseconds) { | |
| if (milliseconds <= 0) { | |
| return "reset imminent"; | |
| } | |
| var totalHours = Math.floor(milliseconds / (60 * 60 * 1000)); | |
| var days = Math.floor(totalHours / 24); | |
| var hours = totalHours % 24; | |
| if (days > 0) { | |
| return days + "d " + hours + "h left"; | |
| } | |
| var minutes = Math.floor((milliseconds % (60 * 60 * 1000)) / (60 * 1000)); | |
| return hours + "h " + minutes + "m left"; | |
| } | |
| function formatSnapshotLabel(timestamp, now) { | |
| var snapshotDate = new Date(timestamp); | |
| var sameDay = snapshotDate.toDateString() === new Date(now).toDateString(); | |
| var timeLabel = snapshotDate.toLocaleTimeString([], { | |
| hour: "numeric", | |
| minute: "2-digit", | |
| }); | |
| if (sameDay) { | |
| return timeLabel; | |
| } | |
| return ( | |
| snapshotDate.toLocaleDateString([], { | |
| month: "short", | |
| day: "numeric", | |
| }) + | |
| " " + | |
| timeLabel | |
| ); | |
| } | |
| function loadHistory() { | |
| try { | |
| var rawHistory = window.localStorage.getItem(HISTORY_STORAGE_KEY); | |
| if (!rawHistory) { | |
| return []; | |
| } | |
| var parsedHistory = JSON.parse(rawHistory); | |
| if (!Array.isArray(parsedHistory)) { | |
| return []; | |
| } | |
| var validHistory = []; | |
| for (var index = 0; index < parsedHistory.length; index += 1) { | |
| var entry = parsedHistory[index]; | |
| if ( | |
| !entry || | |
| typeof entry.recordedAt !== "number" || | |
| typeof entry.remainingPercent !== "number" || | |
| typeof entry.resetAt !== "number" | |
| ) { | |
| continue; | |
| } | |
| validHistory.push({ | |
| recordedAt: entry.recordedAt, | |
| remainingPercent: entry.remainingPercent, | |
| resetAt: entry.resetAt, | |
| }); | |
| } | |
| return validHistory.slice(-MAX_HISTORY_ENTRIES); | |
| } catch (error) { | |
| return []; | |
| } | |
| } | |
| function saveHistory(history) { | |
| try { | |
| window.localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(history.slice(-MAX_HISTORY_ENTRIES))); | |
| } catch (error) { | |
| return; | |
| } | |
| } | |
| function recordSnapshot(remainingPercent, resetDate) { | |
| var history = loadHistory(); | |
| var snapshot = { | |
| recordedAt: Date.now(), | |
| remainingPercent: roundToTenths(remainingPercent), | |
| resetAt: resetDate.getTime(), | |
| }; | |
| var lastSnapshot = history.length > 0 ? history[history.length - 1] : null; | |
| if ( | |
| !lastSnapshot || | |
| lastSnapshot.resetAt !== snapshot.resetAt || | |
| Math.abs(lastSnapshot.remainingPercent - snapshot.remainingPercent) >= 0.1 || | |
| snapshot.recordedAt - lastSnapshot.recordedAt >= HISTORY_REFRESH_INTERVAL_MS | |
| ) { | |
| history.push(snapshot); | |
| saveHistory(history); | |
| } | |
| return history; | |
| } | |
| function getCurrentCycleHistory(history, resetDate) { | |
| var resetTimestamp = resetDate.getTime(); | |
| var currentCycleHistory = []; | |
| for (var index = 0; index < history.length; index += 1) { | |
| if (history[index].resetAt === resetTimestamp) { | |
| currentCycleHistory.push(history[index]); | |
| } | |
| } | |
| return currentCycleHistory; | |
| } | |
| function getHistoryDetails(history) { | |
| if (history.length <= 1) { | |
| return { | |
| historyLine: "History: first snapshot this cycle", | |
| lastChangeLine: "Last change: waiting for another snapshot", | |
| }; | |
| } | |
| var lastSnapshot = history[history.length - 1]; | |
| var previousSnapshot = history[history.length - 2]; | |
| var delta = roundToTenths(lastSnapshot.remainingPercent - previousSnapshot.remainingPercent); | |
| var elapsedMs = lastSnapshot.recordedAt - previousSnapshot.recordedAt; | |
| var recentSnapshots = history.slice(-4); | |
| var historyParts = []; | |
| for (var index = 0; index < recentSnapshots.length; index += 1) { | |
| var snapshot = recentSnapshots[index]; | |
| var isLatest = index === recentSnapshots.length - 1; | |
| historyParts.push( | |
| snapshot.remainingPercent.toFixed(1) + | |
| "%@" + | |
| (isLatest ? "now" : formatSnapshotLabel(snapshot.recordedAt, lastSnapshot.recordedAt)) | |
| ); | |
| } | |
| return { | |
| historyLine: "History: " + historyParts.join(" -> "), | |
| lastChangeLine: | |
| "Last change: " + | |
| formatSignedPercent(delta) + | |
| " since " + | |
| formatSnapshotLabel(previousSnapshot.recordedAt, lastSnapshot.recordedAt) + | |
| " (" + | |
| formatDuration(elapsedMs).replace(" left", "") + | |
| ")", | |
| }; | |
| } | |
| function findAllModelsCard() { | |
| var divs = document.querySelectorAll("div"); | |
| var candidates = []; | |
| for (var index = 0; index < divs.length; index += 1) { | |
| var element = divs[index]; | |
| var paragraphs = element.querySelectorAll("p"); | |
| var hasAllModelsLabel = false; | |
| var hasResetText = false; | |
| var hasUsedText = false; | |
| for (var paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex += 1) { | |
| var text = normalizeText(paragraphs[paragraphIndex].textContent); | |
| if (text === "All models") { | |
| hasAllModelsLabel = true; | |
| } | |
| if (/^Resets\s+[A-Z][a-z]{2}\s+\d{1,2}:\d{2}\s+(AM|PM)$/i.test(text)) { | |
| hasResetText = true; | |
| } | |
| if (/^\d+(?:\.\d+)?%\s*used$/i.test(text)) { | |
| hasUsedText = true; | |
| } | |
| } | |
| if (hasAllModelsLabel && hasResetText && hasUsedText) { | |
| candidates.push(element); | |
| } | |
| } | |
| if (candidates.length === 0) { | |
| return null; | |
| } | |
| candidates.sort(function (left, right) { | |
| return normalizeText(left.textContent).length - normalizeText(right.textContent).length; | |
| }); | |
| return candidates[0]; | |
| } | |
| function findUsedElement(card) { | |
| var paragraphs = card.querySelectorAll("p"); | |
| for (var index = 0; index < paragraphs.length; index += 1) { | |
| var text = normalizeText(paragraphs[index].textContent); | |
| if (/^\d+(?:\.\d+)?%\s*used$/i.test(text)) { | |
| return paragraphs[index]; | |
| } | |
| } | |
| return null; | |
| } | |
| function getRemainingPercent(usedElement) { | |
| var text = normalizeText(usedElement.textContent); | |
| var match = text.match(/(\d+(?:\.\d+)?)%\s*used/i); | |
| if (!match) { | |
| return null; | |
| } | |
| return 100 - Number(match[1]); | |
| } | |
| function findResetText(card) { | |
| var paragraphs = card.querySelectorAll("p"); | |
| for (var index = 0; index < paragraphs.length; index += 1) { | |
| var text = normalizeText(paragraphs[index].textContent); | |
| if (/^Resets\s+[A-Z][a-z]{2}\s+\d{1,2}:\d{2}\s+(AM|PM)$/i.test(text)) { | |
| return text; | |
| } | |
| } | |
| return null; | |
| } | |
| function parseResetDate(resetText) { | |
| var match = resetText.match(/^Resets\s+([A-Z][a-z]{2})\s+(\d{1,2}):(\d{2})\s+(AM|PM)$/i); | |
| if (!match) { | |
| return null; | |
| } | |
| var weekdayIndex = WEEKDAYS[match[1]]; | |
| if (typeof weekdayIndex !== "number") { | |
| return null; | |
| } | |
| var hour = Number(match[2]); | |
| var minute = Number(match[3]); | |
| var meridiem = match[4].toUpperCase(); | |
| if (meridiem === "PM" && hour !== 12) { | |
| hour += 12; | |
| } | |
| if (meridiem === "AM" && hour === 12) { | |
| hour = 0; | |
| } | |
| var now = new Date(); | |
| var resetDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0); | |
| var dayDelta = (weekdayIndex - now.getDay() + 7) % 7; | |
| resetDate.setDate(resetDate.getDate() + dayDelta); | |
| if (resetDate.getTime() <= now.getTime()) { | |
| resetDate.setDate(resetDate.getDate() + 7); | |
| } | |
| return resetDate; | |
| } | |
| function getMetrics(remainingPercent, resetDate) { | |
| var now = Date.now(); | |
| var resetTimestamp = resetDate.getTime(); | |
| var timeLeftMs = Math.max(0, resetTimestamp - now); | |
| var daysLeft = timeLeftMs / DAY_MS; | |
| var cycleStartMs = resetTimestamp - WEEK_MS; | |
| var elapsedMs = clamp(now - cycleStartMs, 0, WEEK_MS); | |
| var daysElapsed = elapsedMs / DAY_MS; | |
| var usedPercent = 100 - remainingPercent; | |
| var idealRemaining = clamp((timeLeftMs / WEEK_MS) * 100, 0, 100); | |
| var paceDelta = remainingPercent - idealRemaining; | |
| var perDayBudget = daysLeft > 0 ? remainingPercent / daysLeft : remainingPercent; | |
| var avgDailyUsed = daysElapsed > 0 ? usedPercent / daysElapsed : 0; | |
| return { | |
| avgDailyUsed: avgDailyUsed, | |
| idealRemaining: idealRemaining, | |
| paceDelta: paceDelta, | |
| perDayBudget: perDayBudget, | |
| resetTimestamp: resetTimestamp, | |
| timeLeftMs: timeLeftMs, | |
| usedPercent: usedPercent, | |
| }; | |
| } | |
| function upsertBadge(usedElement, metrics) { | |
| var badge = document.getElementById(BADGE_ID); | |
| if (!badge) { | |
| badge = document.createElement("span"); | |
| badge.id = BADGE_ID; | |
| } | |
| badge.textContent = metrics.perDayBudget.toFixed(1) + "%/day | " + formatSignedPercent(metrics.paceDelta) + " pace"; | |
| badge.title = | |
| "Ideal remaining now: " + | |
| metrics.idealRemaining.toFixed(1) + | |
| "% | Avg used: " + | |
| metrics.avgDailyUsed.toFixed(1) + | |
| "%/day | Reset: " + | |
| new Date(metrics.resetTimestamp).toLocaleString(); | |
| var tone = getTone(metrics.perDayBudget); | |
| if (tone === "ok") { | |
| badge.removeAttribute("data-tone"); | |
| } else { | |
| badge.setAttribute("data-tone", tone); | |
| } | |
| if (badge.parentElement !== usedElement.parentElement) { | |
| usedElement.insertAdjacentElement("afterend", badge); | |
| } | |
| } | |
| function upsertSummary(metrics, currentCycleHistory, remainingPercent) { | |
| var summary = document.getElementById(SUMMARY_ID); | |
| if (!summary) { | |
| summary = document.createElement("div"); | |
| summary.id = SUMMARY_ID; | |
| document.body.appendChild(summary); | |
| } | |
| var historyDetails = getHistoryDetails(currentCycleHistory); | |
| var paceLabel = metrics.paceDelta >= 0 ? "ahead" : "behind"; | |
| var toneClass = getSummaryTone(metrics); | |
| summary.innerHTML = | |
| '<div class="tm-summary-title">Claude All Models</div>' + | |
| '<div class="tm-summary-chip">' + | |
| remainingPercent.toFixed(1) + | |
| "% left</div>" + | |
| '<div class="tm-summary-chip">' + | |
| metrics.usedPercent.toFixed(1) + | |
| "% used</div>" + | |
| '<div class="tm-summary-chip">' + | |
| metrics.perDayBudget.toFixed(1) + | |
| "%/day budget</div>" + | |
| '<div class="tm-summary-chip"><span class="' + | |
| toneClass + | |
| "\">" + | |
| paceLabel + | |
| " " + | |
| formatSignedPercent(metrics.paceDelta) + | |
| "</span></div>" + | |
| '<div class="tm-summary-chip">Ideal ' + | |
| metrics.idealRemaining.toFixed(1) + | |
| "% now</div>" + | |
| '<div class="tm-summary-chip">Avg use ' + | |
| metrics.avgDailyUsed.toFixed(1) + | |
| "%/day</div>" + | |
| '<div class="tm-summary-chip">' + | |
| formatDuration(metrics.timeLeftMs) + | |
| "</div>" + | |
| '<div class="tm-summary-chip">' + | |
| historyDetails.lastChangeLine + | |
| "</div>" + | |
| '<div class="tm-summary-chip">' + | |
| historyDetails.historyLine + | |
| "</div>"; | |
| } | |
| function refreshUsage() { | |
| var card = findAllModelsCard(); | |
| if (!card) { | |
| return; | |
| } | |
| var usedElement = findUsedElement(card); | |
| if (!usedElement) { | |
| return; | |
| } | |
| var remainingPercent = getRemainingPercent(usedElement); | |
| if (remainingPercent === null) { | |
| return; | |
| } | |
| var resetText = findResetText(card); | |
| if (!resetText) { | |
| return; | |
| } | |
| var resetDate = parseResetDate(resetText); | |
| if (!resetDate) { | |
| return; | |
| } | |
| var history = recordSnapshot(remainingPercent, resetDate); | |
| var currentCycleHistory = getCurrentCycleHistory(history, resetDate); | |
| var metrics = getMetrics(remainingPercent, resetDate); | |
| upsertBadge(usedElement, metrics); | |
| upsertSummary(metrics, currentCycleHistory, remainingPercent); | |
| } | |
| var refreshTimer = null; | |
| function scheduleRefresh() { | |
| if (refreshTimer !== null) { | |
| clearTimeout(refreshTimer); | |
| } | |
| refreshTimer = setTimeout(function () { | |
| refreshTimer = null; | |
| refreshUsage(); | |
| }, 150); | |
| } | |
| function init() { | |
| injectStyles(); | |
| refreshUsage(); | |
| var observer = new MutationObserver(function () { | |
| scheduleRefresh(); | |
| }); | |
| observer.observe(document.documentElement, { | |
| childList: true, | |
| subtree: true, | |
| characterData: true, | |
| }); | |
| window.addEventListener("popstate", scheduleRefresh); | |
| window.addEventListener("hashchange", scheduleRefresh); | |
| } | |
| init(); | |
| })(); |
Author
cozuya
commented
Mar 12, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment