Integrate Embedded Process Groups with Expand/Collapse Support

Wabifocus committed Dec 7, 2025 at 00:32 UTC c0faaafd57cdaae2b2c00283efc0ca33a26f3f3b
6 files changed +1023 -6
webui/components/messages/process-group/process-group-store.js new
+131
@@ -0,0 +1,131 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +// Process Group Store - manages collapsible process groups in chat
4 +const model = {
5 + // Track which process groups are expanded (by group ID)
6 + expandedGroups: {},
7 +
8 + // Track which individual steps are expanded within a group
9 + expandedSteps: {},
10 +
11 + // Default collapsed state for new process groups
12 + defaultCollapsed: true,
13 +
14 + init() {
15 + try {
16 + // Load persisted state
17 + const stored = localStorage.getItem("processGroupState");
18 + if (stored) {
19 + const parsed = JSON.parse(stored);
20 + this.expandedGroups = parsed.expandedGroups || {};
21 + this.expandedSteps = parsed.expandedSteps || {};
22 + this.defaultCollapsed = parsed.defaultCollapsed ?? true;
23 + }
24 + } catch (e) {
25 + console.error("Failed to load process group state", e);
26 + }
27 + },
28 +
29 + _persist() {
30 + try {
31 + localStorage.setItem("processGroupState", JSON.stringify({
32 + expandedGroups: this.expandedGroups,
33 + expandedSteps: this.expandedSteps,
34 + defaultCollapsed: this.defaultCollapsed
35 + }));
36 + } catch (e) {
37 + console.error("Failed to persist process group state", e);
38 + }
39 + },
40 +
41 + // Check if a process group is expanded
42 + isGroupExpanded(groupId) {
43 + if (groupId in this.expandedGroups) {
44 + return this.expandedGroups[groupId];
45 + }
46 + return !this.defaultCollapsed;
47 + },
48 +
49 + // Toggle process group expansion
50 + toggleGroup(groupId) {
51 + const current = this.isGroupExpanded(groupId);
52 + this.expandedGroups[groupId] = !current;
53 + this._persist();
54 + },
55 +
56 + // Expand a specific group
57 + expandGroup(groupId) {
58 + this.expandedGroups[groupId] = true;
59 + this._persist();
60 + },
61 +
62 + // Collapse a specific group
63 + collapseGroup(groupId) {
64 + this.expandedGroups[groupId] = false;
65 + this._persist();
66 + },
67 +
68 + // Check if a step within a group is expanded
69 + isStepExpanded(groupId, stepId) {
70 + const key = `${groupId}:${stepId}`;
71 + return this.expandedSteps[key] || false;
72 + },
73 +
74 + // Toggle step expansion
75 + toggleStep(groupId, stepId) {
76 + const key = `${groupId}:${stepId}`;
77 + this.expandedSteps[key] = !this.expandedSteps[key];
78 + this._persist();
79 + },
80 +
81 + // Get icon for step type
82 + getStepIcon(type) {
83 + const icons = {
84 + 'agent': 'psychology',
85 + 'tool': 'build',
86 + 'code_exe': 'terminal',
87 + 'browser': 'language',
88 + 'info': 'info',
89 + 'hint': 'lightbulb',
90 + 'util': 'settings',
91 + 'warning': 'warning',
92 + 'error': 'error'
93 + };
94 + return icons[type] || 'circle';
95 + },
96 +
97 + // Get label for step type
98 + getStepLabel(type) {
99 + const labels = {
100 + 'agent': 'Thinking',
101 + 'tool': 'Tool',
102 + 'code_exe': 'Code',
103 + 'browser': 'Browser',
104 + 'info': 'Info',
105 + 'hint': 'Hint',
106 + 'util': 'Utility',
107 + 'warning': 'Warning',
108 + 'error': 'Error'
109 + };
110 + return labels[type] || 'Process';
111 + },
112 +
113 + // Clear state for a specific context (when chat is reset)
114 + clearContext(contextPrefix) {
115 + // Clear groups matching the context
116 + for (const key of Object.keys(this.expandedGroups)) {
117 + if (key.startsWith(contextPrefix)) {
118 + delete this.expandedGroups[key];
119 + }
120 + }
121 + // Clear steps matching the context
122 + for (const key of Object.keys(this.expandedSteps)) {
123 + if (key.startsWith(contextPrefix)) {
124 + delete this.expandedSteps[key];
125 + }
126 + }
127 + this._persist();
128 + }
129 +};
130 +
131 +export const store = createStore("processGroup", model);
webui/components/messages/process-group/process-group.css new
+415
@@ -0,0 +1,415 @@
1 +.process-group {
2 + display: inline-flex;
3 + flex-direction: column;
4 + position: relative;
5 + z-index: 1;
6 + margin: var(--spacing-sm) 0;
7 + padding: var(--spacing-sm) var(--spacing-md);
8 + border-radius: var(--border-radius);
9 + background: rgba(31, 60, 30, 0.3);
10 + border: 1px solid rgba(255, 255, 255, 0.06);
11 + min-width: 200px;
12 + max-width: 100%;
13 + box-sizing: border-box;
14 + flex-shrink: 0;
15 + width: fit-content;
16 +}
17 +
18 +/* Embedded Process Group inside Response */
19 +.process-group.embedded {
20 + display: flex;
21 + flex-direction: column;
22 + width: 100%;
23 + margin: 0;
24 + border-radius: var(--border-radius) var(--border-radius) 0 0;
25 + border-bottom: 1px solid rgba(255, 255, 255, 0.08);
26 + background: rgba(31, 60, 30, 0.4);
27 +}
28 +
29 +/* Message container with embedded process group */
30 +.message-container.has-process-group {
31 + display: inline-flex;
32 + flex-direction: column;
33 + background: #1f3c1e;
34 + border-radius: var(--border-radius);
35 + border: 1px solid rgba(255, 255, 255, 0.06);
36 + padding: 0;
37 + overflow: hidden;
38 + min-width: 200px;
39 + max-width: 100%;
40 +}
41 +
42 +.message-container.has-process-group > .message {
43 + border-radius: 0 0 var(--border-radius) var(--border-radius);
44 + border: none;
45 + background: transparent;
46 + margin: 0;
47 +}
48 +
49 +.process-group:hover {
50 + border-color: rgba(255, 255, 255, 0.10);
51 +}
52 +
53 +/* Process Group Header */
54 +.process-group-header {
55 + display: flex;
56 + align-items: center;
57 + padding: 0;
58 + cursor: pointer;
59 + user-select: none;
60 + transition: opacity 0.15s ease;
61 + min-height: 24px;
62 + gap: var(--spacing-xs);
63 + white-space: nowrap;
64 +}
65 +
66 +.process-group-header:hover {
67 + opacity: 0.85;
68 +}
69 +
70 +.process-group-header .expand-icon {
71 + font-size: 1rem;
72 + color: var(--color-text);
73 + transition: transform 0.2s ease;
74 + opacity: 0.5;
75 + flex-shrink: 0;
76 +}
77 +
78 +.process-group.expanded .process-group-header .expand-icon {
79 + transform: rotate(90deg);
80 +}
81 +
82 +.process-group-header .group-icon {
83 + font-size: 0.95rem;
84 + color: var(--color-primary);
85 + opacity: 0.7;
86 + flex-shrink: 0;
87 +}
88 +
89 +.process-group-header .group-title {
90 + font-size: var(--font-size-smaller);
91 + font-weight: 400;
92 + color: var(--color-text);
93 + opacity: 0.7;
94 + white-space: nowrap;
95 +}
96 +
97 +.process-group-header .step-count {
98 + font-size: 0.7rem;
99 + color: var(--color-text);
100 + opacity: 0.5;
101 + flex-shrink: 0;
102 + background-color: rgba(255, 255, 255, 0.06);
103 + padding: 1px 6px;
104 + border-radius: 8px;
105 +}
106 +
107 +/* Process Group Content - Animated expand/collapse */
108 +.process-group-content {
109 + display: grid;
110 + grid-template-rows: 0fr;
111 + opacity: 0;
112 + margin-top: 0;
113 + padding-top: 0;
114 + border-top: 1px solid transparent;
115 + transition: grid-template-rows 0.25s ease-out,
116 + opacity 0.2s ease-out,
117 + margin-top 0.25s ease-out,
118 + padding-top 0.25s ease-out,
119 + border-color 0.2s ease-out;
120 + overflow: hidden;
121 +}
122 +
123 +.process-group-content > .process-steps {
124 + min-height: 0;
125 +}
126 +
127 +.process-group.expanded .process-group-content {
128 + grid-template-rows: 1fr;
129 + opacity: 1;
130 + margin-top: var(--spacing-sm);
131 + padding-top: var(--spacing-sm);
132 + border-top-color: rgba(255, 255, 255, 0.06);
133 +}
134 +
135 +/* Process Steps List */
136 +.process-steps {
137 + padding: 0;
138 + display: flex;
139 + flex-direction: column;
140 + gap: 2px;
141 +}
142 +
143 +/* Individual Process Step */
144 +.process-step {
145 + display: flex;
146 + flex-direction: column;
147 + padding: var(--spacing-xxs) var(--spacing-xs);
148 + border-radius: 4px;
149 + transition: background-color 0.15s ease;
150 +}
151 +
152 +.process-step:hover {
153 + background-color: rgba(255, 255, 255, 0.03);
154 +}
155 +
156 +/* Utility/Info/Hint steps have different background tint */
157 +.process-step[data-type="util"],
158 +.process-step[data-type="info"],
159 +.process-step[data-type="hint"] {
160 + background-color: rgba(35, 33, 26, 0.3);
161 +}
162 +
163 +.process-step[data-type="util"]:hover,
164 +.process-step[data-type="info"]:hover,
165 +.process-step[data-type="hint"]:hover {
166 + background-color: rgba(35, 33, 26, 0.5);
167 +}
168 +
169 +.light-mode .process-step[data-type="util"],
170 +.light-mode .process-step[data-type="info"],
171 +.light-mode .process-step[data-type="hint"] {
172 + background-color: rgba(91, 85, 64, 0.08);
173 +}
174 +
175 +.light-mode .process-step[data-type="util"]:hover,
176 +.light-mode .process-step[data-type="info"]:hover,
177 +.light-mode .process-step[data-type="hint"]:hover {
178 + background-color: rgba(91, 85, 64, 0.12);
179 +}
180 +
181 +/* Step Header (clickable) */
182 +.process-step-header {
183 + display: flex;
184 + align-items: center;
185 + cursor: pointer;
186 + user-select: none;
187 + gap: var(--spacing-xs);
188 + min-height: 20px;
189 +}
190 +
191 +.process-step-header .step-icon {
192 + font-size: 0.85rem;
193 + opacity: 0.6;
194 + width: 16px;
195 + text-align: center;
196 +}
197 +
198 +/* Step type colors */
199 +.process-step[data-type="agent"] .step-icon { color: #64b5f6; }
200 +.process-step[data-type="tool"] .step-icon { color: #81c784; }
201 +.process-step[data-type="code_exe"] .step-icon { color: #ba68c8; }
202 +.process-step[data-type="browser"] .step-icon { color: #ffb74d; }
203 +.process-step[data-type="info"] .step-icon { color: #90a4ae; }
204 +.process-step[data-type="util"] .step-icon { color: #78909c; }
205 +.process-step[data-type="hint"] .step-icon { color: #aed581; }
206 +.process-step[data-type="warning"] .step-icon { color: #ffd54f; }
207 +.process-step[data-type="error"] .step-icon { color: #e57373; }
208 +
209 +.process-step-header .step-type {
210 + font-size: 0.7rem;
211 + font-weight: 500;
212 + opacity: 0.5;
213 + min-width: 50px;
214 + text-transform: uppercase;
215 + letter-spacing: 0.3px;
216 +}
217 +
218 +.process-step-header .step-title {
219 + flex: 1;
220 + font-size: 0.75rem;
221 + color: var(--color-text);
222 + opacity: 0.7;
223 + white-space: nowrap;
224 + overflow: hidden;
225 + text-overflow: ellipsis;
226 +}
227 +
228 +.process-step-header .step-expand-icon {
229 + font-size: 0.8rem;
230 + opacity: 0.4;
231 + transition: transform 0.2s ease, opacity 0.15s ease;
232 +}
233 +
234 +.process-step-header:hover .step-expand-icon {
235 + opacity: 0.7;
236 +}
237 +
238 +.process-step.step-expanded .step-expand-icon {
239 + transform: rotate(180deg);
240 +}
241 +
242 +/* Step Detail Content - Animated expand/collapse */
243 +.process-step-detail {
244 + display: grid;
245 + grid-template-rows: 0fr;
246 + opacity: 0;
247 + transition: grid-template-rows 0.2s ease-out, opacity 0.15s ease-out;
248 + overflow: hidden;
249 +}
250 +
251 +.process-step-detail > .process-step-detail-content {
252 + min-height: 0;
253 + overflow: hidden;
254 +}
255 +
256 +.process-step.step-expanded .process-step-detail {
257 + grid-template-rows: 1fr;
258 + opacity: 1;
259 +}
260 +
261 +.process-step-detail-content {
262 + padding: var(--spacing-xs) var(--spacing-sm);
263 + margin-top: var(--spacing-xxs);
264 + margin-left: 20px; /* Align with icon */
265 + background-color: rgba(0, 0, 0, 0.15);
266 + border-radius: 4px;
267 + font-size: 0.7rem;
268 + line-height: 1.5;
269 + max-height: 300px;
270 + overflow-y: auto;
271 + border-left: 2px solid rgba(255, 255, 255, 0.08);
272 +}
273 +
274 +.process-step-detail-content pre {
275 + margin: 0;
276 + white-space: pre-wrap;
277 + word-break: break-word;
278 + font-family: var(--font-family-code);
279 + font-size: 0.7rem;
280 + color: var(--color-text);
281 + opacity: 0.8;
282 +}
283 +
284 +/* KVPs in step detail */
285 +.process-step-detail-content .step-kvps {
286 + display: flex;
287 + flex-direction: column;
288 + gap: var(--spacing-xxs);
289 +}
290 +
291 +.process-step-detail-content .step-kvp {
292 + display: flex;
293 + gap: var(--spacing-xs);
294 +}
295 +
296 +.process-step-detail-content .step-kvp-key {
297 + color: var(--color-primary);
298 + font-weight: 500;
299 + min-width: 80px;
300 + opacity: 0.8;
301 +}
302 +
303 +.process-step-detail-content .step-kvp-value {
304 + flex: 1;
305 + color: var(--color-text);
306 + opacity: 0.75;
307 + word-break: break-word;
308 + font-size: 0.7rem;
309 +}
310 +
311 +/* Light mode adjustments */
312 +.light-mode .process-group {
313 + background: rgba(0, 0, 0, 0.03);
314 + border-color: rgba(0, 0, 0, 0.08);
315 +}
316 +
317 +.light-mode .process-group.embedded {
318 + background: rgba(0, 0, 0, 0.04);
319 + border-bottom-color: rgba(0, 0, 0, 0.08);
320 +}
321 +
322 +.light-mode .message-container.has-process-group {
323 + background: var(--color-panel);
324 + border-color: rgba(0, 0, 0, 0.08);
325 +}
326 +
327 +.light-mode .process-group:hover {
328 + border-color: rgba(0, 0, 0, 0.12);
329 +}
330 +
331 +.light-mode .process-group.expanded .process-group-content {
332 + border-top-color: rgba(0, 0, 0, 0.06);
333 +}
334 +
335 +.light-mode .process-step:hover {
336 + background-color: rgba(0, 0, 0, 0.02);
337 +}
338 +
339 +.light-mode .process-step-detail-content {
340 + background-color: rgba(0, 0, 0, 0.03);
341 + border-left-color: rgba(0, 0, 0, 0.08);
342 +}
343 +
344 +.light-mode .process-group-header .step-count {
345 + background-color: rgba(0, 0, 0, 0.06);
346 +}
347 +
348 +/* Light mode text colors for process group */
349 +.light-mode .process-group-header .group-title,
350 +.light-mode .process-group-header .step-count,
351 +.light-mode .process-step-header .step-title,
352 +.light-mode .process-step-header .step-type {
353 + color: #188216;
354 +}
355 +
356 +/* Animation for loading state */
357 +@keyframes pulse-step {
358 + 0%, 100% { opacity: 0.5; }
359 + 50% { opacity: 0.8; }
360 +}
361 +
362 +.process-step.loading .step-icon {
363 + animation: pulse-step 1.2s ease-in-out infinite;
364 +}
365 +
366 +/* Responsive adjustments */
367 +@media (max-width: 768px) {
368 + .process-group {
369 + padding: var(--spacing-xs) var(--spacing-sm);
370 + }
371 +
372 + .process-step-header .step-type {
373 + display: none;
374 + }
375 +
376 + .process-step-header .step-title {
377 + font-size: 0.7rem;
378 + }
379 +
380 + .process-step-detail-content {
381 + margin-left: 16px;
382 + }
383 +}
384 +
385 +/* ===========================================
386 + Preferences Visibility Controls
387 + These rules work with preferences-store toggles
388 + =========================================== */
389 +
390 +/* Utility steps - default hidden (controlled by showUtils) */
391 +.process-step.message-util {
392 + display: none;
393 +}
394 +
395 +.process-step.message-util.show-util {
396 + display: flex;
397 +}
398 +
399 +/* Thoughts KVP row - default visible (controlled by showThoughts) */
400 +.step-kvp.msg-thoughts {
401 + display: flex;
402 +}
403 +
404 +.step-kvp.msg-thoughts.hide-thoughts {
405 + display: none;
406 +}
407 +
408 +/* JSON pre content - default hidden (controlled by showJson) */
409 +.process-step-detail-content pre.msg-json {
410 + display: none;
411 +}
412 +
413 +.process-step-detail-content pre.msg-json.show-json {
414 + display: block;
415 +}
webui/components/sidebar/bottom/preferences/preferences-store.js
+47
@@ -59,6 +59,16 @@ const model = {
59 },
60 _showUtils: false,
61
62 + // Process group collapse preference
63 + get collapseProcessGroups() {
64 + return this._collapseProcessGroups;
65 + },
66 + set collapseProcessGroups(value) {
67 + this._collapseProcessGroups = value;
68 + this._applyCollapseProcessGroups(value);
69 + },
70 + _collapseProcessGroups: true, // Default to collapsed
71 +
72 // Initialize preferences and apply current state
73 init() {
74 try {
@@ -77,6 +87,14 @@ const model = {
87 this._speech = false; // Default to speech off if localStorage is unavailable
88 }
89
90 + // Load collapse process groups preference
91 + try {
92 + const storedCollapse = localStorage.getItem("collapseProcessGroups");
93 + this._collapseProcessGroups = storedCollapse !== "false"; // Default true
94 + } catch {
95 + this._collapseProcessGroups = true;
96 + }
97 +
98 // Apply all preferences
99 this._applyDarkMode(this._darkMode);
100 this._applyAutoScroll(this._autoScroll);
@@ -84,6 +102,7 @@ const model = {
102 this._applyShowThoughts(this._showThoughts);
103 this._applyShowJson(this._showJson);
104 this._applyShowUtils(this._showUtils);
105 + this._applyCollapseProcessGroups(this._collapseProcessGroups);
106 } catch (e) {
107 console.error("Failed to initialize preferences store", e);
108 }
@@ -110,23 +129,51 @@ const model = {
129 },
130
131 _applyShowThoughts(value) {
132 + // For original messages
133 css.toggleCssProperty(
134 ".msg-thoughts",
135 "display",
136 value ? undefined : "none"
137 );
138 + // For process steps - toggle class on all existing elements
139 + document.querySelectorAll(".step-kvp.msg-thoughts").forEach((el) => {
140 + el.classList.toggle("hide-thoughts", !value);
141 + });
142 },
143
144 _applyShowJson(value) {
145 + // For original messages
146 css.toggleCssProperty(".msg-json", "display", value ? "block" : "none");
147 + // For process steps - toggle class on pre elements with msg-json
148 + document.querySelectorAll(".process-step-detail-content pre.msg-json").forEach((el) => {
149 + el.classList.toggle("show-json", value);
150 + });
151 },
152
153 _applyShowUtils(value) {
154 + // For original messages
155 css.toggleCssProperty(
156 ".message-util",
157 "display",
158 value ? undefined : "none"
159 );
160 + // For process steps - toggle class on all existing elements
161 + document.querySelectorAll(".process-step.message-util").forEach((el) => {
162 + el.classList.toggle("show-util", value);
163 + });
164 + },
165 +
166 + _applyCollapseProcessGroups(value) {
167 + localStorage.setItem("collapseProcessGroups", value);
168 + // Update process group store default
169 + try {
170 + const processGroupStore = window.Alpine?.store("processGroup");
171 + if (processGroupStore) {
172 + processGroupStore.defaultCollapsed = value;
173 + }
174 + } catch (e) {
175 + // Store may not be initialized yet
176 + }
177 },
178 };
179
webui/index.html
+1
@@ -9,6 +9,7 @@
9 <link rel="stylesheet" href="index.css">
10 <link rel="stylesheet" href="css/messages.css">
11 <link rel="stylesheet" href="components/messages/action-buttons/simple-action-buttons.css">
12 + <link rel="stylesheet" href="components/messages/process-group/process-group.css">
13 <link rel="stylesheet" href="css/toast.css">
14 <link rel="stylesheet" href="css/settings.css">
15 <link rel="stylesheet" href="css/modals.css">
webui/index.js
+5
@@ -10,6 +10,7 @@ import { store as inputStore } from "/components/chat/input/input-store.js";
10 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 processGroupStore } from "/components/messages/process-group/process-group-store.js";
14
15 globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
16
@@ -292,6 +293,7 @@ export async function poll() {
293 if (lastLogGuid != response.log_guid) {
294 const chatHistoryEl = document.getElementById("chat-history");
295 if (chatHistoryEl) chatHistoryEl.innerHTML = "";
296 + msgs.resetProcessGroups(); // Reset process groups on chat reset
297 lastLogVersion = 0;
298 lastLogGuid = response.log_guid;
299 await poll();
@@ -482,6 +484,9 @@ export const setContext = function (id) {
484 // Stop speech when switching chats
485 speechStore.stopAudio();
486
487 + // Reset process groups for new context
488 + msgs.resetProcessGroups();
489 +
490 // Clear the chat history immediately to avoid showing stale content
491 const chatHistoryEl = document.getElementById("chat-history");
492 if (chatHistoryEl) chatHistoryEl.innerHTML = "";
webui/js/messages.js
+424 -6
@@ -4,22 +4,74 @@ 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 preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
9
10 const chatHistory = document.getElementById("chat-history");
11
12 let messageGroup = null;
13 +let currentProcessGroup = null; // Track current process group for collapsible UI
14 +
15 +// Process types that should be grouped into collapsible sections
16 +const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'info', 'hint', 'util'];
17 +// Main types that should always be visible (not collapsed)
18 +const MAIN_TYPES = ['user', 'response', 'warning', 'error', 'rate_limit'];
19
20 // Simplified implementation - no complex interactions needed
21
22 export function setMessage(id, type, heading, content, temp, kvps = null) {
23 + // Check if this is a process type message
24 + const isProcessType = PROCESS_TYPES.includes(type);
25 + const isMainType = MAIN_TYPES.includes(type);
26 +
27 // Search for the existing message container by id
28 let messageContainer = document.getElementById(`message-${id}`);
29 + let processStepElement = document.getElementById(`process-step-${id}`);
30 let isNewMessage = false;
31
19 - if (messageContainer) {
20 - // Don't clear innerHTML - we'll do incremental updates
21 - // messageContainer.innerHTML = "";
22 - } else {
32 + // For user messages, close current process group FIRST (start fresh for next interaction)
33 + if (type === "user") {
34 + currentProcessGroup = null;
35 + }
36 +
37 + // For process types, check if we should add to process group
38 + if (isProcessType) {
39 + if (processStepElement) {
40 + // Update existing process step
41 + updateProcessStep(processStepElement, id, type, heading, content, kvps);
42 + return processStepElement;
43 + }
44 +
45 + // Create or get process group for current interaction
46 + if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) {
47 + currentProcessGroup = createProcessGroup(id);
48 + chatHistory.appendChild(currentProcessGroup);
49 + }
50 +
51 + // Add step to current process group
52 + processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps);
53 + return processStepElement;
54 + }
55 +
56 + // For response type, embed the current process group inside (but keep reference)
57 + if (type === "response" && currentProcessGroup) {
58 + const processGroupToEmbed = currentProcessGroup;
59 + // Keep currentProcessGroup reference - subsequent process messages go to same group
60 +
61 + if (!messageContainer) {
62 + // Create new container with embedded process group
63 + messageContainer = createResponseContainerWithProcessGroup(id, processGroupToEmbed);
64 + isNewMessage = true;
65 + } else {
66 + // Check if already embedded
67 + const existingEmbedded = messageContainer.querySelector(".process-group");
68 + if (!existingEmbedded && processGroupToEmbed) {
69 + embedProcessGroup(messageContainer, processGroupToEmbed);
70 + }
71 + }
72 + }
73 +
74 + if (!messageContainer) {
75 // Create a new container if not found
76 isNewMessage = true;
77 const sender = type === "user" ? "user" : "ai";
@@ -46,7 +98,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null) {
98 };
99 //force new group on these types
100 const groupStart = {
49 - agent: true,
101 + response: true, // response starts a new group
102 // anything else is false
103 };
104
@@ -1007,4 +1059,370 @@ class Scroller {
1059 reApplyScroll() {
1060 if (this.wasAtBottom) this.element.scrollTop = this.element.scrollHeight;
1061 }
1010 -}
\ No newline at end of file
1062 +}
1063 +
1064 +// ============================================
1065 +// Process Group Embedding Functions
1066 +// ============================================
1067 +
1068 +/**
1069 + * Create a response container with an embedded process group
1070 + */
1071 +function createResponseContainerWithProcessGroup(id, processGroup) {
1072 + const messageContainer = document.createElement("div");
1073 + messageContainer.id = `message-${id}`;
1074 + messageContainer.classList.add("message-container", "ai-container", "has-process-group");
1075 +
1076 + // Move process group from chatHistory into the container
1077 + if (processGroup && processGroup.parentNode) {
1078 + processGroup.parentNode.removeChild(processGroup);
1079 + }
1080 +
1081 + // Process group will be the first child
1082 + if (processGroup) {
1083 + processGroup.classList.add("embedded");
1084 + messageContainer.appendChild(processGroup);
1085 + }
1086 +
1087 + return messageContainer;
1088 +}
1089 +
1090 +/**
1091 + * Embed a process group into an existing message container
1092 + */
1093 +function embedProcessGroup(messageContainer, processGroup) {
1094 + if (!messageContainer || !processGroup) return;
1095 +
1096 + // Remove from current parent
1097 + if (processGroup.parentNode) {
1098 + processGroup.parentNode.removeChild(processGroup);
1099 + }
1100 +
1101 + // Add embedded class
1102 + processGroup.classList.add("embedded");
1103 + messageContainer.classList.add("has-process-group");
1104 +
1105 + // Insert at the beginning of the container
1106 + const firstChild = messageContainer.firstChild;
1107 + if (firstChild) {
1108 + messageContainer.insertBefore(processGroup, firstChild);
1109 + } else {
1110 + messageContainer.appendChild(processGroup);
1111 + }
1112 +}
1113 +
1114 +// ============================================
1115 +// Process Group Functions
1116 +// ============================================
1117 +
1118 +/**
1119 + * Create a new collapsible process group
1120 + */
1121 +function createProcessGroup(id) {
1122 + const groupId = `process-group-${id}`;
1123 + const group = document.createElement("div");
1124 + group.id = groupId;
1125 + group.classList.add("process-group");
1126 + group.setAttribute("data-group-id", groupId);
1127 +
1128 + // Default to collapsed state - don't add 'expanded' class
1129 + // (Users can expand manually by clicking)
1130 +
1131 + // Create header
1132 + const header = document.createElement("div");
1133 + header.classList.add("process-group-header");
1134 + header.innerHTML = `
1135 + <span class="material-symbols-outlined expand-icon">chevron_right</span>
1136 + <span class="material-symbols-outlined group-icon">neurology</span>
1137 + <span class="group-title">Processing...</span>
1138 + <span class="step-count">0 steps</span>
1139 + `;
1140 +
1141 + // Add click handler for expansion
1142 + header.addEventListener("click", (e) => {
1143 + processGroupStore.toggleGroup(groupId);
1144 + const newState = processGroupStore.isGroupExpanded(groupId);
1145 + group.classList.toggle("expanded", newState);
1146 + });
1147 +
1148 + group.appendChild(header);
1149 +
1150 + // Create content container
1151 + const content = document.createElement("div");
1152 + content.classList.add("process-group-content");
1153 +
1154 + // Create steps container
1155 + const steps = document.createElement("div");
1156 + steps.classList.add("process-steps");
1157 + content.appendChild(steps);
1158 +
1159 + group.appendChild(content);
1160 +
1161 + return group;
1162 +}
1163 +
1164 +/**
1165 + * Add a step to a process group
1166 + */
1167 +function addProcessStep(group, id, type, heading, content, kvps) {
1168 + const groupId = group.getAttribute("data-group-id");
1169 + const stepsContainer = group.querySelector(".process-steps");
1170 +
1171 + // Create step element
1172 + const step = document.createElement("div");
1173 + step.id = `process-step-${id}`;
1174 + step.classList.add("process-step");
1175 + step.setAttribute("data-type", type);
1176 + step.setAttribute("data-step-id", id);
1177 +
1178 + // Add message-util class for utility/info types (controlled by showUtils preference)
1179 + if (type === "util" || type === "info" || type === "hint") {
1180 + step.classList.add("message-util");
1181 + // Apply current preference state
1182 + if (preferencesStore.showUtils) {
1183 + step.classList.add("show-util");
1184 + }
1185 + }
1186 +
1187 + // Get step info
1188 + const icon = processGroupStore.getStepIcon(type);
1189 + const label = processGroupStore.getStepLabel(type);
1190 + const title = getStepTitle(heading, kvps, type);
1191 +
1192 + // Check if step should be expanded
1193 + const isStepExpanded = processGroupStore.isStepExpanded(groupId, id);
1194 + if (isStepExpanded) {
1195 + step.classList.add("step-expanded");
1196 + }
1197 +
1198 + // Create step header
1199 + const stepHeader = document.createElement("div");
1200 + stepHeader.classList.add("process-step-header");
1201 + stepHeader.innerHTML = `
1202 + <span class="material-symbols-outlined step-icon">${icon}</span>
1203 + <span class="step-type">${label}</span>
1204 + <span class="step-title">${escapeHTML(title)}</span>
1205 + <span class="material-symbols-outlined step-expand-icon">expand_more</span>
1206 + `;
1207 +
1208 + // Add click handler for step expansion
1209 + stepHeader.addEventListener("click", (e) => {
1210 + e.stopPropagation();
1211 + processGroupStore.toggleStep(groupId, id);
1212 + step.classList.toggle("step-expanded", processGroupStore.isStepExpanded(groupId, id));
1213 + });
1214 +
1215 + step.appendChild(stepHeader);
1216 +
1217 + // Create step detail container
1218 + const detail = document.createElement("div");
1219 + detail.classList.add("process-step-detail");
1220 +
1221 + const detailContent = document.createElement("div");
1222 + detailContent.classList.add("process-step-detail-content");
1223 +
1224 + // Add content to detail
1225 + renderStepDetailContent(detailContent, content, kvps);
1226 +
1227 + detail.appendChild(detailContent);
1228 + step.appendChild(detail);
1229 +
1230 + stepsContainer.appendChild(step);
1231 +
1232 + // Update group header
1233 + updateProcessGroupHeader(group);
1234 +
1235 + return step;
1236 +}
1237 +
1238 +/**
1239 + * Update an existing process step
1240 + */
1241 +function updateProcessStep(stepElement, id, type, heading, content, kvps) {
1242 + // Update title
1243 + const titleEl = stepElement.querySelector(".step-title");
1244 + if (titleEl) {
1245 + const title = getStepTitle(heading, kvps, type);
1246 + titleEl.textContent = title;
1247 + }
1248 +
1249 + // Update detail content
1250 + const detailContent = stepElement.querySelector(".process-step-detail-content");
1251 + if (detailContent) {
1252 + renderStepDetailContent(detailContent, content, kvps);
1253 + }
1254 +
1255 + // Update parent group header
1256 + const group = stepElement.closest(".process-group");
1257 + if (group) {
1258 + updateProcessGroupHeader(group);
1259 + }
1260 +}
1261 +
1262 +/**
1263 + * Get a concise title for a process step
1264 + */
1265 +function getStepTitle(heading, kvps, type) {
1266 + // Try to get a meaningful title from heading or kvps
1267 + if (heading && heading.trim()) {
1268 + return cleanStepTitle(heading, 80);
1269 + }
1270 +
1271 + if (kvps) {
1272 + // Try common fields for title
1273 + if (kvps.tool_name) {
1274 + const headline = kvps.headline ? cleanStepTitle(kvps.headline, 60) : '';
1275 + return `${kvps.tool_name}${headline ? ': ' + headline : ''}`;
1276 + }
1277 + if (kvps.headline) {
1278 + return cleanStepTitle(kvps.headline, 80);
1279 + }
1280 + if (kvps.query) {
1281 + return truncateText(kvps.query, 80);
1282 + }
1283 + if (kvps.thoughts) {
1284 + return truncateText(String(kvps.thoughts), 80);
1285 + }
1286 + }
1287 +
1288 + return processGroupStore.getStepLabel(type);
1289 +}
1290 +
1291 +/**
1292 + * Clean step title by removing icon:// prefixes and agent markers
1293 + */
1294 +function cleanStepTitle(text, maxLength) {
1295 + if (!text) return "";
1296 + let cleaned = String(text);
1297 +
1298 + // Remove icon:// patterns (e.g., "icon://network_intelligence")
1299 + cleaned = cleaned.replace(/icon:\/\/[a-zA-Z0-9_]+\s*/g, "");
1300 +
1301 + // Remove agent markers like "A0:" or similar
1302 + cleaned = cleaned.replace(/^[A-Z]\d+:\s*/i, "");
1303 +
1304 + // Trim whitespace
1305 + cleaned = cleaned.trim();
1306 +
1307 + return truncateText(cleaned, maxLength);
1308 +}
1309 +
1310 +/**
1311 + * Render content for step detail panel
1312 + */
1313 +function renderStepDetailContent(container, content, kvps) {
1314 + container.innerHTML = "";
1315 +
1316 + // Add KVPs if present
1317 + if (kvps && Object.keys(kvps).length > 0) {
1318 + const kvpsDiv = document.createElement("div");
1319 + kvpsDiv.classList.add("step-kvps");
1320 +
1321 + for (const [key, value] of Object.entries(kvps)) {
1322 + // Skip internal/display keys
1323 + if (key === "finished" || key === "attachments") continue;
1324 +
1325 + const kvpDiv = document.createElement("div");
1326 + kvpDiv.classList.add("step-kvp");
1327 +
1328 + // Add msg-thoughts class for thoughts-related keys (controlled by showThoughts preference)
1329 + const lowerKey = key.toLowerCase();
1330 +
1331 + if (lowerKey === "thoughts" || lowerKey === "thinking" || lowerKey === "reflection") {
1332 + kvpDiv.classList.add("msg-thoughts");
1333 + // Apply current preference state - hide if showThoughts is false
1334 + if (!preferencesStore.showThoughts) {
1335 + kvpDiv.classList.add("hide-thoughts");
1336 + }
1337 + }
1338 +
1339 + const keySpan = document.createElement("span");
1340 + keySpan.classList.add("step-kvp-key");
1341 + keySpan.textContent = convertToTitleCase(key) + ":";
1342 +
1343 + const valueSpan = document.createElement("span");
1344 + valueSpan.classList.add("step-kvp-value");
1345 +
1346 + let valueText = value;
1347 + if (typeof value === "object") {
1348 + valueText = JSON.stringify(value, null, 2);
1349 + }
1350 +
1351 + valueSpan.textContent = truncateText(String(valueText), 500);
1352 +
1353 + kvpDiv.appendChild(keySpan);
1354 + kvpDiv.appendChild(valueSpan);
1355 + kvpsDiv.appendChild(kvpDiv);
1356 + }
1357 +
1358 + container.appendChild(kvpsDiv);
1359 + }
1360 +
1361 + // Add main content if present (JSON content - controlled by showJson preference)
1362 + if (content && content.trim()) {
1363 + const pre = document.createElement("pre");
1364 + pre.classList.add("msg-json");
1365 + // Apply current preference state
1366 + if (preferencesStore.showJson) {
1367 + pre.classList.add("show-json");
1368 + }
1369 + pre.textContent = truncateText(content, 1000);
1370 + container.appendChild(pre);
1371 + }
1372 +}
1373 +
1374 +/**
1375 + * Update process group header with step count and status
1376 + */
1377 +function updateProcessGroupHeader(group) {
1378 + const steps = group.querySelectorAll(".process-step");
1379 + const countEl = group.querySelector(".step-count");
1380 + const titleEl = group.querySelector(".group-title");
1381 +
1382 + if (countEl) {
1383 + const count = steps.length;
1384 + countEl.textContent = `${count} step${count !== 1 ? "s" : ""}`;
1385 + }
1386 +
1387 + if (titleEl && steps.length > 0) {
1388 + // Get the last step's type for the title
1389 + const lastStep = steps[steps.length - 1];
1390 + const lastType = lastStep.getAttribute("data-type");
1391 + const lastTitle = lastStep.querySelector(".step-title")?.textContent || "";
1392 +
1393 + // Prefer agent type steps for the group title as they contain thinking/planning info
1394 + if (lastType === "agent" && lastTitle) {
1395 + titleEl.textContent = cleanStepTitle(lastTitle, 50);
1396 + } else {
1397 + // Try to find the most recent agent step for a better title
1398 + const agentSteps = group.querySelectorAll('.process-step[data-type="agent"]');
1399 + if (agentSteps.length > 0) {
1400 + const lastAgentStep = agentSteps[agentSteps.length - 1];
1401 + const agentTitle = lastAgentStep.querySelector(".step-title")?.textContent || "";
1402 + if (agentTitle) {
1403 + titleEl.textContent = cleanStepTitle(agentTitle, 50);
1404 + return;
1405 + }
1406 + }
1407 + titleEl.textContent = `Processing (${processGroupStore.getStepLabel(lastType)})`;
1408 + }
1409 + }
1410 +}
1411 +
1412 +/**
1413 + * Truncate text to a maximum length
1414 + */
1415 +function truncateText(text, maxLength) {
1416 + if (!text) return "";
1417 + text = String(text).trim();
1418 + if (text.length <= maxLength) return text;
1419 + return text.substring(0, maxLength - 3) + "...";
1420 +}
1421 +
1422 +/**
1423 + * Reset process group state (called on context switch)
1424 + */
1425 +export function resetProcessGroups() {
1426 + currentProcessGroup = null;
1427 + messageGroup = null;
1428 +}