fix: Ctrl+C double-exit in interactive shell#1105
fix: Ctrl+C double-exit in interactive shell#1105VaibhavUpreti merged 5 commits intoTracer-Cloud:mainfrom
Conversation
Greptile SummaryThis PR implements the Claude-CLI-style double-press Ctrl+C exit pattern: the first Ctrl+C shows a hint and re-displays the prompt; the second within 2 seconds prints "Goodbye!" and exits cleanly. The two contexts (inside a prompt_toolkit Application and between prompts via SIGINT) are handled separately but share a single Confidence Score: 5/5Safe to merge; all remaining findings are P2 style/clarity suggestions that do not affect correctness on any realistic platform. The implementation is well-reasoned and tested. The three findings are all P2: No files require special attention, though reviewers may want to confirm the Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User presses Ctrl+C] --> B{Where?}
B -->|Inside prompt_toolkit Application| C[ControlC key binding fires]
B -->|Between prompts / streaming output| D[SIGINT handler fires]
C --> E["event.app.exit(exception=KeyboardInterrupt)"]
E --> F["_patched_ask catches KeyboardInterrupt"]
D --> G["handle_ctrl_c_press()"]
F --> H{_last_ctrl_c within 2s?}
G --> H
H -->|No - first press| I["Print hint\n(Press Ctrl+C again to exit)\nUpdate _last_ctrl_c"]
H -->|Yes - second press| J["Print Goodbye!\nsys.exit(0)"]
I -->|In _patched_ask| K[Loop: re-run unsafe_ask]
I -->|In SIGINT handler| L[Return to caller]
J --> M[SystemExit 0]
K --> N[Prompt re-displayed]
subgraph Ctrl+Q path hard quit
O[ControlQ key binding] --> P["event.app.exit(exception=_HardQuitInterrupt)"]
P --> Q["_patched_ask re-raises _HardQuitInterrupt\n(bypasses retry loop)"]
end
Reviews (1): Last reviewed commit: "fix: Ctrl+C double-exit in interactive s..." | Re-trigger Greptile |
…nd exits) Implements the two-press Ctrl+C exit pattern: the first Ctrl+C displays "(Press Ctrl+C again to exit)" and re-displays the prompt; the second Ctrl+C within 2 seconds prints "Goodbye!" and exits cleanly (code 0). - Add `_with_ctrl_c_double_exit()` wrapping `question.unsafe_ask()` in a retry loop — avoids key-binding conflicts that caused the sentinel approach to fail - Add `_HardQuitInterrupt(KeyboardInterrupt)` so Ctrl+Q hard-quit bypasses the retry guard - Restore explicit ControlC binding in wizard `_base_bindings()` because `InquirerControl` is not a `BufferControl`, making prompt_toolkit's default Ctrl+C binding inactive for custom wizard prompts - Install a SIGINT handler in `main()` for between-prompt Ctrl+C - Add/update unit tests in `tests/cli/test_prompt_support.py` - Fix smoke test to use Escape (PTY-safe) instead of Ctrl+C Fixes Tracer-Cloud#1091
72866ae to
132a0dd
Compare
|
Already reviewed and solved the greptile-apps comments |
| try: | ||
| # unsafe_ask() propagates KeyboardInterrupt instead of | ||
| # swallowing it the way ask() does. | ||
| return question.unsafe_ask(*args, **kwargs) |
There was a problem hiding this comment.
Reset _last_ctrl_c after a successful prompt return. The current timestamp leaks across prompts, so Ctrl+C in prompt A followed by one Ctrl+C in prompt B within 2s exits unexpectedly.
There was a problem hiding this comment.
Thanks for the suggestion. Fixed in cc78f2e
|
|
||
| try: | ||
| cli(args=argv, standalone_mode=True) | ||
| except KeyboardInterrupt: |
There was a problem hiding this comment.
This exits on the first KeyboardInterrupt for unpatched prompt paths, which bypasses the double-press behavior from issue #1091. Reuse handle_ctrl_c_press here so Ctrl+C behavior stays consistent.
There was a problem hiding this comment.
Thanks for the suggestion! I want to make sure I understand the intent before implementing.
My concern is that calling handle_ctrl_c_press() here and then immediately return 0 would show (Press Ctrl+C again to exit) but exit the process right after — which seems more misleading than the current clean-exit behavior.
To make the double-press actually work here, we'd need a retry loop that re-runs cli(), but that restarts the entire CLI rather than re-displaying the interrupted prompt, which feels unexpected.
Could you clarify what behavior you had in mind? Were you thinking of a retry loop approach, or accepting that the hint fires but the process exits after the first press? Would love to understand your reasoning before deciding on the right fix.
Thank you~
There was a problem hiding this comment.
If consistency is the priority, happy to call handle_ctrl_c_press() here. Trade-off: the hint (Press Ctrl+C again to exit) would still show but the process exits immediately after — we can't re-display the prompt from main(). Let me know if that's acceptable.
…ss-prompt leakage
VaibhavUpreti
left a comment
There was a problem hiding this comment.
Great job @jason8745 ! Looks good, welcome the the OpenSRE community 🚀
|
🍕 @jason8745's PR: crispy edges, no unnecessary toppings, delivered on time. Understood the assignment. 🔥 👋 Join us on Discord - OpenSRE : hang out, contribute, or hunt for features and issues. Everyone's welcome. |
* fix: Ctrl+C double-exit in interactive shell (first press hints, second exits) Implements the two-press Ctrl+C exit pattern: the first Ctrl+C displays "(Press Ctrl+C again to exit)" and re-displays the prompt; the second Ctrl+C within 2 seconds prints "Goodbye!" and exits cleanly (code 0). - Add `_with_ctrl_c_double_exit()` wrapping `question.unsafe_ask()` in a retry loop — avoids key-binding conflicts that caused the sentinel approach to fail - Add `_HardQuitInterrupt(KeyboardInterrupt)` so Ctrl+Q hard-quit bypasses the retry guard - Restore explicit ControlC binding in wizard `_base_bindings()` because `InquirerControl` is not a `BufferControl`, making prompt_toolkit's default Ctrl+C binding inactive for custom wizard prompts - Install a SIGINT handler in `main()` for between-prompt Ctrl+C - Add/update unit tests in `tests/cli/test_prompt_support.py` - Fix smoke test to use Escape (PTY-safe) instead of Ctrl+C Fixes Tracer-Cloud#1091 * fix: add questionary.password to Ctrl+C double-exit and Escape-cancel patches * fix: use None sentinel for _last_ctrl_c to make never-pressed state explicit * fix: print newline on unhandled KeyboardInterrupt in main() for clean terminal output * fix: reset _last_ctrl_c after successful prompt return to prevent cross-prompt leakage

