diff --git a/apps/desktop/src/app/settings/config-settings.tsx b/apps/desktop/src/app/settings/config-settings.tsx index 1d8a2de0127f..b4bb6a82d0e4 100644 --- a/apps/desktop/src/app/settings/config-settings.tsx +++ b/apps/desktop/src/app/settings/config-settings.tsx @@ -5,7 +5,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { Button } from '@/components/ui/button' -import { getElevenLabsVoices, getHermesConfigSchema, saveHermesConfig } from '@/hermes' +import { getElevenLabsVoices, getHermesConfigSchema, getMemoryStatus, saveHermesConfig } from '@/hermes' import { useI18n } from '@/i18n' import { $keepAwake, setKeepAwake } from '@/store/keep-awake' import { notify, notifyError } from '@/store/notifications' @@ -76,6 +76,10 @@ export function ConfigSettings({ const schema = schemaResponse?.fields ?? null const [elevenLabsVoiceOptions, setElevenLabsVoiceOptions] = useState(null) const [elevenLabsVoiceLabels, setElevenLabsVoiceLabels] = useState>({}) + // Discovered memory providers (bundled + user-installed + pip), so the + // memory.provider dropdown reflects what the backend actually serves rather + // than a hardcoded subset. null until the first fetch resolves. + const [memoryProviderOptions, setMemoryProviderOptions] = useState(null) const saveVersionRef = useRef(0) const savedDiscoverySignatureRef = useRef(undefined) const [saveVersion, setSaveVersion] = useState(0) @@ -126,6 +130,27 @@ export function ConfigSettings({ return () => void (cancelled = true) }, []) + useEffect(() => { + let cancelled = false + + getMemoryStatus() + .then(result => { + if (cancelled) { + return + } + + // Empty sentinel first (built-in only), then every discovered plugin. + setMemoryProviderOptions(['', ...result.providers.map(provider => provider.name)]) + }) + .catch(() => { + if (!cancelled) { + setMemoryProviderOptions(null) + } + }) + + return () => void (cancelled = true) + }, []) + useEffect(() => { if (!config || saveVersion === 0) { return @@ -308,7 +333,9 @@ export function ConfigSettings({ enumOptions={ key === 'tts.elevenlabs.voice_id' ? enumOptionsFor(key, getNested(config, key), config, elevenLabsVoiceOptions ?? undefined) - : enumOptionsFor(key, getNested(config, key), config) + : key === 'memory.provider' + ? enumOptionsFor(key, getNested(config, key), config, memoryProviderOptions ?? undefined) + : enumOptionsFor(key, getNested(config, key), config) } onChange={value => updateConfig(setNested(config, key, value))} optionLabels={key === 'tts.elevenlabs.voice_id' ? elevenLabsVoiceLabels : undefined} diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts index 055f72ac19a1..789e8fce3062 100644 --- a/apps/desktop/src/app/settings/constants.ts +++ b/apps/desktop/src/app/settings/constants.ts @@ -249,7 +249,9 @@ export const ENUM_OPTIONS: Record = { 'delegation.reasoning_effort': ['', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], // Built-in memory is not a provider plugin: the empty sentinel renders as // "Built-in only" and a legacy literal `builtin` value is only kept visible - // via enumOptionsFor's current-value passthrough (#49513). + // via enumOptionsFor's current-value passthrough (#49513). This static list + // is only a pre-load fallback: config-settings.tsx feeds enumOptionsFor the + // live discovered set (getMemoryStatus) so user-installed/pip providers show. 'memory.provider': ['', 'honcho', 'hindsight'], // Terminal execution backends — kept in sync with the dispatch ladder in // tools/terminal_tool.py::_create_environment (local/docker/singularity/ diff --git a/apps/desktop/src/app/settings/helpers.test.ts b/apps/desktop/src/app/settings/helpers.test.ts index 33aa771d32b5..825bc2909502 100644 --- a/apps/desktop/src/app/settings/helpers.test.ts +++ b/apps/desktop/src/app/settings/helpers.test.ts @@ -44,6 +44,25 @@ describe('settings helpers', () => { expect(options).toEqual(['', 'honcho', 'hindsight', 'builtin']) }) + it('prefers the discovered provider set over the static fallback for memory.provider', () => { + // config-settings.tsx passes the live getMemoryStatus() providers as + // dynamicOptions; user-installed/pip providers must appear, not just the + // hardcoded honcho/hindsight subset. + const discovered = ['', 'honcho', 'hindsight', 'mem0', 'supermemory'] + const options = enumOptionsFor('memory.provider', '', {}, discovered) + + expect(options).toEqual(discovered) + }) + + it('keeps the current memory.provider value visible even if discovery omits it', () => { + // A provider selected in config but not (yet) returned by discovery must + // still render as the current selection rather than silently vanishing. + const discovered = ['', 'honcho'] + const options = enumOptionsFor('memory.provider', 'hindsight', {}, discovered) + + expect(options).toEqual(['', 'honcho', 'hindsight']) + }) + describe('isExternalMemoryProvider', () => { it('treats only real plugin names as external providers', () => { expect(isExternalMemoryProvider('honcho')).toBe(true)