SLOPSHOPPER

usage-header

Band above the prompt: context window broken down by category in colours, plus the 5-hour and weekly limits. Hungarian or English.

newbandcommand
v0.1.0no licenseupdated 2026-10-07Szotasz/claude-mods/plugins/usage-header
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-header
› 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 › /hasznalat ⎿ usage-header: Usage band off. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mods

Saját Claude Code mod-ok (function hook pluginek) egy pluginpiacon.

Telepítés

claude plugin marketplace add Szotasz/claude-mods
claude plugin install usage-band@claude-mods
claude plugin install usage-header@claude-mods
claude plugin install project-pane@claude-mods
claude plugin install focus-mode@claude-mods
claude plugin install tool-hub@claude-mods
claude plugin install snake-pane@claude-mods
claude plugin install supabase-guard@claude-mods
claude plugin install public-repo-guard@claude-mods
claude plugin install decision-popup@claude-mods

Frissítés: claude plugin update usage-band@claude-mods, majd /reload-plugins (vagy új session).

Pluginek

usage-band

A státuszsorba (a prompt alá) írja:

🤖 Opus 5.5  │  Ctx ███░░░░░ 42% 420k/1M  │  5h ███░░░░░ 34% → 16:20  │  Hét ███████░ 93% ⚠ → Cs 10:00
  • modell neve (/model váltáskor és turn végén frissül)
  • kontextusablak telítettsége
  • 5 órás és heti rate-limit, reset időponttal (csak Pro/Max előfizetéssel)
  • ⚠ 90% felett, és egy toast, amikor egy limit átlépi (ablakonként, resetenként egyszer)

Nyelv / Language: /config → usage-band.language, vagy a settings.json-ban:

"pluginConfigs": { "usage-band": { "language": "en" } }
  • auto (alapértelmezett): magyar, ha a rendszer nyelve (LANG) magyar, különben angol
  • hu: magyar (Hét, Cs 10:00)
  • en: English (Week, Thu 10:00)

Követelmény: Claude Code 2.1.289 körüli verzió. A mod-API early access, kiadásról kiadásra változhat.

usage-header

Sáv a prompt fölött: a kontextusablak kategóriánként színezve, alatta az 5 órás és a heti limit.

Kontextus  ██▓▓▓▓▓▓████████████▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░ 42% 84k/200k
           ■ Rendszerprompt 3k  ■ Eszközök 12k  ■ Memória 400  ■ Üzenetek 69k  ▒ Tömörítési tartalék 33k  ░ Szabad 83k
Limitek    5 óra ███████░░░░░░░░░░░░░ 34% → 16:20   Hét ███████████████████░ 93% ⚠ → Cs 10:00
  • A kontextussáv minden kategóriája a /context saját színével látszik (rendszerprompt, eszközök, MCP, memóriafájlok, skillek, ágensek, üzenetek), utána a tömörítési tartalék és a szabad hely halványan; alatta jelmagyarázat tokenszámmal. Az igény szerint betöltött (halasztott) eszközsémák kimaradnak, mert nincsenek az ablakban.
  • A bontás helyi becslés ($.session.usage({ breakdown: 'summary' })), API-hívás nélkül; minden turn végén és limitmozduláskor frissül.
  • Limitek: zöld 70% alatt, sárga 70–90%, piros 90% fölött (⚠), reset időponttal. Csak Pro/Max előfizetéssel van adat.
  • /hasznalat kapcsolja, /hasznalat be|ki beállítja; az állapotot sessionök között megőrzi. Az eszköztárban (/eszkozok) is kapcsolható.
  • A sávot megosztja a fókusz mód listájával és az eszköztárral (azok alatt jelenik meg).

Beállítás (/config): usage-header.language (auto/hu/en).

project-pane

Oldalpanel az aktuális git repóhoz. Magától megnyílik, ha a session git repóban indul és a terminál legalább 144 oszlop széles (miután egyszer /project-tel megnyitottad, 110 is elég); keskenyebb ablaknál egy toast szól, hogy vár. Bármikor: /project.

