SLOPSHOPPER

optimize-prompt

Optimize prompt button above the prompt box: Haiku rewrites your draft in place

newbandtoastmodeltimer
v0.1.0no licenseupdated 2026-10-15kelokchan/optimize-prompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · optimize-prompt
› 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 [ ✦ Optimize prompt ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
[ ✦ Optimize prompt ]
README

optimize-prompt

A Claude Code mod: an Optimize prompt button above the prompt box. Haiku rewrites your draft in place.

Install

At the prompt of a Claude Code terminal session:

/plugin install optimize-prompt --marketplace kelokchan/optimize-prompt

Answer y to add the marketplace, then pick a scope (user scope is first; press Enter).

From a shell:

claude plugin install optimize-prompt --marketplace kelokchan/optimize-prompt

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with kelokchan/optimize-prompt, then Add plugin.

Source 2 files
hooks/register.tsx 116 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, ToolUseSummary } from 'claude-code'
3
4const isBusy = atom({ plugin: 'optimize-prompt', key: 'isBusy' } as const, false)
5const frame = atom({ plugin: 'optimize-prompt', key: 'frame' } as const, 0)
6
7// Braille dots: one column wide in every terminal font. No Spinner element on
8// the terminal, so a $.clock.every tick advances `frame` and the band redraws.
9const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
10const SPINNER_MS = 100
11const spinnerGlyph = (n: number) => SPINNER[n % SPINNER.length] ?? ''
12
13// Haiku: cheapest + fastest model; rewriting a prompt needs no deep reasoning.
14const MODEL = 'haiku'
15
16const SYSTEM = `You rewrite a draft prompt the user is about to send to an AI coding assistant, in the middle of an ongoing session.
17<conversation> holds the latest messages of that session and the assistant's tool calls between them (lines starting "tool", showing files read or edited, commands run and their results). Use it only to understand what the draft refers to: resolve vague references ("it", "the button", "that error") to the concrete thing, file or feature being discussed. Name a file or command from it only where the draft refers to one.
18Rules:
19- Keep the draft's scope. Never add requirements, deliverables, sections or questions the author did not ask for.
20- Never comment on what the draft leaves unspecified. If the conversation does not resolve something, leave it as written.
21- Stay proportionate: a one-line draft becomes one to three sentences, not a spec with headings.
22- Keep the author's voice, language and every concrete detail (file paths, names, code, numbers).
23- Do not answer or carry out the prompt.
24Reply with the rewritten prompt only, no preamble or quotes.`
25
26// Recent turns ground the rewrite: without them Haiku sees a bare draft and
27// generalises "the spinner" into "the application's UI".
28// Tool calls show which files and commands "it" means. The window is counted in
29// text messages so a tool-heavy turn can't push the person's own words out.
30// ponytail: plain char cuts, not token counts; CONTEXT_TOTAL is the ceiling.
31const CONTEXT_MESSAGES = 8 // text messages; tool calls between them ride along
32const CONTEXT_CHARS = 1200 // per message text, its tail kept
33const TOOL_CHARS = 200 // per tool input and per tool result, the head kept
34const CONTEXT_TOTAL = 12000 // whole block, the newest kept
35
36const head = (s: string, n: number) => (s.length > n ? `${s.slice(0, n)}…` : s)
37const tail = (s: string, n: number) => (s.length > n ? `…${s.slice(-n)}` : s)
38
39const toolLine = (u: ToolUseSummary) =>
40  `tool ${u.tool}(${head(JSON.stringify(u.input), TOOL_CHARS)})` +
41  (u.text === undefined ? '' : ` → ${u.isError ? 'error: ' : ''}${head(u.text, TOOL_CHARS)}`)
42
43const recentContext = async ($: EngineInterface) => {
44  const messages = (await $.session.messages()).filter(m => m.text.trim() || m.toolUses.length)
45  const textAt = messages.flatMap((m, i) => (m.text.trim() ? [i] : []))
46  const rendered = messages
47    .slice(textAt.at(-CONTEXT_MESSAGES) ?? 0)
48    .map(m =>
49      [...(m.text.trim() ? [`${m.role}: ${tail(m.text, CONTEXT_CHARS)}`] : []), ...m.toolUses.map(toolLine)].join('\n'),
50    )
51    .join('\n\n')
52  return tail(rendered, CONTEXT_TOTAL)
53}
54
55export const register: Register = on => {
56  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
57    if (e.props.hasSurvey) {
58      return next(e)
59    }
60
61    const busy = await read($, isBusy)
62    const { Box, Button, Text } = $.ui.resolve(e)
63
64    const optimize = async () => {
65      if (await read($, isBusy)) return
66
67      const draft = (await $.prompt.read()).text
68      if (!draft.trim()) {
69        $.ui.toast('Type a prompt first')
70        return
71      }
72
73      await update($, isBusy, () => true)
74      const tick = $.clock.every(SPINNER_MS, () => void update($, frame, n => n + 1))
75      try {
76        const context = await recentContext($)
77        const r = await $.model.complete({
78          model: MODEL,
79          system: SYSTEM,
80          prompt: `${context && `<conversation>\n${context}\n</conversation>\n\n`}<draft>\n${draft}\n</draft>`,
81          effort: 'low',
82          maxTokens: 2048,
83          timeoutMs: 30000,
84        })
85        if (!r.isAnswered) {
86          $.ui.toast(`Optimize failed: ${r.reason}`)
87          return
88        }
89        // Don't clobber anything typed while the model was working.
90        if ((await $.prompt.read()).text !== draft) {
91          $.ui.toast('Draft changed while optimizing; left it as is')
92          return
93        }
94        await $.prompt.fill({ text: r.text.trim(), mode: 'replace' })
95      } finally {
96        tick.cancel()
97        await update($, isBusy, () => false)
98      }
99    }
100
101    return (
102      <Box>
103        {busy ? (
104          <Text color="suggestion">
105            {spinnerGlyph(await read($, frame))} Optimizing prompt
106          </Text>
107        ) : (
108          <Button key="optimize" variant="primary" onPress={optimize}>
109            ✦ Optimize prompt
110          </Button>
111        )}
112      </Box>
113    )
114  })
115}
116
types/index.d.ts 8 lines
1export type Busy = boolean
2
3declare module 'claude-code' {
4  interface PluginState {
5    'optimize-prompt': { isBusy: Busy; frame: number }
6  }
7}
8