feat(ui): cancel a running command on Ctrl-C#17
Merged
Conversation
A Ctrl-C while a model-invoked run_command child is still running now kills that child immediately, instead of waiting out its (default 600s) timeout. Branch 6 already cancelled the model stream; this threads the same per-turn cancel event down to the command. The cancel event flows through ToolContext -> ToolExecutor -> run_command_process, whose wait loop polls it on a 0.1s interval and killpg's the child's process group the instant the event is set. The worker owns its own Popen, so it kills its own child -- no cross-thread registry; the Event stays the only cross-thread signal. The tool loop then raises GenerationCancelled (keyed off the event, immediately after executor.execute), routing through the same abort_turn path as the model-stream cancel. Before raising, it rolls the cancelled model step out of history so no orphaned tool_call -- an assistant tool_call with no matching result -- is re-sent on the next turn, matching the model-stream cancel's clean discard. Prior completed steps stay; partial command output already streamed to the pane stays visible. The cancel=None legacy path (the default REPL executor) is behaviorally equivalent to before -- the poll loop merely chunks the wait.
lavindeep
added a commit
that referenced
this pull request
Jul 2, 2026
feat(ui): cancel a running command on Ctrl-C
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.
A Ctrl-C while a model-invoked
run_commandchild is still running now kills that child immediately, instead of waiting out its (default 600s) timeout. Branch 6 already cancelled the model stream; this threads the same per-turn cancel event down to the command (DESIGN §31.15).What it does
cancel: threading.EventflowsToolContext→ToolExecutor→run_command_process, whose wait loop polls it on a0.1sinterval andkillpgs the child's process group the instant it's set. The worker owns its ownPopen, so it kills its own child — no cross-thread registry; theEventstays the only cross-thread signal;request_cancelis unchanged.GenerationCancelled(keyed off the event, immediately afterexecutor.execute), routing through the sameabort_turn(⏹ aborted) path as the model-stream cancel.del self._history[history_before_reply:]), so an assistanttool_callwith no matching result is never re-sent on the next turn — matching the model-stream cancel's clean discard. Prior completed steps stay; partial command output already streamed to the pane stays visible.cancel=None(the default REPL executor) is behaviorally equivalent to before — the poll loop merely chunks the wait; a runaway child is still killed at the timeout deadline.Verification
Phase gate (green):
New tests (red-first):
test_cancel_kills_process_group_fast(realsleep 30child → cancel mid-run → killed fast, process group dead viakillpg(pgid,0)→ProcessLookupError, reader thread joined),test_cancel_none_completes_normally,test_cancel_during_tool_execution_aborts(loop raises, history rolls back to just the user message — no orphaned tool_call),test_request_cancel_kills_running_command_and_aborts(TurnRunner →abort_turn, isolated from the model-stream backstop via the history-rollback assertion).Live validation —
gemma4:31b-cloud, full-screen app (SHELLPILOT_UI=app): model calledrun_command(sleep 60)→ approved → child running (real pid) → Ctrl-C → child gone in 0.046s (not the 60s timeout) →⏹ abortedrendered, indicator cleared, no orphaned child process, app responsive after (/statusworks)./statusshowed 2 messages in context = the two user messages only, no orphanedassistanttool_call — live confirmation of the history-rollback.Targets
ui-rework. Does not touchmain.