From d5595476ec9d1cdab4a801e29a049544d391fe1c Mon Sep 17 00:00:00 2001 From: Joshua Villalta <93329405+vgoat21@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:47:52 +0100 Subject: [PATCH] fix(PLU-308): chat agent scroll jump fix --- reai_toolkit/app/components/tabs/chat_tab.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/reai_toolkit/app/components/tabs/chat_tab.py b/reai_toolkit/app/components/tabs/chat_tab.py index 86b71eb..e811065 100644 --- a/reai_toolkit/app/components/tabs/chat_tab.py +++ b/reai_toolkit/app/components/tabs/chat_tab.py @@ -176,6 +176,7 @@ def __init__(self, on_closed: Optional[Callable[[], None]] = None) -> None: self._render_timer: Optional[QtCore.QTimer] = None self._pending_state: Optional[ChatState] = None self._pending_confirm_id: Optional[str] = None + self._last_rendered_md: Optional[str] = None def Create(self, title) -> bool: flags = getattr(kw.PluginForm, "WOPN_DP_TAB", 0) | getattr( @@ -332,14 +333,21 @@ def _flush_render(self) -> None: return sb = self._transcript.verticalScrollBar() + prev = sb.value() if sb is not None else 0 at_bottom = sb is None or sb.value() >= sb.maximum() - 4 md = render_transcript_markdown(state) - if hasattr(self._transcript, "setMarkdown"): - self._transcript.setMarkdown(md) - else: - self._transcript.setPlainText(md) - if at_bottom and sb is not None: - sb.setValue(sb.maximum()) + # Rebuilding the whole document resets the scrollbar to the top, so only + # do it when the content actually changed, and afterwards keep the reader + # where they were: pinned to the bottom if they were following the stream, + # otherwise restored to their previous position instead of snapping up. + if md != self._last_rendered_md: + self._last_rendered_md = md + if hasattr(self._transcript, "setMarkdown"): + self._transcript.setMarkdown(md) + else: + self._transcript.setPlainText(md) + if sb is not None: + sb.setValue(sb.maximum() if at_bottom else prev) running = state.run_status == "running" if self._send_btn is not None: