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
-}