Skip to content

fix: cancel current command/stream on Ctrl+C instead of exiting - #89

Draft
aryansk wants to merge 1 commit into
shauryagangrade:mainfrom
aryansk:codex/issue-51-ctrl-c
Draft

fix: cancel current command/stream on Ctrl+C instead of exiting#89
aryansk wants to merge 1 commit into
shauryagangrade:mainfrom
aryansk:codex/issue-51-ctrl-c

Conversation

@aryansk

@aryansk aryansk commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Problem

Pressing Ctrl+C while the assistant is streaming a reply, or while a bash
command is running, terminates the entire GCode session and prints the
goodbye screen. Any messages since the last save are lost — the turn in
progress is never persisted. Closes #51.

Change

Three interrupt-handling fixes:

  • execute_bash (gcode/tools.py): catches KeyboardInterrupt from
    subprocess.run and returns Command execution cancelled by user.
    instead of letting it kill the REPL. Applies both to the prompt phase
    (already handled) and the running-command phase (new).
  • Streaming (gcode/agent.py _stream): a Ctrl+C during streaming
    stops the stream, keeps whatever was accumulated so far as the turn's
    assistant message (with no half-formed tool calls), and persists it —
    the session stays alive and history is saved.
  • Tool calls (gcode/agent.py _run_tool): a Ctrl+C during any tool
    call returns Command execution cancelled by user. to the model instead
    of propagating.

A Ctrl+C (or Ctrl+D) at the prompt still exits with the goodbye screen,
unchanged.

Why this approach

Minimal, targeted changes: the interrupt is caught at the two async
boundaries where it can arrive mid-work (streaming and subprocess), and the
tool-call boundary is hardened so a cancelled tool result is recorded in
history like any other tool result. No changes to the prompt loop or exit
paths.

Testing

uv run pytest:  111 passed (4 new: 3 agent streaming/tool-cancel tests,
                1 subprocess-interrupt test)
uv run ruff check .:  All checks passed
uv run ruff format --check .:  31 files already formatted
uv run mypy gcode:  Success: no issues found in 12 source files
uv run bandit -q -r gcode/ -c pyproject.toml:  clean
uv run python -m compileall -q gcode demo:  clean

New tests cover: partial reply retained on mid-stream interrupt, empty
reply when interrupted before any chunk, and a tool that raises
KeyboardInterrupt returning the cancelled result.

Documentation and release impact

  • User-facing documentation updated
  • Changelog/release note needed (user-visible behavior change)
  • Migration or compatibility note needed
  • No documentation impact

Review notes

  • Known limitations: a Ctrl+C during streaming intentionally drops any
    tool calls the model had begun to emit (they are half-formed), but keeps
    the text.
  • Follow-up issue: none.

execute_bash now catches KeyboardInterrupt from subprocess.run and
returns "Command execution cancelled by user."; the streaming loop stops
on interrupt and keeps the partial reply so the session and history
survive; tool calls interrupted mid-run cancel cleanly. A Ctrl+C at the
prompt still exits with the goodbye screen. Closes shauryagangrade#51.

@shauryagangrade shauryagangrade left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-scoped fix with solid tests. The three interrupt sites (stream, tool dispatch, subprocess) each get a sensible behavior: keep the session alive, persist whatever text accumulated, and drop half-formed tool_calls from history (tool_calls=[] on interrupt). The new tests cover the important cases, including the empty-stream interrupt and the subprocess-level interrupt.

Notes (non-blocking):

  • execute_bash: on Ctrl+C, subprocess.run may leave the child process running on some platforms (the SIGINT usually hits the whole process group, but not always). Consider killing the result process on KeyboardInterrupt if you want to be airtight. Not blocking.
  • _run_tool now swallows KeyboardInterrupt into a tool result string; if the interrupt lands between tools in the loop it's fine, but double-check the agent loop in cli.py still sees a KeyboardInterrupt escape for the outermost Ctrl+C-to-quit path.
  • Minor: the (streaming stopped by user) info line is printed after assistant_end() — verify it renders below the frozen live region rather than overwriting it (Rich Live.stop() handles this, but worth a quick manual check).

Approve — ready once you mark the PR ready for review.

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.

[Bug]: Ctrl+C mid-stream or during a bash command exits the whole session

2 participants