A pane listing the files read and edited this turn, with each one's diff, in Vesuvius colors

Claude Code interface mods, collected in a private pompeitech marketplace. Just as frescoes decorated the walls of Pompeii's houses, these mods decorate Claude Code's interface.
affreschi/
├── .claude-plugin/marketplace.json # the list of mods
└── plugins/
└── <mod-name>/ # one folder (one plugin) per mod
├── .claude-plugin/plugin.json
├── hooks/
│ ├── hooks.json
│ ├── palette.ts # Vesuvius colors
│ └── register.tsx
├── types/index.d.ts # the mod's $.state contract, when it has one
└── tests/
plugins/<mod-name>/ with its files.plugins array in .claude-plugin/marketplace.json: { "name": "<mod-name>", "source": "./plugins/<mod-name>", "description": "..." }
claude plugin validate ./plugins/<mod-name>Load a mod for the current session only (it reloads when you save):
claude --plugin-dir ./plugins/<mod-name>
/plugin marketplace add pompeitech/affreschi
/plugin install <mod-name>@affreschi
| Mod | What it does | Where it shows |
|---|---|---|
vesuvius-theme | A spinner with a glowing crater and volcanic words, a stripe of color beside every tool call (magma while it runs, lava when it fails, ash once done), and the end-of-turn line as an eruption (▲ Erupted for 1m 4s) | Terminal; on the desktop, only the tool call rows |
touched-files | /touched-files opens a pane listing the files read (R) and edited (M, with +/-) this turn; press a file to see its diff. At the end of a turn a toast says how many files changed | Terminal and desktop |
context-map | A band above the prompt with the context window split by category, a percentage that turns from tuff to magma to lava past 60% and 85%, and details for the legend | Terminal and desktop |
The colors live in each mod's hooks/palette.ts (a plugin cannot import another plugin's files, so all three keep a copy). They are placeholders, to be replaced with the Vesuvius design system's.
claude plugin validate ./plugins/<mod-name>
claude plugin test ./plugins/<mod-name>hooks/register.tsx 162 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { TouchedFile } from '../types'
5import { VESUVIUS } from './palette'
6
7const PANE = 'touched-files'
8const DIFF_MAX = 40_000
9
10const files = atom({ plugin: 'touched-files', key: 'files' } as const, [])
11const selected = atom({ plugin: 'touched-files', key: 'selected' } as const, null)
12
13type Hunk = {
14 oldStart: number
15 oldLines: number
16 newStart: number
17 newLines: number
18 lines: string[]
19}
20
21// Turns Edit/Write's structured patch into unified hunks that `Code` can draw.
22export function toHunks(patch: readonly Hunk[]): { diff: string; added: number; removed: number } {
23 let added = 0
24 let removed = 0
25 const parts = patch.map(h => {
26 for (const line of h.lines) {
27 if (line.startsWith('+')) added += 1
28 else if (line.startsWith('-')) removed += 1
29 }
30
31 return [`@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@`, ...h.lines].join('\n')
32 })
33
34 return { diff: parts.join('\n'), added, removed }
35}
36
37// Adds or updates a file, moving it to the top of the list.
38export function recordTouch(list: readonly TouchedFile[], touch: Partial<TouchedFile> & { path: string }): TouchedFile[] {
39 const before = list.find(f => f.path === touch.path)
40 const base: TouchedFile = before ?? { path: touch.path, isRead: false, isEdited: false, added: 0, removed: 0, diff: '' }
41 const diff = touch.diff ? [base.diff, touch.diff].filter(Boolean).join('\n') : base.diff
42 const next: TouchedFile = {
43 path: touch.path,
44 isRead: base.isRead || Boolean(touch.isRead),
45 isEdited: base.isEdited || Boolean(touch.isEdited),
46 added: base.added + (touch.added ?? 0),
47 removed: base.removed + (touch.removed ?? 0),
48 diff: diff.length > DIFF_MAX ? diff.slice(0, DIFF_MAX) : diff,
49 }
50
51 return [next, ...list.filter(f => f.path !== touch.path)].slice(0, 200)
52}
53
54export function splitPath(path: string): { file: string; folder: string } {
55 const i = path.lastIndexOf('/')
56
57 return i < 0 ? { file: path, folder: '' } : { file: path.slice(i + 1), folder: path.slice(0, i + 1) }
58}
59
60export const register: Register = on => {
61 on('session.start', async ($, e, next) => {
62 await $.command.register({
63 name: 'touched-files',
64 description: 'Open the pane listing the files read and edited this turn',
65 immediate: true,
66 })
67
68 return next(e)
69 })
70
71 on('command.run', { command: 'touched-files' }, async $ => {
72 await $.ui.open({ id: PANE, title: 'Touched files', focus: true, closeOnEscape: true })
73
74 return {}
75 })
76
77 // Every new prompt starts a new turn: the list starts over.
78 on('prompt.submit', async ($, e, next) => {
79 await update($, files, () => [])
80 await update($, selected, () => null)
81
82 return next(e)
83 })
84
85 on('tool.call', { tool: ['Read', 'Edit', 'Write', 'NotebookEdit'] }, async ($, e, next) => {
86 const ran = await next(e)
87 if (ran.deny !== undefined || ran.isError) return ran
88
89 if (e.tool === 'Read') {
90 await update($, files, list => recordTouch(list, { path: e.file_path, isRead: true }))
91
92 return ran
93 }
94
95 const path = e.tool === 'NotebookEdit' ? e.notebook_path : e.file_path
96 const patch = (ran.result as { structuredPatch?: Hunk[] } | undefined)?.structuredPatch ?? []
97 await update($, files, list => recordTouch(list, { path, isEdited: true, ...toHunks(patch) }))
98
99 return ran
100 })
101
102 // At the end of a turn, if anything changed, a toast says the pane is there.
103 on('turn.complete', async ($, e, next) => {
104 const list = await read($, files)
105 const edited = list.filter(f => f.isEdited).length
106 if (edited > 0) {
107 $.ui.toast(`${edited} ${edited === 1 ? 'file' : 'files'} edited this turn · /touched-files`)
108 }
109
110 return next(e)
111 })
112
113 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
114 const { Box, Text, Button, Code } = $.ui.resolve(e)
115 const list = await read($, files)
116 const chosen = await read($, selected)
117 const current = list.find(f => f.path === chosen)
118 const edited = list.filter(f => f.isEdited).length
119
120 if (list.length === 0) {
121 return <Text color={VESUVIUS.ash}>No files touched this turn.</Text>
122 }
123
124 return (
125 <Box flexDirection="column" rowGap={1}>
126 <Text color={VESUVIUS.tuff}>
127 {list.length} {list.length === 1 ? 'file' : 'files'} · <Text color={VESUVIUS.magma}>{edited} edited</Text>
128 </Text>
129 <Box flexDirection="column">
130 {list.map((f, i) => {
131 const { file, folder } = splitPath(f.path)
132
133 return (
134 <Button
135 key={`file-${i}`}
136 plain
137 onPress={() => update($, selected, s => (s === f.path ? null : f.path))}
138 >
139 <Text color={f.isEdited ? VESUVIUS.lava : VESUVIUS.gulf} bold>
140 {f.isEdited ? 'M' : 'R'}
141 </Text>{' '}
142 <Text bold={f.path === chosen}>{file}</Text>{' '}
143 <Text color={VESUVIUS.ash}>{folder}</Text>
144 {f.isEdited ? ' ' : ''}
145 {f.isEdited ? <Text color={VESUVIUS.vineyard}>+{f.added}</Text> : ''}
146 {f.isEdited ? ' ' : ''}
147 {f.isEdited ? <Text color={VESUVIUS.lava}>-{f.removed}</Text> : ''}
148 </Button>
149 )
150 })}
151 </Box>
152 {current !== undefined && current.diff !== '' && (
153 <Code source={current.diff} format="diff" path={current.path} wrap="truncate-end" />
154 )}
155 {current !== undefined && current.diff === '' && (
156 <Text color={VESUVIUS.ash}>{current.isEdited ? 'No differences to show.' : 'Read only, not edited.'}</Text>
157 )}
158 </Box>
159 )
160 })
161}
162hooks/palette.ts 22 lines1// Vesuvius palette (placeholder): replace the hex values with the design system's.
2// Every mod in affreschi keeps its own copy, because a plugin cannot import another plugin's files.
3export const VESUVIUS = {
4 lava: '#E4572E',
5 magma: '#F28C28',
6 sulfur: '#F2C14E',
7 tuff: '#C9B79C',
8 ash: '#8A8D91',
9 gulf: '#2E86AB',
10 vineyard: '#7A9E4E',
11} as const
12
13// The order categories take their colors in, warmest first.
14export const SEQUENCE = [
15 VESUVIUS.lava,
16 VESUVIUS.magma,
17 VESUVIUS.sulfur,
18 VESUVIUS.tuff,
19 VESUVIUS.gulf,
20 VESUVIUS.vineyard,
21] as const
22types/index.d.ts 15 lines1export type TouchedFile = {
2 path: string
3 isRead: boolean
4 isEdited: boolean
5 added: number
6 removed: number
7 diff: string
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'touched-files': { files: TouchedFile[]; selected: string | null }
13 }
14}
15