diff --git a/webui/components/messages/action-buttons/simple-action-buttons.css b/webui/components/messages/action-buttons/simple-action-buttons.css
index 904ef9847..ab148370e 100644
--- a/webui/components/messages/action-buttons/simple-action-buttons.css
+++ b/webui/components/messages/action-buttons/simple-action-buttons.css
@@ -101,7 +101,8 @@
/* .kvps-row:hover .action-buttons, */
.message-text:hover .action-buttons,
.kvps-val:hover .action-buttons,
-.message-body:hover > .action-buttons {
+.message-body:hover > .action-buttons,
+.error-content-inner:hover .action-buttons {
display: flex;
animation: fadeInAfterDelay 0.3s ease-in-out;
animation-delay: 0.3s;
diff --git a/webui/components/messages/process-group/process-group-store.js b/webui/components/messages/process-group/process-group-store.js
index 256c5e570..b85dd93f3 100644
--- a/webui/components/messages/process-group/process-group-store.js
+++ b/webui/components/messages/process-group/process-group-store.js
@@ -175,6 +175,68 @@ const model = {
}
}
this._persist();
+ },
+
+ // Get current detail mode from preferences
+ _getDetailMode() {
+ return window.Alpine?.store("preferences")?.detailMode || "current";
+ },
+
+ expandGroup(groupId, isActiveAndGenerating = false) {
+ const mode = this._getDetailMode();
+ if (mode === "collapsed") {
+ // Only expand if generating, not for completed groups
+ return isActiveAndGenerating;
+ }
+ if (mode === "current" || mode === "expanded") return true;
+ return !this.defaultCollapsed;
+ },
+
+ expandStep(groupId, stepId, isActive = false) {
+ const mode = this._getDetailMode();
+ if (mode === "collapsed") return false;
+ if (mode === "expanded") return true;
+ if (mode === "current") return isActive;
+ return this.isStepExpanded(groupId, stepId);
+ },
+
+ // Apply current mode to all existing DOM elements
+ applyModeSteps() {
+ const mode = this._getDetailMode();
+ const showUtils = window.Alpine?.store("preferences")?.showUtils || false;
+ const allGroups = document.querySelectorAll(".process-group");
+
+ // Find the last VISIBLE step using targeted selector
+ const stepSelector = showUtils ? ".process-step" : ".process-step:not(.message-util)";
+ const visibleSteps = document.querySelectorAll(stepSelector);
+ const lastStep = visibleSteps.length > 0 ? visibleSteps[visibleSteps.length - 1] : null;
+
+ // Get all steps for applying expansion
+ const allSteps = document.querySelectorAll(".process-step");
+
+ // Apply to groups
+ allGroups.forEach(group => {
+ group.classList.toggle("expanded", mode !== "collapsed");
+ });
+
+ // Apply to steps
+ allSteps.forEach(step => {
+ let shouldExpand = false;
+ if (mode === "expanded") {
+ shouldExpand = true;
+ } else if (mode === "current") {
+ // Expand the last step and any parent steps containing it (for nested subordinate steps)
+ shouldExpand = step === lastStep || step.contains(lastStep);
+ }
+ step.classList.toggle("step-expanded", shouldExpand);
+ });
+
+ // Apply to error groups
+ const allErrorGroups = document.querySelectorAll(".error-group");
+ allErrorGroups.forEach(errorGroup => {
+ const shouldExpand = mode === "current" || mode === "expanded";
+ errorGroup.classList.toggle("expanded", shouldExpand);
+ });
}
};
diff --git a/webui/components/messages/process-group/process-group.css b/webui/components/messages/process-group/process-group.css
index ca8e27a1a..c9e9d3f72 100644
--- a/webui/components/messages/process-group/process-group.css
+++ b/webui/components/messages/process-group/process-group.css
@@ -210,7 +210,7 @@
/* ERR - error type (red) */
.status-err {
- --step-accent: #ef4444;
+ --step-accent: var(--color-accent);
color: var(--step-accent);
}
@@ -226,33 +226,6 @@
color: var(--step-accent);
}
-/* Animated spinner for active status (CSS-only, no Material Icons) */
-.status-badge.status-active::after {
- content: "";
- display: inline-block;
- width: 8px;
- height: 8px;
- border: 1.5px solid currentColor;
- border-top-color: transparent;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- margin-left: 4px;
- flex-shrink: 0;
-}
-
-@keyframes spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
-}
-
-/* Don't show spinner pseudo-element on END/ERR (they have their own indicators) */
-.status-badge.status-end::after,
-.status-badge.status-err::after {
- display: none;
-}
-
-/* END status with checkmark icon (icon added via JS, no ::before needed) */
-
/* Completed process group styling */
.process-group-completed {
opacity: 0.95;
@@ -295,6 +268,18 @@
gap: 2px;
}
+.process-group-header .group-metrics .metric-notifications {
+ font-weight: 600;
+}
+
+.process-group-header .group-metrics .metric-notifications[hidden] {
+ display: none;
+}
+
+.process-group-header .group-metrics .metric-notifications .material-symbols-outlined {
+ opacity: 0.85;
+}
+
/* Legacy timestamp/duration (for backwards compatibility) */
.process-group-header .group-timestamp {
font-size: 0.65rem;
@@ -388,16 +373,6 @@
min-height: 18px;
}
-/* Step icon removed - using status badges instead */
-.process-step-header .step-icon {
- display: none;
-}
-
-/* Step type label removed - using status badges instead */
-.process-step-header .step-type {
- display: none;
-}
-
/* Step title */
.process-step-header .step-title {
flex: 1;
@@ -767,7 +742,7 @@
}
.light-mode .status-err {
- --step-accent: #b91c1c;
+ --step-accent: var(--color-accent);
color: var(--step-accent);
}
@@ -787,9 +762,8 @@
50% { opacity: 0.8; }
}
-.process-step.loading .step-icon {
- animation: pulse-step 1.2s ease-in-out infinite;
-}
+.step-title.shiny-text { color: transparent !important; -webkit-background-clip: text; background-clip: text; animation: shine 1s linear infinite; }
+ @keyframes shine { to { background-position: -100% center; } }
/* Responsive adjustments */
@media (max-width: 768px) {
@@ -797,10 +771,6 @@
padding: var(--spacing-xs) var(--spacing-sm);
}
- .process-step-header .step-type {
- display: none;
- }
-
.process-step-header .step-title {
font-size: 0.7rem;
}
@@ -923,3 +893,17 @@
.light-mode .process-step-detail-content .screenshot-img:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
+
+/* View Details button in step detail */
+.step-detail-actions {
+ display: flex;
+ padding-left: 18px;
+ min-height: 0; /* Required for grid collapse animation */
+}
+
+/* Hide View Details button when step is collapsed */
+.process-step:not(.step-expanded) > .process-step-detail > .step-detail-actions {
+ display: none;
+}
+
+
diff --git a/webui/components/modals/process-step-detail/process-step-detail.html b/webui/components/modals/process-step-detail/process-step-detail.html
new file mode 100644
index 000000000..0667646b1
--- /dev/null
+++ b/webui/components/modals/process-step-detail/process-step-detail.html
@@ -0,0 +1,603 @@
+
+
+
+ Step Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
psychology Reasoning
+
+
+
+
+
+
+
+
+
+
+
+ build
+ Tool Call
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
terminal Command
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
screenshot_monitor Screenshot
+
![Browser Screenshot]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webui/components/modals/process-step-detail/step-detail-store.js b/webui/components/modals/process-step-detail/step-detail-store.js
new file mode 100644
index 000000000..306bedcc6
--- /dev/null
+++ b/webui/components/modals/process-step-detail/step-detail-store.js
@@ -0,0 +1,197 @@
+import { createStore } from "/js/AlpineStore.js";
+
+// Step Detail Store - manages the step detail modal
+
+const model = {
+ // Selected step for detail modal view
+ selectedStepForDetail: null,
+
+ // ACE editor instance for raw JSON view
+ _rawEditor: null,
+
+ // Show step detail modal
+ showStepDetail(stepData) {
+ if (!stepData) return;
+ this.selectedStepForDetail = stepData;
+ window.openModal("modals/process-step-detail/process-step-detail.html");
+ },
+
+ // Close step detail modal
+ closeStepDetail() {
+ this.selectedStepForDetail = null;
+ window.closeModal();
+ },
+
+ // Copy text to clipboard
+ async copyToClipboard(text) {
+ if (!text) return false;
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch (err) {
+ console.error("Failed to copy to clipboard:", err);
+ return false;
+ }
+ },
+
+ // Format step data for full copy (all metadata + content)
+ formatStepForCopy(step) {
+ if (!step) return "";
+ const lines = [];
+ lines.push(`Type: ${step.type || "unknown"}`);
+ if (step.heading) lines.push(`Heading: ${step.heading}`);
+ if (step.timestamp) {
+ const date = new Date(parseFloat(step.timestamp) * 1000);
+ lines.push(`Timestamp: ${date.toISOString()}`);
+ }
+ if (step.durationMs) lines.push(`Duration: ${step.durationMs}ms`);
+ if (step.kvps) {
+ lines.push("");
+ lines.push("--- Data ---");
+ for (const [key, value] of Object.entries(step.kvps)) {
+ if (key === "reasoning") continue;
+ const formattedValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
+ lines.push(`${key}: ${formattedValue}`);
+ }
+ }
+ if (step.content) {
+ lines.push("");
+ lines.push("--- Content ---");
+ lines.push(step.content);
+ }
+ return lines.join("\n");
+ },
+
+ // Get primary content for a step (type-aware)
+ getStepPrimaryContent(step) {
+ if (!step) return "";
+ if (step.type === "code_exe") {
+ return step.content || "";
+ }
+ if (step.type === "agent" && step.kvps) {
+ return step.kvps.thoughts || step.kvps.headline || step.content || "";
+ }
+ if ((step.type === "tool" || step.type === "mcp") && step.kvps) {
+ return step.kvps.result || step.content || "";
+ }
+ return step.content || "";
+ },
+
+ // Initialize ACE editor for raw JSON view
+ initRawEditor() {
+ const container = document.getElementById("step-detail-raw-editor");
+ if (!container) return;
+
+ this.destroyRawEditor();
+
+ if (!window.ace?.edit) {
+ console.warn("ACE editor not available");
+ return;
+ }
+
+ const stepData = this.selectedStepForDetail;
+ if (!stepData) return;
+
+ const editorInstance = window.ace.edit("step-detail-raw-editor");
+ if (!editorInstance) return;
+
+ this._rawEditor = editorInstance;
+
+ const darkMode = window.localStorage?.getItem("darkMode");
+ const theme = darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow";
+
+ this._rawEditor.setTheme(theme);
+ this._rawEditor.session.setMode("ace/mode/json");
+ this._rawEditor.setValue(JSON.stringify(stepData, null, 2), -1);
+ this._rawEditor.setReadOnly(true);
+ this._rawEditor.clearSelection();
+ this._rawEditor.setOptions({
+ showPrintMargin: false,
+ highlightActiveLine: false,
+ highlightGutterLine: false
+ });
+ },
+
+ // Destroy ACE editor instance
+ destroyRawEditor() {
+ if (this._rawEditor?.destroy) {
+ this._rawEditor.destroy();
+ this._rawEditor = null;
+ }
+ },
+
+ // Format step type for display
+ formatStepType(type) {
+ const typeMap = {
+ 'agent': 'Generation',
+ 'code_exe': 'Code Execution',
+ 'tool': 'Tool Call',
+ 'mcp': 'MCP Tool',
+ 'browser': 'Browser',
+ 'response': 'Response',
+ 'info': 'Info',
+ 'hint': 'Hint',
+ 'warning': 'Warning',
+ 'error': 'Error',
+ 'util': 'Utility',
+ 'progress': 'Progress'
+ };
+ return typeMap[type] || (type ? type.charAt(0).toUpperCase() + type.slice(1) : 'Unknown');
+ },
+
+ // Format timestamp for display
+ formatTimestamp(timestamp) {
+ if (!timestamp) return '';
+ const date = new Date(parseFloat(timestamp) * 1000);
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ const seconds = String(date.getSeconds()).padStart(2, '0');
+ return `${hours}:${minutes}:${seconds}`;
+ },
+
+ // Format duration for display
+ formatDuration(ms) {
+ if (!ms) return '';
+ if (ms < 1000) return `${ms}ms`;
+ const seconds = Math.floor(ms / 1000);
+ if (seconds < 60) return `${seconds}s`;
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ return `${minutes}m ${remainingSeconds}s`;
+ },
+
+ // Format value for display (handles objects)
+ formatValue(value) {
+ if (value === null || value === undefined) return '';
+ if (typeof value === 'object') return JSON.stringify(value, null, 2);
+ return String(value);
+ },
+
+ // Format key for display (title case)
+ formatKey(key) {
+ return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ },
+
+ // Clean text value (handle arrays, remove brackets)
+ cleanTextValue(value) {
+ if (Array.isArray(value)) {
+ return value
+ .filter(item => item && String(item).trim() && !/^[\[\]]$/.test(String(item).trim()))
+ .join('\n');
+ }
+ if (typeof value === 'object' && value !== null) {
+ return JSON.stringify(value, null, 2);
+ }
+ return String(value).replace(/^\s*[\[\]]\s*$/gm, '').trim();
+ },
+
+ // Clean heading by removing icon:// prefixes
+ cleanHeading(text) {
+ if (!text) return "";
+ return String(text)
+ .replace(/icon:\/\/[a-zA-Z0-9_]+\s*/g, "")
+ .trim();
+ }
+};
+
+export const store = createStore("stepDetail", model);
diff --git a/webui/components/sidebar/bottom/preferences/preferences-panel.html b/webui/components/sidebar/bottom/preferences/preferences-panel.html
index f54626c28..92c0e4867 100644
--- a/webui/components/sidebar/bottom/preferences/preferences-panel.html
+++ b/webui/components/sidebar/bottom/preferences/preferences-panel.html
@@ -27,20 +27,6 @@
-
- Speech
-
-
-
- Show utility messages
-
-
Width
@@ -56,6 +42,37 @@
+
+ Detail
+
+
+
+
+ ยท
+
+
+
+
+
+ Speech
+
+
+
+ Show utility messages
+
+
@@ -144,6 +161,48 @@
margin: 0 0.1rem;
opacity: 0.5;
}
+
+ /* Detail mode button bar */
+ .detail-mode-setting {
+ gap: 0.25rem;
+ }
+ .detail-mode-setting .detail-label {
+ flex: 1;
+ }
+ .detail-buttons {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ }
+ .detail-btn-wrapper {
+ display: flex;
+ align-items: center;
+ }
+ .detail-btn {
+ background: none;
+ border: none;
+ color: var(--color-primary);
+ padding: 0.15rem 0.25rem;
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.15s ease;
+ }
+ .detail-btn .material-symbols-outlined {
+ font-size: 1rem;
+ }
+ .detail-btn:hover {
+ opacity: 0.85;
+ }
+ .detail-btn.active {
+ opacity: 1;
+ color: var(--color-text);
+ }
+ .detail-sep {
+ color: var(--color-text-secondary, #666);
+ font-size: 0.6rem;
+ margin: 0 0.1rem;
+ opacity: 0.5;
+ }