diff --git a/frontends/stapp.py b/frontends/stapp.py
index 67e8355..f4ee915 100644
--- a/frontends/stapp.py
+++ b/frontends/stapp.py
@@ -89,21 +89,15 @@ def fold_turns(text):
segments.append({'type': 'fold', 'title': title, 'content': content})
else: segments.append({'type': 'text', 'content': marker + content})
return segments
-def render_segments(segments, placeholders=None, rendered_cache=None, suffix='', force_text=False):
- def _render_seg(target, seg, suf=''):
+def render_segments(segments, suffix=''):
+ # 整块重画:调用方用 slot.container() 包裹,保证 DOM 路径稳定、跨 rerun 对齐(消除"灰色重影")。
+ # heartbeat 空转时 segments 不变 → Streamlit 后端 diff 无变化 → 前端零闪烁;
+ # 但 container/markdown 本身是 API 调用,StopException 仍会被抛出(abort 照常起作用)。
+ for seg in segments:
if seg['type'] == 'fold':
- with target.expander(seg['title'], expanded=False): st.markdown(seg['content'])
- else: target.markdown(seg['content'] + suf, unsafe_allow_html=not not suf)
- if placeholders is not None:
- while len(placeholders) < len(segments):
- placeholders.append(st.empty()); rendered_cache.append(None)
- for i, seg in enumerate(segments):
- if rendered_cache[i] != (seg, suffix):
- if not force_text or seg['type'] == 'text':
- with placeholders[i].container(): _render_seg(st, seg, suffix)
- rendered_cache[i] = (seg, suffix)
- else:
- for seg in segments: _render_seg(st, seg)
+ with st.expander(seg['title'], expanded=False): st.markdown(seg['content'])
+ else:
+ st.markdown(seg['content'] + suffix, unsafe_allow_html=not not suffix)
def agent_backend_stream(prompt):
display_queue = agent.put_task(prompt, source="user")
@@ -123,8 +117,11 @@ def agent_backend_stream(prompt):
if "messages" not in st.session_state: st.session_state.messages = []
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
- if msg["role"] == "assistant": render_segments(fold_turns(msg["content"]))
- else: st.markdown(msg["content"])
+ # 用 slot=st.empty() + with slot.container(): ... 的外壳,DOM 路径和流式渲染完全一致,跨 rerun 对齐
+ slot = st.empty()
+ with slot.container():
+ if msg["role"] == "assistant": render_segments(fold_turns(msg["content"]))
+ else: st.markdown(msg["content"])
# IME composition fix (macOS only) - prevents Enter from submitting during CJK input
if os.name != 'nt':
@@ -136,11 +133,13 @@ if prompt := st.chat_input("请输入指令"):
with st.chat_message("user"): st.markdown(prompt)
with st.chat_message("assistant"):
- turns = []; cache = []; response = ''
+ slot = st.empty(); response = ''
+ CURSOR = '▌'
for response in agent_backend_stream(prompt):
- render_segments(fold_turns(response), placeholders=turns, rendered_cache=cache, suffix='▌')
- st.empty() # force Streamlit to check StopException on every iteration (incl. heartbeat)
- render_segments(fold_turns(response), placeholders=turns, rendered_cache=cache, force_text=True)
+ # 每轮整块重画(含 heartbeat 空转):segments 不变时 Streamlit diff 零变更 → 不闪烁;
+ # 而 slot.container() 调用本身保证 Streamlit 能抛 StopException(abort 生效)
+ with slot.container(): render_segments(fold_turns(response), suffix=CURSOR)
+ with slot.container(): render_segments(fold_turns(response)) # 收尾去光标
st.session_state.messages.append({"role": "assistant", "content": response})
st.session_state.last_reply_time = int(time.time())