Reasoning
The model's reasoning, folded above its answer — streamed while it thinks, closed once it is done, opened by the reader who wants to know why.
-
type: 'thinking'
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
document.querySelector('aparte-chat-bubble').setSegments([
{
id: 's1',
type: 'thinking',
// Settled: the block folds, and the reader opens it if they want the reasoning.
isStreaming: false,
label: 'Thought for 4 seconds',
content: 'The user asks which transport to use. A prototype keeps the key in the browser; production should not. So: direct for the demo, backend for the release.',
},
{ id: 's2', type: 'text', content: 'Use **AparteDirectTransport** while you prototype, and switch to **AparteBackendTransport** before you ship, so the key stays on your server.' },
]);
</script>A reasoning block is a thinking segment, drawn as a disclosure above the answer. It is closed
by default: the answer is what a reader came for, and the reasoning is one click away. A model
that streams its reasoning (a <think> block, or a provider’s reasoning channel) produces the
segment during the turn; you can also build one yourself, as these demos do.
Variants
Section titled “Variants”While it streams
Section titled “While it streams”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" streaming></aparte-chat-bubble>
<script>
document.querySelector('aparte-chat-bubble').setSegments([
// Still arriving: the client appends tokens to it as the model reasons.
{ id: 's1', type: 'thinking', isStreaming: true, content: 'Comparing the two transports. The direct one sends the key from the browser, which' },
]);
</script>The block takes tokens as they arrive. isStreaming: true marks it open-ended; the client
settles it with isStreaming: false when the reasoning ends.
Opened on purpose
Section titled “Opened on purpose”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
document.querySelector('aparte-chat-bubble').setSegments([
{
id: 's1',
type: 'thinking',
isStreaming: false,
// Absent means folded. false opens it, for a reply whose reasoning is the point.
collapsed: false,
content: 'Three facts decide it: the key must not reach the browser in production, the demo has no server, and both transports stream the same events.',
},
{ id: 's2', type: 'text', content: 'Direct for the demo, backend for production.' },
]);
</script>Leave collapsed out and the block is closed. Set collapsed: false for a reply whose
reasoning is the point, such as a worked answer.
Customize
Section titled “Customize”The label reads “Thinking” until you set label on the segment. The library measures how long
the block streamed but writes no duration of its own, because products phrase it differently
(“Thought for 8s”, “8.2s · 1.2k tokens”). Write yours by wrapping the built-in renderer:
import { getSegmentRenderer, registerSegmentRenderer, isSegmentSettled, segmentDuration } from '@aparte/core';
import type { AparteThinkingSegment } from '@aparte/core';
const builtIn = getSegmentRenderer('thinking')!;
function writeDuration(el: HTMLElement, segment: AparteThinkingSegment): void {
const ms = isSegmentSettled(segment) ? segmentDuration(segment) : undefined;
const label = el.querySelector('.aparte-thinking-label');
if (label && ms !== undefined) label.textContent = `Thought for ${(ms / 1000).toFixed(1)}s`;
}
registerSegmentRenderer<AparteThinkingSegment>({
type: 'thinking',
render(segment) {
// The built-in returns an HTML string; keep its root element as the segment's root.
const out = builtIn.render(segment);
const template = document.createElement('template');
if (typeof out === 'string') template.innerHTML = out;
const el = typeof out === 'string' ? (template.content.firstElementChild as HTMLElement) : out;
writeDuration(el, segment);
return el;
},
setup: (el, segment) => builtIn.setup?.(el, segment),
update(el, segment) {
builtIn.update?.(el, segment);
writeDuration(el, segment); // a block settles through update, never a second render
},
});Keep the root the built-in returns, and delegate setup and update: they carry the
incremental Markdown writer and the scroll anchoring.
Fields
Section titled “Fields”AparteThinkingSegment, discriminated by type: 'thinking'.
| Field | Type | Required | Description |
|---|---|---|---|
content | string | yes | |
collapsed | boolean | — | Open the block. Absent means CLOSED — a reasoning block is a disclosure, and the reader opens it. More |
label | string | — |
collapsed — Open the block. Absent means CLOSED — a reasoning block is a disclosure,
and the reader opens it.
It used to be the other way round: absent meant open, and core’s own parser
emitted collapsed: false on every block it produced, so a reasoning block
stayed unfolded for the whole conversation and buried the answer under it. No
assistant on the market does that — the content is behind a click, streaming
or settled.
false is still how you open one on purpose. Only ABSENT changed meaning.
Accessibility
Section titled “Accessibility”- The block is a native
<details>: its summary opens with Enter or Space and announces whether it is expanded. - The opened panel is a focusable region named by the block’s label, so a screen reader can find the reasoning and step into it.
Related
Section titled “Related”- Tool call — the other segment a reasoning model often emits.
- Markdown and code — how the reasoning’s text is rendered.