SLOPSHOPPER

turn-timestamp

Ends every turn with the date, time, duration and tool call count

newspinnerrowsguardpromptprocess
v0.4.0no licenseupdated 2026-10-04shissncg/claude-mods/mods/turn-timestamp
A shopper browsing a rack in a slop shop

Draws

Turn line
✻ dev · 42s · 9 tool calls
README

Interesting Claude mods

Small Claude Code mods: TypeScript hook modules that run before, after or instead of what Claude Code does.

ModWhat it does
turn-timestampEnds every turn with ✻ 2026-10-03 19:05 CDT · 3s · 1 tool call, so scrollback in multi-day sessions shows when each turn finished. In the terminal it replaces Claude Code's own end-of-turn line (✻ Churned for 1s); in the desktop Code tab, which draws no such line, it goes under Claude's last reply block. Display only.
ci-status/ci opens a pane listing the repo's latest GitHub Actions runs (via gh), refreshes every 30s, shows CI: N running in the status line and a toast when a run finishes.
ai-meter-agentsClassic command hooks (no TypeScript) that record each session's state (working and on which tool, needs you, done) to ~/.config/claude-usage-monitor/agents/, one file per session. The ai-meter daemon shows them on the desk display's Agents page. The same script feeds Codex, Grok and Antigravity sessions: python3 mods/ai-meter-agents/install_other_tools.py adds it to their hook configs (--uninstall removes it). Silent: never blocks a tool call.

Set up a machine

Installs every mod in setup/mods.conf (mine and the community ones I use) and restores their saved settings, such as the neon-cyber skin:

git clone https://github.com/shissncg/claude-mods ~/Dev/claude-mods
~/Dev/claude-mods/setup.sh
  • ./setup.sh --dev registers this checkout as the marketplace instead of GitHub, so edits under mods/ load without pushing.
  • ./setup.sh save copies this machine's mod settings into setup/store/; commit them to carry them to other machines.
  • To add a mod, add a line to setup/mods.conf. Running the script again only installs what's missing.

Plugins turned on at claude.ai sync by themselves and don't need to be listed.

To install just one of mine:

claude plugin marketplace add shissncg/claude-mods
claude plugin install turn-timestamp@shissncg-mods

Or try one for a session without installing:

claude --plugin-dir mods/turn-timestamp --plugin-dir mods/ci-status

Check one with claude plugin validate mods/<name>. Mods need a recent Claude Code build: older CLIs report "session.start" is not an event.

Source 2 files
hooks/register.tsx 110 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Stamp } from '../types'
5
6// Each main-loop turn's end: matched to the terminal's end-of-turn row by its
7// duration, and elsewhere to the reply's last text block by its text.
8const stamps = atom({ plugin: 'turn-timestamp', key: 'stamps' } as const, [])
9
10function describe(stamp: Stamp): string {
11  const parts = [stamp.when, took(stamp.durationMs)]
12  if (stamp.tools > 0) parts.push(`${stamp.tools} tool call${stamp.tools === 1 ? '' : 's'}`)
13  return parts.join(' · ')
14}
15
16function took(ms: number): string {
17  const seconds = Math.round(ms / 1000)
18  if (seconds < 60) return `${seconds}s`
19  const minutes = Math.floor(seconds / 60)
20  if (minutes < 60) return `${minutes}m ${seconds % 60}s`
21  return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
22}
23
24export const register: Register = on => {
25  let tools = 0
26  // Durations whose stamp this module drew; one that no row drew (another mod
27  // drew the row, or the turn ended with no reply text) is logged instead.
28  const drawn = new Set<number>()
29
30  on('prompt.submit', ($, e, next) => {
31    tools = 0
32
33    return next(e)
34  })
35
36  on('tool.call', ($, e, next) => {
37    tools += 1
38
39    return next(e)
40  })
41
42  on('turn.complete', async ($, e, next) => {
43    // Subagent turns end inside the main turn; stamp only the main one.
44    if (e.agentId === undefined) {
45      // The host's `date` knows the local time zone; the module's own clock may not.
46      let when: string
47      try {
48        const out = await $.process.run(['date', '+%Y-%m-%d %H:%M %Z'], { timeoutMs: 2000 })
49        when = out.exitCode === 0 ? out.stdout.trim() : ''
50      } catch {
51        when = ''
52      }
53      if (when === '') {
54        when = new Date(await $.clock.now()).toISOString().slice(0, 16).replace('T', ' ') + ' UTC'
55      }
56      const stamp: Stamp = { durationMs: e.durationMs, answer: e.answer.trim(), when, tools }
57      await update($, stamps, list => [...list, stamp].slice(-500))
58
59      const done = await next(e)
60      void (async () => {
61        await $.clock.sleep(1500)
62        if (!drawn.has(stamp.durationMs)) {
63          $.ui.log(`✻ ${describe(stamp)}`)
64        }
65      })()
66      return done
67    }
68
69    return next(e)
70  })
71
72  // The terminal stamps its end-of-turn row (below); the desktop draws no such
73  // row, so there the stamp goes under the reply's last text block.
74  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
75    if (e.surface === 'terminal') {
76      return next(e)
77    }
78    const text = e.props.text.trim()
79    const list = await read($, stamps)
80    const stamp = text === '' ? undefined : list.findLast(one => one.answer !== '' && one.answer.endsWith(text))
81    if (stamp === undefined) {
82      return next(e)
83    }
84
85    drawn.add(stamp.durationMs)
86    const { Box, Text } = $.ui.resolve(e)
87    const reply = await next(e)
88
89    return (
90      <Box flexDirection="column">
91        {reply}
92        <Text dimColor>✻ {describe(stamp)}</Text>
93      </Box>
94    )
95  })
96
97  on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => {
98    const list = await read($, stamps)
99    const stamp = list.findLast(one => one.durationMs === e.props.durationMs)
100    if (stamp === undefined) {
101      return next(e)
102    }
103
104    drawn.add(stamp.durationMs)
105    const { Text } = $.ui.resolve(e)
106
107    return <Text dimColor>✻ {describe(stamp)}</Text>
108  })
109}
110
types/index.d.ts 8 lines
1export type Stamp = { durationMs: number; answer: string; when: string; tools: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'turn-timestamp': { stamps: Stamp[] }
6  }
7}
8