diff --git a/Agent.md b/Agent.md index a8caef3f..6a0b3b75 100644 --- a/Agent.md +++ b/Agent.md @@ -118,7 +118,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` -Python: `uv run pytest tests/ -v` (801) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (804) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (244: 44 daemon_client + 19 conn-manager + 22 app-commands + 119 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` 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 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) diff --git a/emrg/client/python_tui/terminal.py b/emrg/client/python_tui/terminal.py index 37720b45..47fdb410 100644 --- a/emrg/client/python_tui/terminal.py +++ b/emrg/client/python_tui/terminal.py @@ -23,6 +23,7 @@ from emrg.client.python_tui.buffer import Buffer, CharPool, HyperlinkPool, StylePool, diff_buffers from emrg.client.python_tui.output import ( + CLEAR_SCREEN, CURSOR_HIDE, CURSOR_SHOW, CURSOR_HOME, @@ -142,6 +143,10 @@ def __post_init__(self) -> None: ) self._rendered_cache: dict[str, list[object]] = {} self._scrollback_lines_pushed: int = 0 + # 上次渲染宽度(rant 2026-08-14T11:47:11):窗口缩窄时终端屏幕每行右侧的旧字符 + # 不会被 diff 清除(diff_buffers 只比较 min(prev_w, curr_w) 列,且 Buffer.resize + # 缩窄已物理截断旧宽度信息)→ render 检测到缩窄时先 CLEAR_SCREEN 再全量重绘。 + self._last_render_width: int = self.caps.width @property def viewport_height(self) -> int: @@ -215,6 +220,16 @@ def render(self, full: bool = False) -> None: width = self.viewport.viewport_width ctx = RenderContext(width=width) + # 宽度缩窄 → 终端屏幕右侧残留旧字符(diff 只覆盖新宽度;Buffer.resize 缩窄已 + # 丢弃旧宽度信息,diff_buffers 无从清除)。先清屏 + 清 front buffer 强制全量 + # 重绘,杜绝残留叠加(rant 2026-08-14T11:47:11)。 + if width < self._last_render_width: + sys.stdout.write(CLEAR_SCREEN) + if self._front_buffer: + self._front_buffer.clear() + full = True + self._last_render_width = width + # Cache last rendered output per widget so dirty=False doesn't blank it def _get_lines(name: str) -> list[object]: diff --git a/tests/test_terminal_render.py b/tests/test_terminal_render.py new file mode 100644 index 00000000..b7171c4e --- /dev/null +++ b/tests/test_terminal_render.py @@ -0,0 +1,80 @@ +"""Unit tests for Terminal render — 宽度缩窄残留清除(rant 2026-08-14T11:47:11)。 + +窗口缩窄时终端屏幕每行右侧的旧字符不会被 diff 清除(diff_buffers 只比较 +min(prev_w, curr_w) 列,且 Buffer.resize 缩窄已物理截断旧宽度信息)→ +render 检测到缩窄时先 CLEAR_SCREEN 再全量重绘。本测试验证该行为。 +""" + +from __future__ import annotations + +import io +import sys +from contextlib import redirect_stdout + +import pytest + +# 依赖 posix 路径(raw mode / termios 守卫),Windows 跳过(同 test_app_widgets) +pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Terminal 渲染测试依赖 POSIX 路径") + +from emrg.client.python_tui.output import CLEAR_SCREEN +from emrg.client.python_tui.terminal import Terminal +from emrg.client.python_tui.widgets.base import Line, RenderContext, Span + + +class FakeWidget: + """Minimal widget: renders `width` chars per line.""" + + def __init__(self) -> None: + self.dirty = True + + def render(self, ctx: RenderContext) -> list[Line]: + return [Line(spans=[Span(text="x" * ctx.width)])] + + +def _set_width(term: Terminal, width: int) -> None: + """Force terminal width (SIGWINCH path would probe real tty — not usable in tests).""" + term.caps.width = width + term.viewport.resize(term.caps.height, width) + + +def _render(term: Terminal) -> str: + buf = io.StringIO() + with redirect_stdout(buf): + term.render(full=True) + return buf.getvalue() + + +def test_render_shrink_emits_clear_screen(): + """缩窄 40 → 20:输出必须先 CLEAR_SCREEN(右侧残留清除)。""" + term = Terminal() + term.mount(chat=FakeWidget()) + _set_width(term, 40) + _render(term) # 建立 _last_render_width = 40 + + _set_width(term, 20) + out = _render(term) + + assert CLEAR_SCREEN in out + + +def test_render_same_width_no_clear_screen(): + """同宽两次渲染:无 CLEAR_SCREEN(避免无谓闪烁)。""" + term = Terminal() + term.mount(chat=FakeWidget()) + _set_width(term, 40) + _render(term) + + out = _render(term) + assert CLEAR_SCREEN not in out + + +def test_render_grow_no_clear_screen(): + """拉宽 40 → 60:无 CLEAR_SCREEN(新增列由 diff 覆盖,无需清屏)。""" + term = Terminal() + term.mount(chat=FakeWidget()) + _set_width(term, 40) + _render(term) + + _set_width(term, 60) + out = _render(term) + assert CLEAR_SCREEN not in out