SLOPSHOPPER

footprint

Estimated electricity (⚑) and water (πŸ’§) behind each Claude Code turn and session, shown on the spinner, the turn's closing line and the hint line; /footprint…

newpanespinnerguardcommandprompt
v0.2.0MITupdated 2026-10-11christian-engels/claude-footprint
A shopper browsing a rack in a slop shop
Preview Β· a replayed session in a sandbox
claude Β· ~/work/app Β· footprint
β”‚ ┃ Footprint βœ• β€Ί fix the failing auth test and add an audit log call β”‚ ┃ Session ⚑ 0.00033 Wh 0.00011–0.0011β€¦πŸ’§ 0. β”‚ ┃ Turn ⚑ 0.00033 Wh 0.00011–0.0011β€¦πŸ’§ 0. ⏺ Read(src/auth.ts) β”‚ ┃ ⎿ Read 6 lines β”‚ ┃ tools Bash 4 Β· Write 2 Β· ⏺ Update(src/auth.ts) β”‚ ┃ ⎿ Added 2 lines, removed 1 line β”‚ ┃ Per token: Watershed (2026) factors from ⏺ Bash(bun test) β”‚ ┃ Google's measured 0.24 Wh prompt, matching ⎿ 3 pass, 1 fail β”‚ ┃ Oviedo et al. (2026) for>200B models; ranges β”‚ ┃ Γ—0.5–×2. Cache reads at 10% (1–25%) of fresh ● Done. refresh now rejects expired claims and logs an audit event. β”‚ ┃ input, an unmeasured convention (Hausfather β”‚ ┃ 2026). Water: on-site cooling plus ✻ ⚑ 0.00033 Wh πŸ’§ 0.0012 mL Β· Worked for 42s Β· done 4:20 PM β”‚ ┃ electricity generation (Li et al. 2025). β”‚ ┃ Haiku at 1/6. Counting since the mod loaded: β€Ί /footprint β”‚ ┃ the transcript was too long to read. ⎿ footprint: Footprint opened. β”‚ β”‚ ✻ Thinking ⚑ 0.00033 Wh πŸ’§ 0.0012 mL… ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── β€Ί ? for shortcuts

Draws

Pane Β· Footprint
Session ⚑ 0.00033 Wh 0.00011–0.0011β€¦πŸ’§ 0.0012 mL 0.0001 Turn ⚑ 0.00033 Wh 0.00011–0.0011β€¦πŸ’§ 0.0012 mL 0.0001 tools Bash 4 Β· Write 2 Β· Read 1 Β· Grep 1 Per token: Watershed (2026) factors from Google's measured 0.24 Wh prompt, matching Oviedo et al. (2026) for>200B models; ranges Γ—0.5–×2. Cache reads at 10% (1–25%) of fresh input, an unmeasured convention (Hausfather 2026). Water: on-site cooling plus electricity generation (Li et al. 2025). Haiku at 1/6. Counting since the mod loaded: the transcript was too long to read.
README

footprint

A Claude Code mod that estimates the electricity ⚑ and water πŸ’§ behind your session, and shows them on the lines Claude Code already draws.

Anthropic publishes no energy figures, so these are estimates built from published measurements of comparable systems. Every factor is cited below and in the code.

What you see

While Claude works, beside the time and tokens in the spinner:

✻ Pondering… ⚑⚑ 4.6 Wh πŸ’§πŸ’§ 17 mL

On the line that closes each turn:

✻ ⚑⚑⚑ 230 Wh πŸ’§πŸ’§πŸ’§ 840 mL Β· Baked for 5m 48s

At the end of the hint line under the prompt, the session so far:

⚑⚑⚑ 820 Wh πŸ’§πŸ’§πŸ’§ 3 L this session

Type /footprint for a breakdown: low–high ranges, one row per model and effort level, tool calls and sources. Esc closes it.

The glyphs count decades, so they compare across turns and sessions:

