Draws design-direction questions from AskUserQuestion as visual mockups to pick from; other questions untouched.

Four mods for Claude Code:
Type these at the prompt of a Claude Code session in a terminal:
/plugin install prompt-enhancer --marketplace aravindsriraj/claude-code-mods
/plugin install design-picker --marketplace aravindsriraj/claude-code-mods
/plugin install assumption-tracker --marketplace aravindsriraj/claude-code-mods
/plugin install idea-shelf --marketplace aravindsriraj/claude-code-mods
The first time, Claude Code asks whether to add the marketplace (github:aravindsriraj/claude-code-mods): answer y. Then choose a scope; the user scope is listed first, and Enter picks it. The mod starts working in that session right away, and with the user scope in every session after.
Mods built on Claude Code's hooks API need a recent Claude Code; the API is in early access and may change between releases.
A one-line bar above the prompt box with an ✨ Enhance button.
fix the export bug.ctrl+x tab, then e. The bar shows "Enhancing…", then your draft is replaced with a rewritten prompt.s) or just Enter. Undo (u) puts your original draft back.The rewrite sees what the main model sees: your CLAUDE.md and memory files, the installed skills, and the conversation so far, so "the bug" can become a real file and function. On the first prompt of a new session, before there is a conversation, it reads your instruction files and skill names directly.
Good to know:
@file mentions or pasted images, and the transcript shows the message as sent by the mod. For those prompts, press Enter instead.When Claude asks you to choose a design direction with its AskUserQuestion tool (often while using the frontend-design skill), each option is drawn as a mockup above the usual list:
preview is an SVG it is shown as is; otherwise the card shows the label, description, color swatches for any hex colors mentioned, and the preview text.Press Pick on a mockup, or answer in the list below it as usual; whichever you do first is the answer Claude gets.
Which questions it takes: single-pick questions whose header or text mentions design, direction, aesthetic, visual, theme, palette, colour, typography, layout, mockup, UI or UX, when every question in the call is one. Everything else, including a call that mixes design and other questions, is drawn exactly as before.
Good to know:
While Claude works, it records each assumption it acts on: a requirement nobody stated, a project convention, a fact about the code it hasn't checked. It does this by calling the mod's record_assumption tool, which the mod lists in Claude's tools and tells it about in the system prompt, then keeps working without stopping to ask.
ctrl+x tab, then a. That opens a side pane listing every assumption, with Claude's reason when it gave one.Good to know:
/clear starts the list over.Park a side-idea without derailing what Claude is doing, and send it later.
| Command | What it does |
|---|---|
/idea <text> | Puts the idea on the shelf with a timestamp. Runs at once, even while Claude is working, and shows a 💡 toast. |
/ideas | Opens the shelf in a side pane, numbered, with a Send button per idea. Press its number (or click Send) to run it. Esc closes. |
/idea-send <n> | Sends idea n as a prompt and takes it off the shelf. |
A sent idea is submitted as your own prompt (the transcript labels it "Prompt from the idea-shelf plugin"). If Claude is busy it waits its turn.
Claude never sees a shelved idea. The mod replaces the text of your /idea command with [shelved for later; hidden from Claude] before it reaches the conversation, so Claude only learns that something was shelved. /ideas draws the list in a pane, outside the conversation. Your typed text does stay in your local prompt history (up-arrow) and in the session file's record of the command, neither of which Claude reads.
The shelf lives in ~/.claude/idea-shelf.md, one idea per line (- [2026-10-09 14:32] your idea), shared by every project and session. You can edit it by hand.
Each mod is a folder with .claude-plugin/plugin.json, hooks/hooks.json and the hooks module hooks/register.tsx; .claude-plugin/marketplace.json at the root lists them all.
claude plugin validate .
claude plugin validate ./prompt-enhancer
claude plugin test ./prompt-enhancer
claude plugin validate ./design-picker
claude plugin test ./design-picker
claude plugin validate ./assumption-tracker
claude plugin test ./assumption-tracker
claude plugin validate ./idea-shelf
claude plugin test ./idea-shelf
To try a change, start a session with the mod loaded from your working copy: claude --plugin-dir ./prompt-enhancer. Claude Code then writes the API's type declarations into .claude-plugin/types/ and a tsconfig.json beside them, so tsc -p ./prompt-enhancer type-checks it; both are gitignored.
hooks/register.tsx 200 lines1import { atom, read, update } from 'claude-code'
2import type { BuiltinToolInputs, Register } from 'claude-code'
3
4import type { Picks } from '../types'
5
6type Question = BuiltinToolInputs['AskUserQuestion']['questions'][number]
7type Option = Question['options'][number]
8
9const picks = atom({ plugin: 'design-picker', key: 'picks' } as const, {} as Picks)
10
11// One resolver per open design question call, by tool_use_id: the gallery's Pick settles the call.
12// ponytail: module memory, so a hot reload mid-question falls back to the engine's own dialog.
13const pending = new Map<string, (answers: Record<string, string>) => void>()
14
15// ponytail: keyword heuristic on header + question; widen the list if real design questions slip past.
16const DESIGN =
17 /\b(design|direction|aesthetic|visual|look and feel|look & feel|vibe|mood|theme|palette|colou?rs?|typography|fonts?|layout|mockups?|ui|ux)\b/i
18
19const isDesign = (q: Question) =>
20 !q.multiSelect && q.options.length >= 2 && DESIGN.test(`${q.header} ${q.question}`)
21
22/** A call the gallery takes: every question is a single-pick design question. */
23const isDesignCall = (questions: readonly Question[]) => questions.length > 0 && questions.every(isDesign)
24
25// The engine lets a tree draw 12 rows around its AskUserQuestion dialog, counted by adding up every line
26// of text, every preview line, and each Button label and Svg alt (a row, plus one per 40 columns).
27const DIALOG_ROWS = 12
28const rowsOf = (text: string) => 1 + Math.floor(text.length / 40)
29const pickText = (o: Option) => `✓ ${o.label}${' ██'.repeat(swatches(o).length)}`
30const altText = (o: Option) => `${o.label} mockup`
31
32/** Rows the gallery needs before any preview line: the hint, then per question its header and its options. */
33const fixedRows = (questions: readonly Question[], isTerminal: boolean) =>
34 1 +
35 questions.reduce(
36 (n, q) => n + 1 + q.options.reduce((m, o) => m + rowsOf(pickText(o)) + (isTerminal ? 0 : rowsOf(altText(o))), 0),
37 0,
38 )
39
40const hexes = (o: Option) => [
41 ...new Set(`${o.description} ${o.preview ?? ''}`.match(/#[0-9a-f]{6}\b/gi) ?? []),
42].slice(0, 8)
43
44const swatches = (o: Option) => hexes(o).slice(0, 4)
45
46const esc = (s: string) =>
47 s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
48
49const wrap = (s: string, n: number) =>
50 s.split(/\s+/).reduce<string[]>((lines, word) => {
51 const last = lines[lines.length - 1]
52 if (last !== undefined && `${last} ${word}`.length <= n) lines[lines.length - 1] = `${last} ${word}`
53 else lines.push(word)
54 return lines
55 }, [])
56
57/** One option as an SVG card: its own SVG preview when it has one, else title, palette and preview text. */
58function card(o: Option, isPicked: boolean): string {
59 if (o.preview?.trimStart().startsWith('<svg')) return o.preview
60
61 const W = 360
62 const desc = wrap(o.description, 52).slice(0, 4)
63 const colors = hexes(o)
64 const body = (o.preview ?? '').split('\n').slice(0, 18)
65 let y = 30
66 const parts = [`<text x="16" y="${y}" font-family="system-ui,sans-serif" font-size="16" font-weight="600" fill="#1f1f1f">${esc(o.label)}</text>`]
67 for (const line of desc) {
68 y += 17
69 parts.push(`<text x="16" y="${y}" font-family="system-ui,sans-serif" font-size="12" fill="#5f5f5f">${esc(line)}</text>`)
70 }
71 if (colors.length > 0) {
72 y += 12
73 colors.forEach((c, i) =>
74 parts.push(`<rect x="${16 + i * 40}" y="${y}" width="34" height="24" rx="4" fill="${c}" stroke="#0002"/>`),
75 )
76 y += 24
77 }
78 if (body.some(l => l.trim())) {
79 y += 12
80 parts.push(`<rect x="12" y="${y}" width="${W - 24}" height="${body.length * 14 + 12}" rx="6" fill="#f4f4f2"/>`)
81 body.forEach((line, i) =>
82 parts.push(`<text x="20" y="${y + 18 + i * 14}" font-family="ui-monospace,monospace" font-size="11" fill="#2b2b2b" xml:space="preserve">${esc(line)}</text>`),
83 )
84 y += body.length * 14 + 12
85 }
86 const H = y + 16
87 const stroke = isPicked ? 'stroke="#d97757" stroke-width="3"' : 'stroke="#d6d6d2" stroke-width="1"'
88 return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}"><rect x="1" y="1" width="${W - 2}" height="${H - 2}" rx="10" fill="#ffffff" ${stroke}/>${parts.join('')}</svg>`
89}
90
91export const register: Register = on => {
92 // Holds a design question open and answers it with the gallery's picks; the engine's dialog stays live beneath.
93 on('tool.call', { tool: 'AskUserQuestion' }, async ($, e, next) => {
94 const id = e.tool_use_id
95 if (!id || !isDesignCall(e.questions)) return next(e)
96
97 const picked = new Promise<Record<string, string>>(resolve => pending.set(id, resolve))
98 const answered = next(e)
99 answered.catch(() => undefined) // aborted beneath once the gallery wins
100
101 try {
102 const winner = await Promise.race([answered, picked.then(answers => ({ answers }))])
103 if (!('answers' in winner)) return winner
104 return { result: { questions: e.questions, answers: winner.answers } }
105 } finally {
106 pending.delete(id)
107 await update($, picks, ({ [id]: _, ...rest }) => rest)
108 }
109 })
110
111 on('ui.render', { component: 'AskUserQuestion' }, async ($, e, next) => {
112 const questions = e.props.questions as Question[]
113 if (!pending.has(e.requestId) || !isDesignCall(questions)) return next(e)
114
115 const id = e.requestId
116 const engine = await next(e)
117 const isTerminal = e.surface === 'terminal'
118 const fixed = fixedRows(questions, isTerminal)
119 if (fixed > DIALOG_ROWS) return engine // too many options to draw around the dialog: its own list alone
120 const chosen = (await read($, picks))[id] ?? {}
121
122 const pick = async (question: string, label: string) => {
123 const all = await update($, picks, p => ({ ...p, [id]: { ...p[id], [question]: label } }))
124 const mine = all[id] ?? {}
125 if (questions.every(q => mine[q.question] !== undefined)) pending.get(id)?.(mine)
126 }
127
128 const { Box, Text, Button } = $.ui.resolve(e)
129 // Drawn as pickText says, so the row budget counts what is drawn.
130 const pickButton = (qi: number, oi: number, q: Question, o: Option) => (
131 <Button
132 key={`pick:${qi}:${oi}`}
133 label={o.label}
134 variant={chosen[q.question] === o.label ? 'primary' : 'secondary'}
135 onPress={() => pick(q.question, o.label)}
136 >
137 {chosen[q.question] === o.label ? '✓ ' : ''}
138 {o.label}
139 {swatches(o).map(c => (
140 <Text color={c}> ██</Text>
141 ))}
142 </Button>
143 )
144 const hint = <Text dimColor>Pick a mockup, or answer in the list below.</Text>
145
146 if (e.surface === 'terminal') {
147 const { Code } = $.ui.resolve(e)
148 const withPreview = questions.flatMap(q => q.options).filter(o => o.preview?.trim()).length
149 const previewRows = withPreview === 0 ? 0 : Math.floor((DIALOG_ROWS - fixed) / withPreview)
150 return (
151 <Box flexDirection="column">
152 {questions.map((q, qi) => (
153 <Box key={`q:${qi}`} flexDirection="column">
154 <Text bold wrap="truncate-end">
155 {q.header}: {q.question}
156 </Text>
157 <Box flexDirection="row">
158 {q.options.map((o, oi) => {
159 const lines = (o.preview ?? '').split('\n').filter(l => !l.startsWith('```')).slice(0, previewRows)
160 return (
161 <Box key={`card:${qi}:${oi}`} flexDirection="column" marginRight={2}>
162 {pickButton(qi, oi, q, o)}
163 {lines.length > 0 ? <Code source={lines.join('\n')} language="text" wrap="truncate-end" /> : null}
164 </Box>
165 )
166 })}
167 </Box>
168 </Box>
169 ))}
170 {hint}
171 {engine}
172 </Box>
173 )
174 }
175
176 const { Svg } = $.ui.resolve(e)
177 return (
178 <Box flexDirection="column">
179 {questions.map((q, qi) => (
180 <Box key={`q:${qi}`} flexDirection="column">
181 <Text bold wrap="truncate-end">
182 {q.header}: {q.question}
183 </Text>
184 <Box flexDirection="row" flexWrap="wrap">
185 {q.options.map((o, oi) => (
186 <Box key={`card:${qi}:${oi}`} flexDirection="column">
187 <Svg source={card(o, chosen[q.question] === o.label)} alt={altText(o)} />
188 {pickButton(qi, oi, q, o)}
189 </Box>
190 ))}
191 </Box>
192 </Box>
193 ))}
194 {hint}
195 {engine}
196 </Box>
197 )
198 })
199}
200types/index.d.ts 9 lines1/** Picks made in the gallery so far: tool_use_id -> question text -> option label. */
2export type Picks = Record<string, Record<string, string>>
3
4declare module 'claude-code' {
5 interface PluginState {
6 'design-picker': { picks: Picks }
7 }
8}
9