SLOPSHOPPER

usage-weather

A forecast band above the prompt: context fill, sparkline, rate limits, model and effort.

newbandcommand
v0.1.0no licenseupdated 2026-10-13fhva29/claude-code-mods/usage-weather
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-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 › /weather ⎿ usage-weather: Usage weather hidden. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-code-mods

Personal Claude Code mods, published as one local-style marketplace named heitor-mods.

Install

Requires a Claude Code build with mod (function hooks) support.

1. Add the marketplace. The repository is private, so gh auth login (or any git credential that can read it) must work first.

claude plugin marketplace add fhva29/claude-code-mods

Or clone it and add the folder; Claude Code then reads the mods straight from it, so an edit plus /reload-plugins is enough, with no reinstall:

git clone https://github.com/fhva29/claude-code-mods ~/.claude/mods
claude plugin marketplace add ~/.claude/mods

2. Install the mods you want (user scope):

claude plugin install next-lite@heitor-mods
claude plugin install tidy-tools@heitor-mods
# ...or all of them:
for m in blast-radius commit-gate model-stack next-lite pixel-quest prompt-git \
         replay-theater tidy-tools usage-weather brief-mode; do
  claude plugin install "$m@heitor-mods"
done

3. Reload inside Claude Code with /reload-plugins.

Options (userConfig) are set in /plugin → pick the mod → configure, then /reload-plugins. They are stored in ~/.claude/settings.json under pluginConfigs.

Mods

ModWhat it doesCommand
next-liteSuggests up to three next prompts above the input (press 1/2/3 to fill the prompt box, 0 to dismiss; the top one is also Tab ghost text). Sends a ~1–2k token digest of the turn to a small model instead of forking the whole session, so its cost does not grow with the conversation. Replacement for next-steps. Options: mode (auto/manual), model, minAnswerChars, suggestSkills./next
model-stackRoutes the model and effort of the main chat turn and of each subagent with TypeSafe's Jev, and shows the roster, decisions and agent graph in a pane. Needs TYPESAFE_API_KEY in the environment./stack
tidy-toolsDraws each tool call as one compact line, or folds a whole turn into one "N steps this turn" line./lines
blast-radiusHolds risky shell commands (rm -rf, git reset --hard, git clean, force push, migrations) and shows what they would change, with Proceed and Cancel buttons.
commit-gateHolds git commit in a pane listing the staged files, flags files not edited this session and blocks AI attribution trailers.
replay-theaterSteps through the file edits Claude made in the last turn, one diff at a time./replay
usage-weatherA forecast band above the prompt: context fill, sparkline, rate limits, model and effort.
prompt-gitAppends the git branch and working-tree counts to the hint line under the prompt.
pixel-questReplaces the spinner with a pixel RPG scene for each tool, with XP, levels and achievements. No model calls./toons
brief-modeAppends a short-answer style section to the system prompt: answer or next action first, numbered steps, lists capped at five, no preamble, recap or closers. Edit hooks/rules.ts to tune it.
midnight-neonDraws assistant replies with a dark neon palette: filled h1 banner, colored h2–h6, colored bullets, quote bars, highlighted bold, italic, inline code and links. Fences and tables keep the engine's renderer. Not installed by default; /neon flips it per session, `claude plugin enable\disable` flips it for good./neon

_archive holds mods that are kept but not in use (branch-hud); they are not listed in the marketplace.

Development

Each mod is a folder with .claude-plugin/plugin.json, hooks/hooks.json and a hooks module (register.ts or register.tsx). Check one with:

claude plugin validate <mod>
claude plugin test <mod>

claude plugin test does not type-check; run tsc against the SDK declarations Claude Code writes to <mod>/.claude-plugin/types/ as well.

next-lite builds on next-steps (MIT, Thariq Shihipar): it reuses the text sanitizer and the reply parsing, and replaces the session fork with a small digest.

