Cursor.com Usage Tracker (Enhanced)

Tracks and displays usage statistics and detailed model costs for Premium models on Cursor.com, with consistent model colors and a legend sorted by usage.

اعتبارا من 29-05-2025. شاهد أحدث إصدار.

// ==UserScript==
// @name         Cursor.com Usage Tracker (Enhanced)
// @author       monnef, Sonnet 3.5 (via Perplexity and Cursor), some help from Cursor Tab and Cursor Small, NoahBPeterson, Sonnet 3.7, Gemini
// @namespace    http://monnef.eu
// @version      0.5.11
// @description  Tracks and displays usage statistics and detailed model costs for Premium models on Cursor.com, with consistent model colors and a legend sorted by usage.
// @match        https://www.cursor.com/settings
// @grant        none
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @license      AGPL-3.0
// @icon         https://www.cursor.com/favicon-48x48.png
// ==/UserScript==

(function () {
    'use strict';

    const $ = jQuery.noConflict();

    const $c = (cls, parent) => $(`.${cls}`, parent);

    $.fn.nthParent = function (n) {
        return this.parents().eq(n - 1);
    };

    const log = (...messages) => {
        console.log(`[UsageTracker]`, ...messages);
    };

    const error = (...messages) => {
        console.error(`[UsageTracker]`, ...messages);
    };

    const debug = (...messages) => {
        console.debug(`[UsageTracker Debug]`, ...messages);
    };

    const genCssId = name => `ut-${name}`;

    // --- CSS Class Names ---
    const mainCaptionCls = genCssId('main-caption');
    const hrCls = genCssId('hr');
    const multiBarCls = genCssId('multi-bar');
    const barSegmentCls = genCssId('bar-segment');
    const tooltipCls = genCssId('tooltip');
    const statsContainerCls = genCssId('stats-container');
    const statItemCls = genCssId('stat-item');
    const enhancedTrackerContainerCls = genCssId('enhanced-tracker');
    const legendCls = genCssId('legend');
    const legendItemCls = genCssId('legend-item');
    const legendColorBoxCls = genCssId('legend-color-box');

    const colors = {
        cursor: {
            lightGray: '#e5e7eb',
            gray: '#a7a9ac',
            grayDark: '#333333',
        },
        modelColorPalette: [
            '#FF6F61', '#4CAF50', '#2196F3', '#FFEB3B', '#9C27B0',
            '#FF9800', '#00BCD4', '#E91E63', '#8BC34A', '#3F51B5',
            '#CDDC39', '#673AB7', '#FFC107', '#009688', '#FF5722',
            '#795548', '#607D8B', '#9E9E9E', '#F44336', '#4DD0E1',
            '#FFB74D', '#BA68C8', '#AED581', '#7986CB', '#A1887F'
        ]
    };

    const styles = `
    .${hrCls} { border: 0; height: 1px; background-color: #333333; margin: 15px 0; }
    .${statsContainerCls} { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; margin: 15px 0; padding: 15px; background-color: #1a1a1a; border-radius: 8px; }
    .${statItemCls} { font-size: 14px; }
    .${statItemCls} .label { color: ${colors.cursor.gray}; }
    .${statItemCls} .value { color: white; font-weight: bold; }
    .${multiBarCls} {
        display: flex;
        width: 100%;
        height: 8px;
        background-color: ${colors.cursor.grayDark};
        border-radius: 9999px;
        margin: 10px 0;
    }
    .${barSegmentCls} {
        height: 100%;
        position: relative;
        transition: filter 0.2s ease-in-out;
    }
    .${barSegmentCls}:first-child { border-top-left-radius: 9999px; border-bottom-left-radius: 9999px; }
    .${barSegmentCls}:last-child { border-top-right-radius: 9999px; border-bottom-right-radius: 9999px; }

    .${barSegmentCls}:hover { filter: brightness(1.2); }
    .${barSegmentCls} .${tooltipCls} {
        visibility: hidden;
        width: max-content;
        background-color: black;
        color: #fff;
        text-align: center;
        border-radius: 6px;
        padding: 5px 10px;
        position: absolute;
        z-index: 50;
        bottom: 150%;
        left: 50%;
        transform: translateX(-50%);
        opacity: 0;
        transition: opacity 0.3s;
        border: 1px solid ${colors.cursor.gray};
        font-size: 12px;
        pointer-events: none;
    }
    .${barSegmentCls}:hover .${tooltipCls} {
        visibility: visible;
        opacity: 1;
    }
    .${barSegmentCls} .${tooltipCls}::after {
        content: "";
        position: absolute;
        top: 100%;
        left: 50%;
        margin-left: -5px;
        border-width: 5px;
        border-style: solid;
        border-color: black transparent transparent transparent;
    }
    .${legendCls} {
        margin-top: 15px;
        padding: 10px;
        background-color: #1e1e1e;
        border-radius: 6px;
        display: flex;
        flex-wrap: wrap;
        gap: 8px 15px;
    }
    .${legendItemCls} {
        display: flex;
        align-items: center;
        font-size: 12px;
        color: ${colors.cursor.lightGray};
    }
    .${legendColorBoxCls} {
        width: 12px;
        height: 12px;
        margin-right: 6px;
        border: 1px solid #444;
        flex-shrink: 0;
    }
  `;

    const genHr = () => $('<hr>').addClass(hrCls);

    // --- Data Parsing Functions ---
    const parseUsageEventsTable = () => {
        const modelUsage = {};
        let totalPaidRequests = 0;
        let totalRequests = 0;
        let erroredRequests = 0;

        const table = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr');

        if (table.length === 0) {
            error("Recent Usage Events table: Not found or empty.");
            return { modelUsage, totalPaidRequests, totalRequests, erroredRequests };
        }

        table.each((_, row) => {
            const $row = $(row);
            const model = $row.find('td:eq(1)').text().trim();
            const status = $row.find('td:eq(2)').text().trim();
            const requestsStr = $row.find('td:eq(3)').text().trim();
            const requests = parseFloat(requestsStr) || 0;

            if (status !== 'Errored, Not Charged' && model) {
                totalRequests += requests;
                if (!modelUsage[model]) {
                    modelUsage[model] = { count: 0, cost: 0 };
                }
                modelUsage[model].count += requests;

                if (status === 'Usage-based') {
                    totalPaidRequests += requests;
                }
            } else if (status === 'Errored, Not Charged') {
                erroredRequests += 1;
            }
        });
        return { modelUsage, totalPaidRequests, totalRequests, erroredRequests };
    };

    const parseCurrentUsageCosts = (modelUsage) => {
        let overallTotalCost = 0;
        const costTable = $('p:contains("Current Usage"):last').closest('div').find('table tbody tr');

        if (costTable.length === 0) {
             error("Current Usage (Cost) table: Not found or empty.");
             for (const modelKey in modelUsage) {
                if (!modelUsage[modelKey].hasOwnProperty('cost')) {
                    modelUsage[modelKey].cost = 0;
                }
             }
             return { overallTotalCost };
        }

        for (const modelKey in modelUsage) {
            modelUsage[modelKey].cost = 0;
        }
        if (!modelUsage['Extra/Other Premium']) {
            modelUsage['Extra/Other Premium'] = { count: 0, cost: 0 };
        }
         if (!modelUsage['Other Costs']) {
            modelUsage['Other Costs'] = { count: 0, cost: 0 };
        }

        costTable.each((_, row) => {
            const $row = $(row);
            const description = $row.find('td:eq(0)').text().trim().toLowerCase();
            const costStr = $row.find('td:eq(1)').text().trim().replace('$', '');
            const cost = parseFloat(costStr) || 0;
            overallTotalCost += cost;
            if (cost <= 0 && description.includes('paid for')) {
                return;
            }

            let foundModel = false;
            for (const modelKey in modelUsage) {
                if (modelKey === 'Extra/Other Premium' || modelKey === 'Other Costs') continue;
                if (description.includes(modelKey.toLowerCase())) {
                    modelUsage[modelKey].cost += cost;
                    foundModel = true;
                }
            }

            if (!foundModel && (description.includes('extra') || description.includes('premium')) && cost > 0 ) {
                 modelUsage['Extra/Other Premium'].cost += cost;
                 foundModel = true;
            }
            if (!foundModel && cost > 0) {
                modelUsage['Other Costs'].cost += cost;
            }
        });

        if (modelUsage['Extra/Other Premium'] && modelUsage['Extra/Other Premium'].cost === 0 && modelUsage['Extra/Other Premium'].count === 0) {
            delete modelUsage['Extra/Other Premium'];
        }
        if (modelUsage['Other Costs'] && modelUsage['Other Costs'].cost === 0 && modelUsage['Other Costs'].count === 0) {
            delete modelUsage['Other Costs'];
        }

        return { overallTotalCost };
    };

    const getBaseUsageData = () => {
        const premiumLabel = $('span:contains("Premium models")').first();
        if (premiumLabel.length === 0) {
            return {};
        }
        const usageSpan = premiumLabel.siblings('span').last();
        const usageText = usageSpan.text();
        const regex = /(\d+) \/ (\d+)/;
        const matches = usageText.match(regex);
        if (matches && matches.length === 3) {
            return { used: parseInt(matches[1], 10), total: parseInt(matches[2], 10) };
        }
        return {};
    };

    // --- Display Functions ---

    const createGenericProgressBar = (modelUsage, weightField, modelToColorMap) => {
        const barContainer = $('<div>').addClass(multiBarCls);
        const totalWeight = Object.values(modelUsage)
                                .reduce((sum, model) => sum + (model[weightField] || 0), 0);

        if (totalWeight === 0) {
            return barContainer.text(`No data for progress bar.`).css({ height: 'auto', padding: '5px' });
        }

        const sortedModels = Object.entries(modelUsage)
            .filter(([_, data]) => (data[weightField] || 0) > 0)
            .sort(([, a], [, b]) => (b[weightField] || 0) - (a[weightField] || 0));

        sortedModels.forEach((entry) => {
            const [model, data] = entry;
            const percentage = (data[weightField] / totalWeight) * 100;
            const reqCount = (data.count || 0).toFixed(1);
            const costAmount = (data.cost || 0).toFixed(2);
            const color = modelToColorMap[model] || colors.cursor.gray;

            const tooltipText = `${model}: ${reqCount} reqs ($${costAmount})`;
            const tooltip = $('<span>').addClass(tooltipCls).text(tooltipText);
            const segment = $('<div>')
                .addClass(barSegmentCls)
                .css({ width: `${percentage}%`, backgroundColor: color })
                .append(tooltip);
            barContainer.append(segment);
        });
        return barContainer;
    };

     const createLegend = (modelToColorMap, modelUsage) => {
        const legendContainer = $('<div>').addClass(legendCls);

        // Get models that are in the color map (meaning they have some usage/cost)
        const modelsInLegend = Object.keys(modelToColorMap);

        // Sort these models by their usage count (descending)
        const sortedModelsForLegend = modelsInLegend.sort((modelA, modelB) => {
            const countA = modelUsage[modelA]?.count || 0;
            const countB = modelUsage[modelB]?.count || 0;
            return countB - countA; // Sort descending by request count
        });

        for (const model of sortedModelsForLegend) {
            const color = modelToColorMap[model];
            const count = (modelUsage[model]?.count || 0).toFixed(1);
            const cost = (modelUsage[model]?.cost || 0).toFixed(2);

            const colorBox = $('<span>')
                .addClass(legendColorBoxCls)
                .css('background-color', color);
            const legendItem = $('<div>')
                .addClass(legendItemCls)
                .append(colorBox)
                .append(document.createTextNode(`${model}`));
            legendContainer.append(legendItem);
        }
        return legendContainer;
    };

    const displayEnhancedTrackerData = () => {
        debug('displayEnhancedTrackerData: Function START');
        const tracker = $c(mainCaptionCls);
        if (tracker.length === 0) {
            error('displayEnhancedTrackerData: Main caption element NOT FOUND.');
            return false;
        }
        debug(`displayEnhancedTrackerData: Found tracker caption element.`);
        tracker.siblings(`.${enhancedTrackerContainerCls}`).remove();

        const { modelUsage, totalPaidRequests, totalRequests, erroredRequests } = parseUsageEventsTable();
        const { overallTotalCost } = parseCurrentUsageCosts(modelUsage);
        const baseUsage = getBaseUsageData();

        const modelToColorMap = {};
        let colorPaletteIndex = 0;
        const uniqueModelsInUsage = Object.keys(modelUsage)
                                      .filter(modelName => (modelUsage[modelName].count || 0) > 0 || (modelUsage[modelName].cost || 0) > 0);

        for (const modelName of uniqueModelsInUsage) {
            modelToColorMap[modelName] = colors.modelColorPalette[colorPaletteIndex % colors.modelColorPalette.length];
            colorPaletteIndex++;
        }
        debug('displayEnhancedTrackerData: modelToColorMap built:', modelToColorMap);

        const container = $('<div>').addClass(enhancedTrackerContainerCls);
        const statsContainer = $('<div>').addClass(statsContainerCls);

        const addStat = (label, value) => {
             statsContainer.append(
                $('<div>').addClass(statItemCls).append(
                    $('<span>').addClass('label').text(`${label}: `),
                    $('<span>').addClass('value').text(value)
                )
            );
        };

        addStat('Usage-Based Requests', totalPaidRequests.toFixed(1));
        addStat('Total Requests', (totalPaidRequests+baseUsage.used).toFixed(1));
        addStat('Errored Requests', erroredRequests);


        container.append(statsContainer);
        container.append($('<p>').css({ fontSize: '13px', color: colors.cursor.gray, marginTop: '15px', marginBottom: '2px' })
            .text('Model Usage Breakdown:')
        );
        container.append(createGenericProgressBar(modelUsage, 'count', modelToColorMap));


        if (Object.keys(modelToColorMap).length > 0) {
            // Pass modelUsage to createLegend to access counts for sorting and display
            container.append(createLegend(modelToColorMap, modelUsage));
        }

        debug('displayEnhancedTrackerData: Stats container PREPARED. Appending to DOM...');
        tracker.after(container);
        debug('displayEnhancedTrackerData: Enhanced tracker data supposedly displayed.');
        return true;
    };

    // --- Core Script Functions ---
    const decorateUsageCard = () => {
        debug("decorateUsageCard: Function START");
        if ($c(mainCaptionCls).length > 0) {
            debug("decorateUsageCard: Card already decorated.");
            return true;
        }
        const usageHeading = $('h2:contains("Usage")');
        if (usageHeading.length > 0) {
            debug(`decorateUsageCard: Found 'h2:contains("Usage")' (count: ${usageHeading.length}). Decorating...`);
            const caption = $('<div>')
                .addClass('font-medium gt-standard-mono text-xl/[1.375rem] font-semibold -tracking-4 md:text-2xl/[1.875rem]')
                .addClass(mainCaptionCls)
                .text('Usage Tracker');

            usageHeading.after(genHr(), caption);
            debug(`decorateUsageCard: Added tracker caption. Check DOM for class '${mainCaptionCls}'. Current count in DOM: ${$c(mainCaptionCls).length}`);
            return true;
        }
        debug("decorateUsageCard: 'h2:contains(\"Usage\")' NOT FOUND.");
        return false;
    };

    const addUsageTracker = () => {
        debug("addUsageTracker: Function START");
        const success = displayEnhancedTrackerData();
        debug(`addUsageTracker: displayEnhancedTrackerData returned ${success}`);
        return success;
    };

    // --- Main Execution Logic ---
    const state = {
        addingUsageTrackerSucceeded: false,
        addingUsageTrackerAttempts: 0,
    };

    const ATTEMPTS_LIMIT = 15;
    const ATTEMPTS_INTERVAL = 750;
    const ATTEMPTS_MAX_DELAY = 5000;

    const main = () => {
        state.addingUsageTrackerAttempts++;
        log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts}...`);

        const scheduleNextAttempt = () => {
            if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) {
                const delay = Math.min(ATTEMPTS_INTERVAL * (state.addingUsageTrackerAttempts), ATTEMPTS_MAX_DELAY);
                log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts} incomplete/failed. Retrying in ${delay}ms...`);
                setTimeout(main, delay);
            } else if (state.addingUsageTrackerSucceeded) {
                log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts} SUCCEEDED.`);
            } else {
                error(`Main Execution: All ${ATTEMPTS_LIMIT} attempts FAILED. Could not add Usage Tracker.`);
            }
        };

        debug("Main Execution: Calling decorateUsageCard...");
        const decorationOkay = decorateUsageCard();
        debug(`Main Execution: decorateUsageCard returned ${decorationOkay}`);

        if (!decorationOkay) {
             scheduleNextAttempt();
             return;
        }

        debug("Main Execution: Checking for Recent Usage Events table...");
        const usageEventsTable = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr');
        if (usageEventsTable.length === 0) {
            debug("Main Execution: 'Recent Usage Events' table NOT FOUND YET.");
            scheduleNextAttempt();
            return;
        }
        debug(`Main Execution: 'Recent Usage Events' table FOUND (${usageEventsTable.length} rows).`);

        debug("Main Execution: Attempting to add/update tracker UI via addUsageTracker...");
        try {
            state.addingUsageTrackerSucceeded = addUsageTracker();
        } catch (e) {
            error("Main Execution: CRITICAL ERROR during addUsageTracker call:", e);
            state.addingUsageTrackerSucceeded = false;
        }
        debug(`Main Execution: addUsageTracker process finished. Success: ${state.addingUsageTrackerSucceeded}`);
        scheduleNextAttempt();
    };

    $(document).ready(() => {
        log('Document ready. Script starting...');
        window.ut = {
            jq: $,
            parseEvents: parseUsageEventsTable,
            parseCosts: parseCurrentUsageCosts,
            getBase: getBaseUsageData
        };
        $('head').append($('<style>').text(styles));
        setTimeout(main, ATTEMPTS_INTERVAL);
    });

})();