SLOPSHOPPER

pint

Your context window as a pint of stout above the prompt. Fills as you go. Last orders at 85%.

newbandcommandtoast
v0.1.0MITupdated 2026-10-15Rudrajmehta/pint
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · pint
› 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 › /pint ⎿ pint: pint hidden (context at 49%). /pint to bring it back ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

pint

Your context window as a pint of stout, drawn above the Claude Code prompt.

pint ▕████████████░░░░░░░░░░░░▏ 51%  settling nicely

The glass fills with the context window. The head is what is left. At 85% the barman calls last orders (a toast, and the line turns to the warning colour), because that is about where autocompact tends to fire. At 95% it is time, please. After a compaction or /clear the glass is poured again and the band says which round you are on.

/pint hides or shows it.

Install

/plugin install pint --marketplace Rudrajmehta/pint

Answer y to add the marketplace, pick the user scope. Needs Claude Code 2.1.287 or newer (mods).

Run from this folder while developing

git clone https://github.com/Rudrajmehta/pint && claude --plugin-dir ./pint

The folder is watched; saving hooks/register.tsx reloads it.

Files

  • .claude-plugin/plugin.json, .claude-plugin/marketplace.json: manifest and the one-entry marketplace that makes the repo installable.
  • hooks/hooks.json, hooks/register.tsx: the mod. It reads $.session.usage().context.percent after every turn and draws a band with ui.render on AbovePrompt.
  • types/index.d.ts: the $.state contract (percent, isHidden, pours).

claude plugin validate . passes.

Source 2 files
hooks/register.tsx 88 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4// Your context window, drawn as a pint of stout above the prompt.
5// The glass fills with the context window. The head is what is left.
6// At 85% the barman calls last orders (that is also when autocompact tends to fire).
7
8const percent = atom({ plugin: 'pint', key: 'percent' } as const, null)
9const isHidden = atom({ plugin: 'pint', key: 'isHidden' } as const, false)
10const pours = atom({ plugin: 'pint', key: 'pours' } as const, 0)
11
12const GLASS = 24 // characters wide
13const LAST_ORDERS = 85
14const TIME_PLEASE = 95
15
16function glassOf(p: number): { stout: string; head: string } {
17  const full = Math.max(0, Math.min(GLASS, Math.round((p / 100) * GLASS)))
18  return { stout: '█'.repeat(full), head: '░'.repeat(GLASS - full) }
19}
20
21function lineOf(p: number): string {
22  if (p >= TIME_PLEASE) return 'time, please. /compact or /clear before the glass overflows'
23  if (p >= LAST_ORDERS) return 'last orders. autocompact is near, save what matters'
24  if (p >= 60) return 'settling nicely'
25  if (p >= 30) return 'a good start'
26  return 'fresh pour'
27}
28
29async function refresh($: EngineInterface): Promise<void> {
30  try {
31    const u = await $.session.usage()
32    const p = u.context.percent ?? null
33    await update($, percent, () => p)
34  } catch {
35    // usage is absent until the first response of a window; leave the last value
36  }
37}
38
39export const register: Register = on => {
40  on('session.start', async ($, e, next) => {
41    await $.command.register({ name: 'pint', description: 'Show or hide the pint (context window) above the prompt' })
42    await refresh($)
43    return next(e)
44  })
45
46  on('turn.complete', async ($, e, next) => {
47    const before = await read($, percent)
48    await refresh($)
49    const after = await read($, percent)
50    if (before !== null && after !== null && before < LAST_ORDERS && after >= LAST_ORDERS) {
51      $.ui.toast('pint: last orders. context at ' + after + '%')
52    }
53    if (before !== null && after !== null && after < before - 30) {
54      // a compaction or /clear poured a fresh one
55      await update($, pours, n => n + 1)
56    }
57    return next(e)
58  })
59
60  on('command.run', { command: 'pint' }, async ($, e) => {
61    const hidden = await read($, isHidden)
62    await update($, isHidden, () => !hidden)
63    const p = await read($, percent)
64    return { text: hidden ? 'pint is back on the bar' : `pint hidden (context at ${p ?? '?'}%). /pint to bring it back` }
65  })
66
67  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
68    const p = await read($, percent)
69    const hidden = await read($, isHidden)
70    if (hidden || p === null || e.props.hasSurvey) return next(e)
71    const { Box, Text } = $.ui.resolve(e)
72    const g = glassOf(p)
73    const n = await read($, pours)
74    const warn = p >= LAST_ORDERS
75    return (
76      <Box>
77        <Text dimColor>pint </Text>
78        <Text color={warn ? 'warning' : 'text'}>▕</Text>
79        <Text color="#3b2a20">{g.stout}</Text>
80        <Text color="#f3e9d2">{g.head}</Text>
81        <Text color={warn ? 'warning' : 'text'}>▏ </Text>
82        <Text bold color={warn ? 'warning' : 'text'}>{p}%</Text>
83        <Text dimColor>  {lineOf(p)}{n > 0 ? `  · round ${n + 1}` : ''}</Text>
84      </Box>
85    )
86  })
87}
88
types/index.d.ts 8 lines
1declare module 'claude-code' {
2  interface PluginState {
3    pint: { percent: number | null; isHidden: boolean; pours: number }
4  }
5}
6
7export {}
8