SLOPSHOPPER

next-steps

After each turn, 2 or 3 likely next prompts above the prompt. Press 1, 2 or 3 in an empty prompt to draft one, 0 to dismiss.

newbandpromptmodel
★ 167v0.1.1MITupdated 2026-10-03hamzafer/claude-code-mods/mods/next-steps
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · next-steps
› 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 next: 1 OK · 0 dismiss ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
next: 1 OK · 0 dismiss ⟨Claude Code's own drawing⟩
README

<h1 align="center">Claude Code mods</h1>

<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT"></a> <a href="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml"><img src="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>

<a href="#-install">Install</a> · <a href="#-the-mods">All mods</a> · <a href="docs/mods.md">Docs</a> · <a href="https://claude.dev/blog/getting-started-with-claude-code-mods/">What are mods?</a>

<table> <tr> <td align="center" width="33%"><a href="docs/mods.md#-context-bar"><img src="images/context-bar.png" alt="context-bar: Claude Code context window usage as a stacked bar, a color per category" width="260"></a><br>📊 <b>context-bar</b><br>what fills your context</td> <td align="center" width="33%"><a href="docs/mods.md#-review-watch"><img src="images/review-watch.png" alt="review-watch: live lines for running Codex and subagent code reviews in Claude Code" width="260"></a><br>🔍 <b>review-watch</b><br>running code reviews, live</td> <td align="center" width="33%"><a href="docs/mods.md#-md-preview"><img src="images/md-preview.png" alt="md-preview: Markdown that Claude Code edits, rendered like GitHub next to the diff" width="260"></a><br>📝 <b>md-preview</b><br>Markdown rendered like GitHub</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-blast-radius"><img src="images/gallery/blast-radius.png" alt="blast-radius: a Claude Code hook holds rm -rf and lists the files it would delete" width="260"></a><br>💥 <b>blast-radius</b><br>see what <code>rm -rf</code> would delete</td> <td align="center" width="33%"><a href="docs/mods.md#-now-playing"><img src="images/now-playing.png" alt="now-playing: Spotify track, progress bar and synced lyrics inside Claude Code" width="260"></a><br>🎵 <b>now-playing</b><br>Spotify and its lyrics, live</td> <td align="center" width="33%"><a href="docs/mods.md#-reels-and-snake"><img src="images/reels-demo.gif" alt="reels: YouTube Shorts in a Claude Code pane while it works" width="260"></a><br>📱 <b>reels</b><br>Shorts while Claude works</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-where-am-i"><img src="images/gallery/where-am-i.png" alt="where-am-i: the session goal, current step and what waits on you, above the Claude Code prompt" width="260"></a><br>📍 <b>where-am-i</b><br>goal, now, waiting on you</td> <td align="center" width="33%"><a href="docs/mods.md#-lines-above-the-prompt"><img src="images/gallery/lines.png" alt="token-weather, usage-meter and other Claude Code status lines stacked above the prompt" width="260"></a><br>🌦️ <b>token-weather and friends</b><br>lines above the prompt</td> <td align="center" width="33%"><a href="#mission-control"><img src="images/gallery/mission-control.png" alt="mission-control: Claude Code subagents, tool calls and the files they touch, live" width="260"></a><br>🛰️ <b>mission-control</b><br>agents and the code they touch</td> </tr> </table>

🚀 Install

Add the marketplace once, then install any mod by name:

claude plugin marketplace add hamzafer/claude-code-mods
claude plugin install context-bar@claude-code-mods

Or install the general-purpose set in one go:

for m in context-bar token-weather usage-meter where-am-i next-steps agent-radar review-watch replay-theater md-preview blast-radius mission-control; do
  claude plugin install "$m@claude-code-mods"
done

Restart Claude Code after installing. To try one without installing:

git clone https://github.com/hamzafer/claude-code-mods && cd claude-code-mods
claude --plugin-dir mods/context-bar

Needs Claude Code 2.1.287+. A few mods need more (Chrome, gh, a connector); the tables say which.

🧩 The mods

