Power gauge above the prompt: context tokens left, the 5-hour limit, and a weekly pacing plan that gives you a daily ration so the 7-day limit lasts to the…

Nine local Claude Code mods (UI and hook plugins) with a light Star Wars flavor.
| Mod | What it does |
|---|---|
| brain-dump | Intercepts long, unstructured prompts, asks clarifying questions, rewrites them into a structured prompt. !raw prefix skips it. |
| helper-desk | Progress bar from the to-do list, plus a side panel for sub-agents (/helper-desk). |
| cache-clock | Prompt-cache countdown, Keep Warm button, /keep-warm. |
| handoff | Generates a handoff prompt at 250k tokens, on cold cache, or on /handoff. |
| reflect | Offers to save rules/preferences to memory, CLAUDE.md, or a second brain. |
| chat-inventory | Zero-token "In This Chat" panel (/inventory). |
| switchboard | /mods panel to toggle mods and hide panels. |
| power-cells | Gauges above the prompt for context, the 5-hour limit, and a weekly pacing plan: a daily ration, work-day weighting, over/under-plan warnings. /power plan shows the week. |
| droid-console | Restyles the whole UI as a droid console: personality palettes, transmission headers, droid spinner words, status band. /droid switches personality. |
Status: experimental. They pass claude plugin validate and tsc, but have not been tested at runtime. Expect rough edges.
Add the marketplace once, then install the mods you want (user scope makes them load in every session):
claude plugin marketplace add ikentrock/starwars-claude-mods
claude plugin install brain-dump@starwars-claude-mods --scope user
claude plugin install cache-clock@starwars-claude-mods --scope user
claude plugin install chat-inventory@starwars-claude-mods --scope user
claude plugin install droid-console@starwars-claude-mods --scope user
claude plugin install handoff@starwars-claude-mods --scope user
claude plugin install helper-desk@starwars-claude-mods --scope user
claude plugin install power-cells@starwars-claude-mods --scope user
claude plugin install reflect@starwars-claude-mods --scope user
claude plugin install switchboard@starwars-claude-mods --scope user
Or one at a time from a session prompt:
/plugin install <mod> --marketplace ikentrock/starwars-claude-mods
Update later with claude plugin marketplace update starwars-claude-mods.
hooks/register.tsx 233 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit, SessionContextUsage } from 'claude-code'
3
4import type { PowerCell, WeekWindow } from '../types'
5import { baselineFor, computePace, dayKey, parseWorkdays, planRows } from './pace'
6import type { PaceConfig } from './pace'
7
8const cells = atom({ plugin: 'power-cells', key: 'cells' } as const, [], { shape: 'cells-1' })
9const isHidden = atom({ plugin: 'power-cells', key: 'isHidden' } as const, false, {
10 shape: 'hidden-1',
11})
12
13const week = atom({ plugin: 'power-cells', key: 'week' } as const, null, { shape: 'week-1' })
14
15type DayRecord = { date: string; windowEnd: number; baseline: number; toasts: number[] }
16
17const WARN_AT = [20, 10, 5]
18const RATION_WARN_AT = [80, 100]
19const NAMES: Record<string, string> = {
20 five_hour: 'Shields (5h)',
21 seven_day: 'Reactor (7d)',
22 spend_limit: 'Credits',
23}
24
25const compact = (n: number) =>
26 n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n)
27
28const bar = (percent: number, width: number) => {
29 const full = Math.round((Math.max(0, Math.min(100, percent)) / 100) * width)
30
31 return '█'.repeat(full) + '░'.repeat(width - full)
32}
33
34const colorFor = (percentLeft: number) =>
35 percentLeft > 40 ? 'green' : percentLeft > 15 ? 'yellow' : 'red'
36
37const until = (iso: string | undefined) => {
38 if (!iso) return ''
39 const mins = Math.max(0, Math.round((Date.parse(iso) - Date.now()) / 60000))
40 if (mins >= 1440) return ` · resets ${Math.round(mins / 1440)}d`
41 if (mins >= 60) return ` · resets ${Math.floor(mins / 60)}h${mins % 60}m`
42
43 return ` · resets ${mins}m`
44}
45
46const measure = (context: SessionContextUsage, limits: SessionRateLimit[]): PowerCell[] => {
47 const out: PowerCell[] = []
48 if (context.percent !== undefined && context.tokens !== undefined) {
49 out.push({
50 label: 'Holocron (context)',
51 percentLeft: 100 - context.percent,
52 detail: `${compact(Math.max(0, context.window - context.tokens))} tokens left`,
53 })
54 }
55 for (const limit of limits) {
56 out.push({
57 label: NAMES[limit.kind] ?? limit.kind,
58 percentLeft: Math.max(0, 100 - limit.percentUsed),
59 detail: `${Math.max(0, 100 - limit.percentUsed).toFixed(0)}% left${until(limit.resetsAt)}`,
60 })
61 }
62
63 return out
64}
65
66// Tracks where today's counting starts (kept across sessions) and speaks up at the day's milestones.
67async function trackDay($: EngineInterface, limits: SessionRateLimit[], cfg: PaceConfig) {
68 const limit = limits.find(one => one.kind === 'seven_day')
69 const windowEnd = limit?.resetsAt ? Date.parse(limit.resetsAt) : NaN
70
71 if (!limit || Number.isNaN(windowEnd)) {
72 await update($, week, () => null)
73
74 return
75 }
76
77 const now = Date.now()
78 const today = dayKey(now)
79 const saved = (await $.store.get('day-1')) as DayRecord | undefined
80 const isSameDay =
81 saved !== undefined && saved.date === today && Math.abs(saved.windowEnd - windowEnd) < 3_600_000
82 const record: DayRecord = isSameDay
83 ? saved
84 : { date: today, windowEnd, baseline: baselineFor(now, windowEnd, limit.percentUsed), toasts: [] }
85 const window: WeekWindow = { windowEnd, baseline: record.baseline, percentUsed: limit.percentUsed }
86 const pace = computePace(now, window, cfg)
87 await update($, week, () => window)
88
89 if (!pace) return
90
91 if (!isSameDay) {
92 $.ui.toast(
93 `Rations: ${pace.budget.toFixed(1)}% today (${pace.isWork ? 'work day' : 'off day'}), ${(100 - limit.percentUsed).toFixed(0)}% left for ${pace.daysLeft.toFixed(1)} days.`,
94 )
95 }
96
97 const crossed = RATION_WARN_AT.filter(
98 t => pace.budget > 0 && (pace.used / pace.budget) * 100 >= t && !record.toasts.includes(t),
99 )
100
101 for (const t of crossed) {
102 $.ui.toast(
103 t >= 100
104 ? `Rations: today's ${pace.budget.toFixed(1)}% is spent. Tomorrow's cells are being borrowed.`
105 : `Rations: ${t}% of today's ${pace.budget.toFixed(1)}% used.`,
106 )
107 }
108
109 if (!isSameDay || crossed.length > 0) {
110 await $.store.set('day-1', { ...record, toasts: [...record.toasts, ...crossed] })
111 }
112}
113
114export const register: Register = (on, options) => {
115 const cfg: PaceConfig = {
116 workdays: parseWorkdays(String(options.workdays ?? 'mon,tue,wed,thu,fri')),
117 workWeight: Math.max(0.1, Number(options.workWeight ?? 2) || 2),
118 }
119
120 // Lowest threshold already warned about, per gauge; a recharge re-arms it.
121 const warned = new Map<string, number>()
122
123 on('session.start', async ($, e, next) => {
124 const usage = await $.session.usage()
125 await update($, cells, () => measure(usage.context, usage.rateLimits))
126 await trackDay($, usage.rateLimits, cfg)
127 await $.command.register({
128 name: 'power',
129 description: 'Show or hide the Power Cells gauge; "/power plan" shows the week\'s daily rations',
130 })
131
132 return next(e)
133 })
134
135 on('command.run', { command: 'power' }, async ($, e) => {
136 if (String(e.args ?? '').trim().toLowerCase() === 'plan') {
137 const window = await read($, week)
138 const rows = window ? planRows(Date.now(), window, cfg) : []
139
140 if (rows.length === 0) {
141 return { text: 'No weekly limit reported yet. It appears after the first reply on a subscription account.' }
142 }
143
144 const lines = rows.map(
145 row =>
146 `| ${row.label}${row.isToday ? ' (today)' : ''} | ${row.isWork ? 'work' : 'off'} | ${row.budget.toFixed(1)}% | ${row.isToday ? `${row.used.toFixed(1)}%` : ''} |`,
147 )
148
149 return {
150 text: ['| Day | Type | Ration | Used |', '|---|---|---|---|', ...lines].join('\n'),
151 }
152 }
153
154 const hidden = !(await read($, isHidden))
155 await update($, isHidden, () => hidden)
156
157 return { text: hidden ? 'Power Cells hidden.' : 'Power Cells shown.' }
158 })
159
160 on('session.measure', async ($, e, next) => {
161 const list = measure(e.context, e.rateLimits)
162 await update($, cells, () => list)
163 await trackDay($, e.rateLimits, cfg)
164
165 for (const cell of list) {
166 const last = warned.get(cell.label) ?? 101
167 if (cell.percentLeft > (WARN_AT[0] ?? 20)) {
168 warned.delete(cell.label)
169 continue
170 }
171 const crossed = WARN_AT.filter(t => cell.percentLeft <= t && t < last).sort((a, b) => a - b)[0]
172 if (crossed !== undefined) {
173 warned.set(cell.label, crossed)
174 $.ui.toast(`Power Cells: ${cell.label} at ${cell.percentLeft.toFixed(0)}%. Low on power, we are.`)
175 }
176 }
177
178 return next(e)
179 })
180
181 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
182 const list = await read($, cells)
183 if (e.props.hasSurvey || list.length === 0 || (await read($, isHidden))) return next(e)
184
185 const { Box, Text } = $.ui.resolve(e)
186 const columns = e.props.bodyColumns ?? 80
187 const width = columns >= 100 ? 10 : 6
188 const window = await read($, week)
189 const pace = window ? computePace(Date.now(), window, cfg) : null
190 const ratio = pace && pace.budget > 0 ? pace.used / pace.budget : 0
191 const delta = pace ? pace.weekUsed - pace.expected : 0
192
193 return (
194 <Box flexDirection="column">
195 <Box flexWrap="wrap">
196 {list.map(cell => (
197 <Box key={cell.label} marginRight={2}>
198 <Text dimColor>{columns >= 70 ? cell.label : cell.label.split(' ')[0]} </Text>
199 <Text color={colorFor(cell.percentLeft)}>{bar(cell.percentLeft, width)} </Text>
200 <Text dimColor>{cell.detail}</Text>
201 </Box>
202 ))}
203 </Box>
204 {pace && (
205 <Box flexWrap="wrap">
206 <Box marginRight={2}>
207 <Text dimColor>{columns >= 70 ? 'Ration (today)' : 'Today'} </Text>
208 <Text color={ratio >= 1 ? 'red' : ratio >= 0.85 ? 'yellow' : 'green'}>
209 {bar(ratio * 100, width)}{' '}
210 </Text>
211 <Text dimColor>
212 {pace.used.toFixed(1)} / {pace.budget.toFixed(1)}% · {pace.isWork ? 'work day' : 'off day'}
213 </Text>
214 </Box>
215 <Box marginRight={2}>
216 <Text color={delta > 4 ? 'red' : delta < -4 ? 'cyan' : 'green'}>
217 {delta > 4
218 ? `${delta.toFixed(0)}% over plan, ease off`
219 : delta < -4
220 ? `${(-delta).toFixed(0)}% under plan, room to use more`
221 : 'on plan'}
222 </Text>
223 </Box>
224 <Text dimColor>
225 {pace.daysLeft.toFixed(1)}d left · {pace.perDay.toFixed(1)}%/day avg
226 </Text>
227 </Box>
228 )}
229 </Box>
230 )
231 })
232}
233hooks/pace.ts 130 lines1export type PaceConfig = { workdays: number[]; workWeight: number }
2
3export type Pace = {
4 used: number
5 budget: number
6 isWork: boolean
7 weekUsed: number
8 expected: number
9 daysLeft: number
10 perDay: number
11}
12
13export type PlanRow = { label: string; isWork: boolean; isToday: boolean; budget: number; used: number }
14
15const DAY = 86_400_000
16const NAMES = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
17
18export const parseWorkdays = (text: string): number[] =>
19 text
20 .toLowerCase()
21 .split(/[\s,]+/)
22 .map(name => NAMES.indexOf(name.slice(0, 3)))
23 .filter(index => index >= 0)
24
25export const startOfDay = (time: number) => {
26 const d = new Date(time)
27
28 return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
29}
30
31export const nextDay = (time: number) => {
32 const d = new Date(time)
33
34 return new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1).getTime()
35}
36
37export const dayKey = (time: number) => {
38 const d = new Date(time)
39
40 return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
41}
42
43export const dayLabel = (time: number) => {
44 const d = new Date(time)
45
46 return `${NAMES[d.getDay()]} ${d.getDate()}`
47}
48
49const isWorkday = (time: number, cfg: PaceConfig) => cfg.workdays.includes(new Date(time).getDay())
50
51// Weight of the stretch from..to: each local day counts its share of the day times its day weight.
52const weightBetween = (from: number, to: number, cfg: PaceConfig) => {
53 let total = 0
54 let cursor = from
55
56 while (cursor < to) {
57 const dayEnd = nextDay(cursor)
58 const end = Math.min(dayEnd, to)
59 total += ((end - cursor) / (dayEnd - startOfDay(cursor))) * (isWorkday(cursor, cfg) ? cfg.workWeight : 1)
60 cursor = end
61 }
62
63 return total
64}
65
66// Where today's counting starts: this local midnight, or the window's start when it opened today.
67export const baselineFor = (now: number, windowEnd: number, percentUsed: number) =>
68 windowEnd - 7 * DAY >= startOfDay(now) ? 0 : percentUsed
69
70export const computePace = (
71 now: number,
72 week: { windowEnd: number; baseline: number; percentUsed: number },
73 cfg: PaceConfig,
74): Pace | null => {
75 const windowStart = week.windowEnd - 7 * DAY
76
77 if (week.windowEnd <= now) return null
78
79 const from = Math.max(startOfDay(now), windowStart)
80 const todayWeight = weightBetween(from, Math.min(nextDay(now), week.windowEnd), cfg)
81 const restWeight = weightBetween(from, week.windowEnd, cfg)
82 const budget = restWeight > 0 ? ((100 - week.baseline) * todayWeight) / restWeight : 0
83 const total = weightBetween(windowStart, week.windowEnd, cfg)
84 const daysLeft = (week.windowEnd - now) / DAY
85
86 return {
87 used: Math.max(0, week.percentUsed - week.baseline),
88 budget,
89 isWork: isWorkday(now, cfg),
90 weekUsed: week.percentUsed,
91 expected: total > 0 ? (100 * weightBetween(windowStart, now, cfg)) / total : 0,
92 daysLeft,
93 perDay: Math.max(0, 100 - week.percentUsed) / Math.max(daysLeft, 0.01),
94 }
95}
96
97// Today's ration, then each later day's share of what is left if today is spent exactly on budget.
98export const planRows = (
99 now: number,
100 week: { windowEnd: number; baseline: number; percentUsed: number },
101 cfg: PaceConfig,
102): PlanRow[] => {
103 const pace = computePace(now, week, cfg)
104
105 if (!pace) return []
106
107 const rows: PlanRow[] = [
108 { label: dayLabel(now), isWork: pace.isWork, isToday: true, budget: pace.budget, used: pace.used },
109 ]
110 const left = Math.max(0, 100 - week.baseline - Math.max(pace.used, pace.budget))
111 const days: number[] = []
112
113 for (let day = nextDay(now); day < week.windowEnd; day = nextDay(day)) days.push(day)
114
115 const weights = days.map(day => weightBetween(day, Math.min(nextDay(day), week.windowEnd), cfg))
116 const sum = weights.reduce((a, b) => a + b, 0)
117
118 days.forEach((day, i) =>
119 rows.push({
120 label: dayLabel(day),
121 isWork: isWorkday(day, cfg),
122 isToday: false,
123 budget: sum > 0 ? (left * (weights[i] ?? 0)) / sum : 0,
124 used: 0,
125 }),
126 )
127
128 return rows
129}
130types/index.d.ts 13 lines1export type PowerCell = { label: string; percentLeft: number; detail: string }
2export type WeekWindow = { windowEnd: number; baseline: number; percentUsed: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'power-cells': {
7 cells: Shaped<PowerCell[]>
8 isHidden: Shaped<boolean>
9 week: Shaped<WeekWindow | null>
10 }
11 }
12}
13