SLOPSHOPPER

write-for-me

Ctrl+N writes your next prompt from the conversation so far; a draft in the box is used as a hint

newbandtoastmodel
v0.1.0no licenseupdated 2026-10-01ekinertac/claude-code-mods/write-for-me
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · write-for-me
› 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 ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩
README

claude-code-mods

Claude Code mods I use daily.

  • rewrite-prompt: press Ctrl+P and the draft in the prompt box is rewritten in place by a model (Sonnet, on your plan). Nothing is sent until you press Enter, and Ctrl+_ undoes it.
  • write-for-me: press Ctrl+N and the next prompt is written for you from the last 12 messages of the conversation. If the box has text, it is used as a hint ("tests too") and expanded. Nothing is sent, and Ctrl+_ undoes it.
  • prompt-box: the prompts you type are drawn in the scrollback inside a thick cyan box instead of one > text line. Task notifications and messages from other sessions keep their normal rows. No key binding needed.
  • tool-oneline: each tool call is drawn as one line (⏺ Bash(echo hello) (ctrl+o to expand)) with no result block under it. Set detail to runs for one line per run of calls (⏺ Ran 3 tools: Bash ×2, Read). Ctrl+O still shows the full rows. A failed call keeps its error text.

Setup

claude plugin marketplace add ekinertac/claude-code-mods
claude plugin install rewrite-prompt@ekinertac-mods
claude plugin install write-for-me@ekinertac-mods
claude plugin install prompt-box@ekinertac-mods
claude plugin install tool-oneline@ekinertac-mods

Then bind the keys. Add these to the Global block of ~/.claude/keybindings.json and start a new session:

"ctrl+p": "app:toggleDiffNoiseFilter",
"ctrl+n": "app:toggleDiffPreSession"

To use the runs level, add this to ~/.claude/settings.json:

"pluginConfigs": { "tool-oneline": { "options": { "detail": "runs" } } }

How the shortcut works

No keybinding action runs a mod command. The mod draws a zero-size button above the prompt whose action is app:toggleDiffNoiseFilter (inert outside the diff panel; write-for-me uses app:toggleDiffPreSession the same way, so the two never share an action), and the chord you bind to that action presses it. Nothing shows on screen.

Limits

  • Tested on Claude Code 2.1.287. Mod events and methods can change between releases.
  • The rewrite replaces the draft. It keeps the intent, language and concrete details, and adds no requirements.
  • Not checked in every terminal: if you see a blank row above the prompt, open an issue.
  • Tests: claude plugin test <mod> (stubs the model, no network).
Source 1 files
hooks/register.js 87 lines
1// write-for-me: Ctrl+N writes the next prompt for you, from the conversation so far, into the prompt box.
2//
3// Empty box: the model proposes the most likely next message. Text in the box: it is treated as a
4// hint ("tests too", "do the second option") and expanded into a full prompt using the conversation.
5// Nothing is sent; the result lands in the box for editing, and ctrl+_ (chat:undo) restores the hint.
6//
7// Why a Button: no keybinding action runs a mod command. A Button's `action` is pressed by whatever
8// chord the person bound to that engine action, so keybindings.json maps ctrl+n to
9// app:toggleDiffPreSession (inert outside the diff panel) and this Button answers it. rewrite-prompt
10// uses app:toggleDiffNoiseFilter the same way; the two must not share an action.
11// Setup: keybindings.json (Global: "ctrl+n": "app:toggleDiffPreSession") and the plugin loaded.
12// Depends on: $.session.messages, $.prompt.read/fill, $.model.complete (the session's plan), $.ui.
13
14const SYSTEM =
15  'You write the next message a developer will send to their coding agent, in their voice, ' +
16  'from the conversation so far. Pick the single most useful next step the conversation points to ' +
17  '(a follow-up, a fix for what just failed, the next item of a plan). Be specific: reuse the ' +
18  'paths, names and numbers already in the conversation, in the language the developer writes in. ' +
19  'If a hint is given, it is what the developer wants; expand it into a full prompt and add nothing ' +
20  'it does not imply. Never invent requirements. Reply with the message text only, no quotes, no preamble.'
21
22const MESSAGES = 12 // recent turns are enough to know the next step; older ones only cost tokens
23const PER_MESSAGE = 1500 // chars; keeps one huge tool dump from crowding out the rest
24
25// One write at a time, so a second press can't fill the box with a stale answer.
26// Declared at the top level because `claude plugin validate` only follows `$` into such functions.
27let busy = false
28
29function transcript(messages) {
30  return messages
31    .slice(-MESSAGES)
32    .map(m => `${m.role === 'user' ? 'Developer' : 'Agent'}: ${(m.text || '').slice(0, PER_MESSAGE)}`)
33    .join('\n\n')
34}
35
36async function write($) {
37  if (busy) return
38  const [{ text: hint }, messages] = await Promise.all([$.prompt.read(), $.session.messages()])
39  if (!messages.length) {
40    $.ui.toast('write-for-me: nothing to go on yet, start the conversation first')
41    return
42  }
43  busy = true
44  $.ui.toast('writing…')
45  try {
46    const r = await $.model.complete({
47      model: 'sonnet',
48      system: SYSTEM,
49      prompt: transcript(messages) + (hint.trim() ? `\n\nHint from the developer: ${hint.trim()}` : ''),
50      maxTokens: 1024,
51      timeoutMs: 60000,
52    })
53    if (!r.isAnswered) {
54      $.ui.toast('write-for-me failed: ' + r.reason)
55      return
56    }
57    await $.prompt.fill({ text: r.text.trim(), mode: 'replace' })
58  } finally {
59    busy = false
60  }
61}
62
63export function register(on) {
64  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
65    const { Box, Button } = $.ui.resolve(e)
66    // Keep what other mods draw in the band
67    const rest = await next(e)
68    // Zero-size and out of the layout: the Button must be mounted for its chord to fire, but it
69    // should not take a row or show a label
70    const mine = Box({
71      position: 'absolute',
72      width: 0,
73      height: 0,
74      children: [
75        Button({
76          key: 'write',
77          label: ' ',
78          plain: true,
79          action: 'app:toggleDiffPreSession',
80          onPress: () => write($),
81        }),
82      ],
83    })
84    return Box({ flexDirection: 'column', children: rest ? [rest, mine] : [mine] })
85  })
86}
87