Minimal usage bar below Claude input
// ==UserScript==
// @name Claude Inline Usage Tracker
// @namespace usage-tracker-of-claude
// @author Niko
// @version 2.7
// @description Minimal usage bar below Claude input
// @match https://claude.ai/*
// @grant none
// @run-at document-idle
// @license GNU General Public License v3.0
// ==/UserScript==
(() => {
'use strict';
const ID = 'cut', SID = 'cut-style', API = '/api/organizations';
const POLL = 60_000, HOVER_REFRESH = 30_000, MIN_GAP = 15_000;
const WARN = 60, DANGER = 80;
const A = 'cut-anchor', H = 'cut-hover';
const ROWS = [
['five_hour', 'Current Session'],
['seven_day', 'Weekly Limit (All)'],
['seven_day_opus', 'Weekly Limit (Opus)'],
];
const S = { org: null, inflight: null, last: null, lastAt: 0, anchor: null, ui: null, poll: 0, sched: 0, mo: null };
const clamp = (v) => (v = +v || 0) < 0 ? 0 : v > 100 ? 100 : v;
const fmt = (iso) => {
if (!iso) return 'N/A';
const m = Math.round((new Date(iso).getTime() - Date.now()) / 60000);
if (m < 1) return 'Resetting soon';
if (m < 60) return `In ${m} min`;
const h = (m / 60) | 0;
return h < 24 ? `In ${h} hr` : `In ${((h / 24) | 0)} days`;
};
const jget = (u) => fetch(u, { credentials: 'include' }).then(r => { if (!r.ok) throw new Error(r.status); return r.json(); });
async function orgId() {
if (S.org) return S.org;
const orgs = await jget(API);
return (S.org = orgs?.[0]?.uuid ?? null);
}
function usage(force) {
const now = Date.now();
if (!force && now - S.lastAt < MIN_GAP) return Promise.resolve(S.last);
if (S.inflight) return S.inflight;
return (S.inflight = (async () => {
try {
const id = await orgId();
if (!id) return S.last;
const d = await jget(`${API}/${id}/usage`);
if (d) { S.last = d; S.lastAt = Date.now(); }
return S.last;
} catch (e) {
S.org = null;
return S.last;
} finally {
S.inflight = null;
}
})());
}
function style() {
if (document.getElementById(SID)) return;
const s = document.createElement('style');
s.id = SID;
s.textContent = `
#${ID}{position:absolute;inset:auto 16px -15px;z-index:30;font-family:var(--font-ui,system-ui,-apple-system,Segoe UI,Roboto,sans-serif);color:hsl(var(--text-100))}
#${ID} .t{height:12px;display:flex;align-items:center;cursor:pointer}
#${ID} .b{width:100%;height:3px;background:hsla(var(--border-300)/.12);border-radius:999px;overflow:hidden;transition:height .16s ease}
#${ID} .t:hover .b{height:4px}
#${ID} .f{height:100%;width:0%;background:hsl(var(--brand-000));transition:width .25s ease}
#${ID} .w{background:hsl(var(--warning-100))}
#${ID} .d{background:hsl(var(--danger-100))}
#${ID} .p{position:absolute;bottom:14px;left:0;right:0;background:hsl(var(--bg-000));border-radius:16px;display:flex;flex-direction:column;gap:10px;padding:12px 14px 10px;box-shadow:0 .25rem 1.25rem hsl(var(--always-black)/3.5%),0 0 0 .5px hsla(var(--border-300)/.15);opacity:0;visibility:hidden;pointer-events:none;transform:translateY(8px);transition:opacity .16s ease,transform .16s ease,visibility 0s linear .16s}
#${ID} .t:hover + .p{opacity:1;visibility:visible;transform:translateY(0);transition:opacity .16s ease,transform .16s ease}
#${ID} .hh{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;margin-bottom:6px;font-size:13px;line-height:1.1}
#${ID} .l{font-weight:550;color:hsl(var(--text-100))}
#${ID} .m{font-size:12px;font-weight:430;color:hsl(var(--text-500));white-space:nowrap}
#${ID} .k{width:100%;height:6px;background:hsla(var(--border-300)/.12);border-radius:999px;overflow:hidden}
.${A}{transition:background-color .2s ease,box-shadow .2s ease,border-color .2s ease}
.${A}.${H}{background-color:transparent!important;box-shadow:none!important;border-color:transparent!important}
.${A}>:not(#${ID}){transition:opacity .2s ease}
.${A}.${H}>:not(#${ID}){opacity:0!important;pointer-events:none!important}
@media (prefers-reduced-motion:reduce){#${ID} .b,#${ID} .f,#${ID} .p,.${A},.${A}>:not(#${ID}){transition:none!important}}
`;
document.head.appendChild(s);
}
function clsFor(p) { return p > DANGER ? 'f d' : p > WARN ? 'f w' : 'f'; }
function setFill(el, p) {
const sp = '' + p;
if (el.dataset.p !== sp) {
el.dataset.p = sp;
el.style.width = sp + '%';
const c = clsFor(p);
if (el.className !== c) el.className = c;
}
}
function build() {
const root = document.createElement('div');
root.id = ID;
root.innerHTML =
`<div class="t"><div class="b"><div class="f" data-role="tf"></div></div></div>` +
`<div class="p">` +
ROWS.map(([, label], i) =>
`<div class="r" data-i="${i}">` +
`<div class="hh"><span class="l">${label}</span><span class="m" data-role="m"></span></div>` +
`<div class="k"><div class="f" data-role="f"></div></div>` +
`</div>`
).join('') +
`</div>`;
const tf = root.querySelector('[data-role="tf"]');
const rEls = [...root.querySelectorAll('.r')];
const metas = rEls.map(r => r.querySelector('[data-role="m"]'));
const fills = rEls.map(r => r.querySelector('[data-role="f"]'));
root.addEventListener('pointerenter', () => {
S.anchor && S.anchor.classList.add(H);
if (Date.now() - S.lastAt > HOVER_REFRESH) refresh(1);
}, { passive: true });
root.addEventListener('pointerleave', () => { S.anchor && S.anchor.classList.remove(H); }, { passive: true });
return { root, tf, rEls, metas, fills };
}
function render(d) {
if (!S.ui || !d) return;
setFill(S.ui.tf, clamp(d?.five_hour?.utilization));
for (let i = 0; i < ROWS.length; i++) {
const key = ROWS[i][0];
const b = d?.[key];
const row = S.ui.rEls[i];
if (!b) { row.hidden = true; continue; }
row.hidden = false;
const p = clamp(b.utilization);
setFill(S.ui.fills[i], p);
const t = `${p}% · ${fmt(b.resets_at)}`;
const m = S.ui.metas[i];
if (m.dataset.t !== t) { m.dataset.t = t; m.textContent = t; }
}
}
async function refresh(force) {
if (!S.ui || (!force && document.hidden)) return;
render(await usage(!!force));
}
function findAnchor() {
const ed = document.querySelector('[contenteditable="true"].tiptap');
if (!ed) return null;
const fs = ed.closest('fieldset');
if (!fs) return null;
return fs.querySelector('div[class*="bg-bg-000"][class*="rounded-[20px]"]') || fs;
}
function attach() {
const a = findAnchor();
if (!a) return;
const existing = document.getElementById(ID);
if (a === S.anchor && existing && a.contains(existing)) return;
existing && existing.remove();
a.classList.add(A);
if (getComputedStyle(a).position === 'static') a.style.position = 'relative';
S.anchor = a;
S.ui = build();
a.insertBefore(S.ui.root, a.firstChild);
refresh(1);
}
function scheduleAttach() {
if (S.sched) return;
const cb = () => { S.sched = 0; attach(); };
S.sched = window.requestIdleCallback ? requestIdleCallback(cb, { timeout: 800 }) : requestAnimationFrame(cb);
}
function startPoll() {
stopPoll();
const tick = () => {
if (document.hidden) { S.poll = 0; return; }
refresh(0);
S.poll = setTimeout(tick, POLL);
};
S.poll = setTimeout(tick, POLL);
}
function stopPoll() { S.poll && clearTimeout(S.poll); S.poll = 0; }
function hooks() {
const patch = (m) => {
const o = history[m];
history[m] = function () { const r = o.apply(this, arguments); scheduleAttach(); return r; };
};
patch('pushState'); patch('replaceState');
addEventListener('popstate', scheduleAttach, { passive: true });
addEventListener('hashchange', scheduleAttach, { passive: true });
let t = 0;
S.mo = new MutationObserver(() => {
if (t) return;
t = setTimeout(() => { t = 0; scheduleAttach(); }, 200);
});
S.mo.observe(document.body, { childList: true, subtree: true });
document.addEventListener('visibilitychange', () => {
if (document.hidden) stopPoll();
else { scheduleAttach(); refresh(1); startPoll(); }
}, { passive: true });
addEventListener('focus', () => !document.hidden && refresh(1), { passive: true });
}
function init() {
style();
hooks();
scheduleAttach();
startPoll();
}
init();
})();