Hides tool-call rows while Claude works; shows a plan checklist band (done / current / left).

Hides the tool calls and code that scroll by while Claude works, and shows a short checklist of the plan instead.
╭──────────────────────────────────────╮
│ Plan ████░░░░░░░░ 3/8 done, 5 left │
│ ✔ Read the config │
│ ▶ Writing the migration │
│ ○ Run the tests │
╰──────────────────────────────────────╯
Done items are green and struck through, the current step is amber and bold, the rest are dim.
/cleanview turns the mod off and on, which shows the tool rows again.The plan comes from the main session's TodoWrite calls, so it only appears when Claude keeps a todo list. A subagent's list is ignored.
claude --plugin-dir /path/to/claude-code-mods/clean-view
To load it in every session, see Install.
None. It makes no model calls.
hooks/register.tsx 91 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Todo } from '../types'
5
6const todos = atom({ plugin: 'clean-view', key: 'todos' } as const, [])
7const isOn = atom({ plugin: 'clean-view', key: 'isOn' } as const, true)
8
9const ACCENT = '#7aa2f7'
10const GREEN = '#9ece6a'
11const AMBER = '#e0af68'
12
13const HIDDEN = ['ToolUse', 'ToolResult', 'ToolGroup', 'ToolProgress'] as const
14
15export const register: Register = on => {
16 on('session.start', async ($, e, next) => {
17 await $.command.register({
18 name: 'cleanview',
19 description: 'Toggle Clean View (hide tool-call rows, show plan checklist)',
20 })
21
22 return next(e)
23 })
24
25 on('command.run', { command: 'cleanview' }, async $ => {
26 const now = await update($, isOn, v => !v)
27
28 return { text: `Clean View ${now ? 'on' : 'off'}.` }
29 })
30
31 // Main loop only: a subagent's TodoWrite is not the plan.
32 on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
33 if (!e.agentId) {
34 const list = (e as unknown as { todos?: Todo[] }).todos ?? []
35 await update($, todos, () => list)
36 }
37
38 return next(e)
39 })
40
41 for (const component of HIDDEN) {
42 on('ui.render', { component }, async ($, e, next) => {
43 if (!(await read($, isOn))) {
44 return next(e)
45 }
46
47 return <Fragment />
48 })
49 }
50
51 // Collapses to nothing when idle or when there is no plan; never taller than the room allows.
52 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
53 const list: Todo[] = await read($, todos)
54
55 if (e.props.hasSurvey || !e.props.isWorking || !(await read($, isOn)) || list.length === 0) {
56 return next(e)
57 }
58
59 const { Box, Text } = $.ui.resolve(e)
60 const done = list.filter(t => t.status === 'completed').length
61 const room = Math.max(2, Math.min(6, e.props.maxRows - 3))
62 const cur = Math.max(0, list.findIndex(t => t.status === 'in_progress'))
63 const start = Math.max(0, Math.min(cur - 1, list.length - room))
64 const shown = list.slice(start, start + room)
65 const filled = Math.round((done / list.length) * 12)
66
67 return (
68 <Box flexDirection="column" borderStyle="round" borderColor={ACCENT} paddingX={1}>
69 <Text>
70 <Text bold color={ACCENT}>Plan </Text>
71 <Text color={GREEN}>{'█'.repeat(filled)}</Text>
72 <Text dimColor>{'░'.repeat(12 - filled)}</Text>
73 <Text dimColor> {done}/{list.length} done, {list.length - done} left</Text>
74 </Text>
75 {shown.map(t => {
76 const cur = t.status === 'in_progress'
77 const ok = t.status === 'completed'
78
79 return (
80 <Text color={ok ? GREEN : cur ? AMBER : undefined} dimColor={t.status === 'pending'} bold={cur} strikethrough={ok}>
81 {ok ? '✔ ' : cur ? '▶ ' : '○ '}
82 {cur ? t.activeForm : t.content}
83 </Text>
84 )
85 })}
86 {list.length > shown.length && <Text dimColor>… {list.length - shown.length} more</Text>}
87 </Box>
88 )
89 })
90}
91types/index.d.ts 8 lines1export type Todo = { content: string; status: 'pending' | 'in_progress' | 'completed'; activeForm: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'clean-view': { todos: Todo[]; isOn: boolean }
6 }
7}
8