SLOPSHOPPER

turn-timer

A band above the prompt with the last turn's duration and tool calls; toasts on slow turns

newbandguardtoastprompt
v0.1.0MITupdated 2026-10-05vincentlauriat/ModsTools/mods/turn-timer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · turn-timer
› 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 ⏱ last turn 42s · 9 tools[ Hide ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⏱ last turn 42s · 9 tools[ Hide ]
README

turn-timer

A small band above the prompt showing how long the last turn took and how many tools it called.

What it does

  • After each main-conversation turn (not subagents), shows ⏱ last turn 42s · 12 tools above the prompt. Durations over a minute read 1m05s.
  • Tool count includes every tool call since you submitted the prompt (subagents' calls too).
  • A Hide button dismisses the band; it reappears after the next turn.
  • Toast Slow turn: <duration>, N tool calls when a turn lasts more than 2 minutes.
  • Uses the turn's reported duration, falling back to the clock since prompt submission.

Install

claude --plugin-dir /path/to/ModsTools/mods/turn-timer

Limits

  • No command and no options: the 2-minute toast threshold is fixed in the code.
  • Nothing is shown until a first turn has completed; state is not kept across sessions.
  • The band is hidden while a survey is displayed.

Develop

claude plugin validate mods/turn-timer
claude plugin test mods/turn-timer   # 6 tests
Source 3 files
hooks/register.tsx 53 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Turn } from '../types'
5import { formatDuration } from './format'
6
7const SLOW_MS = 120_000
8const last = atom({ plugin: 'turn-timer', key: 'last' } as const, null)
9const isHidden = atom({ plugin: 'turn-timer', key: 'isHidden' } as const, false)
10
11export const register: Register = on => {
12  let startedAt: number | null = null
13  let tools = 0
14
15  on('prompt.submit', async ($, e, next) => {
16    startedAt = await $.clock.now()
17    tools = 0
18
19    return next(e)
20  })
21
22  on('tool.call', ($, e, next) => {
23    tools += 1
24
25    return next(e)
26  })
27
28  on('turn.complete', async ($, e, next) => {
29    if (e.agentId !== undefined) return next(e)
30    const ms = e.durationMs ?? (startedAt === null ? 0 : (await $.clock.now()) - startedAt)
31    const turn: Turn = { ms, tools }
32    await update($, last, () => turn)
33    await update($, isHidden, () => false)
34    if (ms > SLOW_MS) $.ui.toast(`Slow turn: ${formatDuration(ms)}, ${tools} tool calls`)
35
36    return next(e)
37  })
38
39  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
40    const turn = await read($, last)
41    if (e.props.hasSurvey || turn === null || (await read($, isHidden))) return next(e)
42    const { Box, Button, Text } = $.ui.resolve(e)
43    const label = `⏱ last turn ${formatDuration(turn.ms)} · ${turn.tools} tool${turn.tools === 1 ? '' : 's'}`
44
45    return (
46      <Box>
47        <Text dimColor>{label}</Text>
48        <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
49      </Box>
50    )
51  })
52}
53
hooks/format.ts 7 lines
1export function formatDuration(ms: number): string {
2  const total = Math.round(ms / 1000)
3  if (total < 60) return `${total}s`
4
5  return `${Math.floor(total / 60)}m${String(total % 60).padStart(2, '0')}s`
6}
7
types/index.d.ts 8 lines
1export type Turn = { ms: number; tools: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'turn-timer': { last: Turn | null; isHidden: boolean }
6  }
7}
8