From f4a54060f1710a3a7a1d298e07132f150cc1fe11 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Fri, 28 Aug 2026 23:29:46 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20gui=20=E2=80=94=20fix=20TUI=20composer?= =?UTF-8?q?=20height=20for=20multiline=20input=20misalignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 2 +- emrg/client/python_tui/terminal.py | 11 +++++++ tests/test_terminal_render.py | 49 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/Agent.md b/Agent.md index c129871a..2e3e3a91 100644 --- a/Agent.md +++ b/Agent.md @@ -119,7 +119,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg ``` -Python: `uv run pytest tests/ -v` (1146) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (1147) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (95: 45 daemon_client + 20 conn-manager + 8 integration + 6 build-config + 7 gui-state + 3 preload-api + 4 boot-contract + 2 theme-guard) — syntax: `node --check main.js preload.js daemon_client.js` Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (476: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 16 transcript + 10 TranscriptView + 15 history + 22 composer + 31 Composer + 6 LinkDialog + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 10 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 15 daemonBridge + 7 DaemonBridgeProvider + 26 Shell + 15 DialogHost + 20 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown) + `npm run build` → `renderer/dist/` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) diff --git a/emrg/client/python_tui/terminal.py b/emrg/client/python_tui/terminal.py index d6ef67be..5c9cebf5 100644 --- a/emrg/client/python_tui/terminal.py +++ b/emrg/client/python_tui/terminal.py @@ -249,6 +249,17 @@ def _get_lines(name: str) -> list[object]: status_lines = _get_lines("status")[:1] composer_lines = _get_lines("composer")[:10] prompt_lines = _get_lines("prompts")[:1] + # rant 2026-08-28T22:53:24 — composer height must track the composer's + # actual rendered line count, not a hard-coded 3. A multiline input + # (text containing "\n", e.g. pasted text or pressing Enter first) + # renders more lines than single-line input; a fixed height makes the + # viewport's region model (composer_height / chat_height / chat_region) + # disagree with what write_lines_to_buffer actually lays out, which is + # the reported "错行" (input content misaligned with the "> " prompt). + # Here we sync the model with reality so every downstream consumer uses + # the correct height. The render loop already computes allocation from + # len(composer_lines) below; this keeps the viewport object in lockstep. + self.viewport.composer_height = len(composer_lines) # Get ALL chat lines first (without truncation) so we know the real # needed height. Truncation depends on viewport_height, but # viewport_height should depend on content — not the other way around. diff --git a/tests/test_terminal_render.py b/tests/test_terminal_render.py index 377f9eb5..28cb9a40 100644 --- a/tests/test_terminal_render.py +++ b/tests/test_terminal_render.py @@ -94,3 +94,52 @@ def test_shutdown_clears_screen(): # 清屏后归位 (0,0):CLEAR_SCREEN 之后必须紧跟 CURSOR_HOME from emrg.client.python_tui.output import CURSOR_HOME assert out.index(CLEAR_SCREEN) < out.index(CURSOR_HOME), "clear screen must precede cursor home" + + +class FakeComposer: + """Composer fake that mimics InputWidget.render: one top/bottom separator line + plus one Line per logical input line (multi-line text → more than 3 rows). + + rant 2026-08-28T22:53:24 — a fixed composer_height=3 mis-models multiline + input; the viewport must track the actual rendered row count. + """ + + def __init__(self, text: str) -> None: + self.text = text + self.dirty = True + + def render(self, ctx: RenderContext) -> list[Line]: + lines = [Line(spans=[Span(text="─" * ctx.width)])] + # Split on "\n": each logical line, including leading/trailing empty + # strings (matches InputWidget.render's raw = text.split("\n")). + for logical in self.text.split("\n"): + lines.append(Line(spans=[Span(text="> " + logical)])) + lines.append(Line(spans=[Span(text="─" * ctx.width)])) + return lines + + +def test_composer_height_tracks_multiline_render(): + """viewport.composer_height must reflect the composer's actual line count. + + For a single-line input the composer renders 3 rows (sep + content + sep). + For multiline input (text containing "\n") it renders more. A hard-coded + height of 3 would make the viewport's region model disagree with the real + layout → the reported "错行" (composer content misaligned with "> "). + """ + term = Terminal() + term.mount(composer=FakeComposer("hello")) + with redirect_stdout(io.StringIO()): + term.render(full=True) + assert term.viewport.composer_height == 3, "single-line → 3 rows (sep+content+sep)" + + term2 = Terminal() + term2.mount(composer=FakeComposer("line1\nline2")) + with redirect_stdout(io.StringIO()): + term2.render(full=True) + assert term2.viewport.composer_height == 4, "2 logical lines → 4 rows" + + term3 = Terminal() + term3.mount(composer=FakeComposer("\ntest")) + with redirect_stdout(io.StringIO()): + term3.render(full=True) + assert term3.viewport.composer_height == 4, "leading newline → 4 rows"