diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js index 5e0d7cb0..38f123f2 100644 --- a/ccw/src/core/server.js +++ b/ccw/src/core/server.js @@ -1,6 +1,6 @@ import http from 'http'; import { URL } from 'url'; -import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, promises as fsPromises } from 'fs'; +import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, statSync, promises as fsPromises } from 'fs'; import { join, dirname } from 'path'; import { homedir } from 'os'; import { createHash } from 'crypto'; @@ -44,7 +44,8 @@ const MODULE_CSS_FILES = [ '05-context.css', '06-cards.css', '07-managers.css', - '08-review.css' + '08-review.css', + '09-explorer.css' ]; /** @@ -84,6 +85,7 @@ const MODULE_FILES = [ 'components/sidebar.js', 'components/carousel.js', 'components/notifications.js', + 'components/global-notifications.js', 'components/mcp-manager.js', 'components/hook-manager.js', 'components/_exp_helpers.js', @@ -102,6 +104,7 @@ const MODULE_FILES = [ 'views/fix-session.js', 'views/mcp-manager.js', 'views/hook-manager.js', + 'views/explorer.js', 'main.js' ]; /** @@ -399,6 +402,41 @@ export async function startServer(options = {}) { return; } + // API: List directory files with .gitignore filtering (Explorer view) + if (pathname === '/api/files') { + const dirPath = url.searchParams.get('path') || initialPath; + const filesData = await listDirectoryFiles(dirPath); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(filesData)); + return; + } + + // API: Get file content for preview (Explorer view) + if (pathname === '/api/file-content') { + const filePath = url.searchParams.get('path'); + if (!filePath) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'File path is required' })); + return; + } + const fileData = await getFileContent(filePath); + res.writeHead(fileData.error ? 404 : 200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(fileData)); + return; + } + + // API: Update CLAUDE.md using CLI tools (Explorer view) + if (pathname === '/api/update-claude-md' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { path: targetPath, tool = 'gemini', strategy = 'single-layer' } = body; + if (!targetPath) { + return { error: 'path is required', status: 400 }; + } + return await triggerUpdateClaudeMd(targetPath, tool, strategy); + }); + return; + } + // Serve dashboard HTML if (pathname === '/' || pathname === '/index.html') { const html = generateServerDashboard(initialPath); @@ -1521,3 +1559,305 @@ function deleteHookFromSettings(projectPath, scope, event, hookIndex) { return { error: error.message }; } } + +// ======================================== +// Explorer View Functions +// ======================================== + +// Directories to always exclude from file tree +const EXPLORER_EXCLUDE_DIRS = [ + '.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env', + 'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache', + 'coverage', '.nyc_output', 'logs', 'tmp', 'temp', '.next', + '.nuxt', '.output', '.turbo', '.parcel-cache' +]; + +// File extensions to language mapping for syntax highlighting +const EXT_TO_LANGUAGE = { + '.js': 'javascript', + '.jsx': 'javascript', + '.ts': 'typescript', + '.tsx': 'typescript', + '.py': 'python', + '.rb': 'ruby', + '.java': 'java', + '.go': 'go', + '.rs': 'rust', + '.c': 'c', + '.cpp': 'cpp', + '.h': 'c', + '.hpp': 'cpp', + '.cs': 'csharp', + '.php': 'php', + '.swift': 'swift', + '.kt': 'kotlin', + '.scala': 'scala', + '.sh': 'bash', + '.bash': 'bash', + '.zsh': 'bash', + '.ps1': 'powershell', + '.sql': 'sql', + '.html': 'html', + '.htm': 'html', + '.css': 'css', + '.scss': 'scss', + '.sass': 'sass', + '.less': 'less', + '.json': 'json', + '.xml': 'xml', + '.yaml': 'yaml', + '.yml': 'yaml', + '.toml': 'toml', + '.ini': 'ini', + '.cfg': 'ini', + '.conf': 'nginx', + '.md': 'markdown', + '.markdown': 'markdown', + '.txt': 'plaintext', + '.log': 'plaintext', + '.env': 'bash', + '.dockerfile': 'dockerfile', + '.vue': 'html', + '.svelte': 'html' +}; + +/** + * Parse .gitignore file and return patterns + * @param {string} gitignorePath - Path to .gitignore file + * @returns {string[]} Array of gitignore patterns + */ +function parseGitignore(gitignorePath) { + try { + if (!existsSync(gitignorePath)) return []; + const content = readFileSync(gitignorePath, 'utf8'); + return content + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + } catch { + return []; + } +} + +/** + * Check if a file/directory should be ignored based on gitignore patterns + * Simple pattern matching (supports basic glob patterns) + * @param {string} name - File or directory name + * @param {string[]} patterns - Gitignore patterns + * @param {boolean} isDirectory - Whether the entry is a directory + * @returns {boolean} + */ +function shouldIgnore(name, patterns, isDirectory) { + // Always exclude certain directories + if (isDirectory && EXPLORER_EXCLUDE_DIRS.includes(name)) { + return true; + } + + // Skip hidden files/directories (starting with .) + if (name.startsWith('.') && name !== '.claude' && name !== '.workflow') { + return true; + } + + for (const pattern of patterns) { + let p = pattern; + + // Handle negation patterns (we skip them for simplicity) + if (p.startsWith('!')) continue; + + // Handle directory-only patterns + if (p.endsWith('/')) { + if (!isDirectory) continue; + p = p.slice(0, -1); + } + + // Simple pattern matching + if (p === name) return true; + + // Handle wildcard patterns + if (p.includes('*')) { + const regex = new RegExp('^' + p.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); + if (regex.test(name)) return true; + } + + // Handle extension patterns like *.log + if (p.startsWith('*.')) { + const ext = p.slice(1); + if (name.endsWith(ext)) return true; + } + } + + return false; +} + +/** + * List directory files with .gitignore filtering + * @param {string} dirPath - Directory path to list + * @returns {Promise} + */ +async function listDirectoryFiles(dirPath) { + try { + // Normalize path + let normalizedPath = dirPath.replace(/\\/g, '/'); + if (normalizedPath.match(/^\/[a-zA-Z]\//)) { + normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2); + } + + if (!existsSync(normalizedPath)) { + return { error: 'Directory not found', files: [] }; + } + + if (!statSync(normalizedPath).isDirectory()) { + return { error: 'Not a directory', files: [] }; + } + + // Parse .gitignore patterns + const gitignorePath = join(normalizedPath, '.gitignore'); + const gitignorePatterns = parseGitignore(gitignorePath); + + // Read directory entries + const entries = readdirSync(normalizedPath, { withFileTypes: true }); + + const files = []; + for (const entry of entries) { + const isDirectory = entry.isDirectory(); + + // Check if should be ignored + if (shouldIgnore(entry.name, gitignorePatterns, isDirectory)) { + continue; + } + + const entryPath = join(normalizedPath, entry.name); + const fileInfo = { + name: entry.name, + type: isDirectory ? 'directory' : 'file', + path: entryPath.replace(/\\/g, '/') + }; + + // Check if directory has CLAUDE.md + if (isDirectory) { + const claudeMdPath = join(entryPath, 'CLAUDE.md'); + fileInfo.hasClaudeMd = existsSync(claudeMdPath); + } + + files.push(fileInfo); + } + + // Sort: directories first, then alphabetically + files.sort((a, b) => { + if (a.type === 'directory' && b.type !== 'directory') return -1; + if (a.type !== 'directory' && b.type === 'directory') return 1; + return a.name.localeCompare(b.name); + }); + + return { + path: normalizedPath.replace(/\\/g, '/'), + files, + gitignorePatterns + }; + } catch (error) { + console.error('Error listing directory:', error); + return { error: error.message, files: [] }; + } +} + +/** + * Get file content for preview + * @param {string} filePath - Path to file + * @returns {Promise} + */ +async function getFileContent(filePath) { + try { + // Normalize path + let normalizedPath = filePath.replace(/\\/g, '/'); + if (normalizedPath.match(/^\/[a-zA-Z]\//)) { + normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2); + } + + if (!existsSync(normalizedPath)) { + return { error: 'File not found' }; + } + + const stats = statSync(normalizedPath); + if (stats.isDirectory()) { + return { error: 'Cannot read directory' }; + } + + // Check file size (limit to 1MB for preview) + if (stats.size > 1024 * 1024) { + return { error: 'File too large for preview (max 1MB)', size: stats.size }; + } + + // Read file content + const content = readFileSync(normalizedPath, 'utf8'); + const ext = normalizedPath.substring(normalizedPath.lastIndexOf('.')).toLowerCase(); + const language = EXT_TO_LANGUAGE[ext] || 'plaintext'; + const isMarkdown = ext === '.md' || ext === '.markdown'; + const fileName = normalizedPath.split('/').pop(); + + return { + content, + language, + isMarkdown, + fileName, + path: normalizedPath, + size: stats.size, + lines: content.split('\n').length + }; + } catch (error) { + console.error('Error reading file:', error); + return { error: error.message }; + } +} + +/** + * Trigger update-module-claude tool + * @param {string} targetPath - Directory path to update + * @param {string} tool - CLI tool to use (gemini, qwen, codex) + * @param {string} strategy - Update strategy (single-layer, multi-layer) + * @returns {Promise} + */ +async function triggerUpdateClaudeMd(targetPath, tool, strategy) { + const { execSync } = await import('child_process'); + + try { + // Normalize path + let normalizedPath = targetPath.replace(/\\/g, '/'); + if (normalizedPath.match(/^\/[a-zA-Z]\//)) { + normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2); + } + + if (!existsSync(normalizedPath)) { + return { error: 'Directory not found' }; + } + + if (!statSync(normalizedPath).isDirectory()) { + return { error: 'Not a directory' }; + } + + // Build ccw tool command + const ccwBin = join(import.meta.dirname, '../../bin/ccw.js'); + const command = `node "${ccwBin}" tool update_module_claude --strategy="${strategy}" --path="${normalizedPath}" --tool="${tool}"`; + + console.log(`[Explorer] Running: ${command}`); + + const output = execSync(command, { + encoding: 'utf8', + timeout: 300000, // 5 minutes + cwd: normalizedPath + }); + + return { + success: true, + message: `CLAUDE.md updated successfully using ${tool} (${strategy})`, + output, + path: normalizedPath + }; + } catch (error) { + console.error('Error updating CLAUDE.md:', error); + return { + success: false, + error: error.message, + output: error.stdout || error.stderr || '' + }; + } +} diff --git a/ccw/src/templates/dashboard-css/09-explorer.css b/ccw/src/templates/dashboard-css/09-explorer.css new file mode 100644 index 00000000..c665419e --- /dev/null +++ b/ccw/src/templates/dashboard-css/09-explorer.css @@ -0,0 +1,1353 @@ +/* ========================================== + EXPLORER VIEW STYLES + ========================================== */ + +/* Main Container - Split Panel Layout */ +.explorer-container { + display: flex; + height: calc(100vh - 200px); + min-height: 500px; + gap: 1px; + background: hsl(var(--border)); + border: 1px solid hsl(var(--border)); + border-radius: 8px; + overflow: hidden; +} + +/* Left Panel - File Tree */ +.explorer-tree-panel { + width: 320px; + min-width: 240px; + max-width: 480px; + background: hsl(var(--card)); + display: flex; + flex-direction: column; + resize: horizontal; + overflow: hidden; +} + +.explorer-tree-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: hsl(var(--muted)); + border-bottom: 1px solid hsl(var(--border)); +} + +.explorer-tree-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 14px; + color: hsl(var(--foreground)); +} + +.explorer-icon { + font-size: 16px; +} + +.explorer-refresh-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + background: transparent; + color: hsl(var(--muted-foreground)); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; +} + +.explorer-refresh-btn:hover { + background: hsl(var(--hover)); + color: hsl(var(--foreground)); +} + +.explorer-refresh-btn.refreshing svg { + animation: spin 1s linear infinite; +} + +.explorer-tree-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 8px 0; +} + +/* Tree Items */ +.tree-item { + user-select: none; +} + +.tree-item-row { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + cursor: pointer; + transition: background 0.1s ease; + min-height: 28px; +} + +.tree-item-row:hover { + background: hsl(var(--hover)); +} + +.tree-item-row.selected, +.tree-file.selected .tree-item-row { + background: hsl(var(--primary) / 0.15); + color: hsl(var(--primary)); +} + +.tree-chevron { + width: 16px; + font-size: 10px; + color: hsl(var(--muted-foreground)); + text-align: center; + flex-shrink: 0; + transition: transform 0.15s ease; +} + +.tree-chevron-spacer { + width: 16px; + flex-shrink: 0; +} + +.tree-icon { + font-size: 14px; + flex-shrink: 0; + width: 20px; + text-align: center; +} + +.tree-name { + font-size: 13px; + color: hsl(var(--foreground)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +} + +/* File Type Icons */ +.file-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 16px; + font-size: 9px; + font-weight: 700; + border-radius: 3px; + font-family: system-ui, -apple-system, sans-serif; + letter-spacing: -0.3px; +} + +.file-icon-js { background: #f7df1e; color: #000; } +.file-icon-jsx { background: #61dafb; color: #000; } +.file-icon-ts { background: #3178c6; color: #fff; } +.file-icon-tsx { background: #3178c6; color: #fff; } +.file-icon-py { background: #3776ab; color: #ffd43b; } +.file-icon-go { background: #00add8; color: #fff; } +.file-icon-rs { background: #dea584; color: #000; } +.file-icon-java { background: #ed8b00; color: #fff; } +.file-icon-rb { background: #cc342d; color: #fff; } +.file-icon-php { background: #777bb4; color: #fff; } +.file-icon-c { background: #a8b9cc; color: #000; } +.file-icon-cpp { background: #00599c; color: #fff; } +.file-icon-h { background: #a8b9cc; color: #000; } +.file-icon-cs { background: #68217a; color: #fff; } +.file-icon-swift { background: #fa7343; color: #fff; } +.file-icon-kt { background: #7f52ff; color: #fff; } + +.file-icon-html { background: #e34f26; color: #fff; } +.file-icon-css { background: #1572b6; color: #fff; } +.file-icon-scss { background: #c6538c; color: #fff; } +.file-icon-less { background: #1d365d; color: #fff; } +.file-icon-vue { background: #42b883; color: #fff; } +.file-icon-svelte { background: #ff3e00; color: #fff; } + +.file-icon-json { background: #292929; color: #f5a623; } +.file-icon-yaml { background: #cb171e; color: #fff; } +.file-icon-xml { background: #0060ac; color: #fff; } +.file-icon-toml { background: #9c4121; color: #fff; } +.file-icon-ini { background: #6d6d6d; color: #fff; } +.file-icon-env { background: #ecd53f; color: #000; } + +.file-icon-md { background: #083fa1; color: #fff; } +.file-icon-txt { background: #6d6d6d; color: #fff; } +.file-icon-log { background: #4a4a4a; color: #8bc34a; } + +.file-icon-sh { background: #4eaa25; color: #fff; } +.file-icon-ps1 { background: #012456; color: #fff; } +.file-icon-bat { background: #c1f12e; color: #000; } + +.file-icon-sql { background: #f29111; color: #fff; } +.file-icon-db { background: #003b57; color: #fff; } + +.file-icon-img { background: #8bc34a; color: #fff; } +.file-icon-svg { background: #ffb13b; color: #000; } + +.file-icon-lock { background: transparent; font-size: 14px; } +.file-icon-docker { background: transparent; font-size: 14px; } +.file-icon-default { background: transparent; font-size: 14px; } +.file-icon-claude { background: #d97706; color: #fff; font-size: 12px; } + +/* Folder Children */ +.tree-children { + display: none; + overflow: hidden; +} + +.tree-children.show { + display: block; +} + +.tree-folder.expanded > .tree-item-row .tree-chevron { + transform: rotate(0deg); +} + +.tree-folder:not(.expanded) > .tree-item-row .tree-chevron { + transform: rotate(-90deg); +} + +/* CLAUDE.md Badge and Update Button */ +.claude-md-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 2px 6px; + background: hsl(var(--success) / 0.15); + color: hsl(var(--success)); + border-radius: 4px; + font-size: 10px; + font-weight: 600; + margin-left: 6px; + flex-shrink: 0; +} + +.claude-md-badge .badge-icon { + font-size: 10px; +} + +.claude-md-badge .badge-text { + text-transform: uppercase; + letter-spacing: 0.3px; +} + +/* Folder Action Buttons */ +.tree-folder-actions { + display: none; + align-items: center; + gap: 2px; + margin-left: auto; + flex-shrink: 0; +} + +.tree-item-row:hover .tree-folder-actions { + display: flex; +} + +.tree-update-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 22px; + border: none; + background: hsl(var(--muted)); + color: hsl(var(--foreground)); + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.15s ease; +} + +.tree-update-btn .update-icon { + font-size: 12px; +} + +.tree-update-btn:hover { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + transform: scale(1.05); +} + +.tree-update-btn.tree-update-multi:hover { + background: hsl(var(--success)); +} + +/* Highlight folders with CLAUDE.md */ +.tree-folder.has-claude-md > .tree-item-row { + background: hsl(var(--success) / 0.05); +} + +.tree-folder.has-claude-md > .tree-item-row:hover { + background: hsl(var(--success) / 0.1); +} + +/* Highlight CLAUDE.md files */ +.tree-file.is-claude-md .tree-name { + color: hsl(var(--primary)); + font-weight: 500; +} + +/* Tree States */ +.tree-loading, +.tree-error, +.tree-empty { + padding: 8px 16px; + font-size: 12px; + color: hsl(var(--muted-foreground)); +} + +.tree-error { + color: hsl(var(--destructive)); +} + +/* Right Panel - Preview */ +.explorer-preview-panel { + flex: 1; + background: hsl(var(--card)); + display: flex; + flex-direction: column; + min-width: 0; +} + +.explorer-preview-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: hsl(var(--muted)); + border-bottom: 1px solid hsl(var(--border)); + min-height: 44px; + flex-shrink: 0; +} + +.preview-header-left { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + flex: 1; +} + +.preview-filename { + font-weight: 600; + font-size: 14px; + color: hsl(var(--foreground)); + white-space: nowrap; + flex-shrink: 0; +} + +.preview-path { + font-size: 12px; + color: hsl(var(--muted-foreground)); + font-family: monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.preview-header-tabs { + display: flex; + gap: 4px; + flex-shrink: 0; + background: hsl(var(--background)); + padding: 3px; + border-radius: 6px; +} + +.explorer-preview-content { + flex: 1; + overflow: auto; + padding: 0; +} + +/* Preview Empty State */ +.explorer-preview-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: hsl(var(--muted-foreground)); +} + +.preview-empty-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.preview-empty-text { + font-size: 14px; +} + +/* Preview Info Bar */ +.preview-info { + display: flex; + align-items: center; + gap: 16px; + padding: 8px 16px; + background: hsl(var(--muted) / 0.5); + border-bottom: 1px solid hsl(var(--border)); + font-size: 12px; + color: hsl(var(--muted-foreground)); +} + +.preview-lang { + padding: 2px 8px; + background: hsl(var(--primary) / 0.15); + color: hsl(var(--primary)); + border-radius: 4px; + font-weight: 500; + text-transform: uppercase; + font-size: 11px; +} + +/* Code Preview */ +.preview-code { + margin: 0; + padding: 16px; + background: hsl(var(--muted) / 0.3); + overflow: auto; + font-size: 13px; + line-height: 1.5; +} + +.preview-code code { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + background: transparent; +} + +/* Preview Tabs (for Markdown) - in header */ +.preview-tab { + padding: 5px 12px; + border: none; + background: transparent; + color: hsl(var(--muted-foreground)); + font-size: 12px; + font-weight: 500; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; +} + +.preview-tab:hover { + color: hsl(var(--foreground)); +} + +.preview-tab.active { + background: hsl(var(--card)); + color: hsl(var(--foreground)); + box-shadow: 0 1px 2px hsl(var(--foreground) / 0.08); +} + +.preview-tab-content { + display: none; +} + +.preview-tab-content.show { + display: block; +} + +/* Markdown Preview */ +.markdown-preview { + padding: 20px; + line-height: 1.6; +} + +.markdown-preview h1, +.markdown-preview h2, +.markdown-preview h3, +.markdown-preview h4 { + color: hsl(var(--foreground)); + margin-top: 24px; + margin-bottom: 12px; +} + +.markdown-preview h1 { font-size: 1.75em; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 8px; } +.markdown-preview h2 { font-size: 1.5em; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 6px; } +.markdown-preview h3 { font-size: 1.25em; } +.markdown-preview h4 { font-size: 1.1em; } + +.markdown-preview p { + margin-bottom: 12px; + color: hsl(var(--foreground)); +} + +.markdown-preview ul, +.markdown-preview ol { + margin-bottom: 12px; + padding-left: 24px; +} + +.markdown-preview li { + margin-bottom: 4px; +} + +.markdown-preview code { + background: hsl(var(--muted)); + padding: 2px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 0.9em; +} + +.markdown-preview pre { + background: hsl(var(--muted)); + padding: 16px; + border-radius: 8px; + overflow-x: auto; + margin-bottom: 16px; +} + +.markdown-preview pre code { + background: transparent; + padding: 0; +} + +.markdown-preview blockquote { + border-left: 4px solid hsl(var(--primary)); + padding-left: 16px; + margin: 16px 0; + color: hsl(var(--muted-foreground)); +} + +.markdown-preview a { + color: hsl(var(--primary)); + text-decoration: none; +} + +.markdown-preview a:hover { + text-decoration: underline; +} + +.markdown-preview table { + width: 100%; + border-collapse: collapse; + margin-bottom: 16px; +} + +.markdown-preview th, +.markdown-preview td { + border: 1px solid hsl(var(--border)); + padding: 8px 12px; + text-align: left; +} + +.markdown-preview th { + background: hsl(var(--muted)); + font-weight: 600; +} + +/* Loading and Error States */ +.explorer-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: hsl(var(--muted-foreground)); + font-size: 14px; +} + +.explorer-error { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: hsl(var(--destructive)); + font-size: 14px; +} + +/* ========================================== + UPDATE CLAUDE.MD MODAL + ========================================== */ + +.claude-md-modal { + animation: fadeIn 0.15s ease-out; +} + +.claude-md-modal-backdrop { + animation: fadeIn 0.15s ease-out; +} + +.claude-md-modal-content { + animation: slideUp 0.2s ease-out; +} + +.claude-md-modal.hidden { + display: none; +} + +.claude-md-target-path { + background: hsl(var(--muted)); + padding: 8px 12px; + border-radius: 6px; + font-family: monospace; + font-size: 12px; + color: hsl(var(--foreground)); + word-break: break-all; +} + +.claude-md-form-group { + margin-bottom: 16px; +} + +.claude-md-form-group label { + display: block; + margin-bottom: 6px; + font-size: 14px; + font-weight: 500; + color: hsl(var(--foreground)); +} + +.claude-md-form-group select { + width: 100%; + padding: 8px 12px; + border: 1px solid hsl(var(--border)); + border-radius: 6px; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 14px; +} + +.claude-md-form-group select:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2); +} + +.claude-md-status { + margin-top: 16px; + min-height: 40px; +} + +.status-running { + color: hsl(var(--warning)); + font-size: 13px; +} + +.status-success { + color: hsl(var(--success)); + font-size: 13px; +} + +.status-error { + color: hsl(var(--destructive)); + font-size: 13px; +} + +/* Responsive */ +@media (max-width: 768px) { + .explorer-container { + flex-direction: column; + height: auto; + } + + .explorer-tree-panel { + width: 100%; + max-width: none; + height: 300px; + resize: vertical; + } + + .explorer-preview-panel { + height: 400px; + } +} + +/* ========================================== + FLOATING ACTION BUTTON & TASK QUEUE + ========================================== */ + +/* Floating Action Button */ +.explorer-fab { + position: fixed; + bottom: 80px; + right: 30px; + width: 56px; + height: 56px; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 4px 12px hsl(var(--primary) / 0.4); + transition: all 0.2s ease; + z-index: 100; +} + +.explorer-fab:hover { + transform: scale(1.1); + box-shadow: 0 6px 20px hsl(var(--primary) / 0.5); +} + +.explorer-fab.active { + background: hsl(var(--foreground)); + transform: rotate(45deg); +} + +.explorer-fab .fab-icon { + font-size: 24px; + transition: transform 0.2s ease; +} + +.explorer-fab.active .fab-icon { + transform: rotate(-45deg); +} + +.explorer-fab .fab-badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 20px; + height: 20px; + background: hsl(var(--destructive)); + color: hsl(var(--destructive-foreground)); + border-radius: 10px; + font-size: 11px; + font-weight: 600; + display: none; + align-items: center; + justify-content: center; + padding: 0 5px; +} + +/* Task Queue Panel */ +.task-queue-panel { + position: fixed; + bottom: 150px; + right: 30px; + width: 360px; + max-height: 450px; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 12px; + box-shadow: 0 8px 32px hsl(var(--foreground) / 0.15); + z-index: 99; + display: flex; + flex-direction: column; + opacity: 0; + transform: translateY(20px) scale(0.95); + pointer-events: none; + transition: all 0.2s ease; +} + +.task-queue-panel.show { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; +} + +.task-queue-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted)); + border-radius: 12px 12px 0 0; + gap: 8px; +} + +.queue-tabs { + display: flex; + gap: 4px; + flex: 1; +} + +.queue-tab { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + border: none; + background: transparent; + color: hsl(var(--muted-foreground)); + font-size: 12px; + font-weight: 500; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; +} + +.queue-tab:hover { + background: hsl(var(--hover)); + color: hsl(var(--foreground)); +} + +.queue-tab.active { + background: hsl(var(--background)); + color: hsl(var(--foreground)); + box-shadow: 0 1px 2px hsl(var(--foreground) / 0.1); +} + +.queue-tab-badge { + display: none; + align-items: center; + justify-content: center; + min-width: 16px; + height: 16px; + padding: 0 4px; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-radius: 8px; + font-size: 10px; + font-weight: 600; +} + +.queue-tab-badge:not(:empty) { + display: inline-flex; +} + +.queue-tab-content { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.queue-tab-content.hidden { + display: none; +} + +.task-queue-title { + font-weight: 600; + font-size: 14px; + color: hsl(var(--foreground)); +} + +.task-queue-close { + width: 28px; + height: 28px; + border: none; + background: transparent; + color: hsl(var(--muted-foreground)); + font-size: 20px; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.task-queue-close:hover { + background: hsl(var(--hover)); + color: hsl(var(--foreground)); +} + +.task-queue-actions { + display: flex; + gap: 8px; + padding: 10px 16px; + border-bottom: 1px solid hsl(var(--border)); +} + +.queue-action-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 8px 12px; + border: 1px solid hsl(var(--border)); + background: hsl(var(--background)); + color: hsl(var(--foreground)); + border-radius: 6px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.queue-action-btn:hover:not(:disabled) { + background: hsl(var(--hover)); + border-color: hsl(var(--primary)); +} + +.queue-action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.queue-action-btn.queue-start-btn { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-color: hsl(var(--primary)); +} + +.queue-action-btn.queue-start-btn:hover:not(:disabled) { + background: hsl(var(--primary) / 0.9); +} + +.queue-action-btn.queue-clear-btn:hover:not(:disabled) { + background: hsl(var(--destructive) / 0.1); + border-color: hsl(var(--destructive)); + color: hsl(var(--destructive)); +} + +.task-queue-list { + flex: 1; + overflow-y: auto; + padding: 8px; + max-height: 280px; +} + +.task-queue-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 32px 16px; + text-align: center; + color: hsl(var(--muted-foreground)); +} + +.task-queue-empty span { + font-size: 14px; + font-weight: 500; + margin-bottom: 4px; +} + +.task-queue-empty p { + font-size: 12px; + margin: 0; +} + +/* Task Item */ +.task-queue-item { + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 8px; + padding: 10px 12px; + margin-bottom: 8px; + transition: all 0.15s ease; +} + +.task-queue-item:last-child { + margin-bottom: 0; +} + +.task-queue-item.status-running { + border-color: hsl(var(--warning)); + background: hsl(var(--warning) / 0.05); +} + +.task-queue-item.status-completed { + border-color: hsl(var(--success)); + background: hsl(var(--success) / 0.05); +} + +.task-queue-item.status-failed { + border-color: hsl(var(--destructive)); + background: hsl(var(--destructive) / 0.05); +} + +.task-item-header { + display: flex; + align-items: center; + gap: 8px; +} + +.task-status-icon { + font-size: 14px; + flex-shrink: 0; +} + +.task-folder-name { + font-size: 13px; + font-weight: 500; + color: hsl(var(--foreground)); + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.task-remove-btn { + width: 20px; + height: 20px; + border: none; + background: transparent; + color: hsl(var(--muted-foreground)); + font-size: 16px; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.15s ease; +} + +.task-remove-btn:hover { + background: hsl(var(--destructive) / 0.1); + color: hsl(var(--destructive)); +} + +.task-item-meta { + display: flex; + gap: 8px; + margin-top: 6px; +} + +.task-strategy, +.task-tool { + font-size: 11px; + padding: 2px 6px; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + border-radius: 4px; +} + +.task-item-message { + font-size: 11px; + color: hsl(var(--muted-foreground)); + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid hsl(var(--border)); +} + +.task-queue-item.status-running .task-status-icon { + animation: spin 1s linear infinite; +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .explorer-fab { + bottom: 70px; + right: 20px; + width: 50px; + height: 50px; + } + + .task-queue-panel { + right: 10px; + left: 10px; + width: auto; + bottom: 130px; + } +} + +/* ========================================== + GLOBAL NOTIFICATION SYSTEM + ========================================== */ + +/* Global FAB - positioned above explorer FAB */ +.global-notif-fab { + position: fixed; + bottom: 150px; + right: 30px; + width: 48px; + height: 48px; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + color: hsl(var(--foreground)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 4px 12px hsl(var(--foreground) / 0.15); + transition: all 0.2s ease; + z-index: 100; +} + +.global-notif-fab:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px hsl(var(--foreground) / 0.2); + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} + +.global-notif-fab.active { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} + +.global-notif-fab .fab-icon { + font-size: 20px; +} + +.global-notif-fab .fab-badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 18px; + height: 18px; + background: hsl(var(--destructive)); + color: hsl(var(--destructive-foreground)); + border-radius: 9px; + font-size: 10px; + font-weight: 600; + display: none; + align-items: center; + justify-content: center; + padding: 0 4px; +} + +/* Global Notification Panel */ +.global-notif-panel { + position: fixed; + bottom: 210px; + right: 30px; + width: 380px; + max-height: 500px; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 12px; + box-shadow: 0 8px 32px hsl(var(--foreground) / 0.15); + z-index: 99; + display: flex; + flex-direction: column; + opacity: 0; + transform: translateY(20px) scale(0.95); + pointer-events: none; + transition: all 0.2s ease; +} + +.global-notif-panel.show { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; +} + +.global-notif-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted)); + border-radius: 12px 12px 0 0; +} + +.global-notif-title { + font-weight: 600; + font-size: 14px; + color: hsl(var(--foreground)); +} + +.global-notif-close { + width: 28px; + height: 28px; + border: none; + background: transparent; + color: hsl(var(--muted-foreground)); + font-size: 20px; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.global-notif-close:hover { + background: hsl(var(--hover)); + color: hsl(var(--foreground)); +} + +.global-notif-actions { + display: flex; + gap: 8px; + padding: 10px 16px; + border-bottom: 1px solid hsl(var(--border)); +} + +.notif-action-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + border: 1px solid hsl(var(--border)); + background: hsl(var(--background)); + color: hsl(var(--foreground)); + border-radius: 6px; + font-size: 12px; + cursor: pointer; + transition: all 0.15s ease; +} + +.notif-action-btn:hover { + background: hsl(var(--hover)); +} + +.global-notif-list { + flex: 1; + overflow-y: auto; + padding: 8px; + max-height: 350px; +} + +.global-notif-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 16px; + text-align: center; + color: hsl(var(--muted-foreground)); +} + +.global-notif-empty span { + font-size: 14px; + font-weight: 500; + margin-bottom: 4px; +} + +.global-notif-empty p { + font-size: 12px; + margin: 0; +} + +/* Notification Item */ +.global-notif-item { + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 8px; + padding: 10px 12px; + margin-bottom: 8px; + transition: all 0.15s ease; +} + +.global-notif-item:last-child { + margin-bottom: 0; +} + +.global-notif-item.type-success { + border-left: 3px solid hsl(var(--success)); +} + +.global-notif-item.type-error { + border-left: 3px solid hsl(var(--destructive)); +} + +.global-notif-item.type-warning { + border-left: 3px solid hsl(var(--warning)); +} + +.global-notif-item.type-info { + border-left: 3px solid hsl(var(--primary)); +} + +.global-notif-item.read { + opacity: 0.7; +} + +.notif-item-header { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.notif-icon { + font-size: 14px; + flex-shrink: 0; +} + +.notif-message { + font-size: 13px; + color: hsl(var(--foreground)); + flex: 1; + line-height: 1.4; +} + +.notif-source { + font-size: 10px; + padding: 2px 6px; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + border-radius: 4px; + text-transform: uppercase; + flex-shrink: 0; +} + +.notif-details { + font-size: 12px; + color: hsl(var(--muted-foreground)); + margin-top: 6px; + padding-left: 22px; + line-height: 1.4; +} + +.notif-meta { + display: flex; + justify-content: flex-end; + margin-top: 6px; +} + +.notif-time { + font-size: 11px; + color: hsl(var(--muted-foreground)); +} + +/* Toast Notification */ +.notif-toast { + position: fixed; + top: 70px; + right: 20px; + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 8px; + box-shadow: 0 4px 16px hsl(var(--foreground) / 0.15); + z-index: 1000; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; +} + +.notif-toast.show { + opacity: 1; + transform: translateX(0); +} + +.notif-toast.type-success { + border-left: 3px solid hsl(var(--success)); +} + +.notif-toast.type-error { + border-left: 3px solid hsl(var(--destructive)); +} + +.notif-toast .toast-icon { + font-size: 16px; +} + +.notif-toast .toast-message { + font-size: 13px; + color: hsl(var(--foreground)); +} + +/* Mobile responsive for global notifications */ +@media (max-width: 768px) { + .global-notif-fab { + bottom: 130px; + right: 20px; + width: 44px; + height: 44px; + } + + .global-notif-panel { + right: 10px; + left: 10px; + width: auto; + bottom: 185px; + } +} + diff --git a/ccw/src/templates/dashboard-js/components/global-notifications.js b/ccw/src/templates/dashboard-js/components/global-notifications.js new file mode 100644 index 00000000..2fbdb5a8 --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/global-notifications.js @@ -0,0 +1,219 @@ +// ========================================== +// GLOBAL NOTIFICATION SYSTEM +// ========================================== +// Floating notification panel accessible from any view + +/** + * Initialize global notification panel + */ +function initGlobalNotifications() { + // Create FAB and panel if not exists + if (!document.getElementById('globalNotificationFab')) { + const fabHtml = ` +
+ 🔔 + 0 +
+ +
+
+ 🔔 Notifications + +
+
+ +
+
+
+ No notifications +

System events and task updates will appear here

+
+
+
+ `; + + const container = document.createElement('div'); + container.id = 'globalNotificationContainer'; + container.innerHTML = fabHtml; + document.body.appendChild(container); + } + + renderGlobalNotifications(); +} + +/** + * Toggle notification panel visibility + */ +function toggleGlobalNotifications() { + isNotificationPanelVisible = !isNotificationPanelVisible; + const panel = document.getElementById('globalNotificationPanel'); + const fab = document.getElementById('globalNotificationFab'); + + if (panel && fab) { + if (isNotificationPanelVisible) { + panel.classList.add('show'); + fab.classList.add('active'); + } else { + panel.classList.remove('show'); + fab.classList.remove('active'); + } + } +} + +/** + * Add a global notification + * @param {string} type - 'info', 'success', 'warning', 'error' + * @param {string} message - Main notification message + * @param {string} details - Optional details + * @param {string} source - Optional source identifier (e.g., 'explorer', 'mcp') + */ +function addGlobalNotification(type, message, details = null, source = null) { + const notification = { + id: Date.now(), + type, + message, + details, + source, + timestamp: new Date().toISOString(), + read: false + }; + + globalNotificationQueue.unshift(notification); + + // Keep only last 100 notifications + if (globalNotificationQueue.length > 100) { + globalNotificationQueue = globalNotificationQueue.slice(0, 100); + } + + renderGlobalNotifications(); + updateGlobalNotifBadge(); + + // Show toast for important notifications + if (type === 'error' || type === 'success') { + showNotificationToast(notification); + } +} + +/** + * Show a brief toast notification + */ +function showNotificationToast(notification) { + const typeIcon = { + 'info': 'â„šī¸', + 'success': '✅', + 'warning': 'âš ī¸', + 'error': '❌' + }[notification.type] || 'â„šī¸'; + + // Remove existing toast + const existing = document.querySelector('.notif-toast'); + if (existing) existing.remove(); + + const toast = document.createElement('div'); + toast.className = `notif-toast type-${notification.type}`; + toast.innerHTML = ` + ${typeIcon} + ${escapeHtml(notification.message)} + `; + document.body.appendChild(toast); + + // Animate in + requestAnimationFrame(() => toast.classList.add('show')); + + // Auto-remove + setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => toast.remove(), 300); + }, 3000); +} + +/** + * Render notification list + */ +function renderGlobalNotifications() { + const listEl = document.getElementById('globalNotificationList'); + if (!listEl) return; + + if (globalNotificationQueue.length === 0) { + listEl.innerHTML = ` +
+ No notifications +

System events and task updates will appear here

+
+ `; + return; + } + + listEl.innerHTML = globalNotificationQueue.map(notif => { + const typeIcon = { + 'info': 'â„šī¸', + 'success': '✅', + 'warning': 'âš ī¸', + 'error': '❌' + }[notif.type] || 'â„šī¸'; + + const time = formatNotifTime(notif.timestamp); + const sourceLabel = notif.source ? `${notif.source}` : ''; + + return ` +
+
+ ${typeIcon} + ${escapeHtml(notif.message)} + ${sourceLabel} +
+ ${notif.details ? `
${escapeHtml(notif.details)}
` : ''} +
+ ${time} +
+
+ `; + }).join(''); +} + +/** + * Format notification time + */ +function formatNotifTime(timestamp) { + const date = new Date(timestamp); + const now = new Date(); + const diff = now - date; + + if (diff < 60000) return 'Just now'; + if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; + if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; + return date.toLocaleDateString(); +} + +/** + * Update notification badge + */ +function updateGlobalNotifBadge() { + const badge = document.getElementById('globalNotifBadge'); + if (badge) { + const unreadCount = globalNotificationQueue.filter(n => !n.read).length; + badge.textContent = unreadCount; + badge.style.display = unreadCount > 0 ? 'flex' : 'none'; + } +} + +/** + * Clear all notifications + */ +function clearGlobalNotifications() { + globalNotificationQueue = []; + renderGlobalNotifications(); + updateGlobalNotifBadge(); +} + +/** + * Mark all as read + */ +function markAllNotificationsRead() { + globalNotificationQueue.forEach(n => n.read = true); + renderGlobalNotifications(); + updateGlobalNotifBadge(); +} + diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js index 935f8dcf..3af5597f 100644 --- a/ccw/src/templates/dashboard-js/components/navigation.js +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -96,6 +96,8 @@ function initNavigation() { renderMcpManager(); } else if (currentView === 'project-overview') { renderProjectOverview(); + } else if (currentView === 'explorer') { + renderExplorer(); } }); }); @@ -112,6 +114,8 @@ function updateContentTitle() { titleEl.textContent = 'Project Overview'; } else if (currentView === 'mcp-manager') { titleEl.textContent = 'MCP Server Management'; + } else if (currentView === 'explorer') { + titleEl.textContent = 'File Explorer'; } else if (currentView === 'liteTasks') { const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' }; titleEl.textContent = names[currentLiteType] || 'Lite Tasks'; diff --git a/ccw/src/templates/dashboard-js/components/theme.js b/ccw/src/templates/dashboard-js/components/theme.js index b3da36e2..46928755 100644 --- a/ccw/src/templates/dashboard-js/components/theme.js +++ b/ccw/src/templates/dashboard-js/components/theme.js @@ -6,6 +6,7 @@ function initTheme() { const saved = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', saved); updateThemeIcon(saved); + updateHljsTheme(saved); document.getElementById('themeToggle').addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme'); @@ -13,9 +14,36 @@ function initTheme() { document.documentElement.setAttribute('data-theme', next); localStorage.setItem('theme', next); updateThemeIcon(next); + updateHljsTheme(next); }); } function updateThemeIcon(theme) { - document.getElementById('themeToggle').textContent = theme === 'light' ? '🌙' : 'â˜€ī¸'; + const darkIcon = document.querySelector('.theme-icon-dark'); + const lightIcon = document.querySelector('.theme-icon-light'); + if (darkIcon && lightIcon) { + if (theme === 'light') { + darkIcon.classList.remove('hidden'); + lightIcon.classList.add('hidden'); + } else { + darkIcon.classList.add('hidden'); + lightIcon.classList.remove('hidden'); + } + } +} + +function updateHljsTheme(theme) { + // Toggle highlight.js theme stylesheets + const darkTheme = document.getElementById('hljs-theme-dark'); + const lightTheme = document.getElementById('hljs-theme-light'); + + if (darkTheme && lightTheme) { + if (theme === 'dark') { + darkTheme.disabled = false; + lightTheme.disabled = true; + } else { + darkTheme.disabled = true; + lightTheme.disabled = false; + } + } } diff --git a/ccw/src/templates/dashboard-js/main.js b/ccw/src/templates/dashboard-js/main.js index cdb22452..a0ae670d 100644 --- a/ccw/src/templates/dashboard-js/main.js +++ b/ccw/src/templates/dashboard-js/main.js @@ -2,6 +2,9 @@ // Initializes all components and sets up global event handlers document.addEventListener('DOMContentLoaded', async () => { + // Initialize Lucide icons (must be first to render SVG icons) + try { lucide.createIcons(); } catch (e) { console.error('Lucide icons init failed:', e); } + // Initialize components with error handling to prevent cascading failures try { initTheme(); } catch (e) { console.error('Theme init failed:', e); } try { initSidebar(); } catch (e) { console.error('Sidebar init failed:', e); } @@ -12,6 +15,7 @@ document.addEventListener('DOMContentLoaded', async () => { try { initCarousel(); } catch (e) { console.error('Carousel init failed:', e); } try { initMcpManager(); } catch (e) { console.error('MCP Manager init failed:', e); } try { initHookManager(); } catch (e) { console.error('Hook Manager init failed:', e); } + try { initGlobalNotifications(); } catch (e) { console.error('Global notifications init failed:', e); } // Initialize real-time features (WebSocket + auto-refresh) try { initWebSocket(); } catch (e) { console.log('WebSocket not available:', e.message); } diff --git a/ccw/src/templates/dashboard-js/state.js b/ccw/src/templates/dashboard-js/state.js index d37a74f6..a012bdb6 100644 --- a/ccw/src/templates/dashboard-js/state.js +++ b/ccw/src/templates/dashboard-js/state.js @@ -35,3 +35,8 @@ const liteTaskDataStore = {}; // Store task JSON data in a global map instead of inline script tags // Key: unique task ID, Value: raw task JSON data const taskJsonStore = {}; + +// ========== Global Notification Queue ========== +// Notification queue visible from any view +let globalNotificationQueue = []; +let isNotificationPanelVisible = false; \ No newline at end of file diff --git a/ccw/src/templates/dashboard-js/views/explorer.js b/ccw/src/templates/dashboard-js/views/explorer.js new file mode 100644 index 00000000..cc48d453 --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/explorer.js @@ -0,0 +1,821 @@ +// ============================================ +// EXPLORER VIEW +// ============================================ +// File tree browser with .gitignore filtering and CLAUDE.md update support +// Split-panel layout: file tree (left) + preview (right) + +// Explorer state +let explorerCurrentPath = null; +let explorerSelectedFile = null; +let explorerExpandedDirs = new Set(); + +// Task queue for CLAUDE.md updates +let updateTaskQueue = []; +let isTaskQueueVisible = false; +let isTaskRunning = false; + + +/** + * Render the Explorer view + */ +async function renderExplorer() { + const container = document.getElementById('mainContent'); + if (!container) return; + + // Hide stats grid and search + const statsGrid = document.getElementById('statsGrid'); + const searchInput = document.getElementById('searchInput'); + if (statsGrid) statsGrid.style.display = 'none'; + if (searchInput) searchInput.parentElement.style.display = 'none'; + + // Initialize explorer path to project path + explorerCurrentPath = projectPath; + + container.innerHTML = ` +
+ +
+
+
+ + Explorer +
+ +
+
+
Loading file tree...
+
+
+ + +
+
+ Select a file to preview +
+
+
+
+
Select a file from the tree to preview its contents
+
+
+
+
+ + +
+ + 0 +
+ + +
+
+ Update Tasks + +
+
+ + + +
+
+
+ No tasks in queue +

Hover folder and click or to add tasks

+
+
+
+ `; + + // Load initial file tree + await loadExplorerTree(explorerCurrentPath); + + // Initialize Lucide icons for dynamically rendered content + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +/** + * Load and render file tree for a directory + */ +async function loadExplorerTree(dirPath) { + const treeContent = document.getElementById('explorerTreeContent'); + if (!treeContent) return; + + try { + const response = await fetch(`/api/files?path=${encodeURIComponent(dirPath)}`); + const data = await response.json(); + + if (data.error) { + treeContent.innerHTML = `
${escapeHtml(data.error)}
`; + return; + } + + // Render root level + treeContent.innerHTML = renderTreeLevel(data.files, dirPath, 0); + attachTreeEventListeners(); + + // Initialize Lucide icons for tree items + if (typeof lucide !== 'undefined') lucide.createIcons(); + + } catch (error) { + treeContent.innerHTML = `
Failed to load: ${escapeHtml(error.message)}
`; + } +} + +/** + * Render a level of the file tree + */ +function renderTreeLevel(files, parentPath, depth) { + if (!files || files.length === 0) { + return `
Empty directory
`; + } + + return files.map(file => { + const isExpanded = explorerExpandedDirs.has(file.path); + const isSelected = explorerSelectedFile === file.path; + + if (file.type === 'directory') { + const folderIcon = getFolderIcon(file.name, isExpanded, file.hasClaudeMd); + const chevronIcon = isExpanded ? '' : ''; + return ` +
+
+ ${chevronIcon} + ${folderIcon} + ${escapeHtml(file.name)} + ${file.hasClaudeMd ? ` + + + DOC + + ` : ''} +
+ + +
+
+
+ ${isExpanded ? '' : ''} +
+
+ `; + } else { + const ext = file.name.includes('.') ? file.name.split('.').pop().toLowerCase() : ''; + const fileIcon = getFileIcon(ext); + // Special highlight for CLAUDE.md files + const isClaudeMd = file.name === 'CLAUDE.md'; + return ` +
+
+ + ${isClaudeMd ? '' : fileIcon} + ${escapeHtml(file.name)} +
+
+ `; + } + }).join(''); +} + +/** + * Get file icon based on extension - using colored SVG icons for better distinction + */ +function getFileIcon(ext) { + const iconMap = { + // JavaScript/TypeScript - distinct colors + 'js': 'JS', + 'mjs': 'JS', + 'cjs': 'JS', + 'jsx': 'JSX', + 'ts': 'TS', + 'tsx': 'TSX', + + // Python + 'py': 'PY', + 'pyw': 'PY', + + // Other languages + 'go': 'GO', + 'rs': 'RS', + 'java': 'JV', + 'rb': 'RB', + 'php': 'PHP', + 'c': 'C', + 'cpp': 'C++', + 'h': 'H', + 'cs': 'C#', + 'swift': 'SW', + 'kt': 'KT', + + // Web + 'html': 'HTML', + 'htm': 'HTML', + 'css': 'CSS', + 'scss': 'SCSS', + 'sass': 'SASS', + 'less': 'LESS', + 'vue': 'VUE', + 'svelte': 'SV', + + // Config/Data + 'json': '{}', + 'yaml': 'YML', + 'yml': 'YML', + 'xml': 'XML', + 'toml': 'TML', + 'ini': 'INI', + 'env': 'ENV', + + // Documentation + 'md': 'MD', + 'markdown': 'MD', + 'txt': 'TXT', + 'log': 'LOG', + + // Shell/Scripts + 'sh': 'SH', + 'bash': 'SH', + 'zsh': 'ZSH', + 'ps1': 'PS1', + 'bat': 'BAT', + 'cmd': 'CMD', + + // Database + 'sql': 'SQL', + 'db': 'DB', + + // Docker/Container + 'dockerfile': '', + + // Images + 'png': 'IMG', + 'jpg': 'IMG', + 'jpeg': 'IMG', + 'gif': 'GIF', + 'svg': 'SVG', + 'ico': 'ICO', + + // Package + 'lock': '' + }; + + return iconMap[ext] || ''; +} + +/** + * Get folder icon based on folder name and state + */ +function getFolderIcon(name, isExpanded, hasClaudeMd) { + // Only special icon for .workflow folder + if (name === '.workflow') { + return ''; + } + return isExpanded + ? '' + : ''; +} + +/** + * Attach event listeners to tree items + */ +function attachTreeEventListeners() { + // Folder click - toggle expand + document.querySelectorAll('.tree-folder > .tree-item-row').forEach(row => { + row.addEventListener('click', async (e) => { + const folder = row.closest('.tree-folder'); + const path = folder.dataset.path; + await toggleFolderExpand(path, folder); + }); + }); + + // File click - preview + document.querySelectorAll('.tree-file').forEach(item => { + item.addEventListener('click', async () => { + const path = item.dataset.path; + await previewFile(path); + + // Update selection + document.querySelectorAll('.tree-item-row.selected, .tree-file.selected').forEach(el => { + el.classList.remove('selected'); + }); + item.classList.add('selected'); + explorerSelectedFile = path; + }); + }); +} + +/** + * Toggle folder expand/collapse + */ +async function toggleFolderExpand(path, folderElement) { + const isExpanded = explorerExpandedDirs.has(path); + const childrenContainer = folderElement.querySelector('.tree-children'); + const chevron = folderElement.querySelector('.tree-chevron'); + const folderIcon = folderElement.querySelector('.tree-icon'); + + if (isExpanded) { + // Collapse + explorerExpandedDirs.delete(path); + folderElement.classList.remove('expanded'); + childrenContainer.classList.remove('show'); + // Update chevron and folder icon + if (chevron) chevron.innerHTML = ''; + if (folderIcon && !folderElement.querySelector('[data-lucide="zap"]')) { + folderIcon.innerHTML = ''; + } + } else { + // Expand - load children if not loaded + explorerExpandedDirs.add(path); + folderElement.classList.add('expanded'); + childrenContainer.classList.add('show'); + // Update chevron and folder icon + if (chevron) chevron.innerHTML = ''; + if (folderIcon && !folderElement.querySelector('[data-lucide="zap"]')) { + folderIcon.innerHTML = ''; + } + + if (!childrenContainer.innerHTML.trim()) { + childrenContainer.innerHTML = '
Loading...
'; + + try { + const response = await fetch(`/api/files?path=${encodeURIComponent(path)}`); + const data = await response.json(); + + const depth = (path.match(/\//g) || []).length - (explorerCurrentPath.match(/\//g) || []).length + 1; + childrenContainer.innerHTML = renderTreeLevel(data.files, path, depth); + attachTreeEventListeners(); + } catch (error) { + childrenContainer.innerHTML = `
Failed to load
`; + } + } + } + + // Reinitialize Lucide icons after DOM changes + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +/** + * Preview a file in the right panel + */ +async function previewFile(filePath) { + const previewHeader = document.getElementById('explorerPreviewHeader'); + const previewContent = document.getElementById('explorerPreviewContent'); + + const fileName = filePath.split('/').pop(); + const ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : ''; + const isMarkdown = ext === 'md' || ext === 'markdown'; + + // Build header with tabs for markdown files + previewHeader.innerHTML = ` +
+ ${escapeHtml(fileName)} + ${escapeHtml(filePath)} +
+ ${isMarkdown ? ` +
+ + +
+ ` : ''} + `; + + previewContent.innerHTML = '
Loading file...
'; + + try { + const response = await fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`); + const data = await response.json(); + + if (data.error) { + previewContent.innerHTML = `
${escapeHtml(data.error)}
`; + return; + } + + if (data.isMarkdown) { + // Render markdown with tabs content (tabs are in header) + const rendered = marked.parse(data.content); + previewContent.innerHTML = ` +
+
${rendered}
+
+
+
${escapeHtml(data.content)}
+
+ `; + } else { + // Render code with syntax highlighting + previewContent.innerHTML = ` +
+ ${data.language} + ${data.lines} lines + ${formatFileSize(data.size)} +
+
${escapeHtml(data.content)}
+ `; + } + + // Apply syntax highlighting if hljs is available + if (typeof hljs !== 'undefined') { + previewContent.querySelectorAll('pre code').forEach(block => { + hljs.highlightElement(block); + }); + } + + } catch (error) { + previewContent.innerHTML = `
Failed to load: ${escapeHtml(error.message)}
`; + } +} + +/** + * Switch preview tab (for markdown files) + */ +function switchPreviewTab(btn, tabName) { + const previewPanel = btn.closest('.explorer-preview-panel'); + const contentArea = previewPanel.querySelector('.explorer-preview-content'); + + // Update tab buttons in header + previewPanel.querySelectorAll('.preview-tab').forEach(t => t.classList.remove('active')); + btn.classList.add('active'); + + // Update tab content + contentArea.querySelectorAll('.preview-tab-content').forEach(c => c.classList.remove('show')); + contentArea.querySelector(`[data-tab="${tabName}"]`).classList.add('show'); +} + +/** + * Format file size + */ +function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; +} + +/** + * Refresh the explorer tree + */ +async function refreshExplorerTree() { + const btn = document.querySelector('.explorer-refresh-btn'); + if (btn) { + btn.classList.add('refreshing'); + } + + explorerExpandedDirs.clear(); + await loadExplorerTree(explorerCurrentPath); + + if (btn) { + btn.classList.remove('refreshing'); + } +} + +/** + * Open Update CLAUDE.md modal + */ +function openUpdateClaudeMdModal(folderPath) { + const modal = document.getElementById('updateClaudeMdModal'); + if (!modal) return; + + // Set folder path + document.getElementById('claudeMdTargetPath').textContent = folderPath; + document.getElementById('claudeMdTargetPath').dataset.path = folderPath; + + // Reset form + document.getElementById('claudeMdTool').value = 'gemini'; + document.getElementById('claudeMdStrategy').value = 'single-layer'; + + // Show modal + modal.classList.remove('hidden'); +} + +/** + * Close Update CLAUDE.md modal + */ +function closeUpdateClaudeMdModal() { + const modal = document.getElementById('updateClaudeMdModal'); + if (modal) { + modal.classList.add('hidden'); + } +} + +/** + * Execute Update CLAUDE.md + */ +async function executeUpdateClaudeMd() { + const pathEl = document.getElementById('claudeMdTargetPath'); + const toolSelect = document.getElementById('claudeMdTool'); + const strategySelect = document.getElementById('claudeMdStrategy'); + const executeBtn = document.getElementById('claudeMdExecuteBtn'); + const statusEl = document.getElementById('claudeMdStatus'); + + const path = pathEl.dataset.path; + const tool = toolSelect.value; + const strategy = strategySelect.value; + + // Update UI + executeBtn.disabled = true; + executeBtn.textContent = 'Updating...'; + statusEl.innerHTML = '
âŗ Running update...
'; + + try { + const response = await fetch('/api/update-claude-md', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, tool, strategy }) + }); + + const result = await response.json(); + + if (result.success) { + statusEl.innerHTML = `
${escapeHtml(result.message)}
`; + // Refresh tree to update CLAUDE.md indicators + await refreshExplorerTree(); + if (typeof lucide !== 'undefined') lucide.createIcons(); + } else { + statusEl.innerHTML = `
${escapeHtml(result.error || 'Update failed')}
`; + if (typeof lucide !== 'undefined') lucide.createIcons(); + } + + } catch (error) { + statusEl.innerHTML = `
${escapeHtml(error.message)}
`; + if (typeof lucide !== 'undefined') lucide.createIcons(); + } finally { + executeBtn.disabled = false; + executeBtn.textContent = 'Execute'; + } +} + +// ============================================ +// TASK QUEUE FUNCTIONS +// ============================================ + +/** + * Toggle task queue panel visibility + */ +function toggleTaskQueue() { + isTaskQueueVisible = !isTaskQueueVisible; + const panel = document.getElementById('taskQueuePanel'); + const fab = document.querySelector('.explorer-fab'); + + if (isTaskQueueVisible) { + panel.classList.add('show'); + fab.classList.add('active'); + } else { + panel.classList.remove('show'); + fab.classList.remove('active'); + } +} + +/** + * Update the FAB badge count + */ +function updateFabBadge() { + const badge = document.getElementById('fabBadge'); + if (badge) { + const pendingCount = updateTaskQueue.filter(t => t.status === 'pending' || t.status === 'running').length; + badge.textContent = pendingCount || ''; + badge.style.display = pendingCount > 0 ? 'flex' : 'none'; + } +} + +/** + * Open add task modal + */ +function openAddTaskModal() { + const modal = document.getElementById('updateClaudeMdModal'); + if (!modal) return; + + // Set default path to current project + document.getElementById('claudeMdTargetPath').textContent = explorerCurrentPath; + document.getElementById('claudeMdTargetPath').dataset.path = explorerCurrentPath; + + // Reset form + document.getElementById('claudeMdTool').value = 'gemini'; + document.getElementById('claudeMdStrategy').value = 'single-layer'; + document.getElementById('claudeMdStatus').innerHTML = ''; + + // Change button to "Add to Queue" + const executeBtn = document.getElementById('claudeMdExecuteBtn'); + executeBtn.textContent = 'Add to Queue'; + executeBtn.onclick = addTaskToQueue; + + modal.classList.remove('hidden'); +} + +/** + * Add task to queue from modal + */ +function addTaskToQueue() { + const pathEl = document.getElementById('claudeMdTargetPath'); + const toolSelect = document.getElementById('claudeMdTool'); + const strategySelect = document.getElementById('claudeMdStrategy'); + + const path = pathEl.dataset.path; + const tool = toolSelect.value; + const strategy = strategySelect.value; + + addUpdateTask(path, tool, strategy); + closeUpdateClaudeMdModal(); + + // Show task queue + if (!isTaskQueueVisible) { + toggleTaskQueue(); + } +} + +/** + * Add a task to the update queue + */ +function addUpdateTask(path, tool = 'gemini', strategy = 'single-layer') { + const task = { + id: Date.now(), + path, + tool, + strategy, + status: 'pending', // pending, running, completed, failed + message: '', + addedAt: new Date().toISOString() + }; + + updateTaskQueue.push(task); + renderTaskQueue(); + updateFabBadge(); + + // Enable start button + document.getElementById('startQueueBtn').disabled = false; +} + +/** + * Add task from folder context (right-click or button) + */ +function addFolderToQueue(folderPath, strategy = 'single-layer') { + addUpdateTask(folderPath, 'gemini', strategy); + + // Show task queue if not visible + if (!isTaskQueueVisible) { + toggleTaskQueue(); + } +} + +/** + * Render the task queue list + */ +function renderTaskQueue() { + const listEl = document.getElementById('taskQueueList'); + + if (updateTaskQueue.length === 0) { + listEl.innerHTML = ` +
+ No tasks in queue +

Right-click a folder or click "Add Task" to queue CLAUDE.md updates

+
+ `; + return; + } + + listEl.innerHTML = updateTaskQueue.map(task => { + const folderName = task.path.split('/').pop() || task.path; + const strategyLabel = task.strategy === 'multi-layer' + ? ' With subdirs' + : ' Current only'; + const statusIcon = { + 'pending': '', + 'running': '', + 'completed': '', + 'failed': '' + }[task.status]; + + return ` +
+
+ ${statusIcon} + ${escapeHtml(folderName)} + ${task.status === 'pending' ? ` + + ` : ''} +
+
+ ${strategyLabel} + ${task.tool} +
+ ${task.message ? `
${escapeHtml(task.message)}
` : ''} +
+ `; + }).join(''); + + // Reinitialize Lucide icons + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +/** + * Remove a task from queue + */ +function removeTask(taskId) { + updateTaskQueue = updateTaskQueue.filter(t => t.id !== taskId); + renderTaskQueue(); + updateFabBadge(); + + // Disable start button if no pending tasks + const hasPending = updateTaskQueue.some(t => t.status === 'pending'); + document.getElementById('startQueueBtn').disabled = !hasPending; +} + +/** + * Clear completed/failed tasks + */ +function clearCompletedTasks() { + updateTaskQueue = updateTaskQueue.filter(t => t.status === 'pending' || t.status === 'running'); + renderTaskQueue(); + updateFabBadge(); +} + +/** + * Start processing task queue + */ +async function startTaskQueue() { + if (isTaskRunning) return; + + const pendingTasks = updateTaskQueue.filter(t => t.status === 'pending'); + if (pendingTasks.length === 0) return; + + isTaskRunning = true; + document.getElementById('startQueueBtn').disabled = true; + + addGlobalNotification('info', `Starting ${pendingTasks.length} task(s)...`, null, 'Explorer'); + + let successCount = 0; + let failCount = 0; + + for (const task of pendingTasks) { + const folderName = task.path.split('/').pop() || task.path; + + // Update status to running + task.status = 'running'; + task.message = 'Processing...'; + renderTaskQueue(); + + addGlobalNotification('info', `Processing: ${folderName}`, `Strategy: ${task.strategy}, Tool: ${task.tool}`, 'Explorer'); + + try { + const response = await fetch('/api/update-claude-md', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + path: task.path, + tool: task.tool, + strategy: task.strategy + }) + }); + + const result = await response.json(); + + if (result.success) { + task.status = 'completed'; + task.message = 'Updated successfully'; + successCount++; + addGlobalNotification('success', `Completed: ${folderName}`, result.message, 'Explorer'); + } else { + task.status = 'failed'; + task.message = result.error || 'Update failed'; + failCount++; + addGlobalNotification('error', `Failed: ${folderName}`, result.error || 'Unknown error', 'Explorer'); + } + } catch (error) { + task.status = 'failed'; + task.message = error.message; + failCount++; + addGlobalNotification('error', `Error: ${folderName}`, error.message, 'Explorer'); + } + + renderTaskQueue(); + updateFabBadge(); + } + + isTaskRunning = false; + + // Summary notification + addGlobalNotification( + failCount === 0 ? 'success' : 'warning', + `Queue completed: ${successCount} succeeded, ${failCount} failed`, + null, + 'Explorer' + ); + + // Re-enable start button if there are pending tasks + const hasPending = updateTaskQueue.some(t => t.status === 'pending'); + document.getElementById('startQueueBtn').disabled = !hasPending; + + // Refresh tree to show updated CLAUDE.md files + await refreshExplorerTree(); +} + diff --git a/ccw/src/templates/dashboard.html b/ccw/src/templates/dashboard.html index 7a1e9f12..2de926a6 100644 --- a/ccw/src/templates/dashboard.html +++ b/ccw/src/templates/dashboard.html @@ -190,6 +190,34 @@ .task-detail-drawer.open { transform: translateX(0); } .drawer-overlay.active { display: block; } + /* Unified Icon System */ + .nav-icon { + width: 18px; + height: 18px; + flex-shrink: 0; + stroke-width: 2; + } + .nav-section-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + stroke-width: 2; + opacity: 0.8; + } + .sidebar.collapsed .nav-icon { + width: 20px; + height: 20px; + } + + /* Icon Animations */ + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + .animate-spin { + animation: spin 1s linear infinite; + } + /* Injected from dashboard-css/*.css modules */ {{CSS_CONTENT}} @@ -199,9 +227,11 @@
- +
- ⚡ +
@@ -223,7 +253,8 @@
@@ -240,7 +271,10 @@ - +
@@ -255,36 +289,40 @@
- đŸ—ī¸ + Project
    +
- 📁 + Sessions
    @@ -294,17 +332,17 @@
    - ⚡ + Lite Tasks
      @@ -314,12 +352,12 @@
      - 🔌 + MCP Servers
        @@ -329,12 +367,12 @@
        - đŸĒ + Hooks
          @@ -345,7 +383,7 @@
          @@ -358,22 +396,22 @@
          -
          📊
          +
          0
          Total Sessions
          -
          đŸŸĸ
          +
          0
          Active Sessions
          -
          📋
          +
          0
          Total Tasks
          -
          ✅
          +
          0
          Completed Tasks
          @@ -386,7 +424,7 @@ @@ -421,7 +459,7 @@

          All Sessions

          - 🔍 +
          @@ -618,10 +656,53 @@
          + + + + + + + + +