Every tool call is one line: status, verb, target, result; ctrl+o still shows the full rows

Every tool call is one line in three columns: status and verb, target, result (✓ Edit app/Price.php +14 −3). Errors are red and keep the first useful error line. Edit and Write keep their diff unless you turn keepDiffs off. ctrl+o still shows the full engine rows.
hooks/register.tsx 122 lines1import type { Register } from 'claude-code'
2
3import { hex, hueOf } from './avatar'
4import { clip, noteOf, target } from './describe'
5
6const RED = '#F0776B'
7
8// Tools whose row ledger draws; every other tool keeps the engine's own row.
9const LEDGER = new Set(['Read', 'Edit', 'MultiEdit', 'Write', 'NotebookEdit', 'Bash', 'WebFetch', 'WebSearch', 'Agent', 'Skill', 'Grep', 'Glob', 'LSP'])
10
11// The engine's result block stays for these: the diff is the review.
12const KEEP_RESULT = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit'])
13
14const isLedger = (tool: string) => LEDGER.has(tool) || tool.startsWith('mcp__')
15
16type Input = Parameters<typeof target>[0]
17
18const asInput = (tool: string, input: unknown): Input => ({ ...(input as object), tool }) as Input
19
20const plural = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
21
22// The folded group's line: "Read 3 files · searched 2 patterns · listed 1 folder".
23export function groupLine(calls: readonly { tool: string; input: unknown }[]): string {
24 let files = 0
25 let patterns = 0
26 let folders = 0
27 let commands = 0
28
29 for (const call of calls) {
30 const command = String((call.input as { command?: unknown } | null)?.command ?? '')
31
32 if (call.tool === 'Read') files += 1
33 else if (call.tool === 'Grep' || call.tool === 'Glob' || /^\s*(rg|grep|find|fd|ag)\b/.test(command)) patterns += 1
34 else if (/^\s*(ls|tree)\b/.test(command)) folders += 1
35 else commands += 1
36 }
37
38 const parts = [
39 files ? `read ${plural(files, 'file')}` : '',
40 patterns ? `searched ${plural(patterns, 'pattern')}` : '',
41 folders ? `listed ${plural(folders, 'folder')}` : '',
42 commands ? `ran ${plural(commands, 'command')}` : '',
43 ].filter(Boolean)
44 const line = parts.join(' · ')
45
46 return line.charAt(0).toUpperCase() + line.slice(1)
47}
48
49export const register: Register = (on, options) => {
50 const ACCENT = String(options.accent || '#D97757')
51 let cwd = ''
52
53 on('session.start', async ($, e, next) => {
54 const started = await next(e)
55 cwd = e.cwd
56
57 return started
58 })
59
60 on('ui.render', { component: 'ToolUse' }, ($, e, next) => {
61 if (e.surface !== 'terminal' || e.props.isExpanded || !isLedger(e.props.tool)) {
62 return next(e)
63 }
64
65 const { Box, Text } = $.ui.resolve(e)
66 const { tool, isRunning, isErrored, isInterrupted, output } = e.props
67 const input = asInput(tool, e.props.input)
68 const { verb, what } = target(input, cwd)
69 const note = isRunning
70 ? 'running'
71 : isInterrupted
72 ? 'interrupted'
73 : noteOf(tool, { result: output, isError: isErrored, text: isErrored ? String(output ?? '') : undefined })
74 const glyph = isRunning ? '●' : isInterrupted ? '–' : isErrored ? '✗' : '✓'
75 const colour = isErrored ? RED : undefined
76 const agentType = tool === 'Agent' ? String((e.props.input as { subagent_type?: unknown }).subagent_type ?? 'agent') : null
77
78 return (
79 <Box flexDirection="row">
80 <Text color={isRunning ? ACCENT : colour} dimColor={isInterrupted}>
81 {glyph}
82 </Text>
83 <Text color={colour}>{` ${verb.padEnd(7)} `}</Text>
84 {agentType !== null && <Text backgroundColor={hex(hueOf(agentType))}>{' '}</Text>}
85 {agentType !== null && <Text> </Text>}
86 <Box flexGrow={1} flexShrink={1}>
87 <Text color={colour} wrap="truncate-end">
88 {what}
89 </Text>
90 </Box>
91 {note ? <Text color={colour} dimColor={!isErrored}>{` ${clip(note, 60)}`}</Text> : null}
92 </Box>
93 )
94 })
95
96 on('ui.render', { component: 'ToolResult' }, ($, e, next) => {
97 if (e.surface !== 'terminal' || e.props.isExpanded || !isLedger(e.props.tool) || (options.keepDiffs !== false && KEEP_RESULT.has(e.props.tool))) {
98 return next(e)
99 }
100
101 const { Box } = $.ui.resolve(e)
102
103 return <Box />
104 })
105
106 on('ui.render', { component: 'ToolGroup' }, ($, e, next) => {
107 if (e.surface !== 'terminal' || e.props.isExpanded || e.props.isCallsOnly !== true || e.props.calls.length === 0) {
108 return next(e)
109 }
110
111 const { Text } = $.ui.resolve(e)
112 const isRunning = e.props.calls.some(call => call.isRunning)
113
114 return (
115 <Text wrap="truncate-end">
116 <Text color={isRunning ? ACCENT : undefined}>{isRunning ? '●' : '✓'}</Text>
117 {` ${groupLine(e.props.calls)}`}
118 </Text>
119 )
120 })
121}
122hooks/avatar.ts 95 lines1// A 6×6 mirrored identicon per agent: colour from its type, pattern from its id.
2// Drawn as a Raster of 6 columns × 3 rows, each cell two pixels high.
3
4const HUES: Record<string, number> = {
5 Explore: 0x4fa3a5,
6 Plan: 0x6a8fd8,
7 'general-purpose': 0xc9a66b,
8 fork: 0x7faf6a,
9}
10
11const SPARE = [0x9b84d6, 0xcf7f9a, 0x7faf6a, 0x4fa3a5, 0x6a8fd8, 0xc9a66b]
12
13const DEFAULT = 0x01000000
14const UPPER = 0x2580
15const LOWER = 0x2584
16const FULL = 0x2588
17const SPACE = 0x20
18
19export const COLUMNS = 6
20export const ROWS = 3
21
22export function hash(text: string): number {
23 let h = 2166136261
24
25 for (let i = 0; i < text.length; i++) {
26 h ^= text.charCodeAt(i)
27 h = Math.imul(h, 16777619)
28 }
29
30 h ^= h >>> 15
31 h = Math.imul(h, 0x2c1b3c6d)
32 h ^= h >>> 12
33
34 return h >>> 0
35}
36
37export function hueOf(type: string): number {
38 return HUES[type] ?? SPARE[hash(type) % SPARE.length]!
39}
40
41export const hex = (color: number) => '#' + color.toString(16).padStart(6, '0')
42
43function dim(color: number): number {
44 const scale = (shift: number) => Math.round(((color >> shift) & 0xff) * 0.45) << shift
45
46 return scale(16) | scale(8) | scale(0)
47}
48
49export function pixels(id: string, type: string): boolean[][] {
50 const h = hash(id + ':' + type)
51
52 return Array.from({ length: 6 }, (_, y) =>
53 Array.from({ length: 6 }, (_, x) => ((h >>> (y * 3 + (x < 3 ? x : 5 - x))) & 1) === 1),
54 )
55}
56
57const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
58
59function base64(bytes: Uint8Array): string {
60 let out = ''
61
62 for (let i = 0; i < bytes.length; i += 3) {
63 const a = bytes[i]!
64 const b = bytes[i + 1]
65 const c = bytes[i + 2]
66 const n = (a << 16) | ((b ?? 0) << 8) | (c ?? 0)
67 out += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]!
68 out += b === undefined ? '=' : B64[(n >> 6) & 63]!
69 out += c === undefined ? '=' : B64[n & 63]!
70 }
71
72 return out
73}
74
75// The Raster's `cells`: one [codePoint, foreground, background] triplet per cell.
76export function avatarCells(id: string, type: string, isDim: boolean): string {
77 const grid = pixels(id, type)
78 const color = isDim ? dim(hueOf(type)) : hueOf(type)
79 const words = new Uint32Array(COLUMNS * ROWS * 3)
80
81 for (let row = 0; row < ROWS; row++) {
82 for (let x = 0; x < COLUMNS; x++) {
83 const top = grid[row * 2]![x]!
84 const bottom = grid[row * 2 + 1]![x]!
85 const glyph = top && bottom ? FULL : top ? UPPER : bottom ? LOWER : SPACE
86 const at = (row * COLUMNS + x) * 3
87 words[at] = glyph
88 words[at + 1] = glyph === SPACE ? DEFAULT : color
89 words[at + 2] = DEFAULT
90 }
91 }
92
93 return base64(new Uint8Array(words.buffer))
94}
95hooks/describe.ts 269 lines1type Input = { tool: string } & Record<string, unknown>
2
3export type FileDiff = { file: string; added: number; removed: number }
4
5export type TurnStat = { durationMs: number; tools: number; files: number; added: number; removed: number }
6
7const HOME = /^\/(Users|home)\/[^/]+/
8
9export function relative(path: string, cwd: string): string {
10 if (cwd && path.startsWith(cwd + '/')) {
11 return path.slice(cwd.length + 1)
12 }
13
14 return path.replace(HOME, '~')
15}
16
17export function clip(text: string, max: number): string {
18 const line = text.split('\n')[0]!.replace(/\s+/g, ' ').trim()
19
20 return line.length > max ? line.slice(0, max - 1) + '…' : line
21}
22
23function str(input: Input, key: string): string {
24 const value = input[key]
25
26 return typeof value === 'string' ? value : ''
27}
28
29// What the main loop is doing, as one short present-tense phrase.
30export function describe(input: Input, cwd: string): string {
31 const file = relative(str(input, 'file_path') || str(input, 'notebook_path'), cwd)
32
33 switch (input.tool) {
34 case 'Read':
35 return `Reading ${file}`
36 case 'Edit':
37 case 'MultiEdit':
38 case 'NotebookEdit':
39 return `Editing ${file}`
40 case 'Write':
41 return `Writing ${file}`
42 case 'Bash':
43 return `Running ${clip(str(input, 'command'), 60)}`
44 case 'WebFetch':
45 return `Fetching ${clip(str(input, 'url').replace(/^https?:\/\//, ''), 50)}`
46 case 'WebSearch':
47 return `Searching the web for "${clip(str(input, 'query'), 40)}"`
48 case 'Agent': {
49 const type = str(input, 'subagent_type') || 'agent'
50
51 return `Waiting on ${type}: ${clip(str(input, 'description'), 40)}`
52 }
53 case 'Skill':
54 return `Using skill ${str(input, 'skill')}`
55 case 'TodoWrite':
56 case 'TaskCreate':
57 case 'TaskUpdate':
58 return 'Updating tasks'
59 }
60
61 const mcp = /^mcp__(.+?)__(.+)$/.exec(input.tool)
62
63 if (mcp) {
64 return `Calling ${mcp[1]} ${mcp[2]}`
65 }
66
67 return `Using ${input.tool}`
68}
69
70// The tool's short verb and what it acts on, for a ledger-style row.
71export function target(input: Input, cwd: string): { verb: string; what: string } {
72 const file = relative(str(input, 'file_path') || str(input, 'notebook_path'), cwd)
73
74 switch (input.tool) {
75 case 'Read':
76 case 'Write':
77 return { verb: input.tool, what: file }
78 case 'Edit':
79 case 'MultiEdit':
80 case 'NotebookEdit':
81 return { verb: 'Edit', what: file }
82 case 'Bash':
83 return { verb: 'Bash', what: clip(str(input, 'command'), 200) }
84 case 'WebFetch':
85 return { verb: 'Fetch', what: clip(str(input, 'url').replace(/^https?:\/\//, ''), 200) }
86 case 'WebSearch':
87 return { verb: 'Search', what: `"${clip(str(input, 'query'), 200)}" on the web` }
88 case 'Agent':
89 return { verb: 'Agent', what: `${str(input, 'subagent_type') || 'agent'} · ${clip(str(input, 'description'), 200)}` }
90 case 'Skill':
91 return { verb: 'Skill', what: str(input, 'skill') }
92 }
93
94 const mcp = /^mcp__(.+?)__(.+)$/.exec(input.tool)
95
96 if (mcp) {
97 return { verb: mcp[1]!, what: mcp[2]! }
98 }
99
100 const first = Object.entries(input).find(([key, value]) => key !== 'tool' && typeof value === 'string')
101
102 return { verb: input.tool, what: first ? clip(String(first[1]), 200) : '' }
103}
104
105type Ran = { result?: unknown; text?: string; isError?: boolean; deny?: string }
106
107// One plain summary of a finished call: lines read, diff size, the first error line.
108export function noteOf(tool: string, ran: Ran): string {
109 if (ran.deny !== undefined) {
110 return clip(`refused: ${ran.deny}`, 80)
111 }
112
113 if (ran.isError) {
114 const lines = (ran.text ?? '').split('\n').map(one => one.trim())
115 const code = lines.map(one => /^(?:error: )?exit code (\d+)/i.exec(one)).find(Boolean)
116 const first = lines.find(one => one && !/^(?:error: )?exit code \d+/i.test(one))
117 const parts = [code ? `exit ${code[1]}` : '', first ?? (code ? '' : 'failed')].filter(Boolean)
118
119 return clip(parts.join(' · '), 80)
120 }
121
122 const r = (ran.result ?? {}) as Record<string, unknown>
123
124 if (tool === 'Read') {
125 const file = r.file as { numLines?: number } | undefined
126
127 return typeof file?.numLines === 'number' ? `${file.numLines} lines` : ''
128 }
129
130 const diffs = diffsOf(tool, ran.result)
131
132 if (tool === 'Write' && r.type === 'create' && diffs[0]) {
133 return `new · ${diffs[0].added} lines`
134 }
135
136 if (diffs.length) {
137 const added = diffs.reduce((sum, d) => sum + d.added, 0)
138 const removed = diffs.reduce((sum, d) => sum + d.removed, 0)
139
140 return removed ? `+${added} −${removed}` : `+${added}`
141 }
142
143 return ''
144}
145
146type Hunk = { lines: string[] }
147
148function count(file: string, hunks: readonly Hunk[]): FileDiff {
149 let added = 0
150 let removed = 0
151
152 for (const hunk of hunks) {
153 for (const line of hunk.lines) {
154 if (line.startsWith('+')) added += 1
155 else if (line.startsWith('-')) removed += 1
156 }
157 }
158
159 return { file, added, removed }
160}
161
162// The files a finished call changed, with line counts, from the tool's own result.
163export function diffsOf(tool: string, result: unknown): FileDiff[] {
164 if (!result || typeof result !== 'object') {
165 return []
166 }
167
168 const r = result as Record<string, unknown>
169
170 if ((tool === 'Edit' || tool === 'Write') && typeof r.filePath === 'string') {
171 if (tool === 'Write' && r.type === 'create' && typeof r.content === 'string') {
172 return [{ file: r.filePath, added: r.content.replace(/\n$/, '').split('\n').length, removed: 0 }]
173 }
174
175 return [count(r.filePath, (r.structuredPatch as Hunk[] | undefined) ?? [])]
176 }
177
178 if (tool === 'Bash' && r.bashEditDiff && typeof r.bashEditDiff === 'object') {
179 const files = (r.bashEditDiff as { files?: { filePath: string; hunks: Hunk[] }[] }).files ?? []
180
181 return files.map(one => count(one.filePath, one.hunks))
182 }
183
184 return []
185}
186
187export function duration(ms: number): string {
188 const s = Math.round(ms / 1000)
189
190 if (s < 60) return `${s}s`
191 if (s < 3600) return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
192
193 return `${Math.floor(s / 3600)}h ${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}m`
194}
195
196const plural = (n: number, one: string) => `${n} ${one}${n === 1 ? '' : 's'}`
197
198export function turnLine(durationMs: number, stat: TurnStat | undefined): string {
199 const parts = [`✓ Done in ${duration(durationMs)}`]
200
201 if (stat) {
202 parts.push(stat.tools ? plural(stat.tools, 'tool') : 'no tools')
203 if (stat.files) parts.push(`${plural(stat.files, 'file')} changed`)
204 if (stat.added || stat.removed) parts.push(`+${stat.added} −${stat.removed}`)
205 }
206
207 return parts.join(' · ')
208}
209
210// The turn whose recorded length is nearest to the line's, within 1.5 s.
211export function closest(list: readonly TurnStat[], durationMs: number): TurnStat | undefined {
212 let best: TurnStat | undefined
213 let gap = 1500
214
215 for (const stat of list) {
216 const d = Math.abs(stat.durationMs - durationMs)
217
218 if (d <= gap) {
219 best = stat
220 gap = d
221 }
222 }
223
224 return best
225}
226
227export function modelLabel(model: string): string {
228 const m = /^claude-([a-z]+)-(\d+)-(\d+)/.exec(model)
229
230 return m ? `${m[1]}-${m[2]}.${m[3]}` : model.replace(/^claude-/, '')
231}
232
233type Usage = {
234 context?: { percent?: number; tokens?: number; window?: number }
235 rateLimits?: readonly { kind: string; percentUsed: number }[]
236 cost?: { usd: number }
237}
238
239// 950, 76k, 1M, 1.2M: token counts the way the status line reads them.
240export function tokenCount(n: number): string {
241 if (n < 1000) return String(n)
242 if (n < 1_000_000) return `${Math.round(n / 1000)}k`
243
244 return `${Number((n / 1_000_000).toFixed(1))}M`
245}
246
247export function statusLine(model: string, usage: Usage, effort?: string | number, warnAt = 75): string {
248 const parts = [effort === undefined ? modelLabel(model) : `${modelLabel(model)} ${effort}`]
249 const pct = usage.context?.percent
250 const tokens = usage.context?.tokens
251 const window = usage.context?.window
252
253 if (pct !== undefined) {
254 const fill = tokens !== undefined && window ? `context ${tokenCount(tokens)}/${tokenCount(window)} (${pct}%), ${tokenCount(Math.max(0, window - tokens))} left` : `context ${pct}%`
255 parts.push(pct >= warnAt ? `${fill}, compact soon` : fill)
256 }
257
258 for (const limit of usage.rateLimits ?? []) {
259 if (limit.kind === 'five_hour') parts.push(`5h limit ${Math.round(limit.percentUsed)}%`)
260 if (limit.kind === 'seven_day') parts.push(`week limit ${Math.round(limit.percentUsed)}%`)
261 }
262
263 if (usage.cost) {
264 parts.push(`$${usage.cost.usd.toFixed(2)}`)
265 }
266
267 return parts.join(' · ')
268}
269