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
-