Claude Usage Reticle

Visual usage tracker showing time delta and percentage - see if you're OVER or UNDER budget

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Claude Usage Reticle
// @namespace    https://github.com/KatsuJinCode
// @version      2.0.1
// @description  Visual usage tracker showing time delta and percentage - see if you're OVER or UNDER budget
// @author       KatsuJinCode
// @match        https://claude.ai/*
// @icon         https://claude.ai/favicon.ico
// @grant        none
// @license      MIT
// @homepageURL  https://github.com/KatsuJinCode/claude-usage-reticle
// @supportURL   https://github.com/KatsuJinCode/claude-usage-reticle/issues
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    var style = document.createElement('style');
    style.textContent = '.usage-reticle{position:absolute;width:2px;height:100%;background:#3b82f6;box-shadow:0 0 2px rgba(0,0,0,.5);pointer-events:none;z-index:10;top:0}.usage-reticle::after{content:"";position:absolute;left:-3px;bottom:-5px;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:5px solid #3b82f6}.usage-reticle-label{position:absolute;bottom:-22px;left:50%;transform:translateX(-50%);background:#3b82f6;color:#fff;padding:1px 4px;border-radius:2px;font-size:9px;font-weight:600;white-space:nowrap}.delta-reticle{position:absolute;width:2px;height:100%;box-shadow:0 0 2px rgba(0,0,0,.5);pointer-events:none;z-index:10;top:0}.delta-reticle::before{content:"";position:absolute;left:-3px;top:-5px;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent}.delta-reticle-label{position:absolute;top:-22px;left:50%;transform:translateX(-50%);padding:1px 4px;border-radius:2px;font-size:9px;font-weight:600;white-space:nowrap;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.9),0 0 4px rgba(0,0,0,0.7),0 0 8px rgba(0,0,0,0.4);border:1px solid #000}.reticle-overlay{position:absolute;height:100%;top:0;pointer-events:none;z-index:4;border-radius:4px}.reticle-glow{position:absolute;height:100%;top:0;pointer-events:none;z-index:3;border-radius:4px}';
    document.head.appendChild(style);

    var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

    function fmtTime(d, short) {
        var day = days[d.getDay()];
        var h = d.getHours();
        var m = d.getMinutes();
        var ap = h >= 12 ? 'PM' : 'AM';
        h = h % 12;
        if (h === 0) h = 12;
        var ts = h + ':' + (m < 10 ? '0' : '') + m + ' ' + ap;
        return short ? ts : day + ' ' + ts;
    }

    function fmtDelta(hrs, pct) {
        var over = hrs >= 0;
        hrs = Math.abs(hrs);
        var d = Math.floor(hrs / 24);
        var h = Math.floor(hrs % 24);
        var m = Math.round((hrs - Math.floor(hrs)) * 60);
        var t = '';
        if (d > 0) t = d + 'd ' + h + 'h';
        else if (h > 0) t = h + 'h' + (m > 0 ? ' ' + m + 'm' : '');
        else t = m + 'm';
        return t + ' ' + (over ? 'OVER' : 'UNDER') + ' (' + Math.abs(Math.round(pct)) + '%)';
    }

    function getColor(pct) {
        var raw = Math.min(Math.abs(pct) / 100 * 2, 1);
        var p = 0.35 + (1 - 0.35) * raw;
        if (pct < 0) {
            var sat = 5 + p * 70;
            var lit = 95 - p * 55;
            return 'hsl(142,' + sat + '%,' + lit + '%)';
        } else {
            var sat = 5 + p * 75;
            var lit = 95 - p * 55;
            return 'hsl(0,' + sat + '%,' + lit + '%)';
        }
    }

    function addReticles() {
        var containers = document.querySelectorAll('div.flex.flex-row.gap-x-8.justify-between.items-center');
        var added = 0;

        containers.forEach(function(c) {
            var p = c.querySelector('p.text-text-400.whitespace-nowrap');
            if (!p) return;
            var t = p.textContent;

            var titleEl = c.querySelector('p.text-text-100');
            var isSession = titleEl && titleEl.textContent.toLowerCase().includes('current session');
            var windowHrs = isSession ? 5 : 168;
            var hrsUntil, reset;

            var m1 = t.match(/in\s+(?:(\d+)\s*hr?)?\s*(?:(\d+)\s*min)?/i);
            if (m1 && (m1[1] || m1[2])) {
                hrsUntil = parseInt(m1[1] || 0) + (parseInt(m1[2] || 0) / 60);
                reset = new Date(Date.now() + hrsUntil * 3600000);
            } else {
                var m2 = t.match(/(sun|mon|tue|wed|thu|fri|sat)\w*\s+(\d+):(\d+)\s*(am|pm)/i);
                if (!m2) return;
                var h = parseInt(m2[2]);
                if (m2[4].toLowerCase() === 'pm' && h !== 12) h += 12;
                if (m2[4].toLowerCase() === 'am' && h === 12) h = 0;
                var di = {sun:0, mon:1, tue:2, wed:3, thu:4, fri:5, sat:6};
                var rd = di[m2[1].toLowerCase().slice(0, 3)];
                var now = new Date();
                reset = new Date();
                reset.setHours(h, parseInt(m2[3]), 0, 0);
                var d = rd - now.getDay();
                if (d < 0) d += 7;
                if (d === 0 && reset <= now) d = 7;
                reset.setDate(now.getDate() + d);
                hrsUntil = (reset - now) / 3600000;
            }

            var nowPos = Math.max(0, Math.min(100, ((windowHrs - hrsUntil) / windowHrs) * 100));

            var bar = c.querySelector('div.bg-bg-000.rounded.border.h-4');
            if (!bar) return;

            var fill = bar.querySelector('div');
            var usagePos = 0;
            if (fill) {
                var w = fill.style.width;
                if (w) usagePos = parseFloat(w);
            }

            var windowStart = new Date(reset.getTime() - windowHrs * 3600000);
            var usageHrs = (usagePos / 100) * windowHrs;
            var usageTime = new Date(windowStart.getTime() + usageHrs * 3600000);
            var usageLbl = fmtTime(usageTime, isSession);

            var diffPct = usagePos - nowPos;
            var diffHrs = (diffPct / 100) * windowHrs;
            var deltaLbl = fmtDelta(diffHrs, diffPct);
            var color = getColor(diffPct);

            var raw = Math.min(Math.abs(diffPct) / 100 * 2, 1);
            var intensity = 0.35 + (1 - 0.35) * raw;

            bar.style.position = 'relative';
            bar.style.overflow = 'visible';

            // Remove old elements
            bar.querySelectorAll('.delta-reticle,.usage-reticle,.reticle-overlay,.reticle-glow').forEach(function(el) {
                el.remove();
            });

            // Add overlay/glow
            if (diffPct > 0) {
                // Over budget - red glow + overlay
                var glow = document.createElement('div');
                glow.className = 'reticle-glow';
                glow.style.left = nowPos + '%';
                glow.style.width = Math.abs(diffPct) + '%';
                glow.style.boxShadow = '0 0 ' + (8 + intensity * 15) + 'px ' + (2 + intensity * 5) + 'px hsla(0,' + (50 + intensity * 30) + '%,' + (50 - intensity * 10) + '%,' + (0.4 + intensity * 0.4) + ')';
                bar.appendChild(glow);

                var ov = document.createElement('div');
                ov.className = 'reticle-overlay';
                ov.style.left = nowPos + '%';
                ov.style.width = Math.abs(diffPct) + '%';
                ov.style.background = 'hsla(0,' + (60 + intensity * 20) + '%,' + (40 - intensity * 10) + '%,' + (0.55 + intensity * 0.25) + ')';
                bar.appendChild(ov);
            } else if (diffPct < 0) {
                // Under budget - green overlay
                var ov = document.createElement('div');
                ov.className = 'reticle-overlay';
                ov.style.left = usagePos + '%';
                ov.style.width = Math.abs(diffPct) + '%';
                ov.style.background = 'hsla(142,' + (40 + intensity * 30) + '%,' + (50 - intensity * 10) + '%,' + (0.4 + intensity * 0.35) + ')';
                bar.appendChild(ov);
            }

            // Delta reticle (at NOW position)
            var dr = document.createElement('div');
            dr.className = 'delta-reticle';
            dr.style.left = nowPos + '%';
            dr.style.background = color;

            var arrowStyle = document.createElement('style');
            arrowStyle.textContent = '.delta-reticle::before{border-top:5px solid ' + color + '}';
            dr.appendChild(arrowStyle);

            var dlbl = document.createElement('div');
            dlbl.className = 'delta-reticle-label';
            dlbl.style.background = color;
            dlbl.textContent = deltaLbl;
            dr.appendChild(dlbl);
            bar.appendChild(dr);

            // Usage reticle (at usage position)
            var ur = document.createElement('div');
            ur.className = 'usage-reticle';
            ur.style.left = usagePos + '%';

            var ulbl = document.createElement('div');
            ulbl.className = 'usage-reticle-label';
            ulbl.textContent = usageLbl;
            ur.appendChild(ulbl);
            bar.appendChild(ur);

            added++;
        });

        return added;
    }

    // Initial attempt
    var count = addReticles();

    // Retry if nothing found (page still loading)
    if (count === 0) {
        var attempts = 0;
        var interval = setInterval(function() {
            attempts++;
            if (addReticles() > 0 || attempts >= 10) {
                clearInterval(interval);
            }
        }, 1000);
    }

    // Auto-refresh every minute to keep positions current
    setInterval(addReticles, 60000);

    // Watch for SPA navigation
    var lastUrl = location.href;
    new MutationObserver(function() {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            setTimeout(addReticles, 1000);
        }
    }).observe(document.body, {childList: true, subtree: true});

})();