mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
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:
@@ -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;
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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': '复制失败',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>') +
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "'")}'
|
||||
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, "'")}'
|
||||
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, "'")}'
|
||||
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, "'")}'
|
||||
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, "'")}'
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user