SLOPSHOPPER

plan-bar

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

newbandguardcommandprompttool
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · plan-bar
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /plans ⎿ plan-bar: no plans right now. Try /plans demo ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

<h1 align="center">Awesome Claude Code Mods</h1>

<a id="showcase"></a>

Mod showcase

<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>

Usage Mods

Browse Usage Mods

Track model usage, costs, effort and quotas.

  • Auto Effort · arthur-fontaine - Let Jev select Claude's reasoning effort for each prompt, adapting the configured effort level to the incoming request rather than choosing it manually every time.
  • Automatic Model and Effort Router · sofanaja44 - Choose a Claude model and reasoning effort according to task difficulty, with rules that limit unnecessary prompt-cache rewrites. Cost and cache statistics help you follow the routing decisions.
  • Braille Usage · kawase1295 - Check your current model, context usage and five-hour or weekly limits in compact text-based meters above the prompt.
  • Budget and Context Toolkit · Arunjay4213 - Track session spending, subscription quotas, cache use and per-turn token costs. Set budget limits that warn before exhaustion and stop tool calls when a configured limit is exceeded.
  • Budget Governor · ccdwyer - Set session and daily spending caps, monitor them above the prompt, and receive a wrap-up reminder at eighty percent. New prompts are refused once the cap is reached.
  • Cache, Context and Cost Panel · anthonyhungnguyen - Track token spending for the session and recent days, get context-limit warnings, and see what rebuilding the prompt cache cost. A completion alert announces long turns.
  • Clawd Usage Meters · Toshkee - Watch live context, rate-limit and cost meters above the prompt, accompanied by Clawd's wand animations and banner tricks as you work in Claude Code.
  • Cockpit and Bodyguard · Para-FR - Inspect session turns, tools, tokens, durations and changed files in a live cockpit. A companion guard blocks access to .env files and destructive Bash commands, displaying blocked-action alerts.
  • Context and Quota Forecast · imsalik - See a context forecast alongside five-hour and weekly rate-limit usage above the prompt. Reset countdowns help you understand when each plan allowance becomes available again.
  • Context Saver · AlmogBaku - Spot repeated tool calls and wasteful workflows during a Claude Code session. Review their measured time and context cost, then send a suggested correction with one click.
  • Context Weather and Usage · travisoa - Read context fullness as changing weather symbols alongside token totals and quota consumption. Recent-turn bars show how the conversation grew, helping you spot increasing pressure on the context window.
  • Cost and Output Token Band · Luma-Sa - Keep five-hour and seven-day usage limits, session cost and output-token totals visible in a band above the Claude Code prompt while you work.
  • Cost Meter · zaferayan - Track API-equivalent session cost above the prompt, including subagent usage, with a configurable budget warning. On subscription plans, the displayed amount estimates API pricing rather than your bill.
  • Desktop Next Steps and Limits · shichang4fun - See quota limits and reset countdowns above the prompt in Claude Code Desktop. After each turn, suggested next prompts can be sent with a click or copied for editing.
  • Desktop Session Status Bar · sgmonda - Keep five-hour and weekly usage limits, reset times, session clocks and turn duration visible in Claude Code Desktop. The bar also lists active subagents, background jobs and scheduled wakeups.
  • Desktop SVG Usage Meters · s-hiraoku - See context use and the five-hour and seven-day quota windows as SVG meters above the prompt in Claude Code Desktop. The compact display keeps capacity visible while you work.
  • Effort Guard · stefanochieli - Track context and token usage, review an effort log for each turn and receive an escalation alert after repeated command or test failures. It observes activity without blocking it.
  • Effort Router · totally-tim - Choose reasoning effort for each turn with Jev or a compatible classifier, and display the selected level. Shadow mode records the recommendation; enforce mode applies it to the session.
  • Effort Shortcut · daanqq - Cycle Claude's reasoning effort through low, medium, high, and extra-high with one command. Bind that command to a key to change effort quickly while skipping the maximum setting.
  • Focus Band · chaoshengsc - Keep your five-hour and weekly plan usage visible above the prompt. Progress bars, percentages, and reset times help you see how much of each allowance you have used.
  • Jet Router · jetsongdev - Experiment with per-turn reasoning effort recommendations while keeping your chosen model. Use fixed test decisions or opt into Jev classification, with observation and optional application modes.
  • Jev Claude Router · Flam1ngFir3ball - Let Jev choose the model and reasoning effort for each turn while considering the cost of switching. It also provides Jev-guided context compaction.
  • Jev Model and Effort Selection · unclecode - Observe or pin model and effort choices, or opt into automatic selection with Jev. The experimental router lets you compare recommendations before allowing them to steer the session.
  • Jev Model Router · satviksinha - Choose a model and reasoning effort for each turn using TypeSafe's Jev, through its direct API or Vercel AI Gateway. A visible routing decision shows which model will handle the work.
  • Jev Route · drewpayment - Choose a Claude model for each turn using Jev's estimate of what the task needs. Toggle routing with /route to balance model capability against cost.
  • Limitpace · fstandhartinger - Track subscription limits and spending pace through live usage bars. An optional delegation adviser can suggest using another provider as part of managing your available capacity.
  • Model and Effort Router · moritalous - Choose reasoning effort for each prompt while keeping the model fixed, or enable a separate router that selects Haiku, Sonnet, or Opus using Jev. Both tools adjust resources per turn.
  • Model Shortcuts · richkuo - Use keyboard shortcuts to switch models and adjust reasoning effort, with the active choices displayed in the prompt footer.
  • Netrunner HUD · ccdwyer - Watch context use, token activity, tool calls, costs, and job status in an animated cyberpunk dashboard. Git and test widgets keep related session information alongside the live usage gauges.
  • Next Steps and Usage Band · pawandeepdhall - Keep five-hour and weekly usage limits visible with reset countdowns, and choose from suggested next steps after each reply. Buttons also provide shortcuts for starting a new chat or pushing work.
  • Oxide Jev Model Router · Kiy-K - Use Jev to choose subagent models and adjust the main conversation's model and reasoning effort as work progresses. A built-in classifier provides a fallback without an API key.
  • Plan and Context Usage Line · KhadeerBasha1232 - Track five-hour and weekly plan limits, reset times, context usage, the active model and session cost in one line above the Claude Code prompt.
  • Plan Usage and Reset Times · jumoog - Keep five-hour and weekly plan usage visible above the prompt, together with each reset time. Check the current allowance windows without opening a separate usage screen.
  • Power Bottom Status Line · brianSchanbacher - See the model, folder, branch, context fill, cost, elapsed time, cache hits and live agents in a cross-platform status line at the bottom of Claude Code.
  • Project Spend and Cache Status · Sma1lboy - Track project spending alongside context usage, cache hits and the cache countdown. Inspect why a turn missed the cache, and open the project ledger with /usage.
  • Prompt Enhancement and Telemetry · MiguelMachado-dev - Inspect per-turn duration, first-token latency, token throughput, requests and stalls in a live band. An additional command rewrites a draft prompt with Sonnet and returns it for your review.
  • Prompt Footer Usage · WoBok - Keep context usage and rate-limit consumption visible in Claude Code's prompt footer, so you can check those readings without opening a separate usage view.
  • Prompt Usage Timeline · augiefra - Compare five-hour and weekly usage with elapsed time, inspect token bars for recent prompts and watch context status above the prompt. Labels are available in English or French.
  • Quota and Cost Pills · Sh0ckWaveZero - Track five-hour and seven-day rate limits, session tokens and cost in compact indicators above the prompt. The band can sit alongside other status bands.
  • Quota Reserve · chrisns - Reserve part of your five-hour and weekly quota for later work. The mod pauses at configured thresholds, asks whether to continue, and releases the reserve shortly before the reset.
  • Rate-Limit History Band · mohammed-alsalhi - Follow rate-limit consumption since each window last reset, including five-hour, weekly, and per-model weekly allowances. A band above the prompt graphs how usage accumulates over time.
  • Retro Usage Band · iamkhalid2 - Keep your plan's five-hour and weekly usage limits in view through a slim retro-style band above the prompt. Check both allowances as you work.
  • Sessclone · NotTahaAli - Send Claude Code usage to a SessClone deployment so laptop, cloud and CI sessions can contribute to a shared team ledger of usage and cost.
  • Session and Weekly Usage Band · Gooner44 - See session, weekly and Fable 5.1 usage together in a band above the Claude Code prompt, keeping these separate usage readings visible while you work.
  • Session Cockpit · nvr0x5 - Track context, spending, usage limits, reset countdowns and burn rates above the prompt. Plan progress and active subagents share the same cockpit, with display options for terminal and Desktop sessions.
  • Session Cost and Remaining Usage · HydrowZer - See the current session cost and remaining usage in a band above the Claude Code prompt, keeping spending and available capacity visible beside your conversation.
  • Session Dashboard and Alerts · lucenity0 - Follow turn durations, token counts, files touched, and tool activity through a timer and dashboard. Companion mods notify you when long turns finish and block destructive commands or edits to secrets.
  • Session Internals Dashboard · tomstagl - Inspect context, tokens, cost, limits, tools and agents in a live side-pane dashboard. An accompanying insights interface lets the session answer questions using the displayed measurements.
  • Session Sidebar · richard-parayno - Toggle a sidebar showing usage limits, context size, prompt-cache expiry, current activity and touched files. It keeps the session's resource picture visible beside the conversation.
  • Stack and Wait Time · ShahriarBijoy - Follow tasks, subagents, rate limits and monthly cost in a stacked progress band. A companion vocabulary game uses German words related to the session's topic while Claude works.
  • Terminal Usage Monitor · ilkayGkbdk - Monitor context fill, rate limits, token speed, cost, and active subagents in a live terminal dashboard. The display brings the session's resource use together in one place.
  • Turn Receipts and Error Cues · roaringsoul404 - Receive a receipt after each turn listing touched files, commands, tokens and cost. A companion animation turns the spinner into a pixel-art failure cue when a tool call fails.
  • Usage and Active Agent Dashboard · quango2304 - Follow usage bars, limits, session cost, cache expiry and active time alongside the agents currently running. The combined display makes resource consumption visible while work is underway.
  • Usage and Cache Warmth Band · Vosssa - See five-hour and weekly usage, context fill and prompt-cache warmth together above the Claude Code prompt, keeping the main resource readings visible while you work.
  • Usage and Spend Chips · kreddevils18 - Track five-hour and weekly allowances, session cost, and spending history through colored chips above the prompt. Open /usage-mod for a fuller view of the usage details.
  • Usage Line · Steven-ODell - See five-hour, weekly and context usage in the prompt footer, including whether consumption is ahead of or behind an even pace through the usage window.
  • Usage Log · tanuu5 - Record five-hour and weekly usage after each turn, then compare consumption with your chosen pace in a graph. The separate Clawd Dance mod can also show that pacing above the prompt.
  • Usage Report · Schweem - Monitor context usage, five-hour and weekly quotas, and session cost in the status line, with warnings as you approach your limits.
  • Usage Wrapup · DaKev - Ask running agents to wrap up when your remaining plan allowance gets low, helping them finish their current work before the usage limit interrupts it.
  • Weekly Usage and Spend Board · aycandv - Compare spending by model with a weekly-limit pace gauge, using a sliding ticker in the terminal or a strip and details pane in the desktop app.

