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.
| Field | Value |
|---|---|
| Severity | SEV-2 (user can’t stop a runaway Sidekick stream) |
| Category | Sidekick / UX |
| Date | 2026-05-04 |
| Status | Archived |
Symptom
Section titled “Symptom”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.
Root cause (three overlapping)
Section titled “Root cause (three overlapping)”stopStreamingonly calledabortControllerRef.current?.abort(). TheAbortErrorcatch path insendStreamingearly-returned BEFORE finalizing messageisStreaming: false, so the streaming bubble kept rendering.- The stream-resumption polling loop ran on a separate
setInterval.abort()did nothing to it; the loop kept fetching/api/ai/conversations/:idevery 2 seconds and replacing messages with the latest DB content. - The polling start path never set
isStreamActive = true, sochat.isSendingwas false during polled resumption — input rendered the send arrow instead of Stop square. Nothing to click.
In stopStreaming:
abort()(existing)clearInterval(streamPollIntervalRef.current)+ null the ref- Cancel 429 retry timer
setMessages(prev => prev.map(m => m.isStreaming ? { ...m, isStreaming: false, streamingPhase: null } : m))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.
Prevention
Section titled “Prevention”- 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
stopStreamingand MUST setisStreamActive. - Never early-return in an AbortError catch without finalizing the visible UI state.