import { ComponentPropsWithoutRef, ReactNode } from 'react';
import { Icon, IconProps } from '../Icon/Icon';
import { Spinner } from '../Spinner/Spinner';
import './agent-step.css';
export type AgentStepStatus = 'active' | 'done' | 'error' | 'pending';
export interface AgentStepProps extends Omit<
ComponentPropsWithoutRef<'div'>,
'children'
> {
/**
* Additional class names
*/
className?: string;
/**
* Optional expandable content, such as a tool call's input and output. A
* step with `detail` becomes a toggle.
*/
detail?: ReactNode;
/**
* Quiet text for secondary rows such as a search query or "+7 more".
*/
isQuiet?: boolean;
/**
* Replaces the status icon, for example a wrench for a tool call. While the
* step is `active` the spinner still shows, and the status color and
* screen reader text still follow `status`.
*/
iconName?: IconProps['iconName'];
/**
* What the agent did or is doing.
*/
label: ReactNode;
/**
* Object with translatable strings, read by screen readers with the status.
*/
labelStrings?: Record<AgentStepStatus, string>;
/**
* Quiet supporting text after the label. Wrap file names, commands and
* other code in `<code>`.
*/
secondaryText?: ReactNode;
/**
* Where the step is at. Without it (and without `iconName`) the step has no
* icon.
*/
status?: AgentStepStatus;
}
// Stand-in for the v2 `mergeClasses` util, which the published package does
// not export. The port uses the real one.
const mergeClasses = (classes: Array<string | null | undefined>) =>
classes.filter(Boolean).join(' ');
const defaultLabelStrings: Record<AgentStepStatus, string> = {
active: 'In progress',
done: 'Done',
error: 'Failed',
pending: 'Pending',
};
const defaultIcons: Record<AgentStepStatus, IconProps['iconName']> = {
active: 'circle',
done: 'check',
error: 'diamondExclamation',
pending: 'circle',
};
/**
* AgentStep UI component. One thing an agent did or is doing: a status icon, a
* label, quiet secondary text and optional expandable detail. Use it alone inline or
* inside `ChainOfThought`.
*/
export const AgentStep = ({
className,
detail,
iconName,
isQuiet = false,
label,
labelStrings = defaultLabelStrings,
secondaryText,
status,
...props
}: AgentStepProps) => {
const baseClass = 'pds-agent-step';
const hasIcon = status !== undefined || iconName !== undefined;
const statusIcon = () => {
if (status === 'active') {
return (
<Spinner
className={`${baseClass}__status`}
color='var(--agent-step-status-color)'
label={labelStrings.active}
size='s'
/>
);
}
return (
<>
<Icon
className={`${baseClass}__status`}
iconName={iconName ?? defaultIcons[status ?? 'pending']}
size='xs'
/>
{status && (
<span className='visually-hidden'>{labelStrings[status]}</span>
)}
</>
);
};
const row = (
<>
{hasIcon && statusIcon()}
<span className={`${baseClass}__label`}>{label}</span>
{secondaryText && (
<span className={`${baseClass}__secondary-text`}>{secondaryText}</span>
)}
</>
);
return (
<div
className={mergeClasses([
baseClass,
status ? `${baseClass}--${status}` : null,
isQuiet ? `${baseClass}--quiet` : null,
className,
])}
{...props}
>
{detail ? (
<details>
<summary className={`${baseClass}__row`}>
{row}
<Icon
className={`${baseClass}__toggle`}
iconName='angleRight'
size='xs'
/>
</summary>
<div className={`${baseClass}__detail`}>{detail}</div>
</details>
) : (
<div className={`${baseClass}__row`}>{row}</div>
)}
</div>
);
};
Agent Step
One row: an optional icon (status or custom), a label, quiet secondary text and optional expandable detail.
Net-new componentOpen in Storybook