Skip to content

fix(THU-791): stop button not working properly - #1230

Open
arienemaiara wants to merge 7 commits into
mainfrom
THU-791/stop-button-isnt-working
Open

fix(THU-791): stop button not working properly#1230
arienemaiara wants to merge 7 commits into
mainfrom
THU-791/stop-button-isnt-working

Conversation

@arienemaiara

@arienemaiara arienemaiara commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Stop button did nothing while the model was thinking or auto-retrying — it only worked once tokens were already streaming. Stop now ends the turn from any state and settles the thread back to idle.

Changes

  • Show Stop whenever the thread is busy — thinking (submitted), streaming, or empty-turn/auto-retry recovery — via a shared getTurnActivity signal used by both the composer and the message list, so the button and the loading spinner can't disagree.
  • stop cancels any pending auto-retry and drops a trailing empty assistant turn, so the recovery spinner can't restart the turn.
  • On abort, onFinish settles the turn in one of three ways: drop an empty loader shell, finalize a mid-reasoning turn so its spinner stops, or keep a streamed partial answer.
  • Collapse the blank space left below "Thought for …" after stopping a reasoning-only turn.

Evidences

Simulator.Screen.Recording.-.iPhone.17.-.2026-08-19.at.10.39.26.mov

@github-actions

Copy link
Copy Markdown

Semgrep Security Scan

No security issues found.

@arienemaiara arienemaiara changed the title fix(THU-791): make the Stop button reliably end a turn fix(THU-791): stop button not working properly Aug 19, 2026
@arienemaiara
arienemaiara requested a lite review from Copilot August 19, 2026 13:45
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Preview environment deployed 🚀

Service URL
Marketing / blog / docs https://thunderbolt-pr-1230.preview.thunderbolt.io
App https://app-pr-1230.preview.thunderbolt.io
API https://api-pr-1230.preview.thunderbolt.io
Keycloak https://auth-pr-1230.preview.thunderbolt.io
PowerSync https://powersync-pr-1230.preview.thunderbolt.io

Stack: preview-pr-1230 · Commit: 925e844f14c32a091b5e16e376f8454ddbc82405

Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — demo@thunderbolt.io / demo by default.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

PR Metrics

Metric Value
Lines changed (prod code) +343 / -54
JS bundle size (gzipped) 🟢 640.0 KB → 640.4 KB (+420 B, +0.1%)
Test coverage 🟢 82.15% → 82.17% (+0.0%)
Performance (preview) Preview not ready — Render deploy may have timed out
Accessibility
Best Practices
SEO

Updated Fri, 04 Sep 2026 18:21:35 GMT · run #2925

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes THU-791 by unifying “turn activity” detection across the composer and message list so the Stop button and loading spinner stay in sync, and by improving abort/stop settling behavior (including empty-turn recovery and reasoning-only layout gaps).

Changes:

  • Introduces a shared getTurnActivity helper used by both ChatPromptInput (Stop button / submit gating) and ChatMessages (spinner / error gating).
  • Improves aborted-turn settling in createChatInstance (drop empty assistant shells; finalize streaming reasoning parts; persist partial streamed answers on abort).
  • Updates reasoning display layout to avoid a persistent blank gap after stopping a reasoning-only turn, and adds targeted unit tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/components/chat/reasoning-display.tsx Collapse reserved min-height once ephemeral reasoning fades to prevent post-stop blank space.
src/components/chat/reasoning-display.test.tsx Tests for height reservation behavior while streaming vs settled.
src/components/chat/chat-prompt-input.tsx Uses shared getTurnActivity to show Stop and gate submits whenever a turn is active.
src/components/chat/chat-prompt-input.test.tsx Adds Stop-button visibility/behavior tests across submitted/streaming/recovery/backoff states.
src/components/chat/chat-messages.tsx Replaces local activity derivation with getTurnActivity to keep spinner logic in sync with composer.
src/chats/turn-activity.ts New pure helper to compute “turn activity” signals from useChat + session retry state.
src/chats/turn-activity.test.ts Unit tests covering the activity matrix (submitted/streaming/empty-turn recovery/backoff/exhausted/error).
src/chats/chat-instance.ts Enhances abort handling and overrides stop() to cancel retries and settle UI state.
src/chats/chat-instance.test.ts Adds regression tests for aborted empty shells, reasoning finalization, and stop canceling retries.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/chats/chat-instance.ts

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 28f2bfea302f · mode: single · deferred 0 item(s) already reported by other bots (best-effort dedup)

