SLOPSHOPPER

puppy-bowl

the puppy's food bowl above the prompt: how much context is left

newbandrowscommand
v0.2.1MITupdated 2026-10-14nonbiri1004/baibai-mods/puppy-bowl
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · puppy-bowl
› 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 › /can the puppy wags its tail. thank you for the can ♡ ko-fi.com/lofipuppy https://ko-fi.com/lofipuppy ◖•ᴥ•◗ ●●●●●○○○○○ 51% left ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
◖•ᴥ•◗ ●●●●●○○○○○ 51% left
Command output
the puppy wags its tail. thank you for the can ♡ ko-fi.com/lofipuppy https://ko-fi.com/lofipuppy
README

puppy-bowl

the puppy's food bowl, sitting above the prompt in claude code. the food is your context window: as the conversation grows, the puppy eats it.

◖•ᴥ•◗ ●●●●○○○○○○ 42% left
  • by default the bowl only comes out once half of it is eaten (the status line already shows the number before that).
  • past 80% used, the puppy says the bowl is almost empty and shows a compact button, so you can tidy the conversation before it runs out.
  • nothing pops up, nothing talks to the network.

install

in a claude code session in your terminal:

/plugin install puppy-bowl --marketplace nonbiri1004/baibai-mods

answer y to add the marketplace, then pick a scope.

commands

commandwhat it does
/bowlsteps through the modes: auto (out when half empty) → always → off
/bowl auto, /bowl always, /bowl offpicks one; it's remembered for every new conversation
/cangive the puppy a can: a thank-you and a link to ko-fi. unlocks nothing ♡

develop

claude plugin validate puppy-bowl
claude plugin test puppy-bowl

license

MIT

Source 2 files
hooks/register.tsx 128 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Bowl, Mode } from '../types'
5
6// the puppy's orange (--phones in shared/theme.css): the one accent, on the headphones
7const PHONES = '#e07b35'
8const CELLS = 10
9// the puppy only comes out once half the bowl is eaten (the status line already has the number)
10const SHOW = 50
11// past this much context used, the puppy mentions /compact
12const HUNGRY = 80
13const KOFI = 'https://ko-fi.com/lofipuppy'
14
15const bowl = atom({ plugin: 'puppy-bowl', key: 'bowl' } as const, { used: null } as Bowl)
16const mode = atom({ plugin: 'puppy-bowl', key: 'mode' } as const, 'auto' as Mode)
17
18// /bowl steps through the modes in this order; /bowl auto|always|off picks one
19const MODES: readonly Mode[] = ['auto', 'always', 'off']
20const SAID: Record<Mode, string> = {
21  auto: 'the bowl comes out when it is half empty.',
22  always: 'the bowl stays out.',
23  off: 'the bowl is put away. /bowl brings it back.',
24}
25
26export function nextMode(was: Mode, arg: string): Mode {
27  const asked = arg.trim().toLowerCase()
28  if ((MODES as readonly string[]).includes(asked)) {
29    return asked as Mode
30  }
31
32  return MODES[(MODES.indexOf(was) + 1) % MODES.length] ?? 'auto'
33}
34
35export function isShown(m: Mode, used: number): boolean {
36  return m === 'always' || (m === 'auto' && used >= SHOW)
37}
38
39// food left in the bowl: ●●●●●●○○○○
40export function food(used: number): string {
41  const left = Math.round(((100 - used) / 100) * CELLS)
42  const full = Math.max(0, Math.min(CELLS, left))
43  return '●'.repeat(full) + '○'.repeat(CELLS - full)
44}
45
46export const register: Register = on => {
47  on('session.start', async ($, e, next) => {
48    await $.command.register({
49      name: 'can',
50      description: 'give the puppy a can (opens ko-fi)',
51    })
52    await $.command.register({
53      name: 'bowl',
54      description: 'the puppy bowl: auto (out when half empty) → always → off',
55      argumentHint: '[auto|always|off]',
56    })
57    // the mode is kept in $.store, so every new conversation remembers it
58    const kept = (await $.store.get('mode')) as Mode | undefined
59    if (kept && MODES.includes(kept)) {
60      await update($, mode, () => kept)
61    }
62    const { context } = await $.session.usage()
63    await update($, bowl, () => ({ used: context.percent ?? null }))
64
65    return next(e)
66  })
67
68  // the engine pushes new figures after each turn; the write redraws the band
69  on('session.measure', async ($, e, next) => {
70    if (e.changed.includes('context')) {
71      await update($, bowl, () => ({ used: e.context.percent ?? null }))
72    }
73
74    return next(e)
75  })
76
77  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
78    // before the first reply there's no reading yet: the bowl starts full
79    const used = (await read($, bowl)).used ?? 0
80    if (e.props.hasSurvey || !isShown(await read($, mode), used)) {
81      return next(e)
82    }
83
84    const { Box, Text, Button } = $.ui.resolve(e)
85    const isHungry = used >= HUNGRY
86
87    return (
88      <Box>
89        <Text>
90          <Text color={PHONES}>◖</Text>
91          <Text dimColor>•ᴥ•</Text>
92          <Text color={PHONES}>◗</Text>
93          <Text dimColor>
94            {' '}
95            {food(used)} {100 - used}% left
96            {isHungry ? ' · the bowl is almost empty ' : ''}
97          </Text>
98        </Text>
99        {isHungry && !e.props.isWorking && (
100          <Button key="compact" label="compact" onPress={() => $.session.compact()} />
101        )}
102      </Box>
103    )
104  })
105
106  on('command.run', { command: 'bowl' }, async ($, e) => {
107    const now = await update($, mode, was => nextMode(was, e.args))
108    await $.store.set('mode', now)
109
110    return { text: SAID[now] }
111  })
112
113  on('command.run', { command: 'can' }, () => ({
114    text: `the puppy wags its tail. thank you for the can ♡ ${KOFI}`,
115  }))
116
117  // draws /can's row with a real link instead of plain text
118  on('ui.render', { component: 'CommandOutput', props: { command: 'can' } }, ($, e) => {
119    const { Text, Link } = $.ui.resolve(e)
120
121    return (
122      <Text dimColor>
123        the puppy wags its tail. thank you for the can ♡ <Link href={KOFI} label="ko-fi.com/lofipuppy" />
124      </Text>
125    )
126  })
127}
128
types/index.d.ts 16 lines
1// what the bowl remembers for this session
2export type Bowl = {
3  // context used, 0–100; null before the first reading
4  used: number | null
5}
6
7// auto: out once half the bowl is eaten; always: out the whole time; off: put away
8export type Mode = 'auto' | 'always' | 'off'
9
10declare module 'claude-code' {
11  interface PluginState {
12    // mode: copied from $.store at session start so the drawing can read it
13    'puppy-bowl': { bowl: Bowl; mode: Mode }
14  }
15}
16