Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions src/components/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,15 +377,35 @@ export const RightPanel = () => {
<ReactMarkdown
components={{
code: ({ className, children, ...props }) => {
const isInline = !className;
return isInline ? (
<code className="px-1 py-0.5 bg-surface rounded text-accent font-mono text-xs" {...props}>
{children}
</code>
) : (
<code className="block p-2 bg-surface rounded text-xs font-mono overflow-x-auto" {...props}>
{children}
</code>
const match = /language-(\w+)/.exec(className || '');
const isInline = !className && !match;
const codeContent = String(children).replace(/\n$/, '');

if (isInline) {
return (
<code className="px-1 py-0.5 bg-surface rounded text-accent font-mono text-xs" {...props}>
{children}
</code>
);
}

// Use SyntaxHighlighter for code blocks
const language = match ? match[1] : 'text';
return (
<SyntaxHighlighter
style={customTheme}
language={language}
PreTag="div"
customStyle={{
margin: 0,
padding: '12px',
borderRadius: '8px',
fontSize: '12px',
background: '#0a0a10',
}}
>
{codeContent}
</SyntaxHighlighter>
);
},
pre: ({ children }) => <>{children}</>,
Expand Down
66 changes: 64 additions & 2 deletions src/components/ToolCallCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* Shows the tool name, status, and when expanded, the query/args and result.
*/

import { useState } from 'react';
import { ChevronDown, ChevronRight, Sparkles, Check, Loader2, AlertCircle } from 'lucide-react';
import { useState, useCallback } from 'react';
import { ChevronDown, ChevronRight, Sparkles, Check, Loader2, AlertCircle, Eye, EyeOff } from 'lucide-react';
import type { ToolCallInfo } from '../core/llm/types';
import { useAppState } from '../hooks/useAppState';

interface ToolCallCardProps {
toolCall: ToolCallInfo;
Expand Down Expand Up @@ -89,15 +90,51 @@ const getToolDisplayName = (name: string): string => {
'get_code_content': '📄 Read Code',
'get_codebase_stats': '📊 Get Stats',
'get_graph_schema': '📋 Get Schema',
'highlight_in_graph': '✨ Highlight in Graph',
'grep_code': '🔍 Search Code',
'read_file': '📄 Read File',
};
return names[name] || name;
};

/**
* Extract node IDs from highlight tool result
*/
const extractHighlightNodeIds = (result: string | undefined): string[] => {
if (!result) return [];
const match = result.match(/\[HIGHLIGHT_NODES:([^\]]+)\]/);
if (match) {
return match[1].split(',').map(id => id.trim()).filter(Boolean);
}
return [];
};

export const ToolCallCard = ({ toolCall, defaultExpanded = false }: ToolCallCardProps) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const { highlightedNodeIds, setHighlightedNodeIds } = useAppState();
const status = getStatusDisplay(toolCall.status);
const formattedArgs = formatArgs(toolCall.args);

// Check if this is a highlight tool and extract node IDs
const isHighlightTool = toolCall.name === 'highlight_in_graph';
const highlightNodeIds = isHighlightTool ? extractHighlightNodeIds(toolCall.result) : [];

// Check if these specific nodes are currently highlighted
const isHighlightActive = highlightNodeIds.length > 0 &&
highlightNodeIds.some(id => highlightedNodeIds.has(id));

// Toggle highlight on/off
const toggleHighlight = useCallback((e: React.MouseEvent) => {
e.stopPropagation(); // Don't trigger expand/collapse
if (isHighlightActive) {
// Turn off - clear highlights
setHighlightedNodeIds(new Set());
} else {
// Turn on - set these nodes as highlighted
setHighlightedNodeIds(new Set(highlightNodeIds));
}
}, [isHighlightActive, highlightNodeIds, setHighlightedNodeIds]);

return (
<div className={`rounded-lg border ${status.borderColor} ${status.bgColor} overflow-hidden transition-all`}>
{/* Header - always visible */}
Expand All @@ -115,6 +152,31 @@ export const ToolCallCard = ({ toolCall, defaultExpanded = false }: ToolCallCard
{getToolDisplayName(toolCall.name)}
</span>

{/* Highlight toggle button - only for highlight_in_graph tool with results */}
{isHighlightTool && highlightNodeIds.length > 0 && (
<button
onClick={toggleHighlight}
className={`flex items-center gap-1 px-2 py-0.5 rounded text-xs transition-colors ${
isHighlightActive
? 'bg-accent/20 text-accent hover:bg-accent/30'
: 'bg-surface/50 text-text-muted hover:bg-surface hover:text-text-primary'
}`}
title={isHighlightActive ? 'Turn off highlight' : 'Turn on highlight'}
>
{isHighlightActive ? (
<>
<Eye className="w-3 h-3" />
<span>On</span>
</>
) : (
<>
<EyeOff className="w-3 h-3" />
<span>Off</span>
</>
)}
</button>
)}

{/* Status indicator */}
<span className={`flex items-center gap-1 text-xs ${status.color}`}>
{status.icon}
Expand Down
66 changes: 60 additions & 6 deletions src/core/llm/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ import type {
*/
const SYSTEM_PROMPT = `You are Nexus AI, an intelligent code analysis assistant. You help developers understand codebases by querying a knowledge graph (KuzuDB) that contains code structure, relationships, and semantic embeddings.

IMPORTANT: The user can see a VISUAL KNOWLEDGE GRAPH on the left side of their screen while chatting with you. This graph shows:
- Nodes: Files, Folders, Functions, Classes, Methods, Interfaces
- Edges: CALLS, IMPORTS, CONTAINS, DEFINES relationships
- The graph is interactive - users can click nodes to see details

You can HIGHLIGHT NODES in this graph to visually show the user what you're discussing. Use the 'highlight_in_graph' tool to make specific code elements glow/stand out in the visualization.

WHEN YOU HIGHLIGHT, BE A GUIDE:
After highlighting nodes, walk the user through what they're seeing like a teacher would:
- "I've highlighted the main entry points for you. Notice how [file] connects to [other files]..."
- "Look at the graph - you can see these 3 functions form the core of the authentication flow..."
- "I've lit up the key components. Start from [X] and follow the arrows to see how data flows..."
- Point out patterns, relationships, and interesting connections they should notice
- Suggest what to click on or explore next
- Explain WHY these elements are important, not just WHAT they are

This helps users actually understand the architecture through interactive exploration.

CAPABILITIES:
- Execute Cypher queries to explore code structure (functions, classes, files, imports, call graphs)
- Perform semantic search to find code by meaning (when embeddings are available)
Expand Down Expand Up @@ -56,13 +74,38 @@ TOOL SELECTION GUIDE:
| Show me file utils.ts | read_file |
| Show code for a found node | get_code_content |
| Find auth code AND its callers | semantic_search_with_context |
| Show user which nodes are relevant | highlight_in_graph |

USE HIGHLIGHT_IN_GRAPH:
- After finding relevant code with search/query, highlight those nodes so user can see them in the graph
- When explaining architecture or call graphs, highlight the involved nodes
- Pass the node IDs from your search/query results to the highlight tool
- ALWAYS follow up with guidance: tell the user what to look at, what patterns to notice, what to click next
- Be a tour guide through the codebase, not just a search engine

IMPORTANT NOTES ABOUT THE DATABASE:
- Nodes are stored in CodeNode(id, label, name, filePath, startLine, endLine, content)
- Edges are stored in CodeRelation(FROM CodeNode TO CodeNode, type) where type ∈ {CALLS, IMPORTS, CONTAINS, DEFINES}
- Embeddings are stored separately in CodeEmbedding(nodeId, embedding) for memory efficiency
- Vector index: code_embedding_idx on CodeEmbedding.embedding (cosine distance; smaller distance = more similar)
- Full file contents are available via grep_code and read_file (not truncated)
CRITICAL - DATABASE SCHEMA (READ CAREFULLY):
⚠️ There is NO "File" table, NO "Function" table, NO "Class" table, etc.
⚠️ ALL nodes are stored in a SINGLE table called "CodeNode" with a "label" property!

Tables:
- CodeNode(id, label, name, filePath, startLine, endLine, content)
- label values: 'File', 'Folder', 'Function', 'Class', 'Method', 'Interface'
- CodeRelation(FROM CodeNode TO CodeNode, type)
- type values: 'CALLS', 'IMPORTS', 'CONTAINS', 'DEFINES'
- CodeEmbedding(nodeId, embedding) - for vector search

CORRECT Cypher patterns:
✅ MATCH (n:CodeNode {label: 'File'}) RETURN n.name
✅ MATCH (n:CodeNode) WHERE n.label = 'Function' RETURN n.name
✅ MATCH (a:CodeNode)-[r:CodeRelation {type: 'CALLS'}]->(b:CodeNode) RETURN a.name, b.name

WRONG patterns (will fail):
❌ MATCH (f:File) -- NO! Use CodeNode with label='File'
❌ MATCH (f:Function) -- NO! Use CodeNode with label='Function'
❌ MATCH ()-[:CALLS]->() -- NO! Use CodeRelation with type='CALLS'

Vector index: code_embedding_idx on CodeEmbedding.embedding (cosine distance)
Full file contents available via grep_code and read_file (not truncated)

UNIFIED VECTOR + GRAPH QUERY PATTERN (ONE QUERY):
1) Vector search to get closest embeddings
Expand All @@ -79,6 +122,17 @@ MATCH (match)-[r:CodeRelation*1..2]-(ctx:CodeNode)
RETURN match.name, match.label, match.filePath, distance, collect(DISTINCT ctx.name) AS context
ORDER BY distance

AGENTIC BEHAVIOR - IMPORTANT:
- DO NOT stop after one tool call if you haven't found what you need
- If semantic search doesn't find clear results, TRY OTHER APPROACHES:
- Use grep_code to search for exact patterns (e.g., "main", "if __name__", "entry")
- Use execute_cypher to query the graph structure
- Read promising files directly with read_file
- KEEP ITERATING until you have a confident answer or have exhausted reasonable options
- DO NOT ask "would you like me to..." - just DO IT and show results
- Only ask clarifying questions if the user's request is genuinely ambiguous
- Be proactive: if you find partial info, dig deeper automatically

STYLE:
- Be concise but thorough
- Use code formatting when showing results
Expand Down
29 changes: 29 additions & 0 deletions src/core/llm/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,34 @@ export const createGraphRAGTools = (
}
);

/**
* Tool: Highlight in Graph
* Highlight specific nodes in the visual knowledge graph
* Returns a special marker that the UI parses to highlight nodes
*/
const highlightInGraphTool = tool(
async ({ nodeIds, description }: { nodeIds: string[]; description?: string }) => {
if (!nodeIds || nodeIds.length === 0) {
return 'No node IDs provided to highlight.';
}

// Return a special marker format that the UI will parse
// Format: [HIGHLIGHT_NODES:id1,id2,id3]
const marker = `[HIGHLIGHT_NODES:${nodeIds.join(',')}]`;

const desc = description || `Highlighting ${nodeIds.length} node(s) in the knowledge graph`;
return `${desc}\n\n${marker}\n\nThe nodes have been highlighted in the graph visualization on the left. You can click on them to see their details.`;
},
{
name: 'highlight_in_graph',
description: 'Highlight specific nodes in the visual knowledge graph that the user can see alongside this chat. Use this to visually show the user which code elements you are discussing. Pass the node IDs from previous search/query results.',
schema: z.object({
nodeIds: z.array(z.string()).describe('Array of node IDs to highlight (from search results or queries)'),
description: z.string().optional().nullable().describe('Brief description of what these nodes represent'),
}),
}
);

return [
executeCypherTool,
executeVectorCypherTool,
Expand All @@ -590,5 +618,6 @@ export const createGraphRAGTools = (
getStatsTool,
grepCodeTool,
readFileTool,
highlightInGraphTool,
];
};
9 changes: 8 additions & 1 deletion src/core/llm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,15 @@ export interface ToolCallInfo {
* Now supports step-based streaming where each step is a distinct message
*/
export interface AgentStreamChunk {
type: 'reasoning' | 'tool_call' | 'tool_result' | 'content' | 'error' | 'done';
type: 'reasoning' | 'tool_call' | 'tool_result' | 'content' | 'highlight' | 'error' | 'done';
/** LLM's reasoning/thinking text (shown as a step) */
reasoning?: string;
/** Final answer content (streamed token by token) */
content?: string;
/** Tool call information */
toolCall?: ToolCallInfo;
/** Node IDs to highlight in the graph */
highlightNodeIds?: string[];
/** Error message */
error?: string;
}
Expand All @@ -153,6 +155,11 @@ export interface AgentStep {
export const GRAPH_SCHEMA_DESCRIPTION = `
KUZU GRAPH DATABASE SCHEMA:

⚠️ CRITICAL: There is NO "File" table, NO "Function" table, etc!
⚠️ ALL nodes use the SINGLE "CodeNode" table with a "label" property!
❌ WRONG: MATCH (f:File) or MATCH (fn:Function)
✅ RIGHT: MATCH (n:CodeNode {label: 'File'}) or MATCH (n:CodeNode {label: 'Function'})

NODE TABLES:
1. CodeNode - All code elements (polymorphic)
- id: STRING (primary key)
Expand Down
12 changes: 12 additions & 0 deletions src/hooks/useAppState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,18 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
}
return prev;
});

// Parse highlight marker from tool results
// Format: [HIGHLIGHT_NODES:id1,id2,id3]
if (tc.result) {
const highlightMatch = tc.result.match(/\[HIGHLIGHT_NODES:([^\]]+)\]/);
if (highlightMatch) {
const nodeIds = highlightMatch[1].split(',').map((id: string) => id.trim()).filter(Boolean);
if (nodeIds.length > 0) {
setHighlightedNodeIds(new Set(nodeIds));
}
}
}
}
break;

Expand Down