The list belongs to the answer rather than to the tool line, so it is drawn with
tool calls hidden too. It waits for the answer to cite something before appearing,
because otherwise it arrives in full with the first token and then shrinks as
citations arrive. A copied conversation carries the same list, worked out from a
plain scan: it is markdown on its way somewhere else, where a [1] inside a snippet
counting as a citation costs nothing.
renderModelOutput now takes the turn's sources and reports back which numbers it
linked, so the list has one source of truth for what the answer actually cited.
Maths runs before citations, not after. A link label has to escape its brackets,
and that escaped form is exactly LaTeX's own display delimiter, so converting the
model's maths first is what stops the maths pass from eating a marker and handing
back a bare 1.
runAgent owns the list, so every call in one turn shares its numbering, and each
record carries only the entries its own call added: the turn's whole list is the
flattening of its records. The schema accepts the field as optional, because zod
drops unknown keys and a reopened conversation would otherwise lose every link.
Each result now carries the number the model cites, assigned from the turn's
list rather than from its position in one call, so three searches in a turn no
longer produce three runs of 1. The entries a call added come back beside the
content, for the transcript to store and draw.
One number per page per turn, assigned where the tools run so the model and
the reader agree on what [3] means. The module also owns the marker pass and
the list under the answer, because Vicinae cannot draw a footnote: it parses
markdown with CMARK_OPT_DEFAULT and attaches only the table and strikethrough
extensions, so [^1] arrives as literal text.
A marker stays text unless every number in it is a source with a destination
this window should open, and a footnote definition for a source we already
list is dropped while a markdown reference definition is left alone.