Disegna nell'interfaccia con un mod
Disegna riquadri, una fascia sopra il prompt, pulsanti e campi di testo da un mod di Claude Code, gestisci pressioni e input e mantieni lo stato tra i ridisegni e le sessioni.
Un mod può disegnare la propria interfaccia in Claude Code e modificare parti dell'interfaccia che Claude Code già disegna. Ogni posizione in cui un mod può disegnare è chiamata punto di rendering, ad esempio un riquadro, la fascia sopra il prompt o lo spinner. Claude Code attiva l'evento ui.render ogni volta che sta per disegnare un punto di rendering, e il tuo hook per quell'evento restituisce cosa disegnare lì.
Questa mappa mostra dove un mod può disegnare in una sessione del terminale:
In un terminale più stretto, il riquadro si trova sopra il prompt anziché accanto alla trascrizione.
Crea il tuo primo mod prima di iniziare da qui. Comincia con l'esempio pratico, che crea un riquadro con due schede e un contatore, poi leggi la sezione relativa a ciascun elemento che vuoi modificare.
[!NOTE]
Per consultare una singola prop o un limite, vedi il riferimento.
Crea un pannello con schede
In questa sezione crei un mod che aggiunge un comando /hello-tabs, e il comando apre un pannello. Un pannello è una barra laterale accanto alla trascrizione in un terminale ampio a schermo intero, oppure un'area incorniciata sopra il prompt negli altri casi. Questo pannello mostra due schede, e la seconda scheda ha un pulsante che aggiunge uno a un contatore. Il conteggio è ancora lì dopo che riavvii Claude Code.
Il mod finito ha questo aspetto. La registrazione apre il pannello, passa alla seconda scheda, preme il pulsante alcune volte e torna alla prima scheda:
Le schede sono due pulsanti in fila. Il mod tiene traccia di quale sia attiva e disegna il contenuto di quella scheda sotto la fila.
1. Crea il plugin
Un mod è un plugin con un manifest, un hooks.json che punta al tuo codice, e il file di codice. Crea un mod spiega ciascuno di essi. Crea una directory chiamata hello-tabs con le directory .claude-plugin e hooks al suo interno, poi salva i primi due file.
Salva il manifest come hello-tabs/.claude-plugin/plugin.json:
{
"name": "hello-tabs",
"version": "0.1.0",
"description": "Opens a pane with two tabs and a counter",
"author": { "name": "Your Name" }
}
Indica il tuo punto di ingresso in hello-tabs/hooks/hooks.json:
{
"modules": ["./register.js"]
}
2. Scrivi il codice
Questo elenco indica cosa fa ciascun hook, nell'ordine in cui compaiono nel codice:
- Aggiunge il comando
/hello-tabse carica il conteggio salvato da una sessione precedente - Apre il pannello quando esegui quel comando
- Disegna il contenuto del pannello: la fila di schede e il corpo della scheda aperta
Due variabili a livello di modulo, tab e count, contengono lo stato del pannello.
Salva questo come hello-tabs/hooks/register.js:
// The pane's id, used to open the pane and to recognize it when drawing
const PANE = 'hello-tabs'
// What the pane shows: which tab is open, and the counter's value
let tab = 'one'
let count = 0
export function register(on) {
// Runs before your first prompt, and again after a reload
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
// Load the count an earlier session saved, if there is one
const saved = await $.store.get('count')
if (typeof saved === 'number') count = saved
return next(e)
})
// Runs when you type /hello-tabs
on('command.run', { command: 'hello-tabs' }, async ($) => {
// Open the pane, give it the keyboard, and let Esc close it
await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
// Print nothing in the transcript
return {}
})
// Runs each time Claude Code draws a pane
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Leave other mods' panes alone
if (e.requestId !== PANE) return next(e)
// Get the elements this app can draw
const { Box, Text, Button } = $.ui.resolve(e)
// Ask Claude Code to run this hook again
const redraw = () => $.ui.invalidate('ui.render')
// One tab: a button that switches to its tab when pressed
const tabButton = (name, label, hotkey) =>
Button({
key: 'tab-' + name,
label,
hotkey,
plain: true,
// Dim the tab that isn't open
dimColor: tab !== name,
onPress: () => {
tab = name
redraw()
},
})
// What goes under the tabs, depending on which one is open
const body =
tab === 'one'
? [Text({ children: ['This is the first tab.'] })]
: [
Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({
key: 'more',
label: 'Add one',
hotkey: 'a',
onPress: async () => {
count += 1
redraw()
// Save the count so it's there after a restart
await $.store.set('count', count)
},
}),
Text({ children: ['Count: ' + count] }),
],
}),
]
// The whole pane: the row of tabs, a blank line, then the body
return Box({
flexDirection: 'column',
children: [
Box({
flexDirection: 'row',
columnGap: 3,
children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
}),
Text({ children: [' '] }),
...body,
],
})
})
}
Ogni hook fa anche qualcosa che il codice non rende esplicito:
session.startlegge anche il conteggio salvato da$.store, un archivio chiave-valore che persiste tra le sessioni.command.runcomunica soltanto a Claude Code che il pannello esiste. Aprire un pannello di per sé non disegna nulla: Claude Code attiva poiui.renderper chiedere cosa inserirvi.ui.renderrestituisce l'albero degli elementi, unBoxche contiene altri box, testo e pulsanti, e lo ricostruisce a partire databecountogni volta che viene eseguito.
Premere un pulsante esegue la sua callback onPress, che modifica una variabile e chiama redraw. Claude Code esegue quindi di nuovo l'hook ui.render, e l'hook costruisce un nuovo albero a partire dai nuovi valori. Ogni disegno interattivo usa questo ciclo di rendering: una callback modifica lo stato, e l'hook esegue di nuovo il rendering a partire dal nuovo stato.
3. Apri il pannello
Nella tua shell, avvia Claude Code con claude --plugin-dir ./hello-tabs. Nel prompt di Claude Code, esegui /hello-tabs. Si apre un pannello con 1: One e 2: Two nella parte superiore. Premi 2, poi premi a, il tasto di scelta rapida per Add one, alcune volte. Il conteggio sale.
4. Verifica che il conteggio sia stato salvato
Premi Esc per chiudere il pannello, poi esci dalla sessione. Nella tua shell, avvia di nuovo Claude Code con lo stesso comando claude --plugin-dir ./hello-tabs, e nel prompt di Claude Code esegui /hello-tabs. Il conteggio è dove l'avevi lasciato.
Per azzerare il conteggio, fai chiamare al mod $.store.delete('count'). Mantieni lo stato spiega quanto dura ciascun tipo di valore.
Scegli dove disegnare
Un hook ui.render viene eseguito per ogni punto di rendering, a meno che tu non lo limiti a quello in cui vuoi disegnare. Per scegliere il punto di rendering, passa un filtro, chiamato matcher, come secondo argomento di on. { component: 'Pane' } esegue l'hook solo per i pannelli. Nell'hook, e.component indica il punto, e.surface indica quale app sta disegnando e e.props contiene i dati propri del punto. Per un pannello, e.requestId è l'id con cui lo hai aperto.
Il pannello e la fascia sono vuoti finché un mod non li riempie. Seleziona una scheda per vedere che cos'è ciascuno e come disegnarci dentro:
Pannello
Un pannello è una barra laterale accanto alla trascrizione in un terminale a schermo intero ampio, oppure, negli altri casi, un'area incorniciata sopra il prompt. Con più pannelli aperti, ognuno ha una scheda che ne mostra il titolo.
Un pannello appare quando il tuo mod chiama $.ui.open con un id che scegli tu, come in $.ui.open({ id: 'hello-tabs' }). Apri un pannello al momento giusto descrive gli altri campi e quando un pannello attende un terminale più ampio.
Per disegnare nel tuo pannello, filtra su { component: 'Pane' } e verifica che e.requestId sia il tuo id.
Fascia sopra il prompt
La fascia è una striscia direttamente sopra il campo di input del prompt. È sempre presente e tutti i mod la condividono.
Il tuo hook restituisce un albero per mostrare qualcosa nella fascia, oppure next(e) per non mostrare nulla. Un albero sostituisce ciò che i mod dopo il tuo disegnano lì. Per mantenere ciò che disegnano loro, inserisci il risultato di await next(e) tra i figli di un Box nel tuo albero.
Per disegnare nella fascia, filtra su { component: 'AbovePrompt' }.
Modifica ciò che Claude Code disegna già
Claude Code disegna da sé la maggior parte della propria interfaccia: i messaggi, le righe delle chiamate agli strumenti, lo spinner e altro ancora. Anche ciascuna di queste parti è un punto di rendering, quindi un mod può cambiarne lo stile o sostituirla. Per modificarne una, filtra il tuo hook ui.render sul suo nome preso da questa tabella:
| Punto | Che cos'è |
|---|---|
UserMessage, AssistantMessage |
Un messaggio nella trascrizione |
ToolUse, ToolResult, ToolGroup |
La riga di una chiamata a uno strumento, il suo risultato e un gruppo compresso di chiamate |
CommandOutput |
La riga stampata da un comando |
AskUserQuestion |
La finestra di dialogo che Claude apre per farti una domanda |
Spinner, ToolProgress, TurnDuration |
Le righe di stato di un turno: la riga animata mentre Claude lavora, la riga di avanzamento in tempo reale di uno strumento in esecuzione e la riga che chiude un turno |
InfoNotice, SessionMode, PromptHint |
Le righe di stato sotto il logo, le etichette della modalità nel piè di pagina e la riga di suggerimento sotto il prompt |
Pane, AskUserQuestion, Spinner, UserMessage, AssistantMessage e i punti delle chiamate agli strumenti funzionano nel terminale, nell'app Desktop e nell'estensione VS Code. La tabella dei punti di rendering indica dove viene generato ciascuno degli altri.
In un punto che Claude Code disegna già, il tuo hook può modificare un dettaglio, sostituire il disegno o lasciarlo invariato. Seleziona una scheda per vedere ciascuna opzione applicata allo spinner. Gli esempi leggono una variabile calls conteggiata da un altro hook, come nel mod del tutorial.
Modifica un dettaglio
Per mantenere il disegno di Claude Code e modificarne una parte, passa a next una copia dell'evento con props modificate. Questo hook modifica il testo dopo la parola dello spinner:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, and change the text after its word
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
Lo spinner mantiene la sua animazione e la sua parola, e il tuo testo segue la parola:
Thinking · tool calls: 2…
Sostituisci il disegno
Per disegnare qualcosa di tuo al posto del punto, restituisci un albero e non chiamare next. Questo hook disegna una riga di testo dove si troverebbe lo spinner:
on('ui.render', { component: 'Spinner' }, async ($, e) => {
const { Text } = $.ui.resolve(e)
// No call to next, so this line is drawn in the spinner's place
return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
})
Mentre Claude lavora, viene mostrata la tua riga e non lo spinner di Claude Code:
Claude has made 2 tool calls
Lascialo invariato
Per lasciare il punto così come lo disegna Claude Code, restituisci next(e). Spesso un hook lo fa per alcuni eventi e non per altri. Questo hook lascia invariato lo spinner finché non c'è una chiamata da contare:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Nothing to show yet, so pass the event on unchanged
if (calls === 0) return next(e)
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
Prima della prima chiamata a uno strumento, lo spinner appare come senza il mod:
Thinking…
In questi punti, next(e) restituisce un riferimento al disegno di Claude Code, { type: 'engine', ref }, a meno che un mod eseguito dopo il tuo non abbia restituito un proprio albero. Per modificare il contenuto di quel disegno, passa a next una copia dell'evento con prop diverse, come fa la scheda Modifica un dettaglio. Puoi restituire il riferimento così com'è, oppure inserirlo in un Box accanto a elementi tuoi:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
const { Box, Text } = $.ui.resolve(e)
const theirs = await next(e)
return Box({ flexDirection: 'column', children: [theirs, Text({ children: ['under the spinner'] })] })
})
Mentre Claude lavora, lo spinner si anima come prima e under the spinner appare sotto di esso.
La richiesta di permesso non è un punto di rendering, quindi un mod non può modificare ciò che mostra. La finestra di dialogo delle domande, AskUserQuestion, lo è, quindi un mod può modificarla. Un albero per la finestra di dialogo deve contenere il riferimento esattamente una volta, con i tuoi elementi sopra di esso. In caso contrario, Claude Code disegna la propria finestra di dialogo.
Apri un pannello al momento giusto
Un pannello appare solo quando il tuo mod lo apre. Il modo e il momento in cui lo apri determinano se riceve il focus della tastiera, quanto spazio richiede e se viene mostrato o meno in un terminale stretto.
Per aprire un pannello, chiama $.ui.open con un id che scegli tu. L'id è il nome del pannello: il tuo hook ui.render lo verifica e lo passi di nuovo per chiudere il pannello.
await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
Per chiudere il pannello, chiama $.ui.close con l'id con cui lo hai aperto:
await $.ui.close({ id: 'hello-tabs' })
Oltre a id, $.ui.open accetta questi campi facoltativi:
| Campo | Che cosa fa |
|---|---|
title |
L'etichetta della scheda del pannello quando sono aperti più pannelli |
focus |
Richiede il focus della tastiera |
closeOnEscape |
Fa sì che Esc chiuda il pannello |
holdToasts |
Nel terminale, trattiene i toast finché è questo il pannello visualizzato. Consulta Trattieni i toast dietro una finestra di dialogo. |
rows |
L'altezza da richiedere quando il pannello si trova sopra il prompt. Il valore predefinito è un terzo dello spazio. |
columns |
La larghezza da richiedere quando il pannello si trova accanto alla trascrizione, al massimo il 45% della larghezza del terminale. Dopo che l'utente ha ridimensionato un pannello accanto alla trascrizione, in questa sessione o in una precedente, i pannelli si aprono lì con la larghezza da lui impostata. Lo stesso limite del 45% si applica a quella larghezza. |
L'estensione VS Code ignora rows e columns e dimensiona il pannello da sé.
focus, closeOnEscape e holdToasts sono facoltativi e accettano solo true. Per non usarne uno, omettilo. Passare false genera un errore come ui.open: focus is true or left out. Per impostarne uno in modo condizionale, aggiungi il campo solo quando la condizione è soddisfatta. Questa chiamata richiede il focus della tastiera solo quando items non è vuoto:
const pane = { id: 'hello-tabs', title: 'Hello tabs' }
await $.ui.open(items.length > 0 ? { ...pane, focus: true } : pane)
Per consentire a un comando di aprire il pannello mentre Claude sta lavorando, aggiungi immediate: true quando registri il comando. Senza questo campo, un comando digitato durante un turno attende la fine del turno.
Trattieni i toast dietro una finestra di dialogo
Passa holdToasts: true a $.ui.open quando il pannello è una finestra di dialogo a cui l'utente risponde e che poi lascia, in modo che i toast non appaiano mentre decide. Nel terminale, la sospensione dura finché è quel pannello a essere visualizzato, e un toast generato in quel lasso di tempo attende la fine della sospensione.
Claude Code trattiene i toast degli altri mod e le proprie notifiche di breve durata, oltre a quelli che il tuo mod genera con $.ui.toast. Ometti il campo in un pannello che resta aperto, così l'utente continua a vederli.
Quando un pannello attende un terminale più ampio
Un pannello che il tuo mod apre senza che gli venga chiesto non appare in un terminale stretto, così non può occupare uno schermo piccolo. Se appare o meno dipende da che cosa lo ha aperto:
- Aperto da un'azione dell'utente, come un comando che ha eseguito o un pulsante che ha premuto, il pannello appare a qualsiasi larghezza
- Aperto dal tuo mod di sua iniziativa, ad esempio da un timer o da un hook
turn.start, il pannello appare solo in un terminale largo almeno 144 colonne. Dopo che l'utente ha aperto quel pannello una volta da sé, bastano 110 colonne.
Quando il pannello appare, $.ui.open si risolve in { isPlaced: true }. Quando il pannello è in attesa, isPlaced è false e reason è una stringa che ne indica il motivo. Un pannello in attesa appare quando l'utente lo apre o allarga il terminale. Per segnalare che qualcosa è disponibile senza aprire un pannello, chiama $.ui.toast('Your message'), che mostra una notifica toast.
Disegna nell'estensione VS Code
Nell'estensione VS Code, ciò che un mod disegna appare nel pannello della chat con l'estensione alla versione 2.1.299 o successiva. Una sessione che non recupera i feature flag, come una su Amazon Bedrock, richiede la versione 2.1.304 o successiva. In un hook ui.render per il pannello, e.surface è vscode. Per verificare quali punti ed elementi il pannello disegna e quali prop ignora, consulta le tabelle dei punti di rendering e degli elementi.
Per far funzionare un mod nel pannello oltre che nel terminale, tieni conto di queste differenze:
- Pannelli: in un pannello largo 800 pixel o più, i pannelli si agganciano accanto alla conversazione,
e.props.placementè'dock'ede.viewport.isFullscreenètrue. In un pannello più stretto, i pannelli vengono disegnati sopra la casella del prompt,placementè'inline'e un pannello mostra 12 righe prima di scorrere. - Toast e righe di stato: si trovano in un'unica pila direttamente sopra la casella del prompt, insieme ai pannelli quando il pannello della chat è stretto, con una riga di stato per mod. La pila è nascosta mentre è visualizzata una richiesta di permesso o una finestra di dialogo.
- Etichette: il pannello incornicia ciò che un mod disegna o riscrive e lo etichetta con il nome del plugin, come in From the token-chart plugin o Edited by the token-chart plugin.
- Tastiera: i pulsanti non hanno tasti di scelta rapida, quindi l'utente fa clic su un pulsante o preme Enter su di esso quando ha il focus.
- Focus della tastiera: il pannello concede la richiesta
focus: truedi un pannello solo quando il pannello della chat ha il focus, Claude è inattivo senza domande in attesa e la casella del prompt è vuota. - La finestra di dialogo delle domande: in
AskUserQuestion, il pannello mantiene la propria scheda della domanda e disegna i tuoi elementi sopra di essa. Le prop che passi anextnon modificano la scheda. - Suggerimenti per il prompt:
$.prompt.suggestsi risolve conisShown: falsee non mostra nulla, perché il pannello riserva il testo di suggerimento attenuato della casella del prompt ai suggerimenti di Claude. - Limiti del testo: il pannello tronca una riga di stato a 200 caratteri o due righe, e un toast a 500 caratteri.
Costruire un albero di elementi
Ciò che un hook ui.render restituisce è un albero di elementi: una descrizione di cosa disegnare, composta da box, testo e controlli annidati l'uno dentro l'altro. Tu descrivi il disegno e Claude Code lo esegue nell'app che l'utente ha aperto: il terminale, l'app Desktop o l'estensione VS Code.
Per ottenere gli elementi, chiama $.ui.resolve(e) nel tuo hook, come in const { Box, Text, Button } = $.ui.resolve(e). Ogni elemento è una funzione. Le passi delle prop e inserisci in children gli elementi e le stringhe che vanno al suo interno.
Seleziona una scheda per vedere ciascuno degli elementi più comuni e come il terminale lo disegna:
Text
Text disegna una stringa, con uno stile facoltativo come bold e color:
Text({ children: ['This is the first tab.'] })
This is the first tab.
Box
Box dispone ciò che contiene, in una riga o in una colonna. Questo mette un pulsante e una riga di testo uno accanto all'altro, a due colonne di distanza:
Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({ key: 'more', label: 'Add one', onPress: addOne }),
Text({ children: ['Count: 0'] }),
],
})
[ Add one ] Count: 0
Button
Button è un controllo che l'utente può premere. Esegue la tua callback onPress. Con plain: true non ha parentesi e mostra il suo tasto di scelta rapida:
Button({ key: 'more', label: 'Add one', onPress: addOne })
Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
[ Add one ]
1: One
Input
Input è un campo di testo. Esegue la tua callback onSubmit con il testo quando l'utente preme Enter:
Input({
key: 'new-note',
label: 'Note',
placeholder: 'Type a note and press Enter',
value: '',
submitLabel: 'add',
onSubmit: addNote,
})
Note: Type a note and press Enter
Imposta rows a 2 o più per un campo che contiene più righe.
La galleria dell'interfaccia contiene esempi e screenshot della maggior parte degli elementi. Questa tabella elenca tutti gli elementi:
| Elemento | Cosa disegna | Dove |
|---|---|---|
Box |
Un contenitore flex che accetta prop di layout come flexDirection, borderStyle e width. Le prop di spaziatura come columnGap e padding accettano un numero o un nome di step da 'xs' a 'xl'. |
Ovunque |
Text |
Testo con stile. Accetta color, bold, dimColor, italic, wrap e altre come weight. Un color è una chiave del tema o un colore come 'red'. Un wrap è 'wrap', 'truncate', 'truncate-start', 'truncate-middle' o 'truncate-end'. |
Ovunque |
Button |
Un controllo che chiama onPress |
Ovunque |
Link, Code, Markdown |
Un link con href e una label facoltativa, un blocco di codice e testo formattato come le risposte di Claude. Markdown riceve il suo contenuto in una prop text, non in children, e richiede una key quando passi onLinkPress. |
Ovunque |
Input, Select |
Un campo di testo e un menu a discesa | Terminale, Desktop, VS Code |
Svg |
Un documento SVG | Desktop, VS Code |
Client |
Un'area disegnata da un tuo secondo file, per animazioni e input del puntatore. Quel file non riceve l'API dei mod. Raggiunge i tuoi hook inviando dati, che arrivano come evento ui.message. Se non riesce a caricarsi, a disegnare o a essere eseguito, i tuoi hook ricevono un evento ui.fault. |
Terminale, Desktop |
Raster, Image |
Una griglia di celle colorate e un'immagine | Terminale |
Se il tuo modulo è un file .tsx o .jsx, puoi scrivere l'albero in JSX. Destruttura prima gli elementi da $.ui.resolve(e).
Se il tuo disegno non compare, controlla nel suo albero se c'è una prop che un elemento non accetta o un figlio dove non ne sono previsti. Un hook che restituisce un albero del genere fallisce, e una riga hook skipped indica il tuo mod e la regola violata dall'albero. Per un Text in un pannello con una prop chiamata bogusProp, quella riga è:
my-mod: ui.render hook skipped: returned the wrong shape (a tree Pane refuses: Text prop "bogusProp" is not allowed)
Link nell'app Desktop e in VS Code
Nell'app Desktop e nell'estensione VS Code, un Link viene disegnato come testo semplice a meno che il suo href non soddisfi questi requisiti:
- Schema e host: un URL
https:o un URLhttp://localhostcomehttp://localhost:3000 - Nessuna
@: scrivi una@nel percorso o nella query come%40 - Grafia: quella restituita da
new URL(href).href, a parte una/mancante dopo l'host. Questo esclude un host in maiuscolo, uno spazio e:443in un URLhttps:.
Nel terminale questi requisiti non si applicano.
Quando un Client fallisce
Nel terminale, quando il file eseguito da un Client fallisce, una riga attenuata come my-mod: Client client/spinner.js: boom prende il posto del Client, e il resto del tuo disegno viene comunque mostrato.
Se il tuo mod gestisce ui.fault, Claude Code allora disegna di nuovo il punto.
Disegnare una griglia di celle colorate
Per una mappa di calore, una sparkline o un tabellone di gioco nel terminale, disegna un solo Raster e non un Box per ogni cella. Un Raster accetta una key, le sue dimensioni in columns e rows, e cells, una stringa base64 che contiene tutte le celle. Ogni cella è composta da tre numeri: il code point del carattere, il suo colore e il suo colore di sfondo. Un colore è un valore RGB a 24 bit in esadecimale, come 0xc62828 per un rosso. Il valore 0x01000000, uno sopra quell'intervallo, indica il colore predefinito del terminale.
Le app diverse dal terminale non hanno Raster, quindi controlla e.surface e disegna del testo in quei casi. Questo corpo del pannello disegna una mappa di calore tre per due:
// The value that means "use the terminal's default color"
const DEFAULT_COLOR = 0x01000000
// Pack rows of [character, color] pairs into the one string a Raster takes
// One cell is three numbers: the character's code point, its color, and its background
function cellsOf(rows) {
const numbers = rows.flat().flatMap(([char, color]) => [char.codePointAt(0), color, DEFAULT_COLOR])
return new Uint8Array(Uint32Array.from(numbers).buffer).toBase64()
}
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Draw only in the pane opened with the id 'heat'
if (e.requestId !== 'heat') return next(e)
const { Box, Text, Raster } = $.ui.resolve(e)
// Two rows of three cells, each a block character and its color
const rows = [
[['█', 0x2e7d32], ['█', 0xf9a825], ['█', 0xc62828]],
[['█', 0x2e7d32], ['█', 0x2e7d32], ['█', 0xf9a825]],
]
if (e.surface !== 'terminal') {
return Text({ children: ['The heat map needs the terminal.'] })
}
return Box({
flexDirection: 'column',
children: [Raster({ key: 'grid', columns: 3, rows: 2, cells: cellsOf(rows) })],
})
})
Nel terminale, il pannello mostra la griglia:
L'array rows è la parte che modificheresti, e cellsOf lo trasforma nella stringa compatta. L'hook disegna solo in un pannello il cui id è heat, quindi aprine uno con $.ui.open({ id: 'heat' }) da un comando, come l'esempio hello-tabs apre il suo pannello.
Ogni carattere deve essere largo una cella. Per animare un Raster già sullo schermo, chiama $.ui.blit con l'id del pannello come requestId, la key del Raster, le stesse dimensioni e le nuove celle. Per questo esempio, è $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). Ridisegna quel singolo elemento senza eseguire di nuovo il tuo hook ui.render.
Rispondere alle pressioni e alla digitazione
Quando l'utente preme un pulsante, digita in un campo o sceglie da un elenco disegnato dal tuo mod, Claude Code chiama la callback di quel controllo, che viene eseguita nel tuo modulo. Ogni controllo accetta le proprie callback:
Button: accettaonPress(e), dovee.surfaceè l'app da cui proviene la pressioneInput: accettaonSubmit(value)eonInput(value)Select: accettaonSelect(value)con le sue scelte inoptions, un elenco di almeno una scelta con valori univoci, come[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
Quali controlli richiedono una key
Assegna a ogni Input e Select una prop key, come key: 'size'. Un Button può fare a meno di una key quando ha una prop label o un singolo figlio di tipo stringa. La sua chiave è allora il valore di label o quella stringa.
I test e gli altri mod identificano un controllo tramite la sua chiave:
- Test: un test preme un controllo o vi digita tramite la sua chiave.
- Altri mod: Claude Code attiva
ui.press,ui.inputoui.selectcon la chiave del controllo ine.elementprima di eseguire la callback del controllo. L'hook di un altro mod per quell'evento vede ciò che l'utente ha digitato nel tuoInpute può modificare quel testo o impedire l'esecuzione della tua callback. L'API dei mod non fornisce a quel mod alcun metodo per premere i tuoi pulsanti.
Se un messaggio dice needs a key o is drawn twice, trova la causa in base a quel messaggio.
Focus della tastiera e tasti di scelta rapida
Il tuo mod non legge mai direttamente la tastiera. L'utente preme un tasto, Claude Code decide a quale dei tuoi controlli è destinato e viene eseguita la callback di quel controllo. A parte un tasto di scelta rapida numerico sulla banda o un'associazione action, questo avviene solo mentre il tuo riquadro o la tua banda ha il focus della tastiera. Il resto del tempo, i tasti vanno al prompt.
Nell'estensione VS Code, i pulsanti non hanno tasti di scelta rapida: l'utente fa clic su un pulsante o preme Enter su di esso, e digita nei campi.
Come un riquadro ottiene il focus della tastiera
Un riquadro ottiene il focus della tastiera quando:
- Il tuo mod lo apre con
focus: trueda un comando o da una pressione - L'utente preme Ctrl+X e poi Tab
- L'utente vi fa clic, cosa che nel terminale richiede il rendering a schermo intero
Claude Code concede focus: true solo mentre il prompt è vuoto e nient'altro ha il focus della tastiera. Un riquadro che si apre mentre l'utente sta digitando non ne intercetta i tasti premuti.
Cosa fa ciascun tasto
Questa tabella elenca cosa fa un tasto mentre il tuo riquadro o la tua banda ha il focus della tastiera:
| Tasto | Cosa fa |
|---|---|
| Tab | Passa al controllo successivo |
| Su e Giù | Spostano tra i controlli finché il tuo disegno ci sta. Quando il riquadro o la banda ha più righe di quante ne possa mostrare, lo scorrono. In un Input nel terminale, spostano tra le righe del campo, e dalla sua prima o ultima riga passano al controllo precedente o successivo. |
| Enter | Preme il Button con il focus, invia l'Input con il focus o effettua la scelta in un Select |
| Il tasto di scelta rapida di un pulsante | Preme quel pulsante. Mentre un Input ha il focus, ogni tasto stampabile va al campo. |
| Pagina su, Pagina giù, Home e Fine | Scorrono il tuo riquadro o la tua banda quando ha più righe di quante ne possa mostrare |
| Ctrl+X e poi un tasto freccia | Ridimensiona il tuo riquadro. Sinistra o Su gli dà più spazio, e Destra o Giù restituisce lo spazio. Un riquadro accanto alla trascrizione smette di allargarsi al 45% della larghezza del terminale. |
| Ctrl+X e poi X | Chiude il tuo riquadro, anche mentre uno dei suoi campi ha il focus |
| Ctrl+X e poi Ctrl+A | Comprime la banda a una riga mentre un mod vi disegna, o la espande di nuovo. Questi tasti funzionano anche mentre il prompt ha il focus. |
| Esc | Restituisce il focus della tastiera al prompt. Con closeOnEscape: true, chiude anche il riquadro. |
Un mod non può associare Tab o i tasti freccia ad altro, quindi un gioco si comanda con w, a, s e d.
Impostare un tasto di scelta rapida e il focus iniziale
hotkey: per permettere all'utente di premere unButtoncon un solo tasto, assegnagli unhotkeydi una cifra o di una lettera minuscola, come inhotkey: 'a'autoFocus: per scegliere quale controllo ha il focus quando il riquadro si apre, aggiungiautoFocus: truea quel controllo. Nell'estensione VS Code,autoFocusnon fa nulla: il controllo viene disegnato come se l'avessi omesso, e non devi modificare nulla. La prop accetta solotrue, quindi omettila negli altri controlli.
Il modo in cui viene mostrato un tasto di scelta rapida dipende dal pulsante e dall'app:
| Pulsante | Nel terminale | Nell'app Desktop | Nell'estensione VS Code |
|---|---|---|---|
| Con parentesi quadre, il valore predefinito | [ Add one ], senza alcun tasto di scelta rapida mostrato |
L'etichetta con un piccolo tasto accanto | L'etichetta, e il tasto di scelta rapida non fa nulla |
Con plain: true |
1: One |
L'etichetta con un piccolo tasto accanto | L'etichetta, e il tasto di scelta rapida non fa nulla |
Nel terminale, indica il tasto nell'etichetta di un pulsante con parentesi quadre, oppure usa plain: true, in modo che l'utente veda cosa premere. Il riferimento degli elementi contiene le altre regole di Button: action, i tasti di scelta rapida numerici sulla banda e due pulsanti sullo stesso tasto di scelta rapida.
Quando l'utente comprime la banda
Nel terminale, mentre un mod disegna nella banda, l'utente può comprimere la banda a una riga per toglierla di mezzo:
- Fai clic su
[-]: mentre la banda è espansa, Claude Code disegna questo segno all'estremità destra della sua prima riga. Nel rendering a schermo intero, l'utente vi fa clic per comprimere la banda. - Premi Ctrl+X e poi Ctrl+A: funziona sia che il focus della tastiera sia sul prompt, sulla banda o su un riquadro. L'utente può associarvi altri tasti tramite l'azione
abovePrompt:toggle.
Una banda compressa nasconde tutto ciò che i mod vi disegnano e mostra invece una riga attenuata, che indica i tasti che la espandono secondo le associazioni dell'utente:
▸ plugin panel hidden · ctrl+x ctrl+a or click to show
Gli stessi tasti o, nel rendering a schermo intero, un clic sulla riga espandono di nuovo la banda. Fino ad allora, nulla preme un pulsante al suo interno: nessun tasto di scelta rapida, nessuna cifra digitata in un prompt vuoto e nessuna scorciatoia da tastiera per l'action di un pulsante. Se la banda ha il focus della tastiera quando l'utente la comprime, il focus torna al prompt, e Ctrl+X e poi Tab salta la banda finché è compressa.
La banda rimane compressa finché l'utente non la espande, anche dopo /clear, e torna espansa al successivo avvio di Claude Code. Il tuo mod non può espanderla né sapere che è compressa: l'hook ui.render continua a essere eseguito, e nulla in e.props lo indica. Non fare della banda l'unico posto in cui il tuo mod mostra qualcosa a cui l'utente deve rispondere.
Ricevere input digitato e disegnare una riga per ogni elemento
Molti riquadri sono un campo di testo con un elenco sotto. L'esempio in questa sezione è un riquadro di note: digiti una nota e premi Enter per aggiungerla, e ogni nota ha un pulsante x che la elimina. Con due note aggiunte, il terminale disegna il riquadro in questo modo:
╭────────────────────────────────────────────────────────✕─╮
│ Note: Type a note and press Enter ⏎ add │
│ x buy milk │
│ x call bob │
╰──────────────────────────────────────────────────────────╯
La ✕ sul bordo superiore è il segno proprio di Claude Code per chiudere il riquadro.
L'esempio usa queste tecniche:
- Ricevere input digitato: un
InputchiamaonSubmit(value)con il testo del campo quando l'utente preme Enter, eonInput(value)a ogni modifica - Disegnare un elenco: mappa i tuoi dati a una riga ciascuno, e assegna al pulsante di ogni riga la propria
key
Questo hook disegna il contenuto del riquadro:
// The list the pane draws
let notes = []
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Draw only in the pane opened with the id 'notes'
if (e.requestId !== 'notes') return next(e)
const { Box, Text, Button, Input } = $.ui.resolve(e)
const redraw = () => $.ui.invalidate('ui.render')
return Box({
flexDirection: 'column',
children: [
Input({
key: 'new-note',
label: 'Note',
placeholder: 'Type a note and press Enter',
// The text the field starts with
value: '',
submitLabel: 'add',
autoFocus: true,
// Runs when you press Enter in the field
onSubmit: async (value) => {
// Ignore an empty line
if (!value.trim()) return
notes = [...notes, value.trim()]
redraw()
await $.store.set('notes', notes)
},
}),
// One row for each note: a delete button, then the note's text
...notes.map((note, i) =>
Box({
flexDirection: 'row',
columnGap: 1,
children: [
Button({
// A key of its own, so each row's button can be told apart
key: 'delete-' + i,
label: 'x',
plain: true,
onPress: async () => {
notes = notes.filter((_, j) => j !== i)
redraw()
await $.store.set('notes', notes)
},
}),
Text({ children: [note] }),
],
}),
),
],
})
})
Per provare il riquadro:
- Aggiungere una nota: digita una riga e premi Enter. La riga appare come una nuova riga dell'elenco, e il campo si svuota.
- Eliminare una nota: premi Tab finché il pulsante
xdella nota non ha il focus, poi premi Enter. Laxè l'etichetta del pulsante e non un tasto di scelta rapida, quindi digitare la lettera non lo preme.
Ogni modifica segue lo stesso ciclo di rendering di hello-tabs: la callback modifica notes, chiama redraw e salva l'elenco in $.store. Se il tuo hook ridisegna il riquadro mentre l'utente sta digitando una nota, il campo mantiene il suo testo purché l'hook passi ogni volta lo stesso value. Per sostituire il suo testo con il tuo, passa un value diverso.
L'esempio salva le note ma non le carica. Per ripristinarle nella sessione successiva, leggile in un hook session.start, nello stesso modo in cui hello-tabs legge count.
Queste prop compongono la riga del campo, Note: Type a note and press Enter ⏎ add:
| Prop | Nell'esempio | Cos'è |
|---|---|---|
label |
Note |
Il testo prima del campo. Il terminale disegna : dopo di esso. |
placeholder |
Type a note and press Enter |
Testo attenuato che viene mostrato mentre il campo è vuoto |
value |
'' |
Il testo con cui inizia il campo. Qui è vuoto, quindi viene mostrato il placeholder. |
submitLabel |
add |
La parola dopo ⏎ che indica cosa fa Enter |
L'invio di un Input non avvia un turno a meno che la tua callback non chiami $.prompt.submit.
Ridisegnare un sito
Un disegno è un'istantanea: mostra ciò che il tuo hook ui.render ha restituito l'ultima volta che l'hook è stato eseguito. Per mostrare qualcosa di nuovo, l'hook deve essere eseguito di nuovo. Claude Code lo riesegue per alcune modifiche, e il tuo mod lo richiede per le altre.
Quando Claude Code ridisegna senza che venga richiesto
Claude Code esegue di nuovo il tuo hook ui.render quando cambiano le prop del sito o quando cambia la larghezza del terminale. Quando un Client nel sito fallisce e il tuo mod gestisce ui.fault, Claude Code esegue l'hook ancora una volta dopo che i tuoi hook ui.fault hanno restituito il risultato, così il tuo hook ui.render può escludere il Client. Non esegue l'hook in base a un timer, e non può sapere quando cambia una variabile nel tuo modulo.
Ridisegnare quando i tuoi dati cambiano
Per far ridisegnare i tuoi siti dopo che i tuoi dati sono cambiati, chiama $.ui.invalidate('ui.render'). Questo pannello conta le pressioni. La callback del pulsante modifica count, poi richiede un nuovo disegno:
let count = 0
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
if (e.requestId !== 'counter') return next(e)
const { Box, Text, Button } = $.ui.resolve(e)
return Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({
key: 'more',
label: 'Add one',
onPress: () => {
count += 1
// The data changed, so ask Claude Code to draw the pane again
$.ui.invalidate('ui.render')
},
}),
Text({ children: ['Count: ' + count] }),
],
})
})
Ogni pressione incrementa il numero nel pannello. L'esempio hello-tabs racchiude la stessa chiamata nella sua funzione redraw.
Un valore che conservi in $.state non ha bisogno della chiamata, perché scrivere il valore ridisegna i siti che lo leggono.
Ridisegnare in base a un timer
Per mantenere aggiornati un orologio, un conto alla rovescia o un valore esterno alla sessione, ridisegna secondo una pianificazione. Avvia un timer nell'hook session.start del modulo. Se il modulo ne ha già uno, come nel caso di hello-tabs, aggiungi a esso la riga $.clock.every:
on('session.start', async ($, e, next) => {
// Every 1000 milliseconds, ask Claude Code to draw your sites again
$.clock.every(1000, () => $.ui.invalidate('ui.render'))
return next(e)
})
Ora Claude Code esegue il tuo hook ui.render una volta al secondo. Il timer si arresta quando il modulo viene ricaricato, e la nuova istanza del modulo avvia il proprio.
Con quale frequenza un sito può essere ridisegnato
Claude Code limita la frequenza dei ridisegni di un sito, quindi il tuo mod può chiamare $.ui.invalidate tutte le volte che i suoi dati cambiano. Per sapere con quale frequenza ciascun sito può essere ridisegnato, consulta la tabella dei limiti.
Le chiamate che arrivano più velocemente del limite vengono accorpate in un unico ridisegno. Quel ridisegno esegue il tuo hook una sola volta, e l'hook legge i tuoi dati così come sono in quel momento, quindi viene mostrato il valore più recente e non quelli intermedi. Un'animazione non può essere eseguita più velocemente del limite.
Mantenere lo stato
Il punto in cui un mod conserva un valore determina quanto a lungo il valore dura: fino al ricaricamento del modulo, fino alla fine della sessione, oppure da una sessione all'altra. Scegli in base a quanto a lungo il valore deve durare:
| Conservalo in | Dura fino a quando | Usalo per |
|---|---|---|
| Una variabile a livello di modulo | Il modulo si ricarica, cosa che avviene ogni volta che salvi un file durante lo sviluppo | Valori che puoi perdere, come tab in hello-tabs |
$.state |
La sessione termina, oppure l'utente esegue /clear, /resume o /branch |
Valori da cui dipende un disegno e che devono sopravvivere a un ricaricamento |
$.store |
Il tuo mod lo elimina, oppure nessuna sessione legge o scrive lo store per cleanupPeriodDays. Lo store è un archivio chiave-valore, salvato come file JSON proprio del tuo plugin in ~/.claude/plugins/store/. |
Impostazioni, cronologia, tutto ciò che l'utente si aspetta di ritrovare la volta successiva |
$.store.get(key) si risolve nel valore o in undefined, e $.store.set(key, value) accetta qualsiasi valore JSON.
Conservare un valore in $.state
$.state conserva i valori per la durata di una sessione e ridisegna al posto tuo. È uno stato reattivo: un hook ui.render che legge un valore si sottoscrive a esso, quindi Claude Code ridisegna quel punto ogni volta che scrivi il valore, e non devi chiamare $.ui.invalidate. Un valore in $.state sopravvive inoltre a un ricaricamento del modulo, cosa che una variabile non fa.
Per configurarlo, dichiara i tuoi valori, fai puntare il manifest alla dichiarazione, poi definisci e usa ciascun valore. Gli esempi spostano il count di hello-tabs in $.state.
Dichiarare i valori
Dichiara i valori in un file di dichiarazione dei tipi. La chiave esterna è il nome del tuo plugin, e ogni voce sotto di essa è un valore con il suo tipo. Salva questo come hello-tabs/types/index.d.ts:
declare module 'claude-code' {
interface PluginState {
'hello-tabs': {
tab: 'one' | 'two'
count: number
}
}
}
Far puntare il manifest alla dichiarazione
Per consentire a claude plugin validate di verificare il tuo codice rispetto a quel file, aggiungi al manifest un campo types con il suo percorso:
{
"name": "hello-tabs",
"version": "0.1.0",
"description": "Opens a pane with two tabs and a counter",
"author": { "name": "Your Name" },
"types": "./types/index.d.ts"
}
Definire, leggere e scrivere un valore
Nel tuo modulo, definisci ciascun valore con un valore predefinito, leggilo durante il disegno e scrivilo da una callback. atom assegna un nome a un valore e al suo valore predefinito, read lo restituisce e update lo scrive. I tre helper chiamano $.state.get e $.state.set al posto tuo:
import { atom, read, update } from 'claude-code'
// At the top of the module: name the value and give its default
const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)
// In the ui.render hook: read the value to draw it
const n = await read($, count)
// In a Button: write a new value from the old one
onPress: () => update($, count, (value) => value + 1)
Poiché l'hook ui.render ha letto count, Claude Code esegue di nuovo l'hook ogni volta che il pulsante lo scrive.
Al codice si applicano queste regole:
- Scrivi
pluginekeycome stringhe letterali:claude plugin validateli legge dal tuo codice sorgente - Conserva il risultato di ogni chiamata ad
atomin unaconst: se dichiaricountconlet, la validazione fallisce contakes a source the scan can read - Dichiara ogni valore nel file di dichiarazione dei tipi: altrimenti la validazione fallisce con
hello-tabs.count is not declared - Scrivi da una callback o dall'hook di un altro evento: un hook
ui.renderpuò leggere lo stato ma non può scriverlo, quindi scrivi daonPress,onSubmito da un hook per un altro evento
Modificare hello-tabs per usare $.state
Per spostare count di hello-tabs in $.state, modifica ogni riga che lo usa:
- In cima al modulo: aggiungi la riga
importe sostituiscilet count = 0con la rigaatom - Nell'hook
ui.render: aggiungi la rigareadprima ditabButtone disegna'Count: ' + nnelText - Nel pulsante Add one: sostituisci
onPresscon quello in Salvare da più di una sessione, che salva il conteggio oltre a scriverlo - Nell'hook
session.start: sostituisci le due righe che leggonosavedcon la chiamata aloadCountda Caricare di nuovo un valore salvato dopo/clear
Mantieni redraw per i pulsanti delle schede, perché tab è ancora una variabile.
Caricare di nuovo un valore salvato dopo /clear
Se il tuo mod copia un valore salvato da $.store in $.state a session.start, deve copiarlo di nuovo dopo /clear, /resume o /branch. Questi comandi reimpostano ogni valore di $.state al suo valore predefinito, e session.start non viene attivato di nuovo. classic.SessionStart invece viene attivato dopo ciascuno di essi, con e.source impostato su clear, resume o fork, quindi copia di nuovo il valore in un hook su di esso. Altrimenti il tuo disegno mostra il valore predefinito, e una callback che salva il valore di $.state scrive il valore predefinito sopra quello che avevi memorizzato.
Questo codice carica count da entrambi gli hook. Si basa sulla versione di hello-tabs con $.state, in cui count è un atom e update è importato. Metti loadCount sopra register e aggiungi la chiamata a loadCount all'hook session.start che hai già. classic.SessionStart viene attivato anche all'avvio e dopo la compattazione, che non reimposta $.state, quindi il filtro su source limita l'hook ai tre reset:
// Copy the saved count from $.store into $.state, or 0 if nothing is saved
async function loadCount($) {
const saved = Number((await $.store.get('count')) ?? 0)
await update($, count, () => saved)
}
// Runs before your first prompt, and again after a reload
on('session.start', async ($, e, next) => {
await loadCount($)
return next(e)
})
// Runs again after /clear, /resume, and /branch, which reports fork
on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
await loadCount($)
return next(e)
})
Con entrambi gli hook in posizione, il riquadro mostra il conteggio salvato dopo /clear e non 0, e la successiva pressione di Add one si aggiunge al conteggio salvato.
loadCount scrive il valore memorizzato sopra quello in $.state, e session.start viene attivato di nuovo ogni volta che il modulo si ricarica. Per evitare che lo store rimanga indietro, salva a ogni modifica, come fa il pulsante Add one.
Per verificare il ricaricamento senza una sessione, testa il disegno dopo /clear.
Salvare da più di una sessione
Ogni sessione sulla tua macchina che esegue il tuo mod condivide un unico $.store. Un get seguito da un set non è atomico. Quando due sessioni leggono ciascuna un valore, lo modificano e lo riscrivono, entrano in competizione, e la seconda scrittura sostituisce la prima.
Per renderlo meno probabile:
- Assegna a ogni elemento la propria chiave: un
setmodifica solo la propria chiave, quindi le sessioni che scrivono chiavi diverse non si sovrascrivono a vicenda - Rileggi subito prima di scrivere: per un valore che più sessioni modificano, esegui
getsulla chiave nella callback e costruisci il nuovo valore a partire da quello, non da una copia caricata asession.start. La scrittura di un'altra sessione va comunque persa se avviene tra il tuogete il tuoset.
Questo pulsante aggiunge uno a qualunque valore lo store contenga in quel momento, poi aggiorna il disegno:
onPress: async () => {
// Read what the store holds now, which another session may have changed
const saved = Number((await $.store.get('count')) ?? 0)
// Save the new count, then show it
await $.store.set('count', saved + 1)
await update($, count, () => saved + 1)
}
Se una seconda sessione ha premuto il proprio pulsante tre volte dall'avvio di questa sessione, questa pressione mostra e salva un conteggio che include quelle tre.
Passaggi successivi
- Reagire agli eventi: alimenta il tuo disegno dalle chiamate agli strumenti e dai turni
- Usare l'API dei mod: alimenta il tuo disegno da timer e chiamate al modello
- Testare un disegno: premi i tuoi pulsanti da un test, su più di una superficie
- Punti di rendering ed elementi: le prop di ciascun punto e le prop di ciascun elemento