UI fixes, cleanups

frdel committed Jan 28, 2026 at 15:53 UTC dd717dc8cc73a1bb388467071b952833dc2e002d
5 files changed +66 -627
webui/components/messages/process-group/process-group.css
+2 -1
@@ -493,6 +493,7 @@
493 .process-step.expanded > .process-step-detail > .process-step-detail-scroll {
494 max-height: 40em;
495 overflow-y: auto;
496 + border-radius: var(--border-radius-sm);
497 }
498
499 .process-step.expanded > .process-step-detail > .process-step-detail-scroll > .process-step-detail-scroll {
@@ -625,7 +626,7 @@
626 margin: var(--spacing-xs) 0 0 0;
627 padding: var(--spacing-xs);
628 background: rgba(0, 0, 0, 0.8);
628 - border-radius: 8px;
629 + border-radius: var(--border-radius-sm);
630 color: #c9d1d9;
631 white-space: pre;
632 font-family: monospace;
webui/css/messages.css
+9 -10
@@ -10,18 +10,14 @@
10 overflow-y: scroll;
11 overflow-x: hidden;
12 scroll-behavior: auto !important; /* avoid infinite scrolling! */
13 - /* padding: var(--spacing-md) 10em 0 !important; */
13 + padding-left: var(--spacing-sm) !important;
14 + padding-right: var(--spacing-md) !important;
15 + padding-top: 4rem !important;
16 + padding-bottom: 4rem !important;
17 -webkit-transition: all 0.3s ease;
18 transition: all 0.3s ease;
19 scrollbar-width: thin;
20 scrollbar-color: #555 transparent;
18 - padding-left: var(--spacing-sm);
19 - padding-right: var(--spacing-sm);
20 - padding-bottom: 5em;
21 -}
22 -
23 -#chat-history > *:first-child {
24 - margin-top: 4.4em;
21 }
22
23 /* Scrollbar styling for Firefox */
@@ -577,8 +573,11 @@
573
574 @media (max-width: 1024px) {
575 #chat-history {
580 - padding: var(--spacing-md) var(--spacing-sm) 0 !important;
581 - }
576 + padding-left: var(--spacing-sm) !important;
577 + padding-right: var(--spacing-md) !important;
578 + padding-top: 4rem !important;
579 + padding-bottom: 2rem !important;
580 +}
581 }
582
583 @media (max-width: 640px) {
webui/index.css
+1 -24
@@ -81,6 +81,7 @@
81
82 /* Other variables */
83 --border-radius: 1.125rem;
84 + --border-radius-sm: 0.5rem;
85 --transition-speed: 0.3s;
86 --svg-filter: brightness(0) saturate(100%) var(--color-primary-filter);
87 --color-primary-filter: invert(73%) sepia(17%) saturate(360%)
@@ -739,30 +740,6 @@ h4 {
740 padding: 0 !important;
741 }
742
742 -/* #chat-history .attachment-item {
743 - padding: 0 !important;
744 - background-color: transparent !important;
745 - border: none !important;
746 - box-shadow: none !important;
747 - transform: none !important;
748 -}
749 -
750 -#chat-history .attachment-item:hover {
751 - background-color: transparent !important;
752 - border: none !important;
753 - box-shadow: none !important;
754 - transform: none !important;
755 -}
756 -
757 -#chat-history .attachment-preview,
758 -#chat-history .attachment-item.image-type .attachment-preview {
759 - background: transparent;
760 -}
761 -
762 -.attachment-image .attachment-preview {
763 - margin-right: 0;
764 -} */
765 -
743 .attachment-info,
744 .file-info {
745 display: flex;
webui/index.js
-2
@@ -459,8 +459,6 @@ function setProgressBarShine(progressBarEl, active) {
459 if (!progressBarEl) return;
460 if (!active) {
461 removeClassFromElement(progressBarEl, "shiny-text");
462 - // clear any lingering shines in process steps
463 - msgs.clearActiveStepShine();
462 } else {
463 addClassToElement(progressBarEl, "shiny-text");
464 }
webui/js/messages.js
+54 -590
@@ -9,73 +9,15 @@ import { store as stepDetailStore } from "/components/modals/process-step-detail
9 import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
10 import { formatDuration } from "./time-utils.js";
11
12 -// ============================================
13 -// Timing Constants
14 -// ============================================
12 // Delay before collapsing previous steps when a new step is added
13 const STEP_COLLAPSE_DELAY_MS = 3000;
14 // Delay before collapsing the last step when processing completes
15 const FINAL_STEP_COLLAPSE_DELAY_MS = 3000;
16
20 -// // Tool-specific status codes (fallback for tool steps)
21 -// const TOOL_STATUS_CODES = {
22 -// call_subordinate: "SUB",
23 -// search_engine: "WEB",
24 -// a2a_chat: "A2A",
25 -// behaviour_adjustment: "ADJ",
26 -// document_query: "DOC",
27 -// vision_load: "EYE",
28 -// notify_user: "NTF",
29 -// scheduler: "SCH",
30 -// unknown: "UNK",
31 -// memory_save: "MEM",
32 -// memory_load: "MEM",
33 -// memory_forget: "MEM",
34 -// memory_delete: "MEM",
35 -// };
36 -
37 -// Tool-specific status classes (fallback for tool steps)
38 -// const TOOL_STATUS_CLASSES = {
39 -// call_subordinate: "status-sub",
40 -// };
41 -
42 -const TYPE_STATUS_CODES = {
43 - // agent: "GEN",
44 - response: "END",
45 - // tool: "USE",
46 - // code_exe: "EXE",
47 - // browser: "WWW",
48 - // progress: "HLD",
49 - // mcp: "MCP",
50 - // subagent: "SUB",
51 - info: "INF",
52 - hint: "HNT",
53 - // warning: "WRN",
54 - rate_limit: "WRN",
55 - // error: "ERR",
56 - // util: "UTL",
57 - done: "END",
58 -};
59 -
60 -const TYPE_STATUS_CLASSES = {
61 - // agent: "status-gen",
62 - response: "status-end",
63 - // tool: "status-tool",
64 - // code_exe: "status-exe",
65 - // browser: "status-www",
66 - // progress: "status-wait",
67 - // mcp: "status-mcp",
68 - // subagent: "status-sub",
69 - info: "status-inf",
70 - hint: "status-hnt",
71 - // warning: "status-wrn",
72 - rate_limit: "status-wrn",
73 - // error: "status-err",
74 - // util: "status-utl",
75 - done: "status-end",
76 -};
77 -
17 +// dom references
18 let _chatHistory = null;
19 +
20 +// state vars
21 let _massRender = false;
22
23 // handlers for log message rendering
@@ -120,34 +62,25 @@ export function getMessageHandler(type) {
62 * Mark a process group as the active one (via .active class)
63 */
64 function setActiveProcessGroup(group) {
123 - if (!group) return;
124 -
125 - // Already active? Nothing to do
126 - if (group.classList.contains("active")) return;
127 -
128 - // Clear active + shiny from all other groups
129 - getChatHistoryEl()
130 - .querySelectorAll(".process-group.active")
131 - .forEach((g) => {
132 - if (g !== group) {
133 - g.classList.remove("active");
134 - g.querySelectorAll(".step-title.shiny-text").forEach((el) =>
135 - el.classList.remove("shiny-text"),
136 - );
137 - }
138 - });
139 -
140 - // Mark this group as active
141 - group.classList.add("active");
142 -}
65 + // if (!group) return;
66 +
67 + // // Already active? Nothing to do
68 + // if (group.classList.contains("active")) return;
69 +
70 + // // Clear active + shiny from all other groups
71 + // getChatHistoryEl()
72 + // .querySelectorAll(".process-group.active")
73 + // .forEach((g) => {
74 + // if (g !== group) {
75 + // g.classList.remove("active");
76 + // g.querySelectorAll(".step-title.shiny-text").forEach((el) =>
77 + // el.classList.remove("shiny-text"),
78 + // );
79 + // }
80 + // });
81
144 -export function clearActiveStepShine() {
145 - // Clear all shiny step titles in process steps
146 - getChatHistoryEl()
147 - .querySelectorAll(".process-step .step-title.shiny-text")
148 - .forEach((el) => {
149 - el.classList.remove("shiny-text");
150 - });
82 + // // Mark this group as active
83 + // group.classList.add("active");
84 }
85
86 function getChatHistoryEl() {
@@ -393,7 +326,7 @@ function drawProcessStep({
326 let step = document.getElementById(stepId);
327
328 const isNewStep = !step;
396 - const isGroupCompleted = group.classList.contains("process-group-completed");
329 + const isGroupComplete = isProcessGroupComplete(group);
330
331 if (isNewStep) {
332 // create the base DOM element for the step
@@ -409,25 +342,6 @@ function drawProcessStep({
342 // apply step classes
343 if (classes) step.classList.add(...classes);
344
412 - // if (toolName) {
413 - // step.setAttribute("data-tool-name", toolName);
414 - // }
415 - // timestamp data
416 - // if (log.timestamp) {
417 - // step.setAttribute("data-timestamp", log.timestamp);
418 -
419 - // if (!group.getAttribute("data-start-timestamp")) {
420 - // group.setAttribute("data-start-timestamp", log.timestamp);
421 - // const timeMetricEl = group.querySelector(".metric-time .metric-value");
422 - // if (timeMetricEl) {
423 - // const date = new Date(parseFloat(timestamp) * 1000);
424 - // const hours = String(date.getHours()).padStart(2, "0");
425 - // const minutes = String(date.getMinutes()).padStart(2, "0");
426 - // timeMetricEl.textContent = `${hours}:${minutes}`;
427 - // }
428 - // }
429 - // }
430 -
345 let appendTarget = stepsContainer;
346 const parentStep = findParentDelegationStep(group, log.agentno);
347 if (parentStep) {
@@ -450,24 +364,22 @@ function drawProcessStep({
364
365 // expand all or current step based on settings
366 const detailMode = preferencesStore.detailMode;
453 - const isActiveGroup = group.classList.contains("active");
367 + // const isActiveGroup = group.classList.contains("active");
368
369 //expand all
370 if (detailMode === "expanded") {
371 step.classList.add("expanded");
372 // expand current step and schedule collapse of previous
459 - } else if (detailMode === "current") {
460 - if (isActiveGroup && !isGroupCompleted) {
461 - step.classList.add("expanded");
462 - const allExpandedSteps = stepsContainer.querySelectorAll(
463 - ".process-step.expanded",
464 - );
465 - allExpandedSteps.forEach((expandedStep) => {
466 - if (expandedStep.id !== stepId) {
373 + } else if (detailMode === "current" && !isMassRender() && !isGroupComplete) {
374 + step.classList.add("expanded");
375 + const allExpandedSteps = stepsContainer.querySelectorAll(
376 + ".process-step.expanded",
377 + );
378 + allExpandedSteps.forEach((expandedStep) => {
379 + if (expandedStep.id !== stepId) {
380 scheduleStepCollapse(expandedStep, STEP_COLLAPSE_DELAY_MS);
381 }
382 });
470 - }
383 }
384 }
385
@@ -496,20 +408,7 @@ function drawProcessStep({
408 "process-step-detail-scroll",
409 );
410
499 - // else {
500 - // if (timestamp && !step.hasAttribute("data-timestamp")) {
501 - // step.setAttribute("data-timestamp", timestamp);
502 - // }
503 - // if (agentno !== undefined) {
504 - // step.setAttribute("data-agent-number", agentno);
505 - // }
506 - // }
507 -
508 - // const toolNameToUse = resolveToolName(type, kvps, step) || toolName;
509 - // if (toolNameToUse) {
510 - // step.setAttribute("data-tool-name", toolNameToUse);
511 - // }
512 -
411 + // expand/collapse handler for step header
412 if (!stepHeader.hasAttribute("data-expand-handler")) {
413 stepHeader.setAttribute("data-expand-handler", "true");
414 stepHeader.addEventListener("click", (e) => {
@@ -544,39 +443,7 @@ function drawProcessStep({
443 const titleEl = ensureChild(stepHeader, ".step-title", "span", "step-title");
444 titleEl.textContent = title;
445
547 - // const detail = ensureChild(step, ".process-step-detail", "div", "process-step-detail");
548 - // const detailContent = ensureChild(
549 - // detail,
550 - // ".process-step-detail-content",
551 - // "div",
552 - // "process-step-detail-content",
553 - // );
554 -
555 - // let skipFullRender = false;
556 -
557 - // const terminal = stepDetailContent.querySelector(".terminal-output");
558 - // const scroller = terminal ? new Scroller(terminal) : null;
559 -
560 - // if (type === "browser" && kvps?.screenshot) {
561 - // const existingImg = detailContent.querySelector(".screenshot-img");
562 - // const newSrc = kvps.screenshot.replace("img://", "/image_get?path=");
563 - // if (existingImg) {
564 - // if (!existingImg.src.endsWith(newSrc.split("?path=")[1])) {
565 - // existingImg.src = newSrc;
566 - // }
567 - // skipFullRender = true;
568 - // }
569 - // }
570 -
571 - // if (!skipFullRender) {
572 - // renderStepDetailContent(detailContent, content, kvps, type);
573 -
574 - // const newTerminal = detailContent.querySelector(".terminal-output");
575 - // if (newTerminal && scroller?.wasAtBottom) {
576 - // newTerminal.scrollTop = newTerminal.scrollHeight;
577 - // }
578 - // }
579 -
446 + // auto-scroller of the step detail
447 const detailScroller = new Scroller(stepDetailScroll); // scroller for step detail content
448
449 // update KVPs of the step detail
@@ -592,14 +459,9 @@ function drawProcessStep({
459 );
460 stepDetailContent.textContent = content;
461
595 - // const detailDataToUse =
596 - // detailData ||
597 - // buildDetailPayload({
598 - // ...stepData,
599 - // toolName: toolNameToUse,
600 - // statusCode: resolvedStatusCode,
601 - // statusClass: resolvedStatusClass,
602 - // });
462 + // reapply scroll position (autoscroll if bottom) - only when expanded already and not mass rendering
463 + if (isExpanded && !isMassRender()) detailScroller.reApplyScroll();
464 +
465
466 // Render action buttons: get/create container, clear, append
467 const stepActionBtns = ensureChild(
@@ -614,11 +476,15 @@ function drawProcessStep({
476 .filter(Boolean)
477 .forEach((button) => stepActionBtns.appendChild(button));
478
617 - if (isExpanded && !isMassRender()) detailScroller.reApplyScroll(); // reapply scroll position (autoscroll if bottom) - only when expanded already and not
479
480 + // update the process grop header by this step
481 updateProcessGroupHeader(group);
482
621 - if (isNewStep && !isGroupCompleted) {
483 + // remove shine from previous steps and add to this one if new and not completed
484 + if (isNewStep && !isGroupComplete) {
485 + stepDetailScroll.querySelectorAll(".step-title.shiny-text").forEach((el) => {
486 + el.classList.remove("shiny-text");
487 + });
488 titleEl.classList.add("shiny-text");
489 }
490
@@ -1553,137 +1419,6 @@ export function drawMessageError({
1419 });
1420 }
1421
1556 -// export function drawMessageError({
1557 -// id,
1558 -// heading,
1559 -// content,
1560 -// kvps = null,
1561 -// ...additional
1562 -// }) {
1563 -// const messageContainer = getOrCreateMessageContainer(id, "mid", [
1564 -// "ai-container",
1565 -// "center-container",
1566 -// ]);
1567 -
1568 -// // Create or get the message div
1569 -// let messageDiv = messageContainer.querySelector(".message");
1570 -// if (!messageDiv) {
1571 -// messageDiv = document.createElement("div");
1572 -// messageDiv.classList.add("message", "message-error-group");
1573 -// messageContainer.appendChild(messageDiv);
1574 -// }
1575 -
1576 -// // Check if error group already exists
1577 -// let errorGroup = messageDiv.querySelector(".error-group");
1578 -// if (!errorGroup) {
1579 -// errorGroup = document.createElement("div");
1580 -// errorGroup.classList.add("error-group");
1581 -// errorGroup.setAttribute("data-error-id", id);
1582 -
1583 -// // Create header (clickable for expand/collapse)
1584 -// const header = document.createElement("div");
1585 -// header.classList.add("error-group-header");
1586 -
1587 -// // Expand icon (triangle)
1588 -// const expandIcon = document.createElement("span");
1589 -// expandIcon.classList.add("expand-icon");
1590 -// header.appendChild(expandIcon);
1591 -
1592 -// // Status badge (before title)
1593 -// const badge = document.createElement("span");
1594 -// badge.classList.add("step-badge", "status-err");
1595 -// badge.textContent = "ERR";
1596 -// header.appendChild(badge);
1597 -
1598 -// // Title
1599 -// const title = document.createElement("span");
1600 -// title.classList.add("error-title");
1601 -// title.textContent = "Error";
1602 -// header.appendChild(title);
1603 -
1604 -// // Subtitle (short error description)
1605 -// const subtitle = document.createElement("span");
1606 -// subtitle.classList.add("error-subtitle");
1607 -// header.appendChild(subtitle);
1608 -
1609 -// // Click handler for expand/collapse
1610 -// header.addEventListener("click", () => {
1611 -// errorGroup.classList.toggle("expanded");
1612 -// });
1613 -
1614 -// errorGroup.appendChild(header);
1615 -
1616 -// // Create content container (collapsible)
1617 -// const contentWrapper = document.createElement("div");
1618 -// contentWrapper.classList.add("error-group-content");
1619 -
1620 -// const contentInner = document.createElement("div");
1621 -// contentInner.classList.add("error-content-inner");
1622 -// contentWrapper.appendChild(contentInner);
1623 -
1624 -// errorGroup.appendChild(contentWrapper);
1625 -// messageDiv.appendChild(errorGroup);
1626 -
1627 -// // Check detail mode and expand if needed
1628 -// const detailMode = preferencesStore.detailMode || "current";
1629 -// if (detailMode === "current" || detailMode === "expanded") {
1630 -// errorGroup.classList.add("expanded");
1631 -// }
1632 -// }
1633 -
1634 -// // Update subtitle with short error description
1635 -// const subtitle = errorGroup.querySelector(".error-subtitle");
1636 -// if (subtitle) {
1637 -// // Extract short description from heading or content
1638 -// let shortDesc = "";
1639 -// // Skip if heading is just "Error" (redundant with title)
1640 -// if (heading && heading.trim() && heading.trim().toLowerCase() !== "error") {
1641 -// shortDesc = heading.trim();
1642 -// }
1643 -// // If no useful heading, try to extract from content
1644 -// if (!shortDesc && content && content.trim()) {
1645 -// const lines = content.trim().split("\n");
1646 -// // Look for the error line (usually last meaningful line or one matching ErrorType: pattern)
1647 -// for (let i = lines.length - 1; i >= 0; i--) {
1648 -// const line = lines[i].trim();
1649 -// if (line && /^[\w\.]+Error[:\s]/.test(line)) {
1650 -// shortDesc = line;
1651 -// break;
1652 -// }
1653 -// }
1654 -// // Fallback to first non-empty line if no error pattern found
1655 -// if (!shortDesc) {
1656 -// for (const line of lines) {
1657 -// if (line.trim() && !line.startsWith("Traceback")) {
1658 -// shortDesc = line.trim();
1659 -// break;
1660 -// }
1661 -// }
1662 -// }
1663 -// }
1664 -// // Truncate if too long
1665 -// if (shortDesc.length > 100) {
1666 -// shortDesc = shortDesc.substring(0, 97) + "...";
1667 -// }
1668 -// subtitle.textContent = shortDesc;
1669 -// subtitle.title = shortDesc; // Full text on hover
1670 -// }
1671 -
1672 -// // Update content (full callstack)
1673 -// const contentInner = errorGroup.querySelector(".error-content-inner");
1674 -// if (contentInner && content) {
1675 -// contentInner.innerHTML = "";
1676 -
1677 -// // Create pre element for callstack/content
1678 -// const pre = document.createElement("pre");
1679 -// pre.classList.add("error-callstack");
1680 -// pre.textContent = content;
1681 -// contentInner.appendChild(pre);
1682 -
1683 -// }
1684 -
1685 -// messageContainer.classList.add("center-container");
1686 -// }
1422
1423 function drawKvpsIncremental(container, kvps, latex) {
1424 // existing KVPS table
@@ -1809,27 +1544,6 @@ function convertToTitleCase(str) {
1544 });
1545 }
1546
1812 -/**
1813 - * Clean text value by removing standalone bracket lines and trimming
1814 - * Handles both strings and arrays (filters out bracket-only items)
1815 - */
1816 -// function cleanTextValue(value) {
1817 -// if (Array.isArray(value)) {
1818 -// return value
1819 -// .filter(
1820 -// (item) =>
1821 -// item && String(item).trim() && !/^[\[\]]$/.test(String(item).trim()),
1822 -// )
1823 -// .join("\n");
1824 -// }
1825 -// if (typeof value === "object" && value !== null) {
1826 -// return JSON.stringify(value, null, 2);
1827 -// }
1828 -// return String(value)
1829 -// .replace(/^\s*[\[\]]\s*$/gm, "")
1830 -// .trim();
1831 -// }
1832 -
1547 function convertImageTags(content) {
1548 // Regular expression to match <image> tags and extract base64 content
1549 const imageTagRegex = /<image>(.*?)<\/image>/g;
@@ -2173,15 +1887,6 @@ function findParentDelegationStep(group, agentno) {
1887 * Get a concise title for a process step
1888 */
1889 function getStepTitle(heading, kvps, type) {
2176 - // code_exe: show command when finished
2177 - // const showCommand =
2178 - // type === "code_exe" &&
2179 - // kvps?.code &&
2180 - // /done_all|code_execution_tool/.test(heading || "");
2181 - // if (showCommand) {
2182 - // const s = kvps.session ?? kvps.Session;
2183 - // return `${s != null ? `[${s}] ` : ""}${kvps.runtime || "bash"}> ${kvps.code.trim()}`;
2184 - // }
1890
1891 // Try to get a meaningful title from heading or kvps
1892 if (heading && heading.trim()) {
@@ -2243,222 +1948,6 @@ function cleanStepTitle(text, maxLength = 100) {
1948 return truncateText(cleaned, maxLength);
1949 }
1950
2246 -/**
2247 - * Render content for step detail panel
2248 - */
2249 -// function renderStepDetailContent(container, content, kvps, type = null) {
2250 -// container.innerHTML = "";
2251 -
2252 -// drawKvpsIncremental(container, kvps);
2253 -
2254 -// // Special handling for response type - show content as markdown (for subordinate responses)
2255 -// if (type === "response" && content && content.trim()) {
2256 -// const responseDiv = document.createElement("div");
2257 -// responseDiv.classList.add("step-response-content");
2258 -
2259 -// // Parse markdown
2260 -// let processedContent = content;
2261 -// processedContent = convertImageTags(processedContent);
2262 -// processedContent = convertImgFilePaths(processedContent);
2263 -// processedContent = convertFilePaths(processedContent);
2264 -// processedContent = marked.parse(processedContent, { breaks: true });
2265 -// processedContent = convertPathsToLinks(processedContent);
2266 -// processedContent = addBlankTargetsToLinks(processedContent);
2267 -
2268 -// responseDiv.innerHTML = processedContent;
2269 -// container.appendChild(responseDiv);
2270 -// return;
2271 -// }
2272 -
2273 -// // Special handling for warning/error types - always show content prominently
2274 -// if ((type === "warning" || type === "error") && content && content.trim()) {
2275 -// const warningDiv = document.createElement("div");
2276 -// warningDiv.classList.add("step-warning-content");
2277 -// warningDiv.textContent = content;
2278 -// container.appendChild(warningDiv);
2279 -// // Don't return - also show kvps if present
2280 -// }
2281 -
2282 -// // Special handling for code_exe type - render as terminal-style output
2283 -// if (type === "code_exe" && kvps) {
2284 -// const runtime = kvps.runtime || kvps.Runtime || "bash";
2285 -// const code = kvps.code || kvps.Code || "";
2286 -// const output = content || "";
2287 -
2288 -// if (code || output) {
2289 -// const terminalDiv = document.createElement("div");
2290 -// terminalDiv.classList.add("step-terminal");
2291 -
2292 -// // Show output if present (no truncation - CSS handles max-height)
2293 -// if (output && output.trim()) {
2294 -// const outputPre = document.createElement("pre");
2295 -// outputPre.classList.add("terminal-output");
2296 -// // Escape HTML first, then convert paths to clickable links
2297 -// let processedOutput = escapeHTML(output);
2298 -// processedOutput = convertPathsToLinks(processedOutput);
2299 -// outputPre.innerHTML = processedOutput;
2300 -// terminalDiv.appendChild(outputPre);
2301 -// }
2302 -
2303 -// container.appendChild(terminalDiv);
2304 -// }
2305 -
2306 -// // Still render thoughts if present (but not reasoning - that's native model thinking, not structured output)
2307 -// if (kvps.thoughts || kvps.thinking) {
2308 -// const thoughtKey = kvps.thoughts ? "thoughts" : "thinking";
2309 -// const thoughtValue = kvps[thoughtKey];
2310 -// renderThoughts(container, thoughtValue);
2311 -// }
2312 -
2313 -// return;
2314 -// }
2315 -
2316 -// // Add KVPs if present
2317 -// if (kvps && Object.keys(kvps).length > 0) {
2318 -// const kvpsDiv = document.createElement("div");
2319 -// kvpsDiv.classList.add("step-kvps");
2320 -
2321 -// for (const [key, value] of Object.entries(kvps)) {
2322 -// // Skip internal/display keys
2323 -// if (key === "finished" || key === "attachments") continue;
2324 -
2325 -// // Skip code_exe specific keys that we handle specially above
2326 -// if (
2327 -// type === "code_exe" &&
2328 -// (key.toLowerCase() === "runtime" ||
2329 -// key.toLowerCase() === "session" ||
2330 -// key.toLowerCase() === "code")
2331 -// ) {
2332 -// continue;
2333 -// }
2334 -
2335 -// const lowerKey = key.toLowerCase();
2336 -
2337 -// // Skip headline and tool_name - they're shown elsewhere
2338 -// if (lowerKey === "headline" || lowerKey === "tool_name") continue;
2339 -
2340 -// // Skip query in agent steps - it's shown in the tool call step
2341 -// if (type === "agent" && lowerKey === "query") continue;
2342 -
2343 -// // Special handling for thoughts - render with single lightbulb icon
2344 -// // Skip reasoning
2345 -// if (lowerKey === "reasoning") continue;
2346 -// if (
2347 -// lowerKey === "thoughts" ||
2348 -// lowerKey === "thinking" ||
2349 -// lowerKey === "reflection"
2350 -// ) {
2351 -// renderThoughts(kvpsDiv, value);
2352 -// continue;
2353 -// }
2354 -
2355 -// // Special handling for tool_args - render only for tool/mcp types (skip for agent)
2356 -// if (lowerKey === "tool_args") {
2357 -// // Skip tool_args for agent steps - it's shown in the tool call step
2358 -// if (type === "agent") continue;
2359 -
2360 -// if (typeof value !== "object" || value === null) continue;
2361 -// const argsDiv = document.createElement("div");
2362 -// argsDiv.classList.add("step-tool-args");
2363 -
2364 -// for (const [argKey, argValue] of Object.entries(value)) {
2365 -// const argRow = document.createElement("div");
2366 -// argRow.classList.add("tool-arg-row");
2367 -
2368 -// const argLabel = document.createElement("span");
2369 -// argLabel.classList.add("tool-arg-label");
2370 -
2371 -// const iconName = extractIconFromKey(argKey);
2372 -// if (iconName) {
2373 -// argLabel.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
2374 -// } else {
2375 -// argLabel.textContent = convertToTitleCase(argKey) + ":";
2376 -// }
2377 -
2378 -// const argVal = document.createElement("span");
2379 -// argVal.classList.add("tool-arg-value");
2380 -
2381 -// const argText = cleanTextValue(argValue);
2382 -
2383 -// argVal.textContent = truncateText(argText, 300);
2384 -
2385 -// argRow.appendChild(argLabel);
2386 -// argRow.appendChild(argVal);
2387 -// argsDiv.appendChild(argRow);
2388 -// }
2389 -
2390 -// kvpsDiv.appendChild(argsDiv);
2391 -// continue;
2392 -// }
2393 -
2394 -// const kvpDiv = document.createElement("div");
2395 -// kvpDiv.classList.add("step-kvp");
2396 -
2397 -// const keySpan = document.createElement("span");
2398 -// keySpan.classList.add("step-kvp-key");
2399 -
2400 -// const iconName = extractIconFromKey(key);
2401 -// if (iconName) {
2402 -// keySpan.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
2403 -// } else {
2404 -// keySpan.textContent = convertToTitleCase(key) + ":";
2405 -// }
2406 -
2407 -// const valueSpan = document.createElement("div");
2408 -// valueSpan.classList.add("step-kvp-value");
2409 -
2410 -// if (typeof value === "string" && value.startsWith("img://")) {
2411 -// const imgElement = document.createElement("img");
2412 -// imgElement.classList.add("screenshot-img");
2413 -// imgElement.src = value.replace("img://", "/image_get?path=");
2414 -// imgElement.alt = "Image Attachment";
2415 -// imgElement.style.cursor = "pointer";
2416 -// imgElement.style.maxWidth = "100%";
2417 -// imgElement.style.display = "block";
2418 -// imgElement.style.marginTop = "4px";
2419 -
2420 -// // Add click handler and cursor change
2421 -// imgElement.addEventListener("click", () => {
2422 -// imageViewerStore.open(imgElement.src, { name: "Image Attachment" });
2423 -// });
2424 -
2425 -// valueSpan.appendChild(imgElement);
2426 -// } else {
2427 -// const valueText = cleanTextValue(value);
2428 -// valueSpan.textContent = truncateText(valueText, 1000);
2429 -// }
2430 -
2431 -// kvpDiv.appendChild(keySpan);
2432 -// kvpDiv.appendChild(valueSpan);
2433 -// kvpsDiv.appendChild(kvpDiv);
2434 -// }
2435 -
2436 -// container.appendChild(kvpsDiv);
2437 -// }
2438 -
2439 -// // Add main content if present (JSON content)
2440 -// if (content && content.trim()) {
2441 -// const pre = document.createElement("pre");
2442 -// pre.classList.add("msg-json");
2443 -// pre.textContent = truncateText(content, 1000);
2444 -// container.appendChild(pre);
2445 -// }
2446 -// }
2447 -
2448 -/**
2449 - * Helper to render thoughts/reasoning with lightbulb icon
2450 - */
2451 -// function renderThoughts(container, value) {
2452 -// const thoughtsDiv = document.createElement("div");
2453 -// thoughtsDiv.classList.add("step-thoughts", "msg-thoughts");
2454 -
2455 -// const thoughtText = cleanTextValue(value);
2456 -
2457 -// if (thoughtText) {
2458 -// thoughtsDiv.innerHTML = `<span class="thought-icon material-symbols-outlined">lightbulb</span><span class="thought-text">${escapeHTML(thoughtText)}</span>`;
2459 -// container.appendChild(thoughtsDiv);
2460 -// }
2461 -// }
1951
1952 /**
1953 * Update process group header with step count, status, and metrics
@@ -2469,8 +1958,7 @@ function updateProcessGroupHeader(group) {
1958 const titleEl = header.querySelector(".group-title");
1959 const badgeEl = header.querySelector(".step-badge");
1960 const metricsEl = header.querySelector(".group-metrics");
2472 - const response = group.querySelector(".process-group-response");
2473 - const isCompleted = !!response;
1961 + const isCompleted = isProcessGroupComplete(group);
1962 const notificationsEl = metricsEl?.querySelector(".metric-notifications");
1963
1964 // Update group title with the latest agent step heading
@@ -2494,7 +1982,12 @@ function updateProcessGroupHeader(group) {
1982
1983 // If completed, set badge to END
1984 if (isCompleted) {
1985 + // set end badge
1986 badgeEl.outerHTML = `<span class="step-badge END">END</span>`;
1987 + // remove shine from any steps
1988 + group.querySelectorAll(".step-title.shiny-text").forEach((el) => {
1989 + el.classList.remove("shiny-text");
1990 + });
1991 } else {
1992 // if not complete, clone the last step badge
1993 if (badgeEl && steps.length > 0) {
@@ -2502,6 +1995,7 @@ function updateProcessGroupHeader(group) {
1995 const code = lastStep.getAttribute("data-step-code");
1996 badgeEl.outerHTML = `<span class="step-badge ${code}">${code}</span>`;
1997 }
1998 +
1999 }
2000
2001 // Update step count in metrics - All GEN steps from all agents per process group
@@ -2573,42 +2067,12 @@ function updateProcessGroupHeader(group) {
2067 }
2068 }
2069
2576 - if (steps.length > 0) {
2577 - // Get the last step's type for status
2578 - // const lastStep = steps[steps.length - 1];
2579 - // const lastType = lastStep.getAttribute("data-type");
2580 - // const lastToolName = lastStep.getAttribute("data-tool-name");
2581 - // const lastTitle = lastStep.querySelector(".step-title")?.textContent || "";
2582 - // Update status badge
2583 - // if (statusEl) {
2584 - // const statusCode = getStatusCode(lastType, lastToolName);
2585 - // const statusColorClass = getStatusClass(lastType, lastToolName);
2586 - // statusEl.textContent = statusCode;
2587 - // statusEl.className = `step-badge ${statusColorClass} group-status`;
2588 - // }
2589 - // Update title
2590 - // if (titleEl) {
2591 - // // Prefer agent type steps for the group title as they contain thinking/planning info
2592 - // if (lastType === "agent" && lastTitle) {
2593 - // titleEl.textContent = cleanStepTitle(lastTitle, 50);
2594 - // } else {
2595 - // // Try to find the most recent agent step for a better title
2596 - // const agentSteps = group.querySelectorAll(
2597 - // '.process-step[data-type="agent"]',
2598 - // );
2599 - // if (agentSteps.length > 0) {
2600 - // const lastAgentStep = agentSteps[agentSteps.length - 1];
2601 - // const agentTitle =
2602 - // lastAgentStep.querySelector(".step-title")?.textContent || "";
2603 - // if (agentTitle) {
2604 - // titleEl.textContent = cleanStepTitle(agentTitle, 50);
2605 - // return;
2606 - // }
2607 - // }
2608 - // titleEl.textContent = cleanStepTitle(lastTitle, 50) || `Processing...`;
2609 - // }
2610 - // }
2611 - }
2070 +
2071 +}
2072 +
2073 +function isProcessGroupComplete(group) {
2074 + const response = group.querySelector(".process-group-response");
2075 + return !!response;
2076 }
2077
2078 /**