SLOPSHOPPER

prompt-enhancer

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…

newpanebandcommandtoastprompt
★ 1v?no licenseupdated 2026-10-06growmax/growmax-skills/mods/prompt-enhancer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-enhancer
│ ┃ prompt-enhancer ✕ › fix the failing auth te╭─────────────────────────────────────────╮ │ ┃ Draft: │ prompt-enhancer │ │ ┃ ⏺ Read(src/auth.ts) │ Type a prompt first, then press Enhance │ │ ┃ [ Precise ] [ Expert agents ] Esc to clos ⎿ Read 6 lines ╰─────────────────────────────────────────╯ │ ┃ ⏺ Update(src/auth.ts) │ ┃ Pick one: 1 Precise, 2 Expert agents ⎿ 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 │ │ › /enhance │ ⎿ prompt-enhancer: Usage: /enhance <your rough prompt> │ │ [ ✦ Enhance ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
[ ✦ Enhance ]
Pane · prompt-enhancer
Draft: [ Precise ] [ Expert agents ] Esc to close Pick one: 1 Precise, 2 Expert agents
README

prompt-enhancer

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:

ChoiceWhat you get
1 PreciseA 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 agentsWorks 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:

  • Enter (Use this) puts it in your prompt box in place of the draft. Nothing is sent until you press Enter again.
  • r regenerates. Esc closes and leaves your draft alone.
  • ↶ Undo (in the band, after using it) brings your original draft back.

No button where you work (VS Code, browser)? Type /enhance <your draft> instead; it opens the same popover.

Install

/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).

How it works

  • The rewrite forks your current session ($.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.
  • Cost: one extra model call each time you press Precise, Expert agents or Regenerate, on your own Claude usage. Nothing runs unless you press a button.
  • Errors are shown as they are, for example API error 529 (overloaded), and your draft is left untouched.
  • Sharing the band: only one mod can draw the line above the prompt. Prompt Enhancer draws whatever the mod beneath it drew (for example Token Weather) on the left and its button on the right.

Where it works

Surface✦ Enhance button/enhance + popover
Terminal (and JetBrains)✅✅
Desktop app, Code tab (local)✅✅
VS Code extension—✅
Cloud sessionsOnly 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 }
}

Develop

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.

Source 2 files
hooks/register.tsx 285 lines
1// 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}
285
types/index.d.ts 23 lines
1// 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