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

A pane showing which tools Claude calls most, and which of them fail.
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.| Command | Effect |
|---|---|
/tool-heatmap | Open the pane and print the totals. |
claude --plugin-dir /path/to/ModsTools/mods/tool-heatmap
claude plugin validate mods/tool-heatmap
claude plugin test mods/tool-heatmap # 8 testshooks/register.tsx 54 lines1import { 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}
54hooks/heat.ts 41 lines1import 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}✗)` : ''}`
41types/index.d.ts 8 lines1export type HeatmapTool = { tool: string; calls: number; failures: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'tool-heatmap': { tools: HeatmapTool[] }
6 }
7}
8