SLOPSHOPPER

session-hud

Session Tracker above the prompt box: context window with a /compact recommendation, 5-hour plan limit, prompt clocks and session cost.

newbandguardtoastprompttimer
★ 5v2.3.3MITupdated 2026-10-04ofeklevy11/claude-code-hud/session-hud
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · session-hud
› 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 ⚡ Session Tracker 🧠 Context window ⚡ 5-hour limit ███████████████████░░░░░░░░░░░░░░░░░░░░ 49% ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 31% 103K left of 200K ↻ resets now ⏱ 0:00 last · ⌛ 0:00 all · 💵 $0.42 ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⚡ Session Tracker 🧠 Context window ⚡ 5-hour limit ███████████████████░░░░░░░░░░░░░░░░░░░░ 49% ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 31% 103K left of 200K ↻ resets now ⏱ 0:00 last · ⌛ 0:00 all · 💵 $0.42 ⟨Claude Code's own drawing⟩
README

claude-code-hud

mod ל-Claude Code שמציג מעל תיבת הכתיבה את מה שבאמת חשוב לדעת תוך כדי עבודה: כמה מקום נשאר בחלון הקונטקסט, כמה נשאר ממכסת 5 השעות ומתי היא מתאפסת, כמה זמן לקח הפרומפט האחרון וכמה זמן עבדו כל הפרומפטים בסשן ביחד, וכמה עלה הסשן.

<img src="assets/preview.png" alt="Session Tracker מעל תיבת הכתיבה" width="460">

עובד באפליקציית הדסקטופ (לשונית Code) ובטרמינל, ב-Windows, ב-Mac ובלינוקס.

התקנה: שורה אחת

בתוך Claude Code (באפליקציה או בטרמינל), מדביקים בתיבת הכתיבה ושולחים:

/plugin install session-hud --marketplace ofeklevy11/claude-code-hud

מאשרים את ההתקנה, ופותחים סשן חדש. זהו.

בתוך Claude Code, אחת אחרי השנייה:

/plugin marketplace add ofeklevy11/claude-code-hud
/plugin install session-hud@claude-code-hud

או מהטרמינל, בלי לפתוח את Claude Code:

claude plugin marketplace add ofeklevy11/claude-code-hud && claude plugin install session-hud@claude-code-hud

אפשר גם פשוט לבקש מ-Claude: "תתקין לי את הפלאגין session-hud מה-marketplace ofeklevy11/claude-code-hud".

צריך: Claude Code בגרסה 2.1.287 ומעלה. מהגרסה הזו mods פועלים כברירת מחדל, בלי שום הגדרה.

מה מקבלים

ארבעה כרטיסים זה לצד זה:

כרטיסמה הוא מציג
🧠 Context windowפס של מילוי חלון הקונטקסט באחוזים וכמה טוקנים נשארו. כשהשיחה ארוכה מדי, הכרטיס אומר לכם להקליד /compact.
⚡ 5-hour limitפס של מכסת 5 השעות באחוזים, וכמה זמן נשאר עד שהיא מתאפסת.
⏱ Prompt timeשני שעונים: now / last הוא הזמן של פרומפט אחד, ו-all הוא סכום הזמן של כל הפרומפטים בסשן.
💵 Session costכמה הסשן עלה עד עכשיו, בדולרים.

צבעים והתראות:

  • הפסים ירוקים עד 60%, צהובים עד 85%, ואדומים מעל זה.
  • כשפרומפט ארוך (2 דקות ומעלה) נגמר, קופצת הודעה.

שני השעונים, ומה ההבדל ביניהם:

  • now / last: הזמן של פרומפט אחד, מהשליחה ועד ש-Claude סיים. בזמן שהוא עובד השעון רץ בתכלת ("now"). כשהוא מסיים, נשאר הזמן של הפרומפט האחרון ("last").
  • all: סכום הזמנים של כל הפרומפטים בסשן. אם היו פרומפטים של 3, 7 ו-4 דקות, יופיע 14:00. נספר רק הזמן ש-Claude עבד בפועל, בלי הזמן שבו הסשן חיכה לכם.

💡 המלצה לדחוס את השיחה: שיחה ארוכה מדי פוגעת באיכות התשובות, גם כשעוד נשאר מקום בחלון. לכן ה-mod ממליץ לדחוס הרבה לפני שהחלון מתמלא:

  • מעל 300K טוקנים: מופיעות בכרטיס שתי שורות צהובות: "Compact recommended" ו-"👉 Type /compact now". קופצת גם הודעה, פעם אחת בכל פעם שעוברים את הסף. הדחיסה עצמה: מקלידים /compact בתיבת הכתיבה.
  • מעל 400K טוקנים: השורות הופכות לאדומות.
  • בחלון קטן מ-1M: הספים הם 50% ו-70% מגודל החלון.

