SLOPSHOPPER

context-files

Pane listing the CLAUDE.md, memory and other instruction files this session loaded, by tier, with size.

newbandcommandprompt
v0.1.0MITupdated 2026-10-26namtrdev/claude-mods/context-files
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-files
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /context-files ⎿ context-files: Context files band collapsed. [ ▸ Expand ] CONTEXT no instruction files loaded yet ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
[ ▸ Expand ] CONTEXT no instruction files loaded yet
README

Context Files

A band above the prompt that lists the CLAUDE.md, memory and other instruction files the session loaded, grouped by tier with an estimated token count, in collapsed or expanded form.

Install

/plugin marketplace add namtrdev/claude-mods
/plugin install context-files@jackie-mods

Use

Commands: /context-files.

Built for Claude Code mods (hook modules). Nothing here runs outside your machine: the mod reads session events and draws its own interface.

Options

You set these when you install the mod, and can change them later in the Claude Code config menu.

  • Start expanded (startExpanded, default true): Open the band with the full file list; off starts it collapsed to one line.

License

MIT, Copyright (c) 2026 Jackie.

Icons: Lucide (ISC). See THIRD_PARTY_NOTICES.md.

Source 3 files
hooks/register.tsx 162 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { icon } from './icons'
5
6import type { Loaded } from '../types'
7
8const files = atom({ plugin: 'context-files', key: 'files' } as const, [])
9const isExpanded = atom({ plugin: 'context-files', key: 'isExpanded' } as const, true)
10
11const ORDER = ['managed', 'user', 'project', 'local', 'memory']
12const COLOR: Record<string, string> = {
13  managed: '#ff7a7a',
14  user: '#5fe3ff',
15  project: '#7dff9a',
16  local: '#ffe066',
17  memory: '#ff8ae8',
18}
19
20const tokens = (chars: number) => {
21  const n = Math.round(chars / 4)
22  return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
23}
24
25export const register: Register = (on, options) => {
26  on('session.start', async ($, e, next) => {
27    await $.command.register({
28      name: 'context-files',
29      description: 'Expand or collapse the loaded-instruction-files band',
30    })
31    await update($, isExpanded, () => options.startExpanded !== false)
32    $.ui.invalidate('prompt.context')
33
34    return next(e)
35  })
36
37  on('command.run', { command: 'context-files' }, async $ => {
38    const was = await read($, isExpanded)
39    await update($, isExpanded, () => !was)
40
41    return { text: was ? 'Context files band collapsed.' : 'Context files band expanded.' }
42  })
43
44  on('prompt.context', async ($, e, next) => {
45    const out = await next(e)
46    const list = out.instructionFiles ?? e.instructionFiles ?? []
47    const loaded: Loaded[] = list.map(one => ({
48      path: one.path,
49      kind: one.kind,
50      chars: one.content.length,
51      parent: one.parent,
52    }))
53    await update($, files, () => loaded)
54
55    return out
56  })
57
58  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
59    if (e.props.hasSurvey) {
60      return next(e)
61    }
62
63    const ui = $.ui.resolve(e)
64    const { Box, Button, Text } = ui
65    const hasSvg = ui.Svg !== undefined
66    const [list, open] = [await read($, files), await read($, isExpanded)]
67    const total = list.reduce((sum, one) => sum + one.chars, 0)
68    const cols = e.viewport?.columns ?? 80
69    const groups = ORDER.map(kind => {
70      const group = list.filter(one => one.kind === kind)
71      return { kind, count: group.length, chars: group.reduce((a, one) => a + one.chars, 0) }
72    }).filter(one => one.count > 0)
73    const pct = (part: number) => (total === 0 ? 0 : Math.round((part / total) * 100))
74    const cells = (part: number, width: number) =>
75      total === 0 ? 0 : Math.max(1, Math.round((part / total) * width))
76    const stacked = (width: number) =>
77      groups.map(one => (
78        <Text color={COLOR[one.kind]}>{'▓'.repeat(cells(one.chars, width))}</Text>
79      ))
80    const summary =
81      list.length === 0 ? 'no instruction files loaded yet' : `${list.length} files · ~${tokens(total)} tok`
82
83    const toggle = (
84      <Box gap={1}>
85        {hasSvg && icon(ui, open ? 'chevron-down' : 'chevron-right', '#9ca3af', '')}
86        <Button
87          key="toggle"
88          label={hasSvg ? (open ? 'Collapse' : 'Expand  ') : open ? '▾ Collapse' : '▸ Expand  '}
89          onPress={() => update($, isExpanded, () => !open)}
90        />
91      </Box>
92    )
93
94    if (!open) {
95      return (
96        <Box gap={2}>
97          {toggle}
98          <Text bold>CONTEXT</Text>
99          <Text dimColor>{summary}</Text>
100          <Box>{stacked(Math.max(8, Math.min(24, cols - 50)))}</Box>
101        </Box>
102      )
103    }
104
105    const bar = Math.max(10, Math.min(30, cols - 30))
106
107    return (
108      <Box flexDirection="column" gap={1}>
109        <Box gap={2}>
110          {toggle}
111          <Text bold>CONTEXT</Text>
112          <Text dimColor>{summary}</Text>
113        </Box>
114        {groups.length > 0 && (
115          <Box flexDirection="column" gap={1}>
116            <Box>{stacked(Math.max(10, Math.min(60, cols - 10)))}</Box>
117            <Box gap={2}>
118              {groups.map(one => (
119                <Text color={COLOR[one.kind]}>
120                  {one.kind} {one.count} · {tokens(one.chars)}
121                </Text>
122              ))}
123            </Box>
124          </Box>
125        )}
126        {groups.map(group => (
127          <Box flexDirection="column" gap={1}>
128            <Text bold color={COLOR[group.kind]}>
129              {group.kind.toUpperCase()}
130            </Text>
131            {list
132              .filter(one => one.kind === group.kind)
133              .map(one => {
134                const name = one.path.split('/').pop() ?? one.path
135                const dir = one.path.slice(0, one.path.length - name.length).replace(/^\/Users\/[^/]+/, '~')
136                const n = cells(one.chars, bar)
137                return (
138                  <Box flexDirection="column">
139                    <Box gap={2}>
140                      <Box gap={1}>
141                        {one.parent !== undefined && icon(ui, 'corner-down-right', '#9ca3af', '↳')}
142                        {hasSvg && icon(ui, 'file-text', COLOR[group.kind], '')}
143                        <Text bold>{name}</Text>
144                      </Box>
145                      <Text dimColor>~{tokens(one.chars)} tok</Text>
146                    </Box>
147                    <Box gap={1}>
148                      <Text color={COLOR[group.kind]}>{'▓'.repeat(n)}</Text>
149                      <Text dimColor>{'░'.repeat(Math.max(0, bar - n))}</Text>
150                      <Text dimColor>{String(pct(one.chars)).padStart(3)}%</Text>
151                    </Box>
152                    <Text dimColor>{dir}</Text>
153                  </Box>
154                )
155              })}
156          </Box>
157        ))}
158      </Box>
159    )
160  })
161}
162
hooks/icons.tsx 26 lines
1// Icons from Lucide (https://lucide.dev, ISC license), lucide-static 0.469.0.
2// Svg draws on the desktop, editor and mobile surfaces; the terminal has none, so a glyph stands in.
3const PATHS = {
4  'chevron-right': '<path d="m9 18 6-6-6-6" />',
5  'chevron-down': '<path d="m6 9 6 6 6-6" />',
6  'file-text': '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /> <path d="M14 2v4a2 2 0 0 0 2 2h4" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" />',
7  'corner-down-right': '<polyline points="15 10 20 15 15 20" /> <path d="M4 4v7a4 4 0 0 0 4 4h12" />',
8}
9
10export type IconName = keyof typeof PATHS
11
12export const svg = (name: IconName, color: string, size: number) =>
13  `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${PATHS[name]}</svg>`
14
15// eslint-disable-next-line @typescript-eslint/no-explicit-any
16export type Ui = { Svg?: any; Text: any }
17
18export const icon = (ui: Ui, name: IconName, color: string, glyph: string, size = 16) => {
19  const { Svg, Text } = ui
20  return Svg === undefined ? (
21    <Text color={color}>{glyph}</Text>
22  ) : (
23    <Svg source={svg(name, color, size)} alt={name} width={size} height={size} />
24  )
25}
26
types/index.d.ts 8 lines
1export type Loaded = { path: string; kind: string; chars: number; parent?: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'context-files': { files: Loaded[]; isExpanded: boolean }
6  }
7}
8