Live progress bars above the prompt for multi-step work: one row per plan, with stages, percent, waiting and failed colors, and sounds

<h1 align="center">Awesome Claude Code Mods</h1>
<a id="showcase"></a>
<table> <tr><td width="33%" align="center" valign="top"><p><a href="https://github.com/therahul-yo/clawdman"><img src="assets/showcase-01.gif" width="100%" alt="Clawdman: An animated companion that reacts to your coding session. Demo · 10×."></a></p><p><strong><a href="https://github.com/therahul-yo/clawdman">Clawdman</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="AgentMods/pi-agent/"><img src="assets/showcase-02.gif" width="100%" alt="Pi Agent: Run pi-powered models as native Claude Code subagents. Demo · 10×."></a></p><p><strong><a href="AgentMods/pi-agent/">Pi Agent</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="FocusMods/breathing-exercises/"><img src="assets/showcase-03.gif" width="100%" alt="Breathing Exercises: A guided breathing animation while Claude works."></a></p><p><strong><a href="FocusMods/breathing-exercises/">Breathing Exercises</a></strong></p></td></tr> <tr><td width="33%" align="center" valign="top"><p><a href="sources/creative-toolkit/"><img src="assets/showcase-04.png" width="100%" alt="Creative Toolkit: A collection of live panels, session tools and playful mods. Code Pet screenshot."></a></p><p><strong><a href="sources/creative-toolkit/">Creative Toolkit</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="GameMods/agent-cartoons/"><img src="assets/showcase-05.gif" width="100%" alt="Agent Cartoons: Coding activity becomes a cartoon in a choice of visual styles."></a></p><p><strong><a href="GameMods/agent-cartoons/">Agent Cartoons</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="https://github.com/adamholter/claude-subway-surfers"><img src="assets/showcase-06.gif" width="100%" alt="Subway Surfers Desktop: Gameplay beside Claude Desktop while it works. Desktop demo · 10×."></a></p><p><strong><a href="https://github.com/adamholter/claude-subway-surfers">Subway Surfers Desktop</a></strong></p></td></tr> <tr><td width="33%" align="center" valign="top"><p><a href="GameMods/dino-game/"><img src="assets/showcase-07.gif" width="100%" alt="Dino Game: Play the T-rex runner above your prompt."></a></p><p><strong><a href="GameMods/dino-game/">Dino Game</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="GameMods/spell-bar/"><img src="assets/showcase-08.gif" width="100%" alt="Spell Bar: An animated spell bar for your selected effort level."></a></p><p><strong><a href="GameMods/spell-bar/">Spell Bar</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="AgentMods/agent-dashboard/"><img src="assets/showcase-09.gif" width="100%" alt="Agent Dashboard: A live dashboard for context, costs, permissions and agents."></a></p><p><strong><a href="AgentMods/agent-dashboard/">Agent Dashboard</a></strong></p></td></tr> </table>
<a id="browse-by-category"></a>
<h2>Browse by category <img src="assets/category-magnifier.gif" width="52" height="40" alt="Clawd with a magnifying glass"></h2>
Usage Mods · Agent Mods · Planning Mods · File Mods · Git Mods · Safety Mods · Memory Mods · Interface Mods · Prompt Mods · Testing Mods · Web Mods · Focus Mods · Game Mods · Integration Mods
Source packages available here: 82 mods from 38 repositories, with their complete tracked files and notices. Local package links open inside this collection. Other entries currently lead to upstream references while the wider source import is reviewed.
Entries are grouped by their main purpose. Collections can contain several kinds of mods; hosted mod pages are categorized individually.
<a id="usage"></a>
Track model usage, costs, effort and quotas.
<a id="agents"></a>
Coordinate agents and follow their actual work.
hooks/register.tsx 273 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Plan, PlanState } from '../types'
5import {
6 COLOR,
7 GLYPH,
8 SET_SCHEMA,
9 UPDATE_SCHEMA,
10 USAGE,
11 applySet,
12 applyUpdate,
13 labelOf,
14 listText,
15 percentOf,
16 runsOf,
17} from './bar'
18import type { CellKind, Change, Sound } from './bar'
19import { STRINGS, langOf } from './i18n'
20import type { Strings } from './i18n'
21
22const SHOWN = 4
23const LABEL_WIDTH = 16
24const PULSE_MS = 600
25
26const plans = atom({ plugin: 'plan-bar', key: 'plans' } as const, [])
27const isMuted = atom({ plugin: 'plan-bar', key: 'isMuted' } as const, false)
28const frame = atom({ plugin: 'plan-bar', key: 'frame' } as const, 0)
29
30// [幾毫秒後, 是不是 plan_set, 工具的 input]
31const demoOf = (t: Strings): readonly (readonly [number, boolean, Record<string, unknown>])[] => {
32 const { refactor, release, steps, stages } = t.demo
33
34 return [
35 [0, true, { plan: refactor, stages: [steps] }],
36 [0, true, { plan: release, stages }],
37 [1000, false, { plan: release, completed: 1 }],
38 [2000, false, { plan: refactor, completed: 1 }],
39 [3000, false, { plan: release, completed: 2 }],
40 [4000, false, { plan: release, completed: 3 }],
41 [5000, false, { plan: refactor, completed: 2 }],
42 [6000, false, { plan: release, completed: 5 }],
43 [7000, false, { plan: release, completed: 6, state: 'waiting' }],
44 [9000, false, { plan: release, completed: 7 }],
45 [10000, false, { plan: refactor, completed: 3 }],
46 [11000, false, { plan: release, completed: 7, state: 'failed' }],
47 [13000, false, { plan: release, completed: 8 }],
48 [14000, false, { plan: release, completed: 9 }],
49 [15000, false, { plan: refactor, completed: 5 }],
50 ]
51}
52
53let isWorking = false
54
55const play = async ($: EngineInterface, sound: Sound) => {
56 if (await read($, isMuted)) {
57 return
58 }
59
60 try {
61 if (sound === 'stage') {
62 await $.audio.play({ asset: 'sounds/stage.wav' }, { gain: 0.6 })
63 } else if (sound === 'done') {
64 await $.audio.play({ asset: 'sounds/done.wav' }, { gain: 0.6 })
65 } else if (sound === 'waiting') {
66 await $.audio.play({ asset: 'sounds/waiting.wav' }, { gain: 0.6 })
67 } else {
68 await $.audio.play({ asset: 'sounds/failed.wav' }, { gain: 0.6 })
69 }
70 } catch {
71 $.ui.log('plan-bar: could not play sound', { to: 'debug' })
72 }
73}
74
75// 套用一次變更:寫 state 讓 band 重畫,再放這次變更帶出的音效
76const commit = async ($: EngineInterface, change: (list: Plan[]) => Change): Promise<Change> => {
77 let outcome: Change = { plans: [], sounds: [] }
78
79 await update($, plans, list => {
80 outcome = change(list)
81
82 return outcome.plans
83 })
84
85 for (const sound of outcome.sounds) {
86 await play($, sound)
87 }
88
89 return outcome
90}
91
92const drop = ($: EngineInterface, names: readonly string[]) =>
93 update($, plans, list => list.filter(plan => !names.includes(plan.name)))
94
95// 進行中那一段隨時間明暗交替,只在 Claude 正在跑而且有 plan 進行中時才動
96const pulse = async ($: EngineInterface) => {
97 try {
98 if (isWorking && (await read($, plans)).some(plan => plan.state === 'running')) {
99 await update($, frame, beat => (beat + 1) % 2)
100 }
101 } catch {
102 $.ui.log('plan-bar: pulse failed', { to: 'debug' })
103 }
104}
105
106const startDemo = ($: EngineInterface, t: Strings) => {
107 for (const [at, isSet, input] of demoOf(t)) {
108 $.clock.after(at, () => {
109 void commit($, list => (isSet ? applySet(list, input) : applyUpdate(list, input))).catch(() => undefined)
110 })
111 }
112 $.clock.after(19000, () => {
113 void drop($, [t.demo.refactor, t.demo.release]).catch(() => undefined)
114 })
115}
116
117const styleOf = (kind: CellKind, state: PlanState, beat: number) => {
118 if (kind === 'done') {
119 return { color: COLOR[state] }
120 }
121 if (kind === 'current') {
122 return { color: COLOR[state], dimColor: state === 'running' && beat === 1 }
123 }
124
125 return kind === 'stage' ? { bold: true } : { dimColor: true }
126}
127
128export const register: Register = (on, options) => {
129 const t = STRINGS[langOf(options)]
130
131 on('session.start', async ($, e, next) => {
132 await $.tool.register({
133 name: 'plan_set',
134 description:
135 "Show a live progress bar above the user's prompt for multi-step work. Call once when starting work with three or more steps: name the plan and list its stages, each with its tasks in order. Calling again with the same plan name replaces it. Then call plan_update as each task finishes.",
136 inputSchema: SET_SCHEMA,
137 })
138 await $.tool.register({
139 name: 'plan_update',
140 description:
141 "Move a plan's progress bar. `completed` is the total number of tasks finished so far, not an increment. Set state to `waiting` when blocked on the user, `failed` when a task failed, `running` to resume. The bar turns done when completed reaches the task count.",
142 inputSchema: UPDATE_SCHEMA,
143 })
144 await $.command.register({
145 name: 'plans',
146 description: t.commandDescription,
147 argumentHint: '[demo|clear|sound off|on]',
148 })
149
150 if ((await $.store.get('isMuted')) === true) {
151 await update($, isMuted, () => true)
152 }
153 $.clock.every(PULSE_MS, () => {
154 void pulse($)
155 })
156
157 return next(e)
158 })
159
160 on('prompt.compose', async ($, e, next) => {
161 const composed = await next(e)
162
163 return {
164 ...composed,
165 sections: [...composed.sections, { id: 'plan-bar:usage', text: USAGE, scope: 'session' }],
166 }
167 })
168
169 on('tool.call', { tool: 'mcp__plan-bar__plan_set' }, async ($, e) => {
170 const outcome = await commit($, list => applySet(list, e))
171
172 return outcome.error === undefined ? { result: `Plan "${String(e.plan)}" is on screen.` } : { deny: outcome.error }
173 })
174
175 on('tool.call', { tool: 'mcp__plan-bar__plan_update' }, async ($, e) => {
176 const outcome = await commit($, list => applyUpdate(list, e))
177 const plan = outcome.plans.find(one => one.name === e.plan)
178
179 return outcome.error !== undefined || plan === undefined
180 ? { deny: outcome.error ?? 'plan_update failed.' }
181 : { result: `${plan.name}: ${percentOf(plan)}%, ${labelOf(plan, STRINGS.en)}` }
182 })
183
184 on('turn.start', ($, e, next) => {
185 isWorking = true
186
187 return next(e)
188 })
189
190 on('turn.complete', async ($, e, next) => {
191 const ended = await next(e)
192
193 if (e.agentId === undefined) {
194 isWorking = false
195 }
196
197 return ended
198 })
199
200 // 做完的 plan 留到使用者下一次送出 prompt 才收掉
201 on('prompt.submit', async ($, e, next) => {
202 if (e.origin.kind === 'composer' && (await read($, plans)).some(plan => plan.state === 'done')) {
203 await update($, plans, list => list.filter(plan => plan.state !== 'done'))
204 }
205
206 return next(e)
207 })
208
209 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
210 const list = await read($, plans)
211
212 if (e.props.hasSurvey || list.length === 0) {
213 return next(e)
214 }
215
216 const { Box, Button, Text } = $.ui.resolve(e)
217 const beat = await read($, frame)
218 const nameWidth = e.props.bodyColumns < 80 ? 14 : 22
219 const barWidth = Math.max(10, Math.min(60, e.props.bodyColumns - nameWidth - LABEL_WIDTH - 16))
220
221 return (
222 <Box flexDirection="column">
223 {await next(e)}
224 {list.slice(-SHOWN).map((plan, index) => (
225 <Box columnGap={1}>
226 <Text color={COLOR[plan.state]}>{GLYPH[plan.state]}</Text>
227 <Box width={nameWidth}>
228 <Text wrap="truncate-end">{plan.name}</Text>
229 </Box>
230 <Box>
231 {runsOf(plan, barWidth).map(run => (
232 <Text {...styleOf(run.kind, plan.state, beat)}>{run.text}</Text>
233 ))}
234 </Box>
235 <Box width={LABEL_WIDTH}>
236 <Text color={COLOR[plan.state]} wrap="truncate-end">
237 {labelOf(plan, t)}
238 </Text>
239 </Box>
240 <Box width={4} justifyContent="flex-end">
241 <Text dimColor>{percentOf(plan)}%</Text>
242 </Box>
243 <Button key={`close-${index + 1}`} label="×" plain dimColor onPress={() => drop($, [plan.name])} />
244 </Box>
245 ))}
246 </Box>
247 )
248 })
249
250 on('command.run', { command: 'plans' }, async ($, e) => {
251 const [verb, arg] = e.args.trim().split(/\s+/)
252
253 if (verb === 'clear') {
254 await update($, plans, () => [])
255
256 return { text: t.cleared }
257 }
258 if (verb === 'sound' && (arg === 'on' || arg === 'off')) {
259 await update($, isMuted, () => arg === 'off')
260 await $.store.set('isMuted', arg === 'off')
261
262 return { text: arg === 'off' ? t.soundOff : t.soundOn }
263 }
264 if (verb === 'demo') {
265 startDemo($, t)
266
267 return { text: t.demoStarted }
268 }
269
270 return { text: listText(await read($, plans), t) }
271 })
272}
273hooks/bar.ts 181 lines1import type { Plan, PlanState, Stage } from '../types'
2import type { Strings } from './i18n'
3
4export type Sound = 'stage' | 'done' | 'waiting' | 'failed'
5export type CellKind = 'done' | 'current' | 'pending' | 'tick' | 'stage'
6export type Run = { kind: CellKind; text: string }
7export type Change = { plans: Plan[]; sounds: Sound[]; error?: string }
8
9const MAX_PLANS = 6
10
11export const GLYPH: Record<PlanState, string> = { running: '●', waiting: '?', failed: '!', done: '✓' }
12export const COLOR: Record<PlanState, string> = {
13 running: 'magenta',
14 waiting: 'yellow',
15 failed: 'red',
16 done: 'green',
17}
18
19const CELL: Record<CellKind, string> = { done: '█', current: '▒', pending: '░', tick: '│', stage: '┃' }
20
21export const USAGE = [
22 'plan-bar: the user sees a live progress bar above the prompt for multi-step work.',
23 'When you start work with three or more distinct steps, call mcp__plan-bar__plan_set once with the plan name and its stages, each listing its tasks in order.',
24 'After each task finishes, call mcp__plan-bar__plan_update with the total number of tasks completed so far.',
25 'Set state to "waiting" when you are blocked on the user, "failed" when a task failed, "running" to resume.',
26 'Skip it for short or single-step work. If the tools are deferred, load them with ToolSearch: select:mcp__plan-bar__plan_set,mcp__plan-bar__plan_update',
27].join(' ')
28
29export const SET_SCHEMA = {
30 type: 'object',
31 properties: {
32 plan: { type: 'string', description: 'Short name shown to the user, e.g. "Release pipeline"' },
33 stages: {
34 type: 'array',
35 minItems: 1,
36 description: 'Stages in order; a plan with no real stages uses one stage named "Tasks"',
37 items: {
38 type: 'object',
39 properties: {
40 name: { type: 'string' },
41 tasks: { type: 'array', minItems: 1, items: { type: 'string' } },
42 },
43 required: ['name', 'tasks'],
44 },
45 },
46 },
47 required: ['plan', 'stages'],
48}
49
50export const UPDATE_SCHEMA = {
51 type: 'object',
52 properties: {
53 plan: { type: 'string', description: 'The name given to plan_set' },
54 completed: { type: 'integer', minimum: 0, description: 'Total tasks finished so far, not an increment' },
55 state: { type: 'string', enum: ['running', 'waiting', 'failed'] },
56 },
57 required: ['plan', 'completed'],
58}
59
60export const totalOf = (plan: Plan): number => plan.stages.reduce((sum, stage) => sum + stage.tasks.length, 0)
61
62export const percentOf = (plan: Plan): number => Math.round((plan.completed / Math.max(1, totalOf(plan))) * 100)
63
64// 目前做到哪個階段的第幾個 task;全部做完時停在最後一個
65export const positionOf = (plan: Plan): { stage: Stage; inStage: number; task: string } => {
66 const at = Math.min(plan.completed, totalOf(plan) - 1)
67 let before = 0
68
69 for (const stage of plan.stages) {
70 if (at < before + stage.tasks.length) {
71 return { stage, inStage: at - before + 1, task: stage.tasks[at - before] ?? '' }
72 }
73 before += stage.tasks.length
74 }
75
76 return { stage: { name: '', tasks: [] }, inStage: 0, task: '' }
77}
78
79export const labelOf = (plan: Plan, t: Strings): string => {
80 if (plan.state === 'done') {
81 return t.done
82 }
83
84 const { stage, inStage } = positionOf(plan)
85 const mark = plan.state === 'waiting' ? '? ' : plan.state === 'failed' ? '✗ ' : ''
86
87 return `${mark}${stage.name} ${inStage}/${stage.tasks.length}`
88}
89
90// 把進度條畫成 width 格:做完、進行中、還沒做三種底,task 交界畫細線,階段交界畫粗線
91export const runsOf = (plan: Plan, width: number): Run[] => {
92 const total = Math.max(1, totalOf(plan))
93 const stageStarts = new Set<number>()
94 let before = 0
95
96 for (const stage of plan.stages) {
97 stageStarts.add(before)
98 before += stage.tasks.length
99 }
100
101 const taskAt = (cell: number) => Math.min(total - 1, Math.floor((cell * total) / width))
102 const hasRoomForTicks = width >= total * 2
103 const runs: Run[] = []
104
105 for (let cell = 0; cell < width; cell += 1) {
106 const task = taskAt(cell)
107 const isBoundary = cell > 0 && taskAt(cell - 1) !== task
108 const fill: CellKind = task < plan.completed ? 'done' : task === plan.completed ? 'current' : 'pending'
109 const kind: CellKind =
110 isBoundary && stageStarts.has(task) ? 'stage' : isBoundary && hasRoomForTicks ? 'tick' : fill
111 const last = runs.at(-1)
112
113 if (last !== undefined && last.kind === kind) {
114 last.text += CELL[kind]
115 } else {
116 runs.push({ kind, text: CELL[kind] })
117 }
118 }
119
120 return runs
121}
122
123const stageIndexOf = (plan: Plan): number => plan.stages.indexOf(positionOf(plan).stage)
124
125// 模型給的 input 沒有型別保證,壞的直接回錯誤訊息讓它重叫
126export const applySet = (plans: readonly Plan[], input: Record<string, unknown>): Change => {
127 const name = typeof input.plan === 'string' ? input.plan.trim() : ''
128 const stages = (Array.isArray(input.stages) ? input.stages : []).flatMap((raw: unknown): Stage[] => {
129 const { name: stageName, tasks } = (raw ?? {}) as { name?: unknown; tasks?: unknown }
130 const list = (Array.isArray(tasks) ? tasks : []).filter((task): task is string => typeof task === 'string')
131
132 return typeof stageName === 'string' && list.length > 0 ? [{ name: stageName, tasks: list }] : []
133 })
134
135 if (name === '' || stages.length === 0) {
136 return { plans: [...plans], sounds: [], error: 'plan_set needs a plan name and at least one stage with tasks.' }
137 }
138
139 const plan: Plan = { name, stages, completed: 0, state: 'running' }
140 const others = plans.filter(one => one.name !== name)
141
142 return { plans: [...others, plan].slice(-MAX_PLANS), sounds: [] }
143}
144
145export const applyUpdate = (plans: readonly Plan[], input: Record<string, unknown>): Change => {
146 const current = plans.find(plan => plan.name === input.plan)
147
148 if (current === undefined || typeof input.completed !== 'number') {
149 return { plans: [...plans], sounds: [], error: `No plan named "${String(input.plan)}". Call plan_set first.` }
150 }
151
152 const total = totalOf(current)
153 const completed = Math.max(0, Math.min(total, Math.floor(input.completed)))
154 const asked = input.state === 'waiting' || input.state === 'failed' ? input.state : 'running'
155 const state: PlanState = completed >= total ? 'done' : asked
156 const next: Plan = { ...current, completed, state }
157 const sounds: Sound[] = []
158
159 if (state !== current.state && state !== 'running') {
160 sounds.push(state)
161 } else if (completed > current.completed && stageIndexOf(next) > stageIndexOf(current)) {
162 sounds.push('stage')
163 }
164
165 return { plans: plans.map(plan => (plan === current ? next : plan)), sounds }
166}
167
168export const listText = (plans: readonly Plan[], t: Strings): string => {
169 if (plans.length === 0) {
170 return t.noPlans
171 }
172
173 const lines = plans.map(plan => {
174 const task = plan.state === 'done' ? '' : t.current(positionOf(plan).task)
175
176 return `${GLYPH[plan.state]} ${plan.name} ${labelOf(plan, t)} ${percentOf(plan)}%${task}`
177 })
178
179 return [...lines, '', t.listHint].join('\n')
180}
181hooks/i18n.ts 79 lines1import type { PluginOptions } from 'claude-code'
2
3import type { Stage } from '../types'
4
5export type Lang = 'en' | 'zh-TW' | 'zh-CN'
6
7export const langOf = (options: PluginOptions): Lang =>
8 options.language === 'zh-TW' || options.language === 'zh-CN' ? options.language : 'en'
9
10const RELEASE_STAGES: Stage[] = [
11 { name: 'Build', tasks: ['install', 'compile', 'bundle'] },
12 { name: 'Test', tasks: ['unit', 'e2e'] },
13 { name: 'Deploy', tasks: ['STG', 'PROD'] },
14 { name: 'Verify', tasks: ['smoke', 'metrics'] },
15]
16
17const en = {
18 done: 'Done',
19 noPlans: 'no plans right now. Try /plans demo',
20 current: (task: string) => `, now: ${task}`,
21 listHint: '/plans clear removes all, /plans sound off|on toggles sound, /plans demo shows a demo',
22 commandDescription: 'List the plans on the progress bar, or run a demo',
23 cleared: 'cleared.',
24 soundOff: 'sound off.',
25 soundOn: 'sound on.',
26 demoStarted: 'demo running for about 20 seconds, watch above the prompt.',
27 demo: {
28 refactor: 'Refactor auth module',
29 release: 'Release v2.4',
30 steps: { name: 'Tasks', tasks: ['read', 'split', 'rewrite', 'test', 'clean up'] },
31 stages: [
32 { name: 'Build', tasks: ['install', 'compile', 'bundle'] },
33 { name: 'Test', tasks: ['unit', 'e2e'] },
34 { name: 'Deploy', tasks: ['staging', 'production'] },
35 { name: 'Verify', tasks: ['smoke', 'metrics'] },
36 ],
37 },
38}
39
40export type Strings = typeof en
41
42export const STRINGS: Record<Lang, Strings> = {
43 en,
44 'zh-TW': {
45 done: '完成',
46 noPlans: '目前沒有 plan。/plans demo 看示範',
47 current: task => `,進行中:${task}`,
48 listHint: '/plans clear 清掉全部,/plans sound off|on 開關音效,/plans demo 看示範',
49 commandDescription: '列出進度條上的 plan,或看示範',
50 cleared: '清掉了。',
51 soundOff: '音效關了。',
52 soundOn: '音效開了。',
53 demoStarted: '示範開始,大約 20 秒,看 prompt 上方。',
54 demo: {
55 refactor: '示範:重構',
56 release: '示範:Release',
57 steps: { name: 'Tasks', tasks: ['讀', '拆', '改', '測', '收'] },
58 stages: RELEASE_STAGES,
59 },
60 },
61 'zh-CN': {
62 done: '完成',
63 noPlans: '当前没有 plan。可以用 /plans demo 看演示',
64 current: task => `,进行中:${task}`,
65 listHint: '/plans clear 清空全部,/plans sound off|on 开关音效,/plans demo 看演示',
66 commandDescription: '列出进度条上的 plan,或看演示',
67 cleared: '已清空。',
68 soundOff: '音效已关闭。',
69 soundOn: '音效已开启。',
70 demoStarted: '演示开始,大约 20 秒,留意输入框上方。',
71 demo: {
72 refactor: '演示:重构',
73 release: '演示:发版',
74 steps: { name: '任务', tasks: ['读', '拆', '改', '测', '收'] },
75 stages: RELEASE_STAGES,
76 },
77 },
78}
79types/index.d.ts 17 lines1export type PlanState = 'running' | 'waiting' | 'failed' | 'done'
2
3export type Stage = { name: string; tasks: string[] }
4
5export type Plan = {
6 name: string
7 stages: Stage[]
8 completed: number
9 state: PlanState
10}
11
12declare module 'claude-code' {
13 interface PluginState {
14 'plan-bar': { plans: Plan[]; isMuted: boolean; frame: number }
15 }
16}
17