הספים מבוססים על כלל אצבע מקובל, שלפיו האיכות מתחילה לרדת בסביבות 300K עד 400K טוקנים. זה תלוי במשימה, אז זו המלצה ולא חוק.

הפריסה מתאימה את עצמה לרוחב החלון:

  • חלון רחב: ארבעת הכרטיסים בשורה אחת.
  • חלון בינוני ו-split view: שני הפסים זה לצד זה, ומתחתם שורה אחת עם השעונים והעלות.
  • חלון צר מאוד: שורה קצרה לכל פס, מתחתן שורת השעונים והעלות, ובסוף, כשצריך, שורת "type /compact". זו הפריסה שבצילום למעלה.

למי זה עובד

מצבמה קורה
מנוי Pro / Maxהכול עובד.
מפתח API במקום מנויאין מכסת 5 שעות, אז הכרטיס ⚡ נשאר על "after the first reply". שאר הכרטיסים עובדים.
💵 עלות במנויזו העלות המשוערת לפי מחירי ה-API, לא כסף שיורד מהכרטיס.
תוסף VS Code, claude -p, סשן בענןה-mod רץ, אבל לא מוצג. התצוגה קיימת רק בטרמינל ובאפליקציית הדסקטופ.

עדכון והסרה

  • עדכון אוטומטי: כבוי כברירת מחדל ב-marketplace שאינו של Anthropic. כדי להפעיל אותו: /plugin, לשונית Marketplaces, בוחרים claude-code-hud, ואז Enable auto-update.
  • עדכון ידני: /plugin, לשונית Installed, בוחרים session-hud, ואז Update now.
  • הסרה: /plugin uninstall session-hud@claude-code-hud.

לא מופיע?

  1. פתחתם סשן חדש אחרי ההתקנה? סשן שכבר היה פתוח לא טוען את ה-mod. אפשר גם להריץ בו /reload-plugins.
  2. גרסת Claude Code: מריצים claude --version בטרמינל. צריך 2.1.287 ומעלה, ואם הגרסה ישנה יותר, מעדכנים.
  3. בדיקה שהוא נטען: בטרמינל, /plugin מציג מתחת ללשוניות שורה כמו 1 mod active · session-hud.
  4. ה-HUD מופיע פעמיים? כנראה התקנתם בעבר גרסה ידנית. מחקו את ~/.claude/skills/session-hud ואת ~/.claude/skills/context-meter, אם הם קיימים.

התאמה אישית

הקוד נמצא ב-session-hud/hooks/register.tsx:

  • ספי הצבעים: הפונקציה tone.
  • ספי ההמלצה לדחוס: הפונקציות compactAt ו-compactHard בראש הקובץ.
  • נקודות המעבר בין הפריסות: הבדיקות W >= 96 ו-W >= 40 בסוף הקובץ.
  • להציג גם את המכסה השבועית: הנתונים של seven_day כבר נשלפים, ונשאר רק להוסיף להם כרטיס.
  • מלכודת אחת: לא לקרוא למשתנה בשם h. רכיבי ה-JSX נבנים דרך פונקציה בשם הזה, ומשתנה באותו שם מסתיר אותה. ה-mod קורס עם h is not a function.

לפתח בלי להתקין: claude --plugin-dir ./session-hud טוען את התיקייה לסשן אחד. אחרי כל שינוי מריצים claude plugin validate ./session-hud.

פרטיות

ה-mod קורא רק נתונים שהסשן עצמו כבר מחזיק: מילוי הקונטקסט, מכסות התוכנית והעלות. הוא לא שולח שום דבר לשום מקום ולא כותב קבצים.

כמו כל mod, הוא רץ עם ההרשאות שלכם. אפשר לבדוק בדיוק מה הוא עושה לפני ההתקנה: claude plugin validate ./session-hud מציג את כל האירועים שהוא מאזין להם ואת כל הקריאות שהוא עושה.

רישיון

MIT


English

Session Tracker is a Claude Code mod that sits above the prompt box as four side-by-side cards:

  • 🧠 Context window: a fill bar and tokens left. Past 300K tokens it tells you to type /compact (yellow, plus a one-time toast), and past 400K it turns red.
  • ⚡ 5-hour limit: a fill bar and the time until the limit resets.
  • ⏱ Prompt time: two clocks. now/last is one prompt end to end. all is every prompt in the session added up, with idle time excluded.
  • 💵 Session cost: in USD.

