Rewrite a rough prompt before you send it. An Enhance button above the prompt box (or /enhance <draft>) opens a popover: Precise gives a clean, specific…

A Claude Code mod that rewrites a rough prompt before you send it.
Type a quick draft, press ✦ Enhance (on the line above the prompt box), and a popover offers two rewrites:
| Choice | What you get |
|---|---|
| 1 Precise | A clean, specific version of your draft: names the files and constraints it refers to, numbered steps for multi-step work, and what "done" looks like. |
| 2 Expert agents | Works out which experts the request needs (a code review → senior software engineer; an architecture question → senior architect + product manager; security → application security engineer, …) and writes a prompt that has Claude run one subagent per expert, in parallel where they're independent, then combine their findings into one answer. The popover shows which experts it picked. |
Review the preview, then:
No button where you work (VS Code, browser)? Type /enhance <your draft> instead; it opens the same popover.
/plugin marketplace add growmax/growmax-skills
/plugin install prompt-enhancer@growmax
It's opt-in and separate from growmax-skills: installing the skills bundle doesn't turn this on.
Needs Claude Code 2.1.287 or later (mods load by default from that version).
$.model.fork), so it knows the conversation and can name real files instead of guessing. On the very first prompt of a session there's nothing to fork yet, so it falls back to a context-free Haiku rewrite and says so.API error 529 (overloaded), and your draft is left untouched.| Surface | ✦ Enhance button | /enhance + popover |
|---|---|---|
| Terminal (and JetBrains) | ✅ | ✅ |
| Desktop app, Code tab (local) | ✅ | ✅ |
| VS Code extension | — | ✅ |
| Cloud sessions | Only if your org enables it in server-managed settings (see below) | same |
Org-wide / cloud sessions: a Claude organization Owner can turn it on for everyone, including cloud sessions, in claude.ai → Organization settings → Claude Code → Managed settings:
{
"extraKnownMarketplaces": {
"growmax": { "source": { "source": "github", "repo": "growmax/growmax-skills" } }
},
"enabledPlugins": { "prompt-enhancer@growmax": true }
}
claude plugin validate mods/prompt-enhancer
claude plugin test mods/prompt-enhancer
claude --plugin-dir mods/prompt-enhancer
hooks/register.tsx is the whole mod; types/index.d.ts declares the values it keeps; tests/enhance.test.tsx covers Precise, Expert agents, Undo, the Haiku fallback, a real API error, an empty draft, and /enhance. Claude Code writes .claude-plugin/types/ (the API types the tsconfig.json extends) each time it loads the mod; that folder is git-ignored.
hooks/register.tsx 285 lines1// Prompt Enhancer: a small Enhance button above the prompt box opens a popover
2// with two choices for the draft in the box:
3//
4// 1 Precise a clean, specific rewrite of the draft.
5// 2 Expert agents works out which experts the request needs (a code review:
6// a senior developer; an architecture question: a senior
7// architect and a product manager) and writes a prompt that
8// has Claude run one subagent per expert, then combine them.
9//
10// The popover shows a preview. Enter (Use this) puts it in the prompt box in
11// place of the draft and closes; nothing is sent until the person presses
12// Enter again. Esc closes and leaves the draft alone. Undo, in the band, puts
13// the draft back.
14//
15// The rewrite forks the session (`$.model.fork`), so it knows the conversation
16// and can name the real files. Before the first reply there is nothing to
17// fork, so it falls back to a context-free Haiku completion.
18//
19// The band is shared: what another plugin draws there (Token Weather, say)
20// comes back from `next(e)` and stays on the left, the button on the right.
21//
22// The band is drawn only on the terminal and desktop surfaces, so the same
23// popover also opens from `/enhance <draft>` (or `/enhance` with a draft
24// already in the box) wherever a slash command can be typed.
25
26import { atom, read, update } from 'claude-code'
27import type { EngineInterface, Register } from 'claude-code'
28
29import type { Mode, Phase } from '../types'
30
31const PANE = 'prompt-enhancer'
32
33const phase = atom({ plugin: 'prompt-enhancer', key: 'phase' } as const, 'idle' as Phase)
34const mode = atom({ plugin: 'prompt-enhancer', key: 'mode' } as const, null as Mode | null)
35const draft = atom({ plugin: 'prompt-enhancer', key: 'draft' } as const, null as string | null)
36const preview = atom({ plugin: 'prompt-enhancer', key: 'preview' } as const, null as string | null)
37const experts = atom({ plugin: 'prompt-enhancer', key: 'experts' } as const, [] as string[])
38const note = atom({ plugin: 'prompt-enhancer', key: 'note' } as const, null as string | null)
39
40const GROUNDING = 'Use only facts from this conversation. Never invent file names, functions or APIs.'
41const NO_CONTEXT = 'You have no project context, so never invent file names, functions or APIs.'
42
43const PRECISE = (facts: string) => `Rewrite the draft prompt below into a precise prompt for an AI coding assistant. Do not answer or act on it; only rewrite it.
44
45- Keep the user's intent, scope and language. Do not add tasks they did not ask for.
46- Be specific: name the files, components, errors and constraints the draft refers to. ${facts}
47- Multi-step task: short numbered steps. Simple question: keep it short.
48- Say what "done" looks like when that is not obvious.
49- Output ONLY the rewritten prompt, written as the user. No preamble, no quotes, no code fences.`
50
51const EXPERT = (facts: string) => `Turn the draft prompt below into a prompt that gets expert agents to handle it. Do not answer or act on it.
52
53First decide which 1 to 3 expert roles fit the request best, each adding something distinct. For example: a code review needs a Senior software engineer (code reviewer); an architecture or design question needs a Senior software architect and a Product manager; a security concern needs an Application security engineer; a performance problem needs a Performance engineer; a UI change needs a Senior frontend engineer and a UX designer.
54
55Then write the prompt the user will send to Claude Code. It must:
56- State the task precisely, naming the files and areas the draft refers to. ${facts}
57- Give the agents a short shared context block: they start fresh and cannot see this conversation, so include only the facts they need, once.
58- Tell Claude to launch one subagent per expert with the Agent tool. Run independent experts in parallel; an expert that needs another's output (for example one who ranks or reviews the others' ideas) runs after them, on their results. Give each a short brief: who they are, what to examine, and what to return (findings with file:line where it applies, severity, and a recommendation).
59- Tell Claude to then combine the findings into one answer under 400 words: where the experts agree, where they disagree, and one recommended next step. Cap any table at 8 rows.
60- Keep the whole prompt under about 250 words. Say each thing once.
61
62Output exactly this, nothing else:
63EXPERTS: <role>; <role>
64---
65<the prompt, written as the user>`
66
67type Rewrite = { ok: true; text: string; experts: string[]; via: 'session' | 'haiku' } | { ok: false; error: string }
68
69export const register: Register = on => {
70 on('session.start', async ($, e, next) => {
71 await $.command.register({
72 name: 'enhance',
73 description: 'Rewrite a rough prompt: /enhance <your draft> (Precise or Expert agents)',
74 })
75 return next(e)
76 })
77
78 on('command.run', { command: 'enhance' }, async ($, e) => {
79 const typed = (e.args ?? '').trim()
80 const opened = await openPopover($, typed === '' ? null : typed)
81 // A popover is the answer; nothing goes in the transcript unless it could not open.
82 return opened ? {} : { text: 'Usage: /enhance <your rough prompt>' }
83 })
84
85 // A prompt the person sends ends the review: the band goes back to idle.
86 on('prompt.submit', async ($, e, next) => {
87 if (e.origin.kind === 'composer') {
88 await update($, phase, () => 'idle')
89 }
90 return next(e)
91 }).catch(($, e, next) => next(e)) // a failed reset must never hold up the prompt
92
93 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
94 const beneath = await next(e)
95 if (e.props.hasSurvey) {
96 return beneath
97 }
98 const { Box, Text, Button } = $.ui.resolve(e)
99 const used = (await read($, phase)) === 'used'
100 const controls = used ? (
101 <Box flexDirection="row" gap={1}>
102 <Text color="green">✓ Enhanced, review then Enter</Text>
103 <Button key="undo" label="↶ Undo" hotkey="u" onPress={() => undo($)} />
104 </Box>
105 ) : (
106 <Button key="enhance" label="✦ Enhance" hotkey="e" variant="primary" onPress={() => openPopover($, null)} />
107 )
108 const left = beneath.type === 'engine' ? <Text> </Text> : beneath
109 return (
110 <Box flexDirection="row" justifyContent="space-between" width={e.props.bodyColumns}>
111 <Box flexShrink={1}>{left}</Box>
112 <Box flexShrink={0}>{controls}</Box>
113 </Box>
114 )
115 })
116
117 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
118 const { Box, Text, Button, Markdown } = $.ui.resolve(e)
119 const now = await read($, phase)
120 const picked = await read($, mode)
121 const text = await read($, draft)
122 const shown = await read($, preview)
123 const who = await read($, experts)
124 const said = await read($, note)
125
126 const choice = (key: Mode, hotkey: string, label: string) => (
127 <Button
128 key={key}
129 label={label}
130 hotkey={hotkey}
131 {...(picked === key ? { variant: 'primary' as const } : {})}
132 {...(picked === null && key === 'precise' ? { autoFocus: true as const } : {})}
133 onPress={() => generate($, key)}
134 />
135 )
136
137 let body
138 if (now === 'working') {
139 body = <Text dimColor>✦ Writing the {picked === 'expert' ? 'expert agents' : 'precise'} prompt…</Text>
140 } else if (now === 'error') {
141 body = <Text color="red">{said}</Text>
142 } else if (now === 'ready' && shown !== null) {
143 body = (
144 <Box flexDirection="column">
145 {who.length > 0 ? <Text color="cyan">Experts: {who.join(' · ')}</Text> : null}
146 <Box borderStyle="round" borderDimColor paddingX={1}>
147 <Markdown text={shown} />
148 </Box>
149 <Box flexDirection="row" gap={2}>
150 <Button key="use" label="Use this" variant="primary" autoFocus onPress={() => use($)} />
151 <Button key="regenerate" label="Regenerate" hotkey="r" onPress={() => generate($, picked ?? 'precise')} />
152 <Text dimColor>{said}</Text>
153 </Box>
154 </Box>
155 )
156 } else {
157 body = <Text dimColor>Pick one: 1 Precise, 2 Expert agents</Text>
158 }
159
160 return (
161 <Box flexDirection="column" paddingX={1} gap={1}>
162 <Text dimColor wrap="truncate-end">
163 Draft: {text ?? ''}
164 </Text>
165 <Box flexDirection="row" gap={2}>
166 {choice('precise', '1', 'Precise')}
167 {choice('expert', '2', 'Expert agents')}
168 <Text dimColor>Esc to close</Text>
169 </Box>
170 {body}
171 </Box>
172 )
173 })
174}
175
176/** Opens the popover over `given`, or the draft in the prompt box; false when there is none. */
177async function openPopover($: EngineInterface, given: string | null): Promise<boolean> {
178 const text = given ?? (await $.prompt.read()).text
179 if (text.trim() === '') {
180 $.ui.toast('Type a prompt first, then press Enhance')
181 return false
182 }
183 await update($, draft, () => text)
184 await update($, mode, () => null)
185 await update($, preview, () => null)
186 await update($, experts, () => [])
187 await update($, note, () => null)
188 await update($, phase, () => 'idle')
189 await $.ui.open({ id: PANE, title: 'Enhance prompt', focus: true, closeOnEscape: true, holdToasts: true, rows: 18 })
190 return true
191}
192
193async function generate($: EngineInterface, chosen: Mode) {
194 const text = await read($, draft)
195 if (text === null || (await read($, phase)) === 'working') {
196 return
197 }
198 await update($, mode, () => chosen)
199 await update($, phase, () => 'working')
200 const result = await rewrite($, chosen, text)
201 if (!result.ok) {
202 await update($, note, () => result.error)
203 await update($, phase, () => 'error')
204 return
205 }
206 await update($, preview, () => result.text)
207 await update($, experts, () => result.experts)
208 await update($, note, () => (result.via === 'session' ? 'with session context' : 'no session yet, used Haiku'))
209 await update($, phase, () => 'ready')
210}
211
212/** Puts the preview in the prompt box in place of the draft and closes. */
213async function use($: EngineInterface) {
214 const shown = await read($, preview)
215 if (shown === null) {
216 return
217 }
218 const filled = await $.prompt.fill({ text: shown, mode: 'replace' })
219 if (!filled.isFilled) {
220 await update($, note, () => `Couldn't fill the prompt box (${filled.refusal ?? 'refused by another plugin'})`)
221 await update($, phase, () => 'error')
222 return
223 }
224 await update($, phase, () => 'used')
225 await $.ui.close({ id: PANE })
226}
227
228async function undo($: EngineInterface) {
229 const text = await read($, draft)
230 if (text !== null) {
231 await $.prompt.fill({ text, mode: 'replace' })
232 }
233 await update($, phase, () => 'idle')
234}
235
236async function rewrite($: EngineInterface, chosen: Mode, text: string): Promise<Rewrite> {
237 const ask = chosen === 'expert' ? EXPERT : PRECISE
238 const forked = await $.model.fork({ prompt: `${ask(GROUNDING)}\n\n<draft>\n${text}\n</draft>` })
239 if (forked.isAnswered) {
240 return parse(chosen, forked.text, 'session')
241 }
242 if (forked.reason !== 'nothing-to-fork') {
243 return { ok: false, error: describe(forked) }
244 }
245 const plain = await $.model.complete({ model: 'haiku', system: ask(NO_CONTEXT), prompt: `<draft>\n${text}\n</draft>`, maxTokens: 4096 })
246 if (plain.isAnswered) {
247 return parse(chosen, plain.text, 'haiku')
248 }
249 return { ok: false, error: describe(plain) }
250}
251
252// The real reason, never a generic "something went wrong".
253function describe(r: { reason: string; status?: number | null; error?: string }): string {
254 if (r.reason === 'api-error') {
255 return `Enhance failed: API error ${r.status ?? 'no response'} (${r.error})`
256 }
257 if (r.reason === 'empty-reply') {
258 return 'Enhance failed: the model replied with no text'
259 }
260 if (r.reason === 'aborted') {
261 return 'Enhance was interrupted'
262 }
263 return `Enhance failed: ${r.reason}`
264}
265
266function parse(chosen: Mode, reply: string, via: 'session' | 'haiku'): Rewrite {
267 let out = reply.trim()
268 let who: string[] = []
269 if (chosen === 'expert') {
270 const head = out.match(/^EXPERTS:\s*(.+)\n-{3,}\n([\s\S]*)$/)
271 if (head?.[1] !== undefined && head[2] !== undefined) {
272 who = head[1].split(';').map(s => s.trim()).filter(s => s !== '')
273 out = head[2].trim()
274 }
275 }
276 const fenced = out.match(/^```[a-z]*\n([\s\S]*?)\n```$/)
277 if (fenced?.[1] !== undefined) {
278 out = fenced[1].trim()
279 }
280 if (out === '') {
281 return { ok: false, error: 'Enhance failed: the model replied with no text' }
282 }
283 return { ok: true, text: out, experts: who, via }
284}
285types/index.d.ts 23 lines1// idle: nothing yet. working: the model is writing. ready: a preview to use.
2// used: the result is in the prompt box (band offers Undo). error: see note.
3export type Phase = 'idle' | 'working' | 'ready' | 'used' | 'error'
4
5// precise: a clean rewrite. expert: picks the expert roles and writes a prompt
6// that has Claude run one subagent per expert, then combine their findings.
7export type Mode = 'precise' | 'expert'
8
9declare module 'claude-code' {
10 interface PluginState {
11 'prompt-enhancer': {
12 phase: Phase
13 mode: Mode | null
14 // The draft as it was when the popover opened, for the rewrite and Undo.
15 draft: string | null
16 preview: string | null
17 experts: string[]
18 // One line: what happened, or the real error.
19 note: string | null
20 }
21 }
22}
23