// CLI Status Component // Displays CLI tool availability status and allows setting default tool // ========== CLI State ========== let cliToolStatus = {}; // Dynamically populated from config 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 let cliToolsConfig = {}; // CLI tools enable/disable config let apiEndpoints = []; // API endpoints from LiteLLM config let cliSettingsEndpoints = []; // CLI Settings endpoints (for Claude wrapper) // Smart Context settings let smartContextEnabled = localStorage.getItem('ccw-smart-context') === 'true'; let smartContextMaxFiles = parseInt(localStorage.getItem('ccw-smart-context-max-files') || '10', 10); // Native Resume settings let nativeResumeEnabled = localStorage.getItem('ccw-native-resume') !== 'false'; // default true // Recursive Query settings (for hierarchical storage aggregation) let recursiveQueryEnabled = localStorage.getItem('ccw-recursive-query') !== 'false'; // default true // ========== Initialization ========== function initCliStatus() { // Load all statuses in one call using aggregated endpoint loadAllStatuses(); } // ========== Data Loading ========== /** * Load all statuses using aggregated endpoint (single API call) */ async function loadAllStatuses() { const totalStart = performance.now(); console.log('[PERF][Frontend] loadAllStatuses START'); // 1. 尝试从缓存获取(预加载的数据) if (window.cacheManager) { const cached = window.cacheManager.get('all-status'); if (cached) { console.log(`[PERF][Frontend] Cache hit: ${(performance.now() - totalStart).toFixed(1)}ms`); // 应用缓存数据 cliToolStatus = cached.cli || {}; codexLensStatus = cached.codexLens || { ready: false }; semanticStatus = cached.semantic || { available: false }; ccwInstallStatus = cached.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' }; // Load CLI tools config, API endpoints, and CLI Settings(这些有自己的缓存) const configStart = performance.now(); await Promise.all([ loadCliToolsConfig(), loadApiEndpoints(), loadCliSettingsEndpoints() ]); console.log(`[PERF][Frontend] Config/Endpoints load: ${(performance.now() - configStart).toFixed(1)}ms`); // Update badges updateCliBadge(); updateCodexLensBadge(); updateCcwInstallBadge(); console.log(`[PERF][Frontend] loadAllStatuses TOTAL (cached): ${(performance.now() - totalStart).toFixed(1)}ms`); return cached; } } // 2. 缓存未命中,从服务器获取 try { const fetchStart = performance.now(); console.log('[PERF][Frontend] Fetching /api/status/all...'); const response = await fetch('/api/status/all'); if (!response.ok) throw new Error('Failed to load status'); const data = await response.json(); console.log(`[PERF][Frontend] /api/status/all fetch: ${(performance.now() - fetchStart).toFixed(1)}ms`); // 存入缓存 if (window.cacheManager) { window.cacheManager.set('all-status', data, 300000); // 5分钟 } // Update all status data - merge with config tools to ensure all tools are tracked cliToolStatus = data.cli || {}; codexLensStatus = data.codexLens || { ready: false }; semanticStatus = data.semantic || { available: false }; ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' }; // Load CLI tools config, API endpoints, and CLI Settings const configStart = performance.now(); const [configResult, endpointsResult, settingsResult] = await Promise.all([ loadCliToolsConfig().then(r => { console.log(`[PERF][Frontend] loadCliToolsConfig: ${(performance.now() - configStart).toFixed(1)}ms`); return r; }), loadApiEndpoints().then(r => { console.log(`[PERF][Frontend] loadApiEndpoints: ${(performance.now() - configStart).toFixed(1)}ms`); return r; }), loadCliSettingsEndpoints().then(r => { console.log(`[PERF][Frontend] loadCliSettingsEndpoints: ${(performance.now() - configStart).toFixed(1)}ms`); return r; }) ]); // Update badges updateCliBadge(); updateCodexLensBadge(); updateCcwInstallBadge(); console.log(`[PERF][Frontend] loadAllStatuses TOTAL: ${(performance.now() - totalStart).toFixed(1)}ms`); return data; } catch (err) { console.error('Failed to load aggregated status:', err); console.log(`[PERF][Frontend] loadAllStatuses ERROR after: ${(performance.now() - totalStart).toFixed(1)}ms`); // Fallback to individual calls if aggregated endpoint fails return await loadAllStatusesFallback(); } } /** * Fallback: Load statuses individually if aggregated endpoint fails */ async function loadAllStatusesFallback() { console.warn('[CLI Status] Using fallback individual API calls'); await Promise.all([ loadCliToolsConfig(), // Ensure config is loaded (auto-creates if missing) loadCliToolStatus() // CodexLens status removed - managed in dedicated CodexLens Manager page ]); } /** * Legacy: Load CLI tool status individually * 优先从缓存读取,如果缓存有效则直接使用 */ async function loadCliToolStatus() { // 尝试从缓存获取 if (window.cacheManager) { const cached = window.cacheManager.get('cli-status'); if (cached) { cliToolStatus = cached; updateCliBadge(); console.log('[CLI Status] Loaded from cache'); return cached; } } try { const response = await fetch('/api/cli/status'); if (!response.ok) throw new Error('Failed to load CLI status'); const data = await response.json(); cliToolStatus = data; // 存入缓存 if (window.cacheManager) { window.cacheManager.set('cli-status', data, 300000); // 5分钟 } // Update badge updateCliBadge(); return data; } catch (err) { console.error('Failed to load CLI status:', err); return null; } } /** * Legacy: Load CodexLens status individually */ async function loadCodexLensStatus() { try { const response = await fetch('/api/codexlens/status'); if (!response.ok) throw new Error('Failed to load CodexLens status'); const data = await response.json(); codexLensStatus = data; // Expose to window for other modules (e.g., codexlens-manager.js) if (!window.cliToolsStatus) { window.cliToolsStatus = {}; } window.cliToolsStatus.codexlens = { installed: data.ready || false, version: data.version || null, installedModels: [] // Will be populated by loadSemanticStatus }; // Update CodexLens badge updateCodexLensBadge(); // If CodexLens is ready, also check semantic status and models if (data.ready) { await loadSemanticStatus(); await loadInstalledModels(); } return data; } catch (err) { console.error('Failed to load CodexLens status:', err); return null; } } /** * Load CodexLens dashboard data using aggregated endpoint (single API call) * This is optimized for the CodexLens Manager page initialization * 优先从缓存读取,如果缓存有效则直接使用 * @returns {Promise} Dashboard init data or null on error */ async function loadCodexLensDashboardInit() { // 尝试从缓存获取 if (window.cacheManager) { const cached = window.cacheManager.get('dashboard-init'); if (cached) { applyDashboardInitData(cached); console.log('[CLI Status] CodexLens dashboard init loaded from cache'); return cached; } } try { const response = await fetch('/api/codexlens/dashboard-init'); if (!response.ok) throw new Error('Failed to load CodexLens dashboard init'); const data = await response.json(); applyDashboardInitData(data); // 存入缓存 if (window.cacheManager) { window.cacheManager.set('dashboard-init', data, 300000); // 5分钟 } console.log('[CLI Status] CodexLens dashboard init loaded:', { installed: data.installed, version: data.status?.version, semanticAvailable: data.semantic?.available }); return data; } catch (err) { console.error('Failed to load CodexLens dashboard init:', err); // Fallback to individual calls return await loadCodexLensStatus(); } } /** * 应用 dashboard-init 数据到状态变量 * @param {object} data - dashboard init 响应数据 */ function applyDashboardInitData(data) { // Update status variables from aggregated response codexLensStatus = data.status || { ready: false }; semanticStatus = data.semantic || { available: false }; // Expose to window for other modules if (!window.cliToolsStatus) { window.cliToolsStatus = {}; } window.cliToolsStatus.codexlens = { installed: data.installed || false, version: data.status?.version || null, installedModels: [], config: data.config || {}, semantic: data.semantic || {} }; // Store config globally for easy access window.codexLensConfig = data.config || {}; window.codexLensStatusData = data.statusData || {}; // Update badges updateCodexLensBadge(); } /** * Legacy: Load semantic status individually */ async function loadSemanticStatus() { try { const response = await fetch('/api/codexlens/semantic/status'); if (!response.ok) throw new Error('Failed to load semantic status'); const data = await response.json(); semanticStatus = data; return data; } catch (err) { console.error('Failed to load semantic status:', err); return null; } } /** * Load installed embedding models */ async function loadInstalledModels() { try { const response = await fetch('/api/codexlens/models'); if (!response.ok) throw new Error('Failed to load models'); const data = await response.json(); if (data.success && data.result && data.result.models) { // Filter to only installed models const installedModels = data.result.models .filter(m => m.installed) .map(m => m.profile); // Update window.cliToolsStatus if (window.cliToolsStatus && window.cliToolsStatus.codexlens) { window.cliToolsStatus.codexlens.installedModels = installedModels; window.cliToolsStatus.codexlens.allModels = data.result.models; } console.log('[CLI Status] Installed models:', installedModels); return installedModels; } return []; } catch (err) { console.error('Failed to load installed models:', err); return []; } } /** * Load CLI tools config from .claude/cli-tools.json (project or global fallback) * 优先从缓存读取,如果缓存有效则直接使用 */ async function loadCliToolsConfig() { // 尝试从缓存获取 if (window.cacheManager) { const cached = window.cacheManager.get('cli-tools-config'); if (cached) { cliToolsConfig = cached.tools?.tools || {}; window.claudeCliToolsConfig = cached; if (cached.defaultTool) { defaultCliTool = cached.defaultTool; } console.log('[CLI Tools Config] Loaded from cache'); return cached; } } try { const response = await fetch('/api/cli/tools-config'); if (!response.ok) return null; const data = await response.json(); // Store full config and extract tools object (data.tools is full config, data.tools.tools is the actual tools) cliToolsConfig = data.tools?.tools || {}; window.claudeCliToolsConfig = data; // Full config available globally // Load default tool from config if (data.defaultTool) { defaultCliTool = data.defaultTool; } // 存入缓存 if (window.cacheManager) { window.cacheManager.set('cli-tools-config', data, 300000); // 5分钟 } console.log('[CLI Config] Loaded from:', data._configInfo?.source || 'unknown', '| Default:', data.defaultTool); return data; } catch (err) { console.error('Failed to load CLI tools config:', err); return null; } } /** * Update CLI tool enabled status */ async function updateCliToolEnabled(tool, enabled) { try { const response = await csrfFetch('/api/cli/tools-config/' + tool, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: enabled }) }); if (!response.ok) throw new Error('Failed to update'); showRefreshToast(tool + (enabled ? ' enabled' : ' disabled'), 'success'); return await response.json(); } catch (err) { console.error('Failed to update CLI tool:', err); showRefreshToast('Failed to update ' + tool, 'error'); return null; } } /** * Load API endpoints from LiteLLM config */ async function loadApiEndpoints() { try { const response = await fetch('/api/litellm-api/endpoints'); if (!response.ok) return []; const data = await response.json(); apiEndpoints = data.endpoints || []; return apiEndpoints; } catch (err) { console.error('Failed to load API endpoints:', err); return []; } } /** * Load CLI Settings endpoints (Claude wrapper configurations) */ async function loadCliSettingsEndpoints() { try { const response = await fetch('/api/cli/settings'); if (!response.ok) return []; const data = await response.json(); cliSettingsEndpoints = data.endpoints || []; return cliSettingsEndpoints; } catch (err) { console.error('Failed to load CLI settings endpoints:', err); return []; } } // ========== Badge Update ========== function updateCliBadge() { const badge = document.getElementById('badgeCliTools'); if (badge) { // Only count builtin and cli-wrapper tools (exclude api-endpoint tools) const cliTools = Object.keys(cliToolsConfig).filter(t => { if (!t || t === '_configInfo') return false; const config = cliToolsConfig[t]; // Include if: no type (legacy builtin), type is builtin, or type is cli-wrapper return !config?.type || config.type === 'builtin' || config.type === 'cli-wrapper'; }); // Count available and enabled CLI tools only let available = 0; cliTools.forEach(tool => { const status = cliToolStatus[tool] || {}; const config = cliToolsConfig[tool] || { enabled: true }; if (status.available && config.enabled !== false) { available++; } }); // CLI tools badge shows only CLI tools count const total = cliTools.length; badge.textContent = `${available}/${total}`; badge.classList.toggle('text-success', available === total && total > 0); badge.classList.toggle('text-warning', available > 0 && available < total); badge.classList.toggle('text-destructive', available === 0); } } function updateCodexLensBadge() { const badge = document.getElementById('badgeCodexLens'); if (badge) { badge.textContent = codexLensStatus.ready ? 'Ready' : 'Not Installed'; badge.classList.toggle('text-success', codexLensStatus.ready); badge.classList.toggle('text-muted-foreground', !codexLensStatus.ready); } } 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'); if (!container) return; const toolDescriptions = { gemini: 'Google AI for code analysis', qwen: 'Alibaba AI assistant', codex: 'OpenAI code generation', claude: 'Anthropic AI assistant', opencode: 'OpenCode multi-model API' }; const toolIcons = { gemini: 'sparkle', qwen: 'bot', codex: 'code-2', claude: 'brain', opencode: 'globe' // Default icon for new tools }; // Helper to get description for any tool (with fallback) const getToolDescription = (tool) => { return toolDescriptions[tool] || `${tool.charAt(0).toUpperCase() + tool.slice(1)} CLI tool`; }; // Helper to get icon for any tool (with fallback) const getToolIcon = (tool) => { return toolIcons[tool] || 'terminal'; }; // Get tools dynamically from config, merging with status for complete list // Only show builtin and cli-wrapper tools in the tools grid (api-endpoint tools show in API Endpoints section) const tools = [...new Set([ ...Object.keys(cliToolsConfig), ...Object.keys(cliToolStatus) ])].filter(t => { if (!t || t === '_configInfo') return false; const config = cliToolsConfig[t]; // Include if: no type (legacy builtin), type is builtin, or type is cli-wrapper return !config?.type || config.type === 'builtin' || config.type === 'cli-wrapper'; }); const toolsHtml = tools.map(tool => { const status = cliToolStatus[tool] || {}; const isAvailable = status.available; const isDefault = defaultCliTool === tool; const config = cliToolsConfig[tool] || { enabled: true }; const isEnabled = config.enabled !== false; const canSetDefault = isAvailable && isEnabled && !isDefault; // Special handling for Claude: show CLI Settings info const isClaude = tool === 'claude'; const enabledCliSettings = isClaude ? cliSettingsEndpoints.filter(ep => ep.enabled) : []; const hasCliSettings = enabledCliSettings.length > 0; // Build CLI Settings badge for Claude let cliSettingsBadge = ''; if (isClaude && hasCliSettings) { cliSettingsBadge = `${enabledCliSettings.length} Endpoint${enabledCliSettings.length > 1 ? 's' : ''}`; } // Build CLI Settings info for Claude let cliSettingsInfo = ''; if (isClaude) { if (hasCliSettings) { const epNames = enabledCliSettings.slice(0, 2).map(ep => ep.name).join(', '); const moreCount = enabledCliSettings.length > 2 ? ` +${enabledCliSettings.length - 2}` : ''; cliSettingsInfo = `
CLI Wrapper Endpoints:
${epNames}${moreCount}
Configure
`; } else { cliSettingsInfo = `
No CLI wrapper configured
Add Endpoint
`; } } return `
${tool.charAt(0).toUpperCase() + tool.slice(1)} ${isDefault ? 'Default' : ''} ${!isEnabled && isAvailable ? 'Disabled' : ''} ${cliSettingsBadge}
${getToolDescription(tool)}
${isAvailable ? (isEnabled ? ` Ready` : ` Disabled`) : ` Not Installed` }
${cliSettingsInfo}
${isAvailable ? (isEnabled ? `` : `` ) : ''} ${canSetDefault ? `` : '' }
`; }).join(''); // CodexLens and Semantic Search removed from CLI status panel // They are managed in the dedicated CodexLens Manager page // 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
` : ''; // API Endpoints section const apiEndpointsHtml = apiEndpoints.length > 0 ? `

