mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-07 17:22:09 +00:00
Merge branch 'pr/907' into development
This commit is contained in:
commit
e81f24ec99
11 changed files with 1537 additions and 263 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,603 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>Step Details</title>
|
||||
<script type="module">
|
||||
import { store } from "/components/modals/process-step-detail/step-detail-store.js";
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div x-data="{ showRaw: false }"
|
||||
x-effect="if (showRaw) { $nextTick(() => $store.stepDetail.initRawEditor()) } else { $store.stepDetail.destroyRawEditor() }">
|
||||
<template x-if="$store.stepDetail && $store.stepDetail.selectedStepForDetail">
|
||||
<div class="step-detail-container">
|
||||
<!-- Compact Modal Header -->
|
||||
<div class="modal-header modal-header-compact">
|
||||
<div class="header-info">
|
||||
<span class="status-badge"
|
||||
:class="$store.processGroup.getStatusColorClass($store.stepDetail.selectedStepForDetail?.type, $store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name)"
|
||||
x-text="$store.processGroup.getStepCode($store.stepDetail.selectedStepForDetail?.type, $store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name)"></span>
|
||||
<span class="step-type-label" x-text="$store.stepDetail.formatStepType($store.stepDetail.selectedStepForDetail?.type)"></span>
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name">
|
||||
<span class="tool-name-badge" x-text="$store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name"></span>
|
||||
</template>
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.timestamp">
|
||||
<span class="timestamp-text" x-text="$store.stepDetail.formatTimestamp($store.stepDetail.selectedStepForDetail?.timestamp)"></span>
|
||||
</template>
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.durationMs">
|
||||
<span class="duration-badge" x-text="$store.stepDetail.formatDuration($store.stepDetail.selectedStepForDetail?.durationMs)"></span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-action-header copy-all"
|
||||
@click="$store.stepDetail.copyToClipboard($store.stepDetail.formatStepForCopy($store.stepDetail.selectedStepForDetail))"
|
||||
title="Copy Complete Step with Metadata">
|
||||
<span class="material-symbols-outlined">copy_all</span> All
|
||||
</button>
|
||||
<button class="btn btn-action-header copy-content"
|
||||
@click="$store.stepDetail.copyToClipboard($store.stepDetail.getStepPrimaryContent($store.stepDetail.selectedStepForDetail))"
|
||||
title="Copy Primary Content Only">
|
||||
<span class="material-symbols-outlined">content_copy</span> Content
|
||||
</button>
|
||||
<button class="btn btn-action-header toggle-raw"
|
||||
@click="showRaw = !showRaw"
|
||||
:class="{ 'active': showRaw }"
|
||||
title="Toggle Raw JSON View">
|
||||
<span class="material-symbols-outlined">data_object</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
<!-- Raw JSON View with ACE Editor -->
|
||||
<div class="raw-json-section" x-show="showRaw">
|
||||
<div id="step-detail-raw-editor"></div>
|
||||
</div>
|
||||
|
||||
<!-- Formatted View -->
|
||||
<div class="formatted-content" x-show="!showRaw">
|
||||
<!-- Heading/Title Section -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.heading">
|
||||
<div class="content-block">
|
||||
<h4>Heading</h4>
|
||||
<div class="content-text" x-text="$store.stepDetail.cleanHeading($store.stepDetail.selectedStepForDetail?.heading)"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- GEN (agent) Type: Thoughts and Reasoning -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.type === 'agent'">
|
||||
<div class="type-specific-content">
|
||||
<!-- Reasoning (native model thinking) -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.reasoning">
|
||||
<div class="content-block reasoning-block">
|
||||
<h4><span class="material-symbols-outlined">psychology</span> Reasoning</h4>
|
||||
<div class="content-text reasoning-content" x-text="$store.stepDetail.cleanTextValue($store.stepDetail.selectedStepForDetail?.kvps?.reasoning)"></div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Thoughts -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.thoughts">
|
||||
<div class="content-block thoughts-block">
|
||||
<h4><span class="material-symbols-outlined">lightbulb</span> Thoughts</h4>
|
||||
<div class="content-text thoughts-content" x-text="$store.stepDetail.cleanTextValue($store.stepDetail.selectedStepForDetail?.kvps?.thoughts)"></div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Tool Call Preview -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.tool_name || $store.stepDetail.selectedStepForDetail?.kvps?.tool_args">
|
||||
<div class="content-block tool-call-block">
|
||||
<h4>
|
||||
<span class="material-symbols-outlined">build</span>
|
||||
<span>Tool Call</span>
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name">
|
||||
<span class="tool-name-badge" x-text="$store.stepDetail.selectedStepForDetail?.toolName || $store.stepDetail.selectedStepForDetail?.kvps?.tool_name"></span>
|
||||
</template>
|
||||
</h4>
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.tool_args">
|
||||
<div class="tool-args-container">
|
||||
<template x-for="[key, value] in Object.entries($store.stepDetail.selectedStepForDetail?.kvps?.tool_args || {})" :key="key">
|
||||
<div class="tool-arg-row">
|
||||
<span class="arg-key" x-text="key"></span>
|
||||
<span class="arg-value" x-text="$store.stepDetail.formatValue(value)"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- EXE (code_exe) Type: Terminal Output -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.type === 'code_exe'">
|
||||
<div class="type-specific-content">
|
||||
<!-- Command Info -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.code">
|
||||
<div class="content-block command-block">
|
||||
<h4><span class="material-symbols-outlined">terminal</span> Command</h4>
|
||||
<div class="command-info">
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.runtime">
|
||||
<span class="runtime-badge" x-text="$store.stepDetail.selectedStepForDetail?.kvps?.runtime"></span>
|
||||
</template>
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.session != null">
|
||||
<span class="session-badge" x-text="'Session ' + $store.stepDetail.selectedStepForDetail?.kvps?.session"></span>
|
||||
</template>
|
||||
</div>
|
||||
<pre class="command-text" x-text="$store.stepDetail.selectedStepForDetail?.kvps?.code"></pre>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Terminal Output -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.content">
|
||||
<div class="content-block terminal-block">
|
||||
<h4><span class="material-symbols-outlined">output</span> Output</h4>
|
||||
<pre class="terminal-output terminal-output-full" x-text="$store.stepDetail.selectedStepForDetail?.content"></pre>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Tool/MCP Type: Arguments and Result -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.type === 'tool' || $store.stepDetail.selectedStepForDetail?.type === 'mcp'">
|
||||
<div class="type-specific-content">
|
||||
<!-- Tool Arguments -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.tool_args">
|
||||
<div class="content-block">
|
||||
<h4><span class="material-symbols-outlined">build</span> Arguments</h4>
|
||||
<div class="tool-args-grid">
|
||||
<template x-for="[key, value] in Object.entries($store.stepDetail.selectedStepForDetail?.kvps?.tool_args || {})" :key="key">
|
||||
<div class="tool-arg-item result-content">
|
||||
<span class="arg-key" x-text="key"></span>
|
||||
<span class="arg-value" x-text="$store.stepDetail.formatValue(value)"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Result -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.result || $store.stepDetail.selectedStepForDetail?.content">
|
||||
<div class="content-block">
|
||||
<h4><span class="material-symbols-outlined">output</span> Result</h4>
|
||||
<pre class="result-content" x-text="$store.stepDetail.selectedStepForDetail?.kvps?.result || $store.stepDetail.selectedStepForDetail?.content"></pre>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Browser Type: Screenshot -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.type === 'browser'">
|
||||
<div class="type-specific-content">
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps?.screenshot">
|
||||
<div class="content-block">
|
||||
<h4><span class="material-symbols-outlined">screenshot_monitor</span> Screenshot</h4>
|
||||
<img class="screenshot-full"
|
||||
:src="$store.stepDetail.selectedStepForDetail?.kvps?.screenshot?.replace('img://', '/image_get?path=')"
|
||||
alt="Browser Screenshot" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Generic KVPs for other types -->
|
||||
<template x-if="!['agent', 'code_exe', 'tool', 'mcp', 'browser'].includes($store.stepDetail.selectedStepForDetail?.type)">
|
||||
<div class="type-specific-content">
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps">
|
||||
<div class="content-block">
|
||||
<h4>Details</h4>
|
||||
<div class="kvps-display">
|
||||
<template x-for="[key, value] in Object.entries($store.stepDetail.selectedStepForDetail?.kvps || {}).filter(([k]) => k !== 'reasoning')" :key="key">
|
||||
<div class="kvp-item">
|
||||
<span class="kvp-key" x-text="$store.stepDetail.formatKey(key)"></span>
|
||||
<span class="kvp-value" x-text="$store.stepDetail.formatValue(value)"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Content -->
|
||||
<template x-if="$store.stepDetail.selectedStepForDetail?.content">
|
||||
<div class="content-block">
|
||||
<h4>Content</h4>
|
||||
<pre class="tool-arg-item" x-text="$store.stepDetail.selectedStepForDetail?.content"></pre>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.step-detail-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.modal-header-compact {
|
||||
background: linear-gradient(135deg, var(--color-panel) 0%, var(--color-background) 100%);
|
||||
position: sticky;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px 6px 0 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.step-type-label {
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tool-name-badge {
|
||||
background: var(--color-message-bg);
|
||||
color: var(--color-text);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-mono);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.timestamp-text,
|
||||
.duration-badge {
|
||||
color: var(--color-text);
|
||||
opacity: 0.7;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-action-header {
|
||||
padding: 0.4rem 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: var(--color-message-bg);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-action-header .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-action-header:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-action-header.active {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.raw-json-section {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#step-detail-raw-editor {
|
||||
width: 100%;
|
||||
height: 60vh;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
#step-detail-raw-editor .ace_scrollbar-v {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#step-detail-raw-editor::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.formatted-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
background: var(--color-panel);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.content-block h4 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.content-block h4 .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.content-block:last-child {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
color: var(--color-text);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.thoughts-block {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.thoughts-content {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.reasoning-block {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.reasoning-content {
|
||||
opacity: 0.75;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tool-call-block {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tool-name-inline {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.command-block {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.command-info {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.runtime-badge,
|
||||
.session-badge {
|
||||
background: var(--color-message-bg);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-mono);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.command-text {
|
||||
background: var(--color-background);
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.terminal-output-full {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: var(--color-text);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.light-mode .terminal-output-full {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
.tool-args-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-arg-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* GEN step tool call - single container with KV rows */
|
||||
.tool-args-container {
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.tool-arg-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.tool-arg-row .arg-key,
|
||||
.tool-arg-row .arg-value {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.arg-key {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.arg-value {
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
background: var(--color-background);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.screenshot-full {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.kvps-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.kvp-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.kvp-key {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
opacity: 0.7;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.kvp-value {
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.modal-body::-webkit-scrollbar,
|
||||
.result-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-track,
|
||||
.result-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb,
|
||||
.result-content::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb:hover,
|
||||
.result-content::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(155, 155, 155, 0.7);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.modal-header-compact {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tool-arg-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.arg-key {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
197
webui/components/modals/process-step-detail/step-detail-store.js
Normal file
197
webui/components/modals/process-step-detail/step-detail-store.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -27,20 +27,6 @@
|
|||
<span class="slider"></span>
|
||||
</label>
|
||||
</li>
|
||||
<li x-data>
|
||||
<span class="switch-label">Speech</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" x-model="$store.preferences.speech">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</li>
|
||||
<li x-data>
|
||||
<span>Show utility messages</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" x-model="$store.preferences.showUtils">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</li>
|
||||
<li x-data class="chat-width-setting">
|
||||
<span class="width-label">Width</span>
|
||||
<div class="width-buttons">
|
||||
|
|
@ -56,6 +42,37 @@
|
|||
</template>
|
||||
</div>
|
||||
</li>
|
||||
<li x-data class="detail-mode-setting">
|
||||
<span class="detail-label">Detail</span>
|
||||
<div class="detail-buttons">
|
||||
<template x-for="(opt, idx) in $store.preferences.detailModeOptions" :key="opt.value">
|
||||
<span class="detail-btn-wrapper">
|
||||
<button type="button"
|
||||
class="detail-btn"
|
||||
:class="{ 'active': $store.preferences.detailMode === opt.value }"
|
||||
@click="$store.preferences.detailMode = opt.value"
|
||||
:title="opt.title">
|
||||
<span class="material-symbols-outlined" x-text="opt.icon"></span>
|
||||
</button>
|
||||
<span class="detail-sep" x-show="idx < $store.preferences.detailModeOptions.length - 1">·</span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</li>
|
||||
<li x-data>
|
||||
<span class="switch-label">Speech</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" x-model="$store.preferences.speech">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</li>
|
||||
<li x-data>
|
||||
<span>Show utility messages</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" x-model="$store.preferences.showUtils">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createStore } from "/js/AlpineStore.js";
|
||||
import * as css from "/js/css.js";
|
||||
import { store as speechStore } from "/components/chat/speech/speech-store.js";
|
||||
import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
|
||||
|
||||
// Preferences store centralizes user preference toggles and side-effects
|
||||
const model = {
|
||||
|
|
@ -68,6 +69,23 @@ const model = {
|
|||
{ label: "FULL", value: "full" },
|
||||
],
|
||||
|
||||
// Detail mode for process groups/steps expansion
|
||||
get detailMode() {
|
||||
return this._detailMode;
|
||||
},
|
||||
set detailMode(value) {
|
||||
this._detailMode = value;
|
||||
this._applyDetailMode(value);
|
||||
},
|
||||
_detailMode: "current", // Default: show current step only
|
||||
|
||||
// Detail mode options for UI sidebar
|
||||
detailModeOptions: [
|
||||
{ label: "MIN", value: "collapsed", icon: "unfold_less", title: "All collapsed" },
|
||||
{ label: "CUR", value: "current", icon: "step", title: "Current step only" },
|
||||
{ label: "ALL", value: "expanded", icon: "unfold_more", title: "All expanded" },
|
||||
],
|
||||
|
||||
// Initialize preferences and apply current state
|
||||
init() {
|
||||
try {
|
||||
|
|
@ -104,6 +122,16 @@ const model = {
|
|||
this._chatWidth = "55"; // Default to standard
|
||||
}
|
||||
|
||||
// Load detail mode preference
|
||||
try {
|
||||
const storedDetailMode = localStorage.getItem("detailMode");
|
||||
if (storedDetailMode && this.detailModeOptions.some(opt => opt.value === storedDetailMode)) {
|
||||
this._detailMode = storedDetailMode;
|
||||
}
|
||||
} catch {
|
||||
this._detailMode = "current"; // Default
|
||||
}
|
||||
|
||||
// Apply all preferences
|
||||
this._applyDarkMode(this._darkMode);
|
||||
this._applyAutoScroll(this._autoScroll);
|
||||
|
|
@ -111,6 +139,7 @@ const model = {
|
|||
this._applyShowUtils(this._showUtils);
|
||||
this._applyCollapseProcessGroups(this._collapseProcessGroups);
|
||||
this._applyChatWidth(this._chatWidth);
|
||||
this._applyDetailMode(this._detailMode);
|
||||
} catch (e) {
|
||||
console.error("Failed to initialize preferences store", e);
|
||||
}
|
||||
|
|
@ -148,19 +177,14 @@ const model = {
|
|||
document.querySelectorAll(".process-step.message-util").forEach((el) => {
|
||||
el.classList.toggle("show-util", value);
|
||||
});
|
||||
// Re-apply detail mode to reset current visible step
|
||||
processGroupStore.applyModeSteps();
|
||||
},
|
||||
|
||||
_applyCollapseProcessGroups(value) {
|
||||
localStorage.setItem("collapseProcessGroups", value);
|
||||
// Update process group store default
|
||||
try {
|
||||
const processGroupStore = window.Alpine?.store("processGroup");
|
||||
if (processGroupStore) {
|
||||
processGroupStore.defaultCollapsed = value;
|
||||
}
|
||||
} catch (e) {
|
||||
// Store may not be initialized yet
|
||||
}
|
||||
processGroupStore.defaultCollapsed = value;
|
||||
},
|
||||
|
||||
_applyChatWidth(value) {
|
||||
|
|
@ -173,6 +197,14 @@ const model = {
|
|||
root.style.setProperty("--chat-max-width", `${value}em`);
|
||||
}
|
||||
},
|
||||
|
||||
_applyDetailMode(value) {
|
||||
localStorage.setItem("detailMode", value);
|
||||
// Sync defaultCollapsed based on mode
|
||||
processGroupStore.defaultCollapsed = value === "collapsed";
|
||||
// Apply mode to all existing DOM elements
|
||||
processGroupStore.applyModeSteps();
|
||||
},
|
||||
};
|
||||
|
||||
export const store = createStore("preferences", model);
|
||||
|
|
|
|||
|
|
@ -70,14 +70,6 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.center-container .message {
|
||||
/* margin-bottom: var(--spacing-sm); */
|
||||
}
|
||||
|
||||
.message .message-body {
|
||||
/* padding-top: 0.5em;
|
||||
padding-bottom: 0.5em; */
|
||||
}
|
||||
|
||||
.message-user {
|
||||
text-align: end;
|
||||
|
|
@ -111,19 +103,12 @@
|
|||
color: #2e2e2e;
|
||||
} */
|
||||
|
||||
.message-ai {
|
||||
/* border-bottom-left-radius: var(--spacing-xxs); */
|
||||
}
|
||||
|
||||
.message-center {
|
||||
align-self: center;
|
||||
/* border-bottom-left-radius: unset; */
|
||||
}
|
||||
|
||||
.message-followup {
|
||||
/* margin-left: var(--spacing-lg); */
|
||||
/* margin-bottom: var(--spacing-lg); */
|
||||
}
|
||||
|
||||
.message-followup .message {
|
||||
border-radius: 0;
|
||||
|
|
@ -287,13 +272,138 @@
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
background-color: rgba(180, 40, 40, 0.25);
|
||||
border: 1px solid rgba(220, 60, 60, 0.5);
|
||||
border-radius: 8px !important;
|
||||
padding: 12px !important;
|
||||
/* Collapsible Error Group */
|
||||
|
||||
.message-error-group {
|
||||
background: transparent;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.error-group {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: var(--spacing-xs) 0;
|
||||
padding: var(--spacing-xs) 0;
|
||||
min-width: 200px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Error Group Header */
|
||||
.error-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.15s ease;
|
||||
min-height: 22px;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error-group-header:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Expand/collapse triangle */
|
||||
.error-group-header .expand-icon {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 4px 0 4px 6px;
|
||||
border-color: transparent transparent transparent #ef4444;
|
||||
opacity: 0.7;
|
||||
transition: transform 0.2s ease, opacity 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
font-size: 0;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.error-group-header:hover .expand-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.error-group.expanded .error-group-header .expand-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Error title */
|
||||
.error-group-header .error-title {
|
||||
flex: 0 0 auto;
|
||||
font-size: var(--font-size-medium);
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* Error subtitle (short description) */
|
||||
.error-group-header .error-subtitle {
|
||||
flex: 1 1 auto;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text);
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: var(--spacing-xs);
|
||||
font-family: var(--font-family-code);
|
||||
}
|
||||
|
||||
/* Error Group Content - Animated expand/collapse */
|
||||
.error-group-content {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
transition: grid-template-rows 0.25s ease-out,
|
||||
opacity 0.2s ease-out,
|
||||
margin-top 0.25s ease-out,
|
||||
padding-top 0.25s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error-group-content > .error-content-inner {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.error-group.expanded .error-group-content {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
margin-top: var(--spacing-xs);
|
||||
padding-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.error-content-inner {
|
||||
padding: var(--spacing-md);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-accent);
|
||||
margin-left: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Callstack styling */
|
||||
.error-callstack {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-family-code);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-accent);
|
||||
opacity: 0.9;
|
||||
max-height: 30vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-accent) transparent;
|
||||
}
|
||||
|
||||
|
||||
/* Terminal styling moved to new terminal block above */
|
||||
|
||||
/* Agent and AI Info */
|
||||
|
|
@ -512,10 +622,17 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
.light-mode .message-error {
|
||||
background-color: rgba(220, 60, 60, 0.15);
|
||||
border: 1px solid rgba(180, 40, 40, 0.4);
|
||||
color: #8f1010;
|
||||
/* Light mode error group */
|
||||
.light-mode .error-group-header .expand-icon {
|
||||
border-color: transparent transparent transparent var(--color-accent);
|
||||
}
|
||||
|
||||
.light-mode .error-group-header .error-title {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.light-mode .error-content-inner {
|
||||
border: 1px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.light-mode .message-user {
|
||||
|
|
@ -651,10 +768,6 @@
|
|||
margin-bottom:5em;
|
||||
}
|
||||
|
||||
.message-group-mid {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
animation: fadeIn 0.2s;
|
||||
-webkit-animation: fadeIn 0.2s;
|
||||
|
|
|
|||
|
|
@ -747,6 +747,32 @@ h4 {
|
|||
|
||||
}
|
||||
|
||||
/* Chat-history attachments overrides (flat styling) */
|
||||
|
||||
#chat-history .attachments-container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#chat-history .attachment-item {
|
||||
padding: 0 !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
#chat-history .attachment-item:hover {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
#chat-history .attachment-preview,
|
||||
#chat-history .attachment-item.image-type .attachment-preview {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.attachment-image .attachment-preview {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
|
|||
import { store as tasksStore } from "/components/sidebar/tasks/tasks-store.js";
|
||||
import { store as chatTopStore } from "/components/chat/top-section/chat-top-store.js";
|
||||
import { store as _tooltipsStore } from "/components/tooltips/tooltip-store.js";
|
||||
import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
|
||||
|
||||
globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
|
||||
|
||||
|
|
@ -414,6 +415,8 @@ function afterMessagesUpdate(logs) {
|
|||
if (localStorage.getItem("speech") == "true") {
|
||||
speakMessages(logs);
|
||||
}
|
||||
// Apply messages expansion mode after rendering
|
||||
processGroupStore.applyModeSteps();
|
||||
}
|
||||
|
||||
function speakMessages(logs) {
|
||||
|
|
@ -458,11 +461,7 @@ function updateProgress(progress, active) {
|
|||
if (!progressBarEl) return;
|
||||
if (!progress) progress = "";
|
||||
|
||||
if (!active) {
|
||||
removeClassFromElement(progressBarEl, "shiny-text");
|
||||
} else {
|
||||
addClassToElement(progressBarEl, "shiny-text");
|
||||
}
|
||||
setProgressBarShine(progressBarEl, active);
|
||||
|
||||
progress = msgs.convertIcons(progress);
|
||||
|
||||
|
|
@ -471,6 +470,17 @@ function updateProgress(progress, active) {
|
|||
}
|
||||
}
|
||||
|
||||
function setProgressBarShine(progressBarEl, active) {
|
||||
if (!progressBarEl) return;
|
||||
if (!active) {
|
||||
removeClassFromElement(progressBarEl, "shiny-text");
|
||||
// clear any lingering shines in process steps
|
||||
msgs.clearActiveStepShine();
|
||||
} else {
|
||||
addClassToElement(progressBarEl, "shiny-text");
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.pauseAgent = async function (paused) {
|
||||
await inputStore.pauseAgent(paused);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { store as _messageResizeStore } from "/components/messages/resize/messag
|
|||
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
|
||||
import { addActionButtonsToElement } from "/components/messages/action-buttons/simple-action-buttons.js";
|
||||
import { store as processGroupStore } from "/components/messages/process-group/process-group-store.js";
|
||||
import { store as stepDetailStore } from "/components/modals/process-step-detail/step-detail-store.js";
|
||||
import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
|
||||
import { formatDuration } from "./time-utils.js";
|
||||
|
||||
|
|
@ -13,6 +14,42 @@ const chatHistory = document.getElementById("chat-history");
|
|||
let messageGroup = null;
|
||||
let currentProcessGroup = null; // Track current process group for collapsible UI
|
||||
let currentDelegationSteps = {}; // Track delegation steps by agent number for nesting
|
||||
let activeProcessGroupId = null; // Only one process group should show "running" indicators at a time
|
||||
let activeProcessGroupEl = null;
|
||||
let activeStepTitleEl = null;
|
||||
|
||||
// Expose activeProcessGroupId for store access
|
||||
window.activeProcessGroupId = null;
|
||||
|
||||
/**
|
||||
* Mark current process group as active and clear active badges.
|
||||
*/
|
||||
function setActiveProcessGroup(group) {
|
||||
if (!group || !group.id) return;
|
||||
if (activeProcessGroupId === group.id) return;
|
||||
|
||||
// Clear shiny effect from the previous active step title if we moved to a new group
|
||||
if (activeStepTitleEl && activeProcessGroupEl && activeProcessGroupEl !== group && activeProcessGroupEl.contains(activeStepTitleEl)) {
|
||||
activeStepTitleEl.classList.remove("shiny-text");
|
||||
activeStepTitleEl = null;
|
||||
}
|
||||
|
||||
activeProcessGroupId = group.id;
|
||||
activeProcessGroupEl = group;
|
||||
window.activeProcessGroupId = group.id; // Keep window copy in sync for store access
|
||||
|
||||
}
|
||||
|
||||
export function clearActiveStepShine() {
|
||||
if (activeStepTitleEl) {
|
||||
activeStepTitleEl.classList.remove("shiny-text");
|
||||
activeStepTitleEl = null;
|
||||
}
|
||||
// clear any lingering shine in process steps
|
||||
document.querySelectorAll(".process-step .step-title.shiny-text").forEach((el) => {
|
||||
el.classList.remove("shiny-text");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve tool name from kvps, existing attribute, or previous siblings
|
||||
|
|
@ -54,15 +91,35 @@ const PROCESS_TYPES = ['agent', 'tool', 'code_exe', 'browser', 'progress', 'info
|
|||
// Main types that should always be visible (not collapsed)
|
||||
const MAIN_TYPES = ['user', 'response', 'error', 'rate_limit'];
|
||||
|
||||
/**
|
||||
* Helper to append a message container to the correct group in chat history
|
||||
*/
|
||||
function appendMessageToHistory(messageContainer, groupType, forceNewGroup, id) {
|
||||
// Check if current messageGroup is still in DOM, if not, reset it (context switch)
|
||||
if (messageGroup && !document.getElementById(messageGroup.id)) {
|
||||
messageGroup = null;
|
||||
}
|
||||
|
||||
// Create new group if needed
|
||||
if (!messageGroup || forceNewGroup || groupType !== messageGroup.getAttribute("data-group-type")) {
|
||||
messageGroup = document.createElement("div");
|
||||
messageGroup.id = `message-group-${id}`;
|
||||
messageGroup.classList.add("message-group", `message-group-${groupType}`);
|
||||
messageGroup.setAttribute("data-group-type", groupType);
|
||||
chatHistory.appendChild(messageGroup);
|
||||
}
|
||||
|
||||
// Append message to group
|
||||
messageGroup.appendChild(messageContainer);
|
||||
}
|
||||
|
||||
export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null, durationMs = null, agentNumber = 0) {
|
||||
// Check if this is a process type message
|
||||
const isProcessType = PROCESS_TYPES.includes(type);
|
||||
const isMainType = MAIN_TYPES.includes(type);
|
||||
|
||||
// Search for the existing message container by id
|
||||
let messageContainer = document.getElementById(`message-${id}`);
|
||||
let processStepElement = document.getElementById(`process-step-${id}`);
|
||||
let isNewMessage = false;
|
||||
|
||||
// For user messages, close current process group FIRST (start fresh for next interaction)
|
||||
if (type === "user") {
|
||||
|
|
@ -71,7 +128,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
|
|||
}
|
||||
|
||||
// For process types, check if we should add to process group
|
||||
if (isProcessType) {
|
||||
if (isProcessType || (type === "response" && agentNumber !== 0)) {
|
||||
if (processStepElement) {
|
||||
// Update existing process step
|
||||
updateProcessStep(processStepElement, id, type, heading, content, kvps, durationMs, agentNumber);
|
||||
|
|
@ -80,59 +137,35 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
|
|||
|
||||
// Create or get process group for current interaction
|
||||
if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) {
|
||||
// Create response container for this process group immediately (Option B)
|
||||
messageContainer = document.createElement("div");
|
||||
messageContainer.id = `message-${id}`;
|
||||
messageContainer.classList.add("message-container", "ai-container", "has-process-group");
|
||||
|
||||
currentProcessGroup = createProcessGroup(id);
|
||||
chatHistory.appendChild(currentProcessGroup);
|
||||
currentProcessGroup.classList.add("embedded");
|
||||
messageContainer.appendChild(currentProcessGroup);
|
||||
|
||||
// Handle DOM insertion immediately
|
||||
appendMessageToHistory(messageContainer, "left", false, id);
|
||||
|
||||
setActiveProcessGroup(currentProcessGroup);
|
||||
}
|
||||
|
||||
// Add step to current process group
|
||||
processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp, durationMs, agentNumber);
|
||||
const stepType = (type === "response" && agentNumber !== 0) ? "response" : type;
|
||||
processStepElement = addProcessStep(currentProcessGroup, id, stepType, heading, content, kvps, timestamp, durationMs, agentNumber);
|
||||
return processStepElement;
|
||||
}
|
||||
|
||||
// For subordinate agent responses (A1, A2, ...), treat as a process step instead of main response
|
||||
// agentNumber: 0 = main agent, 1+ = subordinate agents
|
||||
// Note: subordinate "response" is a completion marker with content
|
||||
if (type === "response" && agentNumber !== 0) {
|
||||
if (processStepElement) {
|
||||
updateProcessStep(processStepElement, id, "response", heading, content, kvps, durationMs, agentNumber);
|
||||
return processStepElement;
|
||||
}
|
||||
|
||||
// Create or get process group for current interaction
|
||||
if (!currentProcessGroup || !document.getElementById(currentProcessGroup.id)) {
|
||||
currentProcessGroup = createProcessGroup(id);
|
||||
chatHistory.appendChild(currentProcessGroup);
|
||||
}
|
||||
|
||||
// Add subordinate response as a response step (special type to show content)
|
||||
processStepElement = addProcessStep(currentProcessGroup, id, "response", heading, content, kvps, timestamp, durationMs, agentNumber);
|
||||
return processStepElement;
|
||||
}
|
||||
|
||||
// For main agent (A0) response, embed the current process group and mark as complete
|
||||
// For main agent (A0) response, mark the current process group as complete
|
||||
if (type === "response" && currentProcessGroup) {
|
||||
const processGroupToEmbed = currentProcessGroup;
|
||||
// Keep currentProcessGroup reference - subsequent process messages go to same group
|
||||
|
||||
// Mark process group as complete (END state)
|
||||
markProcessGroupComplete(processGroupToEmbed, heading);
|
||||
|
||||
if (!messageContainer) {
|
||||
// Create new container with embedded process group
|
||||
messageContainer = createResponseContainerWithProcessGroup(id, processGroupToEmbed);
|
||||
isNewMessage = true;
|
||||
} else {
|
||||
// Check if already embedded
|
||||
const existingEmbedded = messageContainer.querySelector(".process-group");
|
||||
if (!existingEmbedded && processGroupToEmbed) {
|
||||
embedProcessGroup(messageContainer, processGroupToEmbed);
|
||||
}
|
||||
}
|
||||
markProcessGroupComplete(currentProcessGroup, heading);
|
||||
}
|
||||
|
||||
if (!messageContainer) {
|
||||
// Create a new container if not found
|
||||
isNewMessage = true;
|
||||
const sender = type === "user" ? "user" : "ai";
|
||||
messageContainer = document.createElement("div");
|
||||
messageContainer.id = `message-${id}`;
|
||||
|
|
@ -142,8 +175,8 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
|
|||
const handler = getHandler(type);
|
||||
handler(messageContainer, id, type, heading, content, temp, kvps);
|
||||
|
||||
// If this is a new message, handle DOM insertion
|
||||
if (!document.getElementById(`message-${id}`)) {
|
||||
// If this is a new message (not yet in DOM), handle DOM insertion
|
||||
if (!messageContainer.parentNode) {
|
||||
// message type visual grouping
|
||||
const groupTypeMap = {
|
||||
user: "right",
|
||||
|
|
@ -163,27 +196,11 @@ export function setMessage(id, type, heading, content, temp, kvps = null, timest
|
|||
};
|
||||
|
||||
const groupType = groupTypeMap[type] || "left";
|
||||
const forceNewGroup = groupStart[type] || false;
|
||||
|
||||
// here check if messageGroup is still in DOM, if not, then set it to null (context switch)
|
||||
if (messageGroup && !document.getElementById(messageGroup.id))
|
||||
messageGroup = null;
|
||||
|
||||
if (
|
||||
!messageGroup || // no group yet exists
|
||||
groupStart[type] || // message type forces new group
|
||||
groupType != messageGroup.getAttribute("data-group-type") // message type changes group
|
||||
) {
|
||||
messageGroup = document.createElement("div");
|
||||
messageGroup.id = `message-group-${id}`;
|
||||
messageGroup.classList.add(`message-group`, `message-group-${groupType}`);
|
||||
messageGroup.setAttribute("data-group-type", groupType);
|
||||
}
|
||||
messageGroup.appendChild(messageContainer);
|
||||
chatHistory.appendChild(messageGroup);
|
||||
appendMessageToHistory(messageContainer, groupType, forceNewGroup, id);
|
||||
}
|
||||
|
||||
// Simplified implementation - no setup needed
|
||||
|
||||
return messageContainer;
|
||||
}
|
||||
|
||||
|
|
@ -772,16 +789,126 @@ export function drawMessageError(
|
|||
temp,
|
||||
kvps = null
|
||||
) {
|
||||
return drawMessageAgentPlain(
|
||||
"message-error",
|
||||
messageContainer,
|
||||
id,
|
||||
type,
|
||||
heading,
|
||||
content,
|
||||
temp,
|
||||
kvps
|
||||
);
|
||||
// Create or get the message div
|
||||
let messageDiv = messageContainer.querySelector(".message");
|
||||
if (!messageDiv) {
|
||||
messageDiv = document.createElement("div");
|
||||
messageDiv.classList.add("message", "message-error-group");
|
||||
messageContainer.appendChild(messageDiv);
|
||||
}
|
||||
|
||||
// Check if error group already exists
|
||||
let errorGroup = messageDiv.querySelector(".error-group");
|
||||
if (!errorGroup) {
|
||||
errorGroup = document.createElement("div");
|
||||
errorGroup.classList.add("error-group");
|
||||
errorGroup.setAttribute("data-error-id", id);
|
||||
|
||||
// Create header (clickable for expand/collapse)
|
||||
const header = document.createElement("div");
|
||||
header.classList.add("error-group-header");
|
||||
|
||||
// Expand icon (triangle)
|
||||
const expandIcon = document.createElement("span");
|
||||
expandIcon.classList.add("expand-icon");
|
||||
header.appendChild(expandIcon);
|
||||
|
||||
// Status badge (before title)
|
||||
const badge = document.createElement("span");
|
||||
badge.classList.add("status-badge", "status-err");
|
||||
badge.textContent = "ERR";
|
||||
header.appendChild(badge);
|
||||
|
||||
// Title
|
||||
const title = document.createElement("span");
|
||||
title.classList.add("error-title");
|
||||
title.textContent = "Error";
|
||||
header.appendChild(title);
|
||||
|
||||
// Subtitle (short error description)
|
||||
const subtitle = document.createElement("span");
|
||||
subtitle.classList.add("error-subtitle");
|
||||
header.appendChild(subtitle);
|
||||
|
||||
// Click handler for expand/collapse
|
||||
header.addEventListener("click", () => {
|
||||
errorGroup.classList.toggle("expanded");
|
||||
});
|
||||
|
||||
errorGroup.appendChild(header);
|
||||
|
||||
// Create content container (collapsible)
|
||||
const contentWrapper = document.createElement("div");
|
||||
contentWrapper.classList.add("error-group-content");
|
||||
|
||||
const contentInner = document.createElement("div");
|
||||
contentInner.classList.add("error-content-inner");
|
||||
contentWrapper.appendChild(contentInner);
|
||||
|
||||
errorGroup.appendChild(contentWrapper);
|
||||
messageDiv.appendChild(errorGroup);
|
||||
|
||||
// Check detail mode and expand if needed
|
||||
const detailMode = window.Alpine?.store("preferences")?.detailMode || "current";
|
||||
if (detailMode === "current" || detailMode === "expanded") {
|
||||
errorGroup.classList.add("expanded");
|
||||
}
|
||||
}
|
||||
|
||||
// Update subtitle with short error description
|
||||
const subtitle = errorGroup.querySelector(".error-subtitle");
|
||||
if (subtitle) {
|
||||
// Extract short description from heading or content
|
||||
let shortDesc = "";
|
||||
// Skip if heading is just "Error" (redundant with title)
|
||||
if (heading && heading.trim() && heading.trim().toLowerCase() !== "error") {
|
||||
shortDesc = heading.trim();
|
||||
}
|
||||
// If no useful heading, try to extract from content
|
||||
if (!shortDesc && content && content.trim()) {
|
||||
const lines = content.trim().split("\n");
|
||||
// Look for the error line (usually last meaningful line or one matching ErrorType: pattern)
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (line && /^[\w\.]+Error[:\s]/.test(line)) {
|
||||
shortDesc = line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fallback to first non-empty line if no error pattern found
|
||||
if (!shortDesc) {
|
||||
for (const line of lines) {
|
||||
if (line.trim() && !line.startsWith("Traceback")) {
|
||||
shortDesc = line.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Truncate if too long
|
||||
if (shortDesc.length > 100) {
|
||||
shortDesc = shortDesc.substring(0, 97) + "...";
|
||||
}
|
||||
subtitle.textContent = shortDesc;
|
||||
subtitle.title = shortDesc; // Full text on hover
|
||||
}
|
||||
|
||||
// Update content (full callstack)
|
||||
const contentInner = errorGroup.querySelector(".error-content-inner");
|
||||
if (contentInner && content) {
|
||||
contentInner.innerHTML = "";
|
||||
|
||||
// Create pre element for callstack/content
|
||||
const pre = document.createElement("pre");
|
||||
pre.classList.add("error-callstack");
|
||||
pre.textContent = content;
|
||||
contentInner.appendChild(pre);
|
||||
|
||||
// Add action buttons for copy functionality
|
||||
addActionButtonsToElement(contentInner);
|
||||
}
|
||||
|
||||
messageContainer.classList.add("center-container");
|
||||
}
|
||||
|
||||
function drawKvps(container, kvps, latex) {
|
||||
|
|
@ -1128,52 +1255,6 @@ class Scroller {
|
|||
// Process Group Embedding Functions
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Create a response container with an embedded process group
|
||||
*/
|
||||
function createResponseContainerWithProcessGroup(id, processGroup) {
|
||||
const messageContainer = document.createElement("div");
|
||||
messageContainer.id = `message-${id}`;
|
||||
messageContainer.classList.add("message-container", "ai-container", "has-process-group");
|
||||
|
||||
// Move process group from chatHistory into the container
|
||||
if (processGroup && processGroup.parentNode) {
|
||||
processGroup.parentNode.removeChild(processGroup);
|
||||
}
|
||||
|
||||
// Process group will be the first child
|
||||
if (processGroup) {
|
||||
processGroup.classList.add("embedded");
|
||||
messageContainer.appendChild(processGroup);
|
||||
}
|
||||
|
||||
return messageContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed a process group into an existing message container
|
||||
*/
|
||||
function embedProcessGroup(messageContainer, processGroup) {
|
||||
if (!messageContainer || !processGroup) return;
|
||||
|
||||
// Remove from current parent
|
||||
if (processGroup.parentNode) {
|
||||
processGroup.parentNode.removeChild(processGroup);
|
||||
}
|
||||
|
||||
// Add embedded class
|
||||
processGroup.classList.add("embedded");
|
||||
messageContainer.classList.add("has-process-group");
|
||||
|
||||
// Insert at the beginning of the container
|
||||
const firstChild = messageContainer.firstChild;
|
||||
if (firstChild) {
|
||||
messageContainer.insertBefore(processGroup, firstChild);
|
||||
} else {
|
||||
messageContainer.appendChild(processGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Process Group Functions
|
||||
// ============================================
|
||||
|
|
@ -1188,8 +1269,8 @@ function createProcessGroup(id) {
|
|||
group.classList.add("process-group");
|
||||
group.setAttribute("data-group-id", groupId);
|
||||
|
||||
// Check initial expansion state from store (respects user preference)
|
||||
const initiallyExpanded = processGroupStore.isGroupExpanded(groupId);
|
||||
// Check initial expansion state from store
|
||||
const initiallyExpanded = processGroupStore.expandGroup(groupId, true); // true = is active
|
||||
if (initiallyExpanded) {
|
||||
group.classList.add('expanded');
|
||||
}
|
||||
|
|
@ -1200,10 +1281,11 @@ function createProcessGroup(id) {
|
|||
header.innerHTML = `
|
||||
<span class="expand-icon"></span>
|
||||
<span class="group-title">Processing...</span>
|
||||
<span class="status-badge status-gen status-active group-status">GEN</span>
|
||||
<span class="status-badge status-gen group-status">GEN</span>
|
||||
<span class="group-metrics">
|
||||
<span class="metric-time" title="Start time"><span class="material-symbols-outlined">schedule</span><span class="metric-value">--:--</span></span>
|
||||
<span class="metric-steps" title="Steps"><span class="material-symbols-outlined">footprint</span><span class="metric-value">0</span></span>
|
||||
<span class="metric-notifications" title="Warnings/Info/Hint" hidden><span class="material-symbols-outlined">priority_high</span><span class="metric-value">0</span></span>
|
||||
<span class="metric-duration" title="Duration"><span class="material-symbols-outlined">timer</span><span class="metric-value">0s</span></span>
|
||||
</span>
|
||||
`;
|
||||
|
|
@ -1260,6 +1342,9 @@ function getNestedContainer(parentStep) {
|
|||
* Add a step to a process group
|
||||
*/
|
||||
function addProcessStep(group, id, type, heading, content, kvps, timestamp = null, durationMs = null, agentNumber = 0) {
|
||||
// group with newest step becomes the active one
|
||||
setActiveProcessGroup(group);
|
||||
|
||||
const groupId = group.getAttribute("data-group-id");
|
||||
let stepsContainer = group.querySelector(".process-steps");
|
||||
const isGroupCompleted = group.classList.contains("process-group-completed");
|
||||
|
|
@ -1321,9 +1406,17 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
|
|||
const title = getStepTitle(heading, kvps, type);
|
||||
|
||||
// Check if step should be expanded
|
||||
// Warning/error steps auto-expand to show content
|
||||
const isStepExpanded = processGroupStore.isStepExpanded(groupId, id) ||
|
||||
(type === "warning" || type === "error");
|
||||
const isActiveStep = !isGroupCompleted && group.id === activeProcessGroupId;
|
||||
const isStepExpanded = processGroupStore.expandStep(groupId, id, isActiveStep);
|
||||
|
||||
// In "current" mode, collapse all other steps
|
||||
const detailMode = preferencesStore.detailMode;
|
||||
if (detailMode === "current" && isStepExpanded) {
|
||||
document.querySelectorAll(".process-step.step-expanded").forEach(s => {
|
||||
s.classList.remove("step-expanded");
|
||||
});
|
||||
}
|
||||
|
||||
if (isStepExpanded) {
|
||||
step.classList.add("step-expanded");
|
||||
}
|
||||
|
|
@ -1339,10 +1432,9 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
|
|||
// Add status color class to step for cascading --step-accent to internal icons
|
||||
step.classList.add(statusColorClass);
|
||||
|
||||
const activeClass = isGroupCompleted ? "" : " status-active";
|
||||
stepHeader.innerHTML = `
|
||||
<span class="step-expand-icon"></span>
|
||||
<span class="status-badge ${statusColorClass}${activeClass}">${statusCode}</span>
|
||||
<span class="status-badge ${statusColorClass}">${statusCode}</span>
|
||||
<span class="step-title">${escapeHTML(title)}</span>
|
||||
`;
|
||||
|
||||
|
|
@ -1354,11 +1446,6 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
|
|||
// Explicitly add or remove the class based on state
|
||||
if (newState) {
|
||||
step.classList.add("step-expanded");
|
||||
// Scroll terminal for newly expanded steps
|
||||
requestAnimationFrame(() => {
|
||||
const terminal = step.querySelector(".terminal-output");
|
||||
if (terminal) terminal.scrollTop = terminal.scrollHeight;
|
||||
});
|
||||
} else {
|
||||
step.classList.remove("step-expanded");
|
||||
}
|
||||
|
|
@ -1377,15 +1464,24 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
|
|||
renderStepDetailContent(detailContent, content, kvps, type);
|
||||
|
||||
detail.appendChild(detailContent);
|
||||
step.appendChild(detail);
|
||||
|
||||
// Scroll terminal for already expanded steps
|
||||
if (isStepExpanded) {
|
||||
requestAnimationFrame(() => {
|
||||
const terminal = step.querySelector(".terminal-output");
|
||||
if (terminal) terminal.scrollTop = terminal.scrollHeight;
|
||||
});
|
||||
}
|
||||
// Store step data on the element for fresh access on modal open
|
||||
step._stepData = {
|
||||
type,
|
||||
heading,
|
||||
content,
|
||||
kvps,
|
||||
timestamp,
|
||||
durationMs,
|
||||
agentNumber,
|
||||
toolName: toolNameToUse
|
||||
};
|
||||
|
||||
// Add "View Details" button for full modal view (reads fresh data from step._stepData)
|
||||
const viewDetailsBtn = createViewDetailsButton(step);
|
||||
detail.appendChild(viewDetailsBtn);
|
||||
|
||||
step.appendChild(detail);
|
||||
|
||||
// Track delegation steps for nesting
|
||||
if (toolNameToUse === "call_subordinate") {
|
||||
|
|
@ -1400,21 +1496,33 @@ function addProcessStep(group, id, type, heading, content, kvps, timestamp = nul
|
|||
const parentStep = currentDelegationSteps[agentNumber - 1];
|
||||
appendTarget = getNestedContainer(parentStep);
|
||||
step.classList.add("nested-step");
|
||||
|
||||
// Auto-expand parent if this nested step is a warning/error
|
||||
if (type === "warning" || type === "error") {
|
||||
parentStep.classList.add("step-expanded");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove status-active from all previous steps (only the current step is active)
|
||||
const prevSteps = stepsContainer.querySelectorAll(".process-step .status-badge.status-active");
|
||||
prevSteps.forEach(badge => badge.classList.remove("status-active"));
|
||||
// Remove shiny effect from the previously active step title (O(1))
|
||||
if (activeStepTitleEl) {
|
||||
activeStepTitleEl.classList.remove("shiny-text");
|
||||
activeStepTitleEl = null;
|
||||
}
|
||||
|
||||
appendTarget.appendChild(step);
|
||||
|
||||
// Scroll terminal to bottom on initial render (including page refresh)
|
||||
const initialTerminal = step.querySelector(".terminal-output");
|
||||
if (initialTerminal) {
|
||||
initialTerminal.scrollTop = initialTerminal.scrollHeight;
|
||||
}
|
||||
|
||||
// Update group header
|
||||
updateProcessGroupHeader(group);
|
||||
|
||||
// Apply shiny effect to the active step title
|
||||
if (!isGroupCompleted && group.id === activeProcessGroupId) {
|
||||
const titleEl = step.querySelector(".process-step-header .step-title");
|
||||
if (titleEl) {
|
||||
titleEl.classList.add("shiny-text");
|
||||
activeStepTitleEl = titleEl;
|
||||
}
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
|
@ -1453,6 +1561,10 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
|
|||
let skipFullRender = false;
|
||||
|
||||
if (detailContent) {
|
||||
// Capture scroll state before re-render (uses existing Scroller pattern)
|
||||
const terminal = detailContent.querySelector(".terminal-output");
|
||||
const scroller = terminal ? new Scroller(terminal) : null;
|
||||
|
||||
// For browser, update image src incrementally to avoid flashing
|
||||
if (type === "browser" && kvps?.screenshot) {
|
||||
const existingImg = detailContent.querySelector(".screenshot-img");
|
||||
|
|
@ -1469,9 +1581,28 @@ function updateProcessStep(stepElement, id, type, heading, content, kvps, durati
|
|||
|
||||
if (!skipFullRender) {
|
||||
renderStepDetailContent(detailContent, content, kvps, type);
|
||||
|
||||
// Re-apply scroll (stays at bottom if was at bottom)
|
||||
const newTerminal = detailContent.querySelector(".terminal-output");
|
||||
if (newTerminal && scroller?.wasAtBottom) {
|
||||
newTerminal.scrollTop = newTerminal.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update stored step data for fresh access by modal
|
||||
const timestamp = stepElement._stepData?.timestamp; // preserve original timestamp
|
||||
stepElement._stepData = {
|
||||
type,
|
||||
heading,
|
||||
content,
|
||||
kvps,
|
||||
timestamp,
|
||||
durationMs,
|
||||
agentNumber,
|
||||
toolName: toolNameToUse
|
||||
};
|
||||
|
||||
// Update parent group header
|
||||
const group = stepElement.closest(".process-group");
|
||||
if (group) {
|
||||
|
|
@ -1602,8 +1733,6 @@ function renderStepDetailContent(container, content, kvps, type = null) {
|
|||
processedOutput = convertPathsToLinks(processedOutput);
|
||||
outputPre.innerHTML = processedOutput;
|
||||
terminalDiv.appendChild(outputPre);
|
||||
// Scroll terminal to bottom
|
||||
outputPre.scrollTop = outputPre.scrollHeight;
|
||||
}
|
||||
|
||||
container.appendChild(terminalDiv);
|
||||
|
|
@ -1800,6 +1929,30 @@ function renderThoughts(container, value) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create "View Details" button for opening step detail modal
|
||||
* @param {HTMLElement} stepElement - The step DOM element containing _stepData property
|
||||
*/
|
||||
function createViewDetailsButton(stepElement) {
|
||||
const btnContainer = document.createElement("div");
|
||||
btnContainer.classList.add("step-detail-actions");
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.classList.add("btn", "text-button");
|
||||
btn.innerHTML = '<span class="material-symbols-outlined">open_in_full</span> View Details';
|
||||
btn.title = "Open full step details in modal";
|
||||
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
// Read fresh data from the step element at click time
|
||||
const freshData = stepElement._stepData || {};
|
||||
stepDetailStore.showStepDetail(freshData);
|
||||
});
|
||||
|
||||
btnContainer.appendChild(btn);
|
||||
return btnContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update process group header with step count, status, and metrics
|
||||
*/
|
||||
|
|
@ -1810,10 +1963,40 @@ function updateProcessGroupHeader(group) {
|
|||
const metricsEl = group.querySelector(".group-metrics");
|
||||
const isCompleted = group.classList.contains("process-group-completed");
|
||||
|
||||
// If completed, only remove active badges and exit early (don't update metrics)
|
||||
const notificationsEl = metricsEl?.querySelector(".metric-notifications");
|
||||
if (notificationsEl) {
|
||||
const counts = { warning: 0, info: 0, hint: 0 };
|
||||
steps.forEach((step) => {
|
||||
const stepType = step.getAttribute("data-type");
|
||||
if (Object.prototype.hasOwnProperty.call(counts, stepType)) {
|
||||
counts[stepType] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const totalNotifications = counts.warning + counts.info + counts.hint;
|
||||
const countEl = notificationsEl.querySelector(".metric-value");
|
||||
notificationsEl.classList.remove("status-wrn", "status-inf", "status-hnt");
|
||||
|
||||
if (totalNotifications > 0) {
|
||||
if (countEl) {
|
||||
countEl.textContent = totalNotifications.toString();
|
||||
}
|
||||
if (counts.warning > 0) {
|
||||
notificationsEl.classList.add("status-wrn");
|
||||
} else if (counts.info > 0) {
|
||||
notificationsEl.classList.add("status-inf");
|
||||
} else {
|
||||
notificationsEl.classList.add("status-hnt");
|
||||
}
|
||||
notificationsEl.hidden = false;
|
||||
notificationsEl.title = `Warnings: ${counts.warning}, Info: ${counts.info}, Hints: ${counts.hint}`;
|
||||
} else {
|
||||
notificationsEl.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If completed, don't update metrics
|
||||
if (isCompleted) {
|
||||
const activeBadges = group.querySelectorAll(".status-badge.status-active");
|
||||
activeBadges.forEach(badge => badge.classList.remove("status-active"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1881,14 +2064,14 @@ function updateProcessGroupHeader(group) {
|
|||
const lastToolName = lastStep.getAttribute("data-tool-name");
|
||||
const lastTitle = lastStep.querySelector(".step-title")?.textContent || "";
|
||||
|
||||
// Update status badge (keep status-active during execution)
|
||||
// Update status badge
|
||||
if (statusEl) {
|
||||
// Status code and color class from store (maps backend types)
|
||||
const statusCode = processGroupStore.getStepCode(lastType, lastToolName);
|
||||
const statusColorClass = processGroupStore.getStatusColorClass(lastType, lastToolName);
|
||||
|
||||
statusEl.textContent = statusCode;
|
||||
statusEl.className = `status-badge ${statusColorClass} status-active group-status`;
|
||||
statusEl.className = `status-badge ${statusColorClass} group-status`;
|
||||
}
|
||||
|
||||
// Update title
|
||||
|
|
@ -1929,17 +2112,13 @@ function truncateText(text, maxLength) {
|
|||
function markProcessGroupComplete(group, responseTitle) {
|
||||
if (!group) return;
|
||||
|
||||
// Update status badge to END (remove status-active)
|
||||
// Update status badge to END
|
||||
const statusEl = group.querySelector(".group-status");
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = '<span class="badge-icon material-symbols-outlined">check</span>END';
|
||||
statusEl.className = "status-badge status-end group-status"; // No status-active
|
||||
statusEl.className = "status-badge status-end group-status";
|
||||
}
|
||||
|
||||
// Remove status-active from all step badges (stop spinners)
|
||||
const stepBadges = group.querySelectorAll(".process-step .status-badge.status-active");
|
||||
stepBadges.forEach(badge => badge.classList.remove("status-active"));
|
||||
|
||||
// Update title if response title is available
|
||||
const titleEl = group.querySelector(".group-title");
|
||||
if (titleEl && responseTitle) {
|
||||
|
|
@ -1952,6 +2131,7 @@ function markProcessGroupComplete(group, responseTitle) {
|
|||
// Add completed class to group
|
||||
group.classList.add("process-group-completed");
|
||||
|
||||
|
||||
// Calculate final duration from backend data (sum of all step durations)
|
||||
const steps = group.querySelectorAll(".process-step");
|
||||
let totalDurationMs = 0;
|
||||
|
|
@ -1975,6 +2155,13 @@ export function resetProcessGroups() {
|
|||
currentProcessGroup = null;
|
||||
currentDelegationSteps = {};
|
||||
messageGroup = null;
|
||||
activeProcessGroupId = null;
|
||||
activeProcessGroupEl = null;
|
||||
window.activeProcessGroupId = null; // Keep window copy in sync
|
||||
if (activeStepTitleEl) {
|
||||
activeStepTitleEl.classList.remove("shiny-text");
|
||||
}
|
||||
activeStepTitleEl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue