Skip to main content
Gremorie
Chatbot

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.
'use client';import { Reasoning, ReasoningContent, ReasoningTrigger } from '@gremorie/rx-ai';const REASONING_TEXT = `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.`;export function ReasoningPreview() {  return (    <Reasoning isStreaming={false} defaultOpen>      <ReasoningTrigger />      <ReasoningContent>{REASONING_TEXT}</ReasoningContent>    </Reasoning>  );}

Streaming

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

'use client';import { Reasoning, ReasoningContent, ReasoningTrigger } from '@gremorie/rx-ai';export function ReasoningStreamingPreview() {  return (    <Reasoning isStreaming defaultOpen>      <ReasoningTrigger />      <ReasoningContent>        Considering whether to recommend rx-tool or rx-task for this case...      </ReasoningContent>    </Reasoning>  );}

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.
'use client';import { Reasoning, ReasoningContent, ReasoningTrigger } from '@gremorie/rx-ai';const REASONING_TEXT = `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.`;export function ReasoningExpandedPreview() {  return (    <Reasoning isStreaming={false} defaultOpen>      <ReasoningTrigger />      <ReasoningContent>{REASONING_TEXT}</ReasoningContent>    </Reasoning>  );}

Anatomy

Reasoning
├─ ReasoningTrigger   "Thinking…" label (shimmers while streaming)
└─ ReasoningContent   the collapsible reasoning text

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>
  );
}
npx gremorie@latest add ng-reasoning

Prerequisite

Angular components emit Tailwind utility classes and ship no compiled CSS. Without the setup below in src/styles.css the component mounts but renders completely unstyled. Requires Tailwind CSS v4.

@import 'tailwindcss';@import '@gremorie/tokens/theme.css';/* Required. Tailwind v4 skips node_modules by default, so without this line   the components' utility classes never reach your compiled CSS and every   Gremorie component renders unstyled. */@source '../node_modules/@gremorie';

The @source line is required: Tailwind v4 skips node_modules by default. Adjust the relative path if your stylesheet is not at src/styles.css. Full setup in Installation.

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