mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-12 08:41:34 +00:00
UI minor fixes
This commit is contained in:
parent
2e59a894c1
commit
88ecbf5eb4
17 changed files with 705 additions and 233 deletions
|
|
@ -41,12 +41,12 @@ const AppContent = () => {
|
|||
const projectName = file.name.replace('.zip', '');
|
||||
setProjectName(projectName);
|
||||
setViewMode('loading');
|
||||
|
||||
|
||||
try {
|
||||
const result = await runPipeline(file, (progress) => {
|
||||
setProgress(progress);
|
||||
});
|
||||
|
||||
|
||||
setGraph(result.graph);
|
||||
setFileContents(result.fileContents);
|
||||
setViewMode('exploring');
|
||||
|
|
@ -54,9 +54,9 @@ const AppContent = () => {
|
|||
// Initialize (or re-initialize) the agent AFTER a repo loads so it captures
|
||||
// the current codebase context (file contents + graph tools) in the worker.
|
||||
if (getActiveProviderConfig()) {
|
||||
initializeAgent();
|
||||
initializeAgent(projectName);
|
||||
}
|
||||
|
||||
|
||||
// Auto-start embeddings pipeline in background
|
||||
// Uses WebGPU if available, falls back to WASM
|
||||
startEmbeddings().catch((err) => {
|
||||
|
|
@ -86,15 +86,15 @@ const AppContent = () => {
|
|||
// Extract project name from first file path (e.g., "owner-repo-123/src/..." -> "owner-repo")
|
||||
const firstPath = files[0]?.path || 'repository';
|
||||
const projectName = firstPath.split('/')[0].replace(/-\d+$/, '') || 'repository';
|
||||
|
||||
|
||||
setProjectName(projectName);
|
||||
setViewMode('loading');
|
||||
|
||||
|
||||
try {
|
||||
const result = await runPipelineFromFiles(files, (progress) => {
|
||||
setProgress(progress);
|
||||
});
|
||||
|
||||
|
||||
setGraph(result.graph);
|
||||
setFileContents(result.fileContents);
|
||||
setViewMode('exploring');
|
||||
|
|
@ -102,9 +102,9 @@ const AppContent = () => {
|
|||
// Initialize (or re-initialize) the agent AFTER a repo loads so it captures
|
||||
// the current codebase context (file contents + graph tools) in the worker.
|
||||
if (getActiveProviderConfig()) {
|
||||
initializeAgent();
|
||||
initializeAgent(projectName);
|
||||
}
|
||||
|
||||
|
||||
// Auto-start embeddings pipeline in background
|
||||
// Uses WebGPU if available, falls back to WASM
|
||||
startEmbeddings().catch((err) => {
|
||||
|
|
@ -154,11 +154,11 @@ const AppContent = () => {
|
|||
return (
|
||||
<div className="flex flex-col h-screen bg-void overflow-hidden">
|
||||
<Header onFocusNode={handleFocusNode} />
|
||||
|
||||
|
||||
<main className="flex-1 flex min-h-0">
|
||||
{/* Left Panel - File Tree */}
|
||||
<FileTreePanel onFocusNode={handleFocusNode} />
|
||||
|
||||
|
||||
{/* Graph area - takes remaining space */}
|
||||
<div className="flex-1 relative min-w-0">
|
||||
<GraphCanvas ref={graphCanvasRef} />
|
||||
|
|
@ -170,13 +170,13 @@ const AppContent = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right Panel - Code & Chat (tabbed) */}
|
||||
{isRightPanelOpen && <RightPanel />}
|
||||
</main>
|
||||
|
||||
|
||||
<StatusBar />
|
||||
|
||||
|
||||
{/* Settings Panel (modal) */}
|
||||
<SettingsPanel
|
||||
isOpen={isSettingsPanelOpen}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Shows connection status and setup instructions if bridge not found.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Copy, Check, AlertCircle, X, Sparkles } from 'lucide-react';
|
||||
import { getMCPClient, type CodebaseContext } from '../core/mcp/mcp-client';
|
||||
|
||||
|
|
@ -45,6 +45,8 @@ export function MCPToggle({
|
|||
const [status, setStatus] = useState<ConnectionState>('disconnected');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [hasConnectionError, setHasConnectionError] = useState(false);
|
||||
const setupPopupRef = useRef<HTMLDivElement | null>(null);
|
||||
const [showTip, setShowTip] = useState(false);
|
||||
|
||||
const isConnected = status === 'connected';
|
||||
|
|
@ -62,6 +64,21 @@ export function MCPToggle({
|
|||
}
|
||||
}, [showOnboardingTip]);
|
||||
|
||||
// Close setup popup when clicking outside
|
||||
useEffect(() => {
|
||||
if (!showSetup) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!setupPopupRef.current) return;
|
||||
if (!setupPopupRef.current.contains(event.target as Node)) {
|
||||
setShowSetup(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [showSetup]);
|
||||
|
||||
const dismissTip = () => {
|
||||
setShowTip(false);
|
||||
localStorage.setItem(MCP_TIP_DISMISSED_KEY, 'true');
|
||||
|
|
@ -71,6 +88,7 @@ export function MCPToggle({
|
|||
const client = getMCPClient();
|
||||
setStatus('connecting');
|
||||
setShowTip(false);
|
||||
setHasConnectionError(false);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
|
|
@ -125,6 +143,7 @@ export function MCPToggle({
|
|||
|
||||
setStatus('connected');
|
||||
setShowSetup(false);
|
||||
setHasConnectionError(false);
|
||||
localStorage.setItem(MCP_TIP_DISMISSED_KEY, 'true');
|
||||
|
||||
// Send codebase context after connecting
|
||||
|
|
@ -141,6 +160,7 @@ export function MCPToggle({
|
|||
} catch {
|
||||
setStatus('error');
|
||||
setShowSetup(true);
|
||||
setHasConnectionError(true);
|
||||
}
|
||||
}, [onSearch, onCypher, onBlastRadius, onHighlight, onGrep, onRead, getContext]);
|
||||
|
||||
|
|
@ -148,6 +168,7 @@ export function MCPToggle({
|
|||
const client = getMCPClient();
|
||||
client.disconnect();
|
||||
setStatus('disconnected');
|
||||
setHasConnectionError(false);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
|
|
@ -158,8 +179,8 @@ export function MCPToggle({
|
|||
}
|
||||
}, [isConnected, isConnecting, connect, disconnect]);
|
||||
|
||||
const copyCommand = () => {
|
||||
navigator.clipboard.writeText('npx gitnexus-mcp setup');
|
||||
const copyCommand = (command: string) => {
|
||||
navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
|
@ -168,13 +189,19 @@ export function MCPToggle({
|
|||
useEffect(() => {
|
||||
const client = getMCPClient();
|
||||
const unsubscribe = client.onConnectionChange((connected) => {
|
||||
setStatus(connected ? 'connected' : 'disconnected');
|
||||
if (connected) {
|
||||
setStatus('connected');
|
||||
setShowSetup(false);
|
||||
setHasConnectionError(false);
|
||||
} else {
|
||||
setStatus('disconnected');
|
||||
}
|
||||
});
|
||||
return () => { unsubscribe(); };
|
||||
}, []);
|
||||
|
||||
// Error state - show setup popup
|
||||
if (status === 'error' && showSetup) {
|
||||
if (hasConnectionError && showSetup) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<span className="text-xs text-text-primary font-medium">MCP</span>
|
||||
|
|
@ -183,30 +210,74 @@ export function MCPToggle({
|
|||
className="flex items-center gap-1.5 px-2 py-1 text-xs text-amber-400 hover:bg-amber-400/10 rounded transition-colors"
|
||||
>
|
||||
<AlertCircle className="w-3.5 h-3.5" />
|
||||
<span>Setup</span>
|
||||
<span>Daemon</span>
|
||||
</button>
|
||||
|
||||
<div className="absolute top-full right-0 mt-2 p-4 bg-surface border border-border rounded-lg shadow-xl z-50 w-80">
|
||||
<p className="text-sm text-text-secondary mb-3">
|
||||
Run this command to enable external AI agents:
|
||||
<div
|
||||
ref={setupPopupRef}
|
||||
className="absolute top-full right-0 mt-2 p-5 bg-surface border border-border rounded-lg shadow-xl z-50 w-[28rem]"
|
||||
>
|
||||
<p className="text-sm text-text-primary mb-4">
|
||||
MCP daemon isn't running. Follow the steps below:
|
||||
</p>
|
||||
<div className="flex items-center gap-2 bg-background p-2 rounded font-mono text-sm">
|
||||
<code className="flex-1 text-green-400">npx gitnexus-mcp setup</code>
|
||||
<button
|
||||
onClick={copyCommand}
|
||||
className="p-1 hover:bg-surface-hover rounded"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-sm text-text-secondary mb-2">1) Install/configure your IDE:</p>
|
||||
<div className="flex items-center gap-2 bg-background/60 border border-border/60 px-3 py-2 rounded font-mono text-sm">
|
||||
<code className="flex-1 text-text-primary">npx gitnexus-mcp setup</code>
|
||||
<button
|
||||
onClick={() => copyCommand('npx gitnexus-mcp setup')}
|
||||
className="p-1.5 hover:bg-surface-hover rounded"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-sm text-text-secondary mb-2">2) Start the daemon (recommended):</p>
|
||||
<div className="flex items-center gap-2 bg-background/60 border border-border/60 px-3 py-2 rounded font-mono text-sm">
|
||||
<code className="flex-1 text-text-primary">npx gitnexus-mcp daemon</code>
|
||||
<button
|
||||
onClick={() => copyCommand('npx gitnexus-mcp daemon')}
|
||||
className="p-1.5 hover:bg-surface-hover rounded"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-sm text-text-secondary mb-2">Dev mode (from `gitnexus-mcp` dir):</p>
|
||||
<div className="flex items-center gap-2 bg-background/60 border border-border/60 px-3 py-2 rounded font-mono text-sm">
|
||||
<code className="flex-1 text-text-primary">npm run build && node dist/cli.js daemon</code>
|
||||
<button
|
||||
onClick={() => copyCommand('npm run build && node dist/cli.js daemon')}
|
||||
className="p-1.5 hover:bg-surface-hover rounded"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowSetup(false); connect(); }}
|
||||
className="mt-3 w-full px-3 py-1.5 text-sm bg-green-500/20 text-green-400 rounded hover:bg-green-500/30 transition-colors"
|
||||
className="mt-2 w-full px-3 py-1.5 text-sm bg-green-500/20 text-green-400 rounded hover:bg-green-500/30 transition-colors"
|
||||
>
|
||||
Retry Connection
|
||||
</button>
|
||||
|
|
@ -282,7 +353,7 @@ export function MCPToggle({
|
|||
<div className="flex items-center gap-2 bg-background p-2 rounded font-mono text-sm">
|
||||
<code className="flex-1 text-green-400">npx gitnexus-mcp setup</code>
|
||||
<button
|
||||
onClick={copyCommand}
|
||||
onClick={() => copyCommand('npx gitnexus-mcp setup')}
|
||||
className="p-1 hover:bg-surface-hover rounded"
|
||||
title="Copy command"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ export interface GraphRelationship {
|
|||
sourceId: string,
|
||||
targetId: string,
|
||||
type: RelationshipType,
|
||||
/** Confidence score 0-1 (1.0 = certain, lower = uncertain resolution) */
|
||||
confidence: number,
|
||||
/** Resolution reason: 'import-resolved', 'same-file', 'fuzzy-global', or empty for non-CALLS */
|
||||
reason: string,
|
||||
}
|
||||
|
||||
export interface KnowledgeGraph {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ const findEnclosingFunction = (
|
|||
if (nodeId) return nodeId;
|
||||
|
||||
// Fallback: generate ID based on name and file
|
||||
return generateId('Function', `${filePath}:${funcName}`);
|
||||
const fallbackLabel = current.type === 'method_definition' ? 'Method' : 'Function';
|
||||
return generateId(fallbackLabel, `${filePath}:${funcName}`);
|
||||
}
|
||||
|
||||
// Couldn't determine function name - try parent (might be nested)
|
||||
|
|
@ -143,15 +144,15 @@ export const processCalls = async (
|
|||
// Skip common built-ins and noise
|
||||
if (isBuiltInOrNoise(calledName)) return;
|
||||
|
||||
// 4. Resolve the target using priority strategy
|
||||
const targetNodeId = resolveCallTarget(
|
||||
// 4. Resolve the target using priority strategy (returns confidence)
|
||||
const resolved = resolveCallTarget(
|
||||
calledName,
|
||||
file.path,
|
||||
symbolTable,
|
||||
importMap
|
||||
);
|
||||
|
||||
if (!targetNodeId) return;
|
||||
if (!resolved) return;
|
||||
|
||||
// 5. Find the enclosing function (caller)
|
||||
const callNode = captureMap['call'];
|
||||
|
|
@ -160,13 +161,15 @@ export const processCalls = async (
|
|||
// Use enclosing function as source, fallback to file for top-level calls
|
||||
const sourceId = enclosingFuncId || generateId('File', file.path);
|
||||
|
||||
const relId = generateId('CALLS', `${sourceId}:${calledName}->${targetNodeId}`);
|
||||
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
||||
|
||||
graph.addRelationship({
|
||||
id: relId,
|
||||
sourceId,
|
||||
targetId: targetNodeId,
|
||||
type: 'CALLS'
|
||||
targetId: resolved.nodeId,
|
||||
type: 'CALLS',
|
||||
confidence: resolved.confidence,
|
||||
reason: resolved.reason,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -177,35 +180,52 @@ export const processCalls = async (
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolution result with confidence scoring
|
||||
*/
|
||||
interface ResolveResult {
|
||||
nodeId: string;
|
||||
confidence: number; // 0-1: how sure are we?
|
||||
reason: string; // 'import-resolved' | 'same-file' | 'fuzzy-global'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a function call to its target node ID using priority strategy:
|
||||
* A. Check imported files first (highest confidence)
|
||||
* B. Check local file definitions
|
||||
* C. Fuzzy global search (lowest confidence)
|
||||
*
|
||||
* Returns confidence score so agents know what to trust.
|
||||
*/
|
||||
const resolveCallTarget = (
|
||||
calledName: string,
|
||||
currentFile: string,
|
||||
symbolTable: SymbolTable,
|
||||
importMap: ImportMap
|
||||
): string | null => {
|
||||
// Strategy A: Check imported files
|
||||
): ResolveResult | null => {
|
||||
// Strategy A: Check imported files (HIGH confidence - we know the import chain)
|
||||
const importedFiles = importMap.get(currentFile);
|
||||
if (importedFiles) {
|
||||
for (const importedFile of importedFiles) {
|
||||
const nodeId = symbolTable.lookupExact(importedFile, calledName);
|
||||
if (nodeId) return nodeId;
|
||||
if (nodeId) {
|
||||
return { nodeId, confidence: 0.9, reason: 'import-resolved' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy B: Check local file (same file definition)
|
||||
// Strategy B: Check local file (HIGH confidence - same file definition)
|
||||
const localNodeId = symbolTable.lookupExact(currentFile, calledName);
|
||||
if (localNodeId) return localNodeId;
|
||||
if (localNodeId) {
|
||||
return { nodeId: localNodeId, confidence: 0.85, reason: 'same-file' };
|
||||
}
|
||||
|
||||
// Strategy C: Fuzzy global search (pick first match)
|
||||
// Strategy C: Fuzzy global search (LOW confidence - just matching by name)
|
||||
const fuzzyMatches = symbolTable.lookupFuzzy(calledName);
|
||||
if (fuzzyMatches.length > 0) {
|
||||
return fuzzyMatches[0].nodeId;
|
||||
// Lower confidence if multiple matches exist (more ambiguous)
|
||||
const confidence = fuzzyMatches.length === 1 ? 0.5 : 0.3;
|
||||
return { nodeId: fuzzyMatches[0].nodeId, confidence, reason: 'fuzzy-global' };
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@ export const processHeritage = async (
|
|||
id: relId,
|
||||
sourceId: childId,
|
||||
targetId: parentId,
|
||||
type: 'EXTENDS'
|
||||
type: 'EXTENDS',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +111,9 @@ export const processHeritage = async (
|
|||
id: relId,
|
||||
sourceId: classId,
|
||||
targetId: interfaceId,
|
||||
type: 'IMPLEMENTS'
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,9 @@ export const processImports = async (
|
|||
id: relId,
|
||||
sourceId,
|
||||
targetId,
|
||||
type: 'IMPORTS'
|
||||
type: 'IMPORTS',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
|
||||
// B. Update Import Map (For Pass 4)
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@ export const processParsing = async (
|
|||
id: relId,
|
||||
sourceId: fileId,
|
||||
targetId: nodeId,
|
||||
type: 'DEFINES'
|
||||
type: 'DEFINES',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
};
|
||||
|
||||
graph.addRelationship(relationship);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ export const processStructure = ( graph: KnowledgeGraph, paths: string[])=>{
|
|||
id: relId,
|
||||
type: 'CONTAINS',
|
||||
sourceId: parentId,
|
||||
targetId: nodeId
|
||||
targetId: nodeId,
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
}
|
||||
|
||||
graph.addRelationship(relationship)
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ const extractContent = (
|
|||
|
||||
export interface CSVData {
|
||||
nodes: Map<NodeTableName, string>;
|
||||
relCSV: string; // Single relation CSV with from,to,type columns
|
||||
relCSV: string; // Single relation CSV with from,to,type,confidence,reason columns
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -202,10 +202,13 @@ const generateCodeElementCSV = (
|
|||
|
||||
/**
|
||||
* Generate CSV for the single CodeRelation table
|
||||
* Headers: from,to,type
|
||||
* Headers: from,to,type,confidence,reason
|
||||
*
|
||||
* confidence: 0-1 score for CALLS edges (how sure are we about the target?)
|
||||
* reason: 'import-resolved' | 'same-file' | 'fuzzy-global' (or empty for non-CALLS)
|
||||
*/
|
||||
const generateRelationCSV = (graph: KnowledgeGraph): string => {
|
||||
const headers = ['from', 'to', 'type'];
|
||||
const headers = ['from', 'to', 'type', 'confidence', 'reason'];
|
||||
const rows: string[] = [headers.join(',')];
|
||||
|
||||
for (const rel of graph.relationships) {
|
||||
|
|
@ -213,6 +216,8 @@ const generateRelationCSV = (graph: KnowledgeGraph): string => {
|
|||
escapeCSVField(rel.sourceId),
|
||||
escapeCSVField(rel.targetId),
|
||||
escapeCSVField(rel.type),
|
||||
escapeCSVNumber(rel.confidence, 1.0),
|
||||
escapeCSVField(rel.reason),
|
||||
].join(','));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,35 +113,56 @@ export const loadGraphToKuzu = async (
|
|||
}
|
||||
|
||||
// 5. INSERT relations one by one (COPY doesn't work with multi-pair REL tables)
|
||||
// Parse CSV format: "from","to","type"
|
||||
// Parse CSV format: "from","to","type",confidence,"reason"
|
||||
let insertedRels = 0;
|
||||
let skippedRels = 0;
|
||||
const skippedRelStats = new Map<string, number>();
|
||||
for (const line of relLines) {
|
||||
try {
|
||||
// Parse CSV - handle quoted fields
|
||||
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)"/);
|
||||
// Parse CSV - handle quoted fields and numeric confidence
|
||||
// Format: "from","to","type",confidence,"reason"
|
||||
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)"/);
|
||||
if (!match) continue;
|
||||
|
||||
const [, fromId, toId, relType] = match;
|
||||
const [, fromId, toId, relType, confidenceStr, reason] = match;
|
||||
const confidence = parseFloat(confidenceStr) || 1.0;
|
||||
|
||||
// Extract labels from node IDs (format: Label:path:name)
|
||||
const fromLabel = fromId.split(':')[0];
|
||||
const toLabel = toId.split(':')[0];
|
||||
|
||||
// INSERT with explicit node matching
|
||||
// INSERT with explicit node matching (including confidence and reason)
|
||||
const insertQuery = `
|
||||
MATCH (a:${fromLabel} {id: '${fromId.replace(/'/g, "''")}'})
|
||||
MATCH (b:${toLabel} {id: '${toId.replace(/'/g, "''")}'})
|
||||
CREATE (a)-[:${REL_TABLE_NAME} {type: '${relType}'}]->(b)
|
||||
CREATE (a)-[:${REL_TABLE_NAME} {type: '${relType}', confidence: ${confidence}, reason: '${reason.replace(/'/g, "''")}'}]->(b)
|
||||
`;
|
||||
await conn.query(insertQuery);
|
||||
insertedRels++;
|
||||
} catch {
|
||||
// Skip failed insertions (nodes might not exist)
|
||||
// Skip failed insertions (nodes might not exist, or relation pair not allowed by schema)
|
||||
skippedRels++;
|
||||
if (import.meta.env.DEV) {
|
||||
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)"/);
|
||||
if (match) {
|
||||
const [, fromId, toId, relType] = match;
|
||||
const fromLabel = fromId.split(':')[0];
|
||||
const toLabel = toId.split(':')[0];
|
||||
const key = `${relType}:${fromLabel}->${toLabel}`;
|
||||
skippedRelStats.set(key, (skippedRelStats.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`KuzuDB: Inserted ${insertedRels}/${relCount} relations`);
|
||||
if (skippedRels > 0) {
|
||||
const topSkipped = Array.from(skippedRelStats.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10);
|
||||
console.warn(`KuzuDB: Skipped ${skippedRels}/${relCount} relations (top by kind/pair):`, topSkipped);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Verify results
|
||||
|
|
@ -189,6 +210,7 @@ const getCopyQuery = (table: NodeTableName, path: string): string => {
|
|||
|
||||
/**
|
||||
* Execute a Cypher query against the database
|
||||
* Returns results as named objects (not tuples) for better usability
|
||||
*/
|
||||
export const executeQuery = async (cypher: string): Promise<any[]> => {
|
||||
if (!conn) {
|
||||
|
|
@ -198,11 +220,45 @@ export const executeQuery = async (cypher: string): Promise<any[]> => {
|
|||
try {
|
||||
const result = await conn.query(cypher);
|
||||
|
||||
// Extract column names from RETURN clause
|
||||
const returnMatch = cypher.match(/RETURN\s+(.+?)(?:\s+ORDER|\s+LIMIT|\s+SKIP|\s*$)/is);
|
||||
let columnNames: string[] = [];
|
||||
if (returnMatch) {
|
||||
// Parse RETURN clause to get column names/aliases
|
||||
// Handles: "a.name, b.filePath AS path, count(x) AS cnt"
|
||||
const returnClause = returnMatch[1];
|
||||
columnNames = returnClause.split(',').map(col => {
|
||||
col = col.trim();
|
||||
// Check for AS alias
|
||||
const asMatch = col.match(/\s+AS\s+(\w+)\s*$/i);
|
||||
if (asMatch) return asMatch[1];
|
||||
// Check for property access like n.name
|
||||
const propMatch = col.match(/\.(\w+)\s*$/);
|
||||
if (propMatch) return propMatch[1];
|
||||
// Check for function call like count(x)
|
||||
const funcMatch = col.match(/^(\w+)\s*\(/);
|
||||
if (funcMatch) return funcMatch[1];
|
||||
// Just use as-is if simple identifier
|
||||
return col.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||
});
|
||||
}
|
||||
|
||||
// Collect all rows
|
||||
const rows: any[] = [];
|
||||
while (await result.hasNext()) {
|
||||
const row = await result.getNext();
|
||||
rows.push(row);
|
||||
|
||||
// Convert tuple to named object if we have column names and row is array
|
||||
if (Array.isArray(row) && columnNames.length === row.length) {
|
||||
const namedRow: Record<string, any> = {};
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
namedRow[columnNames[i]] = row[i];
|
||||
}
|
||||
rows.push(namedRow);
|
||||
} else {
|
||||
// Already an object or column count doesn't match
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
|
|
|
|||
|
|
@ -123,11 +123,17 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
|
|||
FROM Folder TO File,
|
||||
FROM Function TO Function,
|
||||
FROM Function TO Method,
|
||||
FROM Function TO Class,
|
||||
FROM Class TO Method,
|
||||
FROM Class TO Function,
|
||||
FROM Class TO Class,
|
||||
FROM Class TO Interface,
|
||||
type STRING
|
||||
FROM Method TO Function,
|
||||
FROM Method TO Method,
|
||||
FROM Method TO Class,
|
||||
type STRING,
|
||||
confidence DOUBLE,
|
||||
reason STRING
|
||||
)`;
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -232,6 +232,11 @@ export const createGraphRAGAgent = (
|
|||
? buildDynamicSystemPrompt(BASE_SYSTEM_PROMPT, codebaseContext)
|
||||
: BASE_SYSTEM_PROMPT;
|
||||
|
||||
// Log the full prompt for debugging
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🤖 AGENT SYSTEM PROMPT:\n', systemPrompt);
|
||||
}
|
||||
|
||||
const agent = createReactAgent({
|
||||
llm: model as any,
|
||||
tools: tools as any,
|
||||
|
|
|
|||
|
|
@ -145,11 +145,11 @@ export async function getHotspots(
|
|||
|
||||
/**
|
||||
* Build folder tree structure from file paths
|
||||
* Returns TOON format for token efficiency (~50% reduction vs ASCII tree)
|
||||
* Returns ASCII tree format with smart truncation for readability
|
||||
*/
|
||||
export async function getFolderTree(
|
||||
executeQuery: (cypher: string) => Promise<any[]>,
|
||||
maxDepth: number = 3
|
||||
maxDepth: number = 10
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Get all file paths
|
||||
|
|
@ -163,8 +163,8 @@ export async function getFolderTree(
|
|||
|
||||
if (paths.length === 0) return '';
|
||||
|
||||
// Use TOON format for token efficiency
|
||||
return formatPathsAsTOON(paths, maxDepth);
|
||||
// Use hybrid ASCII format: clear hierarchy with smart truncation
|
||||
return formatAsHybridAscii(paths, maxDepth);
|
||||
} catch (error) {
|
||||
console.error('Failed to get folder tree:', error);
|
||||
return '';
|
||||
|
|
@ -172,75 +172,88 @@ export async function getFolderTree(
|
|||
}
|
||||
|
||||
/**
|
||||
* Format paths as TOON (Token-Oriented Object Notation)
|
||||
* Much more token-efficient than ASCII trees
|
||||
* Format paths as indented tree (TOON-style, no ASCII box chars)
|
||||
* Uses indentation only for hierarchy - more token efficient than ASCII tree
|
||||
* Shows complete structure with no truncation
|
||||
*
|
||||
* Example output:
|
||||
* folders:5
|
||||
* src,src/cli,src/core,src/utils,test
|
||||
* files:8
|
||||
* path
|
||||
* src/index.ts
|
||||
* src/cli/main.ts
|
||||
* ...
|
||||
* src/
|
||||
* components/ (45 files)
|
||||
* hooks/
|
||||
* useAppState.tsx
|
||||
* useSigma.ts
|
||||
* core/ (15 files)
|
||||
* test/ (12 files)
|
||||
*/
|
||||
function formatPathsAsTOON(paths: string[], maxDepth: number): string {
|
||||
const folders = new Set<string>();
|
||||
const files: string[] = [];
|
||||
function formatAsHybridAscii(paths: string[], maxDepth: number): string {
|
||||
// Build tree structure
|
||||
interface TreeNode {
|
||||
isFile: boolean;
|
||||
children: Map<string, TreeNode>;
|
||||
fileCount: number;
|
||||
}
|
||||
|
||||
const root: TreeNode = { isFile: false, children: new Map(), fileCount: 0 };
|
||||
|
||||
for (const path of paths) {
|
||||
// Normalize path
|
||||
const normalized = path.replace(/\\/g, '/');
|
||||
const parts = normalized.split('/').filter(Boolean);
|
||||
|
||||
// Add file (truncate to maxDepth)
|
||||
if (parts.length <= maxDepth + 1) {
|
||||
files.push(normalized);
|
||||
} else {
|
||||
// Show truncated path for deep files
|
||||
files.push(parts.slice(0, maxDepth + 1).join('/') + '/...');
|
||||
}
|
||||
|
||||
// Add folders
|
||||
for (let i = 0; i < Math.min(parts.length - 1, maxDepth); i++) {
|
||||
const folderPath = parts.slice(0, i + 1).join('/');
|
||||
folders.add(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort folders and files
|
||||
const sortedFolders = Array.from(folders).sort();
|
||||
const sortedFiles = [...new Set(files)].sort();
|
||||
|
||||
// Build TOON output
|
||||
const lines: string[] = [];
|
||||
|
||||
// Folders section
|
||||
lines.push(`folders:${sortedFolders.length}`);
|
||||
if (sortedFolders.length > 0) {
|
||||
// Compact comma-separated for small lists
|
||||
if (sortedFolders.length <= 10) {
|
||||
lines.push(` ${sortedFolders.join(',')}`);
|
||||
} else {
|
||||
// Chunked for larger lists
|
||||
for (let i = 0; i < sortedFolders.length; i += 8) {
|
||||
lines.push(` ${sortedFolders.slice(i, i + 8).join(',')}`);
|
||||
let current = root;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const isFile = i === parts.length - 1;
|
||||
|
||||
if (!current.children.has(part)) {
|
||||
current.children.set(part, { isFile, children: new Map(), fileCount: 0 });
|
||||
}
|
||||
|
||||
current = current.children.get(part)!;
|
||||
if (isFile) {
|
||||
// Count files in parent directories
|
||||
let parent = root;
|
||||
for (let j = 0; j < i; j++) {
|
||||
parent = parent.children.get(parts[j])!;
|
||||
parent.fileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Files section
|
||||
lines.push(`files:${sortedFiles.length}`);
|
||||
lines.push(' path');
|
||||
// Show first 30 files, then summarize
|
||||
const maxFiles = 30;
|
||||
for (let i = 0; i < Math.min(sortedFiles.length, maxFiles); i++) {
|
||||
lines.push(` ${sortedFiles[i]}`);
|
||||
}
|
||||
if (sortedFiles.length > maxFiles) {
|
||||
lines.push(` ...(${sortedFiles.length - maxFiles} more)`);
|
||||
// Render tree with indentation only (no ASCII box chars)
|
||||
const lines: string[] = [];
|
||||
|
||||
function renderNode(node: TreeNode, indent: string, depth: number): void {
|
||||
const entries = [...node.children.entries()];
|
||||
// Sort: folders first (by file count desc), then files alphabetically
|
||||
entries.sort(([aName, aNode], [bName, bNode]) => {
|
||||
if (aNode.isFile !== bNode.isFile) return aNode.isFile ? 1 : -1;
|
||||
if (!aNode.isFile && !bNode.isFile) return bNode.fileCount - aNode.fileCount;
|
||||
return aName.localeCompare(bName);
|
||||
});
|
||||
|
||||
for (const [name, childNode] of entries) {
|
||||
if (childNode.isFile) {
|
||||
// File
|
||||
lines.push(`${indent}${name}`);
|
||||
} else {
|
||||
// Directory
|
||||
const childCount = childNode.children.size;
|
||||
const fileCount = childNode.fileCount;
|
||||
|
||||
// Only collapse if beyond maxDepth
|
||||
if (depth >= maxDepth) {
|
||||
lines.push(`${indent}${name}/ (${fileCount} files)`);
|
||||
} else {
|
||||
lines.push(`${indent}${name}/`);
|
||||
renderNode(childNode, indent + ' ', depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderNode(root, '', 0);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ export const createGraphRAGTools = (
|
|||
OPTIONAL MATCH (n)-[r1:CodeRelation]->(dst)
|
||||
OPTIONAL MATCH (src)-[r2:CodeRelation]->(n)
|
||||
RETURN
|
||||
collect(DISTINCT {name: dst.name, type: r1.type}) AS outgoing,
|
||||
collect(DISTINCT {name: src.name, type: r2.type}) AS incoming
|
||||
collect(DISTINCT {name: dst.name, type: r1.type, confidence: r1.confidence}) AS outgoing,
|
||||
collect(DISTINCT {name: src.name, type: r2.type, confidence: r2.confidence}) AS incoming
|
||||
LIMIT 1
|
||||
`;
|
||||
const connRes = await executeQuery(connectionsQuery);
|
||||
|
|
@ -97,8 +97,16 @@ export const createGraphRAGTools = (
|
|||
const rawIncoming = Array.isArray(row) ? row[1] : (row.incoming || []);
|
||||
const outgoing = (rawOutgoing || []).filter((c: any) => c && c.name).slice(0, 3);
|
||||
const incoming = (rawIncoming || []).filter((c: any) => c && c.name).slice(0, 3);
|
||||
const outList = outgoing.map((c: any) => `-[${c.type}]-> ${c.name}`);
|
||||
const inList = incoming.map((c: any) => `<-[${c.type}]- ${c.name}`);
|
||||
|
||||
const fmt = (c: any, dir: 'out' | 'in') => {
|
||||
const conf = c.confidence ? Math.round(c.confidence * 100) : 100;
|
||||
return dir === 'out'
|
||||
? `-[${c.type} ${conf}%]-> ${c.name}`
|
||||
: `<-[${c.type} ${conf}%]- ${c.name}`;
|
||||
};
|
||||
|
||||
const outList = outgoing.map((c: any) => fmt(c, 'out'));
|
||||
const inList = incoming.map((c: any) => fmt(c, 'in'));
|
||||
if (outList.length || inList.length) {
|
||||
connections = `\n Connections: ${[...outList, ...inList].join(', ')}`;
|
||||
}
|
||||
|
|
@ -168,18 +176,39 @@ export const createGraphRAGTools = (
|
|||
return 'Query returned no results.';
|
||||
}
|
||||
|
||||
// Format results
|
||||
// Get column names from first result (now objects from executeQuery)
|
||||
const firstRow = results[0];
|
||||
const columnNames = typeof firstRow === 'object' && !Array.isArray(firstRow)
|
||||
? Object.keys(firstRow)
|
||||
: [];
|
||||
|
||||
// Format as markdown table (more token efficient than JSON per row)
|
||||
if (columnNames.length > 0) {
|
||||
const header = `| ${columnNames.join(' | ')} |`;
|
||||
const separator = `|${columnNames.map(() => '---').join('|')}|`;
|
||||
|
||||
const rows = results.slice(0, 50).map(row => {
|
||||
const values = columnNames.map(col => {
|
||||
const val = row[col];
|
||||
if (val === null || val === undefined) return '';
|
||||
if (typeof val === 'object') return JSON.stringify(val);
|
||||
// Truncate long values and escape pipe characters
|
||||
const str = String(val).replace(/\|/g, '\\|');
|
||||
return str.length > 60 ? str.slice(0, 57) + '...' : str;
|
||||
});
|
||||
return `| ${values.join(' | ')} |`;
|
||||
}).join('\n');
|
||||
|
||||
const truncated = results.length > 50 ? `\n\n_(${results.length - 50} more rows)_` : '';
|
||||
return `**${results.length} results:**\n\n${header}\n${separator}\n${rows}${truncated}`;
|
||||
}
|
||||
|
||||
// Fallback for non-object results
|
||||
const formatted = results.slice(0, 50).map((row, i) => {
|
||||
if (Array.isArray(row)) {
|
||||
return `[${i + 1}] ${row.join(', ')}`;
|
||||
}
|
||||
return `[${i + 1}] ${JSON.stringify(row)}`;
|
||||
});
|
||||
|
||||
const resultText = formatted.join('\n');
|
||||
const truncated = results.length > 50 ? `\n... (${results.length - 50} more results)` : '';
|
||||
|
||||
return `Query returned ${results.length} results:\n${resultText}${truncated}`;
|
||||
const truncated = results.length > 50 ? `\n... (${results.length - 50} more)` : '';
|
||||
return `${results.length} results:\n${formatted.join('\n')}${truncated}`;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return `Cypher error: ${message}\n\nCheck your query syntax. Node tables: File, Folder, Function, Class, Interface, Method, CodeElement. Relation: CodeRelation with type property (CONTAINS, DEFINES, IMPORTS, CALLS). Example: MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f, g`;
|
||||
|
|
@ -197,7 +226,8 @@ Example queries:
|
|||
- Class inheritance: MATCH (child:Class)-[:CodeRelation {type: 'EXTENDS'}]->(parent:Class) RETURN child.name, parent.name
|
||||
- Classes implementing interface: MATCH (c:Class)-[:CodeRelation {type: 'IMPLEMENTS'}]->(i:Interface) RETURN c.name, i.name
|
||||
- Files importing a file: MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(target:File) WHERE target.name = 'utils.ts' RETURN f.name
|
||||
- All connections: MATCH (n)-[r:CodeRelation]-(m) WHERE n.name = 'MyClass' RETURN m.name, r.type
|
||||
- All connections (with confidence): MATCH (n)-[r:CodeRelation]-(m) WHERE n.name = 'MyClass' AND r.confidence > 0.8 RETURN m.name, r.type, r.confidence
|
||||
- Find fuzzy matches: MATCH (n)-[r:CodeRelation]-(m) WHERE r.confidence < 0.8 RETURN n.name, r.reason
|
||||
|
||||
For semantic+graph queries, include {{QUERY_VECTOR}} placeholder and provide a 'query' parameter:
|
||||
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', {{QUERY_VECTOR}}, 10) YIELD node AS emb, distance
|
||||
|
|
@ -388,23 +418,43 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
// ============================================================================
|
||||
|
||||
const blastRadiusTool = tool(
|
||||
async ({ target, direction, maxDepth }: {
|
||||
async ({ target, direction, maxDepth, relationTypes, includeTests, minConfidence }: {
|
||||
target: string;
|
||||
direction: 'upstream' | 'downstream';
|
||||
maxDepth?: number;
|
||||
relationTypes?: string[];
|
||||
includeTests?: boolean;
|
||||
minConfidence?: number;
|
||||
}) => {
|
||||
const depth = Math.min(maxDepth ?? 3, 10);
|
||||
const showTests = includeTests ?? false; // Default: exclude test files
|
||||
const minConf = minConfidence ?? 0.7; // Default: exclude fuzzy matches (<70% confidence)
|
||||
|
||||
// Test file patterns
|
||||
const isTestFile = (path: string): boolean => {
|
||||
if (!path) return false;
|
||||
const p = path.toLowerCase();
|
||||
return p.includes('.test.') || p.includes('.spec.') ||
|
||||
p.includes('__tests__') || p.includes('__mocks__') ||
|
||||
p.endsWith('.test.ts') || p.endsWith('.test.tsx') ||
|
||||
p.endsWith('.spec.ts') || p.endsWith('.spec.tsx');
|
||||
};
|
||||
|
||||
// Default to usage-based relation types (exclude CONTAINS, DEFINES for impact analysis)
|
||||
const defaultRelTypes = ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
||||
const activeRelTypes = relationTypes && relationTypes.length > 0
|
||||
? relationTypes
|
||||
: defaultRelTypes;
|
||||
const relTypeFilter = activeRelTypes.map(t => `'${t}'`).join(', ');
|
||||
|
||||
// Determine the traversal direction
|
||||
const directionArrow = direction === 'upstream' ? '<-' : '->';
|
||||
const directionLabel = direction === 'upstream'
|
||||
? 'what depends on this (callers, importers, child classes)'
|
||||
: 'what this depends on (callees, imports, parent classes)';
|
||||
? 'Files that DEPEND ON this (breakage risk)'
|
||||
: 'Dependencies this RELIES ON';
|
||||
|
||||
// Try to find the target node first
|
||||
const findTargetQuery = `
|
||||
MATCH (n)
|
||||
WHERE n.name = '${target.replace(/'/g, "''")}'
|
||||
WHERE n.name = '${target.replace(/'/g, "''")}'
|
||||
RETURN n.id AS id, label(n) AS nodeType, n.filePath AS filePath
|
||||
LIMIT 5
|
||||
`;
|
||||
|
|
@ -424,34 +474,214 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
const targetNode = targetResults[0];
|
||||
const targetId = Array.isArray(targetNode) ? targetNode[0] : targetNode.id;
|
||||
const targetType = Array.isArray(targetNode) ? targetNode[1] : targetNode.nodeType;
|
||||
const targetFilePath = Array.isArray(targetNode) ? targetNode[2] : targetNode.filePath;
|
||||
|
||||
// Note: KuzuDB doesn't support [r IN relationships(path) | r.type] list comprehension
|
||||
// So we query each depth level separately for accurate depth tracking
|
||||
// For File targets, find what calls code INSIDE the file (by filePath)
|
||||
// For code elements (Function, Class, etc.), use the direct id
|
||||
const isFileTarget = targetType === 'File';
|
||||
|
||||
// Get depth info with separate simpler queries for each depth level
|
||||
// Query each depth level separately (KuzuDB doesn't support list comprehensions on paths)
|
||||
// For depth 1: direct connections only
|
||||
// For depth 2+: chain multiple single-hop queries
|
||||
const depthQueries: Promise<any[]>[] = [];
|
||||
for (let d = 1; d <= Math.min(depth, 3); d++) {
|
||||
const dQuery = direction === 'upstream'
|
||||
|
||||
// Depth 1 query - direct connections with edge metadata
|
||||
// For File targets: find callers of any code element with matching filePath
|
||||
const d1Query = direction === 'upstream'
|
||||
? isFileTarget
|
||||
? `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (affected)-[:CodeRelation*${d}]->(target)
|
||||
RETURN DISTINCT affected.id AS id, affected.name AS name, label(affected) AS nodeType, affected.filePath AS filePath, ${d} AS depth
|
||||
MATCH (affected)-[r:CodeRelation]->(callee)
|
||||
WHERE callee.filePath = '${(targetFilePath || target).replace(/'/g, "''")}'
|
||||
AND r.type IN [${relTypeFilter}]
|
||||
AND affected.filePath <> callee.filePath
|
||||
AND (r.confidence IS NULL OR r.confidence >= ${minConf})
|
||||
RETURN DISTINCT
|
||||
affected.id AS id,
|
||||
affected.name AS name,
|
||||
label(affected) AS nodeType,
|
||||
affected.filePath AS filePath,
|
||||
affected.startLine AS startLine,
|
||||
1 AS depth,
|
||||
r.type AS edgeType,
|
||||
r.confidence AS confidence,
|
||||
r.reason AS reason
|
||||
LIMIT 100
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (target)-[:CodeRelation*${d}]->(affected)
|
||||
RETURN DISTINCT affected.id AS id, affected.name AS name, label(affected) AS nodeType, affected.filePath AS filePath, ${d} AS depth
|
||||
MATCH (affected)-[r:CodeRelation]->(target)
|
||||
WHERE r.type IN [${relTypeFilter}]
|
||||
AND (r.confidence IS NULL OR r.confidence >= ${minConf})
|
||||
RETURN DISTINCT
|
||||
affected.id AS id,
|
||||
affected.name AS name,
|
||||
label(affected) AS nodeType,
|
||||
affected.filePath AS filePath,
|
||||
affected.startLine AS startLine,
|
||||
1 AS depth,
|
||||
r.type AS edgeType,
|
||||
r.confidence AS confidence,
|
||||
r.reason AS reason
|
||||
LIMIT 100
|
||||
`
|
||||
: isFileTarget
|
||||
? `
|
||||
MATCH (caller)-[r:CodeRelation]->(affected)
|
||||
WHERE caller.filePath = '${(targetFilePath || target).replace(/'/g, "''")}'
|
||||
AND r.type IN [${relTypeFilter}]
|
||||
AND caller.filePath <> affected.filePath
|
||||
AND (r.confidence IS NULL OR r.confidence >= ${minConf})
|
||||
RETURN DISTINCT
|
||||
affected.id AS id,
|
||||
affected.name AS name,
|
||||
label(affected) AS nodeType,
|
||||
affected.filePath AS filePath,
|
||||
affected.startLine AS startLine,
|
||||
1 AS depth,
|
||||
r.type AS edgeType,
|
||||
r.confidence AS confidence,
|
||||
r.reason AS reason
|
||||
LIMIT 100
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (target)-[r:CodeRelation]->(affected)
|
||||
WHERE r.type IN [${relTypeFilter}]
|
||||
AND (r.confidence IS NULL OR r.confidence >= ${minConf})
|
||||
RETURN DISTINCT
|
||||
affected.id AS id,
|
||||
affected.name AS name,
|
||||
label(affected) AS nodeType,
|
||||
affected.filePath AS filePath,
|
||||
affected.startLine AS startLine,
|
||||
1 AS depth,
|
||||
r.type AS edgeType,
|
||||
r.confidence AS confidence,
|
||||
r.reason AS reason
|
||||
LIMIT 100
|
||||
`;
|
||||
depthQueries.push(executeQuery(dQuery).catch(() => []));
|
||||
depthQueries.push(executeQuery(d1Query).catch(err => {
|
||||
if (import.meta.env.DEV) console.warn('Blast radius d=1 query failed:', err);
|
||||
return [];
|
||||
}));
|
||||
|
||||
// Depth 2 query - 2 hops
|
||||
if (depth >= 2) {
|
||||
const d2Query = direction === 'upstream'
|
||||
? `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (a)-[r1:CodeRelation]->(target)
|
||||
MATCH (affected)-[r2:CodeRelation]->(a)
|
||||
WHERE r1.type IN [${relTypeFilter}] AND r2.type IN [${relTypeFilter}]
|
||||
AND affected.id <> target.id
|
||||
AND (r1.confidence IS NULL OR r1.confidence >= ${minConf})
|
||||
AND (r2.confidence IS NULL OR r2.confidence >= ${minConf})
|
||||
RETURN DISTINCT
|
||||
affected.id AS id,
|
||||
affected.name AS name,
|
||||
label(affected) AS nodeType,
|
||||
affected.filePath AS filePath,
|
||||
affected.startLine AS startLine,
|
||||
2 AS depth,
|
||||
r2.type AS edgeType,
|
||||
r2.confidence AS confidence,
|
||||
r2.reason AS reason
|
||||
LIMIT 100
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (target)-[r1:CodeRelation]->(a)
|
||||
MATCH (a)-[r2:CodeRelation]->(affected)
|
||||
WHERE r1.type IN [${relTypeFilter}] AND r2.type IN [${relTypeFilter}]
|
||||
AND affected.id <> target.id
|
||||
AND (r1.confidence IS NULL OR r1.confidence >= ${minConf})
|
||||
AND (r2.confidence IS NULL OR r2.confidence >= ${minConf})
|
||||
RETURN DISTINCT
|
||||
affected.id AS id,
|
||||
affected.name AS name,
|
||||
label(affected) AS nodeType,
|
||||
affected.filePath AS filePath,
|
||||
affected.startLine AS startLine,
|
||||
2 AS depth,
|
||||
r2.type AS edgeType,
|
||||
r2.confidence AS confidence,
|
||||
r2.reason AS reason
|
||||
LIMIT 100
|
||||
`;
|
||||
depthQueries.push(executeQuery(d2Query).catch(err => {
|
||||
if (import.meta.env.DEV) console.warn('Blast radius d=2 query failed:', err);
|
||||
return [];
|
||||
}));
|
||||
}
|
||||
|
||||
// Depth 3 query - 3 hops
|
||||
if (depth >= 3) {
|
||||
const d3Query = direction === 'upstream'
|
||||
? `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (a)-[r1:CodeRelation]->(target)
|
||||
MATCH (b)-[r2:CodeRelation]->(a)
|
||||
MATCH (affected)-[r3:CodeRelation]->(b)
|
||||
WHERE r1.type IN [${relTypeFilter}] AND r2.type IN [${relTypeFilter}] AND r3.type IN [${relTypeFilter}]
|
||||
AND affected.id <> target.id AND affected.id <> a.id
|
||||
AND (r1.confidence IS NULL OR r1.confidence >= ${minConf})
|
||||
AND (r2.confidence IS NULL OR r2.confidence >= ${minConf})
|
||||
AND (r3.confidence IS NULL OR r3.confidence >= ${minConf})
|
||||
RETURN DISTINCT
|
||||
affected.id AS id,
|
||||
affected.name AS name,
|
||||
label(affected) AS nodeType,
|
||||
affected.filePath AS filePath,
|
||||
affected.startLine AS startLine,
|
||||
3 AS depth,
|
||||
r3.type AS edgeType,
|
||||
r3.confidence AS confidence,
|
||||
r3.reason AS reason
|
||||
LIMIT 50
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (target)-[r1:CodeRelation]->(a)
|
||||
MATCH (a)-[r2:CodeRelation]->(b)
|
||||
MATCH (b)-[r3:CodeRelation]->(affected)
|
||||
WHERE r1.type IN [${relTypeFilter}] AND r2.type IN [${relTypeFilter}] AND r3.type IN [${relTypeFilter}]
|
||||
AND affected.id <> target.id AND affected.id <> a.id
|
||||
AND (r1.confidence IS NULL OR r1.confidence >= ${minConf})
|
||||
AND (r2.confidence IS NULL OR r2.confidence >= ${minConf})
|
||||
AND (r3.confidence IS NULL OR r3.confidence >= ${minConf})
|
||||
RETURN DISTINCT
|
||||
affected.id AS id,
|
||||
affected.name AS name,
|
||||
label(affected) AS nodeType,
|
||||
affected.filePath AS filePath,
|
||||
affected.startLine AS startLine,
|
||||
3 AS depth,
|
||||
r3.type AS edgeType,
|
||||
r3.confidence AS confidence,
|
||||
r3.reason AS reason
|
||||
LIMIT 50
|
||||
`;
|
||||
depthQueries.push(executeQuery(d3Query).catch(err => {
|
||||
if (import.meta.env.DEV) console.warn('Blast radius d=3 query failed:', err);
|
||||
return [];
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all depth queries
|
||||
const depthResults = await Promise.all(depthQueries);
|
||||
|
||||
// Combine results by depth
|
||||
const byDepth: Map<number, any[]> = new Map();
|
||||
interface NodeInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
nodeType: string;
|
||||
filePath: string;
|
||||
startLine?: number;
|
||||
edgeType: string;
|
||||
confidence: number;
|
||||
reason: string;
|
||||
}
|
||||
const byDepth: Map<number, NodeInfo[]> = new Map();
|
||||
const allNodeIds: string[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
|
||||
|
|
@ -459,11 +689,27 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
const d = idx + 1;
|
||||
results.forEach((row: any) => {
|
||||
const nodeId = Array.isArray(row) ? row[0] : row.id;
|
||||
const filePath = Array.isArray(row) ? row[3] : row.filePath;
|
||||
|
||||
// Skip test files if includeTests is false
|
||||
if (!showTests && isTestFile(filePath)) return;
|
||||
|
||||
// Avoid duplicates (a node might appear at multiple depths)
|
||||
if (nodeId && !seenIds.has(nodeId)) {
|
||||
seenIds.add(nodeId);
|
||||
if (!byDepth.has(d)) byDepth.set(d, []);
|
||||
byDepth.get(d)!.push(row);
|
||||
|
||||
const info: NodeInfo = {
|
||||
id: nodeId,
|
||||
name: Array.isArray(row) ? row[1] : row.name,
|
||||
nodeType: Array.isArray(row) ? row[2] : row.nodeType,
|
||||
filePath: filePath,
|
||||
startLine: Array.isArray(row) ? row[4] : row.startLine,
|
||||
edgeType: Array.isArray(row) ? row[5] : row.edgeType || 'CALLS',
|
||||
confidence: Array.isArray(row) ? row[6] : row.confidence ?? 1.0,
|
||||
reason: Array.isArray(row) ? row[7] : row.reason || '',
|
||||
};
|
||||
byDepth.get(d)!.push(info);
|
||||
allNodeIds.push(nodeId);
|
||||
}
|
||||
});
|
||||
|
|
@ -472,83 +718,97 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
const totalAffected = allNodeIds.length;
|
||||
|
||||
if (totalAffected === 0) {
|
||||
return `No ${direction} dependencies found for "${target}". This code appears to be ${direction === 'upstream' ? 'unused (not called by anything)' : 'self-contained (no outgoing dependencies)'}.`;
|
||||
return `No ${direction} dependencies found for "${target}" (types: ${activeRelTypes.join(', ')}). This code appears to be ${direction === 'upstream' ? 'unused (not called by anything)' : 'self-contained (no outgoing dependencies)'}.`;
|
||||
}
|
||||
|
||||
// Build tiered output
|
||||
// ===== COMPACT TABULAR OUTPUT =====
|
||||
const lines: string[] = [
|
||||
`🔴 BLAST RADIUS: ${target}`,
|
||||
``,
|
||||
`Direction: ${direction} (${directionLabel})`,
|
||||
`Total affected: ${totalAffected} components`,
|
||||
`🔴 BLAST RADIUS: ${target} | ${direction} | ${totalAffected} affected`,
|
||||
``,
|
||||
];
|
||||
|
||||
// Depth 1 - Critical
|
||||
// Format helper: Type|Name|File:Line|EdgeType|Confidence
|
||||
const formatNode = (n: NodeInfo): string => {
|
||||
const fileName = n.filePath?.split('/').pop() || '';
|
||||
const loc = n.startLine ? `${fileName}:${n.startLine}` : fileName;
|
||||
const confPct = Math.round((n.confidence ?? 1) * 100);
|
||||
const fuzzyMarker = confPct < 80 ? '[fuzzy]' : '';
|
||||
return ` ${n.nodeType}|${n.name}|${loc}|${n.edgeType}|${confPct}%${fuzzyMarker}`;
|
||||
};
|
||||
|
||||
// Helper to get code snippet for a node (call site context)
|
||||
const getCallSiteSnippet = (n: NodeInfo): string | null => {
|
||||
if (!n.filePath || !n.startLine) return null;
|
||||
|
||||
// Find the file in fileContents (try multiple path formats)
|
||||
let content: string | undefined;
|
||||
const normalizedPath = n.filePath.replace(/\\/g, '/');
|
||||
|
||||
for (const [path, c] of fileContents.entries()) {
|
||||
const normalizedKey = path.replace(/\\/g, '/');
|
||||
if (normalizedKey === normalizedPath ||
|
||||
normalizedKey.endsWith(normalizedPath) ||
|
||||
normalizedPath.endsWith(normalizedKey)) {
|
||||
content = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
const lines = content.split('\n');
|
||||
const lineIdx = n.startLine - 1;
|
||||
if (lineIdx < 0 || lineIdx >= lines.length) return null;
|
||||
|
||||
// Get the line and trim it, max 80 chars
|
||||
let snippet = lines[lineIdx].trim();
|
||||
if (snippet.length > 80) snippet = snippet.slice(0, 77) + '...';
|
||||
return snippet;
|
||||
};
|
||||
|
||||
// Depth 1 - Critical (with call site snippets)
|
||||
const depth1 = byDepth.get(1) || [];
|
||||
if (depth1.length > 0) {
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`DEPTH 1 — WILL BREAK (${depth1.length} components):`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
depth1.slice(0, 20).forEach((r: any) => {
|
||||
const name = Array.isArray(r) ? r[1] : r.name;
|
||||
const nodeType = Array.isArray(r) ? r[2] : r.nodeType;
|
||||
const filePath = Array.isArray(r) ? r[3] : r.filePath;
|
||||
const fileName = filePath?.split('/').pop() || '';
|
||||
lines.push(`• ${name} (${nodeType}) at ${fileName}`);
|
||||
const header = direction === 'upstream'
|
||||
? `d=1 (Directly DEPEND ON ${target}):`
|
||||
: `d=1 (${target} USES these):`;
|
||||
lines.push(header);
|
||||
depth1.slice(0, 15).forEach(n => {
|
||||
lines.push(formatNode(n));
|
||||
// Add call site snippet for d=1 results
|
||||
const snippet = getCallSiteSnippet(n);
|
||||
if (snippet) {
|
||||
lines.push(` ↳ "${snippet}"`);
|
||||
}
|
||||
});
|
||||
if (depth1.length > 20) lines.push(` ... and ${depth1.length - 20} more`);
|
||||
if (depth1.length > 15) lines.push(` ... +${depth1.length - 15} more`);
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// Depth 2 - High impact
|
||||
const depth2 = byDepth.get(2) || [];
|
||||
if (depth2.length > 0) {
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`DEPTH 2 — LIKELY AFFECTED (${depth2.length} components):`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
depth2.slice(0, 15).forEach((r: any) => {
|
||||
const name = Array.isArray(r) ? r[1] : r.name;
|
||||
const nodeType = Array.isArray(r) ? r[2] : r.nodeType;
|
||||
const filePath = Array.isArray(r) ? r[3] : r.filePath;
|
||||
const fileName = filePath?.split('/').pop() || '';
|
||||
lines.push(`• ${name} (${nodeType}) at ${fileName}`);
|
||||
});
|
||||
if (depth2.length > 15) lines.push(` ... and ${depth2.length - 15} more`);
|
||||
const header = direction === 'upstream'
|
||||
? `d=2 (Indirectly DEPEND ON ${target}):`
|
||||
: `d=2 (${target} USES these indirectly):`;
|
||||
lines.push(header);
|
||||
depth2.slice(0, 15).forEach(n => lines.push(formatNode(n)));
|
||||
if (depth2.length > 15) lines.push(` ... +${depth2.length - 15} more`);
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// Depth 3 - Transitive
|
||||
const depth3 = byDepth.get(3) || [];
|
||||
if (depth3.length > 0) {
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`DEPTH 3 — MAY NEED TESTING (${depth3.length} components):`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
depth3.slice(0, 5).forEach((r: any) => {
|
||||
const name = Array.isArray(r) ? r[1] : r.name;
|
||||
const nodeType = Array.isArray(r) ? r[2] : r.nodeType;
|
||||
lines.push(`• ${name} (${nodeType})`);
|
||||
});
|
||||
if (depth3.length > 5) lines.push(` ... and ${depth3.length - 5} more`);
|
||||
lines.push(`d=3 (Deep impact/dependency):`);
|
||||
depth3.slice(0, 5).forEach(n => lines.push(formatNode(n)));
|
||||
if (depth3.length > 5) lines.push(` ... +${depth3.length - 5} more`);
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// Trusted analysis marker
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
// Compact footer
|
||||
lines.push(`✅ GRAPH ANALYSIS COMPLETE (trusted)`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`The above results are verified from code graph traversal.`);
|
||||
lines.push(`No additional validation needed for static dependencies.`);
|
||||
lines.push(``);
|
||||
|
||||
// Optional dynamic detection
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`⚠️ OPTIONAL: Dynamic Pattern Check`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`The graph cannot track: event listeners, DI, dynamic imports.`);
|
||||
lines.push(`If thoroughness is needed, run:`);
|
||||
lines.push(`• grep({ pattern: "${target}", fileFilter: "*.ts" })`);
|
||||
lines.push(`• grep({ pattern: "${target}", fileFilter: "*.json" })`);
|
||||
lines.push(`⚠️ Optional: grep("${target}") for dynamic patterns`);
|
||||
lines.push(``);
|
||||
|
||||
// Add the marker for UI highlighting
|
||||
|
|
@ -561,24 +821,31 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
name: 'blastRadius',
|
||||
description: `Analyze the blast radius (impact) of changing a function, class, or file.
|
||||
|
||||
Use this when users ask:
|
||||
Use when users ask:
|
||||
- "What would break if I changed X?"
|
||||
- "What depends on X?"
|
||||
- "Impact analysis for X"
|
||||
- "Blast radius of X"
|
||||
|
||||
Direction:
|
||||
- upstream: Find what CALLS/IMPORTS/EXTENDS this target (what would break)
|
||||
- downstream: Find what this target CALLS/IMPORTS/EXTENDS (dependencies)
|
||||
|
||||
Results are grouped by depth:
|
||||
- Depth 1: Direct dependencies (will definitely break)
|
||||
- Depth 2: Indirect (likely affected)
|
||||
- Depth 3+: Transitive (may need testing)`,
|
||||
Output format (compact tabular):
|
||||
Type|Name|File:Line|EdgeType|Confidence%
|
||||
|
||||
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS
|
||||
Confidence: 100% = certain, <80% = fuzzy match (may be false positive)
|
||||
|
||||
relationTypes filter (optional):
|
||||
- Default: CALLS, IMPORTS, EXTENDS, IMPLEMENTS (usage-based)
|
||||
- Can add CONTAINS, DEFINES for structural analysis`,
|
||||
schema: z.object({
|
||||
target: z.string().describe('Name of the function, class, or file to analyze'),
|
||||
direction: z.enum(['upstream', 'downstream']).describe('upstream = what depends on this; downstream = what this depends on'),
|
||||
maxDepth: z.number().optional().nullable().describe('Max traversal depth (default: 3, max: 10)'),
|
||||
relationTypes: z.array(z.string()).optional().nullable().describe('Filter by relation types: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS, DEFINES (default: usage-based)'),
|
||||
includeTests: z.boolean().optional().nullable().describe('Include test files in results (default: false, excludes .test.ts, .spec.ts, __tests__)'),
|
||||
minConfidence: z.number().optional().nullable().describe('Minimum edge confidence 0-1 (default: 0.7, excludes fuzzy/inferred matches)'),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ interface AppState {
|
|||
|
||||
// LLM methods
|
||||
refreshLLMSettings: () => void;
|
||||
initializeAgent: () => Promise<void>;
|
||||
initializeAgent: (overrideProjectName?: string) => Promise<void>;
|
||||
sendChatMessage: (message: string) => Promise<void>;
|
||||
clearChat: () => void;
|
||||
|
||||
|
|
@ -540,7 +540,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
setLLMSettings(loadSettings());
|
||||
}, []);
|
||||
|
||||
const initializeAgent = useCallback(async (): Promise<void> => {
|
||||
const initializeAgent = useCallback(async (overrideProjectName?: string): Promise<void> => {
|
||||
const api = apiRef.current;
|
||||
if (!api) {
|
||||
setAgentError('Worker not initialized');
|
||||
|
|
@ -557,7 +557,9 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
setAgentError(null);
|
||||
|
||||
try {
|
||||
const result = await api.initializeAgent(config);
|
||||
// Use override if provided (for fresh loads), fallback to state (for re-init)
|
||||
const effectiveProjectName = overrideProjectName || projectName || 'project';
|
||||
const result = await api.initializeAgent(config, effectiveProjectName);
|
||||
if (result.success) {
|
||||
setIsAgentReady(true);
|
||||
setAgentError(null);
|
||||
|
|
@ -575,7 +577,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
} finally {
|
||||
setIsAgentInitializing(false);
|
||||
}
|
||||
}, []);
|
||||
}, [projectName]);
|
||||
|
||||
const sendChatMessage = useCallback(async (message: string): Promise<void> => {
|
||||
const api = apiRef.current;
|
||||
|
|
|
|||
|
|
@ -396,17 +396,28 @@ export const useSigma = (options: UseSigmaOptions = {}): UseSigmaReturn => {
|
|||
|
||||
const currentSelected = selectedNodeRef.current;
|
||||
const highlighted = highlightedRef.current;
|
||||
const hasHighlights = highlighted.size > 0;
|
||||
const blastRadius = blastRadiusRef.current;
|
||||
const hasHighlights = highlighted.size > 0 || blastRadius.size > 0; // Check BOTH sets
|
||||
|
||||
if (hasHighlights && !currentSelected) {
|
||||
const graph = graphRef.current;
|
||||
if (graph) {
|
||||
const [source, target] = graph.extremities(edge);
|
||||
const bothHighlighted = highlighted.has(source) && highlighted.has(target);
|
||||
const oneHighlighted = highlighted.has(source) || highlighted.has(target);
|
||||
|
||||
// Check if nodes are in EITHER set
|
||||
const isSourceActive = highlighted.has(source) || blastRadius.has(source);
|
||||
const isTargetActive = highlighted.has(target) || blastRadius.has(target);
|
||||
|
||||
const bothHighlighted = isSourceActive && isTargetActive;
|
||||
const oneHighlighted = isSourceActive || isTargetActive;
|
||||
|
||||
if (bothHighlighted) {
|
||||
res.color = '#06b6d4';
|
||||
// If both nodes are in blast radius, use red edge
|
||||
if (blastRadius.has(source) && blastRadius.has(target)) {
|
||||
res.color = '#ef4444';
|
||||
} else {
|
||||
res.color = '#06b6d4';
|
||||
}
|
||||
res.size = Math.max(2, (data.size || 1) * 3);
|
||||
res.zIndex = 2;
|
||||
} else if (oneHighlighted) {
|
||||
|
|
|
|||
|
|
@ -382,8 +382,9 @@ const workerApi = {
|
|||
* Initialize the Graph RAG agent with a provider configuration
|
||||
* Must be called before using chat methods
|
||||
* @param config - Provider configuration (Azure OpenAI or Gemini)
|
||||
* @param projectName - Name of the loaded project/repository
|
||||
*/
|
||||
async initializeAgent(config: ProviderConfig): Promise<{ success: boolean; error?: string }> {
|
||||
async initializeAgent(config: ProviderConfig, projectName?: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
if (!kuzu.isKuzuReady()) {
|
||||
|
|
@ -424,14 +425,15 @@ const workerApi = {
|
|||
return mergeWithRRF(bm25Results, semanticResults, k ?? 10);
|
||||
};
|
||||
|
||||
// Build codebase context for dynamic prompt injection
|
||||
// Extract project name from first file path
|
||||
const firstPath = storedFileContents.keys().next().value || '';
|
||||
const projectName = firstPath.split('/')[0] || firstPath.split('\\')[0] || 'project';
|
||||
// Use provided projectName, or fallback to 'project' if not provided
|
||||
const resolvedProjectName = projectName || 'project';
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('📛 Project name received:', { provided: projectName, resolved: resolvedProjectName });
|
||||
}
|
||||
|
||||
let codebaseContext;
|
||||
try {
|
||||
codebaseContext = await buildCodebaseContext(kuzu.executeQuery, projectName);
|
||||
codebaseContext = await buildCodebaseContext(kuzu.executeQuery, resolvedProjectName);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('📊 Codebase context built:', {
|
||||
files: codebaseContext.stats.fileCount,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue