SLOPSHOPPER

context-chart

Claude Code skill that builds, debugs, audits and packages Claude Code mods from a plain-language description

newpanecommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-chart
│ ┃ Context chart ✕ › fix the failing auth test and add an audit log call │ ┃ ◆ Context 49% · 97.4k / 200.0k │ ┃ ⏺ Read(src/auth.ts) │ ┃ 1 ⎿ Read 6 lines │ ┃ ██████████████████████░░░░░░░░░░░░░░░░░░░░ ⏺ Update(src/auth.ts) │ ┃ ░░ 49% ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ one point per turn · dashed lines at 50% ⎿ 3 pass, 1 fail │ ┃ and 90% · Esc closes │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /context-chart │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Context chart
◆ Context 49% · 97.4k / 200.0k 1 ██████████████████████░░░░░░░░░░░░░░░░░░░░░░ 49% one point per turn · dashed lines at 50% and 90% · Esc closes
README

context-chart

Leia em português

A chart of how full Claude's memory of the conversation got, turn by turn.

What is a mod?

Claude Code is Anthropic's assistant for working with code, in the terminal or in the Claude desktop app. A mod is a small add-on for it: it can add a panel, a bar above the box where you type, a new /command, or a rule that checks what Claude is about to do. This folder is one complete mod you can install and use as it is, or copy as a starting point.

What it does

Claude can only keep a limited amount of text in mind at once (the context window). The context-band mod shows the current number; this one shows the whole story as a chart, so you can see which turns made it grow fast, for example a large file being read.

What you see

  • In the desktop app: a line chart, one point per turn, with dashed lines at 50% and 90%.
  • In the terminal: the same data as rows of text bars.
  • Before Claude's first answer: a line saying the chart starts after it.

How to use it

  1. Type /context-chart and press Enter. The panel opens.
  2. Keep working; a new point appears after each of Claude's answers.
  3. Press Esc to close.

Keyboard shortcuts

KeyWhat it does
EscClose the panel

Commands

TypeWhat it does
/context-chartOpen the chart

Settings

Claude Code asks for these when you enable the mod. To change them later, open /config and find the mod by name.

SettingWhat it meansDefault
language (Language)Language of the text the mod shows: en (English) or pt-BR (Brazilian Portuguese).en

Good to know

  • The chart only gets new data when Claude Code reports it, after each answer; it does not poll in the background.

Install

You need Claude Code 2.1.287 or newer in the terminal, or 2.1.286 or newer in the desktop app.

  1. Copy this folder anywhere on your computer.
  2. Inside the copy, rename the folder _claude-plugin to .claude-plugin (with a dot at the start). It ships without the dot so the examples never load by accident.
  3. Start Claude Code with the mod:
claude --plugin-dir path/to/context-chart

To keep it installed for good, ask the claude-mod-forge skill to package it ("package this mod").

Check it

From the skill folder (skills/claude-mod-forge), this runs the manifest check, a strict type check and the 5 automated tests of this mod:

node scripts/check-mod.mjs examples/context-chart

The tests cover the mod's logic and what it draws in the terminal and the desktop app. How it looks in your own window is worth a quick look in a real session.

For developers: the examples table lists the hooks and API calls this mod demonstrates.

