From 6a5c58b83907a0a526eab9034bc774bf941d6362 Mon Sep 17 00:00:00 2001 From: Jamie Pond Date: Tue, 10 Feb 2026 17:37:16 -0800 Subject: [PATCH 1/3] feat: add PR link to GitStatus UI New /api/projects/:id/pr endpoint fetches PR info via gh CLI. GitStatus shows clickable PR link in dropdown and icon in compact badge. Co-Authored-By: Claude Opus 4.6 --- client/src/components/GitStatus.tsx | 72 +++++++++++++++++++++++++++-- server.ts | 34 ++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/client/src/components/GitStatus.tsx b/client/src/components/GitStatus.tsx index afcbc80..e7fb5e2 100644 --- a/client/src/components/GitStatus.tsx +++ b/client/src/components/GitStatus.tsx @@ -19,6 +19,13 @@ interface GitStatusData { branches: string[]; } +interface PrData { + url: string; + number: number; + title: string; + state: string; +} + // Git status code to color/label function fileStatusColor(status: string): string { if (status.includes("M")) return "text-yellow-300"; @@ -64,6 +71,27 @@ export default function GitStatus({ const [creating, setCreating] = useState(false); const [createError, setCreateError] = useState(null); const [deleting, setDeleting] = useState(false); + const [pr, setPr] = useState(null); + + const fetchPr = useCallback(async () => { + if (!projectId) { + setPr(null); + return; + } + try { + const res = await apiFetch( + `/api/projects/${encodeURIComponent(projectId)}/pr`, + { serverId, serverUrl }, + ); + if (!res.ok) { + setPr(null); + return; + } + setPr(await res.json()); + } catch { + setPr(null); + } + }, [projectId, serverId, serverUrl]); const fetchStatus = useCallback(async () => { if (!projectId) { @@ -100,14 +128,18 @@ export default function GitStatus({ // Fetch on mount and when projectId changes useEffect(() => { fetchStatus(); - }, [fetchStatus]); + fetchPr(); + }, [fetchStatus, fetchPr]); // Refresh periodically (every 30s) useEffect(() => { if (!projectId) return; - const interval = setInterval(fetchStatus, 30000); + const interval = setInterval(() => { + fetchStatus(); + fetchPr(); + }, 30000); return () => clearInterval(interval); - }, [projectId, fetchStatus]); + }, [projectId, fetchStatus, fetchPr]); // Reset worktree create state when dropdown closes useEffect(() => { @@ -240,6 +272,13 @@ export default function GitStatus({ ↓{status.behind} )} + {pr && ( + + + + + + )} {/* Expanded details dropdown */} @@ -320,6 +359,32 @@ export default function GitStatus({ )} )} + + {/* PR link */} + {pr && ( +
+ + + + e.stopPropagation()} + className="inline-flex items-center gap-1 text-sm text-[var(--color-accent)] hover:text-[#d97a5a] underline decoration-[var(--color-accent-muted)] hover:decoration-[#d97a5a] underline-offset-2" + > + PR #{pr.number} + + + + + {pr.state !== "OPEN" && ( + + {pr.state.toLowerCase()} + + )} +
+ )} {/* Changed files list */} @@ -352,6 +417,7 @@ export default function GitStatus({ onClick={(e) => { e.stopPropagation(); fetchStatus(); + fetchPr(); }} className="w-full flex items-center justify-center gap-2 px-3 py-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] rounded transition-colors" > diff --git a/server.ts b/server.ts index 7d5ea89..15a1b89 100644 --- a/server.ts +++ b/server.ts @@ -775,6 +775,40 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse) { } } + // API: Get PR info for a project's current branch + if ( + pathname?.startsWith("/api/projects/") && + pathname.endsWith("/pr") && + method === "GET" + ) { + const projectId = decodeURIComponent( + pathname.split("/api/projects/")[1].replace("/pr", ""), + ); + if (!validateProjectId(projectId)) + return json(res, { error: "Invalid project ID" }, 400); + const project = getProject(projectId); + if (!project) { + return json(res, { error: "Project not found" }, 404); + } + + try { + const prJson = execSync( + "gh pr view --json url,number,title,state", + { + cwd: project.path, + encoding: "utf-8", + timeout: 10000, + }, + ).trim(); + const pr = JSON.parse(prJson); + console.log(`[api] PR info for ${projectId}: #${pr.number} (${pr.state})`); + return json(res, pr); + } catch { + console.log(`[api] No PR found for ${projectId}`); + return json(res, { error: "No PR found" }, 404); + } + } + // API: Worktree management if ( pathname?.startsWith("/api/projects/") && From a08a063850d19b2101823a3df6a4bfd2c15ef215 Mon Sep 17 00:00:00 2001 From: Jamie Pond Date: Tue, 10 Feb 2026 17:46:30 -0800 Subject: [PATCH 2/3] fix: move PR badge next to branch button as separate clickable link Co-Authored-By: Claude Opus 4.6 --- client/src/components/GitStatus.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/client/src/components/GitStatus.tsx b/client/src/components/GitStatus.tsx index e7fb5e2..59a7475 100644 --- a/client/src/components/GitStatus.tsx +++ b/client/src/components/GitStatus.tsx @@ -227,7 +227,7 @@ export default function GitStatus({ } return ( -
+
+ {pr && ( + + + + + #{pr.number} + + )} {/* Expanded details dropdown */} {expanded && ( From a0d9490e304827a51637af3907007b6899b00171 Mon Sep 17 00:00:00 2001 From: Jamie Pond Date: Tue, 10 Feb 2026 18:05:41 -0800 Subject: [PATCH 3/3] style: fix prettier formatting Co-Authored-By: Claude Opus 4.6 --- client/src/components/GitStatus.tsx | 28 +++++++++++++++++++++++----- server.ts | 17 ++++++++--------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/client/src/components/GitStatus.tsx b/client/src/components/GitStatus.tsx index 59a7475..a4b13a3 100644 --- a/client/src/components/GitStatus.tsx +++ b/client/src/components/GitStatus.tsx @@ -282,7 +282,10 @@ export default function GitStatus({ title={`PR #${pr.number}: ${pr.title}`} > - + #{pr.number} @@ -370,8 +373,15 @@ export default function GitStatus({ {/* PR link */} {pr && (
- - + + PR #{pr.number} - - + + {pr.state !== "OPEN" && ( diff --git a/server.ts b/server.ts index 15a1b89..1057faa 100644 --- a/server.ts +++ b/server.ts @@ -792,16 +792,15 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse) { } try { - const prJson = execSync( - "gh pr view --json url,number,title,state", - { - cwd: project.path, - encoding: "utf-8", - timeout: 10000, - }, - ).trim(); + const prJson = execSync("gh pr view --json url,number,title,state", { + cwd: project.path, + encoding: "utf-8", + timeout: 10000, + }).trim(); const pr = JSON.parse(prJson); - console.log(`[api] PR info for ${projectId}: #${pr.number} (${pr.state})`); + console.log( + `[api] PR info for ${projectId}: #${pr.number} (${pr.state})`, + ); return json(res, pr); } catch { console.log(`[api] No PR found for ${projectId}`);