Refactor search modes and optimize embedding generation

- Updated the dashboard template to hide the Code Graph Explorer feature.
- Enhanced the `executeCodexLens` function to use `exec` for better cross-platform compatibility and improved command execution.
- Changed the default `maxResults` and `limit` parameters in the smart search tool to 10 for better performance.
- Introduced a new `priority` search mode in the smart search tool, replacing the previous `parallel` mode, which now follows a fallback strategy: hybrid -> exact -> ripgrep.
- Optimized the embedding generation process in the embedding manager by batching operations and using a cached embedder instance to reduce model loading overhead.
- Implemented a thread-safe singleton pattern for the embedder to improve performance across multiple searches.
This commit is contained in:
catlog22
2025-12-20 11:08:34 +08:00
parent 7adde91e9f
commit e1cac5dd50
16 changed files with 852 additions and 284 deletions

View File

@@ -224,6 +224,13 @@
border-radius: 8px;
padding: 1.25rem;
transition: all 0.2s ease;
cursor: pointer;
/* Fixed card dimensions */
display: flex;
flex-direction: column;
height: 280px;
min-height: 280px;
max-height: 280px;
}
.memory-card:hover {
@@ -239,8 +246,9 @@
.memory-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
align-items: center;
margin-bottom: 0.75rem;
flex-shrink: 0;
}
.memory-id {
@@ -248,7 +256,8 @@
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
color: hsl(var(--foreground));
font-weight: 600;
flex-wrap: wrap;
}
@@ -257,6 +266,10 @@
height: 16px;
}
.memory-id i[data-lucide="star"] {
color: hsl(38 92% 50%);
}
.memory-actions {
display: flex;
gap: 0.5rem;
@@ -282,34 +295,83 @@
color: hsl(var(--destructive));
}
.icon-btn.favorite-active {
color: hsl(38 92% 50%);
}
.icon-btn.favorite-active:hover {
color: hsl(38 92% 40%);
}
.icon-btn i {
width: 18px;
height: 18px;
}
/* Copy ID Button */
.copy-id-btn {
background: none;
border: none;
padding: 0.125rem 0.25rem;
cursor: pointer;
color: hsl(var(--muted-foreground));
transition: color 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 0.25rem;
border-radius: 4px;
}
.copy-id-btn:hover {
color: hsl(var(--primary));
background: hsl(var(--muted));
}
.copy-id-btn i {
width: 14px;
height: 14px;
}
.copy-id-btn.copied {
color: hsl(var(--success, 142 76% 36%));
}
/* Memory Content */
.memory-content {
margin-bottom: 1rem;
}
.memory-summary {
font-size: 0.9375rem;
line-height: 1.6;
color: hsl(var(--foreground));
flex: 1;
min-height: 0;
overflow: hidden;
margin-bottom: 0.75rem;
}
.memory-summary,
.memory-preview {
font-size: 0.875rem;
line-height: 1.5;
color: hsl(var(--muted-foreground));
/* Fixed height with ellipsis */
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
max-height: 6em;
}
.memory-summary {
color: hsl(var(--foreground));
}
/* Memory Tags */
.memory-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
gap: 0.375rem;
margin-bottom: 0.5rem;
flex-shrink: 0;
max-height: 2rem;
overflow: hidden;
}
.tag {
@@ -321,13 +383,14 @@
font-weight: 500;
}
/* Memory Footer */
/* Memory Footer - Fixed at bottom */
.memory-footer {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding-top: 1rem;
gap: 0.5rem;
padding-top: 0.75rem;
border-top: 1px solid hsl(var(--border));
margin-top: auto;
}
.memory-meta {
@@ -850,6 +913,89 @@
margin: 0;
}
/* Relations Detail Styles */
.relations-detail h4 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
color: hsl(var(--foreground));
}
.clusters-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.relation-cluster-item {
padding: 1rem;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 8px;
}
.relation-cluster-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.relation-cluster-header i {
width: 18px;
height: 18px;
color: hsl(var(--primary));
}
.relation-cluster-header .cluster-name {
font-weight: 600;
color: hsl(var(--foreground));
}
.cluster-relations-list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
padding-top: 0.75rem;
border-top: 1px solid hsl(var(--border));
}
.relations-label {
font-size: 0.8125rem;
color: hsl(var(--muted-foreground));
}
.relation-tag {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.625rem;
background: hsl(var(--accent));
border-radius: 4px;
font-size: 0.8125rem;
}
.relation-tag i {
width: 12px;
height: 12px;
color: hsl(var(--muted-foreground));
}
.relation-tag .relation-type {
font-size: 0.6875rem;
padding: 0.125rem 0.375rem;
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
border-radius: 3px;
margin-left: 0.25rem;
}
.text-muted {
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
}
/* Dark Mode Adjustments */
[data-theme="dark"] .memory-card {
background: #1e293b;

View File

@@ -928,11 +928,19 @@ function selectCcwTools(type) {
}
// Build CCW Tools config with selected tools
// Uses isWindowsPlatform from state.js to generate platform-appropriate commands
function buildCcwToolsConfig(selectedTools) {
const config = {
command: "cmd",
args: ["/c", "npx", "-y", "ccw-mcp"]
};
// Windows requires 'cmd /c' wrapper to execute npx
// Other platforms (macOS, Linux) can run npx directly
const config = isWindowsPlatform
? {
command: "cmd",
args: ["/c", "npx", "-y", "ccw-mcp"]
}
: {
command: "npx",
args: ["-y", "ccw-mcp"]
};
// Add env if not all tools or not default 4 core tools
const coreTools = ['write_file', 'edit_file', 'codex_lens', 'smart_search'];

View File

@@ -1291,8 +1291,27 @@ const i18n = {
'coreMemory.clusterUpdateError': 'Failed to update cluster',
'coreMemory.memberRemoved': 'Member removed',
'coreMemory.memberRemoveError': 'Failed to remove member',
'coreMemory.favorites': 'Favorites',
'coreMemory.totalFavorites': 'Total Favorites',
'coreMemory.noFavorites': 'No favorites yet',
'coreMemory.toggleFavorite': 'Toggle Favorite',
'coreMemory.addedToFavorites': 'Added to favorites',
'coreMemory.removedFromFavorites': 'Removed from favorites',
'coreMemory.favoriteError': 'Failed to update favorite',
'coreMemory.relations': 'Relations',
'coreMemory.showRelations': 'Show Relations',
'coreMemory.relationsFor': 'Relations',
'coreMemory.noRelations': 'No cluster relations found',
'coreMemory.noRelationsHint': 'Use Auto Cluster in the Clusters tab to create relations',
'coreMemory.belongsToClusters': 'Belongs to Clusters',
'coreMemory.relationsError': 'Failed to load relations',
// Common additions
'common.copyId': 'Copy ID',
'common.copied': 'Copied!',
'common.copyError': 'Failed to copy',
},
zh: {
// App title and brand
'app.title': 'CCW 控制面板',
@@ -1589,13 +1608,13 @@ const i18n = {
'index.projects': '项目数',
'index.totalSize': '总大小',
'index.vectorIndexes': '向量',
'index.ftsIndexes': '全文',
'index.ftsIndexes': 'FTS',
'index.projectId': '项目 ID',
'index.size': '大小',
'index.type': '类型',
'index.lastModified': '修改时间',
'index.vector': '向量',
'index.fts': '全文',
'index.fts': 'FTS',
'index.noIndexes': '暂无索引',
'index.notConfigured': '未配置',
'index.initCurrent': '索引当前项目',
@@ -1608,7 +1627,7 @@ const i18n = {
'index.cleanAllConfirm': '确定要清理所有索引吗?此操作无法撤销。',
'index.cleanAllSuccess': '所有索引已清理',
'index.vectorIndex': '向量索引',
'index.normalIndex': '全文索引',
'index.normalIndex': 'FTS索引',
'index.vectorDesc': '语义搜索(含嵌入向量)',
'index.normalDesc': '快速全文搜索',
@@ -2585,6 +2604,25 @@ const i18n = {
'coreMemory.clusterUpdateError': '更新聚类失败',
'coreMemory.memberRemoved': '成员已移除',
'coreMemory.memberRemoveError': '移除成员失败',
'coreMemory.favorites': '收藏',
'coreMemory.totalFavorites': '收藏总数',
'coreMemory.noFavorites': '暂无收藏',
'coreMemory.toggleFavorite': '切换收藏',
'coreMemory.addedToFavorites': '已添加到收藏',
'coreMemory.removedFromFavorites': '已从收藏移除',
'coreMemory.favoriteError': '更新收藏失败',
'coreMemory.relations': '关联',
'coreMemory.showRelations': '显示关联',
'coreMemory.relationsFor': '关联关系',
'coreMemory.noRelations': '未找到聚类关联',
'coreMemory.noRelationsHint': '在聚类 Tab 中使用自动聚类来创建关联',
'coreMemory.belongsToClusters': '所属聚类',
'coreMemory.relationsError': '加载关联失败',
// Common additions
'common.copyId': '复制 ID',
'common.copied': '已复制!',
'common.copyError': '复制失败',
}
};

View File

@@ -10,6 +10,11 @@ let workflowData = {{WORKFLOW_DATA}};
let projectPath = '{{PROJECT_PATH}}';
let recentPaths = {{RECENT_PATHS}};
// Platform detection for cross-platform MCP command generation
// 'win32' for Windows, 'darwin' for macOS, 'linux' for Linux
const serverPlatform = '{{SERVER_PLATFORM}}';
const isWindowsPlatform = serverPlatform === 'win32';
// ========== Application State ==========
// Current filter for session list view ('all', 'active', 'archived')
let currentFilter = 'all';

View File

@@ -5,6 +5,37 @@
// ========== HTML/Text Processing ==========
/**
* Encode JSON config data to Base64 for safe HTML attribute storage
* @param {Object} config - Configuration object to encode
* @returns {string} Base64 encoded JSON string
*/
function encodeConfigData(config) {
try {
const jsonStr = JSON.stringify(config);
return btoa(encodeURIComponent(jsonStr));
} catch (err) {
console.error('[Utils] Error encoding config data:', err);
return '';
}
}
/**
* Decode Base64 config data back to JSON object
* @param {string} encoded - Base64 encoded string
* @returns {Object|null} Decoded configuration object or null on error
*/
function decodeConfigData(encoded) {
try {
if (!encoded) return null;
const jsonStr = decodeURIComponent(atob(encoded));
return JSON.parse(jsonStr);
} catch (err) {
console.error('[Utils] Error decoding config data:', err);
return null;
}
}
/**
* Escape HTML special characters to prevent XSS attacks
* @param {string} str - String to escape

View File

@@ -392,7 +392,8 @@ function renderToolsSection() {
'<div class="tool-item-right">' +
(codexLensStatus.ready
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> ' + t('cli.initIndex') + '</button>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'vector\')" title="' + (t('index.vectorDesc') || 'Semantic search with embeddings') + '"><i data-lucide="sparkles" class="w-3 h-3"></i> ' + (t('index.vectorIndex') || 'Vector') + '</button>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'normal\')" title="' + (t('index.normalDesc') || 'Fast full-text search only') + '"><i data-lucide="file-text" class="w-3 h-3"></i> ' + (t('index.normalIndex') || 'FTS') + '</button>' +
'<button class="btn-sm btn-outline btn-danger" onclick="event.stopPropagation(); uninstallCodexLens()"><i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('cli.uninstall') + '</button>'
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>' +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> ' + t('cli.install') + '</button>') +

View File

@@ -66,6 +66,10 @@ async function renderCoreMemoryView() {
<i data-lucide="brain"></i>
${t('coreMemory.memories')}
</button>
<button class="tab-btn" id="favoritesViewBtn" onclick="showFavoritesView()">
<i data-lucide="star"></i>
${t('coreMemory.favorites') || 'Favorites'}
</button>
<button class="tab-btn" id="clustersViewBtn" onclick="showClustersView()">
<i data-lucide="folder-tree"></i>
${t('coreMemory.clusters')}
@@ -106,6 +110,22 @@ async function renderCoreMemoryView() {
</div>
</div>
<!-- Favorites Tab Content (hidden by default) -->
<div class="cm-tab-panel" id="favoritesGrid" style="display: none;">
<div class="memory-stats">
<div class="stat-item">
<span class="stat-label">${t('coreMemory.totalFavorites') || 'Total Favorites'}</span>
<span class="stat-value" id="totalFavoritesCount">0</span>
</div>
</div>
<div class="memories-grid" id="favoritesGridContent">
<div class="empty-state">
<i data-lucide="star"></i>
<p>${t('coreMemory.noFavorites') || 'No favorites yet'}</p>
</div>
</div>
</div>
<!-- Clusters Tab Content (hidden by default) -->
<div class="cm-tab-panel clusters-container" id="clustersContainer" style="display: none;">
<div class="clusters-layout">
@@ -213,21 +233,21 @@ function renderMemoryCard(memory) {
const priority = metadata.priority || 'medium';
return `
<div class="memory-card ${isArchived ? 'archived' : ''}" data-memory-id="${memory.id}">
<div class="memory-card ${isArchived ? 'archived' : ''}" data-memory-id="${memory.id}" onclick="viewMemoryDetail('${memory.id}')">
<div class="memory-card-header">
<div class="memory-id">
<i data-lucide="bookmark"></i>
${metadata.favorite ? '<i data-lucide="star"></i>' : ''}
<span>${memory.id}</span>
${isArchived ? `<span class="badge badge-archived">${t('common.archived')}</span>` : ''}
${priority !== 'medium' ? `<span class="badge badge-priority-${priority}">${priority}</span>` : ''}
</div>
<div class="memory-actions">
<button class="icon-btn" onclick="viewMemoryDetail('${memory.id}')" title="${t('common.view')}">
<i data-lucide="eye"></i>
</button>
<div class="memory-actions" onclick="event.stopPropagation()">
<button class="icon-btn" onclick="editMemory('${memory.id}')" title="${t('common.edit')}">
<i data-lucide="edit"></i>
</button>
<button class="icon-btn ${metadata.favorite ? 'favorite-active' : ''}" onclick="toggleFavorite('${memory.id}')" title="${t('coreMemory.toggleFavorite') || 'Toggle Favorite'}">
<i data-lucide="star"></i>
</button>
${!isArchived
? `<button class="icon-btn" onclick="archiveMemory('${memory.id}')" title="${t('common.archive')}">
<i data-lucide="archive"></i>
@@ -270,11 +290,19 @@ function renderMemoryCard(memory) {
: ''
}
</div>
<div class="memory-features">
<div class="memory-features" onclick="event.stopPropagation()">
<button class="feature-btn" onclick="generateMemorySummary('${memory.id}')" title="${t('coreMemory.generateSummary')}">
<i data-lucide="sparkles"></i>
${t('coreMemory.summary')}
</button>
<button class="feature-btn" onclick="copyMemoryId('${memory.id}')" title="${t('common.copyId') || 'Copy ID'}">
<i data-lucide="copy"></i>
${t('common.copyId') || 'Copy ID'}
</button>
<button class="feature-btn" onclick="showMemoryRelations('${memory.id}')" title="${t('coreMemory.showRelations') || 'Show Relations'}">
<i data-lucide="git-branch"></i>
${t('coreMemory.relations') || 'Relations'}
</button>
</div>
</div>
</div>
@@ -565,18 +593,44 @@ function escapeHtml(text) {
return div.innerHTML;
}
async function copyMemoryId(memoryId) {
try {
await navigator.clipboard.writeText(memoryId);
showNotification(t('common.copied') || 'Copied!', 'success');
} catch (error) {
console.error('Failed to copy:', error);
showNotification(t('common.copyError') || 'Failed to copy', 'error');
}
}
// View Toggle Functions
function showMemoriesView() {
document.getElementById('memoriesGrid').style.display = '';
document.getElementById('favoritesGrid').style.display = 'none';
document.getElementById('clustersContainer').style.display = 'none';
document.getElementById('memoriesViewBtn').classList.add('active');
document.getElementById('favoritesViewBtn').classList.remove('active');
document.getElementById('clustersViewBtn').classList.remove('active');
}
async function showFavoritesView() {
document.getElementById('memoriesGrid').style.display = 'none';
document.getElementById('favoritesGrid').style.display = '';
document.getElementById('clustersContainer').style.display = 'none';
document.getElementById('memoriesViewBtn').classList.remove('active');
document.getElementById('favoritesViewBtn').classList.add('active');
document.getElementById('clustersViewBtn').classList.remove('active');
// Load favorites
await refreshFavorites();
}
function showClustersView() {
document.getElementById('memoriesGrid').style.display = 'none';
document.getElementById('favoritesGrid').style.display = 'none';
document.getElementById('clustersContainer').style.display = '';
document.getElementById('memoriesViewBtn').classList.remove('active');
document.getElementById('favoritesViewBtn').classList.remove('active');
document.getElementById('clustersViewBtn').classList.add('active');
// Load clusters from core-memory-clusters.js
@@ -586,3 +640,143 @@ function showClustersView() {
console.error('loadClusters is not available. Make sure core-memory-clusters.js is loaded.');
}
}
// Favorites Functions
async function refreshFavorites() {
const allMemories = await fetchCoreMemories(false);
const favorites = allMemories.filter(m => m.metadata && m.metadata.favorite);
const countEl = document.getElementById('totalFavoritesCount');
const gridEl = document.getElementById('favoritesGridContent');
if (countEl) countEl.textContent = favorites.length;
if (gridEl) {
if (favorites.length === 0) {
gridEl.innerHTML = `
<div class="empty-state">
<i data-lucide="star"></i>
<p>${t('coreMemory.noFavorites') || 'No favorites yet'}</p>
</div>
`;
} else {
gridEl.innerHTML = favorites.map(memory => renderMemoryCard(memory)).join('');
}
}
lucide.createIcons();
}
async function showMemoryRelations(memoryId) {
try {
// Fetch all clusters
const response = await fetch(`/api/core-memory/clusters?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
const clusters = result.clusters || [];
// Find clusters containing this memory
const relatedClusters = [];
for (const cluster of clusters) {
const detailRes = await fetch(`/api/core-memory/clusters/${cluster.id}?path=${encodeURIComponent(projectPath)}`);
if (detailRes.ok) {
const detail = await detailRes.json();
const members = detail.members || [];
if (members.some(m => m.session_id === memoryId || m.id === memoryId)) {
relatedClusters.push({
...cluster,
relations: detail.relations || []
});
}
}
}
// Show in modal
const modal = document.getElementById('memoryDetailModal');
document.getElementById('memoryDetailTitle').textContent = t('coreMemory.relationsFor') || `Relations: ${memoryId}`;
const body = document.getElementById('memoryDetailBody');
if (relatedClusters.length === 0) {
body.innerHTML = `
<div class="empty-state">
<i data-lucide="git-branch"></i>
<p>${t('coreMemory.noRelations') || 'No cluster relations found'}</p>
<p class="text-muted">${t('coreMemory.noRelationsHint') || 'Use Auto Cluster in the Clusters tab to create relations'}</p>
</div>
`;
} else {
body.innerHTML = `
<div class="relations-detail">
<h4>${t('coreMemory.belongsToClusters') || 'Belongs to Clusters'}</h4>
<div class="clusters-list">
${relatedClusters.map(cluster => `
<div class="relation-cluster-item">
<div class="relation-cluster-header">
<i data-lucide="folder"></i>
<span class="cluster-name">${escapeHtml(cluster.name)}</span>
<span class="badge badge-${cluster.status}">${cluster.status}</span>
</div>
${cluster.relations && cluster.relations.length > 0 ? `
<div class="cluster-relations-list">
<span class="relations-label">${t('coreMemory.relatedClusters')}:</span>
${cluster.relations.map(rel => `
<span class="relation-tag">
<i data-lucide="link"></i>
${escapeHtml(rel.target_name || rel.target_id)}
<span class="relation-type">${rel.relation_type}</span>
</span>
`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`;
}
modal.style.display = 'flex';
lucide.createIcons();
} catch (error) {
console.error('Failed to load relations:', error);
showNotification(t('coreMemory.relationsError') || 'Failed to load relations', 'error');
}
}
async function toggleFavorite(memoryId) {
try {
const memory = await fetchMemoryById(memoryId);
if (!memory) return;
const metadata = memory.metadata || {};
metadata.favorite = !metadata.favorite;
const response = await fetch('/api/core-memory/memories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...memory, metadata, path: projectPath })
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
showNotification(
metadata.favorite
? (t('coreMemory.addedToFavorites') || 'Added to favorites')
: (t('coreMemory.removedFromFavorites') || 'Removed from favorites'),
'success'
);
// Refresh current view
await refreshCoreMemories();
// Also refresh favorites if visible
const favoritesGrid = document.getElementById('favoritesGrid');
if (favoritesGrid && favoritesGrid.style.display !== 'none') {
await refreshFavorites();
}
} catch (error) {
console.error('Failed to toggle favorite:', error);
showNotification(t('coreMemory.favoriteError') || 'Failed to update favorite', 'error');
}
}

View File

@@ -324,7 +324,7 @@ async function renderMcpManager() {
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
data-action="copy-to-codex"
data-server-name="${escapeHtml(serverName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
data-server-config="${encodeConfigData(serverConfig)}"
title="${t('mcp.codex.copyToCodex')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
</button>
@@ -684,7 +684,7 @@ async function renderMcpManager() {
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
data-action="copy-codex-to-claude"
data-server-name="${escapeHtml(serverName)}"
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "&#39;")}'
data-server-config="${encodeConfigData(serverConfig)}"
title="${t('mcp.claude.copyToClaude')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Claude
</button>
@@ -823,7 +823,7 @@ function renderProjectAvailableServerCard(entry) {
return `
<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-config="${encodeConfigData(config)}"
data-server-source="${source}"
data-action="view-details"
title="${t('mcp.clickToViewDetails')}">
@@ -867,7 +867,7 @@ function renderProjectAvailableServerCard(entry) {
<div class="flex items-center gap-2">
<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))}"
data-server-config="${encodeConfigData(config)}"
data-action="save-as-template"
onclick="event.stopPropagation()"
title="${t('mcp.saveAsTemplate')}">
@@ -898,7 +898,7 @@ function renderGlobalManagementCard(serverName, serverConfig) {
return `
<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-config="${encodeConfigData(serverConfig)}"
data-server-source="global"
data-action="view-details"
title="${t('mcp.clickToEdit')}">
@@ -976,7 +976,7 @@ function renderAvailableServerCard(serverName, serverInfo) {
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
data-server-name="${escapeHtml(originalName)}"
data-server-key="${escapeHtml(serverName)}"
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "&#39;")}'
data-server-config="${encodeConfigData(serverConfig)}"
data-scope="project"
data-action="add-from-other"
title="${t('mcp.installToProject')}">
@@ -985,7 +985,7 @@ function renderAvailableServerCard(serverName, serverInfo) {
<button class="px-3 py-1 text-xs bg-success text-success-foreground rounded hover:opacity-90 transition-opacity"
data-server-name="${escapeHtml(originalName)}"
data-server-key="${escapeHtml(serverName)}"
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "&#39;")}'
data-server-config="${encodeConfigData(serverConfig)}"
data-scope="global"
data-action="add-from-other"
title="${t('mcp.installToGlobal')}">
@@ -1014,7 +1014,7 @@ function renderAvailableServerCard(serverName, serverInfo) {
<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-server-config="${encodeConfigData(serverConfig)}"
data-action="install-to-project"
title="${t('mcp.installToProject')}">
<i data-lucide="download" class="w-3 h-3"></i>
@@ -1022,7 +1022,7 @@ function renderAvailableServerCard(serverName, serverInfo) {
</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-server-config="${encodeConfigData(serverConfig)}"
data-action="install-to-global"
title="${t('mcp.installToGlobal')}">
<i data-lucide="globe" class="w-3 h-3"></i>
@@ -1072,7 +1072,7 @@ function renderAvailableServerCardForCodex(serverName, serverInfo) {
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
data-action="copy-to-codex"
data-server-name="${escapeHtml(originalName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
data-server-config="${encodeConfigData(serverConfig)}"
title="${t('mcp.codex.copyToCodex')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
</button>
@@ -1100,7 +1100,7 @@ function renderAvailableServerCardForCodex(serverName, serverInfo) {
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
data-action="copy-to-codex"
data-server-name="${escapeHtml(originalName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
data-server-config="${encodeConfigData(serverConfig)}"
title="${t('mcp.codex.copyToCodex')}">
<i data-lucide="download" class="w-3 h-3"></i>
${t('mcp.codex.install')}
@@ -1130,7 +1130,7 @@ function renderCodexServerCard(serverName, serverConfig) {
return `
<div class="mcp-server-card bg-card border border-primary/20 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-server-config="${encodeConfigData(serverConfig)}"
data-cli-type="codex"
data-action="view-details-codex"
title="${t('mcp.clickToEdit')}">
@@ -1180,7 +1180,7 @@ function renderCodexServerCard(serverName, serverConfig) {
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
data-action="copy-codex-to-claude"
data-server-name="${escapeHtml(serverName)}"
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "&#39;")}'
data-server-config="${encodeConfigData(serverConfig)}"
title="${t('mcp.codex.copyToClaude')}">
<i data-lucide="copy" class="w-3 h-3"></i>
${t('mcp.codex.copyToClaude')}
@@ -1250,7 +1250,7 @@ function renderCrossCliServerCard(server, isClaude) {
<button class="w-full px-3 py-2 text-sm font-medium bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg transition-colors flex items-center justify-center gap-1.5"
data-action="copy-cross-cli"
data-server-name="${escapeHtml(name)}"
data-server-config='${JSON.stringify(config).replace(/'/g, "&#39;")}'
data-server-config="${encodeConfigData(config)}"
data-from-cli="${fromCli}"
data-target-cli="${targetCli}">
<i data-lucide="copy" class="w-4 h-4"></i>
@@ -1357,14 +1357,18 @@ function attachMcpEventListeners() {
// Add from other projects (with scope selection)
document.querySelectorAll('.mcp-server-card button[data-action="add-from-other"]').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' or 'global'
try {
const serverName = btn.dataset.serverName;
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
const scope = btn.dataset.scope; // 'project' or 'global'
if (scope === 'global') {
await addGlobalMcpServer(serverName, serverConfig);
} else {
await copyMcpServerToProject(serverName, serverConfig);
if (scope === 'global') {
await addGlobalMcpServer(serverName, serverConfig);
} else {
await copyMcpServerToProject(serverName, serverConfig);
}
} catch (err) {
console.error('[MCP] Error adding server from other project:', err);
}
});
});
@@ -1392,27 +1396,39 @@ function attachMcpEventListeners() {
// 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);
await copyMcpServerToProject(serverName, serverConfig);
try {
const serverName = btn.dataset.serverName;
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
await copyMcpServerToProject(serverName, serverConfig);
} catch (err) {
console.error('[MCP] Error installing to project:', err);
}
});
});
// 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);
try {
const serverName = btn.dataset.serverName;
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
await addGlobalMcpServer(serverName, serverConfig);
} catch (err) {
console.error('[MCP] Error installing to global:', err);
}
});
});
// Save as template buttons
document.querySelectorAll('.mcp-server-card button[data-action="save-as-template"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
const serverName = btn.dataset.serverName;
const serverConfig = JSON.parse(btn.dataset.serverConfig);
await saveMcpAsTemplate(serverName, serverConfig);
try {
const serverName = btn.dataset.serverName;
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
await saveMcpAsTemplate(serverName, serverConfig);
} catch (err) {
console.error('[MCP] Error saving as template:', err);
}
});
});
@@ -1507,10 +1523,14 @@ function attachMcpEventListeners() {
document.querySelectorAll('button[data-action="copy-to-codex"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.preventDefault();
const serverName = btn.dataset.serverName;
const serverConfig = JSON.parse(btn.dataset.serverConfig);
console.log('[MCP] Copying to Codex:', serverName);
await copyClaudeServerToCodex(serverName, serverConfig);
try {
const serverName = btn.dataset.serverName;
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
console.log('[MCP] Copying to Codex:', serverName);
await copyClaudeServerToCodex(serverName, serverConfig);
} catch (err) {
console.error('[MCP] Error copying to Codex:', err);
}
});
});
@@ -1522,7 +1542,7 @@ function attachMcpEventListeners() {
const serverName = btn.dataset.serverName;
let serverConfig;
try {
serverConfig = JSON.parse(btn.dataset.serverConfig);
serverConfig = decodeConfigData(btn.dataset.serverConfig);
} catch (err) {
console.error('[MCP] JSON Parse Error:', err);
if (typeof showRefreshToast === 'function') {
@@ -1543,7 +1563,7 @@ function attachMcpEventListeners() {
const serverName = btn.dataset.serverName;
let serverConfig;
try {
serverConfig = JSON.parse(btn.dataset.serverConfig);
serverConfig = decodeConfigData(btn.dataset.serverConfig);
} catch (err) {
console.error('[MCP] JSON Parse Error:', err);
if (typeof showRefreshToast === 'function') {
@@ -1567,14 +1587,21 @@ function attachMcpEventListeners() {
}
try {
const serverName = card.dataset.serverName;
// Decode HTML entities before parsing JSON
const configStr = unescapeHtml(card.dataset.serverConfig);
const serverConfig = JSON.parse(configStr);
const configData = card.dataset.serverConfig;
if (!configData) {
console.error('[MCP] Missing server config for:', serverName);
return;
}
const serverConfig = decodeConfigData(configData);
if (!serverConfig) {
console.error('[MCP] Failed to decode server config for:', serverName);
return;
}
const serverSource = card.dataset.serverSource;
console.log('[MCP] Card clicked:', serverName, serverSource);
showMcpEditModal(serverName, serverConfig, serverSource, 'claude');
} catch (err) {
console.error('[MCP] Error handling card click:', err, card.dataset.serverConfig);
console.error('[MCP] Error handling card click:', err);
}
});
});
@@ -1588,13 +1615,20 @@ function attachMcpEventListeners() {
}
try {
const serverName = card.dataset.serverName;
// Decode HTML entities before parsing JSON
const configStr = unescapeHtml(card.dataset.serverConfig);
const serverConfig = JSON.parse(configStr);
const configData = card.dataset.serverConfig;
if (!configData) {
console.error('[MCP] Missing server config for:', serverName);
return;
}
const serverConfig = decodeConfigData(configData);
if (!serverConfig) {
console.error('[MCP] Failed to decode server config for:', serverName);
return;
}
console.log('[MCP] Codex card clicked:', serverName);
showMcpEditModal(serverName, serverConfig, 'codex', 'codex');
} catch (err) {
console.error('[MCP] Error handling Codex card click:', err, card.dataset.serverConfig);
console.error('[MCP] Error handling Codex card click:', err);
}
});
});

View File

@@ -331,10 +331,12 @@
<i data-lucide="history" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.history">History</span>
</li>
<!-- Hidden: Code Graph Explorer (feature disabled)
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="graph-explorer" data-tooltip="Code Graph Explorer">
<i data-lucide="git-branch" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.graphExplorer">Graph</span>
</li>
-->
</ul>
</div>