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

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./plugin install cache-watch --marketplace augustoFranke/cache-watch
Answer y to add the marketplace, then pick a scope.
hooks/register.tsx 83 lines1import { 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}
83types/index.d.ts 14 lines1export 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