Root cause: stream loop emitted a bare st.empty() heartbeat per tick, accumulating dozens of empty slots under chat_message. On the next rerun the history replay path had no such slots, so Streamlit's incremental DOM diff misaligned and left the previous render of the last text block as a grayed-out ghost. Fix: unify streaming and history replay under the same shell (slot = st.empty(); with slot.container(): render_segments(...)). Heartbeat now re-enters the same slot each tick; when response is unchanged Streamlit's diff is a no-op (no flicker), but the container() call still lets StopException propagate so abort keeps working. Simplified render_segments from 5 params (placeholders/rendered_cache/force_text) to 2 (segments, suffix). Net: +19 -20 lines. Verified: no ghost, abort interrupts mid-stream, per-turn fold-collapse preserved.
7.2 KiB
7.2 KiB