Claude Inline Usage Tracker

Minimal usage bar below Claude input

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
})();