1 glyph2345
⚑ electricityunder 1 Wh1–10 Wh10–100 Wh100 Wh–1 kWhover 1 kWh
πŸ’§ waterunder 5 mL5–50 mL50–500 mL0.5–5 Lover 5 L

Install

You need a Claude Code version that runs function-hook mods (tested on 2.1.296), on macOS or Linux.

Clone the repo, then link it into your skills folder. Claude Code loads any plugin it finds there.

git clone https://github.com/christian-engels/claude-footprint.git ~/claude-footprint
mkdir -p ~/.claude/skills
ln -s ~/claude-footprint ~/.claude/skills/footprint

Start a new session. To check that it loaded:

claude plugin list

You should see footprint@skills-dir with βœ” loaded.

If you set CLAUDE_CONFIG_DIR, link into $CLAUDE_CONFIG_DIR/skills instead.

Update: git -C ~/claude-footprint pull. A running session reloads the mod by itself.

Remove: rm ~/.claude/skills/footprint. This deletes only the link, not your clone.

Without cloning: type /plugin install footprint --marketplace christian-engels/claude-footprint at the Claude Code prompt. This installs a copy, which you update with claude plugin update. Use one method or the other, not both, or the mod loads twice.

How the numbers are made

The mod records what each model call processes: new input tokens, tokens written to and read from the prompt cache, and output tokens. It also records how long each tool runs. Energy and water are worked out from those counts each time the display updates, so changing a factor recalculates everything already recorded.

When it loads, the mod rebuilds the session so far from its transcript files, including subagents. Loading it partway through a session, or resuming one, therefore still counts everything.

For a frontier model (Opus, Sonnet, Fable):

Wh = PUE Γ— [ (input + cache writes + 10% Γ— cache reads) Γ— 0.32 + output Γ— 0.96 ] / 1,000
FactorValueSource
Output tokens0.96 Wh per 1,000Watershed (2026), derived from Google's measured 0.24 Wh median Gemini prompt
Input tokens and cache writes0.32 Wh per 1,000Same
Cache reads10% of an input token (range 1–25%)Hausfather (2026), following the API price ratio. Not measured by anyone
Data-centre overheadPUE 1.10Watershed (2026), hyperscaler default
Haiku modelsone sixth of the aboveHausfather (2026)
Uncertainty in the per-token factorsΓ—0.5 to Γ—2Oviedo et al. (2026), interquartile range
Wateron-site cooling Γ· PUE + water used to generate the electricityLi et al. (2025)
On-site cooling water0.12 / 0.55 / 1.15 L per kWhAWS / US average / Google
Power-station water1.29 / 3.14 / 9.50 L per kWhLi et al. (2025), Texas / US average / Washington
Toolsrun time Γ— 2 / 6 / 20 WJudgement; small next to the model

Reasoning effort needs no factor of its own. Thinking is billed as output tokens, so more effort shows up as more output.

The displayed figure is the central estimate. /footprint shows the low–high range, which combines the low ends and the high ends of all factors.

Checks against published work

  • On Hausfather's token mix (3.2 billion tokens, 96% cache reads, about 14,000 model calls), the factors give 11.6 Wh per call. He reports about 12 Wh.
  • A chat question with 500 tokens in and 300 out comes to 0.49 Wh. Microsoft's estimate for models over 200B parameters is 0.31 Wh (interquartile range 0.16–0.60).

Caveats

  • In agentic sessions about 97% of tokens are cache reads: the model re-reading its context at every step. They make up about 70% of the estimate, and their energy cost has never been measured. The 1–25% range on that factor is the main reason the low–high range is wide.
  • Tool calls that the API runs inside a request (the advisor, for example) are not counted.
  • The transcript backfill reads at most 4 MiB per transcript file. A longer session counts from the moment the mod loaded, and /footprint says so.
  • Water covers consumption (water evaporated), not withdrawal (water taken and returned).

Changing the numbers

All factors sit at the top of hooks/register.tsx, each with its source. If better measurements appear, change them there.

claude plugin validate .
claude plugin test .

