claude-code.d.ts do Claude Code 2.1.289 e na referência oficial (seção "Drawing: ui.render"), em 05/10/2026. A tabela de elementos por superfície é a parte que mais muda entre versões: confira o tipo Elements da sua instalação.
Entenda o que o ui.render recebe
Um hook em ui.render recebe uma instância de um componente por vez: a faixa acima do prompt, um painel, a linha de saída de um comando, uma mensagem do modelo. Ele devolve uma árvore de elementos, ou passa adiante com next.
O e de um ui.render tem cinco campos que você vai usar sempre. O matcher filtra por eles: { component: 'Pane', requestId: 'meu-painel' } faz o hook acordar só para o seu painel.
🆕 Novo aqui? Três palavras do desenho
- Componente — o lugar da tela que está sendo desenhado. Os nomes estão no tipo
RenderComponent:AbovePrompt,Pane,CommandOutput,UserMessage,ToolUse,Spinnere outros. - Superfície — onde o desenho aparece:
terminal,desktop(o app Claude Code Desktop),mobile(o app do celular) ouvscode. - requestId — qual instância: o id do painel, o id da mensagem, o
tool_use_idde uma linha de ferramenta. É o endereço que$.ui.blite$.ui.focususam depois.
Campo de e | O que diz | Para que serve |
|---|---|---|
e.component | qual componente | matcher; um hook pode atender vários |
e.surface | terminal, desktop, mobile ou vscode | escolher o que desenhar em cada uma |
e.requestId | qual instância | separar o seu painel dos outros |
e.props | os dados do componente, como dado simples | bodyColumns, hasSurvey, text... |
e.viewport | columns, rows e, quando a superfície diz, isFullscreen | saber o tamanho total; pode faltar |
O que olhar na tabela: o e.viewport é opcional (use e.viewport?.rows ?? 24, como o exemplo oficial do painel). E para a faixa e o painel, a largura certa não é viewport.columns: é e.props.bodyColumns, a caixa que sobra sem as marcas do engine.
Como ler o desenho: o cartão da esquerda é o e que chega. O hook olha esses campos e escolhe um dos dois caminhos: devolve uma árvore própria (seta azul) ou passa a vez (seta cinza). Existe um terceiro caminho, next({ ...e, props }), que muda os dados antes de o engine desenhar.
qual lugar
qual tela
qual instância
tamanho, se houver
Pegue os elementos com ui.resolve
O módulo do mod não tem Box nem Text globais. Quem entrega os elementos é $.ui.resolve(e): uma tabela de construtores da superfície em que aquele e vai ser desenhado.
Você desestrutura o que precisa e usa como tag JSX. Não é uma chamada ao engine: o engine já resolveu a tabela quando os plugins carregaram. Por isso pode ser chamada no começo de todo render, sem custo.
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
const { Box, Text } = $.ui.resolve(e)
const list = await read($, calls)
const room = Math.max(1, (e.viewport?.rows ?? 24) - 4)
return (
<Box flexDirection="column">
{list.length === 0 && <Text dimColor>No tool calls yet.</Text>}
{list.slice(-room).map(call => (
<Text dimColor={call.isDone}>
{call.isDone ? 'done' : 'runs'} {call.tool}
</Text>
))}
</Box>
)
})
$.ui.resolve(e). Repare também que este hook não recebe next: o painel é dele, não há o que passar adiante.Resolva a tabela
const { Box, Text, Button } = $.ui.resolve(e). Peça só o que vai usar.
Leia o estado
await read($, calls): a leitura inscreve este desenho para redesenhar quando o valor mudar (tópico 4).
Meça o espaço
Linhas pelo viewport, colunas pelo e.props.bodyColumns no painel e na faixa.
Devolva a árvore
JSX com h como fábrica. No .mjs do kit, sem JSX, a mesma árvore é Box({ flexDirection: "column", children: [...] }).
🆕 Novo aqui? Elements
Elements é o tipo que lista, por superfície, os construtores que $.ui.resolve devolve. Quando você estreita o e.surface (if (e.surface === 'terminal')), o TypeScript estreita a tabela junto: Raster só aparece dentro desse if.
Respeite as diferenças entre superfícies
Cada superfície pede o desenho por conta própria. O terminal desenha a árvore inteira com Ink; as outras recebem a árvore pela rede e desenham do jeito delas. Por isso cada uma tem sua tabela de elementos.
E nem todo componente existe em toda superfície: a faixa AbovePrompt só é pedida no terminal e no desktop; o Pane é pedido em todas.
| Elemento | terminal | desktop | mobile | vscode |
|---|---|---|---|---|
| Box, Text, Button, Link, Code, Markdown | ✓ | ✓ | ✓ | ✓ |
| Input, Select | ✓ | ✓ | ✗ | ✓ |
| Client | ✓ | ✓ | ✗ | ✗ |
| Svg | ✗ | ✓ | ✓ | ✓ |
| Raster, Image | ✓ | ✗ | ✗ | ✗ |
O que olhar na tabela: o básico (Box, Text, Button) funciona em todo lugar. O resto exige um if (e.surface === ...). Um Svg devolvido ao terminal é uma árvore que não valida (tópico 5).
if (e.surface === "desktop") {
const { Svg } = $.ui.resolve(e);
// ... desenha o bicho como SVG animado
}
const fits = e.surface === "terminal" && columns >= COLS + 2 && (e.props.maxRows || 0) >= ROWS + 1;
if (!fits) {
stopTicker();
return stack(fallbackLine(Text, prefs, mood, stats, fill));
}
const { Raster } = $.ui.resolve(e);
Svg. Terminal com espaço: Raster. Qualquer outro caso: uma linha de texto. Nunca uma superfície sem nada.✓ Faça
- ✓ Estreite
e.surfaceantes de pedir Svg, Raster ou Image - ✓ Tenha uma versão só com Box e Text para o resto
- ✓ Teste em terminal e desktop (módulo 3.2)
✗ Evite
- ✗ Supor que a pessoa usa o terminal
- ✗
Inputnum painel que precisa abrir no celular - ✗ Contar com a faixa no VS Code ou no celular
Desenhe do estado, nunca escreva ao desenhar
O render é uma leitura. Ele lê o $.state (direto ou com read) e devolve uma árvore. A leitura feita durante o desenho inscreve aquela instância: quando alguém grava um valor novo, o engine redesenha exatamente quem leu. Ninguém precisa pedir redesenho.
Quem escreve é outro: um evento (tool.call, turn.complete) ou um handler de botão. Uma escrita feita dentro do render é recusada. Você viu o porquê do estado em $.state no módulo 2.3: variável de módulo some no hot-reload.
const isHidden = atom({ plugin: 'turn-band', key: 'isHidden' } as const, false)
// ...
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const turn = await read($, last)
// ...
<Button
key="hide"
label="Hide"
onPress={() => update($, isHidden, () => true)}
/>
read). A escrita (update) mora dentro do onPress, que roda depois, quando a pessoa aperta. Aí o render volta sozinho, porque leu isHidden.✓ No render
- ✓
read($, atom)ou$.state.get(ref) - ✓
$.clock.now()para um relógio - ✓ Montar closures de botão que escrevem depois
✗ No render
- ✗
$.state.setouupdatedireto: negado - ✗ Ler de uma variável de módulo que o reload zera
- ✗ Escrever a partir de um valor capturado no desenho
⚠️ O valor capturado engana
onPress: () => $.state.set(ref, n + 1), com n lido no desenho, perde cliques: dois toques antes do redesenho gravam o mesmo número. Use update($, ref, n => (n ?? 0) + 1), que lê, aplica e grava com ifVersion e tenta de novo se perder a corrida.
💡 E quando o estado não é do $.state?
Um relógio que conta minutos não muda nenhum valor guardado. Nesse caso o mod pede $.ui.invalidate('ui.render') de um timer ($.clock.every), como o terminal-pet faz quando o humor do bicho expira.
Saiba o que acontece quando a árvore não valida
Uma árvore não valida quando usa um elemento que a superfície não tem, uma prop que o elemento não aceita, ou um filho onde não cabe filho. O engine não quebra a sessão: desenha a dele no lugar e anota o motivo.
Por isso um mod com erro de desenho parece "não fazer nada". O motivo está escrito; você só precisa saber onde olhar.
O hook devolve a árvore
Por exemplo, um Svg no terminal.
O engine valida e recusa
No debug log entra uma linha que começa com ui.render (<Component>): a hook returned a tree that does not validate, seguida do motivo.
Com hot-reload, a conversa também avisa
Uma vez por carga: <plugin>: ui.render (<Component>) refused: <reason>; the engine drew its own. Fora disso, só o debug log sabe.
O lugar fica com o desenho do engine
Onde o engine não desenha nada, nada sobra: a faixa fica vazia (a linha termina em nothing was drawn) e o painel é fechado (the pane was closed).
Como ler o desenho: o portão do meio confere três coisas. Passou, a sua árvore aparece. Não passou, a tela mostra o desenho padrão e o motivo vai para o log. O módulo 4.3 mostra como corrigir uma árvore recusada.
💡 Dois valores que não derrubam a árvore
Um Box com borderStyle que o terminal não conhece é desenhado sem borda. Um Code com format: 'diff' cujo texto não é um diff vira código simples. O resto da árvore aparece; o log avisa. Os estilos de borda válidos: single, double, round, bold, singleDouble, doubleSingle, classic, arrow, dashed, quote.
Use Raster e Image no terminal
O Rafa quer um mapa de calor do repositório: uma grade de quadradinhos coloridos. O instinto é um Box por célula. Não faça: uma grade colorida é um único Raster, com todas as células empacotadas numa string.
Para uma figura de verdade (PNG, quadro de vídeo) existe o Image. Os dois só existem no terminal.
🆕 Novo aqui? Raster e Image
- Raster —
key,columns(1 a 512),rows(1 a 256) ecells: base64 de trincas[codePoint, frente, fundo]em u32 little-endian, linha por linha. Cor0x00RRGGBB;0x01000000é a cor padrão do terminal. Cada caractere precisa ter largura 1 (blocos, bordas, braille). - Image —
source({ png },{ rgba, width, height }, ou o nome de um arquivo ou memória compartilhada),columns,rowsealt(obrigatório). Usa o protocolo gráfico do kitty onde o terminal tem (kitty, Ghostty); nos outros, aparece oalt. - $.ui.blit — repinta um Raster (ou troca a fonte de um Image) já montado, pelo
requestIde pelakey, sem passar pelo render.
Como ler o desenho: as duas grades mostram a mesma coisa. A da esquerda custa um elemento por célula; a da direita, um só. O repo-heatmap do kit vai além: dois "pixels" por célula com o meio bloco ▀ (frente = pixel de cima, fundo = pixel de baixo) e animação por $.ui.blit num $.clock.every.
⚠️ E no desktop?
O desktop não tem Raster. Para uma barra que funcione nas duas superfícies, use caracteres █░ num Text com cor, como o mapa-calor do inema-mods faz (pegadinha 20 do COMO-FAZER-UM-MOD). Ou estreite e.surface e mande um Svg ao desktop, como o terminal-pet.
Teste rápido (opcional): o Rafa quer, no painel, uma grade 40×10 com a cor de cada arquivo pela quantidade de edições. Ele usa terminal e desktop. O que faz sentido?
🎓 Resumo do módulo
Próximo módulo:
3.2 — Faixa, status e avisos