acme/shop                          ↻ most [ Frissítés ]
GIT
🌿 develop  ↑0 ↓0  · 7 módosított fájl
utolsó commit: 57704fe · 7 hónapja — Fix checkout flow
⚠ a main 13 committal előrébb jár, te a develop branch-en vagy
Előbb commitold a módosításokat, vagy tedd félre őket:
[ Stash + szinkron ]
DEPLOY (Netlify)
✔ ready  main  3 perce
PULL REQUESTEK
✔ #142 Stripe webhook retry  · approved
CI (GitHub Actions)
✘ netlify-deploy-verify  main · 5 napja      [ Kérdezd Claude-ot ]
SUPABASE
abcdefghijklmnopqrst · Acme Shop · eu-west-3
18 migráció · utolsó: 018_add_orders_rls.sql
GitHub  Netlify  Supabase
  • Szinkron gomb (s): csak fast-forward, soha nem merge-öl, rebase-el vagy töröl.
  • a branch lemaradt az upstreamtől → git pull --ff-only
  • másik branchen vagy, és az alapértelmezett (main) előrébb jár → git switch main + git merge --ff-only origin/main
  • commitolatlan módosítás esetén csak „Stash + szinkron” van: git stash push -u, vissza: git stash pop
  • ha elakad, a hibát Claude-nak szóló kérdésként a promptba teszi
  • Frissítés (r): 60 mp-enként és minden turn végén; git fetch 5 percenként.
  • Kérdezd Claude-ot (a): a legutóbbi elbukott CI-futásról kérdést tesz a promptba.
  • Toast, ha a session alatt elbukik egy Netlify deploy vagy CI-futás.

Adatforrások (a CLI-k saját bejelentkezésével, a plugin tokent nem olvas):

SzekcióForrásHa hiányzik
Gitgit—
PR, CIgh (gh auth login)tipp a panelen
Deploynetlify CLI + .netlify/state.jsonnpm i -g netlify-cli && netlify login
Supabasesupabase/.temp/project-ref, supabase/migrations, supabase projects listsupabase login az állapothoz

Beállítás (/config): project-pane.language (auto/hu/en), project-pane.autoOpen (alapból be).

focus-mode

Fókusz mód: a transcriptből eltűnnek az eszközhívások és eredményeik, a köztes szövegek, a saját promptjaid és a parancsok kimenete. Csak két dolog marad: a prompt fölötti sávban a részfeladatok listája állapotsávval és pipával, alatta a turn végső válasza.

✔ Meglévő mod-repo áttekintése
◐ Játék-mod megírása  ██████░░░░ 60%
○ Felvétel az eszközök közé
  • /fokusz kapcsolja, /fokusz be / /fokusz ki beállítja; az állapotot sessionök között megőrzi.
  • A listát a modell tölti a mod által regisztrált mcp__focus-mode__plan eszközzel; a rendszerprompt bekapcsolt állapotban erre utasítja (minden több lépéses kérésnél előbb a részfeladatok, aztán frissítés indításkor, haladáskor és befejezéskor).
  • Kikapcsolva minden a megszokott módon látszik.

Beállítás (/config): focus-mode.language (auto/hu/en).

tool-hub

Eszköztár: egy helyről kapcsolhatók a fenti modok. A prompt alatti lábléc jobb oldalán lévő ⚙ Eszközök gomb (vagy /eszkozok) a prompt fölötti sávban nyitja meg:

⚙ Szabolcs eszközei  kattintás, vagy ctrl+x tab után szám  [ Bezár ]
1. Fókusz mód     ● BE  [ Kikapcsol ] · Csak a részfeladatok listája és a végső válasz látszik
2. Limitsáv       ○ KI  [ Bekapcsol ] · Alsó sáv: 5 órás és heti limit, kontextusablak
3. Használati sáv ● BE  [ Kikapcsol ] · Prompt fölött: kontextus kategóriánként színezve, 5 órás és heti limit
4. Projektpanel   ● BE  [ Kikapcsol ] · GitHub, CI, Netlify és Supabase állapot oldalt
5. Kígyó játék    ● BE  [ Kikapcsol ] · Oldalt nyílik promptküldéskor, amíg Claude dolgozik
  • Modonként BE/KI állapot és kapcsoló gomb, ctrl+x tab után az 1–5 számbillentyűvel is.
  • Ami nincs telepítve, „nincs betöltve” felirattal jelenik meg.
  • Bekapcsolt fókusz módnál a lábléc is kiírja: „Fókusz mód”.
  • Parancsból is ugyanez: /fokusz, /limitsav, /hasznalat, /project, /jatek (mind be|ki argumentummal).
  • A sávot megosztja a fókusz mód listájával (a kapcsolók alatta jelennek meg).

Új mod felvétele az eszköztárba: a mod írja a saját isOn állapotát, adjon /<parancs> be|ki-t, és hookolja a state.set-et { plugin: 'tool-hub', key: 'request' }-re; a hubban egy sor a TOOLS listába, egy ág a stateOf-ba és a kulcs a types/index.d.ts-be.

Beállítás (/config): tool-hub.language (auto/hu/en).

snake-pane

Kígyó játék az oldalpanelben, amíg Claude dolgozik.

⏳ Claude dolgozik…
Pont: 4  Rekord: 17
╭──────────────────────────────────────╮
│                                      │
│          ████████                    │
│                ██      ●             │
╰──────────────────────────────────────╯
p: szünet · r: újra
w: ↑ a: ← s: ↓ d: → p: szünet r: új
  • Promptküldéskor magától megnyílik (ha ebben a sessionben bezártad, már nem; /jatek mindig nyitja). /jatek ki kikapcsolja, az eszköztárban (/eszkozok) is kapcsolható.
  • Irányítás: kattints a táblára, utána nyilak / wasd / hjkl, szóköz vagy p szünet, r új játék. Vagy ctrl+x tab a panelre, és a w a s d p r gombok.
  • A turn végén a futó játék szünetel („Claude végzett”), hogy visszatérj a munkához.
  • A pontszámmal gyorsul; a rekordot sessionök között megőrzi.
  • A játék a rajzoló szálon fut (Client surface modul), nem terheli a sessiont.

supabase-guard

Supabase-őr: megállítja a Supabase-hívást, ha nem az aktuális mappa projektjére menne, és egygombos felugróban rákérdez.

  • A mappa projektjét a supabase/.temp/project-ref, a .env* fájlok https://<ref>.supabase.co címei és a CLAUDE.md (ha egyetlen projekt-ID-t említ) adják, plusz amit a felugróban „Ez a mappa projektje” gombbal megerősítettél.
  • Más projektre menő írás (MCP execute_sql, apply_migration, deploy_edge_function, branch-műveletek, illetve supabase db push --project-ref …): kérdez — Megtiltom / Engedélyezem / Ez a mappa projektje.
  • Romboló művelet a saját projekten is kérdez: DROP, TRUNCATE, DELETE FROM, WHERE nélküli UPDATE, REVOKE, RLS kikapcsolása, reset_branch, delete_branch, pause_project, supabase db reset --linked.
  • Ha a mappához nem talál projektet, írás előtt kérdez (van „Engedem a session végéig” gomb is).
  • Más projekt olvasása fut, de a modell kap egy figyelmeztetést, hogy ne a rossz projektről vonjon le következtetést.
  • Hiba esetén zárva marad: ha az ellenőrzés elhasal, a Supabase-hívás nem fut le. /supaor be|ki, vagy az eszköztárban.

public-repo-guard

Push-őr: nyilvános GitHub-repóba push előtt (és gh repo edit --visibility public / gh repo create --public előtt) átnézi, mi menne ki.

  • A távoli ágon még nem lévő commitokat nézi (üzenet + hozzáadott sorok, a history is); ha nincs mihez mérni, az egész fát. Kezeli a git push origin forrás:cél alakot is.
  • Keres: Supabase projekt-ID (URL, project_id, táblázatsor), JWT / Supabase-kulcs, Stripe/Anthropic/OpenAI/GitHub/AWS/Google/Slack/Resend/Telegram/ElevenLabs kulcsok, privát kulcs, e-mail-cím (a saját git e-mail és a noreply/példa címek kivételével), helyi /Users/<te>/ útvonal, privát repóid nevei (gh repo list --visibility private), .env fájlok, és amit /pushor figyel <szöveg>-gel felvettél.
  • Találatnál felugró, maszkolt listával — Megtiltom / Pushold így is / Pushold, jegyezd meg (az utóbbi kivételként megjegyzi a találatokat). Tiltáskor a modell megkapja a teljes listát a javításhoz.
  • Privát repóba menő pushnál nem csinál semmit. Kell hozzá a gh CLI (bejelentkezve). /pushor be|ki.

decision-popup

