SLOPSHOPPER

working-band

While a turn runs, shows what Claude is doing above the prompt: current action, tool count, elapsed time.

newbandspinnerrowsguardtimer
v0.1.0MITupdated 2026-10-14healkeiser/claude-mods/working-band
A shopper browsing a rack in a slop shop

Draws

Turn line
✻ Worked for 42s · 9 tools · 3 files edited
README

<img src="https://cdn.simpleicons.org/claude/D97757" alt="Claude" width="96">

<h3 align="center">claude-mods</h3>

Claude Code mods that make the terminal easier to read: polished replies, a live working band, and prompts you can find again. <a href="https://claude.com/resources/articles/claude-code-mods"><strong>What are mods?</strong></a>

##

<!-- Maintenance status --> <img src="https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg?&label=Maintenance">&nbsp;&nbsp; <!-- License --> <img src="https://img.shields.io/badge/License-MIT-brightgreen.svg?&logo=open-source-initiative&logoColor=white" alt="License: MIT"/>&nbsp;&nbsp; <!-- Claude Code version --> <img src="https://img.shields.io/badge/Claude%20Code-2.1.287%2B-D97757?&logo=claude&logoColor=white" alt="Claude Code 2.1.287+"/>&nbsp;&nbsp; <!-- Last Commit --> <img src="https://img.shields.io/github/last-commit/healkeiser/claude-mods?logo=github&label=Last%20Commit" alt="Last Commit"/>&nbsp;&nbsp; <!-- Commit Activity --> <a href="https://github.com/healkeiser/claude-mods/pulse" alt="Activity"> <img src="https://img.shields.io/github/commit-activity/m/healkeiser/claude-mods?&logo=github&label=Commit%20Activity"/></a>&nbsp;&nbsp; <!-- GitHub stars --> <img src="https://img.shields.io/github/stars/healkeiser/claude-mods" alt="GitHub Stars"/>

<!-- TABLE OF CONTENTS -->

Table of Contents

<!-- ABOUT -->

About

Three mods for Claude Code: small TypeScript modules that redraw parts of its terminal interface. Each one works on its own, so install only the ones you want.

ModWhat it changes
pretty-markdownClaude's replies: bordered code blocks with a copy button, inline-code chips, coloured diffs, rounded tables, accented headings, callouts, and a highlighted Next: line
working-bandThe line above the prompt while Claude works, the folded tool rows, and the footer that closes each turn
prompt-cardsYour own prompts: a blue card per prompt, a grey card while one is queued, and a /prompts pane to jump back to any of them

A matching status line script comes alongside. It is not a mod, so it installs on its own.

<!-- INSTALLATION -->

Installation

Add this repository as a plugin marketplace, then install the mods you want at user scope:

claude plugin marketplace add healkeiser/claude-mods
claude plugin install pretty-markdown@claude-mods --scope user
claude plugin install working-band@claude-mods --scope user
claude plugin install prompt-cards@claude-mods --scope user

Run /reload-plugins in a session that is already open; new sessions load them by themselves.

[!IMPORTANT] Mods need Claude Code 2.1.287 or later.

<!-- USAGE -->

Usage

pretty-markdown

