mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +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>
63 lines
2.0 KiB
Python
63 lines
2.0 KiB
Python
"""Tests for multiline paste and CJK-aware composer cursor offsets."""
|
|
|
|
import pytest
|
|
from textual import events
|
|
from textual.app import App, ComposeResult
|
|
from textual.geometry import Offset
|
|
|
|
from deerflow.tui.widgets.composer import ComposerInput
|
|
|
|
|
|
class _Harness(App):
|
|
def compose(self) -> ComposeResult:
|
|
yield ComposerInput(id="c")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cursor_offset_after_cjk_has_no_off_by_one():
|
|
app = _Harness()
|
|
async with app.run_test() as pilot:
|
|
await pilot.pause()
|
|
comp = app.query_one("#c", ComposerInput)
|
|
comp.value = "总结一下" # 4 wide chars = 8 cells
|
|
comp.cursor_position = len(comp.value)
|
|
# Stock Input would report 9 here (unconditional +1); the fix reports 8,
|
|
# so the hardware/IME cursor sits exactly after the last character.
|
|
assert comp._cursor_offset == Offset(8, 0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cursor_offset_mid_text_is_unchanged():
|
|
app = _Harness()
|
|
async with app.run_test() as pilot:
|
|
await pilot.pause()
|
|
comp = app.query_one("#c", ComposerInput)
|
|
comp.value = "总结一下"
|
|
comp.cursor_position = 3 # not at end -> 3 wide chars = 6 cells
|
|
assert comp._cursor_offset == Offset(6, 0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cursor_offset_ascii_end_is_exact():
|
|
app = _Harness()
|
|
async with app.run_test() as pilot:
|
|
await pilot.pause()
|
|
comp = app.query_one("#c", ComposerInput)
|
|
comp.value = "abcd"
|
|
comp.cursor_position = 4
|
|
assert comp._cursor_offset == Offset(4, 0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_paste_preserves_newlines_and_indentation():
|
|
app = _Harness()
|
|
async with app.run_test() as pilot:
|
|
await pilot.pause()
|
|
comp = app.query_one("#c", ComposerInput)
|
|
comp.focus()
|
|
app.post_message(events.Paste("if ready:\n run()\nelse:\n wait()"))
|
|
await pilot.pause()
|
|
|
|
assert comp.value == "if ready:\n run()\nelse:\n wait()"
|
|
assert comp.cursor_location == (3, 10)
|