diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 4a0ccb646b0..1b08fa29f37 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -154,6 +154,7 @@ --color-status-warning-border: var(--status-warning-border); --color-callout-note: var(--callout-note); --color-callout-important: var(--callout-important); + --color-merged-state-foreground: var(--merged-state-foreground); --color-workspace-status-done: var(--workspace-status-done); --color-workspace-status-review: var(--workspace-status-review); --color-workspace-status-progress: var(--workspace-status-progress); @@ -467,6 +468,8 @@ /* GitHub's own hues for the two callout kinds no status token covers. */ --callout-note: #0969da; --callout-important: #8250df; + /* Merged text on a callout-important tint: GitHub's deeper purple keeps 4.5:1 at rest and on hover. */ + --merged-state-foreground: #6639ba; --git-decoration-added: #587c0c; --git-decoration-modified: #895503; --git-decoration-deleted: #ad0707; @@ -502,6 +505,7 @@ --status-warning-border: color-mix(in srgb, var(--status-warning) 25%, transparent); --callout-note: #4493f8; --callout-important: #ab7df8; + --merged-state-foreground: #d2a8ff; --git-decoration-added: #81b88b; --git-decoration-modified: #e2c08d; --git-decoration-deleted: #c74e39; diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.fixture.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.fixture.tsx index f4eeb47c644..3cd9ff565c2 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.fixture.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.fixture.tsx @@ -37,8 +37,10 @@ function BranchSectionFrame({ uncommittedCount?: number isMainWorktree?: boolean }) { + const owner = { id: worktreeId } const view = useSourceControlPanelViewState({ activeWorktreeId: worktreeId, + activeWorktreeOwner: owner, settings: null, updateSettings: async () => {} }) @@ -59,7 +61,8 @@ function BranchSectionFrame({ totalBranchEntryCount={entries.length} collapsedSections={view.collapsedSections} toggleSection={view.toggleSection} - setBranchSectionCollapsed={view.setBranchSectionCollapsed} + setBranchSectionAutoCollapsed={view.setBranchSectionAutoCollapsed} + worktreeOwner={owner} review={review} statusHeadOid={statusHeadOid} uncommittedCount={uncommittedCount} @@ -128,9 +131,18 @@ export const minimumSidebarWidth = defineFixture({ interact: expandBranchSection }) +const germanSettings = { ...getDefaultSettings('/home/test'), uiLanguage: 'de' as const } + export const longLocale = defineFixture({ - width: 300, - store: { settings: { ...getDefaultSettings('/home/test'), uiLanguage: 'de' } }, + width: 308, + store: { settings: germanSettings }, render: () => , interact: expandBranchSection }) + +export const longLocaleMinimumWidth = defineFixture({ + width: RIGHT_SIDEBAR_MIN_WIDTH, + store: { settings: germanSettings }, + render: () => , + interact: expandBranchSection +}) diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.test.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.test.tsx index 80a77380706..fb2b928e930 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.test.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.test.tsx @@ -3,6 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, expect, it } from 'vitest' import { useAppStore } from '@/store' +import type { ExecutionHostId } from '../../../../../../shared/execution-host' import type { HostedReviewInfo } from '../../../../../../shared/source-control/hosted-review/hosted-review-contracts' import type { GitBranchChangeEntry } from '../../../../../../shared/git/diff/git-diff-compare-types' import type { SourceControlTreeNode } from '../../../../../../shared/source-control/listing/source-control-tree' @@ -43,6 +44,8 @@ const mergedReview: HostedReviewInfo = { type SectionInputs = { worktreeId: string + hostId?: ExecutionHostId + instanceId?: string review?: HostedReviewInfo | null statusHeadOid?: string | null uncommittedCount?: number @@ -53,6 +56,8 @@ type SectionInputs = { function Panel({ worktreeId, + hostId, + instanceId, review = mergedReview, statusHeadOid = PR_HEAD, uncommittedCount = 0, @@ -60,8 +65,10 @@ function Panel({ isBranchDiffEditorActive = false, tree = false }: SectionInputs) { + const owner = { id: worktreeId, hostId, instanceId } const view = useSourceControlPanelViewState({ activeWorktreeId: worktreeId, + activeWorktreeOwner: owner, settings: null, updateSettings: async () => {} }) @@ -81,7 +88,8 @@ function Panel({ totalBranchEntryCount={entries.length} collapsedSections={view.collapsedSections} toggleSection={view.toggleSection} - setBranchSectionCollapsed={view.setBranchSectionCollapsed} + setBranchSectionAutoCollapsed={view.setBranchSectionAutoCollapsed} + worktreeOwner={owner} review={review} statusHeadOid={statusHeadOid} uncommittedCount={uncommittedCount} @@ -253,13 +261,60 @@ it('never collapses a user-toggled section', async () => { expect(isExpanded()).toBe(true) }) -it('waits until no branch diff is the active editor before collapsing', async () => { +it('skips the collapse for the session when a branch diff is active as the workspace turns safe', async () => { const worktreeId = nextWorktreeId() await render({ worktreeId, isBranchDiffEditorActive: true }) expect(isExpanded()).toBe(true) + await render({ worktreeId }) + expect(isExpanded()).toBe(true) + expect(hasMergedChip()).toBe(true) +}) + +it('reopens an automatic collapse when the review is no longer merged', async () => { + const worktreeId = nextWorktreeId() await render({ worktreeId }) expect(isExpanded()).toBe(false) + + await render({ worktreeId, review: { ...mergedReview, state: 'open' } }) + expect(isExpanded()).toBe(true) + expect(hasMergedChip()).toBe(false) +}) + +it('names a merge request by its host label', async () => { + await render({ worktreeId: nextWorktreeId(), review: { ...mergedReview, provider: 'gitlab' } }) + + expect(toggle().textContent).toContain('MR #412 is merged') +}) + +it('keeps disclosure and merged proof apart for same-ID workspaces on other hosts', async () => { + const worktreeId = nextWorktreeId() + await render({ worktreeId, hostId: 'ssh:host-a' }) + await clickToggle() + expect(isExpanded()).toBe(true) + + await render({ worktreeId, hostId: 'ssh:host-b', review: null }) + expect(hasMergedChip()).toBe(false) + await render({ worktreeId, hostId: 'ssh:host-b' }) + expect(isExpanded()).toBe(false) + + await render({ worktreeId, hostId: 'ssh:host-a', review: null }) + expect(isExpanded()).toBe(true) + expect(hasMergedChip()).toBe(true) +}) + +it('starts a recreated workspace fresh and leaves the old checkout restorable', async () => { + const worktreeId = nextWorktreeId() + await render({ worktreeId, instanceId: 'first' }) + expect(isExpanded()).toBe(false) + + await render({ worktreeId, instanceId: 'second', review: null }) + expect(isExpanded()).toBe(true) + expect(hasMergedChip()).toBe(false) + + await render({ worktreeId, instanceId: 'first', review: null }) + expect(isExpanded()).toBe(false) + expect(hasMergedChip()).toBe(true) }) it('moves focus from a row to the header when collapsing', async () => { diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.tsx index 82149818e6e..3c3642cadc1 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/branch-section.tsx @@ -16,6 +16,7 @@ import { SourceControlBranchTreeDirectoryRow } from './tree-directory-rows' import { VirtualizedList } from '../../../virtualized-list' import { MergedBranchChip } from './merged-branch-chip' import { useMergedBranchSectionDisclosure } from './use-merged-branch-section-disclosure' +import type { MergedBranchSectionOwner } from './merged-branch-section-disclosure' export function SourceControlBranchSection({ activeOpenRowKeys, @@ -24,7 +25,8 @@ export function SourceControlBranchSection({ totalBranchEntryCount, collapsedSections, toggleSection, - setBranchSectionCollapsed, + setBranchSectionAutoCollapsed, + worktreeOwner, review, statusHeadOid, uncommittedCount, @@ -49,7 +51,8 @@ export function SourceControlBranchSection({ totalBranchEntryCount: number collapsedSections: Set toggleSection: (section: string) => void - setBranchSectionCollapsed: (collapsed: boolean) => void + setBranchSectionAutoCollapsed: (collapsed: boolean) => void + worktreeOwner: MergedBranchSectionOwner /** Null for folder workspaces and branches without a review. */ review: HostedReviewInfo | null /** HEAD from the latest successful status read; null while unknown. */ @@ -93,13 +96,13 @@ export function SourceControlBranchSection({ const sectionRef = useRef(null) const { mergedReview } = useMergedBranchSectionDisclosure({ - worktreeId: currentWorktreeId, + owner: worktreeOwner, isMainWorktree, review, statusHeadOid, uncommittedCount, branchDiffEditorActive: isBranchDiffEditorActive, - setBranchSectionCollapsed, + setBranchSectionAutoCollapsed, sectionRef }) diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/merged-branch-chip.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/merged-branch-chip.tsx index 1c326d3eb82..d8626fdd6c2 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/merged-branch-chip.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/merged-branch-chip.tsx @@ -21,7 +21,7 @@ export function MergedBranchChip({ // Title is for mouse hover; screen readers get the sr-only text in the toggle's name.