Mischief

22 / Agent UI

Thinking State

A status row for work in progress, with a live elapsed timer and optional reasoning behind a disclosure.

Thinking0.0s

Installation

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

npx shadcn@latest add Tinkerers-Labs/mischief-ui/thinking-state
import { ThinkingState } from "mischief-ui/thinking-state"
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/thinking-state/thinking-state.tsx
"use client" import * as React from "react"import { ChevronRight, Loader, Sparkles, TriangleAlert } from "lucide-react"import { cn } from "@/lib/utils" export type ThinkingStatus = "idle" | "thinking" | "done" | "error" export type ThinkingStateProps = Omit<  React.HTMLAttributes<HTMLDivElement>,  "children"> & {  status?: ThinkingStatus  label?: React.ReactNode

Usage

export function Status({ startedAt }: { startedAt: number }) {
  return (
    <ThinkingState
      status="thinking"
      startedAt={startedAt}
      reasoning={<StreamingText source={reasoningStream} />}
    />
  )
}

The four states

The component shows one of four things, and each is announced politely as it changes so a reader who is not watching still learns that the answer has started or finished.

StatusShows
idleNothing is happening. Render it or do not, as you prefer.
thinkingThe working indicator, and a live duration if startedAt is set.
donedoneLabel, and the final duration.
errorerrorLabel in place of the label.

Pass startedAt and the duration counts up on its own; pass elapsedMs and that fixed figure is shown instead. The second is what you want when replaying a conversation, where a live counter would start again from zero on every render of an old message.

Showing the reasoning

reasoning goes behind a disclosure that starts closed, because the point of this component is to say that work is happening without burying the answer underneath the working. Someone curious can open it; nobody has to scroll past it.

Think about what you put in there. Intermediate reasoning is often less careful than the final answer, and once it is on screen it can be screenshotted and quoted as though it were the conclusion. A summary of the steps is usually more useful, and more defensible, than the raw trace.

API

status"idle" | "thinking" | "done" | "error"The current phase. Defaults to "thinking".
label, doneLabel, errorLabelReactNodeCopy for each phase.
startedAtnumberEpoch milliseconds. Drives a timer that ticks while thinking.
elapsedMsnumberA fixed duration, used instead of the timer when supplied.
showElapsedbooleanShows the duration. Defaults to true.
reasoningReactNodeOptional detail behind a disclosure. Compose Streaming Text here for live reasoning.
open, defaultOpen, onOpenChangeboolean, boolean, (open: boolean) => voidControls the reasoning disclosure.

Accessibility

The root carries aria-busy while thinking and drops it once the work settles. Reasoning uses a native button with aria-expanded and aria-controls rather than a details element, so it can animate and stay predictable. The spinner and label stop animating under reduced motion.