Skip to main content
Um mod pode desenhar sua própria interface no Claude Code e alterar partes da interface que o Claude Code já desenha. Cada lugar onde um mod pode desenhar é chamado de site de renderização, como um painel, a faixa acima do prompt ou o spinner. O Claude Code dispara o evento 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: Mapa de uma sessão de terminal Claude Code. Um mod pode adicionar um painel como uma barra lateral à direita, um toast no canto superior direito da transcrição, uma linha de log na transcrição, uma faixa acima do prompt e uma linha de status sob o prompt. Um mod pode redesenhar mensagens, linhas de chamada de ferramenta e o spinner. O prompt é do próprio Claude Code. Mapa de uma sessão de terminal Claude Code. Um mod pode adicionar um painel como uma barra lateral à direita, um toast no canto superior direito da transcrição, uma linha de log na transcrição, uma faixa acima do prompt e uma linha de status sob o prompt. Um mod pode redesenhar mensagens, linhas de chamada de ferramenta e o spinner. O prompt é do próprio Claude Code. Em um terminal mais estreito, o painel fica acima do prompt em vez de ao lado da transcrição. Construa seu primeiro mod antes de começar aqui. Comece com o exemplo trabalhado, que constrói um painel com duas abas e um contador, depois leia a seção para cada parte que você deseja alterar.
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:
O Claude Code não tem um elemento de abas integrado, então as abas são dois botões em uma linha. O mod acompanha qual está ativo e desenha o conteúdo dessa aba sob a linha.
1

Criar o plugin

Um mod é um plugin com um manifesto, um 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
Nomeie seu ponto de entrada em 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:
  • 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
Duas variáveis no nível do módulo, tab e count, mantêm o estado do painel.Salve isto como hello-tabs/hooks/register.js:
hello-tabs/hooks/register.js
Cada hook também faz algo que o código não deixa claro:
  • session.start também lê a contagem salva de $.store, um armazenamento de chave-valor que persiste entre sessões.
  • command.run apenas diz ao Claude Code que o painel existe. Abrir um painel não desenha nada por si só: o Claude Code então dispara ui.render para perguntar o que colocar nele.
  • ui.render retorna a árvore de elementos, uma Box que contém outras caixas, texto e botões, e a constrói novamente a partir de tab e count cada vez que é executada.
Pressionar um botão executa seu callback 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 hook ui.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:
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 hook ui.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.
Para manter o desenho do Claude Code e alterar uma parte dele, passe para next uma cópia do evento com props alteradas. Este hook altera o texto após a palavra do spinner:
O spinner mantém sua animação e sua palavra, e seu texto segue a palavra:
O prompt de permissão não é um site de renderização, então um mod não pode alterar o que mostra. O diálogo de pergunta, 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.
Para fechar o painel, chame $.ui.close com o id que você abriu com:
Além de 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.
Quando o painel aparece, $.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 hook ui.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 desenha uma string, com estilo opcional como bold e color:
Esta tabela lista cada elemento: 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 um Raster 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:
No terminal, o painel mostra a grade: Um painel no terminal que contém uma pequena grade de blocos coloridos, duas linhas de três. A linha superior é verde, âmbar e vermelho. A linha inferior é verde, verde e âmbar. O array 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: leva onPress(e), onde e.surface é o app de onde veio o pressionamento
  • Input: leva onSubmit(value) e onInput(value)
  • Select: leva onSelect(value) com suas escolhas em options, uma lista de pelo menos uma escolha com valores únicos, como [{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
Um teste pressiona ou digita em um controle por sua 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: true de um comando ou um pressionamento
  • O usuário pressiona Ctrl+X depois Tab
  • O usuário clica nele
O Claude Code concede 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 um Button com uma tecla, dê a ele um hotkey de um dígito ou uma letra minúscula, como em hotkey: 'a'
  • autoFocus: para escolher qual controle tem o foco quando o painel abre, adicione autoFocus: true a ele. Deixe a propriedade de fora dos outros, porque o Claude Code recusa autoFocus: false.
Como um atalho de teclado mostra depende do botão e do app: 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ão x que a deleta. Com duas notas adicionadas, o terminal desenha o painel desta forma:
O exemplo usa duas técnicas:
  • Tomar entrada digitada: um Input chama onSubmit(value) com o texto do campo quando o usuário pressiona Enter, e onInput(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
Este hook desenha o conteúdo do painel:
Para tentar o painel:
  • 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 x da nota ter o foco, depois pressione Enter. O x é o rótulo do botão e não um atalho de teclado, então digitar a letra não o pressiona.
Cada mudança segue o mesmo ciclo de renderização que 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 hook ui.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 hook ui.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:
Cada pressionamento levanta o número no painel. O exemplo 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 hook session.start do módulo. Se o módulo já tiver um, como hello-tabs tem, adicione a linha $.clock.every a ele:
O Claude Code agora executa seu hook 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 como hello-tabs/types/index.d.ts:
hello-tabs/types/index.d.ts

Apontar o manifesto para a declaração

Para deixar claude 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ê:
Porque o hook 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 plugin e key como strings literais: claude plugin validate as 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.render pode ler estado e não pode escrevê-lo, então escreva de onPress, onSubmit ou 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 import e substitua let count = 0 pela linha atom
  • No hook ui.render: adicione a linha read antes de tabButton e desenhe 'Count: ' + n no Text
  • No botão Add one: substitua onPress pelo 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 leem saved pela chamada loadCount de Carregar um valor salvo novamente após /clear
Mantenha 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:
Com ambos os hooks em vigor, o painel mostra a contagem salva após /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 set altera 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, get a chave no callback e construa o novo valor a partir disso, não de uma cópia que você carregou em session.start. Outra escrita de sessão ainda é perdida se cair entre seu get e seu set.
Este botão adiciona um ao que o armazenamento contém agora, depois atualiza o desenho:
Se uma segunda sessão pressionou seu próprio botão três vezes desde que esta sessão começou, este pressionamento mostra e salva uma contagem que inclui esses três.

Próximos passos