Skip to content

The thinking segment — a collapsible AI reasoning block

Thinking/reasoning segment - collapsible

A segment is data, not an element — it has no tag and dispatches nothing. This one is AparteThinkingSegment, and it is the object you push into a bubble; something else draws it.

Width Open in a tab
Core's own renderer, drawing the example printed below inside a real viewport. A segment is data — this is what the library makes of it, and it looks the same in every framework.
// No `collapsed` key, so it renders CLOSED — see the field's own note below.
{
id: 's1',
type: 'thinking',
label: 'Reasoning',
content: 'The question is about transports, so name the two and say where the key lives.',
}

Discriminated by type: 'thinking'.

FieldTypeRequiredDescription
contentstringyes
collapsedbooleanOpen the block. Absent means CLOSED — a reasoning block is a disclosure, and the reader opens it. More
labelstring

From AparteSegmentBase, which is also what a segment type of your own extends.

FieldTypeRequiredDescription
idstringyesUnique segment identifier
typestringyesSegment type discriminator
isStreamingbooleanWhether segment is currently being streamed
messageIdstringId of the message this segment belongs to. Stamped on insertion.
indexnumberPosition in the owning message’s segments[], maintained across insertions and removals.
metaRecord<string, unknown> & { aparte?: AparteSegmentTiming }Extras the producer of the segment knows — token counts, cost, compute device — plus aparte, the one sub-object core writes. More

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.

Extras the producer of the segment knows — token counts, cost, compute device — plus aparte, the one sub-object core writes.

Everything except meta.aparte is yours: fill it with updateSegment(segmentId, { meta }), which MERGES rather than replaces. Mirrors AparteMessage.metadata.

An app pushes one with addSegment() on the viewport, or a parser produces it while a reply streams. Core stamps its identity and timing on insert, so an emitted segment does not have to carry them.

A built-in renderer draws this one, from packages/core/src/renderers/segments/thinking.ts. It installs itself the first time a segment of this type needs it, so a chat renders it with no setup.

Replace it with your own for this config only:

import { registerSegmentRenderer } from '@aparte/core';
import type { AparteThinkingSegment } from '@aparte/core';
registerSegmentRenderer({
type: 'thinking',
render: (segment: AparteThinkingSegment) => {
const el = document.createElement('div');
el.textContent = JSON.stringify(segment);
return el;
},
});

See Customization for the whole renderer seam, and bring your own loop for emitting segments yourself.