diff --git a/binary/src/IpcMessenger.ts b/binary/src/IpcMessenger.ts index b92a8e6a238..c5776199ae9 100644 --- a/binary/src/IpcMessenger.ts +++ b/binary/src/IpcMessenger.ts @@ -88,7 +88,7 @@ class IPCMessengerBase< truncatedLine = line.substring(0, 100) + "..." + line.substring(line.length - 100); } - console.error("Error parsing line: ", truncatedLine, e); + console.error("Error parsing JSON from line: ", truncatedLine); return; } } diff --git a/binary/src/TcpMessenger.ts b/binary/src/TcpMessenger.ts index 907e32ed753..69244e23631 100644 --- a/binary/src/TcpMessenger.ts +++ b/binary/src/TcpMessenger.ts @@ -124,7 +124,7 @@ export class TcpMessenger< truncatedLine = line.substring(0, 100) + "..." + line.substring(line.length - 100); } - console.error("Error parsing line: ", truncatedLine, e); + console.error("Error parsing JSON from line: ", truncatedLine); return; } } diff --git a/core/llm/llms/Ollama.ts b/core/llm/llms/Ollama.ts index d2d8660b644..033bdff4e77 100644 --- a/core/llm/llms/Ollama.ts +++ b/core/llm/llms/Ollama.ts @@ -161,47 +161,61 @@ class Ollama extends BaseLLM implements ModelInstaller { private static modelsBeingInstalledMutex = new Mutex(); private fimSupported: boolean = false; + private modelInfoPromise: Promise | undefined; + constructor(options: LLMOptions) { super(options); + } - if (options.model === "AUTODETECT") { + /** + * Lazily fetch model info from Ollama's api/show endpoint. + * This is called on first use rather than in the constructor to avoid + * making HTTP requests when models are just being instantiated for config serialization. + */ + private async ensureModelInfo(): Promise { + if (this.model === "AUTODETECT") { return; } - const headers: Record = { - "Content-Type": "application/json", - }; - if (this.apiKey) { - headers.Authorization = `Bearer ${this.apiKey}`; + // If already fetched or in progress, reuse the promise + if (this.modelInfoPromise) { + return this.modelInfoPromise; } - this.fetch(this.getEndpoint("api/show"), { - method: "POST", - headers: headers, - body: JSON.stringify({ name: this._getModel() }), - }) - .then(async (response) => { + this.modelInfoPromise = (async () => { + const headers: Record = { + "Content-Type": "application/json", + }; + + if (this.apiKey) { + headers.Authorization = `Bearer ${this.apiKey}`; + } + + try { + const response = await this.fetch(this.getEndpoint("api/show"), { + method: "POST", + headers: headers, + body: JSON.stringify({ name: this._getModel() }), + }); + if (response?.status !== 200) { - // console.warn( - // "Error calling Ollama /api/show endpoint: ", - // await response.text(), - // ); return; } + const body = await response.json(); if (body.parameters) { - const params = []; for (const line of body.parameters.split("\n")) { - let parts = line.match(/^(\S+)\s+((?:".*")|\S+)$/); - if (parts.length < 2) { + const parts = line.match(/^(\S+)\s+((?:".*")|\S+)$/); + if (!parts || parts.length < 2) { continue; } - let key = parts[1]; - let value = parts[2]; + const key = parts[1]; + const value = parts[2]; switch (key) { case "num_ctx": - this._contextLength = - options.contextLength ?? Number.parseInt(value); + if (!this._contextLength) { + this._contextLength = Number.parseInt(value); + } break; case "stop": if (!this.completionOptions.stop) { @@ -210,9 +224,7 @@ class Ollama extends BaseLLM implements ModelInstaller { try { this.completionOptions.stop.push(JSON.parse(value)); } catch (e) { - console.warn( - `Error parsing stop parameter value "{value}: ${e}`, - ); + // Ignore parse errors } break; default: @@ -227,10 +239,12 @@ class Ollama extends BaseLLM implements ModelInstaller { * it's a good indication the model supports FIM. */ this.fimSupported = !!body?.template?.includes(".Suffix"); - }) - .catch((e) => { - // console.warn("Error calling the Ollama /api/show endpoint: ", e); - }); + } catch (e) { + // Silently fail - model info is optional + } + })(); + + return this.modelInfoPromise; } // Map of "continue model name" to Ollama actual model name @@ -369,6 +383,7 @@ class Ollama extends BaseLLM implements ModelInstaller { signal: AbortSignal, options: CompletionOptions, ): AsyncGenerator { + await this.ensureModelInfo(); const headers: Record = { "Content-Type": "application/json", }; @@ -414,6 +429,7 @@ class Ollama extends BaseLLM implements ModelInstaller { signal: AbortSignal, options: CompletionOptions, ): AsyncGenerator { + await this.ensureModelInfo(); const ollamaMessages = messages.map(this._convertToOllamaMessage); const chatOptions: OllamaChatOptions = { model: this._getModel(), @@ -565,6 +581,8 @@ class Ollama extends BaseLLM implements ModelInstaller { } supportsFim(): boolean { + // Note: this returns false until model info is fetched + // Could be made async if needed return this.fimSupported; } @@ -574,6 +592,7 @@ class Ollama extends BaseLLM implements ModelInstaller { signal: AbortSignal, options: CompletionOptions, ): AsyncGenerator { + await this.ensureModelInfo(); const headers: Record = { "Content-Type": "application/json", }; @@ -622,21 +641,29 @@ class Ollama extends BaseLLM implements ModelInstaller { if (this.apiKey) { headers.Authorization = `Bearer ${this.apiKey}`; } - const response = await this.fetch( - // localhost was causing fetch failed in pkg binary only for this Ollama endpoint - this.getEndpoint("api/tags"), - { + + try { + const response = await this.fetch(this.getEndpoint("api/tags"), { method: "GET", headers: headers, - }, - ); - const data = await response.json(); - if (response.ok) { - return data.models.map((model: any) => model.name); - } else { - throw new Error( - "Failed to list Ollama models. Make sure Ollama is running.", - ); + }); + const data = await response.json(); + if (response.ok) { + return data.models.map((model: any) => model.name); + } else { + console.warn( + `Ollama /api/tags returned status ${response.status}:`, + data, + ); + throw new Error( + "Failed to list Ollama models. Make sure Ollama is running.", + ); + } + } catch (error) { + console.warn("Failed to list Ollama models:", error); + // If Ollama is not running or returns an error, return an empty list + // This allows the application to continue without blocking on Ollama + return []; } } diff --git a/gui/src/components/OSRContextMenu.tsx b/gui/src/components/OSRContextMenu.tsx index f6cf6e33c04..4c0e93f62c0 100644 --- a/gui/src/components/OSRContextMenu.tsx +++ b/gui/src/components/OSRContextMenu.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useRef, useState } from "react"; -import useIsOSREnabled from "../hooks/useIsOSREnabled"; import { IdeMessengerContext } from "../context/IdeMessenger"; +import useIsOSREnabled from "../hooks/useIsOSREnabled"; import { getPlatform } from "../util"; interface Position { @@ -140,7 +140,7 @@ const OSRContextMenu = () => { } setPosition(null); - if (isOSREnabled && platform.current !== "mac") { + if (isOSREnabled) { document.addEventListener("mousedown", clickHandler); document.addEventListener("mouseleave", leaveWindowHandler); document.addEventListener("contextmenu", contextMenuHandler); @@ -153,7 +153,7 @@ const OSRContextMenu = () => { }; }, [isOSREnabled]); - if (platform.current === "mac" || !isOSREnabled || !position) { + if (!isOSREnabled || !position) { return null; } return ( diff --git a/gui/src/context/IdeMessenger.tsx b/gui/src/context/IdeMessenger.tsx index ab028a1390e..27f8b4d5045 100644 --- a/gui/src/context/IdeMessenger.tsx +++ b/gui/src/context/IdeMessenger.tsx @@ -83,7 +83,7 @@ export class IdeMessenger implements IIdeMessenger { if (typeof vscode === "undefined") { if (isJetBrains()) { if (window.postIntellijMessage === undefined) { - console.log( + console.debug( "Unable to send message: postIntellijMessage is undefined. ", messageType, data, diff --git a/gui/src/hooks/ParallelListeners.tsx b/gui/src/hooks/ParallelListeners.tsx index 172ab5f9d1f..b046631a8c8 100644 --- a/gui/src/hooks/ParallelListeners.tsx +++ b/gui/src/hooks/ParallelListeners.tsx @@ -35,6 +35,9 @@ import { setLocalStorage } from "../util/localStorage"; import { migrateLocalStorage } from "../util/migrateLocalStorage"; import { useWebviewListener } from "./useWebviewListener"; +const INITIAL_CONFIG_POLLING_INTERVAL = 2_000; +const INITIAL_CONFIG_POLLING_MAX_ATTEMPTS = 100; + function ParallelListeners() { const dispatch = useAppDispatch(); const ideMessenger = useContext(IdeMessengerContext); @@ -43,25 +46,71 @@ function ParallelListeners() { const selectedProfileId = useAppSelector( (store) => store.profiles.selectedProfileId, ); - const hasDoneInitialConfigLoad = useRef(false); + + const hasReceivedOrRetrievedConfig = useRef(false); // Load symbols for chat on any session change const sessionId = useAppSelector((state) => state.session.id); const lastSessionId = useAppSelector((store) => store.session.lastSessionId); const [initialSessionId] = useState(sessionId || lastSessionId); + // Once we know core is up + const onInitialConfigLoad = useCallback(async () => { + debugger; + dispatch(setConfigLoading(false)); + void dispatch(refreshSessionMetadata({})); + + const jetbrains = isJetBrains(); + if (jetbrains) { + // Save theme colors to local storage for immediate loading in JetBrains + void ideMessenger + .request("jetbrains/getColors", undefined) + .then((result) => { + if (result.status === "success") { + setDocumentStylesFromTheme(result.content); + } + }); + + // Tell JetBrains the webview is ready + void ideMessenger + .request("jetbrains/onLoad", undefined) + .then((result) => { + if (result.status === "success") { + const msg = result.content; + (window as any).windowId = msg.windowId; + (window as any).serverUrl = msg.serverUrl; + (window as any).workspacePaths = msg.workspacePaths; + (window as any).vscMachineId = msg.vscMachineId; + (window as any).vscMediaUrl = msg.vscMediaUrl; + } + }); + } + + ideMessenger.post("docs/initStatuses", undefined); + void dispatch(updateFileSymbolsFromHistory()); + + if (initialSessionId) { + await dispatch( + loadSession({ + sessionId: initialSessionId, + saveCurrentSession: false, + }), + ); + } + }, [initialSessionId, ideMessenger]); + const handleConfigUpdate = useCallback( - async (isInitial: boolean, result: FromCoreProtocol["configUpdate"][0]) => { + async (result: FromCoreProtocol["configUpdate"][0]) => { const { result: configResult, profileId, organizations, selectedOrgId, } = result; - if (isInitial && hasDoneInitialConfigLoad.current) { - return; + if (hasReceivedOrRetrievedConfig.current === false) { + await onInitialConfigLoad(); } - hasDoneInitialConfigLoad.current = true; + hasReceivedOrRetrievedConfig.current = true; dispatch(setOrganizations(organizations)); dispatch(setSelectedOrgId(selectedOrgId)); dispatch(setSelectedProfile(profileId)); @@ -92,56 +141,65 @@ function ParallelListeners() { setHasReasoningEnabled(supportsReasoning && !isReasoningDisabled), ); }, - [dispatch, hasDoneInitialConfigLoad], + [dispatch, hasReceivedOrRetrievedConfig, onInitialConfigLoad], ); - // Load config from the IDE + // Startup activity useEffect(() => { - async function initialLoadConfig() { - dispatch(setIsSessionMetadataLoading(true)); - dispatch(setConfigLoading(true)); + void dispatch(cancelStream()); + dispatch(setIsSessionMetadataLoading(true)); + dispatch(setConfigLoading(true)); + + // Local storage migration/jetbrains styles loading + const jetbrains = isJetBrains(); + setDocumentStylesFromLocalStorage(jetbrains); + + migrateLocalStorage(dispatch); + + // Poll config just to be safe + async function pollInitialConfigLoad() { const result = await ideMessenger.request( "config/getSerializedProfileInfo", undefined, ); if (result.status === "success") { - await handleConfigUpdate(true, result.content); - } - dispatch(setConfigLoading(false)); - if (initialSessionId) { - await dispatch( - loadSession({ - sessionId: initialSessionId, - saveCurrentSession: false, - }), - ); + console.log("succeeded", result.content); + if (hasReceivedOrRetrievedConfig.current === false) { + await handleConfigUpdate(result.content); + } + } else { + console.log("Failed"); + console.error(result.error); } } - void initialLoadConfig(); + void pollInitialConfigLoad(); + let pollAttempts = 0; const interval = setInterval(() => { - if (hasDoneInitialConfigLoad.current) { - // Init to run on initial config load - ideMessenger.post("docs/initStatuses", undefined); - void dispatch(updateFileSymbolsFromHistory()); - void dispatch(refreshSessionMetadata({})); - - // This triggers sending pending status to the GUI for relevant docs indexes + if (hasReceivedOrRetrievedConfig.current) { clearInterval(interval); + } else if (pollAttempts >= INITIAL_CONFIG_POLLING_MAX_ATTEMPTS) { + clearInterval(interval); + console.warn( + `Config polling stopped after ${INITIAL_CONFIG_POLLING_MAX_ATTEMPTS} attempts`, + ); } else { - void initialLoadConfig(); + console.log("Config load attempt #" + pollAttempts); + pollAttempts++; + void pollInitialConfigLoad(); } - }, 2_000); + }, INITIAL_CONFIG_POLLING_INTERVAL); return () => clearInterval(interval); - }, [hasDoneInitialConfigLoad, ideMessenger, initialSessionId]); + }, [hasReceivedOrRetrievedConfig, ideMessenger, handleConfigUpdate]); + // Handle config update events from core useWebviewListener( "configUpdate", async (update) => { if (!update) { return; } - await handleConfigUpdate(false, update); + await handleConfigUpdate(update); }, [handleConfigUpdate], ); @@ -152,42 +210,6 @@ function ParallelListeners() { } }, [sessionId]); - // ON LOAD - useEffect(() => { - // Override persisted state - void dispatch(cancelStream()); - - const jetbrains = isJetBrains(); - setDocumentStylesFromLocalStorage(jetbrains); - - if (jetbrains) { - // Save theme colors to local storage for immediate loading in JetBrains - void ideMessenger - .request("jetbrains/getColors", undefined) - .then((result) => { - if (result.status === "success") { - setDocumentStylesFromTheme(result.content); - } - }); - - // Tell JetBrains the webview is ready - void ideMessenger - .request("jetbrains/onLoad", undefined) - .then((result) => { - if (result.status === "error") { - return; - } - - const msg = result.content; - (window as any).windowId = msg.windowId; - (window as any).serverUrl = msg.serverUrl; - (window as any).workspacePaths = msg.workspacePaths; - (window as any).vscMachineId = msg.vscMachineId; - (window as any).vscMediaUrl = msg.vscMediaUrl; - }); - } - }, []); - useWebviewListener( "jetbrains/setColors", async (data) => { @@ -253,10 +275,6 @@ function ParallelListeners() { } }, [isInEdit, history]); - useEffect(() => { - migrateLocalStorage(dispatch); - }, []); - return <>; }