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:
parent
3927dfb07e
commit
4499bcda7a
2 changed files with 200 additions and 93 deletions
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
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(
|
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) {
|
||||||
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) {
|
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
|
|
||||||
updateItems(childEl.querySelector(".tree-items"), [
|
|
||||||
...(child.item_groups || []),
|
|
||||||
...(child.items || []),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update section-level items
|
// Update section-level items
|
||||||
updateItems(sectionEl.querySelector(".tree-children"), [
|
const childrenContainer = sectionEl.querySelector(".tree-children");
|
||||||
...(section.item_groups || []),
|
if (childrenContainer) {
|
||||||
...(section.items || []),
|
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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue