mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: 添加忽略模式配置接口和前端支持,允许用户自定义索引排除项
This commit is contained in:
@@ -590,6 +590,33 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
|
||||
// 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<boolean> {
|
||||
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) {
|
||||
|
||||
@@ -909,5 +909,139 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IGNORE PATTERNS CONFIGURATION ENDPOINTS
|
||||
// ============================================================
|
||||
|
||||
// API: Get ignore patterns configuration
|
||||
if (pathname === '/api/codexlens/ignore-patterns' && req.method === 'GET') {
|
||||
try {
|
||||
const { homedir } = await import('os');
|
||||
const { join } = await import('path');
|
||||
const { readFile } = await import('fs/promises');
|
||||
|
||||
const settingsPath = join(homedir(), '.codexlens', 'settings.json');
|
||||
let settings: Record<string, any> = {};
|
||||
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<string, any> = {};
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<Record<string, unknown>> {
|
||||
// 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<Record<st
|
||||
location,
|
||||
subdirectory,
|
||||
projectPath,
|
||||
enableReview
|
||||
enableReview,
|
||||
broadcastToClients
|
||||
} = params;
|
||||
|
||||
let prompt = '';
|
||||
@@ -608,7 +614,7 @@ async function generateRuleViaCLI(params: RuleGenerateParams): Promise<Record<st
|
||||
if (generationType === 'description') {
|
||||
mode = 'write';
|
||||
prompt = buildStructuredRulePrompt({
|
||||
description,
|
||||
description: description || '',
|
||||
fileName,
|
||||
subdirectory: subdirectory || '',
|
||||
location,
|
||||
@@ -638,15 +644,59 @@ FILE NAME: ${fileName}`;
|
||||
return { error: `Unknown generation type: ${generationType}` };
|
||||
}
|
||||
|
||||
// Broadcast CLI_EXECUTION_STARTED event
|
||||
if (broadcastToClients) {
|
||||
broadcastToClients({
|
||||
type: 'CLI_EXECUTION_STARTED',
|
||||
payload: {
|
||||
executionId,
|
||||
tool: 'claude',
|
||||
mode,
|
||||
category: 'internal',
|
||||
context: 'rule-generation',
|
||||
fileName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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 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<boolean> {
|
||||
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<boolean> {
|
||||
fileName: resolvedFileName,
|
||||
location: resolvedLocation,
|
||||
subdirectory: resolvedSubdirectory || '',
|
||||
projectPath
|
||||
projectPath,
|
||||
broadcastToClients
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ interface GenerationParams {
|
||||
skillName: string;
|
||||
location: SkillLocation;
|
||||
projectPath: string;
|
||||
broadcastToClients?: (data: unknown) => void;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
description,
|
||||
skillName,
|
||||
location,
|
||||
projectPath: validatedProjectPath
|
||||
projectPath: validatedProjectPath,
|
||||
broadcastToClients
|
||||
});
|
||||
} else {
|
||||
return { error: 'Invalid mode. Must be "import" or "cli-generate"' };
|
||||
|
||||
@@ -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': '项目数',
|
||||
|
||||
@@ -4170,6 +4170,52 @@ function buildCodexLensManagerPage(config) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// Ignore Patterns Section
|
||||
'<div class="bg-card border border-border rounded-lg overflow-hidden" id="ignorePatternsSection">' +
|
||||
'<div class="bg-muted/30 border-b border-border px-4 py-3 flex items-center justify-between cursor-pointer" onclick="toggleIgnorePatternsSection()">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<i data-lucide="eye-off" class="w-4 h-4 text-muted-foreground"></i>' +
|
||||
'<span class="font-medium text-foreground">' + (t('codexlens.ignorePatterns') || 'Ignore Patterns') + '</span>' +
|
||||
'<span class="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground" id="ignorePatternsCount">-</span>' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<i data-lucide="chevron-down" class="w-4 h-4 text-muted-foreground transition-transform" id="ignorePatternsChevron" style="transform: rotate(180deg)"></i>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="ignorePatternsContent" class="p-4">' +
|
||||
'<p class="text-xs text-muted-foreground mb-4">' + (t('codexlens.ignorePatternsDesc') || 'Configure directories and files to exclude from indexing. Changes apply to new indexes only.') + '</p>' +
|
||||
'<div class="grid grid-cols-2 gap-4">' +
|
||||
// Directory Patterns
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2 flex items-center gap-1.5">' +
|
||||
'<i data-lucide="folder-x" class="w-3.5 h-3.5"></i>' +
|
||||
(t('codexlens.directoryPatterns') || 'Directory Patterns') +
|
||||
'</label>' +
|
||||
'<textarea id="ignorePatternsInput" rows="8" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm font-mono resize-none" placeholder="node_modules .git dist"></textarea>' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.directoryPatternsHint') || 'One pattern per line') + '</p>' +
|
||||
'</div>' +
|
||||
// Extension Filters
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2 flex items-center gap-1.5">' +
|
||||
'<i data-lucide="file-x" class="w-3.5 h-3.5"></i>' +
|
||||
(t('codexlens.extensionFilters') || 'Extension Filters') +
|
||||
'</label>' +
|
||||
'<textarea id="extensionFiltersInput" rows="8" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm font-mono resize-none" placeholder="*.min.js package-lock.json *.map"></textarea>' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.extensionFiltersHint') || 'Files skipped for embedding') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center justify-between mt-4">' +
|
||||
'<button onclick="resetIgnorePatterns()" class="text-xs px-3 py-1.5 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors flex items-center gap-1.5">' +
|
||||
'<i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>' +
|
||||
(t('common.resetToDefaults') || 'Reset to Defaults') +
|
||||
'</button>' +
|
||||
'<button onclick="saveIgnorePatterns()" class="text-xs px-3 py-1.5 bg-primary text-primary-foreground hover:bg-primary/90 rounded transition-colors flex items-center gap-1.5">' +
|
||||
'<i data-lucide="save" class="w-3.5 h-3.5"></i>' +
|
||||
(t('common.save') || 'Save') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// Index Manager Section
|
||||
'<div class="bg-card border border-border rounded-lg overflow-hidden" id="indexManagerSection">' +
|
||||
'<div class="bg-muted/30 border-b border-border px-4 py-3 flex items-center justify-between">' +
|
||||
@@ -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;
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface ExecutionOutput {
|
||||
conversation: ConversationRecord; // Full conversation record
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
parsedOutput?: string; // Extracted text from stream JSON response
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user