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

<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"> <!-- License --> <img src="https://img.shields.io/badge/License-MIT-brightgreen.svg?&logo=open-source-initiative&logoColor=white" alt="License: MIT"/> <!-- 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+"/> <!-- Last Commit --> <img src="https://img.shields.io/github/last-commit/healkeiser/claude-mods?logo=github&label=Last%20Commit" alt="Last Commit"/> <!-- 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> <!-- GitHub stars --> <img src="https://img.shields.io/github/stars/healkeiser/claude-mods" alt="GitHub Stars"/>
<!-- TABLE OF CONTENTS -->
<!-- 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.
| Mod | What it changes |
|---|---|
| pretty-markdown | Claude'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-band | The line above the prompt while Claude works, the folded tool rows, and the footer that closes each turn |
| prompt-cards | Your 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 -->
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 -->
Redraws each block of Claude's reply that has something to gain; plain prose keeps Claude Code's own look.
⧉ copy button. Past 10 lines they get line numbers; past 20 they fold to 12 with a ▾ show all N lines button. `diff ) draw in red and green, a hunk header added when the fence has none.## lines and bold-only lines such as What changed:) draw in the accent colour with a rule under them.> [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]) get their own colour and icon.Next: draws in its own box, so the action a reply ends on is easy to spot.⠹ editing parse.ts · 3 tools · 42s: a spinner, the current action, the tool count and the elapsed time.◇ 3 reads · ⌕ 2 searches · ⌘ 1 shell, dim, with failures in red.✻ Baked for 29s · 6 tools · 2 files edited.⏸ 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.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
/promptspane) 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.
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,gitand GNUdate. On Windows, Git Bash brings all of them exceptjq.
<!-- 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 -->
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 -->
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 -->
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> <!-- 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> <!-- Behance --> <a href="https://www.behance.net/el1ven"> <img src="https://img.shields.io/badge/el1ven-1769FF?logo=behance&style=social" alt="Behance"/></a> <!-- X --> <a href="https://twitter.com/valentinbeaumon"> <img src="https://img.shields.io/badge/@valentinbeaumon-1DA1F2?logo=x&style=social" alt="Twitter"/></a> <!-- 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> <!-- Gumroad --> <a href="https://healkeiser.gumroad.com/subscribe"> <img src="https://img.shields.io/badge/healkeiser-36a9ae?logo=gumroad&style=social" alt="Gumroad"/></a> <!-- 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> <!-- 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>
hooks/register.tsx 168 lines1import { 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}
168types/index.d.ts 12 lines1/** 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