SLOPSHOPPER

task-steps

Shows the steps Claude plans for your task, and its progress through them, above the prompt.

newbandguardprompttool
v0.1.0no licenseupdated 2026-10-10moonteek/claude-mods/task-steps
A shopper browsing a rack in a slop shop
README

claude-mods

Two small mods for Claude Code that draw above the prompt.

◇ Build a login page  ━━━━━──────────  1/3
  ✓ Set up routes
  ◉ Building the form…
  ○ Write tests
◑ ━━━━━━━━━──────────  45%  90k/200k
  • memory-bar: how full Claude's context window (its working memory) is. The ○◔◑◕● icon fills with it; the bar turns amber at 60% and red at 85%, where it suggests /compact.
  • task-steps: the steps Claude plans for a task, ticked off as it works. It reads Claude's to-do list (TodoWrite / TaskCreate) and also gives Claude a steps tool where those aren't available.

The two stack: the task checklist on top, the memory bar beneath.

Install

In Claude Code:

/plugin install memory-bar --marketplace moonteek/claude-mods
/plugin install task-steps --marketplace moonteek/claude-mods

Answer y to add the marketplace, then pick a scope (user scope loads them in every session).

Develop

claude plugin validate memory-bar
claude plugin test memory-bar
Source 2 files
hooks/register.tsx 194 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Step, StepStatus, Task } from '../types'
5
6const ACCENT = '#D97757'
7const BAR = 14
8const task = atom({ plugin: 'task-steps', key: 'task' } as const, null)
9const prompt = atom({ plugin: 'task-steps', key: 'prompt' } as const, '')
10
11const cut = (text: string, room: number) =>
12  text.length <= room ? text : `${text.slice(0, Math.max(1, room - 1))}…`
13
14const isFinished = (current: Task | null) =>
15  current === null || current.steps.every(step => step.status === 'completed')
16
17// The first list after a prompt (or after the last list finished) takes that prompt as its title.
18const withSteps = (current: Task | null, title: string, steps: Step[]): Task | null =>
19  steps.length === 0 ? null : { title: isFinished(current) ? title : (current?.title ?? title), steps }
20
21type Todo = { content: string; activeForm: string; status: StepStatus }
22
23const showTodos = async ($: Parameters<typeof read>[0], todos: readonly Todo[]) => {
24  const title = await read($, prompt)
25  const steps = todos.map((todo, i) => ({
26    id: String(i),
27    label: todo.content,
28    active: todo.activeForm,
29    status: todo.status,
30  }))
31  await update($, task, current => withSteps(current, title, steps))
32}
33
34export const register: Register = on => {
35  on('prompt.submit', async ($, e, next) => {
36    const line = e.text.trim().split('\n')[0] ?? ''
37    if (line !== '' && !line.startsWith('/')) {
38      // A new prompt is a new task: clear the old checklist so the next plan starts fresh under this title.
39      await update($, prompt, () => line)
40      await update($, task, () => null)
41    }
42    return next(e)
43  }).catch(($, e, next) => next(e))
44
45  // Where neither TodoWrite nor TaskCreate is on hand, Claude reports its plan through this mod's own tool.
46  on('session.start', async ($, e, next) => {
47    const result = await next(e)
48    await $.tool.register({
49      name: 'steps',
50      description:
51        'Show the user a live checklist of the steps for their current task, above their prompt. ' +
52        'For any task with 3 or more steps: call it once with the full plan before starting, then again ' +
53        'each time a step starts or finishes, always sending the whole list. Keep exactly one step ' +
54        'in_progress while working, and mark every step completed when done.',
55      inputSchema: {
56        type: 'object',
57        properties: {
58          steps: {
59            type: 'array',
60            items: {
61              type: 'object',
62              properties: {
63                content: { type: 'string', description: 'The step, imperative: "Write tests"' },
64                activeForm: { type: 'string', description: 'The step while it runs: "Writing tests"' },
65                status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
66              },
67              required: ['content', 'activeForm', 'status'],
68            },
69          },
70        },
71        required: ['steps'],
72      },
73      isDeferred: false,
74    })
75    return result
76  }).catch(($, e, next) => next(e))
77
78  on('tool.call', { tool: /^mcp__task-steps__steps$/ }, async ($, e) => {
79    const todos = (e as unknown as { steps?: Todo[] }).steps ?? []
80    await showTodos($, todos)
81    const done = todos.filter(todo => todo.status === 'completed').length
82    return { result: `Checklist shown: ${done}/${todos.length} steps done.` }
83  })
84
85  // Claude's plan arrives whole through TodoWrite...
86  on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
87    const ran = await next(e)
88    if (ran.deny === undefined && ran.isError !== true) {
89      await showTodos($, e.todos)
90    }
91    return ran
92  }).catch(($, e, next) => next(e))
93
94  // ...or one step at a time through TaskCreate and TaskUpdate.
95  on('tool.call', { tool: 'TaskCreate' }, async ($, e, next) => {
96    const ran = await next(e)
97    if (ran.deny === undefined && ran.isError !== true) {
98      const id = (ran.result as { task?: { id?: string } } | undefined)?.task?.id ?? e.subject
99      const title = await read($, prompt)
100      const step: Step = { id, label: e.subject, active: e.activeForm ?? e.subject, status: 'pending' }
101      await update($, task, current =>
102        withSteps(current, title, [...(isFinished(current) ? [] : (current?.steps ?? [])), step]),
103      )
104    }
105    return ran
106  }).catch(($, e, next) => next(e))
107
108  on('tool.call', { tool: 'TaskUpdate' }, async ($, e, next) => {
109    const ran = await next(e)
110    if (ran.deny === undefined && ran.isError !== true) {
111      await update($, task, current => {
112        if (current === null) return null
113        const steps = current.steps
114          .filter(step => !(step.id === e.taskId && e.status === 'deleted'))
115          .map(step =>
116            step.id !== e.taskId
117              ? step
118              : {
119                  ...step,
120                  label: e.subject ?? step.label,
121                  active: e.activeForm ?? step.active,
122                  status: (e.status === undefined || e.status === 'deleted' ? step.status : e.status) as StepStatus,
123                },
124          )
125        return steps.length === 0 ? null : { ...current, steps }
126      })
127    }
128    return ran
129  }).catch(($, e, next) => next(e))
130
131  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
132    const below = await next(e)
133    const current = await read($, task)
134    if (e.props.hasSurvey || current === null) {
135      return below
136    }
137
138    const { Box, Text } = $.ui.resolve(e)
139    const width = Math.max(30, e.props.bodyColumns)
140    const total = current.steps.length
141    const done = current.steps.filter(step => step.status === 'completed').length
142    const filled = Math.round((done / total) * BAR)
143    const isDone = done === total
144
145    // Show a window of steps around the one in hand when they don't all fit.
146    const room = Math.max(1, Math.min(8, e.props.maxRows - 4))
147    const focus = Math.max(0, current.steps.findIndex(step => step.status !== 'completed'))
148    const start = Math.max(0, Math.min(focus - 1, total - room))
149    const shown = current.steps.slice(start, start + room)
150    const hidden = total - shown.length
151
152    const row = (step: Step) => {
153      if (step.status === 'completed') {
154        return (
155          <Box>
156            <Text color={ACCENT}>{'  ✓ '}</Text>
157            <Text dimColor strikethrough>{cut(step.label, width - 4)}</Text>
158          </Box>
159        )
160      }
161      if (step.status === 'in_progress') {
162        return (
163          <Box>
164            <Text color={ACCENT} bold>{'  ◉ '}</Text>
165            <Text bold>{cut(`${step.active}…`, width - 4)}</Text>
166          </Box>
167        )
168      }
169      return (
170        <Box>
171          <Text dimColor>{`  ○ ${cut(step.label, width - 4)}`}</Text>
172        </Box>
173      )
174    }
175
176    return (
177      <Box flexDirection="column">
178        <Box>
179          <Text color={ACCENT}>{isDone ? '◆ ' : '◇ '}</Text>
180          <Text bold>{cut(current.title || 'Task', Math.max(8, width - BAR - 16))}</Text>
181          <Text>{'  '}</Text>
182          <Text color={ACCENT}>{'━'.repeat(filled)}</Text>
183          <Text dimColor>{'─'.repeat(BAR - filled)}</Text>
184          <Text color={ACCENT} bold>{`  ${done}/${total}`}</Text>
185          {isDone ? <Text dimColor>{'  done'}</Text> : null}
186        </Box>
187        {isDone ? null : shown.map(row)}
188        {!isDone && hidden > 0 ? <Text dimColor>{`    +${hidden} more`}</Text> : null}
189        {below}
190      </Box>
191    )
192  })
193}
194
types/index.d.ts 10 lines
1export type StepStatus = 'pending' | 'in_progress' | 'completed'
2export type Step = { id: string; label: string; active: string; status: StepStatus }
3export type Task = { title: string; steps: Step[] }
4
5declare module 'claude-code' {
6  interface PluginState {
7    'task-steps': { task: Task | null; prompt: string }
8  }
9}
10