SLOPSHOPPER

inline-next-steps

The main model lists next steps at the end of its final answer; they show as checkboxes above the prompt, with no extra model calls

newbandrowstoastprompt
A shopper browsing a rack in a slop shop
README

claude-mods

Mods for Claude Code: plugins that run inside Claude Code and draw in its interface.

License: MIT Claude Code mod Works in

English · 中文说明

inline-next-steps

Claude ends each answer with a few things you might ask next. They show up as checkboxes above the prompt. Tick one or more, and they go into the prompt box for you to edit and send.

<img src="docs/demo.svg" alt="The end of Claude's answer, then four next-step checkboxes above the prompt with one ticked, and the prompt box holding the ticked request" width="820"> <sub>Illustration of the Claude Code desktop app with the mod on.</sub>

  • No extra model calls. The model that is already answering writes the suggestions, with the whole conversation in view.
  • Nothing is sent for you. Ticked steps are put in the prompt box; you edit and press Enter.
  • Work or questions. 2 to 6 concrete next steps after a task; 1 to 3 follow-up questions after a plain question.
  • Stays out of the way. The raw block is hidden from the transcript, and nothing is attached where the mod can't draw buttons.

Install

Requires Claude Code with mods support (v2.1.286 or later).

claude plugin marketplace add shijieli708-ship-it/claude-mods
claude plugin install inline-next-steps@claude-mods

Then start a new session, or run /reload-plugins in an open one.

How it works

<img src="docs/how-it-works.svg" alt="Four steps: attach an instruction, Claude answers with a next-steps block, the block is shown as checkboxes, ticked steps fill the prompt box" width="820">

  1. Attach: each prompt you send carries a short instruction you don't see, asking Claude to end its final answer with a <next-steps> block. Each line is short label | the full request.
  2. Answer: Claude writes the block at the end of its final answer, never in progress notes between tool calls or in a subagent.
  3. Show: the block is removed from how the reply is drawn and becomes checkboxes in the band above the prompt. The stored conversation is left untouched, so the model's thinking blocks stay valid.
  4. Fill: the ticked requests go into the prompt box, numbered if there are several.

Cost

Each turn costs roughly 100 extra output tokens for the block and about 250 input tokens for the instruction, which is cached afterwards. Mods that ask a second model for suggestions after each turn pay for re-reading the conversation, and see only part of it.

Limits

  • The model may now and then leave the block out or write it in a different shape; that turn simply shows no buttons.
  • Instructions are attached only where the mod can draw: the terminal and the desktop app. Elsewhere (the VS Code chat panel, claude -p) nothing is attached, so no raw block appears.
  • While a reply streams in, the block is hidden as soon as its opening tag arrives.

Writing your own

Each mod is a plugin directory under plugins/ with .claude-plugin/plugin.json, hooks/hooks.json and a hooks module. See the mods documentation. Check a mod before loading it with:

claude plugin validate plugins/<mod>

License

MIT


中文说明

inline-next-steps

Claude 在每次回答的末尾列出几条你接下来可能会问的事情,以复选框的形式显示在输入框上方。勾选一条或几条后,它们会被填进输入框,你可以先修改,确认后再发送。

  • 不额外调用模型:建议由正在回答的模型顺便写出,它能看到完整的对话。
  • 不会替你发送:勾选的内容只是填进输入框,由你修改后按回车发送。
  • 做事和问答都适用:完成任务后给出 2–6 个具体的下一步;回答普通问题后给出 1–3 个延伸问题。
  • 不打扰:原始的建议块在对话里是隐藏的;在无法显示按钮的地方,也不会附加说明。

安装

claude plugin marketplace add shijieli708-ship-it/claude-mods
claude plugin install inline-next-steps@claude-mods

然后新开一个会话,或者在已打开的会话里运行 /reload-plugins。

原理

  1. 附加说明:你每发一条消息,mod 会附上一段你看不到的简短说明,让 Claude 在最终回答末尾写一个 <next-steps> 块,每行格式是 短标签 | 完整指令。
  2. 回答:Claude 只在最终回答里写这个块,工具调用之间的进度说明和子 agent 里都不会写。
  3. 显示:这个块只从界面显示上去掉,变成输入框上方的复选框。存储的对话记录不动,所以模型的思考块依然有效。
  4. 填入:勾选的内容会填进输入框,多条会自动编号。

成本:每轮大约多 100 个输出 token,外加约 250 个输入 token 的说明,这部分之后会被缓存。如果改用第二个模型在每轮结束后生成建议,就得把对话重新读一遍,而且只能看到其中一部分。

局限:模型偶尔会漏写这个块或写错格式,这一轮就不会出现按钮。在 VS Code 聊天面板和 claude -p 里不会附加说明,所以也不会出现块的原文。回答逐字输出时,块的开头标签一出现就会被隐藏。