API Endpoints

${apiEndpoints.length}
${apiEndpoints.map(ep => `
${ep.id}
${ep.model}
--tool custom --model ${ep.id}
`).join('')}
` : ''; // Config source info const configInfo = window.claudeCliToolsConfig?._configInfo || {}; const configSourceLabel = configInfo.source === 'project' ? 'Project' : configInfo.source === 'global' ? 'Global' : 'Default'; const configSourceClass = configInfo.source === 'project' ? 'text-success' : configInfo.source === 'global' ? 'text-primary' : 'text-muted-foreground'; // CLI Settings section const settingsHtml = `

Settings

${configSourceLabel}

Format for multi-turn conversation concatenation

SQLite

CLI history stored in SQLite with FTS search

Auto-analyze prompt and add relevant file paths

Use native tool resume (gemini -r, qwen --resume, codex resume, claude --resume)

Maximum files to include in smart context

Cache prefix/suffix injection mode for prompts

`; container.innerHTML = `

CLI Tools

${ccwInstallHtml}
${toolsHtml}
${apiEndpointsHtml} ${settingsHtml} `; // Initialize Lucide icons if (window.lucide) { lucide.createIcons(); } } // ========== Actions ========== function setDefaultCliTool(tool) { // Validate: tool must be available and enabled const status = cliToolStatus[tool] || {}; const config = cliToolsConfig[tool] || { enabled: true }; if (!status.available) { showRefreshToast(`Cannot set ${tool} as default: not installed`, 'error'); return; } if (config.enabled === false) { showRefreshToast(`Cannot set ${tool} as default: tool is disabled`, 'error'); return; } defaultCliTool = tool; // Save to config if (window.claudeCliToolsConfig) { window.claudeCliToolsConfig.defaultTool = tool; csrfFetch('/api/cli/tools-config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ defaultTool: tool }) }).catch(err => console.error('Failed to save default tool:', err)); } renderCliStatus(); showRefreshToast(`Default CLI tool set to ${tool}`, 'success'); } function setPromptFormat(format) { promptConcatFormat = format; localStorage.setItem('ccw-prompt-format', format); showRefreshToast(`Prompt format set to ${format.toUpperCase()}`, 'success'); } /** * Sync builtin tools availability with installed CLI tools * Checks system PATH and updates cli-tools.json accordingly */ async function syncBuiltinTools() { const syncButton = document.querySelector('[onclick="syncBuiltinTools()"]'); if (syncButton) { syncButton.disabled = true; const icon = syncButton.querySelector('i'); if (icon) icon.classList.add('spin'); } try { const response = await csrfFetch('/api/cli/settings/sync-tools', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error('Sync failed'); } const result = await response.json(); // Reload the config after sync await loadCliToolsConfig(); await loadAllStatuses(); renderCliStatus(); // Show summary of changes const { enabled, disabled, unchanged } = result.changes; let message = 'Tools synced: '; const parts = []; if (enabled.length > 0) parts.push(`${enabled.join(', ')} enabled`); if (disabled.length > 0) parts.push(`${disabled.join(', ')} disabled`); if (unchanged.length > 0) parts.push(`${unchanged.length} unchanged`); message += parts.join(', '); showRefreshToast(message, 'success'); // Also invalidate the CLI tool cache to ensure fresh checks if (window.cacheManager) { window.cacheManager.delete('cli-tools-status'); } } catch (err) { console.error('Failed to sync tools:', err); showRefreshToast('Failed to sync tools: ' + (err.message || String(err)), 'error'); } finally { if (syncButton) { syncButton.disabled = false; const icon = syncButton.querySelector('i'); if (icon) icon.classList.remove('spin'); } } } function setSmartContextEnabled(enabled) { smartContextEnabled = enabled; localStorage.setItem('ccw-smart-context', enabled.toString()); // Re-render the appropriate settings panel if (typeof renderCliSettingsSection === 'function') { renderCliSettingsSection(); } else { renderCliStatus(); } showRefreshToast(`Smart Context ${enabled ? 'enabled' : 'disabled'}`, 'success'); } function setSmartContextMaxFiles(max) { smartContextMaxFiles = parseInt(max, 10); localStorage.setItem('ccw-smart-context-max-files', max); showRefreshToast(`Smart Context max files set to ${max}`, 'success'); } function setNativeResumeEnabled(enabled) { nativeResumeEnabled = enabled; localStorage.setItem('ccw-native-resume', enabled.toString()); showRefreshToast(`Native Resume ${enabled ? 'enabled' : 'disabled'}`, 'success'); } function setRecursiveQueryEnabled(enabled) { recursiveQueryEnabled = enabled; localStorage.setItem('ccw-recursive-query', enabled.toString()); showRefreshToast(`Recursive Query ${enabled ? 'enabled' : 'disabled'}`, 'success'); } function getCacheInjectionMode() { if (window.claudeCliToolsConfig && window.claudeCliToolsConfig.settings) { return window.claudeCliToolsConfig.settings.cache?.injectionMode || 'auto'; } return localStorage.getItem('ccw-cache-injection-mode') || 'auto'; } async function setCacheInjectionMode(mode) { try { const response = await csrfFetch('/api/cli/tools-config/cache', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ injectionMode: mode }) }); if (response.ok) { localStorage.setItem('ccw-cache-injection-mode', mode); if (window.claudeCliToolsConfig) { window.claudeCliToolsConfig.settings.cache.injectionMode = mode; } showRefreshToast(`Cache injection mode set to ${mode}`, 'success'); } else { showRefreshToast('Failed to update cache settings', 'error'); } } catch (err) { console.error('Failed to update cache settings:', err); showRefreshToast('Failed to update cache settings', 'error'); } } async function refreshAllCliStatus() { await loadAllStatuses(); renderCliStatus(); } async function toggleCliTool(tool, enabled) { // If disabling the current default tool, switch to another available+enabled tool if (!enabled && defaultCliTool === tool) { // Get tools dynamically from config const tools = Object.keys(cliToolsConfig).filter(t => t && t !== '_configInfo'); const newDefault = tools.find(t => { if (t === tool) return false; const status = cliToolStatus[t] || {}; const config = cliToolsConfig[t] || { enabled: true }; return status.available && config.enabled !== false; }); if (newDefault) { defaultCliTool = newDefault; if (window.claudeCliToolsConfig) { window.claudeCliToolsConfig.defaultTool = newDefault; } showRefreshToast(`Default tool switched to ${newDefault}`, 'info'); } else { showRefreshToast(`Warning: No other enabled tool available for default`, 'warning'); } } await updateCliToolEnabled(tool, enabled); await loadAllStatuses(); renderCliStatus(); } function installCodexLens() { openCodexLensInstallWizard(); } function openCodexLensInstallWizard() { const modal = document.createElement('div'); modal.id = 'codexlensInstallModal'; modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; modal.innerHTML = `

