ui.render ogni volta che sta per disegnare un sito di rendering, e il tuo hook per quell’evento restituisce cosa disegnare lì.
Questa mappa mostra dove un mod può disegnare in una sessione terminale:
Per cercare una proprietà o un limite, consulta il riferimento.
Costruire un riquadro con schede
In questa sezione costruisci un mod che aggiunge un comando/hello-tabs e il comando apre un riquadro. Un riquadro è una barra laterale accanto alla trascrizione in un terminale fullscreen ampio, o una regione incorniciata sopra il prompt altrimenti. Questo riquadro mostra due schede, e la seconda scheda ha un pulsante che aggiunge uno a un contatore. Il conteggio è ancora lì dopo aver riavviato Claude Code.
Il mod finito assomiglia a questo. La registrazione apre il riquadro, passa alla seconda scheda, preme il pulsante alcune volte e ritorna alla prima scheda:
1
Creare il plugin
Un mod è un plugin con un manifesto, un Nomina il tuo punto di ingresso in
hooks.json che punta al tuo codice, e il file di codice. Creare un mod spiega ognuno. Crea una directory denominata hello-tabs con directory .claude-plugin e hooks al suo interno, quindi salva i primi due file.Salva il manifesto come hello-tabs/.claude-plugin/plugin.json:hello-tabs/.claude-plugin/plugin.json
hello-tabs/hooks/hooks.json:hello-tabs/hooks/hooks.json
2
Scrivere il codice
Il codice fa tre lavori, uno in ogni hook:Ogni hook fa anche qualcosa che il codice non rende evidente:
- Aggiunge il comando
/hello-tabs - Apre il riquadro quando esegui quel comando
- Disegna il contenuto del riquadro: la riga di schede e il corpo della scheda aperta
tab e count, mantengono lo stato del riquadro.Salva questo come hello-tabs/hooks/register.js:hello-tabs/hooks/register.js
session.startlegge anche il conteggio salvato da$.store, un archivio chiave-valore che persiste tra le sessioni.command.rundice solo a Claude Code che il riquadro esiste. Aprire un riquadro non disegna nulla di per sé: Claude Code quindi generaui.renderper chiedere cosa metterci.ui.renderrestituisce l’albero degli elementi, unBoxche contiene altri box, testo e pulsanti, e lo costruisce di nuovo databecountogni volta che viene eseguito.
onPress, che cambia una variabile e chiama redraw. Claude Code quindi esegue di nuovo l’hook ui.render, e l’hook costruisce un nuovo albero dai nuovi valori. Ogni disegno interattivo utilizza quel ciclo di rendering: un callback cambia lo stato, e l’hook esegue il rendering di nuovo dal nuovo stato.3
Aprire il riquadro
Nella tua shell, avvia Claude Code con
claude --plugin-dir ./hello-tabs. Al prompt di Claude Code, esegui /hello-tabs. Un riquadro si apre con 1: One e 2: Two nella parte superiore. Premi 2, quindi premi a, la scorciatoia da tastiera per Add one, alcune volte. Il conteggio aumenta.4
Verificare che il conteggio sia stato salvato
Premi Esc per chiudere il riquadro, quindi esci dalla sessione. Nella tua shell, avvia di nuovo Claude Code con lo stesso comando
claude --plugin-dir ./hello-tabs, e al prompt di Claude Code esegui /hello-tabs. Il conteggio è dove l’hai lasciato.Per cancellare il conteggio, fai in modo che il mod chiami $.store.delete('count'). Mantenere lo stato spiega quanto tempo dura ogni tipo di valore.Scegliere dove disegnare
Un hookui.render viene eseguito per ogni sito di rendering a meno che non lo restringi a quello che desideri disegnare. Per scegliere il sito di rendering, passa un filtro, chiamato matcher, come secondo argomento a on. { component: 'Pane' } esegue l’hook solo per i riquadri. Nell’hook, e.component nomina il sito, e.surface dice quale app sta disegnando, e e.props contiene i dati propri del sito. Per un riquadro, e.requestId è l’id con cui l’hai aperto.
Due siti sono vuoti finché un mod non li riempie, il riquadro e la banda. Seleziona una scheda per vedere cosa è ognuno e come disegnare in esso:
- Pane
- Band above the prompt
Un riquadro è una barra laterale accanto alla trascrizione in un terminale fullscreen ampio, o una regione incorniciata sopra il prompt altrimenti. Con più riquadri aperti, ognuno ottiene una scheda che mostra il suo titolo.Un riquadro appare quando il tuo mod chiama
$.ui.open con un id che scegli, come in $.ui.open({ id: 'hello-tabs' }). Aprire un riquadro al momento giusto copre gli altri campi e quando un riquadro aspetta un terminale più ampio.Per disegnare nel tuo riquadro, filtra su { component: 'Pane' } e verifica che e.requestId sia il tuo id.Modificare quello che Claude Code disegna già
Claude Code disegna la maggior parte della sua interfaccia da solo: messaggi, righe di chiamate di strumenti, lo spinner, e altro. Ognuna di quelle parti è un sito di rendering anche, quindi un mod può ridisegnarlo o sostituirlo. Per modificarne uno, filtra il tuo hookui.render sul suo nome da questa tabella:
In un sito che Claude Code disegna già, il tuo hook ha tre scelte: modificare un dettaglio, sostituire il disegno, o lasciarlo in pace. Seleziona una scheda per vedere ognuno applicato allo spinner. Gli esempi leggono una variabile
calls che un altro hook conta, come nel mod del tutorial.
- Change a detail
- Replace the drawing
- Leave it alone
Per mantenere il disegno di Claude Code e modificare una parte di esso, passa a Lo spinner mantiene la sua animazione e la sua parola, e il tuo testo segue la parola:
next una copia dell’evento con props modificati. Questo hook cambia il testo dopo la parola dello spinner:AskUserQuestion, è uno, quindi un mod può modificare quello.
Il terminale e l’app Desktop non generano tutti gli stessi siti. Pane, AbovePrompt, Spinner, e i siti della trascrizione funzionano in entrambi. Poche altre righe di stato vengono generate solo nel terminale. La tabella dei siti di rendering elenca dove viene generato ognuno.
Aprire un riquadro al momento giusto
Un riquadro appare solo quando il tuo mod lo apre. Come e quando lo apri decide se prende il focus della tastiera, quanto spazio chiede, e se appare affatto in un terminale stretto. Per aprire un riquadro, chiama$.ui.open con un id che scegli. L’id è il nome del riquadro: il tuo hook ui.render lo controlla, e lo passi di nuovo per chiudere il riquadro.
$.ui.close con l’id con cui l’hai aperto:
id, $.ui.open accetta questi campi opzionali:
Per lasciare che un comando apra il riquadro mentre Claude sta lavorando, aggiungi
immediate: true quando registri il comando. Senza di esso, un comando digitato durante un turno aspetta che il turno finisca.
Quando un riquadro aspetta un terminale più ampio
Un riquadro che il tuo mod apre senza essere chiesto non appare in un terminale stretto, quindi non può prendere il controllo di uno schermo piccolo. Se appare dipende da cosa l’ha aperto:- Aperto da qualcosa che l’utente ha fatto, come un comando che ha eseguito o un pulsante che ha premuto, il riquadro appare a qualsiasi larghezza
- Aperto dal tuo mod che agisce da solo, come da un timer o un hook
turn.start, il riquadro appare solo in un terminale di almeno 144 colonne di larghezza. Dopo che l’utente ha aperto quel riquadro una volta da solo, 110 colonne sono sufficienti.
$.ui.open si risolve in { isPlaced: true }. Quando il riquadro è in attesa, isPlaced è false e reason è una stringa che dice perché. Un riquadro in attesa appare quando l’utente lo apre o allarga il terminale. Per dire che qualcosa è disponibile senza aprire un riquadro, chiama $.ui.toast('Your message'), che mostra un piccolo avviso che scompare dopo pochi secondi.
Costruire un albero da elementi
Quello che un hookui.render restituisce è un albero di elementi: una descrizione di cosa disegnare, fatta di box, testo e controlli annidati l’uno dentro l’altro. Descrivi il disegno, e Claude Code lo renderizza nel terminale o nell’app Desktop.
Per ottenere gli elementi, chiama $.ui.resolve(e) nel tuo hook, come in const { Box, Text, Button } = $.ui.resolve(e). Ogni elemento è una funzione. Passi le proprietà, e metti gli elementi e le stringhe che vanno dentro di esso in children.
La maggior parte dei disegni usa quattro elementi. Seleziona una scheda per vedere ognuno e come il terminale lo disegna:
- Text
- Box
- Input
Text disegna una stringa, con stile opzionale come bold e color:
Se il tuo modulo è un file
.tsx o .jsx, puoi scrivere l’albero come JSX. Destruttura gli elementi da $.ui.resolve(e) per primo, perché un modulo di hook non ha globali di elementi.
Se un albero usa un elemento che l’app non ha, una proprietà che un elemento non accetta, o un figlio dove nessuno va, Claude Code disegna la sua versione del sito.
In una sessione avviata con --plugin-dir, una riga di trascrizione lo dice, come ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own. Il log di debug lo registra come ui.render (Pane): a hook returned a tree that does not validate con la stessa ragione. Nient’altro appare nella sessione, quindi quando un disegno non si mostra, controlla quella riga o il log.
Disegnare una griglia di celle colorate
Per una mappa di calore, una sparkline, o una tavola di gioco nel terminale, disegna unRaster e non un Box per ogni cella. Un Raster accetta una key, la sua dimensione in columns e rows, e cells, che compatta ogni cella in una stringa. Ogni cella è tre numeri: il punto di codice del carattere, il suo colore, e il colore di sfondo. Un colore è un numero esadecimale con due cifre ciascuno per rosso, verde e blu, come 0xc62828 per un rosso, o 0x01000000 per il default del terminale.
L’app Desktop non ha Raster, quindi controlla e.surface e disegna testo lì. Questo corpo del riquadro disegna una mappa di calore tre per due:
rows è la parte che cambieresti, e cellsOf lo trasforma nella stringa compatta. L’hook disegna solo in un riquadro il cui id è heat, quindi aprine uno con $.ui.open({ id: 'heat' }) da un comando, come l’esempio hello-tabs apre il suo riquadro.
Ogni carattere deve essere largo una cella. Per animare un Raster che è già sullo schermo, chiama $.ui.blit con l’id del riquadro come requestId, la key del Raster, la stessa dimensione, e celle nuove. Per questo esempio, è $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). Ridipinge solo quell’elemento senza eseguire di nuovo il tuo hook ui.render.
Rispondere a pressioni e digitazione
Quando l’utente preme un pulsante, digita in un campo, o sceglie da un elenco che il tuo mod ha disegnato, Claude Code chiama la funzione che hai dato a quel controllo, e viene eseguita nel tuo modulo. Ogni controllo accetta i suoi callback:Button: accettaonPress(e), dovee.surfaceè l’app da cui viene la pressioneInput: accettaonSubmit(value)eonInput(value)Select: accettaonSelect(value)con le sue scelte inoptions, un elenco di almeno una scelta con valori unici, come[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
key, quindi dagli uno. Ogni uso di un controllo genera anche ui.press, ui.input, o ui.select con la key in e.element, e un altro mod può agganciare quegli eventi. Il suo hook viene eseguito prima del tuo callback, quindi vede quello che l’utente digita nel tuo Input e può cambiarlo o rispondere al posto del tuo callback. L’API dei mod non ha un metodo che preme il pulsante di un altro mod.
Focus della tastiera e scorciatoie da tastiera
Il tuo mod non legge mai la tastiera da solo. L’utente preme un tasto, Claude Code decide quale dei tuoi controlli è per esso, e il callback di quel controllo viene eseguito. A parte una scorciatoia da tastiera con cifra sulla banda, questo accade solo mentre il tuo riquadro o banda ha il focus della tastiera. Il resto del tempo, i tasti vanno al prompt.Come un riquadro ottiene il focus della tastiera
Un riquadro ottiene il focus della tastiera in uno di tre modi:- Il tuo mod lo apre con
focus: trueda un comando o una pressione - L’utente preme Ctrl+X poi Tab
- L’utente lo fa clic
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 prende i suoi tasti.
Cosa fa ogni tasto
Questa tabella elenca cosa fa un tasto mentre il tuo riquadro o banda ha il focus della tastiera:
Un mod non può associare Tab o i tasti freccia a nient’altro, quindi un gioco si muove con
w, a, s, e d.
Impostare una scorciatoia da tastiera e il primo focus
Due proprietà su un controllo decidono come la tastiera lo raggiunge:hotkey: per lasciare che l’utente preme unButtoncon un tasto, dagli unahotkeydi una cifra o una lettera minuscola, come inhotkey: 'a'autoFocus: per scegliere quale controllo ha il focus quando il riquadro si apre, aggiungiautoFocus: truead esso. Lascia la proprietà fuori dagli altri, perché Claude Code rifiutaautoFocus: false.
Nel terminale, nomina il tasto nell’etichetta di un pulsante tra parentesi, o usa
plain: true, così l’utente può vedere cosa premere. Il riferimento degli elementi ha le altre regole di Button: action, scorciatoie da tastiera con cifra sulla banda, e due pulsanti su una scorciatoia da tastiera.
Prendere 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 Invio per aggiungerla, e ogni nota ha un pulsantex che la elimina. Con due note aggiunte, il terminale disegna il riquadro in questo modo:
- Prendere input digitato: un
InputchiamaonSubmit(value)con il testo del campo quando l’utente preme Invio, eonInput(value)ad ogni cambio - Disegnare un elenco: mappa i tuoi dati a una riga ciascuno, e dai a ogni pulsante della riga la sua
key
- Aggiungi una nota: digita una riga e premi Invio. La riga appare come una nuova riga, e il campo si svuota.
- Elimina una nota: premi Tab finché il pulsante
xdella nota non ha il focus, quindi premi Invio. Laxè l’etichetta del pulsante e non una scorciatoia da tastiera, quindi digitare la lettera non lo preme.
hello-tabs: il callback cambia notes, chiama redraw, e salva l’elenco in $.store.
Il campo si svuota dopo ogni invio a causa della sua proprietà value. value è il testo che il campo contiene quando viene disegnato, e la digitazione dell’utente lo sostituisce finché il tuo hook non disegna il campo di nuovo. L’esempio disegna sempre il campo con ''.
L’esempio salva le note e non le carica. Per riportarle nella sessione successiva, leggile in un hook session.start, come hello-tabs legge count.
Tre proprietà compongono la riga del campo, Note: Type a note and press Enter ⏎ add:
Inviare un
Input non avvia un turno a meno che il tuo callback non chiami $.prompt.submit.
Ridisegnare un sito
Un disegno è un’istantanea: mostra quello che il tuo hookui.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 esegue di nuovo per alcuni cambiamenti, e il tuo mod chiede il resto.
Quando Claude Code ridisegna senza essere chiesto
Claude Code esegue di nuovo il tuo hookui.render quando le proprietà del sito cambiano o la larghezza del terminale cambia. Non esegue l’hook su un timer, e non può dire quando una variabile nel tuo modulo cambia.
Ridisegnare quando i tuoi dati cambiano
Per avere i tuoi siti disegnati di nuovo dopo che i tuoi dati cambiano, chiama$.ui.invalidate('ui.render'). Questo riquadro conta le pressioni. Il callback del pulsante cambia count, quindi chiede un ridisegno:
hello-tabs avvolge la stessa chiamata nella sua funzione redraw.
Un valore che mantieni in $.state non ha bisogno della chiamata, perché scrivere il valore ridisegna i siti che lo leggono.
Ridisegnare su un timer
Per mantenere un orologio, un conto alla rovescia, o un valore da fuori la sessione attuale, ridisegna su un programma. Avvia un timer nell’hooksession.start del modulo. Se il modulo ne ha già uno, come hello-tabs, aggiungi la riga $.clock.every ad esso:
ui.render una volta al secondo. Il timer si ferma quando il modulo si ricarica, e la nuova copia del modulo avvia il suo.
Quanto spesso un sito può ridisegnare
Claude Code limita quanto spesso ridisegna un sito, quindi il tuo mod può chiamare$.ui.invalidate quanto spesso i suoi dati cambiano. Il riquadro visibile e la banda hanno un limite più alto rispetto ad altri siti, e la tabella dei limiti contiene i numeri.
Le chiamate che arrivano più velocemente del limite vengono combinate in un ridisegno. Quel ridisegno esegue il tuo hook una volta, e l’hook legge i tuoi dati come sono in quel momento, quindi il valore più recente si mostra e i valori in mezzo no. Un’animazione non può essere eseguita più velocemente del limite.
Mantenere lo stato
Un mod ha tre posti per mantenere un valore, e differiscono in quanto tempo il valore dura: finché il modulo non si ricarica, finché la sessione non finisce, o da una sessione all’altra. Scegli in base a quanto tempo il valore deve durare:$.store.get(key) si risolve nel valore o undefined, e $.store.set(key, value) accetta qualsiasi valore JSON.
Mantenere un valore in $.state
$.state contiene valori per la durata di una sessione, e ridisegna per te. È uno stato reattivo: un hook ui.render che legge un valore si iscrive ad esso, quindi Claude Code ridisegna quel sito ogni volta che scrivi il valore, e non chiami $.ui.invalidate. Un valore in $.state sopravvive anche a un ricaricamento del modulo, che una variabile non fa.
Per configurarlo, dichiara i tuoi valori, punta il tuo manifesto alla dichiarazione, quindi definisci e usa ogni valore. Gli esempi spostano il count da hello-tabs in $.state.
Dichiarare i valori
Dichiara i valori in un file di tipi. La chiave esterna è il nome del tuo plugin, e ogni voce sotto di esso è un valore e il suo tipo. Salva questo comehello-tabs/types/index.d.ts:
hello-tabs/types/index.d.ts
Puntare il manifesto alla dichiarazione
Per lasciare checlaude plugin validate controlli il tuo codice contro quel file, aggiungi un campo types al manifesto con il suo percorso:
hello-tabs/.claude-plugin/plugin.json
Definire, leggere e scrivere un valore
Nel tuo modulo, definisci ogni valore con un default, leggilo mentre disegni, e scrivilo da un callback.atom nomina un valore e il suo default, read lo restituisce, e update lo scrive. I tre helper chiamano $.state.get e $.state.set per te:
ui.render ha letto count, Claude Code esegue l’hook di nuovo ogni volta che il pulsante lo scrive.
Tre regole si applicano al codice:
- Scrivi
pluginekeycome stringhe letterali:claude plugin validatele legge dal tuo sorgente - Dichiara ogni valore nel file di tipi: altrimenti la validazione fallisce con
hello-tabs.count is not declared - Scrivi da un callback o dall’hook di un altro evento: un hook
ui.renderpuò leggere lo stato e non può scriverlo, quindi scrivi daonPress,onSubmit, o un hook per un altro evento
Cambiare hello-tabs per usare $.state
Per spostare count in hello-tabs in $.state, cambia ogni riga che lo usa:
- In cima al modulo: aggiungi la riga
import, e sostituiscilet count = 0con la rigaatom - Nell’hook
ui.render: aggiungi la rigareadprima ditabButton, e disegna'Count: ' + nnelText - Nel pulsante Add one: sostituisci
onPresscon quello in Salvare da più di una sessione, che salva il conteggio così come lo scrive - Nell’hook
session.start: sostituisci le due righe che leggonosavedcon la chiamataloadCountda Caricare un valore salvato di nuovo dopo/clear
redraw per i pulsanti delle schede, perché tab è ancora una variabile.
Caricare un valore salvato di nuovo 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 rimettono ogni valore $.state al suo default, e session.start non viene generato di nuovo. classic.SessionStart viene generato dopo ognuno di essi, con e.source impostato a clear, resume, o fork, quindi copia il valore di nuovo in un hook su di esso. Altrimenti il tuo disegno mostra il default, e un callback che salva il valore $.state scrive il default su quello che hai archiviato.
Questo codice carica count da entrambi gli hook. Si basa sulla versione $.state di hello-tabs, dove count è un atom e update è importato. Metti loadCount sopra register, e aggiungi la chiamata loadCount all’hook session.start che hai già. classic.SessionStart viene generato anche all’avvio e dopo la compattazione, che non ripristina $.state, quindi il filtro su source mantiene l’hook ai tre ripristini:
/clear e non 0, e la prossima pressione di Add one aggiunge al conteggio salvato.
loadCount scrive il valore archiviato su quello in $.state, e session.start viene generato di nuovo ogni volta che il modulo si ricarica. Per mantenere lo store dal rimanere indietro, salva ad ogni cambio, come il pulsante Add one fa.
Per controllare 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 uno$.store. Un get seguito da un set non è atomico. Quando due sessioni leggono ciascuna un valore, lo cambiano, e lo scrivono di nuovo, corrono, e la seconda scrittura sostituisce la prima.
Due scelte lo rendono meno probabile:
- Dai a ogni elemento la sua chiave: un
setcambia solo la sua chiave, quindi le sessioni che scrivono chiavi diverse non si sovrascrivono a vicenda - Leggi di nuovo subito prima di scrivere: per un valore che più sessioni cambiano,
getla chiave nel callback e costruisci il nuovo valore da quello, non da una copia che hai caricato asession.start. Un’altra scrittura della sessione è ancora persa se atterra tra il tuogete il tuoset.
Prossimi passi
- Reagire agli eventi: alimenta il tuo disegno da chiamate di strumenti e turni
- Usare l’API dei mod: alimenta il tuo disegno da timer e chiamate di modello
- Testare un disegno: premi i tuoi pulsanti da un test, su più di una superficie
- Siti di rendering e elementi: le proprietà di ogni sito e le proprietà di ogni elemento