SLOPSHOPPER

tps-meter

Shows the model's live output tokens per second in the prompt footer.

newspinner
v0.2.1no licenseupdated 2026-10-06xingkaixin/claude-mods/mods/tps-meter
A shopper browsing a rack in a slop shop
README

claude-mods

English · 简体中文

A collection of Claude Code mods. Each mod is a function-hook plugin that runs in the terminal and in the Code tab of Claude Desktop.

ModWhat it does
tps-meterShows the model's live output speed (tokens/s)

Install

In a terminal:

claude plugin marketplace add xingkaixin/claude-mods
claude plugin install tps-meter@claude-mods

Or with slash commands at the Claude Code prompt (Desktop does not support these; use the terminal commands above):

/plugin marketplace add xingkaixin/claude-mods
/plugin install tps-meter@claude-mods

Choose the user scope so both the terminal and Desktop load it. After installing, run /reload-plugins in any terminal session that is already open. Desktop loads it in a new session.

Check the version and update

See the installed version:

claude plugin list

The Version line under tps-meter@claude-mods is the installed version.

Check for and install a new version:

claude plugin marketplace update claude-mods   # fetch the latest marketplace from GitHub
claude plugin update tps-meter@claude-mods      # update if there is a newer version, otherwise report it is up to date

After updating, run /reload-plugins in the terminal, or start a new session in Desktop. Sessions that are already running keep the old version.

The latest version number is in mods/<mod>/.claude-plugin/plugin.json in this repository.

tps-meter

Shows the model's live output speed (tokens/s) while it responds:

SurfaceWhere
TerminalAt the end of the hint line under the prompt, e.g. after bypass permissions on (shift+tab to cycle)
DesktopIn the footer under the input box

The readout has two states:

  • ⚡ 42.3 tok/s: the model is still responding. Refreshed every 250 ms. This value is estimated from character counts.
  • · 38.1 tok/s: the response has finished. The exact value, computed from the output_tokens the API returns.

Before any output, Desktop shows – tok/s, which also tells you the mod is loaded.

How it is computed

Each model request (turn.step) is measured on its own. Timing starts at the first output chunk and ends at the stop chunk, so time to first token is excluded. Thinking, text and tool-call arguments all count as output.

  • Live value: characters received ÷ characters per token ÷ seconds elapsed. Characters per token starts at 4 (a typical value for English) and is recalibrated from the real output_tokens after each request. For CJK text the live value of the first request therefore reads low.
  • Final value: output_tokens ÷ seconds from the first chunk to the end.

Limits

  • Only the main thread is measured; subagent output is not counted.
  • A request whose first chunk and end are less than 200 ms apart does not update the readout. Over such a short span the timing mostly reflects network jitter, not generation speed.
  • An interrupted request carries no usage, so the readout keeps the last estimate.

Development

Layout

