mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
release: v0.50.243 (#1302)
release: v0.50.243 Batch release of 2 PRs. - #1301 — fix: remove PRIMARY chip badge + add Claude Opus 4.7 label Drops the chip-projected configured-model badge added in #1287 (chip width 235px → 164px). Adds Claude Opus 4.7 label entries so the picker no longer renders "Claude Opus 4 7" (missing dot). Independently reviewed and approved by nesquena (commitc0bbd23). - #1297 (@franksong2702) — fix: preserve cron output response snippets Fixes #1295. /api/crons/output now preserves the ## Response section when a large skill dump appears in the prompt section; falls back to file tail when no marker exists. Tests: 3254 passed, 2 skipped, 3 xpassed. Independently reviewed and approved by nesquena (commitb262e4d).
This commit is contained in:
@@ -4,6 +4,13 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
## [v0.50.243] — 2026-04-30
|
||||
|
||||
### Fixed
|
||||
- **Chip composer model badge — removed the `PRIMARY` projection** — The chip-projected configured-model badge added in #1287 was eating ≈30% of chip width (235px → 164px) without adding signal, since the model name is already right next to it. The dropdown rows still show `Primary` / `Fallback N` badges where they actually help distinguish picker entries. Backend `_build_configured_model_badges()` and the `configured_model_badges` payload on `/api/models` are preserved for the dropdown to consume. (`static/index.html`, `static/ui.js`, `static/style.css`, `tests/test_model_picker_badges.py`) — PR #1301
|
||||
- **Claude Opus 4.7 label rendering** — Adds explicit label entries for `anthropic/claude-opus-4.7`, `claude-opus-4.7`, and `claude-opus-4-7` so the picker no longer renders "Claude Opus 4 **7**" (missing dot) when the dashed-form model ID falls through to the generic dash-replace formatter. (`api/config.py`) — PR #1301
|
||||
- **Cron output snippet preserves the `## Response` section** — `/api/crons/output` returned `txt[:8000]` which could drop the useful response section when a large skill dump appeared in the prompt context. Now: if `## Response` exists, preserves a short header plus the response section; if no marker exists, returns the file tail rather than the head. (`api/routes.py`, `tests/test_sprint10.py`) @franksong2702 — PR #1297, fixes #1295
|
||||
|
||||
## [v0.50.242] — 2026-04-30
|
||||
|
||||
### Reverted
|
||||
|
||||
@@ -503,6 +503,7 @@ _FALLBACK_MODELS = [
|
||||
{"provider": "OpenAI", "id": "openai/gpt-5.4-mini", "label": "GPT-5.4 Mini"},
|
||||
{"provider": "OpenAI", "id": "openai/gpt-5.4", "label": "GPT-5.4"},
|
||||
# Anthropic — 4.6 flagship + 4.5 generation
|
||||
{"provider": "Anthropic", "id": "anthropic/claude-opus-4.7", "label": "Claude Opus 4.7"},
|
||||
{"provider": "Anthropic", "id": "anthropic/claude-opus-4.6", "label": "Claude Opus 4.6"},
|
||||
{"provider": "Anthropic", "id": "anthropic/claude-sonnet-4.6", "label": "Claude Sonnet 4.6"},
|
||||
{"provider": "Anthropic", "id": "anthropic/claude-sonnet-4-5", "label": "Claude Sonnet 4.5"},
|
||||
@@ -641,6 +642,7 @@ def _resolve_provider_alias(name: str) -> str:
|
||||
# Well-known models per provider (used to populate dropdown for direct API providers)
|
||||
_PROVIDER_MODELS = {
|
||||
"anthropic": [
|
||||
{"id": "claude-opus-4.7", "label": "Claude Opus 4.7"},
|
||||
{"id": "claude-opus-4.6", "label": "Claude Opus 4.6"},
|
||||
{"id": "claude-sonnet-4.6", "label": "Claude Sonnet 4.6"},
|
||||
{"id": "claude-sonnet-4-5", "label": "Claude Sonnet 4.5"},
|
||||
@@ -738,6 +740,7 @@ _PROVIDER_MODELS = {
|
||||
{"id": "gpt-5", "label": "GPT-5"},
|
||||
{"id": "gpt-5-codex", "label": "GPT-5 Codex"},
|
||||
{"id": "gpt-5-nano", "label": "GPT-5 Nano"},
|
||||
{"id": "claude-opus-4-7", "label": "Claude Opus 4.7"},
|
||||
{"id": "claude-opus-4-6", "label": "Claude Opus 4.6"},
|
||||
{"id": "claude-opus-4-5", "label": "Claude Opus 4.5"},
|
||||
{"id": "claude-opus-4-1", "label": "Claude Opus 4.1"},
|
||||
|
||||
+37
-1
@@ -35,6 +35,8 @@ _CLIENT_DISCONNECT_ERRORS = (
|
||||
# Track job IDs currently being executed so the frontend can poll status.
|
||||
_RUNNING_CRON_JOBS: dict[str, float] = {} # job_id → start_timestamp
|
||||
_RUNNING_CRON_LOCK = threading.Lock()
|
||||
_CRON_OUTPUT_CONTENT_LIMIT = 8000
|
||||
_CRON_OUTPUT_HEADER_CONTEXT = 200
|
||||
|
||||
|
||||
def _mark_cron_running(job_id: str):
|
||||
@@ -56,6 +58,40 @@ def _is_cron_running(job_id: str) -> tuple[bool, float]:
|
||||
return True, time.time() - t
|
||||
|
||||
|
||||
def _cron_response_marker_index(text: str) -> int:
|
||||
"""Return the start index of a markdown Response heading, if present."""
|
||||
candidates = []
|
||||
for heading in ("## Response", "# Response"):
|
||||
if text.startswith(heading):
|
||||
candidates.append(0)
|
||||
idx = text.find(f"\n{heading}")
|
||||
if idx >= 0:
|
||||
candidates.append(idx + 1)
|
||||
return min(candidates) if candidates else -1
|
||||
|
||||
|
||||
def _cron_output_content_window(text: str, limit: int = _CRON_OUTPUT_CONTENT_LIMIT) -> str:
|
||||
"""Return a bounded cron output window that preserves useful response text.
|
||||
|
||||
Cron output files can contain large skill dumps in the Prompt section. The
|
||||
UI already extracts ``## Response`` when present, so keep that section in
|
||||
the API payload instead of blindly returning the first ``limit`` chars.
|
||||
"""
|
||||
if limit <= 0:
|
||||
return ""
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
|
||||
response_idx = _cron_response_marker_index(text)
|
||||
if response_idx >= 0:
|
||||
header = text[:min(_CRON_OUTPUT_HEADER_CONTEXT, response_idx)].rstrip()
|
||||
response = text[response_idx:].lstrip("\n")
|
||||
content = f"{header}\n...\n{response}" if header else response
|
||||
return content[:limit]
|
||||
|
||||
return text[-limit:]
|
||||
|
||||
|
||||
def _run_cron_tracked(job):
|
||||
"""Wrapper that tracks running state around cron.scheduler.run_job."""
|
||||
try:
|
||||
@@ -2932,7 +2968,7 @@ def _handle_cron_output(handler, parsed):
|
||||
for f in files:
|
||||
try:
|
||||
txt = f.read_text(encoding="utf-8", errors="replace")
|
||||
outputs.append({"filename": f.name, "content": txt[:8000]})
|
||||
outputs.append({"filename": f.name, "content": _cron_output_content_window(txt)})
|
||||
except Exception:
|
||||
logger.debug("Failed to read cron output file %s", f)
|
||||
return j(handler, {"job_id": job_id, "outputs": outputs})
|
||||
|
||||
@@ -401,7 +401,6 @@
|
||||
<button class="composer-model-chip" id="composerModelChip" type="button" onclick="toggleModelDropdown()" title="Conversation model">
|
||||
<span class="composer-model-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg></span>
|
||||
<span class="composer-model-label" id="composerModelLabel"></span>
|
||||
<span class="composer-model-badge" id="composerModelBadge" hidden></span>
|
||||
<span class="composer-model-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
<select id="modelSelect" class="composer-model-select" title="Conversation model" aria-hidden="true" tabindex="-1">
|
||||
|
||||
@@ -824,10 +824,6 @@
|
||||
.composer-model-chip:hover{color:var(--text);background-color:var(--hover-bg);}
|
||||
.composer-model-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
|
||||
.composer-model-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.composer-model-badge{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;padding:2px 7px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;border:1px solid transparent;}
|
||||
.composer-model-badge[hidden]{display:none;}
|
||||
.composer-model-badge--primary{background:rgba(50,184,198,.16);border-color:rgba(50,184,198,.32);color:#8fe7ef;}
|
||||
.composer-model-badge--fallback{background:rgba(255,184,77,.14);border-color:rgba(255,184,77,.28);color:#ffd18a;}
|
||||
.composer-model-icon,.composer-model-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
||||
.composer-model-select{position:absolute!important;left:-9999px!important;width:1px!important;height:1px!important;opacity:0!important;pointer-events:none!important;}
|
||||
.composer-right{display:flex;gap:8px;align-items:center;flex-shrink:0;}
|
||||
|
||||
@@ -425,28 +425,16 @@ function syncModelChip(){
|
||||
const sel=$('modelSelect');
|
||||
const chip=$('composerModelChip');
|
||||
const label=$('composerModelLabel');
|
||||
const badgeEl=$('composerModelBadge');
|
||||
const dd=$('composerModelDropdown');
|
||||
if(!sel||!chip||!label) return;
|
||||
// Don't show a model label until boot has finished loading to prevent flash of wrong default
|
||||
if(!S._bootReady){
|
||||
label.textContent='';
|
||||
chip.title='Conversation model';
|
||||
if(badgeEl){
|
||||
badgeEl.textContent='';
|
||||
badgeEl.hidden=true;
|
||||
badgeEl.className='composer-model-badge';
|
||||
}
|
||||
return;
|
||||
}
|
||||
const opt=_selectedModelOption();
|
||||
label.textContent=opt?opt.textContent:getModelLabel(sel.value||'');
|
||||
const badge=_getConfiguredModelBadge(sel.value||'',window._configuredModelBadges||{});
|
||||
if(badgeEl){
|
||||
badgeEl.textContent=badge&&badge.label?badge.label:'';
|
||||
badgeEl.hidden=!badgeEl.textContent;
|
||||
badgeEl.className='composer-model-badge'+(badge&&badge.role?` composer-model-badge--${badge.role}`:'');
|
||||
}
|
||||
chip.title=sel.value||'Conversation model';
|
||||
chip.classList.toggle('active',!!(dd&&dd.classList.contains('open')));
|
||||
}
|
||||
|
||||
@@ -106,18 +106,17 @@ def test_ui_renders_model_badges_from_api_payload():
|
||||
"A UI precisa de um helper de matching resiliente para religar badges mesmo quando "
|
||||
"o update do catálogo mudar prefixos/formas do model ID."
|
||||
)
|
||||
assert 'id="composerModelBadge"' in html, (
|
||||
"O chip principal do modelo precisa de um container dedicado para exibir o badge "
|
||||
"do modelo selecionado fora do dropdown."
|
||||
# Chip-projected badge was removed in v0.50.243 (added too much width to the
|
||||
# composer chip; signal value low since the model name is right next to it).
|
||||
# Badges remain in the dropdown rows (model-opt-badge) for picker rows.
|
||||
assert 'id="composerModelBadge"' not in html, (
|
||||
"composer-model-badge chip projection was intentionally removed — "
|
||||
"do not re-add it to the composer chip."
|
||||
)
|
||||
assert "composer-model-badge" in css, (
|
||||
"O badge do chip principal precisa de estilo próprio para ficar visível ao lado "
|
||||
"do nome do modelo selecionado."
|
||||
assert "composer-model-badge" not in css, (
|
||||
"composer-model-badge CSS was intentionally removed alongside the chip span."
|
||||
)
|
||||
assert "const badge=_getConfiguredModelBadge(sel.value||'',window._configuredModelBadges||{});" in js, (
|
||||
"syncModelChip() deve buscar o badge configurado do modelo selecionado e projetá-lo "
|
||||
"no chip principal da composer."
|
||||
)
|
||||
assert "badgeEl.textContent=badge&&badge.label?badge.label:'';" in js, (
|
||||
"syncModelChip() deve preencher o texto do badge visível no chip principal quando houver metadata configurada."
|
||||
assert "composerModelBadge" not in js, (
|
||||
"syncModelChip() must not reference composerModelBadge — the chip-projected "
|
||||
"badge was removed because it added too much width to the composer chip."
|
||||
)
|
||||
|
||||
@@ -115,6 +115,40 @@ def test_cron_output_snippet_helper(cleanup_test_sessions):
|
||||
src, _ = get_text("/static/panels.js")
|
||||
assert "_cronOutputSnippet" in src
|
||||
|
||||
|
||||
def test_cron_output_window_preserves_response_after_large_prompt(cleanup_test_sessions):
|
||||
"""Large skill dumps before ## Response must not hide the useful output."""
|
||||
from api.routes import _cron_output_content_window
|
||||
|
||||
content = (
|
||||
"Job metadata\n"
|
||||
"## Prompt\n"
|
||||
+ ("skill dump\n" * 1200)
|
||||
+ "user prompt\n"
|
||||
"## Response\n"
|
||||
"actual useful cron result\n"
|
||||
)
|
||||
|
||||
window = _cron_output_content_window(content, limit=8000)
|
||||
|
||||
assert len(window) <= 8000
|
||||
assert "## Response" in window
|
||||
assert "actual useful cron result" in window
|
||||
assert "Job metadata" in window
|
||||
|
||||
|
||||
def test_cron_output_window_without_response_uses_tail(cleanup_test_sessions):
|
||||
"""Without a response marker, keep the newest tail rather than old prompt text."""
|
||||
from api.routes import _cron_output_content_window
|
||||
|
||||
content = "old prompt\n" + ("x" * 9000) + "tail result"
|
||||
|
||||
window = _cron_output_content_window(content, limit=8000)
|
||||
|
||||
assert len(window) == 8000
|
||||
assert window.endswith("tail result")
|
||||
assert "old prompt" not in window
|
||||
|
||||
# ── Tool card polish ───────────────────────────────────────────────────────
|
||||
|
||||
def test_tool_card_running_dot_in_css(cleanup_test_sessions):
|
||||
|
||||
Reference in New Issue
Block a user