SLOPSHOPPER

model-picker

Pick the session's model and effort from the mods pane.

newtoast
v0.1.0no licenseupdated 2026-10-14Sales16/claude-mods/plugins/model-picker
A shopper browsing a rack in a slop shop
README

claude-mods

Mods para o Claude Code: plugins de function hooks que mudam a interface e o comportamento do próprio Claude Code.

ModO que faz
mods-hubBotão ◆ mods na linha do mode e janela lateral para ligar, desligar e configurar os mods.
status-inlineMedidores compactos (contexto, rate limits, modelo, custo, tokens) na linha do mode.
model-pickerTroca o modelo e o effort da sessão pela janela de mods.

<img width="1084" height="101" alt="image" src="https://github.com/user-attachments/assets/ac343e6d-1300-4469-9443-7b241f6e7b14" /> <img width="216" height="427" alt="image" src="https://github.com/user-attachments/assets/b03d8f45-afad-45ec-b53d-415e3e89bba8" />

Instalar

Em todos os terminais (recomendado)

Em qualquer sessão do Claude Code:

/plugin marketplace add Sales16/claude-mods
/plugin install mods-hub@claude-mods
/plugin install status-inline@claude-mods
/plugin install model-picker@claude-mods
  • Escolha o escopo user na instalação: os mods passam a carregar em todo terminal novo, em qualquer projeto.
  • Instale o mods-hub junto: o status-inline depende dele.

Para pegar uma versão nova do repositório:

/plugin marketplace update claude-mods
/reload-plugins

Para remover:

/plugin uninstall model-picker@claude-mods
/plugin uninstall status-inline@claude-mods
/plugin uninstall mods-hub@claude-mods

Direto das pastas, sem instalar

Útil enquanto você mexe nos mods: o terminal vigia as pastas e recarrega o mod sozinho quando um arquivo muda.

Clone o repositório primeiro. Os exemplos abaixo usam C:\MyProjects\claude-mods; troque pelo caminho do seu clone.

git clone https://github.com/Sales16/claude-mods.git C:\MyProjects\claude-mods

Para todos os terminais, em ~/.claude/settings.json:

{
  "env": {
    "CLAUDE_CODE_PLUGIN_DIRS": "C:\\MyProjects\\claude-mods\\plugins\\mods-hub;C:\\MyProjects\\claude-mods\\plugins\\status-inline;C:\\MyProjects\\claude-mods\\plugins\\model-picker"
  }
}

Para um terminal só:

claude --plugin-dir C:\MyProjects\claude-mods\plugins\mods-hub --plugin-dir C:\MyProjects\claude-mods\plugins\status-inline --plugin-dir C:\MyProjects\claude-mods\plugins\model-picker

Use um jeito só. Instalado e carregado pela pasta ao mesmo tempo, o mod roda duas vezes (dois ◆ mods na linha).

Configurações

Cada opção dos mods fica salva no settings.json do usuário, em pluginConfigs, e vale entre sessões. O nome da chave muda com o jeito de carregar (status-inline@claude-mods instalado, status-inline@inline pela pasta): ao trocar de jeito, refaça as escolhas pelo ◆ mods.

mods-hub

Adiciona ◆ mods no começo da linha do mode:

◆ mods ⏵⏵ auto mode on │ ctx ▰▰▰▰▰▱▱▱ 62% │ …

Clicar em ◆ mods (ou rodar /mods) abre uma janela lateral com cada mod que tem configurações:

● model-picker
  ■ Ativo
  ▸ Modelo: opus
  ▸ Effort: high

● status-inline
  ■ Ativo
  ▾ Barra: ▰▱
      ● ▰▱
      ○ █░
  ■ ctx %
  Amarelo a partir de % − 70 +
  • ●/○ liga ou desliga o mod inteiro; ■/□ liga ou desliga cada opção.
  • Uma opção com vários valores (▸) abre a lista de valores embaixo dela; escolher um aplica na hora.
  • Números têm − e +, de 5 em 5.
  • Passar o mouse numa opção mostra, no rodapé da janela, o que ela faz e um exemplo já na cor e no estilo daquela escolha.
  • Toda mudança vale na hora: o mod mexido recarrega com a opção nova.

Opção própria: Cor do ◆ mods (verde-neon, ciano, magenta, laranja, azul, roxo, branco).

Clique e hover dependem de o terminal enviar o mouse para o Claude Code, o que acontece no modo tela cheia. Sem mouse, use /mods, Tab e Enter.

status-inline

Desenha medidores depois da linha do mode, em verde neon:

⏵⏵ auto mode on │ ctx ▰▰▰▰▰▱▱▱ 62% │ 5h ▰▱▱▱▱▱▱▱ 18% ↻14:30 │ 7d ▰▰▰▱▱▱▱▱ 41% ↻sex │ opus5.5·xh │ $1.23 │ tok 1.2M │ cmp ≈18k │ 🔥 →36%
MedidorO que mostra
ctxQuanto da janela de contexto o último request usou.
5hUso da janela de rate limit de 5 horas e quando ela reseta.
7dUso da janela semanal e o dia em que ela reseta.
opus5.5·xhModelo e effort (lo, md, hi, xh, max) do request atual.
$1.23Custo da sessão, como o /cost soma.
tok 1.2MTokens gastos na sessão, conversa principal e subagents juntos, desde que o mod carregou.
cmp ≈18kTokens que faltam até o auto-compact.
🔥 →36%Uso que a janela de 5h terá no reset, se o ritmo atual continuar.

