Skip to content

Commit 5f31c9a

Browse files
author
catlog22
committed
feat(dashboard): unify icons with Lucide Icons library
- Introduce Lucide Icons via CDN for consistent SVG icons - Replace emoji icons with Lucide SVG icons in sidebar navigation - Fix Sessions/Explorer icon confusion (πŸ“/πŸ“‚ β†’ history/folder-tree) - Update top bar icons (logo, theme toggle, search, refresh) - Update stats section icons with colored Lucide icons - Add icon animations support (animate-spin for loading states) - Update Explorer view with Lucide folder/file icons - Support dark/light theme icon adaptation Icon mapping: - Explorer: folder-tree (was πŸ“‚) - Sessions: history (was πŸ“) - Overview: bar-chart-3 - Active: play-circle - Archived: archive - Lite Plan: file-edit - Lite Fix: wrench - MCP Servers: plug - Hooks: webhook
1 parent 818d9f3 commit 5f31c9a

File tree

9 files changed

+2882
-27
lines changed

9 files changed

+2882
-27
lines changed

β€Žccw/src/core/server.jsβ€Ž

Lines changed: 342 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import http from 'http';
22
import { URL } from 'url';
3-
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, promises as fsPromises } from 'fs';
3+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, statSync, promises as fsPromises } from 'fs';
44
import { join, dirname } from 'path';
55
import { homedir } from 'os';
66
import { createHash } from 'crypto';
@@ -44,7 +44,8 @@ const MODULE_CSS_FILES = [
4444
'05-context.css',
4545
'06-cards.css',
4646
'07-managers.css',
47-
'08-review.css'
47+
'08-review.css',
48+
'09-explorer.css'
4849
];
4950

