From 818d9f3f5d89954f097da66f8c4dab337cb36d72 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Mon, 8 Dec 2025 22:11:14 +0800 Subject: [PATCH] Add enhanced styles for the review tab, including layout, buttons, and responsive design --- ccw/src/core/dashboard-generator.js | 21 +- ccw/src/core/server.js | 21 +- ccw/src/core/server.js.bak | 385 - ccw/src/core/server_original.bak | 385 - ccw/src/templates/dashboard-css/01-base.css | 128 + .../templates/dashboard-css/02-session.css | 726 ++ ccw/src/templates/dashboard-css/03-tasks.css | 512 ++ .../templates/dashboard-css/04-lite-tasks.css | 843 ++ .../templates/dashboard-css/05-context.css | 2206 +++++ ccw/src/templates/dashboard-css/06-cards.css | 1570 ++++ .../templates/dashboard-css/07-managers.css | 936 ++ ccw/src/templates/dashboard-css/08-review.css | 1266 +++ ccw/src/templates/dashboard.css | 8187 ----------------- ccw/src/templates/dashboard.html | 2 +- ccw/src/templates/dashboard_tailwind.html | 42 - ccw/src/templates/dashboard_test.html | 37 - ccw/src/templates/tailwind-base.css | 212 - ccw/src/tools/edit-file.js | 40 +- package.json | 1 + 19 files changed, 8259 insertions(+), 9261 deletions(-) delete mode 100644 ccw/src/core/server.js.bak delete mode 100644 ccw/src/core/server_original.bak create mode 100644 ccw/src/templates/dashboard-css/01-base.css create mode 100644 ccw/src/templates/dashboard-css/02-session.css create mode 100644 ccw/src/templates/dashboard-css/03-tasks.css create mode 100644 ccw/src/templates/dashboard-css/04-lite-tasks.css create mode 100644 ccw/src/templates/dashboard-css/05-context.css create mode 100644 ccw/src/templates/dashboard-css/06-cards.css create mode 100644 ccw/src/templates/dashboard-css/07-managers.css create mode 100644 ccw/src/templates/dashboard-css/08-review.css delete mode 100644 ccw/src/templates/dashboard.css delete mode 100644 ccw/src/templates/dashboard_tailwind.html delete mode 100644 ccw/src/templates/dashboard_test.html delete mode 100644 ccw/src/templates/tailwind-base.css diff --git a/ccw/src/core/dashboard-generator.js b/ccw/src/core/dashboard-generator.js index 4b7e9e06..ff48c73c 100644 --- a/ccw/src/core/dashboard-generator.js +++ b/ccw/src/core/dashboard-generator.js @@ -8,10 +8,22 @@ const __dirname = dirname(__filename); // Bundled template paths const UNIFIED_TEMPLATE = join(__dirname, '../templates/dashboard.html'); const JS_FILE = join(__dirname, '../templates/dashboard.js'); -const CSS_FILE = join(__dirname, '../templates/dashboard.css'); +const MODULE_CSS_DIR = join(__dirname, '../templates/dashboard-css'); const WORKFLOW_TEMPLATE = join(__dirname, '../templates/workflow-dashboard.html'); const REVIEW_TEMPLATE = join(__dirname, '../templates/review-cycle-dashboard.html'); +// Modular CSS files in load order +const MODULE_CSS_FILES = [ + '01-base.css', + '02-session.css', + '03-tasks.css', + '04-lite-tasks.css', + '05-context.css', + '06-cards.css', + '07-managers.css', + '08-review.css' +]; + const MODULE_FILES = [ 'utils.js', 'state.js', @@ -63,8 +75,11 @@ export async function generateDashboard(data) { function generateFromUnifiedTemplate(data) { let html = readFileSync(UNIFIED_TEMPLATE, 'utf8'); - // Read CSS file - let cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : ''; + // Read and concatenate modular CSS files in load order + let cssContent = MODULE_CSS_FILES.map(file => { + const filePath = join(MODULE_CSS_DIR, file); + return existsSync(filePath) ? readFileSync(filePath, 'utf8') : ''; + }).join('\n\n'); // Read JS content let jsContent = ''; diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js index 9545e5b0..5e0d7cb0 100644 --- a/ccw/src/core/server.js +++ b/ccw/src/core/server.js @@ -31,10 +31,22 @@ function getEnterpriseMcpPath() { const wsClients = new Set(); const TEMPLATE_PATH = join(import.meta.dirname, '../templates/dashboard.html'); -const CSS_FILE = join(import.meta.dirname, '../templates/dashboard.css'); +const MODULE_CSS_DIR = join(import.meta.dirname, '../templates/dashboard-css'); const JS_FILE = join(import.meta.dirname, '../templates/dashboard.js'); const MODULE_JS_DIR = join(import.meta.dirname, '../templates/dashboard-js'); +// Modular CSS files in load order +const MODULE_CSS_FILES = [ + '01-base.css', + '02-session.css', + '03-tasks.css', + '04-lite-tasks.css', + '05-context.css', + '06-cards.css', + '07-managers.css', + '08-review.css' +]; + /** * Handle POST request with JSON body */ @@ -965,8 +977,11 @@ async function updateTaskStatus(sessionPath, taskId, newStatus) { function generateServerDashboard(initialPath) { let html = readFileSync(TEMPLATE_PATH, 'utf8'); - // Read CSS file - const cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : ''; + // Read and concatenate modular CSS files in load order + const cssContent = MODULE_CSS_FILES.map(file => { + const filePath = join(MODULE_CSS_DIR, file); + return existsSync(filePath) ? readFileSync(filePath, 'utf8') : ''; + }).join('\n\n'); // Read and concatenate modular JS files in dependency order let jsContent = MODULE_FILES.map(file => { diff --git a/ccw/src/core/server.js.bak b/ccw/src/core/server.js.bak deleted file mode 100644 index eaa5e33a..00000000 --- a/ccw/src/core/server.js.bak +++ /dev/null @@ -1,385 +0,0 @@ -import http from 'http'; -import { URL } from 'url'; -import { readFileSync, existsSync, readdirSync } from 'fs'; -import { join } from 'path'; -import { scanSessions } from './session-scanner.js'; -import { aggregateData } from './data-aggregator.js'; -import { resolvePath, getRecentPaths, trackRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js'; - -const TEMPLATE_PATH = join(import.meta.dirname, '../templates/dashboard.html'); -const CSS_FILE = join(import.meta.dirname, '../templates/dashboard.css'); -const JS_FILE = join(import.meta.dirname, '../templates/dashboard.js'); - -/** - * Create and start the dashboard server - * @param {Object} options - Server options - * @param {number} options.port - Port to listen on (default: 3456) - * @param {string} options.initialPath - Initial project path - * @returns {Promise} - */ -export async function startServer(options = {}) { - const port = options.port || 3456; - const initialPath = options.initialPath || process.cwd(); - - const server = http.createServer(async (req, res) => { - const url = new URL(req.url, `http://localhost:${port}`); - const pathname = url.pathname; - - // CORS headers for API requests - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.writeHead(200); - res.end(); - return; - } - - try { - // API: Get workflow data for a path - if (pathname === '/api/data') { - const projectPath = url.searchParams.get('path') || initialPath; - const data = await getWorkflowData(projectPath); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(data)); - return; - } - - // API: Get recent paths - if (pathname === '/api/recent-paths') { - const paths = getRecentPaths(); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ paths })); - return; - } - - // API: Get session detail data (context, summaries, impl-plan, review) - if (pathname === '/api/session-detail') { - const sessionPath = url.searchParams.get('path'); - const dataType = url.searchParams.get('type') || 'all'; - - if (!sessionPath) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Session path is required' })); - return; - } - - const detail = await getSessionDetailData(sessionPath, dataType); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(detail)); - return; - } - - // Serve dashboard HTML - if (pathname === '/' || pathname === '/index.html') { - const html = generateServerDashboard(initialPath); - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); - res.end(html); - return; - } - - // 404 - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('Not Found'); - - } catch (error) { - console.error('Server error:', error); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: error.message })); - } - }); - - return new Promise((resolve, reject) => { - server.listen(port, () => { - console.log(`Dashboard server running at http://localhost:${port}`); - resolve(server); - }); - server.on('error', reject); - }); -} - -/** - * Get workflow data for a project path - * @param {string} projectPath - * @returns {Promise} - */ -async function getWorkflowData(projectPath) { - const resolvedPath = resolvePath(projectPath); - const workflowDir = join(resolvedPath, '.workflow'); - - // Track this path - trackRecentPath(resolvedPath); - - // Check if .workflow exists - if (!existsSync(workflowDir)) { - return { - generatedAt: new Date().toISOString(), - activeSessions: [], - archivedSessions: [], - liteTasks: { litePlan: [], liteFix: [] }, - reviewData: { dimensions: {} }, - projectOverview: null, - statistics: { - totalSessions: 0, - activeSessions: 0, - totalTasks: 0, - completedTasks: 0, - reviewFindings: 0, - litePlanCount: 0, - liteFixCount: 0 - }, - projectPath: normalizePathForDisplay(resolvedPath), - recentPaths: getRecentPaths() - }; - } - - // Scan and aggregate data - const sessions = await scanSessions(workflowDir); - const data = await aggregateData(sessions, workflowDir); - - data.projectPath = normalizePathForDisplay(resolvedPath); - data.recentPaths = getRecentPaths(); - - return data; -} - -/** - * Get session detail data (context, summaries, impl-plan, review) - * @param {string} sessionPath - Path to session directory - * @param {string} dataType - Type of data to load: context, summary, impl-plan, review, or all - * @returns {Promise} - */ -async function getSessionDetailData(sessionPath, dataType) { - const result = {}; - - // Normalize path - const normalizedPath = sessionPath.replace(/\\/g, '/'); - - try { - // Load context-package.json (in .process/ subfolder) - if (dataType === 'context' || dataType === 'all') { - // Try .process/context-package.json first (common location) - let contextFile = join(normalizedPath, '.process', 'context-package.json'); - if (!existsSync(contextFile)) { - // Fallback to session root - contextFile = join(normalizedPath, 'context-package.json'); - } - if (existsSync(contextFile)) { - try { - result.context = JSON.parse(readFileSync(contextFile, 'utf8')); - } catch (e) { - result.context = null; - } - } - } - - // Load task JSONs from .task/ folder - if (dataType === 'tasks' || dataType === 'all') { - const taskDir = join(normalizedPath, '.task'); - result.tasks = []; - if (existsSync(taskDir)) { - const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f.startsWith('IMPL-')); - for (const file of files) { - try { - const content = JSON.parse(readFileSync(join(taskDir, file), 'utf8')); - result.tasks.push({ - filename: file, - task_id: file.replace('.json', ''), - ...content - }); - } catch (e) { - // Skip unreadable files - } - } - // Sort by task ID - result.tasks.sort((a, b) => a.task_id.localeCompare(b.task_id)); - } - } - - // Load summaries from .summaries/ - if (dataType === 'summary' || dataType === 'all') { - const summariesDir = join(normalizedPath, '.summaries'); - result.summaries = []; - if (existsSync(summariesDir)) { - const files = readdirSync(summariesDir).filter(f => f.endsWith('.md')); - for (const file of files) { - try { - const content = readFileSync(join(summariesDir, file), 'utf8'); - result.summaries.push({ name: file.replace('.md', ''), content }); - } catch (e) { - // Skip unreadable files - } - } - } - } - - // Load plan.json (for lite tasks) - if (dataType === 'plan' || dataType === 'all') { - const planFile = join(normalizedPath, 'plan.json'); - if (existsSync(planFile)) { - try { - result.plan = JSON.parse(readFileSync(planFile, 'utf8')); - } catch (e) { - result.plan = null; - } - } - } - - // Load IMPL_PLAN.md - if (dataType === 'impl-plan' || dataType === 'all') { - const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md'); - if (existsSync(implPlanFile)) { - try { - result.implPlan = readFileSync(implPlanFile, 'utf8'); - } catch (e) { - result.implPlan = null; - } - } - } - - // Load review data from .review/ - if (dataType === 'review' || dataType === 'all') { - const reviewDir = join(normalizedPath, '.review'); - result.review = { - state: null, - dimensions: [], - severityDistribution: null, - totalFindings: 0 - }; - - if (existsSync(reviewDir)) { - // Load review-state.json - const stateFile = join(reviewDir, 'review-state.json'); - if (existsSync(stateFile)) { - try { - const state = JSON.parse(readFileSync(stateFile, 'utf8')); - result.review.state = state; - result.review.severityDistribution = state.severity_distribution || {}; - result.review.totalFindings = state.total_findings || 0; - result.review.phase = state.phase || 'unknown'; - result.review.dimensionSummaries = state.dimension_summaries || {}; - result.review.crossCuttingConcerns = state.cross_cutting_concerns || []; - result.review.criticalFiles = state.critical_files || []; - } catch (e) { - // Skip unreadable state - } - } - - // Load dimension findings - const dimensionsDir = join(reviewDir, 'dimensions'); - if (existsSync(dimensionsDir)) { - const files = readdirSync(dimensionsDir).filter(f => f.endsWith('.json')); - for (const file of files) { - try { - const dimName = file.replace('.json', ''); - const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8')); - - // Handle array structure: [ { findings: [...] } ] - let findings = []; - let summary = null; - - if (Array.isArray(data) && data.length > 0) { - const dimData = data[0]; - findings = dimData.findings || []; - summary = dimData.summary || null; - } else if (data.findings) { - findings = data.findings; - summary = data.summary || null; - } - - result.review.dimensions.push({ - name: dimName, - findings: findings, - summary: summary, - count: findings.length - }); - } catch (e) { - // Skip unreadable files - } - } - } - } - } - - } catch (error) { - console.error('Error loading session detail:', error); - result.error = error.message; - } - - return result; -} - -/** - * Generate dashboard HTML for server mode - * @param {string} initialPath - * @returns {string} - */ -function generateServerDashboard(initialPath) { - let html = readFileSync(TEMPLATE_PATH, 'utf8'); - - // Read CSS and JS files - const cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : ''; - let jsContent = existsSync(JS_FILE) ? readFileSync(JS_FILE, 'utf8') : ''; - - // Inject CSS content - html = html.replace('{{CSS_CONTENT}}', cssContent); - - // Prepare JS content with empty initial data (will be loaded dynamically) - const emptyData = { - generatedAt: new Date().toISOString(), - activeSessions: [], - archivedSessions: [], - liteTasks: { litePlan: [], liteFix: [] }, - reviewData: { dimensions: {} }, - projectOverview: null, - statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0 } - }; - - // Replace JS placeholders - jsContent = jsContent.replace('{{WORKFLOW_DATA}}', JSON.stringify(emptyData, null, 2)); - jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/')); - jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(getRecentPaths())); - - // Add server mode flag and dynamic loading functions at the start of JS - const serverModeScript = ` -// Server mode - load data dynamically -window.SERVER_MODE = true; -window.INITIAL_PATH = '${normalizePathForDisplay(initialPath).replace(/\\/g, '/')}'; - -async function loadDashboardData(path) { - try { - const res = await fetch('/api/data?path=' + encodeURIComponent(path)); - if (!res.ok) throw new Error('Failed to load data'); - return await res.json(); - } catch (err) { - console.error('Error loading data:', err); - return null; - } -} - -async function loadRecentPaths() { - try { - const res = await fetch('/api/recent-paths'); - if (!res.ok) return []; - const data = await res.json(); - return data.paths || []; - } catch (err) { - return []; - } -} - -`; - - // Prepend server mode script to JS content - jsContent = serverModeScript + jsContent; - - // Inject JS content - html = html.replace('{{JS_CONTENT}}', jsContent); - - // Replace any remaining placeholders in HTML - html = html.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/')); - - return html; -} diff --git a/ccw/src/core/server_original.bak b/ccw/src/core/server_original.bak deleted file mode 100644 index eaa5e33a..00000000 --- a/ccw/src/core/server_original.bak +++ /dev/null @@ -1,385 +0,0 @@ -import http from 'http'; -import { URL } from 'url'; -import { readFileSync, existsSync, readdirSync } from 'fs'; -import { join } from 'path'; -import { scanSessions } from './session-scanner.js'; -import { aggregateData } from './data-aggregator.js'; -import { resolvePath, getRecentPaths, trackRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js'; - -const TEMPLATE_PATH = join(import.meta.dirname, '../templates/dashboard.html'); -const CSS_FILE = join(import.meta.dirname, '../templates/dashboard.css'); -const JS_FILE = join(import.meta.dirname, '../templates/dashboard.js'); - -/** - * Create and start the dashboard server - * @param {Object} options - Server options - * @param {number} options.port - Port to listen on (default: 3456) - * @param {string} options.initialPath - Initial project path - * @returns {Promise} - */ -export async function startServer(options = {}) { - const port = options.port || 3456; - const initialPath = options.initialPath || process.cwd(); - - const server = http.createServer(async (req, res) => { - const url = new URL(req.url, `http://localhost:${port}`); - const pathname = url.pathname; - - // CORS headers for API requests - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.writeHead(200); - res.end(); - return; - } - - try { - // API: Get workflow data for a path - if (pathname === '/api/data') { - const projectPath = url.searchParams.get('path') || initialPath; - const data = await getWorkflowData(projectPath); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(data)); - return; - } - - // API: Get recent paths - if (pathname === '/api/recent-paths') { - const paths = getRecentPaths(); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ paths })); - return; - } - - // API: Get session detail data (context, summaries, impl-plan, review) - if (pathname === '/api/session-detail') { - const sessionPath = url.searchParams.get('path'); - const dataType = url.searchParams.get('type') || 'all'; - - if (!sessionPath) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Session path is required' })); - return; - } - - const detail = await getSessionDetailData(sessionPath, dataType); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(detail)); - return; - } - - // Serve dashboard HTML - if (pathname === '/' || pathname === '/index.html') { - const html = generateServerDashboard(initialPath); - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); - res.end(html); - return; - } - - // 404 - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('Not Found'); - - } catch (error) { - console.error('Server error:', error); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: error.message })); - } - }); - - return new Promise((resolve, reject) => { - server.listen(port, () => { - console.log(`Dashboard server running at http://localhost:${port}`); - resolve(server); - }); - server.on('error', reject); - }); -} - -/** - * Get workflow data for a project path - * @param {string} projectPath - * @returns {Promise} - */ -async function getWorkflowData(projectPath) { - const resolvedPath = resolvePath(projectPath); - const workflowDir = join(resolvedPath, '.workflow'); - - // Track this path - trackRecentPath(resolvedPath); - - // Check if .workflow exists - if (!existsSync(workflowDir)) { - return { - generatedAt: new Date().toISOString(), - activeSessions: [], - archivedSessions: [], - liteTasks: { litePlan: [], liteFix: [] }, - reviewData: { dimensions: {} }, - projectOverview: null, - statistics: { - totalSessions: 0, - activeSessions: 0, - totalTasks: 0, - completedTasks: 0, - reviewFindings: 0, - litePlanCount: 0, - liteFixCount: 0 - }, - projectPath: normalizePathForDisplay(resolvedPath), - recentPaths: getRecentPaths() - }; - } - - // Scan and aggregate data - const sessions = await scanSessions(workflowDir); - const data = await aggregateData(sessions, workflowDir); - - data.projectPath = normalizePathForDisplay(resolvedPath); - data.recentPaths = getRecentPaths(); - - return data; -} - -/** - * Get session detail data (context, summaries, impl-plan, review) - * @param {string} sessionPath - Path to session directory - * @param {string} dataType - Type of data to load: context, summary, impl-plan, review, or all - * @returns {Promise} - */ -async function getSessionDetailData(sessionPath, dataType) { - const result = {}; - - // Normalize path - const normalizedPath = sessionPath.replace(/\\/g, '/'); - - try { - // Load context-package.json (in .process/ subfolder) - if (dataType === 'context' || dataType === 'all') { - // Try .process/context-package.json first (common location) - let contextFile = join(normalizedPath, '.process', 'context-package.json'); - if (!existsSync(contextFile)) { - // Fallback to session root - contextFile = join(normalizedPath, 'context-package.json'); - } - if (existsSync(contextFile)) { - try { - result.context = JSON.parse(readFileSync(contextFile, 'utf8')); - } catch (e) { - result.context = null; - } - } - } - - // Load task JSONs from .task/ folder - if (dataType === 'tasks' || dataType === 'all') { - const taskDir = join(normalizedPath, '.task'); - result.tasks = []; - if (existsSync(taskDir)) { - const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f.startsWith('IMPL-')); - for (const file of files) { - try { - const content = JSON.parse(readFileSync(join(taskDir, file), 'utf8')); - result.tasks.push({ - filename: file, - task_id: file.replace('.json', ''), - ...content - }); - } catch (e) { - // Skip unreadable files - } - } - // Sort by task ID - result.tasks.sort((a, b) => a.task_id.localeCompare(b.task_id)); - } - } - - // Load summaries from .summaries/ - if (dataType === 'summary' || dataType === 'all') { - const summariesDir = join(normalizedPath, '.summaries'); - result.summaries = []; - if (existsSync(summariesDir)) { - const files = readdirSync(summariesDir).filter(f => f.endsWith('.md')); - for (const file of files) { - try { - const content = readFileSync(join(summariesDir, file), 'utf8'); - result.summaries.push({ name: file.replace('.md', ''), content }); - } catch (e) { - // Skip unreadable files - } - } - } - } - - // Load plan.json (for lite tasks) - if (dataType === 'plan' || dataType === 'all') { - const planFile = join(normalizedPath, 'plan.json'); - if (existsSync(planFile)) { - try { - result.plan = JSON.parse(readFileSync(planFile, 'utf8')); - } catch (e) { - result.plan = null; - } - } - } - - // Load IMPL_PLAN.md - if (dataType === 'impl-plan' || dataType === 'all') { - const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md'); - if (existsSync(implPlanFile)) { - try { - result.implPlan = readFileSync(implPlanFile, 'utf8'); - } catch (e) { - result.implPlan = null; - } - } - } - - // Load review data from .review/ - if (dataType === 'review' || dataType === 'all') { - const reviewDir = join(normalizedPath, '.review'); - result.review = { - state: null, - dimensions: [], - severityDistribution: null, - totalFindings: 0 - }; - - if (existsSync(reviewDir)) { - // Load review-state.json - const stateFile = join(reviewDir, 'review-state.json'); - if (existsSync(stateFile)) { - try { - const state = JSON.parse(readFileSync(stateFile, 'utf8')); - result.review.state = state; - result.review.severityDistribution = state.severity_distribution || {}; - result.review.totalFindings = state.total_findings || 0; - result.review.phase = state.phase || 'unknown'; - result.review.dimensionSummaries = state.dimension_summaries || {}; - result.review.crossCuttingConcerns = state.cross_cutting_concerns || []; - result.review.criticalFiles = state.critical_files || []; - } catch (e) { - // Skip unreadable state - } - } - - // Load dimension findings - const dimensionsDir = join(reviewDir, 'dimensions'); - if (existsSync(dimensionsDir)) { - const files = readdirSync(dimensionsDir).filter(f => f.endsWith('.json')); - for (const file of files) { - try { - const dimName = file.replace('.json', ''); - const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8')); - - // Handle array structure: [ { findings: [...] } ] - let findings = []; - let summary = null; - - if (Array.isArray(data) && data.length > 0) { - const dimData = data[0]; - findings = dimData.findings || []; - summary = dimData.summary || null; - } else if (data.findings) { - findings = data.findings; - summary = data.summary || null; - } - - result.review.dimensions.push({ - name: dimName, - findings: findings, - summary: summary, - count: findings.length - }); - } catch (e) { - // Skip unreadable files - } - } - } - } - } - - } catch (error) { - console.error('Error loading session detail:', error); - result.error = error.message; - } - - return result; -} - -/** - * Generate dashboard HTML for server mode - * @param {string} initialPath - * @returns {string} - */ -function generateServerDashboard(initialPath) { - let html = readFileSync(TEMPLATE_PATH, 'utf8'); - - // Read CSS and JS files - const cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : ''; - let jsContent = existsSync(JS_FILE) ? readFileSync(JS_FILE, 'utf8') : ''; - - // Inject CSS content - html = html.replace('{{CSS_CONTENT}}', cssContent); - - // Prepare JS content with empty initial data (will be loaded dynamically) - const emptyData = { - generatedAt: new Date().toISOString(), - activeSessions: [], - archivedSessions: [], - liteTasks: { litePlan: [], liteFix: [] }, - reviewData: { dimensions: {} }, - projectOverview: null, - statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0 } - }; - - // Replace JS placeholders - jsContent = jsContent.replace('{{WORKFLOW_DATA}}', JSON.stringify(emptyData, null, 2)); - jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/')); - jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(getRecentPaths())); - - // Add server mode flag and dynamic loading functions at the start of JS - const serverModeScript = ` -// Server mode - load data dynamically -window.SERVER_MODE = true; -window.INITIAL_PATH = '${normalizePathForDisplay(initialPath).replace(/\\/g, '/')}'; - -async function loadDashboardData(path) { - try { - const res = await fetch('/api/data?path=' + encodeURIComponent(path)); - if (!res.ok) throw new Error('Failed to load data'); - return await res.json(); - } catch (err) { - console.error('Error loading data:', err); - return null; - } -} - -async function loadRecentPaths() { - try { - const res = await fetch('/api/recent-paths'); - if (!res.ok) return []; - const data = await res.json(); - return data.paths || []; - } catch (err) { - return []; - } -} - -`; - - // Prepend server mode script to JS content - jsContent = serverModeScript + jsContent; - - // Inject JS content - html = html.replace('{{JS_CONTENT}}', jsContent); - - // Replace any remaining placeholders in HTML - html = html.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/')); - - return html; -} diff --git a/ccw/src/templates/dashboard-css/01-base.css b/ccw/src/templates/dashboard-css/01-base.css new file mode 100644 index 00000000..d35f139d --- /dev/null +++ b/ccw/src/templates/dashboard-css/01-base.css @@ -0,0 +1,128 @@ +/* =================================== + Dashboard - Complementary Styles + ================================== */ + +/* This file contains only essential CSS that cannot be achieved + with Tailwind utilities. All layout, colors, and basic styling + are handled by Tailwind classes in dashboard.html. + + CSS variables are defined inline in dashboard.html diff --git a/ccw/src/templates/dashboard_tailwind.html b/ccw/src/templates/dashboard_tailwind.html deleted file mode 100644 index 4a2a60a6..00000000 --- a/ccw/src/templates/dashboard_tailwind.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - CCW Dashboard - - - - - - - - -
-
-
- - Claude Code Workflow -
-
-
-

