fix(tui): stop the paste burst from swallowing Enter - #179
Conversation
try_detect_paste_burst drains the event queue into one paste when a character arrives with more input already behind it. It appended KeyCode::Enter to the buffer as a literal '\n', so the keystroke that submits the line was consumed as text: the message landed in the prompt with a trailing newline and was never sent. Enter appeared dead and the conversation could not advance past its first turn. This fires deterministically whenever a whole line arrives at once — a paste in a terminal without bracketed paste, or a host app writing `text + "\n"` into the pane's PTY in a single write. Peek before absorbing: a newline with more input behind it is an interior line break of a multi-line paste and stays in the text; a newline with nothing behind it ends the line, so it is stashed in pending_key and replayed to the caller, which submits. The reason the coalescing exists — a multi-line paste arriving as several separate messages — still holds. Also gate the CLI's Enter handling on any_blocking_modal_open() rather than any_modal_open(). The latter also counts passive banners that render as overlays but never take a keystroke; while one was visible, Enter was neither submitted nor queued. Latent today, same failure mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Fixes an input-handling bug where paste-burst coalescing could consume the Enter keystroke, leaving the first prompt turn “stuck” and never submitted. Also aligns CLI Enter/text gating with the app’s “blocking modal” predicate so passive overlay banners don’t drop Enter.
Changes:
- Update
try_detect_paste_burstto treat a terminal Enter as “submit” (replayed viapending_key) when it’s the trailing terminator of a burst. - Switch CLI text-input and Enter-submission gates from
any_modal_open()toany_blocking_modal_open()to avoid passive banners eating keystrokes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src-rust/crates/tui/src/app.rs |
Adjusts paste-burst draining logic to avoid swallowing the submit Enter by stashing it for replay. |
src-rust/crates/cli/src/main.rs |
Uses the blocking-modal predicate when deciding whether to accept prompt text / Enter submission in the interactive loop. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) { | ||
| buf.push('\n'); | ||
| } else { | ||
| self.pending_key = Some(k); | ||
| break; |
| ) { | ||
| if let KeyCode::Char(c) = key.code { | ||
| if app.prompt_is_accepting_text() && !app.any_modal_open() { | ||
| if app.prompt_is_accepting_text() && !app.any_blocking_modal_open() { |
Verified in a real ptyThe PR description noted this was diagnosed by reading the event loop rather than reproduced. It has now been reproduced and verified end to end, driving the built binary over a pty — the same input path a host pane uses. MethodSpawned Negative controlA green test proves nothing unless it can actually detect the bug, so the pre-fix That is the reported symptom exactly: 332 bytes of output — just the prompt redrawing with With the fix
The burst case went from 332 bytes of output pre-fix to 6728 post-fix. The Also still green
The harness is currently a scratch script and is not included in this PR — it needs a real pty, so landing it would want a scripted integration slot rather than a plain 🤖 Generated with Claude Code |
Adds cases/07_paste_burst.sh to the tmux-driven interactive suite, guarding the fix in 65dda5c. The bug only reproduces when a whole line lands in the pty in one write — how a host application drives an embedded pane, and how a terminal without bracketed paste delivers a clipboard paste. Typing keystroke by keystroke escapes it entirely, so none of the existing cases could have caught it. New tui_paste helper delivers a string in a single write via tmux set-buffer/paste-buffer. Deliberately not paste-buffer -p: bracketed paste arrives as one Paste event and bypasses the burst detector, which is the code under test. Two assertions, both offline (the payload is /help, handled inside the TUI): 1. A burst-delivered line plus its Enter submits — the overlay opens. 2. A burst whose newline is interior does NOT submit. The first line is anchored to the composer prompt glyph, so it fails if an interior newline ever starts submitting: the text would move to the transcript and the prompt would carry the second line instead. This is the guard on why the coalescing exists at all. Verified against the pre-fix binary: assertion 1 fails with '/help' stranded on the prompt row. Full suite 44/44 with the fix in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regression test added —
|
The bug
The CLI could not get past the first turn of a conversation: you type a message, press Enter, and nothing is sent. The transcript stays frozen on turn 1 with the status stuck on "Waiting for you".
Root cause
try_detect_paste_burst(crates/tui/src/app.rs) drains the event queue into a single paste when a character arrives with more input already behind it. It appendedKeyCode::Enterto the buffer as a literal'\n':The caller then hands the burst to
handle_paste_dataandcontinues, so the message lands in the prompt buffer with a trailing newline and is never submitted. The keystroke that would have sent it was eaten.This fires deterministically whenever a whole line arrives at once:
text + "\n"into the pane's PTY in a single write — which is how a desktop shell drives an embedded Coven paneTyping by hand usually escapes it, since each keystroke arrives alone — which is why the first message of a session often goes through and everything after it appears dead.
The fix
Peek before absorbing. A newline with more input behind it is an interior line break of a multi-line paste and stays in the text; a newline with nothing behind it is the keystroke that ends the line, so it is stashed in
pending_keyand replayed to the caller, which submits.The reason the coalescing exists in the first place — a multi-line paste arriving as several separate messages — still holds:
"a\nb\nc\n"now sends as one message instead of three.Both event loops (
crates/cli/src/main.rsandcrates/tui/src/app.rs::run) share this helper, so one change covers both.Second fix, same failure mode
The CLI's Enter handling gated on
any_modal_open(), whose own doc comment says it counts passive banners "for rendering purposes only". Those banners never take a keystroke, so while one was visible Enter was neither submitted nor queued — it was dropped. Switched both input gates toany_blocking_modal_open().This one is latent today (nothing currently calls
show()onoverage_upsell,voice_mode_notice, ormemory_update_notification), but it is the same trap sitting on the same line of code.Verification
cargo check -p claurst-tui -p claurst— cleancargo test -p claurst-tui— 722 passed, 0 failedDiagnosed by reading the event loop rather than reproducing interactively, so a hands-on confirmation that a pasted or host-injected message now sends is worth doing before merge.
🤖 Generated with Claude Code