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

Personal Claude Code mods for a calmer, more readable terminal.
| Mod | What it does |
|---|---|
readable-width | Wraps 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-meters | One 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-log | Folds 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-details | Replaces 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. |
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.
claude plugin validate ~/.claude/mods/<mod>
claude plugin test ~/.claude/mods/<mod>
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.
hooks/register.tsx 116 lines1import { 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}
116types/index.d.ts 16 lines1export 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