Skip to content

feat(ui): cancel a running command on Ctrl-C#17

Merged
lavindeep merged 1 commit into
ui-reworkfrom
feat/ui-subprocess-cancel
Jun 28, 2026
Merged

feat(ui): cancel a running command on Ctrl-C#17
lavindeep merged 1 commit into
ui-reworkfrom
feat/ui-subprocess-cancel

Conversation

@lavindeep

@lavindeep lavindeep commented Jun 28, 2026

Copy link
Copy Markdown
Owner

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 (DESIGN §31.15).

What it does

  • The per-turn cancel: threading.Event flows ToolContextToolExecutorrun_command_process, whose wait loop polls it on a 0.1s interval and killpgs the child's process group the instant it's 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; request_cancel is unchanged.
  • The tool loop raises GenerationCancelled (keyed off the event, immediately after executor.execute), routing through the same abort_turn (⏹ aborted) path as the model-stream cancel.
  • No orphaned tool_call: before raising, it rolls the cancelled model step out of history (del self._history[history_before_reply:]), so an assistant tool_call with 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):

ruff / ruff format / mypy --strict → all pass
pytest -q → 1451 passed

New tests (red-first): test_cancel_kills_process_group_fast (real sleep 30 child → cancel mid-run → killed fast, process group dead via killpg(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 validationgemma4:31b-cloud, full-screen app (SHELLPILOT_UI=app): model called run_command(sleep 60) → approved → child running (real pid) → Ctrl-C → child gone in 0.046s (not the 60s timeout) → ⏹ aborted rendered, indicator cleared, no orphaned child process, app responsive after (/status works). /status showed 2 messages in context = the two user messages only, no orphaned assistant tool_call — live confirmation of the history-rollback.

Targets ui-rework. Does not touch main.

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 lavindeep merged commit 9b483cd into ui-rework Jun 28, 2026
2 checks passed
@lavindeep lavindeep deleted the feat/ui-subprocess-cancel branch June 28, 2026 14:20
lavindeep added a commit that referenced this pull request Jul 2, 2026
feat(ui): cancel a running command on Ctrl-C
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