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:
@@ -10,7 +10,7 @@
|
|||||||
smart_search(query="authentication logic")
|
smart_search(query="authentication logic")
|
||||||
|
|
||||||
// Step 2: Only if search warns "No CodexLens index found", then init
|
// Step 2: Only if search warns "No CodexLens index found", then init
|
||||||
smart_search(action="init", path=".") // Creates FTS index only
|
smart_search(action="init", path=".") // Creates FTS index only
|
||||||
|
|
||||||
// Note: For semantic/vector search, use "ccw view" dashboard to create vector index
|
// Note: For semantic/vector search, use "ccw view" dashboard to create vector index
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ function generateFromUnifiedTemplate(data: unknown): string {
|
|||||||
jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, projectPath.replace(/\\/g, '/'));
|
jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, projectPath.replace(/\\/g, '/'));
|
||||||
jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(recentPaths));
|
jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(recentPaths));
|
||||||
|
|
||||||
|
// Inject platform information for cross-platform MCP command generation
|
||||||
|
// 'win32' for Windows, 'darwin' for macOS, 'linux' for Linux
|
||||||
|
jsContent = jsContent.replace(/\{\{SERVER_PLATFORM\}\}/g, process.platform);
|
||||||
|
|
||||||
// Inject JS and CSS into HTML template
|
// Inject JS and CSS into HTML template
|
||||||
html = html.replace('{{JS_CONTENT}}', jsContent);
|
html = html.replace('{{JS_CONTENT}}', jsContent);
|
||||||
html = html.replace('{{CSS_CONTENT}}', cssContent);
|
html = html.replace('{{CSS_CONTENT}}', cssContent);
|
||||||
|
|||||||
@@ -224,6 +224,13 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
transition: all 0.2s ease;
|
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 {
|
.memory-card:hover {
|
||||||
@@ -239,8 +246,9 @@
|
|||||||
.memory-card-header {
|
.memory-card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-id {
|
.memory-id {
|
||||||
@@ -248,7 +256,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--foreground));
|
||||||
|
font-weight: 600;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +266,10 @@
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.memory-id i[data-lucide="star"] {
|
||||||
|
color: hsl(38 92% 50%);
|
||||||
|
}
|
||||||
|
|
||||||
.memory-actions {
|
.memory-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -282,34 +295,83 @@
|
|||||||
color: hsl(var(--destructive));
|
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 {
|
.icon-btn i {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 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 */
|
||||||
.memory-content {
|
.memory-content {
|
||||||
margin-bottom: 1rem;
|
flex: 1;
|
||||||
}
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
.memory-summary {
|
margin-bottom: 0.75rem;
|
||||||
font-size: 0.9375rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.memory-summary,
|
||||||
.memory-preview {
|
.memory-preview {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: hsl(var(--muted-foreground));
|
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 */
|
||||||
.memory-tags {
|
.memory-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.375rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-height: 2rem;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
@@ -321,13 +383,14 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Memory Footer */
|
/* Memory Footer - Fixed at bottom */
|
||||||
.memory-footer {
|
.memory-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
padding-top: 1rem;
|
padding-top: 0.75rem;
|
||||||
border-top: 1px solid hsl(var(--border));
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-meta {
|
.memory-meta {
|
||||||
@@ -850,6 +913,89 @@
|
|||||||
margin: 0;
|
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 */
|
/* Dark Mode Adjustments */
|
||||||
[data-theme="dark"] .memory-card {
|
[data-theme="dark"] .memory-card {
|
||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
|
|||||||
@@ -928,11 +928,19 @@ function selectCcwTools(type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build CCW Tools config with selected tools
|
// Build CCW Tools config with selected tools
|
||||||
|
// Uses isWindowsPlatform from state.js to generate platform-appropriate commands
|
||||||
function buildCcwToolsConfig(selectedTools) {
|
function buildCcwToolsConfig(selectedTools) {
|
||||||
const config = {
|
// Windows requires 'cmd /c' wrapper to execute npx
|
||||||
command: "cmd",
|
// Other platforms (macOS, Linux) can run npx directly
|
||||||
args: ["/c", "npx", "-y", "ccw-mcp"]
|
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
|
// Add env if not all tools or not default 4 core tools
|
||||||
const coreTools = ['write_file', 'edit_file', 'codex_lens', 'smart_search'];
|
const coreTools = ['write_file', 'edit_file', 'codex_lens', 'smart_search'];
|
||||||
|
|||||||
@@ -1291,8 +1291,27 @@ const i18n = {
|
|||||||
'coreMemory.clusterUpdateError': 'Failed to update cluster',
|
'coreMemory.clusterUpdateError': 'Failed to update cluster',
|
||||||
'coreMemory.memberRemoved': 'Member removed',
|
'coreMemory.memberRemoved': 'Member removed',
|
||||||
'coreMemory.memberRemoveError': 'Failed to remove member',
|
'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: {
|
zh: {
|
||||||
// App title and brand
|
// App title and brand
|
||||||
'app.title': 'CCW 控制面板',
|
'app.title': 'CCW 控制面板',
|
||||||
@@ -1589,13 +1608,13 @@ const i18n = {
|
|||||||
'index.projects': '项目数',
|
'index.projects': '项目数',
|
||||||
'index.totalSize': '总大小',
|
'index.totalSize': '总大小',
|
||||||
'index.vectorIndexes': '向量',
|
'index.vectorIndexes': '向量',
|
||||||
'index.ftsIndexes': '全文',
|
'index.ftsIndexes': 'FTS',
|
||||||
'index.projectId': '项目 ID',
|
'index.projectId': '项目 ID',
|
||||||
'index.size': '大小',
|
'index.size': '大小',
|
||||||
'index.type': '类型',
|
'index.type': '类型',
|
||||||
'index.lastModified': '修改时间',
|
'index.lastModified': '修改时间',
|
||||||
'index.vector': '向量',
|
'index.vector': '向量',
|
||||||
'index.fts': '全文',
|
'index.fts': 'FTS',
|
||||||
'index.noIndexes': '暂无索引',
|
'index.noIndexes': '暂无索引',
|
||||||
'index.notConfigured': '未配置',
|
'index.notConfigured': '未配置',
|
||||||
'index.initCurrent': '索引当前项目',
|
'index.initCurrent': '索引当前项目',
|
||||||
@@ -1608,7 +1627,7 @@ const i18n = {
|
|||||||
'index.cleanAllConfirm': '确定要清理所有索引吗?此操作无法撤销。',
|
'index.cleanAllConfirm': '确定要清理所有索引吗?此操作无法撤销。',
|
||||||
'index.cleanAllSuccess': '所有索引已清理',
|
'index.cleanAllSuccess': '所有索引已清理',
|
||||||
'index.vectorIndex': '向量索引',
|
'index.vectorIndex': '向量索引',
|
||||||
'index.normalIndex': '全文索引',
|
'index.normalIndex': 'FTS索引',
|
||||||
'index.vectorDesc': '语义搜索(含嵌入向量)',
|
'index.vectorDesc': '语义搜索(含嵌入向量)',
|
||||||
'index.normalDesc': '快速全文搜索',
|
'index.normalDesc': '快速全文搜索',
|
||||||
|
|
||||||
@@ -2585,6 +2604,25 @@ const i18n = {
|
|||||||
'coreMemory.clusterUpdateError': '更新聚类失败',
|
'coreMemory.clusterUpdateError': '更新聚类失败',
|
||||||
'coreMemory.memberRemoved': '成员已移除',
|
'coreMemory.memberRemoved': '成员已移除',
|
||||||
'coreMemory.memberRemoveError': '移除成员失败',
|
'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 projectPath = '{{PROJECT_PATH}}';
|
||||||
let recentPaths = {{RECENT_PATHS}};
|
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 ==========
|
// ========== Application State ==========
|
||||||
// Current filter for session list view ('all', 'active', 'archived')
|
// Current filter for session list view ('all', 'active', 'archived')
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
|
|||||||
@@ -5,6 +5,37 @@
|
|||||||
|
|
||||||
// ========== HTML/Text Processing ==========
|
// ========== 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
|
* Escape HTML special characters to prevent XSS attacks
|
||||||
* @param {string} str - String to escape
|
* @param {string} str - String to escape
|
||||||
|
|||||||
@@ -392,7 +392,8 @@ function renderToolsSection() {
|
|||||||
'<div class="tool-item-right">' +
|
'<div class="tool-item-right">' +
|
||||||
(codexLensStatus.ready
|
(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>' +
|
? '<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>'
|
'<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>' +
|
: '<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>') +
|
'<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>
|
<i data-lucide="brain"></i>
|
||||||
${t('coreMemory.memories')}
|
${t('coreMemory.memories')}
|
||||||
</button>
|
</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()">
|
<button class="tab-btn" id="clustersViewBtn" onclick="showClustersView()">
|
||||||
<i data-lucide="folder-tree"></i>
|
<i data-lucide="folder-tree"></i>
|
||||||
${t('coreMemory.clusters')}
|
${t('coreMemory.clusters')}
|
||||||
@@ -106,6 +110,22 @@ async function renderCoreMemoryView() {
|
|||||||
</div>
|
</div>
|
||||||
</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) -->
|
<!-- Clusters Tab Content (hidden by default) -->
|
||||||
<div class="cm-tab-panel clusters-container" id="clustersContainer" style="display: none;">
|
<div class="cm-tab-panel clusters-container" id="clustersContainer" style="display: none;">
|
||||||
<div class="clusters-layout">
|
<div class="clusters-layout">
|
||||||
@@ -213,21 +233,21 @@ function renderMemoryCard(memory) {
|
|||||||
const priority = metadata.priority || 'medium';
|
const priority = metadata.priority || 'medium';
|
||||||
|
|
||||||
return `
|
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-card-header">
|
||||||
<div class="memory-id">
|
<div class="memory-id">
|
||||||
<i data-lucide="bookmark"></i>
|
${metadata.favorite ? '<i data-lucide="star"></i>' : ''}
|
||||||
<span>${memory.id}</span>
|
<span>${memory.id}</span>
|
||||||
${isArchived ? `<span class="badge badge-archived">${t('common.archived')}</span>` : ''}
|
${isArchived ? `<span class="badge badge-archived">${t('common.archived')}</span>` : ''}
|
||||||
${priority !== 'medium' ? `<span class="badge badge-priority-${priority}">${priority}</span>` : ''}
|
${priority !== 'medium' ? `<span class="badge badge-priority-${priority}">${priority}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="memory-actions">
|
<div class="memory-actions" onclick="event.stopPropagation()">
|
||||||
<button class="icon-btn" onclick="viewMemoryDetail('${memory.id}')" title="${t('common.view')}">
|
|
||||||
<i data-lucide="eye"></i>
|
|
||||||
</button>
|
|
||||||
<button class="icon-btn" onclick="editMemory('${memory.id}')" title="${t('common.edit')}">
|
<button class="icon-btn" onclick="editMemory('${memory.id}')" title="${t('common.edit')}">
|
||||||
<i data-lucide="edit"></i>
|
<i data-lucide="edit"></i>
|
||||||
</button>
|
</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
|
${!isArchived
|
||||||
? `<button class="icon-btn" onclick="archiveMemory('${memory.id}')" title="${t('common.archive')}">
|
? `<button class="icon-btn" onclick="archiveMemory('${memory.id}')" title="${t('common.archive')}">
|
||||||
<i data-lucide="archive"></i>
|
<i data-lucide="archive"></i>
|
||||||
@@ -270,11 +290,19 @@ function renderMemoryCard(memory) {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="memory-features">
|
<div class="memory-features" onclick="event.stopPropagation()">
|
||||||
<button class="feature-btn" onclick="generateMemorySummary('${memory.id}')" title="${t('coreMemory.generateSummary')}">
|
<button class="feature-btn" onclick="generateMemorySummary('${memory.id}')" title="${t('coreMemory.generateSummary')}">
|
||||||
<i data-lucide="sparkles"></i>
|
<i data-lucide="sparkles"></i>
|
||||||
${t('coreMemory.summary')}
|
${t('coreMemory.summary')}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -565,18 +593,44 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
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
|
// View Toggle Functions
|
||||||
function showMemoriesView() {
|
function showMemoriesView() {
|
||||||
document.getElementById('memoriesGrid').style.display = '';
|
document.getElementById('memoriesGrid').style.display = '';
|
||||||
|
document.getElementById('favoritesGrid').style.display = 'none';
|
||||||
document.getElementById('clustersContainer').style.display = 'none';
|
document.getElementById('clustersContainer').style.display = 'none';
|
||||||
document.getElementById('memoriesViewBtn').classList.add('active');
|
document.getElementById('memoriesViewBtn').classList.add('active');
|
||||||
|
document.getElementById('favoritesViewBtn').classList.remove('active');
|
||||||
document.getElementById('clustersViewBtn').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() {
|
function showClustersView() {
|
||||||
document.getElementById('memoriesGrid').style.display = 'none';
|
document.getElementById('memoriesGrid').style.display = 'none';
|
||||||
|
document.getElementById('favoritesGrid').style.display = 'none';
|
||||||
document.getElementById('clustersContainer').style.display = '';
|
document.getElementById('clustersContainer').style.display = '';
|
||||||
document.getElementById('memoriesViewBtn').classList.remove('active');
|
document.getElementById('memoriesViewBtn').classList.remove('active');
|
||||||
|
document.getElementById('favoritesViewBtn').classList.remove('active');
|
||||||
document.getElementById('clustersViewBtn').classList.add('active');
|
document.getElementById('clustersViewBtn').classList.add('active');
|
||||||
|
|
||||||
// Load clusters from core-memory-clusters.js
|
// 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.');
|
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"
|
<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-action="copy-to-codex"
|
||||||
data-server-name="${escapeHtml(serverName)}"
|
data-server-name="${escapeHtml(serverName)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
title="${t('mcp.codex.copyToCodex')}">
|
title="${t('mcp.codex.copyToCodex')}">
|
||||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
|
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
|
||||||
</button>
|
</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"
|
<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-action="copy-codex-to-claude"
|
||||||
data-server-name="${escapeHtml(serverName)}"
|
data-server-name="${escapeHtml(serverName)}"
|
||||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
title="${t('mcp.claude.copyToClaude')}">
|
title="${t('mcp.claude.copyToClaude')}">
|
||||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Claude
|
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Claude
|
||||||
</button>
|
</button>
|
||||||
@@ -823,7 +823,7 @@ function renderProjectAvailableServerCard(entry) {
|
|||||||
return `
|
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' : ''}"
|
<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-name="${escapeHtml(name)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(config))}"
|
data-server-config="${encodeConfigData(config)}"
|
||||||
data-server-source="${source}"
|
data-server-source="${source}"
|
||||||
data-action="view-details"
|
data-action="view-details"
|
||||||
title="${t('mcp.clickToViewDetails')}">
|
title="${t('mcp.clickToViewDetails')}">
|
||||||
@@ -867,7 +867,7 @@ function renderProjectAvailableServerCard(entry) {
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button class="text-xs text-success hover:text-success/80 transition-colors flex items-center gap-1"
|
<button class="text-xs text-success hover:text-success/80 transition-colors flex items-center gap-1"
|
||||||
data-server-name="${escapeHtml(name)}"
|
data-server-name="${escapeHtml(name)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(config))}"
|
data-server-config="${encodeConfigData(config)}"
|
||||||
data-action="save-as-template"
|
data-action="save-as-template"
|
||||||
onclick="event.stopPropagation()"
|
onclick="event.stopPropagation()"
|
||||||
title="${t('mcp.saveAsTemplate')}">
|
title="${t('mcp.saveAsTemplate')}">
|
||||||
@@ -898,7 +898,7 @@ function renderGlobalManagementCard(serverName, serverConfig) {
|
|||||||
return `
|
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"
|
<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-name="${escapeHtml(serverName)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
data-server-source="global"
|
data-server-source="global"
|
||||||
data-action="view-details"
|
data-action="view-details"
|
||||||
title="${t('mcp.clickToEdit')}">
|
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"
|
<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-name="${escapeHtml(originalName)}"
|
||||||
data-server-key="${escapeHtml(serverName)}"
|
data-server-key="${escapeHtml(serverName)}"
|
||||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
data-scope="project"
|
data-scope="project"
|
||||||
data-action="add-from-other"
|
data-action="add-from-other"
|
||||||
title="${t('mcp.installToProject')}">
|
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"
|
<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-name="${escapeHtml(originalName)}"
|
||||||
data-server-key="${escapeHtml(serverName)}"
|
data-server-key="${escapeHtml(serverName)}"
|
||||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
data-scope="global"
|
data-scope="global"
|
||||||
data-action="add-from-other"
|
data-action="add-from-other"
|
||||||
title="${t('mcp.installToGlobal')}">
|
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">
|
<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"
|
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||||
data-server-name="${escapeHtml(originalName)}"
|
data-server-name="${escapeHtml(originalName)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
data-action="install-to-project"
|
data-action="install-to-project"
|
||||||
title="${t('mcp.installToProject')}">
|
title="${t('mcp.installToProject')}">
|
||||||
<i data-lucide="download" class="w-3 h-3"></i>
|
<i data-lucide="download" class="w-3 h-3"></i>
|
||||||
@@ -1022,7 +1022,7 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
|||||||
</button>
|
</button>
|
||||||
<button class="text-xs text-success hover:text-success/80 transition-colors flex items-center gap-1"
|
<button class="text-xs text-success hover:text-success/80 transition-colors flex items-center gap-1"
|
||||||
data-server-name="${escapeHtml(originalName)}"
|
data-server-name="${escapeHtml(originalName)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
data-action="install-to-global"
|
data-action="install-to-global"
|
||||||
title="${t('mcp.installToGlobal')}">
|
title="${t('mcp.installToGlobal')}">
|
||||||
<i data-lucide="globe" class="w-3 h-3"></i>
|
<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"
|
<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-action="copy-to-codex"
|
||||||
data-server-name="${escapeHtml(originalName)}"
|
data-server-name="${escapeHtml(originalName)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
title="${t('mcp.codex.copyToCodex')}">
|
title="${t('mcp.codex.copyToCodex')}">
|
||||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
|
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
|
||||||
</button>
|
</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"
|
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||||
data-action="copy-to-codex"
|
data-action="copy-to-codex"
|
||||||
data-server-name="${escapeHtml(originalName)}"
|
data-server-name="${escapeHtml(originalName)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
title="${t('mcp.codex.copyToCodex')}">
|
title="${t('mcp.codex.copyToCodex')}">
|
||||||
<i data-lucide="download" class="w-3 h-3"></i>
|
<i data-lucide="download" class="w-3 h-3"></i>
|
||||||
${t('mcp.codex.install')}
|
${t('mcp.codex.install')}
|
||||||
@@ -1130,7 +1130,7 @@ function renderCodexServerCard(serverName, serverConfig) {
|
|||||||
return `
|
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' : ''}"
|
<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-name="${escapeHtml(serverName)}"
|
||||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
data-cli-type="codex"
|
data-cli-type="codex"
|
||||||
data-action="view-details-codex"
|
data-action="view-details-codex"
|
||||||
title="${t('mcp.clickToEdit')}">
|
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"
|
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||||
data-action="copy-codex-to-claude"
|
data-action="copy-codex-to-claude"
|
||||||
data-server-name="${escapeHtml(serverName)}"
|
data-server-name="${escapeHtml(serverName)}"
|
||||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
data-server-config="${encodeConfigData(serverConfig)}"
|
||||||
title="${t('mcp.codex.copyToClaude')}">
|
title="${t('mcp.codex.copyToClaude')}">
|
||||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||||
${t('mcp.codex.copyToClaude')}
|
${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"
|
<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-action="copy-cross-cli"
|
||||||
data-server-name="${escapeHtml(name)}"
|
data-server-name="${escapeHtml(name)}"
|
||||||
data-server-config='${JSON.stringify(config).replace(/'/g, "'")}'
|
data-server-config="${encodeConfigData(config)}"
|
||||||
data-from-cli="${fromCli}"
|
data-from-cli="${fromCli}"
|
||||||
data-target-cli="${targetCli}">
|
data-target-cli="${targetCli}">
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
<i data-lucide="copy" class="w-4 h-4"></i>
|
||||||
@@ -1357,14 +1357,18 @@ function attachMcpEventListeners() {
|
|||||||
// Add from other projects (with scope selection)
|
// Add from other projects (with scope selection)
|
||||||
document.querySelectorAll('.mcp-server-card button[data-action="add-from-other"]').forEach(btn => {
|
document.querySelectorAll('.mcp-server-card button[data-action="add-from-other"]').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const serverName = btn.dataset.serverName;
|
try {
|
||||||
const serverConfig = JSON.parse(btn.dataset.serverConfig);
|
const serverName = btn.dataset.serverName;
|
||||||
const scope = btn.dataset.scope; // 'project' or 'global'
|
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
|
||||||
|
const scope = btn.dataset.scope; // 'project' or 'global'
|
||||||
|
|
||||||
if (scope === 'global') {
|
if (scope === 'global') {
|
||||||
await addGlobalMcpServer(serverName, serverConfig);
|
await addGlobalMcpServer(serverName, serverConfig);
|
||||||
} else {
|
} else {
|
||||||
await copyMcpServerToProject(serverName, serverConfig);
|
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
|
// Install to project buttons
|
||||||
document.querySelectorAll('.mcp-server-card button[data-action="install-to-project"]').forEach(btn => {
|
document.querySelectorAll('.mcp-server-card button[data-action="install-to-project"]').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const serverName = btn.dataset.serverName;
|
try {
|
||||||
const serverConfig = JSON.parse(btn.dataset.serverConfig);
|
const serverName = btn.dataset.serverName;
|
||||||
await copyMcpServerToProject(serverName, serverConfig);
|
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
|
||||||
|
await copyMcpServerToProject(serverName, serverConfig);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MCP] Error installing to project:', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Install to global buttons
|
// Install to global buttons
|
||||||
document.querySelectorAll('.mcp-server-card button[data-action="install-to-global"]').forEach(btn => {
|
document.querySelectorAll('.mcp-server-card button[data-action="install-to-global"]').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const serverName = btn.dataset.serverName;
|
try {
|
||||||
const serverConfig = JSON.parse(btn.dataset.serverConfig);
|
const serverName = btn.dataset.serverName;
|
||||||
await addGlobalMcpServer(serverName, serverConfig);
|
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
|
||||||
|
await addGlobalMcpServer(serverName, serverConfig);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MCP] Error installing to global:', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save as template buttons
|
// Save as template buttons
|
||||||
document.querySelectorAll('.mcp-server-card button[data-action="save-as-template"]').forEach(btn => {
|
document.querySelectorAll('.mcp-server-card button[data-action="save-as-template"]').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const serverName = btn.dataset.serverName;
|
try {
|
||||||
const serverConfig = JSON.parse(btn.dataset.serverConfig);
|
const serverName = btn.dataset.serverName;
|
||||||
await saveMcpAsTemplate(serverName, serverConfig);
|
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 => {
|
document.querySelectorAll('button[data-action="copy-to-codex"]').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const serverName = btn.dataset.serverName;
|
try {
|
||||||
const serverConfig = JSON.parse(btn.dataset.serverConfig);
|
const serverName = btn.dataset.serverName;
|
||||||
console.log('[MCP] Copying to Codex:', serverName);
|
const serverConfig = decodeConfigData(btn.dataset.serverConfig);
|
||||||
await copyClaudeServerToCodex(serverName, 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;
|
const serverName = btn.dataset.serverName;
|
||||||
let serverConfig;
|
let serverConfig;
|
||||||
try {
|
try {
|
||||||
serverConfig = JSON.parse(btn.dataset.serverConfig);
|
serverConfig = decodeConfigData(btn.dataset.serverConfig);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[MCP] JSON Parse Error:', err);
|
console.error('[MCP] JSON Parse Error:', err);
|
||||||
if (typeof showRefreshToast === 'function') {
|
if (typeof showRefreshToast === 'function') {
|
||||||
@@ -1543,7 +1563,7 @@ function attachMcpEventListeners() {
|
|||||||
const serverName = btn.dataset.serverName;
|
const serverName = btn.dataset.serverName;
|
||||||
let serverConfig;
|
let serverConfig;
|
||||||
try {
|
try {
|
||||||
serverConfig = JSON.parse(btn.dataset.serverConfig);
|
serverConfig = decodeConfigData(btn.dataset.serverConfig);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[MCP] JSON Parse Error:', err);
|
console.error('[MCP] JSON Parse Error:', err);
|
||||||
if (typeof showRefreshToast === 'function') {
|
if (typeof showRefreshToast === 'function') {
|
||||||
@@ -1567,14 +1587,21 @@ function attachMcpEventListeners() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const serverName = card.dataset.serverName;
|
const serverName = card.dataset.serverName;
|
||||||
// Decode HTML entities before parsing JSON
|
const configData = card.dataset.serverConfig;
|
||||||
const configStr = unescapeHtml(card.dataset.serverConfig);
|
if (!configData) {
|
||||||
const serverConfig = JSON.parse(configStr);
|
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;
|
const serverSource = card.dataset.serverSource;
|
||||||
console.log('[MCP] Card clicked:', serverName, serverSource);
|
console.log('[MCP] Card clicked:', serverName, serverSource);
|
||||||
showMcpEditModal(serverName, serverConfig, serverSource, 'claude');
|
showMcpEditModal(serverName, serverConfig, serverSource, 'claude');
|
||||||
} catch (err) {
|
} 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 {
|
try {
|
||||||
const serverName = card.dataset.serverName;
|
const serverName = card.dataset.serverName;
|
||||||
// Decode HTML entities before parsing JSON
|
const configData = card.dataset.serverConfig;
|
||||||
const configStr = unescapeHtml(card.dataset.serverConfig);
|
if (!configData) {
|
||||||
const serverConfig = JSON.parse(configStr);
|
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);
|
console.log('[MCP] Codex card clicked:', serverName);
|
||||||
showMcpEditModal(serverName, serverConfig, 'codex', 'codex');
|
showMcpEditModal(serverName, serverConfig, 'codex', 'codex');
|
||||||
} catch (err) {
|
} 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>
|
<i data-lucide="history" class="nav-icon"></i>
|
||||||
<span class="nav-text flex-1" data-i18n="nav.history">History</span>
|
<span class="nav-text flex-1" data-i18n="nav.history">History</span>
|
||||||
</li>
|
</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">
|
<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>
|
<i data-lucide="git-branch" class="nav-icon"></i>
|
||||||
<span class="nav-text flex-1" data-i18n="nav.graphExplorer">Graph</span>
|
<span class="nav-text flex-1" data-i18n="nav.graphExplorer">Graph</span>
|
||||||
</li>
|
</li>
|
||||||
|
-->
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||||
import { spawn, execSync } from 'child_process';
|
import { spawn, execSync, exec } from 'child_process';
|
||||||
import { existsSync, mkdirSync } from 'fs';
|
import { existsSync, mkdirSync } from 'fs';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
@@ -443,22 +443,44 @@ async function executeCodexLens(args: string[], options: ExecuteOptions = {}): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(VENV_PYTHON, ['-m', 'codexlens', ...args], {
|
// Build command string - quote paths for shell execution
|
||||||
cwd,
|
const quotedPython = `"${VENV_PYTHON}"`;
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
const cmdArgs = args.map(arg => {
|
||||||
|
// Quote arguments that contain spaces or special characters
|
||||||
|
if (arg.includes(' ') || arg.includes('\\')) {
|
||||||
|
return `"${arg}"`;
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
// Build full command - on Windows, prepend cd to handle different drives
|
||||||
let stderr = '';
|
let fullCmd: string;
|
||||||
let timedOut = false;
|
if (process.platform === 'win32' && cwd) {
|
||||||
|
// Use cd /d to change drive and directory, then run command
|
||||||
|
fullCmd = `cd /d "${cwd}" && ${quotedPython} -m codexlens ${cmdArgs.join(' ')}`;
|
||||||
|
} else {
|
||||||
|
fullCmd = `${quotedPython} -m codexlens ${cmdArgs.join(' ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
// Use exec with shell option for cross-platform compatibility
|
||||||
const chunk = data.toString();
|
exec(fullCmd, {
|
||||||
stdout += chunk;
|
cwd: process.platform === 'win32' ? undefined : cwd, // Don't use cwd on Windows, use cd command instead
|
||||||
|
timeout,
|
||||||
|
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large outputs
|
||||||
|
shell: process.platform === 'win32' ? process.env.ComSpec : undefined,
|
||||||
|
}, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
if (error.killed) {
|
||||||
|
resolve({ success: false, error: 'Command timed out' });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: stderr || error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Report progress if callback provided
|
// Report final progress if callback provided
|
||||||
if (onProgress) {
|
if (onProgress && stdout) {
|
||||||
const lines = chunk.split('\n');
|
const lines = stdout.split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const progress = parseProgressLine(line.trim());
|
const progress = parseProgressLine(line.trim());
|
||||||
if (progress) {
|
if (progress) {
|
||||||
@@ -466,44 +488,8 @@ async function executeCodexLens(args: string[], options: ExecuteOptions = {}): P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr.on('data', (data) => {
|
resolve({ success: true, output: stdout.trim() });
|
||||||
const chunk = data.toString();
|
|
||||||
stderr += chunk;
|
|
||||||
|
|
||||||
// Also check stderr for progress (some tools output there)
|
|
||||||
if (onProgress) {
|
|
||||||
const lines = chunk.split('\n');
|
|
||||||
for (const line of lines) {
|
|
||||||
const progress = parseProgressLine(line.trim());
|
|
||||||
if (progress) {
|
|
||||||
onProgress(progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
timedOut = true;
|
|
||||||
child.kill('SIGTERM');
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
child.on('close', (code) => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (timedOut) {
|
|
||||||
resolve({ success: false, error: 'Command timed out' });
|
|
||||||
} else if (code === 0) {
|
|
||||||
resolve({ success: true, output: stdout.trim() });
|
|
||||||
} else {
|
|
||||||
resolve({ success: false, error: stderr || `Exit code: ${code}` });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (err) => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
resolve({ success: false, error: `Spawn failed: ${err.message}` });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,26 +27,21 @@ import type { ProgressInfo } from './codex-lens.js';
|
|||||||
const ParamsSchema = z.object({
|
const ParamsSchema = z.object({
|
||||||
action: z.enum(['init', 'search', 'search_files', 'status']).default('search'),
|
action: z.enum(['init', 'search', 'search_files', 'status']).default('search'),
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
mode: z.enum(['auto', 'hybrid', 'exact', 'ripgrep', 'parallel']).default('auto'),
|
mode: z.enum(['auto', 'hybrid', 'exact', 'ripgrep', 'priority']).default('auto'),
|
||||||
output_mode: z.enum(['full', 'files_only', 'count']).default('full'),
|
output_mode: z.enum(['full', 'files_only', 'count']).default('full'),
|
||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
paths: z.array(z.string()).default([]),
|
paths: z.array(z.string()).default([]),
|
||||||
contextLines: z.number().default(0),
|
contextLines: z.number().default(0),
|
||||||
maxResults: z.number().default(100),
|
maxResults: z.number().default(10),
|
||||||
includeHidden: z.boolean().default(false),
|
includeHidden: z.boolean().default(false),
|
||||||
languages: z.array(z.string()).optional(),
|
languages: z.array(z.string()).optional(),
|
||||||
limit: z.number().default(100),
|
limit: z.number().default(10),
|
||||||
parallelWeights: z.object({
|
|
||||||
hybrid: z.number().default(0.5),
|
|
||||||
exact: z.number().default(0.3),
|
|
||||||
ripgrep: z.number().default(0.2),
|
|
||||||
}).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type Params = z.infer<typeof ParamsSchema>;
|
type Params = z.infer<typeof ParamsSchema>;
|
||||||
|
|
||||||
// Search mode constants
|
// Search mode constants
|
||||||
const SEARCH_MODES = ['auto', 'hybrid', 'exact', 'ripgrep', 'parallel'] as const;
|
const SEARCH_MODES = ['auto', 'hybrid', 'exact', 'ripgrep', 'priority'] as const;
|
||||||
|
|
||||||
// Classification confidence threshold
|
// Classification confidence threshold
|
||||||
const CONFIDENCE_THRESHOLD = 0.7;
|
const CONFIDENCE_THRESHOLD = 0.7;
|
||||||
@@ -89,6 +84,7 @@ interface SearchMetadata {
|
|||||||
warning?: string;
|
warning?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
index_status?: 'indexed' | 'not_indexed' | 'partial';
|
index_status?: 'indexed' | 'not_indexed' | 'partial';
|
||||||
|
fallback_history?: string[];
|
||||||
// Init action specific
|
// Init action specific
|
||||||
action?: string;
|
action?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
@@ -120,6 +116,13 @@ interface IndexStatus {
|
|||||||
warning?: string;
|
warning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip ANSI color codes from string (for JSON parsing)
|
||||||
|
*/
|
||||||
|
function stripAnsi(str: string): string {
|
||||||
|
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if CodexLens index exists for current directory
|
* Check if CodexLens index exists for current directory
|
||||||
* @param path - Directory path to check
|
* @param path - Directory path to check
|
||||||
@@ -140,7 +143,7 @@ async function checkIndexStatus(path: string = '.'): Promise<IndexStatus> {
|
|||||||
// Parse status output
|
// Parse status output
|
||||||
try {
|
try {
|
||||||
// Strip ANSI color codes from JSON output
|
// Strip ANSI color codes from JSON output
|
||||||
const cleanOutput = (result.output || '{}').replace(/\x1b\[[0-9;]*m/g, '');
|
const cleanOutput = stripAnsi(result.output || '{}');
|
||||||
const parsed = JSON.parse(cleanOutput);
|
const parsed = JSON.parse(cleanOutput);
|
||||||
// Handle both direct and nested response formats (status returns {success, result: {...}})
|
// Handle both direct and nested response formats (status returns {success, result: {...}})
|
||||||
const status = parsed.result || parsed;
|
const status = parsed.result || parsed;
|
||||||
@@ -293,7 +296,7 @@ function buildRipgrepCommand(params: {
|
|||||||
maxResults: number;
|
maxResults: number;
|
||||||
includeHidden: boolean;
|
includeHidden: boolean;
|
||||||
}): { command: string; args: string[] } {
|
}): { command: string; args: string[] } {
|
||||||
const { query, paths = ['.'], contextLines = 0, maxResults = 100, includeHidden = false } = params;
|
const { query, paths = ['.'], contextLines = 0, maxResults = 10, includeHidden = false } = params;
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
'-n', // Show line numbers
|
'-n', // Show line numbers
|
||||||
@@ -478,7 +481,7 @@ async function executeAutoMode(params: Params): Promise<SearchResult> {
|
|||||||
* No index required, fallback to CodexLens if ripgrep unavailable
|
* No index required, fallback to CodexLens if ripgrep unavailable
|
||||||
*/
|
*/
|
||||||
async function executeRipgrepMode(params: Params): Promise<SearchResult> {
|
async function executeRipgrepMode(params: Params): Promise<SearchResult> {
|
||||||
const { query, paths = [], contextLines = 0, maxResults = 100, includeHidden = false, path = '.' } = params;
|
const { query, paths = [], contextLines = 0, maxResults = 10, includeHidden = false, path = '.' } = params;
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return {
|
return {
|
||||||
@@ -520,8 +523,8 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
|
|||||||
// Parse results
|
// Parse results
|
||||||
let results: SemanticMatch[] = [];
|
let results: SemanticMatch[] = [];
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(result.output || '{}');
|
const parsed = JSON.parse(stripAnsi(result.output || '{}'));
|
||||||
const data = parsed.results || parsed;
|
const data = parsed.result?.results || parsed.results || parsed;
|
||||||
results = (Array.isArray(data) ? data : []).map((item: any) => ({
|
results = (Array.isArray(data) ? data : []).map((item: any) => ({
|
||||||
file: item.path || item.file,
|
file: item.path || item.file,
|
||||||
score: item.score || 0,
|
score: item.score || 0,
|
||||||
@@ -632,7 +635,7 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
|
|||||||
* Requires index
|
* Requires index
|
||||||
*/
|
*/
|
||||||
async function executeCodexLensExactMode(params: Params): Promise<SearchResult> {
|
async function executeCodexLensExactMode(params: Params): Promise<SearchResult> {
|
||||||
const { query, path = '.', limit = 100 } = params;
|
const { query, path = '.', maxResults = 10 } = params;
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return {
|
return {
|
||||||
@@ -653,7 +656,7 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
|
|||||||
// Check index status
|
// Check index status
|
||||||
const indexStatus = await checkIndexStatus(path);
|
const indexStatus = await checkIndexStatus(path);
|
||||||
|
|
||||||
const args = ['search', query, '--limit', limit.toString(), '--mode', 'exact', '--json'];
|
const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'exact', '--json'];
|
||||||
const result = await executeCodexLens(args, { cwd: path });
|
const result = await executeCodexLens(args, { cwd: path });
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -673,8 +676,8 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
|
|||||||
// Parse results
|
// Parse results
|
||||||
let results: SemanticMatch[] = [];
|
let results: SemanticMatch[] = [];
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(result.output || '{}');
|
const parsed = JSON.parse(stripAnsi(result.output || '{}'));
|
||||||
const data = parsed.results || parsed;
|
const data = parsed.result?.results || parsed.results || parsed;
|
||||||
results = (Array.isArray(data) ? data : []).map((item: any) => ({
|
results = (Array.isArray(data) ? data : []).map((item: any) => ({
|
||||||
file: item.path || item.file,
|
file: item.path || item.file,
|
||||||
score: item.score || 0,
|
score: item.score || 0,
|
||||||
@@ -704,7 +707,7 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
|
|||||||
* Requires index with embeddings
|
* Requires index with embeddings
|
||||||
*/
|
*/
|
||||||
async function executeHybridMode(params: Params): Promise<SearchResult> {
|
async function executeHybridMode(params: Params): Promise<SearchResult> {
|
||||||
const { query, path = '.', limit = 100 } = params;
|
const { query, path = '.', maxResults = 10 } = params;
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return {
|
return {
|
||||||
@@ -725,7 +728,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
|
|||||||
// Check index status
|
// Check index status
|
||||||
const indexStatus = await checkIndexStatus(path);
|
const indexStatus = await checkIndexStatus(path);
|
||||||
|
|
||||||
const args = ['search', query, '--limit', limit.toString(), '--mode', 'hybrid', '--json'];
|
const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'hybrid', '--json'];
|
||||||
const result = await executeCodexLens(args, { cwd: path });
|
const result = await executeCodexLens(args, { cwd: path });
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -745,8 +748,8 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
|
|||||||
// Parse results
|
// Parse results
|
||||||
let results: SemanticMatch[] = [];
|
let results: SemanticMatch[] = [];
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(result.output || '{}');
|
const parsed = JSON.parse(stripAnsi(result.output || '{}'));
|
||||||
const data = parsed.results || parsed;
|
const data = parsed.result?.results || parsed.results || parsed;
|
||||||
results = (Array.isArray(data) ? data : []).map((item: any) => ({
|
results = (Array.isArray(data) ? data : []).map((item: any) => ({
|
||||||
file: item.path || item.file,
|
file: item.path || item.file,
|
||||||
score: item.score || 0,
|
score: item.score || 0,
|
||||||
@@ -828,94 +831,114 @@ function applyRRFFusion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mode: parallel - Run all backends simultaneously with RRF fusion
|
* Promise wrapper with timeout support
|
||||||
* Returns best results from hybrid + exact + ripgrep combined
|
* @param promise - The promise to wrap
|
||||||
|
* @param ms - Timeout in milliseconds
|
||||||
|
* @param modeName - Name of the mode for error message
|
||||||
|
* @returns A new promise that rejects on timeout
|
||||||
*/
|
*/
|
||||||
async function executeParallelMode(params: Params): Promise<SearchResult> {
|
function withTimeout<T>(promise: Promise<T>, ms: number, modeName: string): Promise<T> {
|
||||||
const { query, path = '.', limit = 100, parallelWeights } = params;
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error(`'${modeName}' search timed out after ${ms}ms`));
|
||||||
|
}, ms);
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject)
|
||||||
|
.finally(() => clearTimeout(timer));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mode: priority - Fallback search strategy: hybrid -> exact -> ripgrep
|
||||||
|
* Returns results from the first backend that succeeds and provides results.
|
||||||
|
* More efficient than parallel mode - stops as soon as valid results are found.
|
||||||
|
*/
|
||||||
|
async function executePriorityFallbackMode(params: Params): Promise<SearchResult> {
|
||||||
|
const { query, path = '.' } = params;
|
||||||
|
const fallbackHistory: string[] = [];
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
|
return { success: false, error: 'Query is required for search' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check index status first
|
||||||
|
const indexStatus = await checkIndexStatus(path);
|
||||||
|
|
||||||
|
// 1. Try Hybrid search (highest priority) - 90s timeout for large indexes
|
||||||
|
if (indexStatus.indexed && indexStatus.has_embeddings) {
|
||||||
|
try {
|
||||||
|
const hybridResult = await withTimeout(executeHybridMode(params), 90000, 'hybrid');
|
||||||
|
if (hybridResult.success && hybridResult.results && (hybridResult.results as any[]).length > 0) {
|
||||||
|
fallbackHistory.push('hybrid: success');
|
||||||
|
return {
|
||||||
|
...hybridResult,
|
||||||
|
metadata: {
|
||||||
|
...hybridResult.metadata,
|
||||||
|
mode: 'priority',
|
||||||
|
note: 'Result from hybrid search (semantic + vector).',
|
||||||
|
fallback_history: fallbackHistory,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fallbackHistory.push('hybrid: no results');
|
||||||
|
} catch (error) {
|
||||||
|
fallbackHistory.push(`hybrid: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fallbackHistory.push(`hybrid: skipped (${!indexStatus.indexed ? 'no index' : 'no embeddings'})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to Exact search - 10s timeout
|
||||||
|
if (indexStatus.indexed) {
|
||||||
|
try {
|
||||||
|
const exactResult = await withTimeout(executeCodexLensExactMode(params), 10000, 'exact');
|
||||||
|
if (exactResult.success && exactResult.results && (exactResult.results as any[]).length > 0) {
|
||||||
|
fallbackHistory.push('exact: success');
|
||||||
|
return {
|
||||||
|
...exactResult,
|
||||||
|
metadata: {
|
||||||
|
...exactResult.metadata,
|
||||||
|
mode: 'priority',
|
||||||
|
note: 'Result from exact/FTS search (fallback from hybrid).',
|
||||||
|
fallback_history: fallbackHistory,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fallbackHistory.push('exact: no results');
|
||||||
|
} catch (error) {
|
||||||
|
fallbackHistory.push(`exact: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fallbackHistory.push('exact: skipped (no index)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Final fallback to Ripgrep - 5s timeout
|
||||||
|
try {
|
||||||
|
const ripgrepResult = await withTimeout(executeRipgrepMode(params), 5000, 'ripgrep');
|
||||||
|
fallbackHistory.push(ripgrepResult.success ? 'ripgrep: success' : 'ripgrep: failed');
|
||||||
return {
|
return {
|
||||||
success: false,
|
...ripgrepResult,
|
||||||
error: 'Query is required for search',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default weights if not provided
|
|
||||||
const weights = parallelWeights || {
|
|
||||||
hybrid: 0.5,
|
|
||||||
exact: 0.3,
|
|
||||||
ripgrep: 0.2,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run all backends in parallel
|
|
||||||
const [hybridResult, exactResult, ripgrepResult] = await Promise.allSettled([
|
|
||||||
executeHybridMode(params),
|
|
||||||
executeCodexLensExactMode(params),
|
|
||||||
executeRipgrepMode(params),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Collect successful results
|
|
||||||
const resultsMap = new Map<string, any[]>();
|
|
||||||
const backendStatus: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (hybridResult.status === 'fulfilled' && hybridResult.value.success) {
|
|
||||||
resultsMap.set('hybrid', hybridResult.value.results as any[]);
|
|
||||||
backendStatus.hybrid = 'success';
|
|
||||||
} else {
|
|
||||||
backendStatus.hybrid = hybridResult.status === 'rejected'
|
|
||||||
? `error: ${hybridResult.reason}`
|
|
||||||
: `failed: ${(hybridResult as PromiseFulfilledResult<SearchResult>).value.error}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exactResult.status === 'fulfilled' && exactResult.value.success) {
|
|
||||||
resultsMap.set('exact', exactResult.value.results as any[]);
|
|
||||||
backendStatus.exact = 'success';
|
|
||||||
} else {
|
|
||||||
backendStatus.exact = exactResult.status === 'rejected'
|
|
||||||
? `error: ${exactResult.reason}`
|
|
||||||
: `failed: ${(exactResult as PromiseFulfilledResult<SearchResult>).value.error}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ripgrepResult.status === 'fulfilled' && ripgrepResult.value.success) {
|
|
||||||
resultsMap.set('ripgrep', ripgrepResult.value.results as any[]);
|
|
||||||
backendStatus.ripgrep = 'success';
|
|
||||||
} else {
|
|
||||||
backendStatus.ripgrep = ripgrepResult.status === 'rejected'
|
|
||||||
? `error: ${ripgrepResult.reason}`
|
|
||||||
: `failed: ${(ripgrepResult as PromiseFulfilledResult<SearchResult>).value.error}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no results from any backend
|
|
||||||
if (resultsMap.size === 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'All search backends failed',
|
|
||||||
metadata: {
|
metadata: {
|
||||||
mode: 'parallel',
|
...ripgrepResult.metadata,
|
||||||
backend: 'multi-backend',
|
mode: 'priority',
|
||||||
count: 0,
|
note: 'Result from ripgrep search (final fallback).',
|
||||||
query,
|
fallback_history: fallbackHistory,
|
||||||
backend_status: backendStatus,
|
},
|
||||||
} as any,
|
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
fallbackHistory.push(`ripgrep: ${(error as Error).message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply RRF fusion
|
// All modes failed
|
||||||
const fusedResults = applyRRFFusion(resultsMap, weights, limit);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
results: fusedResults,
|
error: 'All search backends in priority mode failed or returned no results.',
|
||||||
metadata: {
|
metadata: {
|
||||||
mode: 'parallel',
|
mode: 'priority',
|
||||||
backend: 'multi-backend',
|
|
||||||
count: fusedResults.length,
|
|
||||||
query,
|
query,
|
||||||
backends_used: Array.from(resultsMap.keys()),
|
fallback_history: fallbackHistory,
|
||||||
backend_status: backendStatus,
|
|
||||||
weights,
|
|
||||||
note: 'Parallel mode runs hybrid + exact + ripgrep simultaneously with RRF fusion',
|
|
||||||
} as any,
|
} as any,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -923,11 +946,11 @@ async function executeParallelMode(params: Params): Promise<SearchResult> {
|
|||||||
// Tool schema for MCP
|
// Tool schema for MCP
|
||||||
export const schema: ToolSchema = {
|
export const schema: ToolSchema = {
|
||||||
name: 'smart_search',
|
name: 'smart_search',
|
||||||
description: `Intelligent code search with five modes: auto, hybrid, exact, ripgrep, parallel.
|
description: `Intelligent code search with five modes: auto, hybrid, exact, ripgrep, priority.
|
||||||
|
|
||||||
**Quick Start:**
|
**Quick Start:**
|
||||||
smart_search(query="authentication logic") # Auto mode (intelligent routing)
|
smart_search(query="authentication logic") # Auto mode (intelligent routing)
|
||||||
smart_search(action="init", path=".") # Initialize FTS index (fast, no embeddings)
|
smart_search(action="init", path=".") # Initialize index (required for hybrid)
|
||||||
smart_search(action="status") # Check index status
|
smart_search(action="status") # Check index status
|
||||||
|
|
||||||
**Five Modes:**
|
**Five Modes:**
|
||||||
@@ -938,7 +961,7 @@ export const schema: ToolSchema = {
|
|||||||
|
|
||||||
2. hybrid: CodexLens RRF fusion (exact + fuzzy + vector)
|
2. hybrid: CodexLens RRF fusion (exact + fuzzy + vector)
|
||||||
- Best quality, semantic understanding
|
- Best quality, semantic understanding
|
||||||
- Requires index with embeddings (create via "ccw view" dashboard)
|
- Requires index with embeddings
|
||||||
|
|
||||||
3. exact: CodexLens FTS (full-text search)
|
3. exact: CodexLens FTS (full-text search)
|
||||||
- Precise keyword matching
|
- Precise keyword matching
|
||||||
@@ -948,20 +971,21 @@ export const schema: ToolSchema = {
|
|||||||
- Fast, no index required
|
- Fast, no index required
|
||||||
- Literal string matching
|
- Literal string matching
|
||||||
|
|
||||||
5. parallel: Run all backends simultaneously
|
5. priority: Fallback strategy for best balance of speed and recall
|
||||||
- Highest recall, runs hybrid + exact + ripgrep in parallel
|
- Tries searches in order: hybrid -> exact -> ripgrep
|
||||||
- Results merged using RRF fusion with configurable weights
|
- Returns results from the first successful search with results
|
||||||
|
- More efficient than running all backends in parallel
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
- search (default): Intelligent search with auto routing
|
- search (default): Intelligent search with auto routing
|
||||||
- init: Create FTS index only (no embeddings, faster). For vector/semantic search, use "ccw view" dashboard
|
- init: Create CodexLens index
|
||||||
- status: Check index and embedding availability
|
- status: Check index and embedding availability
|
||||||
- search_files: Return file paths only
|
- search_files: Return file paths only
|
||||||
|
|
||||||
**Workflow:**
|
**Workflow:**
|
||||||
1. Run action="init" to create FTS index (fast)
|
1. Run action="init" to create index
|
||||||
2. For semantic search: create vector index via "ccw view" dashboard or "codexlens init <path>"
|
2. Use auto mode - it routes to hybrid for NL queries, exact for simple queries
|
||||||
3. Use auto mode - it routes to hybrid for NL queries, exact for simple queries`,
|
3. Use priority mode for comprehensive fallback search`,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -978,7 +1002,7 @@ export const schema: ToolSchema = {
|
|||||||
mode: {
|
mode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: SEARCH_MODES,
|
enum: SEARCH_MODES,
|
||||||
description: 'Search mode: auto (default), hybrid (best quality), exact (CodexLens FTS), ripgrep (fast, no index), parallel (all backends with RRF fusion)',
|
description: 'Search mode: auto (default), hybrid (best quality), exact (CodexLens FTS), ripgrep (fast, no index), priority (fallback: hybrid->exact->ripgrep)',
|
||||||
default: 'auto',
|
default: 'auto',
|
||||||
},
|
},
|
||||||
output_mode: {
|
output_mode: {
|
||||||
@@ -1006,13 +1030,13 @@ export const schema: ToolSchema = {
|
|||||||
},
|
},
|
||||||
maxResults: {
|
maxResults: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Maximum number of results (default: 100)',
|
description: 'Maximum number of results (default: 10)',
|
||||||
default: 100,
|
default: 10,
|
||||||
},
|
},
|
||||||
limit: {
|
limit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Alias for maxResults',
|
description: 'Alias for maxResults',
|
||||||
default: 100,
|
default: 10,
|
||||||
},
|
},
|
||||||
includeHidden: {
|
includeHidden: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -1024,15 +1048,6 @@ export const schema: ToolSchema = {
|
|||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Languages to index (for init action). Example: ["javascript", "typescript"]',
|
description: 'Languages to index (for init action). Example: ["javascript", "typescript"]',
|
||||||
},
|
},
|
||||||
parallelWeights: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
hybrid: { type: 'number', default: 0.5 },
|
|
||||||
exact: { type: 'number', default: 0.3 },
|
|
||||||
ripgrep: { type: 'number', default: 0.2 },
|
|
||||||
},
|
|
||||||
description: 'RRF weights for parallel mode. Weights should sum to 1.0. Default: {hybrid: 0.5, exact: 0.3, ripgrep: 0.2}',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
},
|
},
|
||||||
@@ -1082,12 +1097,13 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
|
|||||||
return { success: false, error: `Invalid params: ${parsed.error.message}` };
|
return { success: false, error: `Invalid params: ${parsed.error.message}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { action, mode, output_mode, limit, maxResults } = parsed.data;
|
const { action, mode, output_mode } = parsed.data;
|
||||||
|
|
||||||
// Use limit if maxResults not provided
|
// Sync limit and maxResults - use the larger of the two if both provided
|
||||||
if (limit && !maxResults) {
|
// This ensures user-provided values take precedence over defaults
|
||||||
parsed.data.maxResults = limit;
|
const effectiveLimit = Math.max(parsed.data.limit || 10, parsed.data.maxResults || 10);
|
||||||
}
|
parsed.data.maxResults = effectiveLimit;
|
||||||
|
parsed.data.limit = effectiveLimit;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result: SearchResult;
|
let result: SearchResult;
|
||||||
@@ -1109,7 +1125,7 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
|
|||||||
|
|
||||||
case 'search':
|
case 'search':
|
||||||
default:
|
default:
|
||||||
// Handle search modes: auto | hybrid | exact | ripgrep | parallel
|
// Handle search modes: auto | hybrid | exact | ripgrep | priority
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'auto':
|
case 'auto':
|
||||||
result = await executeAutoMode(parsed.data);
|
result = await executeAutoMode(parsed.data);
|
||||||
@@ -1123,11 +1139,11 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
|
|||||||
case 'ripgrep':
|
case 'ripgrep':
|
||||||
result = await executeRipgrepMode(parsed.data);
|
result = await executeRipgrepMode(parsed.data);
|
||||||
break;
|
break;
|
||||||
case 'parallel':
|
case 'priority':
|
||||||
result = await executeParallelMode(parsed.data);
|
result = await executePriorityFallbackMode(parsed.data);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported mode: ${mode}. Use: auto, hybrid, exact, ripgrep, or parallel`);
|
throw new Error(`Unsupported mode: ${mode}. Use: auto, hybrid, exact, ripgrep, or priority`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Dict, List, Optional
|
|||||||
try:
|
try:
|
||||||
from codexlens.semantic import SEMANTIC_AVAILABLE
|
from codexlens.semantic import SEMANTIC_AVAILABLE
|
||||||
if SEMANTIC_AVAILABLE:
|
if SEMANTIC_AVAILABLE:
|
||||||
from codexlens.semantic.embedder import Embedder
|
from codexlens.semantic.embedder import Embedder, get_embedder
|
||||||
from codexlens.semantic.vector_store import VectorStore
|
from codexlens.semantic.vector_store import VectorStore
|
||||||
from codexlens.semantic.chunker import Chunker, ChunkConfig
|
from codexlens.semantic.chunker import Chunker, ChunkConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -167,7 +167,8 @@ def generate_embeddings(
|
|||||||
|
|
||||||
# Initialize components
|
# Initialize components
|
||||||
try:
|
try:
|
||||||
embedder = Embedder(profile=model_profile)
|
# Use cached embedder (singleton) for performance
|
||||||
|
embedder = get_embedder(profile=model_profile)
|
||||||
vector_store = VectorStore(index_path)
|
vector_store = VectorStore(index_path)
|
||||||
chunker = Chunker(config=ChunkConfig(max_chunk_size=chunk_size))
|
chunker = Chunker(config=ChunkConfig(max_chunk_size=chunk_size))
|
||||||
|
|
||||||
@@ -201,10 +202,16 @@ def generate_embeddings(
|
|||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback(f"Processing {len(files)} files...")
|
progress_callback(f"Processing {len(files)} files...")
|
||||||
|
|
||||||
# Process each file
|
# Process all files using batch operations for optimal performance
|
||||||
total_chunks = 0
|
|
||||||
failed_files = []
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
failed_files = []
|
||||||
|
|
||||||
|
# --- OPTIMIZATION Step 1: Collect all chunks from all files ---
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Step 1/4: Chunking {len(files)} files...")
|
||||||
|
|
||||||
|
all_chunks_with_paths = [] # List of (chunk, file_path) tuples
|
||||||
|
files_with_chunks = set()
|
||||||
|
|
||||||
for idx, file_row in enumerate(files, 1):
|
for idx, file_row in enumerate(files, 1):
|
||||||
file_path = file_row["full_path"]
|
file_path = file_row["full_path"]
|
||||||
@@ -212,39 +219,88 @@ def generate_embeddings(
|
|||||||
language = file_row["language"] or "python"
|
language = file_row["language"] or "python"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create chunks
|
|
||||||
chunks = chunker.chunk_sliding_window(
|
chunks = chunker.chunk_sliding_window(
|
||||||
content,
|
content,
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
language=language
|
language=language
|
||||||
)
|
)
|
||||||
|
if chunks:
|
||||||
if not chunks:
|
for chunk in chunks:
|
||||||
continue
|
all_chunks_with_paths.append((chunk, file_path))
|
||||||
|
files_with_chunks.add(file_path)
|
||||||
# Generate embeddings
|
|
||||||
for chunk in chunks:
|
|
||||||
embedding = embedder.embed_single(chunk.content)
|
|
||||||
chunk.embedding = embedding
|
|
||||||
|
|
||||||
# Store chunks
|
|
||||||
vector_store.add_chunks(chunks, file_path)
|
|
||||||
total_chunks += len(chunks)
|
|
||||||
|
|
||||||
if progress_callback:
|
|
||||||
progress_callback(f"[{idx}/{len(files)}] {file_path}: {len(chunks)} chunks")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to process {file_path}: {e}")
|
logger.error(f"Failed to chunk {file_path}: {e}")
|
||||||
failed_files.append((file_path, str(e)))
|
failed_files.append((file_path, str(e)))
|
||||||
|
|
||||||
|
if not all_chunks_with_paths:
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"result": {
|
||||||
|
"chunks_created": 0,
|
||||||
|
"files_processed": len(files) - len(failed_files),
|
||||||
|
"files_failed": len(failed_files),
|
||||||
|
"elapsed_time": elapsed_time,
|
||||||
|
"model_profile": model_profile,
|
||||||
|
"model_name": embedder.model_name,
|
||||||
|
"failed_files": failed_files[:5],
|
||||||
|
"index_path": str(index_path),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
total_chunks = len(all_chunks_with_paths)
|
||||||
|
|
||||||
|
# --- OPTIMIZATION Step 2: Batch generate embeddings with memory-safe batching ---
|
||||||
|
# Use smaller batches to avoid OOM errors while still benefiting from batch processing
|
||||||
|
# jina-embeddings-v2-base-code with long chunks needs small batches
|
||||||
|
BATCH_SIZE = 8 # Conservative batch size for memory efficiency
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
num_batches = (total_chunks + BATCH_SIZE - 1) // BATCH_SIZE
|
||||||
|
progress_callback(f"Step 2/4: Generating embeddings for {total_chunks} chunks ({num_batches} batches)...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_embeddings = []
|
||||||
|
for batch_start in range(0, total_chunks, BATCH_SIZE):
|
||||||
|
batch_end = min(batch_start + BATCH_SIZE, total_chunks)
|
||||||
|
batch_contents = [chunk.content for chunk, _ in all_chunks_with_paths[batch_start:batch_end]]
|
||||||
|
batch_embeddings = embedder.embed(batch_contents)
|
||||||
|
all_embeddings.extend(batch_embeddings)
|
||||||
|
|
||||||
|
if progress_callback and total_chunks > BATCH_SIZE:
|
||||||
|
progress_callback(f" Batch {batch_start // BATCH_SIZE + 1}/{(total_chunks + BATCH_SIZE - 1) // BATCH_SIZE}: {len(batch_embeddings)} embeddings")
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Failed to generate embeddings: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- OPTIMIZATION Step 3: Assign embeddings back to chunks ---
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Step 3/4: Assigning {len(all_embeddings)} embeddings...")
|
||||||
|
|
||||||
|
for (chunk, _), embedding in zip(all_chunks_with_paths, all_embeddings):
|
||||||
|
chunk.embedding = embedding
|
||||||
|
|
||||||
|
# --- OPTIMIZATION Step 4: Batch store all chunks in single transaction ---
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Step 4/4: Storing {total_chunks} chunks to database...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
vector_store.add_chunks_batch(all_chunks_with_paths)
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Failed to store chunks: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"result": {
|
"result": {
|
||||||
"chunks_created": total_chunks,
|
"chunks_created": total_chunks,
|
||||||
"files_processed": len(files) - len(failed_files),
|
"files_processed": len(files_with_chunks),
|
||||||
"files_failed": len(failed_files),
|
"files_failed": len(failed_files),
|
||||||
"elapsed_time": elapsed_time,
|
"elapsed_time": elapsed_time,
|
||||||
"model_profile": model_profile,
|
"model_profile": model_profile,
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ class HybridSearchEngine:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# Initialize embedder and vector store
|
# Initialize embedder and vector store
|
||||||
from codexlens.semantic.embedder import Embedder
|
from codexlens.semantic.embedder import get_embedder
|
||||||
from codexlens.semantic.vector_store import VectorStore
|
from codexlens.semantic.vector_store import VectorStore
|
||||||
|
|
||||||
vector_store = VectorStore(index_path)
|
vector_store = VectorStore(index_path)
|
||||||
@@ -285,7 +285,8 @@ class HybridSearchEngine:
|
|||||||
else:
|
else:
|
||||||
profile = "code" # Default fallback
|
profile = "code" # Default fallback
|
||||||
|
|
||||||
embedder = Embedder(profile=profile)
|
# Use cached embedder (singleton) for performance
|
||||||
|
embedder = get_embedder(profile=profile)
|
||||||
|
|
||||||
# Generate query embedding
|
# Generate query embedding
|
||||||
query_embedding = embedder.embed_single(query)
|
query_embedding = embedder.embed_single(query)
|
||||||
|
|||||||
@@ -2,11 +2,57 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Iterable, List
|
import threading
|
||||||
|
from typing import Dict, Iterable, List, Optional
|
||||||
|
|
||||||
from . import SEMANTIC_AVAILABLE
|
from . import SEMANTIC_AVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
# Global embedder cache for singleton pattern
|
||||||
|
_embedder_cache: Dict[str, "Embedder"] = {}
|
||||||
|
_cache_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def get_embedder(profile: str = "code") -> "Embedder":
|
||||||
|
"""Get or create a cached Embedder instance (thread-safe singleton).
|
||||||
|
|
||||||
|
This function provides significant performance improvement by reusing
|
||||||
|
Embedder instances across multiple searches, avoiding repeated model
|
||||||
|
loading overhead (~0.8s per load).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile: Model profile ("fast", "code", "multilingual", "balanced")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cached Embedder instance for the given profile
|
||||||
|
"""
|
||||||
|
global _embedder_cache
|
||||||
|
|
||||||
|
# Fast path: check cache without lock
|
||||||
|
if profile in _embedder_cache:
|
||||||
|
return _embedder_cache[profile]
|
||||||
|
|
||||||
|
# Slow path: acquire lock for initialization
|
||||||
|
with _cache_lock:
|
||||||
|
# Double-check after acquiring lock
|
||||||
|
if profile in _embedder_cache:
|
||||||
|
return _embedder_cache[profile]
|
||||||
|
|
||||||
|
# Create new embedder and cache it
|
||||||
|
embedder = Embedder(profile=profile)
|
||||||
|
# Pre-load model to ensure it's ready
|
||||||
|
embedder._load_model()
|
||||||
|
_embedder_cache[profile] = embedder
|
||||||
|
return embedder
|
||||||
|
|
||||||
|
|
||||||
|
def clear_embedder_cache() -> None:
|
||||||
|
"""Clear the embedder cache (useful for testing or memory management)."""
|
||||||
|
global _embedder_cache
|
||||||
|
with _cache_lock:
|
||||||
|
_embedder_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
class Embedder:
|
class Embedder:
|
||||||
"""Generate embeddings for code chunks using fastembed (ONNX-based).
|
"""Generate embeddings for code chunks using fastembed (ONNX-based).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user