mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-15 02:42:45 +08:00
feat: Enhance JSON streaming parsing and UI updates
- Added a function to parse JSON streaming content in core-memory.js, extracting readable text from messages. - Updated memory detail view to utilize the new parsing function for content and summary. - Introduced an enableReview option in rules-manager.js, allowing users to toggle review functionality in rule creation. - Simplified skill creation modal in skills-manager.js by removing generation type selection UI. - Improved CLI executor to handle tool calls for file writing, ensuring proper output parsing. - Adjusted CLI command tests to set timeout to 0 for immediate execution. - Updated file watcher to implement a true debounce mechanism and added a pending queue status for UI updates. - Enhanced watcher manager to handle queue changes and provide JSON output for better integration with TypeScript backend. - Established TypeScript naming conventions documentation to standardize code style across the project.
This commit is contained in:
88
.claude/rules/coding/ts-naming.md
Normal file
88
.claude/rules/coding/ts-naming.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# TypeScript Naming Conventions
|
||||||
|
|
||||||
|
This rule enforces consistent naming conventions for TypeScript code to improve readability and maintain codebase consistency.
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
1. **Variables and Functions** - Use camelCase for all variable names, function names, and function parameters
|
||||||
|
2. **Classes and Interfaces** - Use PascalCase for class names, interface names, type aliases, and enum names
|
||||||
|
3. **Constants** - Use UPPER_SNAKE_CASE for module-level constants and readonly static class members
|
||||||
|
4. **Private Members** - Use camelCase with no special prefix for private class members (rely on TypeScript's `private` keyword)
|
||||||
|
5. **File Names** - Use kebab-case for file names (e.g., `user-service.ts`, `auth-controller.ts`)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### ✅ Correct
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Variables and functions
|
||||||
|
const userName = 'John';
|
||||||
|
let itemCount = 0;
|
||||||
|
function calculateTotal(orderItems: Item[]): number {
|
||||||
|
return orderItems.reduce((sum, item) => sum + item.price, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classes and interfaces
|
||||||
|
class UserService {
|
||||||
|
private userRepository: UserRepository;
|
||||||
|
|
||||||
|
constructor(userRepository: UserRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
statusCode: number;
|
||||||
|
data: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserId = string;
|
||||||
|
|
||||||
|
enum OrderStatus {
|
||||||
|
Pending,
|
||||||
|
Confirmed,
|
||||||
|
Shipped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
const API_BASE_URL = 'https://api.example.com';
|
||||||
|
|
||||||
|
class Configuration {
|
||||||
|
static readonly DEFAULT_TIMEOUT = 5000;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Incorrect
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Wrong: PascalCase for variables/functions
|
||||||
|
const UserName = 'John';
|
||||||
|
function CalculateTotal(order_items: Item[]): number { }
|
||||||
|
|
||||||
|
// Wrong: camelCase for classes/interfaces
|
||||||
|
class userService { }
|
||||||
|
interface apiResponse { }
|
||||||
|
type userId = string;
|
||||||
|
|
||||||
|
// Wrong: camelCase for constants
|
||||||
|
const maxRetryAttempts = 3;
|
||||||
|
const apiBaseUrl = 'https://api.example.com';
|
||||||
|
|
||||||
|
// Wrong: snake_case usage
|
||||||
|
const user_name = 'John';
|
||||||
|
function calculate_total(items: Item[]): number { }
|
||||||
|
class user_service { }
|
||||||
|
|
||||||
|
// Wrong: Hungarian notation or underscore prefix for private
|
||||||
|
class Service {
|
||||||
|
private _userData: User; // Don't use underscore prefix
|
||||||
|
private m_count: number; // Don't use Hungarian notation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
- Third-party library types and names should maintain their original casing for compatibility
|
||||||
|
- Database column names or API field names may use different conventions when mapped from external systems (use transformation layers to convert)
|
||||||
|
- Test files may use descriptive names like `UserService.test.ts` to match the tested class
|
||||||
@@ -453,7 +453,8 @@ ${memory.content}
|
|||||||
category: 'internal'
|
category: 'internal'
|
||||||
});
|
});
|
||||||
|
|
||||||
const summary = result.stdout?.trim() || 'Failed to generate summary';
|
// Use parsedOutput (extracted text from stream JSON) instead of raw stdout
|
||||||
|
const summary = result.parsedOutput?.trim() || result.stdout?.trim() || 'Failed to generate summary';
|
||||||
|
|
||||||
// Update memory with summary
|
// Update memory with summary
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
|
|||||||
@@ -613,7 +613,7 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
id: syncId
|
id: syncId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success || !result.execution?.output) {
|
if (!result.success) {
|
||||||
return {
|
return {
|
||||||
error: 'CLI execution failed',
|
error: 'CLI execution failed',
|
||||||
details: result.execution?.error || 'No output received',
|
details: result.execution?.error || 'No output received',
|
||||||
@@ -621,10 +621,13 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract CLI output
|
// Extract CLI output - prefer parsedOutput (extracted text from stream JSON)
|
||||||
const cliOutput = typeof result.execution.output === 'string'
|
let cliOutput = result.parsedOutput || '';
|
||||||
? result.execution.output
|
if (!cliOutput && result.execution?.output) {
|
||||||
: result.execution.output.stdout || '';
|
cliOutput = typeof result.execution.output === 'string'
|
||||||
|
? result.execution.output
|
||||||
|
: result.execution.output.stdout || '';
|
||||||
|
}
|
||||||
|
|
||||||
if (!cliOutput || cliOutput.trim().length === 0) {
|
if (!cliOutput || cliOutput.trim().length === 0) {
|
||||||
return { error: 'CLI returned empty output', status: 500 };
|
return { error: 'CLI returned empty output', status: 500 };
|
||||||
|
|||||||
@@ -39,11 +39,32 @@ interface WatcherConfig {
|
|||||||
debounce_ms: number;
|
debounce_ms: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PendingQueueStatus {
|
||||||
|
file_count: number;
|
||||||
|
files: string[];
|
||||||
|
countdown_seconds: number;
|
||||||
|
last_event_time: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexResultDetail {
|
||||||
|
files_indexed: number;
|
||||||
|
files_removed: number;
|
||||||
|
symbols_added: number;
|
||||||
|
symbols_removed: number;
|
||||||
|
files_success: string[];
|
||||||
|
files_failed: string[];
|
||||||
|
errors: string[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface WatcherStats {
|
interface WatcherStats {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
root_path: string;
|
root_path: string;
|
||||||
events_processed: number;
|
events_processed: number;
|
||||||
start_time: Date | null;
|
start_time: Date | null;
|
||||||
|
pending_queue: PendingQueueStatus | null;
|
||||||
|
last_index_result: IndexResultDetail | null;
|
||||||
|
index_history: IndexResultDetail[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActiveWatcher {
|
interface ActiveWatcher {
|
||||||
@@ -58,13 +79,12 @@ const WATCHER_CONFIG_FILE = path.join(WATCHER_CONFIG_DIR, 'watchers.json');
|
|||||||
// Active watchers Map: normalized_path -> { process, stats }
|
// Active watchers Map: normalized_path -> { process, stats }
|
||||||
const activeWatchers = new Map<string, ActiveWatcher>();
|
const activeWatchers = new Map<string, ActiveWatcher>();
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize path for consistent key usage
|
|
||||||
* - Convert to absolute path
|
|
||||||
|
|
||||||
// Flag to ensure watchers are initialized only once
|
// Flag to ensure watchers are initialized only once
|
||||||
let watchersInitialized = false;
|
let watchersInitialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize path for consistent key usage
|
||||||
|
* - Convert to absolute path
|
||||||
* - Convert to lowercase on Windows
|
* - Convert to lowercase on Windows
|
||||||
* - Use forward slashes
|
* - Use forward slashes
|
||||||
*/
|
*/
|
||||||
@@ -183,7 +203,10 @@ async function startWatcherProcess(
|
|||||||
running: true,
|
running: true,
|
||||||
root_path: targetPath,
|
root_path: targetPath,
|
||||||
events_processed: 0,
|
events_processed: 0,
|
||||||
start_time: new Date()
|
start_time: new Date(),
|
||||||
|
pending_queue: null,
|
||||||
|
last_index_result: null,
|
||||||
|
index_history: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register in activeWatchers Map
|
// Register in activeWatchers Map
|
||||||
@@ -201,16 +224,66 @@ async function startWatcherProcess(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle process output for event counting
|
// Handle process output for JSON parsing and event counting
|
||||||
if (childProcess.stdout) {
|
if (childProcess.stdout) {
|
||||||
childProcess.stdout.on('data', (data: Buffer) => {
|
childProcess.stdout.on('data', (data: Buffer) => {
|
||||||
const output = data.toString();
|
const output = data.toString();
|
||||||
const matches = output.match(/Processed \d+ events?/g);
|
const watcher = activeWatchers.get(normalizedPath);
|
||||||
if (matches) {
|
if (!watcher) return;
|
||||||
const watcher = activeWatchers.get(normalizedPath);
|
|
||||||
if (watcher) {
|
// Process output line by line for reliable JSON parsing
|
||||||
watcher.stats.events_processed += matches.length;
|
// (handles nested arrays/objects that simple regex can't match)
|
||||||
|
const lines = output.split('\n');
|
||||||
|
let hasIndexResult = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Parse [QUEUE_STATUS] JSON
|
||||||
|
if (line.includes('[QUEUE_STATUS]')) {
|
||||||
|
const jsonStart = line.indexOf('{');
|
||||||
|
if (jsonStart !== -1) {
|
||||||
|
try {
|
||||||
|
const queueStatus: PendingQueueStatus = JSON.parse(line.slice(jsonStart));
|
||||||
|
watcher.stats.pending_queue = queueStatus;
|
||||||
|
broadcastToClients({
|
||||||
|
type: 'CODEXLENS_WATCHER_QUEUE_UPDATE',
|
||||||
|
payload: { path: targetPath, queue: queueStatus }
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[CodexLens] Failed to parse queue status:', e, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse [INDEX_RESULT] JSON
|
||||||
|
if (line.includes('[INDEX_RESULT]')) {
|
||||||
|
const jsonStart = line.indexOf('{');
|
||||||
|
if (jsonStart !== -1) {
|
||||||
|
try {
|
||||||
|
const indexResult: IndexResultDetail = JSON.parse(line.slice(jsonStart));
|
||||||
|
watcher.stats.last_index_result = indexResult;
|
||||||
|
watcher.stats.index_history.push(indexResult);
|
||||||
|
if (watcher.stats.index_history.length > 10) {
|
||||||
|
watcher.stats.index_history.shift();
|
||||||
|
}
|
||||||
|
watcher.stats.events_processed += indexResult.files_indexed + indexResult.files_removed;
|
||||||
|
watcher.stats.pending_queue = null;
|
||||||
|
hasIndexResult = true;
|
||||||
|
|
||||||
|
broadcastToClients({
|
||||||
|
type: 'CODEXLENS_WATCHER_INDEX_COMPLETE',
|
||||||
|
payload: { path: targetPath, result: indexResult }
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[CodexLens] Failed to parse index result:', e, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy event counting (fallback)
|
||||||
|
const matches = output.match(/Processed \d+ events?/g);
|
||||||
|
if (matches && !hasIndexResult) {
|
||||||
|
watcher.stats.events_processed += matches.length;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2111,6 +2184,68 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: Get Pending Queue Status
|
||||||
|
if (pathname === '/api/codexlens/watch/queue' && req.method === 'GET') {
|
||||||
|
const queryPath = url.searchParams.get('path');
|
||||||
|
const targetPath = queryPath || initialPath;
|
||||||
|
const normalizedPath = normalizePath(targetPath);
|
||||||
|
const watcher = activeWatchers.get(normalizedPath);
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
queue: watcher?.stats.pending_queue || { file_count: 0, files: [], countdown_seconds: 0, last_event_time: null }
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Flush Pending Queue (Immediate Index)
|
||||||
|
if (pathname === '/api/codexlens/watch/flush' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { path: watchPath } = body;
|
||||||
|
const targetPath = watchPath || initialPath;
|
||||||
|
const normalizedPath = normalizePath(targetPath);
|
||||||
|
|
||||||
|
const watcher = activeWatchers.get(normalizedPath);
|
||||||
|
if (!watcher) {
|
||||||
|
return { success: false, error: 'Watcher not running for this path', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create flush.signal file to trigger immediate indexing
|
||||||
|
const signalDir = path.join(targetPath, '.codexlens');
|
||||||
|
const signalFile = path.join(signalDir, 'flush.signal');
|
||||||
|
|
||||||
|
if (!fs.existsSync(signalDir)) {
|
||||||
|
fs.mkdirSync(signalDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(signalFile, Date.now().toString());
|
||||||
|
|
||||||
|
return { success: true, message: 'Flush signal sent' };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, error: err.message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Get Index History
|
||||||
|
if (pathname === '/api/codexlens/watch/history' && req.method === 'GET') {
|
||||||
|
const queryPath = url.searchParams.get('path');
|
||||||
|
const limitParam = url.searchParams.get('limit');
|
||||||
|
const limit = limitParam ? parseInt(limitParam, 10) : 10;
|
||||||
|
const targetPath = queryPath || initialPath;
|
||||||
|
const normalizedPath = normalizePath(targetPath);
|
||||||
|
const watcher = activeWatchers.get(normalizedPath);
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
history: watcher?.stats.index_history?.slice(-limit) || []
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -392,10 +392,11 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
|||||||
category: 'insight'
|
category: 'insight'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Try to parse JSON from response
|
// Try to parse JSON from response - use parsedOutput (extracted text) instead of raw stdout
|
||||||
let insights: { patterns: any[]; suggestions: any[] } = { patterns: [], suggestions: [] };
|
let insights: { patterns: any[]; suggestions: any[] } = { patterns: [], suggestions: [] };
|
||||||
if (result.stdout) {
|
const cliOutput = result.parsedOutput || result.stdout || '';
|
||||||
let outputText = result.stdout;
|
if (cliOutput) {
|
||||||
|
let outputText = cliOutput;
|
||||||
|
|
||||||
// Strip markdown code blocks if present
|
// Strip markdown code blocks if present
|
||||||
const codeBlockMatch = outputText.match(/```(?:json)?\s*([\s\S]*?)```/);
|
const codeBlockMatch = outputText.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||||
@@ -415,14 +416,14 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
|||||||
console.error('[insights/analyze] JSON parse error:', e);
|
console.error('[insights/analyze] JSON parse error:', e);
|
||||||
// Return raw output if JSON parse fails
|
// Return raw output if JSON parse fails
|
||||||
insights = {
|
insights = {
|
||||||
patterns: [{ type: 'raw_analysis', description: result.stdout.substring(0, 500), occurrences: 1, severity: 'low', suggestion: '' }],
|
patterns: [{ type: 'raw_analysis', description: cliOutput.substring(0, 500), occurrences: 1, severity: 'low', suggestion: '' }],
|
||||||
suggestions: []
|
suggestions: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No JSON found, wrap raw output
|
// No JSON found, wrap raw output
|
||||||
insights = {
|
insights = {
|
||||||
patterns: [{ type: 'raw_analysis', description: result.stdout.substring(0, 500), occurrences: 1, severity: 'low', suggestion: '' }],
|
patterns: [{ type: 'raw_analysis', description: cliOutput.substring(0, 500), occurrences: 1, severity: 'low', suggestion: '' }],
|
||||||
suggestions: []
|
suggestions: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -439,7 +440,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
|||||||
promptCount: prompts.length,
|
promptCount: prompts.length,
|
||||||
patterns: insights.patterns,
|
patterns: insights.patterns,
|
||||||
suggestions: insights.suggestions,
|
suggestions: insights.suggestions,
|
||||||
rawOutput: result.stdout || '',
|
rawOutput: cliOutput,
|
||||||
executionId: result.execution?.id,
|
executionId: result.execution?.id,
|
||||||
lang
|
lang
|
||||||
});
|
});
|
||||||
@@ -981,23 +982,26 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
|
|||||||
id: syncId
|
id: syncId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success && result.execution?.output) {
|
if (result.success) {
|
||||||
// Extract stdout from output object with proper serialization
|
// Prefer parsedOutput (extracted text from stream JSON) over raw execution output
|
||||||
const output = result.execution.output;
|
if (result.parsedOutput) {
|
||||||
if (typeof output === 'string') {
|
cliOutput = result.parsedOutput;
|
||||||
cliOutput = output;
|
} else if (result.execution?.output) {
|
||||||
} else if (output && typeof output === 'object') {
|
// Fallback to execution.output
|
||||||
// Handle object output - extract stdout or serialize the object
|
const output = result.execution.output;
|
||||||
if (output.stdout && typeof output.stdout === 'string') {
|
if (typeof output === 'string') {
|
||||||
cliOutput = output.stdout;
|
cliOutput = output;
|
||||||
} else if (output.stderr && typeof output.stderr === 'string') {
|
} else if (output && typeof output === 'object') {
|
||||||
cliOutput = output.stderr;
|
// Handle object output - extract stdout or serialize the object
|
||||||
} else {
|
if (output.stdout && typeof output.stdout === 'string') {
|
||||||
// Last resort: serialize the entire object as JSON
|
cliOutput = output.stdout;
|
||||||
cliOutput = JSON.stringify(output, null, 2);
|
} else if (output.stderr && typeof output.stderr === 'string') {
|
||||||
|
cliOutput = output.stderr;
|
||||||
|
} else {
|
||||||
|
// Last resort: serialize the entire object as JSON
|
||||||
|
cliOutput = JSON.stringify(output, null, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
cliOutput = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,310 @@ function deleteRule(ruleName, location, projectPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infer rule context from file name and subdirectory for better prompt generation
|
||||||
|
* @param {string} fileName - Rule file name
|
||||||
|
* @param {string} subdirectory - Optional subdirectory path
|
||||||
|
* @param {string} location - 'project' or 'user'
|
||||||
|
* @returns {Object} Inferred context
|
||||||
|
*/
|
||||||
|
function inferRuleContext(fileName: string, subdirectory: string, location: string) {
|
||||||
|
const normalizedName = fileName.replace(/\.md$/i, '').toLowerCase();
|
||||||
|
const normalizedSubdir = (subdirectory || '').toLowerCase();
|
||||||
|
|
||||||
|
// Rule category inference from file name and subdirectory
|
||||||
|
const categories = {
|
||||||
|
coding: ['coding', 'code', 'style', 'format', 'lint', 'convention'],
|
||||||
|
testing: ['test', 'spec', 'jest', 'vitest', 'mocha', 'coverage'],
|
||||||
|
security: ['security', 'auth', 'permission', 'access', 'secret', 'credential'],
|
||||||
|
architecture: ['arch', 'design', 'pattern', 'structure', 'module', 'layer'],
|
||||||
|
documentation: ['doc', 'comment', 'readme', 'jsdoc', 'api-doc'],
|
||||||
|
performance: ['perf', 'performance', 'optimize', 'cache', 'memory'],
|
||||||
|
workflow: ['workflow', 'ci', 'cd', 'deploy', 'build', 'release'],
|
||||||
|
tooling: ['tool', 'cli', 'script', 'npm', 'yarn', 'pnpm'],
|
||||||
|
error: ['error', 'exception', 'handling', 'logging', 'debug']
|
||||||
|
};
|
||||||
|
|
||||||
|
let inferredCategory = 'general';
|
||||||
|
let inferredKeywords: string[] = [];
|
||||||
|
|
||||||
|
for (const [category, keywords] of Object.entries(categories)) {
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (normalizedName.includes(keyword) || normalizedSubdir.includes(keyword)) {
|
||||||
|
inferredCategory = category;
|
||||||
|
inferredKeywords = keywords;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inferredCategory !== 'general') break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scope inference from location
|
||||||
|
const scopeHint = location === 'project'
|
||||||
|
? 'This rule applies to the current project only'
|
||||||
|
: 'This rule applies globally to all projects';
|
||||||
|
|
||||||
|
// Technology hints from file name
|
||||||
|
const techPatterns = {
|
||||||
|
typescript: ['ts', 'typescript', 'tsc'],
|
||||||
|
javascript: ['js', 'javascript', 'node'],
|
||||||
|
react: ['react', 'jsx', 'tsx', 'component'],
|
||||||
|
vue: ['vue', 'vuex', 'pinia'],
|
||||||
|
python: ['python', 'py', 'pip', 'poetry'],
|
||||||
|
rust: ['rust', 'cargo', 'rs'],
|
||||||
|
go: ['go', 'golang', 'mod'],
|
||||||
|
java: ['java', 'maven', 'gradle', 'spring']
|
||||||
|
};
|
||||||
|
|
||||||
|
let inferredTech: string | null = null;
|
||||||
|
for (const [tech, patterns] of Object.entries(techPatterns)) {
|
||||||
|
if (patterns.some(p => normalizedName.includes(p) || normalizedSubdir.includes(p))) {
|
||||||
|
inferredTech = tech;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: inferredCategory,
|
||||||
|
keywords: inferredKeywords,
|
||||||
|
scopeHint,
|
||||||
|
technology: inferredTech,
|
||||||
|
isConditional: normalizedSubdir.length > 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build structured prompt for rule generation
|
||||||
|
* @param {Object} params
|
||||||
|
* @returns {string} Structured prompt
|
||||||
|
*/
|
||||||
|
function buildStructuredRulePrompt(params: {
|
||||||
|
description: string;
|
||||||
|
fileName: string;
|
||||||
|
subdirectory: string;
|
||||||
|
location: string;
|
||||||
|
context: ReturnType<typeof inferRuleContext>;
|
||||||
|
enableReview?: boolean;
|
||||||
|
}) {
|
||||||
|
const { description, fileName, subdirectory, location, context, enableReview } = params;
|
||||||
|
|
||||||
|
// Build category-specific guidance
|
||||||
|
const categoryGuidance = {
|
||||||
|
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.',
|
||||||
|
architecture: 'Define module boundaries, dependency rules, layer responsibilities, and design pattern usage.',
|
||||||
|
documentation: 'Specify documentation requirements, comment styles, and API documentation standards.',
|
||||||
|
performance: 'Address caching strategies, optimization guidelines, resource management, and performance metrics.',
|
||||||
|
workflow: 'Define CI/CD requirements, deployment procedures, and release management rules.',
|
||||||
|
tooling: 'Specify tool configurations, script conventions, and dependency management rules.',
|
||||||
|
error: 'Define error handling patterns, logging requirements, and exception management.',
|
||||||
|
general: 'Provide clear, actionable guidelines that Claude can follow consistently.'
|
||||||
|
};
|
||||||
|
|
||||||
|
const guidance = categoryGuidance[context.category] || categoryGuidance.general;
|
||||||
|
|
||||||
|
// Build technology-specific hint
|
||||||
|
const techHint = context.technology
|
||||||
|
? `\nTECHNOLOGY CONTEXT: This rule is for ${context.technology} development. Use ${context.technology}-specific best practices and terminology.`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Build subdirectory context
|
||||||
|
const subdirHint = subdirectory
|
||||||
|
? `\nORGANIZATION: This rule will be placed in the "${subdirectory}" subdirectory, indicating its category/scope.`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Build review instruction if enabled
|
||||||
|
const reviewInstruction = enableReview
|
||||||
|
? `\n\nAFTER GENERATION:
|
||||||
|
- Verify the rule is specific and actionable
|
||||||
|
- Check for ambiguous language that could be misinterpreted
|
||||||
|
- Ensure examples are clear and relevant
|
||||||
|
- Validate markdown formatting is correct`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `PURPOSE: Generate a high-quality Claude Code memory rule that will guide Claude's behavior when working in this codebase
|
||||||
|
SUCCESS CRITERIA: The rule must be (1) specific and actionable, (2) include concrete examples, (3) avoid ambiguous language, (4) follow Claude Code rule format
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
• Parse the user's description to identify core requirements
|
||||||
|
• Infer additional context from file name "${fileName}" and category "${context.category}"
|
||||||
|
• Generate structured markdown content with clear instructions
|
||||||
|
• Include DO and DON'T examples where appropriate
|
||||||
|
• ${context.isConditional ? 'Consider if frontmatter paths are needed for conditional activation' : 'Create as a global rule'}
|
||||||
|
|
||||||
|
MODE: write
|
||||||
|
|
||||||
|
RULE CATEGORY: ${context.category}
|
||||||
|
CATEGORY GUIDANCE: ${guidance}
|
||||||
|
${techHint}
|
||||||
|
${subdirHint}
|
||||||
|
SCOPE: ${context.scopeHint}
|
||||||
|
|
||||||
|
EXPECTED OUTPUT FORMAT:
|
||||||
|
\`\`\`markdown
|
||||||
|
${context.isConditional ? `---
|
||||||
|
paths: [specific/path/patterns/**/*]
|
||||||
|
---
|
||||||
|
|
||||||
|
` : ''}# Rule Title
|
||||||
|
|
||||||
|
Brief description of what this rule enforces.
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
1. **First guideline** - Explanation
|
||||||
|
2. **Second guideline** - Explanation
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### ✅ Correct
|
||||||
|
\`\`\`language
|
||||||
|
// Good example
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### ❌ Incorrect
|
||||||
|
\`\`\`language
|
||||||
|
// Bad example
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
- When this rule may not apply
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
USER DESCRIPTION:
|
||||||
|
${description}
|
||||||
|
|
||||||
|
FILE NAME: ${fileName}
|
||||||
|
${subdirectory ? `SUBDIRECTORY: ${subdirectory}` : ''}
|
||||||
|
${reviewInstruction}
|
||||||
|
|
||||||
|
RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) | Generate ONLY the rule content in markdown | No additional commentary | Do NOT use any tools | Output raw markdown text directly | write=CREATE`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build structured prompt for code extraction
|
||||||
|
* @param {Object} params
|
||||||
|
* @returns {string} Structured prompt
|
||||||
|
*/
|
||||||
|
function buildExtractPrompt(params: {
|
||||||
|
extractScope: string;
|
||||||
|
extractFocus: string;
|
||||||
|
fileName: string;
|
||||||
|
subdirectory: string;
|
||||||
|
context: ReturnType<typeof inferRuleContext>;
|
||||||
|
}) {
|
||||||
|
const { extractScope, extractFocus, fileName, subdirectory, context } = params;
|
||||||
|
|
||||||
|
const scope = extractScope || '**/*';
|
||||||
|
const focus = extractFocus || 'naming conventions, error handling, code structure, patterns';
|
||||||
|
|
||||||
|
return `PURPOSE: Extract and document coding conventions from the existing codebase to create a Claude Code memory rule
|
||||||
|
SUCCESS CRITERIA: The rule must reflect ACTUAL patterns found in the code, not theoretical best practices
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
• Scan files matching "${scope}" for recurring patterns
|
||||||
|
• Identify ${focus.split(',').length} or more distinct conventions
|
||||||
|
• Document each pattern with real code examples from the codebase
|
||||||
|
• Create actionable rules based on observed practices
|
||||||
|
• Note any inconsistencies found (optional section)
|
||||||
|
|
||||||
|
MODE: analysis
|
||||||
|
|
||||||
|
ANALYSIS SCOPE: @${scope}
|
||||||
|
FOCUS AREAS: ${focus}
|
||||||
|
|
||||||
|
EXTRACTION STRATEGY:
|
||||||
|
1. **Pattern Recognition**: Look for repeated code structures, naming patterns, file organization
|
||||||
|
2. **Consistency Check**: Identify which patterns are consistently followed vs. occasionally violated
|
||||||
|
3. **Frequency Analysis**: Prioritize patterns that appear most frequently
|
||||||
|
4. **Context Awareness**: Consider why certain patterns are used (performance, readability, etc.)
|
||||||
|
|
||||||
|
EXPECTED OUTPUT FORMAT:
|
||||||
|
\`\`\`markdown
|
||||||
|
# ${fileName.replace(/\.md$/i, '')} Conventions
|
||||||
|
|
||||||
|
Conventions extracted from codebase analysis of \`${scope}\`.
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
- **Pattern name**: Description with example
|
||||||
|
\`\`\`language
|
||||||
|
// Actual code from codebase
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Code Structure
|
||||||
|
|
||||||
|
- **Pattern name**: Description with example
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- **Pattern name**: Description with example
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Any inconsistencies or variations observed
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
FILE NAME: ${fileName}
|
||||||
|
${subdirectory ? `SUBDIRECTORY: ${subdirectory}` : ''}
|
||||||
|
INFERRED CATEGORY: ${context.category}
|
||||||
|
|
||||||
|
RULES: $(cat ~/.claude/workflows/cli-templates/prompts/analysis/02-analyze-code-patterns.txt) | Extract REAL patterns from code | Include actual code snippets as examples | Do NOT use any tools | Output raw markdown text directly | analysis=READ-ONLY`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build review prompt for validating and improving generated rules
|
||||||
|
* @param {string} content - Generated rule content to review
|
||||||
|
* @param {string} fileName - Target file name
|
||||||
|
* @param {Object} context - Inferred context
|
||||||
|
* @returns {string} Review prompt
|
||||||
|
*/
|
||||||
|
function buildReviewPrompt(
|
||||||
|
content: string,
|
||||||
|
fileName: string,
|
||||||
|
context: ReturnType<typeof inferRuleContext>
|
||||||
|
) {
|
||||||
|
return `PURPOSE: Review and improve a Claude Code memory rule for quality, clarity, and actionability
|
||||||
|
SUCCESS CRITERIA: Output an improved version that is (1) more specific, (2) includes better examples, (3) has no ambiguous language
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
• Analyze the rule for clarity and specificity
|
||||||
|
• Check if guidelines are actionable (Claude can follow them)
|
||||||
|
• Verify examples are concrete and helpful
|
||||||
|
• Remove any ambiguous or vague language
|
||||||
|
• Ensure markdown formatting is correct
|
||||||
|
• Improve structure if needed
|
||||||
|
• Keep the core intent and requirements intact
|
||||||
|
|
||||||
|
MODE: write
|
||||||
|
|
||||||
|
REVIEW CRITERIA:
|
||||||
|
1. **Specificity**: Each guideline should be specific enough to follow without interpretation
|
||||||
|
2. **Actionability**: Guidelines should tell Claude exactly what to do or not do
|
||||||
|
3. **Examples**: Good and bad examples should be clearly different and illustrative
|
||||||
|
4. **Consistency**: Formatting and style should be consistent throughout
|
||||||
|
5. **Completeness**: All necessary aspects of the rule should be covered
|
||||||
|
6. **Conciseness**: No unnecessary verbosity or repetition
|
||||||
|
|
||||||
|
RULE CATEGORY: ${context.category}
|
||||||
|
FILE NAME: ${fileName}
|
||||||
|
|
||||||
|
ORIGINAL RULE CONTENT:
|
||||||
|
\`\`\`markdown
|
||||||
|
${content}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
EXPECTED OUTPUT:
|
||||||
|
- Output ONLY the improved rule content in markdown format
|
||||||
|
- Do NOT include any commentary, explanation, or meta-text
|
||||||
|
- If the original is already high quality, return it unchanged
|
||||||
|
- Preserve any frontmatter (---paths---) if present
|
||||||
|
|
||||||
|
RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) | Output ONLY improved markdown content | No additional text | Do NOT use any tools | Output raw markdown text directly | write=CREATE`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate rule content via CLI tool
|
* Generate rule content via CLI tool
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
@@ -233,6 +537,7 @@ function deleteRule(ruleName, location, projectPath) {
|
|||||||
* @param {string} params.location - 'project' or 'user'
|
* @param {string} params.location - 'project' or 'user'
|
||||||
* @param {string} params.subdirectory - Optional subdirectory
|
* @param {string} params.subdirectory - Optional subdirectory
|
||||||
* @param {string} params.projectPath - Project root path
|
* @param {string} params.projectPath - Project root path
|
||||||
|
* @param {boolean} params.enableReview - Optional: enable secondary review
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async function generateRuleViaCLI(params) {
|
async function generateRuleViaCLI(params) {
|
||||||
@@ -246,47 +551,47 @@ async function generateRuleViaCLI(params) {
|
|||||||
fileName,
|
fileName,
|
||||||
location,
|
location,
|
||||||
subdirectory,
|
subdirectory,
|
||||||
projectPath
|
projectPath,
|
||||||
|
enableReview
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let prompt = '';
|
let prompt = '';
|
||||||
let mode = 'analysis';
|
let mode = 'analysis';
|
||||||
let workingDir = projectPath;
|
let workingDir = projectPath;
|
||||||
|
|
||||||
|
// Infer context from file name and subdirectory
|
||||||
|
const context = inferRuleContext(fileName, subdirectory || '', location);
|
||||||
|
|
||||||
// Build prompt based on generation type
|
// Build prompt based on generation type
|
||||||
if (generationType === 'description') {
|
if (generationType === 'description') {
|
||||||
mode = 'write';
|
mode = 'write';
|
||||||
prompt = `PURPOSE: Generate Claude Code memory rule from description to guide Claude's behavior
|
prompt = buildStructuredRulePrompt({
|
||||||
TASK: • Analyze rule requirements • Generate markdown content with clear instructions
|
description,
|
||||||
MODE: write
|
fileName,
|
||||||
EXPECTED: Complete rule content in markdown format
|
subdirectory: subdirectory || '',
|
||||||
RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) | Follow Claude Code rule format | Use frontmatter for conditional rules if paths specified | write=CREATE
|
location,
|
||||||
|
context,
|
||||||
RULE DESCRIPTION:
|
enableReview
|
||||||
${description}
|
});
|
||||||
|
|
||||||
FILE NAME: ${fileName}`;
|
|
||||||
} else if (generationType === 'template') {
|
} else if (generationType === 'template') {
|
||||||
mode = 'write';
|
mode = 'write';
|
||||||
prompt = `PURPOSE: Generate Claude Code rule from template type
|
prompt = `PURPOSE: Generate Claude Code rule from template type
|
||||||
TASK: • Create rule based on ${templateType} template • Generate structured markdown content
|
TASK: • Create rule based on ${templateType} template • Generate structured markdown content
|
||||||
MODE: write
|
MODE: write
|
||||||
EXPECTED: Complete rule content in markdown format following template structure
|
EXPECTED: Complete rule content in markdown format following template structure
|
||||||
RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) | Follow Claude Code rule format | Use ${templateType} template patterns | write=CREATE
|
RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) | Follow Claude Code rule format | Use ${templateType} template patterns | Do NOT use any tools | Output raw markdown text directly | write=CREATE
|
||||||
|
|
||||||
TEMPLATE TYPE: ${templateType}
|
TEMPLATE TYPE: ${templateType}
|
||||||
FILE NAME: ${fileName}`;
|
FILE NAME: ${fileName}`;
|
||||||
} else if (generationType === 'extract') {
|
} else if (generationType === 'extract') {
|
||||||
mode = 'analysis';
|
mode = 'analysis';
|
||||||
prompt = `PURPOSE: Extract coding rules from existing codebase to document patterns and conventions
|
prompt = buildExtractPrompt({
|
||||||
TASK: • Analyze code patterns in specified scope • Extract common conventions • Identify best practices
|
extractScope: extractScope || '',
|
||||||
MODE: analysis
|
extractFocus: extractFocus || '',
|
||||||
CONTEXT: @${extractScope || '**/*'}
|
fileName,
|
||||||
EXPECTED: Rule content based on codebase analysis with examples
|
subdirectory: subdirectory || '',
|
||||||
RULES: $(cat ~/.claude/workflows/cli-templates/prompts/analysis/02-analyze-code-patterns.txt) | Focus on actual patterns found | Include code examples | analysis=READ-ONLY
|
context
|
||||||
|
});
|
||||||
ANALYSIS SCOPE: ${extractScope || '**/*'}
|
|
||||||
FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structure'}`;
|
|
||||||
} else {
|
} else {
|
||||||
return { error: `Unknown generation type: ${generationType}` };
|
return { error: `Unknown generation type: ${generationType}` };
|
||||||
}
|
}
|
||||||
@@ -308,8 +613,15 @@ FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structu
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract generated content from stdout
|
// Extract generated content - prefer parsedOutput (extracted text from stream JSON)
|
||||||
const generatedContent = result.stdout.trim();
|
let generatedContent = (result.parsedOutput || result.stdout || '').trim();
|
||||||
|
|
||||||
|
// Remove markdown code block wrapper if present (e.g., ```markdown...```)
|
||||||
|
if (generatedContent.startsWith('```markdown')) {
|
||||||
|
generatedContent = generatedContent.replace(/^```markdown\s*\n?/, '').replace(/\n?```\s*$/, '');
|
||||||
|
} else if (generatedContent.startsWith('```')) {
|
||||||
|
generatedContent = generatedContent.replace(/^```\w*\s*\n?/, '').replace(/\n?```\s*$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
if (!generatedContent) {
|
if (!generatedContent) {
|
||||||
return {
|
return {
|
||||||
@@ -319,6 +631,40 @@ FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structu
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional review step - verify and improve the generated rule
|
||||||
|
let reviewResult = null;
|
||||||
|
if (enableReview) {
|
||||||
|
const reviewPrompt = buildReviewPrompt(generatedContent, fileName, context);
|
||||||
|
|
||||||
|
const reviewExecution = await executeCliTool({
|
||||||
|
tool: 'claude',
|
||||||
|
prompt: reviewPrompt,
|
||||||
|
mode: 'write',
|
||||||
|
cd: workingDir,
|
||||||
|
timeout: 300000, // 5 minutes for review
|
||||||
|
category: 'internal'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reviewExecution.success) {
|
||||||
|
let reviewedContent = (reviewExecution.parsedOutput || reviewExecution.stdout || '').trim();
|
||||||
|
// Remove markdown code block wrapper if present
|
||||||
|
if (reviewedContent.startsWith('```markdown')) {
|
||||||
|
reviewedContent = reviewedContent.replace(/^```markdown\s*\n?/, '').replace(/\n?```\s*$/, '');
|
||||||
|
} else if (reviewedContent.startsWith('```')) {
|
||||||
|
reviewedContent = reviewedContent.replace(/^```\w*\s*\n?/, '').replace(/\n?```\s*$/, '');
|
||||||
|
}
|
||||||
|
// Only use reviewed content if it's valid and different
|
||||||
|
if (reviewedContent.length > 50 && reviewedContent !== generatedContent) {
|
||||||
|
generatedContent = reviewedContent;
|
||||||
|
reviewResult = {
|
||||||
|
reviewed: true,
|
||||||
|
originalLength: (result.parsedOutput || result.stdout || '').trim().length,
|
||||||
|
reviewedLength: reviewedContent.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the rule using the generated content
|
// Create the rule using the generated content
|
||||||
const createResult = await createRule({
|
const createResult = await createRule({
|
||||||
fileName,
|
fileName,
|
||||||
@@ -333,7 +679,8 @@ FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structu
|
|||||||
success: createResult.success || false,
|
success: createResult.success || false,
|
||||||
...createResult,
|
...createResult,
|
||||||
generatedContent,
|
generatedContent,
|
||||||
executionId: result.conversation?.id
|
executionId: result.conversation?.id,
|
||||||
|
review: reviewResult
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: (error as Error).message };
|
return { error: (error as Error).message };
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ Create a new Claude Code skill with the following specifications:
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return {
|
return {
|
||||||
error: `CLI generation failed: ${result.stderr || 'Unknown error'}`,
|
error: `CLI generation failed: ${result.stderr || 'Unknown error'}`,
|
||||||
stdout: result.stdout,
|
stdout: result.parsedOutput || result.stdout,
|
||||||
stderr: result.stderr
|
stderr: result.stderr
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -493,7 +493,7 @@ Create a new Claude Code skill with the following specifications:
|
|||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
return {
|
return {
|
||||||
error: `Generated skill is invalid: ${validation.errors.join(', ')}`,
|
error: `Generated skill is invalid: ${validation.errors.join(', ')}`,
|
||||||
stdout: result.stdout,
|
stdout: result.parsedOutput || result.stdout,
|
||||||
stderr: result.stderr
|
stderr: result.stderr
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -503,7 +503,7 @@ Create a new Claude Code skill with the following specifications:
|
|||||||
skillName: validation.skillInfo.name,
|
skillName: validation.skillInfo.name,
|
||||||
location,
|
location,
|
||||||
path: targetPath,
|
path: targetPath,
|
||||||
stdout: result.stdout,
|
stdout: result.parsedOutput || result.stdout,
|
||||||
stderr: result.stderr
|
stderr: result.stderr
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -472,6 +472,65 @@ function handleNotification(data) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'CODEXLENS_WATCHER_UPDATE':
|
||||||
|
// Handle CodexLens watcher real-time updates (file changes detected)
|
||||||
|
if (typeof handleWatcherStatusUpdate === 'function') {
|
||||||
|
handleWatcherStatusUpdate(payload);
|
||||||
|
}
|
||||||
|
console.log('[CodexLens] Watcher update:', payload.events_processed, 'events');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'CODEXLENS_WATCHER_QUEUE_UPDATE':
|
||||||
|
// Handle pending queue status updates
|
||||||
|
if (typeof updatePendingQueueUI === 'function') {
|
||||||
|
updatePendingQueueUI(payload.queue);
|
||||||
|
}
|
||||||
|
// Add activity log entries only for NEW files (not already logged)
|
||||||
|
if (payload.queue && payload.queue.files && payload.queue.files.length > 0) {
|
||||||
|
if (typeof addWatcherLogEntry === 'function') {
|
||||||
|
// Track logged files to avoid duplicates
|
||||||
|
window._watcherLoggedFiles = window._watcherLoggedFiles || new Set();
|
||||||
|
var newFiles = payload.queue.files.filter(function(f) {
|
||||||
|
return !window._watcherLoggedFiles.has(f);
|
||||||
|
});
|
||||||
|
// Only show first few new files to avoid spam
|
||||||
|
newFiles.slice(0, 5).forEach(function(fileName) {
|
||||||
|
window._watcherLoggedFiles.add(fileName);
|
||||||
|
addWatcherLogEntry('modified', fileName);
|
||||||
|
});
|
||||||
|
// Clear tracking when queue is empty (after flush)
|
||||||
|
if (payload.queue.file_count === 0) {
|
||||||
|
window._watcherLoggedFiles.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('[CodexLens] Queue update:', payload.queue?.file_count, 'files pending');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'CODEXLENS_WATCHER_INDEX_COMPLETE':
|
||||||
|
// Handle index completion event
|
||||||
|
if (typeof updateLastIndexResult === 'function') {
|
||||||
|
updateLastIndexResult(payload.result);
|
||||||
|
}
|
||||||
|
// Clear logged files tracking after index completes
|
||||||
|
if (window._watcherLoggedFiles) {
|
||||||
|
window._watcherLoggedFiles.clear();
|
||||||
|
}
|
||||||
|
// Add activity log entry for index completion
|
||||||
|
if (typeof addWatcherLogEntry === 'function' && payload.result) {
|
||||||
|
var summary = 'Indexed ' + (payload.result.files_indexed || 0) + ' files';
|
||||||
|
addWatcherLogEntry('indexed', summary);
|
||||||
|
}
|
||||||
|
// Show toast notification
|
||||||
|
if (typeof showRefreshToast === 'function' && payload.result) {
|
||||||
|
var indexMsg = 'Indexed ' + (payload.result.files_indexed || 0) + ' files, ' +
|
||||||
|
(payload.result.symbols_added || 0) + ' symbols';
|
||||||
|
var toastType = (payload.result.errors && payload.result.errors.length > 0) ? 'warning' : 'success';
|
||||||
|
showRefreshToast(indexMsg, toastType);
|
||||||
|
}
|
||||||
|
console.log('[CodexLens] Index complete:', payload.result?.files_indexed, 'files indexed');
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('[WS] Unknown notification type:', type);
|
console.log('[WS] Unknown notification type:', type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1363,6 +1363,8 @@ const i18n = {
|
|||||||
'rules.extractScopeRequired': 'Analysis scope is required',
|
'rules.extractScopeRequired': 'Analysis scope is required',
|
||||||
'rules.extractFocus': 'Focus Areas',
|
'rules.extractFocus': 'Focus Areas',
|
||||||
'rules.extractFocusHint': 'Comma-separated aspects to focus on (e.g., naming, error-handling)',
|
'rules.extractFocusHint': 'Comma-separated aspects to focus on (e.g., naming, error-handling)',
|
||||||
|
'rules.enableReview': 'Enable Quality Review',
|
||||||
|
'rules.enableReviewHint': 'AI will verify the generated rule for clarity, actionability, and proper formatting',
|
||||||
'rules.cliGenerating': 'Generating rule via CLI (this may take a few minutes)...',
|
'rules.cliGenerating': 'Generating rule via CLI (this may take a few minutes)...',
|
||||||
|
|
||||||
// CLAUDE.md Manager
|
// CLAUDE.md Manager
|
||||||
@@ -3375,6 +3377,8 @@ const i18n = {
|
|||||||
'rules.extractScopeRequired': '分析范围是必需的',
|
'rules.extractScopeRequired': '分析范围是必需的',
|
||||||
'rules.extractFocus': '关注领域',
|
'rules.extractFocus': '关注领域',
|
||||||
'rules.extractFocusHint': '以逗号分隔的关注方面(例如:命名规范, 错误处理)',
|
'rules.extractFocusHint': '以逗号分隔的关注方面(例如:命名规范, 错误处理)',
|
||||||
|
'rules.enableReview': '启用质量审查',
|
||||||
|
'rules.enableReviewHint': 'AI 将验证生成的规则是否清晰、可操作且格式正确',
|
||||||
'rules.cliGenerating': '正在通过 CLI 生成规则(可能需要几分钟)...',
|
'rules.cliGenerating': '正在通过 CLI 生成规则(可能需要几分钟)...',
|
||||||
|
|
||||||
// CLAUDE.md Manager
|
// CLAUDE.md Manager
|
||||||
|
|||||||
@@ -636,12 +636,6 @@ function renderFileMetadata() {
|
|||||||
'<option value="gemini">Gemini</option>' +
|
'<option value="gemini">Gemini</option>' +
|
||||||
'<option value="qwen">Qwen</option>' +
|
'<option value="qwen">Qwen</option>' +
|
||||||
'</select>' +
|
'</select>' +
|
||||||
'<label>' + (t('claude.mode') || 'Mode') + '</label>' +
|
|
||||||
'<select id="cliModeSelect" class="sync-select">' +
|
|
||||||
'<option value="update">' + (t('claude.modeUpdate') || 'Update (Smart Merge)') + '</option>' +
|
|
||||||
'<option value="generate">' + (t('claude.modeGenerate') || 'Generate (Full Replace)') + '</option>' +
|
|
||||||
'<option value="append">' + (t('claude.modeAppend') || 'Append') + '</option>' +
|
|
||||||
'</select>' +
|
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<button class="btn btn-sm btn-primary full-width sync-button" onclick="syncFileWithCLI()" id="cliSyncButton">' +
|
'<button class="btn btn-sm btn-primary full-width sync-button" onclick="syncFileWithCLI()" id="cliSyncButton">' +
|
||||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> ' +
|
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> ' +
|
||||||
@@ -664,7 +658,7 @@ async function syncFileWithCLI() {
|
|||||||
if (!selectedFile) return;
|
if (!selectedFile) return;
|
||||||
|
|
||||||
var tool = document.getElementById('cliToolSelect').value;
|
var tool = document.getElementById('cliToolSelect').value;
|
||||||
var mode = document.getElementById('cliModeSelect').value;
|
var mode = 'generate'; // Default to full replace mode
|
||||||
|
|
||||||
// Show progress
|
// Show progress
|
||||||
showSyncProgress(true, tool);
|
showSyncProgress(true, tool);
|
||||||
|
|||||||
@@ -4536,10 +4536,25 @@ window.toggleWatcher = async function toggleWatcher() {
|
|||||||
// Check current status first
|
// Check current status first
|
||||||
try {
|
try {
|
||||||
console.log('[CodexLens] Checking watcher status...');
|
console.log('[CodexLens] Checking watcher status...');
|
||||||
var statusResponse = await fetch('/api/codexlens/watch/status');
|
// Pass path parameter to get specific watcher status
|
||||||
|
var statusResponse = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath));
|
||||||
var statusResult = await statusResponse.json();
|
var statusResult = await statusResponse.json();
|
||||||
console.log('[CodexLens] Status result:', statusResult);
|
console.log('[CodexLens] Status result:', statusResult);
|
||||||
var isRunning = statusResult.success && statusResult.running;
|
|
||||||
|
// Handle both single watcher response and array response
|
||||||
|
var isRunning = false;
|
||||||
|
if (statusResult.success) {
|
||||||
|
if (typeof statusResult.running === 'boolean') {
|
||||||
|
isRunning = statusResult.running;
|
||||||
|
} else if (statusResult.watchers && Array.isArray(statusResult.watchers)) {
|
||||||
|
var normalizedPath = projectPath.toLowerCase().replace(/\\/g, '/');
|
||||||
|
var matchingWatcher = statusResult.watchers.find(function(w) {
|
||||||
|
var watcherPath = (w.root_path || '').toLowerCase().replace(/\\/g, '/');
|
||||||
|
return watcherPath === normalizedPath || watcherPath.includes(normalizedPath) || normalizedPath.includes(watcherPath);
|
||||||
|
});
|
||||||
|
isRunning = matchingWatcher ? matchingWatcher.running : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle: if running, stop; if stopped, start
|
// Toggle: if running, stop; if stopped, start
|
||||||
var action = isRunning ? 'stop' : 'start';
|
var action = isRunning ? 'stop' : 'start';
|
||||||
@@ -4592,7 +4607,8 @@ function updateWatcherUI(running, stats) {
|
|||||||
var uptimeDisplay = document.getElementById('watcherUptimeDisplay');
|
var uptimeDisplay = document.getElementById('watcherUptimeDisplay');
|
||||||
|
|
||||||
if (filesCount) filesCount.textContent = stats.files_watched || '-';
|
if (filesCount) filesCount.textContent = stats.files_watched || '-';
|
||||||
if (changesCount) changesCount.textContent = stats.changes_detected || '0';
|
// Support both changes_detected and events_processed
|
||||||
|
if (changesCount) changesCount.textContent = stats.events_processed || stats.changes_detected || '0';
|
||||||
if (uptimeDisplay) uptimeDisplay.textContent = formatUptime(stats.uptime_seconds);
|
if (uptimeDisplay) uptimeDisplay.textContent = formatUptime(stats.uptime_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4628,17 +4644,25 @@ function startWatcherPolling() {
|
|||||||
if (watcherPollInterval) return; // Already polling
|
if (watcherPollInterval) return; // Already polling
|
||||||
|
|
||||||
watcherStartTime = Date.now();
|
watcherStartTime = Date.now();
|
||||||
|
var projectPath = window.CCW_PROJECT_ROOT || '.';
|
||||||
|
|
||||||
watcherPollInterval = setInterval(async function() {
|
watcherPollInterval = setInterval(async function() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/codexlens/watch/status');
|
// Must include path parameter to get specific watcher status
|
||||||
|
var response = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath));
|
||||||
var result = await response.json();
|
var result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.running) {
|
if (result.success && result.running) {
|
||||||
// Update uptime
|
// Update uptime from server response
|
||||||
var uptimeDisplay = document.getElementById('watcherUptimeDisplay');
|
var uptimeDisplay = document.getElementById('watcherUptimeDisplay');
|
||||||
if (uptimeDisplay) {
|
if (uptimeDisplay && result.uptime_seconds !== undefined) {
|
||||||
var uptime = (Date.now() - watcherStartTime) / 1000;
|
uptimeDisplay.textContent = formatUptime(result.uptime_seconds);
|
||||||
uptimeDisplay.textContent = formatUptime(uptime);
|
}
|
||||||
|
|
||||||
|
// Update changes count from events_processed
|
||||||
|
if (result.events_processed !== undefined) {
|
||||||
|
var changesCount = document.getElementById('watcherChangesCount');
|
||||||
|
if (changesCount) changesCount.textContent = result.events_processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update files count if available
|
// Update files count if available
|
||||||
@@ -4653,8 +4677,8 @@ function startWatcherPolling() {
|
|||||||
addWatcherLogEntry(event.type, event.path);
|
addWatcherLogEntry(event.type, event.path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (!result.running) {
|
} else if (result.success && result.running === false) {
|
||||||
// Watcher stopped externally
|
// Watcher stopped externally (only if running is explicitly false)
|
||||||
updateWatcherUI(false);
|
updateWatcherUI(false);
|
||||||
stopWatcherPolling();
|
stopWatcherPolling();
|
||||||
}
|
}
|
||||||
@@ -4699,13 +4723,15 @@ function addWatcherLogEntry(type, path) {
|
|||||||
'created': 'text-success',
|
'created': 'text-success',
|
||||||
'modified': 'text-warning',
|
'modified': 'text-warning',
|
||||||
'deleted': 'text-destructive',
|
'deleted': 'text-destructive',
|
||||||
'renamed': 'text-primary'
|
'renamed': 'text-primary',
|
||||||
|
'indexed': 'text-success'
|
||||||
};
|
};
|
||||||
var typeIcons = {
|
var typeIcons = {
|
||||||
'created': '+',
|
'created': '+',
|
||||||
'modified': '~',
|
'modified': '~',
|
||||||
'deleted': '-',
|
'deleted': '-',
|
||||||
'renamed': '→'
|
'renamed': '→',
|
||||||
|
'indexed': '✓'
|
||||||
};
|
};
|
||||||
|
|
||||||
var colorClass = typeColors[type] || 'text-muted-foreground';
|
var colorClass = typeColors[type] || 'text-muted-foreground';
|
||||||
@@ -4748,13 +4774,35 @@ function clearWatcherLog() {
|
|||||||
*/
|
*/
|
||||||
async function initWatcherStatus() {
|
async function initWatcherStatus() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/codexlens/watch/status');
|
var projectPath = window.CCW_PROJECT_ROOT || '.';
|
||||||
|
// Pass path parameter to get specific watcher status
|
||||||
|
var response = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath));
|
||||||
var result = await response.json();
|
var result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
updateWatcherUI(result.running, {
|
// Handle both single watcher response (with path param) and array response (without path param)
|
||||||
files_watched: result.files_watched,
|
var running = result.running;
|
||||||
|
var uptime = result.uptime_seconds || 0;
|
||||||
|
var filesWatched = result.files_watched;
|
||||||
|
|
||||||
|
// If response has watchers array (no path param), find matching watcher
|
||||||
|
if (result.watchers && Array.isArray(result.watchers)) {
|
||||||
|
var normalizedPath = projectPath.toLowerCase().replace(/\\/g, '/');
|
||||||
|
var matchingWatcher = result.watchers.find(function(w) {
|
||||||
|
var watcherPath = (w.root_path || '').toLowerCase().replace(/\\/g, '/');
|
||||||
|
return watcherPath === normalizedPath || watcherPath.includes(normalizedPath) || normalizedPath.includes(watcherPath);
|
||||||
|
});
|
||||||
|
if (matchingWatcher) {
|
||||||
|
running = matchingWatcher.running;
|
||||||
|
uptime = matchingWatcher.uptime_seconds || 0;
|
||||||
|
} else {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWatcherUI(running, {
|
||||||
|
files_watched: filesWatched,
|
||||||
changes_detected: 0,
|
changes_detected: 0,
|
||||||
uptime_seconds: result.uptime_seconds
|
uptime_seconds: uptime
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -5846,6 +5894,45 @@ function buildWatcherControlContent(status, defaultPath) {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
|
||||||
|
// Pending Queue Section (shown when running)
|
||||||
|
'<div id="watcherPendingQueue" class="tool-config-section" style="display:' + (running ? 'block' : 'none') + '">' +
|
||||||
|
'<div class="flex items-center justify-between mb-2">' +
|
||||||
|
'<h4 class="flex items-center gap-2 m-0">' +
|
||||||
|
'<i data-lucide="clock" class="w-4 h-4"></i>' +
|
||||||
|
(t('codexlens.pendingChanges') || 'Pending Changes') +
|
||||||
|
'</h4>' +
|
||||||
|
'<button onclick="flushWatcherNow()" class="btn btn-sm btn-primary" id="flushNowBtn" disabled>' +
|
||||||
|
'<i data-lucide="zap" class="w-3 h-3 mr-1"></i>' +
|
||||||
|
(t('codexlens.indexNow') || 'Index Now') +
|
||||||
|
'</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="flex items-center justify-between p-3 bg-muted/20 rounded-lg mb-2">' +
|
||||||
|
'<div>' +
|
||||||
|
'<span class="text-2xl font-bold text-warning" id="pendingFileCount">0</span>' +
|
||||||
|
'<span class="text-sm text-muted-foreground ml-1">' + (t('codexlens.filesWaiting') || 'files waiting') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="text-right">' +
|
||||||
|
'<div class="text-lg font-mono" id="countdownTimer">--:--</div>' +
|
||||||
|
'<div class="text-xs text-muted-foreground">' + (t('codexlens.untilNextIndex') || 'until next index') + '</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div id="pendingFilesList" class="max-h-24 overflow-y-auto space-y-1 text-sm"></div>' +
|
||||||
|
'</div>' +
|
||||||
|
|
||||||
|
// Last Index Result (shown when running)
|
||||||
|
'<div id="watcherLastIndex" class="tool-config-section" style="display:none">' +
|
||||||
|
'<div class="flex items-center justify-between mb-2">' +
|
||||||
|
'<h4 class="flex items-center gap-2 m-0">' +
|
||||||
|
'<i data-lucide="check-circle" class="w-4 h-4"></i>' +
|
||||||
|
(t('codexlens.lastIndexResult') || 'Last Index Result') +
|
||||||
|
'</h4>' +
|
||||||
|
'<button onclick="showIndexHistory()" class="text-xs text-muted-foreground hover:text-foreground">' +
|
||||||
|
(t('codexlens.viewHistory') || 'View History') +
|
||||||
|
'</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="grid grid-cols-4 gap-2 text-center" id="lastIndexStats"></div>' +
|
||||||
|
'</div>' +
|
||||||
|
|
||||||
// Start Configuration (shown when not running)
|
// Start Configuration (shown when not running)
|
||||||
'<div id="watcherStartConfig" class="tool-config-section" style="display:' + (running ? 'none' : 'block') + '">' +
|
'<div id="watcherStartConfig" class="tool-config-section" style="display:' + (running ? 'none' : 'block') + '">' +
|
||||||
'<h4>' + (t('codexlens.watcherConfig') || 'Configuration') + '</h4>' +
|
'<h4>' + (t('codexlens.watcherConfig') || 'Configuration') + '</h4>' +
|
||||||
@@ -5857,7 +5944,7 @@ function buildWatcherControlContent(status, defaultPath) {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<div>' +
|
'<div>' +
|
||||||
'<label class="block text-sm font-medium mb-1.5">' + (t('codexlens.debounceMs') || 'Debounce (ms)') + '</label>' +
|
'<label class="block text-sm font-medium mb-1.5">' + (t('codexlens.debounceMs') || 'Debounce (ms)') + '</label>' +
|
||||||
'<input type="number" id="watcherDebounce" value="1000" min="100" max="10000" step="100" ' +
|
'<input type="number" id="watcherDebounce" value="60000" min="1000" max="120000" step="1000" ' +
|
||||||
'class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" />' +
|
'class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" />' +
|
||||||
'<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.debounceHint') || 'Time to wait before processing file changes') + '</p>' +
|
'<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.debounceHint') || 'Time to wait before processing file changes') + '</p>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
@@ -5986,6 +6073,204 @@ function stopWatcherStatusPolling() {
|
|||||||
clearInterval(watcherPollingInterval);
|
clearInterval(watcherPollingInterval);
|
||||||
watcherPollingInterval = null;
|
watcherPollingInterval = null;
|
||||||
}
|
}
|
||||||
|
stopCountdownTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Countdown timer for pending queue
|
||||||
|
var countdownInterval = null;
|
||||||
|
var currentCountdownSeconds = 0;
|
||||||
|
|
||||||
|
function startCountdownTimer(seconds) {
|
||||||
|
currentCountdownSeconds = seconds;
|
||||||
|
if (countdownInterval) return;
|
||||||
|
|
||||||
|
countdownInterval = setInterval(function() {
|
||||||
|
var timerEl = document.getElementById('countdownTimer');
|
||||||
|
if (!timerEl) {
|
||||||
|
stopCountdownTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCountdownSeconds <= 0) {
|
||||||
|
timerEl.textContent = '--:--';
|
||||||
|
} else {
|
||||||
|
currentCountdownSeconds--;
|
||||||
|
timerEl.textContent = formatCountdown(currentCountdownSeconds);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCountdownTimer() {
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
countdownInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCountdown(seconds) {
|
||||||
|
if (seconds <= 0) return '--:--';
|
||||||
|
var mins = Math.floor(seconds / 60);
|
||||||
|
var secs = seconds % 60;
|
||||||
|
return (mins < 10 ? '0' : '') + mins + ':' + (secs < 10 ? '0' : '') + secs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately flush pending queue and trigger indexing
|
||||||
|
*/
|
||||||
|
async function flushWatcherNow() {
|
||||||
|
var btn = document.getElementById('flushNowBtn');
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i data-lucide="loader-2" class="w-3 h-3 mr-1 animate-spin"></i> Indexing...';
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var watchPath = document.getElementById('watcherPath');
|
||||||
|
var path = watchPath ? watchPath.value.trim() : '';
|
||||||
|
|
||||||
|
var response = await fetch('/api/codexlens/watch/flush', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path: path || undefined })
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showRefreshToast(t('codexlens.indexTriggered') || 'Indexing triggered', 'success');
|
||||||
|
} else {
|
||||||
|
showRefreshToast(t('common.error') + ': ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i data-lucide="zap" class="w-3 h-3 mr-1"></i>' + (t('codexlens.indexNow') || 'Index Now');
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.flushWatcherNow = flushWatcherNow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show index history in a modal
|
||||||
|
*/
|
||||||
|
async function showIndexHistory() {
|
||||||
|
try {
|
||||||
|
var watchPath = document.getElementById('watcherPath');
|
||||||
|
var path = watchPath ? watchPath.value.trim() : '';
|
||||||
|
|
||||||
|
var response = await fetch('/api/codexlens/watch/history?limit=10&path=' + encodeURIComponent(path));
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success || !result.history || result.history.length === 0) {
|
||||||
|
showRefreshToast(t('codexlens.noHistory') || 'No index history available', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var historyHtml = result.history.slice().reverse().map(function(h, i) {
|
||||||
|
var timestamp = h.timestamp ? new Date(h.timestamp * 1000).toLocaleString() : 'Unknown';
|
||||||
|
return '<div class="p-3 border-b border-border last:border-0">' +
|
||||||
|
'<div class="flex justify-between items-center mb-2">' +
|
||||||
|
'<span class="text-sm font-medium">#' + (result.history.length - i) + '</span>' +
|
||||||
|
'<span class="text-xs text-muted-foreground">' + timestamp + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="grid grid-cols-4 gap-2 text-center text-sm">' +
|
||||||
|
'<div><span class="text-success">' + (h.files_indexed || 0) + '</span> indexed</div>' +
|
||||||
|
'<div><span class="text-warning">' + (h.files_removed || 0) + '</span> removed</div>' +
|
||||||
|
'<div><span class="text-primary">+' + (h.symbols_added || 0) + '</span> symbols</div>' +
|
||||||
|
'<div><span class="text-destructive">' + ((h.errors && h.errors.length) || 0) + '</span> errors</div>' +
|
||||||
|
'</div>' +
|
||||||
|
(h.errors && h.errors.length > 0 ? '<div class="mt-2 text-xs text-destructive">' +
|
||||||
|
h.errors.slice(0, 2).map(function(e) { return '<div>• ' + e + '</div>'; }).join('') +
|
||||||
|
(h.errors.length > 2 ? '<div>... and ' + (h.errors.length - 2) + ' more</div>' : '') +
|
||||||
|
'</div>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
var modal = document.createElement('div');
|
||||||
|
modal.id = 'indexHistoryModal';
|
||||||
|
modal.className = 'modal-backdrop';
|
||||||
|
modal.innerHTML = '<div class="modal-container max-w-md">' +
|
||||||
|
'<div class="modal-header">' +
|
||||||
|
'<h2 class="text-lg font-bold">' + (t('codexlens.indexHistory') || 'Index History') + '</h2>' +
|
||||||
|
'<button onclick="document.getElementById(\'indexHistoryModal\').remove()" class="text-muted-foreground hover:text-foreground">' +
|
||||||
|
'<i data-lucide="x" class="w-5 h-5"></i>' +
|
||||||
|
'</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="modal-body max-h-96 overflow-y-auto">' + historyHtml + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
} catch (err) {
|
||||||
|
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.showIndexHistory = showIndexHistory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update pending queue UI elements
|
||||||
|
*/
|
||||||
|
function updatePendingQueueUI(queue) {
|
||||||
|
var countEl = document.getElementById('pendingFileCount');
|
||||||
|
var timerEl = document.getElementById('countdownTimer');
|
||||||
|
var listEl = document.getElementById('pendingFilesList');
|
||||||
|
var flushBtn = document.getElementById('flushNowBtn');
|
||||||
|
|
||||||
|
if (countEl) countEl.textContent = queue.file_count || 0;
|
||||||
|
|
||||||
|
if (queue.countdown_seconds > 0) {
|
||||||
|
currentCountdownSeconds = queue.countdown_seconds;
|
||||||
|
if (timerEl) timerEl.textContent = formatCountdown(queue.countdown_seconds);
|
||||||
|
startCountdownTimer(queue.countdown_seconds);
|
||||||
|
} else {
|
||||||
|
if (timerEl) timerEl.textContent = '--:--';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flushBtn) flushBtn.disabled = (queue.file_count || 0) === 0;
|
||||||
|
|
||||||
|
if (listEl && queue.files) {
|
||||||
|
listEl.innerHTML = queue.files.map(function(f) {
|
||||||
|
return '<div class="flex items-center gap-2 text-muted-foreground">' +
|
||||||
|
'<i data-lucide="file" class="w-3 h-3"></i>' +
|
||||||
|
'<span class="truncate">' + f + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update last index result UI
|
||||||
|
*/
|
||||||
|
function updateLastIndexResult(result) {
|
||||||
|
var statsEl = document.getElementById('lastIndexStats');
|
||||||
|
var sectionEl = document.getElementById('watcherLastIndex');
|
||||||
|
|
||||||
|
if (sectionEl) sectionEl.style.display = 'block';
|
||||||
|
if (statsEl) {
|
||||||
|
statsEl.innerHTML = '<div class="p-2 bg-success/10 rounded">' +
|
||||||
|
'<div class="text-lg font-bold text-success">' + (result.files_indexed || 0) + '</div>' +
|
||||||
|
'<div class="text-xs text-muted-foreground">Indexed</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="p-2 bg-warning/10 rounded">' +
|
||||||
|
'<div class="text-lg font-bold text-warning">' + (result.files_removed || 0) + '</div>' +
|
||||||
|
'<div class="text-xs text-muted-foreground">Removed</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="p-2 bg-primary/10 rounded">' +
|
||||||
|
'<div class="text-lg font-bold text-primary">' + (result.symbols_added || 0) + '</div>' +
|
||||||
|
'<div class="text-xs text-muted-foreground">+Symbols</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="p-2 bg-destructive/10 rounded">' +
|
||||||
|
'<div class="text-lg font-bold text-destructive">' + ((result.errors && result.errors.length) || 0) + '</div>' +
|
||||||
|
'<div class="text-xs text-muted-foreground">Errors</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear pending queue after indexing
|
||||||
|
updatePendingQueueUI({ file_count: 0, files: [], countdown_seconds: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -5999,12 +6284,37 @@ function closeWatcherModal() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle watcher status update from WebSocket
|
* Handle watcher status update from WebSocket
|
||||||
* @param {Object} payload - { running: boolean, path?: string, error?: string }
|
* @param {Object} payload - { running: boolean, path?: string, error?: string, events_processed?: number, uptime_seconds?: number }
|
||||||
*/
|
*/
|
||||||
function handleWatcherStatusUpdate(payload) {
|
function handleWatcherStatusUpdate(payload) {
|
||||||
var toggle = document.getElementById('watcherToggle');
|
var toggle = document.getElementById('watcherToggle');
|
||||||
var statsDiv = document.getElementById('watcherStats');
|
var statsDiv = document.getElementById('watcherStats');
|
||||||
var configDiv = document.getElementById('watcherStartConfig');
|
var configDiv = document.getElementById('watcherStartConfig');
|
||||||
|
var eventsCountEl = document.getElementById('watcherEventsCount');
|
||||||
|
var uptimeEl = document.getElementById('watcherUptime');
|
||||||
|
|
||||||
|
// Update events count if provided (real-time updates)
|
||||||
|
if (payload.events_processed !== undefined && eventsCountEl) {
|
||||||
|
eventsCountEl.textContent = payload.events_processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uptime if provided
|
||||||
|
if (payload.uptime_seconds !== undefined && uptimeEl) {
|
||||||
|
var seconds = payload.uptime_seconds;
|
||||||
|
var formatted = seconds < 60 ? seconds + 's' :
|
||||||
|
seconds < 3600 ? Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's' :
|
||||||
|
Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
|
||||||
|
uptimeEl.textContent = formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also update main page watcher status badge if it exists
|
||||||
|
var statusBadge = document.getElementById('watcherStatusBadge');
|
||||||
|
if (statusBadge && payload.running !== undefined) {
|
||||||
|
updateWatcherUI(payload.running, {
|
||||||
|
events_processed: payload.events_processed,
|
||||||
|
uptime_seconds: payload.uptime_seconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (payload.error) {
|
if (payload.error) {
|
||||||
// Watcher failed - update UI to show stopped state
|
// Watcher failed - update UI to show stopped state
|
||||||
@@ -6018,8 +6328,8 @@ function handleWatcherStatusUpdate(payload) {
|
|||||||
if (statsDiv) statsDiv.style.display = 'block';
|
if (statsDiv) statsDiv.style.display = 'block';
|
||||||
if (configDiv) configDiv.style.display = 'none';
|
if (configDiv) configDiv.style.display = 'none';
|
||||||
startWatcherStatusPolling();
|
startWatcherStatusPolling();
|
||||||
} else {
|
} else if (payload.running === false) {
|
||||||
// Watcher stopped normally
|
// Watcher stopped normally (only if running is explicitly false)
|
||||||
if (toggle) toggle.checked = false;
|
if (toggle) toggle.checked = false;
|
||||||
if (statsDiv) statsDiv.style.display = 'none';
|
if (statsDiv) statsDiv.style.display = 'none';
|
||||||
if (configDiv) configDiv.style.display = 'block';
|
if (configDiv) configDiv.style.display = 'block';
|
||||||
|
|||||||
@@ -1,6 +1,56 @@
|
|||||||
// Core Memory View
|
// Core Memory View
|
||||||
// Manages strategic context entries with knowledge graph and evolution tracking
|
// Manages strategic context entries with knowledge graph and evolution tracking
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse JSON streaming content and extract readable text
|
||||||
|
* Handles Gemini/Qwen format: {"type":"message","content":"...","delta":true}
|
||||||
|
*/
|
||||||
|
function parseJsonStreamContent(content) {
|
||||||
|
if (!content || typeof content !== 'string') return content;
|
||||||
|
|
||||||
|
// Check if content looks like JSON streaming (multiple JSON objects)
|
||||||
|
if (!content.includes('{"type":')) return content;
|
||||||
|
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const extractedParts = [];
|
||||||
|
let hasJsonLines = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
|
||||||
|
// Try to parse as JSON
|
||||||
|
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(trimmed);
|
||||||
|
// Extract content from message type
|
||||||
|
if (obj.type === 'message' && obj.content) {
|
||||||
|
extractedParts.push(obj.content);
|
||||||
|
hasJsonLines = true;
|
||||||
|
}
|
||||||
|
// Skip init/result/error types (metadata)
|
||||||
|
else if (obj.type === 'init' || obj.type === 'result' || obj.type === 'error') {
|
||||||
|
hasJsonLines = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not valid JSON, keep as plain text
|
||||||
|
extractedParts.push(trimmed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Plain text line
|
||||||
|
extractedParts.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found JSON lines, return extracted content
|
||||||
|
if (hasJsonLines && extractedParts.length > 0) {
|
||||||
|
return extractedParts.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
// Notification function
|
// Notification function
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
// Create notification container if it doesn't exist
|
// Create notification container if it doesn't exist
|
||||||
@@ -527,20 +577,24 @@ async function viewMemoryDetail(memoryId) {
|
|||||||
const modal = document.getElementById('memoryDetailModal');
|
const modal = document.getElementById('memoryDetailModal');
|
||||||
document.getElementById('memoryDetailTitle').textContent = memory.id;
|
document.getElementById('memoryDetailTitle').textContent = memory.id;
|
||||||
|
|
||||||
|
// Parse content and summary in case they contain JSON streaming format
|
||||||
|
const parsedContent = parseJsonStreamContent(memory.content);
|
||||||
|
const parsedSummary = parseJsonStreamContent(memory.summary);
|
||||||
|
|
||||||
const body = document.getElementById('memoryDetailBody');
|
const body = document.getElementById('memoryDetailBody');
|
||||||
body.innerHTML = `
|
body.innerHTML = `
|
||||||
<div class="memory-detail-content">
|
<div class="memory-detail-content">
|
||||||
${memory.summary
|
${parsedSummary
|
||||||
? `<div class="detail-section">
|
? `<div class="detail-section">
|
||||||
<h3>${t('coreMemory.summary')}</h3>
|
<h3>${t('coreMemory.summary')}</h3>
|
||||||
<div class="detail-text">${escapeHtml(memory.summary)}</div>
|
<div class="detail-text">${escapeHtml(parsedSummary)}</div>
|
||||||
</div>`
|
</div>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h3>${t('coreMemory.content')}</h3>
|
<h3>${t('coreMemory.content')}</h3>
|
||||||
<pre class="detail-code">${escapeHtml(memory.content)}</pre>
|
<pre class="detail-code">${escapeHtml(parsedContent)}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${(() => {
|
${(() => {
|
||||||
@@ -564,7 +618,7 @@ async function viewMemoryDetail(memoryId) {
|
|||||||
${memory.raw_output
|
${memory.raw_output
|
||||||
? `<div class="detail-section">
|
? `<div class="detail-section">
|
||||||
<h3>${t('coreMemory.rawOutput')}</h3>
|
<h3>${t('coreMemory.rawOutput')}</h3>
|
||||||
<pre class="detail-code">${escapeHtml(memory.raw_output)}</pre>
|
<pre class="detail-code">${escapeHtml(parseJsonStreamContent(memory.raw_output))}</pre>
|
||||||
</div>`
|
</div>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,7 +347,8 @@ var ruleCreateState = {
|
|||||||
generationType: 'description',
|
generationType: 'description',
|
||||||
description: '',
|
description: '',
|
||||||
extractScope: '',
|
extractScope: '',
|
||||||
extractFocus: ''
|
extractFocus: '',
|
||||||
|
enableReview: false
|
||||||
};
|
};
|
||||||
|
|
||||||
function openRuleCreateModal() {
|
function openRuleCreateModal() {
|
||||||
@@ -363,7 +364,8 @@ function openRuleCreateModal() {
|
|||||||
generationType: 'description',
|
generationType: 'description',
|
||||||
description: '',
|
description: '',
|
||||||
extractScope: '',
|
extractScope: '',
|
||||||
extractFocus: ''
|
extractFocus: '',
|
||||||
|
enableReview: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create modal HTML
|
// Create modal HTML
|
||||||
@@ -506,6 +508,18 @@ function openRuleCreateModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Review Option (CLI mode only) -->
|
||||||
|
<div id="ruleReviewSection" style="display: ${ruleCreateState.mode === 'cli-generate' ? 'block' : 'none'}">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" id="ruleEnableReview"
|
||||||
|
class="w-4 h-4 text-primary bg-background border-border rounded focus:ring-2 focus:ring-primary"
|
||||||
|
${ruleCreateState.enableReview ? 'checked' : ''}
|
||||||
|
onchange="toggleRuleReview()">
|
||||||
|
<span class="text-sm font-medium text-foreground">${t('rules.enableReview')}</span>
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-muted-foreground mt-1 ml-6">${t('rules.enableReviewHint')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Conditional Rule Toggle (Manual mode only) -->
|
<!-- Conditional Rule Toggle (Manual mode only) -->
|
||||||
<div id="ruleConditionalSection" style="display: ${ruleCreateState.mode === 'input' ? 'block' : 'none'}">
|
<div id="ruleConditionalSection" style="display: ${ruleCreateState.mode === 'input' ? 'block' : 'none'}">
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
@@ -667,11 +681,13 @@ function switchRuleCreateMode(mode) {
|
|||||||
const generationTypeSection = document.getElementById('ruleGenerationTypeSection');
|
const generationTypeSection = document.getElementById('ruleGenerationTypeSection');
|
||||||
const descriptionSection = document.getElementById('ruleDescriptionSection');
|
const descriptionSection = document.getElementById('ruleDescriptionSection');
|
||||||
const extractSection = document.getElementById('ruleExtractSection');
|
const extractSection = document.getElementById('ruleExtractSection');
|
||||||
|
const reviewSection = document.getElementById('ruleReviewSection');
|
||||||
const conditionalSection = document.getElementById('ruleConditionalSection');
|
const conditionalSection = document.getElementById('ruleConditionalSection');
|
||||||
const contentSection = document.getElementById('ruleContentSection');
|
const contentSection = document.getElementById('ruleContentSection');
|
||||||
|
|
||||||
if (mode === 'cli-generate') {
|
if (mode === 'cli-generate') {
|
||||||
if (generationTypeSection) generationTypeSection.style.display = 'block';
|
if (generationTypeSection) generationTypeSection.style.display = 'block';
|
||||||
|
if (reviewSection) reviewSection.style.display = 'block';
|
||||||
if (conditionalSection) conditionalSection.style.display = 'none';
|
if (conditionalSection) conditionalSection.style.display = 'none';
|
||||||
if (contentSection) contentSection.style.display = 'none';
|
if (contentSection) contentSection.style.display = 'none';
|
||||||
|
|
||||||
@@ -687,6 +703,7 @@ function switchRuleCreateMode(mode) {
|
|||||||
if (generationTypeSection) generationTypeSection.style.display = 'none';
|
if (generationTypeSection) generationTypeSection.style.display = 'none';
|
||||||
if (descriptionSection) descriptionSection.style.display = 'none';
|
if (descriptionSection) descriptionSection.style.display = 'none';
|
||||||
if (extractSection) extractSection.style.display = 'none';
|
if (extractSection) extractSection.style.display = 'none';
|
||||||
|
if (reviewSection) reviewSection.style.display = 'none';
|
||||||
if (conditionalSection) conditionalSection.style.display = 'block';
|
if (conditionalSection) conditionalSection.style.display = 'block';
|
||||||
if (contentSection) contentSection.style.display = 'block';
|
if (contentSection) contentSection.style.display = 'block';
|
||||||
}
|
}
|
||||||
@@ -724,6 +741,11 @@ function switchRuleGenerationType(type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleRuleReview() {
|
||||||
|
const checkbox = document.getElementById('ruleEnableReview');
|
||||||
|
ruleCreateState.enableReview = checkbox ? checkbox.checked : false;
|
||||||
|
}
|
||||||
|
|
||||||
async function createRule() {
|
async function createRule() {
|
||||||
const fileNameInput = document.getElementById('ruleFileName');
|
const fileNameInput = document.getElementById('ruleFileName');
|
||||||
const subdirectoryInput = document.getElementById('ruleSubdirectory');
|
const subdirectoryInput = document.getElementById('ruleSubdirectory');
|
||||||
@@ -784,7 +806,8 @@ async function createRule() {
|
|||||||
generationType: ruleCreateState.generationType,
|
generationType: ruleCreateState.generationType,
|
||||||
description: ruleCreateState.generationType === 'description' ? description : undefined,
|
description: ruleCreateState.generationType === 'description' ? description : undefined,
|
||||||
extractScope: ruleCreateState.generationType === 'extract' ? extractScope : undefined,
|
extractScope: ruleCreateState.generationType === 'extract' ? extractScope : undefined,
|
||||||
extractFocus: ruleCreateState.generationType === 'extract' ? extractFocus : undefined
|
extractFocus: ruleCreateState.generationType === 'extract' ? extractFocus : undefined,
|
||||||
|
enableReview: ruleCreateState.enableReview || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show progress message
|
// Show progress message
|
||||||
|
|||||||
@@ -510,35 +510,8 @@ function openSkillCreateModal() {
|
|||||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.skillNameHint')}</p>
|
<p class="text-xs text-muted-foreground mt-1">${t('skills.skillNameHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Generation Type Selection -->
|
<!-- Description Text Area -->
|
||||||
<div>
|
<div id="skillDescriptionArea">
|
||||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.generationType')}</label>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button class="flex-1 px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.generationType === 'description' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
|
||||||
onclick="switchSkillGenerationType('description')">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<i data-lucide="file-text" class="w-5 h-5"></i>
|
|
||||||
<div>
|
|
||||||
<div class="font-medium text-sm">${t('skills.fromDescription')}</div>
|
|
||||||
<div class="text-xs text-muted-foreground">${t('skills.fromDescriptionHint')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button class="flex-1 px-4 py-3 text-left border-2 rounded-lg transition-all opacity-50 cursor-not-allowed"
|
|
||||||
disabled>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<i data-lucide="layout-template" class="w-5 h-5"></i>
|
|
||||||
<div>
|
|
||||||
<div class="font-medium text-sm">${t('skills.fromTemplate')}</div>
|
|
||||||
<div class="text-xs text-muted-foreground">${t('skills.comingSoon')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description Text Area (for 'description' type) -->
|
|
||||||
<div id="skillDescriptionArea" style="display: ${skillCreateState.generationType === 'description' ? 'block' : 'none'}">
|
|
||||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.descriptionLabel')} <span class="text-destructive">*</span></label>
|
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.descriptionLabel')} <span class="text-destructive">*</span></label>
|
||||||
<textarea id="skillDescription"
|
<textarea id="skillDescription"
|
||||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
@@ -683,19 +656,6 @@ function switchSkillCreateMode(mode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchSkillGenerationType(type) {
|
|
||||||
skillCreateState.generationType = type;
|
|
||||||
|
|
||||||
// Toggle visibility of description area
|
|
||||||
const descriptionArea = document.getElementById('skillDescriptionArea');
|
|
||||||
if (descriptionArea) {
|
|
||||||
descriptionArea.style.display = type === 'description' ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update generation type button styles (only the description button is active, template is disabled)
|
|
||||||
// No need to update button styles since template button is disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
function browseSkillFolder() {
|
function browseSkillFolder() {
|
||||||
// Use browser prompt for now (Phase 3 will implement file browser)
|
// Use browser prompt for now (Phase 3 will implement file browser)
|
||||||
const path = prompt(t('skills.enterFolderPath'), skillCreateState.sourcePath);
|
const path = prompt(t('skills.enterFolderPath'), skillCreateState.sourcePath);
|
||||||
|
|||||||
@@ -205,6 +205,18 @@ class UnifiedStreamParser {
|
|||||||
this.extractedText += item.text;
|
this.extractedText += item.text;
|
||||||
output += `[响应] ${item.text}\n`; // Add newline for proper line separation
|
output += `[响应] ${item.text}\n`; // Add newline for proper line separation
|
||||||
}
|
}
|
||||||
|
// Extract content from write_file tool calls (for rules generation)
|
||||||
|
// Use type assertion to access tool_use properties
|
||||||
|
const anyItem = item as { type: string; name?: string; input?: { content?: string } };
|
||||||
|
if (anyItem.type === 'tool_use' && anyItem.input?.content && typeof anyItem.input.content === 'string') {
|
||||||
|
const toolName = anyItem.name || '';
|
||||||
|
// Check if this is a file write operation
|
||||||
|
if (toolName.includes('write_file') || toolName.includes('Write')) {
|
||||||
|
// Use the file content as extracted text (overwrite previous text response)
|
||||||
|
this.extractedText = anyItem.input.content;
|
||||||
|
output += `[工具] ${toolName}: 写入文件内容 (${anyItem.input.content.length} 字符)\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1286,9 +1298,10 @@ async function executeCliTool(
|
|||||||
stdout += text;
|
stdout += text;
|
||||||
|
|
||||||
// Parse stream-json for all supported tools
|
// Parse stream-json for all supported tools
|
||||||
if (streamParser && onOutput) {
|
// Always process chunks to populate extractedText, even without onOutput callback
|
||||||
|
if (streamParser) {
|
||||||
const parsedText = streamParser.processChunk(text);
|
const parsedText = streamParser.processChunk(text);
|
||||||
if (parsedText) {
|
if (parsedText && onOutput) {
|
||||||
onOutput({ type: 'stdout', data: parsedText });
|
onOutput({ type: 'stdout', data: parsedText });
|
||||||
}
|
}
|
||||||
} else if (onOutput) {
|
} else if (onOutput) {
|
||||||
@@ -1311,9 +1324,10 @@ async function executeCliTool(
|
|||||||
currentChildProcess = null;
|
currentChildProcess = null;
|
||||||
|
|
||||||
// Flush unified parser buffer if present
|
// Flush unified parser buffer if present
|
||||||
if (streamParser && onOutput) {
|
// Always flush to capture remaining content, even without onOutput callback
|
||||||
|
if (streamParser) {
|
||||||
const remaining = streamParser.flush();
|
const remaining = streamParser.flush();
|
||||||
if (remaining) {
|
if (remaining && onOutput) {
|
||||||
onOutput({ type: 'stdout', data: remaining });
|
onOutput({ type: 'stdout', data: remaining });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ describe('cli command module', async () => {
|
|||||||
assert.equal(call.prompt, 'Hello');
|
assert.equal(call.prompt, 'Hello');
|
||||||
assert.equal(call.mode, 'analysis');
|
assert.equal(call.mode, 'analysis');
|
||||||
assert.equal(call.stream, false);
|
assert.equal(call.stream, false);
|
||||||
assert.equal(call.timeout, 300000);
|
assert.equal(call.timeout, 0);
|
||||||
}
|
}
|
||||||
assert.deepEqual(exitCodes, [0, 0, 0]);
|
assert.deepEqual(exitCodes, [0, 0, 0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -28,7 +29,7 @@ class FileEvent:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class WatcherConfig:
|
class WatcherConfig:
|
||||||
"""Configuration for file watcher."""
|
"""Configuration for file watcher."""
|
||||||
debounce_ms: int = 1000
|
debounce_ms: int = 60000 # Default 60 seconds for debounce
|
||||||
ignored_patterns: Set[str] = field(default_factory=lambda: {
|
ignored_patterns: Set[str] = field(default_factory=lambda: {
|
||||||
# Version control
|
# Version control
|
||||||
".git", ".svn", ".hg",
|
".git", ".svn", ".hg",
|
||||||
@@ -50,13 +51,26 @@ class WatcherConfig:
|
|||||||
languages: Optional[List[str]] = None # None = all supported
|
languages: Optional[List[str]] = None # None = all supported
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PendingQueueStatus:
|
||||||
|
"""Status of pending file changes queue."""
|
||||||
|
file_count: int = 0
|
||||||
|
files: List[str] = field(default_factory=list) # Limited to 20 files
|
||||||
|
countdown_seconds: int = 0
|
||||||
|
last_event_time: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class IndexResult:
|
class IndexResult:
|
||||||
"""Result of processing file changes."""
|
"""Result of processing file changes."""
|
||||||
files_indexed: int = 0
|
files_indexed: int = 0
|
||||||
files_removed: int = 0
|
files_removed: int = 0
|
||||||
symbols_added: int = 0
|
symbols_added: int = 0
|
||||||
|
symbols_removed: int = 0
|
||||||
|
files_success: List[str] = field(default_factory=list)
|
||||||
|
files_failed: List[str] = field(default_factory=list)
|
||||||
errors: List[str] = field(default_factory=list)
|
errors: List[str] = field(default_factory=list)
|
||||||
|
timestamp: float = field(default_factory=time.time)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ from typing import Callable, Dict, List, Optional
|
|||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
from .events import ChangeType, FileEvent, WatcherConfig
|
from .events import ChangeType, FileEvent, WatcherConfig, PendingQueueStatus
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Maximum queue size to prevent unbounded memory growth
|
||||||
|
# When exceeded, forces immediate flush to avoid memory exhaustion
|
||||||
|
MAX_QUEUE_SIZE = 50000
|
||||||
|
|
||||||
|
|
||||||
class _CodexLensHandler(FileSystemEventHandler):
|
class _CodexLensHandler(FileSystemEventHandler):
|
||||||
"""Internal handler for watchdog events."""
|
"""Internal handler for watchdog events."""
|
||||||
@@ -112,8 +116,12 @@ class FileWatcher:
|
|||||||
self._event_queue: List[FileEvent] = []
|
self._event_queue: List[FileEvent] = []
|
||||||
self._queue_lock = threading.Lock()
|
self._queue_lock = threading.Lock()
|
||||||
|
|
||||||
# Debounce thread
|
# Debounce timer (true debounce - waits after last event)
|
||||||
self._debounce_thread: Optional[threading.Thread] = None
|
self._flush_timer: Optional[threading.Timer] = None
|
||||||
|
self._last_event_time: float = 0
|
||||||
|
|
||||||
|
# Queue change callbacks for real-time UI updates
|
||||||
|
self._queue_change_callbacks: List[Callable[[PendingQueueStatus], None]] = []
|
||||||
|
|
||||||
# Config instance for language checking
|
# Config instance for language checking
|
||||||
self._codexlens_config = Config()
|
self._codexlens_config = Config()
|
||||||
@@ -138,16 +146,57 @@ class FileWatcher:
|
|||||||
return language is not None
|
return language is not None
|
||||||
|
|
||||||
def _on_raw_event(self, event: FileEvent) -> None:
|
def _on_raw_event(self, event: FileEvent) -> None:
|
||||||
"""Handle raw event from watchdog handler."""
|
"""Handle raw event from watchdog handler with true debounce."""
|
||||||
|
force_flush = False
|
||||||
|
|
||||||
with self._queue_lock:
|
with self._queue_lock:
|
||||||
|
# Check queue size limit to prevent memory exhaustion
|
||||||
|
if len(self._event_queue) >= MAX_QUEUE_SIZE:
|
||||||
|
logger.warning(
|
||||||
|
"Event queue limit (%d) reached, forcing immediate flush",
|
||||||
|
MAX_QUEUE_SIZE
|
||||||
|
)
|
||||||
|
if self._flush_timer:
|
||||||
|
self._flush_timer.cancel()
|
||||||
|
self._flush_timer = None
|
||||||
|
force_flush = True
|
||||||
|
|
||||||
self._event_queue.append(event)
|
self._event_queue.append(event)
|
||||||
# Debouncing is handled by background thread
|
self._last_event_time = time.time()
|
||||||
|
|
||||||
|
# Cancel previous timer and schedule new one (true debounce)
|
||||||
|
# Skip if we're about to force flush
|
||||||
|
if not force_flush:
|
||||||
|
if self._flush_timer:
|
||||||
|
self._flush_timer.cancel()
|
||||||
|
|
||||||
|
self._flush_timer = threading.Timer(
|
||||||
|
self.config.debounce_ms / 1000.0,
|
||||||
|
self._flush_events
|
||||||
|
)
|
||||||
|
self._flush_timer.daemon = True
|
||||||
|
self._flush_timer.start()
|
||||||
|
|
||||||
|
# Force flush outside lock to avoid deadlock
|
||||||
|
if force_flush:
|
||||||
|
self._flush_events()
|
||||||
|
|
||||||
|
# Notify queue change (outside lock to avoid deadlock)
|
||||||
|
self._notify_queue_change()
|
||||||
|
|
||||||
def _debounce_loop(self) -> None:
|
def _debounce_loop(self) -> None:
|
||||||
"""Background thread for debounced event batching."""
|
"""Background thread for checking flush signal file."""
|
||||||
|
signal_file = self.root_path / '.codexlens' / 'flush.signal'
|
||||||
while self._running:
|
while self._running:
|
||||||
time.sleep(self.config.debounce_ms / 1000.0)
|
time.sleep(1.0) # Check every second
|
||||||
self._flush_events()
|
# Check for flush signal file
|
||||||
|
if signal_file.exists():
|
||||||
|
try:
|
||||||
|
signal_file.unlink()
|
||||||
|
logger.info("Flush signal detected, triggering immediate index")
|
||||||
|
self.flush_now()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to handle flush signal: %s", e)
|
||||||
|
|
||||||
def _flush_events(self) -> None:
|
def _flush_events(self) -> None:
|
||||||
"""Flush queued events with deduplication."""
|
"""Flush queued events with deduplication."""
|
||||||
@@ -162,6 +211,10 @@ class FileWatcher:
|
|||||||
|
|
||||||
events = list(deduped.values())
|
events = list(deduped.values())
|
||||||
self._event_queue.clear()
|
self._event_queue.clear()
|
||||||
|
self._last_event_time = 0 # Reset after flush
|
||||||
|
|
||||||
|
# Notify queue cleared
|
||||||
|
self._notify_queue_change()
|
||||||
|
|
||||||
if events:
|
if events:
|
||||||
try:
|
try:
|
||||||
@@ -169,6 +222,50 @@ class FileWatcher:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Error in on_changes callback: %s", exc)
|
logger.error("Error in on_changes callback: %s", exc)
|
||||||
|
|
||||||
|
def flush_now(self) -> None:
|
||||||
|
"""Immediately flush pending queue (manual trigger)."""
|
||||||
|
with self._queue_lock:
|
||||||
|
if self._flush_timer:
|
||||||
|
self._flush_timer.cancel()
|
||||||
|
self._flush_timer = None
|
||||||
|
self._flush_events()
|
||||||
|
|
||||||
|
def get_pending_queue_status(self) -> PendingQueueStatus:
|
||||||
|
"""Get current pending queue status for UI display."""
|
||||||
|
with self._queue_lock:
|
||||||
|
file_count = len(self._event_queue)
|
||||||
|
files = [str(e.path.name) for e in self._event_queue[:20]]
|
||||||
|
|
||||||
|
# Calculate countdown
|
||||||
|
if self._last_event_time > 0 and file_count > 0:
|
||||||
|
elapsed = time.time() - self._last_event_time
|
||||||
|
remaining = max(0, self.config.debounce_ms / 1000.0 - elapsed)
|
||||||
|
countdown = int(remaining)
|
||||||
|
else:
|
||||||
|
countdown = 0
|
||||||
|
|
||||||
|
return PendingQueueStatus(
|
||||||
|
file_count=file_count,
|
||||||
|
files=files,
|
||||||
|
countdown_seconds=countdown,
|
||||||
|
last_event_time=self._last_event_time if file_count > 0 else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def register_queue_change_callback(
|
||||||
|
self, callback: Callable[[PendingQueueStatus], None]
|
||||||
|
) -> None:
|
||||||
|
"""Register callback for queue change notifications."""
|
||||||
|
self._queue_change_callbacks.append(callback)
|
||||||
|
|
||||||
|
def _notify_queue_change(self) -> None:
|
||||||
|
"""Notify all registered callbacks of queue change."""
|
||||||
|
status = self.get_pending_queue_status()
|
||||||
|
for callback in self._queue_change_callbacks:
|
||||||
|
try:
|
||||||
|
callback(status)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Queue change callback error: %s", e)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Start watching the directory.
|
"""Start watching the directory.
|
||||||
|
|
||||||
@@ -190,13 +287,13 @@ class FileWatcher:
|
|||||||
self._stop_event.clear()
|
self._stop_event.clear()
|
||||||
self._observer.start()
|
self._observer.start()
|
||||||
|
|
||||||
# Start debounce thread
|
# Start signal check thread (for flush.signal file)
|
||||||
self._debounce_thread = threading.Thread(
|
self._signal_check_thread = threading.Thread(
|
||||||
target=self._debounce_loop,
|
target=self._debounce_loop,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name="FileWatcher-Debounce",
|
name="FileWatcher-SignalCheck",
|
||||||
)
|
)
|
||||||
self._debounce_thread.start()
|
self._signal_check_thread.start()
|
||||||
|
|
||||||
logger.info("Started watching: %s", self.root_path)
|
logger.info("Started watching: %s", self.root_path)
|
||||||
|
|
||||||
@@ -212,15 +309,20 @@ class FileWatcher:
|
|||||||
self._running = False
|
self._running = False
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
|
# Cancel pending flush timer
|
||||||
|
if self._flush_timer:
|
||||||
|
self._flush_timer.cancel()
|
||||||
|
self._flush_timer = None
|
||||||
|
|
||||||
if self._observer:
|
if self._observer:
|
||||||
self._observer.stop()
|
self._observer.stop()
|
||||||
self._observer.join(timeout=5.0)
|
self._observer.join(timeout=5.0)
|
||||||
self._observer = None
|
self._observer = None
|
||||||
|
|
||||||
# Wait for debounce thread to finish
|
# Wait for signal check thread to finish
|
||||||
if self._debounce_thread and self._debounce_thread.is_alive():
|
if hasattr(self, '_signal_check_thread') and self._signal_check_thread and self._signal_check_thread.is_alive():
|
||||||
self._debounce_thread.join(timeout=2.0)
|
self._signal_check_thread.join(timeout=2.0)
|
||||||
self._debounce_thread = None
|
self._signal_check_thread = None
|
||||||
|
|
||||||
# Flush any remaining events
|
# Flush any remaining events
|
||||||
self._flush_events()
|
self._flush_events()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
import threading
|
import threading
|
||||||
@@ -13,7 +14,7 @@ from codexlens.config import Config
|
|||||||
from codexlens.storage.path_mapper import PathMapper
|
from codexlens.storage.path_mapper import PathMapper
|
||||||
from codexlens.storage.registry import RegistryStore
|
from codexlens.storage.registry import RegistryStore
|
||||||
|
|
||||||
from .events import FileEvent, IndexResult, WatcherConfig, WatcherStats
|
from .events import FileEvent, IndexResult, PendingQueueStatus, WatcherConfig, WatcherStats
|
||||||
from .file_watcher import FileWatcher
|
from .file_watcher import FileWatcher
|
||||||
from .incremental_indexer import IncrementalIndexer
|
from .incremental_indexer import IncrementalIndexer
|
||||||
|
|
||||||
@@ -36,11 +37,13 @@ class WatcherManager:
|
|||||||
config: Optional[Config] = None,
|
config: Optional[Config] = None,
|
||||||
watcher_config: Optional[WatcherConfig] = None,
|
watcher_config: Optional[WatcherConfig] = None,
|
||||||
on_indexed: Optional[Callable[[IndexResult], None]] = None,
|
on_indexed: Optional[Callable[[IndexResult], None]] = None,
|
||||||
|
on_queue_change: Optional[Callable[[PendingQueueStatus], None]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.root_path = Path(root_path).resolve()
|
self.root_path = Path(root_path).resolve()
|
||||||
self.config = config or Config()
|
self.config = config or Config()
|
||||||
self.watcher_config = watcher_config or WatcherConfig()
|
self.watcher_config = watcher_config or WatcherConfig()
|
||||||
self.on_indexed = on_indexed
|
self.on_indexed = on_indexed
|
||||||
|
self.on_queue_change = on_queue_change
|
||||||
|
|
||||||
self._registry: Optional[RegistryStore] = None
|
self._registry: Optional[RegistryStore] = None
|
||||||
self._mapper: Optional[PathMapper] = None
|
self._mapper: Optional[PathMapper] = None
|
||||||
@@ -56,6 +59,10 @@ class WatcherManager:
|
|||||||
self._original_sigint = None
|
self._original_sigint = None
|
||||||
self._original_sigterm = None
|
self._original_sigterm = None
|
||||||
|
|
||||||
|
# Index history for tracking recent results
|
||||||
|
self._index_history: List[IndexResult] = []
|
||||||
|
self._max_history_size = 10
|
||||||
|
|
||||||
def _handle_changes(self, events: List[FileEvent]) -> None:
|
def _handle_changes(self, events: List[FileEvent]) -> None:
|
||||||
"""Handle file change events from watcher."""
|
"""Handle file change events from watcher."""
|
||||||
if not self._indexer or not events:
|
if not self._indexer or not events:
|
||||||
@@ -68,12 +75,30 @@ class WatcherManager:
|
|||||||
self._stats.events_processed += len(events)
|
self._stats.events_processed += len(events)
|
||||||
self._stats.last_event_time = time.time()
|
self._stats.last_event_time = time.time()
|
||||||
|
|
||||||
|
# Save to history
|
||||||
|
self._index_history.append(result)
|
||||||
|
if len(self._index_history) > self._max_history_size:
|
||||||
|
self._index_history.pop(0)
|
||||||
|
|
||||||
if result.files_indexed > 0 or result.files_removed > 0:
|
if result.files_indexed > 0 or result.files_removed > 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Indexed %d files, removed %d files, %d errors",
|
"Indexed %d files, removed %d files, %d errors",
|
||||||
result.files_indexed, result.files_removed, len(result.errors)
|
result.files_indexed, result.files_removed, len(result.errors)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Output JSON for TypeScript backend parsing
|
||||||
|
result_data = {
|
||||||
|
"files_indexed": result.files_indexed,
|
||||||
|
"files_removed": result.files_removed,
|
||||||
|
"symbols_added": result.symbols_added,
|
||||||
|
"symbols_removed": result.symbols_removed,
|
||||||
|
"files_success": result.files_success[:20], # Limit output
|
||||||
|
"files_failed": result.files_failed[:20],
|
||||||
|
"errors": result.errors[:10],
|
||||||
|
"timestamp": result.timestamp
|
||||||
|
}
|
||||||
|
print(f"[INDEX_RESULT] {json.dumps(result_data)}", flush=True)
|
||||||
|
|
||||||
if self.on_indexed:
|
if self.on_indexed:
|
||||||
try:
|
try:
|
||||||
self.on_indexed(result)
|
self.on_indexed(result)
|
||||||
@@ -129,6 +154,10 @@ class WatcherManager:
|
|||||||
self.root_path, self.watcher_config, self._handle_changes
|
self.root_path, self.watcher_config, self._handle_changes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Register queue change callback for real-time UI updates
|
||||||
|
if self.on_queue_change:
|
||||||
|
self._watcher.register_queue_change_callback(self._on_queue_change_wrapper)
|
||||||
|
|
||||||
# Install signal handlers
|
# Install signal handlers
|
||||||
self._install_signal_handlers()
|
self._install_signal_handlers()
|
||||||
|
|
||||||
@@ -192,3 +221,35 @@ class WatcherManager:
|
|||||||
last_event_time=self._stats.last_event_time,
|
last_event_time=self._stats.last_event_time,
|
||||||
is_running=self._running,
|
is_running=self._running,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _on_queue_change_wrapper(self, status: PendingQueueStatus) -> None:
|
||||||
|
"""Wrapper for queue change callback with JSON output."""
|
||||||
|
# Output JSON for TypeScript backend parsing
|
||||||
|
status_data = {
|
||||||
|
"file_count": status.file_count,
|
||||||
|
"files": status.files,
|
||||||
|
"countdown_seconds": status.countdown_seconds,
|
||||||
|
"last_event_time": status.last_event_time
|
||||||
|
}
|
||||||
|
print(f"[QUEUE_STATUS] {json.dumps(status_data)}", flush=True)
|
||||||
|
|
||||||
|
if self.on_queue_change:
|
||||||
|
try:
|
||||||
|
self.on_queue_change(status)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Error in on_queue_change callback: %s", exc)
|
||||||
|
|
||||||
|
def flush_now(self) -> None:
|
||||||
|
"""Immediately flush pending queue (manual trigger)."""
|
||||||
|
if self._watcher:
|
||||||
|
self._watcher.flush_now()
|
||||||
|
|
||||||
|
def get_pending_queue_status(self) -> Optional[PendingQueueStatus]:
|
||||||
|
"""Get current pending queue status."""
|
||||||
|
if self._watcher:
|
||||||
|
return self._watcher.get_pending_queue_status()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_index_history(self, limit: int = 5) -> List[IndexResult]:
|
||||||
|
"""Get recent index history."""
|
||||||
|
return self._index_history[-limit:]
|
||||||
|
|||||||
Reference in New Issue
Block a user