Skip to content

Bracketed paste mode: detect and coalesce multi-line paste input #134

@Aaronontheweb

Description

@Aaronontheweb

Problem

When a user pastes multi-line text into a TextInputNode, the terminal delivers pasted content character-by-character. Each \n in the pasted text triggers HandleEnter(), which fires the Submitted observable. This results in each line of the paste being treated as a separate submission.

Observed behavior in Netclaw TUI (netclaw chat):

  • User pastes a long blog post into the input field
  • Each CRLF in the pasted text fires Submitted separately
  • The app receives 50+ individual submissions instead of one
  • The app becomes unresponsive because each submission triggers an LLM call
  • Ctrl+Q (quit) can't fire until all queued submissions drain through the event loop

Expected Behavior

Termina should detect bracketed paste mode from the terminal:

  1. Detect paste start — terminals that support bracketed paste mode send ESC[200~ before pasted content and ESC[201~ after
  2. Buffer paste content — while inside a paste bracket, buffer all characters (including newlines) without triggering Submitted
  3. Deliver as single event — when the paste bracket closes, either:
    • Insert all pasted text into the TextInputNode as a single string (without submitting), OR
    • Fire a separate Pasted observable with the full pasted text
  4. Fallback for terminals without bracketed paste — detect rapid sequential Enter keystrokes (e.g., within 50ms of each other) and coalesce them. This is the same heuristic Claude Code and OpenCode use.

UX Reference

Claude Code and OpenCode both handle this well:

  • They show "pasted text" as a summary/collapsible block
  • The pasted content is treated as a single message, not line-by-line submissions
  • The user can review the pasted content before submitting

Workaround

Applications can work around this today by debouncing Submitted:

_promptInput.Submitted
    .Where(text => !string.IsNullOrWhiteSpace(text))
    .Buffer(TimeSpan.FromMilliseconds(100))
    .Where(batch => batch.Count > 0)
    .Subscribe(batch =>
    {
        _promptInput.Clear();
        var combined = string.Join("\n", batch);
        // ... handle as single submission
    });

This works but is application-level — the framework should handle it.

Technical Notes

  • TextInputNode.HandleInput() processes ConsoleKey.Enter via HandleEnter() which calls _submitted.OnNext(_text)
  • The Ctrl+V handler in HandleCharacter() currently returns true without doing anything (it's a no-op)
  • Bracketed paste mode is widely supported: xterm, iTerm2, Windows Terminal, GNOME Terminal, Alacritty, kitty
  • The terminal must be put into bracketed paste mode by sending ESC[?2004h on startup and ESC[?2004l on shutdown

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions