Painel lateral (~50% do terminal, aberto sob comando /educar) com textos curtos que explicam as tecnologias escolhidas enquanto algo é desenvolvido.

Mod do Claude Code: um painel à direita (largura fixa, pensada para ~50% de um terminal comum — ajuste LARGURA_PAINEL em hooks/register.tsx se destoar do seu terminal) que mostra textos curtos explicando as tecnologias que estão sendo escolhidas enquanto algo é desenvolvido.
explicar (titulo, texto, tecnologia?) antes de decidir ou usar uma tecnologia./educar abre ou fecha o painel; /educar limpar apaga as notas.explicar vive na própria descrição da ferramenta; para reforçar em todos os projetos, uma instrução global complementar pode ser adicionada ao ~/.claude/CLAUDE.md do usuário./plugin marketplace add osceiatech/skills-osceia
/plugin install educa-lateral --marketplace osceiatech/skills-osceia
Para testar a partir da pasta, sem instalar:
claude --plugin-dir ~/.agents/skills/educa-lateral
claude plugin validate .
(claude plugin test . não existe nesta versão do CLI — tests/educa.test.ts usa uma API de teste de mods que não pôde ser confirmada como real; trate-a como não verificada até alguém confirmar o comando/API corretos.)
Tipos: tsc -p com o tsconfig.json da pasta, depois que o Claude Code carregar o mod uma vez (ele grava .claude-plugin/types/).
hooks/register.tsx 94 lines1import type { Register } from 'claude-code'
2
3import type { Nota } from '../types'
4
5const PANE = 'educa'
6const TOOL = 'explicar'
7// Largura fixa aproximando ~50% de um terminal comum; sem acesso à largura real no
8// momento da abertura, ajuste este valor se o painel ficar maior/menor que o esperado.
9const LARGURA_PAINEL = 60
10
11export const register: Register = on => {
12 let notas: Nota[] = []
13 let painelAberto = false
14 let proximoId = 1
15
16 on('session.start', async ($, e, next) => {
17 await $.tool.register({
18 name: 'explicar',
19 isDeferred: false,
20 description:
21 'Mostra no painel lateral um texto curto e didático (pt-BR) que explica uma tecnologia ou decisão técnica que está sendo usada agora. Chame esta ferramenta sempre que escolher ou usar uma tecnologia, biblioteca, padrão ou técnica relevante, antes de agir. Escreva em português, em 2 a 4 frases curtas: o que é, por que foi escolhida aqui e o que ela evita ou resolve. Uma nota por decisão; não repita o que já explicou. Nunca inclua dado pessoal, segredo ou conteúdo de registro nas notas.',
22 inputSchema: {
23 type: 'object',
24 properties: {
25 titulo: { type: 'string', description: 'Nome curto do assunto, ex.: "TanStack Query".' },
26 texto: { type: 'string', description: '2 a 4 frases: o que é, por que aqui, o que resolve.' },
27 tecnologia: { type: 'string', description: 'Categoria opcional, ex.: "estado de servidor".' },
28 },
29 required: ['titulo', 'texto'],
30 },
31 })
32 await $.command.register({
33 name: 'educar',
34 description: 'Abre ou fecha o painel lateral que explica as tecnologias em uso (/educar limpar apaga as notas)',
35 })
36
37 return next(e)
38 })
39
40 on('tool.call', { tool: TOOL }, async ($, e) => {
41 const entrada = e as unknown as { titulo?: string; texto?: string; tecnologia?: string }
42 const titulo = String(entrada.titulo ?? '').slice(0, 80)
43 const texto = String(entrada.texto ?? '').slice(0, 600)
44
45 if (titulo === '' || texto === '') {
46 return { deny: 'educa-lateral: informe titulo e texto.' }
47 }
48 notas = [...notas, { id: proximoId++, titulo, texto, tecnologia: entrada.tecnologia?.slice(0, 40) }].slice(-50)
49
50 return { result: 'Nota exibida no painel lateral.' }
51 })
52
53 on('command.run', { command: 'educar' }, async ($, e) => {
54 const args = String((e as unknown as { args?: string }).args ?? '').trim()
55
56 if (args === 'limpar') {
57 notas = []
58
59 return { text: 'Notas apagadas.' }
60 }
61 if (painelAberto) {
62 await $.ui.close({ id: PANE })
63 painelAberto = false
64
65 return { text: 'Painel fechado.' }
66 }
67 await $.ui.open({ id: PANE, title: 'Aprendendo', columns: LARGURA_PAINEL })
68 painelAberto = true
69
70 return { text: 'Painel aberto.' }
71 })
72
73 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
74 const { Box, Text } = $.ui.resolve(e)
75 const espaco = Math.max(1, 24 - 4)
76 const visiveis = notas.slice(-Math.max(1, Math.floor(espaco / 5)))
77
78 return (
79 <Box flexDirection="column">
80 {notas.length === 0 && (
81 <Text dimColor>As tecnologias escolhidas aparecerão aqui, com uma explicação curta de cada uma.</Text>
82 )}
83 {visiveis.map(n => (
84 <Box key={`nota-${n.id}`} flexDirection="column" marginBottom={1}>
85 <Text bold>{n.titulo}</Text>
86 {n.tecnologia && <Text dimColor>{n.tecnologia}</Text>}
87 <Text>{n.texto}</Text>
88 </Box>
89 ))}
90 </Box>
91 )
92 })
93}
94types/index.d.ts 2 lines1export type Nota = { id: number; titulo: string; texto: string; tecnologia?: string }
2