-
+
{label}
diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/use-merged-branch-section-disclosure.ts b/src/renderer/src/components/right-sidebar/source-control/listing/use-merged-branch-section-disclosure.ts
index 4b8003bf7cc..86c619dcde3 100644
--- a/src/renderer/src/components/right-sidebar/source-control/listing/use-merged-branch-section-disclosure.ts
+++ b/src/renderer/src/components/right-sidebar/source-control/listing/use-merged-branch-section-disclosure.ts
@@ -1,10 +1,12 @@
import { useEffect, useState, type RefObject } from 'react'
import {
+ mergedBranchSectionOwnerKey,
readMergedBranchSectionRecord,
resolveMergedBranchProof,
stepMergedBranchSection,
writeMergedBranchSectionRecord,
type MergedBranchProof,
+ type MergedBranchSectionOwner,
type MergedBranchSectionReview
} from './merged-branch-section-disclosure'
@@ -22,32 +24,36 @@ function moveFocusOutOfBranchRows(section: HTMLElement | null): void {
/** Marks the branch compare section merged and owns its one automatic collapse per session. */
export function useMergedBranchSectionDisclosure({
- worktreeId,
+ owner,
isMainWorktree,
review,
statusHeadOid,
uncommittedCount,
branchDiffEditorActive,
- setBranchSectionCollapsed,
+ setBranchSectionAutoCollapsed,
sectionRef
}: {
- worktreeId: string
+ owner: MergedBranchSectionOwner
isMainWorktree: boolean
review: MergedBranchSectionReview | null
statusHeadOid: string | null
uncommittedCount: number
branchDiffEditorActive: boolean
- setBranchSectionCollapsed: (collapsed: boolean) => void
+ setBranchSectionAutoCollapsed: (collapsed: boolean) => void
sectionRef: RefObject
}): { mergedReview: MergedBranchProof | null } {
+ const ownerKey = mergedBranchSectionOwnerKey(owner)
+ const ownerId = owner.id
+ const ownerHostId = owner.hostId
+ const ownerInstanceId = owner.instanceId
const [memory, setMemory] = useState(() => ({
- worktreeId,
- merged: readMergedBranchSectionRecord(worktreeId).merged
+ ownerKey,
+ merged: readMergedBranchSectionRecord(owner).merged
}))
- if (memory.worktreeId !== worktreeId) {
- setMemory({ worktreeId, merged: readMergedBranchSectionRecord(worktreeId).merged })
+ if (memory.ownerKey !== ownerKey) {
+ setMemory({ ownerKey, merged: readMergedBranchSectionRecord(owner).merged })
}
- const lastMerged = memory.worktreeId === worktreeId ? memory.merged : null
+ const lastMerged = memory.ownerKey === ownerKey ? memory.merged : null
const reviewProvider = review?.provider
const reviewNumber = review?.number
@@ -55,7 +61,8 @@ export function useMergedBranchSectionDisclosure({
const reviewHeadSha = review?.headSha
const reviewContainedHeadOid = review?.confirmedContainedHeadOid
useEffect(() => {
- const record = readMergedBranchSectionRecord(worktreeId)
+ const effectOwner = { id: ownerId, hostId: ownerHostId, instanceId: ownerInstanceId }
+ const record = readMergedBranchSectionRecord(effectOwner)
const step = stepMergedBranchSection(record, {
review:
reviewProvider && reviewNumber !== undefined && reviewState
@@ -75,27 +82,29 @@ export function useMergedBranchSectionDisclosure({
if (step.record === record) {
return
}
- writeMergedBranchSectionRecord(worktreeId, step.record)
- setMemory({ worktreeId, merged: step.record.merged })
+ writeMergedBranchSectionRecord(effectOwner, step.record)
+ setMemory({ ownerKey: mergedBranchSectionOwnerKey(effectOwner), merged: step.record.merged })
if (step.action === 'collapse') {
moveFocusOutOfBranchRows(sectionRef.current)
- setBranchSectionCollapsed(true)
+ setBranchSectionAutoCollapsed(true)
} else if (step.action === 'expand') {
- setBranchSectionCollapsed(false)
+ setBranchSectionAutoCollapsed(false)
}
}, [
branchDiffEditorActive,
isMainWorktree,
+ ownerHostId,
+ ownerId,
+ ownerInstanceId,
reviewContainedHeadOid,
reviewHeadSha,
reviewNumber,
reviewProvider,
reviewState,
sectionRef,
- setBranchSectionCollapsed,
+ setBranchSectionAutoCollapsed,
statusHeadOid,
- uncommittedCount,
- worktreeId
+ uncommittedCount
])
return { mergedReview: resolveMergedBranchProof(lastMerged, review, statusHeadOid) }
diff --git a/src/renderer/src/components/right-sidebar/source-control/panel/panel-content.tsx b/src/renderer/src/components/right-sidebar/source-control/panel/panel-content.tsx
index af98c530e0d..ad9c942fee5 100644
--- a/src/renderer/src/components/right-sidebar/source-control/panel/panel-content.tsx
+++ b/src/renderer/src/components/right-sidebar/source-control/panel/panel-content.tsx
@@ -74,7 +74,7 @@ export function SourceControlPanelContent(props: SourceControlPanelReadyProps) {
revealInExplorer,
selectedKeySet,
setBaseRefDialogOpen,
- setBranchSectionCollapsed,
+ setBranchSectionAutoCollapsed,
sourceControlAiActionsVisible,
sourceControlViewMode,
statusReadError,
@@ -233,7 +233,8 @@ export function SourceControlPanelContent(props: SourceControlPanelReadyProps) {
totalBranchEntryCount={branchEntries.length}
collapsedSections={collapsedSections}
toggleSection={toggleSection}
- setBranchSectionCollapsed={setBranchSectionCollapsed}
+ setBranchSectionAutoCollapsed={setBranchSectionAutoCollapsed}
+ worktreeOwner={activeWorktree}
review={model.isFolder ? null : model.hostedReview}
statusHeadOid={statusHeadOid}
uncommittedCount={model.entries.length}
diff --git a/src/renderer/src/components/right-sidebar/source-control/panel/use-panel-state.ts b/src/renderer/src/components/right-sidebar/source-control/panel/use-panel-state.ts
index b00ad04eff2..f44f91cd749 100644
--- a/src/renderer/src/components/right-sidebar/source-control/panel/use-panel-state.ts
+++ b/src/renderer/src/components/right-sidebar/source-control/panel/use-panel-state.ts
@@ -31,6 +31,7 @@ export function useSourceControlPanelState() {
})
const viewState = useSourceControlPanelViewState({
activeWorktreeId,
+ activeWorktreeOwner: activeWorktree ?? null,
settings,
updateSettings: storeActions.updateSettings
})
diff --git a/src/renderer/src/components/right-sidebar/source-control/panel/use-panel-view-state.ts b/src/renderer/src/components/right-sidebar/source-control/panel/use-panel-view-state.ts
index 10890a202a2..04a21930d85 100644
--- a/src/renderer/src/components/right-sidebar/source-control/panel/use-panel-view-state.ts
+++ b/src/renderer/src/components/right-sidebar/source-control/panel/use-panel-view-state.ts
@@ -8,7 +8,9 @@ import type { SourceControlStoreActions } from '../listing/use-store-actions'
import type { SourceControlWorktreeContext } from '../listing/use-worktree-context'
import {
isBranchSectionAutoCollapsed,
- markBranchSectionToggledByUser
+ markBranchSectionToggledByUser,
+ mergedBranchSectionOwnerKey,
+ type MergedBranchSectionOwner
} from '../listing/merged-branch-section-disclosure'
const DEFAULT_COLLAPSED_SECTIONS = ['history'] as const
@@ -21,24 +23,21 @@ function createDefaultCollapsedSections(): Set {
const collapsedSectionsByWorktree = new BoundedMap>({ maxEntries: 512 })
function readCollapsedSections(worktreeId: string | null): Set {
- const cached = worktreeId ? collapsedSectionsByWorktree.get(worktreeId) : undefined
- if (cached) {
- return cached
- }
- const sections = createDefaultCollapsedSections()
- // The disclosure record outlives this cache, so an evicted automatic collapse stays collapsed.
- if (worktreeId && isBranchSectionAutoCollapsed(worktreeId)) {
- sections.add('branch')
- }
- return sections
+ return (
+ (worktreeId ? collapsedSectionsByWorktree.get(worktreeId) : undefined) ??
+ createDefaultCollapsedSections()
+ )
}
export function useSourceControlPanelViewState({
activeWorktreeId,
+ activeWorktreeOwner,
settings,
updateSettings
}: {
activeWorktreeId: string | null
+ /** Host and checkout identity; automatic disclosure must not leak to a same-ID twin. */
+ activeWorktreeOwner: MergedBranchSectionOwner | null
settings: SourceControlWorktreeContext['settings']
updateSettings: SourceControlStoreActions['updateSettings']
}) {
@@ -47,7 +46,7 @@ export function useSourceControlPanelViewState({
const [fileListScrollElement, setFileListScrollElement] = useState(null)
const isMac = useMemo(() => navigator.userAgent.includes('Mac'), [])
const [filterExpanded, setFilterExpanded] = useState(false)
- const [collapsedSections, setCollapsedSections] = useState>(() =>
+ const [storedCollapsedSections, setCollapsedSections] = useState>(() =>
readCollapsedSections(activeWorktreeId)
)
const persistedSourceControlViewMode = normalizeSourceControlViewMode(
@@ -58,7 +57,28 @@ export function useSourceControlPanelViewState({
const [collapsedTreeDirs, setCollapsedTreeDirs] = useState>(new Set())
const [baseRefDialogOpen, setBaseRefDialogOpen] = useState(false)
const [filterQuery, setFilterQuery] = useState('')
- const isGitHistoryExpanded = !collapsedSections.has('history')
+ const isGitHistoryExpanded = !storedCollapsedSections.has('history')
+
+ // The automatic collapse lives in the host-qualified record, not the raw-ID cache shared by twins.
+ const ownerKey = activeWorktreeOwner ? mergedBranchSectionOwnerKey(activeWorktreeOwner) : null
+ const [branchAutoCollapse, setBranchAutoCollapse] = useState(() => ({
+ ownerKey,
+ collapsed: activeWorktreeOwner ? isBranchSectionAutoCollapsed(activeWorktreeOwner) : false
+ }))
+ if (branchAutoCollapse.ownerKey !== ownerKey) {
+ setBranchAutoCollapse({
+ ownerKey,
+ collapsed: activeWorktreeOwner ? isBranchSectionAutoCollapsed(activeWorktreeOwner) : false
+ })
+ }
+ const isBranchAutoCollapsed =
+ branchAutoCollapse.ownerKey === ownerKey && branchAutoCollapse.collapsed
+ const collapsedSections = useMemo(() => {
+ if (!isBranchAutoCollapsed || storedCollapsedSections.has('branch')) {
+ return storedCollapsedSections
+ }
+ return new Set(storedCollapsedSections).add('branch')
+ }, [isBranchAutoCollapsed, storedCollapsedSections])
const handleToggleSourceControlViewMode = useCallback(() => {
const current = useAppStore.getState().settings
@@ -87,8 +107,8 @@ export function useSourceControlPanelViewState({
const toggleSection = useCallback(
(section: string) => {
- const next = new Set(collapsedSections)
- if (next.has(section)) {
+ const next = new Set(storedCollapsedSections)
+ if (collapsedSections.has(section)) {
next.delete(section)
} else {
next.add(section)
@@ -96,32 +116,19 @@ export function useSourceControlPanelViewState({
setCollapsedSections(next)
if (activeWorktreeId) {
collapsedSectionsByWorktree.set(activeWorktreeId, next)
- if (section === 'branch') {
- markBranchSectionToggledByUser(activeWorktreeId)
- }
+ }
+ if (section === 'branch' && activeWorktreeOwner) {
+ markBranchSectionToggledByUser(activeWorktreeOwner)
+ setBranchAutoCollapse({ ownerKey, collapsed: false })
}
},
- [activeWorktreeId, collapsedSections]
+ [activeWorktreeId, activeWorktreeOwner, collapsedSections, ownerKey, storedCollapsedSections]
)
// Automatic disclosure leaves the user's toggle ownership untouched.
- const setBranchSectionCollapsed = useCallback(
- (collapsed: boolean) => {
- if (collapsedSections.has('branch') === collapsed) {
- return
- }
- const next = new Set(collapsedSections)
- if (collapsed) {
- next.add('branch')
- } else {
- next.delete('branch')
- }
- setCollapsedSections(next)
- if (activeWorktreeId) {
- collapsedSectionsByWorktree.set(activeWorktreeId, next)
- }
- },
- [activeWorktreeId, collapsedSections]
+ const setBranchSectionAutoCollapsed = useCallback(
+ (collapsed: boolean) => setBranchAutoCollapse({ ownerKey, collapsed }),
+ [ownerKey]
)
const toggleTreeDir = useCallback((key: string) => {
@@ -147,7 +154,7 @@ export function useSourceControlPanelViewState({
isGitHistoryExpanded,
isMac,
setBaseRefDialogOpen,
- setBranchSectionCollapsed,
+ setBranchSectionAutoCollapsed,
setFileListScrollElement,
setFilterExpanded,
setFilterQuery,
diff --git a/src/renderer/src/components/ui/badge.tsx b/src/renderer/src/components/ui/badge.tsx
index 70e2bdf61e4..c9e8b4bdcb9 100644
--- a/src/renderer/src/components/ui/badge.tsx
+++ b/src/renderer/src/components/ui/badge.tsx
@@ -28,7 +28,7 @@ const badgeVariants = cva(
'h-4 border-current/30 px-1.5 py-0 align-[1px] text-3xs font-semibold tracking-wider uppercase',
/** A merged-review state inside an uppercase section header, kept in normal case. */
mergedState:
- 'h-4 gap-0.5 bg-callout-important/15 px-1 py-0 text-[10px] tracking-normal normal-case text-callout-important',
+ 'h-4 gap-0.5 bg-callout-important/10 px-1 py-0 text-[10px] tracking-normal normal-case text-merged-state-foreground [&>svg]:text-callout-important',
/** A skill or file sitting inline in the chat message box, ringed while selected. */
promptPill:
'max-w-full border-border bg-secondary px-1.5 py-0 align-baseline text-muted-foreground data-[selected=true]:ring-1 data-[selected=true]:ring-ring'