From 6bbedd6645b3e1b5756658393fb4236d1a581bda Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Thu, 1 Jan 2026 10:13:38 -0300 Subject: [PATCH] Fix progress UI: incremental updates to prevent flicker, proper scroll - Rewrite renderManifestProgress to update existing DOM elements - Only rebuild HTML on first render, then update incrementally - Add updateStatusSection, updateProgressTree, updateItems functions - Fix scroll: only task list scrolls, detail panel is fixed - Add status-indicator, status-gear CSS styles - Progress content has max-height with scroll, terminal is fixed size --- ui/suite/tasks/taskmd.css | 70 ++++++- ui/suite/tasks/tasks.css | 37 +++- ui/suite/tasks/tasks.js | 420 ++++++++++++++++++++------------------ 3 files changed, 319 insertions(+), 208 deletions(-) diff --git a/ui/suite/tasks/taskmd.css b/ui/suite/tasks/taskmd.css index 19f0a99..44c0066 100644 --- a/ui/suite/tasks/taskmd.css +++ b/ui/suite/tasks/taskmd.css @@ -147,6 +147,28 @@ .status-time { font-size: 13px; color: var(--text-tertiary, #666); + display: flex; + align-items: center; + gap: 6px; +} + +.status-indicator { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent, #c5f82a); + animation: dot-pulse 1.5s ease-in-out infinite; +} + +.status-gear { + font-size: 14px; + color: var(--text-muted, #444); + cursor: pointer; + transition: color 0.2s; +} + +.status-gear:hover { + color: var(--accent, #c5f82a); } .status-dot { @@ -179,7 +201,11 @@ /* PROGRESS LOG Section */ .taskmd-progress-content { background: var(--bg, #0a0a0a); - flex-shrink: 0; + flex: 1; + min-height: 0; + max-height: 350px; + overflow-y: auto; + overflow-x: hidden; } .taskmd-tree { @@ -434,11 +460,11 @@ /* TERMINAL Section */ .taskmd-terminal { - flex: 1; + flex-shrink: 0; display: flex; flex-direction: column; - min-height: 150px; - max-height: 300px; + min-height: 120px; + max-height: 200px; overflow: hidden; } @@ -582,9 +608,43 @@ .taskmd-actions { display: flex; gap: 12px; - padding: 20px 24px; + padding: 16px 24px; border-top: 1px solid var(--border, #1a1a1a); background: var(--bg-secondary, #0d0d0d); + flex-shrink: 0; +} + +.taskmd-actions .btn-action-rich { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.taskmd-actions .btn-open-app { + background: var(--accent, #c5f82a); + color: var(--bg, #0a0a0a); +} + +.taskmd-actions .btn-open-app:hover { + filter: brightness(1.1); +} + +.taskmd-actions .btn-cancel { + background: transparent; + border: 1px solid var(--border, #333); + color: var(--text-secondary, #888); +} + +.taskmd-actions .btn-cancel:hover { + border-color: var(--error, #ef4444); + color: var(--error, #ef4444); } /* Empty State */ diff --git a/ui/suite/tasks/tasks.css b/ui/suite/tasks/tasks.css index 5078c26..be1d870 100644 --- a/ui/suite/tasks/tasks.css +++ b/ui/suite/tasks/tasks.css @@ -275,6 +275,7 @@ grid-template-columns: 30% 70%; flex: 1; overflow: hidden; + height: 100%; } /* ============================================================================= @@ -286,11 +287,13 @@ flex-direction: column; overflow: hidden; border-right: 1px solid var(--border, #2a2a2a); + height: 100%; } .tasks-list-scroll { flex: 1; overflow-y: auto; + overflow-x: hidden; padding: 20px; display: flex; flex-direction: column; @@ -811,6 +814,15 @@ overflow: hidden; height: 100%; min-height: 0; + position: relative; +} + +/* Task detail content - NO scroll on main container */ +#task-detail-content { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; } /* Detail Header */ @@ -3118,12 +3130,31 @@ .task-detail-rich { display: flex; flex-direction: column; - gap: 20px; - padding: 24px; + gap: 0; + padding: 0; height: 100%; min-height: 0; + overflow: hidden; +} + +/* Sections inside task detail - fixed height, no scroll */ +.task-detail-rich .taskmd-header, +.task-detail-rich .taskmd-section { + flex-shrink: 0; +} + +/* Only terminal output scrolls */ +.task-detail-rich .taskmd-terminal-output { + flex: 1; + min-height: 100px; + max-height: none; + overflow-y: auto; +} + +/* Progress content has fixed max height with scroll */ +.task-detail-rich .taskmd-progress-content { + max-height: 300px; overflow-y: auto; - overflow-x: hidden; } .detail-section-box { diff --git a/ui/suite/tasks/tasks.js b/ui/suite/tasks/tasks.js index 45a84a1..2e429e0 100644 --- a/ui/suite/tasks/tasks.js +++ b/ui/suite/tasks/tasks.js @@ -459,265 +459,285 @@ function handleWebSocketMessage(data) { const pendingManifestUpdates = new Map(); function renderManifestProgress(taskId, manifest, retryCount = 0) { - console.log( - "[Manifest] renderManifestProgress called for task:", - taskId, - "sections:", - manifest?.sections?.length, - "retry:", - retryCount, - ); + // Only update if this is the selected task + if (TasksState.selectedTaskId !== taskId) { + return; + } // Try multiple selectors to find the progress log element let progressLog = document.getElementById(`progress-log-${taskId}`); if (!progressLog) { progressLog = document.querySelector(".taskmd-progress-content"); } - if (!progressLog) { - progressLog = document.querySelector(".progress-summary-content"); - } if (!progressLog) { - console.warn("[Manifest] No progress log element found for task:", taskId); - // Try to find any visible task detail panel - const detailPanel = document.querySelector(".task-detail-rich"); - if (detailPanel) { - const taskDetailId = detailPanel.dataset.taskId; - console.log("[Manifest] Found detail panel for task:", taskDetailId); - if (taskDetailId === taskId) { - progressLog = detailPanel.querySelector(".taskmd-progress-content"); - } + // If task is selected but element not yet loaded, retry after a delay + if (retryCount < 5) { + pendingManifestUpdates.set(taskId, manifest); + setTimeout( + () => { + const pending = pendingManifestUpdates.get(taskId); + if (pending && TasksState.selectedTaskId === taskId) { + renderManifestProgress(taskId, pending, retryCount + 1); + } + }, + 150 * (retryCount + 1), + ); } - if (!progressLog) { - // If task is selected but element not yet loaded, retry after a delay - if (TasksState.selectedTaskId === taskId && retryCount < 5) { - console.log( - "[Manifest] Element not ready, scheduling retry", - retryCount + 1, - ); - pendingManifestUpdates.set(taskId, manifest); - setTimeout( - () => { - const pending = pendingManifestUpdates.get(taskId); - if (pending) { - renderManifestProgress(taskId, pending, retryCount + 1); - } - }, - 200 * (retryCount + 1), - ); - } - return; - } - } - - // Clear pending update since we found the element - pendingManifestUpdates.delete(taskId); - - if (!manifest || !manifest.sections) { - console.log("[Manifest] No sections in manifest"); return; } - console.log("[Manifest] Rendering", manifest.sections.length, "sections"); + // Clear pending update + pendingManifestUpdates.delete(taskId); + + if (!manifest || !manifest.sections) { + return; + } - // Get total steps from manifest progress const totalSteps = manifest.progress?.total || 60; + // Update STATUS section if exists + updateStatusSection(manifest); + + // Update or create progress tree + let tree = progressLog.querySelector(".taskmd-tree"); + if (!tree) { + // First render - create full HTML + 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 changed elements + updateProgressTree(tree, manifest, totalSteps); + } + + // Update terminal stats + updateTerminalStats(taskId, manifest); +} + +function updateStatusSection(manifest) { + const statusContent = document.querySelector(".taskmd-status-content"); + if (!statusContent) return; + + // Update current action + const actionText = statusContent.querySelector( + ".status-current .status-text", + ); + if (actionText && manifest.status?.current_action) { + actionText.textContent = manifest.status.current_action; + } + + // Update runtime + const runtimeEl = statusContent.querySelector(".status-main .status-time"); + if (runtimeEl && manifest.status?.runtime_display) { + runtimeEl.innerHTML = `Runtime: ${manifest.status.runtime_display} `; + } + + // Update estimated + const estimatedEl = statusContent.querySelector( + ".status-current .status-time", + ); + if (estimatedEl && manifest.status?.estimated_display) { + estimatedEl.innerHTML = `Estimated: ${manifest.status.estimated_display} `; + } +} + +function buildProgressTreeHTML(manifest, totalSteps) { let html = '
'; for (const section of manifest.sections) { - const statusClass = section.status - ? section.status.toLowerCase() - : "pending"; - - // Use global step count (e.g., "Step 24/60") + const statusClass = section.status?.toLowerCase() || "pending"; + const isRunning = statusClass === "running"; const globalCurrent = section.progress?.global_current || section.progress?.current || 0; - const globalStart = section.progress?.global_start || 0; - const statusText = section.status || "Pending"; - // Calculate section progress percentage - const sectionCurrent = section.progress?.current || 0; - const sectionTotal = section.progress?.total || 1; - const sectionPercent = Math.round((sectionCurrent / sectionTotal) * 100); - const showProgress = statusClass === "running" && sectionTotal > 1; - html += ` -
+
${escapeHtml(section.name)} View Details › Step ${globalCurrent}/${totalSteps} ${statusText}
- ${ - showProgress - ? ` -
-
- ${sectionPercent}% -
` - : "" - }
`; - // Render children if present - if (section.children && section.children.length > 0) { + // Children + if (section.children?.length > 0) { for (const child of section.children) { - const childStatus = child.status - ? child.status.toLowerCase() - : "pending"; - const childStepCurrent = child.progress?.current || 0; - const childStepTotal = child.progress?.total || 1; - const childStatusText = child.status || "Pending"; - + const childStatus = child.status?.toLowerCase() || "pending"; + const childIsRunning = childStatus === "running"; html += ` -
-
+
+
${escapeHtml(child.name)} View Details › - Step ${childStepCurrent}/${childStepTotal} - ${childStatusText} + Step ${child.progress?.current || 0}/${child.progress?.total || 1} + ${child.status || "Pending"}
`; - // Render item_groups first (grouped fields like "email, password_hash, email_verified") - if (child.item_groups && child.item_groups.length > 0) { - for (const group of child.item_groups) { - const groupStatus = group.status - ? group.status.toLowerCase() - : "pending"; - const checkIcon = groupStatus === "completed" ? "✓" : ""; - const duration = group.duration_seconds - ? group.duration_seconds >= 60 - ? `Duration: ${Math.floor(group.duration_seconds / 60)} min` - : `Duration: ${group.duration_seconds} sec` - : ""; - - html += ` -
- - ${escapeHtml(group.name)} - ${duration} - ${checkIcon} -
`; - } - } - - // Then render individual items - if (child.items && child.items.length > 0) { - for (const item of child.items) { - const itemStatus = item.status - ? item.status.toLowerCase() - : "pending"; - const checkIcon = itemStatus === "completed" ? "✓" : ""; - const duration = item.duration_seconds - ? item.duration_seconds >= 60 - ? `Duration: ${Math.floor(item.duration_seconds / 60)} min` - : `Duration: ${item.duration_seconds} sec` - : ""; - - html += ` -
- - ${escapeHtml(item.name)} - ${duration} - ${checkIcon} -
`; - } + // Items + const items = [...(child.item_groups || []), ...(child.items || [])]; + for (const item of items) { + html += buildItemHTML(item); } html += `
`; } } - // Render section-level item_groups - if (section.item_groups && section.item_groups.length > 0) { - for (const group of section.item_groups) { - const groupStatus = group.status - ? group.status.toLowerCase() - : "pending"; - const checkIcon = groupStatus === "completed" ? "✓" : ""; - const duration = group.duration_seconds - ? group.duration_seconds >= 60 - ? `Duration: ${Math.floor(group.duration_seconds / 60)} min` - : `Duration: ${group.duration_seconds} sec` - : ""; - - html += ` -
- - ${escapeHtml(group.name)} - ${duration} - ${checkIcon} -
`; - } - } - - // Render section-level items - if (section.items && section.items.length > 0) { - for (const item of section.items) { - const itemStatus = item.status ? item.status.toLowerCase() : "pending"; - const checkIcon = itemStatus === "completed" ? "✓" : ""; - const duration = item.duration_seconds - ? item.duration_seconds >= 60 - ? `Duration: ${Math.floor(item.duration_seconds / 60)} min` - : `Duration: ${item.duration_seconds} sec` - : ""; - - html += ` -
- - ${escapeHtml(item.name)} - ${duration} - ${checkIcon} -
`; - } + // Section-level items + const sectionItems = [ + ...(section.item_groups || []), + ...(section.items || []), + ]; + for (const item of sectionItems) { + html += buildItemHTML(item); } html += `
`; } html += "
"; + return html; +} - progressLog.innerHTML = html; +function buildItemHTML(item) { + const status = item.status?.toLowerCase() || "pending"; + const checkIcon = status === "completed" ? "✓" : ""; + const duration = item.duration_seconds + ? item.duration_seconds >= 60 + ? `Duration: ${Math.floor(item.duration_seconds / 60)} min` + : `Duration: ${item.duration_seconds} sec` + : ""; + const name = item.name || item.display_name || ""; - // Also update the STATUS section if it exists - const statusSection = document.querySelector(".taskmd-status-content"); - if (statusSection && manifest.status) { - const runtime = manifest.status.runtime_display || "calculating..."; - const estimated = manifest.status.estimated_display || "calculating..."; - const currentAction = manifest.status.current_action || "Processing..."; + return ` +
+ + ${escapeHtml(name)} + ${duration} + ${checkIcon} +
`; +} - statusSection.innerHTML = ` -
- ${escapeHtml(manifest.status.title || manifest.app_name || "")} - Runtime: ${runtime} -
-
- - ${escapeHtml(currentAction)} - Estimated: ${estimated} -
- `; +function updateProgressTree(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 globalCurrent = + section.progress?.global_current || section.progress?.current || 0; + + // Update section class + sectionEl.className = `tree-section ${statusClass}${statusClass === "running" ? " expanded" : sectionEl.classList.contains("expanded") ? " expanded" : ""}`; + + // Update step badge + const stepBadge = sectionEl.querySelector( + ":scope > .tree-row .tree-step-badge", + ); + if (stepBadge) + stepBadge.textContent = `Step ${globalCurrent}/${totalSteps}`; + + // Update status text + const statusEl = sectionEl.querySelector(":scope > .tree-row .tree-status"); + if (statusEl) { + statusEl.className = `tree-status ${statusClass}`; + statusEl.textContent = section.status || "Pending"; + } + + // 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 || []), + ]); + } + } + + // Update section-level items + updateItems(sectionEl.querySelector(".tree-children"), [ + ...(section.item_groups || []), + ...(section.items || []), + ]); } +} - // Update terminal stats if they exist +function updateItems(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}"]`); + if (!itemEl) { + // New item - append it + container.insertAdjacentHTML("beforeend", buildItemHTML(item)); + continue; + } + + const status = item.status?.toLowerCase() || "pending"; + itemEl.className = `tree-item ${status}`; + + 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" ? "✓" : ""; + } + + const durationEl = itemEl.querySelector(".tree-item-duration"); + if (durationEl && item.duration_seconds) { + durationEl.textContent = + item.duration_seconds >= 60 + ? `Duration: ${Math.floor(item.duration_seconds / 60)} min` + : `Duration: ${item.duration_seconds} sec`; + } + } +} + +function updateTerminalStats(taskId, manifest) { const processedEl = document.getElementById(`terminal-processed-${taskId}`); - if (processedEl && manifest.terminal?.stats) { - processedEl.textContent = manifest.terminal.stats.processed || "0"; + if (processedEl && manifest.terminal?.stats?.processed) { + processedEl.textContent = manifest.terminal.stats.processed; } - console.log( - "[Manifest] Rendered progress for task:", - taskId, - "completed:", - manifest.progress?.current, - "/", - manifest.progress?.total, - ); + const etaEl = document.getElementById(`terminal-eta-${taskId}`); + if (etaEl && manifest.terminal?.stats?.eta) { + etaEl.textContent = manifest.terminal.stats.eta; + } } function escapeHtml(text) {