diff --git a/src/components/RightPanel.tsx b/src/components/RightPanel.tsx index e516fd916d..573b9ec201 100644 --- a/src/components/RightPanel.tsx +++ b/src/components/RightPanel.tsx @@ -377,15 +377,35 @@ export const RightPanel = () => { { - const isInline = !className; - return isInline ? ( - - {children} - - ) : ( - - {children} - + const match = /language-(\w+)/.exec(className || ''); + const isInline = !className && !match; + const codeContent = String(children).replace(/\n$/, ''); + + if (isInline) { + return ( + + {children} + + ); + } + + // Use SyntaxHighlighter for code blocks + const language = match ? match[1] : 'text'; + return ( + + {codeContent} + ); }, pre: ({ children }) => <>{children}, diff --git a/src/components/ToolCallCard.tsx b/src/components/ToolCallCard.tsx index ca98ec1bc2..b5b7dedc2d 100644 --- a/src/components/ToolCallCard.tsx +++ b/src/components/ToolCallCard.tsx @@ -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; @@ -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 (
{/* Header - always visible */} @@ -115,6 +152,31 @@ export const ToolCallCard = ({ toolCall, defaultExpanded = false }: ToolCallCard {getToolDisplayName(toolCall.name)} + {/* Highlight toggle button - only for highlight_in_graph tool with results */} + {isHighlightTool && highlightNodeIds.length > 0 && ( + + )} + {/* Status indicator */} {status.icon} diff --git a/src/core/llm/agent.ts b/src/core/llm/agent.ts index 73ca5ec70a..0efe56f56c 100644 --- a/src/core/llm/agent.ts +++ b/src/core/llm/agent.ts @@ -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) @@ -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 @@ -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 diff --git a/src/core/llm/tools.ts b/src/core/llm/tools.ts index c94fc76ce7..19ba0864a6 100644 --- a/src/core/llm/tools.ts +++ b/src/core/llm/tools.ts @@ -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, @@ -590,5 +618,6 @@ export const createGraphRAGTools = ( getStatsTool, grepCodeTool, readFileTool, + highlightInGraphTool, ]; }; diff --git a/src/core/llm/types.ts b/src/core/llm/types.ts index 282cab508c..d61ba36405 100644 --- a/src/core/llm/types.ts +++ b/src/core/llm/types.ts @@ -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; } @@ -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) diff --git a/src/hooks/useAppState.tsx b/src/hooks/useAppState.tsx index 88c63cb6e8..49c2e3a3fd 100644 --- a/src/hooks/useAppState.tsx +++ b/src/hooks/useAppState.tsx @@ -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;