Sources

  • Bistline, J. et al. (2026). Estimating GHG Emissions from AI Use: Framework for Corporate-Level Measurement. Watershed. watershed.com/blog/ai-emissions-framework
  • Elsworth, C. et al. (2025). Measuring the environmental impact of delivering AI at Google scale. arXiv:2508.15734
  • Oviedo, F. et al. (2026). Energy Use of AI Inference, Efficiency Pathways, and Test-Time Scaling. Microsoft. arXiv:2509.20241
  • Li, P., Yang, J., Islam, M. A. and Ren, S. (2025). Making AI Less "Thirsty". Communications of the ACM. arXiv:2304.03271
  • Hausfather, Z. (2026). The real energy use of agentic AI. The Climate Brink
  • Couch, S. (2026). Electricity use of AI coding agents. simonpcouch.com

Licence

MIT. See LICENSE.

Source 2 files
hooks/register.tsx 378 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, ModelUsage, Register } from 'claude-code'
3
4import type { Activity, Tokens, Turn } from '../types'
5
6// Low, central, high.
7type Band = [number, number, number]
8
9// --- Calibration -------------------------------------------------------------
10// Anthropic publishes no energy, hardware or region data, so every factor is
11// taken from production measurements or models of comparable frontier systems.
12// The activity is stored raw: change a factor and everything recalculates.
13
14// Facility electricity per token of a frontier-scale model (>200B parameters).
15// Watershed Activity Tier, hyperscaler defaults (Bistline et al. 2026,
16// "Estimating GHG Emissions from AI Use", Table 2): EI_in 0.32 and EI_out
17// 0.96 Wh per 1,000 tokens, times PUE 1.10, derived from Google's measured
18// 0.24 Wh median Gemini prompt (Elsworth et al. 2025, arXiv 2508.15734, which
19// counts accelerators, host CPU/DRAM, idle capacity and overhead). Oviedo et al.
20// (2026, arXiv 2509.20241) reach the same ~1 Wh per 1,000 output tokens bottom-up
21// for >200B models on H100 (0.31 Wh for a 300-token answer); their IQR,
22// 0.16-0.60 Wh, sets the x0.5-x2 band, which is also Hausfather's (2026) "factor
23// of ~2 in either direction". Thinking is billed as output, so reasoning effort
24// acts through output tokens. Cache writes are a prefill, so they count as input.
25const WH_PER_TOKEN = { input: 0.32e-3, output: 0.96e-3 }
26const SPREAD: Band = [0.5, 1, 2]
27const PUE = 1.1
28
29// A cache read skips the prefill; what remains (KV transfer and attention over
30// the cached context) has not been measured anywhere. Convention: 10% of fresh
31// input, the API price ratio, bounded at 1% and 25% (Hausfather 2026, "The real
32// energy use of agentic AI"; Couch 2026). It drives most of an agentic session.
33const CACHE_READ: Band = [0.01, 0.1, 0.25]
34
35// Haiku-class models use roughly 5-7x less energy per token than frontier ones
36// (Hausfather 2026, footnote 5); every other Claude model counts as frontier.
37const TIERS: [RegExp, number][] = [[/haiku/, 1 / 6]]
38
39// Operational water (consumption, not withdrawal) per facility Wh: on-site
40// cooling WUE / PUE + the grid's water intensity (EWIF), as in Li et al. (2025,
41// CACM, arXiv 2304.03271, Table 1). On-site: AWS 0.12 L/kWh (2025), US average
42// 0.55, Google 1.15 (Elsworth et al. 2025). Grid: Texas 1.29, US average 3.14,
43// Washington 9.50 L/kWh (Li et al. Table 1).
44const WUE_SITE: Band = [0.12, 0.55, 1.15]
45const EWIF: Band = [1.29, 3.14, 9.5]
46
47// Incremental power while a tool runs, local CPU or remote server: judgement,
48// counted with the same PUE and water. Negligible next to inference.
49// ponytail: wall time x power overcounts tools that mostly wait on the network.
50const TOOL_WATTS: Band = [2, 6, 20]
51// Tools whose wall time is waiting, not work (a subagent's own steps are counted).
52const WAITS = new Set(['Agent', 'Task', 'AskUserQuestion', 'ScheduleWakeup', 'Monitor', 'SendMessage', 'EnterPlanMode', 'ExitPlanMode'])
53
54// Fixed decade bands on the central estimate, so the glyphs compare across
55// turns and sessions. One glyph per band reached.
56const WH_BANDS = [1, 10, 100, 1000]
57const ML_BANDS = [5, 50, 500, 5000]
58// -----------------------------------------------------------------------------
59
60const PANE = 'footprint'
61const EMPTY: Activity = { models: {}, tools: {} }
62const session = atom({ plugin: 'footprint', key: 'session' } as const, EMPTY)
63const turn = atom({ plugin: 'footprint', key: 'turn' } as const, EMPTY)
64const turns = atom({ plugin: 'footprint', key: 'turns' } as const, [] as Turn[])
65const isBackfilled = atom({ plugin: 'footprint', key: 'isBackfilled' } as const, false)
66const isPartial = atom({ plugin: 'footprint', key: 'isPartial' } as const, false)
67
68const band = (f: (i: 0 | 1 | 2) => number): Band => [f(0), f(1), f(2)]
69const tier = (model: string) => TIERS.find(([re]) => re.test(model))?.[1] ?? 1
70
71export const impact = (models: Activity['models'], toolMs: number) => {
72  const wh = band(i => {
73    const inference = Object.entries(models).reduce(
74      (sum, [key, t]) =>
75        sum +
76        tier(key) *
77          SPREAD[i] *
78          ((t.input + t.cacheWrite + t.cacheRead * CACHE_READ[i]) * WH_PER_TOKEN.input + t.output * WH_PER_TOKEN.output),
79      0,
80    )
81
82    return (inference + (toolMs / 3.6e6) * TOOL_WATTS[i]) * PUE
83  })
84
85  return { wh, ml: band(i => wh[i] * (WUE_SITE[i] / PUE + EWIF[i])) }
86}
87
88const toolMs = (a: Activity) => Object.values(a.tools).reduce((sum, t) => sum + t.ms, 0)
89const isEmpty = (a: Activity) => Object.keys(a.models).length === 0 && Object.keys(a.tools).length === 0
90
91export const level = (central: number, bands: number[]) => 1 + bands.filter(b => central >= b).length
92const sig = (x: number) => String(Number(x.toPrecision(2)))
93const count = (n: number) => (n >= 1e6 ? `${sig(n / 1e6)}M` : n >= 1e3 ? `${sig(n / 1e3)}k` : String(n))
94const unit = (x: number, small: string, big: string) => (x >= 1000 ? `${sig(x / 1000)} ${big}` : `${sig(x)} ${small}`)
95const span = ([lo, , hi]: Band, small: string, big: string) =>
96  hi >= 1000 ? `${sig(lo / 1000)}–${sig(hi / 1000)} ${big}` : `${sig(lo)}–${sig(hi)} ${small}`
97const energy = (b: Band) => `${'⚑'.repeat(level(b[1], WH_BANDS))} ${unit(b[1], 'Wh', 'kWh')}`
98const water = (b: Band) => `${'πŸ’§'.repeat(level(b[1], ML_BANDS))} ${unit(b[1], 'mL', 'L')}`
99
100// The one-glance form every line uses: central estimates, a glyph per decade.
101export const glance = (a: Activity) => {
102  const { wh, ml } = impact(a.models, toolMs(a))
103
104  return `${energy(wh)} ${water(ml)}`
105}
106
107const addTokens = (key: string, u: ModelUsage) => (a: Activity): Activity => {
108  const t = a.models[key] ?? { steps: 0, input: 0, cacheWrite: 0, cacheRead: 0, output: 0 }
109  const next: Tokens = {
110    steps: t.steps + 1,
111    input: t.input + u.input_tokens,
112    cacheWrite: t.cacheWrite + u.cache_creation_input_tokens,
113    cacheRead: t.cacheRead + u.cache_read_input_tokens,
114    output: t.output + u.output_tokens,
115  }
116
117  return { ...a, models: { ...a.models, [key]: next } }
118}
119
120const addTool = (tool: string, ms: number) => (a: Activity): Activity => {
121  const t = a.tools[tool] ?? { calls: 0, ms: 0 }
122
123  return { ...a, tools: { ...a.tools, [tool]: { calls: t.calls + 1, ms: t.ms + ms } } }
124}
125
126const record = async ($: EngineInterface, fn: (a: Activity) => Activity) => {
127  await update($, session, fn)
128  await update($, turn, fn)
129}
130
131type Block = { type?: string; id?: string; name?: string; tool_use_id?: string }
132type Row = {
133  type?: string
134  subtype?: string
135  durationMs?: number
136  timestamp?: string
137  message?: { id?: string; model?: string; usage?: ModelUsage; content?: unknown }
138}
139
140// Rebuilds the session from its transcript files (main and subagents), split
141// into turns at each turn_duration row, so the meter covers what happened
142// before the mod loaded and after a --resume. Effort is not in the transcript.
143export const fromTranscript = (texts: string[]) => {
144  const rows = texts
145    .flatMap(text => text.split('\n'))
146    .filter(line => line.includes('"usage"') || line.includes('"tool_result"') || line.includes('"turn_duration"'))
147    .flatMap(line => {
148      try {
149        return [JSON.parse(line) as Row]
150      } catch {
151        return []
152      }
153    })
154    .sort((a, b) => (a.timestamp ?? '').localeCompare(b.timestamp ?? ''))
155  let all = EMPTY
156  let current = EMPTY
157  const done: Turn[] = []
158  const seen = new Set<string>()
159  const started = new Map<string, { tool: string; at: number }>()
160  const apply = (fn: (a: Activity) => Activity) => {
161    all = fn(all)
162    current = fn(current)
163  }
164
165  for (const row of rows) {
166    const at = Date.parse(row.timestamp ?? '')
167    const blocks = Array.isArray(row.message?.content) ? (row.message.content as Block[]) : []
168    if (row.subtype === 'turn_duration' && row.durationMs !== undefined) {
169      done.push({ durationMs: row.durationMs, activity: current })
170      current = EMPTY
171    }
172    const m = row.message
173    if (row.type === 'assistant' && m?.usage && m.id && m.model !== '<synthetic>' && !seen.has(m.id)) {
174      seen.add(m.id)
175      apply(addTokens(`${m.model}|earlier`, m.usage))
176    }
177    for (const b of blocks) {
178      if (b.type === 'tool_use' && b.id && b.name) started.set(b.id, { tool: b.name, at })
179      const call = b.type === 'tool_result' && b.tool_use_id ? started.get(b.tool_use_id) : undefined
180      if (call) apply(addTool(call.tool, WAITS.has(call.tool) ? 0 : Math.max(0, at - call.at)))
181    }
182  }
183
184  return { session: all, turn: current, turns: done }
185}
186
187const backfill = async ($: EngineInterface) => {
188  try {
189    const home = (await $.env.get('CLAUDE_CONFIG_DIR')) ?? `${await $.env.get('HOME')}/.claude`
190    const project = `${home}/projects/${(await $.session.cwd()).replace(/[^a-zA-Z0-9]/g, '-')}`
191    const id = await $.session.id()
192    const subagents = await $.fs.list(`${project}/${id}/subagents`).catch(() => [])
193    // ponytail: $.fs.read stops at 4 MiB per file; a longer session counts from load.
194    const texts = await Promise.all([
195      $.fs.read(`${project}/${id}.jsonl`),
196      ...subagents.filter(f => f.name.endsWith('.jsonl')).map(f => $.fs.read(`${project}/${id}/subagents/${f.name}`)),
197    ])
198    const found = fromTranscript(texts)
199    await update($, session, () => found.session)
200    await update($, turn, () => found.turn)
201    await update($, turns, () => found.turns)
202  } catch {
203    await update($, isPartial, () => true)
204  }
205  await update($, isBackfilled, () => true)
206}
207
208const openPane = async ($: EngineInterface) => {
209  const all = await read($, session)
210  await $.ui.open({ id: PANE, title: 'Footprint', rows: 10 + Object.keys(all.models).length, focus: true, closeOnEscape: true })
211}
212
213export const register: Register = on => {
214  on('session.start', async ($, e, next) => {
215    await $.command.register({
216      name: 'footprint',
217      description: 'Estimated electricity and water of this session, by model and tool',
218    })
219    if (!(await read($, isBackfilled))) await backfill($)
220
221    return next(e)
222  })
223
224  on('session.end', async ($, e, next) => {
225    if (e.reason === 'clear') {
226      await update($, session, () => EMPTY)
227      await update($, turn, () => EMPTY)
228      await update($, turns, () => [])
229      await update($, isPartial, () => false)
230    }
231
232    return next(e)
233  })
234
235  on('command.run', { command: 'footprint' }, async $ => {
236    await openPane($)
237
238    return { text: 'Footprint opened.' }
239  })
240
241  on('prompt.submit', async ($, e, next) => {
242    await update($, turn, () => EMPTY)
243
244    return next(e)
245  })
246
247  // Every model request of the session, main loop and subagents alike.
248  on('turn.step', async function* ($, e, next) {
249    const step = yield* next(e)
250    if (step.usage) await record($, addTokens(`${step.usage.model}|${e.effort ?? 'default'}`, step.usage))
251
252    return step
253  })
254
255  // Side calls other mods make (a Haiku naming call, say).
256  on('model.complete', async ($, e, next) => {
257    const done = await next(e)
258    if (done.value) await record($, addTokens(`${e.model}|${e.effort ?? 'default'}`, done.value.usage))
259
260    return done
261  })
262
263  on('tool.call', async ($, e, next) => {
264    const startedAt = await $.clock.now()
265    const ran = await next(e)
266    const ms = WAITS.has(e.tool) ? 0 : (await $.clock.now()) - startedAt
267    await record($, addTool(e.tool, ms))
268
269    return ran
270  })
271
272  on('turn.complete', async ($, e, next) => {
273    if (e.agentId === undefined) {
274      const activity = await read($, turn)
275      await update($, turns, list => [...list, { durationMs: e.durationMs, activity }].slice(-200))
276    }
277
278    return next(e)
279  })
280
281  // Live, beside the engine's own time and tokens: "Pondering… ⚑ 0.4 Wh πŸ’§ 1.7 mL (12s Β· ↓ 300 tokens)".
282  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
283    const now = await read($, turn)
284
285    return isEmpty(now) ? next(e) : next({ ...e, props: { ...e.props, suffix: `${e.props.suffix} ${glance(now)}` } })
286  })
287
288  // Each turn's closing line: "✻ ⚑⚑ 3.1 Wh πŸ’§πŸ’§ 13 mL Β· Baked for 5m 48s".
289  on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => {
290    const list = await read($, turns)
291    const ms = e.props.durationMs
292    const hit = list.find(t => t.durationMs === ms) ?? list.find(t => Math.abs(t.durationMs - ms) < 1000)
293
294    return hit && !isEmpty(hit.activity)
295      ? next({ ...e, props: { ...e.props, word: `${glance(hit.activity)} Β· ${e.props.word}` } })
296      : next(e)
297  })
298
299  // The session so far, dim at the end of the hint line under the prompt.
300  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
301    const all = await read($, session)
302
303    return isEmpty(all) ? next(e) : next({ ...e, props: { ...e.props, tail: `${glance(all)} this session` } })
304  })
305
306  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
307    const { Box, Text } = $.ui.resolve(e)
308    const all = await read($, session)
309    const now = await read($, turn)
310    const central = impact(all.models, toolMs(all)).wh[1]
311    const share = (b: Band) => (central > 0 ? `${Math.round((100 * b[1]) / central)}%` : '')
312    const cells = (widths: number[], parts: string[], dim: boolean[] = []) => (
313      <Box flexDirection="row">
314        {parts.map((part, i) => (
315          <Box width={widths[i] ?? 10}>
316            <Text dimColor={dim[i] ?? false} wrap="truncate">
317              {part}
318            </Text>
319          </Box>
320        ))}
321      </Box>
322    )
323    const total = (label: string, a: Activity) => {
324      const { wh, ml } = impact(a.models, toolMs(a))
325
326      return cells([9, 15, 15, 15, 16], [label, energy(wh), span(wh, 'Wh', 'kWh'), water(ml), span(ml, 'mL', 'L')], [false, false, true, false, true])
327    }
328    const ROW = [26, 46, 9, 6]
329    const models = Object.entries(all.models).sort(([a, x], [b, y]) => impact({ [b]: y }, 0).wh[1] - impact({ [a]: x }, 0).wh[1])
330    const tools = Object.entries(all.tools).sort(([, a], [, b]) => b.calls - a.calls)
331    const toolWh = impact({}, toolMs(all)).wh
332
333    return (
334      <Box flexDirection="column">
335        {total('Session', all)}
336        {total('Turn', now)}
337        <Text> </Text>
338        {models.map(([key, t]) => {
339          const [model = key, effort = ''] = key.split('|')
340          const wh = impact({ [key]: t }, 0).wh
341
342          return cells(
343            ROW,
344            [
345              `${model.replace(/^claude-/, '')} Β· ${effort}`,
346              `${t.steps} steps Β· out ${count(t.output)} Β· cache ${count(t.cacheRead + t.cacheWrite)} Β· in ${count(t.input)}`,
347              unit(wh[1], 'Wh', 'kWh'),
348              share(wh),
349            ],
350            [false, true, false, true],
351          )
352        })}
353        {tools.length > 0 &&
354          cells(
355            ROW,
356            [
357              'tools',
358              tools
359                .slice(0, 5)
360                .map(([tool, t]) => `${tool.replace(/^mcp__/, '')} ${t.calls}`)
361                .join(' Β· '),
362              unit(toolWh[1], 'Wh', 'kWh'),
363              share(toolWh),
364            ],
365            [false, true, false, true],
366          )}
367        <Text> </Text>
368        <Text dimColor wrap="wrap">
369          Per token: Watershed (2026) factors from Google's measured 0.24 Wh prompt, matching Oviedo et al. (2026) for
370          {'>'}200B models; ranges Γ—0.5–×2. Cache reads at 10% (1–25%) of fresh input, an unmeasured convention (Hausfather
371          2026). Water: on-site cooling plus electricity generation (Li et al. 2025). Haiku at 1/6.
372          {(await read($, isPartial)) ? ' Counting since the mod loaded: the transcript was too long to read.' : ''}
373        </Text>
374      </Box>
375    )
376  })
377}
378
types/index.d.ts 18 lines
1// Raw activity only: the impact is computed from it at display time, so new
2// coefficients recalculate everything already recorded.
3export type Tokens = { steps: number; input: number; cacheWrite: number; cacheRead: number; output: number }
4export type ToolUse = { calls: number; ms: number }
5export type Activity = {
6  // keyed `${model}|${effort}`
7  models: Record<string, Tokens>
8  tools: Record<string, ToolUse>
9}
10// A finished main-loop turn, keyed by the duration its "Baked for" line shows.
11export type Turn = { durationMs: number; activity: Activity }
12
13declare module 'claude-code' {
14  interface PluginState {
15    footprint: { session: Activity; turn: Activity; turns: Turn[]; isBackfilled: boolean; isPartial: boolean }
16  }
17}
18