From 39056292b797970c0efe119de3ccca4b3f9609da Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 23 Dec 2025 18:35:30 +0800 Subject: [PATCH] feat: Add CodexLens Manager to dashboard and enhance GPU management - Introduced a new CodexLens Manager item in the dashboard for easier access. - Implemented GPU management commands in the CLI, including listing available GPUs, selecting a specific GPU, and resetting to automatic detection. - Enhanced the embedding generation process to utilize GPU resources more effectively, including batch size optimization for better performance. - Updated the embedder to support device ID options for GPU selection, ensuring compatibility with DirectML and CUDA. - Added detailed logging and error handling for GPU detection and selection processes. - Updated package version to 6.2.9 and added comprehensive documentation for Codex Agent Execution Protocol. --- .../protocols/analysis-protocol.md | 6 +- .../cli-templates/protocols/write-protocol.md | 21 - AGENTS.md | 331 +++++++++ ccw/src/core/routes/codexlens-routes.ts | 109 ++- ccw/src/core/routes/status-routes.ts | 51 ++ .../dashboard-js/components/cli-status.js | 56 ++ .../dashboard-js/components/navigation.js | 8 + ccw/src/templates/dashboard-js/i18n.js | 87 ++- .../dashboard-js/views/cli-manager.js | 86 ++- .../dashboard-js/views/codexlens-manager.js | 699 +++++++++++++++++- ccw/src/templates/dashboard.html | 5 + ccw/src/tools/codex-lens.ts | 11 + codex-lens/src/codexlens/cli/commands.py | 175 +++++ .../src/codexlens/cli/embedding_manager.py | 5 +- codex-lens/src/codexlens/semantic/embedder.py | 30 +- .../src/codexlens/semantic/gpu_support.py | 228 +++++- package-lock.json | 4 +- 17 files changed, 1834 insertions(+), 78 deletions(-) create mode 100644 AGENTS.md diff --git a/.claude/workflows/cli-templates/protocols/analysis-protocol.md b/.claude/workflows/cli-templates/protocols/analysis-protocol.md index 0183a94c..3ee62740 100644 --- a/.claude/workflows/cli-templates/protocols/analysis-protocol.md +++ b/.claude/workflows/cli-templates/protocols/analysis-protocol.md @@ -1,9 +1,7 @@ # Analysis Mode Protocol ## Mode Definition - **Mode**: `analysis` (READ-ONLY) -**Tools**: Gemini, Qwen (default mode) ## Operation Boundaries @@ -27,8 +25,8 @@ 2. **Read** and analyze CONTEXT files thoroughly 3. **Identify** patterns, issues, and dependencies 4. **Generate** insights and recommendations -5. **Output** structured analysis (text response only) -6. **Validate** EXPECTED deliverables met +5. **Validate** EXPECTED deliverables met +6. **Output** structured analysis (text response only) ## Core Requirements diff --git a/.claude/workflows/cli-templates/protocols/write-protocol.md b/.claude/workflows/cli-templates/protocols/write-protocol.md index 251fda6b..b9f1597d 100644 --- a/.claude/workflows/cli-templates/protocols/write-protocol.md +++ b/.claude/workflows/cli-templates/protocols/write-protocol.md @@ -1,10 +1,5 @@ # Write Mode Protocol -## Mode Definition - -**Mode**: `write` (FILE OPERATIONS) / `auto` (FULL OPERATIONS) -**Tools**: Codex (auto), Gemini/Qwen (write) - ## Operation Boundaries ### MODE: write @@ -15,12 +10,6 @@ **Restrictions**: Follow project conventions, cannot break existing functionality -### MODE: auto (Codex only) -- All `write` mode operations -- Run tests and builds -- Commit code incrementally -- Full autonomous development - **Constraint**: Must test every change ## Execution Flow @@ -33,16 +22,6 @@ 5. **Validate** changes 6. **Report** file changes -### MODE: auto -1. **Parse** all 6 fields -2. **Analyze** CONTEXT files - find 3+ similar patterns -3. **Plan** implementation following RULES -4. **Generate** code with tests -5. **Run** tests continuously -6. **Commit** working code incrementally -7. **Validate** EXPECTED deliverables -8. **Report** results - ## Core Requirements **ALWAYS**: diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..94200091 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,331 @@ +# Codex Agent Execution Protocol + +## Overview + +**Role**: Autonomous development, implementation, and testing specialist + + +## Prompt Structure + +All prompts follow this 6-field format: + +``` +PURPOSE: [development goal] +TASK: [specific implementation task] +MODE: [auto|write] +CONTEXT: [file patterns] +EXPECTED: [deliverables] +RULES: [templates | additional constraints] +``` + +**Subtask indicator**: `Subtask N of M: [title]` or `CONTINUE TO NEXT SUBTASK` + +## MODE Definitions + +### MODE: auto (default) + +**Permissions**: +- Full file operations (create/modify/delete) +- Run tests and builds +- Commit code incrementally + +**Execute**: +1. Parse PURPOSE and TASK +2. Analyze CONTEXT files - find 3+ similar patterns +3. Plan implementation following RULES +4. Generate code with tests +5. Run tests continuously +6. Commit working code incrementally +7. Validate EXPECTED deliverables +8. Report results (with context for next subtask if multi-task) + +**Constraint**: Must test every change + +### MODE: write + +**Permissions**: +- Focused file operations +- Create/modify specific files +- Run tests for validation + +**Execute**: +1. Analyze CONTEXT files +2. Make targeted changes +3. Validate tests pass +4. Report file changes + +## Execution Protocol + +### Core Requirements + +**ALWAYS**: +- Parse all 6 fields (PURPOSE, TASK, MODE, CONTEXT, EXPECTED, RULES) +- Study CONTEXT files - find 3+ similar patterns before implementing +- Apply RULES (templates + constraints) exactly +- Test continuously after every change +- Commit incrementally with working code +- Match project style and patterns exactly +- List all created/modified files at output beginning +- Use direct binary calls (avoid shell wrappers) +- Prefer apply_patch for text edits +- Configure Windows UTF-8 encoding for Chinese support + +**NEVER**: +- Make assumptions without code verification +- Ignore existing patterns +- Skip tests +- Use clever tricks over boring solutions +- Over-engineer solutions +- Break existing code or backward compatibility +- Exceed 3 failed attempts without stopping + +### RULES Processing + +- Parse RULES field to extract template content and constraints +- Recognize `|` as separator: `template content | additional constraints` +- Apply ALL template guidelines as mandatory +- Apply ALL additional constraints as mandatory +- Treat rule violations as task failures + +### Multi-Task Execution (Resume Pattern) + +**First subtask**: Standard execution flow above +**Subsequent subtasks** (via `resume --last`): +- Recall context from previous subtasks +- Build on previous work (don't repeat) +- Maintain consistency with established patterns +- Focus on current subtask scope only +- Test integration with previous work +- Report context for next subtask + +## System Optimization + +**Direct Binary Calls**: Always call binaries directly in `functions.shell`, set `workdir`, avoid shell wrappers (`bash -lc`, `cmd /c`, etc.) + +**Text Editing Priority**: +1. Use `apply_patch` tool for all routine text edits +2. Fall back to `sed` for single-line substitutions if unavailable +3. Avoid Python editing scripts unless both fail + +**apply_patch invocation**: +```json +{ + "command": ["apply_patch", "*** Begin Patch\n*** Update File: path/to/file\n@@\n- old\n+ new\n*** End Patch\n"], + "workdir": "", + "justification": "Brief reason" +} +``` + +**Windows UTF-8 Encoding** (before commands): +```powershell +[Console]::InputEncoding = [Text.UTF8Encoding]::new($false) +[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) +chcp 65001 > $null +``` + +## Output Standards + +### Format Priority + +**If template defines output format** → Follow template format EXACTLY (all sections mandatory) + +**If template has no format** → Use default format below based on task type + +### Default Output Formats + +#### Single Task Implementation + +```markdown +# Implementation: [TASK Title] + +## Changes +- Created: `path/to/file1.ext` (X lines) +- Modified: `path/to/file2.ext` (+Y/-Z lines) +- Deleted: `path/to/file3.ext` + +## Summary +[2-3 sentence overview of what was implemented] + +## Key Decisions +1. [Decision] - Rationale and reference to similar pattern +2. [Decision] - path/to/reference:line + +## Implementation Details +[Evidence-based description with code references] + +## Testing +- Tests written: X new tests +- Tests passing: Y/Z tests +- Coverage: N% + +## Validation +✅ Tests: X passing +✅ Coverage: Y% +✅ Build: Success + +## Next Steps +[Recommendations or future improvements] +``` + +#### Multi-Task Execution (with Resume) + +**First Subtask**: +```markdown +# Subtask 1/N: [TASK Title] + +## Changes +[List of file changes] + +## Implementation +[Details with code references] + +## Testing +✅ Tests: X passing +✅ Integration: Compatible with existing code + +## Context for Next Subtask +- Key decisions: [established patterns] +- Files created: [paths and purposes] +- Integration points: [where next subtask should connect] +``` + +**Subsequent Subtasks**: +```markdown +# Subtask N/M: [TASK Title] + +## Changes +[List of file changes] + +## Integration Notes +✅ Compatible with subtask N-1 +✅ Maintains established patterns +✅ Tests pass with previous work + +## Implementation +[Details with code references] + +## Testing +✅ Tests: X passing +✅ Total coverage: Y% + +## Context for Next Subtask +[If not final subtask, provide context for continuation] +``` + +#### Partial Completion + +```markdown +# Task Status: Partially Completed + +## Completed +- [What worked successfully] +- Files: `path/to/completed.ext` + +## Blocked +- **Issue**: [What failed] +- **Root Cause**: [Analysis of failure] +- **Attempted**: [Solutions tried - attempt X of 3] + +## Required +[What's needed to proceed] + +## Recommendation +[Suggested next steps or alternative approaches] +``` + +### Code References + +**Format**: `path/to/file:line_number` + +**Example**: `src/auth/jwt.ts:45` - Implemented token validation following pattern from `src/auth/session.ts:78` + +### Related Files Section + +**Always include at output beginning** - List ALL files analyzed, created, or modified: + +```markdown +## Related Files +- `path/to/file1.ext` - [Role in implementation] +- `path/to/file2.ext` - [Reference pattern used] +- `path/to/file3.ext` - [Modified for X reason] +``` + +## Error Handling + +### Three-Attempt Rule + +**On 3rd failed attempt**: +1. Stop execution +2. Report: What attempted, what failed, root cause +3. Request guidance or suggest alternatives + +### Recovery Strategies + +| Error Type | Response | +|------------|----------| +| **Syntax/Type** | Review errors → Fix → Re-run tests → Validate build | +| **Runtime** | Analyze stack trace → Add error handling → Test error cases | +| **Test Failure** | Debug in isolation → Review setup → Fix implementation/test | +| **Build Failure** | Check messages → Fix incrementally → Validate each fix | + +## Quality Standards + +### Code Quality +- Follow project's existing patterns +- Match import style and naming conventions +- Single responsibility per function/class +- DRY (Don't Repeat Yourself) +- YAGNI (You Aren't Gonna Need It) + +### Testing +- Test all public functions +- Test edge cases and error conditions +- Mock external dependencies +- Target 80%+ coverage + +### Error Handling +- Proper try-catch blocks +- Clear error messages +- Graceful degradation +- Don't expose sensitive info + +## Core Principles + +**Incremental Progress**: +- Small, testable changes +- Commit working code frequently +- Build on previous work (subtasks) + +**Evidence-Based**: +- Study 3+ similar patterns before implementing +- Match project style exactly +- Verify with existing code + +**Pragmatic**: +- Boring solutions over clever code +- Simple over complex +- Adapt to project reality + +**Context Continuity** (Multi-Task): +- Leverage resume for consistency +- Maintain established patterns +- Test integration between subtasks + +## Execution Checklist + +**Before**: +- [ ] Understand PURPOSE and TASK clearly +- [ ] Review CONTEXT files, find 3+ patterns +- [ ] Check RULES templates and constraints + +**During**: +- [ ] Follow existing patterns exactly +- [ ] Write tests alongside code +- [ ] Run tests after every change +- [ ] Commit working code incrementally + +**After**: +- [ ] All tests pass +- [ ] Coverage meets target +- [ ] Build succeeds +- [ ] All EXPECTED deliverables met diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index 63047d91..8ff78d1b 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -80,6 +80,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise // API: CodexLens Index List - Get all indexed projects with details if (pathname === '/api/codexlens/indexes') { try { + // Check if CodexLens is installed first (without auto-installing) + const venvStatus = await checkVenvStatus(); + if (!venvStatus.ready) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, indexes: [], totalSize: 0, totalSizeFormatted: '0 B' })); + return true; + } // Get config for index directory path const configResult = await executeCodexLens(['config', '--json']); let indexDir = ''; @@ -290,14 +297,24 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise // API: CodexLens Config - GET (Get current configuration with index count) if (pathname === '/api/codexlens/config' && req.method === 'GET') { try { + // Check if CodexLens is installed first (without auto-installing) + const venvStatus = await checkVenvStatus(); + + let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 }; + + // If not installed, return default config without executing CodexLens + if (!venvStatus.ready) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(responseData)); + return true; + } + // Fetch both config and status to merge index_count const [configResult, statusResult] = await Promise.all([ executeCodexLens(['config', '--json']), executeCodexLens(['status', '--json']) ]); - let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 }; - // Parse config (extract JSON from output that may contain log messages) if (configResult.success) { try { @@ -682,6 +699,87 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise return true; } + // API: List available GPU devices for selection + if (pathname === '/api/codexlens/gpu/list' && req.method === 'GET') { + try { + // Check if CodexLens is installed first (without auto-installing) + const venvStatus = await checkVenvStatus(); + if (!venvStatus.ready) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, devices: [], selected_device_id: null })); + return true; + } + const result = await executeCodexLens(['gpu-list', '--json']); + if (result.success) { + try { + const parsed = extractJSON(result.output); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(parsed)); + } catch { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, devices: [], output: result.output })); + } + } else { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: result.error })); + } + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err.message })); + } + return true; + } + + // API: Select GPU device for embedding + if (pathname === '/api/codexlens/gpu/select' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { device_id } = body; + + if (device_id === undefined || device_id === null) { + return { success: false, error: 'device_id is required', status: 400 }; + } + + try { + const result = await executeCodexLens(['gpu-select', String(device_id), '--json']); + if (result.success) { + try { + const parsed = extractJSON(result.output); + return parsed; + } catch { + return { success: true, message: 'GPU selected', output: result.output }; + } + } else { + return { success: false, error: result.error, status: 500 }; + } + } catch (err) { + return { success: false, error: err.message, status: 500 }; + } + }); + return true; + } + + // API: Reset GPU selection to auto-detection + if (pathname === '/api/codexlens/gpu/reset' && req.method === 'POST') { + handlePostRequest(req, res, async () => { + try { + const result = await executeCodexLens(['gpu-reset', '--json']); + if (result.success) { + try { + const parsed = extractJSON(result.output); + return parsed; + } catch { + return { success: true, message: 'GPU selection reset', output: result.output }; + } + } else { + return { success: false, error: result.error, status: 500 }; + } + } catch (err) { + return { success: false, error: err.message, status: 500 }; + } + }); + return true; + } + // API: CodexLens Semantic Search Install (with GPU mode support) if (pathname === '/api/codexlens/semantic/install' && req.method === 'POST') { handlePostRequest(req, res, async (body) => { @@ -721,6 +819,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise // API: CodexLens Model List (list available embedding models) if (pathname === '/api/codexlens/models' && req.method === 'GET') { try { + // Check if CodexLens is installed first (without auto-installing) + const venvStatus = await checkVenvStatus(); + if (!venvStatus.ready) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'CodexLens not installed' })); + return true; + } const result = await executeCodexLens(['model-list', '--json']); if (result.success) { try { diff --git a/ccw/src/core/routes/status-routes.ts b/ccw/src/core/routes/status-routes.ts index 3b18f512..83b0fc02 100644 --- a/ccw/src/core/routes/status-routes.ts +++ b/ccw/src/core/routes/status-routes.ts @@ -4,9 +4,56 @@ * Aggregated status endpoint for faster dashboard loading */ import type { IncomingMessage, ServerResponse } from 'http'; +import { existsSync } from 'fs'; +import { join } from 'path'; +import { homedir } from 'os'; import { getCliToolsStatus } from '../../tools/cli-executor.js'; import { checkVenvStatus, checkSemanticStatus } from '../../tools/codex-lens.js'; +/** + * Check CCW installation status + * Verifies that required workflow files are installed in user's home directory + */ +function checkCcwInstallStatus(): { + installed: boolean; + workflowsInstalled: boolean; + missingFiles: string[]; + installPath: string; +} { + const claudeDir = join(homedir(), '.claude'); + const workflowsDir = join(claudeDir, 'workflows'); + + // Required workflow files for full functionality + const requiredFiles = [ + 'chinese-response.md', + 'windows-platform.md', + 'cli-tools-usage.md', + 'coding-philosophy.md', + 'context-tools.md', + 'file-modification.md' + ]; + + const missingFiles: string[] = []; + + // Check each required file + for (const file of requiredFiles) { + const filePath = join(workflowsDir, file); + if (!existsSync(filePath)) { + missingFiles.push(file); + } + } + + const workflowsInstalled = existsSync(workflowsDir) && missingFiles.length === 0; + const installed = existsSync(claudeDir) && workflowsInstalled; + + return { + installed, + workflowsInstalled, + missingFiles, + installPath: claudeDir + }; +} + export interface RouteContext { pathname: string; url: URL; @@ -27,6 +74,9 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise { // API: Aggregated Status (all statuses in one call) if (pathname === '/api/status/all') { try { + // Check CCW installation status (sync, fast) + const ccwInstallStatus = checkCcwInstallStatus(); + // Execute all status checks in parallel const [cliStatus, codexLensStatus, semanticStatus] = await Promise.all([ getCliToolsStatus(), @@ -39,6 +89,7 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise { cli: cliStatus, codexLens: codexLensStatus, semantic: semanticStatus, + ccwInstall: ccwInstallStatus, timestamp: new Date().toISOString() }; diff --git a/ccw/src/templates/dashboard-js/components/cli-status.js b/ccw/src/templates/dashboard-js/components/cli-status.js index a90d5b47..04e24cb8 100644 --- a/ccw/src/templates/dashboard-js/components/cli-status.js +++ b/ccw/src/templates/dashboard-js/components/cli-status.js @@ -5,6 +5,7 @@ let cliToolStatus = { gemini: {}, qwen: {}, codex: {}, claude: {} }; let codexLensStatus = { ready: false }; let semanticStatus = { available: false }; +let ccwInstallStatus = { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' }; let defaultCliTool = 'gemini'; let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; // plain, yaml, json @@ -38,10 +39,12 @@ async function loadAllStatuses() { cliToolStatus = data.cli || { gemini: {}, qwen: {}, codex: {}, claude: {} }; codexLensStatus = data.codexLens || { ready: false }; semanticStatus = data.semantic || { available: false }; + ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' }; // Update badges updateCliBadge(); updateCodexLensBadge(); + updateCcwInstallBadge(); return data; } catch (err) { @@ -187,6 +190,25 @@ function updateCodexLensBadge() { } } +function updateCcwInstallBadge() { + const badge = document.getElementById('badgeCcwInstall'); + if (badge) { + if (ccwInstallStatus.installed) { + badge.textContent = t('status.installed'); + badge.classList.add('text-success'); + badge.classList.remove('text-warning', 'text-destructive'); + } else if (ccwInstallStatus.workflowsInstalled === false) { + badge.textContent = t('status.incomplete'); + badge.classList.add('text-warning'); + badge.classList.remove('text-success', 'text-destructive'); + } else { + badge.textContent = t('status.notInstalled'); + badge.classList.add('text-destructive'); + badge.classList.remove('text-success', 'text-warning'); + } + } +} + // ========== Rendering ========== function renderCliStatus() { const container = document.getElementById('cli-status-panel'); @@ -310,6 +332,39 @@ function renderCliStatus() { ` : ''; + // CCW Installation Status card (show warning if not fully installed) + const ccwInstallHtml = !ccwInstallStatus.installed ? ` +
+
+ + ${t('status.ccwInstall')} + ${t('status.required')} +
+
+ ${t('status.ccwInstallDesc')} +
+
+ + + ${ccwInstallStatus.missingFiles.length} ${t('status.filesMissing')} + +
+
+
+

