mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-30 08:28:48 +02:00
Deep agent resolution in the sidecar now walks sub-workflow nodes so nested agents carry subworkflowNodeId and subworkflowName on activity events. New subworkflow-started / subworkflow-completed activity types let the frontend track sub-workflow lifecycle. The Activity panel groups nested agents under collapsible sub-workflow cards with status badges, accent-colored left borders, and smooth expand/collapse transitions. Cards auto-expand when a sub-workflow starts running. Workflows without sub-workflow nodes render identically to before. Extracted AgentRow, SubWorkflowGroup, and shared accent constants to a new components/activity/ feature directory. Added resolveWorkflowAgentHierarchy and buildGroupedActivityRows for hierarchical activity grouping with dynamic fallback for unresolved sub-workflow agents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { ChevronDown, GitBranch } from 'lucide-react';
|
|
|
|
import type { AgentNodeConfig } from '@shared/domain/workflow';
|
|
import {
|
|
isAgentActivityActive,
|
|
type AgentUsageAccumulator,
|
|
type SubWorkflowActivityGroup,
|
|
} from '@renderer/lib/sessionActivity';
|
|
import { AgentRow } from './AgentRow';
|
|
import { modeAccent, modeLabels } from './constants';
|
|
|
|
interface SubWorkflowGroupProps {
|
|
group: SubWorkflowActivityGroup;
|
|
agentConfigs: ReadonlyArray<AgentNodeConfig>;
|
|
agentUsage?: Record<string, AgentUsageAccumulator>;
|
|
}
|
|
|
|
const statusPresentation = {
|
|
idle: {
|
|
dot: 'bg-[var(--color-surface-3)]',
|
|
text: 'text-[var(--color-text-muted)]',
|
|
label: 'Idle',
|
|
},
|
|
running: {
|
|
dot: 'animate-pulse',
|
|
text: '',
|
|
label: 'Running',
|
|
},
|
|
completed: {
|
|
dot: 'bg-[var(--color-status-success)]',
|
|
text: 'text-[var(--color-status-success)]',
|
|
label: 'Done',
|
|
},
|
|
} as const;
|
|
|
|
export function SubWorkflowGroup({ group, agentConfigs, agentUsage }: SubWorkflowGroupProps) {
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const prevStatusRef = useRef(group.status);
|
|
|
|
useEffect(() => {
|
|
if (prevStatusRef.current !== 'running' && group.status === 'running') {
|
|
setIsExpanded(true);
|
|
}
|
|
prevStatusRef.current = group.status;
|
|
}, [group.status]);
|
|
|
|
const toggle = useCallback(() => setIsExpanded((prev) => !prev), []);
|
|
|
|
const accent = modeAccent[group.orchestrationMode] ?? modeAccent.single;
|
|
const status = statusPresentation[group.status];
|
|
const hasActiveAgent = group.agents.some((a) => isAgentActivityActive(a.activity));
|
|
|
|
return (
|
|
<div
|
|
className="overflow-hidden rounded-lg border border-[var(--color-border-subtle)] border-l-[3px] bg-[var(--color-surface-1)]"
|
|
style={{ borderLeftColor: accent.color }}
|
|
role="group"
|
|
aria-label={`Sub-workflow: ${group.name}`}
|
|
>
|
|
{/* Collapsible header */}
|
|
<button
|
|
className="flex w-full items-center gap-2 px-3 py-2.5 text-left transition-colors hover:bg-[var(--color-surface-2)]"
|
|
onClick={toggle}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
toggle();
|
|
}
|
|
}}
|
|
aria-expanded={isExpanded}
|
|
type="button"
|
|
>
|
|
<GitBranch className="size-3.5 shrink-0 text-[var(--color-text-muted)]" />
|
|
|
|
<span className="min-w-0 flex-1 truncate text-[12px] font-medium text-[var(--color-text-primary)]">
|
|
{group.name}
|
|
</span>
|
|
|
|
{/* Status badge */}
|
|
<span className="flex items-center gap-1" aria-live="polite">
|
|
<span
|
|
className={`size-1.5 rounded-full ${
|
|
group.status === 'running'
|
|
? `${accent.dot} ${status.dot} ring-1 ring-[var(--color-border-glow)]`
|
|
: status.dot
|
|
}`}
|
|
/>
|
|
<span className={`text-[9px] font-medium ${group.status === 'running' ? accent.label : status.text}`}>
|
|
{status.label}
|
|
</span>
|
|
</span>
|
|
|
|
{/* Agent count pill */}
|
|
<span className="font-mono rounded-full bg-[var(--color-surface-2)] px-1.5 py-0.5 text-[9px] tabular-nums text-[var(--color-text-muted)]">
|
|
{group.agents.length}
|
|
</span>
|
|
|
|
<ChevronDown
|
|
className={`size-3 shrink-0 text-[var(--color-text-muted)] transition-transform duration-150 ${
|
|
isExpanded ? 'rotate-180' : ''
|
|
}`}
|
|
/>
|
|
</button>
|
|
|
|
{/* Expandable agent list */}
|
|
<div
|
|
className="grid transition-[grid-template-rows] duration-150 ease-out"
|
|
style={{ gridTemplateRows: isExpanded ? '1fr' : '0fr' }}
|
|
>
|
|
<div className="overflow-hidden">
|
|
<div className="relative border-t border-[var(--color-border-subtle)] py-1 pl-5 pr-3">
|
|
{/* Connecting vertical accent line */}
|
|
{hasActiveAgent && (
|
|
<div className={`absolute bottom-3 left-[11px] top-3 w-px ${accent.bar}`} />
|
|
)}
|
|
|
|
{group.agents.map((row, index) => {
|
|
const agent = agentConfigs.find((c) => c.id === row.key || c.name === row.agentName);
|
|
const usage = agentUsage?.[row.activity?.agentId ?? row.key] ?? agentUsage?.[row.agentName];
|
|
|
|
return (
|
|
<AgentRow
|
|
key={row.key}
|
|
row={row}
|
|
agent={agent}
|
|
accent={accent}
|
|
isLast={index === group.agents.length - 1}
|
|
agentUsage={usage}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{/* Mode label */}
|
|
<div className="flex items-center gap-1 pb-1 pt-0.5">
|
|
<span className={`text-[9px] font-medium ${accent.label}`}>
|
|
{modeLabels[group.orchestrationMode]}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|