While Claude works: elapsed time, actions, the current step, todo progress and running subagents above the prompt

A Claude Code mod from Encore Mods. See what Claude is up to without scrolling the transcript.
⏳ 2m 05s · 14 actions · ▰▰▰▱▱▱▱▱▱▱ 3/10 · 2 agents running.↳ Bash: Run the test suite./plugin marketplace add benjaminb10/encore-mods
/plugin install turn-progress@encore-mods
Then /reload-plugins or restart Claude Code. Needs Claude Code 2.1.287 or later.
git clone https://github.com/benjaminb10/encore-mods
claude --plugin-dir encore-mods/plugins/turn-progresshooks/register.js 90 lines1// turn-progress: while Claude works, a line above the prompt shows how long the turn has run, how many
2// actions it took, what it is doing right now, the todo list's progress and how many subagents run.
3
4let working = false
5let turnStart = 0
6let actions = 0
7let current = ''
8let todos = null // { done, total }
9
10function clean(s) {
11 return String(s).replace(/\*\*|__|`/g, '').replace(/\s+/g, ' ').trim()
12}
13
14function fit(s, n) {
15 return s.length > n ? s.slice(0, Math.max(1, n - 1)) + '…' : s
16}
17
18export function duration(ms) {
19 const s = Math.max(0, Math.floor(ms / 1000))
20 return s < 60 ? s + 's' : Math.floor(s / 60) + 'm ' + String(s % 60).padStart(2, '0') + 's'
21}
22
23export function bar(done, total, width) {
24 const n = Math.round((done / Math.max(1, total)) * width)
25 return '▰'.repeat(n) + '▱'.repeat(width - n)
26}
27
28export function toolLabel(e) {
29 const t = String(e.tool || '').replace(/^mcp__/, '').replace(/__/g, ' ')
30 const detail = e.description || e.command || e.file_path || e.pattern || e.url || e.query || e.prompt || ''
31 return clean(t + (detail ? ': ' + String(detail).split('\n')[0] : ''))
32}
33
34export function register(on) {
35 on('session.start', async ($, e, next) => {
36 $.clock.every(1000, () => {
37 if (working) $.ui.invalidate('ui.render')
38 })
39 return next(e)
40 })
41
42 on('turn.start', async ($, e, next) => {
43 if (!e.agentId) {
44 working = true
45 turnStart = await $.clock.now()
46 actions = 0
47 current = ''
48 todos = null
49 $.ui.invalidate('ui.render')
50 }
51 return next(e)
52 })
53
54 on('tool.call', async ($, e, next) => {
55 actions += 1
56 if (!e.agentId) current = toolLabel(e)
57 if (Array.isArray(e.todos)) {
58 todos = { done: e.todos.filter((t) => t.status === 'completed').length, total: e.todos.length }
59 }
60 $.ui.invalidate('ui.render')
61 return next(e)
62 }).catch(() => undefined)
63
64 on('turn.complete', async ($, e, next) => {
65 if (!e.agentId) {
66 working = false
67 current = ''
68 $.ui.invalidate('ui.render')
69 }
70 return next(e)
71 })
72
73 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
74 if (!(e.props.isWorking || working)) return next(e)
75 const { Box, Text } = $.ui.resolve(e)
76 const cols = Math.max(30, (e.props.bodyColumns || 80) - 2)
77 const now = await $.clock.now()
78 const parts = ['⏳ ' + duration(now - turnStart), actions + (actions === 1 ? ' action' : ' actions')]
79 if (todos && todos.total) parts.push(bar(todos.done, todos.total, 10) + ' ' + todos.done + '/' + todos.total)
80 let agents = []
81 try { agents = await $.agent.list() } catch {}
82 const running = agents.filter((a) => a.status === 'running').length
83 if (running) parts.push(running + (running === 1 ? ' agent running' : ' agents running'))
84 const rows = [Text({ dimColor: true, children: [fit(parts.join(' · '), cols)] })]
85 if (current) rows.push(Text({ dimColor: true, children: [fit('↳ ' + current, cols)] }))
86 const theirs = await next(e)
87 return Box({ flexDirection: 'column', children: [...rows, theirs] })
88 })
89}
90