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

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
/compact.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.
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).
claude plugin validate memory-bar
claude plugin test memory-barhooks/register.tsx 194 lines1import { 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}
194types/index.d.ts 10 lines1export 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