SLOPSHOPPER

pr-tracker

Band above the prompt with the current branch's PR review state, approvals, checks and conflicts

newbandtoastprocesstimer
v0.1.0MITupdated 2026-10-12deepr1tester-cloud/claude-pr-tracker
A shopper browsing a rack in a slop shop
README

claude-pr-tracker

A Claude Code mod that shows the pull request for your current branch in a band above the prompt:

PR #7 Add thing · ✗ Changes requested (1) · checks 1✓ 1✗ 2… · ⚠ merge conflicts
  • Review state (approved, changes requested, review pending) and approval count
  • Check counts: passed, failed, pending
  • Merge conflict warning
  • A toast when new comments or reviews arrive

The band is hidden when the branch has no PR.

Requirements

  • Claude Code with function-hook mods (written against 2.1.288; the mod API is early access and may change)
  • The GitHub CLI, logged in (gh auth login)

Install

git clone https://github.com/deepr1tester-cloud/claude-pr-tracker
claude --plugin-dir ./claude-pr-tracker

Run Claude Code from inside a repository whose branch has an open PR.

How it works

It runs gh pr view --json … at session start, every 60 seconds and after each turn, keeps a small summary in session state, and draws it. There is no GitHub API code or token handling of its own; everything goes through gh. All the logic is in hooks/register.tsx.

Limits

  • Counts every check on the PR, not only required ones.
  • Inline review comments are counted per review, not per comment.
  • Tracks the current branch's PR only.
  • A missing or logged-out gh looks the same as "no PR": a hidden band.

Develop

claude plugin validate .
claude plugin test .

Credit

Written from the feature list of the "PR Tracker" mod listed on claudemod.com, whose source repository was no longer available. This is an independent implementation and shares no code with it.

License

MIT

Source 2 files
hooks/register.tsx 117 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Pr } from '../types'
5
6const POLL_MS = 60_000
7const FIELDS =
8  'number,title,reviewDecision,latestReviews,statusCheckRollup,mergeable,comments,reviews'
9const PASSED = ['SUCCESS', 'NEUTRAL', 'SKIPPED']
10const WAITING = ['', 'PENDING', 'EXPECTED']
11const REVIEW: Record<string, [string, string]> = {
12  APPROVED: ['✓ Approved', 'green'],
13  CHANGES_REQUESTED: ['✗ Changes requested', 'red'],
14  REVIEW_REQUIRED: ['● Review pending', 'yellow'],
15}
16
17const pr = atom({ plugin: 'pr-tracker', key: 'pr' } as const, null)
18
19type Check = { conclusion?: string; state?: string }
20type Raw = {
21  number: number
22  title: string
23  reviewDecision: string
24  mergeable: string
25  latestReviews: { state: string }[]
26  statusCheckRollup: Check[] | null
27  comments: unknown[]
28  reviews: unknown[]
29}
30
31const summarize = (raw: Raw): Pr => {
32  // ponytail: counts every check, not only required ones; `gh pr checks --required` if that matters
33  const verdicts = (raw.statusCheckRollup ?? []).map(c => c.conclusion || c.state || '')
34
35  return {
36    number: raw.number,
37    title: raw.title,
38    review: raw.reviewDecision,
39    approvals: raw.latestReviews.filter(r => r.state === 'APPROVED').length,
40    pass: verdicts.filter(v => PASSED.includes(v)).length,
41    pending: verdicts.filter(v => WAITING.includes(v)).length,
42    fail: verdicts.filter(v => !PASSED.includes(v) && !WAITING.includes(v)).length,
43    hasConflict: raw.mergeable === 'CONFLICTING',
44    comments: raw.comments.length + raw.reviews.length,
45  }
46}
47
48// No PR for this branch, no repo, no gh, not logged in: all read as "nothing to show".
49const fetchPr = async ($: EngineInterface): Promise<Pr | null> => {
50  try {
51    const { exitCode, stdout } = await $.process.run(['gh', 'pr', 'view', '--json', FIELDS])
52
53    return exitCode === 0 ? summarize(JSON.parse(stdout) as Raw) : null
54  } catch {
55    return null
56  }
57}
58
59const poll = async ($: EngineInterface) => {
60  const now = await fetchPr($)
61  const before = await read($, pr)
62
63  if (now && before && now.number === before.number && now.comments > before.comments) {
64    $.ui.toast(`PR #${now.number}: ${now.comments - before.comments} new review comment(s)`)
65  }
66
67  await update($, pr, () => now)
68}
69
70export const register: Register = on => {
71  on('session.start', async ($, e, next) => {
72    void poll($)
73    $.clock.every(POLL_MS, () => void poll($))
74
75    return next(e)
76  })
77
78  // A turn often pushes or opens the PR: don't wait for the timer.
79  on('turn.complete', ($, e, next) => {
80    void poll($)
81
82    return next(e)
83  })
84
85  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
86    const now = await read($, pr)
87
88    if (e.props.hasSurvey || now === null) {
89      return next(e)
90    }
91
92    const { Box, Text } = $.ui.resolve(e)
93    const [label, color] = REVIEW[now.review] ?? ['No review yet', undefined]
94
95    return (
96      <Box>
97        <Text wrap="truncate-end">
98          <Text bold>PR #{now.number}</Text>
99          <Text dimColor> {now.title} · </Text>
100          <Text color={color}>
101            {label} ({now.approvals})
102          </Text>
103          <Text dimColor> · checks </Text>
104          <Text color="green">{now.pass}✓ </Text>
105          <Text color={now.fail > 0 ? 'red' : undefined} dimColor={now.fail === 0}>
106            {now.fail}✗{' '}
107          </Text>
108          <Text color={now.pending > 0 ? 'yellow' : undefined} dimColor={now.pending === 0}>
109            {now.pending}…
110          </Text>
111          {now.hasConflict && <Text color="red"> · ⚠ merge conflicts</Text>}
112        </Text>
113      </Box>
114    )
115  })
116}
117
types/index.d.ts 18 lines
1export type Pr = {
2  number: number
3  title: string
4  review: string
5  approvals: number
6  pass: number
7  fail: number
8  pending: number
9  hasConflict: boolean
10  comments: number
11}
12
13declare module 'claude-code' {
14  interface PluginState {
15    'pr-tracker': { pr: Pr | null }
16  }
17}
18