From 05514631f2f763d6e587b19b0af303cdac3eac67 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Wed, 7 Jan 2026 21:51:26 +0800 Subject: [PATCH] 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. --- .claude/rules/coding/ts-naming.md | 88 ++++ ccw/src/core/core-memory-store.ts | 3 +- ccw/src/core/routes/claude-routes.ts | 13 +- ccw/src/core/routes/codexlens-routes.ts | 157 ++++++- ccw/src/core/routes/memory-routes.ts | 48 ++- ccw/src/core/routes/rules-routes.ts | 395 ++++++++++++++++-- ccw/src/core/routes/skills-routes.ts | 6 +- .../dashboard-js/components/notifications.js | 59 +++ ccw/src/templates/dashboard-js/i18n.js | 4 + .../dashboard-js/views/claude-manager.js | 8 +- .../dashboard-js/views/codexlens-manager.js | 350 +++++++++++++++- .../dashboard-js/views/core-memory.js | 62 ++- .../dashboard-js/views/rules-manager.js | 29 +- .../dashboard-js/views/skills-manager.js | 44 +- ccw/src/tools/cli-executor.ts | 22 +- ccw/tests/cli-command.test.ts | 2 +- codex-lens/src/codexlens/watcher/events.py | 16 +- .../src/codexlens/watcher/file_watcher.py | 134 +++++- codex-lens/src/codexlens/watcher/manager.py | 79 +++- 19 files changed, 1346 insertions(+), 173 deletions(-) create mode 100644 .claude/rules/coding/ts-naming.md diff --git a/.claude/rules/coding/ts-naming.md b/.claude/rules/coding/ts-naming.md new file mode 100644 index 00000000..6d1e7e43 --- /dev/null +++ b/.claude/rules/coding/ts-naming.md @@ -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 \ No newline at end of file diff --git a/ccw/src/core/core-memory-store.ts b/ccw/src/core/core-memory-store.ts index 68c4976e..461d6fab 100644 --- a/ccw/src/core/core-memory-store.ts +++ b/ccw/src/core/core-memory-store.ts @@ -453,7 +453,8 @@ ${memory.content} 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 const stmt = this.db.prepare(` diff --git a/ccw/src/core/routes/claude-routes.ts b/ccw/src/core/routes/claude-routes.ts index dfc3a51b..8a3baf77 100644 --- a/ccw/src/core/routes/claude-routes.ts +++ b/ccw/src/core/routes/claude-routes.ts @@ -613,7 +613,7 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { id: syncId }); - if (!result.success || !result.execution?.output) { + if (!result.success) { return { error: 'CLI execution failed', details: result.execution?.error || 'No output received', @@ -621,10 +621,13 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { }; } - // Extract CLI output - const cliOutput = typeof result.execution.output === 'string' - ? result.execution.output - : result.execution.output.stdout || ''; + // Extract CLI output - prefer parsedOutput (extracted text from stream JSON) + let cliOutput = result.parsedOutput || ''; + if (!cliOutput && result.execution?.output) { + cliOutput = typeof result.execution.output === 'string' + ? result.execution.output + : result.execution.output.stdout || ''; + } if (!cliOutput || cliOutput.trim().length === 0) { return { error: 'CLI returned empty output', status: 500 }; diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index 062ab583..e7d19d9e 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -39,11 +39,32 @@ interface WatcherConfig { 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 { running: boolean; root_path: string; events_processed: number; start_time: Date | null; + pending_queue: PendingQueueStatus | null; + last_index_result: IndexResultDetail | null; + index_history: IndexResultDetail[]; } interface ActiveWatcher { @@ -58,13 +79,12 @@ const WATCHER_CONFIG_FILE = path.join(WATCHER_CONFIG_DIR, 'watchers.json'); // Active watchers Map: normalized_path -> { process, stats } const activeWatchers = new Map(); -/** - * Normalize path for consistent key usage - * - Convert to absolute path - // Flag to ensure watchers are initialized only once let watchersInitialized = false; +/** + * Normalize path for consistent key usage + * - Convert to absolute path * - Convert to lowercase on Windows * - Use forward slashes */ @@ -183,7 +203,10 @@ async function startWatcherProcess( running: true, root_path: targetPath, events_processed: 0, - start_time: new Date() + start_time: new Date(), + pending_queue: null, + last_index_result: null, + index_history: [] }; // 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) { childProcess.stdout.on('data', (data: Buffer) => { const output = data.toString(); - const matches = output.match(/Processed \d+ events?/g); - if (matches) { - const watcher = activeWatchers.get(normalizedPath); - if (watcher) { - watcher.stats.events_processed += matches.length; + const watcher = activeWatchers.get(normalizedPath); + if (!watcher) return; + + // Process output line by line for reliable JSON parsing + // (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 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; + } + // ============================================================ diff --git a/ccw/src/core/routes/memory-routes.ts b/ccw/src/core/routes/memory-routes.ts index c6d81aca..c221f6ac 100644 --- a/ccw/src/core/routes/memory-routes.ts +++ b/ccw/src/core/routes/memory-routes.ts @@ -392,10 +392,11 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p 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: [] }; - if (result.stdout) { - let outputText = result.stdout; + const cliOutput = result.parsedOutput || result.stdout || ''; + if (cliOutput) { + let outputText = cliOutput; // Strip markdown code blocks if present 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); // Return raw output if JSON parse fails 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: [] }; } } else { // No JSON found, wrap raw output 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: [] }; } @@ -439,7 +440,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p promptCount: prompts.length, patterns: insights.patterns, suggestions: insights.suggestions, - rawOutput: result.stdout || '', + rawOutput: cliOutput, executionId: result.execution?.id, lang }); @@ -981,23 +982,26 @@ RULES: Be concise. Focus on practical understanding. Include function signatures id: syncId }); - if (result.success && result.execution?.output) { - // Extract stdout from output object with proper serialization - const output = result.execution.output; - if (typeof output === 'string') { - cliOutput = output; - } else if (output && typeof output === 'object') { - // Handle object output - extract stdout or serialize the object - if (output.stdout && typeof output.stdout === 'string') { - cliOutput = output.stdout; - } 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); + if (result.success) { + // Prefer parsedOutput (extracted text from stream JSON) over raw execution output + if (result.parsedOutput) { + cliOutput = result.parsedOutput; + } else if (result.execution?.output) { + // Fallback to execution.output + const output = result.execution.output; + if (typeof output === 'string') { + cliOutput = output; + } else if (output && typeof output === 'object') { + // Handle object output - extract stdout or serialize the object + if (output.stdout && typeof output.stdout === 'string') { + cliOutput = output.stdout; + } 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 = ''; } } diff --git a/ccw/src/core/routes/rules-routes.ts b/ccw/src/core/routes/rules-routes.ts index 5f48c882..a1ae928b 100644 --- a/ccw/src/core/routes/rules-routes.ts +++ b/ccw/src/core/routes/rules-routes.ts @@ -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; + 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; +}) { + 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 +) { + 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 * @param {Object} params @@ -233,6 +537,7 @@ function deleteRule(ruleName, location, projectPath) { * @param {string} params.location - 'project' or 'user' * @param {string} params.subdirectory - Optional subdirectory * @param {string} params.projectPath - Project root path + * @param {boolean} params.enableReview - Optional: enable secondary review * @returns {Object} */ async function generateRuleViaCLI(params) { @@ -246,47 +551,47 @@ async function generateRuleViaCLI(params) { fileName, location, subdirectory, - projectPath + projectPath, + enableReview } = params; let prompt = ''; let mode = 'analysis'; let workingDir = projectPath; + // Infer context from file name and subdirectory + const context = inferRuleContext(fileName, subdirectory || '', location); + // Build prompt based on generation type if (generationType === 'description') { mode = 'write'; - prompt = `PURPOSE: Generate Claude Code memory rule from description to guide Claude's behavior -TASK: • Analyze rule requirements • Generate markdown content with clear instructions -MODE: write -EXPECTED: Complete rule content in markdown format -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 - -RULE DESCRIPTION: -${description} - -FILE NAME: ${fileName}`; + prompt = buildStructuredRulePrompt({ + description, + fileName, + subdirectory: subdirectory || '', + location, + context, + enableReview + }); } else if (generationType === 'template') { mode = 'write'; prompt = `PURPOSE: Generate Claude Code rule from template type TASK: • Create rule based on ${templateType} template • Generate structured markdown content MODE: write 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} FILE NAME: ${fileName}`; } else if (generationType === 'extract') { mode = 'analysis'; - prompt = `PURPOSE: Extract coding rules from existing codebase to document patterns and conventions -TASK: • Analyze code patterns in specified scope • Extract common conventions • Identify best practices -MODE: analysis -CONTEXT: @${extractScope || '**/*'} -EXPECTED: Rule content based on codebase analysis with examples -RULES: $(cat ~/.claude/workflows/cli-templates/prompts/analysis/02-analyze-code-patterns.txt) | Focus on actual patterns found | Include code examples | analysis=READ-ONLY - -ANALYSIS SCOPE: ${extractScope || '**/*'} -FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structure'}`; + prompt = buildExtractPrompt({ + extractScope: extractScope || '', + extractFocus: extractFocus || '', + fileName, + subdirectory: subdirectory || '', + context + }); } else { return { error: `Unknown generation type: ${generationType}` }; } @@ -308,8 +613,15 @@ FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structu }; } - // Extract generated content from stdout - const generatedContent = result.stdout.trim(); + // Extract generated content - prefer parsedOutput (extracted text from stream JSON) + 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) { 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 const createResult = await createRule({ fileName, @@ -333,7 +679,8 @@ FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structu success: createResult.success || false, ...createResult, generatedContent, - executionId: result.conversation?.id + executionId: result.conversation?.id, + review: reviewResult }; } catch (error) { return { error: (error as Error).message }; diff --git a/ccw/src/core/routes/skills-routes.ts b/ccw/src/core/routes/skills-routes.ts index e6a0fd77..42b7aeff 100644 --- a/ccw/src/core/routes/skills-routes.ts +++ b/ccw/src/core/routes/skills-routes.ts @@ -483,7 +483,7 @@ Create a new Claude Code skill with the following specifications: if (!result.success) { return { error: `CLI generation failed: ${result.stderr || 'Unknown error'}`, - stdout: result.stdout, + stdout: result.parsedOutput || result.stdout, stderr: result.stderr }; } @@ -493,7 +493,7 @@ Create a new Claude Code skill with the following specifications: if (!validation.valid) { return { error: `Generated skill is invalid: ${validation.errors.join(', ')}`, - stdout: result.stdout, + stdout: result.parsedOutput || result.stdout, stderr: result.stderr }; } @@ -503,7 +503,7 @@ Create a new Claude Code skill with the following specifications: skillName: validation.skillInfo.name, location, path: targetPath, - stdout: result.stdout, + stdout: result.parsedOutput || result.stdout, stderr: result.stderr }; } catch (error) { diff --git a/ccw/src/templates/dashboard-js/components/notifications.js b/ccw/src/templates/dashboard-js/components/notifications.js index f5bc0e70..7a460b28 100644 --- a/ccw/src/templates/dashboard-js/components/notifications.js +++ b/ccw/src/templates/dashboard-js/components/notifications.js @@ -472,6 +472,65 @@ function handleNotification(data) { } 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: console.log('[WS] Unknown notification type:', type); } diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 8c8cf949..2da3b31f 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -1363,6 +1363,8 @@ const i18n = { 'rules.extractScopeRequired': 'Analysis scope is required', 'rules.extractFocus': 'Focus Areas', '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)...', // CLAUDE.md Manager @@ -3375,6 +3377,8 @@ const i18n = { 'rules.extractScopeRequired': '分析范围是必需的', 'rules.extractFocus': '关注领域', 'rules.extractFocusHint': '以逗号分隔的关注方面(例如:命名规范, 错误处理)', + 'rules.enableReview': '启用质量审查', + 'rules.enableReviewHint': 'AI 将验证生成的规则是否清晰、可操作且格式正确', 'rules.cliGenerating': '正在通过 CLI 生成规则(可能需要几分钟)...', // CLAUDE.md Manager diff --git a/ccw/src/templates/dashboard-js/views/claude-manager.js b/ccw/src/templates/dashboard-js/views/claude-manager.js index b444846e..4fb548ad 100644 --- a/ccw/src/templates/dashboard-js/views/claude-manager.js +++ b/ccw/src/templates/dashboard-js/views/claude-manager.js @@ -636,12 +636,6 @@ function renderFileMetadata() { '' + '' + '' + - '' + - '' + '' + '' + + '' + + '
' + + '
' + + '0' + + '' + (t('codexlens.filesWaiting') || 'files waiting') + '' + + '
' + + '
' + + '
--:--
' + + '
' + (t('codexlens.untilNextIndex') || 'until next index') + '
' + + '
' + + '
' + + '
' + + '' + + + // Last Index Result (shown when running) + '' + + // Start Configuration (shown when not running) '
' + '

' + (t('codexlens.watcherConfig') || 'Configuration') + '

' + @@ -5857,7 +5944,7 @@ function buildWatcherControlContent(status, defaultPath) { '
' + '
' + '' + - '' + '

' + (t('codexlens.debounceHint') || 'Time to wait before processing file changes') + '

' + '
' + @@ -5986,6 +6073,204 @@ function stopWatcherStatusPolling() { clearInterval(watcherPollingInterval); 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 = ' 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 = '' + (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 '
' + + '
' + + '#' + (result.history.length - i) + '' + + '' + timestamp + '' + + '
' + + '
' + + '
' + (h.files_indexed || 0) + ' indexed
' + + '
' + (h.files_removed || 0) + ' removed
' + + '
+' + (h.symbols_added || 0) + ' symbols
' + + '
' + ((h.errors && h.errors.length) || 0) + ' errors
' + + '
' + + (h.errors && h.errors.length > 0 ? '
' + + h.errors.slice(0, 2).map(function(e) { return '
• ' + e + '
'; }).join('') + + (h.errors.length > 2 ? '
... and ' + (h.errors.length - 2) + ' more
' : '') + + '
' : '') + + '
'; + }).join(''); + + var modal = document.createElement('div'); + modal.id = 'indexHistoryModal'; + modal.className = 'modal-backdrop'; + modal.innerHTML = ''; + 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 '
' + + '' + + '' + f + '' + + '
'; + }).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 = '
' + + '
' + (result.files_indexed || 0) + '
' + + '
Indexed
' + + '
' + + '
' + + '
' + (result.files_removed || 0) + '
' + + '
Removed
' + + '
' + + '
' + + '
' + (result.symbols_added || 0) + '
' + + '
+Symbols
' + + '
' + + '
' + + '
' + ((result.errors && result.errors.length) || 0) + '
' + + '
Errors
' + + '
'; + } + + // 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 - * @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) { var toggle = document.getElementById('watcherToggle'); var statsDiv = document.getElementById('watcherStats'); 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) { // Watcher failed - update UI to show stopped state @@ -6018,8 +6328,8 @@ function handleWatcherStatusUpdate(payload) { if (statsDiv) statsDiv.style.display = 'block'; if (configDiv) configDiv.style.display = 'none'; startWatcherStatusPolling(); - } else { - // Watcher stopped normally + } else if (payload.running === false) { + // Watcher stopped normally (only if running is explicitly false) if (toggle) toggle.checked = false; if (statsDiv) statsDiv.style.display = 'none'; if (configDiv) configDiv.style.display = 'block'; diff --git a/ccw/src/templates/dashboard-js/views/core-memory.js b/ccw/src/templates/dashboard-js/views/core-memory.js index af1db942..1a17fb3b 100644 --- a/ccw/src/templates/dashboard-js/views/core-memory.js +++ b/ccw/src/templates/dashboard-js/views/core-memory.js @@ -1,6 +1,56 @@ // Core Memory View // 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 function showNotification(message, type = 'info') { // Create notification container if it doesn't exist @@ -527,20 +577,24 @@ async function viewMemoryDetail(memoryId) { const modal = document.getElementById('memoryDetailModal'); 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'); body.innerHTML = `
- ${memory.summary + ${parsedSummary ? `

${t('coreMemory.summary')}

-
${escapeHtml(memory.summary)}
+
${escapeHtml(parsedSummary)}
` : '' }

${t('coreMemory.content')}

-
${escapeHtml(memory.content)}
+
${escapeHtml(parsedContent)}
${(() => { @@ -564,7 +618,7 @@ async function viewMemoryDetail(memoryId) { ${memory.raw_output ? `

${t('coreMemory.rawOutput')}

-
${escapeHtml(memory.raw_output)}
+
${escapeHtml(parseJsonStreamContent(memory.raw_output))}
` : '' } diff --git a/ccw/src/templates/dashboard-js/views/rules-manager.js b/ccw/src/templates/dashboard-js/views/rules-manager.js index a4b88cc5..8cc29d8b 100644 --- a/ccw/src/templates/dashboard-js/views/rules-manager.js +++ b/ccw/src/templates/dashboard-js/views/rules-manager.js @@ -347,7 +347,8 @@ var ruleCreateState = { generationType: 'description', description: '', extractScope: '', - extractFocus: '' + extractFocus: '', + enableReview: false }; function openRuleCreateModal() { @@ -363,7 +364,8 @@ function openRuleCreateModal() { generationType: 'description', description: '', extractScope: '', - extractFocus: '' + extractFocus: '', + enableReview: false }; // Create modal HTML @@ -506,6 +508,18 @@ function openRuleCreateModal() {
+ +
+ +

${t('rules.enableReviewHint')}

+
+
- -
- -
- - -
-
- - -
+ +