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
- ? `
-
`
- : ""
- }
`;
- // 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) {