Dashboard

-
-
- - - \ No newline at end of file diff --git a/ccw/src/templates/dashboard_test.html b/ccw/src/templates/dashboard_test.html deleted file mode 100644 index e894f6fa..00000000 --- a/ccw/src/templates/dashboard_test.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - CCW Dashboard - - - - - - - -Test - \ No newline at end of file diff --git a/ccw/src/templates/tailwind-base.css b/ccw/src/templates/tailwind-base.css deleted file mode 100644 index 1b3fcdc1..00000000 --- a/ccw/src/templates/tailwind-base.css +++ /dev/null @@ -1,212 +0,0 @@ -/* Tailwind Base Styles with Design Tokens */ - -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - /* CSS Custom Properties - Light Mode (Default) */ - :root { - /* Base Colors */ - --color-background: 0 0% 100%; /* oklch(1 0 0) -> white */ - --color-foreground: 0 0% 25%; /* oklch(0.25 0 0) -> dark gray */ - --color-card: 0 0% 100%; /* oklch(1 0 0) -> white */ - --color-card-foreground: 0 0% 25%; /* oklch(0.25 0 0) -> dark gray */ - --color-border: 0 0% 90%; /* oklch(0.9 0 0) -> light gray */ - --color-input: 0 0% 90%; /* oklch(0.9 0 0) -> light gray */ - --color-ring: 220 65% 50%; /* oklch(0.5 0.15 250) -> primary blue */ - - /* Interactive Colors - Primary */ - --color-interactive-primary-default: 220 65% 50%; /* oklch(0.5 0.15 250) -> #4066bf */ - --color-interactive-primary-hover: 220 65% 55%; /* oklch(0.55 0.15 250) -> lighter blue */ - --color-interactive-primary-active: 220 65% 45%; /* oklch(0.45 0.15 250) -> darker blue */ - --color-interactive-primary-disabled: 220 30% 70%; /* oklch(0.7 0.05 250) -> muted blue */ - --color-interactive-primary-foreground: 0 0% 100%; /* oklch(1 0 0) -> white */ - - /* Interactive Colors - Secondary */ - --color-interactive-secondary-default: 220 60% 65%; /* oklch(0.65 0.12 250) -> #6b8ccc */ - --color-interactive-secondary-hover: 220 60% 70%; /* oklch(0.7 0.12 250) -> lighter */ - --color-interactive-secondary-active: 220 60% 60%; /* oklch(0.6 0.12 250) -> darker */ - --color-interactive-secondary-disabled: 220 30% 80%; /* oklch(0.8 0.05 250) -> muted */ - --color-interactive-secondary-foreground: 0 0% 100%; /* oklch(1 0 0) -> white */ - - /* Interactive Colors - Accent */ - --color-interactive-accent-default: 220 40% 95%; /* oklch(0.95 0.02 250) -> #eef3fa */ - --color-interactive-accent-hover: 220 45% 92%; /* oklch(0.92 0.03 250) -> slightly darker */ - --color-interactive-accent-active: 220 35% 97%; /* oklch(0.97 0.02 250) -> slightly lighter */ - --color-interactive-accent-foreground: 0 0% 25%; /* oklch(0.25 0 0) -> dark gray */ - - /* Interactive Colors - Destructive */ - --color-interactive-destructive-default: 8 75% 55%; /* oklch(0.55 0.22 25) -> #d93025 */ - --color-interactive-destructive-hover: 8 75% 60%; /* oklch(0.6 0.22 25) -> lighter red */ - --color-interactive-destructive-foreground: 0 0% 100%; /* oklch(1 0 0) -> white */ - - /* Semantic Colors */ - --color-muted: 0 0% 97%; /* oklch(0.97 0 0) -> very light gray */ - --color-muted-foreground: 0 0% 50%; /* oklch(0.5 0 0) -> medium gray */ - - /* Sidebar Colors */ - --color-sidebar-background: 0 0% 97.5%; /* oklch(0.975 0 0) -> #f8f8f8 */ - --color-sidebar-foreground: 0 0% 25%; /* oklch(0.25 0 0) -> dark gray */ - --color-sidebar-primary: 220 65% 50%; /* oklch(0.5 0.15 250) -> primary blue */ - --color-sidebar-primary-foreground: 0 0% 100%; /* oklch(1 0 0) -> white */ - --color-sidebar-accent: 220 40% 95%; /* oklch(0.95 0.02 250) -> light blue */ - --color-sidebar-accent-foreground: 0 0% 25%; /* oklch(0.25 0 0) -> dark gray */ - --color-sidebar-border: 0 0% 90%; /* oklch(0.9 0 0) -> light gray */ - - /* Typography */ - --font-sans: 'Inter', system-ui, -apple-system, sans-serif; - --font-mono: 'Consolas', 'Monaco', 'Courier New', monospace; - - --font-size-xs: 0.75rem; /* 12px */ - --font-size-sm: 0.875rem; /* 14px */ - --font-size-base: 1rem; /* 16px */ - --font-size-lg: 1.125rem; /* 18px */ - --font-size-xl: 1.25rem; /* 20px */ - --font-size-2xl: 1.5rem; /* 24px */ - --font-size-3xl: 1.875rem; /* 30px */ - --font-size-4xl: 2.25rem; /* 36px */ - - --line-height-tight: 1.25; - --line-height-normal: 1.5; - --line-height-relaxed: 1.75; - - --letter-spacing-tight: -0.025em; - --letter-spacing-normal: 0; - --letter-spacing-wide: 0.025em; - - /* Spacing */ - --spacing-0: 0; - --spacing-1: 0.25rem; /* 4px */ - --spacing-2: 0.5rem; /* 8px */ - --spacing-3: 0.75rem; /* 12px */ - --spacing-4: 1rem; /* 16px */ - --spacing-6: 1.5rem; /* 24px */ - --spacing-8: 2rem; /* 32px */ - --spacing-12: 3rem; /* 48px */ - --spacing-16: 4rem; /* 64px */ - - /* Effects */ - --opacity-disabled: 0.5; - --opacity-hover: 0.8; - --opacity-active: 1; - - /* Shadows */ - --shadow-2xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-xs: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-sm: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-md: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --shadow-lg: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - --shadow-xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); - - /* Border Radius */ - --border-radius-sm: calc(0.375rem - 4px); /* 2px */ - --border-radius-md: calc(0.375rem - 2px); /* 4px */ - --border-radius-lg: 0.375rem; /* 6px */ - --border-radius-xl: calc(0.375rem + 4px); /* 10px */ - --border-radius-default: 0.375rem; /* 6px */ - - /* Animations */ - --duration-instant: 0ms; - --duration-fast: 150ms; - --duration-normal: 200ms; - --duration-medium: 300ms; - --duration-slow: 500ms; - - --easing-linear: linear; - --easing-ease-in: cubic-bezier(0.4, 0, 1, 1); - --easing-ease-out: cubic-bezier(0, 0, 0.2, 1); - --easing-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); - --easing-spring: cubic-bezier(0.68, -0.55, 0.265, 1.55); - } - - /* Dark Mode Theme */ - [data-theme="dark"] { - /* Base Colors - Dark Mode */ - --color-background: 0 0% 10%; /* Dark background */ - --color-foreground: 0 0% 90%; /* Light text */ - --color-card: 0 0% 15%; /* Dark card background */ - --color-card-foreground: 0 0% 90%; /* Light card text */ - --color-border: 0 0% 25%; /* Dark border */ - --color-input: 0 0% 25%; /* Dark input border */ - --color-ring: 220 65% 60%; /* Brighter ring for dark mode */ - - /* Interactive Colors - Primary (Dark Mode) */ - --color-interactive-primary-default: 220 70% 60%; /* Brighter blue for dark mode */ - --color-interactive-primary-hover: 220 70% 65%; /* Even brighter on hover */ - --color-interactive-primary-active: 220 70% 55%; /* Slightly darker on active */ - --color-interactive-primary-disabled: 220 30% 40%; /* Muted blue for dark mode */ - --color-interactive-primary-foreground: 0 0% 100%; /* White text */ - - /* Interactive Colors - Secondary (Dark Mode) */ - --color-interactive-secondary-default: 220 60% 70%; /* Brighter secondary */ - --color-interactive-secondary-hover: 220 60% 75%; /* Brighter on hover */ - --color-interactive-secondary-active: 220 60% 65%; /* Slightly darker on active */ - --color-interactive-secondary-disabled: 220 30% 50%; /* Muted */ - --color-interactive-secondary-foreground: 0 0% 100%; /* White text */ - - /* Interactive Colors - Accent (Dark Mode) */ - --color-interactive-accent-default: 220 30% 25%; /* Dark accent */ - --color-interactive-accent-hover: 220 35% 30%; /* Slightly lighter on hover */ - --color-interactive-accent-active: 220 25% 20%; /* Darker on active */ - --color-interactive-accent-foreground: 0 0% 90%; /* Light text */ - - /* Interactive Colors - Destructive (Dark Mode) */ - --color-interactive-destructive-default: 8 75% 60%; /* Brighter red for visibility */ - --color-interactive-destructive-hover: 8 75% 65%; /* Even brighter on hover */ - --color-interactive-destructive-foreground: 0 0% 100%; /* White text */ - - /* Semantic Colors (Dark Mode) */ - --color-muted: 0 0% 20%; /* Dark muted background */ - --color-muted-foreground: 0 0% 60%; /* Lighter muted text */ - - /* Sidebar Colors (Dark Mode) */ - --color-sidebar-background: 0 0% 12%; /* Slightly lighter than background */ - --color-sidebar-foreground: 0 0% 90%; /* Light text */ - --color-sidebar-primary: 220 70% 60%; /* Brighter blue */ - --color-sidebar-primary-foreground: 0 0% 100%; /* White text */ - --color-sidebar-accent: 220 30% 25%; /* Dark accent */ - --color-sidebar-accent-foreground: 0 0% 90%; /* Light text */ - --color-sidebar-border: 0 0% 25%; /* Dark border */ - } - - /* Base typography */ - * { - box-sizing: border-box; - } - - body { - @apply bg-background text-foreground font-sans leading-normal; - margin: 0; - padding: 0; - } - - /* Focus styles */ - *:focus-visible { - outline: 2px solid hsl(var(--color-ring)); - outline-offset: 2px; - } -} - -@layer utilities { - /* Custom utility classes */ - .text-balance { - text-wrap: balance; - } - - .transition-default { - transition: all var(--duration-normal) var(--easing-ease-out); - } - - .transition-fast { - transition: all var(--duration-fast) var(--easing-ease-out); - } - - .transition-medium { - transition: all var(--duration-medium) var(--easing-ease-in-out); - } - - .transition-slow { - transition: all var(--duration-slow) var(--easing-ease-in-out); - } -} diff --git a/ccw/src/tools/edit-file.js b/ccw/src/tools/edit-file.js index 8480390f..8f440308 100644 --- a/ccw/src/tools/edit-file.js +++ b/ccw/src/tools/edit-file.js @@ -46,7 +46,7 @@ function writeFile(filePath, content) { * Auto-adapts line endings (CRLF/LF) */ function executeUpdateMode(content, params) { - const { oldText, newText } = params; + const { oldText, newText, replaceAll } = params; if (!oldText) throw new Error('Parameter "oldText" is required for update mode'); if (newText === undefined) throw new Error('Parameter "newText" is required for update mode'); @@ -62,10 +62,19 @@ function executeUpdateMode(content, params) { let newContent = normalizedContent; let status = 'not found'; + let replacements = 0; if (newContent.includes(normalizedOld)) { - newContent = newContent.replace(normalizedOld, normalizedNew); - status = 'replaced'; + if (replaceAll) { + const parts = newContent.split(normalizedOld); + replacements = parts.length - 1; + newContent = parts.join(normalizedNew); + status = 'replaced_all'; + } else { + newContent = newContent.replace(normalizedOld, normalizedNew); + status = 'replaced'; + replacements = 1; + } } // Restore original line ending @@ -77,7 +86,13 @@ function executeUpdateMode(content, params) { content: newContent, modified: content !== newContent, status, - message: status === 'replaced' ? 'Text replaced successfully' : 'oldText not found in file' + replacements, + message: + status === 'replaced_all' + ? `Text replaced successfully (${replacements} occurrences)` + : status === 'replaced' + ? 'Text replaced successfully' + : 'oldText not found in file' }; } @@ -91,7 +106,11 @@ function executeLineMode(content, params) { if (!operation) throw new Error('Parameter "operation" is required for line mode'); if (line === undefined) throw new Error('Parameter "line" is required for line mode'); - const lines = content.split('\n'); + // Detect original line ending and normalize for processing + const hasCRLF = content.includes('\r\n'); + const normalizedContent = hasCRLF ? content.replace(/\r\n/g, '\n') : content; + + const lines = normalizedContent.split('\n'); const lineIndex = line - 1; // Convert to 0-based if (lineIndex < 0 || lineIndex >= lines.length) { @@ -139,7 +158,12 @@ function executeLineMode(content, params) { throw new Error(`Unknown operation: ${operation}. Valid: insert_before, insert_after, replace, delete`); } - const newContent = newLines.join('\n'); + let newContent = newLines.join('\n'); + + // Restore original line endings + if (hasCRLF) { + newContent = newContent.replace(/\n/g, '\r\n'); + } return { content: newContent, @@ -213,6 +237,10 @@ export const editFileTool = { type: 'string', description: '[update mode] Replacement text' }, + replaceAll: { + type: 'boolean', + description: '[update mode] Replace all occurrences of oldText (default: false)' + }, // Line mode params operation: { type: 'string', diff --git a/package.json b/package.json index ef564c45..07158a14 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "ccw-test" "name": "claude-code-workflow", "version": "6.0.5", "description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",