Skip to content

feat: stream cancellation with stop generating button - #13

Merged
quiet-node merged 8 commits into
mainfrom
feat/stream-cancellation
Apr 1, 2026
Merged

feat: stream cancellation with stop generating button#13
quiet-node merged 8 commits into
mainfrom
feat/stream-cancellation

Conversation

@quiet-node

Copy link
Copy Markdown
Owner

Summary

  • Add CancellationToken-based stream cancellation so users can stop Ollama inference mid-generation — dropping the HTTP connection signals Ollama to halt via Go's context.Context propagation
  • New cancel_generation Tauri command with GenerationState managed state; stream_ollama refactored to use biased tokio::select! for instant, deterministic cancellation
  • Stop button replaces the submit button during generation with an animated SVG border-trace effect that follows the rounded-rect path; partial content is preserved as a complete assistant message on cancel

Test plan

  • 145 frontend tests passing, 100% coverage
  • 50 backend tests passing (includes cancellation mid-stream, pre-cancelled token, GenerationState unit tests)
  • bun run validate-build — lint, format, typecheck, clippy all clean
  • Manual: start a long generation, click stop — verify Ollama stops and partial text is preserved
  • Manual: verify stop button border-trace animation is smooth
  • Manual: hide overlay during generation, reopen — verify result arrives normally

🤖 Generated with Claude Code

quiet-node and others added 8 commits April 1, 2026 15:23
Implement CancellationToken-based stream cancellation so users can stop
Ollama inference mid-generation. Dropping the HTTP connection signals
Ollama to halt via Go's context.Context propagation.

- Add tokio-util CancellationToken in GenerationState (Tauri managed state)
- Refactor stream_ollama to use tokio::select! for instant cancellation
- New cancel_generation Tauri command + StreamChunk::Cancelled variant
- Stop button replaces spinner in AskBarView during generation
- Partial content preserved as assistant message on cancel
- 100% test coverage maintained (144 frontend + 50 backend tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rounded-square stop button now has a CSS spinning border ring during
active generation, providing clear visual feedback that inference is
in progress and can be interrupted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace broken conic-gradient and miscalculated dash-offset approach
with pathLength="100" on SVG rects, making all dash math work in clean
percentages. Three layered strokes (head/mid/tail) at staggered offsets
with decreasing opacity create a seamless comet-tail that follows the
actual rounded-rect border path with no stutter at the wrap point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The three trace layers had different initial stroke-dashoffset values,
causing each to animate a different distance per cycle and drift out of
sync. Replacing with animation-delay ensures all layers share the same
keyframe distance (0 to -100) and move in lockstep, with the delay
creating a fixed spatial offset for the comet-tail effect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `biased;` to tokio::select! so the cancellation branch is always
polled first when both futures are ready. Without this, a pre-cancelled
token could non-deterministically lose to an immediately-ready stream
chunk, making the pre_cancelled test flaky.

Update CLAUDE.md architecture to document the new Cancelled variant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
The manual Default impl was uncovered by cargo-llvm-cov since no code
path called it. Replace with #[derive(Default)] so the generated impl
doesn't appear in coverage instrumentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Revert new() from delegating to Self::default() — the derived Default
impl is still instrumented by cargo-llvm-cov and shows as an uncovered
line. Direct construction ensures new() (which is tested) covers its
own body without depending on the derived impl.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
The cancellation test spawned a TCP server with a 10-second sleep to
hold the connection open. When the test cancelled after 100ms, the
tokio runtime aborted the spawned task — its closure never ran to
completion, leaving the implicit return at the closing }); uncovered.

Replace the long sleep with a Notify that the test signals after
assertions, so the spawned task exits cleanly and all regions are
instrumented by cargo-llvm-cov.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node
quiet-node merged commit 0eb4318 into main Apr 1, 2026
3 checks passed
@quiet-node
quiet-node deleted the feat/stream-cancellation branch April 1, 2026 22:12
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: add stream cancellation with stop generating button

Implement CancellationToken-based stream cancellation so users can stop
Ollama inference mid-generation. Dropping the HTTP connection signals
Ollama to halt via Go's context.Context propagation.

