Skip to content

studio/frontend: cancelled message renders only action buttons when generation is stopped mid-thinking #5563

Description

@danielhanchen

Repro

  1. Load any reasoning-capable model (tested with unsloth/Qwen3.5-4B-MTP-GGUF, Think pill default off, so toggle it on).
  2. Send a prompt that triggers a longer thinking phase, e.g. Write a long detailed essay (1500 words) on operating systems. Reply with the essay only, no <think>.
  3. While the message still shows Generating... (before any visible answer tokens arrive), click the Stop button.

Observed

The assistant bubble is replaced by a floating row of 4 action buttons (Copy / Refresh / Delete / More) and nothing else. No message body, no "Generation stopped" indicator, no thinking-time chip. The composer is correctly re-enabled and the send button reappears, but the visible state suggests the message itself disappeared.

A user who is not looking at the network tab would reasonably ask: "did my stop click do anything?", "where is the partial answer?", or "is the chat broken?". This is the exact failure mode that produces the original "tools work, but I don't see anything happening or it looks stuck" complaint.

Why this matters

Studio's cancel path actually works (verified: /api/inference/load-progress resolves, llama-server stops streaming, the composer re-enables in well under 1s). The bug is purely visual: when there is no rendered content above the action-button row, the row floats with nothing to anchor it.

Suggested fix (frontend, smallest diff)

In studio/frontend/src/components/assistant-ui/thread.tsx (the AssistantMessage component, around the <MessagePrimitive.Root> near line 999 / 1166), gate the action-button row on the presence of message content OR show a placeholder when both content.length === 0 and status?.type === "incomplete" with reason: "cancelled". Two reasonable placeholders:

  1. <span className="text-muted-foreground italic">Generation stopped</span> (short, terminal-state-aware)
  2. Surface the partial reasoning panel even if its content was empty - Thought for {seconds}s reads better than blank.

Either is a one-component change. Both have precedent in the codebase: the thinking panel already collapses to "Thought for N seconds" once streaming stops, and the tool-fallback component already renders a "Cancelled tool" label when status.reason is "cancelled".

Screenshots

Pre-cancel: model is in thinking phase, stop button visible bottom right, only "Generating..." in middle of empty assistant area.

Post-cancel (i=05, ~3s after stop click): user prompt at top, 4 action buttons floating mid-screen, composer enabled at bottom. No message body. No "Generation stopped" label.

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions