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
33 changes: 33 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Pre-commit hook (husky): typecheck + unit tests for both packages.
# Mirrors CI checks from ci-quality.yml and ci-tests.yml.
# Skip with: git commit --no-verify
#
# CI coverage:
# quality / typecheck → tsc --noEmit in gitnexus/
# quality / typecheck-web → tsc -b --noEmit in gitnexus-web/
# tests / ubuntu+coverage → vitest run in gitnexus/ (all projects)
# e2e / chromium → playwright (requires servers — skipped)

ROOT="$(git rev-parse --show-toplevel)"

WEB_CHANGED=$(git diff --cached --name-only -- 'gitnexus-web/' | head -1)
CLI_CHANGED=$(git diff --cached --name-only -- 'gitnexus/' | head -1)

if [ -n "$WEB_CHANGED" ]; then
echo "pre-commit: typechecking gitnexus-web (tsc -b)..."
cd "$ROOT/gitnexus-web" && npx tsc -b --noEmit

echo "pre-commit: running gitnexus-web unit tests..."
npx vitest run --reporter=dot
fi

if [ -n "$CLI_CHANGED" ]; then
echo "pre-commit: typechecking gitnexus..."
cd "$ROOT/gitnexus" && npx tsc --noEmit

echo "pre-commit: running gitnexus unit tests (default project)..."
npx vitest run --project default --reporter=dot
fi

echo "pre-commit: all checks passed"
82 changes: 35 additions & 47 deletions gitnexus-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const AppContent = () => {
isRightPanelOpen,
runPipeline,
runPipelineFromFiles,
hydrateServerGraph,
isSettingsPanelOpen,
setSettingsPanelOpen,
isHelpDialogBoxOpen,
Expand All @@ -44,7 +43,7 @@ const AppContent = () => {
availableRepos,
setAvailableRepos,
switchRepo,
hydrateWorkerFromServer,
loadServerGraph,
graph
} = useAppState();

Expand Down Expand Up @@ -138,13 +137,13 @@ const AppContent = () => {
}
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddings, initializeAgent]);

const handleServerConnect = useCallback(async (result: ConnectToServerResult) => {
const handleServerConnect = useCallback((result: ConnectToServerResult): Promise<void> => {
// Extract project name from repoPath
const repoPath = result.repoInfo.repoPath;
const projectName = repoPath.split('/').pop() || 'server-project';
setProjectName(projectName);

// Build KnowledgeGraph from server data (bypasses WASM pipeline entirely)
// Build KnowledgeGraph from server data for visualization
const graph = createKnowledgeGraph();
for (const node of result.nodes) {
graph.addNode(node);
Expand All @@ -161,38 +160,33 @@ const AppContent = () => {
}
setFileContents(fileMap);

try {
await hydrateServerGraph(result);

// Transition directly to exploring view
setViewMode('exploring');
} finally {
setProgress(null);
}

// 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);
}
// Transition directly to exploring view
setViewMode('exploring');

// 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);
// Load graph into LadybugDB (in-browser WASM database) for Nexus AI queries,
// then initialize agent once the database is ready
const loadGraphPromise = loadServerGraph(result.nodes, result.relationships, result.fileContents)
.then(() => {
if (getActiveProviderConfig()) {
return initializeAgent(projectName);
}
})
.then(() => {
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('Failed to load graph into LadybugDB:', err);
// Agent won't work but graph visualization still does
});
Comment thread
magyargergo marked this conversation as resolved.
}).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, setProgress, initializeAgent, startEmbeddings, hydrateServerGraph, hydrateWorkerFromServer]);

return loadGraphPromise;
}, [setViewMode, setGraph, setFileContents, setProjectName, loadServerGraph, initializeAgent, startEmbeddings]);

