diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js
index 268f0898..1e29c2b6 100644
--- a/ccw/src/core/server.js
+++ b/ccw/src/core/server.js
@@ -58,8 +58,9 @@ const MODULE_FILES = [
'components/notifications.js',
'components/mcp-manager.js',
'components/hook-manager.js',
- 'components/tabs-context.js',
+ 'components/_exp_helpers.js',
'components/tabs-other.js',
+ 'components/tabs-context.js',
'components/task-drawer-core.js',
'components/task-drawer-renderers.js',
'components/flowchart.js',
diff --git a/ccw/src/templates/dashboard-js/components/_exp_helpers.js b/ccw/src/templates/dashboard-js/components/_exp_helpers.js
new file mode 100644
index 00000000..551a5374
--- /dev/null
+++ b/ccw/src/templates/dashboard-js/components/_exp_helpers.js
@@ -0,0 +1,52 @@
+// Helper: Render exploration field with smart type detection
+function renderExpField(label, value) {
+ if (value === null || value === undefined) return '';
+ let rendered;
+ if (typeof value === 'string') {
+ rendered = `
${escapeHtml(value)}
`;
+ } else if (Array.isArray(value)) {
+ rendered = renderExpArray(value);
+ } else if (typeof value === 'object') {
+ rendered = renderExpObject(value);
+ } else {
+ rendered = `${escapeHtml(String(value))}
`;
+ }
+ return `${escapeHtml(label)} ${rendered}
`;
+}
+
+// Helper: Render array values
+function renderExpArray(arr) {
+ if (!arr.length) return '-
';
+ if (typeof arr[0] === 'object' && arr[0] !== null) {
+ return `${arr.map(item => {
+ if (item.question) {
+ return `
+
${escapeHtml(item.question)}
+ ${item.impact ? `
Impact: ${escapeHtml(item.impact)}
` : ''}
+ ${item.priority ? `
${item.priority} ` : ''}
+
`;
+ }
+ return `
${renderExpObject(item)}
`;
+ }).join('')}
`;
+ }
+ return `${arr.map(item => `${escapeHtml(String(item))} `).join('')} `;
+}
+
+// Helper: Render object values recursively
+function renderExpObject(obj) {
+ if (!obj || typeof obj !== 'object') return '';
+ const entries = Object.entries(obj).filter(([k]) => !k.startsWith('_'));
+ if (!entries.length) return '-
';
+ return `${entries.map(([key, val]) => {
+ const label = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ if (val === null || val === undefined) return '';
+ if (typeof val === 'string') {
+ return `
${escapeHtml(label)}: ${escapeHtml(val)}
`;
+ } else if (Array.isArray(val)) {
+ return `
${escapeHtml(label)}: ${renderExpArray(val)}
`;
+ } else if (typeof val === 'object') {
+ return `
${escapeHtml(label)} ${renderExpObject(val)}
`;
+ }
+ return `
${escapeHtml(label)}: ${escapeHtml(String(val))}
`;
+ }).join('')}
`;
+}
diff --git a/ccw/src/templates/dashboard-js/components/tabs-context.js b/ccw/src/templates/dashboard-js/components/tabs-context.js
index b5b100d0..8550c593 100644
--- a/ccw/src/templates/dashboard-js/components/tabs-context.js
+++ b/ccw/src/templates/dashboard-js/components/tabs-context.js
@@ -986,25 +986,25 @@ function renderSessionContextContent(context, explorations, conflictResolution)
window._currentContextJson = contextJson;
// Use existing renderContextContent for detailed rendering
- sections.push(\`
+ sections.push(`
- \${renderContextContent(context)}
+ ${renderContextContent(context)}
- \`);
+ `);
}
// If we have any sections, wrap them
if (sections.length > 0) {
- return \`\${sections.join('')}
\`;
+ return `${sections.join('')}
`;
}
- return \`
+ return `
๐ฆ
No Context Data
No context-package.json, exploration files, or conflict resolution data found for this session.
- \`;
+ `;
}
// ==========================================
@@ -1019,79 +1019,79 @@ function renderConflictResolutionContext(conflictResolution) {
let sections = [];
// Header
- sections.push(\`
+ sections.push(`
- \`);
+ `);
// User decisions
if (conflictResolution.user_decisions && Object.keys(conflictResolution.user_decisions).length > 0) {
const decisions = Object.entries(conflictResolution.user_decisions);
- sections.push(\`
+ sections.push(`
- \${decisions.map(([key, decision]) => \`
+ ${decisions.map(([key, decision]) => `
- \${decision.description ? \`
\${escapeHtml(decision.description)}
\` : ''}
- \${decision.implications && decision.implications.length > 0 ? \`
+ ${decision.description ? `
${escapeHtml(decision.description)}
` : ''}
+ ${decision.implications && decision.implications.length > 0 ? `
Implications:
- \${decision.implications.map(impl => \`\${escapeHtml(impl)} \`).join('')}
+ ${decision.implications.map(impl => `${escapeHtml(impl)} `).join('')}
- \` : ''}
+ ` : ''}
- \`).join('')}
+ `).join('')}
- \`);
+ `);
}
// Resolved conflicts
if (conflictResolution.resolved_conflicts && conflictResolution.resolved_conflicts.length > 0) {
- sections.push(\`
+ sections.push(`
- \${conflictResolution.resolved_conflicts.map(conflict => \`
+ ${conflictResolution.resolved_conflicts.map(conflict => `
- \${escapeHtml(conflict.id || 'N/A')}
- \${escapeHtml(conflict.category || 'General')}
+ ${escapeHtml(conflict.id || 'N/A')}
+ ${escapeHtml(conflict.category || 'General')}
-
\${escapeHtml(conflict.brief || '')}
+
${escapeHtml(conflict.brief || '')}
Strategy:
- \${escapeHtml(conflict.strategy || 'N/A')}
+ ${escapeHtml(conflict.strategy || 'N/A')}
- \`).join('')}
+ `).join('')}
- \`);
+ `);
}
- return \`\${sections.join('')}
\`;
+ return `${sections.join('')}
`;
}
diff --git a/ccw/src/templates/dashboard-js/components/tabs-other.js b/ccw/src/templates/dashboard-js/components/tabs-other.js
index a5f4fa36..fd3952a6 100644
--- a/ccw/src/templates/dashboard-js/components/tabs-other.js
+++ b/ccw/src/templates/dashboard-js/components/tabs-other.js
@@ -281,14 +281,9 @@ function renderExplorationContext(explorations) {
function renderExplorationAngle(angle, data) {
let content = [];
- // Project structure (architecture)
+ // Project structure - handle string or object
if (data.project_structure) {
- content.push(`
-
-
Project Structure
-
${escapeHtml(data.project_structure)}
-
- `);
+ content.push(renderExpField('Project Structure', data.project_structure));
}
// Relevant files
@@ -310,69 +305,30 @@ function renderExplorationAngle(angle, data) {
`);
}
- // Patterns
+ // Patterns - handle string or object
if (data.patterns) {
- content.push(`
-
-
Patterns
-
${escapeHtml(data.patterns)}
-
- `);
+ content.push(renderExpField('Patterns', data.patterns));
}
- // Dependencies
+ // Dependencies - handle string or object
if (data.dependencies) {
- content.push(`
-
-
Dependencies
-
${escapeHtml(data.dependencies)}
-
- `);
+ content.push(renderExpField('Dependencies', data.dependencies));
}
- // Integration points
+ // Integration points - handle string or object
if (data.integration_points) {
- content.push(`
-
-
Integration Points
-
${escapeHtml(data.integration_points)}
-
- `);
+ content.push(renderExpField('Integration Points', data.integration_points));
}
- // Constraints
+ // Constraints - handle string or object
if (data.constraints) {
- content.push(`
-
-
Constraints
-
${escapeHtml(data.constraints)}
-
- `);
+ content.push(renderExpField('Constraints', data.constraints));
}
- // Clarification needs
- if (data.clarification_needs && data.clarification_needs.length) {
- content.push(`
-
-
Clarification Needs
-
- ${data.clarification_needs.map(c => `
-
-
${escapeHtml(c.question)}
- ${c.options && c.options.length ? `
-
- ${c.options.map((opt, i) => `
- ${escapeHtml(opt)}
- `).join('')}
-
- ` : ''}
-
- `).join('')}
-
-
- `);
+ // Clarification needs - handle array or object
+ if (data.clarification_needs) {
+ content.push(renderExpField('Clarification Needs', data.clarification_needs));
}
- const result = content.join('') || 'No data available
';
- return result;
+ return content.join('') || 'No data available
';
}
diff --git a/ccw/src/templates/dashboard.css b/ccw/src/templates/dashboard.css
index a2be9e24..c78423fe 100644
--- a/ccw/src/templates/dashboard.css
+++ b/ccw/src/templates/dashboard.css
@@ -3704,7 +3704,109 @@ ol.step-commands code {
white-space: pre-wrap;
}
+
/* Relevant Files Grid */
+/* Exploration Object Rendering */
+.exp-object {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 12px;
+ background: var(--bg-secondary, #f9fafb);
+ border-radius: 8px;
+ border: 1px solid var(--border-color, #e5e7eb);
+}
+
+.exp-obj-field {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: flex-start;
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.exp-obj-key {
+ font-weight: 600;
+ color: var(--text-secondary, #6b7280);
+ min-width: 120px;
+}
+
+.exp-obj-val {
+ color: var(--text-primary, #374151);
+ flex: 1;
+}
+
+.exp-obj-nested {
+ margin-left: 16px;
+ padding: 8px;
+ border-left: 2px solid var(--border-color, #e5e7eb);
+}
+
+.exp-obj-nested > .exp-obj-key {
+ display: block;
+ margin-bottom: 8px;
+ color: var(--primary, #3b82f6);
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.exp-list {
+ margin: 4px 0 0 0;
+ padding-left: 20px;
+ list-style: disc;
+}
+
+.exp-list li {
+ font-size: 13px;
+ color: var(--text-primary, #374151);
+ line-height: 1.6;
+ margin-bottom: 4px;
+}
+
+.exp-array-objects {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.exp-object-item {
+ padding: 10px;
+ background: var(--bg-primary, #fff);
+ border-radius: 6px;
+ border: 1px solid var(--border-color, #e5e7eb);
+}
+
+.clarification-impact {
+ font-size: 12px;
+ color: var(--text-muted, #9ca3af);
+ margin-top: 6px;
+}
+
+.priority-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.priority-badge.priority-high {
+ background: var(--bg-danger, #fee2e2);
+ color: var(--text-danger, #dc2626);
+}
+
+.priority-badge.priority-medium {
+ background: var(--bg-warning, #fef3c7);
+ color: var(--text-warning, #d97706);
+}
+
+.priority-badge.priority-low {
+ background: var(--bg-success, #d1fae5);
+ color: var(--text-success, #059669);
+}
.relevant-files-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
diff --git a/ccw/src/templates/dashboard.js.backup b/ccw/src/templates/dashboard.js.backup
deleted file mode 100644
index fee91bd9..00000000
--- a/ccw/src/templates/dashboard.js.backup
+++ /dev/null
@@ -1,4459 +0,0 @@
-// Data placeholder - will be replaced by generator
-let workflowData = {{WORKFLOW_DATA}};
-let projectPath = '{{PROJECT_PATH}}';
-let recentPaths = {{RECENT_PATHS}};
-
-// State
-let currentFilter = 'all';
-let currentLiteType = null;
-let currentView = 'sessions'; // 'sessions' or 'liteTasks'
-let currentSessionDetailKey = null; // For detail page view
-
-// Initialize
-document.addEventListener('DOMContentLoaded', async () => {
- initTheme();
- initSidebar();
- initPathSelector();
- initNavigation();
- initSearch();
-
- // Server mode: load data from API
- if (window.SERVER_MODE) {
- await switchToPath(window.INITIAL_PATH || projectPath);
- } else {
- renderDashboard();
- }
-});
-
-// Theme
-function initTheme() {
- const saved = localStorage.getItem('theme') || 'light';
- document.documentElement.setAttribute('data-theme', saved);
- updateThemeIcon(saved);
-
- document.getElementById('themeToggle').addEventListener('click', () => {
- const current = document.documentElement.getAttribute('data-theme');
- const next = current === 'light' ? 'dark' : 'light';
- document.documentElement.setAttribute('data-theme', next);
- localStorage.setItem('theme', next);
- updateThemeIcon(next);
- });
-}
-
-function updateThemeIcon(theme) {
- document.getElementById('themeToggle').textContent = theme === 'light' ? '๐' : 'โ๏ธ';
-}
-
-// Sidebar
-function initSidebar() {
- const sidebar = document.getElementById('sidebar');
- const toggle = document.getElementById('sidebarToggle');
- const menuToggle = document.getElementById('menuToggle');
- const overlay = document.getElementById('sidebarOverlay');
-
- // Restore collapsed state
- if (localStorage.getItem('sidebarCollapsed') === 'true') {
- sidebar.classList.add('collapsed');
- }
-
- toggle.addEventListener('click', () => {
- sidebar.classList.toggle('collapsed');
- localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
- });
-
- // Mobile menu
- menuToggle.addEventListener('click', () => {
- sidebar.classList.toggle('open');
- overlay.classList.toggle('open');
- });
-
- overlay.addEventListener('click', () => {
- sidebar.classList.remove('open');
- overlay.classList.remove('open');
- });
-}
-
-// Path Selector
-function initPathSelector() {
- const btn = document.getElementById('pathButton');
- const menu = document.getElementById('pathMenu');
- const recentContainer = document.getElementById('recentPaths');
-
- // Render recent paths
- if (recentPaths && recentPaths.length > 0) {
- recentPaths.forEach(path => {
- const item = document.createElement('div');
- item.className = 'path-item' + (path === projectPath ? ' active' : '');
- item.textContent = path;
- item.dataset.path = path;
- item.addEventListener('click', () => selectPath(path));
- recentContainer.appendChild(item);
- });
- }
-
- btn.addEventListener('click', (e) => {
- e.stopPropagation();
- menu.classList.toggle('open');
- });
-
- document.addEventListener('click', () => {
- menu.classList.remove('open');
- });
-
- document.getElementById('browsePath').addEventListener('click', async () => {
- await browseForFolder();
- });
-}
-
-async function browseForFolder() {
- // Try modern File System Access API first
- if ('showDirectoryPicker' in window) {
- try {
- const dirHandle = await window.showDirectoryPicker({
- mode: 'read',
- startIn: 'documents'
- });
- // Get the directory name (we can't get full path for security reasons)
- const dirName = dirHandle.name;
- showPathSelectedModal(dirName, dirHandle);
- return;
- } catch (err) {
- if (err.name === 'AbortError') {
- // User cancelled
- return;
- }
- console.warn('Directory picker failed:', err);
- }
- }
-
- // Fallback: show input dialog
- showPathInputModal();
-}
-
-// SVG Icons
-const icons = {
- folder: ' ',
- check: ' ',
- copy: ' ',
- terminal: ' '
-};
-
-function showPathSelectedModal(dirName, dirHandle) {
- // Try to guess full path based on current project path
- const currentPath = projectPath || '';
- const basePath = currentPath.substring(0, currentPath.lastIndexOf('/')) || 'D:/projects';
- const suggestedPath = basePath + '/' + dirName;
-
- const modal = document.createElement('div');
- modal.className = 'path-modal-overlay';
- modal.innerHTML = `
-
-
-
-
- ${dirName}
-
-
- Confirm or edit the full path:
-
-
- Full path:
-
- Open
-
-
-
-
- `;
- document.body.appendChild(modal);
-
- // Add event listeners (use arrow functions to ensure proper scope)
- document.getElementById('pathGoBtn').addEventListener('click', () => {
- console.log('Open button clicked');
- goToPath();
- });
- document.getElementById('pathCancelBtn').addEventListener('click', () => closePathModal());
-
- // Focus input, select all text, and add enter key listener
- setTimeout(() => {
- const input = document.getElementById('fullPathInput');
- input?.focus();
- input?.select();
- input?.addEventListener('keypress', (e) => {
- if (e.key === 'Enter') goToPath();
- });
- }, 100);
-}
-
-function showPathInputModal() {
- const modal = document.createElement('div');
- modal.className = 'path-modal-overlay';
- modal.innerHTML = `
-
-
-
-
- Project path:
-
- Open
-
-
-
-
- `;
- document.body.appendChild(modal);
-
- // Add event listeners (use arrow functions to ensure proper scope)
- document.getElementById('pathGoBtn').addEventListener('click', () => {
- console.log('Open button clicked');
- goToPath();
- });
- document.getElementById('pathCancelBtn').addEventListener('click', () => closePathModal());
-
- // Focus input and add enter key listener
- setTimeout(() => {
- const input = document.getElementById('fullPathInput');
- input?.focus();
- input?.addEventListener('keypress', (e) => {
- if (e.key === 'Enter') goToPath();
- });
- }, 100);
-}
-
-function goToPath() {
- const input = document.getElementById('fullPathInput');
- const path = input?.value?.trim();
- if (path) {
- closePathModal();
- selectPath(path);
- } else {
- // Show error - input is empty
- input.style.borderColor = 'var(--danger-color)';
- input.placeholder = 'Please enter a path';
- input.focus();
- }
-}
-
-function closePathModal() {
- const modal = document.querySelector('.path-modal-overlay');
- if (modal) {
- modal.remove();
- }
-}
-
-function copyCommand(btn, dirName) {
- const input = document.getElementById('fullPathInput');
- const path = input?.value?.trim() || `[full-path-to-${dirName}]`;
- const command = `ccw view -p "${path}"`;
- navigator.clipboard.writeText(command).then(() => {
- btn.innerHTML = icons.check + ' Copied! ';
- setTimeout(() => { btn.innerHTML = icons.copy + ' Copy '; }, 2000);
- });
-}
-
-async function selectPath(path) {
- localStorage.setItem('selectedPath', path);
-
- // Server mode: load data dynamically
- if (window.SERVER_MODE) {
- await switchToPath(path);
- return;
- }
-
- // Static mode: show command to run
- const modal = document.createElement('div');
- modal.className = 'path-modal-overlay';
- modal.innerHTML = `
-
-
-
-
To view the dashboard for this project, run:
-
- ccw view -p "${path}"
- ${icons.copy} Copy
-
-
- Or use ccw serve for live path switching.
-
-
-
-
- `;
- document.body.appendChild(modal);
-
- // Add copy handler
- document.getElementById('copyCommandBtn').addEventListener('click', function() {
- navigator.clipboard.writeText('ccw view -p "' + path + '"').then(() => {
- this.innerHTML = icons.check + ' Copied! ';
- setTimeout(() => { this.innerHTML = icons.copy + ' Copy '; }, 2000);
- });
- });
-}
-
-// Switch to a new project path (server mode only)
-async function switchToPath(path) {
- // Show loading state
- const container = document.getElementById('mainContent');
- container.innerHTML = 'Loading...
';
-
- try {
- const data = await loadDashboardData(path);
- if (data) {
- // Update global data
- workflowData = data;
- projectPath = data.projectPath;
- recentPaths = data.recentPaths || [];
-
- // Update UI
- document.getElementById('currentPath').textContent = projectPath;
- renderDashboard();
- refreshRecentPaths();
- }
- } catch (err) {
- console.error('Failed to switch path:', err);
- container.innerHTML = 'Failed to load project data
';
- }
-}
-
-// Refresh recent paths dropdown
-function refreshRecentPaths() {
- const recentContainer = document.getElementById('recentPaths');
- recentContainer.innerHTML = '';
-
- recentPaths.forEach(path => {
- const item = document.createElement('div');
- item.className = 'path-item' + (path === projectPath ? ' active' : '');
- item.textContent = path;
- item.dataset.path = path;
- item.addEventListener('click', () => selectPath(path));
- recentContainer.appendChild(item);
- });
-}
-
-// Navigation
-function initNavigation() {
- document.querySelectorAll('.nav-item[data-filter]').forEach(item => {
- item.addEventListener('click', () => {
- setActiveNavItem(item);
- currentFilter = item.dataset.filter;
- currentLiteType = null;
- currentView = 'sessions';
- currentSessionDetailKey = null;
- updateContentTitle();
- renderSessions();
- });
- });
-
- // Lite Tasks Navigation
- document.querySelectorAll('.nav-item[data-lite]').forEach(item => {
- item.addEventListener('click', () => {
- setActiveNavItem(item);
- currentLiteType = item.dataset.lite;
- currentFilter = null;
- currentView = 'liteTasks';
- currentSessionDetailKey = null;
- updateContentTitle();
- renderLiteTasks();
- });
- });
-
- // Project Overview Navigation
- document.querySelectorAll('.nav-item[data-view]').forEach(item => {
- item.addEventListener('click', () => {
- setActiveNavItem(item);
- currentView = item.dataset.view;
- currentFilter = null;
- currentLiteType = null;
- currentSessionDetailKey = null;
- updateContentTitle();
- renderProjectOverview();
- });
- });
-}
-
-function setActiveNavItem(item) {
- document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
- item.classList.add('active');
-}
-
-function updateContentTitle() {
- const titleEl = document.getElementById('contentTitle');
- if (currentView === 'project-overview') {
- titleEl.textContent = 'Project Overview';
- } else if (currentView === 'liteTasks') {
- const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
- titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
- } else if (currentView === 'sessionDetail') {
- titleEl.textContent = 'Session Detail';
- } else if (currentView === 'liteTaskDetail') {
- titleEl.textContent = 'Lite Task Detail';
- } else {
- const names = { 'all': 'All Sessions', 'active': 'Active Sessions', 'archived': 'Archived Sessions' };
- titleEl.textContent = names[currentFilter] || 'Sessions';
- }
-}
-
-// Search
-function initSearch() {
- const input = document.getElementById('searchInput');
- input.addEventListener('input', (e) => {
- const query = e.target.value.toLowerCase();
- document.querySelectorAll('.session-card').forEach(card => {
- const text = card.textContent.toLowerCase();
- card.style.display = text.includes(query) ? '' : 'none';
- });
- });
-}
-
-// Render Dashboard
-function renderDashboard() {
- updateStats();
- updateBadges();
- renderSessions();
- document.getElementById('generatedAt').textContent = workflowData.generatedAt || new Date().toISOString();
-}
-
-function updateStats() {
- const stats = workflowData.statistics || {};
- document.getElementById('statTotalSessions').textContent = stats.totalSessions || 0;
- document.getElementById('statActiveSessions').textContent = stats.activeSessions || 0;
- document.getElementById('statTotalTasks').textContent = stats.totalTasks || 0;
- document.getElementById('statCompletedTasks').textContent = stats.completedTasks || 0;
-}
-
-function updateBadges() {
- const active = workflowData.activeSessions || [];
- const archived = workflowData.archivedSessions || [];
-
- document.getElementById('badgeAll').textContent = active.length + archived.length;
- document.getElementById('badgeActive').textContent = active.length;
- document.getElementById('badgeArchived').textContent = archived.length;
-
- // Lite Tasks badges
- const liteTasks = workflowData.liteTasks || {};
- document.getElementById('badgeLitePlan').textContent = liteTasks.litePlan?.length || 0;
- document.getElementById('badgeLiteFix').textContent = liteTasks.liteFix?.length || 0;
-}
-
-function renderSessions() {
- const container = document.getElementById('mainContent');
-
- let sessions = [];
-
- if (currentFilter === 'all' || currentFilter === 'active') {
- sessions = sessions.concat((workflowData.activeSessions || []).map(s => ({ ...s, _isActive: true })));
- }
- if (currentFilter === 'all' || currentFilter === 'archived') {
- sessions = sessions.concat((workflowData.archivedSessions || []).map(s => ({ ...s, _isActive: false })));
- }
-
- if (sessions.length === 0) {
- container.innerHTML = `
-
-
๐ญ
-
No Sessions Found
-
No workflow sessions match your current filter.
-
- `;
- return;
- }
-
- container.innerHTML = `${sessions.map(session => renderSessionCard(session)).join('')}
`;
-}
-
-// Store session data for modal access
-const sessionDataStore = {};
-
-function renderSessionCard(session) {
- const tasks = session.tasks || [];
- const taskCount = session.taskCount || tasks.length;
- const completed = tasks.filter(t => t.status === 'completed').length;
- const progress = taskCount > 0 ? Math.round((completed / taskCount) * 100) : 0;
-
- // Use _isActive flag set during rendering, default to true
- const isActive = session._isActive !== false;
- const date = session.created_at;
-
- // Get session type badge
- const sessionType = session.type || 'workflow';
- const typeBadge = sessionType !== 'workflow' ? `${sessionType} ` : '';
-
- // Store session data for modal
- const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
- sessionDataStore[sessionKey] = session;
-
- return `
-
-
-
-
- ๐
${formatDate(date)}
- ๐ ${taskCount} tasks
-
- ${taskCount > 0 ? `
-
-
Progress
-
-
-
${completed}/${taskCount} (${progress}%)
-
-
- ` : ''}
-
-
- `;
-}
-
-// Session Detail Page
-function showSessionDetailPage(sessionKey) {
- const session = sessionDataStore[sessionKey];
- if (!session) return;
-
- currentView = 'sessionDetail';
- currentSessionDetailKey = sessionKey;
- updateContentTitle();
-
- const container = document.getElementById('mainContent');
- const sessionType = session.type || 'workflow';
-
- // Render specialized pages for review and test-fix sessions
- if (sessionType === 'review' || sessionType === 'review-cycle') {
- container.innerHTML = renderReviewSessionDetailPage(session);
- initReviewSessionPage(session);
- return;
- }
-
- if (sessionType === 'test-fix' || sessionType === 'fix') {
- container.innerHTML = renderFixSessionDetailPage(session);
- initFixSessionPage(session);
- return;
- }
-
- // Default workflow session detail page
- const tasks = session.tasks || [];
- const completed = tasks.filter(t => t.status === 'completed').length;
- const inProgress = tasks.filter(t => t.status === 'in_progress').length;
- const pending = tasks.filter(t => t.status === 'pending').length;
- const isActive = session._isActive !== false;
-
- container.innerHTML = `
-
-
-
-
-
-
-
- Created:
- ${formatDate(session.created_at)}
-
- ${session.archived_at ? `
-
- Archived:
- ${formatDate(session.archived_at)}
-
- ` : ''}
-
- Project:
- ${escapeHtml(session.project || '-')}
-
-
- Tasks:
- ${completed}/${tasks.length} completed
-
-
-
-
-
-
- ๐
- Tasks
- ${tasks.length}
-
-
- ๐ฆ
- Context
-
-
- ๐
- Summary
-
-
- ๐
- IMPL Plan
-
- ${session.hasReview ? `
-
- ๐
- Review
-
- ` : ''}
-
-
-
-
- ${renderTasksTab(session, tasks, completed, inProgress, pending)}
-
-
- `;
-}
-
-function goBackToSessions() {
- currentView = 'sessions';
- currentSessionDetailKey = null;
- updateContentTitle();
- renderSessions();
-}
-
-function switchDetailTab(tabName) {
- // Update active tab
- document.querySelectorAll('.detail-tab').forEach(tab => {
- tab.classList.toggle('active', tab.dataset.tab === tabName);
- });
-
- const session = sessionDataStore[currentSessionDetailKey];
- if (!session) return;
-
- const contentArea = document.getElementById('detailTabContent');
- const tasks = session.tasks || [];
- const completed = tasks.filter(t => t.status === 'completed').length;
- const inProgress = tasks.filter(t => t.status === 'in_progress').length;
- const pending = tasks.filter(t => t.status === 'pending').length;
-
- switch (tabName) {
- case 'tasks':
- contentArea.innerHTML = renderTasksTab(session, tasks, completed, inProgress, pending);
- break;
- case 'context':
- loadAndRenderContextTab(session, contentArea);
- break;
- case 'summary':
- loadAndRenderSummaryTab(session, contentArea);
- break;
- case 'impl-plan':
- loadAndRenderImplPlanTab(session, contentArea);
- break;
- case 'review':
- loadAndRenderReviewTab(session, contentArea);
- break;
- }
-}
-
-function renderTasksTab(session, tasks, completed, inProgress, pending) {
- // Populate drawer tasks for click-to-open functionality
- currentDrawerTasks = tasks;
-
- // Auto-load full task details in server mode
- if (window.SERVER_MODE && session.path) {
- // Schedule auto-load after DOM render
- setTimeout(() => loadFullTaskDetails(), 50);
- }
-
- // Show task list with loading state or basic list
- const showLoading = window.SERVER_MODE && session.path;
-
- return `
-
-
- โ ${completed} completed
- โณ ${inProgress} in progress
- โ ${pending} pending
-
-
- ${showLoading ? `
-
Loading task details...
- ` : (tasks.length === 0 ? `
-
-
๐
-
No Tasks
-
This session has no tasks defined.
-
- ` : tasks.map(task => renderDetailTaskItem(task)).join(''))}
-
-
- `;
-}
-
-async function loadFullTaskDetails() {
- const session = sessionDataStore[currentSessionDetailKey];
- if (!session || !window.SERVER_MODE || !session.path) return;
-
- const tasksContainer = document.getElementById('tasksListContent');
- tasksContainer.innerHTML = 'Loading full task details...
';
-
- try {
- const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=tasks`);
- if (response.ok) {
- const data = await response.json();
- if (data.tasks && data.tasks.length > 0) {
- // Populate drawer tasks for click-to-open functionality
- currentDrawerTasks = data.tasks;
- tasksContainer.innerHTML = data.tasks.map(task => renderDetailTaskItem(task)).join('');
- } else {
- tasksContainer.innerHTML = `
-
-
๐
-
No Task Files
-
No IMPL-*.json files found in .task/
-
- `;
- }
- }
- } catch (err) {
- tasksContainer.innerHTML = `Failed to load tasks: ${err.message}
`;
- }
-}
-
-function renderDetailTaskItem(task) {
- const taskId = task.task_id || task.id || 'Unknown';
- const status = task.status || 'pending';
-
- // Simplified task card with only essential elements: task ID badge, title, and status badge
- // Includes status class for border-left color and status-${status} class for background color
- return `
-
-
-
- `;
-}
-
-function getMetaPreview(task) {
- const meta = task.meta || {};
- const parts = [];
- if (meta.type) parts.push(meta.type);
- if (meta.action) parts.push(meta.action);
- if (meta.scope) parts.push(meta.scope);
- return parts.join(' | ') || 'No meta';
-}
-
-function getTaskContextPreview(task) {
- const items = [];
- const ctx = task.context || {};
- if (ctx.requirements?.length) items.push(`${ctx.requirements.length} reqs`);
- if (ctx.focus_paths?.length) items.push(`${ctx.focus_paths.length} paths`);
- if (task.modification_points?.length) items.push(`${task.modification_points.length} mods`);
- if (task.description) items.push('desc');
- return items.join(' | ') || 'No context';
-}
-
-function getFlowPreview(task) {
- const steps = task.flow_control?.implementation_approach?.length || task.implementation?.length || 0;
- return steps > 0 ? `${steps} steps` : 'No steps';
-}
-
-function renderTaskContext(task) {
- const sections = [];
- const ctx = task.context || {};
-
- // Description
- if (task.description) {
- sections.push(`
-
-
description:
-
${escapeHtml(task.description)}
-
- `);
- }
-
- // Requirements
- if (ctx.requirements?.length) {
- sections.push(`
-
-
requirements:
-
${ctx.requirements.map(r => `${escapeHtml(r)} `).join('')}
-
- `);
- }
-
- // Focus paths
- if (ctx.focus_paths?.length) {
- sections.push(`
-
-
focus_paths:
-
${ctx.focus_paths.map(p => `${escapeHtml(p)} `).join('')}
-
- `);
- }
-
- // Modification points
- if (task.modification_points?.length) {
- sections.push(`
-
-
modification_points:
-
- ${task.modification_points.map(m => `
-
-
${escapeHtml(m.file || m)}
- ${m.target ? `
โ ${escapeHtml(m.target)} ` : ''}
- ${m.change ? `
${escapeHtml(m.change)}
` : ''}
-
- `).join('')}
-
-
- `);
- }
-
- // Acceptance criteria
- const acceptance = ctx.acceptance || task.acceptance || [];
- if (acceptance.length) {
- sections.push(`
-
-
acceptance:
-
${acceptance.map(a => `${escapeHtml(a)} `).join('')}
-
- `);
- }
-
- return sections.length > 0
- ? `${sections.join('')}
`
- : 'No context data
';
-}
-
-function renderFlowControl(task) {
- const sections = [];
- const fc = task.flow_control || {};
-
- // Implementation approach
- const steps = fc.implementation_approach || task.implementation || [];
- if (steps.length) {
- sections.push(`
-
-
implementation_approach:
-
- ${steps.map(s => `${escapeHtml(typeof s === 'string' ? s : s.step || s.action || JSON.stringify(s))} `).join('')}
-
-
- `);
- }
-
- // Pre-analysis
- const preAnalysis = fc.pre_analysis || task.pre_analysis || [];
- if (preAnalysis.length) {
- sections.push(`
-
-
pre_analysis:
-
${preAnalysis.map(p => `${escapeHtml(p)} `).join('')}
-
- `);
- }
-
- // Target files
- const targetFiles = fc.target_files || task.target_files || [];
- if (targetFiles.length) {
- sections.push(`
-
-
target_files:
-
${targetFiles.map(f => `${escapeHtml(f)} `).join('')}
-
- `);
- }
-
- return sections.length > 0
- ? `${sections.join('')}
`
- : 'No flow control data
';
-}
-
-async function loadAndRenderContextTab(session, contentArea) {
- contentArea.innerHTML = 'Loading context data...
';
-
- try {
- // Try to load context-package.json from server
- if (window.SERVER_MODE && session.path) {
- const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`);
- if (response.ok) {
- const data = await response.json();
- contentArea.innerHTML = renderContextContent(data.context);
- return;
- }
- }
- // Fallback: show placeholder
- contentArea.innerHTML = `
-
-
๐ฆ
-
Context Data
-
Context data will be loaded from context-package.json
-
- `;
- } catch (err) {
- contentArea.innerHTML = `Failed to load context: ${err.message}
`;
- }
-}
-
-function renderContextContent(context) {
- if (!context) {
- return `
-
-
๐ฆ
-
No Context Data
-
No context-package.json found for this session.
-
- `;
- }
-
- const contextJson = JSON.stringify(context, null, 2);
- // Store in global variable for modal access
- window._currentContextJson = contextJson;
-
- // Parse context structure
- const metadata = context.metadata || {};
- const projectContext = context.project_context || {};
- const techStack = projectContext.tech_stack || metadata.tech_stack || {};
- const codingConventions = projectContext.coding_conventions || {};
- const architecturePatterns = projectContext.architecture_patterns || [];
- const assets = context.assets || {};
- const dependencies = context.dependencies || {};
- const testContext = context.test_context || {};
- const conflictDetection = context.conflict_detection || {};
-
- return `
-
-
-
-
Context Package
-
- ๐๏ธ View JSON
-
-
-
-
- ${metadata.task_description || metadata.session_id ? `
-
-
๐ Task Metadata
-
- ${metadata.task_description ? `
-
- Description:
- ${escapeHtml(metadata.task_description)}
-
- ` : ''}
- ${metadata.session_id ? `
-
- Session ID:
- ${escapeHtml(metadata.session_id)}
-
- ` : ''}
- ${metadata.complexity ? `
-
- Complexity:
- ${escapeHtml(metadata.complexity)}
-
- ` : ''}
- ${metadata.timestamp ? `
-
- Timestamp:
- ${escapeHtml(metadata.timestamp)}
-
- ` : ''}
- ${metadata.keywords && metadata.keywords.length > 0 ? `
-
-
Keywords:
-
- ${metadata.keywords.map(kw => `${escapeHtml(kw)} `).join('')}
-
-
- ` : ''}
-
-
- ` : ''}
-
-
- ${architecturePatterns.length > 0 ? `
-
-
๐๏ธ Architecture Patterns
-
- ${architecturePatterns.map(p => `${escapeHtml(p)} `).join('')}
-
-
- ` : ''}
-
-
- ${Object.keys(techStack).length > 0 ? `
-
-
โ๏ธ Technology Stack
-
- ${renderTechStackSection(techStack)}
-
-
- ` : ''}
-
-
- ${Object.keys(codingConventions).length > 0 ? `
-
-
๐ Coding Conventions
-
- ${renderCodingConventions(codingConventions)}
-
-
- ` : ''}
-
-
- ${Object.keys(assets).length > 0 ? `
-
-
๐ Assets & Resources
-
- ${renderAssetsSection(assets)}
-
-
- ` : ''}
-
-
- ${(dependencies.internal && dependencies.internal.length > 0) || (dependencies.external && dependencies.external.length > 0) ? `
-
-
๐ Dependencies
-
- ${renderDependenciesSection(dependencies)}
-
-
- ` : ''}
-
-
- ${Object.keys(testContext).length > 0 ? `
-
-
๐งช Test Context
-
- ${renderTestContextSection(testContext)}
-
-
- ` : ''}
-
-
- ${Object.keys(conflictDetection).length > 0 ? `
-
-
โ ๏ธ Conflict Detection & Risk Analysis
-
- ${renderConflictDetectionSection(conflictDetection)}
-
-
- ` : ''}
-
- `;
-}
-
-function renderTechStackSection(techStack) {
- const sections = [];
-
- if (techStack.languages) {
- const langs = Array.isArray(techStack.languages) ? techStack.languages : [techStack.languages];
- sections.push(`
-
-
Languages:
-
- ${langs.map(l => `${escapeHtml(String(l))} `).join('')}
-
-
- `);
- }
-
- if (techStack.frameworks) {
- const frameworks = Array.isArray(techStack.frameworks) ? techStack.frameworks : [techStack.frameworks];
- sections.push(`
-
-
Frameworks:
-
- ${frameworks.map(f => `${escapeHtml(String(f))} `).join('')}
-
-
- `);
- }
-
- if (techStack.frontend_frameworks) {
- const ff = Array.isArray(techStack.frontend_frameworks) ? techStack.frontend_frameworks : [techStack.frontend_frameworks];
- sections.push(`
-
-
Frontend:
-
- ${ff.map(f => `${escapeHtml(String(f))} `).join('')}
-
-
- `);
- }
-
- if (techStack.backend_frameworks) {
- const bf = Array.isArray(techStack.backend_frameworks) ? techStack.backend_frameworks : [techStack.backend_frameworks];
- sections.push(`
-
-
Backend:
-
- ${bf.map(f => `${escapeHtml(String(f))} `).join('')}
-
-
- `);
- }
-
- if (techStack.libraries) {
- const libs = techStack.libraries;
- if (typeof libs === 'object' && !Array.isArray(libs)) {
- Object.entries(libs).forEach(([category, libList]) => {
- if (Array.isArray(libList) && libList.length > 0) {
- sections.push(`
-
-
${escapeHtml(category)}:
-
- ${libList.map(lib => `${escapeHtml(String(lib))} `).join('')}
-
-
- `);
- }
- });
- }
- }
-
- return sections.join('');
-}
-
-function renderCodingConventions(conventions) {
- const sections = [];
-
- if (conventions.naming) {
- sections.push(`
-
-
Naming:
-
- ${Object.entries(conventions.naming).map(([key, val]) =>
- `${escapeHtml(key)}: ${escapeHtml(String(val))} `
- ).join('')}
-
-
- `);
- }
-
- if (conventions.error_handling) {
- sections.push(`
-
-
Error Handling:
-
- ${Object.entries(conventions.error_handling).map(([key, val]) =>
- `${escapeHtml(key)}: ${escapeHtml(String(val))} `
- ).join('')}
-
-
- `);
- }
-
- if (conventions.testing) {
- sections.push(`
-
-
Testing:
-
- ${Object.entries(conventions.testing).map(([key, val]) => {
- if (Array.isArray(val)) {
- return `${escapeHtml(key)}: ${val.map(v => escapeHtml(String(v))).join(', ')} `;
- }
- return `${escapeHtml(key)}: ${escapeHtml(String(val))} `;
- }).join('')}
-
-
- `);
- }
-
- return sections.join('');
-}
-
-function renderAssetsSection(assets) {
- const sections = [];
-
- // Documentation
- if (assets.documentation && assets.documentation.length > 0) {
- sections.push(`
-
-
๐ Documentation
-
- ${assets.documentation.map(doc => `
-
-
-
-
${escapeHtml(doc.scope || '')}
- ${doc.contains && doc.contains.length > 0 ? `
-
- ${doc.contains.map(tag => `${escapeHtml(tag)} `).join('')}
-
- ` : ''}
-
-
- `).join('')}
-
-
- `);
- }
-
- // Source Code
- if (assets.source_code && assets.source_code.length > 0) {
- sections.push(`
-
-
๐ป Source Code
-
- ${assets.source_code.map(src => `
-
-
-
-
${escapeHtml(src.role || '')}
- ${src.exports && src.exports.length > 0 ? `
-
Exports: ${src.exports.map(e => `${escapeHtml(e)}`).join(', ')}
- ` : ''}
- ${src.features && src.features.length > 0 ? `
-
${src.features.map(f => `${escapeHtml(f)} `).join('')}
- ` : ''}
-
-
- `).join('')}
-
-
- `);
- }
-
- // Tests
- if (assets.tests && assets.tests.length > 0) {
- sections.push(`
-
-
๐งช Tests
-
- ${assets.tests.map(test => `
-
-
-
-
${escapeHtml(test.type || '')}
- ${test.test_classes ? `
Classes: ${escapeHtml(test.test_classes.join(', '))}
` : ''}
- ${test.coverage ? `
Coverage: ${escapeHtml(test.coverage)}
` : ''}
-
-
- `).join('')}
-
-
- `);
- }
-
- return sections.join('');
-}
-
-function renderDependenciesSection(dependencies) {
- const sections = [];
-
- // Internal Dependencies
- if (dependencies.internal && dependencies.internal.length > 0) {
- sections.push(`
-
-
๐ Internal Dependencies
-
- ${dependencies.internal.slice(0, 10).map(dep => `
-
-
${escapeHtml(dep.from)}
-
- ${escapeHtml(dep.type)}
- โ
-
-
${escapeHtml(dep.to)}
-
- `).join('')}
- ${dependencies.internal.length > 10 ? `
... and ${dependencies.internal.length - 10} more
` : ''}
-
-
- `);
- }
-
- // External Dependencies
- if (dependencies.external && dependencies.external.length > 0) {
- sections.push(`
-
-
๐ฆ External Dependencies
-
- ${dependencies.external.map(dep => `
-
-
${escapeHtml(dep.package)}
-
${escapeHtml(dep.version || '')}
-
${escapeHtml(dep.usage || '')}
-
- `).join('')}
-
-
- `);
- }
-
- return sections.join('');
-}
-
-function renderTestContextSection(testContext) {
- const sections = [];
-
- // Test Frameworks
- if (testContext.frameworks) {
- const frameworks = testContext.frameworks;
- sections.push(`
-
-
๐ ๏ธ Test Frameworks
-
- ${frameworks.backend ? `
-
-
- ${frameworks.backend.plugins ? `
-
${frameworks.backend.plugins.map(p => `${escapeHtml(p)} `).join('')}
- ` : ''}
-
- ` : ''}
- ${frameworks.frontend ? `
-
-
- ${frameworks.frontend.recommended ? `
Recommended: ${escapeHtml(frameworks.frontend.recommended)}
` : ''}
- ${frameworks.frontend.gap ? `
โ ๏ธ ${escapeHtml(frameworks.frontend.gap)}
` : ''}
-
- ` : ''}
-
-
- `);
- }
-
- // Existing Tests Statistics
- if (testContext.existing_tests) {
- const tests = testContext.existing_tests;
- let totalTests = 0;
- let totalClasses = 0;
-
- if (tests.backend) {
- if (tests.backend.integration) {
- totalTests += tests.backend.integration.tests || 0;
- totalClasses += tests.backend.integration.classes || 0;
- }
- if (tests.backend.api_endpoints) {
- totalTests += tests.backend.api_endpoints.tests || 0;
- totalClasses += tests.backend.api_endpoints.classes || 0;
- }
- }
-
- sections.push(`
-
-
๐ Test Statistics
-
-
-
${totalTests}
-
Total Tests
-
-
-
${totalClasses}
-
Test Classes
-
- ${testContext.coverage_config && testContext.coverage_config.target ? `
-
-
${escapeHtml(testContext.coverage_config.target)}
-
Coverage Target
-
- ` : ''}
-
-
- `);
- }
-
- // Test Markers
- if (testContext.test_markers) {
- sections.push(`
-
-
๐ท๏ธ Test Markers
-
- ${Object.entries(testContext.test_markers).map(([marker, desc]) => `
-
- @${escapeHtml(marker)}
- ${escapeHtml(desc)}
-
- `).join('')}
-
-
- `);
- }
-
- return sections.join('');
-}
-
-function renderConflictDetectionSection(conflictDetection) {
- const sections = [];
-
- // Risk Level Indicator
- if (conflictDetection.risk_level) {
- const riskLevel = conflictDetection.risk_level;
- const riskColor = riskLevel === 'high' ? '#ef4444' : riskLevel === 'medium' ? '#f59e0b' : '#10b981';
- sections.push(`
-
-
- ${escapeHtml(riskLevel.toUpperCase())} RISK
-
- ${conflictDetection.mitigation_strategy ? `
-
- Mitigation Strategy: ${escapeHtml(conflictDetection.mitigation_strategy)}
-
- ` : ''}
-
- `);
- }
-
- // Risk Factors
- if (conflictDetection.risk_factors) {
- const factors = conflictDetection.risk_factors;
- sections.push(`
-
-
โ ๏ธ Risk Factors
-
- ${factors.test_gaps && factors.test_gaps.length > 0 ? `
-
-
Test Gaps:
-
- ${factors.test_gaps.map(gap => `${escapeHtml(gap)} `).join('')}
-
-
- ` : ''}
- ${factors.existing_implementations && factors.existing_implementations.length > 0 ? `
-
-
Existing Implementations:
-
- ${factors.existing_implementations.map(impl => `${escapeHtml(impl)} `).join('')}
-
-
- ` : ''}
-
-
- `);
- }
-
- // Affected Modules
- if (conflictDetection.affected_modules && conflictDetection.affected_modules.length > 0) {
- sections.push(`
-
-
๐ฆ Affected Modules
-
- ${conflictDetection.affected_modules.map(mod => `
- ${escapeHtml(mod)}
- `).join('')}
-
-
- `);
- }
-
- // Historical Conflicts
- if (conflictDetection.historical_conflicts && conflictDetection.historical_conflicts.length > 0) {
- sections.push(`
-
-
๐ Historical Lessons
-
- ${conflictDetection.historical_conflicts.map(conflict => `
-
-
Source: ${escapeHtml(conflict.source || 'Unknown')}
- ${conflict.lesson ? `
Lesson: ${escapeHtml(conflict.lesson)}
` : ''}
- ${conflict.recommendation ? `
Recommendation: ${escapeHtml(conflict.recommendation)}
` : ''}
- ${conflict.challenge ? `
Challenge: ${escapeHtml(conflict.challenge)}
` : ''}
-
- `).join('')}
-
-
- `);
- }
-
- return sections.join('');
-}
-
-// Helper functions
-function getRelevanceColor(score) {
- if (score >= 0.95) return '#10b981';
- if (score >= 0.90) return '#3b82f6';
- if (score >= 0.80) return '#f59e0b';
- return '#6b7280';
-}
-
-function getRoleBadgeClass(role) {
- const roleMap = {
- 'core-hook': 'primary',
- 'api-client': 'success',
- 'api-router': 'info',
- 'service-layer': 'warning',
- 'pydantic-schemas': 'secondary',
- 'orm-model': 'secondary',
- 'typescript-types': 'info'
- };
- return roleMap[role] || 'secondary';
-}
-
-async function loadAndRenderSummaryTab(session, contentArea) {
- contentArea.innerHTML = 'Loading summaries...
';
-
- try {
- if (window.SERVER_MODE && session.path) {
- const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
- if (response.ok) {
- const data = await response.json();
- contentArea.innerHTML = renderSummaryContent(data.summaries);
- return;
- }
- }
- contentArea.innerHTML = `
-
-
๐
-
Summaries
-
Session summaries will be loaded from .summaries/
-
- `;
- } catch (err) {
- contentArea.innerHTML = `Failed to load summaries: ${err.message}
`;
- }
-}
-
-function renderSummaryContent(summaries) {
- if (!summaries || summaries.length === 0) {
- return `
-
-
๐
-
No Summaries
-
No summaries found in .summaries/
-
- `;
- }
-
- // Store summaries in global variable for modal access
- window._currentSummaries = summaries;
-
- return `
-
- ${summaries.map((s, idx) => {
- const normalizedContent = normalizeLineEndings(s.content || '');
- return `
-
-
-
๐ ${escapeHtml(s.name || 'Summary')}
-
- ๐๏ธ View
-
-
-
${escapeHtml(normalizedContent)}
-
- `;
- }).join('')}
-
- `;
-}
-
-async function loadAndRenderImplPlanTab(session, contentArea) {
- contentArea.innerHTML = 'Loading IMPL plan...
';
-
- try {
- if (window.SERVER_MODE && session.path) {
- const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=impl-plan`);
- if (response.ok) {
- const data = await response.json();
- contentArea.innerHTML = renderImplPlanContent(data.implPlan);
- return;
- }
- }
- contentArea.innerHTML = `
-
-
๐
-
IMPL Plan
-
IMPL plan will be loaded from IMPL_PLAN.md
-
- `;
- } catch (err) {
- contentArea.innerHTML = `Failed to load IMPL plan: ${err.message}
`;
- }
-}
-
-function renderImplPlanContent(implPlan) {
- if (!implPlan) {
- return `
-
-
๐
-
No IMPL Plan
-
No IMPL_PLAN.md found for this session.
-
- `;
- }
-
- // Normalize and store in global variable for modal access
- const normalizedContent = normalizeLineEndings(implPlan);
- window._currentImplPlan = normalizedContent;
-
- return `
-
-
-
Implementation Plan
-
- ๐๏ธ View in Modal
-
-
-
${escapeHtml(normalizedContent)}
-
- `;
-}
-
-async function loadAndRenderReviewTab(session, contentArea) {
- contentArea.innerHTML = 'Loading review data...
';
-
- try {
- if (window.SERVER_MODE && session.path) {
- const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=review`);
- if (response.ok) {
- const data = await response.json();
- contentArea.innerHTML = renderReviewContent(data.review);
- return;
- }
- }
- contentArea.innerHTML = `
-
-
๐
-
Review Data
-
Review findings will be loaded from .review/
-
- `;
- } catch (err) {
- contentArea.innerHTML = `Failed to load review: ${err.message}
`;
- }
-}
-
-function renderReviewContent(review) {
- if (!review || !review.dimensions) {
- return `
-
-
๐
-
No Review Data
-
No review findings in .review/
-
- `;
- }
-
- const dimensions = Object.entries(review.dimensions);
- if (dimensions.length === 0) {
- return `
-
-
๐
-
No Findings
-
No review findings found.
-
- `;
- }
-
- return `
-
- ${dimensions.map(([dim, rawFindings]) => {
- // Normalize findings to always be an array
- let findings = [];
- if (Array.isArray(rawFindings)) {
- findings = rawFindings;
- } else if (rawFindings && typeof rawFindings === 'object') {
- // If it's an object with a findings array, use that
- if (Array.isArray(rawFindings.findings)) {
- findings = rawFindings.findings;
- } else {
- // Wrap single object in array or show raw JSON
- findings = [{ title: dim, description: JSON.stringify(rawFindings, null, 2), severity: 'info' }];
- }
- }
-
- return `
-
-
-
- ${findings.map(f => `
-
-
-
${escapeHtml(f.description || '')}
- ${f.file ? `
๐ ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}
` : ''}
-
- `).join('')}
-
-
- `}).join('')}
-
- `;
-}
-
-// ==========================================
-// REVIEW SESSION DETAIL PAGE
-// ==========================================
-function renderReviewSessionDetailPage(session) {
- const isActive = session._isActive !== false;
- const tasks = session.tasks || [];
- const dimensions = session.reviewDimensions || [];
-
- // Calculate review statistics
- const totalFindings = dimensions.reduce((sum, d) => sum + (d.findings?.length || 0), 0);
- const criticalCount = dimensions.reduce((sum, d) =>
- sum + (d.findings?.filter(f => f.severity === 'critical').length || 0), 0);
- const highCount = dimensions.reduce((sum, d) =>
- sum + (d.findings?.filter(f => f.severity === 'high').length || 0), 0);
-
- return `
-
-
-
-
-
-
-
-
-
-
-
-
๐
-
${totalFindings}
-
Total Findings
-
-
-
๐ด
-
${criticalCount}
-
Critical
-
-
-
๐
-
${highCount}
-
High
-
-
-
๐
-
${dimensions.length}
-
Dimensions
-
-
-
-
-
- ${dimensions.map((dim, idx) => `
-
-
D${idx + 1}
-
${escapeHtml(dim.name || 'Unknown')}
-
${dim.findings?.length || 0} findings
-
- `).join('')}
-
-
-
-
-
-
-
- ${renderReviewFindingsGrid(dimensions)}
-
-
-
-
-
-
- Created:
- ${formatDate(session.created_at)}
-
- ${session.archived_at ? `
-
- Archived:
- ${formatDate(session.archived_at)}
-
- ` : ''}
-
- Project:
- ${escapeHtml(session.project || '-')}
-
-
-
- `;
-}
-
-function renderReviewFindingsGrid(dimensions) {
- if (!dimensions || dimensions.length === 0) {
- return `
-
-
๐
-
No review dimensions found
-
- `;
- }
-
- let html = '';
- dimensions.forEach(dim => {
- const findings = dim.findings || [];
- if (findings.length === 0) return;
-
- html += `
-
-
-
- ${findings.map(f => `
-
-
-
${escapeHtml(f.title || 'Finding')}
-
${escapeHtml((f.description || '').substring(0, 100))}${f.description?.length > 100 ? '...' : ''}
- ${f.file ? `
๐ ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}
` : ''}
-
- `).join('')}
-
-
- `;
- });
-
- return html || '';
-}
-
-function initReviewSessionPage(session) {
- // Initialize event handlers for review session page
- // Filter handlers are inline onclick
-}
-
-function filterReviewFindings(severity) {
- // Update filter buttons
- document.querySelectorAll('.findings-filters .filter-btn').forEach(btn => {
- btn.classList.toggle('active', btn.dataset.severity === severity);
- });
-
- // Filter finding cards
- document.querySelectorAll('.finding-card').forEach(card => {
- if (severity === 'all' || card.dataset.severity === severity) {
- card.style.display = '';
- } else {
- card.style.display = 'none';
- }
- });
-}
-
-// ==========================================
-// FIX SESSION DETAIL PAGE
-// ==========================================
-function renderFixSessionDetailPage(session) {
- const isActive = session._isActive !== false;
- const tasks = session.tasks || [];
-
- // Calculate fix statistics
- const totalTasks = tasks.length;
- const fixedCount = tasks.filter(t => t.status === 'completed' && t.result === 'fixed').length;
- const failedCount = tasks.filter(t => t.status === 'completed' && t.result === 'failed').length;
- const pendingCount = tasks.filter(t => t.status === 'pending').length;
- const inProgressCount = tasks.filter(t => t.status === 'in_progress').length;
- const percentComplete = totalTasks > 0 ? ((fixedCount + failedCount) / totalTasks * 100) : 0;
-
- return `
-
-
-
-
-
-
-
-
-
-
-
- ${fixedCount + failedCount}/${totalTasks} completed (${percentComplete.toFixed(1)}%)
-
-
-
-
-
-
๐
-
${totalTasks}
-
Total Tasks
-
-
-
โ
-
${fixedCount}
-
Fixed
-
-
-
โ
-
${failedCount}
-
Failed
-
-
-
โณ
-
${pendingCount}
-
Pending
-
-
-
-
- ${session.stages && session.stages.length > 0 ? `
-
- ${session.stages.map((stage, idx) => `
-
-
Stage ${idx + 1}
-
${stage.execution_mode === 'parallel' ? 'โก Parallel' : 'โก๏ธ Serial'}
-
${stage.groups?.length || 0} groups
-
- `).join('')}
-
- ` : ''}
-
-
-
-
-
-
- ${renderFixTasksGrid(tasks)}
-
-
-
-
-
-
- Created:
- ${formatDate(session.created_at)}
-
- ${session.archived_at ? `
-
- Archived:
- ${formatDate(session.archived_at)}
-
- ` : ''}
-
- Project:
- ${escapeHtml(session.project || '-')}
-
-
-
- `;
-}
-
-function renderFixTasksGrid(tasks) {
- if (!tasks || tasks.length === 0) {
- return `
-
-
๐
-
No fix tasks found
-
- `;
- }
-
- return tasks.map(task => {
- const statusClass = task.status === 'completed' ? (task.result || 'completed') : task.status;
- const statusText = task.status === 'completed' ? (task.result || 'completed') : task.status;
-
- return `
-
-
-
${escapeHtml(task.title || 'Untitled Task')}
- ${task.finding_title ? `
${escapeHtml(task.finding_title)}
` : ''}
- ${task.file ? `
๐ ${escapeHtml(task.file)}${task.line ? ':' + task.line : ''}
` : ''}
-
- ${task.dimension ? `${escapeHtml(task.dimension)} ` : ''}
- ${task.attempts && task.attempts > 1 ? `๐ ${task.attempts} attempts ` : ''}
- ${task.commit_hash ? `๐พ ${task.commit_hash.substring(0, 7)} ` : ''}
-
-
- `;
- }).join('');
-}
-
-function initFixSessionPage(session) {
- // Initialize event handlers for fix session page
- // Filter handlers are inline onclick
-}
-
-function filterFixTasks(status) {
- // Update filter buttons
- document.querySelectorAll('.task-filters .filter-btn').forEach(btn => {
- btn.classList.toggle('active', btn.dataset.status === status);
- });
-
- // Filter task cards
- document.querySelectorAll('.fix-task-card').forEach(card => {
- if (status === 'all' || card.dataset.status === status) {
- card.style.display = '';
- } else {
- card.style.display = 'none';
- }
- });
-}
-
-function showRawSessionJson(sessionKey) {
- const session = sessionDataStore[sessionKey];
- if (!session) return;
-
- // Close current modal
- const currentModal = document.querySelector('.session-modal-overlay');
- if (currentModal) currentModal.remove();
-
- // Show JSON modal
- const overlay = document.createElement('div');
- overlay.className = 'json-modal-overlay active';
- overlay.innerHTML = `
-
-
-
-
${escapeHtml(JSON.stringify(session, null, 2))}
-
-
-
- `;
-
- document.body.appendChild(overlay);
-}
-
-// Render Lite Tasks
-function renderLiteTasks() {
- const container = document.getElementById('mainContent');
-
- const liteTasks = workflowData.liteTasks || {};
- const sessions = currentLiteType === 'lite-plan'
- ? liteTasks.litePlan || []
- : liteTasks.liteFix || [];
-
- if (sessions.length === 0) {
- container.innerHTML = `
-
-
โก
-
No ${currentLiteType} Sessions
-
No sessions found in .workflow/.${currentLiteType}/
-
- `;
- return;
- }
-
- container.innerHTML = `${sessions.map(session => renderLiteTaskCard(session)).join('')}
`;
-
- // Initialize collapsible sections
- document.querySelectorAll('.collapsible-header').forEach(header => {
- header.addEventListener('click', () => toggleSection(header));
- });
-
- // Render flowcharts for expanded tasks
- sessions.forEach(session => {
- session.tasks?.forEach(task => {
- if (task.flow_control?.implementation_approach) {
- renderFlowchartForTask(session.id, task);
- }
- });
- });
-}
-
-// Store lite task session data for detail page access
-const liteTaskDataStore = {};
-
-function renderLiteTaskCard(session) {
- const tasks = session.tasks || [];
-
- // Store session data for detail page
- const sessionKey = `lite-${session.type}-${session.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
- liteTaskDataStore[sessionKey] = session;
-
- return `
-
-
-
-
- ๐
${formatDate(session.createdAt)}
- ๐ ${tasks.length} tasks
-
-
-
- `;
-}
-
-// Lite Task Detail Page
-function showLiteTaskDetailPage(sessionKey) {
- const session = liteTaskDataStore[sessionKey];
- if (!session) return;
-
- currentView = 'liteTaskDetail';
- currentSessionDetailKey = sessionKey;
-
- // Also store in sessionDataStore for tab switching compatibility
- sessionDataStore[sessionKey] = {
- ...session,
- session_id: session.id,
- created_at: session.createdAt,
- path: session.path,
- type: session.type
- };
-
- const container = document.getElementById('mainContent');
- const tasks = session.tasks || [];
- const plan = session.plan || {};
- const progress = session.progress || { total: 0, completed: 0, percentage: 0 };
-
- const completed = tasks.filter(t => t.status === 'completed').length;
- const inProgress = tasks.filter(t => t.status === 'in_progress').length;
- const pending = tasks.filter(t => t.status === 'pending').length;
-
- container.innerHTML = `
-
-
-
-
-
-
-
- Created:
- ${formatDate(session.createdAt)}
-
-
- Tasks:
- ${tasks.length} tasks
-
-
-
-
-
-
- ๐
- Tasks
- ${tasks.length}
-
-
- ๐
- Plan
-
-
- ๐ฆ
- Context
-
-
- ๐
- Summary
-
-
-
-
-
- ${renderLiteTasksTab(session, tasks, completed, inProgress, pending)}
-
-
- `;
-
- // Initialize collapsible sections
- setTimeout(() => {
- document.querySelectorAll('.collapsible-header').forEach(header => {
- header.addEventListener('click', () => toggleSection(header));
- });
- }, 50);
-}
-
-function goBackToLiteTasks() {
- currentView = 'liteTasks';
- currentSessionDetailKey = null;
- updateContentTitle();
- renderLiteTasks();
-}
-
-function switchLiteDetailTab(tabName) {
- // Update active tab
- document.querySelectorAll('.detail-tab').forEach(tab => {
- tab.classList.toggle('active', tab.dataset.tab === tabName);
- });
-
- const session = liteTaskDataStore[currentSessionDetailKey];
- if (!session) return;
-
- const contentArea = document.getElementById('liteDetailTabContent');
- const tasks = session.tasks || [];
- const completed = tasks.filter(t => t.status === 'completed').length;
- const inProgress = tasks.filter(t => t.status === 'in_progress').length;
- const pending = tasks.filter(t => t.status === 'pending').length;
-
- switch (tabName) {
- case 'tasks':
- contentArea.innerHTML = renderLiteTasksTab(session, tasks, completed, inProgress, pending);
- // Re-initialize collapsible sections
- setTimeout(() => {
- document.querySelectorAll('.collapsible-header').forEach(header => {
- header.addEventListener('click', () => toggleSection(header));
- });
- }, 50);
- break;
- case 'plan':
- contentArea.innerHTML = renderLitePlanTab(session);
- break;
- case 'context':
- loadAndRenderLiteContextTab(session, contentArea);
- break;
- case 'summary':
- loadAndRenderLiteSummaryTab(session, contentArea);
- break;
- }
-}
-
-function renderLiteTasksTab(session, tasks, completed, inProgress, pending) {
- // Populate drawer tasks for click-to-open functionality
- currentDrawerTasks = tasks;
-
- if (tasks.length === 0) {
- return `
-
-
๐
-
No Tasks
-
This session has no tasks defined.
-
- `;
- }
-
- return `
-
-
- ${tasks.map(task => renderLiteTaskDetailItem(session.id, task)).join('')}
-
-
- `;
-}
-
-function renderLiteTaskDetailItem(sessionId, task) {
- const rawTask = task._raw || task;
- const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
- taskJsonStore[taskJsonId] = rawTask;
-
- // Get preview info for lite tasks
- const action = rawTask.action || '';
- const scope = rawTask.scope || '';
- const modCount = rawTask.modification_points?.length || 0;
- const implCount = rawTask.implementation?.length || 0;
- const acceptCount = rawTask.acceptance?.length || 0;
-
- return `
-
-
-
- ${action ? `${escapeHtml(action)} ` : ''}
- ${scope ? `${escapeHtml(scope)} ` : ''}
- ${modCount > 0 ? `${modCount} mods ` : ''}
- ${implCount > 0 ? `${implCount} steps ` : ''}
- ${acceptCount > 0 ? `${acceptCount} acceptance ` : ''}
-
-
- `;
-}
-
-function getMetaPreviewForLite(task, rawTask) {
- const meta = task.meta || {};
- const parts = [];
- if (meta.type || rawTask.action) parts.push(meta.type || rawTask.action);
- if (meta.scope || rawTask.scope) parts.push(meta.scope || rawTask.scope);
- return parts.join(' | ') || 'No meta';
-}
-
-function openTaskDrawerForLite(sessionId, taskId) {
- const session = liteTaskDataStore[currentSessionDetailKey];
- if (!session) return;
-
- const task = session.tasks?.find(t => t.id === taskId);
- if (!task) return;
-
- // Set current drawer tasks and session context
- currentDrawerTasks = session.tasks || [];
- window._currentDrawerSession = session;
-
- document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
- // Use dedicated lite task drawer renderer
- document.getElementById('drawerContent').innerHTML = renderLiteTaskDrawerContent(task, session);
- document.getElementById('taskDetailDrawer').classList.add('open');
- document.getElementById('drawerOverlay').classList.add('active');
-}
-
-function renderLitePlanTab(session) {
- const plan = session.plan;
-
- if (!plan) {
- return `
-
-
๐
-
No Plan Data
-
No plan.json found for this session.
-
- `;
- }
-
- return `
-
-
- ${plan.summary ? `
-
-
๐ Summary
-
${escapeHtml(plan.summary)}
-
- ` : ''}
-
-
- ${plan.approach ? `
-
-
๐ฏ Approach
-
${escapeHtml(plan.approach)}
-
- ` : ''}
-
-
- ${plan.focus_paths?.length ? `
-
-
๐ Focus Paths
-
- ${plan.focus_paths.map(p => `${escapeHtml(p)} `).join('')}
-
-
- ` : ''}
-
-
-
-
-
-
-
{ } Raw JSON
-
${escapeHtml(JSON.stringify(plan, null, 2))}
-
-
- `;
-}
-
-async function loadAndRenderLiteContextTab(session, contentArea) {
- contentArea.innerHTML = 'Loading context data...
';
-
- try {
- if (window.SERVER_MODE && session.path) {
- const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`);
- if (response.ok) {
- const data = await response.json();
- contentArea.innerHTML = renderLiteContextContent(data.context, session);
- return;
- }
- }
- // Fallback: show plan context if available
- contentArea.innerHTML = renderLiteContextContent(null, session);
- } catch (err) {
- contentArea.innerHTML = `Failed to load context: ${err.message}
`;
- }
-}
-
-
-// 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(`
-
- `);
-
- // 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(`
-
-
-
- ${renderExplorationAngle(angle, expData)}
-
-
- `);
- }
-
- return `${sections.join('')}
`;
-}
-
-function renderExplorationAngle(angle, data) {
- let content = [];
-
- // Project structure (architecture)
- if (data.project_structure) {
- content.push(`
-
-
Project Structure
-
${escapeHtml(data.project_structure)}
-
- `);
- }
-
- // Relevant files
- if (data.relevant_files && data.relevant_files.length) {
- content.push(`
-
-
Relevant Files (${data.relevant_files.length})
-
- ${data.relevant_files.slice(0, 10).map(f => `
-
-
${escapeHtml(f.path || '')}
-
Relevance: ${(f.relevance * 100).toFixed(0)}%
- ${f.rationale ? `
${escapeHtml(f.rationale.substring(0, 200))}...
` : ''}
-
- `).join('')}
- ${data.relevant_files.length > 10 ? `
... and ${data.relevant_files.length - 10} more files
` : ''}
-
-
- `);
- }
-
- // Patterns
- if (data.patterns) {
- content.push(`
-
-
Patterns
-
${escapeHtml(data.patterns)}
-
- `);
- }
-
- // Dependencies
- if (data.dependencies) {
- content.push(`
-
-
Dependencies
-
${escapeHtml(data.dependencies)}
-
- `);
- }
-
- // Integration points
- if (data.integration_points) {
- content.push(`
-
-
Integration Points
-
${escapeHtml(data.integration_points)}
-
- `);
- }
-
- // Constraints
- if (data.constraints) {
- content.push(`
-
-
Constraints
-
${escapeHtml(data.constraints)}
-
- `);
- }
-
- // Clarification needs
- if (data.clarification_needs && data.clarification_needs.length) {
- content.push(`
-
-
Clarification Needs
-
- ${data.clarification_needs.map(c => `
-
-
${escapeHtml(c.question)}
- ${c.options && c.options.length ? `
-
- ${c.options.map((opt, i) => `
- ${escapeHtml(opt)}
- `).join('')}
-
- ` : ''}
-
- `).join('')}
-
-
- `);
- }
-
- return content.join('') || 'No data available
';
-}
-
-
-function renderLiteContextContent(context, session) {
- const plan = session.plan || {};
-
- // If we have context from context-package.json
- if (context) {
- return `
-
-
${escapeHtml(JSON.stringify(context, null, 2))}
-
- `;
- }
-
- // Fallback: show context from plan
- if (plan.focus_paths?.length || plan.summary) {
- return `
-
- ${plan.summary ? `
-
-
Summary
-
${escapeHtml(plan.summary)}
-
- ` : ''}
- ${plan.focus_paths?.length ? `
-
-
Focus Paths
-
- ${plan.focus_paths.map(p => `${escapeHtml(p)} `).join('')}
-
-
- ` : ''}
-
- `;
- }
-
- return `
-
-
๐ฆ
-
No Context Data
-
No context-package.json found for this session.
-
- `;
-}
-
-async function loadAndRenderLiteSummaryTab(session, contentArea) {
- contentArea.innerHTML = 'Loading summaries...
';
-
- try {
- if (window.SERVER_MODE && session.path) {
- const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
- if (response.ok) {
- const data = await response.json();
- contentArea.innerHTML = renderSummaryContent(data.summaries);
- return;
- }
- }
- // Fallback
- contentArea.innerHTML = `
-
-
๐
-
No Summaries
-
No summaries found in .summaries/
-
- `;
- } catch (err) {
- contentArea.innerHTML = `Failed to load summaries: ${err.message}
`;
- }
-}
-
-// Store task JSON data in a global map instead of inline script tags
-const taskJsonStore = {};
-
-function renderTaskDetail(sessionId, task) {
- // Get raw task data for JSON view
- const rawTask = task._raw || task;
- const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
-
- // Store JSON in memory instead of inline script tag
- taskJsonStore[taskJsonId] = rawTask;
-
- return `
-
-
-
-
-
-
-
- ${renderDynamicFields(task.meta || rawTask, ['type', 'action', 'agent', 'scope', 'module', 'execution_group'])}
-
-
-
-
-
-
-
- ${renderContextFields(task.context, rawTask)}
-
-
-
-
-
-
-
-
- ${renderFlowControlDetails(task.flow_control, rawTask)}
-
-
-
- `;
-}
-
-function getContextPreview(context, rawTask) {
- const items = [];
- if (context?.requirements?.length) items.push(`${context.requirements.length} reqs`);
- if (context?.acceptance?.length) items.push(`${context.acceptance.length} acceptance`);
- if (context?.focus_paths?.length) items.push(`${context.focus_paths.length} paths`);
- if (rawTask?.modification_points?.length) items.push(`${rawTask.modification_points.length} mods`);
- return items.join(' | ') || 'No context';
-}
-
-function getFlowControlPreview(flowControl, rawTask) {
- const steps = flowControl?.implementation_approach?.length || rawTask?.implementation?.length || 0;
- return steps > 0 ? `${steps} steps` : 'No steps';
-}
-
-function renderDynamicFields(obj, priorityKeys = []) {
- if (!obj || typeof obj !== 'object') return 'null
';
-
- const entries = Object.entries(obj).filter(([k, v]) => v !== null && v !== undefined && k !== '_raw');
- if (entries.length === 0) return 'Empty
';
-
- // Sort: priority keys first, then alphabetically
- entries.sort(([a], [b]) => {
- const aIdx = priorityKeys.indexOf(a);
- const bIdx = priorityKeys.indexOf(b);
- if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
- if (aIdx !== -1) return -1;
- if (bIdx !== -1) return 1;
- return a.localeCompare(b);
- });
-
- return `${entries.map(([key, value]) => renderFieldRow(key, value)).join('')}
`;
-}
-
-function renderFieldRow(key, value) {
- return `
-
-
${escapeHtml(key)}:
-
${renderFieldValue(key, value)}
-
- `;
-}
-
-function renderFieldValue(key, value) {
- if (value === null || value === undefined) {
- return 'null ';
- }
-
- if (typeof value === 'boolean') {
- return `${value} `;
- }
-
- if (typeof value === 'number') {
- return `${value} `;
- }
-
- if (typeof value === 'string') {
- // Check if it's a path
- if (key.includes('path') || key.includes('file') || value.includes('/') || value.includes('\\')) {
- return `${escapeHtml(value)} `;
- }
- return `${escapeHtml(value)} `;
- }
-
- if (Array.isArray(value)) {
- if (value.length === 0) return '[] ';
-
- // Check if array contains objects or strings
- if (typeof value[0] === 'object') {
- return `${value.map((item, i) => `
-
-
- ${renderDynamicFields(item)}
-
- `).join('')}
`;
- }
-
- // Array of strings/primitives
- const isPathArray = key.includes('path') || key.includes('file');
- return `${value.map(v =>
- `${escapeHtml(String(v))} `
- ).join('')}
`;
- }
-
- if (typeof value === 'object') {
- return renderDynamicFields(value);
- }
-
- return escapeHtml(String(value));
-}
-
-function renderContextFields(context, rawTask) {
- const sections = [];
-
- // Requirements / Description
- const requirements = context?.requirements || [];
- const description = rawTask?.description;
- if (requirements.length > 0 || description) {
- sections.push(`
-
-
requirements:
- ${description ? `
${escapeHtml(description)}
` : ''}
- ${requirements.length > 0 ? `
${requirements.map(r => `${escapeHtml(r)} `).join('')} ` : ''}
-
- `);
- }
-
- // Focus paths / Modification points
- const focusPaths = context?.focus_paths || [];
- const modPoints = rawTask?.modification_points || [];
- if (focusPaths.length > 0 || modPoints.length > 0) {
- sections.push(`
-
-
${modPoints.length > 0 ? 'modification_points:' : 'focus_paths:'}
- ${modPoints.length > 0 ? `
-
- ${modPoints.map(m => `
-
-
${escapeHtml(m.file || m)}
- ${m.target ? `
โ ${escapeHtml(m.target)} ` : ''}
- ${m.change ? `
${escapeHtml(m.change)}
` : ''}
-
- `).join('')}
-
- ` : `
-
${focusPaths.map(p => `${escapeHtml(p)} `).join('')}
- `}
-
- `);
- }
-
- // Acceptance criteria
- const acceptance = context?.acceptance || rawTask?.acceptance || [];
- if (acceptance.length > 0) {
- sections.push(`
-
-
acceptance:
-
${acceptance.map(a => `${escapeHtml(a)} `).join('')}
-
- `);
- }
-
- // Dependencies
- const depends = context?.depends_on || rawTask?.depends_on || [];
- if (depends.length > 0) {
- sections.push(`
-
-
depends_on:
-
${depends.map(d => `${escapeHtml(d)} `).join('')}
-
- `);
- }
-
- // Reference
- const reference = rawTask?.reference;
- if (reference) {
- sections.push(`
-
- reference:
- ${renderDynamicFields(reference)}
-
- `);
- }
-
- return sections.length > 0
- ? `${sections.join('')}
`
- : 'No context data
';
-}
-
-function renderFlowControlDetails(flowControl, rawTask) {
- const sections = [];
-
- // Pre-analysis
- const preAnalysis = flowControl?.pre_analysis || rawTask?.pre_analysis || [];
- if (preAnalysis.length > 0) {
- sections.push(`
-
-
pre_analysis:
-
${preAnalysis.map(p => `${escapeHtml(p)} `).join('')}
-
- `);
- }
-
- // Target files
- const targetFiles = flowControl?.target_files || rawTask?.target_files || [];
- if (targetFiles.length > 0) {
- sections.push(`
-
-
target_files:
-
${targetFiles.map(f => `${escapeHtml(f)} `).join('')}
-
- `);
- }
-
- return sections.join('');
-}
-
-function escapeHtml(str) {
- if (typeof str !== 'string') return str;
- return str
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-}
-
-function showJsonModal(jsonId, taskId) {
- // Get JSON from memory store instead of DOM
- const rawTask = taskJsonStore[jsonId];
- if (!rawTask) return;
-
- const jsonContent = JSON.stringify(rawTask, null, 2);
-
- // Create modal
- const overlay = document.createElement('div');
- overlay.className = 'json-modal-overlay';
- overlay.innerHTML = `
-
-
-
-
${escapeHtml(jsonContent)}
-
-
-
- `;
-
- document.body.appendChild(overlay);
-
- // Trigger animation
- requestAnimationFrame(() => overlay.classList.add('active'));
-
- // Close on overlay click
- overlay.addEventListener('click', (e) => {
- if (e.target === overlay) closeJsonModal(overlay.querySelector('.json-modal-close'));
- });
-
- // Close on Escape key
- const escHandler = (e) => {
- if (e.key === 'Escape') {
- closeJsonModal(overlay.querySelector('.json-modal-close'));
- document.removeEventListener('keydown', escHandler);
- }
- };
- document.addEventListener('keydown', escHandler);
-}
-
-function closeJsonModal(btn) {
- const overlay = btn.closest('.json-modal-overlay');
- overlay.classList.remove('active');
- setTimeout(() => overlay.remove(), 200);
-}
-
-function copyJsonToClipboard(btn) {
- const content = btn.closest('.json-modal').querySelector('.json-modal-content').textContent;
- navigator.clipboard.writeText(content).then(() => {
- const original = btn.textContent;
- btn.textContent = 'Copied!';
- setTimeout(() => btn.textContent = original, 2000);
- });
-}
-
-function toggleSection(header) {
- const content = header.nextElementSibling;
- const icon = header.querySelector('.collapse-icon');
- const isCollapsed = content.classList.contains('collapsed');
-
- content.classList.toggle('collapsed');
- header.classList.toggle('expanded');
- icon.textContent = isCollapsed ? 'โผ' : 'โถ';
-
- // Render flowchart if expanding flow_control section
- if (isCollapsed && header.querySelector('.section-label')?.textContent === 'flow_control') {
- const container = content.querySelector('.flowchart-container');
- if (container && !container.dataset.rendered) {
- const taskId = container.id.replace('flowchart-', '');
- const parts = taskId.split('-');
- const implId = parts.pop();
- const sessionId = parts.join('-');
-
- // Try to find task from multiple sources
- let task = null;
-
- // 1. Try liteTaskDataStore (for lite task detail page)
- if (currentSessionDetailKey && liteTaskDataStore[currentSessionDetailKey]) {
- const session = liteTaskDataStore[currentSessionDetailKey];
- task = session?.tasks?.find(t => t.id === implId || t.id === 'IMPL-' + implId || t.id === 'T' + implId);
- }
-
- // 2. Try workflowData.liteTasks (for lite tasks list view)
- if (!task) {
- const session = [...(workflowData.liteTasks?.litePlan || []), ...(workflowData.liteTasks?.liteFix || [])]
- .find(s => s.id === sessionId);
- task = session?.tasks?.find(t => t.id === implId || t.id === 'IMPL-' + implId || t.id === 'T' + implId);
- }
-
- // 3. Try sessionDataStore (for regular session detail page)
- if (!task && currentSessionDetailKey && sessionDataStore[currentSessionDetailKey]) {
- const session = sessionDataStore[currentSessionDetailKey];
- task = session?.tasks?.find(t => (t.task_id || t.id) === implId || (t.task_id || t.id) === 'IMPL-' + implId);
- }
-
- // Render flowchart if task found
- if (task) {
- const flowSteps = task.flow_control?.implementation_approach ||
- task._raw?.flow_control?.implementation_approach ||
- task._raw?.implementation ||
- task.implementation;
- if (flowSteps && flowSteps.length > 0) {
- renderFlowchart(container.id, flowSteps);
- container.dataset.rendered = 'true';
- }
- }
- }
- }
-}
-
-function renderFlowchartForTask(sessionId, task) {
- // Will render on section expand
-}
-
-function renderFlowchart(containerId, steps) {
- if (!steps || steps.length === 0) return;
- if (typeof d3 === 'undefined') {
- document.getElementById(containerId).innerHTML = 'D3.js not loaded
';
- return;
- }
-
- const container = document.getElementById(containerId);
- const width = container.clientWidth || 500;
- const nodeHeight = 50;
- const nodeWidth = Math.min(width - 40, 300);
- const padding = 15;
- const height = steps.length * (nodeHeight + padding) + padding * 2;
-
- // Clear existing content
- container.innerHTML = '';
-
- const svg = d3.select('#' + containerId)
- .append('svg')
- .attr('width', width)
- .attr('height', height)
- .attr('class', 'flowchart-svg');
-
- // Arrow marker
- svg.append('defs').append('marker')
- .attr('id', 'arrow-' + containerId)
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', 8)
- .attr('refY', 0)
- .attr('markerWidth', 6)
- .attr('markerHeight', 6)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M0,-5L10,0L0,5')
- .attr('fill', 'hsl(var(--border))');
-
- // Draw arrows
- for (let i = 0; i < steps.length - 1; i++) {
- const y1 = padding + i * (nodeHeight + padding) + nodeHeight;
- const y2 = padding + (i + 1) * (nodeHeight + padding);
-
- svg.append('line')
- .attr('x1', width / 2)
- .attr('y1', y1)
- .attr('x2', width / 2)
- .attr('y2', y2)
- .attr('stroke', 'hsl(var(--border))')
- .attr('stroke-width', 2)
- .attr('marker-end', 'url(#arrow-' + containerId + ')');
- }
-
- // Draw nodes
- const nodes = svg.selectAll('.node')
- .data(steps)
- .enter()
- .append('g')
- .attr('class', 'flowchart-node')
- .attr('transform', (d, i) => `translate(${(width - nodeWidth) / 2}, ${padding + i * (nodeHeight + padding)})`);
-
- // Node rectangles
- nodes.append('rect')
- .attr('width', nodeWidth)
- .attr('height', nodeHeight)
- .attr('rx', 6)
- .attr('fill', (d, i) => i === 0 ? 'hsl(var(--primary))' : 'hsl(var(--card))')
- .attr('stroke', 'hsl(var(--border))')
- .attr('stroke-width', 1);
-
- // Step number circle
- nodes.append('circle')
- .attr('cx', 20)
- .attr('cy', nodeHeight / 2)
- .attr('r', 12)
- .attr('fill', (d, i) => i === 0 ? 'rgba(255,255,255,0.2)' : 'hsl(var(--muted))');
-
- nodes.append('text')
- .attr('x', 20)
- .attr('y', nodeHeight / 2)
- .attr('text-anchor', 'middle')
- .attr('dominant-baseline', 'central')
- .attr('font-size', '11px')
- .attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--muted-foreground))')
- .text((d, i) => i + 1);
-
- // Node text (step name)
- nodes.append('text')
- .attr('x', 45)
- .attr('y', nodeHeight / 2)
- .attr('dominant-baseline', 'central')
- .attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--foreground))')
- .attr('font-size', '12px')
- .text(d => {
- const text = d.step || d.action || 'Step';
- return text.length > 35 ? text.substring(0, 32) + '...' : text;
- });
-}
-
-function formatDate(dateStr) {
- if (!dateStr) return '-';
- try {
- const date = new Date(dateStr);
- // Check if date is valid
- if (isNaN(date.getTime())) return '-';
- // Format: YYYY/MM/DD HH:mm
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- return `${year}/${month}/${day} ${hours}:${minutes}`;
- } catch {
- return '-';
- }
-}
-
-// ==========================================
-// TASK DETAIL DRAWER
-// ==========================================
-let currentDrawerTasks = [];
-
-function openTaskDrawer(taskId) {
- const task = currentDrawerTasks.find(t => (t.task_id || t.id) === taskId);
- if (!task) {
- console.error('Task not found:', taskId);
- return;
- }
-
- document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
- document.getElementById('drawerContent').innerHTML = renderTaskDrawerContent(task);
- document.getElementById('taskDetailDrawer').classList.add('open');
- document.getElementById('drawerOverlay').classList.add('active');
-
- // Initialize flowchart after DOM is updated
- setTimeout(() => {
- renderFullFlowchart(task.flow_control);
- }, 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(`
-
-
Description
-
${escapeHtml(task.description)}
-
- `);
- }
-
- // Modification Points
- if (task.modification_points?.length) {
- sections.push(`
-
-
Modification Points
-
- ${task.modification_points.map(mp => `
-
-
- ๐
- ${escapeHtml(mp.file || mp.path || '')}
-
- ${mp.target ? `
Target: ${escapeHtml(mp.target)}
` : ''}
- ${mp.change ? `
${escapeHtml(mp.change)}
` : ''}
-
- `).join('')}
-
-
- `);
- }
-
- // Implementation Steps
- if (task.implementation?.length) {
- sections.push(`
-
-
Implementation Steps
-
- ${task.implementation.map(step => `
- ${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}
- `).join('')}
-
-
- `);
- }
-
- // Reference
- if (task.reference) {
- sections.push(`
-
-
Reference
- ${task.reference.pattern ? `
Pattern: ${escapeHtml(task.reference.pattern)}
` : ''}
- ${task.reference.files?.length ? `
-
-
Files:
-
- ${task.reference.files.map(f => `${escapeHtml(f)} `).join('')}
-
-
- ` : ''}
- ${task.reference.examples ? `
Examples: ${escapeHtml(task.reference.examples)}
` : ''}
-
- `);
- }
-
- // Acceptance Criteria
- if (task.acceptance?.length) {
- sections.push(`
-
-
Acceptance Criteria
-
- ${task.acceptance.map(a => `${escapeHtml(a)} `).join('')}
-
-
- `);
- }
-
- // Dependencies
- if (task.depends_on?.length) {
- sections.push(`
-
-
Dependencies
-
- ${task.depends_on.map(dep => `${escapeHtml(dep)} `).join(' ')}
-
-
- `);
- }
-
- return sections.join('');
-}
-
-
-
-// Dedicated lite task drawer content renderer
-function renderLiteTaskDrawerContent(task, session) {
- const rawTask = task._raw || task;
-
- return `
-
-
-
-
-
- Overview
- Implementation
- Files
- Raw JSON
-
-
-
-
-
-
- ${renderLiteTaskOverview(rawTask)}
-
-
-
-
- ${renderLiteTaskImplementation(rawTask)}
-
-
-
-
- ${renderLiteTaskFiles(rawTask)}
-
-
-
-
-
${escapeHtml(JSON.stringify(rawTask, null, 2))}
-
-
- `;
-}
-
-// Render lite task overview
-function renderLiteTaskOverview(task) {
- let sections = [];
-
- // Description
- if (task.description) {
- sections.push(`
-
-
Description
-
${escapeHtml(task.description)}
-
- `);
- }
-
- // Scope
- if (task.scope) {
- sections.push(`
-
-
Scope
- ${escapeHtml(task.scope)}
-
- `);
- }
-
- // Acceptance Criteria
- if (task.acceptance && task.acceptance.length > 0) {
- sections.push(`
-
-
Acceptance Criteria
-
- ${task.acceptance.map(a => `${escapeHtml(a)} `).join('')}
-
-
- `);
- }
-
- // Dependencies
- if (task.depends_on && task.depends_on.length > 0) {
- sections.push(`
-
-
Dependencies
-
- ${task.depends_on.map(dep => `${escapeHtml(dep)} `).join(' ')}
-
-
- `);
- }
-
- // Reference
- if (task.reference) {
- sections.push(`
-
-
Reference
- ${task.reference.pattern ? `
Pattern: ${escapeHtml(task.reference.pattern)}
` : ''}
- ${task.reference.files && task.reference.files.length > 0 ? `
-
-
Files:
-
- ${task.reference.files.map(f => `${escapeHtml(f)} `).join('')}
-
-
- ` : ''}
- ${task.reference.examples ? `
Examples: ${escapeHtml(task.reference.examples)}
` : ''}
-
- `);
- }
-
- return sections.length > 0 ? sections.join('') : 'No overview data
';
-}
-
-// Render lite task implementation steps
-function renderLiteTaskImplementation(task) {
- let sections = [];
-
- // Implementation Steps
- if (task.implementation && task.implementation.length > 0) {
- sections.push(`
-
-
Implementation Steps
-
- ${task.implementation.map((step, idx) => `
-
- ${idx + 1}
- ${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}
-
- `).join('')}
-
-
- `);
- }
-
- // Modification Points
- if (task.modification_points && task.modification_points.length > 0) {
- sections.push(`
-
-
Modification Points
-
- ${task.modification_points.map(mp => `
-
-
${escapeHtml(mp.file || '')}
- ${mp.target ? `
Target: ${escapeHtml(mp.target)}
` : ''}
- ${mp.change ? `
${escapeHtml(mp.change)}
` : ''}
-
- `).join('')}
-
-
- `);
- }
-
- return sections.length > 0 ? sections.join('') : 'No implementation data
';
-}
-
-// Render lite task files
-function renderLiteTaskFiles(task) {
- const files = [];
-
- // Collect from modification_points
- if (task.modification_points) {
- task.modification_points.forEach(mp => {
- if (mp.file && !files.includes(mp.file)) files.push(mp.file);
- });
- }
-
- // Collect from scope
- if (task.scope && !files.includes(task.scope)) {
- files.push(task.scope);
- }
-
- if (files.length === 0) {
- return 'No files specified
';
- }
-
- return `
-
-
Target Files
-
- ${files.map(f => `
-
- ๐
- ${escapeHtml(f)}
-
- `).join('')}
-
-
- `;
-}
-
-
-function closeTaskDrawer() {
- document.getElementById('taskDetailDrawer').classList.remove('open');
- document.getElementById('drawerOverlay').classList.remove('active');
-}
-
-function switchDrawerTab(tabName) {
- // Update tab buttons
- document.querySelectorAll('.drawer-tab').forEach(tab => {
- tab.classList.toggle('active', tab.dataset.tab === tabName);
- });
-
- // Update tab panels
- document.querySelectorAll('.drawer-panel').forEach(panel => {
- panel.classList.toggle('active', panel.dataset.tab === tabName);
- });
-
- // Render flowchart if switching to flowchart tab
- if (tabName === 'flowchart') {
- const taskId = document.getElementById('drawerTaskTitle').textContent;
- const task = currentDrawerTasks.find(t => t.title === taskId || t.task_id === taskId);
- if (task?.flow_control) {
- setTimeout(() => renderFullFlowchart(task.flow_control), 50);
- }
- }
-}
-
-function renderTaskDrawerContent(task) {
- const fc = task.flow_control || {};
-
- return `
-
-
-
-
-
- Overview
- Flowchart
- Files
- Raw JSON
-
-
-
-
-
-
- ${renderPreAnalysisSteps(fc.pre_analysis)}
- ${renderImplementationStepsList(fc.implementation_approach)}
-
-
-
-
-
-
-
- ${renderTargetFiles(fc.target_files)}
- ${fc.test_commands ? renderTestCommands(fc.test_commands) : ''}
-
-
-
-
-
${escapeHtml(JSON.stringify(task, null, 2))}
-
-
- `;
-}
-
-function renderPreAnalysisSteps(preAnalysis) {
- if (!Array.isArray(preAnalysis) || preAnalysis.length === 0) {
- return 'No pre-analysis steps
';
- }
-
- return `
-
-
Pre-Analysis Steps
-
- ${preAnalysis.map((item, idx) => `
-
-
${idx + 1}
-
-
${escapeHtml(item.step || item.action || 'Step ' + (idx + 1))}
- ${item.action && item.action !== item.step ? `
${escapeHtml(item.action)}
` : ''}
- ${item.commands?.length ? `
-
- ${item.commands.map(c => `${escapeHtml(typeof c === 'string' ? c : JSON.stringify(c))}`).join('')}
-
- ` : ''}
- ${item.output_to ? `
Output: ${escapeHtml(item.output_to)}
` : ''}
-
-
- `).join('')}
-
-
- `;
-}
-
-function renderImplementationStepsList(steps) {
- if (!Array.isArray(steps) || steps.length === 0) {
- return 'No implementation steps
';
- }
-
- return `
-
-
Implementation Approach
-
- ${steps.map((step, idx) => {
- const hasMods = step.modification_points?.length;
- const hasFlow = step.logic_flow?.length;
- const hasColumns = hasMods || hasFlow;
-
- return `
-
-
- ${step.description ? `
${escapeHtml(step.description)}
` : ''}
- ${hasColumns ? `
-
- ${hasMods ? `
-
-
Modifications
-
- ${step.modification_points.map(mp => `
-
- ${typeof mp === 'string' ? escapeHtml(mp) : `
- ${escapeHtml(mp.file || mp.path || '')}
- ${mp.changes ? ` - ${escapeHtml(mp.changes)}` : ''}
- `}
-
- `).join('')}
-
-
- ` : '
'}
- ${hasFlow ? `
-
-
Logic Flow
-
- ${step.logic_flow.map(lf => `${escapeHtml(typeof lf === 'string' ? lf : lf.action || JSON.stringify(lf))} `).join('')}
-
-
- ` : '
'}
-
- ` : ''}
- ${step.depends_on?.length ? `
-
- Depends on: ${step.depends_on.map(d => `${escapeHtml(d)} `).join(' ')}
-
- ` : ''}
-
- `}).join('')}
-
-
- `;
-}
-
-function renderTargetFiles(files) {
- if (!Array.isArray(files) || files.length === 0) {
- return 'No target files
';
- }
-
- // Get current project path for building full paths
- const projectPath = window.currentProjectPath || '';
-
- return `
-
-
Target Files
-
- ${files.map(f => {
- const filePath = typeof f === 'string' ? f : (f.path || JSON.stringify(f));
- // Build full path for vscode link
- const fullPath = filePath.startsWith('/') || filePath.includes(':')
- ? filePath
- : (projectPath ? `${projectPath}/${filePath}` : filePath);
- const vscodeUri = `vscode://file/${fullPath.replace(/\\/g, '/')}`;
-
- return `
-
-
- ๐
- ${escapeHtml(filePath)}
- โ
-
-
- `;
- }).join('')}
-
-
- `;
-}
-
-function renderTestCommands(testCommands) {
- if (!testCommands || typeof testCommands !== 'object') return '';
-
- return `
-
-
Test Commands
-
- ${Object.entries(testCommands).map(([key, val]) => `
-
- ${escapeHtml(key)}:
- ${escapeHtml(typeof val === 'string' ? val : JSON.stringify(val))}
-
- `).join('')}
-
-
- `;
-}
-
-function truncateText(text, maxLen) {
- if (!text) return '';
- return text.length > maxLen ? text.substring(0, maxLen - 3) + '...' : text;
-}
-
-// D3.js Full Flowchart combining pre_analysis and implementation_approach
-function renderFullFlowchart(flowControl) {
- if (!flowControl) return;
-
- const container = document.getElementById('flowchartContainer');
- if (!container) return;
-
- const preAnalysis = Array.isArray(flowControl.pre_analysis) ? flowControl.pre_analysis : [];
- const implSteps = Array.isArray(flowControl.implementation_approach) ? flowControl.implementation_approach : [];
-
- if (preAnalysis.length === 0 && implSteps.length === 0) {
- container.innerHTML = 'No flowchart data available
';
- return;
- }
-
- const width = container.clientWidth || 500;
- const nodeHeight = 90;
- const nodeWidth = Math.min(width - 40, 420);
- const nodeGap = 45;
- const sectionGap = 30;
-
- // Calculate total nodes and height
- const totalPreNodes = preAnalysis.length;
- const totalImplNodes = implSteps.length;
- const hasBothSections = totalPreNodes > 0 && totalImplNodes > 0;
- const height = (totalPreNodes + totalImplNodes) * (nodeHeight + nodeGap) +
- (hasBothSections ? sectionGap + 60 : 0) + 60;
-
- // Clear existing
- d3.select('#flowchartContainer').selectAll('*').remove();
-
- const svg = d3.select('#flowchartContainer')
- .append('svg')
- .attr('width', '100%')
- .attr('height', height)
- .attr('viewBox', `0 0 ${width} ${height}`);
-
- // Add arrow markers
- const defs = svg.append('defs');
-
- defs.append('marker')
- .attr('id', 'arrowhead-pre')
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', 8)
- .attr('refY', 0)
- .attr('markerWidth', 6)
- .attr('markerHeight', 6)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M0,-5L10,0L0,5')
- .attr('fill', '#f59e0b');
-
- defs.append('marker')
- .attr('id', 'arrowhead-impl')
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', 8)
- .attr('refY', 0)
- .attr('markerWidth', 6)
- .attr('markerHeight', 6)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M0,-5L10,0L0,5')
- .attr('fill', 'hsl(var(--primary))');
-
- let currentY = 20;
-
- // Render Pre-Analysis section
- if (totalPreNodes > 0) {
- // Section label
- svg.append('text')
- .attr('x', 20)
- .attr('y', currentY)
- .attr('fill', '#f59e0b')
- .attr('font-weight', 'bold')
- .attr('font-size', '13px')
- .text('๐ Pre-Analysis Steps');
-
- currentY += 25;
-
- preAnalysis.forEach((step, idx) => {
- const x = (width - nodeWidth) / 2;
-
- // Connection line to next node
- if (idx < preAnalysis.length - 1) {
- svg.append('line')
- .attr('x1', width / 2)
- .attr('y1', currentY + nodeHeight)
- .attr('x2', width / 2)
- .attr('y2', currentY + nodeHeight + nodeGap - 10)
- .attr('stroke', '#f59e0b')
- .attr('stroke-width', 2)
- .attr('marker-end', 'url(#arrowhead-pre)');
- }
-
- // Node group
- const nodeG = svg.append('g')
- .attr('class', 'flowchart-node')
- .attr('transform', `translate(${x}, ${currentY})`);
-
- // Node rectangle (pre-analysis style - amber/orange)
- nodeG.append('rect')
- .attr('width', nodeWidth)
- .attr('height', nodeHeight)
- .attr('rx', 10)
- .attr('fill', 'hsl(var(--card))')
- .attr('stroke', '#f59e0b')
- .attr('stroke-width', 2)
- .attr('stroke-dasharray', '5,3');
-
- // Step badge
- nodeG.append('circle')
- .attr('cx', 25)
- .attr('cy', 25)
- .attr('r', 15)
- .attr('fill', '#f59e0b');
-
- nodeG.append('text')
- .attr('x', 25)
- .attr('y', 30)
- .attr('text-anchor', 'middle')
- .attr('fill', 'white')
- .attr('font-weight', 'bold')
- .attr('font-size', '11px')
- .text('P' + (idx + 1));
-
- // Step name
- const stepName = step.step || step.action || 'Pre-step ' + (idx + 1);
- nodeG.append('text')
- .attr('x', 50)
- .attr('y', 28)
- .attr('fill', 'hsl(var(--foreground))')
- .attr('font-weight', '600')
- .attr('font-size', '13px')
- .text(truncateText(stepName, 40));
-
- // Action description
- if (step.action && step.action !== stepName) {
- nodeG.append('text')
- .attr('x', 15)
- .attr('y', 52)
- .attr('fill', 'hsl(var(--muted-foreground))')
- .attr('font-size', '11px')
- .text(truncateText(step.action, 50));
- }
-
- // Output indicator
- if (step.output_to) {
- nodeG.append('text')
- .attr('x', 15)
- .attr('y', 75)
- .attr('fill', '#f59e0b')
- .attr('font-size', '10px')
- .text('โ ' + truncateText(step.output_to, 45));
- }
-
- currentY += nodeHeight + nodeGap;
- });
- }
-
- // Section divider if both sections exist
- if (hasBothSections) {
- currentY += 10;
- svg.append('line')
- .attr('x1', 40)
- .attr('y1', currentY)
- .attr('x2', width - 40)
- .attr('y2', currentY)
- .attr('stroke', 'hsl(var(--border))')
- .attr('stroke-width', 1)
- .attr('stroke-dasharray', '4,4');
-
- // Connecting arrow from pre-analysis to implementation
- svg.append('line')
- .attr('x1', width / 2)
- .attr('y1', currentY - nodeGap + 5)
- .attr('x2', width / 2)
- .attr('y2', currentY + sectionGap - 5)
- .attr('stroke', 'hsl(var(--primary))')
- .attr('stroke-width', 2)
- .attr('marker-end', 'url(#arrowhead-impl)');
-
- currentY += sectionGap;
- }
-
- // Render Implementation section
- if (totalImplNodes > 0) {
- // Section label
- svg.append('text')
- .attr('x', 20)
- .attr('y', currentY)
- .attr('fill', 'hsl(var(--primary))')
- .attr('font-weight', 'bold')
- .attr('font-size', '13px')
- .text('๐ง Implementation Steps');
-
- currentY += 25;
-
- implSteps.forEach((step, idx) => {
- const x = (width - nodeWidth) / 2;
-
- // Connection line to next node
- if (idx < implSteps.length - 1) {
- svg.append('line')
- .attr('x1', width / 2)
- .attr('y1', currentY + nodeHeight)
- .attr('x2', width / 2)
- .attr('y2', currentY + nodeHeight + nodeGap - 10)
- .attr('stroke', 'hsl(var(--primary))')
- .attr('stroke-width', 2)
- .attr('marker-end', 'url(#arrowhead-impl)');
- }
-
- // Node group
- const nodeG = svg.append('g')
- .attr('class', 'flowchart-node')
- .attr('transform', `translate(${x}, ${currentY})`);
-
- // Node rectangle (implementation style - blue)
- nodeG.append('rect')
- .attr('width', nodeWidth)
- .attr('height', nodeHeight)
- .attr('rx', 10)
- .attr('fill', 'hsl(var(--card))')
- .attr('stroke', 'hsl(var(--primary))')
- .attr('stroke-width', 2);
-
- // Step badge
- nodeG.append('circle')
- .attr('cx', 25)
- .attr('cy', 25)
- .attr('r', 15)
- .attr('fill', 'hsl(var(--primary))');
-
- nodeG.append('text')
- .attr('x', 25)
- .attr('y', 30)
- .attr('text-anchor', 'middle')
- .attr('fill', 'white')
- .attr('font-weight', 'bold')
- .attr('font-size', '12px')
- .text(step.step || idx + 1);
-
- // Step title
- nodeG.append('text')
- .attr('x', 50)
- .attr('y', 28)
- .attr('fill', 'hsl(var(--foreground))')
- .attr('font-weight', '600')
- .attr('font-size', '13px')
- .text(truncateText(step.title || 'Step ' + (idx + 1), 40));
-
- // Description
- if (step.description) {
- nodeG.append('text')
- .attr('x', 15)
- .attr('y', 52)
- .attr('fill', 'hsl(var(--muted-foreground))')
- .attr('font-size', '11px')
- .text(truncateText(step.description, 50));
- }
-
- // Output/depends indicator
- if (step.depends_on?.length) {
- nodeG.append('text')
- .attr('x', 15)
- .attr('y', 75)
- .attr('fill', 'var(--warning-color)')
- .attr('font-size', '10px')
- .text('โ Depends: ' + step.depends_on.join(', '));
- }
-
- currentY += nodeHeight + nodeGap;
- });
- }
-}
-
-// D3.js Vertical Flowchart for Implementation Approach (legacy)
-function renderImplementationFlowchart(steps) {
- if (!Array.isArray(steps) || steps.length === 0) return;
-
- const container = document.getElementById('flowchartContainer');
- if (!container) return;
-
- const width = container.clientWidth || 500;
- const nodeHeight = 100;
- const nodeWidth = Math.min(width - 40, 400);
- const nodeGap = 50;
- const height = steps.length * (nodeHeight + nodeGap) + 40;
-
- // Clear existing
- d3.select('#flowchartContainer').selectAll('*').remove();
-
- const svg = d3.select('#flowchartContainer')
- .append('svg')
- .attr('width', '100%')
- .attr('height', height)
- .attr('viewBox', `0 0 ${width} ${height}`);
-
- // Add arrow marker
- svg.append('defs').append('marker')
- .attr('id', 'arrowhead')
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', 8)
- .attr('refY', 0)
- .attr('markerWidth', 6)
- .attr('markerHeight', 6)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M0,-5L10,0L0,5')
- .attr('fill', 'hsl(var(--primary))');
-
- // Draw nodes and connections
- steps.forEach((step, idx) => {
- const y = idx * (nodeHeight + nodeGap) + 20;
- const x = (width - nodeWidth) / 2;
-
- // Connection line to next node
- if (idx < steps.length - 1) {
- svg.append('line')
- .attr('x1', width / 2)
- .attr('y1', y + nodeHeight)
- .attr('x2', width / 2)
- .attr('y2', y + nodeHeight + nodeGap - 10)
- .attr('stroke', 'hsl(var(--primary))')
- .attr('stroke-width', 2)
- .attr('marker-end', 'url(#arrowhead)');
- }
-
- // Node group
- const nodeG = svg.append('g')
- .attr('class', 'flowchart-node')
- .attr('transform', `translate(${x}, ${y})`);
-
- // Node rectangle with gradient
- nodeG.append('rect')
- .attr('width', nodeWidth)
- .attr('height', nodeHeight)
- .attr('rx', 10)
- .attr('fill', 'hsl(var(--card))')
- .attr('stroke', 'hsl(var(--primary))')
- .attr('stroke-width', 2)
- .attr('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))');
-
- // Step number badge
- nodeG.append('circle')
- .attr('cx', 25)
- .attr('cy', 25)
- .attr('r', 15)
- .attr('fill', 'hsl(var(--primary))');
-
- nodeG.append('text')
- .attr('x', 25)
- .attr('y', 30)
- .attr('text-anchor', 'middle')
- .attr('fill', 'white')
- .attr('font-weight', 'bold')
- .attr('font-size', '12px')
- .text(step.step || idx + 1);
-
- // Step title
- nodeG.append('text')
- .attr('x', 50)
- .attr('y', 30)
- .attr('fill', 'hsl(var(--foreground))')
- .attr('font-weight', '600')
- .attr('font-size', '14px')
- .text(truncateText(step.title || 'Step ' + (idx + 1), 35));
-
- // Step description (if available)
- if (step.description) {
- nodeG.append('text')
- .attr('x', 15)
- .attr('y', 55)
- .attr('fill', 'hsl(var(--muted-foreground))')
- .attr('font-size', '12px')
- .text(truncateText(step.description, 45));
- }
-
- // Output indicator
- if (step.output) {
- nodeG.append('text')
- .attr('x', 15)
- .attr('y', 80)
- .attr('fill', 'var(--success-color)')
- .attr('font-size', '11px')
- .text('โ ' + truncateText(step.output, 40));
- }
- });
-}
-
-// ========== Project Overview Rendering ==========
-
-function renderProjectOverview() {
- const container = document.getElementById('mainContent');
- const project = workflowData.projectOverview;
-
- if (!project) {
- container.innerHTML = `
-
-
๐
-
No Project Overview
-
- Run /workflow:init to initialize project analysis
-
-
- `;
- return;
- }
-
- container.innerHTML = `
-
-
-
-
-
${escapeHtml(project.projectName)}
-
${escapeHtml(project.description || 'No description available')}
-
-
-
Initialized: ${formatDate(project.initializedAt)}
-
Mode: ${escapeHtml(project.metadata?.analysis_mode || 'unknown')}
-
-
-
-
-
-
-
- ๐ป Technology Stack
-
-
-
-
-
Languages
-
- ${project.technologyStack.languages.map(lang => `
-
- ${escapeHtml(lang.name)}
- ${lang.file_count} files
- ${lang.primary ? 'Primary ' : ''}
-
- `).join('') || '
No languages detected '}
-
-
-
-
-
-
Frameworks
-
- ${project.technologyStack.frameworks.map(fw => `
- ${escapeHtml(fw)}
- `).join('') || 'No frameworks detected '}
-
-
-
-
-
-
Build Tools
-
- ${project.technologyStack.build_tools.map(tool => `
- ${escapeHtml(tool)}
- `).join('') || 'No build tools detected '}
-
-
-
-
-
-
Test Frameworks
-
- ${project.technologyStack.test_frameworks.map(fw => `
- ${escapeHtml(fw)}
- `).join('') || 'No test frameworks detected '}
-
-
-
-
-
-
-
- ๐๏ธ Architecture
-
-
-
-
-
-
Style
-
- ${escapeHtml(project.architecture.style)}
-
-
-
-
-
-
Layers
-
- ${project.architecture.layers.map(layer => `
- ${escapeHtml(layer)}
- `).join('') || 'None '}
-
-
-
-
-
-
Patterns
-
- ${project.architecture.patterns.map(pattern => `
- ${escapeHtml(pattern)}
- `).join('') || 'None '}
-
-
-
-
-
-
-
-
- โ๏ธ Key Components
-
-
- ${project.keyComponents.length > 0 ? `
-
- ${project.keyComponents.map(comp => {
- const importanceColors = {
- high: 'border-l-4 border-l-destructive bg-destructive/5',
- medium: 'border-l-4 border-l-warning bg-warning/5',
- low: 'border-l-4 border-l-muted-foreground bg-muted'
- };
- const importanceBadges = {
- high: '
High ',
- medium: '
Medium ',
- low: '
Low '
- };
- return `
-
-
-
${escapeHtml(comp.name)}
- ${importanceBadges[comp.importance] || ''}
-
-
${escapeHtml(comp.description)}
-
${escapeHtml(comp.path)}
-
- `;
- }).join('')}
-
- ` : '
No key components identified
'}
-
-
-
-
-
- ๐ Development History
-
-
- ${renderDevelopmentIndex(project.developmentIndex)}
-
-
-
-
-
- ๐ Statistics
-
-
-
-
-
${project.statistics.total_features || 0}
-
Total Features
-
-
-
${project.statistics.total_sessions || 0}
-
Total Sessions
-
-
-
Last Updated
-
${formatDate(project.statistics.last_updated)}
-
-
-
- `;
-}
-
-function renderDevelopmentIndex(devIndex) {
- if (!devIndex) return 'No development history available
';
-
- const categories = [
- { key: 'feature', label: 'Features', icon: 'โจ', badgeClass: 'bg-primary-light text-primary' },
- { key: 'enhancement', label: 'Enhancements', icon: 'โก', badgeClass: 'bg-success-light text-success' },
- { key: 'bugfix', label: 'Bug Fixes', icon: '๐', badgeClass: 'bg-destructive/10 text-destructive' },
- { key: 'refactor', label: 'Refactorings', icon: '๐ง', badgeClass: 'bg-warning-light text-warning' },
- { key: 'docs', label: 'Documentation', icon: '๐', badgeClass: 'bg-muted text-muted-foreground' }
- ];
-
- const totalEntries = categories.reduce((sum, cat) => sum + (devIndex[cat.key]?.length || 0), 0);
-
- if (totalEntries === 0) {
- return 'No development history entries
';
- }
-
- return `
-
- ${categories.map(cat => {
- const entries = devIndex[cat.key] || [];
- if (entries.length === 0) return '';
-
- return `
-
-
- ${cat.icon}
- ${cat.label}
- ${entries.length}
-
-
- ${entries.slice(0, 5).map(entry => `
-
-
-
${escapeHtml(entry.title)}
- ${formatDate(entry.date)}
-
- ${entry.description ? `
${escapeHtml(entry.description)}
` : ''}
-
- ${entry.sub_feature ? `${escapeHtml(entry.sub_feature)} ` : ''}
- ${entry.status ? `${escapeHtml(entry.status)} ` : ''}
-
-
- `).join('')}
- ${entries.length > 5 ? `
... and ${entries.length - 5} more
` : ''}
-
-
- `;
- }).join('')}
-
- `;
-}
-
-// ========== Helper Functions ==========
-
-/**
- * Normalize line endings in content
- * Handles both literal \r\n escape sequences and actual newlines
- */
-function normalizeLineEndings(content) {
- if (!content) return '';
- let normalized = content;
- // If content has literal \r\n or \n as text (escaped), convert to actual newlines
- if (normalized.includes('\\r\\n')) {
- normalized = normalized.replace(/\\r\\n/g, '\n');
- } else if (normalized.includes('\\n')) {
- normalized = normalized.replace(/\\n/g, '\n');
- }
- // Normalize CRLF to LF for consistent rendering
- normalized = normalized.replace(/\r\n/g, '\n');
- return normalized;
-}
-
-// ========== Markdown Modal Functions ==========
-
-function openMarkdownModal(title, content, type = 'markdown') {
- const modal = document.getElementById('markdownModal');
- const titleEl = document.getElementById('markdownModalTitle');
- const rawEl = document.getElementById('markdownRaw');
- const previewEl = document.getElementById('markdownPreview');
-
- // Normalize line endings
- const normalizedContent = normalizeLineEndings(content);
-
- titleEl.textContent = title;
- rawEl.textContent = normalizedContent;
-
- // Render preview based on type
- if (typeof marked !== 'undefined' && type === 'markdown') {
- previewEl.innerHTML = marked.parse(normalizedContent);
- } else if (type === 'json') {
- // For JSON, try to parse and re-stringify with formatting
- try {
- const parsed = typeof normalizedContent === 'string' ? JSON.parse(normalizedContent) : normalizedContent;
- const formatted = JSON.stringify(parsed, null, 2);
- previewEl.innerHTML = '' + escapeHtml(formatted) + ' ';
- } catch (e) {
- // If not valid JSON, show as-is
- previewEl.innerHTML = '' + escapeHtml(normalizedContent) + ' ';
- }
- } else {
- // Fallback: simple text with line breaks
- previewEl.innerHTML = '' + escapeHtml(normalizedContent) + ' ';
- }
-
- // Show modal and default to preview tab
- modal.classList.remove('hidden');
- switchMarkdownTab('preview');
-}
-
-function closeMarkdownModal() {
- const modal = document.getElementById('markdownModal');
- modal.classList.add('hidden');
-}
-
-function switchMarkdownTab(tab) {
- const rawEl = document.getElementById('markdownRaw');
- const previewEl = document.getElementById('markdownPreview');
- const rawTabBtn = document.getElementById('mdTabRaw');
- const previewTabBtn = document.getElementById('mdTabPreview');
-
- if (tab === 'raw') {
- rawEl.classList.remove('hidden');
- previewEl.classList.add('hidden');
- rawTabBtn.classList.add('active', 'bg-background', 'text-foreground');
- rawTabBtn.classList.remove('text-muted-foreground');
- previewTabBtn.classList.remove('active', 'bg-background', 'text-foreground');
- previewTabBtn.classList.add('text-muted-foreground');
- } else {
- rawEl.classList.add('hidden');
- previewEl.classList.remove('hidden');
- previewTabBtn.classList.add('active', 'bg-background', 'text-foreground');
- previewTabBtn.classList.remove('text-muted-foreground');
- rawTabBtn.classList.remove('active', 'bg-background', 'text-foreground');
- rawTabBtn.classList.add('text-muted-foreground');
- }
-}
-
-// Close modal on Escape key
-document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') {
- closeMarkdownModal();
- }
-});