Install CodexLens

Python-based code indexing engine

What will be installed:

  • Python virtual environment - Isolated Python environment
  • CodexLens package - Code indexing and search engine
  • SQLite FTS5 - Full-text search database

Installation Location

~/.codexlens/venv

First installation may take 2-3 minutes to download and setup Python packages.

`; document.body.appendChild(modal); if (window.lucide) { lucide.createIcons(); } } function closeCodexLensInstallWizard() { const modal = document.getElementById('codexlensInstallModal'); if (modal) { modal.remove(); } } async function startCodexLensInstall() { const progressDiv = document.getElementById('codexlensInstallProgress'); const installBtn = document.getElementById('codexlensInstallBtn'); const statusText = document.getElementById('codexlensInstallStatus'); const progressBar = document.getElementById('codexlensProgressBar'); // Show progress, disable button progressDiv.classList.remove('hidden'); installBtn.disabled = true; installBtn.innerHTML = 'Installing...'; // Simulate progress stages const stages = [ { progress: 10, text: 'Creating virtual environment...' }, { progress: 30, text: 'Installing pip packages...' }, { progress: 50, text: 'Installing CodexLens package...' }, { progress: 70, text: 'Setting up Python dependencies...' }, { progress: 90, text: 'Finalizing installation...' } ]; let currentStage = 0; const progressInterval = setInterval(() => { if (currentStage < stages.length) { statusText.textContent = stages[currentStage].text; progressBar.style.width = `${stages[currentStage].progress}%`; currentStage++; } }, 1500); try { const response = await csrfFetch('/api/codexlens/bootstrap', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); clearInterval(progressInterval); const result = await response.json(); if (result.success) { progressBar.style.width = '100%'; statusText.textContent = 'Installation complete!'; // 清理缓存以确保刷新后获取最新状态 if (window.cacheManager) { window.cacheManager.invalidate('all-status'); window.cacheManager.invalidate('dashboard-init'); } if (typeof window.invalidateCodexLensCache === 'function') { window.invalidateCodexLensCache(); } setTimeout(() => { closeCodexLensInstallWizard(); showRefreshToast('CodexLens installed successfully!', 'success'); loadCodexLensStatus().then(() => renderCliStatus()); }, 1000); } else { statusText.textContent = `Error: ${result.error}`; progressBar.classList.add('bg-destructive'); installBtn.disabled = false; installBtn.innerHTML = ' Retry'; if (window.lucide) lucide.createIcons(); } } catch (err) { clearInterval(progressInterval); statusText.textContent = `Error: ${err.message}`; progressBar.classList.add('bg-destructive'); installBtn.disabled = false; installBtn.innerHTML = ' Retry'; if (window.lucide) lucide.createIcons(); } } function uninstallCodexLens() { openCodexLensUninstallWizard(); } function openCodexLensUninstallWizard() { const modal = document.createElement('div'); modal.id = 'codexlensUninstallModal'; modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; modal.innerHTML = `