Configurações:

OpçãoValores
Ativoliga ou desliga os medidores
Corverde-neon, ciano, magenta, laranja, azul, roxo, branco
Barra▰▱, █░, ■□, ●○, ⣿⣀, ━┄
Resetoculto (18%), valor (18% 9:00), ícone (18% ↻9:00), texto (18% reset às 9:00)
Tokenstotal, sem-cache (sem o cache relido), in/out (↑1.2M ↓40k), oculto
ctx, 5h, 7d, Modelo, Effort, Custo, cmp, Projeção 5h, Barrasmostra ou esconde cada item
Amarelo a partir de %padrão 70
Vermelho a partir de %padrão 90
  • Em tela estreita os itens somem nesta ordem: barras, extras (custo, tokens, cmp, projeção), horários de reset, 7d, modelo/effort. ctx e 5h nunca somem.
  • Com a janela do ◆ mods aberta ao lado, o mod conta a largura dela, porque a linha do mode passa por baixo das duas.

model-picker

Troca o modelo e o effort da sessão pela janela do ◆ mods.

OpçãoValores
Ativoliga ou desliga as trocas e a sincronização
Modelodefault, opus, sonnet, haiku, fable
Effortlow, medium, high, xhigh, max
  • Escolher um modelo troca na hora, como o /model. Escolher um effort roda /effort <nível>, que executa quando a sessão fica parada.
  • Na primeira vez que carrega, o mod adota o modelo e o effort atuais da sessão: instalar não troca nada.
  • Um /model, /effort ou troca de modelo pelo /config feita à mão passa a aparecer na janela. Um modelo como claude-opus-5-5 ou opus[1m] aparece como opus.
  • Trocar de modelo não perde a conversa, mas o primeiro request depois da troca relê o contexto sem cache: sai mais caro e mais lento.

Estrutura

claude-mods/
  .claude-plugin/marketplace.json   lista os mods para o /plugin install
  plugins/
    mods-hub/        botão ◆ mods, janela lateral, /mods
    status-inline/   medidores na linha do mode
    model-picker/    modelo e effort pela janela de mods

