SLOPSHOPPER

clean-view

Opt-in, reversible quiet view with observed tool counts. Never hides failures or permissions.

newpanebandspinnerrowsguard
★ 5v0.1.0no licenseupdated 2026-10-06mverab/recursos-ia/recursos/claude-mods/clean-view
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · clean-view
› 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 › /simple ⎿ clean-view: Clean View: ON · /simple off restores full details ⟨Claude Code's own drawing⟩ [ Clean View: ON ] idle · 0 succeeded · 0 failed · 0 denied · 0 running ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ [ Clean View: ON ] idle · 0 succeeded · 0 failed · 0 denied · 0 running
README

Clean View — mod de Claude Code

Menos ruido mientras Claude trabaja. Con /simple on, las filas técnicas de lectura y búsqueda que ya terminaron bien desaparecen de la pantalla y en su lugar ves una lista corta de pasos con su estado real y un resumen al finalizar. /simple off lo revierte todo al instante.

Qué NO hace (a propósito)

  • Nunca oculta permisos, preguntas, errores, rechazos ni comandos con efectos (Bash, escrituras, diffs). Lo que puede romper algo o necesita tu decisión siempre queda visible.
  • No toca el transcript guardado: sólo cambia lo que se dibuja en pantalla.
  • No inventa porcentajes ni barra de progreso falsa: muestra conteos de pasos reales observados.
  • Empieza apagado: tú lo activas cuando quieres.

Requisitos

  • Claude Code 2.1.287 o superior (verificado en 2.1.289).

Instalación

claude --plugin-dir /ruta/a/clean-view

O /plugin dentro de Claude Code para instalarlo permanente. Si el typecheck lo pide, genera los tipos con /plugin-types en una sesión.

Uso

  • /simple — alterna encendido/apagado.
  • /simple on / /simple off — explícito.
  • También hay un botón nativo sobre el prompt para alternar con un clic.

Límites honestos

  • Es un mod de terminal: en la app Desktop el panel superior no aplica con el API actual.
  • Es deliberadamente conservador: no esconde todo el ruido durante la ejecución, sólo las lecturas/búsquedas exitosas ya terminadas.

Verificación

12 pruebas automatizadas (7 de comportamiento + 5 del engine nativo real), validación estricta sin advertencias, montaje y botón verificados en el kit de pruebas de Claude Code 2.1.289. Ninguna prueba envía prompts ni consume tokens.

Licencia

MIT. Implementación original.

