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

An optional one-line mascot above the prompt whose mood follows how the last turn went.
(^‿^) Tout roule after the first main-agent turn.| Face | Caption | When |
|---|---|---|
(-_-) zzz | Je dors | 30 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ïe | At least one tool call failed or was denied |
(•̀ᴗ•́) | Ça chauffe | Turn lasted 2 minutes or more |
(^‿^) | Tout roule | Otherwise |
| Command | Effect |
|---|---|
/mood | Reports whether the band is on |
/mood on / off | Shows / hides the band (kept across sessions) |
claude --plugin-dir /path/to/ModsTools/mods/mood-band
claude plugin validate mods/mood-band
claude plugin test mods/mood-band # 12 testshooks/register.tsx 74 lines1import { 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}
74hooks/mood.ts 23 lines1export 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}
23types/index.d.ts 8 lines1export 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