- Add tokio-util CancellationToken in GenerationState (Tauri managed state)
- Refactor stream_ollama to use tokio::select! for instant cancellation
- New cancel_generation Tauri command + StreamChunk::Cancelled variant
- Stop button replaces spinner in AskBarView during generation
- Partial content preserved as assistant message on cancel
- 100% test coverage maintained (144 frontend + 50 backend tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add spinning border ring animation to stop generating button

Rounded-square stop button now has a CSS spinning border ring during
active generation, providing clear visual feedback that inference is
in progress and can be interrupted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: smooth border-trace animation with pathLength normalization

Replace broken conic-gradient and miscalculated dash-offset approach
with pathLength="100" on SVG rects, making all dash math work in clean
percentages. Three layered strokes (head/mid/tail) at staggered offsets
with decreasing opacity create a seamless comet-tail that follows the
actual rounded-rect border path with no stutter at the wrap point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use animation-delay for border trace stagger instead of dashoffset

The three trace layers had different initial stroke-dashoffset values,
causing each to animate a different distance per cycle and drift out of
sync. Replacing with animation-delay ensures all layers share the same
keyframe distance (0 to -100) and move in lockstep, with the delay
creating a fixed spatial offset for the comet-tail effect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add biased select for cancellation priority, update CLAUDE.md

Add `biased;` to tokio::select! so the cancellation branch is always
polled first when both futures are ready. Without this, a pre-cancelled
token could non-deterministically lose to an immediately-ready stream
chunk, making the pre_cancelled test flaky.

Update CLAUDE.md architecture to document the new Cancelled variant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: derive Default for GenerationState to satisfy CI coverage

The manual Default impl was uncovered by cargo-llvm-cov since no code
path called it. Replace with #[derive(Default)] so the generated impl
doesn't appear in coverage instrumentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: use direct construction in GenerationState::new()

Revert new() from delegating to Self::default() — the derived Default
impl is still instrumented by cargo-llvm-cov and shows as an uncovered
line. Direct construction ensures new() (which is tested) covers its
own body without depending on the derived impl.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: ensure spawned server task exits cleanly for coverage

The cancellation test spawned a TCP server with a 10-second sleep to
hold the connection open. When the test cancelled after 100ms, the
tokio runtime aborted the spawned task — its closure never ran to
completion, leaving the implicit return at the closing }); uncovered.

Replace the long sleep with a Notify that the test signals after
assertions, so the spawned task exits cleanly and all regions are
instrumented by cargo-llvm-cov.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: add stream cancellation with stop generating button

Implement CancellationToken-based stream cancellation so users can stop
Ollama inference mid-generation. Dropping the HTTP connection signals
Ollama to halt via Go's context.Context propagation.

- Add tokio-util CancellationToken in GenerationState (Tauri managed state)
- Refactor stream_ollama to use tokio::select! for instant cancellation
- New cancel_generation Tauri command + StreamChunk::Cancelled variant
- Stop button replaces spinner in AskBarView during generation
- Partial content preserved as assistant message on cancel
- 100% test coverage maintained (144 frontend + 50 backend tests)


* feat: add spinning border ring animation to stop generating button

Rounded-square stop button now has a CSS spinning border ring during
active generation, providing clear visual feedback that inference is
in progress and can be interrupted.


* fix: smooth border-trace animation with pathLength normalization

Replace broken conic-gradient and miscalculated dash-offset approach
with pathLength="100" on SVG rects, making all dash math work in clean
percentages. Three layered strokes (head/mid/tail) at staggered offsets
with decreasing opacity create a seamless comet-tail that follows the
actual rounded-rect border path with no stutter at the wrap point.


* fix: use animation-delay for border trace stagger instead of dashoffset

The three trace layers had different initial stroke-dashoffset values,
causing each to animate a different distance per cycle and drift out of
sync. Replacing with animation-delay ensures all layers share the same
keyframe distance (0 to -100) and move in lockstep, with the delay
creating a fixed spatial offset for the comet-tail effect.


