From d79ea8b15c7f1f58e242454db3a3e8f13ca942f7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 22 Mar 2026 19:07:01 +0700 Subject: [PATCH] fix(sidebar): fix PR hint for fork PRs, remove badge styling, add hover effect - Replace REST API PR lookup with `gh pr view` which handles fork PRs - Fix state comparison to use lowercase (matching server output) - Remove pill/badge styling, show plain icon + #number - Add brighter hover colors and underline for clickability hint --- .../src/components/sidebar-workspace-list.tsx | 32 ++-- .../src/hooks/use-checkout-pr-status-query.ts | 4 +- packages/server/src/utils/checkout-git.ts | 158 +++++------------- 3 files changed, 66 insertions(+), 128 deletions(-) diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 581fd1c3e..12992e434 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -104,6 +104,11 @@ const GITHUB_PR_STATE_COLORS: Record = { merged: "#a371f7", closed: "#f85149", }; +const GITHUB_PR_STATE_COLORS_HOVER: Record = { + open: "#56d364", + merged: "#bc8cff", + closed: "#ff7b72", +}; interface SidebarWorkspaceListProps { projects: SidebarProjectEntry[]; @@ -164,6 +169,9 @@ interface WorkspaceRowInnerProps { function WorkspacePrBadge({ hint }: { hint: PrHint }) { const color = GITHUB_PR_STATE_COLORS[hint.state]; + const hoverColor = GITHUB_PR_STATE_COLORS_HOVER[hint.state]; + const [isHovered, setIsHovered] = useState(false); + const activeColor = isHovered ? hoverColor : color; const handlePressIn = useCallback((event: GestureResponderEvent) => { event.stopPropagation(); @@ -184,17 +192,22 @@ function WorkspacePrBadge({ hint }: { hint: PrHint }) { hitSlop={4} onPressIn={handlePressIn} onPress={handlePress} + onPointerEnter={() => setIsHovered(true)} + onPointerLeave={() => setIsHovered(false)} style={({ pressed }) => [ styles.workspacePrBadge, - { - borderColor: color, - backgroundColor: `${color}1a`, - }, pressed && styles.workspacePrBadgePressed, ]} > - - + + #{hint.number} @@ -2160,10 +2173,6 @@ const styles = StyleSheet.create((theme) => ({ flexDirection: "row", alignItems: "center", gap: theme.spacing[1], - paddingHorizontal: theme.spacing[2], - paddingVertical: 3, - borderRadius: theme.borderRadius.full, - borderWidth: 1, }, workspacePrBadgePressed: { opacity: 0.82, @@ -2173,6 +2182,9 @@ const styles = StyleSheet.create((theme) => ({ fontWeight: theme.fontWeight.medium, lineHeight: 14, }, + workspacePrBadgeTextHovered: { + textDecorationLine: "underline", + }, workspaceCreatingText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, diff --git a/packages/app/src/hooks/use-checkout-pr-status-query.ts b/packages/app/src/hooks/use-checkout-pr-status-query.ts index 3e9e789a3..fd0c032ec 100644 --- a/packages/app/src/hooks/use-checkout-pr-status-query.ts +++ b/packages/app/src/hooks/use-checkout-pr-status-query.ts @@ -51,9 +51,9 @@ function selectWorkspacePrHint(payload: CheckoutPrStatusPayload): PrHint | null return { url: status.url, number, - state: status.isMerged || status.state === "MERGED" + state: status.isMerged || status.state === "merged" ? "merged" - : status.state === "OPEN" + : status.state === "open" ? "open" : "closed", }; diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 14b382afb..6685934ae 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -1745,61 +1745,6 @@ async function resolveGitHubRepo(cwd: string): Promise { return null; } -async function listPullRequestsForHead(options: { - cwd: string; - repo: string; - owner: string; - head: string; - state: "open" | "closed"; -}): Promise { - const { stdout } = await execFileAsync( - "gh", - [ - "api", - `repos/${options.repo}/pulls`, - "-X", - "GET", - "-F", - `head=${options.owner}:${options.head}`, - "-F", - `state=${options.state}`, - ], - { cwd: options.cwd }, - ); - const parsed = JSON.parse(stdout.trim()); - return Array.isArray(parsed) ? parsed : []; -} - -function buildPullRequestStatus(current: any, fallbackHead: string): PullRequestStatus | null { - if (!current || typeof current !== "object") { - return null; - } - const url = current.html_url ?? current.url; - const title = current.title; - if (typeof url !== "string" || typeof title !== "string" || !url || !title) { - return null; - } - - const mergedAt = - typeof current.merged_at === "string" && current.merged_at.trim().length > 0 - ? current.merged_at - : null; - const state = - mergedAt !== null - ? "merged" - : typeof current.state === "string" && current.state.trim().length > 0 - ? current.state - : ""; - - return { - url, - title, - state, - baseRefName: current.base?.ref ?? "", - headRefName: current.head?.ref ?? fallbackHead, - isMerged: mergedAt !== null, - }; -} export async function createPullRequest( cwd: string, @@ -1844,9 +1789,8 @@ export async function createPullRequest( export async function getPullRequestStatus(cwd: string): Promise { await requireGitRepo(cwd); - const repo = await resolveGitHubRepo(cwd); const head = await getCurrentBranch(cwd); - if (!repo || !head) { + if (!head) { return { status: null, githubFeaturesEnabled: false, @@ -1860,69 +1804,51 @@ export async function getPullRequestStatus(cwd: string): Promise 0 + ? pr.mergedAt + : null; + const state = + mergedAt !== null + ? "merged" + : typeof pr.state === "string" && pr.state.trim().length > 0 + ? pr.state.toLowerCase() + : ""; + return { + status: { + url: pr.url, + title: pr.title, + state, + baseRefName: pr.baseRefName ?? "", + headRefName: pr.headRefName ?? head, + isMerged: mergedAt !== null, + }, + githubFeaturesEnabled: true, + }; } catch (error) { if (isGhAuthError(error)) { - return { - status: null, - githubFeaturesEnabled: false, - }; + return { status: null, githubFeaturesEnabled: false }; + } + // gh pr view exits non-zero when no PR exists for the branch + const message = error instanceof Error ? error.message : String(error); + if (message.includes("no pull requests found") || message.includes("Could not resolve")) { + return { status: null, githubFeaturesEnabled: true }; } throw error; } - const openPull = openPulls[0] ?? null; - const openStatus = buildPullRequestStatus(openPull, head); - if (openStatus) { - return { - status: openStatus, - githubFeaturesEnabled: true, - }; - } - - let closedPulls: any[]; - try { - closedPulls = await listPullRequestsForHead({ - cwd, - repo, - owner, - head, - state: "closed", - }); - } catch (error) { - if (isGhAuthError(error)) { - return { - status: null, - githubFeaturesEnabled: false, - }; - } - throw error; - } - const mergedClosedPull = - closedPulls.find( - (entry) => - entry && - typeof entry === "object" && - typeof entry.merged_at === "string" && - entry.merged_at.trim().length > 0, - ) ?? null; - const mergedStatus = buildPullRequestStatus(mergedClosedPull, head); - if (!mergedStatus) { - return { - status: null, - githubFeaturesEnabled: true, - }; - } - return { - status: mergedStatus, - githubFeaturesEnabled: true, - }; }