SLOPSHOPPER

usage-gauge

额度看板:输入框上方一行看到对话上下文、5 小时(会话)额度、本周额度各用了多少、什么时候重置,加本次花费。读引擎自己的数字,零 Token

newcommandtimer
v0.2.0no licenseupdated 2026-10-23weizhongl/claude-code-mods/usage-gauge
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-gauge
› 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 › /gauge ⎿ usage-gauge: 对话上下文:49%(97k / 200k) ⎿ usage-gauge: 5 小时额度:已用 31% ↻ 09:53 ⎿ usage-gauge: 本次对话花费:$0.42 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Claude Code Mods

十个给 Claude Code 用的 Mod。只能装在 Claude Code(终端和桌面 App 的 Code 页)。Codex、Anti-Gravity 没有 Mod 接口,装不了。

先装 switchboard(总控台),其余随意挑。

安装

终端里:

/plugin install switchboard --marketplace weizhongl/claude-code-mods

第一次会问 Add marketplace?,回 y,再选 user scope。其余 Mod 把名字换掉即可:usage-gauge、brain-dump、helper-desk、cache-clock、handoff、reflect、chat-inventory、screen-share、replay-theater。

在 shell 里:

claude plugin install switchboard --marketplace weizhongl/claude-code-mods

桌面 App(/plugin 在那里不能用):点输入框旁的 +,选 Plugins → Add marketplace,填 weizhongl/claude-code-mods,再逐个 Add plugin。

每个 Mod 怎么用

Mod怎么用注意
switchboard输入 /mods 打开总控台。每个 Mod 一行:On/Off、Hide UI Panel。输入框上方那一行条(额度、缓存倒计时、任务进度、👁 按钮)也是它画的:平时只占一行,别的 Mod 有事才在它旁边多出一行。条的右端有按钮:「收起」变成一小行(只剩几个百分比和倒计时),再点「隐藏」只留一个「📊 展开」按钮;也可以输入 /bar(或 /bar full、/bar mini、/bar off)切换,你的选择会记住。进度条在桌面版是细线,在终端里是小方块。也可以 /mods off 名字、/mods on 名字、/mods hide 名字、/mods show 名字。新放进同一个文件夹的 Mod 会自动出现。Off = 该 Mod 的钩子立刻停手;Hide = 后台照跑,只收起面板。
usage-gauge自动。显示在总控台那一行条里:对话上下文用了多少、5 小时(会话)额度、本周额度,各有短进度条、颜色(绿 / 黄 / 红)和重置时间,最后是本次对话花费。/gauge 看文字版。读的是引擎自己的数字(和状态栏同一份),零 Token。额度数字要等发过一条消息、引擎读到后才出现。
brain-dump直接打字或语音说一大段(默认 180 字以上、没有排版)。它会拦住,先问你 2–3 个问题;你在输入框直接回答(回「跳过」= 不补充),然后重写成 The Task / The Why / Guardrails / What Done Looks Like 再发给 Claude。输入框上方有 跳过提问 / 原话直接发 / 放弃 三个按钮,也可用 /dump skip、/dump raw、/dump cancel。消息开头加 >> 可以跳过拦截。会用到两次小的模型调用(提问用 haiku,重写用 sonnet)。
helper-desk自动。Claude 有待办清单时,一行条里显示「任务 n/m」和短进度条;Claude 派出子代理时,侧边栏出现 Helper Desk,每个子代理一张卡(进度、正在做什么、读入/输出 token、花费)。/helpers 手动打开。子代理花费是按公开标价估算;整段对话总花费取自引擎账本。零额外 Token。
cache-clock自动。第一条回复之后,一行条里显示「⏱ mm:ss」。点旁边的 Keep Warm 或输入 /keep warm,快冷之前会自动发一条极小的心跳续命。你发下一条真实消息,或连续保温满 2 小时,会自动关闭。拿不到服务器的缓存时间,所以倒计时是推算(订阅登入 60 分钟,API 5 分钟,并按真实缓存命中自动校正)。设置里的「缓存有效分钟数」可固定。
handoff对话累计到 250,000 token、或缓存变冷时,输入框上方提醒你。按「生成交接」或输入 /handoff,会写出一份详细的交接提示词(存在 ~/.claude/handoffs/,可直接发给同事),再按 Start Fresh Chat with Handoff 带去新对话。生成会花 Token,默认只提醒、不自动生成(设置里「触发后做什么」选 generate 才会自动生成)。桌面 App 不允许自动开新对话时,会改成复制到剪贴板。
reflect你在对话里说「以后永远不要…」「每次都要…」「记住…」这类话,输入框上方弹出小卡片,一键存进:内置 Memory / 项目 CLAUDE.md(没有就 AGENTS.md)/ 你指定的文件 / 仅本次忽略。只在句子里有规则关键词时才调用一次小模型判断。设置里可填默认的第二大脑文件路径。
chat-inventory自动。侧边栏「In this chat」:目标与待办、调用过的连接器(含次数)、用过的技能、读过和新建/修改过的文件(点文件在文件管理器里定位)。/inventory 手动打开。纯追踪,零 Token。
screen-share输入 /screen share 开或关,或点一行条右端的 👁 按钮。开启后,画面上的邮箱、电话、金额、API key、公司名、人名遮成灰条。只改画面,不改真实数据;不遮输入框里正在打的字。人名只遮设置里填的名单和「客户:xxx」「陈小姐」这类写法,没有真正的人名识别。
replay-theater自动录下 Claude 用 Edit / Write / NotebookEdit 对文件做的每次修改。输入 /replay 打开:选文件、播放/暂停、上一步/下一步、点步骤号跳转,看每一步的 diff。Revert 按钮按一次变成「再按一次确认」,第二次才会写回文件。/replay clear 清空。用 Bash 改文件(如 sed)录不到;单个文件超过 20 万字不录。记录只在当前会话。

