SLOPSHOPPER

quick-buttons

Pannello di bottoni rapidi: al primo avvio scegli quali skill e comandi diventano bottoni, poi li lanci con un click

newpanecommandtoast
★ 2v0.2.0MITupdated 2026-10-03DarioFontanel/claude-code-mods/quick-buttons
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · quick-buttons
│ ┃ Scegli i bottoni ✕ › fix the failing auth test and add an audit log call │ ┃ Nessuna skill o comando personalizzato in │ ┃ questa sessione. ⏺ 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 │ │ › /azioni │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Scegli i bottoni
Nessuna skill o comando personalizzato in questa sessione.
README

claude-code-mods

License: MIT Claude Code Mods

claude-code-mods è un marketplace di mod per Claude Code che ti permette di aggiungere all'interfaccia una barra della prompt cache, i prossimi passi a fine turno, un pannello di bottoni rapidi e il replay delle modifiche. Ogni mod è un plugin indipendente, installabile singolarmente con un comando.


Le quattro mod

⏳ cache-meter — una riga sopra il prompt con una barra che si svuota nel tempo: indica quanto resta dell'ora di prompt cache dall'ultima richiesta della conversazione principale. Verde oltre i 20 minuti, gialla oltre i 5, rossa sotto; allo scadere segnala che la richiesta successiva riscriverà la cache. Le richieste dei subagent non rinnovano la barra, perché usano una cache separata.

➡️ next-steps — a fine turno propone sopra il prompt due bottoni con i prossimi passi più naturali della conversazione, più un terzo bottone Replay. Premendo un passo, il prompt corrispondente viene inviato come se lo avessi scritto tu. I passi sono generati con una richiesta al modello a ogni fine turno, che consuma token.

🎛️ quick-buttons — un pannello laterale con un bottone per ogni comando ricorrente. Alla prima sessione dopo l'installazione la mod ti chiede quali skill e comandi vuoi come bottoni, elencando quelli presenti nella tua sessione; la scelta resta salvata tra le sessioni e puoi modificarla in qualsiasi momento.

⏪ replay-theater — registra le modifiche ai file dell'ultimo turno e le mostra in un pannello, un diff alla volta. È una variante della mod di esempio pubblicata da Anthropic in claude-code-playground, distribuita con licenza Apache-2.0; le differenze sono descritte in replay-theater/README.md.


Prerequisiti

  • Claude Code 2.1.287 o successivo — le mod sono attive di default da questa versione. Verifica con claude --version.
  • Terminale o tab Code dell'app Desktop — le sessioni WSL nell'app Desktop non eseguono le mod.

Una mod è codice eseguito con i tuoi permessi: può leggere e scrivere file, avviare processi e vedere prompt e tool call della sessione. Il sorgente di ogni mod è in hooks/, poche decine di righe: leggilo prima dell'installazione.


Installazione

  1. Aggiungi il marketplace a Claude Code:
   claude plugin marketplace add DarioFontanel/claude-code-mods
  1. Installa le mod che ti interessano, una per comando:
   claude plugin install cache-meter@dario-mods
   claude plugin install next-steps@dario-mods
   claude plugin install quick-buttons@dario-mods
   claude plugin install replay-theater@dario-mods

next-steps dichiara replay-theater come dipendenza: installandola, replay-theater viene installata automaticamente.

  1. Avvia una nuova sessione con claude.

Verifica: apri /plugin — sotto i tab compare la riga con le mod attive, ad esempio 4 mods active.

Gli stessi comandi sono disponibili dall'interno di una sessione come /plugin marketplace add e /plugin install. Per provare una mod in una sola sessione senza installarla, clona la repo e avvia claude --plugin-dir ./claude-code-mods/cache-meter.

Per disattivare una mod, disabilitala o disinstallala dal tab Installed di /plugin.


