active groups use DOM; mode bugfix
3clyp50 committed
Jan 22, 2026 at 09:54 UTC
2c8eeda6b4ca43480526c807f874217cd12f0515
2 files changed
+94
-171
webui/components/messages/process-group/process-group-store.js
+50
-115
@@ -40,84 +40,20 @@ const DISPLAY_CODES = {
40
};
41
42
const model = {
43
- // Default collapsed state for new process groups
44
- defaultCollapsed: true,
43
+ init() {},
44
46
- init() {
47
- try {
48
- // Load persisted default collapsed state only
49
- const stored = localStorage.getItem("processGroupState");
50
- if (stored) {
51
- const parsed = JSON.parse(stored);
52
- this.defaultCollapsed = parsed.defaultCollapsed ?? true;
53
- }
54
- } catch (e) {
55
- console.error("Failed to load process group state", e);
56
- }
57
- },
58
-
59
- _persist() {
60
- try {
61
- // Only persist the default collapsed preference
62
- // DOM is the source of truth for actual state
63
- localStorage.setItem("processGroupState", JSON.stringify({
64
- defaultCollapsed: this.defaultCollapsed
65
- }));
66
- } catch (e) {
67
- console.error("Failed to persist process group state", e);
68
- }
69
- },
70
-
71
- // Check if a process group is expanded
72
- isGroupExpanded(groupId) {
73
- const groupElement = document.getElementById(groupId);
74
- if (groupElement) {
75
- return groupElement.classList.contains("expanded");
76
- }
77
- return !this.defaultCollapsed;
78
- },
79
-
80
- // Toggle process group expansion
45
+ // Toggle process group expansion (called from click handler in messages.js)
46
toggleGroup(groupId) {
47
const groupElement = document.getElementById(groupId);
48
if (!groupElement) return;
84
-
85
- const currentState = groupElement.classList.contains("expanded");
86
- groupElement.classList.toggle("expanded", !currentState);
49
+ groupElement.classList.toggle("expanded");
50
},
51
89
- // Expand a specific group
90
- expandGroup(groupId) {
91
- const groupElement = document.getElementById(groupId);
92
- if (groupElement) {
93
- groupElement.classList.add("expanded");
94
- }
95
- },
96
-
97
- // Collapse a specific group
98
- collapseGroup(groupId) {
99
- const groupElement = document.getElementById(groupId);
100
- if (groupElement) {
101
- groupElement.classList.remove("expanded");
102
- }
103
- },
104
-
105
- // Check if a step within a group is expanded
106
- isStepExpanded(groupId, stepId) {
107
- const stepElement = document.getElementById(`process-step-${stepId}`);
108
- if (stepElement) {
109
- return stepElement.classList.contains("step-expanded");
110
- }
111
- return false;
112
- },
113
-
114
- // Toggle step expansion
52
+ // Toggle step expansion (called from click handler in messages.js)
53
toggleStep(groupId, stepId) {
54
const stepElement = document.getElementById(`process-step-${stepId}`);
55
if (!stepElement) return;
118
-
119
- const currentState = stepElement.classList.contains("step-expanded");
120
- stepElement.classList.toggle("step-expanded", !currentState);
56
+ stepElement.classList.toggle("step-expanded");
57
},
58
59
// Status code (3-4 letter) for backend log types
@@ -164,30 +100,15 @@ const model = {
100
return colors[type] || 'status-gen';
101
},
102
167
- // Clear state for a specific context (when chat is reset)
168
- clearContext(contextPrefix) {
169
- },
170
-
103
// Get current detail mode from preferences
104
_getDetailMode() {
105
return preferencesStore.detailMode || "current";
106
},
107
176
- shouldExpandGroup(groupId, isActiveAndGenerating = false) {
108
+ shouldExpandGroup() {
109
const mode = this._getDetailMode();
178
- if (mode === "collapsed") {
179
- return false;
180
- }
181
- if (mode === "current" || mode === "expanded") return true;
182
- return !this.defaultCollapsed;
183
- },
184
-
185
- expandStep(groupId, stepId, isActive = false) {
186
- const mode = this._getDetailMode();
187
- if (mode === "collapsed") return false;
188
- if (mode === "expanded") return true;
189
- if (mode === "current") return isActive;
190
- return this.isStepExpanded(groupId, stepId);
110
+ // Groups expand in "current" and "expanded" modes, collapse only in "collapsed" mode
111
+ return mode !== "collapsed";
112
},
113
114
// Apply current mode to all existing DOM elements
@@ -196,15 +117,18 @@ const model = {
117
const showUtils = preferencesStore.showUtils || false;
118
const allGroups = document.querySelectorAll(".process-group");
119
199
- // Find the last visible step in an active (not completed) group only
200
- const stepSelector = showUtils
201
- ? ".process-group:not(.process-group-completed) .process-step"
202
- : ".process-group:not(.process-group-completed) .process-step:not(.message-util)";
203
- const visibleSteps = document.querySelectorAll(stepSelector);
204
- const lastActiveStep = visibleSteps.length > 0 ? visibleSteps[visibleSteps.length - 1] : null;
120
+ // Find the active group (currently streaming) - DOM is source of truth
121
+ const activeGroup = document.querySelector(".process-group.active");
122
206
- // Get all steps for applying expansion
207
- const allSteps = document.querySelectorAll(".process-step");
123
+ // Find the last visible step in the ACTIVE group only (for "current" mode)
124
+ let lastActiveStep = null;
125
+ if (activeGroup && !activeGroup.classList.contains("process-group-completed")) {
126
+ const stepSelector = showUtils
127
+ ? ".process-step"
128
+ : ".process-step:not(.message-util)";
129
+ const stepsInActiveGroup = activeGroup.querySelectorAll(stepSelector);
130
+ lastActiveStep = stepsInActiveGroup.length > 0 ? stepsInActiveGroup[stepsInActiveGroup.length - 1] : null;
131
+ }
132
133
// Apply to groups
134
allGroups.forEach(group => {
@@ -212,27 +136,38 @@ const model = {
136
group.classList.toggle("expanded", shouldExpandGroup);
137
});
138
215
- // Apply to steps
216
- allSteps.forEach(step => {
217
- let shouldExpand = false;
218
- if (mode === "expanded") {
219
- shouldExpand = true;
220
- } else if (mode === "current") {
221
- // Only expand the last step in an active group
222
- shouldExpand = step === lastActiveStep || step.contains(lastActiveStep);
223
- }
139
+ // Apply to steps - different logic for active vs non-active groups
140
+ allGroups.forEach(group => {
141
+ const isActiveGroup = group.classList.contains("active");
142
+ const steps = group.querySelectorAll(".process-step");
143
225
- // IMPORTANT: Only EXPAND steps here, never collapse them during streaming.
226
- // Collapsing is handled by the timeout mechanism in messages.js to avoid
227
- // fighting with the scheduled delays. We only force-collapse when mode is "collapsed".
228
- if (shouldExpand) {
229
- step.classList.add("step-expanded");
230
- } else if (mode === "collapsed") {
231
- // In collapsed mode, force all steps closed
232
- step.classList.remove("step-expanded");
233
- step.removeAttribute("data-user-pinned");
234
- }
235
- // In "current" mode: don't remove step-expanded - let timeout handle it
144
+ steps.forEach(step => {
145
+ let shouldExpand = false;
146
+
147
+ if (mode === "expanded") {
148
+ shouldExpand = true;
149
+ } else if (mode === "current") {
150
+ if (isActiveGroup) {
151
+ // Active group: only expand the last step
152
+ shouldExpand = step === lastActiveStep;
153
+ }
154
+ // Non-active groups: shouldExpand stays false → all steps collapsed
155
+ }
156
+ // In "collapsed" mode: shouldExpand stays false
157
+
158
+ if (shouldExpand) {
159
+ step.classList.add("step-expanded");
160
+ } else {
161
+ // For non-active groups or collapsed mode, immediately collapse
162
+ // Only defer to timeout for active group in "current" mode when there's an active step
163
+ // (lastActiveStep is null if group is completed - no deferral needed)
164
+ const shouldDefer = mode === "current" && isActiveGroup && lastActiveStep && step !== lastActiveStep;
165
+ if (!shouldDefer) {
166
+ step.classList.remove("step-expanded");
167
+ step.removeAttribute("data-user-pinned");
168
+ }
169
+ }
170
+ });
171
});
172
173
// Apply to error groups
webui/js/messages.js
+44
-56
@@ -25,38 +25,30 @@ const chatHistory = document.getElementById("chat-history");
25
let messageGroup = null;
26
let currentProcessGroup = null; // Track current process group for collapsible UI
27
let currentDelegationSteps = {}; // Track delegation steps by agent number for nesting
28
-let activeProcessGroupId = null; // Only one process group should show "running" indicators at a time
29
-let activeProcessGroupEl = null;
30
-let activeStepTitleEl = null;
31
-
32
-// Expose activeProcessGroupId for store access
33
-window.activeProcessGroupId = null;
28
29
/**
36
- * Mark current process group as active and clear active badges.
30
+ * Mark a process group as the active one (via .active class)
31
*/
32
function setActiveProcessGroup(group) {
39
- if (!group || !group.id) return;
40
- if (activeProcessGroupId === group.id) return;
41
-
42
- // Clear shiny effect from the previous active step title if we moved to a new group
43
- if (activeStepTitleEl && activeProcessGroupEl && activeProcessGroupEl !== group && activeProcessGroupEl.contains(activeStepTitleEl)) {
44
- activeStepTitleEl.classList.remove("shiny-text");
45
- activeStepTitleEl = null;
46
- }
47
-
48
- activeProcessGroupId = group.id;
49
- activeProcessGroupEl = group;
50
- window.activeProcessGroupId = group.id; // Keep window copy in sync for store access
51
-
33
+ if (!group) return;
34
+
35
+ // Already active? Nothing to do
36
+ if (group.classList.contains("active")) return;
37
+
38
+ // Clear active + shiny from all other groups
39
+ document.querySelectorAll(".process-group.active").forEach(g => {
40
+ if (g !== group) {
41
+ g.classList.remove("active");
42
+ g.querySelectorAll(".step-title.shiny-text").forEach(el => el.classList.remove("shiny-text"));
43
+ }
44
+ });
45
+
46
+ // Mark this group as active
47
+ group.classList.add("active");
48
}
49
50
export function clearActiveStepShine() {
55
- if (activeStepTitleEl) {
56
- activeStepTitleEl.classList.remove("shiny-text");
57
- activeStepTitleEl = null;
58
- }
59
- // clear any lingering shine in process steps
51
+ // Clear all shiny step titles in process steps
52
document.querySelectorAll(".process-step .step-title.shiny-text").forEach((el) => {
53
el.classList.remove("shiny-text");
54
});
@@ -1308,7 +1300,7 @@ function createProcessGroup(id) {
1300
group.setAttribute("data-group-id", groupId);
1301
1302
// Determine initial expansion state from current detail mode
1311
- const initiallyExpanded = processGroupStore.shouldExpandGroup(groupId, true); // true = is active
1303
+ const initiallyExpanded = processGroupStore.shouldExpandGroup();
1304
if (initiallyExpanded) {
1305
group.classList.add('expanded');
1306
}
@@ -1448,9 +1440,6 @@ function addStepCollapseInteractionHandlers(stepElement) {
1440
* Add a step to a process group
1441
*/
1442
function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null, agentNumber = 0) {
1451
- // group with newest step becomes the active one
1452
- setActiveProcessGroup(group);
1453
-
1443
const groupId = group.getAttribute("data-group-id");
1444
let stepsContainer = group.querySelector(".process-steps");
1445
const isGroupCompleted = group.classList.contains("process-group-completed");
@@ -1513,22 +1502,27 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1502
1503
// Determine if this new step should be expanded
1504
const detailMode = preferencesStore.detailMode;
1505
+ const isActiveGroup = group.classList.contains("active");
1506
let shouldExpand = false;
1507
1508
if (detailMode === "expanded") {
1509
shouldExpand = true;
1520
- } else if (detailMode === "current" && !isGroupCompleted) {
1521
- // In "current" mode: expand new step, delay-collapse all previous steps
1522
- shouldExpand = true;
1523
-
1524
- // Schedule collapse for ALL previously expanded steps
1525
- const allExpandedSteps = stepsContainer.querySelectorAll(".process-step.step-expanded");
1526
- allExpandedSteps.forEach(expandedStep => {
1527
- // Don't schedule collapse for the newly added step (the current one)
1528
- if (expandedStep.id !== `process-step-${id}`) {
1529
- scheduleStepCollapse(expandedStep, STEP_COLLAPSE_DELAY_MS);
1530
- }
1531
- });
1510
+ } else if (detailMode === "current") {
1511
+ // Only expand and schedule timeouts for the ACTIVE group (currently streaming)
1512
+ // For non-active groups (historical data), render steps collapsed immediately
1513
+ if (isActiveGroup && !isGroupCompleted) {
1514
+ shouldExpand = true;
1515
+
1516
+ // Schedule collapse for ALL previously expanded steps
1517
+ const allExpandedSteps = stepsContainer.querySelectorAll(".process-step.step-expanded");
1518
+ allExpandedSteps.forEach(expandedStep => {
1519
+ // Don't schedule collapse for the newly added step (the current one)
1520
+ if (expandedStep.id !== `process-step-${id}`) {
1521
+ scheduleStepCollapse(expandedStep, STEP_COLLAPSE_DELAY_MS);
1522
+ }
1523
+ });
1524
+ }
1525
+ // Non-active groups: shouldExpand stays false → steps render collapsed
1526
}
1527
// In "collapsed" mode: shouldExpand stays false
1528
@@ -1619,11 +1613,10 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1613
step.classList.add("nested-step");
1614
}
1615
1622
- // Remove shiny effect from the previously active step title (O(1))
1623
- if (activeStepTitleEl) {
1624
- activeStepTitleEl.classList.remove("shiny-text");
1625
- activeStepTitleEl = null;
1626
- }
1616
+ // Clear shiny effect from all previous steps in this group
1617
+ group.querySelectorAll(".process-step .step-title.shiny-text").forEach(el => {
1618
+ el.classList.remove("shiny-text");
1619
+ });
1620
1621
appendTarget.appendChild(step);
1622
@@ -1639,12 +1632,11 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1632
// Update group header
1633
updateProcessGroupHeader(group);
1634
1642
- // Apply shiny effect to the active step title
1643
- if (!isGroupCompleted && group.id === activeProcessGroupId) {
1635
+ // Apply shiny effect to the new step's title if group is still active
1636
+ if (!isGroupCompleted) {
1637
const titleEl = step.querySelector(".process-step-header .step-title");
1638
if (titleEl) {
1639
titleEl.classList.add("shiny-text");
1647
- activeStepTitleEl = titleEl;
1640
}
1641
}
1642
@@ -2301,13 +2293,9 @@ export function resetProcessGroups() {
2293
currentProcessGroup = null;
2294
currentDelegationSteps = {};
2295
messageGroup = null;
2304
- activeProcessGroupId = null;
2305
- activeProcessGroupEl = null;
2306
- window.activeProcessGroupId = null; // Keep window copy in sync
2307
- if (activeStepTitleEl) {
2308
- activeStepTitleEl.classList.remove("shiny-text");
2309
- }
2310
- activeStepTitleEl = null;
2296
+
2297
+ // Clear shiny effect from DOM (source of truth)
2298
+ clearActiveStepShine();
2299
2300
// Clear all pending collapse timeouts
2301
stepCollapseTimeouts.forEach(timeoutId => clearTimeout(timeoutId));