From 006bef5155c02451bee1bb17fc49ec41b446abcd Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:23:35 -0800 Subject: [PATCH] Improve experimental worktree cards with compact agent summaries - Add an optional details row for workspace metadata and indicators - Roll up descendant activity timestamps to parent cards --- .../sidebar/WorktreeCardSlotAgentLines.tsx | 43 ++++- .../WorktreeCardSlotContent.fixture.tsx | 54 +++++-- .../sidebar/WorktreeCardSlotContent.tsx | 153 +++++++++++++----- .../activity/WorktreeCardAgents.tsx | 7 + .../controller/worktree-card-model.ts | 4 +- .../details/worktree-card-details-items.tsx | 4 +- .../workspace-card/surface/WorktreeCard.tsx | 4 +- .../surface/worktree-card-option-items.tsx | 18 +++ .../sidebar/worktree-card-slot-rollup.ts | 30 ++++ .../sidebar/worktree-card-slot-signal.ts | 8 +- .../sidebar/worktree-list/rows/item-row.tsx | 2 +- src/renderer/src/i18n/locales/en.json | 3 +- src/shared/global-settings-base.ts | 2 + src/shared/global-settings-types.ts | 2 - src/shared/sidebar-list/row-types.ts | 4 +- .../sidebar-list/sections/row-builders.ts | 8 +- .../worktree/card-display-settings-types.ts | 6 + 17 files changed, 281 insertions(+), 71 deletions(-) create mode 100644 src/shared/worktree/card-display-settings-types.ts diff --git a/src/renderer/src/components/sidebar/WorktreeCardSlotAgentLines.tsx b/src/renderer/src/components/sidebar/WorktreeCardSlotAgentLines.tsx index 7a210c93c19..b80b513a043 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardSlotAgentLines.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardSlotAgentLines.tsx @@ -1,4 +1,5 @@ import React, { useCallback } from 'react' +import { ChevronDown } from 'lucide-react' import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData' import { useAgentRowConversationName } from '@/components/dashboard/use-agent-row-conversation-name' import { translate } from '@/i18n/i18n' @@ -103,6 +104,46 @@ const SlotAgentLineRow = React.memo(function SlotAgentLineRow({ ) }) +/** Expanded agents' header: the label sits in the icon column and the chevron in the slot column. */ +export function SlotAgentLinesHeader({ + label, + onCollapse +}: { + label: string + onCollapse: () => void +}): React.JSX.Element { + const handleClick = useCallback( + (event: React.MouseEvent) => { + event.stopPropagation() + onCollapse() + }, + [onCollapse] + ) + return ( + + ) +} + +function stopPropagation(event: React.SyntheticEvent): void { + event.stopPropagation() +} + /** Experimental 2.0 agent lines: icon, what it's doing or said, and the line's own slot. */ export function WorktreeCardSlotAgentLines({ worktreeId, @@ -138,7 +179,7 @@ export function WorktreeCardSlotAgentLines({ } return (
= {} const agentStatusByPaneKey: Record = {} const unreadAgentCompletionPanes: Record = {} @@ -76,7 +78,9 @@ function seedWorkspaces(seeds: readonly WorkspaceSeed[], now: number): void { stateHistory: [], ...(agent.toolName ? { toolName: agent.toolName, toolInput: agent.toolInput } : {}), ...(agent.message ? { lastAssistantMessage: agent.message } : {}), - ...(agent.failed ? { mainAgent: { state: 'done', outcome: 'failure', stateStartedAt: now } } : {}) + ...(agent.failed + ? { mainAgent: { state: 'done', outcome: 'failure', stateStartedAt: now } } + : {}) } ptyIdsByTabId[tabId] = [`pty-${tabId}`] if (agent.unread) { @@ -309,7 +313,7 @@ function Card({ children, childCount = 0, collapsed = false, - hiddenDescendantIds + descendantIds }: { worktree: Worktree nested?: boolean @@ -317,7 +321,7 @@ function Card({ children?: React.ReactNode childCount?: number collapsed?: boolean - hiddenDescendantIds?: readonly string[] + descendantIds?: readonly string[] }): React.JSX.Element { return (
@@ -330,7 +334,7 @@ function Card({ nativeDragEnabled={false} lineageChildCount={childCount} lineageCollapsed={collapsed} - lineageHiddenDescendantIds={hiddenDescendantIds} + lineageDescendantIds={descendantIds} lineageChildren={children} onLineageToggle={childCount > 0 ? noop : undefined} /> @@ -348,10 +352,8 @@ const nestedWorktrees = worktreesFor(NESTED, now) FOLDER_WORKSPACE.isUnread = false FOLDER_WORKSPACE.lastActivityAt = now - 9 * MINUTE -export const exceptions = defineFixture({ - width: SIDEBAR_WIDTH, - setup: () => seedWorkspaces(FLAT, now), - render: () => ( +function FlatList(): React.JSX.Element { + return ( {FLAT.map((seed) => ( ) +} + +export const exceptions = defineFixture({ + width: SIDEBAR_WIDTH, + setup: () => seedWorkspaces(FLAT, now), + render: () => }) export const nesting = defineFixture({ @@ -370,8 +378,17 @@ export const nesting = defineFixture({ setup: () => seedWorkspaces(NESTED, now), render: () => ( - - + + @@ -379,8 +396,23 @@ export const nesting = defineFixture({ worktree={nestedWorktrees.get('wt-collapsed-parent')!} childCount={1} collapsed - hiddenDescendantIds={['wt-collapsed-child']} + descendantIds={['wt-collapsed-child']} /> ) }) + +export const agentsExpanded = defineFixture({ + width: SIDEBAR_WIDTH, + setup: () => { + seedWorkspaces(FLAT, now) + const none = new Set() + const state = { + expandedLineageParents: none, + expandedIdleChildGroups: none, + compactRootListExpanded: true + } + FLAT.forEach((seed) => agentExpansion.seedWorktreeAgentExpansionStateForTests(seed.id, state)) + }, + render: () => +}) diff --git a/src/renderer/src/components/sidebar/WorktreeCardSlotContent.tsx b/src/renderer/src/components/sidebar/WorktreeCardSlotContent.tsx index de53bdcb4d2..ba3fc9499b1 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardSlotContent.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardSlotContent.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { ChevronDown, ChevronRight } from 'lucide-react' import { useShallow } from 'zustand/react/shallow' @@ -14,7 +14,7 @@ import { agentRowDisplayDotState as getAgentDotState } from '@/lib/agent-row-dot import { WorktreeCardIdentityHover } from './worktree-card-identity-hover' import type { WorktreeCardPresentation } from './workspace-card/surface/worktree-card-presentation' import { WorktreeCardSecondaryRows } from './workspace-card/surface/worktree-card-secondary-rows' -import { useCollapsedLineageSlot } from './worktree-card-slot-rollup' +import { useCollapsedLineageSlot, useLineageLatestAt } from './worktree-card-slot-rollup' import { classifySlotLine, compareSlotLines, @@ -26,9 +26,21 @@ import { WorktreeCardRenameFailedButton } from './worktree-card-title-actions' import { WorktreeCardSlot } from './WorktreeCardSlot' -import { WorktreeCardSlotAgentLines, type SlotAgentLine } from './WorktreeCardSlotAgentLines' +import { useWorktreeAgentExpansionState } from './workspace-card/activity/worktree-card-agents-expansion-state' +import { + CompactAgentExpansion, + CompactAgentSummaryButton +} from './workspace-card/activity/worktree-card-compact-agents' +import { dispatchSuppressScrollAdjustment } from './workspace-card/activity/WorktreeCardAgents' +import { translate } from '@/i18n/i18n' +import { + SlotAgentLinesHeader, + WorktreeCardSlotAgentLines, + type SlotAgentLine +} from './WorktreeCardSlotAgentLines' import { shouldBeginWorktreeRename } from './workspace-card/controller/worktree-card-model' import { WorktreeTitleInlineRename } from './workspace-card/identity/WorktreeTitleInlineRename' +import { WorktreeCardMetaRow } from './workspace-card/metadata/worktree-card-meta-row' const NO_DESCENDANTS: readonly string[] = [] @@ -79,42 +91,82 @@ export function WorktreeCardSlotContent({ showDeleteQuickAction, showLineageChildChip, lineageCollapsed, - lineageHiddenDescendantIds, + lineageDescendantIds, onLineageToggle, lineageChildAriaLabel, - childWorkspaceShortLabel + childWorkspaceShortLabel, + settings } = card const lines = useSlotAgentLines(worktree.id) const now = useNow(30_000) const hasLiveMarker = useAppStore((s) => workspaceHasLiveMarker(s, worktree.id)) // A flag with no tab or agent marker behind it was set by the user. const manualUnread = worktree.isUnread && !hasLiveMarker + const descendantIds = lineageDescendantIds ?? NO_DESCENDANTS const collapsedLineage = useCollapsedLineageSlot( - lineageCollapsed ? (lineageHiddenDescendantIds ?? NO_DESCENDANTS) : NO_DESCENDANTS + lineageCollapsed ? descendantIds : NO_DESCENDANTS ) - const ownLinesShown = showInlineAgentList && lines.length > 0 + const lineageLatestAt = useLineageLatestAt(descendantIds) + // Several agents fold into the 1.0 summary row; the title slot speaks for them until expanded. + const { compactRootListExpanded: agentsExpanded, toggleCompactRootList } = + useWorktreeAgentExpansionState(worktree.id) + const toggleAgents = useCallback(() => { + dispatchSuppressScrollAdjustment() + toggleCompactRootList() + }, [toggleCompactRootList]) + const showLines = showInlineAgentList && lines.length > 0 + const foldsAgents = showLines && lines.length > 1 + const ownLinesShown = showLines && (!foldsAgents || agentsExpanded) const titleSlot = titleRowSlot({ offline: isRuntimeDisconnected, hiddenLines: ownLinesShown ? [] : lines.map((line) => line.signal), collapsedLineage, manualUnread, ownLinesShown, - lastActivityAt: worktree.lastActivityAt > 0 ? worktree.lastActivityAt : null + lastActivityAt: worktree.lastActivityAt > 0 ? worktree.lastActivityAt : null, + lineageLatestAt }) + const showDetailsRow = + settings?.worktreeCardDetailsRow === true && + (presentation.hasMetaRow || presentation.detailsAndPorts !== null) const titleBold = manualUnread || lines.some((line) => line.signal.kind === 'unread') + const agentLines = showLines ? ( + + ) : null + + const agentCountLabel = translate('sidebar.agentSummary.agentCount', '{{count}} agents', { + count: lines.length, + defaultValue_one: '{{count}} agent' + }) const slotRows = ( <> - {ownLinesShown ? ( - - ) : null} + {foldsAgents ? ( + <> + {agentsExpanded ? ( + + ) : ( +
+ line.agent)} + subjectLabel={agentCountLabel} + expanded={false} + onToggle={toggleAgents} + /> +
+ )} + {agentLines} + + ) : ( + agentLines + )} {showLineageChildChip ? ( -
+