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 }