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

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 | כמה הסשן עלה עד עכשיו, בדולרים. |
צבעים והתראות:
שני השעונים, ומה ההבדל ביניהם:
14:00. נספר רק הזמן ש-Claude עבד בפועל, בלי הזמן שבו הסשן חיכה לכם.💡 המלצה לדחוס את השיחה: שיחה ארוכה מדי פוגעת באיכות התשובות, גם כשעוד נשאר מקום בחלון. לכן ה-mod ממליץ לדחוס הרבה לפני שהחלון מתמלא:
/compact בתיבת הכתיבה.הספים מבוססים על כלל אצבע מקובל, שלפיו האיכות מתחילה לרדת בסביבות 300K עד 400K טוקנים. זה תלוי במשימה, אז זו המלצה ולא חוק.
הפריסה מתאימה את עצמה לרוחב החלון:
| מצב | מה קורה |
|---|---|
| מנוי Pro / Max | הכול עובד. |
| מפתח API במקום מנוי | אין מכסת 5 שעות, אז הכרטיס ⚡ נשאר על "after the first reply". שאר הכרטיסים עובדים. |
| 💵 עלות במנוי | זו העלות המשוערת לפי מחירי ה-API, לא כסף שיורד מהכרטיס. |
תוסף VS Code, claude -p, סשן בענן | ה-mod רץ, אבל לא מוצג. התצוגה קיימת רק בטרמינל ובאפליקציית הדסקטופ. |
/plugin, לשונית Marketplaces, בוחרים claude-code-hud, ואז Enable auto-update./plugin, לשונית Installed, בוחרים session-hud, ואז Update now./plugin uninstall session-hud@claude-code-hud./reload-plugins.claude --version בטרמינל. צריך 2.1.287 ומעלה, ואם הגרסה ישנה יותר, מעדכנים./plugin מציג מתחת ללשוניות שורה כמו 1 mod active · session-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
Session Tracker is a Claude Code mod that sits above the prompt box as four side-by-side cards:
/compact (yellow, plus a one-time toast), and past 400K it turns red.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:
/plugin, go to Marketplaces, select claude-code-hud, then Enable auto-update./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.
hooks/register.tsx 224 lines1import { 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}
224types/index.d.ts 20 lines1export 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