SLOPSHOPPER

ds-primeiro

Anexa a skill do seu design system ao primeiro pedido de peça visual da sessão. Configure o nome da skill para ligar.

newbandcommandtoastprompt
A shopper browsing a rack in a slop shop
README

claude-code-mods

Cinco mods para o Claude Code, escritos como plugins de function hooks. Desenham barras e faixas acima do prompt, avisam sobre cache e uso do plano, sobem um servidor de prévia para HTML e lembram o modelo de usar o seu design system.

Funcionam no terminal e na aba Code do app desktop. Textos e comandos estão em português.

Painel de progresso

Pré-requisitos

  • Claude Code recente, com suporte a function hooks.
  • A variável CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 no bloco env do ~/.claude/settings.json. Sem ela nenhum dos mods carrega.
{
  "env": {
    "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1"
  }
}

Se o arquivo já tem um bloco env, acrescente só a linha da variável.

Instalação

Num terminal com claude aberto, instale cada mod que quiser:

/plugin install progresso --marketplace Kelvi-Maycon/claude-code-mods
/plugin install barra-uso --marketplace Kelvi-Maycon/claude-code-mods
/plugin install cache-frio --marketplace Kelvi-Maycon/claude-code-mods
/plugin install previa --marketplace Kelvi-Maycon/claude-code-mods
/plugin install ds-primeiro --marketplace Kelvi-Maycon/claude-code-mods

Na primeira vez o Claude Code pergunta se quer adicionar o marketplace: responda y. Depois escolha o escopo do usuário (o primeiro da lista). O mod já roda na sessão atual e nas próximas.

Instalado no escopo do usuário pelo terminal, o mod carrega também na aba Code do app desktop. O comando /plugin install só existe no terminal.

Mods

progresso

Barras de progresso

Barras de progresso acima do prompt. O modelo recebe uma ferramenta (progresso) e uma regra curta no system prompt: em tarefa com vários passos, cria uma barra com o plano e a atualiza conforme avança. Cada barra mostra o passo ativo, a etapa, o percentual e o estado (andamento, esperando você, falhou, concluída).

  • Subagentes e comandos Bash em background aparecem como faixas embaixo da barra, com tipo, modelo, esforço, ferramenta em uso, custo estimado e tempo.
  • Painel lateral "Progresso" com custo, tokens, tempo, agentes rodando, concluídos, com falha e planejados. Clicar num agente abre a conversa dele, e dá para mandar mensagem a ele por ali.
  • Som ao concluir, ao falhar e quando o modelo espera uma decisão sua.
  • Se o turno vai terminar com uma barra aberta, o mod pede ao modelo que a feche antes (uma vez por barra).

Comandos:

  • /progresso: mostra ou esconde as barras. Aceita on, off ou painel (abre o painel lateral).
  • /progresso-demo: demonstração de 30 segundos com todos os estados, faixas, painel e sons.
  • /progresso-limpar: remove todas as barras e faixas.

Ícones por tipo de agente: executor-leve, executor, executor-pesado, executor-design, investigador e leitor têm robôs próprios. Explore usa a lupa do investigador. general-purpose, Plan, claude, fork e qualquer outro nome usam o robô padrão, com o nome do tipo em cinza. Modelo e esforço vêm do frontmatter (name, model, effort) dos arquivos em ~/.claude/agents/.

Um passo cujo título termina com o tipo do agente entre parênteses, como Revisar tudo (executor-pesado), aparece no painel como agente planejado.

Ícones

barra-uso

Uma linha de pílulas acima do campo:

  • 5H e 7D: uso do plano nas janelas de 5 horas e 7 dias, com o tempo até renovar.
  • CTX: contexto usado até a compactação automática, pelo limite que o Claude Code informa (275k quando ele não informa nenhum).
  • CACHE: tempo restante do cache de prompt (TTL de 60 minutos, fixo) e taxa de acerto.
  • DS on/off: só aparece com o ds-primeiro configurado. Clicar alterna o DS nesta sessão.

Comando: /uso, com on ou off.

cache-frio