Comment thread src/chats/chat-instance.ts Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Reviewed the diff — no issues to report. ✅ Never approves, never requests changes, never gates merge.
head: ace293af6840 · mode: single · deferred 0 item(s) already reported by other bots (best-effort dedup)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/chats/chat-instance.ts:992

  • instance.stop() no longer returns/awaits the underlying Chat.stop() promise in the in-flight case (void originalStop()). This changes the method’s async contract: callers awaiting stop() may proceed before the abort is actually initiated/settled, and any rejection from originalStop() will be swallowed. Prefer return originalStop() (or await originalStop(); return) to preserve behavior and error propagation.
  instance.stop = async function () {
    if (retryTimeout) {
      clearTimeout(retryTimeout)
      retryTimeout = null
    }
    if (instance.status === 'streaming' || instance.status === 'submitted') {
      void originalStop()
      return
    }

src/chats/chat-instance.ts:752

  • In the hasStreamingReasoning abort branch you finalize and persist finalized, but later the turn completion telemetry is emitted with message (the callback param), which may still contain reasoning.state === 'streaming' or differ from finalized. This can lead to telemetry recording inconsistent message state vs what’s displayed/saved. Consider emitting completion with the finalized message for that branch (e.g., pass finalized into emitTurnCompleted) so telemetry reflects the settled message.
        } else if (hasStreamingReasoning(lastMessage)) {
          // Stopped mid-reasoning: the reasoning part is left `streaming`, so its
          // spinner never stops. Finalize it in the live list and the saved copy.
          const finalized = finalizeReasoning(lastMessage!)
          const next = instance.messages.slice()
          next[lastIndex] = finalized
          instance.messages = next
          finishedTurn.telemetry?.startPhase('final_save')
          await saveMessages({ id, messages: [finalized] })
          finishedTurn.telemetry?.endPhase('final_save')
        } else if (message?.parts?.length) {

src/chats/turn-activity.ts:26

  • The doc for pendingEmptyTurnRecovery says "status is back to ready", but the implementation doesn’t check status === 'ready' (it keys off !isStreaming). Either update the doc to match the actual condition, or tighten the logic to require status === 'ready' so callers can trust the meaning of this flag.
  /** The model returned an empty turn and the app is (or should be) recovering
   *  it — the thread shows a spinner, `status` is back to `ready`. */
  pendingEmptyTurnRecovery: boolean

src/components/chat/chat-messages.tsx:70

  • ChatMessages now derives hasError from getTurnActivity, but the completion haptic still uses chatError only. This can mis-signal success for error states that aren’t represented by chatError (e.g., an empty assistant turn with retriesExhausted that surfaces as hasError). Prefer using the derived hasError here so the notification matches the UI’s error state.
  const wasStreaming = useRef(false)
  useEffect(() => {
    if (wasStreaming.current && !isStreaming) {
      triggerNotification(chatError ? 'error' : 'success')
    }
    wasStreaming.current = isStreaming
  }, [isStreaming, chatError, triggerNotification])

@arienemaiara
arienemaiara marked this pull request as draft August 20, 2026 17:06
@raivieiraadriano92
raivieiraadriano92 marked this pull request as ready for review September 4, 2026 16:52
arienemaiara and others added 6 commits September 4, 2026 13:53
- Stopping a turn now transitions the thread back to idle so the
  stop button reflects the actual streaming state.
- Extract turn-activity tracking into its own module with tests.
- Reserve the 200px min-height only while the ephemeral reasoning is
  shown; a stopped reasoning-only turn never gets a text part to
  unmount the display, so an unconditional reserve left a permanent gap.
- Add tests covering both the streaming (reserved) and settled
  (collapsed) states.
- Delegate stop to the SDK while streaming/submitted so onFinish({ isAbort })
  settles on the correct currentTurn instead of it being swapped out early.
- Keep the retry-cancel, empty-shell drop, and idle settle only for the
  backoff/recovery path, which fires no onFinish.
The AI SDK only aborts the request signal — nothing cancels a custom
fetch's body — so the Pi harness kept calling the model and running
tools after Stop. Thread init.signal into the harness stream.
sendAutomaticallyWhen is gated on isError, not isAbort, so a turn stopped
before its assistant message existed left the user message trailing and
the SDK re-sent it. Suppress it until the next explicit send, stop
dropping the empty shell that created that trailing message, and cancel
an open permission dialog.
Tearing a turn down is asynchronous — an ACP session/cancel round-trip,
the harness draining its loop — so the button looked inert after the
press. Track stopping on the session, derive it against the live request
so a stale flag can't strand the composer, and swap in a spinner.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: c4a64a824f07 · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)

Comment thread src/chats/chat-instance.ts
Stopping before the model's first turn began left a zero-part assistant
message that renders an empty-turn recovery spinner nothing recovers.
Safe to drop now that stopRequested gates the auto-send. Also skip
partial saves of a zero-part turn, so the drop survives a reload.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Reviewed the diff — no issues to report. ✅ Never approves, never requests changes, never gates merge.
head: 925e844f14c3 · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants