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
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-31 23:45:38 -03:00
parent de85b35772
commit 9f922b523d
11 changed files with 3246 additions and 36 deletions

View file

@ -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;
}

View file

@ -26,6 +26,7 @@
<link rel="stylesheet" href="paper/paper.css" />
<link rel="stylesheet" href="research/research.css" />
<link rel="stylesheet" href="tasks/tasks.css?v=20251230" />
<link rel="stylesheet" href="tasks/taskmd.css?v=20251230" />
<link rel="stylesheet" href="analytics/analytics.css" />
<link rel="stylesheet" href="monitoring/monitoring.css" />

View file

@ -476,4 +476,6 @@ Examples:
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<link rel="stylesheet" href="progress-panel.css" />
<script src="progress-panel.js"></script>
<script src="autotask.js"></script>

View file

@ -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) {
</div>
`;
// 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) => {

View file

@ -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);
}

View file

@ -0,0 +1,103 @@
<div class="progress-panel" id="task-progress-panel">
<div class="status-section">
<div class="status-header">
<span class="status-label">STATUS</span>
</div>
<div class="status-content">
<div class="status-title-row">
<span class="status-title" id="status-title">Implement User Authentication System</span>
<div class="status-time">
<span class="runtime-label">Runtime:</span>
<span class="runtime-value" id="status-runtime">42 min</span>
<span class="status-indicator active"></span>
</div>
</div>
<div class="status-current-action">
<span class="action-dot active"></span>
<span class="action-text" id="current-action">Choose Token Expiration Strategy</span>
<div class="estimated-time">
<span class="estimated-label">Estimated:</span>
<span class="estimated-value" id="estimated-time">15 min</span>
<span class="settings-icon"></span>
</div>
</div>
<div class="status-decision-point">
<span class="decision-dot pending"></span>
<span class="decision-text">Decision Point Coming (Step <span id="decision-step">26</span>/<span id="decision-total">60</span>)</span>
<span class="decision-badge" id="decision-note">Will need approval for security configuration</span>
</div>
</div>
</div>
<div class="progress-log-section">
<div class="progress-log-header">
<span class="progress-log-label">PROGRESS LOG</span>
</div>
<div class="progress-log-content" id="progress-log-content">
</div>
</div>
<div class="terminal-section">
<div class="terminal-header">
<span class="terminal-label">TERMINAL (LIVE AGENT ACTIVITY)</span>
<div class="terminal-stats">
<span class="stat-item">Processed: <strong id="terminal-processed">127</strong> data points</span>
<span class="stat-separator">/</span>
<span class="stat-item">Processing speed: <strong id="terminal-speed">~8 sources/min</strong></span>
<span class="stat-separator"></span>
<span class="stat-item">Estimated completion: <strong id="terminal-eta">6 minutes</strong></span>
</div>
</div>
<div class="terminal-content" id="terminal-content">
<div class="terminal-line">Synthesizing competitive insights...</div>
<div class="terminal-line">Cross-referencing pricing data...</div>
</div>
</div>
</div>
<template id="progress-log-section-template">
<div class="log-section" data-section-id="${section_id}">
<div class="log-section-header" onclick="toggleLogSection(this)">
<span class="section-indicator ${status_class}"></span>
<span class="section-name">${section_name}</span>
<span class="section-details-link" onclick="event.stopPropagation(); viewSectionDetails('${section_id}')">View Details ▸</span>
<span class="section-step-badge">${step_current}/${step_total}</span>
<span class="section-status-badge ${status_class}">${status_text}</span>
</div>
<div class="log-section-body">
<div class="log-children" id="log-children-${section_id}">
</div>
</div>
</div>
</template>
<template id="progress-log-child-template">
<div class="log-child" data-child-id="${child_id}">
<div class="log-child-header" onclick="toggleLogChild(this)">
<span class="child-indent"></span>
<span class="child-name">${child_name}</span>
<span class="child-details-link" onclick="event.stopPropagation(); viewChildDetails('${child_id}')">View Details ▸</span>
<span class="child-step-badge">${step_current}/${step_total}</span>
<span class="child-status-badge ${status_class}">${status_text}</span>
</div>
<div class="log-child-body">
<div class="log-items" id="log-items-${child_id}">
</div>
</div>
</div>
</template>
<template id="progress-log-item-template">
<div class="log-item" data-item-id="${item_id}">
<span class="item-dot ${status_class}"></span>
<span class="item-name">${item_name}</span>
<div class="item-info">
<span class="item-duration">Duration: ${duration}</span>
<span class="item-check ${status_class}">${check_icon}</span>
</div>
</div>
</template>
<template id="terminal-line-template">
<div class="terminal-line ${line_type}">${content}</div>
</template>

View file

@ -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 = `
<div class="log-section-header" onclick="ProgressPanel.toggleSection('${section.id}')">
<span class="section-indicator ${statusClass}"></span>
<span class="section-name">${this.escapeHtml(section.name)}</span>
<span class="section-details-link" onclick="event.stopPropagation(); ProgressPanel.viewDetails('${section.id}')">View Details </span>
<span class="section-step-badge">Step ${stepCurrent}/${stepTotal}</span>
<span class="section-status-badge ${statusClass}">${section.status}</span>
</div>
<div class="log-section-body">
<div class="log-children" id="log-children-${section.id}">
</div>
</div>
`;
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 = `
<div class="log-child-header" onclick="ProgressPanel.toggleChild('${child.id}')">
<span class="child-indent"></span>
<span class="child-name">${this.escapeHtml(child.name)}</span>
<span class="child-details-link" onclick="event.stopPropagation(); ProgressPanel.viewChildDetails('${child.id}')">View Details </span>
<span class="child-step-badge">Step ${stepCurrent}/${stepTotal}</span>
<span class="child-status-badge ${statusClass}">${child.status}</span>
</div>
<div class="log-child-body">
<div class="log-items" id="log-items-${child.id}">
</div>
</div>
`;
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 = `
<span class="item-dot ${statusClass}"></span>
<span class="item-name">${this.escapeHtml(item.name)}${item.details ? ` - ${this.escapeHtml(item.details)}` : ''}</span>
<div class="item-info">
<span class="item-duration">${duration}</span>
<span class="item-check ${statusClass}">${checkIcon}</span>
</div>
`;
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;

728
ui/suite/tasks/taskmd.css Normal file
View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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 = '<div class="taskmd-tree">';
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 += `
<div class="tree-section ${statusClass}" data-section-id="${section.id}">
<div class="tree-row tree-level-0" onclick="this.parentElement.classList.toggle('expanded')">
<span class="tree-name">${escapeHtml(section.name)}</span>
<span class="tree-view-details">View Details </span>
<span class="tree-step-badge">Step ${globalCurrent}/${totalSteps}</span>
<span class="tree-status ${statusClass}">${statusText}</span>
</div>
${
showProgress
? `
<div class="tree-progress-bar-container">
<div class="tree-progress-bar" style="width: ${sectionPercent}%"></div>
<span class="tree-progress-percent">${sectionPercent}%</span>
</div>`
: ""
}
<div class="tree-children">`;
// 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 += `
<div class="tree-child ${childStatus}" onclick="this.classList.toggle('expanded')">
<div class="tree-row tree-level-1">
<span class="tree-indent"></span>
<span class="tree-name">${escapeHtml(child.name)}</span>
<span class="tree-view-details">View Details </span>
<span class="tree-step-badge">Step ${childStepCurrent}/${childStepTotal}</span>
<span class="tree-status ${childStatus}">${childStatusText}</span>
</div>
<div class="tree-items">`;
// 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 += `
<div class="tree-item ${groupStatus}">
<span class="tree-item-dot ${groupStatus}"></span>
<span class="tree-item-name">${escapeHtml(group.name)}</span>
<span class="tree-item-duration">${duration}</span>
<span class="tree-item-check ${groupStatus}">${checkIcon}</span>
</div>`;
}
}
// Then render individual items
if (child.items && child.items.length > 0) {
for (const item of child.items) {
const itemStatus = item.status
? item.status.toLowerCase()
: "pending";
const checkIcon = itemStatus === "completed" ? "✓" : "";
const duration = item.duration_seconds
? item.duration_seconds >= 60
? `Duration: ${Math.floor(item.duration_seconds / 60)} min`
: `Duration: ${item.duration_seconds} sec`
: "";
html += `
<div class="tree-item ${itemStatus}">
<span class="tree-item-dot ${itemStatus}"></span>
<span class="tree-item-name">${escapeHtml(item.name)}</span>
<span class="tree-item-duration">${duration}</span>
<span class="tree-item-check ${itemStatus}">${checkIcon}</span>
</div>`;
}
}
html += `</div></div>`;
}
}
// 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 += `
<div class="tree-item ${groupStatus}">
<span class="tree-item-dot ${groupStatus}"></span>
<span class="tree-item-name">${escapeHtml(group.name)}</span>
<span class="tree-item-duration">${duration}</span>
<span class="tree-item-check ${groupStatus}">${checkIcon}</span>
</div>`;
}
}
// Render section-level items
if (section.items && section.items.length > 0) {
for (const item of section.items) {
const itemStatus = item.status ? item.status.toLowerCase() : "pending";
const checkIcon = itemStatus === "completed" ? "✓" : "";
const duration = item.duration_seconds
? item.duration_seconds >= 60
? `Duration: ${Math.floor(item.duration_seconds / 60)} min`
: `Duration: ${item.duration_seconds} sec`
: "";
html += `
<div class="tree-item ${itemStatus}">
<span class="tree-item-dot ${itemStatus}"></span>
<span class="tree-item-name">${escapeHtml(item.name)}</span>
<span class="tree-item-duration">${duration}</span>
<span class="tree-item-check ${itemStatus}">${checkIcon}</span>
</div>`;
}
}
html += `</div></div>`;
}
html += "</div>";
progressLog.innerHTML = html;
// Also update the STATUS section if it exists
const statusSection = document.querySelector(".taskmd-status-content");
if (statusSection && manifest.status) {
const runtime = manifest.status.runtime_display || "calculating...";
const estimated = manifest.status.estimated_display || "calculating...";
const currentAction = manifest.status.current_action || "Processing...";
statusSection.innerHTML = `
<div class="status-row status-main">
<span class="status-title">${escapeHtml(manifest.status.title || manifest.app_name || "")}</span>
<span class="status-time">Runtime: ${runtime}</span>
</div>
<div class="status-row status-current">
<span class="status-dot active"></span>
<span class="status-text">${escapeHtml(currentAction)}</span>
<span class="status-time">Estimated: ${estimated}</span>
</div>
`;
}
// Update terminal stats if they exist
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,
'<strong class="terminal-header">$1</strong>',
);
// Bold (**text**)
text = text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
// Inline code (`code`)
text = text.replace(/`([^`]+)`/g, "<code>$1</code>");
// Code blocks (```code```)
text = text.replace(
/```([\s\S]*?)```/g,
'<div class="terminal-code">$1</div>',
);
// List items (- item)
text = text.replace(/^-\s+(.+)$/gm, " • $1");
// Checkmarks
text = text.replace(/^✓\s*/gm, '<span class="check-mark">✓</span> ');
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 = `<span class="term-time">${timestamp}</span> ${prefix} ${message}`;
if (isLlmStream) {
line.innerHTML = `<span class="llm-text">${formattedMessage}</span>`;
} else if (isHeader) {
line.innerHTML = formattedMessage;
} else {
line.innerHTML = `<span class="terminal-timestamp">${timestamp}</span>${formattedMessage}`;
}
// Remove 'current' class from previous lines
terminal.querySelectorAll(".terminal-line.current").forEach((el) => {
@ -818,20 +1191,36 @@ 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
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");
}
});
}
function updateTaskCard(task) {
@ -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" },
})