Pane that shows each code change Claude makes as a diff with a beginner-friendly explanation of why

Claude Code mod 모음. 이 저장소 자체가 마켓플레이스(.claude-plugin/marketplace.json)라서 누구나 아래 방법으로 설치할 수 있다.
| Mod | 설명 |
|---|---|
context-bars | 컨텍스트 윈도우를 카테고리별 비례 막대 하나로 프롬프트 위에 표시 |
token-weather | 컨텍스트 윈도우 사용량을 날씨 아이콘·퍼센트·최근 턴 차트로 프롬프트 위에 표시 |
y-change | Claude가 코드를 수정할 때마다 diff와 변경 이유(처음 보는 언어의 문법 설명 포함)를 오른쪽 pane에 표시 (기본은 숨김, /y-change로 열기/닫기) |
enhance-prompt | 입력한 프롬프트를 AI가 더 잘 알아듣도록 [THE JOB]/[THE WHY]/[GUARDRAILS]/[DONE MEAN] 구조로 개선해 입력창 위에 보여주고(불명확한 부분은 먼저 입력창 위 질문으로 되묻고 답하면 반복, 최대 3개), [허용]하면 개선본을, [거부]하면 원본을 전송. enhance-prompt: ON/OFF 버튼(데스크탑 입력창 위)을 눌러, 또는 /enhance-prompt로 켜기/끄기 |
task-progress | Claude의 작업 목록(TodoWrite·TaskCreate/TaskUpdate) 진행률을 막대와 완료/전체, 진행 중인 항목 이름으로 프롬프트 위(스피너 아래)에 표시 |
mod-switch | 설치된 mod 목록을 오른쪽 pane에 토글 스위치와 함께 표시하고, 스위치를 누르면 해당 mod를 켜거나 끔 (기본은 숨김, /mod-switch로 열기/닫기) |
터미널의 Claude Code 세션 프롬프트에서 입력한다. 마켓플레이스 등록과 설치가 한 번에 된다.
/plugin install context-bars --marketplace nalpari/claude-mods
/plugin install token-weather --marketplace nalpari/claude-mods
/plugin install y-change --marketplace nalpari/claude-mods
/plugin install task-progress --marketplace nalpari/claude-mods
/plugin install enhance-prompt --marketplace nalpari/claude-mods
/plugin install mod-switch --marketplace nalpari/claude-mods
처음이면 Add marketplace? 에 y 로 답하고 scope(user 권장)를 고르면 Installed <mod>. Plugin is now active. 가 뜬다. 재시작이나 리로드 없이 바로 적용되고, user scope 는 이후 세션에도 유지된다. 마켓플레이스를 이미 추가했다면 확인 질문은 건너뛴다.
마켓플레이스를 먼저 등록하고 나중에 골라 설치해도 된다.
/plugin marketplace add nalpari/claude-mods
/plugin install context-bars@claude-mods
셸에서는 claude plugin marketplace add nalpari/claude-mods 와 claude plugin install context-bars@claude-mods --scope user 로 같은 일을 한다.
claude plugin marketplace update claude-mods # 마켓플레이스 최신화
claude plugin update context-bars@claude-mods # mod 업데이트
claude plugin list # 설치 확인
claude plugin uninstall context-bars # 제거
git clone https://github.com/nalpari/claude-mods.git
cd claude-mods
# 이번 세션에서만 시험
claude --plugin-dir ./context-bars
# 로컬 마켓플레이스로 설치
claude plugin marketplace add ./
claude plugin install context-bars@claude-mods --scope user
token-weather/README.md 기준)./plugin install 은 터미널 전용이다. 데스크톱 앱 Code 탭에서는 쓸 수 없지만, 터미널에서 user scope 로 설치하면 로컬 세션에서도 로드된다.hooks/register.tsx 132 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Change } from '../types'
5
6const PANE = 'y-change'
7const TITLE = '변경 이유'
8// changes kept, and diff rows drawn per change
9const MAX_CHANGES = 30
10const MAX_ROWS = 80
11
12const changes = atom({ plugin: 'y-change', key: 'changes' } as const, [])
13
14type Hunk = { oldStart: number; newStart: number; lines: string[] }
15
16// the tool's patch as unified hunks, cut to MAX_ROWS; a cut hunk gets its counts again so it still parses
17const toDiff = (hunks: readonly Hunk[]) => {
18 let room = MAX_ROWS
19 const out: string[] = []
20
21 for (const hunk of hunks) {
22 if (room <= 0) break
23 const lines = hunk.lines.slice(0, room)
24 room -= lines.length
25 const count = (mark: string) => lines.filter(l => l[0] === ' ' || l[0] === mark).length
26 out.push(`@@ -${hunk.oldStart},${count('-')} +${hunk.newStart},${count('+')} @@`, ...lines)
27 }
28
29 const total = hunks.reduce((n, hunk) => n + hunk.lines.length, 0)
30
31 return { diff: out.join('\n'), more: total - (MAX_ROWS - room) }
32}
33
34// asked over the session's own transcript, so the model answers from why it made the change
35const ask = (pending: readonly Change[]) => `[y-change] For each code change below, which you made earlier in this conversation, explain it to a reader who has never used this programming language. Write in the language the user has been writing in.
36
37For each change cover:
381. What changed: in plain words, line by line where that helps.
392. Why: the reason for this change in this conversation (the request, the bug, the constraint that led to it). If the conversation does not show the reason, say so instead of guessing.
403. Language notes: each piece of syntax, keyword or library call in the diff a newcomer to the language would not know, in a sentence each.
41
42Start each change's explanation with a line holding exactly \`### <number>\`, then markdown. Write nothing before the first such line.
43
44${pending.map((c, i) => `### ${i + 1}: ${c.file}\n\`\`\`diff\n${c.diff}\n\`\`\``).join('\n\n')}`
45
46// '### 1\n…\n### 2\n…' -> the texts by their number
47const parse = (text: string) => {
48 const parts = text.split(/^### (\d+).*$/m)
49 const whys = new Map<number, string>()
50 for (let i = 1; i < parts.length; i += 2) whys.set(Number(parts[i]), (parts[i + 1] ?? '').trim())
51
52 return whys
53}
54
55export const register: Register = on => {
56 on('session.start', async ($, e, next) => {
57 await $.command.register({ name: 'y-change', description: '코드 변경 이유 pane 열기/닫기' })
58
59 return next(e)
60 })
61
62 // a toggle; a pane waiting undrawn (opened unasked in a narrow terminal) is opened, not closed
63 on('command.run', { command: 'y-change' }, async $ => {
64 const isShown = (await $.ui.panes()).some(pane => pane.id === PANE && pane.isPlaced)
65 if (isShown) {
66 await $.ui.close({ id: PANE })
67
68 return { text: '변경 이유 pane을 닫았습니다.' }
69 }
70 await $.ui.open({ id: PANE, title: TITLE })
71
72 return { text: '변경 이유 pane을 열었습니다.' }
73 })
74
75 on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
76 const ran = await next(e)
77 if (ran.deny !== undefined || ran.isError === true || ran.result.staged) return ran
78
79 const { diff, more } = toDiff(ran.result.structuredPatch)
80 if (diff === '') return ran
81
82 const change: Change = { id: e.tool_use_id, file: ran.result.gitDiff?.filename ?? ran.result.filePath, diff, more }
83 await update($, changes, list => [...list, change].slice(-MAX_CHANGES))
84
85 return ran
86 })
87
88 // one question per turn for everything it changed; what stays unexplained is asked again next turn
89 on('turn.complete', async ($, e, next) => {
90 const done = await next(e)
91 if (e.agentId !== undefined) return done
92
93 const pending = (await read($, changes)).filter(c => c.why === undefined)
94 if (pending.length === 0) return done
95
96 const asked = await $.model.fork({ prompt: ask(pending) })
97 if (!asked.isAnswered) {
98 $.ui.toast(`y-change: 변경 이유를 만들지 못했습니다 (${asked.reason})`)
99
100 return done
101 }
102
103 const whys = parse(asked.text)
104 const byId = new Map(pending.map((c, i) => [c.id, whys.get(i + 1)]))
105 await update($, changes, list => list.map(c => (byId.get(c.id) ? { ...c, why: byId.get(c.id) } : c)))
106
107 return done
108 })
109
110 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
111 const { Box, Text, Code, Markdown } = $.ui.resolve(e)
112 const list = await read($, changes)
113
114 return (
115 <Box flexDirection="column">
116 {list.length === 0 && <Text dimColor>아직 변경이 없습니다. Claude가 파일을 수정하면 diff와 변경 이유가 여기에 표시됩니다.</Text>}
117 {/* newest first, so the latest change shows without scrolling */}
118 {[...list].reverse().map(c => (
119 <Box flexDirection="column" marginBottom={1}>
120 <Text bold>{c.file}</Text>
121 <Code source={c.diff} path={c.file} format="diff" />
122 {c.more > 0 && <Text dimColor>… {c.more}줄 더</Text>}
123 {c.why === undefined
124 ? <Text dimColor>이번 턴이 끝나면 변경 이유를 설명합니다.</Text>
125 : <Markdown text={c.why} />}
126 </Box>
127 ))}
128 </Box>
129 )
130 })
131}
132types/index.d.ts 10 lines1// one Edit or Write Claude made: `diff` is unified hunks cut to a few rows, `more` the rows cut,
2// `why` the explanation, absent until the turn that made the change ends
3export type Change = { id: string; file: string; diff: string; more: number; why?: string }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'y-change': { changes: Change[] }
8 }
9}
10