diff --git a/ui/src/adapters/hermes-local/config-fields.tsx b/ui/src/adapters/hermes-local/config-fields.tsx index a35e48dd..568f2f6a 100644 --- a/ui/src/adapters/hermes-local/config-fields.tsx +++ b/ui/src/adapters/hermes-local/config-fields.tsx @@ -1,9 +1,13 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import type { AdapterConfigFieldsProps } from "../types"; import { Field, DraftInput, } from "../../components/agent-config-primitives"; import { ChoosePathButton } from "../../components/PathInstructionsModal"; +import { useCompany } from "../../context/CompanyContext"; +import { ollamaApi } from "../../api/ollama"; const inputClass = "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; @@ -19,6 +23,63 @@ export function HermesLocalConfigFields({ mark, hideInstructionsFile, }: AdapterConfigFieldsProps) { + const { selectedCompanyId } = useCompany(); + const companyId = selectedCompanyId; + + const [manualEntry, setManualEntry] = useState(false); + + const { data: ollamaStatus } = useQuery({ + queryKey: ["ollama", "status", companyId], + queryFn: () => ollamaApi.status(companyId!), + enabled: Boolean(companyId), + staleTime: 60_000, + }); + + const { data: ollamaModels } = useQuery({ + queryKey: ["ollama", "models", companyId], + queryFn: () => ollamaApi.models(companyId!), + enabled: Boolean(companyId && ollamaStatus?.installed), + staleTime: 60_000, + }); + + const currentModel = isCreate + ? (values!.model ?? "") + : eff("adapterConfig", "model", String(config.model ?? "")); + + function handleSelectModel(selectedModel: string) { + if (selectedModel === "__manual__") { + setManualEntry(true); + return; + } + if (selectedModel === "") return; + // Set fields when selecting an Ollama model + if (isCreate) { + // In create mode, CreateConfigValues does not have provider/base_url; + // only model is stored via form values. The server resolves provider + // at runtime from the model name or ~/.hermes/config.yaml. + set!({ model: selectedModel }); + } else { + // In edit mode, set all three fields atomically on adapterConfig + mark("adapterConfig", "model", selectedModel); + mark("adapterConfig", "provider", "custom"); + mark("adapterConfig", "base_url", "http://localhost:11434/v1"); + } + } + + function formatModelLabel(m: { name: string; parameterSize: string; quantization: string; recommended: boolean; recommendationReason: string | null }) { + const params = m.parameterSize ? ` (${m.parameterSize}, ${m.quantization})` : ""; + const recSuffix = m.recommended ? " - Recommended for your system" : ""; + const star = m.recommended ? "* " : ""; + return `${star}${m.name}${params}${recSuffix}`; + } + + const showDropdown = + ollamaStatus?.installed === true && + (ollamaModels?.models?.length ?? 0) > 0 && + !manualEntry; + + const showInstallCallout = ollamaStatus?.installed === false; + return ( <> {!hideInstructionsFile && ( @@ -47,26 +108,59 @@ export function HermesLocalConfigFields({ )} + - - isCreate - ? set!({ model: v }) - : mark("adapterConfig", "model", v || undefined) - } - immediate - className={inputClass} - placeholder="anthropic/claude-sonnet-4" - /> + {showInstallCallout && ( +
+ Ollama is not detected. + + Install Ollama + + to use local models. +
+ )} + + {showDropdown ? ( + + ) : ( + + isCreate + ? set!({ model: v }) + : mark("adapterConfig", "model", v || undefined) + } + immediate + className={inputClass} + placeholder="anthropic/claude-sonnet-4" + /> + )}
+ {!isCreate && ( <> { + return api.get(`/companies/${companyId}/ollama/status`); + }, + models(companyId: string): Promise { + return api.get(`/companies/${companyId}/ollama/models`); + }, +};