<a id="agents"></a>

Agent Mods

Browse Agent Mods

Coordinate agents and follow their actual work.

  • 5dive Telemetry Toolkit · 5dive-ai - Connect Claude Code sessions to 5dive's task and gate controls, with a panel showing assignments and token budgets. Optional telemetry, compaction and tool-call policies support coordinated agent work.
  • Agent Control and Document Desk · masahide - Send messages and interruption requests to Claude through the agentctl command line and collect acknowledgments. A companion document desk gathers questionnaire answers and detailed document-review comments in a browser, then delivers them to Claude.
  • Agent Dashboard · scasella - Monitor the main agent and its subagents in a live dashboard that brings together model usage, permission requests, task activity and session logs.
  • Agent Flow · Charlie0113-T - Open /flow to follow a live tree of the session's subagents and teammates beside the transcript. A text view is available where panes cannot be displayed.
  • Agent Models and Peek · alex2481kobe - Show each subagent's model and reasoning effort beside its task in the native agent list. A companion viewer displays images inline, using real pixels or a terminal-compatible block
Source 4 files
hooks/register.tsx 273 lines
1import { 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}
273
hooks/bar.ts 181 lines
1import 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}
181
hooks/i18n.ts 79 lines
1import 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}
79
types/index.d.ts 17 lines
1export 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