Shows the terminal status line (model, session tokens, context %, 5h/7d rate limits) as a band above the prompt in the Claude desktop app

Clint's personal Claude Code plugin marketplace — skills and agents distributed as installable, auto-updating plugins instead of hand-zipped .skill files.
dev-workflow — Playwright testing tips (playwright-testing skill), a playwright-runner subagent that executes Playwright tests/browser steps and reports back a compact summary, and a demoing-work skill for walking the user through completed work and gating "done" on their explicit confirmation.game-catchup — Generates a structured "what you missed" recap for a game you haven't played in a while.feature-maturity — Shared vocabulary for how done a feature actually is: a Completeness ladder (Spike/Prototype/Functional/Finished) plus independent quality axes (Polish, Robustness, Architecture, Verification, Documentation, Accessibility). Use for scoping work, self-reporting before declaring something "done," auditing existing code, or resolving disagreements about whether something is really Finished/Polished/Hardened.desktop-statusline — A hooks-module mod that mirrors the terminal status line (model, session tokens, context %, 5h/7d rate limits with reset countdown) as a band above the prompt in the Claude desktop app's Code tab. Hidden in the terminal, where the real status line runs.subagent-tree — A hooks-module mod that adds an "Agents" side pane: subagents in a tree with the last tool and how long ago it ran, tokens and runtime per agent, click-through detail (prompt, tool log, result), search, and saved history of past sessions./plugin marketplace add wolf123450/ClaudeSkills
/plugin install dev-workflow@claude-skills
/plugin install game-catchup@claude-skills
/plugin install feature-maturity@claude-skills
/plugin install desktop-statusline@claude-skills
/plugin install subagent-tree@claude-skills
(Or via the claude CLI: claude plugin marketplace add wolf123450/ClaudeSkills, then claude plugin install dev-workflow@claude-skills.)
/plugin marketplace update claude-skills
Or just wait — Claude Code periodically checks configured marketplaces for updates. Since plugins live in this git repo, any commit pushed here is what the next update pulls; no manual re-zipping or redistribution step.
plugins/<name>/.claude-plugin/plugin.json (name, description, version, author).plugins/<name>/skills/<skill-name>/SKILL.md and/or agents under plugins/<name>/agents/<agent-name>.md..claude-plugin/marketplace.json's plugins array with "source": "./plugins/<name>".claude plugin validate . from the repo root to confirm it's picked up cleanly beforecommitting.
hooks/register.tsx 113 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Snapshot, Tokens } from '../types'
5
6const tokens = atom({ plugin: 'desktop-statusline', key: 'tokens' } as const, { startedAt: 0, total: 0 })
7const snapshot = atom({ plugin: 'desktop-statusline', key: 'snapshot' } as const, null)
8const now = atom({ plugin: 'desktop-statusline', key: 'now' } as const, 0)
9
10// Same thresholds as ~/.claude/statusline-command.sh
11const trafficLight = (pct: number, low: string) => (pct >= 75 ? 'red' : pct >= 50 ? 'yellow' : low)
12
13const formatTokens = (n: number) =>
14 n >= 1_000_000 ? `${Math.floor(n / 1_000_000)}M tok` : n >= 1000 ? `${Math.floor(n / 1000)}k tok` : n > 0 ? `${n} tok` : null
15
16const formatReset = (resetsAt: string | undefined, nowMs: number) => {
17 if (!resetsAt) return ''
18 const remaining = Math.floor((Date.parse(resetsAt) - nowMs) / 1000)
19 if (!(remaining > 0)) return ''
20 const h = Math.floor(remaining / 3600)
21 const m = Math.floor((remaining % 3600) / 60)
22 return h > 0 ? ` (resets ${h}h${m}m)` : ` (resets ${m}m)`
23}
24
25const formatClock = (ms: number) => {
26 if (!ms) return '--:--:--'
27 const d = new Date(ms)
28 const pad = (n: number) => String(n).padStart(2, '0')
29 return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
30}
31
32async function refresh($: EngineInterface) {
33 const [usage, model, t] = await Promise.all([$.session.usage(), $.session.model(), $.clock.now()])
34 const snap: Snapshot = {
35 model,
36 contextPercent: usage.context.percent ?? null,
37 rateLimits: usage.rateLimits.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt })),
38 }
39 // /clear restarts the session clock: start the token count over with it.
40 await update($, tokens, (cur: Tokens) => (cur.startedAt === usage.startedAt ? cur : { startedAt: usage.startedAt, total: 0 }))
41 await update($, snapshot, () => snap)
42 await update($, now, () => t)
43}
44
45export const register: Register = on => {
46 on('session.start', async ($, e, next) => {
47 const result = await next(e)
48 await refresh($)
49 // Keeps the reset countdown moving between turns.
50 $.clock.every(60_000, () => void refresh($))
51 return result
52 })
53
54 on('turn.step', async function* ($, e, next) {
55 const result = yield* next(e)
56 const usage = result.usage
57 if (usage) {
58 await update($, tokens, (cur: Tokens) => ({ ...cur, total: cur.total + usage.input_tokens + usage.output_tokens }))
59 }
60 await refresh($)
61 return result
62 })
63
64 on('turn.complete', async ($, e, next) => {
65 const result = await next(e)
66 await refresh($)
67 return result
68 })
69
70 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
71 // The terminal already runs the real status line command.
72 if (e.surface === 'terminal' || e.props.hasSurvey) return next(e)
73
74 const snap = await read($, snapshot)
75 if (snap === null) return next(e)
76
77 const { total } = await read($, tokens)
78 const nowMs = await read($, now)
79 const { Box, Text, Button } = $.ui.resolve(e)
80
81 const parts: { key: string; text: string; color?: string; dim?: boolean }[] = [
82 { key: 'model', text: snap.model, color: 'cyan' },
83 ]
84 const tok = formatTokens(total)
85 if (tok) parts.push({ key: 'tok', text: tok, color: 'white' })
86 if (snap.contextPercent !== null) {
87 const used = Math.round(snap.contextPercent)
88 parts.push({ key: 'ctx', text: `ctx:${used}% (${100 - used}% left)`, color: trafficLight(used, 'green') })
89 }
90 const limits = snap.rateLimits.filter(r => r.kind === 'five_hour' || r.kind === 'seven_day')
91 for (const r of limits) {
92 const pct = Math.round(r.percentUsed)
93 const label = r.kind === 'five_hour' ? '5h' : '7d'
94 const reset = r.kind === 'five_hour' ? formatReset(r.resetsAt, nowMs) : ''
95 const color = trafficLight(pct, '')
96 parts.push({ key: label, text: `${label}:${pct}%${reset}`, color: color || undefined, dim: !color })
97 }
98
99 return (
100 <Box>
101 {parts.map((p, i) => (
102 <Text key={p.key} color={p.color} dimColor={p.dim}>
103 {i > 0 ? ' | ' : ''}
104 {p.text}
105 </Text>
106 ))}
107 <Text dimColor>{` | updated ${formatClock(nowMs)} `}</Text>
108 <Button key="refresh" label="↻" plain onPress={() => void refresh($)} />
109 </Box>
110 )
111 })
112}
113types/index.d.ts 14 lines1export type Tokens = { startedAt: number; total: number }
2
3export type Snapshot = {
4 model: string
5 contextPercent: number | null
6 rateLimits: { kind: string; percentUsed: number; resetsAt?: string }[]
7}
8
9declare module 'claude-code' {
10 interface PluginState {
11 'desktop-statusline': { tokens: Tokens; snapshot: Snapshot | null; now: number }
12 }
13}
14