Comandi disponibili

  • /replay — apre il pannello di replay sull'ultimo turno che ha modificato file. Nel pannello: n passo successivo, p passo precedente, c o Esc chiusura.
  • /azioni — apre il pannello di quick-buttons. Su un terminale di almeno 144 colonne il pannello si apre da solo all'avvio della sessione. Ogni bottone ha come tasto rapido la prima lettera libera del nome del comando.
  • /azioni config — riapre la scelta dei bottoni. Nel pannello di scelta: scrivi nel campo Cerca per filtrare l'elenco, spostati con le frecce, Invio aggiunge o toglie un comando, s salva, n e p cambiano pagina. Puoi scegliere fino a 9 comandi.
  • ctrl+x poi Tab — sposta il focus sulla banda sopra il prompt. Da lì 1 e 2 inviano i passi proposti da next-steps, 3 apre il replay.

Personalizzazione

Bottoni di quick-buttons — si scelgono dal pannello, con /azioni config, senza modificare il codice. L'elenco propone skill, comandi personalizzati e comandi di altri plugin; i comandi integrati di Claude Code sono esclusi. Un comando scelto che una sessione non ha non viene mostrato in quella sessione.

Durata della cache in cache-meter — la costante TTL_MS in cache-meter/hooks/register.js vale un'ora, la durata della prompt cache negli abbonamenti Claude. Se la tua cache dura 5 minuti, imposta 5 * 60 * 1000.

Lingua di next-steps — la richiesta al modello è la costante ASK in next-steps/hooks/register.tsx. I prompt proposti seguono la lingua della conversazione.

Per modificare una mod, clona la repo e caricala con claude --plugin-dir: ogni salvataggio ricarica il modulo nella sessione aperta.


Struttura del progetto

claude-code-mods/
├── .claude-plugin/marketplace.json   # elenco delle mod installabili
├── cache-meter/
├── next-steps/
├── quick-buttons/
└── replay-theater/                   # Apache-2.0, con LICENSE propria

Ogni cartella è un plugin completo:

  • .claude-plugin/plugin.json — nome, versione e descrizione.
  • hooks/hooks.json — il modulo da caricare.
  • hooks/register.* — gli hook della mod.
  • tests/ — i test, eseguibili con claude plugin test ./<mod>.

claude plugin validate ./<mod> elenca gli eventi intercettati e le chiamate al motore di ciascuna mod.


Designed by Dario Fontanel, PhD

Aiuto PMI italiane ad integrare l'intelligenza artificiale per automatizzare i lavori ripetitivi, abbattere i costi e guadagnare tempo per crescere.

Sito YouTube Instagram LinkedIn TikTok AI Academy

Licenza: MIT — replay-theater è distribuita con licenza Apache-2.0.

