feat: add OpenPR button for GitHub pull request creation (#132)

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* improve colors and icons

* drop log

* cr
This commit is contained in:
Brace Sproul 2025-06-12 10:46:16 -07:00 • committed by GitHub
parent 4c29af3849
commit 4fbd1f4861
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 124 additions and 0 deletions

View file

@ -0,0 +1,80 @@
"use client";
import { useStreamContext } from "@/providers/Stream";
import { TooltipIconButton } from "@/components/ui/tooltip-icon-button";
import { useQueryState } from "nuqs";
import {
GitPullRequest,
GitMerge,
GitPullRequestClosed,
GitPullRequestDraft,
} from "lucide-react";
import { useEffect, useState } from "react";
import { getPullRequest } from "@/utils/github";
import { cn } from "@/lib/utils";
export function OpenPRButton() {
const stream = useStreamContext();
const [branch] = useQueryState("branch");
const [prState, setPrState] = useState<
"open" | "closed" | "draft" | "merged"
>();
const [pullRequest, setPullRequest] = useState<Record<string, any>>();
useEffect(() => {
const baseBranch =
stream.values.targetRepository?.branch ??
stream.values.targetRepository?.baseCommit ??
branch;
if (
!stream.values.branchName ||
!stream.values.targetRepository ||
!baseBranch
) {
return;
}
getPullRequest({
owner: stream.values.targetRepository.owner,
repo: stream.values.targetRepository.repo,
baseBranch: baseBranch,
headBranch: stream.values.branchName,
}).then((pr) => {
if (!pr) return;
setPullRequest(pr);
if (pr.merged_at) {
setPrState("merged");
} else if (pr.draft) {
setPrState("draft");
} else {
setPrState(pr.state);
}
});
}, [stream.values?.branchName, stream.values.targetRepository, branch]);
if (!pullRequest) {
return null;
}
const handleOpenPR = () => {
window.open(pullRequest.html_url, "_blank", "noopener,noreferrer");
};
return (
<TooltipIconButton
tooltip={`Open Pull Request #${pullRequest.number}`}
variant="ghost"
onClick={handleOpenPR}
className={cn({
"text-green-500 hover:text-green-600": prState === "open",
"text-red-500 hover:text-red-600": prState === "closed",
"text-gray-500 hover:text-gray-600": prState === "draft",
"text-[#8957e5] hover:text-[#7847d1]": prState === "merged",
})}
>
{prState === "merged" && <GitMerge className="size-4" />}
{prState === "draft" && <GitPullRequestDraft className="size-4" />}
{prState === "closed" && <GitPullRequestClosed className="size-4" />}
{prState === "open" && <GitPullRequest className="size-4" />}
</TooltipIconButton>
);
}

View file

@ -45,6 +45,7 @@ import { ConfigurationSidebar } from "../configuration-sidebar";
import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/useConfigStore";
import { RepositoryBranchSelectors } from "../github/repo-branch-selectors";
import { useRouter } from "next/navigation";
import { OpenPRButton } from "../github/open-pr-button";
import {
Tooltip,
TooltipContent,
@ -115,6 +116,7 @@ export function Thread() {
"chatHistoryOpen",
parseAsBoolean.withDefault(false),
);
const [baseBranch, setBaseBranch] = useQueryState("base-branch");
const [configSidebarOpen, setConfigSidebarOpen] = useState(false);
@ -446,6 +448,7 @@ export function Thread() {
<div className="col-span-2 flex items-center justify-end gap-2 text-gray-700">
<GitHubOAuthButton />
<OpenPRButton />
<TooltipIconButton
tooltip="Configuration"
variant="ghost"

View file

@ -238,3 +238,44 @@ export interface Branch {
};
protected: boolean;
}
/**
* Fetches a pull request for a specific repository using OAuth access token
*/
export async function getPullRequest(inputs: {
owner: string;
repo: string;
baseBranch: string;
headBranch: string;
}) {
try {
const response = await fetch(
`${getBaseApiUrl()}github/proxy/repos/${inputs.owner}/${inputs.repo}/pulls?base=${inputs.baseBranch}&head=${inputs.headBranch}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "OpenSWE-Agent",
},
},
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Failed to fetch pull request: ${JSON.stringify(errorData)}`,
);
}
const data = await response.json();
if (data && Array.isArray(data) && data.length > 0) {
return data.filter((d) => d.head?.ref === inputs.headBranch)[0];
}
return data?.[0];
} catch (e) {
console.error("Failed to get pull request", {
inputs,
error: e,
});
return null;
}
}