fix: improve web UI stability and conversation history restore

- Fix dark mode FOUC: apply theme in <head> before first paint, defer
  transition-colors to post-init to avoid animated flash on load
- Fix Safari IME Enter bug: defer compositionend reset via setTimeout(0)
- Fix history scroll: use requestAnimationFrame before scrollChatToBottom
- Limit restore turns to min(6, max_turns//3) on restart
- Fix load_messages cutoff to start at turn boundary, preventing orphaned
  tool_use/tool_result pairs from being sent to the LLM
- Merge all assistant messages within one user turn into a single bubble;
  render tool_calls in history using same CSS as live SSE view
- Handle empty choices list in stream chunks
This commit is contained in:
zhayujie
2026-02-26 10:35:20 +08:00
parent 29bfbecdc9
commit 9917552b4b
4 changed files with 35 additions and 6 deletions

View File

@@ -574,7 +574,7 @@ class AgentStreamExecutor:
raise Exception(f"{error_msg} (Status: {status_code}, Code: {error_code}, Type: {error_type})")
# Parse chunk
if isinstance(chunk, dict) and "choices" in chunk:
if isinstance(chunk, dict) and chunk.get("choices"):
choice = chunk["choices"][0]
delta = choice.get("delta", {})

View File

@@ -140,13 +140,19 @@ class AgentInitializer:
try:
from agent.memory import get_conversation_store
store = get_conversation_store()
# On restore, load at most min(10, max_turns // 2) turns so that
# a long-running session does not immediately fill the context window
# after a restart. The full max_turns budget is reserved for the
# live conversation that follows.
max_turns = conf().get("agent_max_context_turns", 30)
saved = store.load_messages(session_id, max_turns=max_turns)
restore_turns = min(6, max(1, max_turns // 3))
saved = store.load_messages(session_id, max_turns=restore_turns)
if saved:
with agent.messages_lock:
agent.messages = saved
logger.info(
f"[AgentInitializer] Restored {len(saved)} messages for session={session_id}"
f"[AgentInitializer] Restored {len(saved)} messages "
f"({restore_turns} turns cap) for session={session_id}"
)
except Exception as e:
logger.warning(

View File

@@ -43,8 +43,17 @@
}
</script>
<link rel="stylesheet" href="assets/css/console.css">
<!-- Apply theme/lang before first paint to avoid flash of unstyled content.
This runs synchronously in <head> so the correct class is on <html>
before any CSS or body rendering occurs. -->
<script>
(function() {
var theme = localStorage.getItem('cow_theme') || 'dark';
if (theme === 'dark') document.documentElement.classList.add('dark');
})();
</script>
</head>
<body class="h-screen overflow-hidden bg-gray-50 dark:bg-[#111111] text-slate-800 dark:text-slate-200 font-sans transition-colors duration-200">
<body class="h-screen overflow-hidden bg-gray-50 dark:bg-[#111111] text-slate-800 dark:text-slate-200 font-sans">
<div id="app" class="flex h-screen">
<!-- ================================================================ -->

View File

@@ -284,7 +284,11 @@ const sendBtn = document.getElementById('send-btn');
const messagesDiv = document.getElementById('chat-messages');
chatInput.addEventListener('compositionstart', () => { isComposing = true; });
chatInput.addEventListener('compositionend', () => { isComposing = false; });
// Safari fires compositionend *before* the confirming keydown event, so if we
// reset isComposing synchronously the keydown handler sees !isComposing and
// sends the message prematurely. A setTimeout(0) defers the reset until after
// keydown has been processed, fixing the Safari IME Enter-to-confirm bug.
chatInput.addEventListener('compositionend', () => { setTimeout(() => { isComposing = false; }, 0); });
chatInput.addEventListener('input', function() {
this.style.height = '42px';
@@ -684,7 +688,9 @@ function loadHistory(page) {
historyPage = page;
if (isFirstLoad) {
scrollChatToBottom();
// Use requestAnimationFrame to ensure the DOM has fully rendered
// before scrolling, otherwise scrollHeight may not reflect new content.
requestAnimationFrame(() => scrollChatToBottom());
} else {
// Restore scroll position so loading older messages doesn't jump the view
messagesDiv.scrollTop = messagesDiv.scrollHeight - prevScrollHeight;
@@ -1102,3 +1108,11 @@ applyTheme();
applyI18n();
document.getElementById('sidebar-version').textContent = `CowAgent ${APP_VERSION}`;
chatInput.focus();
// Re-enable color transition AFTER first paint so the theme applied in <head>
// doesn't produce an animated flash on load. The class is missing from the
// body initially; adding it here means transitions only fire on user-triggered
// theme toggles, not on page load.
requestAnimationFrame(() => {
document.body.classList.add('transition-colors', 'duration-200');
});