SLOPSHOPPER

replay-theater

Step through the last turn's file edits, one diff at a time.

newpaneguardcommandtoaststatus
★ 167v0.1.3MITupdated 2026-10-04hamzafer/claude-code-mods/mods/replay-theater
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · replay-theater
│ ┃ Replay Theater ✕ › fix the failing auth╭────────────────────────────────────────────╮ │ ┃ Step 1/3 edit src/auth.ts │ replay-theater │ │ ┃ [1] 2 3 ⏺ Read(src/auth.ts) │ replay-theater: 3 edits last turn. Run │ │ ┃ [ Prev ] [ Next ] [ Close ] ⎿ Read 6 lines │ /replay │ │ ┃ ──────────────────────────────────────────── ⏺ Update(src/auth.ts) ╰────────────────────────────────────────────╯ │ ┃ ────────── ⎿ Added 2 lines, removed 1 line │ ┃ - if (!claims) throw new Error('invalid tok… ⏺ Bash(bun test) │ ┃ + if (!claims || claims.exp < Date.now() / … ⎿ 3 pass, 1 fail │ ┃ + await audit('refresh', claims.sub) │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /replay │ ⎿ replay-theater: Replaying the last turn │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Replay Theater
Step 1/3 edit src/auth.ts [1] 2 3 [ Prev ] [ Next ] [ Close ] ────────────────────────────────────────────────────── - if (!claims) throw new Error('invalid token') + if (!claims || claims.exp < Date.now() / 1000) throw new … + await audit('refresh', claims.sub)
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 182 lines
1// Replay Theater: step through the last turn's file edits, one diff at a time.
2import { atom, read, update } from 'claude-code'
3import type { EngineInterface, Register } from 'claude-code'
4
5import type { ReplayStep } from '../types'
6
7const PANE = 'replay-theater'
8const MAX_DIFF_LINES = 400
9
10// Held by the host, so a replay survives a hot reload of this file.
11const replay = atom({ plugin: 'replay-theater', key: 'replay' } as const, [] as ReplayStep[])
12const pos = atom({ plugin: 'replay-theater', key: 'pos' } as const, 0)
13
14export const register: Register = on => {
15  let pending: ReplayStep[] = []
16  let cwd = ''
17  let toasted = false // one toast per session teaches /replay; later turns use the status line
18  let showing = false // the status line holds a count from the last turn
19
20  on('session.start', async ($, e, next) => {
21    const r = await next(e)
22    cwd = e.cwd
23    toasted = false
24    if (showing) {
25      showing = false
26      $.ui.status(undefined) // a new session starts without the last one's count
27    }
28    await $.command.register({ name: 'replay', description: "Step through the last turn's file edits" }).catch(() => {}) // a name Claude Code already has is refused: start anyway
29    return r
30  })
31
32  on('turn.start', ($, e, next) => {
33    pending = [] // a model turn begins: start a fresh replay
34    if (showing) {
35      showing = false
36      $.ui.status(undefined) // the count belongs to the last turn
37    }
38    return next(e)
39  })
40
41  // Observe only: the edit runs untouched.
42  on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
43    const r = await next(e)
44    if (!r.deny && !r.isError) {
45      pending.push({ file: e.file_path, kind: 'edit', before: e.old_string, after: e.new_string })
46    }
47    return r
48  })
49
50  on('tool.call', { tool: 'Write' }, async ($, e, next) => {
51    const before = await $.fs.read(e.file_path).catch(() => '') // read just before the write lands
52    const r = await next(e)
53    if (!r.deny && !r.isError) {
54      pending.push({ file: e.file_path, kind: 'write', before, after: e.content })
55    }
56    return r
57  })
58
59  on('turn.complete', async ($, e, next) => {
60    const r = await next(e)
61    if (!e.agentId && pending.length > 0) {
62      const steps = pending
63      pending = []
64      await update($, replay, () => steps) // one replay per turn
65      await update($, pos, () => 0)
66      const count = `${steps.length} edit${steps.length === 1 ? '' : 's'}`
67      if (!toasted) {
68        toasted = true
69        $.ui.toast(`replay-theater: ${count} last turn. Run /replay`)
70      } else {
71        showing = true
72        $.ui.status(`▶ /replay: ${count}`)
73      }
74    }
75    return r
76  })
77
78  on('command.run', { command: 'replay' }, async $ => ({
79    text: (await openReplay($)) ? 'Replaying the last turn' : 'No edits to replay yet',
80  }))
81
82  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
83    const { Box, Text, Button } = $.ui.resolve(e)
84    const steps = await read($, replay)
85    const at = Math.min(await read($, pos), Math.max(0, steps.length - 1))
86    const step = steps[at]
87
88    if (!step) {
89      return (
90        <Box flexDirection="column">
91          <Text dimColor>No edits to replay yet.</Text>
92          <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
93        </Box>
94      )
95    }
96
97    const width = Math.max(20, e.props.bodyColumns - 2)
98    return (
99      <Box flexDirection="column">
100        <Text>
101          <Text bold>{`Step ${at + 1}/${steps.length} `}</Text>
102          <Text dimColor>{`${step.kind === 'write' ? 'write' : 'edit'} `}</Text>
103          <Text color="cyan">{relative(step.file, cwd)}</Text>
104        </Text>
105        <Text>
106          {steps.map((_, i) => (
107            <Text color={i === at ? 'cyan' : undefined} bold={i === at} dimColor={i !== at}>
108              {i === at ? `[${i + 1}]` : ` ${i + 1} `}
109            </Text>
110          ))}
111        </Text>
112        <Box flexDirection="row" gap={1}>
113          <Button key="prev" label="Prev" hotkey="p" onPress={() => update($, pos, p => Math.max(0, p - 1))} />
114          <Button key="next" label="Next" hotkey="n" variant="primary" onPress={() => update($, pos, p => Math.min(steps.length - 1, p + 1))} />
115          <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
116        </Box>
117        <Text dimColor>{'─'.repeat(width)}</Text>
118        {diffLines(step).map(line => (
119          <Text color={line.sign === '+' ? 'green' : line.sign === '-' ? 'red' : undefined} dimColor={line.sign === ' '} wrap="truncate-end">
120            {`${line.sign} ${line.text}`}
121          </Text>
122        ))}
123      </Box>
124    )
125  })
126}
127
128async function openReplay($: EngineInterface) {
129  if ((await read($, replay)).length === 0) return false
130  await update($, pos, () => 0)
131  await $.ui.open({ id: PANE, title: 'Replay Theater', focus: true })
132  return true
133}
134
135type DiffLine = { sign: '+' | '-' | ' '; text: string }
136
137// A line diff (LCS) of the step's before and after text, capped for large files.
138export function diffLines(step: ReplayStep): DiffLine[] {
139  const a = lines(step.before)
140  const b = lines(step.after)
141  if (a.length + b.length > MAX_DIFF_LINES * 2) {
142    return [{ sign: ' ', text: `(large change: ${a.length} → ${b.length} lines, diff not drawn)` }]
143  }
144  const n = a.length
145  const m = b.length
146  const lcs: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0))
147  for (let i = n - 1; i >= 0; i--) {
148    for (let j = m - 1; j >= 0; j--) {
149      lcs[i]![j] = a[i] === b[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!)
150    }
151  }
152  const out: DiffLine[] = []
153  let i = 0
154  let j = 0
155  while (i < n && j < m) {
156    if (a[i] === b[j]) {
157      out.push({ sign: ' ', text: a[i]! })
158      i++
159      j++
160    } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) {
161      out.push({ sign: '-', text: a[i++]! })
162    } else {
163      out.push({ sign: '+', text: b[j++]! })
164    }
165  }
166  while (i < n) out.push({ sign: '-', text: a[i++]! })
167  while (j < m) out.push({ sign: '+', text: b[j++]! })
168  return out
169}
170
171// A file's lines, without the empty one after a trailing newline.
172function lines(text: string) {
173  return text === '' ? [] : text.replace(/\n$/, '').split('\n')
174}
175
176// The path from the working directory, or its last two parts when it is outside.
177function relative(file: string, cwd: string) {
178  if (cwd && file.startsWith(`${cwd}/`)) return file.slice(cwd.length + 1)
179  const parts = file.split('/')
180  return parts.length > 3 ? `…/${parts.slice(-2).join('/')}` : file
181}
182
types/index.d.ts 8 lines
1export type ReplayStep = { file: string; kind: 'edit' | 'write'; before: string; after: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'replay-theater': { replay: ReplayStep[]; pos: number }
6  }
7}
8