SLOPSHOPPER

Next prompts

Clickable follow-up prompts above the input: click one to put it in the prompt box, unsent

newbandprompttimer
★ 1v0.1.0MITupdated 2026-10-21benjaminb10/encore-mods/plugins/next-prompts
A shopper browsing a rack in a slop shop
README

Next prompts

A Claude Code mod from Encore Mods. Claude often ends a reply with a few follow-ups ("Next steps: 1. … 2. …") or a question with choices. This mod turns them into buttons above the prompt.

  • Click a follow-up (or type its digit alone in the empty prompt and pause): it lands in the prompt box, unsent, so you can edit it first.
  • The first one shows as dim ghost text in the empty prompt box. Press Tab to take it.
  • Preview with the keyboard: ctrl+x tab to focus the buttons, then up/down. Each one shows as ghost text as you move.
  • Full text, wrapped on as many lines as needed. Nothing is cut.
  • Recognises "Next steps", "Follow-ups", "What's next" and their French, Spanish, German and Italian versions, plus a list of choices that ends a reply right after a question.

Install

/plugin marketplace add benjaminb10/encore-mods
/plugin install next-prompts@encore-mods

Then /reload-plugins or restart Claude Code. Needs Claude Code 2.1.287 or later.

Options

heading: one more heading to recognise, for example Ideas. Set it in /plugin, or with /plugin configure next-prompts@encore-mods.

Tip: ask Claude for follow-ups in your CLAUDE.md, for example: End each reply where work remains with Next steps: and 3 numbered, one-line follow-ups.

Try it without installing

