SLOPSHOPPER

replay-theater

After each turn, a Replay button steps through every file edit Claude just made, one diff at a time.

newpanebandguardcommandprompt
v0.1.0no licenseupdated 2026-10-11KianaVega/KianaVega/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 ✕ › fix the failing auth test and add an audit log call │ ┃ [ Prev ] [ Next ] 1/3 Edit /work/app/src/aut │ ┃ 0 --- /work/app/src/auth.ts ⏺ Read(src/auth.ts) │ ┃ 0 +++ /work/app/src/auth.ts ⎿ Read 6 lines │ ┃ 1 -if (!claims) throw new Error('invalid ⏺ Update(src/auth.ts) │ ┃ 1 +if (!claims || claims.exp < Date.now() ⎿ Added 2 lines, removed 1 line │ ┃ 2 + await audit('refresh', claims.sub) ⏺ 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 │ │ › /replay │ ⎿ replay-theater: Replaying 3 edit(s). │ │ 3 edit(s) last turn [ Replay ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
3 edit(s) last turn [ Replay ]
Pane · Replay
[ Prev ] [ Next ] 1/3 Edit /work/app/src/auth.ts 0 --- /work/app/src/auth.ts 0 +++ /work/app/src/auth.ts 1 -if (!claims) throw new Error('invalid token') 1 +if (!claims || claims.exp < Date.now() / 1000) throw n 2 + await audit('refresh', claims.sub)
README

Claude Code mods

Nine mods for Claude Code (2.1.287 or newer), written from the walkthrough video. Each lives in mods/<name>.

Install one, or all, from a terminal session:

/plugin install <name> --marketplace KianaVega/KianaVega
ModWhat it doesExtra model usage
savvy-progressProgress bar above the prompt and a live list of subagents with model, tokens and rough costnone
tool-skinsIcons and colors for tool calls in seven themes, switch with /skin <name>none
you-should-knowA second, small model reads long answers and flags anything important you might missone small call per long answer
file-treePane showing files Claude reads and edits, green once committed (/file-tree)none
cache-taxHolds a prompt that would reload a cold cache and shows an estimate. /keepwarm pings before the cache expiresnone, except keep-warm pings
blast-radiusStops recursive deletes, git clean, hard resets and shows the affected files before you press Proceednone
reflectOffers to save corrections as a rule in CLAUDE.mdone small call when you save
terminal-browser/browse <url or file> opens a pane with a screenshot (kitty or Ghostty) and the page textnone
replay-theaterReplay button after a turn that steps through each edit as a diff (/replay)none

Notes

  • Settings such as trigger phrases, price tables, skins and which model the watcher uses are constants at the top of each hooks/register.tsx. Edit and reload.
  • Cost figures are estimates from a built-in price table.
  • terminal-browser is a viewer built from a headless screenshot and page text. It is not a live, clickable browser.
  • Mods run in the terminal and the desktop app's Code tab, not the VS Code extension.
Source 2 files
hooks/register.tsx 140 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { EditStep } from '../types'
5
6const PANE = 'replay'
7
8const current = atom({ plugin: 'replay-theater', key: 'current' } as const, [] as EditStep[])
9const last = atom({ plugin: 'replay-theater', key: 'last' } as const, [] as EditStep[])
10const index = atom({ plugin: 'replay-theater', key: 'index' } as const, 0)
11
12const diffOf = (step: EditStep): string => {
13  const before = step.before === '' ? [] : step.before.split('\n')
14  const after = step.after.split('\n')
15
16  return [
17    `--- ${step.path}`,
18    `+++ ${step.path}`,
19    `@@ -1,${before.length} +1,${after.length} @@`,
20    ...before.map(l => `-${l}`),
21    ...after.map(l => `+${l}`),
22  ].join('\n')
23}
24
25export const register: Register = on => {
26  on('session.start', async ($, e, next) => {
27    await $.command.register({ name: 'replay', description: 'Step through the edits from the last turn' })
28
29    return next(e)
30  })
31
32  on('command.run', { command: 'replay' }, async $ => {
33    const steps = await read($, last)
34    if (steps.length === 0) return { text: 'No edits to replay yet.' }
35    await update($, index, () => 0)
36    await $.ui.open({ id: PANE, title: 'Replay' })
37
38    return { text: `Replaying ${steps.length} edit(s).` }
39  })
40
41  // Watch: record each file edit as it is made, in the main loop only.
42  on('tool.call', async ($, e, next) => {
43    if (e.agentId !== undefined) return next(e)
44    const tool = String(e.tool)
45    const ran = await next(e)
46    const hasFailed = ran.deny !== undefined || ran.isError === true
47
48    if (!hasFailed) {
49      const i = e as unknown as Record<string, unknown>
50      const path = String(i.file_path ?? i.notebook_path ?? '')
51      let step: EditStep | undefined
52      if (tool === 'Edit') {
53        step = { path, tool, before: String(i.old_string ?? ''), after: String(i.new_string ?? '') }
54      } else if (tool === 'Write') {
55        step = { path, tool, before: '', after: String(i.content ?? '') }
56      } else if (tool === 'MultiEdit' && Array.isArray(i.edits)) {
57        const edits = i.edits as Array<{ old_string?: string; new_string?: string }>
58        step = {
59          path,
60          tool,
61          before: edits.map(x => x.old_string ?? '').join('\n...\n'),
62          after: edits.map(x => x.new_string ?? '').join('\n...\n'),
63        }
64      }
65      if (step !== undefined) {
66        const s = step
67        await update($, current, list => [...list, s].slice(-200))
68      }
69    }
70
71    return ran
72  })
73
74  on('prompt.submit', async ($, e, next) => {
75    await update($, current, () => [])
76
77    return next(e)
78  })
79
80  on('turn.complete', async ($, e, next) => {
81    if (e.agentId === undefined) {
82      const steps = await read($, current)
83      if (steps.length > 0) {
84        await update($, last, () => steps)
85        await update($, index, () => 0)
86      }
87    }
88
89    return next(e)
90  })
91
92  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
93    const steps = await read($, last)
94    if (e.props.hasSurvey || e.props.isWorking || steps.length === 0) {
95      return next(e)
96    }
97    const { Box, Button, Text } = $.ui.resolve(e)
98
99    return (
100      <Box>
101        <Text dimColor>{steps.length} edit(s) last turn </Text>
102        <Button
103          key="replay"
104          label="Replay"
105          onPress={async () => {
106            await update($, index, () => 0)
107            await $.ui.open({ id: PANE, title: 'Replay' })
108          }}
109        />
110      </Box>
111    )
112  })
113
114  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
115    const steps = await read($, last)
116    const i = Math.min(await read($, index), Math.max(0, steps.length - 1))
117    const step = steps[i]
118    const { Box, Button, Code, Text } = $.ui.resolve(e)
119
120    if (step === undefined) {
121      return <Text dimColor>Nothing to replay.</Text>
122    }
123
124    return (
125      <Box flexDirection="column">
126        <Box>
127          <Button key="prev" label="Prev" hotkey="p" onPress={() => update($, index, n => Math.max(0, n - 1))} />
128          <Text> </Text>
129          <Button key="next" label="Next" hotkey="n" onPress={() => update($, index, n => Math.min(steps.length - 1, n + 1))} />
130          <Text>
131            {' '}
132            {i + 1}/{steps.length} {step.tool} {step.path}
133          </Text>
134        </Box>
135        <Code source={diffOf(step)} format="diff" path={step.path} />
136      </Box>
137    )
138  })
139}
140
types/index.d.ts 8 lines
1export type EditStep = { path: string; tool: string; before: string; after: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'replay-theater': { current: EditStep[]; last: EditStep[]; index: number }
6  }
7}
8