Uninstall CodexLens

Remove CodexLens and all data

What will be removed:

  • Virtual environment at ~/.codexlens/venv
  • All CodexLens indexed data and databases
  • Configuration and semantic search models

Warning

This action cannot be undone. All indexed code data will be permanently deleted.

`; document.body.appendChild(modal); if (window.lucide) { lucide.createIcons(); } } function closeCodexLensUninstallWizard() { const modal = document.getElementById('codexlensUninstallModal'); if (modal) { modal.remove(); } } async function startCodexLensUninstall() { const progressDiv = document.getElementById('codexlensUninstallProgress'); const uninstallBtn = document.getElementById('codexlensUninstallBtn'); const statusText = document.getElementById('codexlensUninstallStatus'); const progressBar = document.getElementById('codexlensUninstallProgressBar'); // Show progress, disable button progressDiv.classList.remove('hidden'); uninstallBtn.disabled = true; uninstallBtn.innerHTML = 'Uninstalling...'; // Simulate progress stages const stages = [ { progress: 25, text: 'Removing virtual environment...' }, { progress: 50, text: 'Deleting indexed data...' }, { progress: 75, text: 'Cleaning up configuration...' }, { progress: 90, text: 'Finalizing removal...' } ]; let currentStage = 0; const progressInterval = setInterval(() => { if (currentStage < stages.length) { statusText.textContent = stages[currentStage].text; progressBar.style.width = `${stages[currentStage].progress}%`; currentStage++; } }, 500); try { const response = await csrfFetch('/api/codexlens/uninstall', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); clearInterval(progressInterval); const result = await response.json(); if (result.success) { progressBar.style.width = '100%'; statusText.textContent = 'Uninstallation complete!'; // 清理缓存以确保刷新后获取最新状态 if (window.cacheManager) { window.cacheManager.invalidate('all-status'); window.cacheManager.invalidate('dashboard-init'); } if (typeof window.invalidateCodexLensCache === 'function') { window.invalidateCodexLensCache(); } setTimeout(() => { closeCodexLensUninstallWizard(); showRefreshToast('CodexLens uninstalled successfully!', 'success'); loadCodexLensStatus().then(() => renderCliStatus()); }, 1000); } else { statusText.textContent = `Error: ${result.error}`; progressBar.classList.remove('bg-destructive'); progressBar.classList.add('bg-destructive'); uninstallBtn.disabled = false; uninstallBtn.innerHTML = ' Retry'; if (window.lucide) lucide.createIcons(); } } catch (err) { clearInterval(progressInterval); statusText.textContent = `Error: ${err.message}`; progressBar.classList.remove('bg-destructive'); progressBar.classList.add('bg-destructive'); uninstallBtn.disabled = false; uninstallBtn.innerHTML = ' Retry'; if (window.lucide) lucide.createIcons(); } } async function initCodexLensIndex() { // Get current workspace path from multiple sources let targetPath = null; // Helper function to check if path is valid const isValidPath = (path) => { return path && typeof path === 'string' && path.length > 0 && (path.includes('/') || path.includes('\\')) && !path.startsWith('{{') && !path.endsWith('}}'); }; console.log('[CodexLens] Attempting to get project path...'); // Try 1: Global projectPath variable if (isValidPath(projectPath)) { targetPath = projectPath; console.log('[CodexLens] ✓ Using global projectPath:', targetPath); } // Try 2: Get from workflowData if (!targetPath && typeof workflowData !== 'undefined' && workflowData && isValidPath(workflowData.projectPath)) { targetPath = workflowData.projectPath; console.log('[CodexLens] ✓ Using workflowData.projectPath:', targetPath); } // Try 3: Get from current path display element if (!targetPath) { const currentPathEl = document.getElementById('currentPath'); if (currentPathEl && currentPathEl.textContent) { const pathText = currentPathEl.textContent.trim(); if (isValidPath(pathText)) { targetPath = pathText; console.log('[CodexLens] ✓ Using currentPath element text:', targetPath); } } } // Final validation if (!targetPath) { showRefreshToast('Error: No workspace loaded. Please open a workspace first.', 'error'); console.error('[CodexLens] No valid project path available'); console.error('[CodexLens] Attempted sources: projectPath:', projectPath, 'workflowData:', workflowData); return; } showRefreshToast('Initializing CodexLens index...', 'info'); console.log('[CodexLens] Initializing index for path:', targetPath); try { const response = await csrfFetch('/api/codexlens/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: targetPath }) }); const result = await response.json(); console.log('[CodexLens] Init result:', result); if (result.success) { let data = null; // Try to parse nested JSON in output field if (result.output && typeof result.output === 'string') { try { // Extract JSON from output (it may contain other text before the JSON) const jsonMatch = result.output.match(/\{[\s\S]*\}/); if (jsonMatch) { const parsed = JSON.parse(jsonMatch[0]); data = parsed.result || parsed; console.log('[CodexLens] Parsed from output:', data); } } catch (e) { console.warn('[CodexLens] Failed to parse output as JSON:', e); } } // Fallback to direct result field if (!data) { data = result.result?.result || result.result || result; } const files = data.files_indexed || 0; const dirs = data.dirs_indexed || 0; const symbols = data.symbols_indexed || 0; console.log('[CodexLens] Parsed data:', { files, dirs, symbols }); if (files === 0 && dirs === 0) { showRefreshToast(`Warning: No files indexed. Path: ${targetPath}`, 'warning'); console.warn('[CodexLens] No files indexed. Full data:', data); } else { showRefreshToast(`Index created: ${files} files, ${dirs} directories`, 'success'); console.log('[CodexLens] Index created successfully'); // Reload CodexLens status and refresh the view loadCodexLensStatus().then(() => renderCliStatus()); } } else { showRefreshToast(`Init failed: ${result.error}`, 'error'); console.error('[CodexLens] Init error:', result.error); } } catch (err) { showRefreshToast(`Init error: ${err.message}`, 'error'); console.error('[CodexLens] Exception:', err); } } // ========== Semantic Search Installation Wizard ========== function openSemanticInstallWizard() { const modal = document.createElement('div'); modal.id = 'semanticInstallModal'; modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; modal.innerHTML = `

