fix(github): keep merged PRs as branch history instead of hiding them
Branch status resolves an open PR across the whole fork network first, so a merged fork PR can never hide an open upstream PR for the same head. Only when no target has an open PR does the branch's newest closed/merged PR come back, as history. The panel shows that history as a compact note and offers creating the next PR below it, instead of either sticking on a terminal PR or going blank after a merge. Terminal associations stay persisted for reload continuity but are never treated as authority: they revalidate on the discovery cadence and on focus. History is looked up only for the branch's own remote and name, and remembered per repo+branch, so the extra lookup cannot exhaust the route's resolve budget. The checks summary and merge-permission lookup are skipped for a closed or merged PR, where neither is actionable.
This commit is contained in:
@@ -508,8 +508,13 @@ export const PullRequestSection: React.FC<{
|
||||
}, [useDetectedUpstream, detectedUpstream?.defaultBranch]);
|
||||
|
||||
const pr = status?.pr ?? null;
|
||||
// A closed/merged PR is the branch's history, not its live status: it still
|
||||
// deserves to be shown (you just merged it), but the branch is free again, so
|
||||
// the panel offers creating the next PR instead of a read-only detail view.
|
||||
const isHistoricalPr = pr?.state === 'merged' || pr?.state === 'closed';
|
||||
const livePr = isHistoricalPr ? null : pr;
|
||||
|
||||
const prContextKey = pr ? getPrContextKey(directory, pr.number) : null;
|
||||
const prContextKey = livePr ? getPrContextKey(directory, livePr.number) : null;
|
||||
const prContextEntry = usePrContextStore((state) => (prContextKey ? state.entries[prContextKey] : undefined));
|
||||
const ensurePrContext = usePrContextStore((state) => state.ensure);
|
||||
const prContext = prContextEntry?.result ?? null;
|
||||
@@ -525,14 +530,14 @@ export const PullRequestSection: React.FC<{
|
||||
|
||||
// Load the context the active segment needs; checks include details.
|
||||
React.useEffect(() => {
|
||||
if (!pr || !github?.prContext || activeSegment === 'overview') {
|
||||
if (!livePr || !github?.prContext || activeSegment === 'overview') {
|
||||
return;
|
||||
}
|
||||
void ensurePrContext(github, directory, pr.number, {
|
||||
void ensurePrContext(github, directory, livePr.number, {
|
||||
includeCheckDetails: activeSegment === 'checks',
|
||||
sourceRepo: status?.repo ?? null,
|
||||
});
|
||||
}, [activeSegment, directory, ensurePrContext, github, pr, status?.repo]);
|
||||
}, [activeSegment, directory, ensurePrContext, github, livePr, status?.repo]);
|
||||
|
||||
const checks = status?.checks ?? null;
|
||||
const checksArePending = (checks?.pending ?? 0) > 0;
|
||||
@@ -1167,12 +1172,11 @@ export const PullRequestSection: React.FC<{
|
||||
}, [remotes, status?.resolvedRemoteName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Terminal (closed/merged) status must still revalidate on focus/visibility:
|
||||
// the branch may now have a newer open PR, or the association may clear.
|
||||
// Recompute staleness inside the handlers — a captured boolean freezes after
|
||||
// the first fresh refresh until lastRefreshAt changes again.
|
||||
const onFocus = () => {
|
||||
const lastRefreshAt = statusEntry?.lastRefreshAt ?? 0;
|
||||
// Coming back to the app is the moment a PR is most likely to have changed
|
||||
// elsewhere — including a merged one being replaced by a newer open PR — so
|
||||
// staleness is read from the store when the event fires, not captured here.
|
||||
const refreshWhenStale = () => {
|
||||
const lastRefreshAt = useGitHubPrStatusStore.getState().entries[prStatusKey]?.lastRefreshAt ?? 0;
|
||||
if (Date.now() - lastRefreshAt > 60_000) {
|
||||
void refresh({ force: true, silent: true });
|
||||
}
|
||||
@@ -1181,19 +1185,16 @@ export const PullRequestSection: React.FC<{
|
||||
if (document.visibilityState !== 'visible') {
|
||||
return;
|
||||
}
|
||||
const lastRefreshAt = statusEntry?.lastRefreshAt ?? 0;
|
||||
if (Date.now() - lastRefreshAt > 60_000) {
|
||||
void refresh({ force: true, silent: true });
|
||||
}
|
||||
refreshWhenStale();
|
||||
};
|
||||
|
||||
window.addEventListener('focus', onFocus);
|
||||
window.addEventListener('focus', refreshWhenStale);
|
||||
document.addEventListener('visibilitychange', onVisibility);
|
||||
return () => {
|
||||
window.removeEventListener('focus', onFocus);
|
||||
window.removeEventListener('focus', refreshWhenStale);
|
||||
document.removeEventListener('visibilitychange', onVisibility);
|
||||
};
|
||||
}, [refresh, statusEntry?.lastRefreshAt]);
|
||||
}, [prStatusKey, refresh]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (githubAuthChecked && githubAuthStatus?.connected === false) {
|
||||
@@ -1614,7 +1615,7 @@ export const PullRequestSection: React.FC<{
|
||||
<Icon name="loader-4" className="size-4 animate-spin" />
|
||||
{t('gitView.pr.checkingStatus')}
|
||||
</div>
|
||||
) : pr ? (
|
||||
) : pr && !isHistoricalPr ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="h-8 min-w-0">
|
||||
<SortableTabsStrip
|
||||
@@ -1967,6 +1968,30 @@ export const PullRequestSection: React.FC<{
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{pr && isHistoricalPr ? (
|
||||
<div className="flex min-w-0 items-center gap-2 rounded-md border border-border/60 bg-surface-muted/40 px-2.5 py-2">
|
||||
<Icon
|
||||
name={pr.state === 'merged' ? 'git-merge' : 'git-close-pull-request'}
|
||||
className="size-4 shrink-0"
|
||||
style={{ color: prColorVar }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1 typography-micro text-muted-foreground">
|
||||
{pr.state === 'merged'
|
||||
? t('gitView.pr.history.merged', { number: pr.number, base: pr.base || targetBaseBranch })
|
||||
: t('gitView.pr.history.closed', { number: pr.number })}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="shrink-0"
|
||||
onClick={() => void openExternal(pr.url)}
|
||||
aria-label={t('gitView.pr.actions.openOnGitHubAria')}
|
||||
>
|
||||
<Icon name="external-link" className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="typography-ui-label text-foreground">{t('gitView.pr.createTitle')}</div>
|
||||
|
||||
Reference in New Issue
Block a user