embed process group at creation
3clyp50 committed
Jan 19, 2026 at 14:03 UTC
2f034e750efd105eccd0c3eaf86d1e632534da13
1 file changed
+58
-125
webui/js/messages.js
+58
-125
@@ -75,15 +75,35 @@ const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'progress', 'info
75
// Main types that should always be visible (not collapsed)
76
const MAIN_TYPES = ['user', 'response', 'error', 'rate_limit'];
77
78
+/**
79
+ * Helper to append a message container to the correct group in chat history
80
+ */
81
+function appendMessageToHistory(messageContainer, groupType, forceNewGroup, id) {
82
+ // Check if current messageGroup is still in DOM, if not, reset it (context switch)
83
+ if (messageGroup && !document.getElementById(messageGroup.id)) {
84
+ messageGroup = null;
85
+ }
86
+
87
+ // Create new group if needed
88
+ if (!messageGroup || forceNewGroup || groupType !== messageGroup.getAttribute("data-group-type")) {
89
+ messageGroup = document.createElement("div");
90
+ messageGroup.id = `message-group-${id}`;
91
+ messageGroup.classList.add("message-group", `message-group-${groupType}`);
92
+ messageGroup.setAttribute("data-group-type", groupType);
93
+ chatHistory.appendChild(messageGroup);
94
+ }
95
+
96
+ // Append message to group
97
+ messageGroup.appendChild(messageContainer);
98
+}
99
+
100
export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
101
// Check if this is a process type message
102
const isProcessType = PROCESS_TYPES.includes(type);
81
- const isMainType = MAIN_TYPES.includes(type);
103
104
// Search for the existing message container by id
105
let messageContainer = document.getElementById(`message-${id}`);
106
let processStepElement = document.getElementById(`process-step-${id}`);
86
- let isNewMessage = false;
107
108
// For user messages, close current process group FIRST (start fresh for next interaction)
109
if (type === "user") {
@@ -92,7 +112,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
112
}
113
114
// For process types, check if we should add to process group
95
- if (isProcessType) {
115
+ if (isProcessType || (type === "response" && agentNumber !== 0)) {
116
if (processStepElement) {
117
// Update existing process step
118
updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs, agentNumber);
@@ -101,61 +121,35 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
121
122
// Create or get process group for current interaction
123
if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) {
124
+ // Create response container for this process group immediately (Option B)
125
+ messageContainer = document.createElement("div");
126
+ messageContainer.id = `message-${id}`;
127
+ messageContainer.classList.add("message-container", "ai-container", "has-process-group");
128
+
129
currentProcessGroup = createProcessGroup(id);
105
- chatHistory.appendChild(currentProcessGroup);
130
+ currentProcessGroup.classList.add("embedded");
131
+ messageContainer.appendChild(currentProcessGroup);
132
+
133
+ // Handle DOM insertion immediately
134
+ appendMessageToHistory(messageContainer, "left", false, id);
135
+
136
setActiveProcessGroup(currentProcessGroup);
137
}
138
139
// Add step to current process group
110
- processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs, agentNumber);
111
- return processStepElement;
112
- }
113
-
114
- // For subordinate agent responses (A1, A2, ...), treat as a process step instead of main response
115
- // agentNumber: 0 = main agent, 1+ = subordinate agents
116
- // Note: subordinate "response" is a completion marker with content
117
- if (type === "response" && agentNumber !== 0) {
118
- if (processStepElement) {
119
- updateProcessStep(processStepElement, id, "response", heading, content, kvps, durationMs, agentNumber);
120
- return processStepElement;
121
- }
122
-
123
- // Create or get process group for current interaction
124
- if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) {
125
- currentProcessGroup = createProcessGroup(id);
126
- chatHistory.appendChild(currentProcessGroup);
127
- setActiveProcessGroup(currentProcessGroup);
128
- }
129
-
130
- // Add subordinate response as a response step (special type to show content)
131
- processStepElement = addProcessStep(currentProcessGroup, id, "response", heading, content, kvps, timestamp, durationMs, agentNumber);
140
+ const stepType = (type === "response" && agentNumber !== 0) ? "response" : type;
141
+ processStepElement = addProcessStep(currentProcessGroup, id, stepType, heading, content, kvps, timestamp, durationMs, agentNumber);
142
return processStepElement;
143
}
144
135
- // For main agent (A0) response, embed the current process group and mark as complete
145
+ // For main agent (A0) response, mark the current process group as complete
146
if (type === "response" && currentProcessGroup) {
137
- const processGroupToEmbed = currentProcessGroup;
138
- // Keep currentProcessGroup reference - subsequent process messages go to same group
139
-
147
// Mark process group as complete (END state)
141
- markProcessGroupComplete(processGroupToEmbed, heading);
142
-
143
- if (!messageContainer) {
144
- // Create new container with embedded process group
145
- messageContainer = createResponseContainerWithProcessGroup(id, processGroupToEmbed);
146
- isNewMessage = true;
147
- } else {
148
- // Check if already embedded
149
- const existingEmbedded = messageContainer.querySelector(".process-group");
150
- if (!existingEmbedded && processGroupToEmbed) {
151
- embedProcessGroup(messageContainer, processGroupToEmbed);
152
- }
153
- }
148
+ markProcessGroupComplete(currentProcessGroup, heading);
149
}
150
151
if (!messageContainer) {
152
// Create a new container if not found
158
- isNewMessage = true;
153
const sender = type === "user" ? "user" : "ai";
154
messageContainer = document.createElement("div");
155
messageContainer.id = `message-${id}`;
@@ -165,8 +159,8 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
159
const handler = getHandler(type);
160
handler(messageContainer, id, type, heading, content, temp, kvps);
161
168
- // If this is a new message, handle DOM insertion
169
- if (!document.getElementById(`message-${id}`)) {
162
+ // If this is a new message (not yet in DOM), handle DOM insertion
163
+ if (!messageContainer.parentNode) {
164
// message type visual grouping
165
const groupTypeMap = {
166
user: "right",
@@ -186,27 +180,11 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
180
};
181
182
const groupType = groupTypeMap[type] || "left";
183
+ const forceNewGroup = groupStart[type] || false;
184
190
- // here check if messageGroup is still in DOM, if not, then set it to null (context switch)
191
- if (messageGroup && !document.getElementById(messageGroup.id))
192
- messageGroup = null;
193
-
194
- if (
195
- !messageGroup || // no group yet exists
196
- groupStart[type] || // message type forces new group
197
- groupType != messageGroup.getAttribute("data-group-type") // message type changes group
198
- ) {
199
- messageGroup = document.createElement("div");
200
- messageGroup.id = `message-group-${id}`;
201
- messageGroup.classList.add(`message-group`, `message-group-${groupType}`);
202
- messageGroup.setAttribute("data-group-type", groupType);
203
- }
204
- messageGroup.appendChild(messageContainer);
205
- chatHistory.appendChild(messageGroup);
185
+ appendMessageToHistory(messageContainer, groupType, forceNewGroup, id);
186
}
187
208
- // Simplified implementation - no setup needed
209
-
188
return messageContainer;
189
}
190
@@ -1151,52 +1129,6 @@ class Scroller {
1129
// Process Group Embedding Functions
1130
// ============================================
1131
1154
-/**
1155
- * Create a response container with an embedded process group
1156
- */
1157
-function createResponseContainerWithProcessGroup(id, processGroup) {
1158
- const messageContainer = document.createElement("div");
1159
- messageContainer.id = `message-${id}`;
1160
- messageContainer.classList.add("message-container", "ai-container", "has-process-group");
1161
-
1162
- // Move process group from chatHistory into the container
1163
- if (processGroup && processGroup.parentNode) {
1164
- processGroup.parentNode.removeChild(processGroup);
1165
- }
1166
-
1167
- // Process group will be the first child
1168
- if (processGroup) {
1169
- processGroup.classList.add("embedded");
1170
- messageContainer.appendChild(processGroup);
1171
- }
1172
-
1173
- return messageContainer;
1174
-}
1175
-
1176
-/**
1177
- * Embed a process group into an existing message container
1178
- */
1179
-function embedProcessGroup(messageContainer, processGroup) {
1180
- if (!messageContainer || !processGroup) return;
1181
-
1182
- // Remove from current parent
1183
- if (processGroup.parentNode) {
1184
- processGroup.parentNode.removeChild(processGroup);
1185
- }
1186
-
1187
- // Add embedded class
1188
- processGroup.classList.add("embedded");
1189
- messageContainer.classList.add("has-process-group");
1190
-
1191
- // Insert at the beginning of the container
1192
- const firstChild = messageContainer.firstChild;
1193
- if (firstChild) {
1194
- messageContainer.insertBefore(processGroup, firstChild);
1195
- } else {
1196
- messageContainer.appendChild(processGroup);
1197
- }
1198
-}
1199
-
1132
// ============================================
1133
// Process Group Functions
1134
// ============================================
@@ -1380,11 +1312,6 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1312
// Explicitly add or remove the class based on state
1313
if (newState) {
1314
step.classList.add("step-expanded");
1383
- // Scroll terminal for newly expanded steps
1384
- requestAnimationFrame(() => {
1385
- const terminal = step.querySelector(".terminal-output");
1386
- if (terminal) terminal.scrollTop = terminal.scrollHeight;
1387
- });
1315
} else {
1316
step.classList.remove("step-expanded");
1317
}
@@ -1405,14 +1332,6 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1332
detail.appendChild(detailContent);
1333
step.appendChild(detail);
1334
1408
- // Scroll terminal for already expanded steps
1409
- if (isStepExpanded) {
1410
- requestAnimationFrame(() => {
1411
- const terminal = step.querySelector(".terminal-output");
1412
- if (terminal) terminal.scrollTop = terminal.scrollHeight;
1413
- });
1414
- }
1415
-
1335
// Track delegation steps for nesting
1336
if (toolNameToUse === "call_subordinate") {
1337
currentDelegationSteps[agentNumber] = step;
@@ -1441,6 +1360,12 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1360
1361
appendTarget.appendChild(step);
1362
1363
+ // Scroll terminal to bottom on initial render (including page refresh)
1364
+ const initialTerminal = step.querySelector(".terminal-output");
1365
+ if (initialTerminal) {
1366
+ initialTerminal.scrollTop = initialTerminal.scrollHeight;
1367
+ }
1368
+
1369
// Update group header
1370
updateProcessGroupHeader(group);
1371
@@ -1490,6 +1415,10 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1415
let skipFullRender = false;
1416
1417
if (detailContent) {
1418
+ // Capture scroll state before re-render (uses existing Scroller pattern)
1419
+ const terminal = detailContent.querySelector(".terminal-output");
1420
+ const scroller = terminal ? new Scroller(terminal) : null;
1421
+
1422
// For browser, update image src incrementally to avoid flashing
1423
if (type === "browser" && kvps?.screenshot) {
1424
const existingImg = detailContent.querySelector(".screenshot-img");
@@ -1506,6 +1435,12 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1435
1436
if (!skipFullRender) {
1437
renderStepDetailContent(detailContent, content, kvps, type);
1438
+
1439
+ // Re-apply scroll (stays at bottom if was at bottom)
1440
+ const newTerminal = detailContent.querySelector(".terminal-output");
1441
+ if (newTerminal && scroller?.wasAtBottom) {
1442
+ newTerminal.scrollTop = newTerminal.scrollHeight;
1443
+ }
1444
}
1445
}
1446
@@ -1639,8 +1574,6 @@ function renderStepDetailContent(container, content, kvps, type = null) {
1574
processedOutput = convertPathsToLinks(processedOutput);
1575
outputPre.innerHTML = processedOutput;
1576
terminalDiv.appendChild(outputPre);
1642
- // Scroll terminal to bottom
1643
- outputPre.scrollTop = outputPre.scrollHeight;
1577
}
1578
1579
container.appendChild(terminalDiv);