import {
Children,
ComponentPropsWithoutRef,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import { Icon, IconProps } from '../Icon/Icon';
import { ThinkingState } from '../ThinkingState/ThinkingState';
import './chain-of-thought.css';
export type ChainOfThoughtSize = 'm' | 's';
export type ChainOfThoughtVariant = 'coding' | 'reasoning' | 'search' | 'steps';
export interface ChainOfThoughtProps extends ComponentPropsWithoutRef<'div'> {
/**
* The body. For `reasoning`, the text (paragraphs). For the other variants,
* `AgentStep` components, each wrapped in a list item.
*/
children?: ReactNode;
/**
* Additional class names
*/
className?: string;
/**
* Open while `isActive` is true, and close again when it turns false. Once
* the user opens or closes the list themselves, it stays as they left it.
*/
expandWhenActive?: boolean;
/**
* Which icon to render before the summary label.
*/
iconName?: IconProps['iconName'];
/**
* Is the agent still working? Shimmers the summary label and pulses its
* icon. Leave `false` for a finished record.
*/
isActive?: boolean;
/**
* Is the list open?
*/
isOpen?: boolean;
/**
* The always-visible line that toggles the list (for example "Thought for 4
* seconds"). Rendered as a `ThinkingState`.
*/
label: string;
/**
* Size of the whole block. `s` is a 14px summary over 12px rows; `m` is a
* 16px summary over 14px rows.
*/
size?: ChainOfThoughtSize;
/**
* Should the elapsed time be shown? Only renders when `time` is also set.
*/
showTimer?: boolean;
/**
* Elapsed time, formatted by the consumer (for example "17.1s").
*/
time?: string;
/**
* What the list shows. `steps` is plain progress with status icons,
* `reasoning` is the model's text, `search` is a query and its results,
* `coding` is tool calls such as read, edit and run.
*/
variant?: ChainOfThoughtVariant;
}
// 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(' ');
/**
* ChainOfThought UI component. A collapsible record of what an agent did: its
* reasoning, a web search, tool calls or plain progress.
*/
export const ChainOfThought = ({
children,
className,
expandWhenActive = false,
iconName,
isActive = false,
isOpen = false,
label,
showTimer = false,
size = 's',
time,
variant = 'steps',
...props
}: ChainOfThoughtProps) => {
const [isExpanded, setIsExpanded] = useState(
isOpen || (expandWhenActive && isActive),
);
const hasToggled = useRef(false);
useEffect(() => {
if (expandWhenActive && !hasToggled.current) setIsExpanded(isActive);
}, [expandWhenActive, isActive]);
const baseClass = 'pds-chain-of-thought';
return (
<div
className={mergeClasses([
baseClass,
size !== 's' ? `${baseClass}--${size}` : null,
variant !== 'steps' ? `${baseClass}--${variant}` : null,
className,
])}
{...props}
>
<details open={isExpanded}>
<summary
onClick={(e) => {
e.preventDefault();
hasToggled.current = true;
setIsExpanded((prev) => !prev);
}}
>
<ThinkingState
iconName={iconName}
label={label}
showTimer={showTimer}
size={size}
state={isActive ? 'active' : 'completed'}
time={time}
/>
<Icon
className={mergeClasses([
`${baseClass}__icon`,
isExpanded ? `${baseClass}__icon--open` : null,
])}
iconName='angleDown'
size='s'
/>
</summary>
{variant === 'reasoning' ? (
<div className={`${baseClass}__list`}>{children}</div>
) : (
<ol className={`${baseClass}__list`}>
{Children.map(children, (child) => (
<li>{child}</li>
))}
</ol>
)}
</details>
</div>
);
};
Chain of Thought
A collapsible list.
Net-new componentOpen in Storybook