Skip to content

fix(tui): follow new content only from the bottom, and steady the chat - #117

Merged
openjiuwenai merged 1 commit into
openJiuwen-ai:mainfrom
openjiuwenai:sync/pr-84
Oct 24, 2026
Merged

openjiuwenai merged 1 commit into
openJiuwen-ai:mainfrom
openjiuwenai:sync/pr-84

Conversation

@openjiuwen-sync-bot

@openjiuwen-sync-bot openjiuwen-sync-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Paired: GitHub #117 ↔ GitCode !84

Summary

Logs (F6) and sub-agent View details follow new content only from the bottom. Before, both jumped to new content even while you were reading further up. Now a scroll up stops the following, and a scroll back to the bottom resumes it.

  • One helper drives both views: app/tui/util/bottom_follow.py.
  • Every scroll request (keys, wheel, scrollbar, code) starts in Textual's _scroll_to, so the views decide there, by where the scroll heads. A selection dragged past the edge scrolls with no request, so it is judged by where the view goes.
  • A following details view waits for the scrolls Textual runs after the next refresh before it moves to new content. Without that, Down or Page Down at the bottom would scroll back up.
  • Past 1,500 lines the log drops its oldest lines without moving the ones a scrolled-up reader is on.

Chat panel:

  • Scrollbar flash after tools expand. The 1fr bottom spacer is left out of the arrangement once content fills the viewport.
  • One-row jitter while an answer streams. The message keeps the rows its text took at the same width: an unclosed ** or a link wraps a line for a moment. It also keeps the rows a folded tool group gave up. Each is limited to a third of the panel, and both are given back once the final text shows.
  • Windows: the user's message didn't show until the reply came. It is now brought into view by the layout that places it, not by a deferred scroll_visible that a held batch_update postponed.
  • Background replay: rows a replay prepends above a newly revealed message no longer count as a reply to follow, and a TOC jump still held for an unlaid turn is dropped when a message is sent.

GC freeze calibration (scheduled workflow, failing since the chat panel began streaming model text): scripts/profile_chat_panel.py still called add_agent_message(is_final=...).

  • Its workloads move to scripts/chat_panel_workloads.py, which has no import side effects and drives the live API.
  • A new PR test builds every workload. The ruff lists in CI and AGENTS.md now include the new script.
  • The workflow's pull_request paths now include both scripts.

Docs (en and zh-Hans) describe the View details behaviour.

