SLOPSHOPPER

tool-tape

Shows each tool call of the current turn as a colored cell above the Claude Code prompt

newbandspinnerguardprompttimer
v0.1.0MITupdated 2026-10-06DonMecha/tool-tape
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tool-tape
› 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 ✻ Thinking… call 9 · Bash… Last 9 calls · 0s read 2 run 4 edit 3 failed 1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Last 9 calls · 0s read 2 run 4 edit 3 failed 1
README

tool-tape

日本語 | English

Claude Codeが1回の依頼の中で行ったツール呼び出しを、入力欄の上に色のマスで並べるmodです。何を何回行ったか、どこで失敗したか、どれくらい時間がかかっているかを、作業中にひと目で確認できます。

作業中の表示

表示の読み方

進行 ▮▮▮▮▮▮ 6回 · 9秒  読3  実2  編1  失1
  • 進行 / 前回: 作業中は「進行」、依頼が終わると「前回」に変わり、最後の結果を残します。
  • 色のマス: ツール呼び出し1回につき1マス、古い順に左から並びます。40マスを超えた分は左端に +12 のように数だけ出します。
  • 6回 · 9秒: 呼び出しの合計と、依頼を送ってからの経過時間です。作業中は毎秒更新します。
  • 読3 実2 …: 種類ごとの回数です。文字の色はマスの色と同じです。失敗はマスを赤にし、「失」で別に数えます。
色種類対象のツール
水色読(read)Read, Grep, Glob, LS, NotebookRead
黄実(run)Bash, BashOutput, KillShell, Monitor
緑編(edit)Edit, Write, MultiEdit, NotebookEdit
青網(web)WebFetch, WebSearch, Playwright MCP
紫委(agent)Agent, Task, SendMessage
灰他(other)上記以外(MCPツールなど)
赤失(failed)拒否された、またはエラーを返した呼び出し

作業中のスピナーの後ろにも、何回目の呼び出しかと直近のツール名を … 6回目 · Write のように足します。

依頼が終わった後の表示

スクリーンショットは英語表示(LANG=en_US.UTF-8)で撮影しています。

動作要件

  • Claude Code v2.1.287以降(modが標準で有効な版)。v2.1.291のLinuxターミナルで動作を確認しています。
  • 表示されるのはターミナルとClaude DesktopのCodeタブです。VS Code拡張のチャット欄と claude -p では何も描画されません。呼び出しを数える処理だけが動きます。

インストール

Claude Codeのセッションで次を実行します。

/plugin install tool-tape --marketplace DonMecha/tool-tape

マーケットプレイスの追加を確認されたら y、スコープは既定のユーザースコープを選びます。

インストールせずに試すときは、このリポジトリをcloneして次のように起動します。

claude --plugin-dir ./tool-tape

設定

表示言語は既定で auto です。LC_ALL・LC_MESSAGES・LANG の順に見て、ja で始まれば日本語、それ以外は英語で表示します。固定する場合は /config の設定欄で選ぶか、ユーザー設定ファイル(settings.json)に次のように書きます。

{
  "pluginConfigs": {
    "tool-tape@tool-tape": { "options": { "language": "ja" } }
  }
}

止めるときは claude plugin disable tool-tape@tool-tape を実行します。

制約

  • Claude Codeのmod APIは早期提供版で、予告なく変わることがあります。Claude Codeの更新で表示が崩れたり、動かなくなったりする可能性があります。
  • 数えるのはこのmodを読み込んだ後の呼び出しだけです。履歴をさかのぼって集計はしません。セッションをまたいだ記録も残しません。
  • 隣り合う同じ色のマスは、つながって1本の帯に見えます。
  • このmodはファイル・ネットワーク・プロセスに触れません。読む環境変数は言語判定用の LC_ALL・LC_MESSAGES・LANG だけです。

開発

claude plugin validate .
claude plugin test .

ライセンス

MIT

