Band above the prompt showing the current plan step and a progress bar

Mod do Claude Code, który nad promptem pokazuje etap zaplanowanej pracy i pasek postępu.
◆ plan etap 4/6 · Dodaję testy 50% [ Lista ] [ Ukryj ]
██████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
✓ 1. Przeanalizuj kod
✓ 2. Zaprojektuj API
✓ 3. Napisz endpointy
▶ 4. Dodaję testy
○ 5. Zaktualizuj dokumentację
○ 6. Zrób code review
/plan-progress przełącza widocznośćTodoWrite albo TaskCreate / TaskUpdate / TaskListmcp__plan-progress__plan (na sesje bez wbudowanych narzędzi do planowania); jest odroczone, więc dopóki model go nie użyje, w kontekście jest tylko jego nazwa/plugin install plan-progress --marketplace DamianoCode/claude-plan-progress
Potwierdź dodanie marketplace (y) i wybierz zakres (domyślnie user).
git clone https://github.com/DamianoCode/claude-plan-progress
claude --plugin-dir ./claude-plan-progress
Mod działa tylko w sesji uruchomionej z tą flagą.
claude plugin validate .
claude plugin test .
Mod to moduł function hooks (hooks/register.tsx); przy --plugin-dir przeładowuje się po każdej zmianie pliku.
hooks/register.tsx 220 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Plan, Step } from '../types'
5
6const plan = atom({ plugin: 'plan-progress', key: 'plan' } as const, null)
7const isExpanded = atom({ plugin: 'plan-progress', key: 'isExpanded' } as const, false)
8const isHidden = atom({ plugin: 'plan-progress', key: 'isHidden' } as const, false)
9
10const PAD = 1
11const PLAN_TOOL = 'mcp__plan-progress__plan'
12
13const isDone = (p: Plan | null) => p !== null && p.steps.every(s => s.status === 'completed')
14
15const bar = (ratio: number, width: number) => {
16 const filled = Math.round(ratio * width)
17 return ['█'.repeat(filled), '░'.repeat(width - filled)] as const
18}
19
20export const register: Register = on => {
21 // TodoWrite sends the whole list each time.
22 on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
23 const ran = await next(e)
24 if (ran.deny === undefined && !ran.isError) {
25 const steps: Step[] = e.todos.map((t, i) => ({
26 id: String(i),
27 title: t.content,
28 active: t.activeForm,
29 status: t.status,
30 }))
31 await update($, plan, () => (steps.length ? { steps } : null))
32 await update($, isHidden, () => false)
33 }
34 return ran
35 }).catch(($, e, next) => next(e))
36
37 // TaskCreate/TaskUpdate build the list one task at a time.
38 on('tool.call', { tool: 'TaskCreate' }, async ($, e, next) => {
39 const ran = await next(e)
40 if (ran.deny === undefined && !ran.isError) {
41 const step: Step = {
42 id: ran.result.task.id,
43 title: e.subject,
44 active: e.activeForm ?? e.subject,
45 status: 'pending',
46 }
47 // A new task after a finished plan starts a new plan.
48 await update($, plan, p => ({ steps: [...(p === null || isDone(p) ? [] : p.steps), step] }))
49 await update($, isHidden, () => false)
50 }
51 return ran
52 }).catch(($, e, next) => next(e))
53
54 on('tool.call', { tool: 'TaskUpdate' }, async ($, e, next) => {
55 const ran = await next(e)
56 if (ran.deny === undefined && !ran.isError && ran.result.success) {
57 await update($, plan, p => {
58 if (p === null) return p
59 const { status } = e
60 const steps = status === 'deleted'
61 ? p.steps.filter(s => s.id !== e.taskId)
62 : p.steps.map(s =>
63 s.id !== e.taskId
64 ? s
65 : {
66 ...s,
67 title: e.subject ?? s.title,
68 active: e.activeForm ?? s.active,
69 status: status ?? s.status,
70 },
71 )
72 return steps.length ? { steps } : null
73 })
74 }
75 return ran
76 }).catch(($, e, next) => next(e))
77
78 // TaskList is the source of truth: reconcile with it.
79 on('tool.call', { tool: 'TaskList' }, async ($, e, next) => {
80 const ran = await next(e)
81 if (ran.deny === undefined && !ran.isError) {
82 await update($, plan, p => {
83 const known = new Map((p?.steps ?? []).map(s => [s.id, s]))
84 const steps: Step[] = ran.result.tasks.map(t => ({
85 id: t.id,
86 title: t.subject,
87 active: known.get(t.id)?.active ?? t.subject,
88 status: t.status,
89 }))
90 return steps.length ? { steps } : null
91 })
92 }
93 return ran
94 }).catch(($, e, next) => next(e))
95
96 on('command.run', { command: 'plan-progress' }, async $ => {
97 const hidden = await update($, isHidden, h => !h)
98 return { text: hidden ? 'Plan band hidden.' : 'Plan band shown.' }
99 })
100
101 // The mod's own planning tool, for sessions without TodoWrite / Task tools.
102 on('tool.call', { tool: PLAN_TOOL }, async ($, e) => {
103 const input = e as unknown as { steps?: { title?: unknown; active?: unknown; status?: unknown }[] }
104 const steps: Step[] = (input.steps ?? []).map((s, i) => ({
105 id: String(i),
106 title: String(s.title ?? ''),
107 active: String(s.active ?? s.title ?? ''),
108 status: s.status === 'completed' || s.status === 'in_progress' ? s.status : 'pending',
109 }))
110 await update($, plan, () => (steps.length ? { steps } : null))
111 await update($, isHidden, () => false)
112 const done = steps.filter(s => s.status === 'completed').length
113 return { result: `Plan updated: ${done}/${steps.length} steps completed.` }
114 })
115
116 on('session.start', async ($, e, next) => {
117 await $.tool.register({
118 name: 'plan',
119 description:
120 'Show the user the plan for multi-step work as a progress band above the prompt. ' +
121 'Send the whole list every time: set a step in_progress when you start it and completed as soon as it is done. ' +
122 'Send an empty list to clear the band.',
123 inputSchema: {
124 type: 'object',
125 properties: {
126 steps: {
127 type: 'array',
128 items: {
129 type: 'object',
130 properties: {
131 title: { type: 'string', description: 'Imperative step name, e.g. "Run tests"' },
132 active: { type: 'string', description: 'Present continuous form shown while in progress, e.g. "Running tests"' },
133 status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
134 },
135 required: ['title', 'status'],
136 },
137 },
138 },
139 required: ['steps'],
140 },
141 })
142 await $.command.register({ name: 'plan-progress', description: 'Show or hide the plan progress band' })
143 return next(e)
144 })
145
146 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
147 const p = await read($, plan)
148 if (e.props.hasSurvey || p === null || (await read($, isHidden))) {
149 return next(e)
150 }
151
152 const { Box, Text, Button } = $.ui.resolve(e)
153 const total = p.steps.length
154 const done = p.steps.filter(s => s.status === 'completed').length
155 const currentIdx = p.steps.findIndex(s => s.status === 'in_progress')
156 const nextIdx = p.steps.findIndex(s => s.status !== 'completed')
157 const idx = currentIdx >= 0 ? currentIdx : nextIdx
158 const ratio = total ? done / total : 0
159 const pct = Math.round(ratio * 100)
160 const finished = done === total
161
162 const label = finished
163 ? 'Wszystkie etapy ukończone'
164 : idx >= 0
165 ? (currentIdx >= 0 ? p.steps[idx]!.active : `Następny: ${p.steps[idx]!.title}`)
166 : ''
167 const stage = finished ? `${total}/${total}` : `${idx + 1}/${total}`
168
169 // One cell of padding each side, so the bar lines up with the header row.
170 const width = Math.max(10, e.props.bodyColumns - 2 * PAD)
171 const [full, empty] = bar(ratio, width)
172 const tone = finished ? 'success' : 'claude'
173 const expanded = await read($, isExpanded)
174
175 return (
176 <Box flexDirection="column" paddingX={PAD} paddingTop={1}>
177 <Box>
178 <Text color="claude" bold>◆ plan </Text>
179 <Text bold>etap {stage} </Text>
180 <Text dimColor>· </Text>
181 <Text wrap="truncate-end">{label}</Text>
182 <Box flexGrow={1} />
183 <Text color={tone} bold> {pct}% </Text>
184 <Button
185 key="toggle"
186 label={expanded ? 'Zwiń' : 'Lista'}
187 onPress={() => update($, isExpanded, x => !x)}
188 />
189 <Text> </Text>
190 <Button key="hide" label="Ukryj" onPress={() => update($, isHidden, () => true)} />
191 </Box>
192 <Box>
193 <Text color={tone}>{full}</Text>
194 <Text color="subtle">{empty}</Text>
195 </Box>
196 {expanded ? (
197 <Box flexDirection="column" paddingTop={1}>
198 {p.steps.map((step, i) => (
199 <Box key={`step-${step.id}`}>
200 <Text color={step.status === 'completed' ? 'success' : step.status === 'in_progress' ? 'claude' : 'inactive'}>
201 {step.status === 'completed' ? ' ✓ ' : step.status === 'in_progress' ? ' ▶ ' : ' ○ '}
202 </Text>
203 <Text dimColor>{String(i + 1).padStart(String(total).length)}. </Text>
204 <Text
205 wrap="truncate-end"
206 bold={step.status === 'in_progress'}
207 dimColor={step.status === 'completed'}
208 color={step.status === 'pending' ? 'inactive' : undefined}
209 >
210 {step.status === 'in_progress' ? step.active : step.title}
211 </Text>
212 </Box>
213 ))}
214 </Box>
215 ) : null}
216 </Box>
217 )
218 })
219}
220types/index.d.ts 9 lines1export type Step = { id: string; title: string; active: string; status: 'pending' | 'in_progress' | 'completed' }
2export type Plan = { steps: Step[] }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'plan-progress': { plan: Plan | null; isHidden: boolean; isExpanded: boolean }
7 }
8}
9