Redraws each block of Claude's reply that has something to gain; plain prose keeps Claude Code's own look.

  • Code blocks draw in a rounded border with their language and a ⧉ copy button. Past 10 lines they get line numbers; past 20 they fold to 12 with a ▾ show all N lines button.
  • Inline code becomes a tinted chip.
  • Diff fences ( `diff ) draw in red and green, a hunk header added when the fence has none.
  • Tables draw in a rounded border with a bold header row; a table too wide for the window narrows its widest column and wraps it.
  • Headings (## lines and bold-only lines such as What changed:) draw in the accent colour with a rule under them.
  • Callouts (> [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]) get their own colour and icon.
  • A paragraph opening with Next: draws in its own box, so the action a reply ends on is easy to spot.

working-band

  • While a turn runs, the band above the prompt reads ⠹ editing parse.ts · 3 tools · 42s: a spinner, the current action, the tool count and the elapsed time.
  • Folded tool rows read ◇ 3 reads · ⌕ 2 searches · ⌘ 1 shell, dim, with failures in red.
  • The footer of a turn that used tools reads ✻ Baked for 29s · 6 tools · 2 files edited.

prompt-cards

  • Each prompt you send draws as a blue card, so your messages stand out when you scroll.
  • A fenced code block in a prompt draws as a grey box inside the card, with its language and syntax colours.
  • A prompt sent while Claude is still working draws as a narrow grey ⏸ queued card until it reaches the model. A queued slash command stays queued until the turn ends, which is when Claude Code runs it.
  • /prompts opens a pane listing your prompts, newest first. Pick one to scroll the transcript back to it; 1 to 9 reach the nine newest.
  • It ships a claude-mods theme, built on the dark theme, that turns the input box border the same blue as the cards. Pick it in /theme.

[!NOTE] Clicking a button (copy, fold, a row of the /prompts pane) needs Claude Code's fullscreen layout, where the terminal reports clicks. The pane's hotkeys work either way.

[!NOTE] Colours are tuned for dark themes: the inline-code chip and the prompt card's background are fixed colours, and read as dark bands on a light theme.

Status line

statusline/statusline.sh draws the status line as a rounded card: the model, the folder, the branch and its uncommitted changes, then one bar each for the context window and the 5-hour and 7-day limits, with their reset time and pace.

╭─ Opus 5.5 · claude-mods ↗ · master +5 ~4 ──────────╮
│ ctx  ━━━━──────   42%     18m 04s  ↑ 13.7k ↓ 2.1k  │
│ 5h   ━━━━━━────   61%   ↻ 2h 12m   17:26      1.1× │
│ 7d   ━━━━━━━━━─   96%   ↻ 3d 5h    Sat 21:00  1.8× │
╰────────────────────────────────────────────────────╯

The pace compares how fast a limit is being used with how much of its window has passed: 1.0× finishes the window right at 100%, and above that it turns yellow, then red at 1.5×.

Copy the script into ~/.claude/ and point statusLine at it in ~/.claude/settings.json:

"statusLine": {
  "type": "command",
  "command": "bash ~/.claude/statusline.sh"
}

[!IMPORTANT] The script needs bash, jq, git and GNU date. On Windows, Git Bash brings all of them except jq.

<!-- SECURITY -->

Security

Mods are not sandboxed: they run with the same access to your machine as Claude Code itself. These three only draw and count; none of them runs a command, reads your files, or makes a network call. Read hooks/ before installing, as you would for any plugin.

<!-- DEVELOPMENT -->

Development

Each mod is a plugin folder: .claude-plugin/plugin.json, hooks/hooks.json, and the hooks module hooks/register.tsx, with its tests beside it.

git clone https://github.com/healkeiser/claude-mods
cd claude-mods
claude plugin marketplace add ./
claude plugin validate ./pretty-markdown
claude plugin test ./pretty-markdown

A marketplace added from a local folder is read in place: edit a mod, run /reload-plugins, and the session picks it up with no reinstall.

<!-- LICENSE -->

License

MIT. See LICENSE.

[!NOTE] Claude and Claude Code are trademarks of Anthropic. These mods are an independent community project and are not affiliated with, endorsed by, or sponsored by Anthropic.

<!-- CONTACT -->

Contact

Project Link: claude-mods

<!-- GitHub profile --> <a href="https://github.com/healkeiser"> <img src="https://img.shields.io/badge/healkeiser-181717?logo=github&style=social" alt="GitHub"/></a>&nbsp;&nbsp; <!-- LinkedIn --> <a href="https://www.linkedin.com/in/valentin-beaumont"> <img src="https://img.shields.io/badge/Valentin%20Beaumont-0A66C2?logo=linkedin&style=social" alt="LinkedIn"/></a>&nbsp;&nbsp; <!-- Behance --> <a href="https://www.behance.net/el1ven"> <img src="https://img.shields.io/badge/el1ven-1769FF?logo=behance&style=social" alt="Behance"/></a>&nbsp;&nbsp; <!-- X --> <a href="https://twitter.com/valentinbeaumon"> <img src="https://img.shields.io/badge/@valentinbeaumon-1DA1F2?logo=x&style=social" alt="Twitter"/></a>&nbsp;&nbsp; <!-- Instagram --> <a href="https://www.instagram.com/val.beaumontart"> <img src="https://img.shields.io/badge/@val.beaumontart-E4405F?logo=instagram&style=social" alt="Instagram"/></a>&nbsp;&nbsp; <!-- Gumroad --> <a href="https://healkeiser.gumroad.com/subscribe"> <img src="https://img.shields.io/badge/healkeiser-36a9ae?logo=gumroad&style=social" alt="Gumroad"/></a>&nbsp;&nbsp; <!-- Gmail --> <a href="mailto:valentin.onze@gmail.com"> <img src="https://img.shields.io/badge/valentin.onze@gmail.com-D14836?logo=gmail&style=social" alt="Email"/></a>&nbsp;&nbsp; <!-- Buy me a coffee --> <a href="https://www.buymeacoffee.com/healkeiser"> <img src="https://img.shields.io/badge/Buy Me A Coffee-FFDD00?&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me A Coffee"/></a>&nbsp;&nbsp;

Source 3 files
hooks/register.tsx 103 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, Timer } from 'claude-code'
3
4import type { Activity } from '../types'
5import { describe, elapsed, summarize } from './label'
6
7const activity = atom({ plugin: 'working-band', key: 'activity' } as const, null)
8const SPINNER = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
9// 8 frames a second: smooth enough for a braille spinner, light on redraws.
10const FRAME_MS = 125
11
12type TurnStats = { tools: number; files: Set<string> }
13
14export const register: Register = on => {
15  let tick: Timer | undefined
16  // This turn's counts, the last finished turn's waiting for its footer, and
17  // each footer's once drawn. Module memory: after a reload, older footers
18  // draw as the engine's.
19  let turn: TurnStats = { tools: 0, files: new Set() }
20  let finished: TurnStats | null = null
21  const footers = new Map<string, TurnStats>()
22
23  on('turn.start', async ($, e, next) => {
24    if (e.agentId !== undefined) return next(e)
25    turn = { tools: 0, files: new Set() }
26    tick?.cancel()
27    await update($, activity, (): Activity => ({ label: 'thinking', tools: 0, frames: 0 }))
28    tick = $.clock.every(FRAME_MS, () => {
29      void update($, activity, a => (a ? { ...a, frames: a.frames + 1 } : a))
30    })
31    return next(e)
32  })
33
34  // Never awaited: a failed band update must not hold up the tool call.
35  on('tool.call', ($, e, next) => {
36    turn.tools += 1
37    const path = (e as { file_path?: unknown }).file_path
38    if (/^(Edit|MultiEdit|Write|NotebookEdit)$/.test(e.tool) && typeof path === 'string') turn.files.add(path)
39    update($, activity, a => (a ? { ...a, label: describe(e), tools: a.tools + 1 } : a)).catch(() => {})
40    return next(e)
41  }).catch(($, e, next) => next(e))
42
43  on('turn.complete', async ($, e, next) => {
44    if (e.agentId === undefined) {
45      finished = turn
46      tick?.cancel()
47      tick = undefined
48      await update($, activity, () => null)
49    }
50    return next(e)
51  })
52
53  // `✻ Baked for 29s · 6 tools · 2 files edited`
54  on('ui.render', { component: 'TurnDuration' }, ($, e, next) => {
55    if (!footers.has(e.requestId) && finished) {
56      footers.set(e.requestId, finished)
57      finished = null
58    }
59    const stats = footers.get(e.requestId)
60    if (!stats || stats.tools === 0) return next(e)
61
62    const { Text } = $.ui.resolve(e)
63    const parts = [
64      `${e.props.word} for ${elapsed(Math.round(e.props.durationMs / 1000))}`,
65      stats.tools === 1 ? '1 tool' : `${stats.tools} tools`,
66      ...(stats.files.size ? [`${stats.files.size} ${stats.files.size === 1 ? 'file' : 'files'} edited`] : []),
67    ]
68    return <Text dimColor>{`✻ ${parts.join('  ·  ')}`}</Text>
69  })
70
71  // `◇ 3 reads  ·  ⌕ 2 searches`, dim, so the replies stand out.
72  on('ui.render', { component: 'ToolGroup' }, ($, e, next) => {
73    if (e.props.isExpanded || e.props.calls.length === 0) return next(e)
74    const { Text } = $.ui.resolve(e)
75    const { text, failed } = summarize(e.props.calls)
76    const isRunning = e.props.calls.some(c => c.isRunning)
77    return (
78      <Text>
79        <Text dimColor>{`  ${text}${isRunning ? '  …' : ''}`}</Text>
80        {failed ? <Text color="error">{`  ·  ✕ ${failed} failed`}</Text> : null}
81      </Text>
82    )
83  })
84
85  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
86    const a = await read($, activity)
87    if (e.props.hasSurvey || !e.props.isWorking || a === null) return next(e)
88
89    const { Box, Text } = $.ui.resolve(e)
90    const tools = a.tools === 1 ? '1 tool' : `${a.tools} tools`
91
92    return (
93      <Box flexDirection="row">
94        <Text color="claude">{SPINNER[a.frames % SPINNER.length]} </Text>
95        <Text wrap="truncate-end">
96          {a.label}
97          <Text dimColor>{`  ·  ${tools}  ·  ${elapsed(Math.floor((a.frames * FRAME_MS) / 1000))}`}</Text>
98        </Text>
99      </Box>
100    )
101  })
102}
103
hooks/label.ts 56 lines
1const base = (path: unknown) => String(path ?? '').split(/[\\/]/).pop() || 'file'
2
3// One short phrase per tool call, for the band.
4export function describe(e: { tool: string; [arg: string]: unknown }): string {
5  switch (e.tool) {
6    case 'Edit':
7    case 'MultiEdit':
8    case 'NotebookEdit':
9      return `editing ${base(e.file_path ?? e.notebook_path)}`
10    case 'Write':
11      return `writing ${base(e.file_path)}`
12    case 'Read':
13      return `reading ${base(e.file_path)}`
14    case 'Grep':
15    case 'Glob':
16      return `searching ${String(e.pattern ?? '')}`
17    case 'Bash':
18    case 'PowerShell':
19      return e.description ? String(e.description) : `running ${String(e.command ?? '').split(/\s+/)[0]}`
20    case 'Agent':
21      return `agent: ${String(e.description ?? 'working')}`
22    case 'WebFetch':
23      try { return `fetching ${new URL(String(e.url)).host}` } catch { return 'fetching' }
24    case 'WebSearch':
25      return `searching the web`
26  }
27  const mcp = /^mcp__(.+?)__(.+)$/.exec(e.tool)
28  return mcp ? `${mcp[1]}: ${mcp[2]!.replace(/_/g, ' ')}` : e.tool
29}
30
31// What a folded run of tool calls did, by kind, in the order first seen.
32const KINDS: [RegExp, string, string, string][] = [
33  [/^(Read|NotebookRead)$/, '◇', 'read', 'reads'],
34  [/^(Grep|Glob|LS)$/, '⌕', 'search', 'searches'],
35  [/^(Bash|PowerShell)$/, '⌘', 'shell', 'shells'],
36  [/^(WebFetch|WebSearch)$/, '⇣', 'fetch', 'fetches'],
37  [/^mcp__/, '⧉', 'mcp call', 'mcp calls'],
38]
39
40export function summarize(calls: readonly { tool: string; isErrored: boolean }[]): { text: string; failed: number } {
41  const counts = new Map<string, { icon: string; one: string; many: string; n: number }>()
42  for (const c of calls) {
43    const kind = KINDS.find(([re]) => re.test(c.tool))
44    const [icon, one, many] = kind ? kind.slice(1) as [string, string, string] : ['·', c.tool, c.tool]
45    const got = counts.get(one) ?? { icon, one, many, n: 0 }
46    got.n += 1
47    counts.set(one, got)
48  }
49  const text = [...counts.values()].map(k => `${k.icon} ${k.n} ${k.n === 1 ? k.one : k.many}`).join('  ·  ')
50  return { text, failed: calls.filter(c => c.isErrored).length }
51}
52
53export function elapsed(seconds: number): string {
54  return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, '0')}s`
55}
56
types/index.d.ts 8 lines
1export type Activity = { label: string; tools: number; frames: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'working-band': { activity: Activity | null }
6  }
7}
8