目录

每个 Mod 一个文件夹:.claude-plugin/plugin.json、hooks/hooks.json、hooks/register.tsx,有状态的还有 types/index.d.ts。switchboard、screen-share、replay-theater 带自动测试(claude plugin test <文件夹>)。

各 Mod 里的 hooks/flag.ts 是同一小段代码(读总控台的开关),Mod 之间不能共享文件,所以每个放一份。

Source 3 files
hooks/register.tsx 61 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { kb, nameOf, resetLabel } from './format'
5
6const REFRESH_MS = 20_000
7
8const snap = atom({ plugin: 'usage-gauge', key: 'snap' } as const, null)
9
10// The engine's own figures (the ones its status line shows). Free: no model call.
11async function refresh($: EngineInterface) {
12  const usage = await $.session.usage()
13  const at = await $.clock.now()
14  await update($, snap, () => ({
15    contextTokens: usage.context.tokens ?? null,
16    contextWindow: usage.context.window,
17    contextPercent: usage.context.percent ?? null,
18    limits: usage.rateLimits.map(one => ({
19      kind: one.kind,
20      percentUsed: one.percentUsed,
21      resetsAt: one.resetsAt,
22    })),
23    usd: usage.cost?.usd ?? null,
24    at,
25  }))
26}
27
28export const register: Register = on => {
29  on('session.start', async ($, e, next) => {
30    await $.command.register({
31      name: 'gauge',
32      description: '额度看板:对话上下文、5 小时额度、本周额度、花费',
33    })
34    void refresh($).catch(() => undefined)
35    $.clock.every(REFRESH_MS, () => void refresh($).catch(() => undefined))
36
37    return next(e)
38  })
39
40  on('turn.complete', async ($, e, next) => {
41    if (e.agentId === undefined) void refresh($).catch(() => undefined)
42
43    return next(e)
44  }).catch(($, e, next) => next(e))
45
46  on('command.run', { command: 'gauge' }, async $ => {
47    await refresh($)
48    const s = await read($, snap)
49    if (s === null) return { text: '还没读到额度。' }
50    const now = await $.clock.now()
51    const lines = [
52      `对话上下文:${s.contextPercent ?? '?'}%(${s.contextTokens === null ? '?' : kb(s.contextTokens)} / ${kb(s.contextWindow)})`,
53      ...s.limits.map(one => `${nameOf(one.kind)}额度:已用 ${one.percentUsed}% ${resetLabel(one.resetsAt, now)}`),
54      s.limits.length === 0 ? '额度:引擎还没读到(发一条消息后出现)' : '',
55      s.usd === null ? '' : `本次对话花费:$${s.usd.toFixed(2)}`,
56    ]
57
58    return { text: lines.filter(line => line !== '').join('\n') }
59  })
60}
61
hooks/format.ts 37 lines
1// Pure: how a number becomes a bar, a color and a reset time.
2
3const WIDTH = 8
4const DAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
5
6export const bar = (percent: number) => {
7  const on = Math.max(0, Math.min(WIDTH, Math.round((percent / 100) * WIDTH)))
8
9  return { on: '▓'.repeat(on), off: '░'.repeat(WIDTH - on) }
10}
11
12export const colorOf = (percent: number) =>
13  percent >= 85 ? 'red' : percent >= 60 ? 'yellow' : 'green'
14
15export const kb = (tokens: number) =>
16  tokens >= 1_000_000 ? `${(tokens / 1_000_000).toFixed(1)}M` : `${Math.round(tokens / 1000)}k`
17
18const two = (n: number) => String(n).padStart(2, '0')
19
20// "15:00" when the reset is today, "周六 17:00" otherwise.
21export const resetLabel = (iso: string | undefined, nowMs: number): string => {
22  if (iso === undefined) return ''
23  const at = new Date(iso)
24  if (Number.isNaN(at.getTime())) return ''
25  const today = new Date(nowMs)
26  const isToday =
27    at.getFullYear() === today.getFullYear() &&
28    at.getMonth() === today.getMonth() &&
29    at.getDate() === today.getDate()
30  const time = `${two(at.getHours())}:${two(at.getMinutes())}`
31
32  return isToday ? `↻ ${time}` : `↻ ${DAYS[at.getDay()]} ${time}`
33}
34
35export const nameOf = (kind: string) =>
36  kind === 'five_hour' ? '5 小时' : kind === 'seven_day' ? '本周' : kind === 'spend_limit' ? '预算' : kind
37
types/index.d.ts 19 lines
1export type GaugeLimit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type GaugeSnap = {
4  contextTokens: number | null
5  contextWindow: number
6  contextPercent: number | null
7  limits: GaugeLimit[]
8  usd: number | null
9  at: number
10}
11
12declare module 'claude-code' {
13  interface PluginState {
14    'usage-gauge': { snap: GaugeSnap | null }
15    // Read-only view of Switchboard's value (it alone writes it).
16    switchboard: { flags: Record<string, { isOn: boolean; isHidden: boolean }> }
17  }
18}
19