SLOPSHOPPER

lazzarone

Lazzarone: when Claude has been waiting on you for a few minutes (a permission, a question, a long turn just finished), a system notification calls you back

newguardprompttimer
v0.1.0no licenseupdated 2026-10-25pompeitech/affreschi/plugins/lazzarone
A shopper browsing a rack in a slop shop
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
agent-usage/agent-usage opens a live pane (it polls every 2s while an agent runs) where every agent plays a character cast from its type or name: Pulcinella for the main conversation, the artist (beret and brush) for frontend and UI work, the nerd (glasses) for planners and architects, the explorer (head lamp), the reviewer (monocle), the guard (helmet) for security, the tester (lab mask) and the handyman (flat cap) for the rest; their eyes follow the status. In scena holds the main conversation, the agents at work and the ones that did something this turn; at the next prompt the finished ones move to the archivio, one row per role with count, tokens and failures (▸ opens it, svuota archivio clears it and keeps its tokens in the totals). Press a name for model, input/output/cache tokens, requests, tool calls and run time. Teammates and agents started before the mod loaded are listed too. The status line shows the subagents running, and a toast says what each one consumed when it finishesTerminal and desktop
pulcinellaThe band above the prompt: Pulcinella in true pixel art: an SVG of 14×16 pixels in the desktop app, a compact 10×10 drawn two pixels to a character with half blocks (five lines) in the terminal (floppy hat, black half mask, pink cheeks, white blouse), animated two frames per mood (the hat sways and a hammer swings while a tool runs, he blinks at rest, a little z floats while he naps), beside the context window, read again after every model request: the tokens used, the headroom before compaction (vineyard green, then magma and lava past 60% and 85% of the threshold), an estimate of the turns left at the recent pace, the window split by category in one bar, and the largest categories as chips (hide/legend toggles them). details or /context-map opens a pane with every category's tokens and share, the auto-compact threshold, and what fills the window: memory files, MCP servers, custom agents, skills He frets when a command fails, cheers green tests, gasps at changes of 300+ lines, naps after ten quiet minutes, and his line in Neapolitan sits under the bar. His hat grows a pixel per level with the tokens you spend (1M, 10M, 50M, 200M), kept across sessionsTerminal and desktop
maschere/maschera opens a picker (or /maschera arlecchino puts one on at once) and Claude answers in that mask's voice: Pulcinella warm and ironic with a Neapolitan word now and then, Arlecchino as terse as it gets, Balanzone thorough with the reasons and trade-offs. Only the tone changes: the mask's section goes last in the system prompt and says every other instruction wins. The mask worn shows in the status line and is remembered for the next sessionTerminal and desktop
spaccanapoliA row above the prompt with the git branch and its base, commits ahead and behind, files changed with lines added and removed (and a bar of the two), and the branch's PR from gh (open, draft, mergeable, conflicts). Changes made right on the base branch turn it into a red warning. Read again every 10s, after every tool that can change files and at the end of a turn; outside a repository it stays away. Next to pulcinella each keeps the other's bandTerminal and desktop
golfo/golfo opens Il Golfo: the last seven days as bars of cost and tokens, today and the priciest day picked out, and the week's cost by project, recorded across sessions (90 days kept). With a weeklyBudget set in the plugin's options, a bar shows how much of it is spent, and a toast warns at 80% and 100%Terminal and desktop
lazzaroneWhen Claude has been waiting on you for minutes (3 by default, in the plugin's options): a permission, a question, a plan to approve, or a turn of two minutes or more just finished, a system notification says what it waits for. One call per wait; answering or writing a prompt clears themTerminal and desktop
chiaveCounts, per project, the calls you approve by hand, as the rule that would allow them (Bash(npm run lint:*), WebFetch(domain:docs.github.com), an MCP tool), with a line under the permission dialog that keeps the tally. Past threshold approvals (3 by default) a toast says a rule is ready, and /chiave offers it: Consenti nel progetto writes it into .claude/settings.json, Solo per me into .claude/settings.local.json, Ignora never offers it again, togli takes an added rule back. Risky commands (rm, sudo, git push, curl, mv…) and chained or piped ones are never offered; a settings file that is not plain JSON is left aloneTerminal and desktop

The palette

The colors live in each mod's hooks/palette.ts (a plugin cannot import another plugin's files, so each keeps 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>
npx -y @biomejs/biome@2.2.4 check plugins

Code follows the officina standards: Biome in StandardJS style (biome.json), braced blocks set off by blank lines, no as casts beyond as const (values the engine hands over as unknown are narrowed by checks), and failures logged to the debug log with $.ui.log(..., { to: 'debug' }), never swallowed.

Source 2 files
hooks/register.tsx 91 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { describe, dueWaits, titleFor, type Wait } from './waits'
4
5// How often the waits are looked at, and how long a turn must last for its end to be worth a call.
6const CHECK_MS = 15_000
7const LONG_TURN_MS = 2 * 60 * 1000
8const TURN = 'turn'
9
10// The module's own: what Claude waits on you for, by tool call (or the finished turn).
11const waits = new Map<string, Wait>()
12
13function logFailure($: EngineInterface, what: string, error: unknown): void {
14  const reason = error instanceof Error ? error.message : String(error)
15  $.ui.log(`lazzarone: ${what} failed: ${reason}`, { to: 'debug' })
16}
17
18// Calls you, once per wait, for every wait that has lasted long enough.
19async function check($: EngineInterface, afterMs: number): Promise<void> {
20  const now = await $.clock.now()
21
22  for (const id of dueWaits(waits, now, afterMs)) {
23    const wait = waits.get(id)
24
25    if (wait !== undefined) {
26      waits.set(id, { ...wait, isCalled: true })
27      await $.ui.notify(wait.what, { title: titleFor(now - wait.since) })
28    }
29  }
30}
31
32export const register: Register = (on, options) => {
33  const minutes = typeof options.minutes === 'number' && options.minutes > 0 ? options.minutes : 3
34  const afterMs = minutes * 60_000
35
36  on('session.start', async ($, e, next) => {
37    waits.clear()
38    $.clock.every(CHECK_MS, () => {
39      check($, afterMs).catch(error => logFailure($, 'calling you', error))
40    })
41
42    return next(e)
43  })
44
45  // A call the engine puts to you (a permission, a question, a plan) starts a wait.
46  on('tool.check', async ($, e, next) => {
47    const verdict = await next(e)
48
49    if (verdict.decision === 'ask' && e.tool_use_id !== undefined) {
50      waits.set(e.tool_use_id, {
51        what: describe(e.tool, e.input),
52        since: await $.clock.now(),
53        isCalled: false
54      })
55    }
56
57    return verdict
58  })
59
60  // However the call went (run, refused, answered), its wait is over.
61  on('tool.call', async (_$, e, next) => {
62    try {
63      return await next(e)
64    } finally {
65      waits.delete(e.tool_use_id)
66    }
67  })
68
69  // A long turn that ends leaves Claude waiting for your next word.
70  on('turn.complete', async ($, e, next) => {
71    const result = await next(e)
72
73    if (!e.isAborted && e.durationMs >= LONG_TURN_MS) {
74      waits.set(TURN, {
75        what: 'ha finito: dai un’occhiata',
76        since: await $.clock.now(),
77        isCalled: false
78      })
79    }
80
81    return result
82  })
83
84  // You are back: nothing is waiting any more.
85  on('prompt.submit', async (_$, e, next) => {
86    waits.clear()
87
88    return next(e)
89  })
90}
91
hooks/waits.ts 46 lines
1// What Claude is waiting on you for, how to say it, and which waits are due a call.
2
3export type Wait = { what: string; since: number; isCalled: boolean }
4
5// The command a Bash call wants to run, when the input carries one.
6function commandOf(input: unknown): string | undefined {
7  if (typeof input !== 'object' || input === null || !('command' in input)) {
8    return undefined
9  }
10
11  return typeof input.command === 'string' ? input.command : undefined
12}
13
14// What a tool call that needs you is waiting for, in a few words.
15export function describe(tool: string, input: unknown): string {
16  if (tool === 'AskUserQuestion') {
17    return 'ti ha fatto una domanda'
18  }
19
20  if (tool === 'ExitPlanMode') {
21    return 'ti propone un piano da approvare'
22  }
23
24  const command = commandOf(input)
25
26  if (command !== undefined) {
27    const short = command.trim().split('\n')[0] ?? ''
28
29    return `vuole il permesso per: ${short.length > 60 ? `${short.slice(0, 59)}…` : short}`
30  }
31
32  return `vuole il permesso per usare ${tool}`
33}
34
35// The waits that have lasted `afterMs` and have not had their call yet.
36export function dueWaits(waits: ReadonlyMap<string, Wait>, now: number, afterMs: number): string[] {
37  return [...waits].filter(([, w]) => !w.isCalled && now - w.since >= afterMs).map(([id]) => id)
38}
39
40// The notification's title for a wait that has lasted `ms`.
41export function titleFor(ms: number): string {
42  const minutes = Math.max(1, Math.round(ms / 60_000))
43
44  return `Claude ti aspetta da ${minutes} ${minutes === 1 ? 'minuto' : 'minuti'}`
45}
46