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