SLOPSHOPPER

task-progress

Always-visible progress band above the prompt: follows the todo list automatically, lets Claude report progress with a tool, and shows a live 'working' timer…

newbandguardcommandprompttool
v0.1.0MITupdated 2026-10-12wablevaibhav/claude-mods/task-progress
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · task-progress
› 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 › /progress ⎿ task-progress: Progress band hidden. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mods

Small, independent mods for Claude Code: a live progress bar, a usage dashboard, a context-window meter, a safety guard, a git status band, and your own notes and todos.

Each mod is a plugin of function hooks. Install only the ones you want; none depends on another.

ModCommandWhat it gives you
task-progress/progressA progress band above the prompt that follows the todo list, with a working timer and your usage.
usage-dashboard/usage-dashboardA pane with 5h and weekly limits, models, tasks, time left and a 7-day chart.
context-meter/ctxmeterContext-window percentage in the status line, with warnings at 70% and 90%.
safety-guard/safeguardBlocks destructive commands and writes to secret or key files before they run.
git-band/gitbandBranch, ahead/behind and change counts on one line above the prompt.
notes-todos/mytodo, /mynoteYour own todo list and notes that persist across sessions.

Install

Run these in a terminal Claude Code session. The desktop app's Code tab does not run /plugin install.

/plugin install task-progress --marketplace wablevaibhav/claude-mods

Answer y to add the marketplace the first time, then pick a scope. Repeat with any other mod name from the table:

/plugin install usage-dashboard --marketplace wablevaibhav/claude-mods
/plugin install context-meter --marketplace wablevaibhav/claude-mods
/plugin install safety-guard --marketplace wablevaibhav/claude-mods
/plugin install git-band --marketplace wablevaibhav/claude-mods
/plugin install notes-todos --marketplace wablevaibhav/claude-mods

Try without installing

Clone the repo and point Claude Code at the mod folders:

git clone https://github.com/wablevaibhav/claude-mods.git
cd claude-mods
claude --plugin-dir ./task-progress --plugin-dir ./usage-dashboard

Add one --plugin-dir per mod.

What it looks like

task-progress is a real screenshot. The other previews are illustrative text with sample numbers; real colours (green / amber / red, orange accents) follow your terminal theme.

task-progress: the band above the prompt

task-progress band: title, smooth bar, step list, 5h / weekly usage and active model

usage-dashboard: the /usage-dashboard pane

Claude usage                                     updated 12s ago
╭──────────────────────────────────────────────────────────────────╮
│ 5-hour session                                  resets in 2h 10m │
│ ██████████████▌░░░░░░░░░░░░░░░░░░░░░░░░░  35%                    │
│ burning 0.40%/min · full in ~2h 5m                               │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Weekly                                           resets in 4d 3h │
│ █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  12%                    │
╰──────────────────────────────────────────────────────────────────╯
● Limit risk green   session cost ~$1.42
╭──────────────────────────────────────────────────────────────────╮
│ Models                                 active: opus-5-5 (3m ago) │
│ ★ opus-5-5    ████████░░   78%    1.2M   34 msgs  ~$0.91         │
│   sonnet-5-5  ██░░░░░░░░   22%    340k   12 msgs  ~$0.18         │
│ tokens: in / out / cache = 80k / 190k / 1.3M                     │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Tasks  ▸ 1 in progress   ○ 3 pending   ✓ 2 today                 │
│ ▸ Wire the charts  1m 20s                                        │
│ ○ Add the tasks board                                            │
│ ○ Polish and test                                                │
│ Time left 25m (best case 14m) · confidence medium, 4 finished    │
│ Should finish within the current 5h window.                      │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Last 7 days                                                      │
│ Wed 07 ██████████████████░░░░░░░░░░░░░░░░░░░░░░  420k            │
│ Thu 08 ███████████████████████████████░░░░░░░░░  740k            │
│ Fri 09 ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  240k            │
│ Sat 10 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  0               │
│ Sun 11 ██████████████████████░░░░░░░░░░░░░░░░░░  530k            │
│ Mon 12 ████████████████████████████████████████  1.0M            │
╰──────────────────────────────────────────────────────────────────╯

context-meter and git-band each add one line above the prompt:

▲ Context 78% ███████████████████░░░░░ 156k / 200k  Getting full: consider /compact soon.   [Hide]
⎇ feature/login ↑2 ↓1 ● 3 staged ✎ 2 changed ? 2 untracked