Source 3 files
hooks/register.tsx 142 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { UsageWeatherReading, UsageWeatherSnapshot } from '../types'
5import {
6  compactTokens,
7  forecastFor,
8  limitColor,
9  limitLabel,
10  meter,
11  shortModel,
12  signedTokens,
13  sparkline,
14} from './forecast'
15
16const HISTORY = 24
17
18const readings = atom({ plugin: 'usage-weather', key: 'readings' } as const, [])
19const snapshot = atom({ plugin: 'usage-weather', key: 'snapshot' } as const, null)
20const isHidden = atom({ plugin: 'usage-weather', key: 'isHidden' } as const, false)
21
22async function takeReading($: EngineInterface, effort?: string | null): Promise<void> {
23  const usage = await $.session.usage()
24  const model = await $.session.model()
25  const { context } = usage
26
27  if (context.window > 0) {
28    const tokens = context.tokens ?? 0
29    const percent = context.percent ?? Math.round((tokens / context.window) * 100)
30    const reading: UsageWeatherReading = { tokens, window: context.window, percent }
31    await update($, readings, list => {
32      const last = list.at(-1)
33      const isSame = last !== undefined && last.tokens === reading.tokens
34      return isSame ? list : [...list, reading].slice(-HISTORY)
35    })
36  }
37
38  await update($, snapshot, previous => {
39    const next: UsageWeatherSnapshot = {
40      limits: usage.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed })),
41      model,
42      effort: effort === undefined ? (previous?.effort ?? null) : effort,
43    }
44    return next
45  })
46}
47
48export const register: Register = on => {
49  on('session.start', async ($, e, next) => {
50    const result = await next(e)
51    await $.command.register({
52      name: 'weather',
53      description: 'Show or hide the usage forecast band',
54    })
55    await takeReading($)
56    return result
57  })
58
59  on('command.run', { command: 'weather' }, async $ => {
60    const hidden = await update($, isHidden, value => !value)
61    return { text: hidden ? 'Usage weather hidden.' : 'Usage weather shown.' }
62  })
63
64  on('turn.step', async function* ($, e, next) {
65    if (e.agentId === undefined) {
66      const effort = e.effort === undefined ? null : String(e.effort)
67      await update($, snapshot, previous =>
68        previous === null ? previous : { ...previous, model: e.model, effort },
69      )
70    }
71    return yield* next(e)
72  })
73
74  on('turn.complete', async ($, e, next) => {
75    const result = await next(e)
76    if (e.agentId === undefined) {
77      await takeReading($)
78    }
79    return result
80  })
81
82  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
83    const below = await next(e)
84    const history = await read($, readings)
85    const snap = await read($, snapshot)
86    const latest = history.at(-1)
87    if (e.props.hasSurvey || latest === undefined || (await read($, isHidden))) {
88      return below
89    }
90
91    const { Box, Text } = $.ui.resolve(e)
92    const columns = e.props.bodyColumns
93    const isWide = columns >= 100
94    const isMedium = columns >= 70
95    const forecast = forecastFor(latest.percent)
96    const previous = history.at(-2)
97    const delta = previous === undefined ? null : latest.tokens - previous.tokens
98    const sep = <Text dimColor> │ </Text>
99
100    return (
101      <Box flexDirection="column">
102        {below}
103        <Box flexDirection="row" paddingX={1}>
104        <Text color={forecast.color} bold>
105          {forecast.glyph} {forecast.label}
106        </Text>
107        <Text> </Text>
108        <Text color={forecast.color}>{meter(latest.percent, isMedium ? 10 : 6)}</Text>
109        <Text bold> {latest.percent}%</Text>
110        {isMedium && (
111          <Text dimColor>
112            {' '}
113            {compactTokens(latest.tokens)}/{compactTokens(latest.window)}
114          </Text>
115        )}
116        {isWide && history.length > 1 && (
117          <Text color="claude"> {sparkline(history, 12)}</Text>
118        )}
119        {isWide && delta !== null && (
120          <Text color={delta > 20_000 ? 'warning' : 'subtle'}> {signedTokens(delta)}</Text>
121        )}
122        {snap !== null &&
123          snap.limits.length > 0 && [
124            sep,
125            ...snap.limits.map((limit, index) => (
126              <Text color={limitColor(limit.percentUsed)}>
127                {index > 0 ? ' · ' : ''}
128                {limitLabel(limit)} {Math.round(limit.percentUsed)}%
129              </Text>
130            )),
131          ]}
132        {snap !== null && [
133          sep,
134          <Text color="claude">{shortModel(snap.model)}</Text>,
135          snap.effort !== null && <Text dimColor> · {snap.effort}</Text>,
136        ]}
137        </Box>
138      </Box>
139    )
140  })
141}
142
hooks/forecast.ts 56 lines
1import type { UsageWeatherLimit, UsageWeatherReading } from '../types'
2
3export type Forecast = { glyph: string; label: string; color: string }
4
5const BARS = '▁▂▃▄▅▆▇█'
6
7export function forecastFor(percent: number): Forecast {
8  if (percent < 25) return { glyph: '☀', label: 'Clear', color: 'success' }
9  if (percent < 50) return { glyph: '☁', label: 'Cloudy', color: 'suggestion' }
10  if (percent < 75) return { glyph: '☂', label: 'Showers', color: 'warning' }
11  if (percent < 90) return { glyph: '☇', label: 'Storm', color: 'error' }
12  return { glyph: '↯', label: 'Compact soon', color: 'error' }
13}
14
15export function sparkline(readings: readonly UsageWeatherReading[], width: number): string {
16  return readings
17    .slice(-width)
18    .map(r => BARS[Math.min(BARS.length - 1, Math.floor((r.percent / 100) * BARS.length))])
19    .join('')
20}
21
22export function meter(percent: number, width: number): string {
23  const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * width)
24  return '▰'.repeat(filled) + '▱'.repeat(width - filled)
25}
26
27export function limitColor(percent: number): string {
28  if (percent >= 90) return 'error'
29  if (percent >= 70) return 'warning'
30  return 'success'
31}
32
33export function limitLabel(limit: UsageWeatherLimit): string {
34  if (limit.kind === 'five_hour') return '5h'
35  if (limit.kind === 'seven_day') return '7d'
36  return limit.kind.replace(/_/g, ' ')
37}
38
39export function compactTokens(tokens: number): string {
40  const trim = (value: string): string => value.replace(/\.0$/, '')
41  if (tokens >= 1_000_000) return `${trim((tokens / 1_000_000).toFixed(1))}M`
42  if (tokens >= 1_000) return `${trim((tokens / 1_000).toFixed(tokens >= 100_000 ? 0 : 1))}k`
43  return String(tokens)
44}
45
46export function signedTokens(delta: number): string {
47  return `${delta >= 0 ? '+' : '−'}${compactTokens(Math.abs(delta))}`
48}
49
50export function shortModel(model: string): string {
51  const match = /claude-(\w+)-(\d+)-(\d+)/.exec(model)
52  if (match === null) return model
53  const [, family = '', major, minor] = match
54  return `${family.charAt(0).toUpperCase()}${family.slice(1)} ${major}.${minor}`
55}
56
types/index.d.ts 20 lines
1export type UsageWeatherReading = { tokens: number; window: number; percent: number }
2
3export type UsageWeatherLimit = { kind: string; percentUsed: number }
4
5export type UsageWeatherSnapshot = {
6  limits: UsageWeatherLimit[]
7  model: string
8  effort: string | null
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    'usage-weather': {
14      readings: UsageWeatherReading[]
15      snapshot: UsageWeatherSnapshot | null
16      isHidden: boolean
17    }
18  }
19}
20