Dessiner dans l'interface avec un mod
Dessinez des volets, une bande au-dessus de l'invite, des boutons et des champs de texte à partir d'un mod Claude Code, gérez les appuis et les entrées, et conservez l'état entre les redessinages et les sessions.
Un mod peut dessiner sa propre interface dans Claude Code et modifier des parties de l'interface que Claude Code dessine déjà. Chaque endroit où un mod peut dessiner s'appelle un site de rendu, comme un volet, la bande au-dessus de l'invite, ou le spinner. Claude Code déclenche l'événement 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 :
Dans un terminal plus étroit, le volet se trouve au-dessus de l'invite au lieu d'être à côté de la transcription.
Créez votre premier mod avant de commencer ici. Commencez par l'exemple travaillé, qui crée un volet avec deux onglets et un compteur, puis lisez la section pour chaque élément que vous voulez modifier.
[!NOTE]
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 :
Les onglets sont deux boutons dans une ligne. Le mod garde la trace de celui qui est actif et dessine le contenu de cet onglet sous la ligne.
1. Créer le plugin
Un mod est un plugin avec un manifeste, un 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 :
{
"name": "hello-tabs",
"version": "0.1.0",
"description": "Opens a pane with two tabs and a counter",
"author": { "name": "Your Name" }
}
Nommez votre point d'entrée dans hello-tabs/hooks/hooks.json :
{
"modules": ["./register.js"]
}
2. Écrire le code
Cette liste indique ce que fait chaque hook, dans l'ordre où ils apparaissent dans le code :
- Ajoute la commande
/hello-tabset charge le compte qu'une session précédente a sauvegardé - 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
Deux variables au niveau du module, tab et count, conservent l'état du volet.
Enregistrez ceci sous 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,
],
})
})
}
Chaque hook fait aussi quelque chose que le code ne rend pas évident :
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.
Appuyer sur un bouton exécute son callback 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 hook ui.render s'exécute pour chaque point de rendu, sauf si vous le limitez à celui dans lequel vous souhaitez dessiner. Pour choisir le point de rendu, passez un filtre, appelé matcher, comme second argument de on. { component: 'Pane' } n'exécute le hook que pour les panneaux. Dans le hook, e.component nomme le point de rendu, e.surface indique quelle application dessine, et e.props contient les données propres au point de rendu. Pour un panneau, e.requestId est l'id avec lequel vous l'avez ouvert.
Le panneau et la bande restent vides tant qu'un mod ne les remplit pas. Sélectionnez un onglet pour découvrir ce qu'est chacun d'eux et comment y dessiner :
Panneau
Un panneau est une barre latérale à côté de la transcription dans un terminal large en plein écran, ou sinon une zone encadrée au-dessus du prompt. Lorsque plusieurs panneaux sont ouverts, chacun dispose d'un onglet qui affiche son titre.
Un panneau apparaît lorsque votre mod appelle $.ui.open avec un id de votre choix, comme dans $.ui.open({ id: 'hello-tabs' }). Ouvrir un panneau au bon moment présente les autres champs et explique quand un panneau attend un terminal plus large.
Pour dessiner dans votre panneau, filtrez sur { component: 'Pane' } et vérifiez que e.requestId correspond à votre id.
Bande au-dessus du prompt
La bande est une bande située juste au-dessus de la zone de saisie du prompt. Elle est toujours présente, et tous les mods la partagent.
Votre hook renvoie un arbre pour afficher quelque chose dans la bande, ou next(e) pour ne rien afficher. Un arbre remplace ce que les mods exécutés après le vôtre y dessinent. Pour conserver leur contenu, placez le résultat de await next(e) parmi les enfants d'une Box de votre arbre.
Pour dessiner dans la bande, filtrez sur { component: 'AbovePrompt' }.
Modifier ce que Claude Code dessine déjà
Claude Code dessine lui-même la plupart de son interface : les messages, les lignes d'appels d'outils, le spinner, et plus encore. Chacune de ces parties est aussi un point de rendu, qu'un mod peut donc restyler ou remplacer. Pour en modifier une, filtrez votre hook ui.render sur son nom tiré de ce tableau :
| Point de rendu | Description |
|---|---|
UserMessage, AssistantMessage |
Un message dans la transcription |
ToolUse, ToolResult, ToolGroup |
La ligne d'un appel d'outil, son résultat, et un groupe d'appels réduit |
CommandOutput |
La ligne qu'une commande a affichée |
AskUserQuestion |
La boîte de dialogue que Claude ouvre pour vous poser une question |
Spinner, ToolProgress, TurnDuration |
Les lignes d'état d'un tour : la ligne animée pendant que Claude travaille, la ligne de progression en direct d'un outil en cours d'exécution, et la ligne qui clôt un tour |
InfoNotice, SessionMode, PromptHint |
Les lignes d'état sous le logo, les libellés de mode dans le pied de page, et la ligne d'indication sous le prompt |
Pane, AskUserQuestion, Spinner, UserMessage, AssistantMessage et les points de rendu des appels d'outils fonctionnent dans le terminal, l'application Desktop et l'extension VS Code. Le tableau des points de rendu indique où chacun des autres est déclenché.
Sur un point de rendu que Claude Code dessine déjà, votre hook peut modifier un détail, remplacer le dessin ou le laisser tel quel. Sélectionnez un onglet pour voir chaque cas appliqué au spinner. Les exemples lisent une variable calls qu'un autre hook incrémente, comme dans le mod du tutoriel.
Modifier un détail
Pour conserver le dessin de Claude Code et en modifier une partie, passez à next une copie de l'événement avec des props modifiées. Ce hook modifie le texte qui suit le mot du 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 + '…' } })
})
Le spinner conserve son animation et son mot, et votre texte suit le mot :
Thinking · tool calls: 2…
Remplacer le dessin
Pour dessiner votre propre contenu à la place du point de rendu, renvoyez un arbre et n'appelez pas next. Ce hook dessine une ligne de texte à l'emplacement du 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'] })
})
Pendant que Claude travaille, votre ligne s'affiche et le spinner de Claude Code n'apparaît pas :
Claude has made 2 tool calls
Le laisser tel quel
Pour laisser le point de rendu tel que Claude Code le dessine, renvoyez next(e). Un hook le fait souvent pour certains événements et pas pour d'autres. Ce hook laisse le spinner tel quel tant qu'il n'y a aucun appel à compter :
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 + '…' } })
})
Avant le premier appel d'outil, le spinner a la même apparence que sans le mod :
Thinking…
Sur ces points de rendu, next(e) renvoie une référence au dessin de Claude Code, { type: 'engine', ref }, sauf si un mod exécuté après le vôtre a renvoyé son propre arbre. Pour modifier le contenu de ce dessin, passez à next une copie de l'événement avec des props différentes, comme le fait l'onglet Modifier un détail. Vous pouvez renvoyer la référence telle quelle, ou la placer dans une Box à côté de vos propres éléments :
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'] })] })
})
Pendant que Claude travaille, le spinner s'anime comme avant, et under the spinner apparaît en dessous.
La demande de permission n'est pas un point de rendu, un mod ne peut donc pas modifier ce qu'elle affiche. La boîte de dialogue de question, AskUserQuestion, en est un, un mod peut donc la modifier. Un arbre pour cette boîte de dialogue doit contenir la référence exactement une fois, avec vos éléments au-dessus. Sinon, Claude Code dessine sa propre boîte de dialogue.
Ouvrir un panneau au bon moment
Un panneau n'apparaît que lorsque votre mod l'ouvre. La manière et le moment où vous l'ouvrez déterminent s'il prend le focus clavier, l'espace qu'il demande, et s'il s'affiche ou non dans un terminal étroit.
Pour ouvrir un panneau, appelez $.ui.open avec un id de votre choix. L'id est le nom du panneau : votre hook ui.render le vérifie, et vous le repassez pour fermer le panneau.
await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
Pour fermer le panneau, appelez $.ui.close avec l'id avec lequel vous l'avez ouvert :
await $.ui.close({ id: 'hello-tabs' })
Outre id, $.ui.open accepte ces champs facultatifs :
| Champ | Rôle |
|---|---|
title |
Le libellé de l'onglet du panneau lorsque plusieurs panneaux sont ouverts |
focus |
Demande le focus clavier |
closeOnEscape |
Permet à Esc de fermer le panneau |
holdToasts |
Dans le terminal, retient les toasts tant que ce panneau est celui qui est affiché. Consultez Retenir les toasts derrière une boîte de dialogue. |
rows |
La hauteur à demander lorsque le panneau se trouve au-dessus du prompt. La valeur par défaut est un tiers de l'espace. |
columns |
La largeur à demander lorsque le panneau se trouve à côté de la transcription, au maximum 45 % de la largeur du terminal. Une fois que l'utilisateur a redimensionné un panneau à côté de la transcription, dans cette session ou une précédente, les panneaux s'y ouvrent plutôt à la largeur qu'il a définie. La même limite de 45 % s'applique à cette largeur. |
L'extension VS Code ignore rows et columns et dimensionne elle-même le panneau.
focus, closeOnEscape et holdToasts sont facultatifs et n'acceptent que true. Pour ne pas en utiliser un, omettez-le. Passer false lève une erreur telle que ui.open: focus is true or left out. Pour en définir un de manière conditionnelle, n'ajoutez le champ que lorsque la condition est remplie. Cet appel ne demande le focus clavier que lorsque items n'est pas vide :
const pane = { id: 'hello-tabs', title: 'Hello tabs' }
await $.ui.open(items.length > 0 ? { ...pane, focus: true } : pane)
Pour permettre à une commande d'ouvrir le panneau pendant que Claude travaille, ajoutez immediate: true lorsque vous enregistrez la commande. Sans cela, une commande saisie pendant un tour attend la fin du tour.
Retenir les toasts derrière une boîte de dialogue
Passez holdToasts: true à $.ui.open lorsque le panneau est une boîte de dialogue à laquelle l'utilisateur répond avant de la quitter, afin que les toasts n'apparaissent pas pendant qu'il décide. Dans le terminal, la retenue dure tant que ce panneau est celui qui est affiché, et un toast déclenché pendant ce temps attend la fin de la retenue.
Claude Code retient les toasts des autres mods et ses propres notifications éphémères, ainsi que ceux que votre mod déclenche avec $.ui.toast. N'utilisez pas ce champ pour un panneau qui reste ouvert, afin que l'utilisateur continue de les voir.
Quand un panneau attend un terminal plus large
Un panneau que votre mod ouvre sans qu'on le lui demande n'apparaît pas dans un terminal étroit, afin qu'il ne puisse pas accaparer un petit écran. Son affichage dépend de ce qui l'a ouvert :
- Ouvert par une action de l'utilisateur, comme une commande qu'il a exécutée ou un bouton sur lequel il a appuyé, le panneau apparaît quelle que soit la largeur
- Ouvert par votre mod de sa propre initiative, par exemple depuis un minuteur ou un hook
turn.start, le panneau n'apparaît que dans un terminal d'au moins 144 colonnes de large. Une fois que l'utilisateur a lui-même ouvert ce panneau, 110 colonnes suffisent.
Lorsque le panneau apparaît, $.ui.open se résout en { isPlaced: true }. Lorsque le panneau est en attente, isPlaced vaut false et reason est une chaîne qui en indique la raison. Un panneau en attente apparaît lorsque l'utilisateur l'ouvre ou élargit le terminal. Pour signaler qu'un élément est disponible sans ouvrir de panneau, appelez $.ui.toast('Your message'), qui affiche une notification toast.
Dessiner dans l'extension VS Code
Dans l'extension VS Code, ce qu'un mod dessine apparaît dans le panneau de chat à partir de la version 2.1.299 de l'extension. Une session qui ne récupère pas les feature flags, comme une session sur Amazon Bedrock, nécessite la version 2.1.304 ou ultérieure. Dans un hook ui.render pour le panneau, e.surface vaut vscode. Pour vérifier quels points de rendu et éléments le panneau dessine, et quelles props il ignore, consultez les tableaux des points de rendu et des éléments.
Pour qu'un mod fonctionne dans le panneau aussi bien que dans le terminal, tenez compte de ces différences :
- Panneaux : dans un panneau de 800 pixels de large ou plus, les panneaux s'ancrent à côté de la conversation,
e.props.placementvaut'dock'ete.viewport.isFullscreenvauttrue. Dans un panneau plus étroit, les panneaux se dessinent au-dessus de la zone de prompt,placementvaut'inline', et un panneau affiche 12 lignes avant de défiler. - Toasts et barres de statut : ils se trouvent dans une seule pile juste au-dessus de la zone de prompt, avec les panneaux lorsque le panneau est étroit, avec une barre de statut par mod. La pile est masquée pendant l'affichage d'une demande de permission ou d'une boîte de dialogue.
- Libellés : le panneau encadre ce qu'un mod dessine ou réécrit et l'étiquette avec le nom du plugin, comme dans From the token-chart plugin ou Edited by the token-chart plugin.
- Clavier : les boutons n'ont pas de raccourcis clavier, l'utilisateur clique donc sur un bouton ou appuie sur Enter une fois que celui-ci a le focus.
- Focus clavier : le panneau n'accorde la demande
focus: trued'un panneau que lorsque le panneau a le focus, que Claude est inactif sans question en attente, et que la zone de prompt est vide. - La boîte de dialogue de question : sur
AskUserQuestion, le panneau conserve sa propre carte de question et dessine vos éléments au-dessus. Les props que vous passez ànextne modifient pas la carte. - Suggestions de prompt :
$.prompt.suggestse résout avecisShown: falseet n'affiche rien, car le panneau réserve le texte de suggestion estompé de la zone de prompt aux propres suggestions de Claude. - Limites de texte : le panneau coupe une barre de statut à 200 caractères ou deux lignes, et un toast à 500 caractères.
Créer un arbre à partir d'éléments
Ce que renvoie un hook ui.render est un arbre d'éléments : une description de ce qu'il faut dessiner, composée de boîtes, de texte et de contrôles imbriqués les uns dans les autres. Vous décrivez le dessin, et Claude Code l'affiche dans l'application que l'utilisateur a ouverte : le terminal, l'application Desktop ou l'extension VS Code.
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 placez les éléments et les chaînes qui vont à l'intérieur dans children.
Sélectionnez un onglet pour voir chacun des éléments les plus courants et la façon dont le terminal le dessine :
Text
Text dessine une chaîne, avec un style facultatif tel que bold et color :
Text({ children: ['This is the first tab.'] })
This is the first tab.
Box
Box dispose ce qu'il contient, en ligne ou en colonne. Celui-ci place un bouton et une ligne de texte côte à côte, à deux colonnes d'écart :
Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({ key: 'more', label: 'Add one', onPress: addOne }),
Text({ children: ['Count: 0'] }),
],
})
[ Add one ] Count: 0
Button
Button est un contrôle sur lequel l'utilisateur peut appuyer. Il exécute votre callback onPress. Avec plain: true, il n'a pas de crochets et affiche son raccourci clavier :
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 est un champ de texte. Il exécute votre callback onSubmit avec le texte lorsque l'utilisateur appuie sur 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
Définissez rows à 2 ou plus pour obtenir un champ qui contient plusieurs lignes.
La galerie d'interface propose des exemples et des captures d'écran de la plupart des éléments. Ce tableau répertorie tous les éléments :
| Élément | Ce qu'il dessine | Où |
|---|---|---|
Box |
Un conteneur flex qui accepte des props de mise en page telles que flexDirection, borderStyle et width. Les props d'espacement telles que columnGap et padding acceptent un nombre ou un nom de palier de 'xs' à 'xl'. |
Partout |
Text |
Du texte stylisé. Accepte color, bold, dimColor, italic, wrap et d'autres comme weight. Une color est une clé de thème ou une couleur telle que 'red'. Un wrap vaut 'wrap', 'truncate', 'truncate-start', 'truncate-middle' ou 'truncate-end'. |
Partout |
Button |
Un contrôle qui appelle onPress |
Partout |
Link, Code, Markdown |
Un lien avec href et un label facultatif, un bloc de code, et du texte mis en forme comme les réponses de Claude. Markdown reçoit son contenu dans une prop text, et non dans children, et nécessite une key lorsque vous passez onLinkPress. |
Partout |
Input, Select |
Un champ de texte et une liste déroulante | Terminal, Desktop, VS Code |
Svg |
Un document SVG | Desktop, VS Code |
Client |
Une zone dessinée par un second fichier qui vous appartient, pour l'animation et la saisie au pointeur. Ce fichier n'a pas accès à l'API des mods. Il communique avec vos hooks en publiant des données, qui arrivent sous forme d'événement ui.message. S'il ne parvient pas à se charger, à dessiner ou à s'exécuter, vos hooks reçoivent un événement ui.fault. |
Terminal, Desktop |
Raster, Image |
Une grille de cellules colorées, et une image | Terminal |
Si votre module est un fichier .tsx ou .jsx, vous pouvez écrire l'arbre en JSX. Déstructurez d'abord les éléments à partir de $.ui.resolve(e).
Si votre dessin ne s'affiche pas, vérifiez que son arbre ne contient pas une prop qu'un élément n'accepte pas ou un enfant là où aucun n'est permis. Un hook qui renvoie un tel arbre échoue, et une ligne hook skipped nomme votre mod et la règle que l'arbre a enfreinte. Pour un Text dans un panneau avec une prop nommée bogusProp, cette ligne indique :
my-mod: ui.render hook skipped: returned the wrong shape (a tree Pane refuses: Text prop "bogusProp" is not allowed)
Link dans l'application Desktop et VS Code
Dans l'application Desktop et l'extension VS Code, un Link s'affiche comme du texte brut, sauf si son href remplit ces conditions :
- Schéma et hôte : une URL
https:, ou une URLhttp://localhosttelle quehttp://localhost:3000 - Pas de
@: écrivez un@dans le chemin ou la requête sous la forme%40 - Orthographe : ce que renvoie
new URL(href).href, à l'exception d'un/manquant après l'hôte. Cela exclut un hôte en majuscules, un espace et:443sur une URLhttps:.
Dans le terminal, ces conditions ne s'appliquent pas.
Lorsqu'un Client échoue
Dans le terminal, lorsque le fichier exécuté par un Client échoue, une ligne grisée telle que my-mod: Client client/spinner.js: boom prend la place du Client, et le reste de votre dessin reste affiché.
Si votre mod gère ui.fault, Claude Code redessine alors l'emplacement.
Dessiner une grille de cellules colorées
Pour une carte de chaleur, une sparkline ou un plateau de jeu dans le terminal, dessinez un seul Raster plutôt qu'un Box par cellule. Un Raster accepte une key, sa taille en columns et rows, et cells, une chaîne base64 qui regroupe toutes les cellules. Chaque cellule est composée de trois nombres : le point de code du caractère, sa couleur et sa couleur d'arrière-plan. Une couleur est une valeur RVB 24 bits en hexadécimal, par exemple 0xc62828 pour un rouge. La valeur 0x01000000, juste au-dessus de cette plage, désigne la couleur par défaut du terminal.
Les applications autres que le terminal n'ont pas de Raster : vérifiez donc e.surface et dessinez du texte à la place. Ce corps de panneau dessine une carte de chaleur de trois sur deux :
// 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) })],
})
})
Dans le terminal, le panneau affiche la grille :
Le tableau rows est la partie que vous modifieriez, et cellsOf le transforme en chaîne compactée. Le hook ne dessine que dans un panneau dont l'id est heat : ouvrez-en donc un avec $.ui.open({ id: 'heat' }) depuis une commande, comme l'exemple hello-tabs ouvre son panneau.
Chaque caractère doit occuper une seule cellule de largeur. Pour animer un Raster déjà affiché à l'écran, appelez $.ui.blit avec l'id du panneau comme requestId, la key du Raster, la même taille et de nouvelles cellules. Pour cet exemple, cela donne $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). Cet appel redessine cet unique élément sans exécuter à nouveau votre hook ui.render.
Réagir aux appuis et à la saisie
Lorsque l'utilisateur appuie sur un bouton, saisit du texte dans un champ ou choisit un élément dans une liste dessinée par votre mod, Claude Code appelle le callback de ce contrôle, qui s'exécute dans votre module. Chaque contrôle accepte ses propres callbacks :
Button: accepteonPress(e), oùe.surfaceest l'application d'où provient l'appuiInput: accepteonSubmit(value)etonInput(value)Select: accepteonSelect(value)avec ses choix dansoptions, une liste d'au moins un choix aux valeurs uniques, comme[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
Quels contrôles ont besoin d'une key
Donnez à chaque Input et Select une prop key, comme key: 'size'. Un Button peut se passer de key lorsqu'il possède une prop label ou un unique enfant de type chaîne. Sa clé est alors la valeur de label ou cette chaîne.
Les tests et les autres mods identifient un contrôle par sa clé :
- Tests : un test appuie sur un contrôle ou y saisit du texte via sa clé.
- Autres mods : Claude Code déclenche
ui.press,ui.inputouui.selectavec la clé du contrôle danse.elementavant d'exécuter le callback du contrôle. Le hook d'un autre mod pour cet événement voit ce que l'utilisateur a saisi dans votreInputet peut modifier ce texte ou empêcher votre callback de s'exécuter. L'API des mods ne donne à ce mod aucune méthode pour appuyer sur vos boutons.
Si un message indique needs a key ou is drawn twice, trouvez la cause à partir de ce message.
Focus clavier et raccourcis
Votre mod ne lit jamais le clavier lui-même. L'utilisateur appuie sur une touche, Claude Code détermine à quel contrôle elle est destinée, et le callback de ce contrôle s'exécute. Hormis un raccourci numérique sur le bandeau ou une liaison action, cela ne se produit que lorsque votre panneau ou votre bandeau a le focus clavier. Le reste du temps, les touches vont au prompt.
Dans l'extension VS Code, les boutons n'ont pas de raccourcis : l'utilisateur clique sur un bouton ou appuie sur Enter dessus, et saisit du texte dans les champs.
Comment un panneau obtient le focus clavier
Un panneau obtient le focus clavier lorsque :
- Votre mod l'ouvre avec
focus: truedepuis une commande ou un appui - L'utilisateur appuie sur Ctrl+X puis Tab
- L'utilisateur clique dessus, ce qui, dans le terminal, nécessite le rendu plein écran
Claude Code n'accorde focus: true que lorsque le prompt est vide et qu'aucun autre élément n'a le focus clavier. Un panneau qui s'ouvre pendant que l'utilisateur tape ne capture pas ses frappes.
Ce que fait chaque touche
Ce tableau indique ce que fait une touche lorsque votre panneau ou votre bandeau a le focus clavier :
| Touche | Ce qu'elle fait |
|---|---|
| Tab | Passe au contrôle suivant |
| Haut et Bas | Se déplacent entre les contrôles tant que votre dessin tient dans l'espace. Lorsque le panneau ou le bandeau contient plus de lignes qu'il ne peut en afficher, elles le font défiler. Dans un Input du terminal, elles se déplacent entre les lignes du champ, et depuis sa première ou sa dernière ligne, elles passent au contrôle précédent ou suivant. |
| Enter | Appuie sur le Button ayant le focus, soumet l'Input ayant le focus, ou effectue un choix dans un Select |
| Le raccourci d'un bouton | Appuie sur ce bouton. Lorsqu'un Input a le focus, chaque touche imprimable va dans le champ. |
| Page Up, Page Down, Home et End | Font défiler votre panneau ou votre bandeau lorsqu'il contient plus de lignes qu'il ne peut en afficher |
| Ctrl+X puis une touche fléchée | Redimensionne votre panneau. Gauche ou Haut lui donne plus d'espace, et Droite ou Bas rend cet espace. Un panneau situé à côté de la transcription cesse de s'élargir à 45 % de la largeur du terminal. |
| Ctrl+X puis X | Ferme votre panneau, même lorsque l'un de ses champs a le focus |
| Ctrl+X puis Ctrl+A | Réduit le bandeau à une ligne lorsqu'un mod y dessine, ou le développe à nouveau. Ces touches fonctionnent aussi lorsque le prompt a le focus. |
| Esc | Rend le focus clavier au prompt. Avec closeOnEscape: true, ferme aussi le panneau. |
Un mod ne peut pas associer Tab ou les touches fléchées à autre chose ; un jeu se dirige donc avec w, a, s et d.
Définir un raccourci et le focus initial
hotkey: pour permettre à l'utilisateur d'appuyer sur unButtonavec une seule touche, donnez-lui unhotkeyd'un chiffre ou d'une lettre minuscule, comme danshotkey: 'a'autoFocus: pour choisir le contrôle qui a le focus à l'ouverture du panneau, ajoutez-luiautoFocus: true. Dans l'extension VS Code,autoFocusn'a aucun effet : le contrôle est dessiné comme si vous l'aviez omis, et vous n'avez rien à modifier. La prop n'accepte quetrue; omettez-la donc sur les autres contrôles.
L'affichage d'un raccourci dépend du bouton et de l'application :
| Bouton | Dans le terminal | Dans l'application Desktop | Dans l'extension VS Code |
|---|---|---|---|
| Avec crochets, par défaut | [ Add one ], sans raccourci affiché |
Le libellé avec une petite touche à côté | Le libellé, et le raccourci n'a aucun effet |
Avec plain: true |
1: One |
Le libellé avec une petite touche à côté | Le libellé, et le raccourci n'a aucun effet |
Dans le terminal, nommez la touche dans le libellé d'un bouton entre crochets, ou utilisez plain: true, afin que l'utilisateur voie sur quoi appuyer. La référence des éléments présente les autres règles de Button : action, les raccourcis numériques sur le bandeau et deux boutons sur un même raccourci.
Lorsque l'utilisateur réduit le bandeau
Dans le terminal, lorsqu'un mod dessine dans le bandeau, l'utilisateur peut réduire le bandeau à une ligne pour le mettre de côté :
- Cliquer sur
[-]: lorsque le bandeau est développé, Claude Code dessine cette marque à l'extrémité droite de sa première ligne. En rendu plein écran, l'utilisateur clique dessus pour réduire le bandeau. - Appuyer sur Ctrl+X puis Ctrl+A : cela fonctionne que ce soit le prompt, le bandeau ou un panneau qui ait le focus clavier. L'utilisateur peut y associer d'autres touches via l'action
abovePrompt:toggle.
Un bandeau réduit masque tout ce que les mods y dessinent et affiche à la place une ligne atténuée, qui nomme les touches permettant de le développer selon la configuration de l'utilisateur :
▸ plugin panel hidden · ctrl+x ctrl+a or click to show
Les mêmes touches, ou en rendu plein écran un clic sur la ligne, développent à nouveau le bandeau. D'ici là, rien n'appuie sur un bouton qu'il contient : ni raccourci, ni chiffre saisi dans un prompt vide, ni raccourci clavier associé à l'action d'un bouton. Si le bandeau a le focus clavier lorsque l'utilisateur le réduit, le focus revient au prompt, et Ctrl+X puis Tab ignore le bandeau tant qu'il est réduit.
Le bandeau reste réduit jusqu'à ce que l'utilisateur le développe, même après /clear, et il est de nouveau développé au prochain démarrage de Claude Code. Votre mod ne peut ni le développer ni savoir qu'il est réduit : le hook ui.render continue de s'exécuter, et rien dans e.props ne l'indique. Ne faites pas du bandeau le seul endroit où votre mod affiche quelque chose auquel l'utilisateur doit répondre.
Recevoir une saisie et dessiner une ligne par élément
De nombreux panneaux se composent d'un champ de texte avec une liste en dessous. L'exemple de cette section est un panneau de notes : vous tapez une note et appuyez sur Enter pour l'ajouter, et chaque note possède un bouton x qui la supprime. Avec deux notes ajoutées, le terminal dessine le panneau ainsi :
╭────────────────────────────────────────────────────────✕─╮
│ Note: Type a note and press Enter ⏎ add │
│ x buy milk │
│ x call bob │
╰──────────────────────────────────────────────────────────╯
Le ✕ sur la bordure supérieure est la marque propre à Claude Code pour fermer le panneau.
L'exemple utilise ces techniques :
- Recevoir une saisie : un
InputappelleonSubmit(value)avec le texte du champ lorsque l'utilisateur appuie sur Enter, etonInput(value)à chaque modification - Dessiner une liste : associez chaque donnée à une ligne, et donnez au bouton de chaque ligne sa propre
key
Ce hook dessine le contenu du panneau :
// 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] }),
],
}),
),
],
})
})
Pour essayer le panneau :
- Ajouter une note : tapez une ligne et appuyez sur Enter. La ligne apparaît comme une nouvelle ligne de la liste, 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 Enter. Lexest le libellé du bouton et non un raccourci ; taper la lettre n'appuie donc pas dessus.
Chaque modification suit le même cycle de rendu que hello-tabs : le callback modifie notes, appelle redraw et enregistre la liste dans $.store. Si votre hook redessine le panneau pendant que l'utilisateur tape une note, le champ conserve son texte tant que le hook transmet la même value à chaque fois. Pour remplacer son texte par le vôtre, transmettez une value différente.
L'exemple enregistre les notes mais ne les charge pas. Pour les retrouver lors de la session suivante, lisez-les dans un hook session.start, comme hello-tabs lit count.
Ces props composent la ligne du champ, Note: Type a note and press Enter ⏎ add :
| Prop | Dans l'exemple | Ce que c'est |
|---|---|---|
label |
Note |
Le texte avant le champ. Le terminal dessine : après celui-ci. |
placeholder |
Type a note and press Enter |
Texte atténué affiché tant que le champ est vide |
value |
'' |
Le texte initial du champ. Il est vide ici, donc le placeholder s'affiche. |
submitLabel |
add |
Le mot après ⏎ qui indique ce que fait Enter |
Soumettre un Input ne démarre pas de tour, sauf si votre callback appelle $.prompt.submit.
Redessiner un site
Un dessin est un instantané : il affiche ce que votre hook ui.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 hook ui.render à nouveau quand les props du site changent ou la largeur du terminal change. Quand un Client du site échoue et que votre mod gère ui.fault, Claude Code exécute le hook une fois de plus après le retour de vos hooks ui.fault, afin que votre hook ui.render puisse omettre le Client. 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 :
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] }),
],
})
})
Chaque appui augmente le nombre dans le volet. L'exemple 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 hook session.start du module. Si le module en a déjà une, comme hello-tabs le fait, ajoutez la ligne $.clock.every à celle-ci :
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)
})
Claude Code exécute maintenant votre hook ui.render une fois par seconde. La minuterie s'arrête quand le module se recharge, et la nouvelle instance 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. Pour connaître la fréquence à laquelle chaque site peut redessiner, consultez le tableau des limites.
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
L'endroit où un mod garde une valeur détermine combien de temps elle 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 :
| La garder dans | Elle dure jusqu'à | L'utiliser pour |
|---|---|---|
| Une variable au niveau du module | Le module se recharge, ce qui se produit chaque fois que vous enregistrez un fichier pendant le développement | Les valeurs que vous pouvez perdre, comme tab dans hello-tabs |
$.state |
La session se termine, ou l'utilisateur exécute /clear, /resume, ou /branch |
Les valeurs dont dépend un dessin et qui devraient survivre à un rechargement |
$.store |
Votre mod la supprime, ou aucune session ne lit ou n'écrit le magasin pendant cleanupPeriodDays. Le magasin est un magasin clé-valeur, enregistré en tant que fichier JSON de votre propre plugin sous ~/.claude/plugins/store/. |
Les paramètres, l'historique, tout ce que l'utilisateur s'attend à trouver la prochaine fois |
$.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 déclaration 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 sous hello-tabs/types/index.d.ts :
declare module 'claude-code' {
interface PluginState {
'hello-tabs': {
tab: 'one' | 'two'
count: number
}
}
}
Pointer le manifeste vers la déclaration
Pour laisser claude plugin validate vérifier votre code par rapport à ce fichier, ajoutez un champ types au manifeste avec son chemin :
{
"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"
}
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 :
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)
Parce que le hook ui.render a lu count, Claude Code exécute le hook à nouveau chaque fois que le bouton l'écrit.
Ces règles s'appliquent au code :
- Écrivez
pluginetkeycomme des chaînes littérales :claude plugin validateles lit de votre source - Conservez le résultat de chaque appel
atomdans uneconst: si vous déclarezcountaveclet, la validation échoue avectakes a source the scan can read - Déclarez chaque valeur dans le fichier de déclaration 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
Gardez 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 en revanche 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 :
// 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)
})
Avec les deux hooks en place, le volet affiche le compte sauvegardé après /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 éviter que le magasin ne prenne du retard, enregistrez à chaque changement, comme le fait le bouton Add one.
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.
Pour rendre 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.
Ce bouton ajoute un à ce que le magasin contient maintenant, puis met à jour le dessin :
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)
}
Si une deuxième session a appuyé sur son propre bouton trois fois depuis le démarrage de cette session, cet appui affiche et enregistre un compte qui inclut ces trois.
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