git clone https://github.com/benjaminb10/encore-mods
claude --plugin-dir encore-mods/plugins/next-prompts
Source 1 files
hooks/register.js 136 lines
1// next-prompts: the follow-ups Claude suggests at the end of a reply become buttons above the prompt.
2// Click one (or type its digit alone in the empty prompt and pause) to put it in the prompt box, unsent.
3// The first one also shows as dim ghost text in the prompt box (Tab takes it), and moving through the
4// buttons with the keyboard (ctrl+x tab, then up/down) previews each one the same way.
5
6const MAX = 6
7
8// Headings that introduce suggested follow-ups, in a few languages. Add your own with the `heading` option.
9const HEADINGS = [
10  'next steps?', 'suggested next steps?', 'possible next steps?', "what's next", 'follow[- ]ups?',
11  'suites possibles', 'prochaines étapes', 'próximos pasos', 'nächste schritte', 'prossimi passi',
12]
13
14let suggestions = []
15let pressed = -1 // the follow-up just chosen, highlighted for a moment
16let headingRe = buildHeading('')
17
18function buildHeading(extra) {
19  const parts = [...HEADINGS]
20  if (extra && extra.trim()) parts.push(extra.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
21  // The heading alone on its line ("**Next steps:**"), not quoted in the middle of a sentence
22  return new RegExp('^[\\s#>*_]*(?:' + parts.join('|') + ')[\\s*_]*:?[\\s*_]*$', 'i')
23}
24
25function clean(s) {
26  return s
27    .replace(/\*\*|__|`/g, '')
28    .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
29    .replace(/\s+/g, ' ')
30    .trim()
31}
32
33// A top-level list item (not an indented sub-item)
34const SOURCES = /^[\s#>*_]*(sources|références|references|refs)[\s*_]*:?[\s*_]*$/i
35const ITEM = /^ {0,1}(?:\d+[.)]|[-*•])\s+(.+?)\s*$/
36
37// The follow-ups: the list that ends the reply, right after a "Next steps" heading or a question
38// ("Which one? - A - B"). Nothing may come after it, so a list in the middle of a reply never counts.
39export function parseSuggestions(answer, heading = headingRe) {
40  if (!answer) return []
41  let lines = answer.replace(/\r/g, '').split('\n')
42  // A trailing "Sources:" block (links after a web search) doesn't hide the follow-ups above it
43  for (let k = lines.length - 1; k >= 0; k--) {
44    if (SOURCES.test(lines[k])) {
45      const rest = lines.slice(k + 1).filter((l) => l.trim())
46      if (rest.length && rest.every((l) => /^\s*(?:[-*•]|\d+[.)])?\s*\[?[^\n]*\]?\(?https?:\/\//.test(l) || /https?:\/\//.test(l))) lines = lines.slice(0, k)
47      break
48    }
49  }
50  let i = lines.length - 1
51  const tail = []
52  while (i >= 0 && (ITEM.test(lines[i]) || !lines[i].trim())) {
53    if (lines[i].trim()) tail.unshift(lines[i].match(ITEM)[1])
54    i--
55  }
56  if (!tail.length || i < 0) return []
57  const intro = lines[i]
58  const ok = heading.test(intro) || (tail.length >= 2 && /\?\s*\**\s*$/.test(intro))
59  return ok ? tail.map(clean).filter(Boolean).slice(0, MAX) : []
60}
61
62// The text to insert at the cursor: a space first when the character before the cursor isn't one.
63async function joinAtCursor($, s) {
64  try {
65    const box = await $.prompt.read()
66    const before = (box.text || '').slice(0, box.cursor ?? (box.text || '').length)
67    return before && !/\s$/.test(before) ? ' ' + s : s
68  } catch {
69    return s
70  }
71}
72
73export function register(on, options = {}) {
74  headingRe = buildHeading(options.heading || '')
75
76  // After the mod reloads (or a resumed session), pick up the follow-ups of the last reply again
77  on('session.start', async ($, e, next) => {
78    try {
79      const last = (await $.session.messages()).filter((m) => m.role === 'assistant').at(-1)
80      if (last && last.text) suggestions = parseSuggestions(last.text)
81    } catch {}
82    return next(e)
83  })
84
85  on('prompt.submit', async ($, e, next) => {
86    suggestions = []
87    $.ui.invalidate('ui.render')
88    return next(e)
89  }).catch(() => undefined)
90
91  on('turn.complete', async ($, e, next) => {
92    if (e.agentId) return next(e)
93    suggestions = e.isAborted ? [] : parseSuggestions(e.answer)
94    if (suggestions.length) $.prompt.suggest({ text: suggestions[0] }).catch(() => {})
95    $.ui.invalidate('ui.render')
96    return next(e)
97  })
98
99  on('ui.focus', { component: 'AbovePrompt' }, async ($, e, next) => {
100    const m = /^next-prompt-(\d+)$/.exec(e.element || '')
101    if (m && suggestions[Number(m[1])]) $.prompt.suggest({ text: suggestions[Number(m[1])] }).catch(() => {})
102    return next(e)
103  })
104
105  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
106    if (e.props.isWorking || !suggestions.length) return next(e)
107    const { Box, Text, Button } = $.ui.resolve(e)
108    const rows = [Text({ dimColor: true, children: ['Next (click, or type the digit):'] })]
109    suggestions.forEach((s, i) => {
110      // Just chosen: shown inverted for a moment, so you see which one went to the prompt box
111      if (i === pressed) {
112        rows.push(Text({ key: 'chosen-' + i, inverse: true, children: [(i + 1) + ': ' + s] }))
113        return
114      }
115      rows.push(
116        Button({
117          key: 'next-prompt-' + i,
118          label: s,
119          hotkey: String(i + 1),
120          plain: true,
121          onPress: async () => {
122            pressed = i
123            $.ui.invalidate('ui.render')
124            $.clock.after(1200, () => { pressed = -1; $.ui.invalidate('ui.render') })
125            // Insert at the cursor instead of replacing the draft, with a space if needed, so
126            // several follow-ups (and your own words between them) add up in the prompt box.
127            await $.prompt.fill({ text: await joinAtCursor($, s), mode: 'insert' })
128          },
129        }),
130      )
131    })
132    const theirs = await next(e)
133    return Box({ flexDirection: 'column', children: [...rows, theirs] })
134  })
135}
136