Skip to main content
Un mod puede dibujar su propia interfaz en Claude Code y cambiar partes de la interfaz que Claude Code ya dibuja. Cada lugar donde un mod puede dibujar se llama sitio de renderizado, como un panel, la banda sobre el símbolo del sistema o el spinner. Claude Code genera el evento 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: Mapa de una sesión de terminal de Claude Code. Un mod puede agregar un panel como barra lateral a la derecha, una notificación en la esquina superior derecha de la transcripción, una línea de registro en la transcripción, una banda sobre el símbolo del sistema y una línea de estado bajo el símbolo del sistema. Un mod puede redibujar mensajes, filas de llamadas de herramientas y el spinner. El símbolo del sistema es propio de Claude Code. Mapa de una sesión de terminal de Claude Code. Un mod puede agregar un panel como barra lateral a la derecha, una notificación en la esquina superior derecha de la transcripción, una línea de registro en la transcripción, una banda sobre el símbolo del sistema y una línea de estado bajo el símbolo del sistema. Un mod puede redibujar mensajes, filas de llamadas de herramientas y el spinner. El símbolo del sistema es propio de Claude Code. En una terminal más estrecha, el panel se sitúa sobre el símbolo del sistema en lugar de junto a la transcripción. Construya su primer mod antes de comenzar aquí. Comience con el ejemplo trabajado, que construye un panel con dos pestañas y un contador, luego lea la sección para cada parte que desee cambiar.
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:
Claude Code no tiene un elemento de pestañas integrado, por lo que las pestañas son dos botones en una fila. El mod realiza un seguimiento de cuál está activo y dibuja el contenido de esa pestaña bajo la fila.
1

Crear el plugin

Un mod es un plugin con un manifiesto, un 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
Nombre su punto de entrada en 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:
  • 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
Dos variables a nivel de módulo, tab y count, mantienen el estado del panel.Guarde esto como hello-tabs/hooks/register.js:
hello-tabs/hooks/register.js
Cada hook también hace algo que el código no deja claro:
  • session.start también lee el recuento guardado de $.store, un almacén de clave-valor que persiste entre sesiones.
  • command.run solo le dice a Claude Code que el panel existe. Abrir un panel no dibuja nada por sí solo: Claude Code luego genera ui.render para preguntar qué va en él.
  • ui.render devuelve el árbol de elementos, un Box que contiene otros cuadros, texto y botones, y lo construye nuevamente desde tab y count cada vez que se ejecuta.
Presionar un botón ejecuta su callback 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 hook ui.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:
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 hook ui.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.
Para mantener el dibujo de Claude Code y cambiar una parte de él, pase a next una copia del evento con props cambiados. Este hook cambia el texto después de la palabra del spinner:
El spinner mantiene su animación y su palabra, y su texto sigue a la palabra:
El símbolo del sistema de permiso no es un sitio de renderizado, por lo que un mod no puede cambiar lo que muestra. El diálogo de pregunta, 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.
Para cerrar el panel, llame a $.ui.close con el id con el que lo abrió:
Además de 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.
Cuando aparece el panel, $.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 hook ui.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:
Text dibuja una cadena, con estilo opcional como bold y color:
Esta tabla enumera cada elemento: 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 un Raster 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:
En la terminal, el panel muestra la cuadrícula: Un panel en la terminal que contiene una pequeña cuadrícula de bloques coloreados, dos filas de tres. La fila superior es verde, ámbar y roja. La fila inferior es verde, verde y ámbar. La matriz 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: toma onPress(e), donde e.surface es la aplicación de la que proviene la pulsación
  • Input: toma onSubmit(value) y onInput(value)
  • Select: toma onSelect(value) con sus opciones en options, una lista de al menos una opción con valores únicos, como [{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
Una prueba presiona o escribe en un control por su 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: true desde un comando o una pulsación
  • El usuario presiona Ctrl+X y luego Tab
  • El usuario hace clic en él
Claude Code otorga 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 un Button con una tecla, dé un hotkey de un dígito o una letra minúscula, como en hotkey: 'a'
  • autoFocus: para elegir qué control tiene el enfoque cuando se abre el panel, agregue autoFocus: true a él. Deje la propiedad fuera de los otros, porque Claude Code rechaza autoFocus: false.
Cómo se muestra un atajo de teclado depende del botón y la aplicación: 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ón x que la elimina. Con dos notas agregadas, la terminal dibuja el panel de esta manera:
El ejemplo utiliza dos técnicas:
  • Tomar entrada escrita: un Input llama a onSubmit(value) con el texto del campo cuando el usuario presiona Enter, y onInput(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
Este hook dibuja el contenido del panel:
Para probar el panel:
  • 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 x de la nota tenga el enfoque, luego presione Enter. La x es la etiqueta del botón y no un atajo de teclado, por lo que escribir la letra no lo presiona.
Cada cambio sigue el mismo ciclo de renderizado que 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 hook ui.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 hook ui.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:
Cada pulsación aumenta el número en el panel. El ejemplo 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 hook session.start del módulo. Si el módulo ya tiene uno, como lo hace hello-tabs, agregue la línea $.clock.every a él:
Claude Code ahora ejecuta su hook 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 como hello-tabs/types/index.d.ts:
hello-tabs/types/index.d.ts

Apuntar el manifiesto a la declaración

Para permitir que claude 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:
Porque el hook 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 plugin y key como cadenas literales: claude plugin validate las 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.render puede leer estado y no puede escribirlo, así que escriba desde onPress, onSubmit u 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 import y reemplace let count = 0 con la línea atom
  • En el hook ui.render: agregue la línea read antes de tabButton y dibuje 'Count: ' + n en el Text
  • En el botón Add one: reemplace onPress con 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 leen saved con la llamada loadCount de Cargar un valor guardado nuevamente después de /clear
Mantenga 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:
Con ambos hooks en su lugar, el panel muestra el recuento guardado después de /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 set cambia 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, get la clave en el callback y construya el nuevo valor a partir de eso, no de una copia que cargó en session.start. La escritura de otra sesión se pierde si llega entre su get y su set.
Este botón suma uno a lo que el almacén contiene ahora, luego actualiza el dibujo:
Si una segunda sesión ha presionado su propio botón tres veces desde que esta sesión comenzó, esta pulsación muestra y guarda un recuento que incluye esos tres.

Próximos pasos