Source 2 files
hooks/register.tsx 142 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Step } from '../types'
5
6// The steps the last answer listed, and which of them are ticked, by index.
7const steps = atom({ plugin: 'inline-next-steps', key: 'steps' } as const, [] as Step[])
8const selected = atom({ plugin: 'inline-next-steps', key: 'selected' } as const, [] as number[])
9
10const MAX = 6
11const OPEN = '<next-steps>'
12const CLOSE = '</next-steps>'
13
14// Fixed text, so every prompt carries the same bytes and the cache keeps them.
15const INSTRUCTIONS = `# Next steps
16
17When you finish a turn with your final answer to the user in the main conversation, end it with a block listing what the user might reasonably ask you to do next:
18
19${OPEN}
20- short label | the full request, as the user would type it
21${CLOSE}
22
23- For work (code, files, tasks): 2 to ${MAX} items, distinct and specific to this conversation; the most useful first. The request names the concrete files, commands or values involved.
24- For a plain question (an explanation, a fact, a concept): 1 to 3 follow-up questions that go deeper or sideways from your answer, each phrased as the question the user would ask.
25- Label: at most 5 words. Request: one or two sentences.
26- Write both in the language the user writes in.
27- Only in a final answer. Never in progress notes between tool calls, never as a subagent, and leave it out when nothing useful follows (a greeting, a thank-you, a yes/no confirmation).
28- The user does not see this block as text: it is turned into buttons. Do not refer to it.`
29
30// Removes the block from a text, including a block still streaming in (no closing tag yet).
31function strip(text: string): string {
32  const start = text.indexOf(OPEN)
33  if (start === -1) return text
34  const end = text.indexOf(CLOSE, start)
35  const rest = end === -1 ? '' : text.slice(end + CLOSE.length)
36  return (text.slice(0, start) + rest).replace(/\s+$/, '')
37}
38
39function parse(answer: string): Step[] {
40  const start = answer.lastIndexOf(OPEN)
41  if (start === -1) return []
42  const end = answer.indexOf(CLOSE, start)
43  if (end === -1) return []
44  return answer
45    .slice(start + OPEN.length, end)
46    .split('\n')
47    .map(line => line.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, '').trim())
48    .filter(line => line !== '')
49    .map(line => {
50      const bar = line.indexOf('|')
51      const label = (bar === -1 ? line : line.slice(0, bar)).trim()
52      const prompt = (bar === -1 ? line : line.slice(bar + 1)).trim() || label
53      return { label, prompt }
54    })
55    .filter(s => s.label !== '')
56    .slice(0, MAX)
57}
58
59export const register: Register = on => {
60  // A new request makes the old steps stale. The instructions ride the prompt as context the
61  // user never sees: the desktop app's main system prompt does not pass through prompt.compose.
62  on('prompt.submit', async ($, e, next) => {
63    if (e.turnId) return next(e)
64    void Promise.all([update($, steps, () => []), update($, selected, () => [])])
65    // Only where the mod can draw the buttons; elsewhere the block would show as raw text.
66    const surfaces = await $.session.surfaces()
67    const canDraw = surfaces.includes('terminal') || surfaces.includes('desktop')
68    if (!canDraw) return next(e)
69    return next({ ...e, context: [...(e.context ?? []), INSTRUCTIONS] })
70  })
71
72  on('turn.complete', async ($, e, next) => {
73    const result = await next(e)
74    if (!e.agentId) {
75      const found = e.reason === 'answer' ? parse(e.answer) : []
76      await Promise.all([update($, steps, () => found), update($, selected, () => [])])
77    }
78    return result
79  })
80
81  // Hide the block in the transcript. This changes the drawing only; the stored message is untouched.
82  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
83    const text = strip(e.props.text)
84    if (text === e.props.text) return next(e)
85    return next({ ...e, props: { ...e.props, text } })
86  })
87
88  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
89    if (e.props.hasSurvey || e.props.isWorking) return next(e)
90    const [list, picked] = await Promise.all([read($, steps), read($, selected)])
91    if (list.length === 0) return next(e)
92
93    const inner = await next(e)
94    const { Box, Text, Button } = $.ui.resolve(e)
95
96    const toggle = (i: number) =>
97      update($, selected, l => (l.includes(i) ? l.filter(x => x !== i) : [...l, i].sort((a, b) => a - b)))
98
99    // Puts the ticked requests in the prompt box for the user to read and send.
100    const fill = async () => {
101      const chosen = picked.map(i => list[i]).filter(Boolean)
102      if (chosen.length === 0) return
103      const text = chosen.length === 1 ? chosen[0].prompt : chosen.map((s, i) => `${i + 1}. ${s.prompt}`).join('\n')
104      const filled = await $.prompt.fill({ text })
105      if (!filled.isFilled) {
106        $.ui.toast('The prompt box is busy; try again')
107        return
108      }
109      await update($, selected, () => [])
110    }
111
112    const item = (s: Step, i: number) => (
113      <Button
114        key={`step-${i}`}
115        label={`${picked.includes(i) ? '☑' : '☐'} ${s.label}`}
116        plain
117        dimColor={!picked.includes(i)}
118        onPress={() => toggle(i)}
119      />
120    )
121    const half = Math.ceil(list.length / 2)
122
123    return (
124      <Box flexDirection="column">
125        <Box flexDirection="row" columnGap={3}>
126          <Box flexDirection="column">{list.slice(0, half).map((s, i) => item(s, i))}</Box>
127          <Box flexDirection="column">{list.slice(half).map((s, i) => item(s, i + half))}</Box>
128        </Box>
129        <Box flexDirection="row" columnGap={2}>
130          {picked.length > 0 ? (
131            <Button key="fill" label={picked.length === 1 ? 'Put in prompt' : `Put ${picked.length} in prompt`} variant="primary" onPress={fill} />
132          ) : (
133            <Text dimColor>Tick next steps, then put them in the prompt</Text>
134          )}
135          <Button key="dismiss" label="Dismiss" plain dimColor onPress={() => update($, steps, () => [])} />
136        </Box>
137        {inner}
138      </Box>
139    )
140  })
141}
142
types/index.d.ts 8 lines
1export type Step = { label: string; prompt: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'inline-next-steps': { steps: Step[]; selected: number[] }
6  }
7}
8