An odometer above the prompt: how long this session has run and what it has cost.

An odometer above the prompt: how long this session has run, and what it has cost so far.
🧭 1h 23m $3.10
The cost is the figure /cost totals for the session. On a Claude plan that is what the session would cost at API prices, not a charge. The time counts from when the session first began, so a resumed session keeps its miles.
/odometer prints it./odometer hide and /odometer show toggle it.It sits beside Gas Gauge and Speedometer on the same dash. It reads only the figures Claude Code already has, and sends nothing anywhere. The band draws in the terminal and in the Code tab of the Claude desktop app.
Nothing goes anywhere. Here is each thing, plainly.
What it sends, and where.
$.session.usage, and shows them on your screen.Which programs it runs, and why.
What it puts in the prompts it submits.
What each hook does.
session.start: adds the /odometer command and reads the start time and cost.session.measure: takes the new cost each time Claude Code measures it.command.run for odometer: prints the reading, or hides or shows the band.ui.render for AbovePrompt: draws the dial above the prompt.Tested with Claude Code 2.1.287.
See every Rostech mod, live, at rostech.app.
hooks/register.tsx 64 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4// The session's mileage: when it began, and what it has cost as /cost totals it.
5const startedAt = atom({ plugin: 'odometer', key: 'startedAt' } as const, 0)
6const usd = atom({ plugin: 'odometer', key: 'usd' } as const, null as number | null)
7const isHidden = atom({ plugin: 'odometer', key: 'isHidden' } as const, false)
8
9function miles(ms: number): string {
10 const m = Math.max(0, Math.floor(ms / 60000))
11 return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`
12}
13
14function money(v: number | null): string {
15 return v === null ? '' : `$${v.toFixed(2)}`
16}
17
18export const register: Register = on => {
19 on('session.start', async ($, e, next) => {
20 await $.command.register({ name: 'odometer', description: 'How long this session has run and what it has cost. /odometer hide or show.' })
21 const u = await $.session.usage()
22 await update($, startedAt, () => u.startedAt)
23 await update($, usd, () => u.cost?.usd ?? null)
24 return next(e)
25 })
26
27 on('session.measure', async ($, e, next) => {
28 if (e.cost) await update($, usd, () => e.cost?.usd ?? null)
29 return next(e)
30 })
31
32 on('command.run', { command: 'odometer' }, async ($, e) => {
33 const arg = e.args.trim().toLowerCase()
34 if (arg === 'hide' || arg === 'show') {
35 await update($, isHidden, () => arg === 'hide')
36 return { text: arg === 'hide' ? 'Odometer hidden.' : 'Odometer showing.' }
37 }
38 const ran = (await $.clock.now()) - (await read($, startedAt))
39 const cost = money(await read($, usd))
40 return { text: `🧭 ${miles(ran)}${cost ? ` ${cost}` : ''}` }
41 })
42
43 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
44 const began = await read($, startedAt)
45 if (e.props.hasSurvey || !began || (await read($, isHidden))) return next(e)
46 const ran = miles((await $.clock.now()) - began)
47 const cost = money(await read($, usd))
48
49 const { Box, Text } = $.ui.resolve(e)
50 // A small dial of our own; every other band above the prompt draws beside it.
51 const below = await next(e)
52 return (
53 <Box flexDirection="row">
54 <Box marginRight={3} flexDirection="row">
55 <Text>🧭 </Text>
56 <Text bold>{ran}</Text>
57 {cost ? <Text dimColor> {cost}</Text> : null}
58 </Box>
59 {below}
60 </Box>
61 )
62 })
63}
64types/index.d.ts 6 lines1declare module 'claude-code' {
2 interface PluginState {
3 'odometer': { startedAt: number; usd: number | null; isHidden: boolean }
4 }
5}
6