Refine workspace lineage child chip

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-05-15 01:29:29 -07:00
co-authored by Orca
parent 7948ef2ce8
commit 8190cd572b
2 changed files with 61 additions and 54 deletions
@@ -3,10 +3,12 @@ import React, { useEffect, useMemo, useCallback, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useAppStore } from '@/store'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import {
AlertTriangle,
Bell,
ChevronDown,
GitMerge,
LoaderCircle,
CircleCheck,
@@ -55,6 +57,9 @@ type WorktreeCardProps = {
hideRepoBadge?: boolean
parentLabel?: string
lineageState?: 'valid' | 'missing'
lineageChildCount?: number
lineageCollapsed?: boolean
onLineageToggle?: (event: React.MouseEvent<HTMLButtonElement>) => void
onSelectionGesture?: (event: React.MouseEvent<HTMLDivElement>, worktreeId: string) => boolean
onContextMenuSelect?: (event: React.MouseEvent<HTMLDivElement>) => readonly Worktree[]
}
@@ -74,7 +79,10 @@ const WorktreeCard = React.memo(function WorktreeCard({
onContextMenuSelect,
hideRepoBadge,
parentLabel,
lineageState
lineageState,
lineageChildCount = 0,
lineageCollapsed = false,
onLineageToggle
}: WorktreeCardProps) {
const openModal = useAppStore((s) => s.openModal)
const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta)
@@ -432,6 +440,13 @@ const WorktreeCard = React.memo(function WorktreeCard({
)
const unreadTooltip = worktree.isUnread ? 'Mark read' : 'Mark unread'
const childWorkspaceLabel = `${lineageChildCount} child ${
lineageChildCount === 1 ? 'workspace' : 'workspaces'
}`
const childWorkspaceShortLabel = `${lineageChildCount} ${
lineageChildCount === 1 ? 'child' : 'children'
}`
const showLineageChildChip = lineageChildCount > 0 && onLineageToggle !== undefined
// Why: the 'unread' card property is the user's opt-out. When off, we render
// as if the workspace is read so bold emphasis never appears. The persisted
@@ -652,6 +667,34 @@ const WorktreeCard = React.memo(function WorktreeCard({
<CacheTimer worktreeId={worktree.id} />
{showLineageChildChip && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="xs"
className="h-[18px] max-w-[7.5rem] shrink-0 gap-1 rounded-md border border-sidebar-border bg-sidebar-accent/55 px-1.5 text-[10px] font-medium leading-none text-muted-foreground shadow-none hover:bg-sidebar-accent hover:text-foreground focus-visible:ring-1 focus-visible:ring-sidebar-ring"
aria-label={`${lineageCollapsed ? 'Show' : 'Hide'} ${childWorkspaceLabel}`}
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 ? 'Show child workspaces' : 'Hide child workspaces'}
</TooltipContent>
</Tooltip>
)}
{parentLabel && (
<Badge
variant="outline"
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
import React, { useMemo, useCallback, useRef, useState, useEffect, useLayoutEffect } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import { ChevronDown, ChevronRight, CircleX, Plus } from 'lucide-react'
import { ChevronDown, CircleX, Plus } from 'lucide-react'
import { useAppStore } from '@/store'
import {
getAllWorktreesFromState,
@@ -94,20 +94,20 @@ function LineageGuides({ row }: { row: Extract<Row, { type: 'item' }> }) {
continues ? (
<span
key={index}
className="absolute top-0 bottom-0 w-px bg-current/80"
className="absolute top-0 bottom-0 w-[2px] rounded-full bg-current/90"
style={{ left: `${index * step + lineX}px` }}
/>
) : null
)}
<span
className="absolute top-0 w-px bg-current/80"
className="absolute top-0 w-[2px] rounded-full bg-current/90"
style={{
left: `${currentX}px`,
height: row.isLastLineageChild ? '50%' : '100%'
}}
/>
<span
className="absolute h-px bg-current/80"
className="absolute h-[2px] rounded-full bg-current/90"
style={{
left: `${currentX}px`,
top: '50%',
@@ -118,45 +118,6 @@ function LineageGuides({ row }: { row: Extract<Row, { type: 'item' }> }) {
)
}
function LineageDisclosure({
row,
onToggle
}: {
row: Extract<Row, { type: 'item' }>
onToggle: (event: React.MouseEvent<HTMLButtonElement>) => void
}) {
if (!row.lineageGroupKey || row.lineageChildCount <= 0) {
return null
}
const label = `${row.lineageCollapsed ? 'Show' : 'Hide'} ${row.lineageChildCount} child ${
row.lineageChildCount === 1 ? 'workspace' : 'workspaces'
}`
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute right-2 top-2 z-10 size-5 rounded-md bg-sidebar/80 text-muted-foreground opacity-80 shadow-sm ring-1 ring-sidebar-border/70 backdrop-blur-sm transition-colors hover:bg-sidebar-accent hover:text-foreground hover:opacity-100 focus-visible:opacity-100"
aria-label={label}
aria-expanded={!row.lineageCollapsed}
onClick={onToggle}
>
<ChevronRight
className={cn('size-3 transition-transform', !row.lineageCollapsed && 'rotate-90')}
/>
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{row.lineageCollapsed ? 'Show child workspaces' : 'Hide child workspaces'}
</TooltipContent>
</Tooltip>
)
}
type VirtualizedWorktreeViewportProps = {
rows: Row[]
activeWorktreeId: string | null
@@ -654,6 +615,8 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
)
}
const lineageToggleGroupKey = row.lineageGroupKey
return (
<div
key={vItem.key}
@@ -684,16 +647,17 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
row.depth > 0 && row.lineageState === 'valid' ? undefined : row.parentLabel
}
lineageState={row.lineageState}
/>
<LineageDisclosure
row={row}
onToggle={(event) => {
event.preventDefault()
event.stopPropagation()
if (row.lineageGroupKey) {
toggleGroup(row.lineageGroupKey)
}
}}
lineageChildCount={row.lineageChildCount}
lineageCollapsed={row.lineageCollapsed}
onLineageToggle={
lineageToggleGroupKey
? (event) => {
event.preventDefault()
event.stopPropagation()
toggleGroup(lineageToggleGroupKey)
}
: undefined
}
/>
</div>
</div>