👀 See what's happening

ModWhat it doesCommand
🛰️mission-controlLive map of agents, tool calls and the code they touch/mission
📊context-barYour context window as one stacked bar, a color per category, with token counts and where it compacts/context-bar
🌦️token-weatherContext fill from Clear to Compact soon, plus a prompt-cache countdown
⏱️cache-clockA prompt-cache line under your status line, from Claude Code's own figures: time left, hit rate and misses, and the tokens your next message re-caches once it goes cold. Needs Node and Claude Code 2.1.251+/cache-clock setup
📍where-am-iGoal, doing now, waiting on you, next step/where
➡️next-steps2 or 3 likely next prompts after each turn, one key to draft one1 2 3, 0 hides
💰usage-meter5-hour and 7-day plan usage, the reset countdown and the session's cost
💳openai-balanceYour OpenAI API credit: an estimated balance with a gauge, today's spend, where the money mostly went, and the last call. Needs an OpenAI organization Admin key/openai-balance
📡agent-radarOne live line per running subagent/radar
🔍review-watchOne live line per running code review (Codex or a review subagent) with the model, target, elapsed time and Codex's latest output. A toast lists the findings when it ends
🌐browser-lanesWhether this session has a browser, and who holds it/browser
🕌prayer-timesThe current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr/prayers
🎬replay-theaterSteps through the last turn's edits, one diff at a time/replay
📝md-previewRenders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images/md

🛡️ Guard your repo

ModWhat it doesCommand
💥blast-radiusHolds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer

💸 Spend less

ModWhat it doesCommand
🔀switchboardPicks the model for each subagent that doesn't name one, with OpenAI's Decisions API or Jev, from its short label only. Shows what every subagent cost/route

🔧 My setup (fork and adapt)

These are built around my own tools and rules. Fork them and change the rules to yours.

ModWhat it doesCommand
👀glanceOne line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors/glance
🚦merge-gateHolds gh pr merge until CI is green and Codex reviewed once. Needs gh and the Codex CLI. Reviews run on one fixed model; change it to yours/gate
📏rulebook-guardEnforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes
💾session-saverSaves where you left off, shows it on resume. Needs unpause/park [note]

🎮 For fun (opt-in)

ModWhat it doesCommand
📱reelsYouTube Shorts while Claude works, pauses when it's done/reels
🐍snakeSnake while Claude works/snake
🎵now-playingWhat Spotify is playing, with a progress bar, the lyric being sung, and ⏮ ⏸ ⏭ buttons. Needs macOS and the Spotify app/music

<a id="mission-control"></a>

🛰️ Flagship: mission-control

Every subagent, every tool call and every file they touch, in a pane next to the chat. Shown at 4x: two subagents building a logout feature across four files.

mission-control at 4x: two subagents and a logout feature landing across four files

Install it like any mod, restart, and type /mission (or /mission code to open the code map). q closes it.

  • 🤖 w shows the agents and every tool call, live
  • 🗺️ c shows the code map, with import arrows
  • 🔵 Blue while the agent reads a file
  • 🟠 Orange while it writes
  • 🟢 Green when done, with one line on what changed

The Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.

📚 More

