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