SLOPSHOPPER

Cache Timer

Shows in the footer how long the prompt cache stays warm, so you ask your next question before it expires

newspinnercommandtimer
v1.0.1MITupdated 2026-10-06artemvoidd/cache-timer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-timer
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /cache-ttl ⎿ cache-timer: Use /cache-ttl 5 or /cache-ttl 60. Now: 60 min ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⬤ 60 min & auto-accept edits
README

Cache Timer

A Claude Code mod that shows how long your prompt cache stays warm, right in the footer next to the model name.

Cache Timer in the footer

Every message you send, Claude Code re-sends the whole conversation. The prompt cache lets the model skip re-reading everything before, so replies come faster and use less of your plan. But the cache expires, and the first message after that reloads the whole conversation. Claude Code doesn't show how much time is left. Cache Timer does.

FooterMeaning
⬤ 60 min (green)Fresh. Stays at full time while Claude is answering
⬤ 8 min (amber) → ⬤ 2 min (red)Cooling down. Amber under 10 min, red under 3
⬤ 1:45 (red)Last two minutes, counted in seconds. Ask now
⬤ cache cold (grey)Expired. Your next message reloads the conversation

Install

Requires Claude Code 2.1.287 or later (mods). Works in the terminal and in the Code tab of the Claude desktop app.

claude plugin marketplace add artemvoidd/cache-timer
claude plugin install cache-timer@artemvoidd

Start a new session. The timer appears after the first answer.

In the desktop app you can also ask Claude in the Code tab: “Install the plugin cache-timer from the marketplace artemvoidd/cache-timer”, then open a new chat.

Settings

  • Cache lifetime. Claude Code uses a 1-hour cache for most sessions and a 5-minute one when you are in usage overage. The mod starts at 60 minutes and picks up the real value whenever you switch models. Set it by hand with /cache-ttl 5 or /cache-ttl 60; the choice is remembered.
  • Language. en (default) or ru, in /config or in ~/.claude/settings.json:
  { "pluginConfigs": { "cache-timer@artemvoidd": { "options": { "language": "ru" } } } }

How it works

The mod hooks turn.start and turn.complete to note when the main conversation last talked to the model, and redraws the footer (SessionMode) every 5 seconds. It makes no network requests, reads no files and runs no commands — claude plugin validate lists its calls:

hooks: session.start, turn.start, turn.complete, classic.PostModelSwitch, command.run{command=cache-ttl}, ui.render{component=SessionMode}
calls: $.clock.every, $.clock.now, $.command.register, $.store.get, $.store.set, $.ui.invalidate, $.ui.resolve

What each hook does:

HookWhat it doesChanges anything?
session.startReads the saved TTL, registers /cache-ttl, starts a 5-second redraw timerNo
turn.start, turn.completeNotes the time of the main conversation's last request (subagent turns are ignored)No, passes the event on unchanged
classic.PostModelSwitchReads cache_ttl (5m or 1h) that Claude Code reports on a model switchNo, passes the event on unchanged
command.run (cache-ttl only)Answers /cache-ttl 5 and /cache-ttl 60 and saves the choice in the mod's own storeOnly its own command; other commands are not touched
ui.render (SessionMode only)Adds the dot and the time in front of the footer's mode labels, which stay as they wereOnly the footer labels

The countdown starts from the end of the last answer, so it can be a few seconds off from the exact API request.


По-русски

Мод для Claude Code: в футере рядом с моделью показывает, сколько ещё живёт кэш промпта. Кружок меняет цвет по мере остывания: зелёный → жёлтый → красный, рядом минуты, в последние две — секунды. Когда «кэш остыл» (серый кружок), следующее сообщение заново прогоняет весь диалог — успей задать вопрос до этого.

Установка — две команды выше. Русские подписи: "language": "ru" в настройках плагина (пример выше). Время жизни кэша: /cache-ttl 5 или /cache-ttl 60.

Privacy

Cache Timer collects and sends nothing. See PRIVACY.md.

License

MIT

