SLOPSHOPPER

turn-recap

After each long turn, a one-line toast: how long it took, how many tools ran, and which files were edited.

newguardtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · turn-recap
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ turn-recap │ ⏺ Read(src/auth.ts) │ Turn done in 42s · 9 tool calls · 3 files │ ⎿ Read 6 lines │ edited │ ⏺ 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

turn-recap

A toast after each long turn: how long, how many tools, which files.

When a turn runs longer than a threshold and used tools, shows a one-line toast so you can tab away during long runs and come back to a summary.

Install

/plugin marketplace add cloudcruncher/claude-mod-garage
/plugin install turn-recap@mod-garage

Run /reload-plugins (or restart Claude Code) afterwards. To try it from a local clone without installing: claude --plugin-dir ./dev-mods/turn-recap

Use it

  • Automatic once installed.

What you'll see

Illustrative output.

Toast

Turn done in 1m 35s · 12 tool calls · 3 files edited

Options

OptionDefaultWhat it does
minSeconds20Turns shorter than this show no recap

Set options when installing, or later in /plugin → the mod → configure.

Source 2 files
hooks/register.ts 27 lines
1import type { Register } from 'claude-code'
2import { fresh, recap, record } from './lib'
3
4export const register: Register = (on, options) => {
5  const minSeconds = Number(options?.minSeconds ?? 20)
6  let tally = fresh()
7
8  on('turn.start', (_$, e, next) => {
9    if (!e.agentId) tally = fresh()
10    return next(e)
11  })
12
13  // Subagent calls count too: they are part of what the turn did.
14  on('tool.call', (_$, e, next) => {
15    record(tally, e.tool, e as unknown as Record<string, unknown>)
16    return next(e)
17  }).catch(($, e, next) => next(e))
18
19  on('turn.complete', ($, e, next) => {
20    if (!e.agentId) {
21      const line = recap(tally, e.durationMs ?? 0, minSeconds)
22      if (line) $.ui.toast(line)
23    }
24    return next(e)
25  })
26}
27
hooks/lib.ts 24 lines
1const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
2
3export type Tally = { tools: number; files: Set<string> }
4
5export const fresh = (): Tally => ({ tools: 0, files: new Set() })
6
7export function record(t: Tally, tool: string, args: Record<string, unknown>): void {
8  t.tools++
9  const path = args.file_path ?? args.notebook_path
10  if (EDIT_TOOLS.has(tool) && typeof path === 'string') t.files.add(path)
11}
12
13export function dur(ms: number): string {
14  const s = Math.round(ms / 1000)
15  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
16}
17
18/** The recap line, or undefined when the turn was too short or did nothing. */
19export function recap(t: Tally, ms: number, minSeconds: number): string | undefined {
20  if (ms < minSeconds * 1000 || t.tools === 0) return undefined
21  const f = t.files.size
22  return `Turn done in ${dur(ms)} · ${t.tools} tool call${t.tools === 1 ? '' : 's'}` + (f ? ` · ${f} file${f === 1 ? '' : 's'} edited` : '')
23}
24