Mischief

53 / Agent UI

Chain of Thought

The steps an assistant took before answering, open while it is working and folded away once it is done.

  1. Read the retry handler800ms, done

    src/upload/retry.ts, 140 lines.
  2. Traced where the listener is added, running

    onReconnect registers it again without removing the previous one.
Working on it

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/chain-of-thought
import { ChainOfThought } from "mischief-ui/chain-of-thought"
Also installs
  • lucide-react

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/chain-of-thought/chain-of-thought.tsx
"use client" import * as React from "react"import { Check, ChevronRight, X } from "lucide-react" import { cn } from "@/lib/utils" export type ThoughtStatus = "pending" | "active" | "done" | "failed" export type Thought = {  id: string  label: string  detail?: React.ReactNode  status?: ThoughtStatus

Usage

export function Answer({ steps, running }) {
  return <ChainOfThought thoughts={steps} thinking={running} />
}

The trace is never announced

Reasoning arrives a token at a time. Put it in a live region and a screen reader reads every revision of every half-finished step, over the top of the answer the person actually asked for. It is the loudest possible way to be helpful.

So the steps are ordinary text, reachable on purpose, and the only thing announced is the summary line: working on it, then thought for four seconds. Each step's status is in its accessible name rather than in the colour of a dot.

It opens and closes itself, once

Open while thinking and folded away when the answer arrives is right, because the reasoning is interesting while there is nothing else to look at and clutter afterwards.

It stops deciding the moment someone touches it. Collapsing a trace under a reader who opened it to read is the rudest thing this component could do, so the first click hands control over for good.

<ChainOfThought thoughts={steps} open={open} onOpenChange={setOpen} />
Or hold it yourself, and it never decides at all.

What a step can be

StatusWhat it means
pendingPlanned, not started
activeRunning now
doneFinished, and how long it took
failedTried and did not work

A step with no status is done. Each one is also written on its element as data-status, so a thread can style around it without lifting the state.

API

thoughtsreadonly Thought[]The steps, in the order they ran.
thinkingbooleanStill working. The trace opens itself while this is true.
openbooleanHold the disclosure yourself.
defaultOpenbooleanStart open, and never decide again.
onOpenChange(open: boolean) => voidSomeone opened or closed it.
...rootPropsHTMLAttributes<HTMLDivElement>Native root attributes.

Thought

idstringUnique within the trace.
labelstringThe step, short enough to read in one breath.
detailReactNodeWhat it found, under the label.
status"pending" | "active" | "done" | "failed"Defaults to done.
durationnumberSeconds this step took.

Accessibility

The disclosure is a button carrying aria-expanded, so the trace can be opened from a keyboard and its state is read out. Only the summary is in a polite live region: streaming reasoning is deliberately kept out of one, because announcing every token would talk over the answer. A step's status reaches a screen reader as words in its accessible name rather than as the colour of its marker, and the summary stops pulsing when reduced motion is preferred.