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

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
The band is hidden when the branch has no PR.
gh auth login)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.
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.
gh looks the same as "no PR": a hidden band.claude plugin validate .
claude plugin test .
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.
MIT
hooks/register.tsx 117 lines1import { 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}
117types/index.d.ts 18 lines1export 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