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
This commit is contained in:
Mohamed Boudra
2026-03-22 19:07:01 +07:00
parent c60ea67a9b
commit d79ea8b15c
3 changed files with 66 additions and 128 deletions
@@ -104,6 +104,11 @@ const GITHUB_PR_STATE_COLORS: Record<PrHint["state"], string> = {
merged: "#a371f7",
closed: "#f85149",
};
const GITHUB_PR_STATE_COLORS_HOVER: Record<PrHint["state"], string> = {
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,
]}
>
<GitPullRequest size={12} color={color} />
<Text style={[styles.workspacePrBadgeText, { color }]} numberOfLines={1}>
<GitPullRequest size={12} color={activeColor} />
<Text
style={[
styles.workspacePrBadgeText,
{ color: activeColor },
isHovered && styles.workspacePrBadgeTextHovered,
]}
numberOfLines={1}
>
#{hint.number}
</Text>
</Pressable>
@@ -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,
@@ -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",
};
+42 -116
View File
@@ -1745,61 +1745,6 @@ async function resolveGitHubRepo(cwd: string): Promise<string | null> {
return null;
}
async function listPullRequestsForHead(options: {
cwd: string;
repo: string;
owner: string;
head: string;
state: "open" | "closed";
}): Promise<any[]> {
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<PullRequestStatusResult> {
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<PullRequestStat
githubFeaturesEnabled: false,
};
}
const owner = repo.split("/")[0];
let openPulls: any[];
try {
openPulls = await listPullRequestsForHead({
cwd,
repo,
owner,
head,
state: "open",
});
const { stdout } = await execFileAsync(
"gh",
[
"pr",
"view",
"--json",
"url,title,state,baseRefName,headRefName,mergedAt",
],
{ cwd },
);
const pr = JSON.parse(stdout.trim());
if (!pr || typeof pr !== "object" || !pr.url || !pr.title) {
return { status: null, githubFeaturesEnabled: true };
}
const mergedAt =
typeof pr.mergedAt === "string" && pr.mergedAt.trim().length > 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,
};
}