SLOPSHOPPER

usage-band

פס מעל תיבת ההודעה: כמה נשאר בחלון 5 שעות ובחלון השבועי, מילוי context ועלות הסשן

newbandcommandtoast
v0.1.0no licenseupdated 2026-10-243floors/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 › /usage-band ⎿ usage-band: פס ה-usage מוסתר. /usage-band מחזיר אותו. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mods

Small mods for Claude Code, made for working in Hebrew.

ModWhat it does
rtl-messagesHebrew and Arabic messages, yours and Claude's, drawn right to left in the terminal. The desktop app already does this by itself, so there the mod stays out of the way.
usage-bandA band above the prompt: how much is left of the 5-hour and weekly limits, context fill and session cost. /usage-band hides or shows it.
long-task-alertA chime and a short message when a task took 5 minutes or more. The minutes are a setting.

Install

In a terminal running Claude Code, install the ones you want:

/plugin install rtl-messages --marketplace 3floors/claude-mods
/plugin install usage-band --marketplace 3floors/claude-mods
/plugin install long-task-alert --marketplace 3floors/claude-mods

In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with 3floors/claude-mods, then Add plugin.

The chime in long-task-alert is an original sound made for this repository, free to use.

Source 3 files
hooks/register.tsx 96 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Snapshot } from '../types'
5import { WARN_AT, bar, colorFor, fmtReset, labelFor, ltr, rtl, snapshotOf, svgBar } from './format'
6
7const usage = atom({ plugin: 'usage-band', key: 'usage' } as const, null as Snapshot | null)
8const isHidden = atom({ plugin: 'usage-band', key: 'isHidden' } as const, false)
9
10export const register: Register = on => {
11  const warned = new Set<string>() // kinds already toasted at WARN_AT
12
13  on('session.start', async ($, e, next) => {
14    await $.command.register({
15      name: 'usage-band',
16      description: 'הצג או הסתר את פס ה-usage מעל תיבת ההודעה',
17    })
18    const snap = snapshotOf(await $.session.usage())
19    await update($, usage, () => snap)
20    return next(e)
21  })
22
23  on('command.run', { command: 'usage-band' }, async $ => {
24    const hidden = await read($, isHidden)
25    await update($, isHidden, () => !hidden)
26    return { text: hidden ? 'פס ה-usage מוצג.' : 'פס ה-usage מוסתר. /usage-band מחזיר אותו.' }
27  })
28
29  on('session.measure', async ($, e, next) => {
30    const snap = snapshotOf(e)
31    await update($, usage, () => snap)
32
33    for (const l of snap.limits) {
34      if (l.percentUsed >= WARN_AT && !warned.has(l.kind)) {
35        warned.add(l.kind)
36        $.ui.toast(`חלון ${labelFor(l.kind)}: נשאר ${Math.round(100 - l.percentUsed)}%`)
37      } else if (l.percentUsed < WARN_AT) {
38        warned.delete(l.kind)
39      }
40    }
41    return next(e)
42  })
43
44  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
45    const snap = await read($, usage)
46    if (e.props.hasSurvey || snap === null || (await read($, isHidden))) return next(e)
47    // before the first reply there is nothing to show: no limits, context or cost yet
48    if (snap.limits.length === 0 && snap.contextPercent === undefined && snap.costUsd === undefined) {
49      return next(e)
50    }
51
52    const now = await $.clock.now()
53    const els = $.ui.resolve(e)
54    const { Box, Text, Button } = els
55    const Svg = 'Svg' in els ? els.Svg : null
56    // terminals do no bidi of their own: flip only where a webview draws
57    const isRtl = e.surface !== 'terminal'
58    const wrap = (text: string) => (isRtl ? rtl(text) : text)
59
60    return (
61      <Box columnGap={3} alignItems="center" flexDirection={isRtl ? 'row-reverse' : 'row'}>
62        {snap.limits
63          .filter(l => l.kind === 'five_hour' || l.kind === 'seven_day')
64          .map(l => {
65            const left = Math.max(0, Math.round(100 - l.percentUsed))
66            const label = labelFor(l.kind)
67            const color = colorFor(left)
68            return (
69              <Box key={l.kind} columnGap={1} alignItems="center" flexDirection={isRtl ? 'row-reverse' : 'row'}>
70                <Text key="l" dimColor>{wrap(label)}</Text>
71                {Svg ? (
72                  <Svg key="b" source={svgBar(left, isRtl)} alt={`${label}: נשאר ${left}%`} width={64} height={8} />
73                ) : (
74                  <Text key="b" color={color}>{bar(left)}</Text>
75                )}
76                <Text key="p" color={color} bold>{wrap(`${left}% נשאר`)}</Text>
77                <Text key="r" dimColor>{wrap(fmtReset(l.resetsAt, now))}</Text>
78              </Box>
79            )
80          })}
81        {snap.contextPercent !== undefined && (
82          <Text key="ctx" dimColor>
83            {wrap(`קונטקסט ${Math.round(snap.contextPercent)}%`)}
84          </Text>
85        )}
86        {snap.costUsd !== undefined && (
87          <Text key="cost" dimColor>
88            {isRtl ? ltr(`$${snap.costUsd.toFixed(2)}`) : `$${snap.costUsd.toFixed(2)}`}
89          </Text>
90        )}
91        <Button key="hide" label="הסתר" onPress={() => update($, isHidden, () => true)} />
92      </Box>
93    )
94  })
95}
96
hooks/format.ts 59 lines
1import type { Snapshot } from '../types'
2
3export const WARN_AT = 90
4
5export const labelFor = (kind: string): string =>
6  kind === 'five_hour' ? '5 שעות' : kind === 'seven_day' ? 'שבוע' : kind
7
8// remaining % -> theme color; undefined keeps the default text color
9export const colorFor = (left: number): 'error' | 'warning' | 'success' =>
10  left < 10 ? 'error' : left < 30 ? 'warning' : 'success'
11
12// same tiers as hex, for the SVG bar (an image: no theme keys there)
13export const hexFor = (left: number): string =>
14  left < 10 ? '#ef4444' : left < 30 ? '#f59e0b' : '#22c55e'
15
16// rounded track + fill; rtl grows the fill from the right edge
17export const svgBar = (left: number, isRtl: boolean, w = 64, h = 8): string => {
18  const fill = Math.round((Math.max(0, Math.min(100, left)) / 100) * w)
19  const x = isRtl ? w - fill : 0
20  return (
21    `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">` +
22    `<rect width="${w}" height="${h}" rx="${h / 2}" fill="#888" fill-opacity="0.25"/>` +
23    (fill > 0 ? `<rect x="${x}" width="${fill}" height="${h}" rx="${h / 2}" fill="${hexFor(left)}"/>` : '') +
24    `</svg>`
25  )
26}
27
28export const bar = (left: number, width = 8): string => {
29  const filled = Math.max(0, Math.min(width, Math.round((left / 100) * width)))
30  return '▰'.repeat(filled) + '▱'.repeat(width - filled)
31}
32
33export const fmtReset = (iso: string | undefined, now: number): string => {
34  if (!iso) return ''
35  const ms = Date.parse(iso) - now
36  if (!(ms > 0)) return 'מתאפס עכשיו'
37  const mins = Math.round(ms / 60000)
38  if (mins < 60) return `איפוס בעוד ${mins} דק׳`
39  const hours = Math.floor(mins / 60)
40  if (hours < 24) return `איפוס בעוד ${hours}:${String(mins % 60).padStart(2, '0')} ש׳`
41  return `איפוס בעוד ${Math.round(hours / 24)} ימים`
42}
43
44// minimal structural view of SessionUsage / SessionMeasureInput
45export const snapshotOf = (u: {
46  rateLimits: { kind: string; percentUsed: number; resetsAt?: string }[]
47  context: { percent?: number }
48  cost?: { usd: number }
49}): Snapshot => ({
50  limits: u.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt })),
51  contextPercent: u.context.percent,
52  costUsd: u.cost?.usd,
53})
54
55// bidi isolates: RLI/LRI ... PDI. Wrap a whole item so its numbers, % and bar
56// glyphs order right-to-left inside Hebrew; ltr() keeps "$1.50" from flipping.
57export const rtl = (s: string): string => `⁧${s}⁩`
58export const ltr = (s: string): string => `⁦${s}⁩`
59
types/index.d.ts 13 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Snapshot = {
3  limits: Limit[]
4  contextPercent?: number
5  costUsd?: number
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'usage-band': { usage: Snapshot | null; isHidden: boolean }
11  }
12}
13