Show child workspace statuses in the children toggle (#32224)

* Show child workspace status counts in lineage toggle

* Give child status summaries room and wrap narrow layouts

* Use solid working dots and keep child summaries on one line

* Restore children icon and clarify worktree tooltip

* Keep child totals clear of status counts in narrow layouts
This commit is contained in:
Neil
2026-10-23 01:50:50 -07:00
committed by GitHub
parent f9e883a4bf
commit b4ee652bcf
10 changed files with 171 additions and 46 deletions
@@ -35,6 +35,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
contentIndent = 0,
flushSurface = false,
lineageChildCount = 0,
lineageChildWorktreeIds,
lineageCollapsed = false,
lineageChildren,
lineageChildrenStyle,
@@ -71,6 +72,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
contentIndent,
flushSurface,
lineageChildCount,
lineageChildWorktreeIds,
lineageCollapsed,
lineageChildren,
lineageChildrenStyle,
@@ -0,0 +1,130 @@
import React, { useId } from 'react'
import { ChevronDown, Workflow } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-status'
import { cn } from '@/lib/utils'
import StatusIndicator from './StatusIndicator'
import { useWorktreeActivityStatuses } from './use-worktree-activity-statuses'
const EMPTY_CHILD_IDS: readonly string[] = []
const STATUS_ORDER: readonly WorktreeStatus[] = [
'permission',
'failed',
'unconfirmed',
'working',
'monitoring',
'interrupted',
'done',
'active'
]
export function WorktreeLineageToggle({
childWorktreeIds = EMPTY_CHILD_IDS,
collapsed,
ariaLabel,
count,
newCardStyle,
onToggle
}: {
childWorktreeIds?: readonly string[]
collapsed: boolean
ariaLabel: string
count: number
newCardStyle: boolean
onToggle?: React.MouseEventHandler<HTMLButtonElement>
}): React.JSX.Element {
const descriptionId = useId()
const statuses = useWorktreeActivityStatuses(childWorktreeIds)
const counts = new Map<WorktreeStatus, number>()
for (const status of statuses.values()) {
counts.set(status, (counts.get(status) ?? 0) + 1)
}
const summary = STATUS_ORDER.filter((status) => counts.has(status))
const description = summary
.map((status) => `${counts.get(status)} ${getWorktreeStatusLabel(status)}`)
.join(', ')
return (
<div
className={cn(
'@container relative mt-1 flex min-w-0 justify-start',
!newCardStyle && '-ml-1'
)}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="outline"
size="chip"
className="relative z-10 h-auto min-h-4.5 max-w-full"
aria-label={ariaLabel}
aria-describedby={description ? descriptionId : undefined}
aria-expanded={!collapsed}
onClick={onToggle}
>
<span className="inline-flex min-w-0 flex-wrap items-center gap-1 px-1 py-0.5">
<span className="inline-flex shrink-0 items-center gap-1">
<Workflow className="size-2.5 shrink-0" />
<span className="tabular-nums">{count}</span>
</span>
<span className={cn('truncate', summary.length > 0 && 'hidden @min-[13rem]:block')}>
{count === 1
? translate('auto.components.sidebar.WorktreeList.0c6ee14f23', 'child')
: translate('auto.components.sidebar.WorktreeList.045a8aed48', 'children')}
</span>
<span className="inline-flex min-w-0 max-w-full items-center gap-0.5">
{summary.length > 0 ? (
<span className="inline-flex min-w-0 flex-wrap items-center gap-1 border-l border-border pl-1">
{summary.map((status) => (
<span
key={status}
className="inline-flex shrink-0 items-center gap-0.5 tabular-nums"
data-child-worktree-status={status}
aria-hidden="true"
>
{status === 'working' ? (
<span className="inline-flex size-3 shrink-0 items-center justify-center">
<span className="size-2 rounded-full bg-agent-working" />
</span>
) : (
<StatusIndicator status={status} showTooltip={false} />
)}
{counts.get(status)}
</span>
))}
</span>
) : null}
<ChevronDown
className={cn(
'size-2.5 shrink-0 transition-transform',
collapsed && '-rotate-90'
)}
/>
</span>
</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{collapsed
? translate(
'auto.components.sidebar.WorktreeCard.showChildWorktrees',
'Show children worktrees'
)
: translate(
'auto.components.sidebar.WorktreeCard.hideChildWorktrees',
'Hide children worktrees'
)}
{description ? <div>{description}</div> : null}
</TooltipContent>
</Tooltip>
{description ? (
<span id={descriptionId} className="sr-only">
{description}
</span>
) : null}
</div>
)
}
@@ -33,6 +33,7 @@ export type WorktreeCardProps = {
contentIndent?: number
flushSurface?: boolean
lineageChildCount?: number
lineageChildWorktreeIds?: readonly string[]
lineageCollapsed?: boolean
lineageChildren?: React.ReactNode
lineageChildrenStyle?: React.CSSProperties
@@ -1,12 +1,11 @@
import React from 'react'
import { AlertTriangle, ChevronDown, Workflow } from 'lucide-react'
import { AlertTriangle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import { LinearAgentSkillSetupPrompt } from './LinearAgentSkillSetupPrompt'
import WorktreeCardAgents from './WorktreeCardAgents'
import { WorktreeLineageToggle } from './WorktreeLineageToggle'
import type { WorktreeCardPresentation } from './worktree-card-presentation'
import type { WorktreeCardController } from './use-worktree-card-controller'
@@ -24,6 +23,7 @@ export function WorktreeCardSecondaryRows({
isActive,
newCardStyle,
lineageCollapsed,
lineageChildWorktreeIds,
onLineageToggle,
remoteBranchConflict,
showInlineAgentList,
@@ -31,7 +31,7 @@ export function WorktreeCardSecondaryRows({
compactInlineAgentRows,
showLineageChildChip,
lineageChildAriaLabel,
childWorkspaceShortLabel,
lineageChildCount,
isDeleting
} = card
const { hasMetaRow } = presentation
@@ -94,43 +94,14 @@ export function WorktreeCardSecondaryRows({
)}
{showLineageChildChip && (
<div
className={cn('relative mt-1 flex min-w-0 justify-start', !newCardStyle && '-ml-1')}
style={{
color: 'color-mix(in srgb, var(--muted-foreground) 42%, var(--worktree-sidebar))'
}}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="xs"
className="relative z-10 h-[18px] max-w-[8rem] gap-1 rounded-md border border-worktree-sidebar-border bg-worktree-sidebar px-1.5 text-[10px] font-medium leading-none text-muted-foreground shadow-none hover:bg-worktree-sidebar-accent hover:text-foreground focus-visible:ring-1 focus-visible:ring-worktree-sidebar-ring"
aria-label={lineageChildAriaLabel}
aria-expanded={!lineageCollapsed}
onClick={onLineageToggle}
>
<Workflow className="size-2.5" />
<span className="truncate">{childWorkspaceShortLabel}</span>
<ChevronDown
className={cn('size-2.5 transition-transform', lineageCollapsed && '-rotate-90')}
/>
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{lineageCollapsed
? translate(
'auto.components.sidebar.WorktreeCard.8cb634cda6',
'Show child workspaces'
)
: translate(
'auto.components.sidebar.WorktreeCard.57eaa61b55',
'Hide child workspaces'
)}
</TooltipContent>
</Tooltip>
</div>
<WorktreeLineageToggle
childWorktreeIds={lineageChildWorktreeIds}
collapsed={lineageCollapsed}
ariaLabel={lineageChildAriaLabel}
count={lineageChildCount}
newCardStyle={newCardStyle}
onToggle={onLineageToggle}
/>
)}
</>
)
@@ -222,7 +222,11 @@ describe('buildRows workspace lineage nesting', () => {
[secondChildId, 1],
[childId, 1]
])
expect(items[0]).toMatchObject({ lineageChildCount: 2, lineageCollapsed: false })
expect(items[0]).toMatchObject({
lineageChildCount: 2,
lineageChildWorktreeIds: [secondChildId, childId],
lineageCollapsed: false
})
})
it('rejects stale resolved lineage after a parent instance is replaced', () => {
@@ -471,6 +475,7 @@ describe('buildRows workspace lineage nesting', () => {
type: 'item',
worktree: { id: parent.id },
lineageChildCount: 1,
lineageChildWorktreeIds: [child.id],
lineageCollapsed: true
})
})
@@ -204,6 +204,7 @@ export function renderWorktreeItemRow(
inPinnedSection={itemRow.sectionKey === PINNED_GROUP_KEY}
renameRowKey={itemRow.rowKey}
lineageChildCount={itemRow.lineageChildCount}
lineageChildWorktreeIds={itemRow.lineageChildWorktreeIds}
lineageCollapsed={itemRow.lineageCollapsed}
lineageChildren={lineageChildren}
lineageChildrenStyle={lineageChildrenStyle}
+2
View File
@@ -2156,6 +2156,8 @@
"01f45d3d8a": "sidebar",
"021538e1d1": "SSH disconnected",
"1d66d84f0b": "string",
"57eaa61b55": "Hide child workspaces",
"8cb634cda6": "Show child workspaces",
"93aebe4529": "Folder",
"automationCreated": "Created by automation",
"branchFolderPathIdentity": "Branch or folder path",
+3 -1
View File
@@ -6101,7 +6101,9 @@
"automationCreated": "Created by automation",
"branchIdentity": "Branch",
"branchFolderPathIdentity": "Branch or folder path",
"ef18787206": "Queued for deletion"
"ef18787206": "Queued for deletion",
"showChildWorktrees": "Show children worktrees",
"hideChildWorktrees": "Hide children worktrees"
},
"WorktreeCardAgents": {
"1b0a156717": "Agents"
+13 -3
View File
@@ -77,9 +77,18 @@ export function appendWorktreeRows(
hostContextLabelByWorktreeIdentity?: ReadonlyMap<string, string>
}
): void {
const occurrences = getExpandedWorktreeOccurrences(
plan.bySection.get(options.sectionKey) ?? [],
(worktree) => options.collapsedGroups.has(getWorktreeLineageGroupKey(worktree))
const sectionOccurrences = plan.bySection.get(options.sectionKey) ?? []
// Keep disclosure membership available while child rows are collapsed.
const childIdsByParent = new Map<string, string[]>()
for (const occurrence of sectionOccurrences) {
if (occurrence.parent) {
const children = childIdsByParent.get(occurrence.parent.key) ?? []
children.push(occurrence.row.id)
childIdsByParent.set(occurrence.parent.key, children)
}
}
const occurrences = getExpandedWorktreeOccurrences(sectionOccurrences, (worktree) =>
options.collapsedGroups.has(getWorktreeLineageGroupKey(worktree))
)
for (const occurrence of occurrences) {
const worktree = occurrence.row
@@ -94,6 +103,7 @@ export function appendWorktreeRows(
groupDepth: options.groupDepth,
isLastLineageChild: occurrence.isLastChild,
lineageChildCount: occurrence.childCount,
lineageChildWorktreeIds: childIdsByParent.get(occurrence.key),
showProjectName: occurrence.showProjectName,
hostContextLabel:
options.hostContextLabelByWorktreeIdentity?.get(getWorktreeHostIdentity(worktree)) ??
+1
View File
@@ -56,6 +56,7 @@ export type WorktreeRow = {
groupDepth: number
isLastLineageChild: boolean
lineageChildCount: number
lineageChildWorktreeIds?: readonly string[]
lineageGroupKey?: string
lineageCollapsed?: boolean
hostContextLabel?: string