5051
/**
@@ -84,6 +85,7 @@ const MODULE_FILES = [
8485
'components/sidebar.js',
8586
'components/carousel.js',
8687
'components/notifications.js',
88+
'components/global-notifications.js',
8789
'components/mcp-manager.js',
8890
'components/hook-manager.js',
8991
'components/_exp_helpers.js',
@@ -102,6 +104,7 @@ const MODULE_FILES = [
102104
'views/fix-session.js',
103105
'views/mcp-manager.js',
104106
'views/hook-manager.js',
107+
'views/explorer.js',
105108
'main.js'
106109
];
107110
/**
@@ -399,6 +402,41 @@ export async function startServer(options = {}) {
399402
return;
400403
}
401404

405+
// API: List directory files with .gitignore filtering (Explorer view)
406+
if (pathname === '/api/files') {
407+
const dirPath = url.searchParams.get('path') || initialPath;
408+
const filesData = await listDirectoryFiles(dirPath);
409+
res.writeHead(200, { 'Content-Type': 'application/json' });
410+
res.end(JSON.stringify(filesData));
411+
return;
412+
}
413+
414+
// API: Get file content for preview (Explorer view)
415+
if (pathname === '/api/file-content') {
416+
const filePath = url.searchParams.get('path');
417+
if (!filePath) {
418+
res.writeHead(400, { 'Content-Type': 'application/json' });
419+
res.end(JSON.stringify({ error: 'File path is required' }));
420+
return;
421+
}
422+
const fileData = await getFileContent(filePath);
423+
res.writeHead(fileData.error ? 404 : 200, { 'Content-Type': 'application/json' });
424+
res.end(JSON.stringify(fileData));
425+
return;
426+
}
427+
428+
// API: Update CLAUDE.md using CLI tools (Explorer view)
429+
if (pathname === '/api/update-claude-md' && req.method === 'POST') {
430+
handlePostRequest(req, res, async (body) => {
431+
const { path: targetPath, tool = 'gemini', strategy = 'single-layer' } = body;
432+
if (!targetPath) {
433+
return { error: 'path is required', status: 400 };
434+
}
435+
return await triggerUpdateClaudeMd(targetPath, tool, strategy);
436+
});
437+
return;
438+
}
439+
402440
// Serve dashboard HTML
403441
if (pathname === '/' || pathname === '/index.html') {
404442
const html = generateServerDashboard(initialPath);
@@ -1521,3 +1559,305 @@ function deleteHookFromSettings(projectPath, scope, event, hookIndex) {
15211559
return { error: error.message };
15221560
}
15231561
}
1562+
1563+
// ========================================
1564+
// Explorer View Functions
1565+
// ========================================
1566+
1567+
// Directories to always exclude from file tree
1568+
const EXPLORER_EXCLUDE_DIRS = [
1569+
'.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env',
1570+
'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache',
1571+
'coverage', '.nyc_output', 'logs', 'tmp', 'temp', '.next',
1572+
'.nuxt', '.output', '.turbo', '.parcel-cache'
1573+
];
1574+
1575+
// File extensions to language mapping for syntax highlighting
1576+
const EXT_TO_LANGUAGE = {
1577+
'.js': 'javascript',
1578+
'.jsx': 'javascript',
1579+
'.ts': 'typescript',
1580+
'.tsx': 'typescript',
1581+
'.py': 'python',
1582+
'.rb': 'ruby',
1583+
'.java': 'java',
1584+
'.go': 'go',
1585+
'.rs': 'rust',
1586+
'.c': 'c',
1587+
'.cpp': 'cpp',
1588+
'.h': 'c',
1589+
'.hpp': 'cpp',
1590+
'.cs': 'csharp',
1591+
'.php': 'php',
1592+
'.swift': 'swift',
1593+
'.kt': 'kotlin',
1594+
'.scala': 'scala',
1595+
'.sh': 'bash',
1596+
'.bash': 'bash',
1597+
'.zsh': 'bash',
1598+
'.ps1': 'powershell',
1599+
'.sql': 'sql',
1600+
'.html': 'html',
1601+
'.htm': 'html',
1602+
'.css': 'css',
1603+
'.scss': 'scss',
1604+
'.sass': 'sass',
1605+
'.less': 'less',
1606+
'.json': 'json',
1607+
'.xml': 'xml',
1608+
'.yaml': 'yaml',
1609+
'.yml': 'yaml',
1610+
'.toml': 'toml',
1611+
'.ini': 'ini',
1612+
'.cfg': 'ini',
1613+
'.conf': 'nginx',
1614+
'.md': 'markdown',
1615+
'.markdown': 'markdown',
1616+
'.txt': 'plaintext',
1617+
'.log': 'plaintext',
1618+
'.env': 'bash',
1619+
'.dockerfile': 'dockerfile',
1620+
'.vue': 'html',
1621+
'.svelte': 'html'
1622+
};
1623+
1624+
/**
1625+
* Parse .gitignore file and return patterns
1626+
* @param {string} gitignorePath - Path to .gitignore file
1627+
* @returns {string[]} Array of gitignore patterns
1628+
*/
1629+
function parseGitignore(gitignorePath) {
1630+
try {
1631+
if (!existsSync(gitignorePath)) return [];
1632+
const content = readFileSync(gitignorePath, 'utf8');
1633+
return content
1634+
.split('\n')
1635+
.map(line => line.trim())
1636+
.filter(line => line && !line.startsWith('#'));
1637+
} catch {
1638+
return [];
1639+
}
1640+
}
1641+
1642+
/**
1643+
* Check if a file/directory should be ignored based on gitignore patterns
1644+
* Simple pattern matching (supports basic glob patterns)
1645+
* @param {string} name - File or directory name
1646+
* @param {string[]} patterns - Gitignore patterns
1647+
* @param {boolean} isDirectory - Whether the entry is a directory
1648+
* @returns {boolean}
1649+
*/
1650+
function shouldIgnore(name, patterns, isDirectory) {
1651+
// Always exclude certain directories
1652+
if (isDirectory && EXPLORER_EXCLUDE_DIRS.includes(name)) {
1653+
return true;
1654+
}
1655+
1656+
// Skip hidden files/directories (starting with .)
1657+
if (name.startsWith('.') && name !== '.claude' && name !== '.workflow') {
1658+
return true;
1659+
}
1660+
1661+
for (const pattern of patterns) {
1662+
let p = pattern;
1663+
1664+
// Handle negation patterns (we skip them for simplicity)
1665+
if (p.startsWith('!')) continue;
1666+
1667+
// Handle directory-only patterns
1668+
if (p.endsWith('/')) {
1669+
if (!isDirectory) continue;
1670+
p = p.slice(0, -1);
1671+
}
1672+
1673+
// Simple pattern matching
1674+
if (p === name) return true;
1675+
1676+
// Handle wildcard patterns
1677+
if (p.includes('*')) {
1678+
const regex = new RegExp('^' + p.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
1679+
if (regex.test(name)) return true;
1680+
}
1681+
1682+
// Handle extension patterns like *.log
1683+
if (p.startsWith('*.')) {
1684+
const ext = p.slice(1);
1685+
if (name.endsWith(ext)) return true;
1686+
}
1687+
}
1688+
1689+
return false;
1690+
}
1691+
1692+
/**
1693+
* List directory files with .gitignore filtering
1694+
* @param {string} dirPath - Directory path to list
1695+
* @returns {Promise<Object>}
1696+
*/
1697+
async function listDirectoryFiles(dirPath) {
1698+
try {
1699+
// Normalize path
1700+
let normalizedPath = dirPath.replace(/\\/g, '/');
1701+
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
1702+
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
1703+
}
1704+
1705+
if (!existsSync(normalizedPath)) {
1706+
return { error: 'Directory not found', files: [] };
1707+
}
1708+
1709+
if (!statSync(normalizedPath).isDirectory()) {
1710+
return { error: 'Not a directory', files: [] };
1711+
}
1712+
1713+
// Parse .gitignore patterns
1714+
const gitignorePath = join(normalizedPath, '.gitignore');
1715+
const gitignorePatterns = parseGitignore(gitignorePath);
1716+
1717+
// Read directory entries
1718+
const entries = readdirSync(normalizedPath, { withFileTypes: true });
1719+
1720+
const files = [];
1721+
for (const entry of entries) {
1722+
const isDirectory = entry.isDirectory();
1723+
1724+
// Check if should be ignored
1725+
if (shouldIgnore(entry.name, gitignorePatterns, isDirectory)) {
1726+
continue;
1727+
}
1728+
1729+
const entryPath = join(normalizedPath, entry.name);
1730+
const fileInfo = {
1731+
name: entry.name,
1732+
type: isDirectory ? 'directory' : 'file',
1733+
path: entryPath.replace(/\\/g, '/')
1734+
};
1735+
1736+
// Check if directory has CLAUDE.md
1737+
if (isDirectory) {
1738+
const claudeMdPath = join(entryPath, 'CLAUDE.md');
1739+
fileInfo.hasClaudeMd = existsSync(claudeMdPath);
1740+
}
1741+
1742+
files.push(fileInfo);
1743+
}
1744+
1745+
// Sort: directories first, then alphabetically
1746+
files.sort((a, b) => {
1747+
if (a.type === 'directory' && b.type !== 'directory') return -1;
1748+
if (a.type !== 'directory' && b.type === 'directory') return 1;
1749+
return a.name.localeCompare(b.name);
1750+
});
1751+
1752+
return {
1753+
path: normalizedPath.replace(/\\/g, '/'),
1754+
files,
1755+
gitignorePatterns
1756+
};
1757+
} catch (error) {
1758+
console.error('Error listing directory:', error);
1759+
return { error: error.message, files: [] };
1760+
}
1761+
}
1762+
1763+
/**
1764+
* Get file content for preview
1765+
* @param {string} filePath - Path to file
1766+
* @returns {Promise<Object>}
1767+
*/
1768+
async function getFileContent(filePath) {
1769+
try {
1770+
// Normalize path
1771+
let normalizedPath = filePath.replace(/\\/g, '/');
1772+
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
1773+
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
1774+
}
1775+
1776+
if (!existsSync(normalizedPath)) {
1777+
return { error: 'File not found' };
1778+
}
1779+
1780+
const stats = statSync(normalizedPath);
1781+
if (stats.isDirectory()) {
1782+
return { error: 'Cannot read directory' };
1783+
}
1784+
1785+
// Check file size (limit to 1MB for preview)
1786+
if (stats.size > 1024 * 1024) {
1787+
return { error: 'File too large for preview (max 1MB)', size: stats.size };
1788+
}
1789+
1790+
// Read file content
1791+
const content = readFileSync(normalizedPath, 'utf8');
1792+
const ext = normalizedPath.substring(normalizedPath.lastIndexOf('.')).toLowerCase();
1793+
const language = EXT_TO_LANGUAGE[ext] || 'plaintext';
1794+
const isMarkdown = ext === '.md' || ext === '.markdown';
1795+
const fileName = normalizedPath.split('/').pop();
1796+
1797+
return {
1798+
content,
1799+
language,
1800+
isMarkdown,
1801+
fileName,
1802+
path: normalizedPath,
1803+
size: stats.size,
1804+
lines: content.split('\n').length
1805+
};
1806+
} catch (error) {
1807+
console.error('Error reading file:', error);
1808+
return { error: error.message };
1809+
}
1810+
}
1811+
1812+
/**
1813+
* Trigger update-module-claude tool
1814+
* @param {string} targetPath - Directory path to update
1815+
* @param {string} tool - CLI tool to use (gemini, qwen, codex)
1816+
* @param {string} strategy - Update strategy (single-layer, multi-layer)
1817+
* @returns {Promise<Object>}
1818+
*/
1819+
async function triggerUpdateClaudeMd(targetPath, tool, strategy) {
1820+
const { execSync } = await import('child_process');
1821+
1822+
try {
1823+
// Normalize path
1824+
let normalizedPath = targetPath.replace(/\\/g, '/');
1825+
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
1826+
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
1827+
}
1828+
1829+
if (!existsSync(normalizedPath)) {
1830+
return { error: 'Directory not found' };
1831+
}
1832+
1833+
if (!statSync(normalizedPath).isDirectory()) {
1834+
return { error: 'Not a directory' };
1835+
}
1836+
1837+
// Build ccw tool command
1838+
const ccwBin = join(import.meta.dirname, '../../bin/ccw.js');
1839+
const command = `node "${ccwBin}" tool update_module_claude --strategy="${strategy}" --path="${normalizedPath}" --tool="${tool}"`;
1840+
1841+
console.log(`[Explorer] Running: ${command}`);
1842+
1843+
const output = execSync(command, {
1844+
encoding: 'utf8',
1845+
timeout: 300000, // 5 minutes
1846+
cwd: normalizedPath
1847+
});
1848+
1849+
return {
1850+
success: true,
1851+
message: `CLAUDE.md updated successfully using ${tool} (${strategy})`,
1852+
output,
1853+
path: normalizedPath
1854+
};
1855+
} catch (error) {
1856+
console.error('Error updating CLAUDE.md:', error);
1857+
return {
1858+
success: false,
1859+
error: error.message,
1860+
output: error.stdout || error.stderr || ''
1861+
};
1862+
}
1863+
}

0 commit comments

Comments
Β (0)