Pular para o conteúdo
MÓDULO 3.2

📟 Faixa, status e avisos

Quatro jeitos de mostrar algo sem abrir janela: a faixa acima do prompt, a linha de status, o toast e a linha no log. A faixa tem uma regra que derruba muito mod: ela é uma só para todos. Aqui o Rafa monta a dele sem apagar a dos outros.

6
Tópicos
~35
Minutos
Médio
Nível
Prático
Tipo
0 de 60%
Versão conferida: props da faixa e métodos de aviso conferidos no claude-code.d.ts do Claude Code 2.1.289, em 05/10/2026. O mod de exemplo deste módulo passou no claude plugin validate nessa versão.
1

Desenhe na faixa acima do prompt

A faixa é o componente AbovePrompt: a área logo acima de onde você digita, a mesma em que aparecem as pesquisas de satisfação. É o lugar de um medidor, um contador, um bichinho. Fica sempre à vista, sem tomar a conversa.

Ela só é pedida no terminal e no desktop. A pessoa pode recolher ([-] ou ctrl+x ctrl+a) e focar (um clique ou ctrl+x tab): aí um Input recebe texto e as hotkey dos botões passam a valer.

🆕 Novo aqui? As props da faixa

  • hasSurvey — uma pesquisa está usando a faixa. O mod cede: devolve next(e).
  • isWorking — um turno do modelo está rodando.
  • maxRows — quantas linhas cabem inteiras; uma árvore maior rola numa janela de scroll.bodyRows.
  • bodyColumns — a largura útil, já sem as cinco colunas do [-]. Dimensione por ela, não por viewport.columns.
> refatora o login ● Edit src/login.ts ● Edit src/sessao.ts minha-faixa: 2 edições registradas turno levou 130 s Rafa: 2 edição(ões) nesta sessão [-] > _ falhou: npm test $.ui.toast $.ui.log faixa (AbovePrompt) $.ui.status

Como ler o desenho: cada etiqueta à direita aponta para onde um jeito de avisar aparece. A faixa (roxa) fica colada no prompt e é desenhada por um hook ui.render. Os outros três são chamadas simples ao $.ui, sem hook de desenho.

2

Conviva com os outros mods

A faixa é uma instância só. Se o Rafa tem o medidor de contexto, o bichinho e o mod dele instalados, os três hooks recebem o mesmo AbovePrompt, em cadeia. Quem devolve uma árvore sem chamar next apaga o que viria dos outros.

A forma que convive (pegadinha 11 do COMO-FAZER-UM-MOD): const resto = await next(e) primeiro, depois uma coluna com a sua linha e o resto. Sem nada a mostrar, devolva o resto e saia.

mod do Rafaresto = await next(e) medidortambém chama next bichinhotambém chama next enginefaixa vazia ou pesquisa Rafa: 2 edição(ões) nesta sessão ◑ contexto 62% (=^.^=) feliz Rafa: 2 edição(ões) nesta sessão (medidor e bichinho sumiram) ✓ com next: três linhas ✗ sem next: só a sua

Como ler o desenho: a fila de cima é a cadeia. Cada mod que chama next recebe o que os de baixo desenharam e acrescenta o seu. À esquerda, a faixa que todo mundo quer; à direita, o que acontece quando um mod responde sozinho.

🎯 Objetivo: uma faixa que mostra as edições da sessão e convive com as outras

Crie a pasta minha-faixa/ com estes quatro arquivos (o caminho de cada um está no comentário):

// minha-faixa/.claude-plugin/plugin.json
{
  "name": "minha-faixa",
  "version": "0.1.0",
  "description": "Faixa do Rafa: quantas edições nesta sessão",
  "author": { "name": "Rafa" },
  "types": "./types/index.d.ts"
}

// minha-faixa/hooks/hooks.json
{ "modules": ["./register.tsx"] }

// minha-faixa/types/index.d.ts
export type Contagem = number

declare module 'claude-code' {
  interface PluginState {
    'minha-faixa': { editados: number }
  }
}

// minha-faixa/hooks/register.tsx
import { atom, read, update } from 'claude-code'
import type { Register } from 'claude-code'

const editados = atom({ plugin: 'minha-faixa', key: 'editados' } as const, 0)

