ui.render cada vez que está prestes a desenhar um site de renderização, e seu hook para esse evento retorna o que desenhar lá.
Este mapa mostra onde um mod pode desenhar em uma sessão de terminal:
Para procurar uma propriedade ou limite, consulte a referência.
Construir um painel com abas
Nesta seção você constrói um mod que adiciona um comando/hello-tabs e o comando abre um painel. Um painel é uma barra lateral ao lado da transcrição em um terminal fullscreen amplo, ou uma região enquadrada acima do prompt caso contrário. Este painel mostra duas abas, e a segunda aba tem um botão que adiciona um ao contador. A contagem ainda está lá depois que você reinicia o Claude Code.
O mod finalizado se parece com isto. A gravação abre o painel, muda para a segunda aba, pressiona o botão algumas vezes e retorna à primeira aba:
1
Criar o plugin
Um mod é um plugin com um manifesto, um Nomeie seu ponto de entrada em
hooks.json que aponta para seu código, e o arquivo de código. Criar um mod explica cada um. Crie um diretório chamado hello-tabs com diretórios .claude-plugin e hooks dentro dele, depois salve os dois primeiros arquivos.Salve o manifesto como hello-tabs/.claude-plugin/plugin.json:hello-tabs/.claude-plugin/plugin.json
hello-tabs/hooks/hooks.json:hello-tabs/hooks/hooks.json
2
Escrever o código
O código faz três trabalhos, um em cada hook:Cada hook também faz algo que o código não deixa claro:
- Adiciona o comando
/hello-tabs - Abre o painel quando você executa esse comando
- Desenha o conteúdo do painel: a linha de abas e o corpo da aba aberta
tab e count, mantêm o estado do painel.Salve isto como hello-tabs/hooks/register.js:hello-tabs/hooks/register.js
session.starttambém lê a contagem salva de$.store, um armazenamento de chave-valor que persiste entre sessões.command.runapenas diz ao Claude Code que o painel existe. Abrir um painel não desenha nada por si só: o Claude Code então disparaui.renderpara perguntar o que colocar nele.ui.renderretorna a árvore de elementos, umaBoxque contém outras caixas, texto e botões, e a constrói novamente a partir detabecountcada vez que é executada.
onPress, que altera uma variável e chama redraw. O Claude Code então executa o hook ui.render novamente, e o hook constrói uma nova árvore a partir dos novos valores. Cada desenho interativo usa esse ciclo de renderização: um callback altera o estado e o hook renderiza novamente a partir do novo estado.3
Abrir o painel
Em seu shell, inicie o Claude Code com
claude --plugin-dir ./hello-tabs. No prompt Claude Code, execute /hello-tabs. Um painel abre com 1: One e 2: Two na parte superior. Pressione 2, depois pressione a, o atalho de teclado para Add one, algumas vezes. A contagem sobe.4
Verificar se a contagem foi salva
Pressione Esc para fechar o painel, depois saia da sessão. Em seu shell, inicie o Claude Code novamente com o mesmo comando
claude --plugin-dir ./hello-tabs e no prompt Claude Code execute /hello-tabs. A contagem está onde você a deixou.Para limpar a contagem, faça o mod chamar $.store.delete('count'). Manter estado cobre quanto tempo cada tipo de valor dura.Escolher onde desenhar
Um hookui.render é executado para cada site de renderização a menos que você o restrinja ao que deseja desenhar. Para escolher o site de renderização, passe um filtro, chamado de matcher, como o segundo argumento para on. { component: 'Pane' } executa o hook apenas para painéis. No hook, e.component nomeia o site, e.surface diz qual app está desenhando, e e.props contém os dados do próprio site. Para um painel, e.requestId é o id que você abriu com.
Dois sites estão vazios até um mod preenchê-los, o painel e a faixa. Selecione uma aba para ver o que cada um é e como desenhar nele:
- Pane
- Band above the prompt
Um painel é uma barra lateral ao lado da transcrição em um terminal fullscreen amplo, ou uma região enquadrada acima do prompt caso contrário. Com vários painéis abertos, cada um recebe uma aba que mostra seu título.Um painel aparece quando seu mod chama
$.ui.open com um id que você escolhe, como em $.ui.open({ id: 'hello-tabs' }). Abrir um painel no momento certo cobre os outros campos e quando um painel espera por um terminal mais amplo.Para desenhar em seu painel, filtre em { component: 'Pane' } e verifique se e.requestId é seu id.Alterar o que o Claude Code já desenha
O Claude Code desenha a maior parte de sua interface por si só: mensagens, linhas de chamada de ferramenta, o spinner e muito mais. Cada uma dessas partes é um site de renderização também, então um mod pode restylar ou substituí-la. Para alterar uma, filtre seu hookui.render em seu nome desta tabela:
Em um site que o Claude Code já desenha, seu hook tem três escolhas: alterar um detalhe, substituir o desenho ou deixá-lo em paz. Selecione uma aba para ver cada um aplicado ao spinner. Os exemplos leem uma variável
calls que outro hook conta, como no mod do tutorial.
- Change a detail
- Replace the drawing
- Leave it alone
Para manter o desenho do Claude Code e alterar uma parte dele, passe para O spinner mantém sua animação e sua palavra, e seu texto segue a palavra:
next uma cópia do evento com props alteradas. Este hook altera o texto após a palavra do spinner:AskUserQuestion, é um, então um mod pode alterar isso.
O terminal e o app Desktop não disparam todos os mesmos sites. Pane, AbovePrompt, Spinner e os sites de transcrição funcionam em ambos. Algumas outras linhas de status são disparadas apenas no terminal. A tabela de sites de renderização lista onde cada um é disparado.
Abrir um painel no momento certo
Um painel aparece apenas quando seu mod o abre. Como e quando você o abre decide se ele toma o foco do teclado, quanto espaço ele pede e se aparece em um terminal estreito. Para abrir um painel, chame$.ui.open com um id que você escolhe. O id é o nome do painel: seu hook ui.render verifica, e você o passa novamente para fechar o painel.
$.ui.close com o id que você abriu com:
id, $.ui.open leva estes campos opcionais:
Para deixar um comando abrir o painel enquanto o Claude está trabalhando, adicione
immediate: true quando você registra o comando. Sem isso, um comando digitado durante uma volta espera a volta terminar.
Quando um painel espera por um terminal mais amplo
Um painel que seu mod abre sem ser solicitado não aparece em um terminal estreito, então não pode assumir uma tela pequena. Se aparece depende do que o abriu:- Aberto por algo que o usuário fez, como um comando que executou ou um botão que pressionou, o painel aparece em qualquer largura
- Aberto por seu mod agindo por si só, como de um timer ou um hook
turn.start, o painel aparece apenas em um terminal com pelo menos 144 colunas de largura. Depois que o usuário abriu esse painel uma vez por si só, 110 colunas é suficiente.
$.ui.open resolve para { isPlaced: true }. Quando o painel está esperando, isPlaced é false e reason é uma string que diz por quê. Um painel esperando aparece quando o usuário o abre ou amplia o terminal. Para dizer que algo está disponível sem abrir um painel, chame $.ui.toast('Your message'), que mostra um pequeno aviso que desaparece após alguns segundos.
Construir uma árvore a partir de elementos
O que um hookui.render retorna é uma árvore de elementos: uma descrição do que desenhar, feita de caixas, texto e controles aninhados um dentro do outro. Você descreve o desenho, e o Claude Code o renderiza no terminal ou no app Desktop.
Para obter os elementos, chame $.ui.resolve(e) em seu hook, como em const { Box, Text, Button } = $.ui.resolve(e). Cada elemento é uma função. Você passa propriedades para ela, e coloca os elementos e strings que vão dentro dela em children.
A maioria dos desenhos usa quatro elementos. Selecione uma aba para ver cada um e como o terminal o desenha:
- Text
- Box
- Input
Text desenha uma string, com estilo opcional como bold e color:
Se seu módulo é um arquivo
.tsx ou .jsx, você pode escrever a árvore como JSX. Desestruture os elementos de $.ui.resolve(e) primeiro, porque um módulo de hooks não tem globais de elementos.
Se uma árvore usa um elemento que o app não tem, uma propriedade que um elemento não leva, ou um filho onde nenhum vai, o Claude Code desenha sua própria versão do site.
Em uma sessão iniciada com --plugin-dir, uma linha de transcrição diz assim, como ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own. O log de depuração registra como ui.render (Pane): a hook returned a tree that does not validate com a mesma razão. Nada mais aparece na sessão, então quando um desenho não aparece, verifique essa linha ou o log.
Desenhar uma grade de células coloridas
Para um mapa de calor, um sparkline ou um tabuleiro de jogo no terminal, desenhe umRaster e não uma Box para cada célula. Um Raster leva uma key, seu tamanho em columns e rows, e cells, que empacota cada célula em uma string. Cada célula é três números: o ponto de código do caractere, sua cor e sua cor de fundo. Uma cor é um número hexadecimal com dois dígitos cada para vermelho, verde e azul, como 0xc62828 para um vermelho, ou 0x01000000 para o padrão do terminal.
O app Desktop não tem Raster, então verifique e.surface e desenhe texto lá. Este corpo de painel desenha um mapa de calor de três por dois:
rows é a parte que você alteraria, e cellsOf a transforma na string empacotada. O hook desenha apenas em um painel cujo id é heat, então abra um com $.ui.open({ id: 'heat' }) de um comando, como o exemplo hello-tabs abre seu painel.
Cada caractere tem que ser uma célula de largura. Para animar um Raster que já está na tela, chame $.ui.blit com o id do painel como requestId, a key do Raster, o mesmo tamanho e novas células. Para este exemplo, é $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). Ele repinta apenas esse elemento sem executar seu hook ui.render novamente.
Responder a pressionamentos e digitação
Quando o usuário pressiona um botão, digita em um campo ou escolhe de uma lista que seu mod desenhou, o Claude Code chama a função que você deu a esse controle, e ela é executada em seu módulo. Cada controle leva seus próprios callbacks:Button: levaonPress(e), ondee.surfaceé o app de onde veio o pressionamentoInput: levaonSubmit(value)eonInput(value)Select: levaonSelect(value)com suas escolhas emoptions, uma lista de pelo menos uma escolha com valores únicos, como[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
key, então dê a cada um uma. Cada uso de um controle também dispara ui.press, ui.input ou ui.select com a key em e.element, e outro mod pode fazer hook nesses eventos. Seu hook é executado antes de seu callback, então vê o que o usuário digita em seu Input e pode alterá-lo ou responder no lugar de seu callback. A API de mods não tem método que pressione o botão de outro mod.
Foco do teclado e atalhos de teclado
Seu mod nunca lê o teclado por si só. O usuário pressiona uma tecla, o Claude Code decide qual de seus controles é para, e o callback desse controle é executado. Além de um atalho de teclado de dígito na faixa, isso acontece apenas enquanto seu painel ou faixa tem foco do teclado. O resto do tempo, as teclas vão para o prompt.Como um painel obtém foco do teclado
Um painel obtém foco do teclado de uma de três maneiras:- Seu mod o abre com
focus: truede um comando ou um pressionamento - O usuário pressiona Ctrl+X depois Tab
- O usuário clica nele
focus: true apenas enquanto o prompt está vazio e nada mais tem foco do teclado. Um painel que abre enquanto o usuário está digitando não toma seus pressionamentos de tecla.
O que cada tecla faz
Esta tabela lista o que uma tecla faz enquanto seu painel ou faixa tem foco do teclado:
Um mod não pode vincular Tab ou as setas para nada mais, então um jogo direciona com
w, a, s e d.
Definir um atalho de teclado e o primeiro foco
Duas propriedades em um controle decidem como o teclado o alcança:hotkey: para deixar o usuário pressionar umButtoncom uma tecla, dê a ele umhotkeyde um dígito ou uma letra minúscula, como emhotkey: 'a'autoFocus: para escolher qual controle tem o foco quando o painel abre, adicioneautoFocus: truea ele. Deixe a propriedade de fora dos outros, porque o Claude Code recusaautoFocus: false.
No terminal, nomeie a tecla no rótulo de um botão entre colchetes, ou use
plain: true, para que o usuário possa ver o que pressionar. A referência de elementos tem as outras regras de Button: action, atalhos de teclado de dígito na faixa e dois botões em um atalho de teclado.
Tomar entrada digitada e desenhar uma linha para cada item
Muitos painéis são um campo de texto com uma lista sob ele. O exemplo nesta seção é um painel de notas: você digita uma nota e pressiona Enter para adicioná-la, e cada nota tem um botãox que a deleta. Com duas notas adicionadas, o terminal desenha o painel desta forma:
- Tomar entrada digitada: um
InputchamaonSubmit(value)com o texto do campo quando o usuário pressiona Enter, eonInput(value)em cada mudança - Desenhar uma lista: mapeie seus dados para uma linha cada, e dê a cada botão de linha sua própria
key
- Adicionar uma nota: digite uma linha e pressione Enter. A linha aparece como uma nova linha e o campo esvazia.
- Deletar uma nota: pressione Tab até o botão
xda nota ter o foco, depois pressione Enter. Oxé o rótulo do botão e não um atalho de teclado, então digitar a letra não o pressiona.
hello-tabs: o callback altera notes, chama redraw e salva a lista em $.store.
O campo esvazia após cada envio por causa de sua propriedade value. value é o texto que o campo contém quando é desenhado, e a digitação do usuário o substitui até seu hook desenhar o campo novamente. O exemplo sempre desenha o campo com ''.
O exemplo salva as notas e não as carrega. Para trazê-las de volta na próxima sessão, leia-as em um hook session.start, da forma que hello-tabs lê count.
Três propriedades compõem a linha do campo, Note: Type a note and press Enter ⏎ add:
Enviar um
Input não inicia uma volta a menos que seu callback chame $.prompt.submit.
Redesenhar um site
Um desenho é um instantâneo: mostra o que seu hookui.render retornou a última vez que o hook foi executado. Para mostrar algo novo, o hook tem que ser executado novamente. O Claude Code o executa novamente para algumas mudanças, e seu mod pede o resto.
Quando o Claude Code redesenha sem ser solicitado
O Claude Code executa seu hookui.render novamente quando as propriedades do site mudam ou a largura do terminal muda. Ele não executa o hook em um timer, e não pode dizer quando uma variável em seu módulo muda.
Redesenhar quando seus dados mudam
Para ter seus sites desenhados novamente após suas próprias mudanças de dados, chame$.ui.invalidate('ui.render'). Este painel conta pressionamentos. O callback do botão altera count, depois pede um redesenho:
hello-tabs envolve a mesma chamada em sua função redraw.
Um valor que você mantém em $.state não precisa da chamada, porque escrever o valor redesenha os sites que o leem.
Redesenhar em um timer
Para manter um relógio, uma contagem regressiva ou um valor de fora da sessão atual, redesenhe em um cronograma. Inicie um timer no hooksession.start do módulo. Se o módulo já tiver um, como hello-tabs tem, adicione a linha $.clock.every a ele:
ui.render uma vez por segundo. O timer para quando o módulo recarrega, e a nova cópia do módulo inicia o seu próprio.
Com que frequência um site pode redesenhar
O Claude Code limita com que frequência redesenha um site, então seu mod pode chamar$.ui.invalidate com a frequência que seus dados mudam. O painel visível e a faixa têm um limite mais alto do que outros sites, e a tabela de limites tem os números.
Chamadas que vêm mais rápido que o limite são combinadas em um redesenho. Esse redesenho executa seu hook uma vez, e o hook lê seus dados como estão naquele momento, então o valor mais recente mostra e os valores no meio não. Uma animação não pode ser executada mais rápido que o limite.
Manter estado
Um mod tem três lugares para manter um valor, e diferem em quanto tempo o valor dura: até o módulo recarregar, até a sessão terminar ou de uma sessão para a próxima. Escolha por quanto tempo o valor tem que durar:$.store.get(key) resolve para o valor ou undefined, e $.store.set(key, value) leva qualquer valor JSON.
Manter um valor em $.state
$.state mantém valores pela duração de uma sessão, e redesenha para você. É estado reativo: um hook ui.render que lê um valor se inscreve nele, então o Claude Code redesenha esse site cada vez que você escreve o valor, e você não chama $.ui.invalidate. Um valor em $.state também sobrevive a um recarregamento do módulo, o que uma variável não faz.
Para configurá-lo, declare seus valores, aponte seu manifesto para a declaração, depois defina e use cada valor. Os exemplos movem o count de hello-tabs para $.state.
Declarar os valores
Declare os valores em um arquivo de tipos. A chave externa é o nome do seu plugin, e cada entrada sob ela é um valor e seu tipo. Salve isto comohello-tabs/types/index.d.ts:
hello-tabs/types/index.d.ts
Apontar o manifesto para a declaração
Para deixarclaude plugin validate verificar seu código contra esse arquivo, adicione um campo types ao manifesto com seu caminho:
hello-tabs/.claude-plugin/plugin.json
Definir, ler e escrever um valor
Em seu módulo, defina cada valor com um padrão, leia-o enquanto desenha e escreva-o de um callback.atom nomeia um valor e seu padrão, read o retorna e update o escreve. Os três ajudantes chamam $.state.get e $.state.set para você:
ui.render leu count, o Claude Code executa o hook novamente cada vez que o botão o escreve.
Três regras se aplicam ao código:
- Escreva
pluginekeycomo strings literais:claude plugin validateas lê de sua fonte - Declare cada valor no arquivo de tipos: caso contrário a validação falha com
hello-tabs.count is not declared - Escreva de um callback ou hook de outro evento: um hook
ui.renderpode ler estado e não pode escrevê-lo, então escreva deonPress,onSubmitou um hook para outro evento
Alterar hello-tabs para usar $.state
Para mover count em hello-tabs para $.state, altere cada linha que o usa:
- No topo do módulo: adicione a linha
importe substitualet count = 0pela linhaatom - No hook
ui.render: adicione a linhareadantes detabButtone desenhe'Count: ' + nnoText - No botão Add one: substitua
onPresspelo da Salvar de mais de uma sessão, que salva a contagem bem como escreve-a - No hook
session.start: substitua as duas linhas que leemsavedpela chamadaloadCountde Carregar um valor salvo novamente após/clear
redraw para os botões de aba, porque tab ainda é uma variável.
Carregar um valor salvo novamente após /clear
Se seu mod copia um valor salvo de $.store para $.state em session.start, tem que copiá-lo novamente após /clear, /resume ou /branch. Esses comandos colocam cada valor $.state de volta ao seu padrão, e session.start não dispara novamente. classic.SessionStart dispara após cada um deles, com e.source definido como clear, resume ou fork, então copie o valor novamente em um hook nele. Caso contrário seu desenho mostra o padrão, e um callback que salva o valor $.state escreve o padrão sobre o que você armazenou.
Este código carrega count de ambos os hooks. Ele se baseia na versão $.state de hello-tabs, onde count é um atom e update é importado. Coloque loadCount acima de register e adicione a chamada loadCount ao hook session.start que você já tem. classic.SessionStart também dispara na inicialização e após compactação, que não redefine $.state, então o filtro em source mantém o hook aos três resets:
/clear e não 0, e o próximo pressionamento de Add one adiciona à contagem salva.
loadCount escreve o valor armazenado sobre o em $.state, e session.start dispara novamente cada vez que o módulo recarrega. Para manter o armazenamento de ficar para trás, salve em cada mudança, como o botão Add one faz.
Para verificar o recarregamento sem uma sessão, teste o desenho após /clear.
Salvar de mais de uma sessão
Cada sessão em sua máquina que executa seu mod compartilha um$.store. Um get seguido por um set não é atômico. Quando duas sessões cada uma lê um valor, o altera e o escreve de volta, elas correm, e a segunda escrita substitui a primeira.
Duas escolhas tornam isso menos provável:
- Dê a cada item sua própria chave: um
setaltera apenas sua própria chave, então sessões que escrevem chaves diferentes não sobrescrevem uma à outra - Leia novamente logo antes de escrever: para um valor que várias sessões alteram,
geta chave no callback e construa o novo valor a partir disso, não de uma cópia que você carregou emsession.start. Outra escrita de sessão ainda é perdida se cair entre seugete seuset.
Próximos passos
- Reagir a eventos: alimente seu desenho de chamadas de ferramenta e voltas
- Use a API de mods: alimente seu desenho de timers e chamadas de modelo
- Teste um desenho: pressione seus botões de um teste, em mais de uma superfície
- Sites de renderização e elementos: propriedades de cada site e propriedades de cada elemento