Muestra el uso restante de los límites de la suscripción (5 h y semanal) y cuándo se reinician

Tres mods para Claude Code que hice porque me cansé de escribir comandos para cosas que quería ver de un vistazo o cambiar con un clic.
Si usas Claude Code con una suscripción de claude.ai, tienes un límite que se reinicia cada 5 horas y otro semanal. Este mod los muestra debajo del prompt, con la hora del reinicio y cuánto falta:
Sesión 5h ▰▰▰▱▱▱▱▱▱▱ 70% libre · reinicia 16:15 (en 2h 15m) │ Semana ▰▰▰▰▰▰▱▱▱▱ 37.5% libre · reinicia vie 09:00 (en 4d 1h)
La cuenta regresiva se actualiza cada minuto. Cuando pasas del 80% y del 95% de un límite te salta un aviso (uno por periodo, no cada vez que respondes). Con /limites ves el detalle en texto.
Ojo con dos cosas. Anthropic no da el número de tokens que te quedan, solo el porcentaje usado, así que eso es lo que muestra. Y al abrir una sesión no hay datos hasta que llega la primera respuesta del modelo. Si usas una API key en vez de suscripción, este mod no tiene nada que mostrar.
Una línea pequeña encima del prompt con el modelo y el esfuerzo que estás usando:
⚙ Opus 5.5 · esfuerzo Alto (clic para cambiar)
Al hacer clic se abre esto:
Modelo: [Fable] [ Opus ] [Sonnet] [Haiku]
Versión: [Opus · último $4/$20] [Opus · 1M contexto $4/$20] [Opus Plan $4/$20 → $2/$10] [ Opus 5.5 $4/$20 ]
precio API en USD por millón de tokens: entrada/salida
Esfuerzo: [Auto] [Bajo] [Medio] [ Alto ] [Muy alto] [Máximo] ✕ cerrar
Las versiones salen del menú /config de tu cuenta, así que ves las que tu plan permite. El precio al lado de cada una es el de lista de la API. Con suscripción no pagas por token, pero sirve para comparar: Fable 5.1 gasta unas dos veces y media lo que Opus 5.5, y Haiku casi nada.
Los precios los escribí a mano en plugins/selector-modelo/hooks/register.tsx (octubre de 2026). Si cambian, hay que actualizarlos ahí. Un modelo que el mod no conoce aparece sin precio en vez de con uno inventado.
Para que los clics funcionen en la terminal necesitas el modo de pantalla completa ("tui": "fullscreen" en tu settings.json). Sin mouse, ctrl+x y luego tab te lleva a los botones.
El esfuerzo no pasa por /effort. En algunas versiones ese comando responde "Kept effort level as auto" y no cambia nada, así que el mod pone el nivel que elegiste en cada petición al modelo. Se aplica desde la siguiente respuesta y se recuerda entre sesiones. Con Auto vuelves a dejar que Claude Code decida. Los subagentes siguen con su propio esfuerzo, y en un modelo que no usa niveles de esfuerzo el botón no hace nada.
Para ver una imagen o un video sin salir de Claude Code ni abrir el explorador de archivos:
/ver captura.png
/ver ~/Videos/demo.mp4
/ver # galería de la carpeta actual
Se abre una ventanita flotante sin bordes en la esquina inferior derecha de la pantalla, siempre encima, con la imagen o el video en su calidad real (480×360 como mucho) y un botón ✕ en su esquina para cerrarla. También la cierran Esc o q, y un video se cierra solo al terminar. Arrastrándola la mueves, y en un video un clic o espacio pausan. La ventana es un script pequeño de Python con GTK y GStreamer (plugins/visor-medios/viewer/preview.py), que Ubuntu con GNOME ya trae; donde falten, se usa ffplay, igual de flotante pero sin el botón.
Donde no hay escritorio gráfico (por SSH, por ejemplo) se usa un panel dentro de la terminal que dibuja con bloques de colores (▀, dos píxeles por celda). Ahí la vista previa es de 64×16 celdas como mucho, pixeleada pero suficiente para reconocer qué es un archivo, y tiene botones para pausar, adelantar o atrasar 5 segundos, silenciar y pasar al archivo anterior o siguiente. Con el panel enfocado sirven las teclas p, a/d, m, j/k, g y Esc.
Con /ver sobre una carpeta, la galería siempre es un panel en la terminal: das clic en un archivo y se abre la vista previa.
También le puedes pedir a Claude "muéstrame la imagen que generaste" y la abre él, porque el mod le da una herramienta para eso.
Si Claude escribe en su respuesta un enlace a una imagen o un video (https://…/foto.jpg, file:///…/clip.mp4), dale un clic y la vista previa se abre directo junto al puntero, sin tapar el enlace (con /ver o cuando la abre Claude, en la esquina inferior derecha). Los demás enlaces se abren como siempre, y ctrl+clic abre cualquiera en el navegador. /ver también acepta URLs. El tipo se reconoce por la extensión, así que un enlace sin .jpg, .mp4 o similar no ofrece el visor.
Necesita ffmpeg y ffprobe, y para la ventana Python 3 con GTK 3 y GStreamer (o, en su defecto, ffplay). La ventana se coloca con lo que dice xrandr; en Wayland funciona a través de XWayland, como en Ubuntu con GNOME.
Dentro de Claude Code, en la terminal:
/plugin install limites-uso --marketplace jairsemblantes01/claude-code-mods
/plugin install selector-modelo --marketplace jairsemblantes01/claude-code-mods
/plugin install visor-medios --marketplace jairsemblantes01/claude-code-mods
La primera vez te pregunta si quieres agregar el marketplace: responde y y luego elige el alcance (con Enter queda para tu usuario).
Desde la shell también funciona:
claude plugin install limites-uso --marketplace jairsemblantes01/claude-code-mods
claude plugin install selector-modelo --marketplace jairsemblantes01/claude-code-mods
claude plugin install visor-medios --marketplace jairsemblantes01/claude-code-mods
En la app de escritorio no existe /plugin. Ahí es: el botón + junto al prompt, Plugins, Add marketplace con jairsemblantes01/claude-code-mods, y después Add plugin.
Cada mod está en plugins/<nombre>/. El código vive en hooks/ y los tests en tests/. Para probar cambios sin instalar nada:
claude --plugin-dir plugins/selector-modelo
Y para validar y correr los tests:
claude plugin validate plugins/selector-modelo
claude plugin test plugins/selector-modelohooks/register.ts 115 lines1import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
2
3const LABELS: Record<string, string> = {
4 five_hour: 'Sesión 5h',
5 seven_day: 'Semana',
6 spend_limit: 'Gasto',
7}
8
9// Umbrales de % usado en los que se avisa con un toast (una vez por ventana y reinicio)
10const ALERTS = [80, 95]
11
12export function formatRemaining (resetsAt: string | undefined, now: number): string {
13 if (!resetsAt) return ''
14 const ms = Date.parse(resetsAt) - now
15 if (!(ms > 0)) return 'reiniciando'
16 const totalMin = Math.ceil(ms / 60000)
17 const days = Math.floor(totalMin / 1440)
18 const hours = Math.floor((totalMin % 1440) / 60)
19 const min = totalMin % 60
20 if (days > 0) return `${days}d ${hours}h`
21 if (hours > 0) return `${hours}h ${String(min).padStart(2, '0')}m`
22 return `${min}m`
23}
24
25export function formatClock (resetsAt: string, now: number): string {
26 const date = new Date(resetsAt)
27 const hh = String(date.getHours()).padStart(2, '0')
28 const mm = String(date.getMinutes()).padStart(2, '0')
29 const sameDay = new Date(now).toDateString() === date.toDateString()
30 if (sameDay) return `${hh}:${mm}`
31 const days = ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb']
32 return `${days[date.getDay()]} ${hh}:${mm}`
33}
34
35function bar (percentUsed: number): string {
36 const filled = Math.max(0, Math.min(10, Math.round(percentUsed / 10)))
37 return '▰'.repeat(filled) + '▱'.repeat(10 - filled)
38}
39
40export function formatLimits (limits: SessionRateLimit[], now: number): string | undefined {
41 if (limits.length === 0) return undefined
42 return limits
43 .map(limit => {
44 const label = LABELS[limit.kind] ?? limit.kind
45 const left = Math.max(0, Math.round((100 - limit.percentUsed) * 10) / 10)
46 const reset = limit.resetsAt
47 ? ` · reinicia ${formatClock(limit.resetsAt, now)} (en ${formatRemaining(limit.resetsAt, now)})`
48 : ''
49 return `${label} ${bar(limit.percentUsed)} ${left}% libre${reset}`
50 })
51 .join(' │ ')
52}
53
54let limits: SessionRateLimit[] = []
55// kind:resetsAt:umbral ya avisados, para no repetir el toast
56const alerted = new Set<string>()
57
58async function refresh ($: EngineInterface) {
59 const now = await $.clock.now()
60 $.ui.status(formatLimits(limits, now) ?? 'Límites de uso: esperando la primera respuesta…')
61}
62
63function checkAlerts ($: EngineInterface) {
64 for (const limit of limits) {
65 for (const threshold of ALERTS) {
66 const key = `${limit.kind}:${limit.resetsAt ?? ''}:${threshold}`
67 if (limit.percentUsed >= threshold && !alerted.has(key)) {
68 alerted.add(key)
69 $.ui.toast(`⚠ ${LABELS[limit.kind] ?? limit.kind}: llevas ${limit.percentUsed}% usado`, { timeoutMs: 8000 })
70 }
71 }
72 }
73}
74
75export const register: Register = on => {
76 on('session.start', async ($, e, next) => {
77 const result = await next(e)
78 await $.command.register({
79 name: 'limites',
80 description: 'Muestra el uso de los límites de la suscripción y cuándo se reinician',
81 })
82 limits = (await $.session.usage()).rateLimits
83 await refresh($)
84 // La cuenta regresiva avanza aunque no haya turnos
85 $.clock.every(60_000, () => { void refresh($) })
86 return result
87 })
88
89 on('session.measure', async ($, e, next) => {
90 if (e.changed.includes('rateLimits')) {
91 limits = e.rateLimits
92 checkAlerts($)
93 }
94 await refresh($)
95 return next(e)
96 })
97
98 on('command.run', { command: 'limites' }, async $ => {
99 limits = (await $.session.usage()).rateLimits
100 await refresh($)
101 const now = await $.clock.now()
102 if (limits.length === 0) {
103 return { text: 'Aún no hay datos de límites: aparecen después de la primera respuesta del modelo (solo con suscripción de claude.ai).' }
104 }
105 const lines = limits.map(limit => {
106 const label = LABELS[limit.kind] ?? limit.kind
107 const reset = limit.resetsAt
108 ? `reinicia ${formatClock(limit.resetsAt, now)} (en ${formatRemaining(limit.resetsAt, now)})`
109 : 'sin hora de reinicio'
110 return `${label}: ${limit.percentUsed}% usado, ${Math.max(0, 100 - limit.percentUsed)}% libre — ${reset}`
111 })
112 return { text: lines.join('\n') }
113 })
114}
115