brainless: la CLI degli agent come componenti React
Claude Code, Codex e Grok smontati in componenti: la UI da terminale perfetta per i tuoi progetti Next.js
6 min di lettura
Il terminale sul web si fa sempre nello stesso modo sbagliato
Il momento è quello in cui decidi che il tuo prodotto deve avere l'aria del terminale. Una dashboard per sviluppatori con dentro una console, la pagina che deve mostrare un agent al lavoro, un tool interno che parla la lingua di chi lo usa. Apri l'editor, e ti accorgi che le strade sono tre, e nessuna è buona.
La prima è la PNG. Costa dieci minuti e invecchia in due settimane: cambia una label nella CLI e l'immagine mente. Non si seleziona, non si cerca con Ctrl+F, non si legge con uno screen reader, e su un display retina o è sfocata o pesa mezzo megabyte.
La seconda è xterm.js. È un emulatore di terminale
vero, e per questo caso è un cannone: ti porta in casa un parser ANSI, un
modello di buffer e un canvas, quando quello che volevi era un componente React
che assomiglia a Claude Code.
La terza è rifarlo a mano. Sembra facile finché non ci provi. Il monospace giusto, il caret che lampeggia alla velocità giusta, lo spinner che non è la solita rotellina, e i colori di un tema che hai davanti agli occhi tutti i giorni e non sapresti citare a memoria. Due giorni di lavoro, e il risultato è ancora "quasi".
brainless è la quarta strada, ed è quella che non mi aspettavo: le interfacce di Claude Code, Codex e Grok smontate in componenti React, distribuite come registry di shadcn. Quaranta pezzi in tutto, trentaquattro componenti e sei blocchi già montati, licenza MIT.
La somiglianza non è a occhio. Il repository ha un harness sotto
tools/capture/ che pilota gli agent dentro tmux e salva i frame come ANSI,
HTML e testo, e i componenti sono costruiti contro quelle catture.
Schermo interoCosa ho digitato e cosa è arrivato
L'ho installato in un progetto di prova, non in produzione. Volevo il costo reale invece del README.
Il primo comando aggancia il registry al progetto. È lo stesso meccanismo che usi per un registry qualsiasi, e non è specifico di brainless:
npx shadcn@latest registry add @brainless=https://brainless.swerdlow.dev/r/{name}.jsonDa lì in avanti il namespace @brainless vive nel tuo components.json accanto
agli altri:
{
"registries": {
"@brainless": "https://brainless.swerdlow.dev/r/{name}.json"
}
}Poi ho chiesto il blocco più grosso, la sessione completa di Claude Code:
npx shadcn@latest add @brainless/claude-session✔ Created 9 files:
- src/components/brainless/claude/claude-header.tsx
- src/components/brainless/claude/claude-message.tsx
- src/components/brainless/claude/claude-todo-list.tsx
- src/components/brainless/claude/claude-tool-call.tsx
- src/components/brainless/claude/claude-diff.tsx
- src/components/brainless/claude/claude-permission.tsx
- src/components/brainless/claude/claude-thinking.tsx
- src/components/brainless/claude/claude-prompt.tsx
- src/components/brainless/blocks/claude-session.tsxNovecentoquarantatré righe in tutto, e il package.json non si è mosso di una
riga. Nessuna dipendenza npm nuova: i file importano React e la cn del
progetto, niente altro. Il progetto di prova aveva l'alias utils puntato su
@/utils/cn invece del solito @/lib/utils, e la CLI ha riscritto gli import
di conseguenza, senza che dovessi toccare niente.
Il blocco è il pezzo grosso, ma non sei obbligato a prenderlo. Se ti serve solo la riga di messaggio di Grok, chiedi quella e arriva un file:
npx shadcn@latest add @brainless/grok-messageQuesta è la differenza pratica con una UI library installata da npm. Non stai scaricando un catalogo per usarne tre pezzi: stai chiedendo tre pezzi. È lo stesso motivo per cui in questo sito le icone Lucide animate sono arrivate una alla volta invece che tutte insieme.
Fra i nove file della sessione, solo tre hanno "use client": il prompt, la
riga di thinking e il pannello dei permessi, cioè quelli che hanno davvero un
input o un timer. Gli altri sei restano Server Components e non finiscono nel
bundle.
I componenti sono già WCAG-ready
Non è finto HTML che assomiglia a un terminale. Sotto ci sono gli elementi giusti:
- l'header di Claude Code è un
<fieldset>con un<legend>, non un div con un::beforeche finge il bordo interrotto; - la chiamata a un tool collassata è un
<details>vero, quindi si apre con Invio ed entra nel tab order da sola; - i glifi decorativi sono
aria-hidden; - la riga di lavorazione è un
role="status"conaria-live; - il testo sotto l'onda di luce usa
background-clip: text, quindi resta testo selezionabile invece di diventare un'immagine.
E l'animazione si spegne da sola:
const GLYPHS = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"];
React.useEffect(() => {
if (!running || prefersReduced) return;
const id = setInterval(() => setGlyph((g) => (g + 1) % GLYPHS.length), 110);
return () => clearInterval(id);
}, [running, prefersReduced]);prefers-reduced-motion non è gestito a metà: il setInterval non parte
proprio, invece di girare a vuoto sotto un'animazione ferma. E i centodieci
millisecondi non sono un'ipotesi, vengono dalle catture.
È codice tuo, non un tema da combattere
Con una UI library classica, prendi MUI, la personalizzazione è una
negoziazione. Scrivi un tema, scopri che quel componente legge un token che non
avevi previsto, finisci a specializzare selettori o a passare sx ovunque, e
ogni major ti rimette in discussione il lavoro. Stai chiedendo il permesso a
codice che non è tuo.
Un registry shadcn ribalta il rapporto. Il codice atterra dentro il tuo repository, in chiaro, e da quel momento è tuo esattamente come il resto: lo apri, lo cambi, lo rinomini, ne butti metà. Non c'è una versione che ti passa sotto ai piedi, perché non c'è nessuna versione.
Su brainless questo si vede subito, perché i colori sono scritti come valori hex dentro i file:
const CLAUDE = "#cd694a"; // terracotta base
const HILITE = "#e79475"; // the highlight the wave carries
const DIM = "#7d7d7d";Nel blocco della sessione ne ho contati sessantuno, ventotto distinti, e nessuna
var(--...). Ha senso: l'obiettivo dichiarato è la fedeltà alla CLI originale,
e un colore che segue il tuo tema smetterebbe di essere quello di Claude Code.
Se il tuo prodotto vuole il feel del terminale ma con i tuoi colori, quelle
costanti sono in cima al file e stanno lì per essere cambiate.
Vale anche per i contenuti, con un'avvertenza che va detta subito:
ClaudeSession è una schermata già scritta, senza props e senza children. Serve
a vedere il risultato in due minuti, non a costruirci sopra. La tua sessione la
componi con i pezzi singoli, che le props ce le hanno:
<ClaudeHeader user="Andrea" cwd="~/dev/acme-app" model="Fable 5 · Claude Max" />
<ClaudeMessage role="user">aggiungi il blocco pricing alla landing</ClaudeMessage>
<ClaudeToolCall
tool="Bash"
arg="bunx shadcn add brainless/pricing"
result="Added 1 block · 2 files"
/>
<ClaudeThinking />
<ClaudePrompt mode="plan" effort="high" />Dove lo userei
Su un progetto personale, prima di tutto. Un portfolio più tecnico, rivolto a persone tecniche, dove la homepage non è un hero con una foto ma una sessione che racconta cosa fai nel linguaggio di chi legge. Chi arriva da un repository o da un thread capisce dove è finito nel primo secondo, senza leggere una riga di copy.
E poi nelle dashboard che hanno un agent dentro. Se il tuo prodotto ha una parte che parla con un modello, e la stai disegnando come una chat con le bolle, stai dicendo all'utente che è un giocattolo. Una CLI dice il contrario: dice che sotto c'è un processo, con dei tool, dei permessi da concedere e un diff da approvare. brainless ti dà quel vocabolario già montato, e a quel punto il lavoro tuo torna a essere quello vero, cioè far funzionare l'agent.
Hai un progetto in mente?
Costruisco software per aziende e startup dal 2018. Se sul tuo prodotto serve una mano, scrivimi: nel peggiore dei casi ti porti a casa un'opinione gratis.
ParliamoneDa leggere dopo
Shippare veloce è la parte facile
Perché l'illusione di chiudere 10 PR al giorno sta distruggendo la qualità del codice
6 min di lettura
HyperFrames: il video della feature lo scrivi in HTML
Motion graphics scritte da un prompt: roba da studio di produzione, senza lo studio di produzione
7 min di lettura