fix: address pr-bug-scan findings from #1444

Threaded linkedPRNumber through all ChecksPanel force-refresh paths; gated useComposerState's PR-URL-from-name recovery on selected repo slug match; wrapped main-process linkedPR fallback gh pr view i
This commit is contained in:
orca-bug-scan-bot
2026-05-05 14:29:21 -07:00
parent 6a7cd2c9ae
commit f287f4695f
3 changed files with 68 additions and 12 deletions
+10 -2
View File
@@ -941,8 +941,16 @@ export async function getPRForBranch(
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,baseRefName,headRefName,baseRefOid,headRefOid'
]
const { stdout } = await ghExecFileAsync(args, { cwd: repoPath })
data = JSON.parse(stdout)
try {
const { stdout } = await ghExecFileAsync(args, { cwd: repoPath })
data = JSON.parse(stdout)
} catch {
// Why: a stale linkedPRNumber (PR deleted, wrong repo, …) makes
// `gh pr view <number>` reject. Treat that as the no-PR case so
// callers see the historical `null` semantics instead of a thrown
// error every poll cycle.
data = null
}
}
if (!data) {
@@ -105,8 +105,8 @@ export default function ChecksPanel(): React.JSX.Element {
// them so we don't keep rendering cached branch summaries or empty file
// lists from an older payload.
conflictSummaryRefreshKeyRef.current = refreshKey
void fetchPRForBranch(repo.path, branch, { force: true })
}, [repo, isFolder, branch, pr, fetchPRForBranch])
void fetchPRForBranch(repo.path, branch, { force: true, linkedPRNumber: linkedPR })
}, [repo, isFolder, branch, pr, linkedPR, fetchPRForBranch])
// Fetch checks via cached store method
const fetchChecks = useCallback(
@@ -235,7 +235,10 @@ export default function ChecksPanel(): React.JSX.Element {
}
setIsRefreshing(true)
try {
const refreshedPR = await fetchPRForBranch(repo.path, branch, { force: true })
const refreshedPR = await fetchPRForBranch(repo.path, branch, {
force: true,
linkedPRNumber: linkedPR
})
if (refreshedPR) {
await Promise.all([
fetchChecks({ force: true, prNumberOverride: refreshedPR.number }),
@@ -248,7 +251,7 @@ export default function ChecksPanel(): React.JSX.Element {
} finally {
setIsRefreshing(false)
}
}, [repo, branch, fetchPRForBranch, fetchChecks, fetchComments])
}, [repo, branch, linkedPR, fetchPRForBranch, fetchChecks, fetchComments])
const handleStartEdit = useCallback(() => {
if (!pr) {
@@ -278,13 +281,13 @@ export default function ChecksPanel(): React.JSX.Element {
})
if (ok) {
// Re-fetch PR to get updated title
await fetchPRForBranch(repo.path, branch, { force: true })
await fetchPRForBranch(repo.path, branch, { force: true, linkedPRNumber: linkedPR })
}
} finally {
setTitleSaving(false)
setEditingTitle(false)
}
}, [repo, pr, titleDraft, branch, fetchPRForBranch])
}, [repo, pr, titleDraft, branch, linkedPR, fetchPRForBranch])
const handleTitleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -318,9 +321,9 @@ export default function ChecksPanel(): React.JSX.Element {
// Refresh PR (passed to PRActions)
const handleRefreshPR = useCallback(async () => {
if (repo && branch) {
await fetchPRForBranch(repo.path, branch, { force: true })
await fetchPRForBranch(repo.path, branch, { force: true, linkedPRNumber: linkedPR })
}
}, [repo, branch, fetchPRForBranch])
}, [repo, branch, linkedPR, fetchPRForBranch])
// Open PR in browser
const handleOpenPR = useCallback(() => {
+47 -2
View File
@@ -358,6 +358,40 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
agentPromptRef.current = agentPrompt
const selectedRepo = eligibleRepos.find((repo) => repo.id === repoId)
// Why: resolves the selected repo's owner/repo slug so a PR URL pasted
// into the workspace name field can be matched against the current repo.
// Pasting a PR URL from a different repo would otherwise recover only the
// PR number, mislinking the worktree to an unrelated PR with the same
// number in the selected repo.
const [selectedRepoSlug, setSelectedRepoSlug] = useState<{ owner: string; repo: string } | null>(
null
)
useEffect(() => {
if (!selectedRepo) {
setSelectedRepoSlug(null)
return
}
let cancelled = false
void (window.api.gh.repoSlug({ repoPath: selectedRepo.path }) as Promise<{
owner: string
repo: string
} | null>)
.then((result) => {
if (cancelled) {
return
}
setSelectedRepoSlug(result)
})
.catch(() => {
if (!cancelled) {
setSelectedRepoSlug(null)
}
})
return () => {
cancelled = true
}
}, [selectedRepo?.id, selectedRepo?.path])
const sparsePresetsForRepo = sparsePresetsByRepo[repoId]
const sparsePresets = sparsePresetsForRepo ?? EMPTY_SPARSE_PRESETS
const normalizedSparseDirectories = useMemo(
@@ -412,10 +446,21 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
}
const fromName = parseGitHubIssueOrPRLink(name)
if (fromName && fromName.type === 'pr') {
return fromName.number
// Why: only adopt a number when the URL's owner/repo matches the
// selected repo. Pasting `github.com/other/repo/pull/1234` must not
// mislink the worktree to an unrelated PR #1234 in the current repo.
// If the slug hasn't resolved yet, suppress recovery rather than
// risking a cross-repo mislink.
if (
selectedRepoSlug &&
fromName.slug.owner.toLowerCase() === selectedRepoSlug.owner.toLowerCase() &&
fromName.slug.repo.toLowerCase() === selectedRepoSlug.repo.toLowerCase()
) {
return fromName.number
}
}
return null
}, [linkedPR, name])
}, [linkedPR, name, selectedRepoSlug])
const setupConfig = useMemo(
() => getSetupConfig(selectedRepo, yamlHooks),
[selectedRepo, yamlHooks]