Greasy Fork is available in English.

Cursor Usage: show Requests + Cost

Add a Cost column after Requests on Cursor Usage page while keeping the original Requests column.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Cursor Usage: show Requests + Cost
// @namespace    https://cursor.com/
// @version      0.1.2
// @description  Add a Cost column after Requests on Cursor Usage page while keeping the original Requests column.
// @match        https://cursor.com/dashboard/usage*
// @match        https://cursor.com/*/dashboard/usage*
// @run-at       document-start
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  const PATCH_KEY = '__tmCursorUsageRequestsAndCost';

  const keyOf = (event) =>
    [
      event?.timestamp,
      event?.model,
      event?.requestsCosts,
      event?.owningUser || '',
    ].join('|');

  const formatCost = (cents) => {
    if (typeof cents !== 'number' || Number.isNaN(cents)) return '-';
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    }).format(cents / 100);
  };

  function install() {
    if (window[PATCH_KEY]?.cleanup) {
      window[PATCH_KEY].cleanup();
    }

    const state = {
      rawByKey: new Map(),
      observer: null,
      renderTimer: null,
      originalFetch: window.fetch.bind(window),
    };

    function getFiberRoot() {
      const table = document.querySelector('.dashboard-table-scroll-container');
      if (!table) return null;

      const fiberKey = Object.keys(table).find((k) => k.startsWith('__reactFiber$'));
      if (!fiberKey) return null;

      let fiber = table[fiberKey];
      while (fiber?.return) fiber = fiber.return;
      return fiber || null;
    }

    function walkFiber(visit) {
      const root = getFiberRoot();
      if (!root) return;

      const seen = new Set();

      const dfs = (node) => {
        if (!node || seen.has(node)) return;
        seen.add(node);
        visit(node);
        dfs(node.child);
        dfs(node.sibling);
      };

      dfs(root);
    }

    function getDisplayData() {
      let data = null;

      walkFiber((node) => {
        const props = node.memoizedProps;
        if (!data && props && Array.isArray(props.data) && Array.isArray(props.columns)) {
          data = props.data;
        }
      });

      return data;
    }

    function getCustomRange() {
      let range = null;

      walkFiber((node) => {
        const props = node.memoizedProps;
        if (!range && props?.customRange?.from && props?.customRange?.to) {
          range = props.customRange;
        }
      });

      return range;
    }

    function getPageSize() {
      const btn = [...document.querySelectorAll('button')].find((el) =>
        /^Rows:\s*\d+$/i.test((el.textContent || '').trim())
      );
      const match = btn?.textContent?.match(/(\d+)/);
      return match ? Number(match[1]) : 100;
    }

    function render() {
      const table = document.querySelector('.dashboard-table-scroll-container');
      const container = table?.querySelector('.dashboard-table-container');
      const headerRow = container?.querySelector('.dashboard-table-header-row');
      const rows = [...(container?.querySelectorAll('.dashboard-table-row') || [])];
      const displayData = getDisplayData();

      if (!table || !container || !headerRow || !rows.length || !displayData?.length) {
        return;
      }

      table.setAttribute('aria-colcount', '6');

      const minWidth = parseInt(container.style.minWidth || '761', 10);
      if (!Number.isNaN(minWidth) && minWidth < 881) {
        container.style.minWidth = '881px';
      }

      let headerCell = headerRow.querySelector('[data-tm-cost-header="1"]');
      if (!headerCell) {
        headerCell = document.createElement('div');
        headerCell.setAttribute('role', 'columnheader');
        headerCell.setAttribute('data-tm-cost-header', '1');
        headerCell.className = 'dashboard-table-header dashboard-table-header-align-right';
        headerCell.style.width = '120px';
        headerCell.style.flexShrink = '0';
        headerCell.style.flexGrow = '0';

        const span = document.createElement('span');
        span.className = 'truncate min-w-0';
        span.textContent = 'Cost';
        headerCell.appendChild(span);

        const requestsHeader = [...headerRow.children].find(
          (el) => el.textContent.trim() === 'Requests'
        );

        headerRow.insertBefore(headerCell, requestsHeader?.nextSibling || null);
      }

      rows.forEach((row, index) => {
        const displayEvent = displayData[index];
        if (!displayEvent) return;

        const raw = state.rawByKey.get(keyOf(displayEvent));
        const cents = raw?.tokenUsage?.totalCents;

        let cell = row.querySelector('[data-tm-cost-cell="1"]');
        if (!cell) {
          cell = document.createElement('div');
          cell.setAttribute('role', 'cell');
          cell.setAttribute('data-tm-cost-cell', '1');
          cell.className = 'dashboard-table-cell dashboard-table-cell-align-right';
          cell.style.width = '120px';
          cell.style.flexShrink = '0';
          cell.style.flexGrow = '0';
          cell.style.color = 'var(--text-secondary)';

          const cells = [...row.querySelectorAll(':scope > [role="cell"]')];
          const requestsCell = cells[cells.length - 1];
          row.insertBefore(cell, requestsCell?.nextSibling || null);
        }

        cell.textContent = formatCost(cents);
        cell.title = typeof cents === 'number' ? `${cents.toFixed(4)} cents` : '';
      });
    }

    function scheduleRender() {
      clearTimeout(state.renderTimer);
      state.renderTimer = setTimeout(render, 0);
    }

    function recordRawUsage(payload) {
      const events = payload?.usageEventsDisplay;
      if (!Array.isArray(events)) return;

      state.rawByKey.clear();
      for (const event of events) {
        state.rawByKey.set(keyOf(event), event);
      }

      scheduleRender();
    }

    window.fetch = async (...args) => {
      const response = await state.originalFetch(...args);

      try {
        const input = args[0];
        const url = typeof input === 'string' ? input : input?.url;

        if (url && url.includes('/api/dashboard/get-filtered-usage-events')) {
          response.clone().json().then(recordRawUsage).catch(() => {});
        }
      } catch {}

      return response;
    };

    state.observer = new MutationObserver(() => scheduleRender());
    state.observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });

    (async () => {
      try {
        const range = getCustomRange();
        if (!range?.from || !range?.to) return;

        const pageSize = getPageSize();
        const res = await state.originalFetch('/api/dashboard/get-filtered-usage-events', {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          credentials: 'same-origin',
          body: JSON.stringify({
            teamId: 0,
            startDate: String(new Date(range.from).getTime()),
            endDate: String(new Date(range.to).getTime()),
            page: 1,
            pageSize,
          }),
        });

        const payload = await res.json();
        recordRawUsage(payload);
      } catch {}
    })();

    state.cleanup = () => {
      clearTimeout(state.renderTimer);
      state.observer?.disconnect();
      window.fetch = state.originalFetch;

      document
        .querySelectorAll('[data-tm-cost-cell="1"], [data-tm-cost-header="1"]')
        .forEach((el) => el.remove());

      const table = document.querySelector('.dashboard-table-scroll-container');
      if (table) table.setAttribute('aria-colcount', '5');
    };

    window[PATCH_KEY] = state;
  }

  install();
})();