feat: clickable shared PR header in git panel and reviews (#1620)

* feat: clickable shared PR header in git panel and reviews

Replace the standalone "View PR" button in the agent git panel with a
clickable PR title, matching the reviews view. Extract a shared PrHeader
component reused by both the git panel and the review main body.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* refactor: drop PrHeader wrapper, use shared component directly

The review-side PrHeader was just a thin adapter mapping detail -> the
shared component's props. Inline it at the call site and use the shared
PrHeader directly so there's a single component.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Caroline di Vittorio 2026-06-26 12:54:51 -07:00 • committed by GitHub
parent 83cb40a01c
commit 85c0f63e29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 113 additions and 86 deletions

View file

@ -10,11 +10,9 @@ import {
useFileTreeSelection,
} from "@pierre/trees/react"
import {
ArrowSquareOutIcon,
ArrowsInIcon,
ArrowsOutIcon,
CaretDownIcon,
GitPullRequestIcon,
SidebarSimpleIcon,
} from "@phosphor-icons/react"
import type { FileContents } from "@pierre/diffs/react"
@ -26,6 +24,7 @@ import type { ChangedFileSummaryItem } from "@/components/agents/messages"
import { agentsApi } from "@/lib/agents/api"
import { useAgentThreadPrDiff } from "@/lib/agents/queries"
import { ReviewTab } from "@/components/agents/ReviewTab"
import { PrHeader } from "@/components/agents/PrHeader"
import { buttonVariants } from "@/components/ui/button"
import {
DIFF_VIRTUALIZER_CONFIG,
@ -518,38 +517,16 @@ export function AgentGitPanel({
) : (
<>
{pr && (
<div className="border-b border-[var(--ui-border)] px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-[var(--ui-text)]">
{pr.title} #{pr.number}
</div>
<div className="mt-1 flex items-center gap-2 text-[11px] text-[var(--ui-text-dim)]">
<span className="inline-flex items-center gap-1 rounded border border-[var(--ui-border)] px-1.5 py-0.5 capitalize">
<GitPullRequestIcon className="size-3" />
{pr.state}
</span>
<span>
{pr.headRef} → {pr.baseRef}
</span>
</div>
</div>
{pr.url && (
<a
href={pr.url}
target="_blank"
rel="noreferrer"
className={buttonVariants({
variant: "outline",
size: "sm",
})}
>
<ArrowSquareOutIcon className="size-3" />
View PR
</a>
)}
</div>
</div>
<PrHeader
className="border-b border-[var(--ui-border)] px-4 py-3"
url={pr.url}
title={pr.title}
number={pr.number}
state={pr.state}
headRef={pr.headRef}
baseRef={pr.baseRef}
titleClassName="truncate text-sm"
/>
)}
<div className="flex items-center gap-1 border-b border-[var(--ui-border)] px-3 py-2">

View file

@ -0,0 +1,88 @@
import { GitPullRequestIcon } from "@phosphor-icons/react"
import { cn } from "@/lib/utils"
const STATE_STYLES: Record<string, string> = {
open: "border-emerald-600/40 text-emerald-500",
draft: "border-border text-muted-foreground",
merged: "border-purple-600/40 text-purple-500",
closed: "border-red-600/40 text-red-500",
}
export interface PrHeaderProps {
url: string
title: string
state: string
headRef: string
baseRef: string
number?: number | null
author?: string | null
stats?: {
changedFiles: number
additions: number
deletions: number
} | null
className?: string
titleClassName?: string
}
export function PrHeader({
url,
title,
state,
headRef,
baseRef,
number,
author,
stats,
className,
titleClassName,
}: PrHeaderProps) {
return (
<div className={className}>
<span
className={cn(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] capitalize",
STATE_STYLES[state] ?? STATE_STYLES.open
)}
>
<GitPullRequestIcon className="size-3" />
{state}
</span>
<h1 className={cn("mt-2 text-base font-medium", titleClassName)}>
<a
href={url}
target="_blank"
rel="noreferrer"
className="hover:underline"
>
{title}
{number != null && (
<span className="text-muted-foreground"> #{number}</span>
)}
</a>
</h1>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
{author && (
<span className="font-medium text-foreground">{author}</span>
)}
<span className="rounded border border-border px-1.5 py-0.5 font-mono text-[11px]">
{baseRef}
</span>
<span>←</span>
<span className="rounded border border-border px-1.5 py-0.5 font-mono text-[11px]">
{headRef}
</span>
{stats && (
<>
<span>
{stats.changedFiles} file{stats.changedFiles === 1 ? "" : "s"}
</span>
<span className="text-emerald-500">+{stats.additions}</span>
<span className="text-red-500">-{stats.deletions}</span>
</>
)}
</div>
</div>
)
}

View file

@ -22,7 +22,6 @@ import {
CodeIcon,
CopyIcon,
FlagIcon,
GitPullRequestIcon,
InfoIcon,
LinkIcon,
ListBulletsIcon,
@ -68,6 +67,7 @@ import type {
import type { ChatAttachment } from "@/components/agents/ReviewChat"
import type { DiffStyle } from "@/components/agents/utils/diffUtils"
import { Markdown } from "@/components/agents/ported"
import { PrHeader } from "@/components/agents/PrHeader"
import {
ReviewChat,
ReviewChatComposerProvider,
@ -1120,7 +1120,19 @@ function ReviewBodyInner({
config={DIFF_VIRTUALIZER_CONFIG}
>
<div ref={scrollerProbe} aria-hidden className="hidden" />
<PrHeader detail={detail} />
<PrHeader
url={detail.url}
title={detail.pr.title}
state={detail.pr.state}
headRef={detail.pr.head_ref}
baseRef={detail.pr.base_ref}
author={detail.pr.author?.login}
stats={{
changedFiles: detail.pr.changed_files,
additions: detail.pr.additions,
deletions: detail.pr.deletions,
}}
/>
<div className="mt-4 rounded-lg border border-border bg-card p-4">
{detail.pr.body ? (
<Markdown
@ -1256,56 +1268,6 @@ function DiffStyleButton({
)
}
function PrHeader({ detail }: { detail: ReviewDetail }) {
const { pr } = detail
const stateStyles: Record<string, string> = {
open: "border-emerald-600/40 text-emerald-500",
draft: "border-border text-muted-foreground",
merged: "border-purple-600/40 text-purple-500",
closed: "border-red-600/40 text-red-500",
}
return (
<div>
<span
className={cn(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] capitalize",
stateStyles[pr.state] ?? stateStyles.open
)}
>
<GitPullRequestIcon className="size-3" />
{pr.state}
</span>
<h1 className="mt-2 text-base font-medium">
<a
href={detail.url}
target="_blank"
rel="noreferrer"
className="hover:underline"
>
{pr.title}
</a>
</h1>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
{pr.author && (
<span className="font-medium text-foreground">{pr.author.login}</span>
)}
<span className="rounded border border-border px-1.5 py-0.5 font-mono text-[11px]">
{pr.base_ref}
</span>
<span>←</span>
<span className="rounded border border-border px-1.5 py-0.5 font-mono text-[11px]">
{pr.head_ref}
</span>
<span>
{pr.changed_files} file{pr.changed_files === 1 ? "" : "s"}
</span>
<span className="text-emerald-500">+{pr.additions}</span>
<span className="text-red-500">-{pr.deletions}</span>
</div>
</div>
)
}
function GroupHeader({ group }: { group: ResolvedGroup }) {
return (
<div className="flex items-center gap-2">