export const register: Register = on => {
  on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
    const ran = await next(e)
    if (ran.deny === undefined && ran.isError !== true) await update($, editados, n => n + 1)
    return ran
  })

  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
    const resto = await next(e)
    const n = await read($, editados)
    if (e.props.hasSurvey || n === 0) return resto
    const { Box, Text } = $.ui.resolve(e)
    return (
      <Box flexDirection="column">
        <Text dimColor>{`Rafa: ${n} edição(ões) nesta sessão`}</Text>
        {resto}
      </Box>
    )
  })
}
Como verificar: rode claude plugin validate minha-faixa. Rodado aqui (2.1.289): a lista mostra hooks: tool.call{tool=Edit}, ui.render{component=AbovePrompt} e termina em ✔ Validation passed. (Os comentários de caminho não fazem parte dos arquivos.)

✓ Convive

  • ✓ await next(e) antes de montar a sua linha
  • ✓ Coluna com a sua linha e o resto
  • ✓ Nada a mostrar ou pesquisa na faixa: return resto

✗ Atropela

  • ✗ Devolver <Box> sem ter chamado next
  • ✗ Ignorar hasSurvey e cobrir a pesquisa
  • ✗ Ocupar mais linhas do que maxRows sem precisar
3

Mostre status sem abrir nada

Nem todo aviso merece um hook de desenho. $.ui.status(texto) fixa uma linha de status do seu plugin embaixo do prompt, ao lado dos avisos fixos do engine. Fica lá até a próxima chamada trocar.

É uma linha por plugin. $.ui.status(undefined) apaga. Pode ser chamada de qualquer hook, sem esperar (devolve void), e não começa turno nem chega ao modelo.

📄 plugin-authoring/examples/tool-call.ts (referência oficial, trecho)
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  const ran = await next({ ...e, command: e.command.trim() })
  const hasFailed = ran.deny === undefined && ran.isError === true

  $.ui.status(hasFailed ? `failed: ${e.command.slice(0, 40)}` : undefined)

  return ran
})
O que olhar: o mesmo hook liga e desliga o status. Um Bash que falhou acende a linha; o próximo que der certo passa undefined e ela some.

💡 Status ou faixa?

Status é texto curto que não precisa de botão nem cor por trecho: "testes falharam", "modo gravação ligado". Se precisar de botão, gráfico ou mais de uma linha, é faixa. O Rafa usa status para "3 arquivos sem commit" e faixa para o contador com botão.

4

Avise com toast e log

$.ui.toast(texto) mostra uma caixinha por alguns segundos no canto de cima, com o nome do plugin. Some sozinha (4 segundos por padrão; mude com { timeoutMs }), some com um clique, fica enquanto o ponteiro estiver em cima.

$.ui.log(texto) acrescenta uma linha apagada na conversa, como um aviso do sistema, que o modelo não lê. Com { to: 'debug' } vai só para o debug log. Num claude -p ou no SDK a linha chega ao host como ui_log.

JeitoDuraFica na conversa?Bom para
Faixa (AbovePrompt)enquanto o hook desenharnãomedidor, contador com botão
$.ui.status até a próxima chamadanãoestado atual em uma linha
$.ui.toast alguns segundosnão"aconteceu agora"
$.ui.log para sempresim (o modelo não lê)registro que a pessoa vai rolar para ver

O que olhar na tabela: só o log deixa rastro. Se o Rafa quer saber depois "quando foi que o teste quebrou", é log. Se só quer um susto na hora, é toast.

✓ Vai para o log

  • ✓ "o teste quebrou às 14h"
  • ✓ arquivo apagado, comando negado
  • ✓ o que a pessoa vai procurar depois

✗ Não vai para o toast

  • ✗ aviso que não pode se perder
  • ✗ texto longo, de várias linhas
  • ✗ algo que precisa de botão (isso é faixa ou painel)

⚠️ Toast pode esperar e até se perder

Enquanto um painel aberto com holdToasts estiver à vista (de qualquer plugin), todo toast espera sem ser desenhado, com o relógio parado. Um aviso que não pode se perder vai para o log, não para o toast.

📟
Faixa

sempre à vista

📌
status

uma por plugin

🔔
toast

some sozinho

📜
log

fica na conversa

5

Disseque o exemplo oficial da faixa

A referência que vem com o Claude Code traz examples/band.tsx: uma faixa que mostra quanto durou o último turno e quantas ferramentas ele chamou, com um botão para esconder. São 61 linhas e usam quase tudo deste módulo.

