feat(dashboard): simplify lite task UI and add exploration context support

- Remove status indicators from lite task cards (progress bars, percentages)
- Remove status icons and badges from task detail items
- Remove stats bar showing completed/in-progress/pending counts
- Add Plan tab in drawer for plan.json data display
- Add exploration-*.json parsing for context tab
- Add collapsible sections for architecture, dependencies, patterns
- Fix currentPath selector bug causing TypeError

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-05 15:47:49 +08:00
parent 72fe6195af
commit 60bb11c315
7 changed files with 2314 additions and 186 deletions

View File

@@ -49,19 +49,19 @@ export function run(argv) {
.description('Claude Code Workflow CLI - Dashboard and workflow tools')
.version(pkg.version);
// View command
// View command (server mode with live path switching)
program
.command('view')
.description('Open workflow dashboard in browser (static HTML)')
.description('Open workflow dashboard server with live path switching')
.option('-p, --path <path>', 'Path to project directory', '.')
.option('--no-browser', 'Generate dashboard without opening browser')
.option('-o, --output <file>', 'Output path for generated HTML')
.option('--port <port>', 'Server port', '3456')
.option('--no-browser', 'Start server without opening browser')
.action(viewCommand);
// Serve command
// Serve command (alias for view)
program
.command('serve')
.description('Start dashboard server with live path switching')
.description('Alias for view command')
.option('-p, --path <path>', 'Initial project directory')
.option('--port <port>', 'Server port', '3456')
.option('--no-browser', 'Start server without opening browser')

View File

@@ -1,144 +1,14 @@
import { scanSessions } from '../core/session-scanner.js';
import { aggregateData } from '../core/data-aggregator.js';
import { generateDashboard } from '../core/dashboard-generator.js';
import { launchBrowser, isHeadlessEnvironment } from '../utils/browser-launcher.js';
import { resolvePath, ensureDir, getWorkflowDir, validatePath, validateOutputPath, trackRecentPath, getRecentPaths, normalizePathForDisplay } from '../utils/path-resolver.js';
import chalk from 'chalk';
import { writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { serveCommand } from './serve.js';
/**
* View command handler - generates and opens workflow dashboard
* View command handler - starts dashboard server (unified with serve mode)
* @param {Object} options - Command options
*/
export async function viewCommand(options) {
// Validate project path
const pathValidation = validatePath(options.path, { mustExist: true });
if (!pathValidation.valid) {
console.error(chalk.red(`\n Error: ${pathValidation.error}\n`));
process.exit(1);
}
const workingDir = pathValidation.path;
const workflowDir = join(workingDir, '.workflow');
// Track this path in recent paths
trackRecentPath(workingDir);
console.log(chalk.blue.bold('\n CCW Dashboard Generator\n'));
console.log(chalk.gray(` Project: ${workingDir}`));
console.log(chalk.gray(` Workflow: ${workflowDir}\n`));
// Check if .workflow directory exists
if (!existsSync(workflowDir)) {
console.log(chalk.yellow(' No .workflow directory found.'));
console.log(chalk.gray(' This project may not have any workflow sessions yet.\n'));
// Still generate an empty dashboard
const emptyData = {
generatedAt: new Date().toISOString(),
activeSessions: [],
archivedSessions: [],
liteTasks: { litePlan: [], liteFix: [] },
reviewData: null,
statistics: {
totalSessions: 0,
activeSessions: 0,
totalTasks: 0,
completedTasks: 0,
reviewFindings: 0,
litePlanCount: 0,
liteFixCount: 0
},
projectPath: normalizePathForDisplay(workingDir),
recentPaths: getRecentPaths()
};
await generateAndOpen(emptyData, workflowDir, options);
return;
}
try {
// Step 1: Scan for sessions
console.log(chalk.cyan(' Scanning sessions...'));
const sessions = await scanSessions(workflowDir);
console.log(chalk.green(` Found ${sessions.active.length} active, ${sessions.archived.length} archived sessions`));
if (sessions.hasReviewData) {
console.log(chalk.magenta(' Review data detected - will include Reviews tab'));
}
// Step 2: Aggregate all data
console.log(chalk.cyan(' Aggregating data...'));
const dashboardData = await aggregateData(sessions, workflowDir);
// Add project path and recent paths
dashboardData.projectPath = normalizePathForDisplay(workingDir);
dashboardData.recentPaths = getRecentPaths();
// Log statistics
const stats = dashboardData.statistics;
console.log(chalk.gray(` Tasks: ${stats.completedTasks}/${stats.totalTasks} completed`));
if (stats.reviewFindings > 0) {
console.log(chalk.gray(` Review findings: ${stats.reviewFindings}`));
}
// Step 3 & 4: Generate and open
await generateAndOpen(dashboardData, workflowDir, options);
} catch (error) {
console.error(chalk.red(`\n Error: ${error.message}\n`));
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
}
/**
* Generate dashboard and optionally open in browser
* @param {Object} data - Dashboard data
* @param {string} workflowDir - Path to .workflow
* @param {Object} options - Command options
*/
async function generateAndOpen(data, workflowDir, options) {
// Step 3: Generate dashboard HTML
console.log(chalk.cyan(' Generating dashboard...'));
const html = await generateDashboard(data);
// Step 4: Validate and write dashboard file
let outputPath;
if (options.output) {
const outputValidation = validateOutputPath(options.output, workflowDir);
if (!outputValidation.valid) {
console.error(chalk.red(`\n Error: ${outputValidation.error}\n`));
process.exit(1);
}
outputPath = outputValidation.path;
} else {
outputPath = join(workflowDir, 'dashboard.html');
}
ensureDir(dirname(outputPath));
writeFileSync(outputPath, html, 'utf8');
console.log(chalk.green(` Dashboard saved: ${outputPath}`));
// Step 5: Open in browser (unless --no-browser or headless environment)
if (options.browser !== false) {
if (isHeadlessEnvironment()) {
console.log(chalk.yellow('\n Running in CI/headless environment - skipping browser launch'));
console.log(chalk.gray(` Open manually: file://${outputPath.replace(/\\/g, '/')}\n`));
} else {
console.log(chalk.cyan(' Opening in browser...'));
try {
await launchBrowser(outputPath);
console.log(chalk.green.bold('\n Dashboard opened in browser!\n'));
} catch (error) {
console.log(chalk.yellow(`\n Could not open browser: ${error.message}`));
console.log(chalk.gray(` Open manually: file://${outputPath.replace(/\\/g, '/')}\n`));
}
}
} else {
console.log(chalk.gray(`\n Open in browser: file://${outputPath.replace(/\\/g, '/')}\n`));
}
// Forward to serve command with same options
await serveCommand({
path: options.path,
port: options.port || 3456,
browser: options.browser
});
}

View File

@@ -87,7 +87,8 @@ async function processSession(session, isActive) {
tasks: [],
taskCount: 0,
hasReview: false,
reviewSummary: null
reviewSummary: null,
reviewDimensions: []
};
// Load tasks for active sessions (full details)
@@ -121,6 +122,10 @@ async function processSession(session, isActive) {
if (existsSync(reviewDir)) {
result.hasReview = true;
result.reviewSummary = loadReviewSummary(reviewDir);
// Load dimension data for review sessions
if (session.type === 'review') {
result.reviewDimensions = await loadDimensionData(reviewDir);
}
}
} else {
// For archived, also load tasks (same as active)
@@ -150,6 +155,10 @@ async function processSession(session, isActive) {
if (existsSync(reviewDir)) {
result.hasReview = true;
result.reviewSummary = loadReviewSummary(reviewDir);
// Load dimension data for review sessions
if (session.type === 'review') {
result.reviewDimensions = await loadDimensionData(reviewDir);
}
}
}
@@ -266,15 +275,33 @@ async function loadDimensionData(reviewDir) {
for (const file of dimFiles) {
try {
const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8'));
// Handle array structure: [ { findings: [...], summary: {...} } ]
let findings = [];
let summary = null;
let status = 'completed';
if (Array.isArray(data) && data.length > 0) {
const dimData = data[0];
findings = dimData.findings || [];
summary = dimData.summary || null;
status = dimData.status || 'completed';
} else if (data.findings) {
findings = data.findings;
summary = data.summary || null;
status = data.status || 'completed';
}
dimensions.push({
name: basename(file, '.json'),
findings: Array.isArray(data) ? data : (data.findings || []),
status: data.status || 'completed'
findings: findings,
summary: summary,
status: status
});
} catch {
// Skip invalid dimension files
}
}
}
return dimensions;
}

View File

@@ -241,16 +241,59 @@ async function getSessionDetailData(sessionPath, dataType) {
// Load review data from .review/
if (dataType === 'review' || dataType === 'all') {
const reviewDir = join(normalizedPath, '.review');
result.review = { dimensions: {} };
result.review = {
state: null,
dimensions: [],
severityDistribution: null,
totalFindings: 0
};
if (existsSync(reviewDir)) {
// Load review-state.json
const stateFile = join(reviewDir, 'review-state.json');
if (existsSync(stateFile)) {
try {
const state = JSON.parse(readFileSync(stateFile, 'utf8'));
result.review.state = state;
result.review.severityDistribution = state.severity_distribution || {};
result.review.totalFindings = state.total_findings || 0;
result.review.phase = state.phase || 'unknown';
result.review.dimensionSummaries = state.dimension_summaries || {};
result.review.crossCuttingConcerns = state.cross_cutting_concerns || [];
result.review.criticalFiles = state.critical_files || [];
} catch (e) {
// Skip unreadable state
}
}
// Load dimension findings
const dimensionsDir = join(reviewDir, 'dimensions');
if (existsSync(dimensionsDir)) {
const files = readdirSync(dimensionsDir).filter(f => f.endsWith('.json'));
for (const file of files) {
try {
const dimName = file.replace('.json', '');
const content = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8'));
result.review.dimensions[dimName] = content.findings || content;
const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8'));
// Handle array structure: [ { findings: [...] } ]
let findings = [];
let summary = null;
if (Array.isArray(data) && data.length > 0) {
const dimData = data[0];
findings = dimData.findings || [];
summary = dimData.summary || null;
} else if (data.findings) {
findings = data.findings;
summary = data.summary || null;
}
result.review.dimensions.push({
name: dimName,
findings: findings,
summary: summary,
count: findings.length
});
} catch (e) {
// Skip unreadable files
}

File diff suppressed because it is too large Load Diff

View File

@@ -231,7 +231,7 @@
</header>
<!-- Sidebar Overlay (mobile) -->
<div class="sidebar-overlay fixed inset-0 bg-black/50 z-40" id="sidebarOverlay"></div>
<div class="sidebar-overlay hidden fixed inset-0 bg-black/50 z-40" id="sidebarOverlay"></div>
<!-- Main Layout -->
<div class="flex flex-1">
@@ -343,7 +343,7 @@
</footer>
<!-- Task Detail Drawer -->
<div class="task-detail-drawer fixed top-0 right-0 w-[400px] max-w-full h-full bg-card border-l border-border shadow-lg z-50 flex flex-col" id="taskDetailDrawer">
<div class="task-detail-drawer fixed top-0 right-0 w-1/2 max-w-full h-full bg-card border-l border-border shadow-lg z-50 flex flex-col" id="taskDetailDrawer">
<div class="flex items-center justify-between px-5 py-4 border-b border-border">
<h3 class="text-lg font-semibold text-foreground" id="drawerTaskTitle">Task Details</h3>
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeTaskDrawer()">&times;</button>
@@ -352,11 +352,34 @@
<!-- Dynamic content -->
</div>
</div>
<div class="drawer-overlay fixed inset-0 bg-black/50 z-40" id="drawerOverlay" onclick="closeTaskDrawer()"></div>
<div class="drawer-overlay hidden fixed inset-0 bg-black/50 z-40" id="drawerOverlay" onclick="closeTaskDrawer()"></div>
</div>
<!-- Markdown Preview Modal -->
<div id="markdownModal" class="markdown-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
<div class="markdown-modal-backdrop absolute inset-0 bg-black/60" onclick="closeMarkdownModal()"></div>
<div class="markdown-modal-content relative bg-card border border-border rounded-lg shadow-2xl w-[90vw] max-w-4xl h-[85vh] flex flex-col">
<div class="markdown-modal-header flex items-center justify-between px-4 py-3 border-b border-border">
<h3 class="text-lg font-semibold text-foreground" id="markdownModalTitle">Content Preview</h3>
<div class="flex items-center gap-2">
<div class="flex bg-muted rounded-lg p-0.5">
<button id="mdTabRaw" class="md-tab-btn px-3 py-1 text-sm rounded-md transition-colors" onclick="switchMarkdownTab('raw')">Raw</button>
<button id="mdTabPreview" class="md-tab-btn px-3 py-1 text-sm rounded-md transition-colors active" onclick="switchMarkdownTab('preview')">Preview</button>
</div>
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeMarkdownModal()">&times;</button>
</div>
</div>
<div class="markdown-modal-body flex-1 overflow-auto p-4">
<pre id="markdownRaw" class="hidden whitespace-pre-wrap text-sm font-mono text-foreground bg-muted p-4 rounded-lg overflow-auto h-full"></pre>
<div id="markdownPreview" class="markdown-preview prose prose-sm max-w-none"></div>
</div>
</div>
</div>
<!-- D3.js for Flowchart -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Marked.js for Markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
{{JS_CONTENT}}

View File

@@ -320,7 +320,7 @@ async function switchToPath(path) {
recentPaths = data.recentPaths || [];
// Update UI
document.querySelector('.path-text').textContent = projectPath;
document.getElementById('currentPath').textContent = projectPath;
renderDashboard();
refreshRecentPaths();
}
@@ -740,7 +740,6 @@ function renderDetailTaskItem(task, showFull = false) {
return `
<div class="detail-task-item ${task.status}" onclick="openTaskDrawer('${escapeHtml(taskId)}')" style="cursor: pointer;">
<div class="task-item-header">
<span class="task-status-icon">${statusIcon}</span>
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<span class="task-status-badge ${task.status}">${task.status}</span>
@@ -751,9 +750,8 @@ function renderDetailTaskItem(task, showFull = false) {
// Full task view with collapsible sections
return `
<div class="detail-task-item-full ${task.status}">
<div class="detail-task-item-full">
<div class="task-item-header-full" onclick="openTaskDrawer('${escapeHtml(taskId)}')" style="cursor: pointer;" title="Click to open task details">
<span class="task-status-icon">${statusIcon}</span>
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span class="task-title">${escapeHtml(task.title || task.meta?.title || 'Untitled')}</span>
<span class="task-status-badge ${task.status}">${task.status}</span>
@@ -1619,7 +1617,6 @@ function renderLiteTasks() {
const liteTaskDataStore = {};
function renderLiteTaskCard(session) {
const progress = session.progress || { total: 0, completed: 0, percentage: 0 };
const tasks = session.tasks || [];
// Store session data for detail page
@@ -1631,25 +1628,14 @@ function renderLiteTaskCard(session) {
<div class="session-header">
<div class="session-title">${escapeHtml(session.id)}</div>
<span class="session-status ${session.type}">
${session.type === 'lite-plan' ? '📝 Plan' : '🔧 Fix'}
${session.type === 'lite-plan' ? '📝 PLAN' : '🔧 FIX'}
</span>
</div>
<div class="session-body">
<div class="session-meta">
<span class="session-meta-item">📅 ${formatDate(session.createdAt)}</span>
<span class="session-meta-item">📋 ${progress.total} tasks</span>
<span class="session-meta-item">📋 ${tasks.length} tasks</span>
</div>
${progress.total > 0 ? `
<div class="progress-container">
<div class="progress-header">
<span>Progress</span>
<span>${progress.completed}/${progress.total} (${progress.percentage}%)</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.percentage}%"></div>
</div>
</div>
` : ''}
</div>
</div>
`;
@@ -1705,11 +1691,7 @@ function showLiteTaskDetailPage(sessionKey) {
</div>
<div class="info-item">
<span class="info-label">Tasks:</span>
<span class="info-value">${completed}/${tasks.length} completed</span>
</div>
<div class="info-item">
<span class="info-label">Progress:</span>
<span class="info-value">${progress.percentage}%</span>
<span class="info-value">${tasks.length} tasks</span>
</div>
</div>
@@ -1809,11 +1791,6 @@ function renderLiteTasksTab(session, tasks, completed, inProgress, pending) {
return `
<div class="tasks-tab-content">
<div class="task-stats-bar">
<span class="task-stat completed">✓ ${completed} completed</span>
<span class="task-stat in-progress">⟳ ${inProgress} in progress</span>
<span class="task-stat pending">○ ${pending} pending</span>
</div>
<div class="tasks-list" id="liteTasksListContent">
${tasks.map(task => renderLiteTaskDetailItem(session.id, task)).join('')}
</div>
@@ -1826,15 +1803,11 @@ function renderLiteTaskDetailItem(sessionId, task) {
const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
taskJsonStore[taskJsonId] = rawTask;
const statusIcon = task.status === 'completed' ? '✓' : task.status === 'in_progress' ? '⟳' : '○';
return `
<div class="detail-task-item-full ${task.status}">
<div class="detail-task-item-full">
<div class="task-item-header-full" onclick="openTaskDrawerForLite('${sessionId}', '${escapeHtml(task.id)}')" style="cursor: pointer;" title="Click to open task details">
<span class="task-status-icon">${statusIcon}</span>
<span class="task-id-badge">${escapeHtml(task.id)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<span class="task-status-badge ${task.status}">${task.status}</span>
<button class="btn-view-json" onclick="event.stopPropagation(); showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
</div>
@@ -1986,6 +1959,157 @@ async function loadAndRenderLiteContextTab(session, contentArea) {
}
}
// Render exploration data for lite task context
function renderExplorationContext(explorations) {
if (!explorations || !explorations.manifest) {
return '';
}
const manifest = explorations.manifest;
const data = explorations.data || {};
let sections = [];
// Header with manifest info
sections.push(`
<div class="exploration-header">
<h4>${escapeHtml(manifest.task_description || 'Exploration Context')}</h4>
<div class="exploration-meta">
<span class="meta-item">Complexity: <strong>${escapeHtml(manifest.complexity || 'N/A')}</strong></span>
<span class="meta-item">Explorations: <strong>${manifest.exploration_count || 0}</strong></span>
</div>
</div>
`);
// Render each exploration angle as collapsible section
const explorationOrder = ['architecture', 'dependencies', 'patterns', 'integration-points'];
const explorationTitles = {
'architecture': 'Architecture',
'dependencies': 'Dependencies',
'patterns': 'Patterns',
'integration-points': 'Integration Points'
};
for (const angle of explorationOrder) {
const expData = data[angle];
if (!expData) continue;
sections.push(`
<div class="exploration-section collapsible-section">
<div class="collapsible-header" onclick="toggleSection(this)">
<span class="collapse-icon">▶</span>
<span class="section-label">${explorationTitles[angle] || angle}</span>
</div>
<div class="collapsible-content collapsed">
${renderExplorationAngle(angle, expData)}
</div>
</div>
`);
}
return `<div class="exploration-context">${sections.join('')}</div>`;
}
function renderExplorationAngle(angle, data) {
let content = [];
// Project structure (architecture)
if (data.project_structure) {
content.push(`
<div class="exp-field">
<label>Project Structure</label>
<p>${escapeHtml(data.project_structure)}</p>
</div>
`);
}
// Relevant files
if (data.relevant_files && data.relevant_files.length) {
content.push(`
<div class="exp-field">
<label>Relevant Files (${data.relevant_files.length})</label>
<div class="relevant-files-list">
${data.relevant_files.slice(0, 10).map(f => `
<div class="file-item-exp">
<div class="file-path"><code>${escapeHtml(f.path || '')}</code></div>
<div class="file-relevance">Relevance: ${(f.relevance * 100).toFixed(0)}%</div>
${f.rationale ? `<div class="file-rationale">${escapeHtml(f.rationale.substring(0, 200))}...</div>` : ''}
</div>
`).join('')}
${data.relevant_files.length > 10 ? `<div class="more-files">... and ${data.relevant_files.length - 10} more files</div>` : ''}
</div>
</div>
`);
}
// Patterns
if (data.patterns) {
content.push(`
<div class="exp-field">
<label>Patterns</label>
<p class="patterns-text">${escapeHtml(data.patterns)}</p>
</div>
`);
}
// Dependencies
if (data.dependencies) {
content.push(`
<div class="exp-field">
<label>Dependencies</label>
<p>${escapeHtml(data.dependencies)}</p>
</div>
`);
}
// Integration points
if (data.integration_points) {
content.push(`
<div class="exp-field">
<label>Integration Points</label>
<p>${escapeHtml(data.integration_points)}</p>
</div>
`);
}
// Constraints
if (data.constraints) {
content.push(`
<div class="exp-field">
<label>Constraints</label>
<p>${escapeHtml(data.constraints)}</p>
</div>
`);
}
// Clarification needs
if (data.clarification_needs && data.clarification_needs.length) {
content.push(`
<div class="exp-field">
<label>Clarification Needs</label>
<div class="clarification-list">
${data.clarification_needs.map(c => `
<div class="clarification-item">
<div class="clarification-question">${escapeHtml(c.question)}</div>
${c.options && c.options.length ? `
<div class="clarification-options">
${c.options.map((opt, i) => `
<span class="option-badge ${i === c.recommended ? 'recommended' : ''}">${escapeHtml(opt)}</span>
`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
return content.join('') || '<p>No data available</p>';
}
function renderLiteContextContent(context, session) {
const plan = session.plan || {};
@@ -2582,6 +2706,116 @@ function openTaskDrawer(taskId) {
}, 100);
}
// Render plan.json task details in drawer (for lite tasks)
function renderPlanTaskDetails(task, session) {
if (!task) return '';
// Get corresponding plan task if available
const planTask = session?.plan?.tasks?.find(pt => pt.id === task.id);
if (!planTask) {
// Fallback: task itself might have plan-like structure
return renderTaskImplementationDetails(task);
}
return renderTaskImplementationDetails(planTask);
}
function renderTaskImplementationDetails(task) {
const sections = [];
// Description
if (task.description) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Description</h4>
<p class="task-description">${escapeHtml(task.description)}</p>
</div>
`);
}
// Modification Points
if (task.modification_points?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Modification Points</h4>
<div class="modification-points-list">
${task.modification_points.map(mp => `
<div class="mod-point-item">
<div class="mod-point-file">
<span class="file-icon">📄</span>
<code>${escapeHtml(mp.file || mp.path || '')}</code>
</div>
${mp.target ? `<div class="mod-point-target">Target: <code>${escapeHtml(mp.target)}</code></div>` : ''}
${mp.change ? `<div class="mod-point-change">${escapeHtml(mp.change)}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Implementation Steps
if (task.implementation?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Implementation Steps</h4>
<ol class="implementation-steps-list">
${task.implementation.map(step => `
<li class="impl-step-item">${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}</li>
`).join('')}
</ol>
</div>
`);
}
// Reference
if (task.reference) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Reference</h4>
${task.reference.pattern ? `<div class="ref-pattern"><strong>Pattern:</strong> ${escapeHtml(task.reference.pattern)}</div>` : ''}
${task.reference.files?.length ? `
<div class="ref-files">
<strong>Files:</strong>
<ul>
${task.reference.files.map(f => `<li><code>${escapeHtml(f)}</code></li>`).join('')}
</ul>
</div>
` : ''}
${task.reference.examples ? `<div class="ref-examples"><strong>Examples:</strong> ${escapeHtml(task.reference.examples)}</div>` : ''}
</div>
`);
}
// Acceptance Criteria
if (task.acceptance?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Acceptance Criteria</h4>
<ul class="acceptance-list">
${task.acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}
</ul>
</div>
`);
}
// Dependencies
if (task.depends_on?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Dependencies</h4>
<div class="dependencies-list">
${task.depends_on.map(dep => `<span class="dep-badge">${escapeHtml(dep)}</span>`).join(' ')}
</div>
</div>
`);
}
return sections.join('');
}
function closeTaskDrawer() {
document.getElementById('taskDetailDrawer').classList.remove('open');
document.getElementById('drawerOverlay').classList.remove('active');