From f0e4e6d1fe195826e449e0c4858587716784ba4c Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Mon, 19 Jan 2026 15:31:29 +0100 Subject: [PATCH 01/11] step-title shiny text --- .../messages/process-group/process-group.css | 46 +----------- webui/js/messages.js | 70 ++++++++++++++----- 2 files changed, 55 insertions(+), 61 deletions(-) diff --git a/webui/components/messages/process-group/process-group.css b/webui/components/messages/process-group/process-group.css index ca8e27a1a..bb5e25e50 100644 --- a/webui/components/messages/process-group/process-group.css +++ b/webui/components/messages/process-group/process-group.css @@ -226,33 +226,6 @@ color: var(--step-accent); } -/* Animated spinner for active status (CSS-only, no Material Icons) */ -.status-badge.status-active::after { - content: ""; - display: inline-block; - width: 8px; - height: 8px; - border: 1.5px solid currentColor; - border-top-color: transparent; - border-radius: 50%; - animation: spin 0.8s linear infinite; - margin-left: 4px; - flex-shrink: 0; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -/* Don't show spinner pseudo-element on END/ERR (they have their own indicators) */ -.status-badge.status-end::after, -.status-badge.status-err::after { - display: none; -} - -/* END status with checkmark icon (icon added via JS, no ::before needed) */ - /* Completed process group styling */ .process-group-completed { opacity: 0.95; @@ -388,16 +361,6 @@ min-height: 18px; } -/* Step icon removed - using status badges instead */ -.process-step-header .step-icon { - display: none; -} - -/* Step type label removed - using status badges instead */ -.process-step-header .step-type { - display: none; -} - /* Step title */ .process-step-header .step-title { flex: 1; @@ -787,9 +750,8 @@ 50% { opacity: 0.8; } } -.process-step.loading .step-icon { - animation: pulse-step 1.2s ease-in-out infinite; -} +.step-title.shiny-text { color: transparent !important; -webkit-background-clip: text; background-clip: text; animation: shine 1s linear infinite; } + @keyframes shine { to { background-position: -100% center; } } /* Responsive adjustments */ @media (max-width: 768px) { @@ -797,10 +759,6 @@ padding: var(--spacing-xs) var(--spacing-sm); } - .process-step-header .step-type { - display: none; - } - .process-step-header .step-title { font-size: 0.7rem; } diff --git a/webui/js/messages.js b/webui/js/messages.js index 37180663f..695e7942a 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -13,6 +13,27 @@ const chatHistory = document.getElementById("chat-history"); let messageGroup = null; let currentProcessGroup = null; // Track current process group for collapsible UI let currentDelegationSteps = {}; // Track delegation steps by agent number for nesting +let activeProcessGroupId = null; // Only one process group should show "running" indicators at a time +let activeProcessGroupEl = null; +let activeStepTitleEl = null; + +/** + * Mark current process group as active and clear active badges. + */ +function setActiveProcessGroup(group) { + if (!group || !group.id) return; + if (activeProcessGroupId === group.id) return; + + // Clear shiny effect from the previous active step title if we moved to a new group + if (activeStepTitleEl && activeProcessGroupEl && activeProcessGroupEl !== group && activeProcessGroupEl.contains(activeStepTitleEl)) { + activeStepTitleEl.classList.remove("shiny-text"); + activeStepTitleEl = null; + } + + activeProcessGroupId = group.id; + activeProcessGroupEl = group; + +} /** * Resolve tool name from kvps, existing attribute, or previous siblings @@ -82,6 +103,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) { currentProcessGroup = createProcessGroup(id); chatHistory.appendChild(currentProcessGroup); + setActiveProcessGroup(currentProcessGroup); } // Add step to current process group @@ -102,6 +124,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) { currentProcessGroup = createProcessGroup(id); chatHistory.appendChild(currentProcessGroup); + setActiveProcessGroup(currentProcessGroup); } // Add subordinate response as a response step (special type to show content) @@ -1200,7 +1223,7 @@ function createProcessGroup(id) { header.innerHTML = ` Processing... - GEN + GEN schedule--:-- footprint0 @@ -1260,6 +1283,9 @@ function getNestedContainer(parentStep) { * Add a step to a process group */ function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null, agentNumber = 0) { + // group with newest step becomes the active one + setActiveProcessGroup(group); + const groupId = group.getAttribute("data-group-id"); let stepsContainer = group.querySelector(".process-steps"); const isGroupCompleted = group.classList.contains("process-group-completed"); @@ -1339,10 +1365,9 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul // Add status color class to step for cascading --step-accent to internal icons step.classList.add(statusColorClass); - const activeClass = isGroupCompleted ? "" : " status-active"; stepHeader.innerHTML = ` - ${statusCode} + ${statusCode} ${escapeHTML(title)} `; @@ -1407,14 +1432,25 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul } } - // Remove status-active from all previous steps (only the current step is active) - const prevSteps = stepsContainer.querySelectorAll(".process-step .status-badge.status-active"); - prevSteps.forEach(badge => badge.classList.remove("status-active")); + // Remove shiny effect from the previously active step title (O(1)) + if (activeStepTitleEl) { + activeStepTitleEl.classList.remove("shiny-text"); + activeStepTitleEl = null; + } appendTarget.appendChild(step); // Update group header updateProcessGroupHeader(group); + + // Apply shiny effect to the active step title + if (!isGroupCompleted && group.id === activeProcessGroupId) { + const titleEl = step.querySelector(".process-step-header .step-title"); + if (titleEl) { + titleEl.classList.add("shiny-text"); + activeStepTitleEl = titleEl; + } + } return step; } @@ -1810,10 +1846,8 @@ function updateProcessGroupHeader(group) { const metricsEl = group.querySelector(".group-metrics"); const isCompleted = group.classList.contains("process-group-completed"); - // If completed, only remove active badges and exit early (don't update metrics) + // If completed, don't update metrics if (isCompleted) { - const activeBadges = group.querySelectorAll(".status-badge.status-active"); - activeBadges.forEach(badge => badge.classList.remove("status-active")); return; } @@ -1881,14 +1915,14 @@ function updateProcessGroupHeader(group) { const lastToolName = lastStep.getAttribute("data-tool-name"); const lastTitle = lastStep.querySelector(".step-title")?.textContent || ""; - // Update status badge (keep status-active during execution) + // Update status badge if (statusEl) { // Status code and color class from store (maps backend types) const statusCode = processGroupStore.getStepCode(lastType, lastToolName); const statusColorClass = processGroupStore.getStatusColorClass(lastType, lastToolName); statusEl.textContent = statusCode; - statusEl.className = `status-badge ${statusColorClass} status-active group-status`; + statusEl.className = `status-badge ${statusColorClass} group-status`; } // Update title @@ -1929,17 +1963,13 @@ function truncateText(text, maxLength) { function markProcessGroupComplete(group, responseTitle) { if (!group) return; - // Update status badge to END (remove status-active) + // Update status badge to END const statusEl = group.querySelector(".group-status"); if (statusEl) { statusEl.innerHTML = 'checkEND'; - statusEl.className = "status-badge status-end group-status"; // No status-active + statusEl.className = "status-badge status-end group-status"; } - // Remove status-active from all step badges (stop spinners) - const stepBadges = group.querySelectorAll(".process-step .status-badge.status-active"); - stepBadges.forEach(badge => badge.classList.remove("status-active")); - // Update title if response title is available const titleEl = group.querySelector(".group-title"); if (titleEl && responseTitle) { @@ -1975,6 +2005,12 @@ export function resetProcessGroups() { currentProcessGroup = null; currentDelegationSteps = {}; messageGroup = null; + activeProcessGroupId = null; + activeProcessGroupEl = null; + if (activeStepTitleEl) { + activeStepTitleEl.classList.remove("shiny-text"); + } + activeStepTitleEl = null; } /** From 41873c99057073edbd2d47bc29bfdc90b0ac8865 Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Mon, 19 Jan 2026 20:52:42 +0100 Subject: [PATCH 02/11] warn/info/hint notifs in process grp --- .../messages/process-group/process-group.css | 8 ++++ webui/js/messages.js | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/webui/components/messages/process-group/process-group.css b/webui/components/messages/process-group/process-group.css index bb5e25e50..518d3e522 100644 --- a/webui/components/messages/process-group/process-group.css +++ b/webui/components/messages/process-group/process-group.css @@ -268,6 +268,14 @@ gap: 2px; } +.process-group-header .group-metrics .metric-notifications { + font-weight: 600; +} + +.process-group-header .group-metrics .metric-notifications .material-symbols-outlined { + opacity: 0.85; +} + /* Legacy timestamp/duration (for backwards compatibility) */ .process-group-header .group-timestamp { font-size: 0.65rem; diff --git a/webui/js/messages.js b/webui/js/messages.js index 695e7942a..25f57d2ce 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -1228,6 +1228,7 @@ function createProcessGroup(id) { schedule--:-- footprint0 timer0s + `; @@ -1846,6 +1847,42 @@ function updateProcessGroupHeader(group) { const metricsEl = group.querySelector(".group-metrics"); const isCompleted = group.classList.contains("process-group-completed"); + const notificationsEl = metricsEl?.querySelector(".metric-notifications"); + if (notificationsEl) { + const counts = { warning: 0, info: 0, hint: 0 }; + steps.forEach((step) => { + const stepType = step.getAttribute("data-type"); + if (Object.prototype.hasOwnProperty.call(counts, stepType)) { + counts[stepType] += 1; + } + }); + + const totalNotifications = counts.warning + counts.info + counts.hint; + const countEl = notificationsEl.querySelector(".metric-value"); + notificationsEl.classList.remove("status-wrn", "status-inf", "status-hnt"); + + if (totalNotifications > 0) { + if (countEl) { + countEl.textContent = totalNotifications.toString(); + } + if (counts.warning > 0) { + notificationsEl.classList.add("status-wrn"); + } else if (counts.info > 0) { + notificationsEl.classList.add("status-inf"); + } else { + notificationsEl.classList.add("status-hnt"); + } + notificationsEl.hidden = false; + notificationsEl.title = `Warnings: ${counts.warning}, Info: ${counts.info}, Hints: ${counts.hint}`; + } else { + if (countEl) { + countEl.textContent = "0"; + } + notificationsEl.hidden = true; + notificationsEl.title = "Warnings/Info/Hint"; + } + } + // If completed, don't update metrics if (isCompleted) { return; From 61f431ae3a1638c0497c917413186b8e76a43e22 Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Mon, 19 Jan 2026 21:34:18 +0100 Subject: [PATCH 03/11] flat style for msg attachments --- webui/css/messages.css | 15 --------------- webui/index.css | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/webui/css/messages.css b/webui/css/messages.css index 3ea402995..9acf087b9 100644 --- a/webui/css/messages.css +++ b/webui/css/messages.css @@ -70,14 +70,6 @@ margin: 0; } -.center-container .message { - /* margin-bottom: var(--spacing-sm); */ -} - -.message .message-body { - /* padding-top: 0.5em; - padding-bottom: 0.5em; */ -} .message-user { text-align: end; @@ -111,19 +103,12 @@ color: #2e2e2e; } */ -.message-ai { - /* border-bottom-left-radius: var(--spacing-xxs); */ -} .message-center { align-self: center; /* border-bottom-left-radius: unset; */ } -.message-followup { - /* margin-left: var(--spacing-lg); */ - /* margin-bottom: var(--spacing-lg); */ -} .message-followup .message { border-radius: 0; diff --git a/webui/index.css b/webui/index.css index c0e29d5c5..e9028cd11 100644 --- a/webui/index.css +++ b/webui/index.css @@ -747,6 +747,32 @@ h4 { } +/* Chat-history attachments overrides (flat styling) */ + +#chat-history .attachments-container { + padding: 0 !important; +} + +#chat-history .attachment-item { + padding: 0 !important; + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + transform: none !important; +} + +#chat-history .attachment-item:hover { + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + transform: none !important; +} + +#chat-history .attachment-preview, +#chat-history .attachment-item.image-type .attachment-preview { + background: transparent; +} + .attachment-image .attachment-preview { margin-right: 0; } From 2f034e750efd105eccd0c3eaf86d1e632534da13 Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Mon, 19 Jan 2026 14:03:25 +0100 Subject: [PATCH 04/11] embed process group at creation --- webui/js/messages.js | 183 ++++++++++++++----------------------------- 1 file changed, 58 insertions(+), 125 deletions(-) diff --git a/webui/js/messages.js b/webui/js/messages.js index 25f57d2ce..70c00d1b8 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -75,15 +75,35 @@ const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'progress', 'info // Main types that should always be visible (not collapsed) const MAIN_TYPES = ['user', 'response', 'error', 'rate_limit']; +/** + * Helper to append a message container to the correct group in chat history + */ +function appendMessageToHistory(messageContainer, groupType, forceNewGroup, id) { + // Check if current messageGroup is still in DOM, if not, reset it (context switch) + if (messageGroup && !document.getElementById(messageGroup.id)) { + messageGroup = null; + } + + // Create new group if needed + if (!messageGroup || forceNewGroup || groupType !== messageGroup.getAttribute("data-group-type")) { + messageGroup = document.createElement("div"); + messageGroup.id = `message-group-${id}`; + messageGroup.classList.add("message-group", `message-group-${groupType}`); + messageGroup.setAttribute("data-group-type", groupType); + chatHistory.appendChild(messageGroup); + } + + // Append message to group + messageGroup.appendChild(messageContainer); +} + export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) { // Check if this is a process type message const isProcessType = PROCESS_TYPES.includes(type); - const isMainType = MAIN_TYPES.includes(type); // Search for the existing message container by id let messageContainer = document.getElementById(`message-${id}`); let processStepElement = document.getElementById(`process-step-${id}`); - let isNewMessage = false; // For user messages, close current process group FIRST (start fresh for next interaction) if (type === "user") { @@ -92,7 +112,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest } // For process types, check if we should add to process group - if (isProcessType) { + if (isProcessType || (type === "response" && agentNumber !== 0)) { if (processStepElement) { // Update existing process step updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs, agentNumber); @@ -101,61 +121,35 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest // Create or get process group for current interaction if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) { + // Create response container for this process group immediately (Option B) + messageContainer = document.createElement("div"); + messageContainer.id = `message-${id}`; + messageContainer.classList.add("message-container", "ai-container", "has-process-group"); + currentProcessGroup = createProcessGroup(id); - chatHistory.appendChild(currentProcessGroup); + currentProcessGroup.classList.add("embedded"); + messageContainer.appendChild(currentProcessGroup); + + // Handle DOM insertion immediately + appendMessageToHistory(messageContainer, "left", false, id); + setActiveProcessGroup(currentProcessGroup); } // Add step to current process group - processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs, agentNumber); + const stepType = (type === "response" && agentNumber !== 0) ? "response" : type; + processStepElement = addProcessStep(currentProcessGroup, id, stepType, heading, content, kvps, timestamp, durationMs, agentNumber); return processStepElement; } - // For subordinate agent responses (A1, A2, ...), treat as a process step instead of main response - // agentNumber: 0 = main agent, 1+ = subordinate agents - // Note: subordinate "response" is a completion marker with content - if (type === "response" && agentNumber !== 0) { - if (processStepElement) { - updateProcessStep(processStepElement, id, "response", heading, content, kvps, durationMs, agentNumber); - return processStepElement; - } - - // Create or get process group for current interaction - if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) { - currentProcessGroup = createProcessGroup(id); - chatHistory.appendChild(currentProcessGroup); - setActiveProcessGroup(currentProcessGroup); - } - - // Add subordinate response as a response step (special type to show content) - processStepElement = addProcessStep(currentProcessGroup, id, "response", heading, content, kvps, timestamp, durationMs, agentNumber); - return processStepElement; - } - - // For main agent (A0) response, embed the current process group and mark as complete + // For main agent (A0) response, mark the current process group as complete if (type === "response" && currentProcessGroup) { - const processGroupToEmbed = currentProcessGroup; - // Keep currentProcessGroup reference - subsequent process messages go to same group - // Mark process group as complete (END state) - markProcessGroupComplete(processGroupToEmbed, heading); - - if (!messageContainer) { - // Create new container with embedded process group - messageContainer = createResponseContainerWithProcessGroup(id, processGroupToEmbed); - isNewMessage = true; - } else { - // Check if already embedded - const existingEmbedded = messageContainer.querySelector(".process-group"); - if (!existingEmbedded && processGroupToEmbed) { - embedProcessGroup(messageContainer, processGroupToEmbed); - } - } + markProcessGroupComplete(currentProcessGroup, heading); } if (!messageContainer) { // Create a new container if not found - isNewMessage = true; const sender = type === "user" ? "user" : "ai"; messageContainer = document.createElement("div"); messageContainer.id = `message-${id}`; @@ -165,8 +159,8 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest const handler = getHandler(type); handler(messageContainer, id, type, heading, content, temp, kvps); - // If this is a new message, handle DOM insertion - if (!document.getElementById(`message-${id}`)) { + // If this is a new message (not yet in DOM), handle DOM insertion + if (!messageContainer.parentNode) { // message type visual grouping const groupTypeMap = { user: "right", @@ -186,27 +180,11 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest }; const groupType = groupTypeMap[type] || "left"; + const forceNewGroup = groupStart[type] || false; - // here check if messageGroup is still in DOM, if not, then set it to null (context switch) - if (messageGroup && !document.getElementById(messageGroup.id)) - messageGroup = null; - - if ( - !messageGroup || // no group yet exists - groupStart[type] || // message type forces new group - groupType != messageGroup.getAttribute("data-group-type") // message type changes group - ) { - messageGroup = document.createElement("div"); - messageGroup.id = `message-group-${id}`; - messageGroup.classList.add(`message-group`, `message-group-${groupType}`); - messageGroup.setAttribute("data-group-type", groupType); - } - messageGroup.appendChild(messageContainer); - chatHistory.appendChild(messageGroup); + appendMessageToHistory(messageContainer, groupType, forceNewGroup, id); } - // Simplified implementation - no setup needed - return messageContainer; } @@ -1151,52 +1129,6 @@ class Scroller { // Process Group Embedding Functions // ============================================ -/** - * Create a response container with an embedded process group - */ -function createResponseContainerWithProcessGroup(id, processGroup) { - const messageContainer = document.createElement("div"); - messageContainer.id = `message-${id}`; - messageContainer.classList.add("message-container", "ai-container", "has-process-group"); - - // Move process group from chatHistory into the container - if (processGroup && processGroup.parentNode) { - processGroup.parentNode.removeChild(processGroup); - } - - // Process group will be the first child - if (processGroup) { - processGroup.classList.add("embedded"); - messageContainer.appendChild(processGroup); - } - - return messageContainer; -} - -/** - * Embed a process group into an existing message container - */ -function embedProcessGroup(messageContainer, processGroup) { - if (!messageContainer || !processGroup) return; - - // Remove from current parent - if (processGroup.parentNode) { - processGroup.parentNode.removeChild(processGroup); - } - - // Add embedded class - processGroup.classList.add("embedded"); - messageContainer.classList.add("has-process-group"); - - // Insert at the beginning of the container - const firstChild = messageContainer.firstChild; - if (firstChild) { - messageContainer.insertBefore(processGroup, firstChild); - } else { - messageContainer.appendChild(processGroup); - } -} - // ============================================ // Process Group Functions // ============================================ @@ -1380,11 +1312,6 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul // Explicitly add or remove the class based on state if (newState) { step.classList.add("step-expanded"); - // Scroll terminal for newly expanded steps - requestAnimationFrame(() => { - const terminal = step.querySelector(".terminal-output"); - if (terminal) terminal.scrollTop = terminal.scrollHeight; - }); } else { step.classList.remove("step-expanded"); } @@ -1405,14 +1332,6 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul detail.appendChild(detailContent); step.appendChild(detail); - // Scroll terminal for already expanded steps - if (isStepExpanded) { - requestAnimationFrame(() => { - const terminal = step.querySelector(".terminal-output"); - if (terminal) terminal.scrollTop = terminal.scrollHeight; - }); - } - // Track delegation steps for nesting if (toolNameToUse === "call_subordinate") { currentDelegationSteps[agentNumber] = step; @@ -1441,6 +1360,12 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul appendTarget.appendChild(step); + // Scroll terminal to bottom on initial render (including page refresh) + const initialTerminal = step.querySelector(".terminal-output"); + if (initialTerminal) { + initialTerminal.scrollTop = initialTerminal.scrollHeight; + } + // Update group header updateProcessGroupHeader(group); @@ -1490,6 +1415,10 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati let skipFullRender = false; if (detailContent) { + // Capture scroll state before re-render (uses existing Scroller pattern) + const terminal = detailContent.querySelector(".terminal-output"); + const scroller = terminal ? new Scroller(terminal) : null; + // For browser, update image src incrementally to avoid flashing if (type === "browser" && kvps?.screenshot) { const existingImg = detailContent.querySelector(".screenshot-img"); @@ -1506,6 +1435,12 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati if (!skipFullRender) { renderStepDetailContent(detailContent, content, kvps, type); + + // Re-apply scroll (stays at bottom if was at bottom) + const newTerminal = detailContent.querySelector(".terminal-output"); + if (newTerminal && scroller?.wasAtBottom) { + newTerminal.scrollTop = newTerminal.scrollHeight; + } } } @@ -1639,8 +1574,6 @@ function renderStepDetailContent(container, content, kvps, type = null) { processedOutput = convertPathsToLinks(processedOutput); outputPre.innerHTML = processedOutput; terminalDiv.appendChild(outputPre); - // Scroll terminal to bottom - outputPre.scrollTop = outputPre.scrollHeight; } container.appendChild(terminalDiv); From c9c5efda408802ef17fe05d5de772ebbbbde898b Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Tue, 20 Jan 2026 11:38:49 +0100 Subject: [PATCH 05/11] progress/steps shiny-text helper --- webui/index.js | 17 ++++++++++++----- webui/js/messages.js | 11 +++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/webui/index.js b/webui/index.js index 5d4179946..8a19ae5c8 100644 --- a/webui/index.js +++ b/webui/index.js @@ -458,11 +458,7 @@ function updateProgress(progress, active) { if (!progressBarEl) return; if (!progress) progress = ""; - if (!active) { - removeClassFromElement(progressBarEl, "shiny-text"); - } else { - addClassToElement(progressBarEl, "shiny-text"); - } + setProgressBarShine(progressBarEl, active); progress = msgs.convertIcons(progress); @@ -471,6 +467,17 @@ function updateProgress(progress, active) { } } +function setProgressBarShine(progressBarEl, active) { + if (!progressBarEl) return; + if (!active) { + removeClassFromElement(progressBarEl, "shiny-text"); + // clear any lingering shines in process steps + msgs.clearActiveStepShine(); + } else { + addClassToElement(progressBarEl, "shiny-text"); + } +} + globalThis.pauseAgent = async function (paused) { await inputStore.pauseAgent(paused); }; diff --git a/webui/js/messages.js b/webui/js/messages.js index 70c00d1b8..6889ff472 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -35,6 +35,17 @@ function setActiveProcessGroup(group) { } +export function clearActiveStepShine() { + if (activeStepTitleEl) { + activeStepTitleEl.classList.remove("shiny-text"); + activeStepTitleEl = null; + } + // clear any lingering shine in process steps + document.querySelectorAll(".process-step .step-title.shiny-text").forEach((el) => { + el.classList.remove("shiny-text"); + }); +} + /** * Resolve tool name from kvps, existing attribute, or previous siblings * For 'tool' type steps, inherits from preceding step if not directly available From 9ed61e93dd422d3bca8ac5d0d4bec33f3b846f80 Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Tue, 20 Jan 2026 13:15:46 +0100 Subject: [PATCH 06/11] hide proc-grp notifs icon when <0 --- webui/components/messages/process-group/process-group.css | 4 ++++ webui/js/messages.js | 6 +----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webui/components/messages/process-group/process-group.css b/webui/components/messages/process-group/process-group.css index 518d3e522..51bd29bfd 100644 --- a/webui/components/messages/process-group/process-group.css +++ b/webui/components/messages/process-group/process-group.css @@ -272,6 +272,10 @@ font-weight: 600; } +.process-group-header .group-metrics .metric-notifications[hidden] { + display: none; +} + .process-group-header .group-metrics .metric-notifications .material-symbols-outlined { opacity: 0.85; } diff --git a/webui/js/messages.js b/webui/js/messages.js index 6889ff472..fc2f7c161 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -1170,8 +1170,8 @@ function createProcessGroup(id) { schedule--:-- footprint0 + timer0s - `; @@ -1819,11 +1819,7 @@ function updateProcessGroupHeader(group) { notificationsEl.hidden = false; notificationsEl.title = `Warnings: ${counts.warning}, Info: ${counts.info}, Hints: ${counts.hint}`; } else { - if (countEl) { - countEl.textContent = "0"; - } notificationsEl.hidden = true; - notificationsEl.title = "Warnings/Info/Hint"; } } From 1ed3262bfd4344b9425bdaf5b8ed193114dfbc5a Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Tue, 20 Jan 2026 13:26:31 +0100 Subject: [PATCH 07/11] remove type error from proc-grp --- webui/js/messages.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webui/js/messages.js b/webui/js/messages.js index fc2f7c161..4d51a7c4b 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -1291,9 +1291,9 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul const title = getStepTitle(heading, kvps, type); // Check if step should be expanded - // Warning/error steps auto-expand to show content + // Warning steps auto-expand to show content (errors are external MAIN_TYPES, not in process groups) const isStepExpanded = processGroupStore.isStepExpanded(groupId, id) || - (type === "warning" || type === "error"); + type === "warning"; if (isStepExpanded) { step.classList.add("step-expanded"); } @@ -1357,8 +1357,8 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul appendTarget = getNestedContainer(parentStep); step.classList.add("nested-step"); - // Auto-expand parent if this nested step is a warning/error - if (type === "warning" || type === "error") { + // Auto-expand parent if this nested step is a warning (errors are external MAIN_TYPES) + if (type === "warning") { parentStep.classList.add("step-expanded"); } } From 54248d493761cbb2569e96710e7e985f0c045239 Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Tue, 20 Jan 2026 14:36:06 +0100 Subject: [PATCH 08/11] detail modes for process groups/steps --- .../process-group/process-group-store.js | 55 ++++++++++++ .../bottom/preferences/preferences-panel.html | 87 ++++++++++++++++--- .../bottom/preferences/preferences-store.js | 48 ++++++++-- webui/index.js | 3 + webui/js/messages.js | 29 ++++--- 5 files changed, 190 insertions(+), 32 deletions(-) diff --git a/webui/components/messages/process-group/process-group-store.js b/webui/components/messages/process-group/process-group-store.js index 256c5e570..e2be3a79b 100644 --- a/webui/components/messages/process-group/process-group-store.js +++ b/webui/components/messages/process-group/process-group-store.js @@ -175,6 +175,61 @@ const model = { } } this._persist(); + }, + + // Get current detail mode from preferences + _getDetailMode() { + return window.Alpine?.store("preferences")?.detailMode || "current"; + }, + + expandGroup(groupId, isActiveAndGenerating = false) { + const mode = this._getDetailMode(); + if (mode === "collapsed") { + // Only expand if generating, not for completed groups + return isActiveAndGenerating; + } + if (mode === "current" || mode === "expanded") return true; + return !this.defaultCollapsed; + }, + + expandStep(groupId, stepId, isActive = false) { + const mode = this._getDetailMode(); + if (mode === "collapsed") return false; + if (mode === "expanded") return true; + if (mode === "current") return isActive; + return this.isStepExpanded(groupId, stepId); + }, + + // Apply current mode to all existing DOM elements + applyModeSteps() { + const mode = this._getDetailMode(); + const showUtils = window.Alpine?.store("preferences")?.showUtils || false; + const allGroups = document.querySelectorAll(".process-group"); + + // Find the last VISIBLE step using targeted selector + const stepSelector = showUtils ? ".process-step" : ".process-step:not(.message-util)"; + const visibleSteps = document.querySelectorAll(stepSelector); + const lastStep = visibleSteps.length > 0 ? visibleSteps[visibleSteps.length - 1] : null; + + // Get all steps for applying expansion + const allSteps = document.querySelectorAll(".process-step"); + + // Apply to groups + allGroups.forEach(group => { + group.classList.toggle("expanded", mode !== "collapsed"); + }); + + // Apply to steps + allSteps.forEach(step => { + let shouldExpand = false; + if (mode === "expanded") { + shouldExpand = true; + } else if (mode === "current") { + // Expand the last step and any parent steps containing it (for nested subordinate steps) + shouldExpand = step === lastStep || step.contains(lastStep); + } + step.classList.toggle("step-expanded", shouldExpand); + }); } }; diff --git a/webui/components/sidebar/bottom/preferences/preferences-panel.html b/webui/components/sidebar/bottom/preferences/preferences-panel.html index f54626c28..92c0e4867 100644 --- a/webui/components/sidebar/bottom/preferences/preferences-panel.html +++ b/webui/components/sidebar/bottom/preferences/preferences-panel.html @@ -27,20 +27,6 @@ -
  • - Speech - -
  • -
  • - Show utility messages - -
  • Width
    @@ -56,6 +42,37 @@
  • +
  • + Detail +
    + +
    +
  • +
  • + Speech + +
  • +
  • + Show utility messages + +
  • @@ -144,6 +161,48 @@ margin: 0 0.1rem; opacity: 0.5; } + + /* Detail mode button bar */ + .detail-mode-setting { + gap: 0.25rem; + } + .detail-mode-setting .detail-label { + flex: 1; + } + .detail-buttons { + display: flex; + align-items: center; + gap: 0; + } + .detail-btn-wrapper { + display: flex; + align-items: center; + } + .detail-btn { + background: none; + border: none; + color: var(--color-primary); + padding: 0.15rem 0.25rem; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.15s ease; + } + .detail-btn .material-symbols-outlined { + font-size: 1rem; + } + .detail-btn:hover { + opacity: 0.85; + } + .detail-btn.active { + opacity: 1; + color: var(--color-text); + } + .detail-sep { + color: var(--color-text-secondary, #666); + font-size: 0.6rem; + margin: 0 0.1rem; + opacity: 0.5; + } diff --git a/webui/components/sidebar/bottom/preferences/preferences-store.js b/webui/components/sidebar/bottom/preferences/preferences-store.js index a11d7269a..2012ff2f9 100644 --- a/webui/components/sidebar/bottom/preferences/preferences-store.js +++ b/webui/components/sidebar/bottom/preferences/preferences-store.js @@ -1,6 +1,7 @@ import { createStore } from "/js/AlpineStore.js"; import * as css from "/js/css.js"; import { store as speechStore } from "/components/chat/speech/speech-store.js"; +import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js"; // Preferences store centralizes user preference toggles and side-effects const model = { @@ -68,6 +69,23 @@ const model = { { label: "FULL", value: "full" }, ], + // Detail mode for process groups/steps expansion + get detailMode() { + return this._detailMode; + }, + set detailMode(value) { + this._detailMode = value; + this._applyDetailMode(value); + }, + _detailMode: "current", // Default: show current step only + + // Detail mode options for UI sidebar + detailModeOptions: [ + { label: "MIN", value: "collapsed", icon: "unfold_less", title: "All collapsed" }, + { label: "CUR", value: "current", icon: "step", title: "Current step only" }, + { label: "ALL", value: "expanded", icon: "unfold_more", title: "All expanded" }, + ], + // Initialize preferences and apply current state init() { try { @@ -104,6 +122,16 @@ const model = { this._chatWidth = "55"; // Default to standard } + // Load detail mode preference + try { + const storedDetailMode = localStorage.getItem("detailMode"); + if (storedDetailMode && this.detailModeOptions.some(opt => opt.value === storedDetailMode)) { + this._detailMode = storedDetailMode; + } + } catch { + this._detailMode = "current"; // Default + } + // Apply all preferences this._applyDarkMode(this._darkMode); this._applyAutoScroll(this._autoScroll); @@ -111,6 +139,7 @@ const model = { this._applyShowUtils(this._showUtils); this._applyCollapseProcessGroups(this._collapseProcessGroups); this._applyChatWidth(this._chatWidth); + this._applyDetailMode(this._detailMode); } catch (e) { console.error("Failed to initialize preferences store", e); } @@ -148,19 +177,14 @@ const model = { document.querySelectorAll(".process-step.message-util").forEach((el) => { el.classList.toggle("show-util", value); }); + // Re-apply detail mode to reset current visible step + processGroupStore.applyModeSteps(); }, _applyCollapseProcessGroups(value) { localStorage.setItem("collapseProcessGroups", value); // Update process group store default - try { - const processGroupStore = window.Alpine?.store("processGroup"); - if (processGroupStore) { - processGroupStore.defaultCollapsed = value; - } - } catch (e) { - // Store may not be initialized yet - } + processGroupStore.defaultCollapsed = value; }, _applyChatWidth(value) { @@ -173,6 +197,14 @@ const model = { root.style.setProperty("--chat-max-width", `${value}em`); } }, + + _applyDetailMode(value) { + localStorage.setItem("detailMode", value); + // Sync defaultCollapsed based on mode + processGroupStore.defaultCollapsed = value === "collapsed"; + // Apply mode to all existing DOM elements + processGroupStore.applyModeSteps(); + }, }; export const store = createStore("preferences", model); diff --git a/webui/index.js b/webui/index.js index 8a19ae5c8..bd3bd37ff 100644 --- a/webui/index.js +++ b/webui/index.js @@ -11,6 +11,7 @@ import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"; import { store as tasksStore } from "/components/sidebar/tasks/tasks-store.js"; import { store as chatTopStore } from "/components/chat/top-section/chat-top-store.js"; import { store as _tooltipsStore } from "/components/tooltips/tooltip-store.js"; +import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js"; globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine @@ -414,6 +415,8 @@ function afterMessagesUpdate(logs) { if (localStorage.getItem("speech") == "true") { speakMessages(logs); } + // Apply messages expansion mode after rendering + processGroupStore.applyModeSteps(); } function speakMessages(logs) { diff --git a/webui/js/messages.js b/webui/js/messages.js index 4d51a7c4b..a92bf47d7 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -17,6 +17,9 @@ let activeProcessGroupId = null; // Only one process group should show "running" let activeProcessGroupEl = null; let activeStepTitleEl = null; +// Expose activeProcessGroupId for store access +window.activeProcessGroupId = null; + /** * Mark current process group as active and clear active badges. */ @@ -32,6 +35,7 @@ function setActiveProcessGroup(group) { activeProcessGroupId = group.id; activeProcessGroupEl = group; + window.activeProcessGroupId = group.id; // Keep window copy in sync for store access } @@ -1154,8 +1158,8 @@ function createProcessGroup(id) { group.classList.add("process-group"); group.setAttribute("data-group-id", groupId); - // Check initial expansion state from store (respects user preference) - const initiallyExpanded = processGroupStore.isGroupExpanded(groupId); + // Check initial expansion state from store + const initiallyExpanded = processGroupStore.expandGroup(groupId, true); // true = is active if (initiallyExpanded) { group.classList.add('expanded'); } @@ -1291,9 +1295,17 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul const title = getStepTitle(heading, kvps, type); // Check if step should be expanded - // Warning steps auto-expand to show content (errors are external MAIN_TYPES, not in process groups) - const isStepExpanded = processGroupStore.isStepExpanded(groupId, id) || - type === "warning"; + const isActiveStep = !isGroupCompleted && group.id === activeProcessGroupId; + const isStepExpanded = processGroupStore.expandStep(groupId, id, isActiveStep); + + // In "current" mode, collapse all other steps + const detailMode = preferencesStore.detailMode; + if (detailMode === "current" && isStepExpanded) { + document.querySelectorAll(".process-step.step-expanded").forEach(s => { + s.classList.remove("step-expanded"); + }); + } + if (isStepExpanded) { step.classList.add("step-expanded"); } @@ -1356,11 +1368,6 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul const parentStep = currentDelegationSteps[agentNumber - 1]; appendTarget = getNestedContainer(parentStep); step.classList.add("nested-step"); - - // Auto-expand parent if this nested step is a warning (errors are external MAIN_TYPES) - if (type === "warning") { - parentStep.classList.add("step-expanded"); - } } // Remove shiny effect from the previously active step title (O(1)) @@ -1959,6 +1966,7 @@ function markProcessGroupComplete(group, responseTitle) { // Add completed class to group group.classList.add("process-group-completed"); + // Calculate final duration from backend data (sum of all step durations) const steps = group.querySelectorAll(".process-step"); let totalDurationMs = 0; @@ -1984,6 +1992,7 @@ export function resetProcessGroups() { messageGroup = null; activeProcessGroupId = null; activeProcessGroupEl = null; + window.activeProcessGroupId = null; // Keep window copy in sync if (activeStepTitleEl) { activeStepTitleEl.classList.remove("shiny-text"); } From cc276a0f5b67e8f9ff908444e603811e3859b37f Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Tue, 20 Jan 2026 15:48:53 +0100 Subject: [PATCH 09/11] step detail modal for all events --- .../messages/process-group/process-group.css | 9 + .../process-step-detail.html | 603 ++++++++++++++++++ .../process-step-detail/step-detail-store.js | 197 ++++++ webui/js/messages.js | 55 ++ 4 files changed, 864 insertions(+) create mode 100644 webui/components/modals/process-step-detail/process-step-detail.html create mode 100644 webui/components/modals/process-step-detail/step-detail-store.js diff --git a/webui/components/messages/process-group/process-group.css b/webui/components/messages/process-group/process-group.css index 51bd29bfd..671ed5414 100644 --- a/webui/components/messages/process-group/process-group.css +++ b/webui/components/messages/process-group/process-group.css @@ -893,3 +893,12 @@ .light-mode .process-step-detail-content .screenshot-img:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } + +/* View Details button in step detail */ +.step-detail-actions { + display: flex; + padding-left: 18px; + margin-bottom: 1rem; +} + + diff --git a/webui/components/modals/process-step-detail/process-step-detail.html b/webui/components/modals/process-step-detail/process-step-detail.html new file mode 100644 index 000000000..0667646b1 --- /dev/null +++ b/webui/components/modals/process-step-detail/process-step-detail.html @@ -0,0 +1,603 @@ + + + + Step Details + + + + +
    + +
    + + + + + diff --git a/webui/components/modals/process-step-detail/step-detail-store.js b/webui/components/modals/process-step-detail/step-detail-store.js new file mode 100644 index 000000000..306bedcc6 --- /dev/null +++ b/webui/components/modals/process-step-detail/step-detail-store.js @@ -0,0 +1,197 @@ +import { createStore } from "/js/AlpineStore.js"; + +// Step Detail Store - manages the step detail modal + +const model = { + // Selected step for detail modal view + selectedStepForDetail: null, + + // ACE editor instance for raw JSON view + _rawEditor: null, + + // Show step detail modal + showStepDetail(stepData) { + if (!stepData) return; + this.selectedStepForDetail = stepData; + window.openModal("modals/process-step-detail/process-step-detail.html"); + }, + + // Close step detail modal + closeStepDetail() { + this.selectedStepForDetail = null; + window.closeModal(); + }, + + // Copy text to clipboard + async copyToClipboard(text) { + if (!text) return false; + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + console.error("Failed to copy to clipboard:", err); + return false; + } + }, + + // Format step data for full copy (all metadata + content) + formatStepForCopy(step) { + if (!step) return ""; + const lines = []; + lines.push(`Type: ${step.type || "unknown"}`); + if (step.heading) lines.push(`Heading: ${step.heading}`); + if (step.timestamp) { + const date = new Date(parseFloat(step.timestamp) * 1000); + lines.push(`Timestamp: ${date.toISOString()}`); + } + if (step.durationMs) lines.push(`Duration: ${step.durationMs}ms`); + if (step.kvps) { + lines.push(""); + lines.push("--- Data ---"); + for (const [key, value] of Object.entries(step.kvps)) { + if (key === "reasoning") continue; + const formattedValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value); + lines.push(`${key}: ${formattedValue}`); + } + } + if (step.content) { + lines.push(""); + lines.push("--- Content ---"); + lines.push(step.content); + } + return lines.join("\n"); + }, + + // Get primary content for a step (type-aware) + getStepPrimaryContent(step) { + if (!step) return ""; + if (step.type === "code_exe") { + return step.content || ""; + } + if (step.type === "agent" && step.kvps) { + return step.kvps.thoughts || step.kvps.headline || step.content || ""; + } + if ((step.type === "tool" || step.type === "mcp") && step.kvps) { + return step.kvps.result || step.content || ""; + } + return step.content || ""; + }, + + // Initialize ACE editor for raw JSON view + initRawEditor() { + const container = document.getElementById("step-detail-raw-editor"); + if (!container) return; + + this.destroyRawEditor(); + + if (!window.ace?.edit) { + console.warn("ACE editor not available"); + return; + } + + const stepData = this.selectedStepForDetail; + if (!stepData) return; + + const editorInstance = window.ace.edit("step-detail-raw-editor"); + if (!editorInstance) return; + + this._rawEditor = editorInstance; + + const darkMode = window.localStorage?.getItem("darkMode"); + const theme = darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow"; + + this._rawEditor.setTheme(theme); + this._rawEditor.session.setMode("ace/mode/json"); + this._rawEditor.setValue(JSON.stringify(stepData, null, 2), -1); + this._rawEditor.setReadOnly(true); + this._rawEditor.clearSelection(); + this._rawEditor.setOptions({ + showPrintMargin: false, + highlightActiveLine: false, + highlightGutterLine: false + }); + }, + + // Destroy ACE editor instance + destroyRawEditor() { + if (this._rawEditor?.destroy) { + this._rawEditor.destroy(); + this._rawEditor = null; + } + }, + + // Format step type for display + formatStepType(type) { + const typeMap = { + 'agent': 'Generation', + 'code_exe': 'Code Execution', + 'tool': 'Tool Call', + 'mcp': 'MCP Tool', + 'browser': 'Browser', + 'response': 'Response', + 'info': 'Info', + 'hint': 'Hint', + 'warning': 'Warning', + 'error': 'Error', + 'util': 'Utility', + 'progress': 'Progress' + }; + return typeMap[type] || (type ? type.charAt(0).toUpperCase() + type.slice(1) : 'Unknown'); + }, + + // Format timestamp for display + formatTimestamp(timestamp) { + if (!timestamp) return ''; + const date = new Date(parseFloat(timestamp) * 1000); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${hours}:${minutes}:${seconds}`; + }, + + // Format duration for display + formatDuration(ms) { + if (!ms) return ''; + if (ms < 1000) return `${ms}ms`; + const seconds = Math.floor(ms / 1000); + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; + }, + + // Format value for display (handles objects) + formatValue(value) { + if (value === null || value === undefined) return ''; + if (typeof value === 'object') return JSON.stringify(value, null, 2); + return String(value); + }, + + // Format key for display (title case) + formatKey(key) { + return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + }, + + // Clean text value (handle arrays, remove brackets) + cleanTextValue(value) { + if (Array.isArray(value)) { + return value + .filter(item => item && String(item).trim() && !/^[\[\]]$/.test(String(item).trim())) + .join('\n'); + } + if (typeof value === 'object' && value !== null) { + return JSON.stringify(value, null, 2); + } + return String(value).replace(/^\s*[\[\]]\s*$/gm, '').trim(); + }, + + // Clean heading by removing icon:// prefixes + cleanHeading(text) { + if (!text) return ""; + return String(text) + .replace(/icon:\/\/[a-zA-Z0-9_]+\s*/g, "") + .trim(); + } +}; + +export const store = createStore("stepDetail", model); diff --git a/webui/js/messages.js b/webui/js/messages.js index a92bf47d7..070410ff8 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -5,6 +5,7 @@ import { store as _messageResizeStore } from "/components/messages/resize/messag import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js"; import { addActionButtonsToElement } from "/components/messages/action-buttons/simple-action-buttons.js"; import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js"; +import { store as stepDetailStore } from "/components/modals/process-step-detail/step-detail-store.js"; import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js"; import { formatDuration } from "./time-utils.js"; @@ -1353,6 +1354,23 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul renderStepDetailContent(detailContent, content, kvps, type); detail.appendChild(detailContent); + + // Store step data on the element for fresh access on modal open + step._stepData = { + type, + heading, + content, + kvps, + timestamp, + durationMs, + agentNumber, + toolName: toolNameToUse + }; + + // Add "View Details" button for full modal view (reads fresh data from step._stepData) + const viewDetailsBtn = createViewDetailsButton(step); + detail.appendChild(viewDetailsBtn); + step.appendChild(detail); // Track delegation steps for nesting @@ -1462,6 +1480,19 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati } } + // Update stored step data for fresh access by modal + const timestamp = stepElement._stepData?.timestamp; // preserve original timestamp + stepElement._stepData = { + type, + heading, + content, + kvps, + timestamp, + durationMs, + agentNumber, + toolName: toolNameToUse + }; + // Update parent group header const group = stepElement.closest(".process-group"); if (group) { @@ -1788,6 +1819,30 @@ function renderThoughts(container, value) { } } +/** + * Create "View Details" button for opening step detail modal + * @param {HTMLElement} stepElement - The step DOM element containing _stepData property + */ +function createViewDetailsButton(stepElement) { + const btnContainer = document.createElement("div"); + btnContainer.classList.add("step-detail-actions"); + + const btn = document.createElement("button"); + btn.classList.add("btn", "text-button"); + btn.innerHTML = 'open_in_full View Details'; + btn.title = "Open full step details in modal"; + + btn.addEventListener("click", (e) => { + e.stopPropagation(); + // Read fresh data from the step element at click time + const freshData = stepElement._stepData || {}; + stepDetailStore.showStepDetail(freshData); + }); + + btnContainer.appendChild(btn); + return btnContainer; +} + /** * Update process group header with step count, status, and metrics */ From 6aaadfaf2f7243eacba720a1879a57c5a872cd2d Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Tue, 20 Jan 2026 16:09:10 +0100 Subject: [PATCH 10/11] view details btn fix --- webui/components/messages/process-group/process-group.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webui/components/messages/process-group/process-group.css b/webui/components/messages/process-group/process-group.css index 671ed5414..267e8869c 100644 --- a/webui/components/messages/process-group/process-group.css +++ b/webui/components/messages/process-group/process-group.css @@ -898,7 +898,12 @@ .step-detail-actions { display: flex; padding-left: 18px; - margin-bottom: 1rem; + min-height: 0; /* Required for grid collapse animation */ +} + +/* Hide View Details button when step is collapsed */ +.process-step:not(.step-expanded) > .process-step-detail > .step-detail-actions { + display: none; } From f95db44a511868dba28f02742a45f5605f38313a Mon Sep 17 00:00:00 2001 From: 3clyp50 Date: Tue, 20 Jan 2026 16:04:48 +0100 Subject: [PATCH 11/11] error redesign error css fixes css cleanup copy btn in errors --- .../action-buttons/simple-action-buttons.css | 3 +- .../process-group/process-group-store.js | 7 + .../messages/process-group/process-group.css | 4 +- webui/css/messages.css | 154 ++++++++++++++++-- webui/js/messages.js | 130 +++++++++++++-- 5 files changed, 272 insertions(+), 26 deletions(-) diff --git a/webui/components/messages/action-buttons/simple-action-buttons.css b/webui/components/messages/action-buttons/simple-action-buttons.css index 904ef9847..ab148370e 100644 --- a/webui/components/messages/action-buttons/simple-action-buttons.css +++ b/webui/components/messages/action-buttons/simple-action-buttons.css @@ -101,7 +101,8 @@ /* .kvps-row:hover .action-buttons, */ .message-text:hover .action-buttons, .kvps-val:hover .action-buttons, -.message-body:hover > .action-buttons { +.message-body:hover > .action-buttons, +.error-content-inner:hover .action-buttons { display: flex; animation: fadeInAfterDelay 0.3s ease-in-out; animation-delay: 0.3s; diff --git a/webui/components/messages/process-group/process-group-store.js b/webui/components/messages/process-group/process-group-store.js index e2be3a79b..b85dd93f3 100644 --- a/webui/components/messages/process-group/process-group-store.js +++ b/webui/components/messages/process-group/process-group-store.js @@ -230,6 +230,13 @@ const model = { } step.classList.toggle("step-expanded", shouldExpand); }); + + // Apply to error groups + const allErrorGroups = document.querySelectorAll(".error-group"); + allErrorGroups.forEach(errorGroup => { + const shouldExpand = mode === "current" || mode === "expanded"; + errorGroup.classList.toggle("expanded", shouldExpand); + }); } }; diff --git a/webui/components/messages/process-group/process-group.css b/webui/components/messages/process-group/process-group.css index 267e8869c..c9e9d3f72 100644 --- a/webui/components/messages/process-group/process-group.css +++ b/webui/components/messages/process-group/process-group.css @@ -210,7 +210,7 @@ /* ERR - error type (red) */ .status-err { - --step-accent: #ef4444; + --step-accent: var(--color-accent); color: var(--step-accent); } @@ -742,7 +742,7 @@ } .light-mode .status-err { - --step-accent: #b91c1c; + --step-accent: var(--color-accent); color: var(--step-accent); } diff --git a/webui/css/messages.css b/webui/css/messages.css index 9acf087b9..61fd4450c 100644 --- a/webui/css/messages.css +++ b/webui/css/messages.css @@ -272,13 +272,138 @@ background-color: transparent; } -.message-error { - background-color: rgba(180, 40, 40, 0.25); - border: 1px solid rgba(220, 60, 60, 0.5); - border-radius: 8px !important; - padding: 12px !important; +/* Collapsible Error Group */ + +.message-error-group { + background: transparent; + border: none !important; + padding: 0 !important; } +.error-group { + display: inline-flex; + flex-direction: column; + position: relative; + z-index: 1; + margin: var(--spacing-xs) 0; + padding: var(--spacing-xs) 0; + min-width: 200px; + max-width: 100%; + box-sizing: border-box; + flex-shrink: 0; +} + +/* Error Group Header */ +.error-group-header { + display: flex; + align-items: center; + padding: 0; + cursor: pointer; + user-select: none; + transition: opacity 0.15s ease; + min-height: 22px; + gap: 6px; + white-space: nowrap; +} + +.error-group-header:hover { + opacity: 0.85; +} + +/* Expand/collapse triangle */ +.error-group-header .expand-icon { + display: inline-block; + width: 0; + height: 0; + border-style: solid; + border-width: 4px 0 4px 6px; + border-color: transparent transparent transparent #ef4444; + opacity: 0.7; + transition: transform 0.2s ease, opacity 0.15s ease; + flex-shrink: 0; + font-size: 0; + margin-right: 2px; +} + +.error-group-header:hover .expand-icon { + opacity: 1; +} + +.error-group.expanded .error-group-header .expand-icon { + transform: rotate(90deg); +} + +/* Error title */ +.error-group-header .error-title { + flex: 0 0 auto; + font-size: var(--font-size-medium); + font-weight: 600; + color: var(--color-accent); + opacity: 0.95; +} + +/* Error subtitle (short description) */ +.error-group-header .error-subtitle { + flex: 1 1 auto; + font-size: 0.75rem; + color: var(--color-text); + opacity: 0.7; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-left: var(--spacing-xs); + font-family: var(--font-family-code); +} + +/* Error Group Content - Animated expand/collapse */ +.error-group-content { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + margin-top: 0; + padding-top: 0; + transition: grid-template-rows 0.25s ease-out, + opacity 0.2s ease-out, + margin-top 0.25s ease-out, + padding-top 0.25s ease-out; + overflow: hidden; +} + +.error-group-content > .error-content-inner { + min-height: 0; +} + +.error-group.expanded .error-group-content { + grid-template-rows: 1fr; + opacity: 1; + margin-top: var(--spacing-xs); + padding-top: var(--spacing-xs); +} + +.error-content-inner { + padding: var(--spacing-md); + border-radius: 8px; + border: 1px solid var(--color-accent); + margin-left: var(--spacing-md); +} + +/* Callstack styling */ +.error-callstack { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--font-family-code); + font-size: 0.72rem; + line-height: 1.5; + color: var(--color-accent); + opacity: 0.9; + max-height: 30vh; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--color-accent) transparent; +} + + /* Terminal styling moved to new terminal block above */ /* Agent and AI Info */ @@ -497,10 +622,17 @@ border-bottom: none; } -.light-mode .message-error { - background-color: rgba(220, 60, 60, 0.15); - border: 1px solid rgba(180, 40, 40, 0.4); - color: #8f1010; +/* Light mode error group */ +.light-mode .error-group-header .expand-icon { + border-color: transparent transparent transparent var(--color-accent); +} + +.light-mode .error-group-header .error-title { + color: var(--color-accent); +} + +.light-mode .error-content-inner { + border: 1px solid var(--color-accent); } .light-mode .message-user { @@ -636,10 +768,6 @@ margin-bottom:5em; } -.message-group-mid { - margin-left: 2em; -} - .message-container { animation: fadeIn 0.2s; -webkit-animation: fadeIn 0.2s; diff --git a/webui/js/messages.js b/webui/js/messages.js index 070410ff8..5925aa86a 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -789,16 +789,126 @@ export function drawMessageError( temp, kvps = null ) { - return drawMessageAgentPlain( - "message-error", - messageContainer, - id, - type, - heading, - content, - temp, - kvps - ); + // Create or get the message div + let messageDiv = messageContainer.querySelector(".message"); + if (!messageDiv) { + messageDiv = document.createElement("div"); + messageDiv.classList.add("message", "message-error-group"); + messageContainer.appendChild(messageDiv); + } + + // Check if error group already exists + let errorGroup = messageDiv.querySelector(".error-group"); + if (!errorGroup) { + errorGroup = document.createElement("div"); + errorGroup.classList.add("error-group"); + errorGroup.setAttribute("data-error-id", id); + + // Create header (clickable for expand/collapse) + const header = document.createElement("div"); + header.classList.add("error-group-header"); + + // Expand icon (triangle) + const expandIcon = document.createElement("span"); + expandIcon.classList.add("expand-icon"); + header.appendChild(expandIcon); + + // Status badge (before title) + const badge = document.createElement("span"); + badge.classList.add("status-badge", "status-err"); + badge.textContent = "ERR"; + header.appendChild(badge); + + // Title + const title = document.createElement("span"); + title.classList.add("error-title"); + title.textContent = "Error"; + header.appendChild(title); + + // Subtitle (short error description) + const subtitle = document.createElement("span"); + subtitle.classList.add("error-subtitle"); + header.appendChild(subtitle); + + // Click handler for expand/collapse + header.addEventListener("click", () => { + errorGroup.classList.toggle("expanded"); + }); + + errorGroup.appendChild(header); + + // Create content container (collapsible) + const contentWrapper = document.createElement("div"); + contentWrapper.classList.add("error-group-content"); + + const contentInner = document.createElement("div"); + contentInner.classList.add("error-content-inner"); + contentWrapper.appendChild(contentInner); + + errorGroup.appendChild(contentWrapper); + messageDiv.appendChild(errorGroup); + + // Check detail mode and expand if needed + const detailMode = window.Alpine?.store("preferences")?.detailMode || "current"; + if (detailMode === "current" || detailMode === "expanded") { + errorGroup.classList.add("expanded"); + } + } + + // Update subtitle with short error description + const subtitle = errorGroup.querySelector(".error-subtitle"); + if (subtitle) { + // Extract short description from heading or content + let shortDesc = ""; + // Skip if heading is just "Error" (redundant with title) + if (heading && heading.trim() && heading.trim().toLowerCase() !== "error") { + shortDesc = heading.trim(); + } + // If no useful heading, try to extract from content + if (!shortDesc && content && content.trim()) { + const lines = content.trim().split("\n"); + // Look for the error line (usually last meaningful line or one matching ErrorType: pattern) + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i].trim(); + if (line && /^[\w\.]+Error[:\s]/.test(line)) { + shortDesc = line; + break; + } + } + // Fallback to first non-empty line if no error pattern found + if (!shortDesc) { + for (const line of lines) { + if (line.trim() && !line.startsWith("Traceback")) { + shortDesc = line.trim(); + break; + } + } + } + } + // Truncate if too long + if (shortDesc.length > 100) { + shortDesc = shortDesc.substring(0, 97) + "..."; + } + subtitle.textContent = shortDesc; + subtitle.title = shortDesc; // Full text on hover + } + + // Update content (full callstack) + const contentInner = errorGroup.querySelector(".error-content-inner"); + if (contentInner && content) { + contentInner.innerHTML = ""; + + // Create pre element for callstack/content + const pre = document.createElement("pre"); + pre.classList.add("error-callstack"); + pre.textContent = content; + contentInner.appendChild(pre); + + // Add action buttons for copy functionality + addActionButtonsToElement(contentInner); + } + + messageContainer.classList.add("center-container"); } function drawKvps(container, kvps, latex) {