From 8dd1c19becd69c8082b3fad6494f443c686917db Mon Sep 17 00:00:00 2001 From: Paul Robello Date: Sun, 22 Feb 2026 10:49:08 -0800 Subject: [PATCH] feat(web): wire agent/chat through HTTP in backend mode Add HTTP wrapper functions (executeQuery, textSearch) to the ingestion worker and a new initializeBackendAgent method that creates the agent with HTTP-backed tool closures instead of local KuzuDB. useAppState detects backend mode and routes agent initialization accordingly. --- README.md | 4 +- gitnexus-web/src/hooks/useAppState.tsx | 34 ++++-- gitnexus-web/src/workers/ingestion.worker.ts | 122 ++++++++++++++++++- gitnexus/README.md | 2 +- 4 files changed, 148 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 840950997..b36cd313b 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ https://github.com/user-attachments/assets/172685ba-8e54-4ea7-9ad1-e31a3398da72 | ----------------- | -------------------------------------------------------------- | ------------------------------------------------------------ | | **What** | Index repos locally, connect AI agents via MCP | Visual graph explorer + AI chat in browser | | **For** | Daily development with Cursor, Claude Code, Windsurf, OpenCode | Quick exploration, demos, one-off analysis | -| **Scale** | Full repos, any size | Limited by browser memory (~5k files) | +| **Scale** | Full repos, any size | Limited by browser memory (~5k files), or unlimited via backend mode | | **Install** | `npm install -g gitnexus` | No install —[gitnexus.vercel.app](https://gitnexus.vercel.app) | | **Storage** | KuzuDB native (fast, persistent) | KuzuDB WASM (in-memory, per session) | | **Parsing** | Tree-sitter native bindings | Tree-sitter WASM | @@ -235,7 +235,7 @@ npm run dev The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAssembly (Tree-sitter WASM, KuzuDB WASM, in-browser embeddings). It's great for quick exploration but limited by browser memory for larger repos. -**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos. No need to re-upload or re-index. +**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically. --- diff --git a/gitnexus-web/src/hooks/useAppState.tsx b/gitnexus-web/src/hooks/useAppState.tsx index faebe915c..b24040157 100644 --- a/gitnexus-web/src/hooks/useAppState.tsx +++ b/gitnexus-web/src/hooks/useAppState.tsx @@ -11,7 +11,7 @@ import type { LLMSettings, ProviderConfig, AgentStreamChunk, ChatMessage, ToolCa import { loadSettings, getActiveProviderConfig, saveSettings } from '../core/llm/settings-service'; import type { AgentMessage } from '../core/llm/agent'; import { DEFAULT_VISIBLE_EDGES, type EdgeType } from '../lib/constants'; -import { runCypherQuery } from '../services/backend'; +import { runCypherQuery, getBackendUrl } from '../services/backend'; export type ViewMode = 'onboarding' | 'loading' | 'exploring'; export type RightPanelTab = 'code' | 'chat'; @@ -571,25 +571,39 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { }, []); const initializeAgent = useCallback(async (overrideProjectName?: string): Promise => { - const api = apiRef.current; - if (!api) { - setAgentError('Worker not initialized'); - return; - } - const config = getActiveProviderConfig(); if (!config) { setAgentError('Please configure an LLM provider in settings'); return; } + const api = apiRef.current; + if (!api) { + setAgentError('Worker not initialized'); + return; + } + setIsAgentInitializing(true); setAgentError(null); try { - // 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); + let result: { success: boolean; error?: string }; + + if (isBackendMode && backendRepo) { + // Backend mode: pass HTTP config + file contents to worker + const entries = Array.from(fileContents.entries()); + result = await api.initializeBackendAgent( + config, + getBackendUrl(), + backendRepo, + entries, + effectiveProjectName, + ); + } else { + result = await api.initializeAgent(config, effectiveProjectName); + } + if (result.success) { setIsAgentReady(true); setAgentError(null); @@ -607,7 +621,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { } finally { setIsAgentInitializing(false); } - }, [projectName]); + }, [projectName, isBackendMode, backendRepo, fileContents]); const sendChatMessage = useCallback(async (message: string): Promise => { const api = apiRef.current; diff --git a/gitnexus-web/src/workers/ingestion.worker.ts b/gitnexus-web/src/workers/ingestion.worker.ts index af50fc2ef..392ba7ca8 100644 --- a/gitnexus-web/src/workers/ingestion.worker.ts +++ b/gitnexus-web/src/workers/ingestion.worker.ts @@ -16,7 +16,7 @@ import { SystemMessage } from '@langchain/core/messages'; import { enrichClustersBatch, ClusterMemberInfo, ClusterEnrichment } from '../core/ingestion/cluster-enricher'; import { CommunityNode } from '../core/ingestion/community-processor'; import { PipelineResult } from '../types/pipeline'; -import { buildCodebaseContext } from '../core/llm/context-builder'; +import { buildCodebaseContext, type CodebaseContext } from '../core/llm/context-builder'; import { buildBM25Index, searchBM25, @@ -54,6 +54,65 @@ let enrichmentCancelled = false; // Chat cancellation flag let chatCancelled = false; +// ============================================================ +// HTTP helpers for backend mode +// ============================================================ + +const httpFetchWithTimeout = async ( + url: string, + init: RequestInit = {}, + timeoutMs: number = 30_000, +): Promise => { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +}; + +const createHttpExecuteQuery = (backendUrl: string, repo: string) => { + return async (cypher: string): Promise => { + const response = await httpFetchWithTimeout(`${backendUrl}/api/query`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cypher, repo }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `Backend query failed: ${response.status}`); + } + const body = await response.json(); + return (body.result ?? body) as any[]; + }; +}; + +const createHttpTextSearch = (executeQuery: (cypher: string) => Promise) => { + return async (query: string, k: number = 15): Promise => { + const sanitized = query.replace(/'/g, "\\'"); + const cypher = ` + MATCH (n) + WHERE n.name IS NOT NULL AND toLower(n.name) CONTAINS toLower('${sanitized}') + RETURN n.id AS id, label(n) AS type, n.name AS name, n.filePath AS filePath, n.content AS content + LIMIT ${k} + `; + try { + const results = await executeQuery(cypher); + return results.map((r: any, i: number) => ({ + id: r.id, + name: r.name, + type: r.type, + filePath: r.filePath, + content: r.content ?? '', + score: 1 - (i * 0.05), + })); + } catch { + return []; + } + }; +}; + /** * Worker API exposed via Comlink * @@ -540,6 +599,67 @@ const workerApi = { } }, + /** + * Initialize the Graph RAG agent in backend mode (HTTP-backed tools). + * Uses HTTP wrappers instead of local KuzuDB for all tool queries. + * @param config - Provider configuration for the LLM + * @param backendUrl - Base URL of the gitnexus serve backend + * @param repoName - Repository name on the backend + * @param fileContentsEntries - File contents as [path, content][] (Comlink can't transfer Maps) + * @param projectName - Display name for the project + */ + async initializeBackendAgent( + config: ProviderConfig, + backendUrl: string, + repoName: string, + fileContentsEntries: [string, string][], + projectName?: string, + ): Promise<{ success: boolean; error?: string }> { + try { + // Rebuild Map from serializable entries (Comlink can't transfer Maps) + const contents = new Map(fileContentsEntries); + storedFileContents = contents; + + // Create HTTP-based tool wrappers + const executeQuery = createHttpExecuteQuery(backendUrl, repoName); + const textSearch = createHttpTextSearch(executeQuery); + + // Build codebase context (uses Cypher queries — works via HTTP) + let codebaseContext: CodebaseContext | undefined; + try { + codebaseContext = await buildCodebaseContext(executeQuery, projectName || repoName); + } catch { + // Non-fatal — agent works without context + } + + // Create agent with HTTP-backed tools + currentAgent = createGraphRAGAgent( + config, + executeQuery, // Cypher via HTTP + textSearch, // semanticSearch → Cypher text search + textSearch, // semanticSearchWithContext → same + textSearch, // hybridSearch → same + () => true, // isEmbeddingReady → always true (routes to textSearch) + () => true, // isBM25Ready → always true (routes to textSearch) + contents, // fileContents Map + codebaseContext, + ); + + currentProviderConfig = config; + + if (import.meta.env.DEV) { + console.log('🤖 Backend agent initialized with provider:', config.provider); + } + + return { success: true }; + } catch (err: any) { + if (import.meta.env.DEV) { + console.error('❌ Backend agent initialization failed:', err); + } + return { success: false, error: err.message || 'Failed to initialize backend agent' }; + } + }, + /** * Check if the agent is initialized */ diff --git a/gitnexus/README.md b/gitnexus/README.md index 22d073675..b69cb1a10 100644 --- a/gitnexus/README.md +++ b/gitnexus/README.md @@ -179,7 +179,7 @@ Installed automatically by both `gitnexus analyze` (per-repo) and `gitnexus setu GitNexus also has a browser-based UI at [gitnexus.vercel.app](https://gitnexus.vercel.app) — 100% client-side, your code never leaves the browser. -**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos. No need to re-upload or re-index. +**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically. ## License