${t('status.missingFiles')}:

+
    + ${ccwInstallStatus.missingFiles.slice(0, 3).map(f => `
  • ${f}
  • `).join('')} + ${ccwInstallStatus.missingFiles.length > 3 ? `
  • +${ccwInstallStatus.missingFiles.length - 3} more...
  • ` : ''} +
+
+
+

${t('status.runToFix')}:

+ ccw install +
+
+
+ ` : ''; + // CLI Settings section const settingsHtml = `
@@ -392,6 +447,7 @@ function renderCliStatus() {
+ ${ccwInstallHtml}
${toolsHtml} ${codexLensHtml} diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js index 4cb26670..069e7e1f 100644 --- a/ccw/src/templates/dashboard-js/components/navigation.js +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -143,6 +143,12 @@ function initNavigation() { } else { console.error('renderCoreMemoryView not defined - please refresh the page'); } + } else if (currentView === 'codexlens-manager') { + if (typeof renderCodexLensManager === 'function') { + renderCodexLensManager(); + } else { + console.error('renderCodexLensManager not defined - please refresh the page'); + } } }); }); @@ -183,6 +189,8 @@ function updateContentTitle() { titleEl.textContent = t('title.helpGuide'); } else if (currentView === 'core-memory') { titleEl.textContent = t('title.coreMemory'); + } else if (currentView === 'codexlens-manager') { + titleEl.textContent = t('title.codexLensManager'); } else if (currentView === 'liteTasks') { const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') }; titleEl.textContent = names[currentLiteType] || t('title.liteTasks'); diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 7922a050..02c9c7ea 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -41,6 +41,7 @@ const i18n = { 'nav.explorer': 'Explorer', 'nav.status': 'Status', 'nav.history': 'History', + 'nav.codexLensManager': 'CodexLens', 'nav.memory': 'Memory', 'nav.contextMemory': 'Context', 'nav.coreMemory': 'Core Memory', @@ -98,7 +99,8 @@ const i18n = { 'title.hookManager': 'Hook Manager', 'title.memoryModule': 'Memory Module', 'title.promptHistory': 'Prompt History', - + 'title.codexLensManager': 'CodexLens Manager', + // Search 'search.placeholder': 'Search...', @@ -215,6 +217,7 @@ const i18n = { 'cli.default': 'Default', 'cli.install': 'Install', 'cli.uninstall': 'Uninstall', + 'cli.openManager': 'Manager', 'cli.initIndex': 'Init Index', 'cli.geminiDesc': 'Google AI for code analysis', 'cli.qwenDesc': 'Alibaba AI assistant', @@ -226,9 +229,11 @@ const i18n = { // CodexLens Configuration 'codexlens.config': 'CodexLens Configuration', + 'codexlens.configDesc': 'Manage code indexing, semantic search, and embedding models', 'codexlens.status': 'Status', 'codexlens.installed': 'Installed', 'codexlens.notInstalled': 'Not Installed', + 'codexlens.installFirst': 'Install CodexLens to access semantic search and model management features', 'codexlens.indexes': 'Indexes', 'codexlens.currentWorkspace': 'Current Workspace', 'codexlens.indexStoragePath': 'Index Storage Path', @@ -237,6 +242,8 @@ const i18n = { 'codexlens.newStoragePath': 'New Storage Path', 'codexlens.pathPlaceholder': 'e.g., /path/to/indexes or ~/.codexlens/indexes', 'codexlens.pathInfo': 'Supports ~ for home directory. Changes take effect immediately.', + 'codexlens.pathUnchanged': 'Path unchanged', + 'codexlens.pathEmpty': 'Path cannot be empty', 'codexlens.migrationRequired': 'Migration Required', 'codexlens.migrationWarning': 'After changing the path, existing indexes will need to be re-initialized for each workspace.', 'codexlens.actions': 'Actions', @@ -244,6 +251,17 @@ const i18n = { 'codexlens.cleanCurrentWorkspace': 'Clean Current Workspace', 'codexlens.cleanAllIndexes': 'Clean All Indexes', 'codexlens.installCodexLens': 'Install CodexLens', + 'codexlens.createIndex': 'Create Index', + 'codexlens.embeddingModel': 'Embedding Model', + 'codexlens.modelHint': 'Select embedding model for vector search (models with ✓ are installed)', + 'codexlens.fullIndex': 'Full', + 'codexlens.vectorIndex': 'Vector', + 'codexlens.ftsIndex': 'FTS', + 'codexlens.fullIndexDesc': 'FTS + Semantic search (recommended)', + 'codexlens.vectorIndexDesc': 'Semantic search with embeddings only', + 'codexlens.ftsIndexDesc': 'Fast full-text search only', + 'codexlens.indexTypeHint': 'Full index includes FTS + semantic search. FTS only is faster but without AI-powered search.', + 'codexlens.maintenance': 'Maintenance', 'codexlens.testSearch': 'Test Search', 'codexlens.testFunctionality': 'test CodexLens functionality', 'codexlens.textSearch': 'Text Search', @@ -291,6 +309,17 @@ const i18n = { 'codexlens.cudaModeDesc': 'NVIDIA GPU (requires CUDA Toolkit)', 'common.recommended': 'Recommended', 'common.unavailable': 'Unavailable', + 'common.auto': 'Auto', + + // GPU Device Selection + 'codexlens.selectGpuDevice': 'Select GPU Device', + 'codexlens.discrete': 'Discrete', + 'codexlens.integrated': 'Integrated', + 'codexlens.selectingGpu': 'Selecting GPU...', + 'codexlens.gpuSelected': 'GPU selected', + 'codexlens.resettingGpu': 'Resetting GPU selection...', + 'codexlens.gpuReset': 'GPU selection reset to auto', + 'codexlens.resetToAuto': 'Reset to Auto', 'codexlens.modelManagement': 'Model Management', 'codexlens.loadingModels': 'Loading models...', 'codexlens.downloadModel': 'Download', @@ -444,6 +473,19 @@ const i18n = { 'lang.windowsDisableSuccess': 'Windows platform guidelines disabled', 'lang.windowsEnableFailed': 'Failed to enable Windows platform guidelines', 'lang.windowsDisableFailed': 'Failed to disable Windows platform guidelines', + 'lang.installRequired': 'Run "ccw install" to enable this feature', + + // CCW Installation Status + 'status.installed': 'Installed', + 'status.incomplete': 'Incomplete', + 'status.notInstalled': 'Not Installed', + 'status.ccwInstall': 'CCW Workflows', + 'status.ccwInstallDesc': 'Required workflow files for full functionality', + 'status.required': 'Required', + 'status.filesMissing': 'files missing', + 'status.missingFiles': 'Missing files', + 'status.runToFix': 'Run to fix', + 'cli.promptFormat': 'Prompt Format', 'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation', 'cli.storageBackend': 'Storage Backend', @@ -1457,6 +1499,7 @@ const i18n = { 'nav.explorer': '文件浏览器', 'nav.status': '状态', 'nav.history': '历史', + 'nav.codexLensManager': 'CodexLens', 'nav.memory': '记忆', 'nav.contextMemory': '活动', 'nav.coreMemory': '核心记忆', @@ -1514,6 +1557,7 @@ const i18n = { 'title.hookManager': '钩子管理', 'title.memoryModule': '记忆模块', 'title.promptHistory': '提示历史', + 'title.codexLensManager': 'CodexLens 管理', // Search 'search.placeholder': '搜索...', @@ -1631,6 +1675,7 @@ const i18n = { 'cli.default': '默认', 'cli.install': '安装', 'cli.uninstall': '卸载', + 'cli.openManager': '管理', 'cli.initIndex': '初始化索引', 'cli.geminiDesc': 'Google AI 代码分析', 'cli.qwenDesc': '阿里通义 AI 助手', @@ -1642,9 +1687,11 @@ const i18n = { // CodexLens 配置 'codexlens.config': 'CodexLens 配置', + 'codexlens.configDesc': '管理代码索引、语义搜索和嵌入模型', 'codexlens.status': '状态', 'codexlens.installed': '已安装', 'codexlens.notInstalled': '未安装', + 'codexlens.installFirst': '安装 CodexLens 以访问语义搜索和模型管理功能', 'codexlens.indexes': '索引', 'codexlens.currentWorkspace': '当前工作区', 'codexlens.indexStoragePath': '索引存储路径', @@ -1653,6 +1700,8 @@ const i18n = { 'codexlens.newStoragePath': '新存储路径', 'codexlens.pathPlaceholder': '例如:/path/to/indexes 或 ~/.codexlens/indexes', 'codexlens.pathInfo': '支持 ~ 表示用户目录。更改立即生效。', + 'codexlens.pathUnchanged': '路径未变更', + 'codexlens.pathEmpty': '路径不能为空', 'codexlens.migrationRequired': '需要迁移', 'codexlens.migrationWarning': '更改路径后,需要为每个工作区重新初始化索引。', 'codexlens.actions': '操作', @@ -1660,6 +1709,17 @@ const i18n = { 'codexlens.cleanCurrentWorkspace': '清理当前工作空间', 'codexlens.cleanAllIndexes': '清理所有索引', 'codexlens.installCodexLens': '安装 CodexLens', + 'codexlens.createIndex': '创建索引', + 'codexlens.embeddingModel': '嵌入模型', + 'codexlens.modelHint': '选择向量搜索的嵌入模型(带 ✓ 的已安装)', + 'codexlens.fullIndex': '全部', + 'codexlens.vectorIndex': '向量', + 'codexlens.ftsIndex': 'FTS', + 'codexlens.fullIndexDesc': 'FTS + 语义搜索(推荐)', + 'codexlens.vectorIndexDesc': '仅语义嵌入搜索', + 'codexlens.ftsIndexDesc': '仅快速全文搜索', + 'codexlens.indexTypeHint': '完整索引包含 FTS + 语义搜索。仅 FTS 更快但无 AI 搜索功能。', + 'codexlens.maintenance': '维护', 'codexlens.testSearch': '测试搜索', 'codexlens.testFunctionality': '测试 CodexLens 功能', 'codexlens.textSearch': '文本搜索', @@ -1707,6 +1767,18 @@ const i18n = { 'codexlens.cudaModeDesc': 'NVIDIA GPU(需要 CUDA Toolkit)', 'common.recommended': '推荐', 'common.unavailable': '不可用', + 'common.auto': '自动', + + // GPU 设备选择 + 'codexlens.selectGpuDevice': '选择 GPU 设备', + 'codexlens.discrete': '独立显卡', + 'codexlens.integrated': '集成显卡', + 'codexlens.selectingGpu': '选择 GPU 中...', + 'codexlens.gpuSelected': 'GPU 已选择', + 'codexlens.resettingGpu': '重置 GPU 选择中...', + 'codexlens.gpuReset': 'GPU 选择已重置为自动', + 'codexlens.resetToAuto': '重置为自动', + 'codexlens.modelManagement': '模型管理', 'codexlens.loadingModels': '加载模型中...', 'codexlens.downloadModel': '下载', @@ -1860,6 +1932,19 @@ const i18n = { 'lang.windowsDisableSuccess': 'Windows 平台规范已禁用', 'lang.windowsEnableFailed': '启用 Windows 平台规范失败', 'lang.windowsDisableFailed': '禁用 Windows 平台规范失败', + 'lang.installRequired': '请运行 "ccw install" 以启用此功能', + + // CCW 安装状态 + 'status.installed': '已安装', + 'status.incomplete': '不完整', + 'status.notInstalled': '未安装', + 'status.ccwInstall': 'CCW 工作流', + 'status.ccwInstallDesc': '完整功能所需的工作流文件', + 'status.required': '必需', + 'status.filesMissing': '个文件缺失', + 'status.missingFiles': '缺失文件', + 'status.runToFix': '修复命令', + 'cli.promptFormat': '提示词格式', 'cli.promptFormatDesc': '多轮对话拼接格式', 'cli.storageBackend': '存储后端', diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index ca9c86c4..93a87330 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -9,6 +9,26 @@ var ccwEndpointTools = []; var cliToolConfig = null; // Store loaded CLI config var predefinedModels = {}; // Store predefined models per tool +// ========== Navigation Helpers ========== + +/** + * Navigate to CodexLens Manager page + */ +function navigateToCodexLensManager() { + var navItem = document.querySelector('.nav-item[data-view="codexlens-manager"]'); + if (navItem) { + navItem.click(); + } else { + // Fallback: try to render directly + if (typeof renderCodexLensManager === 'function') { + currentView = 'codexlens-manager'; + renderCodexLensManager(); + } else { + showRefreshToast(t('common.error') + ': CodexLens Manager not available', 'error'); + } + } +} + // ========== CCW Installations ========== async function loadCcwInstallations() { try { @@ -314,8 +334,7 @@ async function renderCliManager() { '
' + '
' + '
' + - '
' + - '
'; + '
'; // Render sub-panels renderToolsSection(); @@ -329,11 +348,6 @@ async function renderCliManager() { initStorageManager(); } - // Initialize index manager card - if (typeof initIndexManager === 'function') { - initIndexManager(); - } - // Initialize Lucide icons if (window.lucide) lucide.createIcons(); } @@ -434,28 +448,22 @@ function renderToolsSection() { ''; }).join(''); - // CodexLens item - var codexLensHtml = '
' + + // CodexLens item - simplified view with link to manager page + var codexLensHtml = '
' + '
' + '' + '
' + '
CodexLens Index' + - '
' + + '
' + '
' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '
' + '
' + '
' + '
' + (codexLensStatus.ready ? ' v' + (codexLensStatus.version || 'installed') + '' + - '' + - '' + - '' + - '' + - '' + '' : ' ' + t('cli.notInstalled') + '' + - '') + + '') + '
' + '
'; @@ -606,6 +614,16 @@ async function loadWindowsPlatformSettings() { async function toggleChineseResponse(enabled) { if (chineseResponseLoading) return; + + // Pre-check: verify CCW workflows are installed (only when enabling) + if (enabled && typeof ccwInstallStatus !== 'undefined' && !ccwInstallStatus.installed) { + var missingFile = ccwInstallStatus.missingFiles.find(function(f) { return f === 'chinese-response.md'; }); + if (missingFile) { + showRefreshToast(t('lang.installRequired'), 'warning'); + return; + } + } + chineseResponseLoading = true; try { @@ -617,7 +635,14 @@ async function toggleChineseResponse(enabled) { if (!response.ok) { var errData = await response.json(); - throw new Error(errData.error || 'Failed to update setting'); + // Show specific error message from backend + var errorMsg = errData.error || 'Failed to update setting'; + if (errorMsg.includes('not found')) { + showRefreshToast(t('lang.installRequired'), 'warning'); + } else { + showRefreshToast((enabled ? t('lang.enableFailed') : t('lang.disableFailed')) + ': ' + errorMsg, 'error'); + } + throw new Error(errorMsg); } var data = await response.json(); @@ -630,7 +655,7 @@ async function toggleChineseResponse(enabled) { showRefreshToast(enabled ? t('lang.enableSuccess') : t('lang.disableSuccess'), 'success'); } catch (err) { console.error('Failed to toggle Chinese response:', err); - showRefreshToast(enabled ? t('lang.enableFailed') : t('lang.disableFailed'), 'error'); + // Error already shown in the !response.ok block } finally { chineseResponseLoading = false; } @@ -638,6 +663,16 @@ async function toggleChineseResponse(enabled) { async function toggleWindowsPlatform(enabled) { if (windowsPlatformLoading) return; + + // Pre-check: verify CCW workflows are installed (only when enabling) + if (enabled && typeof ccwInstallStatus !== 'undefined' && !ccwInstallStatus.installed) { + var missingFile = ccwInstallStatus.missingFiles.find(function(f) { return f === 'windows-platform.md'; }); + if (missingFile) { + showRefreshToast(t('lang.installRequired'), 'warning'); + return; + } + } + windowsPlatformLoading = true; try { @@ -649,7 +684,14 @@ async function toggleWindowsPlatform(enabled) { if (!response.ok) { var errData = await response.json(); - throw new Error(errData.error || 'Failed to update setting'); + // Show specific error message from backend + var errorMsg = errData.error || 'Failed to update setting'; + if (errorMsg.includes('not found')) { + showRefreshToast(t('lang.installRequired'), 'warning'); + } else { + showRefreshToast((enabled ? t('lang.windowsEnableFailed') : t('lang.windowsDisableFailed')) + ': ' + errorMsg, 'error'); + } + throw new Error(errorMsg); } var data = await response.json(); @@ -662,7 +704,7 @@ async function toggleWindowsPlatform(enabled) { showRefreshToast(enabled ? t('lang.windowsEnableSuccess') : t('lang.windowsDisableSuccess'), 'success'); } catch (err) { console.error('Failed to toggle Windows platform:', err); - showRefreshToast(enabled ? t('lang.windowsEnableFailed') : t('lang.windowsDisableFailed'), 'error'); + // Error already shown in the !response.ok block } finally { windowsPlatformLoading = false; } diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index 79d994ca..85ac89bd 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -337,6 +337,8 @@ function initCodexLensConfigEvents(currentConfig) { // Store detected GPU info var detectedGpuInfo = null; +// Store available GPU devices +var availableGpuDevices = null; /** * Detect GPU support @@ -363,11 +365,13 @@ async function loadSemanticDepsStatus() { if (!container) return; try { - // Detect GPU support in parallel + // Detect GPU support and load GPU devices in parallel var gpuPromise = detectGpuSupport(); + var gpuDevicesPromise = loadGpuDevices(); var response = await fetch('/api/codexlens/semantic/status'); var result = await response.json(); var gpuInfo = await gpuPromise; + var gpuDevices = await gpuDevicesPromise; if (result.available) { // Build accelerator badge @@ -386,6 +390,9 @@ async function loadSemanticDepsStatus() { acceleratorClass = 'bg-red-500/20 text-red-600'; } + // Build GPU device selector if multiple GPUs available + var gpuDeviceSelector = buildGpuDeviceSelector(gpuDevices); + container.innerHTML = '
' + '
' + @@ -402,6 +409,7 @@ async function loadSemanticDepsStatus() { ? '' + result.providers.join(', ') + '' : '') + '
' + + gpuDeviceSelector + '
'; } else { // Build GPU mode options @@ -506,6 +514,134 @@ function getSelectedGpuMode() { return selected ? selected.value : 'cpu'; } +/** + * Load available GPU devices + */ +async function loadGpuDevices() { + try { + var response = await fetch('/api/codexlens/gpu/list'); + var result = await response.json(); + if (result.success && result.result) { + availableGpuDevices = result.result; + return result.result; + } + } catch (err) { + console.error('GPU devices load failed:', err); + } + return { devices: [], selected_device_id: null }; +} + +/** + * Build GPU device selector HTML + */ +function buildGpuDeviceSelector(gpuDevices) { + if (!gpuDevices || !gpuDevices.devices || gpuDevices.devices.length === 0) { + return ''; + } + + // Only show selector if there are multiple GPUs + if (gpuDevices.devices.length < 2) { + return ''; + } + + var html = + '
' + + '
' + + '' + + (t('codexlens.selectGpuDevice') || 'Select GPU Device') + + '
' + + '
'; + + gpuDevices.devices.forEach(function(device) { + var isSelected = device.is_selected; + var vendorIcon = device.vendor === 'nvidia' ? 'zap' : (device.vendor === 'amd' ? 'flame' : 'cpu'); + var vendorColor = device.vendor === 'nvidia' ? 'text-green-500' : (device.vendor === 'amd' ? 'text-red-500' : 'text-blue-500'); + var typeLabel = device.is_discrete ? (t('codexlens.discrete') || 'Discrete') : (t('codexlens.integrated') || 'Integrated'); + + html += + ''; + }); + + html += + '
' + + '' + + '
'; + + return html; +} + +/** + * Select a GPU device + */ +async function selectGpuDevice(deviceId) { + try { + showRefreshToast(t('codexlens.selectingGpu') || 'Selecting GPU...', 'info'); + + var response = await fetch('/api/codexlens/gpu/select', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ device_id: deviceId }) + }); + + var result = await response.json(); + if (result.success) { + showRefreshToast(t('codexlens.gpuSelected') || 'GPU selected', 'success'); + // Reload semantic status to reflect change + loadSemanticDepsStatus(); + } else { + showRefreshToast(result.error || 'Failed to select GPU', 'error'); + } + } catch (err) { + showRefreshToast(err.message, 'error'); + } +} + +/** + * Reset GPU device selection to auto + */ +async function resetGpuDevice() { + try { + showRefreshToast(t('codexlens.resettingGpu') || 'Resetting GPU selection...', 'info'); + + var response = await fetch('/api/codexlens/gpu/reset', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + var result = await response.json(); + if (result.success) { + showRefreshToast(t('codexlens.gpuReset') || 'GPU selection reset to auto', 'success'); + // Reload semantic status to reflect change + loadSemanticDepsStatus(); + } else { + showRefreshToast(result.error || 'Failed to reset GPU', 'error'); + } + } catch (err) { + showRefreshToast(err.message, 'error'); + } +} + /** * Install semantic dependencies with GPU mode */ @@ -570,9 +706,7 @@ async function installSemanticDeps() { function buildManualDownloadGuide() { var modelData = [ { profile: 'code', name: 'jinaai/jina-embeddings-v2-base-code', size: '~150 MB' }, - { profile: 'fast', name: 'BAAI/bge-small-en-v1.5', size: '~80 MB' }, - { profile: 'balanced', name: 'mixedbread-ai/mxbai-embed-large-v1', size: '~600 MB' }, - { profile: 'multilingual', name: 'intfloat/multilingual-e5-large', size: '~1 GB' } + { profile: 'fast', name: 'BAAI/bge-small-en-v1.5', size: '~80 MB' } ]; var html = @@ -807,9 +941,7 @@ async function downloadModel(profile) { // Get model info for size estimation var modelSizes = { 'fast': { size: 80, time: '1-2' }, - 'code': { size: 150, time: '2-5' }, - 'multilingual': { size: 1000, time: '5-15' }, - 'balanced': { size: 600, time: '3-10' } + 'code': { size: 150, time: '2-5' } }; var modelInfo = modelSizes[profile] || { size: 100, time: '2-5' }; @@ -933,9 +1065,7 @@ async function downloadModel(profile) { function showModelDownloadError(modelCard, profile, error, originalHTML) { var modelNames = { 'fast': 'BAAI/bge-small-en-v1.5', - 'code': 'jinaai/jina-embeddings-v2-base-code', - 'multilingual': 'intfloat/multilingual-e5-large', - 'balanced': 'mixedbread-ai/mxbai-embed-large-v1' + 'code': 'jinaai/jina-embeddings-v2-base-code' }; var modelName = modelNames[profile] || profile; @@ -1035,7 +1165,7 @@ async function deleteModel(profile) { /** * Initialize CodexLens index with bottom floating progress bar * @param {string} indexType - 'vector' (with embeddings), 'normal' (FTS only), or 'full' (FTS + Vector) - * @param {string} embeddingModel - Model profile: 'code', 'fast', 'multilingual', 'balanced' + * @param {string} embeddingModel - Model profile: 'code', 'fast' */ async function initCodexLensIndex(indexType, embeddingModel) { indexType = indexType || 'vector'; @@ -1104,7 +1234,7 @@ async function initCodexLensIndex(indexType, embeddingModel) { // Add model info for vector indexes var modelLabel = ''; if (indexType !== 'normal') { - var modelNames = { code: 'Code', fast: 'Fast', multilingual: 'Multi', balanced: 'Balanced' }; + var modelNames = { code: 'Code', fast: 'Fast' }; modelLabel = ' [' + (modelNames[embeddingModel] || embeddingModel) + ']'; } @@ -1148,7 +1278,7 @@ async function initCodexLensIndex(indexType, embeddingModel) { /** * Start the indexing process * @param {string} indexType - 'vector' or 'normal' - * @param {string} embeddingModel - Model profile: 'code', 'fast', 'multilingual', 'balanced' + * @param {string} embeddingModel - Model profile: 'code', 'fast' */ async function startCodexLensIndexing(indexType, embeddingModel) { indexType = indexType || 'vector'; @@ -1727,3 +1857,546 @@ async function cleanCodexLensIndexes() { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } + +// ============================================================ +// CODEXLENS MANAGER PAGE (Independent View) +// ============================================================ + +/** + * Render CodexLens Manager as an independent page view + */ +async function renderCodexLensManager() { + var container = document.getElementById('mainContent'); + if (!container) return; + + // Hide stats grid and search + var statsGrid = document.getElementById('statsGrid'); + var searchContainer = document.querySelector('.search-container'); + if (statsGrid) statsGrid.style.display = 'none'; + if (searchContainer) searchContainer.style.display = 'none'; + + container.innerHTML = '
' + t('common.loading') + '
'; + + try { + // Load CodexLens status first to populate window.cliToolsStatus.codexlens + if (typeof loadCodexLensStatus === 'function') { + await loadCodexLensStatus(); + } + + var response = await fetch('/api/codexlens/config'); + var config = await response.json(); + + container.innerHTML = buildCodexLensManagerPage(config); + if (window.lucide) lucide.createIcons(); + initCodexLensManagerPageEvents(config); + loadSemanticDepsStatus(); + loadModelList(); + // Load index stats for the Index Manager section + if (window.cliToolsStatus?.codexlens?.installed) { + loadIndexStatsForPage(); + } + } catch (err) { + container.innerHTML = '

' + t('common.error') + ': ' + err.message + '

'; + if (window.lucide) lucide.createIcons(); + } +} + +/** + * Build CodexLens Manager page content + */ +function buildCodexLensManagerPage(config) { + var indexDir = config.index_dir || '~/.codexlens/indexes'; + var indexCount = config.index_count || 0; + var isInstalled = window.cliToolsStatus?.codexlens?.installed || false; + + // Build model options for vector indexing + var modelOptions = buildModelSelectOptionsForPage(); + + return '
' + + // Header with status + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '

' + t('codexlens.config') + '

' + + '

' + t('codexlens.configDesc') + '

' + + '
' + + '
' + + '
' + + (isInstalled + ? ' ' + t('codexlens.installed') + '' + : ' ' + t('codexlens.notInstalled') + '') + + '
' + + '' + t('codexlens.indexes') + ':' + + '' + indexCount + '' + + '
' + + '
' + + '
' + + '
' + + + (isInstalled + ? // Installed: Show full management UI + '
' + + // Left Column + '
' + + // Create Index Section + '
' + + '

' + t('codexlens.createIndex') + '

' + + '
' + + // Model selector + '
' + + '' + + '' + + '

' + t('codexlens.modelHint') + '

' + + '
' + + // Index buttons - two modes: full (FTS + Vector) or FTS only + '
' + + '' + + '' + + '
' + + '

' + t('codexlens.indexTypeHint') + '

' + + '
' + + '
' + + // Storage Path Section + '
' + + '

' + t('codexlens.indexStoragePath') + '

' + + '
' + + '
' + + '' + + '
' + indexDir + '
' + + '
' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '

' + t('codexlens.pathInfo') + '

' + + '
' + + '
' + + '
' + + // Maintenance Section + '
' + + '

' + t('codexlens.maintenance') + '

' + + '
' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + // Right Column + '
' + + // Semantic Dependencies + '
' + + '

' + t('codexlens.semanticDeps') + '

' + + '
' + + '
' + + '
' + t('codexlens.checkingDeps') + + '
' + + '
' + + '
' + + // Model Management + '
' + + '

' + t('codexlens.modelManagement') + '

' + + '
' + + '
' + + '
' + t('codexlens.loadingModels') + + '
' + + '
' + + '
' + + '
' + + '
' + + // Index Manager Section + '
' + + '
' + + '
' + + '' + + '' + t('index.manager') + '' + + '-' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '' + + '' + indexDir + '' + + '
' + + '
' + + '
' + + '
-
' + + '
' + t('index.projects') + '
' + + '
' + + '
' + + '
-
' + + '
' + t('index.totalSize') + '
' + + '
' + + '
' + + '
-
' + + '
' + t('index.vectorIndexes') + '
' + + '
' + + '
' + + '
-
' + + '
' + t('index.ftsIndexes') + '
' + + '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + t('index.projectId') + '' + t('index.size') + '' + t('index.type') + '' + t('index.lastModified') + '
' + t('common.loading') + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + // Test Search Section + '
' + + '

' + t('codexlens.testSearch') + '

' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + : // Not installed: Show install prompt + '
' + + '
' + + '
' + + '' + + '
' + + '

' + t('codexlens.installCodexLens') + '

' + + '

' + t('codexlens.installFirst') + '

' + + '' + + '
' + + '
' + ) + + '
'; +} + +/** + * Build model select options for the page + */ +function buildModelSelectOptionsForPage() { + var installedModels = window.cliToolsStatus?.codexlens?.installedModels || []; + var allModels = window.cliToolsStatus?.codexlens?.allModels || []; + + if (allModels.length === 0) { + // Fallback to default models if not loaded + return '' + + ''; + } + + var options = ''; + allModels.forEach(function(model) { + var isInstalled = model.installed || installedModels.includes(model.profile); + var label = model.profile + (isInstalled ? ' ✓' : ''); + var selected = model.profile === 'code' ? ' selected' : ''; + options += ''; + }); + return options; +} + +/** + * Initialize index from page with selected model + */ +function initCodexLensIndexFromPage(indexType) { + var modelSelect = document.getElementById('pageModelSelect'); + var selectedModel = modelSelect ? modelSelect.value : 'code'; + + // For FTS-only index, model is not needed + if (indexType === 'normal') { + initCodexLensIndex(indexType); + } else { + initCodexLensIndex(indexType, selectedModel); + } +} + +/** + * Initialize CodexLens Manager page event handlers + */ +function initCodexLensManagerPageEvents(currentConfig) { + var saveBtn = document.getElementById('saveIndexPathBtn'); + if (saveBtn) { + saveBtn.onclick = async function() { + var indexDirInput = document.getElementById('indexDirInput'); + var newIndexDir = indexDirInput ? indexDirInput.value.trim() : ''; + if (!newIndexDir) { showRefreshToast(t('codexlens.pathEmpty'), 'error'); return; } + if (newIndexDir === currentConfig.index_dir) { showRefreshToast(t('codexlens.pathUnchanged'), 'info'); return; } + saveBtn.disabled = true; + saveBtn.innerHTML = '' + t('common.saving') + ''; + try { + var response = await fetch('/api/codexlens/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ index_dir: newIndexDir }) }); + var result = await response.json(); + if (result.success) { showRefreshToast(t('codexlens.configSaved'), 'success'); renderCodexLensManager(); } + else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); } + } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } + saveBtn.disabled = false; + saveBtn.innerHTML = ' ' + t('codexlens.saveConfig'); + if (window.lucide) lucide.createIcons(); + }; + } + + var runSearchBtn = document.getElementById('runSearchBtn'); + if (runSearchBtn) { + runSearchBtn.onclick = async function() { + var searchType = document.getElementById('searchTypeSelect').value; + var searchMode = document.getElementById('searchModeSelect').value; + var query = document.getElementById('searchQueryInput').value.trim(); + var resultsDiv = document.getElementById('searchResults'); + var resultCount = document.getElementById('searchResultCount'); + var resultContent = document.getElementById('searchResultContent'); + if (!query) { showRefreshToast(t('codexlens.enterQuery'), 'warning'); return; } + runSearchBtn.disabled = true; + runSearchBtn.innerHTML = '' + t('codexlens.searching') + ''; + resultsDiv.classList.add('hidden'); + try { + var endpoint = '/api/codexlens/' + searchType; + var params = new URLSearchParams({ query: query, limit: '20' }); + if (searchType === 'search' || searchType === 'search_files') { params.append('mode', searchMode); } + var response = await fetch(endpoint + '?' + params.toString()); + var result = await response.json(); + if (result.success) { + var results = result.results || result.files || []; + resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount'); + resultContent.textContent = JSON.stringify(results, null, 2); + resultsDiv.classList.remove('hidden'); + } else { + resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError')); + resultsDiv.classList.remove('hidden'); + } + } catch (err) { + resultContent.textContent = t('common.exception') + ': ' + err.message; + resultsDiv.classList.remove('hidden'); + } + runSearchBtn.disabled = false; + runSearchBtn.innerHTML = ' ' + t('codexlens.runSearch'); + if (window.lucide) lucide.createIcons(); + }; + } + + var searchInput = document.getElementById('searchQueryInput'); + if (searchInput) { searchInput.onkeypress = function(e) { if (e.key === 'Enter' && runSearchBtn) { runSearchBtn.click(); } }; } +} + +/** + * Show index initialization modal + */ +function showIndexInitModal() { + // Use initCodexLensIndex with default settings + initCodexLensIndex('vector', 'code'); +} + +/** + * Load index stats for the CodexLens Manager page + */ +async function loadIndexStatsForPage() { + try { + var response = await fetch('/api/codexlens/indexes'); + if (!response.ok) throw new Error('Failed to load index stats'); + var data = await response.json(); + renderIndexStatsForPage(data); + } catch (err) { + console.error('[CodexLens] Failed to load index stats:', err); + var tbody = document.getElementById('indexTableBody'); + if (tbody) { + tbody.innerHTML = '' + err.message + ''; + } + } +} + +/** + * Render index stats in the CodexLens Manager page + */ +function renderIndexStatsForPage(data) { + var summary = data.summary || {}; + var indexes = data.indexes || []; + var indexDir = data.indexDir || ''; + + // Update summary stats + var totalSizeEl = document.getElementById('indexTotalSize'); + var projectCountEl = document.getElementById('indexProjectCount'); + var totalSizeValEl = document.getElementById('indexTotalSizeVal'); + var vectorCountEl = document.getElementById('indexVectorCount'); + var ftsCountEl = document.getElementById('indexFtsCount'); + var indexDirEl = document.getElementById('indexDirDisplay'); + + if (totalSizeEl) totalSizeEl.textContent = summary.totalSizeFormatted || '0 B'; + if (projectCountEl) projectCountEl.textContent = summary.totalProjects || 0; + if (totalSizeValEl) totalSizeValEl.textContent = summary.totalSizeFormatted || '0 B'; + if (vectorCountEl) vectorCountEl.textContent = summary.vectorIndexCount || 0; + if (ftsCountEl) ftsCountEl.textContent = summary.normalIndexCount || 0; + if (indexDirEl && indexDir) { + indexDirEl.textContent = indexDir; + indexDirEl.title = indexDir; + } + + // Render table rows + var tbody = document.getElementById('indexTableBody'); + if (!tbody) return; + + if (indexes.length === 0) { + tbody.innerHTML = '' + (t('index.noIndexes') || 'No indexes yet') + ''; + return; + } + + var rows = ''; + indexes.forEach(function(idx) { + var vectorBadge = idx.hasVectorIndex + ? '' + (t('index.vector') || 'Vector') + '' + : ''; + var normalBadge = idx.hasNormalIndex + ? '' + (t('index.fts') || 'FTS') + '' + : ''; + + rows += '' + + '' + + '' + escapeHtml(idx.id) + '' + + '' + + '' + (idx.sizeFormatted || '-') + '' + + '
' + vectorBadge + normalBadge + '
' + + '' + formatTimeAgoSimple(idx.lastModified) + '' + + '' + + '' + + '' + + ''; + }); + + tbody.innerHTML = rows; + if (window.lucide) lucide.createIcons(); +} + +/** + * Simple time ago formatter + */ +function formatTimeAgoSimple(isoString) { + if (!isoString) return t('common.never') || 'Never'; + var date = new Date(isoString); + var now = new Date(); + var diffMs = now - date; + var diffMins = Math.floor(diffMs / 60000); + var diffHours = Math.floor(diffMins / 60); + var diffDays = Math.floor(diffHours / 24); + if (diffMins < 1) return t('common.justNow') || 'Just now'; + if (diffMins < 60) return diffMins + 'm ' + (t('common.ago') || 'ago'); + if (diffHours < 24) return diffHours + 'h ' + (t('common.ago') || 'ago'); + if (diffDays < 30) return diffDays + 'd ' + (t('common.ago') || 'ago'); + return date.toLocaleDateString(); +} + +/** + * Clean a specific project's index from the page + */ +async function cleanIndexProjectFromPage(projectId) { + if (!confirm((t('index.cleanProjectConfirm') || 'Clean index for') + ' ' + projectId + '?')) { + return; + } + + try { + showRefreshToast(t('index.cleaning') || 'Cleaning index...', 'info'); + + var response = await fetch('/api/codexlens/clean', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId: projectId }) + }); + + var result = await response.json(); + + if (result.success) { + showRefreshToast(t('index.cleanSuccess') || 'Index cleaned successfully', 'success'); + await loadIndexStatsForPage(); + } else { + showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error'); + } + } catch (err) { + showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); + } +} + +/** + * Clean all indexes from the page + */ +async function cleanAllIndexesFromPage() { + if (!confirm(t('index.cleanAllConfirm') || 'Are you sure you want to clean ALL indexes? This cannot be undone.')) { + return; + } + + try { + showRefreshToast(t('index.cleaning') || 'Cleaning indexes...', 'info'); + + var response = await fetch('/api/codexlens/clean', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ all: true }) + }); + + var result = await response.json(); + + if (result.success) { + showRefreshToast(t('index.cleanAllSuccess') || 'All indexes cleaned', 'success'); + await loadIndexStatsForPage(); + } else { + showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error'); + } + } catch (err) { + showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); + } +} diff --git a/ccw/src/templates/dashboard.html b/ccw/src/templates/dashboard.html index a2a0d6e9..ec8a41c9 100644 --- a/ccw/src/templates/dashboard.html +++ b/ccw/src/templates/dashboard.html @@ -331,6 +331,11 @@ History +