diff --git a/client/src/components/GitStatus.tsx b/client/src/components/GitStatus.tsx index afcbc80..a4b13a3 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(() => { @@ -195,7 +227,7 @@ export default function GitStatus({ } return ( -
+
+ {pr && ( + + + + + #{pr.number} + + )} {/* Expanded details dropdown */} {expanded && ( @@ -320,6 +369,47 @@ 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 +442,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..1057faa 100644 --- a/server.ts +++ b/server.ts @@ -775,6 +775,39 @@ 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/") &&