mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
feat: add PR review link route (#1698)
* feat: add PR review link route Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: avoid duplicate review shortcut runs Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> (cherry picked from commit 52fe29168814d7936ee6612b9c81f33339bb05b0)
This commit is contained in:
parent
5964a9f0d4
commit
a6fb7fb16b
2 changed files with 232 additions and 0 deletions
|
|
@ -31,6 +31,7 @@ import { Route as ReviewRepositoriesOwnerRouteImport } from './routes/review_.re
|
|||
import { Route as AgentsAutomationsNewRouteImport } from './routes/agents/automations/new'
|
||||
import { Route as AgentsAutomationsScheduleIdRouteImport } from './routes/agents/automations/$scheduleId'
|
||||
import { Route as AgentsThreadIdPlanRouteImport } from './routes/agents/$threadId_.plan'
|
||||
import { Route as OwnerRepoPullNumberRouteImport } from './routes/$owner.$repo.pull.$number'
|
||||
import { Route as AgentsReviewsOwnerRepoNumberRouteImport } from './routes/agents/reviews/$owner.$repo.$number'
|
||||
|
||||
const UsageRoute = UsageRouteImport.update({
|
||||
|
|
@ -144,6 +145,11 @@ const AgentsThreadIdPlanRoute = AgentsThreadIdPlanRouteImport.update({
|
|||
path: '/$threadId/plan',
|
||||
getParentRoute: () => AgentsRoute,
|
||||
} as any)
|
||||
const OwnerRepoPullNumberRoute = OwnerRepoPullNumberRouteImport.update({
|
||||
id: '/$owner/$repo/pull/$number',
|
||||
path: '/$owner/$repo/pull/$number',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AgentsReviewsOwnerRepoNumberRoute =
|
||||
AgentsReviewsOwnerRepoNumberRouteImport.update({
|
||||
id: '/reviews/$owner/$repo/$number',
|
||||
|
|
@ -174,6 +180,7 @@ export interface FileRoutesByFullPath {
|
|||
'/review/repositories/$owner': typeof ReviewRepositoriesOwnerRoute
|
||||
'/agents/automations/': typeof AgentsAutomationsIndexRoute
|
||||
'/agents/reviews/': typeof AgentsReviewsIndexRoute
|
||||
'/$owner/$repo/pull/$number': typeof OwnerRepoPullNumberRoute
|
||||
'/agents/reviews/$owner/$repo/$number': typeof AgentsReviewsOwnerRepoNumberRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
|
|
@ -198,6 +205,7 @@ export interface FileRoutesByTo {
|
|||
'/review/repositories/$owner': typeof ReviewRepositoriesOwnerRoute
|
||||
'/agents/automations': typeof AgentsAutomationsIndexRoute
|
||||
'/agents/reviews': typeof AgentsReviewsIndexRoute
|
||||
'/$owner/$repo/pull/$number': typeof OwnerRepoPullNumberRoute
|
||||
'/agents/reviews/$owner/$repo/$number': typeof AgentsReviewsOwnerRepoNumberRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
|
|
@ -224,6 +232,7 @@ export interface FileRoutesById {
|
|||
'/review_/repositories/$owner': typeof ReviewRepositoriesOwnerRoute
|
||||
'/agents/automations/': typeof AgentsAutomationsIndexRoute
|
||||
'/agents/reviews/': typeof AgentsReviewsIndexRoute
|
||||
'/$owner/$repo/pull/$number': typeof OwnerRepoPullNumberRoute
|
||||
'/agents/reviews/$owner/$repo/$number': typeof AgentsReviewsOwnerRepoNumberRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
|
|
@ -251,6 +260,7 @@ export interface FileRouteTypes {
|
|||
| '/review/repositories/$owner'
|
||||
| '/agents/automations/'
|
||||
| '/agents/reviews/'
|
||||
| '/$owner/$repo/pull/$number'
|
||||
| '/agents/reviews/$owner/$repo/$number'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
|
|
@ -275,6 +285,7 @@ export interface FileRouteTypes {
|
|||
| '/review/repositories/$owner'
|
||||
| '/agents/automations'
|
||||
| '/agents/reviews'
|
||||
| '/$owner/$repo/pull/$number'
|
||||
| '/agents/reviews/$owner/$repo/$number'
|
||||
id:
|
||||
| '__root__'
|
||||
|
|
@ -300,6 +311,7 @@ export interface FileRouteTypes {
|
|||
| '/review_/repositories/$owner'
|
||||
| '/agents/automations/'
|
||||
| '/agents/reviews/'
|
||||
| '/$owner/$repo/pull/$number'
|
||||
| '/agents/reviews/$owner/$repo/$number'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
|
@ -318,6 +330,7 @@ export interface RootRouteChildren {
|
|||
AgentsSnapshotsRoute: typeof AgentsSnapshotsRoute
|
||||
ReviewStylesRoute: typeof ReviewStylesRoute
|
||||
ReviewRepositoriesOwnerRoute: typeof ReviewRepositoriesOwnerRoute
|
||||
OwnerRepoPullNumberRoute: typeof OwnerRepoPullNumberRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
|
|
@ -476,6 +489,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof AgentsThreadIdPlanRouteImport
|
||||
parentRoute: typeof AgentsRoute
|
||||
}
|
||||
'/$owner/$repo/pull/$number': {
|
||||
id: '/$owner/$repo/pull/$number'
|
||||
path: '/$owner/$repo/pull/$number'
|
||||
fullPath: '/$owner/$repo/pull/$number'
|
||||
preLoaderRoute: typeof OwnerRepoPullNumberRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/agents/reviews/$owner/$repo/$number': {
|
||||
id: '/agents/reviews/$owner/$repo/$number'
|
||||
path: '/reviews/$owner/$repo/$number'
|
||||
|
|
@ -528,6 +548,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||
AgentsSnapshotsRoute: AgentsSnapshotsRoute,
|
||||
ReviewStylesRoute: ReviewStylesRoute,
|
||||
ReviewRepositoriesOwnerRoute: ReviewRepositoriesOwnerRoute,
|
||||
OwnerRepoPullNumberRoute: OwnerRepoPullNumberRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
|
|
|||
211
ui/src/routes/$owner.$repo.pull.$number.tsx
Normal file
211
ui/src/routes/$owner.$repo.pull.$number.tsx
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { Link, Navigate, createFileRoute } from "@tanstack/react-router"
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { ArrowSquareOutIcon, GitPullRequestIcon } from "@phosphor-icons/react"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { api } from "@/lib/api"
|
||||
import { RequireLogin } from "@/lib/auth-redirect"
|
||||
import { useSession } from "@/lib/session"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export const Route = createFileRoute("/$owner/$repo/pull/$number")({
|
||||
component: PullRequestReviewLinkPage,
|
||||
})
|
||||
|
||||
function PullRequestReviewLinkPage() {
|
||||
const { owner, repo, number } = Route.useParams()
|
||||
const prNumber = Number(number)
|
||||
const session = useSession()
|
||||
const stableReviewPath = `/agents/reviews/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${prNumber}`
|
||||
const githubPrUrl = useMemo(
|
||||
() => `https://github.com/${owner}/${repo}/pull/${number}`,
|
||||
[owner, repo, number]
|
||||
)
|
||||
const existingReview = useQuery({
|
||||
queryKey: ["review", owner, repo, prNumber],
|
||||
queryFn: () => api.getReview(owner, repo, prNumber),
|
||||
enabled: !!session.data && Number.isFinite(prNumber),
|
||||
retry: false,
|
||||
})
|
||||
const triggerRef = useRef<string | null>(null)
|
||||
const triggerReview = useMutation({
|
||||
mutationFn: () => api.reReview(owner, repo, prNumber),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!session.data || !Number.isFinite(prNumber)) return
|
||||
if (existingReview.isLoading || existingReview.data?.status === "running") {
|
||||
return
|
||||
}
|
||||
const key = `${owner}/${repo}#${prNumber}`
|
||||
if (triggerRef.current === key) return
|
||||
triggerRef.current = key
|
||||
triggerReview.mutate()
|
||||
}, [
|
||||
existingReview.data?.status,
|
||||
existingReview.isLoading,
|
||||
owner,
|
||||
repo,
|
||||
prNumber,
|
||||
session.data,
|
||||
triggerReview,
|
||||
])
|
||||
|
||||
if (session.isLoading) {
|
||||
return (
|
||||
<main className="flex min-h-svh items-center justify-center p-6">
|
||||
<Skeleton className="h-52 w-full max-w-lg" />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (!session.data) return <RequireLogin />
|
||||
|
||||
if (!Number.isFinite(prNumber)) {
|
||||
return (
|
||||
<ReviewLinkCard
|
||||
title="Invalid pull request link"
|
||||
description="Expected a GitHub-style pull request path like /owner/repo/pull/123."
|
||||
owner={owner}
|
||||
repo={repo}
|
||||
number={number}
|
||||
githubPrUrl={githubPrUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (existingReview.data?.status === "running" || triggerReview.isSuccess) {
|
||||
return (
|
||||
<Navigate
|
||||
to="/agents/reviews/$owner/$repo/$number"
|
||||
params={{ owner, repo, number: String(prNumber) }}
|
||||
replace
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (triggerReview.isError) {
|
||||
return (
|
||||
<ReviewLinkCard
|
||||
title="Could not start review"
|
||||
description={triggerReview.error.message}
|
||||
owner={owner}
|
||||
repo={repo}
|
||||
number={number}
|
||||
githubPrUrl={githubPrUrl}
|
||||
stableReviewPath={stableReviewPath}
|
||||
onRetry={() => triggerReview.mutate()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const isCheckingExistingReview = existingReview.isLoading
|
||||
|
||||
return (
|
||||
<ReviewLinkCard
|
||||
title={
|
||||
isCheckingExistingReview
|
||||
? "Checking review status"
|
||||
: "Starting Open SWE review"
|
||||
}
|
||||
description={
|
||||
isCheckingExistingReview
|
||||
? "This PR link was recognized. Open SWE is checking whether a review is already running."
|
||||
: "Open SWE is starting a review and will redirect you to the stable review page."
|
||||
}
|
||||
owner={owner}
|
||||
repo={repo}
|
||||
number={number}
|
||||
githubPrUrl={githubPrUrl}
|
||||
stableReviewPath={stableReviewPath}
|
||||
loading
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ReviewLinkCard({
|
||||
title,
|
||||
description,
|
||||
owner,
|
||||
repo,
|
||||
number,
|
||||
githubPrUrl,
|
||||
stableReviewPath,
|
||||
loading = false,
|
||||
onRetry,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
owner: string
|
||||
repo: string
|
||||
number: string
|
||||
githubPrUrl: string
|
||||
stableReviewPath?: string
|
||||
loading?: boolean
|
||||
onRetry?: () => void
|
||||
}) {
|
||||
return (
|
||||
<main className="flex min-h-svh items-center justify-center bg-background p-6 text-foreground">
|
||||
<Card className="w-full max-w-lg">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-xl">
|
||||
<GitPullRequestIcon className="size-5 text-muted-foreground" />
|
||||
{title}
|
||||
</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-lg border border-border bg-muted/40 p-3 text-sm">
|
||||
<div className="font-medium">
|
||||
{owner}/{repo} #{number}
|
||||
</div>
|
||||
<a
|
||||
href={githubPrUrl}
|
||||
className="mt-1 inline-flex items-center gap-1 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
View on GitHub
|
||||
<ArrowSquareOutIcon className="size-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
{loading && <Skeleton className="mt-4 h-2 w-full" />}
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-wrap gap-2">
|
||||
{onRetry && (
|
||||
<button
|
||||
type="button"
|
||||
className={buttonVariants()}
|
||||
onClick={onRetry}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
)}
|
||||
{stableReviewPath && (
|
||||
<Link
|
||||
to="/agents/reviews/$owner/$repo/$number"
|
||||
params={{ owner, repo, number }}
|
||||
className={cn(buttonVariants({ variant: "outline" }))}
|
||||
>
|
||||
Open stable review page
|
||||
</Link>
|
||||
)}
|
||||
<a
|
||||
href={githubPrUrl}
|
||||
className={cn(buttonVariants({ variant: "ghost" }))}
|
||||
>
|
||||
Open GitHub PR
|
||||
</a>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue