Merge pull request #4370 from nesquena/stage-brick
Release v0.51.476 — QK (cross-provider model-pick #4363 + non-git update-status #4356)
This commit is contained in:
7 files changed
+157
-9
No files matched your search
@@ -3,6 +3,13 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.476] — 2026-06-17 — Release QK (cross-provider model-pick + non-git update-status fixes)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Selecting a model from a non-default provider no longer silently reverts to the default model (#4363).** When the model dropdown rebuilds (panel switch, provider refresh, or a live-model fetch that times out and serves a fallback catalog), a cross-provider selection such as `@my-local:gemma4:12b` could vanish from the refreshed catalog — and the browser would quietly snap the `<select>` back to its first `<option>` (the default model). The dropdown now re-injects the chosen model as a custom option when it's missing from the rebuilt catalog, so your selection is preserved instead of silently changing under you. Thanks @edoumo.
|
||||
- **Docker and pip installs now show "Can't check for updates" instead of the misleading "Up to date" (#4356).** When the WebUI or agent is installed without a `.git` directory (e.g. via Docker image or `pip install`), the update check previously reported "Up to date" because there was nothing to compare against. It now detects the absence of `.git` and surfaces "Can't check for updates" in the Settings panel. In mixed deployments where one component has a git checkout and the other does not, the valid git-side update info is still shown alongside the can't-check indicator. Thanks @rodboev.
|
||||
|
||||
## [v0.51.475] — 2026-06-17 — Release QJ (ElevenLabs TTS engine)
|
||||
|
||||
### Added
|
||||
|
||||
+9
-1
@@ -651,9 +651,17 @@ def _check_repo(path, name):
|
||||
tag used to silently report "Up to date" with no remediation affordance, so
|
||||
the Settings panel reads this flag to offer ``apply_force_update`` (issue
|
||||
#4085).
|
||||
|
||||
When ``.git`` is absent (Docker images, pip installs), returns a minimal dict
|
||||
with ``no_git: True`` and ``behind: None`` so the frontend can distinguish
|
||||
"can't check" from "up to date" (issue #4356).
|
||||
"""
|
||||
if path is None or not (path / '.git').exists():
|
||||
return None
|
||||
return {
|
||||
'name': name,
|
||||
'behind': None,
|
||||
'no_git': True,
|
||||
}
|
||||
|
||||
# Fetch tags first so update prompts track published releases, not every
|
||||
# development commit that lands on master/main after the latest release.
|
||||
|
||||
@@ -556,6 +556,7 @@ const LOCALES = {
|
||||
settings_updates_available: '{count} update(s) available',
|
||||
settings_updates_disabled: 'Update checks disabled',
|
||||
settings_update_check_failed: 'Update check failed',
|
||||
settings_update_no_git: 'Can\'t check for updates (not a git checkout)',
|
||||
settings_label_workspace_panel_open: 'Keep workspace panel open by default',
|
||||
settings_desc_workspace_panel_open: 'When enabled, the workspace / file browser panel opens automatically with each new session. You can still close it manually at any time.',
|
||||
settings_label_workspace_todos_tab: 'Show Todos tab in workspace panel',
|
||||
@@ -2046,6 +2047,7 @@ const LOCALES = {
|
||||
settings_updates_available: '{count} aggiornamento/i disponibile/i',
|
||||
settings_updates_disabled: 'Verifica aggiornamenti disabilitata',
|
||||
settings_update_check_failed: 'Verifica aggiornamenti fallita',
|
||||
settings_update_no_git: 'Impossibile verificare aggiornamenti (non \u00e8 un checkout git)',
|
||||
settings_label_workspace_panel_open: 'Mantieni il pannello workspace aperto per impostazione predefinita',
|
||||
settings_desc_workspace_panel_open: 'Se abilitato, il pannello workspace/esplora file si apre automaticamente a ogni nuova sessione. Puoi comunque chiuderlo manualmente in qualsiasi momento.',
|
||||
settings_label_workspace_todos_tab: 'Mostra tab Todos nel pannello workspace',
|
||||
@@ -3526,6 +3528,7 @@ const LOCALES = {
|
||||
settings_up_to_date: '最新です ✓',
|
||||
settings_updates_available: '{count} 件のアップデートあり',
|
||||
settings_update_check_failed: 'アップデート確認失敗',
|
||||
settings_update_no_git: 'アップデートを確認できません(gitチェックアウトではありません)',
|
||||
settings_updates_disabled: 'アップデート確認は無効です',
|
||||
settings_label_workspace_panel_open: 'ワークスペースパネルをデフォルトで開いておく',
|
||||
settings_desc_workspace_panel_open: '有効にすると、新しいセッションごとにワークスペース/ファイルブラウザパネルが自動で開きます。手動でいつでも閉じられます。',
|
||||
@@ -5685,6 +5688,7 @@ const LOCALES = {
|
||||
settings_updates_available: 'Доступно обновлений: {count}',
|
||||
settings_updates_disabled: 'Проверка обновлений отключена',
|
||||
settings_update_check_failed: 'Ошибка проверки обновлений',
|
||||
settings_update_no_git: 'Невозможно проверить обновления (не git-клон)',
|
||||
settings_label_workspace_panel_open: 'Открывать панель рабочей области по умолчанию',
|
||||
settings_desc_workspace_panel_open: 'При включении панель файлов будет открываться автоматически в каждой новой сессии.',
|
||||
settings_label_workspace_todos_tab: 'Показывать вкладку Todos в панели рабочей области',
|
||||
@@ -7091,6 +7095,7 @@ const LOCALES = {
|
||||
settings_updates_available: '{count} actualización(es) disponible(s)',
|
||||
settings_updates_disabled: 'Comprobación de actualizaciones desactivada',
|
||||
settings_update_check_failed: 'Error al comprobar actualizaciones',
|
||||
settings_update_no_git: 'No se pueden comprobar actualizaciones (no es un checkout git)',
|
||||
settings_label_workspace_panel_open: 'Mantener panel de espacio abierto',
|
||||
settings_desc_workspace_panel_open: 'Al activar, el panel de archivos se abre automáticamente en cada nueva sesión. Aún puedes cerrarlo manualmente.',
|
||||
settings_label_workspace_todos_tab: 'Mostrar pestaña Todos en el panel de espacio de trabajo',
|
||||
@@ -8191,6 +8196,7 @@ const LOCALES = {
|
||||
settings_updates_available: '{count} Update(s) verfügbar',
|
||||
settings_updates_disabled: 'Update-Prüfung deaktiviert',
|
||||
settings_update_check_failed: 'Update-Prüfung fehlgeschlagen',
|
||||
settings_update_no_git: 'Updates können nicht geprüft werden (kein Git-Checkout)',
|
||||
settings_label_workspace_panel_open: 'Arbeitsbereich-Panel standardmäßig öffnen',
|
||||
settings_desc_workspace_panel_open: 'Wenn aktiviert, wird der Datei-Browser bei jeder neuen Sitzung automatisch geöffnet. Er kann jederzeit manuell geschlossen werden.',
|
||||
settings_label_workspace_todos_tab: 'Todos-Tab im Arbeitsbereich-Panel anzeigen',
|
||||
@@ -9938,6 +9944,7 @@ const LOCALES = {
|
||||
settings_updates_available: '有 {count} 个更新可用',
|
||||
settings_updates_disabled: '更新检查已禁用',
|
||||
settings_update_check_failed: '更新检查失败',
|
||||
settings_update_no_git: '无法检查更新(非 git 签出)',
|
||||
settings_label_workspace_panel_open: '默认保持工作区面板打开',
|
||||
settings_desc_workspace_panel_open: '启用后,工作区/文件浏览器面板会在每次新会话时自动打开。您仍可随时手动关闭。',
|
||||
settings_label_workspace_todos_tab: '在工作区面板中显示待办事项标签',
|
||||
@@ -10681,6 +10688,7 @@ const LOCALES = {
|
||||
settings_updates_available: '有 {count} 個更新可用',
|
||||
settings_updates_disabled: '更新檢查已停用',
|
||||
settings_update_check_failed: '更新檢查失敗',
|
||||
settings_update_no_git: '無法檢查更新(非 git 簽出)',
|
||||
settings_label_workspace_panel_open: '預設保持工作區面板開啟',
|
||||
settings_desc_workspace_panel_open: '啟用後,工作區/檔案瀏覽器面板會在每次新對話時自動開啟。仍可隨時手動關閉。',
|
||||
settings_label_workspace_todos_tab: '在工作區面板顯示待辦事項標籤',
|
||||
@@ -12054,6 +12062,7 @@ const LOCALES = {
|
||||
settings_updates_available: '{count} atualização(ões) disponível(is)',
|
||||
settings_updates_disabled: 'Verificação de updates desativada',
|
||||
settings_update_check_failed: 'Falha ao verificar updates',
|
||||
settings_update_no_git: 'Não é possível verificar updates (não é um checkout git)',
|
||||
settings_label_workspace_panel_open: 'Manter painel workspace aberto por padrão',
|
||||
settings_desc_workspace_panel_open: 'Quando ativo, o painel workspace abre automaticamente com cada nova sessão.',
|
||||
settings_label_workspace_todos_tab: 'Mostrar aba Todos no painel workspace',
|
||||
@@ -13436,6 +13445,7 @@ const LOCALES = {
|
||||
settings_updates_available: '{count} update(s) available',
|
||||
settings_updates_disabled: 'Update checks disabled',
|
||||
settings_update_check_failed: 'Update check failed',
|
||||
settings_update_no_git: '업데이트를 확인할 수 없습니다 (git 체크아웃이 아닙니다)',
|
||||
settings_label_workspace_panel_open: '기본으로 워크스페이스 패널 열기',
|
||||
settings_desc_workspace_panel_open: '활성화하면 새 세션마다 워크스페이스/파일 브라우저 패널이 자동으로 열립니다. 언제든지 수동으로 닫을 수 있습니다.',
|
||||
settings_label_workspace_todos_tab: '워크스페이스 패널에 Todos 탭 표시',
|
||||
@@ -14837,6 +14847,7 @@ const LOCALES = {
|
||||
settings_updates_available: '{count} mises à jour disponibles',
|
||||
settings_updates_disabled: 'Vérifications de mise à jour désactivées',
|
||||
settings_update_check_failed: 'La vérification de la mise à jour a échoué',
|
||||
settings_update_no_git: 'Impossible de vérifier les mises à jour (pas un checkout git)',
|
||||
settings_label_workspace_panel_open: 'Garder le panneau de l\'espace de travail ouvert par défaut',
|
||||
settings_desc_workspace_panel_open: 'Lorsqu\'il est activé, le panneau de l\'espace de travail/navigateur de fichiers s\'ouvre automatiquement à chaque nouvelle session. Vous pouvez toujours le fermer manuellement à tout moment.',
|
||||
settings_label_workspace_todos_tab: 'Afficher l\'onglet Todos dans le panneau de l\'espace de travail',
|
||||
@@ -16335,6 +16346,7 @@ const LOCALES = {
|
||||
settings_updates_available: '{count} güncelleme mevcut',
|
||||
settings_updates_disabled: 'Güncelleme kontrolleri devre dışı bırakıldı',
|
||||
settings_update_check_failed: 'Güncelleme kontrolü başarısız oldu',
|
||||
settings_update_no_git: 'Güncellemeler kontrol edilemiyor (git checkout değil)',
|
||||
settings_label_workspace_panel_open: 'Çalışma alanı panelini varsayılan olarak açık tut',
|
||||
settings_desc_workspace_panel_open: 'Etkinleştirildiğinde, çalışma alanı / dosya tarayıcı paneli her yeni oturumda otomatik olarak açılır. Yine de istediğiniz zaman manuel olarak kapatabilirsiniz.',
|
||||
settings_label_workspace_todos_tab: 'Çalışma alanı panelinde Todos sekmesini göster',
|
||||
@@ -17831,6 +17843,7 @@ const LOCALES = {
|
||||
settings_updates_available: 'Dostępne aktualizacje ({count})',
|
||||
settings_updates_disabled: 'Sprawdzanie aktualizacji wyłączone',
|
||||
settings_update_check_failed: 'Sprawdzanie aktualizacji nie powiodło się',
|
||||
settings_update_no_git: 'Nie można sprawdzić aktualizacji (nie jest to klon git)',
|
||||
settings_label_workspace_panel_open: 'Zachowaj panel obszaru roboczego domyślnie otwarty',
|
||||
settings_desc_workspace_panel_open: 'Gdy ta opcja jest włączona, panel obszaru roboczego / przeglądarki plików otwiera się automatycznie przy każdej nowej sesji. Nadal możesz go zamknąć ręcznie w dowolnym momencie.',
|
||||
settings_label_session_jump_buttons: 'Pokaż przyciski skoku sesji',
|
||||
|
||||
+11
-1
@@ -8213,12 +8213,22 @@ async function checkUpdatesNow(){
|
||||
const agentPart=formatUpdatePart('Agent',data.agent);
|
||||
if(webuiPart) parts.push(webuiPart);
|
||||
if(agentPart) parts.push(agentPart);
|
||||
// Track non-git targets separately so a mixed deployment (one git
|
||||
// checkout + one no-git install) never hides the "can't check" state
|
||||
// behind an up-to-date summary (#4356).
|
||||
const noGitParts=[];
|
||||
if(data.webui&&data.webui.no_git) noGitParts.push('WebUI');
|
||||
if(data.agent&&data.agent.no_git&&!data.agent.ignored) noGitParts.push('Agent');
|
||||
if(parts.length){
|
||||
if(status){status.textContent=t('settings_updates_available').replace('{count}',parts.join(', '));status.style.color='var(--accent)';}
|
||||
let txt=t('settings_updates_available').replace('{count}',parts.join(', '));
|
||||
if(noGitParts.length) txt+=' · '+t('settings_update_no_git');
|
||||
if(status){status.textContent=txt;status.style.color='var(--accent)';}
|
||||
// Also trigger the update banner
|
||||
if(typeof _showUpdateBanner==='function') _showUpdateBanner(data);
|
||||
} else if(errorParts.length){
|
||||
if(status){status.textContent=t('settings_update_check_failed')+': '+errorParts.join(', ');status.style.color='var(--error)';}
|
||||
} else if(noGitParts.length){
|
||||
if(status){status.textContent=t('settings_update_no_git');status.style.color='var(--muted)';}
|
||||
} else {
|
||||
if(status){status.textContent=t('settings_up_to_date');status.style.color='var(--success)';}
|
||||
if(typeof _showUpdateBanner==='function') _showUpdateBanner(data);
|
||||
|
||||
+18
-4
@@ -1531,14 +1531,28 @@ function _reconcileModelDropdownSelection(sel,data,previousState,opts){
|
||||
// rebuild should preserve the loaded session model or the user's current
|
||||
// in-page selection when it still exists in the refreshed catalog.
|
||||
const shouldApplyBootDefault=!!(opts&&opts.preferProfileDefaultOnFreshBoot);
|
||||
|
||||
// Helper: apply the requested model, but if it is missing from the current
|
||||
// catalog (cross-provider selection after a partial/timed-out rebuild), inject
|
||||
// it as a custom option instead of returning null and letting the browser
|
||||
// silently snap to the first <option>. _ensureModelOptionInDropdown already
|
||||
// tries _applyModelToDropdown first, so delegate to it (single scan) and keep
|
||||
// the plain-apply fallback for the unlikely case it is unavailable.
|
||||
const _applyOrEnsure = function(modelId, providerId) {
|
||||
if (typeof _ensureModelOptionInDropdown === 'function') {
|
||||
return _ensureModelOptionInDropdown(modelId, sel, providerId);
|
||||
}
|
||||
return _applyModelToDropdown(modelId, sel, providerId);
|
||||
};
|
||||
|
||||
if(shouldApplyBootDefault && data&&data.default_model && !(activeSession&&activeSession.model)){
|
||||
return _applyModelToDropdown(data.default_model,sel,data.active_provider||null);
|
||||
return _applyOrEnsure(data.default_model, data.active_provider||null);
|
||||
}
|
||||
if(activeSession&&activeSession.model){
|
||||
return _applyModelToDropdown(activeSession.model,sel,activeSession.model_provider||null);
|
||||
return _applyOrEnsure(activeSession.model, activeSession.model_provider||null);
|
||||
}
|
||||
if(previousState&&previousState.model){
|
||||
return _applyModelToDropdown(previousState.model,sel,previousState.model_provider||null);
|
||||
return _applyOrEnsure(previousState.model, previousState.model_provider||null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -6648,7 +6662,7 @@ async function refreshSession() {
|
||||
}
|
||||
// ── Update banner ──
|
||||
function _formatUpdateTargetStatus(label,info){
|
||||
if(!info||!(info.behind>0)) return null;
|
||||
if(!info||info.no_git||!(info.behind>0)) return null;
|
||||
const release=(info.release_based&&info.latest_version)
|
||||
?` (${info.current_version||'unknown'} -> ${info.latest_version})`
|
||||
:(info.branch?` (${info.branch})`:'');
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Tests for issue #4356: update check returns "can't check" for non-git installs."""
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import api.updates as updates
|
||||
|
||||
|
||||
def test_check_repo_returns_no_git_sentinel_when_dot_git_absent(tmp_path):
|
||||
"""_check_repo returns sentinel dict with no_git=True when .git is absent."""
|
||||
result = updates._check_repo(tmp_path, 'webui')
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, dict)
|
||||
assert result['name'] == 'webui'
|
||||
assert result['behind'] is None
|
||||
assert result['no_git'] is True
|
||||
|
||||
|
||||
def test_check_repo_returns_no_git_sentinel_when_path_is_none():
|
||||
"""_check_repo returns sentinel dict with no_git=True when path is None."""
|
||||
result = updates._check_repo(None, 'webui')
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, dict)
|
||||
assert result['name'] == 'webui'
|
||||
assert result['behind'] is None
|
||||
assert result['no_git'] is True
|
||||
|
||||
|
||||
def test_check_repo_still_returns_dict_when_dot_git_exists(tmp_path):
|
||||
"""_check_repo calls git operations when .git exists; no_git should not be True."""
|
||||
(tmp_path / '.git').mkdir()
|
||||
|
||||
def fake_git(args, cwd, timeout=10):
|
||||
if args == ['diff-index', '--quiet', 'HEAD', '--']:
|
||||
return '', True # clean tree
|
||||
if args == ['fetch', 'origin', '--tags', '--force']:
|
||||
return 'network unreachable', False
|
||||
if args == ['tag', '--list', 'v*', '--sort=-v:refname']:
|
||||
return '', True
|
||||
raise AssertionError(f'unexpected git args: {args!r}')
|
||||
|
||||
with patch.object(updates, '_run_git', side_effect=fake_git):
|
||||
result = updates._check_repo(tmp_path, 'webui')
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, dict)
|
||||
# When .git exists, no_git should not be in result or should be False
|
||||
assert result.get('no_git') is not True
|
||||
|
||||
|
||||
def test_format_update_target_status_excludes_no_git():
|
||||
"""_formatUpdateTargetStatus returns null for no_git so it doesn't show as update available."""
|
||||
ui_path = Path(__file__).resolve().parent.parent / 'static' / 'ui.js'
|
||||
content = ui_path.read_text()
|
||||
|
||||
assert '_formatUpdateTargetStatus' in content
|
||||
assert 'info.no_git' in content
|
||||
|
||||
|
||||
def test_panels_has_no_git_branch():
|
||||
"""Update check consumer in static/panels.js has no_git handling.
|
||||
|
||||
Guards the mixed-deployment fix (#4356): a no_git target must surface the
|
||||
can't-check indicator even when a sibling git target is up-to-date — so the
|
||||
consumer tracks noGitParts separately rather than only firing when EVERY
|
||||
target is no_git (which hid the indicator in mixed installs).
|
||||
"""
|
||||
panels_path = Path(__file__).resolve().parent.parent / 'static' / 'panels.js'
|
||||
content = panels_path.read_text()
|
||||
|
||||
# Check for the no_git conditional in the update consumer
|
||||
assert 'no_git' in content
|
||||
assert 'settings_update_no_git' in content
|
||||
# Mixed-deployment: must use a per-target noGitParts accumulator, NOT the
|
||||
# fragile "every target is no_git" gate that hid the indicator when one
|
||||
# target was a git checkout.
|
||||
assert 'noGitParts' in content
|
||||
assert 'every(c=>c.no_git)' not in content
|
||||
|
||||
|
||||
def test_i18n_no_git_key_all_locales():
|
||||
"""settings_update_no_git key appears in all 13 locale blocks in static/i18n.js."""
|
||||
i18n_path = Path(__file__).resolve().parent.parent / 'static' / 'i18n.js'
|
||||
content = i18n_path.read_text()
|
||||
|
||||
# Count occurrences of the new key (should be exactly 13, one per locale)
|
||||
count = content.count("settings_update_no_git")
|
||||
assert count == 13, f"Expected exactly 13 settings_update_no_git keys, found {count}"
|
||||
@@ -196,9 +196,16 @@ def test_populate_model_dropdown_reconciles_selection_after_rebuild():
|
||||
assert "_reconcileModelDropdownSelection(sel,data,previousSelection,opts);" in UI_JS
|
||||
snippet = _reconcile_selection_snippet()
|
||||
assert "preferProfileDefaultOnFreshBoot" in snippet
|
||||
assert "return _applyModelToDropdown(data.default_model,sel,data.active_provider||null);" in snippet
|
||||
assert "return _applyModelToDropdown(activeSession.model,sel,activeSession.model_provider||null);" in snippet
|
||||
assert "return _applyModelToDropdown(previousState.model,sel,previousState.model_provider||null);" in snippet
|
||||
# #4363: each branch now routes through _applyOrEnsure, which delegates to
|
||||
# _ensureModelOptionInDropdown so a cross-provider model missing from a
|
||||
# partially-rebuilt catalog is injected as a custom option instead of the
|
||||
# browser silently snapping the <select> to its first <option>. The
|
||||
# branch ORDER + per-branch model/provider arguments are unchanged.
|
||||
assert "_applyOrEnsure(data.default_model, data.active_provider||null)" in snippet
|
||||
assert "_applyOrEnsure(activeSession.model, activeSession.model_provider||null)" in snippet
|
||||
assert "_applyOrEnsure(previousState.model, previousState.model_provider||null)" in snippet
|
||||
# the helper must fall back to injecting the missing option, not return null
|
||||
assert "_ensureModelOptionInDropdown(modelId, sel, providerId)" in snippet
|
||||
assert "_readPersistedModelState()" not in snippet
|
||||
assert "localStorage.getItem('hermes-webui-model')" not in snippet
|
||||
|
||||
|
||||
Reference in new issue
Block a user