Leia o render com atenção: ele tem uma diferença em relação à forma do tópico 2.

📄 plugin-authoring/examples/band.tsx (referência oficial, trecho)
on('turn.complete', async ($, e, next) => {
  const seconds = Math.round(((await $.clock.now()) - startedAt) / 1000)
  const turn: Turn = { seconds, tools }
  await update($, last, () => turn)

  if (seconds > 120) {
    $.ui.toast(`That turn took ${seconds}s`)
  }

  return next(e)
})

on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
  const turn = await read($, last)
  const isQuiet = e.props.hasSurvey || turn === null || (await read($, isHidden))

  if (isQuiet) {
    return next(e)
  }

  const { Box, Button, Text } = $.ui.resolve(e)

  return (
    <Box>
      <Text dimColor>
        Last turn: {turn.seconds}s, {turn.tools} tool calls{' '}
      </Text>
      <Button
        key="hide"
        label="Hide"
        onPress={() => update($, isHidden, () => true)}
      />
    </Box>
  )
})
O que olhar: quando tem algo a mostrar, o render devolve o próprio <Box> e não chama next. Sozinho funciona; ao lado de outra faixa, apaga a outra.
1

Eventos escrevem

prompt.submit marca a hora, tool.call conta, turn.complete grava o resumo com update.

2

Turno longo vira toast

Acima de 120 s, um $.ui.toast. Avisa na hora e não ocupa a faixa.

3

O render cede quando deve

Pesquisa na faixa, nada gravado ou escondido: return next(e).

4

O botão escreve pelo handler

onPress com update; a faixa redesenha porque leu isHidden.

💡 Para conviver, mude duas linhas

Troque o começo por const resto = await next(e), o return next(e) por return resto, e ponha {resto} dentro de um <Box flexDirection="column"> junto com a sua linha. É o que o context-meter do kit da Prompt Advisers faz em .mjs: const original = await next(e) e depois Box({ flexDirection: "column", children: [original, meter] }).

6

Teste a faixa no terminal e no desktop

A faixa existe em duas superfícies, e cada uma tem sua tabela de elementos. A referência oficial pede: escreva o corpo do teste uma vez e rode em ['terminal', 'desktop'] as const. Assim o teste prova que o mod não depende de uma superfície.

O teste monta o componente com o mount do noun ui do kit de testes, passando { plugin, surface, component, props }, e procura o texto na árvore. O módulo 4.2 ensina o teste inteiro; aqui fica o esqueleto e o teste ao vivo.

📄 Esqueleto (forma usada em inema-mods/mods/clima-contexto/tests/clima.test.ts)
for (const surface of ['terminal', 'desktop'] as const) {
  // 1. monte a faixa nesta superfície (mount do noun ui do teste)
  // 2. procure o Text pelo texto, não pela key (pegadinha 12)
  // 3. desmonte antes da próxima volta
}
O que olhar: o laço é a única coisa que muda entre um teste de uma superfície e um de duas. Se o render pedir Raster sem estreitar e.surface, a volta do desktop falha.
🎯 Objetivo: ver a faixa do Rafa ao vivo, ao lado das outras

Na pasta que contém minha-faixa/, abra uma sessão só com esse mod carregado:

claude --plugin-dir minha-faixa

Peça ao Claude para editar um arquivo qualquer de teste. Para o desktop, aponte CLAUDE_CODE_PLUGIN_DIRS=<caminho absoluto> para a mesma pasta (módulo 1.3).

Como verificar: resultado esperado: depois da primeira edição, a linha Rafa: 1 edição(ões) nesta sessão aparece acima do prompt. Com outro mod de faixa carregado junto (repita a flag --plugin-dir), as duas linhas aparecem.

Teste rápido (opcional): o Rafa instalou o mod dele e o medidor de contexto sumiu da faixa. O que é mais provável?

🎓 Resumo do módulo

✓
A faixa é AbovePrompt — terminal e desktop, largura em bodyColumns.
✓
Uma faixa para todos — resto = await next(e), sua linha + resto.
✓
$.ui.status fixa uma linha — undefined apaga.
✓
Toast passa, log fica — e o modelo não lê nenhum dos dois.
✓
Teste nas duas superfícies — laço em ['terminal', 'desktop'].

Próximo módulo:

3.3 — Painéis e botões