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
This commit is contained in:
parent
9f922b523d
commit
6bbedd6645
3 changed files with 319 additions and 208 deletions
|
|
@ -147,6 +147,28 @@
|
||||||
.status-time {
|
.status-time {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-tertiary, #666);
|
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 {
|
.status-dot {
|
||||||
|
|
@ -179,7 +201,11 @@
|
||||||
/* PROGRESS LOG Section */
|
/* PROGRESS LOG Section */
|
||||||
.taskmd-progress-content {
|
.taskmd-progress-content {
|
||||||
background: var(--bg, #0a0a0a);
|
background: var(--bg, #0a0a0a);
|
||||||
flex-shrink: 0;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskmd-tree {
|
.taskmd-tree {
|
||||||
|
|
@ -434,11 +460,11 @@
|
||||||
|
|
||||||
/* TERMINAL Section */
|
/* TERMINAL Section */
|
||||||
.taskmd-terminal {
|
.taskmd-terminal {
|
||||||
flex: 1;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 150px;
|
min-height: 120px;
|
||||||
max-height: 300px;
|
max-height: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -582,9 +608,43 @@
|
||||||
.taskmd-actions {
|
.taskmd-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 20px 24px;
|
padding: 16px 24px;
|
||||||
border-top: 1px solid var(--border, #1a1a1a);
|
border-top: 1px solid var(--border, #1a1a1a);
|
||||||
background: var(--bg-secondary, #0d0d0d);
|
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 */
|
/* Empty State */
|
||||||
|
|
|
||||||
|
|
@ -275,6 +275,7 @@
|
||||||
grid-template-columns: 30% 70%;
|
grid-template-columns: 30% 70%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
|
|
@ -286,11 +287,13 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-right: 1px solid var(--border, #2a2a2a);
|
border-right: 1px solid var(--border, #2a2a2a);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tasks-list-scroll {
|
.tasks-list-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -811,6 +814,15 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
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 */
|
/* Detail Header */
|
||||||
|
|
@ -3118,12 +3130,31 @@
|
||||||
.task-detail-rich {
|
.task-detail-rich {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 0;
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
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-y: auto;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section-box {
|
.detail-section-box {
|
||||||
|
|
|
||||||
|
|
@ -459,265 +459,285 @@ function handleWebSocketMessage(data) {
|
||||||
const pendingManifestUpdates = new Map();
|
const pendingManifestUpdates = new Map();
|
||||||
|
|
||||||
function renderManifestProgress(taskId, manifest, retryCount = 0) {
|
function renderManifestProgress(taskId, manifest, retryCount = 0) {
|
||||||
console.log(
|
// Only update if this is the selected task
|
||||||
"[Manifest] renderManifestProgress called for task:",
|
if (TasksState.selectedTaskId !== taskId) {
|
||||||
taskId,
|
return;
|
||||||
"sections:",
|
}
|
||||||
manifest?.sections?.length,
|
|
||||||
"retry:",
|
|
||||||
retryCount,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Try multiple selectors to find the progress log element
|
// Try multiple selectors to find the progress log element
|
||||||
let progressLog = document.getElementById(`progress-log-${taskId}`);
|
let progressLog = document.getElementById(`progress-log-${taskId}`);
|
||||||
if (!progressLog) {
|
if (!progressLog) {
|
||||||
progressLog = document.querySelector(".taskmd-progress-content");
|
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 (!progressLog) {
|
if (!progressLog) {
|
||||||
// If task is selected but element not yet loaded, retry after a delay
|
// If task is selected but element not yet loaded, retry after a delay
|
||||||
if (TasksState.selectedTaskId === taskId && retryCount < 5) {
|
if (retryCount < 5) {
|
||||||
console.log(
|
|
||||||
"[Manifest] Element not ready, scheduling retry",
|
|
||||||
retryCount + 1,
|
|
||||||
);
|
|
||||||
pendingManifestUpdates.set(taskId, manifest);
|
pendingManifestUpdates.set(taskId, manifest);
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
const pending = pendingManifestUpdates.get(taskId);
|
const pending = pendingManifestUpdates.get(taskId);
|
||||||
if (pending) {
|
if (pending && TasksState.selectedTaskId === taskId) {
|
||||||
renderManifestProgress(taskId, pending, retryCount + 1);
|
renderManifestProgress(taskId, pending, retryCount + 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
200 * (retryCount + 1),
|
150 * (retryCount + 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Clear pending update since we found the element
|
// Clear pending update
|
||||||
pendingManifestUpdates.delete(taskId);
|
pendingManifestUpdates.delete(taskId);
|
||||||
|
|
||||||
if (!manifest || !manifest.sections) {
|
if (!manifest || !manifest.sections) {
|
||||||
console.log("[Manifest] No sections in manifest");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Manifest] Rendering", manifest.sections.length, "sections");
|
|
||||||
|
|
||||||
// Get total steps from manifest progress
|
|
||||||
const totalSteps = manifest.progress?.total || 60;
|
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} <span class="status-indicator"></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update estimated
|
||||||
|
const estimatedEl = statusContent.querySelector(
|
||||||
|
".status-current .status-time",
|
||||||
|
);
|
||||||
|
if (estimatedEl && manifest.status?.estimated_display) {
|
||||||
|
estimatedEl.innerHTML = `Estimated: ${manifest.status.estimated_display} <span class="status-gear">⚙</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProgressTreeHTML(manifest, totalSteps) {
|
||||||
let html = '<div class="taskmd-tree">';
|
let html = '<div class="taskmd-tree">';
|
||||||
|
|
||||||
for (const section of manifest.sections) {
|
for (const section of manifest.sections) {
|
||||||
const statusClass = section.status
|
const statusClass = section.status?.toLowerCase() || "pending";
|
||||||
? section.status.toLowerCase()
|
const isRunning = statusClass === "running";
|
||||||
: "pending";
|
|
||||||
|
|
||||||
// Use global step count (e.g., "Step 24/60")
|
|
||||||
const globalCurrent =
|
const globalCurrent =
|
||||||
section.progress?.global_current || section.progress?.current || 0;
|
section.progress?.global_current || section.progress?.current || 0;
|
||||||
const globalStart = section.progress?.global_start || 0;
|
|
||||||
|
|
||||||
const statusText = section.status || "Pending";
|
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 += `
|
html += `
|
||||||
<div class="tree-section ${statusClass}" data-section-id="${section.id}">
|
<div class="tree-section ${statusClass}${isRunning ? " expanded" : ""}" data-section-id="${section.id}">
|
||||||
<div class="tree-row tree-level-0" onclick="this.parentElement.classList.toggle('expanded')">
|
<div class="tree-row tree-level-0" onclick="this.parentElement.classList.toggle('expanded')">
|
||||||
<span class="tree-name">${escapeHtml(section.name)}</span>
|
<span class="tree-name">${escapeHtml(section.name)}</span>
|
||||||
<span class="tree-view-details">View Details ›</span>
|
<span class="tree-view-details">View Details ›</span>
|
||||||
<span class="tree-step-badge">Step ${globalCurrent}/${totalSteps}</span>
|
<span class="tree-step-badge">Step ${globalCurrent}/${totalSteps}</span>
|
||||||
<span class="tree-status ${statusClass}">${statusText}</span>
|
<span class="tree-status ${statusClass}">${statusText}</span>
|
||||||
</div>
|
</div>
|
||||||
${
|
|
||||||
showProgress
|
|
||||||
? `
|
|
||||||
<div class="tree-progress-bar-container">
|
|
||||||
<div class="tree-progress-bar" style="width: ${sectionPercent}%"></div>
|
|
||||||
<span class="tree-progress-percent">${sectionPercent}%</span>
|
|
||||||
</div>`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
<div class="tree-children">`;
|
<div class="tree-children">`;
|
||||||
|
|
||||||
// Render children if present
|
// Children
|
||||||
if (section.children && section.children.length > 0) {
|
if (section.children?.length > 0) {
|
||||||
for (const child of section.children) {
|
for (const child of section.children) {
|
||||||
const childStatus = child.status
|
const childStatus = child.status?.toLowerCase() || "pending";
|
||||||
? child.status.toLowerCase()
|
const childIsRunning = childStatus === "running";
|
||||||
: "pending";
|
|
||||||
const childStepCurrent = child.progress?.current || 0;
|
|
||||||
const childStepTotal = child.progress?.total || 1;
|
|
||||||
const childStatusText = child.status || "Pending";
|
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="tree-child ${childStatus}" onclick="this.classList.toggle('expanded')">
|
<div class="tree-child ${childStatus}${childIsRunning ? " expanded" : ""}" data-child-id="${child.id}">
|
||||||
<div class="tree-row tree-level-1">
|
<div class="tree-row tree-level-1" onclick="this.parentElement.classList.toggle('expanded')">
|
||||||
<span class="tree-indent"></span>
|
<span class="tree-indent"></span>
|
||||||
<span class="tree-name">${escapeHtml(child.name)}</span>
|
<span class="tree-name">${escapeHtml(child.name)}</span>
|
||||||
<span class="tree-view-details">View Details ›</span>
|
<span class="tree-view-details">View Details ›</span>
|
||||||
<span class="tree-step-badge">Step ${childStepCurrent}/${childStepTotal}</span>
|
<span class="tree-step-badge">Step ${child.progress?.current || 0}/${child.progress?.total || 1}</span>
|
||||||
<span class="tree-status ${childStatus}">${childStatusText}</span>
|
<span class="tree-status ${childStatus}">${child.status || "Pending"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-items">`;
|
<div class="tree-items">`;
|
||||||
|
|
||||||
// Render item_groups first (grouped fields like "email, password_hash, email_verified")
|
// Items
|
||||||
if (child.item_groups && child.item_groups.length > 0) {
|
const items = [...(child.item_groups || []), ...(child.items || [])];
|
||||||
for (const group of child.item_groups) {
|
for (const item of items) {
|
||||||
const groupStatus = group.status
|
html += buildItemHTML(item);
|
||||||
? 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 += `
|
|
||||||
<div class="tree-item ${groupStatus}">
|
|
||||||
<span class="tree-item-dot ${groupStatus}"></span>
|
|
||||||
<span class="tree-item-name">${escapeHtml(group.name)}</span>
|
|
||||||
<span class="tree-item-duration">${duration}</span>
|
|
||||||
<span class="tree-item-check ${groupStatus}">${checkIcon}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 += `
|
|
||||||
<div class="tree-item ${itemStatus}">
|
|
||||||
<span class="tree-item-dot ${itemStatus}"></span>
|
|
||||||
<span class="tree-item-name">${escapeHtml(item.name)}</span>
|
|
||||||
<span class="tree-item-duration">${duration}</span>
|
|
||||||
<span class="tree-item-check ${itemStatus}">${checkIcon}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `</div></div>`;
|
html += `</div></div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render section-level item_groups
|
// Section-level items
|
||||||
if (section.item_groups && section.item_groups.length > 0) {
|
const sectionItems = [
|
||||||
for (const group of section.item_groups) {
|
...(section.item_groups || []),
|
||||||
const groupStatus = group.status
|
...(section.items || []),
|
||||||
? group.status.toLowerCase()
|
];
|
||||||
: "pending";
|
for (const item of sectionItems) {
|
||||||
const checkIcon = groupStatus === "completed" ? "✓" : "";
|
html += buildItemHTML(item);
|
||||||
const duration = group.duration_seconds
|
|
||||||
? group.duration_seconds >= 60
|
|
||||||
? `Duration: ${Math.floor(group.duration_seconds / 60)} min`
|
|
||||||
: `Duration: ${group.duration_seconds} sec`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="tree-item ${groupStatus}">
|
|
||||||
<span class="tree-item-dot ${groupStatus}"></span>
|
|
||||||
<span class="tree-item-name">${escapeHtml(group.name)}</span>
|
|
||||||
<span class="tree-item-duration">${duration}</span>
|
|
||||||
<span class="tree-item-check ${groupStatus}">${checkIcon}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 += `
|
|
||||||
<div class="tree-item ${itemStatus}">
|
|
||||||
<span class="tree-item-dot ${itemStatus}"></span>
|
|
||||||
<span class="tree-item-name">${escapeHtml(item.name)}</span>
|
|
||||||
<span class="tree-item-duration">${duration}</span>
|
|
||||||
<span class="tree-item-check ${itemStatus}">${checkIcon}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `</div></div>`;
|
html += `</div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
|
return html;
|
||||||
progressLog.innerHTML = html;
|
|
||||||
|
|
||||||
// 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...";
|
|
||||||
|
|
||||||
statusSection.innerHTML = `
|
|
||||||
<div class="status-row status-main">
|
|
||||||
<span class="status-title">${escapeHtml(manifest.status.title || manifest.app_name || "")}</span>
|
|
||||||
<span class="status-time">Runtime: ${runtime}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-row status-current">
|
|
||||||
<span class="status-dot active"></span>
|
|
||||||
<span class="status-text">${escapeHtml(currentAction)}</span>
|
|
||||||
<span class="status-time">Estimated: ${estimated}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update terminal stats if they exist
|
function buildItemHTML(item) {
|
||||||
const processedEl = document.getElementById(`terminal-processed-${taskId}`);
|
const status = item.status?.toLowerCase() || "pending";
|
||||||
if (processedEl && manifest.terminal?.stats) {
|
const checkIcon = status === "completed" ? "✓" : "";
|
||||||
processedEl.textContent = manifest.terminal.stats.processed || "0";
|
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 || "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="tree-item ${status}" data-item-id="${item.id || name}">
|
||||||
|
<span class="tree-item-dot ${status}"></span>
|
||||||
|
<span class="tree-item-name">${escapeHtml(name)}</span>
|
||||||
|
<span class="tree-item-duration">${duration}</span>
|
||||||
|
<span class="tree-item-check ${status}">${checkIcon}</span>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
function updateProgressTree(tree, manifest, totalSteps) {
|
||||||
"[Manifest] Rendered progress for task:",
|
for (const section of manifest.sections) {
|
||||||
taskId,
|
const sectionEl = tree.querySelector(`[data-section-id="${section.id}"]`);
|
||||||
"completed:",
|
if (!sectionEl) continue;
|
||||||
manifest.progress?.current,
|
|
||||||
"/",
|
const statusClass = section.status?.toLowerCase() || "pending";
|
||||||
manifest.progress?.total,
|
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 || []),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.processed) {
|
||||||
|
processedEl.textContent = manifest.terminal.stats.processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const etaEl = document.getElementById(`terminal-eta-${taskId}`);
|
||||||
|
if (etaEl && manifest.terminal?.stats?.eta) {
|
||||||
|
etaEl.textContent = manifest.terminal.stats.eta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue