message handling refactor #1

3clyp50 committed Jan 23, 2026 at 14:13 UTC 23beed94de369ee343541b5012451eb0edffa07a
7 files changed +832 -864
webui/components/messages/action-buttons/simple-action-buttons.js
+111 -62
@@ -1,5 +1,6 @@
1 // Message Action Buttons - Copy and Speak functionality
2 import { store as speechStore } from "/components/chat/speech/speech-store.js";
3 +import { store as stepDetailStore } from "/components/modals/process-step-detail/step-detail-store.js";
4
5 /**
6 * Copy text to clipboard with fallback for non-secure contexts
@@ -48,77 +49,125 @@ function createButton(iconName, label, className) {
49 }
50
51 /**
51 - * Add action buttons (copy, speak, optionally view details) to an element
52 - *
52 + * Add action buttons (copy, speak, optionally view details) to an element.
53 + * Data is attached to buttons as data attributes for DOM-first behavior.
54 + *
55 * @param {HTMLElement} container - Element to append buttons to
56 * @param {Object} options - Configuration
55 - * @param {string|Function|HTMLElement} options.contentRef - Text content source:
56 - * - string: Use directly
57 - * - Function: Call to get text
58 - * - HTMLElement: Get innerText from element
59 - * @param {Function} [options.onViewDetails] - If provided, adds view details button
57 + * @param {string|Function|HTMLElement} [options.contentRef] - Text content source
58 + * @param {Object} [options.detailPayload] - Detail payload for modal
59 + * @param {Function} [options.onViewDetails] - Optional detail handler
60 + * @param {string} [options.copyContent] - Text for copy action
61 + * @param {string} [options.speakContent] - Text for speak action
62 */
63 export function addActionButtonsToElement(container, options = {}) {
62 - const { contentRef, onViewDetails } = options;
63 -
64 - // Skip if buttons already exist
65 - if (container.querySelector(".step-action-buttons")) return;
66 -
67 - // Create buttons container
68 - const buttonsDiv = document.createElement("div");
69 - buttonsDiv.className = "step-action-buttons";
70 -
71 - // Helper to resolve content from contentRef
72 - const getContent = () => {
73 - if (typeof contentRef === "string") return contentRef;
74 - if (typeof contentRef === "function") return contentRef();
75 - if (contentRef instanceof HTMLElement) return contentRef.innerText || "";
64 + const {
65 + contentRef,
66 + detailPayload,
67 + onViewDetails,
68 + copyContent,
69 + speakContent
70 + } = options;
71 +
72 + const resolveContent = (explicit) => {
73 + if (typeof explicit === "string") return explicit;
74 + if (typeof explicit === "function") return explicit();
75 + if (explicit instanceof HTMLElement) return explicit.innerText || "";
76 return "";
77 };
78 -
78 +
79 + const resolvedCopyContent =
80 + resolveContent(copyContent ?? contentRef) || container.innerText || "";
81 + const resolvedSpeakContent =
82 + resolveContent(speakContent ?? contentRef) || container.innerText || "";
83 +
84 + let buttonsDiv = container.querySelector(".step-action-buttons");
85 + if (!buttonsDiv) {
86 + buttonsDiv = document.createElement("div");
87 + buttonsDiv.className = "step-action-buttons";
88 + container.appendChild(buttonsDiv);
89 + }
90 +
91 + const setDetailPayload = (btn) => {
92 + if (detailPayload) {
93 + btn.dataset.detailPayload = JSON.stringify(detailPayload);
94 + } else {
95 + delete btn.dataset.detailPayload;
96 + }
97 + if (onViewDetails) {
98 + btn._detailHandler = onViewDetails;
99 + } else {
100 + delete btn._detailHandler;
101 + }
102 + };
103 +
104 // View Details button (optional)
80 - if (onViewDetails) {
81 - const viewBtn = createButton("open_in_full", "View details", "view-details-action");
82 - viewBtn.onclick = (e) => {
105 + let viewBtn = buttonsDiv.querySelector(".view-details-action");
106 + if (detailPayload || onViewDetails) {
107 + if (!viewBtn) {
108 + viewBtn = createButton("open_in_full", "View details", "view-details-action");
109 + viewBtn.onclick = (e) => {
110 + e.stopPropagation();
111 + const handler = viewBtn._detailHandler;
112 + if (typeof handler === "function") {
113 + handler();
114 + return;
115 + }
116 + const payload = viewBtn.dataset.detailPayload;
117 + if (payload) {
118 + try {
119 + stepDetailStore.showStepDetail(JSON.parse(payload));
120 + } catch (err) {
121 + console.error("Failed to parse detail payload:", err);
122 + }
123 + }
124 + };
125 + buttonsDiv.appendChild(viewBtn);
126 + }
127 + setDetailPayload(viewBtn);
128 + } else if (viewBtn) {
129 + viewBtn.remove();
130 + }
131 +
132 + // Copy button
133 + let copyBtn = buttonsDiv.querySelector(".copy-action");
134 + if (!copyBtn) {
135 + copyBtn = createButton("content_copy", "Copy text", "copy-action");
136 + copyBtn.onclick = async (e) => {
137 e.stopPropagation();
84 - onViewDetails();
138 + const text = copyBtn.dataset.copyContent || "";
139 + if (!text) return;
140 +
141 + try {
142 + await copyToClipboard(text);
143 + showButtonFeedback(copyBtn, true, "content_copy");
144 + } catch (err) {
145 + console.error("Copy failed:", err);
146 + showButtonFeedback(copyBtn, false, "content_copy");
147 + }
148 };
86 - buttonsDiv.appendChild(viewBtn);
149 + buttonsDiv.appendChild(copyBtn);
150 }
88 -
89 - // Copy button
90 - const copyBtn = createButton("content_copy", "Copy text", "copy-action");
91 - copyBtn.onclick = async (e) => {
92 - e.stopPropagation();
93 - const text = getContent();
94 - if (!text) return;
95 -
96 - try {
97 - await copyToClipboard(text);
98 - showButtonFeedback(copyBtn, true, "content_copy");
99 - } catch (err) {
100 - console.error("Copy failed:", err);
101 - showButtonFeedback(copyBtn, false, "content_copy");
102 - }
103 - };
104 - buttonsDiv.appendChild(copyBtn);
105 -
151 + copyBtn.dataset.copyContent = resolvedCopyContent;
152 +
153 // Speak button
107 - const speakBtn = createButton("volume_up", "Speak text", "speak-action");
108 - speakBtn.onclick = async (e) => {
109 - e.stopPropagation();
110 - const text = getContent();
111 - if (!text?.trim()) return;
112 -
113 - try {
114 - showButtonFeedback(speakBtn, true, "volume_up");
115 - await speechStore.speak(text);
116 - } catch (err) {
117 - console.error("Speech failed:", err);
118 - showButtonFeedback(speakBtn, false, "volume_up");
119 - }
120 - };
121 - buttonsDiv.appendChild(speakBtn);
122 -
123 - container.appendChild(buttonsDiv);
154 + let speakBtn = buttonsDiv.querySelector(".speak-action");
155 + if (!speakBtn) {
156 + speakBtn = createButton("volume_up", "Speak text", "speak-action");
157 + speakBtn.onclick = async (e) => {
158 + e.stopPropagation();
159 + const text = speakBtn.dataset.speakContent || "";
160 + if (!text.trim()) return;
161 +
162 + try {
163 + showButtonFeedback(speakBtn, true, "volume_up");
164 + await speechStore.speak(text);
165 + } catch (err) {
166 + console.error("Speech failed:", err);
167 + showButtonFeedback(speakBtn, false, "volume_up");
168 + }
169 + };
170 + buttonsDiv.appendChild(speakBtn);
171 + }
172 + speakBtn.dataset.speakContent = resolvedSpeakContent;
173 }
webui/components/messages/process-group/process-group-dom.js new
+87
@@ -0,0 +1,87 @@
1 +/**
2 + * Process group DOM utilities (no store/state)
3 + */
4 +
5 +export function applyModeSteps(detailMode, showUtils) {
6 + const mode =
7 + detailMode ||
8 + window.Alpine?.store("preferences")?.detailMode ||
9 + "current";
10 + const showUtilsFlag =
11 + typeof showUtils === "boolean"
12 + ? showUtils
13 + : window.Alpine?.store("preferences")?.showUtils || false;
14 +
15 + const chatHistory = document.getElementById("chat-history");
16 + if (!chatHistory) return;
17 +
18 + const shouldExpandGroup = mode !== "collapsed";
19 + const shouldExpandError = mode === "current" || mode === "expanded";
20 +
21 + // Walk DOM once (reverse to match message traversal patterns)
22 + const groups = chatHistory.children;
23 + for (let gi = groups.length - 1; gi >= 0; gi -= 1) {
24 + const messageGroup = groups[gi];
25 + const containers = messageGroup.children;
26 + for (let ci = containers.length - 1; ci >= 0; ci -= 1) {
27 + const container = containers[ci];
28 +
29 + if (container.classList.contains("has-process-group")) {
30 + const processGroup = container.querySelector(".process-group");
31 + if (processGroup) {
32 + applyModeToProcessGroup(processGroup, mode, showUtilsFlag, shouldExpandGroup);
33 + }
34 + }
35 +
36 + const errorGroups = container.getElementsByClassName("error-group");
37 + if (errorGroups.length) {
38 + for (let ei = 0; ei < errorGroups.length; ei += 1) {
39 + errorGroups[ei].classList.toggle("expanded", shouldExpandError);
40 + }
41 + }
42 + }
43 + }
44 +}
45 +
46 +function applyModeToProcessGroup(group, mode, showUtilsFlag, shouldExpandGroup) {
47 + group.classList.toggle("expanded", shouldExpandGroup);
48 +
49 + const isActiveGroup = group.classList.contains("active");
50 + const isGroupCompleted = group.classList.contains("process-group-completed");
51 + const steps = group.getElementsByClassName("process-step");
52 + if (!steps.length) return;
53 +
54 + let lastVisibleStep = null;
55 + const shouldFindLastVisible = mode === "current" && isActiveGroup && !isGroupCompleted;
56 +
57 + for (let i = steps.length - 1; i >= 0; i -= 1) {
58 + const step = steps[i];
59 +
60 + if (shouldFindLastVisible && !lastVisibleStep) {
61 + if (showUtilsFlag || !step.classList.contains("message-util")) {
62 + lastVisibleStep = step;
63 + }
64 + }
65 +
66 + let shouldExpand = false;
67 + if (mode === "expanded") {
68 + shouldExpand = true;
69 + } else if (mode === "current" && isActiveGroup) {
70 + shouldExpand = step === lastVisibleStep;
71 + }
72 +
73 + if (shouldExpand) {
74 + step.classList.add("step-expanded");
75 + } else {
76 + const shouldDefer =
77 + mode === "current" &&
78 + isActiveGroup &&
79 + lastVisibleStep &&
80 + step !== lastVisibleStep;
81 + if (!shouldDefer) {
82 + step.classList.remove("step-expanded");
83 + step.removeAttribute("data-user-pinned");
84 + }
85 + }
86 + }
87 +}
webui/components/messages/process-group/process-group-store.js deleted
-182
@@ -1,182 +0,0 @@
1 -import { createStore } from "/js/AlpineStore.js";
2 -import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
3 -
4 -// Process Group Store - manages collapsible process groups in chat
5 -
6 -// Unified mapping for both Tool Names and Step Types
7 -// Specific tool names (keys) take precedence over generic types
8 -const DISPLAY_CODES = {
9 - // --- Specific Tools ---
10 - 'call_subordinate': 'SUB',
11 - 'search_engine': 'WEB',
12 - 'a2a_chat': 'A2A',
13 - 'behaviour_adjustment': 'ADJ',
14 - 'document_query': 'DOC',
15 - 'vision_load': 'EYE',
16 - 'notify_user': 'NTF',
17 - 'scheduler': 'SCH',
18 - 'unknown': 'UNK',
19 - // Memory operations group
20 - 'memory_save': 'MEM',
21 - 'memory_load': 'MEM',
22 - 'memory_forget': 'MEM',
23 - 'memory_delete': 'MEM',
24 -
25 - // --- Step Types ---
26 - 'agent': 'GEN',
27 - 'response': 'END',
28 - 'tool': 'USE', // Generic fallback for tools
29 - 'code_exe': 'EXE',
30 - 'browser': 'WWW',
31 - 'progress': 'HLD',
32 - 'subagent': 'SUB', // Type fallback if tool name missing
33 - 'mcp': 'MCP',
34 - 'info': 'INF',
35 - 'hint': 'HNT',
36 - 'warning': 'WRN',
37 - 'error': 'ERR',
38 - 'util': 'UTL',
39 - 'done': 'END'
40 -};
41 -
42 -const model = {
43 - init() {},
44 -
45 - // Toggle process group expansion (called from click handler in messages.js)
46 - toggleGroup(groupId) {
47 - const groupElement = document.getElementById(groupId);
48 - if (!groupElement) return;
49 - groupElement.classList.toggle("expanded");
50 - },
51 -
52 - // Toggle step expansion (called from click handler in messages.js)
53 - toggleStep(groupId, stepId) {
54 - const stepElement = document.getElementById(`process-step-${stepId}`);
55 - if (!stepElement) return;
56 - stepElement.classList.toggle("step-expanded");
57 - },
58 -
59 - // Status code (3-4 letter) for backend log types
60 - // Looks up tool name first (specific), then falls back to type (generic)
61 - getStepCode(type, toolName = null) {
62 - // Specific tool codes only apply to generic 'tool' steps
63 - if (type === 'tool' && toolName && DISPLAY_CODES[toolName]) {
64 - return DISPLAY_CODES[toolName];
65 - }
66 -
67 - return DISPLAY_CODES[type] ||
68 - type?.toUpperCase()?.slice(0, 4) ||
69 - 'GEN';
70 - },
71 -
72 - // CSS color class for backend log types
73 - // Looks up tool name first (specific), then falls back to type (generic)
74 - getStatusColorClass(type, toolName = null) {
75 - // Specific tool name mappings for 'tool' steps
76 - if (type === 'tool' && toolName) {
77 - // call_subordinate gets teal (SUB color)
78 - if (toolName === 'call_subordinate') {
79 - return 'status-sub';
80 - }
81 - // Add other specific tool mappings here if needed in the future
82 - }
83 -
84 - const colors = {
85 - 'agent': 'status-gen',
86 - 'response': 'status-end',
87 - 'tool': 'status-tool',
88 - 'mcp': 'status-mcp',
89 - 'subagent': 'status-sub',
90 - 'code_exe': 'status-exe',
91 - 'browser': 'status-www',
92 - 'progress': 'status-wait',
93 - 'info': 'status-inf',
94 - 'hint': 'status-hnt',
95 - 'warning': 'status-wrn',
96 - 'error': 'status-err',
97 - 'util': 'status-utl',
98 - 'done': 'status-end'
99 - };
100 - return colors[type] || 'status-gen';
101 - },
102 -
103 - // Get current detail mode from preferences
104 - _getDetailMode() {
105 - return preferencesStore.detailMode || "current";
106 - },
107 -
108 - shouldExpandGroup() {
109 - const mode = this._getDetailMode();
110 - // Groups expand in "current" and "expanded" modes, collapse only in "collapsed" mode
111 - return mode !== "collapsed";
112 - },
113 -
114 - // Apply current mode to all existing DOM elements
115 - applyModeSteps() {
116 - const mode = this._getDetailMode();
117 - const showUtils = preferencesStore.showUtils || false;
118 - const allGroups = document.querySelectorAll(".process-group");
119 -
120 - // Find the active group (currently streaming) - DOM is source of truth
121 - const activeGroup = document.querySelector(".process-group.active");
122 -
123 - // Find the last visible step in the ACTIVE group only (for "current" mode)
124 - let lastActiveStep = null;
125 - if (activeGroup && !activeGroup.classList.contains("process-group-completed")) {
126 - const stepSelector = showUtils
127 - ? ".process-step"
128 - : ".process-step:not(.message-util)";
129 - const stepsInActiveGroup = activeGroup.querySelectorAll(stepSelector);
130 - lastActiveStep = stepsInActiveGroup.length > 0 ? stepsInActiveGroup[stepsInActiveGroup.length - 1] : null;
131 - }
132 -
133 - // Apply to groups
134 - allGroups.forEach(group => {
135 - const shouldExpandGroup = mode !== "collapsed";
136 - group.classList.toggle("expanded", shouldExpandGroup);
137 - });
138 -
139 - // Apply to steps - different logic for active vs non-active groups
140 - allGroups.forEach(group => {
141 - const isActiveGroup = group.classList.contains("active");
142 - const steps = group.querySelectorAll(".process-step");
143 -
144 - steps.forEach(step => {
145 - let shouldExpand = false;
146 -
147 - if (mode === "expanded") {
148 - shouldExpand = true;
149 - } else if (mode === "current") {
150 - if (isActiveGroup) {
151 - // Active group: only expand the last step
152 - shouldExpand = step === lastActiveStep;
153 - }
154 - // Non-active groups: shouldExpand stays false → all steps collapsed
155 - }
156 - // In "collapsed" mode: shouldExpand stays false
157 -
158 - if (shouldExpand) {
159 - step.classList.add("step-expanded");
160 - } else {
161 - // For non-active groups or collapsed mode, immediately collapse
162 - // Only defer to timeout for active group in "current" mode when there's an active step
163 - // (lastActiveStep is null if group is completed - no deferral needed)
164 - const shouldDefer = mode === "current" && isActiveGroup && lastActiveStep && step !== lastActiveStep;
165 - if (!shouldDefer) {
166 - step.classList.remove("step-expanded");
167 - step.removeAttribute("data-user-pinned");
168 - }
169 - }
170 - });
171 - });
172 -
173 - // Apply to error groups
174 - const allErrorGroups = document.querySelectorAll(".error-group");
175 - allErrorGroups.forEach(errorGroup => {
176 - const shouldExpand = mode === "current" || mode === "expanded";
177 - errorGroup.classList.toggle("expanded", shouldExpand);
178 - });
179 - }
180 -};
181 -
182 -export const store = createStore("processGroup", model);
webui/components/modals/process-step-detail/process-step-detail.html
+2 -2
@@ -16,8 +16,8 @@
16 <div class="modal-header modal-header-compact">
17 <div class="header-info">
18 <span class="status-badge"
19 - :class="$store.processGroup.getStatusColorClass($store.stepDetail.selectedStepForDetail?.type, $store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name)"
20 - x-text="$store.processGroup.getStepCode($store.stepDetail.selectedStepForDetail?.type, $store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name)"></span>
19 + :class="$store.stepDetail.selectedStepForDetail?.statusClass || ''"
20 + x-text="$store.stepDetail.selectedStepForDetail?.statusCode || ''"></span>
21 <span class="step-type-label" x-text="$store.stepDetail.formatStepType($store.stepDetail.selectedStepForDetail?.type)"></span>
22 <template x-if="$store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name">
23 <span class="tool-name-badge" x-text="$store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name"></span>
webui/components/sidebar/bottom/preferences/preferences-store.js
+24 -6
@@ -1,7 +1,7 @@
1 import { createStore } from "/js/AlpineStore.js";
2 import * as css from "/js/css.js";
3 import { store as speechStore } from "/components/chat/speech/speech-store.js";
4 -import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
4 +import { applyModeSteps } from "/components/messages/process-group/process-group-dom.js";
5
6 // Preferences store centralizes user preference toggles and side-effects
7 const model = {
@@ -156,11 +156,29 @@ const model = {
156 value ? undefined : "none"
157 );
158 // For process steps - toggle class on all existing elements
159 - document.querySelectorAll(".process-step.message-util").forEach((el) => {
160 - el.classList.toggle("show-util", value);
161 - });
159 + const chatHistory = document.getElementById("chat-history");
160 + if (chatHistory) {
161 + const groups = chatHistory.children;
162 + for (let gi = groups.length - 1; gi >= 0; gi -= 1) {
163 + const messageGroup = groups[gi];
164 + const containers = messageGroup.children;
165 + for (let ci = containers.length - 1; ci >= 0; ci -= 1) {
166 + const container = containers[ci];
167 + if (!container.classList.contains("has-process-group")) continue;
168 + const processGroup = container.querySelector(".process-group");
169 + if (!processGroup) continue;
170 + const steps = processGroup.getElementsByClassName("process-step");
171 + for (let si = 0; si < steps.length; si += 1) {
172 + const step = steps[si];
173 + if (step.classList.contains("message-util")) {
174 + step.classList.toggle("show-util", value);
175 + }
176 + }
177 + }
178 + }
179 + }
180 // Re-apply detail mode to reset current visible step
163 - processGroupStore.applyModeSteps();
181 + applyModeSteps(this._detailMode, this._showUtils);
182 },
183
184 _applyChatWidth(value) {
@@ -177,7 +195,7 @@ const model = {
195 _applyDetailMode(value) {
196 localStorage.setItem("detailMode", value);
197 // Apply mode to all existing DOM elements
180 - processGroupStore.applyModeSteps();
198 + applyModeSteps(this._detailMode, this._showUtils);
199 },
200 };
201
webui/index.js
+5 -10
@@ -11,7 +11,7 @@ import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
11 import { store as tasksStore } from "/components/sidebar/tasks/tasks-store.js";
12 import { store as chatTopStore } from "/components/chat/top-section/chat-top-store.js";
13 import { store as _tooltipsStore } from "/components/tooltips/tooltip-store.js";
14 -import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
14 +import { applyModeSteps } from "/components/messages/process-group/process-group-dom.js";
15
16 globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
17
@@ -66,7 +66,7 @@ export async function sendMessage() {
66 : "";
67
68 // Render user message with attachments
69 - setMessage(messageId, "user", heading, message, false, {
69 + setMessage(messageId, "user", heading, message, {
70 // attachments: attachmentsWithUrls, // skip here, let the backend properly log them
71 });
72
@@ -200,8 +200,8 @@ async function updateUserTime() {
200 updateUserTime();
201 setInterval(updateUserTime, 1000);
202
203 -function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, /* tokensIn = 0, tokensOut = 0, */ agentNumber = 0) {
204 - const result = msgs.setMessage(id, type, heading, content, temp, kvps, timestamp, durationMs, /* tokensIn, tokensOut, */ agentNumber);
203 +function setMessage(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, /* tokensIn = 0, tokensOut = 0, */ agentNumber = 0) {
204 + const result = msgs.setMessage(id, type, heading, content, kvps, timestamp, durationMs, /* tokensIn, tokensOut, */ agentNumber);
205 const chatHistoryEl = document.getElementById("chat-history");
206 if (preferencesStore.autoScroll && chatHistoryEl) {
207 chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;
@@ -308,7 +308,6 @@ export async function poll() {
308 if (lastLogGuid != response.log_guid) {
309 const chatHistoryEl = document.getElementById("chat-history");
310 if (chatHistoryEl) chatHistoryEl.innerHTML = "";
311 - msgs.resetProcessGroups(); // Reset process groups on chat reset
311 lastLogVersion = 0;
312 lastLogGuid = response.log_guid;
313 await poll();
@@ -324,7 +323,6 @@ export async function poll() {
323 log.type,
324 log.heading,
325 log.content,
327 - log.temp,
326 log.kvps,
327 log.timestamp,
328 log.duration_ms,
@@ -334,7 +332,7 @@ export async function poll() {
332 );
333 }
334 afterMessagesUpdate(response.logs);
337 - processGroupStore.applyModeSteps();
335 + applyModeSteps(preferencesStore.detailMode, preferencesStore.showUtils);
336 }
337
338 lastLogVersion = response.log_version;
@@ -512,9 +510,6 @@ export const setContext = function (id) {
510 // Stop speech when switching chats
511 speechStore.stopAudio();
512
515 - // Reset process groups for new context
516 - msgs.resetProcessGroups();
517 -
513 // Clear the chat history immediately to avoid showing stale content
514 const chatHistoryEl = document.getElementById("chat-history");
515 if (chatHistoryEl) chatHistoryEl.innerHTML = "";
webui/js/messages.js
+603 -602
@@ -4,8 +4,6 @@ import { marked } from "../vendor/marked/marked.esm.js";
4 import { store as _messageResizeStore } from "/components/messages/resize/message-resize-store.js"; // keep here, required in html
5 import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6 import { addActionButtonsToElement } from "/components/messages/action-buttons/simple-action-buttons.js";
7 -import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
8 -import { store as stepDetailStore } from "/components/modals/process-step-detail/step-detail-store.js";
7 import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
8 import { formatDuration } from "./time-utils.js";
9
@@ -17,11 +15,65 @@ const STEP_COLLAPSE_DELAY_MS = 3000;
15 // Delay before collapsing the last step when processing completes
16 const FINAL_STEP_COLLAPSE_DELAY_MS = 3000;
17
20 -const chatHistory = document.getElementById("chat-history");
18 +// Tool-specific status codes (fallback for tool steps)
19 +const TOOL_STATUS_CODES = {
20 + call_subordinate: "SUB",
21 + search_engine: "WEB",
22 + a2a_chat: "A2A",
23 + behaviour_adjustment: "ADJ",
24 + document_query: "DOC",
25 + vision_load: "EYE",
26 + notify_user: "NTF",
27 + scheduler: "SCH",
28 + unknown: "UNK",
29 + memory_save: "MEM",
30 + memory_load: "MEM",
31 + memory_forget: "MEM",
32 + memory_delete: "MEM"
33 +};
34 +
35 +// Tool-specific status classes (fallback for tool steps)
36 +const TOOL_STATUS_CLASSES = {
37 + call_subordinate: "status-sub"
38 +};
39 +
40 +const TYPE_STATUS_CODES = {
41 + agent: "GEN",
42 + response: "END",
43 + tool: "USE",
44 + code_exe: "EXE",
45 + browser: "WWW",
46 + progress: "HLD",
47 + mcp: "MCP",
48 + subagent: "SUB",
49 + info: "INF",
50 + hint: "HNT",
51 + warning: "WRN",
52 + rate_limit: "WRN",
53 + error: "ERR",
54 + util: "UTL",
55 + done: "END"
56 +};
57 +
58 +const TYPE_STATUS_CLASSES = {
59 + agent: "status-gen",
60 + response: "status-end",
61 + tool: "status-tool",
62 + code_exe: "status-exe",
63 + browser: "status-www",
64 + progress: "status-wait",
65 + mcp: "status-mcp",
66 + subagent: "status-sub",
67 + info: "status-inf",
68 + hint: "status-hnt",
69 + warning: "status-wrn",
70 + rate_limit: "status-wrn",
71 + error: "status-err",
72 + util: "status-utl",
73 + done: "status-end"
74 +};
75
22 -let messageGroup = null;
23 -let currentProcessGroup = null; // Track current process group for collapsible UI
24 -let currentDelegationSteps = {}; // Track delegation steps by agent number for nesting
76 +const chatHistory = document.getElementById("chat-history");
77
78 // handlers for log message rendering
79 export function getMessageHandler(type) {
@@ -38,6 +90,12 @@ export function getMessageHandler(type) {
90 return drawMessageCodeExe;
91 case "browser":
92 return drawMessageBrowser;
93 + case "progress":
94 + return drawMessageProgress;
95 + case "mcp":
96 + return drawMessageMcp;
97 + case "subagent":
98 + return drawMessageSubagent;
99 case "warning":
100 return drawMessageWarning;
101 case "rate_limit":
@@ -49,7 +107,7 @@ export function getMessageHandler(type) {
107 case "util":
108 return drawMessageUtil;
109 case "hint":
52 - return drawMessageInfo;
110 + return drawMessageHint;
111 default:
112 return drawMessageDefault;
113 }
@@ -84,6 +142,51 @@ export function clearActiveStepShine() {
142 });
143 }
144
145 +function getChatHistoryEl() {
146 + return chatHistory || document.getElementById("chat-history");
147 +}
148 +
149 +function getLastMessageContainer() {
150 + const chatHistoryEl = getChatHistoryEl();
151 + if (!chatHistoryEl) return null;
152 + const lastGroup = chatHistoryEl.lastElementChild;
153 + if (!lastGroup) return null;
154 + return lastGroup.lastElementChild;
155 +}
156 +
157 +function appendToMessageGroup(messageContainer, position, forceNewGroup = false) {
158 + const chatHistoryEl = getChatHistoryEl();
159 + if (!chatHistoryEl) return;
160 +
161 + const lastGroup = chatHistoryEl.lastElementChild;
162 + const lastGroupType = lastGroup?.getAttribute("data-group-type");
163 +
164 + if (!forceNewGroup && lastGroup && lastGroupType === position) {
165 + lastGroup.appendChild(messageContainer);
166 + return;
167 + }
168 +
169 + const group = document.createElement("div");
170 + group.classList.add("message-group", `message-group-${position}`);
171 + group.setAttribute("data-group-type", position);
172 + group.appendChild(messageContainer);
173 + chatHistoryEl.appendChild(group);
174 +}
175 +
176 +function getStatusCode(type, toolName = null) {
177 + if (type === "tool" && toolName && TOOL_STATUS_CODES[toolName]) {
178 + return TOOL_STATUS_CODES[toolName];
179 + }
180 + return TYPE_STATUS_CODES[type] || type?.toUpperCase()?.slice(0, 4) || "GEN";
181 +}
182 +
183 +function getStatusClass(type, toolName = null) {
184 + if (type === "tool" && toolName && TOOL_STATUS_CLASSES[toolName]) {
185 + return TOOL_STATUS_CLASSES[toolName];
186 + }
187 + return TYPE_STATUS_CLASSES[type] || "status-gen";
188 +}
189 +
190 /**
191 * Resolve tool name from kvps, existing attribute, or previous siblings
192 * For 'tool' type steps, inherits from preceding step if not directly available
@@ -119,123 +222,160 @@ function updateBadgeText(badge, newCode) {
222 badge.textContent = newCode;
223 }
224
122 -// Process types that should be grouped into collapsible sections
123 -const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'progress', 'hint', 'util', 'warning', 'rate_limit'];
124 -// Main types that should always be visible (not collapsed)
125 -const MAIN_TYPES = ['user', 'response', 'error', 'info'];
225
127 -/**
128 - * Helper to append a message container to the correct group in chat history
129 - */
130 -function appendMessageToHistory(messageContainer, groupType, forceNewGroup, id) {
131 - // Check if current messageGroup is still in DOM, if not, reset it (context switch)
132 - if (messageGroup && !document.getElementById(messageGroup.id)) {
133 - messageGroup = null;
226 +// entrypoint called from poll/WS communication, this is how all messages are rendered and updated
227 +export function setMessage(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
228 + const handler = getMessageHandler(type);
229 + return handler(id, type, heading, content, kvps, timestamp, durationMs, agentNumber);
230 +}
231 +
232 +function getOrCreateMessageContainer(id, position, containerClasses = [], forceNewGroup = false) {
233 + let container = document.getElementById(`message-${id}`);
234 + if (!container) {
235 + container = document.createElement("div");
236 + container.id = `message-${id}`;
237 + container.classList.add("message-container");
238 + }
239 +
240 + if (containerClasses.length) {
241 + container.classList.add(...containerClasses);
242 }
243
136 - // Create new group if needed
137 - if (!messageGroup || forceNewGroup || groupType !== messageGroup.getAttribute("data-group-type")) {
138 - messageGroup = document.createElement("div");
139 - messageGroup.id = `message-group-${id}`;
140 - messageGroup.classList.add("message-group", `message-group-${groupType}`);
141 - messageGroup.setAttribute("data-group-type", groupType);
142 - chatHistory.appendChild(messageGroup);
244 + if (!container.parentNode) {
245 + appendToMessageGroup(container, position, forceNewGroup);
246 }
247
145 - // Append message to group
146 - messageGroup.appendChild(messageContainer);
248 + return container;
249 }
250
149 -// entrypoint called from poll/WS communication, this is how all messages are rendered and updated
150 -export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
151 - // Check if this is a process type message
152 - const isProcessType = PROCESS_TYPES.includes(type);
153 -
154 - // Search for the existing message container by id
155 - let messageContainer = document.getElementById(`message-${id}`);
156 - let processStepElement = document.getElementById(`process-step-${id}`);
157 -
158 - // For user messages, close current process group FIRST (start fresh for next interaction)
159 - if (type === "user") {
160 - currentProcessGroup = null;
161 - currentDelegationSteps = {}; // Clear delegation tracking
162 - }
163 -
164 - // For process types, check if we should add to process group
165 - if (isProcessType || (type === "response" && agentNumber !== 0)) {
166 - if (processStepElement) {
167 - // Update existing process step
168 - updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs, agentNumber);
169 - return processStepElement;
170 - }
171 -
172 - // Create or get process group for current interaction
173 - if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) {
174 - // Create response container for this process group immediately (Option B)
175 - messageContainer = document.createElement("div");
176 - messageContainer.id = `message-${id}`;
177 - messageContainer.classList.add("message-container", "ai-container", "has-process-group");
178 -
179 - currentProcessGroup = createProcessGroup(id);
180 - currentProcessGroup.classList.add("embedded");
181 - messageContainer.appendChild(currentProcessGroup);
182 -
183 - // Handle DOM insertion immediately
184 - appendMessageToHistory(messageContainer, "left", false, id);
185 -
186 - setActiveProcessGroup(currentProcessGroup);
187 - }
188 -
189 - // Add step to current process group
190 - const stepType = (type === "response" && agentNumber !== 0) ? "response" : type;
191 - processStepElement = addProcessStep(currentProcessGroup, id, stepType, heading, content, kvps, timestamp, durationMs, agentNumber);
192 - return processStepElement;
251 +function getLastProcessGroup() {
252 + const lastContainer = getLastMessageContainer();
253 + if (!lastContainer) return null;
254 + if (!lastContainer.classList.contains("has-process-group")) return null;
255 + const group = lastContainer.querySelector(".process-group");
256 + if (!group || group.classList.contains("process-group-completed")) {
257 + return null;
258 }
259 + return group;
260 +}
261
195 - // For main agent (A0) response, mark the current process group as complete
196 - if (type === "response" && currentProcessGroup) {
197 - // Mark process group as complete (END state)
198 - markProcessGroupComplete(currentProcessGroup, heading);
262 +function getOrCreateProcessGroup(id) {
263 + const existing = getLastProcessGroup();
264 + if (existing) {
265 + setActiveProcessGroup(existing);
266 + return existing;
267 }
268
201 - if (!messageContainer) {
202 - // Create a new container if not found
203 - const sender = type === "user" ? "user" : "ai";
204 - messageContainer = document.createElement("div");
205 - messageContainer.id = `message-${id}`;
206 - messageContainer.classList.add("message-container", `${sender}-container`);
269 + const messageContainer = document.createElement("div");
270 + messageContainer.id = `message-${id}`;
271 + messageContainer.classList.add("message-container", "ai-container", "has-process-group");
272 +
273 + const group = createProcessGroup(id);
274 + group.classList.add("embedded");
275 + messageContainer.appendChild(group);
276 +
277 + appendToMessageGroup(messageContainer, "left");
278 + setActiveProcessGroup(group);
279 + return group;
280 +}
281 +
282 +function buildDetailPayload(stepData) {
283 + if (!stepData) return null;
284 + return {
285 + type: stepData.type,
286 + heading: stepData.heading,
287 + content: stepData.content,
288 + kvps: stepData.kvps,
289 + timestamp: stepData.timestamp,
290 + durationMs: stepData.durationMs,
291 + agentNumber: stepData.agentNumber,
292 + toolName: stepData.toolName,
293 + statusCode: stepData.statusCode,
294 + statusClass: stepData.statusClass
295 + };
296 +}
297 +
298 +function buildStepCopyContent(stepData) {
299 + if (!stepData) return "";
300 + const parts = [];
301 + if (stepData.heading) parts.push(stepData.heading);
302 + if (stepData.content) parts.push(stepData.content);
303 + if (stepData.kvps) {
304 + for (const [key, value] of Object.entries(stepData.kvps)) {
305 + if (key === "reasoning" || key === "finished" || key === "attachments") continue;
306 + const valStr = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
307 + parts.push(`${key}: ${valStr}`);
308 + }
309 }
310 + return parts.join("\n\n");
311 +}
312
209 - const handler = getMessageHandler(type);
210 - handler(messageContainer, id, type, heading, content, temp, kvps);
211 -
212 - // If this is a new message (not yet in DOM), handle DOM insertion
213 - if (!messageContainer.parentNode) {
214 - // message type visual grouping
215 - const groupTypeMap = {
216 - user: "right",
217 - info: "mid",
218 - warning: "mid",
219 - error: "mid",
220 - rate_limit: "mid",
221 - util: "mid",
222 - hint: "mid",
223 - // anything else is "left"
224 - };
225 - //force new group on these types
226 - const groupStart = {
227 - response: true, // response starts a new group
228 - user: true, // user message starts a new group (each user message should be separate)
229 - // anything else is false
230 - };
231 -
232 - const groupType = groupTypeMap[type] || "left";
233 - const forceNewGroup = groupStart[type] || false;
234 -
235 - appendMessageToHistory(messageContainer, groupType, forceNewGroup, id);
236 - }
237 -
238 - return messageContainer;
313 +function drawProcessStep(id, title, statusClass, statusCode, kvps = null, detailHandler = null, copyContent = null, speakContent = null, options = {}) {
314 + const {
315 + type = "agent",
316 + heading = null,
317 + content = null,
318 + timestamp = null,
319 + durationMs = null,
320 + agentNumber = 0,
321 + toolName = null,
322 + detailPayload = null
323 + } = options;
324 +
325 + const group = getOrCreateProcessGroup(id);
326 + const stepId = `process-step-${id}`;
327 + const stepData = {
328 + id,
329 + type,
330 + title,
331 + heading,
332 + content,
333 + kvps,
334 + timestamp,
335 + durationMs,
336 + agentNumber,
337 + toolName,
338 + statusCode,
339 + statusClass
340 + };
341 +
342 + let step = document.getElementById(stepId);
343 + const detailData = detailPayload || buildDetailPayload(stepData);
344 + const copyText = copyContent ?? buildStepCopyContent(stepData);
345 + const speakText = speakContent ?? copyText;
346 +
347 + if (step) {
348 + updateProcessStep(step, stepData, detailData, copyText, speakText, detailHandler);
349 + return step;
350 + }
351 +
352 + step = addProcessStep(group, stepData, detailData, copyText, speakText, detailHandler);
353 + return step;
354 +}
355 +
356 +function drawStandaloneMessage(id, heading, content, options = {}) {
357 + const {
358 + position = "mid",
359 + forceNewGroup = false,
360 + containerClasses = [],
361 + mainClass = "",
362 + messageClasses = [],
363 + contentClasses = [],
364 + markdown = false,
365 + latex = false,
366 + kvps = null,
367 + copyContent = null,
368 + speakContent = null
369 + } = options;
370 +
371 + const container = getOrCreateMessageContainer(id, position, containerClasses, forceNewGroup);
372 + const messageDiv = _drawMessage(container, heading, content, kvps, messageClasses, contentClasses, markdown, latex, mainClass);
373 +
374 + const copyText = copyContent ?? content ?? "";
375 + const speakText = speakContent ?? copyText;
376 + addActionButtonsToElement(messageDiv, { copyContent: copyText, speakContent: speakText });
377 +
378 + return container;
379 }
380
381
@@ -244,15 +384,12 @@ export function _drawMessage(
384 messageContainer,
385 heading,
386 content,
247 - temp,
248 - followUp,
249 - mainClass = "",
387 kvps = null,
388 messageClasses = [],
389 contentClasses = [],
253 - latex = false,
390 markdown = false,
255 - resizeBtns = true
391 + latex = false,
392 + mainClass = ""
393 ) {
394 // Find existing message div or create new one
395 let messageDiv = messageContainer.querySelector(".message");
@@ -337,8 +474,6 @@ export function _drawMessage(
474 });
475 }
476
340 - // Ensure action buttons exist - pass content directly
341 - addActionButtonsToElement(bodyDiv, { contentRef: content });
477 adjustMarkdownRender(contentDiv);
478
479 } else {
@@ -362,9 +497,6 @@ export function _drawMessage(
497
498 spanElement.innerHTML = convertHTML(content);
499
365 - // Ensure action buttons exist - pass content directly
366 - addActionButtonsToElement(bodyDiv, { contentRef: content });
367 -
500 }
501 } else {
502 // Remove content if it exists but content is empty
@@ -377,10 +509,6 @@ export function _drawMessage(
509 // reapply scroll position or autoscroll
510 scroller.reApplyScroll();
511
380 - if (followUp) {
381 - messageContainer.classList.add("message-followup");
382 - }
383 -
512 return messageDiv;
513 }
514
@@ -409,118 +537,71 @@ export function addBlankTargetsToLinks(str) {
537 return doc.body.innerHTML;
538 }
539
412 -export function drawMessageDefault(
413 - messageContainer,
414 - id,
415 - type,
416 - heading,
417 - content,
418 - temp,
419 - kvps = null
420 -) {
421 - _drawMessage(
422 - messageContainer,
540 +export function drawMessageDefault(id, type, heading, content, kvps = null) {
541 + return drawStandaloneMessage(id, heading, content, {
542 + position: "left",
543 + containerClasses: ["ai-container"],
544 + mainClass: "message-default",
545 + messageClasses: ["message-ai"],
546 + contentClasses: ["msg-json"],
547 + kvps
548 + });
549 +}
550 +
551 +export function drawMessageAgent(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
552 + const title = getStepTitle(heading, kvps, type);
553 + const statusCode = getStatusCode(type);
554 + const statusClass = getStatusClass(type);
555 + const toolName = kvps?.tool_name || null;
556 +
557 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
558 + type,
559 heading,
560 content,
425 - temp,
426 - false,
427 - "message-default",
561 kvps,
429 - ["message-ai"],
430 - ["msg-json"],
431 - false,
432 - false
433 - );
562 + timestamp,
563 + durationMs,
564 + agentNumber,
565 + toolName
566 + });
567 }
568
436 -export function drawMessageAgent(
437 - messageContainer,
438 - id,
439 - type,
440 - heading,
441 - content,
442 - temp,
443 - kvps = null
444 -) {
445 - let kvpsFlat = null;
446 - if (kvps) {
447 - kvpsFlat = { ...kvps, ...(kvps["tool_args"] || {}) };
448 - delete kvpsFlat["tool_args"];
569 +export function drawMessageResponse(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
570 + if (agentNumber && agentNumber > 0) {
571 + const title = getStepTitle(heading, kvps, type);
572 + const statusCode = getStatusCode(type);
573 + const statusClass = getStatusClass(type);
574 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
575 + type,
576 + heading,
577 + content,
578 + kvps,
579 + timestamp,
580 + durationMs,
581 + agentNumber
582 + });
583 }
584
451 - _drawMessage(
452 - messageContainer,
453 - heading,
454 - content,
455 - temp,
456 - false,
457 - "message-agent",
458 - kvpsFlat,
459 - ["message-ai"],
460 - ["msg-json"],
461 - false,
462 - false
463 - );
464 -}
585 + const group = getLastProcessGroup();
586 + if (group) {
587 + markProcessGroupComplete(group, heading);
588 + }
589
466 -export function drawMessageResponse(
467 - messageContainer,
468 - id,
469 - type,
470 - heading,
471 - content,
472 - temp,
473 - kvps = null
474 -) {
475 - _drawMessage(
476 - messageContainer,
477 - heading,
478 - content,
479 - temp,
480 - true,
481 - "message-agent-response",
482 - null,
483 - ["message-ai"],
484 - [],
485 - true,
486 - true
487 - );
590 + return drawStandaloneMessage(id, heading, content, {
591 + position: "left",
592 + forceNewGroup: true,
593 + containerClasses: ["ai-container"],
594 + mainClass: "message-agent-response",
595 + messageClasses: ["message-ai"],
596 + markdown: true,
597 + latex: true
598 + });
599 }
600
490 -export function drawMessageDelegation(
491 - messageContainer,
492 - id,
493 - type,
494 - heading,
495 - content,
496 - temp,
497 - kvps = null
498 -) {
499 - _drawMessage(
500 - messageContainer,
501 - heading,
502 - content,
503 - temp,
504 - true,
505 - "message-agent-delegation",
506 - kvps,
507 - ["message-ai", "message-agent"],
508 - [],
509 - true,
510 - false
511 - );
512 -}
601
514 -export function drawMessageUser(
515 - messageContainer,
516 - id,
517 - type,
518 - heading,
519 - content,
520 - temp,
521 - kvps = null,
522 - latex = false
523 -) {
602 +export function drawMessageUser(id, type, heading, content, kvps = null) {
603 + const messageContainer = getOrCreateMessageContainer(id, "right", ["user-container"], true);
604 +
605 // Find existing message div or create new one
606 let messageDiv = messageContainer.querySelector(".message");
607 if (!messageDiv) {
@@ -623,183 +704,163 @@ export function drawMessageUser(
704 }
705
706 // Add action buttons below text and attachments (hover for pointer, always for touch - via CSS)
626 - addActionButtonsToElement(messageDiv, { contentRef: content });
707 + addActionButtonsToElement(messageDiv, { copyContent: content || "", speakContent: content || "" });
708 }
709
629 -export function drawMessageTool(
630 - messageContainer,
631 - id,
632 - type,
633 - heading,
634 - content,
635 - temp,
636 - kvps = null
637 -) {
638 - _drawMessage(
639 - messageContainer,
710 +export function drawMessageTool(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
711 + const toolName = kvps?.tool_name || null;
712 + const title = getStepTitle(heading, kvps, type);
713 + const statusCode = getStatusCode(type, toolName);
714 + const statusClass = getStatusClass(type, toolName);
715 +
716 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
717 + type,
718 heading,
719 content,
642 - temp,
643 - true,
644 - "message-tool",
720 kvps,
646 - ["message-ai"],
647 - ["msg-output"],
648 - false,
649 - false
650 - );
721 + timestamp,
722 + durationMs,
723 + agentNumber,
724 + toolName
725 + });
726 }
727
653 -export function drawMessageCodeExe(
654 - messageContainer,
655 - id,
656 - type,
657 - heading,
658 - content,
659 - temp,
660 - kvps = null
661 -) {
662 - _drawMessage(
663 - messageContainer,
728 +export function drawMessageCodeExe(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
729 + const title = getStepTitle(heading, kvps, type);
730 + const statusCode = getStatusCode(type);
731 + const statusClass = getStatusClass(type);
732 +
733 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
734 + type,
735 heading,
736 content,
666 - temp,
667 - true,
668 - "message-code-exe",
669 - null,
670 - ["message-ai"],
671 - [],
672 - false,
673 - false
674 - );
737 + kvps,
738 + timestamp,
739 + durationMs,
740 + agentNumber
741 + });
742 }
743
677 -export function drawMessageBrowser(
678 - messageContainer,
679 - id,
680 - type,
681 - heading,
682 - content,
683 - temp,
684 - kvps = null
685 -) {
686 - _drawMessage(
687 - messageContainer,
744 +export function drawMessageBrowser(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
745 + const title = getStepTitle(heading, kvps, type);
746 + const statusCode = getStatusCode(type);
747 + const statusClass = getStatusClass(type);
748 +
749 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
750 + type,
751 heading,
752 content,
690 - temp,
691 - true,
692 - "message-browser",
753 kvps,
694 - ["message-ai"],
695 - ["msg-json"],
696 - false,
697 - false
698 - );
754 + timestamp,
755 + durationMs,
756 + agentNumber
757 + });
758 }
759
701 -export function drawMessageAgentPlain(
702 - mainClass,
703 - messageContainer,
704 - id,
705 - type,
706 - heading,
707 - content,
708 - temp,
709 - kvps = null
710 -) {
711 - _drawMessage(
712 - messageContainer,
760 +export function drawMessageMcp(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
761 + const toolName = kvps?.tool_name || null;
762 + const title = getStepTitle(heading, kvps, type);
763 + const statusCode = getStatusCode(type, toolName);
764 + const statusClass = getStatusClass(type, toolName);
765 +
766 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
767 + type,
768 heading,
769 content,
715 - temp,
716 - false,
717 - mainClass,
770 kvps,
719 - [],
720 - [],
721 - false,
722 - false
723 - );
724 - messageContainer.classList.add("center-container");
771 + timestamp,
772 + durationMs,
773 + agentNumber,
774 + toolName
775 + });
776 }
777
727 -export function drawMessageInfo(
728 - messageContainer,
729 - id,
730 - type,
731 - heading,
732 - content,
733 - temp,
734 - kvps = null
735 -) {
736 - return drawMessageAgentPlain(
737 - "message-info",
738 - messageContainer,
739 - id,
778 +export function drawMessageSubagent(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
779 + const title = getStepTitle(heading, kvps, type);
780 + const statusCode = getStatusCode(type);
781 + const statusClass = getStatusClass(type);
782 +
783 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
784 type,
785 heading,
786 content,
743 - temp,
787 + kvps,
788 + timestamp,
789 + durationMs,
790 + agentNumber
791 + });
792 +}
793 +
794 +
795 +export function drawMessageInfo(id, type, heading, content, kvps = null) {
796 + return drawStandaloneMessage(id, heading, content, {
797 + position: "mid",
798 + containerClasses: ["ai-container", "center-container"],
799 + mainClass: "message-info",
800 kvps
745 - );
801 + });
802 }
803
748 -export function drawMessageUtil(
749 - messageContainer,
750 - id,
751 - type,
752 - heading,
753 - content,
754 - temp,
755 - kvps = null
756 -) {
757 - _drawMessage(
758 - messageContainer,
804 +export function drawMessageUtil(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
805 + const title = getStepTitle(heading, kvps, type);
806 + const statusCode = getStatusCode(type);
807 + const statusClass = getStatusClass(type);
808 +
809 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
810 + type,
811 heading,
812 content,
761 - temp,
762 - false,
763 - "message-util",
813 kvps,
765 - [],
766 - ["msg-json"],
767 - false,
768 - false
769 - );
770 - messageContainer.classList.add("center-container");
814 + timestamp,
815 + durationMs,
816 + agentNumber
817 + });
818 }
819
773 -export function drawMessageWarning(
774 - messageContainer,
775 - id,
776 - type,
777 - heading,
778 - content,
779 - temp,
780 - kvps = null
781 -) {
782 - return drawMessageAgentPlain(
783 - "message-warning",
784 - messageContainer,
785 - id,
820 +export function drawMessageHint(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
821 + const title = getStepTitle(heading, kvps, type);
822 + const statusCode = getStatusCode(type);
823 + const statusClass = getStatusClass(type);
824 +
825 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
826 type,
827 heading,
828 content,
789 - temp,
829 + kvps,
830 + timestamp,
831 + durationMs,
832 + agentNumber
833 + });
834 +}
835 +
836 +export function drawMessageProgress(id, type, heading, content, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
837 + const title = getStepTitle(heading, kvps, type);
838 + const statusCode = getStatusCode(type);
839 + const statusClass = getStatusClass(type);
840 +
841 + return drawProcessStep(id, title, statusClass, statusCode, kvps, null, null, null, {
842 + type,
843 + heading,
844 + content,
845 + kvps,
846 + timestamp,
847 + durationMs,
848 + agentNumber
849 + });
850 +}
851 +
852 +export function drawMessageWarning(id, type, heading, content, kvps = null) {
853 + return drawStandaloneMessage(id, heading, content, {
854 + position: "mid",
855 + containerClasses: ["ai-container", "center-container"],
856 + mainClass: "message-warning",
857 kvps
791 - );
858 + });
859 }
860
794 -export function drawMessageError(
795 - messageContainer,
796 - id,
797 - type,
798 - heading,
799 - content,
800 - temp,
801 - kvps = null
802 -) {
861 +export function drawMessageError(id, type, heading, content, kvps = null) {
862 + const messageContainer = getOrCreateMessageContainer(id, "mid", ["ai-container", "center-container"]);
863 +
864 // Create or get the message div
865 let messageDiv = messageContainer.querySelector(".message");
866 if (!messageDiv) {
@@ -916,7 +977,7 @@ export function drawMessageError(
977 contentInner.appendChild(pre);
978
979 // Add action buttons for copy functionality
919 - addActionButtonsToElement(contentInner);
980 + addActionButtonsToElement(contentInner, { copyContent: content, speakContent: content });
981 }
982
983 messageContainer.classList.add("center-container");
@@ -959,7 +1020,12 @@ function drawKvpsIncremental(container, kvps, latex) {
1020 th = row.insertCell(0);
1021 th.classList.add("kvps-key");
1022 }
962 - th.textContent = convertToTitleCase(key);
1023 + const iconName = extractIconFromKey(key);
1024 + if (iconName) {
1025 + th.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
1026 + } else {
1027 + th.textContent = convertToTitleCase(key);
1028 + }
1029
1030 // Handle value cell
1031 let td = row.cells[1];
@@ -1224,7 +1290,7 @@ function createProcessGroup(id) {
1290 group.setAttribute("data-group-id", groupId);
1291
1292 // Determine initial expansion state from current detail mode
1227 - const initiallyExpanded = processGroupStore.shouldExpandGroup();
1293 + const initiallyExpanded = preferencesStore.detailMode !== "collapsed";
1294 if (initiallyExpanded) {
1295 group.classList.add('expanded');
1296 }
@@ -1245,9 +1311,8 @@ function createProcessGroup(id) {
1311 `;
1312
1313 // Add click handler for expansion
1248 - header.addEventListener("click", (e) => {
1249 - // Toggle group (store directly modifies DOM - single source of truth)
1250 - processGroupStore.toggleGroup(groupId);
1314 + header.addEventListener("click", () => {
1315 + group.classList.toggle("expanded");
1316 });
1317
1318 group.appendChild(header);
@@ -1362,14 +1427,44 @@ function addStepCollapseInteractionHandlers(stepElement) {
1427 });
1428 }
1429
1430 +/**
1431 + * Find parent delegation step for nested agents (DOM-first, reverse scan).
1432 + */
1433 +function findParentDelegationStep(group, agentNumber) {
1434 + if (!group || agentNumber <= 0) return null;
1435 + const steps = group.querySelectorAll(".process-step");
1436 + for (let i = steps.length - 1; i >= 0; i -= 1) {
1437 + const step = steps[i];
1438 + const stepAgent = Number(step.getAttribute("data-agent-number"));
1439 + if (stepAgent === agentNumber - 1 && step.getAttribute("data-tool-name") === "call_subordinate") {
1440 + return step;
1441 + }
1442 + }
1443 + return null;
1444 +}
1445 +
1446 /**
1447 * Add a step to a process group
1448 */
1368 -function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null, agentNumber = 0) {
1369 - const groupId = group.getAttribute("data-group-id");
1370 - let stepsContainer = group.querySelector(".process-steps");
1449 +function addProcessStep(group, stepData, detailPayload, copyContent, speakContent, detailHandler) {
1450 + const {
1451 + id,
1452 + type,
1453 + title,
1454 + heading,
1455 + content,
1456 + kvps,
1457 + timestamp,
1458 + durationMs,
1459 + agentNumber,
1460 + toolName,
1461 + statusCode,
1462 + statusClass
1463 + } = stepData;
1464 +
1465 + const stepsContainer = group.querySelector(".process-steps");
1466 const isGroupCompleted = group.classList.contains("process-group-completed");
1372 -
1467 +
1468 // Create step element
1469 const step = document.createElement("div");
1470 step.id = `process-step-${id}`;
@@ -1377,24 +1472,15 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1472 step.setAttribute("data-type", type);
1473 step.setAttribute("data-step-id", id);
1474 step.setAttribute("data-agent-number", agentNumber);
1380 -
1381 - // Resolve tool name (direct, inherited, or null)
1382 - // For new steps, pass null as stepElement - inheritance uses stepsContainer query
1383 - let toolNameToUse = kvps?.tool_name;
1384 - if (type === 'tool' && !toolNameToUse) {
1385 - const existingSteps = stepsContainer.querySelectorAll('.process-step[data-tool-name]');
1386 - if (existingSteps.length > 0) {
1387 - toolNameToUse = existingSteps[existingSteps.length - 1].getAttribute("data-tool-name");
1388 - }
1389 - }
1390 - if (toolNameToUse) {
1391 - step.setAttribute("data-tool-name", toolNameToUse);
1475 +
1476 + if (toolName) {
1477 + step.setAttribute("data-tool-name", toolName);
1478 }
1393 -
1479 +
1480 // Store timestamp for duration calculation
1481 if (timestamp) {
1482 step.setAttribute("data-timestamp", timestamp);
1397 -
1483 +
1484 // Set group start time from first log item
1485 if (!group.getAttribute("data-start-timestamp")) {
1486 group.setAttribute("data-start-timestamp", timestamp);
@@ -1408,12 +1494,12 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1494 }
1495 }
1496 }
1411 -
1497 +
1498 // Store duration from backend (used for final duration calculation)
1499 if (durationMs != null) {
1500 step.setAttribute("data-duration-ms", durationMs);
1501 }
1416 -
1502 +
1503 // Add message-util class for utility/info types (controlled by showUtils preference)
1504 if (type === "util" || type === "info" || type === "hint") {
1505 step.classList.add("message-util");
@@ -1422,15 +1508,12 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1508 step.classList.add("show-util");
1509 }
1510 }
1425 -
1426 - // Get step info from heading (single source of truth: backend)
1427 - const title = getStepTitle(heading, kvps, type);
1428 -
1511 +
1512 // Determine if this new step should be expanded
1513 const detailMode = preferencesStore.detailMode;
1514 const isActiveGroup = group.classList.contains("active");
1515 let shouldExpand = false;
1433 -
1516 +
1517 if (detailMode === "expanded") {
1518 shouldExpand = true;
1519 } else if (detailMode === "current") {
@@ -1438,7 +1521,7 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1521 // For non-active groups (historical data), render steps collapsed immediately
1522 if (isActiveGroup && !isGroupCompleted) {
1523 shouldExpand = true;
1441 -
1524 +
1525 // Schedule collapse for ALL previously expanded steps
1526 const allExpandedSteps = stepsContainer.querySelectorAll(".process-step.step-expanded");
1527 allExpandedSteps.forEach(expandedStep => {
@@ -1451,38 +1534,40 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1534 // Non-active groups: shouldExpand stays false → steps render collapsed
1535 }
1536 // In "collapsed" mode: shouldExpand stays false
1454 -
1537 +
1538 if (shouldExpand) {
1539 step.classList.add("step-expanded");
1540 }
1458 -
1541 +
1542 // Create step header
1543 const stepHeader = document.createElement("div");
1544 stepHeader.classList.add("process-step-header");
1462 -
1463 - // Status code and color class from store (maps backend types)
1464 - const statusCode = processGroupStore.getStepCode(type, toolNameToUse);
1465 - const statusColorClass = processGroupStore.getStatusColorClass(type, toolNameToUse);
1466 -
1545 +
1546 + const resolvedTitle = title || getStepTitle(heading, kvps, type);
1547 + const resolvedStatusCode = statusCode || getStatusCode(type, toolName);
1548 + const resolvedStatusClass = statusClass || getStatusClass(type, toolName);
1549 +
1550 // Add status color class to step for cascading --step-accent to internal icons
1468 - step.classList.add(statusColorClass);
1469 -
1551 + step.classList.add(resolvedStatusClass);
1552 + step.setAttribute("data-status-code", resolvedStatusCode);
1553 + step.setAttribute("data-status-class", resolvedStatusClass);
1554 +
1555 stepHeader.innerHTML = `
1556 <span class="step-expand-icon"></span>
1472 - <span class="status-badge ${statusColorClass}">${statusCode}</span>
1473 - <span class="step-title">${escapeHTML(title)}</span>
1557 + <span class="status-badge ${resolvedStatusClass}">${resolvedStatusCode}</span>
1558 + <span class="step-title">${escapeHTML(resolvedTitle)}</span>
1559 `;
1475 -
1560 +
1561 // Add click handler for step expansion
1562 stepHeader.addEventListener("click", (e) => {
1563 e.stopPropagation();
1479 -
1564 +
1565 // Cancel any scheduled auto-collapse (user is manually toggling)
1566 cancelStepCollapse(step);
1482 -
1483 - // Toggle step (store directly modifies DOM - single source of truth)
1484 - processGroupStore.toggleStep(groupId, id);
1485 -
1567 +
1568 + // Toggle step
1569 + step.classList.toggle("step-expanded");
1570 +
1571 // If manually expanded, set pinned flag to prevent auto-collapse
1572 // If collapsed, remove it
1573 if (step.classList.contains("step-expanded")) {
@@ -1491,70 +1576,60 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1576 step.removeAttribute("data-user-pinned");
1577 }
1578 });
1494 -
1579 +
1580 step.appendChild(stepHeader);
1496 -
1581 +
1582 // Create step detail container
1583 const detail = document.createElement("div");
1584 detail.classList.add("process-step-detail");
1500 -
1585 +
1586 const detailContent = document.createElement("div");
1587 detailContent.classList.add("process-step-detail-content");
1503 -
1588 +
1589 // Add content to detail
1590 renderStepDetailContent(detailContent, content, kvps, type);
1506 -
1591 +
1592 detail.appendChild(detailContent);
1508 -
1509 - // Store step data on the element for fresh access on modal open
1510 - step._stepData = {
1511 - type,
1512 - heading,
1513 - content,
1514 - kvps,
1515 - timestamp,
1516 - durationMs,
1517 - agentNumber,
1518 - toolName: toolNameToUse
1519 - };
1520 -
1593 +
1594 // Add step action buttons (view details, copy, speak)
1522 - const stepActionBtns = createStepActionButtons(step);
1595 + const stepActionBtns = document.createElement("div");
1596 + stepActionBtns.classList.add("step-detail-actions");
1597 + addActionButtonsToElement(stepActionBtns, {
1598 + detailPayload,
1599 + onViewDetails: detailHandler,
1600 + copyContent,
1601 + speakContent
1602 + });
1603 detail.appendChild(stepActionBtns);
1524 -
1604 +
1605 step.appendChild(detail);
1526 -
1527 - // Track delegation steps for nesting
1528 - if (toolNameToUse === "call_subordinate") {
1529 - currentDelegationSteps[agentNumber] = step;
1530 - }
1531 -
1606 +
1607 // Determine where to append the step (main list or nested in parent)
1608 let appendTarget = stepsContainer;
1534 -
1609 +
1610 // Check if this step belongs to a subordinate agent
1536 - if (agentNumber > 0 && currentDelegationSteps[agentNumber - 1]) {
1537 - const parentStep = currentDelegationSteps[agentNumber - 1];
1611 + const parentStep = findParentDelegationStep(group, agentNumber);
1612 + if (parentStep) {
1613 appendTarget = getNestedContainer(parentStep);
1614 step.classList.add("nested-step");
1615 }
1541 -
1616 +
1617 // Clear shiny effect from all previous steps in this group
1618 group.querySelectorAll(".process-step .step-title.shiny-text").forEach(el => {
1619 el.classList.remove("shiny-text");
1620 });
1546 -
1621 +
1622 appendTarget.appendChild(step);
1548 -
1623 +
1624 // Add interaction handlers to prevent fighting with user during auto-collapse
1625 addStepCollapseInteractionHandlers(step);
1551 -
1626 +
1627 // Scroll terminal to bottom on initial render (including page refresh)
1628 const initialTerminal = step.querySelector(".terminal-output");
1629 if (initialTerminal) {
1630 initialTerminal.scrollTop = initialTerminal.scrollHeight;
1631 }
1557 -
1632 +
1633 // Update group header
1634 updateProcessGroupHeader(group);
1635
@@ -1565,48 +1640,76 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1640 titleEl.classList.add("shiny-text");
1641 }
1642 }
1568 -
1643 +
1644 return step;
1645 }
1646
1647 /**
1648 * Update an existing process step
1649 */
1575 -function updateProcessStep(stepElement, id, type, heading, content, kvps, durationMs = null, agentNumber = 0) {
1576 - // Update title
1650 +function updateProcessStep(stepElement, stepData, detailPayload, copyContent, speakContent, detailHandler) {
1651 + const {
1652 + type,
1653 + title,
1654 + heading,
1655 + content,
1656 + kvps,
1657 + timestamp,
1658 + durationMs,
1659 + agentNumber,
1660 + toolName,
1661 + statusCode,
1662 + statusClass
1663 + } = stepData;
1664 +
1665 const titleEl = stepElement.querySelector(".step-title");
1666 if (titleEl) {
1579 - const title = getStepTitle(heading, kvps, type);
1580 - titleEl.textContent = title;
1667 + const resolvedTitle = title || getStepTitle(heading, kvps, type);
1668 + titleEl.textContent = resolvedTitle;
1669 }
1582 -
1583 - // Update duration from backend
1670 +
1671 + if (timestamp && !stepElement.hasAttribute("data-timestamp")) {
1672 + stepElement.setAttribute("data-timestamp", timestamp);
1673 + }
1674 +
1675 if (durationMs != null) {
1676 stepElement.setAttribute("data-duration-ms", durationMs);
1677 }
1587 -
1588 - // Update agent number if provided
1678 +
1679 if (agentNumber !== undefined) {
1680 stepElement.setAttribute("data-agent-number", agentNumber);
1681 }
1592 -
1593 - // Resolve and update tool name + badge
1594 - const toolNameToUse = resolveToolName(type, kvps, stepElement);
1682 +
1683 + const toolNameToUse = resolveToolName(type, kvps, stepElement) || toolName;
1684 if (toolNameToUse) {
1685 stepElement.setAttribute("data-tool-name", toolNameToUse);
1597 - const newCode = processGroupStore.getStepCode(type, toolNameToUse);
1598 - updateBadgeText(stepElement.querySelector(".status-badge"), newCode);
1686 }
1600 -
1687 +
1688 + const resolvedStatusCode = statusCode || getStatusCode(type, toolNameToUse);
1689 + const resolvedStatusClass = statusClass || getStatusClass(type, toolNameToUse);
1690 + const badge = stepElement.querySelector(".status-badge");
1691 + if (badge) {
1692 + updateBadgeText(badge, resolvedStatusCode);
1693 + badge.className = `status-badge ${resolvedStatusClass}`;
1694 + }
1695 +
1696 + const previousStatusClass = stepElement.getAttribute("data-status-class");
1697 + if (previousStatusClass) {
1698 + stepElement.classList.remove(previousStatusClass);
1699 + }
1700 + stepElement.classList.add(resolvedStatusClass);
1701 + stepElement.setAttribute("data-status-code", resolvedStatusCode);
1702 + stepElement.setAttribute("data-status-class", resolvedStatusClass);
1703 +
1704 // Update detail content
1705 const detailContent = stepElement.querySelector(".process-step-detail-content");
1706 let skipFullRender = false;
1604 -
1707 +
1708 if (detailContent) {
1709 // Capture scroll state before re-render (uses existing Scroller pattern)
1710 const terminal = detailContent.querySelector(".terminal-output");
1711 const scroller = terminal ? new Scroller(terminal) : null;
1609 -
1712 +
1713 // For browser, update image src incrementally to avoid flashing
1714 if (type === "browser" && kvps?.screenshot) {
1715 const existingImg = detailContent.querySelector(".screenshot-img");
@@ -1620,10 +1723,10 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1723 skipFullRender = true;
1724 }
1725 }
1623 -
1726 +
1727 if (!skipFullRender) {
1728 renderStepDetailContent(detailContent, content, kvps, type);
1626 -
1729 +
1730 // Re-apply scroll (stays at bottom if was at bottom)
1731 const newTerminal = detailContent.querySelector(".terminal-output");
1732 if (newTerminal && scroller?.wasAtBottom) {
@@ -1631,20 +1734,26 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1734 }
1735 }
1736 }
1634 -
1635 - // Update stored step data for fresh access by modal
1636 - const timestamp = stepElement._stepData?.timestamp; // preserve original timestamp
1637 - stepElement._stepData = {
1638 - type,
1639 - heading,
1640 - content,
1641 - kvps,
1642 - timestamp,
1643 - durationMs,
1644 - agentNumber,
1645 - toolName: toolNameToUse
1646 - };
1647 -
1737 +
1738 + const detailData = detailPayload || buildDetailPayload({
1739 + ...stepData,
1740 + toolName: toolNameToUse,
1741 + statusCode: resolvedStatusCode,
1742 + statusClass: resolvedStatusClass
1743 + });
1744 +
1745 + const stepActions = stepElement.querySelector(".step-detail-actions") || document.createElement("div");
1746 + if (!stepActions.classList.contains("step-detail-actions")) {
1747 + stepActions.classList.add("step-detail-actions");
1748 + stepElement.querySelector(".process-step-detail")?.appendChild(stepActions);
1749 + }
1750 + addActionButtonsToElement(stepActions, {
1751 + detailPayload: detailData,
1752 + onViewDetails: detailHandler,
1753 + copyContent,
1754 + speakContent
1755 + });
1756 +
1757 // Update parent group header
1758 const group = stepElement.closest(".process-group");
1759 if (group) {
@@ -1697,12 +1806,11 @@ function getStepTitle(heading, kvps, type) {
1806 }
1807
1808 /**
1700 - * Extract icon name from heading with icon:// prefix
1701 - * Returns the icon name (e.g., "terminal") or null if no prefix found
1809 + * Extract icon name from a key with icon:// prefix
1810 */
1703 -function extractIconFromHeading(heading) {
1704 - if (!heading) return null;
1705 - const match = String(heading).match(/^icon:\/\/([a-zA-Z0-9_]+)/);
1811 +function extractIconFromKey(key) {
1812 + if (!key) return null;
1813 + const match = String(key).match(/^icon:\/\/([a-zA-Z0-9_]+)/);
1814 return match ? match[1] : null;
1815 }
1816
@@ -1830,25 +1938,6 @@ function renderStepDetailContent(container, content, kvps, type = null) {
1938 const argsDiv = document.createElement("div");
1939 argsDiv.classList.add("step-tool-args");
1940
1833 - // Icon mapping for common tool arguments
1834 - const argIcons = {
1835 - 'query': 'search',
1836 - 'url': 'link',
1837 - 'path': 'folder',
1838 - 'file': 'description',
1839 - 'code': 'code',
1840 - 'command': 'terminal',
1841 - 'message': 'chat',
1842 - 'text': 'notes',
1843 - 'content': 'article',
1844 - 'name': 'label',
1845 - 'id': 'tag',
1846 - 'type': 'category',
1847 - 'document': 'description',
1848 - 'documents': 'folder_open',
1849 - 'queries': 'search'
1850 - };
1851 -
1941 for (const [argKey, argValue] of Object.entries(value)) {
1942 const argRow = document.createElement("div");
1943 argRow.classList.add("tool-arg-row");
@@ -1856,10 +1945,9 @@ function renderStepDetailContent(container, content, kvps, type = null) {
1945 const argLabel = document.createElement("span");
1946 argLabel.classList.add("tool-arg-label");
1947
1859 - // Use icon if available, otherwise use text label
1860 - const lowerArgKey = argKey.toLowerCase();
1861 - if (argIcons[lowerArgKey]) {
1862 - argLabel.innerHTML = `<span class="material-symbols-outlined">${argIcons[lowerArgKey]}</span>`;
1948 + const iconName = extractIconFromKey(argKey);
1949 + if (iconName) {
1950 + argLabel.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
1951 } else {
1952 argLabel.textContent = convertToTitleCase(argKey) + ":";
1953 }
@@ -1886,32 +1974,9 @@ function renderStepDetailContent(container, content, kvps, type = null) {
1974 const keySpan = document.createElement("span");
1975 keySpan.classList.add("step-kvp-key");
1976
1889 - // Icon mapping for common kvp keys
1890 - const kvpIcons = {
1891 - 'query': 'search',
1892 - 'url': 'link',
1893 - 'path': 'folder',
1894 - 'file': 'description',
1895 - 'code': 'code',
1896 - 'command': 'terminal',
1897 - 'message': 'chat',
1898 - 'text': 'notes',
1899 - 'content': 'article',
1900 - 'name': 'label',
1901 - 'id': 'tag',
1902 - 'type': 'category',
1903 - 'runtime': 'memory',
1904 - 'result': 'output',
1905 - 'progress': 'pending',
1906 - 'document': 'description',
1907 - 'documents': 'folder_open',
1908 - 'queries': 'search',
1909 - 'screenshot': 'image'
1910 - };
1911 -
1912 - // lowerKey already defined above
1913 - if (kvpIcons[lowerKey]) {
1914 - keySpan.innerHTML = `<span class="material-symbols-outlined">${kvpIcons[lowerKey]}</span>`;
1977 + const iconName = extractIconFromKey(key);
1978 + if (iconName) {
1979 + keySpan.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
1980 } else {
1981 keySpan.textContent = convertToTitleCase(key) + ":";
1982 }
@@ -1972,40 +2037,6 @@ function renderThoughts(container, value) {
2037 }
2038 }
2039
1975 -/**
1976 - * Create step action buttons (view details, copy, speak) using unified action buttons
1977 - * @param {HTMLElement} stepElement - The step DOM element containing _stepData property
1978 - */
1979 -function createStepActionButtons(stepElement) {
1980 - const btnContainer = document.createElement("div");
1981 - btnContainer.classList.add("step-detail-actions");
1982 -
1983 - // Use unified action buttons with step-specific options
1984 - addActionButtonsToElement(btnContainer, {
1985 - contentRef: () => {
1986 - // Get text content from step data at action time
1987 - const data = stepElement._stepData || {};
1988 - const parts = [];
1989 - if (data.heading) parts.push(data.heading);
1990 - if (data.content) parts.push(data.content);
1991 - if (data.kvps) {
1992 - for (const [key, value] of Object.entries(data.kvps)) {
1993 - if (key === "reasoning" || key === "finished" || key === "attachments") continue;
1994 - const valStr = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
1995 - parts.push(`${key}: ${valStr}`);
1996 - }
1997 - }
1998 - return parts.join("\n\n");
1999 - },
2000 - onViewDetails: () => {
2001 - // Read fresh data from the step element at click time
2002 - const freshData = stepElement._stepData || {};
2003 - stepDetailStore.showStepDetail(freshData);
2004 - }
2005 - });
2006 -
2007 - return btnContainer;
2008 -}
2040
2041 /**
2042 * Update process group header with step count, status, and metrics
@@ -2120,10 +2151,9 @@ function updateProcessGroupHeader(group) {
2151
2152 // Update status badge
2153 if (statusEl) {
2123 - // Status code and color class from store (maps backend types)
2124 - const statusCode = processGroupStore.getStepCode(lastType, lastToolName);
2125 - const statusColorClass = processGroupStore.getStatusColorClass(lastType, lastToolName);
2126 -
2154 + const statusCode = getStatusCode(lastType, lastToolName);
2155 + const statusColorClass = getStatusClass(lastType, lastToolName);
2156 +
2157 statusEl.textContent = statusCode;
2158 statusEl.className = `status-badge ${statusColorClass} group-status`;
2159 }
@@ -2212,33 +2242,4 @@ function markProcessGroupComplete(group, responseTitle) {
2242 }
2243 }
2244
2215 -/**
2216 - * Reset process group state (called on context switch)
2217 - */
2218 -export function resetProcessGroups() {
2219 - currentProcessGroup = null;
2220 - currentDelegationSteps = {};
2221 - messageGroup = null;
2222 -
2223 - // Clear shiny effect from DOM (source of truth)
2224 - clearActiveStepShine();
2225 -
2226 - // Clear all pending collapse timeouts
2227 - document
2228 - .querySelectorAll('.process-step[data-collapse-timeout-id]')
2229 - .forEach((el) => cancelStepCollapse(el));
2230 -}
2245
2232 -/**
2233 - * Format Unix timestamp as date-time string (YYYY-MM-DD HH:MM:SS)
2234 - */
2235 -function formatDateTime(timestamp) {
2236 - const date = new Date(timestamp * 1000); // Convert seconds to milliseconds
2237 - const year = date.getFullYear();
2238 - const month = String(date.getMonth() + 1).padStart(2, "0");
2239 - const day = String(date.getDate()).padStart(2, "0");
2240 - const hours = String(date.getHours()).padStart(2, "0");
2241 - const minutes = String(date.getMinutes()).padStart(2, "0");
2242 - const seconds = String(date.getSeconds()).padStart(2, "0");
2243 - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
2244 -}