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

Small mods for Claude Code, made for working in Hebrew.
| Mod | What it does |
|---|---|
rtl-messages | Hebrew 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-band | A 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-alert | A chime and a short message when a task took 5 minutes or more. The minutes are a setting. |
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.
hooks/register.tsx 96 lines1import { 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}
96hooks/format.ts 59 lines1import 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}`
59types/index.d.ts 13 lines1export 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