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

Catálogo de mods para Claude Code de Jorge Simoes. Funcionan en la terminal y en la app de escritorio.
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
| Mod | Qué hace |
|---|---|
| agent-monitor | Panel 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-panel | Panel en vivo de GitHub con los issues y PR abiertos del repo: tomar un issue, mergear un PR y reintentar checks fallidos. |
| devtools | Barra 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. |
claude plugin marketplace update jorge-mods
claude plugin update <mod>@jorge-mods
Después escribe /reload-plugins en la sesión abierta.
plugins/<mod>/, con .claude-plugin/plugin.json y hooks/.plugins en .claude-plugin/marketplace.json, con "source": "./plugins/<mod>".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.
MIT
hooks/register.tsx 112 lines1import { 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}
112hooks/format.ts 26 lines1/** `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}
26types/index.d.ts 26 lines1/** 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