diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 79c0349f7e8..62a6dc9e324 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -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 => { - if (!repo || !branch || !createComposerOpen || prGenerating || createPrInFlightRef.current) { - return - } + const handleCreatePullRequest = useCallback( + async (stacked = false): Promise => { + 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 ? (
void handleGeneratePullRequestFields()} onCancelGenerate={handleCancelGeneratePullRequestFields} - onPrimaryAction={() => void handleCreatePullRequest()} + onPrimaryAction={(stacked) => void handleCreatePullRequest(stacked)} />
) : null} diff --git a/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx b/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx index adf30c949d1..e1aca2574d6 100644 --- a/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx +++ b/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx @@ -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 })} diff --git a/src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx b/src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx index e13023a0683..0d78e48059d 100644 --- a/src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx +++ b/src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx @@ -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 ( ) } +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]*?`, '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', () => { diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index adba079b75d..de4db0b3064 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -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 => { - 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 => { + 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 ? ( { - void handleCreatePullRequest() + onPrimaryAction={(stacked) => { + void handleCreatePullRequest(stacked) }} onDropdownAction={handleActionInvoke} /> diff --git a/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts b/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts index f277ffc2ee0..5bf2bc89b21 100644 --- a/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts +++ b/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts @@ -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([]) const [baseSearchError, setBaseSearchError] = useState(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,