Bug Description
After extended use of Code Puppy, mouse actions in the terminal (or other apps) start sending weird escape sequence characters into the Code Puppy prompt, resulting in meaningless garbled input.
Root Cause Analysis
Three interacting bugs compound over long sessions:
Bug 1 (Primary): _listen_for_ctrl_x_posix corrupts stdin by not draining multi-byte escape sequences
Files: code_puppy/agents/base_agent.py (line ~1737), code_puppy/tools/command_runner.py (line ~363)
During every agent execution, a background thread sets the terminal to cbreak mode and reads stdin one byte at a time. It only checks for Ctrl+X or the cancel key and silently discards all other bytes.
The problem: mouse events and terminal escape sequences are multi-byte (e.g., a mouse click generates \x1b[<0;15;20M — 7+ bytes). When the listener stops mid-sequence (stop_event.set() fires between bytes), the remaining bytes stay in the stdin buffer. When prompt_toolkit takes over for the next prompt, those orphaned bytes appear as garbled characters.
Compare to ask_user_question/tui_loop.py line 66-69 which correctly drains trailing escape bytes:
if ch == "\x1b":
while select.select([sys.__stdin__], [], [], 0.01)[0]:
sys.__stdin__.read(1) # drain the rest of the escape sequence
The cbreak listeners do NOT do this.
Bug 2 (Secondary): MCP Custom Server Form leaks mouse tracking escape sequences
File: code_puppy/command_line/mcp/custom_server_form.py (line ~614)
This is the only component in the entire codebase with mouse_support=True. All other TUI components correctly use mouse_support=False.
When this form runs, prompt_toolkit enables mouse tracking (\x1b[?1000h, \x1b[?1003h, \x1b[?1006h). It uses app.run(in_thread=True), and if cleanup fails (thread race condition, exception during alternate screen buffer exit), mouse tracking stays permanently enabled for the rest of the session. Every subsequent mouse action generates escape sequences that feed into stdin as garbage.
Bug 3 (Contributing): Concurrent stdin access between cbreak listener and prompt_toolkit
The cbreak listener thread reads stdin simultaneously with any prompt_toolkit Application the agent spawns (e.g., ask_user_question). Both fight over the same file descriptor, creating race conditions where one thread steals bytes that another needs, fragmenting escape sequences.
Steps to Reproduce
- Start Code Puppy and use it for an extended session (many agent interactions)
- Optionally use
/mcp to add a custom server (triggers the mouse_support=True form)
- After many interactions, click or scroll with mouse in the terminal
- Observe garbled escape sequence characters appearing in the prompt
Expected Behavior
Mouse actions should never produce visible characters in the Code Puppy prompt. Terminal state should be fully restored after every TUI interaction.
Proposed Fix
- Add escape sequence draining to both
_listen_for_ctrl_x_posix functions (in base_agent.py and command_runner.py) — when \x1b is read, drain all subsequent bytes within a short timeout
- Change
custom_server_form.py to use mouse_support=False (consistent with every other TUI component), and add explicit mouse tracking disable in the finally block as a safety net
- Add a terminal state reset helper to
terminal_utils.py that explicitly disables mouse tracking and bracketed paste, callable from cleanup paths
Environment
- macOS (posix)
- Terminal: iTerm2 / Terminal.app
- Code Puppy latest
Bug Description
After extended use of Code Puppy, mouse actions in the terminal (or other apps) start sending weird escape sequence characters into the Code Puppy prompt, resulting in meaningless garbled input.
Root Cause Analysis
Three interacting bugs compound over long sessions:
Bug 1 (Primary):
_listen_for_ctrl_x_posixcorrupts stdin by not draining multi-byte escape sequencesFiles:
code_puppy/agents/base_agent.py(line ~1737),code_puppy/tools/command_runner.py(line ~363)During every agent execution, a background thread sets the terminal to cbreak mode and reads stdin one byte at a time. It only checks for Ctrl+X or the cancel key and silently discards all other bytes.
The problem: mouse events and terminal escape sequences are multi-byte (e.g., a mouse click generates
\x1b[<0;15;20M— 7+ bytes). When the listener stops mid-sequence (stop_event.set()fires between bytes), the remaining bytes stay in the stdin buffer. Whenprompt_toolkittakes over for the next prompt, those orphaned bytes appear as garbled characters.Compare to
ask_user_question/tui_loop.pyline 66-69 which correctly drains trailing escape bytes:The cbreak listeners do NOT do this.
Bug 2 (Secondary): MCP Custom Server Form leaks mouse tracking escape sequences
File:
code_puppy/command_line/mcp/custom_server_form.py(line ~614)This is the only component in the entire codebase with
mouse_support=True. All other TUI components correctly usemouse_support=False.When this form runs, prompt_toolkit enables mouse tracking (
\x1b[?1000h,\x1b[?1003h,\x1b[?1006h). It usesapp.run(in_thread=True), and if cleanup fails (thread race condition, exception during alternate screen buffer exit), mouse tracking stays permanently enabled for the rest of the session. Every subsequent mouse action generates escape sequences that feed into stdin as garbage.Bug 3 (Contributing): Concurrent stdin access between cbreak listener and prompt_toolkit
The cbreak listener thread reads stdin simultaneously with any prompt_toolkit Application the agent spawns (e.g.,
ask_user_question). Both fight over the same file descriptor, creating race conditions where one thread steals bytes that another needs, fragmenting escape sequences.Steps to Reproduce
/mcpto add a custom server (triggers themouse_support=Trueform)Expected Behavior
Mouse actions should never produce visible characters in the Code Puppy prompt. Terminal state should be fully restored after every TUI interaction.
Proposed Fix
_listen_for_ctrl_x_posixfunctions (inbase_agent.pyandcommand_runner.py) — when\x1bis read, drain all subsequent bytes within a short timeoutcustom_server_form.pyto usemouse_support=False(consistent with every other TUI component), and add explicit mouse tracking disable in the finally block as a safety netterminal_utils.pythat explicitly disables mouse tracking and bracketed paste, callable from cleanup pathsEnvironment