SLOPSHOPPER

headroom-snip

Watch Headroom snip tokens off every Claude Code request: an animated band, a /headroom log and a running total.

newpanebandcommandtoaststatus
★ 74,811v0.40.0Apache-2.0updated 2026-10-08headroomlabs-ai/headroom/plugins/headroom-snip
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · headroom-snip
│ ┃ ✂ Headroom ✕ › fix the failing auth test and add an audit log call │ ┃ ✂ 0 tokens snipped on this proxy since this │ ┃ session started (0% of 0) over 0 requests ⏺ Read(src/auth.ts) │ ┃ ≈ less than a page Claude didn't have to ⎿ Read 6 lines │ ┃ reread · 0 all-time ⏺ Update(src/auth.ts) │ ┃ proxy unreachable at http://127.0.0.1:8787 ⎿ Added 2 lines, removed 1 line │ ┃ counting every client on this proxy (`headr… ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ No requests through Headroom yet. Send a │ ┃ prompt. ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ [ hide band ] ✻ Worked for 42s · done 4:20 PM │ │ › /headroom │ ⎿ headroom-snip: Opened the Headroom snip log. │ │ ✂ headroom isn't in the loop: nothing at http://127.0.0.1:8787. Run `headroom wrap claude` to start snipping. hide ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
✂ headroom isn't in the loop: nothing at http://127.0.0.1:8787. Run `headroom wrap claude` to start
Pane · ✂ Headroom
✂ 0 tokens snipped on this proxy since this session started (0% of 0) over 0 requests ≈ less than a page Claude didn't have to reread · 0 all-time proxy unreachable at http://127.0.0.1:8787 counting every client on this proxy (`headroom wrap claude`… No requests through Headroom yet. Send a prompt. [ hide band ]
README

headroom-snip

A Claude Code mod that shows what Headroom does to each request, live. When a request goes through the proxy, a pair of scissors runs along a bar the size of the original prompt, cuts it down to what was actually sent, and the token count drops as it goes:

✂ headroom 21k → 16k   tok ━━━━━━━━━━━━━━━━━━━━━━━✂⡀⠄⠂⠁⢀⠈⠐⠠ −81%
✂ headroom 21k → 7.6k  tok ━━━━━━━━━━━✂⠈⠐⠠⡀⠄⠂⠁⢀⠈⠐⠠⡀⠄⠂······ −81%
✂ headroom 21k → 4.1k  tok ━━━━━━·························· −81%
  JSON crush ×2 · Kompress text · cache align   project 184k saved (62%) over 23 req ≈ 307 pages  details  hide

What it shows

  • The band above the prompt. The latest request: original → sent tokens, the snip animation, and the percentage cut. Below that, which compressors did the cutting (JSON crush, code AST, Kompress text, log squash, cache align, ...) and the running total since the session started. A request sent unchanged says why, for example kept: user message, recent code or nothing worth a snip.
  • /headroom. Opens a pane with the requests since the session started: one bar per request, what was cut and what was protected, how long compression took, the biggest snip so far, and an all-time total. /headroom hide and /headroom show toggle the band.
  • The status line. ✂ 184k tok saved · 62%.
  • Milestone toasts at 10k, 50k, 100k, 250k, 500k, 1M and 5M tokens saved.
  • No proxy? If nothing answers, the band says so and suggests headroom wrap claude.

How it works

While a turn runs, the mod polls the proxy's loopback GET /stats?cached=1 once a second. It reads the recent_requests tail (per-request token counts and transforms_applied), and keeps polling for a few seconds after the turn ends. Requests stamped before the session started count as history: they aren't animated or added to the totals, even if the proxy only comes up later. Nothing leaves your machine.

