diff --git a/CHANGELOG.md b/CHANGELOG.md index 723ae5d3..37cddfbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ ### Fixed +- **The auxiliary-model settings now come from a single catalog, so the task list can't drift.** The auxiliary/helper-model task definitions were maintained separately in Python and JavaScript, which let the two lists fall out of sync (and left the retired `session_search` task lingering in one). The settings panel now renders and saves exclusively from a single backend catalog; the retired task is purged on reset while every other custom binding is preserved, and existing configs referencing the old task still load cleanly. Thanks @starship-s. (#5865) + - **The Experimental update channel now shows the real installed version instead of "unknown" / a bare SHA on a stable-tagged install.** When an install pinned to a stable `v*` tag opts into the Experimental channel, no `exp-v*` tag sits behind HEAD, so the channel-scoped version lookup returned nothing and the UI rendered "unknown" (and a bogus ahead-count of 1). The check now recovers the real installed version and the true number of Experimental releases ahead. The installed-version fallback is scoped to the WebUI repo (never injected into the Agent repo's payload), and the compare-link ref is always a git-verified tag (or omitted) rather than a possibly-dirty version string. Thanks @nesquena-hermes. (#5864, #5862) - **The activity worklog no longer freezes the browser on reasoning-heavy history (#4633 frontend half).** A settled turn's activity worklog (the reasoning + tool steps behind the "Processed Xm Ys" chip) can carry 80+ rows; in compact-worklog mode (the default) every collapsed group still eagerly built all its row DOM, so a long history ballooned the DOM and a later synchronous layout could tip the tab into a multi-GB freeze. Collapsed settled worklogs now defer their row DOM until first expand. Expanded rows still get full post-processing (syntax highlighting, copy buttons, Mermaid, KaTeX, structured trees) and their open/scroll state is preserved across transcript rebuilds — matching (and, for nested-card scroll, improving on) the eager path. Thanks @nesquena-hermes. (#5860, #5839, #4633) diff --git a/api/config.py b/api/config.py index 878a983c..84f28626 100644 --- a/api/config.py +++ b/api/config.py @@ -4144,23 +4144,66 @@ def set_hermes_default_model(model_id: str, provider: str | None = None, advance # ── Auxiliary model configuration ────────────────────────────────────────── -# Canonical auxiliary task slots. Keep in sync with hermes_cli/config.py -# DEFAULT_CONFIG["auxiliary"] and hermes_cli/web_server.py _AUX_TASK_SLOTS. -AUX_TASK_SLOTS: tuple[str, ...] = ( - "vision", - "web_extract", - "compression", - "session_search", - "skills_hub", - "approval", - "mcp", - "title_generation", - "curator", - "kanban_decomposer", - "profile_describer", - "triage_specifier", +# Canonical auxiliary task catalog. +# Keep in sync with hermes_cli/config.py DEFAULT_CONFIG["auxiliary"] and +# hermes_cli/web_server.py _AUX_TASK_SLOTS. +AUXILIARY_TASK_CATALOG: tuple[dict[str, str], ...] = ( + {"key": "vision", "label": "Vision", "description": "image/screenshot analysis"}, + {"key": "web_extract", "label": "Web extract", "description": "web page summarization"}, + {"key": "compression", "label": "Compression", "description": "context summarization"}, + {"key": "approval", "label": "Approval", "description": "smart command approval"}, + {"key": "mcp", "label": "MCP", "description": "MCP tool reasoning"}, + {"key": "title_generation", "label": "Title generation", "description": "session titles"}, + {"key": "skills_hub", "label": "Skills hub", "description": "skills search/install"}, + {"key": "curator", "label": "Curator", "description": "skill-usage review pass"}, + {"key": "kanban_decomposer", "label": "Kanban decomposer", "description": "task decomposition"}, + {"key": "profile_describer", "label": "Profile describer", "description": "profile summaries"}, + {"key": "triage_specifier", "label": "Triage specifier", "description": "issue/task triage specs"}, ) +AUX_TASK_SLOTS: tuple[str, ...] = tuple(item["key"] for item in AUXILIARY_TASK_CATALOG) + +# Slots removed from the WebUI catalog whose persisted assignments should be +# discarded when the user explicitly resets all auxiliary-model routing. +RETIRED_AUX_TASK_SLOTS: tuple[str, ...] = ("session_search",) + + +def _aux_task_payload(task_key: str, entry: dict, fallback_label: str = "", fallback_description: str = "") -> dict: + """Build the API payload row for a single auxiliary task.""" + if not isinstance(entry, dict): + entry = {} + return { + "task": task_key, + "provider": str(entry.get("provider") or "auto").strip() or "auto", + "model": str(entry.get("model") or "").strip(), + "base_url": str(entry.get("base_url") or "").strip(), + "timeout": entry.get("timeout", ""), + "download_timeout": entry.get("download_timeout", ""), + "max_concurrency": entry.get("max_concurrency", ""), + "extra_body": entry.get("extra_body") if isinstance(entry.get("extra_body"), dict) else {}, + "api_key_set": bool(str(entry.get("api_key") or "").strip()), + "label": fallback_label, + "description": fallback_description, + } + + +def _iter_auxiliary_task_rows() -> list[dict]: + """Return canonical auxiliary task payload rows.""" + aux_cfg = cfg.get("auxiliary", {}) + if not isinstance(aux_cfg, dict): + aux_cfg = {} + + rows: list[dict] = [] + + # Canonical, first-class tasks from WebUI's catalog. + for slot in AUXILIARY_TASK_CATALOG: + key = str(slot["key"]).strip() + if not key: + continue + rows.append(_aux_task_payload(key, aux_cfg.get(key, {}), slot["label"], slot["description"])) + + return rows + def get_auxiliary_models() -> dict: """Return current auxiliary task assignments from config.yaml. @@ -4181,26 +4224,7 @@ def get_auxiliary_models() -> dict: main_provider = str(model_cfg.get("provider") or "").strip() main_model = str(model_cfg.get("default") or model_cfg.get("name") or "").strip() - aux_cfg = cfg.get("auxiliary", {}) - if not isinstance(aux_cfg, dict): - aux_cfg = {} - - tasks = [] - for slot in AUX_TASK_SLOTS: - entry = aux_cfg.get(slot, {}) - if not isinstance(entry, dict): - entry = {} - tasks.append({ - "task": slot, - "provider": str(entry.get("provider") or "auto").strip(), - "model": str(entry.get("model") or "").strip(), - "base_url": str(entry.get("base_url") or "").strip(), - "timeout": entry.get("timeout", ""), - "download_timeout": entry.get("download_timeout", ""), - "max_concurrency": entry.get("max_concurrency", ""), - "extra_body": entry.get("extra_body") if isinstance(entry.get("extra_body"), dict) else {}, - "api_key_set": bool(str(entry.get("api_key") or "").strip()), - }) + tasks = _iter_auxiliary_task_rows() return { "tasks": tasks, @@ -4238,20 +4262,19 @@ def set_auxiliary_model(task: str, provider: str, model: str, advanced: dict | N Sensitive api_key values are write-only: get_auxiliary_models() only reports whether one is set. """ - if task != "__reset__" and task not in AUX_TASK_SLOTS: - raise ValueError( - f"Unknown auxiliary task slot: {task!r}. Valid: {list(AUX_TASK_SLOTS)}" - ) config_path = _get_config_path() with _cfg_lock: config_data = _load_yaml_config_file(config_path) - + if task != "__reset__" and task not in AUX_TASK_SLOTS: + raise ValueError(f"Unknown auxiliary task slot: {task!r}. Valid: {list(AUX_TASK_SLOTS)}") if task == "__reset__": # Per-slot reset: set each slot to auto, preserving extra fields # (timeout, extra_body, api_key, base_url, download_timeout, etc.) aux_cfg = config_data.get("auxiliary", {}) if not isinstance(aux_cfg, dict): aux_cfg = {} + for retired_slot in RETIRED_AUX_TASK_SLOTS: + aux_cfg.pop(retired_slot, None) for slot in AUX_TASK_SLOTS: slot_cfg = aux_cfg.get(slot, {}) if not isinstance(slot_cfg, dict): diff --git a/static/i18n.js b/static/i18n.js index 5550b4a5..073f9e57 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -772,8 +772,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'context summarization', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'web page summarization', - settings_aux_task_session_search: 'Session search', - settings_aux_task_session_search_desc: 'past-conversation recall', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'smart command approval', settings_aux_task_mcp: 'MCP', @@ -2475,8 +2473,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'context summarization', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'web page summarization', - settings_aux_task_session_search: 'Session search', - settings_aux_task_session_search_desc: 'past-conversation recall', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'smart command approval', settings_aux_task_mcp: 'MCP', @@ -4169,8 +4165,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'コンテキストの要約・圧縮', settings_aux_task_web_extract: 'ウェブページ抽出', settings_aux_task_web_extract_desc: 'ウェブページの要約', - settings_aux_task_session_search: 'セッション検索', - settings_aux_task_session_search_desc: '過去の会話の呼び出し', settings_aux_task_approval: '承認', settings_aux_task_approval_desc: 'コマンド実行のスマート承認', settings_aux_task_mcp: 'MCP', @@ -5581,8 +5575,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'context summarization', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'web page summarization', - settings_aux_task_session_search: 'Session search', - settings_aux_task_session_search_desc: 'past-conversation recall', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'smart command approval', settings_aux_task_mcp: 'MCP', @@ -7192,8 +7184,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'context summarization', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'web page summarization', - settings_aux_task_session_search: 'Session search', - settings_aux_task_session_search_desc: 'past-conversation recall', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'smart command approval', settings_aux_task_mcp: 'MCP', @@ -8798,8 +8788,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'Kontext-Zusammenfassung', settings_aux_task_web_extract: 'Web-Extraktion', settings_aux_task_web_extract_desc: 'Webseiten-Zusammenfassung', - settings_aux_task_session_search: 'Sitzungssuche', - settings_aux_task_session_search_desc: 'Erinnerung an frühere Gespräche', settings_aux_task_approval: 'Freigabe', settings_aux_task_approval_desc: 'intelligente Befehlsfreigabe', settings_aux_task_mcp: 'MCP', @@ -10475,8 +10463,6 @@ const LOCALES = { settings_aux_task_compression_desc: '上下文摘要', settings_aux_task_web_extract: '网页提取', settings_aux_task_web_extract_desc: '网页内容摘要', - settings_aux_task_session_search: '会话搜索', - settings_aux_task_session_search_desc: '历史对话召回', settings_aux_task_approval: '批准', settings_aux_task_approval_desc: '智能指令批准', settings_aux_task_mcp: 'MCP', @@ -12389,8 +12375,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'context summarization', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'web page summarization', - settings_aux_task_session_search: 'Session search', - settings_aux_task_session_search_desc: 'past-conversation recall', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'smart command approval', settings_aux_task_mcp: 'MCP', @@ -13979,8 +13963,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'context summarization', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'web page summarization', - settings_aux_task_session_search: 'Session search', - settings_aux_task_session_search_desc: 'past-conversation recall', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'smart command approval', settings_aux_task_mcp: 'MCP', @@ -15572,8 +15554,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'context summarization', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'web page summarization', - settings_aux_task_session_search: 'Session search', - settings_aux_task_session_search_desc: 'past-conversation recall', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'smart command approval', settings_aux_task_mcp: 'MCP', @@ -17278,8 +17258,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'résumé du contexte', settings_aux_task_web_extract: 'Extraction web', settings_aux_task_web_extract_desc: 'résumé de page web', - settings_aux_task_session_search: 'Recherche de session', - settings_aux_task_session_search_desc: 'rappel de conversations passées', settings_aux_task_approval: 'Approbation', settings_aux_task_approval_desc: 'approbation intelligente des commandes', settings_aux_task_mcp: 'MCP', @@ -19043,8 +19021,6 @@ const LOCALES = { settings_aux_task_mcp_desc: 'uvažování o nástrojích MCP', settings_aux_task_profile_describer: 'Profil popisovač', settings_aux_task_profile_describer_desc: 'souhrny profilů', - settings_aux_task_session_search: 'Hledání relací', - settings_aux_task_session_search_desc: 'připomenutí minulých konverzací', settings_aux_task_skills_hub: 'Centrum dovedností', settings_aux_task_skills_hub_desc: 'hledání/instalace dovedností', settings_aux_task_title_generation: 'Generování názvů', @@ -20576,8 +20552,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'context summarization', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'web page summarization', - settings_aux_task_session_search: 'Session search', - settings_aux_task_session_search_desc: 'past-conversation recall', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'smart command approval', settings_aux_task_mcp: 'MCP', @@ -22292,8 +22266,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'streszczanie kontekstu', settings_aux_task_web_extract: 'Ekstrakcja treści z sieci', settings_aux_task_web_extract_desc: 'streszczanie stron internetowych', - settings_aux_task_session_search: 'Wyszukiwanie sesji', - settings_aux_task_session_search_desc: 'przywoływanie minionych konwersacji', settings_aux_task_approval: 'Zatwierdzanie', settings_aux_task_approval_desc: 'inteligentne zatwierdzanie poleceń', settings_aux_task_mcp: 'MCP', @@ -24855,8 +24827,6 @@ const LOCALES = { settings_aux_task_compression_desc: 'tóm tắt ngữ cảnh', settings_aux_task_web_extract: 'Web extract', settings_aux_task_web_extract_desc: 'trích xuất và tóm tắt trang web', - settings_aux_task_session_search: 'Tìm phiên', - settings_aux_task_session_search_desc: 'truy hồi hội thoại cũ', settings_aux_task_approval: 'Approval', settings_aux_task_approval_desc: 'phê duyệt lệnh thông minh', settings_aux_task_mcp: 'MCP', diff --git a/static/panels.js b/static/panels.js index 43b062f0..bdd74ac9 100644 --- a/static/panels.js +++ b/static/panels.js @@ -11897,24 +11897,8 @@ async function checkUpdatesNow(channelOverride){ // ── Auxiliary Models ────────────────────────────────────────────────────────── -// Canonical auxiliary task slots with display names. -// Keep in sync with hermes_cli/main.py _AUX_TASKS and hermes_cli/web_server.py _AUX_TASK_SLOTS. -const _AUX_TASK_SLOTS=[ - {key:'vision',nameKey:'settings_aux_task_vision',descKey:'settings_aux_task_vision_desc'}, - {key:'compression',nameKey:'settings_aux_task_compression',descKey:'settings_aux_task_compression_desc'}, - {key:'web_extract',nameKey:'settings_aux_task_web_extract',descKey:'settings_aux_task_web_extract_desc'}, - {key:'session_search',nameKey:'settings_aux_task_session_search',descKey:'settings_aux_task_session_search_desc'}, - {key:'approval',nameKey:'settings_aux_task_approval',descKey:'settings_aux_task_approval_desc'}, - {key:'mcp',nameKey:'settings_aux_task_mcp',descKey:'settings_aux_task_mcp_desc'}, - {key:'title_generation',nameKey:'settings_aux_task_title_generation',descKey:'settings_aux_task_title_generation_desc'}, - {key:'skills_hub',nameKey:'settings_aux_task_skills_hub',descKey:'settings_aux_task_skills_hub_desc'}, - {key:'curator',nameKey:'settings_aux_task_curator',descKey:'settings_aux_task_curator_desc'}, - {key:'kanban_decomposer',nameKey:'settings_aux_task_kanban_decomposer',descKey:'settings_aux_task_kanban_decomposer_desc'}, - {key:'profile_describer',nameKey:'settings_aux_task_profile_describer',descKey:'settings_aux_task_profile_describer_desc'}, - {key:'triage_specifier',nameKey:'settings_aux_task_triage_specifier',descKey:'settings_aux_task_triage_specifier_desc'}, -]; - let _auxProviders=[]; // cached provider list from /api/models +let _auxTasks=[]; // sanitized auxiliary task configs from /api/model/auxiliary let _auxOriginalConfig=null; // snapshot of initial config for dirty detection let _mainAdvancedConfig=null; // current advanced config for the default chat model @@ -11922,6 +11906,50 @@ function _auxSelectStyle(){ return 'width:100%;padding:6px 8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:12px;box-sizing:border-box'; } +function _auxTaskLabelFromMeta(taskKey, taskCfg){ + const nameKey='settings_aux_task_'+taskKey; + const descKey=nameKey+'_desc'; + const tName=t(nameKey); + const tDesc=t(descKey); + const name=(tName&&tName!==nameKey)?String(tName).trim():''; + const description=(tDesc&&tDesc!==descKey)?String(tDesc).trim():''; + const fallbackName=(taskCfg&&typeof taskCfg.label==='string'&&taskCfg.label.trim())?String(taskCfg.label).trim():taskKey; + const fallbackDesc=(taskCfg&&typeof taskCfg.description==='string'&&taskCfg.description.trim())?String(taskCfg.description).trim():''; + return { + task: taskKey, + label: name||fallbackName, + description: description||fallbackDesc, + }; +} + +function _normalizeAuxiliaryTasks(rawTasks){ + const tasks=Array.isArray(rawTasks)?rawTasks:[]; + const out=[]; + const seen=new Set(); + for(const rawTask of tasks){ + if(!rawTask||typeof rawTask!=='object') continue; + const task=(typeof rawTask.task==='string'?String(rawTask.task).trim():''); + if(!task||seen.has(task)) continue; + seen.add(task); + const meta=_auxTaskLabelFromMeta(task,rawTask); + const entry={ + task, + provider:String(rawTask.provider||'auto').trim()||'auto', + model:String(rawTask.model||'').trim(), + base_url:String(rawTask.base_url||'').trim(), + timeout:rawTask.timeout, + download_timeout:rawTask.download_timeout, + max_concurrency:rawTask.max_concurrency, + extra_body:rawTask.extra_body&&typeof rawTask.extra_body==='object'?rawTask.extra_body:{}, + api_key_set:!!rawTask.api_key_set, + label:meta.label, + description:meta.description, + }; + out.push(entry); + } + return out; +} + function _buildAuxProviderOptions(sel,providers,currentProvider){ sel.innerHTML=''; // "auto" = use main model @@ -12055,13 +12083,14 @@ function _mainModelSupportsServiceTier(cfg){ return cfg&&cfg.supports_fast_tier===true; } -function _openAuxAdvancedOptions(taskKey,cfg){ - const isMain=taskKey==='__main__'; - const slot=isMain?{key:taskKey,nameKey:'settings_label_model',descKey:'settings_desc_model'}:(_AUX_TASK_SLOTS.find(s=>s.key===taskKey)||{key:taskKey,nameKey:'',descKey:''}); +function _openAuxAdvancedOptions(taskCfg,cfg){ + const isMain=taskCfg==='__main__'; + const taskKey=isMain?'__main__':(taskCfg&&typeof taskCfg==='object'&&typeof taskCfg.task==='string'?taskCfg.task:typeof taskCfg==='string'?taskCfg:''); + const slot=isMain?{task:taskKey,label:(t('settings_label_model')||'Default model')}:_auxTaskLabelFromMeta(taskKey,taskCfg); const overlay=_ensureAuxAdvancedModal(); overlay.dataset.task=taskKey; const title=$('auxAdvancedTitle'),sub=$('auxAdvancedSubtitle'),body=$('auxAdvancedBody'); - const slotName=t(slot.nameKey)||slot.key; + const slotName=isMain?(t('settings_label_model')||'Default model'):(slot&&slot.label)||taskKey; if(title) title.textContent=isMain?(t('settings_main_advanced_title')||'Main model options'):((t('settings_aux_advanced_title')||'{task} options').replace('{task}',slotName)); if(sub) sub.textContent=isMain?(t('settings_main_advanced_subtitle')||'Advanced config for the default chat model.'):(t('settings_aux_advanced_subtitle')||'Advanced config for auxiliary.'); const extraBody=cfg&&cfg.extra_body&&typeof cfg.extra_body==='object'&&Object.keys(cfg.extra_body).length?JSON.stringify(cfg.extra_body,null,2):''; @@ -12185,68 +12214,68 @@ async function _loadAuxiliaryModels(){ container.innerHTML='