Installation
npx shadcn@latest add @pixeldosa/thinking-experienceRequires `cn` at `@/lib/utils`. Installing this pulls in the whole AI-tier stack it composes — Live Status Line, Reasoning Stream, Agent Ask, AI Approval Gate and AI Action Toolbar, plus Agent Presence, Confidence Meter and AI Context Surface transitively. Drive it from a single agent state; passing `ask` and `approval` alongside is harmless, since only the one matching the current state renders.
Usage
"use client";
import { ThinkingExperience } from "@/components/blocks/thinking-experience";
<ThinkingExperience
state={run.state}
status={run.status}
detail={run.target}
startedAt={run.startedAt}
reasoning={run.reasoningSummaries}
ask={pendingQuestion}
approval={pendingApproval}
result={<Summary data={run.result} />}
resultActions={[{ id: "apply", label: "Apply", intent: "primary" }]}
onCancel={run.abort}
/>
This is a Block, not a component. Every visible element comes from an already-shipped component — what this adds is the orchestration.
What it actually contributes
Wiring Live Status Line, Reasoning Stream, Agent Ask and AI Approval Gate together yourself means re-deriving four decisions:
- Which surface belongs on screen at each state.
- When reasoning stops streaming — the moment control passes to a human, not whenever the text runs out.
- When the elapsed counter freezes — on terminal states, rather than counting forever.
- What happens to the machine's surfaces while a person is being asked something.
That last one is where most implementations go wrong, and it's the interesting part.
The grammar, scaled up
At component level the turn-taking rule is one indicator moving or holding still. At panel level it becomes relative emphasis: when control passes back, the agent's status and reasoning step down (indicators fade, text drops to the muted colour but stays readable) and whatever needs an answer takes a ring.
Two deliberate choices there:
- Dimmed, never disabled. A run must stay stoppable even while it waits on you.
pointer-events-noneon that region would have silently removed the Stop control at exactly the moment someone might want it. - Emphasis by ring, not motion. This is the user's turn, so stillness is the signal. Animating the thing that wants attention would contradict the rule the rest of the system holds.
Variants
Working
Readingledger/august-import.json
Checking the invoice table for duplicate payment warnings.
Found five flagged invoices across the last quarter.
Checking the invoice table for duplicate payment warnings.
Found five flagged invoices across the last quarter.
Waiting for approval
Note the status and reasoning above the gate — still legible, still stoppable, visibly no longer the point.
Waiting for your approval
Three invoices share the same billing contact.
Send the reconciliation summary to 243 contacts
- Recipients
- 243
This cannot be undone.
Complete
Reconciled 5 invoices
Three invoices share the same billing contact.
The duplicate-payment warning had one root cause.
Examples
Driving it from one state — passing ask and approval alongside is harmless, since
only the one matching the current state renders:
<ThinkingExperience
state={toAgentState(run)}
status={run.status}
ask={run.elicitation ? { ...toAsk(run.elicitation), onRespond: resume } : undefined}
approval={run.interrupt ? { ...toApproval(run.interrupt), onApprove, onReject } : undefined}
/>
As a sidebar panel, where it's permanent chrome rather than a moment:
<aside className="w-96 border-l p-3">
<ThinkingExperience {...run} className="border-0 p-0" />
</aside>
Props
| Prop | Type | Description |
|---|---|---|
state | AgentState | Required. Decides which surface renders. |
status | string | Required. The current micro-action. |
detail | string | What the action is acting on. |
startedAt | number | Date | Elapsed counter; freezes on terminal states. |
reasoning | string[] | Reasoning summaries. |
reasoningMs | number | Measured duration, shown once complete. |
ask | AskRequest | Rendered when state is asking. |
approval | ApprovalRequest | Rendered when state is awaitingApproval. |
result | ReactNode | Rendered on terminal states. |
resultActions | AIAction[] | Action strip beneath the result. |
onCancel | () => void | Always available while active. |
headingLevel | 2 | 3 | 4 | 5 | 6 | Heading level, so the component fits your page's outline. Defaults to 3. |
expression | "full" | "subtle" | "off" | How much of the agent shows. full draws the whole figure beside the run, subtle keeps its head in the status line, off uses dots. Defaults to "subtle". |
Composes
Live Status Line · Reasoning Stream · Agent Ask · AI Approval Gate ·
AI Action Toolbar — and transitively Agent Presence, Confidence Meter,
AI Context Surface. Installing this pulls the whole stack.
When not to use
This shows one run in progress, not a history of turns — it isn't a chat thread or
a transcript. For a single agent status in existing chrome without the surrounding
panel, use Live Status Line directly.
why it's built this way
Engineering Notes
The first Block in this system, and it is deliberately an orchestration rather than new UI: every visible element comes from an already-shipped component. What it contributes is the set of decisions a consumer would otherwise have to re-derive — which surface belongs on screen at which state, that reasoning stops streaming the moment control passes to a human rather than continuing to tick behind a question, that the elapsed counter freezes on terminal states instead of counting forever, and that a pending question or approval is what the eye should land on. The interesting decision is how the turn-taking grammar scales: at component level it is one indicator moving or holding still, but at panel level it becomes relative emphasis, so the machine's status and reasoning step down (indicator graphics fade to 40% and primary text drops to the muted colour, keeping text above 4.5:1 contrast rather than dimming it with opacity) while an interrupt takes a ring. They are dimmed but deliberately never disabled — a run must remain stoppable even while it is waiting on the person, and `pointer-events-none` on that region would have silently removed the Stop control at exactly the moment someone might want it. Emphasis is carried by a ring rather than by animation because this is the user's turn and stillness is the signal; animating the thing that wants attention would contradict the grammar the rest of the system holds. Ask and approval are passed as plain data objects rather than as slots so the block owns the decision of when each appears — a slot-based API would hand the orchestration question straight back to the consumer, which is the one thing this component exists to answer.
how it moves, and when it doesn't
Motion Notes
Two transitions, both mapped to a real state change. The machine region cross-fades its text colour and indicator opacity over `--pd-duration-base` with `--pd-ease-standard` as control passes between the agent and the user, which is the panel-level expression of the turn-taking grammar and the only motion that belongs to this block itself. The result fades in once over `--pd-duration-base` with `--pd-ease-decelerate` when the run reaches a terminal state. Everything else is inherited: Live Status Line's status swap, Agent Presence's rotation, Reasoning Stream's step fades and disclosure, AI Action Toolbar's colour transitions and busy spinner. Nothing is duplicated at this level, which is the point of composing rather than reimplementing — one rhythm, set once in the tokens, applied consistently by every part. Under `prefers-reduced-motion` the block's own two transitions drop to instant and each child handles its own fallback, leaving the layout, the ring emphasis and the opacity difference fully legible without any movement.