SLOPSHOPPER

fortschritt

Fortschritt über dem Prompt: Zeit, Tool-Calls und jeder Helfer-Agent live

newbandguardagents
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · fortschritt
› 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 ✅ Fertig · 0:16 · 7 Tool-Calls ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
✅ Fertig · 0:16 · 7 Tool-Calls
README

fortschritt

Ein Claude-Code-Mod, der über dem Prompt live zeigt, woran Claude gerade arbeitet: wie lange der Durchgang schon läuft, wie viele Tool-Calls es gab und, wenn Claude Helfer-Agenten startet, jeden Helfer mit Aufgabe, Modell und Status. Praktisch für lange Aufgaben, bei denen mehrere Subagents parallel arbeiten.

Installieren

Im Terminal in Claude Code eintippen:

/plugin install fortschritt --marketplace boonpflug/claude-mods-deutsch

Danach y zum Hinzufügen des Marketplace und mit Enter den Bereich „user“ wählen. Der Mod läuft sofort.

Aus der Shell geht es auch: claude plugin install fortschritt --marketplace boonpflug/claude-mods-deutsch

Source 2 files
hooks/register.tsx 97 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Run } from '../types'
5
6const run = atom({ plugin: 'fortschritt', key: 'run' } as const, null)
7
8const CRABS = ['🦀', '🦞', '🐙', '🦑', '🐡']
9
10function bar(done: number, total: number, width = 14) {
11  const filled = total === 0 ? 0 : Math.round((done / total) * width)
12  return '█'.repeat(filled) + '░'.repeat(width - filled)
13}
14
15function clock(ms: number) {
16  const s = Math.max(0, Math.floor(ms / 1000))
17  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
18}
19
20async function tick($: EngineInterface) {
21  while ((await read($, run))?.isRunning) {
22    const now = await $.clock.now()
23    await update($, run, r => (r ? { ...r, now } : r))
24    await $.clock.sleep(1000)
25  }
26}
27
28export const register: Register = on => {
29  on('turn.start', async ($, e, next) => {
30    if (e.agentId === undefined) {
31      const now = await $.clock.now()
32      const r = await read($, run)
33      // laufen noch Hintergrund-Helfer, gehört dieser Durchgang zum selben Lauf
34      const waiting = r?.helpers.some(one => !one.isDone)
35      await update($, run, prev =>
36        waiting && prev ? { ...prev, isRunning: true } : { startedAt: now, now, tools: 0, helpers: [], isRunning: true },
37      )
38      if (!r?.isRunning) void tick($)
39    }
40    return next(e)
41  })
42
43  on('turn.complete', async ($, e, next) => {
44    if (e.agentId !== undefined) {
45      await update($, run, r =>
46        r ? { ...r, helpers: r.helpers.map(one => (one.id === e.agentId ? { ...one, isDone: true } : one)) } : r,
47      )
48    } else {
49      await update($, run, r => (r ? { ...r, isRunning: r.helpers.some(one => !one.isDone) } : r))
50    }
51    return next(e)
52  })
53
54  on('tool.call', async ($, e, next) => {
55    await update($, run, r => (r ? { ...r, tools: r.tools + 1 } : r))
56    return next(e)
57  })
58
59  on('agent.spawn', async ($, e, next) => {
60    const started = await next(e)
61    if (started.deny === undefined && started.agentId !== undefined) {
62      const helper = { id: started.agentId, task: e.description, model: started.model.replace(/^claude-/, ''), isDone: false }
63      await update($, run, r => (r ? { ...r, helpers: [...r.helpers, helper] } : r))
64    }
65    return started
66  })
67
68  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
69    const r: Run | null = await read($, run)
70    if (!r || e.props.hasSurvey) return next(e)
71
72    const { Box, Text } = $.ui.resolve(e)
73    const done = r.helpers.filter(one => one.isDone).length
74
75    return (
76      <Box flexDirection="column">
77        <Text>
78          <Text color={r.isRunning ? 'yellow' : 'green'} bold>
79            {r.isRunning ? (done < r.helpers.length && r.helpers.length > 0 ? '⏳ Helfer arbeiten' : '⏳ Claude arbeitet') : '✅ Fertig'}
80          </Text>
81          <Text dimColor> · {clock(r.now - r.startedAt)} · {r.tools} Tool-Calls</Text>
82          {r.helpers.length > 0 && (
83            <Text dimColor> · Helfer {done}/{r.helpers.length} </Text>
84          )}
85          {r.helpers.length > 0 && <Text color="cyan">{bar(done, r.helpers.length)}</Text>}
86        </Text>
87        {r.helpers.map((helper, i) => (
88          <Text dimColor={helper.isDone}>
89            {'  '}{CRABS[i % CRABS.length]} {helper.task} <Text dimColor>({helper.model})</Text>{' '}
90            {helper.isDone ? <Text color="green">✓ fertig</Text> : <Text color="yellow">… arbeitet</Text>}
91          </Text>
92        ))}
93      </Box>
94    )
95  })
96}
97
types/index.d.ts 9 lines
1export type Helper = { id: string; task: string; model: string; isDone: boolean }
2export type Run = { startedAt: number; now: number; tools: number; helpers: Helper[]; isRunning: boolean }
3
4declare module 'claude-code' {
5  interface PluginState {
6    fortschritt: { run: Run | null }
7  }
8}
9