preparation for refactor

frdel committed Jan 23, 2026 at 10:11 UTC 429f737786e590be4bc8cad38fc0dd7b95c30036
2 files changed +63 -136
webui/components/messages/process-group/process-group.css
+1 -1
@@ -475,7 +475,7 @@
475 -webkit-overflow-scrolling: touch; /* smooth scrolling on iOS */
476 scrollbar-width: none; /* Firefox */
477 -ms-overflow-style: none; /* IE/Edge */
478 - overscroll-behavior: contain; /* avoid scroll chaining */
478 + overscroll-behavior-x: contain; /* avoid scroll chaining */
479 }
480
481 .process-step-detail-content pre {
webui/js/messages.js
+62 -135
@@ -13,12 +13,9 @@ import { formatDuration } from "./time-utils.js";
13 // Timing Constants
14 // ============================================
15 // Delay before collapsing previous steps when a new step is added
16 -const STEP_COLLAPSE_DELAY_MS = 2000;
16 +const STEP_COLLAPSE_DELAY_MS = 3000;
17 // Delay before collapsing the last step when processing completes
18 -const FINAL_STEP_COLLAPSE_DELAY_MS = 2000;
19 -
20 -// Track active collapse timeouts for steps (key: step DOM element, value: timeout ID)
21 -const stepCollapseTimeouts = new Map();
18 +const FINAL_STEP_COLLAPSE_DELAY_MS = 3000;
19
20 const chatHistory = document.getElementById("chat-history");
21
@@ -26,6 +23,39 @@ let messageGroup = null;
23 let currentProcessGroup = null; // Track current process group for collapsible UI
24 let currentDelegationSteps = {}; // Track delegation steps by agent number for nesting
25
26 +// handlers for log message rendering
27 +export function getMessageHandler(type) {
28 + switch (type) {
29 + case "user":
30 + return drawMessageUser;
31 + case "agent":
32 + return drawMessageAgent;
33 + case "response":
34 + return drawMessageResponse;
35 + case "tool":
36 + return drawMessageTool;
37 + case "code_exe":
38 + return drawMessageCodeExe;
39 + case "browser":
40 + return drawMessageBrowser;
41 + case "warning":
42 + return drawMessageWarning;
43 + case "rate_limit":
44 + return drawMessageWarning;
45 + case "error":
46 + return drawMessageError;
47 + case "info":
48 + return drawMessageInfo;
49 + case "util":
50 + return drawMessageUtil;
51 + case "hint":
52 + return drawMessageInfo;
53 + default:
54 + return drawMessageDefault;
55 + }
56 +}
57 +
58 +
59 /**
60 * Mark a process group as the active one (via .active class)
61 */
@@ -67,16 +97,16 @@ function resolveToolName(type, kvps, stepElement) {
97 return stepElement.getAttribute('data-tool-name');
98 }
99
70 - // Inherit from previous sibling (for tool steps)
71 - if (type === 'tool' && stepElement) {
72 - let prev = stepElement.previousElementSibling;
73 - while (prev) {
74 - if (prev.hasAttribute('data-tool-name')) {
75 - return prev.getAttribute('data-tool-name');
76 - }
77 - prev = prev.previousElementSibling;
78 - }
79 - }
100 + // // Inherit from previous sibling (for tool steps)
101 + // if (type === 'tool' && stepElement) {
102 + // let prev = stepElement.previousElementSibling;
103 + // while (prev) {
104 + // if (prev.hasAttribute('data-tool-name')) {
105 + // return prev.getAttribute('data-tool-name');
106 + // }
107 + // prev = prev.previousElementSibling;
108 + // }
109 + // }
110
111 return null;
112 }
@@ -116,6 +146,7 @@ function appendMessageToHistory(messageContainer, groupType, forceNewGroup, id)
146 messageGroup.appendChild(messageContainer);
147 }
148
149 +// entrypoint called from poll/WS communication, this is how all messages are rendered and updated
150 export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
151 // Check if this is a process type message
152 const isProcessType = PROCESS_TYPES.includes(type);
@@ -175,7 +206,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
206 messageContainer.classList.add("message-container", `${sender}-container`);
207 }
208
178 - const handler = getHandler(type);
209 + const handler = getMessageHandler(type);
210 handler(messageContainer, id, type, heading, content, temp, kvps);
211
212 // If this is a new message (not yet in DOM), handle DOM insertion
@@ -207,38 +238,6 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
238 return messageContainer;
239 }
240
210 -// Legacy copy button functions removed - now using action buttons component
211 -
212 -export function getHandler(type) {
213 - switch (type) {
214 - case "user":
215 - return drawMessageUser;
216 - case "agent":
217 - return drawMessageAgent;
218 - case "response":
219 - return drawMessageResponse;
220 - case "tool":
221 - return drawMessageTool;
222 - case "code_exe":
223 - return drawMessageCodeExe;
224 - case "browser":
225 - return drawMessageBrowser;
226 - case "warning":
227 - return drawMessageWarning;
228 - case "rate_limit":
229 - return drawMessageWarning;
230 - case "error":
231 - return drawMessageError;
232 - case "info":
233 - return drawMessageInfo;
234 - case "util":
235 - return drawMessageUtil;
236 - case "hint":
237 - return drawMessageInfo;
238 - default:
239 - return drawMessageDefault;
240 - }
241 -}
241
242 // draw a message with a specific type
243 export function _drawMessage(
@@ -923,81 +922,6 @@ export function drawMessageError(
922 messageContainer.classList.add("center-container");
923 }
924
926 -function drawKvps(container, kvps, latex) {
927 - if (kvps) {
928 - const table = document.createElement("table");
929 - table.classList.add("msg-kvps");
930 - for (let [key, value] of Object.entries(kvps)) {
931 - const row = table.insertRow();
932 - row.classList.add("kvps-row");
933 - // Skip reasoning
934 - if (key === "reasoning") continue;
935 - if (key === "thoughts")
936 - // TODO: find a better way to determine special class assignment
937 - row.classList.add("msg-thoughts");
938 -
939 - const th = row.insertCell();
940 - th.textContent = convertToTitleCase(key);
941 - th.classList.add("kvps-key");
942 -
943 - const td = row.insertCell();
944 - const tdiv = document.createElement("div");
945 - tdiv.classList.add("kvps-val");
946 - td.appendChild(tdiv);
947 -
948 - if (Array.isArray(value)) {
949 - for (const item of value) {
950 - addValue(item);
951 - }
952 - } else {
953 - addValue(value);
954 - }
955 -
956 - // addActionButtonsToElement(tdiv);
957 -
958 - // autoscroll the KVP value if needed
959 - // if (getAutoScroll()) #TODO needs a better redraw system
960 - setTimeout(() => {
961 - tdiv.scrollTop = tdiv.scrollHeight;
962 - }, 0);
963 -
964 - function addValue(value) {
965 - if (typeof value === "object") value = JSON.stringify(value, null, 2);
966 -
967 - if (typeof value === "string" && value.startsWith("img://")) {
968 - const imgElement = document.createElement("img");
969 - imgElement.classList.add("kvps-img");
970 - imgElement.src = value.replace("img://", "/image_get?path=");
971 - imgElement.alt = "Image Attachment";
972 - tdiv.appendChild(imgElement);
973 -
974 - // Add click handler and cursor change
975 - imgElement.style.cursor = "pointer";
976 - imgElement.addEventListener("click", () => {
977 - openImageModal(imgElement.src, 1000);
978 - });
979 - } else {
980 - const pre = document.createElement("pre");
981 - const span = document.createElement("span");
982 - span.innerHTML = convertHTML(value);
983 - pre.appendChild(span);
984 - tdiv.appendChild(pre);
985 -
986 - // KaTeX rendering for markdown
987 - if (latex) {
988 - span.querySelectorAll("latex").forEach((element) => {
989 - katex.render(element.innerHTML, element, {
990 - throwOnError: false,
991 - });
992 - });
993 - }
994 - }
995 - }
996 - }
997 - container.appendChild(table);
998 - }
999 -}
1000 -
925 function drawKvpsIncremental(container, kvps, latex) {
926 if (kvps) {
927 // Find existing table or create new one
@@ -1372,30 +1296,32 @@ function getNestedContainer(parentStep) {
1296 * Automatically handles cancellation on click and reset on hover
1297 */
1298 function scheduleStepCollapse(stepElement, delayMs) {
1375 - // Cancel any existing timeout for this step
1376 - cancelStepCollapse(stepElement);
1299 + // skip if any existing timeout for this step
1300 + if (stepElement.hasAttribute("data-collapse-timeout-id")) {
1301 + return;
1302 + }
1303
1304 // Schedule the collapse
1305 const timeoutId = setTimeout(() => {
1306 stepElement.classList.remove("step-expanded");
1381 - stepCollapseTimeouts.delete(stepElement);
1307 + stepElement.removeAttribute("data-collapse-timeout-id");
1308 // Clear user-pinned flag when auto-collapsing
1309 stepElement.removeAttribute("data-user-pinned");
1310 }, delayMs);
1385 -
1311 +
1312 // Store the timeout ID
1387 - stepCollapseTimeouts.set(stepElement, timeoutId);
1313 + stepElement.setAttribute("data-collapse-timeout-id", String(timeoutId));
1314 }
1315
1316 /**
1317 * Cancel a scheduled collapse for a step
1318 */
1319 function cancelStepCollapse(stepElement) {
1394 - const timeoutId = stepCollapseTimeouts.get(stepElement);
1395 - if (timeoutId) {
1396 - clearTimeout(timeoutId);
1397 - stepCollapseTimeouts.delete(stepElement);
1398 - }
1320 + const timeoutIdStr = stepElement.getAttribute("data-collapse-timeout-id");
1321 + if (!timeoutIdStr) return;
1322 + const timeoutId = Number(timeoutIdStr);
1323 + if (!Number.isNaN(timeoutId)) clearTimeout(timeoutId);
1324 + stepElement.removeAttribute("data-collapse-timeout-id");
1325 }
1326
1327 /**
@@ -2298,8 +2224,9 @@ export function resetProcessGroups() {
2224 clearActiveStepShine();
2225
2226 // Clear all pending collapse timeouts
2301 - stepCollapseTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
2302 - stepCollapseTimeouts.clear();
2227 + document
2228 + .querySelectorAll('.process-step[data-collapse-timeout-id]')
2229 + .forEach((el) => cancelStepCollapse(el));
2230 }
2231
2232 /**