Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 路径不受影响)
Expand Down
15 changes: 15 additions & 0 deletions emrg/client/python_tui/terminal.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand DownExpand Up@@ -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:
Expand DownExpand Up@@ -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]:
Expand Down
80 changes: 80 additions & 0 deletions tests/test_terminal_render.py
Original file line numberDiff line numberDiff line change
@@ -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
Loading