SLOPSHOPPER

tldr

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.

newcommandmodel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tldr
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /tldr ⎿ tldr: TL;DR: OK ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

📝 tldr

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.

Features

  • Automatic on main-thread answers of 200+ words. Subagent turns, interrupted turns and API errors are skipped.
  • /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.
  • Cheap and bounded: one low-effort call with a ~120-token cap and a 15-second timeout. Long answers are cut to their head and tail before sending.
  • Never in the way: if the call fails, times out or returns nothing, the answer shows as usual. Headless -p runs skip the automatic call, since they don't print the line.
  • Plays well with others: if another mod already put a line under the answer, the TL;DR goes beneath it.

Install

/plugin marketplace add Singh-AP/awesome-claude-mods
/plugin install tldr@awesome-claude-mods

Requires Claude Code 2.1.287 or later.

Configuration

OptionDefaultWhat it does
minWords200Answers with at least this many words of prose (20–5000) get a TL;DR
modelhaikuThe model that writes it: an alias or a full model id

How it works

Event or callWhy
turn.completeAfter next(e), returns { text: 'TL;DR: …' }, which Claude Code draws beneath the answer
$.model.completeOne tool-less completion through your session's own credentials
command.run on tldrOn-demand summary (from $.session.messages()), plus on/off/status
$.storeKeeps the on/off switch

Test it

claude plugin test mods/productivity/tldr   # 12 tests

Limitations

  • Each TL;DR is an extra small model call, billed like any other request.
  • The line shows in the interactive UI only; claude -p prints the answer alone.
Source 2 files
hooks/register.ts 81 lines
1import 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}
81
hooks/tldr.ts 32 lines
1// 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