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

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.
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.
/simple — alterna encendido/apagado./simple on / /simple off — explícito.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.
MIT. Implementación original.
hooks/register.tsx 4 lines1import type { On } from "claude-code";
2import { registerCleanView } from "./clean-view";
3export function register(on: On) { registerCleanView(on); }
4hooks/clean-view.tsx 92 lines1import 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}
92hooks/state.ts 13 lines1import 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}
13types/index.d.ts 4 lines1export 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