Source 1 files
hooks/register.js 227 lines
1// L'id del pannello dei bottoni, usato per aprirlo e per riconoscerlo quando si disegna
2const PANE = 'quick-buttons'
3// L'id del pannello di scelta: quali comandi diventano bottoni
4const SETUP = 'quick-buttons-setup'
5// La chiave dello store in cui restano i comandi scelti, fra una sessione e l'altra
6const CHOSEN = 'commands'
7// Quanti bottoni al massimo
8const MAX = 9
9
10// I comandi fra cui scegliere: quelli della sessione che non sono di Claude Code né nostri
11let available = []
12// I comandi scelti, o null finché la scelta non è mai stata fatta
13let chosen = null
14// La scelta in corso nel pannello di scelta, il testo che filtra l'elenco e la pagina mostrata
15let draft = []
16let filter = ''
17let page = 0
18// Quanti comandi stanno in una pagina: dipende dall'altezza del pannello, letta quando si disegna
19let pageSize = 10
20// L'esito dell'ultimo click, mostrato in fondo al pannello
21let status = ''
22
23// Rilegge i comandi della sessione: una skill o una mod può essere arrivata dopo
24async function loadCommands($) {
25  try {
26    const list = await $.command.list()
27    available = list
28      .filter((c) => c.source !== 'builtin' && c.name !== 'azioni')
29      .map((c) => c.name)
30      .sort()
31  } catch {
32    available = []
33  }
34}
35
36// I comandi scelti che la sessione ha davvero: un bottone che non parte non serve
37function shown() {
38  return (chosen ?? []).filter((name) => available.includes(name))
39}
40
41// Il tasto rapido di ogni bottone: la prima lettera libera del nome, altrimenti una cifra
42function hotkeys(names) {
43  const taken = new Set()
44  return names.map((name) => {
45    const free = [...name.toLowerCase(), ...'123456789'].find((k) => /[a-z0-9]/.test(k) && !taken.has(k))
46    taken.add(free)
47    return free
48  })
49}
50
51// Il pannello dei bottoni è una barra stretta a lato, alta quanto i suoi bottoni
52function paneSpec() {
53  return { id: PANE, title: 'Azioni', columns: 18, rows: shown().length + 3 }
54}
55
56// Chiude un pannello che può anche non essere aperto
57async function closePane($, id) {
58  try {
59    await $.ui.close({ id })
60  } catch {}
61}
62
63// I comandi che il filtro lascia passare
64function matching() {
65  const text = filter.trim().toLowerCase()
66  return text ? available.filter((name) => name.toLowerCase().includes(text)) : available
67}
68
69// Apre la scelta partendo dai comandi già scelti
70async function openSetup($, focus) {
71  draft = [...(chosen ?? [])]
72  filter = ''
73  page = 0
74  status = ''
75  await closePane($, PANE)
76  const spec = { id: SETUP, title: 'Scegli i bottoni', columns: 34, rows: 16 }
77  return $.ui.open(focus ? { ...spec, focus: true, closeOnEscape: true } : spec)
78}
79
80// Un comando entra nella scelta o ne esce
81function toggle($, name) {
82  if (draft.includes(name)) draft = draft.filter((n) => n !== name)
83  else if (draft.length < MAX) draft = [...draft, name]
84  $.ui.invalidate('ui.render')
85}
86
87// Il testo scritto nel campo restringe l'elenco, che riparte dalla prima pagina
88function setFilter($, text) {
89  filter = text
90  page = 0
91  $.ui.invalidate('ui.render')
92}
93
94function turnPage($, by) {
95  const last = Math.max(0, Math.ceil(matching().length / pageSize) - 1)
96  page = Math.min(last, Math.max(0, page + by))
97  $.ui.invalidate('ui.render')
98}
99
100// La scelta è fatta: resta nello store e il pannello dei bottoni prende il posto di questo
101async function save($) {
102  chosen = draft
103  status = ''
104  await $.store.set(CHOSEN, chosen)
105  await closePane($, SETUP)
106  await $.ui.open({ ...paneSpec(), focus: true })
107}
108
109// Il click su un bottone: lancia il comando e racconta l'esito in `status`
110async function press($, command) {
111  status = '/' + command + ' in coda'
112  $.ui.invalidate('ui.render')
113  try {
114    // Come se fosse stato digitato: parte appena la sessione è libera
115    await $.command.run({ command, args: '' })
116    status = '/' + command + ' lanciato'
117  } catch (error) {
118    status = '/' + command + ' non partito: ' + (error?.message ?? error)
119  }
120  $.ui.invalidate('ui.render')
121}
122
123export function register(on) {
124  on('session.start', async ($, e, next) => {
125    await loadCommands($)
126    const stored = await $.store.get(CHOSEN)
127    chosen = Array.isArray(stored) ? stored.filter((name) => typeof name === 'string') : null
128    // immediate: il pannello si apre anche mentre Claude lavora
129    await $.command.register({
130      name: 'azioni',
131      description: 'Apri i bottoni rapidi; "/azioni config" per scegliere quali comandi mostrare',
132      immediate: true,
133    })
134    if (chosen === null) {
135      // Prima sessione dopo l'installazione: si chiede quali comandi diventano bottoni.
136      // Da solo il pannello si siede solo su un terminale largo; altrimenti lo dice il toast
137      void openSetup($, false).then((opened) => {
138        if (!opened.isPlaced) $.ui.toast('Scegli i tuoi bottoni rapidi con /azioni')
139      })
140    } else {
141      void $.ui.open(paneSpec())
142    }
143    return next(e)
144  })
145
146  on('command.run', { command: 'azioni' }, async ($, e) => {
147    await loadCommands($)
148    const isSetup = chosen === null || e.args.trim() === 'config'
149    const opened = isSetup ? await openSetup($, true) : await $.ui.open({ ...paneSpec(), focus: true })
150    // Se la superficie non disegna il pannello, dirlo invece di tacere
151    return opened.isPlaced ? {} : { text: 'Pannello non disegnato: ' + opened.reason }
152  })
153
154  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
155    if (e.requestId !== PANE && e.requestId !== SETUP) return next(e)
156    const { Box, Text, Button, Input } = $.ui.resolve(e)
157    const dim = (text) => Text({ dimColor: true, wrap: 'wrap', children: [text] })
158
159    if (e.requestId === SETUP) {
160      if (available.length === 0) {
161        return dim('Nessuna skill o comando personalizzato in questa sessione.')
162      }
163      // Le righe che restano all'elenco, tolte quelle di intestazione, campo e comandi
164      pageSize = Math.max(5, (e.props.scroll?.bodyRows ?? 16) - 6)
165      const width = Math.max(10, (e.props.bodyColumns ?? 34) - 3)
166      const found = matching()
167      const pages = Math.max(1, Math.ceil(found.length / pageSize))
168      const rows = found.slice(page * pageSize, (page + 1) * pageSize).map((name) =>
169        Button({
170          key: 'pick-' + name,
171          label: (draft.includes(name) ? '✓ ' : '· ') + (name.length > width ? name.slice(0, width - 1) + '…' : name),
172          plain: true,
173          onPress: () => toggle($, name),
174        }),
175      )
176      return Box({
177        flexDirection: 'column',
178        children: [
179          dim(draft.length + '/' + MAX + ' scelti' + (draft.length ? ': ' + draft.join(', ') : '')),
180          Input({
181            key: 'filter',
182            label: 'Cerca ',
183            placeholder: 'nome della skill',
184            value: filter,
185            submitLabel: 'filtra',
186            autoFocus: true,
187            onInput: (text) => setFilter($, text),
188            onSubmit: (text) => setFilter($, text),
189          }),
190          ...rows,
191          ...(rows.length === 0 ? [dim('Nessun comando con questo nome.')] : []),
192          Box({
193            flexDirection: 'row',
194            columnGap: 2,
195            children: [
196              ...(pages > 1
197                ? [
198                    Button({ key: 'prev', label: 'Indietro', hotkey: 'p', plain: true, onPress: () => turnPage($, -1) }),
199                    Button({ key: 'next', label: 'Avanti', hotkey: 'n', plain: true, onPress: () => turnPage($, 1) }),
200                  ]
201                : []),
202              Button({ key: 'save', label: 'Salva', hotkey: 's', plain: true, onPress: () => save($) }),
203            ],
204          }),
205          dim('pagina ' + (page + 1) + ' di ' + pages + ' · Invio aggiunge o toglie'),
206        ],
207      })
208    }
209
210    const names = shown()
211    const keys = hotkeys(names)
212    const buttons = names.map((name, i) =>
213      Button({ key: name, label: name, hotkey: keys[i], plain: true, onPress: () => press($, name) }),
214    )
215
216    return Box({
217      flexDirection: 'column',
218      children: [
219        ...buttons,
220        ...(buttons.length === 0 ? [dim('Nessun bottone scelto.')] : []),
221        Button({ key: 'setup', label: 'modifica', plain: true, dimColor: true, onPress: () => void openSetup($, true) }),
222        dim(status),
223      ],
224    })
225  })
226}
227