Add comprehensive tests for schema cleanup migration and search comparison

- Implement tests for migration 005 to verify removal of deprecated fields in the database schema.
- Ensure that new databases are created with a clean schema.
- Validate that keywords are correctly extracted from the normalized file_keywords table.
- Test symbol insertion without deprecated fields and subdir operations without direct_files.
- Create a detailed search comparison test to evaluate vector search vs hybrid search performance.
- Add a script for reindexing projects to extract code relationships and verify GraphAnalyzer functionality.
- Include a test script to check TreeSitter parser availability and relationship extraction from sample files.
This commit is contained in:
catlog22
2025-12-16 19:27:05 +08:00
parent 3da0ef2adb
commit df23975a0b
61 changed files with 13114 additions and 366 deletions

View File

@@ -0,0 +1,375 @@
/* ==========================================
MCP MANAGER - ORANGE THEME ENHANCEMENTS
========================================== */
/* MCP CLI Mode Toggle - Orange for Codex */
.mcp-cli-toggle .cli-mode-btn {
position: relative;
overflow: hidden;
}
.mcp-cli-toggle .cli-mode-btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
transform: translateX(-100%);
transition: transform 0.6s;
}
.mcp-cli-toggle .cli-mode-btn:hover::before {
transform: translateX(100%);
}
/* CCW Tools Card - Enhanced Orange Gradient */
.ccw-tools-card {
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.ccw-tools-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(249, 115, 22, 0.1) 0%, transparent 70%);
opacity: 0;
transition: opacity 0.3s ease;
}
.ccw-tools-card:hover::before {
opacity: 1;
}
.ccw-tools-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(249, 115, 22, 0.2);
}
/* Orange-themed buttons and badges */
.bg-orange-500 {
background-color: #f97316;
}
.text-orange-500 {
color: #f97316;
}
.text-orange-600 {
color: #ea580c;
}
.text-orange-700 {
color: #c2410c;
}
.text-orange-800 {
color: #9a3412;
}
.bg-orange-50 {
background-color: #fff7ed;
}
.bg-orange-100 {
background-color: #ffedd5;
}
.border-orange-200 {
border-color: #fed7aa;
}
.border-orange-500\/20 {
border-color: rgba(249, 115, 22, 0.2);
}
.border-orange-500\/30 {
border-color: rgba(249, 115, 22, 0.3);
}
.border-orange-800 {
border-color: #9a3412;
}
/* Dark mode orange colors */
.dark .bg-orange-50 {
background-color: rgba(249, 115, 22, 0.05);
}
.dark .bg-orange-100 {
background-color: rgba(249, 115, 22, 0.1);
}
.dark .bg-orange-900\/30 {
background-color: rgba(124, 45, 18, 0.3);
}
.dark .text-orange-200 {
color: #fed7aa;
}
.dark .text-orange-300 {
color: #fdba74;
}
.dark .text-orange-400 {
color: #fb923c;
}
.dark .border-orange-800 {
border-color: #9a3412;
}
.dark .border-orange-950\/30 {
background-color: rgba(67, 20, 7, 0.3);
}
/* Codex MCP Server Cards - Orange Borders */
.mcp-server-card[data-cli-type="codex"] {
border-left: 3px solid #f97316;
transition: all 0.3s ease;
}
.mcp-server-card[data-cli-type="codex"]:hover {
border-left-width: 4px;
box-shadow: 0 4px 16px rgba(249, 115, 22, 0.15);
}
/* Toggle switches - Orange for Codex */
.mcp-toggle input:checked + div.peer-checked\:bg-orange-500 {
background: #f97316;
}
/* Installation buttons - Enhanced Orange */
.bg-orange-500:hover {
background-color: #ea580c;
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3);
}
/* Info panels - Orange accent */
.bg-orange-50.dark\:bg-orange-950\/30 {
border-left: 3px solid #f97316;
}
/* Codex section headers */
.text-orange-500 svg {
filter: drop-shadow(0 2px 4px rgba(249, 115, 22, 0.3));
}
/* Animated pulse for available/install states */
.border-orange-500\/30 {
animation: orangePulse 2s ease-in-out infinite;
}
@keyframes orangePulse {
0%, 100% {
border-color: rgba(249, 115, 22, 0.3);
box-shadow: 0 0 0 0 rgba(249, 115, 22, 0);
}
50% {
border-color: rgba(249, 115, 22, 0.6);
box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.1);
}
}
/* Server badges with orange accents */
.text-xs.px-2.py-0\.5.bg-orange-100 {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Codex server list enhancements */
.mcp-section h3.text-orange-500 {
background: linear-gradient(90deg, #f97316 0%, #ea580c 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
}
/* Install button hover effects */
.bg-orange-500.rounded-lg {
position: relative;
overflow: hidden;
}
.bg-orange-500.rounded-lg::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s;
}
.bg-orange-500.rounded-lg:active::after {
width: 200px;
height: 200px;
}
/* MCP Server Grid - Enhanced spacing for orange theme */
.mcp-server-grid {
gap: 1.25rem;
}
/* Available servers - Dashed border with orange hints */
.mcp-server-available {
border-style: dashed;
border-width: 2px;
border-color: hsl(var(--border));
transition: all 0.3s ease;
}
.mcp-server-available:hover {
border-style: solid;
border-color: #f97316;
transform: translateY(-2px);
}
/* Status indicators with orange */
.inline-flex.items-center.gap-1.bg-orange-500\/20 {
animation: availablePulse 2s ease-in-out infinite;
}
@keyframes availablePulse {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
/* Section dividers with orange accents */
.mcp-section {
border-bottom: 1px solid hsl(var(--border));
padding-bottom: 1.5rem;
margin-bottom: 2rem;
position: relative;
}
.mcp-section::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 60px;
height: 2px;
background: linear-gradient(90deg, #f97316 0%, transparent 100%);
}
/* Empty state icons with orange */
.mcp-empty-state i {
color: #f97316;
opacity: 0.3;
}
/* Enhanced focus states for orange buttons */
.bg-orange-500:focus-visible {
outline: 2px solid #f97316;
outline-offset: 2px;
}
/* Tooltip styles for orange theme */
[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 4px 8px;
background: #1f2937;
color: #fff;
font-size: 0.75rem;
white-space: nowrap;
border-radius: 4px;
pointer-events: none;
z-index: 1000;
}
/* Orange-themed success badges */
.bg-success-light .inline-flex.items-center.gap-1 {
background: linear-gradient(135deg, hsl(var(--success-light)) 0%, rgba(249, 115, 22, 0.1) 100%);
}
/* Config file status badges */
.inline-flex.items-center.gap-1\.5.bg-success\/10 {
border-left: 2px solid hsl(var(--success));
}
.inline-flex.items-center.gap-1\.5.bg-muted {
border-left: 2px solid #f97316;
}
/* Responsive adjustments for orange theme */
@media (max-width: 768px) {
.ccw-tools-card {
padding: 1rem;
}
.mcp-server-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
/* Loading states with orange */
@keyframes orangeGlow {
0%, 100% {
box-shadow: 0 0 10px rgba(249, 115, 22, 0.3);
}
50% {
box-shadow: 0 0 20px rgba(249, 115, 22, 0.6);
}
}
.loading-orange {
animation: orangeGlow 1.5s ease-in-out infinite;
}
/* Button group for install options */
.flex.gap-2 button.bg-primary,
.flex.gap-2 button.bg-success {
transition: all 0.2s ease;
}
.flex.gap-2 button.bg-primary:hover,
.flex.gap-2 button.bg-success:hover {
transform: scale(1.05);
}
/* Enhanced card shadows for depth */
.mcp-server-card {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.mcp-server-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Orange accent for project server headers */
.mcp-section .flex.items-center.gap-3 button {
position: relative;
overflow: hidden;
}
.mcp-section .flex.items-center.gap-3 button::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transform: translateX(-100%);
transition: transform 0.5s;
}
.mcp-section .flex.items-center.gap-3 button:hover::before {
transform: translateX(100%);
}

View File

@@ -15,6 +15,9 @@ let smartContextMaxFiles = parseInt(localStorage.getItem('ccw-smart-context-max-
// 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
// LLM Enhancement settings for Semantic Search
let llmEnhancementSettings = {
enabled: localStorage.getItem('ccw-llm-enhancement-enabled') === 'true',
@@ -26,12 +29,51 @@ let llmEnhancementSettings = {
// ========== Initialization ==========
function initCliStatus() {
// Load CLI status on init
loadCliToolStatus();
loadCodexLensStatus();
// Load all statuses in one call using aggregated endpoint
loadAllStatuses();
}
// ========== Data Loading ==========
/**
* Load all statuses using aggregated endpoint (single API call)
*/
async function loadAllStatuses() {
try {
const response = await fetch('/api/status/all');
if (!response.ok) throw new Error('Failed to load status');
const data = await response.json();
// Update all status data
cliToolStatus = data.cli || { gemini: {}, qwen: {}, codex: {}, claude: {} };
codexLensStatus = data.codexLens || { ready: false };
semanticStatus = data.semantic || { available: false };
// Update badges
updateCliBadge();
updateCodexLensBadge();
return data;
} catch (err) {
console.error('Failed to load aggregated status:', err);
// 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([
loadCliToolStatus(),
loadCodexLensStatus()
]);
}
/**
* Legacy: Load CLI tool status individually
*/
async function loadCliToolStatus() {
try {
const response = await fetch('/api/cli/status');
@@ -49,6 +91,9 @@ async function loadCliToolStatus() {
}
}
/**
* Legacy: Load CodexLens status individually
*/
async function loadCodexLensStatus() {
try {
const response = await fetch('/api/codexlens/status');
@@ -71,6 +116,9 @@ async function loadCodexLensStatus() {
}
}
/**
* Legacy: Load semantic status individually
*/
async function loadSemanticStatus() {
try {
const response = await fetch('/api/codexlens/semantic/status');
@@ -223,7 +271,7 @@ function renderCliStatus() {
<div class="flex items-center justify-between w-full mt-1">
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<i data-lucide="hard-drive" class="w-3 h-3"></i>
<span>~500MB</span>
<span>~130MB</span>
</div>
<button class="btn-sm btn-outline flex items-center gap-1" onclick="event.stopPropagation(); openSemanticSettingsModal()">
<i data-lucide="settings" class="w-3 h-3"></i>
@@ -377,8 +425,14 @@ function setNativeResumeEnabled(enabled) {
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');
}
async function refreshAllCliStatus() {
await Promise.all([loadCliToolStatus(), loadCodexLensStatus()]);
await loadAllStatuses();
renderCliStatus();
}
@@ -779,6 +833,9 @@ async function initCodexLensIndex() {
} 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');
@@ -820,19 +877,15 @@ function openSemanticInstallWizard() {
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
<span><strong>bge-small-en-v1.5</strong> - Embedding model (~130MB)</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
<span><strong>PyTorch</strong> - Deep learning backend (~300MB)</span>
</li>
</ul>
</div>
<div class="bg-warning/10 border border-warning/20 rounded-lg p-3">
<div class="bg-primary/10 border border-primary/20 rounded-lg p-3">
<div class="flex items-start gap-2">
<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>
<i data-lucide="info" class="w-4 h-4 text-primary mt-0.5"></i>
<div class="text-sm">
<p class="font-medium text-warning">Large Download</p>
<p class="text-muted-foreground">Total size: ~500MB. First-time model loading may take a few minutes.</p>
<p class="font-medium text-primary">Download Size</p>
<p class="text-muted-foreground">Total size: ~130MB. First-time model loading may take a few minutes.</p>
</div>
</div>
</div>
@@ -887,11 +940,10 @@ async function startSemanticInstall() {
// Simulate progress stages
const stages = [
{ progress: 10, text: 'Installing numpy...' },
{ progress: 30, text: 'Installing sentence-transformers...' },
{ progress: 50, text: 'Installing PyTorch dependencies...' },
{ progress: 70, text: 'Downloading embedding model...' },
{ progress: 90, text: 'Finalizing installation...' }
{ 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;

View File

@@ -235,6 +235,35 @@ async function loadHookConfig() {
}
}
async function loadAvailableSkills() {
try {
const response = await fetch('/api/skills?path=' + encodeURIComponent(projectPath));
if (!response.ok) throw new Error('Failed to load skills');
const data = await response.json();
// Combine project and user skills
const projectSkills = (data.projectSkills || []).map(s => ({
name: s.name,
path: s.path,
scope: 'project'
}));
const userSkills = (data.userSkills || []).map(s => ({
name: s.name,
path: s.path,
scope: 'user'
}));
// Store in window for access by wizard
window.availableSkills = [...projectSkills, ...userSkills];
return window.availableSkills;
} catch (err) {
console.error('Failed to load available skills:', err);
window.availableSkills = [];
return [];
}
}
/**
* Convert internal hook format to Claude Code format
* Internal: { command, args, matcher, timeout }
@@ -510,7 +539,7 @@ function getHookEventIconLucide(event) {
let currentWizardTemplate = null;
let wizardConfig = {};
function openHookWizardModal(wizardId) {
async function openHookWizardModal(wizardId) {
const wizard = WIZARD_TEMPLATES[wizardId];
if (!wizard) {
showRefreshToast('Wizard template not found', 'error');
@@ -530,6 +559,11 @@ function openHookWizardModal(wizardId) {
wizardConfig.selectedOptions = [];
}
// Ensure available skills are loaded for SKILL context wizard
if (wizardId === 'skill-context' && typeof window.availableSkills === 'undefined') {
await loadAvailableSkills();
}
const modal = document.getElementById('hookWizardModal');
if (modal) {
renderWizardModalContent();
@@ -792,9 +826,19 @@ function renderSkillContextConfig() {
const availableSkills = window.availableSkills || [];
if (selectedOption === 'auto') {
const skillBadges = availableSkills.map(function(s) {
return '<span class="px-1.5 py-0.5 bg-emerald-500/10 text-emerald-500 rounded text-xs">' + escapeHtml(s.name) + '</span>';
}).join(' ');
let skillBadges = '';
if (typeof window.availableSkills === 'undefined') {
// Still loading
skillBadges = '<span class="px-1.5 py-0.5 bg-muted text-muted-foreground rounded text-xs">' + t('common.loading') + '...</span>';
} else if (availableSkills.length === 0) {
// No skills found
skillBadges = '<span class="px-1.5 py-0.5 bg-warning/10 text-warning rounded text-xs">' + t('hook.wizard.noSkillsFound') + '</span>';
} else {
// Skills found
skillBadges = availableSkills.map(function(s) {
return '<span class="px-1.5 py-0.5 bg-emerald-500/10 text-emerald-500 rounded text-xs">' + escapeHtml(s.name) + '</span>';
}).join(' ');
}
return '<div class="bg-muted/30 rounded-lg p-4 text-sm text-muted-foreground">' +
'<div class="flex items-center gap-2 mb-2">' +
'<i data-lucide="info" class="w-4 h-4"></i>' +
@@ -814,10 +858,15 @@ function renderSkillContextConfig() {
'</div>';
} else {
configListHtml = skillConfigs.map(function(config, idx) {
var skillOptions = availableSkills.map(function(s) {
var selected = config.skill === s.id ? 'selected' : '';
return '<option value="' + s.id + '" ' + selected + '>' + escapeHtml(s.name) + '</option>';
}).join('');
var skillOptions = '';
if (availableSkills.length === 0) {
skillOptions = '<option value="" disabled>' + t('hook.wizard.noSkillsFound') + '</option>';
} else {
skillOptions = availableSkills.map(function(s) {
var selected = config.skill === s.name ? 'selected' : '';
return '<option value="' + escapeHtml(s.name) + '" ' + selected + '>' + escapeHtml(s.name) + '</option>';
}).join('');
}
return '<div class="border border-border rounded-lg p-3 bg-card">' +
'<div class="flex items-center justify-between mb-2">' +
'<select onchange="updateSkillConfig(' + idx + ', \'skill\', this.value)" ' +

View File

@@ -1113,6 +1113,10 @@ async function installCcwToolsMcpToCodex() {
await addCodexMcpServer('ccw-tools', ccwToolsConfig);
// Reload MCP configuration and refresh the view
await loadMcpConfig();
renderMcpManager();
const resultLabel = isUpdate ? 'updated in' : 'installed to';
showRefreshToast(`CCW Tools ${resultLabel} Codex (${selectedTools.length} tools)`, 'success');
} catch (err) {

View File

@@ -293,8 +293,9 @@ function showRefreshToast(message, type) {
toast.textContent = message;
document.body.appendChild(toast);
// Increase display time to 3.5 seconds for better visibility
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 300);
}, 2000);
}, 3500);
}

View File

@@ -233,6 +233,10 @@ const i18n = {
'codexlens.textSearch': 'Text Search',
'codexlens.fileSearch': 'File Search',
'codexlens.symbolSearch': 'Symbol Search',
'codexlens.exactMode': 'Exact',
'codexlens.fuzzyMode': 'Fuzzy (Trigram)',
'codexlens.hybridMode': 'Hybrid (RRF)',
'codexlens.vectorMode': 'Vector (Semantic)',
'codexlens.searchPlaceholder': 'Enter search query (e.g., function name, file path, code snippet)',
'codexlens.runSearch': 'Run Search',
'codexlens.results': 'Results',
@@ -250,6 +254,27 @@ const i18n = {
'codexlens.cleanFailed': 'Failed to clean indexes',
'codexlens.loadingConfig': 'Loading configuration...',
// Model Management
'codexlens.semanticDeps': 'Semantic Dependencies',
'codexlens.checkingDeps': 'Checking dependencies...',
'codexlens.semanticInstalled': 'Semantic dependencies installed',
'codexlens.semanticNotInstalled': 'Semantic dependencies not installed',
'codexlens.installDeps': 'Install Dependencies',
'codexlens.installingDeps': 'Installing dependencies...',
'codexlens.depsInstalled': 'Dependencies installed successfully',
'codexlens.depsInstallFailed': 'Failed to install dependencies',
'codexlens.modelManagement': 'Model Management',
'codexlens.loadingModels': 'Loading models...',
'codexlens.downloadModel': 'Download',
'codexlens.deleteModel': 'Delete',
'codexlens.downloading': 'Downloading...',
'codexlens.deleting': 'Deleting...',
'codexlens.modelDownloaded': 'Model downloaded',
'codexlens.modelDownloadFailed': 'Model download failed',
'codexlens.modelDeleted': 'Model deleted',
'codexlens.modelDeleteFailed': 'Model deletion failed',
'codexlens.deleteModelConfirm': 'Are you sure you want to delete model',
// Semantic Search Configuration
'semantic.settings': 'Semantic Search Settings',
'semantic.configDesc': 'Configure LLM enhancement for semantic indexing',
@@ -291,6 +316,8 @@ const i18n = {
'cli.smartContextDesc': 'Auto-analyze prompt and add relevant file paths',
'cli.nativeResume': 'Native Resume',
'cli.nativeResumeDesc': 'Use native tool resume (gemini -r, qwen --resume, codex resume)',
'cli.recursiveQuery': 'Recursive Query',
'cli.recursiveQueryDesc': 'Aggregate CLI history and memory data from parent and child projects',
'cli.maxContextFiles': 'Max Context Files',
'cli.maxContextFilesDesc': 'Maximum files to include in smart context',
@@ -459,6 +486,48 @@ const i18n = {
'mcp.claudeJsonDesc': 'Save in root .claude.json projects section (shared config)',
'mcp.mcpJsonDesc': 'Save in project .mcp.json file (recommended for version control)',
// New MCP Manager UI
'mcp.title': 'MCP Server Management',
'mcp.subtitle': 'Manage MCP servers for Claude, Codex, and project-level configurations',
'mcp.createNew': 'Create New',
'mcp.createFirst': 'Create Your First Server',
'mcp.noServers': 'No MCP Servers Configured',
'mcp.noServersDesc': 'Get started by creating a new MCP server or installing from templates',
'mcp.totalServers': 'Total Servers',
'mcp.enabled': 'Enabled',
'mcp.viewServer': 'View Server',
'mcp.editServer': 'Edit Server',
'mcp.createServer': 'Create Server',
'mcp.updateServer': 'Update Server',
'mcp.close': 'Close',
'mcp.cancel': 'Cancel',
'mcp.update': 'Update',
'mcp.install': 'Install',
'mcp.save': 'Save',
'mcp.delete': 'Delete',
'mcp.optional': 'Optional',
'mcp.description': 'Description',
'mcp.category': 'Category',
'mcp.installTo': 'Install To',
'mcp.cwd': 'Working Directory',
'mcp.httpHeaders': 'HTTP Headers',
'mcp.error': 'Error',
'mcp.success': 'Success',
'mcp.nameRequired': 'Server name is required',
'mcp.commandRequired': 'Command is required',
'mcp.urlRequired': 'URL is required',
'mcp.invalidArgsJson': 'Invalid JSON format for arguments',
'mcp.invalidEnvJson': 'Invalid JSON format for environment variables',
'mcp.invalidHeadersJson': 'Invalid JSON format for HTTP headers',
'mcp.serverInstalled': 'Server installed successfully',
'mcp.serverEnabled': 'Server enabled successfully',
'mcp.serverDisabled': 'Server disabled successfully',
'mcp.serverDeleted': 'Server deleted successfully',
'mcp.backToManager': 'Back to Manager',
'mcp.noTemplates': 'No Templates Available',
'mcp.noTemplatesDesc': 'Create templates from existing servers or add new ones',
'mcp.templatesDesc': 'Browse and install pre-configured MCP server templates',
// MCP Templates
'mcp.templates': 'MCP Templates',
'mcp.savedTemplates': 'saved templates',
@@ -500,6 +569,7 @@ const i18n = {
'mcp.codex.removeConfirm': 'Remove Codex MCP server "{name}"?',
'mcp.codex.copyToClaude': 'Copy to Claude',
'mcp.codex.copyToCodex': 'Copy to Codex',
'mcp.codex.install': 'Install to Codex',
'mcp.codex.copyFromClaude': 'Copy Claude Servers to Codex',
'mcp.codex.alreadyAdded': 'Already in Codex',
'mcp.codex.scopeCodex': 'Codex - Global (~/.codex/config.toml)',
@@ -510,6 +580,7 @@ const i18n = {
'mcp.claude.copyFromCodex': 'Copy Codex Servers to Claude',
'mcp.claude.alreadyAdded': 'Already in Claude',
'mcp.claude.copyToClaude': 'Copy to Claude Global',
'mcp.claude.copyToCodex': 'Copy to Codex',
// MCP Edit Modal
'mcp.editModal.title': 'Edit MCP Server',
@@ -1292,6 +1363,10 @@ const i18n = {
'codexlens.textSearch': '文本搜索',
'codexlens.fileSearch': '文件搜索',
'codexlens.symbolSearch': '符号搜索',
'codexlens.exactMode': '精确模式',
'codexlens.fuzzyMode': '模糊模式 (Trigram)',
'codexlens.hybridMode': '混合模式 (RRF)',
'codexlens.vectorMode': '向量模式 (语义搜索)',
'codexlens.searchPlaceholder': '输入搜索查询(例如:函数名、文件路径、代码片段)',
'codexlens.runSearch': '运行搜索',
'codexlens.results': '结果',
@@ -1309,6 +1384,27 @@ const i18n = {
'codexlens.cleanFailed': '清理索引失败',
'codexlens.loadingConfig': '加载配置中...',
// 模型管理
'codexlens.semanticDeps': '语义搜索依赖',
'codexlens.checkingDeps': '检查依赖中...',
'codexlens.semanticInstalled': '语义搜索依赖已安装',
'codexlens.semanticNotInstalled': '语义搜索依赖未安装',
'codexlens.installDeps': '安装依赖',
'codexlens.installingDeps': '安装依赖中...',
'codexlens.depsInstalled': '依赖安装成功',
'codexlens.depsInstallFailed': '依赖安装失败',
'codexlens.modelManagement': '模型管理',
'codexlens.loadingModels': '加载模型中...',
'codexlens.downloadModel': '下载',
'codexlens.deleteModel': '删除',
'codexlens.downloading': '下载中...',
'codexlens.deleting': '删除中...',
'codexlens.modelDownloaded': '模型已下载',
'codexlens.modelDownloadFailed': '模型下载失败',
'codexlens.modelDeleted': '模型已删除',
'codexlens.modelDeleteFailed': '模型删除失败',
'codexlens.deleteModelConfirm': '确定要删除模型',
// Semantic Search 配置
'semantic.settings': '语义搜索设置',
'semantic.configDesc': '配置语义索引的 LLM 增强功能',
@@ -1350,6 +1446,8 @@ const i18n = {
'cli.smartContextDesc': '自动分析提示词并添加相关文件路径',
'cli.nativeResume': '原生恢复',
'cli.nativeResumeDesc': '使用工具原生恢复命令 (gemini -r, qwen --resume, codex resume)',
'cli.recursiveQuery': '递归查询',
'cli.recursiveQueryDesc': '聚合显示父项目和子项目的 CLI 历史与内存数据',
'cli.maxContextFiles': '最大上下文文件数',
'cli.maxContextFilesDesc': '智能上下文包含的最大文件数',
@@ -1515,6 +1613,48 @@ const i18n = {
'mcp.claudeJsonDesc': '保存在根目录 .claude.json projects 字段下(共享配置)',
'mcp.mcpJsonDesc': '保存在项目 .mcp.json 文件中(推荐用于版本控制)',
// New MCP Manager UI
'mcp.title': 'MCP 服务器管理',
'mcp.subtitle': '管理 Claude、Codex 和项目级别的 MCP 服务器配置',
'mcp.createNew': '创建新服务器',
'mcp.createFirst': '创建第一个服务器',
'mcp.noServers': '未配置 MCP 服务器',
'mcp.noServersDesc': '开始创建新的 MCP 服务器或从模板安装',
'mcp.totalServers': '总服务器数',
'mcp.enabled': '已启用',
'mcp.viewServer': '查看服务器',
'mcp.editServer': '编辑服务器',
'mcp.createServer': '创建服务器',
'mcp.updateServer': '更新服务器',
'mcp.close': '关闭',
'mcp.cancel': '取消',
'mcp.update': '更新',
'mcp.install': '安装',
'mcp.save': '保存',
'mcp.delete': '删除',
'mcp.optional': '可选',
'mcp.description': '描述',
'mcp.category': '分类',
'mcp.installTo': '安装到',
'mcp.cwd': '工作目录',
'mcp.httpHeaders': 'HTTP 头',
'mcp.error': '错误',
'mcp.success': '成功',
'mcp.nameRequired': '服务器名称为必填项',
'mcp.commandRequired': '命令为必填项',
'mcp.urlRequired': 'URL 为必填项',
'mcp.invalidArgsJson': '参数 JSON 格式无效',
'mcp.invalidEnvJson': '环境变量 JSON 格式无效',
'mcp.invalidHeadersJson': 'HTTP 头 JSON 格式无效',
'mcp.serverInstalled': '服务器安装成功',
'mcp.serverEnabled': '服务器启用成功',
'mcp.serverDisabled': '服务器禁用成功',
'mcp.serverDeleted': '服务器删除成功',
'mcp.backToManager': '返回管理器',
'mcp.noTemplates': '无可用模板',
'mcp.noTemplatesDesc': '从现有服务器创建模板或添加新模板',
'mcp.templatesDesc': '浏览并安装预配置的 MCP 服务器模板',
// MCP CLI Mode
'mcp.cliMode': 'CLI 模式',
'mcp.claudeMode': 'Claude 模式',
@@ -1537,6 +1677,7 @@ const i18n = {
'mcp.codex.removeConfirm': '移除 Codex MCP 服务器 "{name}"',
'mcp.codex.copyToClaude': '复制到 Claude',
'mcp.codex.copyToCodex': '复制到 Codex',
'mcp.codex.install': '安装到 Codex',
'mcp.codex.copyFromClaude': '从 Claude 复制服务器到 Codex',
'mcp.codex.alreadyAdded': '已在 Codex 中',
'mcp.codex.scopeCodex': 'Codex - 全局 (~/.codex/config.toml)',
@@ -1547,6 +1688,7 @@ const i18n = {
'mcp.claude.copyFromCodex': '从 Codex 复制服务器到 Claude',
'mcp.claude.alreadyAdded': '已在 Claude 中',
'mcp.claude.copyToClaude': '复制到 Claude 全局',
'mcp.claude.copyToCodex': '复制到 Codex',
// MCP Edit Modal
'mcp.editModal.title': '编辑 MCP 服务器',

View File

@@ -567,6 +567,19 @@ function renderCliSettingsSection() {
'</div>' +
'<p class="cli-setting-desc">' + t('cli.nativeResumeDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="git-branch" class="w-3 h-3"></i>' +
t('cli.recursiveQuery') +
'</label>' +
'<div class="cli-setting-control">' +
'<label class="cli-toggle">' +
'<input type="checkbox"' + (recursiveQueryEnabled ? ' checked' : '') + ' onchange="setRecursiveQueryEnabled(this.checked)">' +
'<span class="cli-toggle-slider"></span>' +
'</label>' +
'</div>' +
'<p class="cli-setting-desc">' + t('cli.recursiveQueryDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item' + (!smartContextEnabled ? ' disabled' : '') + '">' +
'<label class="cli-setting-label">' +
'<i data-lucide="files" class="w-3 h-3"></i>' +
@@ -1614,6 +1627,26 @@ function buildCodexLensConfigContent(config) {
'</div>' +
'</div>' +
// Semantic Dependencies Section
(isInstalled
? '<div class="tool-config-section">' +
'<h4>' + t('codexlens.semanticDeps') + '</h4>' +
'<div id="semanticDepsStatus" class="space-y-2">' +
'<div class="text-sm text-muted-foreground">' + t('codexlens.checkingDeps') + '</div>' +
'</div>' +
'</div>'
: '') +
// Model Management Section
(isInstalled
? '<div class="tool-config-section">' +
'<h4>' + t('codexlens.modelManagement') + '</h4>' +
'<div id="modelListContainer" class="space-y-2">' +
'<div class="text-sm text-muted-foreground">' + t('codexlens.loadingModels') + '</div>' +
'</div>' +
'</div>'
: '') +
// Test Search Section
(isInstalled
? '<div class="tool-config-section">' +
@@ -1625,6 +1658,12 @@ function buildCodexLensConfigContent(config) {
'<option value="search_files">' + t('codexlens.fileSearch') + '</option>' +
'<option value="symbol">' + t('codexlens.symbolSearch') + '</option>' +
'</select>' +
'<select id="searchModeSelect" class="tool-config-select flex-1">' +
'<option value="exact">' + t('codexlens.exactMode') + '</option>' +
'<option value="fuzzy">' + t('codexlens.fuzzyMode') + '</option>' +
'<option value="hybrid">' + t('codexlens.hybridMode') + '</option>' +
'<option value="vector">' + t('codexlens.vectorMode') + '</option>' +
'</select>' +
'</div>' +
'<div>' +
'<input type="text" id="searchQueryInput" class="tool-config-input w-full" ' +
@@ -1717,6 +1756,7 @@ function initCodexLensConfigEvents(currentConfig) {
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');
@@ -1734,6 +1774,10 @@ function initCodexLensConfigEvents(currentConfig) {
try {
var endpoint = '/api/codexlens/' + searchType;
var params = new URLSearchParams({ query: query, limit: '20' });
// Add mode parameter for search and search_files (not for symbol search)
if (searchType === 'search' || searchType === 'search_files') {
params.append('mode', searchMode);
}
var response = await fetch(endpoint + '?' + params.toString());
var result = await response.json();
@@ -1766,6 +1810,211 @@ function initCodexLensConfigEvents(currentConfig) {
}
};
}
// Load semantic dependencies status
loadSemanticDepsStatus();
// Load model list
loadModelList();
}
// Load semantic dependencies status
async function loadSemanticDepsStatus() {
var container = document.getElementById('semanticDepsStatus');
if (!container) return;
try {
var response = await fetch('/api/codexlens/semantic/status');
var result = await response.json();
if (result.available) {
container.innerHTML =
'<div class="flex items-center gap-2 text-sm">' +
'<i data-lucide="check-circle" class="w-4 h-4 text-success"></i>' +
'<span>' + t('codexlens.semanticInstalled') + '</span>' +
'<span class="text-muted-foreground">(' + (result.backend || 'fastembed') + ')</span>' +
'</div>';
} else {
container.innerHTML =
'<div class="space-y-2">' +
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
'<i data-lucide="alert-circle" class="w-4 h-4"></i>' +
'<span>' + t('codexlens.semanticNotInstalled') + '</span>' +
'</div>' +
'<button class="btn-sm btn-outline" onclick="installSemanticDeps()">' +
'<i data-lucide="download" class="w-3 h-3"></i> ' + t('codexlens.installDeps') +
'</button>' +
'</div>';
}
if (window.lucide) lucide.createIcons();
} catch (err) {
container.innerHTML =
'<div class="text-sm text-error">' + t('common.error') + ': ' + err.message + '</div>';
}
}
// Install semantic dependencies
async function installSemanticDeps() {
var container = document.getElementById('semanticDepsStatus');
if (!container) return;
container.innerHTML =
'<div class="text-sm text-muted-foreground animate-pulse">' + t('codexlens.installingDeps') + '</div>';
try {
var response = await fetch('/api/codexlens/semantic/install', { method: 'POST' });
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.depsInstalled'), 'success');
await loadSemanticDepsStatus();
await loadModelList();
} else {
showRefreshToast(t('codexlens.depsInstallFailed') + ': ' + result.error, 'error');
await loadSemanticDepsStatus();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
await loadSemanticDepsStatus();
}
}
// Load model list
async function loadModelList() {
var container = document.getElementById('modelListContainer');
if (!container) return;
try {
var response = await fetch('/api/codexlens/models');
var result = await response.json();
if (!result.success || !result.result || !result.result.models) {
container.innerHTML =
'<div class="text-sm text-muted-foreground">' + t('codexlens.semanticNotInstalled') + '</div>';
return;
}
var models = result.result.models;
var html = '<div class="space-y-2">';
models.forEach(function(model) {
var statusIcon = model.installed
? '<i data-lucide="check-circle" class="w-4 h-4 text-success"></i>'
: '<i data-lucide="circle" class="w-4 h-4 text-muted"></i>';
var sizeText = model.installed
? model.actual_size_mb.toFixed(1) + ' MB'
: '~' + model.estimated_size_mb + ' MB';
var actionBtn = model.installed
? '<button class="btn-sm btn-outline btn-danger" onclick="deleteModel(\'' + model.profile + '\')">' +
'<i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('codexlens.deleteModel') +
'</button>'
: '<button class="btn-sm btn-outline" onclick="downloadModel(\'' + model.profile + '\')">' +
'<i data-lucide="download" class="w-3 h-3"></i> ' + t('codexlens.downloadModel') +
'</button>';
html +=
'<div class="border rounded-lg p-3 space-y-2" id="model-' + model.profile + '">' +
'<div class="flex items-start justify-between">' +
'<div class="flex-1">' +
'<div class="flex items-center gap-2 mb-1">' +
statusIcon +
'<span class="font-medium">' + model.profile + '</span>' +
'<span class="text-xs text-muted-foreground">(' + model.dimensions + ' dims)</span>' +
'</div>' +
'<div class="text-xs text-muted-foreground mb-1">' + model.model_name + '</div>' +
'<div class="text-xs text-muted-foreground">' + model.use_case + '</div>' +
'</div>' +
'<div class="text-right">' +
'<div class="text-xs text-muted-foreground mb-2">' + sizeText + '</div>' +
actionBtn +
'</div>' +
'</div>' +
'</div>';
});
html += '</div>';
container.innerHTML = html;
if (window.lucide) lucide.createIcons();
} catch (err) {
container.innerHTML =
'<div class="text-sm text-error">' + t('common.error') + ': ' + err.message + '</div>';
}
}
// Download model
async function downloadModel(profile) {
var modelCard = document.getElementById('model-' + profile);
if (!modelCard) return;
var originalHTML = modelCard.innerHTML;
modelCard.innerHTML =
'<div class="flex items-center justify-center p-3">' +
'<span class="text-sm text-muted-foreground animate-pulse">' + t('codexlens.downloading') + '</span>' +
'</div>';
try {
var response = await fetch('/api/codexlens/models/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile: profile })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.modelDownloaded') + ': ' + profile, 'success');
await loadModelList();
} else {
showRefreshToast(t('codexlens.modelDownloadFailed') + ': ' + result.error, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
}
// Delete model
async function deleteModel(profile) {
if (!confirm(t('codexlens.deleteModelConfirm') + ' ' + profile + '?')) {
return;
}
var modelCard = document.getElementById('model-' + profile);
if (!modelCard) return;
var originalHTML = modelCard.innerHTML;
modelCard.innerHTML =
'<div class="flex items-center justify-center p-3">' +
'<span class="text-sm text-muted-foreground animate-pulse">' + t('codexlens.deleting') + '</span>' +
'</div>';
try {
var response = await fetch('/api/codexlens/models/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile: profile })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.modelDeleted') + ': ' + profile, 'success');
await loadModelList();
} else {
showRefreshToast(t('codexlens.modelDeleteFailed') + ': ' + result.error, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
}
async function cleanCodexLensIndexes() {

View File

@@ -0,0 +1,596 @@
// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies
// Extracted from cli-manager.js for better maintainability
// ============================================================
// CODEXLENS CONFIGURATION MODAL
// ============================================================
/**
* Show CodexLens configuration modal
*/
async function showCodexLensConfigModal() {
try {
showRefreshToast(t('codexlens.loadingConfig'), 'info');
// Fetch current config
const response = await fetch('/api/codexlens/config');
const config = await response.json();
const modalHtml = buildCodexLensConfigContent(config);
// Create and show modal
const modalContainer = document.createElement('div');
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
// Initialize icons
if (window.lucide) lucide.createIcons();
// Initialize event handlers
initCodexLensConfigEvents(config);
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
/**
* Build CodexLens configuration modal content
*/
function buildCodexLensConfigContent(config) {
const indexDir = config.index_dir || '~/.codexlens/indexes';
const indexCount = config.index_count || 0;
const isInstalled = window.cliToolsStatus?.codexlens?.installed || false;
return '<div class="modal-backdrop" id="codexlensConfigModal">' +
'<div class="modal-container">' +
'<div class="modal-header">' +
'<div class="flex items-center gap-3">' +
'<div class="modal-icon">' +
'<i data-lucide="database" class="w-5 h-5"></i>' +
'</div>' +
'<div>' +
'<h2 class="text-lg font-bold">' + t('codexlens.config') + '</h2>' +
'<p class="text-xs text-muted-foreground">' + t('codexlens.whereIndexesStored') + '</p>' +
'</div>' +
'</div>' +
'<button onclick="closeModal()" class="text-muted-foreground hover:text-foreground">' +
'<i data-lucide="x" class="w-5 h-5"></i>' +
'</button>' +
'</div>' +
'<div class="modal-body">' +
// Status Section
'<div class="tool-config-section">' +
'<h4>' + t('codexlens.status') + '</h4>' +
'<div class="flex items-center gap-4 text-sm">' +
'<div class="flex items-center gap-2">' +
'<span class="text-muted-foreground">' + t('codexlens.currentWorkspace') + ':</span>' +
'<span class="font-medium">' + (isInstalled ? t('codexlens.installed') : t('codexlens.notInstalled')) + '</span>' +
'</div>' +
'<div class="flex items-center gap-2">' +
'<span class="text-muted-foreground">' + t('codexlens.indexes') + ':</span>' +
'<span class="font-medium">' + indexCount + '</span>' +
'</div>' +
'</div>' +
'</div>' +
// Index Storage Path Section
'<div class="tool-config-section">' +
'<h4>' + t('codexlens.indexStoragePath') + '</h4>' +
'<div class="space-y-3">' +
'<div>' +
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.currentPath') + '</label>' +
'<div class="text-sm text-muted-foreground bg-muted/30 rounded-lg px-3 py-2 font-mono">' +
indexDir +
'</div>' +
'</div>' +
'<div>' +
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.newStoragePath') + '</label>' +
'<input type="text" id="indexDirInput" value="' + indexDir + '" ' +
'placeholder="' + t('codexlens.pathPlaceholder') + '" ' +
'class="tool-config-input w-full" />' +
'<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.pathInfo') + '</p>' +
'</div>' +
'<div class="flex items-start gap-2 bg-warning/10 border border-warning/30 rounded-lg p-3">' +
'<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>' +
'<div class="text-sm">' +
'<p class="font-medium text-warning">' + t('codexlens.migrationRequired') + '</p>' +
'<p class="text-muted-foreground mt-1">' + t('codexlens.migrationWarning') + '</p>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
// Actions Section
'<div class="tool-config-section">' +
'<h4>' + t('codexlens.actions') + '</h4>' +
'<div class="tool-config-actions">' +
(isInstalled
? '<button class="btn-sm btn-outline" onclick="initCodexLensIndex()">' +
'<i data-lucide="database" class="w-3 h-3"></i> ' + t('codexlens.initializeIndex') +
'</button>' +
'<button class="btn-sm btn-outline" onclick="cleanCodexLensIndexes()">' +
'<i data-lucide="trash" class="w-3 h-3"></i> ' + t('codexlens.cleanAllIndexes') +
'</button>' +
'<button class="btn-sm btn-outline btn-danger" onclick="uninstallCodexLens()">' +
'<i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('cli.uninstall') +
'</button>'
: '<button class="btn-sm btn-primary" onclick="installCodexLens()">' +
'<i data-lucide="download" class="w-3 h-3"></i> ' + t('codexlens.installCodexLens') +
'</button>') +
'</div>' +
'</div>' +
// Semantic Dependencies Section
(isInstalled
? '<div class="tool-config-section">' +
'<h4>' + t('codexlens.semanticDeps') + '</h4>' +
'<div id="semanticDepsStatus" class="space-y-2">' +
'<div class="text-sm text-muted-foreground">' + t('codexlens.checkingDeps') + '</div>' +
'</div>' +
'</div>'
: '') +
// Model Management Section
(isInstalled
? '<div class="tool-config-section">' +
'<h4>' + t('codexlens.modelManagement') + '</h4>' +
'<div id="modelListContainer" class="space-y-2">' +
'<div class="text-sm text-muted-foreground">' + t('codexlens.loadingModels') + '</div>' +
'</div>' +
'</div>'
: '') +
// Test Search Section
(isInstalled
? '<div class="tool-config-section">' +
'<h4>' + t('codexlens.testSearch') + ' <span class="text-muted">(' + t('codexlens.testFunctionality') + ')</span></h4>' +
'<div class="space-y-3">' +
'<div class="flex gap-2">' +
'<select id="searchTypeSelect" class="tool-config-select flex-1">' +
'<option value="search">' + t('codexlens.textSearch') + '</option>' +
'<option value="search_files">' + t('codexlens.fileSearch') + '</option>' +
'<option value="symbol">' + t('codexlens.symbolSearch') + '</option>' +
'</select>' +
'<select id="searchModeSelect" class="tool-config-select flex-1">' +
'<option value="exact">' + t('codexlens.exactMode') + '</option>' +
'<option value="fuzzy">' + t('codexlens.fuzzyMode') + '</option>' +
'<option value="hybrid">' + t('codexlens.hybridMode') + '</option>' +
'<option value="vector">' + t('codexlens.vectorMode') + '</option>' +
'</select>' +
'</div>' +
'<div>' +
'<input type="text" id="searchQueryInput" class="tool-config-input w-full" ' +
'placeholder="' + t('codexlens.searchPlaceholder') + '" />' +
'</div>' +
'<div>' +
'<button class="btn-sm btn-primary w-full" id="runSearchBtn">' +
'<i data-lucide="search" class="w-3 h-3"></i> ' + t('codexlens.runSearch') +
'</button>' +
'</div>' +
'<div id="searchResults" class="hidden">' +
'<div class="bg-muted/30 rounded-lg p-3 max-h-64 overflow-y-auto">' +
'<div class="flex items-center justify-between mb-2">' +
'<p class="text-sm font-medium">' + t('codexlens.results') + ':</p>' +
'<span id="searchResultCount" class="text-xs text-muted-foreground"></span>' +
'</div>' +
'<pre id="searchResultContent" class="text-xs font-mono whitespace-pre-wrap break-all"></pre>' +
'</div>' +
'</div>' +
'</div>' +
'</div>'
: '') +
'</div>' +
// Footer
'<div class="tool-config-footer">' +
'<button class="btn btn-outline" onclick="closeModal()">' + t('common.cancel') + '</button>' +
'<button class="btn btn-primary" id="saveCodexLensConfigBtn">' +
'<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig') +
'</button>' +
'</div>' +
'</div>';
}
/**
* Initialize CodexLens config modal event handlers
*/
function initCodexLensConfigEvents(currentConfig) {
// Save button
var saveBtn = document.getElementById('saveCodexLensConfigBtn');
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) {
closeModal();
return;
}
saveBtn.disabled = true;
saveBtn.innerHTML = '<span class="animate-pulse">' + t('common.saving') + '</span>';
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');
closeModal();
// Refresh CodexLens status
if (typeof loadCodexLensStatus === 'function') {
await loadCodexLensStatus();
renderToolsSection();
if (window.lucide) lucide.createIcons();
}
} else {
showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = '<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig');
if (window.lucide) lucide.createIcons();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = '<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig');
if (window.lucide) lucide.createIcons();
}
};
}
// Test Search Button
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 = '<span class="animate-pulse">' + t('codexlens.searching') + '</span>';
resultsDiv.classList.add('hidden');
try {
var endpoint = '/api/codexlens/' + searchType;
var params = new URLSearchParams({ query: query, limit: '20' });
// Add mode parameter for search and search_files (not for symbol search)
if (searchType === 'search' || searchType === 'search_files') {
params.append('mode', searchMode);
}
var response = await fetch(endpoint + '?' + params.toString());
var result = await response.json();
console.log('[CodexLens Test] Search result:', result);
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');
showRefreshToast(t('codexlens.searchCompleted') + ': ' + results.length + ' ' + t('codexlens.resultsCount'), 'success');
} else {
resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError'));
resultsDiv.classList.remove('hidden');
showRefreshToast(t('codexlens.searchFailed') + ': ' + result.error, 'error');
}
runSearchBtn.disabled = false;
runSearchBtn.innerHTML = '<i data-lucide="search" class="w-3 h-3"></i> ' + t('codexlens.runSearch');
if (window.lucide) lucide.createIcons();
} catch (err) {
console.error('[CodexLens Test] Error:', err);
resultContent.textContent = t('common.exception') + ': ' + err.message;
resultsDiv.classList.remove('hidden');
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
runSearchBtn.disabled = false;
runSearchBtn.innerHTML = '<i data-lucide="search" class="w-3 h-3"></i> ' + t('codexlens.runSearch');
if (window.lucide) lucide.createIcons();
}
};
}
// Load semantic dependencies status
loadSemanticDepsStatus();
// Load model list
loadModelList();
}
// ============================================================
// SEMANTIC DEPENDENCIES MANAGEMENT
// ============================================================
/**
* Load semantic dependencies status
*/
async function loadSemanticDepsStatus() {
var container = document.getElementById('semanticDepsStatus');
if (!container) return;
try {
var response = await fetch('/api/codexlens/semantic/status');
var result = await response.json();
if (result.available) {
container.innerHTML =
'<div class="flex items-center gap-2 text-sm">' +
'<i data-lucide="check-circle" class="w-4 h-4 text-success"></i>' +
'<span>' + t('codexlens.semanticInstalled') + '</span>' +
'<span class="text-muted-foreground">(' + (result.backend || 'fastembed') + ')</span>' +
'</div>';
} else {
container.innerHTML =
'<div class="space-y-2">' +
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
'<i data-lucide="alert-circle" class="w-4 h-4"></i>' +
'<span>' + t('codexlens.semanticNotInstalled') + '</span>' +
'</div>' +
'<button class="btn-sm btn-outline" onclick="installSemanticDeps()">' +
'<i data-lucide="download" class="w-3 h-3"></i> ' + t('codexlens.installDeps') +
'</button>' +
'</div>';
}
if (window.lucide) lucide.createIcons();
} catch (err) {
container.innerHTML =
'<div class="text-sm text-error">' + t('common.error') + ': ' + err.message + '</div>';
}
}
/**
* Install semantic dependencies
*/
async function installSemanticDeps() {
var container = document.getElementById('semanticDepsStatus');
if (!container) return;
container.innerHTML =
'<div class="text-sm text-muted-foreground animate-pulse">' + t('codexlens.installingDeps') + '</div>';
try {
var response = await fetch('/api/codexlens/semantic/install', { method: 'POST' });
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.depsInstalled'), 'success');
await loadSemanticDepsStatus();
await loadModelList();
} else {
showRefreshToast(t('codexlens.depsInstallFailed') + ': ' + result.error, 'error');
await loadSemanticDepsStatus();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
await loadSemanticDepsStatus();
}
}
// ============================================================
// MODEL MANAGEMENT
// ============================================================
/**
* Load model list
*/
async function loadModelList() {
var container = document.getElementById('modelListContainer');
if (!container) return;
try {
var response = await fetch('/api/codexlens/models');
var result = await response.json();
if (!result.success || !result.result || !result.result.models) {
container.innerHTML =
'<div class="text-sm text-muted-foreground">' + t('codexlens.semanticNotInstalled') + '</div>';
return;
}
var models = result.result.models;
var html = '<div class="space-y-2">';
models.forEach(function(model) {
var statusIcon = model.installed
? '<i data-lucide="check-circle" class="w-4 h-4 text-success"></i>'
: '<i data-lucide="circle" class="w-4 h-4 text-muted"></i>';
var sizeText = model.installed
? model.actual_size_mb.toFixed(1) + ' MB'
: '~' + model.estimated_size_mb + ' MB';
var actionBtn = model.installed
? '<button class="btn-sm btn-outline btn-danger" onclick="deleteModel(\'' + model.profile + '\')">' +
'<i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('codexlens.deleteModel') +
'</button>'
: '<button class="btn-sm btn-outline" onclick="downloadModel(\'' + model.profile + '\')">' +
'<i data-lucide="download" class="w-3 h-3"></i> ' + t('codexlens.downloadModel') +
'</button>';
html +=
'<div class="border rounded-lg p-3 space-y-2" id="model-' + model.profile + '">' +
'<div class="flex items-start justify-between">' +
'<div class="flex-1">' +
'<div class="flex items-center gap-2 mb-1">' +
statusIcon +
'<span class="font-medium">' + model.profile + '</span>' +
'<span class="text-xs text-muted-foreground">(' + model.dimensions + ' dims)</span>' +
'</div>' +
'<div class="text-xs text-muted-foreground mb-1">' + model.model_name + '</div>' +
'<div class="text-xs text-muted-foreground">' + model.use_case + '</div>' +
'</div>' +
'<div class="text-right">' +
'<div class="text-xs text-muted-foreground mb-2">' + sizeText + '</div>' +
actionBtn +
'</div>' +
'</div>' +
'</div>';
});
html += '</div>';
container.innerHTML = html;
if (window.lucide) lucide.createIcons();
} catch (err) {
container.innerHTML =
'<div class="text-sm text-error">' + t('common.error') + ': ' + err.message + '</div>';
}
}
/**
* Download model
*/
async function downloadModel(profile) {
var modelCard = document.getElementById('model-' + profile);
if (!modelCard) return;
var originalHTML = modelCard.innerHTML;
modelCard.innerHTML =
'<div class="flex items-center justify-center p-3">' +
'<span class="text-sm text-muted-foreground animate-pulse">' + t('codexlens.downloading') + '</span>' +
'</div>';
try {
var response = await fetch('/api/codexlens/models/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile: profile })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.modelDownloaded') + ': ' + profile, 'success');
await loadModelList();
} else {
showRefreshToast(t('codexlens.modelDownloadFailed') + ': ' + result.error, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
}
/**
* Delete model
*/
async function deleteModel(profile) {
if (!confirm(t('codexlens.deleteModelConfirm') + ' ' + profile + '?')) {
return;
}
var modelCard = document.getElementById('model-' + profile);
if (!modelCard) return;
var originalHTML = modelCard.innerHTML;
modelCard.innerHTML =
'<div class="flex items-center justify-center p-3">' +
'<span class="text-sm text-muted-foreground animate-pulse">' + t('codexlens.deleting') + '</span>' +
'</div>';
try {
var response = await fetch('/api/codexlens/models/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile: profile })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.modelDeleted') + ': ' + profile, 'success');
await loadModelList();
} else {
showRefreshToast(t('codexlens.modelDeleteFailed') + ': ' + result.error, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
}
// ============================================================
// CODEXLENS ACTIONS
// ============================================================
/**
* Initialize CodexLens index
*/
function initCodexLensIndex() {
openCliInstallWizard('codexlens');
}
/**
* Install CodexLens
*/
function installCodexLens() {
openCliInstallWizard('codexlens');
}
/**
* Uninstall CodexLens
*/
function uninstallCodexLens() {
openCliUninstallWizard('codexlens');
}
/**
* Clean all CodexLens indexes
*/
async function cleanCodexLensIndexes() {
if (!confirm(t('codexlens.cleanConfirm'))) {
return;
}
try {
showRefreshToast(t('codexlens.cleaning'), '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('codexlens.cleanSuccess'), 'success');
// Refresh status
if (typeof loadCodexLensStatus === 'function') {
await loadCodexLensStatus();
renderToolsSection();
if (window.lucide) lucide.createIcons();
}
} else {
showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error');
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}

View File

@@ -11,8 +11,11 @@ async function renderHookManager() {
if (statsGrid) statsGrid.style.display = 'none';
if (searchInput) searchInput.parentElement.style.display = 'none';
// Always reload hook config to get latest data
await loadHookConfig();
// Always reload hook config and available skills to get latest data
await Promise.all([
loadHookConfig(),
loadAvailableSkills()
]);
const globalHooks = hookConfig.global?.hooks || {};
const projectHooks = hookConfig.project?.hooks || {};

View File

@@ -139,6 +139,27 @@ async function renderMcpManager() {
const codexConfigExists = codexMcpConfig?.exists || false;
const codexConfigPath = codexMcpConfig?.configPath || '~/.codex/config.toml';
// Collect cross-CLI servers (servers from other CLI not yet in current CLI)
const crossCliServers = [];
if (currentCliMode === 'claude') {
// In Claude mode, show Codex servers that aren't in Claude
for (const [name, config] of Object.entries(codexMcpServers || {})) {
const existsInClaude = currentProjectServerNames.includes(name) || globalServerNames.includes(name);
if (!existsInClaude) {
crossCliServers.push({ name, config, fromCli: 'codex' });
}
}
} else {
// In Codex mode, show Claude servers that aren't in Codex
const allClaudeServers = { ...mcpUserServers, ...projectServers };
for (const [name, config] of Object.entries(allClaudeServers)) {
const existsInCodex = codexMcpServers && codexMcpServers[name];
if (!existsInCodex) {
crossCliServers.push({ name, config, fromCli: 'claude' });
}
}
}
container.innerHTML = `
<div class="mcp-manager">
<!-- CLI Mode Toggle -->
@@ -321,7 +342,7 @@ async function renderMcpManager() {
` : ''}
<!-- Available MCP Servers from Other Projects (Codex mode) -->
<div class="mcp-section">
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-foreground">${t('mcp.availableOther')}</h3>
<span class="text-sm text-muted-foreground">${otherProjectServers.length} ${t('mcp.serversAvailable')}</span>
@@ -339,14 +360,30 @@ async function renderMcpManager() {
</div>
`}
</div>
<!-- Cross-CLI Servers: Available from Claude (Codex mode) -->
${crossCliServers.length > 0 ? `
<div class="mcp-section">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-foreground flex items-center gap-2">
<i data-lucide="circle" class="w-5 h-5 text-blue-500"></i>
${t('mcp.codex.copyFromClaude')}
</h3>
<span class="text-sm text-muted-foreground">${crossCliServers.length} ${t('mcp.serversAvailable')}</span>
</div>
<div class="mcp-server-grid grid gap-3">
${crossCliServers.map(server => renderCrossCliServerCard(server, false)).join('')}
</div>
</div>
` : ''}
` : `
<!-- CCW Tools MCP Server Card -->
<div class="mcp-section mb-6">
<div class="ccw-tools-card bg-gradient-to-br from-primary/10 to-primary/5 border-2 ${isCcwToolsInstalled ? 'border-success' : 'border-primary/30'} rounded-lg p-6 hover:shadow-lg transition-all">
<div class="ccw-tools-card bg-gradient-to-br from-orange-500/10 to-orange-500/5 border-2 ${isCcwToolsInstalled ? 'border-success' : 'border-orange-500/30'} rounded-lg p-6 hover:shadow-lg transition-all">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-4 flex-1">
<div class="shrink-0 w-12 h-12 bg-primary rounded-lg flex items-center justify-center">
<i data-lucide="wrench" class="w-6 h-6 text-primary-foreground"></i>
<div class="shrink-0 w-12 h-12 bg-orange-500 rounded-lg flex items-center justify-center">
<i data-lucide="wrench" class="w-6 h-6 text-white"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
@@ -357,7 +394,7 @@ async function renderMcpManager() {
${enabledTools.length} tools
</span>
` : `
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-primary/20 text-primary">
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-500/20 text-orange-600 dark:text-orange-400">
<i data-lucide="package" class="w-3 h-3"></i>
Available
</span>
@@ -375,15 +412,15 @@ async function renderMcpManager() {
`).join('')}
</div>
<div class="flex items-center gap-3 text-xs">
<button class="text-primary hover:underline" onclick="selectCcwTools('core')">Core only</button>
<button class="text-primary hover:underline" onclick="selectCcwTools('all')">All</button>
<button class="text-orange-500 hover:underline" onclick="selectCcwTools('core')">Core only</button>
<button class="text-orange-500 hover:underline" onclick="selectCcwTools('all')">All</button>
<button class="text-muted-foreground hover:underline" onclick="selectCcwTools('none')">None</button>
</div>
</div>
</div>
<div class="shrink-0 flex gap-2">
${isCcwToolsInstalled ? `
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
<button class="px-4 py-2 text-sm bg-orange-500 text-white rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="updateCcwToolsMcp('workspace')"
title="${t('mcp.updateInWorkspace')}">
<i data-lucide="folder" class="w-4 h-4"></i>
@@ -396,7 +433,7 @@ async function renderMcpManager() {
${t('mcp.updateInGlobal')}
</button>
` : `
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
<button class="px-4 py-2 text-sm bg-orange-500 text-white rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="installCcwToolsMcp('workspace')"
title="${t('mcp.installToWorkspace')}">
<i data-lucide="folder" class="w-4 h-4"></i>
@@ -485,7 +522,7 @@ async function renderMcpManager() {
</div>
<!-- Available MCP Servers from Other Projects -->
<div class="mcp-section">
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-foreground">${t('mcp.availableOther')}</h3>
<span class="text-sm text-muted-foreground">${otherProjectServers.length} ${t('mcp.serversAvailable')}</span>
@@ -504,6 +541,22 @@ async function renderMcpManager() {
`}
</div>
<!-- Cross-CLI Servers: Available from Codex (Claude mode) -->
${crossCliServers.length > 0 ? `
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-foreground flex items-center gap-2">
<i data-lucide="circle-dashed" class="w-5 h-5 text-orange-500"></i>
${t('mcp.claude.copyFromCodex')}
</h3>
<span class="text-sm text-muted-foreground">${crossCliServers.length} ${t('mcp.serversAvailable')}</span>
</div>
<div class="mcp-server-grid grid gap-3">
${crossCliServers.map(server => renderCrossCliServerCard(server, true)).join('')}
</div>
</div>
` : ''}
<!-- MCP Templates Section -->
${mcpTemplates.length > 0 ? `
<div class="mcp-section mt-6">
@@ -1010,6 +1063,15 @@ function renderAvailableServerCardForCodex(serverName, serverInfo) {
${sourceProjectName ? `<span class="text-xs text-muted-foreground/70">• ${t('mcp.from')} ${escapeHtml(sourceProjectName)}</span>` : ''}
</div>
</div>
<div class="mt-3 pt-3 border-t border-border flex items-center gap-2">
<button class="text-xs text-orange-500 hover:text-orange-600 transition-colors flex items-center gap-1"
onclick="copyClaudeServerToCodex('${escapeHtml(originalName)}', ${JSON.stringify(serverConfig).replace(/'/g, "&#39;")})"
title="${t('mcp.codex.copyToCodex')}">
<i data-lucide="download" class="w-3 h-3"></i>
${t('mcp.codex.install')}
</button>
</div>
</div>
`;
}
@@ -1098,6 +1160,104 @@ function renderCodexServerCard(serverName, serverConfig) {
`;
}
// Render card for cross-CLI servers (servers from other CLI not in current CLI)
function renderCrossCliServerCard(server, isClaude) {
const { name, config, fromCli } = server;
const isStdio = !!config.command;
const isHttp = !!config.url;
const command = config.command || config.url || 'N/A';
const args = config.args || [];
// Icon and color based on source CLI
const icon = fromCli === 'codex' ? 'circle-dashed' : 'circle';
const iconColor = fromCli === 'codex' ? 'orange' : 'blue';
const sourceBadgeColor = fromCli === 'codex' ? 'orange' : 'primary';
const targetCli = isClaude ? 'project' : 'codex';
const buttonText = isClaude ? t('mcp.codex.copyToClaude') : t('mcp.claude.copyToCodex');
const typeBadge = isHttp
? `<span class="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">HTTP</span>`
: `<span class="text-xs px-2 py-0.5 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 rounded-full">STDIO</span>`;
return `
<div class="mcp-server-card bg-card border border-dashed border-${iconColor}-200 dark:border-${iconColor}-800 rounded-lg p-4 hover:shadow-md hover:border-solid transition-all">
<div class="flex items-start justify-between mb-3">
<div class="flex items-start gap-3">
<div class="shrink-0">
<i data-lucide="${icon}" class="w-5 h-5 text-${iconColor}-500"></i>
</div>
<div>
<div class="flex items-center gap-2 flex-wrap mb-1">
<h4 class="font-semibold text-foreground">${escapeHtml(name)}</h4>
<span class="text-xs px-2 py-0.5 bg-${sourceBadgeColor}/10 text-${sourceBadgeColor} rounded-full">
${fromCli === 'codex' ? 'Codex' : 'Claude'}
</span>
${typeBadge}
</div>
<div class="text-sm space-y-1 text-muted-foreground">
<div class="flex items-center gap-2">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${isHttp ? t('mcp.url') : t('mcp.cmd')}</span>
<span class="truncate text-xs" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
</div>
${args.length > 0 ? `
<div class="flex items-start gap-2">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">${t('mcp.args')}</span>
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
</div>
` : ''}
</div>
</div>
</div>
</div>
<div class="mt-3 pt-3 border-t border-border">
<button class="w-full px-3 py-2 text-sm font-medium bg-${iconColor}-500 hover:bg-${iconColor}-600 text-white rounded-lg transition-colors flex items-center justify-center gap-1.5"
onclick="copyCrossCliServer('${escapeHtml(name)}', ${JSON.stringify(config).replace(/'/g, "&#39;")}, '${fromCli}', '${targetCli}')">
<i data-lucide="copy" class="w-4 h-4"></i>
${buttonText}
</button>
</div>
</div>
`;
}
// Copy server from one CLI to another
async function copyCrossCliServer(name, config, fromCli, targetCli) {
try {
let endpoint, body;
if (targetCli === 'codex') {
// Copy from Claude to Codex
endpoint = '/api/codex-mcp-add';
body = { serverName: name, serverConfig: config };
} else if (targetCli === 'project') {
// Copy from Codex to Claude project
endpoint = '/api/mcp-copy-server';
body = { projectPath, serverName: name, serverConfig: config, configType: 'mcp' };
} else if (targetCli === 'global') {
// Copy to Claude global
endpoint = '/api/mcp-add-global-server';
body = { serverName: name, serverConfig: config };
}
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (data.success) {
const targetName = targetCli === 'codex' ? 'Codex' : 'Claude';
showToast(t('mcp.success'), `${t('mcp.serverInstalled')} (${targetName})`, 'success');
await loadMcpConfig();
renderMcpManager();
} else {
showToast(t('mcp.error'), data.error, 'error');
}
} catch (error) {
showToast(t('mcp.error'), error.message, 'error');
}
}
// ========================================
// Codex MCP Create Modal
// ========================================

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,928 @@
// MCP Manager View - Redesigned with Sectioned Layout
// Comprehensive MCP management for Claude and Codex with clear section separation
// ============================================================
// CONSTANTS & CONFIGURATION
// ============================================================
const CCW_MCP_TOOLS = [
{ name: 'write_file', desc: 'Write/create files', core: true },
{ name: 'edit_file', desc: 'Edit/replace content', core: true },
{ name: 'codex_lens', desc: 'Code index & search', core: true },
{ name: 'smart_search', desc: 'Quick regex/NL search', core: true },
{ name: 'session_manager', desc: 'Workflow sessions', core: false },
{ name: 'generate_module_docs', desc: 'Generate docs', core: false },
{ name: 'update_module_claude', desc: 'Update CLAUDE.md', core: false },
{ name: 'cli_executor', desc: 'Gemini/Qwen/Codex CLI', core: false },
];
const MCP_CATEGORIES = [
'Development Tools',
'Data & APIs',
'Files & Storage',
'AI & ML',
'DevOps',
'Custom'
];
// Get currently enabled tools from installed config (Claude)
function getCcwEnabledTools() {
const currentPath = projectPath;
const projectData = mcpAllProjects[currentPath] || {};
const ccwConfig = projectData.mcpServers?.['ccw-tools'];
if (ccwConfig?.env?.CCW_ENABLED_TOOLS) {
const val = ccwConfig.env.CCW_ENABLED_TOOLS;
if (val.toLowerCase() === 'all') return CCW_MCP_TOOLS.map(t => t.name);
return val.split(',').map(t => t.trim());
}
return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name);
}
// Get currently enabled tools from Codex config
function getCcwEnabledToolsCodex() {
const ccwConfig = codexMcpServers?.['ccw-tools'];
if (ccwConfig?.env?.CCW_ENABLED_TOOLS) {
const val = ccwConfig.env.CCW_ENABLED_TOOLS;
if (val.toLowerCase() === 'all') return CCW_MCP_TOOLS.map(t => t.name);
return val.split(',').map(t => t.trim());
}
return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name);
}
// ============================================================
// MODAL DIALOG COMPONENT
// ============================================================
function showMcpEditorModal(options = {}) {
const {
mode = 'create',
serverName = '',
serverConfig = {},
template = null,
cliMode = currentCliMode, // 'claude' or 'codex'
installTargets = cliMode === 'codex' ? ['codex'] : ['project', 'global']
} = options;
const isView = mode === 'view';
const isEdit = mode === 'edit';
const title = isView ? t('mcp.viewServer') : isEdit ? t('mcp.editServer') : t('mcp.createServer');
const initialName = serverName || template?.name || '';
const initialDesc = template?.description || '';
const initialCategory = template?.category || 'Development Tools';
const initialConfig = serverConfig || template?.serverConfig || {
command: '',
args: [],
env: {},
url: '',
cwd: ''
};
const modalHtml = `
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" id="mcpEditorModal" style="backdrop-filter: blur(4px);">
<div class="bg-card border border-border rounded-xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="flex items-center justify-between px-6 py-4 border-b border-border bg-gradient-to-r from-primary/5 to-transparent">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
<i data-lucide="${isView ? 'eye' : isEdit ? 'edit-3' : 'plus-circle'}" class="w-5 h-5 text-primary"></i>
</div>
<div>
<h2 class="text-lg font-bold text-foreground">${title}</h2>
<p class="text-xs text-muted-foreground">${cliMode === 'codex' ? 'Codex MCP Server' : 'Claude MCP Server'}</p>
</div>
</div>
<button onclick="closeMcpEditorModal()" class="text-muted-foreground hover:text-foreground transition-colors">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto px-6 py-4">
<div class="space-y-4 mb-6">
<div>
<label class="block text-sm font-medium text-foreground mb-1.5">${t('mcp.serverName')}</label>
<input type="text" id="mcpModalName" value="${initialName}" ${isView ? 'disabled' : ''}
placeholder="my-mcp-server"
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed" />
</div>
${!isView && cliMode !== 'codex' ? `
<div>
<label class="block text-sm font-medium text-foreground mb-1.5">${t('mcp.description')} (${t('mcp.optional')})</label>
<input type="text" id="mcpModalDesc" value="${initialDesc}" placeholder="Brief description"
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50" />
</div>
` : ''}
</div>
<div class="mb-4">
<div class="flex items-center gap-2 border-b border-border">
<button class="px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary"
onclick="switchMcpServerType('stdio')" id="mcpTypeStdio" ${isView ? 'disabled' : ''}>
<i data-lucide="terminal" class="w-4 h-4 inline mr-1.5"></i>
STDIO (Command)
</button>
<button class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-muted-foreground hover:text-foreground"
onclick="switchMcpServerType('http')" id="mcpTypeHttp" ${isView ? 'disabled' : ''}>
<i data-lucide="globe" class="w-4 h-4 inline mr-1.5"></i>
HTTP (URL)
</button>
</div>
</div>
<div id="mcpStdioConfig" class="space-y-4">
<div>
<label class="block text-sm font-medium text-foreground mb-1.5">${t('mcp.command')}</label>
<input type="text" id="mcpModalCommand" value="${initialConfig.command || ''}" ${isView ? 'disabled' : ''}
placeholder="node" class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed font-mono text-sm" />
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1.5">${t('mcp.args')} (${t('mcp.optional')})</label>
<textarea id="mcpModalArgs" ${isView ? 'disabled' : ''} rows="3" placeholder='["/path/to/server.js"]'
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed font-mono text-sm">${JSON.stringify(initialConfig.args || [], null, 2)}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1.5">${t('mcp.env')} (${t('mcp.optional')})</label>
<textarea id="mcpModalEnv" ${isView ? 'disabled' : ''} rows="4" placeholder='{"API_KEY": "your-key"}'
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed font-mono text-sm">${JSON.stringify(initialConfig.env || {}, null, 2)}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1.5">${t('mcp.cwd')} (${t('mcp.optional')})</label>
<input type="text" id="mcpModalCwd" value="${initialConfig.cwd || ''}" ${isView ? 'disabled' : ''}
placeholder="/path/to/working/directory" class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed font-mono text-sm" />
</div>
</div>
<div id="mcpHttpConfig" class="space-y-4 hidden">
<div>
<label class="block text-sm font-medium text-foreground mb-1.5">${t('mcp.url')}</label>
<input type="text" id="mcpModalUrl" value="${initialConfig.url || ''}" ${isView ? 'disabled' : ''}
placeholder="https://api.example.com/mcp" class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed font-mono text-sm" />
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1.5">${t('mcp.httpHeaders')} (${t('mcp.optional')})</label>
<textarea id="mcpModalHttpHeaders" ${isView ? 'disabled' : ''} rows="4" placeholder='{"Authorization": "Bearer token"}'
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed font-mono text-sm">${JSON.stringify(initialConfig.http_headers || initialConfig.httpHeaders || {}, null, 2)}</textarea>
</div>
</div>
${!isView && cliMode !== 'codex' ? `
<div class="mt-6 pt-6 border-t border-border">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="mcpModalSaveTemplate" class="w-4 h-4 rounded border-border text-primary focus:ring-2 focus:ring-primary/50" />
<span class="text-sm font-medium text-foreground">${t('mcp.saveAsTemplate')}</span>
</label>
</div>
` : ''}
</div>
<div class="flex items-center justify-between px-6 py-4 border-t border-border bg-muted/30">
${!isView ? `
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-foreground">${t('mcp.installTo')}:</span>
<select id="mcpModalTarget" class="px-3 py-1.5 bg-background border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50">
${installTargets.map(target => {
const labels = {
project: 'Project (.mcp.json)',
global: 'Global (~/.claude.json)',
codex: 'Codex (~/.codex/config.toml)'
};
return `<option value="${target}">${labels[target]}</option>`;
}).join('')}
</select>
</div>
` : '<div></div>'}
<div class="flex items-center gap-2">
<button onclick="closeMcpEditorModal()" class="px-4 py-2 text-sm font-medium text-foreground hover:bg-muted rounded-lg transition-colors">
${isView ? t('mcp.close') : t('mcp.cancel')}
</button>
${!isView ? `
<button onclick="saveMcpFromModal('${cliMode}')" class="px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-2">
<i data-lucide="save" class="w-4 h-4"></i>
${isEdit ? t('mcp.update') : t('mcp.install')}
</button>
` : ''}
</div>
</div>
</div>
</div>
`;
const existingModal = document.getElementById('mcpEditorModal');
if (existingModal) existingModal.remove();
document.body.insertAdjacentHTML('beforeend', modalHtml);
if (typeof lucide !== 'undefined') lucide.createIcons();
if (initialConfig.url) switchMcpServerType('http');
}
function switchMcpServerType(type) {
const stdioConfig = document.getElementById('mcpStdioConfig');
const httpConfig = document.getElementById('mcpHttpConfig');
const stdioBtn = document.getElementById('mcpTypeStdio');
const httpBtn = document.getElementById('mcpTypeHttp');
if (type === 'stdio') {
stdioConfig.classList.remove('hidden');
httpConfig.classList.add('hidden');
stdioBtn.classList.add('border-primary', 'text-primary');
stdioBtn.classList.remove('border-transparent', 'text-muted-foreground');
httpBtn.classList.remove('border-primary', 'text-primary');
httpBtn.classList.add('border-transparent', 'text-muted-foreground');
} else {
stdioConfig.classList.add('hidden');
httpConfig.classList.remove('hidden');
httpBtn.classList.add('border-primary', 'text-primary');
httpBtn.classList.remove('border-transparent', 'text-muted-foreground');
stdioBtn.classList.remove('border-primary', 'text-primary');
stdioBtn.classList.add('border-transparent', 'text-muted-foreground');
}
}
function closeMcpEditorModal() {
const modal = document.getElementById('mcpEditorModal');
if (modal) modal.remove();
}
async function saveMcpFromModal(cliMode) {
const name = document.getElementById('mcpModalName').value.trim();
const desc = document.getElementById('mcpModalDesc')?.value.trim() || '';
const target = document.getElementById('mcpModalTarget')?.value || (cliMode === 'codex' ? 'codex' : 'project');
const saveAsTemplate = document.getElementById('mcpModalSaveTemplate')?.checked || false;
if (!name) {
showToast(t('mcp.error'), t('mcp.nameRequired'), 'error');
return;
}
const isStdio = !document.getElementById('mcpStdioConfig').classList.contains('hidden');
let serverConfig = {};
if (isStdio) {
const command = document.getElementById('mcpModalCommand').value.trim();
if (!command) {
showToast(t('mcp.error'), t('mcp.commandRequired'), 'error');
return;
}
serverConfig.command = command;
const argsText = document.getElementById('mcpModalArgs').value.trim();
if (argsText) {
try {
serverConfig.args = JSON.parse(argsText);
if (!Array.isArray(serverConfig.args)) throw new Error('Args must be an array');
} catch (e) {
showToast(t('mcp.error'), t('mcp.invalidArgsJson'), 'error');
return;
}
}
const envText = document.getElementById('mcpModalEnv').value.trim();
if (envText) {
try {
serverConfig.env = JSON.parse(envText);
if (typeof serverConfig.env !== 'object' || Array.isArray(serverConfig.env)) throw new Error('Env must be an object');
} catch (e) {
showToast(t('mcp.error'), t('mcp.invalidEnvJson'), 'error');
return;
}
}
const cwd = document.getElementById('mcpModalCwd').value.trim();
if (cwd) serverConfig.cwd = cwd;
} else {
const url = document.getElementById('mcpModalUrl').value.trim();
if (!url) {
showToast(t('mcp.error'), t('mcp.urlRequired'), 'error');
return;
}
serverConfig.url = url;
const headersText = document.getElementById('mcpModalHttpHeaders').value.trim();
if (headersText) {
try {
const headers = JSON.parse(headersText);
if (typeof headers !== 'object' || Array.isArray(headers)) throw new Error('Headers must be an object');
serverConfig.http_headers = headers;
} catch (e) {
showToast(t('mcp.error'), t('mcp.invalidHeadersJson'), 'error');
return;
}
}
}
if (saveAsTemplate && cliMode !== 'codex') {
try {
await fetch('/api/mcp-templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description: desc, serverConfig, category: 'Custom' })
});
} catch (error) {
console.error('Error saving template:', error);
}
}
try {
let endpoint = '';
let body = {};
if (cliMode === 'codex') {
endpoint = '/api/codex-mcp-add';
body = { serverName: name, serverConfig };
} else {
if (target === 'global') {
endpoint = '/api/mcp-add-global-server';
body = { serverName: name, serverConfig };
} else {
endpoint = '/api/mcp-copy-server';
body = { projectPath, serverName: name, serverConfig, configType: 'mcp' };
}
}
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (data.success || data.serverName) {
showToast(t('mcp.success'), t('mcp.serverInstalled'), 'success');
closeMcpEditorModal();
await loadMcpConfig();
renderMcpManager();
} else {
showToast(t('mcp.error'), data.error || 'Installation failed', 'error');
}
} catch (error) {
console.error('Error installing MCP server:', error);
showToast(t('mcp.error'), error.message, 'error');
}
}
// ============================================================
// MAIN RENDER FUNCTION
// ============================================================
async function renderMcpManager() {
const container = document.getElementById('mainContent');
if (!container) return;
const statsGrid = document.getElementById('statsGrid');
const searchInput = document.getElementById('searchInput');
if (statsGrid) statsGrid.style.display = 'none';
if (searchInput) searchInput.parentElement.style.display = 'none';
if (!mcpConfig) await loadMcpConfig();
await loadMcpTemplates();
const currentPath = projectPath;
const projectData = mcpAllProjects[currentPath] || {};
const projectServers = projectData.mcpServers || {};
const disabledServers = projectData.disabledMcpServers || [];
const codexServers = codexMcpServers || {};
const isClaude = currentCliMode === 'claude';
// Section 1: Project Available (Enterprise + Global + Project-specific)
const projectAvailable = [];
if (isClaude) {
// Enterprise servers
for (const [name, config] of Object.entries(mcpEnterpriseServers || {})) {
projectAvailable.push({ name, config, source: 'enterprise', enabled: true, canRemove: false, canToggle: false });
}
// Global servers
for (const [name, config] of Object.entries(mcpUserServers || {})) {
if (!mcpEnterpriseServers?.[name]) {
projectAvailable.push({ name, config, source: 'global', enabled: !disabledServers.includes(name), canRemove: false, canToggle: true });
}
}
// Project servers
for (const [name, config] of Object.entries(projectServers)) {
if (!mcpEnterpriseServers?.[name] && !mcpUserServers?.[name]) {
projectAvailable.push({ name, config, source: 'project', enabled: !disabledServers.includes(name), canRemove: true, canToggle: true });
}
}
} else {
// Codex servers
for (const [name, config] of Object.entries(codexServers)) {
projectAvailable.push({ name, config, source: 'codex', enabled: config.enabled !== false, canRemove: true, canToggle: true });
}
}
// Section 2: Global Management (for Claude only)
const globalManagement = isClaude ? Object.entries(mcpUserServers || {}) : [];
// Section 3: Other Projects (for Claude only)
const allAvailableServers = isClaude ? getAllAvailableMcpServers() : {};
const currentProjectServerNames = Object.keys(projectServers);
const otherProjects = isClaude ? Object.entries(allAvailableServers).filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal) : [];
// Section 4: Cross-CLI servers (Available from other CLI)
const crossCliServers = [];
if (isClaude) {
// Show Codex servers when in Claude mode
for (const [name, config] of Object.entries(codexServers)) {
// Check if already exists in Claude (project or global)
const existsInClaude = currentProjectServerNames.includes(name) || mcpUserServers?.[name];
if (!existsInClaude) {
crossCliServers.push({ name, config, fromCli: 'codex' });
}
}
} else {
// Show Claude servers when in Codex mode
// Collect all Claude servers (global + project)
const allClaudeServers = { ...mcpUserServers, ...projectServers };
for (const [name, config] of Object.entries(allClaudeServers)) {
// Check if already exists in Codex
const existsInCodex = codexServers[name];
if (!existsInCodex) {
crossCliServers.push({ name, config, fromCli: 'claude' });
}
}
}
container.innerHTML = `
<div class="mcp-manager">
<!-- CLI Mode Toggle -->
<div class="mcp-cli-toggle mb-6">
<div class="flex items-center justify-between bg-card border border-border rounded-lg p-4">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-foreground">${t('mcp.cliMode')}</span>
<div class="flex items-center bg-muted rounded-lg p-1">
<button class="cli-mode-btn px-4 py-2 text-sm font-medium rounded-md transition-all ${isClaude ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
onclick="setCliMode('claude')">
<i data-lucide="bot" class="w-4 h-4 inline mr-1.5"></i>
Claude
</button>
<button class="cli-mode-btn px-4 py-2 text-sm font-medium rounded-md transition-all ${!isClaude ? 'shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
onclick="setCliMode('codex')"
style="${!isClaude ? 'background-color: #f97316; color: white;' : ''}">
<i data-lucide="code-2" class="w-4 h-4 inline mr-1.5"></i>
Codex
</button>
</div>
</div>
<div class="flex items-center gap-3">
<button onclick="renderMcpTemplates()" class="px-4 py-2 text-sm font-medium bg-muted hover:bg-muted/80 text-foreground rounded-lg transition-colors flex items-center gap-2">
<i data-lucide="bookmark" class="w-4 h-4"></i>
${t('mcp.templates')}
</button>
<button onclick="showMcpEditorModal({ mode: 'create', cliMode: '${currentCliMode}' })"
class="px-4 py-2 text-sm font-medium ${isClaude ? 'bg-primary hover:bg-primary/90 text-primary-foreground' : 'bg-orange-500 hover:bg-orange-600 text-white'} rounded-lg transition-colors flex items-center gap-2">
<i data-lucide="plus-circle" class="w-4 h-4"></i>
${t('mcp.newServer')}
</button>
</div>
</div>
</div>
<!-- Section 1: Current Project Available -->
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-foreground flex items-center gap-2">
<i data-lucide="folder-check" class="w-5 h-5"></i>
${isClaude ? t('mcp.projectAvailable') : 'Codex Global MCP Servers'}
<span class="text-sm text-muted-foreground font-normal">(${projectAvailable.length})</span>
</h2>
</div>
<div class="space-y-3">
${projectAvailable.length === 0 ? `
<div class="bg-card border border-dashed border-border rounded-lg p-8 text-center">
<i data-lucide="inbox" class="w-12 h-12 text-muted-foreground mx-auto mb-3"></i>
<p class="text-sm text-muted-foreground">${isClaude ? t('mcp.noMcpServers') : 'No Codex MCP servers configured'}</p>
</div>
` : projectAvailable.map(server => renderMcpServerCard(server, isClaude ? 'claude' : 'codex')).join('')}
</div>
</div>
${isClaude ? `
<!-- Section 2: Global Management -->
${globalManagement.length > 0 ? `
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-foreground flex items-center gap-2">
<i data-lucide="globe" class="w-5 h-5"></i>
${t('mcp.user')}
<span class="text-sm text-muted-foreground font-normal">(${globalManagement.length})</span>
</h2>
</div>
<div class="space-y-3">
${globalManagement.map(([name, config]) => renderMcpServerCard({ name, config, source: 'global-manage', enabled: true, canRemove: true, canToggle: false }, 'claude')).join('')}
</div>
</div>
` : ''}
<!-- Section 3: Other Projects -->
${otherProjects.length > 0 ? `
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-foreground flex items-center gap-2">
<i data-lucide="folder-open" class="w-5 h-5"></i>
${t('mcp.availableOther')}
<span class="text-sm text-muted-foreground font-normal">(${otherProjects.length})</span>
</h2>
</div>
<div class="space-y-3">
${otherProjects.map(([name, info]) => renderMcpServerCardAvailable(name, info)).join('')}
</div>
</div>
` : ''}
<!-- Section 4: Cross-CLI Servers -->
${crossCliServers.length > 0 ? `
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-foreground flex items-center gap-2">
${isClaude ? `
<i data-lucide="circle-dashed" class="w-5 h-5 text-orange-500"></i>
${t('mcp.claude.copyFromCodex')}
` : `
<i data-lucide="circle" class="w-5 h-5 text-blue-500"></i>
${t('mcp.codex.copyFromClaude')}
`}
<span class="text-sm text-muted-foreground font-normal">(${crossCliServers.length})</span>
</h2>
</div>
<div class="space-y-3">
${crossCliServers.map(server => renderCrossCliServerCard(server, isClaude)).join('')}
</div>
</div>
` : ''}
` : ''}
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function renderMcpServerCard(server, cliMode) {
const { name, config, source, enabled, canRemove, canToggle } = server;
const sourceInfo = {
enterprise: { icon: 'shield', color: 'purple', label: 'Enterprise' },
global: { icon: 'globe', color: 'green', label: 'Global' },
'global-manage': { icon: 'globe', color: 'green', label: 'Global' },
project: { icon: 'folder', color: 'blue', label: 'Project' },
codex: { icon: 'code-2', color: 'orange', label: 'Codex' }
};
const info = sourceInfo[source] || sourceInfo.project;
const isStdio = !!config.command;
const isHttp = !!config.url;
return `
<div class="bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${!enabled ? 'opacity-60' : ''}">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-3 flex-1 min-w-0">
<div class="shrink-0 w-10 h-10 rounded-lg bg-${info.color}-500/10 flex items-center justify-center">
<i data-lucide="${info.icon}" class="w-5 h-5 text-${info.color}-500"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<h3 class="font-semibold text-foreground truncate">${name}</h3>
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-${info.color}-500/10 text-${info.color}-600 dark:text-${info.color}-400">
${info.label}
</span>
${enabled ? `
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-success/10 text-success">
<i data-lucide="check" class="w-3 h-3"></i>
Enabled
</span>
` : `
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-muted text-muted-foreground">
<i data-lucide="x" class="w-3 h-3"></i>
Disabled
</span>
`}
</div>
<div class="text-sm text-muted-foreground space-y-1">
${isStdio ? `
<div class="flex items-center gap-2">
<i data-lucide="terminal" class="w-3 h-3"></i>
<code class="text-xs">${config.command} ${(config.args || []).slice(0, 2).join(' ')}</code>
</div>
` : ''}
${isHttp ? `
<div class="flex items-center gap-2">
<i data-lucide="globe" class="w-3 h-3"></i>
<code class="text-xs truncate">${config.url}</code>
</div>
` : ''}
</div>
</div>
</div>
<div class="flex items-center gap-2">
${canToggle ? `
<button onclick="toggleMcpServer('${name}', '${cliMode}', ${!enabled})" class="p-2 rounded-lg hover:bg-muted transition-colors" title="${enabled ? 'Disable' : 'Enable'}">
<i data-lucide="${enabled ? 'toggle-right' : 'toggle-left'}" class="w-4 h-4 text-${enabled ? 'success' : 'muted-foreground'}"></i>
</button>
` : ''}
<button onclick="showMcpEditorModal({ mode: 'view', serverName: '${name}', serverConfig: ${JSON.stringify(config).replace(/"/g, '&quot;')}, cliMode: '${cliMode}' })" class="p-2 rounded-lg hover:bg-muted transition-colors">
<i data-lucide="eye" class="w-4 h-4 text-foreground"></i>
</button>
${canRemove && source !== 'global-manage' ? `
<button onclick="showMcpEditorModal({ mode: 'edit', serverName: '${name}', serverConfig: ${JSON.stringify(config).replace(/"/g, '&quot;')}, cliMode: '${cliMode}' })" class="p-2 rounded-lg hover:bg-muted transition-colors">
<i data-lucide="edit-3" class="w-4 h-4 text-foreground"></i>
</button>
` : ''}
${canRemove ? `
<button onclick="deleteMcpServer('${name}', '${source}', '${cliMode}')" class="p-2 rounded-lg hover:bg-destructive/10 transition-colors">
<i data-lucide="trash-2" class="w-4 h-4 text-destructive"></i>
</button>
` : ''}
</div>
</div>
</div>
`;
}
function renderMcpServerCardAvailable(name, info) {
return `
<div class="bg-card border border-dashed border-border rounded-lg p-4 hover:shadow-md hover:border-solid transition-all">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-3 flex-1">
<div class="shrink-0 w-10 h-10 rounded-lg bg-muted flex items-center justify-center">
<i data-lucide="folder" class="w-5 h-5 text-muted-foreground"></i>
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<h3 class="font-semibold text-foreground">${name}</h3>
<span class="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">${t('mcp.available')}</span>
</div>
<p class="text-xs text-muted-foreground">${t('mcp.from')} ${info.projectName || info.source}</p>
</div>
</div>
<button onclick="installServerFromOther('${name}', ${JSON.stringify(info.config).replace(/"/g, '&quot;')})" class="px-3 py-1.5 text-sm font-medium bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg transition-colors">
${t('mcp.addToProject')}
</button>
</div>
</div>
`;
}
function renderCrossCliServerCard(server, isClaude) {
const { name, config, fromCli } = server;
const isStdio = !!config.command;
const isHttp = !!config.url;
// Use solid circle for Claude, dashed circle for Codex
const icon = fromCli === 'codex' ? 'circle-dashed' : 'circle';
const iconColor = fromCli === 'codex' ? 'orange' : 'blue';
const targetCli = isClaude ? 'project' : 'codex';
const buttonText = isClaude ? t('mcp.codex.copyToClaude') : t('mcp.claude.copyToCodex');
return `
<div class="bg-card border border-dashed border-${iconColor}-200 dark:border-${iconColor}-800 rounded-lg p-4 hover:shadow-md hover:border-solid transition-all">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-3 flex-1 min-w-0">
<div class="shrink-0 w-10 h-10 rounded-full bg-${iconColor}-50 dark:bg-${iconColor}-950/30 flex items-center justify-center">
<i data-lucide="${icon}" class="w-5 h-5 text-${iconColor}-500"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<h3 class="font-semibold text-foreground truncate">${name}</h3>
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-${iconColor}-50 dark:bg-${iconColor}-950/30 text-${iconColor}-600 dark:text-${iconColor}-400">
<i data-lucide="${icon}" class="w-3 h-3"></i>
${fromCli === 'codex' ? 'Codex' : 'Claude'}
</span>
</div>
<div class="text-sm text-muted-foreground space-y-1">
${isStdio ? `
<div class="flex items-center gap-2">
<i data-lucide="terminal" class="w-3 h-3"></i>
<code class="text-xs truncate">${config.command} ${(config.args || []).slice(0, 2).join(' ')}</code>
</div>
` : ''}
${isHttp ? `
<div class="flex items-center gap-2">
<i data-lucide="globe" class="w-3 h-3"></i>
<code class="text-xs truncate">${config.url}</code>
</div>
` : ''}
</div>
</div>
</div>
<button onclick="copyCrossCliServer('${name}', ${JSON.stringify(config).replace(/"/g, '&quot;')}, '${fromCli}', '${targetCli}')" class="px-3 py-1.5 text-sm font-medium bg-${iconColor}-500 hover:bg-${iconColor}-600 text-white rounded-lg transition-colors flex items-center gap-1.5">
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
${buttonText}
</button>
</div>
</div>
`;
}
async function copyCrossCliServer(name, config, fromCli, targetCli) {
try {
let endpoint, body;
if (targetCli === 'codex') {
// Copy from Claude to Codex
endpoint = '/api/codex-mcp-add';
body = { serverName: name, serverConfig: config };
} else if (targetCli === 'project') {
// Copy from Codex to Claude project
endpoint = '/api/mcp-copy-server';
body = { projectPath, serverName: name, serverConfig: config, configType: 'mcp' };
} else if (targetCli === 'global') {
// Copy to Claude global
endpoint = '/api/mcp-add-global-server';
body = { serverName: name, serverConfig: config };
}
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (data.success) {
const targetName = targetCli === 'codex' ? 'Codex' : 'Claude';
showToast(t('mcp.success'), `${t('mcp.serverInstalled')} (${targetName})`, 'success');
await loadMcpConfig();
renderMcpManager();
} else {
showToast(t('mcp.error'), data.error, 'error');
}
} catch (error) {
showToast(t('mcp.error'), error.message, 'error');
}
}
async function installServerFromOther(name, config) {
try {
const res = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectPath, serverName: name, serverConfig: config, configType: 'mcp' })
});
const data = await res.json();
if (data.success) {
showToast(t('mcp.success'), t('mcp.serverInstalled'), 'success');
await loadMcpConfig();
renderMcpManager();
} else {
showToast(t('mcp.error'), data.error, 'error');
}
} catch (error) {
showToast(t('mcp.error'), error.message, 'error');
}
}
async function toggleMcpServer(serverName, cliMode, enable) {
try {
let endpoint = cliMode === 'codex' ? '/api/codex-mcp-toggle' : '/api/mcp-toggle';
let body = cliMode === 'codex' ? { serverName, enabled: enable } : { projectPath, serverName, enable };
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (data.success || data.serverName) {
showToast(t('mcp.success'), enable ? t('mcp.serverEnabled') : t('mcp.serverDisabled'), 'success');
await loadMcpConfig();
renderMcpManager();
} else {
showToast(t('mcp.error'), data.error, 'error');
}
} catch (error) {
showToast(t('mcp.error'), error.message, 'error');
}
}
async function deleteMcpServer(serverName, source, cliMode) {
if (!confirm(`Are you sure you want to delete "${serverName}"?`)) return;
try {
let endpoint = '';
let body = {};
if (cliMode === 'codex') {
endpoint = '/api/codex-mcp-remove';
body = { serverName };
} else if (source === 'global-manage') {
endpoint = '/api/mcp-remove-global-server';
body = { serverName };
} else if (source === 'project') {
endpoint = '/api/mcp-remove-server';
body = { projectPath, serverName };
} else {
endpoint = '/api/mcp-remove-global-server';
body = { serverName };
}
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (data.success || data.removed) {
showToast(t('mcp.success'), t('mcp.serverDeleted'), 'success');
await loadMcpConfig();
renderMcpManager();
} else {
showToast(t('mcp.error'), data.error, 'error');
}
} catch (error) {
showToast(t('mcp.error'), error.message, 'error');
}
}
async function renderMcpTemplates() {
const container = document.getElementById('mainContent');
if (!container) return;
if (!mcpTemplates || mcpTemplates.length === 0) await loadMcpTemplates();
const categories = [...new Set(mcpTemplates.map(t => t.category || 'Custom'))];
container.innerHTML = `
<div class="mcp-templates-view">
<div class="flex items-center justify-between mb-6">
<div>
<button onclick="renderMcpManager()" class="text-sm text-primary hover:underline flex items-center gap-1 mb-2">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
${t('mcp.backToManager')}
</button>
<h1 class="text-2xl font-bold text-foreground">${t('mcp.templates')}</h1>
</div>
</div>
<div class="space-y-6">
${categories.map(category => {
const templates = mcpTemplates.filter(t => (t.category || 'Custom') === category);
return `
<div>
<h2 class="text-lg font-semibold text-foreground mb-3">${category} (${templates.length})</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${templates.map(template => `
<div class="bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all">
<h3 class="font-semibold text-foreground mb-2">${template.name}</h3>
<p class="text-sm text-muted-foreground mb-4">${template.description || 'No description'}</p>
<div class="flex gap-2">
<button onclick="showMcpEditorModal({ mode: 'create', template: ${JSON.stringify(template).replace(/"/g, '&quot;')} })" class="flex-1 px-3 py-2 text-sm bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg">Install</button>
<button onclick="deleteTemplate('${template.name}')" class="px-3 py-2 text-sm bg-destructive/10 hover:bg-destructive/20 text-destructive rounded-lg">Delete</button>
</div>
</div>
`).join('')}
</div>
</div>
`;
}).join('')}
</div>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
async function deleteTemplate(name) {
if (!confirm(`Delete template "${name}"?`)) return;
try {
const res = await fetch(`/api/mcp-templates/${encodeURIComponent(name)}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
showToast(t('mcp.success'), t('mcp.templateDeleted'), 'success');
await loadMcpTemplates();
renderMcpTemplates();
}
} catch (error) {
showToast(t('mcp.error'), error.message, 'error');
}
}
function showToast(title, message, type = 'info') {
console.log(`[${type.toUpperCase()}] ${title}: ${message}`);
if (typeof window.showNotification === 'function') {
window.showNotification(title, message, type);
} else {
alert(`${title}\n${message}`);
}
}
async function loadMcpTemplates() {
try {
const res = await fetch('/api/mcp-templates');
const data = await res.json();
if (data.success) mcpTemplates = data.templates || [];
} catch (error) {
console.error('Error loading MCP templates:', error);
mcpTemplates = [];
}
}
window.renderMcpManager = renderMcpManager;
window.renderMcpTemplates = renderMcpTemplates;
window.showMcpEditorModal = showMcpEditorModal;
window.closeMcpEditorModal = closeMcpEditorModal;
window.saveMcpFromModal = saveMcpFromModal;
window.switchMcpServerType = switchMcpServerType;
window.toggleMcpServer = toggleMcpServer;
window.deleteMcpServer = deleteMcpServer;
window.deleteTemplate = deleteTemplate;
window.installServerFromOther = installServerFromOther;