mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(desktop): render declared settings rows when backend schema omits them
The Memory & Context tab dropped any section key missing from /api/config/schema, so when the backend began skipping memory.provider (hidden in favor of the web dashboard's Plugins page) the desktop lost its Memory Provider picker entirely, along with the provider config panel and OAuth connect affordance mounted under it. The desktop already declares its form in constants (section keys, labels, enum options), and GET /api/config still returns the value. Gate row existence on config presence instead: use the schema entry when present, otherwise infer the field type from the config value. Keys unknown to an older backend stay hidden since they are absent from its config too.
This commit is contained in:
parent
1508ece2d9
commit
b45f5bef16
3 changed files with 85 additions and 10 deletions
|
|
@ -18,9 +18,9 @@ import { setHermesConfigCache, useHermesConfigRecord } from '../hooks/use-config
|
|||
import { useOnProfileSwitch } from '../hooks/use-on-profile-switch'
|
||||
import { PanelEmpty } from '../overlays/panel'
|
||||
|
||||
import { CONTROL_TEXT, EMPTY_SELECT_VALUE, FIELD_DESCRIPTIONS, FIELD_LABELS, SECTIONS } from './constants'
|
||||
import { CONTROL_TEXT, EMPTY_SELECT_VALUE, FIELD_DESCRIPTIONS, FIELD_LABELS } from './constants'
|
||||
import { fieldCopyForSchemaKey } from './field-copy'
|
||||
import { enumOptionsFor, getNested, prettyName, setNested } from './helpers'
|
||||
import { enumOptionsFor, getNested, prettyName, sectionFieldEntries, setNested } from './helpers'
|
||||
import { MemoryConnect } from './memory/connect'
|
||||
import { ModelSettings, ModelSettingsSkeleton } from './model-settings'
|
||||
import { EmptyState, ListRow, LoadingState, SettingsContent } from './primitives'
|
||||
|
|
@ -328,14 +328,12 @@ export function ConfigSettings({
|
|||
}
|
||||
|
||||
const sectionFields = useMemo(() => {
|
||||
if (!schema) {
|
||||
if (!schema || !config) {
|
||||
return new Map<string, [string, ConfigFieldSchema][]>()
|
||||
}
|
||||
|
||||
return new Map(
|
||||
SECTIONS.map(s => [s.id, s.keys.flatMap(k => (schema[k] ? [[k, schema[k]] as [string, ConfigFieldSchema]] : []))])
|
||||
)
|
||||
}, [schema])
|
||||
return sectionFieldEntries(schema, config)
|
||||
}, [schema, config])
|
||||
|
||||
const fields = sectionFields.get(activeSectionId) ?? []
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,15 @@ import { describe, expect, it } from 'vitest'
|
|||
import type { HermesConfigRecord } from '@/types/hermes'
|
||||
|
||||
import { defineFieldCopy, fieldCopyForSchemaKey, schemaKeyToFieldCopyKey } from './field-copy'
|
||||
import { enumOptionsFor, getNested, providerGroup, setNested, stripToolsetLabel, toolsetDisplayLabel } from './helpers'
|
||||
import {
|
||||
enumOptionsFor,
|
||||
getNested,
|
||||
providerGroup,
|
||||
sectionFieldEntries,
|
||||
setNested,
|
||||
stripToolsetLabel,
|
||||
toolsetDisplayLabel
|
||||
} from './helpers'
|
||||
|
||||
describe('settings helpers', () => {
|
||||
it('lists the desktop memory provider options in their declared order', () => {
|
||||
|
|
@ -175,4 +183,39 @@ describe('settings helpers', () => {
|
|||
expect(opts).toContain('xai')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sectionFieldEntries', () => {
|
||||
it('renders memory.provider from config even when the backend schema omits it', () => {
|
||||
const schema = { 'memory.memory_enabled': { type: 'boolean' as const } }
|
||||
const config: HermesConfigRecord = { memory: { memory_enabled: true, provider: '' } }
|
||||
|
||||
const memoryKeys = (sectionFieldEntries(schema, config).get('memory') ?? []).map(([key]) => key)
|
||||
|
||||
expect(memoryKeys).toContain('memory.provider')
|
||||
})
|
||||
|
||||
it('infers the field type from the config value when the schema omits the key', () => {
|
||||
const config: HermesConfigRecord = { memory: { provider: '', memory_enabled: true, memory_char_limit: 2200 } }
|
||||
|
||||
const fields = new Map(sectionFieldEntries({}, config).get('memory') ?? [])
|
||||
|
||||
expect(fields.get('memory.provider')?.type).toBe('string')
|
||||
expect(fields.get('memory.memory_enabled')?.type).toBe('boolean')
|
||||
expect(fields.get('memory.memory_char_limit')?.type).toBe('number')
|
||||
})
|
||||
|
||||
it('prefers the backend schema entry over inference when both exist', () => {
|
||||
const schema = { 'memory.provider': { type: 'select' as const, options: ['honcho'] } }
|
||||
const config: HermesConfigRecord = { memory: { provider: 'honcho' } }
|
||||
|
||||
const field = new Map(sectionFieldEntries(schema, config).get('memory') ?? []).get('memory.provider')
|
||||
|
||||
expect(field?.type).toBe('select')
|
||||
expect(field?.options).toEqual(['honcho'])
|
||||
})
|
||||
|
||||
it('hides declared keys absent from both schema and config', () => {
|
||||
expect(sectionFieldEntries({}, {}).get('memory') ?? []).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { asText } from '@/lib/text'
|
||||
import type { HermesConfigRecord, ToolsetInfo } from '@/types/hermes'
|
||||
import type { ConfigFieldSchema, HermesConfigRecord, ToolsetInfo } from '@/types/hermes'
|
||||
|
||||
import { BUILTIN_PERSONALITIES, ENUM_OPTIONS, PROVIDER_GROUPS } from './constants'
|
||||
import { BUILTIN_PERSONALITIES, ENUM_OPTIONS, PROVIDER_GROUPS, SECTIONS } from './constants'
|
||||
|
||||
// Canonical implementations live in @/lib/text; re-exported here so the many
|
||||
// settings/capabilities call sites keep their import path.
|
||||
|
|
@ -97,6 +97,40 @@ export function getNested(obj: HermesConfigRecord, path: string): unknown {
|
|||
return cur
|
||||
}
|
||||
|
||||
export function inferFieldSchema(value: unknown): ConfigFieldSchema {
|
||||
if (typeof value === 'boolean') {
|
||||
return { type: 'boolean' }
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return { type: 'number' }
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return { type: 'list' }
|
||||
}
|
||||
|
||||
return { type: 'string' }
|
||||
}
|
||||
|
||||
// Backend schema omits some declared keys (e.g. memory.provider); config presence is the availability signal.
|
||||
export function sectionFieldEntries(
|
||||
schema: Record<string, ConfigFieldSchema>,
|
||||
config: HermesConfigRecord
|
||||
): Map<string, [string, ConfigFieldSchema][]> {
|
||||
return new Map(
|
||||
SECTIONS.map(s => [
|
||||
s.id,
|
||||
s.keys.flatMap(k => {
|
||||
const value = getNested(config, k)
|
||||
const field = schema[k] ?? (value === undefined ? undefined : inferFieldSchema(value))
|
||||
|
||||
return field ? [[k, field] as [string, ConfigFieldSchema]] : []
|
||||
})
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
export function setNested(obj: HermesConfigRecord, path: string, value: unknown): HermesConfigRecord {
|
||||
const clone = structuredClone(obj)
|
||||
const parts = configPathParts(path)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue