Last active
May 29, 2026 02:29
-
-
Save billygl/92d716b2d253396d378d60b8c660b862 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Claude Usage Time Progress Bars (Live Updates) | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2.1 | |
| // @description Adds live-updating time elapsed progress bars below token usage bars on Claude.ai settings | |
| // @author You | |
| // @match https://claude.ai/* | |
| // @run-at document-idle | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| let updateInterval = null; | |
| const activeTrackers = {}; | |
| const CONFIGS = { | |
| session: { | |
| id: 'session', | |
| totalMinutes: 5 * 60, // 5 hours | |
| parseTime: (text) => { | |
| let hrs = 0, mins = 0; | |
| const hrMatch = text.match(/(\d+)\s*hr/i); | |
| const minMatch = text.match(/(\d+)\s*min/i); | |
| if (hrMatch) hrs = parseInt(hrMatch[1], 10); | |
| if (minMatch) mins = parseInt(minMatch[1], 10); | |
| return Date.now() + ((hrs * 60 + mins) * 60 * 1000); | |
| } | |
| }, | |
| weekly: { | |
| id: 'weekly', | |
| totalMinutes: 7 * 24 * 60, // 7 days | |
| parseTime: (text) => { | |
| if (text.includes('hr') || text.includes('min')) { | |
| return CONFIGS.session.parseTime(text); | |
| } | |
| const match = text.match(/Resets\s+([A-Za-z]+)\s+(\d{1,2}):(\d{2})\s+(AM|PM)/i); | |
| if (!match) return Date.now(); | |
| const [ , dayStr, hrStr, minStr, ampm ] = match; | |
| const dayLower = dayStr.toLowerCase(); | |
| let hours = parseInt(hrStr, 10); | |
| const minutes = parseInt(minStr, 10); | |
| if (ampm.toUpperCase() === 'PM' && hours < 12) hours += 12; | |
| if (ampm.toUpperCase() === 'AM' && hours === 12) hours = 0; | |
| const now = new Date(); | |
| let dayDiff = 0; | |
| if (dayLower.startsWith('tod')) { | |
| dayDiff = 0; | |
| } else if (dayLower.startsWith('tom')) { | |
| dayDiff = 1; | |
| } else { | |
| const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; | |
| const targetDay = days.findIndex(d => dayLower.startsWith(d)); | |
| if (targetDay !== -1) { | |
| dayDiff = targetDay - now.getDay(); | |
| if (dayDiff < 0) dayDiff += 7; | |
| else if (dayDiff === 0) { | |
| const tempTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes, 0).getTime(); | |
| if (now.getTime() > tempTime) dayDiff += 7; | |
| } | |
| } | |
| } | |
| return new Date(now.getFullYear(), now.getMonth(), now.getDate() + dayDiff, hours, minutes, 0).getTime(); | |
| } | |
| } | |
| }; | |
| function updateAllBars() { | |
| const trackerIds = Object.keys(activeTrackers); | |
| if (trackerIds.length === 0 && updateInterval) { | |
| clearInterval(updateInterval); | |
| updateInterval = null; | |
| return; | |
| } | |
| trackerIds.forEach(id => { | |
| const timeRow = document.getElementById(`tm-time-row-${id}`); | |
| if (!timeRow) { | |
| delete activeTrackers[id]; | |
| return; | |
| } | |
| const targetTime = activeTrackers[id]; | |
| const config = CONFIGS[id]; | |
| const progressBar = document.getElementById(`tm-time-bar-${id}`); | |
| const progressText = document.getElementById(`tm-time-text-${id}`); | |
| const timeProgressRole = timeRow.querySelector('[role="progressbar"]'); | |
| const remainingMs = targetTime - Date.now(); | |
| const remainingMinutes = Math.max(0, remainingMs / 1000 / 60); | |
| let elapsedMinutes = config.totalMinutes - remainingMinutes; | |
| if (elapsedMinutes < 0) elapsedMinutes = 0; | |
| if (elapsedMinutes > config.totalMinutes) elapsedMinutes = config.totalMinutes; | |
| const elapsedPercent = ((elapsedMinutes / config.totalMinutes) * 100).toFixed(1); | |
| const progressLabel = `${elapsedPercent}% elapsed`; | |
| if (progressBar && progressText && timeProgressRole && progressText.textContent !== progressLabel) { | |
| progressBar.style.width = `${elapsedPercent}%`; | |
| timeProgressRole.setAttribute('aria-valuenow', elapsedPercent); | |
| progressText.textContent = progressLabel; | |
| } | |
| }); | |
| } | |
| function injectTrackers() { | |
| const usageRows = document.querySelectorAll('div.flex.w-full.flex-row.flex-wrap.items-center.justify-between'); | |
| usageRows.forEach(row => { | |
| const titleSpan = row.querySelector('span.text-primary'); | |
| if (!titleSpan) return; | |
| const title = titleSpan.textContent.trim(); | |
| const resetSpan = row.querySelector('span.text-secondary'); | |
| if (!resetSpan || !resetSpan.textContent.includes('Resets')) return; | |
| const rightContainer = row.querySelector('div.pl-6'); | |
| if (!rightContainer) return; | |
| let config; | |
| if (title === 'Current session') { | |
| config = CONFIGS.session; | |
| } else if (title === 'All models') { | |
| config = CONFIGS.weekly; | |
| } else { | |
| return; | |
| } | |
| if (!activeTrackers[config.id]) { | |
| activeTrackers[config.id] = config.parseTime(resetSpan.textContent); | |
| } | |
| let timeRow = document.getElementById(`tm-time-row-${config.id}`); | |
| if (!timeRow) { | |
| const wrapper = document.createElement('div'); | |
| wrapper.id = `tm-time-wrapper-${config.id}`; | |
| wrapper.className = 'flex flex-col flex-1 justify-center gap-1'; | |
| rightContainer.parentNode.insertBefore(wrapper, rightContainer); | |
| wrapper.appendChild(rightContainer); | |
| rightContainer.classList.remove('flex-1'); | |
| timeRow = document.createElement('div'); | |
| timeRow.id = `tm-time-row-${config.id}`; | |
| timeRow.className = 'flex items-center gap-3 pl-6 md:max-w-xl'; | |
| timeRow.innerHTML = ` | |
| <div class="min-w-[200px] flex-1"> | |
| <div role="progressbar" aria-label="Time Elapsed" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="relative flex h-2 w-full items-center overflow-hidden rounded-full bg-alpha-2"> | |
| <div id="tm-time-bar-${config.id}" class="h-full rounded-full transition-all bg-fill-accent" style="width: 0%;"></div> | |
| </div> | |
| </div> | |
| <span id="tm-time-text-${config.id}" class="min-w-[5.5rem] whitespace-nowrap text-right text-footnote text-secondary">0.0% elapsed</span> | |
| `; | |
| wrapper.appendChild(timeRow); | |
| } | |
| }); | |
| updateAllBars(); | |
| if (Object.keys(activeTrackers).length > 0 && !updateInterval) { | |
| updateInterval = setInterval(updateAllBars, 60000); | |
| } | |
| } | |
| const observer = new MutationObserver(() => { | |
| injectTrackers(); | |
| }); | |
| // Fallback to documentElement if body somehow still isn't ready | |
| const targetNode = document.body || document.documentElement; | |
| observer.observe(targetNode, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
you will have something like this using Tampermonkey
