diff --git a/ui/suite/tasks/taskmd.css b/ui/suite/tasks/taskmd.css index 1ac47bc..7a12963 100644 --- a/ui/suite/tasks/taskmd.css +++ b/ui/suite/tasks/taskmd.css @@ -177,16 +177,21 @@ border-radius: 50%; margin-right: 12px; flex-shrink: 0; + background: var(--accent, #c5f82a); + box-shadow: 0 0 8px var(--accent-glow, rgba(197, 248, 42, 0.5)); + animation: dot-pulse 1.5s ease-in-out infinite; } .status-dot.active { background: var(--accent, #c5f82a); box-shadow: 0 0 8px var(--accent-glow, rgba(197, 248, 42, 0.5)); + animation: dot-pulse 1.5s ease-in-out infinite; } .status-dot.pending { - background: transparent; - border: 1px dashed var(--text-muted, #444); + background: var(--text-muted, #444); + box-shadow: none; + animation: none; } .status-badge { @@ -213,6 +218,17 @@ flex-direction: column; } +/* Smooth transitions for all tree elements */ +.tree-section, +.tree-child, +.tree-item, +.tree-item-dot, +.tree-section-dot, +.tree-status, +.tree-step-badge { + transition: all 0.2s ease-out; +} + /* Tree Section (Level 0) - Main sections like "Database & Models" */ .tree-section { border: 1px solid var(--border, #1a1a1a); @@ -281,12 +297,21 @@ margin-left: auto; margin-right: 12px; white-space: nowrap; + transition: + background 0.2s ease-out, + color 0.2s ease-out; } .tree-section.pending .tree-step-badge, .tree-child.pending .tree-step-badge { - background: var(--surface-active, #2a2a2a); - color: var(--text-tertiary, #666); + background: var(--surface-active, #333); + color: var(--text-secondary, #888); +} + +.tree-section.running .tree-step-badge, +.tree-child.running .tree-step-badge { + background: var(--accent, #c5f82a); + color: var(--bg, #0a0a0a); } .tree-status { @@ -397,8 +422,8 @@ border-radius: 50%; margin-right: 12px; flex-shrink: 0; - background: var(--text-muted, #333); - transition: all 0.2s; + background: var(--text-muted, #444); + transition: all 0.3s ease-out; } .tree-item-dot.completed { @@ -423,11 +448,13 @@ 0%, 100% { opacity: 1; - box-shadow: 0 0 8px var(--accent-glow, rgba(197, 248, 42, 0.6)); + box-shadow: 0 0 10px var(--accent-glow, rgba(197, 248, 42, 0.8)); + transform: scale(1); } 50% { - opacity: 0.6; - box-shadow: 0 0 4px var(--accent-glow, rgba(197, 248, 42, 0.3)); + opacity: 0.7; + box-shadow: 0 0 4px var(--accent-glow, rgba(197, 248, 42, 0.4)); + transform: scale(0.9); } } diff --git a/ui/suite/tasks/tasks.js b/ui/suite/tasks/tasks.js index 5281de5..63f200d 100644 --- a/ui/suite/tasks/tasks.js +++ b/ui/suite/tasks/tasks.js @@ -521,24 +521,26 @@ function renderManifestProgress(taskId, manifest, retryCount = 0) { return; } - console.log("[Manifest] Rendering", manifest.sections.length, "sections"); - const totalSteps = manifest.progress?.total || 60; // Update STATUS section if exists updateStatusSection(manifest); - // Always rebuild the tree to ensure children are shown - // This is simpler and more reliable than incremental updates - const html = buildProgressTreeHTML(manifest, totalSteps); - progressLog.innerHTML = html; - - // Auto-expand running sections - progressLog - .querySelectorAll(".tree-section.running, .tree-child.running") - .forEach((el) => { - el.classList.add("expanded"); - }); + // Check if tree exists - if not, create it; if yes, update incrementally + let tree = progressLog.querySelector(".taskmd-tree"); + if (!tree) { + // First render only - create the tree structure + progressLog.innerHTML = buildProgressTreeHTML(manifest, totalSteps); + // Auto-expand running sections + progressLog + .querySelectorAll(".tree-section.running, .tree-child.running") + .forEach((el) => { + el.classList.add("expanded"); + }); + } else { + // Incremental update - only update what changed (no flicker) + updateProgressTreeInPlace(tree, manifest, totalSteps); + } // Update terminal stats updateTerminalStats(taskId, manifest); @@ -546,12 +548,9 @@ function renderManifestProgress(taskId, manifest, retryCount = 0) { function updateStatusSection(manifest) { const statusContent = document.querySelector(".taskmd-status-content"); - if (!statusContent) { - console.log("[Manifest] No status content element found"); - return; - } + if (!statusContent) return; - // Update current action + // Update current action text only if changed const actionText = statusContent.querySelector( ".status-current .status-text", ); @@ -559,19 +558,25 @@ function updateStatusSection(manifest) { manifest.status?.current_action || manifest.current_status?.current_action || "Processing..."; - if (actionText) { + if (actionText && actionText.textContent !== currentAction) { actionText.textContent = currentAction; } - // Update runtime + // Update runtime text only const runtimeEl = statusContent.querySelector(".status-main .status-time"); const runtime = manifest.status?.runtime_display || manifest.runtime || "Not started"; if (runtimeEl) { - runtimeEl.innerHTML = `Runtime: ${runtime} `; + // Only update text content, preserve indicator + const indicator = runtimeEl.querySelector(".status-indicator"); + if (!indicator) { + runtimeEl.innerHTML = `Runtime: ${runtime} `; + } else { + runtimeEl.firstChild.textContent = `Runtime: ${runtime} `; + } } - // Update estimated + // Update estimated text only const estimatedEl = statusContent.querySelector( ".status-current .status-time", ); @@ -581,17 +586,13 @@ function updateStatusSection(manifest) { ? `${manifest.estimated_seconds} sec` : "calculating..."); if (estimatedEl) { - estimatedEl.innerHTML = `Estimated: ${estimated} `; + const gear = estimatedEl.querySelector(".status-gear"); + if (!gear) { + estimatedEl.innerHTML = `Estimated: ${estimated} `; + } else { + estimatedEl.firstChild.textContent = `Estimated: ${estimated} `; + } } - - console.log( - "[Manifest] Status updated - action:", - currentAction, - "runtime:", - runtime, - "estimated:", - estimated, - ); } function buildProgressTreeHTML(manifest, totalSteps) { @@ -698,103 +699,182 @@ function buildItemHTML(item) { `; } -function updateProgressTree(tree, manifest, totalSteps) { +// Incremental update - only change what's different (prevents flicker) +function updateProgressTreeInPlace(tree, manifest, totalSteps) { for (const section of manifest.sections) { const sectionEl = tree.querySelector(`[data-section-id="${section.id}"]`); if (!sectionEl) continue; - const statusClass = section.status?.toLowerCase() || "pending"; + const rawStatus = section.status || "Pending"; + const statusClass = rawStatus.toLowerCase(); const globalCurrent = section.progress?.global_current || section.progress?.current || 0; + const isExpanded = sectionEl.classList.contains("expanded"); - // Update section class - sectionEl.className = `tree-section ${statusClass}${statusClass === "running" ? " expanded" : sectionEl.classList.contains("expanded") ? " expanded" : ""}`; + // Update section classes without removing expanded + const newClasses = `tree-section ${statusClass}${statusClass === "running" || isExpanded ? " expanded" : ""}`; + if (sectionEl.className !== newClasses) { + sectionEl.className = newClasses; + } - // Update step badge + // Update step badge text only if changed const stepBadge = sectionEl.querySelector( ":scope > .tree-row .tree-step-badge", ); - if (stepBadge) - stepBadge.textContent = `Step ${globalCurrent}/${totalSteps}`; + const stepText = `Step ${globalCurrent}/${totalSteps}`; + if (stepBadge && stepBadge.textContent !== stepText) { + stepBadge.textContent = stepText; + } - // Update status text + // Update status text and class only if changed const statusEl = sectionEl.querySelector(":scope > .tree-row .tree-status"); if (statusEl) { - statusEl.className = `tree-status ${statusClass}`; - statusEl.textContent = section.status || "Pending"; + if (statusEl.textContent !== rawStatus) { + statusEl.textContent = rawStatus; + } + const statusClasses = `tree-status ${statusClass}`; + if (statusEl.className !== statusClasses) { + statusEl.className = statusClasses; + } + } + + // Update section dot + const sectionDot = sectionEl.querySelector( + ":scope > .tree-row .tree-section-dot", + ); + if (sectionDot) { + const dotClasses = `tree-section-dot ${statusClass}`; + if (sectionDot.className !== dotClasses) { + sectionDot.className = dotClasses; + } } // Update children if (section.children) { for (const child of section.children) { - const childEl = sectionEl.querySelector( - `[data-child-id="${child.id}"]`, - ); - if (!childEl) continue; - - const childStatus = child.status?.toLowerCase() || "pending"; - childEl.className = `tree-child ${childStatus}${childStatus === "running" ? " expanded" : childEl.classList.contains("expanded") ? " expanded" : ""}`; - - const childStepBadge = childEl.querySelector( - ":scope > .tree-row .tree-step-badge", - ); - if (childStepBadge) - childStepBadge.textContent = `Step ${child.progress?.current || 0}/${child.progress?.total || 1}`; - - const childStatusEl = childEl.querySelector( - ":scope > .tree-row .tree-status", - ); - if (childStatusEl) { - childStatusEl.className = `tree-status ${childStatus}`; - childStatusEl.textContent = child.status || "Pending"; - } - - // Update items - updateItems(childEl.querySelector(".tree-items"), [ - ...(child.item_groups || []), - ...(child.items || []), - ]); + updateChildInPlace(sectionEl, child); } } // Update section-level items - updateItems(sectionEl.querySelector(".tree-children"), [ - ...(section.item_groups || []), - ...(section.items || []), + const childrenContainer = sectionEl.querySelector(".tree-children"); + if (childrenContainer) { + updateItemsInPlace(childrenContainer, [ + ...(section.item_groups || []), + ...(section.items || []), + ]); + } + } +} + +function updateChildInPlace(sectionEl, child) { + const childEl = sectionEl.querySelector(`[data-child-id="${child.id}"]`); + if (!childEl) return; + + const rawStatus = child.status || "Pending"; + const statusClass = rawStatus.toLowerCase(); + const isExpanded = childEl.classList.contains("expanded"); + + // Update child classes + const newClasses = `tree-child ${statusClass}${statusClass === "running" || isExpanded ? " expanded" : ""}`; + if (childEl.className !== newClasses) { + childEl.className = newClasses; + } + + // Update step badge + const stepBadge = childEl.querySelector( + ":scope > .tree-row .tree-step-badge", + ); + const stepText = `Step ${child.progress?.current || 0}/${child.progress?.total || 1}`; + if (stepBadge && stepBadge.textContent !== stepText) { + stepBadge.textContent = stepText; + } + + // Update status + const statusEl = childEl.querySelector(":scope > .tree-row .tree-status"); + if (statusEl) { + if (statusEl.textContent !== rawStatus) { + statusEl.textContent = rawStatus; + } + const statusClasses = `tree-status ${statusClass}`; + if (statusEl.className !== statusClasses) { + statusEl.className = statusClasses; + } + } + + // Update child dot + const childDot = childEl.querySelector(":scope > .tree-row .tree-item-dot"); + if (childDot) { + const dotClasses = `tree-item-dot ${statusClass}`; + if (childDot.className !== dotClasses) { + childDot.className = dotClasses; + } + } + + // Update items within child + const itemsContainer = childEl.querySelector(".tree-items"); + if (itemsContainer) { + updateItemsInPlace(itemsContainer, [ + ...(child.item_groups || []), + ...(child.items || []), ]); } } -function updateItems(container, items) { +function updateItemsInPlace(container, items) { if (!container || !items) return; for (const item of items) { const itemId = item.id || item.name || item.display_name; - const itemEl = container.querySelector(`[data-item-id="${itemId}"]`); + let itemEl = container.querySelector(`[data-item-id="${itemId}"]`); + if (!itemEl) { // New item - append it container.insertAdjacentHTML("beforeend", buildItemHTML(item)); continue; } - const status = item.status?.toLowerCase() || "pending"; - itemEl.className = `tree-item ${status}`; + const rawStatus = item.status || "Pending"; + const status = rawStatus.toLowerCase(); - const dot = itemEl.querySelector(".tree-item-dot"); - if (dot) dot.className = `tree-item-dot ${status}`; - - const check = itemEl.querySelector(".tree-item-check"); - if (check) { - check.className = `tree-item-check ${status}`; - check.textContent = status === "completed" ? "✓" : ""; + // Update item class + const newClasses = `tree-item ${status}`; + if (itemEl.className !== newClasses) { + itemEl.className = newClasses; } + // Update dot + const dot = itemEl.querySelector(".tree-item-dot"); + if (dot) { + const dotClasses = `tree-item-dot ${status}`; + if (dot.className !== dotClasses) { + dot.className = dotClasses; + } + } + + // Update check + const check = itemEl.querySelector(".tree-item-check"); + if (check) { + const checkClasses = `tree-item-check ${status}`; + if (check.className !== checkClasses) { + check.className = checkClasses; + } + const checkText = status === "completed" ? "✓" : ""; + if (check.textContent !== checkText) { + check.textContent = checkText; + } + } + + // Update duration const durationEl = itemEl.querySelector(".tree-item-duration"); if (durationEl && item.duration_seconds) { - durationEl.textContent = + const durationText = item.duration_seconds >= 60 ? `Duration: ${Math.floor(item.duration_seconds / 60)} min` : `Duration: ${item.duration_seconds} sec`; + if (durationEl.textContent !== durationText) { + durationEl.textContent = durationText; + } } } }