SLOPSHOPPER

context-weather

Bar above the prompt: context fill, session cost and plan limits, with a warning before compaction.

newbandguardtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-weather
› 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 Context █████░░░░░ 49% (97k/200k) · $0.42 · 5h 31% [ Hide ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Context █████░░░░░ 49% (97k/200k) · $0.42 · 5h 31% [ Hide ]
README

claude-mods

Function-hook mods for Claude Code (terminal and desktop). Each one adds a small piece of UI or a command to your session.

ModWhat it does
usage-limitsA bar above the prompt: plan limit left (5 hours, week) and time to reset.
proc-registry/procs command and a Processes panel: background shells and subagents with ID, port and start time, plus a counter in the status line.
context-weatherA bar above the prompt: context fill, session cost and plan limits, with a warning before compaction.

Install

claude plugin marketplace add Chronosauros/claude-mods
claude plugin install usage-limits@chronosaur-mods
claude plugin install proc-registry@chronosaur-mods
claude plugin install context-weather@chronosaur-mods

Language

English by default. For Polish, set the language option per mod (/config, or in settings.json):

{
  "pluginConfigs": {
    "usage-limits@chronosaur-mods": { "options": { "language": "pl" } },
    "proc-registry@chronosaur-mods": { "options": { "language": "pl" } },
    "context-weather@chronosaur-mods": { "options": { "language": "pl" } }
  }
}

License

MIT

Source 2 files
hooks/register.tsx 130 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import type { Weather } from '../types'
4
5const weather = { plugin: 'context-weather', key: 'weather' } as const
6const isHidden = { plugin: 'context-weather', key: 'isHidden' } as const
7
8const TEXT = {
9  en: {
10    context: 'Context',
11    waiting: 'Context: waiting for the first reply ',
12    hide: 'Hide',
13    warn1: (percent: number | null) => `Context ${percent}% - consider wrapping up before compaction`,
14    warn2: (percent: number | null) => `Context ${percent}% - compaction is near, save your work state`,
15  },
16  pl: {
17    context: 'Kontekst',
18    waiting: 'Kontekst: czeka na pierwszą odpowiedź ',
19    hide: 'Ukryj',
20    warn1: (percent: number | null) => `Kontekst ${percent}% - warto domknąć wątek przed kompakcją`,
21    warn2: (percent: number | null) => `Kontekst ${percent}% - kompakcja blisko, zapisz stan pracy`,
22  },
23} as const
24
25type Text = (typeof TEXT)[keyof typeof TEXT]
26
27const STEPS = [70, 85, 95]
28const BAR = 10
29
30let warned = 0
31
32const kilo = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`)
33
34const colorOf = (percent: number) =>
35  percent >= 85 ? 'red' : percent >= 70 ? 'yellow' : 'green'
36
37const bar = (percent: number) => {
38  const full = Math.min(BAR, Math.round((percent / 100) * BAR))
39
40  return '█'.repeat(full) + '░'.repeat(BAR - full)
41}
42
43async function measure($: EngineInterface): Promise<Weather> {
44  const usage = await $.session.usage()
45  const limit = (kind: string) =>
46    usage.rateLimits.find(one => one.kind === kind)?.percentUsed ?? null
47
48  return {
49    percent: usage.context.percent ?? null,
50    tokens: usage.context.tokens ?? null,
51    window: usage.context.window,
52    usd: usage.cost?.usd ?? null,
53    fiveHour: limit('five_hour'),
54    sevenDay: limit('seven_day'),
55  }
56}
57
58const stepOf = (percent: number | null) => STEPS.filter(one => (percent ?? 0) >= one).length
59
60const warning = (T: Text, percent: number | null) =>
61  stepOf(percent) === 1 ? T.warn1(percent) : T.warn2(percent)
62
63const setWeather = async ($: EngineInterface) => {
64  await $.state.set(weather, await measure($))
65}
66
67export const register: Register = (on, options) => {
68  const T = TEXT[options.language === 'pl' ? 'pl' : 'en']
69
70  on('session.start', async ($, e, next) => {
71    await setWeather($)
72
73    return next(e)
74  })
75
76  on('turn.complete', async ($, e, next) => {
77    const now = await measure($)
78    await $.state.set(weather, now)
79    if (stepOf(now.percent) > warned) $.ui.toast(warning(T, now.percent))
80    warned = stepOf(now.percent)
81
82    return next(e)
83  })
84
85  on('tool.call', async ($, e, next) => {
86    const ran = await next(e)
87    await setWeather($)
88
89    return ran
90  })
91
92  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
93    const now = ((await $.state.get(weather)).value as Weather | null | undefined) ?? null
94    const hidden = ((await $.state.get(isHidden)).value as boolean | undefined) ?? false
95    if (e.props.hasSurvey || now === null || hidden) {
96      return next(e)
97    }
98
99    const { Box, Button, Text } = $.ui.resolve(e)
100
101    if (now.percent === null) {
102      return (
103        <Box>
104          <Text dimColor>{T.waiting}</Text>
105          <Button key="hide" label={T.hide} onPress={() => $.state.set(isHidden, true)} />
106        </Box>
107      )
108    }
109
110    const tokens = now.tokens === null ? '' : ` (${kilo(now.tokens)}/${kilo(now.window)})`
111    const money = now.usd === null ? '' : ` · $${now.usd.toFixed(2)}`
112    const five = now.fiveHour === null ? '' : ` · 5h ${Math.round(now.fiveHour)}%`
113    const seven = now.sevenDay === null ? '' : ` · 7d ${Math.round(now.sevenDay)}%`
114
115    return (
116      <Box>
117        <Text color={colorOf(now.percent)}>
118          {T.context} {bar(now.percent)} {now.percent}%{tokens}
119        </Text>
120        <Text dimColor>
121          {money}
122          {five}
123          {seven}{' '}
124        </Text>
125        <Button key="hide" label={T.hide} onPress={() => $.state.set(isHidden, true)} />
126      </Box>
127    )
128  })
129}
130
types/index.d.ts 15 lines
1export type Weather = {
2  percent: number | null
3  tokens: number | null
4  window: number
5  usd: number | null
6  fiveHour: number | null
7  sevenDay: number | null
8}
9
10declare module 'claude-code' {
11  interface PluginState {
12    'context-weather': { weather: Weather | null; isHidden: boolean }
13  }
14}
15