A live emoji forecast of the context window above the prompt: weather, fill percentage, and a chart of the last turns, with a one-click context handoff.

A live forecast of your context window, drawn as a band above the prompt.
☀️ Clear 18% of context 36k / 200k last turns ▁▂▂▃▃▄ ⇄ Handoff
| Fill | Forecast |
|---|---|
| < 25% | ☀️ Clear |
| < 50% | ☁️ Cloudy |
| < 75% | 🌧️ Showers |
| < 90% | ⛈️ Storm |
| ≥ 90% | 🌪️ Compact soon |
The sparkline (on wide terminals) shows the last 12 turns. Past 50% it recommends a handoff.
/handoffRun /handoff (or press the ⇄ Handoff button in the band, hotkey h) and Claude writes a self-contained handoff note. The mod then runs /clear, pastes the note into the fresh context, and carries on. You keep the thread without dragging a full context window along.
token-weather by the Claude Code DevRel team at Anthropic, from the Claude Code playground. It came up with the forecast band, the fill reading and the turn chart./handoff with the one-click ⇄ Handoff button.Apache License 2.0, the same license as the original. See NOTICE for attribution and a list of changes. (The rest of this marketplace is MIT.)
hooks/register.tsx 186 lines1// Copyright 2026 Anthropic PBC
2// Copyright 2026 Karthick Arun (modifications)
3// SPDX-License-Identifier: Apache-2.0
4//
5// Token Weather: a live forecast of the context window, above the prompt.
6// Port of anthropics/claude-code-playground mods/token-weather, with emoji icons.
7import type { EngineInterface as Engine, Register } from 'claude-code'
8
9const HISTORY = 12
10const BARS = '▁▂▃▄▅▆▇█'
11
12// Forecast bands, by percent of the window used.
13const FORECAST = [
14 { upTo: 25, icon: '☀️', word: 'Clear', color: 'warning' },
15 { upTo: 50, icon: '☁️', word: 'Cloudy', color: 'cyan' },
16 { upTo: 75, icon: '🌧️', word: 'Showers', color: 'blue' },
17 { upTo: 90, icon: '⛈️', word: 'Storm', color: 'magenta' },
18 { upTo: Infinity, icon: '🌪️', word: 'Compact soon', color: 'red' },
19] as const
20
21const RECOMMEND_AT = 50
22
23const HANDOFF_REQUEST =
24 'Write a handoff for a fresh instance of yourself that will continue this conversation with an empty context. ' +
25 'It will know nothing except what you write, so carry over every fact, name, number, preference and constraint the user stated, verbatim where it matters. ' +
26 'Include: the goal, what is done, what is in progress, key decisions and why, relevant file paths and commands, ' +
27 'open questions, and the exact next steps. Skip CLAUDE.md instructions and memories: they reload on their own. ' +
28 'Be self-contained and concise. Reply with only the handoff, no preamble.'
29
30type Reading = { tokens: number; window: number; percent: number }
31
32// Oldest first.
33let readings: Reading[] = []
34
35// idle -> writing (waiting for the handoff reply) -> idle once cleared and resumed.
36let phase: 'idle' | 'writing' = 'idle'
37let hasRecommended = false
38
39async function startHandoff($: Engine) {
40 if (phase !== 'idle') return
41 phase = 'writing'
42 $.ui.invalidate('ui.render')
43 try {
44 await $.prompt.submit({ text: HANDOFF_REQUEST, asUser: true })
45 } catch {
46 phase = 'idle'
47 $.ui.invalidate('ui.render')
48 }
49}
50
51async function finishHandoff($: Engine) {
52 const messages = await $.session.messages()
53 const handoff = Array.isArray(messages)
54 ? messages.filter(m => m.role === 'assistant' && m.text).at(-1)?.text
55 : undefined
56 if (!handoff) {
57 phase = 'idle'
58 $.ui.toast('Handoff failed: no reply to carry over')
59 $.ui.invalidate('ui.render')
60 return
61 }
62 // Leave the turn.complete hook before clearing, so /clear isn't run inside a turn it waits on.
63 $.clock.after(0, async () => {
64 try {
65 await $.command.run({ command: 'clear' })
66 await registerCommand($)
67 readings = []
68 hasRecommended = false
69 await $.prompt.submit({
70 text: `Handoff from the previous context. Continue from here:\n\n${handoff}`,
71 asUser: true,
72 })
73 } catch {
74 $.ui.toast('Handoff: /clear failed')
75 } finally {
76 phase = 'idle'
77 $.ui.invalidate('ui.render')
78 }
79 })
80}
81
82// /handoff runs the same flow as the band's button.
83async function registerCommand($: Engine) {
84 await $.command.register({ name: 'handoff', description: 'Hand off to a fresh context: write a handoff, /clear, paste it, continue' })
85}
86
87export const register: Register = on => {
88 on('session.start', async ($, e, next) => {
89 const result = await next(e)
90 readings = []
91 await registerCommand($)
92 await takeReading($)
93 return result
94 })
95
96 on('command.run', { command: 'handoff' }, $ => {
97 if (phase !== 'idle') return { text: 'A handoff is already in progress.' }
98 // Not awaited: the prompt queues behind this command and runs once the session is idle.
99 void startHandoff($)
100 return { text: 'Writing a handoff, then clearing context…' }
101 })
102
103 on('turn.complete', async ($, e, next) => {
104 const result = await next(e)
105 if (!e.agentId) {
106 if (phase === 'writing') await finishHandoff($)
107 else await takeReading($)
108 }
109 return result
110 })
111
112 on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
113 const now = readings.at(-1)
114 if (e.props.hasSurvey || !now) return next(e)
115
116 const { Box, Button, Text } = $.ui.resolve(e)
117 const f = forecastFor(now.percent)
118 const trend = trendWord()
119 const recommend = now.percent >= RECOMMEND_AT
120 const wide = (e.props.bodyColumns ?? 80) >= 60
121
122 return (
123 <Box flexDirection="row" paddingX={1}>
124 <Text color={f.color} bold>{`${f.icon} ${f.word}`}</Text>
125 <Text>{` ${now.percent}% of context`}</Text>
126 <Text dimColor>{` ${short(now.tokens)} / ${short(now.window)}`}</Text>
127 {wide && <Text dimColor>{' last turns '}</Text>}
128 {wide && <Text color={f.color}>{chart()}</Text>}
129 {wide && trend && <Text dimColor>{` ${trend}`}</Text>}
130 <Text>{' '}</Text>
131 {phase === 'writing' ? (
132 <Text dimColor>writing handoff…</Text>
133 ) : e.props.isWorking ? null : (
134 <Button key="handoff" hotkey="h" label={recommend ? '⇄ Handoff (recommended)' : '⇄ Handoff'} onPress={() => startHandoff($)} />
135 )}
136 </Box>
137 )
138 })
139}
140
141async function takeReading($: Engine) {
142 try {
143 const { context } = await $.session.usage()
144 if (!context?.window) return
145 const tokens = context.tokens ?? 0
146 const percent = Math.round(context.percent ?? (tokens / context.window) * 100)
147 // The session.start reading is 0 before any response; drop it once real readings arrive.
148 readings = readings.filter(r => r.tokens > 0)
149 readings.push({ tokens, window: context.window, percent })
150 readings = readings.slice(-HISTORY)
151 if (percent >= RECOMMEND_AT && !hasRecommended) {
152 hasRecommended = true
153 $.ui.toast(`Context is ${percent}% full: consider a handoff (press the Handoff button)`)
154 }
155 $.ui.invalidate('ui.render')
156 } catch {
157 // No reading this turn; the band keeps the last one.
158 }
159}
160
161function forecastFor(percent: number) {
162 return FORECAST.find(b => percent < b.upTo) ?? FORECAST[4]
163}
164
165// Bars scale to the busiest reading shown, so growth shows at any fill level.
166function chart() {
167 const top = Math.max(...readings.map(r => r.tokens), 1)
168 return readings
169 .map(r => BARS[Math.min(BARS.length - 1, Math.floor((r.tokens / top) * (BARS.length - 1)))])
170 .join('')
171}
172
173function trendWord() {
174 if (readings.length < 2) return ''
175 const delta = (readings.at(-1)?.tokens ?? 0) - (readings.at(-2)?.tokens ?? 0)
176 if (delta > 0) return `▲ +${short(delta)} last turn`
177 if (delta < 0) return `▼ ${short(-delta)} last turn`
178 return 'steady'
179}
180
181function short(n: number) {
182 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`
183 if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}k`
184 return String(n)
185}
186