SLOPSHOPPER

clean-view

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

newbandrowsguardcommand
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 › /cleanview ⎿ clean-view: Clean View off. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

clean-view

Hides the tool calls and code that scroll by while Claude works, and shows a short checklist of the plan instead.

What it does

  • Hides tool rows: the tool-call rows, their results, grouped calls and the "run in background" hint are not drawn.
  • Plan band: while Claude is working and has a todo list, a band above the prompt shows it:
╭──────────────────────────────────────╮
│ 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.

  • Collapses when idle: the band disappears when the turn ends or when there is no plan. It never takes more than 6 rows (fewer on a short terminal); extra steps show as "… N more".
  • /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.

Install and run

claude --plugin-dir /path/to/claude-code-mods/clean-view

To load it in every session, see Install.

Costs

None. It makes no model calls.

Source 2 files
hooks/register.tsx 91 lines
1import { 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}
91
types/index.d.ts 8 lines
1export 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