Reasoning disclosure A collapsed panel carrying a one-line summary and a duration. What expands is a summarised trace rather than the raw chain, because 200 words of answer buried under 800 words of working reads worse than the answer alone. npx shadcn@latest add collapsible button npm i lucide-react Tokens this needs: --card, --card-foreground, --muted-foreground, --border ──────────────────────────────────────────────────────────────────────── // ReasoningDisclosure.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { ChevronRight, Square } from "lucide-react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; type Props = { /** One line the viewer can read without expanding: enough to catch a * misread question at second three. The panel prefixes "Working:", never * "Thinking:"; this is narration produced alongside the answer, not a * trace of the computation. */ summary: string; /** The summarised trace, a paragraph per step. Never the raw chain. */ trace: string[]; /** The real one. A duration next to the label does more for trust than any adjective. */ durationMs: number; /** True while the working is still arriving. Shows stop, because the viewer * who has spotted a misread should not have to wait for the answer. */ streaming?: boolean; onStop?: () => void; /** Collapsed by default. The working runs longer than the answer and matters less. */ defaultOpen?: boolean; }; const duration = (ms: number) => { const s = Math.round(ms / 1000); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; }; export function ReasoningDisclosure({ summary, trace, durationMs, streaming = false, onStop, defaultOpen = false }: Props) { const [open, setOpen] = useState(defaultOpen); return (
Working: {summary} {streaming && onStop ? ( ) : ( {duration(durationMs)} )}
{trace.map((step, i) => (

{step}

))} {/* Say what it is. The full chain belongs behind a developer affordance. */}

summarised, not the raw chain

); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { ReasoningDisclosure } from "./ReasoningDisclosure"; /** * The working for one lease question, opened so the summarised trace shows. * A product would leave `defaultOpen` off and let the viewer expand it. */ const TRACE = [ "The question is whether the tenant's break date comes before the rent review, and what notice that needs.", "The lease starts 1 March 2024. The break is at the end of year two, so 28 February 2026, on six months' written notice. The review is at the third anniversary, 1 March 2027.", "The break falls a year before the review, so notice has to be served by 31 August 2025, with rent paid up and vacant possession on the day.", ]; export default function Demo() { return (
console.log("stop")} defaultOpen />

Yes. The break on 28 February 2026 comes a year before the review on 1 March 2027. Notice has to be served by 31 August 2025, with rent paid up and the premises returned empty.

); }