SLOPSHOPPER

usage-band

Show context and 5h rate-limit usage above the prompt

newband
★ 2v0.1.0no licenseupdated 2026-10-16korryu3/dotfiles/.claude/mods/usage-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band
› 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 ctx ⣿⣿⣿⣷⠀⠀⠀⠀ 49% 5h ⣿⣿⣤⠀⠀⠀⠀⠀ 31% @09:53 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
ctx ⣿⣿⣿⣷⠀⠀⠀⠀ 49% 5h ⣿⣿⣤⠀⠀⠀⠀⠀ 31% @09:53
README

dotfiles

Zsh Neovim Ghostty Starship Homebrew macOS

Setup

# 1. Clone (ghqのディレクトリ構造に合わせる)
git clone https://github.com/korryu3/dotfiles.git ~/ghq/github.com/korryu3/dotfiles
cd ~/ghq/github.com/korryu3/dotfiles

# 2. Install (Homebrew導入 → パッケージ → シンボリックリンク → セットアップ → macOS設定)
./install.sh

# 個人用パッケージも含める場合
./install.sh --personal

# 3Dパッケージ (colmap, rtabmap, cloudcompare) も含める場合
./install.sh --3d

# 3. Brewfileにないパッケージを削除 (任意)
brew bundle cleanup --file=~/ghq/github.com/korryu3/dotfiles/Brewfile
brew bundle cleanup --file=~/ghq/github.com/korryu3/dotfiles/Brewfile --force  # 実際に削除

Manual Setup

install.shで自動化できない設定はdocs/manual-setup.mdを参照。

memo

  • nvim Tutorをやりたい場合
  • nvim --cleanでクリーン環境なnvimを起動
  • :Turoeを起動
  • -> lazyvimのdisabled_pluginsのtutorを有効化して進めると、デザインが崩れてしまうので、デフォルトnvimで起動する

TODO

  • ☑ macOS system defaults (Dock, Finder, key repeat etc.)
  • ☐ Nix Home Manager migration
  • ☐ VSCode Settings Sync
  • ☐ Serena config (~/.serena/serena_config.yml) の管理(projects リストがPC固有で自動更新されるため要検討)
Source 2 files
hooks/register.tsx 97 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
3
4import type { UsageSnapshot } from '../types'
5
6const snapshot = atom({ plugin: 'usage-band', key: 'snapshot' } as const, null)
7
8const BRAILLE = '⠀⣀⣄⣤⣦⣶⣷⣿'
9const BAR_WIDTH = 8
10
11const toSnapshot = (
12  context: SessionContextUsage,
13  rateLimits: SessionRateLimit[],
14): UsageSnapshot => {
15  const five = rateLimits.find(limit => limit.kind === 'five_hour')
16
17  return {
18    ctxPercent: context.percent,
19    fiveHour: five && { percentUsed: five.percentUsed, resetsAt: five.resetsAt },
20  }
21}
22
23const brailleBar = (pct: number): string => {
24  const level = Math.min(Math.max(pct, 0), 100) / 100
25  let bar = ''
26  for (let i = 0; i < BAR_WIDTH; i++) {
27    const start = i / BAR_WIDTH
28    const end = (i + 1) / BAR_WIDTH
29    if (level >= end) {
30      bar += BRAILLE[7]
31    } else if (level <= start) {
32      bar += BRAILLE[0]
33    } else {
34      bar += BRAILLE[Math.min(Math.floor(((level - start) / (end - start)) * 7), 7)]
35    }
36  }
37
38  return bar
39}
40
41const levelColor = (pct: number) =>
42  pct <= 30 ? 'success' : pct <= 60 ? 'warning' : 'error'
43
44const formatHHMM = (iso: string): string | undefined => {
45  const date = new Date(iso)
46  if (Number.isNaN(date.getTime())) {
47    return undefined
48  }
49  const pad = (n: number) => String(n).padStart(2, '0')
50
51  return `${pad(date.getHours())}:${pad(date.getMinutes())}`
52}
53
54export const register: Register = on => {
55  on('session.start', async ($, e, next) => {
56    const { context, rateLimits } = await $.session.usage()
57    await update($, snapshot, () => toSnapshot(context, rateLimits))
58
59    return next(e)
60  })
61
62  on('session.measure', async ($, e, next) => {
63    await update($, snapshot, () => toSnapshot(e.context, e.rateLimits))
64
65    return next(e)
66  })
67
68  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
69    const snap = await read($, snapshot)
70    const ctx = snap?.ctxPercent
71    const five = snap?.fiveHour
72    if (e.props.hasSurvey || (ctx === undefined && five === undefined)) {
73      return next(e)
74    }
75
76    const { Box, Text } = $.ui.resolve(e)
77    const meter = (label: string, pct: number) => (
78      <Text>
79        <Text dimColor>{label} </Text>
80        <Text color={levelColor(pct)}>
81          {brailleBar(pct)} {Math.round(pct)}%
82        </Text>
83      </Text>
84    )
85    const reset = five?.resetsAt && formatHHMM(five.resetsAt)
86
87    return (
88      <Box>
89        {ctx !== undefined && meter('ctx', ctx)}
90        {ctx !== undefined && five !== undefined && <Text>{'   '}</Text>}
91        {five !== undefined && meter('5h', five.percentUsed)}
92        {reset && <Text dimColor> @{reset}</Text>}
93      </Box>
94    )
95  })
96}
97
types/index.d.ts 11 lines
1export type UsageSnapshot = {
2  ctxPercent?: number
3  fiveHour?: { percentUsed: number; resetsAt?: string }
4}
5
6declare module 'claude-code' {
7  interface PluginState {
8    'usage-band': { snapshot: UsageSnapshot | null }
9  }
10}
11