SLOPSHOPPER

mood-band

A one-line mascot band above the prompt whose mood reflects the last turn (off by default: /mood on|off)

newbandguardcommandprompt
v0.1.0MITupdated 2026-10-05vincentlauriat/ModsTools/mods/mood-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mood-band
› 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 › /mood ⎿ mood-band: mood-band is off. Use /mood on or /mood off. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

mood-band

An optional one-line mascot above the prompt whose mood follows how the last turn went.

What it does

  • Off by default. When on, shows a dim band such as (^‿^) Tout roule after the first main-agent turn.
  • Mood, by priority:
FaceCaptionWhen
(-_-) zzzJe dors30 minutes or more since the last turn
(╥﹏╥)Ça a coincéTurn errored or was aborted, or more than half of its tool calls failed or were denied
(•_•;)AïeAt least one tool call failed or was denied
(•̀ᴗ•́)Ça chauffeTurn lasted 2 minutes or more
(^‿^)Tout rouleOtherwise

Commands

CommandEffect
/moodReports whether the band is on
/mood on / offShows / hides the band (kept across sessions)

Install

claude --plugin-dir /path/to/ModsTools/mods/mood-band

Limits

  • Captions are in French and not configurable.
  • Only main-agent tool calls are counted; subagents are ignored.

Develop

claude plugin validate mods/mood-band
claude plugin test mods/mood-band   # 12 tests
Source 3 files
hooks/register.tsx 74 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { LastTurn } from '../types'
5import { pickMood } from './mood'
6
7const last = atom({ plugin: 'mood-band', key: 'last' } as const, null)
8
9async function isEnabled($: EngineInterface): Promise<boolean> {
10  return (await $.store.get('enabled')) === true
11}
12
13export const register: Register = on => {
14  let startedAt: number | null = null
15  let total = 0
16  let failed = 0
17
18  on('session.start', async ($, e, next) => {
19    await $.command.register({ name: 'mood', description: 'mood-band: "on" or "off" shows or hides the mascot band' })
20
21    return next(e)
22  })
23
24  on('prompt.submit', async ($, e, next) => {
25    startedAt = await $.clock.now()
26    total = 0
27    failed = 0
28
29    return next(e)
30  })
31
32  on('tool.call', async ($, e, next) => {
33    const result = await next(e)
34    if (e.agentId === undefined) {
35      total += 1
36      if (result.deny !== undefined || result.isError) failed += 1
37    }
38
39    return result
40  })
41
42  on('turn.complete', async ($, e, next) => {
43    if (e.agentId !== undefined) return next(e)
44    const now = await $.clock.now()
45    const ms = e.durationMs ?? (startedAt === null ? 0 : now - startedAt)
46    const turn: LastTurn = { failed, total, ms, errored: e.isAborted || e.reason === 'error', at: now }
47    await update($, last, () => turn)
48
49    return next(e)
50  })
51
52  on('command.run', { command: 'mood' }, async ($, e) => {
53    const arg = e.args.trim()
54    if (arg !== 'on' && arg !== 'off') return { text: `mood-band is ${(await isEnabled($)) ? 'on' : 'off'}. Use /mood on or /mood off.` }
55    await $.store.set('enabled', arg === 'on')
56    $.ui.invalidate('ui.render')
57
58    return { text: `mood-band ${arg}.` }
59  })
60
61  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
62    const turn = await read($, last)
63    if (e.props.hasSurvey || turn === null || !(await isEnabled($))) return next(e)
64    const mood = pickMood({ ...turn, idleMs: (await $.clock.now()) - turn.at })
65    const { Box, Text } = $.ui.resolve(e)
66
67    return (
68      <Box>
69        <Text dimColor>{`${mood.face} ${mood.caption}`}</Text>
70      </Box>
71    )
72  })
73}
74
hooks/mood.ts 23 lines
1export type MoodInput = { failed: number; total: number; ms: number; errored: boolean; idleMs: number }
2export type Mood = { kind: 'happy' | 'focused' | 'worried' | 'sad' | 'sleepy'; face: string; caption: string }
3
4export const LONG_TURN_MS = 120_000
5export const SLEEP_MS = 30 * 60_000
6
7const MOODS = {
8  happy: { kind: 'happy', face: '(^‿^)', caption: 'Tout roule' },
9  focused: { kind: 'focused', face: '(•̀ᴗ•́)', caption: 'Ça chauffe' },
10  worried: { kind: 'worried', face: '(•_•;)', caption: 'Aïe' },
11  sad: { kind: 'sad', face: '(╥﹏╥)', caption: 'Ça a coincé' },
12  sleepy: { kind: 'sleepy', face: '(-_-) zzz', caption: 'Je dors' },
13} as const satisfies Record<string, Mood>
14
15export function pickMood({ failed, total, ms, errored, idleMs }: MoodInput): Mood {
16  if (idleMs >= SLEEP_MS) return MOODS.sleepy
17  if (errored || (total > 0 && failed * 2 > total)) return MOODS.sad
18  if (failed > 0) return MOODS.worried
19  if (ms >= LONG_TURN_MS) return MOODS.focused
20
21  return MOODS.happy
22}
23
types/index.d.ts 8 lines
1export type LastTurn = { failed: number; total: number; ms: number; errored: boolean; at: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'mood-band': { last: LastTurn | null }
6  }
7}
8