From 6dd82b974ddb120e8e1a3311a1e31ca12fcede63 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:39:45 -0700 Subject: [PATCH] Track and sync base ref throughout Create PR intent flow (#5907) * Track and sync base ref throughout create PR intent flow * Capture and track the review base ref (`baseRef`) directly inside the Create PR intent run token to keep async steps anchored to the base selected when the run started. * Resolve intent flow and eligibility bases safely using the compare-base picker to override stale default refs. * Keep the PR dialog's base field in sync with the active compare base while untouched by the user. * Ignore stale generated PR fields if base ref syncs while in flight Track user-initiated edits to the base field separately using a ref. When the base ref is synced automatically from Source Control, mark the base field dirty to increment its revision. This ensures stale generated fields from an in-flight request cannot overwrite or revert the newly updated base. --- .../right-sidebar/SourceControl.tsx | 52 +++-- ...urce-control-create-pr-intent-flow.test.ts | 71 ++++++- .../source-control-create-pr-intent-flow.ts | 46 +++- .../useCreatePullRequestDialogFields.test.ts | 201 +++++++++++++++++- .../useCreatePullRequestDialogFields.ts | 53 ++++- 5 files changed, 394 insertions(+), 29 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index d4cd6288a..aa420ecb4 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -236,6 +236,7 @@ import { createPrIntentGitStatusMatchesToken, createPrIntentRunTokenMatches, getCreatePrIntentStagePaths, + resolveCreatePrIntentReviewBase, resolveCreatePrIntentRemoteStep, type CreatePrIntentRunToken } from './source-control-create-pr-intent-flow' @@ -979,7 +980,8 @@ function SourceControlInner(): React.JSX.Element { repoId: null as string | null, worktreeId: null as string | null, worktreePath: null as string | null, - branch: null as string | null + branch: null as string | null, + baseRef: null as string | null }) const [createPrIntentInFlightByWorktree, setCreatePrIntentInFlightByWorktree] = useState< Record @@ -1089,14 +1091,6 @@ function SourceControlInner(): React.JSX.Element { const gitIdentityDisplay = activeWorktree ? getWorktreeGitIdentityDisplay(activeWorktree) : null const detachedHeadDisplay = gitIdentityDisplay?.kind === 'detached' ? gitIdentityDisplay : null const branchName = gitIdentityDisplay?.kind === 'branch' ? gitIdentityDisplay.branchName : '' - useEffect(() => { - createPrIntentCurrentTargetRef.current = { - repoId: activeRepo?.id ?? null, - worktreeId: activeWorktreeId ?? null, - worktreePath, - branch: branchName - } - }, [activeRepo?.id, activeWorktreeId, branchName, worktreePath]) const activePullRequestGenerationKey = getPullRequestGenerationRecordKey({ worktreeId: activeWorktreeId, worktreePath, @@ -1343,6 +1337,15 @@ function SourceControlInner(): React.JSX.Element { pinnedBaseRef, effectiveBaseRef }) + useEffect(() => { + createPrIntentCurrentTargetRef.current = { + repoId: activeRepo?.id ?? null, + worktreeId: activeWorktreeId ?? null, + worktreePath, + branch: branchName, + baseRef: effectiveBaseRef ?? null + } + }, [activeRepo?.id, activeWorktreeId, branchName, effectiveBaseRef, worktreePath]) const linkedGitHubPR = activeWorktree?.linkedPR ?? null const fallbackGitHubPRNumber = linkedGitHubPR == null ? (activePrFromQueue?.number ?? null) : null @@ -2698,6 +2701,7 @@ function SourceControlInner(): React.JSX.Element { worktreePath: worktreePath ?? '', branch: branchName, eligibility: hostedReviewCreation, + currentBaseRef: effectiveBaseRef, repo: activeRepo ?? null, settings: activeRepoSettings, submitting: isCreatingPr, @@ -3070,9 +3074,11 @@ function SourceControlInner(): React.JSX.Element { return false } - const base = stripBaseRef( - eligibility.defaultBaseRef ?? effectiveBaseRef ?? prBase ?? '' - ).trim() + const base = resolveCreatePrIntentReviewBase({ + currentBaseRef: token.baseRef, + eligibilityDefaultBaseRef: eligibility.defaultBaseRef, + composerBaseRef: prBase + }).trim() if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(token.branch).toLowerCase()) { setCreatePrIntentNoticeForWorktree(token.worktreeId, { tone: 'destructive', @@ -3256,7 +3262,6 @@ function SourceControlInner(): React.JSX.Element { [ activeRepo, createHostedReview, - effectiveBaseRef, createPrIntentActiveTargetConflicts, createPrIntentRunStillOwnsWorktree, getCreatePrIntentOperationTarget, @@ -3274,11 +3279,12 @@ function SourceControlInner(): React.JSX.Element { const refreshBranchCompareForCreatePrIntent = useCallback( async (token: CreatePrIntentRunToken): Promise => { - if (!effectiveBaseRef) { + const baseRef = token.baseRef?.trim() + if (!baseRef) { return undefined } - const requestKey = `${token.worktreeId}:${effectiveBaseRef}:${Date.now()}:create-pr-intent` - beginGitBranchCompareRequest(token.worktreeId, requestKey, effectiveBaseRef) + const requestKey = `${token.worktreeId}:${baseRef}:${Date.now()}:create-pr-intent` + beginGitBranchCompareRequest(token.worktreeId, requestKey, baseRef) const result = await getRuntimeGitBranchCompare( { // Why: the intent flow may continue after a worktree switch; use the @@ -3288,12 +3294,12 @@ function SourceControlInner(): React.JSX.Element { worktreePath: token.worktreePath, connectionId: getConnectionId(token.worktreeId) ?? undefined }, - effectiveBaseRef + baseRef ) setGitBranchCompareResult(token.worktreeId, requestKey, result) return result.summary.status === 'ready' ? (result.summary.commitsAhead ?? 0) : undefined }, - [activeRepoSettings, beginGitBranchCompareRequest, effectiveBaseRef, setGitBranchCompareResult] + [activeRepoSettings, beginGitBranchCompareRequest, setGitBranchCompareResult] ) const readHostedReviewCreationEligibilityForIntent = useCallback( @@ -3314,7 +3320,7 @@ function SourceControlInner(): React.JSX.Element { repoId: activeRepo.id, worktreePath: token.worktreePath, branch: token.branch, - base: effectiveBaseRef ?? null, + base: token.baseRef ?? null, hasUncommittedChanges, hasUpstream: upstreamStatus?.hasUpstream, ahead: upstreamStatus?.ahead, @@ -3336,7 +3342,6 @@ function SourceControlInner(): React.JSX.Element { }, [ activeRepo, - effectiveBaseRef, fallbackGitHubPRNumber, getHostedReviewCreationEligibility, linkedAzureDevOpsPR, @@ -3398,7 +3403,10 @@ function SourceControlInner(): React.JSX.Element { repoId: activeRepo.id, worktreeId: activeWorktreeId, worktreePath, - branch: branchName + branch: branchName, + // Why: Create PR intent crosses async commit/push steps; the review + // target must stay tied to the base selected when the run started. + baseRef: effectiveBaseRef ?? null }) const operationTarget = getCreatePrIntentOperationTarget(token) const runIsCurrent = (): boolean => @@ -3622,7 +3630,7 @@ function SourceControlInner(): React.JSX.Element { const remoteOk = await runRemoteAction(remoteStep, { target: operationTarget, remoteStatus: latestUpstreamStatus, - baseRef: effectiveBaseRef + baseRef: token.baseRef }) if (abortIfStale()) { return diff --git a/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts b/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts index dbe5e3f4b..07bff9517 100644 --- a/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts @@ -6,25 +6,33 @@ import { createPrIntentGitStatusMatchesToken, createPrIntentRunTokenMatches, getCreatePrIntentStagePaths, + resolveCreatePrIntentReviewBase, resolveCreatePrIntentRemoteStep } from './source-control-create-pr-intent-flow' import type { GitStatusEntry } from '../../../../shared/types' describe('source-control Create PR intent flow helpers', () => { - it('matches async completions only to the original repo, worktree, path, and branch', () => { + it('matches async completions only to the original repo, worktree, path, branch, and base', () => { const now = vi.spyOn(Date, 'now').mockReturnValue(123) try { const token = createCreatePrIntentRunToken({ repoId: 'repo-1', worktreeId: 'wt-1', worktreePath: '/repo', - branch: 'feature' + branch: 'feature', + baseRef: 'origin/main' }) expect(token.startedAt).toBe(123) expect(createPrIntentRunTokenMatches(token, token)).toBe(true) + expect( + createPrIntentRunTokenMatches(token, { ...token, baseRef: 'refs/remotes/origin/main' }) + ).toBe(true) expect(createPrIntentRunTokenMatches(token, { ...token, branch: 'other' })).toBe(false) expect(createPrIntentRunTokenMatches(token, { ...token, worktreeId: 'wt-2' })).toBe(false) + expect(createPrIntentRunTokenMatches(token, { ...token, baseRef: 'upstream/main' })).toBe( + false + ) } finally { now.mockRestore() } @@ -76,6 +84,47 @@ describe('source-control Create PR intent flow helpers', () => { ).toBe(true) }) + it('treats same-worktree base changes as intent conflicts', () => { + const worktreePath = join(sep, 'repo', 'wt-1') + const token = createCreatePrIntentRunToken({ + repoId: 'repo-1', + worktreeId: 'wt-1', + worktreePath, + branch: 'feature/pr', + baseRef: 'refs/remotes/origin/main' + }) + + expect( + createPrIntentCurrentTargetConflictsWithToken(token, { + repoId: 'repo-1', + worktreeId: 'wt-1', + worktreePath, + branch: 'feature/pr', + baseRef: 'remotes/origin/main' + }) + ).toBe(false) + + expect( + createPrIntentCurrentTargetConflictsWithToken(token, { + repoId: 'repo-1', + worktreeId: 'wt-1', + worktreePath, + branch: 'feature/pr', + baseRef: 'upstream/main' + }) + ).toBe(true) + + expect( + createPrIntentCurrentTargetConflictsWithToken(token, { + repoId: 'repo-1', + worktreeId: 'wt-1', + worktreePath, + branch: 'feature/pr', + baseRef: 'origin/release' + }) + ).toBe(true) + }) + it('stages only safe unstaged and untracked paths', () => { const unresolved = { path: 'conflicted.ts', @@ -93,6 +142,24 @@ describe('source-control Create PR intent flow helpers', () => { ).toEqual(['safe.ts', 'new.ts']) }) + it('prefers the current compare base over stale eligibility defaults', () => { + expect( + resolveCreatePrIntentReviewBase({ + currentBaseRef: 'refs/remotes/origin/release', + eligibilityDefaultBaseRef: 'refs/remotes/origin/main', + composerBaseRef: 'main' + }) + ).toBe('release') + + expect( + resolveCreatePrIntentReviewBase({ + currentBaseRef: null, + eligibilityDefaultBaseRef: 'refs/remotes/upstream/develop', + composerBaseRef: 'main' + }) + ).toBe('develop') + }) + it('resolves safe remote steps for publish, push, and patch-equivalent force-push', () => { expect( resolveCreatePrIntentRemoteStep({ diff --git a/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.ts b/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.ts index 74537d5e9..009315fb1 100644 --- a/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.ts +++ b/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.ts @@ -1,6 +1,9 @@ import { shouldForcePushWithLeaseForUpstream } from '../../../../shared/git-upstream-status' import type { HostedReviewCreationEligibility } from '../../../../shared/hosted-review' -import { normalizeHostedReviewHeadRef } from '../../../../shared/hosted-review-refs' +import { + normalizeHostedReviewBaseRef, + normalizeHostedReviewHeadRef +} from '../../../../shared/hosted-review-refs' import type { GitStatusEntry, GitUpstreamStatus } from '../../../../shared/types' import { getStageAllPaths } from './discard-all-sequence' @@ -11,6 +14,7 @@ export type CreatePrIntentRunToken = { worktreeId: string worktreePath: string branch: string + baseRef?: string | null startedAt: number } @@ -19,12 +23,32 @@ export type CreatePrIntentCurrentTarget = { worktreeId?: string | null worktreePath?: string | null branch?: string | null + baseRef?: string | null } export function createCreatePrIntentRunToken(input: Omit) { return { ...input, startedAt: Date.now() } } +function normalizeCreatePrIntentBaseIdentityRef(ref: string | null | undefined): string { + const trimmed = ref?.trim() + if (!trimmed) { + return '' + } + // Why: compare bases are local git refs; origin/main and upstream/main must + // stay distinct even though hosted review APIs receive only branch names. + if (trimmed.startsWith('refs/remotes/')) { + return trimmed.slice('refs/remotes/'.length) + } + if (trimmed.startsWith('remotes/')) { + return trimmed.slice('remotes/'.length) + } + if (trimmed.startsWith('refs/heads/')) { + return trimmed.slice('refs/heads/'.length) + } + return trimmed +} + export function createPrIntentRunTokenMatches( token: CreatePrIntentRunToken, current: CreatePrIntentCurrentTarget @@ -33,7 +57,9 @@ export function createPrIntentRunTokenMatches( token.repoId === current.repoId && token.worktreeId === current.worktreeId && token.worktreePath === current.worktreePath && - token.branch === current.branch + token.branch === current.branch && + normalizeCreatePrIntentBaseIdentityRef(token.baseRef) === + normalizeCreatePrIntentBaseIdentityRef(current.baseRef) ) } @@ -67,6 +93,22 @@ export function getCreatePrIntentStagePaths(grouped: { ] } +export function resolveCreatePrIntentReviewBase({ + currentBaseRef, + eligibilityDefaultBaseRef, + composerBaseRef +}: { + currentBaseRef?: string | null + eligibilityDefaultBaseRef?: string | null + composerBaseRef?: string | null +}): string { + // Why: the compare-base picker is the user's latest target; eligibility can + // lag behind while Create PR intent is preparing the branch. + return normalizeHostedReviewBaseRef( + currentBaseRef?.trim() || eligibilityDefaultBaseRef?.trim() || composerBaseRef?.trim() || '' + ) +} + export function resolveCreatePrIntentRemoteStep({ upstreamStatus, hostedReviewCreation, diff --git a/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.test.ts b/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.test.ts index d1678f6c5..9a7fbaa21 100644 --- a/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.test.ts +++ b/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.test.ts @@ -1,5 +1,14 @@ +// @vitest-environment happy-dom + +import React from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' import { describe, expect, it } from 'vitest' -import { normalizeCreateReviewBaseSearchResults } from './useCreatePullRequestDialogFields' +import type { HostedReviewCreationEligibility } from '../../../../shared/hosted-review' +import { + normalizeCreateReviewBaseSearchResults, + useCreatePullRequestDialogFields +} from './useCreatePullRequestDialogFields' describe('normalizeCreateReviewBaseSearchResults', () => { it('uses detailed local branch names for base refs from arbitrary remotes', () => { @@ -32,3 +41,193 @@ describe('normalizeCreateReviewBaseSearchResults', () => { ).toEqual(['main', 'release/1.0']) }) }) + +function createEligibility( + overrides: Partial = {} +): HostedReviewCreationEligibility { + return { + provider: 'github', + review: null, + canCreate: true, + blockedReason: null, + nextAction: null, + defaultBaseRef: 'refs/remotes/origin/main', + title: 'Review title', + body: 'Review body', + ...overrides + } +} + +type DialogFields = ReturnType + +type DialogFieldsRenderInput = { + eligibility: HostedReviewCreationEligibility + currentBaseRef?: string | null +} + +function renderDialogFields(input: DialogFieldsRenderInput): { + current: () => DialogFields + rerender: (nextInput: DialogFieldsRenderInput) => Promise + unmount: () => void +} { + globalThis.IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + document.body.appendChild(container) + const root: Root = createRoot(container) + let latest: DialogFields | null = null + let currentInput = input + + function Harness(): null { + latest = useCreatePullRequestDialogFields({ + open: true, + repoId: 'repo-1', + worktreeId: 'wt-1', + worktreePath: '/repo/wt', + branch: 'feature/base-change', + eligibility: currentInput.eligibility, + currentBaseRef: currentInput.currentBaseRef, + settings: null, + submitting: false + }) + return null + } + + function current(): DialogFields { + if (!latest) { + throw new Error('dialog fields were not rendered') + } + return latest + } + + async function render(): Promise { + await act(async () => { + root.render(React.createElement(Harness)) + await Promise.resolve() + }) + } + + return { + current, + rerender: async (nextInput) => { + currentInput = nextInput + await render() + }, + unmount: () => { + act(() => root.unmount()) + container.remove() + } + } +} + +describe('useCreatePullRequestDialogFields', () => { + it('updates an untouched base field when the creation default changes for the same branch', async () => { + const harness = renderDialogFields({ eligibility: createEligibility() }) + try { + await harness.rerender({ eligibility: createEligibility() }) + expect(harness.current().base).toBe('main') + + await harness.rerender({ + eligibility: createEligibility({ + defaultBaseRef: 'refs/remotes/origin/release' + }) + }) + + expect(harness.current().base).toBe('release') + expect(harness.current().title).toBe('Review title') + expect(harness.current().body).toBe('Review body') + + await harness.rerender({ + eligibility: createEligibility({ + defaultBaseRef: 'refs/remotes/origin/develop' + }) + }) + + expect(harness.current().base).toBe('develop') + } finally { + harness.unmount() + } + }) + + it('prefers the selected current base ref over stale eligibility defaults', async () => { + const harness = renderDialogFields({ + eligibility: createEligibility({ defaultBaseRef: 'refs/remotes/origin/main' }), + currentBaseRef: 'refs/remotes/origin/release' + }) + try { + await harness.rerender({ + eligibility: createEligibility({ defaultBaseRef: 'refs/remotes/origin/main' }), + currentBaseRef: 'refs/remotes/origin/release' + }) + + expect(harness.current().base).toBe('release') + } finally { + harness.unmount() + } + }) + + it('keeps a user-edited base when the default base ref changes', async () => { + const harness = renderDialogFields({ + eligibility: createEligibility(), + currentBaseRef: 'refs/remotes/origin/main' + }) + try { + await harness.rerender({ + eligibility: createEligibility(), + currentBaseRef: 'refs/remotes/origin/main' + }) + act(() => { + harness.current().setBase('custom-target') + }) + + await harness.rerender({ + eligibility: createEligibility({ + defaultBaseRef: 'refs/remotes/origin/release' + }), + currentBaseRef: 'refs/remotes/origin/release' + }) + + expect(harness.current().base).toBe('custom-target') + } finally { + harness.unmount() + } + }) + + it('keeps a synced base when stale generated fields arrive', async () => { + const harness = renderDialogFields({ + eligibility: createEligibility(), + currentBaseRef: 'refs/remotes/origin/main' + }) + try { + await harness.rerender({ + eligibility: createEligibility(), + currentBaseRef: 'refs/remotes/origin/main' + }) + const seedRevisions = { ...harness.current().fieldRevisions } + + await harness.rerender({ + eligibility: createEligibility(), + currentBaseRef: 'refs/remotes/origin/release' + }) + expect(harness.current().base).toBe('release') + + act(() => { + harness.current().applyGeneratedFields( + { + base: 'main', + title: 'Generated title', + body: 'Generated body', + draft: true + }, + seedRevisions + ) + }) + + expect(harness.current().base).toBe('release') + expect(harness.current().title).toBe('Generated title') + expect(harness.current().body).toBe('Generated body') + expect(harness.current().draft).toBe(true) + } finally { + harness.unmount() + } + }) +}) diff --git a/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts b/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts index 421acefe9..51ebab1a7 100644 --- a/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts +++ b/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts @@ -41,6 +41,7 @@ type UseCreatePullRequestDialogFieldsOptions = { worktreePath: string branch: string eligibility: HostedReviewCreationEligibility | null + currentBaseRef?: string | null repo?: Pick | null settings: AppState['settings'] submitting: boolean @@ -78,6 +79,16 @@ export function stripBaseRef(ref: string): string { return normalizeHostedReviewBaseRef(ref) } +function resolveCreateReviewDefaultBaseRef({ + currentBaseRef, + eligibilityDefaultBaseRef +}: { + currentBaseRef?: string | null + eligibilityDefaultBaseRef?: string | null +}): string { + return stripBaseRef(currentBaseRef?.trim() || eligibilityDefaultBaseRef?.trim() || '') +} + export function normalizeCreateReviewBaseSearchResults( results: readonly BaseRefSearchResult[] ): string[] { @@ -103,6 +114,7 @@ export function useCreatePullRequestDialogFields({ worktreePath, branch, eligibility, + currentBaseRef, repo, settings, submitting, @@ -124,6 +136,8 @@ export function useCreatePullRequestDialogFields({ } const initializedFromEligibilityRef = useRef(null) const [initializedEligibilityKey, setInitializedEligibilityKey] = useState(null) + const syncedDefaultBaseRef = useRef(null) + const baseEditedByUserRef = useRef(false) const autoGeneratedForKeyRef = useRef(null) const generateInFlightRef = useRef(false) const generationRequestIdRef = useRef(0) @@ -143,6 +157,10 @@ export function useCreatePullRequestDialogFields({ const hasExternalGeneration = Boolean(generation) const currentEligibilityKey = open && eligibility ? `${repoId}:${worktreeId ?? worktreePath}:${branch}` : null + const resolvedDefaultBaseRef = resolveCreateReviewDefaultBaseRef({ + currentBaseRef, + eligibilityDefaultBaseRef: eligibility?.defaultBaseRef + }) const markFieldDirty = useCallback((field: PullRequestFieldName): void => { fieldRevisionsRef.current = { @@ -153,6 +171,7 @@ export function useCreatePullRequestDialogFields({ const setUserBase = useCallback( (value: string): void => { + baseEditedByUserRef.current = true markFieldDirty('base') setBase(value) }, @@ -225,6 +244,8 @@ export function useCreatePullRequestDialogFields({ generateInFlightRef.current = false generationSeedRef.current = null initializedFromEligibilityRef.current = null + syncedDefaultBaseRef.current = null + baseEditedByUserRef.current = false setInitializedEligibilityKey(null) autoGeneratedForKeyRef.current = null setGenerating(false) @@ -263,8 +284,9 @@ export function useCreatePullRequestDialogFields({ setInitializedEligibilityKey(initializationKey) autoGeneratedForKeyRef.current = null fieldRevisionsRef.current = createInitialPullRequestFieldRevisions() - const initialBase = eligibility.defaultBaseRef ?? '' - setBase(stripBaseRef(initialBase)) + baseEditedByUserRef.current = false + syncedDefaultBaseRef.current = resolvedDefaultBaseRef || null + setBase(resolvedDefaultBaseRef) setTitle(eligibility.title ?? '') setBody(eligibility.body ?? '') setDraft(resolvedPrDefaults.draft) @@ -279,11 +301,38 @@ export function useCreatePullRequestDialogFields({ hasExternalGeneration, open, repoId, + resolvedDefaultBaseRef, resolvedPrDefaults.draft, worktreeId, worktreePath ]) + useEffect(() => { + if ( + !open || + !eligibility || + !initializedFromEligibilityRef.current || + !resolvedDefaultBaseRef + ) { + return + } + if (syncedDefaultBaseRef.current === resolvedDefaultBaseRef) { + return + } + syncedDefaultBaseRef.current = resolvedDefaultBaseRef + if (baseEditedByUserRef.current) { + return + } + // Why: the Source Control compare-base picker can change the intended + // review target while generation is in flight; bump the revision so stale + // generated details cannot retarget an untouched base back to the old ref. + markFieldDirty('base') + setBase(resolvedDefaultBaseRef) + setBaseQuery('') + setBaseResults([]) + setBaseSearchError(null) + }, [eligibility, markFieldDirty, open, resolvedDefaultBaseRef]) + const effectiveGenerating = generation?.generating ?? generating const effectiveGenerateError = generation?.generateError ?? generateError