grounding and prompt enhancement

This commit is contained in:
abhigyanpatwari 2026-01-12 20:12:31 +05:30
parent 7a820068ba
commit 034f6ab37f
16 changed files with 816 additions and 80 deletions

294
package-lock.json generated
View file

@ -37,6 +37,7 @@
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"remark-gfm": "^4.0.1",
"sigma": "^3.0.2",
"tailwindcss": "^4.1.18",
"uuid": "^13.0.0",
@ -7088,6 +7089,16 @@
"dev": true,
"license": "ISC"
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/marked": {
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
@ -7121,6 +7132,34 @@
"node": ">= 0.4"
}
},
"node_modules/mdast-util-find-and-replace": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
"integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"escape-string-regexp": "^5.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mdast-util-from-markdown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@ -7145,6 +7184,107 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
"license": "MIT",
"dependencies": {
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-gfm-autolink-literal": "^2.0.0",
"mdast-util-gfm-footnote": "^2.0.0",
"mdast-util-gfm-strikethrough": "^2.0.0",
"mdast-util-gfm-table": "^2.0.0",
"mdast-util-gfm-task-list-item": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-autolink-literal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"ccount": "^2.0.0",
"devlop": "^1.0.0",
"mdast-util-find-and-replace": "^3.0.0",
"micromark-util-character": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-footnote": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.1.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0",
"micromark-util-normalize-identifier": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-strikethrough": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-table": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"markdown-table": "^3.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-task-list-item": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-mdx-expression": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
@ -7394,6 +7534,127 @@
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-extension-gfm": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
"license": "MIT",
"dependencies": {
"micromark-extension-gfm-autolink-literal": "^2.0.0",
"micromark-extension-gfm-footnote": "^2.0.0",
"micromark-extension-gfm-strikethrough": "^2.0.0",
"micromark-extension-gfm-table": "^2.0.0",
"micromark-extension-gfm-tagfilter": "^2.0.0",
"micromark-extension-gfm-task-list-item": "^2.0.0",
"micromark-util-combine-extensions": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-autolink-literal": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-footnote": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-core-commonmark": "^2.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-normalize-identifier": "^2.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-strikethrough": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-util-chunked": "^2.0.0",
"micromark-util-classify-character": "^2.0.0",
"micromark-util-resolve-all": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-table": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-tagfilter": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
"license": "MIT",
"dependencies": {
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-task-list-item": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-factory-destination": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@ -8597,6 +8858,24 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-gfm": "^3.0.0",
"micromark-extension-gfm": "^3.0.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@ -8630,6 +8909,21 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-stringify": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-to-markdown": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",

View file

@ -38,6 +38,7 @@
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"remark-gfm": "^4.0.1",
"sigma": "^3.0.2",
"tailwindcss": "^4.1.18",
"uuid": "^13.0.0",

View file

@ -27,14 +27,14 @@ const EXAMPLE_QUERIES = [
export const QueryFAB = () => {
const { setHighlightedNodeIds, setQueryResult, queryResult, clearQueryHighlights, graph, runQuery, isDatabaseReady } = useAppState();
const [isExpanded, setIsExpanded] = useState(false);
const [query, setQuery] = useState('');
const [isRunning, setIsRunning] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showExamples, setShowExamples] = useState(false);
const [showResults, setShowResults] = useState(true);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@ -67,7 +67,7 @@ export const QueryFAB = () => {
const handleRunQuery = useCallback(async () => {
if (!query.trim() || isRunning) return;
if (!graph) {
setError('No project loaded. Load a project first.');
return;
@ -81,25 +81,52 @@ export const QueryFAB = () => {
setIsRunning(true);
setError(null);
const startTime = performance.now();
try {
const rows = await runQuery(query);
const executionTime = performance.now() - startTime;
// Extract node IDs - handle both array and object formats
// First column should be the ID when using example queries
// Extract node IDs from results - handles various formats
// 1. Array format: first element if it looks like a node ID
// 2. Object format: any field ending with 'id' (case-insensitive)
// 3. Values matching node ID pattern: Label:path:name
const nodeIdPattern = /^(File|Function|Class|Method|Interface|Folder|CodeElement):/;
const nodeIds = rows
.flatMap(row => {
const ids: string[] = [];
if (Array.isArray(row)) {
return [row[0]];
// Array format - check all elements for node ID patterns
row.forEach(val => {
if (typeof val === 'string' && (nodeIdPattern.test(val) || val.includes(':'))) {
ids.push(val);
}
});
} else if (typeof row === 'object' && row !== null) {
// Object format - check fields ending with 'id' and values matching patterns
Object.entries(row).forEach(([key, val]) => {
const keyLower = key.toLowerCase();
if (typeof val === 'string') {
// Field name contains 'id'
if (keyLower.includes('id') || keyLower === 'id') {
ids.push(val);
}
// Value matches node ID pattern
else if (nodeIdPattern.test(val)) {
ids.push(val);
}
}
});
}
return [row.id, row.ID, row['n.id'], row['a.id'], row['b.id']].filter(Boolean);
return ids;
})
.filter(Boolean)
.filter((id, index, arr) => arr.indexOf(id) === index);
setQueryResult({ rows, nodeIds, executionTime });
setHighlightedNodeIds(new Set(nodeIds));
} catch (err) {
@ -338,7 +365,7 @@ export const QueryFAB = () => {
</button>
</div>
</div>
{showResults && queryResult.rows.length > 0 && (
<div className="max-h-48 overflow-auto scrollbar-thin border-t border-border-subtle">
<table className="w-full text-xs">

View file

@ -6,6 +6,7 @@ import {
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useAppState } from '../hooks/useAppState';
import { ToolCallCard } from './ToolCallCard';
import { isProviderConfigured } from '../core/llm/settings-service';
@ -97,7 +98,8 @@ export const RightPanel = () => {
let startLine1: number | undefined;
let endLine1: number | undefined;
const lineMatch = raw.match(/^(.*):(\d+)(?:-(\d+))?$/);
// Match line:num or line:num-num (supports both hyphen - and en dash –)
const lineMatch = raw.match(/^(.*):(\d+)(?:[-–](\d+))?$/);
if (lineMatch) {
rawPath = lineMatch[1].trim();
startLine1 = parseInt(lineMatch[2], 10);
@ -120,15 +122,75 @@ export const RightPanel = () => {
});
}, [addCodeReference, findFileNodeIdForUI, resolveFilePathForUI]);
// Handler for node grounding: [[Class:View]], [[Function:trigger]], etc.
const handleNodeGroundingClick = useCallback((nodeTypeAndName: string) => {
const raw = nodeTypeAndName.trim();
if (!raw || !graph) return;
// Parse Type:Name format
const match = raw.match(/^(Class|Function|Method|Interface|File|Folder|Variable|Enum|Type|CodeElement):(.+)$/);
if (!match) return;
const [, nodeType, nodeName] = match;
const trimmedName = nodeName.trim();
// Find node in graph by type + name
const node = graph.nodes.find(n =>
n.label === nodeType &&
n.properties.name === trimmedName
);
if (!node) {
console.warn(`Node not found: ${nodeType}:${trimmedName}`);
return;
}
// 1. Highlight in graph (add to AI citation highlights)
// Note: This requires accessing the state setter from parent context
// For now, we'll add to code references which triggers the highlight
// 2. Add to Code Panel (if node has file/line info)
if (node.properties.filePath) {
const resolvedPath = resolveFilePathForUI(node.properties.filePath);
if (resolvedPath) {
addCodeReference({
filePath: resolvedPath,
startLine: node.properties.startLine ? node.properties.startLine - 1 : undefined,
endLine: node.properties.endLine ? node.properties.endLine - 1 : undefined,
nodeId: node.id,
label: node.label,
name: node.properties.name,
source: 'ai',
});
}
}
}, [graph, resolveFilePathForUI, addCodeReference]);
const formatMarkdownForDisplay = useCallback((md: string) => {
// Avoid rewriting inside fenced code blocks.
const parts = md.split('```');
for (let i = 0; i < parts.length; i += 2) {
parts[i] = parts[i].replace(/\[\[([^\]\n]+?)\]\]/g, (_m, inner: string) => {
const trimmed = inner.trim();
const href = `code-ref:${encodeURIComponent(trimmed)}`;
return `[${trimmed}](${href})`;
});
// Pattern 1: File grounding - [[file.ext]] or [[file.ext:line]] or [[file.ext:line-line]]
// Line numbers are optional
parts[i] = parts[i].replace(
/\[\[([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+(?::\d+(?:[-–]\d+)?)?)\]\]/g,
(_m, inner: string) => {
const trimmed = inner.trim();
const href = `code-ref:${encodeURIComponent(trimmed)}`;
return `[${trimmed}](${href})`;
}
);
// Pattern 2: Node grounding - [[Type:Name]] or [[graph:Type:Name]]
// Valid types: Class, Function, Method, Interface, File, Folder, Variable, Enum, Type, CodeElement
parts[i] = parts[i].replace(
/\[\[(?:graph:)?(Class|Function|Method|Interface|File|Folder|Variable|Enum|Type|CodeElement):([^\]]+)\]\]/g,
(_m, nodeType: string, nodeName: string) => {
const trimmed = `${nodeType}:${nodeName.trim()}`;
const href = `node-ref:${encodeURIComponent(trimmed)}`;
return `[${trimmed}](${href})`;
}
);
}
return parts.join('```');
}, []);
@ -143,7 +205,8 @@ export const RightPanel = () => {
// Strip query/hash
const cleaned = withoutScheme.split('#')[0].split('?')[0];
const m = cleaned.match(/^(.*):(\d+)(?:-(\d+))?$/);
// Support both hyphen - and en dash – for line ranges
const m = cleaned.match(/^(.*):(\d+)(?:[-–](\d+))?$/);
const path = (m ? m[1] : cleaned).replace(/\\/g, '/');
const base = path.split('/').pop() ?? path;
@ -164,8 +227,8 @@ export const RightPanel = () => {
// Strip query/hash
const cleaned = h.split('#')[0].split('?')[0];
// Looks like: path/to/file.ext or path\to\file.ext:12-34
return /[A-Za-z0-9_\-./\\]+\.[A-Za-z0-9]+(?::\d+(?:-\d+)?)?$/.test(cleaned);
// Looks like: path/to/file.ext or path\to\file.ext:12-34 (supports both hyphen and en dash)
return /[A-Za-z0-9_\-./\\]+\.[A-Za-z0-9]+(?::\d+(?:[-–]\d+)?)?$/.test(cleaned);
}, []);
const extractTextFromChildren = useCallback((children: any): string => {
@ -353,6 +416,7 @@ export const RightPanel = () => {
{step.type === 'reasoning' && step.content && (
<div className="text-text-secondary text-sm italic border-l-2 border-text-muted/30 pl-3 mb-3">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ href, children, ...props }) => {
if (href && href.startsWith('code-ref:')) {
@ -365,7 +429,7 @@ export const RightPanel = () => {
e.preventDefault();
handleGroundingClick(inner);
}}
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
title={`Open in Code panel • ${inner}`}
{...props}
>
@ -373,6 +437,24 @@ export const RightPanel = () => {
</a>
);
}
// Handle node grounding: node-ref:Type:Name
if (href && href.startsWith('node-ref:')) {
const inner = decodeURIComponent(href.slice('node-ref:'.length));
return (
<a
href={href}
onClick={(e) => {
e.preventDefault();
handleNodeGroundingClick(inner);
}}
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-amber-300/55 bg-amber-400/10 !text-amber-200 visited:!text-amber-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-amber-400/15 hover:border-amber-200/70 transition-colors"
title={`View ${inner} in Code panel`}
{...props}
>
<span className="text-inherit">{children}</span>
</a>
);
}
const internalRef = getInternalRefFromLink(href, children);
if (internalRef) {
const label = formatRefChipLabel(internalRef);
@ -383,7 +465,7 @@ export const RightPanel = () => {
e.preventDefault();
handleGroundingClick(internalRef);
}}
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
title={`Open in Code panel • ${internalRef}`}
{...props}
>
@ -437,6 +519,7 @@ export const RightPanel = () => {
{step.type === 'content' && step.content && (
<div className="text-text-primary text-sm">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ href, children, ...props }) => {
if (href && href.startsWith('code-ref:')) {
@ -449,7 +532,7 @@ export const RightPanel = () => {
e.preventDefault();
handleGroundingClick(inner);
}}
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
title={`Open in Code panel • ${inner}`}
{...props}
>
@ -457,6 +540,24 @@ export const RightPanel = () => {
</a>
);
}
// Handle node grounding: node-ref:Type:Name
if (href && href.startsWith('node-ref:')) {
const inner = decodeURIComponent(href.slice('node-ref:'.length));
return (
<a
href={href}
onClick={(e) => {
e.preventDefault();
handleNodeGroundingClick(inner);
}}
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-amber-300/55 bg-amber-400/10 !text-amber-200 visited:!text-amber-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-amber-400/15 hover:border-amber-200/70 transition-colors"
title={`View ${inner} in Code panel`}
{...props}
>
<span className="text-inherit">{children}</span>
</a>
);
}
const internalRef = getInternalRefFromLink(href, children);
if (internalRef) {
const label = formatRefChipLabel(internalRef);
@ -467,7 +568,7 @@ export const RightPanel = () => {
e.preventDefault();
handleGroundingClick(internalRef);
}}
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
title={`Open in Code panel • ${internalRef}`}
{...props}
>
@ -529,6 +630,7 @@ export const RightPanel = () => {
// Fallback: render content + toolCalls separately (old format)
<div className="text-text-primary text-sm">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ href, children, ...props }) => {
if (href && href.startsWith('code-ref:')) {
@ -541,7 +643,7 @@ export const RightPanel = () => {
e.preventDefault();
handleGroundingClick(inner);
}}
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
title={`Open in Code panel • ${inner}`}
{...props}
>
@ -549,6 +651,24 @@ export const RightPanel = () => {
</a>
);
}
// Handle node grounding: node-ref:Type:Name
if (href && href.startsWith('node-ref:')) {
const inner = decodeURIComponent(href.slice('node-ref:'.length));
return (
<a
href={href}
onClick={(e) => {
e.preventDefault();
handleNodeGroundingClick(inner);
}}
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-amber-300/55 bg-amber-400/10 !text-amber-200 visited:!text-amber-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-amber-400/15 hover:border-amber-200/70 transition-colors"
title={`View ${inner} in Code panel`}
{...props}
>
<span className="text-inherit">{children}</span>
</a>
);
}
const internalRef = getInternalRefFromLink(href, children);
if (internalRef) {
const label = formatRefChipLabel(internalRef);
@ -559,7 +679,7 @@ export const RightPanel = () => {
e.preventDefault();
handleGroundingClick(internalRef);
}}
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
title={`Open in Code panel • ${internalRef}`}
{...props}
>

View file

@ -19,7 +19,7 @@ const DEFAULT_IGNORE_LIST = new Set([
'bower_components',
'jspm_packages',
'vendor', // PHP/Go
'packages', // Sometimes used for deps
// 'packages' removed - commonly used for monorepo source code (lerna, pnpm, yarn workspaces)
'venv',
'.venv',
'env',

View file

@ -7,6 +7,81 @@ import { LANGUAGE_QUERIES } from './tree-sitter-queries';
import { generateId } from '../../lib/utils';
import { getLanguageFromFilename } from './utils';
/**
* Node types that represent function/method definitions across languages.
* Used to find the enclosing function for a call site.
*/
const FUNCTION_NODE_TYPES = new Set([
// TypeScript/JavaScript
'function_declaration',
'arrow_function',
'function_expression',
'method_definition',
'generator_function_declaration',
// Python
'function_definition',
// Common async variants
'async_function_declaration',
'async_arrow_function',
]);
/**
* Walk up the AST from a node to find the enclosing function/method.
* Returns null if the call is at module/file level (top-level code).
*/
const findEnclosingFunction = (
node: any,
filePath: string,
symbolTable: SymbolTable
): string | null => {
let current = node.parent;
while (current) {
if (FUNCTION_NODE_TYPES.has(current.type)) {
// Found enclosing function - try to get its name
let funcName: string | null = null;
// Different node types have different name locations
if (current.type === 'function_declaration' ||
current.type === 'function_definition' ||
current.type === 'async_function_declaration' ||
current.type === 'generator_function_declaration') {
// Named function: function foo() {}
const nameNode = current.childForFieldName?.('name') ||
current.children?.find((c: any) => c.type === 'identifier' || c.type === 'property_identifier');
funcName = nameNode?.text;
} else if (current.type === 'method_definition') {
// Method: foo() {} inside class
const nameNode = current.childForFieldName?.('name') ||
current.children?.find((c: any) => c.type === 'property_identifier');
funcName = nameNode?.text;
} else if (current.type === 'arrow_function' || current.type === 'function_expression') {
// Arrow/expression: const foo = () => {} - check parent variable declarator
const parent = current.parent;
if (parent?.type === 'variable_declarator') {
const nameNode = parent.childForFieldName?.('name') ||
parent.children?.find((c: any) => c.type === 'identifier');
funcName = nameNode?.text;
}
}
if (funcName) {
// Look up the function in symbol table to get its node ID
const nodeId = symbolTable.lookupExact(filePath, funcName);
if (nodeId) return nodeId;
// Fallback: generate ID based on name and file
return generateId('Function', `${filePath}:${funcName}`);
}
// Couldn't determine function name - try parent (might be nested)
}
current = current.parent;
}
return null; // Top-level call (not inside any function)
};
export const processCalls = async (
graph: KnowledgeGraph,
files: { path: string; content: string }[],
@ -78,9 +153,14 @@ export const processCalls = async (
if (!targetNodeId) return;
// 5. Create CALLS relationship (File -> Function/Method)
const sourceId = generateId('File', file.path);
const relId = generateId('CALLS', `${file.path}:${calledName}->${targetNodeId}`);
// 5. Find the enclosing function (caller)
const callNode = captureMap['call'];
const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
// 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}`);
graph.addRelationship({
id: relId,

View file

@ -0,0 +1,123 @@
/**
* Heritage Processor
*
* Extracts class inheritance relationships:
* - EXTENDS: Class extends another Class (TS, JS, Python)
* - IMPLEMENTS: Class implements an Interface (TS only)
*/
import { KnowledgeGraph } from '../graph/types';
import { ASTCache } from './ast-cache';
import { SymbolTable } from './symbol-table';
import { loadParser, loadLanguage } from '../tree-sitter/parser-loader';
import { LANGUAGE_QUERIES } from './tree-sitter-queries';
import { generateId } from '../../lib/utils';
import { getLanguageFromFilename } from './utils';
export const processHeritage = async (
graph: KnowledgeGraph,
files: { path: string; content: string }[],
astCache: ASTCache,
symbolTable: SymbolTable,
onProgress?: (current: number, total: number) => void
) => {
const parser = await loadParser();
for (let i = 0; i < files.length; i++) {
const file = files[i];
onProgress?.(i + 1, files.length);
// 1. Check language support
const language = getLanguageFromFilename(file.path);
if (!language) continue;
const queryStr = LANGUAGE_QUERIES[language];
if (!queryStr) continue;
// 2. Load the language
await loadLanguage(language, file.path);
// 3. Get AST
let tree = astCache.get(file.path);
let wasReparsed = false;
if (!tree) {
tree = parser.parse(file.content);
wasReparsed = true;
}
let query;
let matches;
try {
query = parser.getLanguage().query(queryStr);
matches = query.matches(tree.rootNode);
} catch (queryError) {
console.warn(`Heritage query error for ${file.path}:`, queryError);
if (wasReparsed) tree.delete();
continue;
}
// 4. Process heritage matches
matches.forEach(match => {
const captureMap: Record<string, any> = {};
match.captures.forEach(c => {
captureMap[c.name] = c.node;
});
// EXTENDS: Class extends another Class
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
const className = captureMap['heritage.class'].text;
const parentClassName = captureMap['heritage.extends'].text;
// Resolve both class IDs
const childId = symbolTable.lookupExact(file.path, className) ||
symbolTable.lookupFuzzy(className)[0]?.nodeId ||
generateId('Class', `${file.path}:${className}`);
const parentId = symbolTable.lookupFuzzy(parentClassName)[0]?.nodeId ||
generateId('Class', `${parentClassName}`);
if (childId && parentId && childId !== parentId) {
const relId = generateId('EXTENDS', `${childId}->${parentId}`);
graph.addRelationship({
id: relId,
sourceId: childId,
targetId: parentId,
type: 'EXTENDS'
});
}
}
// IMPLEMENTS: Class implements Interface (TypeScript only)
if (captureMap['heritage.class'] && captureMap['heritage.implements']) {
const className = captureMap['heritage.class'].text;
const interfaceName = captureMap['heritage.implements'].text;
// Resolve class and interface IDs
const classId = symbolTable.lookupExact(file.path, className) ||
symbolTable.lookupFuzzy(className)[0]?.nodeId ||
generateId('Class', `${file.path}:${className}`);
const interfaceId = symbolTable.lookupFuzzy(interfaceName)[0]?.nodeId ||
generateId('Interface', `${interfaceName}`);
if (classId && interfaceId) {
const relId = generateId('IMPLEMENTS', `${classId}->${interfaceId}`);
graph.addRelationship({
id: relId,
sourceId: classId,
targetId: interfaceId,
type: 'IMPLEMENTS'
});
}
}
});
// Cleanup
if (wasReparsed) {
tree.delete();
}
}
};

View file

@ -86,8 +86,17 @@ export const processImports = async (
try {
query = parser.getLanguage().query(queryStr);
matches = query.matches(tree.rootNode);
} catch (queryError) {
console.warn(`Query error for ${file.path}:`, queryError);
} catch (queryError: any) {
// Detailed debug logging for query failures
console.group(`🔴 Query Error: ${file.path}`);
console.log('Language:', language);
console.log('Query (first 200 chars):', queryStr.substring(0, 200) + '...');
console.log('Error:', queryError?.message || queryError);
console.log('File content (first 300 chars):', file.content.substring(0, 300));
console.log('AST root type:', tree.rootNode?.type);
console.log('AST has errors:', tree.rootNode?.hasError);
console.groupEnd();
if (wasReparsed) tree.delete();
continue;
}

View file

@ -4,6 +4,7 @@ import { processStructure } from './structure-processor';
import { processParsing } from './parsing-processor';
import { processImports, createImportMap } from './import-processor';
import { processCalls } from './call-processor';
import { processHeritage } from './heritage-processor';
import { createSymbolTable } from './symbol-table';
import { createASTCache } from './ast-cache';
import { PipelineProgress, PipelineResult } from '../../types/pipeline';
@ -131,7 +132,7 @@ export const runPipelineFromFiles = async (
});
await processCalls(graph, files, astCache, symbolTable, importMap, (current, total) => {
const callProgress = 82 + ((current / total) * 16);
const callProgress = 82 + ((current / total) * 10);
onProgress({
phase: 'calls',
percent: Math.round(callProgress),
@ -140,6 +141,24 @@ export const runPipelineFromFiles = async (
});
});
// Phase 6: Heritage - Class inheritance (92-98%)
onProgress({
phase: 'heritage',
percent: 92,
message: 'Extracting class inheritance...',
stats: { filesProcessed: 0, totalFiles: files.length, nodesCreated: graph.nodeCount },
});
await processHeritage(graph, files, astCache, symbolTable, (current, total) => {
const heritageProgress = 92 + ((current / total) * 6);
onProgress({
phase: 'heritage',
percent: Math.round(heritageProgress),
message: 'Extracting class inheritance...',
stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount },
});
});
// Phase 6: Complete (100%)
onProgress({

View file

@ -53,6 +53,20 @@ export const TYPESCRIPT_QUERIES = `
(call_expression
function: (member_expression
property: (property_identifier) @call.name)) @call
; Heritage queries - class extends
(class_declaration
name: (type_identifier) @heritage.class
(class_heritage
(extends_clause
value: (identifier) @heritage.extends))) @heritage
; Heritage queries - class implements interface
(class_declaration
name: (type_identifier) @heritage.class
(class_heritage
(implements_clause
(type_identifier) @heritage.implements))) @heritage.impl
`;
// JavaScript queries - works with tree-sitter-javascript
@ -97,6 +111,13 @@ export const JAVASCRIPT_QUERIES = `
(call_expression
function: (member_expression
property: (property_identifier) @call.name)) @call
; Heritage queries - class extends (JavaScript uses different AST than TypeScript)
; In tree-sitter-javascript, class_heritage directly contains the parent identifier
(class_declaration
name: (identifier) @heritage.class
(class_heritage
(identifier) @heritage.extends)) @heritage
`;
// Python queries - works with tree-sitter-python
@ -119,6 +140,12 @@ export const PYTHON_QUERIES = `
(call
function: (attribute
attribute: (identifier) @call.name)) @call
; Heritage queries - Python class inheritance
(class_definition
name: (identifier) @heritage.class
superclasses: (argument_list
(identifier) @heritage.extends)) @heritage
`;
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {

View file

@ -21,7 +21,7 @@ export type NodeTableName = typeof NODE_TABLES[number];
export const REL_TABLE_NAME = 'CodeRelation';
// Valid relation types
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS'] as const;
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS'] as const;
export type RelType = typeof REL_TYPES[number];
// ============================================================================
@ -125,6 +125,8 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
FROM Function TO Method,
FROM Class TO Method,
FROM Class TO Function,
FROM Class TO Class,
FROM Class TO Interface,
type STRING
)`;

View file

@ -47,21 +47,17 @@ import {
export const BASE_SYSTEM_PROMPT = `You are Nexus, a Code Analysis Agent with access to a Knowledge Graph. Your responses MUST be grounded.
## ⚠️ MANDATORY: GROUNDING
Every factual claim MUST include a citation in [[file:line]] format.
- When you mention a function, class, or file → cite where you found it
- When you describe behavior → cite the code that proves it
- NO citation = NO claim. If you can't cite it, say "I didn't find evidence for this."
Examples:
- "The AuthService handles login [[src/services/auth.ts:45]]."
- "This function calls validateToken [[src/utils.ts:12]] which throws on invalid tokens [[src/utils.ts:28]]."
Every factual claim MUST include a citation.
- File refs: [[src/auth.ts:45-60]] (line range with hyphen)
- Node refs: [[Class:AuthService]] or [[Function:validate]]
- NO citation = NO claim. Say "I didn't find evidence" instead of guessing.
## 🧠 CORE PROTOCOL
You are an investigator. For each question:
1. **Search** → Use search or grep to find relevant code
2. **Read** → Use read to see the actual source
3. **Trace** → Use cypher to follow connections in the graph
4. **Cite** → Ground every finding with [[file:line]]
4. **Cite** → Ground every finding with [[file:line]] or [[Type:Name]]
5. **Highlight** → Visualize key nodes with highlight
## 🛠️ TOOLS
@ -73,20 +69,35 @@ You are an investigator. For each question:
## 📊 GRAPH SCHEMA
Nodes: File, Folder, Function, Class, Interface, Method, CodeElement
Relation: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS
Relation: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS
Cypher examples:
- \`MATCH (f:Function) RETURN f.name LIMIT 10\`
- \`MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name\`
## <EFBFBD> RULES
## 📝 RULES
- **Cite or retract.** Never state something you can't ground.
- **Read before concluding.** Don't guess from names alone.
- **Retry on failure.** If a tool fails, fix the input and try again.`;
- **Retry on failure.** If a tool fails, fix the input and try again.
/**
* Create a chat model instance from provider configuration
*/
## 🎯 OUTPUT STYLE
Think like a senior architect. Be concise—no fluff, short, precise and to the point.
- Use tables for comparisons/rankings
- Use mermaid diagrams for flows/dependencies
- Surface deep insights: patterns, coupling, design decisions
- End with **TL;DR** (1-2 sentences)
## MERMAID RULES
When generating diagrams:
- NO special characters in node labels: quotes, (), /, &, <, >
- Wrap labels with spaces in quotes: A["My Label"]
- Use simple IDs: A, B, C or auth, db, api
- Flowchart: graph TD or graph LR (not flowchart)
- Always test mentally: would this parse?
BAD: A[User's Data] --> B(Process & Save)
GOOD: A["User Data"] --> B["Process and Save"]
`;
export const createChatModel = (config: ProviderConfig): BaseChatModel => {
switch (config.provider) {
case 'azure-openai': {

View file

@ -186,15 +186,17 @@ export const createGraphRAGTools = (
},
{
name: 'cypher',
description: `Execute a Cypher query against the code graph. Use for structural queries like finding callers, tracing imports, or custom traversals.
description: `Execute a Cypher query against the code graph. Use for structural queries like finding callers, tracing imports, class inheritance, or custom traversals.
Node tables: File, Folder, Function, Class, Interface, Method, CodeElement
Relation: CodeRelation (single table with 'type' property: CONTAINS, DEFINES, IMPORTS, CALLS)
Relation: CodeRelation (single table with 'type' property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS)
Example queries:
- Functions calling a function: MATCH (caller:Function)-[:CodeRelation {type: 'CALLS'}]->(fn:Function {name: 'validate'}) RETURN caller.name, caller.filePath
- 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
- Functions defined in file: MATCH (f:File {name: 'main.ts'})-[:CodeRelation {type: 'DEFINES'}]->(fn:Function) RETURN fn.name
- All connections: MATCH (f:File {name: 'index.ts'})-[r:CodeRelation]-(m) RETURN m.name, r.type
- All connections: MATCH (n)-[r:CodeRelation]-(m) WHERE n.name = 'MyClass' RETURN m.name, r.type
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

View file

@ -646,47 +646,68 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
}
updateMessage();
// Parse inline grounding references like [[src/file.ts:10-25]]
// and add them to the Code References panel.
// Parse inline grounding references and add them to the Code References panel.
// Supports: [[file.ts:10-25]] (file refs) and [[Class:View]] (node refs)
const currentContentStep = stepsForMessage[stepsForMessage.length - 1];
const fullText = (currentContentStep && currentContentStep.type === 'content')
? (currentContentStep.content || '')
: '';
const refRegex = /\[\[([^\]\n]+?)\]\]/g;
let match: RegExpExecArray | null;
while ((match = refRegex.exec(fullText)) !== null) {
const inner = match[1].trim();
if (!inner) continue;
let rawPath = inner;
let startLine1: number | undefined;
let endLine1: number | undefined;
const lineMatch = inner.match(/^(.*):(\d+)(?:-(\d+))?$/);
if (lineMatch) {
rawPath = lineMatch[1].trim();
startLine1 = parseInt(lineMatch[2], 10);
endLine1 = parseInt(lineMatch[3] || lineMatch[2], 10);
}
// Pattern 1: File refs - [[path/file.ext]] or [[path/file.ext:line]] or [[path/file.ext:line-line]]
// Line numbers are optional
const fileRefRegex = /\[\[([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)(?::(\d+)(?:[-–](\d+))?)?\]\]/g;
let fileMatch: RegExpExecArray | null;
while ((fileMatch = fileRefRegex.exec(fullText)) !== null) {
const rawPath = fileMatch[1].trim();
const startLine1 = fileMatch[2] ? parseInt(fileMatch[2], 10) : undefined;
const endLine1 = fileMatch[3] ? parseInt(fileMatch[3], 10) : startLine1;
const resolvedPath = resolveFilePath(rawPath);
if (!resolvedPath) continue;
const startLine0 = startLine1 ? Math.max(0, startLine1 - 1) : 0;
const endLine0 = endLine1 ? Math.max(0, endLine1 - 1) : startLine0;
const startLine0 = startLine1 !== undefined ? Math.max(0, startLine1 - 1) : undefined;
const endLine0 = endLine1 !== undefined ? Math.max(0, endLine1 - 1) : startLine0;
const nodeId = findFileNodeId(resolvedPath);
addCodeReference({
filePath: resolvedPath,
startLine: startLine1 ? startLine0 : undefined,
endLine: endLine1 ? endLine0 : (startLine1 ? startLine0 : undefined),
startLine: startLine0,
endLine: endLine0,
nodeId,
label: 'File',
name: resolvedPath.split('/').pop() ?? resolvedPath,
source: 'ai',
});
}
// Pattern 2: Node refs - [[Type:Name]] or [[graph:Type:Name]]
const nodeRefRegex = /\[\[(?:graph:)?(Class|Function|Method|Interface|File|Folder|Variable|Enum|Type|CodeElement):([^\]]+)\]\]/g;
let nodeMatch: RegExpExecArray | null;
while ((nodeMatch = nodeRefRegex.exec(fullText)) !== null) {
const nodeType = nodeMatch[1];
const nodeName = nodeMatch[2].trim();
// Find node in graph
if (!graph) continue;
const node = graph.nodes.find(n =>
n.label === nodeType &&
n.properties.name === nodeName
);
if (!node || !node.properties.filePath) continue;
const resolvedPath = resolveFilePath(node.properties.filePath);
if (!resolvedPath) continue;
addCodeReference({
filePath: resolvedPath,
startLine: node.properties.startLine ? node.properties.startLine - 1 : undefined,
endLine: node.properties.endLine ? node.properties.endLine - 1 : undefined,
nodeId: node.id,
label: node.label,
name: node.properties.name,
source: 'ai',
});
}
}
break;

View file

@ -305,14 +305,14 @@ body {
border-top: 1px solid var(--color-border-subtle);
}
/* Links */
.chat-prose a {
/* Links - but NOT grounding/citation buttons (those have their own Tailwind styles) */
.chat-prose a:not(.code-ref-btn) {
color: var(--color-accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.chat-prose a:hover {
.chat-prose a:not(.code-ref-btn):hover {
color: #a78bfa;
}

View file

@ -1,6 +1,6 @@
import { GraphNode, GraphRelationship, KnowledgeGraph } from '../core/graph/types';
export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'complete' | 'error';
export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'heritage' | 'complete' | 'error';
export interface PipelineProgress {
phase: PipelinePhase;