.claude-plugin/marketplace.json   # marketplace manifest, lists every mod
mods/<mod>/
  .claude-plugin/plugin.json      # mod manifest: name, version, description
  hooks/hooks.json                # points to the hooks module
  hooks/register.tsx              # hooks module, exports register(on)
  types/index.d.ts                # $.state type declarations (needed when the mod uses $.state)
  tests/*.test.tsx                # tests run by claude plugin test

mods/<mod>/.claude-plugin/types/ holds type files Claude Code generates each time it loads a mod; it is ignored in .gitignore.

Add a mod

  1. Create a folder under mods/ with the files listed above.
  2. Add an entry to plugins in .claude-plugin/marketplace.json, with source set to ./mods/<mod>.
  3. Add a row to the mod table in both READMEs and describe the mod.

The authoritative API reference is the type file Claude Code generates, .claude-plugin/types/claude-code/index.d.ts. Loading the plugin-authoring skill in Claude Code gives the full authoring guide.

Debug locally

claude --plugin-dir mods/<mod>           # load from the local folder for this session only
claude plugin validate .                 # check the marketplace and every mod manifest
claude plugin validate mods/<mod>        # check one mod's manifest and hooks module
claude plugin test mods/<mod>            # run its tests

A terminal session started with --plugin-dir watches the folder and reloads the mod when a file is saved.

Release a new version

  1. Change version in mods/<mod>/.claude-plugin/plugin.json. The version must change: an installed plugin is copied into a local cache by version, so with the same version claude plugin update does not fetch the new code.
  2. Commit and push to main.
  3. Optional: run claude plugin tag mods/<mod> --push to create and push a <mod>--v<version> git tag.
  4. Update on your machine as described in Check the version and update.
Source 2 files
hooks/register.tsx 90 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Tps } from '../types'
5
6const tps = atom({ plugin: 'tps-meter', key: 'tps' } as const, null)
7
8const PUBLISH_INTERVAL_MS = 250
9// Below this the elapsed time is mostly chunk-arrival jitter, not generation speed.
10const MIN_ELAPSED_MS = 200
11
12export const register: Register = on => {
13  // Seeded with the usual English ratio; recalibrated from each step's real usage.
14  let charsPerToken = 4
15
16  on('turn.step', async function* ($, e, next) {
17    if (e.agentId !== undefined) {
18      return yield* next(e)
19    }
20
21    let firstAt: number | undefined
22    let lastPublishAt = 0
23    let chars = 0
24
25    const publish = (value: Tps) => update($, tps, () => value)
26
27    for await (const chunk of next(e)) {
28      if (chunk.kind === 'text' || chunk.kind === 'thinking' || chunk.kind === 'input') {
29        const now = await $.clock.now()
30        firstAt ??= now
31        chars += chunk.kind === 'input' ? chunk.json.length : chunk.text.length
32
33        const elapsed = now - firstAt
34        if (elapsed >= MIN_ELAPSED_MS && now - lastPublishAt >= PUBLISH_INTERVAL_MS) {
35          lastPublishAt = now
36          await publish({ value: chars / charsPerToken / (elapsed / 1000), isLive: true })
37        }
38      }
39
40      if (chunk.kind === 'stop' && firstAt !== undefined) {
41        const elapsed = (await $.clock.now()) - firstAt
42        const tokens = chunk.usage?.output_tokens ?? 0
43
44        if (tokens > 0 && chars > 0) {
45          charsPerToken = chars / tokens
46        }
47
48        if (tokens > 0 && elapsed >= MIN_ELAPSED_MS) {
49          await publish({ value: tokens / (elapsed / 1000), isLive: false })
50        } else {
51          const last = await read($, tps)
52          if (last?.isLive) await publish({ ...last, isLive: false })
53        }
54      }
55
56      yield chunk
57    }
58  })
59
60  const label = (current: Tps) => `${current.isLive ? '⚡' : '·'} ${current.value.toFixed(1)} tok/s`
61
62  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
63    const current = await read($, tps)
64    if (current === null || e.surface !== 'terminal') {
65      return next(e)
66    }
67
68    return next({ ...e, props: { ...e.props, tail: `  ${label(current)}` } })
69  })
70
71  // Only the terminal draws PromptHint's tail. The desktop draws a SessionMode tree in its footer,
72  // but not a rewritten `modes`, so the readout is drawn as our own tree around the engine's.
73  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
74    if (e.surface === 'terminal') {
75      return next(e)
76    }
77
78    const current = await read($, tps)
79    const { Box, Text } = $.ui.resolve(e)
80    const below = await next(e)
81
82    return (
83      <Box flexDirection="row" alignItems="center" gap={1}>
84        <Text dimColor>{current === null ? '– tok/s' : label(current)}</Text>
85        {below}
86      </Box>
87    )
88  })
89}
90
types/index.d.ts 8 lines
1export type Tps = { value: number; isLive: boolean }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'tps-meter': { tps: Tps | null }
6  }
7}
8