SakaniUI

Chain of Thought

A visible record of an AI agent's work: steps joined by a rail, each with an icon, a label, an optional query, a result count and its sources, plus a live Working line.

Press Replay to watch a run. A step you append fades in and its rail draws itself; its results land (the count ticks up and the sources fan out); the next step starts; and finishing sweeps the rail down to a check. It only displays: you stream the steps in as your agent works.

    Working

    Basic

    A ChainOfThought holds ChainOfThoughtSteps. With status="done" and the final elapsed it ends with "Worked for 11.2s".

    1. Done: Planning the search3 angles
    2. Done: Searched X fordesign system pricing7 posts
    3. Done: Searched Reddit forr/reactjs worth paying for12 threads
    4. Done: Reading the strongest three threads
    Worked for11.2s

    Step states

    Done steps read muted and the active one reads in full, so the eye lands on what is happening now. A pending step's icon is grey until it starts. Screen readers get each step's status.

    1. Done: Searched X fordesign system pricing7 posts
    2. In progress: Searched Reddit forr/reactjs worth paying for12 threads
    3. Pending: Searching Hacker News fordesign system

    Logos, emoji and icons

    Make each step recognisable. Use the real logo of a site the agent searched, in its brand colour (sourceLogo('reddit')); an emoji for steps with no site (💡 planning, 📄 reading); or any icon. A logo whose brand colour is black (X, GitHub, Notion, Medium, Wikipedia) is drawn in the text colour, so it stays visible in dark mode.

    1. Done: Searched Google fordesign tokens
    2. Done: Planning the search
    3. Done: Reading the docs
    on their own: <SourceLogo name="reddit" />

    Sources

    A SourceStack of overlapping site chips shows where a step's results came from. Add onSourcesClick to make the row a button with a chevron.

    1. Done: Searched Google fordesign system pricing 202610 results

    Working indicator

    The line under the steps: a sparkle and a shimmering "Working" with the elapsed time, or a check and "Worked for". It is also exported on its own. Pass startedAt and the time counts up by itself.

    Working
    Worked for11.2s

    Controlled

    Closed by default here. Pass open and onOpenChange to own the state.

    Open: false

    Streaming from an agent

    Append a step when a tool call starts, give it its count when the results land, mark only the last step active, and give the container status="working" with startedAt. When the run ends, switch to status="done" and the final elapsed.

    // the agent's own tool calls map straight onto steps
    for await (const event of agent.stream(prompt)) {
      if (event.type === 'tool_start')
        setSteps((s) => [...s, { id: event.id, icon: iconFor(event.tool), label: labelFor(event), query: event.input }]);
      if (event.type === 'tool_result')
        setSteps((s) => s.map((x) => (x.id === event.id ? { ...x, count: `${event.results.length} results` } : x)));
    }
    setDone(true);

    Source logos

    17 sites. Logos are trademarks of their owners and appear only to name where a result came from.

    google
    x
    reddit
    github
    linkedin
    youtube
    hackernews
    wikipedia
    stackoverflow
    producthunt
    medium
    notion
    substack
    arxiv
    bluesky
    perplexity
    duckduckgo

    Motion

    MomentWhat happens
    A step arrivesIts row opens while the previous rail flows down into it and turns the corner; the icon lands, the label is written in, the query types itself, the count ticks up and the source chips fan out.
    The active stepA spark runs down the rail into it (in the logo's brand colour) and its icon beats as it lands.
    Pending to activeA pending step's icon is grey; it fades into colour when the step starts.
    WorkingThe label shimmers while the sparkle beside it, in the brand colour, turns and twinkles on the same beat.
    FinishedOne sweep runs down the rail lighting each icon, then the check is drawn.
    Open and closeThe chevron turns with a spring and the steps rise in turn.
    Reduced motionEverything is instant: no spark, shimmer, sweep or tick-up.

    Only what arrives after the first render animates, so a finished run you reload does not replay.

    Accessibility

    • The header is a button with aria-expanded and aria-controls; steps are a list, and each one announces its status ("Done:", "In progress:", "Pending:").
    • The active step has aria-current="step". The Working line is a live region; its ticking time is kept out of it.
    • Text uses the muted foreground, which meets 4.5:1 on the surface in light and dark; only decorative icons are lighter.
    • Logos and emoji are decorative next to a text label; give a SourceStack source a label so it is read out.
    • Reduced motion makes every animation instant.

    ChainOfThought props

    PropTypeDefaultDescription
    titleReactNodeThe header text, e.g. "Ran 3 searches" or "Thinking".
    iconIconComponentsearch iconThe header icon, drawn at 16px. An icon, an emoji or a logo.
    open / defaultOpenbooleantrueControlled or initial open state. The header toggles it.
    onOpenChange(open) => voidCalled with the new state when the header is clicked.
    status"working" | "done"Adds the working line under the steps: "working" (sparkle, shimmer) or "done" ("Worked for 11.2s"). Omit it for no line.
    elapsednumberElapsed seconds for the working line.
    startedAtnumberEpoch ms when the work started; the working line then counts up on its own.
    childrenReactNodeChainOfThoughtStep elements.

    ChainOfThoughtStep props

    PropTypeDefaultDescription
    status"pending" | "active" | "done"donepending (not started, grey), active (in progress, read in full) or done (muted).
    iconIconComponentsearch iconThe step's icon, drawn at 16px: a Sakani icon, an emoji or sourceLogo('reddit').
    labelReactNodeWhat happened, e.g. "Searched X for".
    querystringThe query or argument, shown in monospace after the label.
    countReactNodeA result count on the right, e.g. "7 posts". A leading number ticks up when it arrives.
    accentstringColour of the spark that runs into the step while it is active. Defaults to the icon's brand colour when it is a sourceLogo.
    sourcesReactNodeA row under the step, usually a <SourceStack>.
    sourcesLabelstringSourcesText before the sources.
    onSourcesClick() => voidMakes the sources row a button with a chevron.

    SourceStack props

    PropTypeDefaultDescription
    sources{ label, icon? }[]The sources, in order. Three to five read best. `label` is read out and shown as a tooltip; `icon` defaults to a globe.

    WorkingIndicator props

    PropTypeDefaultDescription
    status"working" | "done"working"working" shows the sparkle and a shimmering label; "done" shows a check.
    labelstringReplaces the default label ("Working" / "Worked for").
    elapsednumberElapsed seconds. Takes precedence over startedAt.
    startedAtnumberEpoch ms when the work started; while "working" the time ticks on its own.

    SourceLogo props

    PropTypeDefaultDescription
    nameSourceLogoNameWhich site (see the list above).
    sizenumber | string16Rendered size: px, or any CSS length.
    titlestringAccessible name. Without it the logo is decorative, which is right next to a text label.

    Why it works this way

    Why

    • It is built for an agent that streams: append a step when a tool call starts, give it a count when the results land, and the rail, counts and source chips animate in by themselves. It only displays, so you keep the run's state.
    • Each step can carry the real logo of the site it searched, in its brand colour, or an emoji, so a long run is scannable at a glance. The Working line and the finishing check are part of it.

    Don't

    • Don't use Stepper for an agent run. A Stepper is a progress bar for a known sequence; a Chain of Thought grows as the agent works.
    • Don't use ActivityFeed for it. That is a log of past events.
    • Don't mark several steps active. Only the step in progress reads in full.
    • Don't use a generic magnifier on every step. Give each step its site's logo or an emoji.