Quando a sessão ficou parada além do TTL do cache e o contexto passa de 100k tokens, o próximo envio regrava o cache inteiro e custa mais. O mod segura esse envio uma vez, devolve o texto ao campo e avisa. Enviar de novo dentro de 3 minutos passa direto. Comandos de barra (/compact, /clear) nunca são segurados.

Comando: /cache-frio mostra contexto e tempo de cache. /cache-frio ttl <minutos> ajusta o TTL (padrão 60), /cache-frio off e /cache-frio on desligam e ligam o aviso.

previa

Quando o modelo grava ou edita um .html, o mod sobe um python3 -m http.server na pasta do arquivo, a partir da porta 8765, e mostra a URL acima do prompt com botões para abrir, copiar e parar. O modelo recebe uma linha dizendo que o servidor já está no ar, para não abrir outro. Comandos Bash que matariam o servidor (kill, pkill, killall no processo dele) são negados.

Arquivos em /tmp e /var/folders não viram prévia.

Comando: /previa <caminho> serve uma pasta, /previa status lista os servidores, /previa parar encerra.

Requisitos: python3 e lsof no PATH. O botão Abrir usa o comando open do macOS; em Linux ele não faz nada e o botão de copiar continua funcionando.

ds-primeiro

No primeiro pedido de peça visual da sessão (página, landing, dashboard, slide, tela, componente...), anexa ao contexto uma linha pedindo que o modelo carregue a skill do seu design system antes de criar. Antes do envio aparece uma faixa avisando, com o botão "Tirar deste envio". A detecção é por verbos e alvos em português.

Não anexa quando o pedido já cita um design system, uma identidade visual, a sua skill, o rótulo ou um dos termos extras. Depois de compactar ou de /clear, volta a anexar no próximo pedido visual.

Sem configuração o mod não faz nada: não anexa, não mostra faixa e não registra comando. Para ligar, configure as opções do plugin. A tela de opções aparece na instalação, e depois fica em /config:

OpçãoO que éExemplo
skillNome da skill do design system. Vazio desliga o mod.meu-design-system
rotuloNome exibido na faixa, no toast e na linha anexada. Vazio usa o nome da skill.Minha Marca
ignorarTermos extras, separados por vírgula, que dispensam a linha quando citados.marca do cliente, sem ds

As opções ficam salvas no ~/.claude/settings.json, em pluginConfigs.

Comando: /ds mostra o estado. /ds on e /ds off valem para a sessão; /ds sempre on e /ds sempre off mudam o padrão.

Atualizar

claude plugin update <mod>@claude-code-mods

Depois, numa sessão aberta, rode /reload-plugins.

Desinstalar

claude plugin uninstall <mod>@claude-code-mods

Desenvolvimento

Cada plugin fica em plugins/<nome>/, com os hooks em hooks/ e os testes em tests/.

claude plugin validate plugins/progresso
claude plugin test plugins/progresso

O tsconfig.json de cada plugin estende .claude-plugin/types/tsconfig.json, que o Claude Code gera no primeiro carregamento do plugin. Num clone novo o editor e o tsc acusam tipos ausentes até esse primeiro carregamento (ou um claude plugin validate). A pasta gerada fica fora do git.

Os sons do progresso são gerados por plugins/progresso/sounds/gerar.py.

Créditos

A estrutura das barras do progresso foi portada do plan-progress (zycck/claude-mods), de Kirill Serditov, sob licença MIT. O texto da licença está em plugins/progresso/licenses/plan-progress-LICENSE.

Licença

MIT. Veja LICENSE.

