A compact floating widget that displays your Claude AI usage limits in real time on chat pages
// ==UserScript==
// @name CLAUSAGE
// @namespace https://github.com/Yorkian/CLAUSAGE
// @version 1.2.2
// @description A compact floating widget that displays your Claude AI usage limits in real time on chat pages
// @author Yorkian
// @homepage https://github.com/Yorkian/CLAUSAGE
// @homepageURL https://github.com/Yorkian/CLAUSAGE
// @supportURL https://github.com/Yorkian/CLAUSAGE/issues
// @icon https://raw.githubusercontent.com/Yorkian/CLAUSAGE/main/icon48.png
// @match https://claude.ai/*
// @run-at document-idle
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
/* eslint-disable */
(function () {
'use strict';
// Do not run inside the hidden same-origin iframe we create for usage polling.
// This short-circuit prevents recursive widget creation and recursive iframes.
if (window.top !== window.self) return;
// Inject styles (single call — keeps the stylesheet in one place).
const CUW_CSS = `/* ── Claude Usage Widget ── */
#claude-usage-widget {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 2147483647;
width: 252px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
color: #1a1a1a;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 10px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.10), 0 1px 4px rgba(0, 0, 0, 0.06);
overflow: hidden;
user-select: none;
transition: box-shadow 0.2s, opacity 0.2s, width 0.2s;
}
#claude-usage-widget.cuw-dragging {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
opacity: 0.92;
}
#claude-usage-widget.cuw-collapsed .cuw-body-wrap,
#claude-usage-widget.cuw-collapsed #cuw-body {
display: none;
}
/* ── Header ── */
.cuw-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
cursor: grab;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.02);
}
.cuw-header:active {
cursor: grabbing;
}
.cuw-title {
font-weight: 600;
font-size: 11px;
letter-spacing: 0.3px;
text-transform: uppercase;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
flex: 1;
}
.cuw-title-brand {
/* Default: shown. Narrow + has-plan hides it. */
}
.cuw-plan-sep {
text-transform: none;
font-weight: 500;
opacity: 0.75;
}
.cuw-plan {
text-transform: none;
font-weight: 500;
opacity: 0.75;
}
.cuw-actions {
display: flex;
gap: 2px;
flex-shrink: 0;
}
.cuw-btn {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border: none;
border-radius: 5px;
background: transparent;
color: #888;
cursor: pointer;
padding: 0;
transition: background 0.15s, color 0.15s;
}
.cuw-btn:hover {
background: rgba(0, 0, 0, 0.07);
color: #333;
}
#cuw-refresh:active svg {
transform: rotate(-180deg);
transition: transform 0.3s ease;
}
/* ── Body ── */
#cuw-body {
padding: 8px 10px 6px;
}
.cuw-loading {
display: flex;
align-items: center;
gap: 6px;
color: #999;
font-size: 11px;
padding: 4px 0;
}
.cuw-spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 1.5px solid rgba(0,0,0,0.1);
border-top-color: #6b8afd;
border-radius: 50%;
animation: cuw-spin 0.7s linear infinite;
}
@keyframes cuw-spin {
to { transform: rotate(360deg); }
}
.cuw-empty {
color: #aaa;
font-size: 11px;
padding: 4px 0;
}
/* ── Usage Row ── */
.cuw-row {
margin-bottom: 6px;
}
.cuw-row:last-child,
.cuw-row:last-of-type {
margin-bottom: 2px;
}
.cuw-row-top {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 3px;
}
.cuw-label {
font-weight: 500;
font-size: 11.5px;
color: #333;
}
.cuw-pct {
font-weight: 600;
font-size: 11.5px;
color: #333;
font-variant-numeric: tabular-nums;
}
.cuw-bar-track {
height: 5px;
background: rgba(0, 0, 0, 0.06);
border-radius: 3px;
overflow: hidden;
}
.cuw-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.4s ease;
}
.cuw-reset {
font-size: 10px;
color: #999;
margin-top: 2px;
}
/* Default: only full reset label is shown */
.cuw-reset-short {
display: none;
}
/* Default: only full row label is shown */
.cuw-label-short {
display: none;
}
.cuw-balance {
color: #22c55e;
}
.cuw-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid rgba(0, 0, 0, 0.04);
}
.cuw-updated {
font-size: 10px;
color: #bbb;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.cuw-github {
display: flex;
align-items: center;
color: #ccc;
text-decoration: none;
transition: color 0.15s;
line-height: 1;
flex-shrink: 0;
}
.cuw-github:hover {
color: #888;
}
/* ───────────────────────────────────────── */
/* ── Narrow mode: browser width < 1300 ── */
/* ───────────────────────────────────────── */
#claude-usage-widget.cuw-narrow {
width: 84px;
}
/* Title: hide brand + sep when plan is available. Otherwise keep brand. */
.cuw-narrow .cuw-title.has-plan .cuw-title-brand,
.cuw-narrow .cuw-title.has-plan .cuw-plan-sep {
display: none;
}
.cuw-narrow .cuw-title {
font-size: 10px;
letter-spacing: 0.2px;
}
/* Narrow-mode plan: base name only (e.g. "Pro", "Max"); multiplier like "(5x)" is hidden. */
.cuw-narrow .cuw-plan {
font-weight: 600;
color: #666;
}
.cuw-narrow .cuw-plan-mult-wide {
display: none;
}
.cuw-narrow .cuw-header {
padding: 5px 6px;
}
.cuw-narrow .cuw-btn {
width: 18px;
height: 18px;
}
.cuw-narrow #cuw-body {
padding: 6px 6px 4px;
}
.cuw-narrow .cuw-row {
margin-bottom: 8px;
}
.cuw-narrow .cuw-row:last-child {
margin-bottom: 2px;
}
/* Stack label / value / reset vertically */
.cuw-narrow .cuw-row-top {
display: block;
margin-bottom: 0;
}
.cuw-narrow .cuw-label {
display: block;
font-size: 10px;
font-weight: 500;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.cuw-narrow .cuw-pct {
display: block;
font-size: 15px;
font-weight: 600;
color: #222;
line-height: 1.2;
margin-top: 1px;
}
/* Hide progress bar entirely in narrow mode */
.cuw-narrow .cuw-bar-track {
display: none;
}
/* Swap reset label: show the short form */
.cuw-narrow .cuw-reset-full {
display: none;
}
.cuw-narrow .cuw-reset-short {
display: inline;
}
/* Swap row label: show the short form (Current, Design, Add'l Feat.) */
.cuw-narrow .cuw-label-full {
display: none;
}
.cuw-narrow .cuw-label-short {
display: inline;
}
.cuw-narrow .cuw-reset {
font-size: 9px;
margin-top: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cuw-narrow .cuw-footer {
margin-top: 3px;
padding-top: 3px;
}
.cuw-narrow .cuw-updated {
font-size: 9px;
}
/* ── Dark mode (claude.ai uses dark theme) ── */
@media (prefers-color-scheme: dark) {
#claude-usage-widget {
background: rgba(32, 33, 36, 0.92);
border-color: rgba(255, 255, 255, 0.08);
color: #e0e0e0;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2);
}
.cuw-header {
border-bottom-color: rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
}
.cuw-title { color: #aaa; }
.cuw-btn { color: #888; }
.cuw-btn:hover { background: rgba(255, 255, 255, 0.08); color: #ddd; }
.cuw-label { color: #ddd; }
.cuw-pct { color: #ddd; }
.cuw-bar-track { background: rgba(255, 255, 255, 0.08); }
.cuw-reset { color: #777; }
.cuw-balance { color: #4ade80; }
.cuw-footer { border-top-color: rgba(255, 255, 255, 0.06); }
.cuw-updated { color: #666; }
.cuw-github { color: #555; }
.cuw-github:hover { color: #aaa; }
.cuw-loading { color: #777; }
.cuw-empty { color: #666; }
.cuw-narrow .cuw-label { color: #aaa; }
.cuw-narrow .cuw-pct { color: #eee; }
.cuw-narrow .cuw-plan { color: #aaa; }
}
/* ── Also detect claude.ai's own dark class ── */
html[data-theme="dark"] #claude-usage-widget,
body.dark #claude-usage-widget,
[data-mode="dark"] #claude-usage-widget {
background: rgba(32, 33, 36, 0.92);
border-color: rgba(255, 255, 255, 0.08);
color: #e0e0e0;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2);
}
html[data-theme="dark"] .cuw-header,
body.dark .cuw-header,
[data-mode="dark"] .cuw-header {
border-bottom-color: rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
}
html[data-theme="dark"] .cuw-title,
body.dark .cuw-title,
[data-mode="dark"] .cuw-title { color: #aaa; }
html[data-theme="dark"] .cuw-btn,
body.dark .cuw-btn,
[data-mode="dark"] .cuw-btn { color: #888; }
html[data-theme="dark"] .cuw-label,
body.dark .cuw-label,
[data-mode="dark"] .cuw-label,
html[data-theme="dark"] .cuw-pct,
body.dark .cuw-pct,
[data-mode="dark"] .cuw-pct { color: #ddd; }
html[data-theme="dark"] .cuw-bar-track,
body.dark .cuw-bar-track,
[data-mode="dark"] .cuw-bar-track { background: rgba(255, 255, 255, 0.08); }
html[data-theme="dark"] .cuw-reset,
body.dark .cuw-reset,
[data-mode="dark"] .cuw-reset { color: #777; }
html[data-theme="dark"] .cuw-balance,
body.dark .cuw-balance,
[data-mode="dark"] .cuw-balance { color: #4ade80; }
html[data-theme="dark"] .cuw-footer,
body.dark .cuw-footer,
[data-mode="dark"] .cuw-footer { border-top-color: rgba(255, 255, 255, 0.06); }
html[data-theme="dark"] .cuw-updated,
body.dark .cuw-updated,
[data-mode="dark"] .cuw-updated { color: #666; }
html[data-theme="dark"] .cuw-github,
body.dark .cuw-github,
[data-mode="dark"] .cuw-github { color: #555; }
html[data-theme="dark"] .cuw-github:hover,
body.dark .cuw-github:hover,
[data-mode="dark"] .cuw-github:hover { color: #aaa; }
html[data-theme="dark"] .cuw-narrow .cuw-label,
body.dark .cuw-narrow .cuw-label,
[data-mode="dark"] .cuw-narrow .cuw-label { color: #aaa; }
html[data-theme="dark"] .cuw-narrow .cuw-pct,
body.dark .cuw-narrow .cuw-pct,
[data-mode="dark"] .cuw-narrow .cuw-pct { color: #eee; }
html[data-theme="dark"] .cuw-narrow .cuw-plan,
body.dark .cuw-narrow .cuw-plan,
[data-mode="dark"] .cuw-narrow .cuw-plan { color: #aaa; }
`;
if (typeof GM_addStyle === 'function') {
GM_addStyle(CUW_CSS);
} else {
const styleEl = document.createElement('style');
styleEl.textContent = CUW_CSS;
(document.head || document.documentElement).appendChild(styleEl);
}
const WIDGET_ID = 'claude-usage-widget';
const IFRAME_ID = 'claude-usage-iframe';
const STORAGE_KEY = 'claude-usage-widget-pos';
const MARGIN_RIGHT = 16;
// Default initial placement for users who haven't dragged the widget:
// the widget's top edge sits this many pixels above the window's bottom edge.
// Falls back to top: 0 when the window is too short to accommodate this offset.
const DEFAULT_TOP_FROM_BOTTOM = 500;
const POLL_INTERVAL = 500;
const MAX_POLLS = 40;
const AUTO_REFRESH_MS = 5 * 60 * 1000;
const NARROW_THRESHOLD = 1300;
let autoRefreshTimer = null;
let lastRefreshTime = null;
let updatedTickerTimer = null;
let wasDragged = false;
let savedTop = null;
const GITHUB_SVG = '<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2Z"/></svg>';
// ── SPA Navigation ──
function isOnChatPage() {
return location.pathname.startsWith('/chat/');
}
function showWidget() {
var w = document.getElementById(WIDGET_ID);
if (w) w.style.display = '';
}
function hideWidget() {
var w = document.getElementById(WIDGET_ID);
if (w) w.style.display = 'none';
}
function monitorNavigation() {
var lastPath = location.pathname;
var check = function () {
var curPath = location.pathname;
if (curPath === lastPath) return;
lastPath = curPath;
onRouteChange();
};
var origPush = history.pushState;
history.pushState = function () {
origPush.apply(this, arguments);
setTimeout(check, 0);
};
var origReplace = history.replaceState;
history.replaceState = function () {
origReplace.apply(this, arguments);
setTimeout(check, 0);
};
window.addEventListener('popstate', check);
setInterval(check, 1000);
}
function onRouteChange() {
if (isOnChatPage()) {
ensureWidget();
showWidget();
} else {
hideWidget();
}
}
// ── Narrow-mode helpers ──
function isNarrowMode() {
return window.innerWidth < NARROW_THRESHOLD;
}
function updateNarrowMode() {
var w = document.getElementById(WIDGET_ID);
if (!w) return;
w.classList.toggle('cuw-narrow', isNarrowMode());
}
// ── Widget creation ──
function ensureWidget() {
if (document.getElementById(WIDGET_ID)) return;
createWidget();
fetchUsageData();
scheduleAutoRefresh();
}
function createWidget() {
var wrap = document.createElement('div');
wrap.id = WIDGET_ID;
wrap.innerHTML =
'<div class="cuw-header" id="cuw-drag-handle">' +
'<span class="cuw-title">' +
'<span class="cuw-title-brand">CLAUSAGE</span>' +
'</span>' +
'<div class="cuw-actions">' +
'<button class="cuw-btn" id="cuw-refresh" title="Refresh">' +
'<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M1.5 2v4.5H6"/>' +
'<path d="M2.2 10.2a6 6 0 1 0 .6-6.7L1.5 6.5"/>' +
'</svg>' +
'</button>' +
'<button class="cuw-btn" id="cuw-minimize" title="Minimize">' +
'<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">' +
'<line x1="3" y1="8" x2="13" y2="8"/>' +
'</svg>' +
'</button>' +
'</div>' +
'</div>' +
'<div class="cuw-body" id="cuw-body">' +
'<div class="cuw-loading">Loading…</div>' +
'</div>';
document.body.appendChild(wrap);
// Restore position
try {
var saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (saved && saved.wasDragged) {
wasDragged = true;
savedTop = saved.top;
}
} catch (_) {}
updateNarrowMode();
applyPosition(wrap);
initDrag(wrap, document.getElementById('cuw-drag-handle'));
window.addEventListener('resize', function () {
var w = document.getElementById(WIDGET_ID);
if (!w) return;
updateNarrowMode();
applyPosition(w);
var line = document.getElementById('cuw-updated-line');
if (line) line.textContent = getRelativeTimeText();
});
var minBtn = document.getElementById('cuw-minimize');
minBtn.addEventListener('click', function (e) {
e.stopPropagation();
var collapsed = wrap.classList.toggle('cuw-collapsed');
minBtn.title = collapsed ? 'Expand' : 'Minimize';
minBtn.innerHTML = collapsed
? '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="3" x2="8" y2="13"/><line x1="3" y1="8" x2="13" y2="8"/></svg>'
: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="8" x2="13" y2="8"/></svg>';
});
document.getElementById('cuw-refresh').addEventListener('click', function (e) {
e.stopPropagation();
fetchUsageData();
scheduleAutoRefresh();
});
return wrap;
}
// ── Position logic ──
function applyPosition(el) {
if (!wasDragged) {
// Horizontal: pin to the right edge (unchanged).
// Vertical: top edge sits DEFAULT_TOP_FROM_BOTTOM px above the window's bottom.
// If the window is too short for that offset, clamp to top: 0 (widget hugs the top).
var desiredTop = window.innerHeight - DEFAULT_TOP_FROM_BOTTOM;
if (desiredTop < 0) desiredTop = 0;
el.style.right = MARGIN_RIGHT + 'px';
el.style.left = 'auto';
el.style.bottom = 'auto';
el.style.top = desiredTop + 'px';
return;
}
var rect = el.getBoundingClientRect();
var left = window.innerWidth - rect.width - MARGIN_RIGHT;
el.style.right = 'auto';
el.style.bottom = 'auto';
el.style.left = left + 'px';
var t = savedTop;
if (t < 0) t = 0;
if (t + rect.height > window.innerHeight) t = window.innerHeight - rect.height;
el.style.top = t + 'px';
}
// ── Drag logic ──
function initDrag(el, handle) {
var startX, startY, startLeft, startTop, dragging = false;
handle.addEventListener('mousedown', function (e) {
if (e.target.closest('.cuw-btn')) return;
e.preventDefault();
dragging = true;
var rect = el.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
el.style.right = 'auto';
el.style.bottom = 'auto';
el.style.left = rect.left + 'px';
el.style.top = rect.top + 'px';
el.classList.add('cuw-dragging');
});
document.addEventListener('mousemove', function (e) {
if (!dragging) return;
el.style.left = (startLeft + (e.clientX - startX)) + 'px';
el.style.top = (startTop + (e.clientY - startY)) + 'px';
});
document.addEventListener('mouseup', function () {
if (!dragging) return;
dragging = false;
el.classList.remove('cuw-dragging');
clampToViewport(el);
var rect = el.getBoundingClientRect();
wasDragged = true;
savedTop = rect.top;
localStorage.setItem(STORAGE_KEY, JSON.stringify({ top: savedTop, wasDragged: true }));
applyPosition(el);
});
}
function clampToViewport(el) {
var rect = el.getBoundingClientRect();
var vw = window.innerWidth, vh = window.innerHeight;
var l = rect.left, t = rect.top;
if (l < 0) l = 0;
if (t < 0) t = 0;
if (l + rect.width > vw) l = vw - rect.width;
if (t + rect.height > vh) t = vh - rect.height;
el.style.left = l + 'px';
el.style.top = t + 'px';
}
// ── Parse usage data ──
function parseUsage(doc) {
var data = { sections: [], plan: null };
var allText = doc.body.innerText;
if (!allText || allText.includes('Sign in') || allText.includes('Log in')) return null;
// ── Extract plan name (e.g. "Pro", "Team", "Free", "Max", "Max (5x)", "Max (10x)") ──
// Matches the whole cell text. The (\d+x) group captures multiplier variants for Max.
var planRE = /^(Free|Pro|Team|Enterprise|Max)(?:\s*\((\d+x)\))?$/i;
var planWalker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
while (planWalker.nextNode()) {
var ptxt = planWalker.currentNode.textContent.trim();
var m = ptxt.match(planRE);
if (m) {
// Normalize casing: first letter uppercase, rest lowercase (Pro, Max, Team…)
var base = m[1].charAt(0).toUpperCase() + m[1].slice(1).toLowerCase();
var mult = m[2] ? m[2].toLowerCase() : null;
data.plan = { base: base, multiplier: mult };
break;
}
}
// ── Pass 1: "% used" rows (Current session, All models, Claude Design, Extra usage $ spent) ──
var walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
var usageTexts = [];
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim().includes('% used')) usageTexts.push(walker.currentNode);
}
usageTexts.forEach(function (node) {
var pctMatch = node.textContent.match(/(\d+)%\s*used/);
if (!pctMatch) return;
var pct = parseInt(pctMatch[1], 10);
var container = findAncestorBlock(node);
if (!container) return;
var blockText = container.innerText || '';
var lines = blockText.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
var label = '', resetInfo = '';
lines.forEach(function (line) {
if (/current session|all models|claude design/i.test(line) && !label) label = line;
if (/resets?\s/i.test(line) && !resetInfo) resetInfo = line;
});
if (!label) {
label = lines.find(function (l) {
return !l.includes('% used') && !l.includes('Resets') && !l.includes('Learn more');
}) || 'Unknown';
}
// Filter rule: Current session / All models always show; everything else hide when 0%.
var alwaysShow = /current session|all models/i.test(label);
if (!alwaysShow && pct === 0) return;
data.sections.push({
type: 'pct',
label: label,
pct: pct,
resetInfo: resetInfo
});
});
// ── Pass 2: Additional features "X / Y" rows (e.g. Daily included routine runs) ──
var routineWalker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
while (routineWalker.nextNode()) {
var rtxt = routineWalker.currentNode.textContent.trim();
if (/routine runs?/i.test(rtxt)) {
var blockEl = routineWalker.currentNode.parentElement;
for (var d = 0; d < 8 && blockEl; d++) {
var btext = blockEl.innerText || '';
var fracMatch = btext.match(/(\d+)\s*\/\s*(\d+)/);
if (fracMatch) {
var cur = parseInt(fracMatch[1], 10);
var tot = parseInt(fracMatch[2], 10);
// Filter rule: Additional features hide if value is 0.
if (cur > 0 && tot > 0) {
data.sections.push({
type: 'frac',
label: 'Routine runs',
current: cur,
total: tot,
resetInfo: ''
});
}
break;
}
blockEl = blockEl.parentElement;
}
break;
}
}
return data.sections.length > 0 ? data : null;
}
// Find the tightest ancestor that still only contains ONE "% used" occurrence.
// As we walk upward, once we hit a level with 2+ "% used" we've crossed into
// a sibling row — return the last single-occurrence candidate.
function findAncestorBlock(node) {
var el = node.parentElement;
var candidate = null;
var depth = 0;
while (el && depth < 15) {
var text = el.innerText || '';
var count = (text.match(/% used/g) || []).length;
if (count >= 2) {
return candidate || el;
}
if (count === 1) {
candidate = el;
}
el = el.parentElement;
depth++;
}
return candidate;
}
// ── Relative time + ticker ──
function getRelativeTimeText() {
if (!lastRefreshTime) return '';
var elapsed = Math.floor((Date.now() - lastRefreshTime) / 1000);
if (isNarrowMode()) {
if (elapsed < 30) return 'just now';
if (elapsed < 60) return '30s ago';
if (elapsed < 120) return '1m ago';
if (elapsed < 180) return '2m ago';
if (elapsed < 240) return '3m ago';
if (elapsed < 290) return '4m ago';
return 'refreshing…';
}
if (elapsed < 30) return 'Updated: just now';
if (elapsed < 60) return 'Updated: half a minute ago';
if (elapsed < 120) return 'Updated: a minute ago';
if (elapsed < 180) return 'Updated: 2 minutes ago';
if (elapsed < 240) return 'Updated: 3 minutes ago';
if (elapsed < 290) return 'Updated: 4 minutes ago';
return 'Refreshing soon';
}
function stopUpdatedTicker() {
if (updatedTickerTimer) { clearInterval(updatedTickerTimer); updatedTickerTimer = null; }
}
function startUpdatedTicker() {
stopUpdatedTicker();
updatedTickerTimer = setInterval(function () {
var el = document.getElementById('cuw-updated-line');
if (el) el.textContent = getRelativeTimeText();
}, 1000);
}
// ── Shorten reset info for narrow mode ──
function shortenReset(s) {
return (s || '')
.replace(/^Resets\s+/i, '')
.replace(/\s*(AM|PM)\s*$/i, '');
}
// ── Shorten row label for narrow mode (Current session → Current, etc.) ──
function shortenLabel(s) {
var t = (s || '').trim();
if (/^current session$/i.test(t)) return 'Current';
if (/^claude design$/i.test(t)) return 'Design';
// "Daily included routine runs" header / or its shortened pre-render form
if (/^routine runs?$/i.test(t)) return "Add'l Feat.";
if (/^daily included routine runs?$/i.test(t)) return "Add'l Feat.";
return t;
}
// ── Render data ──
function renderData(data) {
var body = document.getElementById('cuw-body');
if (!data) {
body.innerHTML = '<div class="cuw-empty">Unable to load</div>';
return;
}
lastRefreshTime = Date.now();
// Update title with plan name
var titleEl = document.querySelector('#' + WIDGET_ID + ' .cuw-title');
if (titleEl) {
if (data.plan) {
titleEl.classList.add('has-plan');
// Wide mode: CLAUSAGE | Max (5x) — literal "(5x)" matches the settings page.
// Narrow mode: shows just the plan base (e.g. "Max"); the multiplier span is hidden via CSS.
var planBase = escapeHtml(data.plan.base);
var planHTML = '<span class="cuw-plan-base">' + planBase + '</span>';
if (data.plan.multiplier) {
var mult = escapeHtml(data.plan.multiplier);
planHTML += '<span class="cuw-plan-mult-wide"> (' + mult + ')</span>';
}
titleEl.innerHTML =
'<span class="cuw-title-brand">CLAUSAGE</span>' +
'<span class="cuw-plan-sep"> | </span>' +
'<span class="cuw-plan">' + planHTML + '</span>';
} else {
titleEl.classList.remove('has-plan');
titleEl.innerHTML = '<span class="cuw-title-brand">CLAUSAGE</span>';
}
}
var html = '';
data.sections.forEach(function (s) {
var barWidth, rightText;
if (s.type === 'frac') {
barWidth = s.total > 0 ? Math.round((s.current / s.total) * 100) : 0;
rightText = s.current + '/' + s.total;
} else {
barWidth = s.pct;
rightText = s.pct + '%';
}
var barColor = barWidth >= 90 ? '#ef4444' : barWidth >= 70 ? '#f59e0b' : '#6b8afd';
var shortR = shortenReset(s.resetInfo);
var shortLbl = shortenLabel(s.label);
html +=
'<div class="cuw-row">' +
'<div class="cuw-row-top">' +
'<span class="cuw-label">' +
'<span class="cuw-label-full">' + escapeHtml(s.label) + '</span>' +
'<span class="cuw-label-short">' + escapeHtml(shortLbl) + '</span>' +
'</span>' +
'<span class="cuw-pct">' + escapeHtml(rightText) + '</span>' +
'</div>' +
'<div class="cuw-bar-track">' +
'<div class="cuw-bar-fill" style="width:' + barWidth + '%;background:' + barColor + '"></div>' +
'</div>' +
(s.resetInfo
? '<div class="cuw-reset">' +
'<span class="cuw-reset-full">' + escapeHtml(s.resetInfo) + '</span>' +
'<span class="cuw-reset-short">' + escapeHtml(shortR) + '</span>' +
'</div>'
: '') +
'</div>';
});
html +=
'<div class="cuw-footer">' +
'<span class="cuw-updated" id="cuw-updated-line">' + getRelativeTimeText() + '</span>' +
'<a class="cuw-github" href="https://github.com/Yorkian/CLAUSAGE" target="_blank" title="GitHub">' + GITHUB_SVG + '</a>' +
'</div>';
body.innerHTML = html;
startUpdatedTicker();
}
function escapeHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// ── Fetch data via hidden iframe ──
function fetchUsageData() {
stopUpdatedTicker();
var body = document.getElementById('cuw-body');
if (body) body.innerHTML = '<div class="cuw-loading"><span class="cuw-spinner"></span> Loading…</div>';
var old = document.getElementById(IFRAME_ID);
if (old) old.remove();
var iframe = document.createElement('iframe');
iframe.id = IFRAME_ID;
iframe.src = 'https://claude.ai/settings/usage';
iframe.style.cssText = 'position:fixed;width:0;height:0;border:none;opacity:0;pointer-events:none;top:-9999px;left:-9999px;';
document.body.appendChild(iframe);
var polls = 0;
var timer = setInterval(function () {
polls++;
try {
var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
if (!doc || !doc.body) {
if (polls >= MAX_POLLS) { clearInterval(timer); iframe.remove(); renderData(null); }
return;
}
var currentUrl = (iframe.contentWindow && iframe.contentWindow.location && iframe.contentWindow.location.href) || '';
if (currentUrl.includes('/login') || currentUrl.includes('/signin')) {
clearInterval(timer); iframe.remove(); hideWidget(); return;
}
var text = doc.body.innerText || '';
if (text.includes('% used')) {
clearInterval(timer);
var data = parseUsage(doc);
renderData(data);
iframe.remove();
return;
}
if (text.includes('Sign in') || text.includes('Log in') || text.includes('Continue with')) {
clearInterval(timer); iframe.remove(); hideWidget(); return;
}
} catch (e) {
if (e.name === 'SecurityError' || (e.message && e.message.includes('cross-origin'))) {
clearInterval(timer); iframe.remove(); hideWidget(); return;
}
}
if (polls >= MAX_POLLS) { clearInterval(timer); iframe.remove(); renderData(null); }
}, POLL_INTERVAL);
}
// ── Auto-refresh ──
function scheduleAutoRefresh() {
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
autoRefreshTimer = setInterval(function () {
fetchUsageData();
}, AUTO_REFRESH_MS);
}
// ── Init ──
monitorNavigation();
if (isOnChatPage()) {
ensureWidget();
}
})();