SLOPSHOPPER

turn-details

Replaces the 'Brewed for 34s' line with a details toggle that folds the turn's duration, tools and model open in place.

newspinnerguard
v1.0.0no licenseupdated 2026-10-13Tvrtk0/claude-code-mods/turn-details
A shopper browsing a rack in a slop shop

Draws

Turn line
📋 details
README

local-mods

Personal Claude Code mods for a calmer, more readable terminal.

ModWhat it does
readable-widthWraps replies and prompts at 80 columns; tables and code blocks that name a language keep the full width; an unlabelled or text block wraps like prose. Your prompts are bold and accent-colored, with a blank line above each message.
usage-metersOne band above the prompt: ctx, 5h and 7d meters, green under 50%, yellow under 80%, red above; 5h also shows the time left until it resets.
tool-logFolds tool calls and their results in the chat into a dim › N tools line and keeps them in a side log grouped by prompt. Toggle it with [ panel ], /panel or a key bound to app:toggleDiffPreSession (ctrl+t in ~/.claude/keybindings.json, see below; this replaces the built-in todo list toggle, which the Todo tab covers); a click on a chat line opens it on that prompt's steps, until the next prompt or ✕ returns it to live. The chat line also sums file changes and failures (› 6 tools · 2 files changed +14 −3 · 1 failed). A turn longer than 30 s ends with a desktop notification. Successful Bash rows expand to show their command, and Edit/Write rows to show their diff. Inside a git repo a [ diff ] button opens the built-in /diff panel. The panel has four tabs: Log, Files (every file changed this session with its diff), Agents (subagents with status, time, tool count and their latest step) and Todo (Claude's plan); [ panel ] reopens it on the tab last in view. The active tab is orange, a tab with something new since you last looked is bright, the rest are dim. Replaces the spinner with a minimal one that names the current step and the plan's progress, e.g. ⠋ 12s · 3/7 · Reading register.tsx or ⠙ 3s · Thinking.
turn-detailsReplaces the Brewed for 34s · done 1:09 PM line with 📋 details ▸, which folds the turn's duration, finish time, tool calls and model open in place.

Install

From GitHub:

claude plugin marketplace add Tvrtk0/claude-code-mods
claude plugin install readable-width@local-mods
claude plugin install usage-meters@local-mods
claude plugin install tool-log@local-mods
claude plugin install turn-details@local-mods

From a local clone (edits apply after /reload-plugins, no reinstall):

claude plugin marketplace add ~/.claude/mods
claude plugin install readable-width@local-mods
claude plugin install usage-meters@local-mods
claude plugin install tool-log@local-mods
claude plugin install turn-details@local-mods

Installed from a local clone, plugins are read straight from the folder.

Develop

claude plugin validate ~/.claude/mods/<mod>
claude plugin test ~/.claude/mods/<mod>

Keyboard shortcut for the panel

A mod cannot add a key of its own, so the [ panel ] button borrows an engine action that only the /diff panel handles. Bind any key to it:

{
  "$schema": "https://www.schemastore.org/claude-code-keybindings.json",
  "bindings": [
    { "context": "Global", "bindings": { "ctrl+t": "app:toggleDiffPreSession" } }
  ]
}

ctrl+t hands the panel the keyboard (opening it if needed) and closes it when it already has it. Inside: h/l or 1–4 switch tabs, j/k move between rows (starting from the newest), enter opens one, d/u scroll half a window, g/b jump to the first or last row, esc returns to the prompt and leaves the panel open. The panel scrolls itself, so its tabs and keys stay at the top; Bottom is b, not G: a button hotkey cannot tell shift+g from g.

Source 2 files
hooks/register.tsx 116 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { TurnRecord } from '../types'
5
6const turns = atom({ plugin: 'turn-details', key: 'turns' } as const, {})
7const expanded = atom({ plugin: 'turn-details', key: 'expanded' } as const, [])
8
9const pad = (n: number) => String(n).padStart(2, '0')
10
11const clockTime = (ms: number) => {
12  const d = new Date(ms)
13
14  return `${pad(d.getHours())}:${pad(d.getMinutes())}`
15}
16
17const duration = (ms: number) => {
18  const s = Math.round(ms / 1000)
19
20  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
21}
22
23// TurnDuration carries no turn id, only the duration it formats, which is the
24// one turn.complete reported: that is the key a row finds its turn by.
25const keyOf = (durationMs: number) => String(durationMs)
26
27export const register: Register = on => {
28  let tools: Record<string, number> = {}
29
30  on('turn.start', ($, e, next) => {
31    tools = {}
32
33    return next(e)
34  })
35
36  on('tool.call', ($, e, next) => {
37    tools = { ...tools, [e.tool]: (tools[e.tool] ?? 0) + 1 }
38
39    return next(e)
40  })
41
42  on('turn.complete', async ($, e, next) => {
43    const done = await next(e)
44    const turn: TurnRecord = {
45      durationMs: e.durationMs,
46      endedAt: await $.clock.now(),
47      model: e.usage?.model,
48      tools,
49    }
50    await update($, turns, all => {
51      const kept = Object.entries(all ?? {}).slice(-199)
52
53      return { ...Object.fromEntries(kept), [keyOf(e.durationMs)]: turn }
54    })
55
56    return done
57  })
58
59  // The duration line becomes one pressable '📋 details' that folds the
60  // turn's details open right beneath it, in the chat.
61  on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => {
62    const key = keyOf(e.props.durationMs)
63    const turn = (await read($, turns))[key]
64
65    if (turn === undefined) {
66      return next(e)
67    }
68
69    const { Box, Button, Text } = $.ui.resolve(e)
70    const isOpen = (await read($, expanded)).includes(key)
71
72    const used = Object.entries(turn.tools).sort((a, b) => b[1] - a[1])
73    const total = used.reduce((sum, [, n]) => sum + n, 0)
74    const row = (label: string, value: string) => (
75      <Box>
76        <Box width={14} flexShrink={0}>
77          <Text dimColor>{label}</Text>
78        </Box>
79        <Text>{value}</Text>
80      </Box>
81    )
82
83    return (
84      <Box marginY={1} flexDirection="column">
85        <Button
86          key="details"
87          plain
88          dimColor
89          onPress={() =>
90            update($, expanded, keys =>
91              (keys ?? []).includes(key)
92                ? (keys ?? []).filter(k => k !== key)
93                : [...(keys ?? []), key].slice(-100),
94            )
95          }
96        >
97          📋 details {isOpen ? '▾' : '▸'}
98        </Button>
99        {isOpen && (
100          <Box flexDirection="column" marginLeft={2}>
101            {row('duration', duration(turn.durationMs))}
102            {row('finished', clockTime(turn.endedAt))}
103            {row(
104              'tool calls',
105              total === 0
106                ? 'none'
107                : `${total} (${used.map(([tool, n]) => `${tool} ${n}`).join(', ')})`,
108            )}
109            {turn.model !== undefined && row('model', turn.model)}
110          </Box>
111        )}
112      </Box>
113    )
114  })
115}
116
types/index.d.ts 16 lines
1export type TurnRecord = {
2  durationMs: number
3  endedAt: number
4  model?: string
5  tools: Record<string, number>
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'turn-details': {
11      turns: Record<string, TurnRecord>
12      expanded: string[]
13    }
14  }
15}
16