From a6f9701679614bfb0211508b94e138b3f797a20c Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 7 Dec 2025 18:07:28 +0800 Subject: [PATCH] Add exploration field rendering helpers for dynamic content display - Implemented `renderExpField` to handle various data types for exploration fields. - Created `renderExpArray` to format arrays, including support for objects with specific properties. - Developed `renderExpObject` for recursive rendering of object values, filtering out private keys. - Introduced HTML escaping for safe rendering of user-generated content. --- ccw/src/core/server.js | 3 +- .../dashboard-js/components/_exp_helpers.js | 52 + .../dashboard-js/components/tabs-context.js | 62 +- .../dashboard-js/components/tabs-other.js | 72 +- ccw/src/templates/dashboard.css | 102 + ccw/src/templates/dashboard.js.backup | 4459 ----------------- 6 files changed, 201 insertions(+), 4549 deletions(-) create mode 100644 ccw/src/templates/dashboard-js/components/_exp_helpers.js delete mode 100644 ccw/src/templates/dashboard.js.backup 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 `
${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 ``; +} + +// 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(`

โš–๏ธ Conflict Resolution Decisions

- Session: \${escapeHtml(conflictResolution.session_id || 'N/A')} - \${conflictResolution.resolved_at ? \`Resolved: \${formatDate(conflictResolution.resolved_at)}\` : ''} + Session: ${escapeHtml(conflictResolution.session_id || 'N/A')} + ${conflictResolution.resolved_at ? `Resolved: ${formatDate(conflictResolution.resolved_at)}` : ''}
- \`); + `); // User decisions if (conflictResolution.user_decisions && Object.keys(conflictResolution.user_decisions).length > 0) { const decisions = Object.entries(conflictResolution.user_decisions); - sections.push(\` + sections.push(`
โ–ถ - +
- \`); + `); } // Resolved conflicts if (conflictResolution.resolved_conflicts && conflictResolution.resolved_conflicts.length > 0) { - sections.push(\` + sections.push(`
โ–ถ - +
- \`); + `); } - 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(` -
- -

${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(` -
- -

${escapeHtml(data.patterns)}

-
- `); + content.push(renderExpField('Patterns', data.patterns)); } - // Dependencies + // Dependencies - handle string or object if (data.dependencies) { - content.push(` -
- -

${escapeHtml(data.dependencies)}

-
- `); + content.push(renderExpField('Dependencies', data.dependencies)); } - // Integration points + // Integration points - handle string or object if (data.integration_points) { - content.push(` -
- -

${escapeHtml(data.integration_points)}

-
- `); + content.push(renderExpField('Integration Points', data.integration_points)); } - // Constraints + // Constraints - handle string or object if (data.constraints) { - content.push(` -
- -

${escapeHtml(data.constraints)}

-
- `); + content.push(renderExpField('Constraints', data.constraints)); } - // Clarification needs - if (data.clarification_needs && data.clarification_needs.length) { - content.push(` -
- -
- ${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 = ` -
-
- ${icons.folder} -

Folder Selected

-
-
-
- ${dirName} -
-

- Confirm or edit the full path: -

-
- - - -
-
- -
- `; - 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 = ` -
-
- ${icons.folder} -

Open Project

-
-
-
- - - -
-
- -
- `; - 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 = ` -
-
- ${icons.terminal} -

Run Command

-
-
-

To view the dashboard for this project, run:

-
- ccw view -p "${path}" - -
-

- 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 ` -
-
-
${escapeHtml(session.session_id || 'Unknown')}
-
- ${typeBadge} - - ${isActive ? 'ACTIVE' : 'ARCHIVED'} - -
-
-
-
- ๐Ÿ“… ${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 = ` -
- -
- -
-

${escapeHtml(session.session_id)}

-
- ${session.type || 'workflow'} - - ${isActive ? 'ACTIVE' : 'ARCHIVED'} - -
-
-
- - -
-
- Created: - ${formatDate(session.created_at)} -
- ${session.archived_at ? ` -
- Archived: - ${formatDate(session.archived_at)} -
- ` : ''} -
- Project: - ${escapeHtml(session.project || '-')} -
-
- Tasks: - ${completed}/${tasks.length} completed -
-
- - -
- - - - - ${session.hasReview ? ` - - ` : ''} -
- - -
- ${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 ` -
-
- ${escapeHtml(taskId)} - ${escapeHtml(task.title || task.meta?.title || 'Untitled')} - ${status} -
-
- `; -} - -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(` -
- -

${escapeHtml(task.description)}

-
- `); - } - - // Requirements - if (ctx.requirements?.length) { - sections.push(` -
- - -
- `); - } - - // Focus paths - if (ctx.focus_paths?.length) { - sections.push(` -
- -
${ctx.focus_paths.map(p => `${escapeHtml(p)}`).join('')}
-
- `); - } - - // Modification points - if (task.modification_points?.length) { - sections.push(` -
- -
- ${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(` -
- - -
- `); - } - - 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(` -
- -
    - ${steps.map(s => `
  1. ${escapeHtml(typeof s === 'string' ? s : s.step || s.action || JSON.stringify(s))}
  2. `).join('')} -
-
- `); - } - - // Pre-analysis - const preAnalysis = fc.pre_analysis || task.pre_analysis || []; - if (preAnalysis.length) { - sections.push(` -
- - -
- `); - } - - // Target files - const targetFiles = fc.target_files || task.target_files || []; - if (targetFiles.length) { - sections.push(` -
- -
${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

- -
- - - ${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)}: - -
- `); - } - }); - } - } - - return sections.join(''); -} - -function renderCodingConventions(conventions) { - const sections = []; - - if (conventions.naming) { - sections.push(` -
- Naming: - -
- `); - } - - if (conventions.error_handling) { - sections.push(` -
- Error Handling: - -
- `); - } - - if (conventions.testing) { - sections.push(` -
- Testing: - -
- `); - } - - 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.path)} - ${(doc.relevance_score * 100).toFixed(0)}% -
-
-
${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.path)} - ${(src.relevance_score * 100).toFixed(0)}% -
-
-
${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.path)} - ${test.test_count ? `${test.test_count} tests` : ''} -
-
-
${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 ? ` -
-
- Backend - ${escapeHtml(frameworks.backend.name || 'N/A')} -
- ${frameworks.backend.plugins ? ` -
${frameworks.backend.plugins.map(p => `${escapeHtml(p)}`).join('')}
- ` : ''} -
- ` : ''} - ${frameworks.frontend ? ` -
-
- Frontend - ${escapeHtml(frameworks.frontend.name || 'N/A')} -
- ${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')}

- -
-
${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

- -
-
${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 ` -
-
- ${escapeHtml(dim)} - ${findings.length} finding${findings.length !== 1 ? 's' : ''} -
-
- ${findings.map(f => ` -
-
- ${f.severity || 'medium'} - ${escapeHtml(f.title || 'Finding')} -
-

${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 ` -
- -
- -
-

๐Ÿ” ${escapeHtml(session.session_id)}

-
- Review - - ${isActive ? 'ACTIVE' : 'ARCHIVED'} - -
-
-
- - -
-
-

๐Ÿ“Š Review Progress

- ${session.phase || 'In Progress'} -
- - -
-
-
๐Ÿ“Š
-
${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('')} -
-
- - -
-
-

๐Ÿ” Findings by Dimension

-
- - - - -
-
-
- ${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 += ` -
-
- ${escapeHtml(dim.name)} - ${findings.length} findings -
-
- ${findings.map(f => ` -
-
- ${f.severity || 'medium'} - ${f.fix_status ? `${f.fix_status}` : ''} -
-
${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 || '
No findings
'; -} - -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 ` -
- -
- -
-

๐Ÿ”ง ${escapeHtml(session.session_id)}

-
- Fix - - ${isActive ? 'ACTIVE' : 'ARCHIVED'} - -
-
-
- - -
-
-

๐Ÿ”ง Fix Progress

- ${session.phase || 'Execution'} -
- - -
-
-
-
- ${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('')} -
- ` : ''} -
- - -
-
-

๐Ÿ“‹ Fix Tasks

-
- - - - - -
-
-
- ${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.task_id || task.id || 'N/A')} - ${statusText} -
-
${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(session.session_id)} - Session JSON -
- -
-
-
${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 ` -
-
-
${escapeHtml(session.id)}
- - ${session.type === 'lite-plan' ? '๐Ÿ“ PLAN' : '๐Ÿ”ง FIX'} - -
-
-
- ๐Ÿ“… ${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 = ` -
- -
- -
-

${session.type === 'lite-plan' ? '๐Ÿ“' : '๐Ÿ”ง'} ${escapeHtml(session.id)}

-
- ${session.type} -
-
-
- - -
-
- Created: - ${formatDate(session.createdAt)} -
-
- Tasks: - ${tasks.length} tasks -
-
- - -
- - - - -
- - -
- ${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 ` -
-
- ${escapeHtml(task.id)} - ${escapeHtml(task.title || 'Untitled')} - -
-
- ${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('')} -
-
- ` : ''} - - -
-

โ„น๏ธ Metadata

-
- ${plan.estimated_time ? `
Estimated Time: ${escapeHtml(plan.estimated_time)}
` : ''} - ${plan.complexity ? `
Complexity: ${escapeHtml(plan.complexity)}
` : ''} - ${plan.recommended_execution ? `
Execution: ${escapeHtml(plan.recommended_execution)}
` : ''} -
-
- - -
-

{ } 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(` -
-

${escapeHtml(manifest.task_description || 'Exploration Context')}

-
- Complexity: ${escapeHtml(manifest.complexity || 'N/A')} - Explorations: ${manifest.exploration_count || 0} -
-
- `); - - // 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(` -
-
- โ–ถ - -
- -
- `); - } - - return `
${sections.join('')}
`; -} - -function renderExplorationAngle(angle, data) { - let content = []; - - // Project structure (architecture) - if (data.project_structure) { - content.push(` -
- -

${escapeHtml(data.project_structure)}

-
- `); - } - - // Relevant files - if (data.relevant_files && data.relevant_files.length) { - content.push(` -
- -
- ${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(` -
- -

${escapeHtml(data.patterns)}

-
- `); - } - - // Dependencies - if (data.dependencies) { - content.push(` -
- -

${escapeHtml(data.dependencies)}

-
- `); - } - - // Integration points - if (data.integration_points) { - content.push(` -
- -

${escapeHtml(data.integration_points)}

-
- `); - } - - // Constraints - if (data.constraints) { - content.push(` -
- -

${escapeHtml(data.constraints)}

-
- `); - } - - // Clarification needs - if (data.clarification_needs && data.clarification_needs.length) { - content.push(` -
- -
- ${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 ` -
-
- ${escapeHtml(task.id)} - ${escapeHtml(task.title || 'Untitled')} - ${task.status} -
- -
-
- - -
-
- โ–ถ - - ${escapeHtml((task.meta?.type || task.meta?.action || '') + (task.meta?.scope ? ' | ' + task.meta.scope : ''))} -
- -
- - -
-
- โ–ถ - - ${escapeHtml(getContextPreview(task.context, rawTask))} -
- -
- - -
-
- โ–ถ - - ${escapeHtml(getFlowControlPreview(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) => ` -
-
[${i + 1}]
- ${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(` -
- - ${description ? `

${escapeHtml(description)}

` : ''} - ${requirements.length > 0 ? `` : ''} -
- `); - } - - // 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 ? ` -
- ${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(` -
- - -
- `); - } - - // Dependencies - const depends = context?.depends_on || rawTask?.depends_on || []; - if (depends.length > 0) { - sections.push(` -
- -
${depends.map(d => `${escapeHtml(d)}`).join('')}
-
- `); - } - - // Reference - const reference = rawTask?.reference; - if (reference) { - sections.push(` -
- - ${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(` -
- - -
- `); - } - - // Target files - const targetFiles = flowControl?.target_files || rawTask?.target_files || []; - if (targetFiles.length > 0) { - sections.push(` -
- -
${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(taskId)} - Task JSON -
- -
-
-
${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 => ` -
  1. ${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}
  2. - `).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

- -
- `); - } - - // 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 ` - -
- ${escapeHtml(task.task_id || task.id || 'N/A')} - ${rawTask.action ? `${escapeHtml(rawTask.action)}` : ''} -
- - -
- - - - -
- - -
- -
- ${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

- -
- `); - } - - // 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) => ` -
  1. - ${idx + 1} - ${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))} -
  2. - `).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

- -
- `; -} - - -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 ` - -
- ${escapeHtml(task.task_id || task.id || 'N/A')} - ${task.status || 'pending'} -
- - -
- - - - -
- - -
- -
- ${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 ${step.step || idx + 1} - ${escapeHtml(step.title || 'Untitled Step')} -
- ${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('')} -
-
- ` : '
Modifications

-

'} - ${hasFlow ? ` -
- Logic Flow -
    - ${step.logic_flow.map(lf => `
  1. ${escapeHtml(typeof lf === 'string' ? lf : lf.action || JSON.stringify(lf))}
  2. `).join('')} -
-
- ` : '
Logic Flow

-

'} -
- ` : ''} - ${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

- -
- `; -} - -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(); - } -});