Skip to main content
Gremorie

Reasoning

Collapsible thinking block that opens during streaming, tracks duration and quietly closes when done.

Overview

Reasoning exposes the model's chain-of-thought without dominating the response. It opens automatically while isStreaming is true, measures elapsed seconds, then auto-collapses one second after streaming ends. The trigger swaps between a Shimmer ("Thinking...") and the recorded duration ("Thought for N seconds").

Use it inside MessageContent for assistant turns whose model exposes a reasoning trace (o-series, Claude extended thinking, DeepSeek R1, etc.).

Preview

Idle

The user wants to add a registry item. I need to:

  1. Confirm the package the primitive lives in.
  2. Check registry.json for the existing schema.
  3. Add the item with registryDependencies pointing at its peers.

Streaming

Considering whether to recommend rx-tool or rx-task for this case...

Expanded

The user wants to add a registry item. I need to:

  1. Confirm the package the primitive lives in.
  2. Check registry.json for the existing schema.
  3. Add the item with registryDependencies pointing at its peers.

Installation

bash npx gremorie@latest add rx-reasoning
bash pnpm dlx gremorie@latest add rx-reasoning
bash yarn dlx gremorie@latest add rx-reasoning
bash bunx --bun gremorie@latest add rx-reasoning

Usage

import {
  Reasoning,
  ReasoningTrigger,
  ReasoningContent,
} from "@gremorie/rx-ai";

export function Example({ status, thinking }) {
  return (
    <Reasoning isStreaming={status === "streaming"}>
      <ReasoningTrigger />
      <ReasoningContent>{thinking}</ReasoningContent>
    </Reasoning>
  );
}
import { Component, input } from "@angular/core";
import {
  Reasoning,
  ReasoningTrigger,
  ReasoningContent,
} from "@gremorie/ng-ai";

@Component({
selector: "app-example",
standalone: true,
imports: [Reasoning, ReasoningTrigger, ReasoningContent],
template: `     <reasoning [isStreaming]="status() === 'streaming'">
      <reasoning-trigger />
      <reasoning-content [text]="thinking()" />
    </reasoning>
  `,
})
export class ExampleComponent {
readonly status = input<"idle" | "streaming" | "done">("idle");
readonly thinking = input<string>("");
}

API

<Reasoning>

PropTypeDefaultDescription
isStreamingbooleanfalseWhile true, the block opens, the trigger shows a shimmering "Thinking..." and the duration timer runs.
openboolean-Controlled open state.
defaultOpenbooleantrueUncontrolled initial open state. Auto-close only fires when this is true.
onOpenChange(open: boolean) => void-Notification when the user toggles the trigger.
durationnumber-External duration override (seconds). If omitted, Reasoning measures it from isStreaming transitions.

Extends ComponentProps<typeof Collapsible>.

<ReasoningTrigger>

PropTypeDefaultDescription
getThinkingMessage(isStreaming: boolean, duration?: number) => ReactNodebuiltinCustomize the trigger label. The default renders the Shimmer while streaming and "Thought for N seconds" once done.
childrenReactNode-Fully replace the trigger contents (icon + label + chevron).

<ReasoningContent>

PropTypeDefaultDescription
childrenstring-Markdown rendered via Streamdown.

useReasoning()

Returns { isStreaming, isOpen, setIsOpen, duration }. Throws when called outside a <Reasoning> boundary.

Composition

  1. <Reasoning> owns the open / streaming / duration state and wraps a Collapsible.
  2. <ReasoningTrigger> renders the brain icon + label + chevron and toggles the collapsible.
  3. <ReasoningContent> lazy-renders the streaming markdown.

Variations

Inside an assistant message

The typical pairing - reasoning above the answer.

<Message from="assistant">
  <MessageContent>
    <Reasoning isStreaming={status === "streaming"}>
      <ReasoningTrigger />
      <ReasoningContent>{thinking}</ReasoningContent>
    </Reasoning>
    <MessageResponse>{answer}</MessageResponse>
  </MessageContent>
</Message>

Custom trigger label

Override getThinkingMessage to localize the copy or surface model metadata.

<Reasoning isStreaming={isStreaming}>
  <ReasoningTrigger
    getThinkingMessage={(streaming, duration) =>
      streaming ? (
        <Shimmer>Reasoning with o4-mini...</Shimmer>
      ) : (
        <span>Reasoning took {duration ?? '?'}s</span>
      )
    }
  />
  <ReasoningContent>{thinking}</ReasoningContent>
</Reasoning>

Controlled open state

When the surrounding shell decides when the reasoning is visible (e.g. a "Hide reasoning" toggle in settings).

const [open, setOpen] = useState(false);

<Reasoning open={open} onOpenChange={setOpen} isStreaming={false} duration={12}>
  <ReasoningTrigger />
  <ReasoningContent>{archivedThinking}</ReasoningContent>
</Reasoning>;

Accessibility

  • Keyboard: ReasoningTrigger is a CollapsibleTrigger, so it toggles on Enter / Space and participates in Tab order.
  • ARIA: the underlying Radix collapsible wires up aria-expanded and aria-controls on the trigger and data-state on the content, so screen readers announce open / closed states correctly.
  • Screen readers: the label uses live text ("Thinking..." -> "Thought for N seconds") so assistive tech narrates the transition without needing a separate live region.
  • Reduced motion: collapse / expand animations come from Tailwind data-state animations; they degrade to instant transitions when prefers-reduced-motion: reduce is set.
  • Message - the typical parent for Reasoning
  • Chain of Thought - step-by-step reasoning when you have discrete actions to surface
  • Shimmer - the loading affordance used inside the default trigger

On this page