detail modes for process groups/steps
3clyp50 committed
Jan 20, 2026 at 14:36 UTC
54248d493761cbb2569e96710e7e985f0c045239
5 files changed
+190
-32
webui/components/messages/process-group/process-group-store.js
+55
@@ -175,6 +175,61 @@ const model = {
175
}
176
}
177
this._persist();
178
+ },
179
+
180
+ // Get current detail mode from preferences
181
+ _getDetailMode() {
182
+ return window.Alpine?.store("preferences")?.detailMode || "current";
183
+ },
184
+
185
+ expandGroup(groupId, isActiveAndGenerating = false) {
186
+ const mode = this._getDetailMode();
187
+ if (mode === "collapsed") {
188
+ // Only expand if generating, not for completed groups
189
+ return isActiveAndGenerating;
190
+ }
191
+ if (mode === "current" || mode === "expanded") return true;
192
+ return !this.defaultCollapsed;
193
+ },
194
+
195
+ expandStep(groupId, stepId, isActive = false) {
196
+ const mode = this._getDetailMode();
197
+ if (mode === "collapsed") return false;
198
+ if (mode === "expanded") return true;
199
+ if (mode === "current") return isActive;
200
+ return this.isStepExpanded(groupId, stepId);
201
+ },
202
+
203
+ // Apply current mode to all existing DOM elements
204
+ applyModeSteps() {
205
+ const mode = this._getDetailMode();
206
+ const showUtils = window.Alpine?.store("preferences")?.showUtils || false;
207
+ const allGroups = document.querySelectorAll(".process-group");
208
+
209
+ // Find the last VISIBLE step using targeted selector
210
+ const stepSelector = showUtils ? ".process-step" : ".process-step:not(.message-util)";
211
+ const visibleSteps = document.querySelectorAll(stepSelector);
212
+ const lastStep = visibleSteps.length > 0 ? visibleSteps[visibleSteps.length - 1] : null;
213
+
214
+ // Get all steps for applying expansion
215
+ const allSteps = document.querySelectorAll(".process-step");
216
+
217
+ // Apply to groups
218
+ allGroups.forEach(group => {
219
+ group.classList.toggle("expanded", mode !== "collapsed");
220
+ });
221
+
222
+ // Apply to steps
223
+ allSteps.forEach(step => {
224
+ let shouldExpand = false;
225
+ if (mode === "expanded") {
226
+ shouldExpand = true;
227
+ } else if (mode === "current") {
228
+ // Expand the last step and any parent steps containing it (for nested subordinate steps)
229
+ shouldExpand = step === lastStep || step.contains(lastStep);
230
+ }
231
+ step.classList.toggle("step-expanded", shouldExpand);
232
+ });
233
}
234
};
235
webui/components/sidebar/bottom/preferences/preferences-panel.html
+73
-14
@@ -27,20 +27,6 @@
27
<span class="slider"></span>
28
</label>
29
</li>
30
- <li x-data>
31
- <span class="switch-label">Speech</span>
32
- <label class="switch">
33
- <input type="checkbox" x-model="$store.preferences.speech">
34
- <span class="slider"></span>
35
- </label>
36
- </li>
37
- <li x-data>
38
- <span>Show utility messages</span>
39
- <label class="switch">
40
- <input type="checkbox" x-model="$store.preferences.showUtils">
41
- <span class="slider"></span>
42
- </label>
43
- </li>
30
<li x-data class="chat-width-setting">
31
<span class="width-label">Width</span>
32
<div class="width-buttons">
@@ -56,6 +42,37 @@
42
</template>
43
</div>
44
</li>
45
+ <li x-data class="detail-mode-setting">
46
+ <span class="detail-label">Detail</span>
47
+ <div class="detail-buttons">
48
+ <template x-for="(opt, idx) in $store.preferences.detailModeOptions" :key="opt.value">
49
+ <span class="detail-btn-wrapper">
50
+ <button type="button"
51
+ class="detail-btn"
52
+ :class="{ 'active': $store.preferences.detailMode === opt.value }"
53
+ @click="$store.preferences.detailMode = opt.value"
54
+ :title="opt.title">
55
+ <span class="material-symbols-outlined" x-text="opt.icon"></span>
56
+ </button>
57
+ <span class="detail-sep" x-show="idx < $store.preferences.detailModeOptions.length - 1">·</span>
58
+ </span>
59
+ </template>
60
+ </div>
61
+ </li>
62
+ <li x-data>
63
+ <span class="switch-label">Speech</span>
64
+ <label class="switch">
65
+ <input type="checkbox" x-model="$store.preferences.speech">
66
+ <span class="slider"></span>
67
+ </label>
68
+ </li>
69
+ <li x-data>
70
+ <span>Show utility messages</span>
71
+ <label class="switch">
72
+ <input type="checkbox" x-model="$store.preferences.showUtils">
73
+ <span class="slider"></span>
74
+ </label>
75
+ </li>
76
</ul>
77
</span>
78
</div>
@@ -144,6 +161,48 @@
161
margin: 0 0.1rem;
162
opacity: 0.5;
163
}
164
+
165
+ /* Detail mode button bar */
166
+ .detail-mode-setting {
167
+ gap: 0.25rem;
168
+ }
169
+ .detail-mode-setting .detail-label {
170
+ flex: 1;
171
+ }
172
+ .detail-buttons {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 0;
176
+ }
177
+ .detail-btn-wrapper {
178
+ display: flex;
179
+ align-items: center;
180
+ }
181
+ .detail-btn {
182
+ background: none;
183
+ border: none;
184
+ color: var(--color-primary);
185
+ padding: 0.15rem 0.25rem;
186
+ cursor: pointer;
187
+ opacity: 0.7;
188
+ transition: opacity 0.15s ease;
189
+ }
190
+ .detail-btn .material-symbols-outlined {
191
+ font-size: 1rem;
192
+ }
193
+ .detail-btn:hover {
194
+ opacity: 0.85;
195
+ }
196
+ .detail-btn.active {
197
+ opacity: 1;
198
+ color: var(--color-text);
199
+ }
200
+ .detail-sep {
201
+ color: var(--color-text-secondary, #666);
202
+ font-size: 0.6rem;
203
+ margin: 0 0.1rem;
204
+ opacity: 0.5;
205
+ }
206
</style>
207
</body>
208
</html>
webui/components/sidebar/bottom/preferences/preferences-store.js
+40
-8
@@ -1,6 +1,7 @@
1
import { createStore } from "/js/AlpineStore.js";
2
import * as css from "/js/css.js";
3
import { store as speechStore } from "/components/chat/speech/speech-store.js";
4
+import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
5
6
// Preferences store centralizes user preference toggles and side-effects
7
const model = {
@@ -68,6 +69,23 @@ const model = {
69
{ label: "FULL", value: "full" },
70
],
71
72
+ // Detail mode for process groups/steps expansion
73
+ get detailMode() {
74
+ return this._detailMode;
75
+ },
76
+ set detailMode(value) {
77
+ this._detailMode = value;
78
+ this._applyDetailMode(value);
79
+ },
80
+ _detailMode: "current", // Default: show current step only
81
+
82
+ // Detail mode options for UI sidebar
83
+ detailModeOptions: [
84
+ { label: "MIN", value: "collapsed", icon: "unfold_less", title: "All collapsed" },
85
+ { label: "CUR", value: "current", icon: "step", title: "Current step only" },
86
+ { label: "ALL", value: "expanded", icon: "unfold_more", title: "All expanded" },
87
+ ],
88
+
89
// Initialize preferences and apply current state
90
init() {
91
try {
@@ -104,6 +122,16 @@ const model = {
122
this._chatWidth = "55"; // Default to standard
123
}
124
125
+ // Load detail mode preference
126
+ try {
127
+ const storedDetailMode = localStorage.getItem("detailMode");
128
+ if (storedDetailMode && this.detailModeOptions.some(opt => opt.value === storedDetailMode)) {
129
+ this._detailMode = storedDetailMode;
130
+ }
131
+ } catch {
132
+ this._detailMode = "current"; // Default
133
+ }
134
+
135
// Apply all preferences
136
this._applyDarkMode(this._darkMode);
137
this._applyAutoScroll(this._autoScroll);
@@ -111,6 +139,7 @@ const model = {
139
this._applyShowUtils(this._showUtils);
140
this._applyCollapseProcessGroups(this._collapseProcessGroups);
141
this._applyChatWidth(this._chatWidth);
142
+ this._applyDetailMode(this._detailMode);
143
} catch (e) {
144
console.error("Failed to initialize preferences store", e);
145
}
@@ -148,19 +177,14 @@ const model = {
177
document.querySelectorAll(".process-step.message-util").forEach((el) => {
178
el.classList.toggle("show-util", value);
179
});
180
+ // Re-apply detail mode to reset current visible step
181
+ processGroupStore.applyModeSteps();
182
},
183
184
_applyCollapseProcessGroups(value) {
185
localStorage.setItem("collapseProcessGroups", value);
186
// Update process group store default
156
- try {
157
- const processGroupStore = window.Alpine?.store("processGroup");
158
- if (processGroupStore) {
159
- processGroupStore.defaultCollapsed = value;
160
- }
161
- } catch (e) {
162
- // Store may not be initialized yet
163
- }
187
+ processGroupStore.defaultCollapsed = value;
188
},
189
190
_applyChatWidth(value) {
@@ -173,6 +197,14 @@ const model = {
197
root.style.setProperty("--chat-max-width", `${value}em`);
198
}
199
},
200
+
201
+ _applyDetailMode(value) {
202
+ localStorage.setItem("detailMode", value);
203
+ // Sync defaultCollapsed based on mode
204
+ processGroupStore.defaultCollapsed = value === "collapsed";
205
+ // Apply mode to all existing DOM elements
206
+ processGroupStore.applyModeSteps();
207
+ },
208
};
209
210
export const store = createStore("preferences", model);
webui/index.js
+3
@@ -11,6 +11,7 @@ import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
11
import { store as tasksStore } from "/components/sidebar/tasks/tasks-store.js";
12
import { store as chatTopStore } from "/components/chat/top-section/chat-top-store.js";
13
import { store as _tooltipsStore } from "/components/tooltips/tooltip-store.js";
14
+import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
15
16
globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
17
@@ -414,6 +415,8 @@ function afterMessagesUpdate(logs) {
415
if (localStorage.getItem("speech") == "true") {
416
speakMessages(logs);
417
}
418
+ // Apply messages expansion mode after rendering
419
+ processGroupStore.applyModeSteps();
420
}
421
422
function speakMessages(logs) {
webui/js/messages.js
+19
-10
@@ -17,6 +17,9 @@ let activeProcessGroupId = null; // Only one process group should show "running"
17
let activeProcessGroupEl = null;
18
let activeStepTitleEl = null;
19
20
+// Expose activeProcessGroupId for store access
21
+window.activeProcessGroupId = null;
22
+
23
/**
24
* Mark current process group as active and clear active badges.
25
*/
@@ -32,6 +35,7 @@ function setActiveProcessGroup(group) {
35
36
activeProcessGroupId = group.id;
37
activeProcessGroupEl = group;
38
+ window.activeProcessGroupId = group.id; // Keep window copy in sync for store access
39
40
}
41
@@ -1154,8 +1158,8 @@ function createProcessGroup(id) {
1158
group.classList.add("process-group");
1159
group.setAttribute("data-group-id", groupId);
1160
1157
- // Check initial expansion state from store (respects user preference)
1158
- const initiallyExpanded = processGroupStore.isGroupExpanded(groupId);
1161
+ // Check initial expansion state from store
1162
+ const initiallyExpanded = processGroupStore.expandGroup(groupId, true); // true = is active
1163
if (initiallyExpanded) {
1164
group.classList.add('expanded');
1165
}
@@ -1291,9 +1295,17 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1295
const title = getStepTitle(heading, kvps, type);
1296
1297
// Check if step should be expanded
1294
- // Warning steps auto-expand to show content (errors are external MAIN_TYPES, not in process groups)
1295
- const isStepExpanded = processGroupStore.isStepExpanded(groupId, id) ||
1296
- type === "warning";
1298
+ const isActiveStep = !isGroupCompleted && group.id === activeProcessGroupId;
1299
+ const isStepExpanded = processGroupStore.expandStep(groupId, id, isActiveStep);
1300
+
1301
+ // In "current" mode, collapse all other steps
1302
+ const detailMode = preferencesStore.detailMode;
1303
+ if (detailMode === "current" && isStepExpanded) {
1304
+ document.querySelectorAll(".process-step.step-expanded").forEach(s => {
1305
+ s.classList.remove("step-expanded");
1306
+ });
1307
+ }
1308
+
1309
if (isStepExpanded) {
1310
step.classList.add("step-expanded");
1311
}
@@ -1356,11 +1368,6 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
1368
const parentStep = currentDelegationSteps[agentNumber - 1];
1369
appendTarget = getNestedContainer(parentStep);
1370
step.classList.add("nested-step");
1359
-
1360
- // Auto-expand parent if this nested step is a warning (errors are external MAIN_TYPES)
1361
- if (type === "warning") {
1362
- parentStep.classList.add("step-expanded");
1363
- }
1371
}
1372
1373
// Remove shiny effect from the previously active step title (O(1))
@@ -1959,6 +1966,7 @@ function markProcessGroupComplete(group, responseTitle) {
1966
// Add completed class to group
1967
group.classList.add("process-group-completed");
1968
1969
+
1970
// Calculate final duration from backend data (sum of all step durations)
1971
const steps = group.querySelectorAll(".process-step");
1972
let totalDurationMs = 0;
@@ -1984,6 +1992,7 @@ export function resetProcessGroups() {
1992
messageGroup = null;
1993
activeProcessGroupId = null;
1994
activeProcessGroupEl = null;
1995
+ window.activeProcessGroupId = null; // Keep window copy in sync
1996
if (activeStepTitleEl) {
1997
activeStepTitleEl.classList.remove("shiny-text");
1998
}