A band above the prompt: the tool running now during a turn, and a one-line summary of the last turn after it

A band above the prompt that tells you what Claude is doing and what the last turn cost in time and tools.
● working 5 tool calls, now: BashLast turn: 34s, 7 tool calls (Bash 4, Read 2, Edit 1), with a Hide button/turn-meter: shows or hides the bandA subagent finishing never replaces the main turn's summary.
claude plugin install turn-meter@bar-mods
From claude plugin validate:
session.start, command.run{command=turn-meter}, prompt.submit, tool.call (observes only, never changes a call), turn.complete, ui.render{component=AbovePrompt}$.clock.now, $.command.register, $.state.get, $.state.set, $.ui.resolve, $.ui.toastNo files, processes, network, environment or model calls. It costs no tokens.
Draws in the terminal and the desktop app's Code tab. In claude -p, the SDK and the VS Code chat panel its hooks run and nothing draws.
Tested on Claude Code 2.1.288.
hooks/register.tsx 121 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Live, Turn } from '../types'
5
6const last = atom({ plugin: 'turn-meter', key: 'last' } as const, null)
7const live = atom({ plugin: 'turn-meter', key: 'live' } as const, null)
8const isHidden = atom({ plugin: 'turn-meter', key: 'isHidden' } as const, false)
9
10const LONG_TURN_SECONDS = 120
11
12const total = (byTool: Record<string, number>) =>
13 Object.values(byTool).reduce((sum, n) => sum + n, 0)
14
15const breakdown = (byTool: Record<string, number>) =>
16 Object.entries(byTool)
17 .sort((a, b) => b[1] - a[1])
18 .slice(0, 4)
19 .map(([tool, n]) => `${tool} ${n}`)
20 .join(', ')
21
22const summary = (turn: Turn) => {
23 const count = total(turn.byTool)
24 const kinds = count > 0 ? ` (${breakdown(turn.byTool)})` : ''
25 return `Last turn: ${turn.seconds}s, ${count} tool calls${kinds}`
26}
27
28export const register: Register = on => {
29 let startedAt = 0
30 let byTool: Record<string, number> = {}
31
32 on('session.start', async ($, e, next) => {
33 await $.command.register({
34 name: 'turn-meter',
35 description: 'Show or hide the turn meter band',
36 })
37
38 return next(e)
39 })
40
41 on('command.run', { command: 'turn-meter' }, async $ => {
42 const hidden = await update($, isHidden, was => !was)
43
44 return { text: hidden ? 'Turn meter hidden.' : 'Turn meter shown.' }
45 })
46
47 on('prompt.submit', async ($, e, next) => {
48 startedAt = await $.clock.now()
49 byTool = {}
50 await update($, live, (): Live => ({ tools: 0, current: null }))
51
52 return next(e)
53 })
54
55 on('tool.call', async ($, e, next) => {
56 byTool[e.tool] = (byTool[e.tool] ?? 0) + 1
57 await update($, live, (): Live => ({ tools: total(byTool), current: e.tool }))
58
59 try {
60 return await next(e)
61 } finally {
62 await update($, live, was => (was ? { ...was, current: null } : was))
63 }
64 })
65
66 on('turn.complete', async ($, e, next) => {
67 // A subagent's turn ends too; only the main loop's end is "the turn".
68 if (e.agentId !== undefined) {
69 return next(e)
70 }
71
72 const seconds = Math.round(((await $.clock.now()) - startedAt) / 1000)
73 const turn: Turn = { seconds, byTool: { ...byTool } }
74 await update($, last, () => turn)
75 await update($, live, () => null)
76
77 if (seconds > LONG_TURN_SECONDS) {
78 $.ui.toast(`Long turn: ${seconds}s, ${total(turn.byTool)} tool calls`)
79 }
80
81 return next(e)
82 })
83
84 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
85 if (e.props.hasSurvey || (await read($, isHidden))) {
86 return next(e)
87 }
88
89 const now = await read($, live)
90 const turn = await read($, last)
91 const { Box, Button, Text } = $.ui.resolve(e)
92
93 if (e.props.isWorking && now) {
94 const doing = now.current ? `, now: ${now.current}` : ''
95 return (
96 <Box>
97 <Text color="cyan">● working </Text>
98 <Text dimColor>{`${now.tools} tool calls${doing}`}</Text>
99 </Box>
100 )
101 }
102
103 if (turn === null) {
104 return next(e)
105 }
106
107 return (
108 <Box gap={1}>
109 <Text dimColor wrap="truncate-end">
110 {summary(turn)}
111 </Text>
112 <Button
113 key="hide"
114 label="Hide"
115 onPress={() => update($, isHidden, () => true)}
116 />
117 </Box>
118 )
119 })
120}
121types/index.d.ts 9 lines1export type Turn = { seconds: number; byTool: Record<string, number> }
2export type Live = { tools: number; current: string | null }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'turn-meter': { last: Turn | null; live: Live | null; isHidden: boolean }
7 }
8}
9