Döntési felugró: a döntések egygombos kérdésként jönnek.

  • A rendszerpromptban arra kéri Claude-ot, hogy minden döntést (engedélyezés, A vagy B, visszafordíthatatlan vagy kifelé ható lépés) az AskUserQuestion dialógussal kérdezzen meg, 2–4 rövid opcióval, a javasoltat vagy a biztonságosabbat elöl — egy számgombbal válaszolsz.
  • A dialógus keretet és címet kap („Döntés kell”); az őr-modok (supabase-guard, public-repo-guard) kérdése piros keretben, „Az őr megállított egy műveletet” címmel.
  • /dontes be|ki, vagy az eszköztárban.

Ötletek

project-pane bővítése:

  • Vercel deploy-ok (vercel ls)
  • remote Supabase migrációk összevetése a lokálissal
  • több repó egy panelen

Státuszsor bővítése (usage-band):

  • figyelmeztetés kontextusablakra (pl. 85%), opcionálisan automatikus /compact
  • session költsége ($.session.usage().cost)
  • fast mód jelzése, git branch, aktuális projekt
  • beállítható küszöb és megjelenő elemek (userConfig)

Új mod-ok:

  • env-guard: .env, kulcsfájlok, supabase/.temp szerkesztésének tiltása (tool.call → deny)
  • turn-timer: hosszú turn végén hang/toast, hogy vissza lehet nézni (turn.complete, $.audio.play)
  • quote: /quote — a kijelölt szöveget idézetként a promptba teszi ($.ui.selection)
  • hu-prompt: a rendszerpromptba magyar válasz- és stílusszabály (prompt.compose)

Fejlesztés

Egy plugin mappája közvetlenül is betölthető:

claude --plugin-dir ./plugins/usage-band

Ellenőrzés:

claude plugin validate plugins/usage-band
claude plugin test plugins/usage-band
Source 2 files
hooks/register.tsx 288 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionContextBreakdown } from 'claude-code'
3
4import type { Breakdown, Limit, Segment, Task } from '../types'
5
6const breakdown = atom({ plugin: 'usage-header', key: 'breakdown' } as const, null)
7const limits = atom({ plugin: 'usage-header', key: 'limits' } as const, [])
8// Kikapcsolva a sáv üres; az eszköztár (tool-hub) is ezt olvassa.
9const isOn = atom({ plugin: 'usage-header', key: 'isOn' } as const, true)
10
11// A hivatkozásnak szó szerint kell a forrásban állnia (így listázható, mit olvas a mod).
12const FOCUS_ON = { plugin: 'focus-mode', key: 'isOn' } as const
13const FOCUS_TASKS = { plugin: 'focus-mode', key: 'tasks' } as const
14
15const WARN_AT = 90
16const CAUTION_AT = 70
17const LABEL = 11
18
19type Lang = 'hu' | 'en'
20
21const STRINGS = {
22  hu: {
23    context: 'Kontextus',
24    limits: 'Limitek',
25    windows: { five_hour: '5 óra', seven_day: 'Hét' } as Record<string, string>,
26    days: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo'],
27    noData: 'még nincs adat (az első válasz után jelenik meg)',
28    command: 'Használati sáv a prompt fölött be/ki',
29    on: 'Használati sáv bekapcsolva.',
30    off: 'Használati sáv kikapcsolva.',
31    usage: 'Használat: /hasznalat [be|ki]',
32    hint: '[be|ki]',
33    names: {
34      'System prompt': 'Rendszerprompt',
35      'System tools': 'Eszközök',
36      'MCP tools': 'MCP eszközök',
37      'Custom agents': 'Ágensek',
38      'Memory files': 'Memória',
39      Skills: 'Skillek',
40      'Slash commands': 'Parancsok',
41      Messages: 'Üzenetek',
42      'Free space': 'Szabad',
43      'Autocompact buffer': 'Tömörítési tartalék',
44    } as Record<string, string>,
45  },
46  en: {
47    context: 'Context',
48    limits: 'Limits',
49    windows: { five_hour: '5h', seven_day: 'Week' } as Record<string, string>,
50    days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
51    noData: 'no data yet (shows after the first response)',
52    command: 'Usage band above the prompt on/off',
53    on: 'Usage band on.',
54    off: 'Usage band off.',
55    usage: 'Usage: /hasznalat [on|off]',
56    hint: '[on|off]',
57    names: {} as Record<string, string>,
58  },
59}
60
61// A modul minden beállításváltáskor újratöltődik, így elég egyszer, induláskor eldönteni.
62let lang: Lang = 'en'
63const t = () => STRINGS[lang]
64
65async function pickLanguage($: EngineInterface, setting: unknown): Promise<Lang> {
66  if (setting === 'hu' || setting === 'en') return setting
67  const locale = (await $.env.get('LC_ALL')) || (await $.env.get('LANG')) || ''
68  return locale.toLowerCase().startsWith('hu') ? 'hu' : 'en'
69}
70
71export function tokens(n: number): string {
72  if (n >= 1_000_000) return `${Number((n / 1_000_000).toFixed(1))}M`
73  if (n >= 10_000) return `${Math.round(n / 1000)}k`
74  return n >= 1000 ? `${Number((n / 1000).toFixed(1))}k` : String(n)
75}
76
77// A halasztott (igény szerint betöltött) eszközsémák nincsenek az ablakban: kimaradnak.
78export function toBreakdown(b: SessionContextBreakdown): Breakdown {
79  const segments: Segment[] = []
80  for (const c of b.categories) {
81    if (c.kind === 'deferred' || c.tokens <= 0) continue
82    segments.push({ name: c.name, tokens: c.tokens, color: c.color, kind: c.kind })
83  }
84  return { segments, total: b.totalTokens, max: b.rawMaxTokens, percent: b.percentage }
85}
86
87// Cellák kategóriánként, legnagyobb maradék szerint kerekítve; ami foglal, legalább egy cellát kap
88// (a szabad helyből), hogy minden szín látsszon.
89export function cells(segments: Segment[], max: number, width: number): number[] {
90  const sum = Math.max(max, segments.reduce((a, s) => a + s.tokens, 0), 1)
91  const exact = segments.map(s => (s.tokens / sum) * width)
92  const out = exact.map(Math.floor)
93  let left = width - out.reduce((a, n) => a + n, 0)
94  const order = exact.map((x, i) => [x - Math.floor(x), i] as const).sort((a, b) => b[0] - a[0])
95  for (const [, i] of order) {
96    if (left <= 0) break
97    out[i]! += 1
98    left -= 1
99  }
100  const free = segments.findIndex(s => s.kind === 'free')
101  for (const [i, s] of segments.entries()) {
102    if (s.kind !== 'used' || out[i]! > 0) continue
103    const donor = free >= 0 && out[free]! > 1 ? free : out.indexOf(Math.max(...out))
104    if (out[donor]! <= 1) break
105    out[donor]! -= 1
106    out[i] = 1
107  }
108  return out
109}
110
111const glyph = (kind: Segment['kind']) => (kind === 'used' ? '█' : kind === 'buffer' ? '▒' : '░')
112
113const levelColor = (percent: number) => (percent >= WARN_AT ? 'red' : percent >= CAUTION_AT ? 'yellow' : 'green')
114
115function resetText(kind: string, resetsAt?: string): string {
116  if (resetsAt === undefined) return ''
117  const at = new Date(resetsAt)
118  const hhmm = `${String(at.getHours()).padStart(2, '0')}:${String(at.getMinutes()).padStart(2, '0')}`
119  return kind === 'five_hour' ? ` → ${hhmm}` : ` → ${t().days[at.getDay()]} ${hhmm}`
120}
121
122// A fókusz mód feliratai és elrendezése (focus-mode/hooks/register.tsx): ebből jön a lista legszélesebb sora.
123const FOCUS_TEXT = {
124  hu: { title: 'Fókusz mód', done: 'kész', waiting: 'a részfeladatok hamarosan megjelennek…', idle: 'bekapcsolva · /fokusz ki a kikapcsoláshoz' },
125  en: { title: 'Focus mode', done: 'done', waiting: 'subtasks will show up shortly…', idle: 'on · /fokusz off to turn it off' },
126}
127
128// A rajzjelek (vonal, sáv, ikon) egy cellát foglalnak. Az asztali appban a betűk arányos betűtípussal,
129// keskenyebben jelennek meg, így ott egy betű csak ~0,55 vonaldarabnyi helyet foglal.
130const GRAPHIC = /[─█░▒▓◆▸○✓■]/
131export function measure(text: string, narrow: boolean): number {
132  if (!narrow) return text.length
133  let units = 0
134  for (const ch of text) units += GRAPHIC.test(ch) ? 1 : 0.55
135  return Math.ceil(units)
136}
137
138export function focusWidth(list: readonly Task[], columns: number, isWorking: boolean, l: Lang, narrow = false): number {
139  const f = FOCUS_TEXT[l]
140  if (list.length === 0) return measure(`◆ ${f.title} ${isWorking ? f.waiting : f.idle}`, narrow)
141  const done = list.filter(task => task.status === 'done').length
142  const total = Math.round(list.reduce((sum, task) => sum + task.progress, 0) / list.length)
143  const header = `◆ ${f.title}  ${done}/${list.length} ${f.done}  ${'█'.repeat(12)} ${total}%`
144  const titleRoom = Math.max(10, columns - 24)
145  const rows = list.map(task => {
146    const name = task.title.slice(0, titleRoom)
147    return task.status === 'in_progress' ? `  ▸ ${name} ${'█'.repeat(8)} ${task.progress}%` : `  ✓ ${name}`
148  })
149  return Math.max(...[header, ...rows].map(row => measure(row, narrow)))
150}
151
152async function refresh($: EngineInterface): Promise<void> {
153  const usage = await $.session.usage({ breakdown: 'summary' })
154  const b = usage.context.breakdown
155  await update($, breakdown, prev => (b === undefined ? prev : toBreakdown(b)))
156  await update($, limits, () => usage.rateLimits)
157}
158
159async function setOn($: EngineInterface, wanted: boolean): Promise<void> {
160  await update($, isOn, () => wanted)
161  await $.store.set('isOn', wanted)
162}
163
164export const register: Register = (on, options) => {
165  if (options.language === 'hu' || options.language === 'en') lang = options.language
166
167  on('session.start', async ($, e, next) => {
168    const result = await next(e)
169    lang = await pickLanguage($, options.language)
170    const saved = (await $.store.get('isOn')) !== false
171    await update($, isOn, () => saved)
172    await $.command.register({ name: 'hasznalat', description: t().command, argumentHint: t().hint, immediate: true })
173    await refresh($)
174    return result
175  })
176
177  on('command.run', { command: 'hasznalat' }, async ($, e) => {
178    const arg = e.args.trim().toLowerCase()
179    const wanted = ['be', 'on'].includes(arg) ? true : ['ki', 'off'].includes(arg) ? false : arg === '' ? !(await read($, isOn)) : null
180    if (wanted === null) return { text: t().usage }
181    await setOn($, wanted)
182    if (wanted) await refresh($)
183    return { text: wanted ? t().on : t().off }
184  })
185
186  // Az eszköztár gombja parancs nélkül kapcsol (így nem marad parancssor a transcriptben).
187  on('state.set', { plugin: 'tool-hub', key: 'request' }, async ($, e, next) => {
188    const done = await next(e)
189    if (e.value?.tool === 'header') await setOn($, e.value.on).catch(() => undefined)
190    return done
191  })
192
193  // Turn végén és limitmozduláskor: a bontás a helyi becslés ('summary'), API-hívás nélkül.
194  on('session.measure', async ($, e, next) => {
195    const result = await next(e)
196    const moved = e.changed.includes('context') || e.changed.includes('rateLimits')
197    if (moved && (await read($, isOn))) await refresh($)
198    return result
199  })
200
201  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
202    if (e.props.hasSurvey || !(await read($, isOn))) return next(e)
203    const below = await next(e)
204    const { Box, Text } = $.ui.resolve(e)
205    const b = await read($, breakdown)
206    const shown = (await read($, limits)).filter(l => l.kind in t().windows)
207    const columns = e.props.bodyColumns || e.viewport?.columns || 80
208    const width = Math.max(10, Math.min(60, columns - LABEL - 20))
209
210    const label = (text: string) => <Text bold>{text.padEnd(LABEL)}</Text>
211
212    let contextRows
213    if (b === null || b.segments.length === 0) {
214      contextRows = (
215        <Box flexDirection="row">
216          {label(t().context)}
217          <Text dimColor>{t().noData}</Text>
218        </Box>
219      )
220    } else {
221      const counts = cells(b.segments, b.max, width)
222      const pct = Math.round(b.percent)
223      contextRows = (
224        <Box flexDirection="column">
225          <Box flexDirection="row">
226            {label(t().context)}
227            {b.segments.map((s, i) =>
228              counts[i]! > 0 ? (
229                <Text key={`bar-${i}`} color={s.color} dimColor={s.kind !== 'used'}>
230                  {glyph(s.kind).repeat(counts[i]!)}
231                </Text>
232              ) : null,
233            )}
234            <Text color={levelColor(pct)} bold>{` ${pct}%`}</Text>
235            <Text dimColor>{` ${tokens(b.total)}/${tokens(b.max)}`}</Text>
236          </Box>
237          <Box flexDirection="row" flexWrap="wrap" columnGap={2} paddingLeft={LABEL}>
238            {b.segments.map((s, i) => (
239              <Box key={`legend-${i}`} flexDirection="row">
240                <Text color={s.color} dimColor={s.kind !== 'used'}>{s.kind === 'used' ? '■ ' : `${glyph(s.kind)} `}</Text>
241                <Text dimColor={s.kind !== 'used'}>{`${t().names[s.name] ?? s.name} ${tokens(s.tokens)}`}</Text>
242              </Box>
243            ))}
244          </Box>
245        </Box>
246      )
247    }
248
249    const limitWidth = Math.max(6, Math.min(20, Math.floor((columns - LABEL) / 2) - 26))
250    const limitRow =
251      shown.length === 0 ? null : (
252        <Box flexDirection="row" flexWrap="wrap" columnGap={3}>
253          {label(t().limits)}
254          {shown.map(l => {
255            const filled = Math.min(limitWidth, Math.round((l.percentUsed / 100) * limitWidth))
256            const color = levelColor(l.percentUsed)
257            return (
258              <Box key={`limit-${l.kind}`} flexDirection="row">
259                <Text>{`${t().windows[l.kind]} `}</Text>
260                <Text color={color}>{'█'.repeat(filled)}</Text>
261                <Text dimColor>{'░'.repeat(limitWidth - filled)}</Text>
262                <Text color={color} bold>{` ${Math.round(l.percentUsed)}%${l.percentUsed >= WARN_AT ? ' ⚠' : ''}`}</Text>
263                <Text dimColor>{resetText(l.kind, l.resetsAt)}</Text>
264              </Box>
265            )
266          })}
267        </Box>
268      )
269
270    // Ha a fókusz mód lista van fölötte, a vonal annak legszélesebb soráig tart, különben a kontextussorig.
271    const focusOn = (await $.state.get(FOCUS_ON)).value === true
272    const focusList = focusOn ? ((await $.state.get(FOCUS_TASKS)).value ?? []) : []
273    const narrow = e.surface !== 'terminal'
274    const ownWidth = b === null || b.segments.length === 0 ? measure(' '.repeat(LABEL) + t().noData, narrow) : LABEL + width + 16
275    const divider = Math.max(10, Math.min(columns - 1, focusOn ? focusWidth(focusList, columns, e.props.isWorking, lang, narrow) : ownWidth))
276
277    // Vonal a sáv tetején: elválasztja a fölötte lévő modoktól (fókusz mód listája, eszköztár).
278    return (
279      <Box flexDirection="column">
280        {below}
281        <Text key="divider" dimColor>{'─'.repeat(divider)}</Text>
282        {contextRows}
283        {limitRow}
284      </Box>
285    )
286  })
287}
288
types/index.d.ts 18 lines
1// Egy kategória a kontextusablakból, ahogy a /context listázza; a szín a téma kulcsa.
2export type Segment = { name: string; tokens: number; color: string; kind: 'used' | 'free' | 'buffer' }
3export type Breakdown = { segments: Segment[]; total: number; max: number; percent: number }
4// A fókusz mód listája: a sáv tetején lévő vonal csak a legszélesebb soráig tart.
5export type Task = { title: string; status: 'pending' | 'in_progress' | 'done'; progress: number }
6export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
7
8// Az eszköztár (tool-hub) kérése: a kapcsoló gomb ezt írja, a mod a state.set hookjában veszi át.
9export type HubRequest = { tool: string; on: boolean; n: number }
10
11declare module 'claude-code' {
12  interface PluginState {
13    'tool-hub': { request: HubRequest | null }
14    'focus-mode': { isOn: boolean; tasks: Task[] }
15    'usage-header': { breakdown: Breakdown | null; limits: Limit[]; isOn: boolean }
16  }
17}
18