Caps reply and prompt text at 80 columns while code blocks and tables keep the full width, and highlights your own prompts.

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 183 lines1import type {
2 EngineInterface,
3 Register,
4 RenderElement,
5 RenderInput,
6} from 'claude-code'
7
8const WIDTH = 80
9const GUTTER = 2
10const PADDING = 0
11
12type Segment = { text: string; isWide: boolean }
13
14// A fixed left padding rather than centering: replies stream in at the left
15// edge, so with none the finished block does not move at all. Null when the
16// terminal is too narrow to spare it.
17const marginFor = (columns: number | undefined) =>
18 columns === undefined || columns < 40 ? null : PADDING
19
20// Splits markdown into prose and wide blocks (code fences and tables), so
21// prose wraps at WIDTH while wide blocks keep the room to the right edge.
22function segment(markdown: string): Segment[] {
23 const segments: Segment[] = []
24 let lines: string[] = []
25 let isWide = false
26 let fence: string | null = null
27
28 const flush = () => {
29 const text = lines.join('\n').trim()
30 if (text !== '') segments.push({ text, isWide })
31 lines = []
32 }
33
34 for (const line of markdown.split('\n')) {
35 const trimmed = line.trimStart()
36
37 if (fence !== null) {
38 lines.push(line)
39 if (trimmed.startsWith(fence)) {
40 flush()
41 fence = null
42 isWide = false
43 }
44 continue
45 }
46
47 // Only code keeps the full width: a fence that names a language other
48 // than plain text. An unlabelled or text fence wraps like prose.
49 const opens = /^(`{3,}|~{3,})\s*([\w+#.-]*)/.exec(trimmed)
50 if (opens) {
51 flush()
52 fence = opens[1] ?? '```'
53 const lang = (opens[2] ?? '').toLowerCase()
54 isWide = lang !== '' && !['text', 'txt', 'plain', 'plaintext', 'markdown', 'md'].includes(lang)
55 lines.push(line)
56 continue
57 }
58
59 const isTableRow = trimmed.startsWith('|')
60 if (isTableRow !== isWide) {
61 flush()
62 isWide = isTableRow
63 }
64 lines.push(line)
65 }
66
67 flush()
68
69 return segments
70}
71
72// Moves a row the engine drew to the column's left edge, so the whole
73// transcript reads as one column instead of jumping between edge and center.
74function shift(
75 $: EngineInterface,
76 e: RenderInput,
77 drawn: RenderElement,
78): RenderElement {
79 const columns = e.viewport?.columns
80 const margin = marginFor(columns)
81
82 if (e.surface !== 'terminal' || margin === null || columns === undefined) {
83 return drawn
84 }
85
86 const { Box } = $.ui.resolve(e)
87
88 // The engine refuses its own row under a Box that sets a width, so the
89 // margin alone shifts it.
90 return <Box marginLeft={margin}>{drawn}</Box>
91}
92
93export const register: Register = on => {
94 on('ui.render', { component: 'AssistantMessage' }, ($, e, next) => {
95 const columns = e.viewport?.columns
96 const margin = marginFor(columns)
97
98 if (e.surface !== 'terminal' || margin === null || columns === undefined) {
99 return next(e)
100 }
101
102 const { Box, Text, Markdown } = $.ui.resolve(e)
103 const wide = columns - margin - GUTTER
104
105 return (
106 <Box
107 marginLeft={margin}
108 marginTop={e.props.isFirstOfReply ? 1 : 0}
109 width={columns - margin}
110 >
111 <Box width={GUTTER} flexShrink={0}>
112 <Text>{e.props.isFirstOfReply ? '●' : ' '}</Text>
113 </Box>
114 <Box flexDirection="column" gap={1}>
115 {segment(e.props.text).map(part => (
116 <Box width={part.isWide ? wide : Math.min(WIDTH - GUTTER, wide)}>
117 <Markdown text={part.text} />
118 </Box>
119 ))}
120 </Box>
121 </Box>
122 )
123 })
124
125 on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
126 const columns = e.viewport?.columns
127 const margin = marginFor(columns)
128
129 if (
130 e.surface !== 'terminal' ||
131 margin === null ||
132 columns === undefined
133 ) {
134 return next(e)
135 }
136
137 // Rows that are not a typed prompt (attachments, notifications) keep the
138 // engine's drawing and only move to the same left edge.
139 if (e.props.origin.kind !== 'composer') {
140 return shift($, e, await next(e))
141 }
142
143 const { Box, Text } = $.ui.resolve(e)
144 const width = Math.min(WIDTH, columns - margin)
145
146 return (
147 <Box marginLeft={margin} marginTop={1} width={width}>
148 <Box width={GUTTER} flexShrink={0}>
149 <Text bold color="claude">
150 ❯
151 </Text>
152 </Box>
153 <Box width={width - GUTTER}>
154 <Text bold color="claude">
155 {e.props.text}
156 </Text>
157 </Box>
158 </Box>
159 )
160 })
161
162 on('ui.render', { component: 'ToolUse' }, async ($, e, next) =>
163 shift($, e, await next(e)),
164 )
165
166 // An expanded group unfolds into ToolUse rows, which shift themselves.
167 on('ui.render', { component: 'ToolGroup' }, async ($, e, next) =>
168 e.props.isExpanded ? next(e) : shift($, e, await next(e)),
169 )
170
171 on('ui.render', { component: 'TurnDuration' }, async ($, e, next) =>
172 shift($, e, await next(e)),
173 )
174
175 on('ui.render', { component: 'Spinner' }, async ($, e, next) =>
176 shift($, e, await next(e)),
177 )
178
179 on('ui.render', { component: 'CommandOutput' }, async ($, e, next) =>
180 shift($, e, await next(e)),
181 )
182}
183