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:
parent
de85b35772
commit
9f922b523d
11 changed files with 3246 additions and 36 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
555
ui/suite/tasks/progress-panel.css
Normal file
555
ui/suite/tasks/progress-panel.css
Normal 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);
|
||||
}
|
||||
103
ui/suite/tasks/progress-panel.html
Normal file
103
ui/suite/tasks/progress-panel.html
Normal 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>
|
||||
464
ui/suite/tasks/progress-panel.js
Normal file
464
ui/suite/tasks/progress-panel.js
Normal 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
728
ui/suite/tasks/taskmd.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue