SLOPSHOPPER

cache-watch

Shows prompt-cache reads/writes, cache expiry countdown, and the re-write cost of switching model.

newbandtimer
v0.1.0no licenseupdated 2026-10-09augustoFranke/cache-watch
A shopper browsing a rack in a slop shop
README

cache-watch

A Claude Code mod that adds a band above the prompt:

ctx 125k · read 125k · written 0.3k · cache expires in 4:40

  • ctx: current context size (same figure as the context window meter).
  • read / written: prompt-cache tokens read / written by the last request.
  • Countdown: time left on the cache, assuming a 5-minute TTL from the last request.
  • After you switch model, the band turns dark red: the next prompt will not read the old cache and rewrites the whole context.

Install

/plugin install cache-watch --marketplace augustoFranke/cache-watch

Answer y to add the marketplace, then pick a scope.

Source 2 files
hooks/register.tsx 83 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { LastTurn } from '../types'
5
6const TTL_MS = 5 * 60 * 1000
7const last = atom({ plugin: 'cache-watch', key: 'last' } as const, null)
8const tick = atom({ plugin: 'cache-watch', key: 'tick' } as const, 0)
9
10const k = (n: number) => (n >= 1000 ? `${Math.round(n / 100) / 10}k` : `${n}`)
11const clock = (ms: number) => {
12  const s = Math.max(0, Math.round(ms / 1000))
13  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
14}
15
16export const register: Register = on => {
17  on('session.start', ($, e, next) => {
18    void $.clock.every(1000, () => {
19      void update($, tick, n => n + 1)
20      $.ui.invalidate('ui.render')
21    })
22
23    return next(e)
24  })
25
26  on('turn.step', async function* ($, e, next) {
27    const stream = next(e)
28
29    while (true) {
30      const { done, value } = await stream.next()
31
32      if (done) {
33        return value
34      }
35
36      if (value.kind === 'stop' && value.usage && e.agentId === undefined) {
37        const u = value.usage
38        const turn: LastTurn = {
39          model: u.model,
40          read: u.cache_read_input_tokens,
41          written: u.cache_creation_input_tokens,
42          fresh: u.input_tokens,
43          endedAt: await $.clock.now(),
44        }
45        await update($, last, () => turn)
46      }
47
48      yield value
49    }
50  })
51
52  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
53    await read($, tick)
54    const turn = await read($, last)
55
56    if (e.props.hasSurvey || turn === null) {
57      return next(e)
58    }
59
60    const now = await $.clock.now()
61    const model = await $.session.model()
62    const left = turn.endedAt + TTL_MS - now
63    const ctx = (await $.session.usage()).context.tokens ?? turn.read + turn.written + turn.fresh
64    const isSwitched = !turn.model.includes(model) && !model.includes(turn.model)
65    const { Box, Text } = $.ui.resolve(e)
66
67    const cache =
68      left > 0 ? `cache expires in ${clock(left)}` : 'cache expired (next prompt rewrites everything)'
69    const warn = isSwitched
70      ? ` · model switched: next prompt will NOT read cache, rewrites ~${k(ctx)} tokens`
71      : ''
72
73    return (
74      <Box>
75        <Text dimColor={!isSwitched && left > 0} color={isSwitched || left <= 0 ? 'rgb(192,71,66)' : undefined}>
76          ctx {k(ctx)} · read {k(turn.read)} · written {k(turn.written)} · {cache}
77          {warn}
78        </Text>
79      </Box>
80    )
81  })
82}
83
types/index.d.ts 14 lines
1export type LastTurn = {
2  model: string
3  read: number
4  written: number
5  fresh: number
6  endedAt: number
7}
8
9declare module 'claude-code' {
10  interface PluginState {
11    'cache-watch': { last: LastTurn | null; tick: number }
12  }
13}
14