One colored line under the prompt: plan, model, effort, context, 7-day and 5-hour limits, and running agents by model and effort.

A Claude Code plugin marketplace.
| Plugin | What it does |
|---|---|
clean-view | Hides tool calls, diffs and command output, and shows a calm checklist of Claude's plan above the prompt, with a row for each helper Claude starts. /simple on or /simple off switches it. Also holds Agent Dock: pick a Team Size from 1 to 100 and every request is split across that many helpers, each shown live in a pane. /dock opens or folds it, /dock 10 sets the size. |
status-bar | One colored line under the prompt: plan, model, effort, context, 7-day and 5-hour limits, and running agents by model and effort. |
model-routing | Routes every delegated agent to the cheapest model tier that can do the job well, at an explicit effort. Refuses agents with no model, fills in effort by tier, blocks spawns from haiku agents and past two levels, and keeps a routing guide in Claude's instructions. |
mods-menu | A ☰ Mods button beside the Clean View toggle. It opens a small menu listing your mods with their controls: Clean View on or off, the Agent Dock and its Team Size, and the mods that are always on. /mods opens or closes it. |
prompt-upgrade | Turns a rough prompt into a great one before it is sent. /better <prompt> opens a pane that asks a few quick multiple-choice questions, rewrites the prompt, and shows your original and the upgrade side by side; nothing runs until you press Run. /better on checks every typed prompt first: one that already has what it needs is sent as typed, one that falls short opens the pane, and the same prompt sent twice goes straight through. Press Enter twice to send a prompt as typed. A Detail setting (Auto, Low, Medium or High; /better level auto) sets the questions and how long and deep the upgrade is: Low, Medium and High ask up to 3, 5 or 7; Auto also turns automatic mode on, asks as many questions as needed (up to 9) and, if you don't answer within 30 seconds or press Do it for me, fills in the rest and sends the upgrade itself. /better history shows the last 20. |
At the prompt of a terminal session:
/plugin install clean-view --marketplace ataege/claude-skills
/plugin install status-bar --marketplace ataege/claude-skills
/plugin install model-routing --marketplace ataege/claude-skills
/plugin install mods-menu --marketplace ataege/claude-skills
/plugin install prompt-upgrade --marketplace ataege/claude-skills
Answer y to add the marketplace, then pick a scope with Enter.
From a shell:
claude plugin install clean-view --marketplace ataege/claude-skills
claude plugin install status-bar --marketplace ataege/claude-skills
claude plugin install model-routing --marketplace ataege/claude-skills
claude plugin install mods-menu --marketplace ataege/claude-skills
claude plugin install prompt-upgrade --marketplace ataege/claude-skills
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with ataege/claude-skills, then Add plugin.
Each plugin lives in plugins/<name>. Check one with:
claude plugin validate plugins/<name>
claude plugin test plugins/<name>hooks/register.tsx 9 lines1import type { Register } from 'claude-code'
2
3import { registerStatusBar } from './status-bar'
4
5// Each mod registers itself here, so more can sit beside the status bar later.
6export const register: Register = on => {
7 registerStatusBar(on)
8}
9hooks/status-bar.tsx 446 lines1// Status bar: one colored line under the prompt with plan, model, effort, context, 7-day usage and agents.
2import { atom, read, update } from 'claude-code'
3import type {
4 AgentInfo,
5 Color,
6 ElementConstructor,
7 EngineInterface,
8 On,
9 RenderElement,
10 SessionRateLimit,
11 TextProps,
12} from 'claude-code'
13
14import type { StatusBarRoute, StatusBarSnapshot } from '../types'
15
16// ---------------------------------------------------------------------------
17// Limits and thresholds
18// ---------------------------------------------------------------------------
19
20const CONTEXT_CELLS = 10
21const SEPARATOR = ' │ '
22// Between the routes of the agents segment, like 3 haiku/low · 1 opus/high.
23const ROUTE_SEPARATOR = ' · '
24
25// One single-width symbol per segment, safe in Windows Terminal and most fonts.
26export const ICONS = {
27 plan: '◆',
28 model: '✻',
29 context: '▣',
30 limits: '◷',
31 agents: '◎',
32 betterPrompt: '◇',
33} as const
34
35// Prompt Upgrade's gold, for its segment.
36const BETTER_PROMPT_COLOR = '#F2C46D'
37
38// The effort symbol fills up with the level: ◔ low, ◑ medium, ◕ high, ● xhigh, ◉ max.
39const EFFORT_ICONS: Readonly<Record<string, string>> = {
40 low: '◔',
41 medium: '◑',
42 high: '◕',
43 xhigh: '●',
44 max: '◉',
45}
46// Usage share at which the plan turns amber, then red.
47const NEAR_LIMIT_PERCENT = 75
48const AT_LIMIT_PERCENT = 95
49// A finished subagent can still read as running for a moment: look again after this.
50const AGENT_RECHECK_MS = 1500
51const ACTIVE_AGENT_STATUSES: ReadonlySet<string> = new Set(['pending', 'running', 'waiting'])
52
53const EMPTY_SNAPSHOT: StatusBarSnapshot = {
54 planName: null,
55 model: null,
56 effort: null,
57 contextPercent: null,
58 fiveHourUsedPercent: null,
59 sevenDayUsedPercent: null,
60 isRateLimited: false,
61 activeAgents: 0,
62 agentRoutes: [],
63}
64
65const snapshotAtom = atom({ plugin: 'status-bar', key: 'snapshot' } as const, EMPTY_SNAPSHOT)
66const routesAtom = atom({ plugin: 'status-bar', key: 'routes' } as const, {})
67// Prompt Upgrade's automatic mode, read while drawing so a switch redraws the line; false without that mod.
68const betterPromptAtom = atom({ plugin: 'prompt-upgrade', key: 'isAutoOn' } as const, false)
69// The route table keeps the most recent agents only.
70const MAX_TRACKED_ROUTES = 300
71
72type Engine = EngineInterface
73type TextElement = ElementConstructor<TextProps>
74
75// ---------------------------------------------------------------------------
76// Turning raw figures into words
77// ---------------------------------------------------------------------------
78
79const PLAN_NAMES: Readonly<Record<string, string>> = {
80 claude_free: 'Free',
81 claude_pro: 'Pro',
82 claude_max: 'Max',
83 claude_team: 'Team',
84 claude_enterprise: 'Enterprise',
85}
86
87/** The plan name from the login record's account fields: "Max 20x", "Pro", "API"... */
88export function planLabel(account: unknown): string {
89 if (account === null || typeof account !== 'object') return 'API'
90 const { organizationType, organizationRateLimitTier, userRateLimitTier } = account as Record<string, unknown>
91 const planType = typeof organizationType === 'string' ? organizationType : ''
92 const baseName = PLAN_NAMES[planType] ?? (planType === '' ? 'Subscription' : titleCase(planType.replace(/^claude_/, '')))
93
94 // The tier names the Max multiplier, like "default_claude_max_20x".
95 const tier = [organizationRateLimitTier, userRateLimitTier].find(value => typeof value === 'string') as string | undefined
96 const multiplier = tier?.match(/(\d+)x\b/)?.[1]
97
98 return multiplier !== undefined && baseName === 'Max' ? `Max ${multiplier}x` : baseName
99}
100
101function titleCase(text: string): string {
102 return text
103 .split(/[_\s-]+/)
104 .filter(Boolean)
105 .map(word => word.charAt(0).toUpperCase() + word.slice(1))
106 .join(' ')
107}
108
109/** "claude-opus-5-5" becomes "Opus 5.5"; an alias like "opus" becomes "Opus". */
110export function formatModel(modelId: string): string {
111 const withoutContextTag = modelId.replace(/\[[^\]]*\]$/, '')
112 const hasMillionContext = /\[1m\]$/i.test(modelId)
113 const versioned = withoutContextTag.match(/(?:claude-)?([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?(?:$|[-@:])/i)
114 const name = versioned
115 ? `${titleCase(versioned[1] ?? '')} ${versioned[2]}${versioned[3] !== undefined ? `.${versioned[3]}` : ''}`
116 : titleCase(withoutContextTag.replace(/^claude-/, ''))
117
118 return hasMillionContext ? `${name} 1M` : name
119}
120
121/** The effort level as a word; a numeric budget stays a number. */
122export function formatEffort(effort: unknown): string | null {
123 if (typeof effort === 'string' && effort !== '') return effort
124 if (typeof effort === 'number' && Number.isFinite(effort)) return String(effort)
125
126 return null
127}
128
129/** An agent's route as the line shows it: "haiku/low", or the tier alone when it has no effort. */
130export function routeLabel(modelId: string, effort: unknown): string {
131 const tier = modelId.toLowerCase().match(/haiku|sonnet|opus|fable/)?.[0] ?? formatModel(modelId).toLowerCase()
132 const effortWord = formatEffort(effort)
133
134 return effortWord === null ? tier : `${tier}/${effortWord}`
135}
136
137/** Running agents grouped by route, most first; agents not yet seen making a request count as "agent". */
138export function groupRoutes(agentIds: readonly string[], routes: Readonly<Record<string, string>>): StatusBarRoute[] {
139 const counts = new Map<string, number>()
140 for (const agentId of agentIds) {
141 const label = routes[agentId] ?? 'agent'
142 counts.set(label, (counts.get(label) ?? 0) + 1)
143 }
144
145 return [...counts.entries()]
146 .map(([label, count]) => ({ label, count }))
147 .sort((left, right) => right.count - left.count || left.label.localeCompare(right.label))
148}
149
150/** The effort symbol for a level; a numeric budget or an unknown level shows half full. */
151export function effortIcon(effort: string | null): string {
152 return (effort !== null ? EFFORT_ICONS[effort] : undefined) ?? '◑'
153}
154
155function percentOf(windows: readonly SessionRateLimit[], kind: string): number | null {
156 const window = windows.find(candidate => candidate.kind === kind)
157 return window === undefined ? null : Math.max(0, window.percentUsed)
158}
159
160/** How close the account is to a usage limit, from the busiest window. */
161export function limitStatus(snapshot: StatusBarSnapshot): 'unknown' | 'ok' | 'near' | 'limited' {
162 if (snapshot.isRateLimited) return 'limited'
163 const readings = [snapshot.fiveHourUsedPercent, snapshot.sevenDayUsedPercent].filter(
164 (value): value is number => value !== null,
165 )
166 if (readings.length === 0) return 'unknown'
167 const busiest = Math.max(...readings)
168 if (busiest >= AT_LIMIT_PERCENT) return 'limited'
169 if (busiest >= NEAR_LIMIT_PERCENT) return 'near'
170
171 return 'ok'
172}
173
174const PLAN_COLORS: Readonly<Record<ReturnType<typeof limitStatus>, Color>> = {
175 unknown: 'text',
176 ok: 'success',
177 near: 'warning',
178 limited: 'error',
179}
180
181/** Green while there is room, amber past half, red when nearly full. */
182export function contextColor(percent: number): Color {
183 if (percent >= 80) return 'error'
184 if (percent >= 50) return 'warning'
185
186 return 'success'
187}
188
189/** Green with plenty left, amber when it gets low, red when nearly gone. */
190export function remainingColor(remainingPercent: number): Color {
191 if (remainingPercent < 20) return 'error'
192 if (remainingPercent < 50) return 'warning'
193
194 return 'success'
195}
196
197/** Ten parallelograms, filled to the context share. */
198export function contextMeter(percent: number): { filled: string; empty: string } {
199 const filledCells = Math.min(CONTEXT_CELLS, Math.max(0, Math.round(percent / 10)))
200 return { filled: '▰'.repeat(filledCells), empty: '▱'.repeat(CONTEXT_CELLS - filledCells) }
201}
202
203// ---------------------------------------------------------------------------
204// Keeping the snapshot current
205// ---------------------------------------------------------------------------
206
207async function patchSnapshot($: Engine, patch: Partial<StatusBarSnapshot>): Promise<void> {
208 await update($, snapshotAtom, snapshot => ({ ...snapshot, ...patch }))
209}
210
211/** Reads the plan's name from the login record; only the plan fields are kept. */
212async function refreshPlan($: Engine): Promise<void> {
213 try {
214 const homeDirectory = await $.env.get('HOME')
215 if (homeDirectory === undefined) return
216 const loginRecord = JSON.parse(await $.fs.read(`${homeDirectory}/.claude.json`)) as Record<string, unknown>
217 await patchSnapshot($, { planName: planLabel(loginRecord.oauthAccount) })
218 } catch {
219 // No login record: the plan segment reads "Subscription" until one is found.
220 }
221}
222
223async function refreshModelAndEffort($: Engine): Promise<void> {
224 try {
225 const [modelId, settings] = await Promise.all([$.session.model(), $.settings.read()])
226 const patch: Partial<StatusBarSnapshot> = { model: formatModel(modelId) }
227 const configuredEffort = formatEffort(settings.effortLevel)
228 if (configuredEffort !== null) patch.effort = configuredEffort
229 await patchSnapshot($, patch)
230 } catch {
231 // Keep what the line showed before.
232 }
233}
234
235async function refreshUsage(
236 $: Engine,
237 usage?: { context: { percent?: number }; rateLimits: readonly SessionRateLimit[] },
238): Promise<void> {
239 try {
240 const figures = usage ?? (await $.session.usage())
241 const patch: Partial<StatusBarSnapshot> = {}
242 if (figures.context.percent !== undefined) patch.contextPercent = figures.context.percent
243 if (figures.rateLimits.length > 0) {
244 patch.fiveHourUsedPercent = percentOf(figures.rateLimits, 'five_hour')
245 patch.sevenDayUsedPercent = percentOf(figures.rateLimits, 'seven_day')
246 patch.isRateLimited = false
247 }
248 await patchSnapshot($, patch)
249 } catch {
250 // Keep what the line showed before.
251 }
252}
253
254async function refreshAgents($: Engine): Promise<void> {
255 try {
256 const agents: readonly AgentInfo[] = await $.agent.list()
257 const activeIds = agents.filter(agent => ACTIVE_AGENT_STATUSES.has(agent.status)).map(agent => agent.id)
258 const routes = await read($, routesAtom)
259 await patchSnapshot($, { activeAgents: activeIds.length, agentRoutes: groupRoutes(activeIds, routes) })
260 } catch {
261 // Keep what the line showed before.
262 }
263}
264
265function refreshAgentsSoon($: Engine): void {
266 $.clock.after(AGENT_RECHECK_MS, () => {
267 void refreshAgents($)
268 })
269}
270
271/** Records an agent's route from its own request; recounts when the route is new. */
272async function recordRoute($: Engine, agentId: string, label: string): Promise<void> {
273 const routes = await read($, routesAtom)
274 if (routes[agentId] === label) return
275 await update($, routesAtom, current => {
276 const entries = Object.entries({ ...current, [agentId]: label })
277 return Object.fromEntries(entries.slice(-MAX_TRACKED_ROUTES))
278 })
279 await refreshAgents($)
280}
281
282async function refreshEverything($: Engine): Promise<void> {
283 await Promise.all([refreshPlan($), refreshModelAndEffort($), refreshUsage($), refreshAgents($)])
284}
285
286// ---------------------------------------------------------------------------
287// Drawing the line
288// ---------------------------------------------------------------------------
289
290/** What is left of a usage window, as a whole percent; null with no reading. */
291export function remainingPercent(usedPercent: number | null): number | null {
292 return usedPercent === null ? null : Math.max(0, Math.round(100 - usedPercent))
293}
294
295/** A dim window label ("7d") and its remaining percent, colored by how much is left. */
296function drawLimit(Text: TextElement, label: string, remaining: number | null): RenderElement[] {
297 return [
298 <Text dimColor>{`${label} `}</Text>,
299 remaining === null ? <Text dimColor>–</Text> : <Text color={remainingColor(remaining)}>{`${remaining}%`}</Text>,
300 ]
301}
302
303/** The status line as one Text: ◆ plan │ ✻ model │ ◕ effort │ ▣ ctx │ ◷ 7d 5h │ ◎ agents, then ◇ Better Prompt On while it is. */
304function drawStatusLine(Text: TextElement, snapshot: StatusBarSnapshot, isBetterPromptOn = false): RenderElement {
305 const separator = <Text dimColor>{SEPARATOR}</Text>
306 const planName = snapshot.planName ?? 'Subscription'
307 const contextPercent = snapshot.contextPercent ?? 0
308 const meter = contextMeter(contextPercent)
309 const sevenDayRemaining = remainingPercent(snapshot.sevenDayUsedPercent)
310 const fiveHourRemaining = remainingPercent(snapshot.fiveHourUsedPercent)
311 // The symbol takes the color of whichever window has less left.
312 const knownRemaining = [sevenDayRemaining, fiveHourRemaining].filter((value): value is number => value !== null)
313 const agentWord = snapshot.activeAgents === 1 ? 'agent' : 'agents'
314 const hasKnownRoutes = snapshot.activeAgents > 0 && snapshot.agentRoutes.some(route => route.label !== 'agent')
315
316 const planColor = PLAN_COLORS[limitStatus(snapshot)]
317 const contextTone = snapshot.contextPercent === null ? 'subtle' : contextColor(contextPercent)
318 const limitsTone = knownRemaining.length === 0 ? 'subtle' : remainingColor(Math.min(...knownRemaining))
319 const agentTone = snapshot.activeAgents > 0 ? 'claude' : 'subtle'
320
321 return (
322 <Text wrap="truncate-end">
323 <Text color={planColor}>{`${ICONS.plan} `}</Text>
324 <Text color={planColor} weight="bold">
325 {planName}
326 </Text>
327 {separator}
328 <Text color="claude">{`${ICONS.model} `}</Text>
329 <Text color="claude">{snapshot.model ?? '…'}</Text>
330 {separator}
331 <Text color="suggestion">{`${effortIcon(snapshot.effort)} `}</Text>
332 <Text dimColor>effort </Text>
333 <Text>{snapshot.effort ?? '…'}</Text>
334 {separator}
335 <Text color={contextTone}>{`${ICONS.context} `}</Text>
336 <Text dimColor>ctx </Text>
337 <Text color={contextTone}>{meter.filled}</Text>
338 <Text dimColor>{meter.empty}</Text>
339 <Text> {snapshot.contextPercent === null ? '–' : `${contextPercent}%`}</Text>
340 {separator}
341 <Text color={limitsTone}>{`${ICONS.limits} `}</Text>
342 {drawLimit(Text, '7d', sevenDayRemaining)}
343 {separator}
344 {drawLimit(Text, '5h', fiveHourRemaining)}
345 {separator}
346 <Text color={agentTone}>{`${ICONS.agents} `}</Text>
347 {hasKnownRoutes ? (
348 snapshot.agentRoutes.map((route, index) => [
349 index > 0 ? <Text dimColor>{ROUTE_SEPARATOR}</Text> : null,
350 <Text color="claude">{`${route.count} ${route.label}`}</Text>,
351 ])
352 ) : (
353 <Text color={snapshot.activeAgents > 0 ? 'claude' : undefined} dimColor={snapshot.activeAgents === 0}>
354 {`${snapshot.activeAgents} ${agentWord}`}
355 </Text>
356 )}
357 {isBetterPromptOn ? [
358 separator,
359 <Text color={BETTER_PROMPT_COLOR}>{`${ICONS.betterPrompt} Better Prompt On`}</Text>,
360 ] : null}
361 </Text>
362 )
363}
364
365// ---------------------------------------------------------------------------
366// Hooks
367// ---------------------------------------------------------------------------
368
369export function registerStatusBar(on: On): void {
370 // Session start (and each reload): read every figure once.
371 on('session.start', async ($, e, next) => {
372 await refreshEverything($)
373 return next(e)
374 })
375
376 // Usage figures pushed by the engine after each turn and as limits move.
377 on('session.measure', async ($, e, next) => {
378 await refreshUsage($, e)
379 return next(e)
380 })
381
382 // Each request names the model and effort actually used: the main thread's for the line, an agent's for its route.
383 on('turn.step', async function* ($, e, next) {
384 if (e.agentId === undefined) {
385 const effort = formatEffort(e.effort)
386 const patch = effort === null ? { model: formatModel(e.model) } : { model: formatModel(e.model), effort }
387 // Never hold up or break the model's stream over a display update.
388 void patchSnapshot($, patch).catch(() => undefined)
389 } else {
390 void recordRoute($, e.agentId, routeLabel(e.model, e.effort)).catch(() => undefined)
391 }
392 return yield* next(e)
393 })
394
395 // A new turn: pick up a changed model or effort setting.
396 on('turn.start', async ($, e, next) => {
397 if (e.agentId === undefined) await refreshModelAndEffort($)
398 return next(e)
399 })
400
401 // A subagent's run ended: recount, now and once its task settles.
402 on('turn.complete', async ($, e, next) => {
403 const result = await next(e)
404 if (e.agentId !== undefined) {
405 await refreshAgents($)
406 refreshAgentsSoon($)
407 }
408 return result
409 })
410
411 // An agent started: count it.
412 on('agent.spawn', async ($, e, next) => {
413 const result = await next(e)
414 await refreshAgents($)
415 return result
416 }).catch(($, e, next) => next(e))
417
418 // A usage-limit error turns the plan red until usage moves again.
419 on('classic.StopFailure', async ($, e, next) => {
420 if (e.error === 'rate_limit') await patchSnapshot($, { isRateLimited: true })
421 return next(e)
422 }).catch(($, e, next) => next(e))
423
424 // /model, /effort, /clear and the like: read the figures again afterwards.
425 on('command.run', async ($, e, next) => {
426 const result = await next(e)
427 await Promise.all([refreshModelAndEffort($), refreshUsage($), refreshAgents($)])
428 return result
429 }).catch(($, e, next) => next(e))
430
431 // The line sits right under the prompt, above the engine's own hint line.
432 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
433 const snapshot = await read($, snapshotAtom)
434 const isBetterPromptOn = (await read($, betterPromptAtom)) === true
435 const engineHint = await next(e)
436 const { Box, Text } = $.ui.resolve(e)
437
438 return (
439 <Box flexDirection="column">
440 {drawStatusLine(Text, snapshot, isBetterPromptOn)}
441 {engineHint}
442 </Box>
443 )
444 })
445}
446types/index.d.ts 40 lines1// The status bar's shared state: other mods can read its figures from here.
2
3/** Running agents that share one model and effort, like 3 on "haiku/low". */
4export type StatusBarRoute = { label: string; count: number }
5
6/** What the status bar under the prompt shows; null where nothing is known yet. */
7export type StatusBarSnapshot = {
8 /** The plan, like "Max 20x" or "Pro"; "API" for a key. */
9 planName: string | null
10 /** The model in plain words, like "Opus 5.5". */
11 model: string | null
12 /** The reasoning effort, like "high". */
13 effort: string | null
14 /** How full the context window is, 0 to 100. */
15 contextPercent: number | null
16 /** How much of the 5-hour and 7-day usage windows is used, 0 to 100. */
17 fiveHourUsedPercent: number | null
18 sevenDayUsedPercent: number | null
19 /** True after a turn ended on a usage-limit error, until usage moves again. */
20 isRateLimited: boolean
21 /** Subagents and teammates that are starting, running or waiting. */
22 activeAgents: number
23 /** The running agents grouped by model and effort, most first. */
24 agentRoutes: StatusBarRoute[]
25}
26
27declare module 'claude-code' {
28 interface PluginState {
29 'status-bar': {
30 snapshot: StatusBarSnapshot
31 /** Each agent's route, like "haiku/low", by agent id, from its own requests. */
32 routes: Record<string, string>
33 }
34 // Read only: Prompt Upgrade's automatic mode, shown while it is on.
35 'prompt-upgrade': {
36 isAutoOn: boolean
37 }
38 }
39}
40