diff --git a/package-lock.json b/package-lock.json index f146351bc7..3f413e27ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@langchain/core": "^0.3.0", "@langchain/google-genai": "^0.1.0", "@langchain/langgraph": "^0.2.74", + "@langchain/ollama": "^1.2.0", "@langchain/openai": "^0.3.0", "@sigma/edge-curve": "^3.1.0", "@tailwindcss/vite": "^4.1.18", @@ -564,6 +565,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -580,6 +582,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -596,6 +599,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -612,6 +616,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -628,6 +633,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -644,6 +650,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -660,6 +667,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -676,6 +684,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -692,6 +701,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -708,6 +718,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -724,6 +735,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -740,6 +752,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -756,6 +769,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -772,6 +786,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -788,6 +803,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -804,6 +820,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -820,6 +837,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -836,6 +854,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -852,6 +871,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -868,6 +888,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -884,6 +905,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -900,6 +922,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -916,6 +939,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1722,6 +1746,35 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@langchain/ollama": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@langchain/ollama/-/ollama-1.2.0.tgz", + "integrity": "sha512-OinxIhssKXdDQKnQoBF4TQTMBuMMV5OcNPk4Zze8UjcaSOGngn3CAI1FVbBxl0bTG5ov61w4AoWWsUwOwiSJFw==", + "license": "MIT", + "dependencies": { + "ollama": "^0.6.3", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.0.0" + } + }, + "node_modules/@langchain/ollama/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@langchain/openai": { "version": "0.3.17", "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.17.tgz", @@ -2013,6 +2066,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2026,6 +2080,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2039,6 +2094,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2052,6 +2108,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2065,6 +2122,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2078,6 +2136,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2091,6 +2150,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2104,6 +2164,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2117,6 +2178,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2130,6 +2192,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2143,6 +2206,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2156,6 +2220,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2169,6 +2234,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2182,6 +2248,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2195,6 +2262,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2208,6 +2276,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2221,6 +2290,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2234,6 +2304,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2247,6 +2318,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2260,6 +2332,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2273,6 +2346,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2286,6 +2360,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2505,17 +2580,6 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "license": "Apache-2.0" }, - "node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@swc/types": { "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", @@ -3239,12 +3303,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4225,6 +4291,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/cytoscape": { @@ -5037,6 +5104,7 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -5678,6 +5746,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5886,13 +5955,6 @@ "graphology-types": ">=0.19.0" } }, - "node_modules/graphology-types": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", - "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==", - "license": "MIT", - "peer": true - }, "node_modules/graphology-utils": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz", @@ -8190,6 +8252,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -8304,6 +8367,15 @@ "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==", "license": "MIT" }, + "node_modules/ollama": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", + "integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==", + "license": "MIT", + "dependencies": { + "whatwg-fetch": "^3.6.20" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8527,6 +8599,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8597,6 +8670,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -8991,6 +9065,7 @@ "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -9984,6 +10059,7 @@ "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -10165,6 +10241,12 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index 54574349e1..55fdc81892 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@langchain/core": "^0.3.0", "@langchain/google-genai": "^0.1.0", "@langchain/langgraph": "^0.2.74", + "@langchain/ollama": "^1.2.0", "@langchain/openai": "^0.3.0", "@sigma/edge-curve": "^3.1.0", "@tailwindcss/vite": "^4.1.18", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1fe99cacd9..44c90fb4e4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,8 +1,9 @@ import { Search, Settings, HelpCircle, Sparkles, Github, Star } from 'lucide-react'; import { useAppState } from '../hooks/useAppState'; -import { useState, useMemo, useRef, useEffect } from 'react'; +import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { GraphNode } from '../core/graph/types'; import { EmbeddingStatus } from './EmbeddingStatus'; +import { MCPToggle } from './MCPToggle'; // Color mapping for node types in search results const NODE_TYPE_COLORS: Record = { @@ -22,7 +23,17 @@ interface HeaderProps { } export const Header = ({ onFocusNode }: HeaderProps) => { - const { projectName, graph, openChatPanel, isRightPanelOpen, rightPanelTab, setSettingsPanelOpen } = useAppState(); + const { + projectName, + graph, + openChatPanel, + isRightPanelOpen, + rightPanelTab, + setSettingsPanelOpen, + runQuery, + semanticSearch, + setHighlightedNodeIds, + } = useAppState(); const [searchQuery, setSearchQuery] = useState(''); const [isSearchOpen, setIsSearchOpen] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); @@ -206,6 +217,35 @@ export const Header = ({ onFocusNode }: HeaderProps) => { {/* Embedding Status */} + {/* MCP Toggle for external AI agents */} + { + // Use semantic search from the app + const results = await semanticSearch(query, limit); + return results; + }} + onCypher={async (query) => { + // Execute Cypher query + const results = await runQuery(query); + return results; + }} + onBlastRadius={async (nodeId, hops = 2) => { + // Run blast radius query + const query = ` + MATCH (start)-[*1..${hops}]-(connected) + WHERE start.id = '${nodeId}' OR start.name = '${nodeId}' + RETURN DISTINCT connected.id AS id, connected.name AS name, labels(connected) AS labels + `; + const results = await runQuery(query); + return results; + }} + onHighlight={(nodeIds) => { + // Highlight nodes in the graph + setHighlightedNodeIds(new Set(nodeIds)); + }} + /> + {/* Icon buttons */} + +
+

+ Run this command to enable external AI agents: +

+
+ npx gitnexus-mcp setup + +
+ +
+ + ); + } + + // Toggle switch UI + return ( +
+ MCP + + {isConnected && ( + Connected + )} + + {/* Onboarding Tip Popup */} + {showTip && !isConnected && ( +
+ + +
+
+ +
+
+

+ Connect your AI coding tool +

+

+ Let Cursor, Claude Code, or Antigravity use GitNexus for code intelligence. +

+
+
+ +
+

Run this in your terminal:

+
+ npx gitnexus-mcp setup + +
+
+ + +
+ )} +
+ ); +} diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 25e8e266c9..ac06cb7ba0 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -1,10 +1,9 @@ import { useState, useEffect, useCallback } from 'react'; -import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw, Loader2 } from 'lucide-react'; +import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw } from 'lucide-react'; import { loadSettings, saveSettings, getProviderDisplayName, - getAvailableModels, } from '../core/llm/settings-service'; import type { LLMSettings, LLMProvider } from '../core/llm/types'; @@ -15,45 +14,28 @@ interface SettingsPanelProps { } /** - * Fetch available Gemini models from the API + * Check connection to local Ollama instance */ -const fetchGeminiModels = async (apiKey: string): Promise => { +const checkOllamaStatus = async (baseUrl: string): Promise<{ ok: boolean; error: string | null }> => { try { - const response = await fetch( - `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}` - ); + const response = await fetch(`${baseUrl}/api/tags`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); if (!response.ok) { - throw new Error(`API error: ${response.status}`); + if (response.status === 0 || response.status === 404) { + return { ok: false, error: 'Cannot connect to Ollama. Make sure it\'s running with `ollama serve`' }; + } + return { ok: false, error: `Ollama API error: ${response.status}` }; } - const data = await response.json(); - - // Filter for chat-capable models and extract model names - const models = (data.models || []) - .filter((model: any) => { - const methods = model.supportedGenerationMethods || []; - return methods.includes('generateContent'); - }) - .map((model: any) => { - const name = model.name || ''; - return name.replace('models/', ''); - }) - .filter((name: string) => name.length > 0) - .sort((a: string, b: string) => { - const score = (n: string) => { - if (n.includes('gemini-2')) return 0; - if (n.includes('gemini-1.5')) return 1; - if (n.includes('gemini-1')) return 2; - return 3; - }; - return score(a) - score(b) || a.localeCompare(b); - }); - - return models; + return { ok: true, error: null }; } catch (error) { - console.warn('Failed to fetch Gemini models:', error); - return []; + return { + ok: false, + error: 'Cannot connect to Ollama. Make sure it\'s running with `ollama serve`' + }; } }; @@ -61,59 +43,38 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane const [settings, setSettings] = useState(loadSettings); const [showApiKey, setShowApiKey] = useState>({}); const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle'); - - // Gemini model fetching state - const [geminiModels, setGeminiModels] = useState([]); - const [isLoadingModels, setIsLoadingModels] = useState(false); - const [modelFetchError, setModelFetchError] = useState(null); - const [useCustomModel, setUseCustomModel] = useState(false); - const [useCustomAnthropicModel, setUseCustomAnthropicModel] = useState(false); + // Ollama connection state + const [ollamaError, setOllamaError] = useState(null); + const [isCheckingOllama, setIsCheckingOllama] = useState(false); // Load settings when panel opens useEffect(() => { if (isOpen) { setSettings(loadSettings()); setSaveStatus('idle'); - setGeminiModels([]); - setModelFetchError(null); - setUseCustomModel(false); - setUseCustomAnthropicModel(false); + setOllamaError(null); } }, [isOpen]); - // Auto-fetch models when Gemini API key changes - const fetchModels = useCallback(async (apiKey: string) => { - if (!apiKey || apiKey.length < 10) { - setGeminiModels([]); - return; - } + // Check Ollama connection when provider is selected or base URL changes + const checkOllamaConnection = useCallback(async (baseUrl: string) => { + setIsCheckingOllama(true); + setOllamaError(null); - setIsLoadingModels(true); - setModelFetchError(null); - - const models = await fetchGeminiModels(apiKey); - - setIsLoadingModels(false); - - if (models.length > 0) { - setGeminiModels(models); - setModelFetchError(null); - } else { - setGeminiModels([]); - setModelFetchError('Could not fetch models. Check your API key or enter model manually.'); - } + const { error } = await checkOllamaStatus(baseUrl); + setIsCheckingOllama(false); + setOllamaError(error); }, []); - // Fetch models when API key is entered (debounced) useEffect(() => { - if (settings.activeProvider === 'gemini' && settings.gemini?.apiKey) { - const apiKey = settings.gemini.apiKey ?? ''; + if (settings.activeProvider === 'ollama') { + const baseUrl = settings.ollama?.baseUrl ?? 'http://localhost:11434'; const timer = setTimeout(() => { - fetchModels(apiKey); - }, 500); + checkOllamaConnection(baseUrl); + }, 300); return () => clearTimeout(timer); } - }, [settings.gemini?.apiKey, settings.activeProvider, fetchModels]); + }, [settings.ollama?.baseUrl, settings.activeProvider, checkOllamaConnection]); const handleProviderChange = (provider: LLMProvider) => { setSettings(prev => ({ ...prev, activeProvider: provider })); @@ -136,17 +97,8 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane if (!isOpen) return null; - const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai']; + const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai', 'ollama']; - const availableGeminiModels = geminiModels.length > 0 - ? geminiModels - : getAvailableModels('gemini'); - const currentGeminiModel = settings.gemini?.model ?? 'gemini-2.0-flash'; - const isCustomModelSelected = !availableGeminiModels.includes(currentGeminiModel); - - const availableAnthropicModels = getAvailableModels('anthropic'); - const currentAnthropicModel = settings.anthropic?.model ?? 'claude-sonnet-4-20250514'; - const isCustomAnthropicModelSelected = !availableAnthropicModels.includes(currentAnthropicModel); return (
@@ -201,7 +153,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane w-8 h-8 rounded-lg flex items-center justify-center text-lg ${settings.activeProvider === provider ? 'bg-accent/20' : 'bg-surface'} `}> - {provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : '☁️'} + {provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : provider === 'ollama' ? '🦙' : '☁️'}
{getProviderDisplayName(provider)} @@ -251,18 +203,16 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
- + placeholder="e.g., gpt-4o, gpt-4-turbo, gpt-3.5-turbo" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + />
@@ -328,92 +278,17 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
-
- -
- {isLoadingModels && ( - - - Fetching models... - - )} - {geminiModels.length > 0 && ( - - {geminiModels.length} models available - - )} - {settings.gemini?.apiKey && !isLoadingModels && ( - - )} -
-
- - {/* Model selector or manual input */} - {(useCustomModel || isCustomModelSelected) ? ( -
- setSettings(prev => ({ - ...prev, - gemini: { ...prev.gemini!, model: e.target.value } - }))} - placeholder="e.g., gemini-2.0-flash-exp" - className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" - /> - -
- ) : ( -
- - -
- )} - - {modelFetchError && !useCustomModel && ( -

- - {modelFetchError} -

- )} + + setSettings(prev => ({ + ...prev, + gemini: { ...prev.gemini!, model: e.target.value } + }))} + placeholder="e.g., gemini-2.0-flash, gemini-1.5-pro" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + />
)} @@ -460,58 +335,16 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
- - {(useCustomAnthropicModel || isCustomAnthropicModelSelected) ? ( -
- setSettings(prev => ({ - ...prev, - anthropic: { ...prev.anthropic!, model: e.target.value } - }))} - placeholder="e.g., claude-sonnet-4-20250514" - className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" - /> - -
- ) : ( -
- - -
- )} + setSettings(prev => ({ + ...prev, + anthropic: { ...prev.anthropic!, model: e.target.value } + }))} + placeholder="e.g., claude-sonnet-4-20250514, claude-3-opus" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + />
)} @@ -620,6 +453,87 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane )} + {/* Ollama Settings */} + {settings.activeProvider === 'ollama' && ( +
+ {/* How to run Ollama */} +
+

+ 📋 Quick Start: Install Ollama from{' '} + + ollama.ai + , then run: +

+ + ollama serve + +
+ +
+ +
+ setSettings(prev => ({ + ...prev, + ollama: { ...prev.ollama!, baseUrl: e.target.value } + }))} + placeholder="http://localhost:11434" + className="flex-1 px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + /> + +
+

+ Default port is 11434. +

+
+ +
+ + + {ollamaError && !isCheckingOllama && ( +
+

+ + {ollamaError} +

+
+ )} + + setSettings(prev => ({ + ...prev, + ollama: { ...prev.ollama!, model: e.target.value } + }))} + placeholder="e.g., llama3.2, mistral, codellama" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + /> +

+ Pull a model with ollama pull llama3.2 +

+
+
+ )} + {/* Privacy Note */}
diff --git a/src/core/llm/agent.ts b/src/core/llm/agent.ts index e26d69a284..27b4032676 100644 --- a/src/core/llm/agent.ts +++ b/src/core/llm/agent.ts @@ -10,6 +10,7 @@ import { SystemMessage } from '@langchain/core/messages'; import { ChatOpenAI, AzureChatOpenAI } from '@langchain/openai'; import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; import { ChatAnthropic } from '@langchain/anthropic'; +import { ChatOllama } from '@langchain/ollama'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { createGraphRAGTools } from './tools'; import type { @@ -18,6 +19,7 @@ import type { AzureOpenAIConfig, GeminiConfig, AnthropicConfig, + OllamaConfig, AgentStreamChunk, } from './types'; import { @@ -160,6 +162,21 @@ export const createChatModel = (config: ProviderConfig): BaseChatModel => { }); } + case 'ollama': { + const ollamaConfig = config as OllamaConfig; + return new ChatOllama({ + baseUrl: ollamaConfig.baseUrl ?? 'http://localhost:11434', + model: ollamaConfig.model, + temperature: ollamaConfig.temperature ?? 0.1, + streaming: true, + // Allow longer responses (Ollama default is often 128-2048) + numPredict: 30000, + // Increase context window (Ollama default is only 2048!) + // This is critical for agentic workflows with tool calls + numCtx: 32768, + }); + } + default: throw new Error(`Unsupported provider: ${(config as any).provider}`); } @@ -289,6 +306,13 @@ export async function* streamAgentResponse( data = event; } + // DEBUG: Enhanced logging + if (import.meta.env.DEV) { + const msgType = mode === 'messages' && data?.[0]?._getType?.() || 'n/a'; + const hasContent = mode === 'messages' && data?.[0]?.content; + const hasToolCalls = mode === 'messages' && data?.[0]?.tool_calls?.length > 0; + console.log(`🔄 [${mode}] type:${msgType} content:${!!hasContent} tools:${hasToolCalls}`); + } // Handle 'messages' mode - token-by-token streaming if (mode === 'messages') { const [msg] = Array.isArray(data) ? data : [data]; @@ -298,11 +322,23 @@ export async function* streamAgentResponse( // AIMessageChunk - streaming text tokens if (msgType === 'ai' || msgType === 'AIMessage' || msgType === 'AIMessageChunk') { - const content = msg.content; + const rawContent = msg.content; const toolCalls = msg.tool_calls || []; + // Handle content that can be string or array of content blocks + let content: string = ''; + if (typeof rawContent === 'string') { + content = rawContent; + } else if (Array.isArray(rawContent)) { + // Content blocks format: [{type: 'text', text: '...'}, ...] + content = rawContent + .filter((block: any) => block.type === 'text' || typeof block === 'string') + .map((block: any) => typeof block === 'string' ? block : block.text || '') + .join(''); + } + // If chunk has content, stream it - if (content && typeof content === 'string' && content.length > 0) { + if (content && content.length > 0) { // Determine if this is reasoning/narration vs final answer content. // - Before the first tool call: treat as reasoning (narration) // - Between tool calls/results: treat as reasoning @@ -416,9 +452,17 @@ export async function* streamAgentResponse( } } + // DEBUG: Stream completed normally + if (import.meta.env.DEV) { + console.log('✅ Stream completed normally, yielding done'); + } yield { type: 'done' }; } catch (error) { const message = error instanceof Error ? error.message : String(error); + // DEBUG: Stream error + if (import.meta.env.DEV) { + console.error('❌ Stream error:', message, error); + } yield { type: 'error', error: message, diff --git a/src/core/mcp/mcp-client.ts b/src/core/mcp/mcp-client.ts new file mode 100644 index 0000000000..7a4dc0cacb --- /dev/null +++ b/src/core/mcp/mcp-client.ts @@ -0,0 +1,139 @@ +/** + * MCP Browser Client + * + * WebSocket client that connects to the gitnexus-mcp bridge. + * Receives tool calls from external AI agents and executes them. + */ + +export interface MCPMessage { + id: string; + method?: string; + params?: Record; + result?: any; + error?: { message: string }; +} + +type ToolHandler = (params: Record) => Promise; + +export class MCPBrowserClient { + private ws: WebSocket | null = null; + private handlers: Map = new Map(); + private connectionListeners: Set<(connected: boolean) => void> = new Set(); + + constructor(private port = 54319) {} + + /** + * Connect to the MCP bridge + */ + async connect(): Promise { + return new Promise((resolve, reject) => { + try { + this.ws = new WebSocket(`ws://localhost:${this.port}`); + + this.ws.onopen = () => { + this.notifyConnectionListeners(true); + resolve(); + }; + + this.ws.onerror = (event) => { + this.notifyConnectionListeners(false); + reject(new Error('Failed to connect to MCP bridge')); + }; + + this.ws.onmessage = (event) => { + try { + const msg: MCPMessage = JSON.parse(event.data); + this.handleMessage(msg); + } catch (error) { + console.error('[MCP] Failed to parse message:', error); + } + }; + + this.ws.onclose = () => { + this.ws = null; + this.notifyConnectionListeners(false); + }; + } catch (error) { + reject(error); + } + }); + } + + /** + * Handle incoming messages from bridge + */ + private async handleMessage(msg: MCPMessage) { + // This is a tool call request from an external agent + if (msg.method && msg.id) { + const handler = this.handlers.get(msg.method); + + if (handler) { + try { + const result = await handler(msg.params || {}); + this.send({ id: msg.id, result }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + this.send({ id: msg.id, error: { message } }); + } + } else { + this.send({ + id: msg.id, + error: { message: `Unknown tool: ${msg.method}` } + }); + } + } + } + + /** + * Send a message to the bridge + */ + private send(msg: MCPMessage) { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(msg)); + } + } + + /** + * Register a handler for a tool + */ + registerHandler(method: string, handler: ToolHandler) { + this.handlers.set(method, handler); + } + + /** + * Listen for connection state changes + */ + onConnectionChange(listener: (connected: boolean) => void) { + this.connectionListeners.add(listener); + return () => this.connectionListeners.delete(listener); + } + + private notifyConnectionListeners(connected: boolean) { + this.connectionListeners.forEach(listener => listener(connected)); + } + + /** + * Check if connected + */ + get isConnected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; + } + + /** + * Disconnect from bridge + */ + disconnect() { + this.ws?.close(); + this.ws = null; + } +} + +// Singleton instance +let mcpClientInstance: MCPBrowserClient | null = null; + +export function getMCPClient(): MCPBrowserClient { + if (!mcpClientInstance) { + mcpClientInstance = new MCPBrowserClient(); + } + return mcpClientInstance; +}