Reasoning
Collapsible thinking block that streams while the model reasons and closes itself when done.
NVDAx is 2.4% over target. The keeper limit is 2%, so a rebalance is allowed. Slippage on 0.42 NVDAx at current depth is about 0.08%.
import { Reasoning, ReasoningContent, ReasoningTrigger,} from "@/components/ai/reasoning";import { useEffect, useState } from "react";
const text = "NVDAx is 2.4% over target. The keeper limit is 2%, so a rebalance is allowed. Slippage on 0.42 NVDAx at current depth is about 0.08%.";
// Streams the text in, then flips `isStreaming` off: the block opens while streaming and closes itself afterwards.export default function Demo() { const [shown, setShown] = useState(0); const [streaming, setStreaming] = useState(true);
useEffect(() => { if (shown >= text.length) { setStreaming(false); return; } const id = setTimeout(() => setShown((n) => n + 3), 40); return () => clearTimeout(id); }, [shown]);
return ( <div className="flex w-full max-w-lg flex-col gap-6"> <Reasoning isStreaming={streaming}> <ReasoningTrigger /> <ReasoningContent>{text.slice(0, shown)}</ReasoningContent> </Reasoning> <Reasoning defaultOpen={false} duration={6}> <ReasoningTrigger /> <ReasoningContent>{text}</ReasoningContent> </Reasoning> </div> );}<script setup lang="ts">import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai/reasoning";import { computed, onBeforeUnmount, ref } from "vue";
const text = "NVDAx is 2.4% over target. The keeper limit is 2%, so a rebalance is allowed. Slippage on 0.42 NVDAx at current depth is about 0.08%.";
// Streams the text in, then flips `isStreaming` off: the block opens while streaming and closes itself afterwards.const shown = ref(0);const streaming = ref(true);const id = setInterval(() => { shown.value += 3; if (shown.value >= text.length) { streaming.value = false; clearInterval(id); }}, 40);onBeforeUnmount(() => clearInterval(id));const partial = computed(() => text.slice(0, shown.value));</script>
<template> <div class="flex w-full max-w-lg flex-col gap-6"> <Reasoning :is-streaming="streaming"> <ReasoningTrigger /> <ReasoningContent :content="partial" /> </Reasoning> <Reasoning :default-open="false" :duration="6"> <ReasoningTrigger /> <ReasoningContent :content="text" /> </Reasoning> </div></template><script lang="ts"> import { Reasoning, ReasoningContent, ReasoningTrigger } from "$lib/components/ai/reasoning"; import { onMount } from "svelte";
const text = "NVDAx is 2.4% over target. The keeper limit is 2%, so a rebalance is allowed. Slippage on 0.42 NVDAx at current depth is about 0.08%.";
// Streams the text in, then flips `isStreaming` off: the block opens while streaming and closes itself afterwards. let shown = $state(0); let streaming = $state(true); onMount(() => { const id = setInterval(() => { shown += 3; if (shown >= text.length) { streaming = false; clearInterval(id); } }, 40); return () => clearInterval(id); });</script>
<div class="flex w-full max-w-lg flex-col gap-6"> <Reasoning isStreaming={streaming}> <ReasoningTrigger /> <ReasoningContent content={text.slice(0, shown)} /> </Reasoning> <Reasoning defaultOpen={false} duration={6}> <ReasoningTrigger /> <ReasoningContent content={text} /> </Reasoning></div>Built on collapsible and ai-shimmer. The content renders markdown with Streamdown; React consumers add the @source line described on the ai-message page.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-reasoningpnpm dlx shadcn@latest add @edmi-ui/ai-reasoningyarn dlx shadcn@latest add @edmi-ui/ai-reasoningbunx shadcn@latest add @edmi-ui/ai-reasoningPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install streamdown @streamdown/cjk @streamdown/code @streamdown/math @streamdown/mermaidTerminal window pnpm add streamdown @streamdown/cjk @streamdown/code @streamdown/math @streamdown/mermaidTerminal window yarn add streamdown @streamdown/cjk @streamdown/code @streamdown/math @streamdown/mermaidTerminal window bun add streamdown @streamdown/cjk @streamdown/code @streamdown/math @streamdown/mermaidAdd the Edmi components this one builds on:
collapsible,ai-shimmer.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-reasoningpnpm dlx shadcn-vue@latest add @edmi-ui/ai-reasoningyarn dlx shadcn-vue@latest add @edmi-ui/ai-reasoningbunx shadcn-vue@latest add @edmi-ui/ai-reasoningPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vue @vueuse/core vue-stream-markdownTerminal window pnpm add @lucide/vue @vueuse/core vue-stream-markdownTerminal window yarn add @lucide/vue @vueuse/core vue-stream-markdownTerminal window bun add @lucide/vue @vueuse/core vue-stream-markdownAdd the Edmi components this one builds on:
collapsible,ai-shimmer.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-reasoning.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-reasoning.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-reasoning.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-reasoning.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
collapsible,ai-shimmer.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Reasoning, ReasoningContent, ReasoningTrigger,} from "@/components/ai/reasoning";
<div className="flex w-full max-w-lg flex-col gap-6"> <Reasoning> <ReasoningTrigger /> <ReasoningContent></ReasoningContent> </Reasoning> <Reasoning defaultOpen={false} duration={6}> <ReasoningTrigger /> <ReasoningContent></ReasoningContent> </Reasoning></div><script setup lang="ts">import { Reasoning, ReasoningContent, ReasoningTrigger,} from "@/components/ai/reasoning";</script>
<template> <div class="flex w-full max-w-lg flex-col gap-6"> <Reasoning> <ReasoningTrigger /> <ReasoningContent /> </Reasoning> <Reasoning :default-open="false" :duration="6"> <ReasoningTrigger /> <ReasoningContent /> </Reasoning> </div></template><script lang="ts"> import { Reasoning, ReasoningContent, ReasoningTrigger, } from "$lib/components/ai/reasoning";</script>
<div class="flex w-full max-w-lg flex-col gap-6"> <Reasoning> <ReasoningTrigger /> <ReasoningContent /> </Reasoning> <Reasoning defaultOpen={false} duration={6}> <ReasoningTrigger /> <ReasoningContent /> </Reasoning></div>Component source
Section titled “Component source”"use client";
import { cjk } from "@streamdown/cjk";import { code } from "@streamdown/code";import { math } from "@streamdown/math";import { mermaid } from "@streamdown/mermaid";import { cn } from "cn";import type { ComponentProps, ReactNode } from "react";import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState,} from "react";import { Streamdown } from "streamdown";import { BrainIcon, CaretDownIcon } from "@phosphor-icons/react";import { Shimmer } from "@/registry/edmi/components/ai/shimmer";import { useControllableState } from "@/registry/edmi/hooks/ai/use-controllable-state";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/registry/edmi/ui/collapsible";
interface ReasoningContextValue { isStreaming: boolean; isOpen: boolean; setIsOpen: (open: boolean) => void; duration: number | undefined;}
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
export const useReasoning = () => { const context = useContext(ReasoningContext); if (!context) { throw new Error("Reasoning components must be used within Reasoning"); } return context;};
export type ReasoningProps = Omit< ComponentProps<typeof Collapsible>, "open" | "defaultOpen" | "onOpenChange"> & { isStreaming?: boolean; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; duration?: number;};
const AUTO_CLOSE_DELAY = 1000;const MS_IN_S = 1000;
export const Reasoning = memo( ({ className, isStreaming = false, open, defaultOpen, onOpenChange, duration: durationProp, children, ...props }: ReasoningProps) => { const resolvedDefaultOpen = defaultOpen ?? isStreaming; // Track if defaultOpen was explicitly set to false (to prevent auto-open) const isExplicitlyClosed = defaultOpen === false;
const [isOpen, setIsOpen] = useControllableState<boolean>({ defaultProp: resolvedDefaultOpen, onChange: onOpenChange, prop: open, }); const [duration, setDuration] = useControllableState<number | undefined>({ defaultProp: undefined, prop: durationProp, });
const hasEverStreamedRef = useRef(isStreaming); const [hasAutoClosed, setHasAutoClosed] = useState(false); const startTimeRef = useRef<number | null>(null);
// Track when streaming starts and compute duration useEffect(() => { if (isStreaming) { hasEverStreamedRef.current = true; if (startTimeRef.current === null) { startTimeRef.current = Date.now(); } } else if (startTimeRef.current !== null) { setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S)); startTimeRef.current = null; } }, [isStreaming, setDuration]);
// Auto-open when streaming starts (unless explicitly closed) useEffect(() => { if (isStreaming && !isOpen && !isExplicitlyClosed) { setIsOpen(true); } }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
// Auto-close when streaming ends (once only, and only if it ever streamed) useEffect(() => { if ( hasEverStreamedRef.current && !isStreaming && isOpen && !hasAutoClosed ) { const timer = setTimeout(() => { setIsOpen(false); setHasAutoClosed(true); }, AUTO_CLOSE_DELAY);
return () => clearTimeout(timer); } }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
const handleOpenChange = useCallback( (newOpen: boolean) => { setIsOpen(newOpen); }, [setIsOpen], );
const contextValue = useMemo( () => ({ duration, isOpen, isStreaming, setIsOpen }), [duration, isOpen, isStreaming, setIsOpen], );
return ( <ReasoningContext.Provider value={contextValue}> <Collapsible data-slot="ai-reasoning" className={cn("not-prose", className)} onOpenChange={handleOpenChange} open={isOpen} {...props} > {children} </Collapsible> </ReasoningContext.Provider> ); },);
export type ReasoningTriggerProps = ComponentProps< typeof CollapsibleTrigger> & { getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;};
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => { if (isStreaming || duration === 0) { return ( <Shimmer as="span" duration={1}> Thinking… </Shimmer> ); } if (duration === undefined) { return <span>Thought for a few seconds</span>; } return <span>Thought for {duration} seconds</span>;};
export const ReasoningTrigger = memo( ({ className, children, getThinkingMessage = defaultGetThinkingMessage, ...props }: ReasoningTriggerProps) => { const { isStreaming, isOpen, duration } = useReasoning();
return ( <CollapsibleTrigger data-slot="ai-reasoning-trigger" className={cn( "flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", className, )} {...props} > {children ?? ( <> <BrainIcon className="size-4" /> {getThinkingMessage(isStreaming, duration)} <CaretDownIcon className={cn( "ml-auto size-4 transition-transform", isOpen ? "rotate-180" : "rotate-0", )} /> </> )} </CollapsibleTrigger> ); },);
export type ReasoningContentProps = Omit< ComponentProps<typeof CollapsibleContent>, "children"> & { children: string;};
const streamdownPlugins = { cjk, code, math, mermaid };
export const ReasoningContent = memo( ({ className, children, ...props }: ReasoningContentProps) => ( <CollapsibleContent data-slot="ai-reasoning-content" className={cn( "h-(--collapsible-panel-height) overflow-hidden outline-none transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0", className, )} {...props} > <div className="mt-2.5 ml-[7px] border-l-2 border-border pl-[23px] text-[13.5px] leading-[1.6] text-muted-foreground"> <Streamdown className="[&_p]:my-0 [&_p+*]:mt-2 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0" plugins={streamdownPlugins} > {children} </Streamdown> </div> </CollapsibleContent> ),);
Reasoning.displayName = "Reasoning";ReasoningTrigger.displayName = "ReasoningTrigger";ReasoningContent.displayName = "ReasoningContent";<script setup lang="ts">import type { HTMLAttributes, Ref } from "vue"import { useVModel } from "@vueuse/core"import { computed, onBeforeUnmount, provide, ref, watch } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Collapsible } from "@/registry/edmi/ui/collapsible"import { ReasoningKey } from "./context"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] isStreaming?: boolean open?: boolean defaultOpen?: boolean /** Seconds. Measured automatically when `isStreaming` flips to false. */ duration?: number}>(), { isStreaming: false, open: undefined, defaultOpen: undefined, duration: undefined,})
const emit = defineEmits<{ (e: "update:open", value: boolean): void (e: "update:duration", value: number): void}>()
const AUTO_CLOSE_DELAY = 1000const MS_IN_S = 1000
const isOpen = useVModel(props, "open", emit, { defaultValue: props.defaultOpen ?? props.isStreaming, passive: true,}) as Ref<boolean>
const internalDuration = ref<number | undefined>(props.duration)watch(() => props.duration, (value) => { internalDuration.value = value})
const hasEverStreamed = ref(props.isStreaming)const hasAutoClosed = ref(false)let startTime: number | null = null
// Track when streaming starts and compute the duration when it ends; auto-open on start unless// `defaultOpen` was explicitly false.watch(() => props.isStreaming, (streaming) => { if (streaming) { hasEverStreamed.value = true if (!isOpen.value && props.defaultOpen !== false) isOpen.value = true if (startTime === null) startTime = Date.now() } else if (startTime !== null) { const seconds = Math.ceil((Date.now() - startTime) / MS_IN_S) internalDuration.value = seconds emit("update:duration", seconds) startTime = null }}, { immediate: true })
// Auto-close once when streaming ends (only if it ever streamed).let timer: ReturnType<typeof setTimeout> | undefinedwatch([() => props.isStreaming, isOpen, hasAutoClosed], () => { clearTimeout(timer) if (hasEverStreamed.value && !props.isStreaming && isOpen.value && !hasAutoClosed.value) { timer = setTimeout(() => { isOpen.value = false hasAutoClosed.value = true }, AUTO_CLOSE_DELAY) }}, { immediate: true })onBeforeUnmount(() => clearTimeout(timer))
provide(ReasoningKey, { isStreaming: computed(() => props.isStreaming), isOpen, setIsOpen: (value: boolean) => { isOpen.value = value }, duration: computed(() => internalDuration.value),})</script>
<template> <Collapsible v-model:open="isOpen" data-slot="ai-reasoning" :class="cn('not-prose', props.class)" > <slot /> </Collapsible></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed, useSlots } from "vue"import { Markdown } from "vue-stream-markdown"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleContent } from "@/registry/edmi/ui/collapsible"import "vue-stream-markdown/index.css"
const props = defineProps<{ class?: HTMLAttributes["class"] /** Markdown source; the default slot text works too. */ content?: string}>()
const slots = useSlots()const slotContent = computed<string | undefined>(() => { const nodes = slots.default?.() if (!Array.isArray(nodes)) return undefined let text = "" for (const node of nodes) { if (typeof node.children === "string") text += node.children } return text || undefined})
const md = computed(() => (slotContent.value ?? props.content ?? "") as string)</script>
<template> <CollapsibleContent data-slot="ai-reasoning-content" :class="cn( 'overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down', props.class, )" > <div class="mt-2.5 ml-[7px] border-l-2 border-border pl-[23px] text-[13.5px] leading-[1.6] text-muted-foreground"> <Markdown :content="md" :link-options="{ favicon: false }" class="text-[13.5px]! leading-[1.6]! text-muted-foreground! [&>*:first-child]:mt-0! [&>*:last-child]:mb-0! [&>*+*]:mt-2! [&_[data-stream-markdown=p]]:my-0!" /> </div> </CollapsibleContent></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { SparklesIcon, ChevronDownIcon } from "@lucide/vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"import { Shimmer } from "@/registry/edmi/components/ai/shimmer"import { useReasoningContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"] /** Custom label; return a string. The shimmer is used while streaming either way. */ getThinkingMessage?: (isStreaming: boolean, duration?: number) => string}>()
const { isStreaming, isOpen, duration } = useReasoningContext()
const label = computed(() => { if (props.getThinkingMessage) return props.getThinkingMessage(isStreaming.value, duration.value) if (isStreaming.value || duration.value === 0) return "Thinking…" if (duration.value === undefined) return "Thought for a few seconds" return `Thought for ${duration.value} seconds`})const shimmering = computed(() => isStreaming.value || duration.value === 0)</script>
<template> <CollapsibleTrigger data-slot="ai-reasoning-trigger" :class="cn( 'flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', props.class, )" > <slot :is-streaming="isStreaming" :is-open="isOpen" :duration="duration"> <SparklesIcon class="size-4" /> <Shimmer v-if="shimmering" as="span" :duration="1"> {{ label }} </Shimmer> <span v-else>{{ label }}</span> <ChevronDownIcon :class="cn('ml-auto size-4 transition-transform', isOpen ? 'rotate-180' : 'rotate-0')" /> </slot> </CollapsibleTrigger></template>import type { InjectionKey, Ref } from "vue";import { inject } from "vue";
export interface ReasoningContextValue { isStreaming: Ref<boolean>; isOpen: Ref<boolean>; setIsOpen: (open: boolean) => void; duration: Ref<number | undefined>;}
export const ReasoningKey: InjectionKey<ReasoningContextValue> = Symbol("ReasoningContext");
export function useReasoningContext() { const ctx = inject(ReasoningKey); if (!ctx) throw new Error("Reasoning components must be used within <Reasoning>"); return ctx;}export * from "./context";export { default as Reasoning } from "./Reasoning.vue";export { default as ReasoningContent } from "./ReasoningContent.vue";export { default as ReasoningTrigger } from "./ReasoningTrigger.vue";import Root from "./reasoning.svelte";import Content from "./reasoning-content.svelte";import Trigger from "./reasoning-trigger.svelte";
export * from "./use-reasoning.svelte.js";export { Content, Content as ReasoningContent, Root, // Root as Reasoning, Trigger, Trigger as ReasoningTrigger,};<script lang="ts"> import { cn } from "$lib/utils.js"; import { CollapsibleContent } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte"; import { Streamdown } from "svelte-streamdown";
let { class: className, content, ...restProps }: Omit<ComponentProps<typeof CollapsibleContent>, "children"> & { /** Markdown source. */ content: string; } = $props();
const theme = { paragraph: { base: "my-0" } };</script>
<CollapsibleContent data-slot="ai-reasoning-content" class={cn( "overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className )} {...restProps}> <div class="mt-2.5 ml-[7px] border-l-2 border-border pl-[23px] text-[13.5px] leading-[1.6] text-muted-foreground [&>*:first-child]:mt-0 [&>*:last-child]:mb-0" > <Streamdown {content} baseTheme="shadcn" {theme} /> </div></CollapsibleContent><script lang="ts"> import BrainIcon from "phosphor-svelte/lib/Brain"; import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn } from "$lib/utils.js"; import { CollapsibleTrigger } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte"; import { Shimmer } from "../shimmer/index.js"; import { useReasoning } from "./use-reasoning.svelte.js";
let { class: className, children, getThinkingMessage, ...restProps }: ComponentProps<typeof CollapsibleTrigger> & { /** Custom label; return a string. The shimmer is used while streaming either way. */ getThinkingMessage?: (isStreaming: boolean, duration?: number) => string; } = $props();
const reasoning = useReasoning();
const label = $derived.by(() => { if (getThinkingMessage) return getThinkingMessage(reasoning.isStreaming, reasoning.duration); if (reasoning.isStreaming || reasoning.duration === 0) return "Thinking…"; if (reasoning.duration === undefined) return "Thought for a few seconds"; return `Thought for ${reasoning.duration} seconds`; }); const shimmering = $derived(reasoning.isStreaming || reasoning.duration === 0);</script>
<CollapsibleTrigger data-slot="ai-reasoning-trigger" class={cn( "flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", className )} {...restProps}> {#if children} {@render children()} {:else} <BrainIcon class="size-4" /> {#if shimmering} <Shimmer as="span" duration={1}>{label}</Shimmer> {:else} <span>{label}</span> {/if} <CaretDownIcon class={cn("ml-auto size-4 transition-transform", reasoning.isOpen ? "rotate-180" : "rotate-0")} /> {/if}</CollapsibleTrigger><script lang="ts"> import { cn } from "$lib/utils.js"; import { Collapsible } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte"; import { setReasoningContext } from "./use-reasoning.svelte.js";
let { class: className, isStreaming = false, open = $bindable(), defaultOpen, duration = $bindable(), children, ...restProps }: Omit<ComponentProps<typeof Collapsible>, "open"> & { isStreaming?: boolean; open?: boolean; defaultOpen?: boolean; /** Seconds. Measured automatically when `isStreaming` flips to false. */ duration?: number; } = $props();
const AUTO_CLOSE_DELAY = 1000; const MS_IN_S = 1000;
// svelte-ignore state_referenced_locally if (open === undefined) open = defaultOpen ?? isStreaming;
// svelte-ignore state_referenced_locally let hasEverStreamed = isStreaming; let hasAutoClosed = $state(false); let startTime: number | null = null;
// Track when streaming starts and compute the duration when it ends; auto-open on start unless // `defaultOpen` was explicitly false. $effect(() => { if (isStreaming) { hasEverStreamed = true; if (!open && defaultOpen !== false) open = true; if (startTime === null) startTime = Date.now(); } else if (startTime !== null) { duration = Math.ceil((Date.now() - startTime) / MS_IN_S); startTime = null; } });
// Auto-close once when streaming ends (only if it ever streamed). $effect(() => { if (hasEverStreamed && !isStreaming && open && !hasAutoClosed) { const timer = setTimeout(() => { open = false; hasAutoClosed = true; }, AUTO_CLOSE_DELAY); return () => clearTimeout(timer); } });
setReasoningContext({ get isStreaming() { return isStreaming; }, get isOpen() { return !!open; }, get duration() { return duration; }, setIsOpen: (value: boolean) => { open = value; }, });</script>
<Collapsible bind:open data-slot="ai-reasoning" class={cn("not-prose", className)} {...restProps}> {@render children?.()}</Collapsible>import { getContext, setContext } from "svelte";
/** Getter object so consumers stay reactive. */export interface ReasoningContextValue { readonly isStreaming: boolean; readonly isOpen: boolean; readonly duration: number | undefined; setIsOpen: (open: boolean) => void;}
const REASONING_KEY = Symbol("ai-reasoning");
export function setReasoningContext(value: ReasoningContextValue) { return setContext(REASONING_KEY, value);}
export function useReasoning(): ReasoningContextValue { const ctx = getContext<ReasoningContextValue | undefined>(REASONING_KEY); if (!ctx) throw new Error("Reasoning components must be used within <Reasoning>"); return ctx;}| Part | Prop | Type | Default |
|---|---|---|---|
| Reasoning | isStreaming |
boolean |
false |
| Reasoning | open, defaultOpen, onOpenChange |
controlled / uncontrolled open state | defaultOpen = isStreaming |
| Reasoning | duration |
number (seconds; measured automatically while isStreaming flips) |
|
| ReasoningTrigger | getThinkingMessage |
(isStreaming, duration) => ReactNode |
shimmer Thinking…, then Thought for N seconds |
| ReasoningContent | children |
string (rendered as markdown) |
✦ Edmi additions
Section titled “✦ Edmi additions”- Opens on its own while
isStreaming, closes one second after streaming ends and keeps the measured duration as the label. - The content sits behind a 2px
--borderrule; the label is muted until hovered. - The shimmer sweep is a
--foregroundhighlight over--muted-foreground, so it reads in light and dark.