mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-11 05:20:35 +00:00
* fix(tui): preserve multiline pasted input The TUI composer used a single-line `Input`, so a bracketed paste containing code, a stack trace, or multiple paragraphs kept only the first line when submitted. The composer is now a multiline `TextArea`: Enter still submits, Up/Down move inside the document before falling back to input history at the first/last line, and the slash palette is unchanged. Originally #4630 by @felix-windsor, cherry-picked onto current main; conflicts in app.py key gating (transcript paging keys), test imports, and the relocated TUI docs were resolved by hand. Fixes #4613 * fix(tui): submit multiline input unchanged and document the widgets layer Address the review on #4630: - Send non-command input to the agent and history exactly as typed or pasted. Only the empty-input check uses strip(), so leading indentation and trailing newlines in pasted code and stack traces survive. Slash commands are still trimmed so they resolve as before. - widgets/composer.py imports Textual, so the module-layout notes in tui/AGENTS.md and docs/TUI.md now exempt widgets/ alongside app.py. - Cover pressing Down on the last document line (history fallback), whitespace-only input, and slash commands with surrounding whitespace. Co-authored-by: Felix <felixwindsor3344@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tui): treat soft-wrapped rows as composer navigation boundaries TextArea soft-wraps long lines, and its Up/Down move between the wrapped rows of one logical line. The history fallback only looked at logical rows, so Up from the second visual row of a long prompt recalled history instead of moving the cursor, and Down from its first visual row could replace the draft with history's empty value. Use the TextArea navigator's first/last wrapped-row checks for the boundaries, and cover wrapped prompts in both directions. Also restore a tui/AGENTS.md sentence about PageUp/PageDown to the app.py entry it belongs to. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Felix <felixwindsor3344@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""Multiline TUI composer with the existing Enter-to-submit contract."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from textual.binding import Binding
|
|
from textual.message import Message
|
|
from textual.widgets import TextArea
|
|
|
|
|
|
class ComposerInput(TextArea):
|
|
"""A ``TextArea`` that submits on Enter and exposes input-like helpers.
|
|
|
|
``TextArea`` preserves bracketed multiline pastes and computes its hardware
|
|
cursor from display-cell widths, including CJK. The small compatibility
|
|
surface keeps the surrounding palette/history code independent of Textual's
|
|
document locations.
|
|
"""
|
|
|
|
BINDINGS = [Binding("enter", "submit", show=False, priority=True)]
|
|
|
|
@dataclass
|
|
class Submitted(Message):
|
|
input: ComposerInput
|
|
value: str
|
|
|
|
@property
|
|
def control(self) -> ComposerInput:
|
|
return self.input
|
|
|
|
@property
|
|
def value(self) -> str:
|
|
return self.text
|
|
|
|
@value.setter
|
|
def value(self, value: str) -> None:
|
|
self.load_text(value)
|
|
|
|
@property
|
|
def cursor_position(self) -> int:
|
|
row, column = self.cursor_location
|
|
return sum(len(line) + 1 for line in self.document.lines[:row]) + column
|
|
|
|
@cursor_position.setter
|
|
def cursor_position(self, position: int) -> None:
|
|
position = max(0, min(position, len(self.text)))
|
|
prefix = self.text[:position]
|
|
row = prefix.count("\n")
|
|
column = len(prefix.rsplit("\n", 1)[-1])
|
|
self.move_cursor((row, column))
|
|
|
|
# Boundaries are visual rows: TextArea soft-wraps long lines, and its Up/Down
|
|
# move between wrapped rows of one logical line before leaving it.
|
|
@property
|
|
def can_move_up(self) -> bool:
|
|
return not self.navigator.is_first_wrapped_line(self.cursor_location)
|
|
|
|
@property
|
|
def can_move_down(self) -> bool:
|
|
return not self.navigator.is_last_wrapped_line(self.cursor_location)
|
|
|
|
def action_submit(self) -> None:
|
|
self.post_message(self.Submitted(self, self.text))
|