mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)) ??
|
||||
|
||||
@@ -56,6 +56,7 @@ export type WorktreeRow = {
|
||||
groupDepth: number
|
||||
isLastLineageChild: boolean
|
||||
lineageChildCount: number
|
||||
lineageChildWorktreeIds?: readonly string[]
|
||||
lineageGroupKey?: string
|
||||
lineageCollapsed?: boolean
|
||||
hostContextLabel?: string
|
||||
|
||||
Reference in New Issue
Block a user