Known limitations

  • If you hold the scrollbar thumb at the bottom while content lands, the next mouse move takes the view back to where it was grabbed, and it stops following. Textual measures a thumb drag from the grab point. Before this change the log jumped to the bottom on every update.
  • A scroll worked out just before a layout uses the old geometry (Textual doesn't guarantee its deferred scroll runs after a pending layout). For a thumb drag this can stop the following. For Page Down near the bottom it can land on the old bottom and follow, as a press one frame earlier does.

Test plan

  • New tests for each follow rule (tests/app/tui/screens/test_log_viewer.py, test_tool_view_modal.py) and each chat panel fix (tests/app/tui/widgets/test_chat_panel_scroll.py, chat/test_replay_tail_first.py). Each fails on an in-memory mutant of the rule it covers. The follow tests also pass repeatedly alone and under heavy CPU load.
  • Smart Test --paths on the changed files: 10302 passed, 6 skipped; architecture 692 passed. The full suite was not run locally.
  • ruff check, ruff format --check, ty check (linux, darwin, win32).
  • GC calibration locally: the 6 gc_calibration probes and scripts/calibrate_gc_freeze.py --turns 12 (every gate passed).
  • Reviewed in repeated Codex + subagent rounds; the final round found nothing open at medium or above.

The log screen (F6) and a sub-agent's View details went to new content
even while the user read further up. Both now follow new content only
while they are at the bottom: a scroll up stops the following, and a
scroll back to the bottom resumes it (app/tui/util/bottom_follow.py).
Every scroll request starts in Textual's _scroll_to, so the views
decide there, by where the scroll heads; a selection dragged past the
edge is judged by where the view goes. A following details view waits
for the scrolls Textual runs after the next refresh before it moves to
new content. Past 1500 lines the log drops its oldest lines without
moving the ones a scrolled-up reader is on.

The chat panel:
- leaves the 1fr bottom spacer out of the arrangement once content
  fills the viewport, so a scrollbar no longer flashes after tools
  expand;
- while an answer streams, keeps the rows its text took at the same
  width (an unclosed ** or a link wraps a line for a moment) and the
  rows a folded tool group gave up, each up to a third of the panel,
  so a view following the bottom no longer moves down a row and back;
- brings a new user message into view with the layout that places it,
  not a deferred scroll_visible that a held batch_update postpones (on
  Windows the message stayed below the viewport until the reply came);
- counts rows a replay prepends above a revealed message as no reply
  to follow, and drops a TOC jump still held when a message is sent.

The scheduled GC freeze calibration had failed since the chat panel
began streaming model text: scripts/profile_chat_panel.py still called
add_agent_message(is_final=...). Its workloads move to
scripts/chat_panel_workloads.py, which drives the live API and is
built by a PR test.
@openjiuwen-collaboration-bot

Copy link
Copy Markdown
Contributor

head_sha: 31bf8653d9d8b5ab4188c2848e0d845d491c0b88

变更摘要

本 PR 统一了 TUI 中“内容增长时是否自动跟随到底部”的行为:新增通用辅助类 BottomFollow,让日志视图(F6)和子代理 View details 只在视图处于底部时跟随新内容,向上滚动即停止跟随,滚回底部则恢复;同时稳定聊天面板在流式输出、工具折叠、重放历史和新消息定位时的滚动表现,并修复了 GC freeze 校准脚本调用已变更 API 的问题。该逻辑接入 Textual 的 _scroll_to、watch_scroll_y、watch_virtual_size 与 on_resize 钩子,使按键、滚轮、滚动条和代码发起的滚动都按目标位置决定是否跟随;对延迟到下次刷新才执行的滚动,通过 defer/deferred_ran 等待其完成后再结算,避免在底部按 Page Down/Down 反而向上跳。

主要改动

  • 新增底部跟随工具 BottomFollow:src/chrys/app/tui/util/bottom_follow.py 提供 scroll()、scrolled()、resized()/_settle()、defer()/deferred_ran() 与 pinning 状态,以“滚动目标是否为底部、是否自下而上、是否仅因尺寸变化被钳到底部”为判定依据,供多处滚动视图复用。
  • 日志视图改为按底部跟随并保持阅读位置:screens/logs/screen.py 新增 _ModuleLog,关闭 auto_scroll 并接管 _scroll_to/watch_scroll_y/watch_virtual_size/on_resize;_drain_pending_logs 删除强制 scroll_end,write 在超过 max_lines 丢弃旧行时按丢弃行数回退 scroll_y 并中止仍在进行的滚动动画。
  • 聊天面板改用布局驱动的新消息定位:scroll_controller.py 将 scroll_user_message_to_top 从延迟的 scroll_visible(top=True) 改为记录 _reveal/_reveal_top,由 arrange/size_updated 在布局后调用 _place_reveal 与 _reveal_placed 定位;新增 stop_scroll_y_animation 端口、_view_hold_rows(使重放插入到上方的行不计为回复增长)以及 _arrange_bottom_spacer/_spacer_rows(内容填满视口时不再为 1fr 占位符多排一行)。
  • 流式回答保持已占行数以减少抖动:messages.py 的 AgentMessage 通过重写 get_content_height、take_rows_folded_above、_give_back_held_rows 及 _holding_rows/_streamed_height/_folded_above 字段,在未闭合 **、链接换行以及上方工具组折叠时保留空行,上限为面板高度的三分之一,并在最终文本真正显示后归还;配套 VirtualizedMarkdown.shown_source、ToolGroup.rows_folding_gives_up() 和 live_renderer.add_answer_delta 的折叠行传递。
  • 校准脚本与 CI 配置同步:将 scripts/profile_chat_panel.py 中的合成负载抽取到新增的 scripts/chat_panel_workloads.py(无导入副作用并改用 add_answer_delta/finish_answer_segment 等现行 API),ci.yml 的 ruff 检查列表与 gc-freeze-calibration.yml 的 pull_request 路径均加入新脚本,并新增 tests/app/tui/widgets/chat/test_chat_panel_workloads.py 覆盖全部负载。

测试补充:test_log_viewer.py、test_tool_view_modal.py、test_chat_panel_scroll.py、test_replay_tail_first.py 增加了覆盖各跟随规则与聊天面板修复的用例,并在 tests/support/tui_helpers.py 新增 make_wheel 辅助函数构造滚轮事件。

@openjiuwen-collaboration-bot

openjiuwen-collaboration-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

head_sha: 31bf8653d9d8b5ab4188c2848e0d845d491c0b88

代码审查

审查结论

  • [建议] src/chrys/app/tui/widgets/chat/messages.py:684 fix: 最终文本若未触发 markdown 重解析,held rows 永久不归还
  • [建议] src/chrys/app/tui/screens/logs/screen.py:236 fix: 日志到达 max_lines 后不再自动跟随底部新内容
  • [建议] tests/app/tui/widgets/chat/test_chat_panel_workloads.py:65 fix: 新增的异步测试缺少 @pytest.mark.asyncio,实际不会执行
类型 数量
🔴 阻塞 0
🟡 建议 3

💬 仅评论

@openjiuwen-collaboration-bot

Copy link
Copy Markdown
Contributor

head_sha: 31bf8653d9d8b5ab4188c2848e0d845d491c0b88

任务名称 结果 日志操作
静态检查 ✅SUCCESS 点此跳转
禁用词扫描 ✅SUCCESS 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A
issue关联检查 N/A N/A
pr size检查 N/A N/A

Comment thread tests/app/tui/widgets/chat/test_chat_panel_workloads.py
Comment thread src/chrys/app/tui/widgets/chat/messages.py
Comment thread src/chrys/app/tui/screens/logs/screen.py
@openjiuwenai
openjiuwenai merged commit 2c0d1bf into openJiuwen-ai:main Oct 24, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants