From fda018a324ee44606b96327e98c8da5e97940b0a Mon Sep 17 00:00:00 2001 From: Rufino Cabrera Date: Thu, 11 Jun 2026 10:25:45 +0200 Subject: [PATCH] Fix REPL y/N prompts showing ^M after Enter Restore cooked TTY state after prompt_toolkit exits so Rich console.input and other follow-up prompts (e.g. /compact confirmation) accept Enter normally. --- src/cai/repl/ui/prompt.py | 10 ++++++++++ tests/repl/test_multiline_prompt.py | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/cai/repl/ui/prompt.py b/src/cai/repl/ui/prompt.py index daf5f950..a3b351e6 100644 --- a/src/cai/repl/ui/prompt.py +++ b/src/cai/repl/ui/prompt.py @@ -216,6 +216,7 @@ def get_user_input(command_completer, key_bindings, history_file, toolbar_func, return [sep_line] # Get user input with all features + result = "" try: result = prompt( [("class:prompt", "CAI> ")], @@ -244,6 +245,15 @@ def get_user_input(command_completer, key_bindings, history_file, toolbar_func, except (AttributeError, OSError): _REPL_STDIN_EXHAUSTED_PENDING = True return "" + finally: + # prompt_toolkit restores termios from the snapshot taken after we cleared + # ICRNL above; Rich console.input and plain input() then echo Enter as ^M. + try: + from cai.util.streaming import restore_terminal_state + + restore_terminal_state(emit_trailing_newline=False) + except Exception: + pass # Print bottom separator only when user submitted non-empty input, # so that empty Enter produces a single separator between prompts. diff --git a/tests/repl/test_multiline_prompt.py b/tests/repl/test_multiline_prompt.py index e4938498..4835055b 100644 --- a/tests/repl/test_multiline_prompt.py +++ b/tests/repl/test_multiline_prompt.py @@ -33,6 +33,29 @@ class TestMultilinePromptConfig: 'Without this, Enter may only insert newlines after long agent turns.' ) + def test_tty_restored_after_prompt(self): + """Cooked TTY must be restored after prompt() for follow-up console.input prompts. + + We clear ICRNL before prompt_toolkit starts; on exit it restores that snapshot, + so /compact and other y/N confirmations see Enter as ^M unless we run + restore_terminal_state again in a finally block. + """ + from cai.repl.ui.prompt import get_user_input + import inspect + + source = inspect.getsource(get_user_input) + + assert 'finally:' in source, ( + 'REGRESSION: get_user_input must use finally to restore the TTY after ' + 'prompt_toolkit exits. Without this, Rich console.input shows ^M on Enter.' + ) + assert source.index('finally:') > source.index('prompt('), ( + 'REGRESSION: TTY restore must run after prompt(), not only before it.' + ) + assert 'restore_terminal_state' in source[source.index('finally:'):], ( + 'REGRESSION: finally block must call restore_terminal_state after prompt().' + ) + def test_icrnl_cleared_before_prompt(self): """ICRNL/INLCR/IGNCR must be cleared before prompt() to keep Enter as submit.