/simple on oculta el detalle técnico de la conversación y muestra los pasos del turno; /simple abre un dashboard interactivo (interruptor, contadores, desglose…

Un mod de Claude Code: /simple on oculta el detalle técnico de la conversación y muestra los pasos del turno; /simple abre un dashboard interactivo; /simple off lo restaura.
p) que abre el dashboard./simple)Un panel lateral (en la terminal necesita /tui fullscreen y 110 columnas; en la app de escritorio se acopla solo) con:
| Zona | Qué hace | Tecla |
|---|---|---|
| Interruptor | Enciende o apaga la vista limpia sin salir del panel | v |
| Resumen del turno | Pasos totales, hechos, en curso, con error y duración del turno | |
| Desglose | Barras por tipo: lectura, edición, comandos, web, agentes, plan | |
| Filtros | Todos · En curso · Hechos · Errores | 1 2 3 4 |
| Lista de pasos | Cada paso es un botón; al pulsarlo se abre su detalle | clic o Tab + Enter |
| Detalle | Herramienta, tipo, estado, duración, ruta o comando completo y el texto del error | |
| Historial | Los últimos 12 turnos: pasos, aciertos, errores, duración y mezcla de tipos | h |
| Limpiar / Cerrar | Vacía la lista del turno / cierra el panel | l c |
Las teclas funcionan cuando el panel tiene el foco (/simple lo abre enfocado; ctrl+x tab lo enfoca después).
Nada se pierde: la transcripción guarda todo y el modelo ve lo mismo de siempre. El mod no llama al modelo ni consume tokens.
/plugin install clean-view --marketplace alvaroruizr2024-cpu/www.innovaqsolution.com
O desde la terminal: claude plugin install clean-view@innovaq-mods tras claude plugin marketplace add alvaroruizr2024-cpu/www.innovaqsolution.com.
hooks/register.tsx engancha tool.call para registrar cada paso (tipo, etiqueta, detalle, tiempos y error al resolverse), turn.start para vaciar la lista, turn.complete para guardar el resumen del turno en el historial, command.run para /simple, y ui.render sobre ToolUse, ToolResult, ToolGroup, AbovePrompt y Pane para dibujar la vista limpia y el dashboard. El estado vive en $.state bajo clean-view, declarado en types/index.d.ts.
claude plugin validate .
claude plugin test .
claude --plugin-dir .hooks/register.tsx 446 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Filter, Step, StepKind, TurnSummary } from '../types'
5
6// Clean View: `/simple on` oculta el detalle técnico de la conversación y
7// muestra los pasos del turno; `/simple` abre el dashboard interactivo, un
8// panel con el interruptor, contadores, desglose por tipo, filtros, la lista
9// de pasos con su detalle y el historial de turnos. `/simple off` lo quita.
10// Toda la información sigue en la transcripción: solo cambia lo que se dibuja.
11
12const PANE = 'clean-view'
13const MAX_STEPS = 80
14const MAX_HISTORY = 12
15const BAR_MAX = 10
16
17const isOn = atom({ plugin: 'clean-view', key: 'isOn' } as const, false)
18const steps = atom({ plugin: 'clean-view', key: 'steps' } as const, [])
19const filter = atom({ plugin: 'clean-view', key: 'filter' } as const, 'all')
20const selectedId = atom({ plugin: 'clean-view', key: 'selectedId' } as const, null)
21const history = atom({ plugin: 'clean-view', key: 'history' } as const, [])
22const showHistory = atom({ plugin: 'clean-view', key: 'showHistory' } as const, false)
23const turnStartedAt = atom({ plugin: 'clean-view', key: 'turnStartedAt' } as const, 0)
24
25const GLYPH: Record<Step['status'], string> = { running: '●', done: '✓', error: '✗' }
26const KINDS: StepKind[] = ['read', 'edit', 'shell', 'web', 'agent', 'plan', 'other']
27const KIND_NAME: Record<StepKind, string> = {
28 read: 'Lectura',
29 edit: 'Edición',
30 shell: 'Comandos',
31 web: 'Web',
32 agent: 'Agentes',
33 plan: 'Plan',
34 other: 'Otros',
35}
36const FILTERS: Array<{ id: Filter; name: string; hotkey: string }> = [
37 { id: 'all', name: 'Todos', hotkey: '1' },
38 { id: 'running', name: 'En curso', hotkey: '2' },
39 { id: 'done', name: 'Hechos', hotkey: '3' },
40 { id: 'error', name: 'Errores', hotkey: '4' },
41]
42
43function short(path: string): string {
44 const parts = path.split('/')
45 return parts.length > 2 ? parts.slice(-2).join('/') : path
46}
47
48function str(value: unknown, max = 48): string {
49 const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : ''
50 return text.length > max ? `${text.slice(0, max - 1)}…` : text
51}
52
53function elapsed(ms: number): string {
54 const seconds = Math.max(0, Math.round(ms / 1000))
55 return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`
56}
57
58function bar(count: number): string {
59 return count === 0 ? '·' : '▇'.repeat(Math.min(count, BAR_MAX)) + (count > BAR_MAX ? '+' : '')
60}
61
62function kindOf(tool: string): StepKind {
63 switch (tool) {
64 case 'Read':
65 case 'Grep':
66 case 'Glob':
67 case 'NotebookRead':
68 return 'read'
69 case 'Edit':
70 case 'MultiEdit':
71 case 'Write':
72 case 'NotebookEdit':
73 return 'edit'
74 case 'Bash':
75 return 'shell'
76 case 'WebFetch':
77 case 'WebSearch':
78 return 'web'
79 case 'Agent':
80 case 'Workflow':
81 return 'agent'
82 case 'TaskCreate':
83 case 'TaskUpdate':
84 case 'TaskList':
85 case 'TaskGet':
86 return 'plan'
87 default:
88 return 'other'
89 }
90}
91
92// Una etiqueta en lenguaje llano y el detalle (ruta, comando, patrón) de cada llamada.
93function describe(tool: string, input: Record<string, unknown>): { label: string; detail: string } {
94 const path = typeof input.file_path === 'string' ? input.file_path : ''
95 const name = path ? short(path) : ''
96 switch (tool) {
97 case 'Read':
98 return { label: `Leyendo ${name || 'un archivo'}`, detail: path }
99 case 'Edit':
100 case 'MultiEdit':
101 return { label: `Editando ${name || 'un archivo'}`, detail: path }
102 case 'Write':
103 return { label: `Escribiendo ${name || 'un archivo'}`, detail: path }
104 case 'Bash':
105 return {
106 label: `Ejecutando: ${str(input.description) || str(input.command, 40) || 'un comando'}`,
107 detail: str(input.command, 160),
108 }
109 case 'Grep':
110 return { label: `Buscando "${str(input.pattern, 30)}"`, detail: str(input.pattern, 120) }
111 case 'Glob':
112 return { label: `Listando ${str(input.pattern, 30) || 'archivos'}`, detail: str(input.pattern, 120) }
113 case 'WebFetch':
114 return { label: `Consultando ${str(input.url, 40)}`, detail: str(input.url, 160) }
115 case 'WebSearch':
116 return { label: `Buscando en la web: ${str(input.query, 36)}`, detail: str(input.query, 160) }
117 case 'Agent':
118 return { label: `Delegando: ${str(input.description, 40) || 'un subagente'}`, detail: str(input.prompt, 160) }
119 case 'TaskCreate':
120 return { label: `Planificando: ${str(input.subject, 40)}`, detail: str(input.description, 160) }
121 case 'TaskUpdate':
122 return { label: 'Actualizando el plan', detail: str(input.status, 40) }
123 case 'AskUserQuestion':
124 return { label: 'Preguntándote algo', detail: '' }
125 default: {
126 const label = tool.startsWith('mcp__') ? `Usando ${tool.split('__').slice(1).join(' / ')}` : `Usando ${tool}`
127 return { label, detail: str(JSON.stringify(input), 160) }
128 }
129 }
130}
131
132function countKinds(list: Step[]): Record<StepKind, number> {
133 const counts = { read: 0, edit: 0, shell: 0, web: 0, agent: 0, plan: 0, other: 0 }
134 for (const step of list) {
135 counts[step.kind] += 1
136 }
137
138 return counts
139}
140
141export const register: Register = on => {
142 on('session.start', async ($, e, next) => {
143 await $.command.register({
144 name: 'simple',
145 description: 'Vista limpia: on | off, o sin argumento abre el dashboard de pasos',
146 argumentHint: 'on|off|panel',
147 immediate: true,
148 })
149
150 return next(e)
151 })
152
153 on('command.run', { command: 'simple' }, async ($, e) => {
154 const arg = e.args.trim().toLowerCase()
155 if (arg === 'on' || arg === 'off') {
156 const next = arg === 'on'
157 await update($, isOn, () => next)
158 if (next) {
159 void $.ui.open({ id: PANE, title: 'Vista limpia' })
160 }
161 $.ui.invalidate('ui.render')
162
163 return {
164 text: next
165 ? 'Clean View activado: el detalle técnico queda oculto y los pasos se muestran en el panel y encima del prompt. `/simple off` lo restaura.'
166 : 'Clean View desactivado: vuelven a verse las herramientas y sus resultados. `/simple` abre el dashboard.',
167 }
168 }
169 const opened = await $.ui.open({ id: PANE, title: 'Vista limpia', focus: true })
170
171 return {
172 text: opened.isPlaced
173 ? 'Dashboard de Vista limpia abierto. Teclas: v interruptor, 1-4 filtros, h historial, l limpiar, c cerrar.'
174 : `El dashboard espera una terminal más ancha o la vista a pantalla completa (/tui fullscreen): ${opened.reason}`,
175 }
176 })
177
178 // Cada turno del hilo principal empieza con la lista en blanco.
179 on('turn.start', async ($, e, next) => {
180 if (e.agentId === undefined) {
181 await update($, steps, () => [])
182 await update($, selectedId, () => null)
183 await update($, turnStartedAt, () => Date.now())
184 }
185
186 return next(e)
187 })
188
189 // Al cerrar el turno principal, su resumen pasa al historial.
190 on('turn.complete', async ($, e, next) => {
191 if (e.agentId === undefined) {
192 const list = await read($, steps)
193 if (list.length > 0) {
194 const startedAt = await read($, turnStartedAt)
195 const endedAt = Date.now()
196 const summary: TurnSummary = {
197 endedAt,
198 durationMs: startedAt > 0 ? endedAt - startedAt : 0,
199 total: list.length,
200 done: list.filter(step => step.status === 'done').length,
201 errors: list.filter(step => step.status === 'error').length,
202 kinds: countKinds(list),
203 }
204 await update($, history, past => [...past, summary].slice(-MAX_HISTORY))
205 }
206 }
207
208 return next(e)
209 })
210
211 // Cada llamada a herramienta es un paso: corriendo, hecho o con error.
212 on('tool.call', async ($, e, next) => {
213 if (e.agentId !== undefined) {
214 return next(e) // los pasos de un subagente no se listan
215 }
216 const { label, detail } = describe(e.tool, e as unknown as Record<string, unknown>)
217 const step: Step = {
218 id: e.tool_use_id,
219 tool: e.tool,
220 kind: kindOf(e.tool),
221 label,
222 detail,
223 status: 'running',
224 startedAt: Date.now(),
225 }
226 await update($, steps, list => [...list, step].slice(-MAX_STEPS))
227
228 const ran = await next(e)
229 const failed = ran.deny !== undefined || ran.isError === true
230 const error = ran.deny ?? (failed ? str(ran.text, 200) || 'La herramienta devolvió un error.' : undefined)
231 const endedAt = Date.now()
232 await update($, steps, list =>
233 list.map(one => (one.id === step.id ? { ...one, status: failed ? 'error' : 'done', endedAt, error } : one)),
234 )
235
236 return ran
237 })
238
239 // Con la vista limpia, cada fila de herramienta es una línea tenue.
240 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
241 if (!(await read($, isOn))) {
242 return next(e)
243 }
244 const { Text } = $.ui.resolve(e)
245 const input = (e.props.input ?? {}) as Record<string, unknown>
246 const mark = e.props.isRunning ? '●' : e.props.isErrored ? '✗' : '✓'
247
248 return (
249 <Text dimColor>
250 {mark} {describe(e.props.tool, input).label}
251 </Text>
252 )
253 })
254
255 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
256 if (!(await read($, isOn))) {
257 return next(e)
258 }
259 const { Text } = $.ui.resolve(e)
260
261 return <Text dimColor>{e.props.isErrored ? ' resultado con error (/simple off para verlo)' : ' resultado oculto'}</Text>
262 })
263
264 on('ui.render', { component: 'ToolGroup' }, async ($, e, next) => {
265 if (!(await read($, isOn))) {
266 return next(e)
267 }
268 const { Text } = $.ui.resolve(e)
269 const total = e.props.calls.length
270 const running = e.props.calls.filter(call => call.isRunning).length
271
272 return (
273 <Text dimColor>
274 {running > 0 ? '●' : '✓'} {total} {total === 1 ? 'paso' : 'pasos'} de lectura
275 {running > 0 ? ` (${running} en curso)` : ''}
276 </Text>
277 )
278 })
279
280 // La banda encima del prompt: los últimos pasos y un botón al dashboard.
281 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
282 const list = await read($, steps)
283 if (!(await read($, isOn)) || e.props.hasSurvey || list.length === 0) {
284 return next(e)
285 }
286 const { Box, Button, Text } = $.ui.resolve(e)
287 const done = list.filter(step => step.status === 'done').length
288 const errors = list.filter(step => step.status === 'error').length
289 const room = Math.max(1, e.props.maxRows - 1)
290 const shown = list.slice(-room)
291
292 return (
293 <Box flexDirection="column">
294 <Box>
295 <Text>
296 Vista limpia · {done}/{list.length} pasos
297 {errors > 0 ? ` · ${errors} con error` : ''}
298 {e.props.isWorking ? ' · trabajando…' : ' · listo'}{' '}
299 </Text>
300 <Button key="panel" label="Panel" hotkey="p" onPress={() => $.ui.open({ id: PANE, title: 'Vista limpia', focus: true })} />
301 </Box>
302 {shown.map(step => (
303 <Text key={step.id} dimColor={step.status === 'done'}>
304 {GLYPH[step.status]} {step.label}
305 </Text>
306 ))}
307 </Box>
308 )
309 })
310
311 // El dashboard.
312 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
313 const { Box, Button, Text } = $.ui.resolve(e)
314 const enabled = await read($, isOn)
315 const list = await read($, steps)
316 const active = await read($, filter)
317 const selected = await read($, selectedId)
318 const past = await read($, history)
319 const inHistory = await read($, showHistory)
320 const startedAt = await read($, turnStartedAt)
321 const now = Date.now()
322
323 const running = list.filter(step => step.status === 'running').length
324 const done = list.filter(step => step.status === 'done').length
325 const errors = list.filter(step => step.status === 'error').length
326 const kinds = countKinds(list)
327 const visible = active === 'all' ? list : list.filter(step => step.status === active)
328 const current = list.find(step => step.id === selected)
329 const turnMs = startedAt > 0 ? (running > 0 ? now - startedAt : Math.max(...list.map(s => s.endedAt ?? now), startedAt) - startedAt) : 0
330
331 const toggle = async () => {
332 await update($, isOn, value => !value)
333 $.ui.invalidate('ui.render')
334 }
335
336 return (
337 <Box flexDirection="column">
338 <Box>
339 <Text weight="bold">Vista limpia </Text>
340 <Button key="toggle" label={enabled ? 'Activada' : 'Desactivada'} hotkey="v" variant={enabled ? 'primary' : 'secondary'} onPress={toggle} />
341 <Text> </Text>
342 <Button
343 key="history"
344 label={inHistory ? 'Turno actual' : 'Historial'}
345 hotkey="h"
346 onPress={async () => {
347 await update($, showHistory, value => !value)
348 $.ui.invalidate('ui.render')
349 }}
350 />
351 <Text> </Text>
352 <Button
353 key="clear"
354 label="Limpiar"
355 hotkey="l"
356 onPress={async () => {
357 await update($, steps, () => [])
358 await update($, selectedId, () => null)
359 $.ui.invalidate('ui.render')
360 }}
361 />
362 <Text> </Text>
363 <Button key="close" label="Cerrar" hotkey="c" onPress={() => $.ui.close({ id: PANE })} />
364 </Box>
365 {!enabled && <Text dimColor>La vista limpia está apagada: la transcripción muestra el detalle completo. Pulsa v para ocultarlo.</Text>}
366
367 {inHistory ? (
368 <Box flexDirection="column">
369 <Text>Últimos turnos</Text>
370 {past.length === 0 && <Text dimColor>Aún no hay turnos cerrados en esta sesión.</Text>}
371 {[...past].reverse().map((turn, i) => (
372 <Text key={`turn-${turn.endedAt}`} dimColor={i > 0}>
373 {GLYPH[turn.errors > 0 ? 'error' : 'done']} hace {elapsed(now - turn.endedAt)} · {turn.total} pasos · {turn.done} ✓
374 {turn.errors > 0 ? ` · ${turn.errors} ✗` : ''} · {elapsed(turn.durationMs)} ·{' '}
375 {KINDS.filter(kind => turn.kinds[kind] > 0)
376 .map(kind => `${KIND_NAME[kind].toLowerCase()} ${turn.kinds[kind]}`)
377 .join(', ')}
378 </Text>
379 ))}
380 </Box>
381 ) : (
382 <Box flexDirection="column">
383 <Text>
384 Turno: {list.length} pasos · {done} ✓ · {running} ● · {errors} ✗ · {elapsed(turnMs)}
385 {running > 0 ? ' · trabajando…' : ''}
386 </Text>
387 <Box flexDirection="column">
388 {KINDS.filter(kind => kinds[kind] > 0 || kind === 'read' || kind === 'edit' || kind === 'shell').map(kind => (
389 <Text key={`kind-${kind}`} dimColor={kinds[kind] === 0}>
390 {KIND_NAME[kind].padEnd(9)} {bar(kinds[kind])} {kinds[kind]}
391 </Text>
392 ))}
393 </Box>
394 <Box>
395 <Text dimColor>Filtro: </Text>
396 {FILTERS.map(option => (
397 <Button
398 key={`filter-${option.id}`}
399 label={option.name}
400 hotkey={option.hotkey}
401 variant={active === option.id ? 'primary' : 'secondary'}
402 onPress={async () => {
403 await update($, filter, () => option.id)
404 $.ui.invalidate('ui.render')
405 }}
406 />
407 ))}
408 </Box>
409 {visible.length === 0 && (
410 <Text dimColor>{list.length === 0 ? 'Aún no hay pasos en este turno.' : 'Ningún paso con ese filtro.'}</Text>
411 )}
412 {visible.map(step => (
413 <Button
414 key={`step-${step.id}`}
415 plain
416 dimColor={step.status === 'done' && step.id !== selected}
417 onPress={async () => {
418 await update($, selectedId, value => (value === step.id ? null : step.id))
419 $.ui.invalidate('ui.render')
420 }}
421 >
422 {step.id === selected ? '▸ ' : ' '}
423 {GLYPH[step.status]} {step.label}
424 <Text dimColor> {elapsed((step.endedAt ?? now) - step.startedAt)}</Text>
425 </Button>
426 ))}
427 {current !== undefined && (
428 <Box flexDirection="column">
429 <Text> </Text>
430 <Text weight="bold">Detalle</Text>
431 <Text>
432 {current.tool} · {KIND_NAME[current.kind].toLowerCase()} ·{' '}
433 {current.status === 'running' ? 'en curso' : current.status === 'done' ? 'hecho' : 'error'} ·{' '}
434 {elapsed((current.endedAt ?? now) - current.startedAt)}
435 </Text>
436 {current.detail !== '' && <Text dimColor>{current.detail}</Text>}
437 {current.error !== undefined && <Text color="red">{current.error}</Text>}
438 </Box>
439 )}
440 </Box>
441 )}
442 </Box>
443 )
444 })
445}
446types/index.d.ts 41 lines1export type StepStatus = 'running' | 'done' | 'error'
2
3export type StepKind = 'read' | 'edit' | 'shell' | 'web' | 'agent' | 'plan' | 'other'
4
5export type Filter = 'all' | 'running' | 'done' | 'error'
6
7export type Step = {
8 id: string
9 tool: string
10 kind: StepKind
11 label: string
12 detail: string
13 status: StepStatus
14 startedAt: number
15 endedAt?: number
16 error?: string
17}
18
19export type TurnSummary = {
20 endedAt: number
21 durationMs: number
22 total: number
23 done: number
24 errors: number
25 kinds: Record<StepKind, number>
26}
27
28declare module 'claude-code' {
29 interface PluginState {
30 'clean-view': {
31 isOn: boolean
32 steps: Step[]
33 filter: Filter
34 selectedId: string | null
35 history: TurnSummary[]
36 showHistory: boolean
37 turnStartedAt: number
38 }
39 }
40}
41