chore: resolve issue #147 — frontend Stop button for streaming chat - #149
Merged
Merged
Conversation
Issue #25 shipped the full backend cancel apparatus (StreamRegistry, cancel_stream command, chat-stream-cancelled event) but the frontend never wired it: no Stop button, and sendChatMessageStreaming never passed a stream_id, so the UI could never learn the id needed to cancel. Users could not stop a streaming response and abandoned streams billed to completion. - tauri-commands: sendChatMessageStreaming now forwards a client-generated streamId; add cancelStream() wrapping the existing cancel_stream command. - ConversationContext: generate a per-send stream id, listen for chat-stream-cancelled to reset streaming UI (backend emits this, not `done`, on cancel), expose stopStreaming(), and cancel on conversation switch/unmount so abandoned streams stop billing. - ChatInput: render a Stop button while streaming (optional isStreaming/onStop props — backward compatible; RecruitingView consumer unchanged). - App: wire stopStreaming + isStreaming into the chat ChatInput. - Extract the send/stop decision into a pure resolveChatInputMode() with unit tests (vitest 56 -> 63). Frontend-only; src-tauri/ untouched (honored do-not-touch). Verified: tsc --noEmit clean, vitest 63/63 green. Note: scope extended by one file beyond the issue's declared scope (src/App.tsx) — the sole consumer that wires the Stop gesture to the context; not in do-not-touch. Flagged in the PR for reviewer awareness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7 tasks
There was a problem hiding this comment.
Pull request overview
This PR wires the existing Rust streaming-cancellation capability into the frontend so users can stop an in-flight streaming chat response (issue #147), preventing abandoned streams from continuing to bill.
Changes:
- Add a client-generated
streamIdto streaming sends and expose acancelStream()wrapper for the existing Tauricancel_streamcommand. - Track the active stream id in
ConversationContext, addstopStreaming(), and reset UI state on the backend’schat-stream-cancelledevent (including cancelling on conversation switch/unmount). - Add a Stop button in the chat input while streaming, and extract the send/stop decision into a unit-tested pure function.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/lib/tauri-commands.ts | Forwards client streamId to send_chat_message_streaming; adds cancelStream() wrapper. |
| src/contexts/ConversationContext.tsx | Generates/tracks active stream id, listens for cancel event, exposes stopStreaming(), cancels on switch/unmount. |
| src/components/chat/ChatInput.tsx | Renders Stop button while streaming; delegates mode decision to extracted pure function. |
| src/App.tsx | Wires isStreaming and onStop into ChatInput from conversation context state/actions. |
| src/lib/chat-input-state.ts | New pure resolveChatInputMode() helper for send/stop button state. |
| src/lib/chat-input-state.test.ts | Unit tests covering the extracted button-mode decision logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
543
to
547
| await sendChatMessageStreaming( | ||
| apiMessages, | ||
| promptResult.system_prompt, | ||
| promptResult.aggregates, | ||
| promptResult.query_type, |
…sue-147-2026-07-12
The backend rejects the streaming invoke with ChatError::Cancelled in addition to emitting chat-stream-cancelled. The catch block routed that rejection through categorizeError -> setMessageError, so hitting Stop decorated the partial message with a generic error + retry chip. Add isCancelledError (pure, tested) and a quiet-finalize branch in the catch, idempotent with the cancelled-event handler regardless of arrival order. Also merges current main (People Map T9 + #152 + #153) into the branch; integrated suite: 89 FE tests, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Auto-generated by the portfolio-orchestrator nightly run on 2026-07-12 (live mode). Resolves #147.
What & why
#25 shipped the full backend cancel apparatus (
StreamRegistry,cancel_stream,chat-stream-cancelledevent), but the frontend never wired it: no Stop button, andsendChatMessageStreamingnever forwarded astream_id, so the UI could never learn the id required to cancel. Users couldn't stop a streaming response and abandoned streams billed to completion.Changes (6 files, frontend-only)
src/lib/tauri-commands.ts—sendChatMessageStreamingforwards a client-generatedstreamId; newcancelStream()wraps the existingcancel_streamcommand.src/contexts/ConversationContext.tsx— generate a per-send stream id; listen forchat-stream-cancelledto reset streaming UI (the backend emits this, not adone, on cancel); exposestopStreaming(); cancel on conversation switch (loadConversation,startNewConversation) and on provider unmount so abandoned streams stop billing.src/components/chat/ChatInput.tsx— render a Stop button while streaming. New propsisStreaming?/onStop?are optional → the other consumer (RecruitingView) is unaffected.src/App.tsx— wirestopStreaming+isStreaminginto the chatChatInput.src/lib/chat-input-state.ts+.test.ts— extract the send/stop decision into a pureresolveChatInputMode()with unit tests (per the issue's "extract-and-move" hint).do-not-touchhonoredsrc-tauri/is untouched —chat.rswas read only to confirm the backend already acceptsstream_idand emitschat-stream-cancelled.The issue's
scope:listed the context, commands wrapper, andchat/components. I extended to one additional file,src/App.tsx— the sole consumer that wires the Stop gesture to the context (a Stop button wired to nothing isn't a working fix). It is not indo-not-touch. Flagging explicitly.Verification
npx tsc --noEmit→ clean (exit 0)npm test(vitest) → 63/63 passing (floor was 56; +7 new)audit_loggains acancelledrow." Please exercise this manually before merge.Opened by an unattended agent — review before merging; not auto-merged.