mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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:
@@ -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')
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()">×</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()">×</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}}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user