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
/**