Skip to content

chore: resolve issue #147 — frontend Stop button for streaming chat - #149

Merged
matthewod11-stack merged 3 commits into
mainfrom
chore/orchestrator-issue-147-2026-07-12
Jul 15, 2026
Merged

chore: resolve issue #147 — frontend Stop button for streaming chat#149
matthewod11-stack merged 3 commits into
mainfrom
chore/orchestrator-issue-147-2026-07-12

Conversation

@matthewod11-stack

Copy link
Copy Markdown
Owner

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-cancelled event), but the frontend never wired it: no Stop button, and sendChatMessageStreaming never forwarded a stream_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.tssendChatMessageStreaming forwards a client-generated streamId; new cancelStream() wraps the existing cancel_stream command.
  • src/contexts/ConversationContext.tsx — generate a per-send stream id; listen for chat-stream-cancelled to reset streaming UI (the backend emits this, not a done, on cancel); expose stopStreaming(); 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 props isStreaming? / onStop? are optional → the other consumer (RecruitingView) is unaffected.
  • src/App.tsx — wire stopStreaming + isStreaming into the chat ChatInput.
  • src/lib/chat-input-state.ts + .test.ts — extract the send/stop decision into a pure resolveChatInputMode() with unit tests (per the issue's "extract-and-move" hint).

do-not-touch honored

src-tauri/ is untouched — chat.rs was read only to confirm the backend already accepts stream_id and emits chat-stream-cancelled.

⚠️ Scope note for the reviewer

The issue's scope: listed the context, commands wrapper, and chat/ 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 in do-not-touch. Flagging explicitly.

Verification

  • npx tsc --noEmit → clean (exit 0)
  • npm test (vitest) → 63/63 passing (floor was 56; +7 new)
  • Backend unchanged → its 799 Rust tests are unaffected (not re-run).
  • ⚠️ Manual QA not run (unattended cron): the issue's acceptance includes "start a long response, hit Stop → stream halts, UI resets, audit_log gains a cancelled row." Please exercise this manually before merge.

Opened by an unattended agent — review before merging; not auto-merged.

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>

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

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 streamId to streaming sends and expose a cancelStream() wrapper for the existing Tauri cancel_stream command.
  • Track the active stream id in ConversationContext, add stopStreaming(), and reset UI state on the backend’s chat-stream-cancelled event (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,
matthewod11-stack and others added 2 commits July 15, 2026 08:36
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>
@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

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.

@matthewod11-stack
matthewod11-stack merged commit 4033982 into main Jul 15, 2026
7 checks passed
@matthewod11-stack
matthewod11-stack deleted the chore/orchestrator-issue-147-2026-07-12 branch July 15, 2026 15:44
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.

No Stop button: #25's stream-cancel backend has no frontend wire-up — users can't stop a streaming response

2 participants