The proxy can't tell Claude Code sessions apart, so the totals are never per session. What they cover depends on how Claude Code reaches the proxy:

  • Through headroom wrap claude: each request carries an X-Headroom-Project header naming the launch directory (its basename, so two directories with the same name share a tag). The mod counts every request with this session's tag, including other sessions in the same project, and leaves other projects out. The proxy only reports tags for its last ten requests, so a request it can't attribute is left out rather than guessed. The band says "project" and /headroom names the project.
  • Any other way: there is no tag, so the totals cover every client on the proxy since this session started. The band says "proxy" and /headroom says so.

It finds the proxy in this order:

  1. HEADROOM_PROXY_URL
  2. ANTHROPIC_BASE_URL (as headroom wrap claude sets it)
  3. http://127.0.0.1:8787

Only loopback URLs are used: http or https on exactly localhost, 127.0.0.1 or [::1], with no user or password. A value that isn't one (a remote proxy, or a host like localhost.example.com) is skipped, so the mod never polls a remote host.

Install

claude plugin marketplace add headroomlabs-ai/headroom
claude plugin install headroom-snip@headroom-marketplace

Or, from a checkout: claude --plugin-dir plugins/headroom-snip.

Develop

claude plugin validate plugins/headroom-snip
claude plugin test plugins/headroom-snip      # tests/snip.test.tsx
tsc -p plugins/headroom-snip                  # after Claude Code has loaded it once

hooks/snip.ts holds the pure parts (parsing, transform labels, the animation frames) and hooks/register.tsx holds the hooks and drawing.

Source 3 files
hooks/register.tsx 337 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Anim, Proxy, Snip, Totals } from '../types'
5import {
6  FRAMES,
7  FRAME_MS,
8  addToTotals,
9  bar,
10  counter,
11  crossed,
12  explain,
13  fmt,
14  isOwn,
15  ownProject,
16  pages,
17  parseRecent,
18  proxyUrl,
19} from './snip'
20import type { Tone } from './snip'
21
22const PANE = 'headroom-snip'
23const POLL_MS = 1000
24const TAIL_TICKS = 4
25
26const feed = atom({ plugin: 'headroom-snip', key: 'feed' } as const, [])
27const totals = atom({ plugin: 'headroom-snip', key: 'totals' } as const, {
28  original: 0,
29  optimized: 0,
30  saved: 0,
31  requests: 0,
32  biggest: null,
33})
34const anim = atom({ plugin: 'headroom-snip', key: 'anim' } as const, null)
35const proxy = atom({ plugin: 'headroom-snip', key: 'proxy' } as const, { url: '', isUp: null, project: null })
36// When this session started, in $.clock time; requests stamped earlier are history. Kept across reloads.
37const startedAt = atom({ plugin: 'headroom-snip', key: 'startedAt' } as const, null)
38const isHidden = atom({ plugin: 'headroom-snip', key: 'isHidden' } as const, false)
39
40const TONE: Record<Tone, { color?: string; dimColor?: boolean; bold?: boolean }> = {
41  kept: { color: '#4ade80' },
42  doomed: { color: '#f59e0b' },
43  blade: { color: '#f472b6', bold: true },
44  crumb: { color: '#a8a29e' },
45  gone: { dimColor: true },
46}
47
48const pct = (t: Pick<Totals, 'saved' | 'original'>) =>
49  t.original > 0 ? Math.round((t.saved / t.original) * 100) : 0
50
51// The proxy tags requests by project (the launch directory's name), not by session, so the totals are
52// the project's traffic since this session started, or the whole proxy's when there is no tag.
53const scope = (project: string | null) => (project ? 'project' : 'proxy')
54
55// The module's own bookkeeping; a reload starts it over while $.state keeps the drawing's values.
56const live = {
57  seen: new Set<string>(),
58  url: proxyUrl(),
59  project: null as string | null,
60  poller: undefined as Timer | undefined,
61  animTimer: undefined as Timer | undefined,
62  tail: 0,
63  // Set once a poll has succeeded: a request with no usable timestamp counts only if it shows up after that.
64  isBaselined: false,
65  isPolling: false,
66}
67
68async function setProxy($: EngineInterface, next: Proxy) {
69  const now = await read($, proxy)
70  if (now.isUp !== next.isUp || now.url !== next.url || now.project !== next.project) {
71    await update($, proxy, () => next)
72  }
73}
74
75async function snipAnimation($: EngineInterface, id: string) {
76  live.animTimer?.cancel()
77  let frame = 0
78  await update($, anim, (): Anim => ({ id, frame }))
79  live.animTimer = $.clock.every(FRAME_MS, () => {
80    frame += 1
81    if (frame >= FRAMES) live.animTimer?.cancel()
82    void update($, anim, (): Anim => ({ id, frame }))
83  })
84}
85
86async function land($: EngineInterface, fresh: Snip[]) {
87  const before = (await read($, totals)).saved
88  await update($, feed, list => [...list, ...fresh].slice(-50))
89  const after = await update($, totals, t => addToTotals(t, fresh))
90  $.ui.status(`✂ ${fmt(after.saved)} tok saved · ${pct(after)}%`)
91
92  const where = live.project
93    ? `in ${live.project} since this session started`
94    : 'on this proxy since this session started'
95  for (const milestone of crossed(before, after.saved)) {
96    $.ui.toast(`✂ ${fmt(milestone)} tokens snipped ${where}: ${pages(milestone)} Claude didn't have to reread`)
97  }
98
99  const added = fresh.reduce((n, s) => n + s.saved, 0)
100  const lifetime = Number((await $.store.get('lifetimeSaved')) ?? 0)
101  await $.store.set('lifetimeSaved', lifetime + added)
102
103  const showcase = [...fresh].reverse().find(s => s.saved > 0) ?? fresh[fresh.length - 1]
104  if (showcase && showcase.saved > 0) await snipAnimation($, showcase.id)
105}
106
107async function poll($: EngineInterface) {
108  if (live.isPolling) return
109  live.isPolling = true
110  try {
111    const res = await $.http.fetch(`${live.url}/stats?cached=1`)
112    if (!res.ok) {
113      await setProxy($, { url: live.url, isUp: false, project: live.project })
114      return
115    }
116    await setProxy($, { url: live.url, isUp: true, project: live.project })
117    for (const s of await read($, feed)) live.seen.add(s.id)
118    const since = (await read($, startedAt)) ?? 0
119    const unseen = parseRecent(res.text).filter(s => !live.seen.has(s.id))
120    unseen.forEach(s => live.seen.add(s.id))
121    // A request stamped before the session started is history, however late the first poll lands.
122    const fresh = unseen.filter(s => isOwn(s, since, live.project) ?? live.isBaselined)
123    live.isBaselined = true
124    if (fresh.length > 0) await land($, fresh)
125  } catch {
126    await setProxy($, { url: live.url, isUp: false, project: live.project })
127  } finally {
128    live.isPolling = false
129  }
130}
131
132function keepPolling($: EngineInterface) {
133  live.tail = Number.POSITIVE_INFINITY
134  live.poller ??= $.clock.every(POLL_MS, () => {
135    live.tail -= 1
136    if (live.tail <= 0) {
137      live.poller?.cancel()
138      live.poller = undefined
139    }
140    void poll($)
141  })
142}
143
144export const register: Register = on => {
145  on('session.start', async ($, e, next) => {
146    live.url = proxyUrl(await $.env.get('HEADROOM_PROXY_URL'), await $.env.get('ANTHROPIC_BASE_URL'))
147    live.project = ownProject(await $.env.get('ANTHROPIC_CUSTOM_HEADERS'))
148    const now = await $.clock.now()
149    await update($, startedAt, at => at ?? now)
150    await $.command.register({
151      name: 'headroom',
152      description: 'Show what Headroom snipped from each request (add "hide" or "show" for the band)',
153    })
154    void poll($)
155
156    return next(e)
157  })
158
159  on('turn.start', async ($, e, next) => {
160    keepPolling($)
161
162    return next(e)
163  })
164
165  on('turn.complete', async ($, e, next) => {
166    live.tail = TAIL_TICKS
167    void poll($)
168
169    return next(e)
170  })
171
172  on('command.run', { command: 'headroom' }, async ($, e) => {
173    const arg = e.args.trim().toLowerCase()
174    if (arg === 'hide' || arg === 'show') {
175      await update($, isHidden, () => arg === 'hide')
176      return { text: `Headroom snip band ${arg === 'hide' ? 'hidden' : 'shown'}.` }
177    }
178    await poll($)
179    await $.ui.open({ id: PANE, title: '✂ Headroom' })
180
181    return { text: 'Opened the Headroom snip log.' }
182  })
183
184  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
185    const [list, sum, now, px, hidden] = await Promise.all([
186      read($, feed),
187      read($, totals),
188      read($, anim),
189      read($, proxy),
190      read($, isHidden),
191    ])
192    if (e.props.hasSurvey || hidden) return next(e)
193
194    const { Box, Button, Text } = $.ui.resolve(e)
195    const columns = e.props.bodyColumns ?? e.viewport?.columns ?? 80
196    const hide = <Button key="hide" label="hide" plain onPress={() => update($, isHidden, () => true)} />
197
198    if (px.isUp === false) {
199      return (
200        <Box>
201          <Text color="#f472b6">✂ </Text>
202          <Text dimColor wrap="truncate-end">
203            headroom isn't in the loop: nothing at {px.url}. Run `headroom wrap claude` to start snipping.{' '}
204          </Text>
205          {hide}
206        </Box>
207      )
208    }
209
210    const last = list[list.length - 1]
211    if (!last) return next(e)
212
213    const frame = now && now.id === last.id ? now.frame : FRAMES
214    const width = Math.max(10, Math.min(40, columns - 46))
215    const { cuts, kept } = explain(last.transforms)
216    const isPassThrough = last.saved <= 0
217
218    return (
219      <Box flexDirection="column">
220        <Box>
221          <Text color="#f472b6" bold>
222            ✂ headroom{' '}
223          </Text>
224          {isPassThrough ? (
225            <Text dimColor wrap="truncate-end">
226              {fmt(last.original)} tok sent as-is ({kept.length > 0 ? `kept: ${kept.join(', ')}` : 'nothing worth a snip'})
227            </Text>
228          ) : (
229            <Box>
230              <Text>
231                {fmt(last.original)} → <Text bold>{fmt(counter(last.original, last.optimized, frame))}</Text> tok{' '}
232              </Text>
233              {bar(width, last.original, last.optimized, frame).map((seg, i) => (
234                <Text key={`b${i}`} {...TONE[seg.tone]}>
235                  {seg.text}
236                </Text>
237              ))}
238              <Text color="#4ade80" bold>
239                {' '}
240                −{Math.round(last.percent)}%
241              </Text>
242            </Box>
243          )}
244        </Box>
245        <Box>
246          <Text dimColor wrap="truncate-end">
247            {'  '}
248            {cuts.length > 0 ? `${cuts.slice(0, 3).join(' · ')}   ` : ''}{scope(px.project)} {fmt(sum.saved)} saved ({pct(sum)}%) over{' '}
249            {sum.requests} req ≈ {pages(sum.saved)}{' '}
250          </Text>
251          <Button key="details" label="details" plain onPress={() => $.ui.open({ id: PANE, title: '✂ Headroom' })} />
252          <Text dimColor> </Text>
253          {hide}
254        </Box>
255      </Box>
256    )
257  })
258
259  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
260    const [list, sum, px, hidden] = await Promise.all([read($, feed), read($, totals), read($, proxy), read($, isHidden)])
261    const lifetime = Number((await $.store.get('lifetimeSaved')) ?? 0)
262    const { Box, Button, Text } = $.ui.resolve(e)
263    const columns = e.props.bodyColumns ?? e.viewport?.columns ?? 60
264    const room = Math.max(1, Math.floor(((e.viewport?.rows ?? 24) - 9) / 2))
265    const width = Math.max(8, Math.min(24, columns - 34))
266
267    return (
268      <Box flexDirection="column">
269        <Text>
270          <Text color="#f472b6" bold>
271            ✂ {fmt(sum.saved)}
272          </Text>{' '}
273          tokens snipped {px.project ? `in ${px.project}` : 'on this proxy'} since this session started ({pct(sum)}% of {fmt(sum.original)}) over{' '}
274          {sum.requests} requests
275        </Text>
276        <Text dimColor>
277          ≈ {pages(sum.saved)} Claude didn't have to reread · {fmt(lifetime)} all-time
278        </Text>
279        {sum.biggest && (
280          <Text dimColor wrap="truncate-end">
281            biggest snip: {fmt(sum.biggest.saved)} tok (−{Math.round(sum.biggest.percent)}%)
282            {explain(sum.biggest.transforms).cuts.length > 0 ? ` by ${explain(sum.biggest.transforms).cuts.join(', ')}` : ''}
283          </Text>
284        )}
285        <Text dimColor>{px.isUp === false ? `proxy unreachable at ${px.url}` : `proxy ${px.url}`}</Text>
286        <Text dimColor wrap="truncate-end">
287          {px.project
288            ? `counting every request tagged ${px.project}, other sessions in that project included`
289            : 'counting every client on this proxy (`headroom wrap claude` tags requests by project)'}
290        </Text>
291        <Text> </Text>
292        {list.length === 0 && <Text dimColor>No requests through Headroom yet. Send a prompt.</Text>}
293        {list
294          .slice(-room)
295          .reverse()
296          .map(s => {
297            const { cuts, kept } = explain(s.transforms)
298            return (
299              <Box key={s.id} flexDirection="column">
300                <Box>
301                  <Text dimColor>{(s.at.split('T')[1] ?? s.at).slice(0, 8).padEnd(9)}</Text>
302                  {bar(width, s.original, s.optimized, FRAMES).map((seg, i) => (
303                    <Text key={`b${i}`} {...TONE[seg.tone]}>
304                      {seg.text}
305                    </Text>
306                  ))}
307                  <Text>
308                    {' '}
309                    {fmt(s.original)}→{fmt(s.optimized)}
310                  </Text>
311                  <Text color={s.saved > 0 ? '#4ade80' : undefined} dimColor={s.saved <= 0}>
312                    {' '}
313                    −{Math.round(s.percent)}%
314                  </Text>
315                </Box>
316                <Text dimColor wrap="truncate-end">
317                  {'         '}
318                  {cuts.length > 0 ? cuts.join(' · ') : 'sent as-is'}
319                  {kept.length > 0 ? `  (kept: ${kept.join(', ')})` : ''}
320                  {s.latencyMs !== null ? `  ${Math.round(s.latencyMs)}ms` : ''}
321                </Text>
322              </Box>
323            )
324          })}
325        <Text> </Text>
326        <Box>
327          <Button
328            key="band"
329            label={hidden ? 'show band' : 'hide band'}
330            onPress={() => update($, isHidden, h => !h)}
331          />
332        </Box>
333      </Box>
334    )
335  })
336}
337
hooks/snip.ts 255 lines
1import type { Snip, Totals } from '../types'
2
3export const FRAMES = 24
4export const FRAME_MS = 45
5export const TOKENS_PER_PAGE = 600
6export const MILESTONES = [10_000, 50_000, 100_000, 250_000, 500_000, 1_000_000, 5_000_000]
7
8const DEFAULT_PROXY = 'http://127.0.0.1:8787'
9const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]'])
10
11/** The origin of `raw` when it is an http(s) URL on a loopback host with no userinfo, else undefined. */
12export function loopbackOrigin(raw?: string): string | undefined {
13  if (!raw) return undefined
14  let url: URL
15  try {
16    url = new URL(raw)
17  } catch {
18    return undefined
19  }
20  const isHttp = url.protocol === 'http:' || url.protocol === 'https:'
21  if (!isHttp || url.username || url.password || !LOOPBACK_HOSTS.has(url.hostname.toLowerCase())) {
22    return undefined
23  }
24
25  return url.origin
26}
27
28/**
29 * Where the Headroom proxy listens: the override, then the wrapped base URL, then the default port.
30 * Each candidate must be a loopback URL; one that is not is skipped, so the mod never polls a remote host.
31 */
32export function proxyUrl(override?: string, baseUrl?: string): string {
33  return loopbackOrigin(override) ?? loopbackOrigin(baseUrl) ?? DEFAULT_PROXY
34}
35
36type RawRequest = {
37  request_id?: string | null
38  timestamp?: string | null
39  model?: string | null
40  input_tokens_original?: number | null
41  input_tokens_optimized?: number | null
42  tokens_saved?: number | null
43  savings_percent?: number | null
44  transforms_applied?: string[] | null
45  optimization_latency_ms?: number | null
46}
47
48type RawLog = { request_id?: string | null; tags?: Record<string, unknown> | null }
49
50/** Request id → the proxy's project tag, from the raw `request_logs` tail a loopback `/stats` carries. */
51function projectsById(logs: unknown): Map<string, string | null> {
52  const byId = new Map<string, string | null>()
53  if (!Array.isArray(logs)) return byId
54  for (const log of logs as RawLog[]) {
55    if (!log?.request_id) continue
56    const project = log.tags?.project
57    byId.set(String(log.request_id), typeof project === 'string' && project.trim() ? project.trim() : null)
58  }
59
60  return byId
61}
62
63/** The per-request rows of a loopback `/stats` payload, oldest first. */
64export function parseRecent(text: string): Snip[] {
65  let body: { recent_requests?: RawRequest[]; request_logs?: unknown }
66  try {
67    body = JSON.parse(text)
68  } catch {
69    return []
70  }
71  // The proxy lists recent_requests newest first.
72  const rows = Array.isArray(body?.recent_requests) ? [...body.recent_requests].reverse() : []
73  const projects = projectsById(body?.request_logs)
74
75  return rows.flatMap(row => {
76    const original = row.input_tokens_original
77    const optimized = row.input_tokens_optimized
78    if (typeof original !== 'number' || typeof optimized !== 'number' || !row.request_id) {
79      return []
80    }
81    const saved = Math.max(0, row.tokens_saved ?? original - optimized)
82    const percent = original > 0 ? (saved / original) * 100 : 0
83
84    return [
85      {
86        id: String(row.request_id),
87        at: row.timestamp ?? '',
88        model: row.model ?? '',
89        original,
90        optimized,
91        saved,
92        percent,
93        transforms: Array.isArray(row.transforms_applied) ? row.transforms_applied : [],
94        latencyMs: typeof row.optimization_latency_ms === 'number' ? row.optimization_latency_ms : null,
95        ...(projects.has(String(row.request_id)) ? { project: projects.get(String(row.request_id)) ?? null } : {}),
96      },
97    ]
98  })
99}
100
101type Kind = 'cut' | 'kept'
102
103function describe(transform: string): { kind: Kind; label: string } | null {
104  const t = transform.toLowerCase()
105  if (t.startsWith('router:protected:')) {
106    return { kind: 'kept', label: t.slice('router:protected:'.length).replace(/_/g, ' ') }
107  }
108  if (t.startsWith('router:excluded')) return { kind: 'kept', label: 'excluded tool' }
109  if (t.startsWith('netcost:skip')) return { kind: 'kept', label: 'not worth a cut' }
110  if (t.startsWith('router:netcost')) return null
111  if (t.startsWith('smart:') || t.includes('smart_crush') || t.includes('json')) return { kind: 'cut', label: 'JSON crush' }
112  if (t.startsWith('kompress') || t.includes('kompress')) return { kind: 'cut', label: 'Kompress text' }
113  if (t.includes('cache_breakpoint') || t.includes('cache_align') || t.includes('dynamic_elements')) {
114    return { kind: 'cut', label: 'cache align' }
115  }
116  if (t.startsWith('read_maturation')) return { kind: 'cut', label: 'stale reads' }
117  if (t.includes('whitespace')) return { kind: 'cut', label: 'whitespace' }
118  if (t.includes('code')) return { kind: 'cut', label: 'code AST' }
119  if (t.includes('log')) return { kind: 'cut', label: 'log squash' }
120  if (t.includes('search') || t.includes('grep')) return { kind: 'cut', label: 'search trim' }
121  if (t.includes('diff')) return { kind: 'cut', label: 'diff trim' }
122  if (t.includes('html')) return { kind: 'cut', label: 'HTML strip' }
123  if (t.includes('text')) return { kind: 'cut', label: 'text trim' }
124  if (t === 'processed_content_blocks') return null
125
126  return { kind: 'cut', label: (t.split(':')[0] ?? t).replace(/_/g, ' ') }
127}
128
129/** Friendly names for what Headroom did ("JSON crush ×2") and what it left alone on purpose. */
130export function explain(transforms: readonly string[]): { cuts: string[]; kept: string[] } {
131  const counts = { cut: new Map<string, number>(), kept: new Map<string, number>() }
132  for (const one of transforms) {
133    const d = describe(one)
134    if (d) counts[d.kind].set(d.label, (counts[d.kind].get(d.label) ?? 0) + 1)
135  }
136  const list = (m: Map<string, number>) =>
137    [...m.entries()].sort((a, b) => b[1] - a[1]).map(([label, n]) => (n > 1 ? `${label} ×${n}` : label))
138
139  return { cuts: list(counts.cut), kept: list(counts.kept) }
140}
141
142export function fmt(n: number): string {
143  const abs = Math.abs(n)
144  if (abs >= 1_000_000) return `${(n / 1_000_000).toFixed(abs >= 10_000_000 ? 0 : 1)}M`
145  if (abs >= 10_000) return `${Math.round(n / 1000)}k`
146  if (abs >= 1000) return `${(n / 1000).toFixed(1)}k`
147
148  return String(Math.round(n))
149}
150
151export function pages(tokens: number): string {
152  const p = tokens / TOKENS_PER_PAGE
153  if (p < 1) return 'less than a page'
154
155  return `${p < 10 ? p.toFixed(1) : Math.round(p)} pages`
156}
157
158export function addToTotals(totals: Totals, snips: readonly Snip[]): Totals {
159  return snips.reduce<Totals>(
160    (t, s) => ({
161      original: t.original + s.original,
162      optimized: t.optimized + s.optimized,
163      saved: t.saved + s.saved,
164      requests: t.requests + 1,
165      biggest: !t.biggest || s.saved > t.biggest.saved ? s : t.biggest,
166    }),
167    totals,
168  )
169}
170
171/** Every milestone a step from `before` to `after` saved tokens crosses, lowest first. */
172export function crossed(before: number, after: number): number[] {
173  return MILESTONES.filter(m => before < m && after >= m)
174}
175
176/** The X-Headroom-Project this session sends, read from ANTHROPIC_CUSTOM_HEADERS as `headroom wrap claude` sets it. */
177export function ownProject(customHeaders?: string): string | null {
178  for (const line of (customHeaders ?? '').split(/\r?\n/)) {
179    const colon = line.indexOf(':')
180    if (colon < 0 || line.slice(0, colon).trim().toLowerCase() !== 'x-headroom-project') continue
181    const raw = line.slice(colon + 1).trim()
182    let name = raw
183    try {
184      name = decodeURIComponent(raw)
185    } catch {
186      // A value that is not percent-encoded is used as written.
187    }
188
189    return name.trim() || null
190  }
191
192  return null
193}
194
195/**
196 * Whether a request belongs to this session: stamped at or after `startedAt` and, when the session
197 * sends a project, tagged with exactly that project. A row whose tag is unknown (outside the proxy's
198 * tagged log tail) is not counted then. Undefined when the request carries no usable timestamp.
199 */
200export function isOwn(snip: Snip, startedAt: number, project: string | null): boolean | undefined {
201  if (project !== null && snip.project !== project) return false
202  const at = Date.parse(snip.at)
203  if (!Number.isFinite(at)) return undefined
204
205  return at >= startedAt
206}
207
208export type Tone = 'kept' | 'doomed' | 'blade' | 'crumb' | 'gone'
209export type Segment = { tone: Tone; text: string }
210
211const CRUMBS = ['⠁', '⠂', '⠄', '⡀', '⠠', '⠐', '⠈', '⢀']
212
213const ease = (x: number) => 1 - Math.pow(1 - x, 3)
214
215/**
216 * One frame of the snip: the bar is the request as it arrived, `width` cells;
217 * the blade travels from the right end to where the kept part ends, the cut
218 * part crumbles behind it, and at `frame >= FRAMES` only dust is left.
219 */
220export function bar(width: number, original: number, optimized: number, frame: number): Segment[] {
221  const w = Math.max(4, Math.floor(width))
222  const ratio = original > 0 ? Math.min(1, Math.max(0, optimized / original)) : 1
223  const kept = Math.max(original > 0 && optimized > 0 ? 1 : 0, Math.round(w * ratio))
224  const p = ease(Math.min(1, Math.max(0, frame / FRAMES)))
225  const done = frame >= FRAMES
226  const blade = done ? -1 : Math.min(w - 1, Math.round(w - (w - kept) * p))
227  const cells: Segment[] = []
228
229  for (let i = 0; i < w; i++) {
230    if (i < kept) cells.push({ tone: 'kept', text: '━' })
231    else if (done) cells.push({ tone: 'gone', text: '·' })
232    else if (i < blade) cells.push({ tone: 'doomed', text: '━' })
233    else if (i === blade) cells.push({ tone: 'blade', text: '✂' })
234    else {
235      const fallen = frame - Math.round(((w - i) / Math.max(1, w - kept)) * FRAMES * 0.6)
236      cells.push(fallen > 6 ? { tone: 'gone', text: '·' } : { tone: 'crumb', text: CRUMBS[(i * 7 + frame) % CRUMBS.length] ?? '·' })
237    }
238  }
239
240  return cells.reduce<Segment[]>((runs, c) => {
241    const last = runs[runs.length - 1]
242    if (last && last.tone === c.tone) last.text += c.text
243    else runs.push({ ...c })
244
245    return runs
246  }, [])
247}
248
249/** The token count shown while the blade travels: from the original down to what was sent. */
250export function counter(original: number, optimized: number, frame: number): number {
251  const p = ease(Math.min(1, Math.max(0, frame / FRAMES)))
252
253  return Math.round(original - (original - optimized) * p)
254}
255
types/index.d.ts 40 lines
1export type Snip = {
2  id: string
3  at: string
4  model: string
5  original: number
6  optimized: number
7  saved: number
8  percent: number
9  transforms: string[]
10  latencyMs: number | null
11  /** The proxy's project tag for the request: a name, null when logged without one, absent when unknown. */
12  project?: string | null
13}
14
15export type Totals = {
16  original: number
17  optimized: number
18  saved: number
19  requests: number
20  biggest: Snip | null
21}
22
23export type Anim = { id: string; frame: number }
24
25/** `project` is the X-Headroom-Project this session sends; null means the counts are proxy-wide. */
26export type Proxy = { url: string; isUp: boolean | null; project: string | null }
27
28declare module 'claude-code' {
29  interface PluginState {
30    'headroom-snip': {
31      feed: Snip[]
32      totals: Totals
33      anim: Anim | null
34      proxy: Proxy
35      isHidden: boolean
36      startedAt: number | null
37    }
38  }
39}
40