SLOPSHOPPER

devtools

Botón devtools encima del prompt que abre una tarjeta con accesos a los paneles de los mods

newbandtoastprompt
v0.2.0MITupdated 2026-10-12jorgehsy/claude-mods/plugins/devtools
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · devtools
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM █████░░░░░ 97k/200k (49%) · Opus 5.5 [ devtools ▾ ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
█████░░░░░ 97k/200k (49%) · Opus 5.5 [ devtools ▾ ]
README

claude-mods

Catálogo de mods para Claude Code de Jorge Simoes. Funcionan en la terminal y en la app de escritorio.

Agregar el catálogo

Una sola vez:

claude plugin marketplace add jorgehsy/claude-mods

Después instala el mod que quieras:

claude plugin install <mod>@jorge-mods --scope user

Mods

ModQué hace
agent-monitorPanel en vivo de subagentes con un Clawd por agente que cambia de skin según lo que hace (research, developer, auditor) y destaca los modelos caros (Opus, Fable): estado, tokens, costo y contexto.
github-panelPanel en vivo de GitHub con los issues y PR abiertos del repo: tomar un issue, mergear un PR y reintentar checks fallidos.
devtoolsBarra encima del prompt: a la izquierda el contexto usado, el modelo, el effort y el output style; a la derecha el botón devtools con accesos a los paneles de Agentes y GitHub.

Actualizar

claude plugin marketplace update jorge-mods
claude plugin update <mod>@jorge-mods

Después escribe /reload-plugins en la sesión abierta.

Agregar un mod nuevo

  1. Crea su carpeta en plugins/<mod>/, con .claude-plugin/plugin.json y hooks/.
  2. Agrégalo a plugins en .claude-plugin/marketplace.json, con "source": "./plugins/<mod>".
  3. Agrega su fila a la tabla de arriba.
  4. Revisa con claude plugin validate . y con claude plugin validate plugins/<mod>.

Para que un cambio les llegue a los demás, sube version en el plugin.json del mod.

Licencia

MIT

Source 3 files
hooks/register.tsx 112 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Info } from '../types'
5import { bar, compact, level, modelName } from './format.ts'
6
7const isOpen = atom({ plugin: 'devtools', key: 'isOpen' } as const, false)
8const info = atom({ plugin: 'devtools', key: 'info' } as const, { model: '', effort: '', style: '', tokens: 0, window: 0 } as Info)
9
10/** Cada acceso de la tarjeta: el botón y el comando del mod que abre. Para sumar uno, agrégalo aquí. */
11const TOOLS = [
12  { key: 'agentes', label: 'Agentes', command: 'agent-monitor' },
13  { key: 'github', label: 'GitHub', command: 'github-panel' },
14]
15
16const text = (v: unknown) => (typeof v === 'string' ? v : '')
17
18export const register: Register = on => {
19  on('session.start', async ($, e, next) => {
20    const result = await next(e)
21    try {
22      const settings = await $.settings.read()
23      const [model, usage] = await Promise.all([$.session.model(), $.session.usage()])
24      await update($, info, v => ({
25        ...v,
26        model,
27        effort: v.effort || text(settings.effortLevel),
28        style: v.style || text(settings.outputStyle),
29        tokens: usage.context.tokens ?? 0,
30        window: usage.context.window,
31      }))
32    } catch (err) {
33      $.ui.log(`devtools: no se pudo leer la sesión: ${String(err)}`, { to: 'debug' })
34    }
35    return result
36  })
37
38  on('session.measure', async ($, e, next) => {
39    const { tokens = 0, window } = e.context
40    await update($, info, v => (v.tokens === tokens && v.window === window ? v : { ...v, tokens, window }))
41    return next(e)
42  })
43
44  // Sólo el bucle principal: los subagentes llevan su propio modelo y effort.
45  on('turn.step', async function* ($, e, next) {
46    if (!e.agentId) {
47      const effort = e.effort === undefined ? '' : String(e.effort)
48      await update($, info, v => (v.model === e.model && v.effort === effort ? v : { ...v, model: e.model, effort }))
49    }
50    return yield* next(e)
51  })
52
53  on('prompt.compose', async ($, e, next) => {
54    const style = e.outputStyle?.name ?? ''
55    await update($, info, v => (v.style === style ? v : { ...v, style }))
56    return next(e)
57  })
58
59  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
60    if (e.props.hasSurvey) return next(e)
61
62    const { Box, Button, Text } = $.ui.resolve(e)
63    const open = await read($, isOpen)
64    const now = await read($, info)
65    const toggle = () => update($, isOpen, v => !v)
66
67    const launch = async (command: string) => {
68      await update($, isOpen, () => false)
69      try {
70        await $.command.run({ command })
71      } catch (err) {
72        $.ui.toast(`devtools: no se pudo abrir /${command}`)
73        $.ui.log(`devtools: /${command} falló: ${String(err)}`, { to: 'debug' })
74      }
75    }
76
77    const percent = now.window > 0 ? Math.round((now.tokens / now.window) * 100) : 0
78    const cells = bar(percent)
79    const facts = [now.model && modelName(now.model), now.effort, now.style].filter(Boolean)
80
81    return (
82      <Box flexDirection="column">
83        <Box justifyContent="space-between" width="100%">
84          <Box gap={1} flexShrink={1}>
85            <Text>
86              <Text color={level(percent)}>{cells.full}</Text>
87              <Text dimColor>{cells.empty}</Text>
88            </Text>
89            <Text dimColor>
90              {now.window > 0 ? `${compact(now.tokens)}/${compact(now.window)} (${percent}%)` : 'contexto —'}
91            </Text>
92            {facts.map((f, i) => (
93              <Text key={`f${i}`} dimColor>· {f}</Text>
94            ))}
95          </Box>
96          <Button key="devtools" label={open ? 'devtools ▴' : 'devtools ▾'} dimColor={!open} onPress={toggle} />
97        </Box>
98        {open && (
99          <Box borderStyle="round" borderDimColor flexDirection="column" paddingX={1} alignSelf="flex-end">
100            <Text dimColor>Paneles</Text>
101            <Box gap={1}>
102              {TOOLS.map(t => (
103                <Button key={t.key} label={t.label} onPress={() => launch(t.command)} />
104              ))}
105            </Box>
106          </Box>
107        )}
108      </Box>
109    )
110  })
111}
112
hooks/format.ts 26 lines
1/** `claude-opus-5-5[1m]` → `Opus 5.5`; lo que no reconoce lo deja como vino. */
2export function modelName(id: string): string {
3  const m = /claude-([a-z]+)-(\d+)(?:-(\d+))?/.exec(id)
4  if (!m) return id
5  const family = m[1][0].toUpperCase() + m[1].slice(1)
6  return m[3] ? `${family} ${m[2]}.${m[3]}` : `${family} ${m[2]}`
7}
8
9/** 184000 → `184k`, 1000000 → `1M`. */
10export function compact(n: number): string {
11  if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`
12  if (n >= 1000) return `${Math.round(n / 1000)}k`
13  return String(n)
14}
15
16/** Barra de `cells` celdas llenas según `percent` (0 a 100). */
17export function bar(percent: number, cells = 10): { full: string; empty: string } {
18  const n = Math.max(0, Math.min(cells, Math.round((percent / 100) * cells)))
19  return { full: '█'.repeat(n), empty: '░'.repeat(cells - n) }
20}
21
22/** Color del tema según qué tan llena está la ventana. */
23export function level(percent: number): 'success' | 'warning' | 'error' {
24  return percent >= 85 ? 'error' : percent >= 60 ? 'warning' : 'success'
25}
26
types/index.d.ts 26 lines
1/** Si la tarjeta con los accesos está abierta. */
2export type IsOpen = boolean
3
4/** Lo que la barra muestra a la izquierda. */
5export type Info = {
6  /** Modelo del bucle principal, tal como lo nombra el motor (`claude-opus-5-5`). */
7  model: string
8  /** Effort del último paso del bucle principal; vacío si el modelo no usa. */
9  effort: string
10  /** Output style elegido; vacío si es el de por defecto. */
11  style: string
12  /** Tokens de entrada de la última respuesta; 0 antes de la primera. */
13  tokens: number
14  /** Tamaño de la ventana de contexto del modelo. */
15  window: number
16}
17
18declare module 'claude-code' {
19  interface PluginState {
20    devtools: {
21      isOpen: IsOpen
22      info: Info
23    }
24  }
25}
26