Source 1 files
hooks/register.tsx 154 lines
1import type { Register } from 'claude-code'
2
3// Lays out each tool call of the current turn as one colored cell above the prompt,
4// with the call count, elapsed time and a per-kind breakdown, and adds the call
5// count and the latest tool to the spinner.
6
7type Kind = 'read' | 'run' | 'edit' | 'web' | 'agent' | 'other'
8type Pip = { kind: Kind; failed: boolean }
9type Lang = 'en' | 'ja'
10
11const COLORS: Record<Kind, string> = {
12  read: '#5fb3d9',
13  run: '#e5c07b',
14  edit: '#98c379',
15  web: '#61afef',
16  agent: '#c678dd',
17  other: '#7f848e',
18}
19const FAILED = '#e06c75'
20const TAPE_MAX = 40
21
22const TEXT = {
23  en: {
24    kinds: { read: 'read ', run: 'run ', edit: 'edit ', web: 'web ', agent: 'agent ', other: 'other ' },
25    failed: 'failed ',
26    running: 'Running ',
27    last: 'Last ',
28    calls: (n: number) => `${n} ${n === 1 ? 'call' : 'calls'}`,
29    nth: (n: number) => `call ${n}`,
30    time: (m: number, s: number) => (m === 0 ? `${s}s` : `${m}m ${s}s`),
31  },
32  ja: {
33    kinds: { read: '読', run: '実', edit: '編', web: '網', agent: '委', other: '他' },
34    failed: '失',
35    running: '進行 ',
36    last: '前回 ',
37    calls: (n: number) => `${n}回`,
38    nth: (n: number) => `${n}回目`,
39    time: (m: number, s: number) => (m === 0 ? `${s}秒` : `${m}分${s}秒`),
40  },
41} as const
42
43const kindOf = (tool: string): Kind => {
44  if (['Read', 'Grep', 'Glob', 'LS', 'NotebookRead'].includes(tool)) return 'read'
45  if (['Bash', 'BashOutput', 'KillShell', 'Monitor'].includes(tool)) return 'run'
46  if (['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(tool)) return 'edit'
47  if (['WebFetch', 'WebSearch'].includes(tool) || tool.startsWith('mcp__playwright__')) return 'web'
48  if (['Agent', 'Task', 'SendMessage'].includes(tool)) return 'agent'
49  return 'other'
50}
51
52const shortName = (tool: string) => (tool.startsWith('mcp__') ? tool.split('__').pop() ?? tool : tool)
53
54const pickLang = (setting: unknown, locale: string | undefined): Lang => {
55  if (setting === 'en' || setting === 'ja') return setting
56  return locale?.toLowerCase().startsWith('ja') ? 'ja' : 'en'
57}
58
59export const register: Register = (on, options) => {
60  let lang: Lang = pickLang(options.language, undefined)
61  let pips: Pip[] = []
62  let lastTool = ''
63  let started = false
64  let startedAt = 0
65  let endedAt = 0
66  let working = false
67  let tick: { cancel: () => void } | undefined
68
69  on('session.start', async ($, e, next) => {
70    if (options.language !== 'en' && options.language !== 'ja') {
71      const locale = (await $.env.get('LC_ALL')) || (await $.env.get('LC_MESSAGES')) || (await $.env.get('LANG'))
72      lang = pickLang(options.language, locale)
73    }
74    return next(e)
75  })
76
77  on('prompt.submit', async ($, e, next) => {
78    pips = []
79    lastTool = ''
80    startedAt = await $.clock.now()
81    started = true
82    endedAt = 0
83    working = true
84    tick?.cancel()
85    tick = $.clock.every(1000, () => $.ui.invalidate('ui.render'))
86    $.ui.invalidate('ui.render')
87    return next(e)
88  }).catch(($, e, next) => next(e))
89
90  on('tool.call', async ($, e, next) => {
91    const pip: Pip = { kind: kindOf(e.tool), failed: false }
92    pips.push(pip)
93    lastTool = shortName(e.tool)
94    $.ui.invalidate('ui.render')
95    const ran = await next(e)
96    if (ran.deny !== undefined || ran.isError === true) {
97      pip.failed = true
98      $.ui.invalidate('ui.render')
99    }
100    return ran
101  }).catch(($, e, next) => next(e))
102
103  on('turn.complete', async ($, e, next) => {
104    endedAt = await $.clock.now()
105    working = false
106    tick?.cancel()
107    tick = undefined
108    $.ui.invalidate('ui.render')
109    return next(e)
110  })
111
112  on('ui.render', { component: 'Spinner' }, ($, e, next) =>
113    pips.length === 0
114      ? next(e)
115      : next({ ...e, props: { ...e.props, suffix: `… ${TEXT[lang].nth(pips.length)} · ${lastTool}` } }),
116  )
117
118  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
119    if (e.props.hasSurvey || !started) return next(e)
120
121    const t = TEXT[lang]
122    const { Box, Text } = $.ui.resolve(e)
123    const sec = Math.round(((working ? await $.clock.now() : endedAt) - startedAt) / 1000)
124    const shown = pips.slice(-TAPE_MAX)
125    const hidden = pips.length - shown.length
126    const failed = pips.filter(p => p.failed).length
127    const counts = (Object.keys(COLORS) as Kind[])
128      .map(kind => ({ kind, n: pips.filter(p => p.kind === kind).length }))
129      .filter(c => c.n > 0)
130
131    return (
132      <Box flexDirection="row">
133        <Text dimColor>{working ? t.running : t.last}</Text>
134        {hidden > 0 ? <Text dimColor>+{hidden} </Text> : null}
135        {shown.map(p => (
136          <Text backgroundColor={p.failed ? FAILED : COLORS[p.kind]}> </Text>
137        ))}
138        {shown.length > 0 ? <Text> </Text> : null}
139        <Text dimColor>
140          {t.calls(pips.length)} · {t.time(Math.floor(sec / 60), sec % 60)}
141        </Text>
142        {counts.map(c => (
143          <Text color={COLORS[c.kind]}>
144            {'  '}
145            {t.kinds[c.kind]}
146            {c.n}
147          </Text>
148        ))}
149        {failed > 0 ? <Text color={FAILED}>{'  '}{t.failed}{failed}</Text> : null}
150      </Box>
151    )
152  })
153}
154