Add comprehensive tests for tokenizer, performance benchmarks, and TreeSitter parser functionality

- Implemented unit tests for the Tokenizer class, covering various text inputs, edge cases, and fallback mechanisms.
- Created performance benchmarks comparing tiktoken and pure Python implementations for token counting.
- Developed extensive tests for TreeSitterSymbolParser across Python, JavaScript, and TypeScript, ensuring accurate symbol extraction and parsing.
- Added configuration documentation for MCP integration and custom prompts, enhancing usability and flexibility.
- Introduced a refactor script for GraphAnalyzer to streamline future improvements.
This commit is contained in:
catlog22
2025-12-15 14:36:09 +08:00
parent 82dcafff00
commit 0fe16963cd
49 changed files with 9307 additions and 438 deletions

View File

@@ -1022,6 +1022,16 @@
overflow: hidden;
}
.diagnosis-card .collapsible-content {
display: block;
padding: 1rem;
background: hsl(var(--card));
}
.diagnosis-card .collapsible-content.collapsed {
display: none;
}
.diagnosis-header {
background: hsl(var(--muted) / 0.3);
}

View File

@@ -15,6 +15,11 @@ let mcpCurrentProjectServers = {};
let mcpConfigSources = [];
let mcpCreateMode = 'form'; // 'form' or 'json'
// ========== CLI Toggle State (Claude / Codex) ==========
let currentCliMode = 'claude'; // 'claude' or 'codex'
let codexMcpConfig = null;
let codexMcpServers = {};
// ========== Initialization ==========
function initMcpManager() {
// Initialize MCP navigation
@@ -44,6 +49,12 @@ async function loadMcpConfig() {
mcpEnterpriseServers = data.enterpriseServers || {};
mcpConfigSources = data.configSources || [];
// Load Codex MCP config
if (data.codex) {
codexMcpConfig = data.codex;
codexMcpServers = data.codex.servers || {};
}
// Get current project servers
const currentPath = projectPath.replace(/\//g, '\\');
mcpCurrentProjectServers = mcpAllProjects[currentPath]?.mcpServers || {};
@@ -58,6 +69,135 @@ async function loadMcpConfig() {
}
}
// ========== CLI Mode Toggle ==========
function setCliMode(mode) {
currentCliMode = mode;
renderMcpManager();
}
function getCliMode() {
return currentCliMode;
}
// ========== Codex MCP Functions ==========
/**
* Add MCP server to Codex config.toml
*/
async function addCodexMcpServer(serverName, serverConfig) {
try {
const response = await fetch('/api/codex-mcp-add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
serverName: serverName,
serverConfig: serverConfig
})
});
if (!response.ok) throw new Error('Failed to add Codex MCP server');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(t('mcp.codex.serverAdded', { name: serverName }), 'success');
} else {
showRefreshToast(result.error || t('mcp.codex.addFailed'), 'error');
}
return result;
} catch (err) {
console.error('Failed to add Codex MCP server:', err);
showRefreshToast(t('mcp.codex.addFailed') + ': ' + err.message, 'error');
return null;
}
}
/**
* Remove MCP server from Codex config.toml
*/
async function removeCodexMcpServer(serverName) {
try {
const response = await fetch('/api/codex-mcp-remove', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ serverName })
});
if (!response.ok) throw new Error('Failed to remove Codex MCP server');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(t('mcp.codex.serverRemoved', { name: serverName }), 'success');
} else {
showRefreshToast(result.error || t('mcp.codex.removeFailed'), 'error');
}
return result;
} catch (err) {
console.error('Failed to remove Codex MCP server:', err);
showRefreshToast(t('mcp.codex.removeFailed') + ': ' + err.message, 'error');
return null;
}
}
/**
* Toggle Codex MCP server enabled state
*/
async function toggleCodexMcpServer(serverName, enabled) {
try {
const response = await fetch('/api/codex-mcp-toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ serverName, enabled })
});
if (!response.ok) throw new Error('Failed to toggle Codex MCP server');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(t('mcp.codex.serverToggled', { name: serverName, state: enabled ? 'enabled' : 'disabled' }), 'success');
}
return result;
} catch (err) {
console.error('Failed to toggle Codex MCP server:', err);
showRefreshToast(t('mcp.codex.toggleFailed') + ': ' + err.message, 'error');
return null;
}
}
/**
* Copy Claude MCP server to Codex
*/
async function copyClaudeServerToCodex(serverName, serverConfig) {
return await addCodexMcpServer(serverName, serverConfig);
}
/**
* Copy Codex MCP server to Claude (global)
*/
async function copyCodexServerToClaude(serverName, serverConfig) {
// Convert Codex format to Claude format
const claudeConfig = {
command: serverConfig.command,
args: serverConfig.args || [],
};
if (serverConfig.env) {
claudeConfig.env = serverConfig.env;
}
// If it's an HTTP server
if (serverConfig.url) {
claudeConfig.url = serverConfig.url;
}
return await addGlobalMcpServer(serverName, claudeConfig);
}
async function toggleMcpServer(serverName, enable) {
try {
const response = await fetch('/api/mcp-toggle', {
@@ -255,7 +395,7 @@ async function removeGlobalMcpServer(serverName) {
function updateMcpBadge() {
const badge = document.getElementById('badgeMcpServers');
if (badge) {
const currentPath = projectPath.replace(/\//g, '\\');
const currentPath = projectPath; // Keep original format (forward slash)
const projectData = mcpAllProjects[currentPath];
const servers = projectData?.mcpServers || {};
const disabledServers = projectData?.disabledMcpServers || [];
@@ -702,7 +842,20 @@ async function createMcpServerWithConfig(name, serverConfig, scope = 'project')
// Submit to API
try {
let response;
if (scope === 'global') {
let scopeLabel;
if (scope === 'codex') {
// Create in Codex config.toml
response = await fetch('/api/codex-mcp-add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
serverName: name,
serverConfig: serverConfig
})
});
scopeLabel = 'Codex';
} else if (scope === 'global') {
response = await fetch('/api/mcp-add-global-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -711,6 +864,7 @@ async function createMcpServerWithConfig(name, serverConfig, scope = 'project')
serverConfig: serverConfig
})
});
scopeLabel = 'global';
} else {
response = await fetch('/api/mcp-copy-server', {
method: 'POST',
@@ -721,6 +875,7 @@ async function createMcpServerWithConfig(name, serverConfig, scope = 'project')
serverConfig: serverConfig
})
});
scopeLabel = 'project';
}
if (!response.ok) throw new Error('Failed to create MCP server');
@@ -730,7 +885,6 @@ async function createMcpServerWithConfig(name, serverConfig, scope = 'project')
closeMcpCreateModal();
await loadMcpConfig();
renderMcpManager();
const scopeLabel = scope === 'global' ? 'global' : 'project';
showRefreshToast(`MCP server "${name}" created in ${scopeLabel} scope`, 'success');
} else {
showRefreshToast(result.error || 'Failed to create MCP server', 'error');
@@ -787,7 +941,7 @@ function buildCcwToolsConfig(selectedTools) {
return config;
}
async function installCcwToolsMcp() {
async function installCcwToolsMcp(scope = 'workspace') {
const selectedTools = getSelectedCcwTools();
if (selectedTools.length === 0) {
@@ -798,27 +952,52 @@ async function installCcwToolsMcp() {
const ccwToolsConfig = buildCcwToolsConfig(selectedTools);
try {
showRefreshToast('Installing CCW Tools MCP...', 'info');
const scopeLabel = scope === 'global' ? 'globally' : 'to workspace';
showRefreshToast(`Installing CCW Tools MCP ${scopeLabel}...`, 'info');
const response = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectPath: projectPath,
serverName: 'ccw-tools',
serverConfig: ccwToolsConfig
})
});
if (scope === 'global') {
// Install to global (~/.claude.json mcpServers)
const response = await fetch('/api/mcp-add-global', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
serverName: 'ccw-tools',
serverConfig: ccwToolsConfig
})
});
if (!response.ok) throw new Error('Failed to install CCW Tools MCP');
if (!response.ok) throw new Error('Failed to install CCW Tools MCP globally');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`CCW Tools installed (${selectedTools.length} tools)`, 'success');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`CCW Tools installed globally (${selectedTools.length} tools)`, 'success');
} else {
showRefreshToast(result.error || 'Failed to install CCW Tools MCP globally', 'error');
}
} else {
showRefreshToast(result.error || 'Failed to install CCW Tools MCP', 'error');
// Install to workspace (.mcp.json)
const response = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectPath: projectPath,
serverName: 'ccw-tools',
serverConfig: ccwToolsConfig
})
});
if (!response.ok) throw new Error('Failed to install CCW Tools MCP to workspace');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`CCW Tools installed to workspace (${selectedTools.length} tools)`, 'success');
} else {
showRefreshToast(result.error || 'Failed to install CCW Tools MCP to workspace', 'error');
}
}
} catch (err) {
console.error('Failed to install CCW Tools MCP:', err);
@@ -826,7 +1005,7 @@ async function installCcwToolsMcp() {
}
}
async function updateCcwToolsMcp() {
async function updateCcwToolsMcp(scope = 'workspace') {
const selectedTools = getSelectedCcwTools();
if (selectedTools.length === 0) {
@@ -837,27 +1016,52 @@ async function updateCcwToolsMcp() {
const ccwToolsConfig = buildCcwToolsConfig(selectedTools);
try {
showRefreshToast('Updating CCW Tools MCP...', 'info');
const scopeLabel = scope === 'global' ? 'globally' : 'in workspace';
showRefreshToast(`Updating CCW Tools MCP ${scopeLabel}...`, 'info');
const response = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectPath: projectPath,
serverName: 'ccw-tools',
serverConfig: ccwToolsConfig
})
});
if (scope === 'global') {
// Update global (~/.claude.json mcpServers)
const response = await fetch('/api/mcp-add-global', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
serverName: 'ccw-tools',
serverConfig: ccwToolsConfig
})
});
if (!response.ok) throw new Error('Failed to update CCW Tools MCP');
if (!response.ok) throw new Error('Failed to update CCW Tools MCP globally');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`CCW Tools updated (${selectedTools.length} tools)`, 'success');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`CCW Tools updated globally (${selectedTools.length} tools)`, 'success');
} else {
showRefreshToast(result.error || 'Failed to update CCW Tools MCP globally', 'error');
}
} else {
showRefreshToast(result.error || 'Failed to update CCW Tools MCP', 'error');
// Update workspace (.mcp.json)
const response = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectPath: projectPath,
serverName: 'ccw-tools',
serverConfig: ccwToolsConfig
})
});
if (!response.ok) throw new Error('Failed to update CCW Tools MCP in workspace');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`CCW Tools updated in workspace (${selectedTools.length} tools)`, 'success');
} else {
showRefreshToast(result.error || 'Failed to update CCW Tools MCP in workspace', 'error');
}
}
} catch (err) {
console.error('Failed to update CCW Tools MCP:', err);

View File

@@ -96,7 +96,7 @@ function renderImplPlanContent(implPlan) {
// Lite Context Tab Rendering
// ==========================================
function renderLiteContextContent(context, explorations, session) {
function renderLiteContextContent(context, explorations, session, diagnoses) {
const plan = session.plan || {};
let sections = [];
@@ -105,6 +105,11 @@ function renderLiteContextContent(context, explorations, session) {
sections.push(renderExplorationContext(explorations));
}
// Render diagnoses if available (from diagnosis-*.json files)
if (diagnoses && diagnoses.manifest) {
sections.push(renderDiagnosisContext(diagnoses));
}
// If we have context from context-package.json
if (context) {
sections.push(`
@@ -153,7 +158,7 @@ function renderLiteContextContent(context, explorations, session) {
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="package" class="w-12 h-12"></i></div>
<div class="empty-title">No Context Data</div>
<div class="empty-text">No context-package.json or exploration files found for this session.</div>
<div class="empty-text">No context-package.json, exploration files, or diagnosis files found for this session.</div>
</div>
`;
}
@@ -185,15 +190,19 @@ function renderExplorationContext(explorations) {
`);
// Render each exploration angle as collapsible section
const explorationOrder = ['architecture', 'dependencies', 'patterns', 'integration-points'];
const explorationOrder = ['architecture', 'dependencies', 'patterns', 'integration-points', 'testing'];
const explorationTitles = {
'architecture': '<i data-lucide="blocks" class="w-4 h-4 inline mr-1"></i>Architecture',
'dependencies': '<i data-lucide="package" class="w-4 h-4 inline mr-1"></i>Dependencies',
'patterns': '<i data-lucide="git-branch" class="w-4 h-4 inline mr-1"></i>Patterns',
'integration-points': '<i data-lucide="plug" class="w-4 h-4 inline mr-1"></i>Integration Points'
'integration-points': '<i data-lucide="plug" class="w-4 h-4 inline mr-1"></i>Integration Points',
'testing': '<i data-lucide="flask-conical" class="w-4 h-4 inline mr-1"></i>Testing'
};
for (const angle of explorationOrder) {
// Collect all angles from data (in case there are exploration angles not in our predefined list)
const allAngles = [...new Set([...explorationOrder, ...Object.keys(data)])];
for (const angle of allAngles) {
const expData = data[angle];
if (!expData) {
continue;
@@ -205,7 +214,7 @@ function renderExplorationContext(explorations) {
<div class="exploration-section collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">${explorationTitles[angle] || angle}</span>
<span class="section-label">${explorationTitles[angle] || ('<i data-lucide="file-search" class="w-4 h-4 inline mr-1"></i>' + escapeHtml(angle.toUpperCase()))}</span>
</div>
<div class="collapsible-content collapsed">
${angleContent}
@@ -271,3 +280,145 @@ function renderExplorationAngle(angle, data) {
return content.join('') || '<p>No data available</p>';
}
// ==========================================
// Diagnosis Context Rendering
// ==========================================
function renderDiagnosisContext(diagnoses) {
if (!diagnoses || !diagnoses.manifest) {
return '';
}
const manifest = diagnoses.manifest;
const data = diagnoses.data || {};
let sections = [];
// Header with manifest info
sections.push(`
<div class="diagnosis-header">
<h4><i data-lucide="stethoscope" class="w-4 h-4 inline mr-1"></i> ${escapeHtml(manifest.task_description || 'Diagnosis Context')}</h4>
<div class="diagnosis-meta">
<span class="meta-item">Diagnoses: <strong>${manifest.diagnosis_count || 0}</strong></span>
</div>
</div>
`);
// Render each diagnosis angle as collapsible section
const diagnosisOrder = ['root-cause', 'api-contracts', 'dataflow', 'performance', 'security', 'error-handling'];
const diagnosisTitles = {
'root-cause': '<i data-lucide="search" class="w-4 h-4 inline mr-1"></i>Root Cause',
'api-contracts': '<i data-lucide="plug" class="w-4 h-4 inline mr-1"></i>API Contracts',
'dataflow': '<i data-lucide="git-merge" class="w-4 h-4 inline mr-1"></i>Data Flow',
'performance': '<i data-lucide="zap" class="w-4 h-4 inline mr-1"></i>Performance',
'security': '<i data-lucide="shield" class="w-4 h-4 inline mr-1"></i>Security',
'error-handling': '<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i>Error Handling'
};
// Collect all angles from data (in case there are diagnosis angles not in our predefined list)
const allAngles = [...new Set([...diagnosisOrder, ...Object.keys(data)])];
for (const angle of allAngles) {
const diagData = data[angle];
if (!diagData) {
continue;
}
const angleContent = renderDiagnosisAngle(angle, diagData);
sections.push(`
<div class="diagnosis-section collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">${diagnosisTitles[angle] || ('<i data-lucide="file-search" class="w-4 h-4 inline mr-1"></i>' + angle)}</span>
</div>
<div class="collapsible-content collapsed">
${angleContent}
</div>
</div>
`);
}
return `<div class="diagnosis-context">${sections.join('')}</div>`;
}
function renderDiagnosisAngle(angle, data) {
let content = [];
// Summary/Overview
if (data.summary || data.overview) {
content.push(renderExpField('Summary', data.summary || data.overview));
}
// Root cause analysis
if (data.root_cause || data.root_cause_analysis) {
content.push(renderExpField('Root Cause', data.root_cause || data.root_cause_analysis));
}
// Issues/Findings
if (data.issues && Array.isArray(data.issues)) {
content.push(`
<div class="exp-field">
<label>Issues Found (${data.issues.length})</label>
<div class="issues-list">
${data.issues.map(issue => {
if (typeof issue === 'string') {
return `<div class="issue-item">${escapeHtml(issue)}</div>`;
} else {
return `
<div class="issue-item">
<div class="issue-title">${escapeHtml(issue.title || issue.description || 'Unknown')}</div>
${issue.location ? `<div class="issue-location"><code>${escapeHtml(issue.location)}</code></div>` : ''}
${issue.severity ? `<span class="severity-badge ${escapeHtml(issue.severity)}">${escapeHtml(issue.severity)}</span>` : ''}
</div>
`;
}
}).join('')}
</div>
</div>
`);
}
// Affected files
if (data.affected_files && Array.isArray(data.affected_files)) {
content.push(`
<div class="exp-field">
<label>Affected Files (${data.affected_files.length})</label>
<div class="path-tags">
${data.affected_files.map(f => {
const filePath = typeof f === 'string' ? f : (f.path || f.file || '');
return `<span class="path-tag">${escapeHtml(filePath)}</span>`;
}).join('')}
</div>
</div>
`);
}
// Recommendations
if (data.recommendations && Array.isArray(data.recommendations)) {
content.push(`
<div class="exp-field">
<label>Recommendations</label>
<ol class="recommendations-list">
${data.recommendations.map(rec => {
const recText = typeof rec === 'string' ? rec : (rec.description || rec.action || '');
return `<li>${escapeHtml(recText)}</li>`;
}).join('')}
</ol>
</div>
`);
}
// API Contracts (specific to api-contracts diagnosis)
if (data.contracts && Array.isArray(data.contracts)) {
content.push(renderExpField('API Contracts', data.contracts));
}
// Data flow (specific to dataflow diagnosis)
if (data.dataflow || data.data_flow) {
content.push(renderExpField('Data Flow', data.dataflow || data.data_flow));
}
return content.join('') || '<p>No diagnosis data available</p>';
}

View File

@@ -378,9 +378,11 @@ const i18n = {
'mcp.newProjectServer': 'New Project Server',
'mcp.newServer': 'New Server',
'mcp.newGlobalServer': 'New Global Server',
'mcp.copyInstallCmd': 'Copy Install Command',
'mcp.installCmdCopied': 'Install command copied to clipboard',
'mcp.installCmdFailed': 'Failed to copy install command',
'mcp.installToProject': 'Install to Project',
'mcp.installToGlobal': 'Install to Global',
'mcp.installToWorkspace': 'Install to Workspace',
'mcp.updateInWorkspace': 'Update in Workspace',
'mcp.updateInGlobal': 'Update in Global',
'mcp.serversConfigured': 'servers configured',
'mcp.serversAvailable': 'servers available',
'mcp.globalAvailable': '全局可用 MCP',
@@ -413,6 +415,26 @@ const i18n = {
'mcp.availableToAll': 'Available to all projects from ~/.claude.json',
'mcp.managedByOrg': 'Managed by organization (highest priority)',
'mcp.variables': 'variables',
'mcp.cmd': 'Command',
'mcp.url': 'URL',
'mcp.args': 'Arguments',
'mcp.env': 'Environment',
'mcp.usedInCount': 'Used in {count} project{s}',
'mcp.from': 'from',
'mcp.variant': 'variant',
'mcp.sourceEnterprise': 'Enterprise',
'mcp.sourceGlobal': 'Global',
'mcp.sourceProject': 'Project',
'mcp.viewDetails': 'View Details',
'mcp.clickToViewDetails': 'Click to view details',
// MCP Details Modal
'mcp.detailsModal.title': 'MCP Server Details',
'mcp.detailsModal.close': 'Close',
'mcp.detailsModal.serverName': 'Server Name',
'mcp.detailsModal.source': 'Source',
'mcp.detailsModal.configuration': 'Configuration',
'mcp.detailsModal.noEnv': 'No environment variables',
// MCP Create Modal
'mcp.createTitle': 'Create MCP Server',
@@ -456,6 +478,34 @@ const i18n = {
'mcp.toProject': 'To Project',
'mcp.toGlobal': 'To Global',
// MCP CLI Mode
'mcp.cliMode': 'CLI Mode',
'mcp.claudeMode': 'Claude Mode',
'mcp.codexMode': 'Codex Mode',
// Codex MCP
'mcp.codex.globalServers': 'Codex Global MCP Servers',
'mcp.codex.newServer': 'New Server',
'mcp.codex.noServers': 'No Codex MCP servers configured',
'mcp.codex.noServersHint': 'Add servers via "codex mcp add" or create one here',
'mcp.codex.infoTitle': 'About Codex MCP',
'mcp.codex.infoDesc': 'Codex MCP servers are global only (stored in ~/.codex/config.toml). Use TOML format for configuration.',
'mcp.codex.serverAdded': 'Codex MCP server "{name}" added',
'mcp.codex.addFailed': 'Failed to add Codex MCP server',
'mcp.codex.serverRemoved': 'Codex MCP server "{name}" removed',
'mcp.codex.removeFailed': 'Failed to remove Codex MCP server',
'mcp.codex.serverToggled': 'Codex MCP server "{name}" {state}',
'mcp.codex.toggleFailed': 'Failed to toggle Codex MCP server',
'mcp.codex.remove': 'Remove',
'mcp.codex.removeConfirm': 'Remove Codex MCP server "{name}"?',
'mcp.codex.copyToClaude': 'Copy to Claude',
'mcp.codex.copyToCodex': 'Copy to Codex',
'mcp.codex.copyFromClaude': 'Copy Claude Servers to Codex',
'mcp.codex.alreadyAdded': 'Already in Codex',
'mcp.codex.scopeCodex': 'Codex - Global (~/.codex/config.toml)',
'mcp.codex.enabledTools': 'Tools',
'mcp.codex.tools': 'tools enabled',
// Hook Manager
'hook.projectHooks': 'Project Hooks',
'hook.projectFile': '.claude/settings.json',
@@ -1316,9 +1366,11 @@ const i18n = {
// MCP Manager
'mcp.currentAvailable': '当前可用 MCP',
'mcp.copyInstallCmd': '复制安装命令',
'mcp.installCmdCopied': '安装命令已复制到剪贴板',
'mcp.installCmdFailed': '复制安装命令失败',
'mcp.installToProject': '安装到项目',
'mcp.installToGlobal': '安装到全局',
'mcp.installToWorkspace': '安装到工作空间',
'mcp.updateInWorkspace': '在工作空间更新',
'mcp.updateInGlobal': '在全局更新',
'mcp.projectAvailable': '当前可用 MCP',
'mcp.newServer': '新建服务器',
'mcp.newGlobalServer': '新建全局服务器',
@@ -1355,7 +1407,27 @@ const i18n = {
'mcp.availableToAll': '可用于所有项目,来自 ~/.claude.json',
'mcp.managedByOrg': '由组织管理(最高优先级)',
'mcp.variables': '个变量',
'mcp.cmd': '命令',
'mcp.url': '地址',
'mcp.args': '参数',
'mcp.env': '环境变量',
'mcp.usedInCount': '用于 {count} 个项目',
'mcp.from': '来自',
'mcp.variant': '变体',
'mcp.sourceEnterprise': '企业级',
'mcp.sourceGlobal': '全局',
'mcp.sourceProject': '项目级',
'mcp.viewDetails': '查看详情',
'mcp.clickToViewDetails': '点击查看详情',
// MCP Details Modal
'mcp.detailsModal.title': 'MCP 服务器详情',
'mcp.detailsModal.close': '关闭',
'mcp.detailsModal.serverName': '服务器名称',
'mcp.detailsModal.source': '来源',
'mcp.detailsModal.configuration': '配置',
'mcp.detailsModal.noEnv': '无环境变量',
// MCP Create Modal
'mcp.createTitle': '创建 MCP 服务器',
'mcp.form': '表单',
@@ -1375,7 +1447,35 @@ const i18n = {
'mcp.installToMcpJson': '安装到 .mcp.json推荐',
'mcp.claudeJsonDesc': '保存在根目录 .claude.json projects 字段下(共享配置)',
'mcp.mcpJsonDesc': '保存在项目 .mcp.json 文件中(推荐用于版本控制)',
// MCP CLI Mode
'mcp.cliMode': 'CLI 模式',
'mcp.claudeMode': 'Claude 模式',
'mcp.codexMode': 'Codex 模式',
// Codex MCP
'mcp.codex.globalServers': 'Codex 全局 MCP 服务器',
'mcp.codex.newServer': '新建服务器',
'mcp.codex.noServers': '未配置 Codex MCP 服务器',
'mcp.codex.noServersHint': '使用 "codex mcp add" 命令或在此处创建',
'mcp.codex.infoTitle': '关于 Codex MCP',
'mcp.codex.infoDesc': 'Codex MCP 服务器仅支持全局配置(存储在 ~/.codex/config.toml。使用 TOML 格式配置。',
'mcp.codex.serverAdded': 'Codex MCP 服务器 "{name}" 已添加',
'mcp.codex.addFailed': '添加 Codex MCP 服务器失败',
'mcp.codex.serverRemoved': 'Codex MCP 服务器 "{name}" 已移除',
'mcp.codex.removeFailed': '移除 Codex MCP 服务器失败',
'mcp.codex.serverToggled': 'Codex MCP 服务器 "{name}" 已{state}',
'mcp.codex.toggleFailed': '切换 Codex MCP 服务器失败',
'mcp.codex.remove': '移除',
'mcp.codex.removeConfirm': '移除 Codex MCP 服务器 "{name}"',
'mcp.codex.copyToClaude': '复制到 Claude',
'mcp.codex.copyToCodex': '复制到 Codex',
'mcp.codex.copyFromClaude': '从 Claude 复制服务器到 Codex',
'mcp.codex.alreadyAdded': '已在 Codex 中',
'mcp.codex.scopeCodex': 'Codex - 全局 (~/.codex/config.toml)',
'mcp.codex.enabledTools': '工具',
'mcp.codex.tools': '个工具已启用',
// Hook Manager
'hook.projectHooks': '项目钩子',
'hook.projectFile': '.claude/settings.json',

View File

@@ -140,7 +140,7 @@ function toggleSection(header) {
function initCollapsibleSections(container) {
setTimeout(() => {
const headers = container.querySelectorAll('.collapsible-header');
headers.forEach(header => {
headers.forEach((header) => {
if (!header._clickBound) {
header._clickBound = true;
header.addEventListener('click', function(e) {

View File

@@ -160,11 +160,13 @@ function showLiteTaskDetailPage(sessionKey) {
</div>
`;
// Initialize collapsible sections
// Initialize collapsible sections and task click handlers
setTimeout(() => {
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
// Bind click events to lite task items on initial load
initLiteTaskClickHandlers();
}, 50);
}
@@ -194,11 +196,13 @@ function switchLiteDetailTab(tabName) {
switch (tabName) {
case 'tasks':
contentArea.innerHTML = renderLiteTasksTab(session, tasks, completed, inProgress, pending);
// Re-initialize collapsible sections
// Re-initialize collapsible sections and task click handlers
setTimeout(() => {
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
// Bind click events to lite task items
initLiteTaskClickHandlers();
}, 50);
break;
case 'plan':
@@ -259,12 +263,16 @@ function renderLiteTaskDetailItem(sessionId, task) {
const implCount = rawTask.implementation?.length || 0;
const acceptCount = rawTask.acceptance?.length || 0;
// Escape for data attributes
const safeSessionId = escapeHtml(sessionId);
const safeTaskId = escapeHtml(task.id);
return `
<div class="detail-task-item-full lite-task-item" onclick="openTaskDrawerForLite('${sessionId}', '${escapeHtml(task.id)}')" style="cursor: pointer;" title="Click to view details">
<div class="detail-task-item-full lite-task-item" data-session-id="${safeSessionId}" data-task-id="${safeTaskId}" style="cursor: pointer;" title="Click to view details">
<div class="task-item-header-lite">
<span class="task-id-badge">${escapeHtml(task.id)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<button class="btn-view-json" onclick="event.stopPropagation(); showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
<button class="btn-view-json" data-task-json-id="${taskJsonId}" data-task-display-id="${safeTaskId}">{ } JSON</button>
</div>
<div class="task-item-meta-lite">
${action ? `<span class="meta-badge action">${escapeHtml(action)}</span>` : ''}
@@ -285,6 +293,39 @@ function getMetaPreviewForLite(task, rawTask) {
return parts.join(' | ') || 'No meta';
}
/**
* Initialize click handlers for lite task items
*/
function initLiteTaskClickHandlers() {
// Task item click handlers
document.querySelectorAll('.lite-task-item').forEach(item => {
if (!item._clickBound) {
item._clickBound = true;
item.addEventListener('click', function(e) {
// Don't trigger if clicking on JSON button
if (e.target.closest('.btn-view-json')) return;
const sessionId = this.dataset.sessionId;
const taskId = this.dataset.taskId;
openTaskDrawerForLite(sessionId, taskId);
});
}
});
// JSON button click handlers
document.querySelectorAll('.btn-view-json').forEach(btn => {
if (!btn._clickBound) {
btn._clickBound = true;
btn.addEventListener('click', function(e) {
e.stopPropagation();
const taskJsonId = this.dataset.taskJsonId;
const displayId = this.dataset.taskDisplayId;
showJsonModal(taskJsonId, displayId);
});
}
});
}
function openTaskDrawerForLite(sessionId, taskId) {
const session = liteTaskDataStore[currentSessionDetailKey];
if (!session) return;
@@ -454,15 +495,15 @@ async function loadAndRenderLiteContextTab(session, contentArea) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderLiteContextContent(data.context, data.explorations, session);
// Re-initialize collapsible sections for explorations (scoped to contentArea)
contentArea.innerHTML = renderLiteContextContent(data.context, data.explorations, session, data.diagnoses);
// Re-initialize collapsible sections for explorations and diagnoses (scoped to contentArea)
initCollapsibleSections(contentArea);
return;
}
}
// Fallback: show plan context if available
contentArea.innerHTML = renderLiteContextContent(null, null, session);
contentArea.innerHTML = renderLiteContextContent(null, null, session, null);
initCollapsibleSections(contentArea);
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load context: ${err.message}</div>`;
@@ -530,7 +571,9 @@ function renderDiagnosesTab(session) {
// Individual diagnosis items
if (diagnoses.items && diagnoses.items.length > 0) {
const diagnosisCards = diagnoses.items.map(diag => renderDiagnosisCard(diag)).join('');
const diagnosisCards = diagnoses.items.map((diag) => {
return renderDiagnosisCard(diag);
}).join('');
sections.push(`
<div class="diagnoses-items-section">
<h4 class="diagnoses-section-title"><i data-lucide="search" class="w-4 h-4 inline mr-1"></i> Diagnosis Details (${diagnoses.items.length})</h4>
@@ -565,7 +608,21 @@ function renderDiagnosisCard(diag) {
function renderDiagnosisContent(diag) {
let content = [];
// Summary/Overview
// Symptom (for detailed diagnosis structure)
if (diag.symptom) {
const symptom = diag.symptom;
content.push(`
<div class="diag-section">
<strong>Symptom:</strong>
${symptom.description ? `<p>${escapeHtml(symptom.description)}</p>` : ''}
${symptom.user_impact ? `<div class="symptom-impact"><strong>User Impact:</strong> ${escapeHtml(symptom.user_impact)}</div>` : ''}
${symptom.frequency ? `<div class="symptom-freq"><strong>Frequency:</strong> <span class="badge">${escapeHtml(symptom.frequency)}</span></div>` : ''}
${symptom.error_message ? `<div class="symptom-error"><strong>Error:</strong> <code>${escapeHtml(symptom.error_message)}</code></div>` : ''}
</div>
`);
}
// Summary/Overview (for simple diagnosis structure)
if (diag.summary || diag.overview) {
content.push(`
<div class="diag-section">
@@ -576,11 +633,34 @@ function renderDiagnosisContent(diag) {
}
// Root Cause Analysis
if (diag.root_cause || diag.root_cause_analysis) {
if (diag.root_cause) {
const rootCause = diag.root_cause;
// Handle both object and string formats
if (typeof rootCause === 'object') {
content.push(`
<div class="diag-section">
<strong>Root Cause:</strong>
${rootCause.file ? `<div class="rc-file"><strong>File:</strong> <code>${escapeHtml(rootCause.file)}</code></div>` : ''}
${rootCause.line_range ? `<div class="rc-line"><strong>Lines:</strong> ${escapeHtml(rootCause.line_range)}</div>` : ''}
${rootCause.function ? `<div class="rc-func"><strong>Function:</strong> <code>${escapeHtml(rootCause.function)}</code></div>` : ''}
${rootCause.issue ? `<p>${escapeHtml(rootCause.issue)}</p>` : ''}
${rootCause.confidence ? `<div class="rc-confidence"><strong>Confidence:</strong> ${(rootCause.confidence * 100).toFixed(0)}%</div>` : ''}
${rootCause.category ? `<div class="rc-category"><strong>Category:</strong> <span class="badge">${escapeHtml(rootCause.category)}</span></div>` : ''}
</div>
`);
} else if (typeof rootCause === 'string') {
content.push(`
<div class="diag-section">
<strong>Root Cause:</strong>
<p>${escapeHtml(rootCause)}</p>
</div>
`);
}
} else if (diag.root_cause_analysis) {
content.push(`
<div class="diag-section">
<strong>Root Cause:</strong>
<p>${escapeHtml(diag.root_cause || diag.root_cause_analysis)}</p>
<p>${escapeHtml(diag.root_cause_analysis)}</p>
</div>
`);
}
@@ -660,6 +740,37 @@ function renderDiagnosisContent(diag) {
`);
}
// Reproduction Steps
if (diag.reproduction_steps && Array.isArray(diag.reproduction_steps)) {
content.push(`
<div class="diag-section">
<strong>Reproduction Steps:</strong>
<ol class="repro-steps-list">
${diag.reproduction_steps.map(step => `<li>${escapeHtml(step)}</li>`).join('')}
</ol>
</div>
`);
}
// Fix Hints
if (diag.fix_hints && Array.isArray(diag.fix_hints)) {
content.push(`
<div class="diag-section">
<strong>Fix Hints (${diag.fix_hints.length}):</strong>
<div class="fix-hints-list">
${diag.fix_hints.map((hint, idx) => `
<div class="fix-hint-item">
<div class="hint-header"><strong>Hint ${idx + 1}:</strong> ${escapeHtml(hint.description || 'No description')}</div>
${hint.approach ? `<div class="hint-approach"><strong>Approach:</strong> ${escapeHtml(hint.approach)}</div>` : ''}
${hint.risk ? `<div class="hint-risk"><strong>Risk:</strong> <span class="badge risk-${hint.risk}">${escapeHtml(hint.risk)}</span></div>` : ''}
${hint.code_example ? `<div class="hint-code"><strong>Code Example:</strong><pre><code>${escapeHtml(hint.code_example)}</code></pre></div>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Recommendations
if (diag.recommendations && Array.isArray(diag.recommendations)) {
content.push(`
@@ -672,10 +783,75 @@ function renderDiagnosisContent(diag) {
`);
}
// If no specific content was rendered, show raw JSON preview
if (content.length === 0) {
// Dependencies
if (diag.dependencies && typeof diag.dependencies === 'string') {
content.push(`
<div class="diag-section">
<strong>Dependencies:</strong>
<p>${escapeHtml(diag.dependencies)}</p>
</div>
`);
}
// Constraints
if (diag.constraints && typeof diag.constraints === 'string') {
content.push(`
<div class="diag-section">
<strong>Constraints:</strong>
<p>${escapeHtml(diag.constraints)}</p>
</div>
`);
}
// Clarification Needs
if (diag.clarification_needs && Array.isArray(diag.clarification_needs)) {
content.push(`
<div class="diag-section">
<strong>Clarification Needs:</strong>
<div class="clarification-list">
${diag.clarification_needs.map(clar => `
<div class="clarification-item">
<div class="clar-question"><strong>Q:</strong> ${escapeHtml(clar.question)}</div>
${clar.context ? `<div class="clar-context"><strong>Context:</strong> ${escapeHtml(clar.context)}</div>` : ''}
${clar.options && Array.isArray(clar.options) ? `
<div class="clar-options">
<strong>Options:</strong>
<ul>
${clar.options.map(opt => `<li>${escapeHtml(opt)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Related Issues
if (diag.related_issues && Array.isArray(diag.related_issues)) {
content.push(`
<div class="diag-section">
<strong>Related Issues:</strong>
<ul class="related-issues-list">
${diag.related_issues.map(issue => `
<li>
${issue.type ? `<span class="issue-type-badge">${escapeHtml(issue.type)}</span>` : ''}
${issue.reference ? `<strong>${escapeHtml(issue.reference)}</strong>: ` : ''}
${issue.description ? escapeHtml(issue.description) : ''}
</li>
`).join('')}
</ul>
</div>
`);
}
// If no specific content was rendered, show raw JSON preview
if (content.length === 0) {
console.warn('[DEBUG] No content rendered for diagnosis:', diag);
content.push(`
<div class="diag-section">
<strong>Debug: Raw JSON</strong>
<pre class="json-content">${escapeHtml(JSON.stringify(diag, null, 2))}</pre>
</div>
`);

View File

@@ -17,7 +17,7 @@ const CCW_MCP_TOOLS = [
// Get currently enabled tools from installed config
function getCcwEnabledTools() {
const currentPath = projectPath.replace(/\//g, '\\');
const currentPath = projectPath; // Keep original format (forward slash)
const projectData = mcpAllProjects[currentPath] || {};
const ccwConfig = projectData.mcpServers?.['ccw-tools'];
if (ccwConfig?.env?.CCW_ENABLED_TOOLS) {
@@ -46,7 +46,7 @@ async function renderMcpManager() {
// Load MCP templates
await loadMcpTemplates();
const currentPath = projectPath.replace(/\//g, '\\');
const currentPath = projectPath; // Keep original format (forward slash)
const projectData = mcpAllProjects[currentPath] || {};
const projectServers = projectData.mcpServers || {};
const disabledServers = projectData.disabledMcpServers || [];
@@ -121,8 +121,136 @@ async function renderMcpManager() {
const isCcwToolsInstalled = currentProjectServerNames.includes("ccw-tools");
const enabledTools = getCcwEnabledTools();
// Prepare Codex servers data
const codexServerEntries = Object.entries(codexMcpServers || {});
const codexConfigExists = codexMcpConfig?.exists || false;
const codexConfigPath = codexMcpConfig?.configPath || '~/.codex/config.toml';
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 ${currentCliMode === 'claude' ? '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 ${currentCliMode === 'codex' ? 'bg-orange-500 text-white shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
onclick="setCliMode('codex')">
<i data-lucide="code-2" class="w-4 h-4 inline mr-1.5"></i>
Codex
</button>
</div>
</div>
<div class="text-xs text-muted-foreground">
${currentCliMode === 'claude'
? `<span class="flex items-center gap-1"><i data-lucide="file-json" class="w-3 h-3"></i> ~/.claude.json</span>`
: `<span class="flex items-center gap-1"><i data-lucide="file-code" class="w-3 h-3"></i> ${codexConfigPath}</span>`
}
</div>
</div>
</div>
${currentCliMode === 'codex' ? `
<!-- Codex MCP Servers Section -->
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<i data-lucide="code-2" class="w-5 h-5 text-orange-500"></i>
<h3 class="text-lg font-semibold text-foreground">${t('mcp.codex.globalServers')}</h3>
</div>
<button class="px-3 py-1.5 text-sm bg-orange-500 text-white rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="openCodexMcpCreateModal()">
<span>+</span> ${t('mcp.codex.newServer')}
</button>
${codexConfigExists ? `
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs bg-success/10 text-success rounded-md border border-success/20">
<i data-lucide="file-check" class="w-3.5 h-3.5"></i>
config.toml
</span>
` : `
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs bg-muted text-muted-foreground rounded-md border border-border" title="Will create ~/.codex/config.toml">
<i data-lucide="file-plus" class="w-3.5 h-3.5"></i>
Will create config.toml
</span>
`}
</div>
<span class="text-sm text-muted-foreground">${codexServerEntries.length} ${t('mcp.serversAvailable')}</span>
</div>
<!-- Info about Codex MCP -->
<div class="bg-orange-50 dark:bg-orange-950/30 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<i data-lucide="info" class="w-5 h-5 text-orange-500 shrink-0 mt-0.5"></i>
<div class="text-sm">
<p class="text-orange-800 dark:text-orange-200 font-medium mb-1">${t('mcp.codex.infoTitle')}</p>
<p class="text-orange-700 dark:text-orange-300 text-xs">${t('mcp.codex.infoDesc')}</p>
</div>
</div>
</div>
${codexServerEntries.length === 0 ? `
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
<div class="text-muted-foreground mb-3"><i data-lucide="plug" class="w-10 h-10 mx-auto"></i></div>
<p class="text-muted-foreground">${t('mcp.codex.noServers')}</p>
<p class="text-sm text-muted-foreground mt-1">${t('mcp.codex.noServersHint')}</p>
</div>
` : `
<div class="mcp-server-grid grid gap-3">
${codexServerEntries.map(([serverName, serverConfig]) => {
return renderCodexServerCard(serverName, serverConfig);
}).join('')}
</div>
`}
</div>
<!-- Copy Claude Servers to Codex -->
${Object.keys(mcpUserServers || {}).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="copy" class="w-5 h-5"></i>
${t('mcp.codex.copyFromClaude')}
</h3>
<span class="text-sm text-muted-foreground">${Object.keys(mcpUserServers || {}).length} ${t('mcp.serversAvailable')}</span>
</div>
<div class="mcp-server-grid grid gap-3">
${Object.entries(mcpUserServers || {}).map(([serverName, serverConfig]) => {
const alreadyInCodex = codexMcpServers && codexMcpServers[serverName];
return `
<div class="mcp-server-card bg-card border ${alreadyInCodex ? 'border-success/50' : 'border-border'} border-dashed rounded-lg p-4 hover:shadow-md transition-all">
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-2">
<i data-lucide="bot" class="w-5 h-5 text-primary"></i>
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
${alreadyInCodex ? `<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">${t('mcp.codex.alreadyAdded')}</span>` : ''}
</div>
${!alreadyInCodex ? `
<button class="px-3 py-1 text-xs bg-orange-500 text-white rounded hover:opacity-90 transition-opacity"
onclick="copyClaudeServerToCodex('${escapeHtml(serverName)}', ${JSON.stringify(serverConfig).replace(/'/g, "&#39;")})"
title="${t('mcp.codex.copyToCodex')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
</button>
` : ''}
</div>
<div class="mcp-server-details text-sm space-y-1">
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.cmd')}</span>
<span class="truncate" title="${escapeHtml(serverConfig.command || 'N/A')}">${escapeHtml(serverConfig.command || 'N/A')}</span>
</div>
</div>
</div>
`;
}).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">
@@ -164,17 +292,32 @@ async function renderMcpManager() {
</div>
</div>
</div>
<div class="shrink-0">
<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"
onclick="updateCcwToolsMcp()">
Update
<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"
onclick="updateCcwToolsMcp('workspace')"
title="${t('mcp.updateInWorkspace')}">
<i data-lucide="folder" class="w-4 h-4"></i>
${t('mcp.updateInWorkspace')}
</button>
<button class="px-4 py-2 text-sm bg-success text-success-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="updateCcwToolsMcp('global')"
title="${t('mcp.updateInGlobal')}">
<i data-lucide="globe" class="w-4 h-4"></i>
${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-2"
onclick="installCcwToolsMcp()">
<i data-lucide="download" class="w-4 h-4"></i>
Install
<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"
onclick="installCcwToolsMcp('workspace')"
title="${t('mcp.installToWorkspace')}">
<i data-lucide="folder" class="w-4 h-4"></i>
${t('mcp.installToWorkspace')}
</button>
<button class="px-4 py-2 text-sm bg-success text-success-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="installCcwToolsMcp('global')"
title="${t('mcp.installToGlobal')}">
<i data-lucide="globe" class="w-4 h-4"></i>
${t('mcp.installToGlobal')}
</button>
`}
</div>
@@ -300,12 +443,12 @@ async function renderMcpManager() {
<div class="mcp-server-details text-sm space-y-1 mb-3">
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">cmd</span>
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.cmd')}</span>
<span class="truncate text-xs" title="${escapeHtml(template.serverConfig.command)}">${escapeHtml(template.serverConfig.command)}</span>
</div>
${template.serverConfig.args && template.serverConfig.args.length > 0 ? `
<div class="flex items-start gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
<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(template.serverConfig.args.join(' '))}">${escapeHtml(template.serverConfig.args.slice(0, 2).join(' '))}${template.serverConfig.args.length > 2 ? '...' : ''}</span>
</div>
` : ''}
@@ -343,7 +486,8 @@ async function renderMcpManager() {
</div>
` : ''}
<!-- All Projects MCP Overview Table -->
<!-- All Projects MCP Overview Table (Claude mode only) -->
${currentCliMode === 'claude' ? `
<div class="mcp-section mt-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-foreground">${t('mcp.allProjects')}</h3>
@@ -411,6 +555,25 @@ async function renderMcpManager() {
</table>
</div>
</div>
` : ''}
<!-- MCP Server Details Modal -->
<div id="mcpDetailsModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
<div class="bg-card border border-border rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-hidden flex flex-col">
<!-- Modal Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
<h2 class="text-lg font-semibold text-foreground">${t('mcp.detailsModal.title')}</h2>
<button id="mcpDetailsModalClose" class="text-muted-foreground hover:text-foreground transition-colors">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<!-- Modal Body -->
<div id="mcpDetailsModalBody" class="px-6 py-4 overflow-y-auto flex-1">
<!-- Content will be dynamically filled -->
</div>
</div>
</div>
</div>
`;
@@ -431,15 +594,20 @@ function renderProjectAvailableServerCard(entry) {
// Source badge
let sourceBadge = '';
if (source === 'enterprise') {
sourceBadge = '<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Enterprise</span>';
sourceBadge = `<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">${t('mcp.sourceEnterprise')}</span>`;
} else if (source === 'global') {
sourceBadge = '<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">Global</span>';
sourceBadge = `<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">${t('mcp.sourceGlobal')}</span>`;
} else if (source === 'project') {
sourceBadge = '<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Project</span>';
sourceBadge = `<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">${t('mcp.sourceProject')}</span>`;
}
return `
<div class="mcp-server-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${canToggle && !isEnabled ? 'opacity-60' : ''}">
<div class="mcp-server-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all cursor-pointer ${canToggle && !isEnabled ? 'opacity-60' : ''}"
data-server-name="${escapeHtml(name)}"
data-server-config="${escapeHtml(JSON.stringify(config))}"
data-server-source="${source}"
data-action="view-details"
title="${t('mcp.clickToViewDetails')}">
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-2">
<span>${canToggle && isEnabled ? '<i data-lucide="check-circle" class="w-5 h-5 text-success"></i>' : '<i data-lucide="circle" class="w-5 h-5 text-muted-foreground"></i>'}</span>
@@ -447,7 +615,7 @@ function renderProjectAvailableServerCard(entry) {
${sourceBadge}
</div>
${canToggle ? `
<label class="mcp-toggle relative inline-flex items-center cursor-pointer">
<label class="mcp-toggle relative inline-flex items-center cursor-pointer" onclick="event.stopPropagation()">
<input type="checkbox" class="sr-only peer"
${isEnabled ? 'checked' : ''}
data-server-name="${escapeHtml(name)}"
@@ -459,33 +627,25 @@ function renderProjectAvailableServerCard(entry) {
<div class="mcp-server-details text-sm space-y-1">
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">cmd</span>
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.cmd')}</span>
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
</div>
${args.length > 0 ? `
<div class="flex items-start gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
<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>
` : ''}
${hasEnv ? `
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
<span class="text-xs">${Object.keys(config.env).length} variables</span>
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.env')}</span>
<span class="text-xs">${Object.keys(config.env).length} ${t('mcp.variables')}</span>
</div>
` : ''}
</div>
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between gap-2">
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between gap-2" onclick="event.stopPropagation()">
<div class="flex items-center gap-2">
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
data-server-name="${escapeHtml(name)}"
data-server-config="${escapeHtml(JSON.stringify(config))}"
data-scope="${source === 'global' ? 'global' : 'project'}"
data-action="copy-install-cmd">
<i data-lucide="copy" class="w-3 h-3"></i>
${t('mcp.copyInstallCmd')}
</button>
<button class="text-xs text-success hover:text-success/80 transition-colors flex items-center gap-1"
data-server-name="${escapeHtml(name)}"
data-server-config="${escapeHtml(JSON.stringify(config))}"
@@ -525,19 +685,19 @@ function renderGlobalManagementCard(serverName, serverConfig) {
<div class="mcp-server-details text-sm space-y-1">
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? t('mcp.cmd') : t('mcp.url')}</span>
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
</div>
${args.length > 0 ? `
<div class="flex items-start gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
<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>
` : ''}
${hasEnv ? `
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.env')}</span>
<span class="text-xs">${Object.keys(serverConfig.env).length} ${t('mcp.variables')}</span>
</div>
` : ''}
<div class="flex items-center gap-2 text-muted-foreground mt-1">
@@ -545,15 +705,7 @@ function renderGlobalManagementCard(serverName, serverConfig) {
</div>
</div>
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between">
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
data-server-name="${escapeHtml(serverName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
data-scope="global"
data-action="copy-install-cmd">
<i data-lucide="copy" class="w-3 h-3"></i>
${t('mcp.copyInstallCmd')}
</button>
<div class="mt-3 pt-3 border-t border-border flex items-center justify-end">
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
data-server-name="${escapeHtml(serverName)}"
data-action="remove-global">
@@ -617,35 +769,162 @@ function renderAvailableServerCard(serverName, serverInfo) {
<div class="mcp-server-details text-sm space-y-1">
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">cmd</span>
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.cmd')}</span>
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
</div>
${argsPreview ? `
<div class="flex items-start gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
<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(argsPreview)}</span>
</div>
` : ''}
<div class="flex items-center gap-2 text-muted-foreground">
<span class="text-xs">Used in ${usedIn.length} project${usedIn.length !== 1 ? 's' : ''}</span>
${sourceProjectName ? `<span class="text-xs text-muted-foreground/70">• from ${escapeHtml(sourceProjectName)}</span>` : ''}
<span class="text-xs">${t('mcp.usedInCount').replace('{count}', usedIn.length).replace('{s}', usedIn.length !== 1 ? 's' : '')}</span>
${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">
<div class="mt-3 pt-3 border-t border-border flex items-center gap-2">
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
data-server-name="${escapeHtml(originalName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
data-scope="project"
data-action="copy-install-cmd">
<i data-lucide="copy" class="w-3 h-3"></i>
${t('mcp.copyInstallCmd')}
data-action="install-to-project"
title="${t('mcp.installToProject')}">
<i data-lucide="download" class="w-3 h-3"></i>
${t('mcp.installToProject')}
</button>
<button class="text-xs text-success hover:text-success/80 transition-colors flex items-center gap-1"
data-server-name="${escapeHtml(originalName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
data-action="install-to-global"
title="${t('mcp.installToGlobal')}">
<i data-lucide="globe" class="w-3 h-3"></i>
${t('mcp.installToGlobal')}
</button>
</div>
</div>
`;
}
// ========================================
// Codex MCP Server Card Renderer
// ========================================
function renderCodexServerCard(serverName, serverConfig) {
const isStdio = !!serverConfig.command;
const isHttp = !!serverConfig.url;
const isEnabled = serverConfig.enabled !== false; // Default to enabled
const command = serverConfig.command || serverConfig.url || 'N/A';
const args = serverConfig.args || [];
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
// Server type badge
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-orange-200 dark:border-orange-800 rounded-lg p-4 hover:shadow-md transition-all ${!isEnabled ? 'opacity-60' : ''}"
data-server-name="${escapeHtml(serverName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
data-cli-type="codex">
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-2">
<span>${isEnabled ? '<i data-lucide="check-circle" class="w-5 h-5 text-orange-500"></i>' : '<i data-lucide="circle" class="w-5 h-5 text-muted-foreground"></i>'}</span>
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
${typeBadge}
</div>
<label class="mcp-toggle relative inline-flex items-center cursor-pointer" onclick="event.stopPropagation()">
<input type="checkbox" class="sr-only peer"
${isEnabled ? 'checked' : ''}
data-server-name="${escapeHtml(serverName)}"
data-action="toggle-codex">
<div class="w-9 h-5 bg-hover peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-orange-500"></div>
</label>
</div>
<div class="mcp-server-details text-sm space-y-1">
<div class="flex items-center gap-2 text-muted-foreground">
<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" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
</div>
${args.length > 0 ? `
<div class="flex items-start gap-2 text-muted-foreground">
<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>
` : ''}
${hasEnv ? `
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.env')}</span>
<span class="text-xs">${Object.keys(serverConfig.env).length} ${t('mcp.variables')}</span>
</div>
` : ''}
${serverConfig.enabled_tools ? `
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.codex.enabledTools')}</span>
<span class="text-xs">${serverConfig.enabled_tools.length} ${t('mcp.codex.tools')}</span>
</div>
` : ''}
</div>
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between gap-2" onclick="event.stopPropagation()">
<div class="flex items-center gap-2">
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
onclick="copyCodexServerToClaude('${escapeHtml(serverName)}', ${JSON.stringify(serverConfig).replace(/'/g, "&#39;")})"
title="${t('mcp.codex.copyToClaude')}">
<i data-lucide="copy" class="w-3 h-3"></i>
${t('mcp.codex.copyToClaude')}
</button>
</div>
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
data-server-name="${escapeHtml(serverName)}"
data-action="remove-codex">
${t('mcp.codex.remove')}
</button>
</div>
</div>
`;
}
// ========================================
// Codex MCP Create Modal
// ========================================
function openCodexMcpCreateModal() {
// Reuse the existing modal with different settings
const modal = document.getElementById('mcpCreateModal');
if (modal) {
modal.classList.remove('hidden');
// Reset to form mode
mcpCreateMode = 'form';
switchMcpCreateTab('form');
// Clear form
document.getElementById('mcpServerName').value = '';
document.getElementById('mcpServerCommand').value = '';
document.getElementById('mcpServerArgs').value = '';
document.getElementById('mcpServerEnv').value = '';
// Clear JSON input
document.getElementById('mcpServerJson').value = '';
document.getElementById('mcpJsonPreview').classList.add('hidden');
// Set scope to codex
const scopeSelect = document.getElementById('mcpServerScope');
if (scopeSelect) {
// Add codex option if not exists
if (!scopeSelect.querySelector('option[value="codex"]')) {
const codexOption = document.createElement('option');
codexOption.value = 'codex';
codexOption.textContent = t('mcp.codex.scopeCodex');
scopeSelect.appendChild(codexOption);
}
scopeSelect.value = 'codex';
}
// Focus on name input
document.getElementById('mcpServerName').focus();
// Setup JSON input listener
setupMcpJsonListener();
}
}
function attachMcpEventListeners() {
// Toggle switches
@@ -692,13 +971,21 @@ function attachMcpEventListeners() {
});
});
// Copy install command buttons
document.querySelectorAll('.mcp-server-card button[data-action="copy-install-cmd"]').forEach(btn => {
// Install to project buttons
document.querySelectorAll('.mcp-server-card button[data-action="install-to-project"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
const serverName = btn.dataset.serverName;
const serverConfig = JSON.parse(btn.dataset.serverConfig);
const scope = btn.dataset.scope || 'project';
await copyMcpInstallCommand(serverName, serverConfig, scope);
await installMcpToProject(serverName, serverConfig);
});
});
// Install to global buttons
document.querySelectorAll('.mcp-server-card button[data-action="install-to-global"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
const serverName = btn.dataset.serverName;
const serverConfig = JSON.parse(btn.dataset.serverConfig);
await addGlobalMcpServer(serverName, serverConfig);
});
});
@@ -729,6 +1016,142 @@ function attachMcpEventListeners() {
}
});
});
// ========================================
// Codex MCP Event Listeners
// ========================================
// Toggle Codex MCP servers
document.querySelectorAll('.mcp-server-card input[data-action="toggle-codex"]').forEach(input => {
input.addEventListener('change', async (e) => {
const serverName = e.target.dataset.serverName;
const enable = e.target.checked;
await toggleCodexMcpServer(serverName, enable);
});
});
// Remove Codex MCP servers
document.querySelectorAll('.mcp-server-card button[data-action="remove-codex"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
const serverName = btn.dataset.serverName;
if (confirm(t('mcp.codex.removeConfirm', { name: serverName }))) {
await removeCodexMcpServer(serverName);
}
});
});
// View details - click on server card
document.querySelectorAll('.mcp-server-card[data-action="view-details"]').forEach(card => {
card.addEventListener('click', (e) => {
const serverName = card.dataset.serverName;
const serverConfig = JSON.parse(card.dataset.serverConfig);
const serverSource = card.dataset.serverSource;
showMcpDetails(serverName, serverConfig, serverSource);
});
});
// Modal close button
const closeBtn = document.getElementById('mcpDetailsModalClose');
const modal = document.getElementById('mcpDetailsModal');
if (closeBtn && modal) {
closeBtn.addEventListener('click', () => {
modal.classList.add('hidden');
});
// Close on background click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.add('hidden');
}
});
}
}
// ========================================
// MCP Details Modal
// ========================================
function showMcpDetails(serverName, serverConfig, serverSource) {
const modal = document.getElementById('mcpDetailsModal');
const modalBody = document.getElementById('mcpDetailsModalBody');
if (!modal || !modalBody) return;
// Build source badge
let sourceBadge = '';
if (serverSource === 'enterprise') {
sourceBadge = `<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-warning/20 text-warning">${t('mcp.sourceEnterprise')}</span>`;
} else if (serverSource === 'global') {
sourceBadge = `<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-success/10 text-success">${t('mcp.sourceGlobal')}</span>`;
} else if (serverSource === 'project') {
sourceBadge = `<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-primary/10 text-primary">${t('mcp.sourceProject')}</span>`;
}
// Build environment variables display
let envHtml = '';
if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
envHtml = '<div class="mt-4"><h4 class="font-semibold text-sm text-foreground mb-2">' + t('mcp.env') + '</h4><div class="bg-muted rounded-lg p-3 space-y-1 font-mono text-xs">';
for (const [key, value] of Object.entries(serverConfig.env)) {
envHtml += `<div class="flex items-start gap-2"><span class="text-muted-foreground shrink-0">${escapeHtml(key)}:</span><span class="text-foreground break-all">${escapeHtml(value)}</span></div>`;
}
envHtml += '</div></div>';
} else {
envHtml = '<div class="mt-4"><h4 class="font-semibold text-sm text-foreground mb-2">' + t('mcp.env') + '</h4><p class="text-sm text-muted-foreground">' + t('mcp.detailsModal.noEnv') + '</p></div>';
}
modalBody.innerHTML = `
<div class="space-y-4">
<!-- Server Name and Source -->
<div>
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide">${t('mcp.detailsModal.serverName')}</label>
<div class="mt-1 flex items-center gap-2">
<h3 class="text-xl font-bold text-foreground">${escapeHtml(serverName)}</h3>
${sourceBadge}
</div>
</div>
<!-- Configuration -->
<div>
<h4 class="font-semibold text-sm text-foreground mb-2">${t('mcp.detailsModal.configuration')}</h4>
<div class="space-y-2">
<!-- Command -->
<div class="flex items-start gap-3">
<span class="font-mono text-xs bg-muted px-2 py-1 rounded shrink-0">${t('mcp.cmd')}</span>
<code class="text-sm font-mono text-foreground break-all">${escapeHtml(serverConfig.command || serverConfig.url || 'N/A')}</code>
</div>
<!-- Arguments -->
${serverConfig.args && serverConfig.args.length > 0 ? `
<div class="flex items-start gap-3">
<span class="font-mono text-xs bg-muted px-2 py-1 rounded shrink-0">${t('mcp.args')}</span>
<div class="flex-1 space-y-1">
${serverConfig.args.map((arg, index) => `
<div class="text-sm font-mono text-foreground flex items-center gap-2">
<span class="text-muted-foreground">[${index}]</span>
<code class="break-all">${escapeHtml(arg)}</code>
</div>
`).join('')}
</div>
</div>
` : ''}
</div>
</div>
<!-- Environment Variables -->
${envHtml}
<!-- Raw JSON -->
<div>
<h4 class="font-semibold text-sm text-foreground mb-2">Raw JSON</h4>
<pre class="bg-muted rounded-lg p-3 text-xs font-mono overflow-x-auto">${escapeHtml(JSON.stringify(serverConfig, null, 2))}</pre>
</div>
</div>
`;
// Show modal
modal.classList.remove('hidden');
// Re-initialize Lucide icons in modal
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// ========================================
@@ -788,15 +1211,15 @@ async function saveMcpAsTemplate(serverName, serverConfig) {
const data = await response.json();
if (data.success) {
showNotification(t('mcp.templateSaved', { name: templateName }), 'success');
showRefreshToast(t('mcp.templateSaved', { name: templateName }), 'success');
await loadMcpTemplates();
await renderMcpManager(); // Refresh view
} else {
showNotification(t('mcp.templateSaveFailed', { error: data.error }), 'error');
showRefreshToast(t('mcp.templateSaveFailed', { error: data.error }), 'error');
}
} catch (error) {
console.error('[MCP] Save template error:', error);
showNotification(t('mcp.templateSaveFailed', { error: error.message }), 'error');
showRefreshToast(t('mcp.templateSaveFailed', { error: error.message }), 'error');
}
}
@@ -808,7 +1231,7 @@ async function installFromTemplate(templateName, scope = 'project') {
// Find template
const template = mcpTemplates.find(t => t.name === templateName);
if (!template) {
showNotification(t('mcp.templateNotFound', { name: templateName }), 'error');
showRefreshToast(t('mcp.templateNotFound', { name: templateName }), 'error');
return;
}
@@ -823,11 +1246,11 @@ async function installFromTemplate(templateName, scope = 'project') {
await addGlobalMcpServer(serverName, template.serverConfig);
}
showNotification(t('mcp.templateInstalled', { name: serverName }), 'success');
showRefreshToast(t('mcp.templateInstalled', { name: serverName }), 'success');
await renderMcpManager();
} catch (error) {
console.error('[MCP] Install from template error:', error);
showNotification(t('mcp.templateInstallFailed', { error: error.message }), 'error');
showRefreshToast(t('mcp.templateInstallFailed', { error: error.message }), 'error');
}
}
@@ -843,14 +1266,14 @@ async function deleteMcpTemplate(templateName) {
const data = await response.json();
if (data.success) {
showNotification(t('mcp.templateDeleted', { name: templateName }), 'success');
showRefreshToast(t('mcp.templateDeleted', { name: templateName }), 'success');
await loadMcpTemplates();
await renderMcpManager();
} else {
showNotification(t('mcp.templateDeleteFailed', { error: data.error }), 'error');
showRefreshToast(t('mcp.templateDeleteFailed', { error: data.error }), 'error');
}
} catch (error) {
console.error('[MCP] Delete template error:', error);
showNotification(t('mcp.templateDeleteFailed', { error: error.message }), 'error');
showRefreshToast(t('mcp.templateDeleteFailed', { error: error.message }), 'error');
}
}