ui.render chaque fois qu’il s’apprête à dessiner un site de rendu, et votre hook pour cet événement retourne ce qu’il faut dessiner là.
Cette carte montre où un mod peut dessiner dans une session de terminal :
Pour rechercher une prop ou une limite, consultez la référence.
Construire un volet avec des onglets
Dans cette section, vous construisez un mod qui ajoute une commande/hello-tabs, et la commande ouvre un volet. Un volet est une barre latérale à côté de la transcription dans un terminal plein écran large, ou une région encadrée au-dessus de l’invite sinon. Ce volet affiche deux onglets, et le deuxième onglet a un bouton qui ajoute un au compteur. Le compte est toujours là après que vous redémarriez Claude Code.
Le mod fini ressemble à ceci. L’enregistrement ouvre le volet, bascule vers le deuxième onglet, appuie sur le bouton quelques fois, et revient au premier onglet :
1
Créer le plugin
Un mod est un plugin avec un manifeste, un Nommez votre point d’entrée dans
hooks.json qui pointe vers votre code, et le fichier de code. Créer un mod explique chacun. Créez un répertoire nommé hello-tabs avec des répertoires .claude-plugin et hooks à l’intérieur, puis enregistrez les deux premiers fichiers.Enregistrez le manifeste sous hello-tabs/.claude-plugin/plugin.json :hello-tabs/.claude-plugin/plugin.json
hello-tabs/hooks/hooks.json :hello-tabs/hooks/hooks.json
2
Écrire le code
Le code fait trois choses, une dans chaque hook :Chaque hook fait aussi quelque chose que le code ne rend pas évident :
- Ajoute la commande
/hello-tabs - Ouvre le volet quand vous exécutez cette commande
- Dessine le contenu du volet : la ligne d’onglets et le corps de l’onglet ouvert
tab et count, conservent l’état du volet.Enregistrez ceci sous hello-tabs/hooks/register.js :hello-tabs/hooks/register.js
session.startlit aussi le compte sauvegardé depuis$.store, un magasin clé-valeur qui persiste entre les sessions.command.rundit seulement à Claude Code que le volet existe. Ouvrir un volet ne dessine rien par lui-même : Claude Code déclenche ensuiteui.renderpour demander ce qu’il faut y mettre.ui.renderretourne l’arbre d’éléments, uneBoxqui contient d’autres boîtes, du texte et des boutons, et le construit à nouveau à partir detabetcountchaque fois qu’il s’exécute.
onPress, qui change une variable et appelle redraw. Claude Code exécute ensuite le hook ui.render à nouveau, et le hook construit un nouvel arbre à partir des nouvelles valeurs. Chaque dessin interactif utilise ce cycle de rendu : un callback change l’état, et le hook dessine à nouveau à partir du nouvel état.3
Ouvrir le volet
Dans votre shell, démarrez Claude Code avec
claude --plugin-dir ./hello-tabs. À l’invite Claude Code, exécutez /hello-tabs. Un volet s’ouvre avec 1: One et 2: Two en haut. Appuyez sur 2, puis appuyez sur a, la touche de raccourci pour Add one, quelques fois. Le compte augmente.4
Vérifier que le compte a été sauvegardé
Appuyez sur Esc pour fermer le volet, puis quittez la session. Dans votre shell, démarrez Claude Code à nouveau avec la même commande
claude --plugin-dir ./hello-tabs, et à l’invite Claude Code exécutez /hello-tabs. Le compte est où vous l’avez laissé.Pour effacer le compte, faites appeler au mod $.store.delete('count'). Conserver l’état couvre combien de temps chaque type de valeur dure.Choisir où dessiner
Un hookui.render s’exécute pour chaque site de rendu sauf si vous le réduisez à celui que vous voulez dessiner. Pour choisir le site de rendu, passez un filtre, appelé un matcher, comme deuxième argument à on. { component: 'Pane' } exécute le hook seulement pour les volets. Dans le hook, e.component nomme le site, e.surface dit quelle application dessine, et e.props contient les données propres du site. Pour un volet, e.requestId est l’id avec lequel vous l’avez ouvert.
Deux sites sont vides jusqu’à ce qu’un mod les remplisse, le volet et la bande. Sélectionnez un onglet pour voir ce que chacun est et comment dessiner dedans :
- Volet
- Bande au-dessus de l'invite
Un volet est une barre latérale à côté de la transcription dans un terminal plein écran large, ou une région encadrée au-dessus de l’invite sinon. Avec plusieurs volets ouverts, chacun obtient un onglet qui affiche son titre.Un volet apparaît quand votre mod appelle
$.ui.open avec un id que vous choisissez, comme dans $.ui.open({ id: 'hello-tabs' }). Ouvrir un volet au bon moment couvre les autres champs et quand un volet attend un terminal plus large.Pour dessiner dans votre volet, filtrez sur { component: 'Pane' } et vérifiez que e.requestId est votre id.Modifier ce que Claude Code dessine déjà
Claude Code dessine la plupart de son interface lui-même : les messages, les lignes d’appels d’outils, le spinner, et plus. Chacune de ces parties est aussi un site de rendu, donc un mod peut le restyler ou le remplacer. Pour en modifier un, filtrez votre hookui.render sur son nom de ce tableau :
À un site que Claude Code dessine déjà, votre hook a trois choix : modifier un détail, remplacer le dessin, ou le laisser tranquille. Sélectionnez un onglet pour voir chacun appliqué au spinner. Les exemples lisent une variable
calls qu’un autre hook compte, comme dans le mod tutoriel.
- Modifier un détail
- Remplacer le dessin
- Le laisser tranquille
Pour garder le dessin de Claude Code et modifier une partie de celui-ci, passez à Le spinner garde son animation et son mot, et votre texte suit le mot :
next une copie de l’événement avec des props modifiées. Ce hook change le texte après le mot du spinner :AskUserQuestion, en est un, donc un mod peut modifier cela.
Le terminal et l’application Desktop ne déclenchent pas tous les mêmes sites. Pane, AbovePrompt, Spinner, et les sites de transcription fonctionnent dans les deux. Quelques autres lignes d’état sont déclenchées seulement dans le terminal. Le tableau des sites de rendu liste où chacun est déclenché.
Ouvrir un volet au bon moment
Un volet n’apparaît que quand votre mod l’ouvre. Comment et quand vous l’ouvrez décide s’il prend le focus clavier, combien d’espace il demande, et s’il s’affiche du tout dans un terminal étroit. Pour ouvrir un volet, appelez$.ui.open avec un id que vous choisissez. L’id est le nom du volet : votre hook ui.render le vérifie, et vous le passez à nouveau pour fermer le volet.
$.ui.close avec l’id avec lequel vous l’avez ouvert :
id, $.ui.open prend ces champs optionnels :
Pour laisser une commande ouvrir le volet pendant que Claude travaille, ajoutez
immediate: true quand vous enregistrez la commande. Sans cela, une commande tapée pendant un tour attend la fin du tour.
Quand un volet attend un terminal plus large
Un volet que votre mod ouvre sans être demandé n’apparaît pas dans un terminal étroit, donc il ne peut pas prendre le contrôle d’un petit écran. S’il apparaît dépend de ce qui l’a ouvert :- Ouvert par quelque chose que l’utilisateur a fait, comme une commande qu’il a exécutée ou un bouton qu’il a appuyé, le volet apparaît à n’importe quelle largeur
- Ouvert par votre mod agissant par lui-même, comme à partir d’une minuterie ou d’un hook
turn.start, le volet n’apparaît que dans un terminal d’au moins 144 colonnes de large. Après que l’utilisateur ait ouvert ce volet une fois lui-même, 110 colonnes suffisent.
$.ui.open se résout en { isPlaced: true }. Quand le volet attend, isPlaced est false et reason est une chaîne qui dit pourquoi. Un volet en attente apparaît quand l’utilisateur l’ouvre ou élargit le terminal. Pour dire que quelque chose est disponible sans ouvrir un volet, appelez $.ui.toast('Your message'), qui affiche un petit avis qui disparaît après quelques secondes.
Construire un arbre à partir d’éléments
Ce qu’un hookui.render retourne est un arbre d’éléments : une description de ce qu’il faut dessiner, faite de boîtes, de texte et de contrôles imbriqués les uns dans les autres. Vous décrivez le dessin, et Claude Code le rend dans le terminal ou l’application Desktop.
Pour obtenir les éléments, appelez $.ui.resolve(e) dans votre hook, comme dans const { Box, Text, Button } = $.ui.resolve(e). Chaque élément est une fonction. Vous lui passez des props, et vous mettez les éléments et les chaînes qui vont à l’intérieur dans children.
La plupart des dessins utilisent quatre éléments. Sélectionnez un onglet pour voir chacun et comment le terminal le dessine :
- Texte
- Boîte
- Bouton
- Entrée
Text dessine une chaîne, avec un style optionnel comme bold et color :
Si votre module est un fichier
.tsx ou .jsx, vous pouvez écrire l’arbre en JSX. Déstructurez les éléments de $.ui.resolve(e) d’abord, car un module de hooks n’a pas de globals d’éléments.
Si un arbre utilise un élément que l’application n’a pas, une prop qu’un élément ne prend pas, ou un enfant où aucun ne va, Claude Code dessine sa propre version du site.
Dans une session démarrée avec --plugin-dir, une ligne de transcription le dit, comme ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own. Le journal de débogage l’enregistre comme ui.render (Pane): a hook returned a tree that does not validate avec la même raison. Rien d’autre n’apparaît dans la session, donc quand un dessin ne s’affiche pas, vérifiez cette ligne ou le journal.
Dessiner une grille de cellules colorées
Pour une carte thermique, une sparkline, ou un plateau de jeu dans le terminal, dessinez unRaster et non une Box pour chaque cellule. Un Raster prend une key, sa taille en columns et rows, et cells, qui empaquette chaque cellule dans une chaîne. Chaque cellule est trois nombres : le point de code du caractère, sa couleur et sa couleur de fond. Une couleur est un nombre hexadécimal avec deux chiffres chacun pour le rouge, le vert et le bleu, comme 0xc62828 pour un rouge, ou 0x01000000 pour la valeur par défaut du terminal.
L’application Desktop n’a pas de Raster, donc vérifiez e.surface et dessinez du texte là. Ce corps de volet dessine une carte thermique de trois par deux :
rows est la partie que vous changeriez, et cellsOf la transforme en chaîne empaquetée. Le hook dessine seulement dans un volet dont l’id est heat, donc ouvrez-en un avec $.ui.open({ id: 'heat' }) à partir d’une commande, comme l’exemple hello-tabs ouvre son volet.
Chaque caractère doit être large d’une cellule. Pour animer un Raster qui est déjà à l’écran, appelez $.ui.blit avec l’id du volet comme requestId, la key du Raster, la même taille, et de nouvelles cellules. Pour cet exemple, c’est $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). Il repeint cet élément sans exécuter votre hook ui.render à nouveau.
Répondre aux appuis et à la saisie
Quand l’utilisateur appuie sur un bouton, tape dans un champ, ou choisit dans une liste que votre mod a dessinée, Claude Code appelle la fonction que vous avez donnée à ce contrôle, et elle s’exécute dans votre module. Chaque contrôle prend ses propres callbacks :Button: prendonPress(e), oùe.surfaceest l’application d’où vient l’appuiInput: prendonSubmit(value)etonInput(value)Select: prendonSelect(value)avec ses choix dansoptions, une liste d’au moins un choix avec des valeurs uniques, comme[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
key, donc donnez-en un à chaque contrôle. Chaque utilisation d’un contrôle déclenche aussi ui.press, ui.input, ou ui.select avec la key dans e.element, et un autre mod peut accrocher ces événements. Son hook s’exécute avant votre callback, donc il voit ce que l’utilisateur tape dans votre Input et peut le modifier ou répondre à la place de votre callback. L’API des mods n’a pas de méthode qui appuie sur le bouton d’un autre mod.
Focus clavier et touches de raccourci
Votre mod ne lit jamais le clavier lui-même. L’utilisateur appuie sur une touche, Claude Code décide lequel de vos contrôles c’est, et le callback de ce contrôle s’exécute. À part une touche de raccourci numérique sur la bande, cela ne se produit que pendant que votre volet ou bande a le focus clavier. Le reste du temps, les touches vont à l’invite.Comment un volet obtient le focus clavier
Un volet obtient le focus clavier de l’une de trois façons :- Votre mod l’ouvre avec
focus: trueà partir d’une commande ou d’un appui - L’utilisateur appuie sur Ctrl+X puis Tab
- L’utilisateur clique dessus
focus: true seulement pendant que l’invite est vide et rien d’autre n’a le focus clavier. Un volet qui s’ouvre pendant que l’utilisateur tape ne prend pas ses frappes.
Ce que chaque touche fait
Ce tableau liste ce qu’une touche fait pendant que votre volet ou bande a le focus clavier :
Un mod ne peut pas lier Tab ou les touches fléchées à autre chose, donc un jeu se dirige avec
w, a, s, et d.
Définir une touche de raccourci et le premier focus
Deux props sur un contrôle décident comment le clavier l’atteint :hotkey: pour laisser l’utilisateur appuyer sur unButtonavec une touche, donnez-lui unehotkeyd’un chiffre ou une lettre minuscule, comme danshotkey: 'a'autoFocus: pour choisir quel contrôle a le focus quand le volet s’ouvre, ajoutezautoFocus: trueà celui-ci. Laissez la prop de côté sur les autres, car Claude Code refuseautoFocus: false.
Dans le terminal, nommez la touche dans l’étiquette d’un bouton entre crochets, ou utilisez
plain: true, pour que l’utilisateur puisse voir ce qu’il faut appuyer. La référence des éléments a les autres règles de Button : action, les touches de raccourci numériques sur la bande, et deux boutons sur une touche de raccourci.
Prendre l’entrée tapée et dessiner une ligne pour chaque élément
De nombreux volets sont un champ de texte avec une liste en dessous. L’exemple de cette section est un volet de notes : vous tapez une note et appuyez sur Entrée pour l’ajouter, et chaque note a un boutonx qui la supprime. Avec deux notes ajoutées, le terminal dessine le volet de cette façon :
- Prendre l’entrée tapée : un
InputappelleonSubmit(value)avec le texte du champ quand l’utilisateur appuie sur Entrée, etonInput(value)à chaque changement - Dessiner une liste : mappez vos données à une ligne chacune, et donnez à chaque bouton de ligne sa propre
key
- Ajouter une note : tapez une ligne et appuyez sur Entrée. La ligne apparaît comme une nouvelle ligne, et le champ se vide.
- Supprimer une note : appuyez sur Tab jusqu’à ce que le bouton
xde la note ait le focus, puis appuyez sur Entrée. Lexest l’étiquette du bouton et non une touche de raccourci, donc taper la lettre ne l’appuie pas.
hello-tabs : le callback change notes, appelle redraw, et enregistre la liste dans $.store.
Le champ se vide après chaque soumission à cause de sa prop value. value est le texte que le champ contient quand il est dessiné, et la saisie de l’utilisateur le remplace jusqu’à ce que votre hook dessine le champ à nouveau. L’exemple dessine toujours le champ avec ''.
L’exemple enregistre les notes et ne les charge pas. Pour les ramener dans la session suivante, lisez-les dans un hook session.start, de la même façon que hello-tabs lit count.
Trois props composent la ligne du champ, Note: Type a note and press Enter ⏎ add :
Soumettre un
Input ne démarre pas un tour sauf si votre callback appelle $.prompt.submit.
Redessiner un site
Un dessin est un instantané : il affiche ce que votre hookui.render a retourné la dernière fois que le hook s’est exécuté. Pour afficher quelque chose de nouveau, le hook doit s’exécuter à nouveau. Claude Code l’exécute à nouveau pour certains changements, et votre mod demande le reste.
Quand Claude Code redessine sans être demandé
Claude Code exécute votre hookui.render à nouveau quand les props du site changent ou la largeur du terminal change. Il n’exécute pas le hook sur une minuterie, et il ne peut pas dire quand une variable dans votre module change.
Redessiner quand vos données changent
Pour avoir vos sites dessinés à nouveau après que vos propres données changent, appelez$.ui.invalidate('ui.render'). Ce volet compte les appuis. Le callback du bouton change count, puis demande un redessin :
hello-tabs enveloppe le même appel dans sa fonction redraw.
Une valeur que vous gardez dans $.state n’a pas besoin de l’appel, car écrire la valeur redessine les sites qui la lisent.
Redessiner sur une minuterie
Pour garder une horloge, un compte à rebours, ou une valeur de l’extérieur de la session actuelle, redessinez selon un calendrier. Démarrez une minuterie dans le hooksession.start du module. Si le module en a déjà une, comme hello-tabs le fait, ajoutez la ligne $.clock.every à celle-ci :
ui.render une fois par seconde. La minuterie s’arrête quand le module se recharge, et la nouvelle copie du module démarre la sienne.
À quelle fréquence un site peut redessiner
Claude Code limite la fréquence à laquelle il redessine un site, donc votre mod peut appeler$.ui.invalidate aussi souvent que ses données changent. Le volet visible et la bande ont une limite plus élevée que les autres sites, et le tableau des limites contient les chiffres.
Les appels qui viennent plus vite que la limite sont combinés en un redessin. Ce redessin exécute votre hook une fois, et le hook lit vos données telles qu’elles sont à ce moment, donc la valeur la plus récente s’affiche et les valeurs entre les deux ne s’affichent pas. Une animation ne peut pas s’exécuter plus vite que la limite.
Conserver l’état
Un mod a trois endroits pour garder une valeur, et ils diffèrent dans la durée pendant laquelle la valeur dure : jusqu’à ce que le module se recharge, jusqu’à ce que la session se termine, ou d’une session à l’autre. Choisissez selon la durée pendant laquelle la valeur doit durer :$.store.get(key) se résout en la valeur ou undefined, et $.store.set(key, value) prend n’importe quelle valeur JSON.
Garder une valeur dans $.state
$.state contient des valeurs pour la durée d’une session, et il redessine pour vous. C’est un état réactif : un hook ui.render qui lit une valeur s’y abonne, donc Claude Code redessine ce site chaque fois que vous écrivez la valeur, et vous n’appelez pas $.ui.invalidate. Une valeur dans $.state survit aussi à un rechargement du module, ce qu’une variable ne fait pas.
Pour le configurer, déclarez vos valeurs, pointez votre manifeste vers la déclaration, puis définissez et utilisez chaque valeur. Les exemples déplacent le count de hello-tabs dans $.state.
Déclarer les valeurs
Déclarez les valeurs dans un fichier de types. La clé externe est le nom de votre plugin, et chaque entrée en dessous est une valeur et son type. Enregistrez ceci soushello-tabs/types/index.d.ts :
hello-tabs/types/index.d.ts
Pointer le manifeste vers la déclaration
Pour laisserclaude plugin validate vérifier votre code par rapport à ce fichier, ajoutez un champ types au manifeste avec son chemin :
hello-tabs/.claude-plugin/plugin.json
Définir, lire et écrire une valeur
Dans votre module, définissez chaque valeur avec une valeur par défaut, lisez-la pendant le dessin, et écrivez-la à partir d’un callback.atom nomme une valeur et sa valeur par défaut, read la retourne, et update l’écrit. Les trois aides appellent $.state.get et $.state.set pour vous :
ui.render a lu count, Claude Code exécute le hook à nouveau chaque fois que le bouton l’écrit.
Trois règles s’appliquent au code :
- Écrivez
pluginetkeycomme des chaînes littérales :claude plugin validateles lit de votre source - Déclarez chaque valeur dans le fichier de types : sinon la validation échoue avec
hello-tabs.count is not declared - Écrivez à partir d’un callback ou du hook d’un autre événement : un hook
ui.renderpeut lire l’état et ne peut pas l’écrire, donc écrivez à partir deonPress,onSubmit, ou un hook pour un autre événement
Modifier hello-tabs pour utiliser $.state
Pour déplacer count dans hello-tabs dans $.state, modifiez chaque ligne qui l’utilise :
- En haut du module : ajoutez la ligne
import, et remplacezlet count = 0par la ligneatom - Dans le hook
ui.render: ajoutez la lignereadavanttabButton, et dessinez'Count: ' + ndans leText - Dans le bouton Add one : remplacez
onPresspar celui dans Enregistrer à partir de plus d’une session, qui enregistre le compte ainsi que l’écrit - Dans le hook
session.start: remplacez les deux lignes qui lisentsavedpar l’appelloadCountde Charger une valeur sauvegardée à nouveau après/clear
redraw pour les boutons d’onglets, car tab est toujours une variable.
Charger une valeur sauvegardée à nouveau après /clear
Si votre mod copie une valeur sauvegardée de $.store dans $.state à session.start, il doit la copier à nouveau après /clear, /resume, ou /branch. Ces commandes remettent chaque valeur $.state à sa valeur par défaut, et session.start ne se déclenche pas à nouveau. classic.SessionStart se déclenche après chacune d’elles, avec e.source défini sur clear, resume, ou fork, donc copiez la valeur à nouveau dans un hook dessus. Sinon votre dessin affiche la valeur par défaut, et un callback qui enregistre la valeur $.state écrit la valeur par défaut sur ce que vous avez stocké.
Ce code charge count à partir des deux hooks. Il s’appuie sur la version $.state de hello-tabs, où count est un atome et update est importé. Mettez loadCount au-dessus de register, et ajoutez l’appel loadCount au hook session.start que vous avez déjà. classic.SessionStart se déclenche aussi au démarrage et après compaction, ce qui ne réinitialise pas $.state, donc le filtre sur source garde le hook aux trois réinitialisations :
/clear et non 0, et le prochain appui sur Add one ajoute au compte sauvegardé.
loadCount écrit la valeur stockée sur celle dans $.state, et session.start se déclenche à nouveau chaque fois que le module se recharge. Pour garder le magasin de prendre du retard, enregistrez à chaque changement, comme le bouton Add one le fait.
Pour vérifier le rechargement sans une session, testez le dessin après /clear.
Enregistrer à partir de plus d’une session
Chaque session sur votre machine qui exécute votre mod partage un$.store. Un get suivi d’un set n’est pas atomique. Quand deux sessions lisent chacune une valeur, la modifient et l’écrivent, elles font la course, et la deuxième écriture remplace la première.
Deux choix rendent cela moins probable :
- Donnez à chaque élément sa propre clé : un
setchange seulement sa propre clé, donc les sessions qui écrivent des clés différentes ne s’écrasent pas mutuellement - Lisez à nouveau juste avant d’écrire : pour une valeur que plusieurs sessions changent,
getla clé dans le callback et construisez la nouvelle valeur à partir de cela, pas à partir d’une copie que vous avez chargée àsession.start. L’écriture d’une autre session est toujours perdue si elle atterrit entre votregetet votreset.
Prochaines étapes
- Réagir aux événements : alimentez votre dessin à partir d’appels d’outils et de tours
- Utiliser l’API des mods : alimentez votre dessin à partir de minuteries et d’appels de modèle
- Tester un dessin : appuyez sur vos boutons à partir d’un test, sur plus d’une surface
- Sites de rendu et éléments : les props de chaque site et les props de chaque élément