// Auto-connect when ?server query param is present (bookmarkable shortcut)
const autoConnectRan = useRef(false);
Expand Down Expand Up @@ -225,15 +219,11 @@ const AppContent = () => {
}
}).then(async (result) => {
await handleServerConnect(result);

// Store server URL and fetch available repos for the repo switcher
setProgress(null);
setServerBaseUrl(baseUrl);
try {
const repos = await fetchRepos(baseUrl);
setAvailableRepos(repos);
} catch (e) {
console.warn('Failed to fetch repo list:', e);
}
fetchRepos(baseUrl)
.then((repos) => setAvailableRepos(repos))
.catch((e) => console.warn('Failed to fetch repo list:', e));
}).catch((err) => {
console.error('Auto-connect failed:', err);
setProgress({
Expand Down Expand Up @@ -268,15 +258,13 @@ const AppContent = () => {
onGitClone={handleGitClone}
onServerConnect={async (result, serverUrl) => {
await handleServerConnect(result);
setProgress(null);
if (serverUrl) {
const baseUrl = normalizeServerUrl(serverUrl);
setServerBaseUrl(baseUrl);
try {
const repos = await fetchRepos(baseUrl);
setAvailableRepos(repos);
} catch (e) {
console.warn('Failed to fetch repo list:', e);
}
fetchRepos(baseUrl)
.then((repos) => setAvailableRepos(repos))
.catch((e) => console.warn('Failed to fetch repo list:', e));
}
}}
/>
Expand Down
11 changes: 9 additions & 2 deletions gitnexus-web/src/components/GraphCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
blastRadiusNodeIds,
isAIHighlightsEnabled,
toggleAIHighlights,
clearAIToolHighlights,
clearAICitationHighlights,
clearBlastRadius,
animatedNodes,
} = useAppState();
const [hoveredNodeName, setHoveredNodeName] = useState<string | null>(null);
Expand Down Expand Up @@ -305,9 +308,13 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
<div className="absolute top-4 right-4 z-20">
<button
onClick={() => {
// If turning off, also clear process highlights
if (isAIHighlightsEnabled) {
setHighlightedNodeIds(new Set());
// Turning off — clear AI highlights and selection (preserve user query highlights)
clearAIToolHighlights();
clearAICitationHighlights();
clearBlastRadius();
setSelectedNode(null);
setSigmaSelectedNode(null);
}
toggleAIHighlights();
}}
Expand Down
69 changes: 35 additions & 34 deletions gitnexus-web/src/hooks/useAppState.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react';
import * as Comlink from 'comlink';
import { KnowledgeGraph, GraphNode, NodeLabel } from '../core/graph/types';
import { KnowledgeGraph, GraphNode, GraphRelationship, NodeLabel } from '../core/graph/types';
import { PipelineProgress, PipelineResult, deserializePipelineResult } from '../types/pipeline';
import { createKnowledgeGraph } from '../core/graph/graph';
import { DEFAULT_VISIBLE_LABELS } from '../lib/constants';
Expand Down Expand Up @@ -95,6 +95,7 @@ interface AppState {
isAIHighlightsEnabled: boolean;
toggleAIHighlights: () => void;
clearAIToolHighlights: () => void;
clearAICitationHighlights: () => void;
clearBlastRadius: () => void;
queryResult: QueryResult | null;
setQueryResult: (result: QueryResult | null) => void;
Expand Down Expand Up @@ -123,10 +124,9 @@ interface AppState {
// Worker API (shared across app)
runPipeline: (file: File, onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise<PipelineResult>;
runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise<PipelineResult>;
hydrateServerGraph: (result: ConnectToServerResult) => Promise<void>;
runQuery: (cypher: string) => Promise<any[]>;
isDatabaseReady: () => Promise<boolean>;
hydrateWorkerFromServer: (nodes: any[], relationships: any[], fileContents: Record<string, string>) => Promise<void>;
loadServerGraph: (nodes: GraphNode[], relationships: GraphRelationship[], fileContents: Record<string, string>) => Promise<void>;

// Embedding state
embeddingStatus: EmbeddingStatus;
Expand Down Expand Up @@ -229,6 +229,10 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
setAIToolHighlightedNodeIds(new Set());
}, []);

const clearAICitationHighlights = useCallback(() => {
setAICitationHighlightedNodeIds(new Set());
}, []);

const clearBlastRadius = useCallback(() => {
setBlastRadiusNodeIds(new Set());
}, []);
Expand Down Expand Up @@ -471,16 +475,6 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
return deserializePipelineResult(serializedResult, createKnowledgeGraph);
}, []);

const hydrateServerGraph = useCallback(async (result: ConnectToServerResult): Promise<void> => {
const api = apiRef.current;
if (!api) throw new Error('Worker not initialized');
await api.hydrateServerGraph({
nodes: result.nodes,
relationships: result.relationships,
fileContents: result.fileContents,
});
}, []);

const runQuery = useCallback(async (cypher: string): Promise<any[]> => {
const api = apiRef.current;
if (!api) throw new Error('Worker not initialized');
Expand All @@ -497,14 +491,14 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
}
}, []);

const hydrateWorkerFromServer = useCallback(async (
nodes: any[],
relationships: any[],
const loadServerGraph = useCallback(async (
nodes: GraphNode[],
relationships: GraphRelationship[],
fileContents: Record<string, string>
): Promise<void> => {
const api = apiRef.current;
if (!api) throw new Error('Worker not initialized');
await api.hydrateFromServerData(nodes, relationships, fileContents);
await api.loadServerGraph(nodes, relationships, fileContents);
}, []);

// Embedding methods
Expand Down Expand Up @@ -1004,10 +998,13 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
setProgress({ phase: 'extracting', percent: 0, message: 'Switching repository...', detail: `Loading ${repoName}` });
setViewMode('loading');

setIsAgentReady(false);

// Clear stale graph state from previous repo (highlights, selections, blast radius)
// Without this, sigma reducers dim ALL nodes/edges because old node IDs don't match
setHighlightedNodeIds(new Set());
clearAIToolHighlights();
clearAICitationHighlights();
clearBlastRadius();
setSelectedNode(null);
setQueryResult(null);
Expand Down Expand Up @@ -1042,37 +1039,41 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
for (const [p, c] of Object.entries(result.fileContents)) fileMap.set(p, c);
setFileContents(fileMap);

await hydrateServerGraph(result);

setViewMode('exploring');
setProgress(null);

// Hydrate the worker-side DB (LadybugDB + BM25) so Query/Processes/embeddings work
hydrateWorkerFromServer(result.nodes, result.relationships, result.fileContents).then(() => {
if (getActiveProviderConfig()) initializeAgent(pName);

// Load graph into LadybugDB for Nexus AI queries, then init agent
try {
await loadServerGraph(result.nodes, result.relationships, result.fileContents);
if (getActiveProviderConfig()) {
await initializeAgent(pName);
}
setViewMode('exploring');
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);
});
setProgress(null);
} catch (err) {
console.warn('Failed to load graph into LadybugDB:', err);
setIsAgentReady(false);
await apiRef.current?.disposeAgent();
setAgentError('Failed to load graph into LadybugDB');
setViewMode('exploring');
setProgress(null);
}
} catch (err) {
console.error('Repo switch failed:', err);
setProgress({
phase: 'error', percent: 0,
message: 'Failed to switch repository',
detail: err instanceof Error ? err.message : 'Unknown error',
});
setIsAgentReady(false);
await apiRef.current?.disposeAgent();
setTimeout(() => { setViewMode('exploring'); setProgress(null); }, 3000);
}
}, [serverBaseUrl, setProgress, setViewMode, setProjectName, setGraph, setFileContents, initializeAgent, startEmbeddings, hydrateServerGraph, hydrateWorkerFromServer, setHighlightedNodeIds, clearAIToolHighlights, clearBlastRadius, setSelectedNode, setQueryResult, setCodeReferences, setCodePanelOpen, setCodeReferenceFocus]);
}, [serverBaseUrl, setProgress, setViewMode, setProjectName, setGraph, setFileContents, loadServerGraph, initializeAgent, startEmbeddings, setHighlightedNodeIds, clearAIToolHighlights, clearAICitationHighlights, clearBlastRadius, setSelectedNode, setQueryResult, setCodeReferences, setCodePanelOpen, setCodeReferenceFocus]);

const removeCodeReference = useCallback((id: string) => {
setCodeReferences(prev => {
Expand Down Expand Up @@ -1155,6 +1156,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
isAIHighlightsEnabled,
toggleAIHighlights,
clearAIToolHighlights,
clearAICitationHighlights,
clearBlastRadius,
queryResult,
setQueryResult,
Expand All @@ -1175,10 +1177,9 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
switchRepo,
runPipeline,
runPipelineFromFiles,
hydrateServerGraph,
runQuery,
isDatabaseReady,
hydrateWorkerFromServer,
loadServerGraph,
// Embedding state and methods
embeddingStatus,
embeddingProgress,
Expand Down
Loading
Loading