A band above the prompt: context usage as a weather forecast, a bar chart of tokens per turn, and session totals.

Mods for Claude Code, distributed as a plugin marketplace.
| Mod | What it does |
|---|---|
| context-weather | A band above the prompt showing context usage as a weather forecast, a bar chart of tokens per turn and session totals. |
Add the marketplace once, then install the mods you want. At the Claude Code prompt:
/plugin marketplace add marco-quicula/claude-mods
/plugin install context-weather@marco-quicula
To update later:
/plugin marketplace update marco-quicula
/plugin update context-weather@marco-quicula
To remove a mod: /plugin uninstall context-weather@marco-quicula.
New mods show up in the catalog once it is updated (/plugin marketplace update marco-quicula). They are never installed on their own: install each one you want with /plugin install <mod>@marco-quicula. See the releases for what is new.
A band above the prompt that tells you how full the context window is, the way a forecast would:
⛅ Cloudy ████████░░░░░░░░░░░░ 41% · 82.4k / 200.0k tokens in context [Hide]
Tokens per turn ▂▃▁▅█▄▂ · last: 3.1k (+78.2k read from cache) · session: 41.7k
| Context used | Weather |
|---|---|
| up to 25% | ☀️ Clear skies |
| up to 50% | ⛅ Cloudy |
| up to 75% | 🌧️ Rainy |
| up to 90% | 🌩️ Thunder |
| above 90% | ⛈️ Storm |
Subagent turns are not counted. The band appears once the session reports context usage, usually after the first turn.
| Command | What it does |
|---|---|
/context-weather | Shows or hides the band. The Hide button on the band hides it too. |
/clima-contexto | The same command, an alias for Portuguese speakers. |
The band is available in English (en) and Brazilian Portuguese (pt-BR). The mod has a Language option:
auto (default): detects the language from the Claude Code language setting, then from the LANG environment variable. When neither names a supported language, the band is in English.en or pt-BR: always that language.You choose it when you install the mod, and you can change it any time in /config. The band switches language as soon as you change it.
Each mod lives in mods/<mod>/ as a plugin of function hooks: a .claude-plugin/plugin.json manifest, a hooks module under hooks/, its state contract under types/ and tests under tests/.
claude plugin validate . for the marketplace, claude plugin validate mods/<mod> and claude plugin test mods/<mod> for a mod.claude plugin marketplace add <folder> and install from it. Claude Code reads the mod straight from the folder; after editing, run /reload-plugins in the session. claude --plugin-dir mods/<mod> also loads a single mod for one session.version in the mod's plugin.json (see Versioning).Each mod is versioned on its own, following semantic versioning:
0.1.0.<mod>-v<version>, for example context-weather-v0.1.0.hooks/register.tsx 149 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionUsage } from 'claude-code'
3
4import type { Context, Stats } from '../types'
5import { MESSAGES, resolveLocale } from './i18n'
6import type { Locale, WeatherKey } from './i18n'
7
8const MAX_BARS = 24
9const BAR_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']
10const GAUGE_WIDTH = 20
11
12const EMPTY: Stats = { history: [], last: 0, total: 0, lastCacheRead: 0, context: null }
13
14const stats = atom({ plugin: 'context-weather', key: 'stats' } as const, EMPTY)
15const isHidden = atom({ plugin: 'context-weather', key: 'isHidden' } as const, false)
16const locale = atom({ plugin: 'context-weather', key: 'locale' } as const, 'en' as Locale)
17
18type Weather = { icon: string; key: WeatherKey; color: string }
19
20/**
21 * The weather for the context usage: up to 25% clear skies, up to 50% cloudy,
22 * up to 75% rainy, up to 90% thunder (between rain and storm), above 90% storm.
23 */
24export function weatherFor(percent: number): Weather {
25 if (percent <= 25) return { icon: '☀️', key: 'clear', color: '#F5B700' }
26 if (percent <= 50) return { icon: '⛅', key: 'cloudy', color: '#8FA3B8' }
27 if (percent <= 75) return { icon: '🌧️', key: 'rainy', color: '#3B82F6' }
28 if (percent <= 90) return { icon: '🌩️', key: 'thunder', color: '#F59E0B' }
29 return { icon: '⛈️', key: 'storm', color: '#EF4444' }
30}
31
32/** 1234 -> "1.2k"; 1234567 -> "1.23M"; below a thousand, the whole number. */
33export function formatTokens(n: number, decimal = '.'): string {
34 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2).replace('.', decimal)}M`
35 if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace('.', decimal)}k`
36 return String(Math.round(n))
37}
38
39/** One vertical bar per turn, scaled to the largest in the series. */
40export function sparkline(values: readonly number[]): string {
41 const max = Math.max(0, ...values)
42 if (max === 0) return values.map(() => BAR_CHARS[0]).join('')
43 return values
44 .map(v => BAR_CHARS[Math.min(BAR_CHARS.length - 1, Math.round((v / max) * (BAR_CHARS.length - 1)))])
45 .join('')
46}
47
48/** The context gauge, filled up to the percentage. */
49export function gauge(percent: number): { filled: string; empty: string } {
50 const cells = Math.max(0, Math.min(GAUGE_WIDTH, Math.round((percent / 100) * GAUGE_WIDTH)))
51 return { filled: '█'.repeat(cells), empty: '░'.repeat(GAUGE_WIDTH - cells) }
52}
53
54function contextFrom(usage: SessionUsage): Context | null {
55 const { tokens, window, percent } = usage.context
56 if (tokens === undefined || percent === undefined) return null
57 return { tokens, window, percent }
58}
59
60/** Shows or hides the band; the reply names the command as it was typed. */
61async function toggle($: EngineInterface, command: string) {
62 const hidden = await update($, isHidden, h => !h)
63 const m = MESSAGES[await read($, locale)]
64
65 return { text: hidden ? m.hidden(command) : m.shown }
66}
67
68export const register: Register = (on, options) => {
69 on('session.start', async ($, e, next) => {
70 // Detection is best effort: a source that cannot be read counts as unset.
71 const settings = await $.settings.read().catch(() => ({}) as Record<string, unknown>)
72 const lang = await $.env.get('LANG').catch(() => undefined)
73 const resolved = resolveLocale(options.language, settings.language, lang)
74 await update($, locale, () => resolved)
75
76 await $.command.register({ name: 'context-weather', description: MESSAGES[resolved].commandDescription })
77 // Alias for Portuguese speakers, always described in Portuguese.
78 await $.command.register({ name: 'clima-contexto', description: MESSAGES['pt-BR'].commandDescription })
79
80 const context = contextFrom(await $.session.usage())
81 if (context) await update($, stats, s => ({ ...s, context }))
82
83 return next(e)
84 })
85
86 on('command.run', { command: 'context-weather' }, $ => toggle($, 'context-weather'))
87 on('command.run', { command: 'clima-contexto' }, $ => toggle($, 'clima-contexto'))
88
89 on('turn.complete', async ($, e, next) => {
90 const result = await next(e)
91 if (e.agentId !== undefined) return result
92
93 const usage = e.usage
94 const spent = usage ? usage.input_tokens + usage.cache_creation_input_tokens + usage.output_tokens : 0
95 const cacheRead = usage ? usage.cache_read_input_tokens : 0
96 const context = contextFrom(await $.session.usage())
97
98 await update($, stats, s => ({
99 history: [...s.history, spent].slice(-MAX_BARS),
100 last: spent,
101 total: s.total + spent,
102 lastCacheRead: cacheRead,
103 context: context ?? s.context,
104 }))
105
106 return result
107 })
108
109 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
110 if (e.props.hasSurvey || (await read($, isHidden))) return next(e)
111
112 const s = await read($, stats)
113 if (s.context === null) return next(e)
114
115 const m = MESSAGES[await read($, locale)]
116 const fmt = (n: number) => formatTokens(n, m.decimal)
117 const { Box, Button, Text } = $.ui.resolve(e)
118 const weather = weatherFor(s.context.percent)
119 const bar = gauge(s.context.percent)
120 const bars = s.history.length > 0 ? sparkline(s.history) : '—'
121
122 return (
123 <Box flexDirection="column">
124 <Box>
125 <Text color={weather.color} bold={s.context.percent > 90}>
126 {weather.icon} {m.weather[weather.key]}{' '}
127 </Text>
128 <Text color={weather.color}>{bar.filled}</Text>
129 <Text dimColor>{bar.empty}</Text>
130 <Text>
131 {' '}
132 {s.context.percent}% · {fmt(s.context.tokens)} / {fmt(s.context.window)} {m.tokensInContext}{' '}
133 </Text>
134 <Button key="hide" label={m.hide} onPress={() => update($, isHidden, () => true)} />
135 </Box>
136 <Box>
137 <Text dimColor>{m.tokensPerTurn} </Text>
138 <Text color="#22D3EE">{bars}</Text>
139 <Text dimColor>
140 {' '}· {m.last}: {fmt(s.last)}
141 {s.lastCacheRead > 0 ? ` (${m.cacheRead(fmt(s.lastCacheRead))})` : ''} · {m.session}:{' '}
142 {fmt(s.total)}
143 </Text>
144 </Box>
145 </Box>
146 )
147 })
148}
149hooks/i18n.ts 80 lines1export const LOCALES = ['en', 'pt-BR'] as const
2export type Locale = (typeof LOCALES)[number]
3
4export type WeatherKey = 'clear' | 'cloudy' | 'rainy' | 'thunder' | 'storm'
5
6export type Messages = {
7 /** Decimal separator used when abbreviating token counts. */
8 decimal: string
9 weather: Record<WeatherKey, string>
10 tokensInContext: string
11 tokensPerTurn: string
12 last: string
13 session: string
14 cacheRead: (tokens: string) => string
15 hide: string
16 commandDescription: string
17 hidden: (command: string) => string
18 shown: string
19}
20
21export const MESSAGES: Record<Locale, Messages> = {
22 en: {
23 decimal: '.',
24 weather: {
25 clear: 'Clear skies',
26 cloudy: 'Cloudy',
27 rainy: 'Rainy',
28 thunder: 'Thunder',
29 storm: 'Storm',
30 },
31 tokensInContext: 'tokens in context',
32 tokensPerTurn: 'Tokens per turn',
33 last: 'last',
34 session: 'session',
35 cacheRead: tokens => `+${tokens} read from cache`,
36 hide: 'Hide',
37 commandDescription: 'Show or hide the context weather band',
38 hidden: command => `Context weather band hidden. Run /${command} to show it again.`,
39 shown: 'Context weather band shown.',
40 },
41 'pt-BR': {
42 decimal: ',',
43 weather: {
44 clear: 'Céu aberto',
45 cloudy: 'Nublado',
46 rainy: 'Chuvoso',
47 thunder: 'Trovoadas',
48 storm: 'Tempestade',
49 },
50 tokensInContext: 'tokens no contexto',
51 tokensPerTurn: 'Tokens por interação',
52 last: 'última',
53 session: 'sessão',
54 cacheRead: tokens => `+${tokens} relidos do cache`,
55 hide: 'Ocultar',
56 commandDescription: 'Mostra ou oculta a faixa do clima do contexto',
57 hidden: command => `Faixa do clima oculta. /${command} mostra de novo.`,
58 shown: 'Faixa do clima visível.',
59 },
60}
61
62/** Maps free text ("pt_BR.UTF-8", "Portuguese", "português", "en-US") to a supported locale. */
63export function localeFromText(text: string | undefined): Locale | undefined {
64 if (!text) return undefined
65 const t = text.trim().toLowerCase()
66 if (/^pt([-_.]|$)/.test(t) || t.includes('portug')) return 'pt-BR'
67 if (/^en([-_.]|$)/.test(t) || t.includes('english') || t.includes('ingl')) return 'en'
68 return undefined
69}
70
71/**
72 * The locale the band is drawn in: the mod's `language` option when it names a
73 * locale; on `auto`, the first source that maps to one (the Claude Code
74 * `language` setting, then the `LANG` variable); English otherwise.
75 */
76export function resolveLocale(option: unknown, settingsLanguage: unknown, lang: string | undefined): Locale {
77 if (typeof option === 'string' && (LOCALES as readonly string[]).includes(option)) return option as Locale
78 return localeFromText(typeof settingsLanguage === 'string' ? settingsLanguage : undefined) ?? localeFromText(lang) ?? 'en'
79}
80types/index.d.ts 21 lines1export type Context = { tokens: number; window: number; percent: number }
2
3export type Stats = {
4 /** New tokens (input + cache written + output) of the latest turns, most recent last. */
5 history: number[]
6 /** New tokens of the last turn. */
7 last: number
8 /** Sum of the new tokens of every turn in the session. */
9 total: number
10 /** Cache reads of the last turn (context read again, not counted in last). */
11 lastCacheRead: number
12 /** The context at the end of the last turn. */
13 context: Context | null
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'context-weather': { stats: Stats; isHidden: boolean; locale: 'en' | 'pt-BR' }
19 }
20}
21