Merge branch 'main' of https://github.com/stablyai/orca into brennanb2025/renderer-crash-loop-dampening

This commit is contained in:
brennanb2025
2026-06-23 21:41:44 -07:00
26 changed files with 958 additions and 75 deletions
@@ -30,6 +30,7 @@ vi.mock('@/store', () => ({
linearIssueCache: {},
openModal,
openTaskPage: vi.fn(),
projectGroups: [],
ptyIdsByTabId: {},
remoteBranchConflictByWorktreeId: {},
renamingWorktreeId: null,
@@ -17,6 +17,7 @@ const setWorkspacePortScanRefreshing = vi.fn()
let worktreeCardProperties: WorktreeCardProperty[] = ['status', 'ports']
let hostedReviewCache: Record<string, unknown> = {}
let projectGroups: unknown[] = []
let workspacePortScan: { key: string; result: WorkspacePortScanResult } | null = null
let settings: Partial<GlobalSettings> | null = { compactWorktreeCards: true }
@@ -35,6 +36,7 @@ vi.mock('@/store', () => ({
linearIssueCache: {},
openModal,
openTaskPage,
projectGroups,
ptyIdsByTabId: {},
recordFeatureInteraction,
remoteBranchConflictByWorktreeId: {},
@@ -172,6 +174,7 @@ describe('WorktreeCard compact hover details', () => {
vi.clearAllMocks()
worktreeCardProperties = ['status', 'ports']
hostedReviewCache = {}
projectGroups = []
workspacePortScan = null
settings = { compactWorktreeCards: true }
})
@@ -561,6 +564,78 @@ describe('WorktreeCard compact hover details', () => {
expect(markup).toContain('text-[13px] leading-5')
})
it('does not show a folder path row in new-card mode when no project groups exist', async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status', 'branch']
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({
displayName: 'Docs folder',
branch: '',
path: ' /Users/x/projects/my-app '
})}
repo={{ ...makeRepo(), kind: 'folder' }}
isActive={false}
hideRepoBadge
/>
)
expect(markup).not.toContain('data-worktree-card-meta-row=""')
expect(markup).toContain('Docs folder')
expect(markup).not.toContain('/Users/x/projects/my-app')
})
it('shows a folder path row in new-card mode through the branch setting when project groups exist', async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status', 'branch']
projectGroups = [{ id: 'group-1' }]
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({
displayName: 'Docs folder',
branch: '',
path: ' /Users/x/projects/my-app '
})}
repo={{ ...makeRepo(), kind: 'folder' }}
isActive={false}
hideRepoBadge
/>
)
expect(markup).toContain('data-worktree-card-meta-row=""')
expect(markup).toContain(' /Users/x/projects/my-app ')
expect(markup).not.toContain('>Folder</span>')
})
it('keeps hidden folder path identity available from the new-card hover', async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status']
projectGroups = [{ id: 'group-1' }]
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({
displayName: 'Docs folder',
branch: '',
path: '/Users/x/projects/my-app'
})}
repo={{ ...makeRepo(), kind: 'folder' }}
isActive={false}
hideRepoBadge
/>
)
expect(markup).not.toContain('data-worktree-card-meta-row=""')
expect(markup).toContain('data-hover-open-delay="100"')
expect(markup).toContain('/Users/x/projects/my-app')
expect(markup).toContain('Docs folder')
})
it('shows the branch row for migrated Default cards with branch enabled', async () => {
settings = { compactWorktreeCards: false }
worktreeCardProperties = ['status', 'branch']
@@ -33,6 +33,7 @@ vi.mock('@/store', () => ({
issueCache: {},
linearIssueCache: {},
openModal,
projectGroups: [],
remoteBranchConflictByWorktreeId: {},
settings: { experimentalNewWorktreeCardStyle: true },
sshConnectionStates: new Map(),
@@ -24,6 +24,7 @@ vi.mock('@/store', () => ({
issueCache: {},
linearIssueCache: {},
openModal,
projectGroups: [],
remoteBranchConflictByWorktreeId: {},
settings: null,
sshConnectionStates: new Map(),
@@ -25,6 +25,7 @@ vi.mock('@/store', () => ({
issueCache: {},
linearIssueCache: {},
openModal,
projectGroups: [],
remoteBranchConflictByWorktreeId: {},
settings,
sshConnectionStates: new Map(),
@@ -29,6 +29,7 @@ vi.mock('@/store', () => ({
issueCache: {},
linearIssueCache: {},
openModal,
projectGroups: [],
remoteBranchConflictByWorktreeId: {},
settings,
sshConnectionStates: new Map(),
@@ -21,6 +21,7 @@ let tabsByWorktree: Record<string, { id: string }[]> = {}
let ptyIdsByTabId: Record<string, string[]> = {}
let browserTabsByWorktree: Record<string, { id: string }[]> = {}
let settings: Partial<GlobalSettings> | null = null
let projectGroups: unknown[] = []
let workspaceDeleteModifierPressed = false
let gitConflictOperationByWorktree: Record<string, GitConflictOperation> = {}
let WorktreeCard: typeof WorktreeCardComponent
@@ -35,6 +36,7 @@ vi.mock('@/store', () => ({
hostedReviewCache: {},
issueCache: {},
openModal,
projectGroups,
remoteBranchConflictByWorktreeId: {},
settings,
sshConnectionStates: new Map(),
@@ -143,6 +145,7 @@ describe('WorktreeCard quick actions', () => {
ptyIdsByTabId = {}
browserTabsByWorktree = {}
settings = null
projectGroups = []
workspaceDeleteModifierPressed = false
gitConflictOperationByWorktree = {}
})
@@ -181,7 +184,7 @@ describe('WorktreeCard quick actions', () => {
expect(markup).not.toContain('bg-black/[0.08]')
})
it('renders folder kind and directory in the detailed metadata row', () => {
it('renders folder directory name in the detailed metadata row without a Folder badge', () => {
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({ displayName: 'Docs folder', branch: '' })}
@@ -191,12 +194,12 @@ describe('WorktreeCard quick actions', () => {
)
expect(markup).toContain('Docs folder')
expect(markup).toContain('>Folder</span>')
expect(markup).not.toContain('>Folder</span>')
expect(markup).toContain('>quick-action</span>')
expect(markup).toContain('data-worktree-card-meta-row=""')
})
it('renders synthetic folder workspace directory in the detailed metadata row', () => {
it('renders synthetic folder workspace directory name in the detailed metadata row without a Folder badge', () => {
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({
@@ -211,11 +214,33 @@ describe('WorktreeCard quick actions', () => {
)
expect(markup).toContain('Docs folder')
expect(markup).toContain('>Folder</span>')
expect(markup).not.toContain('>Folder</span>')
expect(markup).toContain('>quick-action</span>')
expect(markup).toContain('data-worktree-card-meta-row=""')
})
it('does not render a branch icon for synthetic folder workspace path identity', () => {
settings = { compactWorktreeCards: true, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status', 'branch']
projectGroups = [{ id: 'project-group-1' }]
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({
id: 'folder:folder-1',
displayName: 'Docs folder',
branch: '',
path: '/repo/worktrees/quick-action'
})}
repo={undefined}
isActive={false}
/>
)
expect(markup).toContain('/repo/worktrees/quick-action')
expect(markup).not.toContain('lucide-git-branch')
})
it('does not render a pending first-agent rename title badge', () => {
const markup = renderToStaticMarkup(
<WorktreeCard
@@ -28,6 +28,7 @@ vi.mock('@/store', () => ({
issueCache: {},
linearIssueCache: {},
openModal,
projectGroups: [],
remoteBranchConflictByWorktreeId: {},
runtimeStatusByEnvironmentId,
settings: null,
@@ -26,7 +26,7 @@ import WorktreeCardAgents from './WorktreeCardAgents'
import { WorktreeCardStatusSlot } from './WorktreeCardStatusSlot'
import { cn } from '@/lib/utils'
import { activateWorktreeFromSidebar } from '@/lib/sidebar-worktree-activation'
import { getRepoKindLabel, isFolderRepo } from '../../../../shared/repo-kind'
import { isFolderRepo } from '../../../../shared/repo-kind'
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
import type {
GitHubWorkItem,
@@ -45,6 +45,7 @@ import {
import { WorktreeCardPortsDetails, WorktreeCardPortsTrigger } from './WorktreeCardPorts'
import { writeWorkspaceDragData } from './workspace-status'
import { getWorktreeCardPrDisplay } from './worktree-card-pr-display'
import type { WorktreeCardPrDisplay } from './worktree-card-pr-display'
import {
coerceWorktreeCardVisibleTitle,
getWorktreeCardTitleDisplay
@@ -121,6 +122,7 @@ type WorktreeCardProps = {
onCardDragEnd?: (event: React.DragEvent<HTMLDivElement>) => void
nativeDragEnabled?: boolean
affiliateListMode?: boolean
statusPrDisplay?: WorktreeCardPrDisplay | null
}
const EMPTY_WORKSPACE_PORTS = []
@@ -212,7 +214,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
lineageChildrenStyle,
onLineageToggle,
isLineageDropTarget = false,
affiliateListMode = false
affiliateListMode = false,
statusPrDisplay = null
}: WorktreeCardProps) {
const openModal = useAppStore((s) => s.openModal)
const openTaskPage = useAppStore((s) => s.openTaskPage)
@@ -228,6 +231,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
const fetchIssue = useAppStore((s) => s.fetchIssue)
const fetchLinearIssue = useAppStore((s) => s.fetchLinearIssue)
const cardProps = useAppStore((s) => s.worktreeCardProperties)
const projectGroups = useAppStore((s) => s.projectGroups)
const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true
const compactCards = !newCardStyle && settings?.compactWorktreeCards === true
const activeSurfaceIsSecondary = isActiveSurface && activeSurfaceVariant === 'secondary'
@@ -367,6 +371,18 @@ const WorktreeCard = React.memo(function WorktreeCard({
const folderWorkspaceId =
workspaceScope?.type === 'folder' ? workspaceScope.folderWorkspaceId : null
const isFolder = repo ? isFolderRepo(repo) : folderWorkspaceId !== null
// Why: project groups are the product gate for folder workspaces, so folder
// paths stay hidden from identity surfaces until that capability exists.
const hasProjectGroups = projectGroups.length > 0
const branchIdentityDisplay = !isFolder && branch.length > 0 ? branch : undefined
const folderPathIdentityDisplay =
isFolder && hasProjectGroups && worktree.path.trim().length > 0 ? worktree.path : undefined
const identityDisplay = branchIdentityDisplay ?? folderPathIdentityDisplay
const hasPathIdentityEnabled = cardProps.includes('branch')
const showIdentityInNewCard = newCardStyle && hasPathIdentityEnabled && Boolean(identityDisplay)
const folderMetaRowContent = newCardStyle
? hasPathIdentityEnabled && Boolean(folderPathIdentityDisplay)
: isFolder
const hostedReviewCacheKey =
repo && branch
? getHostedReviewCacheKey(
@@ -936,6 +952,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
const hoverIssue = issueDisplay
const hoverLinearIssue = linearIssueDisplay
const hoverReview = prDisplay
const statusLaneReview = statusPrDisplay ?? hoverReview
const hoverComment = worktree.comment
const metaIssue = showIssue ? hoverIssue : null
const metaLinearIssue = showLinearIssue ? hoverLinearIssue : null
@@ -1051,7 +1068,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
const showBranch =
!isFolder &&
branch.length > 0 &&
(newCardStyle ? cardProps.includes('branch') : !compactCards || branch !== worktree.displayName)
!newCardStyle &&
(!compactCards || branch !== worktree.displayName)
// Why: rebases already surface in source control; keep dense cards from
// carrying a persistent rebase chip while preserving other interruption cues.
const showConflictOperationBadge =
@@ -1070,30 +1088,28 @@ const WorktreeCard = React.memo(function WorktreeCard({
const hasDetailedMetaRowContent = Boolean(
(showRepoBadgeInMetaRow && repo) ||
showHostContextBadge ||
isFolder ||
folderMetaRowContent ||
showBranch ||
showIdentityInNewCard ||
showDetachedHeadInMetaRow ||
showConflictOperationBadge ||
cacheStartedAt != null ||
showMetaRowDetails
)
const hasMetaRow = compactCards
? hasMetadataBadge || (newCardStyle && showBranch) || cacheStartedAt != null
? hasMetadataBadge || cacheStartedAt != null
: hasDetailedMetaRowContent
const showHeaderActions = showTitleRowPrimary || showDeleteQuickAction
// Why: the hover owns full identity when the row truncates; normalize once
// so title/branch de-dupe and identity-only hover eligibility stay in sync.
const trimmedVisibleCardTitle = visibleCardTitle.trim()
const showBranchIdentityHover = newCardStyle
? !isFolder &&
branch.length > 0 &&
? Boolean(identityDisplay) &&
!cardProps.includes('branch') &&
branch !== trimmedVisibleCardTitle
identityDisplay !== trimmedVisibleCardTitle
: compactCards && showBranch
const hoverBranchName = newCardStyle
? !isFolder && branch.length > 0
? branch
: undefined
? identityDisplay
: showBranchIdentityHover
? branch
: undefined
@@ -1257,9 +1273,9 @@ const WorktreeCard = React.memo(function WorktreeCard({
unreadTooltip={unreadTooltip}
onPointerDown={stopQuickActionPointerPropagation}
onToggleUnread={handleToggleUnreadQuick}
prDisplay={hoverReview}
prDisplay={statusLaneReview}
newCardStyle={newCardStyle}
hasBranchIdentity={!isFolder && branch.length > 0}
hasBranchIdentity={Boolean(branchIdentityDisplay)}
/>
</div>
) : null}
@@ -1372,17 +1388,6 @@ const WorktreeCard = React.memo(function WorktreeCard({
}
/>
{isFolder && (
<Badge
variant="secondary"
className="h-[16px] px-1.5 text-[10px] font-medium rounded shrink-0 text-muted-foreground bg-accent border border-border dark:bg-accent/80 dark:border-border/50 leading-none"
>
{repo
? getRepoKindLabel(repo)
: translate('auto.components.sidebar.WorktreeCard.93aebe4529', 'Folder')}
</Badge>
)}
{typeof worktree.firstAgentMessageRenameError === 'string' &&
worktree.firstAgentMessageRenameError.length > 0 &&
!titleRenaming ? (
@@ -1542,7 +1547,13 @@ const WorktreeCard = React.memo(function WorktreeCard({
</Badge>
)}
{isFolder ? (
{showIdentityInNewCard ? (
<TruncatedSidebarLabel
text={identityDisplay!}
className="text-[11px] text-muted-foreground leading-none"
tooltipEnabled={!hasHoverDetails}
/>
) : isFolder && !newCardStyle ? (
<span
className="min-w-0 truncate font-mono text-[11px] leading-none text-muted-foreground"
title={worktree.path}
@@ -11,6 +11,7 @@ const setWorktreeCardProperties = vi.fn()
const setAgentActivityDisplayMode = vi.fn()
let settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false }
let projectGroups: unknown[] = []
let worktreeCardProperties = [
'status',
'unread',
@@ -27,6 +28,7 @@ vi.mock('@/store', () => ({
useAppStore: (selector: (state: unknown) => unknown) =>
selector({
agentActivityDisplayMode: 'compact',
projectGroups,
setAgentActivityDisplayMode,
setWorktreeCardMode,
setWorktreeCardProperties,
@@ -112,6 +114,7 @@ function renderMenu(): void {
beforeEach(() => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false }
projectGroups = []
worktreeCardProperties = [
'status',
'unread',
@@ -153,4 +156,23 @@ describe('WorktreeCardDisplayMenuSection', () => {
expect(setWorktreeCardMode).toHaveBeenCalledWith('Compact')
})
it('keeps branch-only copy when project groups are unavailable', () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
renderMenu()
expect(container?.textContent).toContain('Branch name')
expect(container?.textContent).not.toContain('Branch / folder path')
})
it('mentions folder paths when project groups can create folder workspaces', () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
projectGroups = [{ id: 'group-1' }]
renderMenu()
expect(container?.textContent).toContain('Branch / folder path')
expect(container?.textContent).not.toContain('Branch name')
})
})
@@ -32,6 +32,7 @@ export function WorktreeCardDisplayMenuSection({
const setWorktreeCardMode = useAppStore((s) => s.setWorktreeCardMode)
const agentActivityDisplayMode = useAppStore((s) => s.agentActivityDisplayMode)
const setAgentActivityDisplayMode = useAppStore((s) => s.setAgentActivityDisplayMode)
const projectGroups = useAppStore((s) => s.projectGroups)
const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true
const cardLayout = settings?.compactWorktreeCards ? 'compact' : 'detailed'
const cardLayoutLabel =
@@ -39,9 +40,10 @@ export function WorktreeCardDisplayMenuSection({
const visiblePropertyCount = PROPERTY_OPTIONS.filter((opt) =>
worktreeCardProperties.includes(opt.id)
).length
const hasProjectGroups = projectGroups.length > 0
const worktreeCardPropertyOptions = useMemo(
() => getWorktreeCardPropertyOptions({ newCardStyle }),
[newCardStyle]
() => getWorktreeCardPropertyOptions({ newCardStyle, hasProjectGroups }),
[newCardStyle, hasProjectGroups]
)
const handleWorktreeCardPropertyChange = useCallback(
(properties: readonly WorktreeCardProperty[], checked: boolean): void => {
@@ -205,7 +205,7 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).not.toContain('bg-neutral-500/40')
})
it('uses a branch icon instead of the quiet active dot when new card style has no review', () => {
it('uses a branch icon with branch-only tooltip copy by default', () => {
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId="wt-1"
@@ -221,12 +221,33 @@ describe('WorktreeCardStatusSlot', () => {
)
expect(markup).toContain('Branch')
expect(markup).not.toContain('Branch or folder path')
expect(markup).toContain('lucide-git-branch')
expect(markup).toContain('size-[13px] translate-x-px text-muted-foreground/70')
expect(markup).toContain('text-muted-foreground/70')
expect(markup).not.toContain('bg-emerald-500')
})
it('uses context-aware branch or folder path tooltip copy', () => {
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId="wt-1"
showStatus
showUnreadAction={false}
isUnread={false}
unreadTooltip="Mark as unread"
onPointerDown={vi.fn()}
onToggleUnread={vi.fn()}
newCardStyle
hasBranchIdentity
branchIdentityLabel="Branch or folder path"
/>
)
expect(markup).toContain('Branch or folder path')
expect(markup).toContain('lucide-git-branch')
})
it('keeps the quiet dot when the row has no branch identity', () => {
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
@@ -1,6 +1,7 @@
import React from 'react'
import { Bell, GitBranch } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-status'
import { FilledBellIcon } from './WorktreeCardHelpers'
@@ -20,13 +21,16 @@ type WorktreeCardStatusSlotProps = {
prDisplay?: WorktreeCardPrDisplay | null
newCardStyle?: boolean
hasBranchIdentity?: boolean
branchIdentityLabel?: string
className?: string
}
const QUIET_REVIEW_REPLACEABLE_STATUSES = new Set<WorktreeStatus>(['active', 'done', 'inactive'])
// Why: a missing review display can also mean provider state is unavailable,
// so the passive label names the branch cue without claiming no review exists.
const BRANCH_STATUS_LABEL = 'Branch'
// so the passive label names the identity cue without claiming no review exists.
function getDefaultBranchIdentityLabel(): string {
return translate('auto.components.sidebar.WorktreeCardStatusSlot.branchIdentity', 'Branch')
}
// Why: branch-style SVGs are optically left-heavy; this keeps them aligned with
// the centered activity dots in the shared status column.
const compactReviewAndBranchStatusIconClassName = 'size-[13px] translate-x-px'
@@ -93,6 +97,7 @@ export function WorktreeCardStatusSlot({
prDisplay = null,
newCardStyle = false,
hasBranchIdentity = false,
branchIdentityLabel,
className
}: WorktreeCardStatusSlotProps): React.JSX.Element | null {
const status = useWorktreeActivityStatus(worktreeId)
@@ -112,7 +117,7 @@ export function WorktreeCardStatusSlot({
canShowReviewStatus && prDisplay
? getReviewStatusTooltip(prDisplay)
: canShowBranchStatus
? BRANCH_STATUS_LABEL
? (branchIdentityLabel ?? getDefaultBranchIdentityLabel())
: statusLabel
const passiveStatusTooltip =
newCardStyle && isUnread ? `${passiveStatusLabel} · Unread` : passiveStatusLabel
@@ -340,6 +340,7 @@ function makeFolderWorkspacePathStatusState(): Record<string, unknown> {
function setFolderWorkspaceFixtureState(
options: {
createdFrom?: ProjectGroup['createdFrom']
experimentalNewWorktreeCardStyle?: boolean
nestedGroup?: boolean
} = {}
): void {
@@ -405,7 +406,9 @@ function setFolderWorkspaceFixtureState(
setRenamingWorktreeId: vi.fn(),
setShowSleepingWorkspaces: vi.fn(),
setSortBy: vi.fn(),
settings: null,
settings: options.experimentalNewWorktreeCardStyle
? { experimentalNewWorktreeCardStyle: true }
: null,
renamingWorktreeId: null,
showSleepingWorkspaces: true,
sortBy: 'manual',
@@ -1040,6 +1043,25 @@ describe('WorktreeList lineage child card renderer', () => {
).toBe(20)
})
it('uses comparable new-card worktree geometry for experimental folder workspace rows', async () => {
setFolderWorkspaceFixtureState({ experimentalNewWorktreeCardStyle: true })
const markup = await renderWorktreeListMarkup()
const folderWorktreeId = folderWorkspaceKey('folder-workspace-1')
const cardOpeningTag = getCardOpeningTag(markup, folderWorktreeId)
const surfaceOpeningTag = getFolderWorkspaceSurfaceOpeningTag(markup, 'folder-workspace-1')
const cardContentIndent = getDataNumber(cardOpeningTag, 'data-content-indent')
expect(surfaceOpeningTag).toContain('padding-left:14px')
expect(cardOpeningTag).toContain('data-content-indent="16"')
expect(cardOpeningTag).toContain('data-flush-surface="true"')
expect(
getFlushCardContentStart({
cardContentIndent,
surfaceInset: getPaddingLeft(surfaceOpeningTag)
})
).toBe(30)
})
it('preserves manual folder workspace indentation outside folder-scanned groups', async () => {
setFolderWorkspaceFixtureState({ createdFrom: 'manual' })
const markup = await renderWorktreeListMarkup()
@@ -57,6 +57,7 @@ import type {
ProjectOrderBy,
WorktreeLineage,
WorktreeMeta,
WorkspaceLineage,
WorkspaceStatus,
WorkspaceStatusDefinition
} from '../../../../shared/types'
@@ -225,8 +226,7 @@ import {
LINEAGE_CHILDREN_INLINE_OFFSET,
getFolderBackedRepoWorktreeCardContentIndent,
getFolderBackedRepoWorktreeCardSurfaceInset,
getFolderWorkspaceCardContentIndent,
getFolderWorkspaceCardSurfaceInset,
getFolderWorkspaceRowGeometry,
getLineageChildrenInlineStyle,
getLineageNestedRowGeometry,
getProjectGroupHeaderPaddingLeft,
@@ -256,6 +256,7 @@ import {
getFolderWorkspaceExecutionHostIdForRows,
getProjectGroupExecutionHostIdForRows
} from './worktree-list-host-filtering'
import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display'
export {
getScrollTopToRevealBounds,
@@ -613,6 +614,7 @@ type VirtualizedWorktreeViewportProps = {
repoMap: Map<string, Repo>
worktreeMap: Map<string, Worktree>
worktreeLineageById: Record<string, WorktreeLineage>
workspaceLineageByChildKey: Record<string, WorkspaceLineage>
repoOrder: Map<string, number>
// The full canonical state.repos id ordering — the drag controller commits
// permutations of this list, even when some repos aren't currently visible
@@ -622,6 +624,7 @@ type VirtualizedWorktreeViewportProps = {
onReorderHostSections: (orderedHostIds: ExecutionHostId[]) => void
onHostDragActiveChange: (active: boolean) => void
prCache: Record<string, unknown> | null
hostedReviewCache: Record<string, unknown> | null
workspaceStatuses: readonly WorkspaceStatusDefinition[]
projectGrouping?: ProjectGroupingModel
projectGroups?: readonly ProjectGroup[]
@@ -1238,11 +1241,13 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
repoMap,
worktreeMap,
worktreeLineageById,
workspaceLineageByChildKey,
repoOrder,
allRepoIds,
onReorderHostSections,
onHostDragActiveChange,
prCache,
hostedReviewCache,
workspaceStatuses,
projectGrouping,
projectGroups = EMPTY_PROJECT_GROUPS,
@@ -4761,30 +4766,25 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
folderWorkspacePathStatus?.exists === false &&
(isConfirmedStaleFolderPathStatus(folderWorkspacePathStatus) ||
folderWorkspacePathStatus.reason === 'ambiguous-connection')
const folderPrDisplay = getFolderWorkspaceCardPrDisplay({
folderWorkspaceId: folderWorkspaceRow.folderWorkspace.id,
workspaceLineageByChildKey,
worktreeLineageById,
worktreeMap,
repoMap,
hostedReviewCache,
prCache,
settings
})
const isFolderBackedWorkspaceChild =
groupBy === 'repo' && folderWorkspaceRow.projectGroup.createdFrom === 'folder-scan'
const contentIndent = isFolderBackedWorkspaceChild
? getFolderWorkspaceCardContentIndent({
groupDepth: folderWorkspaceRow.groupDepth
})
: getWorktreeCardContentIndent({
isGrouped: groupBy !== 'none',
groupDepth: folderWorkspaceRow.groupDepth,
lineageDepth: folderWorkspaceRow.depth
})
// Why: folder workspace surfaces should step inward with their
// project-group nesting, matching lineage child card surfaces
// instead of spanning from the sidebar edge at every depth.
const surfaceInset = isFolderBackedWorkspaceChild
? getFolderWorkspaceCardSurfaceInset({
isGrouped: true,
groupDepth: folderWorkspaceRow.groupDepth
})
: getWorktreeCardSurfaceInset({
isGrouped: groupBy !== 'none',
groupDepth: folderWorkspaceRow.groupDepth
})
const insetContentIndent = Math.max(0, contentIndent - surfaceInset)
const { surfaceInset, cardContentIndent } = getFolderWorkspaceRowGeometry({
experimentalNewWorktreeCardStyle: newCardStyle,
isFolderBackedWorkspaceChild,
isGrouped: groupBy !== 'none',
groupDepth: folderWorkspaceRow.groupDepth,
lineageDepth: folderWorkspaceRow.depth
})
return (
<div
key={vItem.key}
@@ -4815,7 +4815,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
repo={undefined}
isActive={activeWorktreeId === folderWorktree.id}
isCurrentWorktree={currentWorktreeId === folderWorktree.id}
contentIndent={insetContentIndent}
contentIndent={cardContentIndent}
flushSurface
nativeDragEnabled={false}
onImmediateActivate={
@@ -4826,6 +4826,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
activationRowKey={folderWorktree.id}
onSelectionGesture={onSelectionGesture}
onContextMenuSelect={onContextMenuSelect}
statusPrDisplay={folderPrDisplay}
/>
<div className="pointer-events-auto absolute right-3 top-1.5">
<FolderPathStatusIndicator status={folderWorkspacePathStatus} />
@@ -4910,6 +4911,7 @@ const WorktreeList = React.memo(function WorktreeList({
const repoMap = useRepoMap()
const worktreeMap = useWorktreeMap()
const worktreeLineageById = useAppStore((s) => s.worktreeLineageById)
const workspaceLineageByChildKey = useAppStore((s) => s.workspaceLineageByChildKey)
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
const detectedWorktreesByRepo = useAppStore((s) => s.detectedWorktreesByRepo)
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
@@ -5017,6 +5019,11 @@ const WorktreeList = React.memo(function WorktreeList({
? s.prCache
: null
)
const hostedReviewCache = useAppStore((s) =>
s.settings?.experimentalNewWorktreeCardStyle === true && cardProps.includes('status')
? s.hostedReviewCache
: null
)
const settings = useAppStore((s) => s.settings)
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
@@ -6417,11 +6424,13 @@ const WorktreeList = React.memo(function WorktreeList({
repoMap={repoMap}
worktreeMap={worktreeMap}
worktreeLineageById={worktreeLineageById}
workspaceLineageByChildKey={workspaceLineageByChildKey}
repoOrder={repoOrder}
allRepoIds={allRepoIds}
onReorderHostSections={handleReorderHostSections}
onHostDragActiveChange={setHostDragActive}
prCache={prCache}
hostedReviewCache={hostedReviewCache}
workspaceStatuses={workspaceStatuses}
projectGrouping={projectGrouping}
projectGroups={projectGroups}
@@ -0,0 +1,160 @@
import { describe, expect, it } from 'vitest'
import type {
CheckStatus,
Repo,
Worktree,
WorktreeLineage,
WorkspaceLineage
} from '../../../../shared/types'
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope'
import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display'
const repo: Repo = {
id: 'repo-1',
path: '/repo',
displayName: 'repo',
badgeColor: '#999999',
addedAt: 1
}
function makeWorktree(overrides: Partial<Worktree> & { id: string }): Worktree {
const { id, ...rest } = overrides
return {
id,
repoId: repo.id,
path: `/worktrees/${id}`,
displayName: id,
branch: `refs/heads/${id}`,
head: 'abc123',
isBare: false,
isMainWorktree: false,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
linkedGitLabMR: null,
linkedGitLabIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
...rest
}
}
function makeWorkspaceLineage(worktree: Worktree): WorkspaceLineage {
return {
childWorkspaceKey: worktreeWorkspaceKey(worktree.id),
childInstanceId: worktree.instanceId ?? null,
parentWorkspaceKey: folderWorkspaceKey('folder-1'),
parentInstanceId: null,
origin: 'cli',
capture: { source: 'env-workspace', confidence: 'inferred' },
createdAt: 1
}
}
function makeWorktreeLineage(child: Worktree, parent: Worktree): WorktreeLineage {
return {
worktreeId: child.id,
worktreeInstanceId: child.instanceId ?? '',
parentWorktreeId: parent.id,
parentWorktreeInstanceId: parent.instanceId ?? '',
origin: 'cli',
capture: { source: 'env-workspace', confidence: 'inferred' },
createdAt: 1
}
}
describe('getFolderWorkspaceCardPrDisplay', () => {
it('uses failing attached PR status ahead of pending and passing PRs', () => {
const passing = makeWorktree({ id: 'passing', linkedPR: 1 })
const pending = makeWorktree({ id: 'pending', linkedPR: 2 })
const failing = makeWorktree({ id: 'failing', linkedPR: 3 })
const display = getFolderWorkspaceCardPrDisplay({
folderWorkspaceId: 'folder-1',
workspaceLineageByChildKey: {
[passing.id]: makeWorkspaceLineage(passing),
[pending.id]: makeWorkspaceLineage(pending),
[failing.id]: makeWorkspaceLineage(failing)
},
worktreeLineageById: {},
worktreeMap: new Map([
[passing.id, passing],
[pending.id, pending],
[failing.id, failing]
]),
repoMap: new Map([[repo.id, repo]]),
hostedReviewCache: null,
prCache: {
'repo-1::passing': { data: makePr(1, 'success') },
'repo-1::pending': { data: makePr(2, 'pending') },
'repo-1::failing': { data: makePr(3, 'failure') }
}
})
expect(display).toMatchObject({ number: 3, status: 'failure' })
})
it('uses pending attached PR status ahead of passing PRs', () => {
const passing = makeWorktree({ id: 'passing', linkedPR: 1 })
const pending = makeWorktree({ id: 'pending', linkedPR: 2 })
const display = getFolderWorkspaceCardPrDisplay({
folderWorkspaceId: 'folder-1',
workspaceLineageByChildKey: {
[passing.id]: makeWorkspaceLineage(passing),
[pending.id]: makeWorkspaceLineage(pending)
},
worktreeLineageById: {},
worktreeMap: new Map([
[passing.id, passing],
[pending.id, pending]
]),
repoMap: new Map([[repo.id, repo]]),
hostedReviewCache: null,
prCache: {
'repo-1::passing': { data: makePr(1, 'success') },
'repo-1::pending': { data: makePr(2, 'pending') }
}
})
expect(display).toMatchObject({ number: 2, status: 'pending' })
})
it('includes nested attached worktree PRs', () => {
const parent = makeWorktree({ id: 'parent', instanceId: 'parent' })
const nested = makeWorktree({ id: 'nested', instanceId: 'nested', linkedPR: 4 })
const display = getFolderWorkspaceCardPrDisplay({
folderWorkspaceId: 'folder-1',
workspaceLineageByChildKey: { [parent.id]: makeWorkspaceLineage(parent) },
worktreeLineageById: { [nested.id]: makeWorktreeLineage(nested, parent) },
worktreeMap: new Map([
[parent.id, parent],
[nested.id, nested]
]),
repoMap: new Map([[repo.id, repo]]),
hostedReviewCache: null,
prCache: {
'repo-1::nested': { data: makePr(4, 'success') }
}
})
expect(display).toMatchObject({ number: 4, status: 'success' })
})
})
function makePr(number: number, checksStatus: CheckStatus) {
return {
number,
title: `PR ${number}`,
state: 'open',
url: `https://example.test/pull/${number}`,
checksStatus,
updatedAt: '2026-01-01T00:00:00.000Z',
mergeable: 'UNKNOWN'
}
}
@@ -0,0 +1,272 @@
import type { AppState } from '@/store/types'
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review'
import { getGitHubPRCacheKey, getLegacyGitHubPRCacheKey } from '@/store/slices/github-cache-key'
import { branchName } from '@/lib/git-utils'
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
import type {
PRInfo,
Repo,
Worktree,
WorktreeLineage,
WorkspaceLineage
} from '../../../../shared/types'
import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope'
import { getWorktreeCardPrDisplay, type WorktreeCardPrDisplay } from './worktree-card-pr-display'
type HostedReviewCacheEntry = {
data?: HostedReviewInfo | null
linkedReviewHintKey?: string
}
type PrCacheEntry = {
data?: PRInfo | null
}
type FolderWorkspaceCardPrDisplayArgs = {
folderWorkspaceId: string
workspaceLineageByChildKey: Record<string, WorkspaceLineage> | null | undefined
worktreeLineageById: Record<string, WorktreeLineage> | null | undefined
worktreeMap: ReadonlyMap<string, Worktree>
repoMap: ReadonlyMap<string, Repo>
hostedReviewCache: Record<string, unknown> | null
prCache: Record<string, unknown> | null
settings?: AppState['settings']
}
const REVIEW_STATUS_PRIORITY: Record<NonNullable<WorktreeCardPrDisplay['status']>, number> = {
failure: 0,
pending: 1,
success: 2,
neutral: 3
}
export function getFolderWorkspaceCardPrDisplay({
folderWorkspaceId,
workspaceLineageByChildKey,
worktreeLineageById,
worktreeMap,
repoMap,
hostedReviewCache,
prCache,
settings
}: FolderWorkspaceCardPrDisplayArgs): WorktreeCardPrDisplay | null {
const reviews = getAttachedWorktreesForFolderWorkspaceCard({
folderWorkspaceId,
workspaceLineageByChildKey,
worktreeLineageById,
worktreeMap
})
.map((worktree) =>
getAttachedWorktreePrDisplay({
worktree,
repo: repoMap.get(worktree.repoId),
hostedReviewCache,
prCache,
settings
})
)
.filter((review): review is WorktreeCardPrDisplay => review !== null)
if (reviews.length === 0) {
return null
}
return reviews.sort(compareReviewDisplays)[0] ?? null
}
function getAttachedWorktreesForFolderWorkspaceCard({
folderWorkspaceId,
workspaceLineageByChildKey,
worktreeLineageById,
worktreeMap
}: Pick<
FolderWorkspaceCardPrDisplayArgs,
'folderWorkspaceId' | 'workspaceLineageByChildKey' | 'worktreeLineageById' | 'worktreeMap'
>): Worktree[] {
const folderKey = folderWorkspaceKey(folderWorkspaceId)
const directChildren = Object.values(workspaceLineageByChildKey ?? {})
.filter((lineage) => lineage.parentWorkspaceKey === folderKey)
.map((lineage) => getWorkspaceLineageChild(lineage, worktreeMap))
.filter((worktree): worktree is Worktree => worktree !== null)
const included = new Map(directChildren.map((worktree) => [worktree.id, worktree]))
let added = true
while (added) {
added = false
for (const lineage of Object.values(worktreeLineageById ?? {})) {
if (included.has(lineage.worktreeId) || !included.has(lineage.parentWorktreeId)) {
continue
}
const parent = worktreeMap.get(lineage.parentWorktreeId)
const child = worktreeMap.get(lineage.worktreeId)
if (!isCurrentLineagePair(parent, child, lineage)) {
continue
}
included.set(child.id, child)
added = true
}
}
return [...included.values()]
}
function getWorkspaceLineageChild(
lineage: WorkspaceLineage,
worktreeMap: ReadonlyMap<string, Worktree>
): Worktree | null {
const childScope = parseWorkspaceKey(lineage.childWorkspaceKey)
if (childScope?.type !== 'worktree') {
return null
}
const worktree = worktreeMap.get(childScope.worktreeId)
if (!worktree || worktree.isArchived) {
return null
}
if (lineage.childInstanceId && lineage.childInstanceId !== worktree.instanceId) {
return null
}
return worktree
}
function isCurrentLineagePair(
parent: Worktree | undefined,
child: Worktree | undefined,
lineage: WorktreeLineage
): child is Worktree {
return Boolean(
parent &&
child &&
!parent.isArchived &&
!child.isArchived &&
child.instanceId === lineage.worktreeInstanceId &&
parent.instanceId === lineage.parentWorktreeInstanceId
)
}
function getAttachedWorktreePrDisplay({
worktree,
repo,
hostedReviewCache,
prCache,
settings
}: {
worktree: Worktree
repo: Repo | undefined
hostedReviewCache: Record<string, unknown> | null
prCache: Record<string, unknown> | null
settings?: AppState['settings']
}): WorktreeCardPrDisplay | null {
if (!repo) {
return null
}
const branch = branchName(worktree.branch).trim()
if (!branch) {
return getLinkedReviewDisplay(worktree)
}
const hostedReviewCacheKey = getHostedReviewCacheKey(
repo.path,
branch,
settings,
repo.id,
repo.connectionId,
repo.executionHostId
)
const hostedReviewEntry = hostedReviewCache?.[hostedReviewCacheKey] as
| HostedReviewCacheEntry
| undefined
const hostedReviewDisplay = hostedReviewEntry
? getWorktreeCardPrDisplay(
hostedReviewEntry.data,
worktree.linkedPR,
worktree.linkedGitLabMR ?? null,
worktree.linkedBitbucketPR ?? null,
worktree.linkedAzureDevOpsPR ?? null,
worktree.linkedGiteaPR ?? null,
{ reviewHintKey: hostedReviewEntry.linkedReviewHintKey }
)
: null
if (hostedReviewDisplay) {
return hostedReviewDisplay
}
const cachedGitHubPr = getCachedGitHubPr({ worktree, repo, branch, prCache, settings })
if (cachedGitHubPr) {
return {
provider: 'github',
number: cachedGitHubPr.number,
title: cachedGitHubPr.title,
state: cachedGitHubPr.state,
url: cachedGitHubPr.url,
status: cachedGitHubPr.checksStatus
}
}
return getLinkedReviewDisplay(worktree)
}
function getLinkedReviewDisplay(worktree: Worktree): WorktreeCardPrDisplay | null {
return getWorktreeCardPrDisplay(
undefined,
worktree.linkedPR,
worktree.linkedGitLabMR ?? null,
worktree.linkedBitbucketPR ?? null,
worktree.linkedAzureDevOpsPR ?? null,
worktree.linkedGiteaPR ?? null
)
}
function getCachedGitHubPr({
worktree,
repo,
branch,
prCache,
settings
}: {
worktree: Worktree
repo: Repo
branch: string
prCache: Record<string, unknown> | null
settings?: AppState['settings']
}): PRInfo | null {
if (!prCache || worktree.linkedPR === null) {
return null
}
const cacheKey = getGitHubPRCacheKey(
repo.path,
repo.id,
branch,
settings,
repo.connectionId,
repo.executionHostId
)
const canUseLegacyPRCache =
!settings?.activeRuntimeEnvironmentId?.trim() && !repo.connectionId && !repo.executionHostId
const legacyRepoScopedCacheKey = canUseLegacyPRCache
? getLegacyGitHubPRCacheKey(repo.path, repo.id, branch)
: ''
const legacyPathScopedCacheKey = canUseLegacyPRCache
? getLegacyGitHubPRCacheKey(repo.path, undefined, branch)
: ''
const entry =
(prCache[cacheKey] as PrCacheEntry | undefined) ??
(legacyRepoScopedCacheKey
? (prCache[legacyRepoScopedCacheKey] as PrCacheEntry | undefined)
: undefined) ??
(legacyPathScopedCacheKey
? (prCache[legacyPathScopedCacheKey] as PrCacheEntry | undefined)
: undefined)
const pr = entry?.data ?? null
return pr?.number === worktree.linkedPR ? pr : null
}
function compareReviewDisplays(left: WorktreeCardPrDisplay, right: WorktreeCardPrDisplay): number {
return getReviewDisplayPriority(left) - getReviewDisplayPriority(right)
}
function getReviewDisplayPriority(review: WorktreeCardPrDisplay): number {
return review.status ? REVIEW_STATUS_PRIORITY[review.status] : 4
}
@@ -36,4 +36,30 @@ describe('worktree card property options', () => {
expect(options.map((option) => option.label)).toContain('Linear issues')
expect(options.map((option) => option.label)).toContain('Automation')
})
it('uses branch-only copy by default and without project groups', () => {
const defaultOptions = getWorktreeCardPropertyOptions()
const newCardOptions = getWorktreeCardPropertyOptions({
newCardStyle: true,
hasProjectGroups: false
})
expect(defaultOptions.find((option) => option.id === 'branch')?.label).toBe('Branch name')
expect(newCardOptions.find((option) => option.id === 'branch')?.label).toBe('Branch name')
})
it('keeps branch-only copy for legacy cards even with project groups', () => {
const options = getWorktreeCardPropertyOptions({ hasProjectGroups: true })
expect(options.find((option) => option.id === 'branch')?.label).toBe('Branch name')
})
it('mentions folder paths only for new card style with project groups', () => {
const options = getWorktreeCardPropertyOptions({
newCardStyle: true,
hasProjectGroups: true
})
expect(options.find((option) => option.id === 'branch')?.label).toBe('Branch / folder path')
})
})
@@ -158,21 +158,38 @@ const ISSUE_WORKTREE_CARD_PROPERTY_OPTIONS: WorktreeCardPropertyOption[] = [
type WorktreeCardPropertyOptionsInput = {
newCardStyle?: boolean
hasProjectGroups?: boolean
}
export function getWorktreeCardPropertyOptions({
newCardStyle = false
newCardStyle = false,
hasProjectGroups = false
}: WorktreeCardPropertyOptionsInput = {}): WorktreeCardPropertyOption[] {
const issueOptions = newCardStyle
? ISSUE_WORKTREE_CARD_PROPERTY_OPTIONS
: [TASK_WORKTREE_CARD_PROPERTY_OPTION]
const branchOption: WorktreeCardPropertyOption = {
id: 'branch',
properties: ['branch'],
get label() {
// Why: new-card project groups can contain folder workspaces, so the
// branch setting copy must describe both repo and folder identity.
return newCardStyle && hasProjectGroups
? translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.folderPathIdentity',
'Branch / folder path'
)
: translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.219ebf1961', 'Branch name')
}
}
if (newCardStyle) {
return [...issueOptions, ...BASE_WORKTREE_CARD_PROPERTY_OPTIONS.slice(1)]
return [...issueOptions, ...BASE_WORKTREE_CARD_PROPERTY_OPTIONS.slice(1, -1), branchOption]
}
return [
BASE_WORKTREE_CARD_PROPERTY_OPTIONS[0],
...issueOptions,
...BASE_WORKTREE_CARD_PROPERTY_OPTIONS.slice(1)
...BASE_WORKTREE_CARD_PROPERTY_OPTIONS.slice(1, -1),
branchOption
]
}
@@ -12,6 +12,7 @@ import {
getFolderBackedRepoWorktreeCardSurfaceInset,
getFolderWorkspaceCardContentIndent,
getFolderWorkspaceCardSurfaceInset,
getFolderWorkspaceRowGeometry,
getFlushWorktreeCardPaddingLeft,
getLineageChildrenInlineStyle,
getLineageEffectiveChildStart,
@@ -21,6 +22,17 @@ import {
getWorktreeCardSurfaceInset
} from './worktree-list-indentation'
function getFlushCardContentStart(args: {
surfaceInset: number
cardContentIndent: number
}): number {
return (
args.surfaceInset +
WORKTREE_CARD_SURFACE_MARGIN +
Math.max(FLUSH_CARD_MIN_CONTENT_INSET, args.cardContentIndent - FLUSH_CARD_CONTENT_PULLBACK)
)
}
describe('worktree list indentation', () => {
it('keeps ungrouped workspaces flush with the list', () => {
expect(getWorktreeCardContentIndent({ isGrouped: false, groupDepth: 4, lineageDepth: 0 })).toBe(
@@ -78,6 +90,118 @@ describe('worktree list indentation', () => {
expect(getFolderWorkspaceCardSurfaceInset({ isGrouped: false, groupDepth: 2 })).toBe(0)
})
it('preserves legacy folder-scanned folder workspace row geometry', () => {
const geometry = getFolderWorkspaceRowGeometry({
experimentalNewWorktreeCardStyle: false,
isFolderBackedWorkspaceChild: true,
isGrouped: true,
groupDepth: 1,
lineageDepth: 0
})
expect(geometry).toEqual({
surfaceInset: 14,
cardContentIndent: 6
})
expect(getFlushCardContentStart(geometry)).toBe(20)
})
it('preserves legacy nested folder-scanned folder workspace row geometry', () => {
const geometry = getFolderWorkspaceRowGeometry({
experimentalNewWorktreeCardStyle: false,
isFolderBackedWorkspaceChild: true,
isGrouped: true,
groupDepth: 2,
lineageDepth: 0
})
expect(geometry).toEqual({
surfaceInset: 24,
cardContentIndent: 6
})
expect(getFlushCardContentStart(geometry)).toBe(30)
})
it('preserves legacy manual grouped folder workspace row geometry', () => {
const geometry = getFolderWorkspaceRowGeometry({
experimentalNewWorktreeCardStyle: false,
isFolderBackedWorkspaceChild: false,
isGrouped: true,
groupDepth: 1,
lineageDepth: 0
})
expect(geometry).toEqual({
surfaceInset: 14,
cardContentIndent: 24
})
expect(getFlushCardContentStart(geometry)).toBe(38)
})
it('uses comparable repo worktree geometry for experimental folder-scanned folder workspaces', () => {
const geometry = getFolderWorkspaceRowGeometry({
experimentalNewWorktreeCardStyle: true,
isFolderBackedWorkspaceChild: true,
isGrouped: true,
groupDepth: 1,
lineageDepth: 0
})
expect(geometry).toEqual({
surfaceInset: 14,
cardContentIndent: 16
})
expect(getFlushCardContentStart(geometry)).toBe(30)
})
it('uses comparable repo worktree geometry for experimental nested folder workspaces', () => {
const geometry = getFolderWorkspaceRowGeometry({
experimentalNewWorktreeCardStyle: true,
isFolderBackedWorkspaceChild: true,
isGrouped: true,
groupDepth: 4,
lineageDepth: 3
})
expect(geometry).toEqual({
surfaceInset: 54,
cardContentIndent: 6
})
expect(getFlushCardContentStart(geometry)).toBe(60)
})
it('keeps experimental manual grouped folder workspaces on normal worktree geometry', () => {
const geometry = getFolderWorkspaceRowGeometry({
experimentalNewWorktreeCardStyle: true,
isFolderBackedWorkspaceChild: false,
isGrouped: true,
groupDepth: 1,
lineageDepth: 0
})
expect(geometry).toEqual({
surfaceInset: 14,
cardContentIndent: 24
})
expect(getFlushCardContentStart(geometry)).toBe(38)
})
it('keeps experimental flat folder workspaces on normal worktree geometry', () => {
const geometry = getFolderWorkspaceRowGeometry({
experimentalNewWorktreeCardStyle: true,
isFolderBackedWorkspaceChild: false,
isGrouped: false,
groupDepth: 3,
lineageDepth: 0
})
expect(geometry).toEqual({
surfaceInset: 0,
cardContentIndent: 0
})
expect(getFlushCardContentStart(geometry)).toBe(6)
})
it('caps header indentation separately from workspace content indentation', () => {
expect(getProjectGroupHeaderPaddingLeft(100)).toBe(70)
})
@@ -102,6 +102,61 @@ export function getFolderWorkspaceCardSurfaceInset(args: {
return Math.min(genericSurfaceInset, Math.max(0, maxSurfaceInset))
}
export function getFolderWorkspaceRowGeometry(args: {
experimentalNewWorktreeCardStyle: boolean
isFolderBackedWorkspaceChild: boolean
isGrouped: boolean
groupDepth: number
lineageDepth: number
}): {
surfaceInset: number
cardContentIndent: number
} {
if (args.experimentalNewWorktreeCardStyle && args.isFolderBackedWorkspaceChild) {
// Why: standalone folder workspace rows do not get a lineage wrapper
// offset, so align them to the comparable folder-backed repo row anchor.
const contentAnchor = getFolderBackedRepoWorktreeCardContentIndent({
groupDepth: args.groupDepth,
lineageDepth: 0
})
const surfaceInset = getFolderBackedRepoWorktreeCardSurfaceInset({
groupDepth: args.groupDepth,
lineageDepth: 0
})
return {
surfaceInset,
cardContentIndent: Math.max(0, contentAnchor - surfaceInset)
}
}
const contentIndent = args.isFolderBackedWorkspaceChild
? getFolderWorkspaceCardContentIndent({
groupDepth: args.groupDepth
})
: getWorktreeCardContentIndent({
isGrouped: args.isGrouped,
groupDepth: args.groupDepth,
lineageDepth: args.lineageDepth
})
// Why: legacy folder-scanned folder workspaces keep their compact anchor,
// while all other folder rows share the normal worktree row surface path.
const surfaceInset = args.isFolderBackedWorkspaceChild
? getFolderWorkspaceCardSurfaceInset({
isGrouped: true,
groupDepth: args.groupDepth
})
: getWorktreeCardSurfaceInset({
isGrouped: args.isGrouped,
groupDepth: args.groupDepth
})
return {
surfaceInset,
cardContentIndent: Math.max(0, contentIndent - surfaceInset)
}
}
export function getWorktreeCardSurfaceInset(args: {
isGrouped: boolean
groupDepth: number
+8 -2
View File
@@ -3779,7 +3779,8 @@
"8d62c68b35": "Notes",
"2d74665a56": "Ports",
"65a9820bd1": "Agent statuses",
"219ebf1961": "Branch name"
"219ebf1961": "Branch name",
"folderPathIdentity": "Branch / folder path"
},
"SshDisconnectedDialog": {
"ca4a7892af": "Connecting...",
@@ -3855,7 +3856,9 @@
"021538e1d1": "SSH disconnected",
"runtimeHostDisconnected": "Server disconnected",
"runtimeHostProject": "Project on Orca server",
"automationCreated": "Created by automation"
"automationCreated": "Created by automation",
"branchIdentity": "Branch",
"branchFolderPathIdentity": "Branch or folder path"
},
"WorktreeCardAgents": {
"1b0a156717": "Agents"
@@ -4301,6 +4304,9 @@
"current": "Current",
"searchPlaceholder": "Search worktrees...",
"empty": "No matching eligible worktrees."
},
"WorktreeCardStatusSlot": {
"branchIdentity": "Branch"
}
},
"shared": {
+8 -2
View File
@@ -3765,7 +3765,8 @@
"automation": "Automation",
"2d74665a56": "Puertos",
"65a9820bd1": "Estados del agente",
"219ebf1961": "Nombre de rama"
"219ebf1961": "Nombre de rama",
"folderPathIdentity": "Branch / folder path"
},
"SshDisconnectedDialog": {
"ca4a7892af": "Conectando...",
@@ -3841,7 +3842,9 @@
"021538e1d1": "SSH desconectado",
"runtimeHostDisconnected": "Server disconnected",
"runtimeHostProject": "Project on Orca server",
"automationCreated": "Created by automation"
"automationCreated": "Created by automation",
"branchIdentity": "Branch",
"branchFolderPathIdentity": "Branch or folder path"
},
"WorktreeCardAgents": {
"1b0a156717": "Agents"
@@ -4301,6 +4304,9 @@
"current": "Actual",
"searchPlaceholder": "Buscar árboles de trabajo...",
"empty": "No hay árboles de trabajo elegibles que coincidan."
},
"WorktreeCardStatusSlot": {
"branchIdentity": "Branch"
}
},
"shared": {
+8 -2
View File
@@ -3746,7 +3746,8 @@
"2d74665a56": "ポート",
"65a9820bd1": "Agent ステータス",
"219ebf1961": "ブランチ名",
"automation": "Automation"
"automation": "Automation",
"folderPathIdentity": "Branch / folder path"
},
"SshDisconnectedDialog": {
"ca4a7892af": "接続中...",
@@ -3822,7 +3823,9 @@
"021538e1d1": "SSH が切断されました",
"runtimeHostDisconnected": "Server disconnected",
"runtimeHostProject": "Project on Orca server",
"automationCreated": "Created by automation"
"automationCreated": "Created by automation",
"branchIdentity": "Branch",
"branchFolderPathIdentity": "Branch or folder path"
},
"WorktreeCardAgents": {
"1b0a156717": "Agents"
@@ -4301,6 +4304,9 @@
"current": "現在",
"searchPlaceholder": "ワークツリーを検索...",
"empty": "一致する対象ワークツリーがありません。"
},
"WorktreeCardStatusSlot": {
"branchIdentity": "Branch"
}
},
"shared": {
+8 -2
View File
@@ -3746,7 +3746,8 @@
"2d74665a56": "포트",
"65a9820bd1": "Agent 상태",
"219ebf1961": "브랜치 이름",
"automation": "자동화"
"automation": "자동화",
"folderPathIdentity": "Branch / folder path"
},
"SshDisconnectedDialog": {
"ca4a7892af": "연결 중...",
@@ -3822,7 +3823,9 @@
"021538e1d1": "SSH 연결이 끊어졌습니다.",
"runtimeHostDisconnected": "Server disconnected",
"runtimeHostProject": "Project on Orca server",
"automationCreated": "Created by automation"
"automationCreated": "Created by automation",
"branchIdentity": "Branch",
"branchFolderPathIdentity": "Branch or folder path"
},
"WorktreeCardAgents": {
"1b0a156717": "Agents"
@@ -4301,6 +4304,9 @@
"current": "현재",
"searchPlaceholder": "워크트리 검색...",
"empty": "일치하는 적격 워크트리가 없습니다."
},
"WorktreeCardStatusSlot": {
"branchIdentity": "Branch"
}
},
"shared": {
+10 -3
View File
@@ -1102,7 +1102,8 @@
"importHostSetup": "导入",
"notePasteTooLarge": "笔记字段的粘贴内容过大。",
"reuseExistingBranch": "复用分支",
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。"
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。",
"createMultiple": "Create more"
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "创建工作区之前选择项目、工作区名称和 Agent。"
@@ -3745,7 +3746,8 @@
"2d74665a56": "端口",
"65a9820bd1": "Agent 状态",
"219ebf1961": "分支名称",
"automation": "Automation"
"automation": "Automation",
"folderPathIdentity": "Branch / folder path"
},
"SshDisconnectedDialog": {
"ca4a7892af": "正在连接...",
@@ -3821,7 +3823,9 @@
"021538e1d1": "SSH 已断开连接",
"runtimeHostDisconnected": "Server disconnected",
"runtimeHostProject": "Project on Orca server",
"automationCreated": "Created by automation"
"automationCreated": "Created by automation",
"branchIdentity": "Branch",
"branchFolderPathIdentity": "Branch or folder path"
},
"WorktreeCardAgents": {
"1b0a156717": "Agents"
@@ -4300,6 +4304,9 @@
"current": "当前",
"searchPlaceholder": "搜索工作树...",
"empty": "没有匹配的可用工作树。"
},
"WorktreeCardStatusSlot": {
"branchIdentity": "Branch"
}
},
"shared": {