mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 08:02:33 +00:00
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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user