Source 3 files
hooks/register.tsx 180 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { pedeDs } from './detecta'
5
6const anexado = atom({ plugin: 'ds-primeiro', key: 'anexado' } as const, false)
7const ligado = atom({ plugin: 'ds-primeiro', key: 'ligado' } as const, null)
8const pular = atom({ plugin: 'ds-primeiro', key: 'pular' } as const, false)
9const rascunho = atom({ plugin: 'ds-primeiro', key: 'rascunho' } as const, false)
10// O estado efetivo (`ligado ?? padrao`), publicado para outros mods lerem: `padrao` vive só neste módulo.
11const publicado = atom({ plugin: 'ds-primeiro', key: 'ativo' } as const, true)
12
13const USO = 'Use /ds, /ds on, /ds off, /ds sempre on ou /ds sempre off.'
14
15// Padrão guardado no store (`/ds sempre`) e o último resultado da detecção no
16// rascunho, para só gravar estado quando ele muda.
17let padrao = true
18let casa = false
19
20async function ativo($: EngineInterface) {
21  return (await read($, ligado)) ?? padrao
22}
23
24async function publica($: EngineInterface) {
25  const agora = await ativo($)
26  await update($, publicado, () => agora)
27}
28
29const texto = (valor: unknown) => (typeof valor === 'string' ? valor.trim() : '')
30
31export const register: Register = (on, options) => {
32  // Sem a skill do design system configurada o mod não faz nada: não anexa,
33  // não registra /ds, não desenha faixa e não publica `ativo` (a barra-uso
34  // esconde a pílula DS).
35  const skill = texto(options.skill)
36
37  if (skill === '') {
38    return
39  }
40
41  const rotulo = texto(options.rotulo) || skill
42  const termos = [skill, rotulo, ...texto(options.ignorar).split(',')]
43  const linha = `ds-primeiro: em peça visual o usuário usa por padrão o design system ${rotulo}. Carregue a skill ${skill} antes de criar, a menos que o pedido defina outra identidade. Se o pedido não for visual, ignore esta linha.`
44
45  on('session.start', async ($, e, next) => {
46    padrao = (await $.store.get('sempre')) !== false
47    await $.command.register({
48      name: 'ds',
49      description: `${rotulo} anexado ao primeiro pedido visual da sessão`,
50      argumentHint: '[on | off | sempre on | sempre off]',
51    })
52    await publica($)
53
54    return next(e)
55  })
56
57  on('command.run', { command: 'ds' }, async ($, e) => {
58    const args = e.args.trim().toLowerCase().split(/\s+/).join(' ')
59
60    if (args === 'on' || args === 'off' || args === 'sempre on' || args === 'sempre off') {
61      const liga = args.endsWith('on')
62
63      if (args.startsWith('sempre')) {
64        padrao = liga
65        await $.store.set('sempre', liga)
66      }
67
68      await update($, ligado, () => liga)
69      await publica($)
70
71      if (liga) {
72        await update($, anexado, () => false)
73      }
74    } else if (args !== '') {
75      return { text: USO }
76    }
77
78    const sessao = (await ativo($)) ? 'ligado nesta sessão' : 'desligado nesta sessão'
79    const linha = (await read($, anexado)) ? 'DS já anexado (/ds on anexa de novo)' : 'DS ainda não anexado'
80
81    return { text: `${sessao} · ${linha} · padrão ${padrao ? 'ligado' : 'desligado'}` }
82  })
83
84  on('prompt.edit', async ($, e, next) => {
85    const campo = await next(e)
86    const casaAgora = pedeDs(campo.text, termos)
87
88    if (casaAgora !== casa) {
89      casa = casaAgora
90      await update($, rascunho, () => casaAgora)
91    }
92
93    return campo
94  })
95
96  // O Enter do usuário: `composer` no terminal, `sdk` no app desktop.
97  on('prompt.submit', async ($, e, next) => {
98    if (e.origin.kind !== 'composer' && e.origin.kind !== 'sdk') {
99      return next(e)
100    }
101
102    const pulou = await read($, pular)
103    const anexa = !pulou && pedeDs(e.text, termos) && (await ativo($)) && !(await read($, anexado))
104    const entrou = await next(anexa ? { ...e, context: [...(e.context ?? []), linha] } : e)
105
106    // Um envio segurado por outro hook volta ao campo: nada muda até ele entrar.
107    if (entrou.drop !== undefined) {
108      return entrou
109    }
110
111    if (anexa) {
112      await update($, anexado, () => true)
113      $.ui.toast(`DS ${rotulo} anexado a este pedido · /ds off desliga`)
114    }
115
116    if (pulou) {
117      await update($, pular, () => false)
118    }
119
120    if (casa) {
121      casa = false
122      await update($, rascunho, () => false)
123    }
124
125    return entrou
126  })
127
128  // O DS carregado sai do contexto na compactação: rearma.
129  on('session.compact', async ($, e, next) => {
130    const feito = await next(e)
131
132    if (e.agentId === undefined && e.trigger !== 'precompute' && feito.skip === undefined) {
133      await update($, anexado, () => false)
134    }
135
136    return feito
137  })
138
139  on('session.end', async ($, e, next) => {
140    if (e.reason === 'clear' || e.reason === 'resume') {
141      casa = false
142      await update($, anexado, () => false)
143      await update($, ligado, () => null)
144      await update($, pular, () => false)
145      await update($, rascunho, () => false)
146      await publica($)
147    }
148
149    return next(e)
150  })
151
152  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
153    const mostra =
154      !e.props.hasSurvey &&
155      (await read($, rascunho)) &&
156      !(await read($, anexado)) &&
157      !(await read($, pular)) &&
158      (await ativo($))
159
160    const abaixo = await next(e)
161
162    if (!mostra) {
163      return abaixo
164    }
165
166    const { Box, Button, Text } = $.ui.resolve(e)
167
168    // Empilha sobre as faixas dos outros mods; a deste fica junto do campo.
169    return (
170      <Box flexDirection="column">
171        {abaixo}
172        <Box>
173          <Text dimColor>{`DS ${rotulo} será anexado a este pedido `}</Text>
174          <Button key="tirar" label="Tirar deste envio" onPress={() => update($, pular, () => true)} />
175        </Box>
176      </Box>
177    )
178  })
179}
180
hooks/detecta.ts 36 lines
1const VERBO =
2  /\b(?:cria|crie|criar|faz|faca|fazer|refaz|refaca|recria|monta|monte|constroi|construa|desenha|redesenha|melhora|melhore)\b/
3
4const ALVO =
5  /\b(?:relatorio visual|(?:pagina|landing|lp|site|html|dashboard|painel|visualizacao|slide|deck|layout|tela|hero|componente|blog|video|motion|ui|interface)s?|paineis|visualizacoes)\b/
6
7// O pedido já cita um design system, dispensa skill ou nomeia a identidade de outra marca.
8const FORA =
9  /design system|identidade visual|\bsem ds\b|nao use skill|\b(?:design|identidade|marca) d[ao] /
10
11// Caminho de arquivo com duas partes ou mais, entre aspas ou solto: uma pasta
12// com o nome do DS no meio de um caminho não é o usuário citando o DS. A skill
13// citada como /nome-da-skill tem uma parte só e fica.
14const CAMINHO = /(['"])~?\/[^'"]*\/[^'"]*\1|(?:^|\s)~?\/[^\s/]+\/\S*/g
15
16/** Minúsculas, sem acento: a forma em que o texto e os termos são comparados. */
17export function normaliza(texto: string): string {
18  return texto.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
19}
20
21/**
22 * Diz se o texto pede uma peça visual sem definir a identidade. `termos` são
23 * os nomes que, citados no pedido, dispensam a linha: a skill, o rótulo e os
24 * termos extras da configuração.
25 */
26export function pedeDs(texto: string, termos: readonly string[] = []): boolean {
27  const t = normaliza(texto).replace(CAMINHO, ' ')
28  const citaTermo = termos.some(termo => {
29    const n = normaliza(termo).trim()
30
31    return n !== '' && t.includes(n)
32  })
33
34  return VERBO.test(t) && ALVO.test(t) && !FORA.test(t) && !citaTermo
35}
36
types/index.d.ts 17 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'ds-primeiro': {
4      /** A linha do DS já entrou nesta sessão. */
5      anexado: boolean
6      /** `/ds on` ou `/ds off` nesta sessão; null segue o padrão guardado. */
7      ligado: boolean | null
8      /** "Tirar deste envio" foi pressionado para o rascunho atual. */
9      pular: boolean
10      /** O rascunho no campo casa a detecção. */
11      rascunho: boolean
12      /** Ligado nesta sessão (`/ds on|off`, ou o padrão de `/ds sempre`); a `barra-uso` lê. */
13      ativo: boolean
14    }
15  }
16}
17