SLOPSHOPPER

repo-status

A live git strip above the prompt: branch, ahead/behind, changed files and the open pull request's checks.

newbandprocesstimer
v0.2.1no licenseupdated 2026-10-16Arkboi-GG/claude-mods/mods/repo-status
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · repo-status
› 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 Code's own drawing⟩ ● 1 changed PR #undefined ✓ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ● 1 changed PR #undefined ✓
README

claude-mods

A small collection of Claude Code mods.

Install

/plugin install <mod> --marketplace Arkboi-GG/claude-mods

The bars stack above the prompt, so you can install any combination.

Mods

  • task-progress: the band above the prompt. While Claude works on a multi-step task it shows one block per step: the running step fills as "In Progress", finished steps turn green "Completed", and a turn that stops with steps still open turns amber "Paused". When Claude is idle it shows usage rings (context, 5-hour and 7-day limits) with a Compact button. It reads the task list Claude keeps while working, so it appears for multi-step tasks. If you also use the skins mod, run /skin band off once so its own usage band doesn't show twice.
  • repo-status: branch, ahead/behind, changed-file count and the open PR's check state (needs git; gh is optional).
  • stack-health: status dots for your Docker Compose services and any local ports you list (web=5173,api=3000).
  • preflight: a live pre-commit checklist. After Claude edits files it re-runs your typecheck/lint (auto-detected, or set checks) and shows pass/fail with an error count.
  • env-guard: a green LOCAL badge that turns red PROD when a tool call matches your production patterns, and toasts when it happens.
  • agent-crabs: a pixel-art crab for every running subagent, dressed for its role (beret, hard hat, postman cap and more), with the agent's name, what it is working on and a timer. Finished crabs cheer and leave after a few seconds; failed ones turn grey. Name an agent by starting the Agent call's description with Name: , e.g. Olivia: rewrite welcome email; map names to outfits with the outfits option.
Source 2 files
hooks/register.tsx 110 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Repo } from '../types'
5
6const repoAtom = atom({ plugin: 'repo-status', key: 'repo' } as const, null as Repo | null)
7
8const WIDE = 110
9
10// Shared "dev row" protocol (the same helper sits in every mod of this repo):
11// each mod adds a keyed zone to one row, so the mods fill a single line together
12// and still work alone. Zones sort by their key, `zone-<order>`.
13function addZone(below: any, zone: any, order: number): any {
14  const tagged = { ...zone, props: { ...zone.props, key: `zone-${order}`, flexShrink: 0 } }
15  const byKey = (a: any, b: any) => String(a?.props?.key).localeCompare(String(b?.props?.key))
16  const merge = (node: any): any => {
17    if (!node || typeof node !== 'object') return null
18    if (node.props?.key === 'devrow') return { ...node, children: [...(node.children ?? []), tagged].sort(byKey) }
19    const kids: any[] = node.children ?? []
20    for (let i = 0; i < kids.length; i++) {
21      const hit = merge(kids[i])
22      if (hit) return { ...node, children: kids.map((k, j) => (j === i ? hit : k)) }
23    }
24    return null
25  }
26  const merged = merge(below)
27  if (merged) return merged
28  const row = {
29    type: 'Box',
30    props: { key: 'devrow', paddingX: 1, justifyContent: 'space-between', flexWrap: 'wrap', columnGap: 3 },
31    children: [tagged],
32  }
33  return {
34    type: 'Box',
35    props: { flexDirection: 'column' },
36    children: below ? [below, row] : [row],
37  }
38}
39
40export const register: Register = (on, options) => {
41  const wantPr = options.showPullRequest !== false
42  let prText = ''
43  let prLong = ''
44  let prAt = 0
45
46  on('session.start', async ($, e, next) => {
47    const poll = async () => {
48      try {
49        const st = await $.process.run(['git', 'status', '--porcelain=v1', '-b'], { timeoutMs: 8000 })
50        if (st.exitCode !== 0) return update($, repoAtom, () => null)
51        const lines = st.stdout.split('\n').filter(Boolean)
52        const head = lines[0] ?? ''
53        const branch = head.replace(/^## /, '').replace(/^No commits yet on /, '').split('...')[0].split(' ')[0]
54        const ahead = Number(/ahead (\d+)/.exec(head)?.[1] ?? 0)
55        const behind = Number(/behind (\d+)/.exec(head)?.[1] ?? 0)
56
57        const now = await $.clock.now()
58        if (wantPr && now - prAt > 60000) {
59          prAt = now
60          try {
61            const pr = await $.process.run(['gh', 'pr', 'view', '--json', 'number,state,statusCheckRollup'], { timeoutMs: 15000 })
62            if (pr.exitCode === 0) {
63              const j = JSON.parse(pr.stdout)
64              const checks: Array<{ conclusion?: string; status?: string }> = j.statusCheckRollup ?? []
65              const bad = checks.some(c => ['FAILURE', 'CANCELLED', 'TIMED_OUT'].includes(c.conclusion ?? ''))
66              const pending = checks.some(c => c.status !== 'COMPLETED')
67              const mark = j.state === 'MERGED' ? 'merged' : bad ? '✗' : pending ? '…' : '✓'
68              prText = `PR #${j.number} ${mark}`
69              prLong = j.state === 'MERGED' ? 'merged' : bad ? 'checks failing' : pending ? 'checks running' : 'checks passing'
70            } else {
71              prText = ''
72              prLong = ''
73            }
74          } catch {
75            prText = ''
76            prLong = ''
77          }
78        }
79        await update($, repoAtom, () => ({ branch, ahead, behind, changed: lines.length - 1, pr: prText, prLong }))
80      } catch {
81        // git missing or timed out: keep the last reading
82      }
83    }
84    await poll()
85    $.clock.every(5000, poll)
86    return next(e)
87  })
88
89  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
90    const below = await next(e)
91    const repo = await read($, repoAtom)
92    if (e.props.hasSurvey || !repo) return below
93
94    const { Box, Text } = $.ui.resolve(e)
95    const isWide = e.props.bodyColumns >= WIDE
96    const dirty = repo.changed > 0
97    const prColor = repo.pr.includes('✗') ? '#ef4444' : repo.pr.includes('…') ? '#f59e0b' : '#22c55e'
98
99    const zone = (
100      <Box gap={2}>
101        <Text bold color="#a855f7">{repo.branch}</Text>
102        {repo.ahead > 0 || repo.behind > 0 || isWide ? <Text dimColor>↑{repo.ahead} ↓{repo.behind}</Text> : null}
103        <Text color={dirty ? '#f59e0b' : '#22c55e'}>{dirty ? `● ${repo.changed} changed` : '✓ clean'}</Text>
104        {repo.pr ? <Text color={prColor}>{isWide && repo.prLong ? `${repo.pr.split(' ').slice(0, 2).join(' ')} · ${repo.prLong}` : repo.pr}</Text> : null}
105      </Box>
106    )
107    return addZone(below, zone, 1)
108  })
109}
110
types/index.d.ts 15 lines
1export type Repo = {
2  branch: string
3  ahead: number
4  behind: number
5  changed: number
6  pr: string
7  prLong: string
8}
9
10declare module 'claude-code' {
11  interface PluginState {
12    'repo-status': { repo: Repo | null }
13  }
14}
15