fix(chat): render completed reasoning in full instead of simulating streaming

Reasoning streaming state now derives only from the live stream phase
(streaming/cooldown), never from missing persisted timing data. A cached
part without time.end is no longer treated as live just because the
timing field is absent, so completed reasoning renders in full on load
instead of replaying a fake stream.

While resolving the merge onto main, also fixed a regression the merge
uncovered: main had added block-level streaming reveal (commitStreamedText)
to ReasoningPart since this fix was authored, which caused the busy
"Thinking…" header to stay hidden for the first moments of a short,
single-paragraph streaming response (no committed line yet). The busy
header now mounts as soon as streaming starts, independent of whether
any text has been committed for display.

Closes #2020
This commit is contained in:
Bohdan Triapitsyn
2026-08-28 23:46:14 +03:00
parent 14ca125b5d
commit e64fc710fc
@@ -261,7 +261,11 @@ export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
};
}, []);
if (!text || text.trim().length === 0) {
// While genuinely streaming, the busy header must appear as soon as
// reasoning starts even before the block-level reveal (commitStreamedText)
// has committed a first complete line — otherwise "Thinking…" never shows
// for the first moments of a short, single-paragraph response.
if (!isStreaming && (!text || text.trim().length === 0)) {
return null;
}
@@ -445,9 +449,11 @@ const ReasoningPart = React.memo(({
// never mutates in place.
const throttledText = isStreaming ? commitStreamedText(throttledTextRaw) : throttledTextRaw;
// Show reasoning even if time.end isn't set yet (during streaming)
// Only hide if there's no text content
if (!throttledText || throttledText.trim().length === 0) {
// Show reasoning even if time.end isn't set yet (during streaming).
// While genuinely streaming, keep the block mounted even before the
// block-level reveal commits a first line, so the busy header appears
// immediately instead of waiting on committed text.
if (!isStreaming && (!throttledText || throttledText.trim().length === 0)) {
return null;
}