From 9f922b523deef08f541d0336ed86feecc8588fd1 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Wed, 31 Dec 2025 23:45:38 -0300 Subject: [PATCH] Fix task progress UI: pixel-perfect design with dot indicators Frontend JS: - Replace text checkboxes with dot indicators - Add 'View Details' links to tree sections - Add HTMX afterSwap listener for pending manifest updates - Retry system for manifest updates when elements not yet loaded Frontend CSS: - taskmd.css uses CSS variables for theme compatibility - Dot indicators with pulse animation for running items - Progress bar under running sections - Pixel-perfect tree structure matching design mockup Theme: - Add complete TASKMD Progress Tree styles to theme-sentient.css - Dot pulse animation with accent glow - All components use theme variables --- ui/suite/css/theme-sentient.css | 291 ++++++++++++ ui/suite/index.html | 1 + ui/suite/tasks/autotask.html | 2 + ui/suite/tasks/autotask.js | 33 +- ui/suite/tasks/progress-panel.css | 555 ++++++++++++++++++++++ ui/suite/tasks/progress-panel.html | 103 ++++ ui/suite/tasks/progress-panel.js | 464 ++++++++++++++++++ ui/suite/tasks/taskmd.css | 728 +++++++++++++++++++++++++++++ ui/suite/tasks/tasks.css | 678 ++++++++++++++++++++++++++- ui/suite/tasks/tasks.html | 4 +- ui/suite/tasks/tasks.js | 423 ++++++++++++++++- 11 files changed, 3246 insertions(+), 36 deletions(-) create mode 100644 ui/suite/tasks/progress-panel.css create mode 100644 ui/suite/tasks/progress-panel.html create mode 100644 ui/suite/tasks/progress-panel.js create mode 100644 ui/suite/tasks/taskmd.css diff --git a/ui/suite/css/theme-sentient.css b/ui/suite/css/theme-sentient.css index 5e75548..6704f34 100644 --- a/ui/suite/css/theme-sentient.css +++ b/ui/suite/css/theme-sentient.css @@ -1668,3 +1668,294 @@ [data-theme="sentient"] .task-card.status-failed::before { background: var(--error); } + +/* ============================================ */ +/* TASKMD PROGRESS TREE - PIXEL PERFECT */ +/* ============================================ */ + +/* Main tree container */ +[data-theme="sentient"] .taskmd-tree { + display: flex; + flex-direction: column; + gap: 0; +} + +/* Tree Section (Level 0) - Main sections like "Database & Models" */ +[data-theme="sentient"] .tree-section { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + margin: 8px 16px; + overflow: hidden; +} + +[data-theme="sentient"] .tree-section:last-child { + margin-bottom: 16px; +} + +/* Section row */ +[data-theme="sentient"] .tree-row { + display: flex; + align-items: center; + padding: 16px 20px; + cursor: pointer; + transition: background 0.15s; +} + +[data-theme="sentient"] .tree-row:hover { + background: var(--surface-hover); +} + +[data-theme="sentient"] .tree-level-0 { + padding-left: 20px; +} + +[data-theme="sentient"] .tree-level-1 { + padding-left: 40px; + background: var(--bg-secondary); + border-top: 1px solid var(--border); +} + +/* Section/Child name */ +[data-theme="sentient"] .tree-name { + font-size: 14px; + font-weight: 500; + color: var(--text); +} + +[data-theme="sentient"] .tree-level-1 .tree-name { + font-size: 13px; + color: var(--text-secondary); +} + +/* View Details link */ +[data-theme="sentient"] .tree-view-details { + font-size: 12px; + color: var(--text-tertiary); + margin-left: 12px; + margin-right: auto; + cursor: pointer; + transition: color 0.15s; +} + +[data-theme="sentient"] .tree-view-details:hover { + color: var(--accent); +} + +/* Step badge - green pill */ +[data-theme="sentient"] .tree-step-badge { + padding: 4px 12px; + background: var(--accent); + color: var(--bg); + border-radius: 4px; + font-size: 11px; + font-weight: 600; + margin-right: 12px; + white-space: nowrap; +} + +[data-theme="sentient"] .tree-section.pending .tree-step-badge, +[data-theme="sentient"] .tree-child.pending .tree-step-badge { + background: var(--surface-active); + color: var(--text-tertiary); +} + +/* Status text */ +[data-theme="sentient"] .tree-status { + font-size: 12px; + color: var(--text-tertiary); + min-width: 80px; + text-align: right; +} + +[data-theme="sentient"] .tree-status.completed { + color: var(--text-secondary); +} + +[data-theme="sentient"] .tree-status.running { + color: var(--accent); +} + +[data-theme="sentient"] .tree-status.failed { + color: var(--error); +} + +/* Tree children container */ +[data-theme="sentient"] .tree-children { + display: none; + background: var(--bg); +} + +[data-theme="sentient"] .tree-section.expanded .tree-children { + display: block; +} + +/* Tree child row */ +[data-theme="sentient"] .tree-child { + border-bottom: 1px solid var(--border-light); +} + +[data-theme="sentient"] .tree-child:last-child { + border-bottom: none; +} + +/* Indent line */ +[data-theme="sentient"] .tree-indent { + width: 20px; + height: 1px; + background: var(--border); + margin-right: 12px; +} + +/* Tree items container */ +[data-theme="sentient"] .tree-items { + display: none; + padding: 8px 0 16px 0; + background: var(--bg); +} + +[data-theme="sentient"] .tree-child.expanded .tree-items { + display: block; +} + +/* Section-level items */ +[data-theme="sentient"] .tree-children > .tree-item { + padding-left: 40px; +} + +/* Individual item row */ +[data-theme="sentient"] .tree-item { + display: flex; + align-items: center; + padding: 10px 20px 10px 60px; + min-height: 36px; +} + +[data-theme="sentient"] .tree-item.running { + background: rgba(var(--accent-rgb), 0.03); +} + +/* Item dot indicator - the colored dots */ +[data-theme="sentient"] .tree-item-dot { + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 12px; + flex-shrink: 0; + background: var(--text-muted); + transition: all 0.2s; +} + +[data-theme="sentient"] .tree-item-dot.completed { + background: var(--accent); +} + +[data-theme="sentient"] .tree-item-dot.running { + background: var(--accent); + box-shadow: 0 0 8px var(--accent-glow); + animation: sentient-dot-pulse 1.5s ease-in-out infinite; +} + +[data-theme="sentient"] .tree-item-dot.pending { + background: var(--text-muted); +} + +[data-theme="sentient"] .tree-item-dot.failed { + background: var(--error); +} + +@keyframes sentient-dot-pulse { + 0%, + 100% { + opacity: 1; + box-shadow: 0 0 8px var(--accent-glow); + } + 50% { + opacity: 0.6; + box-shadow: 0 0 4px rgba(var(--accent-rgb), 0.2); + } +} + +/* Item name */ +[data-theme="sentient"] .tree-item-name { + flex: 1; + font-size: 13px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +[data-theme="sentient"] .tree-item.completed .tree-item-name { + color: var(--text-tertiary); +} + +[data-theme="sentient"] .tree-item.running .tree-item-name { + color: var(--text); +} + +/* Item duration */ +[data-theme="sentient"] .tree-item-duration { + font-size: 12px; + color: var(--text-muted); + margin-right: 8px; + white-space: nowrap; +} + +/* Item checkmark */ +[data-theme="sentient"] .tree-item-check { + font-size: 14px; + width: 20px; + text-align: center; + flex-shrink: 0; +} + +[data-theme="sentient"] .tree-item-check.completed { + color: var(--success); +} + +[data-theme="sentient"] .tree-item-check.running { + color: var(--accent); +} + +[data-theme="sentient"] .tree-item-check.pending { + color: transparent; +} + +/* Progress bar under running sections */ +[data-theme="sentient"] .tree-progress-bar-container { + display: flex; + align-items: center; + gap: 12px; + padding: 0 20px 16px 20px; + background: transparent; + position: relative; +} + +[data-theme="sentient"] .tree-progress-bar-container::before { + content: ""; + position: absolute; + left: 20px; + right: 70px; + height: 3px; + background: var(--border); + border-radius: 2px; +} + +[data-theme="sentient"] .tree-progress-bar { + flex: 1; + height: 3px; + background: var(--accent); + border-radius: 2px; + transition: width 0.3s ease-out; + position: relative; + z-index: 1; +} + +[data-theme="sentient"] .tree-progress-percent { + font-size: 11px; + font-weight: 600; + color: var(--accent); + min-width: 36px; + text-align: right; +} diff --git a/ui/suite/index.html b/ui/suite/index.html index 4e9ad28..fe93e11 100644 --- a/ui/suite/index.html +++ b/ui/suite/index.html @@ -26,6 +26,7 @@ + diff --git a/ui/suite/tasks/autotask.html b/ui/suite/tasks/autotask.html index 562e0f9..aaefb0e 100644 --- a/ui/suite/tasks/autotask.html +++ b/ui/suite/tasks/autotask.html @@ -476,4 +476,6 @@ Examples:
+ + diff --git a/ui/suite/tasks/autotask.js b/ui/suite/tasks/autotask.js index 02361cc..8aaa8b9 100644 --- a/ui/suite/tasks/autotask.js +++ b/ui/suite/tasks/autotask.js @@ -206,6 +206,11 @@ function initTaskProgressWebSocket() { } function handleTaskProgressMessage(data) { + // Forward to ProgressPanel if available + if (typeof ProgressPanel !== "undefined" && ProgressPanel.manifest) { + ProgressPanel.handleProgressUpdate(data); + } + console.log("Task progress:", data); switch (data.type) { @@ -394,11 +399,24 @@ function loadIntentDetail(intentId) { `; - // Fetch intent details - fetch(`/api/autotask/${intentId}`) - .then((response) => response.json()) - .then((data) => { - renderIntentDetail(data); + // Fetch intent details and manifest in parallel + Promise.all([ + fetch(`/api/autotask/${intentId}`).then((r) => r.json()), + fetch(`/api/autotask/${intentId}/manifest`) + .then((r) => r.json()) + .catch(() => null), + ]) + .then(([taskData, manifestData]) => { + renderIntentDetail(taskData); + + // If manifest exists, initialize ProgressPanel + if (manifestData && manifestData.success && manifestData.manifest) { + if (typeof ProgressPanel !== "undefined") { + ProgressPanel.manifest = manifestData.manifest; + ProgressPanel.init(intentId); + ProgressPanel.render(); + } + } }) .catch((error) => { console.error("Failed to load intent detail:", error); @@ -590,6 +608,11 @@ function toggleLogEntry(header) { } function closeDetailPanel() { + // Clean up ProgressPanel if active + if (typeof ProgressPanel !== "undefined") { + ProgressPanel.destroy(); + } + AutoTaskState.selectedIntentId = null; document.querySelectorAll(".intent-card").forEach((card) => { diff --git a/ui/suite/tasks/progress-panel.css b/ui/suite/tasks/progress-panel.css new file mode 100644 index 0000000..3c8e7b7 --- /dev/null +++ b/ui/suite/tasks/progress-panel.css @@ -0,0 +1,555 @@ +.progress-panel { + display: flex; + flex-direction: column; + gap: 24px; + padding: 20px; + background: var(--bg-primary, #0a0a0f); + color: var(--text-primary, #e0e0e0); + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; + border-radius: 12px; + border: 1px solid var(--border-color, #1a1a24); + height: 100%; + min-height: 0; + overflow: hidden; +} + +.status-section { + background: var(--bg-secondary, #111118); + border-radius: 8px; + padding: 16px 20px; + border: 1px solid var(--border-color, #1a1a24); + flex-shrink: 0; + min-height: 120px; +} + +.status-header { + margin-bottom: 12px; +} + +.status-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 1.2px; + color: var(--text-muted, #666); + text-transform: uppercase; +} + +.status-content { + display: flex; + flex-direction: column; + gap: 8px; +} + +.status-title-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +.status-title { + font-size: 16px; + font-weight: 500; + color: var(--text-primary, #e0e0e0); +} + +.status-time { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; +} + +.runtime-label, +.estimated-label { + color: var(--text-muted, #666); +} + +.runtime-value, +.estimated-value { + color: var(--text-primary, #e0e0e0); +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted, #666); +} + +.status-indicator.active { + background: var(--accent-yellow, #d4e94c); + box-shadow: 0 0 8px var(--accent-yellow, #d4e94c); +} + +.status-current-action { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; +} + +.action-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted, #666); + flex-shrink: 0; +} + +.action-dot.active { + background: var(--accent-yellow, #d4e94c); +} + +.action-text { + font-size: 14px; + color: var(--text-primary, #e0e0e0); + flex: 1; +} + +.estimated-time { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + margin-left: auto; +} + +.settings-icon { + color: var(--text-muted, #666); + cursor: pointer; + transition: color 0.2s; +} + +.settings-icon:hover { + color: var(--text-primary, #e0e0e0); +} + +.status-decision-point { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + opacity: 0.7; +} + +.decision-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted, #444); + flex-shrink: 0; +} + +.decision-dot.pending { + background: var(--text-muted, #444); + border: 1px dashed var(--text-muted, #666); +} + +.decision-text { + font-size: 13px; + color: var(--text-muted, #888); +} + +.decision-badge { + font-size: 12px; + padding: 4px 10px; + background: var(--bg-tertiary, #1a1a24); + border-radius: 4px; + color: var(--text-muted, #888); + margin-left: auto; +} + +.progress-log-section { + background: var(--bg-secondary, #111118); + border-radius: 8px; + border: 1px solid var(--border-color, #1a1a24); + overflow: hidden; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +.progress-log-header { + padding: 14px 20px; + border-bottom: 1px solid var(--border-color, #1a1a24); + flex-shrink: 0; +} + +.progress-log-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 1.2px; + color: var(--text-muted, #666); + text-transform: uppercase; +} + +.progress-log-content { + padding: 0; + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + +.log-section { + border-bottom: 1px solid var(--border-color, #1a1a24); + min-height: 48px; +} + +.log-section:last-child { + border-bottom: none; +} + +.log-section-header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 20px; + cursor: pointer; + transition: background 0.2s; + min-height: 48px; +} + +.log-section-header:hover { + background: var(--bg-hover, #151520); +} + +.section-indicator { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.section-indicator.completed { + background: var(--accent-yellow, #d4e94c); +} + +.section-indicator.running { + background: var(--accent-blue, #4c9ee9); + animation: pulse 1.5s infinite; +} + +.section-indicator.pending { + background: var(--text-muted, #444); +} + +.section-indicator.failed { + background: var(--accent-red, #e94c4c); +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.section-name { + font-size: 14px; + font-weight: 500; + color: var(--text-primary, #e0e0e0); +} + +.section-details-link { + font-size: 12px; + color: var(--text-muted, #666); + cursor: pointer; + transition: color 0.2s; +} + +.section-details-link:hover { + color: var(--accent-yellow, #d4e94c); +} + +.section-step-badge { + margin-left: auto; + font-size: 12px; + padding: 4px 10px; + background: var(--accent-yellow, #d4e94c); + color: var(--bg-primary, #0a0a0f); + border-radius: 4px; + font-weight: 600; +} + +.section-status-badge { + font-size: 12px; + padding: 4px 10px; + border-radius: 4px; + font-weight: 500; +} + +.section-status-badge.completed { + background: transparent; + color: var(--text-muted, #888); +} + +.section-status-badge.running { + background: var(--accent-blue, #4c9ee9); + color: var(--bg-primary, #0a0a0f); +} + +.section-status-badge.pending { + background: var(--bg-tertiary, #1a1a24); + color: var(--text-muted, #666); +} + +.log-section-body { + display: none; + padding-left: 20px; + background: var(--bg-tertiary, #0d0d14); +} + +.log-section.expanded .log-section-body { + display: block; +} + +.log-children { + padding: 0; +} + +.log-child { + border-bottom: 1px solid var(--border-subtle, #151520); +} + +.log-child:last-child { + border-bottom: none; +} + +.log-child-header { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 20px 12px 32px; + cursor: pointer; + transition: background 0.2s; +} + +.log-child-header:hover { + background: var(--bg-hover, #131320); +} + +.child-indent { + width: 16px; + height: 1px; + background: var(--border-color, #1a1a24); + flex-shrink: 0; +} + +.child-name { + font-size: 13px; + color: var(--text-secondary, #aaa); +} + +.child-details-link { + font-size: 11px; + color: var(--text-muted, #555); + cursor: pointer; + transition: color 0.2s; +} + +.child-details-link:hover { + color: var(--accent-yellow, #d4e94c); +} + +.child-step-badge { + margin-left: auto; + font-size: 11px; + padding: 3px 8px; + background: var(--accent-yellow, #d4e94c); + color: var(--bg-primary, #0a0a0f); + border-radius: 4px; + font-weight: 600; +} + +.child-status-badge { + font-size: 11px; + padding: 3px 8px; + border-radius: 4px; +} + +.child-status-badge.completed { + color: var(--text-muted, #888); +} + +.log-child-body { + display: none; + padding-left: 48px; +} + +.log-child.expanded .log-child-body { + display: block; +} + +.log-items { + padding: 8px 0; +} + +.log-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 20px; + font-size: 12px; +} + +.item-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} + +.item-dot.completed { + background: var(--accent-yellow, #d4e94c); +} + +.item-dot.running { + background: var(--accent-blue, #4c9ee9); +} + +.item-dot.pending { + background: var(--text-muted, #444); +} + +.item-name { + color: var(--text-secondary, #999); + flex: 1; +} + +.item-info { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +} + +.item-duration { + font-size: 11px; + color: var(--text-muted, #666); +} + +.item-check { + font-size: 14px; +} + +.item-check.completed { + color: var(--accent-green, #4ce97a); +} + +.item-check.running { + color: var(--accent-blue, #4c9ee9); +} + +.terminal-section { + background: var(--bg-terminal, #0a0a0f); + border-radius: 8px; + border: 1px solid var(--border-color, #1a1a24); + overflow: hidden; + flex-shrink: 0; + min-height: 150px; + max-height: 250px; + display: flex; + flex-direction: column; +} + +.terminal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-bottom: 1px solid var(--border-color, #1a1a24); + background: var(--bg-secondary, #111118); + flex-shrink: 0; +} + +.terminal-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 1.2px; + color: var(--text-muted, #666); + text-transform: uppercase; +} + +.terminal-stats { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--text-muted, #666); +} + +.terminal-stats strong { + color: var(--text-primary, #e0e0e0); + font-weight: 500; +} + +.stat-separator { + margin: 0 8px; + color: var(--text-muted, #444); +} + +.terminal-content { + padding: 16px 20px; + font-family: "JetBrains Mono", "Fira Code", "Monaco", monospace; + font-size: 13px; + line-height: 1.6; + flex: 1; + min-height: 0; + overflow-y: auto; + background: var(--bg-terminal, #0a0a0f); +} + +.terminal-line { + color: var(--text-terminal, #8b8b8b); + padding: 2px 0; +} + +.terminal-line.info { + color: var(--text-terminal, #8b8b8b); +} + +.terminal-line.success { + color: var(--accent-green, #4ce97a); +} + +.terminal-line.error { + color: var(--accent-red, #e94c4c); +} + +.terminal-line.warning { + color: var(--accent-yellow, #d4e94c); +} + +.terminal-line.progress { + color: var(--accent-blue, #4c9ee9); +} + +.progress-log-content::-webkit-scrollbar, +.terminal-content::-webkit-scrollbar { + width: 6px; +} + +.progress-log-content::-webkit-scrollbar-track, +.terminal-content::-webkit-scrollbar-track { + background: var(--bg-tertiary, #0d0d14); +} + +.progress-log-content::-webkit-scrollbar-thumb, +.terminal-content::-webkit-scrollbar-thumb { + background: var(--border-color, #1a1a24); + border-radius: 3px; +} + +.progress-log-content::-webkit-scrollbar-thumb:hover, +.terminal-content::-webkit-scrollbar-thumb:hover { + background: var(--text-muted, #444); +} diff --git a/ui/suite/tasks/progress-panel.html b/ui/suite/tasks/progress-panel.html new file mode 100644 index 0000000..b3e0aea --- /dev/null +++ b/ui/suite/tasks/progress-panel.html @@ -0,0 +1,103 @@ +
+
+
+ STATUS +
+
+
+ Implement User Authentication System +
+ Runtime: + 42 min + +
+
+
+ + Choose Token Expiration Strategy +
+ Estimated: + 15 min + +
+
+
+ + Decision Point Coming (Step 26/60) + Will need approval for security configuration +
+
+
+ +
+
+ PROGRESS LOG +
+
+
+
+ +
+
+ TERMINAL (LIVE AGENT ACTIVITY) +
+ Processed: 127 data points + / + Processing speed: ~8 sources/min + + Estimated completion: 6 minutes +
+
+
+
Synthesizing competitive insights...
+
Cross-referencing pricing data...
+
+
+
+ + + + + + + + diff --git a/ui/suite/tasks/progress-panel.js b/ui/suite/tasks/progress-panel.js new file mode 100644 index 0000000..5aec84a --- /dev/null +++ b/ui/suite/tasks/progress-panel.js @@ -0,0 +1,464 @@ +const ProgressPanel = { + manifest: null, + wsConnection: null, + startTime: null, + runtimeInterval: null, + + init(taskId) { + this.taskId = taskId; + this.startTime = Date.now(); + this.startRuntimeCounter(); + this.connectWebSocket(taskId); + }, + + connectWebSocket(taskId) { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/task-progress/${taskId}`; + + this.wsConnection = new WebSocket(wsUrl); + + this.wsConnection.onopen = () => { + console.log('Progress panel WebSocket connected'); + }; + + this.wsConnection.onmessage = (event) => { + const data = JSON.parse(event.data); + this.handleProgressUpdate(data); + }; + + this.wsConnection.onclose = () => { + console.log('Progress panel WebSocket closed'); + setTimeout(() => this.connectWebSocket(taskId), 3000); + }; + + this.wsConnection.onerror = (error) => { + console.error('Progress panel WebSocket error:', error); + }; + }, + + handleProgressUpdate(data) { + if (data.type === 'manifest_update') { + this.manifest = data.manifest; + this.render(); + } else if (data.type === 'section_update') { + this.updateSection(data.section_id, data.status, data.progress); + } else if (data.type === 'item_update') { + this.updateItem(data.section_id, data.item_id, data.status, data.duration); + } else if (data.type === 'terminal_line') { + this.addTerminalLine(data.content, data.line_type); + } else if (data.type === 'stats_update') { + this.updateStats(data.stats); + } else if (data.type === 'task_progress') { + this.handleTaskProgress(data); + } + }, + + handleTaskProgress(data) { + if (data.activity && data.activity.manifest) { + this.manifest = data.activity.manifest; + this.render(); + } + + if (data.step) { + this.updateCurrentAction(data.message || data.step); + } + + if (data.details) { + this.addTerminalLine(data.details, 'info'); + } + }, + + startRuntimeCounter() { + this.runtimeInterval = setInterval(() => { + const elapsed = Math.floor((Date.now() - this.startTime) / 1000); + const runtimeEl = document.getElementById('status-runtime'); + if (runtimeEl) { + runtimeEl.textContent = this.formatDuration(elapsed); + } + }, 1000); + }, + + stopRuntimeCounter() { + if (this.runtimeInterval) { + clearInterval(this.runtimeInterval); + this.runtimeInterval = null; + } + }, + + formatDuration(seconds) { + if (seconds < 60) { + return `${seconds} sec`; + } else if (seconds < 3600) { + const mins = Math.floor(seconds / 60); + return `${mins} min`; + } else { + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return `${hours} hr ${mins} min`; + } + }, + + render() { + if (!this.manifest) return; + + this.renderStatus(); + this.renderProgressLog(); + this.renderTerminal(); + }, + + renderStatus() { + const titleEl = document.getElementById('status-title'); + if (titleEl) { + titleEl.textContent = this.manifest.description || this.manifest.app_name; + } + + const estimatedEl = document.getElementById('estimated-time'); + if (estimatedEl && this.manifest.estimated_seconds) { + estimatedEl.textContent = this.formatDuration(this.manifest.estimated_seconds); + } + + const currentAction = this.getCurrentAction(); + const actionEl = document.getElementById('current-action'); + if (actionEl && currentAction) { + actionEl.textContent = currentAction; + } + + this.updateDecisionPoint(); + }, + + getCurrentAction() { + if (!this.manifest || !this.manifest.sections) return null; + + for (const section of this.manifest.sections) { + if (section.status === 'Running') { + for (const child of section.children || []) { + if (child.status === 'Running') { + for (const item of child.items || []) { + if (item.status === 'Running') { + return item.name; + } + } + return child.name; + } + } + return section.name; + } + } + return null; + }, + + updateCurrentAction(action) { + const actionEl = document.getElementById('current-action'); + if (actionEl) { + actionEl.textContent = action; + } + }, + + updateDecisionPoint() { + const decisionStepEl = document.getElementById('decision-step'); + const decisionTotalEl = document.getElementById('decision-total'); + + if (decisionStepEl && this.manifest) { + decisionStepEl.textContent = this.manifest.completed_steps || 0; + } + if (decisionTotalEl && this.manifest) { + decisionTotalEl.textContent = this.manifest.total_steps || 0; + } + }, + + renderProgressLog() { + const container = document.getElementById('progress-log-content'); + if (!container || !this.manifest || !this.manifest.sections) return; + + container.innerHTML = ''; + + for (const section of this.manifest.sections) { + const sectionEl = this.createSectionElement(section); + container.appendChild(sectionEl); + } + }, + + createSectionElement(section) { + const sectionDiv = document.createElement('div'); + sectionDiv.className = 'log-section'; + sectionDiv.dataset.sectionId = section.id; + + if (section.status === 'Running' || section.status === 'Completed') { + sectionDiv.classList.add('expanded'); + } + + const statusClass = section.status.toLowerCase(); + const stepCurrent = section.current_step || 0; + const stepTotal = section.total_steps || 0; + + sectionDiv.innerHTML = ` +
+ + ${this.escapeHtml(section.name)} + View Details ▸ + Step ${stepCurrent}/${stepTotal} + ${section.status} +
+
+
+
+
+ `; + + const childrenContainer = sectionDiv.querySelector('.log-children'); + + for (const child of section.children || []) { + const childEl = this.createChildElement(child, section.id); + childrenContainer.appendChild(childEl); + } + + if (section.items && section.items.length > 0 && (!section.children || section.children.length === 0)) { + for (const item of section.items) { + const itemEl = this.createItemElement(item); + childrenContainer.appendChild(itemEl); + } + } + + return sectionDiv; + }, + + createChildElement(child, parentId) { + const childDiv = document.createElement('div'); + childDiv.className = 'log-child'; + childDiv.dataset.childId = child.id; + + if (child.status === 'Running' || child.status === 'Completed') { + childDiv.classList.add('expanded'); + } + + const statusClass = child.status.toLowerCase(); + const stepCurrent = child.current_step || 0; + const stepTotal = child.total_steps || 0; + const duration = child.duration_seconds ? this.formatDuration(child.duration_seconds) : ''; + + childDiv.innerHTML = ` +
+ + ${this.escapeHtml(child.name)} + View Details ▸ + Step ${stepCurrent}/${stepTotal} + ${child.status} +
+
+
+
+
+ `; + + const itemsContainer = childDiv.querySelector('.log-items'); + + for (const item of child.items || []) { + const itemEl = this.createItemElement(item); + itemsContainer.appendChild(itemEl); + } + + return childDiv; + }, + + createItemElement(item) { + const itemDiv = document.createElement('div'); + itemDiv.className = 'log-item'; + itemDiv.dataset.itemId = item.id; + + const statusClass = item.status.toLowerCase(); + const duration = item.duration_seconds ? `Duration: ${this.formatDuration(item.duration_seconds)}` : ''; + const checkIcon = item.status === 'Completed' ? '✓' : (item.status === 'Running' ? '◎' : '○'); + + itemDiv.innerHTML = ` + + ${this.escapeHtml(item.name)}${item.details ? ` - ${this.escapeHtml(item.details)}` : ''} +
+ ${duration} + ${checkIcon} +
+ `; + + return itemDiv; + }, + + renderTerminal() { + if (!this.manifest || !this.manifest.terminal_output) return; + + const container = document.getElementById('terminal-content'); + if (!container) return; + + container.innerHTML = ''; + + for (const line of this.manifest.terminal_output.slice(-50)) { + this.appendTerminalLine(container, line.content, line.line_type || 'info'); + } + + container.scrollTop = container.scrollHeight; + }, + + addTerminalLine(content, lineType) { + const container = document.getElementById('terminal-content'); + if (!container) return; + + this.appendTerminalLine(container, content, lineType); + container.scrollTop = container.scrollHeight; + + this.incrementProcessedCount(); + }, + + appendTerminalLine(container, content, lineType) { + const lineDiv = document.createElement('div'); + lineDiv.className = `terminal-line ${lineType || 'info'}`; + lineDiv.textContent = content; + container.appendChild(lineDiv); + }, + + incrementProcessedCount() { + const processedEl = document.getElementById('terminal-processed'); + if (processedEl) { + const current = parseInt(processedEl.textContent, 10) || 0; + processedEl.textContent = current + 1; + } + }, + + updateStats(stats) { + const processedEl = document.getElementById('terminal-processed'); + if (processedEl && stats.data_points_processed !== undefined) { + processedEl.textContent = stats.data_points_processed; + } + + const speedEl = document.getElementById('terminal-speed'); + if (speedEl && stats.sources_per_min !== undefined) { + speedEl.textContent = `~${stats.sources_per_min.toFixed(1)} sources/min`; + } + + const etaEl = document.getElementById('terminal-eta'); + if (etaEl && stats.estimated_remaining_seconds !== undefined) { + etaEl.textContent = this.formatDuration(stats.estimated_remaining_seconds); + } + }, + + updateSection(sectionId, status, progress) { + const sectionEl = document.querySelector(`[data-section-id="${sectionId}"]`); + if (!sectionEl) return; + + const indicator = sectionEl.querySelector('.section-indicator'); + const statusBadge = sectionEl.querySelector('.section-status-badge'); + const stepBadge = sectionEl.querySelector('.section-step-badge'); + + if (indicator) { + indicator.className = `section-indicator ${status.toLowerCase()}`; + } + + if (statusBadge) { + statusBadge.className = `section-status-badge ${status.toLowerCase()}`; + statusBadge.textContent = status; + } + + if (stepBadge && progress) { + stepBadge.textContent = `Step ${progress.current}/${progress.total}`; + } + + if (status === 'Running' || status === 'Completed') { + sectionEl.classList.add('expanded'); + } + }, + + updateItem(sectionId, itemId, status, duration) { + const itemEl = document.querySelector(`[data-item-id="${itemId}"]`); + if (!itemEl) return; + + const dot = itemEl.querySelector('.item-dot'); + const check = itemEl.querySelector('.item-check'); + const durationEl = itemEl.querySelector('.item-duration'); + + const statusClass = status.toLowerCase(); + + if (dot) { + dot.className = `item-dot ${statusClass}`; + } + + if (check) { + check.className = `item-check ${statusClass}`; + check.textContent = status === 'Completed' ? '✓' : (status === 'Running' ? '◎' : '○'); + } + + if (durationEl && duration) { + durationEl.textContent = `Duration: ${this.formatDuration(duration)}`; + } + }, + + toggleSection(sectionId) { + const sectionEl = document.querySelector(`[data-section-id="${sectionId}"]`); + if (sectionEl) { + sectionEl.classList.toggle('expanded'); + } + }, + + toggleChild(childId) { + const childEl = document.querySelector(`[data-child-id="${childId}"]`); + if (childEl) { + childEl.classList.toggle('expanded'); + } + }, + + viewDetails(sectionId) { + console.log('View details for section:', sectionId); + }, + + viewChildDetails(childId) { + console.log('View details for child:', childId); + }, + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }, + + loadManifest(taskId) { + fetch(`/api/autotask/${taskId}/manifest`) + .then(response => response.json()) + .then(data => { + if (data.success && data.manifest) { + this.manifest = data.manifest; + this.render(); + } + }) + .catch(error => { + console.error('Failed to load manifest:', error); + }); + }, + + destroy() { + this.stopRuntimeCounter(); + if (this.wsConnection) { + this.wsConnection.close(); + this.wsConnection = null; + } + } +}; + +function toggleLogSection(header) { + const section = header.closest('.log-section'); + if (section) { + section.classList.toggle('expanded'); + } +} + +function toggleLogChild(header) { + const child = header.closest('.log-child'); + if (child) { + child.classList.toggle('expanded'); + } +} + +function viewSectionDetails(sectionId) { + ProgressPanel.viewDetails(sectionId); +} + +function viewChildDetails(childId) { + ProgressPanel.viewChildDetails(childId); +} + +window.ProgressPanel = ProgressPanel; diff --git a/ui/suite/tasks/taskmd.css b/ui/suite/tasks/taskmd.css new file mode 100644 index 0000000..19f0a99 --- /dev/null +++ b/ui/suite/tasks/taskmd.css @@ -0,0 +1,728 @@ +/* ============================================================================= + TASK.MD STYLED VIEW - Exact match to design mockup + Uses CSS variables for theme compatibility + ============================================================================= */ + +/* Task Detail Panel - Fixed scrolling container */ +.task-detail-panel { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; +} + +/* Main Container */ +.task-detail-rich { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + background: var(--bg, #0a0a0a); + color: var(--text-secondary, #e0e0e0); + font-family: var( + --sentient-font-family, + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif + ); + overflow-y: auto; + overflow-x: hidden; +} + +/* Header */ +.taskmd-header { + padding: 20px 24px; + border-bottom: 1px solid var(--border, #1a1a1a); +} + +.taskmd-url { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--text-tertiary, #666); + margin-bottom: 12px; +} + +.taskmd-url .url-icon { + font-size: 14px; +} + +.taskmd-url .url-path { + color: var(--text-secondary, #888); + font-family: monospace; +} + +.taskmd-title { + font-size: 24px; + font-weight: 500; + color: var(--text, #fff); + margin: 0 0 12px 0; +} + +.taskmd-status-badge { + display: inline-block; + padding: 6px 16px; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.taskmd-status-badge.status-running { + background: var(--accent, #c5f82a); + color: var(--bg, #0a0a0a); +} + +.taskmd-status-badge.status-completed { + background: var(--success, #22c55e); + color: #fff; +} + +.taskmd-status-badge.status-error { + background: var(--error, #ef4444); + color: #fff; +} + +.taskmd-status-badge.status-pending { + background: var(--surface-active, #333); + color: var(--text-secondary, #888); +} + +/* Section Container */ +.taskmd-section { + border-bottom: 1px solid var(--border, #1a1a1a); +} + +.taskmd-section-header { + padding: 16px 24px; + font-size: 11px; + font-weight: 600; + letter-spacing: 1.5px; + color: var(--text-tertiary, #666); + text-transform: uppercase; + background: var(--bg-secondary, #0d0d0d); +} + +/* STATUS Section */ +.taskmd-status-content { + padding: 16px 24px; + background: var(--surface, #111); +} + +.status-row { + display: flex; + align-items: center; + padding: 10px 0; +} + +.status-row.status-main { + padding-bottom: 12px; +} + +.status-row.status-current { + padding-left: 8px; +} + +.status-row.status-decision { + padding-left: 8px; + opacity: 0.7; +} + +.status-title { + flex: 1; + font-size: 15px; + color: var(--text, #fff); +} + +.status-text { + flex: 1; + font-size: 14px; + color: var(--text-secondary, #e0e0e0); +} + +.status-time { + font-size: 13px; + color: var(--text-tertiary, #666); +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 12px; + flex-shrink: 0; +} + +.status-dot.active { + background: var(--accent, #c5f82a); + box-shadow: 0 0 8px var(--accent-glow, rgba(197, 248, 42, 0.5)); +} + +.status-dot.pending { + background: transparent; + border: 1px dashed var(--text-muted, #444); +} + +.status-badge { + padding: 4px 12px; + background: var(--surface-hover, #1a1a1a); + border-radius: 4px; + font-size: 12px; + color: var(--text-secondary, #888); + margin-left: 12px; +} + +/* PROGRESS LOG Section */ +.taskmd-progress-content { + background: var(--bg, #0a0a0a); + flex-shrink: 0; +} + +.taskmd-tree { + display: flex; + flex-direction: column; +} + +/* Tree Section (Level 0) - Main sections like "Database & Models" */ +.tree-section { + border: 1px solid var(--border, #1a1a1a); + background: var(--surface, #111); + border-radius: 8px; + margin: 8px 16px; + overflow: hidden; +} + +.tree-section:last-child { + margin-bottom: 16px; +} + +.tree-row { + display: flex; + align-items: center; + padding: 16px 20px; + cursor: pointer; + transition: background 0.15s; +} + +.tree-row:hover { + background: var(--surface-hover, rgba(255, 255, 255, 0.02)); +} + +.tree-level-0 { + padding-left: 20px; +} + +.tree-level-1 { + padding-left: 40px; + background: var(--bg-secondary, #0d0d0d); + border-top: 1px solid var(--border, #1a1a1a); +} + +.tree-level-1 .tree-name { + font-size: 13px; + color: var(--text-secondary, #ccc); +} + +.tree-level-1 .tree-step-badge { + background: var(--accent, #c5f82a); + font-size: 11px; + padding: 3px 10px; +} + +.tree-level-1.pending .tree-step-badge { + background: var(--surface-active, #2a2a2a); + color: var(--text-tertiary, #666); +} + +/* View Details link */ +.tree-view-details { + font-size: 12px; + color: var(--text-tertiary, #666); + margin-left: 12px; + margin-right: auto; + cursor: pointer; + transition: color 0.15s; +} + +.tree-view-details:hover { + color: var(--accent, #c5f82a); +} + +.tree-name { + font-size: 14px; + font-weight: 500; + color: var(--text, #e0e0e0); +} + +.tree-step-badge { + padding: 4px 12px; + background: var(--accent, #c5f82a); + color: var(--bg, #0a0a0a); + border-radius: 4px; + font-size: 11px; + font-weight: 600; + margin-right: 12px; + white-space: nowrap; +} + +.tree-section.pending .tree-step-badge, +.tree-child.pending .tree-step-badge { + background: var(--surface-active, #2a2a2a); + color: var(--text-tertiary, #666); +} + +.tree-status { + font-size: 12px; + color: var(--text-tertiary, #666); + min-width: 80px; + text-align: right; +} + +.tree-status.completed { + color: var(--text-secondary, #888); +} + +.tree-status.running { + color: var(--accent, #c5f82a); +} + +.tree-status.failed { + color: var(--error, #ef4444); +} + +/* Tree Children */ +.tree-children { + display: none; + background: var(--bg, #0a0a0a); +} + +.tree-section.expanded .tree-children { + display: block; +} + +/* Tree Child (Level 1) - Sub-sections like "Database Schema Design" */ +.tree-child { + border-bottom: 1px solid var(--border-light, #151515); +} + +.tree-child:last-child { + border-bottom: none; +} + +.tree-indent { + width: 20px; + height: 1px; + background: var(--border, #2a2a2a); + margin-right: 12px; +} + +/* Tree Items (Level 2) - Individual items like files */ +.tree-items { + display: none; + padding: 8px 0 16px 0; + background: var(--bg, #080808); +} + +.tree-child.expanded .tree-items { + display: block; +} + +/* Section-level items */ +.tree-children > .tree-item { + padding-left: 40px; +} + +/* Item Dot Indicator - the colored dots */ +.tree-item-dot { + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 12px; + flex-shrink: 0; + background: var(--text-muted, #333); + transition: all 0.2s; +} + +.tree-item-dot.completed { + background: var(--accent, #c5f82a); +} + +.tree-item-dot.running { + background: var(--accent, #c5f82a); + box-shadow: 0 0 8px var(--accent-glow, rgba(197, 248, 42, 0.6)); + animation: dot-pulse 1.5s ease-in-out infinite; +} + +.tree-item-dot.pending { + background: var(--text-muted, #333); +} + +.tree-item-dot.failed { + background: var(--error, #ef4444); +} + +@keyframes dot-pulse { + 0%, + 100% { + opacity: 1; + box-shadow: 0 0 8px var(--accent-glow, rgba(197, 248, 42, 0.6)); + } + 50% { + opacity: 0.6; + box-shadow: 0 0 4px var(--accent-glow, rgba(197, 248, 42, 0.3)); + } +} + +.tree-item-name { + flex: 1; + font-size: 13px; + color: var(--text-secondary, #888); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.tree-item-duration { + font-size: 12px; + color: var(--text-muted, #555); + margin-right: 8px; + white-space: nowrap; +} + +.tree-item-check { + font-size: 14px; + width: 20px; + text-align: center; + flex-shrink: 0; +} + +.tree-item-check.completed { + color: var(--success, #22c55e); +} + +.tree-item-check.running { + color: var(--accent, #c5f82a); +} + +.tree-item-check.pending { + color: transparent; +} + +/* Item row with duration and checkmark aligned right */ +.tree-item { + display: flex; + align-items: center; + padding: 10px 20px 10px 60px; + min-height: 36px; +} + +.tree-item.completed .tree-item-name { + color: var(--text-tertiary, #888); +} + +.tree-item.running .tree-item-name { + color: var(--text, #e0e0e0); +} + +.tree-item.running { + background: var(--accent-light, rgba(197, 248, 42, 0.03)); +} + +/* TERMINAL Section */ +.taskmd-terminal { + flex: 1; + display: flex; + flex-direction: column; + min-height: 150px; + max-height: 300px; + overflow: hidden; +} + +.taskmd-terminal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 24px; + background: var(--bg-secondary, #0d0d0d); + border-bottom: 1px solid var(--border, #1a1a1a); +} + +.taskmd-terminal-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 11px; + font-weight: 600; + letter-spacing: 1.5px; + color: var(--text-tertiary, #666); + text-transform: uppercase; +} + +.terminal-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted, #333); +} + +.terminal-dot.active { + background: var(--accent, #c5f82a); + box-shadow: 0 0 8px var(--accent-glow, rgba(197, 248, 42, 0.5)); + animation: dot-pulse 1.5s infinite; +} + +.taskmd-terminal-stats { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--text-muted, #555); +} + +.taskmd-terminal-stats strong { + color: var(--text-secondary, #e0e0e0); + font-weight: 500; +} + +.stat-sep { + color: var(--text-muted, #333); +} + +.taskmd-terminal-output { + flex: 1; + padding: 16px 24px; + background: var(--bg, #0a0a0a); + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 13px; + line-height: 1.7; + color: var(--text-secondary, #888); + overflow-y: auto; + min-height: 0; +} + +.taskmd-terminal-output .terminal-line { + padding: 3px 0; + white-space: pre-wrap; + word-break: break-word; +} + +.taskmd-terminal-output .terminal-line.success { + color: var(--success, #22c55e); +} + +.taskmd-terminal-output .terminal-line.error { + color: var(--error, #ef4444); +} + +.taskmd-terminal-output .terminal-line.progress { + color: var(--accent, #c5f82a); + font-weight: 600; +} + +/* Markdown-like header styling */ +.taskmd-terminal-output .terminal-line:has-text("##"), +.taskmd-terminal-output .terminal-line[data-type="header"] { + color: #fff; + font-weight: 600; + margin-top: 12px; + margin-bottom: 4px; + font-size: 14px; +} + +/* Code block styling */ +.taskmd-terminal-output .terminal-code { + background: #151515; + border: 1px solid #252525; + border-radius: 4px; + padding: 8px 12px; + margin: 6px 0; + font-size: 12px; + color: #aaa; + overflow-x: auto; +} + +/* Checkmark items */ +.taskmd-terminal-output .terminal-line.success::before { + content: ""; +} + +/* List items */ +.taskmd-terminal-output .terminal-line.info { + color: #888; +} + +/* Bold text in terminal (markdown **text**) */ +.taskmd-terminal-output strong, +.taskmd-terminal-output b { + color: #fff; + font-weight: 600; +} + +/* Inline code (markdown `code`) */ +.taskmd-terminal-output code { + background: #1a1a1a; + padding: 2px 6px; + border-radius: 3px; + font-size: 12px; + color: #c5f82a; +} + +/* Timestamp styling */ +.taskmd-terminal-output .terminal-timestamp { + color: #444; + font-size: 11px; + margin-right: 8px; +} + +/* Actions */ +.taskmd-actions { + display: flex; + gap: 12px; + padding: 20px 24px; + border-top: 1px solid var(--border, #1a1a1a); + background: var(--bg-secondary, #0d0d0d); +} + +/* Empty State */ +.progress-empty { + padding: 40px 24px; + text-align: center; + color: #555; + font-size: 14px; +} + +/* Error Alert */ +.error-alert { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 24px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid #ef4444; + border-radius: 6px; + margin: 16px 24px; +} + +.error-alert .error-icon { + color: #ef4444; + font-size: 18px; +} + +.error-alert .error-text { + color: #ef4444; + font-size: 14px; +} + +/* Scrollbar */ +.task-detail-rich::-webkit-scrollbar, +.taskmd-terminal-output::-webkit-scrollbar { + width: 6px; +} + +.task-detail-rich::-webkit-scrollbar-track, +.taskmd-terminal-output::-webkit-scrollbar-track { + background: #0a0a0a; +} + +.task-detail-rich::-webkit-scrollbar-thumb, +.taskmd-terminal-output::-webkit-scrollbar-thumb { + background: #222; + border-radius: 3px; +} + +.task-detail-rich::-webkit-scrollbar-thumb:hover, +.taskmd-terminal-output::-webkit-scrollbar-thumb:hover { + background: #333; +} + +/* ============================================================================= + PROGRESS BAR - Shown under running sections + ============================================================================= */ + +.tree-progress-bar-container { + display: flex; + align-items: center; + gap: 12px; + padding: 0 20px 16px 20px; + background: transparent; +} + +.tree-progress-bar-container::before { + content: ""; + position: absolute; + left: 20px; + right: 70px; + height: 3px; + background: #1a1a1a; + border-radius: 2px; +} + +.tree-progress-bar { + flex: 1; + height: 3px; + background: #c5f82a; + border-radius: 2px; + transition: width 0.3s ease-out; + position: relative; + z-index: 1; +} + +.tree-progress-percent { + font-size: 11px; + font-weight: 600; + color: #c5f82a; + min-width: 36px; + text-align: right; +} + +/* ============================================================================= + PREVENT HEIGHT FLASHING - Stable layout during updates + ============================================================================= */ + +/* STATUS section - fixed height, no collapse */ +.taskmd-section:first-of-type { + flex-shrink: 0; +} + +.taskmd-status-content { + min-height: 100px; +} + +/* Progress log section - stable container */ +.taskmd-section:has(.taskmd-progress-content) { + flex-shrink: 0; +} + +/* Tree sections maintain minimum height */ +.tree-section { + min-height: 48px; +} + +.tree-row { + min-height: 48px; +} + +/* Prevent layout shift when sections expand/collapse */ +.tree-children { + will-change: height; +} + +.tree-items { + will-change: height; +} + +/* Smooth transitions for expand/collapse */ +.tree-section .tree-children, +.tree-child .tree-items { + transition: none; +} + +/* Actions bar - always visible at bottom */ +.taskmd-actions { + flex-shrink: 0; + margin-top: auto; +} diff --git a/ui/suite/tasks/tasks.css b/ui/suite/tasks/tasks.css index 641a136..5078c26 100644 --- a/ui/suite/tasks/tasks.css +++ b/ui/suite/tasks/tasks.css @@ -272,7 +272,7 @@ .tasks-main { display: grid; - grid-template-columns: 1fr 480px; + grid-template-columns: 30% 70%; flex: 1; overflow: hidden; } @@ -809,6 +809,8 @@ flex-direction: column; background: var(--surface, #111); overflow: hidden; + height: 100%; + min-height: 0; } /* Detail Header */ @@ -1573,7 +1575,7 @@ /* Responsive Design */ @media (max-width: 1200px) { .tasks-main { - grid-template-columns: 1fr 400px; + grid-template-columns: 35% 65%; } .terminal-stats { @@ -2106,7 +2108,49 @@ gap: 16px; padding: 20px; height: 100%; + min-height: 0; overflow-y: auto; + overflow-x: hidden; +} + +/* Task URL Bar - Fixed at top */ +.task-url-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: var(--surface, #161616); + border: 1px solid var(--border, #2a2a2a); + border-radius: 8px; + font-family: monospace; +} + +.task-url-bar .url-icon { + font-size: 14px; + opacity: 0.6; +} + +.task-url-bar .url-text { + flex: 1; + font-size: 0.85rem; + color: var(--text-secondary, #888); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-url-bar .btn-copy-url { + background: transparent; + border: none; + padding: 4px 8px; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.2s; + font-size: 14px; +} + +.task-url-bar .btn-copy-url:hover { + opacity: 1; } .detail-header-rich { @@ -2319,19 +2363,29 @@ .progress-info-rich { display: flex; - justify-content: flex-end; - margin-top: 6px; + justify-content: space-between; + align-items: center; + margin-top: 8px; } .progress-label-rich { + font-size: 0.85rem; + color: var(--text, #fff); + font-weight: 600; +} + +.progress-runtime { font-size: 0.8rem; color: var(--text-secondary, #888); + font-family: monospace; } /* Progress Log Section */ .progress-log-content { - max-height: 300px; + flex: 1; + min-height: 0; overflow-y: auto; + overflow-x: hidden; } .log-group { @@ -2576,6 +2630,19 @@ color: var(--success, #22c55e); } +.terminal-line.llm-stream { + color: var(--accent, #a78bfa); + font-family: "Fira Code", "Monaco", monospace; + font-size: 0.85rem; + opacity: 0.9; +} + +.terminal-line.llm-stream .llm-text { + color: var(--text-secondary, #a1a1a1); + white-space: pre-wrap; + word-break: break-word; +} + .terminal-footer-rich { margin-top: 14px; padding-top: 14px; @@ -2608,6 +2675,8 @@ /* Action Buttons */ .detail-actions-rich { display: flex; + align-items: center; + justify-content: flex-start; gap: 14px; margin-top: auto; padding-top: 20px; @@ -2658,17 +2727,126 @@ box-shadow: 0 0 12px rgba(239, 68, 68, 0.3); } -.btn-action-rich.btn-detailed { - margin-left: auto; - border-color: var(--primary, #c5f82a); +.btn-action-rich.btn-open-app { + border-color: var(--success, #22c55e); border-width: 2px; - color: var(--primary, #c5f82a); - background: rgba(197, 248, 42, 0.08); + color: var(--success, #22c55e); + background: rgba(34, 197, 94, 0.1); + text-decoration: none; } -.btn-action-rich.btn-detailed:hover { - background: rgba(197, 248, 42, 0.2); - box-shadow: 0 0 12px rgba(197, 248, 42, 0.3); +.btn-action-rich.btn-open-app:hover { + background: rgba(34, 197, 94, 0.25); + box-shadow: 0 0 12px rgba(34, 197, 94, 0.4); + color: #fff; +} + +/* ============================================================================= + PROGRESS LOG LIST STYLES + ============================================================================= */ + +.progress-log-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.log-empty { + color: var(--text-secondary, #666); + font-size: 0.85rem; + padding: 12px 0; +} + +.log-item-done, +.log-item-current, +.log-item-pending { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 6px; + font-size: 0.85rem; +} + +.log-item-done { + background: rgba(34, 197, 94, 0.08); + border-left: 3px solid var(--success, #22c55e); +} + +.log-item-current { + background: rgba(197, 248, 42, 0.1); + border-left: 3px solid var(--primary, #c5f82a); +} + +.log-item-pending { + background: rgba(255, 255, 255, 0.02); + border-left: 3px solid var(--border, #333); +} + +.log-check { + color: var(--success, #22c55e); + font-weight: bold; +} + +.log-spinner { + color: var(--primary, #c5f82a); + animation: pulse 1s infinite; +} + +.log-dot { + color: var(--text-secondary, #666); +} + +.log-name { + flex: 1; + color: var(--text, #fff); +} + +.log-time { + color: var(--text-secondary, #888); + font-family: monospace; + font-size: 0.75rem; +} + +.log-status { + color: var(--primary, #c5f82a); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.summary-alert { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid var(--error, #ef4444); + border-radius: 6px; + margin-top: 10px; +} + +.summary-alert .alert-icon { + color: var(--error, #ef4444); +} + +.summary-alert .alert-text { + font-size: 0.8rem; + color: var(--error, #ef4444); +} + +.progress-summary-content { + max-height: none; } /* Scrollbar for rich task detail */ @@ -2696,3 +2874,477 @@ .terminal-output-rich::-webkit-scrollbar-thumb:hover { background: var(--primary, #c5f82a); } + +/* ============================================================================= + MANIFEST HIERARCHICAL PROGRESS LOG - PIXEL PERFECT DESIGN + ============================================================================= */ + +.manifest-progress-log { + display: flex; + flex-direction: column; + gap: 0; + background: var(--bg, #0a0a0a); +} + +.log-section { + border-bottom: 1px solid var(--border, #1a1a24); +} + +.log-section:last-child { + border-bottom: none; +} + +.log-section-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + cursor: pointer; + transition: background 0.2s; +} + +.log-section-header:hover { + background: var(--surface-hover, #111115); +} + +.section-indicator { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.section-indicator.completed { + background: var(--primary, #c5f82a); + box-shadow: 0 0 6px var(--primary, #c5f82a); +} + +.section-indicator.running { + background: var(--primary, #c5f82a); + animation: pulse 1.5s infinite; +} + +.section-indicator.pending { + background: var(--text-muted, #3a3a3a); +} + +.section-indicator.failed { + background: var(--error, #ef4444); +} + +.section-name { + font-size: 14px; + font-weight: 500; + color: var(--text, #ffffff); + flex: 1; +} + +.section-step-badge { + font-size: 12px; + padding: 4px 12px; + background: var(--primary, #c5f82a); + color: var(--bg, #0a0a0a); + border-radius: 4px; + font-weight: 600; +} + +.section-status-badge { + font-size: 12px; + padding: 4px 12px; + border-radius: 4px; + font-weight: 500; + min-width: 80px; + text-align: center; +} + +.section-status-badge.completed { + background: transparent; + color: var(--text-secondary, #888); +} + +.section-status-badge.running { + background: var(--primary, #c5f82a); + color: var(--bg, #0a0a0a); +} + +.section-status-badge.pending { + background: var(--surface-active, #1a1a24); + color: var(--text-secondary, #666); +} + +.log-section-body { + display: none; + padding-left: 32px; + background: var(--bg, #080808); + border-top: 1px solid var(--border, #1a1a24); +} + +.log-section.expanded .log-section-body { + display: block; +} + +.log-child { + border-bottom: 1px solid var(--border-subtle, #151520); +} + +.log-child:last-child { + border-bottom: none; +} + +.log-child-header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 20px 14px 24px; + cursor: pointer; + transition: background 0.2s; +} + +.log-child-header:hover { + background: var(--surface-hover, #0e0e12); +} + +.child-indent { + width: 20px; + height: 1px; + background: var(--border, #2a2a2a); + flex-shrink: 0; +} + +.child-name { + font-size: 13px; + color: var(--text, #e0e0e0); + flex: 1; +} + +.child-step-badge { + font-size: 11px; + padding: 3px 10px; + background: var(--primary, #c5f82a); + color: var(--bg, #0a0a0a); + border-radius: 4px; + font-weight: 600; +} + +.child-status-badge { + font-size: 11px; + padding: 3px 10px; + border-radius: 4px; + min-width: 70px; + text-align: center; +} + +.child-status-badge.completed { + color: var(--text-secondary, #888); +} + +.log-child-body { + display: none; + padding-left: 24px; + padding-bottom: 8px; +} + +.log-child.expanded .log-child-body { + display: block; +} + +.log-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 20px; + font-size: 13px; +} + +.item-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.item-dot.completed { + background: var(--primary, #c5f82a); +} + +.item-dot.running { + background: var(--primary, #c5f82a); + animation: pulse 1s infinite; +} + +.item-dot.pending { + background: var(--text-muted, #3a3a3a); +} + +.item-name { + color: var(--text-secondary, #aaa); + flex: 1; +} + +.item-info { + display: flex; + align-items: center; + gap: 12px; + margin-left: auto; +} + +.item-duration { + font-size: 12px; + color: var(--text-muted, #666); +} + +.item-check { + font-size: 16px; +} + +.item-check.completed { + color: var(--success, #22c55e); +} + +.item-check.running { + color: var(--primary, #c5f82a); +} + +/* ============================================================================= + TASK DETAIL PANEL - LARGER & MORE PROMINENT + ============================================================================= */ + +.task-detail-panel { + flex: 1; + min-width: 0; + max-width: none; +} + +.task-detail-rich { + display: flex; + flex-direction: column; + gap: 20px; + padding: 24px; + height: 100%; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + +.detail-section-box { + background: var(--surface, #111111); + border: 1px solid var(--border, #1a1a24); + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; +} + +/* Status section box - stable minimum height */ +.detail-section-box:first-child { + min-height: 120px; +} + +.detail-section-box .section-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 1.5px; + color: var(--text-muted, #666); + text-transform: uppercase; + padding: 16px 20px; + border-bottom: 1px solid var(--border, #1a1a24); + background: var(--surface, #0d0d0d); +} + +.progress-log-section .progress-summary-content { + max-height: none; + padding: 0; + overflow-y: auto; +} + +/* Progress log section - scrollable with stable height */ +.progress-log-section { + flex: 1; + min-height: 200px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.progress-log-section .section-label { + flex-shrink: 0; +} + +/* Terminal Section Enhancements */ +.terminal-section-rich { + background: var(--bg-terminal, #0a0a0a); + flex-shrink: 0; + min-height: 150px; + max-height: 280px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.terminal-section-rich .section-header-rich { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-bottom: 1px solid var(--border, #1a1a24); + background: var(--surface, #111111); + flex-shrink: 0; +} + +.terminal-section-rich .section-label { + display: flex; + align-items: center; + gap: 10px; + font-size: 11px; + font-weight: 600; + letter-spacing: 1.5px; + color: var(--text-muted, #666); + text-transform: uppercase; + padding: 0; + border: none; + background: transparent; +} + +.terminal-dot-rich { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted, #444); +} + +.terminal-dot-rich.active { + background: var(--primary, #c5f82a); + box-shadow: 0 0 8px var(--primary, #c5f82a); + animation: pulse 1.5s infinite; +} + +.terminal-stats-rich { + display: flex; + align-items: center; + gap: 20px; + font-size: 12px; + color: var(--text-muted, #666); +} + +.terminal-stats-rich strong { + color: var(--text, #e0e0e0); + font-weight: 500; +} + +.terminal-output-rich { + padding: 16px 20px; + font-family: "JetBrains Mono", "Fira Code", "Monaco", monospace; + font-size: 13px; + line-height: 1.7; + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + background: var(--bg-terminal, #0a0a0a); + color: var(--text-terminal, #888); +} + +/* ============================================================================= + TASK CARD ENHANCEMENTS - SVG ICONS & OPEN APP BUTTON + ============================================================================= */ + +.task-card { + position: relative; +} + +.task-card .task-type-icon { + width: 20px; + height: 20px; + margin-right: 10px; + color: var(--primary, #c5f82a); +} + +.task-card .btn-open-app { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: var(--primary, #c5f82a); + color: var(--bg, #0a0a0a); + border: none; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + margin-top: 8px; +} + +.task-card .btn-open-app:hover { + background: var(--primary-hover, #d4ff3a); + transform: translateY(-1px); +} + +.task-card .btn-open-app svg { + width: 14px; + height: 14px; +} + +.task-card.completed .task-status-badge { + background: var(--success, #22c55e); +} + +/* Status section in detail panel */ +.status-section-box { + padding: 20px; +} + +.status-section-box .status-title { + font-size: 16px; + font-weight: 500; + color: var(--text, #fff); + margin-bottom: 12px; +} + +.status-section-box .status-current { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; +} + +.status-section-box .status-current .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--primary, #c5f82a); +} + +.status-section-box .status-current .status-text { + color: var(--text, #e0e0e0); + font-size: 14px; +} + +.status-section-box .status-time { + margin-left: auto; + font-size: 13px; + color: var(--text-muted, #666); +} + +.status-section-box .status-decision { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + opacity: 0.7; +} + +.status-section-box .status-decision .decision-dot { + width: 8px; + height: 8px; + border-radius: 50%; + border: 1px dashed var(--text-muted, #444); + background: transparent; +} + +.status-section-box .decision-badge { + padding: 4px 10px; + background: var(--surface-active, #1a1a24); + border-radius: 4px; + font-size: 12px; + color: var(--text-muted, #888); +} diff --git a/ui/suite/tasks/tasks.html b/ui/suite/tasks/tasks.html index e878afd..1b16b8c 100644 --- a/ui/suite/tasks/tasks.html +++ b/ui/suite/tasks/tasks.html @@ -378,7 +378,9 @@ } taskPollingInterval = setInterval(function () { - fetch(`/api/autotask/${taskId}`) + fetch(`/api/tasks/${taskId}`, { + headers: { Accept: "application/json" }, + }) .then((r) => r.json()) .then((task) => { console.log("[TASK] Poll status:", task.status); diff --git a/ui/suite/tasks/tasks.js b/ui/suite/tasks/tasks.js index 6f0dae9..45a84a1 100644 --- a/ui/suite/tasks/tasks.js +++ b/ui/suite/tasks/tasks.js @@ -56,10 +56,41 @@ function initTasksApp() { setupEventListeners(); setupKeyboardShortcuts(); setupIntentInputHandlers(); + setupHtmxListeners(); scrollAgentLogToBottom(); console.log("[Tasks] Initialized"); } +function setupHtmxListeners() { + // Listen for HTMX content swaps to apply pending manifest updates + document.body.addEventListener("htmx:afterSwap", function (evt) { + const target = evt.detail.target; + if ( + target && + (target.id === "task-detail-content" || + target.closest("#task-detail-content")) + ) { + console.log( + "[HTMX] Task detail content loaded, checking for pending manifest updates", + ); + // Check if there's a pending manifest update for the selected task + if ( + TasksState.selectedTaskId && + pendingManifestUpdates.has(TasksState.selectedTaskId) + ) { + const manifest = pendingManifestUpdates.get(TasksState.selectedTaskId); + console.log( + "[HTMX] Applying pending manifest for task:", + TasksState.selectedTaskId, + ); + setTimeout(() => { + renderManifestProgress(TasksState.selectedTaskId, manifest, 0); + }, 50); + } + } + }); +} + function setupIntentInputHandlers() { const input = document.getElementById("quick-intent-input"); const btn = document.getElementById("quick-intent-btn"); @@ -181,7 +212,11 @@ function startTaskPolling(taskId) { } try { - const response = await fetch(`/api/autotask/tasks/${taskId}`); + const response = await fetch(`/api/tasks/${taskId}`, { + headers: { + Accept: "application/json", + }, + }); if (!response.ok) { console.error(`[POLL] Failed to fetch task status: ${response.status}`); return; @@ -400,15 +435,298 @@ function handleWebSocketMessage(data) { break; case "llm_stream": - // Stream LLM output to terminal in real-time - if (data.text) { - addAgentLog("accent", data.text); - addLLMStreamOutput(data.text); + // Don't show raw LLM stream in terminal - it contains HTML/code garbage + // Progress is shown via manifest_update events instead + console.log("[Tasks WS] LLM streaming..."); + break; + + case "manifest_update": + console.log("[Tasks WS] MANIFEST_UPDATE for task:", data.task_id); + // Update the progress log section with manifest data + if (data.details) { + try { + const manifestData = JSON.parse(data.details); + renderManifestProgress(data.task_id, manifestData); + } catch (e) { + console.error("[Tasks WS] Failed to parse manifest:", e); + } } break; } } +// Store pending manifest updates for tasks whose elements aren't loaded yet +const pendingManifestUpdates = new Map(); + +function renderManifestProgress(taskId, manifest, retryCount = 0) { + console.log( + "[Manifest] renderManifestProgress called for task:", + taskId, + "sections:", + manifest?.sections?.length, + "retry:", + retryCount, + ); + + // 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 (!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"); + + // Get total steps from manifest progress + const totalSteps = manifest.progress?.total || 60; + + 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 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) { + 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"; + + html += ` +
+
+ + ${escapeHtml(child.name)} + View Details › + Step ${childStepCurrent}/${childStepTotal} + ${childStatusText} +
+
`; + + // 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} +
`; + } + } + + 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} +
`; + } + } + + html += `
`; + } + + 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 = ` +
+ ${escapeHtml(manifest.status.title || manifest.app_name || "")} + Runtime: ${runtime} +
+
+ + ${escapeHtml(currentAction)} + Estimated: ${estimated} +
+ `; + } + + // Update terminal stats if they exist + const processedEl = document.getElementById(`terminal-processed-${taskId}`); + if (processedEl && manifest.terminal?.stats) { + processedEl.textContent = manifest.terminal.stats.processed || "0"; + } + + console.log( + "[Manifest] Rendered progress for task:", + taskId, + "completed:", + manifest.progress?.current, + "/", + manifest.progress?.total, + ); +} + +function escapeHtml(text) { + if (!text) return ""; + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + function updateActivityMetrics(activity) { if (!activity) return; @@ -503,15 +821,70 @@ function updateDetailTerminal(taskId, message, step, activity) { addTerminalLine(terminalOutput, message, step, activity); } +// Format markdown-like text for terminal display +function formatTerminalMarkdown(text) { + if (!text) return ""; + + // Headers (## Header) + text = text.replace( + /^##\s+(.+)$/gm, + '$1', + ); + + // Bold (**text**) + text = text.replace(/\*\*(.+?)\*\*/g, "$1"); + + // Inline code (`code`) + text = text.replace(/`([^`]+)`/g, "$1"); + + // Code blocks (```code```) + text = text.replace( + /```([\s\S]*?)```/g, + '
$1
', + ); + + // List items (- item) + text = text.replace(/^-\s+(.+)$/gm, " • $1"); + + // Checkmarks + text = text.replace(/^✓\s*/gm, ' '); + + return text; +} + function addTerminalLine(terminal, message, step, activity) { const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false }); - const stepClass = - step === "error" ? "error" : step === "complete" ? "success" : ""; - const prefix = step === "error" ? "✗" : step === "complete" ? "✓" : "►"; + const isLlmStream = step === "llm_stream"; + + // Determine line type based on content + const isHeader = message && message.startsWith("##"); + const isSuccess = message && message.startsWith("✓"); + const isError = step === "error"; + const isComplete = step === "complete"; + + const stepClass = isError + ? "error" + : isComplete || isSuccess + ? "success" + : isHeader + ? "progress" + : isLlmStream + ? "llm-stream" + : "info"; + + // Format the message with markdown + const formattedMessage = formatTerminalMarkdown(message); const line = document.createElement("div"); line.className = `terminal-line ${stepClass} current`; - line.innerHTML = `${timestamp} ${prefix} ${message}`; + + if (isLlmStream) { + line.innerHTML = `${formattedMessage}`; + } else if (isHeader) { + line.innerHTML = formattedMessage; + } else { + line.innerHTML = `${timestamp}${formattedMessage}`; + } // Remove 'current' class from previous lines terminal.querySelectorAll(".terminal-line.current").forEach((el) => { @@ -818,19 +1191,35 @@ function searchTasks(query) { // ============================================================================= function loadTaskDetails(taskId) { + if (!taskId) { + console.warn("[LOAD] No task ID provided"); + return; + } + addAgentLog("info", `[LOAD] Loading task #${taskId} details`); // Show detail panel and hide empty state const emptyState = document.getElementById("detail-empty"); const detailContent = document.getElementById("task-detail-content"); - if (emptyState) emptyState.style.display = "none"; - if (detailContent) detailContent.style.display = "block"; + if (!detailContent) { + console.error("[LOAD] task-detail-content element not found"); + return; + } - // Fetch task details from API - htmx.ajax("GET", `/api/tasks/${taskId}`, { - target: "#task-detail-content", - swap: "innerHTML", + if (emptyState) emptyState.style.display = "none"; + detailContent.style.display = "block"; + + // Fetch task details from API - use requestAnimationFrame to ensure DOM is ready + requestAnimationFrame(() => { + if (typeof htmx !== "undefined" && htmx.ajax) { + htmx.ajax("GET", `/api/tasks/${taskId}`, { + target: "#task-detail-content", + swap: "innerHTML", + }); + } else { + console.error("[LOAD] HTMX not available"); + } }); } @@ -1065,7 +1454,7 @@ function toggleAgentLogPause() { function pauseTask(taskId) { addAgentLog("info", `[TASK] Pausing task #${taskId}...`); - fetch(`/api/autotask/${taskId}/pause`, { + fetch(`/api/tasks/${taskId}/pause`, { method: "POST", headers: { "Content-Type": "application/json" }, }) @@ -1099,7 +1488,7 @@ function cancelTask(taskId) { addAgentLog("info", `[TASK] Cancelling task #${taskId}...`); - fetch(`/api/autotask/${taskId}/cancel`, { + fetch(`/api/tasks/${taskId}/cancel`, { method: "POST", headers: { "Content-Type": "application/json" }, })