diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 47dc89b23..36b47d7e5 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -1,6 +1,6 @@ /* eslint-disable max-lines -- Why: the checks panel co-locates PR header, checks, comments, merge actions, and conflict state in one component to keep the data flow straightforward. */ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { LoaderCircle, RefreshCw, @@ -66,11 +66,11 @@ import { isResolvablePRCommentGroup } from '../pr-comments-resolution-prompt' import { startFixChecksAgent } from '@/lib/fix-checks-agent-launch' -import { CreatePullRequestDialog } from './CreatePullRequestDialog' import type { HostedReviewCreationEligibility, HostedReviewProvider } from '../../../../shared/hosted-review' +import { normalizeHostedReviewHeadRef } from '../../../../shared/hosted-review-refs' import { getHostedReviewCacheKey, refreshHostedReviewCard } from '@/store/slices/hosted-review' import { toast } from 'sonner' import { useConfirmationDialog } from '@/components/confirmation-dialog' @@ -89,7 +89,11 @@ import { getChecksPanelEmptyStateCopy, shouldShowChecksPanelPublishBranchAction } from './checks-panel-empty-state' -import { getRuntimeGitStatus, getRuntimeGitUpstreamStatus } from '@/runtime/runtime-git-client' +import { + getRuntimeGitScope, + getRuntimeGitStatus, + getRuntimeGitUpstreamStatus +} from '@/runtime/runtime-git-client' import { buildChecksPanelGitStatusContextKey, readChecksPanelPublishActionGitStatus, @@ -107,7 +111,13 @@ import { gitLabPipelineJobsToPRChecks } from '../../../../shared/gitlab-pipeline import { getWorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display' import { SourceControlAgentActionDialog } from './SourceControlAgentActionDialog' import { readSourceControlLaunchRecipeAgentId } from '@/lib/source-control-launch-agent-selection' -import { resolveSourceControlActionRecipe } from '../../../../shared/source-control-ai' +import { + DEFAULT_SOURCE_CONTROL_AI_PR_CREATION_DEFAULTS, + resolveSourceControlActionRecipe, + resolveSourceControlAiForOperation, + resolveSourceControlAiPrCreationDefaults +} from '../../../../shared/source-control-ai' +import { getCommitMessageModelDiscoveryHostKeyForScope } from '../../../../shared/commit-message-host-key' import { type SourceControlActionRecipe, type SourceControlLaunchActionId @@ -117,6 +127,10 @@ import { type SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' import { resolveSourceControlLaunchPlatform } from '@/lib/source-control-launch-platform' +import { CreateHostedReviewComposer } from './CreateHostedReviewComposer' +import { formatCreateError } from './create-pull-request-review-copy' +import { stripBaseRef, useCreatePullRequestDialogFields } from './useCreatePullRequestDialogFields' +import { localizedHostedReviewCopy } from '@/i18n/hosted-review-localized-copy' import { translate } from '@/i18n/i18n' import { groupPRComments, type PRCommentGroup } from '@/lib/pr-comment-groups' @@ -325,6 +339,7 @@ export default function ChecksPanel(): React.JSX.Element { const getHostedReviewCreationEligibility = useAppStore( (s) => s.getHostedReviewCreationEligibility ) + const createHostedReview = useAppStore((s) => s.createHostedReview) const enqueueGitHubPRRefresh = useAppStore((s) => s.enqueueGitHubPRRefresh) const conflictOperation = useAppStore((s) => activeWorktreeId ? (s.gitConflictOperationByWorktree[activeWorktreeId] ?? 'unknown') : 'unknown' @@ -371,8 +386,10 @@ export default function ChecksPanel(): React.JSX.Element { const [emptyRefreshing, setEmptyRefreshing] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) const [conflictDetailsRefreshing, setConflictDetailsRefreshing] = useState(false) - const [createPrDialogOpen, setCreatePrDialogOpen] = useState(false) const [createPrPushFirst, setCreatePrPushFirst] = useState(false) + const createPrInFlightRef = useRef(null) + const [isCreatingPr, setIsCreatingPr] = useState(false) + const [createPrError, setCreatePrError] = useState(null) const [isPublishingBranch, setIsPublishingBranch] = useState(false) const isResolvingConflictsWithAI = false const [isFixingChecksWithAI, setIsFixingChecksWithAI] = useState(false) @@ -497,8 +514,10 @@ export default function ChecksPanel(): React.JSX.Element { setIsRefreshing(false) setEmptyRefreshing(false) setConflictDetailsRefreshing(false) - setCreatePrDialogOpen(false) setCreatePrPushFirst(false) + createPrInFlightRef.current = null + setIsCreatingPr(false) + setCreatePrError(null) setIsPublishingBranch(false) setAgentComposerState(null) setHostedReviewCreationSnapshot(null) @@ -636,6 +655,95 @@ export default function ChecksPanel(): React.JSX.Element { hostedReviewCreationSnapshot?.requestKey === hostedReviewCreationRequestKey ? hostedReviewCreationSnapshot.data : null + const hostedReviewCreateProvider: HostedReviewProvider = + hostedReviewCreation?.provider === 'gitlab' ? 'gitlab' : 'github' + const hostedReviewCreateCopy = localizedHostedReviewCopy(hostedReviewCreateProvider) + const handleBranchChangedByPullRequestGeneration = useCallback(async (): Promise => { + if (!activeWorktreeId || !activeWorktree?.path) { + return + } + // Why: AI PR detail generation can rebase before summarizing. If HEAD + // moved, the embedded composer should push before creating the review. + setCreatePrPushFirst(true) + const connectionId = activeConnectionId ?? undefined + await fetchUpstreamStatus(activeWorktreeId, activeWorktree.path, connectionId) + }, [activeConnectionId, activeWorktree?.path, activeWorktreeId, fetchUpstreamStatus]) + const prCreationDefaults = useMemo(() => { + if (!settings) { + return DEFAULT_SOURCE_CONTROL_AI_PR_CREATION_DEFAULTS + } + const hostKey = getCommitMessageModelDiscoveryHostKeyForScope( + getRuntimeGitScope(settings, repo?.connectionId) + ) + const resolved = resolveSourceControlAiForOperation({ + settings, + repo, + operation: 'pullRequest', + discoveryHostKey: hostKey, + prCreationProductDefaults: DEFAULT_SOURCE_CONTROL_AI_PR_CREATION_DEFAULTS + }) + return resolved.ok + ? resolved.value.prCreationDefaults + : resolveSourceControlAiPrCreationDefaults({ + settings, + repo, + prCreationProductDefaults: DEFAULT_SOURCE_CONTROL_AI_PR_CREATION_DEFAULTS + }) + }, [repo, settings]) + const createComposerOpen = + !activeReview && + !isFolder && + Boolean(branch) && + (hostedReviewCreation?.canCreate === true || + hostedReviewCreation?.blockedReason === 'needs_push') + const { + aiGenerationEnabled: prAiGenerationEnabled, + base: prBase, + setBase: setPrBase, + title: prTitle, + setTitle: setPrTitle, + body: prBody, + setBody: setPrBody, + draft: prDraft, + setDraft: setPrDraft, + baseQuery: prBaseQuery, + setBaseQuery: setPrBaseQuery, + baseResults: prBaseResults, + setBaseResults: setPrBaseResults, + baseSearchError: prBaseSearchError, + generating: prGenerating, + generateError: prGenerateError, + generateDisabled: prGenerateDisabled, + generateDisabledReason: prGenerateDisabledReason, + handleGenerate: handleGeneratePullRequestFields, + handleCancelGenerate: handleCancelGeneratePullRequestFields + } = useCreatePullRequestDialogFields({ + open: createComposerOpen, + repoId: repo?.id ?? '', + worktreeId: activeWorktreeId, + worktreePath: activeWorktreePath ?? '', + branch, + eligibility: hostedReviewCreation, + repo, + settings, + submitting: isCreatingPr, + prCreationDefaults, + onBranchChangedByGeneration: handleBranchChangedByPullRequestGeneration + }) + const handlePrBaseChange = useCallback( + (value: string): void => { + setCreatePrError(null) + setPrBase(value) + }, + [setPrBase] + ) + const handlePrTitleChange = useCallback( + (value: string): void => { + setCreatePrError(null) + setPrTitle(value) + }, + [setPrTitle] + ) const stateRequestKey = repo && branch ? activeGitLabReview @@ -2441,17 +2549,6 @@ export default function ChecksPanel(): React.JSX.Element { pushBranch ]) - const handleBranchChangedByPullRequestGeneration = useCallback(async (): Promise => { - if (!activeWorktreeId || !activeWorktree?.path) { - return - } - // Why: AI PR detail generation rebases before summarizing; if HEAD moved, - // the dialog must push before creating from the refreshed branch state. - setCreatePrPushFirst(true) - const connectionId = activeConnectionId ?? undefined - await fetchUpstreamStatus(activeWorktreeId, activeWorktree.path, connectionId) - }, [activeConnectionId, activeWorktree?.path, activeWorktreeId, fetchUpstreamStatus]) - const handlePullRequestCreated = useCallback( async (result: { provider: HostedReviewProvider @@ -2508,6 +2605,166 @@ export default function ChecksPanel(): React.JSX.Element { ] ) + const handleCreatePullRequest = useCallback(async (): 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 + } + ) + ) + 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 + } + if (!ok) { + setCreatePrError('Push failed. Resolve the push error, then try again.') + return + } + pushed = true + } + const result = await createHostedReview(repo.path, { + provider: hostedReviewCreateProvider, + base, + head: normalizeHostedReviewHeadRef(branch), + title, + body: prBody, + draft: prDraft, + worktreePath, + useTemplate: prCreationDefaults.useTemplate + }) + if (!isCurrentCreateRequest()) { + return + } + if (result.ok) { + await handlePullRequestCreated({ + provider: hostedReviewCreateProvider, + number: result.number, + url: result.url + }) + if (prCreationDefaults.openAfterCreate) { + openHttpLink(result.url, { worktreeId: activeWorktreeId }) + } + setCreatePrPushFirst(false) + return + } + if (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 + }) + setCreatePrPushFirst(false) + 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) + } + } + }, [ + activeWorktreePath, + activeWorktreeId, + branch, + createComposerOpen, + createHostedReview, + 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 + ]) + // ── Empty state ── if (!activeWorktree) { return ( @@ -2561,7 +2818,6 @@ export default function ChecksPanel(): React.JSX.Element { linkedGitLabMR !== null || hostedReviewCreation?.provider === 'gitlab' const emptyReviewLabel = emptyReviewIsGitLab ? 'merge request' : 'pull request' const emptyReviewShortLabel = emptyReviewIsGitLab ? 'MR' : 'PR' - const canCreate = hostedReviewCreation?.canCreate const canPushCreate = hostedReviewCreation?.blockedReason === 'needs_push' const canPublishBranch = isPublishingBranch || @@ -2581,74 +2837,78 @@ export default function ChecksPanel(): React.JSX.Element { reviewShortLabel: emptyReviewShortLabel }) return ( - <> - {repo && ( - /* Keyed to the same branch/worktree context as the panel's render-time - reset so dialog-local submission state cannot leak across contexts. */ - +
+ {detachedHeadDisplay && ( +
+ +
)} -
- {detachedHeadDisplay && ( -
- -
- )} -
{emptyStateCopy.title}
-
{emptyStateCopy.description}
- {!operationInProgress && ( -
- {canPublishBranch && ( - - )} - {(canCreate || canPushCreate) && ( - - )} +
{emptyStateCopy.title}
+
{emptyStateCopy.description}
+ {!operationInProgress && createComposerOpen ? ( +
+ void handleGeneratePullRequestFields()} + onCancelGenerate={handleCancelGeneratePullRequestFields} + onPrimaryAction={() => void handleCreatePullRequest()} + /> +
+ ) : null} + {!operationInProgress && (!createComposerOpen || canPublishBranch) && ( +
+ {canPublishBranch && ( + + )} + {!createComposerOpen ? (
- )} -
- + ) : null} +
+ )} +
) } diff --git a/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx b/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx new file mode 100644 index 000000000..63e66c68d --- /dev/null +++ b/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx @@ -0,0 +1,361 @@ +import { + ChevronDown, + GitMerge, + GitPullRequestArrow, + RefreshCw, + Sparkles, + Square +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { cn } from '@/lib/utils' +import { + localizedHostedReviewCopy, + resolveSupportedHostedReviewCopyProvider +} from '@/i18n/hosted-review-localized-copy' +import { translate } from '@/i18n/i18n' +import type { HostedReviewProvider } from '../../../../shared/hosted-review' +import { stripBaseRef } from './useCreatePullRequestDialogFields' +import type { DropdownActionKind, DropdownEntry } from './source-control-dropdown-items' +import { CreateHostedReviewComposerFields } from './CreateHostedReviewComposerFields' + +const EMPTY_DROPDOWN_ITEMS: DropdownEntry[] = [] + +export type CreateHostedReviewFields = { + base: string + title: string + body: string + draft: boolean +} + +export type CreateHostedReviewComposerPrimaryAction = { + disabled: boolean + title: string +} + +export type CreateHostedReviewComposerProps = { + className?: string + provider: HostedReviewProvider + branch: string + base: string + setBase: (value: string) => void + title: string + setTitle: (value: string) => void + body: string + setBody: (value: string) => void + draft: boolean + setDraft: (value: boolean) => void + baseQuery: string + setBaseQuery: (value: string) => void + baseResults: string[] + setBaseResults: (value: string[]) => void + baseSearchError: string | null + aiGenerationEnabled: boolean + generating: boolean + generateDisabled: boolean + generateDisabledReason?: string + generateError: string | null + createError: string | null + isCreating: boolean + pushBeforeCreate?: boolean + primaryAction: CreateHostedReviewComposerPrimaryAction + dropdownItems?: DropdownEntry[] + onGenerate: () => void + onCancelGenerate: () => void + onPrimaryAction: () => void + onDropdownAction?: (kind: DropdownActionKind) => void +} + +export function CreateHostedReviewComposer({ + className, + provider, + branch, + base, + setBase, + title, + setTitle, + body, + setBody, + draft, + setDraft, + baseQuery, + setBaseQuery, + baseResults, + setBaseResults, + baseSearchError, + aiGenerationEnabled, + generating, + generateDisabled, + generateDisabledReason, + generateError, + createError, + isCreating, + pushBeforeCreate = false, + primaryAction, + dropdownItems, + onGenerate, + onCancelGenerate, + onPrimaryAction, + onDropdownAction +}: CreateHostedReviewComposerProps): React.JSX.Element { + const copy = localizedHostedReviewCopy(resolveSupportedHostedReviewCopyProvider(provider)) + const ReviewIcon = provider === 'gitlab' ? GitMerge : GitPullRequestArrow + const normalizedBase = stripBaseRef(base) + const strippedBranch = stripBaseRef(branch) + const baseSameAsBranch = normalizedBase.toLowerCase() === strippedBranch.toLowerCase() + const createDisabled = + primaryAction.disabled || + generating || + title.trim().length === 0 || + normalizedBase.trim().length === 0 || + baseSameAsBranch + // Why: surface a concrete reason on the disabled Create PR button so the + // user knows what's blocking submission instead of a silent gray state. + let createDisabledReason: string | undefined + if (generating) { + createDisabledReason = translate( + 'auto.components.right.sidebar.SourceControl.318e2a7f88', + 'Wait for AI generation to finish.' + ) + } else if (title.trim().length === 0) { + createDisabledReason = translate( + 'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5', + 'Enter a {{value0}} title.', + { value0: copy.reviewLabel } + ) + } else if (normalizedBase.trim().length === 0) { + createDisabledReason = translate( + 'auto.components.right.sidebar.SourceControl.f76307c1f7', + 'Choose a base branch.' + ) + } else if (baseSameAsBranch) { + createDisabledReason = translate( + 'auto.components.right.sidebar.SourceControl.4f76c0a9de', + 'Base branch must differ from the head branch.' + ) + } + + // Why: lock the title/body/base inputs while AI generation is running so + // the user can't race the request; generated fields only hydrate safely if + // the hook still sees untouched field revisions. + const fieldsLocked = generating + const generateDetailsLabel = translate( + 'auto.components.right.sidebar.SourceControl.02d8c04339', + 'Generate {{value0}} details with AI', + { value0: copy.reviewLabel } + ) + const stopGeneratingDetailsLabel = translate( + 'auto.components.right.sidebar.SourceControl.b355e740b2', + 'Stop generating {{value0}} details', + { value0: copy.reviewLabel } + ) + const generateTooltipLabel = generating + ? stopGeneratingDetailsLabel + : (generateDisabledReason ?? generateDetailsLabel) + const generateButton = generating ? ( + + ) : ( + + ) + const effectiveDropdownItems = dropdownItems ?? EMPTY_DROPDOWN_ITEMS + const showDropdown = effectiveDropdownItems.length > 0 && onDropdownAction + + return ( +
+
+
+
+
+ {aiGenerationEnabled ? ( + + {!generating && generateDisabled ? ( + + {generateButton} + + ) : ( + {generateButton} + )} + + {generateTooltipLabel} + + + ) : null} +
+ + + +
+ + {showDropdown ? ( + + + + + + {effectiveDropdownItems.map((entry, index) => + entry.kind === 'separator' ? ( + + ) : ( + { + if (entry.disabled) { + event.preventDefault() + return + } + onDropdownAction(entry.kind) + }} + > + + {entry.label} + {entry.hint ? ( + + {entry.hint} + + ) : null} + + + ) + )} + + + ) : null} +
+
+
+ ) +} + +function getCreateButtonLabel({ + isCreating, + pushBeforeCreate, + draft, + shortLabel +}: { + isCreating: boolean + pushBeforeCreate: boolean + draft: boolean + shortLabel: string +}): string { + if (isCreating) { + return translate('auto.components.right.sidebar.SourceControl.26511c22b4', 'Creating...') + } + if (pushBeforeCreate) { + return translate( + 'auto.components.right.sidebar.CreateHostedReviewComposer.741ff8a0d2', + 'Push & Create {{value0}}', + { value0: shortLabel } + ) + } + if (draft) { + return translate( + 'auto.components.right.sidebar.SourceControl.aaf1451654', + 'Create draft {{value0}}', + { value0: shortLabel } + ) + } + return translate('auto.components.right.sidebar.SourceControl.5acbcedc1a', 'Create {{value0}}', { + value0: shortLabel + }) +} diff --git a/src/renderer/src/components/right-sidebar/CreateHostedReviewComposerFields.tsx b/src/renderer/src/components/right-sidebar/CreateHostedReviewComposerFields.tsx new file mode 100644 index 000000000..68591818e --- /dev/null +++ b/src/renderer/src/components/right-sidebar/CreateHostedReviewComposerFields.tsx @@ -0,0 +1,268 @@ +import { ArrowDownUp, Check, ChevronDown, Sparkles, TriangleAlert } from 'lucide-react' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import type { LocalizedHostedReviewCopy } from '@/i18n/hosted-review-localized-copy' +import { stripBaseRef } from './useCreatePullRequestDialogFields' + +type CreateHostedReviewComposerFieldsProps = { + copy: LocalizedHostedReviewCopy + base: string + setBase: (value: string) => void + title: string + setTitle: (value: string) => void + body: string + setBody: (value: string) => void + draft: boolean + setDraft: (value: boolean) => void + baseQuery: string + setBaseQuery: (value: string) => void + baseResults: string[] + setBaseResults: (value: string[]) => void + baseSearchError: string | null + generateError: string | null + createError: string | null + fieldsLocked: boolean + generating: boolean + normalizedBase: string + strippedBranch: string + baseSameAsBranch: boolean +} + +export function CreateHostedReviewComposerFields({ + copy, + base, + setBase, + title, + setTitle, + body, + setBody, + draft, + setDraft, + baseQuery, + setBaseQuery, + baseResults, + setBaseResults, + baseSearchError, + generateError, + createError, + fieldsLocked, + generating, + normalizedBase, + strippedBranch, + baseSameAsBranch +}: CreateHostedReviewComposerFieldsProps): React.JSX.Element { + return ( + <> + {/* Why: a single line that shows the head->base flow plain-language so + the user can sanity-check the merge direction at a glance. */} +
+ + {strippedBranch} + +
+ +
+ setTitle(event.target.value)} + placeholder={translate('auto.components.right.sidebar.SourceControl.7d6a8f0082', 'Title')} + className="h-8 w-full min-w-0 rounded-md border border-border bg-background px-2 text-xs font-medium text-foreground outline-none placeholder:text-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-60" + /> + +