SLOPSHOPPER

quiet-tools

Draws each tool call as one summary line and hides its output unless it fails. Ctrl+O shows everything.

newrowsprompt
v0.1.0no licenseupdated 2026-10-22caleb-marks/caleb-mods/quiet-tools
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · quiet-tools
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ● Change the file auth.ts ⏺ Bash(bun test) ⎿ 3 pass, 1 fail Done. I made refresh reject expired claims and added an audit call. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Claude's reply
Done. I made refresh reject expired claims and added an audit call.
Tool row
● Change the file auth.ts
README

caleb-mods

Three Claude Code mods.

prompt-optimizer

  • Optimize prompt button above the prompt box (or /optimize <draft>): Opus 5.5 (medium effort) rewrites your draft as one measurable Goal: paragraph. Press it again to restore your draft.
  • Next-prompt suggestion: after each answer, Opus 5.5 (medium effort) writes the prompt you most likely send next (15 words or fewer). It shows as a button above the prompt box. In the desktop app, one click sends it. In the terminal, one click puts it in the box to edit.

adhd-reminder

While your output style is named ADHD, adds the rules from ~/.claude/output-styles/adhd.md beside each prompt, so long sessions keep the style. It follows the style the session actually uses, including one picked in the app. It does nothing without that file.

quiet-tools

Draws each tool call in the terminal as one plain-English line of 20 words or fewer, for example "Check if the job scanner is still running". It hides tool output unless a call fails or is stopped. Press ctrl+o to see the full commands and output. It also asks Claude to write each command description for a beginner. Tool lines are gray so replies stand out, a fully bold first line is drawn orange, and replies wrap at 94 columns on wide terminals.

Install

At the prompt of a terminal session:

/plugin install prompt-optimizer --marketplace caleb-marks/caleb-mods
/plugin install adhd-reminder --marketplace caleb-marks/caleb-mods
/plugin install quiet-tools --marketplace caleb-marks/caleb-mods

From a shell:

claude plugin install prompt-optimizer --marketplace caleb-marks/caleb-mods
claude plugin install adhd-reminder --marketplace caleb-marks/caleb-mods
claude plugin install quiet-tools --marketplace caleb-marks/caleb-mods

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with caleb-marks/caleb-mods, then Add plugin.