The mods

task-progress

A progress band above the prompt, visible while you type.

  • Follows your todo list by itself. No setup: when Claude writes a todo list, the bar, step count and current step follow it.
  • A progress tool. Claude can report title, total, done, note, items and finished explicitly (the tool is mcp__task-progress__progress), for work that doesn't use a todo list.
  • A working timer (⠹ Working 12s) whenever a turn is running and no job is shown.
  • Footer: your 5h and weekly usage with time to reset, plus the active model.
  • Controls: Collapse / Expand hides the step list, Hide or /progress hides the band. A finished job clears when you send your next prompt.

usage-dashboard

Open it with /usage-dashboard. (/usage is Claude Code's own built-in panel; this mod uses a different name so the two don't collide.)

  • Limits: 5-hour and weekly gauges coloured green below 60%, amber from 60–85%, red above 85%, with reset times.
  • Burn rate and limit risk: percent per minute over the last 30 minutes, when the 5h window would fill, and whether your open tasks are likely to finish first.
  • Models: tokens by model (input, output, cache), share, messages and a rough cost, with the top consumer starred and the active model shown.
  • Tasks: in progress, pending and done today, with a time-left estimate from the median time of tasks that finished.
  • Last 7 days: daily tokens as bars.
  • Status line: 5h 34% · wk 12% · opus-5-5 after your first reply.

context-meter

  • Status line: ctx 43%, the share of the context window in use.
  • Warning band from 70%, with the token count and advice. It stays out of the way below that.
  • Toasts once each time usage climbs into 70% and into 90%, so you compact or start fresh before quality drops.
  • /ctxmeter hides or shows the band.

safety-guard

Stops a call before it runs and tells Claude why, so it asks you first.

BlocksExamples
Recursive deletes of root, home or wildcardsrm -rf /, rm -r ~, rm -rf *, rm -rf /etc
Force pushes without a leasegit push --force, git push -f (--force-with-lease is allowed)
Piping a download into a shell`curl … \sh, wget … \sudo bash`
Disk and filesystem damagedd … of=/dev/sda, mkfs.*, > /dev/sda, fork bombs, chmod -R 777
Writing secrets.env, .env.production (not .env.example), *.pem, *.key, id_rsa, service-account JSON, .npmrc, .aws/credentials, .git/ internals
Reading private keysid_rsa, *.pem, *.key, .ssh/, .aws/credentials

/safeguard shows status and how many calls it blocked. /safeguard pause and /safeguard resume turn it off and on for the session. If the guard itself errors, it blocks the call rather than letting it through.

git-band

  • One line: branch (or detached HEAD), ↑ahead ↓behind against the upstream, staged, changed and untracked counts, conflicts, or ✓ clean.
  • Refreshes after every turn and every 15 seconds. Hidden outside a git repository.
  • /gitband hides or shows it.

notes-todos

Your own lists, separate from Claude's todo list, stored locally and kept across sessions. The open-todo count shows in the status line.

/mytodo add Renew the domain      add a todo (a bare /mytodo <text> also adds)
/mytodo                           list todos
/mytodo done 2     /mytodo undo 2     /mytodo rm 2     /mytodo clear

/mynote Wifi password is on the fridge     save a note
/mynote                                    list notes
/mynote find wifi     /mynote rm 3

Notes and limits

  • Usage figures come from the rate-limit windows Anthropic reports on each reply (five_hour, seven_day). Accounts without those windows (some API-key setups) show "No reading yet".
  • Model totals, daily tokens and finished-task durations count only turns the mod has seen since you installed it, and are kept per machine.
  • Cost is a rough per-model price table, not a bill.
  • Task tracking follows Claude's TodoWrite list. If your session uses another task tool, task-progress can still be driven through its progress tool.
  • safety-guard is a pattern-based safety net, not a security boundary. It catches common destructive commands and secret-file writes; an unusual command can get past it.
  • Command names avoid Claude Code built-ins such as /usage, /context and /todos.
  • Everything runs locally and nothing is sent anywhere. git-band runs git status.

Troubleshooting