Install Semantic Search

AI-powered code understanding

What will be installed:

  • sentence-transformers - ML framework
  • bge-small-en-v1.5 - Embedding model (~130MB)

Download Size

Total size: ~130MB. First-time model loading may take a few minutes.

`; document.body.appendChild(modal); // Initialize Lucide icons in modal if (window.lucide) { lucide.createIcons(); } } function closeSemanticInstallWizard() { const modal = document.getElementById('semanticInstallModal'); if (modal) { modal.remove(); } } async function startSemanticInstall() { const progressDiv = document.getElementById('semanticInstallProgress'); const installBtn = document.getElementById('semanticInstallBtn'); const statusText = document.getElementById('semanticInstallStatus'); const progressBar = document.getElementById('semanticProgressBar'); // Show progress, disable button progressDiv.classList.remove('hidden'); installBtn.disabled = true; installBtn.innerHTML = 'Installing...'; // Simulate progress stages const stages = [ { progress: 20, text: 'Installing sentence-transformers...' }, { progress: 50, text: 'Downloading embedding model...' }, { progress: 80, text: 'Setting up model cache...' }, { progress: 95, text: 'Finalizing installation...' } ]; let currentStage = 0; const progressInterval = setInterval(() => { if (currentStage < stages.length) { statusText.textContent = stages[currentStage].text; progressBar.style.width = `${stages[currentStage].progress}%`; currentStage++; } }, 2000); try { const response = await csrfFetch('/api/codexlens/semantic/install', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); clearInterval(progressInterval); const result = await response.json(); if (result.success) { progressBar.style.width = '100%'; statusText.textContent = 'Installation complete!'; setTimeout(() => { closeSemanticInstallWizard(); showRefreshToast('Semantic search installed successfully!', 'success'); loadSemanticStatus().then(() => renderCliStatus()); }, 1000); } else { statusText.textContent = `Error: ${result.error}`; progressBar.classList.add('bg-destructive'); installBtn.disabled = false; installBtn.innerHTML = ' Retry'; if (window.lucide) lucide.createIcons(); } } catch (err) { clearInterval(progressInterval); statusText.textContent = `Error: ${err.message}`; progressBar.classList.add('bg-destructive'); installBtn.disabled = false; installBtn.innerHTML = ' Retry'; if (window.lucide) lucide.createIcons(); } } // ========== Navigation ========== /** * Navigate to API Settings page with optional section * @param {string} section - Target section: 'cli-settings', 'providers', 'endpoints' */ function navigateToApiSettings(section) { // Try to switch to API Settings view if (typeof switchView === 'function') { switchView('api-settings'); } else if (window.switchView) { window.switchView('api-settings'); } // After view switch, select the target section setTimeout(() => { if (section === 'cli-settings') { // Click CLI Settings tab if exists const cliSettingsTab = document.querySelector('[data-section="cli-settings"]'); if (cliSettingsTab) { cliSettingsTab.click(); } else { // Fallback: try to find and click by text content const tabs = document.querySelectorAll('.api-settings-sidebar .sidebar-item, .api-settings-tabs .tab-btn'); tabs.forEach(tab => { if (tab.textContent.includes('CLI') || tab.textContent.includes('Wrapper')) { tab.click(); } }); } } }, 100); } // Export navigation function window.navigateToApiSettings = navigateToApiSettings;