Install: paste this into Claude Code (desktop app or terminal), then open a new session:

/plugin install session-hud --marketplace ofeklevy11/claude-code-hud

Requires Claude Code 2.1.287 or later, where mods are on by default. If the one-line install doesn't work, run /plugin marketplace add ofeklevy11/claude-code-hud first, then /plugin install session-hud@claude-code-hud.

Update and uninstall:

  • Auto-update is off for third-party marketplaces. To turn it on: open /plugin, go to Marketplaces, select claude-code-hud, then Enable auto-update.
  • To uninstall: /plugin uninstall session-hud@claude-code-hud.

Showing twice? Delete any old manual copy in ~/.claude/skills/session-hud or ~/.claude/skills/context-meter.

Privacy: the mod only reads session data. It writes no files and sends nothing anywhere. MIT licensed.

Source 2 files
hooks/register.tsx 224 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Hud, Limit } from '../types'
5
6const EMPTY: Hud = { limits: [], costUsd: null, isRunning: false, turnSeconds: 0, totalSeconds: 0, turnStartedAt: 0, ctxTokens: null, ctxWindow: 0, ctxPercent: null, nudged: false }
7const hud = atom({ plugin: 'session-hud', key: 'hud' } as const, EMPTY)
8
9const tone = (p: number): string => (p >= 85 ? 'red' : p >= 60 ? 'yellow' : 'green')
10const clock = (s: number): string => {
11  const hrs = Math.floor(s / 3600)
12  const min = Math.floor((s % 3600) / 60)
13  const sec = String(s % 60).padStart(2, '0')
14  return hrs > 0 ? `${hrs}:${String(min).padStart(2, '0')}:${sec}` : `${min}:${sec}`
15}
16const short = (n: number): string =>
17  n >= 1_000_000 ? `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M` : n >= 1_000 ? `${Math.round(n / 1_000)}K` : String(n)
18// When to recommend /compact (rule of thumb: quality drops around 300-400K tokens, task dependent)
19// soft: suggest at 300K (or 50% of a smaller window); hard: urge at 400K (or 70%)
20const compactAt = (win: number): number => Math.min(300_000, Math.round(win * 0.5))
21const compactHard = (win: number): number => Math.min(400_000, Math.round(win * 0.7))
22const usd = (n: number | null): string => (n === null ? '—' : `$${n.toFixed(2)}`)
23const inTime = (iso: string | null, now: number): string => {
24  if (!iso) return ''
25  const ms = Date.parse(iso) - now
26  if (!(ms > 0)) return 'resets now'
27  const m = Math.round(ms / 60000)
28  const d = Math.floor(m / 1440)
29  const hr = Math.floor((m % 1440) / 60)
30  return d > 0 ? `resets in ${d}d ${hr}h` : hr > 0 ? `resets in ${hr}h ${m % 60}m` : `resets in ${m}m`
31}
32
33async function pull($: any): Promise<void> {
34  const u = await $.session.usage()
35  const limits: Limit[] = (u.rateLimits ?? []).map((r: any) => ({ kind: r.kind, percent: r.percentUsed, resetsAt: r.resetsAt ?? null }))
36  const c = u.context ?? {}
37  await update($, hud, st => ({
38    ...st, limits, costUsd: u.cost?.usd ?? null,
39    ctxTokens: c.tokens ?? null, ctxWindow: c.window ?? st.ctxWindow, ctxPercent: c.percent ?? null,
40  }))
41  // One nudge per crossing; it re-arms after a compaction brings the window back under
42  const st = await read($, hud)
43  const isOver = st.ctxTokens !== null && st.ctxWindow > 0 && st.ctxTokens >= compactAt(st.ctxWindow)
44  if (isOver && !st.nudged) {
45    $.ui.toast(`💡 Context passed ${short(compactAt(st.ctxWindow))} tokens: Compact recommended, type /compact`)
46    await update($, hud, x => ({ ...x, nudged: true }))
47  } else if (!isOver && st.nudged) {
48    await update($, hud, x => ({ ...x, nudged: false }))
49  }
50}
51
52// Seconds since the prompt started. The start time lives in state, not in a module variable: a reload
53// mid-prompt would reset that to 0 and count the time since 1970 as the prompt's length
54const MAX_TURN = 24 * 3600
55// A session total from before 2.3.1 may hold the time since 1970; anything past 30 days is that, not work
56const cleanTotal = (n: number | undefined): number => (n !== undefined && n >= 0 && n <= 30 * 24 * 3600 ? n : 0)
57async function elapsed($: any, startedAt: number): Promise<number> {
58  if (!(startedAt > 0)) return 0
59  const s = Math.round(((await $.clock.now()) - startedAt) / 1000)
60  return s >= 0 && s <= MAX_TURN ? s : 0
61}
62
63export const register: Register = on => {
64  let tick: { cancel: () => void } | null = null
65
66  on('session.start', async ($, e, next) => {
67    const r = await next(e)
68    await pull($)
69    return r
70  })
71
72  on('prompt.submit', async ($, e, next) => {
73    const now = await $.clock.now()
74    await update($, hud, st => ({
75      ...st, isRunning: true, turnSeconds: 0, turnStartedAt: now,
76      totalSeconds: cleanTotal(st.totalSeconds),
77    }))
78    tick?.cancel()
79    tick = $.clock.every(1000, async () => {
80      const s = await elapsed($, (await read($, hud)).turnStartedAt)
81      await update($, hud, st => ({ ...st, turnSeconds: s }))
82    })
83    return next(e)
84  })
85
86  on('turn.complete', async ($, e, next) => {
87    const r = await next(e)
88    tick?.cancel()
89    tick = null
90    const s = await elapsed($, (await read($, hud)).turnStartedAt)
91    // Every finished prompt adds its time to the session total
92    await update($, hud, st => ({ ...st, isRunning: false, turnSeconds: s, turnStartedAt: 0, totalSeconds: cleanTotal(st.totalSeconds) + s }))
93    await pull($)
94    if (s >= 120) $.ui.toast(`✅ Turn finished in ${clock(s)}`)
95    return r
96  })
97
98  // Keep the context window live during a turn
99  on('tool.call', async ($, e, next) => {
100    const r = await next(e)
101    await pull($)
102    return r
103  })
104
105  on('session.compact', async ($, e, next) => {
106    const r = await next(e)
107    await pull($)
108    return r
109  })
110
111  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
112    const below = await next(e)
113    if (e.props.hasSurvey) return below
114
115    const st = await read($, hud)
116    const now = await $.clock.now()
117    const { Box, Text } = $.ui.resolve(e)
118    const W = Math.max(20, (e.props.bodyColumns ?? 80) - 6)
119
120    // Section header: the mod's name
121    const header = <Text bold color="blueBright" wrap="truncate">{'⚡ Session Tracker'}</Text>
122    const five = st.limits.find(l => l.kind === 'five_hour')
123
124    const ctxLeft = st.ctxTokens === null ? st.ctxWindow : Math.max(0, st.ctxWindow - st.ctxTokens)
125    // Two clocks: the prompt running now (or the last one), and every prompt of the session added up
126    const total = cleanTotal(st.totalSeconds) + (st.isRunning ? st.turnSeconds : 0)
127    // Labels in blue, values in their own color
128    const label = (t: string) => <Text bold color="blueBright">{t}</Text>
129
130    // Side-by-side cards: each metric is a small titled column, and the cards wrap with the width
131    const bar = (cells: number, pct: number | null) => {
132      const p = pct ?? 0
133      const filled = Math.max(pct === null ? 0 : 1, Math.min(cells, Math.round((p / 100) * cells)))
134      return [
135        <Text color={tone(p)}>{'█'.repeat(filled)}</Text>,
136        <Text dimColor>{'░'.repeat(Math.max(0, cells - filled))}</Text>,
137        <Text bold color={tone(p)}>{pct === null ? ' —' : ` ${Math.round(p)}%`}</Text>,
138      ]
139    }
140    const reset5 = five ? inTime(five.resetsAt, now) : ''
141    const card = (width: number, ...rowsOfSpans: any[][]) => (
142      <Box flexDirection="column" width={width}>
143        {rowsOfSpans.map(spans => <Text wrap="truncate">{spans}</Text>)}
144      </Box>
145    )
146    const isOver = st.ctxTokens !== null && st.ctxWindow > 0 && st.ctxTokens >= compactAt(st.ctxWindow)
147    const isHard = isOver && st.ctxTokens !== null && st.ctxTokens >= compactHard(st.ctxWindow)
148    // Mods cannot press /compact for you here, so the card tells you what to type
149    const compactHint = isOver
150      ? <Text bold color={isHard ? 'red' : 'yellow'}>{'👉 Type /compact now'}</Text>
151      : null
152    const ctxNote = st.ctxTokens === null
153      ? <Text dimColor>no data yet</Text>
154      : isHard
155        ? <Text bold color="red">{`⚠ ${short(st.ctxTokens)} · Compact recommended 300-400K+ (if 1M context window)`}</Text>
156        : isOver
157        ? <Text color="yellow">{`💡 ${short(st.ctxTokens)} · Compact recommended 300-400K+ (if 1M context window)`}</Text>
158        : <Text dimColor>{`${short(ctxLeft)} left of ${short(st.ctxWindow)}`}</Text>
159    const ctxCard = (w: number) => (
160      <Box flexDirection="column" width={w}>
161        <Text wrap="truncate">{label('🧠 Context window')}</Text>
162        <Text wrap="truncate">{bar(Math.max(4, w - 6), st.ctxPercent)}</Text>
163        <Text wrap="truncate">{ctxNote}</Text>
164        {compactHint ? <Text wrap="truncate">{compactHint}</Text> : null}
165      </Box>
166    )
167    const fiveCard = (w: number) => card(w,
168      [label('⚡ 5-hour limit')],
169      five ? bar(Math.max(4, w - 6), five.percent) : [<Text dimColor>after the first reply</Text>],
170      [<Text dimColor>{reset5 ? `↻ ${reset5.replace('resets in ', '')}` : ''}</Text>],
171    )
172    const timeCard = (w: number) => card(w,
173      [label('⏱ Prompt time')],
174      [<Text dimColor>{st.isRunning ? 'now  ' : 'last  '}</Text>, <Text bold color={st.isRunning ? 'cyan' : undefined}>{clock(st.turnSeconds)}</Text>],
175      [<Text dimColor>{'all   '}</Text>, <Text bold>{clock(total)}</Text>],
176    )
177    const costCard = (w: number) => card(w,
178      [label('💵 Session cost')],
179      [<Text bold color="green">{usd(st.costUsd)}</Text>],
180      [<Text dimColor>{'this session'}</Text>],
181    )
182    // One line for the clocks and the cost, used when only two cards fit across
183    const statsLine = (
184      <Text wrap="truncate">
185        {label('⏱ ')}<Text bold color={st.isRunning ? 'cyan' : undefined}>{clock(st.turnSeconds)}</Text>
186        <Text dimColor>{st.isRunning ? ' now' : ' last'}</Text>
187        <Text dimColor>{'  ·  '}</Text>
188        {label('⌛ ')}<Text bold>{clock(total)}</Text><Text dimColor>{' all'}</Text>
189        <Text dimColor>{'  ·  '}</Text>
190        {label('💵 ')}<Text bold color="green">{usd(st.costUsd)}</Text>
191      </Text>
192    )
193
194    const GAP = 3
195    let rows: any[]
196    if (W >= 96) {
197      // Wide: four cards in one band
198      const w = Math.floor((W - GAP * 3) / 4)
199      rows = [<Box flexDirection="row" columnGap={GAP}>{ctxCard(w)}{fiveCard(w)}{timeCard(w)}{costCard(w)}</Box>]
200    } else if (W >= 40) {
201      // Medium and split view: the two gauges side by side, then one stats line
202      const w = Math.floor((W - GAP) / 2)
203      rows = [<Box flexDirection="row" columnGap={GAP}>{ctxCard(w)}{fiveCard(w)}</Box>, statsLine]
204    } else {
205      // Very narrow: one compact line per gauge, then the stats
206      const cells = Math.max(4, W - 26)
207      rows = [
208        <Text wrap="truncate">{label('🧠 ')}{bar(cells, st.ctxPercent)}<Text dimColor>{st.ctxTokens === null ? '' : ` ${short(ctxLeft)} left`}</Text></Text>,
209        <Text wrap="truncate">{label('⚡ ')}{five ? bar(cells, five.percent) : <Text dimColor>—</Text>}<Text dimColor>{reset5 ? ` ↻ ${reset5.replace('resets in ', '')}` : ''}</Text></Text>,
210        statsLine,
211      ]
212      if (isOver) rows.push(<Text wrap="truncate" bold color={isHard ? 'red' : 'yellow'}>{'💡 Compact recommended · type /compact'}</Text>)
213    }
214
215    return (
216      <Box flexDirection="column">
217        {header}
218        {rows}
219        {below}
220      </Box>
221    )
222  })
223}
224
types/index.d.ts 20 lines
1export type Limit = { kind: string; percent: number; resetsAt: string | null }
2export type Hud = {
3  limits: Limit[]
4  costUsd: number | null
5  isRunning: boolean
6  turnSeconds: number
7  totalSeconds: number
8  turnStartedAt: number
9  ctxTokens: number | null
10  ctxWindow: number
11  ctxPercent: number | null
12  nudged: boolean
13}
14
15declare module 'claude-code' {
16  interface PluginState {
17    'session-hud': { hud: Hud }
18  }
19}
20