Prompt üstünde model, effort, context, 5 saatlik ve haftalık kullanım ile oturum maliyetini gösteren bant

Claude Code için prompt'un üstünde tek satırlık bir kullanım bandı:
Opus 5.5 · high │ ctx ▓▓▓░░░░░ 42% · compact'a 60k │ 5h ▓▓░░░░░░ 23% ↻1s12d │ 7d ▓▓▓▓▓░░░ 61% ↻3g │ $1.24
5h/7d bilgisi yalnızca Claude aboneliğiyle gelir; API anahtarıyla bu bölümler görünmez.
Claude Code'da prompt'a yazın:
/plugin install usage-band --marketplace aliyusufergin/claude-usage-band
Marketplace'i eklemek için y, ardından kapsam olarak user'ı seçin.
Güncellemek için:
claude plugin update usage-band
ardından açık oturumda /reload-plugins.
5h veya 7d kullanımı uyarı eşiğini (%80) geçince bir toast, kritik eşiği (%95) geçince toast ve sistem bildirimi gelir. Her eşik, her limit penceresi için bir kez uyarır; bu bilgi oturumlar arası hatırlanır, pencere sıfırlanınca uyarılar yeniden devreye girer.
/usage-band: bandı gizle / göster/usage-band kompakt: çubukları kapat / açBu tercihler oturumlar arası hatırlanır.
/config)| Ayar | Varsayılan | Açıklama |
|---|---|---|
| Uyarı eşiği (%) | 80 | Toast |
| Kritik eşik (%) | 95 | Toast ve sistem bildirimi |
| Sistem bildirimi | açık | Kritik eşikte masaüstü bildirimi |
| Sarı / kırmızı renk eşiği (%) | 70 / 90 | Renk geçişleri |
| Çubuk genişliği | 8 | 0 verilirse çubuk yok |
| Modeli / effort'u / compact mesafesini / geri sayımı / maliyeti göster | açık | Bölümleri tek tek aç/kapa |
claude plugin validate .
claude plugin test .hooks/register.tsx 267 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit, Timer } from 'claude-code'
3
4import type { ModelInfo, Prefs, Snapshot } from '../types'
5import { readConfig } from './config'
6import type { Config } from './config'
7import {
8 bar,
9 colorOf,
10 countdown,
11 formatCost,
12 formatPercent,
13 formatTokens,
14 isSameWindow,
15 labelOf,
16 levelOf,
17 nameOf,
18 prettyModel,
19 toSnapshot,
20} from './format'
21
22const snapAtom = atom({ plugin: 'usage-band', key: 'snap' } as const, null)
23const modelAtom = atom({ plugin: 'usage-band', key: 'model' } as const, {})
24const compactAtom = atom({ plugin: 'usage-band', key: 'compactAt' } as const, null)
25const prefsAtom = atom({ plugin: 'usage-band', key: 'prefs' } as const, { isHidden: false, isCompact: false })
26
27// $.store keys, kept across sessions.
28const PREFS_KEY = 'prefs'
29const ALERTED_KEY = 'alerted'
30
31type Alerted = Record<string, { level: number; resetsAt?: string }>
32
33type Part = { text: string; color?: string; dim?: boolean }
34type Layout = { barWidth: number; showReset: boolean; showCompact: boolean }
35
36const SEP: Part = { text: ' │ ', dim: true }
37
38/** Widest first; the band takes the first one that fits its columns. */
39const layoutsFor = (config: Config, prefs: Prefs): Layout[] => {
40 const widest = prefs.isCompact ? 0 : config.barWidth
41 return [
42 { barWidth: widest, showReset: config.showReset, showCompact: config.showCompact },
43 { barWidth: Math.min(widest, 5), showReset: config.showReset, showCompact: config.showCompact },
44 { barWidth: 0, showReset: config.showReset, showCompact: config.showCompact },
45 { barWidth: 0, showReset: false, showCompact: config.showCompact },
46 { barWidth: 0, showReset: false, showCompact: false },
47 ]
48}
49
50export const buildParts = (
51 snap: Snapshot | null,
52 model: ModelInfo,
53 compactAt: number | null,
54 now: number,
55 layout: Layout,
56 config: Config,
57): Part[] => {
58 const meter = (label: string, percent: number | undefined): Part[] => {
59 if (percent === undefined) return [{ text: `${label} —`, dim: true }]
60 const color = colorOf(percent, config.warnAt, config.dangerAt)
61 const parts: Part[] = [{ text: `${label} `, dim: true }]
62 if (layout.barWidth > 0) parts.push({ text: `${bar(percent, layout.barWidth)} `, color })
63 parts.push({ text: formatPercent(percent), color })
64 return parts
65 }
66
67 const groups: Part[][] = []
68
69 const effort = config.showEffort ? model.effort : undefined
70 if (config.showModel && model.model) {
71 const group: Part[] = [{ text: prettyModel(model.model), color: 'claude' }]
72 if (effort) group.push({ text: ` · ${effort}`, dim: true })
73 groups.push(group)
74 } else if (effort) {
75 groups.push([{ text: effort, dim: true }])
76 }
77
78 const ctx = meter('ctx', snap?.contextPercent)
79 if (layout.showCompact && compactAt !== null && snap?.contextTokens !== undefined) {
80 const left = compactAt - snap.contextTokens
81 ctx.push(
82 left > 0
83 ? { text: ` · compact'a ${formatTokens(left)}`, dim: true }
84 : { text: ' · compact yakın', color: 'warning' },
85 )
86 }
87 groups.push(ctx)
88
89 for (const limit of snap?.limits ?? []) {
90 const group = meter(labelOf(limit.kind), limit.percent)
91 const left = layout.showReset ? countdown(limit.resetsAt, now) : undefined
92 if (left) group.push({ text: ` ↻${left}`, dim: true })
93 groups.push(group)
94 }
95
96 if (config.showCost && snap?.costUsd !== undefined) groups.push([{ text: formatCost(snap.costUsd), dim: true }])
97
98 return groups.flatMap((group, i) => (i === 0 ? group : [SEP, ...group]))
99}
100
101const widthOf = (parts: Part[]) => parts.reduce((n, p) => n + p.text.length, 0)
102
103async function readAlerted($: EngineInterface): Promise<Alerted> {
104 const value = await $.store.get(ALERTED_KEY)
105 return value && typeof value === 'object' ? (value as Alerted) : {}
106}
107
108async function readPrefs($: EngineInterface): Promise<Prefs | undefined> {
109 const value = (await $.store.get(PREFS_KEY)) as Partial<Prefs> | undefined
110 if (!value || typeof value !== 'object') return undefined
111 return { isHidden: value.isHidden === true, isCompact: value.isCompact === true }
112}
113
114async function savePrefs($: EngineInterface, fn: (p: Prefs) => Prefs) {
115 await update($, prefsAtom, fn)
116 const prefs = await read($, prefsAtom)
117 await $.store.set(PREFS_KEY, prefs)
118 return prefs
119}
120
121// The model whose compaction threshold `compactAtom` holds.
122let compactFor: string | undefined
123let isCompactKnown = false
124
125async function refreshCompact($: EngineInterface, config: Config) {
126 if (!config.showCompact) return
127 const { model } = await read($, modelAtom)
128 if (isCompactKnown && compactFor === model) return
129 // `summary` estimates locally: no token-count requests.
130 const usage = await $.session.usage({ breakdown: 'summary' })
131 const breakdown = usage.context.breakdown
132 const at = breakdown?.isAutoCompactEnabled ? (breakdown.autoCompactThreshold ?? null) : null
133 await update($, compactAtom, () => at)
134 compactFor = model
135 isCompactKnown = breakdown !== undefined
136}
137
138async function alertLimits($: EngineInterface, limits: SessionRateLimit[], config: Config) {
139 const thresholds = [...new Set([config.toastAt, config.notifyAt])].sort((a, b) => a - b)
140 const alerted = await readAlerted($)
141 const next: Alerted = { ...alerted }
142 const now = await $.clock.now()
143 let isChanged = false
144
145 for (const limit of limits) {
146 const prev = alerted[limit.kind]
147 const isSame = prev !== undefined && isSameWindow(prev.resetsAt, limit.resetsAt)
148 const was = isSame ? prev.level : 0
149 const level = levelOf(limit.percentUsed, thresholds)
150 if (isSame && level === was) continue
151
152 next[limit.kind] = { level, resetsAt: limit.resetsAt }
153 isChanged = true
154 // Only rising past a threshold alerts; a new window or a drop re-arms it.
155 if (level <= was) continue
156
157 const left = countdown(limit.resetsAt, now)
158 const text = `${nameOf(limit.kind)} kullanım %${Math.round(limit.percentUsed)}${left ? ` · sıfırlanmaya ${left}` : ''}`
159 $.ui.toast(text, { timeoutMs: 8000 })
160 if (config.notify && level >= config.notifyAt && was < config.notifyAt) {
161 await $.ui.notify(text, { title: 'Claude kullanım limiti' })
162 }
163 }
164
165 if (isChanged) await $.store.set(ALERTED_KEY, next)
166}
167
168export const register: Register = (on, options) => {
169 const config = readConfig(options)
170 // A reload may bring new options: read the threshold afresh.
171 isCompactKnown = false
172
173 let tick: Timer | undefined
174
175 on('session.start', async ($, e, next) => {
176 const result = await next(e)
177
178 const prefs = await readPrefs($)
179 if (prefs) await update($, prefsAtom, () => prefs)
180
181 const model = await $.session.model()
182 await update($, modelAtom, m => (m.model ? m : { ...m, model }))
183
184 const usage = await $.session.usage()
185 await update($, snapAtom, () => toSnapshot(usage))
186 await refreshCompact($, config)
187
188 await $.command.register({
189 name: 'usage-band',
190 description: 'Kullanım bandını gizle/göster; "kompakt" ile çubukları aç/kapa',
191 argumentHint: '[kompakt]',
192 })
193
194 // Countdowns move while the session is idle: redraw once a minute.
195 tick?.cancel()
196 tick = $.clock.every(60_000, () => {
197 void $.ui.invalidate('ui.render')
198 })
199
200 return result
201 })
202
203 on('session.end', ($, e, next) => {
204 tick?.cancel()
205 tick = undefined
206 return next(e)
207 })
208
209 on('command.run', { command: 'usage-band' }, async ($, e) => {
210 const arg = e.args.trim().toLowerCase()
211 if (arg === 'kompakt' || arg === 'compact') {
212 const prefs = await savePrefs($, p => ({ isHidden: false, isCompact: !p.isCompact }))
213 return { text: prefs.isCompact ? 'Kullanım bandı kompakt modda.' : 'Kullanım bandı tam modda.' }
214 }
215 const prefs = await savePrefs($, p => ({ ...p, isHidden: !p.isHidden }))
216 return { text: prefs.isHidden ? 'Kullanım bandı gizlendi.' : 'Kullanım bandı gösteriliyor.' }
217 })
218
219 on('session.measure', async ($, e, next) => {
220 await update($, snapAtom, () => toSnapshot(e))
221 if (e.changed.includes('context')) await refreshCompact($, config)
222 if (e.changed.includes('rateLimits')) await alertLimits($, e.rateLimits, config)
223 return next(e)
224 })
225
226 // The model and effort the main loop actually sends, after /model or a fallback.
227 on('turn.step', async function* ($, e, next) {
228 if (!e.agentId) {
229 const effort = e.effort === undefined ? undefined : String(e.effort)
230 const current = await read($, modelAtom)
231 if (current.model !== e.model || current.effort !== effort) {
232 await update($, modelAtom, () => ({ model: e.model, effort }))
233 }
234 }
235 return yield* next(e)
236 })
237
238 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
239 if (e.props.hasSurvey) return next(e)
240
241 const prefs = await read($, prefsAtom)
242 if (prefs.isHidden) return next(e)
243
244 const snap = await read($, snapAtom)
245 const model = await read($, modelAtom)
246 if (!snap && !model.model) return next(e)
247
248 const compactAt = await read($, compactAtom)
249 const now = await $.clock.now()
250 const columns = e.props.bodyColumns
251 const candidates = layoutsFor(config, prefs).map(layout => buildParts(snap, model, compactAt, now, layout, config))
252 const parts = candidates.find(p => widthOf(p) <= columns) ?? candidates[candidates.length - 1] ?? []
253
254 const { Box, Text } = $.ui.resolve(e)
255
256 return (
257 <Box>
258 {parts.map((p, i) => (
259 <Text key={`p${i}`} color={p.color} dimColor={p.dim} wrap="truncate">
260 {p.text}
261 </Text>
262 ))}
263 </Box>
264 )
265 })
266}
267hooks/config.ts 40 lines1import type { PluginOptions } from 'claude-code'
2
3export type Config = {
4 toastAt: number
5 notifyAt: number
6 notify: boolean
7 warnAt: number
8 dangerAt: number
9 barWidth: number
10 showModel: boolean
11 showEffort: boolean
12 showCompact: boolean
13 showReset: boolean
14 showCost: boolean
15}
16
17export const DEFAULTS: Config = {
18 toastAt: 80,
19 notifyAt: 95,
20 notify: true,
21 warnAt: 70,
22 dangerAt: 90,
23 barWidth: 8,
24 showModel: true,
25 showEffort: true,
26 showCompact: true,
27 showReset: true,
28 showCost: true,
29}
30
31export const readConfig = (options: PluginOptions | undefined): Config => {
32 const config = { ...DEFAULTS }
33 for (const key of Object.keys(DEFAULTS) as (keyof Config)[]) {
34 const value = options?.[key]
35 if (typeof value === typeof DEFAULTS[key]) (config as Record<string, unknown>)[key] = value
36 }
37 config.barWidth = Math.max(0, Math.min(20, Math.round(config.barWidth)))
38 return config
39}
40hooks/format.ts 86 lines1import type { SessionContextUsage, SessionCost, SessionRateLimit } from 'claude-code'
2
3import type { Snapshot } from '../types'
4
5const LABELS: Record<string, string> = {
6 five_hour: '5h',
7 seven_day: '7d',
8 spend_limit: 'limit',
9}
10
11const NAMES: Record<string, string> = {
12 five_hour: '5 saatlik',
13 seven_day: 'Haftalık',
14 spend_limit: 'Harcama',
15}
16
17export const labelOf = (kind: string) => LABELS[kind] ?? kind
18export const nameOf = (kind: string) => NAMES[kind] ?? kind
19
20export const toSnapshot = (u: {
21 context: SessionContextUsage
22 rateLimits: SessionRateLimit[]
23 cost?: SessionCost
24}): Snapshot => ({
25 contextPercent: u.context.percent,
26 contextTokens: u.context.tokens,
27 contextWindow: u.context.window,
28 limits: u.rateLimits.map(l => ({ kind: l.kind, percent: l.percentUsed, resetsAt: l.resetsAt })),
29 costUsd: u.cost?.usd,
30})
31
32/** "claude-opus-5-5[1m]" → "Opus 5.5 1M", "sonnet" → "Sonnet". */
33export const prettyModel = (id: string) => {
34 const is1m = /\[1m\]/i.test(id)
35 const m = /(opus|sonnet|haiku|fable)(?:-(\d+)(?:-(\d{1,2}))?(?!\d))?/i.exec(id)
36 const family = m?.[1]
37 const base = family
38 ? [family.charAt(0).toUpperCase() + family.slice(1).toLowerCase(), m[2] && (m[3] ? `${m[2]}.${m[3]}` : m[2])]
39 .filter(Boolean)
40 .join(' ')
41 : id.replace(/^claude-/, '').replace(/\[1m\]/i, '')
42 return is1m ? `${base} 1M` : base
43}
44
45/** Kalan süre: "45d", "1s12d", "3g4s" (gün, saat, dakika). */
46export const countdown = (resetsAt: string | undefined, now: number) => {
47 if (!resetsAt) return undefined
48 const ms = Date.parse(resetsAt) - now
49 if (Number.isNaN(ms)) return undefined
50 const minutes = Math.max(0, Math.ceil(ms / 60_000))
51 if (minutes < 60) return `${minutes}d`
52 const hours = Math.floor(minutes / 60)
53 if (hours < 24) return `${hours}s${minutes % 60 ? `${minutes % 60}d` : ''}`
54 const days = Math.floor(hours / 24)
55 return `${days}g${hours % 24 ? `${hours % 24}s` : ''}`
56}
57
58export const colorOf = (percent: number, warnAt = 70, dangerAt = 90) =>
59 percent >= dangerAt ? 'error' : percent >= warnAt ? 'warning' : 'success'
60
61export const bar = (percent: number, width: number) => {
62 const filled = Math.min(width, Math.max(0, Math.round((percent / 100) * width)))
63 return '▓'.repeat(filled) + '░'.repeat(width - filled)
64}
65
66export const formatPercent = (p: number) => `${Math.round(p)}%`
67
68export const formatTokens = (n: number) =>
69 n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`
70
71export const formatCost = (usd: number) => `$${usd < 10 ? usd.toFixed(2) : usd.toFixed(1)}`
72
73/** The highest threshold `percent` has reached, 0 below all. */
74export const levelOf = (percent: number, thresholds: readonly number[]) =>
75 thresholds.reduce((level, t) => (percent >= t && t > level ? t : level), 0)
76
77/**
78 * Whether two readings name the same rate-limit window: the reported reset
79 * time can drift a little between responses, a new window moves it by hours.
80 */
81export const isSameWindow = (a: string | undefined, b: string | undefined) => {
82 if (a === b) return true
83 if (!a || !b) return false
84 return Math.abs(Date.parse(a) - Date.parse(b)) < 15 * 60_000
85}
86types/index.d.ts 27 lines1export type Limit = { kind: string; percent: number; resetsAt?: string }
2
3export type Snapshot = {
4 contextPercent?: number
5 contextTokens?: number
6 contextWindow: number
7 limits: Limit[]
8 costUsd?: number
9}
10
11export type ModelInfo = { model?: string; effort?: string }
12
13/** The person's view choices from /usage-band, kept across sessions too. */
14export type Prefs = { isHidden: boolean; isCompact: boolean }
15
16declare module 'claude-code' {
17 interface PluginState {
18 'usage-band': {
19 snap: Snapshot | null
20 model: ModelInfo
21 /** Token count auto-compaction runs at; null when off or unknown. */
22 compactAt: number | null
23 prefs: Prefs
24 }
25 }
26}
27