ProblemWhat to try
/usage shows "Session limit / Breakdown"That is Claude Code's built-in panel. Use /usage-dashboard.
A mod's command isn't foundThe mod isn't loaded. Install it, or start Claude with --plugin-dir ./<mod>.
Gauges say "No reading yet"Send one message; the reading arrives with the first reply.
Band shows the old layout after an updateClose the pane and reopen it, or restart Claude.
A command was blocked unexpectedlyRun /safeguard to see why, /safeguard pause if you really want it.
/plugin install isn't availableUse a terminal Claude Code session, not the desktop app's Code tab.

Develop

claude plugin validate .                 # the marketplace
claude plugin validate ./<mod>           # one mod
claude plugin test ./<mod>               # its tests

Each mod is a folder with .claude-plugin/plugin.json, hooks/hooks.json, a hooks module, pure logic with *.test.ts files, and (where it keeps state) a types/index.d.ts contract. The marketplace file is .claude-plugin/marketplace.json.

Releases are git tags: v0.2.0 added context-meter, safety-guard, git-band and notes-todos.

License

MIT, see LICENSE.

Source 3 files
hooks/register.tsx 256 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Flow, Limit, Working } from '../types'
5import {
6  applyReport,
7  applyTodos,
8  barWidth,
9  elapsed,
10  etaLeft,
11  percent,
12  shortModel,
13  smoothBar,
14  spinner,
15  usageParts,
16  windowItems,
17} from './progress'
18import type { Report } from './progress'
19
20const TOOL = 'mcp__task-progress__progress'
21
22const flow = atom({ plugin: 'task-progress', key: 'flow' } as const, null as Flow | null)
23const working = atom({ plugin: 'task-progress', key: 'working' } as const, null as Working | null)
24const nowAtom = atom({ plugin: 'task-progress', key: 'now' } as const, 0)
25const isHidden = atom({ plugin: 'task-progress', key: 'isHidden' } as const, false)
26const isCompact = atom({ plugin: 'task-progress', key: 'isCompact' } as const, false)
27const limits = atom({ plugin: 'task-progress', key: 'limits' } as const, [] as Limit[])
28const model = atom({ plugin: 'task-progress', key: 'model' } as const, null as string | null)
29
30/** The 5h and weekly windows the last reply reported; the same source the usage dashboard reads. */
31async function refreshLimits($: EngineInterface) {
32  const usage = await $.session.usage()
33  const list: Limit[] = usage.rateLimits.map(l => ({
34    kind: l.kind,
35    percentUsed: l.percentUsed,
36    resetsAt: l.resetsAt ?? null,
37  }))
38  await update($, limits, () => list)
39}
40
41export const register: Register = on => {
42  let isWorking = false
43
44  on('session.start', async ($, e, next) => {
45    await $.tool.register({
46      name: 'progress',
47      description:
48        'Report progress of the current job to the progress bar above the prompt. ' +
49        'Call it once after planning (title, total) and again as steps finish (done, note). ' +
50        'Optional when a todo list is in use: the bar follows the todo list by itself. ' +
51        'Pass finished: true at the end. Fields left out keep their value.',
52      inputSchema: {
53        type: 'object',
54        properties: {
55          title: { type: 'string', description: 'Short name of the job, a few words.' },
56          total: { type: 'integer', minimum: 0, description: 'Number of steps planned.' },
57          done: { type: 'integer', minimum: 0, description: 'Steps finished so far.' },
58          note: { type: 'string', description: 'The step in progress, a few words.' },
59          finished: { type: 'boolean', description: 'True once the job is complete.' },
60          items: {
61            type: 'array',
62            description: 'Optional step list; total and done follow it.',
63            items: {
64              type: 'object',
65              properties: {
66                title: { type: 'string' },
67                status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
68              },
69              required: ['title'],
70            },
71          },
72        },
73      },
74    })
75    await $.command.register({
76      name: 'progress',
77      description: 'Show or hide the progress band above the prompt',
78    })
79
80    await refreshLimits($).catch(() => undefined)
81
82    // Redraws the spinner and timer, only while a turn runs.
83    $.clock.every(250, () => {
84      if (!isWorking) return
85      void $.clock.now().then(t => update($, nowAtom, () => t))
86    })
87    // Keeps the usage figures fresh between replies.
88    $.clock.every(30_000, () => {
89      void refreshLimits($).catch(() => undefined)
90    })
91
92    return next(e)
93  })
94
95  on('command.run', { command: 'progress' }, async $ => {
96    const hidden = await update($, isHidden, h => !h)
97    return { text: hidden ? 'Progress band hidden.' : 'Progress band shown.' }
98  }).catch(() => ({ text: 'Progress band could not toggle.' }))
99
100  on('prompt.submit', async ($, e, next) => {
101    const t = await $.clock.now()
102    isWorking = true
103    await update($, working, () => ({ since: t }))
104    await update($, nowAtom, () => t)
105    // A finished job is done being shown once the next prompt starts.
106    await update($, flow, f => (f && f.isFinished ? null : f))
107
108    return next(e)
109  }).catch(($, e, next) => next(e))
110
111  on('turn.complete', async ($, e, next) => {
112    if (e.agentId === undefined) {
113      isWorking = false
114      await update($, working, () => null)
115    }
116    if (e.usage !== undefined) {
117      const used = e.usage.model
118      await update($, model, () => used)
119    }
120    await refreshLimits($).catch(() => undefined)
121
122    return next(e)
123  }).catch(($, e, next) => next(e))
124
125  on('tool.call', { tool: TOOL }, async ($, e) => {
126    const input = e as unknown as Report
127    const t = await $.clock.now()
128    const next = await update($, flow, prev => applyReport(prev, input, t))
129
130    return { result: `ok: ${next ? `${next.title} ${next.done}/${next.total}` : 'cleared'}` }
131  }).catch(() => ({ result: 'progress report failed' }))
132
133  on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
134    const t = await $.clock.now()
135    await update($, flow, prev => applyTodos(prev, e.todos, t))
136
137    return next(e)
138  }).catch(($, e, next) => next(e))
139
140  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
141    if (e.props.hasSurvey || (await read($, isHidden))) return next(e)
142
143    const job = await read($, flow)
144    const busy = await read($, working)
145    const now = await read($, nowAtom)
146    if (job === null && busy === null) return next(e)
147
148    const { Box, Button, Text } = $.ui.resolve(e)
149    const compact = await read($, isCompact)
150    const usage = usageParts(await read($, limits), now)
151    const active = await read($, model)
152    const secs = busy ? elapsed(now - busy.since) : null
153    const hide = <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
154
155    if (job === null) {
156      return (
157        <Box paddingX={1} gap={1}>
158          <Text color="claude">{spinner(now)}</Text>
159          <Text dimColor>Working {secs}</Text>
160          {usage.map(u => (
161            <Text key={u.label} color={u.color}>
162              {u.label} {u.pct}%
163            </Text>
164          ))}
165          {hide}
166        </Box>
167      )
168    }
169
170    const sizeKnown = job.total > 0
171    const finished = job.isFinished && sizeKnown
172    const pct = percent(job)
173    const accent = finished ? 'success' : 'claude'
174    const left = etaLeft(job, now)
175    const track = smoothBar(pct, barWidth(e.viewport?.columns))
176    const view = windowItems(job.items, 5)
177    const icon = finished ? '✓' : spinner(now)
178    const showSteps = !compact && job.items.length > 0
179    const toggle = job.items.length > 0 && (
180      <Button
181        key="compact"
182        label={compact ? 'Expand' : 'Collapse'}
183        onPress={() => update($, isCompact, c => !c)}
184      />
185    )
186
187    return (
188      <Box flexDirection="column" borderStyle="round" borderColor={accent} paddingX={1}>
189        <Box justifyContent="space-between" gap={2}>
190          <Text bold color={accent} wrap="truncate-end">
191            {icon} {job.title}
192          </Text>
193          <Text dimColor wrap="truncate-end">
194            {sizeKnown ? `${job.done}/${job.total} · ${pct}%` : 'no step count yet'}
195            {left !== null ? ` · ~${elapsed(left)} left` : ''}
196            {secs !== null ? ` · ${secs}` : ''}
197          </Text>
198        </Box>
199        {sizeKnown && (
200          <Text>
201            <Text color={accent}>{track.filled}</Text>
202            <Text dimColor>{track.empty}</Text>
203          </Text>
204        )}
205        {compact && job.note !== null && !finished && (
206          <Text dimColor wrap="truncate-end">
207            ▸ {job.note}
208          </Text>
209        )}
210        {showSteps && view.before > 0 && <Text dimColor>  ↑ {view.before} earlier</Text>}
211        {showSteps &&
212          view.rows.map(({ item, index }) => (
213            <Text
214              key={`${index}-${item.title}`}
215              dimColor={item.status !== 'in_progress'}
216              wrap="truncate-end"
217            >
218              <Text
219                color={
220                  item.status === 'completed' ? 'success' : item.status === 'in_progress' ? 'claude' : undefined
221                }
222              >
223                {item.status === 'completed' ? '✓' : item.status === 'in_progress' ? '▸' : '○'}
224              </Text>{' '}
225              <Text bold={item.status === 'in_progress'}>{item.title}</Text>
226            </Text>
227          ))}
228        {showSteps && view.after > 0 && <Text dimColor>  ↓ {view.after} more</Text>}
229        {job.items.length === 0 && job.note !== null && !finished && (
230          <Text dimColor wrap="truncate-end">
231            ▸ {job.note}
232          </Text>
233        )}
234        <Box justifyContent="space-between" gap={2}>
235          <Text wrap="truncate-end">
236            {usage.length === 0 && active === null && <Text dimColor>usage: n/a</Text>}
237            {usage.map(u => (
238              <Text key={u.label}>
239                <Text dimColor>{u.label} </Text>
240                <Text color={u.color}>{u.pct}%</Text>
241                {u.reset !== null && <Text dimColor> ({u.reset})</Text>}
242                <Text dimColor> · </Text>
243              </Text>
244            ))}
245            {active !== null && <Text dimColor>{shortModel(active)}</Text>}
246          </Text>
247          <Box gap={1}>
248            {toggle}
249            {hide}
250          </Box>
251        </Box>
252      </Box>
253    )
254  })
255}
256
hooks/progress.ts 146 lines
1import type { Flow, Item, ItemStatus, Limit } from '../types'
2
3export type Report = {
4  title?: string
5  total?: number
6  done?: number
7  note?: string
8  items?: ReadonlyArray<{ title: string; status?: ItemStatus }>
9  finished?: boolean
10}
11
12export type Todo = { content: string; activeForm: string; status: ItemStatus }
13
14const int = (n: number | undefined): number | undefined =>
15  n === undefined || !Number.isFinite(n) ? undefined : Math.max(0, Math.round(n))
16
17const count = (items: readonly Item[]) => items.filter(i => i.status === 'completed').length
18
19export const blank = (now: number): Flow => ({
20  title: 'Working',
21  total: 0,
22  done: 0,
23  note: null,
24  items: [],
25  isFinished: false,
26  startedAt: now,
27  updatedAt: now,
28})
29
30/** Merges one explicit report (the progress tool) into the flow; fields left out keep their value. */
31export const applyReport = (prev: Flow | null, input: Report, now: number): Flow => {
32  const base = prev && !prev.isFinished ? prev : blank(now)
33  const items: Item[] =
34    input.items?.map(i => ({ title: i.title, status: i.status ?? 'pending' })) ?? base.items
35  const fromItems = input.items !== undefined
36  const total = int(input.total) ?? (fromItems ? items.length : base.total)
37  const done = int(input.done) ?? (fromItems ? count(items) : base.done)
38
39  return {
40    ...base,
41    title: input.title?.trim() || base.title,
42    total,
43    done: total > 0 ? Math.min(total, done) : done,
44    note: input.note === undefined ? base.note : input.note.trim() || null,
45    items,
46    isFinished: input.finished === true,
47    updatedAt: now,
48  }
49}
50
51/** The todo list is the plan: counts and the current step follow it with no cooperation needed. */
52export const applyTodos = (prev: Flow | null, todos: readonly Todo[], now: number): Flow | null => {
53  if (todos.length === 0) return null
54  const base = prev && !prev.isFinished ? prev : blank(now)
55  const items: Item[] = todos.map(t => ({ title: t.content, status: t.status }))
56  const current = todos.find(t => t.status === 'in_progress')
57  const done = count(items)
58
59  return {
60    ...base,
61    total: items.length,
62    done,
63    items,
64    note: current ? current.activeForm : null,
65    isFinished: done === items.length,
66    updatedAt: now,
67  }
68}
69
70export const percent = (flow: Pick<Flow, 'total' | 'done'>): number =>
71  flow.total <= 0 ? 0 : Math.round((Math.min(flow.done, flow.total) / flow.total) * 100)
72
73export const bar = (pct: number, width = 16): string => {
74  const filled = Math.max(0, Math.min(width, Math.round((pct / 100) * width)))
75  return '█'.repeat(filled) + '░'.repeat(width - filled)
76}
77
78export const elapsed = (ms: number): string => {
79  const s = Math.max(0, Math.floor(ms / 1000))
80  if (s < 60) return `${s}s`
81  const m = Math.floor(s / 60)
82  return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`
83}
84
85const EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']
86
87/** A bar with 1/8-cell resolution, split into the filled and the empty part for two colours. */
88export const smoothBar = (pct: number, width = 36): { filled: string; empty: string } => {
89  const clamped = Math.max(0, Math.min(100, pct))
90  const eighths = Math.round((clamped / 100) * width * 8)
91  const full = Math.floor(eighths / 8)
92  const part = EIGHTHS[eighths % 8]
93  const used = full + (part === '' ? 0 : 1)
94  return { filled: '█'.repeat(full) + part, empty: '░'.repeat(Math.max(0, width - used)) }
95}
96
97const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
98
99export const spinner = (now: number): string => FRAMES[Math.floor(now / 120) % FRAMES.length]
100
101/** Rough time left from the average time per finished step; null until a step has finished. */
102export const etaLeft = (flow: Pick<Flow, 'total' | 'done' | 'startedAt' | 'isFinished'>, now: number): number | null => {
103  if (flow.isFinished || flow.done <= 0 || flow.total <= flow.done) return null
104  const per = (now - flow.startedAt) / flow.done
105  return Math.max(0, per * (flow.total - flow.done))
106}
107
108/** At most `max` steps, windowed around the current one, with counts of what is cut off. */
109export const windowItems = (
110  items: readonly Item[],
111  max: number,
112): { rows: Array<{ item: Item; index: number }>; before: number; after: number } => {
113  if (items.length <= max) {
114    return { rows: items.map((item, index) => ({ item, index })), before: 0, after: 0 }
115  }
116  let current = items.findIndex(i => i.status === 'in_progress')
117  if (current < 0) current = items.findIndex(i => i.status === 'pending')
118  if (current < 0) current = items.length - 1
119  const start = Math.max(0, Math.min(items.length - max, current - 1))
120  const rows = items.slice(start, start + max).map((item, i) => ({ item, index: start + i }))
121  return { rows, before: start, after: items.length - start - max }
122}
123
124export const gaugeColor = (pct: number): 'success' | 'warning' | 'error' =>
125  pct > 85 ? 'error' : pct >= 60 ? 'warning' : 'success'
126
127export type UsagePart = { label: string; pct: number; color: 'success' | 'warning' | 'error'; reset: string | null }
128
129const LABELS: Record<string, string> = { five_hour: '5h', seven_day: 'wk' }
130
131/** The 5h and weekly windows, in that order, with a colour and time to reset. Other kinds are left out. */
132export const usageParts = (limits: readonly Limit[], now: number): UsagePart[] =>
133  ['five_hour', 'seven_day'].flatMap(kind => {
134    const l = limits.find(x => x.kind === kind)
135    if (!l) return []
136    const pct = Math.max(0, Math.min(100, Math.round(l.percentUsed)))
137    const at = l.resetsAt ? Date.parse(l.resetsAt) : NaN
138    return [{ label: LABELS[kind], pct, color: gaugeColor(pct), reset: Number.isNaN(at) ? null : elapsed(at - now) }]
139  })
140
141export const shortModel = (model: string): string => model.replace(/^claude-/, '')
142
143/** Bar width that fits the terminal: leaves room for the border and padding, within 12..60. */
144export const barWidth = (columns: number | undefined): number =>
145  Math.max(12, Math.min(60, (columns ?? 80) - 6))
146
types/index.d.ts 33 lines
1export type ItemStatus = 'pending' | 'in_progress' | 'completed'
2
3export type Item = { title: string; status: ItemStatus }
4
5export type Flow = {
6  title: string
7  total: number
8  done: number
9  note: string | null
10  items: Item[]
11  isFinished: boolean
12  startedAt: number
13  updatedAt: number
14}
15
16export type Working = { since: number }
17
18export type Limit = { kind: string; percentUsed: number; resetsAt: string | null }
19
20declare module 'claude-code' {
21  interface PluginState {
22    'task-progress': {
23      flow: Flow | null
24      working: Working | null
25      now: number
26      isHidden: boolean
27      isCompact: boolean
28      limits: Limit[]
29      model: string | null
30    }
31  }
32}
33