* fix: add biased select for cancellation priority, update CLAUDE.md

Add `biased;` to tokio::select! so the cancellation branch is always
polled first when both futures are ready. Without this, a pre-cancelled
token could non-deterministically lose to an immediately-ready stream
chunk, making the pre_cancelled test flaky.

Update CLAUDE.md architecture to document the new Cancelled variant.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: derive Default for GenerationState to satisfy CI coverage

The manual Default impl was uncovered by cargo-llvm-cov since no code
path called it. Replace with #[derive(Default)] so the generated impl
doesn't appear in coverage instrumentation.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: use direct construction in GenerationState::new()

Revert new() from delegating to Self::default() — the derived Default
impl is still instrumented by cargo-llvm-cov and shows as an uncovered
line. Direct construction ensures new() (which is tested) covers its
own body without depending on the derived impl.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: ensure spawned server task exits cleanly for coverage

The cancellation test spawned a TCP server with a 10-second sleep to
hold the connection open. When the test cancelled after 100ms, the
tokio runtime aborted the spawned task — its closure never ran to
completion, leaving the implicit return at the closing }); uncovered.

Replace the long sleep with a Notify that the test signals after
assertions, so the spawned task exits cleanly and all regions are
instrumented by cargo-llvm-cov.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request Apr 11, 2026
* feat: add stream cancellation with stop generating button

Implement CancellationToken-based stream cancellation so users can stop
Ollama inference mid-generation. Dropping the HTTP connection signals
Ollama to halt via Go's context.Context propagation.

- Add tokio-util CancellationToken in GenerationState (Tauri managed state)
- Refactor stream_ollama to use tokio::select! for instant cancellation
- New cancel_generation Tauri command + StreamChunk::Cancelled variant
- Stop button replaces spinner in AskBarView during generation
- Partial content preserved as assistant message on cancel
- 100% test coverage maintained (144 frontend + 50 backend tests)

* feat: add spinning border ring animation to stop generating button

Rounded-square stop button now has a CSS spinning border ring during
active generation, providing clear visual feedback that inference is
in progress and can be interrupted.

* fix: smooth border-trace animation with pathLength normalization

Replace broken conic-gradient and miscalculated dash-offset approach
with pathLength="100" on SVG rects, making all dash math work in clean
percentages. Three layered strokes (head/mid/tail) at staggered offsets
with decreasing opacity create a seamless comet-tail that follows the
actual rounded-rect border path with no stutter at the wrap point.

* fix: use animation-delay for border trace stagger instead of dashoffset

The three trace layers had different initial stroke-dashoffset values,
causing each to animate a different distance per cycle and drift out of
sync. Replacing with animation-delay ensures all layers share the same
keyframe distance (0 to -100) and move in lockstep, with the delay
creating a fixed spatial offset for the comet-tail effect.

* fix: add biased select for cancellation priority, update CLAUDE.md

Add `biased;` to tokio::select! so the cancellation branch is always
polled first when both futures are ready. Without this, a pre-cancelled
token could non-deterministically lose to an immediately-ready stream
chunk, making the pre_cancelled test flaky.

Update CLAUDE.md architecture to document the new Cancelled variant.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: derive Default for GenerationState to satisfy CI coverage

The manual Default impl was uncovered by cargo-llvm-cov since no code
path called it. Replace with #[derive(Default)] so the generated impl
doesn't appear in coverage instrumentation.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: use direct construction in GenerationState::new()

Revert new() from delegating to Self::default() — the derived Default
impl is still instrumented by cargo-llvm-cov and shows as an uncovered
line. Direct construction ensures new() (which is tested) covers its
own body without depending on the derived impl.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: ensure spawned server task exits cleanly for coverage

The cancellation test spawned a TCP server with a 10-second sleep to
hold the connection open. When the test cancelled after 100ms, the
tokio runtime aborted the spawned task — its closure never ran to
completion, leaving the implicit return at the closing }); uncovered.

Replace the long sleep with a Notify that the test signals after
assertions, so the spawned task exits cleanly and all regions are
instrumented by cargo-llvm-cov.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
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.

1 participant