mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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
This commit is contained in:
@@ -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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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 || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
1353
ccw/src/templates/dashboard-css/09-explorer.css
Normal file
1353
ccw/src/templates/dashboard-css/09-explorer.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 = `
|
||||
<div class="global-notif-fab" id="globalNotificationFab" onclick="toggleGlobalNotifications()" title="Notifications">
|
||||
<span class="fab-icon">🔔</span>
|
||||
<span class="fab-badge" id="globalNotifBadge">0</span>
|
||||
</div>
|
||||
|
||||
<div class="global-notif-panel" id="globalNotificationPanel">
|
||||
<div class="global-notif-header">
|
||||
<span class="global-notif-title">🔔 Notifications</span>
|
||||
<button class="global-notif-close" onclick="toggleGlobalNotifications()">×</button>
|
||||
</div>
|
||||
<div class="global-notif-actions">
|
||||
<button class="notif-action-btn" onclick="clearGlobalNotifications()">
|
||||
<span>🗑️</span> Clear All
|
||||
</button>
|
||||
</div>
|
||||
<div class="global-notif-list" id="globalNotificationList">
|
||||
<div class="global-notif-empty">
|
||||
<span>No notifications</span>
|
||||
<p>System events and task updates will appear here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<span class="toast-icon">${typeIcon}</span>
|
||||
<span class="toast-message">${escapeHtml(notification.message)}</span>
|
||||
`;
|
||||
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 = `
|
||||
<div class="global-notif-empty">
|
||||
<span>No notifications</span>
|
||||
<p>System events and task updates will appear here</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = globalNotificationQueue.map(notif => {
|
||||
const typeIcon = {
|
||||
'info': 'ℹ️',
|
||||
'success': '✅',
|
||||
'warning': '⚠️',
|
||||
'error': '❌'
|
||||
}[notif.type] || 'ℹ️';
|
||||
|
||||
const time = formatNotifTime(notif.timestamp);
|
||||
const sourceLabel = notif.source ? `<span class="notif-source">${notif.source}</span>` : '';
|
||||
|
||||
return `
|
||||
<div class="global-notif-item type-${notif.type} ${notif.read ? 'read' : ''}" data-id="${notif.id}">
|
||||
<div class="notif-item-header">
|
||||
<span class="notif-icon">${typeIcon}</span>
|
||||
<span class="notif-message">${escapeHtml(notif.message)}</span>
|
||||
${sourceLabel}
|
||||
</div>
|
||||
${notif.details ? `<div class="notif-details">${escapeHtml(notif.details)}</div>` : ''}
|
||||
<div class="notif-meta">
|
||||
<span class="notif-time">${time}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).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();
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
821
ccw/src/templates/dashboard-js/views/explorer.js
Normal file
821
ccw/src/templates/dashboard-js/views/explorer.js
Normal file
@@ -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 = `
|
||||
<div class="explorer-container">
|
||||
<!-- Left Panel: File Tree -->
|
||||
<div class="explorer-tree-panel">
|
||||
<div class="explorer-tree-header">
|
||||
<div class="explorer-tree-title">
|
||||
<i data-lucide="folder-tree" class="explorer-icon"></i>
|
||||
<span class="explorer-title-text">Explorer</span>
|
||||
</div>
|
||||
<button class="explorer-refresh-btn" onclick="refreshExplorerTree()" title="Refresh">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="explorer-tree-content" id="explorerTreeContent">
|
||||
<div class="explorer-loading">Loading file tree...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Preview -->
|
||||
<div class="explorer-preview-panel">
|
||||
<div class="explorer-preview-header" id="explorerPreviewHeader">
|
||||
<span class="preview-filename">Select a file to preview</span>
|
||||
</div>
|
||||
<div class="explorer-preview-content" id="explorerPreviewContent">
|
||||
<div class="explorer-preview-empty">
|
||||
<div class="preview-empty-icon"><i data-lucide="file-text" class="w-12 h-12"></i></div>
|
||||
<div class="preview-empty-text">Select a file from the tree to preview its contents</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Action Button -->
|
||||
<div class="explorer-fab" onclick="toggleTaskQueue()" title="Task Queue">
|
||||
<span class="fab-icon"><i data-lucide="list-todo" class="w-5 h-5"></i></span>
|
||||
<span class="fab-badge" id="fabBadge">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Task Queue Panel -->
|
||||
<div class="task-queue-panel" id="taskQueuePanel">
|
||||
<div class="task-queue-header">
|
||||
<span class="task-queue-title"><i data-lucide="clipboard-list" class="w-4 h-4 inline-block mr-1"></i> Update Tasks</span>
|
||||
<button class="task-queue-close" onclick="toggleTaskQueue()">×</button>
|
||||
</div>
|
||||
<div class="task-queue-actions">
|
||||
<button class="queue-action-btn" onclick="openAddTaskModal()" title="Add update task">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i> Add
|
||||
</button>
|
||||
<button class="queue-action-btn queue-start-btn" onclick="startTaskQueue()" id="startQueueBtn" disabled>
|
||||
<i data-lucide="play" class="w-4 h-4"></i> Start
|
||||
</button>
|
||||
<button class="queue-action-btn queue-clear-btn" onclick="clearCompletedTasks()" title="Clear completed">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="task-queue-list" id="taskQueueList">
|
||||
<div class="task-queue-empty">
|
||||
<span>No tasks in queue</span>
|
||||
<p>Hover folder and click <i data-lucide="file" class="w-3 h-3 inline"></i> or <i data-lucide="folder" class="w-3 h-3 inline"></i> to add tasks</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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 = `<div class="explorer-error">${escapeHtml(data.error)}</div>`;
|
||||
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 = `<div class="explorer-error">Failed to load: ${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a level of the file tree
|
||||
*/
|
||||
function renderTreeLevel(files, parentPath, depth) {
|
||||
if (!files || files.length === 0) {
|
||||
return `<div class="tree-empty" style="padding-left: ${depth * 16 + 8}px">Empty directory</div>`;
|
||||
}
|
||||
|
||||
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 ? '<i data-lucide="chevron-down" class="w-3 h-3"></i>' : '<i data-lucide="chevron-right" class="w-3 h-3"></i>';
|
||||
return `
|
||||
<div class="tree-item tree-folder ${isExpanded ? 'expanded' : ''} ${file.hasClaudeMd ? 'has-claude-md' : ''}" data-path="${escapeHtml(file.path)}" data-type="directory">
|
||||
<div class="tree-item-row ${isSelected ? 'selected' : ''}" style="padding-left: ${depth * 16}px">
|
||||
<span class="tree-chevron">${chevronIcon}</span>
|
||||
<span class="tree-icon">${folderIcon}</span>
|
||||
<span class="tree-name">${escapeHtml(file.name)}</span>
|
||||
${file.hasClaudeMd ? `
|
||||
<span class="claude-md-badge" title="Contains CLAUDE.md documentation">
|
||||
<span class="badge-icon"><i data-lucide="file-check" class="w-3 h-3"></i></span>
|
||||
<span class="badge-text">DOC</span>
|
||||
</span>
|
||||
` : ''}
|
||||
<div class="tree-folder-actions">
|
||||
<button class="tree-update-btn" onclick="event.stopPropagation(); addFolderToQueue('${escapeHtml(file.path)}', 'single-layer')" title="Update CLAUDE.md (current folder only)">
|
||||
<span class="update-icon"><i data-lucide="file" class="w-3.5 h-3.5"></i></span>
|
||||
</button>
|
||||
<button class="tree-update-btn tree-update-multi" onclick="event.stopPropagation(); addFolderToQueue('${escapeHtml(file.path)}', 'multi-layer')" title="Update CLAUDE.md (with subdirectories)">
|
||||
<span class="update-icon"><i data-lucide="folder-tree" class="w-3.5 h-3.5"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tree-children ${isExpanded ? 'show' : ''}" id="children-${btoa(file.path).replace(/[^a-zA-Z0-9]/g, '')}">
|
||||
${isExpanded ? '' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} 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 `
|
||||
<div class="tree-item tree-file ${isSelected ? 'selected' : ''} ${isClaudeMd ? 'is-claude-md' : ''}" data-path="${escapeHtml(file.path)}" data-type="file">
|
||||
<div class="tree-item-row" style="padding-left: ${depth * 16}px">
|
||||
<span class="tree-chevron-spacer"></span>
|
||||
<span class="tree-icon">${isClaudeMd ? '<span class="file-icon file-icon-claude"><i data-lucide="bot" class="w-3 h-3"></i></span>' : fileIcon}</span>
|
||||
<span class="tree-name">${escapeHtml(file.name)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file icon based on extension - using colored SVG icons for better distinction
|
||||
*/
|
||||
function getFileIcon(ext) {
|
||||
const iconMap = {
|
||||
// JavaScript/TypeScript - distinct colors
|
||||
'js': '<span class="file-icon file-icon-js">JS</span>',
|
||||
'mjs': '<span class="file-icon file-icon-js">JS</span>',
|
||||
'cjs': '<span class="file-icon file-icon-js">JS</span>',
|
||||
'jsx': '<span class="file-icon file-icon-jsx">JSX</span>',
|
||||
'ts': '<span class="file-icon file-icon-ts">TS</span>',
|
||||
'tsx': '<span class="file-icon file-icon-tsx">TSX</span>',
|
||||
|
||||
// Python
|
||||
'py': '<span class="file-icon file-icon-py">PY</span>',
|
||||
'pyw': '<span class="file-icon file-icon-py">PY</span>',
|
||||
|
||||
// Other languages
|
||||
'go': '<span class="file-icon file-icon-go">GO</span>',
|
||||
'rs': '<span class="file-icon file-icon-rs">RS</span>',
|
||||
'java': '<span class="file-icon file-icon-java">JV</span>',
|
||||
'rb': '<span class="file-icon file-icon-rb">RB</span>',
|
||||
'php': '<span class="file-icon file-icon-php">PHP</span>',
|
||||
'c': '<span class="file-icon file-icon-c">C</span>',
|
||||
'cpp': '<span class="file-icon file-icon-cpp">C++</span>',
|
||||
'h': '<span class="file-icon file-icon-h">H</span>',
|
||||
'cs': '<span class="file-icon file-icon-cs">C#</span>',
|
||||
'swift': '<span class="file-icon file-icon-swift">SW</span>',
|
||||
'kt': '<span class="file-icon file-icon-kt">KT</span>',
|
||||
|
||||
// Web
|
||||
'html': '<span class="file-icon file-icon-html">HTML</span>',
|
||||
'htm': '<span class="file-icon file-icon-html">HTML</span>',
|
||||
'css': '<span class="file-icon file-icon-css">CSS</span>',
|
||||
'scss': '<span class="file-icon file-icon-scss">SCSS</span>',
|
||||
'sass': '<span class="file-icon file-icon-scss">SASS</span>',
|
||||
'less': '<span class="file-icon file-icon-less">LESS</span>',
|
||||
'vue': '<span class="file-icon file-icon-vue">VUE</span>',
|
||||
'svelte': '<span class="file-icon file-icon-svelte">SV</span>',
|
||||
|
||||
// Config/Data
|
||||
'json': '<span class="file-icon file-icon-json">{}</span>',
|
||||
'yaml': '<span class="file-icon file-icon-yaml">YML</span>',
|
||||
'yml': '<span class="file-icon file-icon-yaml">YML</span>',
|
||||
'xml': '<span class="file-icon file-icon-xml">XML</span>',
|
||||
'toml': '<span class="file-icon file-icon-toml">TML</span>',
|
||||
'ini': '<span class="file-icon file-icon-ini">INI</span>',
|
||||
'env': '<span class="file-icon file-icon-env">ENV</span>',
|
||||
|
||||
// Documentation
|
||||
'md': '<span class="file-icon file-icon-md">MD</span>',
|
||||
'markdown': '<span class="file-icon file-icon-md">MD</span>',
|
||||
'txt': '<span class="file-icon file-icon-txt">TXT</span>',
|
||||
'log': '<span class="file-icon file-icon-log">LOG</span>',
|
||||
|
||||
// Shell/Scripts
|
||||
'sh': '<span class="file-icon file-icon-sh">SH</span>',
|
||||
'bash': '<span class="file-icon file-icon-sh">SH</span>',
|
||||
'zsh': '<span class="file-icon file-icon-sh">ZSH</span>',
|
||||
'ps1': '<span class="file-icon file-icon-ps1">PS1</span>',
|
||||
'bat': '<span class="file-icon file-icon-bat">BAT</span>',
|
||||
'cmd': '<span class="file-icon file-icon-bat">CMD</span>',
|
||||
|
||||
// Database
|
||||
'sql': '<span class="file-icon file-icon-sql">SQL</span>',
|
||||
'db': '<span class="file-icon file-icon-db">DB</span>',
|
||||
|
||||
// Docker/Container
|
||||
'dockerfile': '<span class="file-icon file-icon-docker"><i data-lucide="container" class="w-3 h-3"></i></span>',
|
||||
|
||||
// Images
|
||||
'png': '<span class="file-icon file-icon-img">IMG</span>',
|
||||
'jpg': '<span class="file-icon file-icon-img">IMG</span>',
|
||||
'jpeg': '<span class="file-icon file-icon-img">IMG</span>',
|
||||
'gif': '<span class="file-icon file-icon-img">GIF</span>',
|
||||
'svg': '<span class="file-icon file-icon-svg">SVG</span>',
|
||||
'ico': '<span class="file-icon file-icon-img">ICO</span>',
|
||||
|
||||
// Package
|
||||
'lock': '<span class="file-icon file-icon-lock"><i data-lucide="lock" class="w-3 h-3"></i></span>'
|
||||
};
|
||||
|
||||
return iconMap[ext] || '<span class="file-icon file-icon-default"><i data-lucide="file" class="w-3 h-3"></i></span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get folder icon based on folder name and state
|
||||
*/
|
||||
function getFolderIcon(name, isExpanded, hasClaudeMd) {
|
||||
// Only special icon for .workflow folder
|
||||
if (name === '.workflow') {
|
||||
return '<i data-lucide="zap" class="w-4 h-4 text-warning"></i>';
|
||||
}
|
||||
return isExpanded
|
||||
? '<i data-lucide="folder-open" class="w-4 h-4 text-warning"></i>'
|
||||
: '<i data-lucide="folder" class="w-4 h-4 text-warning"></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '<i data-lucide="chevron-right" class="w-3 h-3"></i>';
|
||||
if (folderIcon && !folderElement.querySelector('[data-lucide="zap"]')) {
|
||||
folderIcon.innerHTML = '<i data-lucide="folder" class="w-4 h-4 text-warning"></i>';
|
||||
}
|
||||
} 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 = '<i data-lucide="chevron-down" class="w-3 h-3"></i>';
|
||||
if (folderIcon && !folderElement.querySelector('[data-lucide="zap"]')) {
|
||||
folderIcon.innerHTML = '<i data-lucide="folder-open" class="w-4 h-4 text-warning"></i>';
|
||||
}
|
||||
|
||||
if (!childrenContainer.innerHTML.trim()) {
|
||||
childrenContainer.innerHTML = '<div class="tree-loading">Loading...</div>';
|
||||
|
||||
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 = `<div class="tree-error">Failed to load</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = `
|
||||
<div class="preview-header-left">
|
||||
<span class="preview-filename">${escapeHtml(fileName)}</span>
|
||||
<span class="preview-path" title="${escapeHtml(filePath)}">${escapeHtml(filePath)}</span>
|
||||
</div>
|
||||
${isMarkdown ? `
|
||||
<div class="preview-header-tabs" id="previewHeaderTabs">
|
||||
<button class="preview-tab active" data-tab="rendered" onclick="switchPreviewTab(this, 'rendered')">Preview</button>
|
||||
<button class="preview-tab" data-tab="source" onclick="switchPreviewTab(this, 'source')">Source</button>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
previewContent.innerHTML = '<div class="explorer-loading">Loading file...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
previewContent.innerHTML = `<div class="explorer-error">${escapeHtml(data.error)}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.isMarkdown) {
|
||||
// Render markdown with tabs content (tabs are in header)
|
||||
const rendered = marked.parse(data.content);
|
||||
previewContent.innerHTML = `
|
||||
<div class="preview-tab-content rendered show" data-tab="rendered">
|
||||
<div class="markdown-preview prose">${rendered}</div>
|
||||
</div>
|
||||
<div class="preview-tab-content source" data-tab="source">
|
||||
<pre><code class="language-markdown">${escapeHtml(data.content)}</code></pre>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Render code with syntax highlighting
|
||||
previewContent.innerHTML = `
|
||||
<div class="preview-info">
|
||||
<span class="preview-lang">${data.language}</span>
|
||||
<span class="preview-lines">${data.lines} lines</span>
|
||||
<span class="preview-size">${formatFileSize(data.size)}</span>
|
||||
</div>
|
||||
<pre class="preview-code"><code class="language-${data.language}">${escapeHtml(data.content)}</code></pre>
|
||||
`;
|
||||
}
|
||||
|
||||
// Apply syntax highlighting if hljs is available
|
||||
if (typeof hljs !== 'undefined') {
|
||||
previewContent.querySelectorAll('pre code').forEach(block => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
previewContent.innerHTML = `<div class="explorer-error">Failed to load: ${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '<div class="status-running">⏳ Running update...</div>';
|
||||
|
||||
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 = `<div class="status-success"><i data-lucide="check-circle" class="w-4 h-4 inline text-success"></i> ${escapeHtml(result.message)}</div>`;
|
||||
// Refresh tree to update CLAUDE.md indicators
|
||||
await refreshExplorerTree();
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} else {
|
||||
statusEl.innerHTML = `<div class="status-error"><i data-lucide="x-circle" class="w-4 h-4 inline text-destructive"></i> ${escapeHtml(result.error || 'Update failed')}</div>`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
statusEl.innerHTML = `<div class="status-error"><i data-lucide="x-circle" class="w-4 h-4 inline text-destructive"></i> ${escapeHtml(error.message)}</div>`;
|
||||
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 = `
|
||||
<div class="task-queue-empty">
|
||||
<span>No tasks in queue</span>
|
||||
<p>Right-click a folder or click "Add Task" to queue CLAUDE.md updates</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = updateTaskQueue.map(task => {
|
||||
const folderName = task.path.split('/').pop() || task.path;
|
||||
const strategyLabel = task.strategy === 'multi-layer'
|
||||
? '<i data-lucide="folder-tree" class="w-3 h-3 inline"></i> With subdirs'
|
||||
: '<i data-lucide="file" class="w-3 h-3 inline"></i> Current only';
|
||||
const statusIcon = {
|
||||
'pending': '<i data-lucide="clock" class="w-4 h-4"></i>',
|
||||
'running': '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>',
|
||||
'completed': '<i data-lucide="check-circle" class="w-4 h-4 text-success"></i>',
|
||||
'failed': '<i data-lucide="x-circle" class="w-4 h-4 text-destructive"></i>'
|
||||
}[task.status];
|
||||
|
||||
return `
|
||||
<div class="task-queue-item status-${task.status}" data-task-id="${task.id}">
|
||||
<div class="task-item-header">
|
||||
<span class="task-status-icon">${statusIcon}</span>
|
||||
<span class="task-folder-name" title="${escapeHtml(task.path)}">${escapeHtml(folderName)}</span>
|
||||
${task.status === 'pending' ? `
|
||||
<button class="task-remove-btn" onclick="removeTask(${task.id})" title="Remove">×</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="task-item-meta">
|
||||
<span class="task-strategy">${strategyLabel}</span>
|
||||
<span class="task-tool">${task.tool}</span>
|
||||
</div>
|
||||
${task.message ? `<div class="task-item-message">${escapeHtml(task.message)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).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();
|
||||
}
|
||||
|
||||
@@ -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}}
|
||||
</style>
|
||||
@@ -199,9 +227,11 @@
|
||||
<!-- Top Bar -->
|
||||
<header class="flex items-center justify-between px-5 h-14 bg-card border-b border-border sticky top-0 z-50 shadow-sm">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="hidden md:hidden p-2 text-foreground hover:bg-hover rounded menu-toggle-btn" id="menuToggle">☰</button>
|
||||
<button class="hidden md:hidden p-2 text-foreground hover:bg-hover rounded menu-toggle-btn" id="menuToggle">
|
||||
<i data-lucide="menu" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||
<span class="text-2xl">⚡</span>
|
||||
<i data-lucide="workflow" class="w-6 h-6"></i>
|
||||
<span class="hidden sm:inline">Claude Code Workflow</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +253,8 @@
|
||||
</div>
|
||||
<div class="p-2 border-t border-border">
|
||||
<button class="w-full flex items-center justify-center gap-2 px-3 py-2 bg-background border border-border rounded text-sm text-muted-foreground hover:bg-hover" id="browsePath">
|
||||
📂 Browse...
|
||||
<i data-lucide="folder-open" class="w-4 h-4"></i>
|
||||
<span>Browse...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,7 +271,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<button class="p-2 text-xl hover:bg-hover rounded" id="themeToggle" title="Toggle theme">🌙</button>
|
||||
<button class="p-2 hover:bg-hover rounded flex items-center justify-center" id="themeToggle" title="Toggle theme">
|
||||
<i data-lucide="moon" class="w-5 h-5 theme-icon-dark"></i>
|
||||
<i data-lucide="sun" class="w-5 h-5 theme-icon-light hidden"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -255,36 +289,40 @@
|
||||
<!-- Project Overview Section -->
|
||||
<div class="mb-2" id="projectOverviewNav">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<span class="mr-2">🏗️</span>
|
||||
<i data-lucide="layout-dashboard" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">Project</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="project-overview" data-tooltip="Project Overview">
|
||||
<span>📊</span>
|
||||
<i data-lucide="bar-chart-3" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Overview</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="explorer" data-tooltip="File Explorer">
|
||||
<i data-lucide="folder-tree" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Explorer</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Sessions Section -->
|
||||
<div class="mb-2">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<span class="mr-2">📁</span>
|
||||
<i data-lucide="history" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">Sessions</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors active" data-filter="all" data-tooltip="All Sessions">
|
||||
<span>📋</span>
|
||||
<i data-lucide="list" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">All</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeAll">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-filter="active" data-tooltip="Active Sessions">
|
||||
<span>🟢</span>
|
||||
<i data-lucide="play-circle" class="nav-icon text-success"></i>
|
||||
<span class="nav-text flex-1">Active</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-success-light text-success" id="badgeActive">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-filter="archived" data-tooltip="Archived Sessions">
|
||||
<span>📦</span>
|
||||
<i data-lucide="archive" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Archived</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeArchived">0</span>
|
||||
</li>
|
||||
@@ -294,17 +332,17 @@
|
||||
<!-- Lite Tasks Section -->
|
||||
<div class="mb-2" id="liteTasksNav">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<span class="mr-2">⚡</span>
|
||||
<i data-lucide="zap" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">Lite Tasks</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="lite-plan" data-tooltip="Lite Plan Sessions">
|
||||
<span>📝</span>
|
||||
<i data-lucide="file-edit" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Lite Plan</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeLitePlan">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="lite-fix" data-tooltip="Lite Fix Sessions">
|
||||
<span>🔧</span>
|
||||
<i data-lucide="wrench" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Lite Fix</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeLiteFix">0</span>
|
||||
</li>
|
||||
@@ -314,12 +352,12 @@
|
||||
<!-- MCP Servers Section -->
|
||||
<div class="mb-2" id="mcpServersNav">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<span class="mr-2">🔌</span>
|
||||
<i data-lucide="plug" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">MCP Servers</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="mcp-manager" data-tooltip="MCP Server Management">
|
||||
<span>⚙️</span>
|
||||
<i data-lucide="settings" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Manage</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeMcpServers">0</span>
|
||||
</li>
|
||||
@@ -329,12 +367,12 @@
|
||||
<!-- Hooks Section -->
|
||||
<div class="mb-2" id="hooksNav">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<span class="mr-2">🪝</span>
|
||||
<i data-lucide="webhook" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">Hooks</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="hook-manager" data-tooltip="Hook Management">
|
||||
<span>⚙️</span>
|
||||
<i data-lucide="cable" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Manage</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeHooks">0</span>
|
||||
</li>
|
||||
@@ -345,7 +383,7 @@
|
||||
<!-- Sidebar Footer -->
|
||||
<div class="p-3 border-t border-border">
|
||||
<button class="flex items-center justify-center gap-2 w-full px-3 py-2 border border-border rounded text-sm text-muted-foreground hover:bg-hover transition-colors" id="sidebarToggle">
|
||||
<span class="toggle-icon transition-transform duration-300">◀</span>
|
||||
<i data-lucide="panel-left-close" class="toggle-icon w-4 h-4 transition-transform duration-300"></i>
|
||||
<span class="toggle-text">Collapse</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -358,22 +396,22 @@
|
||||
<!-- Left: 4 Metrics Grid -->
|
||||
<div class="stats-metrics grid grid-cols-2 gap-3 shrink-0">
|
||||
<div class="bg-card border border-border rounded-lg p-4 text-center hover:shadow-md transition-all duration-200 min-w-[140px]">
|
||||
<div class="text-xl mb-1">📊</div>
|
||||
<div class="flex justify-center mb-1 text-primary"><i data-lucide="layers" class="w-5 h-5"></i></div>
|
||||
<div class="text-2xl font-bold text-foreground" id="statTotalSessions">0</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Total Sessions</div>
|
||||
</div>
|
||||
<div class="bg-card border border-border rounded-lg p-4 text-center hover:shadow-md transition-all duration-200 min-w-[140px]">
|
||||
<div class="text-xl mb-1">🟢</div>
|
||||
<div class="flex justify-center mb-1 text-success"><i data-lucide="activity" class="w-5 h-5"></i></div>
|
||||
<div class="text-2xl font-bold text-foreground" id="statActiveSessions">0</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Active Sessions</div>
|
||||
</div>
|
||||
<div class="bg-card border border-border rounded-lg p-4 text-center hover:shadow-md transition-all duration-200 min-w-[140px]">
|
||||
<div class="text-xl mb-1">📋</div>
|
||||
<div class="flex justify-center mb-1 text-primary"><i data-lucide="clipboard-list" class="w-5 h-5"></i></div>
|
||||
<div class="text-2xl font-bold text-foreground" id="statTotalTasks">0</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Total Tasks</div>
|
||||
</div>
|
||||
<div class="bg-card border border-border rounded-lg p-4 text-center hover:shadow-md transition-all duration-200 min-w-[140px]">
|
||||
<div class="text-xl mb-1">✅</div>
|
||||
<div class="flex justify-center mb-1 text-success"><i data-lucide="check-circle-2" class="w-5 h-5"></i></div>
|
||||
<div class="text-2xl font-bold text-foreground" id="statCompletedTasks">0</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Completed Tasks</div>
|
||||
</div>
|
||||
@@ -386,7 +424,7 @@
|
||||
<!-- Dynamic carousel slides -->
|
||||
<div class="carousel-empty flex items-center justify-center h-full text-muted-foreground">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl mb-2">🎯</div>
|
||||
<div class="flex justify-center mb-2"><i data-lucide="target" class="w-8 h-8"></i></div>
|
||||
<p class="text-sm">No active sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -421,7 +459,7 @@
|
||||
<div class="flex items-center justify-between flex-wrap gap-3 mb-5">
|
||||
<h2 class="text-xl font-semibold text-foreground" id="contentTitle">All Sessions</h2>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-sm">🔍</span>
|
||||
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground"></i>
|
||||
<input type="text" placeholder="Search..." id="searchInput"
|
||||
class="pl-9 pr-4 py-2 w-60 border border-border rounded-lg bg-background text-foreground text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</div>
|
||||
@@ -618,10 +656,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update CLAUDE.md Modal -->
|
||||
<div id="updateClaudeMdModal" class="claude-md-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div class="claude-md-modal-backdrop absolute inset-0 bg-black/60" onclick="closeUpdateClaudeMdModal()"></div>
|
||||
<div class="claude-md-modal-content relative bg-card border border-border rounded-lg shadow-2xl w-[90vw] max-w-md flex flex-col">
|
||||
<div class="claude-md-modal-header flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">Update CLAUDE.md</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeUpdateClaudeMdModal()">×</button>
|
||||
</div>
|
||||
<div class="claude-md-modal-body p-4 space-y-4">
|
||||
<div class="claude-md-form-group">
|
||||
<label>Target Directory</label>
|
||||
<div class="claude-md-target-path" id="claudeMdTargetPath">-</div>
|
||||
</div>
|
||||
<div class="claude-md-form-group">
|
||||
<label for="claudeMdTool">CLI Tool</label>
|
||||
<select id="claudeMdTool">
|
||||
<option value="gemini">Gemini (gemini-2.5-flash)</option>
|
||||
<option value="qwen">Qwen (coder-model)</option>
|
||||
<option value="codex">Codex (gpt5-codex)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="claude-md-form-group">
|
||||
<label for="claudeMdStrategy">Strategy</label>
|
||||
<select id="claudeMdStrategy">
|
||||
<option value="single-layer">Single Layer - Current dir + child CLAUDE.md refs</option>
|
||||
<option value="multi-layer">Multi Layer - Generate CLAUDE.md in all subdirs</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="claude-md-status" id="claudeMdStatus"></div>
|
||||
</div>
|
||||
<div class="claude-md-modal-footer flex justify-end gap-2 px-4 py-3 border-t border-border">
|
||||
<button class="px-4 py-2 text-sm bg-muted text-foreground rounded-lg hover:bg-hover transition-colors" onclick="closeUpdateClaudeMdModal()">Cancel</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity" id="claudeMdExecuteBtn" onclick="executeUpdateClaudeMd()">Execute</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lucide Icons -->
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<!-- D3.js for Flowchart -->
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<!-- Marked.js for Markdown rendering -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- Highlight.js for Syntax Highlighting -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css" id="hljs-theme-dark">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css" id="hljs-theme-light" disabled>
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
|
||||
|
||||
<script>
|
||||
{{JS_CONTENT}}
|
||||
|
||||
Reference in New Issue
Block a user