fix(webui): keep replayed warnings in process groups

Prefer cached process render metadata over mutable DOM-tail state when re-rendering warnings. Add focused regression coverage and document the replay contract.

Alessandro committed Aug 1, 2026 at 14:21 UTC 52421df9ec5b18f1a1a674ba8c9199a9ab28aac2
3 files changed +18 -3
tests/test_webui_message_window.py
+12
@@ -184,6 +184,18 @@ assert(windowed.newerCount === 1, "an unfollowed live append must remain availab
184 )
185
186
187 +def test_warning_replay_prefers_classified_process_group():
188 + messages = (PROJECT_ROOT / "webui" / "js" / "messages.js").read_text(
189 + encoding="utf-8"
190 + )
191 + warning_handler = messages.split(
192 + "export function drawMessageWarning", maxsplit=1
193 + )[1].split("export function drawMessageError", maxsplit=1)[0]
194 +
195 + assert "arguments[0][PROCESS_GROUP_RENDER_INFO]" in warning_handler
196 + assert "getLastProcessGroup(false)" in warning_handler
197 +
198 +
199 def test_collapsed_process_details_are_deferred_and_discarded():
200 messages = (PROJECT_ROOT / "webui" / "js" / "messages.js").read_text(
201 encoding="utf-8"
webui/js/AGENTS.md
+1
@@ -56,6 +56,7 @@
56 - Long histories stay cached as raw log data but render a contiguous tail-first DOM window. The initial base view contains one 60-entry page; after paging, the base window contains two aligned pages, retaining the adjacent page and discarding only the far page in either direction. Visible boundaries expand to whole logical process groups so a page never reconstructs a partial group; the unit classifier must include plugin-backed process steps such as `code_exe`, and oversized groups use their own 50-step incremental window. Paging must preserve a visible anchor and occur at the scroll boundary after user intent, using passive loading indicators rather than count-bearing controls. Live entries and late content growth follow the tail until the reader deliberately moves away; historical window rebuilds must cancel pending auto-scroll effects, render in an off-screen staging history, and atomically swap fully laid-out content into the live scroller before restoring its anchor.
57 - Message-window cache identity must keep different log types distinct even when they share a backend ID; root-agent GEN and response records intentionally use the same ID and must both survive replay, while same-ID/same-type updates still replace their earlier cached version.
58 - Utility records join a process render unit only when a substantive process step follows before the next standalone boundary. Group visibility must use that full-log classification rather than infer utility-only state from partially mounted DOM children. Standalone utility-only runs must not wrap root responses or reopen completed groups, and their group chrome stays hidden unless utility messages are enabled.
59 +- Warnings classified into a full-log process render unit must remain process steps during replay even when the current DOM tail is already complete; use the live DOM tail only when render metadata is unavailable.
60 - `set_messages_after_loop` receives offscreen live updates as results with `result.virtualized === true` and `result.element === null`; extensions that only need `args` may still react, while DOM-mutating extensions must guard the element.
61 - Context switches, log GUID resets, and full log snapshots must reset both message DOM and message-window cache state.
62 - Context switches clear stale history immediately but defer the chat loading splash for 300 ms so fast loads do not flash; slower loads fade it in and dismiss it only after the matching context snapshot finishes rendering. Stale snapshots and timers must not affect a newer switch's splash.
webui/js/messages.js
+5 -3
@@ -2534,9 +2534,11 @@ export function drawMessageWarning({
2534 ].filter(Boolean)
2535 : [];
2536
2537 - //if process group is running, append there
2538 - const group = getLastProcessGroup(false);
2539 - if (group) {
2537 + // Keep replayed warnings in their classified process group.
2538 + if (
2539 + arguments[0][PROCESS_GROUP_RENDER_INFO] ||
2540 + getLastProcessGroup(false)
2541 + ) {
2542 return drawProcessStep({
2543 id,
2544 title,