Source 1 files
hooks/register.tsx 165 lines
1import type { Register } from 'claude-code'
2
3// Caleb's palette: blue while running, muted gray when done (no pure white).
4// Tool lines are gray so Claude's replies stand out from the steps.
5const RUNNING = '#5FA8FF'
6const DONE = '#6E6A66'
7const TEXT = '#8A8580'
8const MAX = 100
9// Replies wrap at this width so lines stay short enough to scan on a wide terminal.
10const REPLY_COLUMNS = 94
11// A reply's opening line when it is bold as a whole, drawn orange.
12const HEADLINE = /^\s*\*\*([^*\n]*[^*\s][^*\n]*)\*\*[ \t]*(?:\r?\n|$)/
13const REPLY_FG = '#E8E4DE'
14const HEADLINE_COLOR = '#FF9E3D'
15
16type Input = Record<string, unknown>
17
18const str = (v: unknown): string => (typeof v === 'string' ? v : '')
19const base = (p: unknown): string => str(p).split(/[\\/]/).pop() ?? ''
20const firstLine = (s: string): string =>
21  s.split(/\r?\n/).map(l => l.trim()).find(l => l !== '') ?? ''
22const cut = (s: string): string => {
23  const chars = Array.from(s.replace(/[\x00-\x1f\x7f]/g, ' '))
24  return chars.length > MAX ? `${chars.slice(0, MAX - 1).join('')}…` : chars.join('')
25}
26
27// The tool row shows the model's `description`, so ask for one a beginner can read.
28const DESCRIBE_RULE = {
29  id: 'quiet-tools:describe',
30  scope: 'session',
31  text: [
32    '# Tool call descriptions',
33    'The person sees only the `description` you give each Bash, PowerShell or Agent call, as one line. The person is a beginner.',
34    '- Always give a `description`.',
35    '- Write one plain-English sentence of 20 words or fewer that says what the step does and why.',
36    '- Use everyday words. Do not use code, flags, file paths, cmdlet names or jargon. Use only a file name when it helps.',
37    '- Good: "Check if the job scanner is still running". Bad: "Get-Process job-scan | Select-Object Name, Id".',
38  ].join('\n'),
39} as const
40
41// Tools whose `description` input says what the call does. Other tools (many MCP
42// ones) use `description` for content, so they get a summary built from the input.
43const DESCRIBED = new Set(['Bash', 'PowerShell', 'Agent', 'Task'])
44
45// One short sentence for a call: the model's own description when it wrote one,
46// else a summary built from the input.
47const summarize = (tool: string, raw: unknown): string => {
48  const i = (raw ?? {}) as Input
49  const described = DESCRIBED.has(tool) ? firstLine(str(i.description)) : ''
50  if (described) return described
51
52  switch (tool) {
53    case 'Bash':
54    case 'PowerShell':
55      return 'Run a command'
56    case 'Agent':
57    case 'Task':
58      return 'Ask a helper agent to do part of the work'
59    case 'Read':
60      return `Read the file ${base(i.file_path)}`
61    case 'Edit':
62      return `Change the file ${base(i.file_path)}`
63    case 'Write':
64      return `Save the file ${base(i.file_path)}`
65    case 'NotebookEdit':
66      return `Change the notebook ${base(i.notebook_path)}`
67    case 'Grep': {
68      const pattern = str(i.pattern)
69      return /^[\w .-]{1,30}$/.test(pattern) ? `Search the files for "${pattern}"` : 'Search the files for some text'
70    }
71    case 'Glob':
72      return 'Look for files by name'
73    case 'WebFetch': {
74      const host = /^https?:\/\/([^/]+)/.exec(str(i.url))?.[1]
75      return host ? `Open a web page on ${host}` : 'Open a web page'
76    }
77    case 'WebSearch':
78      return `Search the web for "${str(i.query)}"`
79    case 'Skill':
80      return `Follow the ${str(i.skill)} instructions`
81    case 'ToolSearch':
82      return 'Get more tools ready'
83    case 'AskUserQuestion':
84      return 'Ask you a question'
85    case 'Artifact':
86      return 'Work on a web page'
87    default: {
88      const name = tool.startsWith('mcp__') ? tool.split('__').slice(1).join(' ') : tool
89      return name.replace(/_/g, ' ')
90    }
91  }
92}
93
94export const register: Register = on => {
95  on('prompt.compose', async ($, e, next) => {
96    const { sections } = await next(e)
97    // Two loaded copies (dev folder and installed) must not add the rule twice.
98    return { sections: [...sections.filter(s => s.id !== DESCRIBE_RULE.id), DESCRIBE_RULE] }
99  })
100
101  on('ui.render', { component: 'ToolUse' }, ($, e, next) => {
102    const p = e.props
103    // Terminal only. Full view (ctrl+o, --verbose), failures and interrupts keep the engine's row.
104    if (e.surface !== 'terminal' || p.isExpanded || p.isErrored || p.isInterrupted) return next(e)
105
106    const { Box, Text } = $.ui.resolve(e)
107    return (
108      <Box>
109        <Text color={p.isRunning ? RUNNING : DONE}>● </Text>
110        <Text color={TEXT} wrap="truncate-end">{cut(summarize(p.tool, p.input))}</Text>
111      </Box>
112    )
113  }).catch(($, e, next) => next(e))
114
115  on('ui.render', { component: 'ToolResult' }, ($, e, next) => {
116    const p = e.props
117    const isInterrupted = (p.output as { interrupted?: unknown } | null)?.interrupted === true
118    if (e.surface !== 'terminal' || p.isExpanded || p.isErrored || isInterrupted) return next(e)
119    const { Box } = $.ui.resolve(e)
120    return <Box />
121  }).catch(($, e, next) => next(e))
122
123  // Replies are drawn here with Markdown, not the engine's node: the engine refuses its
124  // own node under a Box with a width, and the width is what keeps lines short.
125  on('ui.render', { component: 'AssistantMessage' }, ($, e, next) => {
126    if (e.surface !== 'terminal' || e.props.isSummary) return next(e)
127    const { Box, Text, Markdown } = $.ui.resolve(e)
128    const columns = e.viewport?.columns ?? 0
129    const width = columns > REPLY_COLUMNS ? REPLY_COLUMNS : undefined
130
131    // A reply that opens with a fully bold line: draw that line orange.
132    const match = HEADLINE.exec(e.props.text)
133    const headline = match ? (match[1] ?? '').replace(/`/g, '') : ''
134    const rest = match ? e.props.text.slice(match[0].length).replace(/^(?:[ \t]*\r?\n)+/, '') : e.props.text
135    const bullet = e.props.isFirstOfReply ? '● ' : '  '
136
137    return (
138      // A blank row above a headline sets the answer apart from the tool lines over it.
139      <Box flexDirection="column" width={width} marginTop={headline ? 1 : 0}>
140        {headline ? (
141          // No dot: the headline starts in the dot column, left of the tool lines' text.
142          <Text color={HEADLINE_COLOR} bold>{headline}</Text>
143        ) : null}
144        {headline && rest ? <Text> </Text> : null}
145        {rest ? (
146          <Box>
147            <Text color={REPLY_FG}>{headline ? '  ' : bullet}</Text>
148            <Box flexDirection="column" flexGrow={1}>
149              <Markdown text={rest} />
150            </Box>
151          </Box>
152        ) : null}
153      </Box>
154    )
155  }).catch(($, e, next) => {
156    $.ui.log(`quiet-tools: reply drawing failed: ${next.error?.message ?? 'unknown'}`, { to: 'debug' })
157    return next(e)
158  })
159
160  // Drop the "(ctrl+b to run in background)" line; the key still works.
161  on('ui.render', { component: 'ToolProgress' }, ($, e, next) =>
162    next({ ...e, props: { ...e.props, hint: '' } }),
163  )
164}
165