Fixes #1091
Changes
Implements the two-press Ctrl+C exit pattern, consistent with Claude CLI:
(Press Ctrl+C again to exit)and re-displays the promptGoodbye!and exits cleanly (exit code 0)Implementation
_with_ctrl_c_double_exit()inapp/cli/prompt_support.py— wrapsquestion.unsafe_ask()in a retry loop. This avoids key-binding conflicts that caused an earlier sentinel approach to fail (Application.exit()raises if called twice)._HardQuitInterrupt(KeyboardInterrupt)subclass so Ctrl+Q hard-quit bypasses the retry guard.ControlCkey binding in wizard_base_bindings()—InquirerControlis not aBufferControl, so prompt_toolkit's default Ctrl+C binding is inactive for custom wizard prompts; without this,\x03was silently ignored.main()via_install_sigint_handler()to handle Ctrl+C between prompts (streaming output, long-running ops, etc.).tests/cli/test_prompt_support.pycovering hint-on-first-press, exit-on-second-press, and window reset.test_tests_interactive_launcher_smoketo use Escape instead of Ctrl+C — single Ctrl+C no longer exits immediately, and double Ctrl+C in PTY cooked mode has SIGINT race conditions. Escape is PTY-safe and already the documented exit key in the prompt instruction.Verification
Before


After
Code Understanding and AI Usage
Did you use AI assistance (ChatGPT, Claude, Copilot, etc.) to write any part of this code?
If you used AI assistance:
Explain your implementation approach:
The core insight is that
question.unsafe_ask()propagatesKeyboardInterruptwhilequestion.ask()swallows it. By wrappingunsafe_ask()in awhile Trueretry loop, the first Ctrl+C is caught, the hint is printed, and the sameApplicationinstance is re-run (safe becauseApplication.run()resets_is_runningand creates a new future on each call). The second Ctrl+C within the time window exits. This avoids fighting prompt_toolkit's key binding merge order entirely.Checklist before requesting a review