Hide branch names behind workspace card property

This commit is contained in:
Neil 2026-05-23 19:16:50 -07:00 committed by GitHub
parent b8a7adf8ed
commit 3dd281eb8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 119 additions and 29 deletions

View File

@ -2111,6 +2111,7 @@ describe('Store', () => {
expect(store.getUI().worktreeCardProperties).toContain('inline-agents')
expect(store.getUI().worktreeCardProperties).toContain('linear-issue')
expect(store.getUI().worktreeCardProperties).toContain('ports')
expect(store.getUI().worktreeCardProperties).not.toContain('branch')
expect(store.getUI()._inlineAgentsDefaultedForExperiment).toBe(true)
expect(store.getUI()._inlineAgentsDefaultedForAllUsers).toBe(true)
expect(store.getUI()._expandedWorktreeCardPropertiesDefaulted).toBe(true)

View File

@ -136,7 +136,7 @@ describe('client UI RPC methods', () => {
it('accepts persisted literal UI arrays and nested UI state', async () => {
const updated: PersistedUIState = {
...getDefaultUIState(),
worktreeCardProperties: ['status', 'inline-agents'],
worktreeCardProperties: ['status', 'branch', 'inline-agents'],
statusBarItems: ['codex'],
taskResumeState: {
githubMode: 'items',
@ -163,7 +163,7 @@ describe('client UI RPC methods', () => {
const dispatcher = new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS })
const payload = {
worktreeCardProperties: ['status', 'inline-agents'],
worktreeCardProperties: ['status', 'branch', 'inline-agents'],
statusBarItems: ['codex'],
taskResumeState: {
githubMode: 'items',

View File

@ -10,6 +10,7 @@ const UnknownRecordArray = z.array(UnknownRecord)
const WorktreeCardProperty = z.enum([
'status',
'unread',
'branch',
'ci',
'issue',
'linear-issue',

View File

@ -35,6 +35,7 @@ const GROUP_BY_OPTIONS = [
] as const
const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [
{ id: 'branch', label: 'Branch name' },
{ id: 'issue', label: 'GitHub ticket' },
{ id: 'linear-issue', label: 'Linear issue' },
{ id: 'pr', label: 'PR/MR link' },

View File

@ -125,6 +125,41 @@ describe('WorktreeCard quick actions', () => {
expect(markup).toContain('data-workspace-board-preserve-open=""')
})
it('hides the branch name from workspace card metadata by default', () => {
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({
displayName: 'Visible workspace',
branch: 'refs/heads/feature/secret-ref'
})}
repo={makeRepo()}
isActive={false}
/>
)
expect(markup).toContain('Visible workspace')
expect(markup).not.toContain('feature/secret-ref')
expect(markup).not.toContain('secret-ref')
})
it('renders the branch name when the Branch name property is enabled', () => {
worktreeCardProperties = ['status', 'unread', 'branch']
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({
displayName: 'Visible workspace',
branch: 'refs/heads/feature/show-ref'
})}
repo={makeRepo()}
isActive={false}
/>
)
expect(markup).toContain('Visible workspace')
expect(markup).toContain('feature/show-ref')
})
it('shows delete as the top-right quick action for an inactive workspace', () => {
const markup = renderToStaticMarkup(
<WorktreeCard worktree={makeWorktree()} repo={makeRepo()} isActive={false} />

View File

@ -254,6 +254,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
const showLinearIssue = cardProps.includes('linear-issue')
const showComment = cardProps.includes('comment')
const showPorts = cardProps.includes('ports')
const showBranch = cardProps.includes('branch')
// Skip hosted-review fetches when the corresponding card sections are hidden.
// This preference is purely presentational, so background refreshes would
@ -415,8 +416,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
)
// Why: deleting the active/current workspace or one with live activity is a
// disruptive hover action; keep the quick action delete-only and passive.
const showDeleteQuickAction =
!isCurrentWorktree && !hasActiveActivity && !worktree.isMainWorktree
const showDeleteQuickAction = !isCurrentWorktree && !hasActiveActivity && !worktree.isMainWorktree
const handleWorkspaceQuickAction = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
@ -624,10 +624,9 @@ const WorktreeCard = React.memo(function WorktreeCard({
)}
{/* Why: weight alone carries the unread signal; color stays
at text-foreground in both states so the title keeps
hierarchy against the muted branch row below (muting the
title as well flattened the card same reasoning as the
repo chip comment below). */}
at text-foreground in both states so the title remains
the card's primary scan target even when optional branch
metadata is hidden. */}
<div
className={cn(
'text-[12px] truncate leading-tight text-foreground',
@ -643,8 +642,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
{/* Why: the primary worktree (the original clone directory) cannot be
deleted via `git worktree remove`. Placing this badge next to the
name makes it immediately visible and avoids confusion with the
branch name "main" shown below. */}
name keeps that constraint visible even when branch metadata is
hidden by the user's card-property choice. */}
{worktree.isMainWorktree && !isFolder && (
<Tooltip>
<TooltipTrigger asChild>
@ -735,11 +734,11 @@ const WorktreeCard = React.memo(function WorktreeCard({
>
{repo ? getRepoKindLabel(repo) : 'Folder'}
</Badge>
) : (
) : showBranch && branch ? (
<span className="min-w-0 text-[11px] text-muted-foreground truncate leading-none">
{branch}
</span>
)}
) : null}
{/* Why: the conflict operation (merge/rebase/cherry-pick) is the
only signal that the worktree is in an incomplete operation state.

View File

@ -249,5 +249,23 @@ describe('WorktreeList lineage child card renderer', () => {
expect(agentRowIndex).toBeGreaterThan(childStart)
expect(childToggleIndex).toBeGreaterThan(childStart)
expect(agentRowIndex).toBeLessThan(childToggleIndex)
expect(markup).not.toContain('child-branch')
expect(markup).not.toContain('grandchild-branch')
})
it('renders nested lineage branch labels when the Branch name property is enabled', async () => {
setLineageFixtureState()
mockStore.state.worktreeCardProperties = ['status', 'inline-agents', 'branch']
const { default: WorktreeList } = await import('./WorktreeList')
const markup = renderToStaticMarkup(
React.createElement(WorktreeList, {
scrollOffsetRef: { current: 0 },
scrollAnchorRef: { current: null }
})
)
expect(markup).toContain('child-branch')
expect(markup).toContain('grandchild-branch')
})
})

View File

@ -25,6 +25,7 @@ import {
useWorktreeMap
} from '@/store/selectors'
import WorktreeCard from './WorktreeCard'
import { branchDisplayName } from './WorktreeCardHelpers'
import WorktreeCardAgents from './WorktreeCardAgents'
import { SshDisconnectedDialog } from './SshDisconnectedDialog'
import { WorktreeActivityStatusIndicator } from './WorktreeActivityStatusIndicator'
@ -125,7 +126,6 @@ import {
pruneWorktreeSelection,
updateWorktreeSelection
} from './worktree-multi-selection'
import { branchDisplayName } from './WorktreeCardHelpers'
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { getRepoHeaderCreateState } from './repo-header-create-state'
import type { PendingSidebarWorktreeReveal } from '@/store/slices/ui'
@ -2162,6 +2162,10 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
const renderLineageChildCard = (child: WorktreeItemRow) => {
const isActive = activeWorktreeId === child.worktree.id
const childRepo = child.repo
const showChildRepoBadge = childRepo !== undefined && groupBy !== 'repo'
const showChildBranch =
cardProps.includes('branch') && child.worktree.branch.trim() !== ''
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.preventDefault()
event.stopPropagation()
@ -2211,22 +2215,26 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
<div className="truncate text-[12px] leading-tight text-foreground">
{child.worktree.displayName}
</div>
<div className="mt-1 flex min-w-0 items-center gap-1.5">
{child.repo && groupBy !== 'repo' ? (
<span className="flex h-[16px] shrink-0 items-center gap-1.5 rounded-[4px] border border-border bg-accent px-1.5 text-[10px] font-semibold leading-none text-foreground dark:bg-accent/50 dark:border-border/60">
<span
className="size-1.5 rounded-full"
style={{ backgroundColor: child.repo.badgeColor }}
/>
<span className="max-w-[6rem] truncate lowercase">
{child.repo.displayName}
{showChildRepoBadge || showChildBranch ? (
<div className="mt-1 flex min-w-0 items-center gap-1.5">
{showChildRepoBadge && childRepo ? (
<span className="flex h-[16px] shrink-0 items-center gap-1.5 rounded-[4px] border border-border bg-accent px-1.5 text-[10px] font-semibold leading-none text-foreground dark:bg-accent/50 dark:border-border/60">
<span
className="size-1.5 rounded-full"
style={{ backgroundColor: childRepo.badgeColor }}
/>
<span className="max-w-[6rem] truncate lowercase">
{childRepo.displayName}
</span>
</span>
</span>
) : null}
<span className="truncate text-[10.5px] leading-none text-muted-foreground">
{branchDisplayName(child.worktree.branch)}
</span>
</div>
) : null}
{showChildBranch ? (
<span className="truncate text-[10.5px] leading-none text-muted-foreground">
{branchDisplayName(child.worktree.branch)}
</span>
) : null}
</div>
) : null}
{child.worktree.linkedIssue || child.worktree.comment ? (
<div className="mt-1.5 truncate text-[10.5px] leading-tight text-muted-foreground">
{child.worktree.linkedIssue ? (

View File

@ -547,6 +547,19 @@ describe('createUISlice hydratePersistedUI', () => {
expect(store.getState().worktreeCardProperties).toEqual(expected)
expect(setUI).toHaveBeenCalledWith({ worktreeCardProperties: expected })
})
it('persists Branch name as an opt-in card property', () => {
const setUI = vi.fn().mockResolvedValue(undefined)
vi.stubGlobal('window', { api: { ui: { set: setUI } } })
const store = createUIStore()
store.getState().toggleWorktreeCardProperty('branch')
expect(store.getState().worktreeCardProperties).toContain('branch')
expect(setUI).toHaveBeenCalledWith({
worktreeCardProperties: expect.arrayContaining(['status', 'unread', 'branch'])
})
})
})
describe('createUISlice settings navigation', () => {

View File

@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
import { getDefaultPrimarySelectionMiddleClickPaste, getDefaultSettings } from './constants'
import {
getDefaultPrimarySelectionMiddleClickPaste,
getDefaultSettings,
getDefaultUIState
} from './constants'
describe('getDefaultSettings', () => {
it('enables gitignored file decorations by default', () => {
@ -36,3 +40,9 @@ describe('getDefaultPrimarySelectionMiddleClickPaste', () => {
expect(getDefaultPrimarySelectionMiddleClickPaste('win32')).toBe(false)
})
})
describe('getDefaultUIState', () => {
it('keeps branch names hidden from workspace cards by default', () => {
expect(getDefaultUIState().worktreeCardProperties).not.toContain('branch')
})
})

View File

@ -2025,6 +2025,9 @@ export type NotificationPermissionStatusResult = {
export type WorktreeCardProperty =
| 'status'
| 'unread'
// Git branch metadata shown on workspace cards. Hidden by default because
// workspace names already carry the primary identity for most lists.
| 'branch'
// Legacy persisted preference. CI status is now represented by linked PR metadata.
| 'ci'
// GitHub issue metadata shown on workspace cards.

View File

@ -18,6 +18,7 @@ export const DEFAULT_WORKTREE_CARD_PROPERTIES: WorktreeCardProperty[] = [
const WORKTREE_CARD_PROPERTY_ORDER: WorktreeCardProperty[] = [
'status',
'unread',
'branch',
'ci',
'issue',
'linear-issue',