\s*((?:(?!).)*?)\s*', _c, re.DOTALL)
if matches:
title = matches[0].strip()
title = title.split('\n')[0]
if len(title) > 50: title = title[:50] + '...'
else: title = marker.strip('*')
segments.append({'type': 'fold', 'title': title, 'content': content})
else: segments.append({'type': 'text', 'content': marker + content})
return segments
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 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")
response = ''
try:
while True:
try: item = display_queue.get(timeout=1)
except queue.Empty:
yield response # heartbeat: let outer st.markdown() run → Streamlit checks StopException
continue
if 'next' in item:
response = item['next']; yield response
if 'done' in item:
yield item['done']; break
finally: agent.abort()
if "messages" not in st.session_state: st.session_state.messages = []
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
# 用 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':
import streamlit.components.v1 as components
components.html('', height=0)
if prompt := st.chat_input("请输入指令"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"): st.markdown(prompt)
with st.chat_message("assistant"):
slot = st.empty(); response = ''
CURSOR = '▌'
for response in agent_backend_stream(prompt):
# 每轮整块重画(含 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())
if st.session_state.autonomous_enabled:
st.markdown(f"""{st.session_state.get('last_reply_time', int(time.time()))}
""", unsafe_allow_html=True)