ui.render cada vez que está a punto de dibujar un sitio de renderizado, y su hook para ese evento devuelve lo que se debe dibujar allí.
Este mapa muestra dónde un mod puede dibujar en una sesión de terminal:
Para buscar una propiedad o límite, consulte la referencia.
Construir un panel con pestañas
En esta sección construye un mod que agrega un comando/hello-tabs y el comando abre un panel. Un panel es una barra lateral junto a la transcripción en una terminal de pantalla completa ancha, o una región enmarcada sobre el símbolo del sistema de otra manera. Este panel muestra dos pestañas, y la segunda pestaña tiene un botón que suma uno a un contador. El recuento sigue ahí después de reiniciar Claude Code.
El mod terminado se ve así. La grabación abre el panel, cambia a la segunda pestaña, presiona el botón varias veces y vuelve a la primera pestaña:
1
Crear el plugin
Un mod es un plugin con un manifiesto, un Nombre su punto de entrada en
hooks.json que apunta a su código y el archivo de código. Crear un mod explica cada uno. Cree un directorio llamado hello-tabs con directorios .claude-plugin y hooks dentro, luego guarde los dos primeros archivos.Guarde el manifiesto como hello-tabs/.claude-plugin/plugin.json:hello-tabs/.claude-plugin/plugin.json
hello-tabs/hooks/hooks.json:hello-tabs/hooks/hooks.json
2
Escribir el código
El código realiza tres trabajos, uno en cada hook:Cada hook también hace algo que el código no deja claro:
- Agrega el comando
/hello-tabs - Abre el panel cuando ejecuta ese comando
- Dibuja el contenido del panel: la fila de pestañas y el cuerpo de la pestaña abierta
tab y count, mantienen el estado del panel.Guarde esto como hello-tabs/hooks/register.js:hello-tabs/hooks/register.js
session.starttambién lee el recuento guardado de$.store, un almacén de clave-valor que persiste entre sesiones.command.runsolo le dice a Claude Code que el panel existe. Abrir un panel no dibuja nada por sí solo: Claude Code luego generaui.renderpara preguntar qué va en él.ui.renderdevuelve el árbol de elementos, unBoxque contiene otros cuadros, texto y botones, y lo construye nuevamente desdetabycountcada vez que se ejecuta.
onPress, que cambia una variable y llama a redraw. Claude Code luego ejecuta el hook ui.render nuevamente, y el hook construye un nuevo árbol a partir de los nuevos valores. Cada dibujo interactivo utiliza ese ciclo de renderizado: un callback cambia el estado y el hook se renderiza nuevamente desde el nuevo estado.3
Abrir el panel
En su shell, inicie Claude Code con
claude --plugin-dir ./hello-tabs. En el símbolo del sistema de Claude Code, ejecute /hello-tabs. Se abre un panel con 1: One y 2: Two en la parte superior. Presione 2, luego presione a, el atajo de teclado para Add one, varias veces. El recuento sube.4
Verificar que el recuento fue guardado
Presione Esc para cerrar el panel, luego salga de la sesión. En su shell, inicie Claude Code nuevamente con el mismo comando
claude --plugin-dir ./hello-tabs y en el símbolo del sistema de Claude Code ejecute /hello-tabs. El recuento está donde lo dejó.Para borrar el recuento, haga que el mod llame a $.store.delete('count'). Mantener estado cubre cuánto tiempo dura cada tipo de valor.Elegir dónde dibujar
Un hookui.render se ejecuta para cada sitio de renderizado a menos que lo reduzca al que desea dibujar. Para elegir el sitio de renderizado, pase un filtro, llamado matcher, como segundo argumento a on. { component: 'Pane' } ejecuta el hook solo para paneles. En el hook, e.component nombra el sitio, e.surface dice qué aplicación está dibujando, y e.props contiene los datos propios del sitio. Para un panel, e.requestId es el id con el que lo abrió.
Dos sitios están vacíos hasta que un mod los llena, el panel y la banda. Seleccione una pestaña para ver qué es cada uno y cómo dibujar en él:
- Panel
- Banda sobre el símbolo del sistema
Un panel es una barra lateral junto a la transcripción en una terminal de pantalla completa ancha, o una región enmarcada sobre el símbolo del sistema de otra manera. Con varios paneles abiertos, cada uno obtiene una pestaña que muestra su título.Un panel aparece cuando su mod llama a
$.ui.open con un id que elige, como en $.ui.open({ id: 'hello-tabs' }). Abrir un panel en el momento adecuado cubre los otros campos y cuándo un panel espera una terminal más ancha.Para dibujar en su panel, filtre en { component: 'Pane' } y verifique que e.requestId sea su id.Cambiar lo que Claude Code ya dibuja
Claude Code dibuja la mayoría de su interfaz a sí mismo: mensajes, filas de llamadas de herramientas, el spinner y más. Cada una de esas partes es un sitio de renderizado también, por lo que un mod puede cambiar el estilo o reemplazarlo. Para cambiar uno, filtre su hookui.render en su nombre de esta tabla:
En un sitio que Claude Code ya dibuja, su hook tiene tres opciones: cambiar un detalle, reemplazar el dibujo o dejarlo solo. Seleccione una pestaña para ver cada una aplicada al spinner. Los ejemplos leen una variable
calls que otro hook cuenta, como en el mod de tutorial.
- Cambiar un detalle
- Reemplazar el dibujo
- Dejarlo solo
Para mantener el dibujo de Claude Code y cambiar una parte de él, pase a El spinner mantiene su animación y su palabra, y su texto sigue a la palabra:
next una copia del evento con props cambiados. Este hook cambia el texto después de la palabra del spinner:AskUserQuestion, es uno, por lo que un mod puede cambiar eso.
La terminal y la aplicación de escritorio no generan todos los mismos sitios. Pane, AbovePrompt, Spinner y los sitios de transcripción funcionan en ambos. Algunas otras líneas de estado se generan solo en la terminal. La tabla de sitios de renderizado enumera dónde se genera cada uno.
Abrir un panel en el momento adecuado
Un panel aparece solo cuando su mod lo abre. Cómo y cuándo lo abre decide si toma el enfoque del teclado, cuánto espacio solicita y si aparece en absoluto en una terminal estrecha. Para abrir un panel, llame a$.ui.open con un id que elija. El id es el nombre del panel: su hook ui.render lo verifica y lo pasa nuevamente para cerrar el panel.
$.ui.close con el id con el que lo abrió:
id, $.ui.open toma estos campos opcionales:
Para permitir que un comando abra el panel mientras Claude está trabajando, agregue
immediate: true cuando registre el comando. Sin él, un comando escrito durante un turno espera a que el turno termine.
Cuando un panel espera una terminal más ancha
Un panel que su mod abre sin ser solicitado no aparece en una terminal estrecha, por lo que no puede ocupar una pantalla pequeña. Si aparece depende de lo que lo abrió:- Abierto por algo que hizo el usuario, como un comando que ejecutó o un botón que presionó, el panel aparece en cualquier ancho
- Abierto por su mod actuando por sí solo, como desde un temporizador o un hook
turn.start, el panel aparece solo en una terminal de al menos 144 columnas de ancho. Después de que el usuario haya abierto ese panel una vez por sí mismo, 110 columnas es suficiente.
$.ui.open se resuelve en { isPlaced: true }. Cuando el panel está esperando, isPlaced es false y reason es una cadena que dice por qué. Un panel en espera aparece cuando el usuario lo abre o amplía la terminal. Para decir que algo está disponible sin abrir un panel, llame a $.ui.toast('Your message'), que muestra un pequeño aviso que desaparece después de unos segundos.
Construir un árbol a partir de elementos
Lo que devuelve un hookui.render es un árbol de elementos: una descripción de qué dibujar, hecha de cuadros, texto y controles anidados entre sí. Describe el dibujo y Claude Code lo renderiza en la terminal o en la aplicación de escritorio.
Para obtener los elementos, llame a $.ui.resolve(e) en su hook, como en const { Box, Text, Button } = $.ui.resolve(e). Cada elemento es una función. Pasa sus propiedades y pone los elementos y cadenas que van dentro en children.
La mayoría de los dibujos utilizan cuatro elementos. Seleccione una pestaña para ver cada uno y cómo la terminal lo dibuja:
- Texto
- Cuadro
- Botón
- Entrada
Text dibuja una cadena, con estilo opcional como bold y color:
Si su módulo es un archivo
.tsx o .jsx, puede escribir el árbol como JSX. Desestructure los elementos de $.ui.resolve(e) primero, porque un módulo de hooks no tiene globales de elementos.
Si un árbol utiliza un elemento que la aplicación no tiene, una propiedad que un elemento no toma o un hijo donde no va ninguno, Claude Code dibuja su propia versión del sitio.
En una sesión iniciada con --plugin-dir, una línea de transcripción lo dice, como ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own. El registro de depuración lo registra como ui.render (Pane): a hook returned a tree that does not validate con la misma razón. Nada más aparece en la sesión, por lo que cuando un dibujo no aparece, verifique esa línea o el registro.
Dibujar una cuadrícula de celdas coloreadas
Para un mapa de calor, un gráfico de chispa o un tablero de juego en la terminal, dibuje unRaster y no un Box para cada celda. Un Raster toma una key, su tamaño en columns y rows, y cells, que empaqueta cada celda en una cadena. Cada celda son tres números: el punto de código del carácter, su color y su color de fondo. Un color es un número hexadecimal con dos dígitos cada uno para rojo, verde y azul, como 0xc62828 para un rojo, o 0x01000000 para el predeterminado de la terminal.
La aplicación de escritorio no tiene Raster, por lo que verifique e.surface y dibuje texto allí. Este cuerpo de panel dibuja un mapa de calor de tres por dos:
rows es la parte que cambiaría, y cellsOf la convierte en la cadena empaquetada. El hook dibuja solo en un panel cuyo id es heat, por lo que abra uno con $.ui.open({ id: 'heat' }) desde un comando, como el ejemplo hello-tabs abre su panel.
Cada carácter tiene que tener un ancho de una celda. Para animar un Raster que ya está en pantalla, llame a $.ui.blit con el id del panel como requestId, la key del Raster, el mismo tamaño y celdas nuevas. Para este ejemplo, eso es $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). Repinta ese elemento sin ejecutar su hook ui.render nuevamente.
Responder a pulsaciones y escritura
Cuando el usuario presiona un botón, escribe en un campo o elige de una lista que su mod dibujó, Claude Code llama a la función que le dio a ese control, y se ejecuta en su módulo. Cada control toma sus propios callbacks:Button: tomaonPress(e), dondee.surfacees la aplicación de la que proviene la pulsaciónInput: tomaonSubmit(value)yonInput(value)Select: tomaonSelect(value)con sus opciones enoptions, una lista de al menos una opción con valores únicos, como[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
key, así que dé a cada control uno. Cada uso de un control también genera ui.press, ui.input o ui.select con la key en e.element, y otro mod puede enganchar esos eventos. Su hook se ejecuta antes de su callback, por lo que ve lo que el usuario escribe en su Input y puede cambiarlo o responder en lugar de su callback. La API de mods no tiene método que presione el botón de otro mod.
Enfoque del teclado y atajos de teclado
Su mod nunca lee el teclado a sí mismo. El usuario presiona una tecla, Claude Code decide cuál de sus controles es para, y se ejecuta el callback de ese control. Aparte de un atajo de teclado de dígito en la banda, eso sucede solo mientras su panel o banda tiene enfoque del teclado. El resto del tiempo, las teclas van al símbolo del sistema.Cómo un panel obtiene enfoque del teclado
Un panel obtiene enfoque del teclado de una de tres maneras:- Su mod lo abre con
focus: truedesde un comando o una pulsación - El usuario presiona Ctrl+X y luego Tab
- El usuario hace clic en él
focus: true solo mientras el símbolo del sistema está vacío y nada más tiene enfoque del teclado. Un panel que se abre mientras el usuario está escribiendo no toma sus pulsaciones de teclas.
Qué hace cada tecla
Esta tabla enumera qué hace una tecla mientras su panel o banda tiene enfoque del teclado:
Un mod no puede vincular Tab o las teclas de flecha a nada más, por lo que un juego se dirige con
w, a, s y d.
Establecer un atajo de teclado y el primer enfoque
Dos propiedades en un control deciden cómo el teclado lo alcanza:hotkey: para permitir que el usuario presione unButtoncon una tecla, dé unhotkeyde un dígito o una letra minúscula, como enhotkey: 'a'autoFocus: para elegir qué control tiene el enfoque cuando se abre el panel, agregueautoFocus: truea él. Deje la propiedad fuera de los otros, porque Claude Code rechazaautoFocus: false.
En la terminal, nombre la tecla en la etiqueta de un botón entre corchetes, o use
plain: true, para que el usuario pueda ver qué presionar. La referencia de elementos tiene las otras reglas de Button: action, atajos de teclado de dígitos en la banda y dos botones en un atajo de teclado.
Tomar entrada escrita y dibujar una fila para cada elemento
Muchos paneles son un campo de texto con una lista debajo. El ejemplo en esta sección es un panel de notas: escribe una nota y presiona Enter para agregarla, y cada nota tiene un botónx que la elimina. Con dos notas agregadas, la terminal dibuja el panel de esta manera:
- Tomar entrada escrita: un
Inputllama aonSubmit(value)con el texto del campo cuando el usuario presiona Enter, yonInput(value)en cada cambio - Dibujar una lista: asigne sus datos a una fila cada uno, y dé a cada botón de fila su propia
key
- Agregar una nota: escriba una línea y presione Enter. La línea aparece como una nueva fila y el campo se vacía.
- Eliminar una nota: presione Tab hasta que el botón
xde la nota tenga el enfoque, luego presione Enter. Laxes la etiqueta del botón y no un atajo de teclado, por lo que escribir la letra no lo presiona.
hello-tabs: el callback cambia notes, llama a redraw y guarda la lista en $.store.
El campo se vacía después de cada envío debido a su propiedad value. value es el texto que el campo contiene cuando se dibuja, y la escritura del usuario lo reemplaza hasta que su hook dibuja el campo nuevamente. El ejemplo siempre dibuja el campo con ''.
El ejemplo guarda las notas y no las carga. Para traerlas de vuelta en la siguiente sesión, léalas en un hook session.start, de la manera que hello-tabs lee count.
Tres propiedades componen la línea del campo, Note: Type a note and press Enter ⏎ add:
Enviar un
Input no inicia un turno a menos que su callback llame a $.prompt.submit.
Redibujar un sitio
Un dibujo es una instantánea: muestra lo que devolvió su hookui.render la última vez que se ejecutó. Para mostrar algo nuevo, el hook tiene que ejecutarse de nuevo. Claude Code lo ejecuta de nuevo para algunos cambios, y su mod solicita el resto.
Cuándo Claude Code redibuja sin ser solicitado
Claude Code ejecuta su hookui.render de nuevo cuando cambian los props del sitio o cambia el ancho de la terminal. No ejecuta el hook en un temporizador y no puede saber cuándo cambia una variable en su módulo.
Redibujar cuando sus datos cambian
Para que sus sitios se redibjen después de que sus propios datos cambien, llame a$.ui.invalidate('ui.render'). Este panel cuenta pulsaciones. La devolución de llamada del botón cambia count y luego solicita un redibujado:
hello-tabs envuelve la misma llamada en su función redraw.
Un valor que mantiene en $.state no necesita la llamada, porque escribir el valor redibuja los sitios que lo leen.
Redibujar en un temporizador
Para mantener un reloj, una cuenta atrás o un valor de fuera de la sesión actual, redibuje según un cronograma. Inicie un temporizador en el hooksession.start del módulo. Si el módulo ya tiene uno, como lo hace hello-tabs, agregue la línea $.clock.every a él:
ui.render una vez por segundo. El temporizador se detiene cuando el módulo se recarga, y la nueva copia del módulo inicia el suyo propio.
Con qué frecuencia se puede redibujar un sitio
Claude Code limita la frecuencia con la que redibuja un sitio, por lo que su mod puede llamar a$.ui.invalidate con la frecuencia que cambien sus datos. El panel visible y la banda tienen un límite más alto que otros sitios, y la tabla de límites tiene los números.
Las llamadas que llegan más rápido que el límite se combinan en un redibujado. Ese redibujado ejecuta su hook una vez, y el hook lee sus datos tal como están en ese momento, por lo que se muestra el valor más reciente y los valores intermedios no se muestran. Una animación no puede ejecutarse más rápido que el límite.
Mantener estado
Un mod tiene tres lugares para mantener un valor, y difieren en cuánto tiempo dura el valor: hasta que el módulo se recarga, hasta que termina la sesión o de una sesión a la siguiente. Elija según cuánto tiempo tenga que durar el valor:$.store.get(key) se resuelve en el valor o undefined, y $.store.set(key, value) toma cualquier valor JSON.
Mantener un valor en $.state
$.state mantiene valores durante la duración de una sesión, y se redibuja por usted. Es estado reactivo: un hook ui.render que lee un valor se suscribe a él, por lo que Claude Code redibuja ese sitio cada vez que escribe el valor, y no llama a $.ui.invalidate. Un valor en $.state también sobrevive a una recarga del módulo, lo que una variable no.
Para configurarlo, declare sus valores, apunte su manifiesto a la declaración, luego defina y use cada valor. Los ejemplos mueven el count de hello-tabs a $.state.
Declarar los valores
Declare los valores en un archivo de tipos. La clave externa es el nombre de su plugin, y cada entrada bajo ella es un valor y su tipo. Guarde esto comohello-tabs/types/index.d.ts:
hello-tabs/types/index.d.ts
Apuntar el manifiesto a la declaración
Para permitir queclaude plugin validate verifique su código contra ese archivo, agregue un campo types al manifiesto con su ruta:
hello-tabs/.claude-plugin/plugin.json
Definir, leer y escribir un valor
En su módulo, defina cada valor con un predeterminado, léalo mientras dibuja y escríbalo desde un callback.atom nombra un valor y su predeterminado, read lo devuelve y update lo escribe. Los tres ayudantes llaman a $.state.get y $.state.set por usted:
ui.render leyó count, Claude Code ejecuta el hook nuevamente cada vez que el botón lo escribe.
Tres reglas se aplican al código:
- Escriba
pluginykeycomo cadenas literales:claude plugin validatelas lee de su fuente - Declare cada valor en el archivo de tipos: de lo contrario, la validación falla con
hello-tabs.count is not declared - Escriba desde un callback u otro hook de evento: un hook
ui.renderpuede leer estado y no puede escribirlo, así que escriba desdeonPress,onSubmitu otro hook de evento
Cambiar hello-tabs para usar $.state
Para mover count en hello-tabs a $.state, cambie cada línea que lo use:
- En la parte superior del módulo: agregue la línea
importy reemplacelet count = 0con la líneaatom - En el hook
ui.render: agregue la líneareadantes detabButtony dibuje'Count: ' + nen elText - En el botón Add one: reemplace
onPresscon el de Guardar desde más de una sesión, que guarda el recuento además de escribirlo - En el hook
session.start: reemplace las dos líneas que leensavedcon la llamadaloadCountde Cargar un valor guardado nuevamente después de/clear
redraw para los botones de pestaña, porque tab sigue siendo una variable.
Cargar un valor guardado nuevamente después de /clear
Si su mod copia un valor guardado de $.store a $.state en session.start, tiene que copiarlo nuevamente después de /clear, /resume o /branch. Esos comandos devuelven cada valor de $.state a su predeterminado, y session.start no se dispara nuevamente. classic.SessionStart se dispara después de cada uno de ellos, con e.source establecido en clear, resume o fork, así que copie el valor nuevamente en un hook en él. De lo contrario, su dibujo muestra el predeterminado, y un callback que guarda el valor de $.state escribe el predeterminado sobre lo que almacenó.
Este código carga count de ambos hooks. Se basa en la versión de $.state de hello-tabs, donde count es un átomo y update se importa. Ponga loadCount arriba de register y agregue la llamada loadCount al hook session.start que ya tiene. classic.SessionStart también se dispara al inicio y después de la compactación, que no reinicia $.state, por lo que el filtro en source mantiene el hook a los tres reiniciados:
/clear y no 0, y la siguiente pulsación de Add one suma al recuento guardado.
loadCount escribe el valor almacenado sobre el que está en $.state, y session.start se dispara nuevamente cada vez que el módulo se recarga. Para mantener el almacén actualizado, guarde en cada cambio, como lo hace el botón Add one.
Para verificar la recarga sin una sesión, pruebe el dibujo después de /clear.
Guardar desde más de una sesión
Cada sesión en su máquina que ejecuta su mod comparte un$.store. Un get seguido de un set no es atómico. Cuando dos sesiones leen un valor, lo cambian y lo escriben de vuelta, compiten, y la segunda escritura reemplaza la primera.
Dos opciones hacen que sea menos probable:
- Dé a cada elemento su propia clave: un
setcambia solo su propia clave, por lo que las sesiones que escriben claves diferentes no se sobrescriben entre sí - Lea nuevamente justo antes de escribir: para un valor que varias sesiones cambian,
getla clave en el callback y construya el nuevo valor a partir de eso, no de una copia que cargó ensession.start. La escritura de otra sesión se pierde si llega entre sugety suset.
Próximos pasos
- Reaccionar a eventos: alimente su dibujo desde llamadas de herramientas y turnos
- Usar la API de mods: alimente su dibujo desde temporizadores y llamadas de modelo
- Probar un dibujo: presione sus botones desde una prueba, en más de una superficie
- Sitios de renderizado y elementos: propiedades de cada sitio y propiedades de cada elemento