From 5cb0a6aec1b75ea0673d571e53e0851a9db22631 Mon Sep 17 00:00:00 2001 From: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:46:04 -0700 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20searchable=20timezone=20dropdo?= =?UTF-8?q?wn=20in=20Settings=20=E2=86=92=20Chat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The timezone field was a free-text input with no guidance on format. Users had to know the exact IANA identifier (e.g. America/New_York) to configure it. Replace it with a searchable combobox built on Popover + cmdk Command — the same stack as Shadcn's Combobox. Backend: - Add `_timezone_options()` to web_server.py (cached at import time, returns sorted zoneinfo.available_timezones() — ~598 identifiers) - Add `"timezone"` to `_SCHEMA_OVERRIDES` with `type: "select"`, `options`, and `searchable: true` Frontend: - New `SearchableSelect` component (Popover + cmdk Command) — closed-world filterable dropdown for large option lists - `ConfigField` routes to `SearchableSelect` when `schema.searchable === true` (explicit opt-in, no threshold) - Add `searchable?: boolean` to `ConfigFieldSchema` type - Add i18n keys: `searchPlaceholder`, `noResults` The `searchable` flag is deterministic — no existing field is affected unless explicitly opted in. Future large-list fields can adopt the same pattern by adding `searchable: true` to their schema override. --- .../desktop/src/app/settings/config-field.tsx | 17 +++ apps/desktop/src/app/settings/constants.ts | 2 +- .../src/app/settings/searchable-select.tsx | 109 ++++++++++++++++++ apps/desktop/src/i18n/en.ts | 2 + apps/desktop/src/i18n/types.ts | 2 + apps/desktop/src/i18n/zh.ts | 2 + apps/desktop/src/types/hermes.ts | 3 + hermes_cli/web_server.py | 15 +++ 8 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/app/settings/searchable-select.tsx 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",