fix(webui): keep late reasoning attached above the answer
Some providers only surface structured `reasoning_content` after answer text has already streamed. The WebUI was treating those late `reasoning_delta` frames as a fresh assistant placeholder, so the Thinking bubble rendered below the already-visible answer. Attach late reasoning back to the active assistant turn instead. The bubble still renders above the message content, preserving the expected Thinking -> answer order even when the provider protocol delivers the reasoning post-hoc. Added a regression test for answer-first followed by reasoning_delta/reasoning_end. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,17 +21,23 @@ interface StreamBuffer {
|
||||
/**
|
||||
* Append a reasoning chunk to the last open reasoning stream in ``prev``.
|
||||
*
|
||||
* Lookup rule: find the most recent assistant turn that is either still
|
||||
* streaming reasoning (``reasoningStreaming``) or has no answer text yet.
|
||||
* Anything else starts a fresh streaming placeholder so a new turn's
|
||||
* reasoning never bleeds into the previous answer.
|
||||
* Lookup rule: prefer the most recent assistant turn in the active UI tail.
|
||||
* Most providers emit reasoning before answer text, but some only expose
|
||||
* ``reasoning_content`` after the answer stream completes. In that post-hoc
|
||||
* case the reasoning still belongs to the same assistant turn and must render
|
||||
* above the answer, not as a new row below it.
|
||||
*/
|
||||
function attachReasoningChunk(prev: UIMessage[], chunk: string): UIMessage[] {
|
||||
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
||||
const candidate = prev[i];
|
||||
if (candidate.role !== "assistant" || candidate.kind === "trace") continue;
|
||||
const hasAnswer = candidate.content.length > 0;
|
||||
if (candidate.reasoningStreaming || (!hasAnswer && candidate.reasoning !== undefined)) {
|
||||
if (
|
||||
candidate.reasoningStreaming
|
||||
|| candidate.reasoning !== undefined
|
||||
|| hasAnswer
|
||||
|| candidate.isStreaming
|
||||
) {
|
||||
const merged: UIMessage = {
|
||||
...candidate,
|
||||
reasoning: (candidate.reasoning ?? "") + chunk,
|
||||
|
||||
Reference in New Issue
Block a user