diff --git a/gitnexus-web/src/App.tsx b/gitnexus-web/src/App.tsx index 2de2a4a34a..72b78a5d9d 100644 --- a/gitnexus-web/src/App.tsx +++ b/gitnexus-web/src/App.tsx @@ -40,6 +40,7 @@ const AppContent = () => { availableRepos, setAvailableRepos, switchRepo, + hydrateWorkerFromServer, } = useAppState(); const graphCanvasRef = useRef(null); @@ -157,21 +158,31 @@ const AppContent = () => { // Transition directly to exploring view setViewMode('exploring'); + setProgress(null); - // Initialize agent if LLM is configured - if (getActiveProviderConfig()) { - initializeAgent(projectName); - } + // Hydrate the worker-side DB (LadybugDB + BM25) so Query/Processes/embeddings work + hydrateWorkerFromServer(result.nodes, result.relationships, result.fileContents).then(() => { + // Initialize agent if LLM is configured + if (getActiveProviderConfig()) { + initializeAgent(projectName); + } - // Auto-start embeddings - startEmbeddings().catch((err) => { - if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) { - startEmbeddings('wasm').catch(console.warn); - } else { - console.warn('Embeddings auto-start failed:', err); + // Auto-start embeddings (now that LadybugDB is ready) + startEmbeddings().catch((err) => { + if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) { + startEmbeddings('wasm').catch(console.warn); + } else { + console.warn('Embeddings auto-start failed:', err); + } + }); + }).catch((err) => { + console.warn('Worker hydration failed (non-fatal):', err); + // Still initialize agent even if hydration fails + if (getActiveProviderConfig()) { + initializeAgent(projectName); } }); - }, [setViewMode, setGraph, setFileContents, setProjectName, initializeAgent, startEmbeddings]); + }, [setViewMode, setGraph, setFileContents, setProjectName, setProgress, initializeAgent, startEmbeddings, hydrateWorkerFromServer]); // Auto-connect when ?server query param is present (bookmarkable shortcut) const autoConnectRan = useRef(false); diff --git a/gitnexus-web/src/core/lbug/lbug-adapter.ts b/gitnexus-web/src/core/lbug/lbug-adapter.ts index eed042ce25..916d4dacc6 100644 --- a/gitnexus-web/src/core/lbug/lbug-adapter.ts +++ b/gitnexus-web/src/core/lbug/lbug-adapter.ts @@ -190,7 +190,7 @@ export const loadGraphToLbug = async ( for (const tableName of NODE_TABLES) { try { const countRes = await conn.query(`MATCH (n:${tableName}) RETURN count(n) AS cnt`); - const countRows = await countRes.getAll(); + const countRows = await (countRes.getAll?.() ?? countRes.getAllObjects?.() ?? countRes.getAllRows?.() ?? []); const countRow = countRows[0]; const count = countRow ? (countRow.cnt ?? countRow[0] ?? 0) : 0; totalNodes += Number(count); @@ -293,8 +293,8 @@ export const executeQuery = async (cypher: string): Promise => { }); } - // Collect all rows - const allRows = await result.getAll(); + // Collect all rows (handle API differences across LadybugDB versions) + const allRows = await (result.getAll?.() ?? result.getAllObjects?.() ?? result.getAllRows?.() ?? []); const rows: any[] = []; for (const row of allRows) { // Convert tuple to named object if we have column names and row is array @@ -331,7 +331,7 @@ export const getLbugStats = async (): Promise<{ nodes: number; edges: number }> for (const tableName of NODE_TABLES) { try { const nodeResult = await conn.query(`MATCH (n:${tableName}) RETURN count(n) AS cnt`); - const nodeRows = await nodeResult.getAll(); + const nodeRows = await (nodeResult.getAll?.() ?? nodeResult.getAllObjects?.() ?? nodeResult.getAllRows?.() ?? []); const nodeRow = nodeRows[0]; totalNodes += Number(nodeRow?.cnt ?? nodeRow?.[0] ?? 0); } catch { @@ -343,7 +343,7 @@ export const getLbugStats = async (): Promise<{ nodes: number; edges: number }> let totalEdges = 0; try { const edgeResult = await conn.query(`MATCH ()-[r:${REL_TABLE_NAME}]->() RETURN count(r) AS cnt`); - const edgeRows = await edgeResult.getAll(); + const edgeRows = await (edgeResult.getAll?.() ?? edgeResult.getAllObjects?.() ?? edgeResult.getAllRows?.() ?? []); const edgeRow = edgeRows[0]; totalEdges = Number(edgeRow?.cnt ?? edgeRow?.[0] ?? 0); } catch { @@ -408,7 +408,7 @@ export const executePrepared = async ( const result = await conn.execute(stmt, params); - const rows = await result.getAll(); + const rows = await (result.getAll?.() ?? result.getAllObjects?.() ?? result.getAllRows?.() ?? []); await stmt.close(); return rows; @@ -472,7 +472,7 @@ export const testArrayParams = async (): Promise<{ success: boolean; error?: str for (const tableName of NODE_TABLES) { try { const nodeResult = await conn.query(`MATCH (n:${tableName}) RETURN n.id AS id LIMIT 1`); - const nodeRows = await nodeResult.getAll(); + const nodeRows = await (nodeResult.getAll?.() ?? nodeResult.getAllObjects?.() ?? nodeResult.getAllRows?.() ?? []); const nodeRow = nodeRows[0]; if (nodeRow) { testNodeId = nodeRow.id ?? nodeRow[0]; @@ -509,7 +509,7 @@ export const testArrayParams = async (): Promise<{ success: boolean; error?: str const verifyResult = await conn.query( `MATCH (e:${EMBEDDING_TABLE_NAME} {nodeId: '${testNodeId}'}) RETURN e.embedding AS emb` ); - const verifyRows = await verifyResult.getAll(); + const verifyRows = await (verifyResult.getAll?.() ?? verifyResult.getAllObjects?.() ?? verifyResult.getAllRows?.() ?? []); const verifyRow = verifyRows[0]; const storedEmb = verifyRow?.emb ?? verifyRow?.[0]; diff --git a/gitnexus-web/src/hooks/useAppState.tsx b/gitnexus-web/src/hooks/useAppState.tsx index 233a710ee0..843f967d7a 100644 --- a/gitnexus-web/src/hooks/useAppState.tsx +++ b/gitnexus-web/src/hooks/useAppState.tsx @@ -125,6 +125,7 @@ interface AppState { runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise; runQuery: (cypher: string) => Promise; isDatabaseReady: () => Promise; + hydrateWorkerFromServer: (nodes: any[], relationships: any[], fileContents: Record) => Promise; // Embedding state embeddingStatus: EmbeddingStatus; @@ -482,6 +483,16 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { } }, []); + const hydrateWorkerFromServer = useCallback(async ( + nodes: any[], + relationships: any[], + fileContents: Record + ): Promise => { + const api = apiRef.current; + if (!api) throw new Error('Worker not initialized'); + await api.hydrateFromServerData(nodes, relationships, fileContents); + }, []); + // Embedding methods const startEmbeddings = useCallback(async (forceDevice?: 'webgpu' | 'wasm'): Promise => { const api = apiRef.current; @@ -1018,15 +1029,23 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { setFileContents(fileMap); setViewMode('exploring'); + setProgress(null); - if (getActiveProviderConfig()) initializeAgent(pName); + // Hydrate the worker-side DB (LadybugDB + BM25) so Query/Processes/embeddings work + hydrateWorkerFromServer(result.nodes, result.relationships, result.fileContents).then(() => { + if (getActiveProviderConfig()) initializeAgent(pName); - startEmbeddings().catch((err) => { - if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) { - startEmbeddings('wasm').catch(console.warn); - } else { - console.warn('Embeddings auto-start failed:', err); - } + startEmbeddings().catch((err) => { + if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) { + startEmbeddings('wasm').catch(console.warn); + } else { + console.warn('Embeddings auto-start failed:', err); + } + }); + }).catch((err) => { + console.warn('Worker hydration failed (non-fatal):', err); + // Still initialize agent even if hydration fails + if (getActiveProviderConfig()) initializeAgent(pName); }); } catch (err) { console.error('Repo switch failed:', err); @@ -1037,7 +1056,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { }); setTimeout(() => { setViewMode('exploring'); setProgress(null); }, 3000); } - }, [serverBaseUrl, setProgress, setViewMode, setProjectName, setGraph, setFileContents, initializeAgent, startEmbeddings, setHighlightedNodeIds, clearAIToolHighlights, clearBlastRadius, setSelectedNode, setQueryResult, setCodeReferences, setCodePanelOpen, setCodeReferenceFocus]); + }, [serverBaseUrl, setProgress, setViewMode, setProjectName, setGraph, setFileContents, initializeAgent, startEmbeddings, hydrateWorkerFromServer, setHighlightedNodeIds, clearAIToolHighlights, clearBlastRadius, setSelectedNode, setQueryResult, setCodeReferences, setCodePanelOpen, setCodeReferenceFocus]); const removeCodeReference = useCallback((id: string) => { setCodeReferences(prev => { @@ -1142,6 +1161,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { runPipelineFromFiles, runQuery, isDatabaseReady, + hydrateWorkerFromServer, // Embedding state and methods embeddingStatus, embeddingProgress, diff --git a/gitnexus-web/src/workers/ingestion.worker.ts b/gitnexus-web/src/workers/ingestion.worker.ts index 0f36d85771..54974ca56b 100644 --- a/gitnexus-web/src/workers/ingestion.worker.ts +++ b/gitnexus-web/src/workers/ingestion.worker.ts @@ -1,5 +1,7 @@ import * as Comlink from 'comlink'; import { runIngestionPipeline, runPipelineFromFiles } from '../core/ingestion/pipeline'; +import { createKnowledgeGraph } from '../core/graph/graph'; +import type { GraphNode, GraphRelationship } from '../core/graph/types'; import { PipelineProgress, SerializablePipelineResult, serializePipelineResult } from '../types/pipeline'; import { FileEntry } from '../services/zip'; import { @@ -207,6 +209,50 @@ const workerApi = { return serializePipelineResult(result); }, + /** + * Hydrate the worker-side database and indexes from server-loaded data. + * This is the missing step when using server/bridge mode — the main thread + * builds the React graph, but the worker's LadybugDB + BM25 stay empty. + */ + async hydrateFromServerData( + nodes: GraphNode[], + relationships: GraphRelationship[], + fileContents: Record + ): Promise { + // 1. Build a KnowledgeGraph the same way the pipeline does + const graph = createKnowledgeGraph(); + for (const node of nodes) graph.addNode(node); + for (const rel of relationships) graph.addRelationship(rel); + + // 2. Store file contents for grep/read tools + storedFileContents = new Map(Object.entries(fileContents)); + + // 3. Build BM25 keyword index + const bm25DocCount = buildBM25Index(storedFileContents); + if (import.meta.env.DEV) { + console.log(`🔍 BM25 index built (server mode): ${bm25DocCount} documents`); + } + + // 4. Set currentGraphResult so the agent context builder works + currentGraphResult = { graph, fileContents: storedFileContents }; + + // 5. Load graph into LadybugDB for Cypher queries (optional — gracefully degrades) + try { + const lbug = await getLbugAdapter(); + await lbug.loadGraphToLbug(graph, storedFileContents); + + if (import.meta.env.DEV) { + const stats = await lbug.getLbugStats(); + console.log('✅ LadybugDB hydrated (server mode):', stats); + } + } catch (err) { + // LadybugDB is optional — silently continue without it + if (import.meta.env.DEV) { + console.warn('⚠️ LadybugDB hydration failed (non-fatal):', err); + } + } + }, + /** * Execute a Cypher query against the LadybugDB database * @param cypher - The Cypher query string