subordinates agents nesting in process groups

3clyp50 committed Jan 6, 2026 at 18:32 UTC 9295cd30cc02194e90ec195b43dc67466b5c34c3
3 files changed +220 -22
webui/components/messages/process-group/process-group-store.js
+2 -1
@@ -111,7 +111,8 @@ const model = {
111 // Toggle step expansion
112 toggleStep(groupId, stepId) {
113 const key = `${groupId}:${stepId}`;
114 - this.expandedSteps[key] = !this.expandedSteps[key];
114 + const currentState = this.expandedSteps[key] || false;
115 + this.expandedSteps[key] = !currentState;
116 this._persist();
117 },
118
webui/components/messages/process-group/process-group.css
+94 -3
@@ -9,7 +9,6 @@
9 max-width: 100%;
10 box-sizing: border-box;
11 flex-shrink: 0;
12 - width: fit-content;
12 }
13
14 /* Embedded Process Group inside Response */
@@ -433,7 +432,8 @@
432 opacity: 0.7;
433 }
434
436 -.process-step.step-expanded .step-expand-icon {
435 +/* Use direct child selector to prevent affecting nested steps */
436 +.process-step.step-expanded > .process-step-header > .step-expand-icon {
437 transform: rotate(90deg);
438 }
439
@@ -467,7 +467,8 @@
467 min-height: 0;
468 }
469
470 -.process-step.step-expanded .process-step-detail {
470 +/* Use direct child selector (>) to prevent cascading to nested steps */
471 +.process-step.step-expanded > .process-step-detail {
472 grid-template-rows: 1fr;
473 opacity: 1;
474 overflow: visible;
@@ -810,3 +811,93 @@
811 display: block;
812 margin-top: var(--spacing-xs);
813 }
814 +
815 +/* Nested Process Steps (Subordinate Agents) */
816 +
817 +.process-nested-container {
818 + display: grid;
819 + grid-template-rows: 0fr;
820 + opacity: 0;
821 + overflow: hidden;
822 + transition: grid-template-rows 0.25s ease-out, opacity 0.2s ease-out;
823 + gap: 2px;
824 +}
825 +
826 +/* Only show nested container when parent step is expanded */
827 +.process-step.step-expanded > .process-nested-container {
828 + grid-template-rows: 1fr;
829 + opacity: 1;
830 + margin-top: 2px;
831 + /* Keep overflow hidden to allow nested steps to control their own expansion */
832 +}
833 +
834 +/* Inner wrapper to handle grid transition content */
835 +.process-nested-inner {
836 + min-height: 0;
837 + display: flex;
838 + flex-direction: column;
839 + gap: 2px;
840 +}
841 +
842 +/* All nested containers have same indentation (no cascading) */
843 +.nested-step .process-nested-container {
844 + margin-left: 0;
845 +}
846 +
847 +/* Response content in process steps */
848 +.process-step-detail-content .step-response-content {
849 + font-size: 0.75rem;
850 + line-height: 1.6;
851 + color: var(--color-text);
852 + opacity: 0.9;
853 + margin: var(--spacing-xs) 0;
854 +}
855 +
856 +.process-step-detail-content .step-response-content p {
857 + margin: 0.5em 0;
858 +}
859 +
860 +.process-step-detail-content .step-response-content ul,
861 +.process-step-detail-content .step-response-content ol {
862 + margin: 0.5em 0;
863 + padding-left: 1.5em;
864 +}
865 +
866 +/* Warning/error content in process steps */
867 +.process-step-detail-content .step-warning-content {
868 + font-size: 0.72rem;
869 + line-height: 1.5;
870 + color: var(--color-warning);
871 + opacity: 0.9;
872 + margin: var(--spacing-xs) 0;
873 + padding: var(--spacing-xs);
874 + background: rgba(255, 165, 0, 0.08);
875 + border-left: 2px solid var(--color-warning);
876 + border-radius: 3px;
877 +}
878 +
879 +/* Browser screenshot content in process steps */
880 +.process-step-detail-content .screenshot-img {
881 + max-width: 100%;
882 + max-height: 400px;
883 + border-radius: 4px;
884 + border: 1px solid rgba(255, 255, 255, 0.1);
885 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
886 + transition: transform 0.2s ease, box-shadow 0.2s ease;
887 + object-fit: contain;
888 +}
889 +
890 +.process-step-detail-content .screenshot-img:hover {
891 + transform: scale(1.02);
892 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
893 +}
894 +
895 +/* Light mode screenshot border */
896 +.light-mode .process-step-detail-content .screenshot-img {
897 + border: 1px solid rgba(0, 0, 0, 0.15);
898 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
899 +}
900 +
901 +.light-mode .process-step-detail-content .screenshot-img:hover {
902 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
903 +}
webui/js/messages.js
+124 -18
@@ -12,6 +12,7 @@ const chatHistory = document.getElementById("chat-history");
12
13 let messageGroup = null;
14 let currentProcessGroup = null; // Track current process group for collapsible UI
15 +let currentDelegationSteps = {}; // Track delegation steps by agent number for nesting
16
17 /**
18 * Resolve tool name from kvps, existing attribute, or previous siblings
@@ -66,13 +67,14 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
67 // For user messages, close current process group FIRST (start fresh for next interaction)
68 if (type === "user") {
69 currentProcessGroup = null;
70 + currentDelegationSteps = {}; // Clear delegation tracking
71 }
72
73 // For process types, check if we should add to process group
74 if (isProcessType) {
75 if (processStepElement) {
76 // Update existing process step
75 - updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs);
77 + updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs, agentNumber);
78 return processStepElement;
79 }
80
@@ -83,15 +85,16 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
85 }
86
87 // Add step to current process group
86 - processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs);
88 + processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs, agentNumber);
89 return processStepElement;
90 }
91
92 // For subordinate agent responses (A1, A2, ...), treat as a process step instead of main response
93 // agentNumber: 0 = main agent, 1+ = subordinate agents
94 + // Note: subordinate "response" is a completion marker with content
95 if (type === "response" && agentNumber !== 0) {
96 if (processStepElement) {
94 - updateProcessStep(processStepElement, id, "agent", heading, content, kvps, durationMs);
97 + updateProcessStep(processStepElement, id, "response", heading, content, kvps, durationMs, agentNumber);
98 return processStepElement;
99 }
100
@@ -101,8 +104,8 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
104 chatHistory.appendChild(currentProcessGroup);
105 }
106
104 - // Add subordinate response as a step (type "agent" for appropriate styling)
105 - processStepElement = addProcessStep(currentProcessGroup, id, "agent", heading, content, kvps, timestamp, durationMs);
107 + // Add subordinate response as a response step (special type to show content)
108 + processStepElement = addProcessStep(currentProcessGroup, id, "response", heading, content, kvps, timestamp, durationMs, agentNumber);
109 return processStepElement;
110 }
111
@@ -1202,12 +1205,37 @@ function createProcessGroup(id) {
1205 return group;
1206 }
1207
1208 +/**
1209 + * Create or get nested container within a parent step
1210 + */
1211 +function getNestedContainer(parentStep) {
1212 + let nestedContainer = parentStep.querySelector(".process-nested-container");
1213 +
1214 + if (!nestedContainer) {
1215 + // Create new container
1216 + nestedContainer = document.createElement("div");
1217 + nestedContainer.classList.add("process-nested-container");
1218 +
1219 + // Create inner wrapper for animation support
1220 + const innerWrapper = document.createElement("div");
1221 + innerWrapper.classList.add("process-nested-inner");
1222 + nestedContainer.appendChild(innerWrapper);
1223 +
1224 + parentStep.appendChild(nestedContainer);
1225 + parentStep.classList.add("has-nested-steps");
1226 + }
1227 +
1228 + // Return the inner wrapper for appending steps
1229 + const innerWrapper = nestedContainer.querySelector(".process-nested-inner");
1230 + return innerWrapper || nestedContainer; // Fallback to container if wrapper missing
1231 +}
1232 +
1233 /**
1234 * Add a step to a process group
1235 */
1208 -function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null) {
1236 +function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null, agentNumber = 0) {
1237 const groupId = group.getAttribute("data-group-id");
1210 - const stepsContainer = group.querySelector(".process-steps");
1238 + let stepsContainer = group.querySelector(".process-steps");
1239 const isGroupCompleted = group.classList.contains("process-group-completed");
1240
1241 // Create step element
@@ -1216,6 +1244,7 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1244 step.classList.add("process-step");
1245 step.setAttribute("data-type", type);
1246 step.setAttribute("data-step-id", id);
1247 + step.setAttribute("data-agent-number", agentNumber);
1248
1249 // Resolve tool name (direct, inherited, or null)
1250 // For new steps, pass null as stepElement - inheritance uses stepsContainer query
@@ -1263,9 +1292,9 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1292 const title = getStepTitle(heading, kvps, type);
1293
1294 // Check if step should be expanded
1266 - // Agent (GEN) steps expand based on showThoughts preference
1295 + // Warning/error steps auto-expand to show content
1296 const isStepExpanded = processGroupStore.isStepExpanded(groupId, id) ||
1268 - (type === "agent" && preferencesStore.showThoughts);
1297 + (type === "warning" || type === "error");
1298 if (isStepExpanded) {
1299 step.classList.add("step-expanded");
1300 }
@@ -1292,7 +1321,13 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1321 stepHeader.addEventListener("click", (e) => {
1322 e.stopPropagation();
1323 processGroupStore.toggleStep(groupId, id);
1295 - step.classList.toggle("step-expanded", processGroupStore.isStepExpanded(groupId, id));
1324 + const newState = processGroupStore.isStepExpanded(groupId, id);
1325 + // Explicitly add or remove the class based on state
1326 + if (newState) {
1327 + step.classList.add("step-expanded");
1328 + } else {
1329 + step.classList.remove("step-expanded");
1330 + }
1331 });
1332
1333 step.appendChild(stepHeader);
@@ -1310,11 +1345,26 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1345 detail.appendChild(detailContent);
1346 step.appendChild(detail);
1347
1348 + // Track delegation steps for nesting
1349 + if (toolNameToUse === "call_subordinate") {
1350 + currentDelegationSteps[agentNumber] = step;
1351 + }
1352 +
1353 + // Determine where to append the step (main list or nested in parent)
1354 + let appendTarget = stepsContainer;
1355 +
1356 + // Check if this step belongs to a subordinate agent
1357 + if (agentNumber > 0 && currentDelegationSteps[agentNumber - 1]) {
1358 + const parentStep = currentDelegationSteps[agentNumber - 1];
1359 + appendTarget = getNestedContainer(parentStep);
1360 + step.classList.add("nested-step");
1361 + }
1362 +
1363 // Remove status-active from all previous steps (only the current step is active)
1364 const prevSteps = stepsContainer.querySelectorAll(".process-step .status-badge.status-active");
1365 prevSteps.forEach(badge => badge.classList.remove("status-active"));
1366
1317 - stepsContainer.appendChild(step);
1367 + appendTarget.appendChild(step);
1368
1369 // Update group header
1370 updateProcessGroupHeader(group);
@@ -1325,7 +1375,7 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1375 /**
1376 * Update an existing process step
1377 */
1328 -function updateProcessStep(stepElement, id, type, heading, content, kvps, durationMs = null) {
1378 +function updateProcessStep(stepElement, id, type, heading, content, kvps, durationMs = null, agentNumber = 0) {
1379 // Update title
1380 const titleEl = stepElement.querySelector(".step-title");
1381 if (titleEl) {
@@ -1338,6 +1388,11 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
1388 stepElement.setAttribute("data-duration-ms", durationMs);
1389 }
1390
1391 + // Update agent number if provided
1392 + if (agentNumber !== undefined) {
1393 + stepElement.setAttribute("data-agent-number", agentNumber);
1394 + }
1395 +
1396 // Resolve and update tool name + badge
1397 const toolNameToUse = resolveToolName(type, kvps, stepElement);
1398 if (toolNameToUse) {
@@ -1368,6 +1423,12 @@ function getStepTitle(heading, kvps, type) {
1423 return cleanStepTitle(heading, 80);
1424 }
1425
1426 + // For warnings/errors without heading, use content preview as title
1427 + if ((type === "warning" || type === "error")) {
1428 + // We'll show full content in detail, so just use type as title
1429 + return type === "warning" ? "Warning" : "Error";
1430 + }
1431 +
1432 if (kvps) {
1433 // Try common fields for title
1434 if (kvps.tool_name) {
@@ -1422,6 +1483,33 @@ function cleanStepTitle(text, maxLength) {
1483 function renderStepDetailContent(container, content, kvps, type = null) {
1484 container.innerHTML = "";
1485
1486 + // Special handling for response type - show content as markdown (for subordinate responses)
1487 + if (type === "response" && content && content.trim()) {
1488 + const responseDiv = document.createElement("div");
1489 + responseDiv.classList.add("step-response-content");
1490 +
1491 + // Parse markdown
1492 + let processedContent = content;
1493 + processedContent = convertImageTags(processedContent);
1494 + processedContent = convertImgFilePaths(processedContent);
1495 + processedContent = marked.parse(processedContent, { breaks: true });
1496 + processedContent = convertPathsToLinks(processedContent);
1497 + processedContent = addBlankTargetsToLinks(processedContent);
1498 +
1499 + responseDiv.innerHTML = processedContent;
1500 + container.appendChild(responseDiv);
1501 + return;
1502 + }
1503 +
1504 + // Special handling for warning/error types - always show content prominently
1505 + if ((type === "warning" || type === "error") && content && content.trim()) {
1506 + const warningDiv = document.createElement("div");
1507 + warningDiv.classList.add("step-warning-content");
1508 + warningDiv.textContent = content;
1509 + container.appendChild(warningDiv);
1510 + // Don't return - also show kvps if present
1511 + }
1512 +
1513 // Special handling for code_exe type - render as terminal-style output
1514 if (type === "code_exe" && kvps) {
1515 const runtime = kvps.runtime || kvps.Runtime || "bash";
@@ -1565,7 +1653,8 @@ function renderStepDetailContent(container, content, kvps, type = null) {
1653 'progress': 'pending',
1654 'document': 'description',
1655 'documents': 'folder_open',
1568 - 'queries': 'search'
1656 + 'queries': 'search',
1657 + 'screenshot': 'image'
1658 };
1659
1660 // lowerKey already defined above
@@ -1575,12 +1664,29 @@ function renderStepDetailContent(container, content, kvps, type = null) {
1664 keySpan.textContent = convertToTitleCase(key) + ":";
1665 }
1666
1578 - const valueSpan = document.createElement("span");
1667 + const valueSpan = document.createElement("div");
1668 valueSpan.classList.add("step-kvp-value");
1669
1581 - const valueText = cleanTextValue(value);
1582 -
1583 - valueSpan.textContent = truncateText(valueText, 1000);
1670 + if (typeof value === "string" && value.startsWith("img://")) {
1671 + const imgElement = document.createElement("img");
1672 + imgElement.classList.add("screenshot-img");
1673 + imgElement.src = value.replace("img://", "/image_get?path=");
1674 + imgElement.alt = "Image Attachment";
1675 + imgElement.style.cursor = "pointer";
1676 + imgElement.style.maxWidth = "100%";
1677 + imgElement.style.display = "block";
1678 + imgElement.style.marginTop = "4px";
1679 +
1680 + // Add click handler and cursor change
1681 + imgElement.addEventListener("click", () => {
1682 + imageViewerStore.open(imgElement.src, { name: "Image Attachment" });
1683 + });
1684 +
1685 + valueSpan.appendChild(imgElement);
1686 + } else {
1687 + const valueText = cleanTextValue(value);
1688 + valueSpan.textContent = truncateText(valueText, 1000);
1689 + }
1690
1691 kvpDiv.appendChild(keySpan);
1692 kvpDiv.appendChild(valueSpan);
@@ -1795,6 +1901,7 @@ function markProcessGroupComplete(group, responseTitle) {
1901 */
1902 export function resetProcessGroups() {
1903 currentProcessGroup = null;
1904 + currentDelegationSteps = {};
1905 messageGroup = null;
1906 }
1907
@@ -1811,4 +1918,3 @@ function formatDateTime(timestamp) {
1918 const seconds = String(date.getSeconds()).padStart(2, "0");
1919 return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
1920 }
1814 -