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.
This commit is contained in:
Rufino Cabrera 2026-06-11 10:25:45 +02:00
parent 1c79507140
commit fda018a324
2 changed files with 33 additions and 0 deletions

View File

@ -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.

View File

@ -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.