fix(github): avoid effect-driven stack mode state

This commit is contained in:
Jinwoo-H
2026-08-11 01:34:31 -04:00
parent 291400ff4c
commit 2957a6e80c
5 changed files with 354 additions and 345 deletions
@@ -1319,8 +1319,6 @@ export default function ChecksPanel(): React.JSX.Element {
setBody: setPrBody,
draft: prDraft,
setDraft: setPrDraft,
stacked: prStacked,
setStacked: setPrStacked,
stackedCreationSupported: prStackedCreationSupported,
baseQuery: prBaseQuery,
setBaseQuery: setPrBaseQuery,
@@ -3980,124 +3978,86 @@ export default function ChecksPanel(): React.JSX.Element {
]
)
const handleCreatePullRequest = useCallback(async (): Promise<void> => {
if (!repo || !branch || !createComposerOpen || prGenerating || createPrInFlightRef.current) {
return
}
const handleCreatePullRequest = useCallback(
async (stacked = false): Promise<void> => {
if (!repo || !branch || !createComposerOpen || prGenerating || createPrInFlightRef.current) {
return
}
const requestContextKey = panelContextKey
const isCurrentCreateRequest = (): boolean =>
panelContextKeyRef.current === requestContextKey &&
createPrInFlightRef.current === requestContextKey
const base = stripBaseRef(prBase).trim()
const title = prTitle.trim()
const worktreePath = activeWorktreePath ?? repo.path
if (!title) {
setCreatePrError(
translate(
'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5',
'Enter a {{value0}} title.',
{
value0: hostedReviewCreateCopy.reviewLabel
const requestContextKey = panelContextKey
const isCurrentCreateRequest = (): boolean =>
panelContextKeyRef.current === requestContextKey &&
createPrInFlightRef.current === requestContextKey
const base = stripBaseRef(prBase).trim()
const title = prTitle.trim()
const worktreePath = activeWorktreePath ?? repo.path
if (!title) {
setCreatePrError(
translate(
'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5',
'Enter a {{value0}} title.',
{
value0: hostedReviewCreateCopy.reviewLabel
}
)
)
return
}
if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(branch).toLowerCase()) {
setCreatePrError(
translate(
'auto.components.right.sidebar.SourceControl.ae743199cd',
'Choose a different base branch before creating a {{value0}}.',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
)
return
}
createPrInFlightRef.current = requestContextKey
setIsCreatingPr(true)
setCreatePrError(null)
let pushed = false
try {
const shouldPushBeforeCreate =
createPrPushFirst || hostedReviewCreation?.blockedReason === 'needs_push'
if (shouldPushBeforeCreate) {
const ok = await pushBeforeCreatePullRequest()
if (!isCurrentCreateRequest()) {
return
}
)
)
return
}
if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(branch).toLowerCase()) {
setCreatePrError(
translate(
'auto.components.right.sidebar.SourceControl.ae743199cd',
'Choose a different base branch before creating a {{value0}}.',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
)
return
}
createPrInFlightRef.current = requestContextKey
setIsCreatingPr(true)
setCreatePrError(null)
let pushed = false
try {
const shouldPushBeforeCreate =
createPrPushFirst || hostedReviewCreation?.blockedReason === 'needs_push'
if (shouldPushBeforeCreate) {
const ok = await pushBeforeCreatePullRequest()
if (!ok) {
setCreatePrError('Push failed. Resolve the push error, then try again.')
return
}
pushed = true
}
const createInput = {
repoId: repo.id,
provider: hostedReviewCreateProvider,
base,
head: normalizeHostedReviewHeadRef(branch),
title,
body: prBody,
draft: prDraft,
worktreePath,
useTemplate: prCreationDefaults.useTemplate
}
const result = stacked
? await createStackedHostedReview(repo.path, createInput)
: await createHostedReview(repo.path, createInput)
if (!isCurrentCreateRequest()) {
return
}
if (!ok) {
setCreatePrError('Push failed. Resolve the push error, then try again.')
return
}
pushed = true
}
const createInput = {
repoId: repo.id,
provider: hostedReviewCreateProvider,
base,
head: normalizeHostedReviewHeadRef(branch),
title,
body: prBody,
draft: prDraft,
worktreePath,
useTemplate: prCreationDefaults.useTemplate
}
const result = prStacked
? await createStackedHostedReview(repo.path, createInput)
: await createHostedReview(repo.path, createInput)
if (!isCurrentCreateRequest()) {
return
}
if (result.ok) {
await handlePullRequestCreated({
provider: hostedReviewCreateProvider,
number: result.number,
url: result.url
})
if (prCreationDefaults.openAfterCreate) {
openHttpLink(result.url, { worktreeId: activeWorktreeId })
}
if (activePullRequestGenerationKey) {
updatePullRequestGenerationRecord(
activePullRequestGenerationKey,
clearPullRequestGenerationRequiresPushBeforeCreate
)
}
return
}
if ('existingReview' in result && result.existingReview?.url) {
const number = result.existingReview.number
toast.success(
number
? translate(
'auto.components.right.sidebar.ChecksPanel.b6ce28da5b',
'{{value0}} #{{value1}} is already open',
{ value0: hostedReviewCreateCopy.titleLabel, value1: number }
)
: translate(
'auto.components.right.sidebar.ChecksPanel.cf9e69f3be',
'{{value0}} is already open',
{ value0: hostedReviewCreateCopy.titleLabel }
),
{
action: {
label: translate(
'auto.components.right.sidebar.ChecksPanel.192e686e57',
'Open on {{value0}}',
{ value0: hostedReviewCreateCopy.providerName }
),
onClick: () => window.api.shell.openUrl(result.existingReview!.url)
}
}
)
if (number) {
if (result.ok) {
await handlePullRequestCreated({
provider: hostedReviewCreateProvider,
number,
url: result.existingReview.url
number: result.number,
url: result.url
})
if (prCreationDefaults.openAfterCreate) {
openHttpLink(result.url, { worktreeId: activeWorktreeId })
}
if (activePullRequestGenerationKey) {
updatePullRequestGenerationRecord(
activePullRequestGenerationKey,
@@ -4106,57 +4066,97 @@ export default function ChecksPanel(): React.JSX.Element {
}
return
}
if ('existingReview' in result && result.existingReview?.url) {
const number = result.existingReview.number
toast.success(
number
? translate(
'auto.components.right.sidebar.ChecksPanel.b6ce28da5b',
'{{value0}} #{{value1}} is already open',
{ value0: hostedReviewCreateCopy.titleLabel, value1: number }
)
: translate(
'auto.components.right.sidebar.ChecksPanel.cf9e69f3be',
'{{value0}} is already open',
{ value0: hostedReviewCreateCopy.titleLabel }
),
{
action: {
label: translate(
'auto.components.right.sidebar.ChecksPanel.192e686e57',
'Open on {{value0}}',
{ value0: hostedReviewCreateCopy.providerName }
),
onClick: () => window.api.shell.openUrl(result.existingReview!.url)
}
}
)
if (number) {
await handlePullRequestCreated({
provider: hostedReviewCreateProvider,
number,
url: result.existingReview.url
})
if (activePullRequestGenerationKey) {
updatePullRequestGenerationRecord(
activePullRequestGenerationKey,
clearPullRequestGenerationRequiresPushBeforeCreate
)
}
return
}
}
setCreatePrError(formatCreateError(result, pushed, hostedReviewCreateCopy.shortLabel))
} catch (error) {
if (!isCurrentCreateRequest()) {
return
}
setCreatePrError(
error instanceof Error
? error.message
: translate(
'auto.components.right.sidebar.SourceControl.e2b7a1c0d9f4',
'Failed to create {{value0}}',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
)
} finally {
if (createPrInFlightRef.current === requestContextKey) {
createPrInFlightRef.current = null
setIsCreatingPr(false)
setGitStatusRefreshNonce((value) => value + 1)
}
}
setCreatePrError(formatCreateError(result, pushed, hostedReviewCreateCopy.shortLabel))
} catch (error) {
if (!isCurrentCreateRequest()) {
return
}
setCreatePrError(
error instanceof Error
? error.message
: translate(
'auto.components.right.sidebar.SourceControl.e2b7a1c0d9f4',
'Failed to create {{value0}}',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
)
} finally {
if (createPrInFlightRef.current === requestContextKey) {
createPrInFlightRef.current = null
setIsCreatingPr(false)
setGitStatusRefreshNonce((value) => value + 1)
}
}
}, [
activeWorktreePath,
activeWorktreeId,
activePullRequestGenerationKey,
branch,
createComposerOpen,
createHostedReview,
createStackedHostedReview,
createPrPushFirst,
handlePullRequestCreated,
hostedReviewCreateCopy.providerName,
hostedReviewCreateCopy.reviewLabel,
hostedReviewCreateCopy.shortLabel,
hostedReviewCreateCopy.titleLabel,
hostedReviewCreateProvider,
hostedReviewCreation?.blockedReason,
panelContextKey,
prBase,
prBody,
prCreationDefaults.openAfterCreate,
prCreationDefaults.useTemplate,
prDraft,
prGenerating,
prStacked,
prTitle,
pushBeforeCreatePullRequest,
repo,
updatePullRequestGenerationRecord
])
},
[
activeWorktreePath,
activeWorktreeId,
activePullRequestGenerationKey,
branch,
createComposerOpen,
createHostedReview,
createStackedHostedReview,
createPrPushFirst,
handlePullRequestCreated,
hostedReviewCreateCopy.providerName,
hostedReviewCreateCopy.reviewLabel,
hostedReviewCreateCopy.shortLabel,
hostedReviewCreateCopy.titleLabel,
hostedReviewCreateProvider,
hostedReviewCreation?.blockedReason,
panelContextKey,
prBase,
prBody,
prCreationDefaults.openAfterCreate,
prCreationDefaults.useTemplate,
prDraft,
prGenerating,
prTitle,
pushBeforeCreatePullRequest,
repo,
updatePullRequestGenerationRecord
]
)
// ── Empty state ──
if (!activeWorktree) {
@@ -4298,6 +4298,7 @@ export default function ChecksPanel(): React.JSX.Element {
{!operationInProgress && createComposerOpen ? (
<div className="mt-4 border-t border-border pt-3">
<CreateHostedReviewComposer
key={panelContextKey}
className="p-0"
provider={hostedReviewCreateProvider}
branch={branch}
@@ -4309,8 +4310,6 @@ export default function ChecksPanel(): React.JSX.Element {
setBody={setPrBody}
draft={prDraft}
setDraft={setPrDraft}
stacked={prStacked}
setStacked={setPrStacked}
stackedCreationSupported={prStackedCreationSupported}
baseQuery={prBaseQuery}
setBaseQuery={setPrBaseQuery}
@@ -4341,7 +4340,7 @@ export default function ChecksPanel(): React.JSX.Element {
}}
onGenerate={() => void handleGeneratePullRequestFields()}
onCancelGenerate={handleCancelGeneratePullRequestFields}
onPrimaryAction={() => void handleCreatePullRequest()}
onPrimaryAction={(stacked) => void handleCreatePullRequest(stacked)}
/>
</div>
) : null}
@@ -1,3 +1,4 @@
import { useState } from 'react'
import {
ChevronDown,
GitMerge,
@@ -50,8 +51,6 @@ export type CreateHostedReviewComposerProps = {
setBody: (value: string) => void
draft: boolean
setDraft: (value: boolean) => void
stacked: boolean
setStacked: (value: boolean) => void
stackedCreationSupported: boolean
baseQuery: string
setBaseQuery: (value: string) => void
@@ -70,7 +69,7 @@ export type CreateHostedReviewComposerProps = {
dropdownItems?: DropdownEntry[]
onGenerate: () => void
onCancelGenerate: () => void
onPrimaryAction: () => void
onPrimaryAction: (stacked: boolean) => void
onDropdownAction?: (kind: DropdownActionKind) => void
}
@@ -86,8 +85,6 @@ export function CreateHostedReviewComposer({
setBody,
draft,
setDraft,
stacked,
setStacked,
stackedCreationSupported,
baseQuery,
setBaseQuery,
@@ -109,8 +106,11 @@ export function CreateHostedReviewComposer({
onPrimaryAction,
onDropdownAction
}: CreateHostedReviewComposerProps): React.JSX.Element {
const [stacked, setStacked] = useState(false)
const copy = localizedHostedReviewCopy(resolveSupportedHostedReviewCopyProvider(provider))
const ReviewIcon = provider === 'gitlab' ? GitMerge : GitPullRequestArrow
const stackedModeAvailable = provider === 'github' && stackedCreationSupported
const effectiveStacked = stackedModeAvailable && stacked
const normalizedBase = stripBaseRef(base)
const strippedBranch = stripBaseRef(branch)
const baseSameAsBranch = normalizedBase.toLowerCase() === strippedBranch.toLowerCase()
@@ -235,9 +235,9 @@ export function CreateHostedReviewComposer({
setBody={setBody}
draft={draft}
setDraft={setDraft}
stacked={stacked}
stacked={effectiveStacked}
setStacked={setStacked}
showStackedMode={provider === 'github' && stackedCreationSupported}
showStackedMode={stackedModeAvailable}
baseQuery={baseQuery}
setBaseQuery={setBaseQuery}
baseResults={baseResults}
@@ -257,7 +257,7 @@ export function CreateHostedReviewComposer({
type="button"
size="xs"
disabled={createDisabled}
onClick={() => onPrimaryAction()}
onClick={() => onPrimaryAction(effectiveStacked)}
className={cn(
'h-7 px-3 text-xs',
showDropdown && 'rounded-r-none',
@@ -275,7 +275,7 @@ export function CreateHostedReviewComposer({
isCreating,
pushBeforeCreate,
draft,
stacked,
stacked: effectiveStacked,
shortLabel: copy.shortLabel
})}
</span>
@@ -1,5 +1,8 @@
// @vitest-environment happy-dom
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render as renderDom, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { CreateHostedReviewComposer } from './CreateHostedReviewComposer'
import { resolveDropdownItems } from './source-control-dropdown-items'
@@ -10,18 +13,18 @@ type RenderPullRequestComposerOptions = {
generating?: boolean
generateDisabled?: boolean
generateDisabledReason?: string
stacked?: boolean
stackedCreationSupported?: boolean
onPrimaryAction?: (stacked: boolean) => void
}
function renderPullRequestComposer({
function pullRequestComposerElement({
aiGenerationEnabled = true,
generating = false,
generateDisabled = false,
generateDisabledReason,
stacked = false,
stackedCreationSupported = true
}: RenderPullRequestComposerOptions = {}): string {
stackedCreationSupported = true,
onPrimaryAction = vi.fn()
}: RenderPullRequestComposerOptions = {}): React.JSX.Element {
const sourceControlInputs = {
stagedCount: 1,
hasUnstagedChanges: false,
@@ -35,21 +38,19 @@ function renderPullRequestComposer({
}
const primaryAction = resolvePrimaryAction(sourceControlInputs)
return renderToStaticMarkup(
return (
<TooltipProvider>
<CreateHostedReviewComposer
provider="github"
branch="branch-login-issue"
base="master"
setBase={vi.fn()}
title=""
title="Ready to create"
setTitle={vi.fn()}
body=""
setBody={vi.fn()}
draft={false}
setDraft={vi.fn()}
stacked={stacked}
setStacked={vi.fn()}
stackedCreationSupported={stackedCreationSupported}
baseQuery=""
setBaseQuery={vi.fn()}
@@ -67,13 +68,17 @@ function renderPullRequestComposer({
dropdownItems={resolveDropdownItems(sourceControlInputs)}
onGenerate={vi.fn()}
onCancelGenerate={vi.fn()}
onPrimaryAction={vi.fn()}
onPrimaryAction={onPrimaryAction}
onDropdownAction={vi.fn()}
/>
</TooltipProvider>
)
}
function renderPullRequestComposer(options: RenderPullRequestComposerOptions = {}): string {
return renderToStaticMarkup(pullRequestComposerElement(options))
}
function elementByLabel(markup: string, tagName: string, label: string): string {
const element = [...markup.matchAll(new RegExp(`<${tagName}\\b[\\s\\S]*?</${tagName}>`, 'g'))]
.map((match) => match[0])
@@ -87,6 +92,8 @@ function elementByLabel(markup: string, tagName: string, label: string): string
}
describe('CreateHostedReviewComposer generate tooltip', () => {
afterEach(cleanup)
it('renders hosted review labels without leaking interpolation placeholders', () => {
const markup = renderPullRequestComposer()
@@ -131,13 +138,21 @@ describe('CreateHostedReviewComposer generate tooltip', () => {
})
it('shows the parent-child preview and stacked create action', () => {
const markup = renderPullRequestComposer({ stacked: true })
const onPrimaryAction = vi.fn()
const { container } = renderDom(pullRequestComposerElement({ onPrimaryAction }))
fireEvent.click(screen.getByText('Stacked PR'))
const markup = container.innerHTML
expect(markup).toContain('Stacked PR')
expect(markup).toContain('master')
expect(markup).toContain('branch-login-issue')
expect(markup).toContain('The base branch must have an open PR and be the top of its stack.')
expect(markup).toContain('Create stacked PR')
fireEvent.click(screen.getByRole('button', { name: /Create stacked PR/ }))
expect(onPrimaryAction).toHaveBeenCalledWith(true)
})
it('hides stacked creation when the executing host lacks the capability', () => {
@@ -3021,8 +3021,6 @@ function SourceControlInner(): React.JSX.Element {
setBody: setPrBody,
draft: prDraft,
setDraft: setPrDraft,
stacked: prStacked,
setStacked: setPrStacked,
stackedCreationSupported: prStackedCreationSupported,
baseQuery: prBaseQuery,
setBaseQuery: setPrBaseQuery,
@@ -3284,168 +3282,170 @@ function SourceControlInner(): React.JSX.Element {
worktreePath
])
const handleCreatePullRequest = useCallback(async (): Promise<void> => {
if (
!activeRepo ||
!activeWorktreeId ||
!worktreePath ||
!hostedReviewCreation ||
prGenerating ||
createPrInFlightRef.current[activeWorktreeId]
) {
return
}
if (!hostedReviewCreation.canCreate) {
// Why: blocked Create Review clicks are intentional; the inline notice tells the user which prerequisite to clear.
const message = resolveBlockedCreateReviewNoticeMessage(hostedReviewCreation)
if (message) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message
})
const handleCreatePullRequest = useCallback(
async (stacked = false): Promise<void> => {
if (
!activeRepo ||
!activeWorktreeId ||
!worktreePath ||
!hostedReviewCreation ||
prGenerating ||
createPrInFlightRef.current[activeWorktreeId]
) {
return
}
return
}
const base = stripBaseRef(prBase).trim()
const title = prTitle.trim()
if (!title) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message: translate(
'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5',
'Enter a {{value0}} title.',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
})
return
}
if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(branchName).toLowerCase()) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message: translate(
'auto.components.right.sidebar.SourceControl.ae743199cd',
'Choose a different base branch before creating a {{value0}}.',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
})
return
}
createPrInFlightRef.current[activeWorktreeId] = true
setCreatePrInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: true }))
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
try {
const createInput = {
repoId: activeRepo.id,
provider: hostedReviewCreateProvider,
base,
head: normalizeHostedReviewHeadRef(branchName),
title,
body: prBody,
draft: prDraft,
worktreePath,
useTemplate: resolvedPrCreationDefaults.useTemplate
}
const result = prStacked
? await createStackedHostedReview(activeRepo.path, createInput)
: await createHostedReview(activeRepo.path, createInput)
if (result.ok) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
await handlePullRequestCreated({
provider: hostedReviewCreateProvider,
number: result.number,
url: result.url
})
if (resolvedPrCreationDefaults.openAfterCreate) {
window.api.shell.openUrl(result.url)
if (!hostedReviewCreation.canCreate) {
// Why: blocked Create Review clicks are intentional; the inline notice tells the user which prerequisite to clear.
const message = resolveBlockedCreateReviewNoticeMessage(hostedReviewCreation)
if (message) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message
})
}
return
}
if ('existingReview' in result && result.existingReview?.url) {
const number = result.existingReview.number
toast.success(
number
? translate(
'auto.components.right.sidebar.SourceControl.eef5446523',
'{{value0}} #{{value1}} is already open',
{ value0: hostedReviewCreateCopy.titleLabel, value1: number }
)
: translate(
'auto.components.right.sidebar.SourceControl.d6fb1df5fe',
'{{value0}} is already open',
{ value0: hostedReviewCreateCopy.titleLabel }
),
{
action: {
label: translate(
'auto.components.right.sidebar.SourceControl.812cb992ee',
'Open on {{value0}}',
{ value0: hostedReviewCreateCopy.providerName }
),
onClick: () => window.api.shell.openUrl(result.existingReview!.url)
}
}
)
if (number) {
const base = stripBaseRef(prBase).trim()
const title = prTitle.trim()
if (!title) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message: translate(
'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5',
'Enter a {{value0}} title.',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
})
return
}
if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(branchName).toLowerCase()) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message: translate(
'auto.components.right.sidebar.SourceControl.ae743199cd',
'Choose a different base branch before creating a {{value0}}.',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
})
return
}
createPrInFlightRef.current[activeWorktreeId] = true
setCreatePrInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: true }))
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
try {
const createInput = {
repoId: activeRepo.id,
provider: hostedReviewCreateProvider,
base,
head: normalizeHostedReviewHeadRef(branchName),
title,
body: prBody,
draft: prDraft,
worktreePath,
useTemplate: resolvedPrCreationDefaults.useTemplate
}
const result = stacked
? await createStackedHostedReview(activeRepo.path, createInput)
: await createHostedReview(activeRepo.path, createInput)
if (result.ok) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
await handlePullRequestCreated({
provider: hostedReviewCreateProvider,
number,
url: result.existingReview.url
number: result.number,
url: result.url
})
if (resolvedPrCreationDefaults.openAfterCreate) {
window.api.shell.openUrl(result.url)
}
return
}
}
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message: result.error
})
} catch (error) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message:
error instanceof Error
? error.message
: translate(
'auto.components.right.sidebar.SourceControl.e2b7a1c0d9f4',
'Failed to create {{value0}}',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
})
} finally {
createPrInFlightRef.current[activeWorktreeId] = false
setCreatePrInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: false }))
}
}, [
activeRepo,
activeWorktreeId,
branchName,
createHostedReview,
createStackedHostedReview,
handlePullRequestCreated,
hostedReviewCreation,
hostedReviewCreateCopy.providerName,
hostedReviewCreateCopy.reviewLabel,
hostedReviewCreateCopy.titleLabel,
hostedReviewCreateProvider,
prBase,
prBody,
prDraft,
prGenerating,
prStacked,
prTitle,
resolvedPrCreationDefaults.openAfterCreate,
resolvedPrCreationDefaults.useTemplate,
setCreatePrIntentNoticeForWorktree,
worktreePath
])
if ('existingReview' in result && result.existingReview?.url) {
const number = result.existingReview.number
toast.success(
number
? translate(
'auto.components.right.sidebar.SourceControl.eef5446523',
'{{value0}} #{{value1}} is already open',
{ value0: hostedReviewCreateCopy.titleLabel, value1: number }
)
: translate(
'auto.components.right.sidebar.SourceControl.d6fb1df5fe',
'{{value0}} is already open',
{ value0: hostedReviewCreateCopy.titleLabel }
),
{
action: {
label: translate(
'auto.components.right.sidebar.SourceControl.812cb992ee',
'Open on {{value0}}',
{ value0: hostedReviewCreateCopy.providerName }
),
onClick: () => window.api.shell.openUrl(result.existingReview!.url)
}
}
)
if (number) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
await handlePullRequestCreated({
provider: hostedReviewCreateProvider,
number,
url: result.existingReview.url
})
return
}
}
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message: result.error
})
} catch (error) {
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
tone: 'destructive',
message:
error instanceof Error
? error.message
: translate(
'auto.components.right.sidebar.SourceControl.e2b7a1c0d9f4',
'Failed to create {{value0}}',
{ value0: hostedReviewCreateCopy.reviewLabel }
)
})
} finally {
createPrInFlightRef.current[activeWorktreeId] = false
setCreatePrInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: false }))
}
},
[
activeRepo,
activeWorktreeId,
branchName,
createHostedReview,
createStackedHostedReview,
handlePullRequestCreated,
hostedReviewCreation,
hostedReviewCreateCopy.providerName,
hostedReviewCreateCopy.reviewLabel,
hostedReviewCreateCopy.titleLabel,
hostedReviewCreateProvider,
prBase,
prBody,
prDraft,
prGenerating,
prTitle,
resolvedPrCreationDefaults.openAfterCreate,
resolvedPrCreationDefaults.useTemplate,
setCreatePrIntentNoticeForWorktree,
worktreePath
]
)
const createHostedReviewForCreatePrIntent = useCallback(
async (
@@ -5826,6 +5826,7 @@ function SourceControlInner(): React.JSX.Element {
{shouldRenderCommitArea(unresolvedConflicts.length, conflictOperation) &&
(directCreatePrAction ? (
<CreateHostedReviewComposer
key={`${activeRepo?.id ?? ''}:${activeWorktreeId ?? worktreePath ?? ''}:${branchName}`}
provider={hostedReviewCreateProvider}
branch={branchName}
base={prBase}
@@ -5836,8 +5837,6 @@ function SourceControlInner(): React.JSX.Element {
setBody={setPrBody}
draft={prDraft}
setDraft={setPrDraft}
stacked={prStacked}
setStacked={setPrStacked}
stackedCreationSupported={prStackedCreationSupported}
baseQuery={prBaseQuery}
setBaseQuery={setPrBaseQuery}
@@ -5857,8 +5856,8 @@ function SourceControlInner(): React.JSX.Element {
dropdownItems={dropdownItems}
onGenerate={handleGeneratePullRequestFieldsClick}
onCancelGenerate={handleCancelGeneratePullRequestFields}
onPrimaryAction={() => {
void handleCreatePullRequest()
onPrimaryAction={(stacked) => {
void handleCreatePullRequest(stacked)
}}
onDropdownAction={handleActionInvoke}
/>
@@ -166,7 +166,6 @@ export function useCreatePullRequestDialogFields({
const [title, setTitle] = useState('')
const [body, setBody] = useState('')
const [draft, setDraft] = useState(false)
const [stacked, setStacked] = useState(false)
const [baseQuery, setBaseQuery] = useState('')
const [baseResults, setBaseResults] = useState<string[]>([])
const [baseSearchError, setBaseSearchError] = useState<string | null>(null)
@@ -315,7 +314,6 @@ export function useCreatePullRequestDialogFields({
setTitle(resolveCreateReviewDraftTitle({ branch, eligibilityTitle: eligibility.title }))
setBody(eligibility.body ?? '')
setDraft(resolvedPrDefaults.draft)
setStacked(false)
setBaseQuery('')
setBaseResults([])
setBaseSearchError(null)
@@ -611,8 +609,6 @@ export function useCreatePullRequestDialogFields({
draft,
setDraft: setUserDraft,
stackedCreationSupported: eligibility?.stackedCreationSupported === true,
stacked: eligibility?.stackedCreationSupported === true && stacked,
setStacked,
fieldRevisions: fieldRevisionsRef.current,
applyGeneratedFields,
baseQuery,