SLOPSHOPPER

prompt-cards

Draws each prompt you send as a blue bordered card, and /prompts lists them in a pane that jumps to any one.

newpanerowscommandtoast
v0.2.0MITupdated 2026-10-14healkeiser/claude-mods/prompt-cards
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-cards
│ ┃ Your prompts ✕ ╭─────────────────────────────────────────────────────────────────╮ │ ┃ 1: p1 │ you │ │ │ 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 │ │ › /prompts │ ⎿ prompt-cards: Prompts pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Your prompts
1: p1
Your message
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ you │ │ fix the failing auth test and add an audit log call │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
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 2 files
hooks/register.tsx 168 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { PromptsSent, PromptsVersion } from '../types'
5
6// Blue, so a prompt card never reads as one of pretty-markdown's grey code blocks.
7const CARD = 'suggestion'
8// shortcut: a fixed dark blue tint, made for dark themes; on a light theme it
9// reads as a dark band. Switch on the theme if a light one is ever used.
10const CARD_BG = '#1c2233'
11// A neutral grey, so a code block reads apart from the blue card around it.
12const CODE_BG = '#2a2c31'
13const PANE = 'prompts'
14
15const version = atom({ plugin: 'prompt-cards', key: 'version' } as const, 0 as PromptsVersion)
16const sent = atom({ plugin: 'prompt-cards', key: 'sent' } as const, { ids: [], turnId: null, opener: '' } as PromptsSent)
17
18// Splits a prompt into prose and fenced code, so a pasted block draws as code.
19// A fence left open runs to the end, as Markdown reads it.
20export function pieces(text: string): { text: string; language?: string }[] {
21  const out: { text: string; language?: string }[] = []
22  const prose: string[] = []
23  // The engine wraps a paste in `<pasted_content>` tags for the model; the person
24  // never typed them, so the card leaves them out.
25  const lines = text.split(/\r?\n/).filter(l => !/^\s*<\/?pasted_content\b[^>]*>\s*$/.test(l))
26  const flush = () => {
27    const t = prose.join('\n').replace(/^\n+|\n+$/g, '')
28    if (t) out.push({ text: t })
29    prose.length = 0
30  }
31  for (let i = 0; i < lines.length; i++) {
32    const open = /^ {0,3}(`{3,}|~{3,})\s*([\w+#.-]*)/.exec(lines[i]!)
33    if (!open) {
34      prose.push(lines[i]!)
35      continue
36    }
37    const body: string[] = []
38    let j = i + 1
39    while (j < lines.length && !lines[j]!.trimStart().startsWith(open[1]!)) body.push(lines[j++]!)
40    flush()
41    out.push({ text: body.join('\n').replace(/^(\s*\n)+|\s+$/g, ''), language: open[2] ?? '' })
42    i = j
43  }
44  flush()
45  return out
46}
47
48export const register: Register = on => {
49  // The person's prompts in transcript order, as their rows were drawn. A row is
50  // drawn again after a reload, so the list fills back up by itself.
51  const prompts = new Map<string, string>()
52  // The turn each prompt row was first drawn in: a row first drawn while a turn
53  // runs, and not yet stored, is one the person queued.
54  const firstSeen = new Map<string, string | null>()
55
56
57  on('turn.start', async ($, e, next) => {
58    if (e.agentId === undefined) {
59      await update($, sent, s => ({ ...s, turnId: e.turnId, opener: e.text.trim() }))
60    }
61    return next(e)
62  })
63
64  // A typed prompt is stored with door `prompt`, a queued one with door `queue`
65  // as it enters the queue (still unsent) and with door `delivery` once it
66  // reaches the model. The `prompt` and `delivery` uuids are the sent rows' ids.
67  // Awaited, so the id is recorded before the row it names is drawn.
68  on('session.append', async ($, e, next) => {
69    const isSent = e.agentId === undefined && e.message.role === 'user' &&
70      (e.door === 'prompt' || e.door === 'delivery')
71    if (isSent) await update($, sent, s => ({ ...s, ids: [...s.ids, e.uuid].slice(-500) }))
72    return next(e)
73  }).catch(($, e, next) => next(e))
74
75  on('session.start', async ($, e, next) => {
76    await $.command.register({ name: 'prompts', description: 'List the prompts you sent and jump to one' })
77    return next(e)
78  })
79
80  on('command.run', { command: 'prompts' }, async $ => {
81    const { isPlaced } = await $.ui.open({ id: PANE, title: 'Your prompts', focus: true })
82    return { text: isPlaced ? 'Prompts pane opened.' : 'The prompts pane needs a wider window.' }
83  })
84
85  on('turn.complete', async ($, e, next) => {
86    if (e.agentId === undefined) {
87      await update($, version, n => n + 1)
88      await update($, sent, s => ({ ...s, turnId: null, opener: '' }))
89    }
90    return next(e)
91  })
92
93  on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
94    // Only the person's own prompts: typed here, or sent from another device.
95    const kind = e.props.origin.kind
96    if (kind !== 'composer' && kind !== 'bridge') return next(e)
97    prompts.set(e.requestId, e.props.text)
98
99    const s = await read($, sent)
100    if (!firstSeen.has(e.requestId)) firstSeen.set(e.requestId, s.turnId)
101    // shortcut: the row carries no queued flag, so a row new in this turn is
102    // queued until a sent id names it. A queued command stays queued until the
103    // turn ends, when the engine runs it.
104    const isQueued = s.turnId !== null && firstSeen.get(e.requestId) === s.turnId && !s.ids.includes(e.requestId) &&
105      e.props.text.trim() !== s.opener
106
107    const { Box, Text, Code } = $.ui.resolve(e)
108
109    // Queued: a grey card only as wide as its text and set in, so it never
110    // reads as a sent prompt.
111    if (isQueued) {
112      return (
113        <Box flexDirection="column" borderStyle="round" borderColor="subtle" paddingX={1} marginLeft={4} alignSelf="flex-start">
114          <Text dimColor italic>⏸ queued</Text>
115          <Text dimColor>{e.props.text}</Text>
116        </Box>
117      )
118    }
119
120    return (
121      <Box flexDirection="column" borderStyle="round" borderColor={CARD} backgroundColor={CARD_BG} paddingX={1}>
122        <Text color={CARD} weight="bold">you</Text>
123        {pieces(e.props.text).map((p, k) =>
124          p.language === undefined ? (
125            <Text key={`t${k}`}>{p.text}</Text>
126          ) : (
127            <Box key={`c${k}`} flexDirection="column" borderStyle="round" borderColor="subtle" backgroundColor={CODE_BG} paddingX={1}>
128              {p.language ? <Text dimColor italic>{p.language}</Text> : null}
129              <Code source={p.text} language={p.language || undefined} />
130            </Box>
131          ),
132        )}
133      </Box>
134    )
135  })
136
137  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
138    await read($, version)
139    const { Box, Text, Button } = $.ui.resolve(e)
140    const width = Math.max(10, e.props.bodyColumns - 8)
141    // Newest first, so the hotkeys 1 to 9 reach the recent ones.
142    const list = [...prompts].map(([id, text], n) => ({ id, text, n: n + 1 })).reverse()
143
144    return (
145      <Box flexDirection="column">
146        {list.length === 0 && <Text dimColor>No prompts yet.</Text>}
147        {list.map((p, row) => {
148          const line = p.text.split('\n')[0]!.trim()
149          return (
150            <Button
151              key={`p${p.n}`}
152              plain
153              hotkey={row < 9 ? String(row + 1) : undefined}
154              onPress={async () => {
155                const moved = await $.ui.scroll({ to: { requestId: p.id }, block: 'start' })
156                if ('deny' in moved && moved.deny) $.ui.toast(`Can't jump there: ${moved.deny}`)
157              }}
158            >
159              <Text color={CARD}>{`#${p.n} `}</Text>
160              {line.length > width ? `${line.slice(0, width - 1)}…` : line}
161            </Button>
162          )
163        })}
164      </Box>
165    )
166  })
167}
168
types/index.d.ts 12 lines
1/** Bumped after each turn so an open /prompts pane redraws with the new prompt. */
2export type PromptsVersion = number
3
4/** The sent rows' ids, the running turn, and the prompt that opened it (never a queued one). */
5export type PromptsSent = { ids: readonly string[]; turnId: string | null; opener: string }
6
7declare module 'claude-code' {
8  interface PluginState {
9    'prompt-cards': { version: PromptsVersion; sent: PromptsSent }
10  }
11}
12