Fix compact agent status nesting (#4649)

* fix: address review findings

* Fix compact agent row overflow in worktree cards

- Allow inline agent rows to outdent into the card gutter without clipping
- Scope compact agent list sizing to the list container instead of the button
- Keep expanded agent headers quiet while preserving collapsed pill styling
This commit is contained in:
Jinjing
2026-06-04 13:50:51 -07:00
committed by GitHub
parent f78716ca46
commit 68b2e41e61
5 changed files with 94 additions and 30 deletions
+57 -6
View File
@@ -1013,10 +1013,8 @@
animation: compact-agent-expansion-reveal 180ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* Why: the expanded summary becomes one enclosed panel. Keep the box model
stable across states (padding always reserved, border drawn as a non-layout
box-shadow) and fade only the chrome, so toggling doesn't snap the header and
rows while the content height animates. */
/* 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 {
padding: 2px;
border-radius: var(--radius-sm, 0.25rem);
@@ -1028,8 +1026,61 @@
}
.compact-agent-summary-panel-expanded {
background: color-mix(in srgb, var(--sidebar-accent) 20%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sidebar-border) 70%, transparent);
background: transparent;
box-shadow: inset 0 0 0 1px transparent;
}
[data-compact-agent-list='true'] {
margin-inline-start: -0.5rem;
width: calc(100% + 0.5rem);
}
/* Why: compact expanded agents live inside an already-framed worktree card.
Keep the tree legible with indentation and interaction states instead of
stacking row cards and shaded panels. */
[data-compact-agent-list='true'] .compact-agent-row.worktree-agent-lineage-parent-row,
[data-compact-agent-list='true'] .compact-agent-row.worktree-agent-lineage-child-row {
background: transparent;
box-shadow: none;
}
[data-compact-agent-list='true'] .compact-agent-row.worktree-agent-lineage-child-row::before {
content: none;
}
[data-compact-agent-list='true']
.compact-agent-row.worktree-agent-lineage-parent-row.worktree-agent-row-hover:hover,
[data-compact-agent-list='true']
.compact-agent-row.worktree-agent-lineage-child-row.worktree-agent-row-hover:hover {
background: color-mix(in srgb, var(--sidebar-foreground) 1.25%, transparent);
}
.dark
[data-compact-agent-list='true']
.compact-agent-row.worktree-agent-lineage-parent-row.worktree-agent-row-hover:hover,
.dark
[data-compact-agent-list='true']
.compact-agent-row.worktree-agent-lineage-child-row.worktree-agent-row-hover:hover {
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
[data-compact-agent-list='true']
.compact-agent-row.worktree-agent-row-hover[data-focused-agent-pane='true'] {
background: color-mix(in srgb, var(--sidebar-foreground) 12%, var(--sidebar-accent));
}
.dark
[data-compact-agent-list='true']
.compact-agent-row.worktree-agent-row-hover[data-focused-agent-pane='true'] {
background: color-mix(in srgb, var(--accent) 70%, transparent);
}
[data-compact-agent-list='true'] .worktree-agent-lineage-children {
border-left-color: color-mix(in srgb, var(--sidebar-foreground) 16%, transparent);
}
.dark [data-compact-agent-list='true'] .worktree-agent-lineage-children {
border-left-color: color-mix(in srgb, var(--accent) 20%, transparent);
}
@media (prefers-reduced-motion: reduce) {
@@ -662,6 +662,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
: hasDetailedMetaRowContent
const showHeaderActions = showTitleRowUnread || showTitleRowPrimary || showDeleteQuickAction
const showBranchIdentityHover = compactCards && showBranch
const showInlineAgentList = showDetailedCardProperties && cardProps.includes('inline-agents')
// Why: sidebar rows need a small surface inset, while their content remains
// aligned with the pre-inset layout and the repo header hierarchy.
const cardStyle = flushSurface
@@ -808,7 +809,9 @@ const WorktreeCard = React.memo(function WorktreeCard({
<div
className={cn(
'flex min-w-0 flex-1 flex-col gap-1.5',
lineageChildren ? 'overflow-visible' : 'overflow-hidden'
// Why: inline agent rows intentionally outdent into the card gutter;
// title/meta truncation is handled by their own inner elements.
lineageChildren || showInlineAgentList ? 'overflow-visible' : 'overflow-hidden'
)}
>
{/* Header row: Title */}
@@ -1054,7 +1057,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
naturally when agents appear/disappear. When agents directly
follow the title, counterbalance the card stack gap so both rows
read as one compact header group. */}
{showDetailedCardProperties && cardProps.includes('inline-agents') && (
{showInlineAgentList && (
<WorktreeCardAgents
worktreeId={worktree.id}
className={hasMetaRow || remoteBranchConflict ? 'mt-0' : '-mt-1'}
@@ -97,7 +97,8 @@ vi.mock('@/components/dashboard/DashboardAgentRow', () => ({
onSendTargetClick,
childAgentCount,
childAgentsExpanded,
onToggleChildAgents
onToggleChildAgents,
reserveDisclosureGutter
}: {
agent: { paneKey: string }
isFocusedPane?: boolean
@@ -107,6 +108,7 @@ vi.mock('@/components/dashboard/DashboardAgentRow', () => ({
childAgentCount?: number
childAgentsExpanded?: boolean
onToggleChildAgents?: () => void
reserveDisclosureGutter?: boolean
}) => (
<div
data-testid="agent-row"
@@ -115,6 +117,7 @@ vi.mock('@/components/dashboard/DashboardAgentRow', () => ({
data-disabled-reason={sendTargetDisabledReason}
data-has-send-handler={typeof onSendTargetClick === 'function' ? 'true' : 'false'}
data-pane-key={agent.paneKey}
data-reserve-disclosure-gutter={reserveDisclosureGutter ? 'true' : 'false'}
>
{agent.paneKey}
{typeof childAgentCount === 'number' && childAgentCount > 0 ? (
@@ -222,6 +225,7 @@ 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-child:1" data-reserve-disclosure-gutter="false"')
expect(markup).toContain('aria-label="Hide 1 child agent"')
expect(markup).toContain('aria-expanded="true"')
})
@@ -362,6 +366,7 @@ describe('WorktreeCardAgents', () => {
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
expect(markup).toContain('compact-agent-row')
expect(markup).toContain('group/compact-agent-row')
expect(markup).toContain('<img')
expect(markup).toContain('alt="Image #1"')
@@ -504,6 +509,7 @@ describe('WorktreeCardAgents', () => {
expect(markup).toContain('aria-expanded="true"')
expect(markup).toContain('Collapse 5 agents')
expect(markup).toContain('compact-agent-summary-button-expanded')
expect(markup).toContain('>5 agents<')
expect(markup).not.toContain('>+2<')
expect(markup).not.toContain('Expand All 5 agents working')
@@ -568,12 +574,14 @@ describe('WorktreeCardAgents', () => {
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
expect(markup).toContain('data-compact-agent-list="true"')
expect(markup).toContain('role="tree"')
expect(markup).toContain('3 parents: 1 waiting, 1 working, 1 done')
expect(markup).toContain('3 agents: 1 waiting, 1 working, 1 done')
expect(markup).not.toContain('title="Gemini waiting"')
expect(markup).not.toContain('title="Codex working"')
expect(markup).not.toContain('title="Codex done"')
expect(markup).not.toContain('Parent A')
expect(markup).not.toContain('Child A')
expect(markup).not.toContain('compact-agent-row')
})
})
@@ -253,11 +253,10 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
e.stopPropagation()
}, [])
// Why: when any root row has a disclosure chevron, leaf siblings reserve a
// matching leading spacer so the state-dot column stays aligned across the
// card. Without this, parent rows shift right by the chevron's width while
// leaf rows hug the gutter — visible misalignment when the user sweeps the
// leading column.
// Why: when any root row has a disclosure chevron, root leaf siblings reserve
// a matching leading spacer so the state-dot column stays aligned across the
// card. Descendants already have the child rail indent, so adding this spacer
// there double-indents child agents.
const anyRootHasChildren = rootAgents.some(
(agent) => (childrenByParentPaneKey.get(agent.paneKey) ?? []).length > 0
)
@@ -273,6 +272,7 @@ 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 they should be visible
// as soon as the parent appears; the disclosure remains available to fold noise.
const expanded = !collapsedLineageParents.has(agent.paneKey)
@@ -315,7 +315,7 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
}
// Why: keep leaf rows aligned with parent rows in the same card —
// see anyRootHasChildren above.
reserveDisclosureGutter={anyRootHasChildren && !hasChildAgents}
reserveDisclosureGutter={isRootAgent && anyRootHasChildren && !hasChildAgents}
isFocusedPane={agent.paneKey === focusedAgentPaneKey}
sendTargetStatus={sendTarget?.status}
sendTargetDisabledReason={sendTarget?.disabledReason}
@@ -346,6 +346,7 @@ 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 descendantAncestorPaneKeys = new Set(ancestorPaneKeys)
descendantAncestorPaneKeys.add(agent.paneKey)
@@ -360,7 +361,7 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
onToggleChildAgents={
hasChildAgents ? () => toggleLineageParent(agent.paneKey) : undefined
}
reserveDisclosureGutter={anyRootHasChildren && !hasChildAgents}
reserveDisclosureGutter={isRootAgent && anyRootHasChildren && !hasChildAgents}
isFocusedPane={agent.paneKey === focusedAgentPaneKey}
/>
{hasChildAgents ? (
@@ -381,9 +382,7 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
// Why: compact worktree cards keep multiple active agents to a single
// predictable status line, even when there are only two agents.
const shouldUseSummaryRow = summaryAgents.length > 1
const subjectLabel = hasLineage
? `${rootAgents.length} ${rootAgents.length === 1 ? 'parent' : 'parents'}`
: `${agents.length} agents`
const subjectLabel = `${hasLineage ? rootAgents.length : agents.length} agents`
return (
<div
@@ -395,11 +394,11 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
onPointerDown={stopBubble}
role={hasLineage ? 'tree' : 'group'}
aria-label="Agents"
data-compact-agent-list="true"
>
{shouldUseSummaryRow ? (
// Why: when expanded, the header and its agent rows read as a single
// enclosed panel rather than a standalone pill with rows floating
// below it — the border/fill wraps the whole group.
// Why: the worktree card is already the surface. Expanded compact
// agents stay a quiet tree; only the collapsed summary reads as a pill.
<div
className={cn(
'compact-agent-summary-panel',
@@ -164,14 +164,17 @@ export function CompactAgentSummaryButton({
type="button"
draggable={false}
className={cn(
'group/agent-summary flex h-6 w-full min-w-0 items-center gap-1 rounded-sm',
'compact-agent-summary-button group/agent-summary flex h-6 w-full min-w-0 items-center gap-1 rounded-sm',
'px-1 text-left text-[11px] leading-none text-muted-foreground',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-sidebar-ring',
// Why: collapsed it's a standalone pill; expanded it's the header of the
// enclosing panel, so it drops its own border/fill to avoid double chrome.
// Why: sidebar-accent is near-white in light mode and dark in dark
// mode, so hover lightening needs a theme-specific token mix.
'hover:bg-sidebar-accent/55 dark:hover:bg-sidebar-foreground/[0.035]',
// Why: expanded is a tree header inside the card, so only the
// standalone collapsed pill gets a resting surface and border.
expanded
? 'hover:bg-sidebar-accent/60'
: 'border border-sidebar-border/70 bg-sidebar-accent/35 hover:bg-sidebar-accent'
? 'compact-agent-summary-button-expanded'
: 'border border-sidebar-border/70 bg-sidebar-accent/35'
)}
aria-label={
expanded ? `Collapse ${subjectLabel}` : `Expand ${summary}. ${agentIdentitySummary}`
@@ -293,7 +296,7 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
{hasChildDisclosure ? (
<button
type="button"
className="flex size-4 shrink-0 items-center justify-center rounded-sm border border-sidebar-border/80 bg-sidebar text-foreground/80 shadow-xs hover:bg-sidebar-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-sidebar-ring"
className="compact-agent-child-disclosure-button flex size-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-sidebar-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-sidebar-ring"
aria-label={`${childAgentsExpanded ? 'Hide' : 'Show'} ${childAgentCount} child ${
childAgentCount === 1 ? 'agent' : 'agents'
}`}
@@ -357,7 +360,7 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
<div
draggable={false}
className={cn(
'group/compact-agent-row min-w-0 cursor-pointer rounded-sm px-1 text-[11px] leading-none',
'compact-agent-row group/compact-agent-row min-w-0 cursor-pointer rounded-sm px-1 text-[11px] leading-none',
'text-muted-foreground worktree-agent-row-hover',
hasChildDisclosure && 'worktree-agent-lineage-parent-row',
isLineageChild && 'worktree-agent-lineage-child-row',