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: