A file panel that shimmers on files Claude is reading or editing and turns them green once committed.

Eight mods for Claude Code (terminal and the desktop app's Code tab). Requires Claude Code 2.1.287 or newer.
| Mod | What it does | Commands |
|---|---|---|
| savvy-progress | Progress bar above the prompt; an Agents side panel with a pixel crab per subagent showing model, effort, task progress, context, tokens and estimated cost | /savvy, /crab-style <keyword> <color> <hat> |
| tool-skins | Themed icons and colors for tool calls (Tokyo Night, Nord, Dracula, Gruvbox, Solarized, Catppuccin, Rose Pine) | /skin <theme> |
| you-should-know | After a long reply, a Haiku side agent flags costs, risks and actions you might miss | /ysk on, /ysk off, /ysk focus <text> |
| file-tree | A panel showing which files Claude is reading or editing; turns green once committed | /files |
| cache-tax | Warns before a prompt reloads a cold prompt cache, with a token and cost estimate | /keepwarm, /cachetax ttl <minutes> |
| blast-radius | Stops rm -r, Remove-Item -Recurse, git clean -f, git reset --hard and similar, lists the files at stake, and waits for Proceed or Cancel | automatic |
| reflect | Spots corrections in your prompts and offers to save them as rules in CLAUDE.md | /reflect <rule> |
| terminal-browser | A text-mode browser pane; Claude can open pages there too | /browse <url or path> |
Cost figures are estimates at API list prices. you-should-know and reflect make small Haiku calls, which count toward your usage.
At a Claude Code prompt in a terminal:
/plugin install savvy-progress --marketplace Isaacg1000/isaac-mods
Answer y to add the marketplace, then pick the user scope. Repeat with each mod name you want.
Or from a shell, all at once:
claude plugin marketplace add Isaacg1000/isaac-mods
claude plugin install savvy-progress@isaac-mods --scope user
claude plugin install tool-skins@isaac-mods --scope user
claude plugin install you-should-know@isaac-mods --scope user
claude plugin install file-tree@isaac-mods --scope user
claude plugin install cache-tax@isaac-mods --scope user
claude plugin install blast-radius@isaac-mods --scope user
claude plugin install reflect@isaac-mods --scope user
claude plugin install terminal-browser@isaac-mods --scope user
Each mod lives in plugins/<name>/: hooks/register.tsx is the hooks module, types/index.d.ts its state contract. Check and test one with:
claude plugin validate plugins/<name>
claude plugin test plugins/<name>
When this folder is added as a local marketplace (claude plugin marketplace add <this folder>), edits are picked up with /reload-plugins.
hooks/register.tsx 201 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { FileStatus, TrackedFile } from '../types'
5
6const PANE = 'file-tree'
7const files = atom({ plugin: 'file-tree', key: 'files' } as const, [] as TrackedFile[])
8const root = atom({ plugin: 'file-tree', key: 'root' } as const, '')
9
10const norm = (p: string) => p.replace(/\\/g, '/')
11
12function relative(path: string, base: string): string {
13 const p = norm(path)
14 const b = norm(base).replace(/\/$/, '')
15 return b && p.toLowerCase().startsWith(b.toLowerCase() + '/') ? p.slice(b.length + 1) : p
16}
17
18type Node = { name: string; dirs: Map<string, Node>; files: TrackedFile[] }
19
20function buildTree(list: TrackedFile[], base: string): Node {
21 const top: Node = { name: '', dirs: new Map(), files: [] }
22 for (const f of list) {
23 const parts = relative(f.path, base).split('/').filter(Boolean)
24 let node = top
25 for (const dir of parts.slice(0, -1)) {
26 let child = node.dirs.get(dir)
27 if (!child) node.dirs.set(dir, (child = { name: dir, dirs: new Map(), files: [] }))
28 node = child
29 }
30 node.files.push(f)
31 }
32 return top
33}
34
35// Collapse single-child directory chains ("src/lib/utils/").
36function flatten(node: Node, depth: number, out: Array<{ depth: number; dir?: string; file?: TrackedFile }>) {
37 for (const [, child] of [...node.dirs].sort(([a], [b]) => a.localeCompare(b))) {
38 let name = child.name
39 let cur = child
40 while (cur.files.length === 0 && cur.dirs.size === 1) {
41 const [only] = [...cur.dirs.values()]
42 name += '/' + only!.name
43 cur = only!
44 }
45 out.push({ depth, dir: name })
46 flatten(cur, depth + 1, out)
47 }
48 for (const f of [...node.files].sort((a, b) => a.path.localeCompare(b.path))) out.push({ depth, file: f })
49}
50
51const isActive = (s: FileStatus) => s === 'reading' || s === 'editing'
52
53let ticker: { cancel: () => void } | undefined
54
55async function touch($: EngineInterface, path: string, status: FileStatus) {
56 const p = norm(path)
57 await update($, files, list => {
58 const found = list.find(f => f.path.toLowerCase() === p.toLowerCase())
59 if (!found) return [...list, { path: p, status, wasCreated: false, touches: 1 }].slice(-300)
60 return list.map(f => {
61 if (f !== found) return f
62 // A file once edited stays edited after a later read.
63 const keep = (status === 'read' || status === 'reading') && (f.status === 'edited' || f.status === 'created')
64 return { ...f, status: keep && status === 'read' ? f.status : status, touches: f.touches + 1 }
65 })
66 })
67 if (isActive(status) && !ticker) ticker = $.clock.every(150, () => $.ui.invalidate('ui.render'))
68 if (!isActive(status) && !(await read($, files)).some(f => isActive(f.status))) {
69 ticker?.cancel()
70 ticker = undefined
71 }
72}
73
74export const register: Register = on => {
75 on('session.start', async ($, e, next) => {
76 const cwd = await $.session.cwd()
77 await update($, root, () => cwd)
78 await $.command.register({ name: 'files', description: 'Open the File Tree panel' })
79 return next(e)
80 })
81
82 on('command.run', { command: 'files' }, async $ => {
83 await $.ui.open({ id: PANE, title: 'Files' })
84 return { text: 'File Tree opened.' }
85 })
86
87 on('tool.call', async ($, e, next) => {
88 let path: string | undefined
89 let kind: 'read' | 'edit' | 'write' | undefined
90 if (e.tool === 'Read') (path = e.file_path), (kind = 'read')
91 else if (e.tool === 'Edit') (path = e.file_path), (kind = 'edit')
92 else if (e.tool === 'Write') (path = e.file_path), (kind = 'write')
93 else if (e.tool === 'NotebookEdit') (path = (e as { notebook_path?: string }).notebook_path), (kind = 'edit')
94
95 if (path && kind) {
96 let isNew = false
97 if (kind === 'write') isNew = !(await $.fs.exists(path).catch(() => true))
98 if ((await read($, files)).length === 0) void $.ui.open({ id: PANE, title: 'Files' })
99 await touch($, path, kind === 'read' ? 'reading' : 'editing')
100 const ran = await next(e)
101 const failed = ran.deny !== undefined || ran.isError === true
102 const p = norm(path)
103 if (kind === 'read') await touch($, path, 'read')
104 else if (failed) await touch($, path, 'read')
105 else {
106 await touch($, path, isNew ? 'created' : 'edited')
107 if (isNew) await update($, files, list => list.map(f => (f.path === p ? { ...f, wasCreated: true } : f)))
108 }
109 return ran
110 }
111
112 if ((e.tool === 'Bash' || e.tool === 'PowerShell') && /\bgit\b[^\n|;&]*\bcommit\b/.test(e.command)) {
113 const ran = await next(e)
114 if (ran.deny === undefined && !ran.isError) {
115 const base = await read($, root)
116 const status = await $.process.run(['git', 'status', '--porcelain'], { cwd: base, timeoutMs: 10000 }).catch(() => undefined)
117 const dirty = new Set(
118 (status?.exitCode === 0 ? status.stdout : '')
119 .split('\n')
120 .map(l => l.slice(3).trim().replace(/^"|"$/g, '').toLowerCase())
121 .filter(Boolean),
122 )
123 await update($, files, list =>
124 list.map(f =>
125 (f.status === 'edited' || f.status === 'created') && !dirty.has(relative(f.path, base).toLowerCase())
126 ? { ...f, status: 'committed' as const }
127 : f,
128 ),
129 )
130 }
131 return ran
132 }
133 return next(e)
134 })
135
136 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
137 const { Box, Text, Button } = $.ui.resolve(e)
138 const list = await read($, files)
139 const base = await read($, root)
140 const now = await $.clock.now()
141 const frame = Math.floor(now / 150)
142
143 if (list.length === 0) return <Text dimColor>No files touched yet.</Text>
144
145 const rows: Array<{ depth: number; dir?: string; file?: TrackedFile }> = []
146 flatten(buildTree(list, base), 0, rows)
147 const counts = {
148 edited: list.filter(f => f.status === 'edited' || f.status === 'created' || f.status === 'editing').length,
149 committed: list.filter(f => f.status === 'committed').length,
150 read: list.filter(f => f.status === 'read' || f.status === 'reading').length,
151 }
152 const shimmer = ['#D97757', '#E89B7F', '#F5C4AE', '#E89B7F']
153 const spin = ['◐', '◓', '◑', '◒']
154
155 return (
156 <Box flexDirection="column">
157 <Box flexDirection="row" justifyContent="space-between">
158 <Text dimColor>
159 {counts.edited} changed · {counts.committed} committed · {counts.read} read
160 </Text>
161 <Button key="clear" label="Clear" plain onPress={() => update($, files, () => [])} />
162 </Box>
163 {rows.map(row => {
164 const pad = ' '.repeat(row.depth)
165 if (row.dir) {
166 return (
167 <Text dimColor wrap="truncate-end">
168 {pad}▾ {row.dir}/
169 </Text>
170 )
171 }
172 const f = row.file!
173 const name = f.path.split('/').pop() ?? f.path
174 if (isActive(f.status)) {
175 const c = shimmer[frame % shimmer.length]!
176 return (
177 <Text color={c} bold wrap="truncate-end">
178 {pad}
179 {spin[frame % spin.length]} {name} {f.status === 'editing' ? '(editing)' : '(reading)'}
180 </Text>
181 )
182 }
183 const look: Record<string, [string, string, boolean]> = {
184 read: ['○', 'subtle', true],
185 edited: ['●', 'warning', false],
186 created: ['✚', 'warning', false],
187 committed: ['✓', 'success', false],
188 }
189 const [glyph, color, dim] = look[f.status] ?? ['○', 'subtle', true]
190 return (
191 <Text color={color} dimColor={dim} wrap="truncate-end">
192 {pad}
193 {glyph} {name}
194 </Text>
195 )
196 })}
197 </Box>
198 )
199 })
200}
201types/index.d.ts 10 lines1export type FileStatus = 'reading' | 'editing' | 'read' | 'edited' | 'created' | 'committed'
2
3export type TrackedFile = { path: string; status: FileStatus; wasCreated: boolean; touches: number }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'file-tree': { files: TrackedFile[]; root: string }
8 }
9}
10