mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
feat: Enhance navigation and cleanup for graph explorer view
- Added a cleanup function to reset the state when navigating away from the graph explorer. - Updated navigation logic to call the cleanup function before switching views. - Improved internationalization by adding new translations for graph-related terms. - Adjusted icon sizes for better UI consistency in the graph explorer. - Implemented impact analysis button functionality in the graph explorer. - Refactored CLI tool configuration to use updated model names. - Enhanced CLI executor to handle prompts correctly for codex commands. - Introduced code relationship storage for better visualization in the index tree. - Added support for parsing Markdown and plain text files in the symbol parser. - Updated tests to reflect changes in language detection logic.
This commit is contained in:
@@ -139,8 +139,9 @@ async function renderMcpManager() {
|
||||
<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')">
|
||||
<button class="cli-mode-btn px-4 py-2 text-sm font-medium rounded-md transition-all ${currentCliMode === 'codex' ? 'shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCliMode('codex')"
|
||||
style="${currentCliMode === 'codex' ? 'background-color: #f97316; color: white;' : ''}">
|
||||
<i data-lucide="code-2" class="w-4 h-4 inline mr-1.5"></i>
|
||||
Codex
|
||||
</button>
|
||||
@@ -228,6 +229,7 @@ async function renderMcpManager() {
|
||||
<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>
|
||||
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Claude</span>
|
||||
${alreadyInCodex ? `<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">${t('mcp.codex.alreadyAdded')}</span>` : ''}
|
||||
</div>
|
||||
${!alreadyInCodex ? `
|
||||
@@ -250,6 +252,26 @@ async function renderMcpManager() {
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Available MCP Servers from Other Projects (Codex mode) -->
|
||||
<div class="mcp-section">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
${otherProjectServers.length === 0 ? `
|
||||
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<p class="text-muted-foreground">${t('empty.noAdditionalMcp')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${otherProjectServers.map(([serverName, serverInfo]) => {
|
||||
return renderAvailableServerCardForCodex(serverName, serverInfo);
|
||||
}).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
` : `
|
||||
<!-- CCW Tools MCP Server Card -->
|
||||
<div class="mcp-section mb-6">
|
||||
@@ -486,6 +508,55 @@ async function renderMcpManager() {
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Copy Codex Servers to Claude (Claude mode only) -->
|
||||
${currentCliMode === 'claude' && Object.keys(codexMcpServers || {}).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.claude.copyFromCodex')}
|
||||
</h3>
|
||||
<span class="text-sm text-muted-foreground">${Object.keys(codexMcpServers || {}).length} ${t('mcp.serversAvailable')}</span>
|
||||
</div>
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${Object.entries(codexMcpServers || {}).map(([serverName, serverConfig]) => {
|
||||
const alreadyInClaude = mcpUserServers && mcpUserServers[serverName];
|
||||
const isStdio = !!serverConfig.command;
|
||||
const isHttp = !!serverConfig.url;
|
||||
return `
|
||||
<div class="mcp-server-card bg-card border ${alreadyInClaude ? 'border-success/50' : 'border-orange-200 dark:border-orange-800'} 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 flex-wrap">
|
||||
<i data-lucide="code-2" class="w-5 h-5 text-orange-500"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300 rounded-full">Codex</span>
|
||||
${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>'
|
||||
}
|
||||
${alreadyInClaude ? '<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">' + t('mcp.claude.alreadyAdded') + '</span>' : ''}
|
||||
</div>
|
||||
${!alreadyInClaude ? `
|
||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
onclick="copyCodexServerToClaude('${escapeHtml(serverName)}', ${JSON.stringify(serverConfig).replace(/'/g, "'")})"
|
||||
title="${t('mcp.claude.copyToClaude')}">
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Claude
|
||||
</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">${isHttp ? t('mcp.url') : t('mcp.cmd')}</span>
|
||||
<span class="truncate" title="${escapeHtml(serverConfig.command || serverConfig.url || 'N/A')}">${escapeHtml(serverConfig.command || serverConfig.url || 'N/A')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- All Projects MCP Overview Table (Claude mode only) -->
|
||||
${currentCliMode === 'claude' ? `
|
||||
<div class="mcp-section mt-6">
|
||||
@@ -676,7 +747,12 @@ function renderGlobalManagementCard(serverName, serverConfig) {
|
||||
const serverType = serverConfig.type || 'stdio';
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card mcp-server-global bg-card border border-success/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="mcp-server-card mcp-server-global bg-card border border-success/30 rounded-lg p-4 hover:shadow-md transition-all cursor-pointer"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
||||
data-server-source="global"
|
||||
data-action="view-details"
|
||||
title="${t('mcp.clickToEdit')}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="globe" class="w-5 h-5 text-success"></i>
|
||||
@@ -706,7 +782,7 @@ function renderGlobalManagementCard(serverName, serverConfig) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-end">
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-end" onclick="event.stopPropagation()">
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-action="remove-global">
|
||||
@@ -807,6 +883,70 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Render available server card for Codex mode (with Claude badge and copy to Codex button)
|
||||
function renderAvailableServerCardForCodex(serverName, serverInfo) {
|
||||
const serverConfig = serverInfo.config;
|
||||
const usedIn = serverInfo.usedIn || [];
|
||||
const command = serverConfig.command || serverConfig.url || 'N/A';
|
||||
const args = serverConfig.args || [];
|
||||
|
||||
// Get the actual name to use when adding
|
||||
const originalName = serverInfo.originalName || serverName;
|
||||
const hasVariant = serverInfo.originalName && serverInfo.originalName !== serverName;
|
||||
|
||||
// Get source project info
|
||||
const sourceProject = serverInfo.sourceProject;
|
||||
const sourceProjectName = sourceProject ? (sourceProject.split('\\').pop() || sourceProject.split('/').pop()) : null;
|
||||
|
||||
// Generate args preview
|
||||
const argsPreview = args.length > 0 ? args.slice(0, 3).join(' ') + (args.length > 3 ? '...' : '') : '';
|
||||
|
||||
// Check if already in Codex
|
||||
const alreadyInCodex = codexMcpServers && codexMcpServers[originalName];
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card mcp-server-available bg-card border ${alreadyInCodex ? 'border-success/50' : 'border-border'} border-dashed 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-center gap-2 flex-wrap">
|
||||
<span><i data-lucide="circle-dashed" class="w-5 h-5 text-muted-foreground"></i></span>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(originalName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Claude</span>
|
||||
${hasVariant ? `
|
||||
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full" title="Different config from: ${escapeHtml(sourceProject || '')}">
|
||||
${escapeHtml(sourceProjectName || 'variant')}
|
||||
</span>
|
||||
` : ''}
|
||||
${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(originalName)}', ${JSON.stringify(serverConfig).replace(/'/g, "'")})"
|
||||
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(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">${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">${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>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Codex MCP Server Card Renderer
|
||||
// ========================================
|
||||
@@ -825,14 +965,17 @@ function renderCodexServerCard(serverName, serverConfig) {
|
||||
: `<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' : ''}"
|
||||
<div class="mcp-server-card bg-card border border-orange-200 dark:border-orange-800 rounded-lg p-4 hover:shadow-md transition-all cursor-pointer ${!isEnabled ? 'opacity-60' : ''}"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
||||
data-cli-type="codex">
|
||||
data-cli-type="codex"
|
||||
data-action="view-details-codex"
|
||||
title="${t('mcp.clickToEdit')}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<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>
|
||||
<span class="text-xs px-2 py-0.5 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300 rounded-full">Codex</span>
|
||||
${typeBadge}
|
||||
</div>
|
||||
<label class="mcp-toggle relative inline-flex items-center cursor-pointer" onclick="event.stopPropagation()">
|
||||
@@ -1041,13 +1184,22 @@ function attachMcpEventListeners() {
|
||||
});
|
||||
});
|
||||
|
||||
// View details - click on server card
|
||||
// View details / Edit - click on Claude 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);
|
||||
showMcpEditModal(serverName, serverConfig, serverSource, 'claude');
|
||||
});
|
||||
});
|
||||
|
||||
// View details / Edit - click on Codex server card
|
||||
document.querySelectorAll('.mcp-server-card[data-action="view-details-codex"]').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
const serverName = card.dataset.serverName;
|
||||
const serverConfig = JSON.parse(card.dataset.serverConfig);
|
||||
showMcpEditModal(serverName, serverConfig, 'codex', 'codex');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1068,15 +1220,39 @@ function attachMcpEventListeners() {
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MCP Details Modal
|
||||
// MCP Edit Modal (replaces Details Modal)
|
||||
// ========================================
|
||||
|
||||
function showMcpDetails(serverName, serverConfig, serverSource) {
|
||||
// Store current editing context
|
||||
let mcpEditContext = {
|
||||
serverName: null,
|
||||
serverConfig: null,
|
||||
serverSource: null,
|
||||
cliType: 'claude'
|
||||
};
|
||||
|
||||
function showMcpDetails(serverName, serverConfig, serverSource, cliType = 'claude') {
|
||||
showMcpEditModal(serverName, serverConfig, serverSource, cliType);
|
||||
}
|
||||
|
||||
function showMcpEditModal(serverName, serverConfig, serverSource, cliType = 'claude') {
|
||||
const modal = document.getElementById('mcpDetailsModal');
|
||||
const modalBody = document.getElementById('mcpDetailsModalBody');
|
||||
|
||||
if (!modal || !modalBody) return;
|
||||
|
||||
// Store editing context
|
||||
mcpEditContext = {
|
||||
serverName,
|
||||
serverConfig: JSON.parse(JSON.stringify(serverConfig)), // Deep clone
|
||||
serverSource,
|
||||
cliType
|
||||
};
|
||||
|
||||
// Check if editable (enterprise is read-only)
|
||||
const isReadOnly = serverSource === 'enterprise';
|
||||
const isCodex = cliType === 'codex';
|
||||
|
||||
// Build source badge
|
||||
let sourceBadge = '';
|
||||
if (serverSource === 'enterprise') {
|
||||
@@ -1085,74 +1261,271 @@ function showMcpDetails(serverName, serverConfig, serverSource) {
|
||||
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>`;
|
||||
} else if (isCodex) {
|
||||
sourceBadge = `<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300">Codex</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>';
|
||||
}
|
||||
// Format args and env for textarea
|
||||
const argsText = (serverConfig.args || []).join('\n');
|
||||
const envText = Object.entries(serverConfig.env || {}).map(([k, v]) => `${k}=${v}`).join('\n');
|
||||
|
||||
// Build edit form HTML
|
||||
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>
|
||||
<input type="text" id="mcpEditName" value="${escapeHtml(serverName)}"
|
||||
class="text-lg font-bold text-foreground bg-transparent border-b border-border focus:border-primary outline-none px-1 py-0.5 flex-1"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="${t('mcp.editModal.serverNamePlaceholder')}">
|
||||
${sourceBadge}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<!-- Command/URL -->
|
||||
<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>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1 block">
|
||||
${serverConfig.url ? t('mcp.url') : t('mcp.cmd')}
|
||||
</label>
|
||||
<input type="text" id="mcpEditCommand" value="${escapeHtml(serverConfig.command || serverConfig.url || '')}"
|
||||
class="w-full px-3 py-2 text-sm font-mono bg-muted border border-border rounded-lg focus:border-primary outline-none"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="${serverConfig.url ? 'https://...' : 'npx, node, python...'}">
|
||||
</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>
|
||||
<!-- Arguments -->
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1 block">
|
||||
${t('mcp.args')} <span class="font-normal">(${t('mcp.editModal.onePerLine')})</span>
|
||||
</label>
|
||||
<textarea id="mcpEditArgs" rows="3"
|
||||
class="w-full px-3 py-2 text-sm font-mono bg-muted border border-border rounded-lg focus:border-primary outline-none resize-none"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="-y package-name">${escapeHtml(argsText)}</textarea>
|
||||
</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>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1 block">
|
||||
${t('mcp.env')} <span class="font-normal">(KEY=VALUE ${t('mcp.editModal.onePerLine')})</span>
|
||||
</label>
|
||||
<textarea id="mcpEditEnv" rows="3"
|
||||
class="w-full px-3 py-2 text-sm font-mono bg-muted border border-border rounded-lg focus:border-primary outline-none resize-none"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="API_KEY=your-key DEBUG=true">${escapeHtml(envText)}</textarea>
|
||||
</div>
|
||||
|
||||
${isCodex ? `
|
||||
<!-- Codex-specific: enabled_tools -->
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1 block">
|
||||
${t('mcp.codex.enabledTools')} <span class="font-normal">(${t('mcp.editModal.onePerLine')})</span>
|
||||
</label>
|
||||
<textarea id="mcpEditEnabledTools" rows="2"
|
||||
class="w-full px-3 py-2 text-sm font-mono bg-muted border border-border rounded-lg focus:border-primary outline-none resize-none"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="tool1 tool2">${escapeHtml((serverConfig.enabled_tools || []).join('\n'))}</textarea>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Raw JSON Preview (collapsible) -->
|
||||
<details class="group">
|
||||
<summary class="text-xs font-semibold text-muted-foreground uppercase tracking-wide cursor-pointer flex items-center gap-1">
|
||||
<i data-lucide="chevron-right" class="w-3 h-3 transition-transform group-open:rotate-90"></i>
|
||||
Raw JSON
|
||||
</summary>
|
||||
<pre id="mcpEditJsonPreview" class="mt-2 bg-muted rounded-lg p-3 text-xs font-mono overflow-x-auto">${escapeHtml(JSON.stringify(serverConfig, null, 2))}</pre>
|
||||
</details>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
${!isReadOnly ? `
|
||||
<div class="flex items-center justify-between pt-4 border-t border-border">
|
||||
<div class="flex items-center gap-2">
|
||||
${serverSource === 'project' || isCodex ? `
|
||||
<button onclick="deleteMcpFromEdit()" class="px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors flex items-center gap-1.5">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
${t('mcp.editModal.delete')}
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="closeMcpEditModal()" class="px-4 py-2 text-sm text-muted-foreground hover:bg-muted rounded-lg transition-colors">
|
||||
${t('common.cancel')}
|
||||
</button>
|
||||
<button onclick="saveMcpEdit()" class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1.5">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
${t('mcp.editModal.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
` : `
|
||||
<div class="flex items-center justify-end pt-4 border-t border-border">
|
||||
<button onclick="closeMcpEditModal()" class="px-4 py-2 text-sm bg-muted text-foreground rounded-lg hover:bg-muted/80 transition-colors">
|
||||
${t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Update modal title
|
||||
const modalTitle = modal.querySelector('h2');
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = isReadOnly ? t('mcp.detailsModal.title') : t('mcp.editModal.title');
|
||||
}
|
||||
|
||||
// Show modal
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Re-initialize Lucide icons in modal
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
// Add input listeners to update JSON preview
|
||||
if (!isReadOnly) {
|
||||
['mcpEditCommand', 'mcpEditArgs', 'mcpEditEnv', 'mcpEditEnabledTools'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('input', updateMcpEditJsonPreview);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closeMcpEditModal() {
|
||||
const modal = document.getElementById('mcpDetailsModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
mcpEditContext = { serverName: null, serverConfig: null, serverSource: null, cliType: 'claude' };
|
||||
}
|
||||
|
||||
function updateMcpEditJsonPreview() {
|
||||
const preview = document.getElementById('mcpEditJsonPreview');
|
||||
if (!preview) return;
|
||||
|
||||
const config = buildConfigFromEditForm();
|
||||
preview.textContent = JSON.stringify(config, null, 2);
|
||||
}
|
||||
|
||||
function buildConfigFromEditForm() {
|
||||
const command = document.getElementById('mcpEditCommand')?.value.trim() || '';
|
||||
const argsText = document.getElementById('mcpEditArgs')?.value.trim() || '';
|
||||
const envText = document.getElementById('mcpEditEnv')?.value.trim() || '';
|
||||
const enabledToolsEl = document.getElementById('mcpEditEnabledTools');
|
||||
|
||||
// Build config
|
||||
const config = {};
|
||||
|
||||
// Command or URL
|
||||
if (mcpEditContext.serverConfig?.url) {
|
||||
config.url = command;
|
||||
} else {
|
||||
config.command = command;
|
||||
}
|
||||
|
||||
// Args
|
||||
if (argsText) {
|
||||
config.args = argsText.split('\n').map(a => a.trim()).filter(a => a);
|
||||
}
|
||||
|
||||
// Env
|
||||
if (envText) {
|
||||
config.env = {};
|
||||
envText.split('\n').forEach(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && trimmed.includes('=')) {
|
||||
const eqIndex = trimmed.indexOf('=');
|
||||
const key = trimmed.substring(0, eqIndex).trim();
|
||||
const value = trimmed.substring(eqIndex + 1).trim();
|
||||
if (key) {
|
||||
config.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Codex-specific: enabled_tools
|
||||
if (enabledToolsEl) {
|
||||
const toolsText = enabledToolsEl.value.trim();
|
||||
if (toolsText) {
|
||||
config.enabled_tools = toolsText.split('\n').map(t => t.trim()).filter(t => t);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function saveMcpEdit() {
|
||||
const newName = document.getElementById('mcpEditName')?.value.trim();
|
||||
if (!newName) {
|
||||
showRefreshToast(t('mcp.editModal.nameRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const newConfig = buildConfigFromEditForm();
|
||||
|
||||
if (!newConfig.command && !newConfig.url) {
|
||||
showRefreshToast(t('mcp.editModal.commandRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const { serverName, serverSource, cliType } = mcpEditContext;
|
||||
const nameChanged = newName !== serverName;
|
||||
|
||||
try {
|
||||
if (cliType === 'codex') {
|
||||
// Codex MCP update
|
||||
// If name changed, remove old and add new
|
||||
if (nameChanged) {
|
||||
await removeCodexMcpServer(serverName);
|
||||
}
|
||||
await addCodexMcpServer(newName, newConfig);
|
||||
} else if (serverSource === 'global') {
|
||||
// Global MCP update
|
||||
if (nameChanged) {
|
||||
await removeGlobalMcpServer(serverName);
|
||||
}
|
||||
await addGlobalMcpServer(newName, newConfig);
|
||||
} else if (serverSource === 'project') {
|
||||
// Project MCP update
|
||||
if (nameChanged) {
|
||||
await removeMcpServerFromProject(serverName);
|
||||
}
|
||||
await copyMcpServerToProject(newName, newConfig, 'mcp');
|
||||
}
|
||||
|
||||
closeMcpEditModal();
|
||||
showRefreshToast(t('mcp.editModal.saved', { name: newName }), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to save MCP edit:', err);
|
||||
showRefreshToast(t('mcp.editModal.saveFailed') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMcpFromEdit() {
|
||||
const { serverName, serverSource, cliType } = mcpEditContext;
|
||||
|
||||
if (!confirm(t('mcp.editModal.deleteConfirm', { name: serverName }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (cliType === 'codex') {
|
||||
await removeCodexMcpServer(serverName);
|
||||
} else if (serverSource === 'global') {
|
||||
await removeGlobalMcpServer(serverName);
|
||||
} else if (serverSource === 'project') {
|
||||
await removeMcpServerFromProject(serverName);
|
||||
}
|
||||
|
||||
closeMcpEditModal();
|
||||
showRefreshToast(t('mcp.editModal.deleted', { name: serverName }), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to delete MCP:', err);
|
||||
showRefreshToast(t('mcp.editModal.deleteFailed') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
Reference in New Issue
Block a user