diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css
index 05ffc660732..7f555bf724d 100644
--- a/src/renderer/src/assets/main.css
+++ b/src/renderer/src/assets/main.css
@@ -1626,6 +1626,31 @@ html.native-shell .app-layout {
animation: compact-agent-expansion-reveal 180ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
+/* Keep the disclosure in the timestamp slot without moving adjacent content. */
+.agent-child-disclosure-time {
+ visibility: hidden;
+}
+
+@media (hover: hover) {
+ .agent-child-disclosure[data-expanded='false'] .agent-child-disclosure-time {
+ visibility: visible;
+ }
+
+ .agent-child-disclosure[data-expanded='false'] [data-agent-child-disclosure-button] {
+ opacity: 0;
+ }
+
+ .agent-disclosure-row:hover .agent-child-disclosure-time,
+ .agent-disclosure-row:focus-within .agent-child-disclosure-time {
+ visibility: hidden;
+ }
+
+ .agent-disclosure-row:hover [data-agent-child-disclosure-button],
+ .agent-disclosure-row:focus-within [data-agent-child-disclosure-button] {
+ opacity: 1;
+ }
+}
+
/* Why: the collapsed summary still uses pill spacing, while expanded compact
agents read as a quiet tree inside the existing worktree card. */
.compact-agent-summary-panel {
diff --git a/src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.test.tsx b/src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.test.tsx
new file mode 100644
index 00000000000..6d0994eb760
--- /dev/null
+++ b/src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.test.tsx
@@ -0,0 +1,157 @@
+/** @vitest-environment happy-dom */
+import { fireEvent, render, screen, cleanup } from '@testing-library/react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { CompactAgentRow } from '../sidebar/worktree-card-compact-agent-row'
+import { TooltipProvider } from '../ui/tooltip'
+import DashboardAgentRow from './DashboardAgentRow'
+import type { DashboardAgentRow as AgentRow } from './useDashboardData'
+import { DashboardAgentChildDisclosure } from './DashboardAgentChildDisclosure'
+
+vi.mock('./use-agent-row-conversation-name', () => ({ useAgentRowConversationName: () => null }))
+vi.mock('../sidebar/CacheTimer', () => ({
+ default: () => null,
+ usePromptCacheCountdownForPane: () => null
+}))
+
+afterEach(cleanup)
+
+const agent: AgentRow = {
+ paneKey: 'tab:leaf',
+ agentType: 'claude',
+ state: 'working',
+ startedAt: 60000,
+ entry: {
+ paneKey: 'tab:leaf',
+ state: 'working',
+ prompt: 'Review the change',
+ updatedAt: 60000,
+ stateStartedAt: 60000,
+ stateHistory: []
+ },
+ tab: {
+ id: 'tab',
+ ptyId: null,
+ worktreeId: 'workspace',
+ title: 'Agent',
+ customTitle: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: 1
+ }
+}
+
+describe('agent child disclosure', () => {
+ it('toggles children without activating the surrounding agent or workspace', () => {
+ const activate = vi.fn()
+ const toggle = vi.fn()
+ const pointerDown = vi.fn()
+ const keyDown = vi.fn()
+ render(
+
+
+
+ )
+ const button = screen.getByRole('button', { name: 'Show 2 child agents' })
+ fireEvent.pointerDown(button)
+ fireEvent.keyDown(button, { key: 'Enter' })
+ fireEvent.keyDown(button, { key: ' ' })
+ fireEvent.click(button)
+ expect(toggle).toHaveBeenCalledTimes(1)
+ expect(activate).not.toHaveBeenCalled()
+ expect(pointerDown).not.toHaveBeenCalled()
+ expect(keyDown).not.toHaveBeenCalled()
+ })
+
+ it('updates the accessible action and retains the timestamp when expanded', () => {
+ const toggle = vi.fn()
+ const { rerender } = render(
+
+ )
+ expect(
+ screen.getByRole('button', { name: 'Show 1 child agent' }).getAttribute('aria-expanded')
+ ).toBe('false')
+ rerender(
+
+ )
+ expect(
+ screen.getByRole('button', { name: 'Hide 1 child agent' }).getAttribute('aria-expanded')
+ ).toBe('true')
+ expect(screen.getByText('5m')).toBeTruthy()
+ })
+
+ it('does not offer a disclosure without children or a toggle action', () => {
+ const { rerender } = render(
+
+ )
+ expect(screen.queryByRole('button')).toBeNull()
+ rerender()
+ expect(screen.queryByRole('button')).toBeNull()
+ })
+})
+
+describe.each(['compact', 'full'] as const)('%s row disclosure actions', (mode) => {
+ it.each([undefined, 'eligible'] as const)(
+ 'keeps child expansion separate from row actions in %s send mode',
+ (sendTargetStatus) => {
+ const onActivate = vi.fn(),
+ onSendTargetClick = vi.fn(),
+ onDismiss = vi.fn(),
+ onToggleChildAgents = vi.fn()
+ const props = {
+ agent,
+ now: 120000,
+ childAgentCount: 2,
+ childAgentsExpanded: false,
+ onToggleChildAgents,
+ onActivate,
+ sendTargetStatus,
+ onSendTargetClick
+ }
+ render(
+
+ {mode === 'compact' ? (
+
+ ) : (
+
+ )}
+
+ )
+ fireEvent.click(screen.getByRole('button', { name: 'Show 2 child agents' }))
+ expect(onToggleChildAgents).toHaveBeenCalledTimes(1)
+ expect(onActivate).not.toHaveBeenCalled()
+ expect(onSendTargetClick).not.toHaveBeenCalled()
+ expect(onDismiss).not.toHaveBeenCalled()
+ fireEvent.click(screen.getByText('Review the change'))
+ if (sendTargetStatus) {
+ expect(onSendTargetClick).toHaveBeenCalledWith(agent.paneKey)
+ expect(onActivate).not.toHaveBeenCalled()
+ } else {
+ expect(onActivate).toHaveBeenCalledWith(agent.tab.id, agent.paneKey)
+ if (mode === 'full') {
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss agent' }))
+ expect(onDismiss).toHaveBeenCalledWith(agent.paneKey)
+ expect(onActivate).toHaveBeenCalledTimes(1)
+ }
+ }
+ }
+ )
+})
diff --git a/src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.tsx b/src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.tsx
index fb8e45b4216..4e225a287dd 100644
--- a/src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.tsx
+++ b/src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.tsx
@@ -1,18 +1,21 @@
import React, { useCallback } from 'react'
-import { ChevronRight } from 'lucide-react'
+import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
type Props = {
childAgentCount?: number
childAgentsExpanded: boolean
onToggleChildAgents?: () => void
+ timestamp?: React.ReactNode
}
export function DashboardAgentChildDisclosure({
childAgentCount,
childAgentsExpanded,
- onToggleChildAgents
+ onToggleChildAgents,
+ timestamp
}: Props) {
const hasChildDisclosure =
typeof childAgentCount === 'number' &&
@@ -40,29 +43,46 @@ export function DashboardAgentChildDisclosure({
}
return (
-
+
)
}
diff --git a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
index 4b38d8d1b0d..1b64ec6af83 100644
--- a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
+++ b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
@@ -4,7 +4,6 @@ import { AgentStateDot, agentStateLabel, type AgentDotState } from '@/components
import { AgentIcon } from '@/lib/agent-catalog'
import { agentTypeToIconAgent, formatAgentTypeLabel } from '@/lib/agent-status'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
-import { DashboardAgentChildDisclosure } from './DashboardAgentChildDisclosure'
import { DashboardAgentRowMessage } from './DashboardAgentRowMessage'
import { DashboardAgentRowTrailingControls } from './DashboardAgentRowTrailingControls'
import { DashboardAgentRowToolStep } from './DashboardAgentRowToolStep'
@@ -56,12 +55,10 @@ type Props = {
hideExpand?: boolean
/** Reuse the row's hover tint to show the focused terminal pane's agent. */
isFocusedPane?: boolean
- // Why: inline-card orchestration rows fold children under a leading chevron.
+ // Why: inline-card orchestration rows can fold their child agents.
childAgentCount?: number
childAgentsExpanded?: boolean
onToggleChildAgents?: () => void
- // Why: a top-level chevron hangs in the card gutter so the state dot keeps the column of chevron-less rows.
- disclosureInGutter?: boolean
// Why: chevron indentation replaces fixed-offset lineage connector art.
hideLineageConnectors?: boolean
// Why: send-popover target mode makes row clicks send/no-op instead of navigating.
@@ -83,7 +80,6 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
childAgentCount,
childAgentsExpanded = false,
onToggleChildAgents,
- disclosureInGutter = false,
hideLineageConnectors = false,
sendTargetStatus,
sendTargetDisabledReason,
@@ -185,8 +181,7 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
onClick={handleActivate}
className={cn(
// Why: named group scopes the X-reveal to this row, not every row in the card.
- 'group/agent-row relative flex flex-col py-1',
- hasChildDisclosure && disclosureInGutter ? '-ml-7' : '-ml-2',
+ 'agent-disclosure-row group/agent-row relative -ml-2 flex flex-col py-1',
isLineageChild ? 'pl-5 pr-2' : 'px-2',
// Why: hover wash stays softer than the enclosing card's highlight.
'cursor-pointer rounded-sm worktree-agent-row-hover',
@@ -229,11 +224,6 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
) : null}
-
{/* Why: state dot sits in the leading gutter so the eye can scan one column for row state. */}
@@ -288,6 +278,9 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
void
paneKey: string
relativeTimestamp: string | null
expanded: boolean
@@ -18,6 +22,9 @@ type DashboardAgentRowTrailingControlsProps = {
}
export function DashboardAgentRowTrailingControls({
+ childAgentCount,
+ childAgentsExpanded = false,
+ onToggleChildAgents,
paneKey,
relativeTimestamp,
expanded,
@@ -64,8 +71,15 @@ export function DashboardAgentRowTrailingControls({
[onSendTargetClick, paneKey, sendTargetStatus]
)
+ const hasChildDisclosure = Boolean(childAgentCount && onToggleChildAgents)
+
return (
-
+
{(sendTargetStatus === 'eligible' || sendTargetStatus === 'sending') && (
{translate('auto.components.dashboard.DashboardAgentRow.912e136cd9', 'Send')}
)}
- {!sendTargetStatus && hideDismiss && relativeTimestamp !== null && (
+ {!hasChildDisclosure && !sendTargetStatus && hideDismiss && relativeTimestamp !== null && (
)}
- {!sendTargetStatus && !hideDismiss && relativeTimestamp === null && (
+ {!sendTargetStatus && !hideDismiss && (hasChildDisclosure || relativeTimestamp === null) && (
)}
+ {hasChildDisclosure && (
+
+ {relativeTimestamp}
+
+ }
+ />
+ )}
{!hideExpand && (
({
childAgentCount,
childAgentsExpanded,
onToggleChildAgents,
- disclosureInGutter,
onActivate
}: {
agent: { paneKey: string }
@@ -143,7 +142,6 @@ vi.mock('@/components/dashboard/DashboardAgentRow', () => ({
childAgentCount?: number
childAgentsExpanded?: boolean
onToggleChildAgents?: () => void
- disclosureInGutter?: boolean
onActivate: (tabId: string, paneKey: string) => void
}) => {
capturedRowActivations.push({ paneKey: agent.paneKey, onActivate })
@@ -155,7 +153,6 @@ vi.mock('@/components/dashboard/DashboardAgentRow', () => ({
data-disabled-reason={sendTargetDisabledReason}
data-has-send-handler={typeof onSendTargetClick === 'function' ? 'true' : 'false'}
data-pane-key={agent.paneKey}
- data-disclosure-in-gutter={disclosureInGutter ? 'true' : 'false'}
>
{agent.paneKey}
{typeof childAgentCount === 'number' && childAgentCount > 0 ? (
@@ -438,8 +435,6 @@ describe('WorktreeCardAgents', () => {
expect(markup).toContain('role="tree"')
expect(markup).toContain('data-pane-key="tab-parent:1"')
expect(markup).toContain('data-pane-key="tab-child:1"')
- expect(markup).toContain('data-pane-key="tab-parent:1" data-disclosure-in-gutter="true"')
- expect(markup).toContain('data-pane-key="tab-child:1" data-disclosure-in-gutter="false"')
expect(markup).toContain('aria-label="Hide 1 child agent"')
expect(markup).toContain('aria-expanded="true"')
})
diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx
index 42deab4bee3..77918e22b91 100644
--- a/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx
@@ -245,7 +245,6 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
}
const childAgents = childrenByParentPaneKey.get(agent.paneKey) ?? []
const hasChildAgents = childAgents.length > 0
- const isRootAgent = ancestorPaneKeys.size === 0
// Why: spawned child agents are actionable work, so show them as soon as the parent appears (disclosure still folds noise).
const expanded = !collapsedLineageParents.has(agent.paneKey)
const sendTarget = isAgentSendTargetModeActive
@@ -271,14 +270,12 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
stateDotSize="sm"
// Why: clicking the row jumps straight to the agent, so the expand chevron is redundant (keep the identity glyph).
hideExpand
- // Why: fold children under the parent row's leading chevron so a parent reads as a tree node (Variant B in the mockups).
+ // Why: child expansion belongs to the parent agent row.
childAgentCount={hasChildAgents ? childAgents.length : undefined}
childAgentsExpanded={expanded}
onToggleChildAgents={
hasChildAgents ? () => toggleLineageParent(agent.paneKey) : undefined
}
- // Why: nested levels keep the chevron inline; their gutter is the parent's guide line.
- disclosureInGutter={isRootAgent}
isFocusedPane={agent.paneKey === focusedAgentPaneKey}
sendTargetStatus={sendTarget?.status}
sendTargetDisabledReason={sendTarget?.disabledReason}
@@ -307,7 +304,6 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
}
const childAgents = childrenByParentPaneKey.get(agent.paneKey) ?? []
const hasChildAgents = childAgents.length > 0
- const isRootAgent = ancestorPaneKeys.size === 0
const expanded = !collapsedLineageParents.has(agent.paneKey)
const sendTarget = isAgentSendTargetModeActive
? (sendTargetsByPaneKey.get(agent.paneKey) ?? {
@@ -334,7 +330,6 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
onToggleChildAgents={
hasChildAgents ? () => toggleLineageParent(agent.paneKey) : undefined
}
- disclosureInGutter={isRootAgent}
isFocusedPane={agent.paneKey === focusedAgentPaneKey}
cacheTimerActive={cacheTimerActive}
/>
diff --git a/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx b/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx
index ab043fadb44..094cd2e7db9 100644
--- a/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx
+++ b/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx
@@ -1,12 +1,11 @@
import React, { useCallback, useEffect, useRef } from 'react'
-import { ChevronRight } from 'lucide-react'
+import { DashboardAgentChildDisclosure } from '@/components/dashboard/DashboardAgentChildDisclosure'
import { AgentStateDot, agentStateLabel } from '@/components/AgentStateDot'
import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
import { AgentIcon } from '@/lib/agent-catalog'
import { agentTypeToIconAgent, formatAgentTypeLabel } from '@/lib/agent-status'
import { cn } from '@/lib/utils'
import { getAgentDotState } from './worktree-card-agent-summary'
-import { translate } from '@/i18n/i18n'
import { getAgentRowPrimaryText } from '@/lib/agent-row-primary-text'
import { formatAgentToolPreview } from '@/lib/agent-row-tool-preview'
import { agentNoUpdateLabel } from '@/lib/agent-row-decay-state'
@@ -70,14 +69,6 @@ function getCompactAgentTime(agent: DashboardAgentRowData, now: number): string
return startedAt > 0 ? formatShortTimeAgo(startedAt, now) : null
}
-function stopActivationKeyPropagation(e: React.KeyboardEvent): void {
- // Why: the surrounding worktree list handles Enter/Space as row activation.
- // Focused nested buttons need those keys to stay local.
- if (e.key === 'Enter' || e.key === ' ') {
- e.stopPropagation()
- }
-}
-
type CompactAgentRowProps = {
agent: DashboardAgentRowData
now: number
@@ -90,7 +81,6 @@ type CompactAgentRowProps = {
childAgentCount?: number
childAgentsExpanded?: boolean
onToggleChildAgents?: () => void
- disclosureInGutter?: boolean
isFocusedPane?: boolean
hideIdentityIcon?: boolean
cacheTimerActive?: boolean
@@ -107,7 +97,6 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
childAgentCount,
childAgentsExpanded = false,
onToggleChildAgents,
- disclosureInGutter = false,
isFocusedPane = false,
hideIdentityIcon = false,
cacheTimerActive = true,
@@ -183,43 +172,19 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
},
[agent.paneKey, onSendTargetClick, sendTargetStatus]
)
- const handleToggleChildren = useCallback(
- (e: React.MouseEvent) => {
- e.preventDefault()
- e.stopPropagation()
- onToggleChildAgents?.()
- },
- [onToggleChildAgents]
- )
+ const timestamp = shortTime ? (
+
+ {shortTime}
+
+ ) : null
const rowBody = (
<>
- {hasChildDisclosure ? (
-
-
-
- ) : null}
{/* Why: the row's actionable disabled reason must win on every hit area. */}
)}
{cacheTimer && }
- {shortTime && (
-
- {shortTime}
-
+ {hasChildDisclosure ? (
+
+ ) : (
+ timestamp
)}
>
)
@@ -293,11 +257,9 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({