Since #49 wired the chat parser correctly, a reasoning model's thinking is
stripped from the shown answer unconditionally — including when the user asked
for thinking. With the in-app Thinking toggle ON, the answer area stays blank
while the model reasons and only the final answer ever appears; on a slow
streamed decode that reads as a hang for the whole thinking span.
The reasoning was being parsed and thrown away: shown_text() kept only
common_chat_parse(...).content and dropped .reasoning_content, and no field
downstream carried it. Surface it instead of discarding it:
- TokenMetrics gains `reasoning`, RunResult gains `reasoning_text`; session's
shown_view returns {content, reasoning} from a single parse (the partial
parse already fills reasoning_content incrementally, so it streams).
- The line protocol carries a `reasoning` field on BMOE_PROGRESS and
BMOE_DONE, kept apart from `text` so a UI can render it as a distinct
thinking block rather than inline. Documented in docs/telemetry.md.
The answer in `text`/generated_text is unchanged (reasoning still stripped),
so this is display-only: the byte-identity gates are untouched. The chat-parse
host gate now also asserts a partial parse exposes the reasoning span — the
contract the live thinking block depends on.
The reasoning parser was wired two ways short of ever running, so <think>
markers leaked into the shown answer on Qwen and Gemma:
1. common_chat_templates_apply was called with the default
reasoning_format = NONE, which bakes a content-only grammar that never
strips a reasoning span. AUTO must be set on the templates inputs,
before apply — that is where stripping is decided, not at parse time.
2. parse params were built with the common_chat_parser_params(cp)
convenience constructor, which copies only format/generation_prompt and
leaves the PEG grammar arena empty. common_chat_parse then threw on the
first token and a bare catch returned the raw stream verbatim.
Fix both: set reasoning_format = AUTO on the inputs, and load the serialized
grammar into the arena (extracted into detail::build_parse_params so the seam
is unit-testable without a model). The two swallowing catches now warn once
instead of silently passing the raw stream through — a silent passthrough is
how this stayed invisible.
gpt-oss was unaffected only by accident: --no-think primes the harmony final
channel and returns the raw stream by design, never exercising the parser.
Adds tests/chat_parse_test.cpp: a model-free host gate that drives
build_parse_params against the vendored Qwen3 template, asserts the reasoning
is stripped, and pins the pre-fix wiring (empty arena) as a negative control.
Runs unconditionally in ctest (no gguf needed). Byte-identity gates unchanged.