Source 4 files
hooks/register.tsx 75 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Point } from '../types'
5import { HISTORY, short, svgChart, textBars } from './chart'
6import { strings } from './i18n'
7
8const PANE = 'context-chart'
9const points = atom({ plugin: 'context-chart', key: 'points' } as const, [])
10
11export const register: Register = (on, options) => {
12  const t = strings(options.language)
13
14  // Pushed after each main-thread turn: no timer, no polling. Readings without tokens
15  // (before the first reply, right after a compaction) are skipped, never drawn as 0.
16  on('session.measure', async ($, e, next) => {
17    const { tokens, window, percent } = e.context
18    if (e.changed.includes('context') && tokens !== undefined && window > 0) {
19      const point: Point = { tokens, window, percent: Math.round(percent ?? (tokens / window) * 100) }
20      await update($, points, list => [...list, point].slice(-HISTORY))
21    }
22    return next(e)
23  })
24
25  on('session.start', async ($, e, next) => {
26    try {
27      await $.command.register({ name: 'context-chart', description: t.commandDescription, immediate: true })
28    } catch {
29      // name taken
30    }
31    return next(e)
32  })
33
34  on('command.run', { command: 'context-chart' }, async $ => {
35    const opened = await $.ui.open({ id: PANE, title: t.paneTitle, focus: true, closeOnEscape: true })
36    if (opened.isPlaced) return {}
37    const list = await read($, points)
38    return { text: list.length === 0 ? t.noReading : textBars(list, 60).join('\n') }
39  })
40
41  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
42    const ui = $.ui.resolve(e)
43    const { Box, Text } = ui
44    const list = await read($, points)
45    const now = list[list.length - 1]
46
47    let chart
48    if (now === undefined) {
49      chart = <Text dimColor>{t.noReading}</Text>
50    } else if (e.surface !== 'terminal' && 'Svg' in ui) {
51      // Decide by e.surface: at run time every surface's table lists every element, so
52      // `'Svg' in ui` alone is true on the terminal too (it only narrows the type).
53      const width = Math.max(120, e.props.bodyColumns * 8)
54      chart = <ui.Svg key="chart" source={svgChart(list, width, 120)} alt={t.alt(list.length, now.percent)} />
55    } else {
56      chart = <Text color="claude">{textBars(list, e.props.bodyColumns).join('\n')}</Text>
57    }
58
59    return (
60      <Box flexDirection="column" paddingX={1} gap={1}>
61        <Box justifyContent="space-between">
62          <Text bold color="claude">{t.heading}</Text>
63          {now !== undefined && (
64            <Text color={now.percent < 75 ? 'success' : now.percent < 90 ? 'warning' : 'error'}>
65              {`${now.percent}% · ${short(now.tokens)} / ${short(now.window)}`}
66            </Text>
67          )}
68        </Box>
69        {chart}
70        <Text dimColor>{t.footer}</Text>
71      </Box>
72    )
73  })
74}
75
hooks/chart.ts 33 lines
1// Pure drawing helpers: one data layer, two renderings. No $ here.
2import type { Point } from '../types'
3
4export const HISTORY = 40
5
6// A self-contained SVG line chart (desktop). Colours use currentColor so it follows the theme.
7export function svgChart(points: readonly Point[], width: number, height: number): string {
8  const pad = 6
9  const w = Math.max(60, width)
10  const h = Math.max(40, height)
11  const x = (i: number) => pad + (points.length <= 1 ? 0 : (i * (w - 2 * pad)) / (points.length - 1))
12  const y = (p: number) => Number((h - pad - (Math.max(0, Math.min(100, p)) / 100) * (h - 2 * pad)).toFixed(1))
13  const line = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${x(i).toFixed(1)},${y(p.percent).toFixed(1)}`).join(' ')
14  const guide = (p: number) => `<line x1="${pad}" x2="${w - pad}" y1="${y(p)}" y2="${y(p)}" stroke="currentColor" stroke-opacity="0.15" stroke-dasharray="3 3"/>`
15  const last = points[points.length - 1]
16  const dot = last === undefined ? '' : `<circle cx="${x(points.length - 1)}" cy="${y(last.percent)}" r="3" fill="currentColor"/>`
17  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">${guide(50)}${guide(90)}<path d="${line}" fill="none" stroke="currentColor" stroke-width="2"/>${dot}</svg>`
18}
19
20// The same data as text bars (terminal and anywhere Svg is missing).
21export function textBars(points: readonly Point[], columns: number): string[] {
22  const width = Math.max(8, columns - 12)
23  return points.slice(-12).map((p, i, all) => {
24    const filled = Math.round((Math.max(0, Math.min(100, p.percent)) / 100) * width)
25    const n = points.length - all.length + i + 1
26    return `${String(n).padStart(3)} ${'█'.repeat(filled)}${'░'.repeat(width - filled)} ${String(p.percent).padStart(3)}%`
27  })
28}
29
30export function short(n: number): string {
31  return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
32}
33
hooks/i18n.ts 30 lines
1// The text this mod shows, per language. Pure: no $ calls, so tests can cover it directly.
2// Anything other than a known language falls back to English.
3
4export type Language = 'en' | 'pt-BR'
5
6const en = {
7  commandDescription: 'Chart context-window use per turn',
8  paneTitle: 'Context chart',
9  noReading: 'No reading yet: the chart starts after Claude’s first reply.',
10  alt: (turns: number, percent: number) => `Context use over ${turns} turns, now ${percent}%`,
11  heading: '◆ Context',
12  footer: 'one point per turn · dashed lines at 50% and 90% · Esc closes',
13}
14
15const ptBR: typeof en = {
16  commandDescription: 'Gráfico do uso de contexto por turno',
17  paneTitle: 'Gráfico de contexto',
18  noReading: 'Nenhuma leitura ainda: o gráfico começa depois da primeira resposta do Claude.',
19  alt: (turns: number, percent: number) => `Uso de contexto em ${turns} turnos, agora ${percent}%`,
20  heading: '◆ Contexto',
21  footer: 'um ponto por turno · linhas tracejadas em 50% e 90% · Esc fecha',
22}
23
24export type Strings = typeof en
25const TABLE: Record<Language, Strings> = { en, 'pt-BR': ptBR }
26
27export function strings(language: unknown): Strings {
28  return language === 'pt-BR' ? TABLE['pt-BR'] : TABLE.en
29}
30
types/index.d.ts 8 lines
1export type Point = { percent: number; tokens: number; window: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'context-chart': { points: Point[] }
6  }
7}
8