SLOPSHOPPER

omp-statusline

An Oh My Pi style status band: model, cwd, git branch, PR, plan, context use and window.

newbandprocess
v0.1.0no licenseupdated 2026-10-06yi-john-huang/claude-garage/plugins/omp-statusline
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · omp-statusline
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM π › ◑ claude-opus-5-5 › 📁 /work/app › ⑂ feat/auth-refresh › (sub) › 49% │ 200K ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
π › ◑ claude-opus-5-5 › 📁 /work/app › ⑂ feat/auth-refresh › (sub) › 49% │ 200K
README

claude-garage

A garage of Claude Code mods: a plugin marketplace that grows one mod at a time.

Install

# From GitHub
claude plugin marketplace add yi-john-huang/claude-garage
# Or from a local clone
claude plugin marketplace add ~/workspace/claude-garage

claude plugin install omp-statusline@claude-garage

Then run /reload-plugins in a session, or start a new one.

Mods

ModWhat it does
omp-statuslineAn Oh My Pi style band above the prompt: model, cwd, git branch, PR, (sub), context use and window. Needs git; the PR number needs gh.

Develop

claude plugin validate .                      # the marketplace
claude plugin validate plugins/omp-statusline # one mod
claude plugin test plugins/omp-statusline

Bump version in both the mod's plugin.json and marketplace.json when releasing a change, so claude plugin update picks it up.

Source 2 files
hooks/register.tsx 118 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Git } from '../types'
5
6const git = atom({ plugin: 'omp-statusline', key: 'git' } as const, null)
7// Bumped whenever usage or the model may have moved, so the band redraws.
8const tick = atom({ plugin: 'omp-statusline', key: 'tick' } as const, 0)
9
10const SEP = ' › '
11
12const shortPath = (path: string): string =>
13  path.replace(/^\/(Users|home)\/[^/]+/, '~')
14
15const shortTokens = (n: number): string =>
16  n >= 1_000_000
17    ? `${+(n / 1_000_000).toFixed(1)}M`
18    : n >= 1000
19      ? `${Math.round(n / 1000)}K`
20      : `${n}`
21
22const percentColor = (p: number): string =>
23  p >= 85 ? 'red' : p >= 60 ? 'yellow' : 'magenta'
24
25async function run($: EngineInterface, argv: string[]): Promise<string | null> {
26  try {
27    const r = await $.process.run(argv, { timeoutMs: 5000 })
28    return r.exitCode === 0 ? r.stdout.trim() : null
29  } catch {
30    return null
31  }
32}
33
34// PR lookups hit the network, so only redo one when the branch changes.
35let prFor: string | null = null
36let pr: number | null = null
37
38async function refreshGit($: EngineInterface): Promise<void> {
39  const branch = await run($, ['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
40  if (branch === null) {
41    await update($, git, () => null)
42    return
43  }
44  if (branch !== prFor) {
45    prFor = branch
46    const n = await run($, ['gh', 'pr', 'view', '--json', 'number', '-q', '.number'])
47    pr = n !== null && /^\d+$/.test(n) ? Number(n) : null
48  }
49  const next: Git = { branch, pr }
50  await update($, git, () => next)
51}
52
53async function bump($: EngineInterface): Promise<void> {
54  await update($, tick, n => n + 1)
55}
56
57export const register: Register = on => {
58  on('session.start', async ($, e, next) => {
59    const started = await next(e)
60    void refreshGit($)
61    return started
62  })
63
64  on('turn.complete', async ($, e, next) => {
65    const done = await next(e)
66    if (e.agentId === undefined) {
67      void refreshGit($)
68      await bump($)
69    }
70    return done
71  })
72
73  on('session.measure', async ($, e, next) => {
74    const measured = await next(e)
75    await bump($)
76    return measured
77  })
78
79  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
80    if (e.props.hasSurvey) return next(e)
81
82    await read($, tick)
83    const [model, cwd, usage, repo] = await Promise.all([
84      $.session.model(),
85      $.session.cwd(),
86      $.session.usage(),
87      read($, git),
88    ])
89    const { context, rateLimits } = usage
90    const isSub = rateLimits.length > 0
91
92    const { Box, Text } = $.ui.resolve(e)
93    const sep = <Text dimColor>{SEP}</Text>
94
95    return (
96      <Box flexDirection="row">
97        <Text dimColor>π</Text>
98        {sep}
99        <Text color="magenta">◑ {model}</Text>
100        {sep}
101        <Text color="cyan">📁 {shortPath(cwd)}</Text>
102        {repo && sep}
103        {repo && <Text color="green">⑂ {repo.branch}</Text>}
104        {repo?.pr != null && sep}
105        {repo?.pr != null && <Text color="magenta">↗#{repo.pr}</Text>}
106        {isSub && sep}
107        {isSub && <Text color="magenta">(sub)</Text>}
108        {sep}
109        {context.percent !== undefined && (
110          <Text color={percentColor(context.percent)}>{context.percent}% </Text>
111        )}
112        <Text dimColor>│ </Text>
113        <Text color="magenta">{shortTokens(context.window)}</Text>
114      </Box>
115    )
116  })
117}
118
types/index.d.ts 8 lines
1export type Git = { branch: string; pr: number | null }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'omp-statusline': { git: Git | null; tick: number }
6  }
7}
8