Cada mod tem .claude-plugin/plugin.json (nome, opções em userConfig), hooks/hooks.json e o módulo em hooks/register.ts(x), com seus testes em hooks/*.test.ts(x).

Criar um mod que aparece no ◆ mods

Um mod aparece na janela quando o plugin.json declara campos em userConfig.

  • Um campo booleano chamado enabled vira o liga/desliga do mod; o mod deve checar options.enabled e ficar inerte quando for false.
  • Um campo string com options vira uma lista de valores.
  • A ajuda do hover vem do description do campo:
Explicação do campo, uma ideia por linha.
Ex.: {#39FF14}ctx ▰▰▰▰▰▱▱▱ 62%{/}
-- <opção>
Explicação daquela opção.
Ex.: {#00F0FF}ctx ▰▰▰▰▰▱▱▱ 62%{/}

O Claude Code remove as quebras de linha do description, então o hub reencontra as linhas: uma linha começa em -- <opção> (uma das opções do campo), em Ex.: e depois de ., ! ou ? quando a frase seguinte começa com letra maiúscula. Comece toda linha assim e termine cada frase com ponto. Uma linha Ex.: é desenhada como exemplo, e {#RRGGBB}texto{/} pinta aquele trecho na cor.

No /config do Claude Code a mesma descrição aparece como texto puro, com as marcações cruas.

Desenvolver

claude plugin validate plugins/<mod>
claude plugin test plugins/<mod>
Source 2 files
hooks/register.ts 147 lines
1import type { EngineInterface, PluginOptions, Register } from 'claude-code'
2
3import { EFFORTS, MODELS } from './choices'
4
5/** What this mod last set or adopted, so a reload applies only a pick that changed. */
6type Applied = { model?: string; effort?: string }
7
8type Field = 'model' | 'effort'
9
10/** True while this mod sets the model or effort itself, so its own change is not adopted back. */
11let isApplyingModel = false
12let isApplyingEffort = false
13
14function message(error: unknown): string {
15  return error instanceof Error ? error.message : String(error)
16}
17
18/** The pick a model or effort spelling stands for, or undefined when none does. */
19export function toPick(field: Field, raw: string): string | undefined {
20  const value = raw.trim().toLowerCase()
21  if (field === 'effort') return EFFORTS.includes(value) ? value : undefined
22  if (MODELS.includes(value)) return value
23
24  // A full id or a variant (`claude-opus-5-5`, `opus[1m]`) counts as its family.
25  return MODELS.find(model => model !== 'default' && value.includes(model))
26}
27
28async function readApplied($: EngineInterface): Promise<Applied | undefined> {
29  const value = await $.store.get('applied')
30
31  return value !== null && typeof value === 'object' ? (value as Applied) : undefined
32}
33
34async function setOwnRow($: EngineInterface, field: Field, value: string): Promise<void> {
35  await $.config.set({ key: field === 'model' ? 'model-picker.model' : 'model-picker.effort', value })
36}
37
38/**
39 * The first load takes the session's model and effort as the picks, so
40 * installing the mod changes nothing.
41 */
42async function adoptSession($: EngineInterface, options: PluginOptions): Promise<void> {
43  const [model, effort] = await Promise.all([$.session.model(), $.env.get('CLAUDE_EFFORT')])
44  const applied: Applied = {
45    model: toPick('model', model) ?? String(options['model']),
46    effort: (effort === undefined ? undefined : toPick('effort', effort)) ?? String(options['effort']),
47  }
48  // Stored before the rows change: each change reloads this mod.
49  await $.store.set('applied', applied)
50  if (applied.model !== options['model']) await setOwnRow($, 'model', applied.model ?? '')
51  if (applied.effort !== options['effort']) await setOwnRow($, 'effort', applied.effort ?? '')
52}
53
54/**
55 * Sets the session's model and effort to the picks in `options`, each only
56 * when it differs from what this mod last set: a config change reloads the
57 * mod, and every load runs this again.
58 */
59async function applyPicks($: EngineInterface, options: PluginOptions, applied: Applied): Promise<void> {
60  const model = String(options['model'])
61  const effort = String(options['effort'])
62  const next: Applied = { ...applied }
63
64  if (model !== applied.model) {
65    isApplyingModel = true
66    const result = await $.config.set({ key: 'model', value: model }).finally(() => {
67      isApplyingModel = false
68    })
69    if (result.deny === undefined) {
70      next.model = model
71      $.ui.toast(`modelo → ${model}`)
72    } else {
73      $.ui.toast(`modelo: ${result.deny}`)
74    }
75  }
76
77  if (effort !== applied.effort) {
78    next.effort = effort
79    $.ui.toast(`effort → ${effort}`)
80    isApplyingEffort = true
81    // Queued until the session is idle; nothing here waits on it.
82    void $.command
83      .run({ command: 'effort', args: effort })
84      .catch(error => $.ui.toast(`effort: ${message(error)}`))
85      .finally(() => {
86        isApplyingEffort = false
87      })
88  }
89
90  await $.store.set('applied', next)
91}
92
93/**
94 * Takes a model or effort the person set elsewhere (/model, /effort, /config)
95 * as this mod's pick, so the mods pane shows what the session runs.
96 */
97async function adopt($: EngineInterface, options: PluginOptions, field: Field, raw: string): Promise<void> {
98  const pick = toPick(field, raw)
99  if (pick === undefined) return
100
101  const applied = (await readApplied($)) ?? {}
102  await $.store.set('applied', { ...applied, [field]: pick })
103  if (options[field] !== pick) await setOwnRow($, field, pick)
104}
105
106export const register: Register = (on, options) => {
107  const isEnabled = options['enabled'] !== false
108
109  on('session.start', async ($, e, next) => {
110    const started = await next(e)
111    if (!isEnabled) return started
112
113    // A pick never set or adopted yet (a first load, or a store from before
114    // every field had a pick) takes the session's own instead of changing it.
115    const applied = await readApplied($)
116    if (applied?.model === undefined || applied.effort === undefined) {
117      await adoptSession($, options)
118    } else {
119      await applyPicks($, options, applied)
120    }
121
122    return started
123  })
124
125  on('command.run', { command: 'model' }, async ($, e, next) => {
126    const ran = await next(e)
127    if (isEnabled && !isApplyingModel && e.args.trim() !== '') await adopt($, options, 'model', e.args)
128
129    return ran
130  }).catch(($, e, next) => next(e))
131
132  on('command.run', { command: 'effort' }, async ($, e, next) => {
133    const ran = await next(e)
134    if (isEnabled && !isApplyingEffort && e.args.trim() !== '') await adopt($, options, 'effort', e.args)
135
136    return ran
137  }).catch(($, e, next) => next(e))
138
139  on('config.set', { key: 'model' }, async ($, e, next) => {
140    const set = await next(e)
141    const isOwn = isApplyingModel || (e.origin?.kind === 'plugin' && e.origin.name === 'model-picker')
142    if (isEnabled && !isOwn && set.deny === undefined) await adopt($, options, 'model', String(set.value))
143
144    return set
145  }).catch(($, e, next) => next(e))
146}
147
hooks/choices.ts 6 lines
1/** The `model` option's picks: the base models `/model` offers. */
2export const MODELS: readonly string[] = ['default', 'opus', 'sonnet', 'haiku', 'fable']
3
4/** The `effort` option's picks: the levels `/effort` takes. */
5export const EFFORTS: readonly string[] = ['low', 'medium', 'high', 'xhigh', 'max']
6