SLOPSHOPPER

clean-view

Hides tool rows and shows a plain-English checklist of Claude's plan above the prompt

bandrowsguardcommandtoastprompt
v0.1.0no licenseupdated 2026-10-13hassanhmd23/claude-plugins/plugins/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 ╭─────────────────────────────────────────╮ │ clean-view │ ⏺ Read(src/auth.ts) │ Clean View is off: showing every detail │ ⎿ Read 6 lines ╰─────────────────────────────────────────╯ ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Read(/work/app/src/auth.ts) ⎿ Denied by clean-view: Clean View: call mcp__clean-view__plan_steps with the steps of this job before any o ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /simple ⎿ clean-view: Clean View is off. [ ○ Clean View: OFF ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
[ ○ Clean View: OFF ]
README

clean-view

A Claude Code mod that makes Claude calm and friendly to watch. While Claude works, it hides tool calls, file diffs and command output. In their place, one plain checklist sits above the prompt:

Build my landing page · 1m 12s                     [ ● Clean View: ON ]
✓ Read your brand notes            ██████████  Done
▶ Build the pricing section        ██████░░░░  60%
○ Add the contact form             ░░░░░░░░░░  Next
○ Polish the footer                ░░░░░░░░░░  Up next

The header says Needs you when Claude waits for your OK or your reply, Stuck when something goes wrong, Stopped after Esc, and All done when the job finishes.

Turn it on or off with the button at the right of the band, or type /simple on, /simple off or /simple. Your choice is remembered.

Install

Type at the prompt of a Claude Code terminal session:

/plugin install clean-view --marketplace hassanhmd23/claude-plugins

Answer y to add the marketplace, then pick a scope.

Source 3 files
hooks/register.tsx 8 lines
1import type { Register } from 'claude-code'
2
3import { registerCleanView } from './clean-view'
4
5export const register: Register = on => {
6  registerCleanView(on)
7}
8
hooks/clean-view.tsx 507 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, On, Timer } from 'claude-code'
3
4import type { Checklist, Task, TaskStatus } from '../types'
5
6type $ = EngineInterface
7
8const PLUGIN = 'clean-view'
9const PLAN_TOOL = `mcp__${PLUGIN}__plan_steps`
10const REPORT_TOOL = `mcp__${PLUGIN}__report_progress`
11const ALWAYS_ALLOWED = new Set(['ToolSearch', 'TodoWrite', 'TaskCreate', 'TaskUpdate', 'AskUserQuestion'])
12const METER = 10
13const MAX_NAME = 40
14const FALLBACK_NAME = 'Working on it'
15
16const enabled = atom({ plugin: 'clean-view', key: 'cleanViewEnabled' } as const, true)
17const checklist = atom({ plugin: 'clean-view', key: 'checklist' } as const, null)
18const tick = atom({ plugin: 'clean-view', key: 'tick' } as const, 0)
19
20const SAID_NO = 'you said no to a step, so Claude paused'
21const KEEPS_FAILING = 'a step keeps failing, Claude is trying another way'
22const WAITING_FOR_REPLY = 'Claude is waiting for your reply'
23const NEEDS_OK = 'Claude needs your OK to continue'
24const HAS_QUESTION = 'Claude has a question for you'
25
26const GUIDE = `## Clean View is on
27
28The person sees a plain checklist of your plan instead of your tool calls. Keep it accurate:
29- For every request, even a quick question, call \`${PLAN_TOOL}\` first with every step of the job in order (2 to 8 steps). If it is deferred, load it with ToolSearch first. If this session has TodoWrite or TaskCreate you may use your to-do list as the plan instead.
30- Then call \`${REPORT_TOOL}\` as real progress happens, and with percent 100 the moment a step finishes.
31- Write every step name in plain English a non-technical person understands, under 40 characters, starting with a verb, like "Build the pricing section".
32- Never put file paths, file names, commands, code or tool names in a step name.`
33
34// ---------- plain names ----------
35
36const CODE_FILE = /\b[\w.-]+\.(?:[cm]?[jt]sx?|py|rb|go|rs|java|kt|swift|c|h|cc|cpp|hpp|cs|php|json|ya?ml|toml|ini|md|mdx|css|scss|less|html?|vue|svelte|sh|bash|zsh|sql|lock|env|xml|gradle|txt)\b/gi
37
38export function cleanName(raw: string): string {
39  const text = raw
40    .replace(/`[^`]*`?/g, ' ')
41    .replace(/\S*\/\S*/g, ' ')
42    .replace(CODE_FILE, ' ')
43    .replace(/\s+/g, ' ')
44    .trim()
45  if (text === '') return FALLBACK_NAME
46  const name = text[0].toUpperCase() + text.slice(1)
47  if (name.length <= MAX_NAME) return name
48  const cut = name.slice(0, MAX_NAME - 1)
49  const space = cut.lastIndexOf(' ')
50
51  return `${(space > 0 ? cut.slice(0, space) : cut).trimEnd()}…`
52}
53
54// ---------- the checklist, as pure steps ----------
55
56const task = (name: string, status: TaskStatus, id = name): Task => ({
57  id,
58  name: cleanName(name),
59  status,
60  percent: status === 'done' ? 100 : 0,
61  hasReported: status === 'done',
62})
63
64/** One task active: the first not done, when none is. */
65function settle(tasks: Task[]): Task[] {
66  if (tasks.some(t => t.status === 'active')) return tasks
67  const next = tasks.findIndex(t => t.status === 'upcoming')
68
69  return tasks.map((t, i) => (i === next ? { ...t, status: 'active' } : t))
70}
71
72export function startJob(prompt: string, now: number): Checklist {
73  return {
74    title: cleanName(prompt.split(/\s+/).slice(0, 6).join(' ')),
75    phase: 'working',
76    tasks: [task('Understand your request', 'active'), task('Plan the steps', 'upcoming')],
77    hasPlan: false,
78    needsYouReason: null,
79    stuckReason: null,
80    startedAt: now,
81    finishedAt: null,
82    isCollapsed: false,
83  }
84}
85
86export function planSteps(list: Checklist, steps: readonly string[]): Checklist {
87  const tasks = steps.map((s, i) => task(s, i === 0 ? 'active' : 'upcoming', `step-${i}`))
88
89  return { ...list, tasks, hasPlan: true }
90}
91
92export function reportProgress(list: Checklist, name: string, percent: number): Checklist {
93  const clean = cleanName(name)
94  const pct = Math.max(0, Math.min(100, Math.round(Number.isFinite(percent) ? percent : 0)))
95  let tasks = [...list.tasks]
96  let at = tasks.findIndex(t => t.name.toLowerCase() === clean.toLowerCase())
97  if (at === -1) {
98    // A new step goes after the last one already started.
99    const lastStarted = tasks.map(t => t.status !== 'upcoming').lastIndexOf(true)
100    at = lastStarted + 1
101    tasks.splice(at, 0, task(clean, 'upcoming', `step-${tasks.length}-${clean}`))
102  }
103  tasks = tasks.map((t, i): Task => {
104    if (i < at) return t.status === 'done' ? t : { ...t, status: 'done', percent: 100, hasReported: true }
105    if (i === at) return { ...t, status: pct >= 100 ? 'done' : 'active', percent: pct, hasReported: true }
106
107    return t.status === 'active' ? { ...t, status: 'upcoming' } : t
108  })
109
110  return { ...list, tasks: settle(tasks), hasPlan: true }
111}
112
113type Todo = { content: string; status: 'pending' | 'in_progress' | 'completed' }
114const FROM_TODO = { pending: 'upcoming', in_progress: 'active', completed: 'done' } as const
115
116export function fromTodos(list: Checklist, todos: readonly Todo[]): Checklist {
117  const tasks = todos.map((todo, i) => {
118    const fresh = task(todo.content, FROM_TODO[todo.status], `todo-${i}`)
119    const old = list.tasks.find(t => t.name === fresh.name)
120    return fresh.status === 'active' && old?.status === 'active' ? { ...fresh, percent: old.percent, hasReported: old.hasReported } : fresh
121  })
122
123  return { ...list, tasks: settle(tasks), hasPlan: true }
124}
125
126export function addTask(list: Checklist, id: string, subject: string): Checklist {
127  const tasks = list.hasPlan ? list.tasks : []
128
129  return { ...list, tasks: settle([...tasks, task(subject, 'upcoming', id)]), hasPlan: true }
130}
131
132export function updateTask(list: Checklist, id: string, status?: string, subject?: string): Checklist {
133  if (status === 'deleted') return { ...list, tasks: settle(list.tasks.filter(t => t.id !== id)) }
134  const tasks = list.tasks.map((t): Task => {
135    if (t.id !== id) return t
136    const named = subject ? { ...t, name: cleanName(subject) } : t
137    if (status === 'completed') return { ...named, status: 'done', percent: 100, hasReported: true }
138    if (status === 'in_progress') return { ...named, status: 'active' }
139    if (status === 'pending') return { ...named, status: 'upcoming' }
140    return named
141  })
142  const active = tasks.find(t => t.id === id && t.status === 'active')
143
144  return {
145    ...list,
146    tasks: settle(active ? tasks.map(t => (t.status === 'active' && t !== active ? { ...t, status: 'upcoming' } : t)) : tasks),
147  }
148}
149
150// ---------- calm words for what went wrong ----------
151
152export function apiErrorReason(kind: string, details = ''): string {
153  if (/too long|context|max_output/i.test(details) || kind === 'max_output_tokens') return 'type /compact and try again'
154  if (kind === 'rate_limit' || kind === 'billing_error') return 'you hit your usage limit, try again a little later'
155  if (kind === 'overloaded' || kind === 'server_error') return "Claude's servers are busy, try again in a minute"
156  if (/auth|oauth|credential|verification|account/.test(kind)) return 'type /login'
157  if (/network|connect|ECONN|ENOTFOUND|fetch failed|socket|timed? ?out/i.test(details) || kind === 'unknown') {
158    return 'the internet connection dropped'
159  }
160
161  return 'something went wrong, try again'
162}
163
164// ---------- drawing ----------
165
166const duration = (ms: number) => {
167  const s = Math.max(0, Math.round(ms / 1000))
168  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
169}
170
171function meter(t: Task, frame: number): string {
172  if (t.status === 'done') return '█'.repeat(METER)
173  if (t.status === 'upcoming') return '░'.repeat(METER)
174  if (t.hasReported) {
175    const filled = Math.round(t.percent / 10)
176    return '█'.repeat(filled) + '░'.repeat(METER - filled)
177  }
178  const at = frame % (METER + 2)
179  return Array.from({ length: METER }, (_, i) => (i >= at - 2 && i <= at ? '█' : '░')).join('')
180}
181
182const fit = (text: string, width: number) =>
183  text.length <= width ? text.padEnd(width) : `${text.slice(0, Math.max(0, width - 1))}…`
184
185// ---------- the mod ----------
186
187let clock: Timer | undefined
188let failures = 0
189let lastApiError: { kind: string; details: string } | undefined
190
191const isOn = ($: $) => read($, enabled)
192const setList = ($: $, fn: (list: Checklist) => Checklist) =>
193  update($, checklist, list => (list === null ? list : fn(list)))
194
195/** The 250ms clock runs only while a job is working or waiting on the person. */
196async function syncClock($: $) {
197  const list = await read($, checklist)
198  const isLive = list !== null && (list.phase === 'working' || list.phase === 'needs-you')
199  if (isLive && clock === undefined) clock = $.clock.every(250, () => void update($, tick, n => n + 1))
200  if (!isLive && clock !== undefined) {
201    clock.cancel()
202    clock = undefined
203  }
204}
205
206async function change($: $, fn: (list: Checklist) => Checklist) {
207  await setList($, fn)
208  await syncClock($)
209}
210
211const working = (list: Checklist): Checklist => ({ ...list, phase: 'working', needsYouReason: null, stuckReason: null })
212const needsYou = (reason: string) => (list: Checklist): Checklist => ({ ...list, phase: 'needs-you', needsYouReason: reason })
213const stuck = (reason: string) => (list: Checklist): Checklist => ({ ...list, phase: 'stuck', stuckReason: reason })
214
215async function finish($: $, fn: (list: Checklist) => Checklist) {
216  const now = await $.clock.now()
217  await change($, list => ({ ...fn(list), finishedAt: now }))
218}
219
220async function setEnabled($: $, value: boolean) {
221  await update($, enabled, () => value)
222  await $.store.set('cleanViewEnabled', value)
223  $.ui.toast(value ? 'Clean View is on: tool details are hidden' : 'Clean View is off: showing every detail')
224}
225
226async function nameJob($: $, prompt: string, startedAt: number) {
227  const reply = await $.model.complete({
228    model: 'haiku',
229    effort: 'low',
230    maxTokens: 30,
231    system: 'You name tasks for people who are not technical. Answer with the name alone.',
232    prompt: `Name this request in 2 to 6 plain words, starting with a verb, no punctuation:\n\n${prompt.slice(0, 2000)}`,
233  })
234  if (!reply.isAnswered) return
235  const title = cleanName(reply.text.replace(/["'.]/g, ''))
236  await setList($, list => (list.startedAt === startedAt ? { ...list, title } : list))
237}
238
239export function registerCleanView(on: On) {
240  on('session.start', async ($, e, next) => {
241    const saved = await $.store.get('cleanViewEnabled')
242    await update($, enabled, () => saved !== false)
243    await $.tool.register({
244      name: 'plan_steps',
245      isDeferred: false,
246      description:
247        'Lay out every step of the job up front, 2 to 8 short plain-English names in order, before any other tool. The first step starts right away.',
248      inputSchema: {
249        type: 'object',
250        properties: { steps: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 8 } },
251        required: ['steps'],
252      },
253    })
254    await $.tool.register({
255      name: 'report_progress',
256      isDeferred: false,
257      description:
258        'Report progress on the current step by its plan name, 0 to 100. Send 100 the moment a step finishes; the next one starts on its own.',
259      inputSchema: {
260        type: 'object',
261        properties: { task: { type: 'string' }, percent: { type: 'number', minimum: 0, maximum: 100 } },
262        required: ['task', 'percent'],
263      },
264    })
265    await $.command.register({ name: 'simple', description: 'Turn Clean View on or off', argumentHint: 'on|off' })
266    await syncClock($)
267
268    return next(e)
269  })
270
271  on('command.run', { command: 'simple' }, async ($, e) => {
272    const arg = e.args.trim().toLowerCase()
273    const value = arg === 'on' ? true : arg === 'off' ? false : !(await isOn($))
274    await setEnabled($, value)
275
276    return { text: `Clean View is ${value ? 'on' : 'off'}.` }
277  })
278
279  on('prompt.compose', async ($, e, next) => {
280    const composed = await next(e)
281    if (!(await isOn($))) return composed
282
283    return { sections: [...composed.sections, { id: `${PLUGIN}:guide`, text: GUIDE, scope: 'session' }] }
284  })
285
286  on('turn.start', async ($, e, next) => {
287    const text = e.text.trim()
288    if (text === '' || text.startsWith('/')) return next(e)
289    const list = await read($, checklist)
290    if (list?.phase === 'working') return next(e)
291    if (list?.phase === 'needs-you') {
292      await change($, working)
293      return next(e)
294    }
295    const now = await $.clock.now()
296    failures = 0
297    await update($, checklist, () => startJob(text, now))
298    await syncClock($)
299    $.clock.after(0, () => void nameJob($, text, now))
300
301    return next(e)
302  })
303
304  on('tool.call', { tool: PLAN_TOOL }, async ($, e) => {
305    const steps = (Array.isArray(e.steps) ? e.steps : []).map(String).slice(0, 8)
306    if (steps.length === 0) return { deny: 'plan_steps needs at least one step name.' }
307    const now = await $.clock.now()
308    await update($, checklist, list => planSteps(working(list ?? startJob(steps[0], now)), steps))
309    await syncClock($)
310    const text = `Planned ${steps.length} steps. The first one has started.`
311
312    return { result: text, text }
313  })
314
315  on('tool.call', { tool: REPORT_TOOL }, async ($, e) => {
316    const list = await read($, checklist)
317    if (list === null || !list.hasPlan) return { deny: `Call ${PLAN_TOOL} first.` }
318    const percent = Math.max(0, Math.min(100, Math.round(Number(e.percent) || 0)))
319    await change($, l => reportProgress(working(l), String(e.task ?? ''), percent))
320    const text = `Progress noted: ${percent}%.`
321
322    return { result: text, text }
323  })
324
325  on('tool.call', async ($, e, next) => {
326    if (e.agentId !== undefined || e.tool === PLAN_TOOL || e.tool === REPORT_TOOL) return next(e)
327    const isEnabled = await isOn($)
328    const list = await read($, checklist)
329    if (isEnabled && !ALWAYS_ALLOWED.has(e.tool) && !list?.hasPlan) {
330      return { deny: `Clean View: call ${PLAN_TOOL} with the steps of this job before any other tool.` }
331    }
332    if (e.tool === 'AskUserQuestion') await change($, needsYou(HAS_QUESTION))
333    else if (list?.phase === 'needs-you') await change($, working)
334
335    const ran = await next(e)
336
337    if (ran.isError) {
338      if (/doesn't want to proceed|user (denied|rejected|declined)|permission .*denied/i.test(ran.text ?? '')) {
339        await change($, stuck(SAID_NO))
340      } else if (++failures >= 3) {
341        await change($, stuck(KEEPS_FAILING))
342      }
343    } else if (ran.deny === undefined) {
344      failures = 0
345      const now = await read($, checklist)
346      if (now?.phase === 'stuck' || now?.phase === 'needs-you') await change($, working)
347      if (e.tool === 'TodoWrite') await change($, l => fromTodos(l, e.todos))
348      if (e.tool === 'TaskCreate') {
349        const id = (ran.result as { task?: { id: string } } | undefined)?.task?.id
350        if (id !== undefined) await change($, l => addTask(l, id, e.subject))
351      }
352      if (e.tool === 'TaskUpdate') await change($, l => updateTask(l, e.taskId, e.status, e.subject))
353    }
354
355    return ran
356  }).catch(($, e, next) => next(e))
357
358  on('classic.Notification', async ($, e, next) => {
359    if (/permission/i.test(e.notification_type)) await change($, needsYou(NEEDS_OK))
360
361    return next(e)
362  })
363
364  on('classic.PermissionDenied', async ($, e, next) => {
365    await change($, stuck(SAID_NO))
366
367    return next(e)
368  })
369
370  on('classic.StopFailure', async ($, e, next) => {
371    lastApiError = { kind: e.error, details: e.error_details ?? '' }
372    await finish($, stuck(apiErrorReason(e.error, e.error_details)))
373
374    return next(e)
375  })
376
377  on('turn.complete', async ($, e, next) => {
378    if (e.agentId !== undefined) return next(e)
379    const list = await read($, checklist)
380    if (list === null || list.phase === 'done' || list.phase === 'stopped') return next(e)
381
382    if (e.reason === 'error') {
383      const err = lastApiError ?? { kind: 'unknown', details: '' }
384      await finish($, stuck(apiErrorReason(err.kind, err.details)))
385    } else if (e.reason === 'refusal') {
386      await finish($, stuck("Claude couldn't help with that request"))
387    } else if (e.reason === 'aborted') {
388      await finish($, l => (l.stuckReason === SAID_NO ? l : { ...l, phase: 'stopped' }))
389    } else if (list.hasPlan && list.tasks.some(t => t.status !== 'done')) {
390      await finish($, needsYou(WAITING_FOR_REPLY))
391    } else {
392      await finish($, l => ({
393        ...l,
394        phase: 'done',
395        needsYouReason: null,
396        stuckReason: null,
397        tasks: l.tasks.map(t => ({ ...t, status: 'done', percent: 100, hasReported: true })),
398      }))
399      const startedAt = list.startedAt
400      $.clock.after(5000, () =>
401        void setList($, l => (l.startedAt === startedAt && l.phase === 'done' ? { ...l, isCollapsed: true } : l)),
402      )
403    }
404    lastApiError = undefined
405
406    return next(e)
407  })
408
409  // ---------- hide the technical rows ----------
410
411  on('ui.render', { component: ['ToolUse', 'ToolResult', 'ToolGroup'] }, async ($, e, next) => {
412    if (!(await isOn($))) return next(e)
413    const { Box } = $.ui.resolve(e)
414
415    return <Box display="none" />
416  })
417
418  on('ui.render', { component: 'ToolProgress' }, async ($, e, next) =>
419    (await isOn($)) ? next({ ...e, props: { ...e.props, hint: '' } }) : next(e),
420  )
421
422  // ---------- the band ----------
423
424  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
425    if (e.props.hasSurvey) return next(e)
426    const { Box, Text, Button } = $.ui.resolve(e)
427    const isEnabled = await isOn($)
428    const list = isEnabled ? await read($, checklist) : null
429    const frame = await read($, tick)
430    const now = await $.clock.now()
431    const width = e.props.bodyColumns
432
433    const toggle = (
434      <Button
435        key="clean-view-toggle"
436        label={isEnabled ? '● Clean View: ON' : '○ Clean View: OFF'}
437        variant={isEnabled ? 'primary' : 'secondary'}
438        onPress={() => setEnabled($, !isEnabled)}
439      />
440    )
441
442    let header = <Text> </Text>
443    if (list !== null) {
444      const elapsed = duration((list.finishedAt ?? now) - list.startedAt)
445      if (list.phase === 'working') header = <Text bold wrap="truncate-end">{list.title} <Text dimColor>· {elapsed}</Text></Text>
446      if (list.phase === 'needs-you') {
447        header = (
448          <Text wrap="truncate-end">
449            <Text bold inverse color="warning"> Needs you </Text> {list.needsYouReason}
450          </Text>
451        )
452      }
453      if (list.phase === 'stuck') header = <Text color="warning" wrap="truncate-end">⚠ Stuck: {list.stuckReason}</Text>
454      if (list.phase === 'stopped') header = <Text wrap="truncate-end">■ Stopped · {list.title} · <Text dimColor>you pressed Esc</Text></Text>
455      if (list.phase === 'done') {
456        header = (
457          <Text wrap="truncate-end">
458            <Text color="success">✓ All done</Text> · {list.title} · <Text dimColor>took {elapsed}</Text>
459          </Text>
460        )
461      }
462    }
463
464    const headerRow = (
465      <Box flexDirection="row" justifyContent="space-between">
466        <Box flexShrink={1}>{header}</Box>
467        {toggle}
468      </Box>
469    )
470    if (list === null || (list.phase === 'done' && list.isCollapsed)) return headerRow
471
472    const nameWidth = Math.max(8, width - 2 - 1 - METER - 2 - 7)
473    const firstUpcoming = list.tasks.findIndex(t => t.status === 'upcoming')
474    const rows = list.tasks.map((t, i) => {
475      const name = fit(t.name, nameWidth)
476      const bar = meter(t, frame)
477      if (t.status === 'done') {
478        return (
479          <Text key={t.id}>
480            <Text color="success">✓ </Text><Text dimColor>{name} </Text><Text color="success">{bar}</Text>  Done
481          </Text>
482        )
483      }
484      if (t.status === 'active') {
485        const icon = list.phase === 'needs-you' ? '‖ ' : '▶ '
486        return (
487          <Text key={t.id}>
488            <Text color="claude">{icon}</Text><Text bold>{name} </Text><Text color="claude">{bar}</Text>  {t.hasReported ? `${t.percent}%` : 'Working'}
489          </Text>
490        )
491      }
492      return (
493        <Text key={t.id} dimColor>
494          ○ {name} {bar}  {i === firstUpcoming ? 'Next' : 'Up next'}
495        </Text>
496      )
497    })
498
499    return (
500      <Box flexDirection="column">
501        {headerRow}
502        {rows}
503      </Box>
504    )
505  })
506}
507
types/index.d.ts 35 lines
1export type TaskStatus = 'done' | 'active' | 'upcoming'
2
3export type Task = {
4  id: string
5  name: string
6  status: TaskStatus
7  percent: number
8  hasReported: boolean
9}
10
11export type Phase = 'working' | 'needs-you' | 'stuck' | 'stopped' | 'done'
12
13export type Checklist = {
14  title: string
15  phase: Phase
16  tasks: Task[]
17  /** False while only the placeholder steps show. */
18  hasPlan: boolean
19  needsYouReason: string | null
20  stuckReason: string | null
21  startedAt: number
22  finishedAt: number | null
23  isCollapsed: boolean
24}
25
26declare module 'claude-code' {
27  interface PluginState {
28    'clean-view': {
29      cleanViewEnabled: boolean
30      checklist: Checklist | null
31      tick: number
32    }
33  }
34}
35