Source 2 files
hooks/register.tsx 179 lines
1// Next Steps: after each turn, 2 or 3 likely next prompts above the input.
2// In an empty prompt, 1 to 3 drafts one (nothing sends on its own) and 0 dismisses.
3import { atom, read, update } from 'claude-code'
4import type { EngineInterface, Register } from 'claude-code'
5
6import type { NextSteps } from '../types'
7
8const MODEL = 'haiku'
9const MAX_ITEMS = 3
10const MAX_CHARS = 60 // what the model is asked for
11const MAX_KEPT = 120 // a longer line is dropped, never cut: a cut draft would send half a prompt
12const MIN_ANSWER = 20 // a reply shorter than this is not worth a model call
13
14// Held by the host, so the list survives a hot reload of this file.
15const steps = atom({ plugin: 'next-steps', key: 'steps' } as const, null as NextSteps | null)
16// True while the list is on screen. where-am-i reads it and leaves out its own "next" meanwhile.
17const active = atom({ plugin: 'next-steps', key: 'active' } as const, false)
18
19export const register: Register = on => {
20  // The turn a suggestion may still be shown for. A submit or a new turn moves it on.
21  let latest = ''
22  let submits = 0
23  // A survey answers bare digits in the band: leave them to it.
24  let hasSurvey = false
25
26  on('prompt.submit', async ($, e, next) => {
27    latest = `submit-${++submits}`
28    await setSteps($, () => null)
29    return next(e)
30  })
31
32  on('turn.start', async ($, e, next) => {
33    latest = e.turnId
34    await setSteps($, () => null)
35    return next(e)
36  })
37
38  on('turn.complete', async ($, e, next) => {
39    const r = await next(e)
40    if (e.agentId) return r // a subagent's turn, not the person's
41    latest = e.turnId // also for a turn that raised no turn.start, and after a hot reload
42    if (e.reason !== 'answer' || e.answer.trim().length < MIN_ANSWER) return r
43    void suggest($, e.turnId, e.answer, () => latest).catch(() => {}) // in the background, so the turn ends at once
44    return r
45  })
46
47  // A digit typed into an empty prompt picks a suggestion instead of landing as text.
48  // A paste carries no key, so a pasted "1" lands as typed.
49  on('prompt.edit', async ($, e, next) => {
50    if (hasSurvey || !e.key || e.key.ctrl || e.key.meta || e.text !== '' || e.start !== 0 || e.end !== 0 || !/^[0-9]$/.test(e.inputText)) return next(e)
51    const s = await read($, steps)
52    if (!s) return next(e)
53    if (e.inputText === '0') {
54      await setSteps($, () => null)
55      return { text: '', cursor: 0 } // consumed: the box stays empty
56    }
57    const pick = s.items[Number(e.inputText) - 1]
58    if (!pick) return next(e)
59    return next({ ...e, inputText: pick }) // a draft: the person edits it or presses Enter
60  })
61
62  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
63    const rest = await next(e) // what other mods and Claude Code draw here stays
64    hasSurvey = e.props.hasSurvey
65    const s = await read($, steps)
66    if (e.props.hasSurvey || e.props.isWorking || !s || s.items.length === 0) return rest
67
68    const { Box, Text } = $.ui.resolve(e)
69    const sep = <Text dimColor>{'  ·  '}</Text>
70    const item = (text: string, i: number) => [
71      <Text color="cyan" bold>{`${i + 1} `}</Text>,
72      <Text>{text}</Text>,
73    ]
74    const dismiss = [<Text color="cyan" bold>{'0 '}</Text>, <Text dimColor>dismiss</Text>]
75
76    const lines =
77      // One per row needs a row each; a short band gets the one row, cut at its end.
78      fitsOnOneLine(s.items, e.props.bodyColumns) || e.props.maxRows < s.items.length + 2 ? (
79        <Text wrap="truncate-end">
80          <Text dimColor>{'next:  '}</Text>
81          {s.items.flatMap((t, i) => [...item(t, i), sep])}
82          {dismiss}
83        </Text>
84      ) : (
85        <Box flexDirection="column">
86          {s.items.map((t, i) => (
87            <Text wrap="truncate-end">
88              <Text dimColor>{i === 0 ? 'next:  ' : '       '}</Text>
89              {item(t, i)}
90              {i === s.items.length - 1 && sep}
91              {i === s.items.length - 1 && dismiss}
92            </Text>
93          ))}
94        </Box>
95      )
96
97    return (
98      <Box flexDirection="column">
99        <Box paddingX={1}>{lines}</Box>
100        {rest}
101      </Box>
102    )
103  })
104}
105
106// Writes the list, then whether it shows, for where-am-i.
107async function setSteps($: EngineInterface, fn: (before: NextSteps | null) => NextSteps | null) {
108  const after = await update($, steps, fn)
109  const showing = after !== null && after.items.length > 0
110  if ((await read($, active)) !== showing) await update($, active, () => showing)
111}
112
113// The whole list on one row, or one suggestion per row when it would not fit.
114export function fitsOnOneLine(items: string[], columns: number) {
115  const width = 'next:  '.length + items.reduce((n, t) => n + 2 + t.length + 5, 0) + '0 dismiss'.length
116  return width <= columns - 2 // the band's padding
117}
118
119async function suggest($: EngineInterface, turnId: string, answer: string, current: () => string) {
120  // Wait for background agents: the main agent picks up again once they finish.
121  const agents = await $.agent.list().catch(() => [])
122  if (agents.some(a => a.status === 'running')) return
123
124  const all = (await $.session.messages().catch(() => [])).filter(m => m.text.trim() !== '')
125  if (all.at(-1)?.role === 'assistant') all.pop() // the latest reply goes in on its own below
126  const messages = all.slice(-6)
127  const r = await $.model.complete({
128    model: MODEL,
129    maxTokens: 200,
130    timeoutMs: 20_000,
131    system:
132      'You predict what a person will type next to their coding assistant, after reading a transcript of their session. ' +
133      `Give 2 or 3 short, concrete follow-up prompts, each an instruction in the imperative, under ${MAX_CHARS} characters, ` +
134      'specific to what just happened (name the file, test or feature). Plain words, no em dashes, no numbering, no quotes. ' +
135      'One per line and nothing else. If nothing sensible follows, reply with the single word NONE.\n' +
136      'Example reply:\nRun the login tests again\nAdd the same check to signup.ts\nOpen a draft PR',
137    prompt: [
138      `<transcript>\n${messages.map(m => `[${m.role === 'user' ? 'person' : 'assistant'}] ${m.text.slice(0, 800)}`).join('\n')}\n</transcript>`,
139      `<latest_reply>\n${answer.slice(0, 2000)}\n</latest_reply>`,
140      'Write the follow-up prompts the person is likely to send next: the lines only, not a reply to the transcript.',
141    ].join('\n\n'),
142  })
143  if (!r.isAnswered) return
144  const items = parseSteps(r.text)
145  // Checked inside the write, so a submit or a new turn that lands meanwhile always wins.
146  await setSteps($, before => (current() !== turnId ? before : items.length > 0 ? { turnId, items } : null))
147}
148
149// The model's lines, cleaned: no fences, bullets, numbers or em dashes; at most three.
150// A preamble, a "nothing to add" or a line too long to draft whole is dropped.
151export function parseSteps(text: string): string[] {
152  const body = text.replace(/```[a-z]*\n?/gi, '').trim()
153  let lines = body.split('\n')
154  if (body.startsWith('[') || body.startsWith('{')) {
155    try {
156      const o = JSON.parse(body) as unknown
157      const arr = Array.isArray(o) ? o : Object.values(o as Record<string, unknown>).find(Array.isArray)
158      lines = Array.isArray(arr) ? arr.filter((x): x is string => typeof x === 'string') : []
159    } catch {
160      // not JSON after all: read it as lines
161    }
162  }
163  const out: string[] = []
164  for (const line of lines) {
165    const clean = line
166      .replace(/\*\*/g, '')
167      .replace(/^\s*(?:[-*•]|\d+[.):])\s*/, '')
168      .replace(/^["'`]+|["'`,]+$/g, '')
169      .replace(/\s*\u2014\s*/g, ', ')
170      .replace(/\.$/, '')
171      .trim()
172    if (clean === '' || clean.endsWith(':') || clean.length > MAX_KEPT) continue
173    if (/^(none|no |nothing|here are|here's|sure)/i.test(clean) || /^[[\]{}]$/.test(clean)) continue
174    if (!out.some(o => o.toLowerCase() === clean.toLowerCase())) out.push(clean)
175    if (out.length === MAX_ITEMS) break
176  }
177  return out
178}
179
types/index.d.ts 8 lines
1export type NextSteps = { turnId: string; items: string[] }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'next-steps': { steps: NextSteps | null; active: boolean }
6  }
7}
8