Shows a one-line summary instead of the diff under Edit, Write, NotebookEdit and Bash calls (ctrl+q), and folds each shell command to one line (alt+q)

Tired of long diffs filling your Claude Code screen? This mod swaps them for a short summary:
Press ctrl+q any time to show the full diffs again.
It also folds each shell command to one line, ● Bash(npm test -- --run src/api) ⎿ 24 lines. Press alt+q to show commands in full.
Without hide-diffs
With hide-diffs
With compact mode (turn it on in settings)
Without folded commands
With folded commands (alt+q switches them)
Diffs hidden (ctrl+q to show) · Commands folded (alt+q to show) · 14 files (+320 −85) this session● Update(src/api/index.ts) 🔶 (+12 −3), so each change takes one line.Works on Windows, macOS and Linux.
1. Add the mod. In Claude Code, type these two commands:
/plugin marketplace add gixxy22/hide-diffs
/plugin install hide-diffs@gixxy22-plugins
2. Turn on the ctrl+q and alt+q shortcuts. Open your keybindings file:
C:\Users\<you>\.claude\keybindings.json~/.claude/keybindings.jsonIf the file doesn't exist, create it with this:
{
"bindings": [
{ "context": "Global", "bindings": { "ctrl+q": "settings:sortByTokens", "alt+q": "settings:periodWeek" } }
]
}
If it already exists, just add the "ctrl+q": "settings:sortByTokens" and "alt+q": "settings:periodWeek" lines inside your Global bindings.
Why
settings:sortByTokensandsettings:periodWeek? A mod can only borrow a shortcut action that Claude Code isn't using at that moment. Those two are only used inside the/usagescreen, so ctrl+q and alt+q always reach hide-diffs.
Prefer a different key? Use any key you like in place of
ctrl+q.
3. Restart Claude Code. That's it.
Type /config and look for hide-diffs.
| Setting | Default | What it does |
|---|---|---|
| Show small diffs | 5 | Diffs with this many changed lines or fewer still show in full. Set it to 0 to hide every diff. |
| Icon | 🔶 | Shown at the start of each hidden diff. Use any emoji or symbol, or leave it empty for none. |
| Compact mode | off | Puts the line counts on an edit's own line, like ● Write(src/a.ts) 🔶 (+12 −0), and drops the summary line under it. Shell commands and notebooks keep their summary line. |
Run these in a terminal, then restart Claude Code:
claude plugin marketplace update gixxy22-plugins
claude plugin update hide-diffs@gixxy22-plugins
Run this in a terminal, then restart Claude Code:
claude plugin uninstall hide-diffs@gixxy22-plugins
MIT
hooks/register.tsx 507 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { CallChanges, CommandOutcome, CommandOutcomes, SessionTally } from '../types'
5
6type PatchHunk = { newStart?: number; lines: string[] }
7type FileChangeOutput = {
8 filePath: string
9 structuredPatch?: PatchHunk[]
10 type?: 'create' | 'update'
11 content?: string
12 gitDiff?: { additions: number; deletions: number }
13 staged?: boolean
14}
15type NotebookOutput = { notebook_path: string; cell_id?: string; old_source?: string; new_source: string }
16type BashEditDiff = { files: { filePath: string; hunks: PatchHunk[] }[]; moreFiles: number }
17type BashOutput = { bashEditDiff?: BashEditDiff }
18type ShellOutput = BashOutput & {
19 stdout?: string
20 stderr?: string
21 interrupted?: boolean
22 backgroundTaskId?: string
23 returnCodeInterpretation?: string
24}
25type Change = { added: number; removed: number }
26type TextElement = ReturnType<EngineInterface['ui']['resolve']>['Text']
27
28const DIFF_TOOLS = new Set(['Edit', 'Write', 'MultiEdit'])
29const TRACKED_TOOLS = ['Edit', 'Write', 'NotebookEdit', 'Bash'] as const
30// Tools whose runs the transcript folds into one line (`Made 3 edits +33 -5`).
31const FILE_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit'])
32
33// The chord bound to this action in ~/.claude/keybindings.json (ctrl+q) presses the toggle button.
34// A plugin can only borrow an engine action while no engine handler of it is mounted: the diff
35// panel holds app:toggleDiffNoiseFilter whenever the diff has test files, so ctrl+q never reached
36// the button. settings:sortByTokens is only handled inside the /usage dialog.
37const TOGGLE_ACTION = 'settings:sortByTokens'
38const STORE_KEY = 'isHiding'
39const isHiding = atom({ plugin: 'hide-diffs', key: 'isHiding' } as const, true)
40// The second chord (alt+q) folds shell commands; settings:periodWeek is another /usage-only action.
41const COMMANDS_ACTION = 'settings:periodWeek'
42const COMMANDS_STORE_KEY = 'isFoldingCommands'
43const isFoldingCommands = atom({ plugin: 'hide-diffs', key: 'isFoldingCommands' } as const, true)
44// Each finished shell call's one-line outcome: a finished row need not carry its result.
45const outcomes = atom({ plugin: 'hide-diffs', key: 'outcomes' } as const, {} as CommandOutcomes)
46const SHELL_TOOLS = new Set(['Bash', 'PowerShell'])
47const tally = atom({ plugin: 'hide-diffs', key: 'tally' } as const, { files: [], added: 0, removed: 0 } as SessionTally)
48// The project root when the session started; paths stay relative to it after `/cd` or a worktree move.
49const startRoot = atom({ plugin: 'hide-diffs', key: 'startRoot' } as const, null as string | null)
50// Each finished call's counts, for the compact line: a live call row need not carry its result.
51const changes = atom({ plugin: 'hide-diffs', key: 'changes' } as const, {} as CallChanges)
52
53function countChangedLines(hunks: PatchHunk[] = []): Change {
54 let added = 0
55 let removed = 0
56 for (const hunk of hunks) {
57 for (const line of hunk.lines) {
58 if (line.startsWith('+')) added += 1
59 else if (line.startsWith('-')) removed += 1
60 }
61 }
62 return { added, removed }
63}
64
65// The lines a finished Edit or Write added and removed; undefined for one held for
66// review, which leaves the file unchanged and whose diff the person needs to see.
67function fileChange(output: FileChangeOutput): Change | undefined {
68 if (output.staged) return undefined
69 // A new file's patch can be empty: count the lines it was written with.
70 if (output.type === 'create') {
71 return { added: output.content ? output.content.replace(/\n$/, '').split('\n').length : 0, removed: 0 }
72 }
73 // An empty patch means the diff timed out or the file was too large; git's counts stand in.
74 if (!output.structuredPatch?.length && output.gitDiff) {
75 return { added: output.gitDiff.additions, removed: output.gitDiff.deletions }
76 }
77 return countChangedLines(output.structuredPatch)
78}
79
80// The new-file line number of the first added or removed line.
81function firstChangedLine(hunks: PatchHunk[] = []) {
82 const hunk = hunks[0]
83 if (!hunk?.newStart) return undefined
84 let line = hunk.newStart
85 for (const text of hunk.lines) {
86 if (text.startsWith('+') || text.startsWith('-')) return line
87 line += 1
88 }
89 return undefined
90}
91
92// Lines only in one side of a cell, counted as a multiset: close enough for a summary.
93function countNotebookLines(output: NotebookOutput): Change {
94 const before = output.old_source ? output.old_source.split('\n') : []
95 const after = output.new_source ? output.new_source.split('\n') : []
96 const remaining = new Map<string, number>()
97 for (const line of before) remaining.set(line, (remaining.get(line) ?? 0) + 1)
98 let added = 0
99 for (const line of after) {
100 const left = remaining.get(line) ?? 0
101 if (left > 0) remaining.set(line, left - 1)
102 else added += 1
103 }
104 let removed = 0
105 for (const left of remaining.values()) removed += left
106 return { added, removed }
107}
108
109function relativePath(filePath: string, projectRoot: string) {
110 const path = filePath.replace(/\\/g, '/')
111 const root = projectRoot.replace(/\\/g, '/').replace(/\/$/, '') + '/'
112 // Windows paths ignore case; elsewhere a folder differing only in case is another folder.
113 const isWindows = /^[a-z]:\//i.test(root)
114 const isInside = isWindows ? path.toLowerCase().startsWith(root.toLowerCase()) : path.startsWith(root)
115 return isInside ? path.slice(root.length) : path
116}
117
118function summarize(change: Change) {
119 return `(+${change.added} −${change.removed})`
120}
121
122// `(+N −N)` with the added count green and the removed count red, in the theme's colours.
123function coloredSummary(Text: TextElement, change: Change) {
124 return (
125 <Text>
126 (<Text color="success">+{change.added}</Text> <Text color="error">−{change.removed}</Text>)
127 </Text>
128 )
129}
130
131// What a hidden diff's summary says: the file it changed and the lines it added and removed.
132// Undefined for anything drawn in full: another tool, a missing output, a review, a small diff.
133type HiddenChange = { what: string; change: Change }
134
135function describeChange(tool: string, output: unknown, root: string, isSmall: (change: Change) => boolean): HiddenChange | undefined {
136 if (tool === 'Bash') {
137 const changes = (output as BashOutput | undefined)?.bashEditDiff
138 if (!changes) return undefined
139 const fileCount = changes.files.length + changes.moreFiles
140 const change = countChangedLines(changes.files.flatMap(file => file.hunks))
141 if (fileCount === 0 || isSmall(change)) return undefined
142 return { what: `Changed ${fileCount} file${fileCount === 1 ? '' : 's'}`, change }
143 }
144
145 if (tool === 'NotebookEdit') {
146 const notebook = output as NotebookOutput | undefined
147 if (!notebook?.notebook_path) return undefined
148 const change = countNotebookLines(notebook)
149 if (isSmall(change)) return undefined
150 const cell = notebook.cell_id ? ` cell ${notebook.cell_id}` : ''
151 return { what: `Changed ${relativePath(notebook.notebook_path, root)}${cell}`, change }
152 }
153
154 if (!DIFF_TOOLS.has(tool)) return undefined
155 const file = output as FileChangeOutput | undefined
156 if (!file?.filePath) return undefined
157 const change = fileChange(file)
158 if (!change || isSmall(change)) return undefined
159 const verb = file.type === 'create' ? 'Created' : 'Changed'
160 const line = file.type === 'create' ? undefined : firstChangedLine(file.structuredPatch)
161 return { what: `${verb} ${relativePath(file.filePath, root)}${line ? `:${line}` : ''}`, change }
162}
163
164// How the engine lays out an Edit or Write row, which no API reports (as akilin/claude-plugins'
165// better-tool-rows measures it): a blank line, then `● Update(path)` as wide as the line,
166// up to GROUP_INDENT columns further in inside a group, a result line opening with ROW_GUTTER.
167const GROUP_INDENT = 6
168const ROW_GUTTER = ' ⎿ '
169// Tools whose row label is `● Name(file_path)`, so the counts can be laid just past it.
170const LABELED_TOOLS = new Set(['Edit', 'Write'])
171
172// The name the engine draws an Edit or Write row under.
173function rowName(tool: string, input: { old_string?: unknown }) {
174 return tool === 'Write' ? 'Write' : input.old_string === '' ? 'Create' : 'Update'
175}
176
177// The columns a string takes on a terminal: 2 for wide CJK characters and pictographs.
178function textWidth(text: string) {
179 let width = 0
180 for (const char of text) {
181 const c = char.codePointAt(0) ?? 0
182 if (/^[\p{Mn}\p{Me}-︀-️]$/u.test(char)) continue
183 const isWide =
184 (c >= 0x1100 && c <= 0x115f) || (c >= 0x2e80 && c <= 0xa4cf) || (c >= 0xac00 && c <= 0xd7a3) ||
185 (c >= 0xf900 && c <= 0xfaff) || (c >= 0xff00 && c <= 0xff60) || (c >= 0x1f300 && c <= 0x1faff)
186 width += isWide ? 2 : 1
187 }
188 return width
189}
190
191// Relative to `root` with its own separators, as the engine draws a path it is handed.
192function shortPath(path: string, root: string) {
193 const dir = /[\\/]$/.test(root) ? root : `${root}/`
194 const head = path.slice(0, dir.length)
195 const isWindows = /^([A-Za-z]:[\\/]|\\\\)/.test(root)
196 const fold = (text: string) => text.replace(/\\/g, '/').toLowerCase()
197 return (isWindows ? fold(head) === fold(dir) : head === dir) ? path.slice(dir.length) : path
198}
199
200// `text` cut to `width` columns, ending in `…` where it was cut.
201function fitWidth(text: string, width: number) {
202 if (textWidth(text) <= width) return text
203 let fitted = ''
204 for (const char of text) {
205 if (textWidth(fitted + char) > width - 1) break
206 fitted += char
207 }
208 return `${fitted}…`
209}
210
211function lineCount(text = '') {
212 const trimmed = text.replace(/\s+$/, '')
213 return trimmed ? trimmed.split('\n').length : 0
214}
215
216function plural(count: number, word: string) {
217 return `${count} ${word}${count === 1 ? '' : 's'}`
218}
219
220// What a folded command's line says of its result: `16 lines`, `no output`, or an
221// error's first line (`Exit code 1 (+3 lines)`).
222function commandOutcome(isErrored: boolean, output: unknown): CommandOutcome {
223 if (isErrored) {
224 const lines = (typeof output === 'string' ? output : '').split('\n').map(line => line.trim()).filter(Boolean)
225 const first = fitWidth((lines[0] ?? 'Failed').replace(/^Error:\s*/, ''), 40)
226 return { text: lines.length > 1 ? `${first} (+${plural(lines.length - 1, 'line')})` : first, isError: true }
227 }
228 const result = output as ShellOutput | undefined
229 if (result?.backgroundTaskId) return { text: 'running in background', isError: false }
230 const lines = lineCount(result?.stdout) + lineCount(result?.stderr)
231 if (lines > 0) return { text: plural(lines, 'line'), isError: false }
232 return { text: result?.returnCodeInterpretation || 'no output', isError: false }
233}
234
235export const register: Register = (on, options) => {
236 // Diffs with this many changed lines or fewer are drawn in full; 0 hides every diff.
237 const smallDiffLines = Math.max(0, Number(options.smallDiffLines ?? 5))
238 const isSmall = (change: Change) => change.added + change.removed <= smallDiffLines
239 // Marks where a diff was hidden; empty for none.
240 const icon = String(options.icon ?? '🔶').trim()
241 const lead = icon ? `${icon} ` : ''
242 // Puts the counts on the call's own line and drops the summary line under it.
243 const compact = options.compact === true
244
245 // Restore the last toggle from the store, so it survives restarts, and record the starting root.
246 on('session.start', async ($, e, next) => {
247 if ((await read($, startRoot)) === null) {
248 const root = await $.session.root()
249 await update($, startRoot, () => root)
250 }
251 const saved = await $.store.get(STORE_KEY)
252 if (typeof saved === 'boolean') await update($, isHiding, () => saved)
253 const savedFolding = await $.store.get(COMMANDS_STORE_KEY)
254 if (typeof savedFolding === 'boolean') await update($, isFoldingCommands, () => savedFolding)
255 return next(e)
256 })
257
258 // Record each finished shell command's outcome for its folded line.
259 on('tool.call', { tool: ['Bash', 'PowerShell'] }, async ($, e, next) => {
260 const ran = await next(e)
261 if (ran.deny !== undefined) return ran
262 const outcome = commandOutcome(ran.isError === true, ran.isError ? ran.text : ran.result)
263 await update($, outcomes, current => ({ ...current, [e.tool_use_id]: outcome }))
264 return ran
265 })
266
267 // Add each finished change to the session tally shown on the switch.
268 on('tool.call', { tool: TRACKED_TOOLS }, async ($, e, next) => {
269 const ran = await next(e)
270 if (ran.deny !== undefined || ran.isError) return ran
271
272 const files: string[] = []
273 let change: Change = { added: 0, removed: 0 }
274 if (e.tool === 'Bash') {
275 const diff = (ran.result as BashOutput | undefined)?.bashEditDiff
276 if (!diff) return ran
277 files.push(...diff.files.map(file => file.filePath))
278 change = countChangedLines(diff.files.flatMap(file => file.hunks))
279 } else if (e.tool === 'NotebookEdit') {
280 const output = ran.result as NotebookOutput | undefined
281 if (!output?.notebook_path) return ran
282 files.push(output.notebook_path)
283 change = countNotebookLines(output)
284 } else {
285 const output = ran.result as FileChangeOutput | undefined
286 const changed = output?.filePath ? fileChange(output) : undefined
287 if (!output || !changed) return ran
288 files.push(output.filePath)
289 change = changed
290 }
291
292 await update($, tally, current => ({
293 files: [...new Set([...current.files, ...files])],
294 added: current.added + change.added,
295 removed: current.removed + change.removed,
296 }))
297 await update($, changes, current => ({ ...current, [e.tool_use_id]: change }))
298 return ran
299 })
300
301 // A small dim switch above the prompt; it must stay mounted for the chord to reach it.
302 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
303 if (e.props.hasSurvey) return next(e)
304
305 const hiding = await read($, isHiding)
306 const folding = await read($, isFoldingCommands)
307 const totals = await read($, tally)
308 const { Box, Button, Text } = $.ui.resolve(e)
309 const fileCount = totals.files.length
310 const tallyText = fileCount
311 ? ` · ${fileCount} file${fileCount === 1 ? '' : 's'} ${summarize(totals)} this session`
312 : ''
313
314 return (
315 <Box>
316 <Button
317 action={TOGGLE_ACTION}
318 dimColor
319 key="toggle-diffs"
320 label={hiding ? 'Diffs hidden (ctrl+q to show)' : 'Diffs shown (ctrl+q to hide)'}
321 onPress={async () => {
322 const nowHiding = await update($, isHiding, value => !value)
323 await $.store.set(STORE_KEY, nowHiding)
324 $.ui.toast(nowHiding ? 'Diffs hidden' : 'Diffs shown')
325 }}
326 plain
327 />
328 <Text dimColor> · </Text>
329 <Button
330 action={COMMANDS_ACTION}
331 dimColor
332 key="toggle-commands"
333 label={folding ? 'Commands folded (alt+q to show)' : 'Commands shown (alt+q to fold)'}
334 onPress={async () => {
335 const nowFolding = await update($, isFoldingCommands, value => !value)
336 await $.store.set(COMMANDS_STORE_KEY, nowFolding)
337 $.ui.toast(nowFolding ? 'Commands folded to one line' : 'Commands shown in full')
338 }}
339 plain
340 />
341 {tallyText ? <Text dimColor>{tallyText}</Text> : null}
342 </Box>
343 )
344 })
345
346 // The transcript folds runs of edits into one line; while diffs are shown, unfold
347 // those runs so ctrl+q visibly brings every diff back.
348 on('ui.render', { component: 'ToolGroup' }, async ($, e, next) => {
349 if (e.props.isExpanded || !e.props.calls.some(call => FILE_TOOLS.has(call.tool))) return next(e)
350 if (!(await read($, isHiding))) return next({ ...e, props: { ...e.props, isExpanded: true } })
351
352 // Folded: lay the icon over the line's blank indent, so the engine's text stays where it is.
353 const row = await next(e)
354 if (!icon || e.surface !== 'terminal') return row
355 const { Box, Text } = $.ui.resolve(e)
356 return (
357 <Box>
358 {row}
359 <Box position="absolute" bottom={0} left={0}>
360 <Text>{icon}</Text>
361 </Box>
362 </Box>
363 )
364 })
365
366 // Compact: the counts ride on the call's own line (`● Write(src/a.ts) (+12 −0)`) and
367 // the result line under it is dropped, so each hidden diff takes one line.
368 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
369 if (!compact || !LABELED_TOOLS.has(e.props.tool)) return next(e)
370 if (e.props.isRunning || e.props.isErrored || e.props.isInterrupted) return next(e)
371 const input = e.props.input as { file_path?: unknown; old_string?: unknown } | undefined
372 if (typeof input?.file_path !== 'string' || !(await read($, isHiding))) return next(e)
373
374 // The row's own result when it carries one, else what the call recorded.
375 const root = (await read($, startRoot)) ?? (await $.session.root())
376 const recorded = (await read($, changes))[e.props.tool_use_id]
377 const change =
378 describeChange(e.props.tool, e.props.output, root, isSmall)?.change ??
379 (recorded && !isSmall(recorded) ? recorded : undefined)
380 if (!change) return next(e)
381
382 // Hand the engine the path relative to the root, so the label's width is known, and
383 // leave its row whole (status dot and all): the counts are laid over its last line.
384 const path = shortPath(input.file_path, root)
385 const row = await next({ ...e, props: { ...e.props, input: { ...input, file_path: path } } })
386 const { Box, Text } = $.ui.resolve(e)
387 const counts = (
388 <Text dimColor>
389 {lead}
390 {coloredSummary(Text, change)}
391 </Text>
392 )
393 const labelWidth = textWidth(`● ${rowName(e.props.tool, input)}(${path})`)
394 const countsWidth = textWidth(`${lead}${summarize(change)}`)
395 const columns = e.viewport?.columns ?? 80
396
397 // A label too long for the line wraps; the counts go on a line of their own beneath it.
398 if (labelWidth + 2 + countsWidth > columns - GROUP_INDENT) {
399 return (
400 <Box flexDirection="column">
401 {row}
402 <Box>
403 <Text dimColor>{ROW_GUTTER}</Text>
404 {counts}
405 </Box>
406 </Box>
407 )
408 }
409 return (
410 <Box>
411 {row}
412 <Box position="absolute" bottom={0} left={labelWidth + 2}>
413 {counts}
414 </Box>
415 </Box>
416 )
417 })
418
419 // Folded commands: a finished shell call draws one line of its own, `● Bash(cmd…) ⎿ 16 lines`,
420 // and its output is dropped; a running one keeps the engine's row, its command cut to one line.
421 // A command whose file changes are on show (ctrl+q) stays whole.
422 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
423 if (e.surface !== 'terminal' || !SHELL_TOOLS.has(e.props.tool) || e.props.isInterrupted) return next(e)
424 const input = e.props.input as { command?: unknown } | undefined
425 if (typeof input?.command !== 'string' || !(await read($, isFoldingCommands))) return next(e)
426 const output = e.props.isErrored ? undefined : (e.props.output as ShellOutput | undefined)
427 if (!(await read($, isHiding)) && output?.bashEditDiff?.files.length) return next(e)
428
429 const width = (e.viewport?.columns ?? 80) - GROUP_INDENT
430 const command = input.command.replace(/\s+/g, ' ').trim()
431 const head = `● ${e.props.tool}()`
432 const outcome =
433 e.props.output !== undefined ? commandOutcome(e.props.isErrored, e.props.output) : (await read($, outcomes))[e.props.tool_use_id]
434 if (e.props.isRunning || !outcome) {
435 const fitted = fitWidth(command, Math.max(12, width - textWidth(head)))
436 return next({ ...e, props: { ...e.props, input: { ...input, command: fitted } } })
437 }
438
439 // What the command changed in files rides on the line too; its diff is never drawn.
440 const edits = output?.bashEditDiff
441 const change = edits?.files.length ? countChangedLines(edits.files.flatMap(file => file.hunks)) : undefined
442 const files = edits ? plural(edits.files.length + edits.moreFiles, 'file') : ''
443 const tail = ` ⎿ ${outcome.text}${change ? ` · ${lead}${files} ${summarize(change)}` : ''}`
444 const fitted = fitWidth(command, Math.max(12, width - textWidth(head) - textWidth(tail)))
445 const { Box, Text } = $.ui.resolve(e)
446 return (
447 <Box>
448 <Text color={outcome.isError ? 'error' : 'success'}>● </Text>
449 <Text bold>{e.props.tool}</Text>
450 <Text>({fitted})</Text>
451 <Text dimColor> ⎿ </Text>
452 {outcome.isError ? <Text color="error">{outcome.text}</Text> : <Text dimColor>{outcome.text}</Text>}
453 {change ? (
454 <Text dimColor>
455 {' · '}
456 {lead}
457 {files} {coloredSummary(Text, change)}
458 </Text>
459 ) : null}
460 </Box>
461 )
462 })
463
464 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
465 // A folded command's row carries its outcome, so its result block is dropped.
466 if (e.surface === 'terminal' && SHELL_TOOLS.has(e.props.tool) && (await read($, isFoldingCommands))) {
467 const output = e.props.isErrored ? undefined : (e.props.output as ShellOutput | undefined)
468 const showsEdits = !(await read($, isHiding)) && output?.bashEditDiff?.files.length
469 if (!output?.interrupted && !showsEdits) {
470 const { Box } = $.ui.resolve(e)
471 return <Box />
472 }
473 }
474 if (e.props.isErrored || !(await read($, isHiding))) return next(e)
475
476 const root = (await read($, startRoot)) ?? (await $.session.root())
477 const hidden = describeChange(e.props.tool, e.props.output, root, isSmall)
478 if (!hidden) return next(e)
479 const { Box, Text } = $.ui.resolve(e)
480
481 // Bash: draw its own output, minus the per-file diff of what the command changed.
482 if (e.props.tool === 'Bash') {
483 const { bashEditDiff: _hidden, ...outputWithoutDiff } = e.props.output as BashOutput
484 const drawn = await next({ ...e, props: { ...e.props, output: outputWithoutDiff } })
485 return (
486 <Box flexDirection="column">
487 {drawn}
488 <Text dimColor>
489 {lead}
490 {hidden.what} {coloredSummary(Text, hidden.change)}
491 </Text>
492 </Box>
493 )
494 }
495
496 // Compact: an Edit or Write row already carries the counts.
497 if (compact && LABELED_TOOLS.has(e.props.tool)) return <Box />
498
499 return (
500 <Text dimColor>
501 {lead}
502 {hidden.what} {coloredSummary(Text, hidden.change)}
503 </Text>
504 )
505 })
506}
507types/index.d.ts 21 lines1export type SessionTally = { files: string[]; added: number; removed: number }
2// The lines one finished call added and removed, keyed by its tool_use_id.
3export type CallChanges = Record<string, { added: number; removed: number }>
4// What a folded shell command's line says of its result (`16 lines`, `Exit code 1`).
5export type CommandOutcome = { text: string; isError: boolean }
6// Each finished shell call's outcome, keyed by its tool_use_id.
7export type CommandOutcomes = Record<string, CommandOutcome>
8
9declare module 'claude-code' {
10 interface PluginState {
11 'hide-diffs': {
12 isHiding: boolean
13 tally: SessionTally
14 startRoot: string | null
15 changes: CallChanges
16 isFoldingCommands: boolean
17 outcomes: CommandOutcomes
18 }
19 }
20}
21