Skip to content

Conversation

@Ttian18
Copy link

@Ttian18 Ttian18 commented Feb 11, 2026

Description

Fixes #4423

The TUI chat input uses a single-line Input widget that drops multiline
pastes and has no newline support. Copy-to-clipboard also fails on some
platforms.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Related Issues

Fixes #4423

Changes Made

  • Replace single-line Input widget with TextArea in chat REPL
    • Enter submits, Shift+Enter inserts a newline
    • Dynamic height (1–5 visible lines, scrolls beyond that)
    • Multiline paste now works correctly
  • Add Windows clipboard support (clip.exe) in _copy_to_clipboard
  • Add xsel fallback for Linux when xclip is not installed
  • Update CSS selectors and widget queries in app.py to match new widget

Testing

  • Unit tests pass (cd core && pytest tests/)
  • Lint passes (cd core && ruff check .)
  • Manual testing performed

Manual verification:

  • Shift+Enter inserts newlines in input box
  • Multiline paste preserves all lines with visible cursor
  • Ctrl+C copies selected text to clipboard
  • Enter submits message and clears input
  • Slash commands (/help, /sessions) still work

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes

…denhq#4423)

Replace single-line Input widget with TextArea in chat REPL so
Shift+Enter inserts newlines and multiline paste works correctly.
Add Windows clipboard support (clip.exe) and xsel fallback for Linux.
Copy link

@jose-blockchain jose-blockchain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this: multiline input and cross-platform clipboard are good improvements to have.

I found two issues that would prevent the Enter-to-submit mechanism from working at all, and a couple of smaller items. Details in the inline comments.

Blocking:

  • event.shift does not exist on Textual Key events — will raise AttributeError
  • TextArea._on_key calls event.stop() on Enter, so the event never bubbles to ChatRepl.on_key — submission never triggers, Enter just inserts a newline

Suggested approach: subclass TextArea and override _on_key to intercept Enter before the base class consumes it.

Happy to re-review once those are addressed.

focused = self.app.focused
if not isinstance(focused, TextArea):
return
if event.key == "enter" and not event.shift:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key events have no .shift attribute. The code not event.shift will raise AttributeError. The correct approach is to check event.key == "enter" (Shift+Enter arrives as event.key == "shift+enter", so it naturally wouldn't match).

async def on_input_submitted(self, message: Input.Submitted) -> None:
"""Handle input submission — either start new execution or inject input."""
user_input = message.value.strip()
async def on_key(self, event) -> None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TextArea._on_key handles "enter" by:

  1. Inserting "\n" into the document
  2. Calling event.stop() — which stops event propagation entirely

This means ChatRepl.on_key will never receive the Enter key event at all. The Enter key is consumed by TextArea before it can bubble. The entire submission mechanism is broken — pressing Enter will just insert newlines, and submission will never trigger.

The fix requires subclassing TextArea and overriding _on_key to intercept Enter before the base class processes it.

chat_input = chat_repl.query_one("#chat-input", Input)
chat_input.value = "/sessions"
chat_input = chat_repl.query_one("#chat-input", TextArea)
chat_input.text = "/sessions"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (pre-existing): Setting chat_input.text = "/sessions" only places text in the widget — it does not trigger submission. The comment says # Trigger submission but nothing actually submits. This was already broken in the original Input.value code, so not a regression from this PR, but worth fixing while you're here. You could call await chat_repl._submit_input("/sessions") directly instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: TUI copy + newline

2 participants