Source 1 files
hooks/register.tsx 107 lines
1// Prompt cache countdown in the footer, next to the model label:
2// a dot whose color cools from green to red, and the minutes left beside it.
3import type { Register } from 'claude-code'
4
5const MINUTE = 60_000
6
7let ttlMs = 60 * MINUTE   // 1h by default; /cache-ttl 5 switches to 5 minutes
8let lastActivity = 0      // ms of the last main-loop request (0 = no turn yet)
9let working = false       // while Claude answers, the cache stays fresh
10
11// Muted, so the footer stays quiet; only the glyph carries the color
12const GREEN = '#6a9a78'
13const AMBER = '#b39552'
14const RED = '#b8645c'
15const GREY = '#7d8590'
16
17const WORDS = {
18  en: { min: 'min', cold: 'cache cold' },
19  ru: { min: 'мин', cold: 'кэш остыл' },
20}
21let words = WORDS.en
22
23export const register: Register = (on, options) => {
24  words = options?.language === 'ru' ? WORDS.ru : WORDS.en
25
26  on('session.start', async ($, e, next) => {
27    const saved = await $.store.get('ttlMinutes')
28    if (saved === 5 || saved === 60) ttlMs = saved * MINUTE
29    await $.command.register({
30      name: 'cache-ttl',
31      description: 'Prompt cache timer: /cache-ttl 5 or /cache-ttl 60 (minutes)',
32    })
33    // Redraw the footer so the minutes tick down
34    $.clock.every(5000, () => $.ui.invalidate('ui.render'))
35    return next(e)
36  })
37
38  // Only the main loop raises turn.start; subagents don't
39  on('turn.start', async ($, e, next) => {
40    working = true
41    lastActivity = await $.clock.now()
42    $.ui.invalidate('ui.render')
43    return next(e)
44  })
45
46  on('turn.complete', async ($, e, next) => {
47    if (!e.agentId) {
48      working = false
49      lastActivity = await $.clock.now()
50      $.ui.invalidate('ui.render')
51    }
52    return next(e)
53  })
54
55  // A model switch reports the TTL Claude Code actually uses
56  on('classic.PostModelSwitch', async ($, e, next) => {
57    ttlMs = (e.cache_ttl === '5m' ? 5 : 60) * MINUTE
58    $.ui.invalidate('ui.render')
59    return next(e)
60  })
61
62  on('command.run', { command: 'cache-ttl' }, async ($, e) => {
63    const minutes = parseInt(String(e.args || '').trim(), 10)
64    if (minutes !== 5 && minutes !== 60) {
65      return { text: 'Use /cache-ttl 5 or /cache-ttl 60. Now: ' + ttlMs / MINUTE + ' min' }
66    }
67    ttlMs = minutes * MINUTE
68    await $.store.set('ttlMinutes', minutes)
69    $.ui.invalidate('ui.render')
70    return { text: 'Cache timer: ' + minutes + ' min' }
71  })
72
73  // The labels at the right of the prompt footer, beside the model.
74  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
75    if (!lastActivity) return next(e)
76    const left = working ? ttlMs : lastActivity + ttlMs - (await $.clock.now())
77    const rest = e.props.modes.join(' & ')
78
79    // The desktop footer draws text only (an Svg there shows nothing), so the
80    // progress is a large dot whose color tracks the time left
81    const { Box, Text } = $.ui.resolve(e)
82    return (
83      <Box flexDirection="row" columnGap={1}>
84        <Text color={color(left)}>{'⬤'}</Text>
85        <Text dimColor>{label(left)}</Text>
86        {rest ? <Text dimColor>{' & ' + rest}</Text> : null}
87      </Box>
88    )
89  })
90}
91
92function label(left: number): string {
93  if (left <= 0) return words.cold
94  if (left < 2 * MINUTE) {
95    const s = Math.ceil(left / 1000)
96    return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0')
97  }
98  return Math.ceil(left / MINUTE) + ' ' + words.min
99}
100
101function color(left: number): string {
102  if (left <= 0) return GREY
103  if (left > 10 * MINUTE) return GREEN
104  return left > 3 * MINUTE ? AMBER : RED
105}
106
107