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

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.
⏳ 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.
claude --version.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.
claude plugin marketplace add DarioFontanel/claude-code-mods
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.
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.
/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.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.
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.
Licenza: MIT — replay-theater è distribuita con licenza Apache-2.0.
hooks/register.js 227 lines1// 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