The thinking segment — a collapsible AI reasoning block
Thinking/reasoning segment - collapsible.
A literal <think>…</think> in a reply is read as reasoning on every path — while
it streams and when the reply is restored from storage — so a message that merely
quotes the tag in its prose becomes this segment rather than text; and reasoning is
deliberately not sent back to the model, so those words leave the wire history with
it. Models whose reasoning is marked differently are handled by the parser’s
thinkingDelimiters.
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.
Example
Section titled “Example”// 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'.
| 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 | — |
Shared by every segment
Section titled “Shared by every segment”From AparteSegmentBase, which is also what a segment type of your own extends.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique segment identifier |
type | string | yes | Segment type discriminator |
isStreaming | boolean | — | Whether segment is currently being streamed |
fallback | string | — | One sentence standing in for the segment when no renderer claims its type — DRAWN for every type, not only for custom. More |
messageId | string | — | Id of the message this segment belongs to. Stamped on insertion. |
index | number | — | Position in the owning message’s segments[], maintained across insertions and removals. |
meta | Record<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 |
Notes on individual fields
Section titled “Notes on individual fields”collapsed
Section titled “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.
fallback
Section titled “fallback”One sentence standing in for the segment when no renderer claims its type —
DRAWN for every type, not only for custom.
It is the answer to a real moment: a conversation replayed in another app, a
client that loads its views lazily, an export. Core draws it as text (no markup)
under .aparte-segment-fallback. Supplying one also silences the “no renderer”
developer warning: an author who wrote a fallback has already said this can happen.
In the HISTORY sent back to the model it stands in for the segment only where core
does not serialise the type itself — custom, and any type core does not know (a
registered block grammar’s, a plugin’s). A text or a code segment contributes
its content and nothing else; thinking and tool_call are left out of the history
on purpose (_segmentsToText in AparteClient states why), and so is error.
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.
Who emits it
Section titled “Who emits it”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.
Who draws it
Section titled “Who draws it”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.