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);