/replay blättert Schritt für Schritt durch die Dateiänderungen der letzten Runde, als Diff in einem Pane

Ein Wetterbericht für das Kontextfenster, als Zeile über dem Claude-Code-Prompt. Er aktualisiert sich nach jeder Runde.
☂ Regenschauer (showers) 67% · 134,4k / 200k ▂▆ ▲ +98,3k letzte Runde (last turn)
| Füllstand | Anzeige |
|---|---|
| unter 25 % | ☀ Heiter (clear), gelb |
| 25–49 % | ☁ Bewölkt (cloudy), cyan |
| 50–74 % | ☂ Regenschauer (showers), blau |
| 75–89 % | ☇ Gewitter (storm), magenta |
| ab 90 % | ↯ Bald komprimieren (compact soon), rot |
Danach folgen der Füllstand in Prozent, die belegten Tokens von der Fenstergröße, ein Verlauf der letzten 12 Runden (jeder Balken gemessen am ganzen Fenster) und der Zuwachs der letzten Runde. Nach einem Komprimieren zeigt er ▼ und den Rückgang.
Benötigt eine Claude-Code-Version mit Mod-Unterstützung (Function Hooks).
/plugin marketplace add reifen01/claude-mods
/plugin install token-weather@reifen01-mods
session.measure: Die Engine misst nach jeder Runde; ändert sich der Kontext, speichert der Mod den neuen Stand. Kostet keine Tokens.$.state: Die letzten 12 Stände, das Fenster und der Zuwachs. Ein Schreiben zeichnet die Zeile neu.ui.render auf AbovePrompt: zeichnet die Zeile unter dem, was andere Plugins dort zeichnen.claude plugin test plugins/token-weather
claude plugin test plugins/blast-radius
claude plugin test plugins/replay
Hält gefährliche Bash-Befehle von Claude an, bevor sie laufen, misst was sie anrichten würden, und fragt dich.
⚠ Rekursives Löschen angehalten
$ rm -rf build
Würde 1.204 Dateien und Ordner (48,3 MB) unwiderruflich löschen
build: 1.204 Einträge, 48,3 MB
[ Abbrechen ] [ Trotzdem ausführen ]
| Erkannt | Gemessen mit |
|---|---|
rm -r / rm -rf | du, find (Anzahl und Größe) |
git reset --hard [ref] | git status, git log ref..HEAD |
git clean -f… | git clean -n (Probelauf) |
git checkout -- …, git restore … | git diff |
git push --force / -f | git log HEAD..@{u} (Stand letzter fetch) |
git branch -D | Commits, die nur auf dem Branch liegen |
find … -delete | derselbe find mit -print |
n, auch ✕ oder Esc): Claude bekommt eine Absage mit dem, was der Befehl angerichtet hätte.j): der Befehl läuft normal.claude -p) läuft der Befehl wie ohne Mod.*.log oder $VAR werden genannt, nicht ausgewertet.Installieren:
/plugin install blast-radius@reifen01-mods
Nach jeder Runde merkt sich der Mod, welche Dateien Claude geändert hat. /replay öffnet ein Pane und blättert Schritt für Schritt durch die Diffs. Die Beschriftungen sind deutsch, mit dem englischen Begriff in Klammern.
Schritt (step) 2/5 …/src/app.ts (geändert (changed)) +12 −3
@@ -40,7 +40,16 @@
…
[ ◀ Zurück (back) ] [ Weiter (next) ▶ ] [ Schließen (close) ]
Edit, Write und NotebookEdit, auch die von Subagenten derselben Runde.w weiter, z zurück, q oder Esc schließen.Installieren:
/plugin install replay@reifen01-mods
MIT
hooks/register.tsx 108 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderInput } from 'claude-code'
3
4import type { Replay, Step } from '../types'
5import { hunkOf, shortOf, stepOf } from './diff'
6import type { Hunk } from './diff'
7
8const PANE = 'replay'
9const replay = atom({ plugin: 'replay', key: 'replay' } as const, null)
10
11// the edits of the turn under way; one list per main-thread turn, a subagent's edits counted with it
12let pending: Step[] = []
13
14type Patched = { structuredPatch?: readonly Hunk[] }
15
16const move = ($: EngineInterface, by: number) =>
17 update($, replay, r => (r ? { ...r, index: Math.max(0, Math.min(r.steps.length - 1, r.index + by)) } : r))
18
19export const register: Register = on => {
20 on('tool.call', async ($, e, next) => {
21 const ran = await next(e) // the edit runs untouched
22 if (ran.deny !== undefined || ran.isError) return ran
23 const r = ran.result as Patched | undefined
24 if (e.tool === 'Edit' && r?.structuredPatch) {
25 pending.push(stepOf('Edit', e.file_path, 'update', r.structuredPatch))
26 } else if (e.tool === 'Write') {
27 const w = ran.result as (Patched & { type?: 'create' | 'update'; originalFile?: string | null }) | undefined
28 const hunks = w?.structuredPatch?.length ? w.structuredPatch : hunkOf(w?.originalFile ?? '', e.content)
29 pending.push(stepOf('Write', e.file_path, w?.type ?? 'update', hunks))
30 } else if (e.tool === 'NotebookEdit') {
31 const n = ran.result as { old_source?: string; new_source?: string } | undefined
32 const cell = e.cell_id ? `${e.notebook_path} · Zelle ${e.cell_id}` : e.notebook_path
33 pending.push(stepOf('NotebookEdit', cell, 'update', hunkOf(n?.old_source ?? '', e.edit_mode === 'delete' ? '' : (n?.new_source ?? e.new_source))))
34 }
35 return ran
36 })
37
38 // a turn begins (main thread: a subagent's loop has no turn.start of its own): the list starts over
39 on('turn.start', ($, e, next) => {
40 pending = []
41 return next(e)
42 })
43
44 on('turn.complete', async ($, e, next) => {
45 const r = await next(e)
46 if (!e.agentId && pending.length > 0) {
47 await update($, replay, () => ({ steps: pending, index: 0 })) // one replay per turn
48 pending = []
49 }
50 return r
51 })
52
53 on('session.start', async ($, e, next) => {
54 const r = await next(e)
55 await $.command.register({ name: 'replay', description: 'Die Dateiänderungen der letzten Runde Schritt für Schritt ansehen (step through the last turn\'s file edits)' })
56 return r
57 })
58
59 on('command.run', { command: 'replay' }, async $ => {
60 const r = await read($, replay)
61 if (!r || r.steps.length === 0) return { text: 'Keine Dateiänderungen in der letzten Runde (no edits in the last turn).' }
62 await update($, replay, cur => (cur ? { ...cur, index: 0 } : cur))
63 const opened = await $.ui.open({ id: PANE, title: 'Wiedergabe (Replay)', focus: true, closeOnEscape: true })
64 const n = r.steps.length
65 return {
66 text: opened.isPlaced
67 ? `Wiedergabe (replay): ${n} ${n === 1 ? 'Änderung' : 'Änderungen'} der letzten Runde.`
68 : `Wiedergabe (replay): ${n} ${n === 1 ? 'Änderung' : 'Änderungen'}; das Fenster ist zu schmal für ein Pane (too narrow: ${opened.reason}).`,
69 }
70 }).catch(($, e, next) => (next.called ? next(e) : { text: 'Wiedergabe konnte nicht geöffnet werden (replay could not open).' }))
71
72 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
73 const r = await read($, replay)
74 const { Text } = $.ui.resolve(e)
75 return r && r.steps.length > 0 ? card($, e, r) : <Text dimColor>Keine Dateiänderungen in der letzten Runde (no edits in the last turn).</Text>
76 })
77}
78
79function card($: EngineInterface, e: RenderInput, r: Replay) {
80 const { Box, Text, Button, Code } = $.ui.resolve(e)
81 const i = r.index
82 const step = r.steps[i]
83 if (!step) return <Text dimColor>Keine Dateiänderungen in der letzten Runde (no edits in the last turn).</Text>
84 const n = r.steps.length
85 const kind = step.kind === 'create' ? 'neu (new)' : step.tool === 'NotebookEdit' ? 'Notebook' : 'geändert (changed)'
86 return (
87 <Box key="replay" flexDirection="column">
88 <Box flexDirection="row" gap={1}>
89 <Text bold>{`Schritt (step) ${i + 1}/${n}`}</Text>
90 <Text color="cyan" wrap="truncate-start">{shortOf(step.file)}</Text>
91 <Text dimColor>{`(${kind})`}</Text>
92 <Text color="green">{`+${step.added}`}</Text>
93 <Text color="red">{`−${step.removed}`}</Text>
94 </Box>
95 {step.diff ? (
96 <Code source={step.diff} format="diff" path={step.file} wrap="truncate-end" />
97 ) : (
98 <Text dimColor>Kein Diff verfügbar, die Engine hat keinen Patch geliefert (no diff available).</Text>
99 )}
100 <Box flexDirection="row" gap={2} marginTop={1}>
101 <Button key="prev" label="◀ Zurück (back)" hotkey="z" dimColor={i === 0} onPress={() => move($, -1)} />
102 <Button key="next" label="Weiter (next) ▶" hotkey="w" variant="primary" autoFocus dimColor={i === n - 1} onPress={() => move($, 1)} />
103 <Button key="close" label="Schließen (close)" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
104 </Box>
105 </Box>
106 )
107}
108hooks/diff.ts 49 lines1import type { Step } from '../types'
2
3export type Hunk = { oldStart: number; oldLines: number; newStart: number; newLines: number; lines: string[] }
4
5/** The engine's structured patch as unified diff text, the form `Code format="diff"` draws. */
6export function unifiedOf(hunks: readonly Hunk[]): string {
7 return hunks
8 .map(h => [`@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@`, ...h.lines].join('\n'))
9 .join('\n')
10}
11
12export function countsOf(diff: string): { added: number; removed: number } {
13 let added = 0
14 let removed = 0
15 for (const line of diff.split('\n')) {
16 if (line.startsWith('+')) added++
17 else if (line.startsWith('-')) removed++
18 }
19 return { added, removed }
20}
21
22// a hunk for a cell or a file the engine gave no patch for: the old lines out, the new lines in
23export function hunkOf(oldText: string, newText: string): Hunk[] {
24 const o = oldText === '' ? [] : oldText.split('\n')
25 const n = newText === '' ? [] : newText.split('\n')
26 // trim the common head and tail so the hunk shows what changed, not the whole text
27 let head = 0
28 while (head < o.length && head < n.length && o[head] === n[head]) head++
29 let tail = 0
30 while (tail < o.length - head && tail < n.length - head && o[o.length - 1 - tail] === n[n.length - 1 - tail]) tail++
31 const lines = [
32 ...o.slice(head, o.length - tail).map(l => `-${l}`),
33 ...n.slice(head, n.length - tail).map(l => `+${l}`),
34 ]
35 if (lines.length === 0) return []
36 return [{ oldStart: head + 1, oldLines: o.length - head - tail, newStart: head + 1, newLines: n.length - head - tail, lines }]
37}
38
39export function stepOf(tool: Step['tool'], file: string, kind: Step['kind'], hunks: readonly Hunk[]): Step {
40 const diff = unifiedOf(hunks)
41 return { file, tool, kind, diff, ...countsOf(diff) }
42}
43
44/** The path shortened to its last three parts, for a title line. */
45export function shortOf(path: string): string {
46 const parts = path.split('/').filter(Boolean)
47 return parts.length <= 3 ? path : `…/${parts.slice(-3).join('/')}`
48}
49types/index.d.ts 22 lines1/** One file edit of a turn, as a unified diff. */
2export type Step = {
3 /** The file's path as the tool was given it. */
4 file: string
5 tool: 'Edit' | 'Write' | 'NotebookEdit'
6 /** `create` for a new file, `update` otherwise. */
7 kind: 'create' | 'update'
8 /** Unified diff hunks (`@@ … @@` then ` `, `+`, `-` lines); empty when the engine had none. */
9 diff: string
10 added: number
11 removed: number
12}
13
14/** The edits of the last finished main-thread turn, and where the person is in them. */
15export type Replay = { steps: Step[]; index: number }
16
17declare module 'claude-code' {
18 interface PluginState {
19 replay: { replay: Replay | null }
20 }
21}
22