Skip to content

FM-043 — Sidekick Stop Button Doesn't Halt the Visible Stream

Stop button aborted fetch but UI kept streaming — three overlapping issues in use-sidekick-chat.ts.

FieldValue
SeveritySEV-2 (user can’t stop a runaway Sidekick stream)
CategorySidekick / UX
Date2026-05-04
StatusArchived

User clicks the square Stop button on a Sidekick chat. The fetch is aborted at the network level, but the chat UI continues to show streaming indicators and tokens keep appearing. In Smart Brief specifically, the Stop button isn’t even visible while a stream-resumption is emitting tokens.

  1. stopStreaming only called abortControllerRef.current?.abort(). The AbortError catch path in sendStreaming early-returned BEFORE finalizing message isStreaming: false, so the streaming bubble kept rendering.
  2. The stream-resumption polling loop ran on a separate setInterval. abort() did nothing to it; the loop kept fetching /api/ai/conversations/:id every 2 seconds and replacing messages with the latest DB content.
  3. The polling start path never set isStreamActive = true, so chat.isSending was false during polled resumption — input rendered the send arrow instead of Stop square. Nothing to click.

In stopStreaming:

  1. abort() (existing)
  2. clearInterval(streamPollIntervalRef.current) + null the ref
  3. Cancel 429 retry timer
  4. setMessages(prev => prev.map(m => m.isStreaming ? { ...m, isStreaming: false, streamingPhase: null } : m))
  5. isStreamingRef.current = false; setIsStreamActive(false); markStreamingFinished(convId)

In polling resumption path: set isStreamingRef = true; setIsStreamActive(true); markStreamingStarted when polling begins. Mirror in every exit branch.

  • Stop must be a full halt, not just a fetch abort. Any future “stream from a different source” (poll, websocket, server-sent re-attach) MUST be cancelled by stopStreaming and MUST set isStreamActive.
  • Never early-return in an AbortError catch without finalizing the visible UI state.