From 94d3db4a2467bb8f78ce814362f84275a7758bd6 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:35:58 -0700 Subject: [PATCH] feat(source-control): show current branch without evicting Create PR (#10215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(source-control): show current branch without evicting Create PR #9787 added the current-branch identity to the Source Control header but did it by replacing the Create PR button's toolbar slot, so #10032 reverted the whole thing. Create PR is the primary entry point into the stage→commit→push→generate-PR flow, so it can't be traded away. Restore the branch identity as its own row above the toolbar so it coexists with the Create PR button (Option 2 layout). Detached HEAD renders in the same identity row via DetachedHeadBadge (re-adds its tabIndex/aria-label), replacing the separate below-toolbar badge row. * style(source-control): match branch identity text to the 'vs main' base ref Same font-mono / 10.5px / foreground-90 / underline treatment so the current branch name reads visually consistent with the base ref in the context row. * style(source-control): drop branch identity underline Keeps the 'vs main' font/size/color match but no underline — the label isn't clickable, so the underline read as a false affordance. * test(source-control): harden identity-row detached + no-identity contracts Add a stable data-testid to the identity row so the no-identity case proves no row renders, and assert the detached badge's accessible label + focusability. Addresses CodeRabbit review on #10215. --- .../src/components/DetachedHeadBadge.tsx | 8 +- .../right-sidebar/SourceControl.tsx | 9 +- ...e-control-header-toolbar-identity.test.tsx | 115 ++++++++++++++++++ .../source-control-header-toolbar.tsx | 57 ++++++++- src/renderer/src/i18n/locales/en.json | 3 +- src/renderer/src/i18n/locales/es.json | 3 +- src/renderer/src/i18n/locales/ja.json | 3 +- src/renderer/src/i18n/locales/ko.json | 3 +- src/renderer/src/i18n/locales/zh.json | 3 +- 9 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx diff --git a/src/renderer/src/components/DetachedHeadBadge.tsx b/src/renderer/src/components/DetachedHeadBadge.tsx index 58a12e639..bc03ce2ab 100644 --- a/src/renderer/src/components/DetachedHeadBadge.tsx +++ b/src/renderer/src/components/DetachedHeadBadge.tsx @@ -12,13 +12,15 @@ type DetachedHeadBadgeProps = { label?: 'sidebar' | 'source-control' side?: React.ComponentProps['side'] className?: string + tabIndex?: number } export function DetachedHeadBadge({ display, label = 'source-control', side = 'right', - className + className, + tabIndex }: DetachedHeadBadgeProps): React.JSX.Element { const visibleLabel = label === 'sidebar' ? display.sidebarLabel : display.sourceControlLabel @@ -27,6 +29,8 @@ export function DetachedHeadBadge({ - {visibleLabel} + {visibleLabel} diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 36553cbc9..16b6cf95d 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -46,7 +46,6 @@ import { isFolderRepo } from '../../../../shared/repo-kind' import { mapSettledWithConcurrency } from '../../../../shared/map-with-concurrency' import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip' import { Button } from '@/components/ui/button' -import { DetachedHeadBadge } from '@/components/DetachedHeadBadge' import { DropdownMenu, DropdownMenuContent, @@ -811,7 +810,6 @@ function SourceControlInner(): React.JSX.Element { const activeRepoConnectionId = activeRepo?.connectionId ?? null const activeRepoExecutionHostId = activeRepo?.executionHostId ?? null const gitIdentityDisplay = activeWorktree ? getWorktreeGitIdentityDisplay(activeWorktree) : null - const detachedHeadDisplay = gitIdentityDisplay?.kind === 'detached' ? gitIdentityDisplay : null const branchName = gitIdentityDisplay?.kind === 'branch' ? gitIdentityDisplay.branchName : '' const entries = useAppStore((s) => activeWorktreeId @@ -5461,6 +5459,7 @@ function SourceControlInner(): React.JSX.Element { <>
- {detachedHeadDisplay && ( -
- -
- )} - {/* Why: hidden when count is 0 — notes are created from the diff view, so an empty Notes shelf here is pure chrome. */} {activeWorktreeId && worktreePath && diffCommentCount > 0 && (
diff --git a/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx b/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx new file mode 100644 index 000000000..a70397959 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx @@ -0,0 +1,115 @@ +import { renderToStaticMarkup } from 'react-dom/server' +import type { ReactNode } from 'react' +import { describe, expect, it, vi } from 'vitest' +import type { WorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display' +import type { PrimaryAction } from './source-control-primary-action' +import { SourceControlHeaderToolbar } from './source-control-header-toolbar' + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children }: { children: ReactNode }) => <>{children}, + TooltipContent: ({ children }: { children: ReactNode }) => <>{children}, + TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children} +})) + +vi.mock('./source-control-header-overflow-menu', () => ({ + SourceControlHeaderOverflowMenu: () => +})) + +vi.mock('./source-control-branch-context-row', () => ({ + shouldShowSourceControlBranchContextRow: () => false, + SourceControlBranchContextRow: () => null +})) + +const CREATE_PR_ACTION: PrimaryAction = { + kind: 'create_pr', + label: 'Create PR', + title: 'Create a pull request', + disabled: false +} + +function renderToolbar( + overrides: { + gitIdentityDisplay?: WorktreeGitIdentityDisplay | null + createPrAction?: PrimaryAction | null + } = {} +): string { + const gitIdentityDisplay = + overrides.gitIdentityDisplay === undefined + ? ({ kind: 'branch', branchName: 'brennanb2025/source-control-branch-name' } as const) + : overrides.gitIdentityDisplay + const createPrAction = + overrides.createPrAction === undefined ? CREATE_PR_ACTION : overrides.createPrAction + + return renderToStaticMarkup( + + ) +} + +describe('SourceControlHeaderToolbar branch identity', () => { + it('keeps the Create PR button while showing the branch identity above it', () => { + const markup = renderToolbar() + const branchIndex = markup.indexOf('brennanb2025/source-control-branch-name') + const createPrIndex = markup.indexOf('Create PR') + + // Why: the #9787 revert regression — identity must not evict Create PR. + expect(markup).toContain('data-testid="source-control-git-identity-row"') + expect(branchIndex).toBeGreaterThan(-1) + expect(createPrIndex).toBeGreaterThan(-1) + // Identity row renders above the toolbar row that hosts Create PR. + expect(branchIndex).toBeLessThan(createPrIndex) + expect(markup).toContain('aria-label="Current branch: brennanb2025/source-control-branch-name"') + expect(markup).toContain('min-w-0 truncate') + }) + + it('renders detached HEAD identity alongside the Create PR button', () => { + const markup = renderToolbar({ + gitIdentityDisplay: { + kind: 'detached', + shortHead: '8cec248', + sidebarLabel: 'Detached HEAD @ 8cec248', + sourceControlLabel: 'Detached HEAD · 8cec248', + tooltip: 'Detached HEAD at 8cec248. You are viewing a commit, not a branch.' + } + }) + + expect(markup).not.toContain('aria-label="Current branch:') + expect(markup).toContain('data-testid="source-control-git-identity-row"') + expect(markup).toContain('Detached HEAD · 8cec248') + // Detached badge stays keyboard-reachable and exposes the full tooltip as its label. + expect(markup).toContain( + 'aria-label="Detached HEAD at 8cec248. You are viewing a commit, not a branch."' + ) + expect(markup).toContain('tabindex="0"') + expect(markup).toContain('Create PR') + }) + + it('omits the identity row when there is no git identity', () => { + const markup = renderToolbar({ gitIdentityDisplay: null }) + + expect(markup).not.toContain('data-testid="source-control-git-identity-row"') + expect(markup).not.toContain('aria-label="Current branch:') + expect(markup).toContain('Create PR') + }) +}) diff --git a/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx b/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx index 3cac51e1e..d56d56b0c 100644 --- a/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef } from 'react' -import { GitPullRequestArrow, Loader2, Search, X } from 'lucide-react' +import { GitBranch, GitPullRequestArrow, Loader2, Search, X } from 'lucide-react' import type { GitBranchCompareSummary, GitUpstreamStatus, @@ -11,6 +11,8 @@ import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' +import { DetachedHeadBadge } from '@/components/DetachedHeadBadge' +import type { WorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display' import { HostedReviewHeaderLink, HostedReviewIcon } from './hosted-review-header-chrome' import { shouldShowSourceControlBranchContextRow, @@ -19,6 +21,7 @@ import { import { SourceControlHeaderOverflowMenu } from './source-control-header-overflow-menu' type SourceControlHeaderToolbarProps = { + gitIdentityDisplay: WorktreeGitIdentityDisplay | null filterQuery: string filterExpanded: boolean onFilterQueryChange: (value: string) => void @@ -43,6 +46,56 @@ type SourceControlHeaderToolbarProps = { manualReviewUrl?: string | null } +// Why: its own row above the toolbar so branch identity coexists with the Create PR +// button instead of competing for the single toolbar slot (#9787 restore). +function SourceControlGitIdentityRow({ + display +}: { + display: WorktreeGitIdentityDisplay +}): React.JSX.Element { + if (display.kind === 'detached') { + return ( +
+ +
+ ) + } + + const branchName = display.branchName + const label = translate( + 'auto.components.right.sidebar.SourceControl.a4e93c21d7', + 'Current branch: {{value0}}', + { value0: branchName } + ) + + return ( +
+ + + + + + + {branchName} + + +
+ ) +} + function HostedReviewToolbarLink({ review, onOpenHostedReviewInChecks, @@ -124,6 +177,7 @@ function renderOverflowMenu( } export function SourceControlHeaderToolbar({ + gitIdentityDisplay, filterQuery, filterExpanded, onFilterQueryChange, @@ -190,6 +244,7 @@ export function SourceControlHeaderToolbar({ return (
+ {gitIdentityDisplay ? : null}