diff --git a/apps/desktop/src/app/settings/config-field.tsx b/apps/desktop/src/app/settings/config-field.tsx index aac75a2ee971..7e5418b30709 100644 --- a/apps/desktop/src/app/settings/config-field.tsx +++ b/apps/desktop/src/app/settings/config-field.tsx @@ -13,6 +13,7 @@ import { CONTROL_TEXT, EMPTY_SELECT_VALUE, FIELD_DESCRIPTIONS, FIELD_LABELS, FRE import { FallbackModelsField } from './fallback-models-field' import { fieldCopyForSchemaKey } from './field-copy' import { ListRow } from './primitives' +import { SearchableSelect } from './searchable-select' /** * One generic config row: label + description resolved from the i18n field @@ -93,6 +94,22 @@ export function ConfigField({ const selectOptions = enumOptions ?? (schema.type === 'select' ? (schema.options ?? []).map(String) : undefined) + // Large closed-world lists (e.g. ~590 IANA timezones) get a searchable + // Popover + cmdk combobox instead of a closed Select dropdown. The schema + // opt-in via `searchable: true` keeps this deterministic — no field + // accidentally triggers based on dynamic option count. + if (selectOptions && schema.searchable) { + return row( + onChange(next)} + options={selectOptions.filter(o => o !== '')} + placeholder={c.searchPlaceholder} + value={String(value ?? '')} + /> + ) + } + // Voice/model name fields are open-world (custom voice IDs, cloned voices, // brand-new model names) — render a free-input combobox where the known // options are datalist suggestions instead of a closed Select gate. diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts index f041b2992470..3e12e08e70d7 100644 --- a/apps/desktop/src/app/settings/constants.ts +++ b/apps/desktop/src/app/settings/constants.ts @@ -564,7 +564,7 @@ export const FIELD_DESCRIPTIONS: Record = defineFieldCopy({ repoScanRoots: 'Folders to scan. Leave empty to scan your home directory.', repoScanExcludePaths: 'Folders and their descendants to skip during repository discovery.' }, - timezone: 'Used when Hermes needs local time context. Blank uses the system timezone.', + timezone: 'IANA timezone identifier. Blank uses the system timezone.', agent: { imageInputMode: 'Controls how image attachments are sent to the model.', maxTurns: 'Upper bound for tool-calling turns before Hermes stops a run.' diff --git a/apps/desktop/src/app/settings/searchable-select.tsx b/apps/desktop/src/app/settings/searchable-select.tsx new file mode 100644 index 000000000000..c2c55be0af30 --- /dev/null +++ b/apps/desktop/src/app/settings/searchable-select.tsx @@ -0,0 +1,109 @@ +import { useCallback, useRef, useState } from 'react' + +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Codicon } from '@/components/ui/codicon' +import { controlVariants } from '@/components/ui/control' +import { cn } from '@/lib/utils' + +/** + * Searchable select for large option lists (e.g. ~590 IANA timezones). + * Built on Popover + cmdk Command — the same stack as Shadcn's Combobox. + * + * The trigger renders like the existing closed `` dropdown. For large option lists like IANA timezones. */ + searchable?: boolean type?: 'boolean' | 'list' | 'number' | 'select' | 'string' | 'text' } diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 674df896fc41..5c1de9df75a7 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -799,7 +799,22 @@ def _memory_provider_options() -> List[str]: return list(dict.fromkeys(options)) +def _timezone_options() -> List[str]: + """Return sorted IANA timezone identifiers, cached at import time.""" + try: + import zoneinfo + return sorted(zoneinfo.available_timezones()) + except Exception: # pragma: no cover + return ["UTC"] + + _SCHEMA_OVERRIDES: Dict[str, Dict[str, Any]] = { + "timezone": { + "type": "select", + "description": "IANA timezone (e.g. America/New_York). Blank uses the system timezone.", + "options": _timezone_options(), + "searchable": True, + }, "memory.provider": { "type": "select", "description": "Memory provider plugin",