SLOPSHOPPER

tool-heatmap

A pane counting tool calls and failures per tool, main conversation and subagents together

newpaneguardcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tool-heatmap
│ ┃ Tool heatmap ✕ › fix the failing auth test and add an audit log call │ ┃ 9 calls · 1 failed · 5 tools │ ┃ Bash ⏺ Read(src/auth.ts) │ ┃ ███████████████████████████████████████████ ⎿ Read 6 lines │ ┃ 4 (1✗) ⏺ Update(src/auth.ts) │ ┃ Write ██████████████████████ ⎿ Added 2 lines, removed 1 line │ ┃ 2 ⏺ Bash(bun test) │ ┃ Edit ███████████ ⎿ 3 pass, 1 fail │ ┃ 1 │ ┃ Grep ███████████ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ 1 │ ┃ Read ███████████ ✻ Worked for 42s · done 4:20 PM │ ┃ 1 │ › /tool-heatmap │ ⎿ tool-heatmap: Tool heatmap pane opened. 9 calls · 1 failed · 5 t │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Tool heatmap
9 calls · 1 failed · 5 tools Bash ███████████████████████████████████████████ 4 (1✗) Write ██████████████████████ 2 Edit ███████████ 1 Grep ███████████ 1 Read ███████████ 1
README

tool-heatmap

A pane showing which tools Claude calls most, and which of them fail.

What it does

  • Counts every tool call, main conversation and subagents together.
  • A call counts as a failure when it is denied or returns an error.
  • The pane (opened at session start) shows a summary line (N calls · N failed · N tools), then one row per tool sorted by call count: name, a █ bar scaled to the top tool, the count, and (N✗) when there are failures.

Commands

CommandEffect
/tool-heatmapOpen the pane and print the totals.

Install

claude --plugin-dir /path/to/ModsTools/mods/tool-heatmap

Limits

  • Counts are per session and not reset by a command; there is no close or clear subcommand.
  • Tool names are cut to 20 characters; rows beyond the pane height are not shown.
  • A call counts when it completes (or is denied): calls still running are not included.

Develop

claude plugin validate mods/tool-heatmap
claude plugin test mods/tool-heatmap   # 8 tests
Source 3 files
hooks/register.tsx 54 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { HeatmapTool } from '../types'
5import { count, rows, total } from './heat'
6
7const PANE = 'tool-heatmap'
8const TITLE = 'Tool heatmap'
9const tools = atom({ plugin: 'tool-heatmap', key: 'tools' } as const, [])
10
11export const register: Register = on => {
12  on('session.start', async ($, e, next) => {
13    await $.command.register({
14      name: 'tool-heatmap',
15      description: 'Show tool calls and failures per tool in a pane',
16    })
17    void $.ui.open({ id: PANE, title: TITLE })
18
19    return next(e)
20  })
21
22  on('command.run', { command: 'tool-heatmap' }, async $ => {
23    await $.ui.open({ id: PANE, title: TITLE })
24
25    return { text: `Tool heatmap pane opened. ${total(await read($, tools))}.` }
26  })
27
28  on('tool.call', async ($, e, next) => {
29    const ran = await next(e)
30    const isFailure = ran.deny !== undefined || ran.isError === true
31    await update($, tools, list => count(list, String(e.tool), isFailure))
32
33    return ran
34  })
35
36  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
37    const { Box, Text } = $.ui.resolve(e)
38    const list: HeatmapTool[] = await read($, tools)
39    const room = Math.max(1, (e.viewport?.rows ?? 24) - 4)
40
41    return (
42      <Box flexDirection="column">
43        <Text bold>{total(list)}</Text>
44        {list.length === 0 && <Text dimColor>No tool calls yet.</Text>}
45        {rows(list, e.props.bodyColumns)
46          .slice(0, room)
47          .map(line => (
48            <Text>{line}</Text>
49          ))}
50      </Box>
51    )
52  })
53}
54
hooks/heat.ts 41 lines
1import type { HeatmapTool } from '../types'
2
3const NAME_MAX = 20
4
5export function count(list: HeatmapTool[], tool: string, isFailure: boolean): HeatmapTool[] {
6  const found = list.find(one => one.tool === tool) ?? { tool, calls: 0, failures: 0 }
7  const counted = { tool, calls: found.calls + 1, failures: found.failures + (isFailure ? 1 : 0) }
8
9  return [...list.filter(one => one.tool !== tool), counted]
10}
11
12export const sorted = (list: HeatmapTool[]) =>
13  [...list].sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool))
14
15export function total(list: HeatmapTool[]): string {
16  const calls = list.reduce((sum, one) => sum + one.calls, 0)
17  const failures = list.reduce((sum, one) => sum + one.failures, 0)
18
19  return `${calls} call${calls === 1 ? '' : 's'} · ${failures} failed · ${list.length} tool${list.length === 1 ? '' : 's'}`
20}
21
22const fit = (name: string, width: number) =>
23  name.length > width ? name.slice(0, width - 1) + '…' : name.padEnd(width)
24
25/** One line per tool, most called first: name, a bar scaled to the top tool, count and failures. */
26export function rows(list: HeatmapTool[], columns: number): string[] {
27  const ordered = sorted(list)
28  const top = ordered[0]?.calls ?? 0
29  const nameWidth = Math.min(NAME_MAX, Math.max(0, ...ordered.map(one => one.tool.length)))
30  const tailWidth = Math.max(0, ...ordered.map(one => tail(one).length))
31  const barWidth = Math.max(1, columns - nameWidth - tailWidth - 2)
32
33  return ordered.map(one => {
34    const bar = '█'.repeat(Math.max(1, Math.round((one.calls / top) * barWidth)))
35
36    return `${fit(one.tool, nameWidth)} ${bar.padEnd(barWidth)} ${tail(one)}`.trimEnd()
37  })
38}
39
40const tail = (one: HeatmapTool) => `${one.calls}${one.failures > 0 ? ` (${one.failures}✗)` : ''}`
41
types/index.d.ts 8 lines
1export type HeatmapTool = { tool: string; calls: number; failures: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'tool-heatmap': { tools: HeatmapTool[] }
6  }
7}
8