feat(ui): Ctrl-C cancels a model turn mid-stream#12
Merged
Conversation
In the full-screen app a long turn was a blind wait with no escape. Add a clean turn-abort: Ctrl-C (a key press now — raw mode disables ISIG, so it is not a SIGINT) sets a per-turn cancel Event; the Ollama stream checks it at each read boundary and raises a typed GenerationCancelled, closing the response in-thread via the stream context manager. The exception is neither an OllamaError nor an httpx error, so the think-retry and the transport handler leave it alone and it propagates cleanly. History stays intact: the model call sits in a try/finally that only closes the UI response; the reply is recorded after it, so a cancelled turn skips the record site and never stores a truncated partial — the user message stands, the aborted reply is discarded. The worker tells a cancel apart from a failure: GenerationCancelled routes to a clean abort_turn (clear the thinking indicator, mark the partial '⏹ aborted'), any other exception is a pane error, and the busy flag clears on every path so a cancel never wedges the app. The cancel Event is passed to the worker as a thread argument (a captured local), so the worker never reads the mutable instance attribute and the loop-thread-only invariant holds. cancel defaults to None on every hop, so the default REPL and non-TTY paths are byte-identical. Cancelling a running subprocess (killpg) is a follow-up; today a cancel during a command aborts the turn once the command returns. DESIGN.md §31.15 added.
lavindeep
added a commit
that referenced
this pull request
Jul 2, 2026
feat(ui): Ctrl-C cancels a model turn mid-stream
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.
Branch 6/9 of the UI v2 rework (model-stream cancellation). A long turn was a blind wait with no escape; now Ctrl-C aborts it cleanly.
What
Event.OllamaClient._stream_chatchecks the event at each read boundary and raises a typedGenerationCancelled, closing the response in-thread via thewith self._client.stream(...)context manager (the only cross-thread op isEvent.set()). The exception is neitherOllamaErrornor an httpx error, so the think-retry and transport handler don't swallow it.try/finallythat only closes the UI response;_record(reply)is after it, so a cancelled turn skips the record site — the user message stands, the partial reply is never stored.GenerationCancelled→ cleanabort_turn(clear indicator, mark⏹ aborted); other exceptions → pane error;_mark_doneclears busy on every path (no wedge). The cancel event is passed as a thread argument so the worker never reads the mutableself._cancel(invariant holds literally).Scope
Subprocess
killpgis branch 6b — a cancel during a running command aborts the turn once the command returns. No approval/queue (branches 7/9).Verification
mypy --strict, 1368 passed.canceldefaults toNoneeverywhere → default REPL + non-TTY byte-identical. DESIGN §31.15 in the same commit.⏹ aborted, indicator clears, next turn works.