Seitenpanel, das zeigt, welche Dateien Claude gerade liest und ändert

Ein Claude-Code-Mod, der rechts neben dem Chat ein Datei-Panel öffnet. Jede Datei, die Claude liest, erscheint dort mit einem Auge, jede Datei, die gerade bearbeitet wird, ist markiert, und fertig geänderte Dateien werden grün mit einem Haken. So siehst du jederzeit auf einen Blick, was Claude in deinem Projekt anfasst, ohne die Chat-Antworten durchlesen zu müssen. Öffnen mit /dateibaum.
Im Terminal in Claude Code eintippen:
/plugin install dateibaum --marketplace boonpflug/claude-mods-deutsch
Danach y zum Hinzufügen des Marketplace und mit Enter den Bereich „user“ wählen. Der Mod läuft sofort.
Aus der Shell geht es auch: claude plugin install dateibaum --marketplace boonpflug/claude-mods-deutsch
hooks/register.tsx 120 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { FileState, Touched } from '../types'
5
6const PANE = 'dateibaum'
7const files = atom({ plugin: 'dateibaum', key: 'files' } as const, [])
8
9const ICON: Record<FileState, string> = { gelesen: '👀', läuft: '✏️ ', geändert: '✅' }
10const COLOR: Record<FileState, string> = { gelesen: 'gray', läuft: 'yellow', geändert: 'green' }
11
12async function mark($: EngineInterface, absolute: string, state: FileState) {
13 const cwd = await $.session.cwd()
14 const path = absolute.startsWith(cwd + '/') ? absolute.slice(cwd.length + 1) : absolute
15 await update($, files, list => {
16 const known = list.find(f => f.path === path)
17 // eine bereits geänderte Datei bleibt grün, auch wenn Claude sie danach nur liest
18 if (known && known.state === 'geändert' && state === 'gelesen') return list
19 const next: Touched = { path, state }
20 return known ? list.map(f => (f.path === path ? next : f)) : [...list, next]
21 })
22}
23
24const SKIP = new Set(['.git', 'node_modules', '.claude', 'dist', 'build'])
25
26// Änderungszeiten aller Projektdateien, damit auch Änderungen per Shell-Befehl auffallen
27async function snapshot($: EngineInterface, dir: string, out: Map<string, number>, budget = { left: 2000 }) {
28 for (const entry of await $.fs.list(dir)) {
29 if (budget.left-- <= 0) return out
30 const path = `${dir}/${entry.name}`
31 if (entry.kind === 'dir' && !SKIP.has(entry.name)) await snapshot($, path, out, budget)
32 if (entry.kind === 'file') out.set(path, entry.mtimeMs)
33 }
34 return out
35}
36
37// Dateien, die ein Lesebefehl wie `cat a b` oder `head -n 5 x` anfasst
38async function readsOf($: EngineInterface, command: string): Promise<string[]> {
39 const cwd = await $.session.cwd()
40 const words = command
41 .split(/&&|;|\||\n/)
42 .filter(part => /^\s*(cat|head|tail|less|wc|sed\s+-n)\b/.test(part))
43 .flatMap(part => part.trim().split(/\s+/).slice(1))
44 .filter(word => !word.startsWith('-') && !/^['"]?\d/.test(word))
45 const found: string[] = []
46 for (const word of words) {
47 const path = word.startsWith('/') ? word : `${cwd}/${word}`
48 if ((await $.fs.exists(path)) && (await $.fs.stat(path)).kind === 'file') found.push(path)
49 }
50 return found
51}
52
53export const register: Register = on => {
54 on('session.start', async ($, e, next) => {
55 await $.command.register({ name: 'dateibaum', description: 'Datei-Panel öffnen' })
56 void $.ui.open({ id: PANE, title: 'Dateibaum' })
57 return next(e)
58 })
59
60 on('command.run', { command: 'dateibaum' }, async $ => {
61 await $.ui.open({ id: PANE, title: 'Dateibaum' })
62 return { text: 'Dateibaum geöffnet.' }
63 })
64
65 on('tool.call', { tool: 'Read' }, async ($, e, next) => {
66 await mark($, e.file_path, 'gelesen')
67 return next(e)
68 })
69
70 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
71 await mark($, e.file_path, 'läuft')
72 const ran = await next(e)
73 await mark($, e.file_path, ran.isError ? 'gelesen' : 'geändert')
74 return ran
75 })
76
77 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
78 await mark($, e.file_path, 'läuft')
79 const ran = await next(e)
80 await mark($, e.file_path, ran.isError ? 'gelesen' : 'geändert')
81 return ran
82 })
83
84 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
85 const cwd = await $.session.cwd()
86 for (const path of await readsOf($, e.command)) await mark($, path, 'gelesen')
87 const before = await snapshot($, cwd, new Map())
88 const ran = await next(e)
89 const after = await snapshot($, cwd, new Map())
90 for (const [path, mtime] of after) {
91 if (before.get(path) !== mtime) await mark($, path, 'geändert')
92 }
93 return ran
94 })
95
96 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
97 const { Box, Text } = $.ui.resolve(e)
98 const list = [...(await read($, files))].sort((a, b) => a.path.localeCompare(b.path))
99 const changed = list.filter(f => f.state === 'geändert').length
100
101 return (
102 <Box flexDirection="column">
103 <Text bold>📁 {list.length} Dateien angefasst · {changed} geändert</Text>
104 <Text> </Text>
105 {list.length === 0 && <Text dimColor>Noch nichts angefasst.</Text>}
106 {list.map(f => {
107 const depth = f.path.split('/').length - 1
108 const name = f.path.split('/').pop() ?? f.path
109 const folder = depth > 0 ? f.path.slice(0, f.path.length - name.length) : ''
110 return (
111 <Text color={COLOR[f.state]} bold={f.state === 'läuft'}>
112 {ICON[f.state]} <Text dimColor>{folder}</Text>{name}
113 </Text>
114 )
115 })}
116 </Box>
117 )
118 })
119}
120types/index.d.ts 9 lines1export type FileState = 'gelesen' | 'läuft' | 'geändert'
2export type Touched = { path: string; state: FileState }
3
4declare module 'claude-code' {
5 interface PluginState {
6 dateibaum: { files: Touched[] }
7 }
8}
9