[mod] Records the file edits Claude makes in a turn; /replay or r steps through the diffs one at a time in a pane. Based on Anthropic's claude-code-playground…

japan-da-man の Claude Code プラグイン(mods を含む)を管理するマーケットプレイス。
| 名前 | 内容 | | :- | :- | | japan-da-man-mods | まとめてインストール用。下のプラグインを全部入れる | | todo-mod | /todo で TODO パネルを開く。チェックを入れたタスクを Claude に依頼する | | token-weather | コンテキストの埋まり具合を天気でプロンプトの上に表示する(☀ Clear → ☁ Cloudy → ☂ Showers → ☇ Storm → ↯ Compact soon)。直近12ターンのグラフと、直前のターンの増減(▲ +98.3k last turn)付き。/weather で表示切り替え | | usage-stats | /stats で利用統計のパネルを開く。セッション数・メッセージ数・トークン数・利用日数・連続日数・よく使うモデル・日ごとのヒートマップ | | blast-radius | 危険なシェルコマンドを実行前に止め、何が消えるか・変わるかをペインに出して Proceed / Cancel で選ばせる(10分答えがなければ止める)。rm -r/-f、git reset --hard、force push、git clean、変更の破棄、git branch -D、git stash drop/clear、マイグレーション、find -delete、xargs rm。bash -c / eval の中も調べる。/blast-radius で履歴 | | replay-theater | ターンの中で Claude が行ったファイル編集を記録する。ターンが終わるとプロンプトの上に ▶ Replay: N edits が出て、r か /replay でペインを開き、編集を 1 件ずつ差分で見られる(Prev / Next / Close) | | spec-flow | 「〇〇な機能が欲しい」と話しかけるか /spec-flow:spec <要望> で、要求整理 → 影響画面確認 → 要件定義 → 機能案・UI案 を 1 段階ずつ承認しながら進める。曖昧な点はヒアリングし、影響調査と案出しはサブエージェントが行う。成果物は docs/specs/<id>/(UI 案の HTML モックは Desktop なら Artifact にも公開。--local で公開しない)。/specs でチェックリストを開き、チェックで承認・修正依頼もできる | | dev-flow | 汎用の開発フロー。skills plan(小さなタスクに分けて承認をもらってから着手)・tdd(RED → GREEN → REFACTOR)・debug(再現 → 仮説 → 検証で原因を確かめてから直す)・review(重要度つきの差分レビュー)。サブエージェント code-explorer(横断調査)・test-runner(テスト結果の要約)・reviewer(別コンテキストでレビュー、修正はしない)が長い出力をメインに持ち込まない | | dev-extras | 他の OSS から選んで取り込んだ skill(すべて MIT、出典は各 README と licenses/)。ponytail(最小の実装に絞る)・ponytail-review(作り込みすぎだけを探す)・grill-me(質問攻めで計画を詰める)・handoff(バックグラウンドエージェントに引き継ぐ)・verification-before-completion(検証してから完了と言う)・security-audit(Cloudflare 製のセキュリティ監査) | | mod-dev | skill add-mod。「〇〇する mod を作って」で雛形・テスト・登録まで行う。marketplace リポジトリでは marketplace.json / bundle / README も更新し、それ以外では単体の mod を作る |
claude plugin marketplace add japan-da-man/claude-code
claude plugin install japan-da-man-mods@japan-da-man
1つずつ入れる場合は claude plugin install <名前>@japan-da-man。
更新:
claude plugin marketplace update japan-da-man
claude plugin update <名前>@japan-da-man
bundle に増えたプラグインを入れるときは、update → install の順で実行する(update だけだと増えた分は入らず、bundle が failed to load になる):
claude plugin marketplace update japan-da-man
claude plugin update japan-da-man-mods@japan-da-man
claude plugin install japan-da-man-mods@japan-da-man
セッション内なら update のあと /reload-plugins でもよい。
インストールせずに作業中のディレクトリを読み込む(保存するとホットリロードされる):
claude --plugin-dir ./plugins/todo-mod
チェック:
claude plugin validate .
claude plugin validate ./plugins/todo-mod
テスト(tests/*.test.ts があるプラグイン):
cd plugins/<name> && claude plugin test
mod-dev を入れた状態でこのリポジトリを開き「〇〇する mod を追加して」と頼むと、skill add-mod が下の手順(雛形・テスト・marketplace / bundle / README 更新・validate)をまとめてやる。手でやる場合:
plugins/<name>/ に .claude-plugin/plugin.json と中身を置く.claude-plugin/marketplace.json の plugins にエントリを追加する(name は plugin.json と同じにする)plugins/japan-da-man-mods/.claude-plugin/plugin.json の dependencies にも名前を足すplugin.json に version を書いていないので、コミットごとに新しいバージョンとして扱われる。
mods は Claude Code v2.1.287 以降が必要。
token-weather・blast-radius・replay-theater は、Anthropic の claude-code-playground にある同名の mod(Apache-2.0)をもとに手を加えたもの。各プラグインに LICENSE を同梱し、各 hooks/register.js の先頭に元のコードの著作権表示と、こちらで変えた点を書いている。
hooks/register.js 287 lines1// Copyright 2026 Anthropic PBC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Replay Theater: records the file edits Claude makes in a turn, then lets you
5// step through them in a pane, one diff at a time.
6// Based on mods/replay-theater in anthropics/claude-code-playground (Apache-2.0, see LICENSE).
7//
8// japan-da-man による変更:
9// - 記録したステップと見ている位置を $.state に置く(プラグインを読み込み直しても消えない)
10// - 成功した編集だけ記録する(拒否されたり失敗したりした編集は記録しない)
11// - 帯に他の mod が描くものを残す(Token Weather などと共存する)
12// - 描画サイトの値は e.props から読む
13
14import { atom, read, update } from 'claude-code'
15
16const PANE_ID = 'replay-theater'
17const MAX_DIFF_LINES = 12
18const MAX_LCS_LINES = 400
19const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit'])
20
21// 直前に編集があったターンのステップと、ペインで見ている位置
22const replayAtom = atom({ plugin: 'replay-theater', key: 'replay' }, { steps: [], index: 0 })
23// 今のターンのステップ(ターンが終わったら replay に移す)
24let pending = []
25// ペインを開いているか。狭い画面でペインを置けないときは帯に描く
26const view = { isOpen: false, inBand: false }
27
28export function relPath(cwd, path) {
29 if (!path) return '(unknown file)'
30 if (cwd && path.startsWith(cwd + '/')) return path.slice(cwd.length + 1)
31 return path
32}
33
34function splitLines(text) {
35 if (text === undefined || text === null || text === '') return []
36 const lines = String(text).split('\n')
37 if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop()
38 return lines
39}
40
41// 行単位の差分。小さいものは LCS で変わっていない行も文脈として残し、大きいものは全部削除→全部追加にする
42export function diffLines(oldText, newText) {
43 const a = splitLines(oldText)
44 const b = splitLines(newText)
45 if (a.length > MAX_LCS_LINES || b.length > MAX_LCS_LINES) {
46 return [...a.map((t) => ({ op: '-', t })), ...b.map((t) => ({ op: '+', t }))]
47 }
48 const n = a.length
49 const m = b.length
50 const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0))
51 for (let i = n - 1; i >= 0; i--) {
52 for (let j = m - 1; j >= 0; j--) dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
53 }
54 const out = []
55 let i = 0
56 let j = 0
57 while (i < n && j < m) {
58 if (a[i] === b[j]) {
59 out.push({ op: ' ', t: a[i] })
60 i++
61 j++
62 } else if (dp[i + 1][j] >= dp[i][j + 1]) out.push({ op: '-', t: a[i++] })
63 else out.push({ op: '+', t: b[j++] })
64 }
65 while (i < n) out.push({ op: '-', t: a[i++] })
66 while (j < m) out.push({ op: '+', t: b[j++] })
67 return trimContext(out)
68}
69
70// 変更の前後 1 行だけ文脈を残す。長いファイルの Write でも変わった行が見える
71export function trimContext(lines) {
72 if (!lines.some((l) => l.op !== ' ')) return lines.slice(0, MAX_DIFF_LINES)
73 const changed = (l) => l !== undefined && l.op !== ' '
74 const keep = lines.map((l, k) => changed(l) || changed(lines[k - 1]) || changed(lines[k + 1]))
75 const out = []
76 let skipped = false
77 lines.forEach((l, k) => {
78 if (keep[k]) {
79 if (skipped && out.length) out.push({ op: '~', t: '⋯' })
80 out.push(l)
81 skipped = false
82 } else {
83 skipped = true
84 }
85 })
86 return out
87}
88
89export function countChanges(diff) {
90 let add = 0
91 let del = 0
92 for (const l of diff) {
93 if (l.op === '+') add++
94 else if (l.op === '-') del++
95 }
96 return { add, del }
97}
98
99// 1 回のツール呼び出しを 1 つ以上のステップにする。before は Write の前のファイルの中身
100export function stepsFor(e, cwd, before) {
101 const file = relPath(cwd, e.file_path)
102 if (e.tool === 'Edit') {
103 return [{ tool: 'Edit', file, diff: diffLines(e.old_string, e.new_string), note: e.replace_all ? 'replace all' : '' }]
104 }
105 if (e.tool === 'MultiEdit' && Array.isArray(e.edits)) {
106 return e.edits.map((ed, k) => ({ tool: 'MultiEdit', file, diff: diffLines(ed.old_string, ed.new_string), note: `edit ${k + 1} of ${e.edits.length}` }))
107 }
108 if (e.tool === 'Write') {
109 return [{ tool: 'Write', file, diff: diffLines(before ?? '', e.content), note: before === null ? 'new file' : 'rewrite' }]
110 }
111 return []
112}
113
114// Write の前のファイルの中身。ないか読めなければ null(新しいファイルとして扱う)
115async function readBefore($, path) {
116 try {
117 return path && (await $.fs.exists(path)) ? await $.fs.read(path) : null
118 } catch {
119 return null
120 }
121}
122
123export function hintText(n) {
124 return `▶ Replay: ${n} edit${n === 1 ? '' : 's'} (press r)`
125}
126
127async function openReplay($) {
128 const { steps } = await read($, replayAtom)
129 if (!steps.length) return false
130 await update($, replayAtom, (s) => ({ ...s, index: 0 }))
131 view.isOpen = true
132 // ペインはほかにキーボードを持つものがないときだけキーを受け取る。先に帯を消してから開く
133 $.ui.invalidate('ui.render')
134 await $.clock.sleep(200)
135 const rows = Math.min(MAX_DIFF_LINES + 8, 22)
136 const placed = await $.ui.open({ id: PANE_ID, title: 'Replay Theater', focus: true, closeOnEscape: true, rows, columns: 58 })
137 // ペインを置く場所がない(狭いターミナル)ときは帯に描く
138 view.inBand = placed?.isPlaced === false
139 $.ui.invalidate('ui.render')
140 return true
141}
142
143// 1 ステップずつ見る画面。ペインに描くか、ペインを置けないときは帯に描く
144async function replayView($, e, inBand) {
145 const { Box, Text, Button } = $.ui.resolve(e)
146 const { steps, index } = await read($, replayAtom)
147 const maxDiff = inBand ? Math.max(3, Math.min(MAX_DIFF_LINES, (e.props.maxRows || 20) - 7)) : MAX_DIFF_LINES
148 const total = steps.length
149 if (!total) return Text({ dimColor: true, children: 'No edits to replay.' })
150 const k = Math.max(0, Math.min(index, total - 1))
151 const step = steps[k]
152 const width = Math.max(40, (e.props.bodyColumns || 100) - 4)
153 const { add, del } = countChanges(step.diff)
154 const shown = step.diff.slice(0, maxDiff)
155
156 const diffRows = shown.map((l, n) => {
157 const color = l.op === '+' ? 'green' : l.op === '-' ? 'red' : undefined
158 const line = `${l.op === '~' ? ' ' : l.op} ${l.t}`.slice(0, width)
159 return Text({ key: `d${n}`, color, dimColor: l.op === ' ' || l.op === '~', wrap: 'truncate-end', children: line })
160 })
161 if (step.diff.length > maxDiff) diffRows.push(Text({ key: 'more', dimColor: true, children: ` … ${step.diff.length - maxDiff} more lines` }))
162 if (!shown.length) diffRows.push(Text({ key: 'empty', dimColor: true, children: ' (no line changes)' }))
163
164 // ステップの並び。今のステップを反転表示する
165 const strip = steps.map((s, n) => Text({ key: `s${n}`, inverse: n === k, color: n === k ? 'cyan' : undefined, dimColor: n !== k, children: ` ${n + 1} ` }))
166
167 const go = (to) => update($, replayAtom, (s) => ({ ...s, index: Math.max(0, Math.min(to, s.steps.length - 1)) }))
168 const close = () => {
169 view.isOpen = false
170 if (!view.inBand) $.ui.close({ id: PANE_ID })
171 view.inBand = false
172 $.ui.invalidate('ui.render')
173 }
174
175 return Box({
176 flexDirection: 'column',
177 borderStyle: 'round',
178 borderColor: 'magenta',
179 paddingX: 1,
180 children: [
181 Box({
182 flexDirection: 'row',
183 justifyContent: 'space-between',
184 children: [Text({ bold: true, color: 'magenta', children: '▶ Replay Theater' }), Text({ bold: true, children: `step ${k + 1} of ${total}` })],
185 }),
186 Box({ flexDirection: 'row', children: strip }),
187 Text({ bold: true, color: 'cyan', wrap: 'truncate-start', children: step.file }),
188 Box({
189 flexDirection: 'row',
190 gap: 2,
191 children: [
192 Text({ dimColor: true, children: `${step.tool}${step.note ? ' · ' + step.note : ''}` }),
193 Text({ color: 'green', children: `+${add}` }),
194 Text({ color: 'red', children: `-${del}` }),
195 ],
196 }),
197 Box({ flexDirection: 'column', marginTop: 1, children: diffRows }),
198 Box({
199 flexDirection: 'row',
200 gap: 2,
201 marginTop: 1,
202 children: [
203 Button({ key: 'prev', label: '◀ Prev', hotkey: 'p', onPress: () => go(k - 1) }),
204 Button({ key: 'next', label: 'Next ▶', hotkey: 'n', autoFocus: true, onPress: () => go(k + 1) }),
205 Button({ key: 'close', label: 'Close', hotkey: 'c', onPress: close }),
206 ],
207 }),
208 ],
209 })
210}
211
212export function register(on) {
213 on('session.start', async ($, e, next) => {
214 const r = await next(e)
215 await $.command.register({ name: 'replay', description: 'Replay Theater: step through the last turn\'s file edits', immediate: true })
216 return r
217 })
218
219 on('command.run', { command: 'replay' }, async ($) => {
220 const opened = await openReplay($)
221 const { steps } = await read($, replayAtom)
222 return { text: opened ? `Replay Theater: ${steps.length} edits` : 'Replay Theater: no edits in the last turn.' }
223 })
224
225 // 編集を記録して、そのまま実行させる。記録が失敗しても編集は止めない。成功した編集だけ残す
226 on('tool.call', async ($, e, next) => {
227 if (!EDIT_TOOLS.has(e.tool)) return next(e)
228 let steps = []
229 try {
230 const before = e.tool === 'Write' ? await readBefore($, e.file_path) : null
231 steps = stepsFor(e, await $.session.cwd(), before)
232 } catch {
233 steps = []
234 }
235 const r = await next(e)
236 if (r && !r.deny && !r.isError) pending.push(...steps)
237 return r
238 })
239
240 on('turn.start', async ($, e, next) => {
241 if (!e.agentId) pending = []
242 return next(e)
243 })
244
245 // メインのターンが終わったら、そのターンの編集を replay にする。編集のないターンでは前の replay を残す
246 on('turn.complete', async ($, e, next) => {
247 const r = await next(e)
248 if (e.agentId || !pending.length) return r
249 const steps = pending
250 pending = []
251 await update($, replayAtom, () => ({ steps, index: 0 }))
252 return r
253 })
254
255 // プロンプトの上: ヒントとペインを開くボタン
256 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
257 if (view.isOpen && view.inBand) return replayView($, e, true)
258 const { steps } = await read($, replayAtom)
259 if (!steps.length || view.isOpen || e.props.hasSurvey) return next(e)
260 const { Box, Text, Button } = $.ui.resolve(e)
261 const line = Box({
262 flexDirection: 'row',
263 gap: 2,
264 paddingX: 1,
265 children: [
266 Text({ color: 'magenta', bold: true, children: hintText(steps.length) }),
267 Button({ key: 'open-replay', label: 'Replay', hotkey: 'r', onPress: () => openReplay($) }),
268 ],
269 })
270 // 他の mod が帯に描くものも残す
271 const rest = await next(e)
272 return rest ? Box({ flexDirection: 'column', children: [line, rest] }) : line
273 })
274
275 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
276 if (e.requestId !== PANE_ID) return next(e)
277 return replayView($, e, false)
278 })
279
280 // Esc などでペインが閉じられたら、こちらの状態も合わせる
281 on('ui.close', async ($, e, next) => {
282 if (e.id === PANE_ID) view.isOpen = false
283 $.ui.invalidate('ui.render')
284 return next(e)
285 })
286}
287types/index.d.ts 17 lines1declare module 'claude-code' {
2 interface PluginState {
3 'replay-theater': {
4 // 直前に編集があったターンの編集(1 件 = 1 ステップ)と、ペインで見ている位置(0 始まり)
5 replay: {
6 steps: Array<{
7 tool: string
8 file: string
9 note: string
10 diff: Array<{ op: string; t: string }>
11 }>
12 index: number
13 }
14 }
15 }
16}
17