Fix flicker: incremental DOM updates only, smooth CSS transitions

- updateProgressTreeInPlace: only updates changed text/classes, no innerHTML rebuild
- updateChildInPlace, updateItemsInPlace: granular element updates
- Check if value changed before updating (prevents unnecessary DOM writes)
- CSS transitions on all tree elements (0.2s ease-out)
- Status dot now yellow/pulsing by default (was missing)
- Running step badges get accent color
- dot-pulse animation with scale effect
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-01 10:49:30 -03:00
parent 3927dfb07e
commit 4499bcda7a
2 changed files with 200 additions and 93 deletions

View file

@ -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);
}
}

View file

@ -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} <span class="status-indicator"></span>`;
// Only update text content, preserve indicator
const indicator = runtimeEl.querySelector(".status-indicator");
if (!indicator) {
runtimeEl.innerHTML = `Runtime: ${runtime} <span class="status-indicator"></span>`;
} 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} <span class="status-gear">⚙</span>`;
const gear = estimatedEl.querySelector(".status-gear");
if (!gear) {
estimatedEl.innerHTML = `Estimated: ${estimated} <span class="status-gear">⚙</span>`;
} 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) {
</div>`;
}
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;
}
}
}
}