mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: Enhance graph exploration with file and module filtering options
This commit is contained in:
@@ -15,11 +15,9 @@ Before implementation, always:
|
|||||||
```javascript
|
```javascript
|
||||||
smart_search(query="authentication logic") // Auto mode (recommended)
|
smart_search(query="authentication logic") // Auto mode (recommended)
|
||||||
smart_search(action="init", path=".") // First-time setup
|
smart_search(action="init", path=".") // First-time setup
|
||||||
smart_search(query="LoginUser", mode="exact") // Precise matching
|
|
||||||
smart_search(query="import", mode="ripgrep") // Fast, no index
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Modes**: `auto` (intelligent routing), `hybrid` (best quality), `exact` (FTS), `ripgrep` (fast)
|
**Modes**: `auto` (intelligent routing), `hybrid` (best quality), `exact` (FTS)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -167,8 +167,11 @@ function mapRelationType(relType: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Query symbols from all codex-lens databases (hierarchical structure)
|
* Query symbols from all codex-lens databases (hierarchical structure)
|
||||||
|
* @param projectPath Root project path
|
||||||
|
* @param fileFilter Optional file path filter (supports wildcards)
|
||||||
|
* @param moduleFilter Optional module/directory filter
|
||||||
*/
|
*/
|
||||||
async function querySymbols(projectPath: string): Promise<GraphNode[]> {
|
async function querySymbols(projectPath: string, fileFilter?: string, moduleFilter?: string): Promise<GraphNode[]> {
|
||||||
const mapper = new PathMapper();
|
const mapper = new PathMapper();
|
||||||
const rootDbPath = mapper.sourceToIndexDb(projectPath);
|
const rootDbPath = mapper.sourceToIndexDb(projectPath);
|
||||||
const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, '');
|
const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, '');
|
||||||
@@ -190,7 +193,21 @@ async function querySymbols(projectPath: string): Promise<GraphNode[]> {
|
|||||||
try {
|
try {
|
||||||
const db = Database(dbPath, { readonly: true });
|
const db = Database(dbPath, { readonly: true });
|
||||||
|
|
||||||
const rows = db.prepare(`
|
// Build WHERE clause for filtering
|
||||||
|
let whereClause = '';
|
||||||
|
const params: string[] = [];
|
||||||
|
|
||||||
|
if (fileFilter) {
|
||||||
|
const sanitized = sanitizeForLike(fileFilter);
|
||||||
|
whereClause = 'WHERE f.full_path LIKE ?';
|
||||||
|
params.push(`%${sanitized}%`);
|
||||||
|
} else if (moduleFilter) {
|
||||||
|
const sanitized = sanitizeForLike(moduleFilter);
|
||||||
|
whereClause = 'WHERE f.full_path LIKE ?';
|
||||||
|
params.push(`${sanitized}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
s.name,
|
s.name,
|
||||||
@@ -199,8 +216,11 @@ async function querySymbols(projectPath: string): Promise<GraphNode[]> {
|
|||||||
f.full_path as file
|
f.full_path as file
|
||||||
FROM symbols s
|
FROM symbols s
|
||||||
JOIN files f ON s.file_id = f.id
|
JOIN files f ON s.file_id = f.id
|
||||||
|
${whereClause}
|
||||||
ORDER BY f.full_path, s.start_line
|
ORDER BY f.full_path, s.start_line
|
||||||
`).all();
|
`;
|
||||||
|
|
||||||
|
const rows = params.length > 0 ? db.prepare(query).all(...params) : db.prepare(query).all();
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
|
|
||||||
@@ -223,8 +243,11 @@ async function querySymbols(projectPath: string): Promise<GraphNode[]> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Query code relationships from all codex-lens databases (hierarchical structure)
|
* Query code relationships from all codex-lens databases (hierarchical structure)
|
||||||
|
* @param projectPath Root project path
|
||||||
|
* @param fileFilter Optional file path filter (supports wildcards)
|
||||||
|
* @param moduleFilter Optional module/directory filter
|
||||||
*/
|
*/
|
||||||
async function queryRelationships(projectPath: string): Promise<GraphEdge[]> {
|
async function queryRelationships(projectPath: string, fileFilter?: string, moduleFilter?: string): Promise<GraphEdge[]> {
|
||||||
const mapper = new PathMapper();
|
const mapper = new PathMapper();
|
||||||
const rootDbPath = mapper.sourceToIndexDb(projectPath);
|
const rootDbPath = mapper.sourceToIndexDb(projectPath);
|
||||||
const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, '');
|
const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, '');
|
||||||
@@ -246,7 +269,21 @@ async function queryRelationships(projectPath: string): Promise<GraphEdge[]> {
|
|||||||
try {
|
try {
|
||||||
const db = Database(dbPath, { readonly: true });
|
const db = Database(dbPath, { readonly: true });
|
||||||
|
|
||||||
const rows = db.prepare(`
|
// Build WHERE clause for filtering
|
||||||
|
let whereClause = '';
|
||||||
|
const params: string[] = [];
|
||||||
|
|
||||||
|
if (fileFilter) {
|
||||||
|
const sanitized = sanitizeForLike(fileFilter);
|
||||||
|
whereClause = 'WHERE f.full_path LIKE ?';
|
||||||
|
params.push(`%${sanitized}%`);
|
||||||
|
} else if (moduleFilter) {
|
||||||
|
const sanitized = sanitizeForLike(moduleFilter);
|
||||||
|
whereClause = 'WHERE f.full_path LIKE ?';
|
||||||
|
params.push(`${sanitized}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
s.name as source_name,
|
s.name as source_name,
|
||||||
s.start_line as source_line,
|
s.start_line as source_line,
|
||||||
@@ -257,8 +294,11 @@ async function queryRelationships(projectPath: string): Promise<GraphEdge[]> {
|
|||||||
FROM code_relationships r
|
FROM code_relationships r
|
||||||
JOIN symbols s ON r.source_symbol_id = s.id
|
JOIN symbols s ON r.source_symbol_id = s.id
|
||||||
JOIN files f ON s.file_id = f.id
|
JOIN files f ON s.file_id = f.id
|
||||||
|
${whereClause}
|
||||||
ORDER BY f.full_path, s.start_line
|
ORDER BY f.full_path, s.start_line
|
||||||
`).all();
|
`;
|
||||||
|
|
||||||
|
const rows = params.length > 0 ? db.prepare(query).all(...params) : db.prepare(query).all();
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
|
|
||||||
@@ -384,6 +424,8 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
const projectPath = validateProjectPath(rawPath, initialPath);
|
const projectPath = validateProjectPath(rawPath, initialPath);
|
||||||
const limitStr = url.searchParams.get('limit') || '1000';
|
const limitStr = url.searchParams.get('limit') || '1000';
|
||||||
const limit = Math.min(parseInt(limitStr, 10) || 1000, 5000); // Max 5000 nodes
|
const limit = Math.min(parseInt(limitStr, 10) || 1000, 5000); // Max 5000 nodes
|
||||||
|
const fileFilter = url.searchParams.get('file') || undefined;
|
||||||
|
const moduleFilter = url.searchParams.get('module') || undefined;
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
@@ -392,14 +434,15 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allNodes = await querySymbols(projectPath);
|
const allNodes = await querySymbols(projectPath, fileFilter, moduleFilter);
|
||||||
const nodes = allNodes.slice(0, limit);
|
const nodes = allNodes.slice(0, limit);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({
|
res.end(JSON.stringify({
|
||||||
nodes,
|
nodes,
|
||||||
total: allNodes.length,
|
total: allNodes.length,
|
||||||
limit,
|
limit,
|
||||||
hasMore: allNodes.length > limit
|
hasMore: allNodes.length > limit,
|
||||||
|
filters: { file: fileFilter, module: moduleFilter }
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Graph] Error fetching nodes:`, err);
|
console.error(`[Graph] Error fetching nodes:`, err);
|
||||||
@@ -415,6 +458,8 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
const projectPath = validateProjectPath(rawPath, initialPath);
|
const projectPath = validateProjectPath(rawPath, initialPath);
|
||||||
const limitStr = url.searchParams.get('limit') || '2000';
|
const limitStr = url.searchParams.get('limit') || '2000';
|
||||||
const limit = Math.min(parseInt(limitStr, 10) || 2000, 10000); // Max 10000 edges
|
const limit = Math.min(parseInt(limitStr, 10) || 2000, 10000); // Max 10000 edges
|
||||||
|
const fileFilter = url.searchParams.get('file') || undefined;
|
||||||
|
const moduleFilter = url.searchParams.get('module') || undefined;
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
@@ -423,14 +468,15 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allEdges = await queryRelationships(projectPath);
|
const allEdges = await queryRelationships(projectPath, fileFilter, moduleFilter);
|
||||||
const edges = allEdges.slice(0, limit);
|
const edges = allEdges.slice(0, limit);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({
|
res.end(JSON.stringify({
|
||||||
edges,
|
edges,
|
||||||
total: allEdges.length,
|
total: allEdges.length,
|
||||||
limit,
|
limit,
|
||||||
hasMore: allEdges.length > limit
|
hasMore: allEdges.length > limit,
|
||||||
|
filters: { file: fileFilter, module: moduleFilter }
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Graph] Error fetching edges:`, err);
|
console.error(`[Graph] Error fetching edges:`, err);
|
||||||
@@ -440,6 +486,68 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: Get available files and modules for filtering
|
||||||
|
if (pathname === '/api/graph/files') {
|
||||||
|
const rawPath = url.searchParams.get('path') || initialPath;
|
||||||
|
const projectPath = validateProjectPath(rawPath, initialPath);
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Invalid project path', files: [], modules: [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mapper = new PathMapper();
|
||||||
|
const rootDbPath = mapper.sourceToIndexDb(projectPath);
|
||||||
|
const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, '');
|
||||||
|
|
||||||
|
if (!existsSync(indexRoot)) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ files: [], modules: [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbPaths = findAllIndexDbs(indexRoot);
|
||||||
|
const filesSet = new Set<string>();
|
||||||
|
const modulesSet = new Set<string>();
|
||||||
|
|
||||||
|
for (const dbPath of dbPaths) {
|
||||||
|
try {
|
||||||
|
const db = Database(dbPath, { readonly: true });
|
||||||
|
const rows = db.prepare(`SELECT DISTINCT full_path FROM files`).all();
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
rows.forEach((row: any) => {
|
||||||
|
const filePath = row.full_path;
|
||||||
|
filesSet.add(filePath);
|
||||||
|
|
||||||
|
// Extract module path (directory)
|
||||||
|
const lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
|
||||||
|
if (lastSlash > 0) {
|
||||||
|
const modulePath = filePath.substring(0, lastSlash);
|
||||||
|
modulesSet.add(modulePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error(`[Graph] Failed to query files from ${dbPath}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = Array.from(filesSet).sort();
|
||||||
|
const modules = Array.from(modulesSet).sort();
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ files, modules }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Graph] Error fetching files:`, err);
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Failed to fetch files and modules', files: [], modules: [] }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// API: Impact Analysis - Get impact analysis for a symbol
|
// API: Impact Analysis - Get impact analysis for a symbol
|
||||||
if (pathname === '/api/graph/impact') {
|
if (pathname === '/api/graph/impact') {
|
||||||
const rawPath = url.searchParams.get('path') || initialPath;
|
const rawPath = url.searchParams.get('path') || initialPath;
|
||||||
|
|||||||
@@ -1108,6 +1108,12 @@ const i18n = {
|
|||||||
'graph.references': 'references',
|
'graph.references': 'references',
|
||||||
'graph.affectedSymbols': 'Affected Symbols',
|
'graph.affectedSymbols': 'Affected Symbols',
|
||||||
'graph.depth': 'Depth',
|
'graph.depth': 'Depth',
|
||||||
|
'graph.scope': 'Scope',
|
||||||
|
'graph.allFiles': 'All Files',
|
||||||
|
'graph.byModule': 'By Module',
|
||||||
|
'graph.byFile': 'By File',
|
||||||
|
'graph.selectModule': 'Select a module...',
|
||||||
|
'graph.selectFile': 'Select a file...',
|
||||||
|
|
||||||
// CLI Sync (used in claude-manager.js)
|
// CLI Sync (used in claude-manager.js)
|
||||||
'claude.cliSync': 'CLI Auto-Sync',
|
'claude.cliSync': 'CLI Auto-Sync',
|
||||||
@@ -2300,6 +2306,12 @@ const i18n = {
|
|||||||
'graph.references': '引用',
|
'graph.references': '引用',
|
||||||
'graph.symbolType': '符号类型',
|
'graph.symbolType': '符号类型',
|
||||||
'graph.affectedSymbols': '受影响符号',
|
'graph.affectedSymbols': '受影响符号',
|
||||||
|
'graph.scope': '范围',
|
||||||
|
'graph.allFiles': '所有文件',
|
||||||
|
'graph.byModule': '按模块',
|
||||||
|
'graph.byFile': '按文件',
|
||||||
|
'graph.selectModule': '选择模块...',
|
||||||
|
'graph.selectFile': '选择文件...',
|
||||||
|
|
||||||
// CLI Sync (used in claude-manager.js)
|
// CLI Sync (used in claude-manager.js)
|
||||||
'claude.cliSync': 'CLI 自动同步',
|
'claude.cliSync': 'CLI 自动同步',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ var coreMemGraphZoom = null;
|
|||||||
var coreMemGraphSimulation = null;
|
var coreMemGraphSimulation = null;
|
||||||
|
|
||||||
async function renderCoreMemoryView() {
|
async function renderCoreMemoryView() {
|
||||||
const content = document.getElementById('content');
|
const content = document.getElementById('mainContent');
|
||||||
hideStatsAndCarousel();
|
hideStatsAndCarousel();
|
||||||
|
|
||||||
// Fetch core memories
|
// Fetch core memories
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ var edgeFilters = {
|
|||||||
};
|
};
|
||||||
var selectedNode = null;
|
var selectedNode = null;
|
||||||
var searchProcessData = null;
|
var searchProcessData = null;
|
||||||
|
var availableFiles = [];
|
||||||
|
var availableModules = [];
|
||||||
|
var selectedFile = null;
|
||||||
|
var selectedModule = null;
|
||||||
|
var filterMode = 'all'; // 'all', 'file', 'module'
|
||||||
|
|
||||||
// ========== Node/Edge Colors ==========
|
// ========== Node/Edge Colors ==========
|
||||||
var NODE_COLORS = {
|
var NODE_COLORS = {
|
||||||
@@ -53,7 +58,8 @@ async function renderGraphExplorer() {
|
|||||||
// Load data
|
// Load data
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadGraphData(),
|
loadGraphData(),
|
||||||
loadSearchProcessData()
|
loadSearchProcessData(),
|
||||||
|
loadFilesAndModules()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Render layout
|
// Render layout
|
||||||
@@ -71,11 +77,22 @@ async function renderGraphExplorer() {
|
|||||||
// ========== Data Loading ==========
|
// ========== Data Loading ==========
|
||||||
async function loadGraphData() {
|
async function loadGraphData() {
|
||||||
try {
|
try {
|
||||||
var nodesResp = await fetch('/api/graph/nodes');
|
// Build query parameters based on filter mode
|
||||||
|
var queryParams = new URLSearchParams();
|
||||||
|
if (filterMode === 'file' && selectedFile) {
|
||||||
|
queryParams.set('file', selectedFile);
|
||||||
|
} else if (filterMode === 'module' && selectedModule) {
|
||||||
|
queryParams.set('module', selectedModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodesUrl = '/api/graph/nodes' + (queryParams.toString() ? '?' + queryParams.toString() : '');
|
||||||
|
var edgesUrl = '/api/graph/edges' + (queryParams.toString() ? '?' + queryParams.toString() : '');
|
||||||
|
|
||||||
|
var nodesResp = await fetch(nodesUrl);
|
||||||
if (!nodesResp.ok) throw new Error('Failed to load graph nodes');
|
if (!nodesResp.ok) throw new Error('Failed to load graph nodes');
|
||||||
var nodesData = await nodesResp.json();
|
var nodesData = await nodesResp.json();
|
||||||
|
|
||||||
var edgesResp = await fetch('/api/graph/edges');
|
var edgesResp = await fetch(edgesUrl);
|
||||||
if (!edgesResp.ok) throw new Error('Failed to load graph edges');
|
if (!edgesResp.ok) throw new Error('Failed to load graph edges');
|
||||||
var edgesData = await edgesResp.json();
|
var edgesData = await edgesResp.json();
|
||||||
|
|
||||||
@@ -91,6 +108,23 @@ async function loadGraphData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadFilesAndModules() {
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/graph/files');
|
||||||
|
if (!response.ok) throw new Error('Failed to load files and modules');
|
||||||
|
var data = await response.json();
|
||||||
|
|
||||||
|
availableFiles = data.files || [];
|
||||||
|
availableModules = data.modules || [];
|
||||||
|
return { files: availableFiles, modules: availableModules };
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load files and modules:', err);
|
||||||
|
availableFiles = [];
|
||||||
|
availableModules = [];
|
||||||
|
return { files: [], modules: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCoreMemoryGraphData() {
|
async function loadCoreMemoryGraphData() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/core-memory/graph');
|
var response = await fetch('/api/core-memory/graph');
|
||||||
@@ -219,6 +253,40 @@ function renderGraphView() {
|
|||||||
|
|
||||||
function renderFilterDropdowns() {
|
function renderFilterDropdowns() {
|
||||||
return '<div class="filter-dropdowns">' +
|
return '<div class="filter-dropdowns">' +
|
||||||
|
// Scope filter
|
||||||
|
'<div class="filter-group scope-filter">' +
|
||||||
|
'<label>' + t('graph.scope') + '</label>' +
|
||||||
|
'<div class="scope-selector">' +
|
||||||
|
'<label class="filter-radio">' +
|
||||||
|
'<input type="radio" name="scopeMode" value="all" ' + (filterMode === 'all' ? 'checked' : '') + ' onchange="changeScopeMode(\'all\')">' +
|
||||||
|
'<span>' + t('graph.allFiles') + '</span>' +
|
||||||
|
'</label>' +
|
||||||
|
'<label class="filter-radio">' +
|
||||||
|
'<input type="radio" name="scopeMode" value="module" ' + (filterMode === 'module' ? 'checked' : '') + ' onchange="changeScopeMode(\'module\')">' +
|
||||||
|
'<span>' + t('graph.byModule') + '</span>' +
|
||||||
|
'</label>' +
|
||||||
|
'<label class="filter-radio">' +
|
||||||
|
'<input type="radio" name="scopeMode" value="file" ' + (filterMode === 'file' ? 'checked' : '') + ' onchange="changeScopeMode(\'file\')">' +
|
||||||
|
'<span>' + t('graph.byFile') + '</span>' +
|
||||||
|
'</label>' +
|
||||||
|
'</div>' +
|
||||||
|
// Module selector (shown when filterMode === 'module')
|
||||||
|
(filterMode === 'module' ?
|
||||||
|
'<select id="moduleSelect" class="filter-select" onchange="selectModule(this.value)">' +
|
||||||
|
'<option value="">' + t('graph.selectModule') + '</option>' +
|
||||||
|
availableModules.map(function(module) {
|
||||||
|
return '<option value="' + escapeHtml(module) + '" ' + (selectedModule === module ? 'selected' : '') + '>' + escapeHtml(module) + '</option>';
|
||||||
|
}).join('') +
|
||||||
|
'</select>' : '') +
|
||||||
|
// File selector (shown when filterMode === 'file')
|
||||||
|
(filterMode === 'file' ?
|
||||||
|
'<select id="fileSelect" class="filter-select" onchange="selectFile(this.value)">' +
|
||||||
|
'<option value="">' + t('graph.selectFile') + '</option>' +
|
||||||
|
availableFiles.map(function(file) {
|
||||||
|
return '<option value="' + escapeHtml(file) + '" ' + (selectedFile === file ? 'selected' : '') + '>' + escapeHtml(file) + '</option>';
|
||||||
|
}).join('') +
|
||||||
|
'</select>' : '') +
|
||||||
|
'</div>' +
|
||||||
'<div class="filter-group">' +
|
'<div class="filter-group">' +
|
||||||
'<label>' + t('graph.nodeTypes') + '</label>' +
|
'<label>' + t('graph.nodeTypes') + '</label>' +
|
||||||
Object.keys(NODE_COLORS).map(function(type) {
|
Object.keys(NODE_COLORS).map(function(type) {
|
||||||
@@ -845,6 +913,42 @@ function cleanupGraphExplorer() {
|
|||||||
searchProcessData = null;
|
searchProcessData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Scope Filter Actions ==========
|
||||||
|
async function changeScopeMode(mode) {
|
||||||
|
filterMode = mode;
|
||||||
|
selectedFile = null;
|
||||||
|
selectedModule = null;
|
||||||
|
|
||||||
|
// Re-render the filter panel
|
||||||
|
var sidebar = document.querySelector('.graph-sidebar');
|
||||||
|
if (sidebar) {
|
||||||
|
var controlsSection = sidebar.querySelector('.graph-controls-section');
|
||||||
|
if (controlsSection) {
|
||||||
|
controlsSection.innerHTML = '<h3>' + t('graph.filters') + '</h3>' + renderFilterDropdowns();
|
||||||
|
if (window.lucide) lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If mode is 'all', reload graph immediately
|
||||||
|
if (mode === 'all') {
|
||||||
|
await refreshGraphData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectModule(modulePath) {
|
||||||
|
selectedModule = modulePath;
|
||||||
|
if (modulePath) {
|
||||||
|
await refreshGraphData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectFile(filePath) {
|
||||||
|
selectedFile = filePath;
|
||||||
|
if (filePath) {
|
||||||
|
await refreshGraphData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register cleanup on navigation (called by navigation.js before switching views)
|
// Register cleanup on navigation (called by navigation.js before switching views)
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.cleanupGraphExplorer = cleanupGraphExplorer;
|
window.cleanupGraphExplorer = cleanupGraphExplorer;
|
||||||
|
|||||||
@@ -213,8 +213,9 @@ async function checkToolAvailability(tool: string): Promise<ToolAvailability> {
|
|||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const command = isWindows ? 'where' : 'which';
|
const command = isWindows ? 'where' : 'which';
|
||||||
|
|
||||||
|
// Direct spawn - where/which are system commands that don't need shell wrapper
|
||||||
const child = spawn(command, [tool], {
|
const child = spawn(command, [tool], {
|
||||||
shell: isWindows,
|
shell: false,
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -757,25 +758,11 @@ async function executeCliTool(
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const isWindows = process.platform === 'win32';
|
// Direct spawn without shell - CLI tools (codex/gemini/qwen) don't need shell wrapper
|
||||||
|
// This avoids Windows cmd.exe ENOENT errors and simplifies argument handling
|
||||||
// On Windows with shell:true, we need to properly quote args containing spaces
|
const child = spawn(command, args, {
|
||||||
let spawnArgs = args;
|
|
||||||
|
|
||||||
if (isWindows) {
|
|
||||||
// Quote arguments containing spaces for cmd.exe
|
|
||||||
spawnArgs = args.map(arg => {
|
|
||||||
if (arg.includes(' ') || arg.includes('"')) {
|
|
||||||
// Escape existing quotes and wrap in quotes
|
|
||||||
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const child = spawn(command, spawnArgs, {
|
|
||||||
cwd: workingDir,
|
cwd: workingDir,
|
||||||
shell: isWindows,
|
shell: false,
|
||||||
stdio: [useStdin ? 'pipe' : 'ignore', 'pipe', 'pipe']
|
stdio: [useStdin ? 'pipe' : 'ignore', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user