diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index bbcc35e750e..ea543a0966f 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -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, diff --git a/src/renderer/src/components/sidebar/WorktreeLineageToggle.tsx b/src/renderer/src/components/sidebar/WorktreeLineageToggle.tsx new file mode 100644 index 00000000000..69f215f8604 --- /dev/null +++ b/src/renderer/src/components/sidebar/WorktreeLineageToggle.tsx @@ -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 +}): React.JSX.Element { + const descriptionId = useId() + const statuses = useWorktreeActivityStatuses(childWorktreeIds) + const counts = new Map() + 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 ( +
+ + + + + + {collapsed + ? translate( + 'auto.components.sidebar.WorktreeCard.showChildWorktrees', + 'Show children worktrees' + ) + : translate( + 'auto.components.sidebar.WorktreeCard.hideChildWorktrees', + 'Hide children worktrees' + )} + {description ?
{description}
: null} +
+
+ {description ? ( + + {description} + + ) : null} +
+ ) +} diff --git a/src/renderer/src/components/sidebar/worktree-card-model.ts b/src/renderer/src/components/sidebar/worktree-card-model.ts index 43aa0fad75e..282521d911d 100644 --- a/src/renderer/src/components/sidebar/worktree-card-model.ts +++ b/src/renderer/src/components/sidebar/worktree-card-model.ts @@ -33,6 +33,7 @@ export type WorktreeCardProps = { contentIndent?: number flushSurface?: boolean lineageChildCount?: number + lineageChildWorktreeIds?: readonly string[] lineageCollapsed?: boolean lineageChildren?: React.ReactNode lineageChildrenStyle?: React.CSSProperties diff --git a/src/renderer/src/components/sidebar/worktree-card-secondary-rows.tsx b/src/renderer/src/components/sidebar/worktree-card-secondary-rows.tsx index c8188596f14..6062821252d 100644 --- a/src/renderer/src/components/sidebar/worktree-card-secondary-rows.tsx +++ b/src/renderer/src/components/sidebar/worktree-card-secondary-rows.tsx @@ -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 && ( -
- - - - - - {lineageCollapsed - ? translate( - 'auto.components.sidebar.WorktreeCard.8cb634cda6', - 'Show child workspaces' - ) - : translate( - 'auto.components.sidebar.WorktreeCard.57eaa61b55', - 'Hide child workspaces' - )} - - -
+ )} ) diff --git a/src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts b/src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts index e0fc9f8873b..48284e03bfb 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts @@ -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 }) }) diff --git a/src/renderer/src/components/sidebar/worktree-list/rows/item-row.tsx b/src/renderer/src/components/sidebar/worktree-list/rows/item-row.tsx index 9540b9023ec..9e7be884547 100644 --- a/src/renderer/src/components/sidebar/worktree-list/rows/item-row.tsx +++ b/src/renderer/src/components/sidebar/worktree-list/rows/item-row.tsx @@ -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} diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 75f112ba3a1..a35b10f3b3b 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -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", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 0745ee9c113..689f564e4af 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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" diff --git a/src/shared/sidebar-list/row-builders.ts b/src/shared/sidebar-list/row-builders.ts index f92a12af757..646cecb2b01 100644 --- a/src/shared/sidebar-list/row-builders.ts +++ b/src/shared/sidebar-list/row-builders.ts @@ -77,9 +77,18 @@ export function appendWorktreeRows( hostContextLabelByWorktreeIdentity?: ReadonlyMap } ): 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() + 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)) ?? diff --git a/src/shared/sidebar-list/row-types.ts b/src/shared/sidebar-list/row-types.ts index 9c58f04be2f..8a7c2f966c5 100644 --- a/src/shared/sidebar-list/row-types.ts +++ b/src/shared/sidebar-list/row-types.ts @@ -56,6 +56,7 @@ export type WorktreeRow = { groupDepth: number isLastLineageChild: boolean lineageChildCount: number + lineageChildWorktreeIds?: readonly string[] lineageGroupKey?: string lineageCollapsed?: boolean hostContextLabel?: string