SLOPSHOPPER

desktop-statusline

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

newbandtimer
v1.0.1no licenseupdated 2026-10-04wolf123450/ClaudeSkills/plugins/desktop-statusline
A shopper browsing a rack in a slop shop
README

ClaudeSkills

Clint's personal Claude Code plugin marketplace — skills and agents distributed as installable, auto-updating plugins instead of hand-zipped .skill files.

Plugins

  • 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.

Install

/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.)

Updating

/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.

Adding a new plugin

  1. Create plugins/<name>/.claude-plugin/plugin.json (name, description, version, author).
  2. Add its skills under plugins/<name>/skills/<skill-name>/SKILL.md and/or agents under plugins/<name>/agents/<agent-name>.md.
  3. Add an entry to .claude-plugin/marketplace.json's plugins array with "source": "./plugins/<name>".
  4. Run claude plugin validate . from the repo root to confirm it's picked up cleanly before

committing.

Source 2 files
hooks/register.tsx 113 lines
1import { 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}
113
types/index.d.ts 14 lines
1export 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