From d2d6cce5f4c80740f8859efd4fdde6f3336b3e63 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Wed, 7 Jan 2026 23:33:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BF=BD=E7=95=A5?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E9=85=8D=E7=BD=AE=E6=8E=A5=E5=8F=A3=E5=92=8C?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=94=AF=E6=8C=81=EF=BC=8C=E5=85=81=E8=AE=B8?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=87=AA=E5=AE=9A=E4=B9=89=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E6=8E=92=E9=99=A4=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ccw/src/core/routes/claude-routes.ts | 38 +++ .../core/routes/codexlens/config-handlers.ts | 134 ++++++++++ ccw/src/core/routes/memory-routes.ts | 38 +++ ccw/src/core/routes/rules-routes.ts | 114 +++++++- ccw/src/core/routes/skills-routes.ts | 60 ++++- ccw/src/templates/dashboard-js/i18n.js | 24 ++ .../dashboard-js/views/codexlens-manager.js | 246 ++++++++++++++++++ ccw/src/tools/cli-executor-state.ts | 1 + ccw/tests/codex-lens.test.js | 35 +++ 9 files changed, 676 insertions(+), 14 deletions(-) diff --git a/ccw/src/core/routes/claude-routes.ts b/ccw/src/core/routes/claude-routes.ts index 8198dc28..61f0b562 100644 --- a/ccw/src/core/routes/claude-routes.ts +++ b/ccw/src/core/routes/claude-routes.ts @@ -590,6 +590,33 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { // Execute CLI tool const syncId = `claude-sync-${level}-${Date.now()}`; + + // Broadcast CLI_EXECUTION_STARTED event + broadcastToClients({ + type: 'CLI_EXECUTION_STARTED', + payload: { + executionId: syncId, + tool: tool === 'qwen' ? 'qwen' : 'gemini', + mode: 'analysis', + category: 'internal', + context: 'claude-sync', + level + } + }); + + // Create onOutput callback for real-time streaming + const onOutput = (chunk: { type: string; data: string }) => { + broadcastToClients({ + type: 'CLI_OUTPUT', + payload: { + executionId: syncId, + chunkType: chunk.type, + data: chunk.data + } + }); + }; + + const startTime = Date.now(); const result = await executeCliTool({ tool: tool === 'qwen' ? 'qwen' : 'gemini', prompt: cliPrompt, @@ -600,6 +627,17 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { stream: false, category: 'internal', id: syncId + }, onOutput); + + // Broadcast CLI_EXECUTION_COMPLETED event + broadcastToClients({ + type: 'CLI_EXECUTION_COMPLETED', + payload: { + executionId: syncId, + success: result.success, + status: result.execution?.status || (result.success ? 'success' : 'error'), + duration_ms: Date.now() - startTime + } }); if (!result.success) { diff --git a/ccw/src/core/routes/codexlens/config-handlers.ts b/ccw/src/core/routes/codexlens/config-handlers.ts index 2c27f34e..64f29ea8 100644 --- a/ccw/src/core/routes/codexlens/config-handlers.ts +++ b/ccw/src/core/routes/codexlens/config-handlers.ts @@ -909,5 +909,139 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise = {}; + try { + const content = await readFile(settingsPath, 'utf-8'); + settings = JSON.parse(content); + } catch { + // File doesn't exist + } + + // Default ignore patterns (matching WatcherConfig defaults in events.py) + const defaultPatterns = [ + // Version control + '.git', '.svn', '.hg', + // Python environments & cache + '.venv', 'venv', 'env', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', + // Node.js + 'node_modules', 'bower_components', '.npm', '.yarn', + // Build artifacts + 'dist', 'build', 'out', 'target', 'bin', 'obj', '_build', 'coverage', 'htmlcov', + // IDE & Editor + '.idea', '.vscode', '.vs', '.eclipse', + // CodexLens internal + '.codexlens', + // Package manager caches + '.cache', '.parcel-cache', '.turbo', '.next', '.nuxt', + // Logs & temp + 'logs', 'tmp', 'temp', + ]; + + // Default extension filters for embeddings (files skipped for vector index) + const defaultExtensionFilters = [ + // Lock files (large, repetitive) + 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'composer.lock', 'Gemfile.lock', 'poetry.lock', + // Generated/minified + '*.min.js', '*.min.css', '*.bundle.js', + // Binary-like text + '*.svg', '*.map', + ]; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + patterns: settings.ignore_patterns || defaultPatterns, + extensionFilters: settings.extension_filters || defaultExtensionFilters, + defaults: { + patterns: defaultPatterns, + extensionFilters: defaultExtensionFilters + } + })); + } catch (err: unknown) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })); + } + return true; + } + + // API: Save ignore patterns configuration + if (pathname === '/api/codexlens/ignore-patterns' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { patterns, extensionFilters } = body as { + patterns?: string[]; + extensionFilters?: string[]; + }; + + try { + const { homedir } = await import('os'); + const { join, dirname } = await import('path'); + const { writeFile, mkdir, readFile } = await import('fs/promises'); + + const settingsPath = join(homedir(), '.codexlens', 'settings.json'); + await mkdir(dirname(settingsPath), { recursive: true }); + + // Read existing settings + let settings: Record = {}; + try { + const content = await readFile(settingsPath, 'utf-8'); + settings = JSON.parse(content); + } catch { + // File doesn't exist, start fresh + } + + // Validate patterns (alphanumeric, dots, underscores, dashes, asterisks) + const validPatternRegex = /^[\w.*\-/]+$/; + if (patterns) { + const invalidPatterns = patterns.filter(p => !validPatternRegex.test(p)); + if (invalidPatterns.length > 0) { + return { + success: false, + error: `Invalid patterns: ${invalidPatterns.join(', ')}`, + status: 400 + }; + } + settings.ignore_patterns = patterns; + } + + if (extensionFilters) { + const invalidFilters = extensionFilters.filter(p => !validPatternRegex.test(p)); + if (invalidFilters.length > 0) { + return { + success: false, + error: `Invalid extension filters: ${invalidFilters.join(', ')}`, + status: 400 + }; + } + settings.extension_filters = extensionFilters; + } + + // Write updated settings + await writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8'); + + return { + success: true, + message: 'Ignore patterns saved successfully', + patterns: settings.ignore_patterns, + extensionFilters: settings.extension_filters + }; + } catch (err: unknown) { + return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 }; + } + }); + return true; + } + return false; } diff --git a/ccw/src/core/routes/memory-routes.ts b/ccw/src/core/routes/memory-routes.ts index c221f6ac..cf8c9a45 100644 --- a/ccw/src/core/routes/memory-routes.ts +++ b/ccw/src/core/routes/memory-routes.ts @@ -970,6 +970,33 @@ RULES: Be concise. Focus on practical understanding. Include function signatures // Try to execute CLI using CCW's built-in executor try { const syncId = `active-memory-${Date.now()}`; + + // Broadcast CLI_EXECUTION_STARTED event + broadcastToClients({ + type: 'CLI_EXECUTION_STARTED', + payload: { + executionId: syncId, + tool: tool === 'qwen' ? 'qwen' : 'gemini', + mode: 'analysis', + category: 'internal', + context: 'active-memory-sync', + fileCount: hotFiles.length + } + }); + + // Create onOutput callback for real-time streaming + const onOutput = (chunk: { type: string; data: string }) => { + broadcastToClients({ + type: 'CLI_OUTPUT', + payload: { + executionId: syncId, + chunkType: chunk.type, + data: chunk.data + } + }); + }; + + const startTime = Date.now(); const result = await executeCliTool({ tool: tool === 'qwen' ? 'qwen' : 'gemini', prompt: cliPrompt, @@ -980,6 +1007,17 @@ RULES: Be concise. Focus on practical understanding. Include function signatures stream: false, category: 'internal', id: syncId + }, onOutput); + + // Broadcast CLI_EXECUTION_COMPLETED event + broadcastToClients({ + type: 'CLI_EXECUTION_COMPLETED', + payload: { + executionId: syncId, + success: result.success, + status: result.execution?.status || (result.success ? 'success' : 'error'), + duration_ms: Date.now() - startTime + } }); if (result.success) { diff --git a/ccw/src/core/routes/rules-routes.ts b/ccw/src/core/routes/rules-routes.ts index fb90e8e2..f794e443 100644 --- a/ccw/src/core/routes/rules-routes.ts +++ b/ccw/src/core/routes/rules-routes.ts @@ -46,6 +46,8 @@ interface RuleGenerateParams { location: string; subdirectory: string; projectPath: string; + enableReview?: boolean; + broadcastToClients?: (data: unknown) => void; } /** @@ -351,7 +353,7 @@ function buildStructuredRulePrompt(params: { const { description, fileName, subdirectory, location, context, enableReview } = params; // Build category-specific guidance - const categoryGuidance = { + const categoryGuidance: Record = { coding: 'Focus on code style, naming conventions, and formatting rules. Include specific examples of correct and incorrect patterns.', testing: 'Emphasize test structure, coverage expectations, mocking strategies, and assertion patterns.', security: 'Highlight security best practices, input validation, authentication requirements, and sensitive data handling.', @@ -583,6 +585,9 @@ RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-ri * @returns {Object} */ async function generateRuleViaCLI(params: RuleGenerateParams): Promise> { + // Generate unique execution ID for tracking + const executionId = `rule-gen-${params.fileName.replace('.md', '')}-${Date.now()}`; + try { const { generationType, @@ -594,7 +599,8 @@ async function generateRuleViaCLI(params: RuleGenerateParams): Promise { + broadcastToClients({ + type: 'CLI_OUTPUT', + payload: { + executionId, + chunkType: chunk.type, + data: chunk.data + } + }); + } + : undefined; + // Execute CLI tool (Claude) with at least 10 minutes timeout + const startTime = Date.now(); const result = await executeCliTool({ tool: 'claude', prompt, mode, cd: workingDir, timeout: 600000, // 10 minutes - category: 'internal' - }); + category: 'internal', + id: executionId + }, onOutput); + + // Broadcast CLI_EXECUTION_COMPLETED event + if (broadcastToClients) { + broadcastToClients({ + type: 'CLI_EXECUTION_COMPLETED', + payload: { + executionId, + success: result.success, + status: result.execution?.status || (result.success ? 'success' : 'error'), + duration_ms: Date.now() - startTime + } + }); + } if (!result.success) { return { @@ -677,15 +727,60 @@ FILE NAME: ${fileName}`; let reviewResult = null; if (enableReview) { const reviewPrompt = buildReviewPrompt(generatedContent, fileName, context); + const reviewExecutionId = `${executionId}-review`; + // Broadcast review CLI_EXECUTION_STARTED event + if (broadcastToClients) { + broadcastToClients({ + type: 'CLI_EXECUTION_STARTED', + payload: { + executionId: reviewExecutionId, + tool: 'claude', + mode: 'write', + category: 'internal', + context: 'rule-review', + fileName + } + }); + } + + // Create onOutput callback for review step + const reviewOnOutput = broadcastToClients + ? (chunk: { type: string; data: string }) => { + broadcastToClients({ + type: 'CLI_OUTPUT', + payload: { + executionId: reviewExecutionId, + chunkType: chunk.type, + data: chunk.data + } + }); + } + : undefined; + + const reviewStartTime = Date.now(); const reviewExecution = await executeCliTool({ tool: 'claude', prompt: reviewPrompt, mode: 'write', cd: workingDir, timeout: 300000, // 5 minutes for review - category: 'internal' - }); + category: 'internal', + id: reviewExecutionId + }, reviewOnOutput); + + // Broadcast review CLI_EXECUTION_COMPLETED event + if (broadcastToClients) { + broadcastToClients({ + type: 'CLI_EXECUTION_COMPLETED', + payload: { + executionId: reviewExecutionId, + success: reviewExecution.success, + status: reviewExecution.execution?.status || (reviewExecution.success ? 'success' : 'error'), + duration_ms: Date.now() - reviewStartTime + } + }); + } if (reviewExecution.success) { let reviewedContent = (reviewExecution.parsedOutput || reviewExecution.stdout || '').trim(); @@ -801,7 +896,7 @@ paths: [${paths.join(', ')}] * @returns true if route was handled, false otherwise */ export async function handleRulesRoutes(ctx: RouteContext): Promise { - const { pathname, url, req, res, initialPath, handlePostRequest } = ctx; + const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx; // API: Get all rules if (pathname === '/api/rules') { @@ -920,7 +1015,8 @@ export async function handleRulesRoutes(ctx: RouteContext): Promise { fileName: resolvedFileName, location: resolvedLocation, subdirectory: resolvedSubdirectory || '', - projectPath + projectPath, + broadcastToClients }); } diff --git a/ccw/src/core/routes/skills-routes.ts b/ccw/src/core/routes/skills-routes.ts index 2b6ca1c4..0354fb7d 100644 --- a/ccw/src/core/routes/skills-routes.ts +++ b/ccw/src/core/routes/skills-routes.ts @@ -55,6 +55,7 @@ interface GenerationParams { skillName: string; location: SkillLocation; projectPath: string; + broadcastToClients?: (data: unknown) => void; } function isRecord(value: unknown): value is Record { @@ -488,9 +489,13 @@ async function importSkill(sourcePath: string, location: SkillLocation, projectP * @param {string} params.skillName - Name for the skill * @param {string} params.location - 'project' or 'user' * @param {string} params.projectPath - Project root path + * @param {Function} params.broadcastToClients - WebSocket broadcast function * @returns {Object} */ -async function generateSkillViaCLI({ generationType, description, skillName, location, projectPath }: GenerationParams) { +async function generateSkillViaCLI({ generationType, description, skillName, location, projectPath, broadcastToClients }: GenerationParams) { + // Generate unique execution ID for tracking + const executionId = `skill-gen-${skillName}-${Date.now()}`; + try { // Validate inputs if (!skillName) { @@ -557,15 +562,59 @@ Create a new Claude Code skill with the following specifications: 4. Follow Claude Code skill design patterns and best practices 5. Output all files to: ${targetPath}`; + // Broadcast CLI_EXECUTION_STARTED event + if (broadcastToClients) { + broadcastToClients({ + type: 'CLI_EXECUTION_STARTED', + payload: { + executionId, + tool: 'claude', + mode: 'write', + category: 'internal', + context: 'skill-generation', + skillName + } + }); + } + + // Create onOutput callback for real-time streaming + const onOutput = broadcastToClients + ? (chunk: { type: string; data: string }) => { + broadcastToClients({ + type: 'CLI_OUTPUT', + payload: { + executionId, + chunkType: chunk.type, + data: chunk.data + } + }); + } + : undefined; + // Execute CLI tool (Claude) with write mode + const startTime = Date.now(); const result = await executeCliTool({ tool: 'claude', prompt, mode: 'write', cd: baseDir, timeout: 600000, // 10 minutes - category: 'internal' - }); + category: 'internal', + id: executionId + }, onOutput); + + // Broadcast CLI_EXECUTION_COMPLETED event + if (broadcastToClients) { + broadcastToClients({ + type: 'CLI_EXECUTION_COMPLETED', + payload: { + executionId, + success: result.success, + status: result.execution?.status || (result.success ? 'success' : 'error'), + duration_ms: Date.now() - startTime + } + }); + } // Check if execution was successful if (!result.success) { @@ -606,7 +655,7 @@ Create a new Claude Code skill with the following specifications: * @returns true if route was handled, false otherwise */ export async function handleSkillsRoutes(ctx: RouteContext): Promise { - const { pathname, url, req, res, initialPath, handlePostRequest } = ctx; + const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx; // API: Get all skills (project and user) if (pathname === '/api/skills') { @@ -991,7 +1040,8 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { description, skillName, location, - projectPath: validatedProjectPath + projectPath: validatedProjectPath, + broadcastToClients }); } else { return { error: 'Invalid mode. Must be "import" or "cli-generate"' }; diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 2da3b31f..6fd91450 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -563,6 +563,18 @@ const i18n = { 'codexlens.uninstallComplete': 'Uninstallation complete!', 'codexlens.uninstallSuccess': 'CodexLens uninstalled successfully!', + // Ignore Patterns + 'codexlens.ignorePatterns': 'Ignore Patterns', + 'codexlens.ignorePatternsDesc': 'Configure directories and files to exclude from indexing. Changes apply to new indexes only.', + 'codexlens.directoryPatterns': 'Directory Patterns', + 'codexlens.extensionFilters': 'Extension Filters', + 'codexlens.directoryPatternsHint': 'One pattern per line (e.g., node_modules, .git)', + 'codexlens.extensionFiltersHint': 'Files skipped for embedding (e.g., *.min.js)', + 'codexlens.ignorePatternsSaved': 'Ignore patterns saved', + 'codexlens.ignorePatternReset': 'Reset to defaults (click Save to apply)', + 'common.patterns': 'patterns', + 'common.resetToDefaults': 'Reset to Defaults', + // Index Manager 'index.manager': 'Index Manager', 'index.projects': 'Projects', @@ -2599,6 +2611,18 @@ const i18n = { 'codexlens.uninstallComplete': '卸载完成!', 'codexlens.uninstallSuccess': 'CodexLens 卸载成功!', + // 忽略规则 + 'codexlens.ignorePatterns': '忽略规则', + 'codexlens.ignorePatternsDesc': '配置索引时要排除的目录和文件。更改仅对新索引生效。', + 'codexlens.directoryPatterns': '目录规则', + 'codexlens.extensionFilters': '文件过滤', + 'codexlens.directoryPatternsHint': '每行一个规则(如 node_modules, .git)', + 'codexlens.extensionFiltersHint': '跳过 embedding 的文件(如 *.min.js)', + 'codexlens.ignorePatternsSaved': '忽略规则已保存', + 'codexlens.ignorePatternReset': '已重置为默认值(点击保存应用)', + 'common.patterns': '条规则', + 'common.resetToDefaults': '重置为默认', + // 索引管理器 'index.manager': '索引管理器', 'index.projects': '项目数', diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index 5ab25410..bbd333c0 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -4170,6 +4170,52 @@ function buildCodexLensManagerPage(config) { '' + '' + '' + + // Ignore Patterns Section + '
' + + '
' + + '
' + + '' + + '' + (t('codexlens.ignorePatterns') || 'Ignore Patterns') + '' + + '-' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '

' + (t('codexlens.ignorePatternsDesc') || 'Configure directories and files to exclude from indexing. Changes apply to new indexes only.') + '

' + + '
' + + // Directory Patterns + '
' + + '' + + '' + + '

' + (t('codexlens.directoryPatternsHint') || 'One pattern per line') + '

' + + '
' + + // Extension Filters + '
' + + '' + + '' + + '

' + (t('codexlens.extensionFiltersHint') || 'Files skipped for embedding') + '

' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + // Index Manager Section '
' + '
' + @@ -4943,6 +4989,13 @@ function initCodexLensManagerPageEvents(currentConfig) { var searchInput = document.getElementById('searchQueryInput'); if (searchInput) { searchInput.onkeypress = function(e) { if (e.key === 'Enter' && runSearchBtn) { runSearchBtn.click(); } }; } + + // Initialize ignore patterns count badge (delayed to ensure function is defined) + setTimeout(function() { + if (typeof initIgnorePatternsCount === 'function') { + initIgnorePatternsCount(); + } + }, 100); } /** @@ -6396,3 +6449,196 @@ function handleWatcherStatusUpdate(payload) { stopWatcherStatusPolling(); } } + +// ============================================================ +// IGNORE PATTERNS CONFIGURATION +// ============================================================ + +// Cache for default patterns (loaded once) +var ignorePatternsDefaults = null; + +/** + * Toggle ignore patterns section visibility + */ +function toggleIgnorePatternsSection() { + var content = document.getElementById('ignorePatternsContent'); + var chevron = document.getElementById('ignorePatternsChevron'); + if (content && chevron) { + var isHidden = content.classList.contains('hidden'); + content.classList.toggle('hidden'); + chevron.style.transform = isHidden ? 'rotate(180deg)' : ''; + } +} +window.toggleIgnorePatternsSection = toggleIgnorePatternsSection; + +/** + * Load ignore patterns from server + */ +async function loadIgnorePatterns() { + try { + var response = await fetch('/api/codexlens/ignore-patterns'); + var data = await response.json(); + + if (data.success) { + // Cache defaults + ignorePatternsDefaults = data.defaults; + + // Populate textareas + var patternsInput = document.getElementById('ignorePatternsInput'); + var filtersInput = document.getElementById('extensionFiltersInput'); + + if (patternsInput) { + patternsInput.value = (data.patterns || []).join('\n'); + } + if (filtersInput) { + filtersInput.value = (data.extensionFilters || []).join('\n'); + } + + // Update count badge + var countBadge = document.getElementById('ignorePatternsCount'); + if (countBadge) { + var total = (data.patterns || []).length + (data.extensionFilters || []).length; + countBadge.textContent = total + ' ' + (t('common.patterns') || 'patterns'); + } + } + } catch (err) { + console.error('Failed to load ignore patterns:', err); + } +} +window.loadIgnorePatterns = loadIgnorePatterns; + +/** + * Save ignore patterns to server + */ +async function saveIgnorePatterns() { + var patternsInput = document.getElementById('ignorePatternsInput'); + var filtersInput = document.getElementById('extensionFiltersInput'); + + var patterns = patternsInput ? patternsInput.value.split('\n').map(function(p) { return p.trim(); }).filter(function(p) { return p; }) : []; + var extensionFilters = filtersInput ? filtersInput.value.split('\n').map(function(p) { return p.trim(); }).filter(function(p) { return p; }) : []; + + try { + var response = await fetch('/api/codexlens/ignore-patterns', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ patterns: patterns, extensionFilters: extensionFilters }) + }); + + var result = await response.json(); + + if (result.success) { + showRefreshToast(t('codexlens.ignorePatternsSaved') || 'Ignore patterns saved', 'success'); + + // Update count badge + var countBadge = document.getElementById('ignorePatternsCount'); + if (countBadge) { + var total = patterns.length + extensionFilters.length; + countBadge.textContent = total + ' ' + (t('common.patterns') || 'patterns'); + } + } else { + showRefreshToast(t('common.error') + ': ' + result.error, 'error'); + } + } catch (err) { + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + } +} +window.saveIgnorePatterns = saveIgnorePatterns; + +/** + * Reset ignore patterns to defaults + */ +async function resetIgnorePatterns() { + if (!ignorePatternsDefaults) { + // Load defaults first if not cached + try { + var response = await fetch('/api/codexlens/ignore-patterns'); + var data = await response.json(); + if (data.success) { + ignorePatternsDefaults = data.defaults; + } + } catch (err) { + console.error('Failed to load defaults:', err); + return; + } + } + + if (ignorePatternsDefaults) { + var patternsInput = document.getElementById('ignorePatternsInput'); + var filtersInput = document.getElementById('extensionFiltersInput'); + + if (patternsInput) { + patternsInput.value = (ignorePatternsDefaults.patterns || []).join('\n'); + } + if (filtersInput) { + filtersInput.value = (ignorePatternsDefaults.extensionFilters || []).join('\n'); + } + + showRefreshToast(t('codexlens.ignorePatternReset') || 'Reset to defaults (click Save to apply)', 'info'); + } +} +window.resetIgnorePatterns = resetIgnorePatterns; + +/** + * Initialize ignore patterns count badge (called on page load) + * Also loads patterns into textarea if section is visible + */ +async function initIgnorePatternsCount() { + // Fallback defaults in case API fails + var fallbackDefaults = { + patterns: [ + '.git', '.svn', '.hg', + '.venv', 'venv', 'env', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', + 'node_modules', 'bower_components', '.npm', '.yarn', + 'dist', 'build', 'out', 'target', 'bin', 'obj', '_build', 'coverage', 'htmlcov', + '.idea', '.vscode', '.vs', '.eclipse', + '.codexlens', + '.cache', '.parcel-cache', '.turbo', '.next', '.nuxt', + 'logs', 'tmp', 'temp' + ], + extensionFilters: [ + 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'composer.lock', 'Gemfile.lock', 'poetry.lock', + '*.min.js', '*.min.css', '*.bundle.js', + '*.svg', '*.map' + ] + }; + + var patterns = fallbackDefaults.patterns; + var extensionFilters = fallbackDefaults.extensionFilters; + + try { + var response = await fetch('/api/codexlens/ignore-patterns'); + var data = await response.json(); + + if (data.success) { + // Cache defaults + ignorePatternsDefaults = data.defaults || fallbackDefaults; + patterns = data.patterns || fallbackDefaults.patterns; + extensionFilters = data.extensionFilters || fallbackDefaults.extensionFilters; + } else { + console.warn('Ignore patterns API returned error, using defaults'); + ignorePatternsDefaults = fallbackDefaults; + } + } catch (err) { + console.warn('Failed to fetch ignore patterns, using defaults:', err); + ignorePatternsDefaults = fallbackDefaults; + } + + // Update count badge + var countBadge = document.getElementById('ignorePatternsCount'); + if (countBadge) { + var total = patterns.length + extensionFilters.length; + countBadge.textContent = total + ' ' + (t('common.patterns') || 'patterns'); + } + + // Populate textareas if they exist + var patternsInput = document.getElementById('ignorePatternsInput'); + var filtersInput = document.getElementById('extensionFiltersInput'); + + if (patternsInput) { + patternsInput.value = patterns.join('\n'); + } + if (filtersInput) { + filtersInput.value = extensionFilters.join('\n'); + } +} +window.initIgnorePatternsCount = initIgnorePatternsCount; diff --git a/ccw/src/tools/cli-executor-state.ts b/ccw/src/tools/cli-executor-state.ts index b12214c2..c7700d52 100644 --- a/ccw/src/tools/cli-executor-state.ts +++ b/ccw/src/tools/cli-executor-state.ts @@ -102,6 +102,7 @@ export interface ExecutionOutput { conversation: ConversationRecord; // Full conversation record stdout: string; stderr: string; + parsedOutput?: string; // Extracted text from stream JSON response } /** diff --git a/ccw/tests/codex-lens.test.js b/ccw/tests/codex-lens.test.js index 51c80d6a..c2d46ca0 100644 --- a/ccw/tests/codex-lens.test.js +++ b/ccw/tests/codex-lens.test.js @@ -352,6 +352,39 @@ describe('CodexLens Path Configuration', () => { describe('CodexLens Error Handling', async () => { let codexLensModule; + const testTempDirs = []; // Track temp directories for cleanup + + after(() => { + // Clean up temp directories created during tests + for (const dir of testTempDirs) { + try { + rmSync(dir, { recursive: true, force: true }); + } catch (e) { + // Ignore cleanup errors + } + } + + // Clean up any indexes created for temp directories + const indexDir = join(homedir(), '.codexlens', 'indexes'); + const tempIndexPattern = join(indexDir, 'C', 'Users', '*', 'AppData', 'Local', 'Temp', 'ccw-codexlens-update-*'); + try { + const glob = require('glob'); + const matches = glob.sync(tempIndexPattern.replace(/\\/g, '/')); + for (const match of matches) { + rmSync(match, { recursive: true, force: true }); + } + } catch (e) { + // glob may not be available, try direct cleanup + try { + const tempPath = join(indexDir, 'C', 'Users'); + if (existsSync(tempPath)) { + console.log('Note: Temp indexes may need manual cleanup at:', indexDir); + } + } catch (e2) { + // Ignore + } + } + }); before(async () => { try { @@ -395,6 +428,7 @@ describe('CodexLens Error Handling', async () => { } const updateRoot = mkdtempSync(join(tmpdir(), 'ccw-codexlens-update-')); + testTempDirs.push(updateRoot); // Track for cleanup writeFileSync(join(updateRoot, 'main.py'), 'def hello():\n return 1\n', 'utf8'); const result = await codexLensModule.codexLensTool.execute({ @@ -419,6 +453,7 @@ describe('CodexLens Error Handling', async () => { } const updateRoot = mkdtempSync(join(tmpdir(), 'ccw-codexlens-update-')); + testTempDirs.push(updateRoot); // Track for cleanup writeFileSync(join(updateRoot, 'main.py'), 'def hello():\n return 1\n', 'utf8'); const result = await codexLensModule.codexLensTool.execute({