mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
fix(sidebar): move agent child disclosure to the right (#23577)
* fix(sidebar): move agent child disclosure to the timestamp slot * fix(sidebar): align agent disclosure with summary caret
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
<div onClick={activate} onPointerDown={pointerDown} onKeyDown={keyDown}>
|
||||
<DashboardAgentChildDisclosure
|
||||
childAgentCount={2}
|
||||
childAgentsExpanded={false}
|
||||
onToggleChildAgents={toggle}
|
||||
timestamp="5m"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
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(
|
||||
<DashboardAgentChildDisclosure
|
||||
childAgentCount={1}
|
||||
childAgentsExpanded={false}
|
||||
onToggleChildAgents={toggle}
|
||||
timestamp="5m"
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Show 1 child agent' }).getAttribute('aria-expanded')
|
||||
).toBe('false')
|
||||
rerender(
|
||||
<DashboardAgentChildDisclosure
|
||||
childAgentCount={1}
|
||||
childAgentsExpanded
|
||||
onToggleChildAgents={toggle}
|
||||
timestamp="5m"
|
||||
/>
|
||||
)
|
||||
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(
|
||||
<DashboardAgentChildDisclosure
|
||||
childAgentCount={0}
|
||||
childAgentsExpanded={false}
|
||||
onToggleChildAgents={vi.fn()}
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByRole('button')).toBeNull()
|
||||
rerender(<DashboardAgentChildDisclosure childAgentCount={2} childAgentsExpanded={false} />)
|
||||
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(
|
||||
<TooltipProvider>
|
||||
{mode === 'compact' ? (
|
||||
<CompactAgentRow {...props} />
|
||||
) : (
|
||||
<DashboardAgentRow {...props} onDismiss={onDismiss} hideExpand />
|
||||
)}
|
||||
</TooltipProvider>
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleChildren}
|
||||
onMouseDown={stopMouseDown}
|
||||
onKeyDown={stopKeyDown}
|
||||
className="-ml-0.5 inline-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"
|
||||
aria-label={translate(
|
||||
'auto.components.dashboard.DashboardAgentChildDisclosure.1b57ce9fa4',
|
||||
'{{value0}} {{value1}} child {{value2}}',
|
||||
{
|
||||
value0: childAgentsExpanded ? 'Hide' : 'Show',
|
||||
value1: childAgentCount,
|
||||
value2: childAgentCount === 1 ? 'agent' : 'agents'
|
||||
}
|
||||
)}
|
||||
aria-expanded={childAgentsExpanded}
|
||||
<span
|
||||
className="agent-child-disclosure relative inline-grid h-5 min-w-5 shrink-0 items-center justify-items-end"
|
||||
data-expanded={childAgentsExpanded}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'size-3 transition-transform duration-150',
|
||||
childAgentsExpanded && 'rotate-90'
|
||||
<span
|
||||
className="agent-child-disclosure-time col-start-1 row-start-1 pointer-events-none"
|
||||
aria-hidden
|
||||
>
|
||||
{timestamp}
|
||||
</span>
|
||||
{/* Why: align the 12px icon with adjacent row icons while keeping its 24px hit target. */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
type="button"
|
||||
onClick={handleToggleChildren}
|
||||
onMouseDown={stopMouseDown}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onKeyDown={stopKeyDown}
|
||||
data-agent-child-disclosure-button=""
|
||||
className="col-start-1 row-start-1 -mr-1.5"
|
||||
aria-label={translate(
|
||||
'auto.components.dashboard.DashboardAgentChildDisclosure.1b57ce9fa4',
|
||||
'{{value0}} {{value1}} child {{value2}}',
|
||||
{
|
||||
value0: childAgentsExpanded ? 'Hide' : 'Show',
|
||||
value1: childAgentCount,
|
||||
value2: childAgentCount === 1 ? 'agent' : 'agents'
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
aria-expanded={childAgentsExpanded}
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'size-3 transition-transform duration-150',
|
||||
childAgentsExpanded && 'rotate-180'
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
</span>
|
||||
) : null}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DashboardAgentChildDisclosure
|
||||
childAgentCount={childAgentCount}
|
||||
childAgentsExpanded={childAgentsExpanded}
|
||||
onToggleChildAgents={onToggleChildAgents}
|
||||
/>
|
||||
{/* Why: state dot sits in the leading gutter so the eye can scan one column for row state. */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -288,6 +278,9 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
|
||||
<DashboardAgentRowTrailingControls
|
||||
paneKey={agent.paneKey}
|
||||
relativeTimestamp={relativeTimestamp}
|
||||
childAgentCount={childAgentCount}
|
||||
childAgentsExpanded={childAgentsExpanded}
|
||||
onToggleChildAgents={onToggleChildAgents}
|
||||
expanded={expanded}
|
||||
hideExpand={hideExpand}
|
||||
hideDismiss={agent.rowSource === 'subagent'}
|
||||
|
||||
@@ -2,8 +2,12 @@ import React, { useCallback } from 'react'
|
||||
import { ChevronDown, Send, X } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { DashboardAgentChildDisclosure } from './DashboardAgentChildDisclosure'
|
||||
|
||||
type DashboardAgentRowTrailingControlsProps = {
|
||||
childAgentCount?: number
|
||||
childAgentsExpanded?: boolean
|
||||
onToggleChildAgents?: () => 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 (
|
||||
<span className="relative ml-auto flex h-3.5 w-12 shrink-0 items-center justify-end">
|
||||
<span
|
||||
className={cn(
|
||||
'relative ml-auto flex shrink-0 items-center justify-end',
|
||||
hasChildDisclosure ? 'h-5 gap-1' : 'h-3.5 w-12'
|
||||
)}
|
||||
>
|
||||
{(sendTargetStatus === 'eligible' || sendTargetStatus === 'sending') && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -75,6 +89,7 @@ export function DashboardAgentRowTrailingControls({
|
||||
disabled={sendTargetStatus === 'sending'}
|
||||
className={cn(
|
||||
'worktree-agent-send-target-button absolute right-0 top-1/2 z-10 inline-flex h-5 -translate-y-1/2 items-center gap-1 rounded-md border px-1.5 text-[10px] font-medium leading-none transition-[background-color,border-color,color,opacity]',
|
||||
hasChildDisclosure && 'static translate-y-0',
|
||||
sendTargetStatus === 'sending' && 'cursor-progress opacity-75'
|
||||
)}
|
||||
aria-label={translate(
|
||||
@@ -90,7 +105,7 @@ export function DashboardAgentRowTrailingControls({
|
||||
<span>{translate('auto.components.dashboard.DashboardAgentRow.912e136cd9', 'Send')}</span>
|
||||
</button>
|
||||
)}
|
||||
{!sendTargetStatus && hideDismiss && relativeTimestamp !== null && (
|
||||
{!hasChildDisclosure && !sendTargetStatus && hideDismiss && relativeTimestamp !== null && (
|
||||
<span
|
||||
className="pointer-events-none shrink-0 text-[10px] leading-none text-muted-foreground/60"
|
||||
aria-hidden
|
||||
@@ -100,7 +115,7 @@ export function DashboardAgentRowTrailingControls({
|
||||
)}
|
||||
{/* Why: timestamp and dismiss-X share one slot. On no-hover devices the X
|
||||
is visible by default, so the timestamp must yield there too. */}
|
||||
{!sendTargetStatus && !hideDismiss && relativeTimestamp !== null && (
|
||||
{!hasChildDisclosure && !sendTargetStatus && !hideDismiss && relativeTimestamp !== null && (
|
||||
<span className="relative grid grid-cols-1 grid-rows-1 shrink-0 items-center justify-items-end">
|
||||
<span
|
||||
className={cn(
|
||||
@@ -132,7 +147,7 @@ export function DashboardAgentRowTrailingControls({
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{!sendTargetStatus && !hideDismiss && relativeTimestamp === null && (
|
||||
{!sendTargetStatus && !hideDismiss && (hasChildDisclosure || relativeTimestamp === null) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
@@ -152,6 +167,18 @@ export function DashboardAgentRowTrailingControls({
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{hasChildDisclosure && (
|
||||
<DashboardAgentChildDisclosure
|
||||
childAgentCount={childAgentCount}
|
||||
childAgentsExpanded={childAgentsExpanded}
|
||||
onToggleChildAgents={onToggleChildAgents}
|
||||
timestamp={
|
||||
<span className="text-[10px] leading-none text-muted-foreground/60">
|
||||
{relativeTimestamp}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!hideExpand && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -132,7 +132,6 @@ vi.mock('@/components/dashboard/DashboardAgentRow', () => ({
|
||||
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"')
|
||||
})
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onToggleChildAgents?.()
|
||||
},
|
||||
[onToggleChildAgents]
|
||||
)
|
||||
const timestamp = shortTime ? (
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 text-[10px] tabular-nums',
|
||||
isFocusedPane ? 'text-foreground/70' : 'text-muted-foreground/60'
|
||||
)}
|
||||
>
|
||||
{shortTime}
|
||||
</span>
|
||||
) : null
|
||||
|
||||
const rowBody = (
|
||||
<>
|
||||
{hasChildDisclosure ? (
|
||||
<button
|
||||
type="button"
|
||||
className="compact-agent-child-disclosure-button flex size-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-worktree-sidebar-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-worktree-sidebar-ring"
|
||||
aria-label={translate(
|
||||
'auto.components.sidebar.worktree.card.compact.agents.a128d7006b',
|
||||
'{{value0}} {{value1}} child {{value2}}',
|
||||
{
|
||||
value0: childAgentsExpanded ? 'Hide' : 'Show',
|
||||
value1: childAgentCount,
|
||||
value2: childAgentCount === 1 ? 'agent' : 'agents'
|
||||
}
|
||||
)}
|
||||
aria-expanded={childAgentsExpanded}
|
||||
onClick={handleToggleChildren}
|
||||
onKeyDown={stopActivationKeyPropagation}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'size-3 transition-transform duration-150',
|
||||
childAgentsExpanded && 'rotate-90'
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
{/* Why: the row's actionable disabled reason must win on every hit area. */}
|
||||
<AgentStateDot
|
||||
state={dotState}
|
||||
@@ -275,16 +240,15 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
|
||||
</span>
|
||||
)}
|
||||
{cacheTimer && <CacheTimer startedAt={cacheTimer.startedAt} ttlMs={cacheTimer.ttlMs} />}
|
||||
{shortTime && (
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 text-[10px] tabular-nums',
|
||||
// Why: the muted timestamp drops out against the selected-row fill.
|
||||
isFocusedPane ? 'text-foreground/70' : 'text-muted-foreground/60'
|
||||
)}
|
||||
>
|
||||
{shortTime}
|
||||
</span>
|
||||
{hasChildDisclosure ? (
|
||||
<DashboardAgentChildDisclosure
|
||||
childAgentCount={childAgentCount}
|
||||
childAgentsExpanded={childAgentsExpanded}
|
||||
onToggleChildAgents={onToggleChildAgents}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
) : (
|
||||
timestamp
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -293,11 +257,9 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
|
||||
<div
|
||||
draggable={false}
|
||||
className={cn(
|
||||
'compact-agent-row group/compact-agent-row min-w-0 overflow-hidden cursor-pointer rounded-sm px-1 text-[11px] leading-none',
|
||||
'compact-agent-row agent-disclosure-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',
|
||||
// Why: hang the chevron in the card gutter so the state dot keeps the column of chevron-less rows.
|
||||
hasChildDisclosure && disclosureInGutter && '-ml-5',
|
||||
isLineageChild && 'worktree-agent-lineage-child-row',
|
||||
'flex h-6 items-center gap-1',
|
||||
isFocusedPane && 'bg-worktree-sidebar-accent',
|
||||
|
||||
Reference in New Issue
Block a user