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

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
| Mod | What it does | Extra model usage |
|---|---|---|
| savvy-progress | Progress bar above the prompt and a live list of subagents with model, tokens and rough cost | none |
| tool-skins | Icons and colors for tool calls in seven themes, switch with /skin <name> | none |
| you-should-know | A second, small model reads long answers and flags anything important you might miss | one small call per long answer |
| file-tree | Pane showing files Claude reads and edits, green once committed (/file-tree) | none |
| cache-tax | Holds a prompt that would reload a cold cache and shows an estimate. /keepwarm pings before the cache expires | none, except keep-warm pings |
| blast-radius | Stops recursive deletes, git clean, hard resets and shows the affected files before you press Proceed | none |
| reflect | Offers to save corrections as a rule in CLAUDE.md | one small call when you save |
| terminal-browser | /browse <url or file> opens a pane with a screenshot (kitty or Ghostty) and the page text | none |
| replay-theater | Replay button after a turn that steps through each edit as a diff (/replay) | none |
hooks/register.tsx. Edit and reload.hooks/register.tsx 140 lines1import { 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}
140types/index.d.ts 8 lines1export 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