pi-pr-status 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Garcia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # pi-pr-status
2
+
3
+ A [Pi](https://github.com/badlogic/pi) extension that shows your current PR's status right in the footer — so you always know which PR you're working on, whether CI is green, and if there are review comments to address.
4
+
5
+ ## What it shows
6
+
7
+ When your current git branch has an open pull request, the footer displays:
8
+
9
+ ```
10
+ 🟢 PR #42 · ✅ 5 checks passed · https://github.com/owner/repo/pull/42
11
+ ```
12
+
13
+ **CI failures?**
14
+ ```
15
+ 🟢 PR #42 · ❌ 2/5 checks failed · https://github.com/owner/repo/pull/42
16
+ ```
17
+
18
+ **Checks still running?**
19
+ ```
20
+ 🟢 PR #42 · ⏳ 3/5 checks pending · https://github.com/owner/repo/pull/42
21
+ ```
22
+
23
+ **Unresolved review comments?**
24
+ ```
25
+ 🟢 PR #42 · ✅ 5 checks passed · 💬 3 unresolved · https://github.com/owner/repo/pull/42
26
+ ```
27
+
28
+ **PR state icons:**
29
+
30
+ | Icon | State |
31
+ |------|-------|
32
+ | 🟢 | Open |
33
+ | 🟣 | Merged |
34
+ | 🔴 | Closed |
35
+
36
+ When there's no PR for the current branch, nothing is shown.
37
+
38
+ ## Requirements
39
+
40
+ - [Pi](https://github.com/badlogic/pi) coding agent
41
+ - [GitHub CLI](https://cli.github.com/) (`gh`) — authenticated with `gh auth login`
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pi install npm:pi-pr-status
47
+ ```
48
+
49
+ Or try it without installing:
50
+
51
+ ```bash
52
+ pi -e npm:pi-pr-status
53
+ ```
54
+
55
+ You can also install from git:
56
+
57
+ ```bash
58
+ pi install git:github.com/bruno-garcia/pi-pr-status
59
+ ```
60
+
61
+ ## How it works
62
+
63
+ 1. Detects the current git branch
64
+ 2. Runs `gh pr view` to find the associated pull request
65
+ 3. Parses CI status check results (pass / fail / pending)
66
+ 4. Queries unresolved review threads via the GitHub GraphQL API
67
+ 5. Displays everything in the pi footer status bar
68
+
69
+ The extension polls every 30 seconds to pick up CI and review changes. Repo metadata is cached so only two API calls are made per poll (one for PR + checks, one GraphQL query for review threads). When no PR exists for the branch, no API calls are made after the initial check.
70
+
71
+ ## Development
72
+
73
+ ```bash
74
+ npm install
75
+ npm test
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,66 @@
1
+ /**
2
+ * PR Status Extension
3
+ *
4
+ * Shows the current branch's PR URL, CI check status, and unresolved
5
+ * review comment count in the pi footer status bar. Polls every 30 seconds.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { getBranch, getRepoInfo, getPrForBranch, formatStatus, type PrInfo, type RepoInfo } from "../lib/github.ts";
10
+
11
+ const POLL_INTERVAL = 30_000;
12
+ const STATUS_KEY = "pr-status";
13
+
14
+ export default function (pi: ExtensionAPI) {
15
+ let timer: ReturnType<typeof setInterval> | undefined;
16
+ let lastBranch: string | undefined;
17
+ let lastPr: PrInfo | undefined;
18
+ let cachedRepo: RepoInfo | undefined;
19
+
20
+ function update(cwd: string, ui: { setStatus: (key: string, value: string | undefined) => void }) {
21
+ const branch = getBranch(cwd);
22
+
23
+ if (branch !== lastBranch) {
24
+ lastBranch = branch;
25
+ lastPr = undefined;
26
+ }
27
+
28
+ if (!branch || branch === "HEAD") {
29
+ lastPr = undefined;
30
+ ui.setStatus(STATUS_KEY, undefined);
31
+ return;
32
+ }
33
+
34
+ if (!cachedRepo) {
35
+ cachedRepo = getRepoInfo(cwd);
36
+ }
37
+
38
+ const pr = getPrForBranch(cwd, cachedRepo);
39
+ lastPr = pr ?? undefined;
40
+
41
+ if (lastPr) {
42
+ ui.setStatus(STATUS_KEY, formatStatus(lastPr));
43
+ } else {
44
+ ui.setStatus(STATUS_KEY, undefined);
45
+ }
46
+ }
47
+
48
+ pi.on("session_start", async (_event, ctx) => {
49
+ update(ctx.cwd, ctx.ui);
50
+ timer = setInterval(() => update(ctx.cwd, ctx.ui), POLL_INTERVAL);
51
+ });
52
+
53
+ pi.on("session_switch", async (_event, ctx) => {
54
+ lastBranch = undefined;
55
+ lastPr = undefined;
56
+ cachedRepo = undefined;
57
+ update(ctx.cwd, ctx.ui);
58
+ });
59
+
60
+ pi.on("session_shutdown", async () => {
61
+ if (timer) {
62
+ clearInterval(timer);
63
+ timer = undefined;
64
+ }
65
+ });
66
+ }
package/lib/github.ts ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Pure logic for querying GitHub PR status via the `gh` CLI.
3
+ * Separated from the pi extension API for testability.
4
+ */
5
+
6
+ import { execSync } from "node:child_process";
7
+
8
+ export interface CheckStatus {
9
+ total: number;
10
+ pass: number;
11
+ fail: number;
12
+ pending: number;
13
+ }
14
+
15
+ export interface PrInfo {
16
+ number: number;
17
+ title: string;
18
+ url: string;
19
+ state: string;
20
+ checks: CheckStatus;
21
+ unresolvedThreads: number;
22
+ }
23
+
24
+ export interface RepoInfo {
25
+ owner: string;
26
+ name: string;
27
+ }
28
+
29
+ export function getBranch(cwd: string): string | undefined {
30
+ try {
31
+ return execSync("git rev-parse --abbrev-ref HEAD", {
32
+ cwd,
33
+ encoding: "utf-8",
34
+ timeout: 3000,
35
+ stdio: ["pipe", "pipe", "pipe"],
36
+ }).trim();
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ export function getRepoInfo(cwd: string): RepoInfo | undefined {
43
+ try {
44
+ const json = execSync("gh repo view --json owner,name", {
45
+ cwd,
46
+ encoding: "utf-8",
47
+ timeout: 5000,
48
+ stdio: ["pipe", "pipe", "pipe"],
49
+ }).trim();
50
+ const repo = JSON.parse(json);
51
+ return repo.owner?.login && repo.name ? { owner: repo.owner.login, name: repo.name } : undefined;
52
+ } catch {
53
+ return undefined;
54
+ }
55
+ }
56
+
57
+ export function parseChecks(statusCheckRollup: unknown[]): CheckStatus {
58
+ const checks: CheckStatus = { total: 0, pass: 0, fail: 0, pending: 0 };
59
+ for (const check of statusCheckRollup) {
60
+ const c = check as Record<string, string>;
61
+ checks.total++;
62
+ const conclusion = (c.conclusion || "").toUpperCase();
63
+ const status = (c.status || "").toUpperCase();
64
+ if (conclusion === "SUCCESS" || conclusion === "NEUTRAL" || conclusion === "SKIPPED") {
65
+ checks.pass++;
66
+ } else if (
67
+ conclusion === "FAILURE" ||
68
+ conclusion === "TIMED_OUT" ||
69
+ conclusion === "CANCELLED" ||
70
+ conclusion === "ACTION_REQUIRED"
71
+ ) {
72
+ checks.fail++;
73
+ } else if (
74
+ status === "IN_PROGRESS" ||
75
+ status === "QUEUED" ||
76
+ status === "PENDING" ||
77
+ status === "WAITING" ||
78
+ !conclusion
79
+ ) {
80
+ checks.pending++;
81
+ }
82
+ }
83
+ return checks;
84
+ }
85
+
86
+ export function countUnresolvedThreads(threads: { isResolved: boolean }[]): number {
87
+ return threads.filter((t) => !t.isResolved).length;
88
+ }
89
+
90
+ export function getPrForBranch(cwd: string, repo?: RepoInfo): PrInfo | undefined {
91
+ try {
92
+ const json = execSync("gh pr view --json number,title,url,state,statusCheckRollup", {
93
+ cwd,
94
+ encoding: "utf-8",
95
+ timeout: 10_000,
96
+ stdio: ["pipe", "pipe", "pipe"],
97
+ }).trim();
98
+ if (!json) return undefined;
99
+ const pr = JSON.parse(json);
100
+ if (!pr.number || !pr.url) return undefined;
101
+
102
+ const checks = Array.isArray(pr.statusCheckRollup) ? parseChecks(pr.statusCheckRollup) : { total: 0, pass: 0, fail: 0, pending: 0 };
103
+
104
+ let unresolvedThreads = 0;
105
+ if (repo) {
106
+ try {
107
+ const gql = execSync(
108
+ `gh api graphql -f query='{ repository(owner: "${repo.owner}", name: "${repo.name}") { pullRequest(number: ${pr.number}) { reviewThreads(first: 100) { nodes { isResolved } } } } }'`,
109
+ {
110
+ cwd,
111
+ encoding: "utf-8",
112
+ timeout: 10_000,
113
+ stdio: ["pipe", "pipe", "pipe"],
114
+ },
115
+ ).trim();
116
+ const data = JSON.parse(gql);
117
+ const threads = data?.data?.repository?.pullRequest?.reviewThreads?.nodes;
118
+ if (Array.isArray(threads)) {
119
+ unresolvedThreads = countUnresolvedThreads(threads);
120
+ }
121
+ } catch {
122
+ // GraphQL failed — show PR without thread count
123
+ }
124
+ }
125
+
126
+ return {
127
+ number: pr.number,
128
+ title: pr.title,
129
+ url: pr.url,
130
+ state: pr.state,
131
+ checks,
132
+ unresolvedThreads,
133
+ };
134
+ } catch {
135
+ return undefined;
136
+ }
137
+ }
138
+
139
+ export function formatStatus(pr: PrInfo): string {
140
+ const stateIcon = pr.state === "MERGED" ? "🟣" : pr.state === "CLOSED" ? "🔴" : "🟢";
141
+ const parts: string[] = [`${stateIcon} PR #${pr.number}`];
142
+
143
+ if (pr.checks.total > 0) {
144
+ if (pr.checks.fail > 0) {
145
+ parts.push(`❌ ${pr.checks.fail}/${pr.checks.total} checks failed`);
146
+ } else if (pr.checks.pending > 0) {
147
+ parts.push(`⏳ ${pr.checks.pending}/${pr.checks.total} checks pending`);
148
+ } else {
149
+ parts.push(`✅ ${pr.checks.total} checks passed`);
150
+ }
151
+ }
152
+
153
+ if (pr.unresolvedThreads > 0) {
154
+ parts.push(`💬 ${pr.unresolvedThreads} unresolved`);
155
+ }
156
+
157
+ parts.push(pr.url);
158
+ return parts.join(" · ");
159
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "pi-pr-status",
3
+ "version": "0.1.0",
4
+ "description": "A Pi extension that shows the current PR link, CI check status, and unresolved review comments in the footer status bar",
5
+ "author": "Bruno Garcia",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/bruno-garcia/pi-pr-status#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/bruno-garcia/pi-pr-status"
11
+ },
12
+ "keywords": ["pi-package", "github", "pull-request", "ci", "pi-extension"],
13
+ "files": [
14
+ "extensions/pr-status.ts",
15
+ "lib/github.ts",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "peerDependencies": {
20
+ "@mariozechner/pi-coding-agent": "*"
21
+ },
22
+ "devDependencies": {
23
+ "@mariozechner/pi-coding-agent": "^0.52.12",
24
+ "vitest": "^3.0.0"
25
+ },
26
+ "pi": {
27
+ "extensions": ["./extensions"]
28
+ },
29
+ "scripts": {
30
+ "test": "vitest run",
31
+ "test:watch": "vitest"
32
+ }
33
+ }