From 9917552b4b9eaada09a7c4ffab63802e5fe6ef19 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Thu, 26 Feb 2026 10:35:20 +0800 Subject: [PATCH] fix: improve web UI stability and conversation history restore - Fix dark mode FOUC: apply theme in 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 --- agent/protocol/agent_stream.py | 2 +- bridge/agent_initializer.py | 10 ++++++++-- channel/web/chat.html | 11 ++++++++++- channel/web/static/js/console.js | 18 ++++++++++++++++-- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/agent/protocol/agent_stream.py b/agent/protocol/agent_stream.py index 19ca33b..d1b5283 100644 --- a/agent/protocol/agent_stream.py +++ b/agent/protocol/agent_stream.py @@ -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", {}) diff --git a/bridge/agent_initializer.py b/bridge/agent_initializer.py index b481d83..5154eac 100644 --- a/bridge/agent_initializer.py +++ b/bridge/agent_initializer.py @@ -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( diff --git a/channel/web/chat.html b/channel/web/chat.html index 6f28ebc..904eb45 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -43,8 +43,17 @@ } + + - +
diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js index d4a0f35..7320d7e 100644 --- a/channel/web/static/js/console.js +++ b/channel/web/static/js/console.js @@ -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 +// 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'); +});