A live forecast of the context window, drawn above the prompt.

Claude Code mods from Barebone Agents. Each mod is a plain Claude Code plugin you can read in a few minutes.
| Mod | What it does |
|---|---|
activity | A live crew panel: every agent in your session, its model, effort, tokens, estimated cost and time, with a friendly pixel crab. Terminal and desktop app. |
token-weather | A one-line forecast of your context window above the prompt. |
/plugin marketplace add gveshk/barebone-mods
/plugin install activity@barebone-mods
/plugin install token-weather@barebone-mods
/reload-plugins
Mods run with the same access Claude Code has, so read the code first. Neither mod here makes network calls, touches the filesystem or starts a process; the call manifest at the bottom lists every engine API the code uses. To pin what you reviewed, clone the repo at a commit and load it by hand with claude --plugin-dir ./activity instead of installing from the marketplace (a marketplace install tracks main).
Mods use Claude Code's function-hooks API, which is early access and may change. They were tested on Claude Code 2.1.291. The unit tests and a headless load pass, but the interactive panes have not yet been checked by eye in a live terminal or the desktop app; see docs/manual-checklist.md.
Open or close the pane with /activity. In a fullscreen terminal at 110 columns or wider it docks beside the transcript; otherwise it sits above the prompt. It also tries to open by itself once per flow (or per session when no flow is declared), when a flow with tasks is declared or a subagent starts. The engine seats a pane nobody asked for only at 144 columns or wider (110 once you have opened it yourself); below that it waits undrawn until you run /activity. In a terminal that is not fullscreen, an opened pane sits above the prompt.
What the pane lists, top to bottom:
+N older line for the rest. Failures are counted in the heading (N failed) even while the list is collapsed.flow tool that have not started yet.How a row is drawn:
Raster of half-block pixels. It needs a truecolor-capable terminal for exact colours and falls back to the nearest palette otherwise. It only appears when the pane body is at least 60 columns wide; a narrower pane shows the text lines only.Numbers:
activity/hooks/prices.ts, not a bill. Documented prices exist for Fable and Mythos 5 and 5.1, Opus 5.5, Opus 5 and 4.6 to 4.8, Sonnet 5.5, 5 and 4.6, and Haiku 4.5. Cache prices are derived from the input price (reads 0.1x, writes 1.25x) except where the table cites them: cache reads are $1/MTok for Fable 5 and Mythos 5, $0.25 for Fable 5.1 and Mythos 5.1, and $0.20 for Opus 5.5 and Sonnet 5.5. A model that matches none of these uses an Opus-tier fallback price. The session total in the header comes from Claude Code itself.The band above the prompt:
agents, phase and progress, running count, tokens and estimated cost. ×N toggles the pane.token-weather, next-steps) instead of replacing them.finished: true); the pane keeps showing the finished flow and its tasks. A later batch of agents shows the band again.✕ hides it for the current flow. The choice is remembered per flow, and it does not delete the plan: the pane still lists the planned tasks.Optional tools for skills:
mcp__activity__flow lets a skill report a plan (title, phases, tasks, current phase, done count) and mcp__activity__step lets a subagent report its own progress bar. Both exist only when the reportTools option is not false; set it to false to leave the model's tool list untouched.What it never does:
MIT. The activity plugin is derived from savvy-progress in JohnnyVizz/claude-kit (commit 272a877), MIT, Copyright (c) 2026 johnnyvizz; see NOTICE. The crab mascot and all artwork in this repository are original and are not taken from that project.
Output of claude plugin validate, listing each hook and engine call a plugin makes:
Validating hooks: activity/hooks/hooks.json
❯ ./register.tsx hooks: session.start, command.run{command=activity}, tool.call{tool=mcp__activity__flow}, tool.call{tool=mcp__activity__step}, agent.spawn, turn.step, turn.complete, ui.render{component=AbovePrompt}, ui.render{component=Pane, requestId=activity}
❯ ./register.tsx gating hook with .catch: tool.call{tool=mcp__activity__flow}
❯ ./register.tsx gating hook with .catch: tool.call{tool=mcp__activity__step}
❯ ./register.tsx gating hook without .catch: agent.spawn
❯ ./register.tsx calls: $.clock.every, $.clock.now, $.command.register, $.session.model, $.session.usage, $.state.get, $.state.set, $.tool.register, $.ui.close (via togglePane), $.ui.open (via autoOpen, togglePane), $.ui.panes (via togglePane), $.ui.resolve
❯ ./register.tsx state writes: activity.crew, activity.flow, activity.now, activity.panel
❯ ./register.tsx state reads: activity.crew, activity.flow, activity.now, activity.panel
✔ Validation passed
Validating hooks: token-weather/hooks/hooks.json
❯ ./register.tsx hooks: session.start, turn.complete, ui.render{component=AbovePrompt}
❯ ./register.tsx calls: $.session.usage (via takeReading), $.state.get, $.state.set, $.ui.resolve
❯ ./register.tsx state writes: token-weather.readings
❯ ./register.tsx state reads: token-weather.readings
✔ Validation passedhooks/register.tsx 112 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Reading } from '../types'
5
6const HISTORY = 12
7const BARS = '▁▂▃▄▅▆▇█'
8
9const FORECAST = [
10 { upTo: 25, icon: '☀', word: 'Clear', color: 'yellow' },
11 { upTo: 50, icon: '☁', word: 'Cloudy', color: 'cyan' },
12 { upTo: 75, icon: '☂', word: 'Showers', color: 'blue' },
13 { upTo: 90, icon: '☇', word: 'Storm', color: 'magenta' },
14 { upTo: Infinity, icon: '↯', word: 'Compact soon', color: 'red' }
15] as const
16
17// Held by the host, so the history survives a hot reload of this file.
18const readings = atom({ plugin: 'token-weather', key: 'readings' } as const, [])
19
20export const register: Register = on => {
21 on('session.start', async ($, e, next) => {
22 const result = await next(e)
23 await takeReading($)
24
25 return result
26 })
27
28 on('turn.complete', async ($, e, next) => {
29 const result = await next(e)
30
31 if (!e.agentId) {
32 await takeReading($) // main-loop turns only, not subagents
33 }
34
35 return result
36 })
37
38 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
39 const below = await next(e)
40 const history = await read($, readings)
41
42 if (e.props.hasSurvey || history.length === 0) {
43 return below
44 }
45
46 const { Box, Text } = $.ui.resolve(e)
47 const now = history[history.length - 1]!
48 const percent = Number.isFinite(now.percent) ? now.percent : 0
49 const forecast = FORECAST.find(f => percent < f.upTo) ?? FORECAST[FORECAST.length - 1]!
50 const isWide = e.props.bodyColumns >= 60
51
52 return (
53 <Box flexDirection="column">
54 {below}
55 <Box paddingX={1}>
56 <Text color={forecast.color} bold>
57 {forecast.icon} {forecast.word}
58 </Text>
59 <Text>{` ${percent}% of context`}</Text>
60 <Text dimColor>{` ${short(now.tokens)} / ${short(now.window)}`}</Text>
61 {isWide && <Text dimColor>{' last turns '}</Text>}
62 {isWide && <Text color={forecast.color}>{sparkline(history)}</Text>}
63 {isWide && history.length > 1 && <Text dimColor>{trend(history)}</Text>}
64 </Box>
65 </Box>
66 )
67 })
68}
69
70async function takeReading($: EngineInterface) {
71 const { context } = await $.session.usage()
72
73 if (!context?.window) {
74 return
75 }
76
77 const tokens = context.tokens ?? 0
78 const percent = context.percent ?? Math.round((tokens / context.window) * 100)
79 const reading: Reading = { tokens, window: context.window, percent }
80
81 await update($, readings, history => [...history, reading].slice(-HISTORY))
82}
83
84function sparkline(history: Reading[]) {
85 const top = Math.max(...history.map(r => r.tokens), 1)
86
87 return history.map(r => BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join('')
88}
89
90function trend(history: Reading[]) {
91 const delta = history[history.length - 1]!.tokens - history[history.length - 2]!.tokens
92
93 if (delta === 0) {
94 return ' steady'
95 }
96
97 return delta > 0 ? ` ▲ +${short(delta)} last turn` : ` ▼ ${short(-delta)} last turn`
98}
99
100function short(n: number) {
101 // 999_950 rounds to 1.0M, so promote before toFixed can print "1000k".
102 if (n >= 999_950) {
103 return `${+(n / 1_000_000).toFixed(1)}M`
104 }
105
106 if (n >= 1_000) {
107 return `${+(n / 1_000).toFixed(1)}k`
108 }
109
110 return String(n)
111}
112types/index.d.ts 8 lines1export type Reading = { tokens: number; window: number; percent: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'token-weather': { readings: Reading[] }
6 }
7}
8