SLOPSHOPPER

vesuvius-theme

Spinner, righe delle tool call e fine turno nei colori del design system Vesuvius

newspinnerrowstimer
v0.1.0no licenseupdated 2026-10-10pompeitech/affreschi/plugins/vesuvius-theme
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · vesuvius-theme
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ▍ ⟨Claude Code's own drawing⟩ ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ▲ Erupted for 42s · Rumbling… ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Spinner
· Rumbling…
Turn line
▲ Erupted for 42s
Tool row
▍ ⟨Claude Code's own drawing⟩
README

affreschi

Claude Code interface mods, collected in a private pompeitech marketplace. Just as frescoes decorated the walls of Pompeii's houses, these mods decorate Claude Code's interface.

Layout

affreschi/
├── .claude-plugin/marketplace.json   # the list of mods
└── plugins/
    └── <mod-name>/                   # one folder (one plugin) per mod
        ├── .claude-plugin/plugin.json
        ├── hooks/
        │   ├── hooks.json
        │   ├── palette.ts            # Vesuvius colors
        │   └── register.tsx
        ├── types/index.d.ts          # the mod's $.state contract, when it has one
        └── tests/

Adding a mod

  1. Create the folder plugins/<mod-name>/ with its files.
  2. Add it to the plugins array in .claude-plugin/marketplace.json:
   { "name": "<mod-name>", "source": "./plugins/<mod-name>", "description": "..." }
  1. Check it: claude plugin validate ./plugins/<mod-name>

Development

Load a mod for the current session only (it reloads when you save):

claude --plugin-dir ./plugins/<mod-name>

Installing

/plugin marketplace add pompeitech/affreschi
/plugin install <mod-name>@affreschi

The mods

ModWhat it doesWhere it shows
vesuvius-themeA spinner with a glowing crater and volcanic words, a stripe of color beside every tool call (magma while it runs, lava when it fails, ash once done), and the end-of-turn line as an eruption (▲ Erupted for 1m 4s)Terminal; on the desktop, only the tool call rows
touched-files/touched-files opens a pane listing the files read (R) and edited (M, with +/-) this turn; press a file to see its diff. At the end of a turn a toast says how many files changedTerminal and desktop
context-mapA band above the prompt with the context window split by category, a percentage that turns from tuff to magma to lava past 60% and 85%, and details for the legendTerminal and desktop

The palette

The colors live in each mod's hooks/palette.ts (a plugin cannot import another plugin's files, so all three keep a copy). They are placeholders, to be replaced with the Vesuvius design system's.

Tests

claude plugin validate ./plugins/<mod-name>
claude plugin test ./plugins/<mod-name>
Source 2 files
hooks/register.tsx 100 lines
1import type { Register, Timer } from 'claude-code'
2
3import { VESUVIUS } from './palette'
4
5// The words for the spinner and the end-of-turn line, in place of Claude Code's.
6const WORDS: [string, ...string[]] = ['Bubbling', 'Erupting', 'Smoking', 'Rumbling', 'Venting', 'Flowing', 'Heating the magma']
7const PAST_WORDS: [string, ...string[]] = ['Erupted', 'Bubbled', 'Smoked', 'Rumbled', 'Flowed', 'Vented']
8
9// The crater's glow: one frame per tick, alternating colors.
10const FRAMES = ['·', '∘', '○', '◎', '●', '◎', '○', '∘']
11const TICK_MS = 140
12
13export function formatDuration(ms: number): string {
14  const seconds = Math.max(0, Math.round(ms / 1000))
15  const minutes = Math.floor(seconds / 60)
16
17  return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`
18}
19
20export function pick<T>(list: readonly [T, ...T[]], seed: number): T {
21  return list[Math.abs(Math.floor(seed)) % list.length] ?? list[0]
22}
23
24export const register: Register = on => {
25  // Module values: they reset on every reload, which is fine here.
26  let frame = 0
27  let word = WORDS[0]
28  let timer: Timer | undefined
29
30  on('turn.start', async ($, e, next) => {
31    word = pick(WORDS, await $.clock.now())
32    timer?.cancel()
33    timer = $.clock.every(TICK_MS, () => {
34      frame = (frame + 1) % FRAMES.length
35      $.ui.invalidate('ui.render')
36    })
37
38    return next(e)
39  })
40
41  on('turn.complete', async ($, e, next) => {
42    timer?.cancel()
43    timer = undefined
44
45    return next(e)
46  })
47
48  // The spinner: redrawn in the terminal; on the desktop Claude Code's own stays,
49  // since there it says what the step is doing ("Creating notes.md").
50  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
51    if (e.surface !== 'terminal') return next(e)
52
53    const { Box, Text } = $.ui.resolve(e)
54    const text = e.props.message ?? word
55    const color = frame % 2 === 0 ? VESUVIUS.lava : VESUVIUS.magma
56
57    return (
58      <Box flexDirection="row" columnGap={1}>
59        <Text color={color} bold>
60          {FRAMES[frame]}
61        </Text>
62        <Text color={VESUVIUS.magma}>{text}…</Text>
63      </Box>
64    )
65  })
66
67  // Every tool call row: Claude Code's row, with a stripe of color on its left.
68  on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
69    const { Box, Text } = $.ui.resolve(e)
70    const theirs = await next(e)
71    const color = e.props.isErrored
72      ? VESUVIUS.lava
73      : e.props.isRunning
74        ? VESUVIUS.magma
75        : VESUVIUS.ash
76
77    return (
78      <Box flexDirection="row" columnGap={1}>
79        <Text color={color}>▍</Text>
80        {theirs}
81      </Box>
82    )
83  })
84
85  // The line that closes a turn ("Baked for 3s") becomes an eruption.
86  on('ui.render', { component: 'TurnDuration' }, async ($, e) => {
87    const { Box, Text } = $.ui.resolve(e)
88    const past = pick(PAST_WORDS, e.props.durationMs / 1000)
89
90    return (
91      <Box flexDirection="row" columnGap={1}>
92        <Text color={VESUVIUS.lava}>▲</Text>
93        <Text color={VESUVIUS.tuff}>
94          {past} for {formatDuration(e.props.durationMs)}
95        </Text>
96      </Box>
97    )
98  })
99}
100
hooks/palette.ts 22 lines
1// Vesuvius palette (placeholder): replace the hex values with the design system's.
2// Every mod in affreschi keeps its own copy, because a plugin cannot import another plugin's files.
3export const VESUVIUS = {
4  lava: '#E4572E',
5  magma: '#F28C28',
6  sulfur: '#F2C14E',
7  tuff: '#C9B79C',
8  ash: '#8A8D91',
9  gulf: '#2E86AB',
10  vineyard: '#7A9E4E',
11} as const
12
13// The order categories take their colors in, warmest first.
14export const SEQUENCE = [
15  VESUVIUS.lava,
16  VESUVIUS.magma,
17  VESUVIUS.sulfur,
18  VESUVIUS.tuff,
19  VESUVIUS.gulf,
20  VESUVIUS.vineyard,
21] as const
22