SLOPSHOPPER

Turn progress

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

newbandguardtimer
★ 1v0.1.0MITupdated 2026-10-21benjaminb10/encore-mods/plugins/turn-progress
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · turn-progress
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ⏳ 0s · 9 actions ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⏳ 0s · 9 actions ⟨Claude Code's own drawing⟩
README

Turn progress

A Claude Code mod from Encore Mods. See what Claude is up to without scrolling the transcript.

  • While a turn runs: ⏳ 2m 05s · 14 actions · ▰▰▰▱▱▱▱▱▱▱ 3/10 · 2 agents running.
  • A second line with the step running now, such as ↳ Bash: Run the test suite.
  • Disappears when the turn ends.

Install

/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.

Try it without installing

git clone https://github.com/benjaminb10/encore-mods
claude --plugin-dir encore-mods/plugins/turn-progress
Source 1 files
hooks/register.js 90 lines
1// 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