A one-line TL;DR under every long answer, written by a small fast model, so you can skim a wall of text in two seconds.

A one-line TL;DR under every long answer, written by a small, fast model, so you can skim a wall of text in two seconds.
● A trie stores strings as paths of shared character nodes: each edge is
one character, and a word is the path from the root to a marked node.
Lookups cost O(m) for a key of length m, independent of how many keys
… (300 more words) …
TL;DR: A trie is a tree for fast prefix lookups, at the cost of more memory than a hash table.
When an answer runs past minWords words of prose (code blocks don't count), tldr asks a small model (Haiku by default) for one sentence of at most 25 words that leads with the outcome. Claude Code shows that line under the answer. The answer itself, and what the model remembers of it, stay untouched.
/tldr summarizes the last answer on demand, whatever its length./tldr off and /tldr on switch the automatic line; the setting is kept across sessions. /tldr status shows it.-p runs skip the automatic call, since they don't print the line./plugin marketplace add Singh-AP/awesome-claude-mods
/plugin install tldr@awesome-claude-mods
Requires Claude Code 2.1.287 or later.
| Option | Default | What it does |
|---|---|---|
minWords | 200 | Answers with at least this many words of prose (20–5000) get a TL;DR |
model | haiku | The model that writes it: an alias or a full model id |
| Event or call | Why |
|---|---|
turn.complete | After next(e), returns { text: 'TL;DR: …' }, which Claude Code draws beneath the answer |
$.model.complete | One tool-less completion through your session's own credentials |
command.run on tldr | On-demand summary (from $.session.messages()), plus on/off/status |
$.store | Keeps the on/off switch |
claude plugin test mods/productivity/tldr # 12 tests
claude -p prints the answer alone.hooks/register.ts 81 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { clean, promptFor, SYSTEM, wordCount } from './tldr'
4
5// Whether long answers get a TL;DR on their own; /tldr on|off keeps it in $.store.
6let isAuto = true
7// A -p run prints the answer alone, so a TL;DR there would cost a call nobody sees.
8let isHeadless = false
9
10/** One `TL;DR: …` line for `answer`, or undefined when the model gave none. */
11async function summarize($: EngineInterface, answer: string, model: string): Promise<string | undefined> {
12 const reply = await $.model
13 .complete({ model, system: SYSTEM, prompt: promptFor(answer), maxTokens: 120, effort: 'low', timeoutMs: 15_000 })
14 .catch(() => undefined)
15 if (reply === undefined || !reply.isAnswered) return undefined
16 const line = clean(reply.text)
17 return line === '' ? undefined : `TL;DR: ${line}`
18}
19
20async function setAuto($: EngineInterface, value: boolean): Promise<void> {
21 isAuto = value
22 await $.store.set('auto', value)
23}
24
25/** The last answer Claude gave in this conversation, or '' before the first. */
26async function lastAnswer($: EngineInterface): Promise<string> {
27 const messages = await $.session.messages()
28 for (let i = messages.length - 1; i >= 0; i--) {
29 const message = messages[i]
30 if (message?.role === 'assistant' && message.text.trim() !== '') return message.text
31 }
32 return ''
33}
34
35export const register: Register = (on, options) => {
36 const minWords = Math.max(1, Number(options.minWords ?? 200))
37 const model = String(options.model ?? 'haiku')
38
39 on('session.start', async ($, e, next) => {
40 isAuto = (await $.store.get('auto')) !== false
41 isHeadless = !e.isInteractive
42 try {
43 await $.command.register({
44 name: 'tldr',
45 description: 'TL;DR of the last answer, or turn the automatic one on/off',
46 argumentHint: '[on | off | status]',
47 })
48 } catch (error) {
49 $.ui.log(`tldr: could not add /tldr: ${String(error)}`, { to: 'debug' })
50 }
51 return next(e)
52 })
53
54 on('turn.complete', async ($, e, next) => {
55 const result = await next(e)
56 const isEligible = isAuto && !isHeadless && e.agentId === undefined && e.reason === 'answer' && !e.isAborted
57 if (!isEligible || wordCount(e.answer) < minWords) return result
58
59 const line = await summarize($, e.answer, model)
60 if (line === undefined) return result
61 // A text other than the answer is shown beneath it; keep another mod's line too.
62 return { ...result, text: result.text === e.answer ? line : `${result.text}\n${line}` }
63 })
64
65 on('command.run', { command: 'tldr' }, async ($, e) => {
66 const word = e.args.trim().toLowerCase()
67 if (word === 'on' || word === 'off') {
68 await setAuto($, word === 'on')
69 return { text: word === 'on' ? `tldr is on: answers of ${minWords}+ words get a one-line TL;DR.` : 'tldr is off. /tldr still summarizes the last answer on demand.' }
70 }
71 if (word === 'status') {
72 return { text: `tldr is ${isAuto ? 'on' : 'off'}: answers of ${minWords}+ words get a TL;DR from ${model}.` }
73 }
74 if (word !== '') return { text: 'Usage: /tldr (summarize the last answer) · /tldr on · /tldr off · /tldr status' }
75
76 const answer = await lastAnswer($)
77 if (answer === '') return { text: 'Nothing to summarize yet.' }
78 return { text: (await summarize($, answer, model)) ?? `tldr: ${model} gave no summary; try again.` }
79 })
80}
81hooks/tldr.ts 32 lines1// Pure helpers: no `$`, so tests import them directly.
2
3export const SYSTEM =
4 "You write TL;DR lines. Given an AI coding assistant's answer, reply with ONE plain sentence of at most 25 words " +
5 'that tells a developer skimming a terminal the outcome: what was found, done, decided or recommended. ' +
6 'No markdown, no preamble, no quotes, no "TL;DR" label.'
7
8const FENCE = /```[\s\S]*?(```|$)/g
9
10/** Words of prose: fenced code blocks don't count. */
11export function wordCount(text: string): number {
12 return text.replace(FENCE, ' ').split(/\s+/).filter(word => /[\p{L}\p{N}]/u.test(word)).length
13}
14
15/** The answer as the summarizer reads it: long ones keep their head and their end. */
16export function promptFor(answer: string): string {
17 const body = answer.length > 16_000 ? `${answer.slice(0, 12_000)}\n[…]\n${answer.slice(-4_000)}` : answer
18 return `Write the TL;DR of this answer.\n\n<answer>\n${body}\n</answer>`
19}
20
21/** One clean line from whatever the model wrote, or '' when nothing usable came back. */
22export function clean(reply: string): string {
23 const line = reply
24 .replace(/^\s*(\*\*)?\s*(tl;?dr|summary)\s*[:\-–—]\s*(\*\*)?/i, '')
25 .replace(/[*_`#>]/g, '')
26 .split('\n')
27 .map(part => part.trim())
28 .find(part => part !== '') ?? ''
29 const flat = line.replace(/\s+/g, ' ').replace(/^["“']|["”']$/g, '').trim()
30 return flat.length > 280 ? `${flat.slice(0, 279).trimEnd()}…` : flat
31}
32