Source 4 files
hooks/register.tsx 4 lines
1import type { On } from "claude-code";
2import { registerCleanView } from "./clean-view";
3export function register(on: On) { registerCleanView(on); }
4
hooks/clean-view.tsx 92 lines
1import type { On } from 'claude-code';
2import { createState, command, counts } from './state';
3const labels: Record<string, string> = {Read:'Read information',Grep:'Search information',Glob:'Find files',LS:'List files',Bash:'Run a command',Write:'Write a file',Edit:'Update a file',AskUserQuestion:'Answer needed'};
4export function registerCleanView(on: On) {
5  const state = createState();
6  // Only calls observed settling successfully may be hidden. No inference from a drawing.
7  const quiet = new Set<string>();
8  const safeTools = new Set(['Read', 'Grep', 'Glob', 'LS']);
9  const unsafe = (output: unknown): boolean => {
10    if (output === undefined) return true;
11    try { return /error|fail|denied|refus|interrupt|warning|stderr/i.test(JSON.stringify(output)); }
12    catch { return true; }
13  };
14  const canHide = (row: {tool: string; tool_use_id?: string; isErrored: boolean; isRunning?: boolean; isInterrupted?: boolean; output?: unknown}) =>
15    !!row.tool_use_id && quiet.has(row.tool_use_id) && safeTools.has(row.tool) && !row.isRunning && !row.isErrored && !row.isInterrupted && !unsafe(row.output);
16  on('session.start', async ($, e, next) => {
17    await $.command.register({name: 'simple', description: 'Toggle reversible Clean View', argumentHint: 'on|off', immediate: true});
18    return next(e);
19  });
20  on('command.run', async ($, e, next) => {
21    if (e.command !== 'simple') return next(e);
22    const text = command(state, e.args);
23    try { $.ui.invalidate('ui.render'); } catch { /* Visual failure must never gate execution. */ }
24    return {text};
25  });
26  on('classic.Notification', async ($, e, next) => {
27    if (state.enabled && !e.agent_id && e.notification_type === 'permission_prompt') {
28      state.phase = 'needs-you';
29      try { $.ui.invalidate('ui.render'); } catch { /* Visual failure must never gate execution. */ }
30    }
31    return next(e);
32  });
33  on('turn.start', async ($, e, next) => {
34    if (state.enabled && !e.text.trim().startsWith('/')) {
35      state.turnId = e.turnId; state.phase = 'working'; state.steps = []; quiet.clear();
36      try { $.ui.invalidate('ui.render'); } catch { /* Visual failure must never gate execution. */ }
37    }
38    return next(e);
39  });
40  on('tool.call', async ($, e, next) => {
41    if (!state.enabled || e.agentId || !state.turnId || state.phase === 'complete') return next(e);
42    const turnId = state.turnId;
43    const step = {id:e.tool_use_id, name:labels[e.tool] ?? 'Use a tool', status:'active' as 'active'|'done'|'failed'|'denied'};
44    state.steps.push(step); state.phase = step.name === 'Answer needed' ? 'needs-you' : 'working';
45    try { $.ui.invalidate('ui.render'); } catch { /* Visual failure must never gate execution. */ }
46    let result;
47    try { result = await next(e); }
48    catch (error) {
49      if (turnId === state.turnId) {
50        step.status = 'failed'; state.phase = 'error';
51        try { $.ui.invalidate('ui.render'); } catch { /* Visual failure must never gate execution. */ }
52      }
53      throw error;
54    }
55    if (turnId === state.turnId) {
56      step.status = result.deny ? 'denied' : result.isError ? 'failed' : 'done';
57      if (state.phase === 'needs-you' && !state.steps.some(s => s.name === 'Answer needed' && s.status === 'active')) state.phase = 'working';
58      if (step.status === 'done' && safeTools.has(e.tool) && !unsafe(result.result) && !unsafe(result.text)) quiet.add(e.tool_use_id);
59      try { $.ui.invalidate('ui.render'); } catch { /* Visual failure must never gate execution. */ }
60    }
61    return result;
62  });
63  on('turn.complete', async ($, e, next) => {
64    const result = await next(e);
65    if (e.agentId || e.turnId !== state.turnId) return result;
66    state.phase = e.reason === 'answer' ? 'complete' : e.reason === 'aborted' ? 'stopped' : e.reason;
67    if (state.enabled) {
68      try { await $.ui.log(`${state.phase === 'complete' ? 'Turn complete' : state.phase} · ${counts(state)}`); }
69      catch { /* The original response and errors remain authoritative. */ }
70    }
71    try { $.ui.invalidate('ui.render'); } catch { /* Visual failure must never gate execution. */ }
72    return result;
73  });
74  on('ui.render', async ($, e, next) => {
75    // AbovePrompt is terminal-only in these official types. Other surfaces stay
76    // fully visible: do not hide anything without a mounted reversal control.
77    if (e.surface === 'terminal' && state.enabled) {
78      if ((e.component === 'ToolUse' || e.component === 'ToolResult') && canHide(e.props)) return $.ui.resolve(e).Box({children:[]});
79      if (e.component === 'ToolGroup' && !e.props.isActive && e.props.calls.length > 0 && e.props.calls.every(canHide)) return $.ui.resolve(e).Box({children:[]});
80    }
81    if (e.component !== 'AbovePrompt' || e.props.hasSurvey) return next(e);
82    const { Box, Button, Text } = $.ui.resolve(e);
83    const panel = state.enabled ? [Text({children:`${state.phase === 'complete' ? 'Turn complete' : state.phase === 'working' ? 'Working' : state.phase === 'needs-you' ? 'Needs you' : state.phase} · ${counts(state)}`}),
84      ...state.steps.slice(-6).map(step => Text({children:`${step.status === 'done' ? '✓' : step.status === 'active' ? '▶' : '⚠'} ${step.name} · ${step.status}`,wrap:'truncate'}))] : [];
85    return Box({flexDirection:'column', children:[await next(e), Button({key:'clean-view-toggle', label:`Clean View: ${state.enabled ? 'ON' : 'OFF'}`, onPress:async()=>{
86      const text = command(state, '');
87      try { $.ui.invalidate('ui.render'); } catch { /* Visual failure must never gate execution. */ }
88      try { await $.ui.log(text); } catch { /* Toggle still took effect. */ }
89    }}), ...panel]});
90  });
91}
92
hooks/state.ts 13 lines
1import type { CleanViewState } from '../types';
2export function createState(): CleanViewState { return { enabled: false, turnId: null, phase: 'idle', steps: [] }; }
3export function command(s: CleanViewState, args: string) {
4  const arg = args.trim();
5  if (arg !== '' && arg !== 'on' && arg !== 'off') return 'Usage: /simple on|off';
6  s.enabled = arg === '' ? !s.enabled : arg === 'on';
7  return `Clean View: ${s.enabled ? 'ON' : 'OFF'} · /simple off restores full details`;
8}
9export function counts(s: CleanViewState): string {
10  const count = (status: string) => s.steps.filter(step => step.status === status).length;
11  return `${count('done')} succeeded · ${count('failed')} failed · ${count('denied')} denied · ${count('active')} running`;
12}
13
types/index.d.ts 4 lines
1export type CleanViewPhase = 'idle' | 'working' | 'needs-you' | 'complete' | 'error' | 'refusal' | 'stopped';
2export type CleanViewStep = { id: string; name: string; status: 'active' | 'done' | 'failed' | 'denied' };
3export type CleanViewState = { enabled: boolean; turnId: string | null; phase: CleanViewPhase; steps: CleanViewStep[] };
4