diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js
index 1e29c2b6..1da4c0b6 100644
--- a/ccw/src/core/server.js
+++ b/ccw/src/core/server.js
@@ -61,6 +61,8 @@ const MODULE_FILES = [
'components/_exp_helpers.js',
'components/tabs-other.js',
'components/tabs-context.js',
+ 'components/_conflict_tab.js',
+ 'components/_review_tab.js',
'components/task-drawer-core.js',
'components/task-drawer-renderers.js',
'components/flowchart.js',
diff --git a/ccw/src/templates/dashboard-js/components/_conflict_tab.js b/ccw/src/templates/dashboard-js/components/_conflict_tab.js
new file mode 100644
index 00000000..78b66104
--- /dev/null
+++ b/ccw/src/templates/dashboard-js/components/_conflict_tab.js
@@ -0,0 +1,112 @@
+// ==========================================
+// Conflict Resolution Tab
+// ==========================================
+
+async function loadAndRenderConflictTab(session, contentArea) {
+ contentArea.innerHTML = '
Loading conflict resolution...
';
+
+ try {
+ if (window.SERVER_MODE && session.path) {
+ const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=conflict`);
+ if (response.ok) {
+ const data = await response.json();
+ contentArea.innerHTML = renderConflictCards(data.conflictResolution);
+ return;
+ }
+ }
+ contentArea.innerHTML = `
+
+
⚖️
+
No Conflict Resolution
+
No conflict-resolution-decisions.json found for this session.
+
+ `;
+ } catch (err) {
+ contentArea.innerHTML = `Failed to load conflict resolution: ${err.message}
`;
+ }
+}
+
+function renderConflictCards(conflictResolution) {
+ if (!conflictResolution) {
+ return `
+
+
⚖️
+
No Conflict Resolution
+
No conflict decisions found for this session.
+
+ `;
+ }
+
+ let cards = [];
+
+ // Header info
+ cards.push(`
+
+ `);
+
+ // User Decisions as cards
+ if (conflictResolution.user_decisions && Object.keys(conflictResolution.user_decisions).length > 0) {
+ const decisions = Object.entries(conflictResolution.user_decisions);
+
+ cards.push(`🎯 User Decisions (${decisions.length})
`);
+ cards.push('');
+
+ for (const [key, decision] of decisions) {
+ const label = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ cards.push(`
+
+
+
+ Choice:
+ ${escapeHtml(decision.choice || 'N/A')}
+
+ ${decision.description ? `
+
${escapeHtml(decision.description)}
+ ` : ''}
+ ${decision.implications && decision.implications.length > 0 ? `
+
+
Implications:
+
+ ${decision.implications.map(impl => `- ${escapeHtml(impl)}
`).join('')}
+
+
+ ` : ''}
+
+ `);
+ }
+ cards.push('
');
+ }
+
+ // Resolved Conflicts as cards
+ if (conflictResolution.resolved_conflicts && conflictResolution.resolved_conflicts.length > 0) {
+ cards.push(`✅ Resolved Conflicts (${conflictResolution.resolved_conflicts.length})
`);
+ cards.push('');
+
+ for (const conflict of conflictResolution.resolved_conflicts) {
+ cards.push(`
+
+
+
${escapeHtml(conflict.brief || '')}
+
+ Strategy:
+ ${escapeHtml(conflict.strategy || 'N/A')}
+
+
+ `);
+ }
+ cards.push('
');
+ }
+
+ return `${cards.join('')}
`;
+}
diff --git a/ccw/src/templates/dashboard-js/components/_exp_helpers.js b/ccw/src/templates/dashboard-js/components/_exp_helpers.js
index 551a5374..07c44e7d 100644
--- a/ccw/src/templates/dashboard-js/components/_exp_helpers.js
+++ b/ccw/src/templates/dashboard-js/components/_exp_helpers.js
@@ -1,3 +1,5 @@
+// Exploration helpers loaded
+
// Helper: Render exploration field with smart type detection
function renderExpField(label, value) {
if (value === null || value === undefined) return '';
diff --git a/ccw/src/templates/dashboard-js/components/_review_tab.js b/ccw/src/templates/dashboard-js/components/_review_tab.js
new file mode 100644
index 00000000..67b92e1f
--- /dev/null
+++ b/ccw/src/templates/dashboard-js/components/_review_tab.js
@@ -0,0 +1,640 @@
+// ==========================================
+// Enhanced Review Tab with Multi-Select & Preview
+// ==========================================
+
+// Review tab state
+let reviewTabState = {
+ allFindings: [],
+ filteredFindings: [],
+ selectedFindings: new Set(),
+ currentFilters: {
+ dimension: 'all',
+ severities: new Set(),
+ search: ''
+ },
+ sortConfig: {
+ field: 'severity',
+ order: 'desc'
+ },
+ previewFinding: null,
+ sessionPath: null,
+ sessionId: null
+};
+
+// ==========================================
+// Main Review Tab Render
+// ==========================================
+
+function renderReviewContent(review) {
+ if (!review || !review.dimensions) {
+ return `
+
+
🔍
+
No Review Data
+
No review findings in .review/
+
+ `;
+ }
+
+ // Convert dimensions object to flat findings array
+ const findings = [];
+ let findingIndex = 0;
+
+ Object.entries(review.dimensions).forEach(([dim, rawFindings]) => {
+ let dimFindings = [];
+ if (Array.isArray(rawFindings)) {
+ dimFindings = rawFindings;
+ } else if (rawFindings && typeof rawFindings === 'object') {
+ if (Array.isArray(rawFindings.findings)) {
+ dimFindings = rawFindings.findings;
+ }
+ }
+
+ dimFindings.forEach(f => {
+ findings.push({
+ id: f.id || `finding-${findingIndex++}`,
+ title: f.title || 'Finding',
+ description: f.description || '',
+ severity: (f.severity || 'medium').toLowerCase(),
+ dimension: dim,
+ category: f.category || '',
+ file: f.file || '',
+ line: f.line || '',
+ code_context: f.code_context || f.snippet || '',
+ recommendations: f.recommendations || (f.recommendation ? [f.recommendation] : []),
+ root_cause: f.root_cause || '',
+ impact: f.impact || '',
+ references: f.references || [],
+ metadata: f.metadata || {}
+ });
+ });
+ });
+
+ if (findings.length === 0) {
+ return `
+
+
🔍
+
No Findings
+
No review findings found.
+
+ `;
+ }
+
+ // Store findings in state
+ reviewTabState.allFindings = findings;
+ reviewTabState.filteredFindings = [...findings];
+ reviewTabState.selectedFindings.clear();
+ reviewTabState.previewFinding = null;
+
+ // Get dimensions for tabs
+ const dimensions = [...new Set(findings.map(f => f.dimension))];
+
+ // Count by severity
+ const severityCounts = {
+ critical: findings.filter(f => f.severity === 'critical').length,
+ high: findings.filter(f => f.severity === 'high').length,
+ medium: findings.filter(f => f.severity === 'medium').length,
+ low: findings.filter(f => f.severity === 'low').length
+ };
+
+ return `
+
+
+
+
+
+
+
+
+
+ Sort:
+
+
+
+
+
+
+
+
+
+
+ ${dimensions.map(dim => `
+
+ `).join('')}
+
+
+
+
+
+
+
+
+ ${renderReviewFindingsList(findings)}
+
+
+
+
+
+
+
👆
+
Click on a finding to preview details
+
+
+
+
+ `;
+}
+
+// ==========================================
+// Findings List Rendering
+// ==========================================
+
+function renderReviewFindingsList(findings) {
+ if (findings.length === 0) {
+ return `
+
+ ✨
+ No findings match your filters
+
+ `;
+ }
+
+ return findings.map(finding => `
+
+
+
+
+ ${finding.severity}
+ ${escapeHtml(finding.dimension)}
+
+
${escapeHtml(finding.title)}
+ ${finding.file ? `
📄 ${escapeHtml(finding.file)}${finding.line ? ':' + finding.line : ''}
` : ''}
+
+
+ `).join('');
+}
+
+// ==========================================
+// Preview Panel Rendering
+// ==========================================
+
+function previewReviewFinding(findingId) {
+ const finding = reviewTabState.allFindings.find(f => f.id === findingId);
+ if (!finding) return;
+
+ reviewTabState.previewFinding = finding;
+
+ // Update active state in list
+ document.querySelectorAll('.review-finding-item').forEach(item => {
+ item.classList.toggle('previewing', item.dataset.findingId === findingId);
+ });
+
+ const previewPanel = document.getElementById('reviewPreviewPanel');
+ if (!previewPanel) return;
+
+ previewPanel.innerHTML = `
+
+
+
+
${escapeHtml(finding.title)}
+
+ ${finding.file ? `
+
+
📄 Location
+
+ ${escapeHtml(finding.file)}${finding.line ? ':' + finding.line : ''}
+
+
+ ` : ''}
+
+
+
📝 Description
+
${escapeHtml(finding.description)}
+
+
+ ${finding.code_context ? `
+
+
💻 Code Context
+
${escapeHtml(finding.code_context)}
+
+ ` : ''}
+
+ ${finding.recommendations && finding.recommendations.length > 0 ? `
+
+
✅ Recommendations
+
+ ${finding.recommendations.map(r => `- ${escapeHtml(r)}
`).join('')}
+
+
+ ` : ''}
+
+ ${finding.root_cause ? `
+
+
🔍 Root Cause
+
${escapeHtml(finding.root_cause)}
+
+ ` : ''}
+
+ ${finding.impact ? `
+
+
⚠️ Impact
+
${escapeHtml(finding.impact)}
+
+ ` : ''}
+
+ ${finding.references && finding.references.length > 0 ? `
+
+
🔗 References
+
+ ${finding.references.map(ref => {
+ const isUrl = ref.startsWith('http');
+ return `- ${isUrl ? `${ref}` : ref}
`;
+ }).join('')}
+
+
+ ` : ''}
+
+ ${finding.metadata && Object.keys(finding.metadata).length > 0 ? `
+
+ ` : ''}
+
+ `;
+}
+
+// ==========================================
+// Selection Management
+// ==========================================
+
+function toggleReviewFindingSelection(findingId, event) {
+ if (event) {
+ event.stopPropagation();
+ }
+
+ if (reviewTabState.selectedFindings.has(findingId)) {
+ reviewTabState.selectedFindings.delete(findingId);
+ } else {
+ reviewTabState.selectedFindings.add(findingId);
+ }
+
+ updateReviewSelectionUI();
+
+ // Update preview panel button if this finding is being previewed
+ if (reviewTabState.previewFinding && reviewTabState.previewFinding.id === findingId) {
+ previewReviewFinding(findingId);
+ }
+}
+
+function selectAllReviewFindings() {
+ reviewTabState.allFindings.forEach(f => reviewTabState.selectedFindings.add(f.id));
+ updateReviewSelectionUI();
+}
+
+function selectVisibleReviewFindings() {
+ reviewTabState.filteredFindings.forEach(f => reviewTabState.selectedFindings.add(f.id));
+ updateReviewSelectionUI();
+}
+
+function selectReviewBySeverity(severity) {
+ reviewTabState.allFindings
+ .filter(f => f.severity === severity)
+ .forEach(f => reviewTabState.selectedFindings.add(f.id));
+ updateReviewSelectionUI();
+}
+
+function clearReviewSelection() {
+ reviewTabState.selectedFindings.clear();
+ updateReviewSelectionUI();
+}
+
+function updateReviewSelectionUI() {
+ // Update counter
+ const counter = document.getElementById('reviewSelectionCounter');
+ if (counter) {
+ counter.textContent = `${reviewTabState.selectedFindings.size} selected`;
+ }
+
+ // Update export button
+ const exportBtn = document.getElementById('exportFixBtn');
+ if (exportBtn) {
+ exportBtn.disabled = reviewTabState.selectedFindings.size === 0;
+ }
+
+ // Update checkbox states in list
+ document.querySelectorAll('.review-finding-item').forEach(item => {
+ const findingId = item.dataset.findingId;
+ const isSelected = reviewTabState.selectedFindings.has(findingId);
+ item.classList.toggle('selected', isSelected);
+ const checkbox = item.querySelector('.finding-checkbox');
+ if (checkbox) {
+ checkbox.checked = isSelected;
+ }
+ });
+}
+
+// ==========================================
+// Filtering & Sorting
+// ==========================================
+
+function filterReviewByDimension(dimension) {
+ reviewTabState.currentFilters.dimension = dimension;
+
+ // Update tab active state
+ document.querySelectorAll('.dim-tab').forEach(tab => {
+ tab.classList.toggle('active', tab.dataset.dimension === dimension);
+ });
+
+ applyReviewFilters();
+}
+
+function filterReviewBySeverity(severity) {
+ // Toggle the severity filter
+ if (reviewTabState.currentFilters.severities.has(severity)) {
+ reviewTabState.currentFilters.severities.delete(severity);
+ } else {
+ reviewTabState.currentFilters.severities.add(severity);
+ }
+
+ // Update filter chip UI
+ const filterChip = document.getElementById(`filter-${severity}`);
+ if (filterChip) {
+ filterChip.classList.toggle('active', reviewTabState.currentFilters.severities.has(severity));
+ const checkbox = filterChip.querySelector('input[type="checkbox"]');
+ if (checkbox) {
+ checkbox.checked = reviewTabState.currentFilters.severities.has(severity);
+ }
+ }
+
+ applyReviewFilters();
+}
+
+function toggleReviewSeverityFilter(severity) {
+ filterReviewBySeverity(severity);
+}
+
+function onReviewSearch(searchText) {
+ reviewTabState.currentFilters.search = searchText.toLowerCase();
+ applyReviewFilters();
+}
+
+function applyReviewFilters() {
+ reviewTabState.filteredFindings = reviewTabState.allFindings.filter(finding => {
+ // Dimension filter
+ if (reviewTabState.currentFilters.dimension !== 'all') {
+ if (finding.dimension !== reviewTabState.currentFilters.dimension) {
+ return false;
+ }
+ }
+
+ // Severity filter (multi-select)
+ if (reviewTabState.currentFilters.severities.size > 0) {
+ if (!reviewTabState.currentFilters.severities.has(finding.severity)) {
+ return false;
+ }
+ }
+
+ // Search filter
+ if (reviewTabState.currentFilters.search) {
+ const searchText = `${finding.title} ${finding.description} ${finding.file} ${finding.category}`.toLowerCase();
+ if (!searchText.includes(reviewTabState.currentFilters.search)) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ sortReviewFindings();
+}
+
+function sortReviewFindings() {
+ const sortBy = document.getElementById('reviewSortSelect')?.value || 'severity';
+ reviewTabState.sortConfig.field = sortBy;
+
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
+
+ reviewTabState.filteredFindings.sort((a, b) => {
+ let comparison = 0;
+
+ if (sortBy === 'severity') {
+ comparison = severityOrder[a.severity] - severityOrder[b.severity];
+ } else if (sortBy === 'dimension') {
+ comparison = a.dimension.localeCompare(b.dimension);
+ } else if (sortBy === 'file') {
+ comparison = (a.file || '').localeCompare(b.file || '');
+ }
+
+ return reviewTabState.sortConfig.order === 'asc' ? comparison : -comparison;
+ });
+
+ renderFilteredReviewFindings();
+}
+
+function toggleReviewSortOrder() {
+ reviewTabState.sortConfig.order = reviewTabState.sortConfig.order === 'asc' ? 'desc' : 'asc';
+
+ const icon = document.getElementById('reviewSortOrderIcon');
+ if (icon) {
+ icon.textContent = reviewTabState.sortConfig.order === 'asc' ? '↑' : '↓';
+ }
+
+ sortReviewFindings();
+}
+
+function resetReviewFilters() {
+ // Reset state
+ reviewTabState.currentFilters.dimension = 'all';
+ reviewTabState.currentFilters.severities.clear();
+ reviewTabState.currentFilters.search = '';
+ reviewTabState.sortConfig.field = 'severity';
+ reviewTabState.sortConfig.order = 'desc';
+
+ // Reset UI
+ document.querySelectorAll('.dim-tab').forEach(tab => {
+ tab.classList.toggle('active', tab.dataset.dimension === 'all');
+ });
+
+ document.querySelectorAll('.filter-chip').forEach(chip => {
+ chip.classList.remove('active');
+ const checkbox = chip.querySelector('input[type="checkbox"]');
+ if (checkbox) checkbox.checked = false;
+ });
+
+ const searchInput = document.getElementById('reviewSearchInput');
+ if (searchInput) searchInput.value = '';
+
+ const sortSelect = document.getElementById('reviewSortSelect');
+ if (sortSelect) sortSelect.value = 'severity';
+
+ const sortIcon = document.getElementById('reviewSortOrderIcon');
+ if (sortIcon) sortIcon.textContent = '↓';
+
+ // Re-apply filters
+ reviewTabState.filteredFindings = [...reviewTabState.allFindings];
+ sortReviewFindings();
+}
+
+function renderFilteredReviewFindings() {
+ const listContainer = document.getElementById('reviewFindingsList');
+ const countEl = document.getElementById('reviewFindingsCount');
+
+ if (listContainer) {
+ listContainer.innerHTML = renderReviewFindingsList(reviewTabState.filteredFindings);
+ }
+
+ if (countEl) {
+ countEl.textContent = `${reviewTabState.filteredFindings.length} findings`;
+ }
+}
+
+// ==========================================
+// Export Fix JSON
+// ==========================================
+
+function exportReviewFixJson() {
+ if (reviewTabState.selectedFindings.size === 0) {
+ showToast('Please select at least one finding to export', 'error');
+ return;
+ }
+
+ const selectedFindingsData = reviewTabState.allFindings.filter(f =>
+ reviewTabState.selectedFindings.has(f.id)
+ );
+
+ const session = sessionDataStore[currentSessionDetailKey];
+ const sessionId = session?.session_id || 'unknown';
+ const exportId = `fix-${Date.now()}`;
+
+ const exportData = {
+ export_id: exportId,
+ export_timestamp: new Date().toISOString(),
+ review_id: `review-${sessionId}`,
+ session_id: sessionId,
+ findings_count: selectedFindingsData.length,
+ findings: selectedFindingsData.map(f => ({
+ id: f.id,
+ title: f.title,
+ description: f.description,
+ severity: f.severity,
+ dimension: f.dimension,
+ category: f.category || 'uncategorized',
+ file: f.file,
+ line: f.line,
+ code_context: f.code_context || null,
+ recommendations: f.recommendations || [],
+ root_cause: f.root_cause || null
+ }))
+ };
+
+ // Convert to JSON and download
+ const jsonStr = JSON.stringify(exportData, null, 2);
+ const blob = new Blob([jsonStr], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ const filename = `fix-export-${exportId}.json`;
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ // Show success notification
+ const severityCounts = {
+ critical: selectedFindingsData.filter(f => f.severity === 'critical').length,
+ high: selectedFindingsData.filter(f => f.severity === 'high').length,
+ medium: selectedFindingsData.filter(f => f.severity === 'medium').length,
+ low: selectedFindingsData.filter(f => f.severity === 'low').length
+ };
+
+ showToast(`Exported ${selectedFindingsData.length} findings for fixing (Critical: ${severityCounts.critical}, High: ${severityCounts.high}, Medium: ${severityCounts.medium}, Low: ${severityCounts.low})`, 'success');
+}
diff --git a/ccw/src/templates/dashboard-js/components/carousel.js b/ccw/src/templates/dashboard-js/components/carousel.js
index a8b27d16..93bfd15d 100644
--- a/ccw/src/templates/dashboard-js/components/carousel.js
+++ b/ccw/src/templates/dashboard-js/components/carousel.js
@@ -118,6 +118,14 @@ function renderCarouselSlide(direction = 'none') {
}
const session = carouselSessions[carouselIndex];
+ const sessionType = session.type || 'workflow';
+
+ // Use simplified view for review sessions
+ if (sessionType === 'review') {
+ renderReviewCarouselSlide(container, session, direction);
+ return;
+ }
+
const tasks = session.tasks || [];
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
@@ -126,7 +134,6 @@ function renderCarouselSlide(direction = 'none') {
const progress = taskCount > 0 ? Math.round((completed / taskCount) * 100) : 0;
// Get session type badge
- const sessionType = session.type || 'workflow';
const typeBadgeClass = getSessionTypeBadgeClass(sessionType);
const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
@@ -217,6 +224,48 @@ function renderCarouselSlide(direction = 'none') {
}
}
+// Simplified carousel slide for review sessions
+function renderReviewCarouselSlide(container, session, direction) {
+ const typeBadgeClass = getSessionTypeBadgeClass('review');
+ const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
+ const animClass = direction === 'left' ? 'carousel-slide-left' :
+ direction === 'right' ? 'carousel-slide-right' : 'carousel-fade-in';
+ const createdTime = session.created_at ? formatRelativeTime(session.created_at) : '';
+
+ container.innerHTML = `
+
+
+
+
+
+ review
+
+
${escapeHtml(session.session_id)}
+
+
+
+
+
🔍
+
Click to view findings
+
+
+
+
+
+ 📅 ${createdTime}
+
+
+
+
+ `;
+
+ // Store session data for navigation
+ if (!sessionDataStore[sessionKey]) {
+ sessionDataStore[sessionKey] = session;
+ }
+}
+
function getSessionTypeBadgeClass(type) {
const classes = {
'tdd': 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
diff --git a/ccw/src/templates/dashboard-js/components/mcp-manager.js b/ccw/src/templates/dashboard-js/components/mcp-manager.js
index 1d3db02d..4e5ba733 100644
--- a/ccw/src/templates/dashboard-js/components/mcp-manager.js
+++ b/ccw/src/templates/dashboard-js/components/mcp-manager.js
@@ -5,6 +5,7 @@
let mcpConfig = null;
let mcpAllProjects = {};
let mcpCurrentProjectServers = {};
+let mcpCreateMode = 'form'; // 'form' or 'json'
// ========== Initialization ==========
function initMcpManager() {
@@ -189,13 +190,21 @@ function openMcpCreateModal() {
const modal = document.getElementById('mcpCreateModal');
if (modal) {
modal.classList.remove('hidden');
+ // Reset to form mode
+ mcpCreateMode = 'form';
+ switchMcpCreateTab('form');
// Clear form
document.getElementById('mcpServerName').value = '';
document.getElementById('mcpServerCommand').value = '';
document.getElementById('mcpServerArgs').value = '';
document.getElementById('mcpServerEnv').value = '';
+ // Clear JSON input
+ document.getElementById('mcpServerJson').value = '';
+ document.getElementById('mcpJsonPreview').classList.add('hidden');
// Focus on name input
document.getElementById('mcpServerName').focus();
+ // Setup JSON input listener
+ setupMcpJsonListener();
}
}
@@ -206,7 +215,139 @@ function closeMcpCreateModal() {
}
}
+function switchMcpCreateTab(tab) {
+ mcpCreateMode = tab;
+ const formMode = document.getElementById('mcpFormMode');
+ const jsonMode = document.getElementById('mcpJsonMode');
+ const tabForm = document.getElementById('mcpTabForm');
+ const tabJson = document.getElementById('mcpTabJson');
+
+ if (tab === 'form') {
+ formMode.classList.remove('hidden');
+ jsonMode.classList.add('hidden');
+ tabForm.classList.add('active');
+ tabJson.classList.remove('active');
+ } else {
+ formMode.classList.add('hidden');
+ jsonMode.classList.remove('hidden');
+ tabForm.classList.remove('active');
+ tabJson.classList.add('active');
+ }
+}
+
+function setupMcpJsonListener() {
+ const jsonInput = document.getElementById('mcpServerJson');
+ if (jsonInput && !jsonInput.hasAttribute('data-listener-attached')) {
+ jsonInput.setAttribute('data-listener-attached', 'true');
+ jsonInput.addEventListener('input', () => {
+ updateMcpJsonPreview();
+ });
+ }
+}
+
+function parseMcpJsonConfig(jsonText) {
+ if (!jsonText.trim()) {
+ return { servers: {}, error: null };
+ }
+
+ try {
+ const parsed = JSON.parse(jsonText);
+ let servers = {};
+
+ // Support multiple formats:
+ // 1. {"servers": {...}} format (claude desktop style)
+ // 2. {"mcpServers": {...}} format (claude.json style)
+ // 3. {"serverName": {command, args}} format (direct server config)
+ // 4. {command, args} format (single server without name)
+
+ if (parsed.servers && typeof parsed.servers === 'object') {
+ servers = parsed.servers;
+ } else if (parsed.mcpServers && typeof parsed.mcpServers === 'object') {
+ servers = parsed.mcpServers;
+ } else if (parsed.command && typeof parsed.command === 'string') {
+ // Single server without name - will prompt for name
+ servers = { '__unnamed__': parsed };
+ } else {
+ // Check if all values are server configs (have 'command' property)
+ const isDirectServerConfig = Object.values(parsed).every(
+ v => v && typeof v === 'object' && v.command
+ );
+ if (isDirectServerConfig && Object.keys(parsed).length > 0) {
+ servers = parsed;
+ } else {
+ return { servers: {}, error: 'Invalid MCP server JSON format' };
+ }
+ }
+
+ // Validate each server config
+ for (const [name, config] of Object.entries(servers)) {
+ if (!config.command || typeof config.command !== 'string') {
+ return { servers: {}, error: `Server "${name}" missing required "command" field` };
+ }
+ if (config.args && !Array.isArray(config.args)) {
+ return { servers: {}, error: `Server "${name}" has invalid "args" (must be array)` };
+ }
+ if (config.env && typeof config.env !== 'object') {
+ return { servers: {}, error: `Server "${name}" has invalid "env" (must be object)` };
+ }
+ }
+
+ return { servers, error: null };
+ } catch (e) {
+ return { servers: {}, error: 'Invalid JSON: ' + e.message };
+ }
+}
+
+function updateMcpJsonPreview() {
+ const jsonInput = document.getElementById('mcpServerJson');
+ const previewContainer = document.getElementById('mcpJsonPreview');
+ const previewContent = document.getElementById('mcpJsonPreviewContent');
+
+ const jsonText = jsonInput.value;
+ const { servers, error } = parseMcpJsonConfig(jsonText);
+
+ if (!jsonText.trim()) {
+ previewContainer.classList.add('hidden');
+ return;
+ }
+
+ previewContainer.classList.remove('hidden');
+
+ if (error) {
+ previewContent.innerHTML = `${escapeHtml(error)}
`;
+ return;
+ }
+
+ const serverCount = Object.keys(servers).length;
+ if (serverCount === 0) {
+ previewContent.innerHTML = `No servers found
`;
+ return;
+ }
+
+ const previewHtml = Object.entries(servers).map(([name, config]) => {
+ const displayName = name === '__unnamed__' ? '(will prompt for name)' : name;
+ const argsPreview = config.args ? config.args.slice(0, 2).join(' ') + (config.args.length > 2 ? '...' : '') : '';
+ return `
+
+ +
+ ${escapeHtml(displayName)}
+ ${escapeHtml(config.command)} ${escapeHtml(argsPreview)}
+
+ `;
+ }).join('');
+
+ previewContent.innerHTML = previewHtml;
+}
+
async function submitMcpCreate() {
+ if (mcpCreateMode === 'json') {
+ await submitMcpCreateFromJson();
+ } else {
+ await submitMcpCreateFromForm();
+ }
+}
+
+async function submitMcpCreateFromForm() {
const name = document.getElementById('mcpServerName').value.trim();
const command = document.getElementById('mcpServerCommand').value.trim();
const argsText = document.getElementById('mcpServerArgs').value.trim();
@@ -255,6 +396,86 @@ async function submitMcpCreate() {
serverConfig.env = env;
}
+ await createMcpServerWithConfig(name, serverConfig);
+}
+
+async function submitMcpCreateFromJson() {
+ const jsonText = document.getElementById('mcpServerJson').value.trim();
+
+ if (!jsonText) {
+ showRefreshToast('Please enter JSON configuration', 'error');
+ document.getElementById('mcpServerJson').focus();
+ return;
+ }
+
+ const { servers, error } = parseMcpJsonConfig(jsonText);
+
+ if (error) {
+ showRefreshToast(error, 'error');
+ return;
+ }
+
+ if (Object.keys(servers).length === 0) {
+ showRefreshToast('No valid servers found in JSON', 'error');
+ return;
+ }
+
+ // Handle unnamed server case
+ if (servers['__unnamed__']) {
+ const serverName = prompt('Enter a name for this MCP server:');
+ if (!serverName || !serverName.trim()) {
+ showRefreshToast('Server name is required', 'error');
+ return;
+ }
+ servers[serverName.trim()] = servers['__unnamed__'];
+ delete servers['__unnamed__'];
+ }
+
+ // Add all servers
+ let successCount = 0;
+ let failCount = 0;
+ const serverNames = Object.keys(servers);
+
+ for (const [name, config] of Object.entries(servers)) {
+ try {
+ const response = await fetch('/api/mcp-copy-server', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ projectPath: projectPath,
+ serverName: name,
+ serverConfig: config
+ })
+ });
+
+ if (!response.ok) throw new Error('Failed to create MCP server');
+
+ const result = await response.json();
+ if (result.success) {
+ successCount++;
+ } else {
+ failCount++;
+ }
+ } catch (err) {
+ console.error(`Failed to create MCP server "${name}":`, err);
+ failCount++;
+ }
+ }
+
+ closeMcpCreateModal();
+ await loadMcpConfig();
+ renderMcpManager();
+
+ if (failCount === 0) {
+ showRefreshToast(`${successCount} MCP server${successCount > 1 ? 's' : ''} created successfully`, 'success');
+ } else if (successCount > 0) {
+ showRefreshToast(`${successCount} created, ${failCount} failed`, 'warning');
+ } else {
+ showRefreshToast('Failed to create MCP servers', 'error');
+ }
+}
+
+async function createMcpServerWithConfig(name, serverConfig) {
// Submit to API
try {
const response = await fetch('/api/mcp-copy-server', {
diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js
index 1f24b284..559605fd 100644
--- a/ccw/src/templates/dashboard-js/components/navigation.js
+++ b/ccw/src/templates/dashboard-js/components/navigation.js
@@ -62,6 +62,7 @@ function initNavigation() {
currentView = 'sessions';
currentSessionDetailKey = null;
updateContentTitle();
+ showStatsAndSearch();
renderSessions();
});
});
@@ -75,6 +76,7 @@ function initNavigation() {
currentView = 'liteTasks';
currentSessionDetailKey = null;
updateContentTitle();
+ showStatsAndSearch();
renderLiteTasks();
});
});
diff --git a/ccw/src/templates/dashboard-js/components/tabs-context.js b/ccw/src/templates/dashboard-js/components/tabs-context.js
index 8550c593..85da4849 100644
--- a/ccw/src/templates/dashboard-js/components/tabs-context.js
+++ b/ccw/src/templates/dashboard-js/components/tabs-context.js
@@ -970,10 +970,6 @@ function renderConflictDetectionSection(conflictDetection) {
function renderSessionContextContent(context, explorations, conflictResolution) {
let sections = [];
- // Render conflict resolution decisions if available
- if (conflictResolution) {
- sections.push(renderConflictResolutionContext(conflictResolution));
- }
// Render explorations if available (from exploration-*.json files)
if (explorations && explorations.manifest) {
@@ -1002,7 +998,7 @@ function renderSessionContextContent(context, explorations, conflictResolution)
📦
No Context Data
-
No context-package.json, exploration files, or conflict resolution data found for this session.
+
No context-package.json or exploration files found for this session.
`;
}
diff --git a/ccw/src/templates/dashboard-js/components/tabs-other.js b/ccw/src/templates/dashboard-js/components/tabs-other.js
index fd3952a6..dd1be6f1 100644
--- a/ccw/src/templates/dashboard-js/components/tabs-other.js
+++ b/ccw/src/templates/dashboard-js/components/tabs-other.js
@@ -89,69 +89,8 @@ function renderImplPlanContent(implPlan) {
// ==========================================
// Review Tab Rendering
// ==========================================
-
-function renderReviewContent(review) {
- if (!review || !review.dimensions) {
- return `
-
-
🔍
-
No Review Data
-
No review findings in .review/
-
- `;
- }
-
- const dimensions = Object.entries(review.dimensions);
- if (dimensions.length === 0) {
- return `
-
-
🔍
-
No Findings
-
No review findings found.
-
- `;
- }
-
- return `
-
- ${dimensions.map(([dim, rawFindings]) => {
- // Normalize findings to always be an array
- let findings = [];
- if (Array.isArray(rawFindings)) {
- findings = rawFindings;
- } else if (rawFindings && typeof rawFindings === 'object') {
- // If it's an object with a findings array, use that
- if (Array.isArray(rawFindings.findings)) {
- findings = rawFindings.findings;
- } else {
- // Wrap single object in array or show raw JSON
- findings = [{ title: dim, description: JSON.stringify(rawFindings, null, 2), severity: 'info' }];
- }
- }
-
- return `
-
-
-
- ${findings.map(f => `
-
-
-
${escapeHtml(f.description || '')}
- ${f.file ? `
📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}
` : ''}
-
- `).join('')}
-
-
- `}).join('')}
-
- `;
-}
+// NOTE: Enhanced review tab with multi-select, filtering, and preview panel
+// is now in _review_tab.js - renderReviewContent() function defined there
// ==========================================
// Lite Context Tab Rendering
diff --git a/ccw/src/templates/dashboard-js/views/home.js b/ccw/src/templates/dashboard-js/views/home.js
index ac0d9ab7..17cf4604 100644
--- a/ccw/src/templates/dashboard-js/views/home.js
+++ b/ccw/src/templates/dashboard-js/views/home.js
@@ -20,6 +20,13 @@ function showStatsAndSearch() {
if (searchInput) searchInput.parentElement.style.display = '';
}
+function hideStatsAndCarousel() {
+ const statsGrid = document.getElementById('statsGrid');
+ const searchInput = document.getElementById('searchInput');
+ if (statsGrid) statsGrid.style.display = 'none';
+ if (searchInput) searchInput.parentElement.style.display = 'none';
+}
+
function updateStats() {
const stats = workflowData.statistics || {};
document.getElementById('statTotalSessions').textContent = stats.totalSessions || 0;
@@ -95,6 +102,11 @@ function renderSessionCard(session) {
const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[sessionKey] = session;
+ // Special rendering for review sessions
+ if (sessionType === 'review') {
+ return renderReviewSessionCard(session, sessionKey, typeBadge, isActive, date);
+ }
+
return `
`;
}
+
+// Special card rendering for review sessions
+function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, date) {
+ // Calculate findings stats from reviewDimensions
+ const dimensions = session.reviewDimensions || [];
+ let totalFindings = 0;
+ let criticalCount = 0;
+ let highCount = 0;
+ let mediumCount = 0;
+ let lowCount = 0;
+
+ dimensions.forEach(dim => {
+ const findings = dim.findings || [];
+ totalFindings += findings.length;
+ criticalCount += findings.filter(f => f.severity === 'critical').length;
+ highCount += findings.filter(f => f.severity === 'high').length;
+ mediumCount += findings.filter(f => f.severity === 'medium').length;
+ lowCount += findings.filter(f => f.severity === 'low').length;
+ });
+
+ return `
+
+
+
+
+ 📅 ${formatDate(date)}
+ 🔍 ${totalFindings} findings
+
+ ${totalFindings > 0 ? `
+
+
+ ${criticalCount > 0 ? `🔴 ${criticalCount}` : ''}
+ ${highCount > 0 ? `🟠 ${highCount}` : ''}
+ ${mediumCount > 0 ? `🟡 ${mediumCount}` : ''}
+ ${lowCount > 0 ? `🟢 ${lowCount}` : ''}
+
+
+ ${dimensions.length} dimensions
+
+
+ ` : ''}
+
+
+ `;
+}
diff --git a/ccw/src/templates/dashboard-js/views/lite-tasks.js b/ccw/src/templates/dashboard-js/views/lite-tasks.js
index 69979fb6..d07c6842 100644
--- a/ccw/src/templates/dashboard-js/views/lite-tasks.js
+++ b/ccw/src/templates/dashboard-js/views/lite-tasks.js
@@ -72,6 +72,9 @@ function showLiteTaskDetailPage(sessionKey) {
currentView = 'liteTaskDetail';
currentSessionDetailKey = sessionKey;
+ // Hide stats grid and carousel on detail pages
+ hideStatsAndCarousel();
+
// Also store in sessionDataStore for tab switching compatibility
sessionDataStore[sessionKey] = {
...session,
@@ -158,6 +161,7 @@ function goBackToLiteTasks() {
currentView = 'liteTasks';
currentSessionDetailKey = null;
updateContentTitle();
+ showStatsAndSearch();
renderLiteTasks();
}
diff --git a/ccw/src/templates/dashboard-js/views/review-session.js b/ccw/src/templates/dashboard-js/views/review-session.js
index 2219baeb..7eafc282 100644
--- a/ccw/src/templates/dashboard-js/views/review-session.js
+++ b/ccw/src/templates/dashboard-js/views/review-session.js
@@ -1,18 +1,73 @@
// ==========================================
// REVIEW SESSION DETAIL PAGE
// ==========================================
+// Enhanced with multi-select, filters, split preview panel, and export fix JSON
+
+// Review session state
+let reviewSessionState = {
+ allFindings: [],
+ filteredFindings: [],
+ selectedFindings: new Set(),
+ currentFilters: {
+ dimension: 'all',
+ severities: new Set(),
+ search: ''
+ },
+ sortConfig: {
+ field: 'severity',
+ order: 'desc'
+ },
+ previewFinding: null,
+ dimensions: [],
+ session: null
+};
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);
+ // Store session and dimensions
+ reviewSessionState.session = session;
+ reviewSessionState.dimensions = dimensions;
+
+ // Build flat findings array with dimension info
+ const allFindings = [];
+ let findingIndex = 0;
+ dimensions.forEach(dim => {
+ (dim.findings || []).forEach(f => {
+ allFindings.push({
+ id: f.id || `finding-${findingIndex++}`,
+ title: f.title || 'Finding',
+ description: f.description || '',
+ severity: (f.severity || 'medium').toLowerCase(),
+ dimension: dim.name || 'unknown',
+ category: f.category || '',
+ file: f.file || '',
+ line: f.line || '',
+ code_context: f.code_context || f.snippet || '',
+ recommendations: f.recommendations || (f.recommendation ? [f.recommendation] : []),
+ root_cause: f.root_cause || '',
+ impact: f.impact || '',
+ references: f.references || [],
+ metadata: f.metadata || {},
+ fix_status: f.fix_status || null
+ });
+ });
+ });
+
+ reviewSessionState.allFindings = allFindings;
+ reviewSessionState.filteredFindings = [...allFindings];
+ reviewSessionState.selectedFindings.clear();
+ reviewSessionState.previewFinding = null;
+
+ // Calculate statistics
+ const totalFindings = allFindings.length;
+ const severityCounts = {
+ critical: allFindings.filter(f => f.severity === 'critical').length,
+ high: allFindings.filter(f => f.severity === 'high').length,
+ medium: allFindings.filter(f => f.severity === 'medium').length,
+ low: allFindings.filter(f => f.severity === 'low').length
+ };
return `
@@ -37,7 +92,7 @@ function renderReviewSessionDetailPage(session) {
@@ -49,12 +104,12 @@ function renderReviewSessionDetailPage(session) {
🔴
-
${criticalCount}
+
${severityCounts.critical}
Critical
🟠
-
${highCount}
+
${severityCounts.high}
High
@@ -64,31 +119,117 @@ function renderReviewSessionDetailPage(session) {
-
-
- ${dimensions.map((dim, idx) => `
-
-
D${idx + 1}
-
${escapeHtml(dim.name || 'Unknown')}
-
${dim.findings?.length || 0} findings
-
- `).join('')}
-
-
-
-