Fix WebUI message replay ordering

Treat full log snapshots as authoritative replays by clearing the message DOM when the snapshot starts at log no 0, then render logs in backend order. This prevents streamed responses from staying above earlier welcome/user messages after sync races while keeping incremental updates patch-based.

Alessandro committed Jun 26, 2026 at 14:14 UTC a4b2848172119232f6533ad7ab73f9091dc231b5
4 files changed +32
tests/test_webui_message_ordering_static.py new
+24
@@ -0,0 +1,24 @@
1 +from pathlib import Path
2 +
3 +
4 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
5 +
6 +
7 +def read(*parts: str) -> str:
8 + return (PROJECT_ROOT / Path(*parts)).read_text(encoding="utf-8")
9 +
10 +
11 +def test_full_log_replays_replace_existing_message_dom():
12 + index_js = read("webui", "index.js")
13 + messages_js = read("webui", "js", "messages.js")
14 +
15 + assert "snapshot.logs?.[0]?.no === 0" in index_js
16 + assert 'chatHistoryEl.innerHTML = "";' in index_js
17 + assert "messages.sort((a, b)" in messages_js
18 +
19 +
20 +def test_message_ordering_fix_does_not_add_renderer_cache_state():
21 + messages_js = read("webui", "js", "messages.js")
22 +
23 + assert "_messageCacheByNo" not in messages_js
24 + assert "resetMessageRenderState" not in messages_js
webui/index.js
+4
@@ -348,6 +348,10 @@ export async function applySnapshot(snapshot, options = {}) {
348
349 if (lastLogVersion != snapshot.log_version) {
350 updated = true;
351 + if (snapshot.logs?.[0]?.no === 0) {
352 + const chatHistoryEl = document.getElementById("chat-history");
353 + if (chatHistoryEl) chatHistoryEl.innerHTML = "";
354 + }
355 await setMessages(snapshot.logs);
356 afterMessagesUpdate(snapshot.logs);
357 }
webui/js/AGENTS.md
+1
@@ -39,6 +39,7 @@
39 - Frontend extension hooks such as `confirm_dialog_after_render` and `get_tool_message_handler` must preserve their mutable context contracts.
40 - Sanitize or safely render user/model-provided HTML and markdown.
41 - Do not expose secrets in localStorage, console logs, URLs, or WebSocket payloads.
42 +- Full message snapshots that start at backend log `no` 0 must replace the current message DOM before rendering; incremental snapshots should keep patching existing messages.
43
44 ## Work Guidance
45
webui/js/messages.js
+3
@@ -137,6 +137,9 @@ export async function getMessageHandler(type) {
137 // entrypoint called from poll/WS communication, this is how all messages are rendered and updated
138 // input is raw log format
139 export async function setMessages(messages) {
140 + messages = Array.isArray(messages) ? [...messages].filter(Boolean) : [];
141 + messages.sort((a, b) => (a.no ?? Number.MAX_SAFE_INTEGER) - (b.no ?? Number.MAX_SAFE_INTEGER));
142 +
143 const context = {
144 messages,
145 history: getChatHistoryEl(),