ModsCode / Gallery / Designs / Tokens per turn
Tokens per turn
Stacked columns of input, output, cache read and cache write tokens for each turn of a session, with totals in the legend and the largest turn labelled.


hooks/designs/token-turns.mjs
// token-turns: the tokens of each turn in one Claude Code session, 30 stacked columns in candle's look
// (62% of a slot, no frame, 4px tick stubs). Bottom to top: input, output, cache read, cache write, as
// ink at the category steps 0.9 / 0.65 / 0.42 / 0.22, so the two kinds billed at the full rate sit brightest at the base.
// The sample session is seeded: cache read grows with the context each turn, turn 22 follows a /compact
// (no cache read, one large cache write for the summary), and input and output stay a few thousand a
// turn, so cache read is most of the 2.12M total, as it is in a real session. The largest turn carries
// its total. Stacking uses d3-shape's stack() (ISC, Mike Bostock). Sample data only.
import { stack, stackOrderNone } from '../vendor/d3.mjs'
const W = 380
const H = 222
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const PLOT = { left: 44, right: 368, top: 56, bottom: 184 }
const N = 30
const COMPACT = 22 // the first turn after /compact
const Y_MAX = 160000 // above the largest turn, so its label stays clear of the legend
const KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
const SERIES = {
input: { name: 'Input', opacity: 0.9 },
output: { name: 'Output', opacity: 0.65 },
cacheRead: { name: 'Cache read', opacity: 0.42 },
cacheWrite: { name: 'Cache write', opacity: 0.22 },
}
const f = n => n.toFixed(1)
function rng(seed) {
return () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
}
const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start' } = {}) =>
`<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${FONT}" fill="${fill}">${s}</text>`
// 1,835,266 -> 1.84M, 162,423 -> 162K, 64,640 -> 64.6K
const short = v => (v >= 1e6 ? (v / 1e6).toFixed(2) + 'M' : v >= 1e5 ? Math.round(v / 1e3) + 'K' : (v / 1e3).toFixed(1) + 'K')
// A seeded session: the context (cache read) grows by what each turn writes, and /compact empties it
function sample() {
const rnd = rng(20261012)
const turns = []
let context = 0
for (let i = 1; i <= N; i++) {
let cacheWrite
if (i === 1) cacheWrite = 19800 + rnd() * 1200 // system prompt and tools, cached on the first turn
else if (i === COMPACT) cacheWrite = 24000 + rnd() * 3000 // the summary that replaces the history
else cacheWrite = 1500 + rnd() ** 2 * 9000
if (i === COMPACT) context = 0
const input = 200 + rnd() ** 3 * 5200
const output = 400 + rnd() ** 1.5 * 3200
turns.push({ input: Math.round(input), output: Math.round(output), cacheRead: Math.round(context), cacheWrite: Math.round(cacheWrite) })
context += cacheWrite + output * 0.6
}
return turns
}
function draw() {
const turns = sample()
const layers = stack().keys(KEYS).order(stackOrderNone)(turns)
const slot = (PLOT.right - PLOT.left) / N
const bw = slot * 0.62
const cx = i => PLOT.left + slot * (i + 0.5)
const y = v => PLOT.bottom - ((PLOT.bottom - PLOT.top) * v) / Y_MAX
const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
// Header: the session total on the left, the legend with each kind's total on the right
const totals = Object.fromEntries(KEYS.map(k => [k, turns.reduce((s, t) => s + t[k], 0)]))
const all = KEYS.reduce((s, k) => s + totals[k], 0)
parts.push(text(12, 24, short(all), { size: 14, fill: INK }))
parts.push(text(12, 38, `TOKENS IN ${N} TURNS`))
KEYS.forEach((k, i) => {
const x0 = i % 2 ? 262 : 144
const x1 = i % 2 ? W - 12 : 250
const ly = i < 2 ? 22 : 36
parts.push(`<rect x="${x0}" y="${ly - 6}" width="6" height="6" fill="${INK}" fill-opacity="${SERIES[k].opacity}"/>`)
parts.push(text(x0 + 12, ly, SERIES[k].name, { size: 10, fill: INK }))
parts.push(text(x1, ly, short(totals[k]), { size: 10, fill: INK, anchor: 'end' }))
})
// Left scale: 4px stubs and plain numbers, no axis line
for (const v of [0, 50000, 100000, 150000]) {
parts.push(`<line x1="${PLOT.left - 4}" x2="${PLOT.left}" y1="${f(y(v))}" y2="${f(y(v))}" stroke="${MUTED}" stroke-width="1"/>`)
parts.push(text(PLOT.left - 8, y(v) + 3, v ? v / 1000 + 'K' : '0', { anchor: 'end' }))
}
// The /compact boundary: a reference line between the last full turn and the first short one
const bx = PLOT.left + slot * (COMPACT - 1)
parts.push(`<line x1="${f(bx)}" x2="${f(bx)}" y1="100" y2="${PLOT.bottom}" stroke="${INK}" stroke-opacity="0.35" stroke-dasharray="3 3"/>`)
parts.push(text(bx + 4, 108, 'COMPACT'))
for (const layer of layers) {
const op = SERIES[layer.key].opacity
layer.forEach(([y0, y1], i) => {
if (y1 - y0 <= 0) return
parts.push(`<rect x="${f(cx(i) - bw / 2)}" y="${f(y(y1))}" width="${f(bw)}" height="${f(y(y0) - y(y1))}" fill="${INK}" fill-opacity="${op}"/>`)
})
}
// The largest turn carries its total above the column
const sums = turns.map(t => KEYS.reduce((s, k) => s + t[k], 0))
const big = sums.indexOf(Math.max(...sums))
parts.push(text(cx(big), y(sums[big]) - 4, short(sums[big]), { fill: INK, anchor: 'middle' }))
for (const t of [1, 10, 20, 30]) {
parts.push(`<line x1="${f(cx(t - 1))}" x2="${f(cx(t - 1))}" y1="${PLOT.bottom + 2}" y2="${PLOT.bottom + 6}" stroke="${MUTED}" stroke-width="1"/>`)
parts.push(text(cx(t - 1), PLOT.bottom + 16, String(t), { anchor: 'middle' }))
}
parts.push(text(12, H - 8, 'SAMPLE SESSION, TOKENS PER TURN'))
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}
let source // drawn on first use and kept: the pane loads every design, so none pays at load time
export default {
id: 'token-turns',
title: 'Tokens per turn',
category: 'usage',
description: 'Stacked columns of input, output, cache read and cache write tokens for each turn of a session, with totals in the legend and the largest turn labelled.',
element: ui => ui.Svg({ source: (source ??= draw()), alt: 'Stacked columns of tokens per turn split into input, output, cache read and cache write, with a compaction drop, sample data', width: W, height: H }),
}
Imports d3-shape 3.2.0 (ISC), bundled with its licence in look's hooks/vendor/.
To use it in a mod: copy the file to your mod's hooks/designs/ and return its element(ui) from a pane's ui.render. How a pane is built.
Code: PolyForm Noncommercial License 1.0.0: noncommercial use only. Required Notice: Copyright 2026 Haruki Nakada (https://modscode.com).